1. 快速开始
前提条件:商户必须先完成资质审核,开通话费权益并启用 OpenAPI 应用,获取 app_id 和 app_secret;同时在商户后台配置固定公网出口 IP 白名单及 SDK 通知密钥(apiSecret)。
接入步骤总览
- 获取商户开放应用
app_id 与 app_secret(用于 OpenAPI 签名,详见第 5 节),以及 SDK 通知密钥 apiSecret(用于异步通知验签,详见第 9 节)。
- 下游服务端调用创建订单接口,传入手机号、面额、异步通知地址等参数,获得来福
orderNo。
- 调用查询接口取得本单锁价(
totalAmount/payAmount),按 totalAmount 在自有收银台向用户收款。
- 下游服务端确认用户真实付款成功后,调用支付成功通知接口,传入
totalAmount 触发来福履约。
- 接收来福异步通知,按
X-LYP-Sign 验签,以 orderStatus=SUCCESS 为充值成功最终口径。
- 任何时候不确定订单状态,优先查单而非重复创建。
安全红线:app_secret 和 apiSecret 严禁暴露到客户端、前端或 H5;支付成功通知必须由下游服务端调用,不得从前端直接请求。
2. 标准业务时序
① POST /openapi/v1/order/create 创建 WAIT_PAY 订单,保存 orderNo
② GET /openapi/v1/order/query 查询 totalAmount 与 payAmount
③ 下游按 totalAmount 在自有收银台向用户收款
④ 下游服务端验真自己的支付结果
⑤ POST /openapi/v1/sdk/payment/notify 通知来福已收款
⑥ 来福校验订单、金额、签名和幂等,按 payAmount 扣商户账户并请求上游
⑦ 来福向下游 notifyUrl 发送异步通知(含 X-LYP-* 签名头)
⑧ 下游验签后仅以 SUCCESS 作为充值成功;PROCESSING 继续等待
callbackUrl 只是浏览器同步回跳地址,不能证明用户已付款,也不能证明上游已充值成功。下游应验签接收异步通知,并在状态不确定时主动查单。
⚠ 唯一契约边界:本文只覆盖话费 PrepaidRefill 产品的创建订单、查询锁价与状态、支付成功通知、异步通知四个接口。SDK 演示接口、其他权益产品(积分兑换、卡券直充等)的接入规范不在本文。任何本文之外的字段、路径、参数不做保证,也不作为新接入依据。
3. 公共响应结构
所有 OpenAPI 接口统一使用以下 JSON 响应结构。下游必须同时检查 HTTP 状态码(应为 200)和业务码(code 应为 200)以判定成功。
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
code | 业务状态码 | Integer | 始终出现 | 200 | 200 表示业务受理成功;非 200 表示业务拒绝,见第 11 节错误排查。 |
msg | 提示消息 | String | 始终出现 | "操作成功" | 供调试参考。生产环境下游不应依赖消息文本做逻辑判断。 |
data | 业务数据 | Object | 成功时出现 | {"orderNo":"UP..."} | 具体业务字段见各接口说明。失败时可能为 null。 |
4. 产品、面额与价格
| 字段 | 正式值/规则 |
typeCode | PrepaidRefill。不要传不存在的 PHONE_RECHARGE;其他历史编码不属于本文契约。 |
carrierCode | 可选;明确知道时可传 01 移动、02 电信、03 联通。未传时平台向智信提交空 cp,由智信识别;平台不按手机号段猜测运营商。 |
faceValue | 运行接口允许 100、200、300、500;能否销售还取决于该商户、产品、运营商、面额是否存在唯一有效 SKU。 |
totalAmount | 用户应付金额,由商户有效 SKU 的 userPayAmount 锁定。 |
payAmount | 来福扣商户账户金额,由有效 SKU 的 merchantSettleAmount 锁定。 |
价格边界:下游不能通过创建订单的 amount 修改价格。用户售价与平台扣商户金额可以不同;下游必须先查单取得本单锁价,再向用户收款。
5. 创建待支付订单
由下游服务端调用,创建一笔话费充值待支付订单。请求必须携带第 6 节 OpenAPI 签名头。
POST https://api.laifuagi.cn/prod-api/openapi/v1/order/create
Content-Type: application/json
app_id: {appId}
timestamp: {unix-seconds}
nonce: {unique-random-string}
sign: {base64url-hmac-sha256}
{"typeCode":"PrepaidRefill","merchantRequestNo":"M202607250001","bizParams":{"mobile":"13800138000","faceValue":"100","notifyUrl":"https://merchant.example.com/laifu/notify","callbackUrl":"https://merchant.example.com/order/result"}}
请求头字段
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
Content-Type | 内容类型 | String | 必填 | application/json | 固定为 application/json。 |
app_id | 商户应用 ID | String | 必填 | laifu_app_abc123 | 商户后台已启用 OpenAPI 应用的 AppID。 |
timestamp | 时间戳 | String | 必填 | 1721827200 | Unix 秒级整数,与平台时间差不超过 300 秒。 |
nonce | 随机串 | String | 必填 | n9k8m7j6 | 每次请求唯一;5 分钟内不可重复。建议 UUID 或安全随机。 |
sign | 签名 | String | 必填 | aBcDeF...Z0 | HMAC-SHA256 后做 Base64URL 编码,去掉 = 补位。签名算法见第 6 节。 |
请求体字段
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
typeCode | 产品编码 | String | 必填 | "PrepaidRefill" | 固定传 PrepaidRefill。其他编码不属于本文契约。 |
merchantRequestNo | 商户请求号 | String | 必填 | "M202607250001" | 下游唯一业务请求号,建议全局唯一并持久化。用于幂等和查单。 |
bizParams | 业务参数 | Object | 必填 | {...} | JSON 子对象,包含具体充值参数。 |
bizParams.mobile | 充值手机号 | String | 必填 | "13800138000" | 目标充值手机号。新接入只使用该字段,不重复传 phoneNumber。 |
bizParams.faceValue | 充值面额 | String | 必填 | "100" | 面额字符串,必须命中唯一有效 SKU。允许值:100、200、300、500。 |
bizParams.carrierCode | 运营商编码 | String | 否 | "01" | 明确运营商时可传 01 移动、02 电信、03 联通。不确定时省略,平台交由上游智能识别,禁止下游按手机号段自行猜测。 |
bizParams.notifyUrl | 异步通知地址 | String | 否 | "https://merchant.example.com/laifu/notify" | 接收来福订单状态异步通知的公网 HTTPS 地址;未传时以商户后台配置为准。见第 8 节。 |
bizParams.callbackUrl | 回跳地址 | String | 否 | "https://merchant.example.com/order/result" | 浏览器同步回跳地址,不参与支付和终态判断,不建议作为异步通知替代。 |
成功响应体字段
{"code":200,"msg":"操作成功","data":{"orderNo":"UP20260725123456789","requestId":"req_a1b2c3d4"}}
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
code | 业务状态码 | Integer | 始终出现 | 200 | 200 表示创建成功。 |
msg | 提示消息 | String | 始终出现 | "操作成功" | 调试参考。 |
data.orderNo | 来福订单号 | String | 成功时出现 | "UP20260725123456789" | 来福系统唯一订单号,以 UP 开头。下游必须持久化,用于后续查单和支付通知。 |
data.requestId | 请求流水 ID | String | 成功时出现 | "req_a1b2c3d4" | 本次请求内部流水号,排查问题时提供。 |
创建成功时订单状态为 WAIT_PAY,不会立即向上游充值。必须先查单取得锁价,再收款并调用支付通知。
6. 查询订单锁价与状态
创建订单后,下游必须主动查询以获取本单锁价(totalAmount/payAmount),并按 totalAmount 向用户收款。请求携带 OpenAPI 签名头。
GET https://api.laifuagi.cn/prod-api/openapi/v1/order/query?orderNo={orderNo}
app_id: {appId}
timestamp: {unix-seconds}
nonce: {unique-random-string}
sign: {base64url-hmac-sha256}
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
orderNo | 查询订单号 | String | Query 必填 | UP20260725123456789 | 创建接口返回的来福 orderNo。GET QueryString 传递,不参与签名原文。 |
签名规则补充
GET 请求签名原文:METHOD + "\n" + URI + "\n" + timestamp + "\n" + nonce + "\n"。URI 固定为 /openapi/v1/order/query,不包含 ?orderNo=... QueryString,也不包含公网 /prod-api 前缀。
成功响应体字段
{"code":200,"msg":"操作成功","data":{"orderNo":"UP20260725123456789","status":"WAIT_PAY","totalAmount":100.00,"payAmount":101.00,"createTime":"2026-07-25T10:00:00","requestId":"req_b2c3d4e5"}}
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
data.orderNo | 来福订单号 | String | 成功时出现 | "UP20260725123456789" | 与创建返回一致。 |
data.status | 订单状态 | String | 成功时出现 | "WAIT_PAY" | 来福订单当前状态,枚举值见第 10 节状态码表。 |
data.totalAmount | 用户应付金额 | Number | 成功时出现 | 100.00 | 下游应收/用户应付金额,也是支付成功通知的 amount 字段。固定两位小数。 |
data.payAmount | 来福扣款金额 | Number | 成功时出现 | 101.00 | 来福最终扣商户账户金额。该值可与 totalAmount 不同,下游不应向用户据此收款。 |
data.createTime | 创建时间 | String | 成功时出现 | "2026-07-25T10:00:00" | 订单创建时间,当前接口按 Java LocalDateTime.toString() 输出,日期与时间之间为 T。 |
data.requestId | 请求流水 ID | String | 成功时出现 | "req_b2c3d4e5" | 本次查询内部流水号。 |
只能查询当前 App 所属商户的订单;跨商户查询统一返回 code=200 data=null 或订单不存在。
7. 下游支付成功通知
安全关键:仅在下游服务端确认用户真实付款成功后调用。该入口不使用 OpenAPI 签名头,而使用本节独立 SHA-256 body 签名。前端/H5 不得直接调用此接口。
POST https://api.laifuagi.cn/prod-api/openapi/v1/sdk/payment/notify
Content-Type: application/json
{"merchantId":44,"orderNo":"UP20260725123456789","quoteId":"","amount":"100.00","timestamp":"1784532897689","nonce":"f40ff48166e5e663","sign":"{sha256-hex}"}
请求体字段
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
merchantId | 商户 ID | Integer | 必填 | 44 | 订单所属商户 ID,由来福分配。通过正文传递即可。 |
orderNo | 来福订单号 | String | 必填 | "UP20260725123456789" | 创建接口返回的 orderNo。 |
quoteId | 报价 ID | String | 必填(可空串) | "" | 无 quoteId 时传空字符串 ""。签名原文仍保留 quoteId=。 |
amount | 支付金额 | String | 必填 | "100.00" | 查单返回的 totalAmount,固定两位小数字符串。不是 payAmount。 |
timestamp | 时间戳 | String/Number | 必填 | "1784532897689" | 当前毫秒时间戳,字符串或 JSON 数字均可,签名时使用其文本值。 |
nonce | 随机串 | String | 必填 | "f40ff48166e5e663" | 本次请求唯一随机串。字段名是 nonce 而非 nonceStr。 |
sign | 签名 | String | 必填 | "a1b2c3d4e5f6..." | 64 位 SHA-256 十六进制小写串,不是 HMAC 也不是 Base64URL。 |
签名算法
payload = merchantId={merchantId}&orderNo={orderNo}"eId={quoteId}&amount={amount两位小数}×tamp={timestamp}&nonce={nonce}
signText = payload + "&key=" + app_secret + "&sandbox=false"
sign = SHA256(signText).hexLowercase()
签名使用该商户启用的 OpenAPI 应用 app_secret。参数按固定顺序拼接待签字符串,不排序。
成功响应
{"code":200,"msg":"操作成功","data":true}
成功响应 code=200 只代表支付通知已被来福受理,不代表充值成功。充值终态通过异步通知送达。
8. 来福异步通知
来福在订单状态变更时向下游配置的通知地址发送异步通知。通知可能重复送达,下游须按 orderNo + event + orderStatus 幂等处理。通知地址优先级以订单/商户实际配置解析结果为准,接入前请在各商户后台的「通知配置」页面确认实际生效地址。
POST {notifyUrl}
Content-Type: application/json
X-LYP-Timestamp: {unix-milliseconds}
X-LYP-Nonce: {unique-random-string}
X-LYP-Sign: {hmac-sha256-hex}
{"event":"ORDER_SUCCESS","orderNo":"UP20260725123456789","merchantId":44,"typeCode":"PrepaidRefill","orderStatus":"SUCCESS","totalAmount":100.00,"payAmount":101.00,"userPayAmount":100.00,"merchantSettleAmount":101.00,"merchantProfitAmount":-1.00,"upstreamOrderNo":"UPSTREAM20260725001","upstreamStatus":"SUCCESS","mobile":"13800138000","faceValue":"100","timestamp":1784532897689,"nonce":"abc123def456","sign":"{hmac-sha256-hex}"}
通知请求头
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
Content-Type | 内容类型 | String | 始终出现 | application/json | 固定 application/json。 |
X-LYP-Timestamp | 通知时间戳 | String | 始终出现 | 1784532897689 | Unix 毫秒时间戳,用于防重放和时间窗口校验。 |
X-LYP-Nonce | 通知随机串 | String | 始终出现 | abc123def456 | 本次通知唯一随机串,与正文中 nonce 对应。 |
X-LYP-Sign | 通知签名 | String | 始终出现 | a1b2c3d4e5f6... | 64 位 HMAC-SHA256 十六进制小写串,签名算法见下文。 |
通知体字段
| 字段 | 中文名称 | 类型 | 必填/出现条件 | 示例 | 详细说明 |
event | 事件类型 | String | 业务通知出现 | "ORDER_SUCCESS" | 事件枚举值:ORDER_PROCESSING、ORDER_SUCCESS、ORDER_FAIL、ORDER_REFUND。现行协议以正文该字段为准。 |
orderNo | 来福订单号 | String | 始终出现 | "UP20260725123456789" | 来福系统唯一订单号。 |
merchantId | 商户 ID | Integer | 始终出现 | 44 | 订单归属商户 ID。 |
typeCode | 产品编码 | String | 始终出现 | "PrepaidRefill" | 产品类型编码。 |
interfaceTypeCode | 接口类型编码 | String | 出现时 | "PREPAID_REFILL" | 接口类型编码,部分场景出现。 |
orderStatus | 订单状态 | String | 始终出现 | "SUCCESS" | 订单最新状态,枚举值见第 10 节。 |
totalAmount | 用户应付金额 | Number | 始终出现 | 100.00 | 用户应付总金额。 |
payAmount | 来福扣款金额 | Number | 始终出现 | 101.00 | 来福从商户账户扣款金额。 |
userPayAmount | 用户支付金额 | Number | 始终出现 | 100.00 | 用户实际支付金额。 |
merchantSettleAmount | 商户结算金额 | Number | 始终出现 | 101.00 | 来福与商户的结算金额。 |
merchantProfitAmount | 商户利润 | Number | 出现时 | -1.00 | 商户毛利金额(可为负数表示亏损),部分场景可能不出现。 |
upstreamOrderNo | 上游订单号 | String | 出现时 | "UPSTREAM20260725001" | 上游(智信等)返回的订单号;上游未响应时可能缺失。 |
upstreamStatus | 上游状态 | String | 出现时 | "SUCCESS" | 上游返回的状态文本;上游未响应时可能缺失。 |
upstreamMessage | 上游消息 | String | 出现时 | "充值成功" | 上游返回的消息文本;上游未响应时可能缺失。 |
upstreamRaw | 上游原始响应 | String | 出现时 | {"code":0,...} | 上游返回的原始响应数据,调试用;上游未响应时可能缺失。 |
refundAmount | 退款金额 | Number | 出现时(退款场景) | 101.00 | 发生退款时的退款金额。 |
refundStatus | 退款状态 | String | 出现时(退款场景) | "SUCCESS" | 退款当前状态。 |
refundTime | 退款时间 | String | 出现时(退款场景) | "2026-07-25 12:00:00" | 退款完成时间。 |
refundReason | 退款原因 | String | 出现时(退款场景) | "上游充值失败" | 触发退款的原因描述。 |
refundRemark | 退款备注 | String | 出现时(退款场景) | "人工处理" | 退款相关补充说明。 |
refundRequestNo | 退款请求号 | String | 出现时(退款场景) | "REF20260725001" | 退款请求流水号。 |
refundExternalNo | 退款外部编号 | String | 出现时(退款场景) | "EXTREF001" | 退款外部参考编号。 |
notifyUrl | 通知地址 | String | 出现时 | "https://..." | 本次通知目标地址,部分场景出现。 |
callbackUrl | 回跳地址 | String | 出现时 | "https://..." | 浏览器同步回跳地址,部分场景出现。 |
mobile | 充值手机号 | String | 出现时 | "13800138000" | 目标充值手机号。部分通知场景中不包含该字段,以创建订单时传入值为准。 |
faceValue | 充值面额 | String | 出现时 | "100" | 充值面额字符串,部分通知场景中不包含该字段。 |
carrierCode | 运营商编码 | String | 出现时 | "01" | 运营商编码:01 移动、02 电信、03 联通;创建时未传则可能不出现。 |
timestamp | 通知时间戳 | Number | 始终出现 | 1784532897689 | Unix 毫秒时间戳,与请求头 X-LYP-Timestamp 对应。签名层强制添加。 |
nonce | 通知随机串 | String | 始终出现 | "abc123def456" | 本次通知唯一随机串,与请求头 X-LYP-Nonce 对应。签名层强制添加。 |
sign | 签名 | String | 始终出现 | "a1b2c3d4..." | HMAC-SHA256 十六进制小写串。该字段不参与自身签名原文。签名层强制添加。 |
通知验签算法
1. 解析 JSON 通知体,取得全部一级字段
2. 排除 sign 字段(该字段不参与自身签名原文)
3. 排除值为 null 的字段
4. 剩余一级字段放入按字段名字典序排列的对象(Java TreeMap 语义)
5. 将整个排序后对象序列化为紧凑 JSON;字符串仍带 JSON 双引号,数字和布尔值保持 JSON 类型,嵌套值按原 JSON 值参与
6. 将该完整紧凑 JSON 字符串作为 HMAC 的 data 输入
7. expectedSign = HMAC-SHA256(apiSecret, jsonString).hexLowercase()
8. 使用常量时间比较 expectedSign 与请求头 X-LYP-Sign
注意:header 中的 sign 与 body 中的 sign 值相同。验签密钥使用商户后台 SDK 安全配置中的通知密钥(apiSecret),不是 OpenAPI 的 app_secret。两者不同。
通知密钥:使用商户后台 SDK 安全配置中的通知密钥(apiSecret),不是 OpenAPI 的 app_secret。两者不同。
成功应答要求
| 应答条件 | 详细规则 |
| HTTP 状态码 204 | 直接视为成功。 |
| HTTP 状态码 2xx | 同时要求 body 去除首尾空白后为纯文本 success 或 ok(不区分大小写)。 |
| JSON 响应 | body 解析为 JSON 后,code 字段字符串值为 "200",或 success 字段布尔值为 true,视为成功。 |
| 不符合上述条件 | 来福视为投递失败并进入重试。 |
重试策略:最多投递 5 次(含首次)。失败后重试延迟按 2、4、8、16 分钟退避(如仍失败最后停止自动重试)。来福人工补发仍发送订单当前标准事件,不发送内部操作事件。
9. 签名算法汇总
本文档涉及两套签名算法,其用途和密钥各不相同,请严格区分。
| 签名类型 | 接口 | 密钥 | 算法 | 输出格式 |
| OpenAPI 签名 | 创建订单、查单 | app_secret | HMAC-SHA256 | Base64URL(去掉 = 补位) |
| 支付通知签名 | 支付成功通知 | app_secret | SHA-256(非 HMAC) | 十六进制小写(64 位) |
| 异步通知验签 | 来福通知回调 | apiSecret(通知密钥) | HMAC-SHA256 | 十六进制小写(64 位) |
OpenAPI 签名(创建/查单)
// POST 请求
signText = METHOD + "\n" + URI + "\n" + timestamp + "\n" + nonce + "\n" + rawBody
// GET 请求
signText = METHOD + "\n" + URI + "\n" + timestamp + "\n" + nonce + "\n"
sign = base64url(HMAC-SHA256(app_secret, signText))
// 去掉 base64url 结果中的 '=' 补位符
URI 使用内部路径,不含公网 /prod-api 前缀,不含 QueryString:创建为 /openapi/v1/order/create,查询为 /openapi/v1/order/query。
rawBody 必须与实际发送的 HTTP 请求体字节一致。签名后不要重新格式化 JSON。
- 不使用 MD5、字段排序签名或
nonceStr。app_secret 只能保存在下游服务端。
支付通知签名
payload = merchantId={merchantId}&orderNo={orderNo}"eId={quoteId}&amount={amount}×tamp={timestamp}&nonce={nonce}
signText = payload + "&key=" + app_secret + "&sandbox=false"
sign = SHA256(signText).hexLowercase()
异步通知验签
// 1. 取 JSON 体所有一级字段
// 2. 排除 sign 字段和 null 值字段
// 3. ASCII 升序排列字段名
// 4. 将排序后的字段值序列化为紧凑 JSON 字符串(无空格、无换行)
// - 对象/Map → 紧凑 JSON(如 {"a":1})
// - 数组/List → 紧凑 JSON(如 [1,2,3])
// - 其他值按 JSON 语义使用文本值
// 5. expected = HMAC-SHA256(apiSecret, jsonString).hexLowercase()
// 6. 使用常量时间比较 expected 与 X-LYP-Sign
10. 状态码、订单状态与事件
公共业务状态码
| code | 含义 | 建议处理 |
200 | 业务受理成功 | 正常处理 data。 |
400 | 请求参数错误 | 检查请求体字段格式、必填项。 |
401 | 签名验证失败 | 核对 app_id、密钥、URI、rawBody、时间戳单位、nonce 字段名。 |
403 | 无权限(IP 白名单/商户未授权) | 联系来福确认 IP 白名单和产品授权。 |
404 | 订单不存在 | 检查 orderNo 所属 App 是否正确的商户。 |
409 | 订单状态冲突 | 订单状态不允许当前操作,查单确认最新状态。 |
429 | 请求频率超限 | 降低调用频率,稍后重试。 |
500 | 平台内部错误 | 等待后重试,持续失败则联系来福技术支持。 |
订单状态枚举
| 状态 | 含义 | 下游处理 |
WAIT_PAY | 待下游收款确认 | 不要触发充值。查询锁价并在自有渠道收款。 |
CREATED | 已创建 | 订单已创建但尚未进入支付确认流程。 |
PAID | 支付确认已受理 | 来福已收到支付通知,准备履约。展示处理中。 |
PROCESSING | 上游处理中 | 等待终态异步通知。不重复下单。 |
SUCCESS | 充值成功 | 完成订单并向用户展示成功。 |
FAILED | 充值失败 | 等待资金冲正/退款结果及终态通知。 |
REFUNDED | 平台冲正完成 | 按合同和自身支付渠道处理用户侧退款。 |
CLOSED | 未支付或异常关闭 | 需要时重新创建订单。 |
异步通知事件枚举
| 事件 | 触发条件 | 对应 orderStatus |
ORDER_PROCESSING | 订单进入上游处理中 | PROCESSING |
ORDER_SUCCESS | 充值成功终态 | SUCCESS |
ORDER_FAIL | 充值失败 | FAILED |
ORDER_REFUND | 已冲正/退款完成 | REFUNDED |
11. 成功判定与错误排查
成功判定原则
- 同时检查 HTTP 状态码(应为
200)与 JSON code(应为 200),两者均满足才是接口受理成功。
- 支付成功通知的
code=200 只代表来福受理,不代表充值成功。充值成功以异步通知 orderStatus=SUCCESS 为准。
- 网络超时/连接中断不等于失败,先使用原
orderNo 查单,禁止直接新建订单。
常见错误与排查
| 错误现象 | 可能原因 | 排查步骤 |
| 签名验证失败 | 密钥错误、URI 路径不对、rawBody 被重新格式化、时间戳超差 | 核对内部 URI(不含 /prod-api)、原始 JSON 字节、时间戳单位及 nonce 字段名;确认使用了 app_secret 而非 apiSecret。 |
| 当前商户该话费 SKU 未配置有效价格 | 运营商/面额未授权或 SKU 不唯一 | 确认商户已开通该运营商+面额的 SKU,且价格有效。 |
| 支付金额与订单锁价不一致 | 支付通知传了错误金额 | 支付通知必须传查单返回的 totalAmount,不能随意填写。 |
| 订单状态不允许支付确认 | 订单已在 PAID 或终态 | 先查单确认当前状态,不得再次触发真实履约。 |
| 订单不存在 | orderNo 不存在或跨商户查询 | 确认 orderNo 属于当前 App 对应商户。 |
| 异步通知验签不通过 | 使用了错误密钥或 canonical 算法有误 | 确认使用 apiSecret 而非 app_secret;确认已排除 sign 和 null 字段;确认 ASCII 升序排列;确认 timestamp/nonce 使用请求头值。 |
12. 幂等机制与重试策略
创建订单防重
merchantRequestNo 是下游业务请求号。下游必须自行保证唯一并持久化它与来福 orderNo 的映射;网络结果不确定时先查已保存的 orderNo,不要假设重复创建必然返回同一订单。
支付通知幂等
相同 orderNo + sign 重复请求被幂等处理。每次须复用完全相同的数据,平台按订单和签名自动去重。
异步通知幂等
下游收到重复通知时,按 orderNo + event + orderStatus 去重。终态通知(SUCCESS/FAILED/REFUNDED)到达后不应再触发用户侧重复处理。
下游重试指南
| 场景 | 推荐策略 |
| 创建订单超时 | 先按本地保存的映射核查是否已取得 orderNo;无法确认时联系来福排查,不直接生成新请求号重复下单。 |
| 查单超时 | 使用原 orderNo 重试。 |
| 支付通知超时或非 200 | 使用完全相同的数据重试,幂等处理。 |
| 异步通知接收失败 | 来福最多投递 5 次(含首次),失败后按 2/4/8/16 分钟退避。下游可查来福订单,但不要绕过来福直接查上游。 |
13. 联调与安全上线门禁
以下为上线前的必检项目,逐项确认后方可切换真实流量。
- 基本信息确认:商户已审核、权益已开通、应用已启用、产品已授权、固定出口 IP 已加白名单。
- SKU 价格确认:传
carrierCode 时确认 PrepaidRefill + carrierCode + faceValue 存在唯一有效 SKU;不传时确认该面额三网 SKU 完整且商户结算价、用户支付价一致。
- 数据持久化:下游保存
merchantRequestNo / orderNo / totalAmount / payAmount 并可对账。
- 服务端安全:支付回调必须由下游服务端验真;前端/H5 不得直接调用支付成功通知。密钥仅保存在服务端。
- 边界测试:验证金额不一致、错误签名、重复通知、超时查单和异步通知幂等场景。
- 不可逆操作:真实话费充值不可逆。未经双方明确授权不得使用真实号码执行测试。联调应使用来福提供的测试手机号。
- 异步通知验签确认:已按第 9 节算法实现
X-LYP-Sign 验签,并确认使用了 apiSecret 而非 app_secret。
- 响应处理确认:异步通知收到后返回 HTTP
2xx 且 body 为 ok 或 success。
协议冻结:本页版本为 v1.3 / 2026-07-25。后续若无正式版本升级通知,下游继续按本版本实现,不接受聊天记录、历史样例或内部兼容字段覆盖本文。
14. Java 与 Node.js 签名示例
以下示例供参考,生产环境请将密钥配置在安全配置中心或环境变量中。
Java OpenAPI 签名示例
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class OpenApiSigner {
public static String sign(String method, String uri, long timestamp,
String nonce, String rawBody, String secret) throws Exception {
String signText = method + "\n" + uri + "\n" + timestamp + "\n" + nonce + "\n";
if (rawBody != null && !rawBody.isEmpty()) {
signText += rawBody;
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] hmac = mac.doFinal(signText.getBytes("UTF-8"));
String base64 = Base64.getUrlEncoder().withoutPadding().encodeToString(hmac);
return base64;
}
// 使用示例
public static void main(String[] args) throws Exception {
String secret = "your-app-secret";
long timestamp = System.currentTimeMillis() / 1000;
String nonce = java.util.UUID.randomUUID().toString();
String body = "{\"typeCode\":\"PrepaidRefill\",\"merchantRequestNo\":\"M202607250001\",\"bizParams\":{\"mobile\":\"13800138000\",\"faceValue\":\"100\",\"notifyUrl\":\"https://merchant.example.com/laifu/notify\"}}";
String sign = sign("POST", "/openapi/v1/order/create",
timestamp, nonce, body, secret);
System.out.println("sign: " + sign);
}
}
Java 支付通知签名示例
import java.security.MessageDigest;
public class PaymentNotifySigner {
public static String sign(long merchantId, String orderNo, String quoteId,
String amount, String timestamp, String nonce,
String secret) throws Exception {
String payload = "merchantId=" + merchantId
+ "&orderNo=" + orderNo
+ ""eId=" + (quoteId == null ? "" : quoteId)
+ "&amount=" + amount
+ "×tamp=" + timestamp
+ "&nonce=" + nonce;
String signText = payload + "&key=" + secret + "&sandbox=false";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(signText.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b & 0xFF));
}
return hex.toString();
}
}
Java 异步通知验签示例
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class NotifyVerifier {
public static boolean verify(String jsonBody,
String apiSecret) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> body = mapper.readValue(jsonBody, Map.class);
// 1. 排除 sign 和 null 值字段
TreeMap<String, Object> sorted = new TreeMap<>();
for (Map.Entry<String, Object> e : body.entrySet()) {
if ("sign".equals(e.getKey()) || e.getValue() == null) continue;
sorted.put(e.getKey(), e.getValue());
}
// 2. 按 ASCII 排序后的键值对,序列化为紧凑 JSON 字符串
// TreeMap 保证字典序;Jackson writeValueAsString 输出紧凑 JSON
String jsonString = mapper.writeValueAsString(sorted);
// 3. HMAC-SHA256
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes("UTF-8"), "HmacSHA256"));
byte[] hmac = mac.doFinal(jsonString.getBytes("UTF-8"));
StringBuilder expectedHex = new StringBuilder();
for (byte b : hmac) {
expectedHex.append(String.format("%02x", b & 0xFF));
}
// 4. 从原始 JSON 体中提取 sign 值做常量时间比较
String actualSign = (String) body.get("sign");
return MessageDigest.isEqual(
expectedHex.toString().getBytes("UTF-8"),
actualSign.getBytes("UTF-8"));
}
}
Node.js OpenAPI 签名示例
const crypto = require('crypto');
function sign(method, uri, timestamp, nonce, rawBody, secret) {
let signText = method + '\n' + uri + '\n' + timestamp + '\n' + nonce + '\n';
if (rawBody) signText += rawBody;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(signText, 'utf8');
return hmac.digest('base64url'); // Node.js 15.7+ 支持 base64url
}
// 使用示例
const secret = 'your-app-secret';
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomUUID();
const body = JSON.stringify({
typeCode: 'PrepaidRefill',
merchantRequestNo: 'M202607250001',
bizParams: {
mobile: '13800138000',
faceValue: '100',
notifyUrl: 'https://merchant.example.com/laifu/notify'
}
});
const s = sign('POST', '/openapi/v1/order/create',
timestamp, nonce, body, secret);
console.log('sign:', s);
Node.js 支付通知签名示例
const crypto = require('crypto');
function paymentSign(merchantId, orderNo, quoteId, amount,
timestamp, nonce, secret) {
const payload = `merchantId=${merchantId}&orderNo=${orderNo}` +
`"eId=${quoteId || ''}&amount=${amount}` +
`×tamp=${timestamp}&nonce=${nonce}`;
const signText = payload + `&key=${secret}&sandbox=false`;
return crypto.createHash('sha256')
.update(signText, 'utf8')
.digest('hex');
}
Node.js 异步通知验签示例
const crypto = require('crypto');
function verifyNotify(jsonBody, apiSecret) {
const body = JSON.parse(jsonBody);
// 1. 排除 sign 和 null,ASCII 排序后再序列化为紧凑 JSON
const sorted = {};
Object.keys(body)
.filter(k => k !== 'sign' && body[k] !== null)
.sort()
.forEach(k => { sorted[k] = body[k]; });
// 2. 紧凑 JSON 字符串即为签名原文
const jsonString = JSON.stringify(sorted);
// 3. HMAC-SHA256 hex
const hmac = crypto.createHmac('sha256', apiSecret);
hmac.update(jsonString, 'utf8');
const expected = hmac.digest('hex');
// 4. 常量时间比较
return crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(body.sign, 'utf8')
);
}
生产注意事项:密钥应通过环境变量或安全配置中心读取,不硬编码。HMAC 算法的 key 和 data 均使用 UTF-8 编码。确保 rawBody 与实际的 HTTP 请求体字节一致。