20 KiB
20 KiB
🔗 存客宝接口对接规范 (CunKeBao API Standard)
用途: 所有项目对接存客宝系统的统一规范 版本: v1.0 适用场景: 线索上报、用户画像、流量池管理
📋 一、快速对接指南
1.1 对接前准备
必须获取:
- apiKey: 存客宝分配的接口密钥(每个任务/场景唯一)
接口地址:
- 生产环境: https://ckbapi.quwanzhi.com/v1/api/scenarios
- 测试环境: [按需配置]
请求格式:
- Content-Type: application/json (推荐)
- 备选: application/x-www-form-urlencoded
- 编码: UTF-8
1.2 一分钟接入
// 最简调用示例
const response = await fetch('https://ckbapi.quwanzhi.com/v1/api/scenarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiKey: 'YOUR_API_KEY',
timestamp: Math.floor(Date.now() / 1000),
phone: '13800000000',
sign: generateSign(params) // 见签名算法
})
});
🔐 二、签名算法(核心)
2.1 签名生成流程图
┌─────────────────────────────────────────────────────────────────────┐
│ 签名生成流程 │
├─────────────────────────────────────────────────────────────────────┤
│ Step 1: 准备参数 │
│ └── 收集所有请求参数(含 apiKey, timestamp, 业务参数) │
│ │
│ Step 2: 移除特殊字段 │
│ └── 移除: sign, apiKey, portrait │
│ │
│ Step 3: 移除空值 │
│ └── 移除: null, ''(空字符串) │
│ │
│ Step 4: 按键名排序 │
│ └── ASCII 升序排序(a→z) │
│ │
│ Step 5: 拼接参数值 │
│ └── 只取值,顺序拼接,无分隔符 │
│ │
│ Step 6: 第一次 MD5 │
│ └── firstMd5 = MD5(拼接字符串) │
│ │
│ Step 7: 第二次 MD5 │
│ └── sign = MD5(firstMd5 + apiKey) │
└─────────────────────────────────────────────────────────────────────┘
2.2 签名规则详解
# 签名规则
不参与签名的字段:
- sign: 签名本身不参与
- apiKey: 不参与拼接,只在最后一步参与二次MD5
- portrait: 整个画像对象不参与(避免复杂度)
空值处理:
- null 值字段: 不参与签名
- 空字符串 '': 不参与签名
排序规则:
- 按参数名(键名)ASCII 升序
- 例如: name, phone, source, timestamp
拼接规则:
- 只取值,不取键
- 顺序直接拼接,无分隔符
- 例如: "张三13800000000微信广告1710000000"
MD5 规则:
- 使用小写 MD5
- 两次 MD5: 先对拼接字符串,再对结果+apiKey
2.3 签名代码实现
TypeScript/JavaScript 实现
/**
* 存客宝签名生成器
* @description 生成符合存客宝接口规范的签名
* @param params 请求参数(不含sign)
* @param apiKey 接口密钥
* @returns 签名字符串(小写MD5)
*/
function generateCKBSign(params: Record<string, any>, apiKey: string): string {
// Step 1: 复制参数,移除特殊字段
const signParams = { ...params };
delete signParams.sign;
delete signParams.apiKey;
delete signParams.portrait;
// Step 2: 移除空值
Object.keys(signParams).forEach(key => {
if (signParams[key] === null || signParams[key] === '') {
delete signParams[key];
}
});
// Step 3: 按键名排序
const sortedKeys = Object.keys(signParams).sort();
// Step 4: 拼接参数值
const stringToSign = sortedKeys.map(key => signParams[key]).join('');
// Step 5: 第一次 MD5
const firstMd5 = md5(stringToSign);
// Step 6: 第二次 MD5(拼接 apiKey)
const sign = md5(firstMd5 + apiKey);
return sign;
}
// 使用示例
const params = {
apiKey: 'YOUR_API_KEY',
timestamp: Math.floor(Date.now() / 1000),
phone: '13800000000',
name: '张三',
source: '微信广告'
};
const sign = generateCKBSign(params, params.apiKey);
params.sign = sign;
Python 实现
"""
存客宝签名生成器
"""
import hashlib
from typing import Dict, Any
def generate_ckb_sign(params: Dict[str, Any], api_key: str) -> str:
"""
生成存客宝接口签名
Args:
params: 请求参数(不含sign)
api_key: 接口密钥
Returns:
签名字符串(小写MD5)
"""
# Step 1: 复制参数,移除特殊字段
sign_params = {k: v for k, v in params.items()
if k not in ['sign', 'apiKey', 'portrait']}
# Step 2: 移除空值
sign_params = {k: v for k, v in sign_params.items()
if v is not None and v != ''}
# Step 3: 按键名排序
sorted_keys = sorted(sign_params.keys())
# Step 4: 拼接参数值
string_to_sign = ''.join(str(sign_params[k]) for k in sorted_keys)
# Step 5: 第一次 MD5
first_md5 = hashlib.md5(string_to_sign.encode('utf-8')).hexdigest()
# Step 6: 第二次 MD5
sign = hashlib.md5((first_md5 + api_key).encode('utf-8')).hexdigest()
return sign
# 使用示例
import time
params = {
'apiKey': 'YOUR_API_KEY',
'timestamp': int(time.time()),
'phone': '13800000000',
'name': '张三',
'source': '微信广告'
}
sign = generate_ckb_sign(params, params['apiKey'])
params['sign'] = sign
PHP 实现
<?php
/**
* 存客宝签名生成器
*
* @param array $params 请求参数(不含sign)
* @param string $apiKey 接口密钥
* @return string 签名字符串(小写MD5)
*/
function generateCKBSign(array $params, string $apiKey): string {
// Step 1: 移除特殊字段
unset($params['sign'], $params['apiKey'], $params['portrait']);
// Step 2: 移除空值
$params = array_filter($params, function($value) {
return !is_null($value) && $value !== '';
});
// Step 3: 按键名排序
ksort($params);
// Step 4: 拼接参数值
$stringToSign = implode('', array_values($params));
// Step 5: 第一次 MD5
$firstMd5 = md5($stringToSign);
// Step 6: 第二次 MD5
$sign = md5($firstMd5 . $apiKey);
return $sign;
}
// 使用示例
$params = [
'apiKey' => 'YOUR_API_KEY',
'timestamp' => time(),
'phone' => '13800000000',
'name' => '张三',
'source' => '微信广告'
];
$sign = generateCKBSign($params, $params['apiKey']);
$params['sign'] = $sign;
📤 三、请求参数规范
3.1 鉴权字段(必填)
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
apiKey |
string | ✅ | 存客宝分配的接口密钥 |
sign |
string | ✅ | 签名值(见签名算法) |
timestamp |
int | ✅ | 秒级时间戳,与服务器时间差 ≤ 5分钟 |
3.2 主标识字段(至少传一个)
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
wechatId |
string | 二选一 | 微信号,优先作为主标识 |
phone |
string | 二选一 | 手机号,wechatId 为空时用作主标识 |
3.3 基础信息字段(可选)
| 字段名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
name |
string | ❌ | 客户姓名 | "张三" |
source |
string | ❌ | 线索来源 | "抖音直播间" |
remark |
string | ❌ | 备注信息 | "通过H5落地页留资" |
tags |
string | ❌ | 微信标签(逗号分隔) | "高意向,电商,女装" |
siteTags |
string | ❌ | 站内标签(逗号分隔) | "新客,VIP" |
3.4 用户画像字段(可选)
interface Portrait {
/**
* 画像类型
* 0-浏览 1-点击 2-下单/购买 3-注册 4-互动
*/
type?: 0 | 1 | 2 | 3 | 4;
/**
* 画像来源
* 0-本站 1-老油条 2-老坑爹
*/
source?: 0 | 1 | 2;
/**
* 画像明细数据(任意键值对)
*/
sourceData?: {
age?: number;
gender?: string;
city?: string;
productId?: string;
pageUrl?: string;
[key: string]: any;
};
/**
* 画像备注,最大100字符
*/
remark?: string;
/**
* 去重唯一ID
* 相同 uniqueId 在半小时内会合并统计
* 建议格式: {来源}_{用户标识}_{时间戳}_{序号}
*/
uniqueId?: string;
}
画像类型说明
| 值 | 类型 | 说明 | 适用场景 |
|---|---|---|---|
| 0 | 浏览 | 用户浏览了页面或内容 | 页面访问、商品浏览 |
| 1 | 点击 | 用户点击了某个元素 | 按钮点击、广告点击 |
| 2 | 下单/购买 | 用户完成了购买行为 | 订单提交、支付完成 |
| 3 | 注册 | 用户完成了注册 | 账号注册、会员注册 |
| 4 | 互动 | 用户进行了互动行为 | 点赞、评论、分享 |
📥 四、响应格式规范
4.1 统一响应结构
interface CKBResponse<T = any> {
code: number; // 200=成功,其他=失败
message: string; // 提示信息
data: T | null; // 业务数据
}
4.2 成功响应
// 新增成功
{ "code": 200, "message": "新增成功", "data": "13800000000" }
// 已存在
{ "code": 200, "message": "已存在", "data": "13800000000" }
4.3 错误响应
| code | message | 说明 | 处理建议 |
|---|---|---|---|
| 400 | apiKey不能为空 | 缺少 apiKey | 检查参数 |
| 400 | sign不能为空 | 缺少签名 | 检查签名生成 |
| 400 | timestamp不能为空 | 缺少时间戳 | 添加时间戳 |
| 400 | 请求已过期 | 时间戳超过5分钟 | 同步服务器时间 |
| 401 | 无效的apiKey | apiKey 错误 | 检查 apiKey |
| 401 | 签名验证失败 | 签名错误 | 检查签名算法 |
| 500 | 系统错误 | 服务端异常 | 联系技术支持 |
📝 五、完整请求示例
5.1 基础线索上报
{
"apiKey": "YOUR_API_KEY",
"timestamp": 1710000000,
"phone": "13800000000",
"name": "张三",
"source": "微信广告",
"remark": "通过H5落地页留资",
"tags": "高意向,电商",
"sign": "a1b2c3d4e5f6..."
}
5.2 带微信号的线索上报
{
"apiKey": "YOUR_API_KEY",
"timestamp": 1710000000,
"wechatId": "wxid_abcdefg123",
"phone": "13800000001",
"name": "李四",
"source": "小程序落地页",
"tags": "中意向,直播",
"sign": "a1b2c3d4e5f6..."
}
5.3 带用户画像的线索上报
{
"apiKey": "YOUR_API_KEY",
"timestamp": 1710000000,
"phone": "13800000002",
"name": "王五",
"source": "百度推广",
"portrait": {
"type": 1,
"source": 0,
"sourceData": {
"age": 28,
"gender": "female",
"city": "上海",
"productId": "P12345",
"pageUrl": "https://example.com/product/123"
},
"remark": "点击了立即咨询按钮",
"uniqueId": "site_13800000002_1710000000_001"
},
"sign": "a1b2c3d4e5f6..."
}
🛠️ 六、封装工具类
6.1 TypeScript 完整封装
/**
* 存客宝 API 客户端
* @description 封装存客宝接口调用,自动处理签名
*/
import crypto from 'crypto';
interface CKBConfig {
apiKey: string;
baseUrl?: string;
}
interface LeadData {
phone?: string;
wechatId?: string;
name?: string;
source?: string;
remark?: string;
tags?: string;
siteTags?: string;
portrait?: {
type?: 0 | 1 | 2 | 3 | 4;
source?: 0 | 1 | 2;
sourceData?: Record<string, any>;
remark?: string;
uniqueId?: string;
};
}
interface CKBResponse<T = any> {
code: number;
message: string;
data: T;
}
export class CunKeBaoClient {
private apiKey: string;
private baseUrl: string;
constructor(config: CKBConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://ckbapi.quwanzhi.com';
}
/**
* 生成 MD5
*/
private md5(str: string): string {
return crypto.createHash('md5').update(str, 'utf8').digest('hex');
}
/**
* 生成签名
*/
private generateSign(params: Record<string, any>): string {
// 复制参数,移除特殊字段
const signParams = { ...params };
delete signParams.sign;
delete signParams.apiKey;
delete signParams.portrait;
// 移除空值
Object.keys(signParams).forEach(key => {
if (signParams[key] === null || signParams[key] === '') {
delete signParams[key];
}
});
// 按键名排序
const sortedKeys = Object.keys(signParams).sort();
// 拼接参数值
const stringToSign = sortedKeys.map(key => signParams[key]).join('');
// 两次 MD5
const firstMd5 = this.md5(stringToSign);
return this.md5(firstMd5 + this.apiKey);
}
/**
* 上报线索
* @param data 线索数据
*/
async reportLead(data: LeadData): Promise<CKBResponse<string>> {
const timestamp = Math.floor(Date.now() / 1000);
const params: Record<string, any> = {
apiKey: this.apiKey,
timestamp,
...data,
};
// 生成签名
params.sign = this.generateSign(params);
// 发送请求
const response = await fetch(`${this.baseUrl}/v1/api/scenarios`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
return response.json();
}
/**
* 上报用户画像
* @param identifier 用户标识(phone 或 wechatId)
* @param portrait 画像数据
*/
async reportPortrait(
identifier: { phone?: string; wechatId?: string },
portrait: LeadData['portrait']
): Promise<CKBResponse<string>> {
return this.reportLead({
...identifier,
portrait,
});
}
}
// ============ 使用示例 ============
// 初始化客户端
const ckb = new CunKeBaoClient({
apiKey: 'YOUR_API_KEY',
});
// 上报线索
await ckb.reportLead({
phone: '13800000000',
name: '张三',
source: '微信广告',
tags: '高意向,电商',
});
// 上报带画像的线索
await ckb.reportLead({
phone: '13800000001',
name: '李四',
source: '抖音直播',
portrait: {
type: 1, // 点击
sourceData: {
productId: 'P12345',
pageUrl: 'https://example.com/product',
},
uniqueId: 'site_13800000001_' + Date.now(),
},
});
6.2 Python 完整封装
"""
存客宝 API 客户端
封装存客宝接口调用,自动处理签名
"""
import hashlib
import time
import requests
from typing import Optional, Dict, Any
from dataclasses import dataclass, asdict
@dataclass
class Portrait:
"""用户画像"""
type: int = 0 # 0-浏览 1-点击 2-下单 3-注册 4-互动
source: int = 0 # 0-本站 1-老油条 2-老坑爹
sourceData: Optional[Dict[str, Any]] = None
remark: Optional[str] = None
uniqueId: Optional[str] = None
class CunKeBaoClient:
"""存客宝 API 客户端"""
def __init__(self, api_key: str, base_url: str = "https://ckbapi.quwanzhi.com"):
self.api_key = api_key
self.base_url = base_url
def _md5(self, s: str) -> str:
"""生成 MD5"""
return hashlib.md5(s.encode('utf-8')).hexdigest()
def _generate_sign(self, params: Dict[str, Any]) -> str:
"""生成签名"""
# 复制参数,移除特殊字段
sign_params = {k: v for k, v in params.items()
if k not in ['sign', 'apiKey', 'portrait']}
# 移除空值
sign_params = {k: v for k, v in sign_params.items()
if v is not None and v != ''}
# 按键名排序
sorted_keys = sorted(sign_params.keys())
# 拼接参数值
string_to_sign = ''.join(str(sign_params[k]) for k in sorted_keys)
# 两次 MD5
first_md5 = self._md5(string_to_sign)
return self._md5(first_md5 + self.api_key)
def report_lead(
self,
phone: Optional[str] = None,
wechat_id: Optional[str] = None,
name: Optional[str] = None,
source: Optional[str] = None,
remark: Optional[str] = None,
tags: Optional[str] = None,
site_tags: Optional[str] = None,
portrait: Optional[Portrait] = None
) -> Dict[str, Any]:
"""
上报线索
Args:
phone: 手机号
wechat_id: 微信号
name: 客户姓名
source: 线索来源
remark: 备注
tags: 微信标签(逗号分隔)
site_tags: 站内标签(逗号分隔)
portrait: 用户画像
Returns:
API 响应
"""
params = {
'apiKey': self.api_key,
'timestamp': int(time.time()),
}
# 添加可选参数
if phone:
params['phone'] = phone
if wechat_id:
params['wechatId'] = wechat_id
if name:
params['name'] = name
if source:
params['source'] = source
if remark:
params['remark'] = remark
if tags:
params['tags'] = tags
if site_tags:
params['siteTags'] = site_tags
if portrait:
params['portrait'] = asdict(portrait)
# 生成签名
params['sign'] = self._generate_sign(params)
# 发送请求
response = requests.post(
f"{self.base_url}/v1/api/scenarios",
json=params,
headers={'Content-Type': 'application/json'}
)
return response.json()
# ============ 使用示例 ============
# 初始化客户端
ckb = CunKeBaoClient(api_key='YOUR_API_KEY')
# 上报线索
result = ckb.report_lead(
phone='13800000000',
name='张三',
source='微信广告',
tags='高意向,电商'
)
# 上报带画像的线索
result = ckb.report_lead(
phone='13800000001',
name='李四',
source='抖音直播',
portrait=Portrait(
type=1, # 点击
sourceData={
'productId': 'P12345',
'pageUrl': 'https://example.com/product'
},
uniqueId=f'site_13800000001_{int(time.time())}'
)
)
❓ 七、常见问题 (FAQ)
Q1: 签名验证失败怎么排查?
A: 按以下步骤排查:
- 确认 apiKey 正确
- 确认 timestamp 在 5 分钟内
- 确认移除了 sign、apiKey、portrait 字段
- 确认移除了空值字段
- 确认按键名排序
- 确认 MD5 是小写
Q2: portrait 字段是否必传?
A: 不是必传。只有需要记录用户画像时才传递。
Q3: uniqueId 的作用是什么?
A: 防止重复记录。相同 uniqueId 的画像数据在半小时内会合并统计。
Q4: phone 和 wechatId 必须传哪个?
A: 至少传一个。wechatId 优先作为主标识。
Q5: 时间戳超时怎么处理?
A: 确保服务器时间准确,或使用 NTP 同步时间。
🔗 八、与开发模板联动
8.1 在项目中使用
1. 复制本文件中的工具类到项目 lib/ckb.ts
2. 配置 apiKey 到环境变量
3. 在需要的地方调用 ckb.reportLead()
8.2 联动指令
# 生成存客宝对接代码
@联动 存客宝→后端:生成线索上报 Service
# 生成前端表单
@联动 存客宝→前端:生成留资表单组件
📞 九、技术支持
- 接口问题: 联系存客宝技术支持
- apiKey 申请: 联系卡若(微信 28533368)
更新日志:
- v1.0 (2026-01-18): 初始版本,支持线索上报和用户画像