本文概述了K/3 Cloud的Web API接口在第三方系统集成中的应用,并指出调用简单但参数构建复杂。特别是,K/3 Cloud提供的示例代码仅包含单据头字段,未涉及单据体字段,导致直接使用可能保存失败。文章通过采购订单保存接口的案例,详细展示了如何构建包含单据体字段的完整参数,并提供了示例代码,帮助读者在示例基础上扩展和构建其他单据的Web API接口参数。
需求背景:
K/3 Cloud提供了Web API接口,供第三方程序调用,以实现与第三方系统之间的数据集成。
调用Web API接口虽然简单,但是构建参数却非常复杂,与各单据上的字段密切相关。
K/3 Cloud内置了一个Web API接口说明功能,提供了示例代码,演示如何调用各单据的各种操作。
遗憾的是,示例代码中的参数,包含了全部单据头字段,但没有包括单据体字段,完全按照此示例代码调用接口,往往会保存失败。
案例说明:
本案例,基于采购订单的保存接口,演示如何构建各种字段参数,单据体字段参数。
以便读者在K/3 Cloud内置的Web API接口示例代码基础上,据此类推,自行构建出各种单据,Web API保存接口需要的,包含了单据体字段的完整参数。
为方便阅读,本案例仅构建了采购订单必须的字段参数,其他类似的字段,直接忽略。
示例代码:
//*********************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Newtonsoft.Json.Linq;
using Kingdee.BOS.WebApi.Client;
//*********************************************************
/// <summary>
/// 采购订单
/// </summary>
private string Sample2015121401()
{
try
{
// K/3 Cloud业务站点URL
// using Kingdee.BOS.WebApi.Client; // (需引用Kingdee.BOS.WebApi.Client.dll)
ApiClient client = new ApiClient(this.txtWebSite.Text);
// 调用登录接口:
// 参数说明:
// dbid : 数据中心id。到管理中心数据库搜索:
// select FDataCenterId, * from T_BAS_DataCenter
// userName : 用户名
// password :原始密码(未加密)
// loid : 语言id,中文为2052,中文繁体为3076,英文为1033
var loginResult = client.Login(
this.txtDbId.Text,
this.txtUser.Text,
this.txtPassword.Text,
2052);
string result = "登录失败,请检查与站点地址、数据中心Id,用户名及密码!";
// 登陆成功,开始保存数据
if (loginResult == true)
{
// 开始构建Web API参数对象
// 参数根对象:包含Creator、NeedUpDateFields、Model这三个子参数
// using Newtonsoft.Json.Linq; // (需引用Newtonsoft.Json.dll)
JObject jsonRoot = new JObject();
// Creator: 创建用户
jsonRoot.Add("Creator", "Demo");
// NeedUpDateFields: 哪些字段需要更新?为空则表示参数中全部字段,均需要更新
jsonRoot.Add("NeedUpDateFields", new JArray(""));
// Model: 单据详细数据参数
JObject model = new JObject();
jsonRoot.Add("Model", model);
// 开始设置单据字段值
// 必须设置的字段:主键、单据类型、主业务组织,各必录且没有设置默认值的字段
// 特别注意:字段Key大小写是敏感的,建议从BOS设计器中,直接复制字段的标识属性过来
// 单据主键:必须填写,系统据此判断是新增还是修改单据;新增单据,填0
model.Add("FID", 0);
// 采购组织:必须填写,是基础资料字段
JObject basedata = new JObject();
basedata.Add("FNumber", "101.2");
model.Add("FPurchaseOrgId", basedata);
// 单据类型:必须填写,是基础资料字段
// 基础资料类型字段填值,必须再构建一个JObject对象,设置基础资料编码
basedata = new JObject();
basedata.Add("FNumber", "CGDD01_SYS");
model.Add("FBillTypeID", basedata);
// 单据编号:可以忽略,由系统根据编码规则自动生成
model.Add("FBillNo", "JDCGDD1605190001");
//采购日期(FDate)
model.Add("FDate", DateTime.Today);
// 录入业务上要求必须提前设置的字段
// 这些字段通常无需由用户手工录入,提前设好默认值,如汇率类型、本位币等
// 汇率类型(FExchangeTypeId):基础资料
basedata = new JObject();
basedata.Add("FNumber", "HLTX01_SYS");
model.Add("FExchangeTypeId", basedata);
// 本位币(FLocalCurrId):基础资料
basedata = new JObject();
basedata.Add("FNumber", "PRE001");
model.Add("FLocalCurrId", basedata);
// 按手工录入顺序,录入其他关键业务字段
// 供应商(FSupplierId):基础资料
basedata = new JObject();
basedata.Add("FNumber", "VEN00002");
model.Add("FSupplierId", basedata);
// 其他单据头字段,非必录,本示例代码忽略
// 开始构建单据体参数:集合参数JArray
JArray entryRows = new JArray();
// 把单据体行集合,添加到model中,以单据体Key为标识
string entityKey = "FPOOrderEntry";
model.Add(entityKey, entryRows);
// 通过循环创建单据体行:示例代码仅创建一行
for (int i = 0; i <= 0; i++)
{
// 添加新行,把新行加入到单据体行集合
JObject entryRow = new JObject();
entryRows.Add(entryRow);
// 给新行,设置关键字段值
// 单据体主键:必须填写,系统据此判断是新增还是修改行
entryRow.Add("FEntryID", 0);
//物料(FMaterialId):基础资料,填写编码
basedata = new JObject();
basedata.Add("FNumber", "1.01.001.0070");
entryRow.Add("FMaterialId", basedata);
// 单位(FUnitId):基础资料,填写编码
basedata = new JObject();
basedata.Add("FNumber", "Pcs");
entryRow.Add("FUnitId", basedata);
// 数量(FQty):数量字段
entryRow.Add("FQty", 10);
// 单价(FPrice):单价字段
entryRow.Add("FPrice", 11.7);
// 其他单据体字段,非必录,本示例代码忽略
// 创建与源单之间的关联关系,以支持上查与反写源单
// 本示例演示创建与采购申请单之间的关联关系
// 源单类型、源单编号
entryRow.Add("FSrcBillTypeId", "PUR_Requisition");
entryRow.Add("FSrcBillNo", "CGSQ000009");
// 创建Link行集合
JArray linkRows = new JArray();
// 添加到单据体行中:Link子单据体标识 = 关联主单据体标识(POOrderEntry) + _Link
string linkEntityKey = string.Format("{0}_Link", entityKey);
entryRow.Add(linkEntityKey, linkRows);
// 创建Link行:
// 如有多条源单行,则分别创建Link行记录各条源单行信息
JObject linkRow = new JObject();
linkRows.Add(linkRow);
// 填写Link行上的字段值
// 特别说明:Link子单据体上字段的标识,必须在前面增加子单据体标识
// FFlowId : 业务流程图,可选
string fldFlowIdKey = string.Format("{0}_FFlowId", linkEntityKey);
linkRow.Add(fldFlowIdKey, "");
// FFlowLineId :业务流程图路线,可选
string fldFlowLineIdKey = string.Format("{0}_FFlowLineId", linkEntityKey);
linkRow.Add(fldFlowLineIdKey, "");
// FRuleId :两单之间的转换规则内码,必填
// 可以通过如下SQL语句到数据库获取
// select FID, *
// from T_META_CONVERTRULE
// where FSOURCEFORMID = 'PUR_Requisition'
// and FTARGETFORMID = 'PUR_PurchaseOrder'
// and FDEVTYPE = 0;
string fldRuleIdKey = string.Format("{0}_FRuleId", linkEntityKey);
linkRow.Add(fldRuleIdKey, "PUR_Requisition-PUR_PurchaseOrder");
// FSTableName :必填,源单单据体表格编码,通过如下语句获取:
// SELECT FTableNumber
// FROM t_bf_tabledefine
// WHERE fformid = 'PUR_Requisition'
// AND fentitykey = 'FEntity'
// 如果如上语句未返回结果,请到K/3 Cloud中,手工选单一次,后台会自动产生表格编码
string fldSTableNameKey = string.Format("{0}_FSTableName", linkEntityKey);
linkRow.Add(fldSTableNameKey, "T_PUR_ReqEntry");
// FSBillId :必填,源单单据内码
string fldSBillIdKey = string.Format("{0}_FSBillId", linkEntityKey);
linkRow.Add(fldSBillIdKey, 100011);
// FSId : 必填,源单单据体行内码。如果源单主关联实体是单据头,则此属性也填写源单单据内码
string fldSIdKey = string.Format("{0}_FSId", linkEntityKey);
linkRow.Add(fldSIdKey, 100029);
// FEntity_Link_FBaseQtyOld :数量原始携带值,下推时,从源单带了多少下来 // 在合并下推时,系统会把多个源行的数量,合并后填写在单据体数量字段上的;
// 合并后对各源单行的反写,需要使用合并前的数量进行反写,也就是本字段上记录的数量
// 因此,如果有合并下推,本字段就必须填写,否则反写不准确
// 如果没有合并下推,单据体数量会直接反写到唯一的源行上,本字段就不需填写
string fldBaseQtyOldKey = string.Format("{0}_FBaseUnitQtyOld", linkEntityKey);
linkRow.Add(fldBaseQtyOldKey, 10);
// FEntity_Link_FBaseQty :数量实际携带值,下推后,用户可以手工修改数量值;此字段存储最终的数量值
// 可选字段:
// 在保存时,系统会自动把单据体上数量值,更新到此字段;因此,这个字段可以不用填写(即使填写了,也会被覆盖)
string fldBaseQtyKey = string.Format("{0}_FBaseUnitQty", linkEntityKey);
linkRow.Add(fldBaseQtyKey, 10);
}
// 调用Web API接口服务,保存采购订单
result = client.Execute<string>(
"Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save",
new object[] {"PUR_PurchaseOrder", jsonRoot.ToString() });
}
return result;
}
catch(Exception exp)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("程序运行遇到了未知的错误:");
sb.Append("错误提示:").AppendLine(exp.Message);
sb.Append("错误堆栈:").AppendLine(exp.StackTrace);
return sb.ToString();
}
}
如何分析调用Web API,字段值没有赋值成功?
需求背景:
调用Web API保存单据时,常常出现在参数中,明明已经设置了字段值,但是却没有效果,返回结果还是提示该字段必录。
发生此问题的原因,通常有如下几个方面:
1. 字段Key值弄错:字段Key是大小写敏感的,必须严格按照BOS设计器中设定的字段标识属性设置参数;
2. 字段值不被认可:如给基础资料字段赋值,填写的编码不存在,或未分配到目标组织,或未审核、已禁用等;
3. 字段赋值顺序不对:某些字段有依赖字段,必须在依赖字段赋值后,才能赋值,需要调整Web API中字段参数顺序;
4. 字段值被其他字段的值更新事件覆盖:此点最难确认,本帖提供一个插件代码来解决。
案例说明:
本帖提供一个插件代码,挂到单据表单插件上,捕获字段改变DataChanged事件,输出字段及其字段值。
最终效果,会按顺序输出全部字段的赋值过程,据此可以追查出,整个Web API调用过程:
1. 字段有没有被赋值?
2. 字段赋值是否正确?
3. 字段有没有多次赋值,值由正确值变为无效值,即被其他字段覆盖了?
此插件也适合调用WebService接口保存单据。
日志输出效果:
示例代码:
本插件代码,需要编译后,挂到单据的表单插件上,本地验证OK。
//*********************************************
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using Kingdee.BOS;
using Kingdee.BOS.Util;
using Kingdee.BOS.Core;
using Kingdee.BOS.Core.DynamicForm.PlugIn;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;
using Kingdee.BOS.Core.Bill;
using Kingdee.BOS.Core.Bill.PlugIn;
using Kingdee.BOS.Core.Bill.PlugIn.Args;
using Kingdee.BOS.Core.Metadata.FieldElement;
using Kingdee.BOS.Orm.DataEntity;
namespace JDSample.WatchImport
{
/// <summary>
/// 监控调用Web API接口保存单据时,给字段赋值过程
/// </summary>
/// <remarks>
/// 背景说明:
/// 调用Web API或者WebService接口,保存单据;
/// 经常发生参数中已经设置了字段值,但是保存结果,却提示字段必录;
/// 发生此现象时,非常难以排查问题;
///
/// 解决方案:
/// 发生此现象时,可以把本插件,挂在单据表单插件上;
/// 本插件监控到单据被Web API接口,设置单据字段值时,
/// 会即时把字段的历史数值、新数据输出到K/3 Cloud日志文件中,
/// 通过日志文件,就可以了解字段赋值的顺序、所填写的值,
/// 确认出字段为何没有填写上:
/// 1. 字段未接收到值:字段Key错误,或字段值不合法
/// 2. 被其他字段覆盖了:字段录入顺序需要调整
/// </remarks>
[Description("Web API接口赋值监控")]
public class Watch : AbstractBillPlugIn
{
/// <summary>
/// 当前进程,是否为WebService、Web API方式调用;
/// 只有WebService调用,才输出日志
/// </summary>
bool _isWebService = false;
/// <summary>
/// 界面初始化事件:判断当前进程是否为WebService调用
/// </summary>
/// <param name="e"></param>
public override void OnInitialize(InitializeEventArgs e)
{
if (this.Context.ServiceType == WebType.WebService)
{
this._isWebService = true;
}
}
/// <summary>
/// 单据数据包创建、加载完毕:在日志中,输出一个开始语句,以便阅读
/// </summary>
/// <param name="e"></param>
public override void AfterCreateNewData(EventArgs e)
{
if (this._isWebService == false)
{
return;
}
string message = string.Format("现在开始创建单据{0}的新数据包",
this.View.BillBusinessInfo.GetForm().Name.ToString());
Kingdee.BOS.Log.Logger.Info("WebService", message);
}
/// <summary>
/// 值改变事件:监控字段值修改顺序及值
/// </summary>
/// <param name="e"></param>
public override void DataChanged(DataChangedEventArgs e)
{
if (this._isWebService)
{
string message = this.BuildFldChangedLog(e.Field, e.NewValue, e.OldValue);
Kingdee.BOS.Log.Logger.Info("WebService", message);
}
}
/// <summary>
/// 保存前调用此事件:可以通过此事件,观察单据数据包中的字段值
/// </summary>
/// <param name="e"></param>
public override void BeforeSave(BeforeSaveEventArgs e)
{
base.BeforeSave(e);
}
/// <summary>
/// 构建字段的修改日志
/// </summary>
/// <param name="field"></param>
/// <param name="newValue"></param>
/// <param name="oldValue"></param>
/// <returns></returns>
private string BuildFldChangedLog(Field field, object newValue, object oldValue)
{
string newValueString = Convert.ToString(newValue);
string oldValueString = Convert.ToString(oldValue);
string logMessage = string.Format("给字段【{0}({1})】赋值为 \"{2}\";旧值为\"{3}\"{4}",
field.Name.ToString(),
field.Key,
string.IsNullOrWhiteSpace(newValueString) ? "(空值)" : newValueString,
string.IsNullOrWhiteSpace(oldValueString) ? "(空值)" : oldValueString,
string.IsNullOrWhiteSpace(newValueString) ? "【有异常,清空了字段值!】" : string.Empty);
return logMessage;
}
}
}
推荐阅读