活动商品发放接口文档
接口概述
合作方通过本接口向用户发放活动商品,支持免费 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 | 下单频繁,请稍后重试 |
签名说明
签名生成规则
- 将所有请求参数(除
partnerId、appId、token外)按参数名 ASCII 码升序排列 - 将排序后的参数拼接为 URL 格式:
{请求路径}?{key1}={value1}&{key2}={value2}... - 在拼接结果末尾追加合作方密钥(referKey)
- 对拼接字符串进行 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 加密:
- 密钥:合作方 referKey 的前 16 个字符(UTF-8 编码为 16 字节,即 128 位 AES 密钥)
- 算法:
AES/ECB/PKCS5Padding - 输入编码:手机号明文以 UTF-8 编码为字节数组
- 输出编码:加密后的字节数组进行小写十六进制(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) // 小写十六进制
业务规则
- 幂等性:同一
appId+outOrderNo组合重复请求,VIP 类型会返回成功(不重复发放),体验卡类型通过服务端唯一 ID 保障幂等 - 请求频率:同一用户的领取请求有频率限制(10 秒内同一 account 不可重复请求)
- 时效性:请求时间戳
ts与服务端时间差不能超过 60 秒 - 领取限制:根据后台配置的限制规则(月限、时间段限、每日金额上限等),超出限制将返回错误码 27
- 用户限制:部分商品配置为仅限新用户领取,已有账号的用户将返回错误码 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();
}
}