NC65主子表單據(jù):從向?qū)Ы5奖4娌瓤尤馕? alt=)
簡介面向用友UAP NC65開發(fā)新手的一份主子表單據(jù)創(chuàng)建詳解資源圍繞向?qū)ч_發(fā)流程系統(tǒng)講解主表字段控件配置、子表數(shù)據(jù)網(wǎng)格關(guān)聯(lián)、主鍵與外鍵對應(yīng)、主子表聯(lián)動刷新及增刪改查實(shí)現(xiàn)尤其適合剛接觸企業(yè)級表單開發(fā)、需要快速上手UAP平臺的讀者。壓縮包共126個文件體積約4.56MB以37個Java源文件、37個Class編譯文件、16個SQL腳本為主另有properties配置、xml描述及bmf/bpf表單模型文件可配合源代碼逐項(xiàng)理解數(shù)據(jù)庫設(shè)計(jì)、UI布局和數(shù)據(jù)綁定邏輯。目前已有1677人學(xué)習(xí)下載。資源內(nèi)容并非孤立代碼而是將主表查詢條件設(shè)置、子表外鍵指向、保存刷新規(guī)則、測試調(diào)試與性能優(yōu)化等環(huán)節(jié)一并整理配合樣例文件可完整跑通“向?qū)芍髯颖怼钡娜鞒桃材茏鳛楹罄m(xù)NC65二次開發(fā)時的參考模板。1. 用友UAP開發(fā)NC65向?qū)е髯颖韱螕?jù)這套機(jī)制到底解決什么問題做過用友NC65二次開發(fā)的人應(yīng)該都有印象UAP平臺里最讓人又愛又恨的功能就是“向?qū)Ы▎巍薄鄣氖撬_實(shí)能把一張主子表單據(jù)的代碼骨架、模板、按鈕、事件一次性生成出來省掉大量重復(fù)代碼恨的是如果沒搞懂底層規(guī)則向?qū)c(diǎn)完生成的單據(jù)不是保存報(bào)錯就是卡片打不開排查一個晚上不知道問題出在哪。這篇文章就圍繞“用友UAP開發(fā)NC65向?qū)е髯颖韱螕?jù)”完整拆一遍從建表、元數(shù)據(jù)到向?qū)伞⒛0宄跏蓟?、代碼補(bǔ)全再把我自己踩過的坑按現(xiàn)象、原因、解決列出來。適合剛接手NC65二開的開發(fā)者也適合被主子表單據(jù)折騰過的熟手拿來對照排查。2. 建表與元數(shù)據(jù)準(zhǔn)備主子表建模先于代碼2.1 NC65里的主子表數(shù)據(jù)模型長什么樣用友NC65的業(yè)務(wù)單據(jù)基本上都是主子表結(jié)構(gòu)。典型的場景比如采購訂單表頭存供應(yīng)商、單據(jù)號、日期表體存物料、數(shù)量、價格。UAP向?qū)蓡螕?jù)前底層表必須已經(jīng)建好而且要有明確的關(guān)聯(lián)字段。主子表在數(shù)據(jù)庫層面的關(guān)系是子表通過一個外鍵字段指向主表的主鍵。NC65里主表主鍵一般叫pk_xxx子表外鍵字段一般也叫pk_xxx同名字段類型是VARCHAR2長度20或36。這個字段在生成VO和UI時會被平臺自動識別為“主子關(guān)系”的錨點(diǎn)。我見過最典型的翻車就是把子表外鍵字段建成了別的名字向?qū)珊笃脚_根本認(rèn)不出主子關(guān)系子表數(shù)據(jù)永遠(yuǎn)帶不上。建模前還要注意一件事NC65的表一般建議都帶上這五個標(biāo)準(zhǔn)字段——pk主鍵、ts時間戳、dr刪除標(biāo)記、creator創(chuàng)建人、creationtime創(chuàng)建時間。向?qū)蒝O時會直接引用這些字段表里沒有會在保存時拋列不存在的異常。2.2 創(chuàng)建數(shù)據(jù)庫表的DDL實(shí)踐以最常用的Oracle數(shù)據(jù)庫為例我一般會寫這樣一對主子表-- 主表培訓(xùn)申請單 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) ); -- 子表培訓(xùn)人員明細(xì) 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元數(shù)據(jù)識別主子關(guān)系的慣例建議不要改動。主鍵數(shù)據(jù)由誰生成常見做法是平臺統(tǒng)一生成“PK”字符串也可以用Oracle的sys_guid()但要注意如果用了非平臺方式生成主鍵插入前必須顯式設(shè)置VO的pk值否則會出現(xiàn)“主鍵為空”的保存異常。ts字段在向?qū)蒝O后會被代碼自動更新dr是邏輯刪除標(biāo)記查詢條件里平臺會自動追加dr0所以不更新dr值的刪除模型會出問題。2.3 元數(shù)據(jù)建模的完整步驟表建好后要進(jìn)UAP開發(fā)平臺的“元數(shù)據(jù)建模”功能。路徑一般是在開發(fā)平臺左側(cè)的模塊樹上找到自己的模塊右鍵新建“業(yè)務(wù)實(shí)體”。注意這里必須把主表、子表都選上然后建立“主子關(guān)聯(lián)”。操作上是這樣先在實(shí)體列表里新增主實(shí)體對應(yīng)t_train_apply再新增子實(shí)體對應(yīng)t_train_apply_b然后在主實(shí)體的“子實(shí)體”頁簽添加子實(shí)體并設(shè)置關(guān)聯(lián)字段pk_train_apply。保存后平臺會在后臺生成對應(yīng)的實(shí)體元數(shù)據(jù)XML文件。這一步非常關(guān)鍵的點(diǎn)是主實(shí)體的“類型”要選“業(yè)務(wù)單據(jù)”子實(shí)體的“類型”選“業(yè)務(wù)單據(jù)子實(shí)體”。如果選錯后面向?qū)У摹靶略鰡螕?jù)”功能里根本看不到這個實(shí)體。還有一個容易被忽略的設(shè)置——子實(shí)體的“是否主子表體”屬性必須置為“是”否則即使外鍵匹配子表數(shù)據(jù)也不會被平臺當(dāng)成表體處理列表界面不會出現(xiàn)表體編輯區(qū)域。2.4 權(quán)限與集團(tuán)隔離設(shè)置元數(shù)據(jù)保存后緊接著要在“權(quán)限模型”里給這個單據(jù)分配功能節(jié)點(diǎn)權(quán)限。NC65的習(xí)慣是按集團(tuán)、公司、部門等維度做數(shù)據(jù)權(quán)限隔離。如果只建了表、建了實(shí)體沒掛權(quán)限登錄后節(jié)點(diǎn)能看到但點(diǎn)新建按鈕時會提示“無權(quán)操作”。常規(guī)流程是在模塊樹下找到“功能節(jié)點(diǎn)”右鍵新增節(jié)點(diǎn)指向剛創(chuàng)建的實(shí)體然后給節(jié)點(diǎn)分配按鈕權(quán)限。向?qū)傻拇a里默認(rèn)會有增、刪、改、查四個按鈕權(quán)限模型里對應(yīng)著add、delete、update、query四個動作。這一步在向?qū)芍白鲞€是之后做沒有硬性要求但強(qiáng)烈建議先生成后立刻去做避免代碼跑通了但用戶點(diǎn)不動。3. 走向?qū)芍髯颖韱螕?jù)從新建模塊到第一版代碼跑起來3.1 在UAP開發(fā)平臺新建業(yè)務(wù)模塊打開UAP開發(fā)平臺在“模塊管理”里新建模塊。模塊編碼有講究一般用業(yè)務(wù)線縮寫加功能縮寫比如培訓(xùn)管理可以叫trng_mgr模塊編碼會寫進(jìn)類包名和插件配置文件里后期改起來極麻煩寧可花十分鐘想好名字。新建模塊時會要求設(shè)置“模塊依賴”。NC65平臺依賴的基礎(chǔ)模塊一般是uapbd基礎(chǔ)數(shù)據(jù)、pu采購這類。如果業(yè)務(wù)要引用人員檔案還得依賴hrhi。依賴配錯編譯時可能不報(bào)錯但運(yùn)行時加載XML會報(bào)找不到類。這個是我踩過最隱蔽的坑之一因?yàn)橐蕾嚾笔У腻e誤經(jīng)常在發(fā)布時才出現(xiàn)而且日志非常繞。模塊建好后開發(fā)平臺會自動生成一個插件工程目錄。打開后能看到src源碼目錄、META-INF配置文件目錄、resources資源目錄等標(biāo)準(zhǔn)布局。gif3.2 使用“新增單據(jù)”向?qū)芍髯颖韱螕?jù)模塊就緒后在模塊樹的“業(yè)務(wù)組件”上右鍵選擇“新增單據(jù)”向?qū)謳撞阶叩谝徊捷斁幋a和名稱編碼建議與業(yè)務(wù)模塊前綴一致比如trng_apply。第二步選“單據(jù)類型”。這里要選“主子表”而不是“單表”。很多第一次用的人在這里選錯后面表單體區(qū)域出不來。第三步選主實(shí)體和子實(shí)體。下拉框里列出的就是你在元數(shù)據(jù)建模里創(chuàng)建的實(shí)體。如果沒有出現(xiàn)回頭檢查實(shí)體的“類型”和“是否主子表體”屬性。第四步選擇要生成的界面類型。NC65的向?qū)J(rèn)會同時生成“卡片界面”和“列表界面”卡片界面用于新增、編輯時展示單張單據(jù)列表界面用于查詢展示多條單據(jù)。建議全選省得后面手動補(bǔ)。第五步配置按鈕。向?qū)J(rèn)生成新增、修改、刪除、保存、取消、查詢這些標(biāo)準(zhǔn)按鈕??梢栽谶@里加“復(fù)制”“審批”等業(yè)務(wù)按鈕也可以在生成后到按鈕管理里再配。我一般選擇默認(rèn)后續(xù)再改。所有配置完成后點(diǎn)擊生成。平臺會創(chuàng)建一套完整代碼包括主表VO類、子表VO類、單據(jù)UI類、表格UI類、業(yè)務(wù)組件類以及客戶端和服務(wù)端的插件XML。3.3 生成的代碼骨架解讀以Java為例向?qū)傻暮诵奈募笾率沁@樣一組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對應(yīng)主表TrainApplyBVO對應(yīng)子表。打開代碼你會發(fā)現(xiàn)每個VO都繼承了平臺的基礎(chǔ)VO類并實(shí)現(xiàn)了IClass接口。更重要的是主VO里通常會有一個子VO數(shù)組的成員變量比如private TrainApplyBVO[] children;這個children數(shù)組就是主子表在內(nèi)存中的關(guān)聯(lián)方式。保存時平臺會把主表和所有子表一并進(jìn)行更新操作前提是子VO數(shù)組正確設(shè)置。如果手動new了一個子VO但沒有set到主VO的children上子表數(shù)據(jù)不會入庫。FormAction是卡片界面的動作類它繼承平臺自帶的CardFormAction里面會有一個內(nèi)部類繼承CardEventHandler保存、刪除、取消這些事件的默認(rèn)實(shí)現(xiàn)都在這個內(nèi)部類里。向?qū)傻拇a里通常只寫了空殼具體的保存邏輯要自己補(bǔ)。3.4 界面按鈕與事件注冊生成單據(jù)后平臺會生成一套“單據(jù)模板”XML里面定義了卡片界面顯示哪些字段、字段順序、是否只讀等。模板文件一般存放在resources目錄下文件名類似/trng/apply/FormTemplate.xml。按鈕事件注冊是在FormAction的構(gòu)造方法里完成的。向?qū)珊竽J(rèn)是空的需要你把按鈕和對應(yīng)的事件處理器綁定起來public FormAction() throws Exception { super(); // 注冊保存按鈕事件 String saveBtnId getMenuCode(保存); registerListener(saveBtnId, new SaveEventHandler()); // 注冊刪除按鈕事件 String delBtnId getMenuCode(刪除); registerListener(delBtnId, new DeleteEventHandler()); }這里的getMenuCode方法是從界面模板里讀取按鈕編碼按鈕編碼對不上時事件不觸發(fā)現(xiàn)象是點(diǎn)擊保存沒反應(yīng)。排查時先去看模板XML里按鈕的id。4. 把主子單據(jù)跑通保存、刪除、審批流程的默認(rèn)行為4.1 保存邏輯的實(shí)現(xiàn)方式生成的BP類里saveVO方法是一個空殼子需要往里補(bǔ)真正的保存邏輯。主子表單據(jù)保存時先插主表再插子表或者用平臺封裝的方法整單保存。我一般建議用平臺自帶的AggVO整單保存也就是先構(gòu)造AggregateVO把主VO和子VO數(shù)組聚合起來然后調(diào)BillSaveBPpublic Object saveVO(TrainApplyVO vo) throws Exception { // 1. 構(gòu)造主子聚合VO AggregateVO aggVO new AggregateVO(vo, new Object[]{vo.getChildren()}); // 2. 調(diào)用平臺標(biāo)準(zhǔn)保存BP BillSaveBP saveBP new BillSaveBP(); return saveBP.save(aggVO, BillTypeEnum.BILL, BillActionEnum.SAVE); }這段代碼的關(guān)鍵在AggregateVO的構(gòu)造參數(shù)。第一個參數(shù)是主VO第二個參數(shù)是一個二維結(jié)構(gòu)Object[]每個元素對應(yīng)一條子表記錄數(shù)組。如果子表只有一張就是new Object[]{vo.getChildren()}。如果有多張子表這個數(shù)組要按子表順序?qū)?yīng)好比如兩張子表就是new Object[]{bvo1Arr, bvo2Arr}。如果不走整單保存自己寫插入邏輯就要注意主表先插入然后遍歷子VO把主表主鍵set到子表的pk_train_apply字段上再批量插入。這個方案更直觀但要注意事務(wù)控制主子表都必須在同一個事務(wù)里平臺里用DbUtil.getConnection()拿連接后開setAutoCommit(false)。4.2 刪除與主子聯(lián)動的默認(rèn)處理NC65的刪除分為物理刪除和邏輯刪除兩種。平臺的“刪除”按鈕默認(rèn)執(zhí)行邏輯刪除也就是把主表的dr置為1子表也要跟著置為1。向?qū)傻腄eleteEventHandler里其實(shí)已經(jīng)依賴了平臺默認(rèn)的刪除機(jī)制但有個前提——主VO的children必須已經(jīng)加載否則刪除時子表不會被處理。一個典型的坑是在列表界面勾選多條單據(jù)后點(diǎn)刪除平臺會把主表數(shù)據(jù)查出來再逐條調(diào)用刪除事件。如果子表數(shù)據(jù)量大逐條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 (...)這樣做速度快很多但是要自己保證事務(wù)不能用平臺的事件。還有一點(diǎn)因?yàn)閐r置1的話查詢界面默認(rèn)不會顯示如果之后要“反刪除”需要額外開發(fā)。4.3 卡片與列表模板的差異卡片模板管的是新增、編輯界面的布局列表模板管的是查詢界面的列。向?qū)珊筮@兩個模板是分開的XML文件。模板里字段的“顯示”和“編輯”屬性經(jīng)常造成困擾??ㄆ0謇镏鞅碜侄文J(rèn)都可編輯子表字段默認(rèn)也可編輯但列表模板里主表字段大多只是顯示。如果列表模板里把子表字段也加進(jìn)去了查詢結(jié)果里同一行會顯示子表數(shù)據(jù)容易出現(xiàn)“記錄重復(fù)”——因?yàn)橹鞅硪粭l記錄對應(yīng)子表多條列表查詢是join子表查出的自然會出現(xiàn)多行。解決方法是列表查詢不使用join而是主子表分開查在UI層對主表結(jié)果集執(zhí)行聚合。這個邏輯向?qū)Р粫詣由梢约焊膌ist查詢的VO元數(shù)據(jù)。具體做法是在元數(shù)據(jù)建模時給主表實(shí)體加一個“聚合屬性”平臺會識別并自動完成1N查詢。4.4 審批流怎么掛到這個單據(jù)上主子表單據(jù)最常見的場景是審批控制。NC65里一個業(yè)務(wù)單據(jù)要掛審批流必須在“業(yè)務(wù)流程配置”里新建流程選擇對應(yīng)的單據(jù)類型然后部署。向?qū)傻膯螕?jù)編碼就是流程配置里的“單據(jù)類型”值比如trng_apply。配置流程時審批節(jié)點(diǎn)、審批人、條件分支都在流程設(shè)計(jì)器里畫。掛完流程代碼層面要在保存后觸發(fā)流程啟動。常見做法是在保存成功的事件里調(diào)WorkflowStartBPWorkflowStartBP wfBP new WorkflowStartBP(); wfBP.startFlow(aggVO, trng_apply, null);這里第二個參數(shù)是流程編碼必須和流程配置里“流程類型編碼”一致不一致時流程起不來也不會報(bào)明顯錯誤日志里只顯示找不到流程類型。審批過程中如果要對單據(jù)做修改還得處理“審批中鎖定”邏輯平臺提供了BillFlowState這個工具類可以在卡片打開時判斷單據(jù)是否在審批中是的話不可編輯。5. 向?qū)ч_發(fā)常見問題排查六個反復(fù)出現(xiàn)的翻車點(diǎn)5.1 現(xiàn)象新建單據(jù)點(diǎn)保存子表數(shù)據(jù)全部丟失原因主VO和子VO沒有建立內(nèi)存關(guān)聯(lián)。最常見的是在UI層手動new了一個子VO但沒有set到主VO的children數(shù)組上。解決檢查保存按鈕對應(yīng)的事件代碼確保子表數(shù)據(jù)是通過平臺的標(biāo)準(zhǔn)表格組件獲取并組裝成數(shù)組后set進(jìn)主VO的。不要手動new數(shù)據(jù)集再塞參數(shù)直接用表格組件提供的getSelectedData或getData。5.2 現(xiàn)象列表界面點(diǎn)修改卡片能打開但子表不顯示數(shù)據(jù)原因查詢BP只查了主表沒有把子表查出來并聚合成children屬性。解決重寫查詢邏輯先查主表VO再按主表主鍵批量查子表VO然后把子表數(shù)據(jù)按主鍵分組后set到對應(yīng)主VO的children數(shù)組上。如果用了元數(shù)據(jù)聚合屬性檢查元數(shù)據(jù)里有沒有把子實(shí)體掛到主實(shí)體的“聚合查詢”下。5.3 現(xiàn)象向?qū)珊缶幾g失敗報(bào)“找不到父類”原因模塊依賴沒有配置完整。向?qū)傻念惱^承的平臺基類位于其他模塊的jar包里比如nc.uap.wfm相關(guān)類在審批流模塊里如果不依賴該模塊編譯時類找不到。解決回到模塊管理界面在“依賴模塊”里把用到的功能依賴模塊全部勾上。拿不準(zhǔn)時可以參考平臺自帶的示例模塊對比它依賴了哪些基礎(chǔ)模塊。5.4 現(xiàn)象保存時提示“單據(jù)號重復(fù)”原因NC65的單據(jù)號一般在保存時通過單據(jù)號規(guī)則生成。向?qū)傻拇a里默認(rèn)沒有配置單據(jù)號規(guī)則但模板初始化時可能自動套用了某個公共規(guī)則。解決在“單據(jù)號管理”節(jié)點(diǎn)為這個單據(jù)配置獨(dú)立的編碼規(guī)則按年、月、流水號組合。還要注意流水號長度不要太小超過流水長度會跳號報(bào)錯。5.5 現(xiàn)象列表界面點(diǎn)查詢卡片界面的按鈕全部置灰原因功能節(jié)點(diǎn)的按鈕權(quán)限沒有配置。模板的按鈕和權(quán)限模型的按鈕不是同一套體系界面按鈕要真正可用必須在“權(quán)限管理”里給對應(yīng)角色分配該節(jié)點(diǎn)的新增、修改、刪除按鈕權(quán)限。解決登錄后再“系統(tǒng)管理-角色管理-功能權(quán)限”里找到這個節(jié)點(diǎn)把按鈕權(quán)限勾上。建議保存一套開發(fā)用角色把所有按鈕權(quán)限打開。5.6 現(xiàn)象發(fā)布到NC65服務(wù)端后界面還是舊的原因本地開發(fā)環(huán)境改了模板XML或代碼但沒有發(fā)布到服務(wù)器或者發(fā)布時沒有勾選“資源文件”。解決發(fā)布時選擇完整發(fā)布包括class文件、XML模板和權(quán)限元數(shù)據(jù)。熱發(fā)布只更新class失效時先強(qiáng)制清一次客戶端緩存。NC65的客戶端緩存目錄在用戶主目錄下面刪除后重新登錄即可。6. 驗(yàn)證習(xí)慣SQL回查、模板檢查和一條多年驗(yàn)證的主子表檢查清單主子表單據(jù)跑通后驗(yàn)證環(huán)節(jié)反而比開發(fā)環(huán)節(jié)更值得下功夫。我給自己定了一套固定的驗(yàn)證順序每次都按這個走。先做數(shù)據(jù)庫回查。保存一張主子數(shù)據(jù)后用SQL檢查主表和子表是否都存在、子表外鍵是否指向主表主鍵。這條SQL是關(guān)鍵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表查詢?yōu)榭諉栴}多半在children組裝而不是SQL本身。再做模板檢查。打開單據(jù)模板初始化功能把主表字段、子表字段的顯示標(biāo)簽、只讀屬性、必填屬性逐一過一遍。子表的必填屬性特別容易漏導(dǎo)致保存時主表有值子表空著也提示成功。最后做按鈕權(quán)限驗(yàn)證。每次用新角色登錄把新增、修改、刪除、審批、棄審這幾個節(jié)點(diǎn)的權(quán)限全部測試一遍。權(quán)限配置錯誤是最慢暴露的問題因?yàn)榻缑婺艽蜷_、數(shù)據(jù)能正常操作只有換個角色才發(fā)現(xiàn)按鈕灰了。從那以后我每次做完一個主子表單據(jù)都強(qiáng)制走完這個驗(yàn)證流程沒有一次再被半夜的數(shù)據(jù)問題叫起來。經(jīng)驗(yàn)就是UAP的向?qū)苁〉魧懘a的時間但省不掉對主子表關(guān)系、模板配置和權(quán)限模型的完整檢查。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取