简介:用友UAP NC65环境下,基于向导开发主子表单据的完整流程与配套源码包,适合正在学习或初次接触企业级表单开发的新手。内容覆盖创建主表、创建子表、主子表关联、交互逻辑、源代码解析、测试调试与优化等核心环节,以实操步骤为主线,帮助读者快速建立从数据库设计到界面联动的整体认知。压缩包共126个文件,大小约4.56MB,其中包含37个Java源代码、37个编译后的class文件、16个SQL脚本,以及properties、xml、bmf/bpf等配置与工程描述文件,源码与数据库脚本配套齐全,便于直接导入开发环境对照学习。已有1673人学习使用,可作系统化自学或项目参考。通过阅读代码与执行文档,可掌握用友UAP平台API调用、控件数据绑定、主子表联动刷新等关键技能,为后续承担更复杂的NC65二次开发任务打下扎实基础。
1. 用友 UAP 开发 NC65 主子表单据:向导只是开始,链路才是重点
在 NC65 上做一张带明细行的主子表单据,很多人第一反应是“跑一下 UAP 向导,点几下就完事”,结果要么菜单里找不到新单据,要么保存时直接报主键为空,要么子表数据怎么都存不进去。用友 UAP 开发 NC65 主子表单据,真正的工作量不在向导,而在后面的整条链路:元数据建模、卡片布局、动作配置、权限分配、发布部署,每一环断了,前端都会以各种奇怪的方式翻车。这篇笔记按我实际做过的顺序,把主子表单据从新建向导到发布验收的完整过程拆开写,关键参数和踩坑点都放在对应位置,适合刚接手 NC65 开发的实施工程师,也适合被主子表折磨过、想系统查漏补缺的开发人员。
2. 环境与向导:建表之前必须确认的四件事
跑向导本身并不难,难的是跑之前没人告诉你的那几件事。环境选错、账号权限不够、中间件连接不上,都会让向导生成之后的发布步骤变成黑匣子。我先说清我一般怎么确认环境,再进向导。
2.1 开发环境:UAP 客户端、数据库账号与中间件的关系
NC65 的主子表单据开发链路是:UAP 客户端连数据库建表,元数据发布写到 NC 中间件,界面模板最终跑在 Web 端。所以开发环境至少要确认三样东西:UAP 客户端版本要和 NC65 补丁版本对应;数据库账号要有建表、建序列的权限;中间件地址要能连通,并且允许发布元数据。
我一般会在正式建表之前先跑一段 SQL,确认当前账号到底有没有建表权限,避免向导走到一半报无权限错误,还得回头找 DBA 开权限。
-- 确认当前数据库账号的关键权限,重点看 CREATE TABLE 和 CREATE SEQUENCE SELECT privilege FROM user_tab_privs WHERE privilege IN ('CREATE TABLE', 'CREATE SEQUENCE', 'CREATE VIEW') ORDER BY privilege;这段 SQL 的逻辑是:以当前登录的数据库用户身份,查询它拥有的表权限和序列权限。正常开发库会返回 CREATE TABLE、CREATE SEQUENCE、CREATE VIEW 等记录;如果返回为空,说明当前账号是只读账号,向导建表这一步过不去。注意权限核查一定要在开发库上做,生产库账号通常不会给建表权限,也不需要给。
顺带确认一个容易忽略的参数:序列。NC65 生成主子表时,主键经常依赖序列来生成,如果账号没有 CREATE SEQUENCE 权限,发布后保存依然会失败。我习惯提前在库里建好专用序列并授权,而不是依赖向导去碰运气,这样能少一次隐藏的翻车。
2.2 向导入口与主子结构选择:先建模再建表
UAP 客户端登录后,在集成开发平台下新建实体,会弹出选择实体结构的向导,常见选项有单表实体、主子实体、树形实体。做主子表单据时选“主子实体”,系统会要求先定义主实体(单据头),再定义子实体(单据体),并约定二者之间的关联字段。这个顺序不能反过来,因为子表必须在主表基础上挂接。
关联字段是这套机制的核心:主表主键(比如 pk_purchase)会被子表引用为外键字段,向导界面里一般叫做“从实体引用主实体主键”。这一步选对,后续子表数据才能和主表挂上关系。选错或者不选,生成出来的子表就是一张孤立的明细表,保存时外键永远为空。
注意:向导阶段不要追求字段一步到位。先把主表主键、子表主键、子表外键、状态字段建好,其它业务字段等实体生成后在元数据编辑器里统一增补,改动成本比在向导里反复回退低得多。
2.3 字段规划:命名、类型与精度的三个约定
UAP 的表结构最终会落到数据库,命名不统一,后续写 SQL、做报表、对接第三方系统都非常痛苦。我给自己定了三条约定,现在基本每张主子表单据都按这个来。
第一条是主键命名。主表主键统一叫 pk_业务模块名,比如 pk_purchase;子表主键叫 pk_业务模块名b,也就是在主表主键后面加一个 b 后缀;子表里的主表引用字段直接复制主表主键字段名。这样从子表 SQL 里一眼就能看出它属于哪张主表。
第二条是数量与金额精度。金额字段用 NUMBER(16,2),单价允许小数多一位的用 NUMBER(16,6),数量用 NUMBER(16,4)。NC65 的很多报表模板对精度有默认预期,乱用 INTEGER 会导致单价小数点后直接截断,对账的时候对不上。
第三条是状态和日期字段。状态字段统一用 SMALLINT,0 表示未保存、1 表示已保存、2 表示已提交,写动作脚本时这样的语义最好记;日期字段要么统一 VARCHAR 存储带格式的字符串,要么统一 DATE 类型,最怕主表用 DATE、子表用 VARCHAR,后面联查和统计会疯掉。
2.4 向导生成后的产物:自动生成的类与配置清单
向导跑完,不只是建了几张表,它会同时生成一套标准化的开发产物。很多新手不知道这些东西是干什么用的,发布失败也不知道从哪里查,我列了一张清单方便对照。
| 产物 | 作用 | 常见问题 |
|---|---|---|
| 数据库表 | 存储主表与子表数据 | 字段类型不对需要手工改表 |
| 持久化 VO 类 | 主表和子表的数据结构映射 | 增删字段后未同步更新 |
| BM 配置文件 | 实体元数据的描述文件 | 元数据发布不完整导致单据无法识别 |
| 界面模板 | 列表模板和卡片模板 | 模板未发布时菜单打开空白 |
| 功能注册记录 | 把单据挂到菜单和权限体系 | 未分配权限时菜单里看不到 |
向导生成后我会先去查一遍实际建表结果,确认表名和字段名跟自己规划的一致。
-- 按表名模糊查询向导生成的表,确认主表和子表都已落库 SELECT table_name FROM user_tables WHERE table_name LIKE 'PK_PURCHASE%' ORDER BY table_name;参数说明:把 PK_PURCHASE 换成你自己的主表前缀即可。返回结果里应该能看到主表和子表至少两张表。如果一张都没返回,说明向导并没有真正执行建表,或者当前数据库账号连接的是另一个库,这是最常见的环境错位问题。
3. 元数据与界面模板:主子关联的灵魂在“主键引用”和“卡片布局”
向导生成的实体只是一副骨架。主子表能不能在界面上正常增删改,取决于元数据里的引用关系配没配好,以及卡片模板里子表区域绑定的数据是否正确。这一章是整套开发里最容易踩坑的地方,我按建模、卡片、列表三层展开。
3.1 元数据建模:子表如何引用主表主键
实体生成后,打开元数据编辑器,能看到主实体和子实体两个对象。关键操作是选中子实体的主表引用字段,把它的引用目标设为主实体的主键字段。这个引用关系不会创建数据库物理外键,但会告诉运行框架:保存子表数据时,用主表主键去填充子表里的这个字段。
我每次都会检查两个配置项:子实体是否勾选了“引用主实体主键”;主实体是否开启了“级联保存子实体”和“级联删除子实体”。级联删除默认是关闭的,如果希望删除主表时连带清掉子表明细,需要在这里显式打开。生产环境尽量谨慎用级联删除,一旦误删,后悔药不好找。
下面这段 Java 代码结构对应元数据配置完成后的 VO,理解它有助于排查后续保存逻辑的问题。
// 主表 VO:一个单据头对应一个 List 的明细 public class PurchaseMainVO { private String pkPurchase; // 主表主键 private String billNo; // 单据号 private Integer billState; // 单据状态:0草稿 1保存 2提交 private List<PurchaseSubVO> subList; // 子表数据集 } // 子表 VO:每一行明细都持有主表主键作为外键 public class PurchaseSubVO { private String pkPurchaseb; // 子表主键 private String pkPurchase; // 引用主表主键,外键字段 private String materialCode; // 物料编码 private BigDecimal quantity; // 数量 private BigDecimal price; // 单价 }这段代码的逻辑说明:主表 VO 里的 subList 是子表数据在内存中的集合,前端卡片把明细行装配到这个 List 里;子表 VO 里的 pkPurchase 必须与主表 pkPurchase 值一致,否则保存后子表记录找不到自己的单据头。参数说明:billState 用 Integer 而不是 String,避免和数字类型比较时报类型错;quantity 和 price 用 BigDecimal,不能用 double 存金额,否则精度会在后续计算中出问题。
3.2 卡片界面模板:主表区、子表区的拖拽顺序
元数据完成后,进入界面模板设计器。新建卡片模板时,系统会自动按主实体字段生成默认布局,但这个默认布局基本都要手工调整。我的固定套路是:上半部分放主表字段,包括单据号、单据类型、制单人、业务日期、备注;下半部分放一个表格区域,表格区域绑定子实体数据集。
子表区域绑定是整个卡片模板的关键。在模板设计器里选中子表表格控件,数据源属性要选择子实体的数据集,而不是主实体的数据集。这一步做错的表现是:卡片打开后明细区域永远空白,或者新增明细行时提示找不到数据源。
卡片模板还有两个属性我每次都会确认:一个是“主子联动”是否开启,开启后选中主表记录自动切换对应明细;另一个是“子表可编辑”状态,如果发布后子表只能看不能改,大概率是这个属性处于只读状态,检查一下模板属性就能定位。
3.3 列表模板与查询模板:主表查询与子表过滤
列表模板负责在菜单打开时展示单据列表。这里要明确一点:列表展示的是主表字段,子表明细不直接出现在列表里,用户通过单击列表行打开卡片查看明细。所以列表模板上把主表的单据号、制单人、状态、业务日期拖进去就行,不要尝试把子表字段拖到列表列上,运行时会取不到值。
查询模板则是列表页顶部的筛选区域。主子表场景下,查询模板经常要增加按子表明细反查主表的条件,比如按物料编码查单据:物料编码属于子表字段,查询时先过滤子实体,再关联回主表。如果查询模板没有配置这层关联,用户按物料编码查单据时就会发现查不到任何结果,这个坑很隐蔽。
3.4 模板发布与默认值设置
模板设计完成并不代表生效。卡片模板和列表模板都要执行发布,并且生成对应的功能节点编码。发布成功后,还要在功能节点分配里把节点授权给测试角色,否则用测试账号登录时菜单里依然找不到单据。
模板上还可以给主表字段配默认值,比如制单人默认当前登录用户、业务日期默认当前日期。这些默认值可以在字段属性的默认值窗口里直接写表达式,也可以用脚本来算。我倾向于能用表达式就不写脚本,表达式简单而且不容易干扰保存动作,同时排错时一眼能看到默认值是什么。
4. 动作与按钮:保存、提交、按钮权限的配置链路
到了这一章,数据模型和界面都齐了,但按钮还是死的。需要使用 UAP 的动作配置机制,把界面按钮和业务动作绑定起来,并处理保存时的级联逻辑。这里也是“保存成功但子表丢失”的重灾区,我详细拆一下。
4.1 动作配置:新建、保存、提交的分工
在界面模板里,工具栏的每个按钮都要绑定一个动作。新建动作负责创建一张空的主表 VO;保存动作负责把主表 VO 和子表 List 持久化;提交动作负责将单据状态从已保存改成已提交,并触发后续流程。动作和数据分开配置,可以在动作集里统一管理,不用在界面模板里逐个写。
一个常见误区是:把业务逻辑直接写在按钮的客户端事件里。UAP 更推荐的方式是服务端动作,客户端按钮只负责触发。写在客户端看着省事,但换一个界面模板就要重新配一遍,而且不同 Web 端的执行行为可能不一致。我见过一个项目把校验逻辑写在按钮点击事件里,结果换了个访问端之后校验完全失效,最后花了一下午把逻辑全部迁回服务端。
4.2 保存动作的关键参数:级联保存与主键回填
保存动作里最重要的两个参数是“是否级联保存子表”和“是否回填外键”。级联保存决定框架要不要遍历主表 VO 的 subList 并逐条插入子表;外键回填决定子表插入时,用哪个字段值填充 pkPurchase。有的单据保存后子表记录能查到但 pkPurchase 为空,就是外键回填配置没生效。
我还会在保存动作里挂一段后置脚本来处理主键和默认状态的赋值。这是这类脚本的典型写法,注意它只处理内存对象,不做数据库操作。
// 保存动作的后置脚本,负责主键、外键和状态补齐 public void beforeSave(PurchaseMainVO main) { if (main.getPkPurchase() == null || main.getPkPurchase().isEmpty()) { main.setPkPurchase(uuid()); // 没有编码规则时用UUID兜底 } main.setBillState(1); // 状态置为已保存 List<PurchaseSubVO> subs = main.getSubList(); if (subs != null) { for (PurchaseSubVO sub : subs) { sub.setPkPurchase(main.getPkPurchase()); // 外键回填 if (sub.getPkPurchaseb() == null || sub.getPkPurchaseb().isEmpty()) { sub.setPkPurchaseb(uuid()); // 子表主键兜底 } } } }这段代码的逻辑说明:先保证主表主键非空,再遍历子表给每一行补外键,最后统一修改状态。参数说明:uuid() 是平台提供的 UUID 生成方法,没有配置单号规则时用它兜底;如果你的环境已经配置了单据号规则,主键可以由编码规则自动生成,脚本里就不需要再设一次,避免单据号被覆盖。
这里要提醒:脚本里只做内存赋值,不要在这里执行 update 或 insert 语句。UAP 保存动作会统一提交事务,手工再执行一次插入容易造成同一单据写了两遍,或者主外键对不上,反而更难查。
4.3 按钮权限与功能节点分配
动作配好后,按钮也不一定马上显示。NC65 的按钮显示受两个条件控制:当前登录用户是否拥有按钮对应的权限码;界面模板中按钮的可用状态是否满足条件。权限码在功能节点注册时生成,需要在角色管理里勾选对应项。
我对按钮权限的习惯是列表和卡片分开控制:列表页允许普通用户查询;卡片页的新增、删除、修改、提交按钮单独授权。这样测试时不会出现角色权限全给完、结果误删数据的尴尬,也方便后续按岗位收缩权限范围。
4.4 工具栏顺序与常用按钮组合
主子表单据的典型按钮顺序是:新增、修改、删除、保存、提交、撤销、刷新。其中“保存”和“提交”是两个独立动作,提交动作执行后建议把保存按钮置灰,防止用户反复提交同一张单据。置灰可以通过按钮的可用状态脚本实现,也可以由单据状态字段直接控制。
有一类问题我遇到很多:按钮存在,但点击后没有任何反应。这时打开浏览器开发者工具看请求,如果请求根本没发出,多半是按钮没有绑定成功动作;如果请求发出但报错,则要看服务端日志。判断这个边界能省掉一大半的排查时间,别一上来就怀疑框架有问题。
5. 避坑排查:从“保存报错”到“列表空白”的五个实战问题
下面这些问题全是实际开发里反复出现的。我按“现象、原因、解决”三个维度写,每一条都是一个完整的小案例。
5.1 环境与发布相关的问题
现象:元数据、界面模板、功能节点全部做了,但用测试账号登录后,菜单里根本找不到新发布的单据;有些节点能找到,但点击后报接口 404。
原因:主要是两个方向:功能节点虽然注册了,但没有执行发布,或者角色菜单权限没有同步;节点编码与界面模板编码不一致,前端加载模板时按节点编码去找对应的列表模板和卡片模板,编码对不上就报 404。
解决:按元数据发布、模板发布、功能节点分配、角色菜单授权这个顺序完整走一遍,不要跳步;然后核对功能节点注册里的模板编码与模板设计器里的模板编码是否完全一致,包括大小写。编码这问题很小,写错一个字母,前端就会直接空白,排查时最容易忽略。
5.2 保存与数据相关的两个高频问题
问题一:保存主表成功,子表一条都没存进去。
现象是单据头能保存,重新打开卡片后明细区为空。原因多半是保存动作里没有开启级联保存子表,或者卡片模板里子表区域的数据源没有绑定到子实体数据集。解决方法是先检查保存动作里的级联保存参数,再检查卡片模板里子表数据源的绑定情况;数据源绑定错误时,前端新增明细行根本不会进入主表 VO 的 subList,自然也就无从保存。
问题二:保存时直接报“主键为空”异常。
现象是点击保存按钮后,服务端日志报主键为空,数据库里也看不到记录。原因有两个方向:主表主键没有配置生成规则,保存时框架拿不到主键;或者前端新增时主键字段被模板设置成了不可见,导致主键一直没参与提交。解决方法是先给主键配置单号规则或用 UUID 兜底,然后去卡片模板确认主键字段是否被隐藏。注意模板隐藏字段导致的问题,光看数据库是看不出来的。
5.3 列表与按钮链接相关的问题
问题一:列表能查到数据,但按钮显示为灰色不可点。
现象是数据都在,但修改、删除这类按钮一直是灰色。原因大多是卡片模板里按钮的状态控制脚本返回了 false,或者当前单据状态不满足按钮的可用条件。解决方法是先看按钮属性里有没有“可用状态脚本”,把脚本条件逐个核对;再确认测试账号对应的角色是否勾选了这个按钮的权限码。这两步都做完了,按钮基本就能正常显示。
问题二:列表查不到刚保存的单据。
现象是保存成功,不刷新时能看到,刷新后列表变空。原因通常是查询模板里配置了默认过滤条件,比如默认只显示本人制单或未提交的单据,而当前测试账号制单人字段为空。解决方法是把查询模板里的过滤条件临时去掉,再判断到底是查询条件问题还是数据权限问题。我习惯先看数据库里记录是否存在:记录存在但列表看不到,就是查询链路的问题;记录根本不存在,才需要回头查保存链路。这个先后顺序能少走很多弯路。
6. 进阶验证:单据号规则与主子表联动验收
前面五章把主子表单据从向导到发布完整走了一遍。最后一章讲一个我每次都会多做一步的收尾动作:配好单据号规则,再针对主子联动做一次完整的联调验收。别小看这一步,它能暴露很多开发时发现不了的数据一致性问题。
单号规则在单据号管理中配置,把主表主键的生成方式从 UUID 换成“前缀+日期+流水号”。例如采购类单据可以配成 CG + 202506 + 四位流水号。这样做的好处是单据号可读,业务人员在列表里一眼能判断单据类型,打印模板也更友好。配置时注意流水号重置周期:一般按日重置或按月重置,跨年不清零会导致单据号越来越长,打印空间会被撑爆。
联调验收我固定跑下面这个流程:新建一张主表,加两行明细,保存后打开列表确认能查到;再次打开卡片,修改第二行明细的数量,保存后再重新打开,确认修改没有丢失;接着把子表明细删掉一行再保存,确认主表数据还在、子表只少一行;最后执行提交动作,确认状态字段从已保存变成已提交。
验收的最后一个动作是跑一段查询 SQL,检查主表与子表的外键关联是否完整。这段 SQL 是我每张主子表开发完都会执行的收尾检查。
-- 联调验收:检查主表与子表的外键完整性,正常应该返回0行 SELECT b.pk_purchase, b.bill_no FROM purchase_main b LEFT JOIN purchase_sub s ON s.pk_purchase = b.pk_purchase WHERE s.pk_purchase IS NULL;这段 SQL 的逻辑说明:以主表为左表按外键去关联子表,如果主表记录找不到任何一行子表明细,说明这张单据的主子关联已经断裂。参数说明:表名替换为你实际的主表和子表表名,外键字段名换成你的实际引用字段。正常情况下返回为空;一旦有记录,保存动作里的外键回填肯定出了问题,需要回到第 4 章去排查。
从那以后,我每次接到主子表单据的需求,都强制自己按“环境确认、向导建模、元数据引用、模板绑定、动作配置、发布授权、联调验收”这套顺序完整走一遍,而且一定先配好单据号再写业务脚本。许多看起来像框架 bug 的问题,最后都出在某个字段没回填、某个模板没绑定、某个权限没分配这种细节上。这套流程不会让开发变得更快,但能让你少加一半的夜班,希望帮到你。
本文还有配套的精品资源,点击获取