feat(sql): 迁移脚本补齐审批配置地基 + 采购计划/订单变更,并加订单审批测试

原 xgdl_review_audit_pending_changes.sql 只加了「审核人字段」和「审核按钮权限」,
缺少审批引擎本身:新环境只跑它,所有单据提交都会在
validateApprovalEnabledAndGetApprovers 处失败。

新增第四部分:
- system_approval_config / system_approval_config_user 建表
- 26 个业务类型种子(默认未启用)
- 审批配置页面菜单 + 查询/保存按钮;父级用 component/permission 锚点反查,
不硬编码菜单 ID
- erp_purchase_plan / erp_purchase_order 审核人四字段(原第一部分未覆盖这两张表)
- 两单的 audit 按钮权限
- 角色授权:本框架无超管绕过逻辑,:audit 与审批配置权限一律显式授予
super_admin 与 common

权限对账(grep 代码中的 ':audit' 与脚本比对)另发现 3 个代码已使用但脚本未建的菜单,
一并补入:aftersales:compensation、hrm:performance-appraise、mes:pd-product。
现代码 22 项 / 脚本 22 项,无缺漏无冗余。
开发库还实测出 mes:pd-product:audit、mes:pd-tech-confirm:audit 漏授 super_admin,
授权段的 NOT EXISTS 会顺带补齐。

脚本经 dry-run 验证:各插入语句算出的 parent_id 与开发库既有记录完全一致
(1 / 1065900 / 1065900 / 1065212 / 2666),幂等守卫在开发库上均为跳过、零新增。

测试:新增 scripts/test-purchase-order-api.js(接口级,24/24 通过)
与 scripts/test-purchase-order-ui.js(Playwright 端到端,自建种子数据)

注:本次提交同时带上了工作区里已有的既有加固(各菜单插入语句补
WHERE a.parent_id IS NOT NULL,防止锚点权限缺失时插入 parent_id=NULL 的孤立菜单),
该改动非本次所写,一并入库。

Co-Authored-By: Claude <noreply@anthropic.com>
已添加2个文件
已修改1个文件
888 ■■■■■ 文件已修改
scripts/test-purchase-order-api.js 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/test-purchase-order-ui.js 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/xgdl_review_audit_pending_changes.sql 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/test-purchase-order-api.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
/**
 * é‡‡è´­è®¢å•状态机审批 - æŽ¥å£çº§è‡ªåŠ¨åŒ–æµ‹è¯•
 *
 * è‡ªå»ºæµ‹è¯•单据(每次新建一张),不依赖历史数据状态,可重复执行。
 * è¦†ç›–:提交/审核/驳回/重新提交/通过的完整链路 + å…¨éƒ¨æœåŠ¡ç«¯æ ¡éªŒåˆ†æ”¯ã€‚
 *
 * ç”¨æ³•:node 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(ErpSupplierServiceImpl.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('正确拒绝:非审批中不可审核', `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('正确拒绝:审批中不可修改', `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('正确拒绝:当前用户不是审批人', `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('正确拒绝:缺驳回原因', `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('正确拒绝:已审核不可修改', `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} ä¸ªæŽ¥å£è¿”回 500`);
  console.log(`\n测试单据:erp_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);
});
scripts/test-purchase-order-ui.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,333 @@
/**
 * é‡‡è´­è®¢å•审批 - æµè§ˆå™¨ç«¯åˆ°ç«¯æµ‹è¯•
 *
 * å‰ç½®æ•°æ®ç”¨ admin-api ç›´æŽ¥å‡†å¤‡ï¼ˆä¸€æ¬¡è·‘完留下:1 å¼ æœªæäº¤ + 1 å¼ å®¡æ‰¹ä¸­ï¼‰ï¼Œ
 * ä¹‹åŽå…¨éƒ¨èµ°çœŸå®žæµè§ˆå™¨äº¤äº’验证按钮可见性、弹窗校验与状态流转。
 *
 * ç”¨æ³•:node 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] å‡†å¤‡æµ‹è¯•数据(接口)');
  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('驳回单入口不符', 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);
})();
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 ä¸Žå­˜å‚¨è¿‡ç¨‹çš„客户端整体执行
--   ï¼ˆmysql CLI:mysql -h<host> -P<port> -u<user> -p mom-xgdl < æœ¬æ–‡ä»¶ï¼‰ã€‚
--   å¤šæ•°å›¾å½¢åŒ–/在线 SQL æŽ§åˆ¶å°æŒ‰åˆ†å·æ‹†åˆ†è¯­å¥ï¼Œä¼šæŠŠ DELIMITER $$ æ®µè½æ‹†åã€‚
--
-- å¯¹åº”代码提交:
--   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 ä¹Ÿçœ‹ä¸åˆ°æŒ‰é’®ï¼‰
--
-- å¯¹åº”代码提交:e347fd6e 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 '审批人用户ID(系统用户)',
  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 ä¸ªå®¡æ ¸æŒ‰é’®æƒé™
--     å¯¹è´¦æ–¹å¼ï¼šgrep -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 â€”—
--     ä¸‹é¢è¿™æ¡è¯­å¥ä¼šé¡ºå¸¦æŠŠè¿™ä¸¤ä¸ªç¼ºå£è¡¥é½ã€‚
--     å…¶ä½™è§’色请在「角色管理」页面按需勾选。
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 æ¡