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

20 KiB
Raw Blame History

🔗 存客宝接口对接规范 (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: 按以下步骤排查:

  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): 初始版本,支持线索上报和用户画像