尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

金蝶ERP插件开发实战:从事件机制到二次开发落地

金蝶ERP插件开发实战:从事件机制到二次开发落地 做金蝶ERP实施和开发这些年我接过不少客户的二次开发需求最开始大家口中的“插件开发”往往指的就是在KingdeeERP上扩展功能。真正上手之后会发现它跟你平时做的普通.NET开发差别不小界面逻辑、业务校验、数据流转、部署发布每一个环节都有自己的套路。这篇文章我打算把金蝶ERP插件开发这件事从头到尾捋一遍适合刚接触金蝶二次开发的程序员也适合那些想评估“客户提的需求到底能不能做、怎么做”的实施顾问参考。1. 先想明白金蝶ERP插件开发到底在“开发”什么1.1 金蝶ERP的插件体系与产品线差异金蝶ERP并不是一个单一产品而是一个产品家族。常见的有面向中小企业的KIS系列、面向成长型企业的K/3 WISE、面向中型集团企业的金蝶云星空Cloud Galaxy以及面向大型集团的金蝶EAS。不同产品的插件开发方式差别非常大。K/3 WISE时代插件开发主要围绕“单据模板”来做开发人员写一个继承自特定基类的DLL注册到单据插件接口上客户端在加载单据时反射调用。这个时期的插件以VB6和C#为主调试比较痛苦动不动就要“注册组件”“重启客户端”。金蝶云星空也就是大家常说的金蝶云以前的K/3 Cloud的升级版本是目前最主流的产品它的插件体系基于BOSBusiness Operating System平台提供了一套相对统一的事件模型。你写一个继承自AbstractFormPlugIn的类重写OnLoad、BeforeSave、AfterSave这类方法就能把自定义逻辑挂到标准流程上。这套体系对.NET开发者友好得多Visual Studio里写好代码生成DLL然后在BOS设计器里注册配置完成后即可生效。EAS走的又是另一条路线它的扩展更多依赖“实体扩展”和“策略模式”插件要跟实体模型、事件总线打交道复杂度比云星空高一截。这篇文章里的内容大部分以金蝶云星空为参照来讲因为它是当前需求最集中的方向。K/3和EAS的开发思路有共通之处但细节差异大需要时可以单独再展开。1.2 能用插件解决的问题不要用改标准程序解决金蝶ERP的插件开发在项目上有一个极其重要的价值隔离定制内容与标准产品。金蝶每年要发补丁、升级版本客户也常常提新需求。如果你拿到需求直接改标准单据页面、改标准业务流程后患无穷——版本升级的时候你的改动会被覆盖或者跟新版本的标准逻辑冲突导致系统行为不可控。插件机制的存在就是让你在“标准产品之上”架一层自己的逻辑通过事件拦截、界面扩展、服务替换等方式实现需求而不动标准程序的代码。这一点在交付项目中特别关键。我举个实际例子。客户在销售订单上希望保存时自动把订单金额回写到客户档案的“最近订单金额”字段上。如果直接改销售订单的保存逻辑下次金蝶发补丁你的代码可能就失效了还得重新改一遍。但是以插件方式做在销售订单的BeforeSave事件里写逻辑你的代码作为独立DLL存在版本升级时只要重新测试插件兼容性即可不用重新“手术”。从这个角度看插件开发本质上是一种“非侵入式”的扩展方案。做交付项目时这种方案能让你的代码活得更久减少后续维护成本。客户那边IT人员也更容易接受这种方式因为他们知道标准程序没有被动过。1.3 金蝶ERP插件的几种常见形态金蝶云星空的插件种类不少但项目上最常用的就是三类表单插件FormPlugIn拦截单据界面的生命周期事件比如加载、保存、审核、反审核、按钮点击。这是用的最多的类型像字段联动、必录校验、审核前检查这类需求基本都是表单插件。列表插件ListPlugIn作用于单据列表界面比如动态加过滤条件、处理批量操作、自定义列表按钮等。服务插件ServicePlugIn作用于业务操作的后台服务层比如操作服务插件可以在审核、提交、保存等操作前后执行逻辑跟界面无关适合做跨单据的逻辑比如保存销售订单时联动生成下游单据。理解这几类插件的差别是选型的第一步。如果涉及到单据与单据之间联动、数据在后台流转优先考虑服务插件如果只是界面上展示、提示、字段更新用表单插件就够了。2. 插件开发的核心技术模式事件驱动的拦截器2.1 那套“钩子机制”是怎么运作的金蝶插件开发最核心的机制说穿了就是“事件驱动”。标准程序在运行到某个关键节点时会主动向外“广播”一个事件比如“我现在要保存单据了”“我已经把数据绑定到界面了”。插件就像是在这些节点上挂了一个钩子事件一触发你的代码就开始执行。这套机制的正式称呼是“拦截器模式”。你把自定义逻辑包装成插件对象在BOS平台里注册平台在运行标准流程的时候会通过反射创建你的插件实例按顺序调用你重写的那些方法。用生活的话解释标准程序就是一条流水线每个工位干固定的事。你现在想在某个工位多检查一道不用拆掉整条流水线只需要在那个工位旁边加一道检查岗。插件就是那道检查岗。拿金蝶云星空为例保存一张销售订单时插件的执行顺序大致是OnLoad页面加载→AfterLoad→BeforeBindData→AfterBindData数据绑定→ 用户编辑 →BeforeSave保存前校验→ 平台校验 →AfterSave保存后处理在这些环节里你的插件代码执行时机不同能达到的目的也不同。比如BeforeSave适合做拦截校验因为在这里抛异常能阻止保存AfterSave适合做后续动作因为此时数据已经落库你可以大胆去读库、写日志、推送消息。一个插件类里可以同时重写多个方法也可以针对不同事件分别写不同类。关键是要知道在哪个节点干哪件事这是金蝶插件开发的经验核心。2.2 表单插件直接跟界面和数据打交道表单插件是绝大多数开发人员第一次接触金蝶插件时写的东西。它的基类是AbstractFormPlugIn。你写一个类继承它然后重写你需要的方法比如using Kingdee.BOS; using Kingdee.BOS.Core.DynamicForm.PlugIn; using Kingdee.BOS.Core.DynamicForm.PlugIn.ControlModel; using Kingdee.BOS.Core.Metadata; using System.ComponentModel; namespace MyPlugIns { [Description(销售订单审核前校验)] public class SaleOrderAuditCheck : AbstractFormPlugIn { public override void OnLoad(Kingdee.BOS.Core.DynamicForm.PlugIn.Args.DynamicFormLoadEventArgs e) { base.OnLoad(e); // 页面加载后自动给某个字段赋值 this.View.Model.SetValue(F_MyNeedDate, DateTime.Now.AddDays(7)); } public override void BeforeDoOperation(Kingdee.BOS.Core.DynamicForm.PlugIn.Args.BeforeDoOperationEventArgs e) { base.BeforeDoOperation(e); // 拦截操作判断用户点击的是否为“审核” if (e.Operation.OperationNumber Audit) { string billNo Convert.ToString(this.View.Model.GetValue(FBillNo)); if (string.IsNullOrEmpty(billNo)) { throw new KDPException(单号为空不能审核); } } } } }写这段代码的时候有两点必须注意。第一只要重写一个方法就一定要记得调用base.xxx(e)否则会跳过标准逻辑导致系统行为异常。这一点我见过很多新手踩坑重写OnLoad不调base.OnLoad结果页面上的标准初始化全部失效报一堆奇怪的错。第二操作类型判断要用e.Operation.OperationNumber而不是拿按钮文本去比较。按钮文本是可以被用户修改的操作编号才是稳定标识比如Save、Submit、Audit、UnAudit。表单插件里最常用的操作对象是this.View.Model。它代表当前单据的数据模型GetValue取字段、SetValue赋值、GetBillModel()拿动态数据包基本构成了插件的日常三件套。再深入一点你还能通过this.View.GetControl(某个字段)拿到界面控件动态控制它的可见性、锁定状态、下拉选项数据源等。这些操作组合起来能实现非常灵活的业务规则比如“客户类型为‘个人’时手机号必录且地址置灰不可填”。2.3 服务插件把逻辑从界面剥离出来服务插件是另一类非常重要的扩展手段。它的设计初衷是业务逻辑不要依赖界面。无论你是通过界面操作还是通过WebAPI调用还是通过其他系统集成的接口触发的业务流服务层的逻辑都应该被正确执行。金蝶云星空把很多业务动作抽成了“操作服务”这些服务在执行时同样会释放插件事件。服务插件的基类是AbstractOperationServicePlugIn常用的写法using Kingdee.BOS; using Kingdee.BOS.Core.Metadata; using Kingdee.BOS.Core.Metadata.ConvertElement; using Kingdee.BOS.Core.Operation; using Kingdee.BOS.ServiceHelper; using System.ComponentModel; namespace MyPlugIns { [Description(销售订单保存后自动更新客户累计金额)] public class SaleOrderSaveUpdateCustomer : AbstractOperationServicePlugIn { public override void OnPrepareOperationServiceOption(OperationServiceOption option) { base.OnPrepareOperationServiceOption(option); // 设置某些选项比如保存时是否需要校验 } public override void OnAddValidators(Kingdee.BOS.Core.Validation.ValidatorCollection validators) { base.OnAddValidators(validators); // 添加自定义校验器 } public override void AfterExecute(IServiceContext context, DynamicObject billObject, OperationResult result) { base.AfterExecute(context, billObject, result); string billNo Convert.ToString(billObject[FBillNo]); // 在这里做保存后的逻辑 } } }这里要注意的是服务插件的billObject参数是DynamicObject类型的元数据包不再通过this.View.Model来拿数据。因为你可能不是在界面环境里被调起来而是在后台被服务直接触发的。操作数据包的方式是直接用索引器billObject[字段标识]子表数据要拿billObject[子表标识]再往下取。服务插件和表单插件常常配合使用。比如你有一个很复杂的单据联动逻辑——销售订单保存后要自动检查库存、如果库存不足则锁库、同时更新客户信用额度。这件事如果用表单插件做用户不开界面就走不到逻辑如果用服务插件做无论从哪里触发保存都会执行。所以跨单据、需要稳定触发时服务插件是更优的选择。2.4 插件注册开发完不注册等于白写插件代码写完之后还有一个关键环节——注册。金蝶云星空的插件注册通常在BOS设计器里完成。操作路径大概是BOS设计器 → 打开目标单据 → 找到“插件注册”入口 → 新增一行 → 填上“插件程序集名称.完整类名”。这里的程序集名称指的是DLL的文件名完整类名是带命名空间的类路径。比如你的DLL叫MyPlugIns.dll类全名叫MyPlugIns.SaleOrderAuditCheck就填MyPlugIns.MyPlugIns.SaleOrderAuditCheck。对中间那一截是命名空间别漏了。注册之后还需要在“事件绑定”里设置插件要监听哪些事件比如勾选“加载”“保存”“审核”等。这一步很容易被忽略。我之前有一个项目插件写好了DLL也部署了注册也注册了就是不触发排查了半天最后发现是事件绑定里没勾选对应的事件类型。这个坑写的详细一点BOS设计器里的插件注册有两个层次第一层是“插件列表”写明有哪些插件参与第二层是“事件订阅”告诉系统哪些事件会调用这些插件。两处都配好才算真正挂上钩。另外如果插件需要加界面按钮则要在“工具栏”里添加按钮并设置按钮标识然后在插件代码里通过this.View.Toolbar.Build(按钮标识, 显示文本)来绑定点击事件处理逻辑。自定义按钮和插件事件是两套体系但经常一起用。比如客户想要一个“一键生成对账单”的按钮你就得在界面上加按钮然后在插件里响应按钮点击事件。3. 从0到1搭环境、写插件、跑起来3.1 开发环境搭建的完整清单金蝶ERP插件开发的环境搭建比普通的.NET Web开发要多几个步骤。如果你是第一次接触建议按这个清单来准备金蝶云星空服务器环境可以是客户环境也可以是本地虚拟机。插件最终要发布到服务器上开发环境最好跟生产环境保持一致的版本。BOS集成开发平台BOS Studio / K3Cloud BOS设计器金蝶云星空通常已经自带BOS设计器你在服务器安装目录下能找到。开发时用设计器设计单据、注册插件、查看元数据它是插件开发的地基。Visual Studio写插件代码用。金蝶云星空的插件是基于.NET 4.5以上开发的用VS2019或VS2022比较稳。注意目标框架跟金蝶运行环境匹配。金蝶程序集引用开发插件时需要引用金蝶的DLL比如Kingdee.BOS.dll、Kingdee.BOS.Core.dll。这些DLL在服务器安装目录下的Bin文件夹里能找到。代码写完之后编译生成的插件DLL也要复制到这个目录下不同版本可能是bin或者WebSite\Bin目录。这一步有一个容易踩的坑不要随手把整个金蝶安装目录里的几百个DLL全都引用到项目里会拖慢编译速度不说还容易引发版本冲突。实际上写一个表单插件引用Kingdee.BOS、Kingdee.BOS.Core这两个再加上系统自带的System和System.ComponentModel多半就够用了。等编译报错缺什么再补什么不要一开始就做“全家桶引用”。3.2 一个最简单的表单插件从创建项目到部署我习惯把插件代码单独建一个“类库项目”不跟其他业务代码混在一起。这样做的好处是插件DLL可以独立编译、独立发布不会因为改一个工具类就要重新打包整个系统。具体步骤如下第一步在Visual Studio里新建一个“类库(.NET Framework)”项目目标框架选.NET Framework 4.5或4.6具体看金蝶版本要求一般4.5起步没问题。第二步添加引用。右键“引用” → “添加引用” → “浏览”找到金蝶服务器安装目录下的Kingdee.BOS.dll和Kingdee.BOS.Core.dll把这两个文件选中并添加进来。如果后面写代码时提示缺少别的类型就再到Bin目录下面找对应的程序集。第三步新建一个类继承AbstractFormPlugIn或者AbstractOperationServicePlugIn写你的业务逻辑。我贴一个更完整的示例实现一个很常见的场景在采购订单新增时根据供应商档案里的“默认采购员”字段自动把采购员带出来。using Kingdee.BOS; using Kingdee.BOS.Core.DynamicForm.PlugIn; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; using Kingdee.BOS.Orm.DataEntity; using Kingdee.BOS.ServiceHelper; using System; using System.ComponentModel; namespace MyPlugIns { [Description(采购订单自动带出采购员)] public class PurchaseOrderDefaultBuyer : AbstractFormPlugIn { public override void AfterBindData(EventArgs e) { base.AfterBindData(e); // 获取供应商ID字段的值 long? supplierId Convert.ToInt64(this.View.Model.GetValue(FSupplierId)); if (supplierId null || supplierId 0) return; // 用业务对象服务读取供应商档案 DynamicObject supplier BusinessDataServiceHelper.LoadSingle(supplierId.Value, bd_Supplier); if (supplier null) return; // 获取“默认采购员”基础资料属性 object defaultBuyerId supplier[F_MyDefaultBuyer]; if (defaultBuyerId null) return; // 回填到当前单据的采购员字段 this.View.Model.SetValue(FPurchaserId, defaultBuyerId); } } }第四步编译生成DLL。把编译得到的MyPlugIns.dll复制到金蝶服务器的Bin目录下。如果服务器跟开发机是同一台机这一步很快如果是远程环境就要用文件传输工具或者发布系统把DLL推上去。注意如果服务器上的金蝶服务正在运行替换DLL之后可能需要重启相关服务进程才能生效。一般来说Web服务会自动检测文件变化但为了保险替换完最好等几秒钟再测试不行就重启。第五步BOS设计器里打开采购订单进行插件注册绑定“数据绑定后”这个事件也就是AfterBindData。这样用户在点击“新增”时供应商字段被绑定成默认值后插件会自动带出采购员。3.3 调试方法与断点技巧别当愣头青直接上生产金蝶插件调试和普通.NET程序调试不太一样因为金蝶是一个运行在服务器上的大型应用你的插件代码是被它动态加载的做不到按F5直接调试。项目现场最常用的调试方式就是把Visual Studio附加到金蝶进程上。在Visual Studio里打开你的插件代码项目在菜单栏选“调试” → “附加到进程”。进程类型需要从列表里找云端版一般是w3wp.exeIIS工作进程或者K3CloudJobManager.exe定时任务进程、K3CloudManager.exe服务管理器。附加成功之后在你插件代码里打上断点去金蝶界面触发对应操作断点就会被命中。这里有几个细节要提醒。第一附加进程之前代码必须是当前编译的版本并且DLL必须部署到服务器对应目录否则调试的根本不是你的最新代码断点会显示“未命中”。第二金蝶会有多个w3wp.exe进程如果附加错了进程断点不会命中这时可以打开任务管理器看PID再在附加进程窗口里对号入座。第三调试模式下性能会掉得厉害不要在生产环境里长时间挂着测完就拆。如果没有条件附加进程也可以用最原始的日志法。在金蝶插件代码里写一个工具方法把关键变量写到服务器的文本文件里比如public static void WriteLog(string msg) { string logDir D:\Logs\PlugInLog; if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir); File.AppendAllText(Path.Combine(logDir, DateTime.Now.ToString(yyyyMMdd) .log), ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}\r\n); }这个方法虽然比不上断点调试效率高但在远程服务器、没有开调试端口的环境下非常管用。我处理过很多“客户现场突然报错”的情况都是让客户IT把插件日志发送回来分析比远程附加进程更快更稳。4. 跑一个真实需求销售订单的信用额度校验插件4.1 需求描述与冲突点分析讲完基础我用一个真实项目里的需求来串一下客户是制造业企业销售部门提交销售订单时系统需要校验“该客户当前累计未收款的销售订单金额不得超过客户档案里设定的信用额度”。如果超过保存必须被拦截提示“该客户信用额度不足”。这个需求之所以适合用插件做因为它卡在“保存”这个必经关卡上且校验标准涉及跨单据计算——要把所有未审核通过的销售订单金额加起来跟客户档案的额度字段比较。客户在两套系统里也提过类似需求最终选择做在金蝶ERP插件层而不是改造前端页面或者单独开发一个校验系统原因很简单销售订单的唯一入口就是金蝶业务量也不大没必要单独拉一套外部校验服务。方案选型上校验逻辑用表单插件的BeforeSave事件拦截最合适。原因是校验必须阻止用户保存前台表单插件抛出异常即可阻止而且用户能立刻看到错误提示如果放到服务插件里虽然也能拦截但错误反馈可能不会像表单操作那样弹窗提醒得直观。4.2 实现思路与代码落地明确一下校验口径信用额度 客户档案的“信用额度”字段当前已用额度 所有“已保存”但“未审核”的销售订单金额之和含当前这张单子。审核已通过的说明款已回或风险已释放不再占用额度而“暂存”状态的单子一般不计入因为还不算正式承诺。数据查询部分我直接用金蝶的BusinessDataServiceHelper执行一个查询避免引入复杂的ORM。为了性能查询只取FBillNo、FAmount两个字段避免整张大单据取出来。代码结构大致这样using Kingdee.BOS; using Kingdee.BOS.Core.DynamicForm.PlugIn; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; using Kingdee.BOS.Orm.DataEntity; using Kingdee.BOS.ServiceHelper; using System; using System.ComponentModel; using System.Linq; namespace MyPlugIns { [Description(销售订单保存校验信用额度)] public class SaleOrderCreditCheck : AbstractFormPlugIn { public override void BeforeSave(EventArgs e) { base.BeforeSave(e); // 不拦截“暂存”暂存不是正式单据 if (this.View.ParentFormId 0 this.View.Model.DataObject[DocumentStatus].ToString() Z) { return; } long customerId Convert.ToInt64(this.View.Model.GetValue(FCustomerId)); if (customerId 0) return; // 1. 读取客户档案信用额度 DynamicObject customer BusinessDataServiceHelper.LoadSingle(customerId, bd_Customer); if (customer null) return; decimal creditLimit Convert.ToDecimal(customer[FCreditLimit]); // 2. 计算当前单据金额 decimal billAmount Convert.ToDecimal(this.View.Model.GetValue(FAmount)); // 3. 查询未审核销售订单金额之和排除当前单据本身 string filter $ FCustomerId {customerId} AND FDocumentStatus A AND FBillNo {this.View.Model.GetValue(FBillNo)} ; var result BusinessDataServiceHelper.Query(SAL_SaleOrder, FAmount, filter, null, 0, 0); decimal usedAmount 0; foreach (var item in result) { usedAmount Convert.ToDecimal(item[FAmount]); } decimal totalAmount usedAmount billAmount; if (totalAmount creditLimit) { throw new KDPException($客户信用额度不足当前未审核订单合计{totalAmount.ToString(N2)}元超过信用额度{creditLimit.ToString(N2)}元禁止保存。); } } } }这段代码有几个细节需要好好说。第一异常用KDPException。在金蝶插件里抛出的异常必须是KDPException类型才能被平台正常捕获并转成中文业务提示框。如果你抛一个普通的Exception有时候会被平台当作系统级错误处理界面显示的是一堆英文堆栈信息客户一看就懵了。第二过滤条件里排除了当前单据。编辑状态下当前单据可能还没保存也可能已经保存过比如修改单子时再保存。如果查询结果把当前单子自身也算进去就会造成重复计算。用FBillNo 当前单号来排除是最简单实用的方式。第三这里假设“信用额度”和“订单金额”两个字段都已在元数据里配置好。真实项目里客户档案可能没有“信用额度”字段需要先在BOS设计器里扩展客户档案新增一个基础资料属性。扩展字段的标识建议用金蝶推荐的“F_字母开头”格式比如F_MyCreditLimit避免跟标准字段冲突。4.3 除了表单校验还能怎么扩展这个方案上面这个插件能解决“保存时拦截”的问题。但业务跑起来之后你会发现光有一个校验还不够。比如客户公司想加一条规则如果客户是“战略客户”信用额度超了10%以内可以提示警告但不阻止超10%以外才严格拦截。又比如希望超额度时自动走审批流程而不是直接拒绝。这些场景都可以在同一个插件体系里逐步扩展。警告而不拦截的做法很简单把throw new KDPException换成this.View.ShowMessage或者提示确认框。金蝶表单插件里可以用this.View.ShowTip做气泡提示或者用this.View.SendCommand弹出确认提示。是否继续由用户自己决定你的插件代码只需要在用户点击“确定继续”后放行。做成这样业务灵活度会高很多。再往深做如果客户想“超额自动生成一张超额度审批单”那么BeforeSave里校验逻辑不变在AfterSave事件里另写一个方法调用金蝶的SaveServiceHelper或者审核服务服务生成另一张单据比如自定义的“信用额度审批单”。这种跨单据联动就是服务插件或者表单插件配合服务调用的典型用法。项目上很多“系统自动处理业务”的需求本质上就是这些事件组合起来的。5. 常见问题、坑位与排查清单5.1 插件不生效先查注册再查部署插件开发完部署上去点击操作没反应这是项目群里出现频率最高的问题。排查的时候我建议按下面这个顺序来效率最高检查插件是否注册到正确单据上。BOS设计器打开目标单据确认插件列表和事件订阅都有记录。有时候客户IT在测试环境注册好了发布到生产忘记重新注册一遍也会“不生效”。检查DLL是否部署到正确目录。确认你编译的最新版本DLL已经复制到服务器的Bin目录而且文件名跟注册时填写的程序集名一致。很多坑出在文件名上——项目名改了程序集名变了但BOS设计器里填的还是旧名字系统找不到类型自然不会执行。检查事件订阅是否勾选。这是金蝶插件最容易漏掉的一步。只加了插件列表没有绑定事件OK写再多代码都不会被调用。去BOS设计器的插件事件里重新配置一遍确保你重写的那个方法对应的事件处于勾选状态。检查调用进程。金蝶在Web端和客户端可能走不同的进程。如果你在Web端测试插件的调用进程通常是w3wp.exe如果在Windows客户端上测试可能是另一个进程。DLL更新之后相关进程需要重启才能加载新版本。有时候插件不生效是“旧版本被缓存”导致的。金蝶会缓存元数据DLL更新后最好重启金蝶应用服务或者至少清一下缓存目录。在测试环境里如果重启服务不方便可以尝试把DLL文件名加一个版本号后缀如MyPlugIns_20250312.dll重新注册一下避开缓存问题。这个方法虽然有点土但在现场处理紧急问题时非常管用。5.2 插件执行了两次多半是事件重复绑定插件代码被重复执行也是一个经典问题。最常见的诱发原因是同一个插件类名在BOS设计器里被注册了两遍或者同时绑定在表单级和服务级上导致事件触发时跑了两套相同逻辑。这种情况在大团队协作开发时特别容易发生因为两个开发人员各自开发了功能相近的插件文件名相似部署时没有检查重复。另一个隐蔽原因是表单上有“按钮”和“操作”两条触发链路。比如审核操作界面上点“审核”按钮会触发操作服务但如果你在插件里又手动调用了AuditServiceHelper来审核就可能引发二次执行。排查方法是在插件里加一个静态标识或者在日志里记录执行次数。我踩过最离谱的一次是客户IT把插件DLL放到了服务器上下两个不同的目录金蝶两个进程各自加载了一份导致保存订单时信誉额度校验被跑了两遍提示都弹了两遍。后来清理了多余目录只保留标准Bin目录下的那份问题才解决。所以排查重复执行时不要只盯着代码看先检查服务器上是不是存在多份同名DLL。5.3 性能、事务与并发高调上线低调出坑插件代码是在金蝶主流程里执行的你写得多慢用户就得等多久。尤其是做了跨单据查询的插件如果每次保存单据都要扫描全库客户能明显感觉到“保存变卡了”。在金蝶ERP这种企业级系统里性能劣化往往不是突然出现的而是随着数据量增长慢慢暴露的等客户抱怨的时候数据库里可能已经有几十万条单据了。缓解思路有几个。第一查询尽量走索引字段比如客户ID单据状态第二能取单条聚合值的不要拉全部明细第三不要在插件里循环调用查询服务尽量拼一个批量查询一次取回。我在信用额度校验那段代码里就是只查询了FAmount一个字段而不是把整个动态单据包拉出来这对性能是有帮助的。事务问题也要注意。BeforeSave里抛异常金蝶平台会自动回滚当前保存操作的事务这是标准保证的。但如果你的插件在BeforeSave里自己额外调用了一些写库操作比如插表、改状态而且没有跟主事务保持一致就可能在保存失败时留下脏数据。我的习惯是插件里尽量不要自己写额外的数据变更如果一定要写就在AfterSave事件里做并包好事务或者调用平台服务来完成保证数据一致性。还有并发问题。比如两个销售人员同时为同一个客户录入销售订单保存时都通过了校验但额度明明是固定的拆开看每一张都没超合计却超了。这种情况光靠插件校验无法完全避免除非引入锁机制。不过金蝶标准产品层面的单据在这种场景下一般允许超卖因为加锁会影响正常业务流程。如果你面对的是严控额度的客户可以额外做一把数据库锁在信用额度校验时锁定客户记录行但这属于深度定制我建议谨慎评估后再做。5.4 关于版本兼容和升级维护的几点建议金蝶的插件API在不同版本之间有细微差异尤其是大版本升级的时候某些方法的签名可能会变。比如旧的AbstractFormPlugIn里某个事件参数类型有了新包装导致旧DLL在新平台上报“找不到方法”。我的建议是插件开发做好版本基线管理每次升级前先在测试环境把插件编译一遍跑一遍核心业务测试用例再放行。另外插件项目里的程序集引用要固定版本。不要在服务器上直接引用正在运行的那份DLL文件因为服务器DLL一旦被金蝶自身升级替换你的项目引用也会跟着漂移需要重新编译确认。比较好的做法是在开发机本地保留一份固定的金蝶DLL引用版本并且在代码注释里写上“基于Kingdee Cloud 7.x版本开发”方便后人评估。我个人的经验是真正成熟的插件工程一定是“小而独立”的。单个插件类尽量只做一件事不要一个类里塞一堆逻辑。这样后续改需求只需要改对应类、重新编译发布不影响其他模块。还有插件代码也要写注释和操作说明文档因为现场实施人员通常不是代码原作者他们需要根据文档判断某个插件是做什么的、影响哪个单据。项目交接时一份清晰的插件清单比任何口头讲解都管用。结尾一点个人体会金蝶ERP插件开发这个东西难度其实不吓人核心就是搞清楚三件事什么时候执行事件、在哪里注册配置、怎么部署发布。把这三点吃透大部分客户需求都能用插件体系覆盖。我见过很多开发人员一上来就钻代码细节却忽略了先理解业务流转和平台机制结果代码写得漂亮但挂错位置、触发时机不对来回返工。所以我建议新人学金蝶插件开发时先不要急着写代码花点时间在BOS设计器里把一张标准单据的事件顺序理一遍把“谁在什么时候干什么”背下来开发效率会高得多。这套经验我在好几个项目里验证过对于踩坑排查也特别有用。
返回列表