har-openapi.md 24 KB

10. 门店与订单接口

本章是另一条通道,路径前缀 /payment/openapi/har,与 5 章的收款接口不是同一个接口族。

相同的部分:请求头与签名算法(3.2 / 3.3 逐字适用)、错误处理总则(3.6)。

不同的部分 —— 动手前先读完这张表:

项 5 章收款接口 本章
路径前缀 /payment/openapi/pay /payment/openapi/har
响应信封 三个下单接口 A 型,其余 B 型 4 个接口全部 B 型
金额 整数,单位分 小数,单位元
订单状态 文本枚举(SUCCESS / PAYING…),见 6 数字字符串("0" / "1"…),见 10.6
下单幂等 同 out_trade_no 重发返回原单 不幂等,每次调用都新建一笔,见 10.2
退款 有(5.6 / 5.7),可指定金额 只有全额退款,没有金额可填,见 10.4

⚠️ 「不幂等」与「金额单位是元」这两条最容易出事。 前者会让你在超时重发时收到两笔扣款,后者会把 1 元写成 1 分。请读完 10.2 的注意框再动手。

关于寻址:本章四个接口都按你这条 API Key 所属的租户账号寻址,取该租户名下由平台运营报备好的商户配置。你不需要在请求里指定归属。

关于 merchant_code(新增,2026-09-24):平台允许一个租户报备多个商户号(例如你名下有多家门店主体,各自独立结算)。这时「这笔钱收进哪一个商户」必须由你在请求里说清楚,用 merchant_code 指定。

  • 不传 ⇒ 平台取你租户名下的商户配置。
  • 传了 ⇒ 一律以它为准,且必须是你租户名下已报备的编号,否则返回 404。

⚠️ 你租户下配了多个商户号、而你又不传 merchant_code 时,接口会直接拒绝(code=400,msg 为 当前租户配置了多个商户编号,请在请求中指定 merchant_code),不会替你挑一个。这是有意的** —— 挑错了钱会进另一个商户的账户,而且全程没有任何报错,要等到对账才发现。只配了一个商户号的调用方不受影响,什么都不用改。

怎么知道自己配了几个:登录平台,在「商户配置」页可以看到你名下的全部商户号。四个接口都填同一个 merchant_code 即可。

门店与下单的关系:10.2 里的 shop_code / third_shop_code 都是选填。门店不是下单的前置条件 —— 只有需要把交易归属到某个具体门店时才填。

但门店的「地区」是有用的:平台的收款链接(第 6 章的顾客付款页)下单时会按顾客 IP 的归属地自动挑一家同地区的门店,见 10.1 的地区说明。你的接口自己下单时若指定了门店,那个地区的风控判断同样适用;或者不指定门店、改传顾客归属(顾客 IP 或省级编码,2026-10-09 起),由平台在该地区替你挑一家 —— 见 10.2。


10.1 创建门店

为你的一个线下门店建号。平台侧没有门店查询接口,本接口的返回就是你日后核对门店状态的依据,请把 shop_id 存下来。

POST /payment/openapi/har/shop/create

请求

字段 类型 必填 说明
merchant_code string 否 你租户下配了多个商户号时必填:门店建在哪一个商户下,见本章开头。只配了一个的不用传
third_shop_id string 是 你方的门店编码,≤32 字符。重复提交会被拒绝,见下
shop_name string 是 门店名称,≤128 字符
province_code string 是(2026-10-08 起) 门店所在省,GB/T 2260 两位码,如 32(江苏省)、11(北京市)
city_code string 是(2026-10-08 起) 门店所在市,GB/T 2260 四位码,如 3201(南京市)、1101(北京市)。必须是 province_code 下的市,编码填错一律 code=400 拒绝(msg 形如 城市编码不属于该省: 1101(省 32);省码写错则为 省份编码不存在: 99)—— 是调用方的输入错误,改对再调即可

⚠️ province_code / city_code 自 2026-10-08 起为必填。 在此之前提交成功的门店不传这两个字段也能建号,现在不传会返回 code=400(msg 为 省份不能为空 / 城市不能为空)。取值用国标 GB/T 2260 的行政区划代码:省级两位、地级四位,直辖市取 1101(北京市)/ 1201(天津市)/ 3101(上海市)/ 5001(重庆市)、5002(重庆市县)。

⚠️ 地区填错不会有任何报错,但这家门店会永远收不到款。 支付宝对异地收款有风控,所以平台的收款链接(第 6 章的顾客付款页)在下单时会按顾客 IP 的归属地自动挑一家同地区的门店去发起收款。地区为空、或与顾客所在地不一致的门店,在挑店时直接落选。上游没有门店更新接口,建号时填错就补不回来,只能换一个新的 third_shop_id 重新建号。

2026-10-08 之前建的门店:那批门店没有地区,无法回填,也不会被收款链接挑中。让顾客扫码付款的门店必须重新建号。

响应(B 型,data 内)

字段 说明
third_shop_id 原样返回
shop_name 原样返回
province_code / city_code 原样返回(建号时提交的那两个码)
shop_id 平台门店编号,建号成功才有值;下单时作为 shop_code 传入
create_status 建号结果,见下
merchant_code 平台侧为你配置的商户编号(你无需关心,原样带回)
fail_reason create_status 非 SUCCESS 时的原因
created_time / updated_time 记录时间

create_status 取值:

值 含义 你该做什么
SUCCESS 门店已建好,shop_id 可用 保存 shop_id
FAILED 明确被拒,不会建号 看 fail_reason,改好再调一次
PENDING 结果未知 —— 调用超时或网络失败,无法确认门店是否已经建出来 不要直接重发(可能建出两个门店),联系平台方人工核实

⚠️ third_shop_id 重复会被直接拒绝,而不是「返回原有门店」。 重复提交同一个 third_shop_id,返回 code=500,msg 形如 门店编码已存在: <你的编码>。所以本接口的正确幂等做法是先查你本地的记录,再决定要不要调,而不是无脑重发。

⚠️ PENDING 不是「处理中」。 门店创建不自动重试 —— 重试可能在上游建出两个门店。收到 PENDING 时唯一正确的动作是找平台方核实,不要自行重发。


10.2 下单收款

同一个接口承担两种场景,由 auth_code 是否有值决定:

auth_code 场景 行为
有值 被扫 —— 用户出示付款码,你扫码后把码传进来 立即发起扣款
不传 主扫 —— 平台返回一条收款链接,你渲染成二维码给用户扫 只下单,等用户付款
POST /payment/openapi/har/order/create

请求

字段 类型 必填 说明
merchant_code string 否 你租户下配了多个商户号时必填:这笔钱收进哪一个商户,见本章开头
price number 是 金额,单位元,必须 > 0。与 5 章的「整数分」不同,见下
title string 是 商品名称,≤64 字符
trade_type string 是 alipay_un_stage 支付宝支付 / alipay_stage 花呗分期
biz string 否 业务类型。非分期传 har_ali_un_stage
out_biz_no string 否 你自己的订单号,≤64 字符,原样存下供你查询
auth_code string 否 用户的付款码。有值走被扫,不传走主扫
stage_num string 否 分期期数:3 / 6 / 12 / 24。trade_type=alipay_stage 时必填
percent string 否 分期手续费承担方:100 商户承担 / 0 用户承担。trade_type=alipay_stage 时必填
shop_code string 否 平台门店编号(10.1 返回的 shop_id)
third_shop_code string 否 你方门店编码,≤32 字符
ip string 否 不指定门店时由平台按地区挑店:顾客的公网 IP(不是服务器的),精度到市,优先于 province_code,见下
province_code string 否 顾客所在省,GB/T 2260 两位码("32" 江苏省、"11" 北京市),与 10.1 建店同一套码;ip 解析不出(或 IP 地区没有可用门店)时的回退,见下
pay_timeout int 否 支付过期时间,取值 2–60(单位由上游定义,平台原样透传)
extra_params string 否 透传字段

按地区自动挑店(2026-10-09 起,选填):不传 shop_code / third_shop_code、但传了 ip 或 province_code 时,平台替你在顾客所在地区挑一家门店去发起交易,与平台的收款链接(第 6 章顾客付款页)同一套规则、同一个地区目录。ip 与 province_code 可以只传一个,也可以都传。

  • 优先级:ip 能解析出归属地时以 ip 为准(精确到市);解析不出(内网地址、非大陆 IP、查询失败)或 IP 地区没有可用门店时,回退用 province_code 按全省挑。两个都传最稳 —— IP 不灵时省码兜底。
  • ip 填什么:顾客发起支付的公网 IP,不是你服务器的出口 IP、也不是你后台的 IP。格式照常("114.114.114.114")。
  • province_code 格式(照这个填,填错会挑不到门店):GB/T 2260 行政区划代码,与 10.1 建店完全同一套 —— 省级两位,传的是代码不是地名(写 "32",写成 "江苏省" 会被拒)。
  • 怎么挑:先在同市可用门店里挑(市由 ip 解析决定);该市没有则回退到全省挑(不报错);同地区有多家时按 out_biz_no 稳定挑一家(同一 out_biz_no 重复调用落同一家;没传 out_biz_no 时随机挑一家)。
  • 挑中哪家:响应里的 third_shop_code 就是挑中的那家(本来该你传的字段,现在由平台回显)。
  • 挑不出来会被拒收(code=400,这单不会建立、不会扣款):省码不认识 —— 省份编码不存在: 99;只传 ip 而 IP 解析不出 —— 用户 IP 无法解析出归属地: 1.2.3.4;IP 地区与省码都挑不到 —— 该地区没有可用门店: 省 32(ip 解析到了市时形如 该地区没有可用门店: 4301(省 43))。「可用门店」= 10.1 建号成功(create_status=SUCCESS)且地区与所传编码一致的门店;2026-10-08 之前建的门店没有地区,不会被挑中(见 10.1 的警告)。
  • 传了任一门店编码时,这两个字段被忽略(显式指定优先,平台不会用挑出来的店覆盖你指定的店)。

响应(B 型,data 内)

字段 说明
order_no 平台订单号,后续查询(10.3)用它
out_biz_no 原样返回
upstream_order_no 上游订单号。下单成功才有值,下单超时时为空
platform_trade_no 平台交易号,支付成功后才有值
pay_url 主扫:收款链接,渲染成二维码即可。被扫:为空
trade_status 订单状态,见 10.6
price 订单金额(元)
title / trade_type / stage_num / percent 原样返回
shop_code / third_shop_code 你传的那个原样返回;按地区挑店时 third_shop_code 是挑中的那家
trade_time 支付成功时间
rcv_amount 实收金额(元),支付成功后有值
fee_amount / channel_amount 手续费 / 渠道金额
refund_time 退款时间。退款到账(trade_status 为 "2")后才有值
notify_status 平台向你推送回调的结果,见 10.5
created_time / updated_time 记录时间

⚠️ 本接口不幂等 —— 重复调用会产生两笔订单、两次扣款。 每次调用都会生成一个新的 order_no 并向上游发起一笔新的交易,平台不做 out_biz_no 去重(也不做唯一约束)。超时后不要直接重发:先用 10.3 订单查询 按 out_biz_no 查一次,查到就用那一笔的结果。

⚠️ price 的单位是「元」,是小数,不是「分」。 1.5 表示 1 元 5 角;5 章那一套要写 total_amount=150 才是同样的金额。跨通道搬代码时这是最容易错的一处。

⚠️ 金额的写法会直接影响签名。 签名是对你实际发出的那个值做字符串化后算的(3.3),而平台把 JSON 数字读成双精度数:你写 0.010,平台看到的是 0.01 —— 签名因此对不上,只会返回「签名验证失败」,不会告诉你原因。请写成不带多余零的最简小数(0.01 / 1.5 / 100),不要写 0.010、1.50。写成字符串("price": "0.01")也可以,字符串原样参与签名,最不容易出错。

⚠️ 下单失败时本地记录仍在,不要当作「什么都没发生」。 上游调用失败会以 code=500 返回(msg 形如 下单失败: <原因>),但平台已经落了本地记录并排入了状态轮询 —— 这笔交易仍可能成功。收到失败后请用 10.3 确认,不要凭这一次返回就向用户宣称交易未发生。


10.3 订单查询

POST /payment/openapi/har/order/query

请求(order_no 与 out_biz_no 至少提供一个;都提供时以 order_no 为准)

字段 类型 必填 说明
merchant_code string 否 你租户下配了多个商户号时必填,与上面两个接口同一个值。传了就只查这个商户的订单
order_no string 二选一 平台订单号(10.2 响应里的 order_no)
out_biz_no string 二选一 你自己的订单号

响应(B 型,data 内)—— 字段与 10.2 的响应一致,另加:

字段 说明
upstream_create_time 上游建单时间

本接口查的是平台已记录的状态,不会当场向上游发起查询。 订单状态由平台的任务自动收敛(下单约 30 秒后开始查,之后按退避节奏重试),所以你刚下单就查、拿到的可能仍是 "0";过一会儿再查即可。

order_no 与 out_biz_no 都没传,返回 code=500,msg 为 order_no 与 out_biz_no 至少填一个。

⚠️ 查不到订单时返回的是 code=500,不是 404。 这与 5.4 的行为不同。msg 为 订单不存在。不属于你的订单也返回同一个 msg —— 平台有意不区分「不存在」与「不属于你」,避免被用来探测他人订单是否存在。


10.4 订单退款

对一笔已支付成功的订单发起退款。

POST /payment/openapi/har/refund

请求(order_no 与 out_biz_no 至少提供一个,规则同 10.3)

字段 类型 必填 说明
merchant_code string 否 你租户下配了多个商户号时必填,与其它三个接口同一个值
order_no string 二选一 平台订单号(10.2 响应里的 order_no)
out_biz_no string 二选一 你自己的订单号
reason string 否 退款原因。留空时平台补一个默认值(商户申请退款);填了就以你填的为准

⚠️ 本接口只有全额退款,请求里没有金额字段。 这条通道的上游不支持部分退款 —— 发起即退整笔。需要退一部分的场景请先联系平台。

ℹ️ reason 为什么是选填:通道上游把它列为必填,但没公布长度上限,所以平台不强制你填 —— 留空就替你补一个默认值,不会因此失败。想在自己的账上区分退款来源就填上。

响应(B 型,data 内):结构与 10.3 完全相同(同一份订单)。

⚠️ 返回时 trade_status 是 "4"(退款中),不是 "2"。 同步返回只代表平台已把退款指令发给上游,不代表钱已经退回用户账上。

结果怎么收敛:"4" 由平台的查询任务持续向上游确认,直到拿到终态(上游若推了通知,平台也会转推给你,见 10.5):

最终 trade_status 含义
"2" 退款成功,钱已退回
"1" 退款没有发生,订单回到已支付

⚠️ 不要把 "4" 当成已退款,也不要凭它给用户承诺到账。 用户看到的是钱有没有回来,不是接口返回了什么。同 10.5 的提醒:通知可能丢,请对停在 "4" 的订单做主动查询兜底。

⚠️ 只有 trade_status = "1" 的订单能退款。 其它状态("0" / "4" / "2" / "8")一律拒绝,code=500,msg 形如 订单当前状态不可退款: <状态值>。查不到订单、或不是你的订单,仍然是 10.3 那句 code=500 的 订单不存在。

重复调用会怎样 —— 这是最容易误解的一处:

  • 退款不会重复发生。第二次调用时订单已不在 "1",会被上面那句错误挡下。
  • 但它不是幂等接口:重复调用拿到的是错误,不是第一次的结果。所以不要写「重试到成功为止」,要按下面的分支处理。

失败分两条路,处理方式相反:

返回 订单状态 你该做什么
code=500,msg 形如 退款失败: <原因> 退回 "1" 上游明确拒绝了,什么都没发生。可用 10.3 确认状态是 "1" 后重新发起
code=500,msg 为 退款结果未知(上游未响应),订单已置为退款中,请稍后查询订单状态确认 保持 "4" 上游没应答,这笔退款可能已经发生。不要重发 —— 用 10.3 查到终态为止

⚠️ 两条路都是 code=500,只能靠 msg 区分。 拿不准时一律按「结果未知」处理(不重发、去查询):重发一笔可能已经退掉的款是真实资金损失,多查一次只是麻烦。

⚠️ 「退款失败就再退一次」的前提是订单已回到 "1",不是你上次调用的返回。 并发重复发起时只有一次会成功,另一次会被状态挡下 —— 但两次的返回里只有一次是「已受理」,别把两者搞混。


10.5 回调通知

订单状态变化时,平台会向你推送一条回调。

回调地址取自你这把 API Key 上配置的 return_url(未配置时退回该租户的默认回调地址)。没配就不会推送,此时只能靠主动查询(10.3)拿结果。

POST <你的 return_url>
Content-Type: application/x-www-form-urlencoded

请求体(表单编码,三个字段):

字段 说明
notify_id 本次通知的唯一 ID,可用于去重
timestamp 平台发出通知的时间,Unix 毫秒
content JSON 字符串,业务内容见下(注意:是字符串,需要再解析一次)

content 示例:

{
  "third_order_no": "HAR1234567890",
  "out_biz_no": "T20260924001",
  "order_no": "2026092499001",
  "platform_trade_no": "2026092499002",
  "merchant_code": "M0001",
  "third_shop_code": "S001",
  "price": 1.50,
  "title": "测试商品",
  "trade_type": "alipay_un_stage",
  "status": "1",
  "trade_time": "2026-09-24T10:30:00+08:00",
  "refund_time": null,
  "rcv_amount": 1.50,
  "fee_amount": 0.01,
  "channel_amount": 1.49
}

金额字段是 JSON 数字(不是字符串);时间字段是 ISO-8601 带时区偏移的字符串。

⚠️ 回调里的 order_no 与 10.2 响应里的 order_no 不是同一个号。 回调里的 order_no 是上游订单号(等于 10.2 响应里的 upstream_order_no); 回调里的 third_order_no 才是平台订单号(等于 10.2 响应里的 order_no)。 名字是反的,但对账必须靠它们,请按这层对应关系取用。 最省事的做法:始终用你自己生成的 out_biz_no 作主键做幂等,不要依赖这两个 order_no。

你方需要做的:

  1. 应答:处理成功后返回 HTTP 2xx。返回非 2xx 视为投递失败。
  2. 去重:同一笔订单可能收到多次通知。请用 out_biz_no + status 做幂等。
  3. 快速应答:先应答再异步处理。平台最多尝试 3 次(失败后隔 1 秒、2 秒各重试一次),收到 4xx 时不再重试。

⚠️ 平台只在收到上游通知时才会向你推送回调。 若上游那条通知丢了,平台会靠自己的任务把本地状态收敛正确,但不会补推一条给你 —— 此时 10.3 能查到正确结果,你的回调却永远不会响。请务必给长期停在 "0" 的订单做主动查询兜底。

⚠️ 退款的最终结果("4" → "2" / "1")走的是同一条回调,content.status 里就是新状态,refund_time 也在里面。所以上面的兜底对退款同样适用,而且更要紧:退款停在 "4" 时你等的是钱有没有回来,回调不响就只能自己查。给长期停在 "4" 的订单也做主动查询。

⚠️ 回调不带签名。 与 7.2 的情况相同 —— 不能仅凭收到回调就认定收款成功。请以 10.3 订单查询 的结果为准,把回调当作「该查了」的触发信号。


10.6 状态说明

trade_status 是数字字符串(JSON 里带引号,不是数字):

值 含义 是否终态
"0" 等待支付 否
"1" 支付成功,钱已到账 是
"2" 已退款 是
"4" 退款中 否
"8" 交易关闭 是

判断「是否收到过钱」用 "1" / "2";判断「是否还要等」用 "0" / "4"。 判断「退款是否已经到账」只看 "2" —— 停在 "4" 表示平台还在向上游确认,不代表已退(见 10.4)。

门店 create_status:SUCCESS / FAILED / PENDING,见 10.1。

订单 notify_status(平台向你推送回调的结果):

值 含义
PENDING 尚未推送成功(含「你没有配置回调地址」这种情况)
SUCCESS 你方已成功接收
FAILED 推送失败(对方返回非 2xx,或 3 次尝试均失败)

11. 门店与订单通道的联调检查清单

  • 认证与签名复用 3.3 的实现,先跑通 3.4 的样例
  • 建号带了 province_code / city_code(GB/T 2260),且市码属于该省码
  • 不指定门店下单时传了 ip(顾客的公网 IP)或 province_code,且拿响应里的 third_shop_code 核对了挑中的门店
  • price 用元、写成最简小数(不要 1.50、0.010)
  • 下单不重发 —— 超时先查 10.3,确认没有再决定
  • auth_code 有值走被扫、不传走主扫,两种都测过
  • 主扫拿到了 pay_url 并能渲染成二维码
  • 按 trade_status 的数字字符串分支处理("1" 才是钱到账)
  • 回调接口能处理表单编码,并把 content 再解析一次 JSON
  • 回调按 out_biz_no 做幂等
  • 对长期停在 "0" 的订单有主动查询兜底
  • 退款只对 trade_status = "1" 的订单发起,且发起前用 10.3 确认过没有退过
  • 退款返回 退款结果未知… 时不重发,改用 10.3 查到终态
  • 退款结果按 "2"(已到账)判定,不把 "4" 当已退款
  • 门店 create_status 收到 PENDING 时走人工核实,不自行重发