feat: OpenAPI 契约对齐与框架优化
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s

主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
This commit is contained in:
2026-01-30 11:40:36 +08:00
parent 1290160728
commit 409a68d60b
88 changed files with 27358 additions and 990 deletions

View File

@@ -0,0 +1,396 @@
# CI 检查脚本规范
## 概述
本变更新增了自动化代码规范检查脚本,用于在 CI/CD 流程中检测规范违规。
## 检查脚本列表
### 1. Service 层错误处理检查
**文件**`scripts/check-service-errors.sh`
**用途**:检查 Service 层是否使用 `fmt.Errorf` 对外返回错误
**检查范围**
- 目录:`internal/service/**/*.go`
- 排除:测试文件(`*_test.go`
- 排除:带有 `// whitelist:` 注释的行
**检查逻辑**
```bash
FILES=$(find internal/service -name "*.go" -type f)
VIOLATIONS=$(grep -n "fmt\.Errorf" $FILES | grep -v "// whitelist:")
if [ -n "$VIOLATIONS" ]; then
echo "❌ 发现 Service 层使用 fmt.Errorf"
exit 1
fi
```
**退出码**
- `0`:检查通过
- `1`:检查失败(发现违规)
**白名单机制**
如果某处确实需要使用 `fmt.Errorf`(如内部调试),添加注释:
```go
// 特殊场景:内部日志调试
debugErr := fmt.Errorf("debug info: %v", data) // whitelist:
logger.Debug("调试信息", zap.Error(debugErr))
```
### 2. 注释路径一致性检查
**文件**`scripts/check-comment-paths.sh`
**用途**:检查 Handler 层注释中是否残留已弃用的 `/api/v1` 路径
**检查范围**
- 目录:`internal/handler/**/*.go`
- 排除:测试文件(`*_test.go`
**检查逻辑**
```bash
VIOLATIONS=$(grep -rn "/api/v1" internal/handler/ | grep -v "_test.go")
if [ -n "$VIOLATIONS" ]; then
echo "❌ 发现残留的 /api/v1 路径注释"
exit 1
fi
```
**退出码**
- `0`:检查通过
- `1`:检查失败(发现残留路径)
**正确路径**
- `/api/admin/*`:后台管理接口
- `/api/h5/*`H5 端接口
- `/api/c/v1/*`:个人客户接口
### 3. 统一检查脚本
**文件**`scripts/check-all.sh`
**用途**:运行所有代码规范检查
**检查流程**
```bash
set -e # 任何检查失败立即退出
bash scripts/check-service-errors.sh
bash scripts/check-comment-paths.sh
# 未来可添加更多检查...
echo "✅ 所有检查通过"
```
**使用场景**
- 本地开发:提交代码前运行
- CI/CD自动化检查流程
- Pre-commit hook提交前自动检查可选
## 脚本规范
### 输出格式
所有检查脚本应遵循统一的输出格式:
```bash
# 1. 开始提示
echo "🔍 检查 [检查项名称]..."
# 2. 检查逻辑
VIOLATIONS=$(检查命令)
# 3. 结果输出
if [ -n "$VIOLATIONS" ]; then
echo ""
echo "❌ 发现违规:"
echo "$VIOLATIONS"
echo ""
echo "修复建议:"
echo " - 建议1"
echo " - 建议2"
exit 1
fi
echo "✅ [检查项名称]检查通过"
```
### 错误消息规范
错误消息应包含:
1. **问题描述**:明确说明发现了什么问题
2. **违规位置**:文件路径和行号
3. **修复建议**:如何修复这些问题
4. **白名单机制**:如何豁免特殊场景(如适用)
**示例**
```
❌ 发现 Service 层使用 fmt.Errorf
internal/service/shop.go:45: return fmt.Errorf("店铺不存在")
internal/service/account.go:78: return fmt.Errorf("创建失败: %w", err)
请使用以下方式替代:
- 业务错误errors.New(code, msg)
- 系统错误errors.Wrap(code, err, msg)
如果某处确实需要使用 fmt.Errorf如内部调试请添加注释// whitelist:
```
### 脚本权限
所有脚本应添加执行权限:
```bash
chmod +x scripts/check-service-errors.sh
chmod +x scripts/check-comment-paths.sh
chmod +x scripts/check-all.sh
```
### Shell 兼容性
脚本应使用 Bash 标准语法,兼容 Linux 和 macOS
- 使用 `#!/bin/bash` 作为 shebang
- 避免使用非标准工具(仅依赖 grep、find、bash 等)
- 使用 `set -e` 确保错误自动退出
## CI 集成(可选)
### GitHub Actions 配置
**文件**`.github/workflows/lint.yml`
```yaml
name: Code Quality Check
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
- name: Run Code Quality Checks
run: bash scripts/check-all.sh
```
### 本地使用
开发者可以在本地运行检查:
```bash
# 运行所有检查
bash scripts/check-all.sh
# 运行单项检查
bash scripts/check-service-errors.sh
bash scripts/check-comment-paths.sh
```
### Pre-commit Hook可选
可以配置 Git pre-commit hook 在提交前自动检查:
**文件**`.git/hooks/pre-commit`
```bash
#!/bin/bash
echo "运行代码规范检查..."
bash scripts/check-all.sh
if [ $? -ne 0 ]; then
echo ""
echo "代码规范检查失败,提交已取消"
echo "请修复上述问题后重新提交"
exit 1
fi
echo "代码规范检查通过,继续提交..."
```
## 扩展性设计
### 添加新的检查规则
添加新的检查规则的步骤:
1. **创建检查脚本**`scripts/check-{name}.sh`
```bash
#!/bin/bash
echo "🔍 检查 [检查项名称]..."
# 检查逻辑
VIOLATIONS=$(检查命令)
if [ -n "$VIOLATIONS" ]; then
echo "❌ 发现违规"
echo "$VIOLATIONS"
exit 1
fi
echo "✅ 检查通过"
```
2. **添加执行权限**
```bash
chmod +x scripts/check-{name}.sh
```
3. **集成到统一脚本**
在 `scripts/check-all.sh` 中添加:
```bash
bash scripts/check-{name}.sh
```
4. **测试脚本**
```bash
# 测试通过场景
bash scripts/check-{name}.sh # 应返回退出码 0
# 测试失败场景(制造违规)
# 验证能检测到违规并返回退出码 1
```
5. **更新文档**
在 `README.md` 和本规范文档中添加新检查的说明
### 检查规则示例
以下是一些可能添加的检查规则:
| 检查项 | 脚本名称 | 检查内容 |
|-------|---------|---------|
| 常量硬编码 | `check-constants.sh` | 检查代码中是否有硬编码的 magic numbers 和字符串 |
| 日志规范 | `check-logging.sh` | 检查日志是否使用结构化字段zap.String、zap.Int 等) |
| TODO 标记 | `check-todos.sh` | 统计代码中的 TODO 数量,超过阈值时警告 |
| 导入路径 | `check-imports.sh` | 检查是否使用了禁止的包(如 `fmt.Println` |
| 测试覆盖率 | `check-coverage.sh` | 检查测试覆盖率是否达标 |
## 性能考虑
### 检查耗时
所有检查脚本应在合理时间内完成:
- 单项检查:< 10 秒
- 统一检查:< 30 秒
### 优化建议
1. **并行执行**:多个独立检查可以并行运行
2. **缓存结果**:避免重复扫描相同文件
3. **增量检查**仅检查变更的文件CI 场景)
### 并行执行示例
```bash
#!/bin/bash
# scripts/check-all-parallel.sh
# 在后台运行检查
bash scripts/check-service-errors.sh &
PID1=$!
bash scripts/check-comment-paths.sh &
PID2=$!
# 等待所有检查完成
wait $PID1
RESULT1=$?
wait $PID2
RESULT2=$?
# 检查结果
if [ $RESULT1 -ne 0 ] || [ $RESULT2 -ne 0 ]; then
echo "❌ 至少有一项检查失败"
exit 1
fi
echo "✅ 所有检查通过"
```
## 测试策略
### 脚本测试清单
每个检查脚本应测试以下场景:
1. **通过场景**:无违规时返回 0
2. **失败场景**:有违规时返回 1 并输出错误
3. **白名单机制**:白名单注释生效(如适用)
4. **边界情况**:空目录、特殊字符等
### 测试示例
```bash
# 测试 Service 层错误检查
# 1. 通过场景
bash scripts/check-service-errors.sh
echo "退出码: $?" # 应为 0
# 2. 失败场景
echo 'return fmt.Errorf("test")' >> internal/service/test_violation.go
bash scripts/check-service-errors.sh
echo "退出码: $?" # 应为 1
rm internal/service/test_violation.go
# 3. 白名单机制
echo 'return fmt.Errorf("debug") // whitelist:' >> internal/service/test_whitelist.go
bash scripts/check-service-errors.sh
echo "退出码: $?" # 应为 0
rm internal/service/test_whitelist.go
```
## 维护指南
### 定期维护
- **每月审查**:检查是否有新的规范需要自动化检查
- **每季度更新**:根据团队反馈优化错误消息和修复建议
- **每半年评估**:评估检查脚本的性能和有效性
### 处理误报
如果检查脚本产生误报:
1. **评估规则**:检查规则是否过于严格
2. **白名单机制**:考虑添加白名单支持
3. **改进检测**:优化正则表达式或检查逻辑
4. **文档说明**:在规范文档中说明特殊场景
### 版本控制
检查脚本应纳入版本控制:
- 脚本修改需要通过 Code Review
- 重大变更需要更新文档
- 保持脚本向后兼容(或提供迁移指南)
## 总结
本 CI 检查规范定义了:
1. **检查脚本列表**Service 层错误检查、注释路径检查、统一检查
2. **脚本规范**输出格式、错误消息、Shell 兼容性
3. **CI 集成**GitHub Actions、本地使用、Pre-commit Hook
4. **扩展性设计**:添加新规则的步骤和示例
5. **性能优化**:并行执行、增量检查
6. **测试策略**:通过/失败/白名单/边界情况
7. **维护指南**:定期审查、处理误报、版本控制
这些脚本确保代码质量和规范一致性,支持自动化检查和团队协作。

View File

@@ -0,0 +1,298 @@
# 错误处理规范更新
## 概述
本变更更新了错误处理规范文档,补充了缺失的内容和实际案例。
## 更新的规范文件
### 1. openspec/specs/error-handling/spec.md
**新增内容**
#### Purpose 章节
补充规范的目的说明:
- 错误码一致性和可追踪性
- 客户端能准确识别错误类型
- 日志记录完整便于排查
- 避免泄露内部实现细节
#### 错误报错规范章节
新增"错误报错规范(必须遵守)"章节,详细说明:
**Handler 层规范**
- ❌ 禁止直接返回/拼接底层错误信息给客户端
- ✅ 参数校验失败统一返回 `errors.New(CodeInvalidParam)`
- ✅ 详细校验错误写日志,对外返回通用消息
**Service 层规范**
- ❌ 禁止对外返回 `fmt.Errorf(...)`
- ✅ 业务错误使用 `errors.New(code[, msg])`
- ✅ 系统错误使用 `errors.Wrap(code, err[, msg])`
**代码示例**
```go
// ❌ 错误示例 - Handler 层
if err := c.BodyParser(&req); err != nil {
return response.Error(c, 400, errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
// ✅ 正确示例 - Handler 层
if err := c.BodyParser(&req); err != nil {
logger.Error("参数解析失败", zap.Error(err))
return errors.New(errors.CodeInvalidParam)
}
// ❌ 错误示例 - Service 层
if user == nil {
return fmt.Errorf("用户不存在: %w", err)
}
// ✅ 正确示例 - Service 层
if user == nil {
return errors.New(errors.CodeUserNotFound, "用户不存在")
}
if err := db.Save(&user).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "保存用户失败")
}
```
### 2. AGENTS.md
**新增内容**
#### 错误处理摘要
在"错误处理"章节补充"错误报错规范(必须遵守)"摘要:
- Handler 层禁止直接返回/拼接底层错误信息(例如 `"参数验证失败: "+err.Error()`
- 参数校验失败:对外统一返回 `errors.New(CodeInvalidParam)`(详细错误写日志)
- Service 层禁止对外返回 `fmt.Errorf(...)`,必须返回 `errors.New(...)``errors.Wrap(...)`
#### Code Review 检查清单
新增完整的 Code Review 检查清单:
**错误处理**
- [ ] Service 层无 `fmt.Errorf` 对外返回
- [ ] Handler 层参数校验不泄露细节
- [ ] 错误码使用正确4xx vs 5xx
- [ ] 错误日志完整(包含上下文)
**代码质量**
- [ ] 遵循 Handler → Service → Store → Model 分层
- [ ] 函数长度 ≤ 100 行(核心逻辑 ≤ 50 行)
- [ ] 常量定义在 `pkg/constants/`
- [ ] 使用 Go 惯用法(非 Java 风格)
**测试覆盖**
- [ ] 核心业务逻辑测试覆盖率 ≥ 90%
- [ ] 所有 API 端点有集成测试
- [ ] 测试验证真实功能(不绕过核心逻辑)
**文档和注释**
- [ ] 所有注释使用中文
- [ ] 导出函数/类型有文档注释
- [ ] API 路径注释与真实路由一致
### 3. docs/003-error-handling/使用指南.md
**新增内容**
#### Service 层错误处理
补充 Service 层错误处理实际案例:
**示例 1资源不存在**
```go
func (s *ShopService) GetShop(ctx context.Context, shopID uint) (*model.Shop, error) {
shop, err := s.store.Shop.GetByID(ctx, shopID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败")
}
return shop, nil
}
```
**示例 2状态不允许**
```go
func (s *SIMService) Activate(ctx context.Context, iccid string) error {
sim, err := s.store.SIM.GetByICCID(ctx, iccid)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "查询SIM卡失败")
}
if sim.Status != constants.SIMStatusInactive {
return errors.New(errors.CodeInvalidOperation, "只有未激活的SIM卡才能激活")
}
// 执行激活逻辑...
return nil
}
```
**示例 3数据库错误**
```go
func (s *AccountService) CreateAccount(ctx context.Context, req *dto.CreateAccountRequest) error {
account := &model.Account{
Username: req.Username,
Phone: req.Phone,
// ...
}
if err := s.store.Account.Create(ctx, account); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
}
return nil
}
```
#### Handler 层参数校验
补充 Handler 层参数校验案例:
**参数解析错误**
```go
func (h *AccountHandler) CreateAccount(c *fiber.Ctx) error {
var req dto.CreateAccountRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("参数解析失败", zap.Error(err))
return errors.New(errors.CodeInvalidParam)
}
if err := h.validator.Struct(&req); err != nil {
h.logger.Error("参数验证失败", zap.Error(err))
return errors.New(errors.CodeInvalidParam)
}
// 调用 Service...
return nil
}
```
**参数验证错误**
```go
func (h *ShopHandler) UpdateShop(c *fiber.Ctx) error {
shopID, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
h.logger.Error("店铺ID格式错误", zap.Error(err))
return errors.New(errors.CodeInvalidParam)
}
var req dto.UpdateShopRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("参数解析失败", zap.Error(err))
return errors.New(errors.CodeInvalidParam)
}
// 调用 Service...
return nil
}
```
#### 错误场景单元测试
补充测试代码示例:
**Service 层测试**
```go
func TestShopService_GetShop_NotFound(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := postgres.NewShopStore(tx, rdb)
service := service.NewShopService(store, logger)
// 测试不存在的店铺
_, err := service.GetShop(context.Background(), 99999)
assert.Error(t, err)
assert.True(t, errors.Is(err, errors.CodeShopNotFound))
}
func TestSIMService_Activate_InvalidStatus(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := postgres.NewSIMStore(tx, rdb)
service := service.NewSIMService(store, logger)
// 创建已激活的 SIM 卡
sim := &model.SIM{
ICCID: "898600123456789",
Status: constants.SIMStatusActive,
}
store.Create(context.Background(), sim)
// 尝试再次激活
err := service.Activate(context.Background(), sim.ICCID)
assert.Error(t, err)
assert.True(t, errors.Is(err, errors.CodeInvalidOperation))
}
```
**Handler 层测试**
```go
func TestAccountHandler_CreateAccount_InvalidParam(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
t.Run("缺少必填字段", func(t *testing.T) {
reqBody := map[string]interface{}{
"username": "test",
// 缺少 phone 字段
}
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", reqBody)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
var result map[string]interface{}
json.Unmarshal(resp.Body, &result)
assert.Equal(t, float64(errors.CodeInvalidParam), result["code"])
})
t.Run("手机号格式错误", func(t *testing.T) {
reqBody := map[string]interface{}{
"username": "test",
"phone": "invalid",
}
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", reqBody)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
})
}
```
## 检查清单
在实施这些更新后,需要验证:
- [x] `openspec/specs/error-handling/spec.md` 包含 Purpose 章节
- [x] `openspec/specs/error-handling/spec.md` 包含"错误报错规范"章节
- [x] `AGENTS.md` 包含错误处理摘要
- [x] `AGENTS.md` 包含 Code Review 检查清单
- [x] `docs/003-error-handling/使用指南.md` 包含 Service 层实际案例
- [x] `docs/003-error-handling/使用指南.md` 包含 Handler 层实际案例
- [x] `docs/003-error-handling/使用指南.md` 包含单元测试示例
## 影响范围
这些文档更新不影响现有代码逻辑,仅完善规范说明和最佳实践指引。
## 后续维护
- 新增错误码时,同步更新使用指南中的案例
- 发现新的错误处理模式时,补充到文档中
- 定期检查文档案例与代码实际实现的一致性