ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用友UAP开发NC65主子表单据:从向导建模到保存踩坑全解析

用友UAP开发NC65主子表单据:从向导建模到保存踩坑全解析

简介:面向用友UAP NC65开发新手的一份主子表单据创建详解资源,围绕向导开发流程,系统讲解主表字段控件配置、子表数据网格关联、主键与外键对应、主子表联动刷新及增删改查实现,尤其适合刚接触企业级表单开发、需要快速上手UAP平台的读者。压缩包共126个文件,体积约4.56MB,以37个Java源文件、37个Class编译文件、16个SQL脚本为主,另有properties配置、xml描述及bmf/bpf表单模型文件,可配合源代码逐项理解数据库设计、UI布局和数据绑定逻辑。目前已有1677人学习下载。资源内容并非孤立代码,而是将主表查询条件设置、子表外键指向、保存刷新规则、测试调试与性能优化等环节一并整理,配合样例文件可完整跑通“向导生成主子表”的全流程,也能作为后续NC65二次开发时的参考模板。

1. 用友UAP开发NC65向导主子表单据:这套机制到底解决什么问题

做过用友NC65二次开发的人应该都有印象,UAP平台里最让人又爱又恨的功能就是“向导建单”。爱的是它确实能把一张主子表单据的代码骨架、模板、按钮、事件一次性生成出来,省掉大量重复代码;恨的是如果没搞懂底层规则,向导点完生成的单据不是保存报错就是卡片打不开,排查一个晚上不知道问题出在哪。这篇文章就围绕“用友UAP开发NC65向导主子表单据”完整拆一遍,从建表、元数据到向导生成、模板初始化、代码补全,再把我自己踩过的坑按现象、原因、解决列出来。适合刚接手NC65二开的开发者,也适合被主子表单据折腾过的熟手拿来对照排查。

2. 建表与元数据准备:主子表建模先于代码

2.1 NC65里的主子表数据模型长什么样

用友NC65的业务单据基本上都是主子表结构。典型的场景比如采购订单,表头存供应商、单据号、日期,表体存物料、数量、价格。UAP向导生成单据前,底层表必须已经建好,而且要有明确的关联字段。

主子表在数据库层面的关系是:子表通过一个外键字段指向主表的主键。NC65里,主表主键一般叫pk_xxx,子表外键字段一般也叫pk_xxx(同名字段),类型是VARCHAR2,长度20或36。这个字段在生成VO和UI时会被平台自动识别为“主子关系”的锚点。我见过最典型的翻车,就是把子表外键字段建成了别的名字,向导生成后平台根本认不出主子关系,子表数据永远带不上。

建模前还要注意一件事:NC65的表一般建议都带上这五个标准字段——pk主键、ts时间戳、dr删除标记、creator创建人、creationtime创建时间。向导生成VO时会直接引用这些字段,表里没有会在保存时抛列不存在的异常。

2.2 创建数据库表的DDL实践

以最常用的Oracle数据库为例,我一般会写这样一对主子表:

-- 主表:培训申请单 CREATE TABLE t_train_apply ( pk_train_apply VARCHAR2(36) NOT NULL, billno VARCHAR2(30), dbilldate DATE, pk_dept VARCHAR2(20), memo VARCHAR2(200), ts TIMESTAMP, dr CHAR(1) DEFAULT '0', creator VARCHAR2(36), creationtime TIMESTAMP, CONSTRAINT pk_train_apply PRIMARY KEY (pk_train_apply) ); -- 子表:培训人员明细 CREATE TABLE t_train_apply_b ( pk_train_apply_b VARCHAR2(36) NOT NULL, pk_train_apply VARCHAR2(36) NOT NULL, rowno NUMBER(6), pk_psndoc VARCHAR2(20), train_hours NUMBER(8,2), train_result VARCHAR2(30), ts TIMESTAMP, dr CHAR(1) DEFAULT '0', creator VARCHAR2(36), creationtime TIMESTAMP, CONSTRAINT pk_train_apply_b PRIMARY KEY (pk_train_apply_b) );

主表主键用pk_train_apply,子表主键用pk_train_apply_b,子表外键字段直接命名pk_train_apply,与主表主键同名。这是NC65元数据识别主子关系的惯例,建议不要改动。

主键数据由谁生成?常见做法是平台统一生成“PK”字符串,也可以用Oracle的sys_guid(),但要注意如果用了非平台方式生成主键,插入前必须显式设置VO的pk值,否则会出现“主键为空”的保存异常。ts字段在向导生成VO后会被代码自动更新,dr是逻辑删除标记,查询条件里平台会自动追加dr='0',所以不更新dr值的删除模型会出问题。

2.3 元数据建模的完整步骤

表建好后,要进UAP开发平台的“元数据建模”功能。路径一般是在开发平台左侧的模块树上找到自己的模块,右键新建“业务实体”。注意这里必须把主表、子表都选上,然后建立“主子关联”。

操作上是这样:先在实体列表里新增主实体,对应t_train_apply;再新增子实体,对应t_train_apply_b;然后在主实体的“子实体”页签添加子实体,并设置关联字段pk_train_apply。保存后,平台会在后台生成对应的实体元数据XML文件。

这一步非常关键的点是:主实体的“类型”要选“业务单据”,子实体的“类型”选“业务单据子实体”。如果选错,后面向导的“新增单据”功能里根本看不到这个实体。还有一个容易被忽略的设置——子实体的“是否主子表体”属性,必须置为“是”,否则即使外键匹配,子表数据也不会被平台当成表体处理,列表界面不会出现表体编辑区域。

2.4 权限与集团隔离设置

元数据保存后,紧接着要在“权限模型”里给这个单据分配功能节点权限。NC65的习惯是按集团、公司、部门等维度做数据权限隔离。如果只建了表、建了实体,没挂权限,登录后节点能看到,但点新建按钮时会提示“无权操作”。

常规流程是:在模块树下找到“功能节点”,右键新增节点,指向刚创建的实体,然后给节点分配按钮权限。向导生成的代码里默认会有增、删、改、查四个按钮,权限模型里对应着add、delete、update、query四个动作。这一步在向导生成之前做还是之后做没有硬性要求,但强烈建议先生成后立刻去做,避免代码跑通了但用户点不动。

3. 走向导生成主子表单据:从新建模块到第一版代码跑起来

3.1 在UAP开发平台新建业务模块

打开UAP开发平台,在“模块管理”里新建模块。模块编码有讲究,一般用业务线缩写加功能缩写,比如培训管理可以叫trng_mgr,模块编码会写进类包名和插件配置文件里,后期改起来极麻烦,宁可花十分钟想好名字。

新建模块时会要求设置“模块依赖”。NC65平台依赖的基础模块一般是uapbd(基础数据)、pu(采购)这类。如果业务要引用人员档案,还得依赖hrhi。依赖配错,编译时可能不报错,但运行时加载XML会报找不到类。这个是我踩过最隐蔽的坑之一,因为依赖缺失的错误经常在发布时才出现,而且日志非常绕。

模块建好后,开发平台会自动生成一个插件工程目录。打开后能看到src(源码目录)、META-INF(配置文件目录)、resources(资源目录)等标准布局。gif

3.2 使用“新增单据”向导生成主子表单据

模块就绪后,在模块树的“业务组件”上右键,选择“新增单据”,向导会分几步走:

第一步输编码和名称,编码建议与业务模块前缀一致,比如trng_apply。

第二步选“单据类型”。这里要选“主子表”而不是“单表”。很多第一次用的人在这里选错,后面表单体区域出不来。

第三步选主实体和子实体。下拉框里列出的就是你在元数据建模里创建的实体。如果没有出现,回头检查实体的“类型”和“是否主子表体”属性。

第四步选择要生成的界面类型。NC65的向导默认会同时生成“卡片界面”和“列表界面”,卡片界面用于新增、编辑时展示单张单据,列表界面用于查询展示多条单据。建议全选,省得后面手动补。

第五步配置按钮。向导默认生成新增、修改、删除、保存、取消、查询这些标准按钮。可以在这里加“复制”“审批”等业务按钮,也可以在生成后到按钮管理里再配。我一般选择默认,后续再改。

所有配置完成后,点击生成。平台会创建一套完整代码,包括主表VO类、子表VO类、单据UI类、表格UI类、业务组件类以及客户端和服务端的插件XML。

3.3 生成的代码骨架解读

以Java为例,向导生成的核心文件大致是这样一组:

src/ nc/vo/trng/apply/TrainApplyVO.java nc/vo/trng/apply/TrainApplyBVO.java nc/ui/trng/apply/FormAction.java nc/ui/trng/apply/ListAction.java nc/bs/trng/apply/TrainApplyBP.java nc/bs/trng/apply/TrainApplyQueryBP.java resources/ META-INF/module.xml META-INF/plugin.xml

TrainApplyVO对应主表,TrainApplyBVO对应子表。打开代码你会发现每个VO都继承了平台的基础VO类,并实现了IClass接口。更重要的是,主VO里通常会有一个子VO数组的成员变量,比如:

private TrainApplyBVO[] children;

这个children数组就是主子表在内存中的关联方式。保存时平台会把主表和所有子表一并进行更新操作,前提是子VO数组正确设置。如果手动new了一个子VO但没有set到主VO的children上,子表数据不会入库。

FormAction是卡片界面的动作类,它继承平台自带的CardFormAction,里面会有一个内部类继承CardEventHandler,保存、删除、取消这些事件的默认实现都在这个内部类里。向导生成的代码里通常只写了空壳,具体的保存逻辑要自己补。

3.4 界面按钮与事件注册

生成单据后,平台会生成一套“单据模板”XML,里面定义了卡片界面显示哪些字段、字段顺序、是否只读等。模板文件一般存放在resources目录下,文件名类似/trng/apply/FormTemplate.xml。

按钮事件注册是在FormAction的构造方法里完成的。向导生成后默认是空的,需要你把按钮和对应的事件处理器绑定起来:

public FormAction() throws Exception { super(); // 注册保存按钮事件 String saveBtnId = getMenuCode("保存"); registerListener(saveBtnId, new SaveEventHandler()); // 注册删除按钮事件 String delBtnId = getMenuCode("删除"); registerListener(delBtnId, new DeleteEventHandler()); }

这里的getMenuCode方法是从界面模板里读取按钮编码,按钮编码对不上时事件不触发,现象是点击保存没反应。排查时先去看模板XML里按钮的id。

4. 把主子单据跑通:保存、删除、审批流程的默认行为

4.1 保存逻辑的实现方式

生成的BP类里,saveVO方法是一个空壳子,需要往里补真正的保存逻辑。主子表单据保存时,先插主表再插子表,或者用平台封装的方法整单保存。

我一般建议用平台自带的AggVO整单保存,也就是先构造AggregateVO,把主VO和子VO数组聚合起来,然后调BillSaveBP:

public Object saveVO(TrainApplyVO vo) throws Exception { // 1. 构造主子聚合VO AggregateVO aggVO = new AggregateVO(vo, new Object[]{vo.getChildren()}); // 2. 调用平台标准保存BP BillSaveBP saveBP = new BillSaveBP(); return saveBP.save(aggVO, BillTypeEnum.BILL, BillActionEnum.SAVE); }

这段代码的关键在AggregateVO的构造参数。第一个参数是主VO,第二个参数是一个二维结构Object[],每个元素对应一条子表记录数组。如果子表只有一张,就是new Object[]{vo.getChildren()}。如果有多张子表,这个数组要按子表顺序对应好,比如两张子表就是new Object[]{bvo1Arr, bvo2Arr}。

如果不走整单保存,自己写插入逻辑就要注意:主表先插入,然后遍历子VO,把主表主键set到子表的pk_train_apply字段上,再批量插入。这个方案更直观,但要注意事务控制,主子表都必须在同一个事务里,平台里用DbUtil.getConnection()拿连接后开setAutoCommit(false)。

4.2 删除与主子联动的默认处理

NC65的删除分为物理删除和逻辑删除两种。平台的“删除”按钮默认执行逻辑删除,也就是把主表的dr置为1,子表也要跟着置为1。向导生成的DeleteEventHandler里其实已经依赖了平台默认的删除机制,但有个前提——主VO的children必须已经加载,否则删除时子表不会被处理。

一个典型的坑是:在列表界面勾选多条单据后点删除,平台会把主表数据查出来再逐条调用删除事件。如果子表数据量大,逐条set children再删除效率极慢。我一般在BP里重写删除方法,直接用SQL批量更新:

UPDATE t_train_apply SET dr = '1' WHERE pk_train_apply IN (...) UPDATE t_train_apply_b SET dr = '1' WHERE pk_train_apply IN (...)

这样做速度快很多,但是要自己保证事务,不能用平台的事件。还有一点,因为dr置1的话,查询界面默认不会显示,如果之后要“反删除”,需要额外开发。

4.3 卡片与列表模板的差异

卡片模板管的是新增、编辑界面的布局,列表模板管的是查询界面的列。向导生成后,这两个模板是分开的XML文件。

模板里字段的“显示”和“编辑”属性经常造成困扰。卡片模板里,主表字段默认都可编辑,子表字段默认也可编辑;但列表模板里,主表字段大多只是显示。如果列表模板里把子表字段也加进去了,查询结果里同一行会显示子表数据,容易出现“记录重复”——因为主表一条记录对应子表多条,列表查询是join子表查出的,自然会出现多行。

解决方法是:列表查询不使用join,而是主子表分开查,在UI层对主表结果集执行聚合。这个逻辑向导不会自动生成,要自己改list查询的VO元数据。具体做法是在元数据建模时给主表实体加一个“聚合属性”,平台会识别并自动完成1+N查询。

4.4 审批流怎么挂到这个单据上

主子表单据最常见的场景是审批控制。NC65里,一个业务单据要挂审批流,必须在“业务流程配置”里新建流程,选择对应的单据类型,然后部署。

向导生成的单据编码就是流程配置里的“单据类型”值,比如trng_apply。配置流程时,审批节点、审批人、条件分支都在流程设计器里画。

挂完流程,代码层面要在保存后触发流程启动。常见做法是在保存成功的事件里调WorkflowStartBP:

WorkflowStartBP wfBP = new WorkflowStartBP(); wfBP.startFlow(aggVO, "trng_apply", null);

这里第二个参数是流程编码,必须和流程配置里“流程类型编码”一致,不一致时流程起不来也不会报明显错误,日志里只显示找不到流程类型。审批过程中如果要对单据做修改,还得处理“审批中锁定”逻辑,平台提供了BillFlowState这个工具类,可以在卡片打开时判断单据是否在审批中,是的话不可编辑。

5. 向导开发常见问题排查:六个反复出现的翻车点

5.1 现象:新建单据点保存,子表数据全部丢失

原因:主VO和子VO没有建立内存关联。最常见的是在UI层手动new了一个子VO,但没有set到主VO的children数组上。

解决:检查保存按钮对应的事件代码,确保子表数据是通过平台的标准表格组件获取并组装成数组后set进主VO的。不要手动new数据集再塞参数,直接用表格组件提供的getSelectedData或getData。

5.2 现象:列表界面点修改,卡片能打开但子表不显示数据

原因:查询BP只查了主表,没有把子表查出来并聚合成children属性。

解决:重写查询逻辑,先查主表VO,再按主表主键批量查子表VO,然后把子表数据按主键分组后set到对应主VO的children数组上。如果用了元数据聚合属性,检查元数据里有没有把子实体挂到主实体的“聚合查询”下。

5.3 现象:向导生成后编译失败,报“找不到父类”

原因:模块依赖没有配置完整。向导生成的类继承的平台基类位于其他模块的jar包里,比如nc.uap.wfm相关类在审批流模块里,如果不依赖该模块,编译时类找不到。

解决:回到模块管理界面,在“依赖模块”里把用到的功能依赖模块全部勾上。拿不准时可以参考平台自带的示例模块,对比它依赖了哪些基础模块。

5.4 现象:保存时提示“单据号重复”

原因:NC65的单据号一般在保存时通过单据号规则生成。向导生成的代码里默认没有配置单据号规则,但模板初始化时可能自动套用了某个公共规则。

解决:在“单据号管理”节点为这个单据配置独立的编码规则,按年、月、流水号组合。还要注意流水号长度不要太小,超过流水长度会跳号报错。

5.5 现象:列表界面点查询,卡片界面的按钮全部置灰

原因:功能节点的按钮权限没有配置。模板的按钮和权限模型的按钮不是同一套体系,界面按钮要真正可用,必须在“权限管理”里给对应角色分配该节点的新增、修改、删除按钮权限。

解决:登录后再“系统管理-角色管理-功能权限”里找到这个节点,把按钮权限勾上。建议保存一套开发用角色,把所有按钮权限打开。

5.6 现象:发布到NC65服务端后,界面还是旧的

原因:本地开发环境改了模板XML或代码,但没有发布到服务器,或者发布时没有勾选“资源文件”。

解决:发布时选择完整发布,包括class文件、XML模板和权限元数据。热发布只更新class失效时,先强制清一次客户端缓存。NC65的客户端缓存目录在用户主目录下面,删除后重新登录即可。

6. 验证习惯:SQL回查、模板检查和一条多年验证的主子表检查清单

主子表单据跑通后,验证环节反而比开发环节更值得下功夫。我给自己定了一套固定的验证顺序,每次都按这个走。

先做数据库回查。保存一张主子数据后,用SQL检查主表和子表是否都存在、子表外键是否指向主表主键。这条SQL是关键:

SELECT b.pk_psndoc, b.train_hours, a.billno FROM t_train_apply a LEFT JOIN t_train_apply_b b ON b.pk_train_apply = a.pk_train_apply WHERE a.billno = 'TRNG20250001';

如果b表查询为空,问题多半在children组装,而不是SQL本身。

再做模板检查。打开单据模板初始化功能,把主表字段、子表字段的显示标签、只读属性、必填属性逐一过一遍。子表的必填属性特别容易漏,导致保存时主表有值子表空着也提示成功。

最后做按钮权限验证。每次用新角色登录,把新增、修改、删除、审批、弃审这几个节点的权限全部测试一遍。权限配置错误是最慢暴露的问题,因为界面能打开、数据能正常操作,只有换个角色才发现按钮灰了。

从那以后,我每次做完一个主子表单据都强制走完这个验证流程,没有一次再被半夜的数据问题叫起来。经验就是:UAP的向导能省掉写代码的时间,但省不掉对主子表关系、模板配置和权限模型的完整检查。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进