**摘要**:本文面向CRM/ERP系统管理员、数据库管理员等,介绍如何实现纷享销客与金蝶系统之间的数据同步。通过利用双方提供的API接口,结合业务需求,可以实现客户、订单等数据在两个系统间的同步。文章详细说明了使用纷享销客和金蝶云星空Web API的方法,包括获取授权、接口调用、数据处理等步骤,并指出了API接口对异构系统数据互通的重要性及实现难点。
阅读对象:CRM/ERP系统管理员、数据库管理员、系统实施、财务、数据分析等相关人员,希望实现两个系统之间数据同步的程序员。
适用场景:纷享销客创建完客户以后,希望客户信息直接同步到金蝶系统客户列表里;纷享销客创建完订单,直接同步到金蝶销售订单;金蝶系统做完发货单,纷享销客同时收到发货通知等。
实现原理:利用纷享销客开放平台Open API接口实现对纷享销客CRM对象(客户、订单、联系人、销售线索、回款…...)的操作;利用金蝶云星空(K3cloud)Web API实现对金蝶云星空表单数据的操作;利用两个系统各自的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获取这些参数的路径并不是在纷享销客后台的应用页,而是在纷享销客的 管理=>应用管理中心=>自建应用
第二步:获取CorpAccessToken
通过第一步获取的 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; }
第三步:对CorpAccessToken做缓存处理
每个 access_token 的有效期为7200秒(2小时),有效期内重复获取返回相同结果,并自动续期。所以为了防止因为频率调用次数超出限制而影响功能正常使用的问题,建议开发者将中间生成的 CorpAccessToken 进行缓存,过期以后再重新获取。同时由于每个应用的 CorpAccessToken 是彼此独立的,所以进行缓存时需要区分应用来进行存储。上述PHP示例代码已经对CorpAccessToken写入数据库,每7200秒再次获取。
一般都会对纷享销客API接口进行封装,方便进行调用,给出部分PHP示例代码:
<?php/* 封装纷享销客开放平台OpenAPI; author:王志锋 email:wangzhifeng@tongdog.com.cn */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类的一个方法,其他需要封装的接口,按照开发者文档说明,进行封装即可,纷享销客开放平台开发者文档:https://open.fxiaoke.com/wiki.html#artiId=15
这里提供两个官方文档里没有的错误返回码:
errorCode: 30003errorMessage: xxxx has no openapi quote currently!
这个错误就是我吐槽里说的,CRM系统API接口是收费的,需要开通频次调用包才能使用,客服会让你跟销售沟通。
errorCode: 20020"https://vip.kingdee.com/download/0100cf9fa846336043a9932d9f8ca0322243.png" title="截屏2019-11-17下午11.23.04.png" alt="截屏2019-11-17下午11.23.04.png" target="_blank">金蝶云星空的所有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的示例代码:
<?php/* 封装金蝶K3cloud webapi; author:王志锋 email:wangzhifeng@tongdog.com.cn */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:wangzhifeng@tongdog.com.cn