315 lines
12 KiB
Markdown
315 lines
12 KiB
Markdown
---
|
||
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>
|