Files
shensheshou/开发文档/5、接口/存客宝对接规范.md

804 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔗 存客宝接口对接规范 (CunKeBao API Standard)
> **用途**: 所有项目对接存客宝系统的统一规范
> **版本**: v1.0
> **适用场景**: 线索上报、用户画像、流量池管理
---
## 📋 一、快速对接指南
### 1.1 对接前准备
```yaml
必须获取:
- apiKey: 存客宝分配的接口密钥(每个任务/场景唯一)
接口地址:
- 生产环境: https://ckbapi.quwanzhi.com/v1/api/scenarios
- 测试环境: [按需配置]
请求格式:
- Content-Type: application/json (推荐)
- 备选: application/x-www-form-urlencoded
- 编码: UTF-8
```
### 1.2 一分钟接入
```javascript
// 最简调用示例
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 签名规则详解
```yaml
# 签名规则
不参与签名的字段:
- sign: 签名本身不参与
- apiKey: 不参与拼接只在最后一步参与二次MD5
- portrait: 整个画像对象不参与(避免复杂度)
空值处理:
- null 值字段: 不参与签名
- 空字符串 '': 不参与签名
排序规则:
- 按参数名键名ASCII 升序
- 例如: name, phone, source, timestamp
拼接规则:
- 只取值,不取键
- 顺序直接拼接,无分隔符
- 例如: "张三13800000000微信广告1710000000"
MD5 规则:
- 使用小写 MD5
- 两次 MD5: 先对拼接字符串,再对结果+apiKey
```
### 2.3 签名代码实现
#### TypeScript/JavaScript 实现
```typescript
/**
* 存客宝签名生成器
* @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 实现
```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
<?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 用户画像字段(可选)
```typescript
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 统一响应结构
```typescript
interface CKBResponse<T = any> {
code: number; // 200=成功,其他=失败
message: string; // 提示信息
data: T | null; // 业务数据
}
```
### 4.2 成功响应
```json
// 新增成功
{ "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 基础线索上报
```json
{
"apiKey": "YOUR_API_KEY",
"timestamp": 1710000000,
"phone": "13800000000",
"name": "张三",
"source": "微信广告",
"remark": "通过H5落地页留资",
"tags": "高意向,电商",
"sign": "a1b2c3d4e5f6..."
}
```
### 5.2 带微信号的线索上报
```json
{
"apiKey": "YOUR_API_KEY",
"timestamp": 1710000000,
"wechatId": "wxid_abcdefg123",
"phone": "13800000001",
"name": "李四",
"source": "小程序落地页",
"tags": "中意向,直播",
"sign": "a1b2c3d4e5f6..."
}
```
### 5.3 带用户画像的线索上报
```json
{
"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 完整封装
```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 完整封装
```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**: 按以下步骤排查:
1. 确认 apiKey 正确
2. 确认 timestamp 在 5 分钟内
3. 确认移除了 sign、apiKey、portrait 字段
4. 确认移除了空值字段
5. 确认按键名排序
6. 确认 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): 初始版本,支持线索上报和用户画像