## Context

当前系统通过环境变量配置微信参数（`pkg/config/config.go` 中的 `WechatConfig` 结构体，包含 `OfficialAccountConfig` 和 `PaymentConfig`），在启动时创建全局单例 `PaymentService`，注入到 `order.Service` 中使用。这种模式无法动态切换支付凭证，也不支持富友支付渠道。

公众号 OAuth 配置同样硬编码在环境变量中，与支付配置相互独立。业务上，一套"微信身份"包含公众号 AppID、小程序 AppID 和支付凭证，三者必须原子性切换，否则会出现 OpenID 与 AppID 不匹配的问题。

代理充值模块目前只有 Store 层（`internal/store/postgres/agent_recharge_store.go`），缺少 Service 层和 Handler 层，无法通过 API 发起充值。

## Goals / Non-Goals

**Goals:**

- 在管理后台 CRUD 管理多套微信配置（OAuth + 支付），支持微信直连和富友两种渠道
- 全局唯一激活约束：任意时刻最多一个配置生效
- 配置切换秒级生效，不影响在途支付
- 无生效配置时系统降级为仅支持钱包/线下支付
- 接入富友支付的公众号 JSAPI + 小程序支付能力
- 支付回调按订单关联的 `payment_config_id` 验签，解决切换期间的竞态问题
- 代理充值完整实现 Service/Handler/回调
- 所有配置操作记录审计日志

**Non-Goals:**

- 不支持同时激活多个配置进行负载均衡或路由
- 不支持支付宝直连（现有支付宝代码保留不改造）
- 不加密存储敏感字段（明文存储，接口返回时脱敏）
- 不自动检测配置有效性（如证书过期、商户号封禁）
- 不实现富友退款功能（后续按需扩展）
- 不动态加载 OAuth 配置（本次只存不用，`OfficialAccountService` 暂时继续从环境变量读取）
- 不实现客户端支付发起（留桩，另一个会话讨论）

## Decisions

### Decision 1: 表名和提案名称

**选择：`tb_wechat_config`（非 `tb_payment_config`）**

OAuth 配置纳入管理后，每套配置代表一套完整的微信身份（公众号 + 小程序 + 支付），切换配置等于原子性切换一切。用 `payment_config` 命名会遗漏 OAuth 语义，用 `wechat_config` 更准确地反映其职责范围。

### Decision 2: 扁平字段按前缀分组

**选择：扁平字段，按前缀分组（`oa_` / `miniapp_` / `wx_` / `fy_`）**

替代方案为 JSONB 字段存储不同渠道的参数。选择扁平字段的理由：系统使用者为非技术人员，前端表单可直接映射字段，无需 JSON 编辑器；字段级别的 NOT NULL 约束和验证更明确；不适用的字段留空即可（`provider_type=wechat` 时 `fy_*` 字段全部为空）。

### Decision 3: Payment 实例生命周期管理

**选择：按需创建 + Service 层缓存，配置变更时清除**

- 替代方案 A：启动时创建单例（当前方案）—— 无法动态切换
- 替代方案 B：每次支付请求都创建新实例 —— 性能浪费
- 选择方案：`PaymentConfigService` 维护当前生效配置的 Payment 实例内存缓存，配置切换时清除缓存，下次请求时按新配置重建实例

**「留桩」期间的过渡说明：**
本次实现后，`WechatPayJSAPI`/`WechatPayH5` 方法加 TODO 注释但保留对 `s.wechatPayment` 单例的调用。这意味着在「留桩」期间：
- `internal/bootstrap/dependencies.go` 的 `WechatPayment` 字段**暂时保留**
- `internal/bootstrap/services.go` 和 `handlers.go` 的 `deps.WechatPayment` 注入**暂时保留**
- 等 WechatPayJSAPI/WechatPayH5 完成动态加载改造后，再统一删除上述字段和注入点

**回调（callback）不属于留桩范围**：任务 1.6.1 的「留桩」仅指验签逻辑，回调的订单分发（按 payment_config_id 路由）必须完整实现，详见 Decision 5。

### Decision 4: 生效配置缓存策略

**选择：Redis 缓存 + 主动失效**

- 缓存 Key：**`wechat:config:active`**（全项目统一使用此命名，spec 文档中任何地方写 `payment:config:active` 均为笔误，以本文档为准），存储完整配置 JSON，TTL 5 分钟
- 主动失效：激活/停用/更新/删除配置时主动 DEL 缓存
- 读取流程：Redis GET → 命中返回 → MISS 则查 DB → SET 缓存
- 空标记：无配置时缓存 `"none"` TTL 1 分钟，防止缓存穿透
- Redis Key 定义在 `pkg/constants/redis.go`：`RedisWechatConfigActiveKey()`

### Decision 5: 配置切换时在途订单处理

**选择：不取消在途订单，自然完成或过期**

替代方案为切换时批量取消所有待支付的第三方订单，但用户可能已拉起支付正在输密码，取消订单会导致钱扣了但订单已取消，需要人工退款。

实现方式：
1. `tb_order` 新增 `payment_config_id` 字段（nullable，钱包/线下支付不需要）
2. `tb_asset_recharge_record` 新增 `payment_config_id` 字段
3. `tb_agent_recharge_record` 新增 `payment_config_id` 字段
4. 下单时记录当前使用的配置 ID
5. 回调处理时，按 `payment_config_id` 加载对应配置进行验签
6. 未支付的旧订单靠现有的 30 分钟超时自动取消机制清理
7. 有待支付订单引用的配置不允许删除（软删除后仍可用于回调验签）

### Decision 6: 富友支付接入方案

**选择：基于 `wxPreCreate` 接口，支持公众号 JSAPI 和小程序支付**

- 接口地址：`POST /wxPreCreate`（生产地址从配置读取）
- 公众号 JSAPI：`trade_type=JSAPI`，`sub_appid=公众号AppID`，`sub_openid=用户公众号OpenID`
- 小程序支付：`trade_type=LETPAY`，`sub_appid=小程序AppID`，`sub_openid=用户小程序OpenID`
- 签名算法：RSA + MD5（字典序排列参数 → GBK 编码 → MD5 哈希 → RSA 签名 → Base64）
- 通信协议：XML + GBK 编码 + 双重 URL 编码
- 回调处理：GBK → UTF-8 转换 → XML 解析 → RSA 验签
- 代码组织：`pkg/fuiou/` 包，从 cc-coding 项目移植核心逻辑，适配本项目的日志和错误处理规范

### Decision 7: 模块分层设计

**遵循 Handler → Service → Store → Model 分层：**

```
internal/
├── model/
│   ├── wechat_config.go              # WechatConfig 模型 + 常量
│   └── dto/
│       ├── wechat_config_dto.go      # 配置管理 DTO
│       └── agent_recharge_dto.go     # 代理充值 DTO
├── store/postgres/
│   ├── wechat_config_store.go        # CRUD + 激活/停用
│   └── agent_recharge_store.go       # 已有，需扩展
├── service/
│   ├── wechat_config/service.go      # 配置管理业务逻辑
│   └── agent_recharge/service.go     # 代理充值业务逻辑（新建）
├── handler/
│   ├── admin/wechat_config.go        # 配置管理 Handler
│   ├── admin/agent_recharge.go       # 代理充值 Handler（新建）
│   └── callback/payment.go           # 改造：支持富友回调 + 按配置验签
├── routes/
│   ├── wechat_config.go              # 配置管理路由
│   └── agent_recharge.go             # 代理充值路由（新建）
└── bootstrap/                        # 注册新模块

pkg/
└── fuiou/
    ├── client.go                     # 富友 HTTP 客户端
    └── types.go                      # 请求/响应结构体
```

### Decision 8: 接口脱敏策略

**敏感字段在 API 响应中脱敏，数据库明文存储：**

| 字段类型 | 脱敏规则 | 示例 |
|---------|---------|------|
| Secret/Key（短） | 显示前4后4，中间 `***` | `secr****7890` |
| 证书/私钥（长） | 仅显示状态 | `[已配置]` / `[未配置]` |

更新时：不传或传空字符串 = 不修改，传新值 = 替换。

### Decision 9: 前端支付方式展示

富友支付对用户透明，前端统一显示"微信支付"。后端根据生效配置的 `provider_type` 自动路由到微信直连或富友，前端不感知具体支付渠道。

### Decision 10: OAuth 配置管理策略

OAuth 字段（公众号 AppID/AppSecret、小程序 AppID/AppSecret）存入 `tb_wechat_config`。本次只存不用——`OfficialAccountService` 暂时继续从环境变量读取。H5/小程序重构时切换为从数据库动态加载。保证切换配置时 OAuth 和支付 AppID 原子性同步。

**因此，以下代码本次不删除：**
- `pkg/config/config.go` 中的 `OfficialAccountConfig` 结构体（`WechatConfig.OfficialAccount` 字段仍需使用）
- `pkg/config/defaults/config.yaml` 中的 `wechat.official_account` 配置节（`OfficialAccountService` 仍从中读取）
- `cmd/api/main.go` 中 `validateWechatConfig` 里对公众号配置的校验逻辑（仅删除支付相关校验）

**以下代码本次必须删除（Payment 相关的 YAML 方案完全废弃）：**
- `pkg/config/config.go` 的 `PaymentConfig` 结构体 + `WechatConfig.Payment` 字段
- `pkg/config/defaults/config.yaml` 的 `wechat.payment` 配置节
- `pkg/wechat/config.go` 的 `NewPaymentApp(cfg *config.Config, ...)` 函数（从 YAML/CertPath 创建 Payment 实例，被 DB 方案彻底取代）
- `cmd/api/main.go` `validateWechatConfig` 中所有 `wechatCfg.Payment.*` 相关校验

### Decision 11: 代理充值设计

- 仅充到余额钱包（`wallet_type=main`）
- 代理只能充自己店铺，平台可指定任意店铺
- 支付方式：`wechat`（在线）/ `offline`（线下，仅平台，需操作密码）
- 回调处理完整实现，客户端支付发起留桩

### Decision 12: Card → Asset 常量重命名

`pkg/constants/wallet.go` 中 `Card*` 前缀统一改为 `Asset*`，原 `Card*` 常量保留为废弃别名（向后兼容）。

影响范围：`CardWalletResourceType*`、`CardWalletStatus*`、`CardTransactionType*`、`CardRechargeOrderPrefix`、`CardRechargeMinAmount`、`CardRechargeMaxAmount`。

## tb_wechat_config 表结构

```sql
CREATE TABLE tb_wechat_config (
    -- 基础字段
    id              BIGSERIAL PRIMARY KEY,
    name            VARCHAR(100)  NOT NULL,                    -- 配置名称
    description     TEXT,                                       -- 配置描述
    provider_type   VARCHAR(20)   NOT NULL,                    -- 支付渠道: wechat / fuiou
    is_active       BOOLEAN       NOT NULL DEFAULT FALSE,      -- 是否激活

    -- OAuth 公众号
    oa_app_id              VARCHAR(100)  NOT NULL,             -- 公众号 AppID
    oa_app_secret          VARCHAR(200)  NOT NULL,             -- 公众号 AppSecret
    oa_token               VARCHAR(200),                       -- 消息验证 Token
    oa_aes_key             VARCHAR(200),                       -- 消息加密 AESKey
    oa_oauth_redirect_url  VARCHAR(500),                       -- OAuth 回调地址

    -- OAuth 小程序
    miniapp_app_id         VARCHAR(100),                       -- 小程序 AppID
    miniapp_app_secret     VARCHAR(200),                       -- 小程序 AppSecret

    -- 支付-微信直连 (provider_type=wechat 时使用)
    wx_mch_id              VARCHAR(100),                       -- 商户号
    wx_api_v3_key          VARCHAR(200),                       -- API V3 密钥
    wx_api_v2_key          VARCHAR(200),                       -- API V2 密钥
    wx_cert_content        TEXT,                                -- 证书内容 (Base64)
    wx_key_content         TEXT,                                -- 私钥内容 (Base64)
    wx_serial_no           VARCHAR(200),                       -- 证书序列号
    wx_notify_url          VARCHAR(500),                       -- 支付回调地址

    -- 支付-富友 (provider_type=fuiou 时使用)
    fy_ins_cd              VARCHAR(50),                        -- 机构号
    fy_mchnt_cd            VARCHAR(50),                        -- 商户号
    fy_term_id             VARCHAR(50),                        -- 终端号
    fy_private_key         TEXT,                                -- 商户 RSA 私钥 (Base64)
    fy_public_key          TEXT,                                -- 富友 RSA 公钥 (Base64)
    fy_api_url             VARCHAR(500),                       -- API 地址
    fy_notify_url          VARCHAR(500),                       -- 回调地址

    -- 审计字段
    creator         BIGINT        NOT NULL DEFAULT 0,          -- 创建人 ID
    updater         BIGINT        NOT NULL DEFAULT 0,          -- 更新人 ID
    created_at      TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
    deleted_at      TIMESTAMPTZ                                -- 软删除
);

CREATE INDEX idx_wechat_config_is_active ON tb_wechat_config(is_active) WHERE deleted_at IS NULL;
CREATE INDEX idx_wechat_config_provider_type ON tb_wechat_config(provider_type) WHERE deleted_at IS NULL;
```

## 流程图

### 配置切换流程

```
管理员调用 PUT /api/admin/wechat-configs/:id/activate
    |
    +--① BEGIN 事务
    |   +-- UPDATE tb_wechat_config SET is_active=false WHERE is_active=true
    |   +-- UPDATE tb_wechat_config SET is_active=true WHERE id=:id
    +--② COMMIT
    +--③ DEL Redis "wechat:config:active"
    +--④ 清除内存中的 Payment 实例缓存
    +--⑤ 记录审计日志
         |
         +-- 新订单 --> 使用新配置
         +-- 旧订单(待支付) --> 回调时按 payment_config_id 加载旧配置验签
                                +-- 30分钟超时自动取消
```

### 改造后的第三方支付流程（两步走）

```
步骤1: H5 创建订单
POST /api/h5/orders
    +-- payment_method = "wechat"
    +-- 系统查询 active 配置
    +-- IF 有配置 --> 记录 payment_config_id 到订单
    +-- 返回 order (payment_status=1 待支付)

步骤2: 发起微信支付（留桩）
POST /api/h5/orders/:id/wechat-pay/jsapi
    +-- 加载 order.payment_config_id 对应配置
    +-- 按 provider_type 分发:
    |   +-- wechat --> PaymentService.CreateJSAPIOrder()
    |   +-- fuiou  --> FuiouClient.WxPreCreate()
    +-- 返回支付参数给前端（本次留桩）

步骤3: 支付回调
POST /api/callback/wechat-pay 或 /api/callback/fuiou-pay
    +-- 解析订单号
    +-- 按订单号前缀分发:
    |   +-- "ORD"  --> 套餐订单 --> 查 tb_order
    |   +-- "CRCH" --> 资产充值 --> 查 tb_asset_recharge_record
    |   +-- "ARCH" --> 代理充值 --> 查 tb_agent_recharge_record
    +-- 按 payment_config_id 加载配置
    +-- 用对应凭证验签
    +-- 调用对应 Service.HandlePaymentCallback()
```

### 代理预充值流程

```
代理/平台 --> POST /api/admin/agent-recharges
    |
    +-- 验证权限: 代理只能充自己店铺，平台可指定店铺
    +-- 验证金额范围 (100元~100万元)
    +-- 查找目标店铺的 main 钱包
    |
    +-- IF payment_method = "wechat"
    |   +-- 查询 active 配置 --> 无配置则拒绝
    |   +-- 记录 payment_config_id
    |   +-- 创建充值订单 (status=1 待支付)
    |   +-- 返回订单信息（客户端支付发起留桩）
    |
    +-- IF payment_method = "offline"
        +-- 验证是否平台账号 --> 非平台拒绝
        +-- 返回订单信息（status=1 待支付，等待线下确认）

平台确认线下充值 --> POST /api/admin/agent-recharges/:id/offline-pay
    +-- 验证操作密码
    +-- 事务内:
    |   +-- 更新充值订单状态 (status=2 已支付)
    |   +-- 增加余额钱包余额（乐观锁）
    |   +-- 创建钱包交易记录
    +-- 记录审计日志
```

### 回调统一分发流程

```
回调到达
    |
    +-- 微信回调 POST /api/callback/wechat-pay
    |   +-- PowerWeChat SDK 解析 + 取 out_trade_no
    |
    +-- 富友回调 POST /api/callback/fuiou-pay
    |   +-- GBK->UTF-8 --> XML解析 --> 取 mchnt_order_no
    |
    +-- 按订单号前缀分发
        |
        +-- "ORD"  --> 套餐订单
        |   +-- 查询 tb_order --> 取 payment_config_id
        |   +-- 加载配置验签
        |   +-- orderService.HandlePaymentCallback()
        |
        +-- "CRCH" --> 资产充值（修复：当前代码用废弃的 "RCH"）
        |   +-- 查询 tb_asset_recharge_record --> 取 payment_config_id
        |   +-- 加载配置验签
        |   +-- rechargeService.HandlePaymentCallback()
        |
        +-- "ARCH" --> 代理充值（全新）
            +-- 查询 tb_agent_recharge_record --> 取 payment_config_id
            +-- 加载配置验签
            +-- agentRechargeService.HandlePaymentCallback()
```

## Risks / Trade-offs

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 配置切换后旧回调验签失败 | 用户付了钱但订单未处理 | 订单记录 `payment_config_id`，回调按该字段加载配置验签 |
| Redis 缓存与 DB 不一致 | 短暂使用旧配置 | TTL 5 分钟兜底 + 切换时主动失效 |
| 富友 SDK 是自研非开源 | 维护成本 | 从已验证的 cc-coding 项目移植，核心逻辑已线上运行 |
| 证书 Base64 存 DB 体量大 | 单行数据量增大 | 证书文件通常 2-4KB，Base64 后约 3-6KB，可接受 |
| 多实例部署缓存一致性 | 某实例使用旧缓存 | Redis 是共享的，DEL 操作对所有实例生效；内存缓存通过 Redis MISS 时重建 |
| Card→Asset 常量重命名 | 引用处需要更新 | 旧常量保留为废弃别名，渐进迁移 |
| OAuth 只存不用 | 数据在但不生效 | 明确标注为预留，H5 重构时切换 |

## Migration Plan

1. **数据库迁移**：新建 `tb_wechat_config` 表 + `tb_order`、`tb_asset_recharge_record`、`tb_agent_recharge_record` 各新增 `payment_config_id` 列（nullable，不影响存量数据）
2. **常量重命名**：`Card*` → `Asset*`，旧名保留为废弃别名
3. **删除 YAML 支付配置基础设施**（以下操作必须作为独立任务明确执行，不可遗漏）：
   - `pkg/config/config.go`：删除 `PaymentConfig` 结构体、删除 `WechatConfig.Payment` 字段
   - `pkg/config/defaults/config.yaml`：删除 `wechat.payment:` 整个配置节（保留 `wechat.official_account:` 节，OAuth 继续使用）
   - `pkg/wechat/config.go`：删除 `NewPaymentApp(cfg *config.Config, ...)` 函数（从文件路径创建 Payment 的方式已被 DB Base64 方案取代）
   - `cmd/api/main.go`：删除 `validateWechatConfig` 中所有 `wechatCfg.Payment.*` 相关的校验代码（保留公众号校验部分）
4. **代码部署**：新代码兼容无配置场景（无生效配置 = 降级为纯钱包支付）
5. **配置迁移**：将当前环境变量中的微信参数手动录入为第一个配置并激活
6. **回滚策略**：回滚代码后，支付流程回退到读取环境变量的单例模式（若 `deps.WechatPayment` 仍存在），新表数据不影响旧逻辑

## Closed Questions

- ~~富友支付是否需要退款？~~ → 暂不包含（Non-Goals 已声明）
- ~~是否需要配置变更审计日志？~~ → 必须纳入，复用 `AuditServiceInterface`
- ~~充值模块是否需要改造？~~ → 全部纳入，资产充值 + 代理充值都加 `payment_config_id`
- ~~支付宝如何处理？~~ → 保留不改造
- ~~OpenID 与 AppID 一致性？~~ → OAuth 字段纳入同一配置，切换配置时原子性同步
