# 🔗 存客宝接口对接规范 (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, 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 '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 { 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; remark?: string; uniqueId?: string; }; } interface CKBResponse { 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 { // 复制参数,移除特殊字段 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> { const timestamp = Math.floor(Date.now() / 1000); const params: Record = { 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> { 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): 初始版本,支持线索上报和用户画像