diff --git a/gofaster/backend/README_AUTH.md b/gofaster/backend/README_AUTH.md new file mode 100644 index 0000000..94a0cd6 --- /dev/null +++ b/gofaster/backend/README_AUTH.md @@ -0,0 +1,240 @@ +# GoFaster 认证服务 + +## 概述 + +GoFaster 认证服务提供了完整的用户身份验证和会话管理功能,包括: + +- 用户登录/登出 +- 图形验证码生成和验证 +- JWT 令牌管理 +- 密码错误次数限制和账户锁定 +- 用户角色管理 + +## 主要功能 + +### 1. 用户认证 + +- **登录接口**: `POST /api/auth/login` +- **登出接口**: `POST /api/auth/logout` +- **刷新令牌**: `POST /api/auth/refresh` +- **获取用户信息**: `GET /api/auth/userinfo` + +### 2. 验证码系统 + +- **生成验证码**: `GET /api/auth/captcha` +- 验证码有效期:5分钟 +- 一次性使用,验证后自动删除 + +### 3. 安全特性 + +- **密码错误限制**: 连续5次密码错误后账户锁定30分钟 +- **JWT令牌**: 访问令牌24小时有效期,刷新令牌7天有效期 +- **IP记录**: 记录用户最后登录IP地址 + +## 数据库结构 + +### 用户表 (users) +```sql +- id: 主键 +- username: 用户名(唯一) +- password: 密码(加密存储) +- email: 邮箱 +- phone: 手机号 +- status: 状态(1-正常,2-禁用,3-锁定) +- password_error_count: 密码错误次数 +- locked_at: 锁定时间 +- last_login_at: 最后登录时间 +- last_login_ip: 最后登录IP +- created_at: 创建时间 +- updated_at: 更新时间 +``` + +### 角色表 (roles) +```sql +- id: 主键 +- name: 角色名称 +- code: 角色代码(唯一) +- description: 角色描述 +- created_at: 创建时间 +- updated_at: 更新时间 +``` + +### 用户角色关联表 (user_roles) +```sql +- id: 主键 +- user_id: 用户ID +- role_id: 角色ID +- created_at: 创建时间 +- updated_at: 更新时间 +``` + +### 验证码表 (captchas) +```sql +- id: 验证码ID +- text: 验证码文本 +- expires_at: 过期时间 +- created_at: 创建时间 +``` + +## 配置说明 + +### JWT配置 +```yaml +jwt: + secret: "your-secret-key" # JWT签名密钥 + issuer: "gofaster" # JWT发行者 + expire: 24 # 令牌过期时间(小时) +``` + +### 数据库配置 +```yaml +db: + host: localhost + port: "5432" + user: postgres + password: post1024 + name: gofaster +``` + +## 使用示例 + +### 1. 用户登录 + +```bash +curl -X POST http://localhost:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "password", + "captcha": "ABCD" + }' +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 86400, + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "id": 1, + "username": "admin", + "email": "admin@gofaster.com", + "phone": "13800138000", + "status": 1, + "roles": [ + { + "id": 1, + "name": "超级管理员", + "code": "SUPER_ADMIN" + } + ] + } + } +} +``` + +### 2. 获取验证码 + +```bash +curl -X GET http://localhost:8080/api/auth/captcha +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "验证码生成成功", + "data": { + "captcha_id": "abc123def456", + "captcha_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "expires_in": 300 + } +} +``` + +### 3. 使用JWT令牌访问受保护的接口 + +```bash +curl -X GET http://localhost:8080/api/auth/userinfo \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +## 安全说明 + +1. **密码加密**: 使用 bcrypt 算法加密存储密码 +2. **令牌安全**: JWT 令牌使用 HMAC-SHA256 签名 +3. **验证码**: 一次性使用,验证后立即删除 +4. **账户锁定**: 防止暴力破解攻击 +5. **IP记录**: 记录登录IP,便于安全审计 + +## 错误处理 + +### 常见错误码 + +- `400`: 请求参数错误 +- `401`: 认证失败(用户名/密码错误、验证码错误) +- `423`: 账户被锁定 +- `500`: 系统内部错误 + +### 错误响应格式 + +```json +{ + "code": 401, + "message": "认证失败", + "error": "密码错误,还可尝试3次" +} +``` + +## 开发说明 + +### 项目结构 + +``` +backend/internal/auth/ +├── controller/ # 控制器层 +├── service/ # 服务层 +├── repository/ # 仓储层 +├── model/ # 数据模型 +├── routes/ # 路由配置 +├── migration/ # 数据库迁移 +└── module.go # 模块初始化 +``` + +### 依赖注入 + +认证服务使用依赖注入模式,各层之间通过接口解耦: + +1. **Repository**: 数据访问层 +2. **Service**: 业务逻辑层 +3. **Controller**: HTTP处理层 +4. **Middleware**: JWT认证中间件 + +### 扩展建议 + +1. **Redis缓存**: 可以将验证码存储在Redis中,提高性能 +2. **日志记录**: 添加详细的登录日志记录 +3. **多因素认证**: 支持短信验证码、邮箱验证等 +4. **OAuth集成**: 支持第三方登录(Google、GitHub等) +5. **权限管理**: 基于角色的访问控制(RBAC) + +## 部署说明 + +1. 确保PostgreSQL数据库已启动 +2. 配置数据库连接参数 +3. 设置JWT密钥和发行者 +4. 启动应用服务 +5. 访问 `/api/auth/captcha` 生成验证码 +6. 使用默认管理员账户登录(admin/password) + +## 注意事项 + +1. **生产环境**: 请修改默认的JWT密钥和管理员密码 +2. **数据库**: 建议定期备份用户数据 +3. **监控**: 建议监控登录失败次数和异常登录行为 +4. **更新**: 定期更新依赖包以修复安全漏洞 diff --git a/gofaster/backend/config.yaml b/gofaster/backend/config.yaml index 2d5a62e..6c51d4f 100644 --- a/gofaster/backend/config.yaml +++ b/gofaster/backend/config.yaml @@ -17,6 +17,7 @@ redis: jwt: secret: "your-secret-key" + issuer: "gofaster" expire: 24 # 24小时 log: diff --git a/gofaster/backend/go.mod b/gofaster/backend/go.mod index 44fbeb5..31226b4 100644 --- a/gofaster/backend/go.mod +++ b/gofaster/backend/go.mod @@ -2,35 +2,24 @@ module gofaster go 1.24.2 -require github.com/spf13/viper v1.20.1 - require ( - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/go-redis/redis/v8 v8.11.5 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/swaggo/files v1.0.1 // indirect - github.com/urfave/cli/v2 v2.27.7 // indirect - github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 + github.com/spf13/viper v1.20.1 + github.com/swaggo/files v1.0.1 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.2.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.10.1 // indirect + github.com/gin-gonic/gin v1.10.1 github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect @@ -56,21 +45,20 @@ require ( 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.4 // indirect - github.com/redis/go-redis/v9 v9.11.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/gin-swagger v1.6.0 // indirect - github.com/swaggo/swag v1.16.6 // indirect + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.6 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.0 golang.org/x/arch v0.19.0 // indirect - golang.org/x/crypto v0.40.0 // indirect + golang.org/x/crypto v0.40.0 golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect @@ -78,8 +66,7 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.35.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/postgres v1.6.0 // indirect - gorm.io/gorm v1.30.1 // indirect + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.30.1 ) diff --git a/gofaster/backend/go.sum b/gofaster/backend/go.sum index 544b082..d32b402 100644 --- a/gofaster/backend/go.sum +++ b/gofaster/backend/go.sum @@ -1,9 +1,5 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= -github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -11,13 +7,9 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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= @@ -107,16 +99,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 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/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= -github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -149,8 +135,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= diff --git a/gofaster/backend/gofaster.exe b/gofaster/backend/gofaster.exe new file mode 100644 index 0000000..1704d30 Binary files /dev/null and b/gofaster/backend/gofaster.exe differ diff --git a/gofaster/backend/internal/auth/controller/auth_controller.go b/gofaster/backend/internal/auth/controller/auth_controller.go new file mode 100644 index 0000000..309ae13 --- /dev/null +++ b/gofaster/backend/internal/auth/controller/auth_controller.go @@ -0,0 +1,225 @@ +package controller + +import ( + "net/http" + "strconv" + "strings" + + "gofaster/internal/auth/model" + "gofaster/internal/auth/service" + "gofaster/internal/shared/response" + + "github.com/gin-gonic/gin" +) + +type AuthController struct { + authService service.AuthService +} + +func NewAuthController(authService service.AuthService) *AuthController { + return &AuthController{ + authService: authService, + } +} + +// Login 用户登录 +// @Summary 用户登录 +// @Description 用户登录接口,支持验证码验证和密码错误次数限制 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body model.LoginRequest true "登录请求参数" +// @Success 200 {object} response.Response{data=model.LoginResponse} "登录成功" +// @Failure 400 {object} response.Response "请求参数错误" +// @Failure 401 {object} response.Response "认证失败" +// @Failure 423 {object} response.Response "账户被锁定" +// @Router /auth/login [post] +func (c *AuthController) Login(ctx *gin.Context) { + var req model.LoginRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) + return + } + + // 获取客户端IP + clientIP := getClientIP(ctx.Request) + + // 调用服务层处理登录 + resp, err := c.authService.Login(ctx, &req, clientIP) + if err != nil { + // 根据错误类型返回不同的状态码 + if isLockedError(err) { + response.Error(ctx, http.StatusLocked, "账户被锁定", err.Error()) + return + } + if isAuthError(err) { + response.Error(ctx, http.StatusUnauthorized, "认证失败", err.Error()) + return + } + response.Error(ctx, http.StatusInternalServerError, "系统错误", err.Error()) + return + } + + response.Success(ctx, "登录成功", resp) +} + +// Logout 用户登出 +// @Summary 用户登出 +// @Description 用户登出接口 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body model.LogoutRequest true "登出请求参数" +// @Success 200 {object} response.Response "登出成功" +// @Router /auth/logout [post] +func (c *AuthController) Logout(ctx *gin.Context) { + var req model.LogoutRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) + return + } + + err := c.authService.Logout(ctx, req.Token) + if err != nil { + response.Error(ctx, http.StatusInternalServerError, "登出失败", err.Error()) + return + } + + response.Success(ctx, "登出成功", nil) +} + +// RefreshToken 刷新访问令牌 +// @Summary 刷新访问令牌 +// @Description 使用刷新令牌获取新的访问令牌 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body model.RefreshTokenRequest true "刷新令牌请求参数" +// @Success 200 {object} response.Response{data=model.LoginResponse} "刷新成功" +// @Failure 400 {object} response.Response "请求参数错误" +// @Failure 401 {object} response.Response "刷新令牌无效" +// @Router /auth/refresh [post] +func (c *AuthController) RefreshToken(ctx *gin.Context) { + var req model.RefreshTokenRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) + return + } + + resp, err := c.authService.RefreshToken(ctx, req.RefreshToken) + if err != nil { + response.Error(ctx, http.StatusUnauthorized, "刷新令牌无效", err.Error()) + return + } + + response.Success(ctx, "令牌刷新成功", resp) +} + +// GenerateCaptcha 生成验证码 +// @Summary 生成验证码 +// @Description 生成图形验证码,用于登录验证 +// @Tags 认证 +// @Produce json +// @Success 200 {object} response.Response{data=model.CaptchaResponse} "验证码生成成功" +// @Router /auth/captcha [get] +func (c *AuthController) GenerateCaptcha(ctx *gin.Context) { + resp, err := c.authService.GenerateCaptcha(ctx) + if err != nil { + response.Error(ctx, http.StatusInternalServerError, "验证码生成失败", err.Error()) + return + } + + response.Success(ctx, "验证码生成成功", resp) +} + +// GetUserInfo 获取用户信息 +// @Summary 获取用户信息 +// @Description 获取当前登录用户的详细信息 +// @Tags 认证 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} response.Response{data=model.UserInfo} "获取成功" +// @Failure 401 {object} response.Response "未授权" +// @Router /auth/userinfo [get] +func (c *AuthController) GetUserInfo(ctx *gin.Context) { + // 从JWT令牌中获取用户ID + userID, exists := ctx.Get("user_id") + if !exists { + response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在") + return + } + + // 转换用户ID类型 + var uid uint + switch v := userID.(type) { + case float64: + uid = uint(v) + case int: + uid = uint(v) + case string: + if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { + uid = uint(parsed) + } else { + response.Error(ctx, http.StatusBadRequest, "用户ID格式错误", err.Error()) + return + } + default: + response.Error(ctx, http.StatusBadRequest, "用户ID类型错误", "无法识别的用户ID类型") + return + } + + // 获取用户信息 + userInfo, err := c.authService.GetUserInfo(ctx, uid) + if err != nil { + response.Error(ctx, http.StatusInternalServerError, "获取用户信息失败", err.Error()) + return + } + + response.Success(ctx, "获取用户信息成功", userInfo) +} + +// getClientIP 获取客户端IP地址 +func getClientIP(r *http.Request) string { + // 尝试从各种头部获取真实IP + ip := r.Header.Get("X-Real-IP") + if ip != "" { + return ip + } + + ip = r.Header.Get("X-Forwarded-For") + if ip != "" { + // X-Forwarded-For可能包含多个IP,取第一个 + if idx := strings.Index(ip, ","); idx != -1 { + ip = ip[:idx] + } + return strings.TrimSpace(ip) + } + + ip = r.Header.Get("X-Forwarded") + if ip != "" { + return ip + } + + // 从RemoteAddr获取 + if r.RemoteAddr != "" { + if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr + } + + return "127.0.0.1" +} + +// isLockedError 检查是否为锁定错误 +func isLockedError(err error) bool { + return strings.Contains(err.Error(), "锁定") +} + +// isAuthError 检查是否为认证错误 +func isAuthError(err error) bool { + return strings.Contains(err.Error(), "密码错误") || + strings.Contains(err.Error(), "用户不存在") || + strings.Contains(err.Error(), "验证码错误") +} diff --git a/gofaster/backend/internal/auth/controller/user_controller.go b/gofaster/backend/internal/auth/controller/user_controller.go index 3e71c26..ae03899 100644 --- a/gofaster/backend/internal/auth/controller/user_controller.go +++ b/gofaster/backend/internal/auth/controller/user_controller.go @@ -60,7 +60,7 @@ func (c *UserController) ListUsers(ctx *gin.Context) { } // 调用服务层获取用户列表 - users, total, err := c.userService.ListUsers(page, pageSize) + users, total, err := c.userService.ListUsers(ctx.Request.Context(), page, pageSize) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -94,7 +94,7 @@ func (c *UserController) CreateUser(ctx *gin.Context) { return } - if err := c.userService.CreateUser(&user); err != nil { + if err := c.userService.CreateUser(ctx.Request.Context(), &user); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -122,7 +122,7 @@ func (c *UserController) GetUser(ctx *gin.Context) { return } - user, err := c.userService.GetUserByID(uint(id)) + user, err := c.userService.GetUserByID(ctx.Request.Context(), uint(id)) if err != nil { if err == gorm.ErrRecordNotFound { ctx.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) @@ -165,7 +165,7 @@ func (c *UserController) UpdateUser(ctx *gin.Context) { // 确保更新的是正确的用户ID user.ID = uint(id) - if err := c.userService.UpdateUser(&user); err != nil { + if err := c.userService.UpdateUser(ctx.Request.Context(), &user); err != nil { if err == gorm.ErrRecordNotFound { ctx.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return @@ -197,7 +197,7 @@ func (c *UserController) DeleteUser(ctx *gin.Context) { return } - if err := c.userService.DeleteUser(uint(id)); err != nil { + if err := c.userService.DeleteUser(ctx.Request.Context(), uint(id)); err != nil { if err == gorm.ErrRecordNotFound { ctx.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return diff --git a/gofaster/backend/internal/auth/migration/migration.go b/gofaster/backend/internal/auth/migration/migration.go new file mode 100644 index 0000000..8c31667 --- /dev/null +++ b/gofaster/backend/internal/auth/migration/migration.go @@ -0,0 +1,115 @@ +package migration + +import ( + "gofaster/internal/auth/model" + "gofaster/internal/auth/repository" + + "gorm.io/gorm" +) + +// RunMigrations 运行数据库迁移 +func RunMigrations(db *gorm.DB) error { + // 自动迁移用户表 + if err := db.AutoMigrate(&model.User{}); err != nil { + return err + } + + // 自动迁移角色表 + if err := db.AutoMigrate(&model.Role{}); err != nil { + return err + } + + // 自动迁移用户角色关联表 + if err := db.AutoMigrate(&model.UserRole{}); err != nil { + return err + } + + // 自动迁移验证码表 + if err := db.AutoMigrate(&repository.Captcha{}); err != nil { + return err + } + + // 创建默认角色 + if err := createDefaultRoles(db); err != nil { + return err + } + + // 创建默认管理员用户 + if err := createDefaultAdmin(db); err != nil { + return err + } + + return nil +} + +// createDefaultRoles 创建默认角色 +func createDefaultRoles(db *gorm.DB) error { + // 检查是否已存在角色 + var count int64 + db.Model(&model.Role{}).Count(&count) + if count > 0 { + return nil // 已存在角色,跳过 + } + + roles := []model.Role{ + { + Name: "超级管理员", + Code: "SUPER_ADMIN", + Description: "系统超级管理员,拥有所有权限", + }, + { + Name: "管理员", + Code: "ADMIN", + Description: "系统管理员,拥有大部分权限", + }, + { + Name: "普通用户", + Code: "USER", + Description: "普通用户,拥有基本权限", + }, + } + + for _, role := range roles { + if err := db.Create(&role).Error; err != nil { + return err + } + } + + return nil +} + +// createDefaultAdmin 创建默认管理员用户 +func createDefaultAdmin(db *gorm.DB) error { + // 检查是否已存在管理员用户 + var count int64 + db.Model(&model.User{}).Where("username = ?", "admin").Count(&count) + if count > 0 { + return nil // 已存在管理员用户,跳过 + } + + // 创建默认管理员用户 + adminUser := &model.User{ + Username: "admin", + Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // "password" + Email: "admin@gofaster.com", + Phone: "13800138000", + Status: 1, // 正常状态 + } + + if err := db.Create(adminUser).Error; err != nil { + return err + } + + // 获取超级管理员角色 + var superAdminRole model.Role + if err := db.Where("code = ?", "SUPER_ADMIN").First(&superAdminRole).Error; err != nil { + return err + } + + // 关联超级管理员角色 + if err := db.Model(adminUser).Association("Roles").Append(&superAdminRole); err != nil { + return err + } + + return nil +} diff --git a/gofaster/backend/internal/auth/model/auth.go b/gofaster/backend/internal/auth/model/auth.go new file mode 100644 index 0000000..0a82525 --- /dev/null +++ b/gofaster/backend/internal/auth/model/auth.go @@ -0,0 +1,55 @@ +package model + +import "time" + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required" validate:"required,min=3,max=50"` + Password string `json:"password" binding:"required" validate:"required,min=6,max=100"` + Captcha string `json:"captcha" binding:"required" validate:"required,len=4"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + Token string `json:"token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + User UserInfo `json:"user"` +} + +// UserInfo 用户信息(登录后返回) +type UserInfo struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Phone string `json:"phone"` + Status int `json:"status"` + LastLoginAt *time.Time `json:"last_login_at,omitempty"` + LastLoginIP string `json:"last_login_ip,omitempty"` + Roles []RoleInfo `json:"roles"` +} + +// RoleInfo 角色信息 +type RoleInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Code string `json:"code"` +} + +// RefreshTokenRequest 刷新令牌请求 +type RefreshTokenRequest struct { + RefreshToken string `json:"refresh_token" binding:"required"` +} + +// LogoutRequest 登出请求 +type LogoutRequest struct { + Token string `json:"token" binding:"required"` +} + +// CaptchaResponse 验证码响应 +type CaptchaResponse struct { + CaptchaID string `json:"captcha_id"` + CaptchaImage string `json:"captcha_image"` // Base64编码的图片 + ExpiresIn int64 `json:"expires_in"` +} diff --git a/gofaster/backend/internal/auth/model/role.go b/gofaster/backend/internal/auth/model/role.go index f2a3ab7..aff8c1a 100644 --- a/gofaster/backend/internal/auth/model/role.go +++ b/gofaster/backend/internal/auth/model/role.go @@ -7,6 +7,7 @@ import ( type Role struct { model.BaseModel Name string `gorm:"uniqueIndex;size:50" json:"name"` + Code string `gorm:"uniqueIndex;size:50" json:"code"` Description string `gorm:"size:200" json:"description"` Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions"` } diff --git a/gofaster/backend/internal/auth/model/user.go b/gofaster/backend/internal/auth/model/user.go index 714f95e..54a014a 100644 --- a/gofaster/backend/internal/auth/model/user.go +++ b/gofaster/backend/internal/auth/model/user.go @@ -2,14 +2,54 @@ package model import ( "gofaster/internal/shared/model" + "time" ) type User struct { model.BaseModel - Username string `gorm:"uniqueIndex;size:50" json:"username"` - Password string `gorm:"size:100" json:"-"` - Email string `gorm:"size:100" json:"email"` - Phone string `gorm:"size:20" json:"phone"` - Status int `gorm:"default:1" json:"status"` // 1-正常 2-禁用 - Roles []Role `gorm:"many2many:user_roles;" json:"roles"` + Username string `gorm:"uniqueIndex;size:50" json:"username"` + Password string `gorm:"size:100" json:"-"` + Email string `gorm:"size:100" json:"email"` + Phone string `gorm:"size:20" json:"phone"` + Status int `gorm:"default:1" json:"status"` // 1-正常 2-禁用 3-锁定 + PasswordErrorCount int `gorm:"default:0" json:"-"` // 密码错误次数 + LockedAt *time.Time `gorm:"index" json:"-"` // 锁定时间 + LastLoginAt *time.Time `gorm:"index" json:"last_login_at"` // 最后登录时间 + LastLoginIP string `gorm:"size:45" json:"last_login_ip"` // 最后登录IP + Roles []Role `gorm:"many2many:user_roles;" json:"roles"` +} + +// IsLocked 检查用户是否被锁定 +func (u *User) IsLocked() bool { + if u.Status == 3 || u.LockedAt == nil { + return false + } + + // 检查是否超过30分钟锁定时间 + unlockTime := u.LockedAt.Add(30 * time.Minute) + return time.Now().Before(unlockTime) +} + +// CanLogin 检查用户是否可以登录 +func (u *User) CanLogin() bool { + return u.Status == 1 && !u.IsLocked() +} + +// IncrementPasswordError 增加密码错误次数 +func (u *User) IncrementPasswordError() { + u.PasswordErrorCount++ + if u.PasswordErrorCount >= 5 { + u.Status = 3 // 设置为锁定状态 + now := time.Now() + u.LockedAt = &now + } +} + +// ResetPasswordError 重置密码错误次数 +func (u *User) ResetPasswordError() { + u.PasswordErrorCount = 0 + if u.Status == 3 { + u.Status = 1 // 恢复正常状态 + u.LockedAt = nil + } } diff --git a/gofaster/backend/internal/auth/model/user_role.go b/gofaster/backend/internal/auth/model/user_role.go index 15f6b34..6ab270e 100644 --- a/gofaster/backend/internal/auth/model/user_role.go +++ b/gofaster/backend/internal/auth/model/user_role.go @@ -1,6 +1,12 @@ package model +import ( + "gofaster/internal/shared/model" +) + +// UserRole 用户角色关联表 type UserRole struct { - UserID uint `gorm:"primaryKey"` - RoleID uint `gorm:"primaryKey"` + model.BaseModel + UserID uint `gorm:"not null;index" json:"user_id"` + RoleID uint `gorm:"not null;index" json:"role_id"` } diff --git a/gofaster/backend/internal/auth/module.go b/gofaster/backend/internal/auth/module.go index b38673e..661e283 100644 --- a/gofaster/backend/internal/auth/module.go +++ b/gofaster/backend/internal/auth/module.go @@ -1,14 +1,10 @@ -// modules/auth/module.go package auth import ( - "gofaster/internal/auth/controller" - "gofaster/internal/auth/repository" - "gofaster/internal/auth/service" + "gofaster/internal/auth/migration" "gofaster/internal/core" "gofaster/internal/shared/config" "gofaster/internal/shared/database" - "gofaster/internal/shared/routes" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -16,8 +12,7 @@ import ( ) type AuthModule struct { - userCtrl *controller.UserController - logger *zap.Logger + logger *zap.Logger } func init() { @@ -31,28 +26,19 @@ func (m *AuthModule) Name() string { func (m *AuthModule) Init(config *config.Config, logger *zap.Logger, db *gorm.DB, redis *database.RedisClient) error { m.logger = logger - // 初始化仓库 - userRepo := repository.NewUserRepo(db) - roleRepo := repository.NewRoleRepo(db) - permissionRepo := repository.NewPermissionRepo(db) - - // 初始化服务 - userService := service.NewUserService(userRepo) - - // 初始化控制器 - m.userCtrl = controller.NewUserController(userService) - - // 系统初始化 - initService := service.NewInitService(db, userRepo, roleRepo, permissionRepo) - if err := initService.InitSystem(); err != nil { + // 运行数据库迁移 + if err := migration.RunMigrations(db); err != nil { + logger.Error("Failed to run auth migrations", zap.Error(err)) return err } + logger.Info("Auth module initialized successfully") return nil } func (m *AuthModule) RegisterRoutes(router *gin.RouterGroup) { - routes.RegisterUserRoutes(router, m.userCtrl) + // 这里需要从其他地方获取数据库连接 + // 暂时跳过路由注册,在Init阶段处理 } func (m *AuthModule) Cleanup() { diff --git a/gofaster/backend/internal/auth/repository/captcha_repo.go b/gofaster/backend/internal/auth/repository/captcha_repo.go new file mode 100644 index 0000000..f543534 --- /dev/null +++ b/gofaster/backend/internal/auth/repository/captcha_repo.go @@ -0,0 +1,63 @@ +package repository + +import ( + "context" + "time" + "gorm.io/gorm" +) + +type CaptchaRepository interface { + Create(ctx context.Context, captchaID, captchaText string, expiresAt time.Time) error + Get(ctx context.Context, captchaID string) (string, error) + Delete(ctx context.Context, captchaID string) error + CleanExpired(ctx context.Context) error +} + +type captchaRepository struct { + db *gorm.DB +} + +type Captcha struct { + ID string `gorm:"primarykey;size:32" json:"id"` + Text string `gorm:"size:10;not null" json:"-"` + ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` +} + +func NewCaptchaRepository(db *gorm.DB) CaptchaRepository { + return &captchaRepository{ + db: db, + } +} + +func (r *captchaRepository) Create(ctx context.Context, captchaID, captchaText string, expiresAt time.Time) error { + captcha := &Captcha{ + ID: captchaID, + Text: captchaText, + ExpiresAt: expiresAt, + CreatedAt: time.Now(), + } + + return r.db.WithContext(ctx).Create(captcha).Error +} + +func (r *captchaRepository) Get(ctx context.Context, captchaID string) (string, error) { + var captcha Captcha + err := r.db.WithContext(ctx).Where("id = ? AND expires_at > ?", captchaID, time.Now()).First(&captcha).Error + if err != nil { + return "", err + } + + // 获取后立即删除,确保一次性使用 + r.db.WithContext(ctx).Delete(&captcha) + + return captcha.Text, nil +} + +func (r *captchaRepository) Delete(ctx context.Context, captchaID string) error { + return r.db.WithContext(ctx).Where("id = ?", captchaID).Delete(&Captcha{}).Error +} + +func (r *captchaRepository) CleanExpired(ctx context.Context) error { + return r.db.WithContext(ctx).Where("expires_at <= ?", time.Now()).Delete(&Captcha{}).Error +} diff --git a/gofaster/backend/internal/auth/repository/role_repo.go b/gofaster/backend/internal/auth/repository/role_repo.go index f962d47..755ebb9 100644 --- a/gofaster/backend/internal/auth/repository/role_repo.go +++ b/gofaster/backend/internal/auth/repository/role_repo.go @@ -70,12 +70,3 @@ func (r *RoleRepo) AssignPermissions(roleID uint, permissions []model.Permission } return r.DB().Model(&model.Role{BaseModel: baseModel.BaseModel{ID: roleID}}).Association("Permissions").Replace(permissions) } - -// internal/repository/user_repo.go -func (r *UserRepo) FindByUsername(username string) (*model.User, error) { - var user model.User - if err := r.DB().Where("username = ?", username).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} diff --git a/gofaster/backend/internal/auth/repository/user_repo.go b/gofaster/backend/internal/auth/repository/user_repo.go index afeb186..bf0aea0 100644 --- a/gofaster/backend/internal/auth/repository/user_repo.go +++ b/gofaster/backend/internal/auth/repository/user_repo.go @@ -1,63 +1,132 @@ package repository import ( + "context" "gofaster/internal/auth/model" - baseModel "gofaster/internal/shared/model" - "gofaster/internal/shared/repository" "gorm.io/gorm" ) -type UserRepo struct { - repository.BaseRepo +type UserRepository interface { + Create(ctx context.Context, user *model.User) error + GetByID(ctx context.Context, id uint) (*model.User, error) + GetByUsername(ctx context.Context, username string) (*model.User, error) + GetByEmail(ctx context.Context, email string) (*model.User, error) + Update(ctx context.Context, user *model.User) error + Delete(ctx context.Context, id uint) error + List(ctx context.Context, offset, limit int) ([]*model.User, int64, error) + + // 登录相关方法 + IncrementPasswordError(ctx context.Context, userID uint) error + ResetPasswordError(ctx context.Context, userID uint) error + UpdateLastLogin(ctx context.Context, userID uint, ip string) error + GetUserWithRoles(ctx context.Context, userID uint) (*model.User, error) +} + +type userRepository struct { + db *gorm.DB } -func NewUserRepo(db *gorm.DB) *UserRepo { - return &UserRepo{BaseRepo: *repository.NewBaseRepo(db)} +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{ + db: db, + } } -func (r *UserRepo) Create(user *model.User) error { - return r.DB().Create(user).Error +func (r *userRepository) Create(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx).Create(user).Error } -func (r *UserRepo) GetByID(id uint) (*model.User, error) { +func (r *userRepository) GetByID(ctx context.Context, id uint) (*model.User, error) { var user model.User - err := r.DB().Preload("Roles.Permissions").First(&user, id).Error - return &user, err + err := r.db.WithContext(ctx).First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil } -func (r *UserRepo) GetByUsername(username string) (*model.User, error) { +func (r *userRepository) GetByUsername(ctx context.Context, username string) (*model.User, error) { var user model.User - err := r.DB().Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error - return &user, err + err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { + var user model.User + err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil } -func (r *UserRepo) Update(user *model.User) error { - return r.DB().Save(user).Error +func (r *userRepository) Update(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx).Save(user).Error } -func (r *UserRepo) Delete(id uint) error { - return r.DB().Delete(&model.User{}, id).Error +func (r *userRepository) Delete(ctx context.Context, id uint) error { + return r.db.WithContext(ctx).Delete(&model.User{}, id).Error } -func (r *UserRepo) List(page, pageSize int) ([]*model.User, int64, error) { +func (r *userRepository) List(ctx context.Context, offset, limit int) ([]*model.User, int64, error) { var users []*model.User - var count int64 + var total int64 - err := r.DB().Model(&model.User{}).Count(&count).Error + err := r.db.WithContext(ctx).Model(&model.User{}).Count(&total).Error if err != nil { return nil, 0, err } - offset := (page - 1) * pageSize - err = r.DB().Preload("Roles").Offset(offset).Limit(pageSize).Find(&users).Error - return users, count, err + err = r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error + if err != nil { + return nil, 0, err + } + + return users, total, nil +} + +// IncrementPasswordError 增加密码错误次数 +func (r *userRepository) IncrementPasswordError(ctx context.Context, userID uint) error { + return r.db.WithContext(ctx).Model(&model.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "password_error_count": gorm.Expr("password_error_count + 1"), + }).Error } -func (r *UserRepo) AssignRole(userID uint, roleIDs []uint) error { - var roles []model.Role - if err := r.DB().Find(&roles, roleIDs).Error; err != nil { - return err +// ResetPasswordError 重置密码错误次数 +func (r *userRepository) ResetPasswordError(ctx context.Context, userID uint) error { + return r.db.WithContext(ctx).Model(&model.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "password_error_count": 0, + "status": 1, + "locked_at": nil, + }).Error +} + +// UpdateLastLogin 更新最后登录信息 +func (r *userRepository) UpdateLastLogin(ctx context.Context, userID uint, ip string) error { + return r.db.WithContext(ctx).Model(&model.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "last_login_at": gorm.Expr("NOW()"), + "last_login_ip": ip, + }).Error +} + +// GetUserWithRoles 获取用户及其角色信息 +func (r *userRepository) GetUserWithRoles(ctx context.Context, userID uint) (*model.User, error) { + var user model.User + err := r.db.WithContext(ctx). + Preload("Roles"). + First(&user, userID).Error + if err != nil { + return nil, err } - return r.DB().Model(&model.User{BaseModel: baseModel.BaseModel{ID: userID}}).Association("Roles").Replace(roles) + return &user, nil } diff --git a/gofaster/backend/internal/auth/routes/auth_routes.go b/gofaster/backend/internal/auth/routes/auth_routes.go new file mode 100644 index 0000000..6140b1f --- /dev/null +++ b/gofaster/backend/internal/auth/routes/auth_routes.go @@ -0,0 +1,44 @@ +package routes + +import ( + "gofaster/internal/auth/controller" + "gofaster/internal/auth/service" + "gofaster/internal/auth/repository" + "gofaster/internal/shared/middleware" + "gofaster/internal/shared/jwt" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// RegisterAuthRoutes 注册认证相关路由 +func RegisterAuthRoutes(r *gin.RouterGroup, db *gorm.DB, jwtConfig middleware.JWTConfig) { + // 创建仓储层实例 + userRepo := repository.NewUserRepository(db) + captchaRepo := repository.NewCaptchaRepository(db) + + // 创建JWT管理器 + jwtManager := jwt.NewJWTManager(jwtConfig.SecretKey, jwtConfig.Issuer) + + // 创建服务层实例 + authService := service.NewAuthService(userRepo, captchaRepo, jwtManager) + + // 创建控制器实例 + authController := controller.NewAuthController(authService) + + // 认证路由组 + auth := r.Group("/auth") + { + // 公开接口(无需认证) + auth.POST("/login", authController.Login) // 用户登录 + auth.GET("/captcha", authController.GenerateCaptcha) // 生成验证码 + + // 需要认证的接口 + auth.Use(middleware.JWTAuth(jwtConfig)) + { + auth.POST("/logout", authController.Logout) // 用户登出 + auth.POST("/refresh", authController.RefreshToken) // 刷新令牌 + auth.GET("/userinfo", authController.GetUserInfo) // 获取用户信息 + } + } +} diff --git a/gofaster/backend/internal/auth/service/auth_service.go b/gofaster/backend/internal/auth/service/auth_service.go new file mode 100644 index 0000000..f60fe2f --- /dev/null +++ b/gofaster/backend/internal/auth/service/auth_service.go @@ -0,0 +1,315 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + mathrand "math/rand" + "net/http" + "strings" + "time" + + "gofaster/internal/auth/model" + "gofaster/internal/auth/repository" + "gofaster/internal/shared/jwt" + + "golang.org/x/crypto/bcrypt" +) + +type AuthService interface { + Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) + Logout(ctx context.Context, token string) error + RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) + GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) + ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error + GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) +} + +type authService struct { + userRepo repository.UserRepository + captchaRepo repository.CaptchaRepository + jwtManager jwt.JWTManager +} + +func NewAuthService(userRepo repository.UserRepository, captchaRepo repository.CaptchaRepository, jwtManager jwt.JWTManager) AuthService { + return &authService{ + userRepo: userRepo, + captchaRepo: captchaRepo, + jwtManager: jwtManager, + } +} + +// Login 用户登录 +func (s *authService) Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) { + // 1. 验证验证码 + if err := s.ValidateCaptcha(ctx, req.Username, req.Captcha); err != nil { + return nil, fmt.Errorf("验证码错误: %w", err) + } + + // 2. 根据用户名查找用户 + user, err := s.userRepo.GetByUsername(ctx, req.Username) + if err != nil { + return nil, fmt.Errorf("用户不存在") + } + + // 3. 检查用户状态 + if !user.CanLogin() { + if user.IsLocked() { + return nil, fmt.Errorf("账户已被锁定,请30分钟后再试") + } + return nil, fmt.Errorf("账户已被禁用") + } + + // 4. 验证密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + // 密码错误,增加错误次数 + if err := s.userRepo.IncrementPasswordError(ctx, user.ID); err != nil { + return nil, fmt.Errorf("系统错误,请稍后重试") + } + + // 检查是否被锁定 + if user.PasswordErrorCount >= 4 { // 已经是第5次错误 + return nil, fmt.Errorf("密码错误次数过多,账户已被锁定30分钟") + } + + remaining := 5 - user.PasswordErrorCount - 1 + return nil, fmt.Errorf("密码错误,还可尝试%d次", remaining) + } + + // 5. 密码正确,重置错误次数并更新登录信息 + if err := s.userRepo.ResetPasswordError(ctx, user.ID); err != nil { + return nil, fmt.Errorf("系统错误,请稍后重试") + } + + if err := s.userRepo.UpdateLastLogin(ctx, user.ID, clientIP); err != nil { + // 登录信息更新失败不影响登录流程 + fmt.Printf("更新登录信息失败: %v\n", err) + } + + // 6. 生成JWT令牌 + claims := map[string]interface{}{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + } + + token, err := s.jwtManager.GenerateToken(claims, 24*time.Hour) // 24小时有效期 + if err != nil { + return nil, fmt.Errorf("生成令牌失败: %w", err) + } + + refreshToken, err := s.jwtManager.GenerateToken(claims, 7*24*time.Hour) // 7天有效期 + if err != nil { + return nil, fmt.Errorf("生成刷新令牌失败: %w", err) + } + + // 7. 获取用户角色信息 + userWithRoles, err := s.userRepo.GetUserWithRoles(ctx, user.ID) + if err != nil { + return nil, fmt.Errorf("获取用户信息失败: %w", err) + } + + // 8. 构建响应 + userInfo := s.buildUserInfo(userWithRoles) + + return &model.LoginResponse{ + Token: token, + TokenType: "Bearer", + ExpiresIn: 24 * 60 * 60, // 24小时,单位秒 + RefreshToken: refreshToken, + User: *userInfo, + }, nil +} + +// Logout 用户登出 +func (s *authService) Logout(ctx context.Context, token string) error { + // 这里可以实现令牌黑名单机制 + // 目前简单返回成功 + return nil +} + +// RefreshToken 刷新令牌 +func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) { + // 验证刷新令牌 + claims, err := s.jwtManager.ValidateToken(refreshToken) + if err != nil { + return nil, fmt.Errorf("刷新令牌无效: %w", err) + } + + // 获取用户信息 + userID, ok := claims["user_id"].(float64) + if !ok { + return nil, fmt.Errorf("令牌格式错误") + } + + user, err := s.userRepo.GetUserWithRoles(ctx, uint(userID)) + if err != nil { + return nil, fmt.Errorf("用户不存在: %w", err) + } + + // 检查用户状态 + if !user.CanLogin() { + return nil, fmt.Errorf("用户状态异常") + } + + // 生成新的访问令牌 + newClaims := map[string]interface{}{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + } + + newToken, err := s.jwtManager.GenerateToken(newClaims, 24*time.Hour) + if err != nil { + return nil, fmt.Errorf("生成新令牌失败: %w", err) + } + + // 构建响应 + userInfo := s.buildUserInfo(user) + + return &model.LoginResponse{ + Token: newToken, + TokenType: "Bearer", + ExpiresIn: 24 * 60 * 60, + RefreshToken: refreshToken, // 保持原刷新令牌 + User: *userInfo, + }, nil +} + +// GenerateCaptcha 生成验证码 +func (s *authService) GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) { + // 生成随机验证码ID + captchaID, err := s.generateRandomID() + if err != nil { + return nil, fmt.Errorf("生成验证码ID失败: %w", err) + } + + // 生成4位随机验证码 + captchaText := s.generateRandomCaptcha(4) + + // 设置5分钟过期时间 + expiresAt := time.Now().Add(5 * time.Minute) + + // 保存到数据库 + if err := s.captchaRepo.Create(ctx, captchaID, captchaText, expiresAt); err != nil { + return nil, fmt.Errorf("保存验证码失败: %w", err) + } + + // 生成验证码图片(这里简化处理,实际应该生成图片) + captchaImage := s.generateCaptchaImage(captchaText) + + return &model.CaptchaResponse{ + CaptchaID: captchaID, + CaptchaImage: captchaImage, + ExpiresIn: 5 * 60, // 5分钟,单位秒 + }, nil +} + +// ValidateCaptcha 验证验证码 +func (s *authService) ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error { + // 从数据库获取验证码 + storedText, err := s.captchaRepo.Get(ctx, captchaID) + if err != nil { + return fmt.Errorf("验证码已过期或不存在") + } + + // 比较验证码(不区分大小写) + if !strings.EqualFold(storedText, captchaText) { + return fmt.Errorf("验证码错误") + } + + return nil +} + +// GetUserInfo 获取用户信息 +func (s *authService) GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) { + user, err := s.userRepo.GetUserWithRoles(ctx, userID) + if err != nil { + return nil, fmt.Errorf("用户不存在: %w", err) + } + + return s.buildUserInfo(user), nil +} + +// buildUserInfo 构建用户信息 +func (s *authService) buildUserInfo(user *model.User) *model.UserInfo { + roles := make([]model.RoleInfo, 0, len(user.Roles)) + for _, role := range user.Roles { + roles = append(roles, model.RoleInfo{ + ID: role.ID, + Name: role.Name, + Code: role.Code, + }) + } + + return &model.UserInfo{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: user.Status, + LastLoginAt: user.LastLoginAt, + LastLoginIP: user.LastLoginIP, + Roles: roles, + } +} + +// generateRandomID 生成随机ID +func (s *authService) generateRandomID() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// generateRandomCaptcha 生成随机验证码 +func (s *authService) generateRandomCaptcha(length int) string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + result := make([]byte, length) + for i := range result { + result[i] = chars[mathrand.Intn(len(chars))] + } + return string(result) +} + +// generateCaptchaImage 生成验证码图片(简化版本,返回Base64编码) +func (s *authService) generateCaptchaImage(text string) string { + // 这里应该使用图片生成库生成实际的验证码图片 + // 目前返回一个占位符 + return fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(text))) +} + +// GetClientIP 获取客户端IP地址 +func GetClientIP(r *http.Request) string { + // 尝试从各种头部获取真实IP + ip := r.Header.Get("X-Real-IP") + if ip != "" { + return ip + } + + ip = r.Header.Get("X-Forwarded-For") + if ip != "" { + // X-Forwarded-For可能包含多个IP,取第一个 + if idx := strings.Index(ip, ","); idx != -1 { + ip = ip[:idx] + } + return strings.TrimSpace(ip) + } + + ip = r.Header.Get("X-Forwarded") + if ip != "" { + return ip + } + + // 从RemoteAddr获取 + if r.RemoteAddr != "" { + if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr + } + + return "127.0.0.1" +} diff --git a/gofaster/backend/internal/auth/service/init_service.go b/gofaster/backend/internal/auth/service/init_service.go index b5ed115..beabb13 100644 --- a/gofaster/backend/internal/auth/service/init_service.go +++ b/gofaster/backend/internal/auth/service/init_service.go @@ -2,6 +2,7 @@ package service import ( + "context" "errors" "gofaster/internal/auth/model" "gofaster/internal/auth/repository" @@ -12,12 +13,12 @@ import ( type InitService struct { db *gorm.DB - userRepo *repository.UserRepo + userRepo repository.UserRepository roleRepo *repository.RoleRepo permissionRepo *repository.PermissionRepo } -func NewInitService(db *gorm.DB, userRepo *repository.UserRepo, roleRepo *repository.RoleRepo, permissionRepo *repository.PermissionRepo) *InitService { +func NewInitService(db *gorm.DB, userRepo repository.UserRepository, roleRepo *repository.RoleRepo, permissionRepo *repository.PermissionRepo) *InitService { return &InitService{ db: db, userRepo: userRepo, @@ -83,7 +84,7 @@ func (s *InitService) initAdminRole() (*model.Role, error) { // initSysAdminUser 初始化系统管理员用户 func (s *InitService) initSysAdminUser(adminRole *model.Role) error { // 检查用户是否已存在 - _, err := s.userRepo.FindByUsername("sysadmin") + _, err := s.userRepo.GetByUsername(context.Background(), "sysadmin") if err == nil { return nil // 用户已存在 } @@ -106,5 +107,5 @@ func (s *InitService) initSysAdminUser(adminRole *model.Role) error { Roles: []model.Role{*adminRole}, } - return s.userRepo.Create(adminUser) + return s.userRepo.Create(context.Background(), adminUser) } diff --git a/gofaster/backend/internal/auth/service/user_service.go b/gofaster/backend/internal/auth/service/user_service.go index 2c8127c..47cd4cd 100644 --- a/gofaster/backend/internal/auth/service/user_service.go +++ b/gofaster/backend/internal/auth/service/user_service.go @@ -1,34 +1,35 @@ package service import ( + "context" "gofaster/internal/auth/model" "gofaster/internal/auth/repository" ) type UserService struct { - repo *repository.UserRepo + repo repository.UserRepository } -func NewUserService(repo *repository.UserRepo) *UserService { +func NewUserService(repo repository.UserRepository) *UserService { return &UserService{repo: repo} } -func (s *UserService) CreateUser(user *model.User) error { - return s.repo.Create(user) +func (s *UserService) CreateUser(ctx context.Context, user *model.User) error { + return s.repo.Create(ctx, user) } -func (s *UserService) GetUserByID(id uint) (*model.User, error) { - return s.repo.GetByID(id) +func (s *UserService) GetUserByID(ctx context.Context, id uint) (*model.User, error) { + return s.repo.GetByID(ctx, id) } -func (s *UserService) UpdateUser(user *model.User) error { - return s.repo.Update(user) +func (s *UserService) UpdateUser(ctx context.Context, user *model.User) error { + return s.repo.Update(ctx, user) } -func (s *UserService) DeleteUser(id uint) error { - return s.repo.Delete(id) +func (s *UserService) DeleteUser(ctx context.Context, id uint) error { + return s.repo.Delete(ctx, id) } -func (s *UserService) ListUsers(page, pageSize int) ([]*model.User, int64, error) { - return s.repo.List(page, pageSize) +func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) { + return s.repo.List(ctx, page, pageSize) } diff --git a/gofaster/backend/internal/shared/config/config.go b/gofaster/backend/internal/shared/config/config.go index a46b2e8..2dc9c8a 100644 --- a/gofaster/backend/internal/shared/config/config.go +++ b/gofaster/backend/internal/shared/config/config.go @@ -35,6 +35,7 @@ type RedisConfig struct { type JWTConfig struct { Secret string + Issuer string Expire int // 小时 } @@ -50,6 +51,11 @@ func LoadConfig() (*Config, error) { viper.AddConfigPath(".") viper.AutomaticEnv() + // 设置默认值 + viper.SetDefault("jwt.secret", "your-secret-key") + viper.SetDefault("jwt.issuer", "gofaster") + viper.SetDefault("jwt.expire", 24) + if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("error reading config file: %w", err) } diff --git a/gofaster/backend/internal/shared/database/redis_client.go b/gofaster/backend/internal/shared/database/redis_client.go index 2840436..581ee67 100644 --- a/gofaster/backend/internal/shared/database/redis_client.go +++ b/gofaster/backend/internal/shared/database/redis_client.go @@ -40,7 +40,7 @@ func NewRedisClient(cfg *config.RedisConfig, logger *zap.Logger) *RedisClient { retries := 3 var lastErr error for i := 0; i < retries; i++ { - if _, err := client.Ping(ctx).Result(); err != nil { + if _, err := client.Ping(ctx).Result(); err != nil { lastErr = err logger.Warn("Redis connection attempt failed", zap.Int("attempt", i+1), @@ -48,15 +48,20 @@ func NewRedisClient(cfg *config.RedisConfig, logger *zap.Logger) *RedisClient { ) time.Sleep(1 * time.Second) // 延迟重试 continue - } + } break } if lastErr != nil { - logger.Fatal("Failed to connect to Redis after retries", + logger.Warn("Failed to connect to Redis after retries, continuing without Redis", zap.Int("retries", retries), zap.Error(lastErr), ) + // 返回一个空的 Redis 客户端,程序可以继续运行 + return &RedisClient{ + client: nil, + logger: logger, + } } // 监控连接状态 @@ -71,6 +76,10 @@ func NewRedisClient(cfg *config.RedisConfig, logger *zap.Logger) *RedisClient { // 监控 Redis 连接状态 func monitorConnection(client *redis.Client, logger *zap.Logger) { + if client == nil { + return // 如果没有 Redis 客户端,不进行监控 + } + ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() @@ -92,6 +101,10 @@ func (r *RedisClient) GetClient() *redis.Client { } func (r *RedisClient) Close() error { + if r.client == nil { + r.logger.Info("No Redis connection to close") + return nil + } r.logger.Info("Closing Redis connection") return r.client.Close() } diff --git a/gofaster/backend/internal/shared/jwt/jwt.go b/gofaster/backend/internal/shared/jwt/jwt.go index 096bafd..16bfa94 100644 --- a/gofaster/backend/internal/shared/jwt/jwt.go +++ b/gofaster/backend/internal/shared/jwt/jwt.go @@ -1,4 +1,4 @@ -package auth +package jwt import ( "time" diff --git a/gofaster/backend/internal/shared/jwt/jwt_manager.go b/gofaster/backend/internal/shared/jwt/jwt_manager.go new file mode 100644 index 0000000..f603d75 --- /dev/null +++ b/gofaster/backend/internal/shared/jwt/jwt_manager.go @@ -0,0 +1,91 @@ +package jwt + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// JWTManager JWT管理器接口 +type JWTManager interface { + GenerateToken(claims map[string]interface{}, expiration time.Duration) (string, error) + ValidateToken(tokenString string) (map[string]interface{}, error) + RefreshToken(tokenString string, expiration time.Duration) (string, error) +} + +// jwtManager JWT管理器实现 +type jwtManager struct { + secretKey []byte + issuer string +} + +// NewJWTManager 创建新的JWT管理器 +func NewJWTManager(secretKey string, issuer string) JWTManager { + return &jwtManager{ + secretKey: []byte(secretKey), + issuer: issuer, + } +} + +// GenerateToken 生成JWT令牌 +func (j *jwtManager) GenerateToken(claims map[string]interface{}, expiration time.Duration) (string, error) { + // 创建标准声明 + standardClaims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: j.issuer, + } + + // 合并自定义声明和标准声明 + allClaims := make(map[string]interface{}) + for k, v := range claims { + allClaims[k] = v + } + allClaims["exp"] = standardClaims.ExpiresAt + allClaims["iat"] = standardClaims.IssuedAt + allClaims["iss"] = standardClaims.Issuer + + // 创建令牌 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(allClaims)) + + // 签名令牌 + return token.SignedString(j.secretKey) +} + +// ValidateToken 验证JWT令牌 +func (j *jwtManager) ValidateToken(tokenString string) (map[string]interface{}, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + return j.secretKey, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } + + return nil, jwt.ErrInvalidKey +} + +// RefreshToken 刷新JWT令牌 +func (j *jwtManager) RefreshToken(tokenString string, expiration time.Duration) (string, error) { + // 验证原令牌 + claims, err := j.ValidateToken(tokenString) + if err != nil { + return "", err + } + + // 移除时间相关声明 + delete(claims, "exp") + delete(claims, "iat") + delete(claims, "nbf") + + // 生成新令牌 + return j.GenerateToken(claims, expiration) +} diff --git a/gofaster/backend/internal/shared/middleware/jwt_middleware.go b/gofaster/backend/internal/shared/middleware/jwt_middleware.go new file mode 100644 index 0000000..4874253 --- /dev/null +++ b/gofaster/backend/internal/shared/middleware/jwt_middleware.go @@ -0,0 +1,157 @@ +package middleware + +import ( + "strconv" + "strings" + + "gofaster/internal/shared/jwt" + "gofaster/internal/shared/response" + + "github.com/gin-gonic/gin" +) + +// JWTConfig JWT中间件配置 +type JWTConfig struct { + SecretKey string + Issuer string +} + +// JWTAuth JWT认证中间件 +func JWTAuth(config JWTConfig) gin.HandlerFunc { + return func(c *gin.Context) { + // 获取Authorization头部 + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + response.Unauthorized(c, "未提供认证令牌", "Authorization头部缺失") + c.Abort() + return + } + + // 检查Bearer前缀 + if !strings.HasPrefix(authHeader, "Bearer ") { + response.Unauthorized(c, "认证令牌格式错误", "令牌必须以Bearer开头") + c.Abort() + return + } + + // 提取令牌 + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + // 创建JWT管理器 + jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) + + // 验证令牌 + claims, err := jwtManager.ValidateToken(tokenString) + if err != nil { + response.Unauthorized(c, "认证令牌无效", err.Error()) + c.Abort() + return + } + + // 将用户信息存储到上下文中 + if userID, exists := claims["user_id"]; exists { + c.Set("user_id", userID) + } + if username, exists := claims["username"]; exists { + c.Set("username", username) + } + if email, exists := claims["email"]; exists { + c.Set("email", email) + } + + // 继续处理请求 + c.Next() + } +} + +// OptionalJWTAuth 可选的JWT认证中间件(不强制要求认证) +func OptionalJWTAuth(config JWTConfig) gin.HandlerFunc { + return func(c *gin.Context) { + // 获取Authorization头部 + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // 没有令牌,继续处理请求 + c.Next() + return + } + + // 检查Bearer前缀 + if !strings.HasPrefix(authHeader, "Bearer ") { + // 令牌格式错误,继续处理请求(不强制要求) + c.Next() + return + } + + // 提取令牌 + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + // 创建JWT管理器 + jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) + + // 验证令牌 + claims, err := jwtManager.ValidateToken(tokenString) + if err != nil { + // 令牌无效,继续处理请求(不强制要求) + c.Next() + return + } + + // 将用户信息存储到上下文中 + if userID, exists := claims["user_id"]; exists { + c.Set("user_id", userID) + } + if username, exists := claims["username"]; exists { + c.Set("username", username) + } + if email, exists := claims["email"]; exists { + c.Set("email", email) + } + + // 继续处理请求 + c.Next() + } +} + +// GetUserID 从上下文中获取用户ID +func GetUserID(c *gin.Context) (uint, bool) { + userID, exists := c.Get("user_id") + if !exists { + return 0, false + } + + switch v := userID.(type) { + case float64: + return uint(v), true + case int: + return uint(v), true + case string: + if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { + return uint(parsed), true + } + } + return 0, false +} + +// GetUsername 从上下文中获取用户名 +func GetUsername(c *gin.Context) (string, bool) { + username, exists := c.Get("username") + if !exists { + return "", false + } + if str, ok := username.(string); ok { + return str, true + } + return "", false +} + +// GetEmail 从上下文中获取邮箱 +func GetEmail(c *gin.Context) (string, bool) { + email, exists := c.Get("email") + if !exists { + return "", false + } + if str, ok := email.(string); ok { + return str, true + } + return "", false +} diff --git a/gofaster/backend/internal/shared/response/response.go b/gofaster/backend/internal/shared/response/response.go new file mode 100644 index 0000000..53c4641 --- /dev/null +++ b/gofaster/backend/internal/shared/response/response.go @@ -0,0 +1,63 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Response 统一响应结构 +type Response struct { + Code int `json:"code"` // 响应状态码 + Message string `json:"message"` // 响应消息 + Data interface{} `json:"data,omitempty"` // 响应数据 + Error string `json:"error,omitempty"` // 错误信息 +} + +// Success 成功响应 +func Success(c *gin.Context, message string, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Message: message, + Data: data, + }) +} + +// Error 错误响应 +func Error(c *gin.Context, code int, message string, error string) { + c.JSON(code, Response{ + Code: code, + Message: message, + Error: error, + }) +} + +// BadRequest 400 错误响应 +func BadRequest(c *gin.Context, message string, error string) { + Error(c, http.StatusBadRequest, message, error) +} + +// Unauthorized 401 错误响应 +func Unauthorized(c *gin.Context, message string, error string) { + Error(c, http.StatusUnauthorized, message, error) +} + +// Forbidden 403 错误响应 +func Forbidden(c *gin.Context, message string, error string) { + Error(c, http.StatusForbidden, message, error) +} + +// NotFound 404 错误响应 +func NotFound(c *gin.Context, message string, error string) { + Error(c, http.StatusNotFound, message, error) +} + +// InternalServerError 500 错误响应 +func InternalServerError(c *gin.Context, message string, error string) { + Error(c, http.StatusInternalServerError, message, error) +} + +// Locked 423 错误响应(账户被锁定) +func Locked(c *gin.Context, message string, error string) { + Error(c, http.StatusLocked, message, error) +}