# 汇安融支付宝支付通道接入 设计文档 - 日期: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 接口声明表的 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` 段)。读它的任何一列前先假设可能是旧版。 **业务参数(2026-09-24 第二版规范 260924)** 上游新给了一份《支付宝支付接口接入规范260924.docx》,与 260915 逐格对撞后**只有两处**文本差异:上面那条门店 url,以及下表这处枚举: | 位置 | 260915 | 260924 | |---|---|---| | 订单创建参数表 `trade_type` 行 | `alipay_stage:花呗分期 alipay_payment:支付宝支付` | `alipay_stage:花呗分期 alipay_un_stage:支付宝支付` | | 订单创建参数表 `biz` 行(新增) | —— | `biz` 业务类型 `varchar(16)`,备注「非分期传:`har_ali_un_stage`」 | 两处标注格式**不同**,所以第二处的范围判不出来:`trade_type` 整行(参数名/描述/类型/必填/备注五格)都被划了红删除线 ``+`EE0000`,而新增的 `biz` 行是红字**不划线**;全文档共 3 行带红字(这行 6 格、`biz` 行 6 格、门店 url 1 格),带删除线的只有这 1 行(6 格 / 20 个 run)。手工格式,不是修订标记(两份文档 `w:ins`/`w:del` 均为 0)。 - 读法 A:`trade_type` 整行作废,`biz` 取而代之(非分期传 `har_ali_un_stage`) - 读法 B:`trade_type` 保留、非分期值改名 `alipay_un_stage`,另加**选填** `biz` 判不出来 ⇒ **两个都发**(`trade_type=alipay_un_stage` 与 `biz=har_ali_un_stage` 并存),哪一半不被认由联调读数指出。`biz` 是**纯透传**字段,后端不推导;非分期的那一个取值由下单表单补上(`order/index.vue`)。 > ⚠️ 新文档**自己不自洽**:`alipay_payment` 在 `kf.alipay.trade.pay` 参数表、`kf.alipay.trade.query` 参数表、异步通知参数表里一字未动,仍是 3 处。跟 `title`/`subject` 那次一样,**以线上为准,不以文档为准**。 ### 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_un_stage`(支付宝支付) | | `biz` | VARCHAR(16) | 业务类型。非分期传 `har_ali_un_stage`(260924 新增,见 §3.2 末) | | `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/` 供追溯