--- 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 一个) 完成后创建 `.planning/phases/01-p0/01-03-SUMMARY.md`,记录: - 修改文件及改动说明 - 关键代码路径(充值回调 → 入队 → 消费 → 佣金) - 编译验证结果 - CRITICAL-04 和 CRITICAL-05 完成状态