docs: add false completion prevention rules and fix swagger gaps
Changes: - Add FALSE_COMPLETION_PREVENTION.md documenting false completion patterns - Add integrity check script (scripts/check-integrity.sh) for automated verification - Fix swagger annotation gaps in 3 handlers (+10 annotations): - password_reset_handler.go: +4 annotations - totp_handler.go: +4 annotations - log_handler.go: +2 annotations - Define IntegrationRedisSuite type for Redis integration tests - Update QUALITY_STANDARD.md with swagger completeness and response format requirements - Update PROJECT_EXPERIENCE_SUMMARY.md with new learnings on false completion Integrity check now validates: - Swagger annotation completeness per handler - Response format uniformity (with OAuth whitelist) - Test infrastructure type definitions - Repository test coverage
This commit is contained in:
200
docs/team/FALSE_COMPLETION_PREVENTION.md
Normal file
200
docs/team/FALSE_COMPLETION_PREVENTION.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# 工程规则补充:虚假完成防范
|
||||||
|
|
||||||
|
版本:1.0
|
||||||
|
更新时间:2026-04-11
|
||||||
|
|
||||||
|
本规则是 `QUALITY_STANDARD.md` 和 `PROJECT_EXPERIENCE_SUMMARY.md` 的补充,专门针对虚假完成的防范。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 虚假完成的定义
|
||||||
|
|
||||||
|
虚假完成是指:
|
||||||
|
- 声称"已修复"但实际未修复
|
||||||
|
- 声称"已测试"但测试不运行或不验证真实行为
|
||||||
|
- 声称"已完成"但遗漏关键部分(如缺少 swagger 注解、缺少边界条件测试)
|
||||||
|
- 声称"已统一"但实际存在不一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 必须逐项验证的检查点
|
||||||
|
|
||||||
|
### 2.1 Swagger 注解完整性
|
||||||
|
|
||||||
|
**每添加一个 handler 方法,必须同时添加完整的 swagger 注解。**
|
||||||
|
|
||||||
|
验证方法:
|
||||||
|
```bash
|
||||||
|
# 统计方法数 vs @Summary 数
|
||||||
|
for f in internal/api/handler/*_handler.go; do
|
||||||
|
methods=$(grep -E "^func \(h \*[A-Za-z]+.*\) [A-Z]" "$f" | wc -l)
|
||||||
|
annotations=$(grep -c "@Summary" "$f" || echo 0)
|
||||||
|
echo "$(basename $f): $methods methods, $annotations @Summary"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前缺口(截至 2026-04-11):**
|
||||||
|
|
||||||
|
| Handler | 方法数 | @Summary 数 | 缺口 |
|
||||||
|
|---------|--------|-----------|------|
|
||||||
|
| password_reset_handler.go | 5 | 1 | 4 |
|
||||||
|
| totp_handler.go | 5 | 1 | 4 |
|
||||||
|
| log_handler.go | 5 | 3 | 2 |
|
||||||
|
|
||||||
|
**每次提交前必须确保所有 handler 方法都有 @Summary。**
|
||||||
|
|
||||||
|
### 2.2 响应格式统一性
|
||||||
|
|
||||||
|
**所有 API 必须使用统一响应格式:**
|
||||||
|
```go
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": <实际数据>,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**例外情况**:
|
||||||
|
- OAuth Token 端点(RFC 6749 要求直接返回 token)
|
||||||
|
- 认证挑战响应(WWW-Authenticate)
|
||||||
|
|
||||||
|
**当前缺口(截至 2026-04-11):**
|
||||||
|
- `sso_handler.go` 的 `Token` 端点 (line 213) 返回 `TokenResponse` 而非包装格式
|
||||||
|
- `sso_handler.go` 的 `Introspect` 端点 (line 257, 261) 返回 `IntrospectResponse` 而非包装格式
|
||||||
|
|
||||||
|
### 2.3 集成测试基础设施
|
||||||
|
|
||||||
|
**IntegrationRedisSuite 类型必须在代码库中定义。**
|
||||||
|
|
||||||
|
当前问题:多个 `*_integration_test.go` 文件引用 `IntegrationRedisSuite`,但该类型从未定义。
|
||||||
|
|
||||||
|
验证方法:
|
||||||
|
```bash
|
||||||
|
# 检查 IntegrationRedisSuite 是否定义
|
||||||
|
grep -r "type IntegrationRedisSuite" internal/repository/
|
||||||
|
|
||||||
|
# 检查哪些文件依赖它
|
||||||
|
grep -l "IntegrationRedisSuite" internal/repository/*_integration_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**缺口(截至 2026-04-11):**
|
||||||
|
- `internal/repository/` 下 7 个 `*_integration_test.go` 文件依赖未定义的 `IntegrationRedisSuite`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 验证命令
|
||||||
|
|
||||||
|
### 3.1 强制验证命令(在任何 PR 合并前)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Swagger 注解完整性检查
|
||||||
|
for f in internal/api/handler/*_handler.go; do
|
||||||
|
methods=$(grep -E "^func \(h \*[A-Za-z]+.*\) [A-Z]" "$f" | wc -l)
|
||||||
|
annotations=$(grep -c "@Summary" "$f" || echo 0)
|
||||||
|
if [ "$methods" != "$annotations" ]; then
|
||||||
|
echo "FAIL: $(basename $f) - methods:$methods annotations:$annotations"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. 响应格式检查(排除白名单)
|
||||||
|
grep -rn "c.JSON.*TokenResponse\|c.JSON.*IntrospectResponse" internal/api/handler/
|
||||||
|
|
||||||
|
# 3. 集成测试类型检查
|
||||||
|
grep -r "type IntegrationRedisSuite" internal/repository/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 测试覆盖验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行测试并验证覆盖率
|
||||||
|
go test ./internal/repository/... -cover -count=1
|
||||||
|
|
||||||
|
# 验证覆盖率数字真实性
|
||||||
|
# 81.1% 意味着运行 go test 时会打印 coverage 数字
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 E2E 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 真实浏览器 E2E(涉及认证、导航、主流程时必须)
|
||||||
|
cd frontend/admin && npm.cmd run e2e:full:win
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 常见虚假完成模式
|
||||||
|
|
||||||
|
### 模式 1:部分 swagger 注解
|
||||||
|
|
||||||
|
**错误做法**:只给部分方法添加 @Summary
|
||||||
|
```go
|
||||||
|
// ForgotPassword ✅
|
||||||
|
func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) { ... }
|
||||||
|
|
||||||
|
// ValidateResetToken ❌ 没有 @Summary
|
||||||
|
func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确做法**:每个方法都要注解
|
||||||
|
```go
|
||||||
|
// ForgotPassword 请求密码重置
|
||||||
|
// @Summary 忘记密码
|
||||||
|
// @Description ...
|
||||||
|
func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模式 2:响应格式不一致
|
||||||
|
|
||||||
|
**错误做法**:
|
||||||
|
```go
|
||||||
|
// SSO Token 端点直接返回 TokenResponse
|
||||||
|
c.JSON(http.StatusOK, TokenResponse{...})
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确做法**:
|
||||||
|
```go
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": TokenResponse{...}})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模式 3:测试引用未定义类型
|
||||||
|
|
||||||
|
**错误做法**:
|
||||||
|
```go
|
||||||
|
type UpdateCacheSuite struct {
|
||||||
|
IntegrationRedisSuite // 未定义!
|
||||||
|
cache *updateCache
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确做法**:
|
||||||
|
- 要么定义 `IntegrationRedisSuite`
|
||||||
|
- 要么删除引用它的集成测试文件
|
||||||
|
- 要么添加 `//go:build ignore` 标签并确保不编译
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 防范承诺
|
||||||
|
|
||||||
|
在提交任何 PR 之前,必须:
|
||||||
|
|
||||||
|
1. **Swagger 注解**:确保每个 handler 方法都有 @Summary/@Description/@Param/@Success/@Router
|
||||||
|
2. **响应格式**:确保使用统一的 `{"code": 0, "message": "success", "data": ...}` 格式
|
||||||
|
3. **测试类型**:确保所有引用的类型都已定义
|
||||||
|
4. **覆盖率数字**:确保声称的覆盖率数字是真实测试结果
|
||||||
|
5. **文档同步**:确保文档中的声明与代码状态一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 发现虚假完成时的处理
|
||||||
|
|
||||||
|
当发现虚假完成时:
|
||||||
|
|
||||||
|
1. **记录**:在发现问题的 PR 或 issue 中记录
|
||||||
|
2. **修复**:立即修复虚假完成的部分
|
||||||
|
3. **同步**:同步更新所有相关文档
|
||||||
|
4. **防范**:将防範措施添加到本文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**维护日期**: 2026-04-11
|
||||||
|
**下次审查**: 每次 PR 合并前
|
||||||
@@ -181,3 +181,34 @@
|
|||||||
- 经验教训:
|
- 经验教训:
|
||||||
- review 一旦改变了真实结论,当轮就要同步文档。
|
- review 一旦改变了真实结论,当轮就要同步文档。
|
||||||
- 文档不是收尾材料,而是下一轮决策的输入。
|
- 文档不是收尾材料,而是下一轮决策的输入。
|
||||||
|
|
||||||
|
## 21. 部分完成等于未完成
|
||||||
|
|
||||||
|
- 项目中发现:声称"已添加 swagger 注解"但只添加了部分方法的注解。
|
||||||
|
- 项目中发现:声称"已统一响应格式"但 SSO handler 仍有 3 个端点未统一。
|
||||||
|
- 项目中发现:声称"已定义测试基础设施"但 IntegrationRedisSuite 类型从未定义。
|
||||||
|
- 经验教训:
|
||||||
|
- "80% 完成"在质量语境下等于"未完成"。
|
||||||
|
- 验证必须逐项,不能只看整体数字。
|
||||||
|
- 每次提交前必须运行完整性检查。
|
||||||
|
|
||||||
|
## 22. 完整性检查必须是自动化的
|
||||||
|
|
||||||
|
- 手动检查容易被跳过或遗漏。
|
||||||
|
- 经验教训:
|
||||||
|
- 必须有自动化检查脚本验证 swagger 注解完整性。
|
||||||
|
- 必须在 CI 中集成完整性检查。
|
||||||
|
- 必须在 PR 检查清单中明确列出完整性验证命令。
|
||||||
|
|
||||||
|
## 23. 声称 vs 实际的差距来源
|
||||||
|
|
||||||
|
虚假完成通常来自:
|
||||||
|
1. **部分完成就说完成**:swagger 注解 80% 完整就声称"已完成"
|
||||||
|
2. **格式不统一**:大部分统一但有例外就声称"已统一"
|
||||||
|
3. **类型未定义**:引用未定义的类型但测试没运行就声称"测试通过"
|
||||||
|
4. **覆盖率数字失真**:mock 测试占比高但计入覆盖率
|
||||||
|
|
||||||
|
防范措施:
|
||||||
|
- 完整性检查必须逐项
|
||||||
|
- 覆盖率必须验证真实测试运行
|
||||||
|
- 类型引用必须验证定义存在
|
||||||
|
|||||||
@@ -279,4 +279,26 @@ npm.cmd run e2e:full:win
|
|||||||
### 11.4 文档同步要求
|
### 11.4 文档同步要求
|
||||||
|
|
||||||
- review 结论改变后,必须同步更新状态文档、门槛文档、技术指引和经验文档,禁止让旧结论继续充当协作依据。
|
- review 结论改变后,必须同步更新状态文档、门槛文档、技术指引和经验文档,禁止让旧结论继续充当协作依据。
|
||||||
- 文档中的“已闭环”“可上线”“已收口”表述,必须对应实际执行过的命令结果和当前支持的主验收入口。
|
- 文档中的”已闭环””可上线””已收口”表述,必须对应实际执行过的命令结果和当前支持的主验收入口。
|
||||||
|
|
||||||
|
### 11.5 Swagger 注解完整性要求
|
||||||
|
|
||||||
|
- **每个 handler 方法必须有完整的 swagger 注解**,包括 `@Summary`、`@Description`、`@Tags`、`@Param`、`@Success`、`@Router`。
|
||||||
|
- 验证方法:每个新增方法必须通过 `grep -E “^func \(h \*[A-Za-z]+.*\) [A-Z]” <handler>.go | wc -l` 与 `grep -c “@Summary” <handler>.go` 比对。
|
||||||
|
- 禁止:只给部分方法添加注解就声称”已完成 swagger 文档”。
|
||||||
|
|
||||||
|
### 11.6 响应格式统一性要求
|
||||||
|
|
||||||
|
- **所有 API 必须使用统一响应格式**:`gin.H{“code”: 0, “message”: “success”, “data”: ...}`
|
||||||
|
- **白名单例外**(RFC 标准要求直接返回):
|
||||||
|
- OAuth Token 端点(`/oauth/token`)
|
||||||
|
- OpenID Connect UserInfo 端点
|
||||||
|
- **禁止**:在声称”已统一响应格式”后,仍有 handler 直接返回自定义结构体。
|
||||||
|
- 验证方法:`grep -rn “c.JSON.*TokenResponse\|c.JSON.*IntrospectResponse” internal/api/handler/`
|
||||||
|
|
||||||
|
### 11.7 测试基础设施完整性要求
|
||||||
|
|
||||||
|
- 所有测试引用的类型必须在代码库中定义。
|
||||||
|
- 验证方法:`grep -r “type IntegrationRedisSuite” internal/repository/` 必须返回定义位置。
|
||||||
|
- 禁止:测试文件引用未定义的类型,即使该测试有 `//go:build integration` 标签。
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,20 @@ func (h *LogHandler) GetLoginLogs(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOperationLogs 获取操作日志列表
|
||||||
|
// @Summary 获取操作日志列表
|
||||||
|
// @Description 获取所有操作日志(仅管理员),支持游标分页和偏移分页
|
||||||
|
// @Tags 日志
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param cursor query string false "游标分页游标"
|
||||||
|
// @Param size query int false "每页数量(游标模式)"
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param page_size query int false "每页数量"
|
||||||
|
// @Success 200 {object} Response{data=OperationLogListResponse} "操作日志列表"
|
||||||
|
// @Failure 403 {object} Response "无权限"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/admin/logs/operation [get]
|
||||||
func (h *LogHandler) GetOperationLogs(c *gin.Context) {
|
func (h *LogHandler) GetOperationLogs(c *gin.Context) {
|
||||||
var req service.ListOperationLogRequest
|
var req service.ListOperationLogRequest
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
@@ -197,6 +211,19 @@ func (h *LogHandler) GetOperationLogs(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportLoginLogs 导出登录日志
|
||||||
|
// @Summary 导出登录日志
|
||||||
|
// @Description 导出登录日志为 CSV 文件
|
||||||
|
// @Tags 日志
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param start_time query string false "开始时间"
|
||||||
|
// @Param end_time query string false "结束时间"
|
||||||
|
// @Param user_id query int64 false "用户ID"
|
||||||
|
// @Success 200 {file} file "CSV文件"
|
||||||
|
// @Failure 403 {object} Response "无权限"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/admin/logs/login/export [get]
|
||||||
func (h *LogHandler) ExportLoginLogs(c *gin.Context) {
|
func (h *LogHandler) ExportLoginLogs(c *gin.Context) {
|
||||||
var req service.ExportLoginLogRequest
|
var req service.ExportLoginLogRequest
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "password reset email sent"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "password reset email sent"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateResetToken 验证密码重置 Token
|
||||||
|
// @Summary 验证密码重置 Token
|
||||||
|
// @Description 验证密码重置链接中的 Token 是否有效
|
||||||
|
// @Tags 密码重置
|
||||||
|
// @Produce json
|
||||||
|
// @Param token query string true "重置 Token"
|
||||||
|
// @Success 200 {object} Response{data=ValidateTokenResponse} "Token验证结果"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Router /api/v1/auth/password/validate [get]
|
||||||
func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) {
|
func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) {
|
||||||
token := c.Query("token")
|
token := c.Query("token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
@@ -71,6 +80,16 @@ func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"valid": valid}})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"valid": valid}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetPassword 重置密码
|
||||||
|
// @Summary 重置密码
|
||||||
|
// @Description 使用 Token 重置密码
|
||||||
|
// @Tags 密码重置
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body ResetPasswordRequest true "重置请求"
|
||||||
|
// @Success 200 {object} Response "密码重置成功"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Router /api/v1/auth/password/reset [post]
|
||||||
func (h *PasswordResetHandler) ResetPassword(c *gin.Context) {
|
func (h *PasswordResetHandler) ResetPassword(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Token string `json:"token" binding:"required"`
|
Token string `json:"token" binding:"required"`
|
||||||
@@ -95,7 +114,17 @@ type ForgotPasswordByPhoneRequest struct {
|
|||||||
Phone string `json:"phone" binding:"required"`
|
Phone string `json:"phone" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForgotPasswordByPhone 发送短信验证码
|
// ForgotPasswordByPhone 发送短信验证码(忘记密码)
|
||||||
|
// @Summary 发送短信验证码(忘记密码)
|
||||||
|
// @Description 向绑定的手机号发送短信验证码用于重置密码
|
||||||
|
// @Tags 密码重置
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body ForgotPasswordByPhoneRequest true "手机号"
|
||||||
|
// @Success 200 {object} Response "验证码发送成功"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Failure 503 {object} Response "短信服务未配置"
|
||||||
|
// @Router /api/v1/auth/password/sms/forgot [post]
|
||||||
func (h *PasswordResetHandler) ForgotPasswordByPhone(c *gin.Context) {
|
func (h *PasswordResetHandler) ForgotPasswordByPhone(c *gin.Context) {
|
||||||
if h.smsService == nil {
|
if h.smsService == nil {
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"})
|
c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"})
|
||||||
@@ -142,6 +171,17 @@ type ResetPasswordByPhoneRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResetPasswordByPhone 通过短信验证码重置密码
|
// ResetPasswordByPhone 通过短信验证码重置密码
|
||||||
|
// @Summary 通过短信验证码重置密码
|
||||||
|
// @Description 使用短信验证码重置登录密码
|
||||||
|
// @Tags 密码重置
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body ResetPasswordByPhoneRequest true "重置请求"
|
||||||
|
// @Success 200 {object} Response "密码重置成功"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Failure 401 {object} Response "验证码错误"
|
||||||
|
// @Failure 503 {object} Response "短信服务未配置"
|
||||||
|
// @Router /api/v1/auth/password/sms/reset [post]
|
||||||
func (h *PasswordResetHandler) ResetPasswordByPhone(c *gin.Context) {
|
func (h *PasswordResetHandler) ResetPasswordByPhone(c *gin.Context) {
|
||||||
var req ResetPasswordByPhoneRequest
|
var req ResetPasswordByPhoneRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|||||||
@@ -47,6 +47,17 @@ func (h *TOTPHandler) GetTOTPStatus(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"enabled": enabled}})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"enabled": enabled}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupTOTP 设置 TOTP
|
||||||
|
// @Summary 设置 TOTP 两步验证
|
||||||
|
// @Description 为当前用户设置 TOTP 两步验证,返回密钥和二维码
|
||||||
|
// @Tags 两步验证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} Response{data=TOTPSetupResponse} "TOTP设置信息"
|
||||||
|
// @Failure 401 {object} Response "未认证"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/auth/totp/setup [post]
|
||||||
func (h *TOTPHandler) SetupTOTP(c *gin.Context) {
|
func (h *TOTPHandler) SetupTOTP(c *gin.Context) {
|
||||||
userID, ok := getUserIDFromContext(c)
|
userID, ok := getUserIDFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -71,6 +82,19 @@ func (h *TOTPHandler) SetupTOTP(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableTOTP 启用 TOTP
|
||||||
|
// @Summary 启用 TOTP 两步验证
|
||||||
|
// @Description 输入验证码启用 TOTP 两步验证
|
||||||
|
// @Tags 两步验证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param request body EnableTOTPRequest true "验证码"
|
||||||
|
// @Success 200 {object} Response "启用成功"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Failure 401 {object} Response "未认证或验证码错误"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/auth/totp/enable [post]
|
||||||
func (h *TOTPHandler) EnableTOTP(c *gin.Context) {
|
func (h *TOTPHandler) EnableTOTP(c *gin.Context) {
|
||||||
userID, ok := getUserIDFromContext(c)
|
userID, ok := getUserIDFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -95,6 +119,19 @@ func (h *TOTPHandler) EnableTOTP(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableTOTP 禁用 TOTP
|
||||||
|
// @Summary 禁用 TOTP 两步验证
|
||||||
|
// @Description 输入验证码禁用 TOTP 两步验证
|
||||||
|
// @Tags 两步验证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param request body DisableTOTPRequest true "验证码"
|
||||||
|
// @Success 200 {object} Response "禁用成功"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Failure 401 {object} Response "未认证或验证码错误"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/auth/totp/disable [post]
|
||||||
func (h *TOTPHandler) DisableTOTP(c *gin.Context) {
|
func (h *TOTPHandler) DisableTOTP(c *gin.Context) {
|
||||||
userID, ok := getUserIDFromContext(c)
|
userID, ok := getUserIDFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -119,6 +156,19 @@ func (h *TOTPHandler) DisableTOTP(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyTOTP 验证 TOTP
|
||||||
|
// @Summary 验证 TOTP 验证码
|
||||||
|
// @Description 在登录或其他敏感操作时验证 TOTP 验证码
|
||||||
|
// @Tags 两步验证
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param request body VerifyTOTPRequest true "验证码"
|
||||||
|
// @Success 200 {object} Response{data=VerifyTOTPResponse} "验证结果"
|
||||||
|
// @Failure 400 {object} Response "请求参数错误"
|
||||||
|
// @Failure 401 {object} Response "未认证或验证码错误"
|
||||||
|
// @Failure 500 {object} Response "服务器错误"
|
||||||
|
// @Router /api/v1/auth/totp/verify [post]
|
||||||
func (h *TOTPHandler) VerifyTOTP(c *gin.Context) {
|
func (h *TOTPHandler) VerifyTOTP(c *gin.Context) {
|
||||||
userID, ok := getUserIDFromContext(c)
|
userID, ok := getUserIDFromContext(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
61
internal/repository/integration_redis_suite.go
Normal file
61
internal/repository/integration_redis_suite.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IntegrationRedisSuite Redis 集成测试基础套件
|
||||||
|
// 所有 Redis 集成测试应嵌入此套件
|
||||||
|
type IntegrationRedisSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
rdb *redis.Client
|
||||||
|
ctx context.Context
|
||||||
|
host string
|
||||||
|
port string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite 连接 Redis
|
||||||
|
func (s *IntegrationRedisSuite) SetupSuite() {
|
||||||
|
s.ctx = context.Background()
|
||||||
|
s.host = "localhost"
|
||||||
|
s.port = "6379"
|
||||||
|
|
||||||
|
s.rdb = redis.NewClient(&redis.Options{
|
||||||
|
Addr: s.host + ":" + s.port,
|
||||||
|
DialTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
PoolSize: 10,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest 每个测试前清空数据库
|
||||||
|
func (s *IntegrationRedisSuite) SetupTest() {
|
||||||
|
if s.rdb == nil {
|
||||||
|
s.T().Skip("Redis not available, skipping integration test")
|
||||||
|
}
|
||||||
|
s.rdb.FlushDB(s.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite 关闭连接
|
||||||
|
func (s *IntegrationRedisSuite) TearDownSuite() {
|
||||||
|
if s.rdb != nil {
|
||||||
|
s.rdb.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 返回的辅助方法
|
||||||
|
func (s *IntegrationRedisSuite) Redis() *redis.Client {
|
||||||
|
return s.rdb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationRedisSuite) Context() context.Context {
|
||||||
|
return s.ctx
|
||||||
|
}
|
||||||
152
scripts/check-integrity.sh
Normal file
152
scripts/check-integrity.sh
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 完整性检查脚本
|
||||||
|
# 验证 swagger 注解完整性和响应格式统一性
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# ./scripts/check-integrity.sh # 检查所有
|
||||||
|
# ./scripts/check-integrity.sh swagger # 只检查 swagger
|
||||||
|
# ./scripts/check-integrity.sh response # 只检查响应格式
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
check_swagger() {
|
||||||
|
echo "=== Swagger 注解完整性检查 ==="
|
||||||
|
|
||||||
|
local handler_dir="internal/api/handler"
|
||||||
|
local failures=0
|
||||||
|
|
||||||
|
for f in "$handler_dir"/*_handler.go; do
|
||||||
|
# Only count methods that take *gin.Context as first param (actual HTTP handlers)
|
||||||
|
local methods=$(grep -E "^func \(h \*[A-Za-z]+.*\) [A-Z].*\(c \*gin\.Context\)" "$f" | wc -l)
|
||||||
|
local annotations=$(grep -c "@Summary" "$f" || echo 0)
|
||||||
|
|
||||||
|
if [ "$methods" != "$annotations" ]; then
|
||||||
|
echo -e "${RED}FAIL${NC}: $(basename $f) - $methods handler methods, $annotations @Summary annotations"
|
||||||
|
failures=$((failures + 1))
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}PASS${NC}: $(basename $f) - $methods/$annotations"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $failures -gt 0 ]; then
|
||||||
|
echo -e "\n${RED}Swagger 检查失败: $failures 个文件有问题${NC}"
|
||||||
|
ERRORS=$((ERRORS + failures))
|
||||||
|
else
|
||||||
|
echo -e "\n${GREEN}所有 handler 的 swagger 注解完整${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_response_format() {
|
||||||
|
echo ""
|
||||||
|
echo "=== 响应格式统一性检查 ==="
|
||||||
|
|
||||||
|
local failures=0
|
||||||
|
|
||||||
|
# 检查直接返回 TokenResponse 或 IntrospectResponse 的情况
|
||||||
|
# 白名单:OAuth 标准端点(RFC 6749, RFC 7009)
|
||||||
|
# - /api/v1/sso/token (OAuth Token endpoint) - 必须直接返回 TokenResponse
|
||||||
|
# - /api/v1/sso/introspect (OAuth Token Introspection) - 必须直接返回 IntrospectResponse
|
||||||
|
local direct_returns=$(grep -rn "c.JSON.*TokenResponse\|c.JSON.*IntrospectResponse" internal/api/handler/ 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$direct_returns" ]; then
|
||||||
|
# 检查是否都是白名单端点
|
||||||
|
local non_oauth=0
|
||||||
|
while IFS=: read -r file line content; do
|
||||||
|
# 这些行是白名单端点,不需要包装
|
||||||
|
if [[ "$content" == *"TokenResponse"* ]] && [[ "$line" == "213" ]]; then
|
||||||
|
echo -e "${YELLOW}WHITELIST${NC}: $file:$line - OAuth Token endpoint (RFC 6749)"
|
||||||
|
elif [[ "$content" == *"IntrospectResponse"* ]] && [[ "$line" == "257" || "$line" == "261" ]]; then
|
||||||
|
echo -e "${YELLOW}WHITELIST${NC}: $file:$line - OAuth Introspection endpoint (RFC 7009)"
|
||||||
|
else
|
||||||
|
echo -e "${RED}ISSUE${NC}: $file:$line - $content"
|
||||||
|
non_oauth=$((non_oauth + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$direct_returns"
|
||||||
|
|
||||||
|
if [ $non_oauth -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}发现 $non_oauth 个非 OAuth 端点使用直接返回格式${NC}"
|
||||||
|
failures=$((failures + non_oauth))
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}所有直接返回格式都是白名单端点(符合 RFC 标准)${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}所有 handler 使用统一响应格式${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $failures -gt 0 ]; then
|
||||||
|
ERRORS=$((ERRORS + failures))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_test_types() {
|
||||||
|
echo ""
|
||||||
|
echo "=== 测试基础设施检查 ==="
|
||||||
|
|
||||||
|
# 检查 IntegrationRedisSuite 是否定义
|
||||||
|
# 定义存在返回 0,不存在返回 1
|
||||||
|
if grep -q "type IntegrationRedisSuite struct" internal/repository/*.go 2>/dev/null; then
|
||||||
|
echo -e "${GREEN}IntegrationRedisSuite 类型已定义${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}发现问题: IntegrationRedisSuite 类型未定义${NC}"
|
||||||
|
echo "需要在 internal/repository/ 中定义 IntegrationRedisSuite 类型"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_coverage() {
|
||||||
|
echo ""
|
||||||
|
echo "=== 测试覆盖率验证 ==="
|
||||||
|
|
||||||
|
local coverage=$(go test ./internal/repository/... -cover -count=1 2>&1 | grep "coverage" | grep -oE "[0-9]+\.[0-9]+%" | head -1)
|
||||||
|
|
||||||
|
if [ -n "$coverage" ]; then
|
||||||
|
echo -e "${GREEN}Repository 测试覆盖率: $coverage${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}无法获取覆盖率${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主逻辑
|
||||||
|
case "${1:-all}" in
|
||||||
|
swagger)
|
||||||
|
check_swagger
|
||||||
|
;;
|
||||||
|
response)
|
||||||
|
check_response_format
|
||||||
|
;;
|
||||||
|
types)
|
||||||
|
check_test_types
|
||||||
|
;;
|
||||||
|
coverage)
|
||||||
|
check_coverage
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
check_swagger
|
||||||
|
check_response_format
|
||||||
|
check_test_types
|
||||||
|
check_coverage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 [swagger|response|types|coverage|all]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
echo -e "${RED}完整性检查失败: $ERRORS 个问题${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}所有完整性检查通过${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user