docs(01-p0): 创建 Phase 1 P0 紧急修复执行计划(5 个 Plan,涵盖 CRITICAL-01~08)

This commit is contained in:
2026-03-27 22:35:28 +08:00
parent 5862018e7c
commit f48f11beb4
6 changed files with 1169 additions and 2 deletions

View File

@@ -31,7 +31,14 @@
3. 代理代购下级场景(场景 5的订单金额正确使用 operatorCostPrice佣金链计算正常
4. 批量导入设备 Excel 后,设备记录中含 IMEI 字段Gateway 调用不再因 IMEI 缺失而失败
5. 并发提交提现申请时RowsAffected 检查生效,不会创建重复提现单
**Plans**: TBD
**Plans**: 5 plans
Plans:
- [ ] 01-01-PLAN.md — CRITICAL-01/02实名状态常量统一 + C 端实名校验修复Wave 1
- [ ] 01-02-PLAN.md — CRITICAL-03实名激活任务断链PollingHandler 注入 asynqClientWave 2
- [ ] 01-03-PLAN.md — CRITICAL-04/05充值回调触发自动购包 + 佣金链补全Wave 2parallel
- [ ] 01-04-PLAN.md — CRITICAL-06代购订单 sellerCostPrice 6 处语义审查修正Wave 1parallel
- [ ] 01-05-PLAN.md — CRITICAL-07/08设备导入 IMEI + 提现冻结并发校验Wave 1parallel
---
@@ -111,7 +118,7 @@
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. P0 紧急修复 | 0/1 | Not started | - |
| 1. P0 紧急修复 | 0/5 | Planning complete | - |
| 2. 权限/财务/实名/轮询修复 | 0/1 | Not started | - |
| 3. 设备体系完善 | 0/1 | Not started | - |
| 4. 退款 + 支付 + 运营修复 | 0/1 | Not started | - |

View File

@@ -0,0 +1,224 @@
---
phase: 01-p0
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- internal/task/polling_handler.go
- internal/model/iot_card.go
- internal/service/client_order/service.go
autonomous: true
requirements:
- CRITICAL-01
- CRITICAL-02
must_haves:
truths:
- "parseRealnameStatus 返回 constants.RealNameStatusVerified=1不再返回硬编码 2"
- "isFirstRealname 判断使用常量比较,不再使用 == 2"
- "Model 注释与常量一致0=未实名, 1=已实名"
- "client_order 实名校验使用 constants.RealNameStatusVerified不再硬编码 1"
artifacts:
- path: internal/task/polling_handler.go
provides: "parseRealnameStatus 返回常量值isFirstRealname 使用常量"
contains: "constants.RealNameStatusVerified"
- path: internal/model/iot_card.go
provides: "Model 字段注释统一"
contains: "0-未实名 1-已实名"
- path: internal/service/client_order/service.go
provides: "实名校验改为常量比较"
contains: "constants.RealNameStatusVerified"
key_links:
- from: internal/task/polling_handler.go
to: pkg/constants/iot.go
via: "constants.RealNameStatusVerified"
pattern: "constants\\.RealNameStatusVerified"
- from: internal/service/client_order/service.go
to: pkg/constants/iot.go
via: "constants.RealNameStatusVerified"
pattern: "constants\\.RealNameStatusVerified"
---
<objective>
统一实名状态常量CRITICAL-01并修复 C 端实名校验CRITICAL-02
Purpose: 这是整个修复链路的前置条件。轮询写入 2、常量定义 1、校验用 1 三处不一致造成实名链路全面断裂。修复后实名状态全链路统一为 0=未实名、1=已实名。CRITICAL-02 依赖本 Plan 完成才能自动正确。
Output:
- polling_handler.go 写入值改为常量 1
- iot_card.go 注释与常量统一
- client_order/service.go 校验使用常量
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(核心参考)
@.sisyphus/plans/修正业务-完整方案.md
<interfaces>
<!-- 从 pkg/constants/iot.go 提取的常量executor 直接使用 -->
```go
// pkg/constants/iot.go已确认存在第 47-48 行附近)
const (
RealNameStatusNotVerified = 0 // 未实名
RealNameStatusVerified = 1 // 已实名
)
```
<!-- polling_handler.go 当前错误代码(需改掉) -->
<!-- 第 696-700 行parseRealnameStatus 返回 2错误 -->
<!-- 第 170 行isFirstRealname 使用 == 2错误 -->
<!-- client_order/service.go 当前代码(第 115 行附近) -->
<!-- assetInfo.RealNameStatus != 1硬编码需改为常量 -->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: 修复 polling_handler.go 实名状态写入值CRITICAL-01 核心)</name>
<files>internal/task/polling_handler.go, internal/model/iot_card.go</files>
<action>
**修改 `internal/task/polling_handler.go`**
`parseRealnameStatus` 函数(第 696-700 行附近):
```go
// 修改前
func (h *PollingHandler) parseRealnameStatus(realStatus bool) int {
if realStatus {
return 2 // 已实名
}
return 0 // 未实名
}
// 修改后使用常量per D-01
func (h *PollingHandler) parseRealnameStatus(realStatus bool) int {
if realStatus {
return constants.RealNameStatusVerified // = 1已实名
}
return constants.RealNameStatusNotVerified // = 0未实名
}
```
`isFirstRealname` 判断(第 170 行附近):
```go
// 修改前
isFirstRealname := (card.RealNameStatus == 0 || card.RealNameStatus == 1) && newRealnameStatus == 2
// 修改后使用常量比较per D-01
isFirstRealname := card.RealNameStatus != constants.RealNameStatusVerified &&
newRealnameStatus == constants.RealNameStatusVerified
```
③ 全局搜索 `polling_handler.go``internal/model/dto/` 下 DTO 注释,将所有 `2=已实名``1实名中` 改为 `0=未实名, 1=已实名`(执行:`grep -rn "2=已实名\|1实名中\|RealNameStatus.*2" internal/model/dto/ --include="*.go"`)。
**修改 `internal/model/iot_card.go`**
找到第 30 行附近 `RealNameStatus` 字段的 gorm comment
```go
// 修改前
RealNameStatus int `gorm:"...; comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)"`
// 修改后统一per D-01
RealNameStatus int `gorm:"...; comment:实名状态 0-未实名 1-已实名"`
```
修改完成后执行:`go build ./...`,确认编译通过,提交一个独立 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "return 2" internal/task/polling_handler.go && echo "FAIL: 仍存在 return 2" || echo "PASS: return 2 已消除"</automated>
</verify>
<done>
- polling_handler.go 中 parseRealnameStatus 不再 return 2改为 return constants.RealNameStatusVerified
- isFirstRealname 不再用 == 2改为 == constants.RealNameStatusVerified
- iot_card.go 注释统一为 0-未实名 1-已实名
- go build ./... 编译通过
</done>
</task>
<task type="auto">
<name>Task 2: 修复 C 端实名校验CRITICAL-02</name>
<files>internal/service/client_order/service.go</files>
<action>
**修改 `internal/service/client_order/service.go`(第 115 行附近):**
找到实名校验判断:
```go
// 修改前(硬编码 1本质上是 A-1 修完后 DB 里是 1 才会正确,但语义要用常量)
if packagesNeedRealname(validationResult.Packages) && assetInfo.RealNameStatus != 1 {
return errors.New(errors.CodeForbidden, "请先完成实名认证")
}
// 修改后使用常量per CRITICAL-02 规格)
if packagesNeedRealname(validationResult.Packages) && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
return errors.New(errors.CodeForbidden, "请先完成实名认证")
}
```
同时检查同文件和 `polling_handler.go:982` 是否有其他硬编码 `!= 1` 的实名判断(`rg "RealNameStatus.*!= 1\|!= 1.*RealNameStatus" internal/`),若有,同样改为常量。
确认 constants 包已正确 import若未 import在 import 块补充 `pkgpath/constants`)。
修改完成后执行:`go build ./...`,编译通过后提交独立 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "RealNameStatus != 1" internal/service/client_order/service.go && echo "FAIL: 仍有硬编码" || echo "PASS: 硬编码已消除"</automated>
</verify>
<done>
- client_order/service.go 实名校验改为 constants.RealNameStatusVerified
- go build ./... 编译通过
- rg 确认不再有 != 1 的硬编码实名判断
</done>
</task>
</tasks>
<verification>
整体验收(两个任务完成后):
```bash
# 1. 编译验证
go build ./...
# 2. 确认 parseRealnameStatus 不再写 2
rg "return 2" internal/task/polling_handler.go
# 3. 确认实名判断全部使用常量
rg "RealNameStatusVerified" internal/task/polling_handler.go internal/service/client_order/service.go
# 4. 搜索遗漏的硬编码
rg "newRealnameStatus == 2\|RealNameStatus == 2\|RealNameStatus != 1" internal/ --include="*.go"
```
人工验收(参见修正业务-完整方案.md 方案 A 验收清单 A-1、A-2
- DBHub 执行:`SELECT DISTINCT real_name_status FROM tb_iot_card ORDER BY 1;`
- 预期:只存在 0 和 1不存在 2
</verification>
<success_criteria>
1. `go build ./...` 编译通过,无错误
2. `parseRealnameStatus(true)` 返回 1constants.RealNameStatusVerified不再返回 2
3. `isFirstRealname` 不再使用 == 2 判断
4. `client_order/service.go` 实名校验使用常量,不再硬编码 1
5. Model 注释与常量定义一致
6. 两个独立 commit 已提交
</success_criteria>
<output>
完成后创建 `.planning/phases/01-p0/01-01-SUMMARY.md`,记录:
- 修改了哪些文件(文件路径 + 改动说明)
- 关键代码片段(修改前后对比)
- 编译验证结果
- CRITICAL-01 和 CRITICAL-02 完成状态
</output>

View File

@@ -0,0 +1,225 @@
---
phase: 01-p0
plan: 02
type: execute
wave: 2
depends_on:
- 01-01
files_modified:
- internal/task/polling_handler.go
- pkg/queue/handler.go
autonomous: true
requirements:
- CRITICAL-03
must_haves:
truths:
- "PollingHandler struct 有 asynqClient *asynq.Client 字段"
- "NewPollingHandler 构造函数接受 asynqClient 参数"
- "triggerFirstRealnameActivation 使用 asynqClient.EnqueueContext 入队,不再 RPush"
- "_ = task 这行被删除"
- "registerPollingHandlers 传入 h.asynqClient 给 NewPollingHandler"
artifacts:
- path: internal/task/polling_handler.go
provides: "PollingHandler 注入 asynq.ClienttriggerFirstRealnameActivation 正确入队"
contains: "asynqClient.EnqueueContext"
- path: pkg/queue/handler.go
provides: "registerPollingHandlers 传入 asynqClient"
contains: "h.asynqClient"
key_links:
- from: pkg/queue/handler.go
to: internal/task/polling_handler.go
via: "NewPollingHandler(h.db, h.redis, h.asynqClient, ...)"
pattern: "NewPollingHandler"
- from: internal/task/polling_handler.go
to: asynq.Client
via: "h.asynqClient.EnqueueContext(ctx, task)"
pattern: "asynqClient\\.EnqueueContext"
---
<objective>
修复实名激活任务断链CRITICAL-03为 PollingHandler 注入 asynq.Client删除 RPush 降级方案,改用正式 Asynq 入队。
Purpose: 首次实名后自动激活"囤货套餐"的完整链路当前完全断裂——task 被 `_ = task` 丢弃,实际走 Redis RPush 降级,但无消费者消费这个列表。修复后套餐激活任务正常进入 Asynq 队列被消费。
Output:
- polling_handler.go: 新增 asynqClient 字段triggerFirstRealnameActivation 改为 EnqueueContext
- pkg/queue/handler.go: registerPollingHandlers 传入 h.asynqClient
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/01-p0/01-01-SUMMARY.md
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(方案 A-3第 165-299 行)
@.sisyphus/plans/修正业务-完整方案.md
<interfaces>
<!-- pkg/queue/handler.go Handler struct 已有 asynqClient 字段 -->
```go
// pkg/queue/handler.go Handler struct已存在
type Handler struct {
db *gorm.DB
redis *redis.Client
asynqClient *asynq.Client // ← 已有此字段,直接传给 NewPollingHandler
// ...
}
// registerPollingHandlers 当前调用(需修改)
pollingHandler := task.NewPollingHandler(
h.db,
h.redis,
// h.asynqClient ← 缺少此行
h.gatewayClient,
h.workerResult.Services.UsageService,
h.logger,
)
```
<!-- polling_handler.go 当前错误代码triggerFirstRealnameActivation 第 1100 行) -->
```go
// 当前降级代码(需删除 RPush 部分,直接用 EnqueueContext
task := asynq.NewTask(constants.TaskTypePackageFirstActivation, payloadBytes, ...)
activationKey := constants.RedisPollingManualQueueKey(constants.TaskTypePackageFirstActivation)
h.redis.RPush(ctx, activationKey, string(payloadBytes)) // ← 删掉
_ = task // ← 删掉
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: PollingHandler 注入 asynqClientCRITICAL-03</name>
<files>internal/task/polling_handler.go, pkg/queue/handler.go</files>
<action>
**步骤 1修改 `internal/task/polling_handler.go` struct 定义**
在 PollingHandler struct第 31-42 行附近)新增字段:
```go
type PollingHandler struct {
db *gorm.DB
redis *redis.Client
asynqClient *asynq.Client // 新增
gatewayClient *gateway.Client
// ... 其余字段不变
}
```
**步骤 2修改 NewPollingHandler 构造函数签名(第 44 行附近)**
新增 `asynqClient *asynq.Client` 参数位置redis 之后gatewayClient 之前):
```go
func NewPollingHandler(
db *gorm.DB,
redis *redis.Client,
asynqClient *asynq.Client, // 新增
gatewayClient *gateway.Client,
usageService *packagepkg.UsageService,
logger *zap.Logger,
) *PollingHandler {
return &PollingHandler{
db: db,
redis: redis,
asynqClient: asynqClient, // 新增
gatewayClient: gatewayClient,
// ... 其余字段不变
}
}
```
**步骤 3修改 triggerFirstRealnameActivation第 1100 行附近)**
删除 RPush 降级代码,改为 EnqueueContext
```go
// 删除这两行:
// activationKey := constants.RedisPollingManualQueueKey(constants.TaskTypePackageFirstActivation)
// h.redis.RPush(ctx, activationKey, string(payloadBytes))
// _ = task
// 替换为:
if _, err := h.asynqClient.EnqueueContext(ctx, task); err != nil {
h.logger.Warn("提交首次实名激活任务失败",
zap.Uint("package_usage_id", pkg.ID),
zap.Error(err))
continue
}
```
注意:`task` 变量已在上方声明,不需要重新创建。
**步骤 4修改 `pkg/queue/handler.go:registerPollingHandlers`(第 151 行附近)**
在 NewPollingHandler 调用中补充 `h.asynqClient` 参数:
```go
pollingHandler := task.NewPollingHandler(
h.db,
h.redis,
h.asynqClient, // 新增Handler 结构体已有此字段
h.gatewayClient,
h.workerResult.Services.UsageService,
h.logger,
)
```
修改完成后执行 `go build ./...`,确认编译通过,提交独立 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "_ = task" internal/task/polling_handler.go && echo "FAIL: _ = task 仍存在" || echo "PASS: _ = task 已删除"</automated>
</verify>
<done>
- PollingHandler struct 含 asynqClient 字段
- NewPollingHandler 接受 asynqClient 参数
- triggerFirstRealnameActivation 使用 asynqClient.EnqueueContext不再 RPush
- _ = task 这行已被删除
- pkg/queue/handler.go 传入 h.asynqClient
- go build ./... 编译通过
</done>
</task>
</tasks>
<verification>
整体验收:
```bash
# 1. 编译验证
go build ./...
# 2. 确认降级代码已删除
rg "_ = task" internal/task/polling_handler.go
rg "RPush.*TaskTypePackageFirstActivation\|RedisPollingManualQueueKey" internal/task/polling_handler.go
# 3. 确认正确入队代码存在
rg "asynqClient.EnqueueContext" internal/task/polling_handler.go
# 4. 确认 registerPollingHandlers 传了 asynqClient
rg "NewPollingHandler" pkg/queue/handler.go
```
人工验收(参见修正业务-完整方案.md A-3 验收清单):
- Worker 日志:首次实名触发时应出现实名激活任务入队日志
- DBHub`SELECT id, status, pending_realname_activation, activated_at FROM tb_package_usage WHERE pending_realname_activation=true ORDER BY id DESC LIMIT 20;`
</verification>
<success_criteria>
1. `go build ./...` 编译通过
2. PollingHandler 有 asynqClient 字段且被正确初始化
3. triggerFirstRealnameActivation 使用 EnqueueContext不再 RPush
4. `_ = task` 行不存在
5. 独立 commit 已提交
</success_criteria>
<output>
完成后创建 `.planning/phases/01-p0/01-02-SUMMARY.md`,记录:
- 修改文件及改动说明
- 关键代码对比(修改前后)
- 编译验证结果
- CRITICAL-03 完成状态
</output>

View File

@@ -0,0 +1,314 @@
---
phase: 01-p0
plan: 03
type: execute
wave: 2
depends_on:
- 01-01
files_modified:
- internal/service/recharge/service.go
- internal/bootstrap/services.go
- pkg/queue/handler.go
- internal/task/auto_purchase.go
autonomous: true
requirements:
- CRITICAL-04
- CRITICAL-05
must_haves:
truths:
- "recharge.Service struct 有 queueClient *queue.Client 字段"
- "HandlePaymentCallback 在充值成功后,若 LinkedPackageIDs 非空,入队 TaskTypeAutoPurchaseAfterRecharge"
- "AutoPurchaseHandler struct 有 asynqClient *asynq.Client 字段"
- "AutoPurchaseHandler.ProcessTask 事务提交成功后入队 TaskTypeCommission"
- "pkg/queue/handler.go 注册了 AutoPurchaseHandlerregisterAutoPurchaseHandler 被调用)"
- "internal/bootstrap/services.go 的 recharge.New() 传入 queueClient 参数"
artifacts:
- path: internal/service/recharge/service.go
provides: "queueClient 注入HandlePaymentCallback 触发自动购包"
contains: "queueClient.EnqueueTask"
- path: pkg/queue/handler.go
provides: "registerAutoPurchaseHandler 方法和注册调用"
contains: "registerAutoPurchaseHandler"
- path: internal/task/auto_purchase.go
provides: "asynqClient 注入ProcessTask 触发佣金"
contains: "TaskTypeCommission"
- path: internal/bootstrap/services.go
provides: "recharge.New() 传入 queueClient"
contains: "queueClient"
key_links:
- from: internal/service/recharge/service.go
to: pkg/queue/handler.go
via: "queueClient.EnqueueTask(TaskTypeAutoPurchaseAfterRecharge)"
pattern: "TaskTypeAutoPurchaseAfterRecharge"
- from: internal/task/auto_purchase.go
to: asynq.Client
via: "h.asynqClient.EnqueueContext(ctx, commissionTask)"
pattern: "TaskTypeCommission"
---
<objective>
修复充值回调后自动购包触发CRITICAL-04和自动购包后佣金链补全CRITICAL-05
Purpose: C 端"强充绑套餐"功能当前完全断裂——充值成功后无任何代码触发自动购包任务;即使购包完成,佣金链也不会触发。本 Plan 打通完整链路:充值回调 → 自动购包任务入队 → 执行购包 → 佣金计算任务入队。
Output:
- recharge/service.go: 注入 queueClientHandlePaymentCallback 触发自动购包入队
- auto_purchase.go: 注入 asynqClientProcessTask 事务成功后触发佣金入队
- pkg/queue/handler.go: 注册 AutoPurchaseHandler
- bootstrap/services.go: recharge.New() 传入 queueClient
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/01-p0/01-01-SUMMARY.md
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(方案 A-4第 301-575 行)
@.sisyphus/plans/修正业务-完整方案.md
<interfaces>
<!-- 关键事实(来自规格书和 CONTEXT.md -->
<!--
- pkg/queue/Client 有 EnqueueTask(ctx, taskType, payload, opts...) 方法 (pkg/queue/client.go:38)
适合 Service 层调用(自动 Marshal
- pkg/queue/handler.go Handler struct 持有 asynqClient *asynq.Client
适合 Worker 任务处理器使用(直接 EnqueueContext
- TaskTypeAutoPurchaseAfterRecharge = "task:auto_purchase_after_recharge" 常量已存在
- AutoPurchaseHandler 已在 internal/task/auto_purchase.go但未在 handler.go 注册
- QueueClient 已在 internal/bootstrap/dependencies.go 定义,已传给 Order/IotCardImport/DeviceImport
- AssetRechargeRecord.LinkedPackageIDs 字段internal/model/asset_wallet.go:91JSONB
-->
<!-- recharge.Service 当前结构(需新增 queueClient -->
```go
// internal/service/recharge/service.go当前无 queueClient
type Service struct {
db *gorm.DB
// ... 其他字段
}
func New(db *gorm.DB, /* ... */) *Service {
return &Service{db: db, /* ... */}
}
```
<!-- auto_purchase.go NewAutoPurchaseHandler 当前签名(需新增 asynqClient -->
<!-- 注意:规格书里的 nil 列表可能与当前签名不完全一致 -->
<!-- 务必先读实际文件确认参数列表,再决定如何传入 asynqClient -->
<!-- 常量(直接使用) -->
```go
const (
TaskTypeAutoPurchaseAfterRecharge = "task:auto_purchase_after_recharge"
TaskTypeCommission = "..." // 已存在,从 pkg/constants/constants.go 确认
QueueDefault = "..."
QueueLow = "..." // 佣金任务用低优先级队列
)
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: recharge.Service 注入 queueClient触发自动购包CRITICAL-04 Part 1</name>
<files>internal/service/recharge/service.go, internal/bootstrap/services.go</files>
<action>
**步骤 1先读 `internal/service/recharge/service.go` 确认当前 struct 和 New() 参数列表。**
在 Service struct 新增字段:
```go
type Service struct {
db *gorm.DB
// ... 其余字段不变 ...
queueClient *queue.Client // 新增
}
```
在 New() 新增参数(位于最后):
```go
func New(
db *gorm.DB,
// ... 其余参数 ...
queueClient *queue.Client, // 新增
) *Service {
return &Service{
// ... 其余字段 ...
queueClient: queueClient,
}
}
```
确认 import 中有 `"pkgpath/pkg/queue"`(按实际包路径补充)。
**步骤 2在 HandlePaymentCallback 充值成功后添加自动购包入队(第 272 行附近)**
在事务成功返回、记录充值成功日志之前,添加:
```go
// 充值成功后,如有关联套餐则触发自动购包(强充绑套餐功能)
linkedIDs := recharge.LinkedPackageIDs
if len(linkedIDs) > 0 && string(linkedIDs) != "[]" && string(linkedIDs) != "null" {
if s.queueClient != nil {
taskPayload := task.AutoPurchasePayload{RechargeRecordID: recharge.ID}
if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeAutoPurchaseAfterRecharge, taskPayload,
asynq.Queue(constants.QueueDefault),
asynq.MaxRetry(3),
); err != nil {
s.logger.Error("自动购包任务入队失败",
zap.Uint("recharge_id", recharge.ID),
zap.Error(err))
// 不影响充值成功,仅记录日志
}
}
}
```
**步骤 3修改 `internal/bootstrap/services.go`**
找到 `recharge.New(...)` 调用位置(执行 `rg "recharge.New\|recharge\.New" internal/bootstrap/`),在参数列表末尾补充 `deps.QueueClient`
```go
rechargeSvc := recharge.New(
deps.DB,
// ... 其他已有参数 ...
deps.QueueClient, // 新增
)
```
`QueueClient` 已在 `dependencies.go:24` 定义,无需新增字段。
完成后 `go build ./...`,提交 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "queueClient" internal/service/recharge/service.go | grep -c "queueClient" | grep -v "^0$" && echo "PASS: queueClient 已注入" || echo "FAIL"</automated>
</verify>
<done>
- recharge.Service struct 含 queueClient 字段
- HandlePaymentCallback 在充值成功、LinkedPackageIDs 非空时触发入队
- bootstrap/services.go 的 recharge.New() 传入 deps.QueueClient
- go build ./... 编译通过
</done>
</task>
<task type="auto">
<name>Task 2: AutoPurchaseHandler 注入 asynqClient + 注册 + 触发佣金CRITICAL-04 Part 2 + CRITICAL-05</name>
<files>internal/task/auto_purchase.go, pkg/queue/handler.go</files>
<action>
**步骤 1先读 `internal/task/auto_purchase.go` 确认 AutoPurchaseHandler struct 和 NewAutoPurchaseHandler 的真实参数列表。**
在 AutoPurchaseHandler struct 新增字段:
```go
type AutoPurchaseHandler struct {
// ... 已有字段 ...
asynqClient *asynq.Client // 新增,用于事务后触发佣金任务
}
```
在 NewAutoPurchaseHandler 函数签名中新增 `asynqClient *asynq.Client` 参数(位于 logger 参数之前),并在返回时赋值 `asynqClient: asynqClient`
**⚠️ 重要:** 不要盲目照搬规格书的 nil 列表。必须按实际签名决定在哪里插入 `asynqClient`
**步骤 2修改 ProcessTask事务成功后触发佣金CRITICAL-05**
`ProcessTask()` 函数中:
① 在事务前声明 `var createdOrderID uint`
② 在事务内创建 order 后赋值:`createdOrderID = order.ID`
③ 事务提交成功 `return nil` 后,添加佣金任务入队:
```go
// 事务提交成功后,触发佣金计算(不在事务内,防止任务提交后事务回滚的数据一致性问题)
if h.asynqClient != nil && createdOrderID > 0 {
payloadBytes, err := sonic.Marshal(map[string]any{"order_id": createdOrderID})
if err != nil {
h.logger.Warn("佣金任务载荷序列化失败",
zap.Uint("order_id", createdOrderID),
zap.Error(err))
} else {
commissionTask := asynq.NewTask(constants.TaskTypeCommission, payloadBytes,
asynq.Queue(constants.QueueLow),
asynq.MaxRetry(3),
)
if _, err := h.asynqClient.EnqueueContext(ctx, commissionTask); err != nil {
h.logger.Warn("自动购包后提交佣金任务失败",
zap.Uint("order_id", createdOrderID),
zap.Error(err))
}
}
}
```
注意:使用 `sonic.Marshal`(项目规范,不用 `encoding/json`)。
**步骤 3修改 `pkg/queue/handler.go` 注册 AutoPurchaseHandler**
新增方法 `registerAutoPurchaseHandler()`,并在 `RegisterHandlers()` 中调用:
```go
func (h *Handler) registerAutoPurchaseHandler() {
autoPurchaseHandler := task.NewAutoPurchaseHandler(
// 按 auto_purchase.go 实际签名传参
// 在合适位置传 h.asynqClient
// 其他 store 参数若已有传参规律则沿用;若为 nil 则查看其他 handler 的注册方式
)
h.mux.HandleFunc(constants.TaskTypeAutoPurchaseAfterRecharge, autoPurchaseHandler.ProcessTask)
h.logger.Info("注册自动购包任务处理器", zap.String("task_type", constants.TaskTypeAutoPurchaseAfterRecharge))
}
```
`RegisterHandlers()` 中调用:`h.registerAutoPurchaseHandler()`
**参考现有 handler 注册**:先读 `pkg/queue/handler.go` 中其他 register 方法(如 registerCommissionHandler确认 store 参数传递规律。
完成后 `go build ./...`,提交 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "TaskTypeAutoPurchaseAfterRecharge" pkg/queue/handler.go | grep "HandleFunc" && echo "PASS: AutoPurchaseHandler 已注册" || echo "FAIL: 未注册"</automated>
</verify>
<done>
- AutoPurchaseHandler struct 含 asynqClient 字段
- ProcessTask 事务成功后入队 TaskTypeCommission不在事务内
- pkg/queue/handler.go 注册了 AutoPurchaseHandler
- RegisterHandlers() 调用了 registerAutoPurchaseHandler()
- go build ./... 编译通过
</done>
</task>
</tasks>
<verification>
整体验收:
```bash
# 1. 编译验证
go build ./...
# 2. 确认入队代码存在
rg "TaskTypeAutoPurchaseAfterRecharge" internal/service/recharge/service.go
rg "TaskTypeCommission" internal/task/auto_purchase.go
rg "registerAutoPurchaseHandler\|TaskTypeAutoPurchaseAfterRecharge" pkg/queue/handler.go
# 3. 确认 bootstrap 传入了 queueClient
rg "recharge.New\|recharge\.New" internal/bootstrap/services.go
```
人工验收(参见修正业务-完整方案.md A-4 验收清单):
- DBHub 验证顺序tb_asset_recharge_record(auto_purchase_status=success) → tb_order(自动购包订单) → tb_package_usage(套餐激活) → tb_commission_record(佣金记录)
</verification>
<success_criteria>
1. `go build ./...` 编译通过,无错误
2. recharge.Service.HandlePaymentCallback 在 LinkedPackageIDs 非空时触发自动购包入队
3. AutoPurchaseHandler.ProcessTask 事务成功后触发佣金入队(事务外)
4. AutoPurchaseHandler 在 RegisterHandlers 中注册
5. bootstrap/services.go recharge.New() 传入 queueClient
6. 两个独立 commit 已提交CRITICAL-04 一个CRITICAL-05 一个)
</success_criteria>
<output>
完成后创建 `.planning/phases/01-p0/01-03-SUMMARY.md`,记录:
- 修改文件及改动说明
- 关键代码路径(充值回调 → 入队 → 消费 → 佣金)
- 编译验证结果
- CRITICAL-04 和 CRITICAL-05 完成状态
</output>

View File

@@ -0,0 +1,164 @@
---
phase: 01-p0
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- internal/service/order/service.go
autonomous: true
requirements:
- CRITICAL-06
must_haves:
truths:
- "场景 5 分支(代理代购下级)的 sellerCostPrice 使用 operatorCostPrice不再是 buyerCostPrice"
- "其他 5 处 sellerCostPrice 赋值经过语义审查,确认正确或按需修正"
- "佣金差额计算基准正确,场景 5 的上级代理能获得应有佣金"
artifacts:
- path: internal/service/order/service.go
provides: "场景 5 sellerCostPrice 修正"
contains: "operatorCostPrice"
key_links:
- from: internal/service/order/service.go
to: "佣金计算链路"
via: "sellerCostPrice 作为佣金差额基准传递"
pattern: "sellerCostPrice"
---
<objective>
逐一审查并修正代购订单中 sellerCostPrice 的 6 处赋值CRITICAL-06确保场景 5代理代购下级使用正确的成本价基准。
Purpose: sellerCostPrice 是佣金差额计算的基准。场景 5 中错误使用 buyerCostPrice下级成本价导致整个佣金链差额为 0所有上级代理无法获得应有佣金。修复后代购场景佣金链正常工作。
Output:
- order/service.go: 场景 5 的 sellerCostPrice 改为 operatorCostPrice其余 5 处经语义审查确认或按需修正
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(方案 A-5第 579-618 行)
@.sisyphus/plans/修正业务-完整方案.md
<interfaces>
<!-- 五种购买场景对应表(来自规格书 A-5
| # | 操作方 | 资产所属 | sellerCostPrice 应为 |
|---|--------|------------|--------------------------|
| 1 | 平台 | 平台自己客户 | N/A线下不计佣金 |
| 2 | 平台 | 某代理客户 | 该代理成本价operatorCostPrice 即代理价)|
| 3 | 平台 | 某代理客户 | 该代理成本价(线下场景) |
| 4 | 代理 | 自己客户 | 自己成本价(即 operatorCostPrice|
| 5 | 代理 | 下级代理客户 | **operatorCostPrice自己的成本价非下级的** ← Bug 在此 |
-->
<!-- 6 处确认位置(来自 CONTEXT.md code_context -->
<!--
internal/service/order/service.go 第 187, 262, 472, 548, 734, 809 行
其中 525-548 行是场景 5sellerCostPrice = buyerCostPrice 是错误的
-->
<!-- 审查决策依据per D-01, D-02, D-03 -->
<!--
- 逐一语义审查,按场景对照表判断
- 不做模式匹配式全量替换
- 有些 buyerCostPrice 赋值是正确的(如场景 1/2/3/4 的某些分支)
-->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: 逐一审查并修正 sellerCostPrice 6 处赋值CRITICAL-06</name>
<files>internal/service/order/service.go</files>
<action>
**步骤 1定位 6 处赋值**
```bash
rg -n "sellerCostPrice" internal/service/order/service.go
```
确认规格书所述的 6 处(第 187, 262, 472, 548, 734, 809 行)是否与实际一致(行号可能有偏移)。
**步骤 2对照五种场景表逐一审查**
对每一处 `sellerCostPrice = buyerCostPrice`,阅读其所在分支的上下文,确认:
- 这里的"操作方"是谁?(平台/代理)
- 资产归属是谁?(自己客户/下级代理客户)
- 对照上方场景表sellerCostPrice 应该是哪个变量?
**步骤 3修正确认有问题的位置**
规格书明确指出场景 5代理代购下级代理客户约在第 525-548 行:
```go
// 当前错误sellerCostPrice 用了下级成本价)
buyerCostPrice, _ := s.getCostPrice(ctx, *resourceShopID, firstPackageID) // 下级成本价
operatorCostPrice, _ := s.getCostPrice(ctx, operatorShopID, firstPackageID) // 自己成本价
totalAmount = buyerCostPrice // 下级成本价作为面值(正确)
actualPaidAmount = &operatorCostPrice // 实际扣自己钱包(正确)
sellerCostPrice = buyerCostPrice // ← 错误!应是 operatorCostPrice
// 修正后
sellerCostPrice = operatorCostPrice // ✅ 自己(代理)的成本价作为佣金差额基准
```
对其余 5 处,若语义审查确认正确(即该场景下 sellerCostPrice 本就应等于 buyerCostPrice则**不修改**,并在 SUMMARY 中记录每处审查结论。
**步骤 4执行 `go build ./...`,确认编译通过,提交独立 commitper D-05**
注意只改确认有问题的位置禁止全量替换per D-03
</action>
<verify>
<automated>go build ./... && rg -n "sellerCostPrice" internal/service/order/service.go</automated>
</verify>
<done>
- 6 处 sellerCostPrice 赋值全部经过语义审查
- 场景 5 分支已将 sellerCostPrice = buyerCostPrice 改为 sellerCostPrice = operatorCostPrice
- 其余各处审查结论已记录
- go build ./... 编译通过
- 独立 commit 已提交
</done>
</task>
</tasks>
<verification>
整体验收:
```bash
# 1. 编译验证
go build ./...
# 2. 查看所有 sellerCostPrice 赋值(人工确认场景 5 已修正)
rg -n "sellerCostPrice" internal/service/order/service.go
# 3. 搜索场景 5 分支(代购下级)是否仍然存在 sellerCostPrice = buyerCostPrice
# 需人工确认第 525-548 行附近已修改
```
人工验收(参见修正业务-完整方案.md A-5 验收清单):
- DBHub 抽查"代理代购下级代理客户"的订单和佣金记录
- 预期tb_order.total_amount = 下级成本价,佣金链不再全部为 0
</verification>
<success_criteria>
1. `go build ./...` 编译通过
2. 场景 5代理代购下级sellerCostPrice = operatorCostPrice不再是 buyerCostPrice
3. 其余 5 处经语义审查,结论记录在 SUMMARY 中
4. 独立 commit 已提交
</success_criteria>
<output>
完成后创建 `.planning/phases/01-p0/01-04-SUMMARY.md`,记录:
- 6 处 sellerCostPrice 逐一审查结论(行号 + 场景判断 + 是否修改)
- 场景 5 修改前后代码对比
- 编译验证结果
- CRITICAL-06 完成状态
</output>

View File

@@ -0,0 +1,233 @@
---
phase: 01-p0
plan: 05
type: execute
wave: 1
depends_on: []
files_modified:
- pkg/utils/excel.go
- internal/task/device_import.go
- internal/service/my_commission/service.go
autonomous: true
requirements:
- CRITICAL-07
- CRITICAL-08
must_haves:
truths:
- "DeviceRow struct 含 IMEI string 字段"
- "ParseDeviceExcel 中表头映射支持 IMEI 列名imei/IMEI/设备IMEI/IMEI号"
- "device_import.go 创建 model.Device 时赋值 IMEI: row.IMEI"
- "my_commission/service.go 冻结余额后检查 RowsAffected == 0不满足时返回余额不足错误"
artifacts:
- path: pkg/utils/excel.go
provides: "DeviceRow 含 IMEI 字段ParseDeviceExcel 列名映射新增 IMEI"
contains: "IMEI"
- path: internal/task/device_import.go
provides: "导入时填充 IMEI 字段"
contains: "IMEI: row.IMEI"
- path: internal/service/my_commission/service.go
provides: "RowsAffected 校验,并发冻结失败返回错误"
contains: "RowsAffected == 0"
key_links:
- from: pkg/utils/excel.go
to: internal/task/device_import.go
via: "DeviceRow.IMEI → model.Device.IMEI"
pattern: "DeviceRow"
- from: internal/service/my_commission/service.go
to: "tx.Model(&AgentWallet{}).Updates()"
via: "result.RowsAffected == 0 → 返回余额不足错误"
pattern: "RowsAffected"
---
<objective>
修复设备导入缺失 IMEI 字段CRITICAL-07和提现冻结并发校验缺失CRITICAL-08。这两个 Bug 互相独立,也与前序 Plan 无依赖,可以与 Plan 01/04 并行执行。
Purpose:
- CRITICAL-07: 设备导入后 IMEI 为空,导致 Gateway 调用失败IMEI 是调 Gateway 必填字段)
- CRITICAL-08: 并发提现请求中余额冻结失败RowsAffected=0未检测到导致余额不足时仍创建重复提现单
Output:
- excel.go: DeviceRow 新增 IMEI 字段ParseDeviceExcel 支持 IMEI 列名
- device_import.go: 创建 Device 时填充 IMEI
- my_commission/service.go: RowsAffected 校验,并发失败返回错误
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(方案 A-6 第 621-670 行,方案 A-7 第 673-705 行)
@.sisyphus/plans/修正业务-完整方案.md
<interfaces>
<!-- DeviceRow 当前结构pkg/utils/excel.go:35-43-->
```go
type DeviceRow struct {
Line int
VirtualNo string
// IMEI 字段缺失 ← 需新增
DeviceName string
DeviceModel string
DeviceType string
MaxSimSlots int
Manufacturer string
ICCIDs []string
}
```
<!-- my_commission/service.go 当前错误代码(第 162-170 行)-->
```go
// 当前:只检查 Error不检查 RowsAffected
if err := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ?", wallet.ID, req.Amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", req.Amount),
"frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount),
}).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "冻结余额失败")
}
// ← 缺少 RowsAffected == 0 的检查
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: 设备导入补充 IMEI 字段CRITICAL-07</name>
<files>pkg/utils/excel.go, internal/task/device_import.go</files>
<action>
**修改 `pkg/utils/excel.go`**
① 在 DeviceRow struct第 35-43 行)新增 IMEI 字段:
```go
type DeviceRow struct {
Line int
VirtualNo string
IMEI string // 新增,对应 Excel IMEI 列
DeviceName string
DeviceModel string
DeviceType string
MaxSimSlots int
Manufacturer string
ICCIDs []string
}
```
② 在 `ParseDeviceExcel` 函数的表头列名映射 switch-case约第 290-320 行)中,新增 IMEI 的 case
```go
case "imei", "IMEI", "设备IMEI", "IMEI号":
row.IMEI = cellValue
```
找到现有 case 的位置(如 `case "虚拟号", "virtual_no"`),在同一 switch 结构内添加 IMEI 的 case。
**修改 `internal/task/device_import.go`**
`processBatch` 函数(约第 178 行)创建 `model.Device{}` 的位置,新增 `IMEI: row.IMEI`
```go
device := &model.Device{
// ... 已有字段 ...
IMEI: row.IMEI, // 新增
}
```
执行 `go build ./...`,编译通过后提交独立 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "IMEI" pkg/utils/excel.go internal/task/device_import.go | grep -c "IMEI" | grep -v "^0$" && echo "PASS: IMEI 字段已添加" || echo "FAIL"</automated>
</verify>
<done>
- DeviceRow struct 含 IMEI string 字段
- ParseDeviceExcel 支持 imei/IMEI/设备IMEI/IMEI号 列名
- device_import.go processBatch 填充 IMEI: row.IMEI
- go build ./... 编译通过
</done>
</task>
<task type="auto">
<name>Task 2: 提现冻结并发校验CRITICAL-08</name>
<files>internal/service/my_commission/service.go</files>
<action>
**修改 `internal/service/my_commission/service.go`(第 162-170 行):**
将只检查 Error 的写法改为同时检查 RowsAffected
```go
// 修改前(只检查 Error
if err := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ?", wallet.ID, req.Amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", req.Amount),
"frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount),
}).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "冻结余额失败")
}
// 修改后(新增 RowsAffected 校验per CRITICAL-08 规格)
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ?", wallet.ID, req.Amount).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", req.Amount),
"frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount),
})
if result.Error != nil {
return errors.Wrap(errors.CodeInternalError, result.Error, "冻结余额失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突,请稍后重试")
}
```
执行 `go build ./...`,编译通过后提交独立 commitper D-05
</action>
<verify>
<automated>go build ./... && rg "RowsAffected" internal/service/my_commission/service.go && echo "PASS: RowsAffected 校验已添加" || echo "FAIL: RowsAffected 校验缺失"</automated>
</verify>
<done>
- my_commission/service.go 冻结余额使用 result := tx...Updates(),检查 result.Error 和 result.RowsAffected == 0
- RowsAffected == 0 时返回 errors.New(errors.CodeInsufficientBalance, ...)
- go build ./... 编译通过
</done>
</task>
</tasks>
<verification>
整体验收:
```bash
# 1. 编译验证
go build ./...
# 2. CRITICAL-07 验证
rg "IMEI" pkg/utils/excel.go # 应有 struct 字段和 case 映射
rg "IMEI" internal/task/device_import.go # 应有 IMEI: row.IMEI
# 3. CRITICAL-08 验证
rg "RowsAffected" internal/service/my_commission/service.go # 应有 == 0 检查
```
人工验收(参见修正业务-完整方案.md A-6、A-7 验收清单):
- CRITICAL-07: 设备导入后 DBHub 查询 `SELECT id, virtual_no, imei FROM tb_device ORDER BY id DESC LIMIT 20;` 确认 IMEI 已填充
- CRITICAL-08: 并发提现场景下DBHub 确认提现单数量与余额冻结成功次数一致
</verification>
<success_criteria>
1. `go build ./...` 编译通过
2. DeviceRow struct 含 IMEI 字段ParseDeviceExcel 支持 IMEI 列名device_import.go 填充 IMEI
3. my_commission/service.go 冻结余额后检查 RowsAffected为 0 时返回余额不足错误
4. 两个独立 commit 已提交CRITICAL-07 一个CRITICAL-08 一个)
</success_criteria>
<output>
完成后创建 `.planning/phases/01-p0/01-05-SUMMARY.md`,记录:
- CRITICAL-07: DeviceRow 改动说明ParseDeviceExcel 改动说明device_import.go 改动说明
- CRITICAL-08: my_commission/service.go 改动前后对比
- 编译验证结果
- CRITICAL-07 和 CRITICAL-08 完成状态
</output>