From e235b3b7f5a7db97fa8a71dc4a638d918af22c0e Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期四, 17 九月 2026 13:33:18 +0800
Subject: [PATCH] feat(sql): 迁移脚本补齐审批配置地基 + 采购计划/订单变更,并加订单审批测试

---
 scripts/test-purchase-order-api.js              |  284 ++++++++++++++++++
 sql/mysql/xgdl_review_audit_pending_changes.sql |  271 ++++++++++++++++-
 scripts/test-purchase-order-ui.js               |  333 ++++++++++++++++++++++
 3 files changed, 869 insertions(+), 19 deletions(-)

diff --git a/scripts/test-purchase-order-api.js b/scripts/test-purchase-order-api.js
new file mode 100644
index 0000000..2441f34
--- /dev/null
+++ b/scripts/test-purchase-order-api.js
@@ -0,0 +1,284 @@
+/**
+ * 閲囪喘璁㈠崟鐘舵�佹満瀹℃壒 - 鎺ュ彛绾ц嚜鍔ㄥ寲娴嬭瘯
+ *
+ * 鑷缓娴嬭瘯鍗曟嵁锛堟瘡娆℃柊寤轰竴寮狅級锛屼笉渚濊禆鍘嗗彶鏁版嵁鐘舵�侊紝鍙噸澶嶆墽琛屻��
+ * 瑕嗙洊锛氭彁浜�/瀹℃牳/椹冲洖/閲嶆柊鎻愪氦/閫氳繃鐨勫畬鏁撮摼璺� + 鍏ㄩ儴鏈嶅姟绔牎楠屽垎鏀��
+ *
+ * 鐢ㄦ硶锛歯ode scripts/test-purchase-order-api.js
+ *   鍙�夌幆澧冨彉閲忥細TEST_USER / TEST_PASS / PRODUCT_ID / PRODUCT_UNIT_ID
+ */
+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';
+const BIZ_TYPE = 'erp_purchase_order_approve';
+// 鐗╂枡鍙栬嚜 mdm_item锛堟祴璇曞簱涓� id=57銆屾祴璇曘�嶏紝鍗曚綅 id=9锛�
+const PRODUCT_ID = Number(process.env.PRODUCT_ID || 57);
+const PRODUCT_UNIT_ID = Number(process.env.PRODUCT_UNIT_ID || 9);
+
+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,
+  });
+  let json = null;
+  try {
+    json = await res.json();
+  } catch {
+    /* 闈� JSON */
+  }
+  return { status: res.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('鐧诲綍鎴愬姛');
+}
+
+/** 纭繚鐩爣涓氬姟绫诲瀷宸插惎鐢ㄤ笖瀹℃壒浜哄惈 admin(id=1)锛岃繑鍥為厤缃璞� */
+async function ensureConfig(userIds) {
+  const { json } = await api('GET', `/system/approval-config/page?pageNo=1&pageSize=100&bizType=${BIZ_TYPE}`);
+  const row = (json?.data?.list || []).find((x) => x.bizType === BIZ_TYPE);
+  if (!row) {
+    fail('鏈壘鍒板鎵归厤缃瀛愭暟鎹�', BIZ_TYPE);
+    return null;
+  }
+  const save = await api('POST', '/system/approval-config/save', {
+    id: row.id,
+    bizType: row.bizType,
+    bizName: row.bizName,
+    approvalEnabled: true,
+    remark: row.remark,
+    userIds,
+  });
+  if (!save.json || save.json.code !== 0) {
+    fail('淇濆瓨瀹℃壒閰嶇疆澶辫触', JSON.stringify(save.json));
+    return null;
+  }
+  const again = await api('GET', `/system/approval-config/page?pageNo=1&pageSize=100&bizType=${BIZ_TYPE}`);
+  return (again.json?.data?.list || []).find((x) => x.bizType === BIZ_TYPE) || row;
+}
+
+/**
+ * 閲囪喘璁㈠崟鐨勪緵搴斿晢鏍¢獙璧� SRM锛圗rpSupplierServiceImpl.validateSupplier -> srmSupplierApi锛夛紝
+ * 浣嗕緵搴斿晢澧炲垹鏀规煡涓庡墠绔笅鎷夊張璇诲啓 erp_supplier锛屼袱澶勬暟鎹簮涓嶄竴鑷淬��
+ * 鍥犳娴嬭瘯鏁版嵁蹇呴』寤哄湪 SRM 渚э紝鍚﹀垯涓嬪崟蹇呯劧鎶ャ�屼緵搴斿晢涓嶅瓨鍦ㄣ�嶃��
+ */
+async function getSupplierId() {
+  const { json } = await api('GET', '/srm/supplier/simple-list');
+  const list = json?.data || [];
+  if (list.length > 0) return list[0].id;
+  const seq = Date.now().toString().slice(-6);
+  const created = await api('POST', '/srm/supplier/create', {
+    code: `TESTSUP${seq}`,
+    name: `鑷姩鍖栨祴璇曚緵搴斿晢${seq}`,
+    status: 0,
+    remark: '鐢� test-purchase-order-api.js 鍒涘缓',
+  });
+  if (created.json?.code !== 0) {
+    fail('鍒涘缓 SRM 渚涘簲鍟嗗け璐�', JSON.stringify(created.json));
+    return null;
+  }
+  return created.json.data;
+}
+
+async function createOrder(supplierId) {
+  const body = {
+    supplierId,
+    orderTime: '2026-09-16 10:00:00',
+    discountPercent: 0,
+    depositPrice: 0,
+    remark: '鑷姩鍖栨祴璇曢噰璐鍗�',
+    items: [
+      {
+        productId: PRODUCT_ID,
+        productUnitId: PRODUCT_UNIT_ID,
+        productPrice: 100,
+        count: 5,
+        taxPercent: 13,
+        remark: '娴嬭瘯椤�',
+      },
+    ],
+  };
+  const { json } = await api('POST', '/purchase/order/create', body);
+  return json;
+}
+
+async function getOrder(id) {
+  const { json } = await api('GET', `/purchase/order/get?id=${id}`);
+  return json?.data;
+}
+
+(async () => {
+  await login();
+
+  console.log('\n[1] 瀹℃壒閰嶇疆');
+  const cfg = await ensureConfig([1]);
+  if (!cfg) process.exit(1);
+  ok('閲囪喘璁㈠崟瀹℃壒宸插惎鐢�', `bizName=${cfg.bizName}`);
+
+  const ap = await api('GET', `/purchase/order/approver-ids`);
+  if (ap.json?.code === 0 && (ap.json.data || []).includes(1)) ok('approver-ids 鍚� admin(1)', JSON.stringify(ap.json.data));
+  else fail('approver-ids 寮傚父', JSON.stringify(ap.json));
+
+  console.log('\n[2] 鏂板缓閲囪喘璁㈠崟');
+  const supplierId = await getSupplierId();
+  if (!supplierId) {
+    console.log('  鏃犳硶鍑嗗渚涘簲鍟嗭紝缁堟');
+    process.exit(1);
+  }
+  ok('渚涘簲鍟嗗氨缁�', `supplierId=${supplierId}`);
+
+  const created = await createOrder(supplierId);
+  if (!created || created.code !== 0) {
+    fail('鍒涘缓閲囪喘璁㈠崟澶辫触', JSON.stringify(created));
+    process.exit(1);
+  }
+  const id = created.data;
+  ok('鍒涘缓閲囪喘璁㈠崟鎴愬姛', `id=${id}`);
+
+  const o0 = await getOrder(id);
+  if (o0?.status === 0) ok('鏂板崟鍒濆鐘舵�佷负鏈彁浜�(0)', `no=${o0.no}`);
+  else fail('鏂板崟鍒濆鐘舵�佷笉鏄� 0', `status=${o0?.status}`);
+
+  console.log('\n[3] BPM 娈嬬暀鎺ュ彛搴斿凡涓嬬嚎');
+  const legacy = await api('GET', '/purchase/order/approve-process-list');
+  if (legacy.status === 404 || (legacy.json && legacy.json.code !== 0)) {
+    ok('approve-process-list 宸茬Щ闄�', `HTTP ${legacy.status} code=${legacy.json?.code}`);
+  } else {
+    fail('approve-process-list 浠嶅彲鐢�', JSON.stringify(legacy.json).slice(0, 120));
+  }
+
+  console.log('\n[4] 鏈彁浜や笉鑳藉鏍�');
+  const early = await api('PUT', '/purchase/order/audit', { id, pass: true, reviewRemark: '鎶㈣窇' });
+  if (early.json?.code === 1_030_101_016) ok('姝g‘鎷掔粷锛氶潪瀹℃壒涓笉鍙鏍�', `code=${early.json.code}`);
+  else fail('鏈寜棰勬湡鎷掔粷', JSON.stringify(early.json));
+
+  console.log('\n[5] 鎻愪氦 鈫� 瀹℃壒涓�');
+  const sub = await api('PUT', `/purchase/order/submit?id=${id}`);
+  if (sub.json?.code === 0) ok('鎻愪氦鎴愬姛', JSON.stringify(sub.json));
+  else fail('鎻愪氦澶辫触', JSON.stringify(sub.json));
+  const o1 = await getOrder(id);
+  if (o1?.status === 10) ok('鐘舵�佸凡缃负瀹℃壒涓�(10)');
+  else fail('鎻愪氦鍚庣姸鎬佷笉瀵�', `status=${o1?.status}`);
+
+  console.log('\n[6] 瀹℃壒涓笉鍙慨鏀�');
+  const upd = await api('PUT', '/purchase/order/update', {
+    id,
+    supplierId,
+    orderTime: '2026-09-16 10:00:00',
+    remark: '瀹℃壒涓敼鍗�',
+    items: [{ productId: PRODUCT_ID, productUnitId: PRODUCT_UNIT_ID, productPrice: 100, count: 5, taxPercent: 13 }],
+  });
+  if (upd.json?.code === 1_030_101_019) ok('姝g‘鎷掔粷锛氬鎵逛腑涓嶅彲淇敼', `code=${upd.json.code}`);
+  else fail('瀹℃壒涓眳鐒跺彲淇敼', JSON.stringify(upd.json));
+
+  console.log('\n[7] 闈炲鎵逛汉涓嶅彲瀹℃牳');
+  await ensureConfig([111]); // 涓存椂鎶婂鎵逛汉鏀规垚銆屾祴璇曠敤鎴枫��
+  const notAp = await api('PUT', '/purchase/order/audit', { id, pass: true, reviewRemark: '瓒婃潈' });
+  if (notAp.json?.code === 1_002_030_005) ok('姝g‘鎷掔粷锛氬綋鍓嶇敤鎴蜂笉鏄鎵逛汉', `code=${notAp.json.code}`);
+  else fail('瓒婃潈瀹℃牳鏈鎷︽埅', JSON.stringify(notAp.json));
+  await ensureConfig([1]); // 杩樺師
+  const back = await api('GET', '/purchase/order/approver-ids');
+  if ((back.json?.data || []).includes(1)) ok('瀹℃壒浜哄凡杩樺師涓� admin');
+  else fail('瀹℃壒浜鸿繕鍘熷け璐�', JSON.stringify(back.json?.data));
+
+  console.log('\n[8] 椹冲洖蹇呴』濉師鍥�');
+  const noWhy = await api('PUT', '/purchase/order/audit', { id, pass: false });
+  if (noWhy.json?.code === 1_030_101_017) ok('姝g‘鎷掔粷锛氱己椹冲洖鍘熷洜', `code=${noWhy.json.code}`);
+  else fail('缂哄師鍥犳湭琚嫤鎴�', JSON.stringify(noWhy.json));
+
+  console.log('\n[9] 瀹℃牳涓嶉�氳繃 鈫� 閫�鍥�');
+  const rej = await api('PUT', '/purchase/order/audit', { id, pass: false, reviewRemark: '浠锋牸鍋忛珮锛岄��鍥為噸璁�' });
+  if (rej.json?.code === 0) ok('椹冲洖鎴愬姛', JSON.stringify(rej.json));
+  else fail('椹冲洖澶辫触', JSON.stringify(rej.json));
+  const o2 = await getOrder(id);
+  if (o2?.status === 30) ok('鐘舵�佷负瀹℃牳涓嶉�氳繃(30)');
+  else fail('椹冲洖鍚庣姸鎬佷笉瀵�', `status=${o2?.status}`);
+  if (o2?.reviewerName && o2?.reviewRemark === '浠锋牸鍋忛珮锛岄��鍥為噸璁�') ok('瀹℃牳浜轰笌椹冲洖鍘熷洜宸茶惤搴�', `${o2.reviewerName} / ${o2.reviewRemark}`);
+  else fail('瀹℃牳淇℃伅鏈惤搴�', JSON.stringify({ rn: o2?.reviewerName, rr: o2?.reviewRemark }));
+
+  console.log('\n[10] 閫�鍥炲悗鍙慨鏀瑰苟閲嶆柊鎻愪氦');
+  const upd2 = await api('PUT', '/purchase/order/update', {
+    id,
+    supplierId,
+    orderTime: '2026-09-16 10:00:00',
+    remark: '宸查檷浠烽噸璁�',
+    items: [{ productId: PRODUCT_ID, productUnitId: PRODUCT_UNIT_ID, productPrice: 80, count: 5, taxPercent: 13 }],
+  });
+  if (upd2.json?.code === 0) ok('閫�鍥炲悗鍙慨鏀�', JSON.stringify(upd2.json));
+  else fail('閫�鍥炲悗浠嶄笉鍙慨鏀�', JSON.stringify(upd2.json));
+
+  const sub2 = await api('PUT', `/purchase/order/submit?id=${id}`);
+  if (sub2.json?.code === 0) ok('閲嶆柊鎻愪氦鎴愬姛');
+  else fail('閲嶆柊鎻愪氦澶辫触', JSON.stringify(sub2.json));
+  const o3 = await getOrder(id);
+  if (o3?.status === 10 && !o3?.reviewRemark && !o3?.reviewerName) ok('閲嶆柊鎻愪氦宸叉竻绌轰笂涓�杞鏍镐俊鎭�', `reviewRemark=${JSON.stringify(o3?.reviewRemark ?? null)}`);
+  else fail('閲嶆彁鍚庡鏍镐俊鎭畫鐣�', JSON.stringify({ st: o3?.status, rn: o3?.reviewerName, rr: o3?.reviewRemark }));
+
+  console.log('\n[11] 瀹℃牳閫氳繃');
+  const app = await api('PUT', '/purchase/order/audit', { id, pass: true, reviewRemark: '鍚屾剰閲囪喘' });
+  if (app.json?.code === 0) ok('瀹℃牳閫氳繃鎴愬姛', JSON.stringify(app.json));
+  else fail('瀹℃牳閫氳繃澶辫触', JSON.stringify(app.json));
+  const o4 = await getOrder(id);
+  if (o4?.status === 20) ok('鐘舵�佷负瀹℃牳閫氳繃(20)', `瀹℃牳浜�=${o4.reviewerName}`);
+  else fail('閫氳繃鍚庣姸鎬佷笉瀵�', `status=${o4?.status}`);
+
+  console.log('\n[12] 宸插鏍镐笉鍙慨鏀�');
+  const upd3 = await api('PUT', '/purchase/order/update', {
+    id,
+    supplierId,
+    orderTime: '2026-09-16 10:00:00',
+    remark: '瀹℃牳鍚庡啀鏀�',
+    items: [{ productId: PRODUCT_ID, productUnitId: PRODUCT_UNIT_ID, productPrice: 80, count: 5, taxPercent: 13 }],
+  });
+  if (upd3.json?.code === 1_030_101_005) ok('姝g‘鎷掔粷锛氬凡瀹℃牳涓嶅彲淇敼', `code=${upd3.json.code}`);
+  else fail('宸插鏍镐粛鍙慨鏀�', JSON.stringify(upd3.json));
+
+  console.log('\n[13] 鍏ㄧ▼涓嶅簲鍐嶅嚭鐜� 500 绯荤粺寮傚父');
+  const calls = [
+    ['GET', `/purchase/order/page?pageNo=1&pageSize=10`],
+    ['GET', `/purchase/order/approver-ids`],
+  ];
+  let bad500 = 0;
+  for (const [m, p] of calls) {
+    const r = await api(m, p);
+    if (r.json?.code === 500) {
+      bad500++;
+      console.log(`     ${m} ${p} 鈫� 500`);
+    }
+  }
+  if (bad500 === 0) ok('鍏抽敭鎺ュ彛鏃� 500');
+  else fail(`浠嶆湁 ${bad500} 涓帴鍙h繑鍥� 500`);
+
+  console.log(`\n娴嬭瘯鍗曟嵁锛歟rp_purchase_order id=${id}锛�${o0?.no}锛塦);
+  const passed = results.filter((r) => r.pass).length;
+  console.log(`===== 姹囨�伙細${passed}/${results.length} 閫氳繃 =====`);
+  results.filter((r) => !r.pass).forEach((r) => console.log(`  澶辫触: ${r.name} ${r.extra}`));
+  process.exit(passed === results.length ? 0 : 1);
+})().catch((e) => {
+  console.error('鎵ц寮傚父:', e.message);
+  process.exit(1);
+});
diff --git a/scripts/test-purchase-order-ui.js b/scripts/test-purchase-order-ui.js
new file mode 100644
index 0000000..b256e65
--- /dev/null
+++ b/scripts/test-purchase-order-ui.js
@@ -0,0 +1,333 @@
+/**
+ * 閲囪喘璁㈠崟瀹℃壒 - 娴忚鍣ㄧ鍒扮娴嬭瘯
+ *
+ * 鍓嶇疆鏁版嵁鐢� admin-api 鐩存帴鍑嗗锛堜竴娆¤窇瀹岀暀涓嬶細1 寮犳湭鎻愪氦 + 1 寮犲鎵逛腑锛夛紝
+ * 涔嬪悗鍏ㄩ儴璧扮湡瀹炴祻瑙堝櫒浜や簰楠岃瘉鎸夐挳鍙鎬с�佸脊绐楁牎楠屼笌鐘舵�佹祦杞��
+ *
+ * 鐢ㄦ硶锛歯ode scripts/test-purchase-order-ui.js
+ */
+const { chromium } = require('playwright');
+const path = require('path');
+const fs = require('fs');
+
+const API = process.env.TEST_API || 'http://127.0.0.1:48080/admin-api';
+const BASE = process.env.TEST_BASE || 'http://localhost:5666';
+const USER = process.env.TEST_USER || 'admin';
+const PASS = process.env.TEST_PASS || 'admin123';
+const BIZ_TYPE = 'erp_purchase_order_approve';
+const PRODUCT_ID = Number(process.env.PRODUCT_ID || 57);
+const PRODUCT_UNIT_ID = Number(process.env.PRODUCT_UNIT_ID || 9);
+
+const SHOT_DIR = path.join(__dirname, 'shots');
+if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
+
+const results = [];
+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 : ''}`);
+}
+
+let TOKEN = '';
+async function api(method, p, body) {
+  const res = await fetch(API + p, {
+    method,
+    headers: {
+      'Content-Type': 'application/json',
+      'tenant-id': '1',
+      ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
+    },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+  try {
+    return await res.json();
+  } catch {
+    return null;
+  }
+}
+
+async function seed() {
+  console.log('\n[S] 鍑嗗娴嬭瘯鏁版嵁锛堟帴鍙o級');
+  const lg = await api('POST', '/system/auth/login', { username: USER, password: PASS });
+  if (!lg || lg.code !== 0) {
+    fail('鎺ュ彛鐧诲綍澶辫触', JSON.stringify(lg));
+    process.exit(1);
+  }
+  TOKEN = lg.data.accessToken;
+  ok('鎺ュ彛鐧诲綍鎴愬姛');
+
+  const page = await api('GET', `/system/approval-config/page?pageNo=1&pageSize=100&bizType=${BIZ_TYPE}`);
+  const cfg = (page?.data?.list || []).find((x) => x.bizType === BIZ_TYPE);
+  if (!cfg) {
+    fail('缂哄皯瀹℃壒閰嶇疆绉嶅瓙');
+    process.exit(1);
+  }
+  await api('POST', '/system/approval-config/save', {
+    id: cfg.id,
+    bizType: cfg.bizType,
+    bizName: cfg.bizName,
+    approvalEnabled: true,
+    remark: cfg.remark,
+    userIds: [1],
+  });
+  ok('瀹℃壒宸插惎鐢紝瀹℃壒浜�=admin');
+
+  // 姣忔璺戦兘鐢ㄥ敮涓�鍛藉悕鐨勪緵搴斿晢锛屼究浜庡湪鍒楄〃閲岀簿纭攣瀹氭湰娆$瀛愬崟鎹�
+  const seq = Date.now().toString().slice(-6);
+  const supplierName = `UIAUTOSUP${seq}`;
+  let supplierId = null;
+  const c = await api('POST', '/srm/supplier/create', {
+    code: `UIAUTOSUP${seq}`,
+    name: supplierName,
+    status: 0,
+  });
+  supplierId = c?.data;
+  if (!supplierId) {
+    const sup = await api('GET', '/srm/supplier/simple-list');
+    const hit = (sup?.data || []).find((x) => x.name === supplierName) || (sup?.data || [])[0];
+    supplierId = hit?.id;
+  }
+  if (!supplierId) {
+    fail('鏃犳硶鍑嗗渚涘簲鍟嗭紙娉ㄦ剰 ERP/SRM 鏁版嵁婧愪笉涓�鑷撮棶棰橈級', JSON.stringify(c));
+    process.exit(1);
+  }
+  ok('渚涘簲鍟嗗氨缁�', `supplierId=${supplierId} name=${supplierName}`);
+
+  const mk = async () => {
+    return api('POST', '/purchase/order/create', {
+      supplierId,
+      orderTime: '2026-09-16 10:00:00',
+      discountPercent: 0,
+      depositPrice: 0,
+      remark: supplierName,
+      items: [
+        {
+          productId: PRODUCT_ID,
+          productUnitId: PRODUCT_UNIT_ID,
+          productPrice: 100,
+          count: 5,
+          taxPercent: 13,
+        },
+      ],
+    });
+  };
+
+  const a = await mk();
+  const b = await mk();
+  if (a?.code !== 0 || b?.code !== 0) {
+    fail('鍒涘缓娴嬭瘯璁㈠崟澶辫触', JSON.stringify({ a, b }));
+    process.exit(1);
+  }
+  const draftId = a.data;
+  const approvingId = b.data;
+  const sub = await api('PUT', `/purchase/order/submit?id=${approvingId}`);
+  if (sub?.code !== 0) {
+    fail('棰勭疆銆屽鎵逛腑銆嶅崟鎹け璐�', JSON.stringify(sub));
+    process.exit(1);
+  }
+  ok('鏁版嵁灏辩华', `鑽夌 id=${draftId}锛屽鎵逛腑 id=${approvingId}`);
+  return { approvingId, draftId, supplierName };
+}
+
+async function shot(page, name) {
+  const p = path.join(SHOT_DIR, `${name}.png`);
+  await page.screenshot({ path: p, fullPage: true });
+  console.log(`  馃摳 ${p}`);
+}
+
+/**
+ * 鍙栨暟鎹涓庡浐瀹氬彸鍒楄锛屾寜绱㈠紩閰嶅銆�
+ * 娉ㄦ剰锛氭湰椤点�岀姸鎬併�嶅垪鏄� fixed:right锛屼笌鎿嶄綔鎸夐挳鍚屽鍥哄畾鍙冲垪瀹瑰櫒锛�
+ * 鍥犳 main 閲屼笉鍚姸鎬侊紝蹇呴』浠� fixed 鏂囨湰閲岃鐘舵�併��
+ */
+async function rowPairs(page) {
+  const dataRows = await page.locator('.vxe-table--main-wrapper .vxe-body--row').all();
+  const fixedRows = await page.locator('.vxe-table--fixed-right-wrapper .vxe-body--row').all();
+  const out = [];
+  const n = Math.min(dataRows.length, fixedRows.length);
+  for (let i = 0; i < n; i++) {
+    out.push({
+      main: (await dataRows[i].innerText()).replace(/\s+/g, ' '),
+      fixed: (await fixedRows[i].innerText()).replace(/\s+/g, ' ').trim(),
+      actionRow: fixedRows[i],
+    });
+  }
+  return out;
+}
+
+(async () => {
+  const seeded = await seed();
+
+  const browser = await chromium.launch({ headless: true, channel: 'chrome' });
+  const page = await browser.newPage({ viewport: { width: 1680, height: 950 } });
+
+  const apiErrors = [];
+  page.on('response', async (r) => {
+    if (!r.url().includes('/admin-api/')) 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 {
+      /* ignore */
+    }
+  });
+
+  try {
+    // 1. 鐧诲綍
+    console.log('\n[1] 娴忚鍣ㄧ櫥褰�');
+    await page.goto(`${BASE}/auth/login`, { waitUntil: 'networkidle', timeout: 60000 });
+    await page.locator('input[placeholder="璇疯緭鍏ョ敤鎴峰悕"]').first().fill(USER);
+    await page.locator('input[type="password"]').first().fill(PASS);
+    await page.locator('button[aria-label="login"]').first().click();
+    await page.waitForURL((u) => !u.pathname.includes('/login'), { timeout: 30000 });
+    ok('鐧诲綍鎴愬姛');
+
+    // 2. 瀹℃壒閰嶇疆椤靛簲鑳界湅鍒伴噰璐鍗曞鏍�
+    console.log('\n[2] 瀹℃壒閰嶇疆椤�');
+    await page.goto(`${BASE}/system/approval-config`, { waitUntil: 'networkidle', timeout: 60000 });
+    await page.waitForTimeout(2500);
+    const cfgRow = page.locator('.vxe-body--row', { hasText: '閲囪喘璁㈠崟瀹℃牳' });
+    if ((await cfgRow.count()) > 0) {
+      const t = (await cfgRow.first().innerText()).replace(/\s+/g, ' ');
+      if (t.includes('宸插惎鐢�')) ok('閲囪喘璁㈠崟瀹℃壒鏄剧ず宸插惎鐢�', t.slice(0, 100));
+      else fail('閲囪喘璁㈠崟瀹℃壒鏈樉绀哄凡鍚敤', t.slice(0, 100));
+      if (t.includes('瓒呯骇绠$悊鍛�')) ok('瀹℃壒浜烘樉绀鸿秴绾х鐞嗗憳');
+      else fail('瀹℃壒浜烘湭鏄剧ず', t.slice(0, 100));
+    } else {
+      fail('閰嶇疆椤垫壘涓嶅埌銆岄噰璐鍗曞鏍搞�嶈');
+    }
+    await shot(page, 'po-01-config');
+
+    // 3. 閲囪喘璁㈠崟鍒楄〃鎸夐挳鍙鎬�
+    console.log('\n[3] 閲囪喘璁㈠崟鍒楄〃鎸夐挳');
+    await page.goto(`${BASE}/purchase/order`, { waitUntil: 'networkidle', timeout: 60000 });
+    await page.waitForTimeout(3000);
+    await shot(page, 'po-02-list');
+
+    const pairs = await rowPairs(page);
+    if (pairs.length === 0) {
+      fail('鍒楄〃鏃犳暟鎹');
+      throw new Error('empty grid');
+    }
+    // 鏈绉嶅瓙鍗曟嵁闈犲敮涓�渚涘簲鍟嗗悕瀹氫綅锛堝垪琛ㄤ笉灞曠ず澶囨敞鍒楋級
+    const mine = pairs.filter((p) => p.main.includes(seeded.supplierName));
+    if (mine.length < 2) {
+      fail('鏈兘鍦ㄥ垪琛ㄤ腑瀹氫綅鍒版湰娆$瀛愯鍗�', `鍛戒腑 ${mine.length} 琛� / 鍏� ${pairs.length} 琛宍);
+      pairs.forEach((p) => console.log('     ROW:', p.main.slice(0, 60), '||', p.fixed));
+    } else {
+      ok('鎸変緵搴斿晢鍚嶅畾浣嶅埌鏈绉嶅瓙鍗曟嵁', `${mine.length} 琛宍);
+    }
+    const draft = mine.find((p) => p.fixed.includes('鑽夌'));
+    const approving = mine.find((p) => p.fixed.includes('瀹℃壒涓�'));
+    if (draft) ok('鑽夌鍗曟覆鏌撲负銆岃崏绋裤��', draft.fixed);
+    else fail('鏈壘鍒拌崏绋跨姸鎬佺殑绉嶅瓙璁㈠崟', mine.map((p) => p.fixed).join(' | '));
+    if (approving) ok('瀹℃壒涓崟娓叉煋涓恒�屽鎵逛腑銆�', approving.fixed);
+    else fail('鏈壘鍒板鎵逛腑鐘舵�佺殑绉嶅瓙璁㈠崟', mine.map((p) => p.fixed).join(' | '));
+
+    if (draft) {
+      const hasSubmit = /鎻愪氦/.test(draft.fixed);
+      const hasAudit = draft.fixed.includes('瀹℃牳');
+      const hasEdit = /(淇敼|缂栬緫)/.test(draft.fixed);
+      if (hasSubmit && hasEdit && !hasAudit) ok('鑽夌琛岋細鏈夈�屾彁浜ゃ�嶃�屼慨鏀广�嶏紝鏃犲鏍告寜閽�', draft.fixed);
+      else fail('鑽夌琛屾寜閽笉绗�', draft.fixed);
+    }
+    if (approving) {
+      const hasBoth =
+        approving.fixed.includes('瀹℃牳涓嶉�氳繃') &&
+        /瀹℃牳(?!\s*涓嶉�氳繃)/.test(approving.fixed.replace('瀹℃牳涓嶉�氳繃', ''));
+      const noSubmitEdit = !/鎻愪氦/.test(approving.fixed) && !/(淇敼|缂栬緫)/.test(approving.fixed);
+      if (hasBoth && noSubmitEdit) ok('瀹℃壒涓锛氭湁銆屽鏍搞�嶃�屽鏍镐笉閫氳繃銆嶏紝鏃犳彁浜�/淇敼', approving.fixed);
+      else fail('瀹℃壒涓鎸夐挳涓嶇', approving.fixed);
+    }
+
+    // 4. 璧颁竴閬嶅鏍镐笉閫氳繃寮圭獥锛堝惈蹇呭~鏍¢獙锛�
+    if (approving) {
+      console.log('\n[4] 瀹℃牳涓嶉�氳繃寮圭獥');
+      await approving.actionRow.getByText('瀹℃牳涓嶉�氳繃', { exact: true }).first().click();
+      await page.waitForTimeout(1200);
+      const dialog = page.locator('[role="dialog"]').last();
+      if ((await dialog.count()) === 0) {
+        fail('鏈脊鍑哄鏍稿脊绐�');
+      } else {
+        const dlgText = (await dialog.innerText()).replace(/\s+/g, ' ');
+        ok('寮圭獥宸叉墦寮�', dlgText.slice(0, 120));
+        const btns = await dialog.locator('button').allInnerTexts();
+        console.log('     寮圭獥鎸夐挳:', JSON.stringify(btns.map((b) => b.trim()).filter(Boolean)));
+
+        // 绌哄師鍥犵洿鎺ョ‘璁わ紝搴旇鍓嶇蹇呭~鎷︿綇锛堝脊绐椾笉鍏筹級
+        const confirmBtn = dialog
+          .locator('button')
+          .filter({ hasText: /纭甛s*璁纭甛s*瀹殀鎻怽s*浜OK/ })
+          .last();
+        await confirmBtn.click();
+        await page.waitForTimeout(1200);
+        const stillOpen = (await page.locator('[role="dialog"]').count()) > 0;
+        if (stillOpen) ok('鏈~鍘熷洜鏃惰蹇呭~鏍¢獙鎷︿笅');
+        else fail('鏈~鍘熷洜绔熸彁浜ゆ垚鍔燂紙鍓嶇鏍¢獙缂哄け锛�');
+
+        // 濉師鍥犲悗纭
+        await dialog.locator('textarea').first().fill('UI娴嬭瘯锛氬崟浠峰亸楂橈紝閫�鍥為噸璁�');
+        const resp = page
+          .waitForResponse(
+            (r) => r.url().includes('/purchase/order/audit'),
+            { timeout: 15000 },
+          )
+          .catch(() => null);
+        await confirmBtn.click();
+        const res = await resp;
+        if (!res) fail('鏈崟鑾� audit 鍝嶅簲');
+        else {
+          const body = await res.json().catch(() => null);
+          if (body?.code === 0) ok('椹冲洖鎻愪氦鎴愬姛', JSON.stringify(body));
+          else fail('椹冲洖澶辫触', JSON.stringify(body));
+        }
+        await page.waitForTimeout(2000);
+      }
+      await shot(page, 'po-03-after-reject');
+
+      // 5. 鐘舵�佸簲鍙樹负瀹℃壒涓嶉�氳繃锛屼笖鍙噸鏂颁慨鏀�
+      await page.reload({ waitUntil: 'networkidle' });
+      await page.waitForTimeout(3000);
+      const pairs2 = await rowPairs(page);
+      const mine2 = pairs2.filter((p) => p.main.includes(seeded.supplierName));
+      const rejected = mine2.find((p) => p.fixed.includes('瀹℃壒涓嶉�氳繃'));
+      if (rejected) {
+        ok('椹冲洖鍚庢覆鏌撲负銆屽鎵逛笉閫氳繃銆�', rejected.fixed);
+        const canResubmit = /鎻愪氦/.test(rejected.fixed) && /(淇敼|缂栬緫)/.test(rejected.fixed);
+        const noAudit = !rejected.fixed.includes('瀹℃牳');
+        if (canResubmit && noAudit) ok('椹冲洖鍗曞彲銆屼慨鏀广�嶅苟鍙噸鏂般�屾彁浜ゃ�嶏紝涓斾笉鍐嶆樉绀哄鏍告寜閽�', rejected.fixed);
+        else fail('椹冲洖鍗曞叆鍙d笉绗�', rejected.fixed);
+      } else {
+        fail('鏈壘鍒板鎵逛笉閫氳繃鐨勭瀛愯鍗�', mine2.map((p) => p.fixed).join(' | '));
+      }
+      await shot(page, 'po-04-rejected');
+    }
+
+    console.log(`\n锛堢瀛愬崟鎹� id锛氳崏绋�=${seeded.draftId} 瀹℃壒涓啋椹冲洖=${seeded.approvingId}锛塦);
+  } catch (e) {
+    fail('鎵ц寮傚父', e.message);
+    await shot(page, 'po-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') &&
+      !e.url.includes('unread-count'),
+  );
+  if (relevant.length === 0) console.log('  锛堟棤锛�');
+  relevant.forEach((e) => console.log(`  ${e.url} 鈫� code=${e.code} msg=${e.msg}`));
+
+  await browser.close();
+  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(passed === results.length ? 0 : 1);
+})();
diff --git a/sql/mysql/xgdl_review_audit_pending_changes.sql b/sql/mysql/xgdl_review_audit_pending_changes.sql
index 38e98c2..5631353 100644
--- a/sql/mysql/xgdl_review_audit_pending_changes.sql
+++ b/sql/mysql/xgdl_review_audit_pending_changes.sql
@@ -3,9 +3,21 @@
 -- 鐩爣搴擄細mom-xgdl
 -- 璇存槑锛氭湰鑴氭湰骞傜瓑锛屽彲閲嶅鎵ц锛涘凡瀛樺湪鐨勫瓧娈�/鑿滃崟浼氳嚜鍔ㄨ烦杩囥��
 --       寮�鍙戝簱锛�192.168.197.128锛夊凡鍏ㄩ儴鎵ц杩囷紝鏈剼鏈敤浜庡悜鍏跺畠鐜锛堝娴嬭瘯/鐢熶骇锛夊悓姝ャ��
+-- 鍐呭缁撴瀯锛�
+--   绗竴閮ㄥ垎  20 寮犲崟鎹〃鐨勫鏍镐汉鍥涘瓧娈�
+--   绗簩閮ㄥ垎  涓婅堪鍗曟嵁鐨勩�屽鏍搞�嶆寜閽潈闄愶紙system_menu type=3锛�
+--   绗笁閮ㄥ垎  鐢靛姏浣滀笟璁″垝鐢熸垚宸ュ崟缂栫爜瑙勫垯璇存槑锛堟棤闇�鎵ц锛�
+--   绗洓閮ㄥ垎  閫氱敤瀹℃壒閰嶇疆琛� + 26 涓笟鍔$被鍨� + 瀹℃壒閰嶇疆椤佃彍鍗�
+--             + 閲囪喘璁″垝/閲囪喘璁㈠崟瀹℃牳瀛楁涓庢寜閽� + 瑙掕壊鎺堟潈
+--
+-- 鎵ц鍓嶇疆瑕佹眰锛氬繀椤荤敤鏀寔 DELIMITER 涓庡瓨鍌ㄨ繃绋嬬殑瀹㈡埛绔暣浣撴墽琛�
+--   锛坢ysql CLI锛歮ysql -h<host> -P<port> -u<user> -p mom-xgdl < 鏈枃浠讹級銆�
+--   澶氭暟鍥惧舰鍖�/鍦ㄧ嚎 SQL 鎺у埗鍙版寜鍒嗗彿鎷嗗垎璇彞锛屼細鎶� DELIMITER $$ 娈佃惤鎷嗗潖銆�
+--
 -- 瀵瑰簲浠g爜鎻愪氦锛�
 --   refactor(crm/erp/hrm/mes): 鍗曟嵁瀹℃牳鏀逛负瀹℃壒閰嶇疆鐘舵�佹満
 --   fix(mes): 鐢靛姏浣滀笟璁″垝鐢熸垚宸ュ崟琛ュ厖缂栫爜瑙勫垯鍙栧彿
+--   e347fd6e refactor(approval): 閲囪喘璁″垝/閲囪喘璁㈠崟瀹℃壒鐢� BPM 鏀逛负杞婚噺绾х姸鎬佹満
 -- ============================================================================
 
 SET NAMES utf8mb4;
@@ -154,115 +166,115 @@
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '閿�鎴风敵璇峰鏍�' AS name, 'crm:cancellation:audit' AS perm, 'crm:cancellation:update' AS anchor) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'crm:cancellation:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:cancellation:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:cancellation:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鍚堝悓瀹℃牳' AS name, 'crm:contract:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'crm:contract:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:contract:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:contract:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鍙戠エ瀹℃牳' AS name, 'crm:invoice:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'crm:invoice:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:invoice:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:invoice:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '閫�璐瑰崟瀹℃牳' AS name, 'crm:refund:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'crm:refund:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:refund:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'crm:refund:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '浠樻鍗曞鏍�' AS name, 'erp:finance-payment:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'erp:finance-payment:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:finance-payment:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:finance-payment:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '閲囪喘鐢宠瀹℃牳' AS name, 'erp:purchase-request:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'erp:purchase-request:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:purchase-request:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:purchase-request:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '璇峰亣鐢宠瀹℃牳' AS name, 'hrm:leave:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:leave:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:leave:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:leave:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '杞鐢宠瀹℃牳' AS name, 'hrm:regularize:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:regularize:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:regularize:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:regularize:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '绂昏亴鐢宠瀹℃牳' AS name, 'hrm:resignation:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:resignation:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:resignation:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:resignation:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '璋冨姩鐢宠瀹℃牳' AS name, 'hrm:transfer:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:transfer:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:transfer:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:transfer:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鑰冨嫟寮傚父瀹℃牳' AS name, 'hrm:attendance-exception:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:attendance-exception:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:attendance-exception:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:attendance-exception:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '椤圭洰瀹℃牳' AS name, 'mes:pd-project:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:pd-project:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:pd-project:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:pd-project:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鎶�鏈‘璁ゅ嚱瀹℃牳' AS name, 'mes:pd-tech-confirm:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:pd-tech-confirm:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:pd-tech-confirm:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:pd-tech-confirm:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '閲囪喘鍏ュ簱鍗曞鏍�' AS name, 'mes:wm-item-receipt:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm-item-receipt:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-item-receipt:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-item-receipt:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鏉傞」鍑哄簱鍗曞鏍�' AS name, 'mes:wm-misc-issue:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm-misc-issue:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-misc-issue:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-misc-issue:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '閲囪喘閫�璐у崟瀹℃牳' AS name, 'mes:wm-return-vendor:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm-return-vendor:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-return-vendor:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-return-vendor:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鐩樼偣浠诲姟瀹℃牳' AS name, 'mes:wm-stock-taking-task:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm-stock-taking-task:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-stock-taking-task:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm-stock-taking-task:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '璁惧鍏ュ簱鍗曞鏍�' AS name, 'mes:wm:equipment-receipt:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm:equipment-receipt:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm:equipment-receipt:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm:equipment-receipt:audit') m);
 
 INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
 SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
 FROM (SELECT '鏉傞」鍏ュ簱鍗曞鏍�' AS name, 'mes:wm:misc-receipt:audit' AS perm) t
 JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:wm:misc-receipt:update') a ON 1 = 1
-WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm:misc-receipt:audit') m);
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:wm:misc-receipt:audit') m);
 
 -- ============================================================================
 -- 绗笁閮ㄥ垎锛氱數鍔涗綔涓氳鍒掔敓鎴愬伐鍗� 鈥斺�� 宸ュ崟缂栫爜鍙栧彿瑙勫垯锛堟棤鍙樻洿锛屼粎璇存槑锛�
@@ -273,7 +285,228 @@
 --   锛堟敞鎰忥細璇ヨ剼鏈細 TRUNC 寮忛噸寤鸿鍒欒〃锛屼粎閫傜敤浜庨娆″垵濮嬪寲锛屽嬁鍦ㄥ凡鏈夋暟鎹殑鐜鎵ц锛夈��
 -- ============================================================================
 
+-- ============================================================================
+-- 绗洓閮ㄥ垎锛氶�氱敤瀹℃壒閰嶇疆锛堝鎵瑰紩鎿庣殑鍦板熀锛�
+--
+-- 鍓嶄笁閮ㄥ垎鍙姞浜嗐�屽鏍镐汉瀛楁銆嶅拰銆屽鏍告寜閽潈闄愩�嶏紝浣嗘病鏈夊缓瀹℃壒閰嶇疆琛ㄦ湰韬��
+-- 鍙窇鍓嶄笁閮ㄥ垎鐨勮瘽锛屾墍鏈夊崟鎹殑銆屾彁浜ゃ�嶉兘浼氬湪 validateApprovalEnabledAndGetApprovers
+-- 澶勫け璐ワ紙瀹℃壒閰嶇疆涓嶅瓨鍦� / 鏈惎鐢� / 鏈厤缃鎵逛汉锛夈�傛湰閮ㄥ垎琛ラ綈锛�
+--   4.1 瀹℃壒閰嶇疆涓昏〃 + 瀹℃壒浜哄瓙琛�
+--   4.2 26 涓笟鍔$被鍨嬬瀛愶紙榛樿鏈惎鐢級
+--   4.3 瀹℃壒閰嶇疆椤甸潰鑿滃崟涓庢煡璇�/淇濆瓨鎸夐挳
+--   4.4 閲囪喘璁″垝銆侀噰璐鍗曠殑瀹℃牳浜哄洓瀛楁
+--   4.5 閲囪喘璁″垝銆侀噰璐鍗曠殑瀹℃牳鎸夐挳鏉冮檺
+--   4.6 瑙掕壊鎺堟潈锛堟湰妗嗘灦鏃犮�岃秴绠$粫杩囨潈闄愩�嶉�昏緫锛屼笉鎺堟潈鍒� admin 涔熺湅涓嶅埌鎸夐挳锛�
+--
+-- 瀵瑰簲浠g爜鎻愪氦锛歟347fd6e refactor(approval): 閲囪喘璁″垝/閲囪喘璁㈠崟瀹℃壒鐢� BPM 鏀逛负杞婚噺绾х姸鎬佹満
+-- ============================================================================
+
+-- 4.1 瀹℃壒閰嶇疆涓昏〃 / 瀹℃壒浜哄瓙琛�
+CREATE TABLE IF NOT EXISTS system_approval_config (
+  id bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  biz_type varchar(64) NOT NULL COMMENT '涓氬姟绫诲瀷缂栫爜锛屼笌鍚勬ā鍧楀鎵瑰垎绫荤紪鐮佷竴鑷�',
+  biz_name varchar(100) NOT NULL COMMENT '涓氬姟绫诲瀷鍚嶇О',
+  approval_enabled bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍚敤瀹℃壒',
+  remark varchar(500) NULL DEFAULT NULL COMMENT '澶囨敞',
+  creator varchar(64) NULL DEFAULT '' COMMENT '鍒涘缓鑰�',
+  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  updater varchar(64) NULL DEFAULT '' COMMENT '鏇存柊鑰�',
+  update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+  deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+  PRIMARY KEY (id) USING BTREE,
+  KEY idx_biz_type (biz_type) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '閫氱敤瀹℃壒閰嶇疆琛�';
+
+CREATE TABLE IF NOT EXISTS system_approval_config_user (
+  id bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+  config_id bigint NOT NULL COMMENT '瀹℃壒閰嶇疆ID锛堝叧鑱� system_approval_config.id锛�',
+  user_id bigint NOT NULL COMMENT '瀹℃壒浜虹敤鎴稩D锛堢郴缁熺敤鎴凤級',
+  sort int NOT NULL DEFAULT 0 COMMENT '鏄剧ず椤哄簭',
+  creator varchar(64) NULL DEFAULT '' COMMENT '鍒涘缓鑰�',
+  create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  updater varchar(64) NULL DEFAULT '' COMMENT '鏇存柊鑰�',
+  update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+  deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+  PRIMARY KEY (id) USING BTREE,
+  KEY idx_config_id (config_id) USING BTREE,
+  KEY idx_user_id (user_id) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '閫氱敤瀹℃壒閰嶇疆-瀹℃壒浜鸿〃';
+
+-- 4.2 涓氬姟绫诲瀷绉嶅瓙锛氱紪鐮佷笌鍚勬ā鍧� *_APPROVE_* 甯搁噺涓�涓�瀵瑰簲锛岄粯璁ゅ叏閮ㄦ湭鍚敤
+INSERT IGNORE INTO system_approval_config
+  (biz_type, biz_name, approval_enabled, remark, creator, create_time, updater, update_time, deleted) VALUES
+('purchase_plan_approve',        '閲囪喘璁″垝瀹℃牳',   b'0', 'ERP 閲囪喘璁″垝', '1', NOW(), '1', NOW(), b'0'),
+('purchase_request_approve',     '閲囪喘鐢宠瀹℃牳',   b'0', 'ERP 閲囪喘鐢宠', '1', NOW(), '1', NOW(), b'0'),
+('erp_purchase_order_approve',   '閲囪喘璁㈠崟瀹℃牳',   b'0', 'ERP 閲囪喘璁㈠崟锛堝師璇敤 purchase_request_approve锛屽凡鏀逛负鐙珛缂栫爜锛�', '1', NOW(), '1', NOW(), b'0'),
+('erp_purchase_invoice_approve', '閲囪喘鏉ョエ瀹℃牳',   b'0', 'ERP 閲囪喘鏉ョエ', '1', NOW(), '1', NOW(), b'0'),
+('erp_finance_payment_approve',  '浠樻鍗曞鏍�',     b'0', 'ERP 浠樻鍗�', '1', NOW(), '1', NOW(), b'0'),
+('contract_approve',             '鍚堝悓瀹℃壒',       b'0', 'CRM 鍚堝悓', '1', NOW(), '1', NOW(), b'0'),
+('refund_approve',               '閫�璐瑰鎵�',       b'0', 'CRM 閫�娆�', '1', NOW(), '1', NOW(), b'0'),
+('cancellation_approve',         '閿�鎴峰鎵�',       b'0', 'CRM 閿�鎴�', '1', NOW(), '1', NOW(), b'0'),
+('crm_invoice_approve',          '寮�绁ㄥ鎵�',       b'0', 'CRM 鍙戠エ', '1', NOW(), '1', NOW(), b'0'),
+('sale_quotation_approve',       '閿�鍞姤浠峰鎵�',   b'0', 'CRM 閿�鍞姤浠�', '1', NOW(), '1', NOW(), b'0'),
+('hrm_leave_approve',            '璇峰亣瀹℃壒',       b'0', 'HRM 璇峰亣鐢宠', '1', NOW(), '1', NOW(), b'0'),
+('hrm_regularize_approve',       '杞瀹℃壒',       b'0', 'HRM 杞鐢宠', '1', NOW(), '1', NOW(), b'0'),
+('hrm_resignation_approve',      '绂昏亴瀹℃壒',       b'0', 'HRM 绂昏亴鐢宠', '1', NOW(), '1', NOW(), b'0'),
+('hrm_transfer_approve',         '璋冨姩瀹℃壒',       b'0', 'HRM 璋冨姩鐢宠', '1', NOW(), '1', NOW(), b'0'),
+('hrm_attendance_approve',       '鑰冨嫟寮傚父瀹℃壒',   b'0', 'HRM 鑰冨嫟寮傚父', '1', NOW(), '1', NOW(), b'0'),
+('performance_appraise_approve', '缁╂晥鑰冩牳瀹℃壒',   b'0', 'HRM 缁╂晥鑰冩牳', '1', NOW(), '1', NOW(), b'0'),
+('mes_pd_project_approve',       '浜у搧椤圭洰瀹℃壒',   b'0', 'MES 浜у搧椤圭洰', '1', NOW(), '1', NOW(), b'0'),
+('pd_tech_confirm_approve',      '鎶�鏈‘璁ゅ嚱瀹℃壒', b'0', 'MES 浜у搧鎶�鏈‘璁ゅ嚱', '1', NOW(), '1', NOW(), b'0'),
+('item_receipt_approve',         '閲囪喘鍏ュ簱瀹℃壒',   b'0', 'MES 閲囪喘鍏ュ簱鍗�', '1', NOW(), '1', NOW(), b'0'),
+('equipment_receipt_approve',    '璁惧鍏ュ簱瀹℃壒',   b'0', 'MES 璁惧鍏ュ簱鍗�', '1', NOW(), '1', NOW(), b'0'),
+('misc_issue_approve',           '鍏朵粬鍑哄簱瀹℃壒',   b'0', 'MES 鍏朵粬鍑哄簱鍗�', '1', NOW(), '1', NOW(), b'0'),
+('misc_receipt_approve',         '鍏朵粬鍏ュ簱瀹℃壒',   b'0', 'MES 鍏朵粬鍏ュ簱鍗�', '1', NOW(), '1', NOW(), b'0'),
+('return_vendor_approve',        '閫�璐у鎵�',       b'0', 'MES 閫�璐у崟', '1', NOW(), '1', NOW(), b'0'),
+('stock_taking_result_approve',  '鐩樼偣缁撴灉瀹℃壒',   b'0', 'MES 鐩樼偣鍗�', '1', NOW(), '1', NOW(), b'0'),
+('wms_approve',                  '浠撳簱鍗曟嵁瀹℃牳',   b'0', 'MES 浠撳偍閫氱敤瀹℃壒', '1', NOW(), '1', NOW(), b'0'),
+('compensation_approve',         '璧斾粯瀹℃壒',       b'0', '鍞悗璧斿伩鍗�', '1', NOW(), '1', NOW(), b'0');
+
+-- 4.3 瀹℃壒閰嶇疆椤甸潰鑿滃崟
+--     鐖剁骇閿氬畾銆岀郴缁熺鐞嗐�嶏細鍙� system/user/index 鐨� parent_id锛岄伩鍏嶇‖缂栫爜鐩綍 ID
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '瀹℃壒閰嶇疆', '', 2, 8, a.parent_id, 'approval-config', 'ep:stamp', 'system/approvalConfig/index', 'SystemApprovalConfig', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT MIN(m.parent_id) AS parent_id FROM system_menu m WHERE m.deleted = 0 AND m.component = 'system/user/index') a
+WHERE a.parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND component = 'system/approvalConfig/index') x);
+
+-- 4.3.1 瀹℃壒閰嶇疆鏌ヨ鎸夐挳锛堢埗绾� = 涓婇潰鐨勯〉闈㈣彍鍗曪紝鎸� component 鍙嶆煡锛�
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '瀹℃壒閰嶇疆鏌ヨ', 'system:approval-config:query', 3, 1, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT MIN(m.id) AS parent_id FROM system_menu m WHERE m.deleted = 0 AND m.component = 'system/approvalConfig/index') a
+WHERE a.parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'system:approval-config:query') x);
+
+-- 4.3.2 瀹℃壒閰嶇疆淇濆瓨鎸夐挳
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT '瀹℃壒閰嶇疆淇濆瓨', 'system:approval-config:update', 3, 2, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT MIN(m.id) AS parent_id FROM system_menu m WHERE m.deleted = 0 AND m.component = 'system/approvalConfig/index') a
+WHERE a.parent_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'system:approval-config:update') x);
+
+-- 4.4 閲囪喘璁″垝 / 閲囪喘璁㈠崟 瀹℃牳浜哄洓瀛楁
+--     锛堢涓�閮ㄥ垎鍙鐩栦簡鍙﹀ 20 寮犺〃锛岃繖涓ゅ紶鏄噰璐鍒�/閲囪喘璁㈠崟鏀归�犳椂琛ョ殑锛�
+DROP TABLE IF EXISTS _tmp_xgdl_po_review_cols;
+CREATE TABLE _tmp_xgdl_po_review_cols (
+    tbl    VARCHAR(64)  NOT NULL,
+    col    VARCHAR(64)  NOT NULL,
+    coldef VARCHAR(255) NOT NULL
+);
+
+INSERT INTO _tmp_xgdl_po_review_cols (tbl, col, coldef) VALUES
+('erp_purchase_plan',  'reviewer_id',   'bigint COMMENT ''瀹℃牳浜虹紪鍙凤紙瀹為檯鎵ц瀹℃牳鐨勭敤鎴凤級'''),
+('erp_purchase_plan',  'reviewer_name', 'varchar(64) COMMENT ''瀹℃牳浜哄鍚嶏紙鍐椾綑璁板綍锛岄伩鍏嶅垪琛ㄦ煡璇㈠啀鏌ョ敤鎴凤級'''),
+('erp_purchase_plan',  'review_time',   'datetime COMMENT ''瀹℃牳鏃堕棿'''),
+('erp_purchase_plan',  'review_remark', 'varchar(500) COMMENT ''瀹℃牳鎰忚锛堥�氳繃鏃剁殑鎰忚 / 涓嶉�氳繃鏃剁殑鍘熷洜锛�'''),
+('erp_purchase_order', 'reviewer_id',   'bigint COMMENT ''瀹℃牳浜虹紪鍙凤紙瀹為檯鎵ц瀹℃牳鐨勭敤鎴凤級'''),
+('erp_purchase_order', 'reviewer_name', 'varchar(64) COMMENT ''瀹℃牳浜哄鍚嶏紙鍐椾綑璁板綍锛岄伩鍏嶅垪琛ㄦ煡璇㈠啀鏌ョ敤鎴凤級'''),
+('erp_purchase_order', 'review_time',   'datetime COMMENT ''瀹℃牳鏃堕棿'''),
+('erp_purchase_order', 'review_remark', 'varchar(500) COMMENT ''瀹℃牳鎰忚锛堥�氳繃鏃剁殑鎰忚 / 涓嶉�氳繃鏃剁殑鍘熷洜锛�''');
+
+DROP PROCEDURE IF EXISTS xgdl_add_po_review_cols;
+DELIMITER $$
+CREATE PROCEDURE xgdl_add_po_review_cols()
+BEGIN
+    DECLARE v_done INT DEFAULT 0;
+    DECLARE v_tbl VARCHAR(64);
+    DECLARE v_col VARCHAR(64);
+    DECLARE v_def VARCHAR(255);
+    DECLARE cur CURSOR FOR SELECT tbl, col, coldef FROM _tmp_xgdl_po_review_cols;
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
+
+    OPEN cur;
+    read_loop: LOOP
+        FETCH cur INTO v_tbl, v_col, v_def;
+        IF v_done = 1 THEN
+            LEAVE read_loop;
+        END IF;
+        IF EXISTS (SELECT 1 FROM information_schema.tables
+                   WHERE table_schema = DATABASE() AND table_name = v_tbl)
+           AND NOT EXISTS (SELECT 1 FROM information_schema.columns
+                   WHERE table_schema = DATABASE() AND table_name = v_tbl AND column_name = v_col)
+        THEN
+            SET @ddl = CONCAT('ALTER TABLE `', v_tbl, '` ADD COLUMN `', v_col, '` ', v_def);
+            PREPARE stmt FROM @ddl;
+            EXECUTE stmt;
+            DEALLOCATE PREPARE stmt;
+        END IF;
+    END LOOP;
+    CLOSE cur;
+END$$
+DELIMITER ;
+
+CALL xgdl_add_po_review_cols();
+DROP PROCEDURE IF EXISTS xgdl_add_po_review_cols;
+DROP TABLE IF EXISTS _tmp_xgdl_po_review_cols;
+
+-- 4.5 閲囪喘璁″垝 / 閲囪喘璁㈠崟 瀹℃牳鎸夐挳鏉冮檺锛堥敋鐐瑰悓绗竴閮ㄥ垎锛氬悇鑷祫婧愮殑 :update 鎸夐挳锛�
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT '閲囪喘璁″垝瀹℃牳' AS name, 'erp:purchase-plan:audit' AS perm) t
+JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'erp:purchase-plan:update') a ON 1 = 1
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:purchase-plan:audit') m);
+
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT '閲囪喘璁㈠崟瀹℃牳' AS name, 'erp:purchase-order:audit' AS perm) t
+JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'erp:purchase-order:update') a ON 1 = 1
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'erp:purchase-order:audit') m);
+
+-- 4.6 琛ユ紡锛氫唬鐮侀噷宸蹭娇鐢ㄣ�佷絾绗簩閮ㄥ垎鏈垱寤虹殑 3 涓鏍告寜閽潈闄�
+--     瀵硅处鏂瑰紡锛歡rep -rhoE "'[a-z]+:[a-z0-9-]+:audit'" --include=*.java
+--     缂哄け浼氬鑷村搴旈〉闈㈢殑瀹℃牳鎸夐挳瀵逛换浣曚汉閮戒笉鏄剧ず锛堝惈 admin锛夈��
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT '鏁呴殰璧斾粯瀹℃牳' AS name, 'aftersales:compensation:audit' AS perm) t
+JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'aftersales:compensation:update') a ON 1 = 1
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'aftersales:compensation:audit') m);
+
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT '缁╂晥鑰冩牳瀹℃牳' AS name, 'hrm:performance-appraise:audit' AS perm) t
+JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'hrm:performance-appraise:update') a ON 1 = 1
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'hrm:performance-appraise:audit') m);
+
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
+SELECT t.name, t.perm, 3, 99, a.parent_id, '', '', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM (SELECT '浜у搧椤圭洰瀹℃牳' AS name, 'mes:pd-product:audit' AS perm) t
+JOIN (SELECT MIN(x.parent_id) AS parent_id FROM system_menu x WHERE x.deleted = 0 AND x.permission = 'mes:pd-product:update') a ON 1 = 1
+WHERE a.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM system_menu WHERE deleted = 0 AND permission = 'mes:pd-product:audit') m);
+
+-- 4.7 瑙掕壊鎺堟潈
+--     閲嶈锛氭湰妗嗘灦 SecurityFrameworkServiceImpl 娌℃湁銆岃秴绾х鐞嗗憳缁曡繃鏉冮檺鏍¢獙銆嶇殑鐭矾閫昏緫锛�
+--     鏉冮檺涓�寰嬭蛋 system_role_menu銆備笉鎺堟潈鐨勮瘽杩� admin 閮界湅涓嶅埌瀹℃壒閰嶇疆鑿滃崟鍜屽鏍告寜閽��
+--     寮�鍙戝簱瀹炴祴锛氬叏閮� :audit 鍧囧凡鎺堜簣 super_admin(1) 涓� common(2)锛�
+--     浠� mes:pd-product:audit 涓� mes:pd-tech-confirm:audit 婕忔巿 super_admin 鈥斺��
+--     涓嬮潰杩欐潯璇彞浼氶『甯︽妸杩欎袱涓己鍙hˉ榻愩��
+--     鍏朵綑瑙掕壊璇峰湪銆岃鑹茬鐞嗐�嶉〉闈㈡寜闇�鍕鹃�夈��
+INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
+SELECT r.id, m.id, '1', NOW(), '1', NOW(), b'0'
+FROM system_role r
+CROSS JOIN system_menu m
+WHERE r.code IN ('super_admin', 'common')
+  AND r.deleted = 0
+  AND m.deleted = 0
+  AND ( m.component = 'system/approvalConfig/index'
+        OR m.permission IN ('system:approval-config:query', 'system:approval-config:update')
+        OR m.permission LIKE '%:audit' )
+  AND NOT EXISTS (
+      SELECT 1 FROM system_role_menu rm
+      WHERE rm.role_id = r.id AND rm.menu_id = m.id AND rm.deleted = 0
+  );
+
 -- 瀹屾垚銆傚彲鎵ц浠ヤ笅璇彞鏍¢獙锛�
 -- SELECT table_name, column_name FROM information_schema.columns
 --  WHERE table_schema = DATABASE() AND column_name = 'reviewer_id';
 -- SELECT permission FROM system_menu WHERE deleted = 0 AND permission LIKE '%:audit';
+-- SELECT COUNT(*) FROM system_approval_config WHERE deleted = 0;                     -- 鏈熸湜 26
+-- SELECT id, name, permission, parent_id FROM system_menu                            -- 蹇呴』鍥炶涓夐」锛屽嬁鍙湅琛屾暟
+--  WHERE deleted = 0 AND (component = 'system/approvalConfig/index'
+--    OR permission LIKE 'system:approval-config:%'
+--    OR permission IN ('erp:purchase-plan:audit','erp:purchase-order:audit'));
+-- SELECT r.code, COUNT(*) FROM system_role_menu rm JOIN system_role r ON r.id = rm.role_id
+--  JOIN system_menu m ON m.id = rm.menu_id
+--  WHERE rm.deleted = 0 AND (m.component = 'system/approvalConfig/index'
+--    OR m.permission LIKE 'system:approval-config:%'
+--    OR m.permission IN ('erp:purchase-plan:audit','erp:purchase-order:audit'))
+--  GROUP BY r.code;                                                                  -- 鏈熸湜姣忚鑹� 5 鏉�

--
Gitblit v1.9.3