---
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>
