# 店铺微信支付入驻 · 商家申请 · 平台审核 · 支付路由

> **关联：** 《微信支付入驻与对接技术方案.md》v1.1、《确认订单页（多商品）技术方案.md》v1.3  
> **范围：** 支付全流程说明（含 `biz_member.wx_openid` 绑定）；§3～§7 为 **商户入驻扩展能力** 的设计存档。  
> **本期 C 端（含 H5）范围：** **暂不涉及** 商户微信支付入驻（`biz_shop_wechat_pay`）；C 端支付 **统一平台代收**（`wechat.pay.mch-id`），不做按店铺 `pay_mode` 路由。  
> **代码落位：** `com.ruoyi.web.modules.pay`、`com.ruoyi.web.modules.order`、`com.ruoyi.web.modules.account`

---

## 0. 本期 C 端支付范围（定稿）

| 项 | 本期 | 说明 |
|----|:----:|------|
| 平台代收 | ✓ | 所有订单走 `wechat.pay.mch-id` |
| 会员 `wx_openid` 绑定 | 生产 ✓ / Mock 跳过 | 生产微信支付前置；**Mock 不要求绑定**（H5 浏览器联调） |
| JSAPI / Mock 落单 / 回调 | ✓ | 见 §8、§9；**BUY_NOW 与多商品共用** `/api/order/{id}/pay` |
| 商家入驻申请 | — | **暂不涉及** |
| 平台入驻审核 | — | **暂不涉及** |
| 按店铺 `pay_mode` 路由 | — | **暂不涉及**；`resolvePayMchId` 对 C 端等效恒为平台号 |
| 服务商进件 / 分账 | — | 非本期 |

> 下文 §3～§7 描述入驻与路由的 **已实现扩展能力**，供后续多商户结算启用时参考；**当前 C 端联调与 H5 改造不必对接** `/agri/seller/wechat-pay`、`/agri/shop/wechat-pay`。

---

## 1. 模块定位（扩展能力 · 本期 C 端不涉及）

这是农资商城 **「按店铺配置结算方式」** 的扩展模块（`biz_shop_wechat_pay`，一店一行）。启用后可承担：

1. **商家申请** — 提交主体资料、选择结算模式  
2. **平台审核** — 运营确认后启用  
3. **支付路由** — C 端下单时决定使用哪个微信商户号（`mch_id`）

---

## 2. 在整体支付链路中的位置

```text
【商家】填写入驻资料 → PUT /agri/seller/wechat-pay
         ↓
【平台】审核列表 → 通过/驳回 → enabled=1/0
         ↓
【C 端】POST /api/order/{id}/pay → resolvePayMchId(shopId)
         ↓
       微信 JSAPI 统一下单 → 回调 → completeWeChatPay
```

| 层级 | 配置来源 | 作用 |
|------|----------|------|
| **平台全局** | `application.yml` → `wechat.pay.*` | 平台商户号、证书、回调 URL |
| **店铺级** | `biz_shop_wechat_pay` | 结算模式、自有/子商户号、审核状态 |
| **运行时** | `ShopWechatPayServiceImpl.resolvePayMchId()` | 按订单 `shop_id` 决定实际 `mch_id` |

C 端支付入口在 `WeChatPayServiceImpl.createJsapiPay()`：生产模式下调用 `resolvePayMchId(order.getShopId())`，将结果传入微信 V3 统一下单。

---

## 3. 数据模型（`biz_shop_wechat_pay`）

DDL 见 [`sql/biz_shop_wechat_pay.sql`](../../../sql/biz_shop_wechat_pay.sql)。

### 3.1 主体资料（商家填写）

| 字段 | 申请时 | 说明 |
|------|:------:|------|
| `company_name` | 必填 | 企业/个体户名称 |
| `legal_person` | 必填 | 法人 |
| `license_no` | 必填 | 统一社会信用代码 |
| `contact_name` / `contact_mobile` | 必填 | 联系人 |
| `bank_name` / `bank_account_name` / `bank_account_no` | 选填 | 结算账户（v1 仅存档，不做自动分账） |

### 3.2 结算与路由（核心）

| 字段 | 说明 |
|------|------|
| `pay_mode` | **1** 平台代收 / **2** 店铺自有商户号 / **3** 服务商子商户 |
| `mch_id` | 模式 2 时使用 |
| `sub_mch_id` | 模式 3 时使用 |
| `merchant_id` | 冗余商家 ID，便于平台检索 |

### 3.3 审核状态（平台控制）

| 字段 | 说明 |
|------|------|
| `apply_status` | 0 未申请 / 1 审核中 / 2 已通过 / 3 已驳回 |
| `enabled` | **0 否 / 1 是** — 仅平台审核通过后置 1 |
| `audit_remark` | 驳回原因 |

**设计要点：** `apply_status` 管流程展示，`enabled` 管是否参与支付路由，二者需配合使用。

---

## 4. 三种结算模式（`pay_mode`）

| 值 | 名称 | 实际下单 `mch_id` | 典型场景 |
|:--:|------|-------------------|----------|
| **1** | 平台代收 | `wechat.pay.mch-id`（平台配置） | v1 默认；资金进入平台商户号 |
| **2** | 店铺自有商户号 | 入驻表 `mch_id` | 店铺已在微信开通独立商户号 |
| **3** | 服务商子商户 | 入驻表 `sub_mch_id` | 平台为服务商，店铺为子商户 |

> v1 默认 **平台代收**。即使商家未提交入驻或审核未通过，只要平台配置了 `wechat.pay.mch-id`，C 端仍可支付（走平台号代收）。

---

## 5. 商家申请流程

**接口：** `GET/PUT /agri/seller/wechat-pay`  
**Controller：** `SellerShopWechatPayController`  
**权限：** `agri:seller:wechatPay:query` / `agri:seller:wechatPay:edit`  
**上下文：** 当前登录商家账号的 `shop_id`（`SellerShopContext`）

### 5.1 查询当前状态

`GET /agri/seller/wechat-pay`

- **无记录** → 返回虚拟状态：`apply_status=0（未申请）`、`pay_mode=1（平台代收）`、`enabled=0`
- **有记录** → 返回完整资料 + 状态标签（审核中 / 已通过 / 已驳回）

### 5.2 提交 / 重新提交

`PUT /agri/seller/wechat-pay`（Body：`ShopWechatPaySaveDTO`）

提交后系统会：

1. 校验必填字段（公司名、法人、执照号、联系人等）
2. 按 `pay_mode` 校验商户号：
   - 模式 2 → 必须填 `mchId`
   - 模式 3 → 必须填 `subMchId`
3. 写入或更新记录：
   - `apply_status = 1`（审核中）
   - `enabled = 0`（**重置为未启用**）
4. 首次 `insert`，之后 `updateByShopId`（**可覆盖重提**）

```text
未申请(0) ──提交──► 审核中(1, enabled=0)
已通过(2) ──再提交──► 审核中(1, enabled=0)   ← 重新走审核
已驳回(3) ──再提交──► 审核中(1, enabled=0)
```

商家重新提交会将 `enabled` 打回 0，避免旧配置在审核期间继续参与路由。

---

## 6. 平台审核流程

**接口前缀：** `/agri/shop/wechat-pay`  
**Controller：** `PlatformShopWechatPayController`  
**权限：** `agri:shop:wechatPay:list` / `query` / `audit`

| 接口 | 作用 |
|------|------|
| `GET /list` | 分页列表；可按 `applyStatus`、`shopName`、`companyName` 筛选 |
| `GET /pendingCount` | 待审数量（`apply_status=1`） |
| `GET /{id}` | 详情（含店铺名、商户名） |
| `PUT /approve/{shopId}` | 审核通过 |
| `PUT /reject` | 审核驳回；Body: `{ shopId, auditRemark }` |

### 6.1 审核通过

**前置条件：** 当前必须为 `apply_status=1（审核中）`

通过后：

- `apply_status = 2`
- `enabled = 1` ← **开始参与支付路由**
- `audit_remark` 清空

**额外校验：** 模式 2/3 时商户号不能为空，否则拒绝通过。

### 6.2 审核驳回

**前置条件：** 同上；**必须填写驳回原因**

驳回后：

- `apply_status = 3`
- `enabled = 0`
- `audit_remark = 驳回原因`

### 6.3 状态机总览

| apply_status | 含义 | enabled | 能否参与路由（生产） |
|:--:|------|:--:|:--:|
| 0 | 未申请 | 0 | 否 → 回落平台代收 |
| 1 | 审核中 | 0 | 否 → 回落平台代收 |
| 2 | 已通过 | **1** | **是** → 按 pay_mode 路由 |
| 3 | 已驳回 | 0 | 否 → 回落平台代收 |

---

## 7. 支付路由（核心逻辑）

**入口：** `ShopWechatPayServiceImpl.resolvePayMchId(shopId)`  
**调用点：** `WeChatPayServiceImpl.createJsapiPay()`（生产模式）

```java
// 简化逻辑
if (!realPayEnabled) {
    return platformMchId;           // Mock 模式，不走入驻表
}
row = selectByShopId(shopId);
if (row == null || enabled != "1") {
    return platformMchId;           // 未入驻/未启用 → 平台代收
}
if (pay_mode == "2") return row.mchId;
if (pay_mode == "3") return row.subMchId;
return platformMchId;               // pay_mode == "1" 平台代收
```

### 7.1 决策树

```text
C 端 POST /api/order/{id}/pay
    │
    ├─ mock=true（开发默认）
    │     └─ 不调微信，直接 completeWeChatPay，不看入驻表
    │
    └─ 生产模式（enabled=true, mock=false）
          └─ resolvePayMchId(order.shopId)
                │
                ├─ 无记录 或 enabled=0
                │     └─ 用 wechat.pay.mch-id（平台代收）
                │
                ├─ enabled=1 + pay_mode=1
                │     └─ 用 wechat.pay.mch-id
                │
                ├─ enabled=1 + pay_mode=2
                │     └─ 用 row.mch_id（店铺自有号）
                │
                └─ enabled=1 + pay_mode=3
                      └─ 用 row.sub_mch_id（子商户号）
```

### 7.2 与 C 端支付各环节的关系

| 环节 | 是否依赖入驻表 |
|------|:--------------:|
| Mock 直付（开发） | 否 |
| 生产 JSAPI 下单 | **是**（决定 mch_id） |
| 微信回调验签 / 解密 | 否（用平台证书 / APIv3 Key） |
| 落单扣库存 | 否（按 `out_trade_no = order_no`） |

**说明：** 回调 URL、API 证书、平台公钥均为 **平台级配置**，不随店铺变化。变化的是统一下单请求中的 `mch_id`（服务商模式 v1 简化为直接使用子商户号）。

---

## 8. 会员绑定 `wx_openid`

### 8.1 为什么要绑定

微信小程序 **JSAPI 支付** 向微信 V3 统一下单时，请求体 **必须** 携带付款人身份：

```json
"payer": { "openid": "oXXXX..." }
```

没有 `openid`，微信不会返回 `prepay_id`，小程序也无法调起 `wx.requestPayment`。

商城会员体系（`member_id` · 手机号登录）与微信用户（`openid`）是两套身份，支付时需要建立映射：

| 身份 | 来源 | 用途 |
|------|------|------|
| `member_id` | 商城注册 / 登录 | 订单归属、接口鉴权 |
| `openid` | 微信小程序 | 微信识别「谁在付款」 |

绑定后写入 `biz_member.wx_openid`（DDL 见 [`sql/biz_member.sql`](../../../sql/biz_member.sql)），支付时从会员记录读取，无需前端每次传递。

### 8.2 绑定流程

**接口：** `POST /api/member/wx/bind`  
**Controller：** `MemberAppController`  
**Body：** `{ "code": "wx.login 返回的临时 code" }`

```text
小程序 wx.login() → 临时 code
    → POST /api/member/wx/bind { code }
    → 后端 WeChatPaySupport.resolveOpenidByCode(code)  （code2session）
    → 得到 openid → 写入 biz_member.wx_openid
```

**安全要求：** `openid` **必须** 由后端用 `code` 向微信换取，**不可** 信任前端直接传入（可被伪造）。`code` 一次性、短时有效。

表上对 `wx_openid` 建有 **唯一索引**（`uk_wx_openid`），保证一个微信用户只对应一个商城会员。

### 8.3 支付时如何使用

```text
POST /api/order/{orderId}/pay
    → memberFacade.getWxOpenid(memberId)     // 读 biz_member.wx_openid
    → WeChatPayServiceImpl.createJsapiPay(..., openid)
    → WeChatPaySupport.createJsapiPrepayId(..., openid, ...)
         payload.payer.openid = openid
```

若 `openid` 为空，抛出业务异常：**「请先绑定微信后再支付」**（`PayConstants.MSG_OPENID_REQUIRED`）。

### 8.4 与 Mock 模式的关系

| 模式 | openid 要求 |
|------|-------------|
| **Mock**（`wechat.pay.mock=true`，默认） | **不要求**；便于 H5 / 纯浏览器联调，无需 `wx.login` 或 `/api/member/wx/bind` |
| **生产**（`enabled=true` 且 `mock=false`） | **必须** 已绑定 `wx_openid`，否则拒绝支付 |

Mock 模式下 `createJsapiPay` 在校验 openid **之前** 即返回 `{ mock: true }`，由 `OrderAppServiceImpl` 同步调用 `completeWeChatPay` 完成落单。

### 8.5 设计说明

| 问题 | 说明 |
|------|------|
| 为何单独 bind 接口？ | 绑一次、多次支付复用；减少重复 code2session |
| 能否支付时再传 code？ | 可以（每次 pay 带 code 临时换 openid），本期未采用 |
| 与 unionid 的关系 | 本期仅用 **openid**（小程序 appId 维度），未存 unionid |

---

## 9. 完整业务协作链

```text
【商家】店铺设置 → 微信支付入驻
    → 填资料 + 选 pay_mode
    → PUT /agri/seller/wechat-pay
    → apply_status=1, enabled=0

【平台】支付管理 → 入驻审核
    → GET list / detail
    → PUT approve → apply_status=2, enabled=1
    或 PUT reject  → apply_status=3, enabled=0 + auditRemark

【C 端】确认订单 → submit → 待支付
    → POST /api/member/wx/bind（绑 openid）
    → POST /api/order/{id}/pay
        → resolvePayMchId(shopId)
        → 微信统一下单 → 返回 payParams
    → wx.requestPayment
    → 微信 POST /api/pay/wechat/notify
    → completeWeChatPay（扣库存、改状态、删购物车）
```

**C 端取消支付：** 用户在微信收银台取消时，须主动调用 `POST /api/order/{id}/pay/cancel`，否则订单保持待支付。详见《确认订单页（多商品）技术方案》§4.2.1。

---

## 10. 设计要点与边界

### 10.1 为什么默认平台代收

- 商家未入驻也可销售（资金走平台号）
- 降低上线门槛；店铺后续可切换自有 / 子商户号
- 平台可在微信侧完成子商户进件，审核通过后再启用

### 10.2 `enabled` 与 `apply_status` 分工

- `apply_status`：面向商家 / 平台的流程状态
- `enabled`：面向支付路由的功能开关  
  审核通过才 `enabled=1`，避免「审核中」误用新商户号

### 10.3 重新提交的影响

商家再次 `PUT` 会将状态打回「审核中」且 `enabled=0`，期间该店支付 **临时回落平台代收**，直至再次审核通过。

### 10.4 v1 未覆盖能力

| 能力 | 现状 |
|------|------|
| 自动分账 / 结算至店铺银行账户 | 未实现（银行字段仅存档） |
| 微信侧进件 API 对接 | 未实现（平台人工审核 + 手工填商户号） |
| 按店铺切换回调 URL | 未实现（统一平台回调） |
| 查单补偿 Job | 待补强 |
| 退款 / 分账 | 非本期 |

---

## 11. 代码落位速查

| 组件 | 路径 |
|------|------|
| 入驻表结构 | `sql/biz_shop_wechat_pay.sql` |
| 会员 openid | `sql/biz_member.sql` · `BizMember.wxOpenid` |
| 微信 bind | `MemberAppController` · `MemberAppServiceImpl.bindWxOpenid()` |
| openid 换取 | `WeChatPaySupport.resolveOpenidByCode()` |
| 商家 Controller | `SellerShopWechatPayController` |
| 平台 Controller | `PlatformShopWechatPayController` |
| 入驻 Service | `ShopWechatPayServiceImpl` |
| 支付下单 | `WeChatPayServiceImpl.createJsapiPay()` |
| C 端发起支付 | `OrderAppServiceImpl.createPay()` |
| 支付成功落单 | `OrderPaySuccessServiceImpl.completeWeChatPay()` |
| 常量 | `PayConstants`（`APPLY_*`、`PAY_MODE_*`、`MSG_OPENID_REQUIRED`） |
| 技术方案 | `doc/平台后台/支付管理/微信支付入驻与对接技术方案.md` |

---

## 12. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.3** | Mock 模式跳过 openid 校验，支持 H5 / 纯浏览器联调 |
| **v1.2** | 明确本期 C 端/H5 **暂不涉及** 商户入驻；支付统一平台代收（§0） |
| **v1.1** | 新增 §8 会员绑定 `wx_openid` 说明 |
| **v1.0** | 首版：入驻表、商家申请、平台审核、支付路由全流程说明 |

---

*文档版本：v1.3 · 关联《微信支付入驻与对接技术方案.md》v1.1*
