docs(01-p0): 创建 Phase 1 P0 紧急修复执行计划(5 个 Plan,涵盖 CRITICAL-01~08)
This commit is contained in:
@@ -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 注入 asynqClient)(Wave 2)
|
||||
- [ ] 01-03-PLAN.md — CRITICAL-04/05:充值回调触发自动购包 + 佣金链补全(Wave 2,parallel)
|
||||
- [ ] 01-04-PLAN.md — CRITICAL-06:代购订单 sellerCostPrice 6 处语义审查修正(Wave 1,parallel)
|
||||
- [ ] 01-05-PLAN.md — CRITICAL-07/08:设备导入 IMEI + 提现冻结并发校验(Wave 1,parallel)
|
||||
|
||||
---
|
||||
|
||||
@@ -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 | - |
|
||||
|
||||
224
.planning/phases/01-p0/01-01-PLAN.md
Normal file
224
.planning/phases/01-p0/01-01-PLAN.md
Normal 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 ./...`,确认编译通过,提交一个独立 commit(per 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 ./...`,编译通过后提交独立 commit(per 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)` 返回 1(constants.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>
|
||||
225
.planning/phases/01-p0/01-02-PLAN.md
Normal file
225
.planning/phases/01-p0/01-02-PLAN.md
Normal 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.Client,triggerFirstRealnameActivation 正确入队"
|
||||
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 注入 asynqClient(CRITICAL-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 ./...`,确认编译通过,提交独立 commit(per 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>
|
||||
314
.planning/phases/01-p0/01-03-PLAN.md
Normal file
314
.planning/phases/01-p0/01-03-PLAN.md
Normal 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 注册了 AutoPurchaseHandler(registerAutoPurchaseHandler 被调用)"
|
||||
- "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: 注入 queueClient,HandlePaymentCallback 触发自动购包入队
|
||||
- auto_purchase.go: 注入 asynqClient,ProcessTask 事务成功后触发佣金入队
|
||||
- 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:91(JSONB)
|
||||
-->
|
||||
|
||||
<!-- 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 ./...`,提交 commit(per 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 ./...`,提交 commit(per 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>
|
||||
164
.planning/phases/01-p0/01-04-PLAN.md
Normal file
164
.planning/phases/01-p0/01-04-PLAN.md
Normal 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 行是场景 5,sellerCostPrice = 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 ./...`,确认编译通过,提交独立 commit(per 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>
|
||||
233
.planning/phases/01-p0/01-05-PLAN.md
Normal file
233
.planning/phases/01-p0/01-05-PLAN.md
Normal 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 ./...`,编译通过后提交独立 commit(per 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 ./...`,编译通过后提交独立 commit(per 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>
|
||||
Reference in New Issue
Block a user