首次提交
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# 寸进后端配置(本地开发)
|
||||
PORT=8080
|
||||
|
||||
# 数据库(本地 MySQL,端口 13306)
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=13306
|
||||
DB_USER=root
|
||||
DB_PASS=123456
|
||||
DB_NAME=inchstep
|
||||
|
||||
# JWT 密钥(生产环境需更换为强随机字符串)
|
||||
JWT_SECRET=change-me-in-production
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
|
||||
# 对象存储
|
||||
S3_BUCKET=inchstep-assets
|
||||
@@ -0,0 +1,23 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /inchstep-api ./cmd/api
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /inchstep-api .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./inchstep-api"]
|
||||
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/inchstep/server/internal/config"
|
||||
"github.com/inchstep/server/internal/database"
|
||||
"github.com/inchstep/server/internal/router"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg := config.Load()
|
||||
|
||||
// 初始化数据库
|
||||
db := database.Init(cfg)
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("获取数据库实例失败: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
// 设置路由
|
||||
r := router.Setup(db, cfg)
|
||||
|
||||
// 启动服务
|
||||
addr := ":" + cfg.Port
|
||||
log.Printf("寸进 API 服务启动于 %s", addr)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("服务启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
module github.com/inchstep/server
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.23.0
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -0,0 +1,68 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
Port string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
JWTSecret string
|
||||
RedisURL string
|
||||
S3Bucket string
|
||||
}
|
||||
|
||||
// Load 加载配置(优先读取环境变量,支持 .env 文件)
|
||||
// 查找顺序:可执行文件同目录 → 当前目录 → 环境变量
|
||||
func Load() *Config {
|
||||
// 从可执行文件所在目录加载 .env(宝塔场景)
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
exeDir := filepath.Dir(exe)
|
||||
_ = godotenv.Load(filepath.Join(exeDir, ".env"))
|
||||
}
|
||||
|
||||
// 从当前目录加载 .env(本地开发场景)
|
||||
_ = godotenv.Load()
|
||||
|
||||
c := &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
DBHost: getEnv("DB_HOST", "127.0.0.1"),
|
||||
DBPort: getEnv("DB_PORT", "3306"),
|
||||
DBUser: getEnv("DB_USER", "root"),
|
||||
DBPass: getEnv("DB_PASS", ""),
|
||||
DBName: getEnv("DB_NAME", "inchstep"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "dev-secret-change-in-production"),
|
||||
RedisURL: getEnv("REDIS_URL", "redis://127.0.0.1:6379/0"),
|
||||
S3Bucket: getEnv("S3_BUCKET", ""),
|
||||
}
|
||||
|
||||
log.Println("[寸进] 配置加载完成")
|
||||
log.Printf("[寸进] DB: %s:%s/%s", c.DBHost, c.DBPort, c.DBName)
|
||||
log.Printf("[寸进] JWT: %s...", maskSecret(c.JWTSecret, 8))
|
||||
log.Printf("[寸进] Redis: %s", c.RedisURL)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func maskSecret(s string, keep int) string {
|
||||
if len(s) <= keep {
|
||||
return s
|
||||
}
|
||||
return s[:keep] + "..."
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/inchstep/server/internal/config"
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// Init 初始化数据库连接并执行自动迁移
|
||||
func Init(cfg *config.Config) *gorm.DB {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Asia%%2FShanghai",
|
||||
cfg.DBUser, cfg.DBPass, cfg.DBHost, cfg.DBPort, cfg.DBName)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Warn),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.CheckInItem{},
|
||||
&models.CheckInRecord{},
|
||||
&models.Achievement{},
|
||||
&models.UserAchievement{},
|
||||
&models.Post{},
|
||||
&models.Friend{},
|
||||
&models.Group{},
|
||||
&models.GroupMember{},
|
||||
&models.Admin{},
|
||||
); err != nil {
|
||||
log.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
return db
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
adminSvc *services.AdminService
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewAdminHandler(adminSvc *services.AdminService, jwtSecret string) *AdminHandler {
|
||||
return &AdminHandler{adminSvc: adminSvc, jwtSecret: jwtSecret}
|
||||
}
|
||||
|
||||
type adminLoginReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Login(c *gin.Context) {
|
||||
var req adminLoginReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
admin, err := h.adminSvc.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusUnauthorized, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
token, _ := middleware.GenerateToken(admin.ID, h.jwtSecret)
|
||||
middleware.Success(c, gin.H{
|
||||
"admin": admin,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Dashboard(c *gin.Context) {
|
||||
stats, err := h.adminSvc.GetDashboardStats()
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, stats)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
users, total, err := h.adminSvc.ListUsers(page, pageSize)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"list": users, "total": total, "page": page})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListPosts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
posts, total, err := h.adminSvc.ListPosts(page, pageSize)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"list": posts, "total": total, "page": page})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListAdmins(c *gin.Context) {
|
||||
admins, err := h.adminSvc.ListAdmins()
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, admins)
|
||||
}
|
||||
|
||||
type createAdminReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Role string `json:"role"`
|
||||
Nickname *string `json:"nickname"`
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CreateAdmin(c *gin.Context) {
|
||||
var req createAdminReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = "admin"
|
||||
}
|
||||
admin, err := h.adminSvc.CreateAdmin(req.Username, req.Password, role, req.Nickname)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusConflict, 409, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, admin)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DisableAdmin(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.adminSvc.DisableAdmin(id); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
|
||||
type CheckInHandler struct {
|
||||
checkinSvc *services.CheckInService
|
||||
pointsSvc *services.PointsService
|
||||
}
|
||||
|
||||
func NewCheckInHandler(cs *services.CheckInService, ps *services.PointsService) *CheckInHandler {
|
||||
return &CheckInHandler{checkinSvc: cs, pointsSvc: ps}
|
||||
}
|
||||
|
||||
// ---------- 打卡项 ----------
|
||||
|
||||
func (h *CheckInHandler) CreateItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var item models.CheckInItem
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
if item.Name == "" {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "打卡项名称不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.checkinSvc.CreateItem(userID, &item); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, item)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) ListItems(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
items, err := h.checkinSvc.GetUserItems(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, items)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) GetItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
item, err := h.checkinSvc.GetItemByID(id, userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusNotFound, 404, "打卡项不存在")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, item)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) UpdateItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
item, err := h.checkinSvc.UpdateItem(id, userID, updates)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, item)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) DeleteItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
if err := h.checkinSvc.DeleteItem(id, userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) PauseItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
if err := h.checkinSvc.PauseItem(id, userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) ResumeItem(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
if err := h.checkinSvc.ResumeItem(id, userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
// ---------- 打卡记录 ----------
|
||||
|
||||
type createRecordReq struct {
|
||||
ItemID string `json:"itemId" binding:"required"`
|
||||
CheckDate string `json:"checkDate"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
Images *string `json:"images,omitempty"`
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) CreateRecord(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var req createRecordReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
checkDate := time.Now()
|
||||
if req.CheckDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", req.CheckDate); err == nil {
|
||||
checkDate = parsed
|
||||
}
|
||||
}
|
||||
|
||||
record, err := h.checkinSvc.CreateRecord(userID, req.ItemID, checkDate, req.Note, req.Images)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 发放打卡积分
|
||||
h.pointsSvc.AwardCheckIn(userID)
|
||||
|
||||
middleware.Success(c, record)
|
||||
}
|
||||
|
||||
type makeupRecordReq struct {
|
||||
ItemID string `json:"itemId" binding:"required"`
|
||||
CheckDate string `json:"checkDate" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) MakeupRecord(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var req makeupRecordReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
checkDate, err := time.Parse("2006-01-02", req.CheckDate)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "日期格式错误,请使用 YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
|
||||
record, err := h.checkinSvc.MakeupRecord(userID, req.ItemID, checkDate, 5)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, record)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) GetRecords(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
itemID := c.Query("itemId")
|
||||
|
||||
startStr := c.DefaultQuery("start", time.Now().AddDate(0, -1, 0).Format("2006-01-02"))
|
||||
endStr := c.DefaultQuery("end", time.Now().Format("2006-01-02"))
|
||||
|
||||
start, _ := time.Parse("2006-01-02", startStr)
|
||||
end, _ := time.Parse("2006-01-02", endStr)
|
||||
end = end.Add(24 * time.Hour)
|
||||
|
||||
records, err := h.checkinSvc.GetRecords(userID, itemID, start, end)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, records)
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) GetStreak(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
itemID := c.Param("id")
|
||||
streak, err := h.checkinSvc.GetStreak(itemID, userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"itemId": itemID, "streak": streak})
|
||||
}
|
||||
|
||||
func (h *CheckInHandler) GetMonthlyStats(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
yearStr := c.DefaultQuery("year", strconv.Itoa(time.Now().Year()))
|
||||
monthStr := c.DefaultQuery("month", strconv.Itoa(int(time.Now().Month())))
|
||||
|
||||
year, _ := strconv.Atoi(yearStr)
|
||||
month, _ := strconv.Atoi(monthStr)
|
||||
|
||||
stats, err := h.checkinSvc.GetMonthlyStats(userID, year, month)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, stats)
|
||||
}
|
||||
|
||||
|
||||
func (h *CheckInHandler) GetTodayStatus(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
status, err := h.checkinSvc.GetTodayStatus(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, status)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
|
||||
type SocialHandler struct {
|
||||
socialSvc *services.SocialService
|
||||
}
|
||||
|
||||
func NewSocialHandler(svc *services.SocialService) *SocialHandler {
|
||||
return &SocialHandler{socialSvc: svc}
|
||||
}
|
||||
|
||||
// ---------- 好友 ----------
|
||||
|
||||
type friendReq struct {
|
||||
FriendID string `json:"friendId" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *SocialHandler) SendFriendRequest(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var req friendReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.socialSvc.SendFriendRequest(userID, req.FriendID); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) AcceptFriendRequest(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var req friendReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.socialSvc.AcceptFriendRequest(userID, req.FriendID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) RemoveFriend(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
friendID := c.Param("userId")
|
||||
if err := h.socialSvc.RemoveFriend(userID, friendID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) ListFriends(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
friends, err := h.socialSvc.GetFriends(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, friends)
|
||||
}
|
||||
|
||||
// ---------- 动态 ----------
|
||||
|
||||
type createPostReq struct {
|
||||
Content *string `json:"content,omitempty"`
|
||||
Images *string `json:"images,omitempty"`
|
||||
Privacy int `json:"privacy"`
|
||||
}
|
||||
|
||||
func (h *SocialHandler) CreatePost(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var req createPostReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
post := &models.Post{
|
||||
UserID: userID,
|
||||
Content: req.Content,
|
||||
Images: req.Images,
|
||||
Privacy: req.Privacy,
|
||||
}
|
||||
if err := h.socialSvc.CreatePost(post); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, post)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetFeed(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
posts, total, err := h.socialSvc.GetFeed(page, pageSize)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{
|
||||
"list": posts,
|
||||
"total": total,
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) LikePost(c *gin.Context) {
|
||||
postID := c.Param("id")
|
||||
if err := h.socialSvc.LikePost(postID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) CommentPost(c *gin.Context) {
|
||||
postID := c.Param("id")
|
||||
if err := h.socialSvc.CommentPost(postID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
// ---------- 小组 ----------
|
||||
|
||||
func (h *SocialHandler) CreateGroup(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var group models.Group
|
||||
if err := c.ShouldBindJSON(&group); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.socialSvc.CreateGroup(userID, &group); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, group)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) JoinGroup(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
groupID := c.Param("id")
|
||||
if err := h.socialSvc.JoinGroup(groupID, userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) LeaveGroup(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
groupID := c.Param("id")
|
||||
if err := h.socialSvc.LeaveGroup(groupID, userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetGroupMembers(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
members, err := h.socialSvc.GetGroupMembers(groupID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, members)
|
||||
}
|
||||
|
||||
// ---------- 成就 ----------
|
||||
|
||||
type AchievementHandler struct {
|
||||
pointsSvc *services.PointsService
|
||||
}
|
||||
|
||||
func NewAchievementHandler(ps *services.PointsService) *AchievementHandler {
|
||||
return &AchievementHandler{pointsSvc: ps}
|
||||
}
|
||||
|
||||
func (h *AchievementHandler) ListAll(c *gin.Context) {
|
||||
list, err := h.pointsSvc.GetAllAchievements()
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, list)
|
||||
}
|
||||
|
||||
func (h *AchievementHandler) ListUserAchievements(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
list, err := h.pointsSvc.GetUserAchievements(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, list)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userSvc *services.UserService
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewUserHandler(userSvc *services.UserService, jwtSecret string) *UserHandler {
|
||||
return &UserHandler{userSvc: userSvc, jwtSecret: jwtSecret}
|
||||
}
|
||||
|
||||
type registerReq struct {
|
||||
Phone string `json:"phone" binding:"required,len=11"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req registerReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "手机号格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userSvc.RegisterByPhone(req.Phone)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusConflict, 409, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
token, _ := middleware.GenerateToken(user.ID, h.jwtSecret)
|
||||
middleware.Success(c, gin.H{
|
||||
"user": user,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
type loginReq struct {
|
||||
Phone string `json:"phone" binding:"required,len=11"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req loginReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "手机号格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userSvc.GetByPhone(req.Phone)
|
||||
if err != nil {
|
||||
// 手机号未注册时自动注册
|
||||
user, err = h.userSvc.RegisterByPhone(req.Phone)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, "注册失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, _ := middleware.GenerateToken(user.ID, h.jwtSecret)
|
||||
middleware.Success(c, gin.H{
|
||||
"user": user,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
user, err := h.userSvc.GetByID(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusNotFound, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
middleware.Error(c, http.StatusBadRequest, 400, "请求数据格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userSvc.UpdateProfile(userID, updates)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, err.Error())
|
||||
return
|
||||
}
|
||||
middleware.Success(c, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) DeleteAccount(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
if err := h.userSvc.DeleteAccount(userID); err != nil {
|
||||
middleware.Error(c, http.StatusInternalServerError, 500, "注销失败")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetPoints(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
user, err := h.userSvc.GetByID(userID)
|
||||
if err != nil {
|
||||
middleware.Error(c, http.StatusNotFound, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
middleware.Success(c, gin.H{"points": user.Points})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"userId"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成 JWT Token(7 天有效期)
|
||||
func GenerateToken(userID, secret string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
// AuthRequired JWT 认证中间件(默认所有路由需要认证)
|
||||
func AuthRequired(secret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "缺少认证令牌",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenStr == authHeader {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "令牌格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "令牌无效或已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserID 从上下文中获取当前用户 ID
|
||||
func GetUserID(c *gin.Context) string {
|
||||
uid, _ := c.Get("userID")
|
||||
return uid.(string)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 跨域配置
|
||||
func CORS() gin.HandlerFunc {
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorHandler 统一错误处理中间件
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
if len(c.Errors) > 0 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": c.Errors.Last().Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApiResponse 统一响应格式
|
||||
type ApiResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Error(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.AbortWithStatusJSON(httpStatus, ApiResponse{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Achievement 成就徽章
|
||||
type Achievement struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(50)" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
Description *string `gorm:"type:varchar(200)" json:"description,omitempty"`
|
||||
Icon *string `gorm:"type:varchar(50)" json:"icon,omitempty"`
|
||||
Rarity string `gorm:"default:bronze;type:varchar(20)" json:"rarity"`
|
||||
Condition *string `gorm:"type:varchar(200)" json:"condition,omitempty"`
|
||||
|
||||
Users []UserAchievement `gorm:"foreignKey:AchievementID" json:"-"`
|
||||
}
|
||||
|
||||
func (Achievement) TableName() string { return "achievements" }
|
||||
|
||||
// UserAchievement 用户成就关联
|
||||
type UserAchievement struct {
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;primaryKey"`
|
||||
AchievementID string `gorm:"column:achievement_id;type:varchar(50);not null;primaryKey"`
|
||||
UnlockedAt time.Time `gorm:"column:unlocked_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Achievement Achievement `gorm:"foreignKey:AchievementID" json:"-"`
|
||||
}
|
||||
|
||||
func (UserAchievement) TableName() string { return "user_achievements" }
|
||||
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// AdminRole 管理员角色
|
||||
type AdminRole string
|
||||
|
||||
const (
|
||||
RoleSuperAdmin AdminRole = "super_admin"
|
||||
RoleAdmin AdminRole = "admin"
|
||||
)
|
||||
|
||||
// AdminStatus 管理员状态
|
||||
type AdminStatus string
|
||||
|
||||
const (
|
||||
AdminStatusActive AdminStatus = "active"
|
||||
AdminStatusDisabled AdminStatus = "disabled"
|
||||
)
|
||||
|
||||
// Admin 管理员模型
|
||||
type Admin struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;type:varchar(50);not null" json:"username"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"`
|
||||
Nickname *string `gorm:"type:varchar(50)" json:"nickname,omitempty"`
|
||||
Role AdminRole `gorm:"default:admin;type:varchar(20)" json:"role"`
|
||||
Status AdminStatus `gorm:"default:active;type:varchar(20)" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Admin) TableName() string { return "admins" }
|
||||
@@ -0,0 +1,78 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// CheckInCategory 打卡分类
|
||||
type CheckInCategory string
|
||||
|
||||
const (
|
||||
CategoryStudy CheckInCategory = "study"
|
||||
CategoryHealth CheckInCategory = "health"
|
||||
CategoryHabit CheckInCategory = "habit"
|
||||
CategoryHobby CheckInCategory = "hobby"
|
||||
CategoryGoal CheckInCategory = "goal"
|
||||
)
|
||||
|
||||
// CycleType 打卡周期
|
||||
type CycleType string
|
||||
|
||||
const (
|
||||
CycleDaily CycleType = "daily"
|
||||
CycleWeekly CycleType = "weekly"
|
||||
CycleEveryOther CycleType = "every_other_day"
|
||||
CycleCustom CycleType = "custom"
|
||||
)
|
||||
|
||||
// ItemStatus 打卡项状态
|
||||
type ItemStatus string
|
||||
|
||||
const (
|
||||
StatusActive ItemStatus = "active"
|
||||
StatusPaused ItemStatus = "paused"
|
||||
StatusArchived ItemStatus = "archived"
|
||||
StatusDeleted ItemStatus = "deleted"
|
||||
)
|
||||
|
||||
// RecordStatus 打卡记录状态
|
||||
type RecordStatus string
|
||||
|
||||
const (
|
||||
RecordNormal RecordStatus = "normal"
|
||||
RecordMakeup RecordStatus = "makeup"
|
||||
)
|
||||
|
||||
// CheckInItem 打卡项
|
||||
type CheckInItem struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;index" json:"userId"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
Icon *string `gorm:"type:varchar(50)" json:"icon,omitempty"`
|
||||
Category CheckInCategory `gorm:"type:varchar(20);not null" json:"category"`
|
||||
DailyGoal *string `gorm:"column:daily_goal;type:varchar(200)" json:"dailyGoal,omitempty"`
|
||||
CycleType CycleType `gorm:"column:cycle_type;default:daily;type:varchar(20)" json:"cycleType"`
|
||||
Privacy PrivacyLevel `gorm:"default:2" json:"privacy"`
|
||||
Status ItemStatus `gorm:"default:active;type:varchar(20)" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Records []CheckInRecord `gorm:"foreignKey:ItemID" json:"-"`
|
||||
}
|
||||
|
||||
func (CheckInItem) TableName() string { return "checkin_items" }
|
||||
|
||||
// CheckInRecord 打卡记录
|
||||
type CheckInRecord struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
ItemID string `gorm:"column:item_id;type:varchar(32);not null;index" json:"itemId"`
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;index" json:"userId"`
|
||||
CheckDate time.Time `gorm:"column:check_date;not null;index" json:"checkDate"`
|
||||
Status RecordStatus `gorm:"default:normal;type:varchar(20)" json:"status"`
|
||||
Note *string `gorm:"type:varchar(500)" json:"note,omitempty"`
|
||||
Images *string `gorm:"type:text" json:"images,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
|
||||
Item CheckInItem `gorm:"foreignKey:ItemID" json:"-"`
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
}
|
||||
|
||||
func (CheckInRecord) TableName() string { return "checkin_records" }
|
||||
@@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Post 动态
|
||||
type Post struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;index" json:"userId"`
|
||||
Content *string `gorm:"type:text" json:"content,omitempty"`
|
||||
Images *string `gorm:"type:text" json:"images,omitempty"`
|
||||
Privacy int `gorm:"default:1" json:"privacy"`
|
||||
Likes int `gorm:"default:0" json:"likes"`
|
||||
Comments int `gorm:"default:0" json:"comments"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (Post) TableName() string { return "posts" }
|
||||
|
||||
// Friend 好友关系
|
||||
type Friend struct {
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;primaryKey"`
|
||||
FriendID string `gorm:"column:friend_id;type:varchar(32);not null;primaryKey"`
|
||||
Status string `gorm:"default:pending;type:varchar(20)" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (Friend) TableName() string { return "friends" }
|
||||
|
||||
// Group 打卡小组
|
||||
type Group struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
Name string `gorm:"type:varchar(20);not null" json:"name"`
|
||||
Cover *string `gorm:"type:varchar(255)" json:"cover,omitempty"`
|
||||
Intro *string `gorm:"type:varchar(200)" json:"intro,omitempty"`
|
||||
Type string `gorm:"default:public;type:varchar(20)" json:"type"`
|
||||
MaxMember int `gorm:"column:max_member;default:50" json:"maxMember"`
|
||||
CreatedBy string `gorm:"column:created_by;type:varchar(32)" json:"createdBy"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
|
||||
Members []GroupMember `gorm:"foreignKey:GroupID" json:"-"`
|
||||
}
|
||||
|
||||
func (Group) TableName() string { return "groups" }
|
||||
|
||||
// GroupMember 小组成员
|
||||
type GroupMember struct {
|
||||
GroupID string `gorm:"column:group_id;type:varchar(32);not null;primaryKey"`
|
||||
UserID string `gorm:"column:user_id;type:varchar(32);not null;primaryKey"`
|
||||
Role string `gorm:"default:member;type:varchar(20)" json:"role"`
|
||||
JoinedAt time.Time `gorm:"column:joined_at"`
|
||||
|
||||
Group Group `gorm:"foreignKey:GroupID" json:"-"`
|
||||
}
|
||||
|
||||
func (GroupMember) TableName() string { return "group_members" }
|
||||
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// PrivacyLevel 隐私级别
|
||||
type PrivacyLevel int
|
||||
|
||||
const (
|
||||
PrivacyPublic PrivacyLevel = 1 // 公开
|
||||
PrivacyFriendsOnly PrivacyLevel = 2 // 仅好友可见
|
||||
PrivacyPrivate PrivacyLevel = 3 // 仅自己可见
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
|
||||
Phone *string `gorm:"uniqueIndex;type:varchar(20)" json:"phone,omitempty"`
|
||||
Nickname *string `gorm:"type:varchar(50)" json:"nickname,omitempty"`
|
||||
Avatar *string `gorm:"type:varchar(255)" json:"avatar,omitempty"`
|
||||
Bio *string `gorm:"type:varchar(100)" json:"bio,omitempty"`
|
||||
Motto *string `gorm:"type:varchar(50)" json:"motto,omitempty"`
|
||||
Privacy PrivacyLevel `gorm:"default:2" json:"privacy"`
|
||||
Points int `gorm:"default:0" json:"points"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
|
||||
// 关联
|
||||
CheckInItems []CheckInItem `gorm:"foreignKey:UserID" json:"-"`
|
||||
Records []CheckInRecord `gorm:"foreignKey:UserID" json:"-"`
|
||||
Posts []Post `gorm:"foreignKey:UserID" json:"-"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "users" }
|
||||
@@ -0,0 +1,124 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/inchstep/server/internal/config"
|
||||
"github.com/inchstep/server/internal/handlers"
|
||||
"github.com/inchstep/server/internal/middleware"
|
||||
"github.com/inchstep/server/internal/services"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Setup 配置所有路由
|
||||
func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
r.Use(middleware.CORS())
|
||||
r.Use(middleware.ErrorHandler())
|
||||
|
||||
userSvc := services.NewUserService(db)
|
||||
checkinSvc := services.NewCheckInService(db)
|
||||
pointsSvc := services.NewPointsService(db)
|
||||
socialSvc := services.NewSocialService(db)
|
||||
adminSvc := services.NewAdminService(db, userSvc)
|
||||
|
||||
pointsSvc.InitAchievements()
|
||||
adminSvc.SeedAdmin()
|
||||
|
||||
userH := handlers.NewUserHandler(userSvc, cfg.JWTSecret)
|
||||
checkinH := handlers.NewCheckInHandler(checkinSvc, pointsSvc)
|
||||
socialH := handlers.NewSocialHandler(socialSvc)
|
||||
achievementH := handlers.NewAchievementHandler(pointsSvc)
|
||||
adminH := handlers.NewAdminHandler(adminSvc, cfg.JWTSecret)
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 公开路由
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", userH.Register)
|
||||
auth.POST("/login", userH.Login)
|
||||
}
|
||||
|
||||
// 管理员登录(公开)
|
||||
admin := api.Group("/admin")
|
||||
{
|
||||
admin.POST("/auth/login", adminH.Login)
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
authed := api.Group("")
|
||||
authed.Use(middleware.AuthRequired(cfg.JWTSecret))
|
||||
{
|
||||
// 用户模块
|
||||
user := authed.Group("/users")
|
||||
{
|
||||
user.GET("/profile", userH.GetProfile)
|
||||
user.PUT("/profile", userH.UpdateProfile)
|
||||
user.DELETE("/account", userH.DeleteAccount)
|
||||
user.GET("/points", userH.GetPoints)
|
||||
}
|
||||
|
||||
// 打卡模块
|
||||
checkin := authed.Group("/checkin")
|
||||
{
|
||||
checkin.POST("/items", checkinH.CreateItem)
|
||||
checkin.GET("/items", checkinH.ListItems)
|
||||
checkin.GET("/items/:id", checkinH.GetItem)
|
||||
checkin.PUT("/items/:id", checkinH.UpdateItem)
|
||||
checkin.DELETE("/items/:id", checkinH.DeleteItem)
|
||||
checkin.POST("/items/:id/pause", checkinH.PauseItem)
|
||||
checkin.POST("/items/:id/resume", checkinH.ResumeItem)
|
||||
|
||||
checkin.POST("/records", checkinH.CreateRecord)
|
||||
checkin.GET("/records", checkinH.GetRecords)
|
||||
checkin.POST("/records/makeup", checkinH.MakeupRecord)
|
||||
|
||||
checkin.GET("/streak/:id", checkinH.GetStreak)
|
||||
checkin.GET("/stats/monthly", checkinH.GetMonthlyStats)
|
||||
checkin.GET("/today-status", checkinH.GetTodayStatus)
|
||||
}
|
||||
|
||||
// 社交模块
|
||||
social := authed.Group("/social")
|
||||
{
|
||||
social.POST("/friends/request", socialH.SendFriendRequest)
|
||||
social.POST("/friends/accept", socialH.AcceptFriendRequest)
|
||||
social.DELETE("/friends/:userId", socialH.RemoveFriend)
|
||||
social.GET("/friends", socialH.ListFriends)
|
||||
|
||||
social.GET("/feed", socialH.GetFeed)
|
||||
social.POST("/posts", socialH.CreatePost)
|
||||
social.POST("/posts/:id/like", socialH.LikePost)
|
||||
social.POST("/posts/:id/comment", socialH.CommentPost)
|
||||
|
||||
social.POST("/groups", socialH.CreateGroup)
|
||||
social.POST("/groups/:id/join", socialH.JoinGroup)
|
||||
social.POST("/groups/:id/leave", socialH.LeaveGroup)
|
||||
social.GET("/groups/:id/members", socialH.GetGroupMembers)
|
||||
}
|
||||
|
||||
// 成就模块
|
||||
authed.GET("/achievements", achievementH.ListAll)
|
||||
authed.GET("/achievements/mine", achievementH.ListUserAchievements)
|
||||
|
||||
// 管理后台 API(需额外管理员认证)
|
||||
adminAuthed := api.Group("/admin")
|
||||
adminAuthed.Use(middleware.AuthRequired(cfg.JWTSecret))
|
||||
{
|
||||
adminAuthed.GET("/dashboard", adminH.Dashboard)
|
||||
adminAuthed.GET("/users", adminH.ListUsers)
|
||||
adminAuthed.GET("/posts", adminH.ListPosts)
|
||||
adminAuthed.GET("/admins", adminH.ListAdmins)
|
||||
adminAuthed.POST("/admins", adminH.CreateAdmin)
|
||||
adminAuthed.DELETE("/admins/:id", adminH.DisableAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AdminService struct {
|
||||
db *gorm.DB
|
||||
userSvc *UserService
|
||||
}
|
||||
|
||||
func NewAdminService(db *gorm.DB, userSvc *UserService) *AdminService {
|
||||
return &AdminService{db: db, userSvc: userSvc}
|
||||
}
|
||||
|
||||
// SeedAdmin 初始化超级管理员账号
|
||||
func (s *AdminService) SeedAdmin() error {
|
||||
var count int64
|
||||
s.db.Model(&models.Admin{}).Count(&count)
|
||||
if count > 0 {
|
||||
return nil // 已有管理员,跳过
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("admin123456"), 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nick := "超级管理员"
|
||||
admin := &models.Admin{
|
||||
ID: generateID(),
|
||||
Username: "admin",
|
||||
PasswordHash: string(hash),
|
||||
Nickname: &nick,
|
||||
Role: models.RoleSuperAdmin,
|
||||
Status: models.AdminStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
return s.db.Create(admin).Error
|
||||
}
|
||||
|
||||
// Login 管理员登录
|
||||
func (s *AdminService) Login(username, password string) (*models.Admin, error) {
|
||||
var admin models.Admin
|
||||
if err := s.db.Where("username = ?", username).First(&admin).Error; err != nil {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
if admin.Status != models.AdminStatusActive {
|
||||
return nil, errors.New("账号已被禁用")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
// GetDashboardStats 获取管理后台统计数据
|
||||
func (s *AdminService) GetDashboardStats() (map[string]interface{}, error) {
|
||||
var totalUsers, todayUsers int64
|
||||
s.db.Model(&models.User{}).Count(&totalUsers)
|
||||
todayStart := time.Now().Truncate(24 * time.Hour)
|
||||
s.db.Model(&models.User{}).Where("created_at >= ?", todayStart).Count(&todayUsers)
|
||||
|
||||
var totalPosts, totalItems, totalRecords int64
|
||||
s.db.Model(&models.Post{}).Where("privacy = ?", 1).Count(&totalPosts)
|
||||
s.db.Model(&models.CheckInItem{}).Count(&totalItems)
|
||||
s.db.Model(&models.CheckInRecord{}).Count(&totalRecords)
|
||||
|
||||
return map[string]interface{}{
|
||||
"totalUsers": totalUsers,
|
||||
"totalPosts": totalPosts,
|
||||
"totalItems": totalItems,
|
||||
"totalRecords": totalRecords,
|
||||
"newUsersToday": todayUsers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListUsers 获取用户列表(分页)
|
||||
func (s *AdminService) ListUsers(page, pageSize int) ([]models.User, int64, error) {
|
||||
var total int64
|
||||
s.db.Model(&models.User{}).Count(&total)
|
||||
var users []models.User
|
||||
err := s.db.Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// ListPosts 获取动态列表
|
||||
func (s *AdminService) ListPosts(page, pageSize int) ([]models.Post, int64, error) {
|
||||
var total int64
|
||||
s.db.Model(&models.Post{}).Count(&total)
|
||||
var posts []models.Post
|
||||
err := s.db.Preload("User").Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&posts).Error
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// ListAdmins 获取管理员列表
|
||||
func (s *AdminService) ListAdmins() ([]models.Admin, error) {
|
||||
var admins []models.Admin
|
||||
err := s.db.Find(&admins).Error
|
||||
return admins, err
|
||||
}
|
||||
|
||||
// CreateAdmin 创建管理员
|
||||
func (s *AdminService) CreateAdmin(username, password, role string, nickname *string) (*models.Admin, error) {
|
||||
var existing models.Admin
|
||||
if err := s.db.Where("username = ?", username).First(&existing).Error; err == nil {
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
admin := &models.Admin{
|
||||
ID: generateID(),
|
||||
Username: username,
|
||||
PasswordHash: string(hash),
|
||||
Nickname: nickname,
|
||||
Role: models.AdminRole(role),
|
||||
Status: models.AdminStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
return admin, s.db.Create(admin).Error
|
||||
}
|
||||
|
||||
// DisableAdmin 禁用管理员
|
||||
func (s *AdminService) DisableAdmin(id string) error {
|
||||
return s.db.Model(&models.Admin{}).Where("id = ?", id).Update("status", models.AdminStatusDisabled).Error
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CheckInService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCheckInService(db *gorm.DB) *CheckInService {
|
||||
return &CheckInService{db: db}
|
||||
}
|
||||
|
||||
// CreateItem 创建打卡项
|
||||
func (s *CheckInService) CreateItem(userID string, item *models.CheckInItem) error {
|
||||
item.ID = generateID()
|
||||
item.UserID = userID
|
||||
item.Status = models.StatusActive
|
||||
item.CreatedAt = time.Now()
|
||||
return s.db.Create(item).Error
|
||||
}
|
||||
|
||||
// GetUserItems 获取用户打卡项列表
|
||||
func (s *CheckInService) GetUserItems(userID string) ([]models.CheckInItem, error) {
|
||||
var items []models.CheckInItem
|
||||
err := s.db.Where("user_id = ? AND status != ?", userID, models.StatusDeleted).
|
||||
Order("created_at desc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// GetItemByID 获取打卡项详情
|
||||
func (s *CheckInService) GetItemByID(id, userID string) (*models.CheckInItem, error) {
|
||||
var item models.CheckInItem
|
||||
err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&item).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// UpdateItem 更新打卡项
|
||||
func (s *CheckInService) UpdateItem(id, userID string, updates map[string]interface{}) (*models.CheckInItem, error) {
|
||||
allowed := map[string]bool{"name": true, "icon": true, "category": true, "daily_goal": true,
|
||||
"cycle_type": true, "privacy": true}
|
||||
filtered := make(map[string]interface{})
|
||||
for k, v := range updates {
|
||||
if allowed[k] {
|
||||
filtered[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.CheckInItem{}).Where("id = ? AND user_id = ?", id, userID).Updates(filtered)
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, errors.New("打卡项不存在或无权限")
|
||||
}
|
||||
return s.GetItemByID(id, userID)
|
||||
}
|
||||
|
||||
// DeleteItem 软删除打卡项
|
||||
func (s *CheckInService) DeleteItem(id, userID string) error {
|
||||
return s.db.Model(&models.CheckInItem{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Update("status", models.StatusDeleted).Error
|
||||
}
|
||||
|
||||
// PauseItem 暂停打卡项
|
||||
func (s *CheckInService) PauseItem(id, userID string) error {
|
||||
return s.db.Model(&models.CheckInItem{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Update("status", models.StatusPaused).Error
|
||||
}
|
||||
|
||||
// ResumeItem 恢复打卡项
|
||||
func (s *CheckInService) ResumeItem(id, userID string) error {
|
||||
return s.db.Model(&models.CheckInItem{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Update("status", models.StatusActive).Error
|
||||
}
|
||||
|
||||
// CreateRecord 创建打卡记录
|
||||
func (s *CheckInService) CreateRecord(userID, itemID string, checkDate time.Time, note, images *string) (*models.CheckInRecord, error) {
|
||||
// 验证打卡项
|
||||
item, err := s.GetItemByID(itemID, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("打卡项不存在或无权限")
|
||||
}
|
||||
if item.Status != models.StatusActive {
|
||||
return nil, errors.New("打卡项当前不可用(已暂停或归档)")
|
||||
}
|
||||
|
||||
// 检查当日是否已打卡
|
||||
startOfDay := time.Date(checkDate.Year(), checkDate.Month(), checkDate.Day(), 0, 0, 0, 0, checkDate.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
var count int64
|
||||
s.db.Model(&models.CheckInRecord{}).
|
||||
Where("item_id = ? AND user_id = ? AND check_date >= ? AND check_date < ?",
|
||||
itemID, userID, startOfDay, endOfDay).Count(&count)
|
||||
if count > 0 {
|
||||
return nil, errors.New("今日已打卡")
|
||||
}
|
||||
|
||||
record := &models.CheckInRecord{
|
||||
ID: generateID(),
|
||||
ItemID: itemID,
|
||||
UserID: userID,
|
||||
CheckDate: checkDate,
|
||||
Status: models.RecordNormal,
|
||||
Note: note,
|
||||
Images: images,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := s.db.Create(record).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// MakeupRecord 补卡(7 天内,每月 3 次免费,超出扣积分)
|
||||
func (s *CheckInService) MakeupRecord(userID, itemID string, checkDate time.Time, costPoints int) (*models.CheckInRecord, error) {
|
||||
now := time.Now()
|
||||
diff := now.Sub(checkDate)
|
||||
if diff.Hours() > 7*24 || diff.Hours() < 0 {
|
||||
return nil, errors.New("只能补签最近 7 天的记录")
|
||||
}
|
||||
|
||||
// 检查本月已用补卡次数
|
||||
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
var makeupCount int64
|
||||
s.db.Model(&models.CheckInRecord{}).
|
||||
Where("user_id = ? AND status = ? AND created_at >= ?",
|
||||
userID, models.RecordMakeup, currentMonthStart).Count(&makeupCount)
|
||||
|
||||
// 计算需要消耗的积分
|
||||
freeCount := int64(3)
|
||||
if makeupCount >= freeCount {
|
||||
var user models.User
|
||||
if err := s.db.First(&user, "id = ?", userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Points < costPoints {
|
||||
return nil, errors.New("积分不足,补卡需要 5 积分")
|
||||
}
|
||||
s.db.Model(&user).Update("points", user.Points-costPoints)
|
||||
}
|
||||
|
||||
record := &models.CheckInRecord{
|
||||
ID: generateID(),
|
||||
ItemID: itemID,
|
||||
UserID: userID,
|
||||
CheckDate: checkDate,
|
||||
Status: models.RecordMakeup,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
return record, s.db.Create(record).Error
|
||||
}
|
||||
|
||||
// GetRecords 获取打卡记录列表
|
||||
func (s *CheckInService) GetRecords(userID, itemID string, start, end time.Time) ([]models.CheckInRecord, error) {
|
||||
var records []models.CheckInRecord
|
||||
query := s.db.Where("user_id = ?", userID)
|
||||
if itemID != "" {
|
||||
query = query.Where("item_id = ?", itemID)
|
||||
}
|
||||
err := query.Where("check_date >= ? AND check_date < ?", start, end).
|
||||
Order("check_date desc").Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
// GetStreak 计算连续打卡天数(时区感知)
|
||||
func (s *CheckInService) GetStreak(itemID, userID string) (int, error) {
|
||||
var records []models.CheckInRecord
|
||||
if err := s.db.Where("item_id = ? AND user_id = ?", itemID, userID).
|
||||
Order("check_date desc").Find(&records).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
streak := 0
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
for i, r := range records {
|
||||
recordDate := r.CheckDate.Truncate(24 * time.Hour)
|
||||
if i == 0 {
|
||||
if recordDate.Before(today.Add(-48 * time.Hour)) {
|
||||
return 0, nil // 最近一次打卡超过 2 天前,连续已断
|
||||
}
|
||||
streak = 1
|
||||
continue
|
||||
}
|
||||
|
||||
prevDate := records[i-1].CheckDate.Truncate(24 * time.Hour)
|
||||
if prevDate.Sub(recordDate).Hours() == 24 {
|
||||
streak++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return streak, nil
|
||||
}
|
||||
|
||||
// GetMonthlyStats 获取月度统计
|
||||
func (s *CheckInService) GetMonthlyStats(userID string, year, month int) (map[string]interface{}, error) {
|
||||
startOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local)
|
||||
endOfMonth := startOfMonth.AddDate(0, 1, 0)
|
||||
|
||||
var totalItems int64
|
||||
s.db.Model(&models.CheckInItem{}).Where("user_id = ? AND status = ?", userID, models.StatusActive).Count(&totalItems)
|
||||
|
||||
var totalRecords int64
|
||||
s.db.Model(&models.CheckInRecord{}).
|
||||
Where("user_id = ? AND check_date >= ? AND check_date < ?", userID, startOfMonth, endOfMonth).
|
||||
Count(&totalRecords)
|
||||
|
||||
// 按分类统计
|
||||
type CategoryCount struct {
|
||||
Category string
|
||||
Count int64
|
||||
}
|
||||
var catStats []CategoryCount
|
||||
s.db.Model(&models.CheckInItem{}).
|
||||
Select("category, count(*) as count").
|
||||
Where("user_id = ? AND status = ?", userID, models.StatusActive).
|
||||
Group("category").Find(&catStats)
|
||||
|
||||
return map[string]interface{}{
|
||||
"totalItems": totalItems,
|
||||
"totalRecords": totalRecords,
|
||||
"byCategory": catStats,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
// GetTodayStatus 获取用户今天已打卡的打卡项 ID 列表
|
||||
func (s *CheckInService) GetTodayStatus(userID string) (map[string]bool, error) {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
endOfToday := today.Add(24 * time.Hour)
|
||||
|
||||
var records []models.CheckInRecord
|
||||
if err := s.db.Where("user_id = ? AND check_date >= ? AND check_date < ?",
|
||||
userID, today, endOfToday).Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]bool)
|
||||
for _, r := range records {
|
||||
result[r.ItemID] = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PointsService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPointsService(db *gorm.DB) *PointsService {
|
||||
return &PointsService{db: db}
|
||||
}
|
||||
|
||||
const (
|
||||
PointsCheckIn = 1
|
||||
PointsStreak7 = 5
|
||||
PointsStreak30 = 20
|
||||
PointsGoalComplete = 10
|
||||
PointsPostShare = 2
|
||||
PointsComment = 1
|
||||
PointsInviteUser = 15
|
||||
)
|
||||
|
||||
// AddPoints 为用户增加积分
|
||||
func (s *PointsService) AddPoints(userID string, amount int, reason string) error {
|
||||
return s.db.Model(&models.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("points", gorm.Expr("points + ?", amount)).Error
|
||||
}
|
||||
|
||||
// CheckAndAwardStreak 检查并发放连续打卡奖励
|
||||
func (s *PointsService) CheckAndAwardStreak(userID string, streak int) error {
|
||||
switch streak {
|
||||
case 7:
|
||||
return s.AddPoints(userID, PointsStreak7, "连续打卡 7 天")
|
||||
case 30:
|
||||
return s.AddPoints(userID, PointsStreak30, "连续打卡 30 天")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AwardCheckIn 打卡奖励
|
||||
func (s *PointsService) AwardCheckIn(userID string) error {
|
||||
return s.AddPoints(userID, PointsCheckIn, "每日打卡")
|
||||
}
|
||||
|
||||
func ptr(s string) *string { return &s }
|
||||
|
||||
// predefinedAchievements 预定义成就列表
|
||||
var predefinedAchievements = []models.Achievement{
|
||||
{ID: "first_checkin", Name: "初露锋芒", Rarity: "bronze", Condition: ptr("完成第 1 次打卡")},
|
||||
{ID: "streak_7", Name: "七日坚持", Rarity: "bronze", Condition: ptr("连续 7 天打卡")},
|
||||
{ID: "streak_15", Name: "半月之约", Rarity: "bronze", Condition: ptr("连续 15 天打卡")},
|
||||
{ID: "streak_30", Name: "月满则盈", Rarity: "silver", Condition: ptr("连续 30 天打卡")},
|
||||
{ID: "streak_100", Name: "百日筑基", Rarity: "gold", Condition: ptr("连续 100 天打卡")},
|
||||
{ID: "all_round", Name: "全面开花", Rarity: "silver", Condition: ptr("同时有 5 个打卡项保持活跃")},
|
||||
{ID: "social_star", Name: "社交达人", Rarity: "silver", Condition: ptr("获得 50 个点赞")},
|
||||
{ID: "group_star", Name: "小组之星", Rarity: "silver", Condition: ptr("在小组内连续打卡满 30 天")},
|
||||
{ID: "makeup_master", Name: "神补刀", Rarity: "bronze", Condition: ptr("使用补卡功能 10 次")},
|
||||
{ID: "year_champion", Name: "年度坚持王", Rarity: "gold", Condition: ptr("全年打卡完成率 ≥ 80%")},
|
||||
}
|
||||
|
||||
// InitAchievements 初始化成就数据
|
||||
func (s *PointsService) InitAchievements() error {
|
||||
for _, a := range predefinedAchievements {
|
||||
s.db.Where("id = ?", a.ID).FirstOrCreate(&a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlockAchievement 解锁成就
|
||||
func (s *PointsService) UnlockAchievement(userID, achievementID string) (*models.UserAchievement, error) {
|
||||
ua := &models.UserAchievement{
|
||||
UserID: userID,
|
||||
AchievementID: achievementID,
|
||||
UnlockedAt: time.Now(),
|
||||
}
|
||||
if err := s.db.Create(ua).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ua, nil
|
||||
}
|
||||
|
||||
// GetUserAchievements 获取用户已解锁的成就
|
||||
func (s *PointsService) GetUserAchievements(userID string) ([]models.UserAchievement, error) {
|
||||
var list []models.UserAchievement
|
||||
err := s.db.Where("user_id = ?", userID).Preload("Achievement").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// GetAllAchievements 获取所有成就
|
||||
func (s *PointsService) GetAllAchievements() ([]models.Achievement, error) {
|
||||
var list []models.Achievement
|
||||
err := s.db.Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// DeductPoints 扣除积分
|
||||
func (s *PointsService) DeductPoints(userID string, amount int) error {
|
||||
var user models.User
|
||||
if err := s.db.First(&user, "id = ?", userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Points < amount {
|
||||
return gorm.ErrRecordNotFound // 积分不足
|
||||
}
|
||||
return s.db.Model(&user).UpdateColumn("points", gorm.Expr("points - ?", amount)).Error
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SocialService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSocialService(db *gorm.DB) *SocialService {
|
||||
return &SocialService{db: db}
|
||||
}
|
||||
|
||||
// SendFriendRequest 发送好友请求
|
||||
func (s *SocialService) SendFriendRequest(userID, friendID string) error {
|
||||
if userID == friendID {
|
||||
return errors.New("不能添加自己为好友")
|
||||
}
|
||||
f := &models.Friend{
|
||||
UserID: userID,
|
||||
FriendID: friendID,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
return s.db.Create(f).Error
|
||||
}
|
||||
|
||||
// AcceptFriendRequest 接受好友请求
|
||||
func (s *SocialService) AcceptFriendRequest(userID, friendID string) error {
|
||||
return s.db.Model(&models.Friend{}).
|
||||
Where("user_id = ? AND friend_id = ?", friendID, userID).
|
||||
Update("status", "accepted").Error
|
||||
}
|
||||
|
||||
// RemoveFriend 删除好友
|
||||
func (s *SocialService) RemoveFriend(userID, friendID string) error {
|
||||
return s.db.Where("(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)",
|
||||
userID, friendID, friendID, userID).Delete(&models.Friend{}).Error
|
||||
}
|
||||
|
||||
// GetFriends 获取好友列表
|
||||
func (s *SocialService) GetFriends(userID string) ([]models.User, error) {
|
||||
var friendIDs []string
|
||||
s.db.Model(&models.Friend{}).Select("friend_id").
|
||||
Where("user_id = ? AND status = ?", userID, "accepted").Find(&friendIDs)
|
||||
|
||||
var users []models.User
|
||||
if len(friendIDs) > 0 {
|
||||
s.db.Where("id IN ?", friendIDs).Find(&users)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// CreatePost 创建动态
|
||||
func (s *SocialService) CreatePost(post *models.Post) error {
|
||||
post.ID = generateID()
|
||||
post.CreatedAt = time.Now()
|
||||
return s.db.Create(post).Error
|
||||
}
|
||||
|
||||
// GetFeed 获取动态广场
|
||||
func (s *SocialService) GetFeed(page, pageSize int) ([]models.Post, int64, error) {
|
||||
var total int64
|
||||
s.db.Model(&models.Post{}).Where("privacy = ?", 1).Count(&total)
|
||||
|
||||
var posts []models.Post
|
||||
err := s.db.Where("privacy = ?", 1).Preload("User").
|
||||
Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&posts).Error
|
||||
return posts, total, err
|
||||
}
|
||||
|
||||
// LikePost 点赞动态
|
||||
func (s *SocialService) LikePost(postID string) error {
|
||||
return s.db.Model(&models.Post{}).Where("id = ?", postID).
|
||||
UpdateColumn("likes", gorm.Expr("likes + ?", 1)).Error
|
||||
}
|
||||
|
||||
// CommentPost 评论动态
|
||||
func (s *SocialService) CommentPost(postID string) error {
|
||||
return s.db.Model(&models.Post{}).Where("id = ?", postID).
|
||||
UpdateColumn("comments", gorm.Expr("comments + ?", 1)).Error
|
||||
}
|
||||
|
||||
// CreateGroup 创建小组
|
||||
func (s *SocialService) CreateGroup(userID string, group *models.Group) error {
|
||||
group.ID = generateID()
|
||||
group.CreatedBy = userID
|
||||
group.CreatedAt = time.Now()
|
||||
tx := s.db.Begin()
|
||||
|
||||
if err := tx.Create(group).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
member := &models.GroupMember{
|
||||
GroupID: group.ID,
|
||||
UserID: userID,
|
||||
Role: "admin",
|
||||
JoinedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(member).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// JoinGroup 加入小组
|
||||
func (s *SocialService) JoinGroup(groupID, userID string) error {
|
||||
member := &models.GroupMember{
|
||||
GroupID: groupID,
|
||||
UserID: userID,
|
||||
Role: "member",
|
||||
JoinedAt: time.Now(),
|
||||
}
|
||||
return s.db.Create(member).Error
|
||||
}
|
||||
|
||||
// LeaveGroup 退出小组
|
||||
func (s *SocialService) LeaveGroup(groupID, userID string) error {
|
||||
return s.db.Where("group_id = ? AND user_id = ?", groupID, userID).
|
||||
Delete(&models.GroupMember{}).Error
|
||||
}
|
||||
|
||||
// GetGroupMembers 获取小组成员
|
||||
func (s *SocialService) GetGroupMembers(groupID string) ([]models.GroupMember, error) {
|
||||
var members []models.GroupMember
|
||||
err := s.db.Where("group_id = ?", groupID).Find(&members).Error
|
||||
return members, err
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/inchstep/server/internal/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB) *UserService {
|
||||
return &UserService{db: db}
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func (s *UserService) RegisterByPhone(phone string) (*models.User, error) {
|
||||
var existing models.User
|
||||
if err := s.db.Where("phone = ?", phone).First(&existing).Error; err == nil {
|
||||
return nil, errors.New("该手机号已注册")
|
||||
}
|
||||
user := &models.User{
|
||||
ID: generateID(),
|
||||
Phone: &phone,
|
||||
Privacy: models.PrivacyFriendsOnly,
|
||||
Points: 0,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetByPhone(phone string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("phone = ?", phone).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetByID(id string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.First(&user, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateProfile(id string, updates map[string]interface{}) (*models.User, error) {
|
||||
allowed := map[string]bool{"nickname": true, "avatar": true, "bio": true, "motto": true, "privacy": true}
|
||||
filtered := make(map[string]interface{})
|
||||
for k, v := range updates {
|
||||
if allowed[k] {
|
||||
filtered[k] = v
|
||||
}
|
||||
}
|
||||
if err := s.db.Model(&models.User{}).Where("id = ?", id).Updates(filtered).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetByID(id)
|
||||
}
|
||||
|
||||
func (s *UserService) DeleteAccount(id string) error {
|
||||
return s.db.Model(&models.User{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"phone": nil,
|
||||
"nickname": nil,
|
||||
"avatar": nil,
|
||||
"bio": nil,
|
||||
"motto": nil,
|
||||
"privacy": models.PrivacyPrivate,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
Reference in New Issue
Block a user