简介:《U9二次开发技术资料文档说明》是一份面向U9/U9C实施与开发人员的系统化技术合集,覆盖档案、单据、BE插件、UI插件、接口调用、报表打印等二次开发核心模块,助读者从数据建模到系统集成建立完整思路。压缩包共74个文件,约21.8MB,以docx开发文档、cs源码、pdf说明手册为主,辅以sln工程、xsd结构定义、sql脚本、xml配置等,基本包含开发、调试、部署各环节常用材料。已有1393人学习,资料内既有工作手册与培训课程表,也有第三方调用U9服务等集成案例,参照性强。对想快速上手U9C二次开发的技术人员而言,这份资料能有效缩短项目探索周期,补齐从档案设计到打印报表全流程的实战缺口。 “U9二次开发”这个关键词,在ERP实施圈子里几乎天天被刷到。用友U9(以及后来云化形态的U9C)定位在多组织、多工厂的离散制造和项目制造场景,标准功能再完善,落到具体企业里总会出现配不出来的个性化需求:报表格式要改、审批规则要调、上游系统要对接、甚至业务流程要整体重构。这些光靠实施顾问做配置是不够的,必须通过二次开发来补位。这篇东西我从实际项目里的经验出发,把U9二次开发的整体框架、核心操作、一个可复制的案例、以及那些文档里不会写的坑一次性捋清楚,适合准备接U9项目的开发同事、刚入行的实施顾问、以及甲方内部负责ERP运维的信息化人员参考。
1. 先把U9二次开发的整体脉络捋清楚
1.1 U9二次开发到底在“开”什么
很多人一听“二次开发”,第一反应是“写代码”,这个理解没错,但在U9体系里,写代码只是其中一个环节。U9的二次开发对象大致可以分成六类:单据、列表、报表、插件、接口、工作流。单据层面解决的是“标准界面不够用”,列表层面解决的是“查询和列表列不够直观”,报表层面解决的是“企业要的打印和统计格式标准产品给不了”,插件层面解决的是“某个业务动作发生的前后要挂自定义逻辑”,接口层面解决的是“U9要和MES、WMS、PLM、OA等外部系统做数据交换”,工作流则解决的是“审批路径和标准配置对不上”的情况。
这套分类不是平行的,而是围绕一套核心机制运转的:元数据驱动。U9把业务对象的结构、界面、行为全部描述成元数据,数据库中存的是元数据,界面上渲染的也是元数据,你做的二次开发大部分动作都是在“改元数据”或者“扩展元数据”。这点和很多CAD类软件的二次开发思路很不一样——像NX二次开发、SolidWorks二次开发,本质上是在一个三维几何引擎之上做领域适配,重点在图形算法和参数化建模;而U9二次开发面对的是整条业务链上的数据流和状态流,重点在业务语义、事务一致性、权限、审批这些企业级约束。所以你会发现,U9二次开发更像是在一个“业务操作系统”上做应用扩展,而不是在画图工具里加功能。
1.2 开发平台与工具链:UBF是绕不开的入口
U9二次开发的核心工具是UBF(UAP Business Framework),它不是一个独立安装的IDE,而是集成在Visual Studio里的一套插件式开发平台。打开VS后能看到UBF的菜单和工程模板,新建工程时可以选择实体、单据模板、列表模板、报表、服务等不同类型。技术栈方面,后端是C#和.NET,数据库是SQL Server,U9的系统库和业务库在同一套实例里统一管理。
我遇到过不少半路出家的开发,第一反应是“我能不能直接写SQL改表、加存储过程”,我的建议是:先忍一忍。U9的数据库表结构和元数据之间是强绑定关系,你手动在SQL Server里加一张表或者改一个字段,UBF这边完全感知不到,后续做元数据发布时极容易报错。正确的姿势是:一切的起点都从UBF里的元数据设计开始,让UBF来维护数据库表结构,而不是反过来。这套约束是U9二次开发区别于“写脚本”类开发的最核心差异,理解了它,后面很多问题都能归位。
提示:U9二次开发的核心链路是“元数据 → 实体 → 表单 → 插件 → 发布”。把这条链路刻在脑子里,后面遇到的绝大多数问题都能顺着它排查。
2. U9二次开发的核心细节与实操要点
2.1 单据开发:从元数据到界面的一条线
单据开发是U9二次开发里最基础也最高频的工作。一张完整单据涉及三个层次:元数据定义、实体模型、表单模板。元数据定义描述这个单据有哪些字段、哪些子表、字段类型和长度;实体模型是对数据库表的对象化映射,主表一个实体,子表一个实体,两者通过外键关联;表单模板则是用户实际在界面上看到的布局,包括页签、字段位置、按钮事件。
实际开发中,如果你要给标准单据加一个字段,流程是这样的:在UBF的元数据管理里找到对应的标准单据实体,右键扩展新增字段,设置字段名、显示名、类型,然后发布元数据;UBF会自动在数据库表里增加物理字段。这里有一个特别容易犯的错:字段命名。U9有自己的命名约定,建议扩展字段统一加一个前缀,比如公司简称“HX_”,这样既能在数据库里一眼认出是扩展字段,也能避免将来和标准字段或者别的二开字段撞名。我见过两个实施团队给同一张表加自定义字段,都没有前缀,结果一个叫“Remark”一个叫“Remark1”,后续维护起来非常痛苦。
更重要的是理解主表和子表的关系。以销售订单为例,主表存单据头信息(单据号、客户、订单日期、状态),子表存行信息(物料、数量、价格、交期)。做二次开发时,只要涉及金额、数量的汇总逻辑,几乎都要同时操作主表和子表。很多新手只改了主表字段就做保存,结果子表没同步,数据对不上。记住一条经验:在主表保存逻辑里,如果要校验子表明细,不要自己去查数据库,而是通过实体的导航属性直接拿子表集合,这样既安全又能跟上实物体的内存状态。
2.2 列表模板与报表改造的两种路线
列表模板是用户的“看板”,标准列表往往显示不了所有必要字段,或者查询条件不满足实际需要。改造列表模板在UBF里是可视化操作:拖拽字段、调整列宽、增加查询区。但有一个隐藏点——列表的查询性能。U9的列表通常是通过SQL查询出来的,如果你在列表模板里加了一列关联基础资料的字段,标准查询会做表关联,数据量大时明显变慢。我实测下来的经验是:列表改造尽量只展示当前实体主表字段或者直接冗余存储的字段,少去做跨实体动态关联;确实需要关联的,优先考虑在保存时把关联结果冗余到一张扩展表里。
报表开发有两条路线。一条是用UBF报表向导开发,适合格式要求不太复杂、数据源明确挂在某个单据或查询实体上的场景,优点是和U9的权限体系、打印输出无缝衔接;另一条是直接用SQL做数据源、在第三方报表工具里出报表,适合复杂的统计报表,比如跨单据汇总、多维透视这种。两条路线不冲突,我实际项目里的经验是:对外正式单据(对账单、送货单)优先用UBF报表,保证格式和权限统一;对内管理分析报表,尤其是口径经常变的,直接用SQL加报表工具,迭代效率更高。
2.3 插件:把业务逻辑挂到事件点上
插件是U9二次开发里最核心的扩展机制,它让你能在不修改标准代码的前提下,在业务动作发生的前后挂上自己的逻辑。U9的业务对象在生命周期里有大量事件点,比如单据创建后、保存前、保存后、删除前、审批通过后等。插件类需要实现IEventSubscriber接口,在Notify方法里做事件分发,然后通过UBF的元数据配置把插件注册到对应实体的事件点上。
写插件最重要的一件事:搞清楚事件触发的顺序和重复触发的可能性。我遇到过的情况是,一个单据做了两次“保存并提交”,AfterSave事件被触发两次,结果业务数据被重复生成。正确的写法是先在事件逻辑里做幂等判断,比如按来源单号、来源行ID检查是否已经生成过下游单据,没有才往下走。另外,插件的异常处理一定要严谨,插件里的异常会直接影响主流程的保存或者审批结果,所以对外部系统的调用、对可能出现空值的字段访问,都要做好保护和重试设计。调试时,U9的插件能通过VS附加到客户端进程的方式打断点,但服务器端部署后更稳的做法是输出日志文件,把关键入参、判断结果写到特定目录,方便现场排查。
3. 实操复盘:销售订单保存后自动生成一张扩展单据
3.1 需求场景与设计思路
拿一个我在项目里做过的真实需求来完整走一遍。客户的销售订单在审核通过后,业务员必须手工录入一张“订单附加信息单”,记录一些标准销售订单里没有的信息,比如客户指定的运输注意事项、验收方式、实际对接的联系人。这个动作靠手工做,天天有人漏。排查方案定了两条路:一是给标准销售订单界面加字段,直接扩展;二是做一个独立的自定义单据,然后用插件在销售订单保存后自动生成。最终选了第二种,理由是客户要求这些附加信息在录入后只能由特定部门维护,业务流程上属于独立的审核对象,独立单据更容易控权,也不影响标准销售订单的升级兼容性。
设计上分三个部分:自定义单据实体(主表+一个子表)、事件订阅插件、相关权限和编码规则配置。自定义单据的主表字段包括单据编号、来源单据号、来源单据类型、附加说明;子表存维护记录,记录修改人、修改时间、备注内容。这里用主表加子表的结构,不是为了炫技,而是因为客户要求保留每次变更的痕迹,这是一对多关系,标准单据设计里也是这个套路。
3.2 插件代码怎么落地
插件类核心逻辑不复杂:在销售订单的AfterSave事件里,判断单据状态,调用实体创建逻辑。下面是我实际写的代码骨架,去掉了具体公司相关的命名空间,保留了完整的逻辑结构。
using UFSoft.UBF.Business; using UFSoft.UBF.Eventing; namespace U9DevDemo.Plugins { public class SaleOrderAfterSaveExt : IEventSubscriber { public void Notify(object sender, EventArgs e) { if (!(e is AfterSaveEventArgs afterSave)) { return; } // 注意:实际项目中要根据发布版本确认BusinessEntity的实际类型 // 以及单据状态的枚举值,这里以常见的销售订单实体做演示。 var order = afterSave.BusinessEntity as SaleOrder; if (order == null || order.Status != 2) // 2表示已审核,以实际配置为准 { return; } CreateExtOrder(order); } private void CreateExtOrder(SaleOrder order) { // 幂等检查:来源单号已生成则直接返回,防止重复触发 if (CheckExistsBySource(order.ID)) { return; } using (ISession session = Session.Open()) { ExtOrderDoc doc = ExtOrderDoc.Create(); doc.Code = "EXT" + DateTime.Now.ToString("yyyyMMddHHmmss"); doc.Description = "由销售订单自动生成"; doc.SourceDocID = order.ID; doc.SourceDocNo = order.Code; // 做字段映射时,优先用实体属性而不是直接拼SQL // doc.CustomerName = order.Customer.Name; ExtOrderDocLine line = ExtOrderDocLine.Create(); line.Remark = "初始记录:订单审核通过后自动生成"; line.CreatedTime = DateTime.Now; doc.Lines.Add(line); doc.Save(); session.Commit(); } } } }代码里有几个地方值得单独说明。首先是幂等检查,我在生成扩展单据前会先按来源单据ID查一下是否已存在记录,如果存在就直接跳过。这是防止重复生成的关键一步,没有这个判断,一次事件被触发多次就会产生垃圾数据。其次是字段映射,能走实体属性的就尽量走实体属性,不要直接写SQL去查,一是慢,二是脱离事务上下文容易读到脏数据。第三是使用Session管理事务,整个操作在一个事务里提交,避免出现主单据保存成功但附加单据生成失败的半截状态。
3.3 发布、部署和验证的完整流程
代码写完后,发布流程比普通.NET项目要复杂一点。第一步是在UBF里发布元数据,把新建立的扩展单据实体和插件注册信息同步到服务器元数据库。第二步是编译插件工程,把生成的DLL文件拷贝到U9应用服务器对应目录,并确认插件注册信息指向正确。第三步是处理客户端缓存,U9登录时会加载元数据缓存,如果界面还是看不到新单据,通常需要清一下客户端缓存目录,这个动作在新功能发布后几乎必做。第四步是配置编码规则和权限,新单据没有编码规则时保存会直接报错,没有授权时用户根本看不到菜单。
验证阶段,我的习惯是先建一张测试销售订单,确认生成逻辑正常,然后重点测试两个异常场景:一是保存一张未审核的销售订单,确认不会误触发;二是手动把生成的扩展单据删除后再次审核销售订单,确认插件能基于幂等判断正确重建或跳过。每次验证后看一眼数据库里的生成记录和时间,确认没有重复数据。实测下来,这套流程如果顺利,一次发布从开发到验证大约半天能完成,但第一次做的人往往会在权限和缓存环节多耗掉大半天。
4. 常见问题与排查技巧实录
4.1 元数据和数据库对不上的连锁反应
U9二次开发里最隐蔽、破坏力最大的问题,是元数据和数据库结构不一致。典型症状是:某次发布元数据过程中报错,数据库里字段已经建了,但元数据发布没完成;后续再发布时系统认为字段已存在,跳过建表,但元数据却仍然缺失,结果运行时报表或列表找不到字段。这个情况的排查思路是先定位是“库表有、元数据无”还是“元数据有、库表无”。前者在UBF的元数据管理里重新做一次完整发布即可;后者往往需要删掉对应字段/实体后重新发布,操作前一定先备份。
另一个常见的场景是多人同时开发导致的“撞车”。同一个实体,A开发在本地加了一个字段并发布,B开发拿到旧版本元数据,发布时把A的字段覆盖掉了。我建议团队协作时,所有涉及共享元数据的发布尽量通过一个统一的开发服务器进行,本地只做代码开发,不要反复做全量元数据发布,能规避大量此类问题。
4.2 改了界面却不生效:缓存和发布顺序
“我改完表单模板,发布后客户端打开还是老样子”,这是我反复被问到的问题。九成以上是客户端缓存没有更新。U9客户端登录时会把服务器端元数据缓存到本地,发布新版本后,旧缓存不清理,看到的自然是旧界面。处理方式通常是删除客户端缓存目录后重新登录,或者通过U9自带的缓存更新工具手动刷新。注意在这个动作做完之前,先确认服务器端的元数据确实已经发布成功,否则清多少次缓存都没用。
发布顺序也要讲究。标准做法是:先发布元数据,再发布代码程序集,最后验证客户端。如果先往服务器拷贝了新的DLL,但元数据还没发布,运行时容易出现“类型不匹配”的奇怪错误;反过来先发布元数据再拷贝DLL,一般不会出这种问题。我自己的习惯是严格按“元数据 → 程序集 → 缓存刷新”三步走,每一步做完都做一次最小验证。
4.3 权限、审批流、编码规则三个隐形门槛
很多二次开发功能在开发环境测试一切正常,一上生产就用不了,大概率卡在这三个点。权限问题最直观:新单据、新菜单发布后没有授权到任何角色,用户看不到入口。排查方法是找管理员角色先授权,确认功能正常后再给对应业务角色分配。审批流问题隐蔽一点:自定义单据绑定了审批流,但审批流没有在指定组织生效,或者没有绑定到对应单据类型,提交审批时提示找不到审批路径。这个需要在审批流配置里仔细检查组织范围和单据类型范围。
编码规则是最容易被忽略的。新建单据在保存时,如果系统里没有配置对应编码规则,会直接报“无法获取流水号”。解决方案倒不复杂,在基础设置里给新单据配置好编码规则,再把适用组织范围覆盖完整。我见过项目里新单据开发完,调试了很久都过不去保存这一步,最后发现就是编码规则没配,属于“问题本身不难,但是流程上容易忘记”。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 发布元数据报错 | 库表和元数据不一致 | 先确认字段是否存在,完整重发一次元数据 |
| 新单据在客户端看不见 | 客户端缓存未刷新 | 清缓存目录,或用缓存更新工具刷新 |
| 保存单据提示取不到流水号 | 编码规则未配置 | 在基础设置里配置编码规则,覆盖使用组织 |
| 插件逻辑执行了两次 | 事件重复触发 | 增加幂等判断,按来源单号查重 |
| 插件报错但主单据保存成功 | 插件异常时机 | 检查AfterSave中的异常处理,必要时加日志 |
| 列表查询越来越慢 | 跨实体关联过多 | 减少列表关联字段,考虑冗余存储 |
| 新功能用户看不到菜单 | 权限未授权 | 用高级管理员角色授权后再给业务角色分配 |
把这些坑提前记下来,能省下大量在群里到处问人的时间。
我个人在U9二次开发上最深的体会是:这个领域最难的从来不是写代码,而是理解业务边界和标准产品的设计意图。插件、元数据、报表本身都有明确的技术路径,照着做就能通,但“什么时候改标准流程”、“什么时候该用自定义单据”、“什么时候必须让实施顾问介入调整业务流程”,这些判断才真正决定了二开的成败。如果你准备长期做这块,建议给自己定一个规矩:每接一个新需求,先在UBF里熟悉标准单据的字段和事件链,再动手写第一行代码。这样踩坑的概率会小很多。
本文还有配套的精品资源,点击获取