mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-04-20 11:05:18 +08:00
306 lines
9.7 KiB
Markdown
306 lines
9.7 KiB
Markdown
# 万车充小程序未分账补分账 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` 提速
|