docs/支付宝支付接口接入规范260915.docx(v1.10)kf-request-demo/(com.har 包,5 个文件)汇安融(下称 HAR)是支付宝一级服务商,我们是其二级服务商。HAR 出支付能力,我们负责获客,用户是企业商户。本模块把 HAR 的支付宝收单能力(含花呗分期)接进平台,并把能力以开放 API 的形式转给企业商户。
企业商户 ──①──> 我们平台 ──②──> HAR ──> 支付宝
↑ │
└──────④ 回调 / ⑤ 查询───────────┘(③ 通知回流)
① 我们的开放 API tenant apiKey + HMAC-SHA256 签名 [平台已有底座]
② HAR API form-urlencoded + SHA256WithRSA [本模块新建]
③ HAR 通知 推给我们,成功需返回 "success" [本模块新建]
④ 回调企业商户 POST 到 tenant returnUrl [平台已有 sendCallback]
⑤ 企业查单 开放 API / 后台页面 [本模块新建]
本模块不碰钱。 款项直接进入企业商户在 HAR 侧的商户号,平台不记账、不落余额、不做提现。本模块只记录订单生命周期供查询。
不做对账任务。 订单收敛到终态即结束,不按日拉取上游数据做比对。
| 项 | 说明 |
|---|---|
| 数据模型 | pay_har_merchant / pay_har_shop / pay_har_order |
| 签名与验签 | HarSigner,可单元测试 |
| 上游客户端 | HarClient:报文组装、签名、发送、响应解析、验签 |
| 门店 | 后台 + 开放 API 创建,本地落库 |
| 订单 | 下单(主扫 precreate / 被扫 pay)、查询 |
| 通知接收 | 上游通知入口 + 验签 + 幂等 + 转发商户 |
| 轮询补偿 | 非终态订单主动查询 |
| 后台页面 | 商户配置、门店管理、订单管理 |
| 菜单权限 | sys_menu + sys_role_menu 幂等种子 |
| 项 | 原因 | 后续 |
|---|---|---|
| 退款 | HAR 侧接口仍在开发中,响应 detail 结构未公开 |
联调后补,表结构已预留字段 |
| 商户入驻进件 | HAR 侧纯人工办理,我们只做手工配置 | 不做自动化 |
| 对账任务 | 业务上不需要 | 不做 |
| 资金记账 | 平台不碰钱 | 不做 |
| 通用渠道抽象层 | 当前只有一个上游,抽象形状无依据 | 出现第二个上游时再考虑 |
| appId / 密钥的多商户配置形态 | HAR 未确定是机构级还是商户级 | 联调后定,见 §8 |
以 HAR 文档和 kf-request-demo 为准。文档与 demo 冲突处以 demo 为准,理由见各行备注。
application/x-www-form-urlencoded
(文档 §1.1.1 写「整体为 JSON 格式」,与 demo 的 UrlEncodedFormEntity 冲突,以 demo 为准)| 字段 | 值 |
|---|---|
appId |
接口接收方(HAR)提供 |
method |
接口标识 |
version |
固定 2.0 |
sign |
SHA256WithRSA(bizContent, 我方私钥) 的 URL-safe Base64 |
bizContent |
业务参数的 JSON 字符串 |
{baseUrl}/kf-alipay-api,业务路径见表 §3.2
(demo 为 http://localhost:9090/kf-alipay-api/api/alipay/trade/query;文档 §4 接口声明表的 URL 缺 kf-alipay-api 这段 context-path)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段)。读它的任何一列前先假设可能是旧版。
response_code:10000 表示成功detail 里,sign 是 detail 的签名detail 原文用于验签,失败时打全量诊断日志| 用途 | 密钥 | 格式 |
|---|---|---|
| 我方签名 | 我方私钥 | PKCS#8,纯 Base64 |
| 我方验签 HAR 响应/通知 | HAR 公钥 | X.509,纯 Base64 |
算法 SHA256WithRSA;签名结果 Base64.encodeBase64URLSafeString(URL-safe 字母表);验签用宽松解码(兼容标准与 URL-safe 两种字母表)。
successsuccess 时,每隔 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 / signRSA.sign(merchantCode=123&name=111&status=2) 用驼峰命名且含不存在的 name 字段,照文字规则实现:剔除 sign 与空值,按参数名升序,k=v 用 & 连接重试窗口只有 25 秒是本设计的关键约束:任何超过 25 秒的不可用都会永久丢失通知,因此主动查询是主路径而非兜底。
0 等待支付 / 1 支付成功 / 2 退款成功 / 4 退款中 / 8 交易关闭
直接采用上游枚举,不做本地转义。文档 §5.3 查询响应含 4,§5.6 通知枚举不含 4,按「通知可能不推 4」处理。
三张表,均为 BIGSERIAL 主键、Postgres。基类字段沿用 PaymentBaseEntity / PaymentTenantBaseEntity / PaymentEnterpriseBaseEntity 的约定。
注意:
PaymentBaseEntity的status是普通字段(无@TableLogic),业务状态统一走trade_status/create_status,与pay_f2f_trade保持一致。
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)
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,记录原始报文,不自动重试,交人工判断——因为没有查询接口,无任何手段核实远端是否已创建,自动重试可能产生重复门店。
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 三个号都在这张表上,可直接查询。
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 单独单元测试。
企业 → 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
HAR → POST /payment/notify/har (form,免鉴权,见 §7)
→ HarNotifyService.handle()
├ 验签(参数名升序拼接)—— 失败即拒绝并打全量日志
├ 按 order_no + status 幂等
├ 更新订单
├ 异步回调企业 returnUrl
└ 返回 "success"
验签失败一律拒绝,不设跳过开关。 联调期若规则对不上,靠诊断日志定位(打印收到的原始参数、拼串结果、期望签名),不靠关闭校验绕过。
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 封顶)复用 OpenapiService 的既有模式(resolveReturnUrl + sendCallback):取 sys_tenant_api_key.return_url,POST JSON,带重试。
需要改动的既有文件,共两处:
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)),新增接口必须同步登记,漏登记会导致认证被整体跳过。
| # | 项 | 影响 | 当前处理 |
|---|---|---|---|
| 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 是否推送通知 |
状态收敛 | 轮询补偿覆盖 |
| 层 | 方式 | 现在可验证 |
|---|---|---|
HarSigner |
单元测试:签名/验签往返、密钥格式、URL-safe Base64、验签失败返回 false 而非抛异常 | ✅ |
HarClient 报文组装 |
单元测试:断言 form 字段与 bizContent JSON 结构、sign 覆盖内容正确 |
✅ |
HarResponseParser |
单元测试:form / JSON 两种格式、detail 原文保留 |
✅ |
| 门店 / 订单本地生命周期 | 单元测试 + 接口测试 | ✅ |
| 菜单权限种子 | 手工执行 SQL 后核对 | ✅ |
| 真实网络调用 | 联调(2026-09-24) | ❌ |
| 通知端到端 | 联调 | ❌ |
约束:上游接口仍在开发中,联调前不存在可用的测试环境。因此本期所有网络调用路径只能通过单元测试验证「组装与解析逻辑正确」,无法验证「协议理解正确」。协议理解的正确性只能等联调。
docs/ 与 kf-request-demo/ 供追溯