804 lines
20 KiB
Markdown
804 lines
20 KiB
Markdown
# 🔗 存客宝接口对接规范 (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): 初始版本,支持线索上报和用户画像
|