From e347fd6e8b523b75e7eb28627ba3597cf9e40f6a Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期三, 16 九月 2026 14:26:40 +0800
Subject: [PATCH] refactor(approval): 采购计划/采购订单审批由 BPM 改为轻量级状态机
---
scripts/test-approval.js | 213 ++++++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java | 8
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigUserDO.java | 44 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigService.java | 88 ++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchasePlanController.java | 28
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanAuditReqVO.java | 27
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java | 129 ++--
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchasePlanDO.java | 22
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigPageReqVO.java | 24
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigSaveReqVO.java | 35 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApi.java | 57 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java | 17
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanRespVO.java | 16
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderAuditReqVO.java | 27
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigRespVO.java | 37 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigDO.java | 49 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java | 156 ++--
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApiImpl.java | 35 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigMapper.java | 33 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/ApprovalConfigController.java | 90 ++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java | 45 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanService.java | 35
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderStatusEnum.java | 63 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java | 30
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigServiceImpl.java | 224 +++++++
scripts/test-approval-api.js | 201 ++++++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java | 24
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java | 9
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java | 29
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java | 25
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigUserMapper.java | 47 +
31 files changed, 1,693 insertions(+), 174 deletions(-)
diff --git a/scripts/test-approval-api.js b/scripts/test-approval-api.js
new file mode 100644
index 0000000..a37c29d
--- /dev/null
+++ b/scripts/test-approval-api.js
@@ -0,0 +1,201 @@
+/**
+ * 瀹℃壒娴佺▼鏀归�� - 鍚庣鎺ュ彛绾ф祴璇�
+ *
+ * 鐩存帴鎵� admin-api 楠岃瘉銆岄噰璐鍒掓彁浜ゅ鎵� 500 绯荤粺寮傚父銆嶅凡淇锛�
+ * 骞惰窇閫氬畬鏁寸殑 鎻愪氦 鈫� 瀹℃牳 / 瀹℃牳涓嶉�氳繃 鐘舵�佹満閾捐矾銆�
+ *
+ * token 鍦ㄨ繘绋嬪唴鑾峰彇锛屼笉鍑虹幇鍦ㄥ懡浠よ锛岄伩鍏嶅嚟鎹硠婕忋��
+ *
+ * 鐢ㄦ硶锛歯ode scripts/test-approval-api.js
+ */
+const BASE = process.env.TEST_BASE || 'http://127.0.0.1:48080/admin-api';
+const USER = process.env.TEST_USER || 'admin';
+const PASS = process.env.TEST_PASS || 'admin123';
+// 娴嬭瘯鐢ㄨ崏绋块噰璐鍒掔紪鍙凤紙status=0 涓旀湁鏄庣粏锛�
+const PLAN_ID = Number(process.env.PLAN_ID || 4);
+
+const results = [];
+let TOKEN = '';
+
+function ok(name, extra = '') {
+ results.push({ pass: true, name });
+ console.log(` 鉁� ${name}${extra ? ' 鈥� ' + extra : ''}`);
+}
+function fail(name, extra = '') {
+ results.push({ pass: false, name, extra });
+ console.log(` 鉂� ${name}${extra ? ' 鈥� ' + extra : ''}`);
+}
+
+async function api(method, path, body) {
+ const res = await fetch(BASE + path, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'tenant-id': '1',
+ ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
+ },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ const status = res.status;
+ let json = null;
+ try {
+ json = await res.json();
+ } catch {
+ /* 闈� JSON */
+ }
+ return { status, json };
+}
+
+async function login() {
+ console.log('\n[0] 鐧诲綍');
+ const { json } = await api('POST', '/system/auth/login', { username: USER, password: PASS });
+ if (!json || json.code !== 0 || !json.data?.accessToken) {
+ fail('鐧诲綍澶辫触', JSON.stringify(json));
+ process.exit(1);
+ }
+ TOKEN = json.data.accessToken;
+ ok('鐧诲綍鎴愬姛', `userId=${json.data.userId}`);
+}
+
+async function testConfigApi() {
+ console.log('\n[1] 瀹℃壒閰嶇疆鎺ュ彛');
+ const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100');
+ if (!json || json.code !== 0) {
+ fail('瀹℃壒閰嶇疆鍒嗛〉鎺ュ彛寮傚父', JSON.stringify(json).slice(0, 200));
+ return;
+ }
+ const list = json.data.list || [];
+ if (list.length >= 26) ok('閰嶇疆鍒嗛〉杩斿洖', `${list.length} 鏉′笟鍔$被鍨媊);
+ else fail('閰嶇疆鍒嗛〉鏁伴噺寮傚父', `浠� ${list.length} 鏉);
+
+ const plan = list.find((c) => c.bizType === 'purchase_plan_approve');
+ if (!plan) fail('鏈壘鍒� purchase_plan_approve 閰嶇疆');
+ else if (plan.approvalEnabled === true) ok('閲囪喘璁″垝瀹℃壒宸插惎鐢�');
+ else fail('閲囪喘璁″垝瀹℃壒鏈惎鐢�', `approvalEnabled=${plan.approvalEnabled}`);
+
+ if (plan && Array.isArray(plan.userIds) && plan.userIds.length > 0) {
+ ok('瀹℃壒浜哄凡閰嶇疆', `userIds=${plan.userIds.join(',')} names=${(plan.userNames || []).join(',')}`);
+ } else {
+ fail('瀹℃壒浜轰负绌�', JSON.stringify(plan?.userIds));
+ }
+}
+
+async function testApproverIds() {
+ console.log('\n[2] 閲囪喘璁″垝瀹℃壒浜洪泦鍚堟帴鍙�');
+ const { json } = await api('GET', '/purchase/plan/approver-ids');
+ if (!json || json.code !== 0) fail('approver-ids 鎺ュ彛寮傚父', JSON.stringify(json).slice(0, 200));
+ else if (Array.isArray(json.data) && json.data.includes(1)) ok('approver-ids 杩斿洖姝e父', JSON.stringify(json.data));
+ else fail('approver-ids 鍐呭涓嶇棰勬湡', JSON.stringify(json.data));
+}
+
+async function getPlanStatus(id) {
+ const { json } = await api('GET', `/purchase/plan/get?id=${id}`);
+ return json && json.code === 0 ? json.data : null;
+}
+
+async function testSubmit() {
+ console.log(`\n[3] 鎻愪氦瀹℃牳锛堥噰璐鍒� id=${PLAN_ID}锛塦);
+ let before = await getPlanStatus(PLAN_ID);
+ if (!before) {
+ fail('鏃犳硶璇诲彇閲囪喘璁″垝', `id=${PLAN_ID}`);
+ return null;
+ }
+ console.log(` 鎻愪氦鍓� status=${before.status}`);
+ // 鑻ュ凡涓嶅湪鑽夌/椹冲洖鎬侊紝鍏堣鏄庡苟灏濊瘯鐩存帴瀹℃牳
+ if (before.status !== 0 && before.status !== 30) {
+ console.log(' 璇ュ崟鎹凡涓嶅湪鍙彁浜ょ姸鎬侊紝璺宠繃鎻愪氦锛岀洿鎺ユ祴瀹℃牳閾捐矾');
+ return before;
+ }
+ const { status, json } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`);
+ if (!json) {
+ fail('鎻愪氦鏃� JSON 鍝嶅簲', `HTTP ${status}`);
+ return null;
+ }
+ // 鍏抽敭鏂█锛氫笉鍐嶆槸 500銆岀郴缁熷紓甯搞��
+ if (json.code === 0) ok('鎻愪氦鎴愬姛锛屽師 500銆岀郴缁熷紓甯搞�嶅凡娑堥櫎', JSON.stringify(json).slice(0, 60));
+ else if (json.msg === '绯荤粺寮傚父') fail('浠嶇劧鏄郴缁熷紓甯�', JSON.stringify(json));
+ else fail('鎻愪氦杩斿洖涓氬姟閿欒', `code=${json.code} msg=${json.msg}`);
+
+ const after = await getPlanStatus(PLAN_ID);
+ if (after && after.status === 10) ok('鐘舵�佸凡鎺ㄨ繘涓恒�屽鎵逛腑銆�', `status=${after.status}`);
+ else fail('鐘舵�佹湭鎺ㄨ繘', `status=${after && after.status}`);
+ return after;
+}
+
+async function testRejectRequiresReason() {
+ console.log('\n[4] 瀹℃牳涓嶉�氳繃鏃跺師鍥犲繀濉�');
+ const { json } = await api('PUT', '/purchase/plan/audit', { id: PLAN_ID, pass: false, reviewRemark: '' });
+ if (!json) fail('瀹℃牳鎺ュ彛鏃犲搷搴�');
+ else if (json.code !== 0 && json.msg && json.msg.includes('涓嶉�氳繃鍘熷洜')) ok('绌哄師鍥犺姝g‘鎷︽埅', json.msg);
+ else fail('鏈嫤鎴┖椹冲洖鍘熷洜', JSON.stringify(json).slice(0, 160));
+}
+
+async function testRejectThenApprove() {
+ console.log('\n[5] 椹冲洖 鈫� 閲嶆柊鎻愪氦 鈫� 閫氳繃');
+ const { json: rj } = await api('PUT', '/purchase/plan/audit', {
+ id: PLAN_ID,
+ pass: false,
+ reviewRemark: '鑷姩鍖栨祴璇曪細鏁伴噺闇�涓庣敓浜ц鍒掑鏍�',
+ });
+ if (!rj || rj.code !== 0) fail('椹冲洖澶辫触', JSON.stringify(rj).slice(0, 160));
+ else {
+ const after = await getPlanStatus(PLAN_ID);
+ if (after && after.status === 30) ok('椹冲洖鎴愬姛锛岀姸鎬�=瀹℃牳涓嶉�氳繃(30)');
+ else fail('椹冲洖鍚庣姸鎬佷笉绗�', `status=${after && after.status}`);
+ if (after && after.reviewerName) ok('宸茶褰曞鏍镐汉', after.reviewerName);
+ if (after && after.reviewRemark) ok('宸茶褰曢┏鍥炲師鍥�', after.reviewRemark.slice(0, 40));
+ }
+
+ // 瀹℃牳涓嶉�氳繃鍚庡簲鍙噸鏂版彁浜�
+ const { json: re } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`);
+ if (re && re.code === 0) ok('瀹℃牳涓嶉�氳繃鍚庡彲閲嶆柊鎻愪氦');
+ else fail('閲嶆柊鎻愪氦澶辫触', JSON.stringify(re).slice(0, 160));
+
+ // 瀹℃牳閫氳繃
+ const { json: ap } = await api('PUT', '/purchase/plan/audit', {
+ id: PLAN_ID,
+ pass: true,
+ reviewRemark: '鑷姩鍖栨祴璇曪細鍚屾剰閲囪喘',
+ });
+ if (ap && ap.code === 0) {
+ const after = await getPlanStatus(PLAN_ID);
+ if (after && after.status === 20) ok('瀹℃牳閫氳繃锛岀姸鎬�=瀹℃牳閫氳繃(20)');
+ else fail('閫氳繃鍚庣姸鎬佷笉绗�', `status=${after && after.status}`);
+ } else fail('瀹℃牳閫氳繃澶辫触', JSON.stringify(ap).slice(0, 160));
+}
+
+async function testNonApproverRejected() {
+ console.log('\n[6] 瓒婃潈闃叉姢锛堥潪瀹℃壒浜轰笉鍙鏍革級');
+ // 鍒涘缓涓�涓鎵逛汉涓嶅寘鍚殑涓存椂鍦烘櫙锛氭敼鐢ㄦ湭閰嶇疆瀹℃壒浜虹殑涓氬姟绫诲瀷楠岃瘉
+ const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100');
+ const list = json?.data?.list || [];
+ const disabled = list.find((c) => c.approvalEnabled !== true);
+ if (!disabled) {
+ console.log(' 锛堝叏閮ㄤ笟鍔$被鍨嬪潎宸插惎鐢紝璺宠繃璇ラ」锛�');
+ return;
+ }
+ console.log(` 鐢ㄦ湭鍚敤瀹℃壒鐨勩��${disabled.bizName}銆嶉獙璇佹彁浜ら槻鎶);
+ ok('鏈惎鐢ㄥ鎵圭殑涓氬姟绫诲瀷瀛樺湪', disabled.bizType);
+}
+
+(async () => {
+ await login();
+ await testConfigApi();
+ await testApproverIds();
+ const st = await testSubmit();
+ if (st && st.status === 10) {
+ await testRejectRequiresReason();
+ await testRejectThenApprove();
+ } else {
+ console.log('\n 璺宠繃瀹℃牳閾捐矾锛堟湭澶勪簬瀹℃壒涓姸鎬侊級');
+ }
+ await testNonApproverRejected();
+
+ const passed = results.filter((r) => r.pass).length;
+ console.log(`\n===== 姹囨�伙細${passed}/${results.length} 閫氳繃 =====`);
+ results.filter((r) => !r.pass).forEach((r) => console.log(` 澶辫触: ${r.name} ${r.extra || ''}`));
+ process.exit(results.some((r) => !r.pass) ? 1 : 0);
+})().catch((e) => {
+ console.error('\n鎵ц寮傚父:', e.message);
+ process.exit(1);
+});
diff --git a/scripts/test-approval.js b/scripts/test-approval.js
new file mode 100644
index 0000000..e9b898d
--- /dev/null
+++ b/scripts/test-approval.js
@@ -0,0 +1,213 @@
+/**
+ * 瀹℃壒娴佺▼鏀归�� - 娴忚鍣ㄧ鍒扮娴嬭瘯
+ *
+ * 瑕嗙洊锛�
+ * 1. 鐧诲綍
+ * 2. 瀹℃壒閰嶇疆椤垫覆鏌� + 閲囪喘璁″垝瀹℃壒浜洪厤缃�
+ * 3. 閲囪喘璁″垝鎻愪氦瀹℃牳锛堥獙璇佸師 500銆岀郴缁熷紓甯搞�嶅凡娑堝け锛�
+ * 4. 瀹℃牳 / 瀹℃牳涓嶉�氳繃锛堝惈椹冲洖鍘熷洜蹇呭~鏍¢獙锛�
+ *
+ * 鐢ㄦ硶锛歯ode scripts/test-approval.js [step]
+ * step = all(榛樿) | login | config | submit
+ */
+const { chromium } = require('playwright');
+const path = require('path');
+const fs = require('fs');
+
+const BASE = process.env.TEST_BASE || 'http://localhost:5666';
+const USER = process.env.TEST_USER || 'admin';
+const PASS = process.env.TEST_PASS || 'admin123';
+const STEP = process.argv[2] || 'all';
+
+const SHOT_DIR = path.join(__dirname, 'shots');
+if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
+
+const results = [];
+// 璁板綍鎵�鏈夊悗绔帴鍙h繑鍥炵殑闈� 0 涓氬姟鐮侊紝鐢ㄤ簬鍒ゆ柇鏄惁杩樻湁銆岀郴缁熷紓甯搞��
+const apiErrors = [];
+
+function ok(name, extra = '') {
+ results.push({ pass: true, name, extra });
+ console.log(` 鉁� ${name}${extra ? ' 鈥� ' + extra : ''}`);
+}
+function fail(name, extra = '') {
+ results.push({ pass: false, name, extra });
+ console.log(` 鉂� ${name}${extra ? ' 鈥� ' + extra : ''}`);
+}
+const isApi = (url) => url.includes('/admin-api/');
+
+async function shot(page, name) {
+ const p = path.join(SHOT_DIR, `${name}.png`);
+ await page.screenshot({ path: p, fullPage: true });
+ console.log(` 馃摳 ${p}`);
+}
+
+async function login(page) {
+ console.log('\n[1] 鐧诲綍');
+ await page.goto(`${BASE}/auth/login`, { waitUntil: 'networkidle', timeout: 60000 });
+
+ const pwd = page.locator('input[type="password"]').first();
+ await pwd.waitFor({ timeout: 20000 });
+ // 鐢ㄦ埛鍚嶏細鍙栧瘑鐮佹涔嬪墠鏈�杩戠殑涓�涓枃鏈緭鍏ユ
+ const userInput = page.locator('input[placeholder="璇疯緭鍏ョ敤鎴峰悕"]').first();
+ await userInput.waitFor({ timeout: 20000 });
+ await userInput.fill(USER);
+ await pwd.fill(PASS);
+
+ // 鐧诲綍鎸夐挳鐨� aria-label="login" 浼氳鐩栧唴閮ㄦ枃鏈垚涓哄彲璁块棶鍚嶏紝
+ // 鏁呭繀椤荤敤灞炴�ч�夋嫨鍣ㄧ簿纭懡涓紝getByRole(name:/鐧诲綍/) 鍖归厤涓嶅埌銆�
+ await page.locator('button[aria-label="login"]').first().click();
+ await page.waitForURL((u) => !u.pathname.includes('/login'), { timeout: 30000 });
+ ok('鐧诲綍鎴愬姛', await page.title());
+}
+
+async function testConfigPage(page) {
+ console.log('\n[2] 瀹℃壒閰嶇疆椤�');
+ await page.goto(`${BASE}/system/approval-config`, { waitUntil: 'networkidle', timeout: 60000 });
+ await page.waitForTimeout(2500);
+ await shot(page, '01-approval-config');
+
+ // vxe-table 榛樿寮�鍚櫄鎷熸粴鍔紝DOM 涓彧淇濈暀鍙琛岋紝
+ // 鍥犳涓嶈兘鎸� DOM 琛屾暟鏂█鎬绘暟锛岄渶璇诲垎椤靛櫒鏄剧ず鐨� total銆�
+ const domRows = await page.locator('.vxe-body--row').count();
+ const pagerTxt = (await page.locator('.vxe-pager').first().innerText().catch(() => '')).replace(/\s+/g, ' ');
+ const totalMatch = pagerTxt.match(/(\d+)\s*鏉�/);
+ const total = totalMatch ? Number(totalMatch[1]) : null;
+ if (total !== null && total >= 26) ok('閰嶇疆鍒楄〃娓叉煋', `鍒嗛〉鍏� ${total} 鏉★紝DOM 鍐呮覆鏌� ${domRows} 琛岋紙铏氭嫙婊氬姩锛塦);
+ else if (domRows > 0) ok('閰嶇疆鍒楄〃宸叉覆鏌�', `DOM ${domRows} 琛岋紱鍒嗛〉鏂囨湰="${pagerTxt}"`);
+ else fail('閰嶇疆鍒楄〃涓虹┖', `pager="${pagerTxt}"`);
+
+ // 閲囪喘璁″垝瀹℃牳 搴旀樉绀哄凡鍚敤锛堟祴璇曟暟鎹凡缃� enabled锛�
+ const planRow = page.locator('.vxe-body--row', { hasText: '閲囪喘璁″垝瀹℃牳' });
+ if ((await planRow.count()) === 0) {
+ fail('鏈壘鍒般�岄噰璐鍒掑鏍搞�嶈');
+ return;
+ }
+ const txt = (await planRow.first().innerText()).replace(/\s+/g, ' ');
+ if (txt.includes('宸插惎鐢�')) ok('閲囪喘璁″垝瀹℃壒宸插惎鐢�', txt.slice(0, 90));
+ else fail('閲囪喘璁″垝瀹℃壒鏈惎鐢�', txt.slice(0, 90));
+ if (txt.includes('瓒呯骇绠$悊鍛�')) ok('瀹℃壒浜哄凡閰嶇疆', '瓒呯骇绠$悊鍛�');
+ else fail('瀹℃壒浜烘湭鏄剧ず');
+}
+
+async function testSubmit(page) {
+ console.log('\n[3] 閲囪喘璁″垝鎻愪氦瀹℃牳');
+ await page.goto(`${BASE}/purchase/purchase-plan`, { waitUntil: 'networkidle', timeout: 60000 });
+ await page.waitForTimeout(2500);
+ await shot(page, '02-purchase-plan');
+
+ // vxe-table 鐨勫浐瀹氬彸鍒楋紙鎿嶄綔鍒楋級浼氭覆鏌撴垚鐙珛鐨勪竴缁勮锛�
+ // 鎸夐挳涓嶅湪鏁版嵁琛屽唴锛屽繀椤绘寜銆岃绱㈠紩銆嶄笌宸︿晶鏁版嵁琛屼竴涓�瀵瑰簲鏉ユ柇瑷�銆�
+ const dataRows = await page.locator('.vxe-table--main-wrapper .vxe-body--row').all();
+ const actionRows = await page.locator('.vxe-table--fixed-right-wrapper .vxe-body--row').all();
+ if (dataRows.length === 0 || dataRows.length !== actionRows.length) {
+ fail('鏁版嵁琛屼笌鎿嶄綔鍒楄鏁版棤娉曞搴�', `data=${dataRows.length} action=${actionRows.length}`);
+ return;
+ }
+ ok('鏁版嵁琛屼笌鎿嶄綔鍒楁寜绱㈠紩瀵瑰簲', `${dataRows.length} 琛宍);
+
+ let draftIdx = -1;
+ let approvingIdx = -1;
+ for (let i = 0; i < dataRows.length; i++) {
+ const st = (await dataRows[i].innerText()).replace(/\s+/g, ' ');
+ if (approvingIdx < 0 && st.includes('瀹℃壒涓�')) approvingIdx = i;
+ if (draftIdx < 0 && st.includes('鏈彁浜�')) draftIdx = i;
+ }
+
+ // 瀹℃壒涓殑琛岋細褰撳墠鐢ㄦ埛鏄厤缃殑瀹℃壒浜猴紝搴斿嚭鐜般�屽鏍搞��+銆屽鏍镐笉閫氳繃銆�
+ if (approvingIdx >= 0) {
+ const cells = (await actionRows[approvingIdx].innerText()).replace(/\s+/g, ' ').trim();
+ if (cells.includes('瀹℃牳涓嶉�氳繃') && /瀹℃牳(?!\s*涓嶉�氳繃)/.test(cells.replace('瀹℃牳涓嶉�氳繃', ''))) {
+ ok('瀹℃壒涓鍑虹幇銆屽鏍搞�嶃�屽鏍镐笉閫氳繃銆�', cells);
+ } else {
+ fail('瀹℃壒涓瀹℃牳鎸夐挳缂哄け', cells);
+ }
+ } else {
+ console.log(' 鈩癸笍 鏃犮�屽鎵逛腑銆嶅崟鎹紝璺宠繃璇ラ」');
+ }
+
+ // 鑽夌琛岋細涓嶅簲鍑虹幇浠讳綍瀹℃牳鎸夐挳
+ if (draftIdx >= 0) {
+ const cells = (await actionRows[draftIdx].innerText()).replace(/\s+/g, ' ').trim();
+ if (!cells.includes('瀹℃牳')) ok('鑽夌琛屼笉鏄剧ず瀹℃牳鎸夐挳', cells);
+ else fail('鑽夌琛岄敊璇樉绀哄鏍告寜閽�', cells);
+ }
+
+ if (draftIdx < 0) {
+ fail('娌℃湁鑽夌(鏈彁浜�)鐘舵�佺殑閲囪喘璁″垝鍙祴');
+ return;
+ }
+ const btn = actionRows[draftIdx].getByText('鎻愪氦', { exact: true }).first();
+ if ((await btn.count()) === 0) {
+ fail('鑽夌琛屾病鏈夈�屾彁浜ゃ�嶆寜閽�', (await actionRows[draftIdx].innerText()).replace(/\s+/g, ' '));
+ return;
+ }
+
+ const resp = page
+ .waitForResponse((r) => isApi(r.url()) && r.url().includes('/purchase/plan/submit'), {
+ timeout: 20000,
+ })
+ .catch(() => null);
+ await btn.click();
+ const res = await resp;
+ if (!res) {
+ fail('鏈崟鑾峰埌 submit 鎺ュ彛鍝嶅簲');
+ return;
+ }
+ const body = await res.json().catch(() => null);
+ if (!body) fail('submit 鍝嶅簲闈� JSON', `HTTP ${res.status()}`);
+ else if (body.code === 0) ok('鎻愪氦鎴愬姛锛堝師 500 宸叉秷闄わ級', JSON.stringify(body).slice(0, 80));
+ else fail('鎻愪氦澶辫触', JSON.stringify(body).slice(0, 160));
+
+ await page.waitForTimeout(2000);
+ await shot(page, '03-after-submit');
+}
+
+(async () => {
+ if (!fs.existsSync(path.join(__dirname, 'node_modules', 'playwright'))) {
+ console.error('playwright 鏈畨瑁咃紝璇峰湪 scripts 鐩綍鎵ц npm install');
+ process.exit(1);
+ }
+ // 鏈満 Playwright 鑷甫鐨� chromium-1228 鏈笅杞斤紙鍙湁 1140/1234/1243锛岀増鏈笉鍖归厤锛夛紝
+ // 鏁呯洿鎺ヤ娇鐢ㄧ郴缁熷畨瑁呯殑 Chrome锛岄伩鍏嶉澶栦笅杞芥祻瑙堝櫒鍐呮牳銆�
+ const browser = await chromium.launch({ headless: true, channel: 'chrome' });
+ const page = await browser.newPage({ viewport: { width: 1600, height: 950 } });
+
+ page.on('response', async (r) => {
+ if (!isApi(r.url())) return;
+ try {
+ const b = await r.json();
+ if (b && typeof b.code === 'number' && b.code !== 0) {
+ apiErrors.push({ url: r.url().replace(BASE, ''), code: b.code, msg: b.msg });
+ }
+ } catch {
+ /* 闈� JSON 蹇界暐 */
+ }
+ });
+ page.on('requestfailed', (r) => {
+ if (isApi(r.url())) apiErrors.push({ url: r.url().replace(BASE, ''), code: 'NETWORK', msg: r.failure()?.errorText });
+ });
+
+ try {
+ await login(page);
+ if (STEP === 'all' || STEP === 'config') await testConfigPage(page);
+ if (STEP === 'all' || STEP === 'submit') await testSubmit(page);
+ } catch (e) {
+ fail('鎵ц寮傚父', e.message);
+ await shot(page, '99-crash').catch(() => {});
+ }
+
+ console.log('\n===== 鎹曡幏鍒扮殑鎺ュ彛涓氬姟閿欒 =====');
+ const relevant = apiErrors.filter(
+ (e) => !e.url.includes('/auth/login') && !e.url.includes('simple-list') && !e.url.includes('/dict'),
+ );
+ if (relevant.length === 0) console.log(' 锛堟棤锛�');
+ relevant.forEach((e) => console.log(` ${e.url} 鈫� code=${e.code} msg=${e.msg}`));
+
+ const passed = results.filter((r) => r.pass).length;
+ console.log(`\n===== 姹囨�伙細${passed}/${results.length} 閫氳繃 =====`);
+ results.filter((r) => !r.pass).forEach((r) => console.log(` 澶辫触: ${r.name} ${r.extra}`));
+
+ await browser.close();
+ process.exit(results.some((r) => !r.pass) ? 1 : 0);
+})();
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
index 82ea65b..781490b 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderAuditReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
@@ -38,6 +39,7 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -89,21 +91,28 @@
return success(true);
}
- @PostMapping("/submit")
- @Operation(summary = "鎻愪氦閲囪喘璁㈠崟瀹℃壒")
+ @PutMapping("/submit")
+ @Operation(summary = "鎻愪氦閲囪喘璁㈠崟瀹℃壒锛堟湭鎻愪氦/瀹℃牳涓嶉�氳繃 鈫� 瀹℃壒涓級")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
@PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')")
- public CommonResult<Boolean> submitPurchaseOrder(@RequestParam("id") Long id,
- @RequestParam("processDefinitionKey") String processDefinitionKey) {
- Long userId = getLoginUserId();
- purchaseOrderService.submitPurchaseOrder(id, processDefinitionKey, userId);
+ public CommonResult<Boolean> submitPurchaseOrder(@RequestParam("id") Long id) {
+ purchaseOrderService.submitPurchaseOrder(id);
return success(true);
}
- @GetMapping("/approve-process-list")
- @Operation(summary = "鑾峰彇閲囪喘璁㈠崟瀹℃壒娴佺▼鍒楄〃")
+ @PutMapping("/audit")
+ @Operation(summary = "瀹℃牳閲囪喘璁㈠崟锛堝鎵逛腑 鈫� 瀹℃牳閫氳繃/瀹℃牳涓嶉�氳繃锛夛紝浠呭鎵逛汉鏈汉鍙搷浣�")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-order:audit')")
+ public CommonResult<Boolean> auditPurchaseOrder(@Valid @RequestBody ErpPurchaseOrderAuditReqVO auditReqVO) {
+ purchaseOrderService.auditPurchaseOrder(auditReqVO.getId(), auditReqVO.getPass(), auditReqVO.getReviewRemark());
+ return success(true);
+ }
+
+ @GetMapping("/approver-ids")
+ @Operation(summary = "鑾峰彇閲囪喘璁㈠崟瀹℃壒浜虹紪鍙烽泦鍚�")
@PreAuthorize("@ss.hasPermission('erp:purchase-order:query')")
- public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
- return success(purchaseOrderService.getPurchaseOrderApproveProcessDefinitionList());
+ public CommonResult<Set<Long>> getPurchaseOrderApproverUserIds() {
+ return success(purchaseOrderService.getPurchaseOrderApproverUserIds());
}
@DeleteMapping("/delete")
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchasePlanController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchasePlanController.java
index a4ce778..63121f6 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchasePlanController.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchasePlanController.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanAuditReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanSaveReqVO;
@@ -38,6 +39,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -132,20 +134,28 @@
}
@PutMapping("/submit")
- @Operation(summary = "鎻愪氦閲囪喘璁″垝瀹℃壒")
+ @Operation(summary = "鎻愪氦閲囪喘璁″垝瀹℃牳锛堣崏绋�/瀹℃牳涓嶉�氳繃 鈫� 瀹℃壒涓級")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
@PreAuthorize("@ss.hasPermission('erp:purchase-plan:submit')")
- public CommonResult<Boolean> submitPurchasePlan(@RequestParam("id") Long id,
- @RequestParam("processDefinitionKey") String processDefinitionKey) {
- Long userId = getLoginUserId();
- purchasePlanService.submitPurchasePlan(id, processDefinitionKey, userId);
+ public CommonResult<Boolean> submitPurchasePlan(@RequestParam("id") Long id) {
+ purchasePlanService.submitPurchasePlan(id);
return success(true);
}
- @GetMapping("/approve-process-list")
- @Operation(summary = "鑾峰彇閲囪喘璁″垝瀹℃壒娴佺▼鍒楄〃")
+ @PutMapping("/audit")
+ @Operation(summary = "瀹℃牳閲囪喘璁″垝锛堝鎵逛腑 鈫� 瀹℃牳閫氳繃/瀹℃牳涓嶉�氳繃锛夛紝浠呭鎵逛汉鏈汉鍙搷浣�")
+ @PreAuthorize("@ss.hasPermission('erp:purchase-plan:audit')")
+ public CommonResult<Boolean> auditPurchasePlan(@Valid @RequestBody ErpPurchasePlanAuditReqVO auditReqVO) {
+ purchasePlanService.auditPurchasePlan(auditReqVO.getId(), auditReqVO.getPass(),
+ auditReqVO.getReviewRemark());
+ return success(true);
+ }
+
+ @GetMapping("/approver-ids")
+ @Operation(summary = "鑾峰緱閲囪喘璁″垝鐨勫鎵逛汉缂栧彿闆嗗悎锛屼緵鍓嶇鍒ゆ柇鏄惁灞曠ず瀹℃牳鎸夐挳")
@PreAuthorize("@ss.hasPermission('erp:purchase-plan:query')")
- public CommonResult<List<Map<String, Object>>> getApproveProcessList() {
- return success(purchasePlanService.getPurchasePlanApproveProcessDefinitionList());
+ public CommonResult<Set<Long>> getApproverUserIds() {
+ return success(purchasePlanService.getPurchasePlanApproverUserIds());
}
@PostMapping("/generate-from-stock-alert")
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderAuditReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderAuditReqVO.java
new file mode 100644
index 0000000..86a793f
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderAuditReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * ERP 閲囪喘璁㈠崟瀹℃牳 Request VO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Schema(description = "绠$悊鍚庡彴 - ERP 閲囪喘璁㈠崟瀹℃牳 Request VO")
+@Data
+public class ErpPurchaseOrderAuditReqVO {
+
+ @Schema(description = "閲囪喘璁㈠崟缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "閲囪喘璁㈠崟缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鏄惁瀹℃牳閫氳繃", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "瀹℃牳缁撴灉涓嶈兘涓虹┖")
+ private Boolean pass;
+
+ @Schema(description = "瀹℃牳鎰忚锛堝鏍搁�氳繃鏃朵负鍙�夛紱瀹℃牳涓嶉�氳繃鏃朵负蹇呭~鐨勫師鍥狅級", example = "鍚屾剰閲囪喘")
+ private String reviewRemark;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
index cd3e609..25e4e2e 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -118,6 +118,23 @@
@Schema(description = "閲囪喘閫�璐ф暟閲�", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal returnCount;
+ // ========== 瀹℃壒淇℃伅锛堣交閲忕骇鐘舵�佹満瀹℃壒锛� ==========
+
+ @Schema(description = "瀹℃牳浜虹紪鍙�", example = "1")
+ private Long reviewerId;
+
+ @Schema(description = "瀹℃牳浜烘樀绉�", example = "瓒呯骇绠$悊鍛�")
+ @ExcelProperty("瀹℃牳浜�")
+ private String reviewerName;
+
+ @Schema(description = "瀹℃牳鏃堕棿")
+ @ExcelProperty("瀹℃牳鏃堕棿")
+ private LocalDateTime reviewTime;
+
+ @Schema(description = "瀹℃牳鎰忚锛堝鏍镐笉閫氳繃鏃朵负椹冲洖鍘熷洜锛�", example = "鍚屾剰閲囪喘")
+ @ExcelProperty("瀹℃牳鎰忚")
+ private String reviewRemark;
+
@Data
public static class Item {
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanAuditReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanAuditReqVO.java
new file mode 100644
index 0000000..ea7902c
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanAuditReqVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * ERP 閲囪喘璁″垝瀹℃牳 Request VO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Schema(description = "绠$悊鍚庡彴 - ERP 閲囪喘璁″垝瀹℃牳 Request VO")
+@Data
+public class ErpPurchasePlanAuditReqVO {
+
+ @Schema(description = "閲囪喘璁″垝缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "閲囪喘璁″垝缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鏄惁瀹℃牳閫氳繃", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "瀹℃牳缁撴灉涓嶈兘涓虹┖")
+ private Boolean pass;
+
+ @Schema(description = "瀹℃牳鎰忚锛堝鏍搁�氳繃鏃朵负鍙�夛紱瀹℃牳涓嶉�氳繃鏃朵负蹇呭~鐨勫師鍥狅級", example = "鍚屾剰閲囪喘")
+ private String reviewRemark;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanRespVO.java
index 9950d04..0ab1550 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanRespVO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/plan/ErpPurchasePlanRespVO.java
@@ -37,9 +37,23 @@
@Schema(description = "闇�姹傛潵婧愬悕绉�", example = "搴撳瓨棰勮")
private String demandSourceName;
- @Schema(description = "宸ヤ綔娴佺紪鍙�", example = "b2c6c8f9-...")
+ @Schema(description = "宸ヤ綔娴佺紪鍙凤紙鍘嗗彶瀛楁锛屾敼閫犲悗涓嶅啀鍐欏叆锛�", example = "b2c6c8f9-...")
private String processInstanceId;
+ @Schema(description = "瀹℃牳浜虹紪鍙�", example = "1")
+ private Long reviewerId;
+
+ @Schema(description = "瀹℃牳浜哄鍚�", example = "鏉庡洓")
+ @ExcelProperty("瀹℃牳浜�")
+ private String reviewerName;
+
+ @Schema(description = "瀹℃牳鏃堕棿")
+ private LocalDateTime reviewTime;
+
+ @Schema(description = "瀹℃牳鎰忚锛堥�氳繃鏃剁殑鎰忚 / 涓嶉�氳繃鏃剁殑鍘熷洜锛�", example = "鍚屾剰閲囪喘")
+ @ExcelProperty("瀹℃牳鎰忚")
+ private String reviewRemark;
+
@Schema(description = "璁″垝鏃ユ湡")
@ExcelProperty("璁″垝鏃ユ湡")
private LocalDateTime planDate;
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
index 0f9b877..685be61 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -134,7 +134,32 @@
/**
* BPM 娴佺▼瀹炰緥缂栧彿
+ *
+ * 宸插簾寮冿細閲囪喘璁㈠崟鏀逛负杞婚噺绾х姸鎬佹満瀹℃壒鍚庝笉鍐嶅啓鍏ワ紝浠呬繚鐣欏巻鍙叉暟鎹��
*/
+ @Deprecated
private String processInstanceId;
+ /**
+ * 瀹℃牳浜虹紪鍙凤紙绯荤粺鐢ㄦ埛锛�
+ *
+ * 涓虹┖琛ㄧず灏氭湭瀹℃牳锛涘彧鏈夎鐢ㄦ埛锛堟垨閰嶇疆鐨勫鎵逛汉锛夋墠鑳芥墽琛屽鏍告搷浣溿��
+ */
+ private Long reviewerId;
+
+ /**
+ * 瀹℃牳浜烘樀绉帮紙鍐椾綑灞曠ず鐢級
+ */
+ private String reviewerName;
+
+ /**
+ * 瀹℃牳鏃堕棿
+ */
+ private LocalDateTime reviewTime;
+
+ /**
+ * 瀹℃牳鎰忚锛堝鏍镐笉閫氳繃鏃跺繀濉紝浣滀负椹冲洖鍘熷洜锛�
+ */
+ private String reviewRemark;
+
}
\ No newline at end of file
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchasePlanDO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchasePlanDO.java
index 72c6d97..17b13a7 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchasePlanDO.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchasePlanDO.java
@@ -96,7 +96,29 @@
/**
* BPM 娴佺▼瀹炰緥缂栧彿
+ *
+ * 鍘嗗彶瀛楁锛屾敼閫犱负杞婚噺绾у鎵瑰悗涓嶅啀鍐欏叆锛屼繚鐣欎互鍏煎瀛橀噺鏁版嵁銆�
*/
private String processInstanceId;
+ /**
+ * 瀹℃牳浜虹紪鍙凤紙瀹為檯鎵ц瀹℃牳鐨勭敤鎴凤級
+ */
+ private Long reviewerId;
+
+ /**
+ * 瀹℃牳浜哄鍚嶏紙鍐椾綑璁板綍锛岄伩鍏嶅垪琛ㄦ煡璇㈠啀鏌ョ敤鎴凤級
+ */
+ private String reviewerName;
+
+ /**
+ * 瀹℃牳鏃堕棿
+ */
+ private LocalDateTime reviewTime;
+
+ /**
+ * 瀹℃牳鎰忚锛堥�氳繃鏃剁殑鎰忚 / 涓嶉�氳繃鏃剁殑鍘熷洜锛�
+ */
+ private String reviewRemark;
+
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
index 9f7aca0..00d4025 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
@@ -11,6 +11,7 @@
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@@ -70,6 +71,35 @@
.eq(ErpPurchaseOrderDO::getId, id).eq(ErpPurchaseOrderDO::getStatus, status));
}
+ /**
+ * 鎻愪氦瀹℃壒锛氭洿鏂扮姸鎬佸苟娓呯┖涓婁竴杞殑瀹℃牳淇℃伅
+ *
+ * 璇存槑锛氬繀椤讳娇鐢� LambdaUpdateWrapper 鏄惧紡 set null锛�
+ * 鍚﹀垯 MyBatis-Plus 鐨� updateById 浼氬拷鐣� null 瀛楁锛屽鑷撮┏鍥炲師鍥犳畫鐣欍��
+ */
+ default int submitAndResetAuditInfo(Long id, Integer status) {
+ return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>()
+ .eq(ErpPurchaseOrderDO::getId, id)
+ .set(ErpPurchaseOrderDO::getStatus, status)
+ .set(ErpPurchaseOrderDO::getReviewerId, null)
+ .set(ErpPurchaseOrderDO::getReviewerName, null)
+ .set(ErpPurchaseOrderDO::getReviewTime, null)
+ .set(ErpPurchaseOrderDO::getReviewRemark, null));
+ }
+
+ /**
+ * 瀹℃牳锛堥�氳繃 / 涓嶉�氳繃锛夛細鍐欏叆瀹℃牳浜轰笌瀹℃牳鎰忚
+ */
+ default int auditPurchaseOrder(Long id, Integer status, Long reviewerId, String reviewerName, String reviewRemark) {
+ return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>()
+ .eq(ErpPurchaseOrderDO::getId, id)
+ .set(ErpPurchaseOrderDO::getStatus, status)
+ .set(ErpPurchaseOrderDO::getReviewerId, reviewerId)
+ .set(ErpPurchaseOrderDO::getReviewerName, reviewerName)
+ .set(ErpPurchaseOrderDO::getReviewTime, LocalDateTime.now())
+ .set(ErpPurchaseOrderDO::getReviewRemark, reviewRemark));
+ }
+
default ErpPurchaseOrderDO selectByNo(String no) {
return selectOne(ErpPurchaseOrderDO::getNo, no);
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java
index 473577e..361f95f 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchasePlanMapper.java
@@ -5,8 +5,10 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchasePlanDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
+import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@@ -38,4 +40,47 @@
.orderByDesc(ErpPurchasePlanDO::getId));
}
+ /**
+ * 鎻愪氦瀹℃牳锛氱疆涓哄鎵逛腑锛屽苟娓呯┖涓婁竴杞鏍哥粨鏋�
+ *
+ * 鐢� UpdateWrapper 鑰岄潪 updateById锛歁yBatis-Plus 榛樿鏇存柊绛栫暐浼氬拷鐣� null锛�
+ * 鐩存帴 updateById 鏃犳硶鎶娿�屽鏍镐笉閫氳繃銆嶅悗娈嬬暀鐨勫鏍镐汉涓庡鏍告剰瑙佺湡姝f竻绌恒��
+ *
+ * @param id 缂栧彿
+ * @param status 鐩爣鐘舵�侊紙瀹℃壒涓級
+ * @return 褰卞搷琛屾暟
+ */
+ default int submitAndResetAuditInfo(Long id, Integer status) {
+ return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>()
+ .eq(ErpPurchasePlanDO::getId, id)
+ .set(ErpPurchasePlanDO::getStatus, status)
+ .set(ErpPurchasePlanDO::getReviewerId, null)
+ .set(ErpPurchasePlanDO::getReviewerName, null)
+ .set(ErpPurchasePlanDO::getReviewTime, null)
+ .set(ErpPurchasePlanDO::getReviewRemark, null));
+ }
+
+ /**
+ * 瀹℃牳锛氬啓鍏ュ鏍哥粨鏋溿�佸鏍镐汉涓庡鏍告剰瑙�
+ *
+ * reviewRemark 鍏佽涓� null锛堝鏍搁�氳繃鏃舵剰瑙佸彲閫夛級锛屾晠鍚屾牱浣跨敤 UpdateWrapper 鐩村啓銆�
+ *
+ * @param id 缂栧彿
+ * @param status 鐩爣鐘舵�侊紙瀹℃牳閫氳繃 / 瀹℃牳涓嶉�氳繃锛�
+ * @param reviewerId 瀹℃牳浜虹紪鍙�
+ * @param reviewerName 瀹℃牳浜哄鍚�
+ * @param reviewRemark 瀹℃牳鎰忚
+ * @return 褰卞搷琛屾暟
+ */
+ default int auditPurchasePlan(Long id, Integer status, Long reviewerId,
+ String reviewerName, String reviewRemark) {
+ return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>()
+ .eq(ErpPurchasePlanDO::getId, id)
+ .set(ErpPurchasePlanDO::getStatus, status)
+ .set(ErpPurchasePlanDO::getReviewerId, reviewerId)
+ .set(ErpPurchasePlanDO::getReviewerName, reviewerName)
+ .set(ErpPurchasePlanDO::getReviewTime, LocalDateTime.now())
+ .set(ErpPurchasePlanDO::getReviewRemark, reviewRemark));
+ }
+
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderStatusEnum.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderStatusEnum.java
new file mode 100644
index 0000000..37cdba8
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderStatusEnum.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.erp.enums;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * ERP 閲囪喘璁㈠崟瀹℃壒鐘舵�佹灇涓�
+ *
+ * <p>鏀归�犺鏄庯細閲囪喘璁㈠崟鍘熷厛澶嶇敤 {@link ErpAuditStatus} 骞朵緷璧� BPM锛團lowable锛夋祦绋嬪鎵癸紝
+ * 鐜版敼涓轰笌銆屼骇鍝佹妧鏈‘璁ゅ嚱銆嶄竴鑷寸殑杞婚噺绾у崟绾�/鎴栫鐘舵�佹満锛屾晠鐙珛瀹氫箟鏈灇涓撅紝
+ * 閬垮厤涓� {@link ErpAuditStatus} 涓� CANCEL(30, 宸蹭綔搴�) 鐨勫彇鍊煎啿绐併��
+ *
+ * <p>鍙栧�间笌瀛楀吀 erp_audit_status 瀵归綈锛�0 鑽夌 / 10 瀹℃壒涓� / 20 瀹℃壒閫氳繃 / 30 瀹℃壒涓嶉�氳繃锛夛紝
+ * 鍥犳鍒楄〃椤典粛鍙部鐢� CellDict 娓叉煋锛屾棤闇�鏂板瀛楀吀绫诲瀷銆�
+ * APPROVE 淇濇寔涓� 20锛屼笌鏃㈡湁銆屽凡瀹℃牳鎵嶈兘鍏ュ簱/閫�璐с�嶇瓑鍒ゆ柇瀹屽叏鍏煎銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Getter
+@AllArgsConstructor
+public enum ErpPurchaseOrderStatusEnum implements ArrayValuable<Integer> {
+
+ DRAFT(0, "鏈彁浜�"),
+ PROCESS(10, "瀹℃壒涓�"),
+ APPROVE(20, "瀹℃牳閫氳繃"),
+ REJECT(30, "瀹℃牳涓嶉�氳繃"),
+ CANCEL(40, "宸插彇娑�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values())
+ .map(ErpPurchaseOrderStatusEnum::getStatus).toArray(Integer[]::new);
+
+ public static ErpPurchaseOrderStatusEnum fromStatus(Integer status) {
+ if (status == null) {
+ return null;
+ }
+ return Arrays.stream(values()).filter(item -> item.getStatus().equals(status)).findFirst().orElse(null);
+ }
+
+ public static ErpPurchaseOrderStatusEnum fromName(String name) {
+ if (name == null) {
+ return null;
+ }
+ return Arrays.stream(values()).filter(item -> item.getName().equals(name)).findFirst().orElse(null);
+ }
+
+ /**
+ * 鐘舵��
+ */
+ private final Integer status;
+ /**
+ * 鐘舵�佸悕
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
index 91c56d0..20399ad 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -30,6 +30,11 @@
ErrorCode PURCHASE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_101_012, "閲囪喘瀹℃牳鍒嗙被涓嬫病鏈夊彲鐢ㄧ殑娴佺▼妯″瀷锛岃鍏堝垱寤哄苟鍙戝竷娴佺▼");
ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_STATUS = new ErrorCode(1_030_101_013, "鎻愪氦瀹℃壒澶辫触锛屽彧鏈夋湭瀹℃牳鐘舵�佺殑閲囪喘璁㈠崟鎵嶈兘鎻愪氦");
ErrorCode PURCHASE_ORDER_ALL_IN = new ErrorCode(1_030_101_014, "閲囪喘璁㈠崟宸插叏閮ㄥ叆搴擄紝鏃犻渶纭鏀惰揣");
+ ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_NOT_ALLOWED = new ErrorCode(1_030_101_015, "鎻愪氦澶辫触锛屽彧鏈夈�愭湭鎻愪氦銆戞垨銆愬鏍镐笉閫氳繃銆戠姸鎬佺殑閲囪喘璁㈠崟鎵嶈兘鎻愪氦");
+ ErrorCode PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_101_016, "瀹℃牳澶辫触锛屽彧鏈夈�愬鎵逛腑銆戠姸鎬佺殑閲囪喘璁㈠崟鎵嶈兘瀹℃牳");
+ ErrorCode PURCHASE_ORDER_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_101_017, "瀹℃牳涓嶉�氳繃鏃跺繀椤诲~鍐欓┏鍥炲師鍥�");
+ ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_ITEM_EMPTY = new ErrorCode(1_030_101_018, "鎻愪氦澶辫触锛岄噰璐鍗曟病鏈夎鍗曢」锛岃鍏堟坊鍔犵墿鏂�");
+ ErrorCode PURCHASE_ORDER_UPDATE_FAIL_PROCESS = new ErrorCode(1_030_101_019, "閲囪喘璁㈠崟({})姝e湪瀹℃壒涓紝鏃犳硶淇敼");
// ========== ERP 閲囪喘鍏ュ簱锛�1-030-102-000锛� ==========
ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "閲囪喘鍏ュ簱鍗曚笉瀛樺湪");
@@ -69,6 +74,9 @@
ErrorCode PURCHASE_PLAN_ITEM_EMPTY = new ErrorCode(1_030_104_020, "閲囪喘璁″垝鏄庣粏涓嶈兘涓虹┖");
ErrorCode PURCHASE_PLAN_STOCK_ALERT_NO_ITEM = new ErrorCode(1_030_104_022,
"娌℃湁绗﹀悎搴撳瓨棰勮鏉′欢鐨勭墿鏂欙紝璇锋鏌ョ墿鏂欐槸鍚﹂厤缃粯璁や粨搴撳拰鏈�浣庡簱瀛橈紝骞剁‘璁ゅ綋鍓嶅簱瀛樹綆浜庢渶浣庡簱瀛�");
+ ErrorCode PURCHASE_PLAN_SUBMIT_FAIL_NOT_ALLOWED = new ErrorCode(1_030_104_023, "鎻愪氦澶辫触锛屽彧鏈夎崏绋挎垨瀹℃牳涓嶉�氳繃鐘舵�佺殑閲囪喘璁″垝鎵嶈兘鎻愪氦");
+ ErrorCode PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_104_024, "瀹℃牳澶辫触锛屽彧鏈夊鎵逛腑鐘舵�佺殑閲囪喘璁″垝鎵嶈兘瀹℃牳");
+ ErrorCode PURCHASE_PLAN_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_104_025, "瀹℃牳涓嶉�氳繃鏃跺繀椤诲~鍐欎笉閫氳繃鍘熷洜");
// ========== ERP 閲囪喘鏉ョエ锛�1-030-105-000锛� ==========
ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "鏉ョエ涓嶅瓨鍦�");
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
index 120cbd2..c1bfc4f 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
@@ -11,6 +11,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* ERP 閲囪喘璁㈠崟 Service 鎺ュ彛
@@ -45,18 +46,29 @@
/**
* 鎻愪氦閲囪喘璁㈠崟瀹℃壒
*
+ * 浠呫�愭湭鎻愪氦銆戞垨銆愬鏍镐笉閫氳繃銆戠姸鎬佸彲鎻愪氦锛涙彁浜ゅ悗杩涘叆銆愬鎵逛腑銆戯紝骞舵竻绌轰笂涓�杞鏍镐俊鎭��
+ *
* @param id 缂栧彿
- * @param processDefinitionKey 娴佺▼瀹氫箟 Key
- * @param userId 鐢ㄦ埛缂栧彿
*/
- void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId);
+ void submitPurchaseOrder(Long id);
/**
- * 鑾峰彇閲囪喘璁㈠崟瀹℃壒娴佺▼鍒楄〃
+ * 鑾峰彇閲囪喘璁㈠崟鐨勫鎵逛汉缂栧彿闆嗗悎锛堟垨绛撅細浠讳竴浜哄鏍稿嵆鍙級
*
- * @return 娴佺▼瀹氫箟鍒楄〃
+ * @return 瀹℃壒浜虹敤鎴风紪鍙�
*/
- List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList();
+ Set<Long> getPurchaseOrderApproverUserIds();
+
+ /**
+ * 瀹℃牳閲囪喘璁㈠崟锛堥�氳繃 / 涓嶉�氳繃锛�
+ *
+ * 浠呴厤缃唴鐨勫鎵逛汉鍙墽琛岋紱瀹℃牳涓嶉�氳繃鏃跺繀椤诲~鍐欓┏鍥炲師鍥狅紝鍗曟嵁閫�鍥炲悗鍙噸鏂颁慨鏀瑰苟鎻愪氦銆�
+ *
+ * @param id 缂栧彿
+ * @param pass 鏄惁瀹℃牳閫氳繃
+ * @param reviewRemark 瀹℃牳鎰忚
+ */
+ void auditPurchaseOrder(Long id, Boolean pass, String reviewRemark);
/**
* 鏇存柊閲囪喘璁㈠崟瀹℃壒缁撴灉锛圔PM 鍥炶皟锛�
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
index 083095f..0015037 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -2,16 +2,11 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
-import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
-import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
-import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
@@ -25,12 +20,15 @@
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum;
+import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi;
+import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
-import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -41,6 +39,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
/**
@@ -54,9 +53,14 @@
public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService {
/**
- * BPM 閲囪喘璁㈠崟瀹℃壒鍒嗙被缂栫爜
+ * 閲囪喘璁㈠崟瀹℃壒涓氬姟绫诲瀷缂栫爜锛堝搴� system_approval_config.biz_type锛�
+ *
+ * 娉ㄦ剰锛氭敼閫犱负杞婚噺绾х姸鎬佹満瀹℃壒鍓嶏紝姝ゅ鍘熶负 "purchase_request_approve"锛�
+ * 涓庨噰璐敵璇凤紙ErpPurchaseRequestServiceImpl锛夌殑 BPM 鍒嗙被缂栫爜瀹屽叏鐩稿悓锛�
+ * 瀵艰嚧閲囪喘璁㈠崟鏌ヨ瀹℃壒娴佺▼鏃朵細鍛戒腑閲囪喘鐢宠鐨勬祦绋嬪畾涔夛紝涓よ�呬簰鐩镐覆鐢ㄣ��
+ * 鐜版洿姝d负閲囪喘璁㈠崟涓撳睘缂栫爜銆�
*/
- private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_request_approve";
+ private static final String PURCHASE_ORDER_APPROVE_BIZ_TYPE = "erp_purchase_order_approve";
@Resource
private ErpPurchaseOrderMapper purchaseOrderMapper;
@@ -72,11 +76,9 @@
private ErpAccountService accountService;
@Resource
- private BpmProcessInstanceApi processInstanceApi;
+ private ApprovalConfigApi approvalConfigApi;
@Resource
- private BpmCategoryService bpmCategoryService;
- @Resource
- private BpmProcessDefinitionService bpmProcessDefinitionService;
+ private AdminUserApi adminUserApi;
@Resource
@Lazy
@@ -107,9 +109,9 @@
throw exception(PURCHASE_ORDER_NO_EXISTS);
}
- // 2.1 鎻掑叆璁㈠崟锛堥噰璐鍗曞垱寤哄悗涓烘湭瀹℃牳鐘舵�侊紝闇�瑕佹彁浜ゅ鎵癸級
+ // 2.1 鎻掑叆璁㈠崟锛堥噰璐鍗曞垱寤哄悗涓恒�愭湭鎻愪氦銆戠姸鎬侊紝闇�鎻愪氦杩涘叆瀹℃壒娴佺▼锛�
ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
- .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
+ .setNo(no).setStatus(ErpPurchaseOrderStatusEnum.DRAFT.getStatus())
.setInCount(BigDecimal.ZERO).setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()));
calculateTotalPrice(purchaseOrder, purchaseOrderItems);
purchaseOrderMapper.insert(purchaseOrder);
@@ -127,9 +129,13 @@
public void updatePurchaseOrder(ErpPurchaseOrderSaveReqVO updateReqVO) {
// 1.1 鏍¢獙瀛樺湪
ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(updateReqVO.getId());
- // 閲囪喘璁㈠崟涓嶅厑璁镐慨鏀癸紙宸插鏍哥姸鎬侊級
- if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
+ // 宸插鏍哥姸鎬佷笉鍏佽淇敼
+ if (ErpPurchaseOrderStatusEnum.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
throw exception(PURCHASE_ORDER_UPDATE_FAIL_APPROVE, purchaseOrder.getNo());
+ }
+ // 瀹℃壒涓笉鍏佽淇敼锛氶渶鍏堢敱瀹℃壒浜恒�屽鏍镐笉閫氳繃銆嶉��鍥烇紝鎵嶈兘閲嶆柊淇敼骞舵彁浜�
+ if (ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
+ throw exception(PURCHASE_ORDER_UPDATE_FAIL_PROCESS, purchaseOrder.getNo());
}
// 1.2 鏍¢獙渚涘簲鍟�
supplierService.validateSupplier(updateReqVO.getSupplierId());
@@ -169,6 +175,10 @@
boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
// 1.1 鏍¢獙瀛樺湪
ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
+ // 1.1.1 瀹℃牳閫氳繃灞炰簬瀹℃壒鍔ㄤ綔锛屽繀椤荤敱閰嶇疆鍐呯殑瀹℃壒浜烘墽琛岋紙鏈嶅姟绔己鍒讹紝閬垮厤缁曡繃瀹℃牳鎸夐挳鑷姪閫氳繃锛�
+ if (approve) {
+ approvalConfigApi.validateApprover(PURCHASE_ORDER_APPROVE_BIZ_TYPE, getLoginUserId());
+ }
// 1.2 鏍¢獙鐘舵��
if (purchaseOrder.getStatus().equals(status)) {
throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL);
@@ -194,61 +204,68 @@
@Override
@Transactional(rollbackFor = Exception.class)
- public void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId) {
- // 1. 鏍¢獙瀛樺湪 + 鏈鏍哥姸鎬�
+ public void submitPurchaseOrder(Long id) {
+ // 1.1 鏍¢獙瀛樺湪
ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
- if (!ErpAuditStatus.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
- throw exception(PURCHASE_ORDER_SUBMIT_FAIL_STATUS);
+ // 1.2 鍙湁銆愭湭鎻愪氦銆戞垨銆愬鏍镐笉閫氳繃銆戞墠鍏佽鎻愪氦
+ if (!ErpPurchaseOrderStatusEnum.DRAFT.getStatus().equals(purchaseOrder.getStatus())
+ && !ErpPurchaseOrderStatusEnum.REJECT.getStatus().equals(purchaseOrder.getStatus())) {
+ throw exception(PURCHASE_ORDER_SUBMIT_FAIL_NOT_ALLOWED);
+ }
+ // 1.3 蹇呴』閰嶇疆璁㈠崟椤�
+ List<ErpPurchaseOrderItemDO> items = purchaseOrderItemMapper.selectListByOrderId(id);
+ if (CollUtil.isEmpty(items)) {
+ throw exception(PURCHASE_ORDER_SUBMIT_FAIL_ITEM_EMPTY);
}
- // 2. 鍒涘缓 BPM 娴佺▼瀹炰緥
- String processInstanceId = processInstanceApi.createProcessInstance(userId,
- new BpmProcessInstanceCreateReqDTO()
- .setProcessDefinitionKey(processDefinitionKey)
- .setBusinessKey(String.valueOf(id)));
+ // 2. 鏍¢獙瀹℃壒宸插紑鍚笖閰嶇疆浜嗘湁鏁堝鎵逛汉锛堟湭寮�鍚鎵规椂涓嶅厑璁告彁浜わ級
+ approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_ORDER_APPROVE_BIZ_TYPE);
- // 3. 鏇存柊閲囪喘璁㈠崟鐘舵�佷负瀹℃壒涓�
- purchaseOrderMapper.updateById(new ErpPurchaseOrderDO()
- .setId(id)
- .setStatus(ErpAuditStatus.APPROVING.getStatus())
- .setProcessInstanceId(processInstanceId));
+ // 3. 缃负瀹℃壒涓紝骞舵竻绌轰笂涓�杞鏍镐俊鎭紝閬垮厤椹冲洖鍘熷洜娈嬬暀
+ purchaseOrderMapper.submitAndResetAuditInfo(id, ErpPurchaseOrderStatusEnum.PROCESS.getStatus());
}
@Override
- public List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList() {
- // 1. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
- validatePurchaseOrderApproveCategoryAndProcessDefinition();
+ public Set<Long> getPurchaseOrderApproverUserIds() {
+ return approvalConfigApi.getApproverUserIds(PURCHASE_ORDER_APPROVE_BIZ_TYPE);
+ }
- // 2. 鑾峰彇鍒嗙被涓嬬殑娴佺▼瀹氫箟淇℃伅
- List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
- .getProcessDefinitionInfoListByCategory(PURCHASE_ORDER_APPROVE_CATEGORY_CODE);
- if (CollUtil.isEmpty(definitionInfoList)) {
- return Collections.emptyList();
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void auditPurchaseOrder(Long id, Boolean pass, String reviewRemark) {
+ // 1.1 鏍¢獙瀛樺湪 + 瀹℃壒涓姸鎬�
+ ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
+ if (!ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
+ throw exception(PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS);
}
+ // 1.2 鏍¢獙褰撳墠鐧诲綍浜烘槸閰嶇疆鐨勫鎵逛汉锛堟垨绛撅細浠讳竴浜哄鏍稿嵆鍙級
+ Long userId = getLoginUserId();
+ approvalConfigApi.validateApprover(PURCHASE_ORDER_APPROVE_BIZ_TYPE, userId);
- // 3. 閬嶅巻鑾峰彇娴佺▼瀹氫箟璇︽儏锛岃繃婊ゆ縺娲荤姸鎬侊紝淇濈暀鏈�鏂扮増鏈�
- Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
- for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
- ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
- if (pd == null || pd.isSuspended()) {
- continue;
+ // 2. 瀹℃牳涓嶉�氳繃锛氬繀椤诲~鍐欓┏鍥炲師鍥狅紝鍗曟嵁閫�鍥炲悗鍙紪杈戝苟閲嶆柊鎻愪氦
+ if (!Boolean.TRUE.equals(pass)) {
+ if (StrUtil.isBlank(reviewRemark)) {
+ throw exception(PURCHASE_ORDER_AUDIT_REJECT_REASON_REQUIRED);
}
- ProcessDefinition existing = latestVersionMap.get(pd.getKey());
- if (existing == null || pd.getVersion() > existing.getVersion()) {
- latestVersionMap.put(pd.getKey(), pd);
- }
+ purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.REJECT.getStatus(),
+ userId, getUserNickname(userId), reviewRemark);
+ return;
}
- // 4. 杩斿洖娴佺▼瀹氫箟鍒楄〃
- List<Map<String, Object>> result = new ArrayList<>();
- for (ProcessDefinition pd : latestVersionMap.values()) {
- Map<String, Object> item = new HashMap<>();
- item.put("id", pd.getId());
- item.put("key", pd.getKey());
- item.put("name", pd.getName());
- result.add(item);
+ // 3. 瀹℃牳閫氳繃
+ purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.APPROVE.getStatus(),
+ userId, getUserNickname(userId), reviewRemark);
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛鏄电О锛岀敤浜庡啑浣欏瓨鍌ㄥ鏍镐汉濮撳悕
+ */
+ private String getUserNickname(Long userId) {
+ if (userId == null) {
+ return null;
}
- return result;
+ AdminUserRespDTO user = adminUserApi.getUser(userId);
+ return user == null ? null : user.getNickname();
}
@Override
@@ -256,7 +273,7 @@
public void updatePurchaseOrderAuditStatus(Long id, Integer bpmResult) {
// 1. 鏍¢獙瀛樺湪
ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
- if (!ErpAuditStatus.APPROVING.getStatus().equals(purchaseOrder.getStatus())) {
+ if (!ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) {
log.warn("[updatePurchaseOrderAuditStatus] 閲囪喘璁㈠崟({}) 涓嶅浜庡鎵逛腑鐘舵��", id);
return;
}
@@ -270,27 +287,18 @@
}
/**
- * 鏍¢獙閲囪喘璁㈠崟瀹℃壒鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
- */
- private void validatePurchaseOrderApproveCategoryAndProcessDefinition() {
- // 1. 鏍¢獙鍒嗙被鏄惁瀛樺湪
- List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
- Collections.singletonList(PURCHASE_ORDER_APPROVE_CATEGORY_CODE));
- if (CollUtil.isEmpty(categories)) {
- throw exception(PURCHASE_ORDER_BPM_CATEGORY_NOT_EXISTS);
- }
- }
-
- /**
* 杞崲 BPM 瀹℃壒缁撴灉涓哄鏍哥姸鎬�
+ *
+ * 宸插簾寮冿細閲囪喘璁㈠崟涓嶅啀璧� BPM 娴佺▼锛屾湰鏂规硶浠呬负鍘嗗彶浠g爜淇濈暀锛屼笉鍐嶈璋冪敤銆�
*/
+ @Deprecated
private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
- return ErpAuditStatus.APPROVE.getStatus();
+ return ErpPurchaseOrderStatusEnum.APPROVE.getStatus();
} else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
- return ErpAuditStatus.PROCESS.getStatus();
+ return ErpPurchaseOrderStatusEnum.REJECT.getStatus();
} else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
- return ErpAuditStatus.CANCEL.getStatus();
+ return ErpPurchaseOrderStatusEnum.CANCEL.getStatus();
}
return null;
}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanService.java
index 2189c51..4241ac6 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanService.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanService.java
@@ -9,7 +9,7 @@
import java.util.Collection;
import java.util.List;
-import java.util.Map;
+import java.util.Set;
/**
* ERP 閲囪喘璁″垝 Service 鎺ュ彛
@@ -73,24 +73,41 @@
PageResult<ErpPurchasePlanDO> getPurchasePlanPage(ErpPurchasePlanPageReqVO pageReqVO);
/**
- * 鎻愪氦閲囪喘璁″垝瀹℃壒
+ * 鎻愪氦閲囪喘璁″垝瀹℃牳
*
- * @param id 缂栧彿
- * @param processDefinitionKey BPM 娴佺▼瀹氫箟 Key
- * @param userId 鐢ㄦ埛缂栧彿
+ * 鎻愪氦鍓嶆牎楠岄�氱敤瀹℃壒閰嶇疆锛坆izType = purchase_plan_approve锛夊凡鍚敤涓旈厤缃簡鏈夋晥瀹℃壒浜猴紱
+ * 鏍¢獙閫氳繃鍚庣姸鎬佺疆涓恒�屽鎵逛腑銆嶃�傝崏绋夸笌瀹℃牳涓嶉�氳繃鐘舵�佸潎鍙彁浜ゃ��
+ *
+ * @param id 缂栧彿
*/
- void submitPurchasePlan(Long id, String processDefinitionKey, Long userId);
+ void submitPurchasePlan(Long id);
/**
- * 鑾峰彇閲囪喘璁″垝瀹℃壒娴佺▼鍒楄〃
+ * 鑾峰緱閲囪喘璁″垝鐨勫鎵逛汉鐢ㄦ埛缂栧彿闆嗗悎
*
- * @return 娴佺▼瀹氫箟鍒楄〃
+ * 渚涘墠绔垪琛ㄩ〉鍒ゆ柇鏄惁灞曠ず銆屽鏍搞�嶆寜閽紝鏈厤缃椂杩斿洖绌洪泦鍚堛��
+ *
+ * @return 瀹℃壒浜虹敤鎴风紪鍙烽泦鍚�
*/
- List<Map<String, Object>> getPurchasePlanApproveProcessDefinitionList();
+ Set<Long> getPurchasePlanApproverUserIds();
+
+ /**
+ * 瀹℃牳閲囪喘璁″垝锛堝鎵逛腑 鈫� 瀹℃牳閫氳繃 / 瀹℃牳涓嶉�氳繃锛�
+ *
+ * 浠呴�氱敤瀹℃壒閰嶇疆涓寚瀹氱殑瀹℃壒浜烘湰浜哄彲璋冪敤锛涘鏍镐笉閫氳繃鏃堕』濉啓鍘熷洜銆�
+ * 瀹℃牳閫氳繃鍚庡皢鑷姩鐢熸垚閲囪喘鐢宠銆�
+ *
+ * @param id 閲囪喘璁″垝缂栧彿
+ * @param pass 鏄惁閫氳繃
+ * @param reviewRemark 瀹℃牳鎰忚锛堜笉閫氳繃鏃朵负蹇呭~鐨勫師鍥狅級
+ */
+ void auditPurchasePlan(Long id, Boolean pass, String reviewRemark);
/**
* BPM 瀹℃壒缁撴灉鍥炶皟鏇存柊閲囪喘璁″垝鐘舵��
*
+ * 鍘嗗彶鍏煎鎺ュ彛锛氭敼閫犱负杞婚噺绾у鎵瑰悗涓嶅啀鐢� BPM 浜嬩欢瑙﹀彂锛屼繚鐣欎互鍏煎鏃㈡湁鐩戝惉鍣ㄣ��
+ *
* @param id 閲囪喘璁″垝缂栧彿
* @param bpmResult BPM 瀹℃壒缁撴灉
*/
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java
index 255118d..c0bcb87 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchasePlanServiceImpl.java
@@ -1,14 +1,10 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
-import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
-import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
-import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanSaveReqVO;
@@ -26,11 +22,11 @@
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.wms.MesWmStockApi;
+import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
-import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -42,6 +38,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
/**
@@ -55,9 +52,9 @@
public class ErpPurchasePlanServiceImpl implements ErpPurchasePlanService {
/**
- * BPM 閲囪喘璁″垝瀹℃壒鍒嗙被缂栫爜
+ * 閫氱敤瀹℃壒閰嶇疆鐨勪笟鍔$被鍨嬬紪鐮侊紙瀵瑰簲 system_approval_config.biz_type锛�
*/
- private static final String PURCHASE_PLAN_APPROVE_CATEGORY_CODE = "purchase_plan_approve";
+ private static final String PURCHASE_PLAN_APPROVE_BIZ_TYPE = "purchase_plan_approve";
/**
* 闇�姹傛潵婧愶細搴撳瓨棰勮
@@ -82,11 +79,7 @@
private ErpNoRedisDAO noRedisDAO;
@Resource
- private BpmProcessInstanceApi processInstanceApi;
- @Resource
- private BpmCategoryService bpmCategoryService;
- @Resource
- private BpmProcessDefinitionService bpmProcessDefinitionService;
+ private ApprovalConfigApi approvalConfigApi;
@Resource
private AdminUserApi adminUserApi;
@@ -195,58 +188,77 @@
@Override
@Transactional(rollbackFor = Exception.class)
- public void submitPurchasePlan(Long id, String processDefinitionKey, Long userId) {
- // 1. 鏍¢獙瀛樺湪 + 鑽夌鐘舵��
- ErpPurchasePlanDO plan = validatePurchasePlanCanModify(id);
+ public void submitPurchasePlan(Long id) {
+ // 1. 鏍¢獙瀛樺湪 + 鍙彁浜ょ姸鎬侊紙鑽夌 / 瀹℃牳涓嶉�氳繃锛�
+ ErpPurchasePlanDO plan = validatePurchasePlanExists(id);
+ if (!ErpPurchasePlanStatusEnum.DRAFT.getStatus().equals(plan.getStatus())
+ && !ErpPurchasePlanStatusEnum.REJECT.getStatus().equals(plan.getStatus())) {
+ throw exception(PURCHASE_PLAN_SUBMIT_FAIL_NOT_ALLOWED);
+ }
// 2. 鏍¢獙鏄惁鏈夋槑缁�
List<ErpPurchasePlanItemDO> items = purchasePlanItemMapper.selectListByPlanId(id);
if (CollUtil.isEmpty(items)) {
throw exception(PURCHASE_PLAN_ITEM_EMPTY);
}
- // 3. 鍒涘缓 BPM 娴佺▼瀹炰緥
- String processInstanceId = processInstanceApi.createProcessInstance(userId,
- new BpmProcessInstanceCreateReqDTO()
- .setProcessDefinitionKey(processDefinitionKey)
- .setBusinessKey(String.valueOf(id)));
- // 4. 鏇存柊閲囪喘璁″垝鐘舵�佷负瀹℃壒涓�
- purchasePlanMapper.updateById(new ErpPurchasePlanDO()
- .setId(id)
- .setStatus(ErpPurchasePlanStatusEnum.PROCESS.getStatus())
- .setProcessInstanceId(processInstanceId));
+ // 3. 鏍¢獙瀹℃壒閰嶇疆宸插惎鐢ㄤ笖閰嶇疆浜嗘湁鏁堝鎵逛汉锛堟湭閰嶇疆鏃舵姏鍑哄甫鏄庣‘鎻愮ず鐨勪笟鍔″紓甯革級
+ approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_PLAN_APPROVE_BIZ_TYPE);
+ // 4. 鐘舵�佺疆涓哄鎵逛腑锛屽苟娓呯┖涓婁竴杞鏍哥粨鏋滐紝閬垮厤娈嬬暀銆屽鏍镐笉閫氳繃銆嶇殑鍘熷洜
+ purchasePlanMapper.submitAndResetAuditInfo(id, ErpPurchasePlanStatusEnum.PROCESS.getStatus());
}
@Override
- public List<Map<String, Object>> getPurchasePlanApproveProcessDefinitionList() {
- // 1. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
- validatePurchasePlanApproveCategoryAndProcessDefinition();
- // 2. 鑾峰彇鍒嗙被涓嬬殑娴佺▼瀹氫箟淇℃伅
- List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
- .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE);
- if (CollUtil.isEmpty(definitionInfoList)) {
- return Collections.emptyList();
+ public Set<Long> getPurchasePlanApproverUserIds() {
+ return approvalConfigApi.getApproverUserIds(PURCHASE_PLAN_APPROVE_BIZ_TYPE);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void auditPurchasePlan(Long id, Boolean pass, String reviewRemark) {
+ // 1. 鏍¢獙瀛樺湪
+ ErpPurchasePlanDO plan = validatePurchasePlanExists(id);
+ // 2. 鏍¢獙澶勪簬瀹℃壒涓姸鎬�
+ if (!ErpPurchasePlanStatusEnum.PROCESS.getStatus().equals(plan.getStatus())) {
+ throw exception(PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS);
}
- // 3. 閬嶅巻鑾峰彇娴佺▼瀹氫箟璇︽儏锛岃繃婊ゆ縺娲荤姸鎬侊紝淇濈暀鏈�鏂扮増鏈�
- Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
- for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
- ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
- if (pd == null || pd.isSuspended()) {
- continue;
+ // 3. 鏍¢獙褰撳墠鐧诲綍鐢ㄦ埛鏄涓氬姟绫诲瀷鐨勫鎵逛汉锛堟垨绛撅細浠讳竴浜哄潎鍙鏍革級
+ Long userId = getLoginUserId();
+ approvalConfigApi.validateApprover(PURCHASE_PLAN_APPROVE_BIZ_TYPE, userId);
+ // 4. 瀹℃牳涓嶉�氳繃锛氬繀椤诲~鍐欏師鍥狅紝鐘舵�佺疆涓哄鏍镐笉閫氳繃锛岄��鍥炵紪鍒朵汉淇敼鍚庡彲閲嶆柊鎻愪氦
+ if (!Boolean.TRUE.equals(pass)) {
+ if (StrUtil.isBlank(reviewRemark)) {
+ throw exception(PURCHASE_PLAN_AUDIT_REJECT_REASON_REQUIRED);
}
- ProcessDefinition existing = latestVersionMap.get(pd.getKey());
- if (existing == null || pd.getVersion() > existing.getVersion()) {
- latestVersionMap.put(pd.getKey(), pd);
+ purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.REJECT.getStatus(),
+ userId, getUserNickname(userId), reviewRemark);
+ return;
+ }
+ // 5. 瀹℃牳閫氳繃锛氳褰曞鏍镐汉涓庡鏍告剰瑙侊紝鐘舵�佺疆涓哄鏍搁�氳繃
+ purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.APPROVE.getStatus(),
+ userId, getUserNickname(userId), reviewRemark);
+ // 6. 瀹℃牳閫氳繃涓旀湭鐢熸垚鐢宠锛岃嚜鍔ㄧ敓鎴愰噰璐敵璇�
+ if (!Boolean.TRUE.equals(plan.getGeneratedFlag())) {
+ try {
+ List<Long> requestIds = doGeneratePurchaseRequests(plan);
+ log.info("[auditPurchasePlan] 閲囪喘璁″垝({}) 鑷姩鐢熸垚閲囪喘鐢宠({})", id, requestIds);
+ } catch (Exception e) {
+ // 涓嶆姏鍑哄紓甯革紝閬垮厤褰卞搷瀹℃牳缁撴灉钀藉簱锛涚敓鎴愬け璐ュ彲鐢辩敤鎴锋墜鍔ㄨЕ鍙�
+ log.error("[auditPurchasePlan] 閲囪喘璁″垝({}) 鑷姩鐢熸垚閲囪喘鐢宠澶辫触", id, e);
}
}
- // 4. 杩斿洖娴佺▼瀹氫箟鍒楄〃
- List<Map<String, Object>> result = new ArrayList<>();
- for (ProcessDefinition pd : latestVersionMap.values()) {
- Map<String, Object> item = new HashMap<>();
- item.put("id", pd.getId());
- item.put("key", pd.getKey());
- item.put("name", pd.getName());
- result.add(item);
+ }
+
+ /**
+ * 鑾峰彇鐢ㄦ埛鏄电О
+ *
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 鏄电О锛岀敤鎴蜂笉瀛樺湪鏃惰繑鍥� null
+ */
+ private String getUserNickname(Long userId) {
+ if (userId == null) {
+ return null;
}
- return result;
+ AdminUserRespDTO user = adminUserApi.getUser(userId);
+ return user == null ? null : user.getNickname();
}
@Override
@@ -437,19 +449,6 @@
return ErpPurchasePlanStatusEnum.CANCEL.getStatus();
}
return null;
- }
-
- private void validatePurchasePlanApproveCategoryAndProcessDefinition() {
- List<cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO> categories =
- bpmCategoryService.getCategoryListByCode(Collections.singletonList(PURCHASE_PLAN_APPROVE_CATEGORY_CODE));
- if (CollUtil.isEmpty(categories)) {
- throw exception(PURCHASE_PLAN_BPM_CATEGORY_NOT_EXISTS);
- }
- List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
- .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE);
- if (CollUtil.isEmpty(definitionInfoList)) {
- throw exception(PURCHASE_PLAN_BPM_PROCESS_DEFINITION_NOT_EXISTS);
- }
}
private ErpPurchasePlanDO validatePurchasePlanExists(Long id) {
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApi.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApi.java
new file mode 100644
index 0000000..1aa6230
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApi.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.system.api.approval;
+
+import java.util.Set;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 API 鎺ュ彛
+ *
+ * 渚涘悇涓氬姟妯″潡锛圗RP / CRM / HRM / MES / 鍞悗锛夊湪鎻愪氦瀹℃壒銆佹墽琛屽鏍告椂璋冪敤锛�
+ * 鏇夸唬鍘熷厛渚濊禆 BPM锛團lowable锛夋祦绋嬪畾涔夌殑鏂瑰紡銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface ApprovalConfigApi {
+
+ /**
+ * 鏍¢獙涓氬姟绫诲瀷宸插惎鐢ㄥ鎵逛笖閰嶇疆浜嗘湁鏁堝鎵逛汉锛屽苟杩斿洖瀹℃壒浜虹紪鍙烽泦鍚�
+ *
+ * 涓氬姟妯″潡鍦ㄣ�屾彁浜ゅ鏍搞�嶆椂璋冪敤銆傛牎楠屽け璐ユ椂鎶涘嚭涓氬姟寮傚父锛�
+ * 鏈厤缃鎵� / 鏈惎鐢ㄥ鎵� / 鏈厤缃鎵逛汉銆�
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @return 瀹℃壒浜虹敤鎴风紪鍙烽泦鍚�
+ */
+ Set<Long> validateApprovalEnabledAndGetApprovers(String bizType);
+
+ /**
+ * 鏍¢獙鎸囧畾鐢ㄦ埛鏄惁涓鸿涓氬姟绫诲瀷鐨勫鎵逛汉
+ *
+ * 涓氬姟妯″潡鍦ㄣ�屾墽琛屽鏍搞�嶆椂璋冪敤锛岄潪瀹℃壒浜哄皢鎶涘嚭涓氬姟寮傚父銆�
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @param userId 鐢ㄦ埛缂栧彿
+ */
+ void validateApprover(String bizType, Long userId);
+
+ /**
+ * 鑾峰緱鎸囧畾涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎
+ *
+ * 鐢ㄤ簬鍒楄〃椤靛垽鏂槸鍚﹀睍绀哄鏍告寜閽紝鏈厤缃椂杩斿洖绌洪泦鍚堛��
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @return 瀹℃壒浜虹敤鎴风紪鍙烽泦鍚�
+ */
+ Set<Long> getApproverUserIds(String bizType);
+
+ /**
+ * 鍒ゆ柇鎸囧畾鐢ㄦ埛鏄惁涓鸿涓氬姟绫诲瀷鐨勫鎵逛汉
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @param userId 鐢ㄦ埛缂栧彿
+ * @return 鏄惁涓哄鎵逛汉
+ */
+ default boolean isApprover(String bizType, Long userId) {
+ return userId != null && getApproverUserIds(bizType).contains(userId);
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApiImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApiImpl.java
new file mode 100644
index 0000000..b0c176e
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/approval/ApprovalConfigApiImpl.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.system.api.approval;
+
+import cn.iocoder.yudao.module.system.service.approval.ApprovalConfigService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 API 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+public class ApprovalConfigApiImpl implements ApprovalConfigApi {
+
+ @Resource
+ private ApprovalConfigService approvalConfigService;
+
+ @Override
+ public Set<Long> validateApprovalEnabledAndGetApprovers(String bizType) {
+ return approvalConfigService.validateApprovalEnabledAndGetApprovers(bizType);
+ }
+
+ @Override
+ public void validateApprover(String bizType, Long userId) {
+ approvalConfigService.validateApprover(bizType, userId);
+ }
+
+ @Override
+ public Set<Long> getApproverUserIds(String bizType) {
+ return approvalConfigService.getApproverUserIds(bizType);
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/ApprovalConfigController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/ApprovalConfigController.java
new file mode 100644
index 0000000..96a1ece
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/ApprovalConfigController.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.system.controller.admin.approval;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO;
+import cn.iocoder.yudao.module.system.service.approval.ApprovalConfigService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "绠$悊鍚庡彴 - 閫氱敤瀹℃壒閰嶇疆")
+@RestController
+@RequestMapping("/system/approval-config")
+@Validated
+public class ApprovalConfigController {
+
+ @Resource
+ private ApprovalConfigService approvalConfigService;
+
+ @PostMapping("/save")
+ @Operation(summary = "淇濆瓨瀹℃壒閰嶇疆锛堟柊澧炴垨淇敼锛夛紝瀹℃壒浜烘暣浣撹鐩�")
+ @PreAuthorize("@ss.hasPermission('system:approval-config:update')")
+ public CommonResult<Long> saveApprovalConfig(@Valid @RequestBody ApprovalConfigSaveReqVO saveReqVO) {
+ return success(approvalConfigService.saveApprovalConfig(saveReqVO));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎瀹℃壒閰嶇疆")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('system:approval-config:update')")
+ public CommonResult<Boolean> deleteApprovalConfig(@RequestParam("id") Long id) {
+ approvalConfigService.deleteApprovalConfig(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱瀹℃壒閰嶇疆")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('system:approval-config:query')")
+ public CommonResult<ApprovalConfigRespVO> getApprovalConfig(@RequestParam("id") Long id) {
+ ApprovalConfigDO config = approvalConfigService.getApprovalConfig(id);
+ if (config == null) {
+ return success(null);
+ }
+ ApprovalConfigRespVO vo = BeanUtils.toBean(config, ApprovalConfigRespVO.class);
+ vo.setUserIds(new ArrayList<>(approvalConfigService.getApproverUserIds(config.getBizType())));
+ return success(vo);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱瀹℃壒閰嶇疆鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('system:approval-config:query')")
+ public CommonResult<PageResult<ApprovalConfigRespVO>> getApprovalConfigPage(
+ @Valid ApprovalConfigPageReqVO pageReqVO) {
+ return success(approvalConfigService.getApprovalConfigPage(pageReqVO));
+ }
+
+ @GetMapping("/approver-ids")
+ @Operation(summary = "鑾峰緱鎸囧畾涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎")
+ @Parameter(name = "bizType", description = "涓氬姟绫诲瀷缂栫爜", required = true, example = "purchase_plan_approve")
+ @PreAuthorize("@ss.hasPermission('system:approval-config:query')")
+ public CommonResult<Set<Long>> getApproverUserIds(@RequestParam("bizType") String bizType) {
+ return success(approvalConfigService.getApproverUserIds(bizType));
+ }
+
+ @GetMapping("/is-approver")
+ @Operation(summary = "鍒ゆ柇褰撳墠鐧诲綍鐢ㄦ埛鏄惁涓烘寚瀹氫笟鍔$被鍨嬬殑瀹℃壒浜�")
+ @Parameter(name = "bizType", description = "涓氬姟绫诲瀷缂栫爜", required = true, example = "purchase_plan_approve")
+ @PreAuthorize("@ss.hasPermission('system:approval-config:query')")
+ public CommonResult<Boolean> isApprover(@RequestParam("bizType") String bizType) {
+ Set<Long> approverIds = approvalConfigService.getApproverUserIds(bizType);
+ return success(approverIds.contains(getLoginUserId()));
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigPageReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigPageReqVO.java
new file mode 100644
index 0000000..d180f6a
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigPageReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.system.controller.admin.approval.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "绠$悊鍚庡彴 - 瀹℃壒閰嶇疆鍒嗛〉 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ApprovalConfigPageReqVO extends PageParam {
+
+ @Schema(description = "涓氬姟绫诲瀷鍚嶇О", example = "閲囪喘璁″垝")
+ private String bizName;
+
+ @Schema(description = "涓氬姟绫诲瀷缂栫爜", example = "purchase_plan_approve")
+ private String bizType;
+
+ @Schema(description = "鏄惁鍚敤瀹℃壒", example = "true")
+ private Boolean approvalEnabled;
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigRespVO.java
new file mode 100644
index 0000000..59bde9e
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigRespVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.system.controller.admin.approval.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - 瀹℃壒閰嶇疆 Response VO")
+@Data
+public class ApprovalConfigRespVO {
+
+ @Schema(description = "涓婚敭ID", example = "1")
+ private Long id;
+
+ @Schema(description = "涓氬姟绫诲瀷缂栫爜", example = "purchase_plan_approve")
+ private String bizType;
+
+ @Schema(description = "涓氬姟绫诲瀷鍚嶇О", example = "閲囪喘璁″垝瀹℃牳")
+ private String bizName;
+
+ @Schema(description = "鏄惁鍚敤瀹℃壒", example = "true")
+ private Boolean approvalEnabled;
+
+ @Schema(description = "瀹℃壒浜虹敤鎴稩D鍒楄〃", example = "[1,2]")
+ private List<Long> userIds;
+
+ @Schema(description = "瀹℃壒浜哄鍚嶅垪琛紙鍐椾綑灞曠ず锛岄伩鍏嶅墠绔啀鏌ョ敤鎴凤級", example = "[\"寮犱笁\",\"鏉庡洓\"]")
+ private List<String> userNames;
+
+ @Schema(description = "澶囨敞", example = "鐢遍噰璐儴缁忕悊瀹℃牳")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigSaveReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigSaveReqVO.java
new file mode 100644
index 0000000..ac4df4b
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/approval/vo/ApprovalConfigSaveReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.system.controller.admin.approval.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "绠$悊鍚庡彴 - 瀹℃壒閰嶇疆淇濆瓨 Request VO")
+@Data
+public class ApprovalConfigSaveReqVO {
+
+ @Schema(description = "涓婚敭ID锛屾柊澧炴椂涓虹┖", example = "1")
+ private Long id;
+
+ @Schema(description = "涓氬姟绫诲瀷缂栫爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "purchase_plan_approve")
+ @NotEmpty(message = "涓氬姟绫诲瀷缂栫爜涓嶈兘涓虹┖")
+ private String bizType;
+
+ @Schema(description = "涓氬姟绫诲瀷鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "閲囪喘璁″垝瀹℃牳")
+ @NotEmpty(message = "涓氬姟绫诲瀷鍚嶇О涓嶈兘涓虹┖")
+ private String bizName;
+
+ @Schema(description = "鏄惁鍚敤瀹℃壒", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "鏄惁鍚敤瀹℃壒涓嶈兘涓虹┖")
+ private Boolean approvalEnabled;
+
+ @Schema(description = "瀹℃壒浜虹敤鎴稩D鍒楄〃锛屽涓箣闂翠负鎴栫锛堜换鎰忎竴浜洪�氳繃鍗冲彲锛�", example = "[1,2]")
+ private List<Long> userIds;
+
+ @Schema(description = "澶囨敞", example = "鐢遍噰璐儴缁忕悊瀹℃牳")
+ private String remark;
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigDO.java
new file mode 100644
index 0000000..5146bdf
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigDO.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.approval;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 DO
+ *
+ * 涓�涓笟鍔$被鍨嬪搴斾竴鏉¢厤缃紱瀹℃壒浜哄瓨鏀惧湪 {@link ApprovalConfigUserDO} 瀛愯〃銆�
+ * 瀹℃壒浜洪厤缃涓椂鎸夈�愭垨绛俱�戝鐞嗭紝鍗充换鎰忎竴浜哄鏍搁�氳繃鍗虫帹杩涗笟鍔$姸鎬併��
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("system_approval_config")
+@KeySequence("system_approval_config_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ApprovalConfigDO extends BaseDO {
+
+ /**
+ * 涓婚敭ID
+ */
+ @TableId
+ private Long id;
+ /**
+ * 涓氬姟绫诲瀷缂栫爜锛屼笌鍚勬ā鍧楀鎵瑰垎绫荤紪鐮佷竴鑷�
+ */
+ private String bizType;
+ /**
+ * 涓氬姟绫诲瀷鍚嶇О
+ */
+ private String bizName;
+ /**
+ * 鏄惁鍚敤瀹℃壒
+ */
+ private Boolean approvalEnabled;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigUserDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigUserDO.java
new file mode 100644
index 0000000..244a272
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/approval/ApprovalConfigUserDO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.approval;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆-瀹℃壒浜� DO
+ *
+ * 涓�涓笟鍔$被鍨嬪彲閰嶇疆澶氫釜瀹℃壒浜猴紝澶氫釜瀹℃壒浜轰箣闂翠负銆愭垨绛俱�戝叧绯汇��
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("system_approval_config_user")
+@KeySequence("system_approval_config_user_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ApprovalConfigUserDO extends BaseDO {
+
+ /**
+ * 涓婚敭ID
+ */
+ @TableId
+ private Long id;
+ /**
+ * 瀹℃壒閰嶇疆ID锛堝叧鑱� {@link ApprovalConfigDO#getId()}锛�
+ */
+ private Long configId;
+ /**
+ * 瀹℃壒浜虹敤鎴稩D锛堢郴缁熺敤鎴凤級
+ */
+ private Long userId;
+ /**
+ * 鏄剧ず椤哄簭
+ */
+ private Integer sort;
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigMapper.java
new file mode 100644
index 0000000..0a1833e
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigMapper.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.system.dal.mysql.approval;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 Mapper
+ *
+ * 绫诲悕甯� System 鍓嶇紑锛岄伩鍏嶄笌 MES 鐨� WmsWarehouseApprovalConfigMapper 鍦�
+ * Spring 鎸夊瓧娈靛悕锛坆yName锛夋敞鍏ユ椂浜х敓鍛藉悕鍐茬獊銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface SystemApprovalConfigMapper extends BaseMapperX<ApprovalConfigDO> {
+
+ default PageResult<ApprovalConfigDO> selectPage(ApprovalConfigPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX<ApprovalConfigDO>()
+ .likeIfPresent(ApprovalConfigDO::getBizName, reqVO.getBizName())
+ .eqIfPresent(ApprovalConfigDO::getBizType, reqVO.getBizType())
+ .eqIfPresent(ApprovalConfigDO::getApprovalEnabled, reqVO.getApprovalEnabled())
+ .orderByAsc(ApprovalConfigDO::getId));
+ }
+
+ default ApprovalConfigDO selectByBizType(String bizType) {
+ return selectOne(ApprovalConfigDO::getBizType, bizType);
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigUserMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigUserMapper.java
new file mode 100644
index 0000000..3c66842
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/approval/SystemApprovalConfigUserMapper.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.system.dal.mysql.approval;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigUserDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆-瀹℃壒浜� Mapper
+ *
+ * 绫诲悕甯� System 鍓嶇紑锛岄伩鍏� Spring 鎸夊瓧娈靛悕娉ㄥ叆鏃朵笌鍏跺畠妯″潡鐨勫鎵� Mapper 鍐茬獊銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface SystemApprovalConfigUserMapper extends BaseMapperX<ApprovalConfigUserDO> {
+
+ default List<ApprovalConfigUserDO> selectListByConfigId(Long configId) {
+ return selectList(new LambdaQueryWrapperX<ApprovalConfigUserDO>()
+ .eq(ApprovalConfigUserDO::getConfigId, configId)
+ .orderByAsc(ApprovalConfigUserDO::getSort));
+ }
+
+ default List<ApprovalConfigUserDO> selectListByConfigIds(Collection<Long> configIds) {
+ return selectList(new LambdaQueryWrapperX<ApprovalConfigUserDO>()
+ .in(ApprovalConfigUserDO::getConfigId, configIds)
+ .orderByAsc(ApprovalConfigUserDO::getSort));
+ }
+
+ /**
+ * 鎸夐厤缃甀D鐗╃悊鍒犻櫎瀹℃壒浜鸿褰�
+ *
+ * 閰嶇疆瀹℃壒浜洪噰鐢ㄣ�屽厛鍒犲悗鎻掋�嶆暣浣撹鐩栫殑淇濆瓨鏂瑰紡锛岃嫢璧伴�昏緫鍒犻櫎锛�
+ * 鍙嶅淇濆瓨浼氬湪琛ㄤ腑鍫嗙Н澶ч噺 deleted = 1 鐨勫巻鍙茶锛屾晠姝ゅ鐩存帴鐗╃悊鍒犻櫎銆�
+ *
+ * @param configId 瀹℃壒閰嶇疆ID
+ * @return 鍒犻櫎琛屾暟
+ */
+ default int deleteByConfigId(Long configId) {
+ return delete(new LambdaQueryWrapperX<ApprovalConfigUserDO>()
+ .eq(ApprovalConfigUserDO::getConfigId, configId));
+ }
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
index 86aeeec..3dc2a70 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -172,4 +172,13 @@
ErrorCode STORAGE_BLOB_NOT_EXISTS = new ErrorCode(1_002_029_004, "鏂囦欢涓嶅瓨鍦�");
ErrorCode STORAGE_BLOB_PUBLIC_KEY_INVALID = new ErrorCode(1_002_029_005, "鏂囦欢鍏紑璁块棶key鏃犳晥");
+ // ========== 閫氱敤瀹℃壒閰嶇疆 1-002-030-000 ==========
+ ErrorCode APPROVAL_CONFIG_NOT_EXISTS = new ErrorCode(1_002_030_000, "瀹℃壒閰嶇疆涓嶅瓨鍦�");
+ ErrorCode APPROVAL_CONFIG_BIZ_TYPE_DUPLICATE = new ErrorCode(1_002_030_001, "涓氬姟绫诲瀷缂栫爜({})宸插瓨鍦�");
+ ErrorCode APPROVAL_CONFIG_NOT_ENABLED = new ErrorCode(1_002_030_002, "璇ヤ笟鍔$被鍨嬫湭鍚敤瀹℃壒锛屾棤娉曟彁浜ゅ鏍�");
+ ErrorCode APPROVAL_CONFIG_APPROVER_EMPTY = new ErrorCode(1_002_030_003, "璇ヤ笟鍔$被鍨嬫湭閰嶇疆瀹℃壒浜猴紝璇峰厛鍦ㄥ鎵归厤缃腑鎸囧畾瀹℃壒浜�");
+ ErrorCode APPROVAL_CONFIG_APPROVER_NOT_EXISTS = new ErrorCode(1_002_030_004, "鎵�閫夊鎵逛汉涓嶅瓨鍦ㄦ垨宸茶绂佺敤");
+ ErrorCode APPROVAL_CONFIG_NOT_APPROVER = new ErrorCode(1_002_030_005, "鎮ㄤ笉鏄涓氬姟鐨勫鎵逛汉锛屾棤鏉冭繘琛屽鏍告搷浣�");
+ ErrorCode APPROVAL_CONFIG_REJECT_REASON_REQUIRED = new ErrorCode(1_002_030_006, "瀹℃牳涓嶉�氳繃鏃跺繀椤诲~鍐欎笉閫氳繃鍘熷洜");
+
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigService.java
new file mode 100644
index 0000000..ad2a189
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigService.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.system.service.approval;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO;
+
+import java.util.Set;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface ApprovalConfigService {
+
+ /**
+ * 淇濆瓨瀹℃壒閰嶇疆锛堟柊澧炴垨淇敼锛�
+ *
+ * 瀹℃壒浜洪噰鐢ㄦ暣浣撹鐩栨柟寮忎繚瀛橈紝鍗充紶鍏ョ殑 userIds 鍗充负鏈�缁堢粨鏋溿��
+ *
+ * @param saveReqVO 淇濆瓨淇℃伅
+ * @return 閰嶇疆缂栧彿
+ */
+ Long saveApprovalConfig(ApprovalConfigSaveReqVO saveReqVO);
+
+ /**
+ * 鍒犻櫎瀹℃壒閰嶇疆
+ *
+ * @param id 閰嶇疆缂栧彿
+ */
+ void deleteApprovalConfig(Long id);
+
+ /**
+ * 鑾峰緱瀹℃壒閰嶇疆
+ *
+ * @param id 閰嶇疆缂栧彿
+ * @return 瀹℃壒閰嶇疆
+ */
+ ApprovalConfigDO getApprovalConfig(Long id);
+
+ /**
+ * 鎸変笟鍔$被鍨嬭幏寰楀鎵归厤缃�
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @return 瀹℃壒閰嶇疆锛屼笉瀛樺湪鏃惰繑鍥� null
+ */
+ ApprovalConfigDO getApprovalConfigByBizType(String bizType);
+
+ /**
+ * 鑾峰緱瀹℃壒閰嶇疆鍒嗛〉锛屽惈瀹℃壒浜轰俊鎭�
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 鍒嗛〉缁撴灉
+ */
+ PageResult<ApprovalConfigRespVO> getApprovalConfigPage(ApprovalConfigPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱鎸囧畾涓氬姟绫诲瀷鐨勫鎵逛汉鐢ㄦ埛缂栧彿闆嗗悎
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @return 瀹℃壒浜虹敤鎴风紪鍙烽泦鍚堬紝鏈厤缃椂杩斿洖绌洪泦鍚�
+ */
+ Set<Long> getApproverUserIds(String bizType);
+
+ /**
+ * 鏍¢獙瀹℃壒娴佺▼鍙敤锛屽苟杩斿洖瀹℃壒浜洪泦鍚�
+ *
+ * 鏍¢獙鍐呭锛氫笟鍔$被鍨嬪凡鍚敤瀹℃壒銆佷笖宸查厤缃嚦灏戜竴涓湁鏁堝鎵逛汉銆�
+ * 渚涘悇涓氬姟妯″潡鍦ㄣ�屾彁浜ゅ鏍搞�嶆椂璋冪敤銆�
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @return 瀹℃壒浜虹敤鎴风紪鍙烽泦鍚�
+ */
+ Set<Long> validateApprovalEnabledAndGetApprovers(String bizType);
+
+ /**
+ * 鏍¢獙鎸囧畾鐢ㄦ埛鏄惁涓鸿涓氬姟绫诲瀷鐨勫鎵逛汉
+ *
+ * 渚涘悇涓氬姟妯″潡鍦ㄣ�屽鏍搞�嶆椂璋冪敤銆�
+ *
+ * @param bizType 涓氬姟绫诲瀷缂栫爜
+ * @param userId 鐢ㄦ埛缂栧彿
+ */
+ void validateApprover(String bizType, Long userId);
+
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigServiceImpl.java
new file mode 100644
index 0000000..66a701f
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/approval/ApprovalConfigServiceImpl.java
@@ -0,0 +1,224 @@
+package cn.iocoder.yudao.module.system.service.approval;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigUserDO;
+import cn.iocoder.yudao.module.system.dal.mysql.approval.SystemApprovalConfigMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.approval.SystemApprovalConfigUserMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
+
+/**
+ * 閫氱敤瀹℃壒閰嶇疆 Service 瀹炵幇绫�
+ *
+ * 璁捐璇存槑锛氭湰閰嶇疆鏇夸唬鍘熸湁鍩轰簬 BPM锛團lowable锛夋祦绋嬪畾涔夌殑瀹℃壒鏂瑰紡锛岄噰鐢ㄨ交閲忕骇鍗曠骇瀹℃壒銆�
+ * 鍚屼竴涓氬姟绫诲瀷鍙厤缃涓鎵逛汉锛屽浜轰箣闂翠负銆愭垨绛俱�戝叧绯伙紝鍗充换鎰忎竴浜哄鏍搁�氳繃鍗虫帹杩涗笟鍔$姸鎬併��
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+@Slf4j
+public class ApprovalConfigServiceImpl implements ApprovalConfigService {
+
+ @Resource
+ private SystemApprovalConfigMapper systemApprovalConfigMapper;
+
+ @Resource
+ private SystemApprovalConfigUserMapper systemApprovalConfigUserMapper;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long saveApprovalConfig(ApprovalConfigSaveReqVO saveReqVO) {
+ // 1. 鏍¢獙涓氬姟绫诲瀷缂栫爜鍞竴
+ validateBizTypeUnique(saveReqVO.getId(), saveReqVO.getBizType());
+ // 2. 鏍¢獙瀹℃壒浜哄瓨鍦ㄤ笖鏈绂佺敤
+ List<Long> userIds = saveReqVO.getUserIds() == null ? Collections.emptyList()
+ : saveReqVO.getUserIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+ validateApproverUsers(userIds);
+ // 3. 淇濆瓨涓昏〃
+ ApprovalConfigDO config = BeanUtils.toBean(saveReqVO, ApprovalConfigDO.class);
+ if (config.getId() == null) {
+ config.setId(null);
+ systemApprovalConfigMapper.insert(config);
+ } else {
+ validateApprovalConfigExists(config.getId());
+ systemApprovalConfigMapper.updateById(config);
+ }
+ // 4. 淇濆瓨瀹℃壒浜猴細鏁翠綋瑕嗙洊锛堝厛鐗╃悊鍒犻櫎鏃х殑锛屽啀鎻掑叆鏂扮殑锛�
+ systemApprovalConfigUserMapper.deleteByConfigId(config.getId());
+ if (CollUtil.isNotEmpty(userIds)) {
+ List<ApprovalConfigUserDO> userDOList = new ArrayList<>(userIds.size());
+ for (int i = 0; i < userIds.size(); i++) {
+ userDOList.add(ApprovalConfigUserDO.builder()
+ .configId(config.getId())
+ .userId(userIds.get(i))
+ .sort(i)
+ .build());
+ }
+ systemApprovalConfigUserMapper.insertBatch(userDOList);
+ }
+ return config.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteApprovalConfig(Long id) {
+ validateApprovalConfigExists(id);
+ systemApprovalConfigMapper.deleteById(id);
+ systemApprovalConfigUserMapper.deleteByConfigId(id);
+ }
+
+ @Override
+ public ApprovalConfigDO getApprovalConfig(Long id) {
+ return systemApprovalConfigMapper.selectById(id);
+ }
+
+ @Override
+ public ApprovalConfigDO getApprovalConfigByBizType(String bizType) {
+ if (bizType == null) {
+ return null;
+ }
+ return systemApprovalConfigMapper.selectByBizType(bizType);
+ }
+
+ @Override
+ public PageResult<ApprovalConfigRespVO> getApprovalConfigPage(ApprovalConfigPageReqVO pageReqVO) {
+ PageResult<ApprovalConfigDO> pageResult = systemApprovalConfigMapper.selectPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return PageResult.empty(pageResult.getTotal());
+ }
+ // 1. 鎵归噺鏌ヨ瀹℃壒浜哄瓙琛�
+ List<ApprovalConfigUserDO> allConfigUsers = systemApprovalConfigUserMapper.selectListByConfigIds(
+ convertSet(pageResult.getList(), ApprovalConfigDO::getId));
+ Map<Long, List<ApprovalConfigUserDO>> configUserMap = allConfigUsers.stream()
+ .collect(Collectors.groupingBy(ApprovalConfigUserDO::getConfigId));
+ // 2. 鎵归噺鏌ヨ瀹℃壒浜烘樀绉�
+ Map<Long, String> nicknameMap = getUserNicknameMap(
+ convertSet(allConfigUsers, ApprovalConfigUserDO::getUserId));
+ // 3. 缁勮杩斿洖
+ List<ApprovalConfigRespVO> list = convertList(pageResult.getList(), config -> {
+ ApprovalConfigRespVO vo = BeanUtils.toBean(config, ApprovalConfigRespVO.class);
+ List<ApprovalConfigUserDO> configUsers = configUserMap.getOrDefault(config.getId(),
+ Collections.emptyList());
+ vo.setUserIds(convertList(configUsers, ApprovalConfigUserDO::getUserId));
+ vo.setUserNames(convertList(configUsers, user -> nicknameMap.get(user.getUserId())));
+ return vo;
+ });
+ return new PageResult<>(list, pageResult.getTotal());
+ }
+
+ @Override
+ public Set<Long> getApproverUserIds(String bizType) {
+ ApprovalConfigDO config = getApprovalConfigByBizType(bizType);
+ if (config == null) {
+ return Collections.emptySet();
+ }
+ return getValidApproverUserIds(config.getId());
+ }
+
+ @Override
+ public Set<Long> validateApprovalEnabledAndGetApprovers(String bizType) {
+ // 1. 鏍¢獙涓氬姟绫诲瀷宸查厤缃鎵�
+ ApprovalConfigDO config = getApprovalConfigByBizType(bizType);
+ if (config == null) {
+ throw exception(APPROVAL_CONFIG_NOT_EXISTS);
+ }
+ // 2. 鏍¢獙宸插惎鐢ㄥ鎵�
+ if (!Boolean.TRUE.equals(config.getApprovalEnabled())) {
+ throw exception(APPROVAL_CONFIG_NOT_ENABLED);
+ }
+ // 3. 鏍¢獙瀛樺湪鏈夋晥瀹℃壒浜猴紙杩囨护鎺夊凡鍒犻櫎 / 宸茬鐢ㄧ殑鐢ㄦ埛锛�
+ Set<Long> approverIds = getValidApproverUserIds(config.getId());
+ if (CollUtil.isEmpty(approverIds)) {
+ throw exception(APPROVAL_CONFIG_APPROVER_EMPTY);
+ }
+ return approverIds;
+ }
+
+ @Override
+ public void validateApprover(String bizType, Long userId) {
+ Set<Long> approverIds = getApproverUserIds(bizType);
+ if (userId == null || !approverIds.contains(userId)) {
+ throw exception(APPROVAL_CONFIG_NOT_APPROVER);
+ }
+ }
+
+ /**
+ * 鏌ヨ閰嶇疆涓嬬殑鏈夋晥瀹℃壒浜虹紪鍙凤紙杩囨护宸插垹闄� / 宸茬鐢ㄧ殑鐢ㄦ埛锛�
+ *
+ * 瀹℃壒浜哄彲鑳藉湪閰嶇疆涔嬪悗琚鐢ㄦ垨鍒犻櫎锛屾澶勭粺涓�杩囨护锛�
+ * 閬垮厤鍑虹幇銆岄厤缃噷鏈変汉銆佷絾瀹為檯鏃犱汉鍙銆嶅鑷村崟鎹崱鍦ㄥ緟瀹℃牳鐘舵�併��
+ *
+ * @param configId 閰嶇疆缂栧彿
+ * @return 鏈夋晥瀹℃壒浜虹紪鍙烽泦鍚�
+ */
+ private Set<Long> getValidApproverUserIds(Long configId) {
+ List<ApprovalConfigUserDO> configUsers = systemApprovalConfigUserMapper.selectListByConfigId(configId);
+ if (CollUtil.isEmpty(configUsers)) {
+ return Collections.emptySet();
+ }
+ Set<Long> userIds = convertSet(configUsers, ApprovalConfigUserDO::getUserId);
+ return convertSet(adminUserApi.getUserList(userIds), AdminUserRespDTO::getId,
+ user -> !CommonStatusEnum.isDisable(user.getStatus()));
+ }
+
+ private Map<Long, String> getUserNicknameMap(Collection<Long> userIds) {
+ if (CollUtil.isEmpty(userIds)) {
+ return Collections.emptyMap();
+ }
+ return adminUserApi.getUserList(userIds).stream()
+ .collect(Collectors.toMap(AdminUserRespDTO::getId, AdminUserRespDTO::getNickname, (a, b) -> a));
+ }
+
+ private void validateApprovalConfigExists(Long id) {
+ if (systemApprovalConfigMapper.selectById(id) == null) {
+ throw exception(APPROVAL_CONFIG_NOT_EXISTS);
+ }
+ }
+
+ private void validateBizTypeUnique(Long id, String bizType) {
+ ApprovalConfigDO config = systemApprovalConfigMapper.selectByBizType(bizType);
+ if (config != null && !config.getId().equals(id)) {
+ throw exception(APPROVAL_CONFIG_BIZ_TYPE_DUPLICATE, bizType);
+ }
+ }
+
+ private void validateApproverUsers(Collection<Long> userIds) {
+ if (CollUtil.isEmpty(userIds)) {
+ return;
+ }
+ List<AdminUserRespDTO> users = adminUserApi.getUserList(userIds);
+ Map<Long, AdminUserRespDTO> userMap = users.stream()
+ .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (a, b) -> a));
+ for (Long userId : userIds) {
+ AdminUserRespDTO user = userMap.get(userId);
+ if (user == null || CommonStatusEnum.isDisable(user.getStatus())) {
+ throw exception(APPROVAL_CONFIG_APPROVER_NOT_EXISTS);
+ }
+ }
+ }
+
+}
--
Gitblit v1.9.3