阅读对象:
CRM/ERP系统管理员、数据库管理员、系统实施、财务、数据分析等相关人员,希望实现两个系统之间数据同步的程序员。
适用场景:
纷享销客创建完客户以后,希望客户信息直接同步到金蝶系统客户列表里;纷享销客创建完订单,直接同步到金蝶销售订单;金蝶系统做完发货单,纷享销客同时收到发货通知等。
实现原理:
利用纷享销客开放平台Open API接口实现对纷享销客CRM对象(客户、订单、联系人、销售线索、回款……)的操作;利用金蝶云星空(K3cloud)Web API实现对金蝶云星空表单数据的操作;利用两个系统各自的API接口,结合具体的业务需求,即可实现两个系统的数据同步。
我想引用金蝶云Web API概述里的一段话来回答这个问题。
为异构系统访问K/3Cloud系统数据提供通用的接口。
当企业规模逐渐增大时,作为支撑业务运营的IT建设也变得越来越重要,不过往往企业的IT建设过程中会发现某一家软件供应商基本不能完全覆盖企业所有的业务运营流程,这样的结果就是,企业上的IT系统很多很全,从ERP到HR、CRM、PDM、OA等,貌似所有的业务都覆盖到了,但实际上因为这些系统的不集成,而形成了企业很多新的信息孤岛,非常不利于企业的后续的管理和战略发展。K/3Cloud从现今和往后的发展趋势来看,也不可避免会遇到上述问题,毕竟企业经营的多样化,并不是所有的业务都能在K/3Cloud中完成,所以我们必须在产品架构上支持更好的与外部系统进行协同。
纷享销客把自己定义为连接型CRM,从字面意义上就能感受到这个产品互联互通的特性,它首先实现业务互联,连接企业的上下游企业;内部协作互联;以及微信生态的互联,基于Open API实现与其他系统的互联。但在这里要对号称连接型CRM纷享销客稍稍鄙视一下:使用API接口还要收费?还要开通频次调用包?自建应用访问CRM数据还要经过纷享的再次审核?对使用开放平台设置了太多门槛,并且这些都没有写在文档里,通过与客服和销售人员多次沟通才了解到的,这感觉与纷享的连接精神不符!
如何使用纷享销客开放平台?
如果熟悉微信公众平台开发的朋友,会发现纷享开放平台与微信公众平台接口非常类似,参数名几乎都一致,甚至大小写都一样,当然还有几个参数不同,比如:纷享销客API接口的每次调用都需要携带一个currentOpenUserId(当前操作人OpenUserID)的参数;CorpAccessToken有效期同为7200秒,微信平台有效期内再次访问,会生产新的AccessToken,纷享平台会返回相同的CorpAccessToken;纷享平台获取CorpAccessToken会同时获得一个corpId,这也是请求接口的必须参数之一,注意不是appId。要使用纷享销客开放平台的步骤是:
管理员登录纷享销客网页端后进入“应用”频道“应用管理”,点击“添加应用”完成应用添加和配置,在配置过程中对应用开启“开发模式”。开启“开发模式”以后可以看到 appId、appSecret 和 permanentCode,请记录下来,第二步需要用到。详细步骤请参考:http://open.fxiaoke.com/support.html#artiId=61获取这些参数的路径并不是在纷享销客后台的应用页,而是在纷享销客的 管理=>应用管理中心=>自建应用
通过第一步获取的 appId、appSecret 和permanentCode 换取 CoprAccessToken,详细请参考:http://open.fxiaoke.com/wiki.html#artiId=17API接口地址:https://open.fxiaoke.com/cgi/corpAccessToken/get/V2JSON数据:
{ "appId": "APPID”,
"appSecret":"APPSECRET”,
"permanentCode":”PERMANENT_CODE"
}
PHP示例代码:
//获取CorpAccessToken
public function getcorpAccessToken() {
$result = Db::name('fxtoken')->where('type', "corpAccessToken")->find();
$corpId = $result['corpid'];
$corpAccessToken = $result['value'];
$expires_time = $result['expire'];
if (time() > ($expires_time + 7200)) {
$url = "https://open.fxiaoke.com/cgi/corpAccessToken/get/V2";
$data = array('appId' => $this->appId,
'appSecret' => $this->appSecret,
'permanentCode' => $this->permanentCode,
);
$res = json_decode($this->http_request($url, json_encode($data)));
$acctoken = array();
$acctoken['corpid'] = $res->corpId;
$acctoken['value'] = $res->corpAccessToken;
$acctoken['expire'] = time();
$corpId = $acctoken['corpid'];
$corpAccessToken = $acctoken['value'];
Db::name('fxtoken')->where('type', "corpAccessToken")->update($acctoken);
}
$AccessToken = array('corpId' => $corpId,
'corpAccessToken' => $corpAccessToken);
return $AccessToken;
}
每个 access_token 的有效期为7200秒(2小时),有效期内重复获取返回相同结果,并自动续期。所以为了防止因为频率调用次数超出限制而影响功能正常使用的问题,建议开发者将中间生成的 CorpAccessToken 进行缓存,过期以后再重新获取。同时由于每个应用的 CorpAccessToken 是彼此独立的,所以进行缓存时需要区分应用来进行存储。上述PHP示例代码已经对CorpAccessToken写入数据库,每7200秒再次获取。
一般都会对纷享销客API接口进行封装,方便进行调用,给出部分PHP示例代码:
/*
封装纷享销客开放平台OpenAPI;
author:王志锋
email:[email protected]
*/
namespace fxiaoke;
use think\Db;
class fxiaoke {
private $appId;
private $appSecret;
private $permanentCode;
private $corpid;
public $corpAccessToken;
private $currentOpenUserId;
//构造方法
public function __construct($appId, $appSecret, $permanentCode, $currentOpenUserId) {
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->permanentCode = $permanentCode;
$this->currentOpenUserId = $currentOpenUserId;
$AccessToken = $this->getcorpAccessToken();
$this->corpid = $AccessToken["corpId"];
$this->corpAccessToken = $AccessToken["corpAccessToken"];
}
//HTTP请求(支持HTTP/HTTPS,支持GET/POST)
protected function http_request($url, $data = null) {
$header = array(
'Content-Type: application/json',
);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
if (!empty($data)) {
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
$output = curl_exec($curl);
curl_close($curl);
return $output;
}
}
第二部分的getcorpAccessToken也是fxiaoke类的一个方法,其他需要封装的接口,按照开发者文档说明,进行封装即可,纷享销客开放平台开发者文档:
纷享开放平台-开发文档
open.fxiaoke.com
这里提供两个官方文档里没有的错误返回码:
errorCode: 30003
errorMessage: xxxx has no openapi quote currently!
这个错误就是我吐槽里说的,CRM系统API接口是收费的,需要开通频次调用包才能使用,客服会让你跟销售沟通。
errorCode: 20020
errorMessage: app can not access this enterprise 's data
这个错误是自建应用没有访问CRM系统数据授权的错误码,需要联系纷享客服提交给技术人员开通才能使用。
经过了前面这些步骤,纷享销客的API就应该都可以自由调用了,根据业务需求去实现就可以了。
如何使用金蝶云星空(K3Cloud)Web API ?
金蝶K3Cloud使用Web API非常简单,只要使用管理员账号登陆,在最上方搜索:webapi,即可出来:WebSDKAPI Web API功能菜单,或者在 基础管理=>公众设置=>Web API 也能打开Web API功能。其实金蝶云星空的API接口只有15个:
登陆验证接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc
查看表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc
保存表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc
批量保存表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.BatchSave.common.kdsvc
提交表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Submit.common.kdsvc
审核表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Audit.common.kdsvc
反审核表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.UnAudit.common.kdsvc
删除表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Delete.common.kdsvc
单据查询接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc
自定义WebAPI接口:
https://ServerIp/K3Cloud/接口命名空间.接口实现类名.方法,组件名.common.kdsvc
登录验证接口带踢人功能:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser2.common.kdsvc
暂存表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Draft.common.kdsvc
分配表单数据接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Allocate.common.kdsvc
下推接口:
https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Push.common.kdsvc
分组保存接口:
http://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.GroupSave.common.kdsvc
把ServerIp替换为自己服务器的域名就是自己API接口地址了,比如:https://tongdog.kingdee.com/k3cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc
就是一个公有云的登陆接口地址。登陆接口是鉴权用的,金蝶云新版Web API已经对鉴权做了升级,改为第三方应用授权的形式,这里还是讲通用的用登陆接口做鉴权,访问其他接口都需要先通过登陆接口获取cookie,然后携带cookie去访问其他接口,关于cookie的有效期,我也没有去验证过,一个业务逻辑单元内,登陆一次,cookie肯定是有效的。
金蝶云星空的所有API接口,都是两个必要条件,一是接口URL地址,二是JSON数据包,接口地址我都已经给出了,剩下就是JSON数据包了,这个也是调用金蝶云星空API接口最关键最难的地方,主要原因是接口文档不够详细,很多参数不知道该怎么写,我们先来看下登陆接口:登陆接口API地址:https://ServerIp/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc
JSON数据包:
{
"acctid": “账号ID”,//通过 WebAPI测试窗口就能看到
"username": “用户名",
"password": "密码",
"lcid": 2052//语言ID
}
这
里给出PHP的示例代码:
/*
封装金蝶K3cloud webapi;
author:王志锋
email:[email protected]
*/
namespace kingdeeapi;
class kingdeeapi {
private $username;
private $password;
private $apiurl;
private $acctID;
//构造函数,初始化
public function __construct($username, $password, $apiurl, $acctID) {
$this->username = $username;
$this->password = $password;
$this->acctID = $acctID;
$this->apiurl = $apiurl;
$this->cookie = $this->getcookie();
}
//登陆接口获取cookie
private function getcookie() {
$apiurl = "https://" . $this->apiurl . "/k3cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc";
$logindata = array(
"acctid" => $this->acctID,
"username" => $this->username,
"password" => $this->password,
"lcid" => 2052,
);
$postdata = json_encode($logindata);
$result = $this->httpRequest($apiurl, $postdata, false);
}
//http请求
public function httpRequest($url, $post_content, $isLogin = true) {
//cookie文件
//$cookie_jar = tempnam('/Applications/XAMPP/xamppfiles/temp/', 'cookie');
$ch = curl_init($url);
$this_header = array(
'Content-Type: application/json',
'Content-Length: ' . strlen($post_content),
);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, $this_header);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_content);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_HEADER,true);
if ($isLogin) {
curl_setopt($ch, CURLOPT_COOKIEFILE, "/Applications/XAMPP/xamppfiles/htdocs/law/simplewind/extend/kingdeeapi/cookie.txt");
} else {
curl_setopt($ch, CURLOPT_COOKIEJAR, "/Applications/XAMPP/xamppfiles/htdocs/law/simplewind/extend/kingdeeapi/cookie.txt");
}
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
在httpRequest方法里对cookie做了处理,如果是登陆接口,则把获取到的cookie写入文件,如果是调用其他接口,则送cookie.txt中读取cookie,登陆接口并不能做任何事情,只是为了获取使用其他接口的cookie。
其他接口的使用,跟登陆接口差不多,data数据包里必须包含formid,要操作表单的ID,JSON数据包里的字段多到吓人,特别是保存接口,甚至多到上百项,好在是必录的参数不是很多,根据企业要求,构建相应的JSON数据包就可以,我一般是构建一个最完整最全的数组,然后把其他的字段都对应上,如果某值不存在则赋空值,这样保证不用频繁修改数据包,字节改数组就可以,而数组可以保存在数据库里,直接调整数据库就可以,避免修改源码,这里给出一个客户的数据包示例:
$customerdata = array("formid" => "BD_Customer",
"data" => array(
/*
"Creator" => "",
"NeedUpDateFields" => [],
"NeedReturnFields" => [],
"IsDeleteEntry" => "true",
"SubSystemId" => "",
"IsVerifyBaseDataField" => "false",
"IsEntryBatchFill" => "true",
"ValidateFlag" => "true",
"NumberSearch" => "true",
"InterationFlags" => "",
"IsAutoSubmitAndAudit" => "false",
*/
"Model" => array(
"FCUSTID" => 0,
"FCreateOrgId" => array(
"FNumber" => "603",
),
"FNumber" => $customerinfo["field_fnfcA__c"],
"FUseOrgId" => array(
"FNumber" => "603",
),
"FName" => $customerinfo["name"],
"FShortName" => "",
"FCOUNTRY" => array(
"FNumber" => "China",
),
"FPROVINCIAL" => array(
"FNumber" => "",
),
"FADDRESS" => "",
"FZIP" => "",
"FWEBSITE" => "",
"FTEL" => "",
"FFAX" => "",
"FCompanyClassify" => array(
"FNumber" => "",
),
"FCompanyNature" => array(
"FNumber" => "",
),
"FCompanyScale" => array(
"FNumber" => "",
),
"FINVOICETITLE" => "",
"FTAXREGISTERCODE" => "",
"FINVOICEBANKNAME" => "",
"FINVOICETEL" => "",
"FINVOICEBANKACCOUNT" => "",
"FINVOICEADDRESS" => "",
"FSUPPLIERID" => array(
"FNumber" => "",
),
"FIsDefPayer" => "false",
"FGROUPCUSTID" => array(
"FNumber" => "",
),
"FIsGroup" => "false",
"FCustTypeId" => array(
"FNumber" => $customerinfo["field_n5V1I__c"],
),
"FGroup" => array(
"FNumber" => $customerinfo["field_IdOci__c"],
),
"FTRADINGCURRID" => array(
"FNumber" => "PRE001",
),
"FCorrespondOrgId" => array(
"FNumber" => "",
),
"FDescription" => "",
"FSALDEPTID" => array(
"FNumber" => "",
),
"FSELLER" => array(
"FNumber" => "",
),
"FSETTLETYPEID" => array(
"FNumber" => "",
),
"FRECCONDITIONID" => array(
"FNumber" => "",
),
"FDISCOUNTLISTID" => array(
"FNumber" => "",
),
"FPRICELISTID" => array(
"FNumber" => "",
),
"FTRANSLEADTIME" => 0,
"FInvoiceType" => "1",
"FTaxType" => array(
"FNumber" => "SFL02_SYS",
),
"FRECEIVECURRID" => array(
"FNumber" => "",
),
"FPriority" => 1,
"FTaxRate" => array(
"FNumber" => "SL02_SYS",
),
"FISCREDITCHECK" => "true",
"FIsTrade" => "true",
"FT_BD_CUSTOMEREXT" => array(
"FEntryId" => 0,
"FEnableSL" => "false",
"FFreezeLimit" => "",
"FFreezeOperator" => array(
"FUserID" => "",
),
"FFreezeDate" => "1900-01-01",
"FPROVINCE" => array(
"FNumber" => "",
),
"FCITY" => array(
"FNumber" => "",
),
"FDefaultConsiLoc" => array(
"FNUMBER" => "",
),
"FDefaultSettleLoc" => array(
"FNUMBER" => "",
),
"FDefaultPayerLoc" => array(
"FNUMBER" => "",
),
"FDefaultContact" => array(
"FNUMBER" => "",
),
"FMarginLevel" => 0,
"FDebitCard" => "",
"FSettleId" => array(
"FNUMBER" => "",
),
"FChargeId" => array(
"FNUMBER" => "",
),
),
"FT_BD_CUSTLOCATION" => [
array(
"FContactId" => array(
"FNUMBER" => "",
),
"FIsDefaultConsigneeCT" => "false",
"FIsCopy" => "false",
),
],
"FT_BD_CUSTBANK" => [
array(
"FENTRYID" => 0,
"FCOUNTRY1" => array(
"FNumber" => "",
),
"FBANKCODE" => "",
"FACCOUNTNAME" => "",
"FBankTypeRec" => array(
"FNUMBER" => "",
),
"FTextBankDetail" => "",
"FBankDetail" => array(
"FNUMBER" => "",
),
"FOpenAddressRec" => "",
"FOPENBANKNAME" => "",
"FCNAPS" => "",
"FCURRENCYID" => array(
"FNumber" => "",
),
"FISDEFAULT1" => "false",
),
],
"FT_BD_CUSTCONTACT" => [
array(
"FENTRYID" => 0,
"FNUMBER1" => "",
"FNAME1" => $customerinfo["field_zW12m__c"],
"FADDRESS1" => "",
"FTRANSLEADTIME1" => 0,
"FMOBILE" => $customerinfo["field_G45nC__c"],
"FIsDefaultConsignee" => "false",
"FIsDefaultSettle" => "false",
"FIsDefaultPayer" => "false",
"FIsUsed" => "false",
),
],
"FT_BD_CUSTORDERORG" => [
array(
"FEntryID" => 0,
"FOrderOrgId" => array(
"FNumber" => "",
),
"FIsDefaultOrderOrg" => "false",
),
],
),
),
);
一个小技巧,比如单据查询接口,根本没有写字段,这时候可以去保存接口里找。
本文到这里基本就已经结束了,能够熟练的使用双方API接口后,根据业务逻辑去实现就可以了。如果有类似需求,可以一起交流学习,文中若有不对之处,还望指正,谢谢阅读!
Author:王志锋
Email:[email protected]