前置说明
本文档仅供开通CRM的企业,将外部数据传入CRM而使用。要使用此功能,需要联系客户经理开通配置相应的密钥。
接入说明
接口接入域名:https://api.xiaolanben.com
接口功能开通后获得签名密钥,每次调用接口时需要传入签名信息,签名参数如下:
参数 | 参数名 | 传输方式 | 备注 |
h_t | 签名生成的时间戳 | query | 如:当前时间为2024-01-01 12:30:35.345时,应精确毫秒 则该值为1704083435345 注意:该值必须为调用接口时生成,接口服务器端限制10秒内有效 |
h_sign | 请求签名结果 | query | 数据签名结果字符串; 注意:同一签名数据10秒内不允许重复调用,防止数据重放 |
签名方式
在接口调用前需要对调用的数据生成签名,并将签名结果传入接口。签名示例代码见文档最后的代码示例
生成待签名字符串
1、请求URL中的query部份数据按key的顺序进行key1=value1&key2=value2的方式进行组装(h_t/h_sign不参与组装),得到待签名字符串A,注意当value为非URLEncoder的原文内容,空字符串时,不需要参与组装
2、如果当前请求为POST/PUT时,获取请求body块中的字符串原文为待签名字符串B
3、将 待签名字符串A、待签名字符串B、h_t、签名密钥 直接连接,生成待签名完整字符串
签名
将待签名完整字符串,直接md5获取到32为的签名结果串,并将该结果传入至h_sign中
错误响应
本文档所有接口如果出错时,系统将直接响应非2XX的http响应码,具体错误原因可在响应body中获取,如:
{
"trackingId": "12b505a5-ff57-43ac-9571-5a1993a49c9e",
"errors": [
{
"code": 403,
"message": "请求不允许重复执行"
}
]
}
http status | code | message |
403 | 403 | 签名参数错误等原因,如: 请求未包含签名信息 请求不允许重复执行 |
401 | 401 | 签名错误或未开通接口,如: 请求未经过认证 |
400 | 400 | 请求参数错误,如: 有必传参数未传,或参数类型错误等 |
500 | 500 | 服务端异常错误 |
接口
单客户及联系人导入接口
接口说明
该接口仅用于每次导入一个客户及其联系人信息
接口路径:/blue-crm/api/v1/open/customer/import/customerWithContact
调用方式:POST application/json; charset=utf-8 body块数据使用json进行组装
请求参数
参数名 | 类型 | 位置 | 备注 |
companyId | Long | header | 必传 当前企业的标识ID,开通时由客户经理提供 不参与签名 |
checkKeepLimit | Bool | query | 可空 当传入true,如果请求体中指定了staffId时,用于判断是否校验该员工的客户保有量。 默认为true |
ownerConflictUpdate | Bool | query | 可空 当传入true,如果请求体中指定了staffId时,请求的客户在库中归属于其它人时,是否强制更新客户信息(不会更新归属人信息) 默认为false |
autoUnlock | Bool | query | 可空 当传入true,如果添加的客户匹配到企业的时候,是否自动解锁该企业。 默认为false |
autoDraw | Bool | query | 可空 当传入true,并且如果指定了staffId,及库中已有客户在公海中,则自动领取该客户 默认false |
duplicateControl | String | query | 可空 客户入库时查重检测控制 SYSTEM :系统配置查重规则,如果命中则直接响应失败 CUSTOMER_PHONE_DUPLICATE_FAIL :客户归属与手机号归属冲突时直接响应失败 CUSTOMER_PHONE_DUPLICATE_IGNORE :客户归属与手机号归属冲突时则忽略该手机号联系人 默认为CRM系统查重控制逻辑 |
entName | String | body | 可空 企业名称 长度介于0~200 用于标识该客户对应企业的唯一性,如未传入则使用customerName 如客户之前不存在,则会新建客户 如指定了staffId,则会归属到该员工下的私海 如指定了publicSeaId 同时指定了staffId,则该客户在掉保时会掉入该公海 否则该客户新建时,直接进入至该公海 如客户之前已存在,则 该客户是公海客户或指定的staffId客户,则会更新该客户信息 否则直接报错 |
customerName | String | body | 必传 客户名称 长度介于1~200 如同时指定了entName,并该entName记录已经存在,则会更新该客户的客户名称字段 |
staffId | Long | body | 可空 客户归属人ID 员工标识参考其它接口(待提供) 如指定该值,则 如客户之前不存在,则新建客户并分配给该员工 如客户之前已存在,则该值自动无效 |
customerSource | String | body | 可空 客户来源 来源列表参考其它接口(待提供) 如之前客户已存在,则该值自动无效 |
publicSeaId | String | body | 可空 客户公海标识 如客户已经存在,则该值自动无效 |
tag | String | body | 可空 客户分组 如之前未创建该分组,则系统会自动创建 |
customizeField.xxxx | … | body | 可空 自定义字段设置,见 字段标识 列 如果有多个自定义字段的,输入多个字段 |
tagIds | List<String> | body | 可空 客户自定义标签 自定义标签参见其它接口(待提供) 如果提供了该值,则会自动保存或更新客户的自定义标签 |
contacts | List | body | 可空 客户联系人列表 如果是是更新客户及联系人的,则会更新原phone的记录 |
–thirdDataId | String | 必传 本次传输时联系人数据唯一标识 用于响应体生成时返回联系人的ID | |
–name | String | 可空 联系人姓名 长度<=30 | |
–position | String | 可空 联系人职位 长度<=30 | |
–dept | String | 可空 联系人部门 长度<=30 | |
–phone | String | 可空 联系人手机号 长度<=30 如果新建客户时,系统会按phone查询客户归属人,如果有则会将客户归属至该员工 如果更新客户添加联系人时,会查询该phone归属人,如果有冲突则会直接响应失败 | |
–tel | String | 可空 联系人电话 长度<=30 | |
String | 可空 联系人邮箱 长度<=100 | ||
–addr | String | 可空 联系人联系地址 长度<=200 | |
–logo | String | 可空 联系人头像地址 长度<=400 | |
String | 可空 联系人微信号 长度<=30 | ||
String | 可空 联系人QQ 长度<=30 | ||
–gender | String | 可空 联系人性别 男/女/未知;长度<=2 | |
–birthday | String | 可空 联系人生日 建议为yyyy-MM-dd格式;长度<=10 | |
—customizeField.xxxx | … | 可空 联系人自定义字段设置,见 字段标识 列 如果有多个自定义字段的,输入多个字段 | |
–remark | String | 可空 联系人备注 |
响应参数
参数名 | 类型 | 备注 |
customerId | Long | 客户ID |
staffId | Long | 客户归属人ID |
publicSeaId | String | 公海标识 |
isNew | Bool | 是新建/更新客户 |
contactIdMap | Map<String,Long> | 客户联系人唯一标识与联系人ID对 如果是忽略的联系人,则不会返回 |
多客户及联系人导入
接口说明
用于一次性导入多个客户及联系人;出于对接口响应速度的考虑,建议每次客户数不要超过10条,每客户联系人不超过10个
接口路径:/blue-crm/api/v1/open/customer/import/customersWithContact
调用方式:POST application/json; charset=utf-8 body块数据使用json进行组装
请求入参
参数名 | 类型 | 位置 | 备注 |
companyId | Long | header | 必传 当前企业的标识ID,开通时由客户经理提供 不参与签名 |
checkKeepLimit | Bool | query | 可空 当传入true,如果请求体中指定了staffId时,用于判断是否校验该员工的客户保有量。 默认为true |
ownerConflictUpdate | Bool | query | 可空 当传入true,如果请求体中指定了staffId时,请求的客户在库中归属于其它人时,是否强制更新客户信息(不会更新归属人信息) 默认为false |
autoUnlock | Bool | query | 可空 当传入true,如果添加的客户匹配到企业的时候,是否自动解锁该企业。 默认为false |
autoDraw | Bool | query | 可空 当传入true,并且如果指定了staffId,及库中已有客户在公海中,则自动领取该客户 默认false |
duplicateControl | String | query | 可空 客户入库时查重检测控制 SYSTEM :系统配置查重规则,如果命中则直接响应失败 CUSTOMER_PHONE_DUPLICATE_FAIL :客户归属与手机号归属冲突时直接响应失败 CUSTOMER_PHONE_DUPLICATE_IGNORE :客户归属与手机号归属冲突时则忽略该手机号联系人 默认为CRM系统查重控制逻辑 |
entName | String | body | 可空 企业名称 长度介于0~200 用于标识该客户对应企业的唯一性,如未传入则使用customerName 如客户之前不存在,则会新建客户 如指定了staffId,则会归属到该员工下的私海 如指定了publicSeaId 同时指定了staffId,则该客户在掉保时会掉入该公海 否则该客户新建时,直接进入至该公海 如客户之前已存在,则 该客户是公海客户或指定的staffId客户,则会更新该客户信息 否则直接报错 |
List | body | 多个客户的数据列表对象,每个客户信息参照下面的请求体 | |
customerName | String | body | 必传 客户名称 长度介于1~200 如同时指定了entName,并该entName记录已经存在,则会更新该客户的客户名称字段 |
staffId | Long | body | 可空 客户归属人ID 员工标识参考其它接口(待提供) 如指定该值,则 如客户之前不存在,则新建客户并分配给该员工 如客户之前已存在,则该值自动无效 |
customerSource | String | body | 可空 客户来源 来源列表参考其它接口(待提供) 如之前客户已存在,则该值自动无效 |
publicSeaId | String | body | 可空 客户公海标识 如客户已经存在,则该值自动无效 |
tag | String | body | 可空 客户分组 如之前未创建该分组,则系统会自动创建 |
customizeField.xxxx | … | body | 可空 自定义字段设置,见 字段标识 列 如果有多个自定义字段的,输入多个字段 |
tagIds | List<String> | body | 可空 客户自定义标签 自定义标签参见其它接口(待提供) 如果提供了该值,则会自动保存或更新客户的自定义标签 |
contacts | List | body | 可空 客户联系人列表 如果是是更新客户及联系人的,则会更新原phone的记录 |
–thirdDataId | String | 必传 本次传输时联系人数据唯一标识 用于响应体生成时返回联系人的ID | |
–name | String | 可空 联系人姓名 长度<=30 | |
–position | String | 可空 联系人职位 长度<=30 | |
–dept | String | 可空 联系人部门 长度<=30 | |
–phone | String | 可空 联系人手机号 长度<=30 如果新建客户时,系统会按phone查询客户归属人,如果有则会将客户归属至该员工 如果更新客户添加联系人时,会查询该phone归属人,如果有冲突则会直接响应失败 | |
–tel | String | 可空 联系人电话 长度<=30 | |
String | 可空 联系人邮箱 长度<=100 | ||
–addr | String | 可空 联系人联系地址 长度<=200 | |
–logo | String | 可空 联系人头像地址 长度<=400 | |
String | 可空 联系人微信号 长度<=30 | ||
String | 可空 联系人QQ 长度<=30 | ||
–gender | String | 可空 联系人性别 男/女/未知;长度<=2 | |
–birthday | String | 可空 联系人生日 建议为yyyy-MM-dd格式;长度<=10 | |
—customizeField.xxxx | … | 可空 联系人自定义字段设置,见 字段标识 列 如果有多个自定义字段的,输入多个字段 | |
–remark | String | 可空 联系人备注 |
响应参数
参数名 | 类型 | 备注 |
Map | 响应客户名与入库结果响应对 {“客户名”:{“success”:true,”customerId”:xxx…}} | |
success | Bool | 导入是否成功,如果false则原因见failedReason |
customerId | Long | 客户ID |
staffId | Long | 客户归属人ID |
publicSeaId | String | 公海标识 |
isNew | Bool | 是新建/更新客户 |
contactIdMap | Map<String,Long> | 客户联系人唯一标识与联系人ID对 如果是忽略的联系人,则不会返回 |
failedReason | String | 失败原因 |
分页获取员工信息
该接口仅用于获取部门员工列表
接口路径:/blue-crm/api/v1/open/companycenter/staffs
调用方式:GET
请求入参
参数名 | 类型 | 位置 | 备注 |
companyId | Long | header | 必传 当前企业的标识ID,开通时由客户经理提供 不参与签名 |
pageIndex | Int | query | 必传 请求页码,从1开始 |
pageSize | Int | query | 必传 请求每页大小,介于2~100之间 |
search | String | query | 非必传 按姓名搜索(包含)/按手机号搜索完全一至 |
响应参数
参数名 | 类型 | 备注 |
List | 响应为数据列表 如请求是10,但响应体小于10,则表示没有下一页数据 | |
–staffId | Long | 员工ID |
–realName | String | 员工姓名 |
–mobile | String | 员工手机号 |
–department | String | 所属部门 |
–roleId | Long | 角色标识 |
–role | String | 角色名称 |
示例代码
签名工具类
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang.StringUtils;
/**
* 签名验签工具
*/
@Slf4j
public class SignUtil {
public static final String KEY_SIGN = "h_sign";
public static final String KEY_T = "h_t";
private static final List<String> IGNORE_KEYS = Lists.newArrayList(KEY_SIGN, KEY_T);
/**
* 生成签名
*
* @param key 签名的密钥
* @param paramMap 请求的URL参数
* @param body 请求的body,如果是get,则传null
* @param time 请求发出的时间
* @return 返回签名
*/
public static String sign(String key, Map<String, Object> paramMap, String body, long time) {
String params = getParamStr(paramMap);
StringBuilder strb = new StringBuilder();
if (StringUtils.isNotBlank(params)) {
strb.append(params);
}
if (StringUtils.isNotBlank(body)) {
strb.append(body);
}
strb.append(time);
strb.append(key);
String str = strb.toString();
log.info("待生成签名的字符串{}", str);
return DigestUtils.md5Hex(str);
}
/**
* 生成请求的参数待签名串
*/
private static String getParamStr(Map<String, Object> paramMap) {
if (MapUtils.isEmpty(paramMap)) {
return StringUtils.EMPTY;
}
TreeMap<String, Object> treeMap = Maps.newTreeMap();
treeMap.putAll(paramMap);
List<String> params = Lists.newArrayList();
for (Entry<String, Object> entry : treeMap.entrySet()) {
String key = entry.getKey();
Object objVal = entry.getValue();
if (objVal == null) {
continue;
}
List<String> values = Lists.newArrayList();
if (objVal instanceof String) {
values.add((String) objVal);
} else if (objVal.getClass().isArray()) {
values.addAll(Arrays.asList((String[]) objVal));
} else {
values.add(objVal.toString());
}
if (IGNORE_KEYS.contains(key) || CollectionUtils.isEmpty(values)) {
continue;
}
String value = values.stream().filter(StringUtils::isNotEmpty).sorted()
.collect(Collectors.joining(","));
params.add(key + "=" + value);
}
return String.join("&", params);
}
}
导入客户接口示例
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
public class CustomerApiTest {
private final ObjectMapper objectMapper = new ObjectMapper();
// @Test
public void importCustomerWithContact() throws IOException {
Map<String, Object> req = this.getRequestData();
//导入客户的接口地址
final String baseUrl = "https://api.xiaolanben.com/blue-crm/api/v1/open/customer/import/customerWithContact";
final String companyId = "xxx"; //开通后由小蓝本提供的公司id
final String signKey = "xxxxxx-xxxxxx-xxxxxx"; //开通后由小蓝本提供的签名key
//导入客户时的请求参数
Map<String, Object> params = new HashMap<>();
params.put("checkKeepLimit", "true");
params.put("autoUnlock", "false");
params.put("duplicateControl", "CUSTOMER_PHONE_DUPLICATE_FAIL");
String body = objectMapper.writeValueAsString(req); //将对象转为json字符串 请求的body
long timestamp = System.currentTimeMillis(); //接口请求时生成的时间戳
//签名
String sign = SignUtil.sign(signKey, params, body, timestamp);
//加签名参数加入到参数中
params.put("h_t", timestamp);
params.put("h_sign", sign);
//组装完成的参数
String queryString = params.entrySet().stream()
.filter(it -> Objects.nonNull(it.getValue()) && StringUtils.isNoneEmpty(it.getValue().toString()))
.map(it -> {
try {
return URLEncoder.encode(it.getKey(), "UTF-8") + "=" + URLEncoder.encode(it.getValue().toString(),
"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}).collect(Collectors.joining("&"));
String fullUrl = baseUrl + "?" + queryString;
System.out.println(fullUrl);
URL url = new URL(fullUrl);
HttpsURLConnection httpsURLConnection = null;
try {
httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setRequestMethod("POST");
httpsURLConnection.setDoOutput(true);
httpsURLConnection.setRequestProperty("companyId", companyId);
httpsURLConnection.setRequestProperty("Content-Type", "application/json");
try (OutputStream outputStream = httpsURLConnection.getOutputStream()) {
outputStream.write(body.getBytes());
}
int statusCode = httpsURLConnection.getResponseCode();
System.out.println(statusCode);
System.out.println(httpsURLConnection.getResponseMessage());
try (InputStream inputStream =
(statusCode == 200 ? httpsURLConnection.getInputStream() : httpsURLConnection.getErrorStream())) {
String resp = String.join("", IOUtils.readLines(inputStream, StandardCharsets.UTF_8));
System.out.println(resp);
}
} finally {
if (httpsURLConnection != null) {
httpsURLConnection.disconnect();
}
}
}
private Map<String, Object> getRequestData() {
Map<String, Object> contact1 = new HashMap<>();
contact1.put("thirdDataId", "u1");
contact1.put("name", "联系人1");
contact1.put("position", "职位1");
contact1.put("dept", "部门1");
contact1.put("phone", "13909876543");
contact1.put("tel", "");
contact1.put("email", "");
contact1.put("addr", "");
contact1.put("logo", "");
contact1.put("wechat", "");
contact1.put("qq", "");
contact1.put("gender", "男");
contact1.put("birthday", "");
contact1.put("customizeField.text_2", "测试2");
contact1.put("remark", "备注");
Map<String, Object> contact2 = new HashMap<>();
contact2.put("thirdDataId", "u2");
contact2.put("name", "联系人2");
contact2.put("position", "职位2");
contact2.put("dept", "部门2");
contact2.put("phone", "13909876544");
contact2.put("tel", "");
contact2.put("email", "");
contact2.put("addr", "");
contact2.put("logo", "");
contact2.put("wechat", "");
contact2.put("qq", "");
contact2.put("gender", "男");
contact2.put("birthday", "");
contact2.put("customizeField.text_2", "测试2");
contact2.put("remark", "备注");
Map<String, Object> req = new HashMap<>();
req.put("staffId", null);
req.put("entName", "");
req.put("customerName", "鲍哥2024测试1");
req.put("customerSource", "");
req.put("publicSeaId", "");
req.put("tag", "未分组");
req.put("customizeField.text_13", "测试13");
req.put("customizeField.bool_5", true);
req.put("tagIds", new ArrayList<String>());
req.put("contacts", new ArrayList<Map<String, Object>>() {{
add(contact1);
add(contact2);
}});
return req;
}
}