活动商品发放接口文档

接口概述

合作方通过本接口向用户发放活动商品,支持免费 VIP 会员和体验卡两种商品类型。

基本信息

项目 说明
请求地址 /order/receiveAward
请求方式 GET、POST
数据格式 Query String
签名方式 MD5(默认)

请求参数

参数名 类型 必填 说明
appId Long 合作方 ID,由我方分配
accountType Integer 账号凭证类型,固定传 1(手机号)
account String 用户手机号(AES/ECB 加密后 Hex 编码,密钥为合作方 referKey 前 16 位)
goodsCode String 商品编码,格式见下方说明
outOrderNo String 外部唯一订单号,用于幂等去重,同一 appId 下不可重复
ts Long 请求时间戳,精确到毫秒,有效期 60 秒
token String 签名 token,生成规则见「签名说明」

商品编码(goodsCode)

商品编码格式为 {类型}_{天数},支持以下类型:

商品编码 商品类型 说明
vip_7 免费 VIP 会员 7 天 VIP
vip_15 免费 VIP 会员 15 天 VIP
vip_31 免费 VIP 会员 31 天 VIP(1 个月)
experience_7 体验卡 7 天体验卡
experience_15 体验卡 15 天体验卡
experience_31 体验卡 31 天体验卡(1 个月)

响应参数

参数名 类型 说明
status Integer 状态码,0 表示成功
msg String 状态描述

成功响应示例

{
  "status": 0,
  "msg": ""
}

失败响应示例

{
  "status": 4,
  "msg": "参数错误,商品编码错误"
}

错误码

错误码 说明
0 成功
1 发放失败(会员领取失败 / 体验卡领取失败)
4 参数错误(商品编码错误、请求过期、accountType 错误等)
16 创建用户失败
21 合作方非法(合作方不存在或已停用)
22 分销商品不存在(商品已下架或库存为 0)
27 领取数目超出限制
40 已注销用户不能在规定时间内重复注册
41 170/171 等网络虚拟号不支持注册
42 仅限新用户领取
43 商品余额不足
44 下单频繁,请稍后重试

签名说明

签名生成规则

  1. 将所有请求参数(除 partnerIdappIdtoken 外)按参数名 ASCII 码升序排列
  2. 将排序后的参数拼接为 URL 格式:{请求路径}?{key1}={value1}&{key2}={value2}...
  3. 在拼接结果末尾追加合作方密钥(referKey)
  4. 对拼接字符串进行 MD5 签名(UTF-8 编码),得到 token 值

签名示例

假设: - 请求路径:/order/receiveAward - 合作方密钥:your_refer_key_here - 请求参数:accountType=1&account=xxx&goodsCode=experience_7&outOrderNo=ORDER123&ts=1700000000000

调试参数: - 测试域名:https://earth-openapi.lazyaudio.com - 测试appId:220802001407 - 测试referKey:)yzO-V~R51?~:pf&PtTyGYu:

Step 1 - 参数按 ASCII 排序:account, accountType, goodsCode, outOrderNo, ts

Step 2 - 拼接为待签名字符串:

/order/receiveAward?account=xxx&accountType=1&goodsCode=experience_7&outOrderNo=ORDER123&ts=1700000000000

Step 3 - 末尾追加密钥:

/order/receiveAward?account=xxx&accountType=1&goodsCode=experience_7&outOrderNo=ORDER123&ts=1700000000000your_refer_key_here

Step 4 - MD5 签名得到 token

account 字段加密说明

account 字段需要对用户手机号进行 AES 加密:

  1. 密钥:合作方 referKey 的前 16 个字符(UTF-8 编码为 16 字节,即 128 位 AES 密钥)
  2. 算法AES/ECB/PKCS5Padding
  3. 输入编码:手机号明文以 UTF-8 编码为字节数组
  4. 输出编码:加密后的字节数组进行小写十六进制(Hex)编码,不是 Base64

加密伪代码

aes_key = referKey[0:16].encode("UTF-8")    // 取前 16 个字符
encrypted_bytes = AES_ECB_PKCS5(phone.encode("UTF-8"), aes_key)
account = hex_encode(encrypted_bytes)         // 小写十六进制

业务规则

  1. 幂等性:同一 appId + outOrderNo 组合重复请求,VIP 类型会返回成功(不重复发放),体验卡类型通过服务端唯一 ID 保障幂等
  2. 请求频率:同一用户的领取请求有频率限制(10 秒内同一 account 不可重复请求)
  3. 时效性:请求时间戳 ts 与服务端时间差不能超过 60 秒
  4. 领取限制:根据后台配置的限制规则(月限、时间段限、每日金额上限等),超出限制将返回错误码 27
  5. 用户限制:部分商品配置为仅限新用户领取,已有账号的用户将返回错误码 42

调用示例(Java)

import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;

public class ReceiveAwardDemo {

    private static final Long APP_ID = 200323001810L;
    private static final String REFER_KEY = "your_refer_key_here";

    public static void main(String[] args) {
        Map<String, String> params = new HashMap<>();
        params.put("accountType", "1");
        params.put("account", encryptAccount("13800138000", REFER_KEY));
        params.put("goodsCode", "experience_7");
        params.put("outOrderNo", "ORDER_" + System.currentTimeMillis());
        params.put("ts", String.valueOf(System.currentTimeMillis()));

        String uri = "/order/receiveAward";
        String token = generateToken(params, uri, REFER_KEY);

        params.put("appId", String.valueOf(APP_ID));
        params.put("token", token);
        String url = "https://open.example.com" + uri + "?" + buildQueryString(params);
        System.out.println("Request URL: " + url);
    }

    /**
     * account 字段加密
     * 算法: AES/ECB/PKCS5Padding
     * 密钥: referKey 的前 16 个字符(UTF-8 编码)
     * 输出: 加密结果的小写十六进制字符串
     */
    private static String encryptAccount(String phone, String referKey) {
        try {
            byte[] keyBytes = referKey.substring(0, 16).getBytes(StandardCharsets.UTF_8);
            SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encrypted = cipher.doFinal(phone.getBytes(StandardCharsets.UTF_8));
            return Hex.encodeHexString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("account 加密失败", e);
        }
    }

    /**
     * 签名生成
     * 1. 取所有参数(排除 partnerId、appId、token)
     * 2. 按参数名 ASCII 升序排列
     * 3. 拼接为: {uri}?{key1}={value1}&{key2}={value2}...
     * 4. 末尾追加 referKey
     * 5. 对拼接结果做 MD5(UTF-8),得到 32 位小写 hex 字符串
     */
    private static String generateToken(Map<String, String> params, String uri, String key) {
        List<String> keys = new ArrayList<>(params.keySet());
        keys.remove("partnerId");
        keys.remove("appId");
        keys.remove("token");
        Collections.sort(keys);

        StringBuilder sb = new StringBuilder(uri);
        for (int i = 0; i < keys.size(); i++) {
            sb.append(i == 0 ? "?" : "&");
            sb.append(keys.get(i)).append("=").append(params.get(keys.get(i)));
        }
        return DigestUtils.md5Hex((sb.toString() + key).getBytes(StandardCharsets.UTF_8));
    }

    private static String buildQueryString(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (sb.length() > 0) sb.append("&");
            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }
        return sb.toString();
    }
}