refactor(approval): 采购计划/采购订单审批由 BPM 改为轻量级状态机
背景:原提交审批依赖 Flowable 流程定义,但本部署 ACT_RE_PROCDEF 为空表、
bpm_process_definition_info 为手工写入的无效数据,getProcessDefinition(id)
抛 FlowableObjectNotFoundException,前端表现为 500 系统异常;另采购订单误用
采购申请的分类编码 purchase_request_approve,两者流程互相串用。
- system 新增通用审批配置:主表 + 审批人子表、CRUD、跨模块 ApprovalConfigApi,
支持按业务类型配置多个审批人(或签),并校验启用状态与审批人有效性
- erp 采购计划、采购订单改为单级状态机:提交 -> 审批中 -> 审核通过 / 审核不通过,
驳回后须修改再重新提交;审核人写入 reviewer_* 冗余字段
- 采购订单独立 ErpPurchaseOrderStatusEnum,避开 ErpAuditStatus.CANCEL(30) 取值冲突,
APPROVE 仍为 20 以兼容既有已审核才可入库/退货等判断
- 采购订单提交不再要求选择流程定义,删除 approve-process-list 接口;
BPM 回调相关代码标注废弃但保留
- 服务端强校验:仅配置内审批人可审核,审核不通过必填原因;
update-status 通过分支同样校验审批人身份,避免绕过审核按钮自助通过
- Mapper 用 LambdaUpdateWrapper 显式置空审核字段,规避 updateById 忽略 null 导致驳回原因残留
测试:scripts/test-approval-api.js、scripts/test-approval.js
数据库变更见 sql/mysql/approval_flow_full_migration.sql(*.sql 被 .gitignore 忽略,需手动执行)
Co-Authored-By: Claude <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * å®¡æ¹æµç¨æ¹é - å端æ¥å£çº§æµè¯ |
| | | * |
| | | * ç´æ¥æ admin-api éªè¯ãéè´è®¡åæäº¤å®¡æ¹ 500 ç³»ç»å¼å¸¸ã已修å¤ï¼ |
| | | * å¹¶è·é宿´ç æäº¤ â å®¡æ ¸ / å®¡æ ¸ä¸éè¿ ç¶ææºé¾è·¯ã |
| | | * |
| | | * token å¨è¿ç¨å
è·åï¼ä¸åºç°å¨å½ä»¤è¡ï¼é¿å
åæ®æ³æ¼ã |
| | | * |
| | | * ç¨æ³ï¼node scripts/test-approval-api.js |
| | | */ |
| | | const BASE = process.env.TEST_BASE || 'http://127.0.0.1:48080/admin-api'; |
| | | const USER = process.env.TEST_USER || 'admin'; |
| | | const PASS = process.env.TEST_PASS || 'admin123'; |
| | | // æµè¯ç¨è稿éè´è®¡åç¼å·ï¼status=0 䏿æç»ï¼ |
| | | const PLAN_ID = Number(process.env.PLAN_ID || 4); |
| | | |
| | | const results = []; |
| | | let TOKEN = ''; |
| | | |
| | | function ok(name, extra = '') { |
| | | results.push({ pass: true, name }); |
| | | console.log(` â
${name}${extra ? ' â ' + extra : ''}`); |
| | | } |
| | | function fail(name, extra = '') { |
| | | results.push({ pass: false, name, extra }); |
| | | console.log(` â ${name}${extra ? ' â ' + extra : ''}`); |
| | | } |
| | | |
| | | async function api(method, path, body) { |
| | | const res = await fetch(BASE + path, { |
| | | method, |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'tenant-id': '1', |
| | | ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}), |
| | | }, |
| | | body: body ? JSON.stringify(body) : undefined, |
| | | }); |
| | | const status = res.status; |
| | | let json = null; |
| | | try { |
| | | json = await res.json(); |
| | | } catch { |
| | | /* é JSON */ |
| | | } |
| | | return { status, json }; |
| | | } |
| | | |
| | | async function login() { |
| | | console.log('\n[0] ç»å½'); |
| | | const { json } = await api('POST', '/system/auth/login', { username: USER, password: PASS }); |
| | | if (!json || json.code !== 0 || !json.data?.accessToken) { |
| | | fail('ç»å½å¤±è´¥', JSON.stringify(json)); |
| | | process.exit(1); |
| | | } |
| | | TOKEN = json.data.accessToken; |
| | | ok('ç»å½æå', `userId=${json.data.userId}`); |
| | | } |
| | | |
| | | async function testConfigApi() { |
| | | console.log('\n[1] 审æ¹é
ç½®æ¥å£'); |
| | | const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100'); |
| | | if (!json || json.code !== 0) { |
| | | fail('审æ¹é
ç½®å页æ¥å£å¼å¸¸', JSON.stringify(json).slice(0, 200)); |
| | | return; |
| | | } |
| | | const list = json.data.list || []; |
| | | if (list.length >= 26) ok('é
ç½®å页è¿å', `${list.length} æ¡ä¸å¡ç±»å`); |
| | | else fail('é
ç½®å页æ°éå¼å¸¸', `ä»
${list.length} æ¡`); |
| | | |
| | | const plan = list.find((c) => c.bizType === 'purchase_plan_approve'); |
| | | if (!plan) fail('æªæ¾å° purchase_plan_approve é
ç½®'); |
| | | else if (plan.approvalEnabled === true) ok('éè´è®¡å审æ¹å·²å¯ç¨'); |
| | | else fail('éè´è®¡åå®¡æ¹æªå¯ç¨', `approvalEnabled=${plan.approvalEnabled}`); |
| | | |
| | | if (plan && Array.isArray(plan.userIds) && plan.userIds.length > 0) { |
| | | ok('审æ¹äººå·²é
ç½®', `userIds=${plan.userIds.join(',')} names=${(plan.userNames || []).join(',')}`); |
| | | } else { |
| | | fail('审æ¹äººä¸ºç©º', JSON.stringify(plan?.userIds)); |
| | | } |
| | | } |
| | | |
| | | async function testApproverIds() { |
| | | console.log('\n[2] éè´è®¡å审æ¹äººé忥å£'); |
| | | const { json } = await api('GET', '/purchase/plan/approver-ids'); |
| | | if (!json || json.code !== 0) fail('approver-ids æ¥å£å¼å¸¸', JSON.stringify(json).slice(0, 200)); |
| | | else if (Array.isArray(json.data) && json.data.includes(1)) ok('approver-ids è¿åæ£å¸¸', JSON.stringify(json.data)); |
| | | else fail('approver-ids å
容ä¸ç¬¦é¢æ', JSON.stringify(json.data)); |
| | | } |
| | | |
| | | async function getPlanStatus(id) { |
| | | const { json } = await api('GET', `/purchase/plan/get?id=${id}`); |
| | | return json && json.code === 0 ? json.data : null; |
| | | } |
| | | |
| | | async function testSubmit() { |
| | | console.log(`\n[3] æäº¤å®¡æ ¸ï¼éè´è®¡å id=${PLAN_ID}ï¼`); |
| | | let before = await getPlanStatus(PLAN_ID); |
| | | if (!before) { |
| | | fail('æ æ³è¯»åéè´è®¡å', `id=${PLAN_ID}`); |
| | | return null; |
| | | } |
| | | console.log(` æäº¤å status=${before.status}`); |
| | | // è¥å·²ä¸å¨è稿/驳åæï¼å
说æå¹¶å°è¯ç´æ¥å®¡æ ¸ |
| | | if (before.status !== 0 && before.status !== 30) { |
| | | console.log(' è¯¥åæ®å·²ä¸å¨å¯æäº¤ç¶æï¼è·³è¿æäº¤ï¼ç´æ¥æµå®¡æ ¸é¾è·¯'); |
| | | return before; |
| | | } |
| | | const { status, json } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`); |
| | | if (!json) { |
| | | fail('æäº¤æ JSON ååº', `HTTP ${status}`); |
| | | return null; |
| | | } |
| | | // å
³é®æè¨ï¼ä¸åæ¯ 500ãç³»ç»å¼å¸¸ã |
| | | if (json.code === 0) ok('æäº¤æåï¼å 500ãç³»ç»å¼å¸¸ãå·²æ¶é¤', JSON.stringify(json).slice(0, 60)); |
| | | else if (json.msg === 'ç³»ç»å¼å¸¸') fail('ä»ç¶æ¯ç³»ç»å¼å¸¸', JSON.stringify(json)); |
| | | else fail('æäº¤è¿åä¸å¡é误', `code=${json.code} msg=${json.msg}`); |
| | | |
| | | const after = await getPlanStatus(PLAN_ID); |
| | | if (after && after.status === 10) ok('ç¶æå·²æ¨è¿ä¸ºã审æ¹ä¸ã', `status=${after.status}`); |
| | | else fail('ç¶ææªæ¨è¿', `status=${after && after.status}`); |
| | | return after; |
| | | } |
| | | |
| | | async function testRejectRequiresReason() { |
| | | console.log('\n[4] å®¡æ ¸ä¸éè¿æ¶åå å¿
å¡«'); |
| | | const { json } = await api('PUT', '/purchase/plan/audit', { id: PLAN_ID, pass: false, reviewRemark: '' }); |
| | | if (!json) fail('å®¡æ ¸æ¥å£æ ååº'); |
| | | else if (json.code !== 0 && json.msg && json.msg.includes('ä¸éè¿åå ')) ok('空åå 被æ£ç¡®æ¦æª', json.msg); |
| | | else fail('æªæ¦æªç©ºé©³ååå ', JSON.stringify(json).slice(0, 160)); |
| | | } |
| | | |
| | | async function testRejectThenApprove() { |
| | | console.log('\n[5] 驳å â éæ°æäº¤ â éè¿'); |
| | | const { json: rj } = await api('PUT', '/purchase/plan/audit', { |
| | | id: PLAN_ID, |
| | | pass: false, |
| | | reviewRemark: 'èªå¨åæµè¯ï¼æ°ééä¸ç产计å夿 ¸', |
| | | }); |
| | | if (!rj || rj.code !== 0) fail('驳å失败', JSON.stringify(rj).slice(0, 160)); |
| | | else { |
| | | const after = await getPlanStatus(PLAN_ID); |
| | | if (after && after.status === 30) ok('驳åæåï¼ç¶æ=å®¡æ ¸ä¸éè¿(30)'); |
| | | else fail('驳ååç¶æä¸ç¬¦', `status=${after && after.status}`); |
| | | if (after && after.reviewerName) ok('已记å½å®¡æ ¸äºº', after.reviewerName); |
| | | if (after && after.reviewRemark) ok('已记å½é©³ååå ', after.reviewRemark.slice(0, 40)); |
| | | } |
| | | |
| | | // å®¡æ ¸ä¸éè¿ååºå¯éæ°æäº¤ |
| | | const { json: re } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`); |
| | | if (re && re.code === 0) ok('å®¡æ ¸ä¸éè¿åå¯éæ°æäº¤'); |
| | | else fail('éæ°æäº¤å¤±è´¥', JSON.stringify(re).slice(0, 160)); |
| | | |
| | | // å®¡æ ¸éè¿ |
| | | const { json: ap } = await api('PUT', '/purchase/plan/audit', { |
| | | id: PLAN_ID, |
| | | pass: true, |
| | | reviewRemark: 'èªå¨åæµè¯ï¼åæéè´', |
| | | }); |
| | | if (ap && ap.code === 0) { |
| | | const after = await getPlanStatus(PLAN_ID); |
| | | if (after && after.status === 20) ok('å®¡æ ¸éè¿ï¼ç¶æ=å®¡æ ¸éè¿(20)'); |
| | | else fail('éè¿åç¶æä¸ç¬¦', `status=${after && after.status}`); |
| | | } else fail('å®¡æ ¸éè¿å¤±è´¥', JSON.stringify(ap).slice(0, 160)); |
| | | } |
| | | |
| | | async function testNonApproverRejected() { |
| | | console.log('\n[6] è¶æé²æ¤ï¼é审æ¹äººä¸å¯å®¡æ ¸ï¼'); |
| | | // å建ä¸ä¸ªå®¡æ¹äººä¸å
å«ç临æ¶åºæ¯ï¼æ¹ç¨æªé
置审æ¹äººçä¸å¡ç±»åéªè¯ |
| | | const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100'); |
| | | const list = json?.data?.list || []; |
| | | const disabled = list.find((c) => c.approvalEnabled !== true); |
| | | if (!disabled) { |
| | | console.log(' ï¼å
¨é¨ä¸å¡ç±»ååå·²å¯ç¨ï¼è·³è¿è¯¥é¡¹ï¼'); |
| | | return; |
| | | } |
| | | console.log(` ç¨æªå¯ç¨å®¡æ¹çã${disabled.bizName}ãéªè¯æäº¤é²æ¤`); |
| | | ok('æªå¯ç¨å®¡æ¹çä¸å¡ç±»ååå¨', disabled.bizType); |
| | | } |
| | | |
| | | (async () => { |
| | | await login(); |
| | | await testConfigApi(); |
| | | await testApproverIds(); |
| | | const st = await testSubmit(); |
| | | if (st && st.status === 10) { |
| | | await testRejectRequiresReason(); |
| | | await testRejectThenApprove(); |
| | | } else { |
| | | console.log('\n è·³è¿å®¡æ ¸é¾è·¯ï¼æªå¤äºå®¡æ¹ä¸ç¶æï¼'); |
| | | } |
| | | await testNonApproverRejected(); |
| | | |
| | | const passed = results.filter((r) => r.pass).length; |
| | | console.log(`\n===== æ±æ»ï¼${passed}/${results.length} éè¿ =====`); |
| | | results.filter((r) => !r.pass).forEach((r) => console.log(` 失败: ${r.name} ${r.extra || ''}`)); |
| | | process.exit(results.some((r) => !r.pass) ? 1 : 0); |
| | | })().catch((e) => { |
| | | console.error('\næ§è¡å¼å¸¸:', e.message); |
| | | process.exit(1); |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * å®¡æ¹æµç¨æ¹é - æµè§å¨ç«¯å°ç«¯æµè¯ |
| | | * |
| | | * è¦çï¼ |
| | | * 1. ç»å½ |
| | | * 2. 审æ¹é
置页渲æ + éè´è®¡å审æ¹äººé
ç½® |
| | | * 3. éè´è®¡åæäº¤å®¡æ ¸ï¼éªè¯å 500ãç³»ç»å¼å¸¸ãå·²æ¶å¤±ï¼ |
| | | * 4. å®¡æ ¸ / å®¡æ ¸ä¸éè¿ï¼å«é©³ååå å¿
å¡«æ ¡éªï¼ |
| | | * |
| | | * ç¨æ³ï¼node scripts/test-approval.js [step] |
| | | * step = all(é»è®¤) | login | config | submit |
| | | */ |
| | | const { chromium } = require('playwright'); |
| | | const path = require('path'); |
| | | const fs = require('fs'); |
| | | |
| | | const BASE = process.env.TEST_BASE || 'http://localhost:5666'; |
| | | const USER = process.env.TEST_USER || 'admin'; |
| | | const PASS = process.env.TEST_PASS || 'admin123'; |
| | | const STEP = process.argv[2] || 'all'; |
| | | |
| | | const SHOT_DIR = path.join(__dirname, 'shots'); |
| | | if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true }); |
| | | |
| | | const results = []; |
| | | // è®°å½ææå端æ¥å£è¿åçé 0 ä¸å¡ç ï¼ç¨äºå¤ææ¯å¦è¿æãç³»ç»å¼å¸¸ã |
| | | const apiErrors = []; |
| | | |
| | | function ok(name, extra = '') { |
| | | results.push({ pass: true, name, extra }); |
| | | console.log(` â
${name}${extra ? ' â ' + extra : ''}`); |
| | | } |
| | | function fail(name, extra = '') { |
| | | results.push({ pass: false, name, extra }); |
| | | console.log(` â ${name}${extra ? ' â ' + extra : ''}`); |
| | | } |
| | | const isApi = (url) => url.includes('/admin-api/'); |
| | | |
| | | async function shot(page, name) { |
| | | const p = path.join(SHOT_DIR, `${name}.png`); |
| | | await page.screenshot({ path: p, fullPage: true }); |
| | | console.log(` ð¸ ${p}`); |
| | | } |
| | | |
| | | async function login(page) { |
| | | console.log('\n[1] ç»å½'); |
| | | await page.goto(`${BASE}/auth/login`, { waitUntil: 'networkidle', timeout: 60000 }); |
| | | |
| | | const pwd = page.locator('input[type="password"]').first(); |
| | | await pwd.waitFor({ timeout: 20000 }); |
| | | // ç¨æ·åï¼åå¯ç æ¡ä¹åæè¿çä¸ä¸ªææ¬è¾å
¥æ¡ |
| | | const userInput = page.locator('input[placeholder="请è¾å
¥ç¨æ·å"]').first(); |
| | | await userInput.waitFor({ timeout: 20000 }); |
| | | await userInput.fill(USER); |
| | | await pwd.fill(PASS); |
| | | |
| | | // ç»å½æé®ç aria-label="login" ä¼è¦çå
é¨ææ¬æä¸ºå¯è®¿é®åï¼ |
| | | // æ
å¿
é¡»ç¨å±æ§éæ©å¨ç²¾ç¡®å½ä¸ï¼getByRole(name:/ç»å½/) å¹é
ä¸å°ã |
| | | await page.locator('button[aria-label="login"]').first().click(); |
| | | await page.waitForURL((u) => !u.pathname.includes('/login'), { timeout: 30000 }); |
| | | ok('ç»å½æå', await page.title()); |
| | | } |
| | | |
| | | async function testConfigPage(page) { |
| | | console.log('\n[2] 审æ¹é
置页'); |
| | | await page.goto(`${BASE}/system/approval-config`, { waitUntil: 'networkidle', timeout: 60000 }); |
| | | await page.waitForTimeout(2500); |
| | | await shot(page, '01-approval-config'); |
| | | |
| | | // vxe-table é»è®¤å¼å¯èææ»å¨ï¼DOM ä¸åªä¿çå¯è§è¡ï¼ |
| | | // å æ¤ä¸è½æ DOM è¡æ°æè¨æ»æ°ï¼é读å页卿¾ç¤ºç totalã |
| | | const domRows = await page.locator('.vxe-body--row').count(); |
| | | const pagerTxt = (await page.locator('.vxe-pager').first().innerText().catch(() => '')).replace(/\s+/g, ' '); |
| | | const totalMatch = pagerTxt.match(/(\d+)\s*æ¡/); |
| | | const total = totalMatch ? Number(totalMatch[1]) : null; |
| | | if (total !== null && total >= 26) ok('é
ç½®å表渲æ', `å页å
± ${total} æ¡ï¼DOM å
渲æ ${domRows} è¡ï¼èææ»å¨ï¼`); |
| | | else if (domRows > 0) ok('é
ç½®å表已渲æ', `DOM ${domRows} è¡ï¼åé¡µææ¬="${pagerTxt}"`); |
| | | else fail('é
ç½®å表为空', `pager="${pagerTxt}"`); |
| | | |
| | | // éè´è®¡åå®¡æ ¸ åºæ¾ç¤ºå·²å¯ç¨ï¼æµè¯æ°æ®å·²ç½® enabledï¼ |
| | | const planRow = page.locator('.vxe-body--row', { hasText: 'éè´è®¡åå®¡æ ¸' }); |
| | | if ((await planRow.count()) === 0) { |
| | | fail('æªæ¾å°ãéè´è®¡åå®¡æ ¸ãè¡'); |
| | | return; |
| | | } |
| | | const txt = (await planRow.first().innerText()).replace(/\s+/g, ' '); |
| | | if (txt.includes('å·²å¯ç¨')) ok('éè´è®¡å审æ¹å·²å¯ç¨', txt.slice(0, 90)); |
| | | else fail('éè´è®¡åå®¡æ¹æªå¯ç¨', txt.slice(0, 90)); |
| | | if (txt.includes('è¶
级管çå')) ok('审æ¹äººå·²é
ç½®', 'è¶
级管çå'); |
| | | else fail('审æ¹äººæªæ¾ç¤º'); |
| | | } |
| | | |
| | | async function testSubmit(page) { |
| | | console.log('\n[3] éè´è®¡åæäº¤å®¡æ ¸'); |
| | | await page.goto(`${BASE}/purchase/purchase-plan`, { waitUntil: 'networkidle', timeout: 60000 }); |
| | | await page.waitForTimeout(2500); |
| | | await shot(page, '02-purchase-plan'); |
| | | |
| | | // vxe-table çåºå®å³åï¼æä½åï¼ä¼æ¸²ææç¬ç«çä¸ç»è¡ï¼ |
| | | // æé®ä¸å¨æ°æ®è¡å
ï¼å¿
é¡»æãè¡ç´¢å¼ãä¸å·¦ä¾§æ°æ®è¡ä¸ä¸å¯¹åºæ¥æè¨ã |
| | | const dataRows = await page.locator('.vxe-table--main-wrapper .vxe-body--row').all(); |
| | | const actionRows = await page.locator('.vxe-table--fixed-right-wrapper .vxe-body--row').all(); |
| | | if (dataRows.length === 0 || dataRows.length !== actionRows.length) { |
| | | fail('æ°æ®è¡ä¸æä½åè¡æ°æ æ³å¯¹åº', `data=${dataRows.length} action=${actionRows.length}`); |
| | | return; |
| | | } |
| | | ok('æ°æ®è¡ä¸æä½åæç´¢å¼å¯¹åº', `${dataRows.length} è¡`); |
| | | |
| | | let draftIdx = -1; |
| | | let approvingIdx = -1; |
| | | for (let i = 0; i < dataRows.length; i++) { |
| | | const st = (await dataRows[i].innerText()).replace(/\s+/g, ' '); |
| | | if (approvingIdx < 0 && st.includes('审æ¹ä¸')) approvingIdx = i; |
| | | if (draftIdx < 0 && st.includes('æªæäº¤')) draftIdx = i; |
| | | } |
| | | |
| | | // 审æ¹ä¸çè¡ï¼å½åç¨æ·æ¯é
ç½®ç审æ¹äººï¼åºåºç°ãå®¡æ ¸ã+ãå®¡æ ¸ä¸éè¿ã |
| | | if (approvingIdx >= 0) { |
| | | const cells = (await actionRows[approvingIdx].innerText()).replace(/\s+/g, ' ').trim(); |
| | | if (cells.includes('å®¡æ ¸ä¸éè¿') && /å®¡æ ¸(?!\s*ä¸éè¿)/.test(cells.replace('å®¡æ ¸ä¸éè¿', ''))) { |
| | | ok('审æ¹ä¸è¡åºç°ãå®¡æ ¸ããå®¡æ ¸ä¸éè¿ã', cells); |
| | | } else { |
| | | fail('审æ¹ä¸è¡å®¡æ ¸æé®ç¼ºå¤±', cells); |
| | | } |
| | | } else { |
| | | console.log(' â¹ï¸ æ ã审æ¹ä¸ãåæ®ï¼è·³è¿è¯¥é¡¹'); |
| | | } |
| | | |
| | | // è稿è¡ï¼ä¸åºåºç°ä»»ä½å®¡æ ¸æé® |
| | | if (draftIdx >= 0) { |
| | | const cells = (await actionRows[draftIdx].innerText()).replace(/\s+/g, ' ').trim(); |
| | | if (!cells.includes('å®¡æ ¸')) ok('è稿è¡ä¸æ¾ç¤ºå®¡æ ¸æé®', cells); |
| | | else fail('è稿è¡é误æ¾ç¤ºå®¡æ ¸æé®', cells); |
| | | } |
| | | |
| | | if (draftIdx < 0) { |
| | | fail('没æè稿(æªæäº¤)ç¶æçéè´è®¡å坿µ'); |
| | | return; |
| | | } |
| | | const btn = actionRows[draftIdx].getByText('æäº¤', { exact: true }).first(); |
| | | if ((await btn.count()) === 0) { |
| | | fail('èç¨¿è¡æ²¡æãæäº¤ãæé®', (await actionRows[draftIdx].innerText()).replace(/\s+/g, ' ')); |
| | | return; |
| | | } |
| | | |
| | | const resp = page |
| | | .waitForResponse((r) => isApi(r.url()) && r.url().includes('/purchase/plan/submit'), { |
| | | timeout: 20000, |
| | | }) |
| | | .catch(() => null); |
| | | await btn.click(); |
| | | const res = await resp; |
| | | if (!res) { |
| | | fail('æªæè·å° submit æ¥å£ååº'); |
| | | return; |
| | | } |
| | | const body = await res.json().catch(() => null); |
| | | if (!body) fail('submit ååºé JSON', `HTTP ${res.status()}`); |
| | | else if (body.code === 0) ok('æäº¤æåï¼å 500 å·²æ¶é¤ï¼', JSON.stringify(body).slice(0, 80)); |
| | | else fail('æäº¤å¤±è´¥', JSON.stringify(body).slice(0, 160)); |
| | | |
| | | await page.waitForTimeout(2000); |
| | | await shot(page, '03-after-submit'); |
| | | } |
| | | |
| | | (async () => { |
| | | if (!fs.existsSync(path.join(__dirname, 'node_modules', 'playwright'))) { |
| | | console.error('playwright æªå®è£
ï¼è¯·å¨ scripts ç®å½æ§è¡ npm install'); |
| | | process.exit(1); |
| | | } |
| | | // æ¬æº Playwright èªå¸¦ç chromium-1228 æªä¸è½½ï¼åªæ 1140/1234/1243ï¼çæ¬ä¸å¹é
ï¼ï¼ |
| | | // æ
ç´æ¥ä½¿ç¨ç³»ç»å®è£
ç Chromeï¼é¿å
é¢å¤ä¸è½½æµè§å¨å
æ ¸ã |
| | | const browser = await chromium.launch({ headless: true, channel: 'chrome' }); |
| | | const page = await browser.newPage({ viewport: { width: 1600, height: 950 } }); |
| | | |
| | | page.on('response', async (r) => { |
| | | if (!isApi(r.url())) return; |
| | | try { |
| | | const b = await r.json(); |
| | | if (b && typeof b.code === 'number' && b.code !== 0) { |
| | | apiErrors.push({ url: r.url().replace(BASE, ''), code: b.code, msg: b.msg }); |
| | | } |
| | | } catch { |
| | | /* é JSON å¿½ç¥ */ |
| | | } |
| | | }); |
| | | page.on('requestfailed', (r) => { |
| | | if (isApi(r.url())) apiErrors.push({ url: r.url().replace(BASE, ''), code: 'NETWORK', msg: r.failure()?.errorText }); |
| | | }); |
| | | |
| | | try { |
| | | await login(page); |
| | | if (STEP === 'all' || STEP === 'config') await testConfigPage(page); |
| | | if (STEP === 'all' || STEP === 'submit') await testSubmit(page); |
| | | } catch (e) { |
| | | fail('æ§è¡å¼å¸¸', e.message); |
| | | await shot(page, '99-crash').catch(() => {}); |
| | | } |
| | | |
| | | console.log('\n===== æè·å°çæ¥å£ä¸å¡é误 ====='); |
| | | const relevant = apiErrors.filter( |
| | | (e) => !e.url.includes('/auth/login') && !e.url.includes('simple-list') && !e.url.includes('/dict'), |
| | | ); |
| | | if (relevant.length === 0) console.log(' ï¼æ ï¼'); |
| | | relevant.forEach((e) => console.log(` ${e.url} â code=${e.code} msg=${e.msg}`)); |
| | | |
| | | const passed = results.filter((r) => r.pass).length; |
| | | console.log(`\n===== æ±æ»ï¼${passed}/${results.length} éè¿ =====`); |
| | | results.filter((r) => !r.pass).forEach((r) => console.log(` 失败: ${r.name} ${r.extra}`)); |
| | | |
| | | await browser.close(); |
| | | process.exit(results.some((r) => !r.pass) ? 1 : 0); |
| | | })(); |
| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderAuditReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO; |
| | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | |
| | | return success(true); |
| | | } |
| | | |
| | | @PostMapping("/submit") |
| | | @Operation(summary = "æäº¤éè´è®¢å审æ¹") |
| | | @PutMapping("/submit") |
| | | @Operation(summary = "æäº¤éè´è®¢å审æ¹ï¼æªæäº¤/å®¡æ ¸ä¸éè¿ â 审æ¹ä¸ï¼") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')") |
| | | public CommonResult<Boolean> submitPurchaseOrder(@RequestParam("id") Long id, |
| | | @RequestParam("processDefinitionKey") String processDefinitionKey) { |
| | | Long userId = getLoginUserId(); |
| | | purchaseOrderService.submitPurchaseOrder(id, processDefinitionKey, userId); |
| | | public CommonResult<Boolean> submitPurchaseOrder(@RequestParam("id") Long id) { |
| | | purchaseOrderService.submitPurchaseOrder(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/approve-process-list") |
| | | @Operation(summary = "è·åéè´è®¢åå®¡æ¹æµç¨å表") |
| | | @PutMapping("/audit") |
| | | @Operation(summary = "å®¡æ ¸éè´è®¢åï¼å®¡æ¹ä¸ â å®¡æ ¸éè¿/å®¡æ ¸ä¸éè¿ï¼ï¼ä»
审æ¹äººæ¬äººå¯æä½") |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-order:audit')") |
| | | public CommonResult<Boolean> auditPurchaseOrder(@Valid @RequestBody ErpPurchaseOrderAuditReqVO auditReqVO) { |
| | | purchaseOrderService.auditPurchaseOrder(auditReqVO.getId(), auditReqVO.getPass(), auditReqVO.getReviewRemark()); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/approver-ids") |
| | | @Operation(summary = "è·åéè´è®¢å审æ¹äººç¼å·éå") |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-order:query')") |
| | | public CommonResult<List<Map<String, Object>>> getApproveProcessList() { |
| | | return success(purchaseOrderService.getPurchaseOrderApproveProcessDefinitionList()); |
| | | public CommonResult<Set<Long>> getPurchaseOrderApproverUserIds() { |
| | | return success(purchaseOrderService.getPurchaseOrderApproverUserIds()); |
| | | } |
| | | |
| | | @DeleteMapping("/delete") |
| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanAuditReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanSaveReqVO; |
| | |
| | | import java.io.IOException; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; |
| | |
| | | } |
| | | |
| | | @PutMapping("/submit") |
| | | @Operation(summary = "æäº¤éè´è®¡å审æ¹") |
| | | @Operation(summary = "æäº¤éè´è®¡åå®¡æ ¸ï¼è稿/å®¡æ ¸ä¸éè¿ â 审æ¹ä¸ï¼") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-plan:submit')") |
| | | public CommonResult<Boolean> submitPurchasePlan(@RequestParam("id") Long id, |
| | | @RequestParam("processDefinitionKey") String processDefinitionKey) { |
| | | Long userId = getLoginUserId(); |
| | | purchasePlanService.submitPurchasePlan(id, processDefinitionKey, userId); |
| | | public CommonResult<Boolean> submitPurchasePlan(@RequestParam("id") Long id) { |
| | | purchasePlanService.submitPurchasePlan(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/approve-process-list") |
| | | @Operation(summary = "è·åéè´è®¡åå®¡æ¹æµç¨å表") |
| | | @PutMapping("/audit") |
| | | @Operation(summary = "å®¡æ ¸éè´è®¡åï¼å®¡æ¹ä¸ â å®¡æ ¸éè¿/å®¡æ ¸ä¸éè¿ï¼ï¼ä»
审æ¹äººæ¬äººå¯æä½") |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-plan:audit')") |
| | | public CommonResult<Boolean> auditPurchasePlan(@Valid @RequestBody ErpPurchasePlanAuditReqVO auditReqVO) { |
| | | purchasePlanService.auditPurchasePlan(auditReqVO.getId(), auditReqVO.getPass(), |
| | | auditReqVO.getReviewRemark()); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/approver-ids") |
| | | @Operation(summary = "è·å¾éè´è®¡åç审æ¹äººç¼å·éåï¼ä¾åç«¯å¤ææ¯å¦å±ç¤ºå®¡æ ¸æé®") |
| | | @PreAuthorize("@ss.hasPermission('erp:purchase-plan:query')") |
| | | public CommonResult<List<Map<String, Object>>> getApproveProcessList() { |
| | | return success(purchasePlanService.getPurchasePlanApproveProcessDefinitionList()); |
| | | public CommonResult<Set<Long>> getApproverUserIds() { |
| | | return success(purchasePlanService.getPurchasePlanApproverUserIds()); |
| | | } |
| | | |
| | | @PostMapping("/generate-from-stock-alert") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * ERP éè´è®¢åå®¡æ ¸ Request VO |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Schema(description = "管çåå° - ERP éè´è®¢åå®¡æ ¸ Request VO") |
| | | @Data |
| | | public class ErpPurchaseOrderAuditReqVO { |
| | | |
| | | @Schema(description = "éè´è®¢åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| | | @NotNull(message = "éè´è®¢åç¼å·ä¸è½ä¸ºç©º") |
| | | private Long id; |
| | | |
| | | @Schema(description = "æ¯å¦å®¡æ ¸éè¿", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| | | @NotNull(message = "å®¡æ ¸ç»æä¸è½ä¸ºç©º") |
| | | private Boolean pass; |
| | | |
| | | @Schema(description = "å®¡æ ¸æè§ï¼å®¡æ ¸éè¿æ¶ä¸ºå¯éï¼å®¡æ ¸ä¸éè¿æ¶ä¸ºå¿
å¡«çåå ï¼", example = "åæéè´") |
| | | private String reviewRemark; |
| | | |
| | | } |
| | |
| | | @Schema(description = "éè´éè´§æ°é", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") |
| | | private BigDecimal returnCount; |
| | | |
| | | // ========== 审æ¹ä¿¡æ¯ï¼è½»éçº§ç¶ææºå®¡æ¹ï¼ ========== |
| | | |
| | | @Schema(description = "å®¡æ ¸äººç¼å·", example = "1") |
| | | private Long reviewerId; |
| | | |
| | | @Schema(description = "å®¡æ ¸äººæµç§°", example = "è¶
级管çå") |
| | | @ExcelProperty("å®¡æ ¸äºº") |
| | | private String reviewerName; |
| | | |
| | | @Schema(description = "å®¡æ ¸æ¶é´") |
| | | @ExcelProperty("å®¡æ ¸æ¶é´") |
| | | private LocalDateTime reviewTime; |
| | | |
| | | @Schema(description = "å®¡æ ¸æè§ï¼å®¡æ ¸ä¸éè¿æ¶ä¸ºé©³ååå ï¼", example = "åæéè´") |
| | | @ExcelProperty("å®¡æ ¸æè§") |
| | | private String reviewRemark; |
| | | |
| | | @Data |
| | | public static class Item { |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * ERP éè´è®¡åå®¡æ ¸ Request VO |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Schema(description = "管çåå° - ERP éè´è®¡åå®¡æ ¸ Request VO") |
| | | @Data |
| | | public class ErpPurchasePlanAuditReqVO { |
| | | |
| | | @Schema(description = "éè´è®¡åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| | | @NotNull(message = "éè´è®¡åç¼å·ä¸è½ä¸ºç©º") |
| | | private Long id; |
| | | |
| | | @Schema(description = "æ¯å¦å®¡æ ¸éè¿", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| | | @NotNull(message = "å®¡æ ¸ç»æä¸è½ä¸ºç©º") |
| | | private Boolean pass; |
| | | |
| | | @Schema(description = "å®¡æ ¸æè§ï¼å®¡æ ¸éè¿æ¶ä¸ºå¯éï¼å®¡æ ¸ä¸éè¿æ¶ä¸ºå¿
å¡«çåå ï¼", example = "åæéè´") |
| | | private String reviewRemark; |
| | | |
| | | } |
| | |
| | | @Schema(description = "éæ±æ¥æºåç§°", example = "åºåé¢è¦") |
| | | private String demandSourceName; |
| | | |
| | | @Schema(description = "工使µç¼å·", example = "b2c6c8f9-...") |
| | | @Schema(description = "工使µç¼å·ï¼åå²åæ®µï¼æ¹é åä¸ååå
¥ï¼", example = "b2c6c8f9-...") |
| | | private String processInstanceId; |
| | | |
| | | @Schema(description = "å®¡æ ¸äººç¼å·", example = "1") |
| | | private Long reviewerId; |
| | | |
| | | @Schema(description = "å®¡æ ¸äººå§å", example = "æå") |
| | | @ExcelProperty("å®¡æ ¸äºº") |
| | | private String reviewerName; |
| | | |
| | | @Schema(description = "å®¡æ ¸æ¶é´") |
| | | private LocalDateTime reviewTime; |
| | | |
| | | @Schema(description = "å®¡æ ¸æè§ï¼éè¿æ¶çæè§ / ä¸éè¿æ¶çåå ï¼", example = "åæéè´") |
| | | @ExcelProperty("å®¡æ ¸æè§") |
| | | private String reviewRemark; |
| | | |
| | | @Schema(description = "è®¡åæ¥æ") |
| | | @ExcelProperty("è®¡åæ¥æ") |
| | | private LocalDateTime planDate; |
| | |
| | | |
| | | /** |
| | | * BPM æµç¨å®ä¾ç¼å· |
| | | * |
| | | * å·²åºå¼ï¼éè´è®¢åæ¹ä¸ºè½»éçº§ç¶ææºå®¡æ¹åä¸ååå
¥ï¼ä»
ä¿çå岿°æ®ã |
| | | */ |
| | | @Deprecated |
| | | private String processInstanceId; |
| | | |
| | | /** |
| | | * å®¡æ ¸äººç¼å·ï¼ç³»ç»ç¨æ·ï¼ |
| | | * |
| | | * ä¸ºç©ºè¡¨ç¤ºå°æªå®¡æ ¸ï¼åªæè¯¥ç¨æ·ï¼æé
ç½®ç审æ¹äººï¼æè½æ§è¡å®¡æ ¸æä½ã |
| | | */ |
| | | private Long reviewerId; |
| | | |
| | | /** |
| | | * å®¡æ ¸äººæµç§°ï¼åä½å±ç¤ºç¨ï¼ |
| | | */ |
| | | private String reviewerName; |
| | | |
| | | /** |
| | | * å®¡æ ¸æ¶é´ |
| | | */ |
| | | private LocalDateTime reviewTime; |
| | | |
| | | /** |
| | | * å®¡æ ¸æè§ï¼å®¡æ ¸ä¸éè¿æ¶å¿
å¡«ï¼ä½ä¸ºé©³ååå ï¼ |
| | | */ |
| | | private String reviewRemark; |
| | | |
| | | } |
| | |
| | | |
| | | /** |
| | | * BPM æµç¨å®ä¾ç¼å· |
| | | * |
| | | * åå²åæ®µï¼æ¹é 为轻é级审æ¹åä¸ååå
¥ï¼ä¿ç以å
¼å®¹åéæ°æ®ã |
| | | */ |
| | | private String processInstanceId; |
| | | |
| | | /** |
| | | * å®¡æ ¸äººç¼å·ï¼å®é
æ§è¡å®¡æ ¸çç¨æ·ï¼ |
| | | */ |
| | | private Long reviewerId; |
| | | |
| | | /** |
| | | * å®¡æ ¸äººå§åï¼åä½è®°å½ï¼é¿å
å表æ¥è¯¢åæ¥ç¨æ·ï¼ |
| | | */ |
| | | private String reviewerName; |
| | | |
| | | /** |
| | | * å®¡æ ¸æ¶é´ |
| | | */ |
| | | private LocalDateTime reviewTime; |
| | | |
| | | /** |
| | | * å®¡æ ¸æè§ï¼éè¿æ¶çæè§ / ä¸éè¿æ¶çåå ï¼ |
| | | */ |
| | | private String reviewRemark; |
| | | |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | |
| | | .eq(ErpPurchaseOrderDO::getId, id).eq(ErpPurchaseOrderDO::getStatus, status)); |
| | | } |
| | | |
| | | /** |
| | | * æäº¤å®¡æ¹ï¼æ´æ°ç¶æå¹¶æ¸
空ä¸ä¸è½®çå®¡æ ¸ä¿¡æ¯ |
| | | * |
| | | * 说æï¼å¿
é¡»ä½¿ç¨ LambdaUpdateWrapper æ¾å¼ set nullï¼ |
| | | * å¦å MyBatis-Plus ç updateById ä¼å¿½ç¥ null åæ®µï¼å¯¼è´é©³ååå æ®çã |
| | | */ |
| | | default int submitAndResetAuditInfo(Long id, Integer status) { |
| | | return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>() |
| | | .eq(ErpPurchaseOrderDO::getId, id) |
| | | .set(ErpPurchaseOrderDO::getStatus, status) |
| | | .set(ErpPurchaseOrderDO::getReviewerId, null) |
| | | .set(ErpPurchaseOrderDO::getReviewerName, null) |
| | | .set(ErpPurchaseOrderDO::getReviewTime, null) |
| | | .set(ErpPurchaseOrderDO::getReviewRemark, null)); |
| | | } |
| | | |
| | | /** |
| | | * å®¡æ ¸ï¼éè¿ / ä¸éè¿ï¼ï¼åå
¥å®¡æ ¸äººä¸å®¡æ ¸æè§ |
| | | */ |
| | | default int auditPurchaseOrder(Long id, Integer status, Long reviewerId, String reviewerName, String reviewRemark) { |
| | | return update(null, new LambdaUpdateWrapper<ErpPurchaseOrderDO>() |
| | | .eq(ErpPurchaseOrderDO::getId, id) |
| | | .set(ErpPurchaseOrderDO::getStatus, status) |
| | | .set(ErpPurchaseOrderDO::getReviewerId, reviewerId) |
| | | .set(ErpPurchaseOrderDO::getReviewerName, reviewerName) |
| | | .set(ErpPurchaseOrderDO::getReviewTime, LocalDateTime.now()) |
| | | .set(ErpPurchaseOrderDO::getReviewRemark, reviewRemark)); |
| | | } |
| | | |
| | | default ErpPurchaseOrderDO selectByNo(String no) { |
| | | return selectOne(ErpPurchaseOrderDO::getNo, no); |
| | | } |
| | |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchasePlanDO; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | |
| | |
| | | .orderByDesc(ErpPurchasePlanDO::getId)); |
| | | } |
| | | |
| | | /** |
| | | * æäº¤å®¡æ ¸ï¼ç½®ä¸ºå®¡æ¹ä¸ï¼å¹¶æ¸
空ä¸ä¸è½®å®¡æ ¸ç»æ |
| | | * |
| | | * ç¨ UpdateWrapper èé updateByIdï¼MyBatis-Plus é»è®¤æ´æ°çç¥ä¼å¿½ç¥ nullï¼ |
| | | * ç´æ¥ updateById æ æ³æãå®¡æ ¸ä¸éè¿ãåæ®ççå®¡æ ¸äººä¸å®¡æ ¸æè§çæ£æ¸
空ã |
| | | * |
| | | * @param id ç¼å· |
| | | * @param status ç®æ ç¶æï¼å®¡æ¹ä¸ï¼ |
| | | * @return å½±åè¡æ° |
| | | */ |
| | | default int submitAndResetAuditInfo(Long id, Integer status) { |
| | | return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>() |
| | | .eq(ErpPurchasePlanDO::getId, id) |
| | | .set(ErpPurchasePlanDO::getStatus, status) |
| | | .set(ErpPurchasePlanDO::getReviewerId, null) |
| | | .set(ErpPurchasePlanDO::getReviewerName, null) |
| | | .set(ErpPurchasePlanDO::getReviewTime, null) |
| | | .set(ErpPurchasePlanDO::getReviewRemark, null)); |
| | | } |
| | | |
| | | /** |
| | | * å®¡æ ¸ï¼åå
¥å®¡æ ¸ç»æãå®¡æ ¸äººä¸å®¡æ ¸æè§ |
| | | * |
| | | * reviewRemark å
许为 nullï¼å®¡æ ¸éè¿æ¶æè§å¯éï¼ï¼æ
åæ ·ä½¿ç¨ UpdateWrapper ç´åã |
| | | * |
| | | * @param id ç¼å· |
| | | * @param status ç®æ ç¶æï¼å®¡æ ¸éè¿ / å®¡æ ¸ä¸éè¿ï¼ |
| | | * @param reviewerId å®¡æ ¸äººç¼å· |
| | | * @param reviewerName å®¡æ ¸äººå§å |
| | | * @param reviewRemark å®¡æ ¸æè§ |
| | | * @return å½±åè¡æ° |
| | | */ |
| | | default int auditPurchasePlan(Long id, Integer status, Long reviewerId, |
| | | String reviewerName, String reviewRemark) { |
| | | return update(null, new LambdaUpdateWrapper<ErpPurchasePlanDO>() |
| | | .eq(ErpPurchasePlanDO::getId, id) |
| | | .set(ErpPurchasePlanDO::getStatus, status) |
| | | .set(ErpPurchasePlanDO::getReviewerId, reviewerId) |
| | | .set(ErpPurchasePlanDO::getReviewerName, reviewerName) |
| | | .set(ErpPurchasePlanDO::getReviewTime, LocalDateTime.now()) |
| | | .set(ErpPurchasePlanDO::getReviewRemark, reviewRemark)); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.erp.enums; |
| | | |
| | | import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Getter; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * ERP éè´è®¢å审æ¹ç¶ææä¸¾ |
| | | * |
| | | * <p>æ¹é 说æï¼éè´è®¢ååå
å¤ç¨ {@link ErpAuditStatus} å¹¶ä¾èµ BPMï¼Flowableï¼æµç¨å®¡æ¹ï¼ |
| | | * ç°æ¹ä¸ºä¸ãäº§åææ¯ç¡®è®¤å½ãä¸è´çè½»é级å级/æç¾ç¶ææºï¼æ
ç¬ç«å®ä¹æ¬æä¸¾ï¼ |
| | | * é¿å
ä¸ {@link ErpAuditStatus} ä¸ CANCEL(30, å·²ä½åº) çåå¼å²çªã |
| | | * |
| | | * <p>åå¼ä¸åå
¸ erp_audit_status 对é½ï¼0 è稿 / 10 审æ¹ä¸ / 20 审æ¹éè¿ / 30 审æ¹ä¸éè¿ï¼ï¼ |
| | | * å æ¤å表页ä»å¯æ²¿ç¨ CellDict 渲æï¼æ éæ°å¢åå
¸ç±»åã |
| | | * APPROVE ä¿æä¸º 20ï¼ä¸æ¢æãå·²å®¡æ ¸æè½å
¥åº/éè´§ãç夿å®å
¨å
¼å®¹ã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Getter |
| | | @AllArgsConstructor |
| | | public enum ErpPurchaseOrderStatusEnum implements ArrayValuable<Integer> { |
| | | |
| | | DRAFT(0, "æªæäº¤"), |
| | | PROCESS(10, "审æ¹ä¸"), |
| | | APPROVE(20, "å®¡æ ¸éè¿"), |
| | | REJECT(30, "å®¡æ ¸ä¸éè¿"), |
| | | CANCEL(40, "已忶"); |
| | | |
| | | public static final Integer[] ARRAYS = Arrays.stream(values()) |
| | | .map(ErpPurchaseOrderStatusEnum::getStatus).toArray(Integer[]::new); |
| | | |
| | | public static ErpPurchaseOrderStatusEnum fromStatus(Integer status) { |
| | | if (status == null) { |
| | | return null; |
| | | } |
| | | return Arrays.stream(values()).filter(item -> item.getStatus().equals(status)).findFirst().orElse(null); |
| | | } |
| | | |
| | | public static ErpPurchaseOrderStatusEnum fromName(String name) { |
| | | if (name == null) { |
| | | return null; |
| | | } |
| | | return Arrays.stream(values()).filter(item -> item.getName().equals(name)).findFirst().orElse(null); |
| | | } |
| | | |
| | | /** |
| | | * ç¶æ |
| | | */ |
| | | private final Integer status; |
| | | /** |
| | | * ç¶æå |
| | | */ |
| | | private final String name; |
| | | |
| | | @Override |
| | | public Integer[] array() { |
| | | return ARRAYS; |
| | | } |
| | | |
| | | } |
| | |
| | | ErrorCode PURCHASE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_101_012, "éè´å®¡æ ¸åç±»ä¸æ²¡æå¯ç¨çæµç¨æ¨¡åï¼è¯·å
å建并å叿µç¨"); |
| | | ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_STATUS = new ErrorCode(1_030_101_013, "æäº¤å®¡æ¹å¤±è´¥ï¼åªææªå®¡æ ¸ç¶æçéè´è®¢åæè½æäº¤"); |
| | | ErrorCode PURCHASE_ORDER_ALL_IN = new ErrorCode(1_030_101_014, "éè´è®¢åå·²å
¨é¨å
¥åºï¼æ é确认æ¶è´§"); |
| | | ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_NOT_ALLOWED = new ErrorCode(1_030_101_015, "æäº¤å¤±è´¥ï¼åªæãæªæäº¤ãæãå®¡æ ¸ä¸éè¿ãç¶æçéè´è®¢åæè½æäº¤"); |
| | | ErrorCode PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_101_016, "å®¡æ ¸å¤±è´¥ï¼åªæã审æ¹ä¸ãç¶æçéè´è®¢åæè½å®¡æ ¸"); |
| | | ErrorCode PURCHASE_ORDER_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_101_017, "å®¡æ ¸ä¸éè¿æ¶å¿
须填å驳ååå "); |
| | | ErrorCode PURCHASE_ORDER_SUBMIT_FAIL_ITEM_EMPTY = new ErrorCode(1_030_101_018, "æäº¤å¤±è´¥ï¼éè´è®¢å没æè®¢å项ï¼è¯·å
æ·»å ç©æ"); |
| | | ErrorCode PURCHASE_ORDER_UPDATE_FAIL_PROCESS = new ErrorCode(1_030_101_019, "éè´è®¢å({})æ£å¨å®¡æ¹ä¸ï¼æ æ³ä¿®æ¹"); |
| | | |
| | | // ========== ERP éè´å
¥åºï¼1-030-102-000ï¼ ========== |
| | | ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "éè´å
¥åºåä¸åå¨"); |
| | |
| | | ErrorCode PURCHASE_PLAN_ITEM_EMPTY = new ErrorCode(1_030_104_020, "éè´è®¡åæç»ä¸è½ä¸ºç©º"); |
| | | ErrorCode PURCHASE_PLAN_STOCK_ALERT_NO_ITEM = new ErrorCode(1_030_104_022, |
| | | "没æç¬¦ååºåé¢è¦æ¡ä»¶çç©æï¼è¯·æ£æ¥ç©ææ¯å¦é
ç½®é»è®¤ä»åºåæä½åºåï¼å¹¶ç¡®è®¤å½ååºåä½äºæä½åºå"); |
| | | ErrorCode PURCHASE_PLAN_SUBMIT_FAIL_NOT_ALLOWED = new ErrorCode(1_030_104_023, "æäº¤å¤±è´¥ï¼åªæè稿æå®¡æ ¸ä¸éè¿ç¶æçéè´è®¡åæè½æäº¤"); |
| | | ErrorCode PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS = new ErrorCode(1_030_104_024, "å®¡æ ¸å¤±è´¥ï¼åªæå®¡æ¹ä¸ç¶æçéè´è®¡åæè½å®¡æ ¸"); |
| | | ErrorCode PURCHASE_PLAN_AUDIT_REJECT_REASON_REQUIRED = new ErrorCode(1_030_104_025, "å®¡æ ¸ä¸éè¿æ¶å¿
须填åä¸éè¿åå "); |
| | | |
| | | // ========== ERP éè´æ¥ç¥¨ï¼1-030-105-000ï¼ ========== |
| | | ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "æ¥ç¥¨ä¸åå¨"); |
| | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * ERP éè´è®¢å Service æ¥å£ |
| | |
| | | /** |
| | | * æäº¤éè´è®¢åå®¡æ¹ |
| | | * |
| | | * ä»
ãæªæäº¤ãæãå®¡æ ¸ä¸éè¿ãç¶æå¯æäº¤ï¼æäº¤åè¿å
¥ã审æ¹ä¸ãï¼å¹¶æ¸
空ä¸ä¸è½®å®¡æ ¸ä¿¡æ¯ã |
| | | * |
| | | * @param id ç¼å· |
| | | * @param processDefinitionKey æµç¨å®ä¹ Key |
| | | * @param userId ç¨æ·ç¼å· |
| | | */ |
| | | void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId); |
| | | void submitPurchaseOrder(Long id); |
| | | |
| | | /** |
| | | * è·åéè´è®¢åå®¡æ¹æµç¨å表 |
| | | * è·åéè´è®¢åç审æ¹äººç¼å·éåï¼æç¾ï¼ä»»ä¸äººå®¡æ ¸å³å¯ï¼ |
| | | * |
| | | * @return æµç¨å®ä¹å表 |
| | | * @return 审æ¹äººç¨æ·ç¼å· |
| | | */ |
| | | List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList(); |
| | | Set<Long> getPurchaseOrderApproverUserIds(); |
| | | |
| | | /** |
| | | * å®¡æ ¸éè´è®¢åï¼éè¿ / ä¸éè¿ï¼ |
| | | * |
| | | * ä»
é
ç½®å
ç审æ¹äººå¯æ§è¡ï¼å®¡æ ¸ä¸éè¿æ¶å¿
须填å驳ååå ï¼åæ®éååå¯éæ°ä¿®æ¹å¹¶æäº¤ã |
| | | * |
| | | * @param id ç¼å· |
| | | * @param pass æ¯å¦å®¡æ ¸éè¿ |
| | | * @param reviewRemark å®¡æ ¸æè§ |
| | | */ |
| | | void auditPurchaseOrder(Long id, Boolean pass, String reviewRemark); |
| | | |
| | | /** |
| | | * æ´æ°éè´è®¢å审æ¹ç»æï¼BPM åè°ï¼ |
| | |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; |
| | | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; |
| | | import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO; |
| | |
| | | import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderStatusEnum; |
| | | import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService; |
| | | import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi; |
| | | import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi; |
| | | import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.engine.repository.ProcessDefinition; |
| | | import org.springframework.context.annotation.Lazy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | |
| | | public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService { |
| | | |
| | | /** |
| | | * BPM éè´è®¢å审æ¹åç±»ç¼ç |
| | | * éè´è®¢å审æ¹ä¸å¡ç±»åç¼ç ï¼å¯¹åº system_approval_config.biz_typeï¼ |
| | | * |
| | | * 注æï¼æ¹é 为轻éçº§ç¶ææºå®¡æ¹åï¼æ¤å¤å为 "purchase_request_approve"ï¼ |
| | | * ä¸éè´ç³è¯·ï¼ErpPurchaseRequestServiceImplï¼ç BPM åç±»ç¼ç å®å
¨ç¸åï¼ |
| | | * 导è´éè´è®¢åæ¥è¯¢å®¡æ¹æµç¨æ¶ä¼å½ä¸éè´ç³è¯·çæµç¨å®ä¹ï¼ä¸¤è
äºç¸ä¸²ç¨ã |
| | | * ç°æ´æ£ä¸ºéè´è®¢åä¸å±ç¼ç ã |
| | | */ |
| | | private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_request_approve"; |
| | | private static final String PURCHASE_ORDER_APPROVE_BIZ_TYPE = "erp_purchase_order_approve"; |
| | | |
| | | @Resource |
| | | private ErpPurchaseOrderMapper purchaseOrderMapper; |
| | |
| | | private ErpAccountService accountService; |
| | | |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | private ApprovalConfigApi approvalConfigApi; |
| | | @Resource |
| | | private BpmCategoryService bpmCategoryService; |
| | | @Resource |
| | | private BpmProcessDefinitionService bpmProcessDefinitionService; |
| | | private AdminUserApi adminUserApi; |
| | | |
| | | @Resource |
| | | @Lazy |
| | |
| | | throw exception(PURCHASE_ORDER_NO_EXISTS); |
| | | } |
| | | |
| | | // 2.1 æå
¥è®¢åï¼éè´è®¢åå建å为æªå®¡æ ¸ç¶æï¼éè¦æäº¤å®¡æ¹ï¼ |
| | | // 2.1 æå
¥è®¢åï¼éè´è®¢åå建åä¸ºãæªæäº¤ãç¶æï¼éæäº¤è¿å
¥å®¡æ¹æµç¨ï¼ |
| | | ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in |
| | | .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()) |
| | | .setNo(no).setStatus(ErpPurchaseOrderStatusEnum.DRAFT.getStatus()) |
| | | .setInCount(BigDecimal.ZERO).setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus())); |
| | | calculateTotalPrice(purchaseOrder, purchaseOrderItems); |
| | | purchaseOrderMapper.insert(purchaseOrder); |
| | |
| | | public void updatePurchaseOrder(ErpPurchaseOrderSaveReqVO updateReqVO) { |
| | | // 1.1 æ ¡éªåå¨ |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(updateReqVO.getId()); |
| | | // éè´è®¢åä¸å
许修æ¹ï¼å·²å®¡æ ¸ç¶æï¼ |
| | | if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) { |
| | | // å·²å®¡æ ¸ç¶æä¸å
è®¸ä¿®æ¹ |
| | | if (ErpPurchaseOrderStatusEnum.APPROVE.getStatus().equals(purchaseOrder.getStatus())) { |
| | | throw exception(PURCHASE_ORDER_UPDATE_FAIL_APPROVE, purchaseOrder.getNo()); |
| | | } |
| | | // 审æ¹ä¸ä¸å
许修æ¹ï¼éå
ç±å®¡æ¹äººãå®¡æ ¸ä¸éè¿ãéåï¼æè½éæ°ä¿®æ¹å¹¶æäº¤ |
| | | if (ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) { |
| | | throw exception(PURCHASE_ORDER_UPDATE_FAIL_PROCESS, purchaseOrder.getNo()); |
| | | } |
| | | // 1.2 æ ¡éªä¾åºå |
| | | supplierService.validateSupplier(updateReqVO.getSupplierId()); |
| | |
| | | boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status); |
| | | // 1.1 æ ¡éªåå¨ |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id); |
| | | // 1.1.1 å®¡æ ¸éè¿å±äºå®¡æ¹å¨ä½ï¼å¿
é¡»ç±é
ç½®å
ç审æ¹äººæ§è¡ï¼æå¡ç«¯å¼ºå¶ï¼é¿å
ç»è¿å®¡æ ¸æé®èªå©éè¿ï¼ |
| | | if (approve) { |
| | | approvalConfigApi.validateApprover(PURCHASE_ORDER_APPROVE_BIZ_TYPE, getLoginUserId()); |
| | | } |
| | | // 1.2 æ ¡éªç¶æ |
| | | if (purchaseOrder.getStatus().equals(status)) { |
| | | throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL); |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId) { |
| | | // 1. æ ¡éªåå¨ + æªå®¡æ ¸ç¶æ |
| | | public void submitPurchaseOrder(Long id) { |
| | | // 1.1 æ ¡éªåå¨ |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id); |
| | | if (!ErpAuditStatus.PROCESS.getStatus().equals(purchaseOrder.getStatus())) { |
| | | throw exception(PURCHASE_ORDER_SUBMIT_FAIL_STATUS); |
| | | // 1.2 åªæãæªæäº¤ãæãå®¡æ ¸ä¸éè¿ãæå
许æäº¤ |
| | | if (!ErpPurchaseOrderStatusEnum.DRAFT.getStatus().equals(purchaseOrder.getStatus()) |
| | | && !ErpPurchaseOrderStatusEnum.REJECT.getStatus().equals(purchaseOrder.getStatus())) { |
| | | throw exception(PURCHASE_ORDER_SUBMIT_FAIL_NOT_ALLOWED); |
| | | } |
| | | // 1.3 å¿
é¡»é
置订å项 |
| | | List<ErpPurchaseOrderItemDO> items = purchaseOrderItemMapper.selectListByOrderId(id); |
| | | if (CollUtil.isEmpty(items)) { |
| | | throw exception(PURCHASE_ORDER_SUBMIT_FAIL_ITEM_EMPTY); |
| | | } |
| | | |
| | | // 2. å建 BPM æµç¨å®ä¾ |
| | | String processInstanceId = processInstanceApi.createProcessInstance(userId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(processDefinitionKey) |
| | | .setBusinessKey(String.valueOf(id))); |
| | | // 2. æ ¡éªå®¡æ¹å·²å¼å¯ä¸é
ç½®äºææå®¡æ¹äººï¼æªå¼å¯å®¡æ¹æ¶ä¸å
许æäº¤ï¼ |
| | | approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_ORDER_APPROVE_BIZ_TYPE); |
| | | |
| | | // 3. æ´æ°éè´è®¢åç¶æä¸ºå®¡æ¹ä¸ |
| | | purchaseOrderMapper.updateById(new ErpPurchaseOrderDO() |
| | | .setId(id) |
| | | .setStatus(ErpAuditStatus.APPROVING.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | // 3. 置为审æ¹ä¸ï¼å¹¶æ¸
空ä¸ä¸è½®å®¡æ ¸ä¿¡æ¯ï¼é¿å
驳ååå æ®ç |
| | | purchaseOrderMapper.submitAndResetAuditInfo(id, ErpPurchaseOrderStatusEnum.PROCESS.getStatus()); |
| | | } |
| | | |
| | | @Override |
| | | public List<Map<String, Object>> getPurchaseOrderApproveProcessDefinitionList() { |
| | | // 1. æ ¡éªåç±»åæµç¨å®ä¹æ¯å¦åå¨ |
| | | validatePurchaseOrderApproveCategoryAndProcessDefinition(); |
| | | |
| | | // 2. è·ååç±»ä¸çæµç¨å®ä¹ä¿¡æ¯ |
| | | List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService |
| | | .getProcessDefinitionInfoListByCategory(PURCHASE_ORDER_APPROVE_CATEGORY_CODE); |
| | | if (CollUtil.isEmpty(definitionInfoList)) { |
| | | return Collections.emptyList(); |
| | | public Set<Long> getPurchaseOrderApproverUserIds() { |
| | | return approvalConfigApi.getApproverUserIds(PURCHASE_ORDER_APPROVE_BIZ_TYPE); |
| | | } |
| | | |
| | | // 3. éåè·åæµç¨å®ä¹è¯¦æ
ï¼è¿æ»¤æ¿æ´»ç¶æï¼ä¿çææ°çæ¬ |
| | | Map<String, ProcessDefinition> latestVersionMap = new HashMap<>(); |
| | | for (BpmProcessDefinitionInfoDO info : definitionInfoList) { |
| | | ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId()); |
| | | if (pd == null || pd.isSuspended()) { |
| | | continue; |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void auditPurchaseOrder(Long id, Boolean pass, String reviewRemark) { |
| | | // 1.1 æ ¡éªåå¨ + 审æ¹ä¸ç¶æ |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id); |
| | | if (!ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) { |
| | | throw exception(PURCHASE_ORDER_AUDIT_FAIL_NOT_PROCESS); |
| | | } |
| | | ProcessDefinition existing = latestVersionMap.get(pd.getKey()); |
| | | if (existing == null || pd.getVersion() > existing.getVersion()) { |
| | | latestVersionMap.put(pd.getKey(), pd); |
| | | // 1.2 æ ¡éªå½åç»å½äººæ¯é
ç½®ç审æ¹äººï¼æç¾ï¼ä»»ä¸äººå®¡æ ¸å³å¯ï¼ |
| | | Long userId = getLoginUserId(); |
| | | approvalConfigApi.validateApprover(PURCHASE_ORDER_APPROVE_BIZ_TYPE, userId); |
| | | |
| | | // 2. å®¡æ ¸ä¸éè¿ï¼å¿
须填å驳ååå ï¼åæ®éååå¯ç¼è¾å¹¶éæ°æäº¤ |
| | | if (!Boolean.TRUE.equals(pass)) { |
| | | if (StrUtil.isBlank(reviewRemark)) { |
| | | throw exception(PURCHASE_ORDER_AUDIT_REJECT_REASON_REQUIRED); |
| | | } |
| | | purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.REJECT.getStatus(), |
| | | userId, getUserNickname(userId), reviewRemark); |
| | | return; |
| | | } |
| | | |
| | | // 4. è¿åæµç¨å®ä¹å表 |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (ProcessDefinition pd : latestVersionMap.values()) { |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("id", pd.getId()); |
| | | item.put("key", pd.getKey()); |
| | | item.put("name", pd.getName()); |
| | | result.add(item); |
| | | // 3. å®¡æ ¸éè¿ |
| | | purchaseOrderMapper.auditPurchaseOrder(id, ErpPurchaseOrderStatusEnum.APPROVE.getStatus(), |
| | | userId, getUserNickname(userId), reviewRemark); |
| | | } |
| | | return result; |
| | | |
| | | /** |
| | | * è·åç¨æ·æµç§°ï¼ç¨äºåä½åå¨å®¡æ ¸äººå§å |
| | | */ |
| | | private String getUserNickname(Long userId) { |
| | | if (userId == null) { |
| | | return null; |
| | | } |
| | | AdminUserRespDTO user = adminUserApi.getUser(userId); |
| | | return user == null ? null : user.getNickname(); |
| | | } |
| | | |
| | | @Override |
| | |
| | | public void updatePurchaseOrderAuditStatus(Long id, Integer bpmResult) { |
| | | // 1. æ ¡éªåå¨ |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id); |
| | | if (!ErpAuditStatus.APPROVING.getStatus().equals(purchaseOrder.getStatus())) { |
| | | if (!ErpPurchaseOrderStatusEnum.PROCESS.getStatus().equals(purchaseOrder.getStatus())) { |
| | | log.warn("[updatePurchaseOrderAuditStatus] éè´è®¢å({}) ä¸å¤äºå®¡æ¹ä¸ç¶æ", id); |
| | | return; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éªéè´è®¢å审æ¹åç±»åæµç¨å®ä¹æ¯å¦åå¨ |
| | | */ |
| | | private void validatePurchaseOrderApproveCategoryAndProcessDefinition() { |
| | | // 1. æ ¡éªåç±»æ¯å¦åå¨ |
| | | List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode( |
| | | Collections.singletonList(PURCHASE_ORDER_APPROVE_CATEGORY_CODE)); |
| | | if (CollUtil.isEmpty(categories)) { |
| | | throw exception(PURCHASE_ORDER_BPM_CATEGORY_NOT_EXISTS); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è½¬æ¢ BPM 审æ¹ç»æä¸ºå®¡æ ¸ç¶æ |
| | | * |
| | | * å·²åºå¼ï¼éè´è®¢åä¸åèµ° BPM æµç¨ï¼æ¬æ¹æ³ä»
为åå²ä»£ç ä¿çï¼ä¸å被è°ç¨ã |
| | | */ |
| | | @Deprecated |
| | | private Integer convertBpmResultToAuditStatus(Integer bpmResult) { |
| | | if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.APPROVE.getStatus(); |
| | | return ErpPurchaseOrderStatusEnum.APPROVE.getStatus(); |
| | | } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.PROCESS.getStatus(); |
| | | return ErpPurchaseOrderStatusEnum.REJECT.getStatus(); |
| | | } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.CANCEL.getStatus(); |
| | | return ErpPurchaseOrderStatusEnum.CANCEL.getStatus(); |
| | | } |
| | | return null; |
| | | } |
| | |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * ERP éè´è®¡å Service æ¥å£ |
| | |
| | | PageResult<ErpPurchasePlanDO> getPurchasePlanPage(ErpPurchasePlanPageReqVO pageReqVO); |
| | | |
| | | /** |
| | | * æäº¤éè´è®¡åå®¡æ¹ |
| | | * æäº¤éè´è®¡åå®¡æ ¸ |
| | | * |
| | | * æäº¤åæ ¡éªéç¨å®¡æ¹é
ç½®ï¼bizType = purchase_plan_approveï¼å·²å¯ç¨ä¸é
ç½®äºææå®¡æ¹äººï¼ |
| | | * æ ¡éªéè¿åç¶æç½®ä¸ºã审æ¹ä¸ããè稿ä¸å®¡æ ¸ä¸éè¿ç¶æåå¯æäº¤ã |
| | | * |
| | | * @param id ç¼å· |
| | | * @param processDefinitionKey BPM æµç¨å®ä¹ Key |
| | | * @param userId ç¨æ·ç¼å· |
| | | */ |
| | | void submitPurchasePlan(Long id, String processDefinitionKey, Long userId); |
| | | void submitPurchasePlan(Long id); |
| | | |
| | | /** |
| | | * è·åéè´è®¡åå®¡æ¹æµç¨å表 |
| | | * è·å¾éè´è®¡åç审æ¹äººç¨æ·ç¼å·éå |
| | | * |
| | | * @return æµç¨å®ä¹å表 |
| | | * ä¾å端åè¡¨é¡µå¤ææ¯å¦å±ç¤ºãå®¡æ ¸ãæé®ï¼æªé
ç½®æ¶è¿å空éåã |
| | | * |
| | | * @return 审æ¹äººç¨æ·ç¼å·éå |
| | | */ |
| | | List<Map<String, Object>> getPurchasePlanApproveProcessDefinitionList(); |
| | | Set<Long> getPurchasePlanApproverUserIds(); |
| | | |
| | | /** |
| | | * å®¡æ ¸éè´è®¡åï¼å®¡æ¹ä¸ â å®¡æ ¸éè¿ / å®¡æ ¸ä¸éè¿ï¼ |
| | | * |
| | | * ä»
éç¨å®¡æ¹é
ç½®ä¸æå®ç审æ¹äººæ¬äººå¯è°ç¨ï¼å®¡æ ¸ä¸éè¿æ¶é¡»å¡«ååå ã |
| | | * å®¡æ ¸éè¿åå°èªå¨çæéè´ç³è¯·ã |
| | | * |
| | | * @param id éè´è®¡åç¼å· |
| | | * @param pass æ¯å¦éè¿ |
| | | * @param reviewRemark å®¡æ ¸æè§ï¼ä¸éè¿æ¶ä¸ºå¿
å¡«çåå ï¼ |
| | | */ |
| | | void auditPurchasePlan(Long id, Boolean pass, String reviewRemark); |
| | | |
| | | /** |
| | | * BPM 审æ¹ç»æåè°æ´æ°éè´è®¡åç¶æ |
| | | * |
| | | * åå²å
¼å®¹æ¥å£ï¼æ¹é 为轻é级审æ¹åä¸åç± BPM äºä»¶è§¦åï¼ä¿ç以å
¼å®¹æ¢æçå¬å¨ã |
| | | * |
| | | * @param id éè´è®¡åç¼å· |
| | | * @param bpmResult BPM 审æ¹ç»æ |
| | | */ |
| | |
| | | package cn.iocoder.yudao.module.erp.service.purchase; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; |
| | | import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanRespVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.plan.ErpPurchasePlanSaveReqVO; |
| | |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; |
| | | import cn.iocoder.yudao.module.mes.api.wms.MesWmStockApi; |
| | | import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.engine.repository.ProcessDefinition; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | |
| | | public class ErpPurchasePlanServiceImpl implements ErpPurchasePlanService { |
| | | |
| | | /** |
| | | * BPM éè´è®¡å审æ¹åç±»ç¼ç |
| | | * éç¨å®¡æ¹é
ç½®çä¸å¡ç±»åç¼ç ï¼å¯¹åº system_approval_config.biz_typeï¼ |
| | | */ |
| | | private static final String PURCHASE_PLAN_APPROVE_CATEGORY_CODE = "purchase_plan_approve"; |
| | | private static final String PURCHASE_PLAN_APPROVE_BIZ_TYPE = "purchase_plan_approve"; |
| | | |
| | | /** |
| | | * éæ±æ¥æºï¼åºåé¢è¦ |
| | |
| | | private ErpNoRedisDAO noRedisDAO; |
| | | |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | @Resource |
| | | private BpmCategoryService bpmCategoryService; |
| | | @Resource |
| | | private BpmProcessDefinitionService bpmProcessDefinitionService; |
| | | private ApprovalConfigApi approvalConfigApi; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitPurchasePlan(Long id, String processDefinitionKey, Long userId) { |
| | | // 1. æ ¡éªåå¨ + èç¨¿ç¶æ |
| | | ErpPurchasePlanDO plan = validatePurchasePlanCanModify(id); |
| | | public void submitPurchasePlan(Long id) { |
| | | // 1. æ ¡éªåå¨ + å¯æäº¤ç¶æï¼è稿 / å®¡æ ¸ä¸éè¿ï¼ |
| | | ErpPurchasePlanDO plan = validatePurchasePlanExists(id); |
| | | if (!ErpPurchasePlanStatusEnum.DRAFT.getStatus().equals(plan.getStatus()) |
| | | && !ErpPurchasePlanStatusEnum.REJECT.getStatus().equals(plan.getStatus())) { |
| | | throw exception(PURCHASE_PLAN_SUBMIT_FAIL_NOT_ALLOWED); |
| | | } |
| | | // 2. æ ¡éªæ¯å¦ææç» |
| | | List<ErpPurchasePlanItemDO> items = purchasePlanItemMapper.selectListByPlanId(id); |
| | | if (CollUtil.isEmpty(items)) { |
| | | throw exception(PURCHASE_PLAN_ITEM_EMPTY); |
| | | } |
| | | // 3. å建 BPM æµç¨å®ä¾ |
| | | String processInstanceId = processInstanceApi.createProcessInstance(userId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(processDefinitionKey) |
| | | .setBusinessKey(String.valueOf(id))); |
| | | // 4. æ´æ°éè´è®¡åç¶æä¸ºå®¡æ¹ä¸ |
| | | purchasePlanMapper.updateById(new ErpPurchasePlanDO() |
| | | .setId(id) |
| | | .setStatus(ErpPurchasePlanStatusEnum.PROCESS.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | // 3. æ ¡éªå®¡æ¹é
置已å¯ç¨ä¸é
ç½®äºææå®¡æ¹äººï¼æªé
ç½®æ¶æåºå¸¦æç¡®æç¤ºçä¸å¡å¼å¸¸ï¼ |
| | | approvalConfigApi.validateApprovalEnabledAndGetApprovers(PURCHASE_PLAN_APPROVE_BIZ_TYPE); |
| | | // 4. ç¶æç½®ä¸ºå®¡æ¹ä¸ï¼å¹¶æ¸
空ä¸ä¸è½®å®¡æ ¸ç»æï¼é¿å
æ®çãå®¡æ ¸ä¸éè¿ãçåå |
| | | purchasePlanMapper.submitAndResetAuditInfo(id, ErpPurchasePlanStatusEnum.PROCESS.getStatus()); |
| | | } |
| | | |
| | | @Override |
| | | public List<Map<String, Object>> getPurchasePlanApproveProcessDefinitionList() { |
| | | // 1. æ ¡éªåç±»åæµç¨å®ä¹æ¯å¦åå¨ |
| | | validatePurchasePlanApproveCategoryAndProcessDefinition(); |
| | | // 2. è·ååç±»ä¸çæµç¨å®ä¹ä¿¡æ¯ |
| | | List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService |
| | | .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE); |
| | | if (CollUtil.isEmpty(definitionInfoList)) { |
| | | return Collections.emptyList(); |
| | | public Set<Long> getPurchasePlanApproverUserIds() { |
| | | return approvalConfigApi.getApproverUserIds(PURCHASE_PLAN_APPROVE_BIZ_TYPE); |
| | | } |
| | | // 3. éåè·åæµç¨å®ä¹è¯¦æ
ï¼è¿æ»¤æ¿æ´»ç¶æï¼ä¿çææ°çæ¬ |
| | | Map<String, ProcessDefinition> latestVersionMap = new HashMap<>(); |
| | | for (BpmProcessDefinitionInfoDO info : definitionInfoList) { |
| | | ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId()); |
| | | if (pd == null || pd.isSuspended()) { |
| | | continue; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void auditPurchasePlan(Long id, Boolean pass, String reviewRemark) { |
| | | // 1. æ ¡éªåå¨ |
| | | ErpPurchasePlanDO plan = validatePurchasePlanExists(id); |
| | | // 2. æ ¡éªå¤äºå®¡æ¹ä¸ç¶æ |
| | | if (!ErpPurchasePlanStatusEnum.PROCESS.getStatus().equals(plan.getStatus())) { |
| | | throw exception(PURCHASE_PLAN_AUDIT_FAIL_NOT_PROCESS); |
| | | } |
| | | ProcessDefinition existing = latestVersionMap.get(pd.getKey()); |
| | | if (existing == null || pd.getVersion() > existing.getVersion()) { |
| | | latestVersionMap.put(pd.getKey(), pd); |
| | | // 3. æ ¡éªå½åç»å½ç¨æ·æ¯è¯¥ä¸å¡ç±»åç审æ¹äººï¼æç¾ï¼ä»»ä¸äººåå¯å®¡æ ¸ï¼ |
| | | Long userId = getLoginUserId(); |
| | | approvalConfigApi.validateApprover(PURCHASE_PLAN_APPROVE_BIZ_TYPE, userId); |
| | | // 4. å®¡æ ¸ä¸éè¿ï¼å¿
须填ååå ï¼ç¶æç½®ä¸ºå®¡æ ¸ä¸éè¿ï¼éåç¼å¶äººä¿®æ¹åå¯éæ°æäº¤ |
| | | if (!Boolean.TRUE.equals(pass)) { |
| | | if (StrUtil.isBlank(reviewRemark)) { |
| | | throw exception(PURCHASE_PLAN_AUDIT_REJECT_REASON_REQUIRED); |
| | | } |
| | | purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.REJECT.getStatus(), |
| | | userId, getUserNickname(userId), reviewRemark); |
| | | return; |
| | | } |
| | | // 5. å®¡æ ¸éè¿ï¼è®°å½å®¡æ ¸äººä¸å®¡æ ¸æè§ï¼ç¶æç½®ä¸ºå®¡æ ¸éè¿ |
| | | purchasePlanMapper.auditPurchasePlan(id, ErpPurchasePlanStatusEnum.APPROVE.getStatus(), |
| | | userId, getUserNickname(userId), reviewRemark); |
| | | // 6. å®¡æ ¸éè¿ä¸æªçæç³è¯·ï¼èªå¨çæéè´ç³è¯· |
| | | if (!Boolean.TRUE.equals(plan.getGeneratedFlag())) { |
| | | try { |
| | | List<Long> requestIds = doGeneratePurchaseRequests(plan); |
| | | log.info("[auditPurchasePlan] éè´è®¡å({}) èªå¨çæéè´ç³è¯·({})", id, requestIds); |
| | | } catch (Exception e) { |
| | | // 䏿åºå¼å¸¸ï¼é¿å
å½±åå®¡æ ¸ç»æè½åºï¼çæå¤±è´¥å¯ç±ç¨æ·æå¨è§¦å |
| | | log.error("[auditPurchasePlan] éè´è®¡å({}) èªå¨çæéè´ç³è¯·å¤±è´¥", id, e); |
| | | } |
| | | } |
| | | // 4. è¿åæµç¨å®ä¹å表 |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (ProcessDefinition pd : latestVersionMap.values()) { |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("id", pd.getId()); |
| | | item.put("key", pd.getKey()); |
| | | item.put("name", pd.getName()); |
| | | result.add(item); |
| | | } |
| | | return result; |
| | | |
| | | /** |
| | | * è·åç¨æ·æµç§° |
| | | * |
| | | * @param userId ç¨æ·ç¼å· |
| | | * @return æµç§°ï¼ç¨æ·ä¸å卿¶è¿å null |
| | | */ |
| | | private String getUserNickname(Long userId) { |
| | | if (userId == null) { |
| | | return null; |
| | | } |
| | | AdminUserRespDTO user = adminUserApi.getUser(userId); |
| | | return user == null ? null : user.getNickname(); |
| | | } |
| | | |
| | | @Override |
| | |
| | | return ErpPurchasePlanStatusEnum.CANCEL.getStatus(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private void validatePurchasePlanApproveCategoryAndProcessDefinition() { |
| | | List<cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO> categories = |
| | | bpmCategoryService.getCategoryListByCode(Collections.singletonList(PURCHASE_PLAN_APPROVE_CATEGORY_CODE)); |
| | | if (CollUtil.isEmpty(categories)) { |
| | | throw exception(PURCHASE_PLAN_BPM_CATEGORY_NOT_EXISTS); |
| | | } |
| | | List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService |
| | | .getProcessDefinitionInfoListByCategory(PURCHASE_PLAN_APPROVE_CATEGORY_CODE); |
| | | if (CollUtil.isEmpty(definitionInfoList)) { |
| | | throw exception(PURCHASE_PLAN_BPM_PROCESS_DEFINITION_NOT_EXISTS); |
| | | } |
| | | } |
| | | |
| | | private ErpPurchasePlanDO validatePurchasePlanExists(Long id) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.api.approval; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® API æ¥å£ |
| | | * |
| | | * ä¾åä¸å¡æ¨¡åï¼ERP / CRM / HRM / MES / å®åï¼å¨æäº¤å®¡æ¹ãæ§è¡å®¡æ ¸æ¶è°ç¨ï¼ |
| | | * æ¿ä»£åå
ä¾èµ BPMï¼Flowableï¼æµç¨å®ä¹çæ¹å¼ã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | public interface ApprovalConfigApi { |
| | | |
| | | /** |
| | | * æ ¡éªä¸å¡ç±»åå·²å¯ç¨å®¡æ¹ä¸é
ç½®äºææå®¡æ¹äººï¼å¹¶è¿å审æ¹äººç¼å·éå |
| | | * |
| | | * ä¸å¡æ¨¡åå¨ãæäº¤å®¡æ ¸ãæ¶è°ç¨ãæ ¡éªå¤±è´¥æ¶æåºä¸å¡å¼å¸¸ï¼ |
| | | * æªé
ç½®å®¡æ¹ / æªå¯ç¨å®¡æ¹ / æªé
置审æ¹äººã |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @return 审æ¹äººç¨æ·ç¼å·éå |
| | | */ |
| | | Set<Long> validateApprovalEnabledAndGetApprovers(String bizType); |
| | | |
| | | /** |
| | | * æ ¡éªæå®ç¨æ·æ¯å¦ä¸ºè¯¥ä¸å¡ç±»åç审æ¹äºº |
| | | * |
| | | * ä¸å¡æ¨¡åå¨ãæ§è¡å®¡æ ¸ãæ¶è°ç¨ï¼é审æ¹äººå°æåºä¸å¡å¼å¸¸ã |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @param userId ç¨æ·ç¼å· |
| | | */ |
| | | void validateApprover(String bizType, Long userId); |
| | | |
| | | /** |
| | | * è·å¾æå®ä¸å¡ç±»åç审æ¹äººç¼å·éå |
| | | * |
| | | * ç¨äºåè¡¨é¡µå¤ææ¯å¦å±ç¤ºå®¡æ ¸æé®ï¼æªé
ç½®æ¶è¿å空éåã |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @return 审æ¹äººç¨æ·ç¼å·éå |
| | | */ |
| | | Set<Long> getApproverUserIds(String bizType); |
| | | |
| | | /** |
| | | * 夿æå®ç¨æ·æ¯å¦ä¸ºè¯¥ä¸å¡ç±»åç审æ¹äºº |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @param userId ç¨æ·ç¼å· |
| | | * @return æ¯å¦ä¸ºå®¡æ¹äºº |
| | | */ |
| | | default boolean isApprover(String bizType, Long userId) { |
| | | return userId != null && getApproverUserIds(bizType).contains(userId); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.api.approval; |
| | | |
| | | import cn.iocoder.yudao.module.system.service.approval.ApprovalConfigService; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® API å®ç°ç±» |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Service |
| | | public class ApprovalConfigApiImpl implements ApprovalConfigApi { |
| | | |
| | | @Resource |
| | | private ApprovalConfigService approvalConfigService; |
| | | |
| | | @Override |
| | | public Set<Long> validateApprovalEnabledAndGetApprovers(String bizType) { |
| | | return approvalConfigService.validateApprovalEnabledAndGetApprovers(bizType); |
| | | } |
| | | |
| | | @Override |
| | | public void validateApprover(String bizType, Long userId) { |
| | | approvalConfigService.validateApprover(bizType, userId); |
| | | } |
| | | |
| | | @Override |
| | | public Set<Long> getApproverUserIds(String bizType) { |
| | | return approvalConfigService.getApproverUserIds(bizType); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO; |
| | | import cn.iocoder.yudao.module.system.service.approval.ApprovalConfigService; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.Parameter; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.validation.Valid; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | |
| | | @Tag(name = "管çåå° - éç¨å®¡æ¹é
ç½®") |
| | | @RestController |
| | | @RequestMapping("/system/approval-config") |
| | | @Validated |
| | | public class ApprovalConfigController { |
| | | |
| | | @Resource |
| | | private ApprovalConfigService approvalConfigService; |
| | | |
| | | @PostMapping("/save") |
| | | @Operation(summary = "ä¿å审æ¹é
ç½®ï¼æ°å¢æä¿®æ¹ï¼ï¼å®¡æ¹äººæ´ä½è¦ç") |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:update')") |
| | | public CommonResult<Long> saveApprovalConfig(@Valid @RequestBody ApprovalConfigSaveReqVO saveReqVO) { |
| | | return success(approvalConfigService.saveApprovalConfig(saveReqVO)); |
| | | } |
| | | |
| | | @DeleteMapping("/delete") |
| | | @Operation(summary = "å é¤å®¡æ¹é
ç½®") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:update')") |
| | | public CommonResult<Boolean> deleteApprovalConfig(@RequestParam("id") Long id) { |
| | | approvalConfigService.deleteApprovalConfig(id); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/get") |
| | | @Operation(summary = "è·å¾å®¡æ¹é
ç½®") |
| | | @Parameter(name = "id", description = "ç¼å·", required = true, example = "1") |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:query')") |
| | | public CommonResult<ApprovalConfigRespVO> getApprovalConfig(@RequestParam("id") Long id) { |
| | | ApprovalConfigDO config = approvalConfigService.getApprovalConfig(id); |
| | | if (config == null) { |
| | | return success(null); |
| | | } |
| | | ApprovalConfigRespVO vo = BeanUtils.toBean(config, ApprovalConfigRespVO.class); |
| | | vo.setUserIds(new ArrayList<>(approvalConfigService.getApproverUserIds(config.getBizType()))); |
| | | return success(vo); |
| | | } |
| | | |
| | | @GetMapping("/page") |
| | | @Operation(summary = "è·å¾å®¡æ¹é
ç½®å页") |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:query')") |
| | | public CommonResult<PageResult<ApprovalConfigRespVO>> getApprovalConfigPage( |
| | | @Valid ApprovalConfigPageReqVO pageReqVO) { |
| | | return success(approvalConfigService.getApprovalConfigPage(pageReqVO)); |
| | | } |
| | | |
| | | @GetMapping("/approver-ids") |
| | | @Operation(summary = "è·å¾æå®ä¸å¡ç±»åç审æ¹äººç¼å·éå") |
| | | @Parameter(name = "bizType", description = "ä¸å¡ç±»åç¼ç ", required = true, example = "purchase_plan_approve") |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:query')") |
| | | public CommonResult<Set<Long>> getApproverUserIds(@RequestParam("bizType") String bizType) { |
| | | return success(approvalConfigService.getApproverUserIds(bizType)); |
| | | } |
| | | |
| | | @GetMapping("/is-approver") |
| | | @Operation(summary = "夿å½åç»å½ç¨æ·æ¯å¦ä¸ºæå®ä¸å¡ç±»åç审æ¹äºº") |
| | | @Parameter(name = "bizType", description = "ä¸å¡ç±»åç¼ç ", required = true, example = "purchase_plan_approve") |
| | | @PreAuthorize("@ss.hasPermission('system:approval-config:query')") |
| | | public CommonResult<Boolean> isApprover(@RequestParam("bizType") String bizType) { |
| | | Set<Long> approverIds = approvalConfigService.getApproverUserIds(bizType); |
| | | return success(approverIds.contains(getLoginUserId())); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.approval.vo; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | import lombok.ToString; |
| | | |
| | | @Schema(description = "管çåå° - 审æ¹é
ç½®å页 Request VO") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @ToString(callSuper = true) |
| | | public class ApprovalConfigPageReqVO extends PageParam { |
| | | |
| | | @Schema(description = "ä¸å¡ç±»ååç§°", example = "éè´è®¡å") |
| | | private String bizName; |
| | | |
| | | @Schema(description = "ä¸å¡ç±»åç¼ç ", example = "purchase_plan_approve") |
| | | private String bizType; |
| | | |
| | | @Schema(description = "æ¯å¦å¯ç¨å®¡æ¹", example = "true") |
| | | private Boolean approvalEnabled; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.approval.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "管çåå° - 审æ¹é
ç½® Response VO") |
| | | @Data |
| | | public class ApprovalConfigRespVO { |
| | | |
| | | @Schema(description = "主é®ID", example = "1") |
| | | private Long id; |
| | | |
| | | @Schema(description = "ä¸å¡ç±»åç¼ç ", example = "purchase_plan_approve") |
| | | private String bizType; |
| | | |
| | | @Schema(description = "ä¸å¡ç±»ååç§°", example = "éè´è®¡åå®¡æ ¸") |
| | | private String bizName; |
| | | |
| | | @Schema(description = "æ¯å¦å¯ç¨å®¡æ¹", example = "true") |
| | | private Boolean approvalEnabled; |
| | | |
| | | @Schema(description = "审æ¹äººç¨æ·IDå表", example = "[1,2]") |
| | | private List<Long> userIds; |
| | | |
| | | @Schema(description = "审æ¹äººå§åå表ï¼åä½å±ç¤ºï¼é¿å
åç«¯åæ¥ç¨æ·ï¼", example = "[\"å¼ ä¸\",\"æå\"]") |
| | | private List<String> userNames; |
| | | |
| | | @Schema(description = "夿³¨", example = "ç±éè´é¨ç»çå®¡æ ¸") |
| | | private String remark; |
| | | |
| | | @Schema(description = "å建æ¶é´") |
| | | private LocalDateTime createTime; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.controller.admin.approval.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Schema(description = "管çåå° - 审æ¹é
ç½®ä¿å Request VO") |
| | | @Data |
| | | public class ApprovalConfigSaveReqVO { |
| | | |
| | | @Schema(description = "主é®IDï¼æ°å¢æ¶ä¸ºç©º", example = "1") |
| | | private Long id; |
| | | |
| | | @Schema(description = "ä¸å¡ç±»åç¼ç ", requiredMode = Schema.RequiredMode.REQUIRED, example = "purchase_plan_approve") |
| | | @NotEmpty(message = "ä¸å¡ç±»åç¼ç ä¸è½ä¸ºç©º") |
| | | private String bizType; |
| | | |
| | | @Schema(description = "ä¸å¡ç±»ååç§°", requiredMode = Schema.RequiredMode.REQUIRED, example = "éè´è®¡åå®¡æ ¸") |
| | | @NotEmpty(message = "ä¸å¡ç±»ååç§°ä¸è½ä¸ºç©º") |
| | | private String bizName; |
| | | |
| | | @Schema(description = "æ¯å¦å¯ç¨å®¡æ¹", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| | | @NotNull(message = "æ¯å¦å¯ç¨å®¡æ¹ä¸è½ä¸ºç©º") |
| | | private Boolean approvalEnabled; |
| | | |
| | | @Schema(description = "审æ¹äººç¨æ·IDå表ï¼å¤ä¸ªä¹é´ä¸ºæç¾ï¼ä»»æä¸äººéè¿å³å¯ï¼", example = "[1,2]") |
| | | private List<Long> userIds; |
| | | |
| | | @Schema(description = "夿³¨", example = "ç±éè´é¨ç»çå®¡æ ¸") |
| | | private String remark; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.dal.dataobject.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.baomidou.mybatisplus.annotation.KeySequence; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.*; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® DO |
| | | * |
| | | * ä¸ä¸ªä¸å¡ç±»å对åºä¸æ¡é
ç½®ï¼å®¡æ¹äººåæ¾å¨ {@link ApprovalConfigUserDO} å表ã |
| | | * 审æ¹äººé
ç½®å¤ä¸ªæ¶æãæç¾ãå¤çï¼å³ä»»æä¸äººå®¡æ ¸éè¿å³æ¨è¿ä¸å¡ç¶æã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @TableName("system_approval_config") |
| | | @KeySequence("system_approval_config_seq") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @ToString(callSuper = true) |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class ApprovalConfigDO extends BaseDO { |
| | | |
| | | /** |
| | | * 主é®ID |
| | | */ |
| | | @TableId |
| | | private Long id; |
| | | /** |
| | | * ä¸å¡ç±»åç¼ç ï¼ä¸å模å审æ¹åç±»ç¼ç ä¸è´ |
| | | */ |
| | | private String bizType; |
| | | /** |
| | | * ä¸å¡ç±»ååç§° |
| | | */ |
| | | private String bizName; |
| | | /** |
| | | * æ¯å¦å¯ç¨å®¡æ¹ |
| | | */ |
| | | private Boolean approvalEnabled; |
| | | /** |
| | | * 夿³¨ |
| | | */ |
| | | private String remark; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.dal.dataobject.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.baomidou.mybatisplus.annotation.KeySequence; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.*; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½®-审æ¹äºº DO |
| | | * |
| | | * ä¸ä¸ªä¸å¡ç±»åå¯é
ç½®å¤ä¸ªå®¡æ¹äººï¼å¤ä¸ªå®¡æ¹äººä¹é´ä¸ºãæç¾ãå
³ç³»ã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @TableName("system_approval_config_user") |
| | | @KeySequence("system_approval_config_user_seq") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @ToString(callSuper = true) |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class ApprovalConfigUserDO extends BaseDO { |
| | | |
| | | /** |
| | | * 主é®ID |
| | | */ |
| | | @TableId |
| | | private Long id; |
| | | /** |
| | | * 审æ¹é
ç½®IDï¼å
³è {@link ApprovalConfigDO#getId()}ï¼ |
| | | */ |
| | | private Long configId; |
| | | /** |
| | | * 审æ¹äººç¨æ·IDï¼ç³»ç»ç¨æ·ï¼ |
| | | */ |
| | | private Long userId; |
| | | /** |
| | | * æ¾ç¤ºé¡ºåº |
| | | */ |
| | | private Integer sort; |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.dal.mysql.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® Mapper |
| | | * |
| | | * ç±»å带 System åç¼ï¼é¿å
ä¸ MES ç WmsWarehouseApprovalConfigMapper å¨ |
| | | * Spring æå段åï¼byNameï¼æ³¨å
¥æ¶äº§çå½åå²çªã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Mapper |
| | | public interface SystemApprovalConfigMapper extends BaseMapperX<ApprovalConfigDO> { |
| | | |
| | | default PageResult<ApprovalConfigDO> selectPage(ApprovalConfigPageReqVO reqVO) { |
| | | return selectPage(reqVO, new LambdaQueryWrapperX<ApprovalConfigDO>() |
| | | .likeIfPresent(ApprovalConfigDO::getBizName, reqVO.getBizName()) |
| | | .eqIfPresent(ApprovalConfigDO::getBizType, reqVO.getBizType()) |
| | | .eqIfPresent(ApprovalConfigDO::getApprovalEnabled, reqVO.getApprovalEnabled()) |
| | | .orderByAsc(ApprovalConfigDO::getId)); |
| | | } |
| | | |
| | | default ApprovalConfigDO selectByBizType(String bizType) { |
| | | return selectOne(ApprovalConfigDO::getBizType, bizType); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.dal.mysql.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigUserDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½®-审æ¹äºº Mapper |
| | | * |
| | | * ç±»å带 System åç¼ï¼é¿å
Spring æåæ®µåæ³¨å
¥æ¶ä¸å
¶å®æ¨¡åçå®¡æ¹ Mapper å²çªã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Mapper |
| | | public interface SystemApprovalConfigUserMapper extends BaseMapperX<ApprovalConfigUserDO> { |
| | | |
| | | default List<ApprovalConfigUserDO> selectListByConfigId(Long configId) { |
| | | return selectList(new LambdaQueryWrapperX<ApprovalConfigUserDO>() |
| | | .eq(ApprovalConfigUserDO::getConfigId, configId) |
| | | .orderByAsc(ApprovalConfigUserDO::getSort)); |
| | | } |
| | | |
| | | default List<ApprovalConfigUserDO> selectListByConfigIds(Collection<Long> configIds) { |
| | | return selectList(new LambdaQueryWrapperX<ApprovalConfigUserDO>() |
| | | .in(ApprovalConfigUserDO::getConfigId, configIds) |
| | | .orderByAsc(ApprovalConfigUserDO::getSort)); |
| | | } |
| | | |
| | | /** |
| | | * æé
ç½®IDç©çå é¤å®¡æ¹äººè®°å½ |
| | | * |
| | | * é
置审æ¹äººéç¨ãå
å åæãæ´ä½è¦ççä¿åæ¹å¼ï¼è¥èµ°é»è¾å é¤ï¼ |
| | | * åå¤ä¿åä¼å¨è¡¨ä¸å 积大é deleted = 1 çåå²è¡ï¼æ
æ¤å¤ç´æ¥ç©çå é¤ã |
| | | * |
| | | * @param configId 审æ¹é
ç½®ID |
| | | * @return å é¤è¡æ° |
| | | */ |
| | | default int deleteByConfigId(Long configId) { |
| | | return delete(new LambdaQueryWrapperX<ApprovalConfigUserDO>() |
| | | .eq(ApprovalConfigUserDO::getConfigId, configId)); |
| | | } |
| | | |
| | | } |
| | |
| | | ErrorCode STORAGE_BLOB_NOT_EXISTS = new ErrorCode(1_002_029_004, "æä»¶ä¸åå¨"); |
| | | ErrorCode STORAGE_BLOB_PUBLIC_KEY_INVALID = new ErrorCode(1_002_029_005, "æä»¶å
¬å¼è®¿é®keyæ æ"); |
| | | |
| | | // ========== éç¨å®¡æ¹é
ç½® 1-002-030-000 ========== |
| | | ErrorCode APPROVAL_CONFIG_NOT_EXISTS = new ErrorCode(1_002_030_000, "审æ¹é
ç½®ä¸åå¨"); |
| | | ErrorCode APPROVAL_CONFIG_BIZ_TYPE_DUPLICATE = new ErrorCode(1_002_030_001, "ä¸å¡ç±»åç¼ç ({})å·²åå¨"); |
| | | ErrorCode APPROVAL_CONFIG_NOT_ENABLED = new ErrorCode(1_002_030_002, "该ä¸å¡ç±»åæªå¯ç¨å®¡æ¹ï¼æ æ³æäº¤å®¡æ ¸"); |
| | | ErrorCode APPROVAL_CONFIG_APPROVER_EMPTY = new ErrorCode(1_002_030_003, "该ä¸å¡ç±»åæªé
置审æ¹äººï¼è¯·å
å¨å®¡æ¹é
ç½®ä¸æå®å®¡æ¹äºº"); |
| | | ErrorCode APPROVAL_CONFIG_APPROVER_NOT_EXISTS = new ErrorCode(1_002_030_004, "æé审æ¹äººä¸åå¨æå·²è¢«ç¦ç¨"); |
| | | ErrorCode APPROVAL_CONFIG_NOT_APPROVER = new ErrorCode(1_002_030_005, "æ¨ä¸æ¯è¯¥ä¸å¡ç审æ¹äººï¼æ æè¿è¡å®¡æ ¸æä½"); |
| | | ErrorCode APPROVAL_CONFIG_REJECT_REASON_REQUIRED = new ErrorCode(1_002_030_006, "å®¡æ ¸ä¸éè¿æ¶å¿
须填åä¸éè¿åå "); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.service.approval; |
| | | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO; |
| | | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® Service æ¥å£ |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | public interface ApprovalConfigService { |
| | | |
| | | /** |
| | | * ä¿å审æ¹é
ç½®ï¼æ°å¢æä¿®æ¹ï¼ |
| | | * |
| | | * 审æ¹äººéç¨æ´ä½è¦çæ¹å¼ä¿åï¼å³ä¼ å
¥ç userIds å³ä¸ºæç»ç»æã |
| | | * |
| | | * @param saveReqVO ä¿åä¿¡æ¯ |
| | | * @return é
ç½®ç¼å· |
| | | */ |
| | | Long saveApprovalConfig(ApprovalConfigSaveReqVO saveReqVO); |
| | | |
| | | /** |
| | | * å é¤å®¡æ¹é
ç½® |
| | | * |
| | | * @param id é
ç½®ç¼å· |
| | | */ |
| | | void deleteApprovalConfig(Long id); |
| | | |
| | | /** |
| | | * è·å¾å®¡æ¹é
ç½® |
| | | * |
| | | * @param id é
ç½®ç¼å· |
| | | * @return 审æ¹é
ç½® |
| | | */ |
| | | ApprovalConfigDO getApprovalConfig(Long id); |
| | | |
| | | /** |
| | | * æä¸å¡ç±»åè·å¾å®¡æ¹é
ç½® |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @return 审æ¹é
ç½®ï¼ä¸å卿¶è¿å null |
| | | */ |
| | | ApprovalConfigDO getApprovalConfigByBizType(String bizType); |
| | | |
| | | /** |
| | | * è·å¾å®¡æ¹é
ç½®å页ï¼å«å®¡æ¹äººä¿¡æ¯ |
| | | * |
| | | * @param pageReqVO å页æ¥è¯¢ |
| | | * @return åé¡µç»æ |
| | | */ |
| | | PageResult<ApprovalConfigRespVO> getApprovalConfigPage(ApprovalConfigPageReqVO pageReqVO); |
| | | |
| | | /** |
| | | * è·å¾æå®ä¸å¡ç±»åç审æ¹äººç¨æ·ç¼å·éå |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @return 审æ¹äººç¨æ·ç¼å·éåï¼æªé
ç½®æ¶è¿å空éå |
| | | */ |
| | | Set<Long> getApproverUserIds(String bizType); |
| | | |
| | | /** |
| | | * æ ¡éªå®¡æ¹æµç¨å¯ç¨ï¼å¹¶è¿å审æ¹äººéå |
| | | * |
| | | * æ ¡éªå
容ï¼ä¸å¡ç±»åå·²å¯ç¨å®¡æ¹ãä¸å·²é
ç½®è³å°ä¸ä¸ªææå®¡æ¹äººã |
| | | * ä¾åä¸å¡æ¨¡åå¨ãæäº¤å®¡æ ¸ãæ¶è°ç¨ã |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @return 审æ¹äººç¨æ·ç¼å·éå |
| | | */ |
| | | Set<Long> validateApprovalEnabledAndGetApprovers(String bizType); |
| | | |
| | | /** |
| | | * æ ¡éªæå®ç¨æ·æ¯å¦ä¸ºè¯¥ä¸å¡ç±»åç审æ¹äºº |
| | | * |
| | | * ä¾åä¸å¡æ¨¡åå¨ãå®¡æ ¸ãæ¶è°ç¨ã |
| | | * |
| | | * @param bizType ä¸å¡ç±»åç¼ç |
| | | * @param userId ç¨æ·ç¼å· |
| | | */ |
| | | void validateApprover(String bizType, Long userId); |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.service.approval; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigPageReqVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigRespVO; |
| | | import cn.iocoder.yudao.module.system.controller.admin.approval.vo.ApprovalConfigSaveReqVO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigDO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.approval.ApprovalConfigUserDO; |
| | | import cn.iocoder.yudao.module.system.dal.mysql.approval.SystemApprovalConfigMapper; |
| | | import cn.iocoder.yudao.module.system.dal.mysql.approval.SystemApprovalConfigUserMapper; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| | | import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * éç¨å®¡æ¹é
ç½® Service å®ç°ç±» |
| | | * |
| | | * 设计说æï¼æ¬é
ç½®æ¿ä»£åæåºäº BPMï¼Flowableï¼æµç¨å®ä¹çå®¡æ¹æ¹å¼ï¼éç¨è½»é级å级审æ¹ã |
| | | * åä¸ä¸å¡ç±»åå¯é
ç½®å¤ä¸ªå®¡æ¹äººï¼å¤äººä¹é´ä¸ºãæç¾ãå
³ç³»ï¼å³ä»»æä¸äººå®¡æ ¸éè¿å³æ¨è¿ä¸å¡ç¶æã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Service |
| | | @Validated |
| | | @Slf4j |
| | | public class ApprovalConfigServiceImpl implements ApprovalConfigService { |
| | | |
| | | @Resource |
| | | private SystemApprovalConfigMapper systemApprovalConfigMapper; |
| | | |
| | | @Resource |
| | | private SystemApprovalConfigUserMapper systemApprovalConfigUserMapper; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long saveApprovalConfig(ApprovalConfigSaveReqVO saveReqVO) { |
| | | // 1. æ ¡éªä¸å¡ç±»åç¼ç å¯ä¸ |
| | | validateBizTypeUnique(saveReqVO.getId(), saveReqVO.getBizType()); |
| | | // 2. æ ¡éªå®¡æ¹äººåå¨ä¸æªè¢«ç¦ç¨ |
| | | List<Long> userIds = saveReqVO.getUserIds() == null ? Collections.emptyList() |
| | | : saveReqVO.getUserIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | validateApproverUsers(userIds); |
| | | // 3. ä¿å主表 |
| | | ApprovalConfigDO config = BeanUtils.toBean(saveReqVO, ApprovalConfigDO.class); |
| | | if (config.getId() == null) { |
| | | config.setId(null); |
| | | systemApprovalConfigMapper.insert(config); |
| | | } else { |
| | | validateApprovalConfigExists(config.getId()); |
| | | systemApprovalConfigMapper.updateById(config); |
| | | } |
| | | // 4. ä¿å审æ¹äººï¼æ´ä½è¦çï¼å
ç©çå 餿§çï¼åæå
¥æ°çï¼ |
| | | systemApprovalConfigUserMapper.deleteByConfigId(config.getId()); |
| | | if (CollUtil.isNotEmpty(userIds)) { |
| | | List<ApprovalConfigUserDO> userDOList = new ArrayList<>(userIds.size()); |
| | | for (int i = 0; i < userIds.size(); i++) { |
| | | userDOList.add(ApprovalConfigUserDO.builder() |
| | | .configId(config.getId()) |
| | | .userId(userIds.get(i)) |
| | | .sort(i) |
| | | .build()); |
| | | } |
| | | systemApprovalConfigUserMapper.insertBatch(userDOList); |
| | | } |
| | | return config.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteApprovalConfig(Long id) { |
| | | validateApprovalConfigExists(id); |
| | | systemApprovalConfigMapper.deleteById(id); |
| | | systemApprovalConfigUserMapper.deleteByConfigId(id); |
| | | } |
| | | |
| | | @Override |
| | | public ApprovalConfigDO getApprovalConfig(Long id) { |
| | | return systemApprovalConfigMapper.selectById(id); |
| | | } |
| | | |
| | | @Override |
| | | public ApprovalConfigDO getApprovalConfigByBizType(String bizType) { |
| | | if (bizType == null) { |
| | | return null; |
| | | } |
| | | return systemApprovalConfigMapper.selectByBizType(bizType); |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<ApprovalConfigRespVO> getApprovalConfigPage(ApprovalConfigPageReqVO pageReqVO) { |
| | | PageResult<ApprovalConfigDO> pageResult = systemApprovalConfigMapper.selectPage(pageReqVO); |
| | | if (CollUtil.isEmpty(pageResult.getList())) { |
| | | return PageResult.empty(pageResult.getTotal()); |
| | | } |
| | | // 1. æ¹éæ¥è¯¢å®¡æ¹äººå表 |
| | | List<ApprovalConfigUserDO> allConfigUsers = systemApprovalConfigUserMapper.selectListByConfigIds( |
| | | convertSet(pageResult.getList(), ApprovalConfigDO::getId)); |
| | | Map<Long, List<ApprovalConfigUserDO>> configUserMap = allConfigUsers.stream() |
| | | .collect(Collectors.groupingBy(ApprovalConfigUserDO::getConfigId)); |
| | | // 2. æ¹éæ¥è¯¢å®¡æ¹äººæµç§° |
| | | Map<Long, String> nicknameMap = getUserNicknameMap( |
| | | convertSet(allConfigUsers, ApprovalConfigUserDO::getUserId)); |
| | | // 3. ç»è£
è¿å |
| | | List<ApprovalConfigRespVO> list = convertList(pageResult.getList(), config -> { |
| | | ApprovalConfigRespVO vo = BeanUtils.toBean(config, ApprovalConfigRespVO.class); |
| | | List<ApprovalConfigUserDO> configUsers = configUserMap.getOrDefault(config.getId(), |
| | | Collections.emptyList()); |
| | | vo.setUserIds(convertList(configUsers, ApprovalConfigUserDO::getUserId)); |
| | | vo.setUserNames(convertList(configUsers, user -> nicknameMap.get(user.getUserId()))); |
| | | return vo; |
| | | }); |
| | | return new PageResult<>(list, pageResult.getTotal()); |
| | | } |
| | | |
| | | @Override |
| | | public Set<Long> getApproverUserIds(String bizType) { |
| | | ApprovalConfigDO config = getApprovalConfigByBizType(bizType); |
| | | if (config == null) { |
| | | return Collections.emptySet(); |
| | | } |
| | | return getValidApproverUserIds(config.getId()); |
| | | } |
| | | |
| | | @Override |
| | | public Set<Long> validateApprovalEnabledAndGetApprovers(String bizType) { |
| | | // 1. æ ¡éªä¸å¡ç±»åå·²é
ç½®å®¡æ¹ |
| | | ApprovalConfigDO config = getApprovalConfigByBizType(bizType); |
| | | if (config == null) { |
| | | throw exception(APPROVAL_CONFIG_NOT_EXISTS); |
| | | } |
| | | // 2. æ ¡éªå·²å¯ç¨å®¡æ¹ |
| | | if (!Boolean.TRUE.equals(config.getApprovalEnabled())) { |
| | | throw exception(APPROVAL_CONFIG_NOT_ENABLED); |
| | | } |
| | | // 3. æ ¡éªå卿æå®¡æ¹äººï¼è¿æ»¤æå·²å é¤ / å·²ç¦ç¨çç¨æ·ï¼ |
| | | Set<Long> approverIds = getValidApproverUserIds(config.getId()); |
| | | if (CollUtil.isEmpty(approverIds)) { |
| | | throw exception(APPROVAL_CONFIG_APPROVER_EMPTY); |
| | | } |
| | | return approverIds; |
| | | } |
| | | |
| | | @Override |
| | | public void validateApprover(String bizType, Long userId) { |
| | | Set<Long> approverIds = getApproverUserIds(bizType); |
| | | if (userId == null || !approverIds.contains(userId)) { |
| | | throw exception(APPROVAL_CONFIG_NOT_APPROVER); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¥è¯¢é
ç½®ä¸çææå®¡æ¹äººç¼å·ï¼è¿æ»¤å·²å é¤ / å·²ç¦ç¨çç¨æ·ï¼ |
| | | * |
| | | * 审æ¹äººå¯è½å¨é
ç½®ä¹å被ç¦ç¨æå é¤ï¼æ¤å¤ç»ä¸è¿æ»¤ï¼ |
| | | * é¿å
åºç°ãé
ç½®éæäººãä½å®é
æ 人å¯å®¡ã导è´åæ®å¡å¨å¾
å®¡æ ¸ç¶æã |
| | | * |
| | | * @param configId é
ç½®ç¼å· |
| | | * @return ææå®¡æ¹äººç¼å·éå |
| | | */ |
| | | private Set<Long> getValidApproverUserIds(Long configId) { |
| | | List<ApprovalConfigUserDO> configUsers = systemApprovalConfigUserMapper.selectListByConfigId(configId); |
| | | if (CollUtil.isEmpty(configUsers)) { |
| | | return Collections.emptySet(); |
| | | } |
| | | Set<Long> userIds = convertSet(configUsers, ApprovalConfigUserDO::getUserId); |
| | | return convertSet(adminUserApi.getUserList(userIds), AdminUserRespDTO::getId, |
| | | user -> !CommonStatusEnum.isDisable(user.getStatus())); |
| | | } |
| | | |
| | | private Map<Long, String> getUserNicknameMap(Collection<Long> userIds) { |
| | | if (CollUtil.isEmpty(userIds)) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | return adminUserApi.getUserList(userIds).stream() |
| | | .collect(Collectors.toMap(AdminUserRespDTO::getId, AdminUserRespDTO::getNickname, (a, b) -> a)); |
| | | } |
| | | |
| | | private void validateApprovalConfigExists(Long id) { |
| | | if (systemApprovalConfigMapper.selectById(id) == null) { |
| | | throw exception(APPROVAL_CONFIG_NOT_EXISTS); |
| | | } |
| | | } |
| | | |
| | | private void validateBizTypeUnique(Long id, String bizType) { |
| | | ApprovalConfigDO config = systemApprovalConfigMapper.selectByBizType(bizType); |
| | | if (config != null && !config.getId().equals(id)) { |
| | | throw exception(APPROVAL_CONFIG_BIZ_TYPE_DUPLICATE, bizType); |
| | | } |
| | | } |
| | | |
| | | private void validateApproverUsers(Collection<Long> userIds) { |
| | | if (CollUtil.isEmpty(userIds)) { |
| | | return; |
| | | } |
| | | List<AdminUserRespDTO> users = adminUserApi.getUserList(userIds); |
| | | Map<Long, AdminUserRespDTO> userMap = users.stream() |
| | | .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (a, b) -> a)); |
| | | for (Long userId : userIds) { |
| | | AdminUserRespDTO user = userMap.get(userId); |
| | | if (user == null || CommonStatusEnum.isDisable(user.getStatus())) { |
| | | throw exception(APPROVAL_CONFIG_APPROVER_NOT_EXISTS); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |