2026-09-16-har-alipay-channel-design.md 16 KB

汇安融支付宝支付通道接入 设计文档

  • 日期:2026-09-16
  • 状态:设计已定,等待 2026-09-24 联调
  • 上游文档:docs/支付宝支付接口接入规范260915.docx(v1.10)
  • 上游示例:kf-request-demo/(com.har 包,5 个文件)

1. 背景

汇安融(下称 HAR)是支付宝一级服务商,我们是其二级服务商。HAR 出支付能力,我们负责获客,用户是企业商户。本模块把 HAR 的支付宝收单能力(含花呗分期)接进平台,并把能力以开放 API 的形式转给企业商户。

1.1 双向角色

企业商户 ──①──> 我们平台 ──②──> HAR ──> 支付宝
   ↑                                │
   └──────④ 回调 / ⑤ 查询───────────┘(③ 通知回流)

①  我们的开放 API   tenant apiKey + HMAC-SHA256 签名   [平台已有底座]
②  HAR API         form-urlencoded + SHA256WithRSA     [本模块新建]
③  HAR 通知        推给我们,成功需返回 "success"       [本模块新建]
④  回调企业商户      POST 到 tenant returnUrl           [平台已有 sendCallback]
⑤  企业查单         开放 API / 后台页面                  [本模块新建]

1.2 资金流

本模块不碰钱。 款项直接进入企业商户在 HAR 侧的商户号,平台不记账、不落余额、不做提现。本模块只记录订单生命周期供查询。

不做对账任务。 订单收敛到终态即结束,不按日拉取上游数据做比对。

2. 范围

2.1 本期做

项 说明
数据模型 pay_har_merchant / pay_har_shop / pay_har_order
签名与验签 HarSigner,可单元测试
上游客户端 HarClient:报文组装、签名、发送、响应解析、验签
门店 后台 + 开放 API 创建,本地落库
订单 下单(主扫 precreate / 被扫 pay)、查询
通知接收 上游通知入口 + 验签 + 幂等 + 转发商户
轮询补偿 非终态订单主动查询
后台页面 商户配置、门店管理、订单管理
菜单权限 sys_menu + sys_role_menu 幂等种子

2.2 本期不做

项 原因 后续
退款 HAR 侧接口仍在开发中,响应 detail 结构未公开 联调后补,表结构已预留字段
商户入驻进件 HAR 侧纯人工办理,我们只做手工配置 不做自动化
对账任务 业务上不需要 不做
资金记账 平台不碰钱 不做
通用渠道抽象层 当前只有一个上游,抽象形状无依据 出现第二个上游时再考虑
appId / 密钥的多商户配置形态 HAR 未确定是机构级还是商户级 联调后定,见 §8

3. 上游协议要点

以 HAR 文档和 kf-request-demo 为准。文档与 demo 冲突处以 demo 为准,理由见各行备注。

3.1 请求

  • 协议:HTTP POST,application/x-www-form-urlencoded (文档 §1.1.1 写「整体为 JSON 格式」,与 demo 的 UrlEncodedFormEntity 冲突,以 demo 为准)
  • 编码:UTF-8
  • 外层字段(5 个,平铺在 form 里):
字段 值
appId 接口接收方(HAR)提供
method 接口标识
version 固定 2.0
sign SHA256WithRSA(bizContent, 我方私钥) 的 URL-safe Base64
bizContent 业务参数的 JSON 字符串
  • URL 前缀:{baseUrl}/kf-alipay-api,业务路径见表 §3.2 (demo 为 http://localhost:9090/kf-alipay-api/api/alipay/trade/query;文档 §4 接口声明表的 URL 缺 kf-alipay-api 这段 context-path)

3.2 method 取值

接口 method 路径
下单(主扫) kf.alipay.trade.precreate /api/alipay/trade/precreate
收款(被扫) kf.alipay.trade.pay /api/alipay/trade/pay
查询 kf.alipay.trade.query /api/alipay/trade/query
退款(本期不实现) kf.alipay.trade.refund /api/alipay/trade/refund
门店创建 kf.har.shop.create /api/merchant/shop/create

文档 §5.1/§5.2 的 method 备注写成 kf.jh.stage.trade.*,与 §4 接口声明表的 Application 列矛盾。demo(Main.java:13)传的是 kf.alipay.trade.query,故以 kf.alipay.trade.* 为准。

2026-09-24 订正:门店创建的 url 上游口头澄清为 /api/merchant/shop/create,规范 docx 表 1 的 URL 列(/api/shop/create)是上游写错了——上表该行已按澄清值更新。

同一张表的另四行 method 与 url 严格同构(kf.X.Y.Z ↔ /api/X/Y/Z),只有门店那行两列同时破例;按同构推 method 本该是 kf.merchant.shop.create。但上游只更正了 url、没提 method,故 kf.har.shop.create 暂留原值——这是未验证项,由联调读数判(见 HarClient#createShop 的注释与 HarClientOutboundUrlTest)。

这是这份 docx 第三次被证实与线上不一致(前两次:URL 列漏 /kf-alipay-api 前缀、§58 示例漏 alipay 段)。读它的任何一列前先假设可能是旧版。

3.3 响应

  • response_code:10000 表示成功
  • 业务数据在 detail 里,sign 是 detail 的签名
  • 响应报文格式未确认(demo 未解析响应)。实现按「先 form,再 JSON」双路解析,两条路都保留 detail 原文用于验签,失败时打全量诊断日志

3.4 密钥

用途 密钥 格式
我方签名 我方私钥 PKCS#8,纯 Base64
我方验签 HAR 响应/通知 HAR 公钥 X.509,纯 Base64

算法 SHA256WithRSA;签名结果 Base64.encodeBase64URLSafeString(URL-safe 字母表);验签用宽松解码(兼容标准与 URL-safe 两种字母表)。

3.5 通知

  • HAR 推给我们,成功必须返回字符串 success
  • 未收到 success 时,每隔 5 秒重试,共重试 5 次(总窗口约 25 秒)
  • 字段:order_no / merchant_code / third_shop_id / title / trade_type / third_order_no / status / percent / trade_time / stage_num / create_time / refund_time / extra_params / platform_trade_no / sign
  • 验签规则(文档 §5.6):参数名升序拼接后 RSA 签名。文档给的示例 RSA.sign(merchantCode=123&name=111&status=2) 用驼峰命名且含不存在的 name 字段,照文字规则实现:剔除 sign 与空值,按参数名升序,k=v 用 & 连接

重试窗口只有 25 秒是本设计的关键约束:任何超过 25 秒的不可用都会永久丢失通知,因此主动查询是主路径而非兜底。

3.6 订单状态

0 等待支付 / 1 支付成功 / 2 退款成功 / 4 退款中 / 8 交易关闭

直接采用上游枚举,不做本地转义。文档 §5.3 查询响应含 4,§5.6 通知枚举不含 4,按「通知可能不推 4」处理。

4. 数据模型

三张表,均为 BIGSERIAL 主键、Postgres。基类字段沿用 PaymentBaseEntity / PaymentTenantBaseEntity / PaymentEnterpriseBaseEntity 的约定。

注意:PaymentBaseEntity 的 status 是普通字段(无 @TableLogic),业务状态统一走 trade_status / create_status,与 pay_f2f_trade 保持一致。

4.1 pay_har_merchant — 企业 ↔ HAR 商户映射

运营在报备完成后手工录入,无自动化入驻。

列 类型 说明
merchant_code VARCHAR(20) NOT NULL HAR 分配,唯一
enterprise_id VARCHAR(64) NOT NULL 平台企业 ID
merchant_name VARCHAR(128) 商户名称
contact_name / contact_mobile VARCHAR 联系人
version INT 乐观锁

索引:UNIQUE(merchant_code)、INDEX(enterprise_id)

4.2 pay_har_shop — 门店

本地表是唯一真相源:HAR 未提供门店查询接口,创建后只能靠本地记录找回。

列 类型 说明
merchant_code VARCHAR(20) NOT NULL 所属商户
third_shop_id VARCHAR(32) NOT NULL 我方门店编码,传给 HAR,幂等键
shop_name VARCHAR(128) NOT NULL 门店名称
shop_id VARCHAR(32) HAR 返回的门店 ID,未成功时为空
create_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' PENDING / SUCCESS / FAILED
fail_reason VARCHAR(512) 失败原因
request_body / response_body TEXT 原始请求与响应报文
retry_count INT DEFAULT 0 调用次数
version INT 乐观锁

索引:UNIQUE(enterprise_id, third_shop_id)、INDEX(merchant_code)

超时处理:本地先落 PENDING 记录,再调上游,成功回填 shop_id。超时保持 PENDING,记录原始报文,不自动重试,交人工判断——因为没有查询接口,无任何手段核实远端是否已创建,自动重试可能产生重复门店。

4.3 pay_har_order — 订单

形状对齐 pay_f2f_trade。

列 类型 说明
order_no VARCHAR(64) NOT NULL 我方生成,作为 third_order_no 传上游,唯一
out_biz_no VARCHAR(64) 企业商户自己的订单号
upstream_order_no VARCHAR(32) HAR 返回的 order_no
platform_trade_no VARCHAR(64) 支付通道订单号
merchant_code VARCHAR(20) NOT NULL 商户编号
shop_code / third_shop_code VARCHAR(32) 平台门店编号 / 商户门店编号
price NUMERIC(12,2) 交易金额(元)
title VARCHAR(64) 商品名称
trade_type VARCHAR(32) NOT NULL alipay_stage(花呗分期)/ alipay_payment(支付宝支付)
stage_num / percent VARCHAR(8) 分期期数 / 手续费承担方
pay_timeout INTEGER 支付过期时间
extra_params TEXT 额外参数 JSON
pay_url TEXT 收款链接(主扫)
trade_status VARCHAR(8) NOT NULL DEFAULT '0' 上游状态枚举
trade_time / refund_time / upstream_create_time TIMESTAMP 支付 / 退款 / 上游下单时间
rcv_amount / fee_amount / channel_amount NUMERIC(12,2) 到账金额 / 分期手续费 / 通道费
last_query_time / next_query_time / query_count 轮询调度
notify_status VARCHAR(16) 回调企业商户的结果
version INT 乐观锁

索引:UNIQUE(order_no)、INDEX(enterprise_id)、INDEX(out_biz_no)、INDEX(upstream_order_no)、INDEX(trade_status, next_query_time)

无需独立的「下游映射表」:企业的 out_biz_no、我方的 order_no、上游的 order_no 三个号都在这张表上,可直接查询。

5. 模块结构

module/payment/har/
├── config/       HarProperties          baseUrl / appId / version / 密钥
├── client/       HarSigner              签名与验签(纯函数,可单测)
│                 HarClient              报文组装 + 发送 + 响应解析
│                 HarResponseParser      响应解析(form / JSON 双路)
│                 HarApiException
├── controller/   HarMerchantController  运营:商户配置
│                 HarShopController      后台:门店管理
│                 HarOrderController     后台:订单管理
│                 HarNotifyController    上游通知入口
├── open/         HarOpenController      企业调用:/payment/openapi/har/*
├── dto/          req/ resp/ query/ vo/
├── entity/       HarMerchantEntity / HarShopEntity / HarOrderEntity
├── enums/        HarOrderStatus / HarShopCreateStatus / HarTradeType
├── mapper/
├── service/      HarMerchantService / HarShopService / HarOrderService
│                 HarNotifyService / HarCallbackService
└── scheduler/    HarOrderPollScheduler

字段名不做映射层,直接沿用上游命名(third_order_no、merchant_code、pay_url、stage_num 等)。上游即契约。

client/ 的存在不是为了抽象渠道,而是把签名、报文组装、发送收在一处,使其可脱离 Spring 单独单元测试。

6. 关键链路

6.1 下单

企业 → POST /payment/openapi/har/order/create
     → TenantApiKeyAuthFilter 认证
     → HarOrderService.create()
         ├ 校验商户配置存在
         ├ 生成 order_no,落库(trade_status=0)
         ├ HarClient.precreate() 或 pay()
         │    └ sign = SHA256WithRSA(bizContent, 私钥)
         ├ 验签响应 → 成功则回填 pay_url / upstream_order_no
         └ 设置 next_query_time = now + 30s

6.2 通知接收

HAR → POST /payment/notify/har  (form,免鉴权,见 §7)
    → HarNotifyService.handle()
        ├ 验签(参数名升序拼接)—— 失败即拒绝并打全量日志
        ├ 按 order_no + status 幂等
        ├ 更新订单
        ├ 异步回调企业 returnUrl
        └ 返回 "success"

验签失败一律拒绝,不设跳过开关。 联调期若规则对不上,靠诊断日志定位(打印收到的原始参数、拼串结果、期望签名),不靠关闭校验绕过。

6.3 轮询补偿

HarOrderPollScheduler,@Scheduled(fixedDelay = 60_000),照 F2fTradePollScheduler 写。

  • 扫描条件:trade_status IN ('0','4') AND next_query_time <= now() AND query_count < 上限
  • 调 HarClient.query(),更新状态与金额字段
  • 退避:next_query_time 按 query_count 递增(30s → 1m → 2m → 5m → 15m 封顶)
  • 超过次数上限仍非终态 → 停止轮询,等人工介入

6.4 回调企业商户

复用 OpenapiService 的既有模式(resolveReturnUrl + sendCallback):取 sys_tenant_api_key.return_url,POST JSON,带重试。

7. 安全集成

需要改动的既有文件,共两处:

SecurityConfig.WHITE_LIST 追加:

  • /payment/notify/har — 上游通知,无登录态,靠验签保证真实性
  • /payment/openapi/har/** — 企业开放接口,授权层放行,由 ApiKey filter 认证

TenantApiKeyAuthFilter.OPENAPI_ACCOUNT_PATHS 追加企业开放接口的精确路径:

  • /payment/openapi/har/shop/create
  • /payment/openapi/har/order/create
  • /payment/openapi/har/order/query

该数组是精确匹配(path.equals(p)),新增接口必须同步登记,漏登记会导致认证被整体跳过。

8. 未决项(联调时确认)

# 项 影响 当前处理
1 appId 与密钥是机构级还是商户级 决定配置是一份还是多份 先用 HarProperties + application.yml,联调后如需落表是纯加法
2 响应报文格式与真实样例 验签能否通过 双路解析 + 全量诊断日志
3 detail 的签名边界(URL-decode 前 / 后) 验签能否通过 按 decode 后实现,失败时日志同时打印两种结果
4 通知验签的确切拼串规则 通知能否处理 按文档文字实现,日志留全量证据
5 退款响应 detail 结构 退款实现 本期不做
6 pay_timeout 单位(2~60) 参数语义 按文档原样透传,不转换
7 「重试 5 次」是总计 5 次还是额外 5 次 补偿任务频率 不影响设计,按最坏情况(25 秒)处理
8 门店创建是否支持幂等 重复门店风险 不自动重试,人工介入
9 status=4 是否推送通知 状态收敛 轮询补偿覆盖

9. 测试策略

层 方式 现在可验证
HarSigner 单元测试:签名/验签往返、密钥格式、URL-safe Base64、验签失败返回 false 而非抛异常 ✅
HarClient 报文组装 单元测试:断言 form 字段与 bizContent JSON 结构、sign 覆盖内容正确 ✅
HarResponseParser 单元测试:form / JSON 两种格式、detail 原文保留 ✅
门店 / 订单本地生命周期 单元测试 + 接口测试 ✅
菜单权限种子 手工执行 SQL 后核对 ✅
真实网络调用 联调(2026-09-24) ❌
通知端到端 联调 ❌

约束:上游接口仍在开发中,联调前不存在可用的测试环境。因此本期所有网络调用路径只能通过单元测试验证「组装与解析逻辑正确」,无法验证「协议理解正确」。协议理解的正确性只能等联调。

10. 交付约束

  • 本期代码不提交 git,交付到工作区由人工审阅
  • 上游文档与 demo 归入仓库 docs/ 与 kf-request-demo/ 供追溯