chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
803
开发文档/5、接口/存客宝对接规范.md
Normal file
803
开发文档/5、接口/存客宝对接规范.md
Normal file
@@ -0,0 +1,803 @@
|
||||
# 🔗 存客宝接口对接规范 (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): 初始版本,支持线索上报和用户画像
|
||||
Reference in New Issue
Block a user