
简介面向用友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是逻辑删除标记查询条件里平台会自动追加dr0所以不更新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资源目录等标准布局。gif3.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.xmlTrainApplyVO对应主表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数组聚合起来然后调BillSaveBPpublic 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元数据。具体做法是在元数据建模时给主表实体加一个“聚合属性”平台会识别并自动完成1N查询。4.4 审批流怎么挂到这个单据上主子表单据最常见的场景是审批控制。NC65里一个业务单据要挂审批流必须在“业务流程配置”里新建流程选择对应的单据类型然后部署。向导生成的单据编码就是流程配置里的“单据类型”值比如trng_apply。配置流程时审批节点、审批人、条件分支都在流程设计器里画。挂完流程代码层面要在保存后触发流程启动。常见做法是在保存成功的事件里调WorkflowStartBPWorkflowStartBP 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的向导能省掉写代码的时间但省不掉对主子表关系、模板配置和权限模型的完整检查。希望帮到你。本文还有配套的精品资源点击获取