diff --git a/docs/team/FALSE_COMPLETION_PREVENTION.md b/docs/team/FALSE_COMPLETION_PREVENTION.md new file mode 100644 index 0000000..10d2d9f --- /dev/null +++ b/docs/team/FALSE_COMPLETION_PREVENTION.md @@ -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 合并前 diff --git a/docs/team/PROJECT_EXPERIENCE_SUMMARY.md b/docs/team/PROJECT_EXPERIENCE_SUMMARY.md index 8277cd8..fdf476c 100644 --- a/docs/team/PROJECT_EXPERIENCE_SUMMARY.md +++ b/docs/team/PROJECT_EXPERIENCE_SUMMARY.md @@ -181,3 +181,34 @@ - 经验教训: - review 一旦改变了真实结论,当轮就要同步文档。 - 文档不是收尾材料,而是下一轮决策的输入。 + +## 21. 部分完成等于未完成 + +- 项目中发现:声称"已添加 swagger 注解"但只添加了部分方法的注解。 +- 项目中发现:声称"已统一响应格式"但 SSO handler 仍有 3 个端点未统一。 +- 项目中发现:声称"已定义测试基础设施"但 IntegrationRedisSuite 类型从未定义。 +- 经验教训: + - "80% 完成"在质量语境下等于"未完成"。 + - 验证必须逐项,不能只看整体数字。 + - 每次提交前必须运行完整性检查。 + +## 22. 完整性检查必须是自动化的 + +- 手动检查容易被跳过或遗漏。 +- 经验教训: + - 必须有自动化检查脚本验证 swagger 注解完整性。 + - 必须在 CI 中集成完整性检查。 + - 必须在 PR 检查清单中明确列出完整性验证命令。 + +## 23. 声称 vs 实际的差距来源 + +虚假完成通常来自: +1. **部分完成就说完成**:swagger 注解 80% 完整就声称"已完成" +2. **格式不统一**:大部分统一但有例外就声称"已统一" +3. **类型未定义**:引用未定义的类型但测试没运行就声称"测试通过" +4. **覆盖率数字失真**:mock 测试占比高但计入覆盖率 + +防范措施: +- 完整性检查必须逐项 +- 覆盖率必须验证真实测试运行 +- 类型引用必须验证定义存在 diff --git a/docs/team/QUALITY_STANDARD.md b/docs/team/QUALITY_STANDARD.md index 3a930c7..a3fbc96 100644 --- a/docs/team/QUALITY_STANDARD.md +++ b/docs/team/QUALITY_STANDARD.md @@ -279,4 +279,26 @@ npm.cmd run e2e:full:win ### 11.4 文档同步要求 - review 结论改变后,必须同步更新状态文档、门槛文档、技术指引和经验文档,禁止让旧结论继续充当协作依据。 -- 文档中的“已闭环”“可上线”“已收口”表述,必须对应实际执行过的命令结果和当前支持的主验收入口。 +- 文档中的”已闭环””可上线””已收口”表述,必须对应实际执行过的命令结果和当前支持的主验收入口。 + +### 11.5 Swagger 注解完整性要求 + +- **每个 handler 方法必须有完整的 swagger 注解**,包括 `@Summary`、`@Description`、`@Tags`、`@Param`、`@Success`、`@Router`。 +- 验证方法:每个新增方法必须通过 `grep -E “^func \(h \*[A-Za-z]+.*\) [A-Z]” .go | wc -l` 与 `grep -c “@Summary” .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` 标签。 + diff --git a/internal/api/handler/log_handler.go b/internal/api/handler/log_handler.go index e66603d..5867ab1 100644 --- a/internal/api/handler/log_handler.go +++ b/internal/api/handler/log_handler.go @@ -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) { var req service.ListOperationLogRequest 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) { var req service.ExportLoginLogRequest if err := c.ShouldBindQuery(&req); err != nil { diff --git a/internal/api/handler/password_reset_handler.go b/internal/api/handler/password_reset_handler.go index 586697d..7fcef8d 100644 --- a/internal/api/handler/password_reset_handler.go +++ b/internal/api/handler/password_reset_handler.go @@ -55,6 +55,15 @@ func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) { 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) { token := c.Query("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}}) } +// 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) { var req struct { Token string `json:"token" binding:"required"` @@ -95,7 +114,17 @@ type ForgotPasswordByPhoneRequest struct { 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) { if h.smsService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"}) @@ -142,6 +171,17 @@ type ResetPasswordByPhoneRequest struct { } // 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) { var req ResetPasswordByPhoneRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/api/handler/totp_handler.go b/internal/api/handler/totp_handler.go index 2debe8d..70443f8 100644 --- a/internal/api/handler/totp_handler.go +++ b/internal/api/handler/totp_handler.go @@ -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}}) } +// 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) { userID, ok := getUserIDFromContext(c) 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) { userID, ok := getUserIDFromContext(c) if !ok { @@ -95,6 +119,19 @@ func (h *TOTPHandler) EnableTOTP(c *gin.Context) { 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) { userID, ok := getUserIDFromContext(c) if !ok { @@ -119,6 +156,19 @@ func (h *TOTPHandler) DisableTOTP(c *gin.Context) { 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) { userID, ok := getUserIDFromContext(c) if !ok { diff --git a/internal/repository/integration_redis_suite.go b/internal/repository/integration_redis_suite.go new file mode 100644 index 0000000..1cdeb52 --- /dev/null +++ b/internal/repository/integration_redis_suite.go @@ -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 +} diff --git a/scripts/check-integrity.sh b/scripts/check-integrity.sh new file mode 100644 index 0000000..0f1bb03 --- /dev/null +++ b/scripts/check-integrity.sh @@ -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