/group/fast/pay
生成收银台链接 / 二维码
/lakala/order/query
按业务单号查询支付结果
/lakala/order/refund
按业务单号退款
支付成功后,平台会把交易结果 POST JSON 到分组配置的 callback_url。回调验签与接口签名算法相同。
1. 接入准备
平台为每个接入方分配:
| 配置项 | 说明 |
|---|---|
group_id | 分组编码 |
token | 分组密钥,仅用于签名,只放服务端 |
callback_url | 支付成功异步通知地址,由接入方提供,配置在平台侧 |
业务订单号 order_no 由接入方生成:建议 1~10 位字母或数字,本业务内唯一,不要包含下划线 _。
2. 统一签名算法
- 取出参与签名的全部参数。
sign本身不参与签名。 - 每个参数转成
key=value。嵌套对象按「单引号形式的字典字符串」再用双引号包起来。 - 按参数名 ASCII 升序,用
&拼接。 - 末尾追加
&token={分组token}。 - 对整串做 MD5,得到 32 位小写十六进制。
| 场景 | 签名位置 |
|---|---|
| GET /group/fast/pay | URL 查询参数 sign |
| POST 查询 / 退款 | HTTP Header Authorization |
| 平台回调接入方 | HTTP Header Authorization |
只传有值的字段;数字按原样拼接;参数名、值均区分大小写。
import hashlib
def json_to_query_string(data: dict) -> str:
pairs = []
for key, value in data.items():
if key == "sign":
continue
if isinstance(value, dict):
dict_str = str(value).replace('"', "'")
pairs.append(f'{key}="{dict_str}"')
else:
pairs.append(f"{key}={value}")
pairs.sort()
return "&".join(pairs)
def make_sign(params: dict, token: str) -> str:
sign_raw = json_to_query_string(params) + "&token=" + token
return hashlib.md5(sign_raw.encode("utf-8")).hexdigest()
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class HuibanSign {
public static String md5(String s) throws Exception {
byte[] digest = MessageDigest.getInstance("MD5")
.digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static String makeSign(Map<String, String> params, String token) throws Exception {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder sb = new StringBuilder();
for (String key : keys) {
if ("sign".equals(key)) continue;
if (sb.length() > 0) sb.append("&");
sb.append(key).append("=").append(params.get(key));
}
sb.append("&token=").append(token);
return md5(sb.toString());
}
}
<?php
function json_to_query_string(array $data): string {
$pairs = [];
foreach ($data as $key => $value) {
if ($key === 'sign') {
continue;
}
$pairs[] = $key . '=' . $value;
}
sort($pairs, SORT_STRING);
return implode('&', $pairs);
}
function make_sign(array $params, string $token): string {
return md5(json_to_query_string($params) . '&token=' . $token);
}
const crypto = require("crypto");
function jsonToQueryString(data) {
return Object.keys(data)
.filter((key) => key !== "sign")
.sort()
.map((key) => `${key}=${data[key]}`)
.join("&");
}
function makeSign(params, token) {
return crypto
.createHash("md5")
.update(jsonToQueryString(params) + "&token=" + token)
.digest("hex");
}
签名示例(token 请替换为平台分配值):
group_id=123&order_no=8675630657×tamp=1788670000&token={token}
sign / Authorization = md5(上述字符串)
3. 发起支付 GET /group/fast/pay
用户在微信或支付宝内打开该链接(或扫码打开)完成支付。无需指定渠道,按客户端自动识别。
| 参数 | 必填 | 参与签名 | 说明 |
|---|---|---|---|
group_id | 是 | 是 | 分组编码 |
order_no | 是 | 是 | 业务订单号,建议 1~10 位 |
amount | 是 | 是 | 支付金额,单位 元,如 0.01 |
timestamp | 是 | 是 | 订单过期时间(Unix 秒或毫秒),必须大于当前时间 |
sign | 是 | 否 | 按第 2 节计算后放入 URL |
timestamp 是链接有效期截止时间,不是请求发起时刻。建议设为「当前时间 + 有效秒数」。收款商户由平台按分组轮询分配。
完整链接形态:
https://q.lakala.ink/group/fast/pay?group_id=123&amount=0.01&order_no=8675630657×tamp=1788670000&sign=xxxxxxxx
各语言发起支付示例
import hashlib
import time
from urllib.parse import urlencode
TOKEN = "YOUR_TOKEN"
BASE = "https://q.lakala.ink"
def make_sign(params: dict, token: str) -> str:
pairs = [f"{k}={v}" for k, v in params.items() if k != "sign"]
pairs.sort()
return hashlib.md5(("&".join(pairs) + "&token=" + token).encode()).hexdigest()
params = {
"group_id": "123",
"amount": "0.01",
"order_no": "8675630657",
"timestamp": str(int(time.time()) + 600), # 10 分钟后过期
}
params["sign"] = make_sign(params, TOKEN)
pay_url = BASE + "/group/fast/pay?" + urlencode(params)
print(pay_url) # 做成二维码,用户用微信或支付宝扫码
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class CreatePayUrl {
static String md5(String s) throws Exception {
byte[] d = MessageDigest.getInstance("MD5").digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : d) sb.append(String.format("%02x", b));
return sb.toString();
}
static String makeSign(Map<String, String> params, String token) throws Exception {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder raw = new StringBuilder();
for (String k : keys) {
if ("sign".equals(k)) continue;
if (raw.length() > 0) raw.append("&");
raw.append(k).append("=").append(params.get(k));
}
return md5(raw.append("&token=").append(token).toString());
}
public static void main(String[] args) throws Exception {
String token = "YOUR_TOKEN";
Map<String, String> params = new LinkedHashMap<>();
params.put("group_id", "123");
params.put("amount", "0.01");
params.put("order_no", "8675630657");
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000 + 600));
params.put("sign", makeSign(params, token));
StringBuilder url = new StringBuilder("https://q.lakala.ink/group/fast/pay?");
boolean first = true;
for (Map.Entry<String, String> e : params.entrySet()) {
if (!first) url.append("&");
first = false;
url.append(URLEncoder.encode(e.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(e.getValue(), "UTF-8"));
}
System.out.println(url); // 做成二维码即可收款
}
}
<?php
$token = 'YOUR_TOKEN';
function make_sign(array $params, string $token): string {
$pairs = [];
foreach ($params as $k => $v) {
if ($k === 'sign') continue;
$pairs[] = $k . '=' . $v;
}
sort($pairs, SORT_STRING);
return md5(implode('&', $pairs) . '&token=' . $token);
}
$params = [
'group_id' => '123',
'amount' => '0.01',
'order_no' => '8675630657',
'timestamp' => (string) (time() + 600),
];
$params['sign'] = make_sign($params, $token);
$payUrl = 'https://q.lakala.ink/group/fast/pay?' . http_build_query($params);
echo $payUrl;
const crypto = require("crypto");
const TOKEN = "YOUR_TOKEN";
function makeSign(params, token) {
const raw =
Object.keys(params)
.filter((k) => k !== "sign")
.sort()
.map((k) => `${k}=${params[k]}`)
.join("&") + "&token=" + token;
return crypto.createHash("md5").update(raw).digest("hex");
}
const params = {
group_id: "123",
amount: "0.01",
order_no: "8675630657",
timestamp: String(Math.floor(Date.now() / 1000) + 600),
};
params.sign = makeSign(params, TOKEN);
const payUrl =
"https://q.lakala.ink/group/fast/pay?" + new URLSearchParams(params).toString();
console.log(payUrl);
常见失败提示
| 页面提示 | 原因 |
|---|---|
| 参数不能为空 | 缺少必填参数 |
| amount必须大于0 / 必须是有效数字 | 金额不合法 |
| timestamp必须是有效时间戳 | 时间戳无法解析 |
| 订单已过期 | timestamp 早于当前时间 |
| 无效的分组编码 | group_id 不存在 |
| 无效的签名 | sign 与平台计算结果不一致 |
4. 订单查询 POST /lakala/order/query
仅返回该分组商户列表内的订单。Header 携带 Authorization。
POST /lakala/order/query
Content-Type: application/json
Authorization: {签名}
{
"group_id": "123",
"order_no": "8675630657",
"timestamp": "1788670000"
}
| 参数 | 必填 | 说明 |
|---|---|---|
| group_id | 是 | 分组编码 |
| order_no | 是 | 业务订单号 |
| timestamp | 是 | 当前 Unix 时间戳,与服务器相差不超过 24 小时 |
成功 code = 0。data 为数组,查无记录时为 []。金额单位为分。outOrderNo 格式:终端号_业务订单号_随机串。
5. 订单退款 POST /lakala/order/refund
只需业务单号。平台查出原支付商户号、流水号和金额,并校验商户属于当前分组。
POST /lakala/order/refund
Content-Type: application/json
Authorization: {签名}
# 全额退
{ "group_id": "123", "order_no": "8675630657", "timestamp": "1788670000" }
# 部分退,amount 单位为元,必须参与签名
{ "group_id": "123", "order_no": "8675630657", "amount": "0.01", "timestamp": "1788670000" }
成功时 code = 0。data.trade_state = SUCCESS 表示退款成功。退款金额字段单位为分。
6. 支付成功回调
接入时向平台提供可公网访问的 HTTPS 地址,例如 https://your.domain.com/api/lakala/pay/notify。平台写入该分组的 callback_url。
- 公网可达,建议 HTTPS
- 尽快返回 HTTP 200,业务本地异步处理
- 按业务单号做幂等,同一笔可能重复通知
平台确认 trade_main_type == PREORDER 后转发:
POST {callback_url}
Content-Type: application/json
Authorization: {签名}
{
"merchant_no": "822331053111M64",
"log_no": "66232740914878",
"out_trade_no": "Q5236347_8675630657_mB0QaXb7",
"trade_state": "SUCCESS",
"trade_main_type": "PREORDER",
"account_type": "ALIPAY",
"total_amount": "2",
"trade_time": "20260906110113"
}
| 字段 | 说明 |
|---|---|
| out_trade_no | 终端号_业务订单号_随机串,中间一段即接入方 order_no |
| trade_state | 支付成功一般为 SUCCESS |
| account_type | WECHAT / ALIPAY |
| total_amount | 订单金额,单位分 |
验签:用收到的完整 JSON 正文按第 2 节计算,与 Header Authorization 比较。失败则丢弃,不要更新订单。
从 Q5236347_8675630657_mB0QaXb7 解析业务单号:取中间段 8675630657。
7. 安全说明
token只放服务端,不要写进 H5 / App。- 查询、退款必须带
group_id,签名使用该分组 token。 - 查询 / 退款的 timestamp 是请求发起时刻,超过 24 小时会拒绝。
- 支付链接的 timestamp 是过期时间,两者不要混用。
建议流程:申请凭证并配置回调 → 服务端生成支付链接 → 回调验签后幂等更新订单 → 用查询接口对账补偿 → 退款只传 group_id + order_no。
8. 错误码(查询 / 退款)
| code | 说明 |
|---|---|
| 0 | 成功 |
| -1 | 失败,详见 msg |
| msg | 说明 |
|---|---|
| 无效的签名 | Authorization 与平台计算结果不一致 |
| 无效的分组编码 | group_id 不存在 |
| timestamp 与服务器时间相差超过 24 小时 | 请用当前时间 |
| 未查询到该分组下的支付记录 | 无此单,或不属于该分组 |
| 未查询到支付记录 | 查询接口查无记录(此时 code 仍为 0) |
| 请求体必须是JSON对象 | Content-Type 应为 application/json |