Files
junhong_cmp_fiber/AGENTS.md
huang c665f32976
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
feat: 套餐系统升级 - Worker 重构、流量重置、文档与规范更新
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入
- 实现套餐流量重置服务(日/月/年周期重置)
- 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑
- 新增订单创建幂等性防重(Redis 业务键 + 分布式锁)
- 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求
- 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南)
- 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录
- 新增 queue types 抽象和 Redis 常量定义
2026-02-12 14:24:15 +08:00

587 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# 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` | 规范文档流程和维护规则 |
### ⚠️ 新增 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 {
// 已被处理,检查当前状态决定返回成功还是错误
}
```
**策略 2Redis 业务键防重 + 分布式锁(适用于创建类操作,无状态可依赖)**
```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()` | 策略 2Redis 业务键 + 分布式锁 |
| 钱包支付 | `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`