commit eb15ef4b87ca018308cb726348127c8106545a9c Author: xie2can <384968446@qq.com> Date: Sat May 16 00:14:19 2026 +0800 init: taotie-api 项目初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d14bcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Binaries +tmp/ + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store + +# Env +.env + +# Go +*.exe diff --git a/CODEBUDDY.md b/CODEBUDDY.md new file mode 100644 index 0000000..b5365b5 --- /dev/null +++ b/CODEBUDDY.md @@ -0,0 +1,64 @@ +# CODEBUDDY.md + +本文件为 CodeBuddy Code 在本仓库中工作时提供指导。 + +## 构建与运行命令 + +| 操作 | 命令 | +|---|---| +| 构建 | `go build -o tmp/main .` | +| 运行 | `go run .` | +| Wire 代码生成 | `go generate ./...`(修改 `wire.go` 后必须运行) | +| 整理依赖 | `go mod tidy` | +| 测试 | `go test ./...`(暂无测试文件) | + +应用启动端口为 8000(配置在 `config/config.dev.yml`)。环境由 `.env` 控制(`ENV=dev` 对应 `config/config.{env}.yml`)。 + +## 架构 + +Go 后端 API,使用 Gin + MongoDB + Wire 依赖注入。经典三层架构,DTO/DO/PO 模型分离。 + +### 请求流程 + +``` +Client → Gin Router → wrap.Wrap[R,P](绑定/校验/响应)→ API Handler → Service → Repo → MongoDB +``` + +### 各层职责 + +- **API**(`api/`):路由、中间件、HTTP 处理器。`wrap.Wrap[R,P]()` 泛型包装器统一处理 JSON 绑定、参数校验(gookit/validate)和统一响应格式(`Resp{code, msg, data, ok, time}`)。 +- **Service**(`service/`):业务逻辑。Wire 注入集定义在 `service.go`。 +- **Repo**(`repo/`):数据访问。`BaseRepo[T]` 泛型仓储提供 CRUD 操作,支持软删除和租户隔离。子类添加实体特定查询。 +- **Model**(`model/`): + - `dto/` — API 请求/响应结构体 + - `do/` — Service 层输入/输出结构体 + - `po/` — 数据库实体,均嵌入 `TBase`(OID、时间戳、创建人、tenantId) + +### 关键设计模式 + +- **多租户隔离**:`BaseRepo.RawProcessFilter` 自动从 context 注入 `tenantId`(通过 `sctx` 包)。所有 PO 实体继承 `TBase.Tenant_OID`。 +- **Wire 依赖注入**:`wire.go`(构建标签 `wireinject`)定义注入图,`wire_gen.go` 为自动生成代码。注入链:`Configuration → MongoDb → Repos → Services → APIs → Router → Engine`。 +- **泛型 wrap 处理器**:`wrap.Wrap[R, P]()` 将请求绑定到 R,校验后调用 `func(context.Context, *R) (*P, error)`,成功返回 `Ok(c, data)`,失败返回 `Err(c, err)`。 +- **错误码**:系统错误定义在 `common/errsys.go`(0=成功,50-53=内部错误,s0xxx=业务通用),用户错误定义在 `common/erruser.go`(u001-u004)。 + +### API 路由 + +``` +GET / → Hello World +POST /api/v1/login → 用户登录(需 tenantNo + userName + password) +POST /api/v1/register → 用户注册 +GET /api/v1/user/current → 获取当前用户信息(需认证) +``` + +### 配置加载 + +1. `.env` 设置 `ENV` 变量 +2. 通过 Viper 加载 `config/config.{ENV}.yml` +3. 详见 `core/config.go` 中的 `Configuration` 结构体和默认值 + +## 开发约定 + +- 修改 `wire.go` 后,务必运行 `go generate ./...` 重新生成 `wire_gen.go`。 +- 新增 API 端点:在 `model/dto/` 定义 DTO,在 `api/v1/` 添加处理器,在 `api/api.go` 注册路由,实现 service 方法,按需添加 repo 方法。 +- 新增实体:在 `model/po/` 创建 PO(嵌入 `TBase`),在 `model/do/` 创建 DO,创建 `XxxRepo` 继承 `BaseRepo[T]`,添加 Wire provider。 +- 新增错误码:按现有模式在 `common/errsys.go` 或 `common/erruser.go` 中添加业务错误。 diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..2b8fcd3 --- /dev/null +++ b/api/api.go @@ -0,0 +1,45 @@ +package api + +import ( + "taotie-api/api/middleware" + v1 "taotie-api/api/v1" + "taotie-api/api/wrap" + "taotie-api/core" + + "github.com/gin-gonic/gin" + "github.com/google/wire" +) + +// 依赖注入节点 +var RouterProd = wire.NewSet( + NewRouter, + v1.NewUserApi, +) + +func NewRouter(cfg *core.Configuration, userApi *v1.UserApi) *gin.Engine { + r := gin.Default() + + // cors + r.Use(middleware.Cors()) + + // 定义路由 + r.GET("/", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "Hello, World!", + }) + }) + + apiv1 := r.Group("/api/v1") + { + apiv1.POST("/login", wrap.Wrap(userApi.Login)) + apiv1.POST("/register", wrap.Wrap(userApi.Register)) + } + + apiv1_user := apiv1.Group("/user") + apiv1_user.Use(middleware.Auth(cfg)) + { + apiv1_user.GET("/current", wrap.Wrap(userApi.GetCurrentUserInfo)) + } + + return r +} diff --git a/api/middleware/authMiddleware.go b/api/middleware/authMiddleware.go new file mode 100644 index 0000000..2f74c2c --- /dev/null +++ b/api/middleware/authMiddleware.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "net/http" + "strings" + "taotie-api/common" + "taotie-api/core" + "taotie-api/utils/sctx" + "taotie-api/utils/sjwt" + + "github.com/gin-gonic/gin" +) + +func Auth(cfg *core.Configuration) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusOK, gin.H{ + "code": common.ErrSysValidationFailed.Info().Id, + "msg": "缺少 Authorization 头", + "ok": false, + }) + c.Abort() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusOK, gin.H{ + "code": common.ErrSysValidationFailed.Info().Id, + "msg": "Authorization 格式错误,应为 Bearer ", + "ok": false, + }) + c.Abort() + return + } + + claims, err := sjwt.ParseToken(parts[1], cfg.JWT.SignString) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": common.ErrSysValidationFailed.Info().Id, + "msg": "token 无效或已过期", + "ok": false, + }) + c.Abort() + return + } + + sctx.SetCurrentUser(c, &sctx.CurrentUser{ + UserId: claims.UserId, + UserName: claims.UserName, + TenantId: claims.TenantId, + }) + + c.Next() + } +} diff --git a/api/middleware/corsMiddleware.go b/api/middleware/corsMiddleware.go new file mode 100644 index 0000000..6b54826 --- /dev/null +++ b/api/middleware/corsMiddleware.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func Cors() gin.HandlerFunc { + return cors.New(cors.Config{ + AllowOrigins: []string{"*"}, // 允许所有来源(生产环境不建议使用) + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, // 允许的请求方法 + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, // 允许的请求头部 + ExposeHeaders: []string{"Content-Length", "Access-Control-Allow-Origin", "Authorization"}, // 允许客户端获取的响应头部 + AllowCredentials: true, // 允许携带 Cookie + MaxAge: 12 * time.Hour, // 预检请求的缓存时间 + }) +} diff --git a/api/v1/userapi.go b/api/v1/userapi.go new file mode 100644 index 0000000..17de8f3 --- /dev/null +++ b/api/v1/userapi.go @@ -0,0 +1,85 @@ +package v1 + +import ( + "context" + "taotie-api/common" + "taotie-api/model/do/userdo" + "taotie-api/model/dto/userdto" + "taotie-api/service" + "taotie-api/utils/sctx" +) + +type UserApi struct { + userService *service.UserService +} + +func NewUserApi(userService *service.UserService) *UserApi { + return &UserApi{userService} +} + +// Login 登录 +func (api *UserApi) Login(ctx context.Context, req *userdto.LoginReq) (*userdto.LoginRes, error) { + out, err := api.userService.Login(ctx, &userdo.LoginIn{ + TenantNo: req.TenantNo, + UserName: req.UserName, + Password: req.Password, + }) + if err != nil { + return nil, err + } + + // 登录成功,返回 token + tokenOut, err := api.userService.GenToken(ctx, &userdo.GenTokenIn{ + TenantNo: out.TenantId, + UserId: out.UserId, + UserName: out.UserName, + }) + if err != nil { + return nil, err + } + + return &userdto.LoginRes{ + Token: tokenOut.Token, + }, nil +} + +// Register 注册 +func (api *UserApi) Register(ctx context.Context, req *userdto.RegisterReq) (*userdto.RegisterRes, error) { + // 创建租户和用户 + out, err := api.userService.Create(ctx, &userdo.CreateIn{ + TenantName: req.TenantName, + UserName: req.UserName, + Password: req.Password, + }) + if err != nil { + return nil, err + } + + // 自动登录,生成 token + tokenOut, err := api.userService.GenToken(ctx, &userdo.GenTokenIn{ + TenantNo: out.TenantId, + UserId: out.UserId, + UserName: out.UserName, + }) + if err != nil { + return nil, err + } + + return &userdto.RegisterRes{ + Token: tokenOut.Token, + }, nil +} + +// GetCurrentUserInfo 获取当前用户信息 +func (api *UserApi) GetCurrentUserInfo(ctx context.Context, req *userdto.GetCurrentUserInfoReq) (*userdto.GetCurrentUserInfoRes, error) { + cuser := sctx.GetCurrentUser(ctx) + if cuser == nil { + return nil, common.ErrUserNotFound + } + + return &userdto.GetCurrentUserInfoRes{ + UserId: cuser.UserId, + UserName: cuser.UserName, + TenantId: cuser.TenantId, + }, nil +} diff --git a/api/wrap/wrap.go b/api/wrap/wrap.go new file mode 100644 index 0000000..3c23433 --- /dev/null +++ b/api/wrap/wrap.go @@ -0,0 +1,110 @@ +package wrap + +import ( + "context" + "fmt" + "log/slog" + "reflect" + "taotie-api/common" + "time" + + "github.com/duke-git/lancet/v2/xerror" + "github.com/gin-gonic/gin" + "github.com/gookit/validate" +) + +func Wrap[R, P any](fn func(context.Context, *R) (*P, error)) func(c *gin.Context) { + return func(c *gin.Context) { + // defer func() { + // if r := recover(); r != nil { + // Err(c, errors.New(fmt.Sprintf("panic: %v", r))) + // c.Abort() + // } + // }() + + // 绑定请求参数 + var req = new(R) + + // 检查请求结构体是否为空结构体 + reqType := reflect.TypeOf(req).Elem() + isEmptyStruct := reqType.NumField() == 0 + + if !isEmptyStruct { + if c.Request.ContentLength != 0 { + // 绑定请求参数 + if err := c.ShouldBindJSON(req); err != nil { + er := common.ErrSysValidationFailed.Wrap(err) + Err(c, er) + c.Abort() + return + } + } + + // 校验请求参数 + v := validate.New(req) + if !v.Validate() { + er := common.ErrSysValidationFailed.Wrap(v.Errors.OneError()) + Err(c, er) + c.Abort() + return + } + } + + // 调用业务逻辑 + res, err := fn(c, req) + if err != nil { + Err(c, err) + c.Abort() + return + } + + Ok(c, res) + } +} + +type Resp struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data any `json:"data"` + Ok bool `json:"ok"` + Time int64 `json:"time"` +} + +func Ok(c *gin.Context, data any) { + c.JSON(200, Resp{ + Code: common.ErrSysOk.Info().Id, + Msg: common.ErrSysOk.Info().Message, + Data: data, + Ok: true, + Time: time.Now().Unix(), + }) +} + +// Err 处理错误 +// // 注意⚠️:这里需要想想是不是所有的错误都要返回给 web 端 +func Err(c *gin.Context, err error) { + // 返回给 web 端的错误 + var weberr = common.ErrSysInternal + + // 提取最底层的错误 + causeErr := xerror.Unwrap(err) + // 如果是自定义错误,解析错误,返回给 web 端 + if causeErr != nil { + weberr = causeErr + } else { + slog.Error("【SRV-ERR】", "ERR", fmt.Errorf("%+v", err)) + } + + // 系统内部错误的话,就不返回给 web 端,只记录。 + + // 记录日志 + // slog.Error("【WEB-ERR】", "ERR", weberr) + // slog.Error("【SRV-ERR】", "ERR", fmt.Errorf("%+v", err)) + + c.JSON(200, Resp{ + Code: weberr.Info().Id, + Msg: weberr.Info().Message, + Ok: false, + Time: time.Now().Unix(), + }) +} diff --git a/common/common.go b/common/common.go new file mode 100644 index 0000000..268ab28 --- /dev/null +++ b/common/common.go @@ -0,0 +1,38 @@ +package common + +type ( + Map = map[string]any + MapAnyAny = map[any]any + MapAnyStr = map[any]string + MapAnyInt = map[any]int + MapAnyBool = map[any]bool + MapStrAny = map[string]any + MapStrStr = map[string]string + MapStrInt = map[string]int + MapStrBool = map[string]bool + MapIntAny = map[int]any + MapIntStr = map[int]string + MapIntInt = map[int]int + MapIntBool = map[int]bool +) + +type ( + List = []Map + ListAnyAny = []MapAnyAny + ListAnyStr = []MapAnyStr + ListAnyInt = []MapAnyInt + ListAnyBool = []MapAnyBool + ListStrAny = []MapStrAny + ListStrStr = []MapStrStr + ListStrInt = []MapStrInt + ListStrBool = []MapStrBool + ListIntAny = []MapIntAny + ListIntStr = []MapIntStr + ListIntInt = []MapIntInt + ListIntBool = []MapIntBool +) + +const ( + False = 0 + True = 1 +) diff --git a/common/errsys.go b/common/errsys.go new file mode 100644 index 0000000..c5fbb7c --- /dev/null +++ b/common/errsys.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/duke-git/lancet/v2/xerror" +) + +var ( + ErrSysOk = xerror.New("ok").Id("0") // 成功 + + ErrSysInternal = xerror.New("未知内部错误").Id("50") // 未知内部错误 + ErrSysNil = xerror.New("空指针").Id("51") // 空指针 + ErrSysValidationFailed = xerror.New("校验失败").Id("52") // 校验失败 + ErrSysDbError = xerror.New("数据库错误").Id("53") // 数据库错误 + + ErrSysDuplicate = xerror.New("重复数据").Id("s0001") // 重复数据 + ErrSysInvalid = xerror.New("数据无效").Id("s0002") // 数据无效 + ErrSysDataNotFound = xerror.New("数据不存在").Id("s0003") // 数据不存在 +) diff --git a/common/erruser.go b/common/erruser.go new file mode 100644 index 0000000..37c4604 --- /dev/null +++ b/common/erruser.go @@ -0,0 +1,10 @@ +package common + +import "github.com/duke-git/lancet/v2/xerror" + +var ( + ErrUserNotFound = xerror.New("用户不存在").Id("u001") // 用户不存在 + ErrUserPasswordError = xerror.New("密码错误").Id("u002") // 密码错误 + ErrUserTenantNotFound = xerror.New("租户不存在").Id("u003") // 租户不存在 + ErrUserGenTokenFailed = xerror.New("生成token失败").Id("u004") // 生成token失败 +) diff --git a/config/config.dev.yml b/config/config.dev.yml new file mode 100644 index 0000000..a86a4f9 --- /dev/null +++ b/config/config.dev.yml @@ -0,0 +1,22 @@ +server: + port: 8000 + +log: + # const ( + # LevelDebug Level = -4 + # LevelInfo Level = 0 + # LevelWarn Level = 4 + # LevelError Level = 8 + # ) + path: log/app.log + level: -4 + +db: + mongoUri: mongodb://localhost:27017 + dbName: taotie + +jwt: + # 签名字符串 + signString: taotie-api-q2e + # 过期时间,单位秒 + timeOut: 3600 diff --git a/core/config.go b/core/config.go new file mode 100644 index 0000000..2f65fe7 --- /dev/null +++ b/core/config.go @@ -0,0 +1,84 @@ +package core + +import ( + "fmt" + "log/slog" + "os" + + "github.com/joho/godotenv" + "github.com/spf13/viper" +) + +type Configuration struct { + Server struct { + Port string `yaml:"port"` + } `yaml:"server"` + + Log struct { + Path string `json:"path"` + Level slog.Level `json:"level"` + } `yaml:"log"` + + DB struct { + MongoURI string `yaml:"mongoUri"` + DBName string `yaml:"dbName"` + } `yaml:"db"` + + // JWT 配置 + JWT struct { + SignString string `yaml:"signString"` + TimeOut int `yaml:"timeOut"` + } `yaml:"jwt"` + + Viper *viper.Viper `yaml:"-"` +} + +// 初始化配置 +func NewConfiguration() (cfg *Configuration, err error) { + cfg = &Configuration{} + + // 加载 .env 文件 + err = godotenv.Load() + if err != nil { + return nil, err + } + + // 读取环境配置 + env := os.Getenv("ENV") + + filename := "" + if env == "" { + filename = "config/config.yml" + } else { + filename = fmt.Sprintf("config/config.%v.yml", env) + } + + // 读取配置文件 + v := viper.New() + + // 设置默认值 + v.SetDefault("server.port", "8000") + v.SetDefault("log.path", "log/app.log") + v.SetDefault("log.level", 0) + v.SetDefault("jwt.signString", "taotie-api-q2e") + v.SetDefault("jwt.timeOut", 3600) + v.SetDefault("db.mongoUri", "mongodb://localhost:27017") + v.SetDefault("db.dbName", "taotie") + + // 设置配置文件路径 + v.SetConfigFile(filename) + + // 试着读取文件 + err = v.ReadInConfig() + if err != nil { + return nil, err + } + err = v.Unmarshal(cfg) + if err != nil { + return nil, err + } + + cfg.Viper = v + + return cfg, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f0bd72 --- /dev/null +++ b/go.mod @@ -0,0 +1,68 @@ +module taotie-api + +go 1.26.1 + +require ( + github.com/duke-git/lancet v1.4.6 + github.com/duke-git/lancet/v2 v2.3.9 + github.com/gin-contrib/cors v1.7.7 + github.com/gin-gonic/gin v1.12.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/wire v0.7.0 + github.com/gookit/goutil v0.7.4 + github.com/gookit/validate v1.5.7 + github.com/joho/godotenv v1.5.1 + github.com/spf13/viper v1.21.0 + go.mongodb.org/mongo-driver v1.17.9 +) + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.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.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gookit/filter v1.2.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.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/montanaflynn/stats v0.7.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20221208152030-732eee02a75a // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..44017bd --- /dev/null +++ b/go.sum @@ -0,0 +1,184 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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/duke-git/lancet v1.4.6 h1:pFTA06baQ8OceOmJB9tOsGz60y6GsfXOevIJVIFhGfg= +github.com/duke-git/lancet v1.4.6/go.mod h1:Grr6ehF0ig2nRIjeb+NmcxiJ12mkML4XQAx95tlQeJU= +github.com/duke-git/lancet/v2 v2.3.9 h1:ZxUvfoEY7YbsGIeoXRxHWIkRCAt6VN7UBKWgCCqBB3U= +github.com/duke-git/lancet/v2 v2.3.9/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q= +github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM= +github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM= +github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM= +github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= +github.com/gookit/validate v1.5.7 h1:W0nqxAbgCJ4ND/oR+Tx8yyYn6l+PkFVvGHlG7LxpYsc= +github.com/gookit/validate v1.5.7/go.mod h1:VeThM1saXWVJIG/Yn627PJL9yvap0s4bsjPUIV8I0UI= +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/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +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.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20221208152030-732eee02a75a h1:4iLhBPcpqFmylhnkbY3W0ONLUYYkDAW9xMFLfxgsvCw= +golang.org/x/exp v0.0.0-20221208152030-732eee02a75a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1aaf8af --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "log/slog" + "taotie-api/core" + + "github.com/gookit/validate/locales/zhcn" +) + +func main() { + // 初始化应用 + r, err := InitApp() + if err != nil { + panic(err) + } + + // 初始化配置 + cfg, err := core.NewConfiguration() + if err != nil { + slog.Error("初始化配置失败", "error", err) + } + + // for all Validation. + // NOTICE: 必须在调用 validate.New() 前注册, 它只需要一次调用。 + zhcn.RegisterGlobal() + + // 启动服务器 + r.Run(":" + cfg.Server.Port) +} diff --git a/model/do/tenantdo/enter.go b/model/do/tenantdo/enter.go new file mode 100644 index 0000000..7b59054 --- /dev/null +++ b/model/do/tenantdo/enter.go @@ -0,0 +1,9 @@ +package tenantdo + +type CreateIn struct { + TenantName string `json:"tenantName"` +} + +type CreateOut struct { + Id string `json:"id"` +} diff --git a/model/do/userdo/enter.go b/model/do/userdo/enter.go new file mode 100644 index 0000000..27c41be --- /dev/null +++ b/model/do/userdo/enter.go @@ -0,0 +1,35 @@ +package userdo + +type LoginIn struct { + TenantNo string `json:"tenantNo"` + UserName string `json:"userName"` + Password string `json:"password"` +} + +type LoginOut struct { + TenantId string `json:"tenantId"` + UserId string `json:"userId"` + UserName string `json:"userName"` +} + +type GenTokenIn struct { + TenantNo string `json:"tenantNo"` + UserId string `json:"userId"` + UserName string `json:"userName"` +} + +type GenTokenOut struct { + Token string `json:"token"` +} + +type CreateIn struct { + TenantName string `json:"tenantName"` + UserName string `json:"userName"` + Password string `json:"password"` +} + +type CreateOut struct { + TenantId string `json:"tenantId"` + UserId string `json:"userId"` + UserName string `json:"userName"` +} diff --git a/model/dto/userdto/enter.go b/model/dto/userdto/enter.go new file mode 100644 index 0000000..c11dfb0 --- /dev/null +++ b/model/dto/userdto/enter.go @@ -0,0 +1,29 @@ +package userdto + +type LoginReq struct { + TenantNo string `json:"tenantNo"` + UserName string `json:"userName"` + Password string `json:"password"` +} + +type LoginRes struct { + Token string `json:"token"` +} + +type RegisterReq struct { + TenantName string `json:"tenantName"` + UserName string `json:"userName"` + Password string `json:"password"` +} + +type RegisterRes struct { + Token string `json:"token"` +} + +type GetCurrentUserInfoReq struct{} + +type GetCurrentUserInfoRes struct { + UserId string `json:"userId"` + UserName string `json:"userName"` + TenantId string `json:"tenantId"` +} diff --git a/model/po/tbase.go b/model/po/tbase.go new file mode 100644 index 0000000..38c2878 --- /dev/null +++ b/model/po/tbase.go @@ -0,0 +1,43 @@ +package po + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type TBase struct { + OID primitive.ObjectID `bson:"_id,omitempty"` // 主键 + CreatedAt int64 `bson:"createdAt"` // 创建时间 + UpdatedAt int64 `bson:"updatedAt"` // 更新时间 + DeletedAt int64 `bson:"deletedAt"` // 删除时间 + CreatedBy_OID primitive.ObjectID `bson:"createdBy"` // 创建人OID + Tenant_OID primitive.ObjectID `bson:"tenantId"` // 租户ID +} + +// Id 获取主键 +func (t *TBase) Id() string { + return t.OID.Hex() +} + +// CreatedBy 获取创建人 Id +func (t *TBase) CreatedBy() string { + return t.CreatedBy_OID.Hex() +} + +// SetCreatedBy 设置创建人 Id +func (t *TBase) SetCreatedBy(createdBy string) { + t.CreatedBy_OID, _ = primitive.ObjectIDFromHex(createdBy) +} + +// TenantId 获取租户 Id +func (t *TBase) TenantId() string { + return t.Tenant_OID.Hex() +} + +// SetTenantId 设置租户 Id +func (t *TBase) SetTenantId(tenantId string) { + t.Tenant_OID, _ = primitive.ObjectIDFromHex(tenantId) +} + +// SetNow 设置当前时间 +func (t *TBase) SetNow(now int64) { + t.CreatedAt = now + t.UpdatedAt = now +} diff --git a/model/po/ttenant.go b/model/po/ttenant.go new file mode 100644 index 0000000..482ae80 --- /dev/null +++ b/model/po/ttenant.go @@ -0,0 +1,8 @@ +package po + +type TTenant struct { + TBase `bson:",inline"` + + TenantNo string `bson:"tenantNo"` + TenantName string `bson:"tenantName"` +} diff --git a/model/po/tuser.go b/model/po/tuser.go new file mode 100644 index 0000000..964f5b1 --- /dev/null +++ b/model/po/tuser.go @@ -0,0 +1,9 @@ +package po + +type TUser struct { + TBase `bson:",inline"` + + NickName string `bson:"nickName"` // 昵称 + UserName string `bson:"userName"` // 用户名 + Password string `bson:"password"` // 密码 +} diff --git a/repo/mongodb.go b/repo/mongodb.go new file mode 100644 index 0000000..754c61f --- /dev/null +++ b/repo/mongodb.go @@ -0,0 +1,40 @@ +package repo + +import ( + "context" + "taotie-api/core" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type MongoDb struct { + client *mongo.Client + cfg *core.Configuration +} + +// NewMongoDb 创建一个新的 MongoDb 实例 +func NewMongoDb(cfg *core.Configuration) (*MongoDb, error) { + ctx := context.Background() + + // 连接数据库 + client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.DB.MongoURI)) + if err != nil { + return nil, err + } + // 检查连接是否成功 + err = client.Ping(ctx, nil) + if err != nil { + return nil, err + } + + return &MongoDb{ + client: client, + cfg: cfg, + }, nil +} + +// Db 返回数据库实例 +func (m *MongoDb) Db() *mongo.Database { + return m.client.Database(m.cfg.DB.DBName) +} diff --git a/repo/repo.go b/repo/repo.go new file mode 100644 index 0000000..b8b3549 --- /dev/null +++ b/repo/repo.go @@ -0,0 +1,294 @@ +package repo + +import ( + "context" + "errors" + "reflect" + "taotie-api/utils/sctx" + "time" + + "github.com/duke-git/lancet/slice" + "github.com/google/wire" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// 依赖注入节点 +var RepoProd = wire.NewSet( + NewUserRepo, + NewTenantRepo, +) + +type BaseRepo[T IEntity] struct { + mdb *MongoDb + tableName string +} + +// ProcessFilter 处理查询条件 +func (rp *BaseRepo[T]) RawProcessFilter(ctx context.Context, qp *QueryParams) bson.M { + filter := bson.M{} + + // 查询条件 + if len(qp.Where) != 0 { + wh := make(map[string]any, 0) + for k, v := range qp.Where { + if (k[len(k)-2:]) == "Id" { + oid, _ := primitive.ObjectIDFromHex(v.(string)) + wh[k] = oid + continue + } + wh[k] = v + } + filter = wh + } + + // 查询 ids + if len(qp.WhereInIds) != 0 { + oids := make([]primitive.ObjectID, 0) + for _, id := range qp.WhereInIds { + oid, _ := primitive.ObjectIDFromHex(id) + oids = append(oids, oid) + } + filter["_id"] = bson.M{"$in": oids} + } + + // 过滤删除 + filter["deletedAt"] = 0 + + // 过滤租户 + cuser := sctx.GetCurrentUser(ctx) + if cuser != nil { + filter["tenantId"], _ = primitive.ObjectIDFromHex(cuser.TenantId) + } + + return filter +} + +// ProcessSetData 处理需要修改的数据,限制只能修改已拥有的字段 +func (rp *BaseRepo[T]) RawProcessSetData(ctx context.Context, setData map[string]any) (map[string]any, error) { + // 提取结构体 + t := reflect.TypeFor[T]().Elem() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + // 提取结构体字段 + fields := make([]string, 0, t.NumField()) + for i := 0; i < t.NumField(); i++ { + var bs = t.Field(i).Tag.Get("bson") + if bs == ",inline" { + continue + } + fields = append(fields, bs) + } + + // 筛选 + sd := make(map[string]any, 0) + for k, v := range setData { + if slice.Contain(fields, k) { + // 处理 ObjectID 类型 + if (k[len(k)-2:]) == "Id" { + oid, _ := primitive.ObjectIDFromHex(v.(string)) + sd[k] = oid + continue + } + if (k[len(k)-3:]) == "Ids" { + oids := make([]primitive.ObjectID, 0) + for _, id := range v.([]any) { + oid, _ := primitive.ObjectIDFromHex(id.(string)) + oids = append(oids, oid) + } + sd[k] = oids + continue + } + + sd[k] = v + } + } + + return sd, nil +} + +// Create 创建 +func (rp *BaseRepo[T]) Create(ctx context.Context, tdata T) (string, error) { + collection := rp.mdb.Db().Collection(rp.tableName) + + // 设置默认值 + cuser := sctx.GetCurrentUser(ctx) + tdata.SetCreatedBy(cuser.UserId) // 默认创建人 + tdata.SetTenantId(cuser.TenantId) // 默认租户 + + // 设置基础时间 + now := time.Now().UnixMilli() + tdata.SetNow(now) + + result, err := collection.InsertOne(ctx, tdata) + if err != nil { + return "", err + } + + // 插入成功后,返回插入的ID + return result.InsertedID.(primitive.ObjectID).Hex(), nil +} + +// CreateMany 创建多个 +func (rp *BaseRepo[T]) CreateMany(ctx context.Context, tdatas []T) (ids []string, err error) { + collection := rp.mdb.Db().Collection(rp.tableName) + + // 设置默认值 + cuser := sctx.GetCurrentUser(ctx) + now := time.Now().UnixMilli() + tdataanys := make([]any, 0, len(tdatas)) + for _, v := range tdatas { + if cuser != nil { + v.SetCreatedBy(cuser.UserId) // 默认创建人 + v.SetTenantId(cuser.TenantId) // 默认租户 + } + + // 设置基础时间 + v.SetNow(now) + + // 转换 + tdataanys = append(tdataanys, v) + } + // 插入数据 + result, err := collection.InsertMany(ctx, tdataanys) + if err != nil { + return nil, err + } + + ids = make([]string, 0, len(result.InsertedIDs)) + for _, id := range result.InsertedIDs { + ids = append(ids, id.(primitive.ObjectID).Hex()) + } + + return ids, nil +} + +// Delete 删除(软删除) +func (rp *BaseRepo[T]) Delete(ctx context.Context, qp *QueryParams) error { + collection := rp.mdb.Db().Collection(rp.tableName) + + filter := rp.RawProcessFilter(ctx, qp) + + // 查询参数不能为空 + if len(filter) == 0 { + return errors.New("查询参数不能为空") + } + + // 软删除 + _, err := collection.UpdateMany(ctx, filter, bson.M{"$set": bson.M{"deletedAt": time.Now().UnixMilli()}}) + + return err +} + +func (rp *BaseRepo[T]) Update(ctx context.Context, qp *QueryParams, setData map[string]any) error { + collection := rp.mdb.Db().Collection(rp.tableName) + + sd, err := rp.RawProcessSetData(ctx, setData) + if err != nil { + return err + } + + // 检查是否有可更新的字段 + if len(sd) == 0 { + return errors.New("更新参数不能为空") + } + + // 更新时间 + sd["updatedAt"] = time.Now().UnixMilli() + + filter := rp.RawProcessFilter(ctx, qp) + + if len(filter) == 0 { + return errors.New("查询参数不能为空") + } + + // 更新数据 + _, err = collection.UpdateMany(ctx, filter, bson.M{"$set": sd}) + if err != nil { + return err + } + + return nil +} + +// Count 查询数量 +func (rp *BaseRepo[T]) RawCount(ctx context.Context, qp *QueryParams) (int64, error) { + collection := rp.mdb.Db().Collection(rp.tableName) + + filter := rp.RawProcessFilter(ctx, qp) + + // 查询总量 + total, err := collection.CountDocuments(ctx, filter) + if err != nil { + return 0, err + } + + return total, nil +} + +// Find 查询 +func (rp *BaseRepo[T]) RawFind(ctx context.Context, qp *QueryParams) (results []T, total int64, err error) { + collection := rp.mdb.Db().Collection(rp.tableName) + + filter := rp.RawProcessFilter(ctx, qp) + + opts := &options.FindOptions{} + + // 设置分页 + if qp.Page > 0 && qp.PageSize > 0 { + opts.SetSkip(int64((qp.Page - 1) * qp.PageSize)) + opts.SetLimit(int64(qp.PageSize)) + } + + // 设置排序 + if len(qp.OrderBy) != 0 { + opts.SetSort(qp.OrderBy) + } + + // 执行查询 + cursor, err := collection.Find(ctx, filter, opts) + if err != nil { + return nil, 0, err + } + defer cursor.Close(ctx) + + // 解析结果 + if err = cursor.All(ctx, &results); err != nil { + // 处理空文档错误 + if err == mongo.ErrNilDocument { + return nil, 0, nil + } + return nil, 0, err + } + + // 查询总量 + total, err = collection.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + return results, total, nil +} + +// FindOne 查询单条记录 +func (rp *BaseRepo[T]) RawFindOne(ctx context.Context, qp *QueryParams) (result T, err error) { + collection := rp.mdb.Db().Collection(rp.tableName) + + filter := rp.RawProcessFilter(ctx, qp) + + // 执行查询 + err = collection.FindOne(ctx, filter).Decode(&result) + if err != nil { + // 处理空文档错误 + if err == mongo.ErrNilDocument { + return result, nil + } + return result, err + } + + return result, nil +} diff --git a/repo/tenantrepo.go b/repo/tenantrepo.go new file mode 100644 index 0000000..831081e --- /dev/null +++ b/repo/tenantrepo.go @@ -0,0 +1,36 @@ +package repo + +import ( + "context" + "taotie-api/common" + "taotie-api/model/po" +) + +type TenantRepo[T IEntity] struct { + BaseRepo[T] +} + +func NewTenantRepo(mdb *MongoDb) *TenantRepo[*po.TTenant] { + return &TenantRepo[*po.TTenant]{ + BaseRepo: BaseRepo[*po.TTenant]{ + mdb: mdb, + tableName: "ttenant", + }, + } +} + +func (rp *TenantRepo[T]) FindOneByTenantNo(ctx context.Context, tenantNo string) (result T, err error) { + return rp.RawFindOne(ctx, &QueryParams{ + Where: common.Map{ + "tenantNo": tenantNo, + }, + }) +} + +func (rp *TenantRepo[T]) FindOneByTenantName(ctx context.Context, tenantName string) (result T, err error) { + return rp.RawFindOne(ctx, &QueryParams{ + Where: common.Map{ + "tenantName": tenantName, + }, + }) +} diff --git a/repo/types.go b/repo/types.go new file mode 100644 index 0000000..db0b16c --- /dev/null +++ b/repo/types.go @@ -0,0 +1,18 @@ +package repo + +type IEntity interface { + Id() string + CreatedBy() string + SetCreatedBy(string) + TenantId() string + SetTenantId(string) + SetNow(int64) +} + +type QueryParams struct { + Page int + PageSize int + Where map[string]any + WhereInIds []string + OrderBy []string +} diff --git a/repo/userrepo.go b/repo/userrepo.go new file mode 100644 index 0000000..2f0eb49 --- /dev/null +++ b/repo/userrepo.go @@ -0,0 +1,39 @@ +package repo + +import ( + "context" + "taotie-api/common" + "taotie-api/model/po" +) + +type UserRepo[T IEntity] struct { + BaseRepo[T] +} + +func NewUserRepo(mdb *MongoDb) *UserRepo[*po.TUser] { + return &UserRepo[*po.TUser]{ + BaseRepo[*po.TUser]{ + mdb: mdb, + tableName: "tuser", + }, + } +} + +func (rp *UserRepo[T]) FindOneByTenantIdAndUserName(ctx context.Context, tenantId string, userName string) (result T, err error) { + result, err = rp.RawFindOne(ctx, &QueryParams{ + Where: common.Map{ + "tenantId": tenantId, + "userName": userName, + }, + }) + + if err != nil { + return result, err + } + + return result, nil +} + +func (rp *UserRepo[T]) Update(ctx context.Context, qp *QueryParams, setData map[string]any) error { + return nil +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..1e79d7c --- /dev/null +++ b/service/service.go @@ -0,0 +1,11 @@ +package service + +import ( + "github.com/google/wire" +) + +// 依赖注入节点 +var ServiceProd = wire.NewSet( + NewUserService, + NewTenantService, +) diff --git a/service/tenantservice.go b/service/tenantservice.go new file mode 100644 index 0000000..0239f11 --- /dev/null +++ b/service/tenantservice.go @@ -0,0 +1,45 @@ +package service + +import ( + "context" + "taotie-api/common" + "taotie-api/core" + "taotie-api/model/do/tenantdo" + "taotie-api/model/po" + "taotie-api/repo" + + "github.com/gookit/goutil/errorx" +) + +type TenantService struct { + cfg *core.Configuration + tenantRepo *repo.TenantRepo[*po.TTenant] +} + +func NewTenantService(cfg *core.Configuration, tenantRepo *repo.TenantRepo[*po.TTenant]) *TenantService { + return &TenantService{cfg, tenantRepo} +} + +func (rp *TenantService) Create(ctx context.Context, in *tenantdo.CreateIn) (*tenantdo.CreateOut, error) { + // 查重 + tenant, err := rp.tenantRepo.FindOneByTenantName(ctx, in.TenantName) + if err != nil { + return nil, err + } + if tenant != nil { + return nil, errorx.With(common.ErrSysDuplicate, "租户名称重复") + } + + // 创建租户 + tenant = &po.TTenant{ + TenantName: in.TenantName, + } + tenantId, err := rp.tenantRepo.Create(ctx, tenant) + if err != nil { + return nil, err + } + + return &tenantdo.CreateOut{ + Id: tenantId, + }, nil +} diff --git a/service/userservice.go b/service/userservice.go new file mode 100644 index 0000000..72ad66e --- /dev/null +++ b/service/userservice.go @@ -0,0 +1,125 @@ +package service + +import ( + "context" + "fmt" + "taotie-api/common" + "taotie-api/core" + "taotie-api/model/do/userdo" + "taotie-api/model/po" + "taotie-api/repo" + "taotie-api/utils/sctx" + "taotie-api/utils/sjwt" + "time" + + "github.com/gookit/goutil/errorx" +) + +type UserService struct { + cfg *core.Configuration + userRepo *repo.UserRepo[*po.TUser] + tenantRepo *repo.TenantRepo[*po.TTenant] +} + +func NewUserService(mdb *repo.MongoDb, cfg *core.Configuration, userRepo *repo.UserRepo[*po.TUser], tenantRepo *repo.TenantRepo[*po.TTenant]) *UserService { + return &UserService{ + cfg, + userRepo, + tenantRepo, + } +} + +// Login 登录 +func (s *UserService) Login(ctx context.Context, in *userdo.LoginIn) (*userdo.LoginOut, error) { + // 验证租户 + ttenant, err := s.tenantRepo.FindOneByTenantNo(ctx, in.TenantNo) + if err != nil { + return nil, err + } + if ttenant == nil { + return nil, common.ErrUserTenantNotFound + } + + // 验证用户 + tuser, err := s.userRepo.FindOneByTenantIdAndUserName(ctx, ttenant.Id(), in.UserName) + if err != nil { + return nil, err + } + if tuser == nil { + return nil, common.ErrUserNotFound + } + if tuser.Password != in.Password { + return nil, common.ErrUserPasswordError + } + + return &userdo.LoginOut{ + TenantId: ttenant.Id(), + UserId: tuser.Id(), + UserName: tuser.UserName, + }, nil +} + +// GenToken 生成token +func (s *UserService) GenToken(ctx context.Context, in *userdo.GenTokenIn) (*userdo.GenTokenOut, error) { + token, err := sjwt.GenerateToken(in.TenantNo, in.UserId, in.UserName, s.cfg.JWT.TimeOut, s.cfg.JWT.SignString) + if err != nil { + return nil, err + } + return &userdo.GenTokenOut{ + Token: token, + }, nil +} + +func (s *UserService) Create(ctx context.Context, in *userdo.CreateIn) (*userdo.CreateOut, error) { + // 查重:租户名称 + ttenant, err := s.tenantRepo.FindOneByTenantName(ctx, in.TenantName) + if err != nil { + return nil, err + } + if ttenant != nil { + return nil, errorx.With(common.ErrSysDuplicate, "租户名称重复") + } + + // 先设置临时用户到 context(避免 BaseRepo.Create 中 cuser 为 nil 导致 panic) + sctx.SetCurrentUser(ctx, &sctx.CurrentUser{ + UserId: "", + UserName: in.UserName, + TenantId: "", + }) + + // 生成 tenantNo(基于时间戳) + tenantNo := fmt.Sprintf("T%d", time.Now().UnixMilli()) + + // 创建租户 + ttenant = &po.TTenant{ + TenantNo: tenantNo, + TenantName: in.TenantName, + } + tenantId, err := s.tenantRepo.Create(ctx, ttenant) + if err != nil { + return nil, err + } + + // 更新 context 中的 TenantId + sctx.SetCurrentUser(ctx, &sctx.CurrentUser{ + UserId: "", + UserName: in.UserName, + TenantId: tenantId, + }) + + // 创建用户 + tuser := &po.TUser{ + UserName: in.UserName, + Password: in.Password, + } + userId, err := s.userRepo.Create(ctx, tuser) + if err != nil { + return nil, err + } + + return &userdo.CreateOut{ + TenantId: tenantId, + UserId: userId, + UserName: in.UserName, + }, nil +} diff --git a/utils/sctx/sctx.go b/utils/sctx/sctx.go new file mode 100644 index 0000000..c131445 --- /dev/null +++ b/utils/sctx/sctx.go @@ -0,0 +1,42 @@ +package sctx + +import ( + "context" + + "github.com/gin-gonic/gin" +) + +type MyKey string + +const CurrentKey MyKey = "taotie" + +type CurrentUser struct { + UserId string + UserName string + TenantId string +} + +func SetCurrentUser(ctx context.Context, cuser *CurrentUser) context.Context { + if c, ok := ctx.(*gin.Context); ok { + c.Set(CurrentKey, cuser) + return c + } + + return context.WithValue(ctx, CurrentKey, cuser) +} + +func GetCurrentUser(ctx context.Context) *CurrentUser { + if c, ok := ctx.(*gin.Context); ok { + if user, ok := c.Get(CurrentKey); ok { + if cuser, ok := user.(*CurrentUser); ok { + return cuser + } + } + } + + if cuser, ok := ctx.Value(CurrentKey).(*CurrentUser); ok { + return cuser + } + + return nil +} diff --git a/utils/sjwt/sjwt.go b/utils/sjwt/sjwt.go new file mode 100644 index 0000000..dc6bc29 --- /dev/null +++ b/utils/sjwt/sjwt.go @@ -0,0 +1,50 @@ +package sjwt + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type MyClaims struct { + jwt.RegisteredClaims + TenantId string `json:"tenantId"` + UserId string `json:"userId"` + UserName string `json:"userName"` +} + +// 生成 token +func GenerateToken(tenantId string, userId string, userName string, timeOut int, signedString string) (string, error) { + // 创建一个claims + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, MyClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(timeOut))), + Issuer: "taotie", + }, + TenantId: tenantId, + UserId: userId, + UserName: userName, + }) + + // 生成 token + return claims.SignedString([]byte(signedString)) +} + +// 解析 token +func ParseToken(tokenString string, signedString string) (*MyClaims, error) { + // 解析token + token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (any, error) { + return []byte(signedString), nil + }) + if err != nil { + return nil, err + } + + // 对token进行断言 + if claims, ok := token.Claims.(*MyClaims); ok { + return claims, nil + } else { + return nil, err + } +} diff --git a/wire.go b/wire.go new file mode 100644 index 0000000..34b4b6e --- /dev/null +++ b/wire.go @@ -0,0 +1,29 @@ +//go:build wireinject +// +build wireinject + +package main + +import ( + "taotie-api/api" + "taotie-api/core" + "taotie-api/repo" + "taotie-api/service" + + "github.com/gin-gonic/gin" + "github.com/google/wire" +) + +func InitApp() (*gin.Engine, error) { + wire.Build( + core.NewConfiguration, + repo.NewMongoDb, + + api.RouterProd, + + service.ServiceProd, + + repo.RepoProd, + ) + + return nil, nil +} diff --git a/wire_gen.go b/wire_gen.go new file mode 100644 index 0000000..a110ab7 --- /dev/null +++ b/wire_gen.go @@ -0,0 +1,35 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package main + +import ( + "github.com/gin-gonic/gin" + "taotie-api/api" + "taotie-api/api/v1" + "taotie-api/core" + "taotie-api/repo" + "taotie-api/service" +) + +// Injectors from wire.go: + +func InitApp() (*gin.Engine, error) { + configuration, err := core.NewConfiguration() + if err != nil { + return nil, err + } + mongoDb, err := repo.NewMongoDb(configuration) + if err != nil { + return nil, err + } + userRepo := repo.NewUserRepo(mongoDb) + tenantRepo := repo.NewTenantRepo(mongoDb) + userService := service.NewUserService(mongoDb, configuration, userRepo, tenantRepo) + userApi := v1.NewUserApi(userService) + engine := api.NewRouter(configuration, userApi) + return engine, nil +}