# 乐刷支付接入文档

本文根据语雀《联合收单接口文档 / 商户交易》整理，作为本项目接入乐刷扫码/简易支付的内部记录。原始文档地址：
https://www.yuque.com/hayley-boppa/ws2xbg/lk89dvro0oc69f1m

## 接口总览

乐刷交易接口统一使用 `POST`，大部分能力共用网关路径：

- 测试网关：`https://t-paygate.lepass.cn/cgi-bin/lepos_pay_gateway.cgi`
- 生产网关：`https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi`
- Content-Type：`application/x-www-form-urlencoded`
- 金额单位：分，RMB，最小 1 分，不允许小数。
- 通信返回中的 `resp_code=0` 只表示通信成功，业务是否成功要看 `result_code` 和 `status`。

接口目录：

| 场景 | service / 地址 | 说明 | 本期接入 |
| --- | --- | --- | --- |
| 统一下单 | `service=get_tdcode` | 主扫，C 扫 B，返回二维码链接或简易支付跳转链接 | 是 |
| 条码支付 | `service=upload_authcode` | 被扫，B 扫 C，用户出示付款码 | 否 |
| 支付结果通知 | 商户通知 URL | 乐刷推送支付成功等结果 | 是 |
| 扫码交易结果查询 | `service=query_status` | 查询交易结果 | 是 |
| 扫码退款 | `service=unified_refund` | 发起退款，退款有效期和次数受限制 | 暂缓 |
| 扫码退款查询 | `service=unified_query_refund` | 查询退款状态 | 暂缓 |
| 扫码退款通知 | 商户通知 URL | 乐刷推送退款状态 | 暂缓 |
| 订单关闭 | `service=close_order` | 关闭未支付订单 | 暂缓 |
| 授权码查询 openid | `service=query_openid` | 条码/JSAPI 配套 | 暂缓 |
| 授权码查询银联 user_id | `service=query_userid` | 银联 JS 支付配套 | 暂缓 |

## 签名规则

请求、应答和通知都需要验签。当前代码只实现 MD5，SM3 先记录规则但不接入。

MD5 签名步骤：

1. 取所有参与签名的参数集合 `M`。
2. 默认过滤空值，按参数名 ASCII 升序排序。
3. 按 `key=value` 拼接为 `key1=value1&key2=value2`，字段名和值使用原始值，不做 URL Encode。
4. 末尾追加 `&key=商户请求密钥`。
5. 对最终字符串做 MD5，结果转大写。

注意：

- 请求签名：一般不包含 `sign` 本身。
- 应答验签：按乐刷返回参数验签，实际返回字段可能因升级增加，验签时要允许新增字段。
- 支付/退款通知验签：`error_code`、`leshua` 和 `sign` 不参与签名，其他返回字段按原样参与；空值参与签名；密钥只使用乐刷提供的通知验签密钥 `LESHUA_NOTIFY_KEY`。实测通知携带 `sign_type=MD5`，按普通返回字段参与签名。
- 乐刷 XML 通知里的空标签也属于空值参数，必须保留并参与签名，例如 `<goods_tag></goods_tag>` 应进入待签名串为 `goods_tag=`。
- `sign_type=SM3` 时签名结果为 64 位；不上传 `sign_type` 默认 MD5。

## 统一下单

用于主扫支付，消费者主动扫码或打开简易支付收银台。本项目先接扫码/简易支付。

请求固定字段：

| 字段 | 必填 | 说明 | 本项目取值 |
| --- | --- | --- | --- |
| `service` | 是 | 接口名 | `get_tdcode` |
| `merchant_id` | 是 | 乐刷商户号 | `LESHUA_MERCHANT_ID` |
| `third_order_id` | 是 | 商户内部订单号，同商户下唯一 | 当前使用订单号 `order_no` |
| `amount` | 是 | 订单金额，单位分 | 租金 + 押金 |
| `pay_way` | 是 | 支付类型 | 默认 `ZFBZF`，可配置 |
| `jspay_flag` | 是 | 支付形态 | 默认 `2` 简易支付 |
| `nonce_str` | 是 | 32 位随机字符串 | 后端生成 |
| `sign` | 是 | 签名 | MD5 |

常用可选字段：

| 字段 | 说明 |
| --- | --- |
| `sign_type` | `MD5` 或 `SM3`，不传默认 MD5。当前只支持 MD5。 |
| `notify_url` | 支付结果通知地址，必须是乐刷可访问的绝对路径。 |
| `jump_url` | 简易支付前台跳转地址。文档写 `jspay_flag=2` 时必传，但也提示受微信支付宝限制可不传；生产建议配置。 |
| `client_ip` | 发起交易的客户端 IP。 |
| `body` | 商品描述，不能包含换行等特殊字符。 |
| `attach` | 附加数据，成功后原样返回。 |
| `order_expiration` | 订单有效期，单位秒，最大 600 秒；支付宝建议 60 的整数倍。 |

`jspay_flag`：

| 值 | 说明 |
| --- | --- |
| `0` | 支付宝 Native、银联 Native 扫码支付 |
| `1` | 微信/支付宝/银联 JSAPI |
| `2` | 微信、支付宝简易支付，跳转乐刷收银台 |
| `3` | 微信/支付宝小程序 |

`pay_way`：

| 值 | 说明 |
| --- | --- |
| `WXZF` | 微信 |
| `ZFBZF` | 支付宝 |
| `UPSMZF` / `UPSMPAY` | 银联二维码，不同章节/回调字段可能返回不同值 |
| `DCPAY` | 数字货币 |

下单返回重点字段：

| 字段 | 说明 |
| --- | --- |
| `resp_code` | 通信状态，`0` 成功 |
| `result_code` | 业务结果，`0` 成功 |
| `error_code` / `error_msg` | 错误码和错误描述 |
| `merchant_id` | 乐刷商户号 |
| `third_order_id` | 商户订单号 |
| `leshua_order_id` | 乐刷订单号 |
| `td_code` | 二维码链接，`jspay_flag=0/1/2` 可能返回 |
| `jspay_url` | 简易支付跳转地址 |
| `jspay_info` | JSAPI/小程序/银联 JS 支付信息 |
| `pay_way` | 支付类型 |
| `sign` | 返回签名 |

## 支付结果通知

通知采用 HTTP `POST`，建议使用 `application/x-www-form-urlencoded` 接收。支付通知可能延迟或失败，文档建议主接交易查询接口。本项目同时接通知和查单。

通知特点：

- 支付成功后乐刷一般约 30 秒推送通知。
- 通知频率示例：`0s / 15s / 30s / 1m / 4m / 34m / 64m / 94m / 124m / 184m`。
- 回调处理时间要求 5 秒内。
- 乐刷可能重复通知，业务必须幂等。
- 通知成功响应必须是纯文本 `000000`，没有 JSON 和引号；其他响应或超时都会重试。

支付成功通知关键字段：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `sign` | 是 | MD5 签名 |
| `sign_type` | 否 | 签名类型 |
| `merchant_id` | 是 | 乐刷商户号 |
| `leshua_order_id` | 是 | 乐刷订单号 |
| `third_order_id` | 是 | 商户订单号 |
| `amount` | 是 | 订单金额，分 |
| `status` | 是 | 订单状态 |
| `pay_way` | 是 | 支付类型 |
| `pay_time` | 是 | 支付时间，如 `2018-08-20 19:19:19` |
| `bank_type` | 否 | 银行类型 |
| `openid` / `sub_openid` | 否 | 用户标识 |
| `out_transaction_id` | 否 | 微信、支付宝等上游订单号 |
| `attach` | 否 | 下单附加数据原样返回 |
| `trade_type` | 否 | `MICROPAY`、`NATIVE`、`JSAPI`、`SmPgPay`、`JSAPIQuick` 等 |
| `channel_order_id` | 否 | 通道订单号 |
| `settlement_amount` | 否 | 实际结算金额，分 |
| `discount_amount` | 否 | 优惠金额，分 |

通知验签规则：

- `error_code`、`sign` 不参与签名。
- 空值参与签名。
- 使用乐刷提供的通知验签密钥。

## 扫码交易结果查询

查询接口用于主动确认交易状态。文档要求：除非返回明确的支付成功、支付失败、订单关闭等最终状态，其他错误或未支付状态都需要继续查询；超过一定时间未支付可调用关单。

请求字段：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `service` | 是 | 固定 `query_status` |
| `merchant_id` | 是 | 乐刷商户号 |
| `third_order_id` | 二选一 | 商户订单号 |
| `leshua_order_id` | 二选一 | 乐刷订单号，优先使用 |
| `nonce_str` | 是 | 随机字符串 |
| `sign_type` | 否 | 签名类型 |
| `sign` | 是 | 签名 |

返回重点字段：

| 字段 | 说明 |
| --- | --- |
| `resp_code` / `result_code` | 通信和业务结果 |
| `status` | 订单状态 |
| `leshua_order_id` | 乐刷订单号 |
| `third_order_id` | 商户订单号 |
| `amount` | 订单金额 |
| `pay_way` | 支付类型 |
| `pay_time` | 支付完成时间，成功时返回 |
| `refund_amount` | 已退款金额 |
| `simple_url_flag` | `1` 表示简易支付 |
| `interm_state` | 支付中间态，如 `NOTPAY`、`USERPAYING` |

## 退款与退款查询

本期暂不实现退款，但记录接口便于后续接入。

退款限制：

- 超过三个月或发起后超过 60 天的订单无法退款，具体以乐刷返回为准。
- 同一交易订单最多发起 50 次部分退款。
- 退款接口一般返回退款中，如需最终状态要调用退款查询。

退款请求字段：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `service` | 是 | 固定 `unified_refund` |
| `merchant_id` | 是 | 乐刷商户号 |
| `third_order_id` | 二选一 | 商户订单号 |
| `leshua_order_id` | 二选一 | 乐刷订单号，优先使用 |
| `merchant_refund_id` | 是 | 商户退款单号，同商户下唯一，不能含 `_` 等特殊字符 |
| `refund_amount` | 是 | 退款金额，分 |
| `refund_type` | 否 | `0` 不使用余额，`1` 可使用余额，`2` 仅使用余额 |
| `notify_url` | 否 | 退款结果通知地址 |
| `nonce_str` | 是 | 随机字符串 |
| `sign` | 是 | 签名 |

退款查询请求字段：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `service` | 是 | 固定 `unified_query_refund` |
| `merchant_id` | 是 | 乐刷商户号 |
| `third_order_id` / `leshua_order_id` | 二选一 | 支付订单标识 |
| `merchant_refund_id` / `leshua_refund_id` | 二选一 | 退款单标识 |
| `nonce_str` | 是 | 随机字符串 |
| `sign` | 是 | 签名 |

退款返回/通知重点字段：

| 字段 | 说明 |
| --- | --- |
| `status` | 退款状态，见订单状态表 |
| `refund_amount` | 退款金额 |
| `merchant_refund_id` | 商户退款单号 |
| `leshua_refund_id` | 乐刷退款单号 |
| `total_amount` | 原订单总金额 |
| `order_balance` | 退款后订单余额，退款请求返回可能有 |
| `refund_time` | 退款成功时间 |
| `settlement_refund_amount` | 实际退款金额 |
| `discount_refund_amount` | 优惠退款金额 |
| `failure_reason` | 退款失败原因 |

退款通知成功也需要返回纯文本 `000000`。

## 订单关闭

用于关闭未支付订单。本期暂不实现。

限制：

- 条码支付订单需要下单 15 秒后才允许关单。
- 统一下单订单无此 15 秒限制。
- 银联二维码不支持关单。

请求字段：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `service` | 是 | 固定 `close_order` |
| `merchant_id` | 是 | 乐刷商户号 |
| `third_order_id` | 二选一 | 商户订单号 |
| `leshua_order_id` | 二选一 | 乐刷订单号，优先使用 |
| `nonce_str` | 是 | 随机字符串 |
| `sign` | 是 | 签名 |

返回字段包含 `status`、`leshua_order_id`、`pay_way`、`amount` 等。

## 授权码相关

授权码接口主要服务条码支付、微信 JSAPI、银联 JS 支付，本期不接。

查询微信 openid：

- `service=query_openid`
- 必填：`merchant_id`、`auth_code`、`appid`、`nonce_str`、`sign`
- 返回：`openid`、`sub_openid` 等。

查询银联云闪付 user_id：

- `service=query_userid`
- 必填：`merchant_id`、`user_auth_code`、`nonce_str`、`sign`
- 可选/条件：`app_up_identifier`
- 返回：`user_id` 等。

## 状态码

扫码/异步通知常见订单状态：

| 值 | 说明 | 本项目处理 |
| --- | --- | --- |
| `0` | 支付中 | `paying` |
| `2` | 支付成功 | `paid`，推进订单待交接 |
| `6` | 订单关闭 | `closed` |
| `8` | 支付失败 | `failed` |
| `10` | 退款中 | 暂按 `paying`/后续扩展退款状态 |
| `11` | 退款成功 | 暂缓 |
| `12` | 退款失败 | 暂缓 |
| `30` | 刷卡支付成功 | 当前也按支付成功处理 |
| `31` | 刷卡撤销成功 | 暂缓 |
| `32` | 刷卡退货成功 | 暂缓 |
| `33` | 刷卡冲正成功 | 暂缓 |
| `41` | 预授权成功 | 暂缓 |
| `43` | 预授权撤销成功 | 暂缓 |
| `45` | 预授权完成请求成功 | 暂缓 |
| `47` | 预授权完成通知成功 | 暂缓 |
| `49` | 预授权完成撤销成功 | 暂缓 |

## 常见错误码

| 错误码 | 说明 |
| --- | --- |
| `-20001` | 参数错误或无效 |
| `-20002` / `-1002` | 无效乐刷商户号 |
| `-20003` | 商户订单号格式有误 |
| `-20004` / `-4006` | 乐刷订单不存在 |
| `-20005` | 服务错误 |
| `-20006` | 验签失败 |
| `-20007` | 签名 Key 未配置 |
| `-20008` | 支付类型错误 |
| `-20009` | 订单金额填写有误 |
| `-20010` / `-4027` | 系统错误/系统异常 |
| `-20011` | 非法参数 |
| `-20012` | 授权码为空 |
| `-20013` | 非法授权码 |
| `-20014` | 未知类型授权码 |
| `-20015` | 非法公众号支付标识 |
| `-20016` | 无效乐刷订单号 |
| `-20018` | 无效第三方订单号或乐刷订单号 |
| `-20019` | 订单状态不允许退款 |
| `-20020` | 订单未退款 |
| `-20023` / `-5017` | 订单可退余额不足 |
| `-20024` | 乐刷或第三方订单号必填 1 个 |
| `-20025` | 乐刷或第三方退款订单号必填 1 个 |
| `-20026` / `-1006` | 第三方订单号已存在 |
| `-20027` | 该订单已支付 |
| `-4004` | 订单正在处理中 |
| `-5103` | 商户退款 ID 已存在 |
| `-5014` | 商户无退款权限 |
| `-5015` | 暂不支持退款 |
| `-5018` | 订单退款次数超限 |
| `-2125` | 交易被风控拦截 |
| `-5035` | 下单异常，可能是通道网络、通道状态或通道商户异常，以 `error_msg` 为准 |

## 本项目接入映射

后端环境变量：

| 变量 | 说明 |
| --- | --- |
| `PAYMENT_PROVIDER` | `mock` 或 `leshua`。本地默认 `mock`，生产设为 `leshua`。 |
| `LESHUA_GATEWAY_URL` | 乐刷网关地址。 |
| `LESHUA_MERCHANT_ID` | 乐刷商户号。 |
| `LESHUA_SIGN_KEY` | 请求签名密钥。 |
| `LESHUA_NOTIFY_KEY` | 通知验签密钥。 |
| `LESHUA_NOTIFY_URL` | 支付结果通知地址，公网绝对 URL。 |
| `LESHUA_JUMP_URL` | 简易支付完成后的跳转地址。 |
| `LESHUA_PAY_WAY` | 默认支付方式，当前默认 `ZFBZF`。 |
| `LESHUA_JSPAY_FLAG` | 默认支付形态，当前默认 `2`。 |
| `LESHUA_SIGN_TYPE` | 当前仅支持 `MD5`。 |

后端 API：

| API | 说明 |
| --- | --- |
| `POST /api/wallet/recharge/pay` | 创建钱包充值支付单，返回扫码支付二维码链接；mock 模式会立即模拟充值成功。 |
| `POST /api/wallet/recharge/pay/:id/query` | 查询钱包充值支付单，并主动向乐刷查单。 |
| `POST /api/orders/:id/pay` | 订单只使用钱包余额支付，不再直接调用乐刷。 |
| `POST /api/payments/leshua/notify` | 乐刷支付通知回调，不需要登录鉴权，成功返回 `000000`。 |

数据模型：

- 新增 `payment_orders` 表保存支付单、渠道单号、支付链接、请求/响应原文、状态和支付时间。
- 乐刷支付单仅用于钱包充值，`order_id = 0`，`third_order_id` 使用 `payment_no`。
- 渠道确认钱包充值成功后，向用户 `available` 写入 `channel_recharge` 流水。
- 订单支付不再创建乐刷支付单，只扣用户钱包 `available` 并转入 `frozen`，后续结账沿用现有冻结释放/结算逻辑。

当前范围：

- 已接：钱包扫码/简易支付充值、查单、支付成功通知、mock 跑通链路；订单钱包余额支付。
- 暂缓：条码支付、JSAPI/小程序必要 openid 获取、退款、退款通知、关单、刷卡交易查询、SM3 签名。
