Browse Source

后台增加登录服务支持

master
hejl 2 weeks ago
parent
commit
8be18637de
  1. 240
      gofaster/backend/README_AUTH.md
  2. 1
      gofaster/backend/config.yaml
  3. 35
      gofaster/backend/go.mod
  4. 16
      gofaster/backend/go.sum
  5. BIN
      gofaster/backend/gofaster.exe
  6. 225
      gofaster/backend/internal/auth/controller/auth_controller.go
  7. 10
      gofaster/backend/internal/auth/controller/user_controller.go
  8. 115
      gofaster/backend/internal/auth/migration/migration.go
  9. 55
      gofaster/backend/internal/auth/model/auth.go
  10. 1
      gofaster/backend/internal/auth/model/role.go
  11. 52
      gofaster/backend/internal/auth/model/user.go
  12. 10
      gofaster/backend/internal/auth/model/user_role.go
  13. 30
      gofaster/backend/internal/auth/module.go
  14. 63
      gofaster/backend/internal/auth/repository/captcha_repo.go
  15. 9
      gofaster/backend/internal/auth/repository/role_repo.go
  16. 127
      gofaster/backend/internal/auth/repository/user_repo.go
  17. 44
      gofaster/backend/internal/auth/routes/auth_routes.go
  18. 315
      gofaster/backend/internal/auth/service/auth_service.go
  19. 9
      gofaster/backend/internal/auth/service/init_service.go
  20. 25
      gofaster/backend/internal/auth/service/user_service.go
  21. 6
      gofaster/backend/internal/shared/config/config.go
  22. 19
      gofaster/backend/internal/shared/database/redis_client.go
  23. 2
      gofaster/backend/internal/shared/jwt/jwt.go
  24. 91
      gofaster/backend/internal/shared/jwt/jwt_manager.go
  25. 157
      gofaster/backend/internal/shared/middleware/jwt_middleware.go
  26. 63
      gofaster/backend/internal/shared/response/response.go

240
gofaster/backend/README_AUTH.md

@ -0,0 +1,240 @@ @@ -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. **更新**: 定期更新依赖包以修复安全漏洞

1
gofaster/backend/config.yaml

@ -17,6 +17,7 @@ redis: @@ -17,6 +17,7 @@ redis:
jwt:
secret: "your-secret-key"
issuer: "gofaster"
expire: 24 # 24小时
log:

35
gofaster/backend/go.mod

@ -2,35 +2,24 @@ module gofaster @@ -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 ( @@ -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 ( @@ -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
)

16
gofaster/backend/go.sum

@ -1,9 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -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=

BIN
gofaster/backend/gofaster.exe

Binary file not shown.

225
gofaster/backend/internal/auth/controller/auth_controller.go

@ -0,0 +1,225 @@ @@ -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(), "验证码错误")
}

10
gofaster/backend/internal/auth/controller/user_controller.go

@ -60,7 +60,7 @@ func (c *UserController) ListUsers(ctx *gin.Context) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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

115
gofaster/backend/internal/auth/migration/migration.go

@ -0,0 +1,115 @@ @@ -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
}

55
gofaster/backend/internal/auth/model/auth.go

@ -0,0 +1,55 @@ @@ -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"`
}

1
gofaster/backend/internal/auth/model/role.go

@ -7,6 +7,7 @@ import ( @@ -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"`
}

52
gofaster/backend/internal/auth/model/user.go

@ -2,14 +2,54 @@ package model @@ -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
}
}

10
gofaster/backend/internal/auth/model/user_role.go

@ -1,6 +1,12 @@ @@ -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"`
}

30
gofaster/backend/internal/auth/module.go

@ -1,14 +1,10 @@ @@ -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 ( @@ -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 { @@ -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() {

63
gofaster/backend/internal/auth/repository/captcha_repo.go

@ -0,0 +1,63 @@ @@ -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
}

9
gofaster/backend/internal/auth/repository/role_repo.go

@ -70,12 +70,3 @@ func (r *RoleRepo) AssignPermissions(roleID uint, permissions []model.Permission @@ -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
}

127
gofaster/backend/internal/auth/repository/user_repo.go

@ -1,63 +1,132 @@ @@ -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
}

44
gofaster/backend/internal/auth/routes/auth_routes.go

@ -0,0 +1,44 @@ @@ -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) // 获取用户信息
}
}
}

315
gofaster/backend/internal/auth/service/auth_service.go

@ -0,0 +1,315 @@ @@ -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"
}

9
gofaster/backend/internal/auth/service/init_service.go

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
package service
import (
"context"
"errors"
"gofaster/internal/auth/model"
"gofaster/internal/auth/repository"
@ -12,12 +13,12 @@ import ( @@ -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) { @@ -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 { @@ -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)
}

25
gofaster/backend/internal/auth/service/user_service.go

@ -1,34 +1,35 @@ @@ -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)
}

6
gofaster/backend/internal/shared/config/config.go

@ -35,6 +35,7 @@ type RedisConfig struct { @@ -35,6 +35,7 @@ type RedisConfig struct {
type JWTConfig struct {
Secret string
Issuer string
Expire int // 小时
}
@ -50,6 +51,11 @@ func LoadConfig() (*Config, error) { @@ -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)
}

19
gofaster/backend/internal/shared/database/redis_client.go

@ -40,7 +40,7 @@ func NewRedisClient(cfg *config.RedisConfig, logger *zap.Logger) *RedisClient { @@ -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 { @@ -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 { @@ -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 { @@ -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()
}

2
gofaster/backend/internal/shared/jwt/jwt.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package auth
package jwt
import (
"time"

91
gofaster/backend/internal/shared/jwt/jwt_manager.go

@ -0,0 +1,91 @@ @@ -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)
}

157
gofaster/backend/internal/shared/middleware/jwt_middleware.go

@ -0,0 +1,157 @@ @@ -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
}

63
gofaster/backend/internal/shared/response/response.go

@ -0,0 +1,63 @@ @@ -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)
}
Loading…
Cancel
Save