# 万车充小程序未分账补分账 SOP（adapayMemberId=0）

## 1. 目标与背景

- 目标：将未分账订单批量向汇付发起 `PaymentConfirm` 请求，分账对象固定为 `adapayMemberId=0`（平台默认账户）。
- 数据来源：`doc/万车充小程序-未分账明细.xlsx`。
- 编写日期：2026-03-11（Asia/Shanghai）。

说明：本 SOP 面向“拿到一份未分账明细文件后，批量补分账”的场景。文档中的流程可复用于后续同类任务。

## 2. 本次文件分析结论

对 `doc/万车充小程序-未分账明细.xlsx` 的 `Sheet1` 统计结果如下：

- 总记录数：`42,225`
- `paymentId` 去重后重复数：`0`
- `app_id`：全部为 `app_d0c80cb1-ffc8-48cb-a030-fe9bec823aaa`
- 支付时间范围：`2024-09-07 19:29:00` ~ `2026-03-06 00:11:20`
- 未分账金额（列“剩余未分账金额”）
  - 合计：`171,710.73`
  - 最小：`0.01`
  - 中位数：`0.50`
  - P90：`8.45`
  - 最大：`268.39`
- 金额分布：
  - `0.01~1`: `34,367`
  - `1~10`: `4,018`
  - `10~50`: `3,061`
  - `50~100`: `507`
  - `>=100`: `272`
- 交易订单号格式：`42,223/42,225` 包含下划线（通常为 `orderCode_时间戳`）

关键结论：

1. 量大（4.2 万+），必须批处理+可重试+可追踪。
2. 文件最晚数据到 `2026-03-06`，执行时必须先查汇付最新状态，不能盲目按文件金额直发。
3. 小额单非常多（0.01~1 占比高），要考虑接口速率与批次执行时长。

## 3. 现有代码落点（可复用）

- 发起汇付分账请求：
  - `jsowell-pile/src/main/java/com/jsowell/adapay/service/AdapayService.java`
  - 方法：`createPaymentConfirmRequest(PaymentConfirmParam param)`
- 查询支付确认列表：
  - 同上 `queryPaymentConfirmList(QueryPaymentConfirmDTO dto)`
- 分账对象模型：
  - `DivMember.memberId` 设为 `0`，`feeFlag` 设为 `Y`
- 已有“未分账处理”参考（测试代码）：
  - `jsowell-admin/src/test/java/PaymentTestController.java`
  - 方法：`processUnSettledOrder()`

注意：

- `jsowell-quartz` 里的 `processUnSettledOrder()` 当前版本（V2）主要是“查汇付确认信息”，不是生产可直接执行的补分账任务。

## 4. 推荐执行方案（生产可落地）

## 4.1 方案总览

分三阶段执行：

1. 任务清单准备
2. 批量补分账执行
3. 结果复核与失败重试

建议新增一个“一次性批处理工具（仅本次任务分支）”，不要直接依赖手工接口逐条处理。

## 4.2 任务清单准备

从 Excel 抽取以下字段形成任务清单（CSV/临时表均可）：

- `payment_id` ← 交易流水号
- `source_order_no` ← 交易订单号
- `order_code` ← `source_order_no` 按 `_` 分割取前半段；若无 `_` 则回退原值
- `pay_amt` ← 交易订单金额
- `file_confirmed_amt` ← 已确认分账金额
- `file_reserved_amt` ← 支付确认撤销金额
- `file_remaining_amt` ← 剩余未分账金额
- `pay_time` ← 支付时间

过滤条件：

- `file_remaining_amt > 0`
- `payment_id` 非空

## 4.3 批量补分账执行（核心）

单条任务处理逻辑（必须幂等）：

1. 用 `payment_id` 调 `queryPaymentConfirmList` 查询汇付最新确认信息。
2. 计算“执行时最新可分账金额” `latest_remaining_amt`：
   - 基线金额使用文件中的 `file_remaining_amt`。
   - 若汇付返回了确认信息，使用 `pay_amt - confirmed_amt - reserved_amt` 再算一遍，取两者较小值（且不小于 0）。
3. 若 `latest_remaining_amt <= 0`：跳过（记为 `SKIPPED_NO_REMAINING`）。
4. 构造分账请求：
   - `paymentId = payment_id`
   - `confirmAmt = latest_remaining_amt`
   - `divMemberList = [{ memberId: "0", amount: latest_remaining_amt, feeFlag: "Y" }]`
   - `orderCode = order_code`
   - `wechatAppId = 对应 appId`
5. 调用 `createPaymentConfirmRequest(param)`。
6. 按响应记录结果：
   - 成功：记录 `payment_confirm_id`、`confirm_amt`、`fee_amt`
   - 失败：记录 `error_code`、`error_msg`

建议执行参数：

- 批次：每批 `500`（或 `200` 起步）
- QPS：`3~5`（每笔 sleep `200~300ms`）
- 失败重试：对可重试错误最多 `2` 次

建议错误处理：

- `confirm_amt_over_limit`：立即重查汇付最新金额，按新金额重试一次。
- `payment_over_time_doing` / `refund_repeate_request`：放入重试队列，延迟重跑。
- 其他错误：记录失败，人工复核。

## 4.4 结果复核与验收

必须输出三类结果文件（CSV）：

- `success.csv`：成功明细（含 paymentConfirmId）
- `skipped.csv`：跳过明细（无剩余可分账）
- `failed.csv`：失败明细（含错误码）

验收标准：

1. `success + skipped + failed = 总任务数`
2. 对 `success` 样本抽查 `queryPaymentConfirmList`，确认 `div_members` 包含 `memberId=0`
3. `failed` 有明确可重试策略或人工处理结论

## 4.5 回滚预案（必要时）

如果误分账到错误对象或金额错误：

1. 从 `success.csv` 取 `payment_confirm_id`
2. 调用 `createConfirmReverse(paymentConfirmId, wechatAppId)` 撤销支付确认
3. 撤销后重新按正确参数执行补分账

代码参考：

- `AdapayService.createConfirmReverse(...)`

## 5. 下次同类任务复用清单

执行前 Checklist：

1. 明细文件是否包含 `payment_id`、`remaining_amt`、`pay_amt`
2. 是否确认目标账户（本 SOP 为 `memberId=0`）
3. 是否先做 100 条小批次试跑
4. 是否有完整执行日志与 `success/skipped/failed` 三份结果
5. 是否准备了 `payment_confirm_id` 级别回滚能力

执行后 Checklist：

1. 失败项是否重试完毕
2. 关键样本是否验证到汇付侧最新状态
3. 本次任务结果文件是否归档到 `doc/` 或运维归档目录

## 6. 当前已落地入口

已新增任务方法（`JsowellTask`）：

- `importAdapayUnsplitRecordAndCompleteFields()`
- `importAdapayUnsplitRecordAndCompleteFields(String filePath)`

实现位置：

- `jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java`

方法职责：

1. 从 Excel 导入 `adapay_unsplit_record`（按 `insertOrUpdateSelective`，支持重复执行）
2. 自动补齐 `order_code / due_refund_amount / settle_amount / pile_type` 等缺失字段
3. 输出导入与补齐统计日志

执行示例：

- 使用默认路径：`jsowellTask.importAdapayUnsplitRecordAndCompleteFields()`
- 指定路径：`jsowellTask.importAdapayUnsplitRecordAndCompleteFields('doc/万车充小程序-未分账明细.xlsx')`

## 7. 完整执行流程（落地版）

以下流程是当前代码已支持、可直接按顺序执行的标准操作。

### 7.1 执行前检查

1. 使用 JDK8。
2. 编译通过：

```bash
mvn -pl jsowell-quartz -am -DskipTests compile
```

3. 确认 Excel 文件就绪：

- 默认路径：`doc/万车充小程序-未分账明细.xlsx`
- 或你自己的绝对路径/相对路径

### 7.2 第一步：导入并补齐缺失字段

执行任务方法（任选其一）：

1. 默认路径：

```text
jsowellTask.importAdapayUnsplitRecordAndCompleteFields()
```

2. 指定路径：

```text
jsowellTask.importAdapayUnsplitRecordAndCompleteFields('doc/万车充小程序-未分账明细.xlsx')
```

本步骤会自动完成：

1. Excel -> `adapay_unsplit_record` 导入（`insertOrUpdateSelective`，可重复执行）
2. 自动补齐 `order_code / due_refund_amount / settle_amount / pile_type`
3. 输出导入统计与补齐统计日志

### 7.3 第二步：导入后校验

建议执行以下 SQL：

```sql
-- 总量
SELECT COUNT(*) AS total_cnt
FROM adapay_unsplit_record;

-- 核心补齐字段缺失情况
SELECT COUNT(*) AS missing_cnt
FROM adapay_unsplit_record
WHERE order_code IS NULL
   OR settle_amount IS NULL
   OR due_refund_amount IS NULL
   OR pile_type IS NULL;
```

`missing_cnt` 理想结果应接近 `0`（少量异常数据可人工排查）。

### 7.4 第三步：执行未分账处理（分账到 memberId=0）

执行任务方法（任选其一）：

1. 使用默认 appId（`Constants.DEFAULT_APP_ID`）+ pageSize=500：

```text
jsowellTask.processUnsplitRecordToDefaultMember()
```

2. 指定 appId 和分页大小：

```text
jsowellTask.processUnsplitRecordToDefaultMember('你的wechatAppId', 200)
```

建议先用 `200` 小批量试跑，再提升到 `500`。

本步骤会自动完成：

1. 分页读取 `queryList()` 的待分账记录
2. 先查汇付最新确认信息，计算实时剩余可分账金额
3. 取 `min(数据库待分账金额, 汇付实时剩余金额)` 作为本次 `confirmAmt`
4. 调 `PaymentConfirm` 分账，分账对象固定 `memberId=0`
5. 回写结果：
   - 成功：更新 `confirmed_split_amount`，`split_flag=SUCCESS`
   - 失败：`split_flag=FAILED`

### 7.5 第四步：循环执行直到待分账清零

每轮执行后，建议跑以下 SQL：

```sql
-- 按当前业务口径，仍待处理数量
SELECT COUNT(*) AS remain_cnt
FROM adapay_unsplit_record
WHERE (settle_amount > confirmed_split_amount - payment_revoke_amount)
   OR (due_refund_amount > refund_amount);

-- 分账结果分布
SELECT split_flag, COUNT(*) AS cnt
FROM adapay_unsplit_record
GROUP BY split_flag;
```

当 `remain_cnt = 0` 可视为本次补分账完成。

### 7.6 失败重试与回滚

1. 失败重试：重新执行 `processUnsplitRecordToDefaultMember(...)` 即可。
2. 回滚（如金额或目标账户异常）：
   - 按 `paymentId` 查询对应 `payment_confirm_id`
   - 调 `createConfirmReverse(paymentConfirmId, wechatAppId)` 撤销
   - 重新执行正确参数的分账流程

## 8. 一键操作建议（便于下次复用）

按下面顺序执行，基本可覆盖同类任务：

1. `jsowellTask.importAdapayUnsplitRecordAndCompleteFields('文件路径')`
2. 校验 `missing_cnt`
3. `jsowellTask.processUnsplitRecordToDefaultMember('appId', 200)`
4. 查看 `remain_cnt`
5. 若 `remain_cnt > 0`，重复第 3-4 步，最后切 `pageSize=500` 提速
