---
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"
---
修复充值回调后自动购包触发(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
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
@.planning/phases/01-p0/01-01-SUMMARY.md
@.planning/phases/01-p0/01-CONTEXT.md
# 修复规格书(方案 A-4,第 301-575 行)
@.sisyphus/plans/修正业务-完整方案.md
```go
// internal/service/recharge/service.go(当前,无 queueClient)
type Service struct {
db *gorm.DB
// ... 其他字段
}
func New(db *gorm.DB, /* ... */) *Service {
return &Service{db: db, /* ... */}
}
```
```go
const (
TaskTypeAutoPurchaseAfterRecharge = "task:auto_purchase_after_recharge"
TaskTypeCommission = "..." // 已存在,从 pkg/constants/constants.go 确认
QueueDefault = "..."
QueueLow = "..." // 佣金任务用低优先级队列
)
```
Task 1: recharge.Service 注入 queueClient,触发自动购包(CRITICAL-04 Part 1)
internal/service/recharge/service.go, internal/bootstrap/services.go
**步骤 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)。
go build ./... && rg "queueClient" internal/service/recharge/service.go | grep -c "queueClient" | grep -v "^0$" && echo "PASS: queueClient 已注入" || echo "FAIL"
- recharge.Service struct 含 queueClient 字段
- HandlePaymentCallback 在充值成功、LinkedPackageIDs 非空时触发入队
- bootstrap/services.go 的 recharge.New() 传入 deps.QueueClient
- go build ./... 编译通过
Task 2: AutoPurchaseHandler 注入 asynqClient + 注册 + 触发佣金(CRITICAL-04 Part 2 + CRITICAL-05)
internal/task/auto_purchase.go, pkg/queue/handler.go
**步骤 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)。
go build ./... && rg "TaskTypeAutoPurchaseAfterRecharge" pkg/queue/handler.go | grep "HandleFunc" && echo "PASS: AutoPurchaseHandler 已注册" || echo "FAIL: 未注册"
- AutoPurchaseHandler struct 含 asynqClient 字段
- ProcessTask 事务成功后入队 TaskTypeCommission(不在事务内)
- pkg/queue/handler.go 注册了 AutoPurchaseHandler
- RegisterHandlers() 调用了 registerAutoPurchaseHandler()
- go build ./... 编译通过
整体验收:
```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(佣金记录)
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 一个)