
---

# junhong_cmp_fiber 项目开发规范

**重要**: 本文件包含核心规范。详细规范已提取为 Skills，在特定任务时按需加载。

## 专项规范 Skills（按需加载）

以下规范在相关任务时**自动触发**，无需手动加载：

| 任务类型 | 触发 Skill | 说明 |
|---------|-----------|------|
| 创建/修改 DTO 文件 | `dto-standards` | description 标签、枚举字段、验证标签规范 |
| 创建/修改 Model 模型 | `model-standards` | GORM 模型结构、字段标签、TableName 规范 |
| 注册 API 路由 / **新增 Handler** | `api-routing` | Register() 函数、RouteSpec、**文档生成器更新** |
| 测试接口/验证数据 | `db-validation` | PostgreSQL MCP 使用方法和验证示例 |
| 数据库迁移 | `db-migration` | 迁移命令、文件规范、执行流程、失败处理 |
| 维护规范文档 | `doc-management` | 规范文档流程和维护规则 |
| 调试 bug / 排查异常 | `systematic-debugging` | 四阶段根因分析流程、逐层诊断、场景速查表 |

### ⚠️ 新增 Handler 时必须同步更新文档生成器

新增 Handler 后，接口不会自动出现在 OpenAPI 文档中。**必须手动更新以下两个文件**：

```go
// cmd/api/docs.go 和 cmd/gendocs/main.go
handlers := &bootstrap.Handlers{
    // ... 添加新 Handler
    NewHandler: admin.NewXxxHandler(nil),
}
```

**完整检查清单**: 参见 [`docs/api-documentation-guide.md`](docs/api-documentation-guide.md#新增-handler-检查清单)

---

## 语言要求

**必须遵守:**
- 永远用中文交互
- 注释必须使用中文
- 文档必须使用中文
- 日志消息必须使用中文
- 用户可见的错误消息必须使用中文
- 变量名、函数名、类型名必须使用英文（遵循 Go 命名规范）
- GIT提交的commit必须使用中文

## 技术栈

**必须严格遵守，禁止替代方案:**

| 类型 | 技术 |
|------|------|
| HTTP 框架 | Fiber v2.x |
| ORM | GORM v1.25.x |
| 配置管理 | Viper |
| 日志 | Zap + Lumberjack.v2 |
| JSON 序列化 | sonic（优先），encoding/json（必要时） |
| 验证 | Validator |
| 任务队列 | Asynq v0.24.x |
| 数据库 | PostgreSQL 14+ |
| 缓存 | Redis 6.0+ |

**禁止:**
- 直接使用 `database/sql`（必须通过 GORM）
- 使用 `net/http` 替代 Fiber
- 使用 `encoding/json` 替代 sonic（除非必要）

## 架构分层

必须遵循以下分层架构：

```
Handler → Service → Store → Model
```

- **Handler**: 只处理 HTTP 请求/响应，不包含业务逻辑
- **Service**: 包含所有业务逻辑，支持跨模块调用
- **Store**: 统一管理所有数据访问，支持事务处理
- **Model**: 定义数据结构和 DTO

## 核心原则

### 错误处理
- 所有错误必须在 `pkg/errors/` 中定义
- 使用统一错误码系统
- Handler 层通过返回 `error` 传递给全局 ErrorHandler

#### 错误报错规范（必须遵守）
- Handler 层禁止直接返回/拼接底层错误信息给客户端（例如 `"参数验证失败: "+err.Error()`、`err.Error()`）
- 参数校验失败：对外统一返回 `errors.New(errors.CodeInvalidParam)`（详细校验错误写日志）
- Service 层禁止对外返回 `fmt.Errorf(...)`，必须返回 `errors.New(...)` 或 `errors.Wrap(...)`
- 约定用法：`errors.New(code[, msg])`、`errors.Wrap(code, err[, msg])`

### 响应格式
- 所有 API 响应使用 `pkg/response/` 的统一格式
- 格式: `{code, msg, data, timestamp}`

### 常量管理
- 所有常量定义在 `pkg/constants/`
- Redis key 使用函数生成: `Redis{Module}{Purpose}Key(params...)`
- 禁止硬编码字符串和 magic numbers
- **必须为所有常量添加中文注释**

### 注释规范

#### 基本原则

- **所有注释使用中文**（与语言要求一致）
- **导出符号必须有文档注释**（包、函数、方法、类型、接口、常量、变量）
- **复杂逻辑必须有实现注释**（解释"为什么"，而不是"做了什么"）
- **禁止废话注释**（不要用注释复述代码本身）
- **修改代码时必须同步更新注释**（过时的注释比没有注释更有害）

#### 包注释

每个包的入口文件（通常是主文件或 `doc.go`）必须有包注释：

```go
// Package account 提供账号管理的业务逻辑服务
// 包含账号创建、修改、删除、权限分配等功能
package account
```

#### 结构体注释

所有导出结构体必须有文档注释，说明该结构体代表什么：

```go
// Service 账号业务服务
// 负责账号的 CRUD、角色分配、密码管理等业务逻辑
type Service struct {
    store        *Store
    auditService AuditServiceInterface
}
```

#### 接口注释

导出接口必须注释接口用途，每个方法必须说明契约：

```go
// PermissionChecker 权限检查器接口
// 用于查询用户的权限列表
type PermissionChecker interface {
    // CheckPermission 检查用户是否拥有指定权限
    // userID: 用户ID
    // permCode: 权限编码（格式: module:action）
    // platform: 端口类型 (all/web/h5)
    CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error)
}
```

#### 函数和方法注释

导出函数/方法必须以函数名开头，说明功能：

```go
// Create 创建账号
// POST /api/admin/accounts
func (h *AccountHandler) Create(c *fiber.Ctx) error {
```

**复杂方法**（超过 30 行或包含复杂业务逻辑）必须额外说明实现思路：

```go
// ActivateByRealname 首次实名激活套餐
// 当用户完成实名认证后，自动激活处于"囤货待实名"状态的套餐：
// 1. 查找该卡所有 status=3（待实名激活）的套餐
// 2. 按创建时间排序，第一个主套餐立即激活（status=1）
// 3. 其余主套餐进入排队状态（status=4）
// 4. 加油包如果绑定了已激活的主套餐则一并激活
func (s *UsageService) ActivateByRealname(ctx context.Context, cardID uint) error {
```

#### 未导出符号的注释

未导出（小写）的函数/方法：
- **简单逻辑**（< 15 行）：可以不加注释
- **复杂逻辑**（≥ 15 行）或 **非显而易见的算法**：必须加注释

```go
// buildPermissionTree 递归构建权限树
// 采用 map 索引 + 单次遍历算法，时间复杂度 O(n)
func (s *Service) buildPermissionTree(permissions []*model.Permission) []*dto.PermissionTreeNode {
```

#### 内联注释（实现逻辑注释）

以下场景**必须**添加内联注释：

| 场景 | 要求 |
|------|------|
| 复杂条件判断 | 解释判断的业务含义 |
| 多步骤业务流程 | 用编号注释标明每一步 |
| 非显而易见的设计决策 | 解释"为什么这样做"而不是"做了什么" |
| 缓存/事务/并发处理 | 说明策略和原因 |
| 临时方案/兼容逻辑 | 标注 TODO 或说明背景 |

**✅ 好的内联注释（解释为什么）**：
```go
// 使用 Redis 分布式锁防止并发重复创建，锁超时 10 秒
if !s.acquireLock(ctx, lockKey, 10*time.Second) {
    return errors.New(errors.CodeTooManyRequests, "操作过于频繁，请稍后重试")
}

// 先冻结佣金再扣款，保证资金安全（失败时佣金自动解冻）
if err := s.freezeCommission(ctx, tx, orderID); err != nil {
    return err
}
```

**❌ 废话注释（禁止）**：
```go
// 获取用户ID          ← 禁止：代码本身已经很清楚
userID := middleware.GetUserIDFromContext(ctx)

// 创建账号            ← 禁止：变量名已说明意图
account := &model.Account{}

// 返回错误            ← 禁止：return err 不需要注释
return err
```

#### 常量和枚举注释

分组常量必须有组注释，每个值必须有行内注释：

```go
// 用户类型常量
const (
    UserTypeSuperAdmin = 1 // 超级管理员
    UserTypePlatform   = 2 // 平台用户
    UserTypeAgent      = 3 // 代理账号
    UserTypeEnterprise = 4 // 企业账号
)
```

#### Handler 层特殊要求

Handler 方法的注释必须包含 HTTP 方法和路径：

```go
// Create 创建账号
// POST /api/admin/accounts
func (h *AccountHandler) Create(c *fiber.Ctx) error {
```

### Go 代码风格
- 使用 `gofmt` 格式化
- 遵循 [Effective Go](https://go.dev/doc/effective_go)
- 包名: 简短、小写、单数、无下划线
- 接口命名: 使用 `-er` 后缀（Reader、Writer、Logger）

## 数据库设计

**核心规则:**
- ❌ 禁止建立外键约束
- ❌ 禁止使用 GORM 关联关系标签（foreignKey、hasMany、belongsTo）
- ✅ 关联通过存储 ID 字段手动维护
- ✅ 关联数据在代码层面显式查询

## Go 惯用法 vs Java 风格

### ✅ Go 风格（推荐）
- 扁平化包结构（最多 2-3 层）
- 小而专注的接口（1-3 个方法）
- 直接访问导出字段（不用 getter/setter）
- 组合优于继承
- 显式错误返回和检查

### ❌ Java 风格（禁止）
- 过度抽象（不必要的接口、工厂）
- Getter/Setter 方法
- 深层继承层次
- 异常处理（panic/recover）
- 类型前缀（IService、AbstractBase、ServiceImpl）

## ⚠️ 测试禁令（强制执行）

**本项目不使用任何形式的自动化测试代码。**

**绝对禁止：**
- ❌ **禁止编写单元测试** - 无论任何场景
- ❌ **禁止编写集成测试** - 无论任何场景
- ❌ **禁止编写验收测试** - 无论任何场景
- ❌ **禁止编写流程测试** - 无论任何场景
- ❌ **禁止编写 E2E 测试** - 无论任何场景
- ❌ **禁止创建 `*_test.go` 文件** - 除非用户明确要求
- ❌ **禁止在任务中包含测试相关工作** - 规划和实现均不涉及测试
- ❌ **禁止在文档中提及测试要求** - 规范、设计文档均不讨论测试

**唯一例外：**
- ✅ **仅当用户明确要求**时才编写测试代码
- ✅ 用户必须主动说明"请写测试"或"需要测试"

**原因说明：**
- 业务系统的正确性通过人工验证和生产环境监控保证
- 测试代码的维护成本高于价值
- 快速迭代优先于测试覆盖率

**替代方案：**
- 使用 PostgreSQL MCP 工具手动验证数据
- 使用 Postman/curl 手动测试 API
- 依赖生产环境日志和监控发现问题

## 性能要求

- API P95 响应时间 < 200ms
- API P99 响应时间 < 500ms
- 数据库查询 < 50ms
- 列表查询必须分页（默认 20，最大 100）
- 避免 N+1 查询，使用批量操作

## 文档要求

- 每个功能在 `docs/{feature-id}/` 创建总结文档
- 文档文件名和内容使用中文
- 同步更新 README.md
- 为导出的函数、类型编写文档注释

## 函数复杂度

- 函数长度 ≤ 100 行（核心逻辑建议 ≤ 50 行）
- `main()` 函数只做编排，不含具体实现
- 遵循单一职责原则

## 访问日志

- 所有 HTTP 请求记录到 `access.log`
- 记录完整的请求/响应（限制 50KB）
- 包含: method, path, query, status, duration, request_id, ip, user_agent, user_id, bodies
- 使用 JSON 格式，配置自动轮转

## OpenSpec 工作流

创建提案前的检查清单：

1. ✅ 技术栈合规
2. ✅ 架构分层正确
3. ✅ 使用统一错误处理
4. ✅ 常量定义在 pkg/constants/
5. ✅ Go 惯用法（非 Java 风格）
6. ✅ 性能考虑
7. ✅ 文档更新计划
8. ✅ 中文优先

## Code Review 检查清单

### 错误处理
- [ ] Service 层无 `fmt.Errorf` 对外返回
- [ ] Handler 层参数校验不泄露细节
- [ ] 错误码使用正确（4xx vs 5xx）
- [ ] 错误日志完整（包含上下文）

### 代码质量
- [ ] 遵循 Handler → Service → Store → Model 分层
- [ ] 函数长度 ≤ 100 行（核心逻辑 ≤ 50 行）
- [ ] 常量定义在 `pkg/constants/`
- [ ] 使用 Go 惯用法（非 Java 风格）

### 文档和注释
- [ ] 所有注释使用中文
- [ ] 导出函数/类型有文档注释
- [ ] API 路径注释与真实路由一致

### 幂等性
- [ ] 创建类写操作有 Redis 业务键防重
- [ ] 状态变更使用条件更新（`WHERE status = expected`）
- [ ] 余额/库存变更使用乐观锁（version 字段）
- [ ] 分布式锁使用 `defer` 确保释放
- [ ] Redis Key 定义在 `pkg/constants/redis.go`

### 越权防护规范

**适用场景**：任何涉及跨用户、跨店铺、跨企业的资源访问

**三层防护机制**：

1. **路由层中间件**（粗粒度拦截）
   - 用于明显的权限限制（如企业账号禁止访问账号管理）
   - 示例：
     ```go
     group.Use(func(c *fiber.Ctx) error {
         userType := middleware.GetUserTypeFromContext(c.UserContext())
         if userType == constants.UserTypeEnterprise {
             return errors.New(errors.CodeForbidden, "无权限访问账号管理功能")
         }
         return c.Next()
     })
     ```

2. **Service 层业务检查**（细粒度验证）
   - 使用 `middleware.CanManageShop(ctx, targetShopID, shopStore)` 验证店铺权限
   - 使用 `middleware.CanManageEnterprise(ctx, targetEnterpriseID, enterpriseStore, shopStore)` 验证企业权限
   - 类型级权限检查（如代理不能创建平台账号）
   - 示例见 `internal/service/account/service.go`

3. **GORM Callback 自动过滤**（兜底）
   - 已有实现，自动应用到所有查询
   - 代理用户：`WHERE shop_id IN (自己店铺+下级店铺)`
   - 企业用户：`WHERE enterprise_id = 当前企业ID`
   - 无需手动调用

**统一错误返回**：
- 越权访问统一返回：`errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")`
- 不区分"不存在"和"无权限"，防止信息泄露

### 幂等性规范

**适用场景**：任何可能被重复触发的写操作

#### 必须实现幂等性的场景

| 场景 | 原因 | 实现策略 |
|------|------|----------|
| 订单创建 | 用户双击、网络重试 | Redis 业务键防重 + 分布式锁 |
| 支付回调 | 第三方平台重复通知 | 状态条件更新（`WHERE status = pending`） |
| 钱包扣款/充值 | 并发请求、消息重投 | 乐观锁（version 字段）+ 状态条件更新 |
| 套餐激活 | 异步任务重试 | Redis 分布式锁 + 已存在记录检查 |
| 异步任务处理 | Asynq 自动重试 | Redis 任务锁（`RedisTaskLockKey`） |
| 佣金计算 | 支付成功后触发 | 幂等任务入队 + 状态检查 |

#### 不需要幂等性的场景

- 纯查询接口（GET 请求天然幂等）
- 管理后台的配置修改（低频操作，人为确认）
- 日志记录、审计记录（允许重复写入）

#### 实现策略选择

根据场景特征选择合适的策略：

**策略 1：状态条件更新（首选，适用于有明确状态流转的操作）**

```go
// 通过 WHERE 条件确保只有预期状态才能更新，RowsAffected == 0 说明已被处理
result := tx.Model(&model.Order{}).
    Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
    Updates(map[string]any{"payment_status": model.PaymentStatusPaid})

if result.RowsAffected == 0 {
    // 已被处理，检查当前状态决定返回成功还是错误
}
```

**策略 2：Redis 业务键防重 + 分布式锁（适用于创建类操作，无状态可依赖）**

```go
// 业务键 = 唯一标识请求意图的组合字段
// 示例：order:create:{buyer_type}:{buyer_id}:{carrier_type}:{carrier_id}:{sorted_package_ids}
idempotencyKey := buildBusinessKey(...)
redisKey := constants.RedisXxxIdempotencyKey(idempotencyKey)

// 第 1 层：Redis GET 快速检测
val, err := s.redis.Get(ctx, redisKey).Result()
if err == nil && val != "" {
    return existingResult  // 已创建，直接返回
}

// 第 2 层：分布式锁防止并发
lockKey := constants.RedisXxxLockKey(resourceType, resourceID)
locked, _ := s.redis.SetNX(ctx, lockKey, time.Now().String(), lockTTL).Result()
if !locked {
    return errors.New(errors.CodeTooManyRequests, "操作进行中，请勿重复提交")
}
defer s.redis.Del(ctx, lockKey)

// 第 3 层：加锁后二次检测
val, err = s.redis.Get(ctx, redisKey).Result()
if err == nil && val != "" {
    return existingResult
}

// 执行业务逻辑...

// 成功后标记
s.redis.Set(ctx, redisKey, resultID, idempotencyTTL)
```

**策略 3：乐观锁（适用于余额、库存等数值更新）**

```go
result := tx.Model(&model.Wallet{}).
    Where("id = ? AND balance >= ? AND version = ?", walletID, amount, currentVersion).
    Updates(map[string]any{
        "balance": gorm.Expr("balance - ?", amount),
        "version": gorm.Expr("version + 1"),
    })
if result.RowsAffected == 0 {
    return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
```

#### Redis Key 命名规范

幂等性相关的 Redis Key 统一在 `pkg/constants/redis.go` 定义：

```go
// 幂等性检测键：Redis{Module}IdempotencyKey — TTL 通常 3~5 分钟
func RedisOrderIdempotencyKey(businessKey string) string

// 分布式锁键：Redis{Module}{Action}LockKey — TTL 通常 10~30 秒
func RedisOrderCreateLockKey(carrierType string, carrierID uint) string
```

#### 现有幂等性实现参考

| 模块 | 文件 | 策略 |
|------|------|------|
| 订单创建 | `internal/service/order/service.go` → `Create()` | 策略 2：Redis 业务键 + 分布式锁 |
| 钱包支付 | `internal/service/order/service.go` → `WalletPay()` | 策略 1：状态条件更新 |
| 支付回调 | `internal/service/order/service.go` → `HandlePaymentCallback()` | 策略 1：状态条件更新 |
| 套餐激活 | `internal/service/package/activation_service.go` → `ActivateQueuedPackage()` | 策略 2（简化版）：Redis 分布式锁 |
| 钱包扣款 | `internal/service/order/service.go` → `WalletPay()` | 策略 3：乐观锁（version 字段） |

### 审计日志规范

**适用场景**：任何敏感操作（账号管理、权限变更、数据删除等）

**使用方式**：

1. **Service 层集成审计日志**：
   ```go
   type Service struct {
       store       *Store
       auditService AuditServiceInterface
   }
   
   func (s *Service) SensitiveOperation(ctx context.Context, ...) error {
       // 1. 执行业务操作
       err := s.store.DoSomething(ctx, ...)
       if err != nil {
           return err
       }
       
       // 2. 记录审计日志（异步）
       s.auditService.LogOperation(ctx, &model.OperationLog{
           OperatorID:    middleware.GetUserIDFromContext(ctx),
           OperationType: "operation_type",
           OperationDesc: "操作描述",
           BeforeData:    beforeData,  // 变更前数据
           AfterData:     afterData,   // 变更后数据
           RequestID:     middleware.GetRequestIDFromContext(ctx),
           IPAddress:     middleware.GetIPFromContext(ctx),
           UserAgent:     middleware.GetUserAgentFromContext(ctx),
       })
       
       return nil
   }
   ```

2. **审计日志字段说明**：
   - `operator_id`, `operator_type`, `operator_name`: 操作人信息（必填）
   - `target_*`: 目标资源信息（可选）
   - `operation_type`: 操作类型（create/update/delete/assign_roles等）
   - `operation_desc`: 操作描述（中文，便于查看）
   - `before_data`, `after_data`: 变更数据（JSON 格式）
   - `request_id`, `ip_address`, `user_agent`: 请求上下文

3. **异步写入**：
   - 审计日志使用 Goroutine 异步写入
   - 写入失败不影响业务操作
   - 失败时记录 Error 日志，包含完整审计信息

**示例参考**：`internal/service/account/service.go`

---

### ⚠️ 任务执行规范（必须遵守）

**提案中的 tasks.md 是契约，不可擅自变更：**

| 规则 | 说明 |
|------|------|
| ❌ 禁止跳过任务 | 每个任务都是经过规划的，不能因为"简单"或"显而易见"而跳过 |
| ❌ 禁止简化任务 | 不能将多个任务合并或简化执行，除非获得明确许可 |
| ❌ 禁止自作主张优化 | 发现可以优化的地方，必须先询问是否可以调整 |
| ✅ 必须逐项完成 | 按照 tasks.md 中的顺序逐一执行并标记完成 |
| ✅ 必须询问后变更 | 如需调整任务（简化/跳过/合并/优化），先询问用户确认 |

**询问示例**：
> "我注意到任务 2.1 和 2.2 可以合并为一步完成，是否可以这样优化？"
> "任务 3.1 在当前实现中可能不需要，是否可以跳过？"

**详细规范和 OpenSpec 工作流请查看**: `@/openspec/AGENTS.md`
