GROUP FAST PAY

分组支付接入文档

支付、查询、退款、异步回调使用同一套签名。网关:https://q.lakala.ink

GET
/group/fast/pay

生成收银台链接 / 二维码

POST
/lakala/order/query

按业务单号查询支付结果

POST
/lakala/order/refund

按业务单号退款

支付成功后,平台会把交易结果 POST JSON 到分组配置的 callback_url。回调验签与接口签名算法相同。

1. 接入准备

平台为每个接入方分配:

配置项说明
group_id分组编码
token分组密钥,仅用于签名,只放服务端
callback_url支付成功异步通知地址,由接入方提供,配置在平台侧

业务订单号 order_no 由接入方生成:建议 1~10 位字母或数字,本业务内唯一,不要包含下划线 _。

2. 统一签名算法

  1. 取出参与签名的全部参数。sign 本身不参与签名。
  2. 每个参数转成 key=value。嵌套对象按「单引号形式的字典字符串」再用双引号包起来。
  3. 按参数名 ASCII 升序,用 & 拼接。
  4. 末尾追加 &token={分组token}。
  5. 对整串做 MD5,得到 32 位小写十六进制。
场景签名位置
GET /group/fast/payURL 查询参数 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&timestamp=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&timestamp=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。

平台确认 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_typeWECHAT / ALIPAY
total_amount订单金额,单位分

验签:用收到的完整 JSON 正文按第 2 节计算,与 Header Authorization 比较。失败则丢弃,不要更新订单。

从 Q5236347_8675630657_mB0QaXb7 解析业务单号:取中间段 8675630657。

7. 安全说明

  1. token 只放服务端,不要写进 H5 / App。
  2. 查询、退款必须带 group_id,签名使用该分组 token。
  3. 查询 / 退款的 timestamp 是请求发起时刻,超过 24 小时会拒绝。
  4. 支付链接的 timestamp 是过期时间,两者不要混用。

建议流程:申请凭证并配置回调 → 服务端生成支付链接 → 回调验签后幂等更新订单 → 用查询接口对账补偿 → 退款只传 group_id + order_no。

8. 错误码(查询 / 退款)

code说明
0成功
-1失败,详见 msg
msg说明
无效的签名Authorization 与平台计算结果不一致
无效的分组编码group_id 不存在
timestamp 与服务器时间相差超过 24 小时请用当前时间
未查询到该分组下的支付记录无此单,或不属于该分组
未查询到支付记录查询接口查无记录(此时 code 仍为 0)
请求体必须是JSON对象Content-Type 应为 application/json