For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 给汇安融通道加退款 —— 租户通过开放 API 对一笔「支付成功」的订单发起全额退款,结果由既有轮询/通知收敛。
Architecture: 新开放 API 端点 POST /payment/openapi/har/refund → HarOrderService.openRefund:
条件更新把订单从 1 支付成功 抢占成 4 退款中(先落状态、再调上游)→ HarClient.refund
(kf.alipay.trade.refund → /api/alipay/trade/refund)→ 成功保持 4(由轮询/通知收敛到 2),
上游明确拒绝则条件回退到 1,结果未知(超时)保持 4。不新建退款单表。
Tech Stack: Java 17 + Spring Boot + MyBatis-Plus + JUnit5/Mockito;Vue3(仅文档)
Spec: .claude/plan/2026-09-16-har-alipay-channel.md(本通道设计文档)、
docs/支付宝支付接口接入规范260924.docx 5.4 节(上游规范)
trade_status / create_status,不用 status。UPDATE ... WHERE id=? AND trade_status=?),同乐收赢模块。| # | 事实 | 出处 |
|---|---|---|
| F1 | 退款 method = kf.alipay.trade.refund,url = /api/alipay/trade/refund |
docx 5.4 章标题 + 接口声明表第 4 行 + 请求参数表表头行(三处一致) |
| F2 | 表 8 请求参数只有 order_no(varchar20) 与 third_order_no(varchar32),没有金额字段 ⇒ 全额退款 |
docx 表 8 |
| F3 | 表 8 的 method 备注格写 kf.jh.stage.trade.refund —— 与 precreate/query 同款复制粘贴缺陷;precreate 已实测证明该格不可信 |
docx 表 8 vs 表 2;260924 联调实证 |
| F4 | 表 9 响应的 detail 结构完全未公开(只有「detail | 返回详细内容 | json」一行) |
docx 表 9;设计文档 L52 |
| F5 | 表 12 通知携带 status 与 refund_time;通知的 status 枚举是 0/1/2/8(无 4),查询的枚举含 4 |
docx 表 12 / 5.3 响应表 |
| F6 | HarOrderStatus.PENDING = {0, 4}、FINAL = {1, 2, 8};reschedulePoll 已按「4 是非终态」排期 |
HarOrderStatus.java、HarOrderService:344 |
| F7 | 通知处理已写 trade_status 与 refund_time,回调载荷已带 status/refund_time |
HarNotifyService:109-123、HarCallbackService:193-195 |
| F8 | 轮询按 next_query_time IS NOT NULL AND trade_status IN (0,4) 捞单 |
HarOrderService:380-387 |
LeshouyingOpenController:229,管理端 LeshouyingRefundController 是只读视图)。
汇安融订单在管理端已有列表/详情/同步,退款后的状态就显示在同一列上。pay_har_refund 表。 理由三条,缺一不可:
① 上游只支持全额退款(F2),一笔订单最多一条退款记录,没有「多笔/部分」要管理;
② 退款状态本就是订单状态的一个取值(F6),再造一张表就有了两个真相源;
③ 上游 detail 结构未公开(F4),没有可存的字段,建表只能存我们猜的列。
幂等由状态机承担:第二笔在 trade_status 已是 4/2 时被拒。1 支付成功(终态、不轮询),调用方一重试就是第二笔退款指令。
先写 4 则超时后停在「退款中」,轮询会去问上游(F6/F8),自己收敛。WHERE trade_status='4',避免把中途到达的「退款成功」通知覆盖掉。@Transactional —— 同 HarOrderService.create 的理由:上游抛异常时
本地状态必须存活,回滚会把「退款中」一起抹掉,正好退回第 3 条要避免的那个窗口。| 动作 | 文件 | 职责 |
|---|---|---|
| Create | java/.../har/client/dto/HarRefundRequest.java |
上游退款业务参数(order_no / third_order_no) |
| Modify | java/.../har/client/HarClient.java |
METHOD_REFUND + refund(...) |
| Create | java/.../har/dto/HarOpenRefundDTO.java |
开放 API 退款入参 |
| Modify | java/.../har/service/HarOrderService.java |
openRefund + 抽出 requireOpenOrder(与 openQuery 共用归属比对) |
| Modify | java/.../har/controller/open/HarOpenController.java |
POST /refund |
| Modify | frontend/src/views/module_payment/apikey/docs/har-openapi.md |
10.4 退款 + 改掉「本章没有退款接口」那行 |
| Test | java/.../har/service/HarOrderServiceTest.java |
退款服务层用例 |
| Test | java/.../har/client/HarClientOutboundUrlTest.java |
出站 URL 正控 |
| Test | java/.../har/controller/open/HarOpenControllerTest.java(新建或并入既有) |
端点接线 |
Files:
java/src/main/java/com/payment/platform/module/payment/har/client/dto/HarRefundRequest.javajava/src/main/java/com/payment/platform/module/payment/har/client/HarClient.javajava/src/test/java/com/payment/platform/module/payment/har/client/HarClientOutboundUrlTest.javaInterfaces:
Produces: HarClient.refund(HarRefundRequest) → HarResponse;HarRefundRequest#setOrderNo/setThirdOrderNo
[ ] Step 1: 写失败测试 —— refund_hitsTheAlipayRefundPath:起本地 HttpServer,
调 new HarClient(props).refund(req),断言收到的 path ==
/kf-alipay-api/api/alipay/trade/refund、form 的 method == kf.alipay.trade.refund
[ ] Step 2: 跑测试看它红 —— HarClient.refund 不存在 ⇒ 编译失败即红
[ ] Step 3: 实现 —— DTO 照 HarQueryRequest 写(@JsonInclude(NON_NULL));
HarClient 加常量与方法
[ ] Step 4: 跑测试看它绿
[ ] Step 5: Commit
Files:
java/src/main/java/com/payment/platform/module/payment/har/dto/HarOpenRefundDTO.javajava/src/main/java/com/payment/platform/module/payment/har/service/HarOrderService.javajava/src/test/java/com/payment/platform/module/payment/har/service/HarOrderServiceTest.javaInterfaces:
HarClient.refundProduces: HarOrderService.openRefund(HarOpenRefundDTO) → HarOrderVO
[ ] Step 1: 写失败测试(逐条,每条先看它红)
openRefund_paidOrder_preemptsToRefundingBeforeCallingUpstream(InOrder:orderMapper.update 先于 harClient.refund)openRefund_success_keepsRefundingAndSchedulesPoll(next_query_time 非空)openRefund_preemptIsConditional_onPaidStatus(捕获 UpdateWrapper,SET 含 trade_status,WHERE 含 id 与 trade_status)openRefund_preemptMissed_throwsAndDoesNotCallUpstream(update 返回 0 ⇒ 抛且不调上游)openRefund_waitPayOrder_rejected / _refundingOrder_rejected / _refundedOrder_rejectedopenRefund_upstreamTimeout_keepsRefundingState(抛 HarApiException ⇒ 状态不回退)openRefund_upstreamNonSuccess_rollsBackToPaid(条件回退,WHERE 仍带 trade_status='4')openRefund_sendsUpstreamOrderNoAsOrderNo / _fallsBackToThirdOrderNoopenRefund_otherEnterpriseOrder_throwsOrderNotFound(越权口径同 openQuery)[ ] Step 2: 跑测试看全红
[ ] Step 3: 实现 —— 抽 requireOpenOrder(openQuery 改为复用它,行为逐字不变)
[ ] Step 4: 跑 HarOrderServiceTest 全绿,并跑既有 openQuery 用例确认抽取没改行为
[ ] Step 5: Commit
Files:
Modify: java/src/main/java/com/payment/platform/module/payment/har/controller/open/HarOpenController.java
[ ] Step 1: 写失败测试 —— 断言 /refund 路由存在、requireMerchant 在 openRefund 之前被调用
[ ] Step 2: 跑测试看它红
[ ] Step 3: 实现 —— 照 orderQuery 写,requireMerchant(dto.getEnterpriseId(), dto.getMerchantCode()) 第一行
[ ] Step 4: 跑测试看它绿
[ ] Step 5: Commit
Files:
Modify: frontend/src/views/module_payment/apikey/docs/har-openapi.md
[ ] Step 1: 改第 16 行「本章没有退款接口」→ 指向新增的 10.4
[ ] Step 2: 新增 10.4 退款(原 10.4/10.5 顺延为 10.5/10.6),含:请求字段表、 B 型响应、全额退款、不幂等性说明(同一笔订单第二笔会被拒)、 「同步返回不代表已退款到账,以 10.6 的状态 / 回调为准」
[ ] Step 3: 跑 pnpm --dir frontend ts:check 前后差分(本仓基线非零,只看差分)
[ ] Step 4: Commit
mvn test(需库隧道)—— 记录总数与失败数WHERE trade_status='1' 去掉 ⇒ 对应用例必须红;还原 ⇒ 必须绿