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:
File diff suppressed because it is too large
Load Diff
380
docs/api-doc-update-summary.md
Normal file
380
docs/api-doc-update-summary.md
Normal 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
407
docs/auth-architecture.md
Normal 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_token(24h)
|
||||
TokenMgr->>Redis: 存储 refresh_token(7天)
|
||||
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. TokenManager(Token 管理器)
|
||||
|
||||
**职责**:
|
||||
- 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 Token:24 小时自动过期
|
||||
- Refresh Token:7 天自动过期
|
||||
- 修改密码后立即撤销所有旧 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 |
|
||||
|--------|-------------|-----|
|
||||
| 撤销能力 | ✅ 立即生效 | ❌ 无法撤销 |
|
||||
| 性能 | ✅ 5ms(Redis 查询) | ✅ 0ms(本地验证) |
|
||||
| 存储负担 | ⚠️ Redis 内存 | ✅ 无服务端存储 |
|
||||
| 灵活性 | ✅ 可存储复杂信息 | ⚠️ Payload 有大小限制 |
|
||||
| 适用场景 | B 端系统(需要撤销) | C 端系统(高并发) |
|
||||
|
||||
**决策理由**:
|
||||
- B 端用户数量有限(< 1000),Redis 内存负担可接受
|
||||
- 修改密码、账号禁用等场景需要立即撤销 Token
|
||||
- 需要存储完整的用户上下文信息(ShopID、EnterpriseID 等)
|
||||
|
||||
### 为什么使用双令牌机制?
|
||||
|
||||
**问题**:如果只有一个 Token:
|
||||
- 短生命周期:用户频繁掉线,体验差
|
||||
- 长生命周期:Token 泄露风险增加
|
||||
|
||||
**解决方案**:
|
||||
- Access Token(24小时):用于 API 访问,频繁传输,短生命周期降低泄露风险
|
||||
- Refresh Token(7天):用于刷新 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 验证:< 5ms(Redis GET 操作)
|
||||
- Token 生成:< 10ms(Redis SET + SADD 操作)
|
||||
- Token 撤销:< 5ms(Redis 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
505
docs/auth-usage-guide.md
Normal 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 中存储 Token(XSS 风险)
|
||||
- 不要在日志中记录完整 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
|
||||
**维护者**: 君鸿卡管系统开发团队
|
||||
317
docs/openapi-enhancement-summary.md
Normal file
317
docs/openapi-enhancement-summary.md
Normal 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 可用性
|
||||
- 减少手动维护文档的工作量
|
||||
|
||||
所有高优先级功能已完成并验证通过,可以投入使用。
|
||||
817
docs/shop-management/API文档.md
Normal file
817
docs/shop-management/API文档.md
Normal 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) - 项目整体开发规范
|
||||
422
docs/shop-management/使用指南.md
Normal file
422
docs/shop-management/使用指南.md
Normal 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) - 完整错误码列表
|
||||
Reference in New Issue
Block a user