Compare commits
12 Commits
71f6cfed3d
...
3586f86ea6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3586f86ea6 | ||
|
|
bfb00804e6 | ||
|
|
767214b7db | ||
|
|
0ff4a201b0 | ||
| 71f9dab1be | |||
|
|
aefa44b8a9 | ||
| 4aa9952b26 | |||
| c8ce7a6fa3 | |||
|
|
94132ea8da | ||
|
|
e281f7fd92 | ||
|
|
22814cb1a7 | ||
|
|
44a5868349 |
92
jeecg-boot/db/mes-xsl-final-batch-plan-menu.sql
Normal file
92
jeecg-boot/db/mes-xsl-final-batch-plan-menu.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- 终胶计划菜单与权限(挂到「MES密炼工程」目录)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @mixer_parent_id = (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
);
|
||||
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
));
|
||||
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
|
||||
|
||||
INSERT INTO sys_permission(
|
||||
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
|
||||
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
|
||||
)
|
||||
VALUES (
|
||||
'1860000000000099711', @mixer_parent_id, '终胶计划',
|
||||
'/mes/finalbatchplaninfo',
|
||||
'mes/finalbatchplaninfo/index',
|
||||
'MesXslFinalBatchPlanList', 1, NULL, '1', 35,
|
||||
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
url = VALUES(url),
|
||||
component = VALUES(component),
|
||||
component_name = VALUES(component_name),
|
||||
menu_type = VALUES(menu_type),
|
||||
perms = VALUES(perms),
|
||||
perms_type = VALUES(perms_type),
|
||||
sort_no = VALUES(sort_no),
|
||||
is_route = VALUES(is_route),
|
||||
is_leaf = VALUES(is_leaf),
|
||||
hidden = VALUES(hidden),
|
||||
status = VALUES(status),
|
||||
del_flag = VALUES(del_flag),
|
||||
keep_alive = VALUES(keep_alive),
|
||||
internal_or_external = VALUES(internal_or_external);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
|
||||
('1860000000000099712', '1860000000000099711', '新增', 2, 'xslmes:mes_xsl_final_batch_plan:add', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099713', '1860000000000099711', '编辑', 2, 'xslmes:mes_xsl_final_batch_plan:edit', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099714', '1860000000000099711', '删除', 2, 'xslmes:mes_xsl_final_batch_plan:delete', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099715', '1860000000000099711', '批量删除', 2, 'xslmes:mes_xsl_final_batch_plan:deleteBatch', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099716', '1860000000000099711', '导出', 2, 'xslmes:mes_xsl_final_batch_plan:exportXls', '1', '1', 0, 'admin', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
menu_type = VALUES(menu_type),
|
||||
perms = VALUES(perms),
|
||||
perms_type = VALUES(perms_type),
|
||||
status = VALUES(status),
|
||||
del_flag = VALUES(del_flag);
|
||||
|
||||
-- admin 角色授权
|
||||
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
|
||||
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
|
||||
FROM sys_permission p
|
||||
WHERE p.id IN (
|
||||
'1860000000000099711',
|
||||
'1860000000000099712', '1860000000000099713', '1860000000000099714', '1860000000000099715', '1860000000000099716'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_role_permission rp
|
||||
WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
|
||||
AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
-- 强制修复:确保菜单路由与组件路径正确
|
||||
UPDATE sys_permission
|
||||
SET
|
||||
parent_id = @mixer_parent_id,
|
||||
url = '/mes/finalbatchplaninfo',
|
||||
component = 'mes/finalbatchplaninfo/index',
|
||||
component_name = 'MesXslFinalBatchPlanList',
|
||||
menu_type = 1,
|
||||
is_route = 1,
|
||||
is_leaf = 1,
|
||||
hidden = 0,
|
||||
status = '1',
|
||||
del_flag = 0
|
||||
WHERE id = '1860000000000099711';
|
||||
30
jeecg-boot/db/mes-xsl-final-batch-plan-table.sql
Normal file
30
jeecg-boot/db/mes-xsl-final-batch-plan-table.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 终胶计划建表SQL
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_final_batch_plan` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
|
||||
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水',
|
||||
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
|
||||
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
|
||||
`order_date` date DEFAULT NULL COMMENT '订单日期',
|
||||
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编码',
|
||||
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料信息',
|
||||
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
|
||||
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
|
||||
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
|
||||
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
|
||||
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
|
||||
`status` int DEFAULT 0 COMMENT '状态:0未开始 1进行中 2已完成',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT 0 COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mxfb_source_order` (`source_order_id`),
|
||||
KEY `idx_mxfb_material_code` (`material_code`),
|
||||
UNIQUE KEY `uk_mxfb_source_order_del` (`source_order_id`, `del_flag`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES终胶计划';
|
||||
92
jeecg-boot/db/mes-xsl-master-batch-plan-menu.sql
Normal file
92
jeecg-boot/db/mes-xsl-master-batch-plan-menu.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- 母胶计划菜单与权限(挂到「MES密炼工程」目录)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @mixer_parent_id = (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
);
|
||||
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
));
|
||||
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
|
||||
|
||||
INSERT INTO sys_permission(
|
||||
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
|
||||
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
|
||||
)
|
||||
VALUES (
|
||||
'1860000000000099611', @mixer_parent_id, '母胶计划',
|
||||
'/mes/masterbatchplaninfo',
|
||||
'mes/masterbatchplaninfo/index',
|
||||
'MesXslMasterBatchPlanList', 1, NULL, '1', 34,
|
||||
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
url = VALUES(url),
|
||||
component = VALUES(component),
|
||||
component_name = VALUES(component_name),
|
||||
menu_type = VALUES(menu_type),
|
||||
perms = VALUES(perms),
|
||||
perms_type = VALUES(perms_type),
|
||||
sort_no = VALUES(sort_no),
|
||||
is_route = VALUES(is_route),
|
||||
is_leaf = VALUES(is_leaf),
|
||||
hidden = VALUES(hidden),
|
||||
status = VALUES(status),
|
||||
del_flag = VALUES(del_flag),
|
||||
keep_alive = VALUES(keep_alive),
|
||||
internal_or_external = VALUES(internal_or_external);
|
||||
|
||||
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
|
||||
('1860000000000099612', '1860000000000099611', '新增', 2, 'xslmes:mes_xsl_master_batch_plan:add', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099613', '1860000000000099611', '编辑', 2, 'xslmes:mes_xsl_master_batch_plan:edit', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099614', '1860000000000099611', '删除', 2, 'xslmes:mes_xsl_master_batch_plan:delete', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099615', '1860000000000099611', '批量删除', 2, 'xslmes:mes_xsl_master_batch_plan:deleteBatch', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099616', '1860000000000099611', '导出', 2, 'xslmes:mes_xsl_master_batch_plan:exportXls', '1', '1', 0, 'admin', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
menu_type = VALUES(menu_type),
|
||||
perms = VALUES(perms),
|
||||
perms_type = VALUES(perms_type),
|
||||
status = VALUES(status),
|
||||
del_flag = VALUES(del_flag);
|
||||
|
||||
-- admin 角色授权
|
||||
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
|
||||
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
|
||||
FROM sys_permission p
|
||||
WHERE p.id IN (
|
||||
'1860000000000099611',
|
||||
'1860000000000099612', '1860000000000099613', '1860000000000099614', '1860000000000099615', '1860000000000099616'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_role_permission rp
|
||||
WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
|
||||
AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
-- 强制修复:确保菜单路由与组件路径正确
|
||||
UPDATE sys_permission
|
||||
SET
|
||||
parent_id = @mixer_parent_id,
|
||||
url = '/mes/masterbatchplaninfo',
|
||||
component = 'mes/masterbatchplaninfo/index',
|
||||
component_name = 'MesXslMasterBatchPlanList',
|
||||
menu_type = 1,
|
||||
is_route = 1,
|
||||
is_leaf = 1,
|
||||
hidden = 0,
|
||||
status = '1',
|
||||
del_flag = 0
|
||||
WHERE id = '1860000000000099611';
|
||||
30
jeecg-boot/db/mes-xsl-master-batch-plan-table.sql
Normal file
30
jeecg-boot/db/mes-xsl-master-batch-plan-table.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 母胶计划建表SQL
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_master_batch_plan` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
|
||||
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水号',
|
||||
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
|
||||
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
|
||||
`order_date` date DEFAULT NULL COMMENT '订单日期',
|
||||
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编号',
|
||||
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料名称',
|
||||
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
|
||||
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
|
||||
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
|
||||
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
|
||||
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
|
||||
`status` int DEFAULT 0 COMMENT '状态:0未开始 1进行中 2已完成',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT 0 COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mxmbp_source_order` (`source_order_id`),
|
||||
KEY `idx_mxmbp_material_code` (`material_code`),
|
||||
UNIQUE KEY `uk_mxmbp_source_order_del` (`source_order_id`, `del_flag`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES母胶计划';
|
||||
@@ -51,7 +51,8 @@ INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, st
|
||||
('1860000000000099513', '1860000000000099511', '编辑', 2, 'xslmes:mes_xsl_production_order:edit', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099514', '1860000000000099511', '删除', 2, 'xslmes:mes_xsl_production_order:delete', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099515', '1860000000000099511', '批量删除', 2, 'xslmes:mes_xsl_production_order:deleteBatch', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099516', '1860000000000099511', '导出', 2, 'xslmes:mes_xsl_production_order:exportXls', '1', '1', 0, 'admin', NOW())
|
||||
('1860000000000099516', '1860000000000099511', '导出', 2, 'xslmes:mes_xsl_production_order:exportXls', '1', '1', 0, 'admin', NOW()),
|
||||
('1860000000000099517', '1860000000000099511', '拆分', 2, 'xslmes:mes_xsl_production_order:split', '1', '1', 0, 'admin', NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
@@ -67,7 +68,7 @@ SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW()
|
||||
FROM sys_permission p
|
||||
WHERE p.id IN (
|
||||
'1860000000000099511',
|
||||
'1860000000000099512', '1860000000000099513', '1860000000000099514', '1860000000000099515', '1860000000000099516'
|
||||
'1860000000000099512', '1860000000000099513', '1860000000000099514', '1860000000000099515', '1860000000000099516', '1860000000000099517'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
|
||||
64
jeecg-boot/db/mes-xsl-production-order-split-permission.sql
Normal file
64
jeecg-boot/db/mes-xsl-production-order-split-permission.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- 生产订单「拆分」按钮权限补丁
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 优先按组件路径定位“生产订单”菜单
|
||||
SET @prod_menu_id = (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE component = 'mes/productionorderinfo/index'
|
||||
AND menu_type = 1
|
||||
AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 兜底:按URL定位
|
||||
SET @prod_menu_id = IFNULL(@prod_menu_id, (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE url = '/mes/productionorderinfo'
|
||||
AND menu_type = 1
|
||||
AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
));
|
||||
|
||||
-- 再兜底:按名称定位
|
||||
SET @prod_menu_id = IFNULL(@prod_menu_id, (
|
||||
SELECT id
|
||||
FROM sys_permission
|
||||
WHERE name = '生产订单'
|
||||
AND menu_type = 1
|
||||
AND del_flag = 0
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
));
|
||||
|
||||
-- 若找不到页面菜单,回退到约定ID(你现有脚本中使用)
|
||||
SET @prod_menu_id = IFNULL(@prod_menu_id, '1860000000000099511');
|
||||
|
||||
-- 写入/修复“拆分”按钮权限
|
||||
INSERT INTO sys_permission (
|
||||
id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time
|
||||
) VALUES (
|
||||
'1860000000000099517', @prod_menu_id, '拆分', 2, 'xslmes:mes_xsl_production_order:split', '1', '1', 0, 'admin', NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
name = VALUES(name),
|
||||
menu_type = VALUES(menu_type),
|
||||
perms = VALUES(perms),
|
||||
perms_type = VALUES(perms_type),
|
||||
status = VALUES(status),
|
||||
del_flag = VALUES(del_flag);
|
||||
|
||||
-- 给admin角色授权
|
||||
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
|
||||
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', '1860000000000099517', NOW(), '127.0.0.1'
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_role_permission
|
||||
WHERE role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
|
||||
AND permission_id = '1860000000000099517'
|
||||
);
|
||||
@@ -466,3 +466,58 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTes
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
|
||||
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】我的租户下新增审批流设计,钉钉式可视化拖拽设计(先选单据再设计流程),本期实现设计器 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_111__mes_xsl_approval_flow.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalFlowMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalFlowService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalFlowServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowNode.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flow.less
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批运行时:全局悬浮按钮(选单据类型->选单据->发起),本期仅发起(生成审批实例+解析首节点处理人),不办理/不回写业务表 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_112__mes_xsl_approval_instance.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalInstanceMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalInstanceService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalInstanceServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
jeecgboot-vue3/src/layouts/default/index.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批悬浮按钮仅在配置了审批流的功能页显示:审批流定义增加route_path(功能页路由),前端按当前路由匹配后才显示按钮 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_113__mes_xsl_approval_flow_route.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】取消手填功能页路由:publishedList按单据表名自动反查sys_permission菜单url填入routePath,设计表单去掉路由字段 ---
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批支持列表多选联动:useListPage自动同步选中行到全局上下文,悬浮按钮发起弹窗自动带入选中单据并批量发起 ---
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/useApprovalSelection.ts
|
||||
jeecgboot-vue3/src/hooks/system/useListPage.ts
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批与IM聊天结合:IM新增系统单聊消息(绕过同部门校验),发起后把审批消息发给当前节点处理人 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】审批IM消息升级为可跳转业务卡片(biz_record):点击可定位到对应单据,无法定位功能页时退回纯文本 ---
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作标注。
|
||||
*
|
||||
* <p>把本注解打在业务 Controller 的处理方法上,即声明「该按钮/接口可被审批流程作为回调动作选择」。
|
||||
* 系统启动时会反射扫描所有带本注解的接口方法,取其 {@code @RequestMapping} 真实路径与 HTTP 方法,
|
||||
* 按 {@link #table()} 归类,供审批流设计器的节点「回调接口」下拉选择。</p>
|
||||
*
|
||||
* <p>因为基于 Spring 运行时反射 + 真实映射路径,生产环境天然可用,URL 绝对准确,无需任何源码解析。</p>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApprovalBizAction {
|
||||
|
||||
/** 动作展示名(如「批准」「反审核」「启用」),供设计器下拉显示 */
|
||||
String name();
|
||||
|
||||
/** 所属业务表名(如 mes_xsl_rubber_quick_test_std),与审批流绑定的 bizTable 对应 */
|
||||
String table();
|
||||
|
||||
/**
|
||||
* 适用触发时机,可多选;为空表示三种时机均可选用。
|
||||
* 取值:onNodeApprove(本节点通过) / onApprove(最终通过) / onReject(驳回)
|
||||
*/
|
||||
String[] phase() default {};
|
||||
|
||||
/** 排序,越小越靠前 */
|
||||
int order() default 0;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作注册表。
|
||||
*
|
||||
* <p>容器启动完成后,反射扫描所有带 {@link ApprovalBizAction} 的接口方法,
|
||||
* 取真实映射路径 + HTTP 方法,按业务表归类缓存,供设计器查询选择。</p>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalBizActionRegistry {
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/** 业务表 -> 动作列表 */
|
||||
private final Map<String, List<ApprovalBizActionVo>> byTable = new ConcurrentHashMap<>();
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void onContextRefreshed() {
|
||||
try {
|
||||
scan();
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批联动] 扫描 @ApprovalBizAction 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scan() {
|
||||
byTable.clear();
|
||||
RequestMappingHandlerMapping mapping;
|
||||
try {
|
||||
mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批联动] 未取到 RequestMappingHandlerMapping,跳过扫描", e);
|
||||
return;
|
||||
}
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
|
||||
HandlerMethod hm = entry.getValue();
|
||||
ApprovalBizAction ann = hm.getMethodAnnotation(ApprovalBizAction.class);
|
||||
if (ann == null || ann.table() == null || ann.table().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
RequestMappingInfo info = entry.getKey();
|
||||
String url = resolveUrl(info);
|
||||
if (url == null || url.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ApprovalBizActionVo vo = new ApprovalBizActionVo();
|
||||
vo.setName(ann.name());
|
||||
vo.setTable(ann.table());
|
||||
vo.setPhase(ann.phase());
|
||||
vo.setOrder(ann.order());
|
||||
vo.setUrl(url);
|
||||
vo.setMethod(resolveMethod(info));
|
||||
vo.setPerms(resolvePerms(hm));
|
||||
byTable.computeIfAbsent(ann.table(), k -> new ArrayList<>()).add(vo);
|
||||
}
|
||||
// 排序
|
||||
for (List<ApprovalBizActionVo> list : byTable.values()) {
|
||||
list.sort(Comparator.comparingInt(ApprovalBizActionVo::getOrder));
|
||||
}
|
||||
int total = byTable.values().stream().mapToInt(List::size).sum();
|
||||
log.info("[审批联动] 已注册业务动作 {} 个,覆盖 {} 张业务表", total, byTable.size());
|
||||
}
|
||||
|
||||
/** 取第一个映射路径(不含 context-path) */
|
||||
private String resolveUrl(RequestMappingInfo info) {
|
||||
Set<String> patterns = info.getPatternValues();
|
||||
if (patterns == null || patterns.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return patterns.iterator().next();
|
||||
}
|
||||
|
||||
/** 取 HTTP 方法,默认 POST */
|
||||
private String resolveMethod(RequestMappingInfo info) {
|
||||
RequestMethodsRequestCondition cond = info.getMethodsCondition();
|
||||
if (cond == null || cond.getMethods().isEmpty()) {
|
||||
return "POST";
|
||||
}
|
||||
return cond.getMethods().iterator().next().name();
|
||||
}
|
||||
|
||||
/** 取接口权限标识(@RequiresPermissions) */
|
||||
private String resolvePerms(HandlerMethod hm) {
|
||||
RequiresPermissions rp = hm.getMethodAnnotation(RequiresPermissions.class);
|
||||
if (rp != null && rp.value().length > 0) {
|
||||
return rp.value()[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按业务表取可选动作 */
|
||||
public List<ApprovalBizActionVo> getByTable(String table) {
|
||||
if (table == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> list = byTable.get(table);
|
||||
return list == null ? Collections.emptyList() : new ArrayList<>(list);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
|
||||
/**
|
||||
* 按业务表 + 时机取动作(如驳回统一执行 onReject 动作,无需在每个流程节点手动配置)。
|
||||
*/
|
||||
public List<ApprovalBizActionVo> getByTableAndPhase(String table, String phase) {
|
||||
if (table == null || phase == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> list = byTable.get(table);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> result = new ArrayList<>();
|
||||
for (ApprovalBizActionVo vo : list) {
|
||||
String[] phases = vo.getPhase();
|
||||
if (phases == null) {
|
||||
continue;
|
||||
}
|
||||
for (String p : phases) {
|
||||
if (phase.equals(p)) {
|
||||
result.add(vo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作(供设计器节点「回调接口」下拉选择)。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作VO
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalBizActionVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 动作名(如「批准」) */
|
||||
private String name;
|
||||
|
||||
/** 接口真实路径(取自 @RequestMapping,不含 context-path),如 /xslmes/xxx/updateStatus */
|
||||
private String url;
|
||||
|
||||
/** HTTP 方法:GET/POST/PUT/DELETE */
|
||||
private String method;
|
||||
|
||||
/** 所属业务表 */
|
||||
private String table;
|
||||
|
||||
/** 适用时机:onNodeApprove/onApprove/onReject,空表示均可 */
|
||||
private String[] phase;
|
||||
|
||||
/** 接口权限标识(取自 @RequiresPermissions,可空) */
|
||||
private String perms;
|
||||
|
||||
/** 排序 */
|
||||
private int order;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 审批动作领域事件。
|
||||
* 与 {@link IApprovalBizCallback} 等价的另一种接入方式:
|
||||
* 偏好松耦合的业务可使用 {@code @EventListener} 监听本事件(同步、同事务)。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
public class ApprovalActionEvent extends ApplicationEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final ApprovalCallbackContext context;
|
||||
|
||||
public ApprovalActionEvent(Object source, ApprovalCallbackContext context) {
|
||||
super(source);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ApprovalCallbackContext getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 审批节点「回调接口」HTTP 执行器。
|
||||
*
|
||||
* 审批到对应时机时,读取节点配置中录制好的业务接口(url+method),
|
||||
* 以「当前审批处理人」的登录态(透传当前请求的 X-Access-Token)内部调用该接口,
|
||||
* 自动带上单据ID(覆盖 id 参数),从而真实执行业务页面按钮背后的逻辑。
|
||||
*
|
||||
* 限制:自动流转 / 无人值守节点(无登录态请求上下文)时降级跳过并记录日志。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】节点回调接口内部调用执行
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalActionHttpExecutor {
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
private int serverPort;
|
||||
|
||||
@Value("${server.servlet.context-path:}")
|
||||
private String contextPath;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 执行某节点在指定时机配置的所有回调接口。
|
||||
*
|
||||
* @param node 流程节点 JSON(含 props.callbackActions)
|
||||
* @param phase 时机:onNodeApprove / onApprove / onReject
|
||||
* @param inst 审批实例
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
|
||||
/**
|
||||
* 判断节点在指定时机是否配置了回调接口(用于决定是否由业务回调全权负责回退)。
|
||||
*/
|
||||
public boolean hasActions(JSONObject node, String phase) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return false;
|
||||
}
|
||||
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
|
||||
if (callbackActions == null) {
|
||||
return false;
|
||||
}
|
||||
JSONArray actions = callbackActions.getJSONArray(phase);
|
||||
return actions != null && !actions.isEmpty();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
|
||||
|
||||
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
|
||||
if (node == null || inst == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
|
||||
if (callbackActions == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray actions = callbackActions.getJSONArray(phase);
|
||||
if (actions == null || actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
JSONObject action = actions.getJSONObject(i);
|
||||
if (action == null) {
|
||||
continue;
|
||||
}
|
||||
String url = action.getString("url");
|
||||
if (oConvertUtils.isEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
// 无登录态(自动流转/无人值守) -> 降级跳过
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, inst.getBizDataId());
|
||||
continue;
|
||||
}
|
||||
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
|
||||
invoke(method, url, action.getJSONObject("body"), inst.getBizDataId(), token);
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
/**
|
||||
* 直接按 url+method 调用业务接口(用于驳回统一回退,动作来自 @ApprovalBizAction 注解而非节点配置)。
|
||||
* 无当前处理人登录态时降级跳过。
|
||||
*/
|
||||
public void runByUrl(String method, String url, MesXslApprovalInstance inst) {
|
||||
if (oConvertUtils.isEmpty(url) || inst == null) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过驳回业务回退 url={}, bizId={}", url, inst.getBizDataId());
|
||||
return;
|
||||
}
|
||||
String httpMethod = oConvertUtils.getString(method, "POST").toUpperCase();
|
||||
invoke(httpMethod, url, null, inst.getBizDataId(), token);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
|
||||
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
|
||||
String fullUrl = buildFullUrl(url);
|
||||
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
|
||||
headers.add(HttpHeaders.AUTHORIZATION, token);
|
||||
|
||||
// 统一在 url 上附带单据ID的常见参数名,兼容 @RequestParam(id/ids/dataId) 形式的接口
|
||||
// (如密炼PS的 /proofread、/audit、/approve 用的是 @RequestParam ids)
|
||||
String idValue = oConvertUtils.getString(bizDataId, "");
|
||||
String sep = fullUrl.contains("?") ? "&" : "?";
|
||||
fullUrl = fullUrl + sep + "id=" + idValue + "&ids=" + idValue + "&dataId=" + idValue;
|
||||
|
||||
Object bodyToSend = null;
|
||||
if (!("GET".equals(method) || "DELETE".equals(method))) {
|
||||
// 写操作:合并录制的 body,并用单据ID覆盖 id(兼容 @RequestBody 形式)
|
||||
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
|
||||
body.put("id", bizDataId);
|
||||
bodyToSend = body;
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Object> entity = new HttpEntity<>(bodyToSend, headers);
|
||||
ResponseEntity<String> resp = restTemplate.exchange(fullUrl, httpMethod, entity, String.class);
|
||||
String respBody = resp.getBody();
|
||||
if (!resp.getStatusCode().is2xxSuccessful()) {
|
||||
throw new RuntimeException("回调接口返回非2xx:" + resp.getStatusCode());
|
||||
}
|
||||
// Jeecg 统一返回 {success:false,...} 视为业务失败
|
||||
if (oConvertUtils.isNotEmpty(respBody)) {
|
||||
try {
|
||||
JSONObject r = JSONObject.parseObject(respBody);
|
||||
if (r != null && r.containsKey("success") && Boolean.FALSE.equals(r.getBoolean("success"))) {
|
||||
throw new RuntimeException("回调接口业务失败:" + oConvertUtils.getString(r.getString("message"), "未知错误"));
|
||||
}
|
||||
} catch (RuntimeException re) {
|
||||
throw re;
|
||||
} catch (Exception ignore) {
|
||||
// 非JSON响应不强校验
|
||||
}
|
||||
}
|
||||
log.info("[审批回调] 已调用业务接口成功 {} {} bizId={}", method, fullUrl, bizDataId);
|
||||
} catch (RuntimeException e) {
|
||||
log.error("[审批回调] 调用业务接口失败 {} {} bizId={}", method, fullUrl, bizDataId, e);
|
||||
// 抛出以回滚整个审批动作,保证审批与业务一致
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建内部调用绝对地址:http://127.0.0.1:port + context-path + url */
|
||||
private String buildFullUrl(String url) {
|
||||
String ctx = oConvertUtils.getString(contextPath, "");
|
||||
if (oConvertUtils.isNotEmpty(ctx) && !ctx.startsWith("/")) {
|
||||
ctx = "/" + ctx;
|
||||
}
|
||||
String path = url.startsWith("/") ? url : "/" + url;
|
||||
// 录制到的路径已含 context-path 时不重复拼接
|
||||
if (oConvertUtils.isNotEmpty(ctx) && (path.equals(ctx) || path.startsWith(ctx + "/"))) {
|
||||
ctx = "";
|
||||
}
|
||||
return "http://127.0.0.1:" + serverPort + ctx + path;
|
||||
}
|
||||
|
||||
/** 取当前请求的登录 token(处理人身份) */
|
||||
private String currentToken() {
|
||||
try {
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = attrs.getRequest();
|
||||
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批回调上下文。
|
||||
* 由审批引擎在「节点通过 / 最终通过 / 驳回」时构建并传给业务回调,
|
||||
* 业务模块据此调用自身已有的审核/回写接口,实现审批与业务功能的统一联动。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ApprovalCallbackContext implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 回调动作类型 */
|
||||
public enum Action {
|
||||
/** 单个审批节点通过(中间态,每个节点都会触发一次) */
|
||||
NODE_APPROVED,
|
||||
/** 整个流程最终通过 */
|
||||
APPROVED,
|
||||
/** 被驳回(任一节点驳回即终止) */
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/** 回调动作 */
|
||||
private Action action;
|
||||
|
||||
/** 审批实例ID */
|
||||
private String instanceId;
|
||||
|
||||
/** 审批流定义ID */
|
||||
private String flowId;
|
||||
|
||||
/** 审批流名称 */
|
||||
private String flowName;
|
||||
|
||||
/** 业务单据表名 */
|
||||
private String bizTable;
|
||||
|
||||
/** 业务单据中文名 */
|
||||
private String bizTableName;
|
||||
|
||||
/** 业务单据记录ID(业务表主键) */
|
||||
private String bizDataId;
|
||||
|
||||
/** 业务单据展示标题 */
|
||||
private String bizTitle;
|
||||
|
||||
/** 当前/刚处理的节点ID */
|
||||
private String nodeId;
|
||||
|
||||
/** 当前/刚处理的节点名称 */
|
||||
private String nodeName;
|
||||
|
||||
/** 操作人 username(系统自动处理时为 null/system) */
|
||||
private String operatorUsername;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 审批意见 / 驳回理由 */
|
||||
private String comment;
|
||||
|
||||
/** 发起人 username */
|
||||
private String applyUser;
|
||||
|
||||
/** 是否为流程最终结束(APPROVED/REJECTED 时为 true) */
|
||||
private boolean finalResult;
|
||||
|
||||
/** 完整审批实例(供业务读取租户、发起信息等) */
|
||||
private transient MesXslApprovalInstance instance;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批业务回调分发器。
|
||||
* 统一在审批引擎流转关键点调用:
|
||||
* <ol>
|
||||
* <li>按业务表名路由到对应的 {@link IApprovalBizCallback} 实现(强类型,业务直接调用自身接口);</li>
|
||||
* <li>同时发布 {@link ApprovalActionEvent} 供 {@code @EventListener} 松耦合监听。</li>
|
||||
* </ol>
|
||||
* 回调与审批状态变更同事务执行,回调异常将向上抛出以回滚整个审批动作。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalCallbackDispatcher {
|
||||
|
||||
/** 监听所有业务表的通配符 */
|
||||
private static final String ANY_TABLE = "*";
|
||||
|
||||
private final ObjectProvider<List<IApprovalBizCallback>> callbacksProvider;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public ApprovalCallbackDispatcher(ObjectProvider<List<IApprovalBizCallback>> callbacksProvider,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
this.callbacksProvider = callbacksProvider;
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
/** 节点通过(中间态) */
|
||||
public void fireNodeApproved(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.NODE_APPROVED);
|
||||
ctx.setFinalResult(false);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
/** 流程最终通过 */
|
||||
public void fireApproved(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.APPROVED);
|
||||
ctx.setFinalResult(true);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
/** 驳回 */
|
||||
public void fireRejected(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.REJECTED);
|
||||
ctx.setFinalResult(true);
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
private void dispatch(ApprovalCallbackContext ctx) {
|
||||
if (ctx == null || oConvertUtils.isEmpty(ctx.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
// 1) 强类型回调:按表路由 + 通配
|
||||
for (IApprovalBizCallback cb : matchedCallbacks(ctx.getBizTable())) {
|
||||
invoke(cb, ctx);
|
||||
}
|
||||
// 2) 领域事件:松耦合监听(同步、同事务)
|
||||
try {
|
||||
eventPublisher.publishEvent(new ApprovalActionEvent(this, ctx));
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批领域事件处理失败 table={}, bizId={}, action={}", ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private List<IApprovalBizCallback> matchedCallbacks(String bizTable) {
|
||||
List<IApprovalBizCallback> all = callbacksProvider.getIfAvailable();
|
||||
List<IApprovalBizCallback> matched = new ArrayList<>();
|
||||
if (all == null) {
|
||||
return matched;
|
||||
}
|
||||
for (IApprovalBizCallback cb : all) {
|
||||
String support = cb.supportTable();
|
||||
if (ANY_TABLE.equals(support) || (support != null && support.equalsIgnoreCase(bizTable))) {
|
||||
matched.add(cb);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private void invoke(IApprovalBizCallback cb, ApprovalCallbackContext ctx) {
|
||||
try {
|
||||
switch (ctx.getAction()) {
|
||||
case NODE_APPROVED:
|
||||
cb.onNodeApproved(ctx);
|
||||
break;
|
||||
case APPROVED:
|
||||
cb.onApproved(ctx);
|
||||
break;
|
||||
case REJECTED:
|
||||
cb.onRejected(ctx);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批业务回调执行失败 callback={}, table={}, bizId={}, action={}",
|
||||
cb.getClass().getSimpleName(), ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
// 抛出以回滚整个审批动作,保证审批与业务数据一致
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
/**
|
||||
* 业务审批回调扩展点(SPI)。
|
||||
*
|
||||
* 各业务模块按需实现本接口并声明 {@link #supportTable()}(绑定的业务表名),
|
||||
* 审批引擎会在审批流转的关键节点自动回调对应实现,业务模块在回调里
|
||||
* 调用自己「已有的」审核/回写接口(如更新审核状态、扣减库存、生成下游单据等),
|
||||
* 从而把审批流程与业务单据的功能统一串联起来。
|
||||
*
|
||||
* <p>事务说明:回调与审批状态变更处于同一事务内,
|
||||
* 若回调抛出异常,整个审批动作回滚(审批失败),保证审批与业务数据一致。</p>
|
||||
*
|
||||
* <p>使用方式:实现类标注为 Spring Bean(@Component / @Service)即可被自动收集。</p>
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Component
|
||||
* public class RubberStdApprovalCallback implements IApprovalBizCallback {
|
||||
* @Override public String supportTable() { return "mes_xsl_rubber_quick_test_std"; }
|
||||
* @Override public void onApproved(ApprovalCallbackContext ctx) {
|
||||
* // 调用业务自身已有接口完成回写
|
||||
* stdService.lambdaUpdate()
|
||||
* .eq(MesXslRubberQuickTestStd::getId, ctx.getBizDataId())
|
||||
* .set(MesXslRubberQuickTestStd::getAuditStatus, "1").update();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调
|
||||
*/
|
||||
public interface IApprovalBizCallback {
|
||||
|
||||
/**
|
||||
* 绑定的业务表名(与审批流定义 bizTable 一致)。
|
||||
* 返回 "*" 表示监听所有业务表。
|
||||
*/
|
||||
String supportTable();
|
||||
|
||||
/**
|
||||
* 单个审批节点通过(中间态)。每经过一个审批节点通过都会触发一次。
|
||||
* 适合更新中间状态(如「审核中」「已校对」等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onNodeApproved(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 整个审批流程最终通过。适合执行终态业务(如置为「已批准」、生效、扣库存等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onApproved(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批被驳回(流程终止)。适合回退业务状态(如置回「草稿」、释放占用等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onRejected(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.jeecg.modules.xslmes.approval.callback.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||
import org.jeecg.modules.xslmes.approval.callback.IApprovalBizCallback;
|
||||
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 胶料快检实验标准 审批回调示例。
|
||||
* 演示如何把审批流转结果联动到业务单据已有功能:
|
||||
* 审批最终通过 -> 审核状态置「已批准」;驳回 -> 回退「草稿」。
|
||||
*
|
||||
* 业务在回调里直接调用自身的 service(此处用 lambdaUpdate 更新审核状态字段),
|
||||
* 与原有「批准/反审核」逻辑保持统一。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调-示例
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RubberQuickTestStdApprovalCallback implements IApprovalBizCallback {
|
||||
|
||||
private final IMesXslRubberQuickTestStdService stdService;
|
||||
|
||||
public RubberQuickTestStdApprovalCallback(IMesXslRubberQuickTestStdService stdService) {
|
||||
this.stdService = stdService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportTable() {
|
||||
return "mes_xsl_rubber_quick_test_std";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext ctx) {
|
||||
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_APPROVED);
|
||||
log.info("[审批联动] 实验标准 {} 审批通过,审核状态置为已批准", ctx.getBizDataId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext ctx) {
|
||||
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT);
|
||||
log.info("[审批联动] 实验标准 {} 被驳回,审核状态回退为草稿", ctx.getBizDataId());
|
||||
}
|
||||
|
||||
private void updateAuditStatus(String bizDataId, String auditStatus) {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
stdService.lambdaUpdate()
|
||||
.eq(MesXslRubberQuickTestStd::getId, bizDataId)
|
||||
.set(MesXslRubberQuickTestStd::getAuditStatus, auditStatus)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.DictModel;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* MES 审批流设计
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Tag(name = "MES审批流设计")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalFlow")
|
||||
@Slf4j
|
||||
public class MesXslApprovalFlowController extends JeecgController<MesXslApprovalFlow, IMesXslApprovalFlowService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService mesXslApprovalFlowService;
|
||||
|
||||
@Autowired
|
||||
private ISysBaseAPI sysBaseAPI;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
@Autowired
|
||||
private ApprovalBizActionRegistry approvalBizActionRegistry;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
/**
|
||||
* 审批阶段关键字配置(有序):key=阶段标识,name=阶段中文,nodeType=对应节点类型,keywords=列注释匹配关键字。
|
||||
* 解析顺序即默认流程顺序:校对 -> 审核 -> 审批 -> 分发 -> 抄送。
|
||||
*/
|
||||
private static final String[][] STAGE_DEFS = new String[][]{
|
||||
{"proofread", "校对", "approver", "校对"},
|
||||
{"review", "审核", "approver", "审核|审查"},
|
||||
{"approve", "审批", "approver", "审批|批准|核准"},
|
||||
{"distribute", "分发", "approver", "分发|发放"},
|
||||
{"cc", "抄送", "cc", "抄送"},
|
||||
};
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
|
||||
/**
|
||||
* 根据所选单据表名翻译字典,回填单据中文名
|
||||
*/
|
||||
private void fillBizTableName(MesXslApprovalFlow flow) {
|
||||
if (oConvertUtils.isEmpty(flow.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
|
||||
if (items != null) {
|
||||
for (DictModel item : items) {
|
||||
if (flow.getBizTable().equals(item.getValue())) {
|
||||
flow.setBizTableName(item.getText());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "审批流设计-分页列表查询")
|
||||
@RequiresPermissions("approval:flow:list")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslApprovalFlow>> queryPageList(
|
||||
MesXslApprovalFlow mesXslApprovalFlow,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslApprovalFlow> queryWrapper = QueryGenerator.initQueryWrapper(mesXslApprovalFlow, req.getParameterMap());
|
||||
// 列表查询不返回大字段 flowConfig,避免传输冗余
|
||||
queryWrapper.select(MesXslApprovalFlow.class, info -> !"flow_config".equals(info.getColumn()));
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
Page<MesXslApprovalFlow> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslApprovalFlow> pageList = mesXslApprovalFlowService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-添加")
|
||||
@Operation(summary = "审批流设计-添加")
|
||||
@RequiresPermissions("approval:flow:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getFlowName())) {
|
||||
return Result.error("审批流名称不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getBizTable())) {
|
||||
return Result.error("请先选择绑定单据");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getStatus())) {
|
||||
mesXslApprovalFlow.setStatus("0");
|
||||
}
|
||||
fillBizTableName(mesXslApprovalFlow);
|
||||
mesXslApprovalFlowService.save(mesXslApprovalFlow);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-编辑")
|
||||
@Operation(summary = "审批流设计-编辑")
|
||||
@RequiresPermissions("approval:flow:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
fillBizTableName(mesXslApprovalFlow);
|
||||
mesXslApprovalFlowService.updateById(mesXslApprovalFlow);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-保存流程设计")
|
||||
@Operation(summary = "审批流设计-保存流程设计")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@PostMapping(value = "/saveDesign")
|
||||
public Result<String> saveDesign(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
|
||||
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
MesXslApprovalFlow update = new MesXslApprovalFlow();
|
||||
update.setId(mesXslApprovalFlow.getId());
|
||||
update.setFlowConfig(mesXslApprovalFlow.getFlowConfig());
|
||||
// 设计保存时若仍为草稿则置为已发布
|
||||
if (oConvertUtils.isNotEmpty(mesXslApprovalFlow.getStatus())) {
|
||||
update.setStatus(mesXslApprovalFlow.getStatus());
|
||||
}
|
||||
mesXslApprovalFlowService.updateById(update);
|
||||
return Result.OK("流程设计已保存!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-发布/停用")
|
||||
@Operation(summary = "审批流设计-发布/停用")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@PostMapping(value = "/updateStatus")
|
||||
public Result<String> updateStatus(
|
||||
@RequestParam(name = "id") String id,
|
||||
@RequestParam(name = "status") String status) {
|
||||
if (!"0".equals(status) && !"1".equals(status) && !"2".equals(status)) {
|
||||
return Result.error("状态参数非法");
|
||||
}
|
||||
boolean ok = mesXslApprovalFlowService.lambdaUpdate()
|
||||
.eq(MesXslApprovalFlow::getId, id)
|
||||
.set(MesXslApprovalFlow::getStatus, status)
|
||||
.update();
|
||||
return ok ? Result.OK("操作成功") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-删除")
|
||||
@Operation(summary = "审批流设计-删除")
|
||||
@RequiresPermissions("approval:flow:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
mesXslApprovalFlowService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "审批流设计-批量删除")
|
||||
@Operation(summary = "审批流设计-批量删除")
|
||||
@RequiresPermissions("approval:flow:delete")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids") String ids) {
|
||||
mesXslApprovalFlowService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "审批流设计-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslApprovalFlow> queryById(@RequestParam(name = "id") String id) {
|
||||
MesXslApprovalFlow entity = mesXslApprovalFlowService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
|
||||
/**
|
||||
* 设计上下文:供全局"审批流程设计"悬浮按钮调用。
|
||||
* 1) 根据当前功能页路由反查绑定的业务表;
|
||||
* 2) 解析该表的字段,识别"校对/审核/审批/分发/抄送"等阶段字段(不存在不报错,存在即解析);
|
||||
* 3) 取/建该业务表的草稿审批流,返回流程记录(含id)供直接进入设计器。
|
||||
*
|
||||
* @param routePath 当前功能页前端路由(如 /xslmes/mesXslFormulaSpec/MesXslFormulaSpecList)
|
||||
*/
|
||||
@Operation(summary = "审批流设计-当前页设计上下文")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/designContext")
|
||||
public Result<Map<String, Object>> designContext(@RequestParam(name = "routePath") String routePath) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("routePath", routePath);
|
||||
data.put("bizTable", null);
|
||||
data.put("bizTableName", null);
|
||||
data.put("stages", new ArrayList<>());
|
||||
data.put("flow", null);
|
||||
|
||||
// 1) 路由 -> 业务表名(无法反查时不报错,返回空上下文,前端提示)
|
||||
String table = resolveTableByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(table) || !tableExists(table)) {
|
||||
return Result.OK(data);
|
||||
}
|
||||
data.put("bizTable", table);
|
||||
|
||||
// 2) 业务表中文名:优先字典,其次表注释
|
||||
String bizTableName = resolveBizTableName(table);
|
||||
data.put("bizTableName", bizTableName);
|
||||
|
||||
// 3) 解析阶段字段
|
||||
data.put("stages", parseStageFields(table));
|
||||
|
||||
// 4) 取/建草稿审批流
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
MesXslApprovalFlow flow = findFlowByTable(table, tenantId);
|
||||
if (flow == null) {
|
||||
flow = new MesXslApprovalFlow();
|
||||
flow.setFlowName((oConvertUtils.isNotEmpty(bizTableName) ? bizTableName : table) + "审批流");
|
||||
flow.setBizTable(table);
|
||||
flow.setBizTableName(bizTableName);
|
||||
flow.setRoutePath(routePath);
|
||||
flow.setStatus("0");
|
||||
if (tenantId != null) {
|
||||
flow.setTenantId(tenantId);
|
||||
}
|
||||
mesXslApprovalFlowService.save(flow);
|
||||
} else if (oConvertUtils.isEmpty(flow.getRoutePath())) {
|
||||
// 历史数据未记录路由,回填一次便于后续发起按钮匹配
|
||||
mesXslApprovalFlowService.lambdaUpdate()
|
||||
.eq(MesXslApprovalFlow::getId, flow.getId())
|
||||
.set(MesXslApprovalFlow::getRoutePath, routePath)
|
||||
.update();
|
||||
flow.setRoutePath(routePath);
|
||||
}
|
||||
data.put("flow", flow);
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供设计器节点「回调接口」下拉选择。
|
||||
*
|
||||
* @param table 业务表名(审批流绑定的 bizTable)
|
||||
*/
|
||||
@Operation(summary = "审批流设计-业务表可选回调动作")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/bizActions")
|
||||
public Result<List<ApprovalBizActionVo>> bizActions(@RequestParam(name = "table") String table) {
|
||||
return Result.OK(approvalBizActionRegistry.getByTable(table));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
|
||||
/**
|
||||
* 根据前端路由反查业务表名。
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List,sys_permission.component 形如
|
||||
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
|
||||
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
|
||||
*/
|
||||
/** 根据前端路由反查页面组件路径(sys_permission.component),用于前端按 key 取该页面按钮接口映射 */
|
||||
private String resolveComponentByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
String path = routePath.trim().replaceAll("/+$", "");
|
||||
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
try {
|
||||
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
} catch (Exception e) {
|
||||
log.warn("反查菜单组件失败 routePath={}", routePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveTableByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
String component = resolveComponentByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(component)) {
|
||||
return null;
|
||||
}
|
||||
// 取组件路径末段:xslmes/mesXslFormulaSpec/MesXslFormulaSpecList -> MesXslFormulaSpecList
|
||||
String comp = component.contains("/") ? component.substring(component.lastIndexOf('/') + 1) : component;
|
||||
if (comp.endsWith("List")) {
|
||||
comp = comp.substring(0, comp.length() - "List".length());
|
||||
}
|
||||
return camelToUnderline(comp);
|
||||
}
|
||||
|
||||
/** 驼峰转下划线小写:MesXslFormulaSpec -> mes_xsl_formula_spec */
|
||||
private String camelToUnderline(String camel) {
|
||||
if (oConvertUtils.isEmpty(camel)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < camel.length(); i++) {
|
||||
char c = camel.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
sb.append('_');
|
||||
}
|
||||
sb.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 校验表是否存在于当前库 */
|
||||
private boolean tableExists(String table) {
|
||||
if (!IDENTIFIER.matcher(table).matches()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
|
||||
Integer.class, table);
|
||||
return cnt != null && cnt > 0;
|
||||
} catch (Exception e) {
|
||||
log.warn("校验表存在失败 table={}", table, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务表中文名:优先字典 mes_xsl_approval_biz_doc,其次表注释 */
|
||||
private String resolveBizTableName(String table) {
|
||||
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
|
||||
if (items != null) {
|
||||
for (DictModel item : items) {
|
||||
if (table.equals(item.getValue())) {
|
||||
return item.getText();
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
List<String> comments = jdbcTemplate.queryForList(
|
||||
"SELECT table_comment FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
|
||||
String.class, table);
|
||||
if (!comments.isEmpty() && oConvertUtils.isNotEmpty(comments.get(0))) {
|
||||
return comments.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询表注释失败 table={}", table, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表字段,识别审批阶段字段。每个阶段最多取一个字段(优先列注释含"人/员"的人员字段)。
|
||||
* 返回有序列表:[{stageKey, stageName, nodeType, field, fieldComment}]
|
||||
*/
|
||||
private List<Map<String, Object>> parseStageFields(String table) {
|
||||
List<Map<String, Object>> stages = new ArrayList<>();
|
||||
List<Map<String, Object>> columns;
|
||||
try {
|
||||
columns = jdbcTemplate.queryForList(
|
||||
"SELECT column_name AS name, column_comment AS comment FROM information_schema.columns "
|
||||
+ "WHERE table_schema = (SELECT DATABASE()) AND table_name = ? ORDER BY ordinal_position",
|
||||
table);
|
||||
} catch (Exception e) {
|
||||
log.warn("查询表字段失败 table={}", table, e);
|
||||
return stages;
|
||||
}
|
||||
for (String[] def : STAGE_DEFS) {
|
||||
String stageKey = def[0];
|
||||
String stageName = def[1];
|
||||
String nodeType = def[2];
|
||||
String[] keywords = def[3].split("\\|");
|
||||
Map<String, Object> hit = matchStageColumn(columns, keywords);
|
||||
if (hit != null) {
|
||||
Map<String, Object> stage = new LinkedHashMap<>();
|
||||
stage.put("stageKey", stageKey);
|
||||
stage.put("stageName", stageName);
|
||||
stage.put("nodeType", nodeType);
|
||||
stage.put("field", hit.get("name"));
|
||||
stage.put("fieldComment", hit.get("comment"));
|
||||
stages.add(stage);
|
||||
}
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
|
||||
/** 在列集合中按关键字匹配阶段字段,优先返回注释含"人/员"的人员字段 */
|
||||
private Map<String, Object> matchStageColumn(List<Map<String, Object>> columns, String[] keywords) {
|
||||
Map<String, Object> firstMatch = null;
|
||||
for (Map<String, Object> col : columns) {
|
||||
String comment = col.get("comment") == null ? "" : String.valueOf(col.get("comment"));
|
||||
if (oConvertUtils.isEmpty(comment)) {
|
||||
continue;
|
||||
}
|
||||
boolean matched = false;
|
||||
for (String kw : keywords) {
|
||||
if (comment.contains(kw)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
continue;
|
||||
}
|
||||
if (firstMatch == null) {
|
||||
firstMatch = col;
|
||||
}
|
||||
// 人员字段优先(如"校对人""审核员")
|
||||
if (comment.contains("人") || comment.contains("员")) {
|
||||
return col;
|
||||
}
|
||||
}
|
||||
return firstMatch;
|
||||
}
|
||||
|
||||
/** 按业务表+租户查找审批流(取最近一条) */
|
||||
private MesXslApprovalFlow findFlowByTable(String table, Integer tenantId) {
|
||||
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
|
||||
qw.eq("biz_table", table);
|
||||
if (tenantId != null) {
|
||||
qw.eq("tenant_id", tenantId);
|
||||
}
|
||||
qw.orderByDesc("create_time");
|
||||
qw.last("LIMIT 1");
|
||||
return mesXslApprovalFlowService.getOne(qw, false);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 审批办理(运行时-流转)。
|
||||
* 供 IM 审批卡片按钮调用:查看详情 / 审批通过 / 驳回。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
@Tag(name = "MES审批办理")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalHandle")
|
||||
@Slf4j
|
||||
public class MesXslApprovalHandleController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
|
||||
@Operation(summary = "审批办理-单据详情")
|
||||
@GetMapping("/detail")
|
||||
public Result<Map<String, Object>> detail(@RequestParam("instanceId") String instanceId) {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
Map<String, Object> data = approvalHandleService.detail(instanceId, user);
|
||||
if (data == null || data.isEmpty()) {
|
||||
return Result.error("审批实例不存在");
|
||||
}
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-实时状态(卡片置灰判断)")
|
||||
@GetMapping("/status")
|
||||
public Result<Map<String, Object>> status(@RequestParam("instanceId") String instanceId) {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
Map<String, Object> data = approvalHandleService.statusInfo(instanceId, user);
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-通过")
|
||||
@PostMapping("/approve")
|
||||
public Result<String> approve(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String comment = body.get("comment") == null ? null : String.valueOf(body.get("comment"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.approve(instanceId, comment, user);
|
||||
}
|
||||
|
||||
@Operation(summary = "审批办理-驳回")
|
||||
@PostMapping("/reject")
|
||||
public Result<String> reject(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.reject(instanceId, reason, user);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
@Operation(summary = "审批办理-撤销(发起人撤回)")
|
||||
@PostMapping("/cancel")
|
||||
public Result<String> cancel(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.cancel(instanceId, reason, user);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
@Operation(summary = "审批办理-催办(发起人催处理人)")
|
||||
@PostMapping("/urge")
|
||||
public Result<String> urge(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.urge(instanceId, user);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
@Operation(summary = "审批办理-我的待办列表")
|
||||
@GetMapping("/pendingList")
|
||||
public Result<List<Map<String, Object>>> pendingList() {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return Result.OK(approvalHandleService.pendingList(user));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* MES 审批发起(运行时-本期仅发起)
|
||||
* 供全局"发起审批"悬浮按钮调用:选单据类型 -> 选单据记录 -> 发起。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Tag(name = "MES审批发起")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalLaunch")
|
||||
@Slf4j
|
||||
public class MesXslApprovalLaunchController {
|
||||
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService approvalFlowService;
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalInstanceService approvalInstanceService;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(按租户隔离),即"可发起的单据类型"。
|
||||
* 同时按"功能模块(单据表)"自动反查其菜单路由填入 routePath,供前端控制悬浮按钮仅在该功能页显示,无需手工配置。
|
||||
*/
|
||||
@Operation(summary = "发起审批-已发布审批流列表")
|
||||
@GetMapping("/publishedList")
|
||||
public Result<List<MesXslApprovalFlow>> publishedList() {
|
||||
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
|
||||
qw.eq("status", "1");
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
if (tenantId != null) {
|
||||
qw.eq("tenant_id", tenantId);
|
||||
}
|
||||
qw.orderByDesc("create_time");
|
||||
List<MesXslApprovalFlow> list = approvalFlowService.list(qw);
|
||||
// 未手工指定 route_path 时,按单据表名自动反查菜单路由
|
||||
for (MesXslApprovalFlow flow : list) {
|
||||
if (oConvertUtils.isEmpty(flow.getRoutePath())) {
|
||||
flow.setRoutePath(resolveRoutePathByTable(flow.getBizTable()));
|
||||
}
|
||||
}
|
||||
return Result.OK(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据单据表名反查对应功能菜单的前端路由。
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List(如 mes_xsl_formula_spec -> MesXslFormulaSpecList),
|
||||
* 对应 sys_permission.component 形如 xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,取其 url 即路由。
|
||||
*/
|
||||
private String resolveRoutePathByTable(String table) {
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder comp = new StringBuilder();
|
||||
for (String p : table.split("_")) {
|
||||
if (oConvertUtils.isEmpty(p)) {
|
||||
continue;
|
||||
}
|
||||
comp.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
|
||||
}
|
||||
comp.append("List");
|
||||
String sql = "SELECT url FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) "
|
||||
+ "AND (component LIKE ? OR component LIKE ?) "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
try {
|
||||
List<String> urls = jdbcTemplate.queryForList(sql, String.class, "%/" + comp, "%" + comp);
|
||||
return urls.isEmpty() ? null : urls.get(0);
|
||||
} catch (Exception e) {
|
||||
log.warn("反查菜单路由失败 table={}", table, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据审批流绑定的单据表,查询业务单据记录(id + 标题),供发起时选择
|
||||
*/
|
||||
@Operation(summary = "发起审批-业务单据记录列表")
|
||||
@GetMapping("/bizRecords")
|
||||
public Result<List<Map<String, Object>>> bizRecords(
|
||||
@RequestParam("flowId") String flowId,
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
String table = flow.getBizTable();
|
||||
String titleField = flow.getTitleField();
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return Result.error("单据表名非法");
|
||||
}
|
||||
boolean hasTitle = oConvertUtils.isNotEmpty(titleField) && IDENTIFIER.matcher(titleField).matches();
|
||||
|
||||
StringBuilder sql = new StringBuilder("SELECT id, ");
|
||||
sql.append(hasTitle ? titleField : "id").append(" AS title FROM ").append(table);
|
||||
List<Object> args = new ArrayList<>();
|
||||
if (hasTitle && oConvertUtils.isNotEmpty(keyword)) {
|
||||
sql.append(" WHERE ").append(titleField).append(" LIKE CONCAT('%', ?, '%')");
|
||||
args.add(keyword);
|
||||
}
|
||||
// 主键一定存在,按 id 倒序近似最新;限制条数防全表
|
||||
sql.append(" ORDER BY id DESC LIMIT 100");
|
||||
try {
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), args.toArray());
|
||||
return Result.OK(list);
|
||||
} catch (Exception e) {
|
||||
log.error("查询业务单据失败 table={}, field={}", table, titleField, e);
|
||||
return Result.error("查询业务单据失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起审批:根据审批流定义创建审批实例,解析首个审批节点处理人
|
||||
*/
|
||||
@Operation(summary = "发起审批-发起")
|
||||
@PostMapping("/launch")
|
||||
public Result<String> launch(@RequestBody Map<String, Object> body) {
|
||||
String flowId = (String) body.get("flowId");
|
||||
String bizDataId = (String) body.get("bizDataId");
|
||||
String bizTitle = (String) body.get("bizTitle");
|
||||
if (oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("请选择审批流和单据");
|
||||
}
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
if (!"1".equals(flow.getStatus())) {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
return Result.error("该单据已有审批中的流程,请勿重复发起");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
return Result.OK("发起成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发起审批:用于列表多选后一次性发起
|
||||
*/
|
||||
@Operation(summary = "发起审批-批量发起")
|
||||
@PostMapping("/launchBatch")
|
||||
public Result<String> launchBatch(@RequestBody Map<String, Object> body) {
|
||||
String flowId = (String) body.get("flowId");
|
||||
Object itemsObj = body.get("items");
|
||||
if (oConvertUtils.isEmpty(flowId) || !(itemsObj instanceof List) || ((List<?>) itemsObj).isEmpty()) {
|
||||
return Result.error("请选择审批流和单据");
|
||||
}
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
if (!"1".equals(flow.getStatus())) {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
int count = 0;
|
||||
int skipCount = 0;
|
||||
for (Object o : (List<?>) itemsObj) {
|
||||
if (!(o instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<?, ?> item = (Map<?, ?>) o;
|
||||
String bizDataId = item.get("bizDataId") == null ? null : String.valueOf(item.get("bizDataId"));
|
||||
String bizTitle = item.get("bizTitle") == null ? null : String.valueOf(item.get("bizTitle"));
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
return Result.error(skipCount > 0 ? "所选单据均已有审批中的流程,无需重复发起" : "没有有效的单据数据");
|
||||
}
|
||||
String msg = "已发起 " + count + " 条审批!";
|
||||
if (skipCount > 0) {
|
||||
msg += "(" + skipCount + " 条已有审批中流程,已跳过)";
|
||||
}
|
||||
return Result.OK(msg);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一条审批实例(基础字段;处理人解析/卡片由流转引擎完成)
|
||||
*/
|
||||
private MesXslApprovalInstance buildInstance(MesXslApprovalFlow flow, String bizDataId, String bizTitle, LoginUser loginUser) {
|
||||
MesXslApprovalInstance inst = new MesXslApprovalInstance();
|
||||
inst.setFlowId(flow.getId());
|
||||
inst.setFlowName(flow.getFlowName());
|
||||
inst.setBizTable(flow.getBizTable());
|
||||
inst.setBizTableName(flow.getBizTableName());
|
||||
inst.setBizDataId(bizDataId);
|
||||
inst.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
inst.setStatus("0");
|
||||
if (loginUser != null) {
|
||||
inst.setApplyUser(loginUser.getUsername());
|
||||
inst.setApplyUserName(loginUser.getRealname());
|
||||
}
|
||||
inst.setApplyTime(new Date());
|
||||
inst.setTenantId(MesXslTenantUtils.resolveTenantId(flow.getTenantId()));
|
||||
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
|
||||
return inst;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.jeecg.modules.xslmes.approval.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* MES 审批流定义
|
||||
* 钉钉式可视化审批流设计,绑定 MES 业务单据。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_flow")
|
||||
@Schema(description = "MES审批流定义")
|
||||
public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "绑定单据表名")
|
||||
@Dict(dicCode = "mes_xsl_approval_biz_doc")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "绑定单据中文名(冗余展示)")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "单据标题字段名(发起选单据时展示)")
|
||||
private String titleField;
|
||||
|
||||
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
|
||||
private String routePath;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
@Schema(description = "业务单据状态字段名(驳回/撤销时回写其发起时原值)")
|
||||
private String statusField;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
|
||||
@Schema(description = "超时提醒小时数(0=不提醒),默认24")
|
||||
private Integer timeoutHours;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
|
||||
|
||||
@Schema(description = "流程设计JSON(钉钉式节点树)")
|
||||
private String flowConfig;
|
||||
|
||||
@Schema(description = "状态:0草稿 1已发布 2已停用")
|
||||
@Dict(dicCode = "mes_xsl_approval_flow_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Double sortNo;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.jeecg.modules.xslmes.approval.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MES 审批实例(本期仅记录发起,不含办理)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_instance")
|
||||
@Schema(description = "MES审批实例")
|
||||
public class MesXslApprovalInstance extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审批流定义ID")
|
||||
private String flowId;
|
||||
|
||||
@Schema(description = "审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据中文名")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "业务单据展示标题")
|
||||
private String bizTitle;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
@Schema(description = "业务单据状态字段名(发起时快照)")
|
||||
private String statusField;
|
||||
|
||||
@Schema(description = "发起审批时业务状态原值(驳回/撤销回写)")
|
||||
private String originStatus;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
|
||||
@Schema(description = "当前节点ID")
|
||||
private String currentNodeId;
|
||||
|
||||
@Schema(description = "当前节点名称")
|
||||
private String currentNodeName;
|
||||
|
||||
@Schema(description = "当前处理人(username逗号分隔)")
|
||||
private String currentHandlers;
|
||||
|
||||
@Schema(description = "当前处理人展示文本")
|
||||
private String currentHandlersText;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
|
||||
@Schema(description = "当前节点处理进度JSON(nodeId/mode/tasks)")
|
||||
private String nodeProgress;
|
||||
|
||||
@Schema(description = "审批历史JSON数组")
|
||||
private String history;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
|
||||
|
||||
@Schema(description = "状态:0审批中 1已通过 2已驳回 3已撤销")
|
||||
@Dict(dicCode = "mes_xsl_approval_instance_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "发起人username")
|
||||
private String applyUser;
|
||||
|
||||
@Schema(description = "发起人姓名")
|
||||
private String applyUserName;
|
||||
|
||||
@Schema(description = "发起时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date applyTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
|
||||
@com.baomidou.mybatisplus.annotation.Version
|
||||
@Schema(description = "乐观锁版本号")
|
||||
private Integer version;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Mapper
|
||||
public interface MesXslApprovalFlowMapper extends BaseMapper<MesXslApprovalFlow> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
/**
|
||||
* MES 审批实例 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Mapper
|
||||
public interface MesXslApprovalInstanceMapper extends BaseMapper<MesXslApprovalInstance> {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.xslmes.approval.scheduler;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 审批超时提醒定时任务。
|
||||
* 每小时扫描一次所有审批中的实例,对超过各流程配置 timeout_hours 仍未处理的节点
|
||||
* 向当前处理人发送 IM 提醒,同一实例在同一超时周期内只提醒一次。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流完善】超时提醒调度器
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MesXslApprovalTimeoutScheduler {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
|
||||
/**
|
||||
* 每小时整点执行:扫描超时未处理的审批实例并发送提醒。
|
||||
* 使用 fixedDelay 避免并发执行;应用启动 5 分钟后开始首次执行。
|
||||
*/
|
||||
@Scheduled(initialDelayString = "PT5M", fixedDelayString = "PT1H")
|
||||
public void remindTimeout() {
|
||||
log.info("开始扫描审批超时实例...");
|
||||
try {
|
||||
approvalHandleService.remindTimeoutInstances();
|
||||
} catch (Exception e) {
|
||||
log.warn("审批超时扫描异常", e);
|
||||
}
|
||||
log.info("审批超时实例扫描完成");
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
public interface IMesXslApprovalFlowService extends IService<MesXslApprovalFlow> {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MES 审批办理/流转引擎。
|
||||
* 负责:发起后进入首节点、审批通过/驳回的流转推进(支持会签/或签/依次)、查看单据全字段详情。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
public interface IMesXslApprovalHandleService {
|
||||
|
||||
/**
|
||||
* 发起后进入首个审批节点:解析处理人、初始化节点进度、发送审批卡片。
|
||||
* 实例需已先行 save(带 id)。
|
||||
*/
|
||||
void enterFirstNode(MesXslApprovalInstance inst, LoginUser applyUser);
|
||||
|
||||
/**
|
||||
* 审批通过:标记当前处理人任务完成,按节点 multiMode 判断是否流转到下一节点。
|
||||
*/
|
||||
Result<String> approve(String instanceId, String comment, LoginUser user);
|
||||
|
||||
/**
|
||||
* 驳回:任一处理人驳回即终止流程,通知发起人。
|
||||
*/
|
||||
Result<String> reject(String instanceId, String reason, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
/**
|
||||
* 撤销:仅发起人本人在审批中可撤回,流程终止并将业务单据恢复到发起时状态。
|
||||
*/
|
||||
Result<String> cancel(String instanceId, String reason, LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
/**
|
||||
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
|
||||
*/
|
||||
Map<String, Object> detail(String instanceId, LoginUser user);
|
||||
|
||||
/**
|
||||
* 轻量状态查询:供 IM 卡片实时判断是否仍可办理(旧节点卡片置灰)。
|
||||
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
|
||||
*/
|
||||
Map<String, Object> statusInfo(String instanceId, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
/**
|
||||
* 催办:发起人向当前处理人发送一次性催办提醒,同一实例每小时最多催一次。
|
||||
*/
|
||||
Result<String> urge(String instanceId, LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
|
||||
/**
|
||||
* 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。
|
||||
*/
|
||||
List<Map<String, Object>> pendingList(LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒(调度器调用)-----
|
||||
/**
|
||||
* 扫描超时未处理的审批实例并向当前处理人发送提醒(由定时任务调用)。
|
||||
*/
|
||||
void remindTimeoutInstances();
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒(调度器调用)-----
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
/**
|
||||
* MES 审批实例 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
public interface IMesXslApprovalInstanceService extends IService<MesXslApprovalInstance> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalFlowMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批流定义 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalFlowServiceImpl extends ServiceImpl<MesXslApprovalFlowMapper, MesXslApprovalFlow> implements IMesXslApprovalFlowService {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalInstanceMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批实例 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalInstanceServiceImpl extends ServiceImpl<MesXslApprovalInstanceMapper, MesXslApprovalInstance> implements IMesXslApprovalInstanceService {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 启用 Spring @Scheduled,用于审批超时提醒等定时任务。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流完善】启用定时任务支持
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】启用定时任务支持-----
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class XslMesSchedulingConfig {
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】启用定时任务支持-----
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslFinalBatchPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@Tag(name = "终胶计划")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslFinalBatchPlan")
|
||||
public class MesXslFinalBatchPlanController
|
||||
extends JeecgController<MesXslFinalBatchPlan, IMesXslFinalBatchPlanService> {
|
||||
|
||||
@Autowired private IMesXslFinalBatchPlanService finalBatchPlanService;
|
||||
|
||||
@Operation(summary = "终胶计划-分页列表查询")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslFinalBatchPlan>> queryPageList(
|
||||
MesXslFinalBatchPlan model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslFinalBatchPlan> queryWrapper = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
IPage<MesXslFinalBatchPlan> pageList = finalBatchPlanService.page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "终胶计划-添加")
|
||||
@Operation(summary = "终胶计划-添加")
|
||||
@RequiresPermissions("xslmes:mes_xsl_final_batch_plan:add")
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody MesXslFinalBatchPlan model) {
|
||||
fillDerivedFields(model);
|
||||
finalBatchPlanService.save(model);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "终胶计划-编辑")
|
||||
@Operation(summary = "终胶计划-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_final_batch_plan:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslFinalBatchPlan model) {
|
||||
fillDerivedFields(model);
|
||||
finalBatchPlanService.updateById(model);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "终胶计划-通过id删除")
|
||||
@Operation(summary = "终胶计划-通过id删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_final_batch_plan:delete")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
finalBatchPlanService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "终胶计划-批量删除")
|
||||
@Operation(summary = "终胶计划-批量删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_final_batch_plan:deleteBatch")
|
||||
@DeleteMapping("/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
finalBatchPlanService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "终胶计划-通过id查询")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslFinalBatchPlan> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslFinalBatchPlan entity = finalBatchPlanService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_final_batch_plan:exportXls")
|
||||
@RequestMapping("/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslFinalBatchPlan model) {
|
||||
return super.exportXls(request, model, MesXslFinalBatchPlan.class, "终胶计划");
|
||||
}
|
||||
|
||||
private void fillDerivedFields(MesXslFinalBatchPlan model) {
|
||||
BigDecimal planWeight = model.getPlanWeight();
|
||||
BigDecimal perCarWeight = model.getPerCarWeight();
|
||||
if (planWeight == null || perCarWeight == null || perCarWeight.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
model.setPlannedCarCount(0);
|
||||
return;
|
||||
}
|
||||
model.setPlannedCarCount(planWeight.divide(perCarWeight, 0, RoundingMode.CEILING).intValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMasterBatchPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@Tag(name = "母胶计划")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslMasterBatchPlan")
|
||||
public class MesXslMasterBatchPlanController
|
||||
extends JeecgController<MesXslMasterBatchPlan, IMesXslMasterBatchPlanService> {
|
||||
|
||||
@Autowired private IMesXslMasterBatchPlanService masterBatchPlanService;
|
||||
|
||||
@Operation(summary = "母胶计划-分页列表查询")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslMasterBatchPlan>> queryPageList(
|
||||
MesXslMasterBatchPlan model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslMasterBatchPlan> queryWrapper = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
queryWrapper.orderByDesc("create_time");
|
||||
IPage<MesXslMasterBatchPlan> pageList = masterBatchPlanService.page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "母胶计划-添加")
|
||||
@Operation(summary = "母胶计划-添加")
|
||||
@RequiresPermissions("xslmes:mes_xsl_master_batch_plan:add")
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody MesXslMasterBatchPlan model) {
|
||||
fillDerivedFields(model);
|
||||
masterBatchPlanService.save(model);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "母胶计划-编辑")
|
||||
@Operation(summary = "母胶计划-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_master_batch_plan:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslMasterBatchPlan model) {
|
||||
fillDerivedFields(model);
|
||||
masterBatchPlanService.updateById(model);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "母胶计划-通过id删除")
|
||||
@Operation(summary = "母胶计划-通过id删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_master_batch_plan:delete")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
masterBatchPlanService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "母胶计划-批量删除")
|
||||
@Operation(summary = "母胶计划-批量删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_master_batch_plan:deleteBatch")
|
||||
@DeleteMapping("/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
masterBatchPlanService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "母胶计划-通过id查询")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslMasterBatchPlan> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslMasterBatchPlan entity = masterBatchPlanService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_master_batch_plan:exportXls")
|
||||
@RequestMapping("/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslMasterBatchPlan model) {
|
||||
return super.exportXls(request, model, MesXslMasterBatchPlan.class, "母胶计划");
|
||||
}
|
||||
|
||||
private void fillDerivedFields(MesXslMasterBatchPlan model) {
|
||||
BigDecimal planWeight = model.getPlanWeight();
|
||||
BigDecimal perCarWeight = model.getPerCarWeight();
|
||||
if (planWeight == null || perCarWeight == null || perCarWeight.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
model.setPlannedCarCount(0);
|
||||
return;
|
||||
}
|
||||
model.setPlannedCarCount(planWeight.divide(perCarWeight, 0, RoundingMode.CEILING).intValue());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
import org.jeecg.modules.system.service.ISysDepartService;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -168,6 +169,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
}
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】校对/审核/批准-----------
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "校对", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 1)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-校对")
|
||||
@Operation(summary = "MES密炼PS编制-校对")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:proofread")
|
||||
@@ -176,6 +180,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "compile", "proofread", "校对");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "审核", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 2)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-审核")
|
||||
@Operation(summary = "MES密炼PS编制-审核")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:audit")
|
||||
@@ -184,6 +191,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "proofread", "audit", "审核");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "批准", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 3)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-批准")
|
||||
@Operation(summary = "MES密炼PS编制-批准")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:approve")
|
||||
@@ -201,6 +211,34 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return Result.OK(actionLabel + "成功!");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@ApprovalBizAction(name = "拒绝", table = "mes_xsl_mixer_ps_compile", phase = {"onReject"}, order = 4)
|
||||
@AutoLog(value = "MES密炼PS编制-拒绝")
|
||||
@Operation(summary = "MES密炼PS编制-拒绝(一步退回编制并撤销关联操作)")
|
||||
// 拒绝主要由审批流 onReject 回调触发(执行人为当前审批节点处理人),不强制要求其具备密炼PS业务权限码;
|
||||
// 是否允许驳回已由审批节点本身控制,故此处不加 @RequiresPermissions。
|
||||
@PostMapping(value = "/reject")
|
||||
public Result<String> reject(@RequestParam(name = "ids") String ids) {
|
||||
String err = mesXslMixerPsCompileService.rejectBatch(ids, getOperatorName());
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
return Result.OK("拒绝成功,已退回编制状态!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES密炼PS编制-撤回")
|
||||
@Operation(summary = "MES密炼PS编制-撤回(退回上一环节并撤销关联操作)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:withdraw")
|
||||
@PostMapping(value = "/withdraw")
|
||||
public Result<String> withdraw(@RequestParam(name = "ids") String ids) {
|
||||
String err = mesXslMixerPsCompileService.withdrawBatch(ids, getOperatorName());
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
return Result.OK("撤回成功!");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
|
||||
private String getOperatorName() {
|
||||
LoginUser loginUser = null;
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
|
||||
@Tag(name = "生产订单")
|
||||
@RestController
|
||||
@@ -92,6 +93,15 @@ public class MesXslProductionOrderController
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@AutoLog(value = "生产订单-拆分生成母胶计划")
|
||||
@Operation(summary = "生产订单-拆分生成母胶计划")
|
||||
@RequiresPermissions("xslmes:mes_xsl_production_order:split")
|
||||
@PostMapping("/split")
|
||||
public Result<MesXslMasterBatchPlan> split(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslMasterBatchPlan plan = mesXslProductionOrderService.splitToMasterBatchPlan(id);
|
||||
return Result.OK("拆分成功", plan);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_production_order:exportXls")
|
||||
@RequestMapping("/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslProductionOrder model) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.mes.material.entity.MesMaterial;
|
||||
import org.jeecg.modules.mes.material.service.IMesMaterialService;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
@@ -162,6 +163,9 @@ public class MesXslRubberQuickTestStdController
|
||||
return Result.OK(mesXslRubberQuickTestStdService.selectLinesByStdId(id));
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "启用/停用", table = "mes_xsl_rubber_quick_test_std", phase = {"onApprove", "onReject"})
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES胶料快检实验标准-启用/停用")
|
||||
@Operation(summary = "MES胶料快检实验标准-启用/停用(字典 xslmes_rubber_quick_test_std_enable_status:1使用中 0已停用)")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_std:updateStatus")
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.jeecg.modules.xslmes.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* MES 终胶计划
|
||||
*/
|
||||
@Data
|
||||
@TableName("mes_xsl_final_batch_plan")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "MES终胶计划")
|
||||
public class MesXslFinalBatchPlan implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "来源生产订单ID")
|
||||
private String sourceOrderId;
|
||||
|
||||
@Excel(name = "订单流水号", width = 20)
|
||||
private String orderSerialNo;
|
||||
|
||||
@Excel(name = "订单编号", width = 20)
|
||||
private String orderNo;
|
||||
|
||||
@Excel(name = "生产段数", width = 12)
|
||||
private Integer productionSegmentCount;
|
||||
|
||||
@Excel(name = "订单日期", width = 20, format = "yyyy-MM-dd")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date orderDate;
|
||||
|
||||
@Excel(name = "物料编码", width = 20)
|
||||
private String materialCode;
|
||||
|
||||
@Excel(name = "MES胶料信息", width = 20)
|
||||
private String mesMaterialName;
|
||||
|
||||
@Excel(name = "计划重量", width = 15)
|
||||
private BigDecimal planWeight;
|
||||
|
||||
@Excel(name = "每车重量", width = 15)
|
||||
private BigDecimal perCarWeight;
|
||||
|
||||
@Excel(name = "计划车数", width = 12)
|
||||
private Integer plannedCarCount;
|
||||
|
||||
@Excel(name = "已排产车数", width = 12)
|
||||
private Integer scheduledCarCount;
|
||||
|
||||
@Excel(name = "完成车数", width = 12)
|
||||
private Integer finishedCarCount;
|
||||
|
||||
@Excel(name = "状态", width = 12, replace = {"未开始_0", "进行中_1", "已完成_2"})
|
||||
private Integer status;
|
||||
|
||||
private Integer tenantId;
|
||||
private String sysOrgCode;
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.jeecg.modules.xslmes.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/**
|
||||
* MES 母胶计划
|
||||
*/
|
||||
@Data
|
||||
@TableName("mes_xsl_master_batch_plan")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "MES母胶计划")
|
||||
public class MesXslMasterBatchPlan implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "来源生产订单ID")
|
||||
private String sourceOrderId;
|
||||
|
||||
@Excel(name = "订单流水号", width = 20)
|
||||
private String orderSerialNo;
|
||||
|
||||
@Excel(name = "订单编号", width = 20)
|
||||
private String orderNo;
|
||||
|
||||
@Excel(name = "生产段数", width = 12)
|
||||
private Integer productionSegmentCount;
|
||||
|
||||
@Excel(name = "订单日期", width = 20, format = "yyyy-MM-dd")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private Date orderDate;
|
||||
|
||||
@Excel(name = "物料编号", width = 20)
|
||||
private String materialCode;
|
||||
|
||||
@Excel(name = "MES胶料名称", width = 20)
|
||||
private String mesMaterialName;
|
||||
|
||||
@Excel(name = "计划重量", width = 15)
|
||||
private BigDecimal planWeight;
|
||||
|
||||
@Excel(name = "每车重量", width = 15)
|
||||
private BigDecimal perCarWeight;
|
||||
|
||||
@Excel(name = "计划车数", width = 12)
|
||||
private Integer plannedCarCount;
|
||||
|
||||
@Excel(name = "已排产车数", width = 12)
|
||||
private Integer scheduledCarCount;
|
||||
|
||||
@Excel(name = "完成车数", width = 12)
|
||||
private Integer finishedCarCount;
|
||||
|
||||
@Excel(name = "状态", width = 12, replace = {"未开始_0", "进行中_1", "已完成_2"})
|
||||
private Integer status;
|
||||
|
||||
private Integer tenantId;
|
||||
private String sysOrgCode;
|
||||
private String createBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
private String updateBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.jeecg.modules.xslmes.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan;
|
||||
|
||||
public interface MesXslFinalBatchPlanMapper extends BaseMapper<MesXslFinalBatchPlan> {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.jeecg.modules.xslmes.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
|
||||
public interface MesXslMasterBatchPlanMapper extends BaseMapper<MesXslMasterBatchPlan> {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
|
||||
public interface IMesXslFinalBatchPlanService extends IService<MesXslFinalBatchPlan> {
|
||||
|
||||
MesXslFinalBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder);
|
||||
}
|
||||
@@ -39,6 +39,16 @@ public interface IMesXslFormulaSpecService extends IService<MesXslFormulaSpec> {
|
||||
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
/**
|
||||
* 密炼PS拒绝/撤回时,按发行编号(PS编码)将关联配合示方回退到目标状态,并清空高于目标状态的审批人痕迹。
|
||||
*
|
||||
* @param ps 已回退后的密炼PS编制单
|
||||
* @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit
|
||||
*/
|
||||
void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建-----------
|
||||
/**
|
||||
* 根据配合示方混合段数构建生成混炼示方预览行(示方编号 + 段信息)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
|
||||
public interface IMesXslMasterBatchPlanService extends IService<MesXslMasterBatchPlan> {
|
||||
|
||||
MesXslMasterBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder);
|
||||
}
|
||||
@@ -16,4 +16,22 @@ public interface IMesXslMixerPsCompileService extends IService<MesXslMixerPsComp
|
||||
*/
|
||||
String changeStatusBatch(String ids, String expectedStatus, String targetStatus, String actionLabel, String operatorName);
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
/**
|
||||
* 批量拒绝:将单据一步退回到编制(compile),清空本单据校对/审核/批准全部痕迹,
|
||||
* 并联动回退配合示方、混炼示方、原材料检验标准等关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String rejectBatch(String ids, String operatorName);
|
||||
|
||||
/**
|
||||
* 批量撤回:将单据按当前状态退回上一环节(approve→audit→proofread→compile),
|
||||
* 仅清空被撤回那一步的痕迹,并联动回退关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String withdrawBatch(String ids, String operatorName);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -46,5 +46,15 @@ public interface IMesXslMixingSpecService extends IService<MesXslMixingSpec> {
|
||||
* @param mixerPsTargetStatus 密炼PS目标状态:proofread / audit / approve
|
||||
*/
|
||||
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
/**
|
||||
* 密炼PS拒绝/撤回时,按发行编号(PS编码)清空关联混炼示方高于目标状态的审批人痕迹。
|
||||
*
|
||||
* @param ps 已回退后的密炼PS编制单
|
||||
* @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit
|
||||
*/
|
||||
void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
|
||||
public interface IMesXslProductionOrderService extends IService<MesXslProductionOrder> {}
|
||||
public interface IMesXslProductionOrderService extends IService<MesXslProductionOrder> {
|
||||
|
||||
MesXslMasterBatchPlan splitToMasterBatchPlan(String id);
|
||||
}
|
||||
|
||||
@@ -25,4 +25,11 @@ public interface IMesXslRubberQuickTestStdService extends IService<MesXslRubberQ
|
||||
* 密炼PS(原材料检验标准)批准后,将关联实验标准审核状态置为已批准(仅写入,不随PS反审核回退)
|
||||
*/
|
||||
void markAuditApprovedByPsCompileIds(Collection<String> psCompileIds);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
/**
|
||||
* 密炼PS(原材料检验标准)从已批准回退(拒绝/撤回)时,将关联实验标准审核状态置回草稿(未批准)
|
||||
*/
|
||||
void markAuditDraftByPsCompileIds(Collection<String> psCompileIds);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.modules.mes.material.entity.MesMaterial;
|
||||
import org.jeecg.modules.mes.material.mapper.MesMaterialMapper;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslFinalBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslFinalBatchPlanMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslFinalBatchPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class MesXslFinalBatchPlanServiceImpl
|
||||
extends ServiceImpl<MesXslFinalBatchPlanMapper, MesXslFinalBatchPlan>
|
||||
implements IMesXslFinalBatchPlanService {
|
||||
|
||||
@Autowired private MesMaterialMapper mesMaterialMapper;
|
||||
|
||||
@Override
|
||||
public MesXslFinalBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder) {
|
||||
if (productionOrder == null || StringUtils.isBlank(productionOrder.getId())) {
|
||||
throw new JeecgBootException("生产订单不存在,无法拆分终胶计划");
|
||||
}
|
||||
MesXslFinalBatchPlan exists =
|
||||
this.getOne(new LambdaQueryWrapper<MesXslFinalBatchPlan>().eq(MesXslFinalBatchPlan::getSourceOrderId, productionOrder.getId()));
|
||||
if (exists != null) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
MesMaterial finalMaterial = resolveFinalMaterial(productionOrder.getMaterialCode());
|
||||
if (finalMaterial == null) {
|
||||
throw new JeecgBootException("未找到对应终胶物料,请确认MES物料编码");
|
||||
}
|
||||
|
||||
BigDecimal planWeight = productionOrder.getPlanQty() == null ? BigDecimal.ZERO : productionOrder.getPlanQty();
|
||||
BigDecimal perCarWeight = BigDecimal.ZERO;
|
||||
int planCarCount = calcPlanCarCount(planWeight, perCarWeight);
|
||||
|
||||
MesXslFinalBatchPlan plan = new MesXslFinalBatchPlan();
|
||||
plan.setSourceOrderId(productionOrder.getId());
|
||||
plan.setOrderSerialNo(buildOrderSerialNo(productionOrder));
|
||||
plan.setOrderNo(productionOrder.getProductionOrderNo());
|
||||
plan.setProductionSegmentCount(productionOrder.getProcessSegmentCount());
|
||||
plan.setOrderDate(productionOrder.getOrderDate());
|
||||
plan.setMaterialCode(finalMaterial.getMaterialCode());
|
||||
plan.setMesMaterialName(finalMaterial.getMaterialName());
|
||||
plan.setPlanWeight(planWeight);
|
||||
plan.setPerCarWeight(perCarWeight);
|
||||
plan.setPlannedCarCount(planCarCount);
|
||||
plan.setScheduledCarCount(0);
|
||||
plan.setFinishedCarCount(0);
|
||||
plan.setStatus(0);
|
||||
this.save(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private MesMaterial resolveFinalMaterial(String mesMaterialCode) {
|
||||
if (StringUtils.isBlank(mesMaterialCode)) {
|
||||
return null;
|
||||
}
|
||||
return mesMaterialMapper.selectOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialCode, mesMaterialCode.trim())
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private String buildOrderSerialNo(MesXslProductionOrder productionOrder) {
|
||||
String orderNo = StringUtils.defaultIfBlank(productionOrder.getProductionOrderNo(), productionOrder.getId());
|
||||
return orderNo + "-F-" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private int calcPlanCarCount(BigDecimal planWeight, BigDecimal perCarWeight) {
|
||||
if (planWeight == null || perCarWeight == null || perCarWeight.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return planWeight.divide(perCarWeight, 0, RoundingMode.CEILING).intValue();
|
||||
}
|
||||
}
|
||||
@@ -62,12 +62,6 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
|
||||
private static final Set<String> RUBBER_CATEGORY_KEYS = Set.of("S", "P", "T", "C");
|
||||
private static final Pattern RUBBER_CODE_VERSION_PATTERN = Pattern.compile("([A-Z])01$");
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A48】生成混炼示方同步B/F段胶至密炼物料-----------
|
||||
private static final String CATEGORY_MASTER_MAJOR = "XSLMES_MATERIAL_MASTER";
|
||||
private static final String CATEGORY_MASTER_MINOR_A = "XSLMES_MATERIAL_MASTER_A";
|
||||
private static final String CATEGORY_FINAL_MAJOR = "XSLMES_MATERIAL_FINAL";
|
||||
private static final String CATEGORY_FINAL_MINOR_Q = "XSLMES_MATERIAL_FINAL_Q";
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A48】生成混炼示方同步B/F段胶至密炼物料-----------
|
||||
|
||||
@Resource
|
||||
private MesXslFormulaSpecLineMapper lineMapper;
|
||||
@@ -499,6 +493,41 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslFormulaSpec> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslFormulaSpec::getIssueNumber, ps.getPsCode());
|
||||
switch (mixerPsTargetStatus) {
|
||||
case "compile":
|
||||
// 回退到编制:状态回 compile,清空 校对/审核/批准 三组痕迹
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "compile")
|
||||
.set(MesXslFormulaSpec::getProofreadBy, null).set(MesXslFormulaSpec::getProofreadTime, null)
|
||||
.set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null)
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
case "proofread":
|
||||
// 回退到校对:状态回 submit,清空 审核/批准 痕迹(保留校对)
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "submit")
|
||||
.set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null)
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
case "audit":
|
||||
// 回退到审核:状态回 review_pass,清空 批准 痕迹(保留校对/审核)
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "review_pass")
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
wrapper.set(MesXslFormulaSpec::getUpdateTime, new Date());
|
||||
this.update(wrapper);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建-----------
|
||||
@Override
|
||||
public MesXslFormulaMixingGeneratePreviewVO buildMixingGeneratePreview(String formulaSpecId) {
|
||||
@@ -741,10 +770,10 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A44】生成混炼示方时按设备有效体积计算填充体积-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A48】生成混炼示方同步B/F段胶至密炼物料-----------
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】生成混炼示方改为同步B/F段胶至胶料信息-----------
|
||||
/**
|
||||
* 生成混炼示方时,将 B 段/F 段胶料同步写入密炼物料:
|
||||
* B 段 -> 物料大类「母炼胶」+ 小类「A胶」;F 段 -> 物料大类「终炼胶」+ 小类「Q胶」。
|
||||
* 生成混炼示方时,将 B 段/F 段胶料同步写入「胶料信息」(mes_material):
|
||||
* 胶料类别取配合示方所选「胶料代号」对应胶料的类别;不再写入密炼物料,也不再维护比重。
|
||||
*/
|
||||
private void syncGeneratedRubberMixerMaterial(
|
||||
MesXslFormulaMixingGenerateRowVO row,
|
||||
@@ -756,26 +785,13 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
String specCode = row.getSpecCode().trim();
|
||||
boolean isFinalStage = "Q".equalsIgnoreCase(resolveMixingMaterialStep(row));
|
||||
String majorCode = isFinalStage ? CATEGORY_FINAL_MAJOR : CATEGORY_MASTER_MAJOR;
|
||||
String minorCode = isFinalStage ? CATEGORY_FINAL_MINOR_Q : CATEGORY_MASTER_MINOR_A;
|
||||
String majorCategoryId = resolveCategoryIdByCode(majorCode, categoryIdCache);
|
||||
String minorCategoryId = resolveCategoryIdByCode(minorCode, categoryIdCache);
|
||||
if (StringUtils.isBlank(majorCategoryId) || StringUtils.isBlank(minorCategoryId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"未找到物料分类「" + (isFinalStage ? "终炼胶/Q胶" : "母炼胶/A胶") + "」,请先维护 MES 物料分类字典");
|
||||
}
|
||||
BigDecimal specificGravity = isFinalStage ? compoundSpecificGravity : formula.getARubberSg();
|
||||
String categoryId = resolveGeneratedRubberCategoryId(formula);
|
||||
String rubberName = resolveRubberName(formula);
|
||||
String materialDesc = StringUtils.isNotBlank(formula.getPurpose()) ? formula.getPurpose().trim() : rubberName;
|
||||
|
||||
MesMixerMaterial existing = findMixerMaterialByCodeOrName(specCode);
|
||||
MesMaterial existing = findRubberMaterialByCodeOrName(specCode);
|
||||
Date now = new Date();
|
||||
if (existing != null) {
|
||||
existing.setMajorCategoryId(majorCategoryId);
|
||||
existing.setMinorCategoryId(minorCategoryId);
|
||||
existing.setSpecificGravity(specificGravity);
|
||||
if (StringUtils.isNotBlank(materialDesc)) {
|
||||
existing.setMaterialDesc(materialDesc);
|
||||
if (StringUtils.isNotBlank(categoryId)) {
|
||||
existing.setCategoryId(categoryId);
|
||||
}
|
||||
if (StringUtils.isBlank(existing.getMaterialCode())) {
|
||||
existing.setMaterialCode(specCode);
|
||||
@@ -783,74 +799,68 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
if (StringUtils.isBlank(existing.getMaterialName())) {
|
||||
existing.setMaterialName(specCode);
|
||||
}
|
||||
existing.setUseStatus(existing.getUseStatus() == null ? 1 : existing.getUseStatus());
|
||||
if (StringUtils.isBlank(existing.getAliasName()) && StringUtils.isNotBlank(rubberName)) {
|
||||
existing.setAliasName(rubberName);
|
||||
}
|
||||
existing.setEnableFlag(existing.getEnableFlag() == null ? 1 : existing.getEnableFlag());
|
||||
existing.setUpdateTime(now);
|
||||
mesMixerMaterialService.updateById(existing);
|
||||
mesMaterialService.updateById(existing);
|
||||
log.info(
|
||||
"[混炼示方生成] 更新密炼物料 id={}, code={}, major={}, minor={}, sg={}",
|
||||
"[混炼示方生成] 更新胶料信息 id={}, code={}, categoryId={}, isFinal={}",
|
||||
existing.getId(),
|
||||
specCode,
|
||||
majorCode,
|
||||
minorCode,
|
||||
specificGravity);
|
||||
categoryId,
|
||||
isFinalStage);
|
||||
return;
|
||||
}
|
||||
|
||||
MesMixerMaterial material = new MesMixerMaterial();
|
||||
MesMaterial material = new MesMaterial();
|
||||
material.setMaterialCode(specCode);
|
||||
material.setMaterialName(specCode);
|
||||
material.setMaterialDesc(materialDesc);
|
||||
material.setAliasName(StringUtils.isNotBlank(rubberName) ? rubberName : null);
|
||||
material.setMajorCategoryId(majorCategoryId);
|
||||
material.setMinorCategoryId(minorCategoryId);
|
||||
material.setSpecificGravity(specificGravity);
|
||||
material.setFeedManageStatus(0);
|
||||
material.setUseStatus(1);
|
||||
material.setCategoryId(categoryId);
|
||||
material.setEnableFlag(1);
|
||||
material.setIsSpecialRubber(0);
|
||||
material.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||
material.setCreateTime(now);
|
||||
material.setUpdateTime(now);
|
||||
mesMixerMaterialService.save(material);
|
||||
mesMaterialService.save(material);
|
||||
log.info(
|
||||
"[混炼示方生成] 新增密炼物料 id={}, code={}, major={}, minor={}, sg={}",
|
||||
"[混炼示方生成] 新增胶料信息 id={}, code={}, categoryId={}, isFinal={}",
|
||||
material.getId(),
|
||||
specCode,
|
||||
majorCode,
|
||||
minorCode,
|
||||
specificGravity);
|
||||
categoryId,
|
||||
isFinalStage);
|
||||
}
|
||||
|
||||
private MesMixerMaterial findMixerMaterialByCodeOrName(String specCode) {
|
||||
MesMixerMaterial byCode = mesMixerMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMixerMaterial>()
|
||||
.eq(MesMixerMaterial::getMaterialCode, specCode)
|
||||
.and(w -> w.eq(MesMixerMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMixerMaterial::getDelFlag))
|
||||
/** 生成的 B/F 段胶料类别:取配合示方所选「胶料代号」对应胶料(mes_material)的类别 */
|
||||
private String resolveGeneratedRubberCategoryId(MesXslFormulaSpec formula) {
|
||||
if (formula == null || StringUtils.isBlank(formula.getRubberMaterialId())) {
|
||||
return null;
|
||||
}
|
||||
MesMaterial rubber = mesMaterialService.getById(formula.getRubberMaterialId());
|
||||
return rubber != null ? rubber.getCategoryId() : null;
|
||||
}
|
||||
|
||||
private MesMaterial findRubberMaterialByCodeOrName(String specCode) {
|
||||
if (StringUtils.isBlank(specCode)) {
|
||||
return null;
|
||||
}
|
||||
MesMaterial byCode = mesMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialCode, specCode)
|
||||
.and(w -> w.eq(MesMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMaterial::getDelFlag))
|
||||
.last("limit 1"));
|
||||
if (byCode != null) {
|
||||
return byCode;
|
||||
}
|
||||
return mesMixerMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMixerMaterial>()
|
||||
.eq(MesMixerMaterial::getMaterialName, specCode)
|
||||
.and(w -> w.eq(MesMixerMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMixerMaterial::getDelFlag))
|
||||
return mesMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialName, specCode)
|
||||
.and(w -> w.eq(MesMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMaterial::getDelFlag))
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
private String resolveCategoryIdByCode(String categoryCode, Map<String, String> cache) {
|
||||
if (StringUtils.isBlank(categoryCode)) {
|
||||
return null;
|
||||
}
|
||||
if (cache != null && cache.containsKey(categoryCode)) {
|
||||
return cache.get(categoryCode);
|
||||
}
|
||||
SysCategory category = sysCategoryService.getOne(
|
||||
new LambdaQueryWrapper<SysCategory>().eq(SysCategory::getCode, categoryCode.trim()).last("limit 1"));
|
||||
String categoryId = category != null ? category.getId() : null;
|
||||
if (cache != null) {
|
||||
cache.put(categoryCode, categoryId);
|
||||
}
|
||||
return categoryId;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A48】生成混炼示方同步B/F段胶至密炼物料-----------
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】生成混炼示方改为同步B/F段胶至胶料信息-----------
|
||||
|
||||
private int normalizeMixingStages(Integer mixingStages) {
|
||||
if (mixingStages == null || mixingStages <= 0) {
|
||||
@@ -1094,39 +1104,40 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
Map<String, MesMixerMaterial> mixerCache,
|
||||
Map<String, String> categoryNameCache,
|
||||
MesXslMixerMaterialKindLookupVO kindLookup) {
|
||||
ensureMotherRubberMixerMaterialSynced(motherSpecCode, formula, compoundSpecificGravity, categoryIdCache);
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】上一段母胶明细行改绑胶料信息-----------
|
||||
ensureMotherRubberMaterialSynced(motherSpecCode, formula, categoryIdCache);
|
||||
MesXslMixingSpecMaterial material = new MesXslMixingSpecMaterial();
|
||||
material.setSortNo(sortNo);
|
||||
material.setUnitWeight(unitWeight);
|
||||
MesMixerMaterial mixer = findMixerMaterialByCodeOrName(motherSpecCode.trim());
|
||||
if (mixer != null) {
|
||||
fillMixingMaterialFromMixerMaterial(material, mixer, null, mixerCache, categoryNameCache, kindLookup);
|
||||
MesMaterial rubber = findRubberMaterialByCodeOrName(motherSpecCode.trim());
|
||||
if (rubber != null) {
|
||||
fillMixingMaterialFromRubberMaterial(material, rubber, categoryNameCache, kindLookup);
|
||||
return material;
|
||||
}
|
||||
log.warn("[混炼示方生成] 未找到上一段密炼物料 {},明细行回退为示方编号展示", motherSpecCode);
|
||||
log.warn("[混炼示方生成] 未找到上一段胶料信息 {},明细行回退为示方编号展示", motherSpecCode);
|
||||
material.setMixerMaterialName(motherSpecCode);
|
||||
material.setMixerMaterialDesc(motherSpecCode);
|
||||
fillMotherRubberCategoryFallback(material, categoryIdCache, categoryNameCache, kindLookup);
|
||||
fillMotherRubberCategoryFallback(material, formula, categoryNameCache, kindLookup);
|
||||
return material;
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】上一段母胶明细行改绑胶料信息-----------
|
||||
}
|
||||
|
||||
/** 上一段 B 段胶尚未写入主数据时,按母炼胶/A胶分类补建一条密炼物料 */
|
||||
private void ensureMotherRubberMixerMaterialSynced(
|
||||
/** 上一段 B 段胶尚未写入胶料信息时,按配合示方胶料类别补建一条胶料信息 */
|
||||
private void ensureMotherRubberMaterialSynced(
|
||||
String motherSpecCode,
|
||||
MesXslFormulaSpec formula,
|
||||
BigDecimal compoundSpecificGravity,
|
||||
Map<String, String> categoryIdCache) {
|
||||
if (StringUtils.isBlank(motherSpecCode)) {
|
||||
return;
|
||||
}
|
||||
String normalized = motherSpecCode.trim();
|
||||
if (findMixerMaterialByCodeOrName(normalized) != null) {
|
||||
if (findRubberMaterialByCodeOrName(normalized) != null) {
|
||||
return;
|
||||
}
|
||||
MesXslFormulaMixingGenerateRowVO stub = new MesXslFormulaMixingGenerateRowVO();
|
||||
stub.setSpecCode(normalized);
|
||||
stub.setStepType("A");
|
||||
syncGeneratedRubberMixerMaterial(stub, formula, compoundSpecificGravity, categoryIdCache);
|
||||
syncGeneratedRubberMixerMaterial(stub, formula, null, categoryIdCache);
|
||||
}
|
||||
|
||||
/** 按密炼物料主数据回填混炼示方明细行(名称/大小类/种类) */
|
||||
@@ -1160,20 +1171,44 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
}
|
||||
|
||||
/** 未查到密炼物料时,按母炼胶/A胶分类从配置表解析种类 */
|
||||
private void fillMotherRubberCategoryFallback(
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】母胶明细行按胶料信息回填-----------
|
||||
/** 按胶料信息主数据回填混炼示方明细行(名称/类别/种类) */
|
||||
private void fillMixingMaterialFromRubberMaterial(
|
||||
MesXslMixingSpecMaterial material,
|
||||
Map<String, String> categoryIdCache,
|
||||
MesMaterial rubber,
|
||||
Map<String, String> categoryNameCache,
|
||||
MesXslMixerMaterialKindLookupVO kindLookup) {
|
||||
String majorCategoryId = resolveCategoryIdByCode(CATEGORY_MASTER_MAJOR, categoryIdCache);
|
||||
String minorCategoryId = resolveCategoryIdByCode(CATEGORY_MASTER_MINOR_A, categoryIdCache);
|
||||
material.setMaterialMajor(resolveCategoryName(majorCategoryId, categoryNameCache));
|
||||
material.setMaterialMinor(resolveCategoryName(minorCategoryId, categoryNameCache));
|
||||
if (material == null || rubber == null) {
|
||||
return;
|
||||
}
|
||||
String displayName =
|
||||
StringUtils.isNotBlank(rubber.getMaterialName()) ? rubber.getMaterialName() : rubber.getMaterialCode();
|
||||
material.setMixerMaterialName(displayName);
|
||||
material.setMixerMaterialDesc(displayName);
|
||||
String categoryName = resolveCategoryName(rubber.getCategoryId(), categoryNameCache);
|
||||
material.setMaterialMajor(null);
|
||||
material.setMaterialMinor(categoryName);
|
||||
material.setMixerMaterialId(rubber.getId());
|
||||
material.setMaterialKind(
|
||||
mesXslMixerMaterialKindCfgService.resolveMixingMaterialKind(
|
||||
kindLookup, minorCategoryId, null, material.getMaterialMinor()));
|
||||
kindLookup, rubber.getCategoryId(), null, categoryName));
|
||||
}
|
||||
|
||||
/** 未查到胶料信息时,按配合示方胶料类别从配置表解析种类 */
|
||||
private void fillMotherRubberCategoryFallback(
|
||||
MesXslMixingSpecMaterial material,
|
||||
MesXslFormulaSpec formula,
|
||||
Map<String, String> categoryNameCache,
|
||||
MesXslMixerMaterialKindLookupVO kindLookup) {
|
||||
String categoryId = resolveGeneratedRubberCategoryId(formula);
|
||||
String categoryName = resolveCategoryName(categoryId, categoryNameCache);
|
||||
material.setMaterialMajor(null);
|
||||
material.setMaterialMinor(categoryName);
|
||||
material.setMaterialKind(
|
||||
mesXslMixerMaterialKindCfgService.resolveMixingMaterialKind(
|
||||
kindLookup, categoryId, null, categoryName));
|
||||
}
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】母胶明细行按胶料信息回填-----------
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A54】母炼胶明细行绑定已同步密炼物料主数据-----------
|
||||
|
||||
/** 混合段累计合计:优先主表 stageNTotal,否则按明细该列求和 */
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.modules.mes.material.entity.MesMaterial;
|
||||
import org.jeecg.modules.mes.material.mapper.MesMaterialMapper;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslMasterBatchPlanMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMasterBatchPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class MesXslMasterBatchPlanServiceImpl
|
||||
extends ServiceImpl<MesXslMasterBatchPlanMapper, MesXslMasterBatchPlan>
|
||||
implements IMesXslMasterBatchPlanService {
|
||||
|
||||
@Autowired private MesMaterialMapper mesMaterialMapper;
|
||||
|
||||
@Override
|
||||
public MesXslMasterBatchPlan generateFromProductionOrder(MesXslProductionOrder productionOrder) {
|
||||
if (productionOrder == null || StringUtils.isBlank(productionOrder.getId())) {
|
||||
throw new JeecgBootException("生产订单不存在,无法拆分");
|
||||
}
|
||||
MesMaterial motherMaterial = resolveMotherMaterial(productionOrder.getMaterialCode());
|
||||
if (motherMaterial == null) {
|
||||
throw new JeecgBootException("未找到对应母胶物料(优先B1,其次B2)");
|
||||
}
|
||||
|
||||
MesXslMasterBatchPlan exists =
|
||||
this.getOne(new LambdaQueryWrapper<MesXslMasterBatchPlan>().eq(MesXslMasterBatchPlan::getSourceOrderId, productionOrder.getId()));
|
||||
if (exists != null) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
BigDecimal planWeight = productionOrder.getPlanQty() == null ? BigDecimal.ZERO : productionOrder.getPlanQty();
|
||||
BigDecimal perCarWeight = BigDecimal.ZERO;
|
||||
int planCarCount = calcPlanCarCount(planWeight, perCarWeight);
|
||||
|
||||
MesXslMasterBatchPlan plan = new MesXslMasterBatchPlan();
|
||||
plan.setSourceOrderId(productionOrder.getId());
|
||||
plan.setOrderSerialNo(buildOrderSerialNo(productionOrder));
|
||||
plan.setOrderNo(productionOrder.getProductionOrderNo());
|
||||
plan.setProductionSegmentCount(productionOrder.getProcessSegmentCount());
|
||||
plan.setOrderDate(productionOrder.getOrderDate());
|
||||
plan.setMaterialCode(motherMaterial.getMaterialCode());
|
||||
plan.setMesMaterialName(motherMaterial.getMaterialName());
|
||||
plan.setPlanWeight(planWeight);
|
||||
plan.setPerCarWeight(perCarWeight);
|
||||
plan.setPlannedCarCount(planCarCount);
|
||||
plan.setScheduledCarCount(0);
|
||||
plan.setFinishedCarCount(0);
|
||||
plan.setStatus(0);
|
||||
this.save(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private MesMaterial resolveMotherMaterial(String mesMaterialCode) {
|
||||
if (StringUtils.isBlank(mesMaterialCode)) {
|
||||
return null;
|
||||
}
|
||||
String code = mesMaterialCode.trim();
|
||||
List<String> candidates = buildMotherCandidates(code);
|
||||
for (String c : candidates) {
|
||||
MesMaterial found =
|
||||
mesMaterialMapper.selectOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialCode, c)
|
||||
.last("LIMIT 1"));
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> buildMotherCandidates(String code) {
|
||||
List<String> list = new ArrayList<>(2);
|
||||
if (code.length() > 1 && (code.startsWith("F") || code.startsWith("f"))) {
|
||||
String suffix = code.substring(1);
|
||||
list.add("B1" + suffix);
|
||||
list.add("B2" + suffix);
|
||||
} else {
|
||||
list.add("B1" + code);
|
||||
list.add("B2" + code);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String buildOrderSerialNo(MesXslProductionOrder productionOrder) {
|
||||
String orderNo = StringUtils.defaultIfBlank(productionOrder.getProductionOrderNo(), productionOrder.getId());
|
||||
return orderNo + "-" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private int calcPlanCarCount(BigDecimal planWeight, BigDecimal perCarWeight) {
|
||||
if (planWeight == null || perCarWeight == null || perCarWeight.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return planWeight.divide(perCarWeight, 0, RoundingMode.CEILING).intValue();
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,9 @@ public class MesXslMixerMaterialKindCfgServiceImpl
|
||||
MesXslMixerMaterialKindLookupVO lookup = new MesXslMixerMaterialKindLookupVO();
|
||||
Map<String, String> byRefId = new LinkedHashMap<>();
|
||||
Map<String, String> byRefCode = new LinkedHashMap<>();
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
Map<String, String> byRefName = new LinkedHashMap<>();
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
String rubberKindName = null;
|
||||
if (rows != null) {
|
||||
for (MesXslMixerMaterialKindCfg row : rows) {
|
||||
@@ -310,6 +313,14 @@ public class MesXslMixerMaterialKindCfgServiceImpl
|
||||
byRefCode.put(lowerCode, kindName);
|
||||
}
|
||||
}
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
if (oConvertUtils.isNotEmpty(row.getCategoryRefName())) {
|
||||
String refName = row.getCategoryRefName().trim();
|
||||
if (!byRefName.containsKey(refName)) {
|
||||
byRefName.put(refName, kindName);
|
||||
}
|
||||
}
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
if (rubberKindName == null && DEFAULT_RUBBER_KIND_NAME.equals(kindName)) {
|
||||
rubberKindName = kindName;
|
||||
}
|
||||
@@ -317,6 +328,9 @@ public class MesXslMixerMaterialKindCfgServiceImpl
|
||||
}
|
||||
lookup.setByRefId(byRefId);
|
||||
lookup.setByRefCode(byRefCode);
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
lookup.setByRefName(byRefName);
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
lookup.setRubberKindName(oConvertUtils.isNotEmpty(rubberKindName) ? rubberKindName : DEFAULT_RUBBER_KIND_NAME);
|
||||
return lookup;
|
||||
}
|
||||
@@ -342,6 +356,17 @@ public class MesXslMixerMaterialKindCfgServiceImpl
|
||||
return fromMinor;
|
||||
}
|
||||
}
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类解析支持胶料类别名称-----------
|
||||
if (oConvertUtils.isNotEmpty(minorCategoryName)) {
|
||||
Map<String, String> byRefName = lookup.getByRefName();
|
||||
if (byRefName != null) {
|
||||
String fromName = byRefName.get(minorCategoryName.trim());
|
||||
if (oConvertUtils.isNotEmpty(fromName)) {
|
||||
return fromName;
|
||||
}
|
||||
}
|
||||
}
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类解析支持胶料类别名称-----------
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类未命中配置时不回退小类名-----------
|
||||
return null;
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A53】种类未命中配置时不回退小类名-----------
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
@@ -96,4 +98,93 @@ public class MesXslMixerPsCompileServiceImpl extends ServiceImpl<MesXslMixerPsCo
|
||||
}
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String rejectBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "拒绝", true, operatorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String withdrawBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "撤回", false, operatorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆向回退批处理。
|
||||
*
|
||||
* @param toCompile true=拒绝(一步退回编制),false=撤回(退回上一环节)
|
||||
*/
|
||||
private String doRevertBatch(String ids, String actionLabel, boolean toCompile, String operatorName) {
|
||||
if (oConvertUtils.isEmpty(ids)) {
|
||||
return "请选择要" + actionLabel + "的记录";
|
||||
}
|
||||
for (String rawId : ids.split(",")) {
|
||||
if (oConvertUtils.isEmpty(rawId)) {
|
||||
continue;
|
||||
}
|
||||
String id = rawId.trim();
|
||||
MesXslMixerPsCompile entity = getById(id);
|
||||
if (entity == null) {
|
||||
return "记录不存在或已删除";
|
||||
}
|
||||
String current = entity.getStatus() == null ? "" : entity.getStatus();
|
||||
String prev = prevStatus(current);
|
||||
if (prev == null) {
|
||||
String psCode = oConvertUtils.isEmpty(entity.getPsCode()) ? id : entity.getPsCode();
|
||||
return "PS编码[" + psCode + "]当前为编制状态,无需" + actionLabel;
|
||||
}
|
||||
String targetStatus = toCompile ? "compile" : prev;
|
||||
applyRevert(entity, current, targetStatus);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 执行单条单据的逆向回退:本单据状态+痕迹回退,并联动回退关联单据 */
|
||||
private void applyRevert(MesXslMixerPsCompile entity, String currentStatus, String targetStatus) {
|
||||
boolean leavingApprove = "approve".equals(currentStatus);
|
||||
// 本单据:状态回退 + 清空高于目标状态的痕迹(用 UpdateWrapper 显式 set null,绕过 updateById 对 null 不更新)
|
||||
LambdaUpdateWrapper<MesXslMixerPsCompile> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslMixerPsCompile::getId, entity.getId())
|
||||
.set(MesXslMixerPsCompile::getStatus, targetStatus);
|
||||
if ("compile".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getProofreadBy, null).set(MesXslMixerPsCompile::getProofreadTime, null)
|
||||
.set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null)
|
||||
.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
} else if ("proofread".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null)
|
||||
.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
} else if ("audit".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
}
|
||||
update(wrapper);
|
||||
// 同步实体内存值,供关联单据联动判断使用
|
||||
entity.setStatus(targetStatus);
|
||||
|
||||
// 联动回退:配合示方(状态+审批人)、混炼示方(审批人)
|
||||
mesXslFormulaSpecService.revertFromMixerPsWorkflow(entity, targetStatus);
|
||||
mesXslMixingSpecService.revertFromMixerPsWorkflow(entity, targetStatus);
|
||||
|
||||
// 离开批准态:原材料检验标准回退为草稿(仅原料检验标准类型)
|
||||
if (leavingApprove && XslMesBizConstants.PS_TYPE_RAW_INSPECT_STD.equals(entity.getPsType())) {
|
||||
mesXslRubberQuickTestStdService.markAuditDraftByPsCompileIds(Collections.singletonList(entity.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/** 上一环节映射:approve→audit→proofread→compile,compile 无上一环节返回 null */
|
||||
private String prevStatus(String current) {
|
||||
switch (current) {
|
||||
case "approve":
|
||||
return "audit";
|
||||
case "audit":
|
||||
return "proofread";
|
||||
case "proofread":
|
||||
return "compile";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,10 +22,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.mes.material.entity.MesMixerMaterial;
|
||||
import org.jeecg.modules.mes.material.service.IMesMixerMaterialService;
|
||||
import org.jeecg.modules.system.entity.SysCategory;
|
||||
import org.jeecg.modules.system.service.ISysCategoryService;
|
||||
import org.jeecg.modules.mes.material.entity.MesMaterial;
|
||||
import org.jeecg.modules.mes.material.service.IMesMaterialService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixingSpec;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixingSpecDownStep;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixingSpecMaterial;
|
||||
@@ -53,13 +50,9 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
|
||||
private static final String TCU_UP = "up_mixer";
|
||||
private static final String TCU_DOWN = "down_mixer";
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A49】删除混炼示方时同步删除B/F段胶密炼物料-----------
|
||||
private static final String CATEGORY_MASTER_MAJOR = "XSLMES_MATERIAL_MASTER";
|
||||
private static final String CATEGORY_MASTER_MINOR_A = "XSLMES_MATERIAL_MASTER_A";
|
||||
private static final String CATEGORY_FINAL_MAJOR = "XSLMES_MATERIAL_FINAL";
|
||||
private static final String CATEGORY_FINAL_MINOR_Q = "XSLMES_MATERIAL_FINAL_Q";
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
private static final Pattern GENERATED_B_RUBBER_SPEC_PATTERN = Pattern.compile("^B\\d", Pattern.CASE_INSENSITIVE);
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A49】删除混炼示方时同步删除B/F段胶密炼物料-----------
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
|
||||
@Resource
|
||||
private MesXslMixingSpecMaterialMapper materialMapper;
|
||||
@@ -70,11 +63,10 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
@Resource
|
||||
private MesXslMixingSpecTcuMapper tcuMapper;
|
||||
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除联动改为针对胶料信息-----------
|
||||
@Resource
|
||||
private IMesMixerMaterialService mesMixerMaterialService;
|
||||
|
||||
@Resource
|
||||
private ISysCategoryService sysCategoryService;
|
||||
private IMesMaterialService mesMaterialService;
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除联动改为针对胶料信息-----------
|
||||
|
||||
@Resource
|
||||
private IMesXslFormulaSpecEditLogService mesXslFormulaSpecEditLogService;
|
||||
@@ -684,9 +676,10 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
return rows;
|
||||
}
|
||||
|
||||
//update-begin---author:cursor ---date:20260525 for:【XSLMES-20260525-A49】删除混炼示方时同步删除B/F段胶密炼物料-----------
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
/**
|
||||
* 删除混炼示方后,若该示方编号已无其它混炼示方且未被其它示方明细引用,则同步删除生成时写入的 B/F 段胶密炼物料。
|
||||
* 删除混炼示方后,若该 B/F 段胶示方编号已无其它混炼示方、未被其它示方明细引用、且未被配合示方选作胶料代号,
|
||||
* 则同步删除生成时写入的「胶料信息」(mes_material)。仅处理 F 段或 B+数字 形态的自动生成编号,避免误删人工维护胶料。
|
||||
*/
|
||||
private void syncDeleteGeneratedRubberMixerMaterial(String specName) {
|
||||
if (StringUtils.isBlank(specName)) {
|
||||
@@ -700,41 +693,33 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
long remainingSpecCount =
|
||||
this.count(new LambdaQueryWrapper<MesXslMixingSpec>().eq(MesXslMixingSpec::getSpecName, specCode));
|
||||
if (remainingSpecCount > 0) {
|
||||
log.debug("[混炼示方删除] 示方编号 {} 仍有 {} 条混炼示方,跳过密炼物料同步删除", specCode, remainingSpecCount);
|
||||
log.debug("[混炼示方删除] 示方编号 {} 仍有 {} 条混炼示方,跳过胶料信息同步删除", specCode, remainingSpecCount);
|
||||
return;
|
||||
}
|
||||
long referencedCount = countMaterialReference(specCode, null);
|
||||
if (referencedCount > 0) {
|
||||
log.info(
|
||||
"[混炼示方删除] 示方编号 {} 仍被 {} 条混炼示方明细引用,跳过密炼物料同步删除",
|
||||
"[混炼示方删除] 示方编号 {} 仍被 {} 条混炼示方明细引用,跳过胶料信息同步删除",
|
||||
specCode,
|
||||
referencedCount);
|
||||
return;
|
||||
}
|
||||
MesMixerMaterial material = findMixerMaterialByCodeOrName(specCode);
|
||||
MesMaterial material = findRubberMaterialByCodeOrName(specCode);
|
||||
if (material == null) {
|
||||
log.debug("[混炼示方删除] 未找到示方编号 {} 对应密炼物料,跳过同步删除", specCode);
|
||||
log.debug("[混炼示方删除] 未找到示方编号 {} 对应胶料信息,跳过同步删除", specCode);
|
||||
return;
|
||||
}
|
||||
referencedCount = countMaterialReference(specCode, material.getId());
|
||||
if (referencedCount > 0) {
|
||||
log.info(
|
||||
"[混炼示方删除] 密炼物料 id={}, code={} 仍被 {} 条混炼示方明细引用,跳过同步删除",
|
||||
"[混炼示方删除] 胶料信息 id={}, code={} 仍被 {} 条混炼示方明细引用,跳过同步删除",
|
||||
material.getId(),
|
||||
specCode,
|
||||
referencedCount);
|
||||
return;
|
||||
}
|
||||
Map<String, String> categoryIdCache = new HashMap<>();
|
||||
if (!isGeneratedRubberMixerMaterial(material, isFinalStage, categoryIdCache)) {
|
||||
log.info(
|
||||
"[混炼示方删除] 密炼物料 id={}, code={} 分类非生成示方自动同步类型,跳过删除",
|
||||
material.getId(),
|
||||
specCode);
|
||||
return;
|
||||
}
|
||||
mesMixerMaterialService.removeById(material.getId());
|
||||
log.info("[混炼示方删除] 同步删除密炼物料 id={}, code={}", material.getId(), specCode);
|
||||
mesMaterialService.removeById(material.getId());
|
||||
log.info("[混炼示方删除] 同步删除胶料信息 id={}, code={}", material.getId(), specCode);
|
||||
}
|
||||
|
||||
/** 判断是否为生成混炼示方时自动同步的 B/F 段胶示方编号;true=F段,false=B段,null=非自动生成胶料编号 */
|
||||
@@ -752,38 +737,26 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
return null;
|
||||
}
|
||||
|
||||
private MesMixerMaterial findMixerMaterialByCodeOrName(String specCode) {
|
||||
MesMixerMaterial byCode = mesMixerMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMixerMaterial>()
|
||||
.eq(MesMixerMaterial::getMaterialCode, specCode)
|
||||
.and(w -> w.eq(MesMixerMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMixerMaterial::getDelFlag))
|
||||
private MesMaterial findRubberMaterialByCodeOrName(String specCode) {
|
||||
if (StringUtils.isBlank(specCode)) {
|
||||
return null;
|
||||
}
|
||||
MesMaterial byCode = mesMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialCode, specCode)
|
||||
.and(w -> w.eq(MesMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMaterial::getDelFlag))
|
||||
.last("limit 1"));
|
||||
if (byCode != null) {
|
||||
return byCode;
|
||||
}
|
||||
return mesMixerMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMixerMaterial>()
|
||||
.eq(MesMixerMaterial::getMaterialName, specCode)
|
||||
.and(w -> w.eq(MesMixerMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMixerMaterial::getDelFlag))
|
||||
return mesMaterialService.getOne(
|
||||
new LambdaQueryWrapper<MesMaterial>()
|
||||
.eq(MesMaterial::getMaterialName, specCode)
|
||||
.and(w -> w.eq(MesMaterial::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesMaterial::getDelFlag))
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
private boolean isGeneratedRubberMixerMaterial(
|
||||
MesMixerMaterial material, boolean isFinalStage, Map<String, String> categoryIdCache) {
|
||||
if (material == null) {
|
||||
return false;
|
||||
}
|
||||
String majorCode = isFinalStage ? CATEGORY_FINAL_MAJOR : CATEGORY_MASTER_MAJOR;
|
||||
String minorCode = isFinalStage ? CATEGORY_FINAL_MINOR_Q : CATEGORY_MASTER_MINOR_A;
|
||||
String expectedMajorId = resolveCategoryIdByCode(majorCode, categoryIdCache);
|
||||
String expectedMinorId = resolveCategoryIdByCode(minorCode, categoryIdCache);
|
||||
if (StringUtils.isBlank(expectedMajorId) || StringUtils.isBlank(expectedMinorId)) {
|
||||
return false;
|
||||
}
|
||||
return expectedMajorId.equals(material.getMajorCategoryId()) && expectedMinorId.equals(material.getMinorCategoryId());
|
||||
}
|
||||
|
||||
/** 统计混炼示方明细对某示方编号/密炼物料 ID 的引用次数 */
|
||||
/** 统计混炼示方明细对某示方编号/胶料信息 ID 的引用次数 */
|
||||
private long countMaterialReference(String specCode, String mixerMaterialId) {
|
||||
LambdaQueryWrapper<MesXslMixingSpecMaterial> qw = new LambdaQueryWrapper<>();
|
||||
qw.and(wrapper -> {
|
||||
@@ -794,23 +767,7 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
});
|
||||
return materialMapper.selectCount(qw);
|
||||
}
|
||||
|
||||
private String resolveCategoryIdByCode(String categoryCode, Map<String, String> cache) {
|
||||
if (StringUtils.isBlank(categoryCode)) {
|
||||
return null;
|
||||
}
|
||||
if (cache != null && cache.containsKey(categoryCode)) {
|
||||
return cache.get(categoryCode);
|
||||
}
|
||||
SysCategory category = sysCategoryService.getOne(
|
||||
new LambdaQueryWrapper<SysCategory>().eq(SysCategory::getCode, categoryCode.trim()).last("limit 1"));
|
||||
String categoryId = category != null ? category.getId() : null;
|
||||
if (cache != null) {
|
||||
cache.put(categoryCode, categoryId);
|
||||
}
|
||||
return categoryId;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260525 for:【XSLMES-20260525-A49】删除混炼示方时同步删除B/F段胶密炼物料-----------
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】删除混炼示方时同步删除自动生成的B/F段胶料信息-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
@Override
|
||||
@@ -841,6 +798,38 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
}
|
||||
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslMixingSpec> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslMixingSpec::getIssueNumber, ps.getPsCode());
|
||||
switch (mixerPsTargetStatus) {
|
||||
case "compile":
|
||||
// 回退到编制:清空 校对/审核/批准 三组痕迹
|
||||
wrapper.set(MesXslMixingSpec::getProofreadBy, null).set(MesXslMixingSpec::getProofreadTime, null)
|
||||
.set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null)
|
||||
.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
case "proofread":
|
||||
// 回退到校对:清空 审核/批准 痕迹(保留校对)
|
||||
wrapper.set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null)
|
||||
.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
case "audit":
|
||||
// 回退到审核:清空 批准 痕迹(保留校对/审核)
|
||||
wrapper.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
wrapper.set(MesXslMixingSpec::getUpdateTime, new Date());
|
||||
this.update(wrapper);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260527 for:【配方日志查询】从入参直接构建快照,避免二次查库-----------
|
||||
private MesXslMixingSpecPage buildPageFromInput(
|
||||
MesXslMixingSpec main,
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslFinalBatchPlanService;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMasterBatchPlan;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslProductionOrder;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslProductionOrderMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMasterBatchPlanService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslProductionOrderService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class MesXslProductionOrderServiceImpl
|
||||
extends ServiceImpl<MesXslProductionOrderMapper, MesXslProductionOrder>
|
||||
implements IMesXslProductionOrderService {}
|
||||
implements IMesXslProductionOrderService {
|
||||
|
||||
@Autowired private IMesXslMasterBatchPlanService masterBatchPlanService;
|
||||
@Autowired private IMesXslFinalBatchPlanService finalBatchPlanService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MesXslMasterBatchPlan splitToMasterBatchPlan(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
throw new JeecgBootException("生产订单ID不能为空");
|
||||
}
|
||||
MesXslProductionOrder order = this.getById(id.trim());
|
||||
if (order == null) {
|
||||
throw new JeecgBootException("生产订单不存在");
|
||||
}
|
||||
if (order.getSplitStatus() != null && order.getSplitStatus() == 1) {
|
||||
throw new JeecgBootException("该生产订单已拆分,无需重复操作");
|
||||
}
|
||||
MesXslMasterBatchPlan plan = masterBatchPlanService.generateFromProductionOrder(order);
|
||||
finalBatchPlanService.generateFromProductionOrder(order);
|
||||
MesXslProductionOrder update = new MesXslProductionOrder();
|
||||
update.setId(order.getId());
|
||||
update.setSplitStatus(1);
|
||||
this.updateById(update);
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,5 +156,24 @@ public class MesXslRubberQuickTestStdServiceImpl
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260525 for:【MES】原材料检验标准密炼PS批准时关联实验标准置已批准-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
@Override
|
||||
public void markAuditDraftByPsCompileIds(Collection<String> psCompileIds) {
|
||||
if (CollectionUtils.isEmpty(psCompileIds)) {
|
||||
return;
|
||||
}
|
||||
List<String> ids =
|
||||
psCompileIds.stream().filter(id -> oConvertUtils.isNotEmpty(id)).map(String::trim).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
this.lambdaUpdate()
|
||||
.in(MesXslRubberQuickTestStd::getPsCompileId, ids)
|
||||
.and(w -> w.eq(MesXslRubberQuickTestStd::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestStd::getDelFlag))
|
||||
.set(MesXslRubberQuickTestStd::getAuditStatus, XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT)
|
||||
.update();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
|
||||
//update-end---author:jiangxh ---date:20260525 for:【MES】胶料快检实验标准名称同租户唯一、主子保存-----------
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ public class MesXslMixerMaterialKindLookupVO implements Serializable {
|
||||
@Schema(description = "对应分类/字典项编码 -> 种类名称")
|
||||
private Map<String, String> byRefCode = new LinkedHashMap<>();
|
||||
|
||||
//update-begin---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
@Schema(description = "对应分类名称 -> 种类名称(胶料信息 categoryId_dictText)")
|
||||
private Map<String, String> byRefName = new LinkedHashMap<>();
|
||||
//update-end---author:cursor ---date:20260601 for:【XSLMES-20260601-A62】种类查找表支持按胶料类别名称匹配-----------
|
||||
|
||||
@Schema(description = "胶料种类名称(母炼胶行兜底)")
|
||||
private String rubberKindName;
|
||||
}
|
||||
|
||||
@@ -495,13 +495,23 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubber
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestMethodSelectModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestStdMixerPsSelectModal.vue
|
||||
|
||||
-- author:jiangxh---date:20260525--for: 【MES】原材料检验标准密炼PS批准时关联胶料快检实验标准置已批准(反审核不回退,触发由审核改为批准)---
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/XslMesBizConstants.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java
|
||||
|
||||
-- author:xsl---date:20260528--for: 【IM聊天-OA】IM群聊:创建群聊、群列表、群消息收发与未读 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java
|
||||
jeecgboot-vue3/src/views/system/im/im.api.ts
|
||||
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
|
||||
jeecgboot-vue3/src/views/system/im/ImChat.vue
|
||||
|
||||
-- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 ---
|
||||
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql
|
||||
|
||||
@@ -11,10 +11,13 @@ import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||
import org.jeecg.modules.im.service.ISysImChatService;
|
||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||
import org.jeecg.modules.system.entity.SysUser;
|
||||
import org.jeecg.modules.system.service.ISysUserService;
|
||||
@@ -136,4 +139,87 @@ public class SysImChatController {
|
||||
return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword));
|
||||
}
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】联系人接口(兼容保留,同本部门)-----------
|
||||
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口-----------
|
||||
@Operation(summary = "IM聊天-群聊列表")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@GetMapping("/groups")
|
||||
public Result<List<SysImConversationVO>> groups(HttpServletRequest request) {
|
||||
LoginUser user = currentUser();
|
||||
return Result.OK(imChatService.listGroupConversations(user.getId(), resolveTenantId(user, request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-创建群聊")
|
||||
@RequiresPermissions("sys:im:chat:group")
|
||||
@PostMapping("/group/create")
|
||||
public Result<SysImConversationVO> createGroup(@RequestBody SysImCreateGroupDTO dto, HttpServletRequest request) {
|
||||
LoginUser user = currentUser();
|
||||
return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
||||
}
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||
@Operation(summary = "IM聊天-群聊详情")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@GetMapping("/group/detail")
|
||||
public Result<SysImGroupDetailVO> groupDetail(@RequestParam(name = "conversationId") String conversationId) {
|
||||
LoginUser user = currentUser();
|
||||
return Result.OK(imChatService.getGroupDetail(user.getId(), conversationId));
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-添加群成员")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/addMembers")
|
||||
public Result<SysImConversationVO> addGroupMembers(@RequestBody SysImGroupMembersDTO dto, HttpServletRequest request) {
|
||||
LoginUser user = currentUser();
|
||||
return Result.OK(imChatService.addGroupMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-移除群成员")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/removeMember")
|
||||
public Result<String> removeGroupMember(@RequestParam(name = "conversationId") String conversationId,
|
||||
@RequestParam(name = "memberUserId") String memberUserId) {
|
||||
LoginUser user = currentUser();
|
||||
imChatService.removeGroupMember(user.getId(), conversationId, memberUserId);
|
||||
return Result.OK("移除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-修改群名称")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/rename")
|
||||
public Result<SysImConversationVO> renameGroup(@RequestParam(name = "conversationId") String conversationId,
|
||||
@RequestParam(name = "groupName") String groupName) {
|
||||
LoginUser user = currentUser();
|
||||
return Result.OK(imChatService.renameGroup(user.getId(), conversationId, groupName));
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-转让群主")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/transfer")
|
||||
public Result<String> transferGroupOwner(@RequestParam(name = "conversationId") String conversationId,
|
||||
@RequestParam(name = "newOwnerId") String newOwnerId) {
|
||||
LoginUser user = currentUser();
|
||||
imChatService.transferGroupOwner(user.getId(), conversationId, newOwnerId);
|
||||
return Result.OK("转让成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-退出群聊")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/quit")
|
||||
public Result<String> quitGroup(@RequestParam(name = "conversationId") String conversationId) {
|
||||
LoginUser user = currentUser();
|
||||
imChatService.quitGroup(user.getId(), conversationId);
|
||||
return Result.OK("已退出群聊");
|
||||
}
|
||||
|
||||
@Operation(summary = "IM聊天-解散群聊")
|
||||
@RequiresPermissions("sys:im:chat:list")
|
||||
@PostMapping("/group/dismiss")
|
||||
public Result<String> dismissGroup(@RequestParam(name = "conversationId") String conversationId) {
|
||||
LoginUser user = currentUser();
|
||||
imChatService.dismissGroup(user.getId(), conversationId);
|
||||
return Result.OK("群聊已解散");
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jeecg.modules.im.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建 IM 群聊
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "创建IM群聊")
|
||||
public class SysImCreateGroupDTO {
|
||||
|
||||
@Schema(description = "群名称")
|
||||
private String groupName;
|
||||
|
||||
@Schema(description = "群成员用户ID(不含本人时可自动加入创建人)")
|
||||
private List<String> memberUserIds;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.jeecg.modules.im.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IM 群聊添加成员
|
||||
*/
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员-----------
|
||||
@Data
|
||||
@Schema(description = "IM群聊添加成员")
|
||||
public class SysImGroupMembersDTO {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "新增成员用户ID")
|
||||
private List<String> memberUserIds;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员-----------
|
||||
@@ -20,10 +20,14 @@ public class SysImConversation implements Serializable {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
/** 会话类型 single单聊 */
|
||||
/** 会话类型 single单聊 group群聊 */
|
||||
private String convType;
|
||||
/** 单聊唯一键 */
|
||||
private String userPairKey;
|
||||
/** 群名称 */
|
||||
private String groupName;
|
||||
/** 群主用户ID */
|
||||
private String ownerId;
|
||||
/** 租户ID */
|
||||
private Integer tenantId;
|
||||
/** 最后一条消息摘要 */
|
||||
|
||||
@@ -16,4 +16,9 @@ public interface SysImConversationMapper extends BaseMapper<SysImConversation> {
|
||||
* 查询当前用户的会话列表
|
||||
*/
|
||||
List<SysImConversationVO> listMyConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
|
||||
|
||||
/**
|
||||
* 查询当前用户的群聊列表
|
||||
*/
|
||||
List<SysImConversationVO> listMyGroupConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,26 @@
|
||||
ORDER BY c.last_time DESC
|
||||
</select>
|
||||
|
||||
<select id="listMyGroupConversations" resultType="org.jeecg.modules.im.vo.SysImConversationVO">
|
||||
SELECT
|
||||
c.id AS conversationId,
|
||||
c.conv_type AS convType,
|
||||
c.group_name AS groupName,
|
||||
c.owner_id AS ownerId,
|
||||
c.last_content AS lastContent,
|
||||
c.last_time AS lastTime,
|
||||
m.unread_count AS unreadCount,
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
FROM sys_im_conversation_member gm
|
||||
WHERE gm.conversation_id = c.id
|
||||
) AS memberCount
|
||||
FROM sys_im_conversation_member m
|
||||
INNER JOIN sys_im_conversation c ON c.id = m.conversation_id
|
||||
WHERE m.user_id = #{userId}
|
||||
AND c.tenant_id = #{tenantId}
|
||||
AND c.conv_type = 'group'
|
||||
ORDER BY c.last_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -4,12 +4,16 @@ package org.jeecg.modules.im.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||
|
||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||
|
||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||
|
||||
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||
|
||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||
|
||||
|
||||
@@ -34,7 +38,9 @@ public interface ISysImChatService {
|
||||
|
||||
SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId);
|
||||
|
||||
List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId);
|
||||
|
||||
SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto);
|
||||
|
||||
IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime);
|
||||
|
||||
@@ -42,6 +48,21 @@ public interface ISysImChatService {
|
||||
|
||||
SysImMessageVO sendMessage(String userId, Integer tenantId, SysImSendMessageDTO dto);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
||||
/**
|
||||
* 系统消息:以 fromUser 身份给 toUser 发送单聊消息。
|
||||
* 与 sendMessage 区别:自动获取/创建单聊会话,且不做"同部门/同租户聊天"校验,专供审批通知等系统场景。
|
||||
*
|
||||
* @param fromUserId 发送人用户ID(一般为业务发起人)
|
||||
* @param toUserId 接收人用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param content 消息内容
|
||||
* @param msgType 消息类型 text/biz_record 等,空则按 text
|
||||
* @return 消息VO,发送失败返回 null
|
||||
*/
|
||||
SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
|
||||
|
||||
|
||||
|
||||
void markRead(String userId, String conversationId);
|
||||
@@ -50,5 +71,28 @@ public interface ISysImChatService {
|
||||
|
||||
List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword);
|
||||
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||
/** 群聊详情(含成员列表,区分群主) */
|
||||
SysImGroupDetailVO getGroupDetail(String userId, String conversationId);
|
||||
|
||||
/** 添加群成员(所有群成员可操作) */
|
||||
SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto);
|
||||
|
||||
/** 移除群成员(仅群主) */
|
||||
void removeGroupMember(String userId, String conversationId, String memberUserId);
|
||||
|
||||
/** 修改群名称(仅群主) */
|
||||
SysImConversationVO renameGroup(String userId, String conversationId, String groupName);
|
||||
|
||||
/** 转让群主(仅群主) */
|
||||
void transferGroupOwner(String userId, String conversationId, String newOwnerId);
|
||||
|
||||
/** 退出群聊(非群主成员) */
|
||||
void quitGroup(String userId, String conversationId);
|
||||
|
||||
/** 解散群聊(仅群主) */
|
||||
void dismissGroup(String userId, String conversationId);
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.jeecg.common.constant.WebsocketConst;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.DateUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||
import org.jeecg.modules.im.entity.SysImConversation;
|
||||
import org.jeecg.modules.im.entity.SysImConversationMember;
|
||||
@@ -19,6 +21,8 @@ import org.jeecg.modules.im.mapper.SysImMessageMapper;
|
||||
import org.jeecg.modules.im.service.ISysImChatService;
|
||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||
import org.jeecg.modules.im.vo.SysImGroupMemberVO;
|
||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||
import org.jeecg.modules.message.websocket.WebSocket;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
@@ -41,6 +45,8 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -51,6 +57,7 @@ import java.util.stream.Collectors;
|
||||
public class SysImChatServiceImpl implements ISysImChatService {
|
||||
|
||||
private static final String CONV_TYPE_SINGLE = "single";
|
||||
private static final String CONV_TYPE_GROUP = "group";
|
||||
private static final String MSG_TYPE_TEXT = "text";
|
||||
private static final String MSG_TYPE_IMAGE = "image";
|
||||
private static final String MSG_TYPE_BIZ_RECORD = "biz_record";
|
||||
@@ -116,6 +123,271 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
}
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】打开单聊会话-----------
|
||||
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建-----------
|
||||
@Override
|
||||
public List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId) {
|
||||
if (oConvertUtils.isEmpty(userId) || tenantId == null || tenantId <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return conversationMapper.listMyGroupConversations(userId, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto) {
|
||||
if (dto == null || oConvertUtils.isEmpty(dto.getGroupName())) {
|
||||
throw new JeecgBootException("群名称不能为空");
|
||||
}
|
||||
String groupName = dto.getGroupName().trim();
|
||||
if (groupName.length() > 30) {
|
||||
throw new JeecgBootException("群名称不能超过30字");
|
||||
}
|
||||
List<String> memberIds = normalizeGroupMemberIds(userId, dto.getMemberUserIds());
|
||||
if (memberIds.size() < 2) {
|
||||
throw new JeecgBootException("群聊至少需要2名成员");
|
||||
}
|
||||
if (memberIds.size() > 50) {
|
||||
throw new JeecgBootException("群成员不能超过50人");
|
||||
}
|
||||
for (String memberId : memberIds) {
|
||||
if (userId.equals(memberId)) {
|
||||
continue;
|
||||
}
|
||||
validateTenantChat(userId, tenantId, orgCode, memberId);
|
||||
}
|
||||
Date now = new Date();
|
||||
SysImConversation conversation = new SysImConversation();
|
||||
conversation.setConvType(CONV_TYPE_GROUP);
|
||||
conversation.setGroupName(groupName);
|
||||
conversation.setOwnerId(userId);
|
||||
conversation.setTenantId(tenantId);
|
||||
conversation.setCreateBy(userId);
|
||||
conversation.setCreateTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
conversationMapper.insert(conversation);
|
||||
for (String memberId : memberIds) {
|
||||
createMember(conversation.getId(), memberId, now);
|
||||
}
|
||||
return buildGroupConversationVo(conversation, userId);
|
||||
}
|
||||
|
||||
private List<String> normalizeGroupMemberIds(String creatorId, List<String> memberUserIds) {
|
||||
Set<String> memberIds = new LinkedHashSet<>();
|
||||
memberIds.add(creatorId);
|
||||
if (memberUserIds != null) {
|
||||
for (String memberId : memberUserIds) {
|
||||
if (oConvertUtils.isNotEmpty(memberId)) {
|
||||
memberIds.add(memberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(memberIds);
|
||||
}
|
||||
|
||||
private SysImConversationVO buildGroupConversationVo(SysImConversation conversation, String userId) {
|
||||
SysImConversationMember member = getMember(userId, conversation.getId());
|
||||
SysImConversationVO vo = new SysImConversationVO();
|
||||
vo.setConversationId(conversation.getId());
|
||||
vo.setConvType(CONV_TYPE_GROUP);
|
||||
vo.setGroupName(conversation.getGroupName());
|
||||
vo.setOwnerId(conversation.getOwnerId());
|
||||
vo.setLastContent(conversation.getLastContent());
|
||||
vo.setLastTime(conversation.getLastTime());
|
||||
vo.setUnreadCount(member == null ? 0 : member.getUnreadCount());
|
||||
Long memberCount = memberMapper.selectCount(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversation.getId()));
|
||||
vo.setMemberCount(memberCount == null ? 0 : memberCount.intValue());
|
||||
return vo;
|
||||
}
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理-----------
|
||||
private static final int GROUP_MEMBER_MAX = 50;
|
||||
|
||||
@Override
|
||||
public SysImGroupDetailVO getGroupDetail(String userId, String conversationId) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||
.orderByAsc(SysImConversationMember::getCreateTime));
|
||||
List<String> memberUserIds = members.stream()
|
||||
.map(SysImConversationMember::getUserId)
|
||||
.filter(oConvertUtils::isNotEmpty)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<String, SysUser> userMap = new HashMap<>(memberUserIds.size());
|
||||
if (!memberUserIds.isEmpty()) {
|
||||
List<SysUser> users = userMapper.selectBatchIds(memberUserIds);
|
||||
if (users != null) {
|
||||
for (SysUser user : users) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
}
|
||||
}
|
||||
String ownerId = conversation.getOwnerId();
|
||||
List<SysImGroupMemberVO> memberVoList = new ArrayList<>(members.size());
|
||||
for (SysImConversationMember member : members) {
|
||||
SysUser user = userMap.get(member.getUserId());
|
||||
SysImGroupMemberVO memberVo = new SysImGroupMemberVO();
|
||||
memberVo.setUserId(member.getUserId());
|
||||
memberVo.setOwner(member.getUserId() != null && member.getUserId().equals(ownerId));
|
||||
if (user != null) {
|
||||
memberVo.setRealname(user.getRealname());
|
||||
memberVo.setUsername(user.getUsername());
|
||||
memberVo.setAvatar(user.getAvatar());
|
||||
}
|
||||
memberVoList.add(memberVo);
|
||||
}
|
||||
// 群主排在最前
|
||||
memberVoList.sort(Comparator.comparingInt(item -> Boolean.TRUE.equals(item.getOwner()) ? 0 : 1));
|
||||
SysImGroupDetailVO detail = new SysImGroupDetailVO();
|
||||
detail.setConversationId(conversation.getId());
|
||||
detail.setGroupName(conversation.getGroupName());
|
||||
detail.setOwnerId(ownerId);
|
||||
detail.setMemberCount(memberVoList.size());
|
||||
detail.setOwner(userId.equals(ownerId));
|
||||
detail.setMembers(memberVoList);
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto) {
|
||||
if (dto == null || oConvertUtils.isEmpty(dto.getConversationId())) {
|
||||
throw new JeecgBootException("会话不存在");
|
||||
}
|
||||
SysImConversation conversation = assertGroupConversation(userId, dto.getConversationId());
|
||||
if (dto.getMemberUserIds() == null || dto.getMemberUserIds().isEmpty()) {
|
||||
throw new JeecgBootException("请选择要添加的成员");
|
||||
}
|
||||
// 已在群成员
|
||||
Set<String> existIds = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversation.getId()))
|
||||
.stream().map(SysImConversationMember::getUserId).collect(Collectors.toSet());
|
||||
// 去重并过滤已存在成员
|
||||
List<String> toAdd = new ArrayList<>();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (String memberId : dto.getMemberUserIds()) {
|
||||
if (oConvertUtils.isEmpty(memberId) || existIds.contains(memberId) || !seen.add(memberId)) {
|
||||
continue;
|
||||
}
|
||||
toAdd.add(memberId);
|
||||
}
|
||||
if (toAdd.isEmpty()) {
|
||||
throw new JeecgBootException("所选成员已在群内");
|
||||
}
|
||||
if (existIds.size() + toAdd.size() > GROUP_MEMBER_MAX) {
|
||||
throw new JeecgBootException("群成员不能超过" + GROUP_MEMBER_MAX + "人");
|
||||
}
|
||||
// 校验同租户、同部门
|
||||
for (String memberId : toAdd) {
|
||||
validateTenantChat(userId, tenantId, orgCode, memberId);
|
||||
}
|
||||
Date now = new Date();
|
||||
for (String memberId : toAdd) {
|
||||
createMember(conversation.getId(), memberId, now);
|
||||
}
|
||||
return buildGroupConversationVo(conversation, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeGroupMember(String userId, String conversationId, String memberUserId) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
assertGroupOwner(userId, conversation);
|
||||
if (oConvertUtils.isEmpty(memberUserId)) {
|
||||
throw new JeecgBootException("请选择要移除的成员");
|
||||
}
|
||||
if (memberUserId.equals(conversation.getOwnerId())) {
|
||||
throw new JeecgBootException("群主不能被移除");
|
||||
}
|
||||
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||
.eq(SysImConversationMember::getUserId, memberUserId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysImConversationVO renameGroup(String userId, String conversationId, String groupName) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
assertGroupOwner(userId, conversation);
|
||||
if (oConvertUtils.isEmpty(groupName)) {
|
||||
throw new JeecgBootException("群名称不能为空");
|
||||
}
|
||||
String name = groupName.trim();
|
||||
if (name.length() > 30) {
|
||||
throw new JeecgBootException("群名称不能超过30字");
|
||||
}
|
||||
conversation.setGroupName(name);
|
||||
conversation.setUpdateBy(userId);
|
||||
conversation.setUpdateTime(new Date());
|
||||
conversationMapper.updateById(conversation);
|
||||
return buildGroupConversationVo(conversation, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void transferGroupOwner(String userId, String conversationId, String newOwnerId) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
assertGroupOwner(userId, conversation);
|
||||
if (oConvertUtils.isEmpty(newOwnerId)) {
|
||||
throw new JeecgBootException("请选择新群主");
|
||||
}
|
||||
if (newOwnerId.equals(userId)) {
|
||||
throw new JeecgBootException("不能转让给自己");
|
||||
}
|
||||
if (getMember(newOwnerId, conversationId) == null) {
|
||||
throw new JeecgBootException("新群主必须是群成员");
|
||||
}
|
||||
conversation.setOwnerId(newOwnerId);
|
||||
conversation.setUpdateBy(userId);
|
||||
conversation.setUpdateTime(new Date());
|
||||
conversationMapper.updateById(conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void quitGroup(String userId, String conversationId) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
if (userId.equals(conversation.getOwnerId())) {
|
||||
throw new JeecgBootException("群主不能退出群聊,请先转让群主或解散群聊");
|
||||
}
|
||||
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||
.eq(SysImConversationMember::getUserId, userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void dismissGroup(String userId, String conversationId) {
|
||||
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||
assertGroupOwner(userId, conversation);
|
||||
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversationId));
|
||||
conversationMapper.deleteById(conversationId);
|
||||
}
|
||||
|
||||
/** 校验会话存在、为群聊且当前用户是群成员 */
|
||||
private SysImConversation assertGroupConversation(String userId, String conversationId) {
|
||||
if (oConvertUtils.isEmpty(conversationId)) {
|
||||
throw new JeecgBootException("会话不存在");
|
||||
}
|
||||
SysImConversation conversation = conversationMapper.selectById(conversationId);
|
||||
if (conversation == null || !CONV_TYPE_GROUP.equals(conversation.getConvType())) {
|
||||
throw new JeecgBootException("群聊不存在");
|
||||
}
|
||||
assertMember(userId, conversationId);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/** 校验当前用户是群主 */
|
||||
private void assertGroupOwner(String userId, SysImConversation conversation) {
|
||||
if (!userId.equals(conversation.getOwnerId())) {
|
||||
throw new JeecgBootException("仅群主可执行该操作");
|
||||
}
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】消息分页-----------
|
||||
@Override
|
||||
public IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime) {
|
||||
@@ -179,7 +451,15 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
|
||||
SysImConversation conversation = conversationMapper.selectById(dto.getConversationId());
|
||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
||||
conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200));
|
||||
String lastContent = resolveLastContent(message.getMsgType(), message.getContent());
|
||||
if (CONV_TYPE_GROUP.equals(conversation.getConvType())) {
|
||||
SysUser sender = userMapper.selectById(userId);
|
||||
String senderName = sender != null ? oConvertUtils.getString(sender.getRealname(), sender.getUsername()) : "";
|
||||
if (oConvertUtils.isNotEmpty(senderName)) {
|
||||
lastContent = senderName + ": " + lastContent;
|
||||
}
|
||||
}
|
||||
conversation.setLastContent(truncate(lastContent, 200));
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
||||
conversation.setLastTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
@@ -188,11 +468,62 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
memberMapper.incrementUnreadExceptSender(dto.getConversationId(), userId);
|
||||
SysImMessageVO messageVo = toMessageVo(message, userId);
|
||||
fillBizRecordReceiverPermission(messageVo, message, userId, new HashMap<>(4));
|
||||
pushChatMessage(dto.getConversationId(), userId, messageVo);
|
||||
pushChatMessage(dto.getConversationId(), userId, messageVo, conversation.getConvType());
|
||||
return messageVo;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】发送消息-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType) {
|
||||
if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) {
|
||||
return null;
|
||||
}
|
||||
// 不给自己发送
|
||||
if (fromUserId.equals(toUserId)) {
|
||||
return null;
|
||||
}
|
||||
Integer tenant = tenantId == null ? 0 : tenantId;
|
||||
Date now = new Date();
|
||||
// 获取或创建单聊会话(系统通知场景,不做同部门校验)
|
||||
String pairKey = buildPairKey(fromUserId, toUserId);
|
||||
SysImConversation conversation = conversationMapper.selectOne(new LambdaQueryWrapper<SysImConversation>()
|
||||
.eq(SysImConversation::getTenantId, tenant)
|
||||
.eq(SysImConversation::getUserPairKey, pairKey));
|
||||
if (conversation == null) {
|
||||
conversation = new SysImConversation();
|
||||
conversation.setConvType(CONV_TYPE_SINGLE);
|
||||
conversation.setUserPairKey(pairKey);
|
||||
conversation.setTenantId(tenant);
|
||||
conversation.setCreateBy(fromUserId);
|
||||
conversation.setCreateTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
conversationMapper.insert(conversation);
|
||||
createMember(conversation.getId(), fromUserId, now);
|
||||
createMember(conversation.getId(), toUserId, now);
|
||||
}
|
||||
// 写入消息
|
||||
SysImMessage message = new SysImMessage();
|
||||
message.setConversationId(conversation.getId());
|
||||
message.setSenderId(fromUserId);
|
||||
message.setContent(content.trim());
|
||||
message.setMsgType(oConvertUtils.isEmpty(msgType) ? MSG_TYPE_TEXT : msgType);
|
||||
message.setTenantId(tenant);
|
||||
message.setCreateTime(now);
|
||||
messageMapper.insert(message);
|
||||
// 更新会话摘要与未读、推送
|
||||
conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200));
|
||||
conversation.setLastTime(now);
|
||||
conversation.setUpdateTime(now);
|
||||
conversationMapper.updateById(conversation);
|
||||
memberMapper.incrementUnreadExceptSender(conversation.getId(), fromUserId);
|
||||
SysImMessageVO messageVo = toMessageVo(message, fromUserId);
|
||||
pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType());
|
||||
return messageVo;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
|
||||
|
||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】标记已读-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -422,6 +753,10 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
if (!Boolean.TRUE.equals(vo.getMine()) || !MSG_TYPE_BIZ_RECORD.equals(vo.getMsgType())) {
|
||||
return;
|
||||
}
|
||||
SysImConversation conversation = conversationMapper.selectById(message.getConversationId());
|
||||
if (conversation == null || !CONV_TYPE_SINGLE.equals(conversation.getConvType())) {
|
||||
return;
|
||||
}
|
||||
String pagePath = extractBizRecordPagePath(vo.getContent());
|
||||
if (oConvertUtils.isEmpty(pagePath)) {
|
||||
return;
|
||||
@@ -509,7 +844,7 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发送方提示接收方无功能权限-----------
|
||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】消息列表批量填充发送人-----------
|
||||
|
||||
private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo) {
|
||||
private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo, String convType) {
|
||||
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||
.eq(SysImConversationMember::getConversationId, conversationId));
|
||||
for (SysImConversationMember member : members) {
|
||||
@@ -520,6 +855,9 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
||||
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT);
|
||||
obj.put(WebsocketConst.MSG_USER_ID, member.getUserId());
|
||||
obj.put("conversationId", conversationId);
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
|
||||
obj.put("convType", convType);
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
|
||||
obj.put("messageId", messageVo.getId());
|
||||
obj.put("senderId", messageVo.getSenderId());
|
||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】WebSocket推送补全头像字段-----------
|
||||
|
||||
@@ -34,4 +34,10 @@ public class SysImConversationVO {
|
||||
private String targetUsername;
|
||||
@Schema(description = "对方头像")
|
||||
private String targetAvatar;
|
||||
@Schema(description = "群名称")
|
||||
private String groupName;
|
||||
@Schema(description = "群成员数")
|
||||
private Integer memberCount;
|
||||
@Schema(description = "群主用户ID")
|
||||
private String ownerId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jeecg.modules.im.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IM 群聊详情(群设置)
|
||||
*/
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情-----------
|
||||
@Data
|
||||
@Schema(description = "IM群聊详情")
|
||||
public class SysImGroupDetailVO {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private String conversationId;
|
||||
@Schema(description = "群名称")
|
||||
private String groupName;
|
||||
@Schema(description = "群主用户ID")
|
||||
private String ownerId;
|
||||
@Schema(description = "群成员数")
|
||||
private Integer memberCount;
|
||||
@Schema(description = "当前用户是否群主")
|
||||
private Boolean owner;
|
||||
@Schema(description = "群成员列表")
|
||||
private List<SysImGroupMemberVO> members;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情-----------
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jeecg.modules.im.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* IM 群成员
|
||||
*/
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示-----------
|
||||
@Data
|
||||
@Schema(description = "IM群成员")
|
||||
public class SysImGroupMemberVO {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
@Schema(description = "姓名")
|
||||
private String realname;
|
||||
@Schema(description = "账号")
|
||||
private String username;
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
@Schema(description = "是否群主")
|
||||
private Boolean owner;
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示-----------
|
||||
@@ -157,6 +157,7 @@ spring:
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://xsl.qdxsl.top:50768/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
# url: jdbc:mysql://10.30.1.60:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
# url: jdbc:mysql://localhost:3307/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- MES 母胶计划
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_master_batch_plan` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
|
||||
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水号',
|
||||
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
|
||||
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
|
||||
`order_date` date DEFAULT NULL COMMENT '订单日期',
|
||||
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编号',
|
||||
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料名称',
|
||||
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
|
||||
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
|
||||
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
|
||||
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
|
||||
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
|
||||
`status` int DEFAULT 0 COMMENT '状态:0未开始 1进行中 2已完成',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
|
||||
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mxmbp_source_order` (`source_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES母胶计划';
|
||||
@@ -0,0 +1,27 @@
|
||||
-- MES 终胶计划
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_final_batch_plan` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
|
||||
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水',
|
||||
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
|
||||
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
|
||||
`order_date` date DEFAULT NULL COMMENT '订单日期',
|
||||
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编码',
|
||||
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料信息',
|
||||
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
|
||||
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
|
||||
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
|
||||
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
|
||||
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
|
||||
`status` int DEFAULT 0 COMMENT '状态:0未开始 1进行中 2已完成',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
|
||||
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mxfb_source_order` (`source_order_id`),
|
||||
UNIQUE KEY `uk_mxfb_source_order_del` (`source_order_id`, `del_flag`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES终胶计划';
|
||||
@@ -0,0 +1,22 @@
|
||||
-- IM 群聊:扩展会话表字段
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `sys_im_conversation`
|
||||
ADD COLUMN `group_name` varchar(100) DEFAULT NULL COMMENT '群名称' AFTER `user_pair_key`,
|
||||
ADD COLUMN `owner_id` varchar(32) DEFAULT NULL COMMENT '群主用户ID' AFTER `group_name`;
|
||||
|
||||
ALTER TABLE `sys_im_conversation`
|
||||
MODIFY COLUMN `conv_type` varchar(10) NOT NULL DEFAULT 'single' COMMENT '会话类型 single单聊 group群聊';
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000113', '1995000000000000110', '创建群聊', 2, 'sys:im:chat:group', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`role_code` = 'admin'
|
||||
AND p.`id` = '1995000000000000113'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
@@ -0,0 +1,97 @@
|
||||
-- 【QH-MES审批流设计】审批流定义表 + 可审批单据字典 + 我的租户菜单与授权
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 审批流定义表
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_flow` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`flow_name` varchar(100) NOT NULL COMMENT '审批流名称',
|
||||
`biz_table` varchar(100) NOT NULL COMMENT '绑定单据表名',
|
||||
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '绑定单据中文名(冗余展示)',
|
||||
`flow_config` longtext COMMENT '流程设计JSON(钉钉式节点树)',
|
||||
`status` varchar(1) DEFAULT '0' COMMENT '状态 0草稿 1已发布 2已停用',
|
||||
`sort_no` double DEFAULT '0' COMMENT '排序',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_appr_flow_tenant_biz` (`tenant_id`, `biz_table`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批流定义表';
|
||||
|
||||
-- 2) 可审批单据字典(单据来源:MES现有业务表) item_value=表名 item_text=单据中文名
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000310', '可审批单据', 'mes_xsl_approval_biz_doc', 'MES审批流可绑定的业务单据', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000311', '1995000000000000310', '配合示方', 'mes_xsl_formula_spec', '配合示方主表', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000312', '1995000000000000310', '混炼示方', 'mes_xsl_mixing_spec', '混炼示方主表', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000313', '1995000000000000310', '密炼PS编制', 'mes_xsl_mixer_ps_compile', '密炼PS编制', 3, 1, 'admin', NOW()),
|
||||
('1995000000000000314', '1995000000000000310', '胶料快检标准', 'mes_xsl_rubber_quick_test_std', '胶料快检实验标准', 4, 1, 'admin', NOW()),
|
||||
('1995000000000000315', '1995000000000000310', '原料入场记录', 'mes_xsl_raw_material_entry', '原料入场记录', 5, 1, 'admin', NOW());
|
||||
|
||||
-- 2.1) 审批流状态字典
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000320', '审批流状态', 'mes_xsl_approval_flow_status', 'MES审批流定义状态', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000321', '1995000000000000320', '草稿', '0', '草稿', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000322', '1995000000000000320', '已发布', '1', '已发布', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000323', '1995000000000000320', '已停用', '2', '已停用', 3, 1, 'admin', NOW());
|
||||
|
||||
-- 3) 我的租户 -> 审批流设计 菜单(parent_id=我的租户 1674708136602542082)
|
||||
INSERT IGNORE INTO `sys_permission` (
|
||||
`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`,
|
||||
`menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`,
|
||||
`hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`,
|
||||
`del_flag`, `rule_flag`, `status`, `internal_or_external`
|
||||
) VALUES (
|
||||
'1995000000000000301', '1674708136602542082', '审批流设计', '/approval/ApprovalFlowList',
|
||||
'approval/flow/ApprovalFlowList', 1, 'ApprovalFlowList', NULL,
|
||||
1, NULL, '0', 4.00, 0, 'ant-design:partition-outlined', 0, 1,
|
||||
0, 0, '租户审批流可视化设计', 'admin', NOW(), 'admin', NOW(),
|
||||
0, 0, '1', 0
|
||||
);
|
||||
|
||||
-- 按钮权限
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000302', '1995000000000000301', '查询', 2, 'approval:flow:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000303', '1995000000000000301', '新增', 2, 'approval:flow:add', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000304', '1995000000000000301', '编辑', 2, 'approval:flow:edit', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000305', '1995000000000000301', '删除', 2, 'approval:flow:delete', '1', 4.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||
VALUES ('1995000000000000306', '1995000000000000301', '设计/发布', 2, 'approval:flow:design', '1', 5.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||
|
||||
-- 4) 授权给超级管理员 admin
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`role_code` = 'admin'
|
||||
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
|
||||
-- 5) 授权给租户管理员 zuhuadmin(挂在"我的租户"下必须授权,否则租户管理员看不到)
|
||||
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`role_code` = 'zuhuadmin'
|
||||
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
-- 【QH-MES审批流设计】审批实例表 + 审批流定义增加单据标题字段
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 审批流定义表增加"单据标题字段名",发起时用于展示具体单据
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `title_field` varchar(100) DEFAULT NULL COMMENT '单据标题字段名(发起选单据时展示)' AFTER `biz_table_name`;
|
||||
|
||||
-- 2) 审批实例表(本期仅发起,记录实例与当前处理人)
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_instance` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`flow_id` varchar(32) NOT NULL COMMENT '审批流定义ID',
|
||||
`flow_name` varchar(100) DEFAULT NULL COMMENT '审批流名称',
|
||||
`biz_table` varchar(100) DEFAULT NULL COMMENT '业务单据表名',
|
||||
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '业务单据中文名',
|
||||
`biz_data_id` varchar(64) DEFAULT NULL COMMENT '业务单据记录ID',
|
||||
`biz_title` varchar(300) DEFAULT NULL COMMENT '业务单据展示标题',
|
||||
`current_node_id` varchar(64) DEFAULT NULL COMMENT '当前节点ID',
|
||||
`current_node_name` varchar(100) DEFAULT NULL COMMENT '当前节点名称',
|
||||
`current_handlers` varchar(1000) DEFAULT NULL COMMENT '当前处理人(username逗号分隔)',
|
||||
`current_handlers_text` varchar(1000) DEFAULT NULL COMMENT '当前处理人展示文本',
|
||||
`status` varchar(2) DEFAULT '0' COMMENT '状态 0审批中 1已通过 2已驳回 3已撤销',
|
||||
`apply_user` varchar(50) DEFAULT NULL COMMENT '发起人username',
|
||||
`apply_user_name` varchar(100) DEFAULT NULL COMMENT '发起人姓名',
|
||||
`apply_time` datetime DEFAULT NULL COMMENT '发起时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_appr_inst_tenant` (`tenant_id`, `flow_id`),
|
||||
KEY `idx_appr_inst_apply` (`apply_user`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批实例表';
|
||||
|
||||
-- 3) 审批实例状态字典
|
||||
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
VALUES ('1995000000000000330', '审批实例状态', 'mes_xsl_approval_instance_status', 'MES审批实例状态', 0, 'admin', NOW(), 0, 0);
|
||||
|
||||
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
VALUES
|
||||
('1995000000000000331', '1995000000000000330', '审批中', '0', '审批中', 1, 1, 'admin', NOW()),
|
||||
('1995000000000000332', '1995000000000000330', '已通过', '1', '已通过', 2, 1, 'admin', NOW()),
|
||||
('1995000000000000333', '1995000000000000330', '已驳回', '2', '已驳回', 3, 1, 'admin', NOW()),
|
||||
('1995000000000000334', '1995000000000000330', '已撤销', '3', '已撤销', 4, 1, 'admin', NOW());
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 【QH-MES审批流设计】审批流定义增加"功能页面路由",用于控制发起审批悬浮按钮仅在对应功能页显示
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `route_path` varchar(255) DEFAULT NULL COMMENT '对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)' AFTER `title_field`;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 【QH-MES审批流设计】审批办理/流转:实例表增加当前节点处理进度与历史(支持会签/或签/依次)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `node_progress` longtext COMMENT '当前节点处理进度JSON(nodeId/mode/tasks)' AFTER `current_handlers_text`,
|
||||
ADD COLUMN `history` longtext COMMENT '审批历史JSON数组' AFTER `node_progress`;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 【QH-MES审批流设计】驳回/撤销恢复初始状态:审批流配置状态字段名,实例快照发起时业务状态原值
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(驳回/撤销时回写其发起时原值)' AFTER `route_path`;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(发起时快照)' AFTER `biz_title`,
|
||||
ADD COLUMN `origin_status` varchar(64) DEFAULT NULL COMMENT '发起审批时业务状态原值(驳回/撤销回写)' AFTER `status_field`;
|
||||
|
||||
-- 密炼PS编制默认以 status 字段作为可恢复状态字段
|
||||
UPDATE `mes_xsl_approval_flow`
|
||||
SET `status_field` = 'status'
|
||||
WHERE `biz_table` = 'mes_xsl_mixer_ps_compile' AND (`status_field` IS NULL OR `status_field` = '');
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 【QH-MES审批流完善】①乐观锁防并发 ②超时配置 ③催办记录时间
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 审批实例:乐观锁版本号,防止多人同时审批导致进度覆盖写
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER `remark`;
|
||||
|
||||
-- 审批流定义:超时提醒小时数(0=不提醒),默认24h
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `timeout_hours` int(11) NOT NULL DEFAULT 24 COMMENT '超时提醒小时数(0=不提醒)' AFTER `status_field`;
|
||||
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
全局「审批流程设计」悬浮按钮
|
||||
拥有 approval:flow:design 权限的用户,在任意功能页点击即可:
|
||||
1)后端按当前页路由反查绑定的业务表;
|
||||
2)解析该表字段,识别「校对/审核/审批/分发/抄送」等阶段字段(不存在不报错);
|
||||
3)进入可视化设计器,可点选识别到的阶段字段按顺序生成审批流程并保存发布。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-design-float" :style="floatStyle">
|
||||
<div class="approval-design-btn" :class="{ 'is-loading': loading }" title="审批流程设计" @click="openDesigner">
|
||||
<Icon :icon="loading ? 'ant-design:loading-outlined' : 'ant-design:partition-outlined'" :size="20" :spin="loading" />
|
||||
<span class="approval-design-text">流程设计</span>
|
||||
</div>
|
||||
<!-- 流程设计器(全屏) -->
|
||||
<FlowDesign @register="registerDesign" @success="onSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { getApprovalDesignContext } from '/@/views/approval/flow/approvalFlow.api';
|
||||
import FlowDesign from '/@/views/approval/flow/components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalDesignFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const { hasPermission } = usePermission();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const loading = ref(false);
|
||||
// 悬浮位置(位于「发起审批」按钮上方)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '190px' });
|
||||
|
||||
// 仅拥有设计权限的用户可见
|
||||
const show = computed(() => hasPermission('approval:flow:design'));
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async function openDesigner() {
|
||||
if (loading.value) return;
|
||||
const path = normalizePath(currentRoute.value?.path);
|
||||
if (!path) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const ctx: any = await getApprovalDesignContext(path);
|
||||
if (!ctx || !ctx.bizTable || !ctx.flow) {
|
||||
createMessage.info('当前页面未能识别到可绑定的业务单据,无法设计审批流程');
|
||||
return;
|
||||
}
|
||||
openDesign(true, {
|
||||
record: ctx.flow,
|
||||
readonly: false,
|
||||
paletteStages: ctx.stages || [],
|
||||
});
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取设计上下文失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
createMessage.success('审批流程已保存');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-design-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-design-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #15bca3, #0e9e88);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(21, 188, 163, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-design-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(21, 188, 163, 0.6);
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<!--
|
||||
全局「发起审批」悬浮按钮
|
||||
仅在「设计并发布了审批流、且能匹配到对应功能页路由」的页面显示。
|
||||
支持两种发起方式:
|
||||
1)列表多选联动:在列表勾选数据后点击,弹窗自动带入选中单据并可批量发起;
|
||||
2)手动选择:未勾选时,在弹窗内搜索选择单条单据发起。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-float" :style="floatStyle">
|
||||
<div class="approval-float-btn" title="发起审批" @click="openModal">
|
||||
<Icon icon="ant-design:audit-outlined" :size="20" />
|
||||
<span class="approval-float-text">发起审批</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="visible" title="发起审批" :width="540" :confirmLoading="loading" okText="发起审批" @ok="handleLaunch">
|
||||
<a-form layout="vertical" style="margin-top: 8px">
|
||||
<a-form-item label="单据类型(审批流)" required>
|
||||
<a-select v-model:value="flowId" placeholder="请选择审批流" :options="flowOptions" @change="onFlowChange" allowClear />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 批量模式:直接展示列表勾选的单据 -->
|
||||
<a-form-item v-if="isBatch" :label="`已选单据(共 ${batchItems.length} 条)`" required>
|
||||
<div class="approval-float-batch">
|
||||
<div v-for="it in batchItems" :key="it.bizDataId" class="approval-float-batch-item">
|
||||
<Icon icon="ant-design:file-text-outlined" :size="14" />
|
||||
<span class="approval-float-batch-title">{{ it.bizTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 手动模式:搜索选择单条单据 -->
|
||||
<a-form-item v-else label="选择单据" required>
|
||||
<a-select
|
||||
v-model:value="bizDataId"
|
||||
show-search
|
||||
placeholder="请选择需要发起审批的单据"
|
||||
:filter-option="false"
|
||||
:options="recordOptions"
|
||||
:disabled="!flowId"
|
||||
@search="onSearch"
|
||||
@change="onRecordChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<a-spin v-if="recordLoading" size="small" />
|
||||
<span v-else>无单据数据</span>
|
||||
</template>
|
||||
</a-select>
|
||||
<div v-if="flowId && !recordOptions.length && !recordLoading" class="approval-float-tip">
|
||||
该单据暂无数据,或审批流未配置“单据标题字段”
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getPublishedFlows, getBizRecords, launchApproval, launchApprovalBatch } from '/@/views/approval/flow/launch.api';
|
||||
import { useApprovalSelection } from './useApprovalSelection';
|
||||
|
||||
defineOptions({ name: 'ApprovalLaunchFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const approvalSelection = useApprovalSelection();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordLoading = ref(false);
|
||||
|
||||
const flowId = ref<string>();
|
||||
const bizDataId = ref<string>();
|
||||
const bizTitle = ref<string>('');
|
||||
|
||||
const flowList = ref<any[]>([]);
|
||||
const recordList = ref<any[]>([]);
|
||||
// 打开弹窗时快照的列表勾选行(批量模式数据源)
|
||||
const batchRows = ref<any[]>([]);
|
||||
|
||||
// 悬浮位置(右下角)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '120px' });
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// 当前路由匹配到的已发布审批流
|
||||
const matchedFlows = computed(() => {
|
||||
const cur = normalizePath(currentRoute.value?.path);
|
||||
if (!cur) return [];
|
||||
return flowList.value.filter((f) => f.routePath && normalizePath(f.routePath) === cur);
|
||||
});
|
||||
|
||||
const show = computed(() => matchedFlows.value.length > 0);
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
matchedFlows.value.map((f) => ({
|
||||
label: `${f.flowName}(${f.bizTableName || f.bizTable})`,
|
||||
value: f.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// 当前选中的审批流对象
|
||||
const currentFlow = computed(() => matchedFlows.value.find((f) => f.id === flowId.value));
|
||||
|
||||
// 是否批量模式(列表有勾选)
|
||||
const isBatch = computed(() => batchRows.value.length > 0);
|
||||
|
||||
// 批量模式下的单据项(用审批流的标题字段取展示标题)
|
||||
const batchItems = computed(() => {
|
||||
const titleField = currentFlow.value?.titleField;
|
||||
return batchRows.value
|
||||
.filter((r) => r && r.id != null)
|
||||
.map((r) => ({
|
||||
bizDataId: String(r.id),
|
||||
bizTitle: titleField && r[titleField] != null ? String(r[titleField]) : String(r.id),
|
||||
}));
|
||||
});
|
||||
|
||||
const recordOptions = computed(() =>
|
||||
recordList.value.map((r) => ({
|
||||
label: r.title ?? r.id,
|
||||
value: r.id,
|
||||
}))
|
||||
);
|
||||
|
||||
onMounted(loadFlows);
|
||||
|
||||
async function loadFlows() {
|
||||
try {
|
||||
flowList.value = (await getPublishedFlows()) || [];
|
||||
} catch {
|
||||
flowList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
flowId.value = undefined;
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
batchRows.value = [];
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
visible.value = true;
|
||||
resetSelection();
|
||||
// 读取当前列表页的勾选行
|
||||
batchRows.value = approvalSelection.getRowsByPath(currentRoute.value?.path || '');
|
||||
// 当前页只匹配到一个审批流时自动选中
|
||||
if (matchedFlows.value.length === 1) {
|
||||
flowId.value = matchedFlows.value[0].id;
|
||||
}
|
||||
// 手动模式且已确定审批流时预加载单据列表
|
||||
if (!isBatch.value && flowId.value) {
|
||||
await loadRecords();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(keyword?: string) {
|
||||
if (!flowId.value) return;
|
||||
try {
|
||||
recordLoading.value = true;
|
||||
recordList.value = (await getBizRecords({ flowId: flowId.value, keyword })) || [];
|
||||
} finally {
|
||||
recordLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onFlowChange() {
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
if (!isBatch.value && flowId.value) loadRecords();
|
||||
}
|
||||
|
||||
const onSearch = debounce((val: string) => {
|
||||
loadRecords(val);
|
||||
}, 350);
|
||||
|
||||
function onRecordChange() {
|
||||
const hit = recordList.value.find((r) => r.id === bizDataId.value);
|
||||
bizTitle.value = hit ? hit.title ?? hit.id : '';
|
||||
}
|
||||
|
||||
async function handleLaunch() {
|
||||
if (!flowId.value) {
|
||||
createMessage.warning('请选择审批流');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
if (isBatch.value) {
|
||||
await launchApprovalBatch({ flowId: flowId.value, items: batchItems.value });
|
||||
createMessage.success(`已发起 ${batchItems.value.length} 条审批!`);
|
||||
} else {
|
||||
if (!bizDataId.value) {
|
||||
createMessage.warning('请选择需要发起审批的单据');
|
||||
return;
|
||||
}
|
||||
await launchApproval({ flowId: flowId.value, bizDataId: bizDataId.value, bizTitle: bizTitle.value });
|
||||
createMessage.success('发起成功!');
|
||||
}
|
||||
visible.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-float-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3296fa, #1668dc);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(50, 150, 250, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-float-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(50, 150, 250, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-batch {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.approval-float-batch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 2px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.approval-float-batch-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #faad14;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 审批发起-列表选中上下文(全局单例)
|
||||
* 由 useListPage 自动把当前列表页的选中行同步进来,
|
||||
* 全局「发起审批」悬浮按钮发起时直接读取,实现"列表多选 -> 发起弹窗"联动。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批支持列表多选联动
|
||||
*/
|
||||
// 当前选中的行记录
|
||||
const rows = ref<any[]>([]);
|
||||
// 选中来源页面路由,用于校验与当前页是否一致,避免跨页串数据
|
||||
const sourcePath = ref<string>('');
|
||||
|
||||
export function useApprovalSelection() {
|
||||
function setSelection(list: any[], path: string) {
|
||||
rows.value = Array.isArray(list) ? [...list] : [];
|
||||
sourcePath.value = path || '';
|
||||
}
|
||||
|
||||
function clear() {
|
||||
rows.value = [];
|
||||
sourcePath.value = '';
|
||||
}
|
||||
|
||||
/** 获取与指定路由匹配的选中行(不匹配返回空) */
|
||||
function getRowsByPath(path: string): any[] {
|
||||
return sourcePath.value === path ? rows.value : [];
|
||||
}
|
||||
|
||||
return { rows, sourcePath, setSelection, clear, getRowsByPath };
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { filterObj } from '/@/utils/common/compUtils';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { registerImPageListProvider } from '/@/views/system/im/imPageListRegistry';
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
import { useApprovalSelection } from '/@/components/ApprovalLaunch/useApprovalSelection';
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文-----
|
||||
import { buildImPageListSnapshot } from '/@/views/system/im/imPageListUtil';
|
||||
import { IM_RECORD_QUERY_KEY } from '/@/views/system/im/imBizRecordMessage';
|
||||
import {
|
||||
@@ -71,7 +74,7 @@ export function useListPage(options: ListPageOptions) {
|
||||
const tableContext = useListTable(options.tableProps);
|
||||
|
||||
const route = useRoute();
|
||||
const [, tableMethods, { selectedRowKeys }] = tableContext;
|
||||
const [, tableMethods, { selectedRowKeys, selectedRows }] = tableContext;
|
||||
const { getForm, reload, setLoading, getColumns } = tableMethods;
|
||||
const imHighlightRecordId = ref('');
|
||||
let clearHighlightTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -84,6 +87,16 @@ export function useListPage(options: ListPageOptions) {
|
||||
}
|
||||
});
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文,供全局"发起审批"悬浮按钮读取-----
|
||||
const approvalSelection = useApprovalSelection();
|
||||
watch(
|
||||
selectedRows,
|
||||
(rows) => approvalSelection.setSelection((rows as any[]) || [], route.path),
|
||||
{ deep: true },
|
||||
);
|
||||
onUnmounted(() => approvalSelection.clear());
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】列表选中行同步到审批发起上下文,供全局"发起审批"悬浮按钮读取-----
|
||||
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
|
||||
onUnmounted(
|
||||
registerImPageListProvider(() => {
|
||||
|
||||
@@ -10,6 +10,15 @@ import { useUserStore } from '/@/store/modules/user';
|
||||
let result: WebSocketResult<any>;
|
||||
const listeners = new Map();
|
||||
let connectedUrl = '';
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||
let wsConnectionCount = 0;
|
||||
const reconnectListeners = new Set<() => void>();
|
||||
|
||||
export function onWebSocketReconnect(callback: () => void) {
|
||||
reconnectListeners.add(callback);
|
||||
return () => reconnectListeners.delete(callback);
|
||||
}
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||
|
||||
/**
|
||||
* 构建系统 WebSocket 地址(含 context-path,如 /jeecg-boot)
|
||||
@@ -67,7 +76,15 @@ export function connectWebSocket(url: string) {
|
||||
protocols: [token],
|
||||
// 代码逻辑说明: [issues/6662] 演示系统socket总断,换一个写法
|
||||
onConnected: function (ws) {
|
||||
wsConnectionCount++;
|
||||
console.log('[WebSocket] 连接成功', ws);
|
||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||
if (wsConnectionCount > 1) {
|
||||
reconnectListeners.forEach((cb) => {
|
||||
try { cb(); } catch (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||
},
|
||||
onDisconnected: function (ws, event) {
|
||||
console.log('[WebSocket] 连接断开:', ws, event);
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<LayoutFooter />
|
||||
</Layout>
|
||||
</Layout>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮----- -->
|
||||
<ApprovalLaunchFloat />
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮----- -->
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮----- -->
|
||||
<ApprovalDesignFloat />
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮----- -->
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +42,12 @@
|
||||
components: {
|
||||
LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
|
||||
LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮-----
|
||||
ApprovalLaunchFloat: createAsyncComponent(() => import('/@/components/ApprovalLaunch/index.vue')),
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局发起审批悬浮按钮-----
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
|
||||
ApprovalDesignFloat: createAsyncComponent(() => import('/@/components/ApprovalDesign/index.vue')),
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
|
||||
LayoutHeader,
|
||||
LayoutContent,
|
||||
LayoutSideBar,
|
||||
|
||||
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
审批流设计 列表页
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd" v-auth="'approval:flow:add'">新增审批流</a-button>
|
||||
<a-button
|
||||
v-if="selectedRowKeys.length > 0"
|
||||
danger
|
||||
preIcon="ant-design:delete-outlined"
|
||||
@click="handleBatchDelete"
|
||||
v-auth="'approval:flow:delete'"
|
||||
style="margin-left: 8px"
|
||||
>批量删除</a-button
|
||||
>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getActions(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 基本信息弹窗 -->
|
||||
<ApprovalFlowModal @register="registerModal" @success="reload" />
|
||||
<!-- 流程设计器 -->
|
||||
<FlowDesign @register="registerDesign" @success="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './approvalFlow.data';
|
||||
import { getApprovalFlowList, deleteApprovalFlow, batchDeleteApprovalFlow, updateApprovalFlowStatus } from './approvalFlow.api';
|
||||
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
||||
import FlowDesign from './components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowList' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '审批流列表',
|
||||
api: getApprovalFlowList,
|
||||
columns,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
labelWidth: 90,
|
||||
},
|
||||
actionColumn: {
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
},
|
||||
showIndexColumn: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false });
|
||||
}
|
||||
|
||||
function handleEdit(record) {
|
||||
openModal(true, { isUpdate: true, record });
|
||||
}
|
||||
|
||||
// 打开可视化设计器
|
||||
function handleDesign(record, readonly = false) {
|
||||
openDesign(true, { record, readonly });
|
||||
}
|
||||
|
||||
function handleDelete(record) {
|
||||
deleteApprovalFlow({ id: record.id }, reload);
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
batchDeleteApprovalFlow({ ids: selectedRowKeys.value.join(',') }, reload);
|
||||
}
|
||||
|
||||
// 发布 / 停用
|
||||
async function handleToggleStatus(record) {
|
||||
const target = record.status === '1' ? '2' : '1';
|
||||
await updateApprovalFlowStatus({ id: record.id, status: target });
|
||||
createMessage.success(target === '1' ? '已发布' : '已停用');
|
||||
reload();
|
||||
}
|
||||
|
||||
function getActions(record) {
|
||||
return [
|
||||
{
|
||||
label: '设计',
|
||||
auth: 'approval:flow:design',
|
||||
onClick: handleDesign.bind(null, record, false),
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
auth: 'approval:flow:edit',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: record.status === '1' ? '停用' : '发布',
|
||||
auth: 'approval:flow:design',
|
||||
popConfirm: {
|
||||
title: `确认${record.status === '1' ? '停用' : '发布'}该审批流?`,
|
||||
confirm: handleToggleStatus.bind(null, record),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
auth: 'approval:flow:delete',
|
||||
popConfirm: {
|
||||
title: '确认删除该审批流?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
审批流 基本信息 新增/编辑弹窗(先选单据)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="560" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from './approvalFlow.data';
|
||||
import { saveOrUpdateApprovalFlow, getApprovalFlowById } from './approvalFlow.api';
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
|
||||
const isUpdate = ref(true);
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑审批流' : '新增审批流'));
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
labelWidth: 100,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data?.record?.id) {
|
||||
const record = await getApprovalFlowById({ id: data.record.id });
|
||||
await setFieldsValue({ ...record });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdateApprovalFlow(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
86
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
86
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批流设计 API
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
enum Api {
|
||||
list = '/xslmes/approvalFlow/list',
|
||||
save = '/xslmes/approvalFlow/add',
|
||||
edit = '/xslmes/approvalFlow/edit',
|
||||
get = '/xslmes/approvalFlow/queryById',
|
||||
saveDesign = '/xslmes/approvalFlow/saveDesign',
|
||||
updateStatus = '/xslmes/approvalFlow/updateStatus',
|
||||
delete = '/xslmes/approvalFlow/delete',
|
||||
deleteBatch = '/xslmes/approvalFlow/deleteBatch',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
designContext = '/xslmes/approvalFlow/designContext',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
bizActions = '/xslmes/approvalFlow/bizActions',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*/
|
||||
export const getApprovalFlowList = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
/**
|
||||
* 新增/编辑基本信息
|
||||
*/
|
||||
export const saveOrUpdateApprovalFlow = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过 id 查询(含流程设计 JSON)
|
||||
*/
|
||||
export const getApprovalFlowById = (params) => defHttp.get({ url: Api.get, params });
|
||||
|
||||
/**
|
||||
* 保存流程设计(节点树 JSON)
|
||||
*/
|
||||
export const saveApprovalFlowDesign = (params) => defHttp.post({ url: Api.saveDesign, params });
|
||||
|
||||
/**
|
||||
* 发布 / 停用
|
||||
*/
|
||||
export const updateApprovalFlowStatus = (params) => defHttp.post({ url: Api.updateStatus, params }, { joinParamsToUrl: true });
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const deleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
export const batchDeleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
/**
|
||||
* 获取当前功能页的审批流设计上下文:
|
||||
* 返回 { routePath, bizTable, bizTableName, stages[], flow },
|
||||
* stages 为识别到的阶段字段(校对/审核/审批/分发/抄送),flow 为可直接进入设计器的流程记录。
|
||||
*/
|
||||
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作(后端@ApprovalBizAction注解扫描)-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供节点「回调接口」下拉选择。
|
||||
* 返回 [{ name, url, method, table, phase, perms }]
|
||||
*/
|
||||
export const getApprovalBizActions = (table: string) => defHttp.get({ url: Api.bizActions, params: { table } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
/**
|
||||
* 审批流设计 列表列 / 表单 schema
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '审批流名称',
|
||||
dataIndex: 'flowName',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '绑定单据',
|
||||
dataIndex: 'bizTableName',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status_dictText',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_flow_status',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '主键',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入审批流名称',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
placeholder: '请先选择需要审批的单据',
|
||||
},
|
||||
// 编辑时禁止修改绑定单据,避免已设计的节点条件与单据字段错配
|
||||
dynamicDisabled: ({ values }) => !!values.id,
|
||||
},
|
||||
{
|
||||
field: 'titleField',
|
||||
label: '单据标题字段',
|
||||
component: 'Input',
|
||||
helpMessage: '发起审批时用于展示具体单据的字段名(如 spec_name、code),不填则只显示单据ID',
|
||||
componentProps: {
|
||||
placeholder: '选填,业务表中用于展示的字段名,如 spec_name',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
maxlength: 500,
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
33
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
33
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批办理/流转 API(供 IM 审批卡片按钮调用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
enum Api {
|
||||
detail = '/xslmes/approvalHandle/detail',
|
||||
status = '/xslmes/approvalHandle/status',
|
||||
approve = '/xslmes/approvalHandle/approve',
|
||||
reject = '/xslmes/approvalHandle/reject',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
cancel = '/xslmes/approvalHandle/cancel',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
}
|
||||
|
||||
/** 查看单据全部字段 + 审批进度/历史 */
|
||||
export const getApprovalDetail = (instanceId: string) => defHttp.get({ url: Api.detail, params: { instanceId } });
|
||||
|
||||
/** 轻量实时状态:用于卡片判断是否仍可办理(旧节点卡片置灰) */
|
||||
export const getApprovalStatus = (instanceId: string) => defHttp.get({ url: Api.status, params: { instanceId } });
|
||||
|
||||
/** 审批通过(按节点 multiMode 流转到下一处理人/节点) */
|
||||
export const approveApproval = (params: { instanceId: string; comment?: string }) => defHttp.post({ url: Api.approve, data: params });
|
||||
|
||||
/** 驳回(需填写理由) */
|
||||
export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params });
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
/** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */
|
||||
export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
192
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
192
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
钉钉式审批流 可视化设计器(全屏)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave">
|
||||
<div class="fd-design">
|
||||
<div class="fd-toolbar">
|
||||
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
||||
<span class="fd-tb-item">审批流:<b>{{ record.flowName }}</b></span>
|
||||
<span class="fd-tb-tip">点击节点可配置,点击节点间的「+」可插入审批人 / 抄送人 / 条件分支</span>
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
<div class="fd-body">
|
||||
<div class="fd-palette" v-if="!readonly && paletteStages.length">
|
||||
<div class="fd-palette-title">当前页识别到的审批阶段</div>
|
||||
<div class="fd-palette-tip">点击下方阶段,按顺序追加到流程末尾;处理人将取自单据对应字段。</div>
|
||||
<div class="fd-palette-list">
|
||||
<div v-for="s in paletteStages" :key="s.stageKey" class="fd-palette-item" :class="'fd-palette-' + s.nodeType" @click="appendStageNode(s)">
|
||||
<div class="fd-palette-item-name">{{ s.stageName }}</div>
|
||||
<div class="fd-palette-item-field">{{ s.fieldComment || s.field }}</div>
|
||||
<Icon icon="ant-design:plus-circle-outlined" class="fd-palette-item-add" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-canvas">
|
||||
<div class="fd-flow" v-if="root">
|
||||
<FlowNode :node="root" />
|
||||
<div class="fd-end">
|
||||
<div class="fd-end-dot"></div>
|
||||
<span>流程结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
</div>
|
||||
<NodeConfigDrawer ref="drawerRef" :readonly="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import FlowNode from './FlowNode.vue';
|
||||
import NodeConfigDrawer from './NodeConfigDrawer.vue';
|
||||
import {
|
||||
createStartNode,
|
||||
createApproverNode,
|
||||
createCcNode,
|
||||
createConditionNode,
|
||||
createStageNode,
|
||||
insertAfter,
|
||||
removeNode,
|
||||
addBranch,
|
||||
} from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
||||
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowDesign' });
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const root = ref<FlowNodeType | null>(null);
|
||||
const readonly = ref(false);
|
||||
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
||||
const drawerRef = ref();
|
||||
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
|
||||
const bizTableRef = ref('');
|
||||
provide('approvalBizTable', bizTableRef);
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
const paletteStages = ref<StageField[]>([]);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
|
||||
const modalTitle = computed(() => (readonly.value ? '查看审批流' : '设计审批流'));
|
||||
|
||||
// 设计器上下文,提供给递归 FlowNode 使用
|
||||
const flowCtx = reactive({
|
||||
readonly: false,
|
||||
onSelect: (node: FlowNodeType) => {
|
||||
drawerRef.value?.openDrawer(node);
|
||||
},
|
||||
onInsert: (prevId: string, type: NodeType) => {
|
||||
if (!root.value) return;
|
||||
const factory: Record<string, () => FlowNodeType> = {
|
||||
approver: createApproverNode,
|
||||
cc: createCcNode,
|
||||
condition: createConditionNode,
|
||||
};
|
||||
const node = factory[type]?.();
|
||||
if (node) insertAfter(root.value, prevId, node);
|
||||
},
|
||||
onDelete: (id: string) => {
|
||||
if (root.value) removeNode(root.value, id);
|
||||
},
|
||||
addBranch: (conditionId: string) => {
|
||||
if (root.value) addBranch(root.value, conditionId);
|
||||
},
|
||||
});
|
||||
|
||||
// 通过 provide 注入,供递归 FlowNode 调用(避免逐层透传)
|
||||
provide('flowCtx', flowCtx);
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
readonly.value = !!data?.readonly;
|
||||
flowCtx.readonly = readonly.value;
|
||||
bizTableRef.value = data?.record?.bizTable || '';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
const id = data?.record?.id || '';
|
||||
Object.assign(record, {
|
||||
id,
|
||||
flowName: data?.record?.flowName || '',
|
||||
bizTable: data?.record?.bizTable || '',
|
||||
bizTableName: data?.record?.bizTableName || '',
|
||||
});
|
||||
// 列表接口未返回大字段 flow_config,需按 id 重新查询完整记录
|
||||
let cfg = data?.record?.flowConfig;
|
||||
if (id) {
|
||||
try {
|
||||
setModalProps({ loading: true });
|
||||
const full = await getApprovalFlowById({ id });
|
||||
if (full) {
|
||||
cfg = full.flowConfig;
|
||||
Object.assign(record, {
|
||||
flowName: full.flowName || record.flowName,
|
||||
bizTable: full.bizTable || record.bizTable,
|
||||
bizTableName: full.bizTableName || record.bizTableName,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setModalProps({ loading: false });
|
||||
}
|
||||
}
|
||||
// 解析已有流程设计,无则初始化一个发起人节点
|
||||
root.value = parseConfig(cfg);
|
||||
});
|
||||
|
||||
function parseConfig(cfg?: string): FlowNodeType {
|
||||
if (cfg) {
|
||||
try {
|
||||
const obj = JSON.parse(cfg);
|
||||
if (obj && obj.type) return obj;
|
||||
} catch (e) {
|
||||
console.warn('[审批流设计] flowConfig 解析失败,重置为默认', e);
|
||||
}
|
||||
}
|
||||
return createStartNode();
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
/** 将候选阶段追加到主流程链末尾(沿 childNode 找到尾节点后插入) */
|
||||
function appendStageNode(stage: StageField) {
|
||||
if (!root.value || readonly.value) return;
|
||||
let tail: FlowNodeType = root.value;
|
||||
while (tail.childNode) {
|
||||
tail = tail.childNode;
|
||||
}
|
||||
const node = createStageNode(stage);
|
||||
insertAfter(root.value, tail.id, node);
|
||||
createMessage.success(`已添加「${stage.stageName}」节点`);
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
|
||||
async function handleSave() {
|
||||
if (!record.id) {
|
||||
createMessage.error('缺少审批流ID,无法保存');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveApprovalFlowDesign({
|
||||
id: record.id,
|
||||
flowConfig: JSON.stringify(root.value),
|
||||
status: '1',
|
||||
});
|
||||
createMessage.success('流程设计已保存并发布');
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './flow.less';
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
钉钉式审批流 递归节点组件
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="fd-node">
|
||||
<!-- 条件分支节点:渲染多列分支 -->
|
||||
<div v-if="node.type === 'condition'" class="fd-branches">
|
||||
<div class="fd-add-branch-wrap">
|
||||
<a-button size="small" type="primary" ghost class="fd-add-branch" @click="ctx.addBranch(node.id)">+ 条件</a-button>
|
||||
</div>
|
||||
<div class="fd-branch-cols">
|
||||
<div class="fd-branch-col" v-for="(b, idx) in node.conditionNodes" :key="b.id">
|
||||
<!-- 首末列外侧连线遮罩,形成分支汇聚效果 -->
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-tl"></div>
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-bl"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-tr"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-br"></div>
|
||||
<div class="fd-branch-inner">
|
||||
<FlowNode :node="b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 普通卡片:start / approver / cc / branch -->
|
||||
<div v-else class="fd-card-wrap">
|
||||
<div class="fd-card" :class="['fd-' + node.type, { 'fd-error': isPlaceholder }]" @click="ctx.onSelect(node)">
|
||||
<div class="fd-card-header">
|
||||
<span class="fd-title">{{ node.name }}</span>
|
||||
<span v-if="node.type === 'branch' && !node.props.isDefault" class="fd-priority">优先级 {{ node.props.priorityLevel }}</span>
|
||||
<Icon v-if="canDelete && !ctx.readonly" icon="ant-design:close-outlined" class="fd-del" @click.stop="ctx.onDelete(node.id)" />
|
||||
</div>
|
||||
<div class="fd-card-body" :class="{ 'fd-placeholder': isPlaceholder }">{{ summary }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加号:在当前节点之后插入新节点 -->
|
||||
<div v-if="!ctx.readonly" class="fd-add">
|
||||
<a-popover trigger="click" placement="rightTop" v-model:open="addOpen" :overlayClassName="'fd-add-pop'">
|
||||
<template #content>
|
||||
<div class="fd-add-menu">
|
||||
<div class="fd-add-item fd-add-approver" @click="add('approver')">
|
||||
<Icon icon="ant-design:audit-outlined" /> <span>审批人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-cc" @click="add('cc')">
|
||||
<Icon icon="ant-design:mail-outlined" /> <span>抄送人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-condition" @click="add('condition')">
|
||||
<Icon icon="ant-design:share-alt-outlined" /> <span>条件分支</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button class="fd-add-btn" title="添加节点"><Icon icon="ant-design:plus-outlined" /></button>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<!-- 后续链(递归) -->
|
||||
<FlowNode v-if="node.childNode" :node="node.childNode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { nodeSummary } from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType } from './flowTypes';
|
||||
|
||||
defineOptions({ name: 'FlowNode' });
|
||||
|
||||
const props = defineProps<{ node: FlowNodeType }>();
|
||||
|
||||
// 注入设计器上下文
|
||||
const ctx = inject<any>('flowCtx', {
|
||||
readonly: false,
|
||||
onSelect: () => {},
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
addBranch: () => {},
|
||||
});
|
||||
|
||||
const addOpen = ref(false);
|
||||
|
||||
const summary = computed(() => nodeSummary(props.node));
|
||||
|
||||
// 条件分支最后一列索引(用于首末列连线遮罩)
|
||||
const lastBranchIndex = computed(() => (props.node.conditionNodes?.length || 0) - 1);
|
||||
|
||||
// 摘要为"请设置..."视为未配置(红色提示)
|
||||
const isPlaceholder = computed(() => summary.value.startsWith('请'));
|
||||
|
||||
// 发起人节点不可删除
|
||||
const canDelete = computed(() => props.node.type !== 'start');
|
||||
|
||||
function add(type: NodeType) {
|
||||
ctx.onInsert(props.node.id, type);
|
||||
addOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,381 @@
|
||||
<!--
|
||||
审批流节点配置抽屉
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<a-drawer :title="title" :width="480" :open="open" @close="onClose" :maskClosable="!readonly">
|
||||
<template v-if="form">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="节点名称">
|
||||
<a-input v-model:value="form.name" :disabled="readonly || node?.type === 'start'" placeholder="请输入节点名称" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发起人 -->
|
||||
<template v-if="node?.type === 'start'">
|
||||
<a-form-item label="可发起人员">
|
||||
<a-radio-group v-model:value="form.props.initiatorType" :disabled="readonly">
|
||||
<a-radio value="all">所有人</a-radio>
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === 'role'" label="指定角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 审批人 -->
|
||||
<template v-else-if="node?.type === 'approver'">
|
||||
<a-form-item label="审批人类型">
|
||||
<a-radio-group v-model:value="form.props.approverType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
<a-radio value="leader">主管</a-radio>
|
||||
<a-radio value="self">发起人自己</a-radio>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<template v-if="form.props.approverType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,处理人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:校对人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:proofreader" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<a-form-item v-if="form.props.approverType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'role'" label="指定角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'leader'" label="主管层级">
|
||||
<a-select v-model:value="form.props.leaderLevel" :disabled="readonly" style="width: 160px">
|
||||
<a-select-option :value="1">直接主管</a-select-option>
|
||||
<a-select-option :value="2">第2级主管</a-select-option>
|
||||
<a-select-option :value="3">第3级主管</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="多人审批方式">
|
||||
<a-radio-group v-model:value="form.props.multiMode" :disabled="readonly">
|
||||
<a-radio value="and">会签(需全部同意)</a-radio>
|
||||
<a-radio value="or">或签(一人同意)</a-radio>
|
||||
<a-radio value="sequence">依次审批</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批人为空时">
|
||||
<a-radio-group v-model:value="form.props.emptyStrategy" :disabled="readonly">
|
||||
<a-radio value="admin">转交管理员</a-radio>
|
||||
<a-radio value="pass">自动通过</a-radio>
|
||||
<a-radio value="stop">终止流程</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(自动识别页面按钮)----- -->
|
||||
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
:message="
|
||||
pageActionOptions.length
|
||||
? '以下为该业务已标注(@ApprovalBizAction)的可选接口。审批到对应时机时,系统会以「当前处理人」身份调用所选接口(自动带上单据ID)。'
|
||||
: '该业务暂未标注可选接口(在后端 Controller 方法上加 @ApprovalBizAction 注解即可出现在此),也可手动填写接口路径。'
|
||||
"
|
||||
/>
|
||||
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
||||
<div class="fd-cb-title">{{ phase.label }}</div>
|
||||
<div v-for="(a, i) in form.props.callbackActions[phase.key]" :key="i" class="fd-cb-row">
|
||||
<a-input v-model:value="a.name" placeholder="动作名" :disabled="readonly" style="width: 88px" />
|
||||
<a-select v-model:value="a.method" :disabled="readonly" style="width: 82px" :options="methodOptions" />
|
||||
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||
</div>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px" :size="6" wrap>
|
||||
<a-select
|
||||
v-if="pageActionOptions.length"
|
||||
style="width: 240px"
|
||||
placeholder="从页面按钮中选择…"
|
||||
:options="pageActionOptions"
|
||||
v-model:value="actionPicker[phase.key]"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@select="(v) => addFromButton(phase.key, v)"
|
||||
/>
|
||||
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||
<a-alert
|
||||
type="success"
|
||||
show-icon
|
||||
style="margin-top: 4px"
|
||||
message="驳回 / 撤销 已全局统一:系统会自动执行该业务标注为「驳回时执行(@ApprovalBizAction onReject)」的接口完成回退,无需在此逐节点、逐流程配置。"
|
||||
/>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||
</template>
|
||||
|
||||
<!-- 抄送人 -->
|
||||
<template v-else-if="node?.type === 'cc'">
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
<a-form-item label="抄送人来源">
|
||||
<a-radio-group v-model:value="form.props.ccType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员/角色</a-radio>
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="form.props.ccType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,抄送人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:分发人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:distributor" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="抄送成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item label="抄送角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="form.props.allowEditCc" :disabled="readonly">允许审批人自行添加抄送人</a-checkbox>
|
||||
</a-form-item>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
</template>
|
||||
|
||||
<!-- 条件分支 -->
|
||||
<template v-else-if="node?.type === 'branch'">
|
||||
<template v-if="form.props.isDefault">
|
||||
<a-alert type="info" show-icon message="“其它情况”分支:当以上条件均不满足时进入此分支,无需配置条件。" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="条件关系">
|
||||
<a-radio-group v-model:value="form.props.logic" :disabled="readonly">
|
||||
<a-radio value="and">且(同时满足)</a-radio>
|
||||
<a-radio value="or">或(满足其一)</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="条件设置">
|
||||
<div v-for="(c, i) in form.props.conditions" :key="i" class="fd-cond-row">
|
||||
<a-input v-model:value="c.label" placeholder="字段中文名" :disabled="readonly" style="width: 110px" />
|
||||
<a-input v-model:value="c.field" placeholder="字段名" :disabled="readonly" style="width: 110px" />
|
||||
<a-select v-model:value="c.operator" :disabled="readonly" style="width: 100px" :options="operatorOptions" />
|
||||
<a-input v-if="!['empty', 'notEmpty'].includes(c.operator)" v-model:value="c.value" placeholder="值" :disabled="readonly" style="width: 90px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cond-del" @click="removeCond(i)" />
|
||||
</div>
|
||||
<a-button v-if="!readonly" type="dashed" block @click="addCond" style="margin-top: 8px">+ 添加条件</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<template #footer v-if="!readonly">
|
||||
<a-space>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, inject, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { ApiSelect } from '/@/components/Form';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||
import type { FlowNode } from './flowTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApprovalBizActions } from '../approvalFlow.api';
|
||||
|
||||
const props = defineProps<{ readonly?: boolean }>();
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
// 当前审批流绑定的业务表(由 FlowDesign 注入),据此向后端查可选回调动作
|
||||
const bizTable = inject<Ref<string>>('approvalBizTable', ref(''));
|
||||
// 后端 @ApprovalBizAction 标注的可选业务动作
|
||||
const bizActions = ref<any[]>([]);
|
||||
const bizActionsTable = ref('');
|
||||
|
||||
async function loadBizActions() {
|
||||
const table = bizTable.value || '';
|
||||
if (!table || bizActionsTable.value === table) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getApprovalBizActions(table);
|
||||
bizActions.value = Array.isArray(res) ? res : [];
|
||||
bizActionsTable.value = table;
|
||||
} catch (e) {
|
||||
bizActions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 可选动作下拉项(含真实 url/method)
|
||||
const pageActionOptions = computed(() =>
|
||||
(bizActions.value || []).map((a) => ({
|
||||
label: `${a.name}(${a.method} ${a.url})`,
|
||||
value: a.url,
|
||||
raw: a,
|
||||
})),
|
||||
);
|
||||
|
||||
watch(
|
||||
bizTable,
|
||||
() => {
|
||||
loadBizActions();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const open = ref(false);
|
||||
const node = ref<FlowNode | null>(null);
|
||||
const form = ref<any>(null);
|
||||
// 「从页面按钮中选择」下拉的临时选中值(仅作选择器,选完即清空,避免跨节点残留上次的选项)
|
||||
const actionPicker = ref<Record<string, any>>({ onNodeApprove: undefined, onApprove: undefined, onReject: undefined });
|
||||
|
||||
const operatorOptions = OPERATOR_OPTIONS;
|
||||
const readonly = computed(() => !!props.readonly);
|
||||
|
||||
// 回调接口配置:通过类时机需按节点配置;驳回类(onReject)已全局统一(后端按 @ApprovalBizAction 自动执行),无需在此逐节点维护
|
||||
const callbackPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
];
|
||||
const methodOptions = [
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
const title = computed(() => {
|
||||
const map: Record<string, string> = { start: '发起人设置', approver: '审批人设置', cc: '抄送人设置', branch: '条件设置' };
|
||||
return map[node.value?.type || ''] || '节点设置';
|
||||
});
|
||||
|
||||
const roleApi = (params) => defHttp.get({ url: '/sys/role/list', params });
|
||||
|
||||
function openDrawer(n: FlowNode) {
|
||||
node.value = n;
|
||||
// 切换节点时清空下拉选择器的残留值
|
||||
actionPicker.value = { onNodeApprove: undefined, onApprove: undefined, onReject: undefined };
|
||||
// 编辑副本,确定时回写,避免取消后脏数据
|
||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||
// 审批人节点确保回调接口配置结构存在
|
||||
if (n.type === 'approver') {
|
||||
const cb = form.value.props.callbackActions || {};
|
||||
form.value.props.callbackActions = {
|
||||
onNodeApprove: Array.isArray(cb.onNodeApprove) ? cb.onNodeApprove : [],
|
||||
onApprove: Array.isArray(cb.onApprove) ? cb.onApprove : [],
|
||||
onReject: Array.isArray(cb.onReject) ? cb.onReject : [],
|
||||
};
|
||||
}
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function addAction(phaseKey: string) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: 'POST', url: '' });
|
||||
}
|
||||
|
||||
function removeAction(phaseKey: string, i: number) {
|
||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||
}
|
||||
|
||||
/** 从「已标注的业务动作」中选择一个接口加入回调(按 url 值查回原始动作) */
|
||||
function addFromButton(phaseKey: string, url: string) {
|
||||
const opt = pageActionOptions.value.find((o) => o.value === url);
|
||||
const raw = opt?.raw;
|
||||
if (!raw || !raw.url) {
|
||||
return;
|
||||
}
|
||||
const list = form.value.props.callbackActions[phaseKey];
|
||||
// 选完即清空下拉,使其回到 placeholder(仅作选择器用)
|
||||
actionPicker.value[phaseKey] = undefined;
|
||||
if (list.some((a) => a.url === raw.url)) {
|
||||
createMessage.info('该接口已添加');
|
||||
return;
|
||||
}
|
||||
list.push({ name: raw.name, method: raw.method || 'POST', url: raw.url });
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
if (node.value && form.value) {
|
||||
node.value.name = form.value.name;
|
||||
node.value.props = cloneDeep(form.value.props);
|
||||
emit('confirm', node.value);
|
||||
}
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function addCond() {
|
||||
form.value.props.conditions.push({ label: '', field: '', operator: 'eq', value: '' });
|
||||
}
|
||||
|
||||
function removeCond(i: number) {
|
||||
form.value.props.conditions.splice(i, 1);
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.fd-cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cond-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 回调接口配置 */
|
||||
.fd-cb-block {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.fd-cb-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cb-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fd-cb-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
423
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
423
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* 钉钉式审批流设计器 样式
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@line-color: #cacaca;
|
||||
|
||||
.fd-design {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fd-toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-tb-item {
|
||||
margin-right: 24px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fd-tb-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
.fd-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fd-palette {
|
||||
flex-shrink: 0;
|
||||
width: 220px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
padding: 14px 12px;
|
||||
|
||||
.fd-palette-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fd-palette-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fd-palette-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fd-palette-item {
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 8px 30px 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border-left: 3px solid #ff943e;
|
||||
|
||||
&.fd-palette-cc {
|
||||
border-left-color: #3296fa;
|
||||
}
|
||||
|
||||
.fd-palette-item-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fd-palette-item-field {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fd-palette-item-add {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #3296fa;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
box-shadow: 0 2px 8px rgba(50, 150, 250, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
|
||||
.fd-canvas {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #f0f2f5;
|
||||
padding: 40px 20px 80px;
|
||||
}
|
||||
|
||||
.fd-flow {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- 节点列 ---------- */
|
||||
.fd-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.fd-del {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.fd-error {
|
||||
box-shadow: 0 0 0 1px #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.fd-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fd-priority {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.fd-del {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-body {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
min-height: 22px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
|
||||
&.fd-placeholder {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/* 各类型节点头部配色 */
|
||||
.fd-start .fd-card-header {
|
||||
background: #576a95;
|
||||
}
|
||||
.fd-approver .fd-card-header {
|
||||
background: #ff943e;
|
||||
}
|
||||
.fd-cc .fd-card-header {
|
||||
background: #3296fa;
|
||||
}
|
||||
.fd-branch {
|
||||
.fd-card {
|
||||
width: 220px;
|
||||
}
|
||||
.fd-card-header {
|
||||
background: #fff;
|
||||
color: #15bca3;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-priority {
|
||||
color: #999;
|
||||
}
|
||||
.fd-del {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 节点之间的连线 + 加号 ---------- */
|
||||
.fd-add {
|
||||
position: relative;
|
||||
width: 2px;
|
||||
min-height: 70px;
|
||||
background: @line-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fd-add-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #3296fa;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(50, 150, 250, 0.35);
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加号弹出菜单 */
|
||||
.fd-add-menu {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
.fd-add-item {
|
||||
width: 76px;
|
||||
height: 70px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
transition: all 0.15s;
|
||||
|
||||
.anticon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&.fd-add-approver .anticon {
|
||||
color: #ff943e;
|
||||
}
|
||||
&.fd-add-cc .anticon {
|
||||
color: #3296fa;
|
||||
}
|
||||
&.fd-add-condition .anticon {
|
||||
color: #15bca3;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 条件分支 ---------- */
|
||||
.fd-branches {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* +条件 按钮(带进入竖线) */
|
||||
.fd-add-branch-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: @line-color;
|
||||
}
|
||||
|
||||
.fd-add-branch {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-branch-cols {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-branch-col {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #f0f2f5;
|
||||
border-top: 2px solid @line-color;
|
||||
border-bottom: 2px solid @line-color;
|
||||
padding: 0 24px;
|
||||
|
||||
/* 列内顶部进入竖线 + 底部汇出竖线 */
|
||||
.fd-branch-inner {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: @line-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 首末列外侧白块,遮挡多余横线,形成包裹效果 */
|
||||
.fd-cover {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 4px;
|
||||
background: #f0f2f5;
|
||||
z-index: 2;
|
||||
}
|
||||
.fd-cover-tl {
|
||||
top: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-bl {
|
||||
bottom: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-tr {
|
||||
top: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
.fd-cover-br {
|
||||
bottom: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
/* ---------- 结束节点 ---------- */
|
||||
.fd-end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
|
||||
.fd-end-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #dedede;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 钉钉式审批流 节点数据模型 + 工厂函数
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
|
||||
// 节点类型:start发起人 approver审批人 cc抄送人 condition条件路由 branch条件分支
|
||||
export type NodeType = 'start' | 'approver' | 'cc' | 'condition' | 'branch';
|
||||
|
||||
export interface FlowNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
name: string;
|
||||
// 节点配置(按 type 不同含义不同)
|
||||
props: Record<string, any>;
|
||||
// 链式下一个节点
|
||||
childNode?: FlowNode | null;
|
||||
// 仅 condition 节点:分支数组(每个元素 type=branch)
|
||||
conditionNodes?: FlowNode[];
|
||||
}
|
||||
|
||||
let _seed = 0;
|
||||
|
||||
/** 生成唯一节点 id */
|
||||
export function nid(prefix = 'node'): string {
|
||||
_seed += 1;
|
||||
return `${prefix}_${Date.now().toString(36)}_${_seed}`;
|
||||
}
|
||||
|
||||
/** 发起人节点(根节点,固定存在) */
|
||||
export function createStartNode(): FlowNode {
|
||||
return {
|
||||
id: nid('start'),
|
||||
type: 'start',
|
||||
name: '发起人',
|
||||
props: {
|
||||
// initiatorType: all全员 / user指定成员 / role指定角色
|
||||
initiatorType: 'all',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 审批人节点 */
|
||||
export function createApproverNode(): FlowNode {
|
||||
return {
|
||||
id: nid('approver'),
|
||||
type: 'approver',
|
||||
name: '审批人',
|
||||
props: {
|
||||
// approverType: user指定成员 / role指定角色 / leader主管 / self发起人自己
|
||||
approverType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
leaderLevel: 1,
|
||||
// 多人审批方式 and会签(需全部同意) / or或签(一人同意) / sequence依次审批
|
||||
multiMode: 'and',
|
||||
// 审批人为空时 pass自动通过 / admin转交管理员 / stop终止
|
||||
emptyStrategy: 'admin',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 抄送人节点 */
|
||||
export function createCcNode(): FlowNode {
|
||||
return {
|
||||
id: nid('cc'),
|
||||
type: 'cc',
|
||||
name: '抄送人',
|
||||
props: {
|
||||
// ccType: user指定成员/角色 / field取单据字段中的人员
|
||||
ccType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
allowEditCc: false,
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
/** 解析出的页面阶段字段 */
|
||||
export interface StageField {
|
||||
stageKey: string;
|
||||
stageName: string;
|
||||
nodeType: 'approver' | 'cc';
|
||||
field: string;
|
||||
fieldComment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由"当前页字段"生成阶段节点:
|
||||
* 审批阶段(校对/审核/审批/分发) -> 审批人节点,处理人=取单据该字段中的人员;
|
||||
* 抄送阶段 -> 抄送节点,抄送人=取单据该字段中的人员。
|
||||
*/
|
||||
export function createStageNode(stage: StageField): FlowNode {
|
||||
const fieldLabel = stage.fieldComment || stage.field;
|
||||
if (stage.nodeType === 'cc') {
|
||||
const node = createCcNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.ccType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
const node = createApproverNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.approverType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
|
||||
/** 条件分支节点(默认两条分支:条件 + 其它情况) */
|
||||
export function createConditionNode(): FlowNode {
|
||||
return {
|
||||
id: nid('condition'),
|
||||
type: 'condition',
|
||||
name: '条件分支',
|
||||
props: {},
|
||||
conditionNodes: [createBranchNode(1), createBranchNode(2, true)],
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 单个条件分支 */
|
||||
export function createBranchNode(priority: number, isDefault = false): FlowNode {
|
||||
return {
|
||||
id: nid('branch'),
|
||||
type: 'branch',
|
||||
name: isDefault ? '其它情况' : `条件${priority}`,
|
||||
props: {
|
||||
priorityLevel: priority,
|
||||
isDefault,
|
||||
// 条件列表:{ field 字段名, label 字段中文, operator 运算符, value 值 }
|
||||
conditions: [] as any[],
|
||||
// 多条件关系 and / or
|
||||
logic: 'and',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 运算符选项 */
|
||||
export const OPERATOR_OPTIONS = [
|
||||
{ label: '等于', value: 'eq' },
|
||||
{ label: '不等于', value: 'ne' },
|
||||
{ label: '大于', value: 'gt' },
|
||||
{ label: '大于等于', value: 'gte' },
|
||||
{ label: '小于', value: 'lt' },
|
||||
{ label: '小于等于', value: 'lte' },
|
||||
{ label: '包含', value: 'contains' },
|
||||
{ label: '为空', value: 'empty' },
|
||||
{ label: '不为空', value: 'notEmpty' },
|
||||
];
|
||||
|
||||
/** 节点卡片内容摘要文本 */
|
||||
export function nodeSummary(node: FlowNode): string {
|
||||
if (node.type === 'start') {
|
||||
const t = node.props.initiatorType;
|
||||
if (t === 'all') return '所有人可发起';
|
||||
if (t === 'user') return node.props.userText ? `指定成员:${node.props.userText}` : '请设置发起人';
|
||||
if (t === 'role') return node.props.roleList?.length ? `指定角色(${node.props.roleList.length})` : '请设置发起角色';
|
||||
return '请设置发起人';
|
||||
}
|
||||
if (node.type === 'approver') {
|
||||
const t = node.props.approverType;
|
||||
if (t === 'self') return '发起人自己';
|
||||
if (t === 'leader') return `第${node.props.leaderLevel || 1}级主管`;
|
||||
if (t === 'role') return node.props.roleList?.length ? `角色审批(${node.props.roleList.length})` : '请设置审批角色';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
if (t === 'field') return `取单据字段:${node.props.fieldLabel || node.props.fieldName || '未指定字段'}`;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
return node.props.userText ? `指定成员:${node.props.userText}` : '请设置审批人';
|
||||
}
|
||||
if (node.type === 'cc') {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
if (node.props.ccType === 'field') return `抄送单据字段:${node.props.fieldLabel || node.props.fieldName || '未指定字段'}`;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
return node.props.userText ? `抄送:${node.props.userText}` : '请设置抄送人';
|
||||
}
|
||||
if (node.type === 'branch') {
|
||||
if (node.props.isDefault) return '未满足其它条件时进入此分支';
|
||||
const list = node.props.conditions || [];
|
||||
if (!list.length) return '请设置条件';
|
||||
return list.map((c: any) => `${c.label || c.field} ${operatorText(c.operator)} ${c.value ?? ''}`).join(node.props.logic === 'or' ? ' 或 ' : ' 且 ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function operatorText(op: string): string {
|
||||
return OPERATOR_OPTIONS.find((o) => o.value === op)?.label || op;
|
||||
}
|
||||
|
||||
/** 深度遍历每个节点(含分支) */
|
||||
export function eachNode(node: FlowNode | null | undefined, cb: (n: FlowNode) => void) {
|
||||
if (!node) return;
|
||||
cb(node);
|
||||
if (node.childNode) eachNode(node.childNode, cb);
|
||||
if (node.conditionNodes) node.conditionNodes.forEach((b) => eachNode(b, cb));
|
||||
}
|
||||
|
||||
/** 在 prevId 节点之后插入新节点 */
|
||||
export function insertAfter(root: FlowNode, prevId: string, newNode: FlowNode) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === prevId && newNode.id !== prevId) {
|
||||
newNode.childNode = n.childNode || null;
|
||||
n.childNode = newNode;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除节点(普通链节点 / 条件分支,发起人不可删) */
|
||||
export function removeNode(root: FlowNode, id: string): boolean {
|
||||
// 1) 链式节点:找到以其为 childNode 的父节点
|
||||
let parent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === id) parent = n;
|
||||
});
|
||||
if (parent) {
|
||||
(parent as FlowNode).childNode = (parent as FlowNode).childNode?.childNode || null;
|
||||
return true;
|
||||
}
|
||||
// 2) 条件分支:找到包含该分支的 condition
|
||||
let cond: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.conditionNodes && n.conditionNodes.some((b) => b.id === id)) cond = n;
|
||||
});
|
||||
if (cond) {
|
||||
const c = cond as FlowNode;
|
||||
const arr = c.conditionNodes as FlowNode[];
|
||||
if (arr.length <= 2) {
|
||||
// 只剩两条分支时删除一条 => 整个条件节点收起,保留其后续链
|
||||
let condParent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === c.id) condParent = n;
|
||||
});
|
||||
if (condParent) (condParent as FlowNode).childNode = c.childNode || null;
|
||||
} else {
|
||||
const i = arr.findIndex((b) => b.id === id);
|
||||
if (i >= 0) arr.splice(i, 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 给条件节点新增一条分支 */
|
||||
export function addBranch(root: FlowNode, conditionId: string) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === conditionId && n.conditionNodes) {
|
||||
const next = n.conditionNodes.length + 1;
|
||||
// 新分支插入到"其它情况"默认分支之前
|
||||
const defaultIdx = n.conditionNodes.findIndex((b) => b.props.isDefault);
|
||||
const branch = createBranchNode(next);
|
||||
if (defaultIdx >= 0) {
|
||||
n.conditionNodes.splice(defaultIdx, 0, branch);
|
||||
} else {
|
||||
n.conditionNodes.push(branch);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 发起审批 API(全局悬浮按钮使用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
enum Api {
|
||||
publishedList = '/xslmes/approvalLaunch/publishedList',
|
||||
bizRecords = '/xslmes/approvalLaunch/bizRecords',
|
||||
launch = '/xslmes/approvalLaunch/launch',
|
||||
launchBatch = '/xslmes/approvalLaunch/launchBatch',
|
||||
}
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(可发起的单据类型)
|
||||
*/
|
||||
export const getPublishedFlows = () => defHttp.get({ url: Api.publishedList });
|
||||
|
||||
/**
|
||||
* 根据审批流查询其绑定单据的记录列表
|
||||
*/
|
||||
export const getBizRecords = (params: { flowId: string; keyword?: string }) => defHttp.get({ url: Api.bizRecords, params });
|
||||
|
||||
/**
|
||||
* 发起审批(单条)
|
||||
*/
|
||||
export const launchApproval = (params: { flowId: string; bizDataId: string; bizTitle?: string }) => defHttp.post({ url: Api.launch, params });
|
||||
|
||||
/**
|
||||
* 批量发起审批(列表多选)
|
||||
*/
|
||||
export const launchApprovalBatch = (params: { flowId: string; items: { bizDataId: string; bizTitle?: string }[] }) =>
|
||||
defHttp.post({ url: Api.launchBatch, params });
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<MesXslFinalBatchPlanList />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MesXslFinalBatchPlanList from '../../xslmes/mesXslFinalBatchPlan/MesXslFinalBatchPlanList.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<MesXslMasterBatchPlanList />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MesXslMasterBatchPlanList from '../../xslmes/mesXslMasterBatchPlan/MesXslMasterBatchPlanList.vue';
|
||||
</script>
|
||||
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<!--
|
||||
IM 审批卡片「查看详情」弹窗:展示单据全部字段 + 审批进度/历史
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】审批办理-查看详情
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="open" title="审批单据详情" :width="640" :footer="null" :destroyOnClose="true">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="im-appr-detail">
|
||||
<div class="im-appr-detail-head">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="审批流">{{ info.flowName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusColor">{{ info.statusText }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发起人">{{ info.applyUserName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前节点">{{ info.currentNodeName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前处理人" :span="2">{{ info.currentHandlersText || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="im-appr-detail-section-title">单据数据</div>
|
||||
<table class="im-appr-detail-table">
|
||||
<tbody>
|
||||
<tr v-for="f in fields" :key="f.label">
|
||||
<th>{{ f.label }}</th>
|
||||
<td>{{ f.value }}</td>
|
||||
</tr>
|
||||
<tr v-if="!fields.length">
|
||||
<td class="im-appr-detail-empty">暂无单据数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<template v-if="history.length">
|
||||
<div class="im-appr-detail-section-title">审批历史</div>
|
||||
<a-timeline class="im-appr-detail-timeline">
|
||||
<a-timeline-item v-for="(h, i) in history" :key="i" :color="h.actionText === '驳回' ? 'red' : 'green'">
|
||||
<div class="im-appr-his-line">
|
||||
<b>{{ h.nodeName }}</b>
|
||||
<span>{{ h.name }} {{ h.actionText }}</span>
|
||||
<span class="im-appr-his-time">{{ h.time }}</span>
|
||||
</div>
|
||||
<div v-if="h.comment" class="im-appr-his-comment">意见:{{ h.comment }}</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApprovalDetail } from '/@/views/approval/flow/approvalHandle.api';
|
||||
|
||||
defineOptions({ name: 'ImApprovalDetailModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const info = ref<any>({});
|
||||
const fields = ref<{ label: string; value: string }[]>([]);
|
||||
const history = ref<any[]>([]);
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const s = info.value?.status;
|
||||
if (s === '1') return 'green';
|
||||
if (s === '2') return 'red';
|
||||
if (s === '3') return 'default';
|
||||
return 'blue';
|
||||
});
|
||||
|
||||
async function openModal(instanceId: string) {
|
||||
open.value = true;
|
||||
info.value = {};
|
||||
fields.value = [];
|
||||
history.value = [];
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data: any = await getApprovalDetail(instanceId);
|
||||
info.value = data || {};
|
||||
fields.value = data?.fields || [];
|
||||
history.value = data?.history || [];
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.im-appr-detail-section-title {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-left: 3px solid #1677ff;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.im-appr-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 32%;
|
||||
background: #fafafa;
|
||||
color: #595959;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.im-appr-detail-empty {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
.im-appr-his-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-comment {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -1,442 +1,545 @@
|
||||
<template>
|
||||
|
||||
<div class="im-biz-record-message">
|
||||
|
||||
<div v-if="showNoPermission" class="im-biz-record-no-permission">暂无当前消息权限</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 单条:详情表 -->
|
||||
<template v-if="isSingleItem">
|
||||
<div class="im-biz-record-item">
|
||||
<div class="im-biz-record-table-wrap">
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
<tbody>
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td>{{ field.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<template v-if="isSingleItem">
|
||||
|
||||
<div class="im-biz-record-item">
|
||||
|
||||
<div class="im-biz-record-table-wrap">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
|
||||
<th>{{ field.label }}</th>
|
||||
|
||||
<td>{{ field.value }}</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<!-- 审批卡片:底部操作按钮 -->
|
||||
<div v-if="isApprovalCard" class="im-biz-record-actions">
|
||||
<a-button size="small" @click="handleDetail(singleItem)">
|
||||
<Icon icon="ant-design:file-search-outlined" />
|
||||
<span>查看详情</span>
|
||||
</a-button>
|
||||
<template v-if="liveActionable">
|
||||
<a-button size="small" type="primary" :loading="approving" @click="handleApprove(singleItem)">
|
||||
<Icon icon="ant-design:check-outlined" />
|
||||
<span>{{ singleItem.actionLabel || '审批' }}</span>
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="openReject(singleItem)">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
<span>拒绝</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||||
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
||||
<!-- 发起人:审批中可撤销 -->
|
||||
<a-button v-if="canCancel" size="small" danger :loading="cancelling" @click="openCancel(singleItem)">
|
||||
<Icon icon="ant-design:rollback-outlined" />
|
||||
<span>撤销</span>
|
||||
</a-button>
|
||||
<!-- 发起人:审批已结束的状态提示 -->
|
||||
<span v-else-if="mineEndedText" class="im-biz-record-disabled">{{ mineEndedText }}</span>
|
||||
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:unordered-list-outlined" />
|
||||
<span>跳转至列表</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 普通分享卡片:定位链接 -->
|
||||
<a v-else class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>查看并定位到此数据</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>查看并定位到此数据</span>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
<template v-else>
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
<td class="im-biz-record-link-col">
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>定位</span>
|
||||
</a>
|
||||
</td>
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
</template>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<ImApprovalDetailModal ref="detailModalRef" />
|
||||
|
||||
<!-- 驳回理由弹窗 -->
|
||||
<a-modal v-model:open="rejectOpen" title="驳回审批" :confirmLoading="rejecting" okText="确认驳回" @ok="confirmReject">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="驳回理由" required>
|
||||
<a-textarea v-model:value="rejectReason" :rows="3" placeholder="请填写驳回理由" :maxlength="500" show-count />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
|
||||
<td class="im-biz-record-link-col">
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>定位</span>
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- 撤销弹窗 -->
|
||||
<a-modal v-model:open="cancelOpen" title="撤销审批" :confirmLoading="cancelling" okText="确认撤销" @ok="confirmCancel">
|
||||
<a-alert type="warning" show-icon :message="'撤销后流程将终止,单据将恢复到发起审批前的状态。'" style="margin-bottom: 12px" />
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="撤销原因">
|
||||
<a-textarea v-model:value="cancelReason" :rows="3" placeholder="可选填写撤销原因" :maxlength="500" show-count />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
||||
|
||||
import {
|
||||
|
||||
getImBizRecordFieldValueByLabel,
|
||||
|
||||
resolveImBizRecordItemFields,
|
||||
|
||||
resolveImBizRecordListColumnLabels,
|
||||
|
||||
} from './imBizRecordMessage';
|
||||
|
||||
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||||
import { navigateImBizRecordLink } from './imRecordLocate';
|
||||
|
||||
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
||||
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||
import { onImMessagesUpdated } from './imCache';
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { approveApproval, rejectApproval, cancelApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
||||
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
||||
|
||||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||||
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
payload: ImBizRecordPayload;
|
||||
|
||||
mine?: boolean;
|
||||
|
||||
receiverHasBizPagePermission?: boolean;
|
||||
|
||||
}>();
|
||||
|
||||
// 办理成功后通知父级(ImChat)刷新当前会话,使下一节点卡片/结果通知即时出现
|
||||
const emit = defineEmits(['handled']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const detailModalRef = ref();
|
||||
const approving = ref(false);
|
||||
const rejecting = ref(false);
|
||||
const rejectOpen = ref(false);
|
||||
const rejectReason = ref('');
|
||||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||||
// 本地办理结果:approved / rejected / cancelled(卡片消息为静态,办理后本地标记)
|
||||
const actionDone = ref<'' | 'approved' | 'rejected' | 'cancelled'>('');
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
const cancelling = ref(false);
|
||||
const cancelOpen = ref(false);
|
||||
const cancelReason = ref('');
|
||||
const cancelItem = ref<ImBizRecordItem | null>(null);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
// 审批实例实时状态(用于旧节点卡片置灰)
|
||||
interface LiveStatus {
|
||||
exists: boolean;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
currentNodeId?: string;
|
||||
currentHandlersText?: string;
|
||||
canApprove?: boolean;
|
||||
}
|
||||
const liveStatus = ref<LiveStatus | null>(null);
|
||||
const liveLoaded = ref(false);
|
||||
|
||||
const isSingleItem = computed(() => props.payload.items.length === 1);
|
||||
|
||||
const singleItem = computed(() => props.payload.items[0]);
|
||||
|
||||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||||
|
||||
// 审批卡片:单条且带审批实例ID
|
||||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||||
|
||||
// 是否仍可办理:本地未办理 且 实例审批中 且 卡片节点==当前节点 且 本人为当前处理人
|
||||
const liveActionable = computed(() => {
|
||||
if (!isApprovalCard.value || actionDone.value || props.mine) {
|
||||
return false;
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s || !s.exists || s.status !== '0' || !s.canApprove) {
|
||||
return false;
|
||||
}
|
||||
// 携带 nodeId 时严格比对当前节点,区分同一实例的新旧卡片
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId) {
|
||||
return s.currentNodeId === cardNodeId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 不可办理时的置灰提示文案
|
||||
const disabledText = computed(() => {
|
||||
if (actionDone.value) {
|
||||
return actionDone.value === 'rejected' ? '已驳回' : '已处理';
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s) {
|
||||
return liveLoaded.value ? '加载失败' : '';
|
||||
}
|
||||
if (!s.exists) {
|
||||
return '审批已失效';
|
||||
}
|
||||
if (s.status === '1') return '已通过';
|
||||
if (s.status === '2') return '已驳回';
|
||||
if (s.status === '3') return '已撤销';
|
||||
// 审批中但本卡片不可办理
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId && s.currentNodeId !== cardNodeId) {
|
||||
return '已流转,无需处理';
|
||||
}
|
||||
return '等待他人处理';
|
||||
});
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
// 是否可撤销:审批卡片 且 本人发起(mine) 且 审批中 且 本地未撤销
|
||||
const canCancel = computed(() => {
|
||||
if (!isApprovalCard.value || !props.mine || actionDone.value) {
|
||||
return false;
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
return !!s && s.exists === true && s.status === '0';
|
||||
});
|
||||
|
||||
// 发起人视角的结束态提示
|
||||
const mineEndedText = computed(() => {
|
||||
if (!isApprovalCard.value || !props.mine) {
|
||||
return '';
|
||||
}
|
||||
if (actionDone.value === 'cancelled') return '已撤销';
|
||||
const s = liveStatus.value;
|
||||
if (!s || !s.exists) return '';
|
||||
if (s.status === '1') return '已通过';
|
||||
if (s.status === '2') return '已驳回';
|
||||
if (s.status === '3') return '已撤销';
|
||||
return '';
|
||||
});
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
async function loadLiveStatus() {
|
||||
const id = singleItem.value?.instanceId;
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
|
||||
if (!isApprovalCard.value || !id) {
|
||||
return;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
|
||||
try {
|
||||
const res: any = await getApprovalStatus(id);
|
||||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||||
} catch {
|
||||
liveStatus.value = null;
|
||||
} finally {
|
||||
liveLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadLiveStatus);
|
||||
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||
// 撤销/驳回/流转时后端会向处理人推送IM消息,WS到达会触发 onImMessagesUpdated,
|
||||
// 借此实时重新拉取实例状态,使本卡片的「审批/拒绝」按钮即时置灰、不可点击。
|
||||
let unsubscribeMsgUpdated: (() => void) | null = null;
|
||||
onMounted(() => {
|
||||
if (!isApprovalCard.value) {
|
||||
return;
|
||||
}
|
||||
unsubscribeMsgUpdated = onImMessagesUpdated(() => {
|
||||
// 本地已办理的卡片无需再刷新,避免覆盖本地置灰文案
|
||||
if (actionDone.value) {
|
||||
return;
|
||||
}
|
||||
// 已是终态(已通过/已驳回/已撤销/已失效)的卡片无需重复拉取
|
||||
const s = liveStatus.value;
|
||||
if (s && (s.exists === false || (!!s.status && s.status !== '0'))) {
|
||||
return;
|
||||
}
|
||||
loadLiveStatus();
|
||||
});
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribeMsgUpdated?.();
|
||||
unsubscribeMsgUpdated = null;
|
||||
});
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||
|
||||
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
||||
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||||
const canLocate = computed(() => props.mine || hasPagePermission.value);
|
||||
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value);
|
||||
|
||||
const showPeerNoPermissionTip = computed(
|
||||
() => !!props.mine && props.receiverHasBizPagePermission === false,
|
||||
);
|
||||
const showPeerNoPermissionTip = computed(() => !!props.mine && props.receiverHasBizPagePermission === false);
|
||||
|
||||
function resolveItemFields(item: ImBizRecordItem) {
|
||||
|
||||
return resolveImBizRecordItemFields(item);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getFieldValue(item: ImBizRecordItem, label: string) {
|
||||
|
||||
return getImBizRecordFieldValueByLabel(item, label);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleLinkClick(linkPath: string) {
|
||||
|
||||
if (!linkPath || showNoPermission.value) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
await navigateImBizRecordLink(linkPath);
|
||||
|
||||
}
|
||||
|
||||
function handleDetail(item: ImBizRecordItem) {
|
||||
if (!item.instanceId) return;
|
||||
detailModalRef.value?.openModal(item.instanceId);
|
||||
}
|
||||
|
||||
async function handleApprove(item: ImBizRecordItem) {
|
||||
if (!item.instanceId || approving.value) return;
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||
await loadLiveStatus();
|
||||
if (!liveActionable.value) {
|
||||
createMessage.warning(disabledText.value || '该审批已无法办理');
|
||||
return;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||
try {
|
||||
approving.value = true;
|
||||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||||
createMessage.success(typeof res === 'string' ? res : '已审批');
|
||||
actionDone.value = 'approved';
|
||||
// 立即刷新本卡片状态(置灰),再通知父级刷新会话
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
approving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(item: ImBizRecordItem) {
|
||||
rejectItem.value = item;
|
||||
rejectReason.value = '';
|
||||
rejectOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmReject() {
|
||||
const item = rejectItem.value;
|
||||
if (!item?.instanceId) return;
|
||||
if (!rejectReason.value.trim()) {
|
||||
createMessage.warning('请填写驳回理由');
|
||||
return;
|
||||
}
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||
await loadLiveStatus();
|
||||
if (!liveActionable.value) {
|
||||
createMessage.warning(disabledText.value || '该审批已无法办理');
|
||||
rejectOpen.value = false;
|
||||
return;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||
try {
|
||||
rejecting.value = true;
|
||||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||||
createMessage.success('已驳回');
|
||||
actionDone.value = 'rejected';
|
||||
rejectOpen.value = false;
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
function openCancel(item: ImBizRecordItem) {
|
||||
cancelItem.value = item;
|
||||
cancelReason.value = '';
|
||||
cancelOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmCancel() {
|
||||
const item = cancelItem.value;
|
||||
if (!item?.instanceId || cancelling.value) return;
|
||||
try {
|
||||
cancelling.value = true;
|
||||
await cancelApproval({ instanceId: item.instanceId, reason: cancelReason.value.trim() });
|
||||
createMessage.success('已撤销,单据已恢复到发起前状态');
|
||||
actionDone.value = 'cancelled';
|
||||
cancelOpen.value = false;
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
cancelling.value = false;
|
||||
}
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.im-biz-record-message {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
min-width: 280px;
|
||||
|
||||
max-width: 420px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-no-permission {
|
||||
|
||||
padding: 12px 10px;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #8c8c8c;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-peer-tip {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
padding: 2px 8px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
background: #fff7e6;
|
||||
|
||||
border: 1px solid #ffd591;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #d46b08;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-item {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
/* 审批办理按钮栏 */
|
||||
.im-biz-record-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
|
||||
.im-biz-record-done {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 不可办理置灰提示 */
|
||||
.im-biz-record-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.im-biz-record-table-wrap {
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
background: #fff;
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-table {
|
||||
|
||||
width: 100%;
|
||||
|
||||
border-collapse: collapse;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
padding: 8px 10px;
|
||||
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tr:last-child {
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--detail {
|
||||
|
||||
table-layout: fixed;
|
||||
|
||||
|
||||
|
||||
th {
|
||||
|
||||
width: 38%;
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
min-width: 100%;
|
||||
|
||||
table-layout: auto;
|
||||
|
||||
|
||||
|
||||
thead th {
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tbody td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link-col {
|
||||
|
||||
width: 72px;
|
||||
|
||||
min-width: 72px;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #1677ff;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
color: #0958d9;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user