feat: 套餐系统升级 - Worker 重构、流量重置、文档与规范更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s

- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入
- 实现套餐流量重置服务(日/月/年周期重置)
- 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑
- 新增订单创建幂等性防重(Redis 业务键 + 分布式锁)
- 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求
- 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南)
- 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录
- 新增 queue types 抽象和 Redis 常量定义
This commit is contained in:
2026-02-12 14:24:15 +08:00
parent 655c9ce7a6
commit c665f32976
51 changed files with 7289 additions and 424 deletions

429
AGENTS.md
View File

@@ -102,6 +102,150 @@ Handler → Service → Store → Model
- 禁止硬编码字符串和 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)
@@ -132,147 +276,33 @@ Handler → Service → Store → Model
- 异常处理panic/recover
- 类型前缀IService、AbstractBase、ServiceImpl
## 测试要求
## ⚠️ 测试禁令(强制执行)
### 测试金字塔(新)
**本项目不使用任何形式的自动化测试代码。**
```
┌─────────────┐
│ E2E 测试 │ ← 手动/自动化 UI很少
─┴─────────────┴─
┌─────────────────┐
│ 业务流程测试 │ ← 15%:多 API 组合验证
│ tests/flows/ │ 来源Spec Business Flow
─┴─────────────────┴─
┌─────────────────────┐
│ 验收测试 │ ← 30%:单 API 契约验证
│ tests/acceptance/ │ 来源Spec Scenario
─┴─────────────────────┴─
┌───────────────────────────┐
│ 集成测试 │ ← 25%:组件集成
─┴───────────────────────────┴─
┌─────────────────────────────────┐
│ 单元测试(精简) │ ← 30%:仅复杂逻辑
└─────────────────────────────────┘
```
**绝对禁止:**
-**禁止编写单元测试** - 无论任何场景
-**禁止编写集成测试** - 无论任何场景
-**禁止编写验收测试** - 无论任何场景
-**禁止编写流程测试** - 无论任何场景
-**禁止编写 E2E 测试** - 无论任何场景
-**禁止创建 `*_test.go` 文件** - 除非用户明确要求
-**禁止在任务中包含测试相关工作** - 规划和实现均不涉及测试
-**禁止在文档中提及测试要求** - 规范、设计文档均不讨论测试
### 三层测试体系
**唯一例外:**
- ✅ **仅当用户明确要求**时才编写测试代码
- ✅ 用户必须主动说明"请写测试"或"需要测试"
| 层级 | 测试类型 | 来源 | 验证什么 | 位置 |
|------|---------|------|---------|------|
| **L1** | 验收测试 | Spec Scenario | 单 API 契约 | `tests/acceptance/` |
| **L2** | 流程测试 | Spec Business Flow | 业务场景完整性 | `tests/flows/` |
| **L3** | 单元测试 | 复杂逻辑 | 算法/规则正确性 | 模块内 `*_test.go` |
**原因说明:**
- 业务系统的正确性通过人工验证和生产环境监控保证
- 测试代码的维护成本高于价值
- 快速迭代优先于测试覆盖率
### 验收测试规范
- **来源于 Spec**:每个 Scenario 对应一个测试用例
- **测试先于实现**:在功能实现前生成,预期全部 FAIL
- **必须有破坏点**:每个测试注释说明什么代码变更会导致失败
- **使用 IntegrationTestEnv**:不要 mock 依赖
详见:[tests/acceptance/README.md](tests/acceptance/README.md)
### 流程测试规范
- **来源于 Spec Business Flow**:每个 Flow 对应一个测试
- **跨 API 验证**:多个 API 调用的组合行为
- **状态共享**:流程中的数据在 steps 之间传递
- **依赖声明**:每个 step 声明依赖哪些前置 step
详见:[tests/flows/README.md](tests/flows/README.md)
### 单元测试精简规则
**保留**
- ✅ 纯函数(计费计算、分佣算法)
- ✅ 状态机(订单状态流转)
- ✅ 复杂业务规则(层级校验、权限计算)
- ✅ 边界条件(时间、金额、精度)
**删除/不再写**
- ❌ 简单 CRUD已被验收测试覆盖
- ❌ DTO 转换
- ❌ 配置读取
- ❌ 重复测试同一逻辑
### ⚠️ 测试真实性原则(严格遵守)
**测试必须真正验证功能,禁止绕过核心逻辑:**
| 规则 | 说明 |
|------|------|
| ❌ 禁止传递 nil 绕过依赖 | 如果功能依赖外部服务(如对象存储、第三方 API测试必须验证该依赖的调用 |
| ❌ 禁止只测试部分流程 | 如果功能包含 A → B → C 三步,不能只测试 B 而跳过 A 和 C |
| ❌ 禁止声称"测试通过"但未验证核心逻辑 | 测试通过必须意味着功能真正可用 |
| ❌ 禁止擅自使用 Mock | 尽量使用真实服务进行集成测试,如需使用 Mock 必须先询问用户并获得同意 |
| ✅ 必须验证端到端流程 | 新增功能必须有完整的集成测试覆盖整个调用链 |
| ✅ 缺少配置时必须询问 | 如果测试需要的配置(如 API Key、环境变量缺失必须询问用户而非跳过测试 |
**反面案例**
```go
// ❌ 错误:传递 nil 绕过 storageService只测试了 processImport
handler := NewIotCardImportHandler(db, redis, store1, store2, nil, logger)
result := handler.processImport(ctx, task) // 跳过了 downloadAndParseCSV
// ✅ 正确:使用真实服务测试完整流程
handler := NewIotCardImportHandler(db, redis, store1, store2, realStorageService, logger)
handler.HandleIotCardImport(ctx, asynqTask) // 测试完整流程,验证真实上传/下载
```
**测试超时 = 生产超时**
- 集成测试超时意味着生产环境也可能超时
- 发现超时必须排查原因,不能简单跳过或增加超时时间
### 测试连接管理(必读)
**详细规范**: [docs/testing/test-connection-guide.md](docs/testing/test-connection-guide.md)
**⚠️ 运行测试必须先加载环境变量**:
```bash
# ✅ 正确
source .env.local && go test -v ./internal/service/xxx/...
# ❌ 错误(会因缺少配置而失败)
go test -v ./internal/service/xxx/...
```
**标准模板**:
```go
func TestXxx(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := postgres.NewXxxStore(tx, rdb)
// 测试代码...
}
```
**核心函数**:
- `NewTestTransaction(t)`: 创建测试事务,自动回滚
- `GetTestRedis(t)`: 获取全局 Redis 连接
- `CleanTestRedisKeys(t, rdb)`: 自动清理测试 Redis 键
**集成测试环境**HTTP API 测试):
```go
func TestAPI_Create(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
t.Run("成功创建", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/resources", jsonBody)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
})
}
```
- `NewIntegrationTestEnv(t)`: 创建完整测试环境事务、Redis、App、Token
- `AsSuperAdmin()`: 以超级管理员身份请求
- `AsUser(account)`: 以指定账号身份请求
**禁止使用(已移除)**:
-`SetupTestDB` / `TeardownTestDB` / `SetupTestDBWithStore`
**替代方案:**
- 使用 PostgreSQL MCP 工具手动验证数据
- 使用 Postman/curl 手动测试 API
- 依赖生产环境日志和监控发现问题
## 性能要求
@@ -311,10 +341,9 @@ func TestAPI_Create(t *testing.T) {
3. ✅ 使用统一错误处理
4. ✅ 常量定义在 pkg/constants/
5. ✅ Go 惯用法(非 Java 风格)
6.包含测试计划
7.性能考虑
8.文档更新计划
9. ✅ 中文优先
6.性能考虑
7.文档更新计划
8.中文优先
## Code Review 检查清单
@@ -330,16 +359,18 @@ func TestAPI_Create(t *testing.T) {
- [ ] 常量定义在 `pkg/constants/`
- [ ] 使用 Go 惯用法(非 Java 风格)
### 测试覆盖
- [ ] 核心业务逻辑测试覆盖率 ≥ 90%
- [ ] 所有 API 端点有集成测试
- [ ] 测试验证真实功能(不绕过核心逻辑)
### 文档和注释
- [ ] 所有注释使用中文
- [ ] 导出函数/类型有文档注释
- [ ] API 路径注释与真实路由一致
### 幂等性
- [ ] 创建类写操作有 Redis 业务键防重
- [ ] 状态变更使用条件更新(`WHERE status = expected`
- [ ] 余额/库存变更使用乐观锁version 字段)
- [ ] 分布式锁使用 `defer` 确保释放
- [ ] Redis Key 定义在 `pkg/constants/redis.go`
### 越权防护规范
**适用场景**:任何涉及跨用户、跨店铺、跨企业的资源访问
@@ -375,6 +406,114 @@ func TestAPI_Create(t *testing.T) {
- 越权访问统一返回:`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 字段) |
### 审计日志规范
**适用场景**:任何敏感操作(账号管理、权限变更、数据删除等)