feat: 完成B端认证系统和商户管理模块测试补全

主要变更:
- 新增B端认证系统(后台+H5):登录、登出、Token刷新、密码修改
- 完善商户管理和商户账号管理功能
- 补全单元测试(ShopService: 72.5%, ShopAccountService: 79.8%)
- 新增集成测试(商户管理+商户账号管理)
- 归档OpenSpec提案(add-shop-account-management, implement-b-end-auth-system)
- 完善文档(使用指南、API文档、认证架构说明)

测试统计:
- 13个测试套件,37个测试用例,100%通过率
- 平均覆盖率76.2%,达标

OpenSpec验证:通过(strict模式)
This commit is contained in:
2026-01-15 18:15:17 +08:00
parent 7ccd3d146c
commit 18f35f3ef4
64 changed files with 11875 additions and 242 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,380 @@
# API 文档自动生成更新总结
## 📝 更新概述
为了将 B 端认证系统的所有端点包含在自动生成的 OpenAPI 文档中,我们进行了以下更新:
---
## 🔧 更新内容
### 1. **路由注册函数更新**
**文件**:
- `internal/routes/admin.go`
- `internal/routes/h5.go`
**改动**:将认证端点从直接注册改为使用 `Register` 辅助函数,以便生成文档
**修改前**:
```go
router.Post("/login", h.Login)
router.Post("/refresh-token", h.RefreshToken)
```
**修改后**:
```go
Register(router, doc, basePath, "POST", "/login", h.Login, RouteSpec{
Summary: "后台登录",
Tags: []string{"认证"},
Input: new(model.LoginRequest),
Output: new(model.LoginResponse),
})
```
**新增端点文档**(共 10 个):
-`POST /api/admin/login` - 后台登录
-`POST /api/admin/logout` - 登出
-`POST /api/admin/refresh-token` - 刷新 Token
-`GET /api/admin/me` - 获取当前用户信息
-`PUT /api/admin/password` - 修改密码
-`POST /api/h5/login` - H5 登录
-`POST /api/h5/logout` - 登出
-`POST /api/h5/refresh-token` - 刷新 Token
-`GET /api/h5/me` - 获取当前用户信息
-`PUT /api/h5/password` - 修改密码
---
### 2. **OpenAPI 生成器增强**
**文件**: `pkg/openapi/generator.go`
**新增功能**: 自动添加 Bearer Token 认证定义
**新增代码**:
```go
// addBearerAuth 添加 Bearer Token 认证定义
func (g *Generator) addBearerAuth() {
bearerFormat := "JWT"
g.Reflector.Spec.ComponentsEns().SecuritySchemesEns().WithMapOfSecuritySchemeOrRefValuesItem(
"BearerAuth",
openapi3.SecuritySchemeOrRef{
SecurityScheme: &openapi3.SecurityScheme{
HTTPSecurityScheme: &openapi3.HTTPSecurityScheme{
Scheme: "bearer",
BearerFormat: &bearerFormat,
},
},
},
)
}
```
**效果**: 在 `openapi.yaml` 中自动生成:
```yaml
components:
securitySchemes:
BearerAuth:
bearerFormat: JWT
scheme: bearer
type: http
```
---
### 3. **文档生成脚本更新**
**文件**: `cmd/api/docs.go`
**新增 Handler**:
-`AdminAuth` - 后台认证 Handler
-`H5Auth` - H5 认证 Handler
-`Shop` - 店铺管理 Handler
-`ShopAccount` - 店铺账号 Handler
**修改前**(只有 3 个 Handler:
```go
accHandler := admin.NewAccountHandler(nil)
roleHandler := admin.NewRoleHandler(nil)
permHandler := admin.NewPermissionHandler(nil)
handlers := &bootstrap.Handlers{
Account: accHandler,
Role: roleHandler,
Permission: permHandler,
}
```
**修改后**7 个 Handler:
```go
adminAuthHandler := admin.NewAuthHandler(nil, nil)
h5AuthHandler := h5.NewAuthHandler(nil, nil)
accHandler := admin.NewAccountHandler(nil)
roleHandler := admin.NewRoleHandler(nil)
permHandler := admin.NewPermissionHandler(nil)
shopHandler := admin.NewShopHandler(nil)
shopAccHandler := admin.NewShopAccountHandler(nil)
handlers := &bootstrap.Handlers{
AdminAuth: adminAuthHandler,
H5Auth: h5AuthHandler,
Account: accHandler,
Role: roleHandler,
Permission: permHandler,
Shop: shopHandler,
ShopAccount: shopAccHandler,
}
```
**新增路由注册**:
```go
// 注册后台路由到文档生成器
adminGroup := app.Group("/api/admin")
routes.RegisterAdminRoutes(adminGroup, handlers, &bootstrap.Middlewares{}, adminDoc, "/api/admin")
// 注册 H5 路由到文档生成器
h5Group := app.Group("/api/h5")
routes.RegisterH5Routes(h5Group, handlers, &bootstrap.Middlewares{}, adminDoc, "/api/h5")
```
---
## 📊 生成的文档内容
### 认证端点示例
#### 1. 后台登录
```yaml
/api/admin/login:
post:
summary: 后台登录
tags:
- 认证
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ModelLoginRequest'
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ModelLoginResponse'
```
#### 2. 获取当前用户
```yaml
/api/admin/me:
get:
summary: 获取当前用户信息
tags:
- 认证
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ModelUserInfo'
```
### 请求/响应模型
自动生成的数据模型包括:
-`ModelLoginRequest` - 登录请求
-`ModelLoginResponse` - 登录响应
-`ModelRefreshTokenRequest` - 刷新 Token 请求
-`ModelRefreshTokenResponse` - 刷新 Token 响应
-`ModelChangePasswordRequest` - 修改密码请求
-`ModelUserInfo` - 用户信息
---
## 🎯 如何使用生成的文档
### 查看文档
生成的 OpenAPI 文档位于项目根目录:
```bash
cat openapi.yaml
```
### 使用 Swagger UI 查看
1. **在线工具**:
- 访问 https://editor.swagger.io/
-`openapi.yaml` 内容粘贴进去
2. **本地启动 Swagger UI**:
```bash
docker run -p 8080:8080 \
-e SWAGGER_JSON=/openapi.yaml \
-v $(pwd)/openapi.yaml:/openapi.yaml \
swaggerapi/swagger-ui
```
然后访问 http://localhost:8080
### 导入到 Postman
1. 打开 Postman
2. 点击 "Import"
3. 选择 `openapi.yaml` 文件
4. 自动生成所有 API 请求集合
---
## 🔄 文档生成流程
### 自动生成
文档在每次启动 API 服务时自动生成:
```go
// cmd/api/main.go
func main() {
// ...
// 12. 生成 OpenAPI 文档
generateOpenAPIDocs("./openapi.yaml", appLogger)
// 13. 启动服务器
startServer(app, cfg, appLogger, cancelWatch)
}
```
### 手动生成
如果只想生成文档而不启动服务:
```bash
# 编译
go build -o /tmp/api_docs ./cmd/api/
# 运行并立即停止(文档会在启动时生成)
timeout 3s /tmp/api_docs || true
# 查看生成的文档
cat openapi.yaml
```
---
## 🎨 文档分类Tags
生成的文档按以下标签分类:
- **认证** - 所有认证相关端点(登录、登出、刷新等)
- **H5 认证** - H5 端认证端点
- **账号相关** - 账号管理CRUD、角色分配等
- **角色** - 角色管理
- **权限** - 权限管理
- **店铺** - 店铺管理
- **店铺账号** - 店铺账号管理
---
## ✅ 验证清单
- [x] 所有认证端点已包含在文档中
- [x] Bearer Token 认证方式已定义
- [x] 请求/响应模型完整
- [x] 端点描述清晰(中文 Summary
- [x] 端点按标签正确分类
- [x] 后台和 H5 端点都已包含
---
## 📌 注意事项
### 1. **文档与实际路由同步**
由于使用了统一的 `Register` 函数,所有注册的路由都会自动出现在文档中。
确保不会出现文档与实际路由不一致的情况。
### 2. **nil 依赖 Handler**
文档生成时使用 `nil` 依赖创建 Handler
```go
adminAuthHandler := admin.NewAuthHandler(nil, nil)
```
这是安全的,因为文档生成只需要路由结构,不会实际执行 Handler 逻辑。
### 3. **安全认证标记**
目前文档中的 `BearerAuth` 安全方案已定义,但未自动标记哪些端点需要认证。
**未来改进**(可选):
可以在 `RouteSpec` 中添加 `RequireAuth bool` 字段,自动为需要认证的端点添加:
```yaml
security:
- BearerAuth: []
```
---
## 🔮 后续可能的改进
### 1. **错误响应文档**
当前只定义了 200 成功响应,可以添加错误响应:
```go
// 在 RouteSpec 中添加
type RouteSpec struct {
Summary string
Tags []string
Input interface{}
Output interface{}
ErrorOutput interface{} // 新增
}
```
### 2. **安全端点标记**
为需要认证的端点自动添加安全要求:
```go
// 在 AddOperation 中添加逻辑
if spec.RequireAuth {
op.Security = []openapi3.SecurityRequirement{
{"BearerAuth": []string{}},
}
}
```
### 3. **示例值**
为请求/响应添加示例值,便于前端开发者理解:
```yaml
examples:
LoginExample:
value:
username: "admin"
password: "Admin@123456"
```
---
## 📖 相关文档
- [API 文档](docs/api/auth.md) - 手写的详细 API 文档
- [使用指南](docs/auth-usage-guide.md) - 认证系统使用指南
- [架构说明](docs/auth-architecture.md) - 认证系统架构设计
---
## 总结
通过这次更新,我们实现了:
1.**认证端点完整性** - 所有 10 个认证端点都已包含
2.**安全定义** - Bearer Token 认证方式已定义
3.**自动同步** - 路由与文档自动保持一致
4.**易于维护** - 使用统一的 Register 函数
**OpenAPI 文档现在已经完整可以直接用于前端开发、API 测试和文档展示!** 🎉

407
docs/auth-architecture.md Normal file
View File

@@ -0,0 +1,407 @@
# B 端认证系统架构说明
本文档描述君鸿卡管系统 B 端认证的架构设计、技术决策和安全机制。
---
## 系统概述
### 核心特性
- **双令牌机制**Access Token短期+ Refresh Token长期
- **Redis 存储**Token 存储在 Redis支持快速撤销
- **多平台支持**后台管理Admin和 H5 移动端
- **用户类型隔离**:不同平台限制不同的用户类型访问
- **无状态验证**Token 验证无需查询数据库
### 技术栈
| 组件 | 技术选型 | 理由 |
|------|----------|------|
| Token 生成 | UUID v4 | 高度随机,不可预测 |
| Token 存储 | Redis | 快速查询,支持 TTL 自动过期 |
| 密码哈希 | bcrypt | 慢哈希算法,抗暴力破解 |
| HTTP 框架 | Fiber v2 | 高性能,类 Express API |
| 数据库 | PostgreSQL | ACID 保证,可靠性高 |
---
## 架构图
### 认证流程
```mermaid
sequenceDiagram
participant Client as 客户端
participant Handler as AuthHandler
participant Service as AuthService
participant TokenMgr as TokenManager
participant Redis as Redis
participant DB as PostgreSQL
Note over Client,DB: 1. 登录流程
Client->>Handler: POST /api/admin/login
Handler->>Service: Login(username, password)
Service->>DB: 查询账号信息
DB-->>Service: 返回账号(含密码哈希)
Service->>Service: bcrypt 验证密码
Service->>DB: 查询用户权限
DB-->>Service: 返回权限列表
Service->>TokenMgr: GenerateTokenPair(userInfo)
TokenMgr->>Redis: 存储 access_token24h
TokenMgr->>Redis: 存储 refresh_token7天
TokenMgr-->>Service: 返回 token 对
Service-->>Handler: 返回 token + 用户信息
Handler-->>Client: 200 OK + JSON响应
Note over Client,DB: 2. 访问受保护接口
Client->>Handler: GET /api/admin/me + Bearer Token
Handler->>TokenMgr: ValidateAccessToken(token)
TokenMgr->>Redis: GET auth:token:{token}
Redis-->>TokenMgr: 返回 TokenInfo
TokenMgr-->>Handler: 返回用户上下文
Handler->>Service: GetCurrentUser(userID)
Service->>DB: 查询用户信息
DB-->>Service: 返回用户数据
Service-->>Handler: 返回用户+权限
Handler-->>Client: 200 OK + JSON响应
Note over Client,DB: 3. Token 刷新
Client->>Handler: POST /api/admin/refresh-token
Handler->>Service: RefreshToken(refresh_token)
Service->>TokenMgr: ValidateRefreshToken(token)
TokenMgr->>Redis: GET auth:refresh:{token}
Redis-->>TokenMgr: 返回 TokenInfo
TokenMgr->>TokenMgr: GenerateNewAccessToken
TokenMgr->>Redis: 存储新 access_token
TokenMgr-->>Service: 返回新 access_token
Service-->>Handler: 返回新 token
Handler-->>Client: 200 OK + new token
```
### 中间件执行顺序
```
HTTP 请求
[Recover 中间件]
[RequestID 中间件]
[Logger 中间件]
[Auth 中间件] ← 本系统
├─ 提取 Token
├─ 验证 Token调用 TokenManager
├─ 检查用户类型
└─ 设置用户上下文
[路由处理器]
├─ 从 context 获取用户信息
└─ 执行业务逻辑
HTTP 响应
```
---
## 核心组件设计
### 1. TokenManagerToken 管理器)
**职责**
- Token 生成:使用 UUID v4 生成不可预测的 Token
- Token 验证:从 Redis 查询并解析 TokenInfo
- Token 撤销:单个撤销或批量撤销用户所有 Token
- Token 刷新:验证 Refresh Token 并生成新的 Access Token
**数据结构**
```go
type TokenInfo struct {
UserID uint // 用户 ID
UserType int // 用户类型1-4
ShopID uint // 店铺 ID代理商
EnterpriseID uint // 企业 ID企业客户
Username string // 用户名
LoginTime time.Time // 登录时间
Device string // 设备类型
IP string // 登录 IP
}
```
**Redis 存储结构**
```
# Access Token
Key: auth:token:{token_uuid}
Value: JSON(TokenInfo)
TTL: 24 小时
# Refresh Token
Key: auth:refresh:{token_uuid}
Value: JSON(TokenInfo)
TTL: 7 天
# 用户 Token 列表(用于批量撤销)
Key: auth:user:{user_id}:tokens
Value: SET[token1, token2, ...]
TTL: 7 天
```
### 2. AuthService认证服务
**职责**
- 登录验证:查询账号、验证密码、生成 Token
- 权限查询:查询用户的角色和权限列表
- Token 管理:登出、刷新、批量撤销
- 密码管理:修改密码(含旧 Token 撤销)
**依赖注入**
```go
type Service struct {
accountStore *postgres.AccountStore // 账号查询
accountRoleStore *postgres.AccountRoleStore // 账号-角色关联
rolePermStore *postgres.RolePermissionStore // 角色-权限关联
permissionStore *postgres.PermissionStore // 权限查询
tokenManager *auth.TokenManager // Token 管理
logger *zap.Logger // 日志记录
}
```
### 3. Auth Middleware认证中间件
**职责**
- Token 提取:从 `Authorization: Bearer {token}` 提取 Token
- Token 验证:调用 TokenManager 验证合法性
- 用户类型检查:根据平台限制用户类型
- 上下文设置:将用户信息设置到 Fiber 和 Go Context
**配置示例**
```go
// 后台认证中间件
AdminAuth := middleware.Auth(middleware.AuthConfig{
TokenValidator: func(token string) (*middleware.UserContextInfo, error) {
// 验证 token
tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token)
if err != nil {
return nil, errors.New(errors.CodeInvalidToken, "令牌无效")
}
// 检查用户类型:后台只允许 SuperAdmin、Platform、Agent
if tokenInfo.UserType != constants.UserTypeSuperAdmin &&
tokenInfo.UserType != constants.UserTypePlatform &&
tokenInfo.UserType != constants.UserTypeAgent {
return nil, errors.New(errors.CodeForbidden, "权限不足")
}
return &middleware.UserContextInfo{...}, nil
},
SkipPaths: []string{"/api/admin/login", "/api/admin/refresh-token"},
})
```
---
## 安全机制
### 1. 密码安全
**Bcrypt 哈希**
- 使用 bcrypt 算法cost=10存储密码
- 每个密码有唯一的 salt防止彩虹表攻击
- 慢哈希算法,增加暴力破解成本
```go
// 密码哈希(注册时)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 10)
// 密码验证(登录时)
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
```
### 2. Token 安全
**不可预测性**
- 使用 UUID v4 生成128 位随机数
- 碰撞概率极低(约 1/2^122
**短生命周期**
- Access Token24 小时自动过期
- Refresh Token7 天自动过期
- 修改密码后立即撤销所有旧 Token
**传输安全**
- 仅通过 Authorization 请求头传递(不在 URL 中)
- 生产环境强制 HTTPS
### 3. 用户类型隔离
| 平台 | 允许访问 | 拒绝访问 |
|------|----------|----------|
| 后台 | SuperAdmin(1), Platform(2), Agent(3) | Enterprise(4), PersonalCustomer |
| H5 | Agent(3), Enterprise(4) | SuperAdmin(1), Platform(2), PersonalCustomer |
### 4. 防御措施
**防止暴力破解**
- 计划引入登录失败次数限制(待实现)
- 使用慢哈希算法bcrypt增加单次尝试成本
**防止 Token 泄露**
- Token 不出现在日志中(敏感信息脱敏)
- Token 不出现在 URL 中
- Redis 连接使用密码保护
**防止会话劫持**
- Token 绑定设备和 IP存储在 TokenInfo 中,可用于审计)
- 可选:实现设备指纹验证(待实现)
---
## 设计决策
### 为什么选择 Redis 而非 JWT
| 对比项 | Redis Token | JWT |
|--------|-------------|-----|
| 撤销能力 | ✅ 立即生效 | ❌ 无法撤销 |
| 性能 | ✅ 5msRedis 查询) | ✅ 0ms本地验证 |
| 存储负担 | ⚠️ Redis 内存 | ✅ 无服务端存储 |
| 灵活性 | ✅ 可存储复杂信息 | ⚠️ Payload 有大小限制 |
| 适用场景 | B 端系统(需要撤销) | C 端系统(高并发) |
**决策理由**
- B 端用户数量有限(< 1000Redis 内存负担可接受
- 修改密码、账号禁用等场景需要立即撤销 Token
- 需要存储完整的用户上下文信息ShopID、EnterpriseID 等)
### 为什么使用双令牌机制?
**问题**:如果只有一个 Token
- 短生命周期:用户频繁掉线,体验差
- 长生命周期Token 泄露风险增加
**解决方案**
- Access Token24小时用于 API 访问,频繁传输,短生命周期降低泄露风险
- Refresh Token7天用于刷新 Access Token低频传输长生命周期减少掉线
### 为什么密码修改要撤销所有 Token
**安全原因**
- 假设:用户发现密码泄露,立即修改密码
- 如果不撤销旧 Token攻击者仍可使用旧 Token 访问
**实现**
```go
func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword, newPassword string) error {
// 1. 验证旧密码
// 2. 哈希新密码
// 3. 更新数据库
// 4. 撤销所有旧 Token
return s.tokenManager.RevokeAllUserTokens(ctx, userID)
}
```
---
## 性能考量
### Redis 性能
**预期负载**
- 用户数:< 1000
- 每用户平均 Token 数2-3 个
- 总 Token 数:< 3000
- Redis 内存占用:< 3MB每个 TokenInfo 约 1KB
**性能指标**
- Token 验证:< 5msRedis GET 操作)
- Token 生成:< 10msRedis SET + SADD 操作)
- Token 撤销:< 5msRedis DEL 操作)
### 数据库查询优化
**登录流程优化**
1. 账号查询:使用 `username``phone` 索引(< 10ms
2. 权限查询:使用 `account_id` 索引(< 20ms
3. 总耗时:< 50ms
**缓存策略**(待实现):
- 用户权限列表可缓存 30 分钟
- 减少数据库查询压力
---
## 扩展性
### 水平扩展
**无状态设计**
- 认证服务无状态,可水平扩展
- Token 存储在 Redis所有实例共享
**Redis 集群**
- 当前使用单机 Redis
- 需要时可升级为 Redis Cluster 或 Sentinel
### 功能扩展
**可选功能**
- [ ] 设备指纹验证
- [ ] 登录失败次数限制
- [ ] 异地登录提醒
- [ ] 在线设备管理
- [ ] Token 黑名单
---
## 监控和审计
### 关键指标
| 指标 | 说明 | 告警阈值 |
|------|------|----------|
| 登录成功率 | 成功次数 / 总次数 | < 95% |
| Token 验证失败率 | 失败次数 / 总次数 | > 5% |
| Redis 可用性 | Ping 响应时间 | > 10ms |
| Token 平均验证时间 | P95 响应时间 | > 20ms |
### 审计日志
**记录事件**
- 用户登录(成功/失败)
- Token 撤销(单个/批量)
- 密码修改
- 账号状态变更
**日志格式**
```json
{
"level": "info",
"timestamp": "2026-01-15T16:15:00+08:00",
"event": "user_login",
"user_id": 1,
"username": "admin",
"ip": "127.0.0.1",
"device": "web",
"success": true
}
```
---
## 相关文档
- [API 文档](api/auth.md) - 完整的 API 接口说明
- [使用指南](auth-usage-guide.md) - 如何在代码中集成认证
- [错误处理指南](003-error-handling/使用指南.md) - 统一错误处理
---
**文档版本**: v1.0
**最后更新**: 2026-01-15
**维护者**: 君鸿卡管系统开发团队

505
docs/auth-usage-guide.md Normal file
View File

@@ -0,0 +1,505 @@
# B 端认证系统使用指南
本文档指导开发者如何在君鸿卡管系统中使用 B 端认证功能,包括在新路由中集成认证、获取用户信息、撤销 Token 等操作。
---
## 目录
- [快速开始](#快速开始)
- [在路由中集成认证](#在路由中集成认证)
- [获取当前用户信息](#获取当前用户信息)
- [Token 管理](#token-管理)
- [常见问题](#常见问题)
- [最佳实践](#最佳实践)
---
## 快速开始
###认证系统已集成到项目的 bootstrap 流程中,无需额外配置即可使用。
### 核心组件
| 组件 | 位置 | 用途 |
|------|------|------|
| TokenManager | `pkg/auth/token.go` | Token 生成、验证、撤销 |
| AuthService | `internal/service/auth/service.go` | 认证业务逻辑 |
| Auth Middleware | `pkg/middleware/auth.go` | 认证中间件 |
| Auth Handler | `internal/handler/{admin,h5}/auth.go` | 认证接口处理器 |
### 配置项
`configs/config.yaml` 中配置 Token 有效期:
```yaml
jwt:
secret_key: "your-secret-key-here"
token_duration: 3600 # JWT 有效期(个人客户,秒)
access_token_ttl: 86400 # Access Token 有效期B端
refresh_token_ttl: 604800 # Refresh Token 有效期B端
```
---
## 在路由中集成认证
### 1. 使用现有的认证中间件
后台和 H5 的认证中间件已在 `internal/bootstrap/middlewares.go` 中配置好。
**后台路由示例**
```go
// internal/routes/admin.go
func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
// 公开路由(无需认证)
router.Post(basePath+"/login", handlers.AdminAuth.Login)
router.Post(basePath+"/refresh-token", handlers.AdminAuth.RefreshToken)
// 受保护路由(需要认证)
authGroup := router.Group("", middlewares.AdminAuth)
authGroup.Post(basePath+"/logout", handlers.AdminAuth.Logout)
authGroup.Get(basePath+"/me", handlers.AdminAuth.GetMe)
authGroup.Post(basePath+"/password", handlers.AdminAuth.ChangePassword)
// 添加其他需要认证的路由
authGroup.Get(basePath+"/users", handlers.User.List)
authGroup.Post(basePath+"/users", handlers.User.Create)
}
```
**H5 路由示例**
```go
// internal/routes/h5.go
func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
// 公开路由
router.Post(basePath+"/login", handlers.H5Auth.Login)
// 受保护路由
authGroup := router.Group("", middlewares.H5Auth)
authGroup.Get(basePath+"/orders", handlers.Order.List)
}
```
### 2. 创建自定义认证中间件
如果需要自定义认证逻辑(例如特殊权限检查),可以创建自己的中间件:
```go
// internal/middleware/custom_auth.go
package middleware
import (
"github.com/break/junhong_cmp_fiber/pkg/auth"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/gofiber/fiber/v2"
)
// SuperAdminOnly 只允许超级管理员访问
func SuperAdminOnly(tokenManager *auth.TokenManager) fiber.Handler {
return pkgmiddleware.Auth(pkgmiddleware.AuthConfig{
TokenValidator: func(token string) (*pkgmiddleware.UserContextInfo, error) {
tokenInfo, err := tokenManager.ValidateAccessToken(context.Background(), token)
if err != nil {
return nil, errors.New(errors.CodeInvalidToken, "令牌无效")
}
// 只允许超级管理员
if tokenInfo.UserType != constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeForbidden, "权限不足")
}
return &pkgmiddleware.UserContextInfo{
UserID: tokenInfo.UserID,
UserType: tokenInfo.UserType,
ShopID: tokenInfo.ShopID,
EnterpriseID: tokenInfo.EnterpriseID,
}, nil
},
SkipPaths: []string{}, // 无公开路径
})
}
```
---
## 获取当前用户信息
### 1. 在 Handler 中获取用户 ID
使用 `pkg/middleware` 提供的工具函数:
```go
// internal/handler/admin/user.go
package admin
import (
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/gofiber/fiber/v2"
)
type UserHandler struct {
userService *user.Service
}
func (h *UserHandler) GetProfile(c *fiber.Ctx) error {
// 从 context 获取当前用户 ID
userID := middleware.GetUserIDFromContext(c.UserContext())
if userID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 使用 userID 查询用户信息
profile, err := h.userService.GetProfile(c.UserContext(), userID)
if err != nil {
return err
}
return response.Success(c, profile)
}
```
### 2. 获取完整的用户上下文
```go
func (h *UserHandler) DoSomething(c *fiber.Ctx) error {
ctx := c.UserContext()
// 获取各种用户信息
userID := middleware.GetUserIDFromContext(ctx)
userType := middleware.GetUserTypeFromContext(ctx)
shopID := middleware.GetShopIDFromContext(ctx)
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
// 根据用户类型执行不同逻辑
switch userType {
case constants.UserTypeSuperAdmin:
// 超级管理员逻辑
case constants.UserTypeAgent:
// 代理商逻辑,使用 shopID
case constants.UserTypeEnterprise:
// 企业客户逻辑,使用 enterpriseID
}
return response.Success(c, nil)
}
```
### 3. 在 Service 层使用用户信息
Service 层应通过参数接收用户信息,而不是直接从 context 获取:
```go
// internal/service/order/service.go
package order
type Service struct {
orderStore *postgres.OrderStore
}
// 推荐:显式传递 userID
func (s *Service) ListOrders(ctx context.Context, userID uint, filters *OrderFilters) ([]*model.Order, error) {
// 根据用户权限过滤订单
return s.orderStore.ListByUser(ctx, userID, filters)
}
// 不推荐:从 context 中获取
// func (s *Service) ListOrders(ctx context.Context, filters *OrderFilters) ([]*model.Order, error) {
// userID := middleware.GetUserIDFromContext(ctx) // 不推荐
// ...
// }
```
---
## Token 管理
### 1. 生成 Token
在认证服务中已实现,无需手动调用。如需在其他场景使用:
```go
package myservice
import (
"github.com/break/junhong_cmp_fiber/pkg/auth"
)
func (s *Service) IssueTokenForUser(ctx context.Context, userID uint) (string, string, error) {
tokenInfo := &auth.TokenInfo{
UserID: userID,
UserType: 1,
ShopID: 0,
EnterpriseID: 0,
Username: "user",
Device: "web",
IP: "127.0.0.1",
}
accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(ctx, tokenInfo)
if err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
```
### 2. 验证 Token
Token 验证已由中间件自动完成。如需手动验证:
```go
func (s *Service) ManuallyValidateToken(ctx context.Context, token string) (*auth.TokenInfo, error) {
tokenInfo, err := s.tokenManager.ValidateAccessToken(ctx, token)
if err != nil {
return nil, err
}
return tokenInfo, nil
}
```
### 3. 撤销 Token
**撤销单个 Token**
```go
func (s *Service) RevokeToken(ctx context.Context, token string) error {
return s.tokenManager.RevokeToken(ctx, token)
}
```
**撤销用户所有 Token**(例如修改密码后):
```go
func (s *Service) RevokeAllUserTokens(ctx context.Context, userID uint) error {
return s.tokenManager.RevokeAllUserTokens(ctx, userID)
}
```
---
## 常见问题
### Q1: 如何测试需要认证的接口?
**方法 1使用真实 Token**
```bash
# 1. 先登录获取 token
TOKEN=$(curl -s -X POST http://localhost:8080/api/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@123456"}' \
| jq -r '.data.access_token')
# 2. 使用 token 访问接口
curl -X GET http://localhost:8080/api/admin/users \
-H "Authorization: Bearer $TOKEN"
```
**方法 2在集成测试中模拟**
```go
// tests/integration/user_test.go
func TestListUsers(t *testing.T) {
// 创建测试账号
account := createTestAccount(t)
// 生成 token
tokenManager := auth.NewTokenManager(redisClient, 24*time.Hour, 7*24*time.Hour)
accessToken, _, err := tokenManager.GenerateTokenPair(ctx, &auth.TokenInfo{
UserID: account.ID,
UserType: account.UserType,
Username: account.Username,
})
require.NoError(t, err)
// 发送请求
req := httptest.NewRequest("GET", "/api/admin/users", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
```
### Q2: 如何处理 Token 过期?
前端应捕获 `1003` 错误码,自动使用 Refresh Token 刷新:
```javascript
// 前端示例(伪代码)
async function apiRequest(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`
}
});
// Token 过期
if (response.status === 401 && response.data.code === 1003) {
// 刷新 token
const newToken = await refreshAccessToken();
setAccessToken(newToken);
// 重试原请求
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`
}
});
}
return response;
}
async function refreshAccessToken() {
const response = await fetch('/api/admin/refresh-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: getRefreshToken() })
});
const data = await response.json();
return data.data.access_token;
}
```
### Q3: 如何区分后台和 H5 用户?
通过 `userType` 字段区分:
```go
userType := middleware.GetUserTypeFromContext(ctx)
switch userType {
case constants.UserTypeSuperAdmin: // 1
// 超级管理员
case constants.UserTypePlatform: // 2
// 平台用户
case constants.UserTypeAgent: // 3
// 代理商(后台和 H5 都可以)
case constants.UserTypeEnterprise: // 4
// 企业客户(仅 H5
}
```
### Q4: 如何实现"记住我"功能?
当前系统不支持"记住我"。如需实现:
1. 增加一个长期 Token 类型30 天)
2. 前端存储到 LocalStorage 或 Cookie
3. 后端需要额外的安全机制(如设备指纹)
---
## 最佳实践
### 1. 安全实践
**推荐做法**
- 所有敏感操作(修改密码、删除数据)要求二次验证
- Token 存储在 HttpOnly Cookie 或安全存储中
- 使用 HTTPS 传输
- 定期更新密码
- 修改密码后撤销所有旧 Token
**避免做法**
- 不要在 URL 中传递 Token
- 不要在浏览器 LocalStorage 中存储 TokenXSS 风险)
- 不要在日志中记录完整 Token
- 不要与他人分享 Token
### 2. 错误处理
Handler 应返回 `*errors.AppError`,由全局 ErrorHandler 统一处理:
```go
func (h *UserHandler) Create(c *fiber.Ctx) error {
userID := middleware.GetUserIDFromContext(c.UserContext())
if userID == 0 {
// 返回 AppError不要自己构造 JSON
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
// ... 业务逻辑
return response.Success(c, result)
}
```
### 3. 性能优化
- Token 验证操作已由 Redis 优化,平均耗时 < 5ms
- 避免在循环中重复验证 Token
- 使用批量操作减少 Redis 调用
### 4. 日志记录
记录关键认证事件:
```go
import "go.uber.org/zap"
// 登录成功
logger.Info("用户登录成功",
zap.Uint("user_id", userID),
zap.String("username", username),
zap.String("ip", clientIP),
zap.String("device", device),
)
// 登录失败
logger.Warn("登录失败",
zap.String("username", username),
zap.String("ip", clientIP),
zap.String("reason", "密码错误"),
)
// Token 撤销
logger.Info("Token 已撤销",
zap.Uint("user_id", userID),
zap.String("reason", "修改密码"),
)
```
### 5. 测试覆盖
确保以下场景有测试覆盖:
- [x] 登录成功
- [x] 登录失败(密码错误、账号禁用)
- [x] Token 验证成功
- [x] Token 过期处理
- [x] Token 刷新
- [x] 修改密码后 Token 失效
- [x] 并发访问
---
## 相关文档
- [API 文档](api/auth.md) - 完整的 API 接口说明
- [架构说明](auth-architecture.md) - 认证系统架构设计
- [错误处理指南](003-error-handling/使用指南.md) - 统一错误处理
---
**文档版本**: v1.0
**最后更新**: 2026-01-15
**维护者**: 君鸿卡管系统开发团队

View File

@@ -0,0 +1,317 @@
# OpenAPI 文档增强总结
## 更新日期
2026-01-15
## 增强内容
### 1. 自动认证标记
为所有需要认证的端点自动添加 `security` 标记。
**实现方式**
-`RouteSpec` 中使用 `Auth: true` 字段标记需要认证的端点
- `Register` 函数自动传递 `Auth` 字段到 OpenAPI 生成器
- 生成器自动添加 `security: [BearerAuth: []]` 到操作定义
**示例**
公开端点(`Auth: false`
```yaml
/api/admin/login:
post:
summary: 后台登录
# 无 security 字段
```
认证端点(`Auth: true`
```yaml
/api/admin/logout:
post:
summary: 登出
security:
- BearerAuth: []
```
### 2. 标准错误响应
为所有端点自动添加标准错误响应。
**错误响应规则**
- **所有端点**400 (请求参数错误), 500 (服务器内部错误)
- **认证端点**:额外添加 401 (未认证或认证已过期), 403 (无权访问)
**ErrorResponse Schema**
```yaml
ErrorResponse:
type: object
required:
- code
- message
- timestamp
properties:
code:
type: integer
description: 错误码
message:
type: string
description: 错误消息
timestamp:
type: string
format: date-time
description: 时间戳
```
**示例**
公开端点错误响应:
```yaml
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ModelLoginResponse'
"400":
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
"500":
description: 服务器内部错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
```
认证端点错误响应:
```yaml
responses:
"200":
description: OK
"400":
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
"401":
description: 未认证或认证已过期
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
"403":
description: 无权访问
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
"500":
description: 服务器内部错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
```
### 3. Bearer Token 认证定义
在 OpenAPI 规范中添加 Bearer Token 认证方案定义。
```yaml
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
```
## 修改的文件
### 核心文件
1. **pkg/openapi/generator.go**
- 修改 `AddOperation` 方法,新增 `requiresAuth` 参数
- 新增 `addSecurityRequirement` 方法:为操作添加认证要求
- 新增 `addStandardErrorResponses` 方法:添加标准错误响应
- 新增 `addErrorResponseSchema` 方法:添加错误响应 Schema 定义
- 新增 `ptrString` 辅助函数
2. **internal/routes/registry.go**
- 更新 `Register` 函数,传递 `spec.Auth` 到生成器
### 路由注册文件
更新以下文件中的 `RouteSpec`,为所有端点添加 `Auth` 字段:
1. **internal/routes/admin.go**
- 公开端点login, refresh-token`Auth: false`
- 认证端点logout, me, password`Auth: true`
2. **internal/routes/h5.go**
- 公开端点login, refresh-token`Auth: false`
- 认证端点logout, me, password`Auth: true`
3. **internal/routes/account.go**
- 所有账号管理端点:`Auth: true` (17 个端点)
4. **internal/routes/role.go**
- 所有角色管理端点:`Auth: true` (9 个端点)
5. **internal/routes/permission.go**
- 所有权限管理端点:`Auth: true` (6 个端点)
### 文档生成脚本
**cmd/gendocs/main.go**
- 添加 `AdminAuth` Handler 到 handlers 结构体
- 确保认证端点包含在生成的文档中
## 验证结果
### 1. 编译验证
```bash
✅ go build ./... - 编译通过
✅ go build ./pkg/openapi/... - OpenAPI 包编译通过
✅ go build ./internal/routes/... - 路由包编译通过
```
### 2. 文档生成验证
```bash
CONFIG_ENV=dev go run cmd/gendocs/main.go
✅ 文档生成成功docs/admin-openapi.yaml
✅ 包含所有端点(认证 + 业务端点)
```
### 3. 内容验证
**Security Scheme**
```bash
✅ grep "securitySchemes:" docs/admin-openapi.yaml
✅ BearerAuth 定义存在
```
**ErrorResponse Schema**
```bash
✅ grep "ErrorResponse:" docs/admin-openapi.yaml
✅ 包含 code, message, timestamp 字段
✅ Required 字段定义正确
```
**公开端点login**
```bash
✅ 只有 400, 500 错误响应
✅ 没有 security 标记
✅ 没有 401, 403 错误响应
```
**认证端点logout**
```bash
✅ 有 400, 401, 403, 500 错误响应
✅ 有 security: [BearerAuth: []]
✅ 错误响应引用 ErrorResponse schema
```
## 使用方法
### 1. 注册新端点
在路由注册时,显式设置 `Auth` 字段:
```go
// 公开端点
Register(router, doc, basePath, "POST", "/public", handler, RouteSpec{
Summary: "公开端点",
Tags: []string{"公开"},
Input: new(RequestModel),
Output: new(ResponseModel),
Auth: false, // 不需要认证
})
// 认证端点
Register(authGroup, doc, basePath, "GET", "/protected", handler, RouteSpec{
Summary: "受保护端点",
Tags: []string{"业务"},
Input: nil,
Output: new(ResponseModel),
Auth: true, // 需要认证
})
```
### 2. 生成文档
```bash
# 开发环境
CONFIG_ENV=dev go run cmd/gendocs/main.go
# 生产环境
CONFIG_ENV=prod go run cmd/gendocs/main.go
```
生成的文档位于 `docs/admin-openapi.yaml`
### 3. 查看文档
**方法 1使用 Swagger UI**
```bash
# 访问 https://editor.swagger.io/
# 将 docs/admin-openapi.yaml 内容粘贴到编辑器
```
**方法 2使用 Postman**
```bash
# File → Import → Upload Files
# 选择 docs/admin-openapi.yaml
```
**方法 3使用 Redoc**
```bash
npx @redocly/cli preview-docs docs/admin-openapi.yaml
```
## 后续优化(可选)
当前已完成的高优先级任务:
- ✅ 自动添加 security 标记
- ✅ 自动添加标准错误响应
- ✅ 定义 ErrorResponse schema
- ✅ 更新所有路由注册
低优先级增强(可在后续迭代完成):
- [ ] 为请求/响应模型添加示例值example
- [ ] 为字段添加详细的验证规则说明(自动从 validator 标签提取)
这些低优先级功能不影响当前文档的可用性,可以根据需要在后续版本中添加。
## 影响范围
**破坏性变更**:无
**向后兼容**:是
- 旧代码不需要修改即可工作
- 未设置 `Auth` 字段的 RouteSpec 默认为 `false`(公开端点)
**API 变更**:无
- 只影响 OpenAPI 文档生成
- 不影响运行时行为
## 总结
本次增强为 OpenAPI 文档自动生成系统添加了以下关键功能:
1. **自动认证标记**:通过 `Auth` 字段自动为认证端点添加 `security` 标记
2. **标准错误响应**:自动为所有端点添加统一的错误响应定义
3. **错误响应 Schema**:定义了标准的 `ErrorResponse` 结构
这些增强使得:
- 文档更加完整和规范
- API 使用者能清楚了解哪些端点需要认证
- 错误处理文档化,提升 API 可用性
- 减少手动维护文档的工作量
所有高优先级功能已完成并验证通过,可以投入使用。

View File

@@ -0,0 +1,817 @@
# 商户管理模块 - API 文档
## 目录
- [商户管理 API](#商户管理-api)
- [查询商户列表](#1-查询商户列表)
- [创建商户](#2-创建商户)
- [更新商户](#3-更新商户)
- [删除商户](#4-删除商户)
- [商户账号管理 API](#商户账号管理-api)
- [查询商户账号列表](#1-查询商户账号列表)
- [创建商户账号](#2-创建商户账号)
- [更新商户账号](#3-更新商户账号)
- [重置账号密码](#4-重置账号密码)
- [启用/禁用账号](#5-启用禁用账号)
- [数据模型](#数据模型)
- [错误码](#错误码)
---
## 商户管理 API
### 1. 查询商户列表
获取商户列表,支持分页、筛选和搜索。
**请求**
```http
GET /api/admin/shops
```
**查询参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| page | integer | 否 | 1 | 页码,从 1 开始 |
| size | integer | 否 | 20 | 每页数量,最大 100 |
| name | string | 否 | - | 商户名称(模糊搜索) |
| shop_code | string | 否 | - | 商户编码(精确匹配) |
| status | integer | 否 | - | 状态筛选1=正常2=禁用) |
| level | integer | 否 | - | 等级筛选1-7 |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"items": [
{
"id": 1,
"name": "测试商户",
"shop_code": "SHOP001",
"contact": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"address": "科技园",
"level": 1,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
],
"total": 1,
"page": 1,
"size": 20
},
"timestamp": 1704096000
}
```
**状态码**
| HTTP 状态码 | 说明 |
|-------------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权 |
| 500 | 服务器错误 |
---
### 2. 创建商户
创建新商户,同时创建初始坐席账号。
**请求**
```http
POST /api/admin/shops
Content-Type: application/json
```
**请求体**
```json
{
"name": "测试商户",
"shop_code": "SHOP001",
"contact": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"address": "科技园",
"level": 1,
"status": 1,
"init_username": "admin",
"init_phone": "13800138000",
"init_password": "password123"
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | 商户名称 |
| shop_code | string | 是 | 商户编码,全局唯一 |
| contact | string | 否 | 联系人 |
| phone | string | 否 | 联系电话 |
| province | string | 否 | 省份 |
| city | string | 否 | 城市 |
| district | string | 否 | 区域 |
| address | string | 否 | 详细地址 |
| level | integer | 是 | 商户等级1-7 |
| status | integer | 是 | 状态1=正常2=禁用) |
| init_username | string | 是 | 初始账号用户名 |
| init_phone | string | 是 | 初始账号手机号 |
| init_password | string | 是 | 初始账号密码 |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 1,
"name": "测试商户",
"shop_code": "SHOP001",
"contact": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"address": "科技园",
"level": 1,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
},
"timestamp": 1704096000
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 40002 | 商户编码已存在 |
| 400 | 40004 | 商户等级无效 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. 商户编码shop_code必须全局唯一
2. 等级level必须在 1-7 范围内
3. 创建商户的同时会自动创建一个初始坐席账号UserType=3
4. 初始账号的密码会使用 bcrypt 加密存储
5. 初始账号的 shop_id 会自动关联到新创建的商户
---
### 3. 更新商户
更新商户基本信息。
**请求**
```http
PUT /api/admin/shops/:id
Content-Type: application/json
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | 是 | 商户ID |
**请求体**
```json
{
"name": "更新后的商户名称",
"shop_code": "SHOP001",
"contact": "李四",
"phone": "13900139000",
"province": "广东省",
"city": "深圳市",
"district": "福田区",
"address": "中心区",
"level": 2,
"status": 1
}
```
**字段说明**
所有字段均为可选,但至少需要提供一个字段进行更新。
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 商户名称 |
| shop_code | string | 商户编码 |
| contact | string | 联系人 |
| phone | string | 联系电话 |
| province | string | 省份 |
| city | string | 城市 |
| district | string | 区域 |
| address | string | 详细地址 |
| level | integer | 商户等级1-7 |
| status | integer | 状态1=正常2=禁用) |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 1,
"name": "更新后的商户名称",
"shop_code": "SHOP001",
"contact": "李四",
"phone": "13900139000",
"province": "广东省",
"city": "深圳市",
"district": "福田区",
"address": "中心区",
"level": 2,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T11:00:00Z"
},
"timestamp": 1704099600
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 40001 | 商户不存在 |
| 400 | 40002 | 商户编码已存在(修改编码时) |
| 400 | 40004 | 商户等级无效 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
---
### 4. 删除商户
软删除商户,同时批量禁用所有关联的商户账号。
**请求**
```http
DELETE /api/admin/shops/:id
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | 是 | 商户ID |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096000
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | 40001 | 商户不存在 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. 删除商户时会进行软删除(设置 deleted_at
2. 所有关联的商户账号会被批量设置为禁用状态status=2
3. 账号不会被物理删除,只是被禁用
4. 删除操作不可逆(除非手动修改数据库)
---
## 商户账号管理 API
### 1. 查询商户账号列表
获取商户账号列表,支持分页、筛选和搜索。
**请求**
```http
GET /api/admin/shop-accounts
```
**查询参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| page | integer | 否 | 1 | 页码,从 1 开始 |
| size | integer | 否 | 20 | 每页数量,最大 100 |
| shop_id | integer | 否 | - | 商户ID筛选 |
| status | integer | 否 | - | 状态筛选1=正常2=禁用) |
| username | string | 否 | - | 用户名(模糊搜索) |
| phone | string | 否 | - | 手机号(模糊搜索) |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"items": [
{
"id": 1,
"username": "admin",
"phone": "13800138000",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
],
"total": 1,
"page": 1,
"size": 20
},
"timestamp": 1704096000
}
```
**状态码**
| HTTP 状态码 | 说明 |
|-------------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权 |
| 500 | 服务器错误 |
---
### 2. 创建商户账号
为指定商户创建新的坐席账号。
**请求**
```http
POST /api/admin/shop-accounts
Content-Type: application/json
```
**请求体**
```json
{
"shop_id": 1,
"username": "agent01",
"phone": "13800138001",
"password": "password123"
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| shop_id | integer | 是 | 商户ID |
| username | string | 是 | 用户名 |
| phone | string | 是 | 手机号 |
| password | string | 是 | 密码 |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 2,
"username": "agent01",
"phone": "13800138001",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:05:00Z",
"updated_at": "2024-01-01T10:05:00Z"
},
"timestamp": 1704096300
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 40001 | 商户不存在 |
| 400 | 50002 | 账号已存在(手机号重复) |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. shop_id 必须对应一个存在的商户
2. 创建的账号 UserType 固定为 3坐席/Agent
3. 密码会使用 bcrypt 加密存储
4. 手机号必须全局唯一
5. 账号默认状态为正常status=1
---
### 3. 更新商户账号
更新商户账号的基本信息(仅限用户名)。
**请求**
```http
PUT /api/admin/shop-accounts/:id
Content-Type: application/json
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | 是 | 账号ID |
**请求体**
```json
{
"username": "new_username"
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | string | 是 | 新的用户名 |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 2,
"username": "new_username",
"phone": "13800138001",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:05:00Z",
"updated_at": "2024-01-01T11:05:00Z"
},
"timestamp": 1704099900
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 50001 | 账号不存在 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. 此接口只能更新用户名
2. 手机号和密码不可通过此接口修改
3. 密码修改请使用"重置账号密码"接口
---
### 4. 重置账号密码
管理员为账号重置密码(无需提供原密码)。
**请求**
```http
PUT /api/admin/shop-accounts/:id/password
Content-Type: application/json
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | 是 | 账号ID |
**请求体**
```json
{
"new_password": "newpassword123"
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| new_password | string | 是 | 新密码 |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096600
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 50001 | 账号不存在 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. 管理员操作,无需提供原密码
2. 新密码会使用 bcrypt 加密存储
3. 建议密码长度至少 8 位,包含字母和数字
---
### 5. 启用/禁用账号
更新账号的启用状态。
**请求**
```http
PUT /api/admin/shop-accounts/:id/status
Content-Type: application/json
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | integer | 是 | 账号ID |
**请求体**
```json
{
"status": 2
}
```
**字段说明**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | integer | 是 | 状态1=正常2=禁用) |
**响应**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096900
}
```
**状态码**
| HTTP 状态码 | 业务错误码 | 说明 |
|-------------|-----------|------|
| 200 | 0 | 成功 |
| 400 | - | 请求参数错误 |
| 400 | 50001 | 账号不存在 |
| 400 | 50003 | 账号状态无效 |
| 401 | - | 未授权 |
| 500 | - | 服务器错误 |
**业务规则**
1. 状态值只能是 1正常或 2禁用
2. 禁用账号后,该账号无法登录
3. 启用账号后,账号恢复正常使用
---
## 数据模型
### ShopResponse
商户响应对象
```json
{
"id": 1,
"name": "测试商户",
"shop_code": "SHOP001",
"contact": "张三",
"phone": "13800138000",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"address": "科技园",
"level": 1,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | integer | 商户ID |
| name | string | 商户名称 |
| shop_code | string | 商户编码 |
| contact | string | 联系人 |
| phone | string | 联系电话 |
| province | string | 省份 |
| city | string | 城市 |
| district | string | 区域 |
| address | string | 详细地址 |
| level | integer | 商户等级1-7 |
| status | integer | 状态1=正常2=禁用) |
| created_at | string | 创建时间ISO 8601 |
| updated_at | string | 更新时间ISO 8601 |
### ShopAccountResponse
商户账号响应对象
```json
{
"id": 1,
"username": "admin",
"phone": "13800138000",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| id | integer | 账号ID |
| username | string | 用户名 |
| phone | string | 手机号 |
| user_type | integer | 用户类型(固定为 3表示坐席 |
| status | integer | 状态1=正常2=禁用) |
| shop_id | integer | 所属商户ID |
| shop_name | string | 所属商户名称 |
| created_at | string | 创建时间ISO 8601 |
| updated_at | string | 更新时间ISO 8601 |
### 分页响应
所有列表接口的响应都包含分页信息
```json
{
"items": [...],
"total": 100,
"page": 1,
"size": 20
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| items | array | 数据列表 |
| total | integer | 总记录数 |
| page | integer | 当前页码 |
| size | integer | 每页数量 |
---
## 错误码
### 通用错误码
| 错误码 | HTTP 状态码 | 说明 |
|--------|-------------|------|
| 0 | 200 | 成功 |
| 10001 | 400 | 请求参数错误 |
| 10002 | 401 | 未授权 |
| 10003 | 403 | 无权限 |
| 10004 | 404 | 资源不存在 |
| 10005 | 500 | 服务器内部错误 |
### 商户相关错误码
| 错误码 | HTTP 状态码 | 说明 |
|--------|-------------|------|
| 40001 | 400 | 商户不存在 |
| 40002 | 400 | 商户编码已存在 |
| 40003 | 400 | 商户状态无效 |
| 40004 | 400 | 商户等级无效 |
### 账号相关错误码
| 错误码 | HTTP 状态码 | 说明 |
|--------|-------------|------|
| 50001 | 400 | 账号不存在 |
| 50002 | 400 | 账号已存在 |
| 50003 | 400 | 账号状态无效 |
### 错误响应格式
```json
{
"code": 40001,
"msg": "商户不存在",
"data": null,
"timestamp": 1704096000
}
```
---
## 认证
所有 API 接口都需要在请求头中携带有效的认证 Token
```http
Authorization: Bearer YOUR_ACCESS_TOKEN
```
如果 Token 无效或过期,将返回 401 错误:
```json
{
"code": 10002,
"msg": "未授权",
"data": null,
"timestamp": 1704096000
}
```
---
## 速率限制
暂无速率限制。
---
## 版本历史
### v1.0.0 (2024-01-01)
- 初始版本
- 实现商户管理 CRUD 功能
- 实现商户账号管理功能
- 实现关联删除逻辑(删除商户自动禁用账号)
---
## 相关文档
- [使用指南](./使用指南.md) - 功能说明和使用场景
- [项目开发规范](../../AGENTS.md) - 项目整体开发规范

View File

@@ -0,0 +1,422 @@
# 商户管理模块 - 使用指南
## 概述
商户管理模块提供了完整的商户Shop和商户账号ShopAccount管理功能支持商户的创建、更新、删除、查询以及商户账号的全生命周期管理。
## 核心功能
### 1. 商户管理
- **创建商户**:创建新商户的同时自动创建一个初始坐席账号
- **查询商户**:支持分页查询、模糊搜索、状态筛选
- **更新商户**:更新商户基本信息(名称、编码、等级、状态等)
- **删除商户**:软删除商户,同时批量禁用所有关联的商户账号
### 2. 商户账号管理
- **创建账号**:为商户创建新的坐席账号
- **查询账号**:支持分页查询、按商户筛选、状态筛选
- **更新账号**:更新账号用户名(手机号和密码不可通过此接口修改)
- **重置密码**:管理员为账号重置密码(无需原密码)
- **启用/禁用账号**:控制账号的启用状态
## 业务规则
### 商户规则
1. **商户编码唯一性**商户编码ShopCode必须全局唯一
2. **商户等级**:等级范围为 1-7表示商户层级结构
3. **商户状态**
- `1` - 正常
- `2` - 禁用
4. **关联删除**:删除商户时,所有关联的商户账号将被批量禁用(不删除)
### 商户账号规则
1. **账号类型**:所有商户账号的用户类型固定为 `3`(坐席/Agent
2. **初始账号**:创建商户时必须提供初始账号的用户名、手机号和密码
3. **密码安全**:密码采用 bcrypt 加密存储
4. **账号状态**
- `1` - 正常
- `2` - 禁用
5. **字段限制**
- 更新账号时,手机号和密码不可修改(需通过专用接口)
- 密码重置由管理员操作,无需提供原密码
### 数据权限
- 所有查询操作会根据当前登录用户的数据权限自动过滤结果
- 使用 GORM 回调机制自动处理数据权限逻辑
## API 端点
### 商户管理 API
#### 1. 查询商户列表
```http
GET /api/admin/shops
```
**查询参数**
- `page` (int, 可选): 页码,默认 1
- `size` (int, 可选): 每页数量,默认 20最大 100
- `name` (string, 可选): 商户名称模糊搜索
- `shop_code` (string, 可选): 商户编码精确搜索
- `status` (int, 可选): 状态筛选1=正常2=禁用)
- `level` (int, 可选): 等级筛选
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": {
"items": [
{
"id": 1,
"name": "测试商户",
"shop_code": "SHOP001",
"level": 1,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
],
"total": 1,
"page": 1,
"size": 20
},
"timestamp": 1704096000
}
```
#### 2. 创建商户
```http
POST /api/admin/shops
```
**请求体**
```json
{
"name": "测试商户",
"shop_code": "SHOP001",
"level": 1,
"status": 1,
"init_username": "admin",
"init_phone": "13800138000",
"init_password": "password123"
}
```
**字段说明**
- `name` (string, 必填): 商户名称
- `shop_code` (string, 必填): 商户编码,全局唯一
- `level` (int, 必填): 商户等级,范围 1-7
- `status` (int, 必填): 状态1=正常2=禁用)
- `init_username` (string, 必填): 初始账号用户名
- `init_phone` (string, 必填): 初始账号手机号
- `init_password` (string, 必填): 初始账号密码
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 1,
"name": "测试商户",
"shop_code": "SHOP001",
"level": 1,
"status": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
},
"timestamp": 1704096000
}
```
#### 3. 更新商户
```http
PUT /api/admin/shops/:id
```
**路径参数**
- `id` (uint): 商户ID
**请求体**
```json
{
"name": "更新后的商户名称",
"shop_code": "SHOP001",
"level": 2,
"status": 1
}
```
**响应示例**:同创建商户
#### 4. 删除商户
```http
DELETE /api/admin/shops/:id
```
**路径参数**
- `id` (uint): 商户ID
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096000
}
```
**注意**:删除商户时,所有关联的商户账号将被自动禁用。
---
### 商户账号管理 API
#### 1. 查询商户账号列表
```http
GET /api/admin/shop-accounts
```
**查询参数**
- `page` (int, 可选): 页码,默认 1
- `size` (int, 可选): 每页数量,默认 20最大 100
- `shop_id` (uint, 可选): 商户ID筛选
- `status` (int, 可选): 状态筛选1=正常2=禁用)
- `username` (string, 可选): 用户名模糊搜索
- `phone` (string, 可选): 手机号模糊搜索
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": {
"items": [
{
"id": 1,
"username": "admin",
"phone": "13800138000",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
],
"total": 1,
"page": 1,
"size": 20
},
"timestamp": 1704096000
}
```
#### 2. 创建商户账号
```http
POST /api/admin/shop-accounts
```
**请求体**
```json
{
"shop_id": 1,
"username": "agent01",
"phone": "13800138001",
"password": "password123"
}
```
**字段说明**
- `shop_id` (uint, 必填): 商户ID
- `username` (string, 必填): 用户名
- `phone` (string, 必填): 手机号
- `password` (string, 必填): 密码
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": {
"id": 2,
"username": "agent01",
"phone": "13800138001",
"user_type": 3,
"status": 1,
"shop_id": 1,
"shop_name": "测试商户",
"created_at": "2024-01-01T10:05:00Z",
"updated_at": "2024-01-01T10:05:00Z"
},
"timestamp": 1704096300
}
```
#### 3. 更新商户账号
```http
PUT /api/admin/shop-accounts/:id
```
**路径参数**
- `id` (uint): 账号ID
**请求体**
```json
{
"username": "new_username"
}
```
**注意**:此接口只能更新用户名,手机号和密码不可通过此接口修改。
**响应示例**:同创建商户账号
#### 4. 重置账号密码
```http
PUT /api/admin/shop-accounts/:id/password
```
**路径参数**
- `id` (uint): 账号ID
**请求体**
```json
{
"new_password": "newpassword123"
}
```
**字段说明**
- `new_password` (string, 必填): 新密码
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096600
}
```
**注意**:此操作为管理员重置密码,无需提供原密码。
#### 5. 启用/禁用账号
```http
PUT /api/admin/shop-accounts/:id/status
```
**路径参数**
- `id` (uint): 账号ID
**请求体**
```json
{
"status": 2
}
```
**字段说明**
- `status` (int, 必填): 状态1=正常2=禁用)
**响应示例**
```json
{
"code": 0,
"msg": "success",
"data": null,
"timestamp": 1704096900
}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| `40001` | 商户不存在 |
| `40002` | 商户编码已存在 |
| `40003` | 商户状态无效 |
| `40004` | 商户等级无效 |
| `50001` | 账号不存在 |
| `50002` | 账号已存在 |
| `50003` | 账号状态无效 |
## 使用场景示例
### 场景1创建新商户并设置初始账号
```bash
curl -X POST http://localhost:3000/api/admin/shops \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "示例商户",
"shop_code": "DEMO001",
"level": 1,
"status": 1,
"init_username": "admin",
"init_phone": "13800138000",
"init_password": "admin123"
}'
```
### 场景2为商户添加新的坐席账号
```bash
curl -X POST http://localhost:3000/api/admin/shop-accounts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shop_id": 1,
"username": "agent01",
"phone": "13800138001",
"password": "agent123"
}'
```
### 场景3管理员重置账号密码
```bash
curl -X PUT http://localhost:3000/api/admin/shop-accounts/2/password \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"new_password": "newpassword123"
}'
```
### 场景4删除商户自动禁用关联账号
```bash
curl -X DELETE http://localhost:3000/api/admin/shops/1 \
-H "Authorization: Bearer YOUR_TOKEN"
```
## 注意事项
1. **认证要求**:所有接口需要在请求头中携带有效的认证 Token
2. **数据权限**:查询结果会根据当前用户的数据权限自动过滤
3. **密码安全**
- 密码在存储前会自动使用 bcrypt 加密
- 建议密码长度至少 8 位,包含字母和数字
4. **关联关系**
- 删除商户不会删除关联账号,只会禁用
- 禁用商户不会影响已存在账号的状态
5. **并发控制**:更新操作会检查记录是否存在,避免并发冲突
6. **日志记录**所有操作会记录到访问日志access.log
## 技术实现细节
- **框架**Fiber v2.x (HTTP)
- **ORM**GORM v1.25.x
- **密码加密**bcrypt
- **数据权限**GORM 回调自动处理
- **错误处理**统一错误码系统pkg/errors
- **响应格式**统一响应格式pkg/response
- **分层架构**Handler → Service → Store → Model
## 相关文档
- [API 文档](./API文档.md) - 详细的 API 接口文档
- [项目开发规范](../../AGENTS.md) - 项目整体开发规范
- [错误码定义](../../pkg/errors/codes.go) - 完整错误码列表