Compare commits
21 Commits
e92cab555f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 75bc744fc8 | |||
| 3431cc6b17 | |||
|
|
5cb24c582d | ||
|
|
617d47a3db | ||
|
|
c4447b91dd | ||
|
|
39a9bd83f1 | ||
|
|
de48bd2324 | ||
|
|
5b8bd2797a | ||
|
|
fd5205e33e | ||
|
|
1d0b4c9fbb | ||
|
|
b9be88ae3f | ||
|
|
fc4e3211ad | ||
|
|
4785c55e52 | ||
| 457089e271 | |||
|
|
1c5cede957 | ||
|
|
2d142dbc9c | ||
| a65ae7be60 | |||
| 38f22ef8bd | |||
| 69a60ca07b | |||
| a08ca8985a | |||
| 29efd6694f |
67
.agents/skills/karpathy-guidelines/SKILL.md
Normal file
67
.agents/skills/karpathy-guidelines/SKILL.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
2
jeecg-boot/.gitignore
vendored
2
jeecg-boot/.gitignore
vendored
@@ -12,6 +12,8 @@ rebel.xml
|
||||
## backend
|
||||
**/target
|
||||
**/logs
|
||||
# 开发者本机钉钉 Stream 接收配置(从 application-dev-local.yml.example 复制)
|
||||
**/application-dev-local.yml
|
||||
|
||||
## front
|
||||
**/*.lock
|
||||
|
||||
64
jeecg-boot/db/mes-fix-rubber-category-visible.sql
Normal file
64
jeecg-boot/db/mes-fix-rubber-category-visible.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- 修复:胶料分类字典(XSLMES_RUBBER)数据库有数据但页面不展示
|
||||
-- 场景:开启多租户后,sys_category 的 tenant_id 与当前登录租户不一致/为空
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 目标租户:默认取 admin 用户租户;若为空则回退到 0
|
||||
SET @target_tenant_id = (
|
||||
SELECT COALESCE(tenant_id, 0)
|
||||
FROM sys_user
|
||||
WHERE username = 'admin'
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
);
|
||||
SET @target_tenant_id = IFNULL(@target_tenant_id, 0);
|
||||
|
||||
-- 2) 定位根分类编码
|
||||
SET @rubber_code = 'XSLMES_RUBBER';
|
||||
|
||||
-- 若根节点不存在则补一个最小根节点(避免前端 pcode 查询直接失败)
|
||||
INSERT INTO sys_category (id, pid, name, code, has_child, tenant_id, create_by, create_time, update_by, update_time)
|
||||
SELECT '1994000000000000001', '0', 'MES胶料分类', @rubber_code, '1', @target_tenant_id, 'admin', NOW(), 'admin', NOW()
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_category WHERE code = @rubber_code
|
||||
);
|
||||
|
||||
SET @rubber_root_id = (
|
||||
SELECT id
|
||||
FROM sys_category
|
||||
WHERE code = @rubber_code
|
||||
ORDER BY create_time ASC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 3) 若根节点存在,统一修复租户与父子标记
|
||||
UPDATE sys_category
|
||||
SET tenant_id = @target_tenant_id
|
||||
WHERE id = @rubber_root_id;
|
||||
|
||||
UPDATE sys_category
|
||||
SET tenant_id = @target_tenant_id
|
||||
WHERE pid = @rubber_root_id;
|
||||
|
||||
-- 根节点是否有子节点,按真实数据回写
|
||||
UPDATE sys_category
|
||||
SET has_child = CASE
|
||||
WHEN EXISTS (SELECT 1 FROM (SELECT id FROM sys_category WHERE pid = @rubber_root_id LIMIT 1) t) THEN '1'
|
||||
ELSE '0'
|
||||
END
|
||||
WHERE id = @rubber_root_id;
|
||||
|
||||
-- 子节点统一标记为无子(当前这批分类通常为叶子)
|
||||
UPDATE sys_category
|
||||
SET has_child = '0'
|
||||
WHERE pid = @rubber_root_id;
|
||||
|
||||
-- 4) 结果检查(执行后看返回)
|
||||
SELECT 'ROOT' AS level_tag, id, pid, code, name, tenant_id, has_child
|
||||
FROM sys_category
|
||||
WHERE id = @rubber_root_id
|
||||
UNION ALL
|
||||
SELECT 'CHILD' AS level_tag, id, pid, code, name, tenant_id, has_child
|
||||
FROM sys_category
|
||||
WHERE pid = @rubber_root_id
|
||||
ORDER BY level_tag, code;
|
||||
65
jeecg-boot/db/mes-xsl-equip-part-mapping-menu-permission.sql
Normal file
65
jeecg-boot/db/mes-xsl-equip-part-mapping-menu-permission.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- MES 设备对应部位:建表 + 菜单 + 按钮 + 租户 admin 授权(可整文件一次执行)
|
||||
-- 权限前缀:mes:mes_xsl_equip_part_mapping:*
|
||||
-- 数据由设备点检配置保存后自动生成,列表无手工新增
|
||||
-- Flyway:V3.9.2_123__mes_xsl_equip_part_mapping.sql
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_equip_part_mapping` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`equipment_ledger_id` varchar(32) NOT NULL COMMENT '设备台账主键 mes_xsl_equipment_ledger.id',
|
||||
`equipment_name` varchar(500) NOT NULL COMMENT '设备名称',
|
||||
`machine_code` varchar(500) DEFAULT NULL COMMENT '机台代号(设备编号冗余)',
|
||||
`equipment_part_id` varchar(32) NOT NULL COMMENT '设备大部位主键 mes_xsl_equipment_part.id',
|
||||
`equipment_part_name` varchar(500) DEFAULT NULL COMMENT '设备大部位名称',
|
||||
`part_code` varchar(500) DEFAULT NULL COMMENT '大部位代码',
|
||||
`equipment_sub_part_id` varchar(32) NOT NULL COMMENT '设备小部位主键 mes_xsl_equipment_sub_part.id',
|
||||
`equipment_sub_part_name` varchar(500) DEFAULT NULL COMMENT '设备小部位名称',
|
||||
`sub_part_code` varchar(500) DEFAULT NULL COMMENT '小部位代码',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门',
|
||||
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_mepm_ledger_part_sub` (`equipment_ledger_id`, `equipment_part_id`, `equipment_sub_part_id`),
|
||||
KEY `idx_mepm_tenant_equip_name` (`tenant_id`, `equipment_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES设备对应部位';
|
||||
|
||||
SET @mes_tenant_id = 1002;
|
||||
|
||||
SET @mes_equip_pid = (
|
||||
SELECT `id` FROM `sys_permission`
|
||||
WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = '设备管理'
|
||||
LIMIT 1
|
||||
);
|
||||
SET @mes_equip_pid = IFNULL(@mes_equip_pid, '1860000000000000133');
|
||||
|
||||
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 ('1860000000000000215', @mes_equip_pid, '设备对应部位', '/xslmes/mesXslEquipPartMapping', 'xslmes/mesXslEquipPartMapping/MesXslEquipPartMappingList', 'MesXslEquipPartMappingList', 1, NULL, '1', 14, 1, 0, 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`), `icon` = 'ant-design:apartment-outlined';
|
||||
|
||||
UPDATE `sys_permission` SET `icon` = 'ant-design:apartment-outlined' WHERE `id` = '1860000000000000215' AND `del_flag` = 0;
|
||||
|
||||
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
|
||||
('1860000000000000216', '1860000000000000215', '导出', 2, 'mes:mes_xsl_equip_part_mapping: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`);
|
||||
|
||||
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.`id`, p.`id`, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`tenant_id` = @mes_tenant_id
|
||||
AND r.`role_code` = 'admin'
|
||||
AND p.`id` IN ('1860000000000000215', '1860000000000000216')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp
|
||||
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
@@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS `mes_xsl_equipment_ledger` (
|
||||
`process_operation_id` varchar(32) NOT NULL COMMENT '所属工序 mes_xsl_process_operation.id',
|
||||
`process_operation_name` varchar(500) DEFAULT NULL COMMENT '工序名称冗余',
|
||||
`equipment_name` varchar(500) NOT NULL COMMENT '设备名称(同租户未删除唯一)',
|
||||
`ledger_no` varchar(16) DEFAULT NULL COMMENT '编号(租户内从001递增自动生成,只读)',
|
||||
`equipment_code` varchar(128) NOT NULL COMMENT '设备编号(同租户未删除唯一)',
|
||||
`manufacturer_id` varchar(32) DEFAULT NULL COMMENT '所属设备厂家 mes_xsl_manufacturer.id',
|
||||
`manufacturer_name` varchar(500) DEFAULT NULL COMMENT '设备厂家名称冗余',
|
||||
@@ -59,6 +60,7 @@ CREATE TABLE IF NOT EXISTS `mes_xsl_equipment_ledger` (
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mel_tenant_code` (`tenant_id`, `equipment_code`),
|
||||
KEY `idx_mel_tenant_ledger_no` (`tenant_id`, `ledger_no`),
|
||||
KEY `idx_mel_tenant_name` (`tenant_id`, `equipment_name`),
|
||||
KEY `idx_mel_process` (`process_operation_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES设备台账';
|
||||
|
||||
@@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS `mes_xsl_equipment_ledger` (
|
||||
`process_operation_id` varchar(32) NOT NULL COMMENT '所属工序',
|
||||
`process_operation_name` varchar(500) DEFAULT NULL COMMENT '工序名称冗余',
|
||||
`equipment_name` varchar(500) NOT NULL COMMENT '设备名称',
|
||||
`equipment_code` varchar(128) NOT NULL COMMENT '设备编号',
|
||||
`ledger_no` varchar(16) DEFAULT NULL COMMENT '编号(租户内从001递增自动生成,只读)',
|
||||
`equipment_code` varchar(128) NOT NULL COMMENT '设备编号(同租户不可重复)',
|
||||
`manufacturer_id` varchar(32) DEFAULT NULL COMMENT '所属设备厂家',
|
||||
`manufacturer_name` varchar(500) DEFAULT NULL COMMENT '设备厂家名称冗余',
|
||||
`equipment_category_id` varchar(32) DEFAULT NULL COMMENT '设备类别',
|
||||
@@ -42,5 +43,6 @@ CREATE TABLE IF NOT EXISTS `mes_xsl_equipment_ledger` (
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mel_tenant_code` (`tenant_id`, `equipment_code`),
|
||||
KEY `idx_mel_tenant_ledger_no` (`tenant_id`, `ledger_no`),
|
||||
KEY `idx_mel_tenant_name` (`tenant_id`, `equipment_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES设备台账';
|
||||
|
||||
79
jeecg-boot/db/mes-xsl-mixing-production-plan-menu.sql
Normal file
79
jeecg-boot/db/mes-xsl-mixing-production-plan-menu.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- 密炼生产计划维护 菜单与权限(挂载到 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`,`always_show`,`icon`,`is_route`,`is_leaf`,`keep_alive`,`hidden`,`hide_tab`,`description`,`status`,`del_flag`,`create_by`,`create_time`,`rule_flag`,`internal_or_external`)
|
||||
VALUES
|
||||
('1860000000000000301',@mixer_parent_id,'密炼生产计划维护','/mes/mixingproductionplaninfo','mes/mixingproductionplaninfo/index','MesXslMixingProductionPlanList',1,NULL,'1',90,0,'ant-design:calendar-outlined',1,1,1,0,0,'密炼生产计划维护',1,0,'admin',NOW(),0,0)
|
||||
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`),
|
||||
`sort_no`=VALUES(`sort_no`),
|
||||
`is_route`=VALUES(`is_route`),
|
||||
`is_leaf`=VALUES(`is_leaf`),
|
||||
`keep_alive`=VALUES(`keep_alive`),
|
||||
`icon`=VALUES(`icon`),
|
||||
`status`=VALUES(`status`),
|
||||
`del_flag`=VALUES(`del_flag`);
|
||||
|
||||
INSERT INTO sys_permission
|
||||
(`id`,`parent_id`,`name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`status`,`del_flag`,`create_by`,`create_time`)
|
||||
VALUES
|
||||
('1860000000000000302','1860000000000000301','查询',2,'xslmes:mes_xsl_mixing_production_plan:list','1',1,'1',0,'admin',NOW()),
|
||||
('1860000000000000303','1860000000000000301','整表保存',2,'xslmes:mes_xsl_mixing_production_plan:saveAll','1',2,'1',0,'admin',NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name`=VALUES(`name`),
|
||||
`perms`=VALUES(`perms`),
|
||||
`sort_no`=VALUES(`sort_no`),
|
||||
`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 (
|
||||
'1860000000000000301',
|
||||
'1860000000000000302', '1860000000000000303'
|
||||
)
|
||||
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/mixingproductionplaninfo',
|
||||
component = 'mes/mixingproductionplaninfo/index',
|
||||
component_name = 'MesXslMixingProductionPlanList',
|
||||
menu_type = 1,
|
||||
is_route = 1,
|
||||
is_leaf = 1,
|
||||
hidden = 0,
|
||||
status = '1',
|
||||
del_flag = 0
|
||||
WHERE id = '1860000000000000301';
|
||||
78
jeecg-boot/db/mes-xsl-raw-material-demand-plan-menu.sql
Normal file
78
jeecg-boot/db/mes-xsl-raw-material-demand-plan-menu.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- 原材料需求计划 菜单与权限(挂载到 MES密炼工程,兼容 MES管理)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @raw_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 @raw_parent_id = IFNULL(@raw_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 @raw_parent_id = IFNULL(@raw_parent_id, '1860000000000000001');
|
||||
|
||||
INSERT INTO sys_permission
|
||||
(`id`,`parent_id`,`name`,`url`,`component`,`component_name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`always_show`,`icon`,`is_route`,`is_leaf`,`keep_alive`,`hidden`,`hide_tab`,`description`,`status`,`del_flag`,`create_by`,`create_time`,`rule_flag`,`internal_or_external`)
|
||||
VALUES
|
||||
('1900000000000000710',@raw_parent_id,'原材料需求计划','/mes/rawmaterialdemandplan','mes/rawmaterialdemandplan/index','MesXslRawMaterialDemandPlanList',1,NULL,'1',95,0,'ant-design:ordered-list-outlined',1,1,1,0,0,'原材料需求计划',1,0,'admin',NOW(),0,0)
|
||||
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`),
|
||||
`sort_no`=VALUES(`sort_no`),
|
||||
`is_route`=VALUES(`is_route`),
|
||||
`is_leaf`=VALUES(`is_leaf`),
|
||||
`keep_alive`=VALUES(`keep_alive`),
|
||||
`icon`=VALUES(`icon`),
|
||||
`status`=VALUES(`status`),
|
||||
`hidden`=VALUES(`hidden`),
|
||||
`del_flag`=VALUES(`del_flag`);
|
||||
|
||||
INSERT INTO sys_permission
|
||||
(`id`,`parent_id`,`name`,`menu_type`,`perms`,`perms_type`,`sort_no`,`status`,`del_flag`,`create_by`,`create_time`)
|
||||
VALUES
|
||||
('1900000000000000711','1900000000000000710','导出',2,'xslmes:mes_xsl_raw_material_demand_plan:exportXls','1',1,'1',0,'admin',NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name`=VALUES(`name`),
|
||||
`perms`=VALUES(`perms`),
|
||||
`sort_no`=VALUES(`sort_no`),
|
||||
`status`=VALUES(`status`),
|
||||
`del_flag`=VALUES(`del_flag`);
|
||||
|
||||
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 ('1900000000000000710', '1900000000000000711')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_role_permission rp
|
||||
WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
|
||||
AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
-- 强制修复:按 ID + 名称 + URL 三重兜底,确保能显示
|
||||
UPDATE sys_permission
|
||||
SET
|
||||
parent_id = @raw_parent_id,
|
||||
url = '/mes/rawmaterialdemandplan',
|
||||
component = 'mes/rawmaterialdemandplan/index',
|
||||
component_name = 'MesXslRawMaterialDemandPlanList',
|
||||
menu_type = 1,
|
||||
is_route = 1,
|
||||
is_leaf = 1,
|
||||
hidden = 0,
|
||||
status = '1',
|
||||
del_flag = 0,
|
||||
redirect = NULL
|
||||
WHERE id = '1900000000000000710'
|
||||
OR name = '原材料需求计划'
|
||||
OR url = '/mes/rawmaterialdemandplan';
|
||||
@@ -0,0 +1,85 @@
|
||||
-- MES 胶料小料锁定日志 + 锁定原因字段 reason_desc:可整文件一次执行
|
||||
-- 若已执行 V3.9.2_119 Flyway 可只跑本脚本补菜单/字段(幂等)
|
||||
-- 权限前缀:mes:mes_xsl_rubber_small_lock_log:*
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @reason_desc_exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_rubber_small_lock_reason'
|
||||
AND COLUMN_NAME = 'reason_desc'
|
||||
);
|
||||
SET @ddl_reason_desc := IF(
|
||||
@reason_desc_exists = 0,
|
||||
'ALTER TABLE `mes_xsl_rubber_small_lock_reason` ADD COLUMN `reason_desc` varchar(500) NOT NULL DEFAULT '''' COMMENT ''原因(手动输入,必填)'' AFTER `barcode_type`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt_reason_desc FROM @ddl_reason_desc;
|
||||
EXECUTE stmt_reason_desc;
|
||||
DEALLOCATE PREPARE stmt_reason_desc;
|
||||
|
||||
UPDATE `mes_xsl_rubber_small_lock_reason` SET `reason_desc` = CONCAT('原因', `reason_code`) WHERE `reason_desc` = '' OR `reason_desc` IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_small_lock_log` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`lock_reason_id` varchar(32) NOT NULL COMMENT '锁定原因ID',
|
||||
`barcode_type` varchar(16) NOT NULL COMMENT '条码类型',
|
||||
`barcode` varchar(128) NOT NULL COMMENT '条码',
|
||||
`lock_type` varchar(16) NOT NULL COMMENT '状态',
|
||||
`reason_desc` varchar(500) NOT NULL COMMENT '原因',
|
||||
`log_date` date NOT NULL COMMENT '日期',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门',
|
||||
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` varchar(32) DEFAULT NULL COMMENT '修改人',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
|
||||
`del_flag` int DEFAULT '0' COMMENT '删除标记(0正常1删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mrsl_log_tenant_date` (`tenant_id`, `log_date`),
|
||||
KEY `idx_mrsl_log_barcode` (`tenant_id`, `barcode_type`, `barcode`),
|
||||
KEY `idx_mrsl_log_reason` (`lock_reason_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料小料锁定日志';
|
||||
|
||||
SET @mes_tenant_id = 1002;
|
||||
|
||||
SET @mes_quality_pid = IFNULL(
|
||||
(SELECT `id` FROM `sys_permission` WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = '质量管理' LIMIT 1),
|
||||
'1860000000000000162'
|
||||
);
|
||||
|
||||
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 ('1860000000000000215', @mes_quality_pid, '胶料小料锁定日志', '/xslmes/mesXslRubberSmallLockLog', 'xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLogList', 'MesXslRubberSmallLockLogList', 1, NULL, '1', 7, 1, 0, 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`),
|
||||
`sort_no` = VALUES(`sort_no`), `is_leaf` = 0, `status` = '1', `del_flag` = 0, `keep_alive` = VALUES(`keep_alive`);
|
||||
|
||||
UPDATE `sys_permission` SET `icon` = 'ant-design:file-text-outlined' WHERE `id` = '1860000000000000215' AND `del_flag` = 0;
|
||||
|
||||
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `is_leaf`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
|
||||
('1860000000000000216', '1860000000000000215', '新增', 2, 'mes:mes_xsl_rubber_small_lock_log:add', '1', 1, '1', 0, 'admin', NOW()),
|
||||
('1860000000000000217', '1860000000000000215', '编辑', 2, 'mes:mes_xsl_rubber_small_lock_log:edit', '1', 1, '1', 0, 'admin', NOW()),
|
||||
('1860000000000000218', '1860000000000000215', '删除', 2, 'mes:mes_xsl_rubber_small_lock_log:delete', '1', 1, '1', 0, 'admin', NOW()),
|
||||
('1860000000000000219', '1860000000000000215', '批量删除', 2, 'mes:mes_xsl_rubber_small_lock_log:deleteBatch', '1', 1, '1', 0, 'admin', NOW()),
|
||||
('1860000000000000220', '1860000000000000215', '导出', 2, 'mes:mes_xsl_rubber_small_lock_log:exportXls', '1', 1, '1', 0, 'admin', NOW()),
|
||||
('1860000000000000221', '1860000000000000215', '导入', 2, 'mes:mes_xsl_rubber_small_lock_log:importExcel', '1', 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`),
|
||||
`is_leaf` = 1, `status` = '1', `del_flag` = 0;
|
||||
|
||||
INSERT INTO `sys_role_permission`(`id`, `role_id`, `permission_id`, `operate_date`, `operate_ip`)
|
||||
SELECT REPLACE(UUID(), '-', ''), r.`id`, p.`id`, NOW(), '127.0.0.1'
|
||||
FROM `sys_role` r
|
||||
CROSS JOIN `sys_permission` p
|
||||
WHERE r.`tenant_id` = @mes_tenant_id
|
||||
AND r.`role_code` = 'admin'
|
||||
AND p.`id` IN (
|
||||
'1860000000000000215',
|
||||
'1860000000000000216', '1860000000000000217', '1860000000000000218', '1860000000000000219',
|
||||
'1860000000000000220', '1860000000000000221'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_permission` rp
|
||||
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||
);
|
||||
@@ -31,6 +31,7 @@ CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_small_lock_reason` (
|
||||
`reason_code` varchar(16) NOT NULL COMMENT '编号(租户内从001递增自动生成,只读)',
|
||||
`lock_type` varchar(16) NOT NULL COMMENT '类型(字典xslmes_rubber_small_lock_type:lock锁定unlock解锁)',
|
||||
`barcode_type` varchar(16) NOT NULL COMMENT '条码类型(字典xslmes_rubber_small_lock_barcode_type:small小料rubber胶料)',
|
||||
`reason_desc` varchar(500) NOT NULL COMMENT '原因(手动输入,必填)',
|
||||
`tenant_id` int DEFAULT NULL COMMENT '租户',
|
||||
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门',
|
||||
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||
|
||||
@@ -659,3 +659,378 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLoc
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLockReason.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLockReasonList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/components/MesXslRubberSmallLockReasonModal.vue
|
||||
|
||||
-- author:jiangxh---date:20250602--for: 【MES】锁定原因增加原因字段、新增胶料小料锁定日志 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_rubber_small_lock_reason_desc_and_log.sql
|
||||
jeecg-boot/db/mes-xsl-rubber-small-lock-log-menu-permission.sql
|
||||
jeecg-boot/db/mes-xsl-rubber-small-lock-reason-menu-permission.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberSmallLockReason.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberSmallLockReasonServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberSmallLockReasonController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberSmallLockLog.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberSmallLockLogMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberSmallLockLogService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberSmallLockLogServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberSmallLockLogController.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLockReason.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLockReason.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLog.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLog.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLogList.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【QH-MES审批台账】跨 MES/钉钉 统一审批门禁台账 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_125__mes_xsl_approval_record.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/constant/ApprovalRecordConstants.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalRecord.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalRecordMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalRecordService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalRecordServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalGateService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalGateServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/vo/ApprovalGateVo.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalGateController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java
|
||||
jeecgboot-vue3/src/views/approval/gate/approvalGate.api.ts
|
||||
jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue
|
||||
jeecgboot-vue3/src/components/DingTplLaunch/index.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【QH-MES审批台账】MESToDing审批配置下增加审批台账菜单与列表页 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_126__mes_xsl_approval_record_menu.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalRecordController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/components/MesXslApprovalRecordDetailModal.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【钉钉Stream回调】Stream模式接收钉钉审批结果并回写MES审批台账 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】修复审批注册启用列不显示、清空启用环节无法保存 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/entity/MesXslBizDocRegistry.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslBizDocRegistryModal.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】审批注册中心操作列新增查看明细抽屉 -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistryList.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】查看明细按钮改用注册中心list权限并放宽接口鉴权 -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistryList.vue
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslApprovalTraceController.java
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】审批注册中心环节同步/回退执行器+密炼PS无代码方案 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_138__mes_xsl_registry_stage_sync.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/RegistryStageFieldHelper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/RegistryStageSyncExecutor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/executor/RegistryStageRevertExecutor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationContext.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlanWizard.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】审批流设计环节改读审批注册中心 -----
|
||||
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/ApprovalFlowList.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
|
||||
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】集成方案与审批注册中心环节绑定 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_137__mes_xsl_integration_plan_stage_bind.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/entity/MesXslIntegrationPlan.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/ApprovalStageResolver.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IMesXslBizDocRegistryService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IMesXslIntegrationPlanService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslBizDocRegistryServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslIntegrationPlanServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlanWizard.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】钉钉校对通过后原单状态同步修复 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/ApprovalStageResolver.java
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】审批环节同步前置状态改字典下拉 -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】VisualActionEditor兼容Flyway扁平actionConfig -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】集成方案动作管理复用可视化编辑器 -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】密炼PS操作接口停用+流程节点改绑集成方案 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerPsCompileController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixerPsCompile/MesXslMixerPsCompileList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixerPsCompile/MesXslMixerPsCompile.api.ts
|
||||
jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】审批注册中心新增查看明细菜单按钮权限并自动授权 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_136__mes_xsl_biz_doc_registry_trace_perm.sql
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistryList.vue
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslApprovalTraceController.java
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】末节点批准方案可在流程设计器下拉中选择 -----
|
||||
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/flowTypes.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/GenerateDefaultPlanModal.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】识别环节支持手选且可为空 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/GenerateDefaultPlanModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】按实际流程节点生成并展示环节配置状态 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/GenerateDefaultPlanModal.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】按审批流程节点一键生成默认集成方案 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlanList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/GenerateDefaultPlanModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】多轮审批集成幂等修复(台账recordId+回退强制重跑) -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
|
||||
|
||||
-- author:GHT---date:20260605--for: 【XSLMES-20260605-K8R2】钉钉回调全链路统一日志前缀[钉钉回调] -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamSdkRunner.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkWorkflowService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/callback/ApprovalCallbackDispatcher.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationBizCallback.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
|
||||
|
||||
-- author:GHT---date:20260608--for: 【审批注册中心】明细表查看钉钉审批流转记录时间轴 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/entity/MesXslApprovalTrace.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingOperationRecordVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingProcessInstanceFlowVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IMesXslApprovalTraceService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslApprovalTraceServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslApprovalTraceController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/DingApprovalFlowTimelineModal.vue
|
||||
|
||||
-- author:GHT---date:20260608--for: 【审批注册中心】钉钉操作人ID映射本地用户姓名 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingOperationRecordVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslApprovalTraceServiceImpl.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/DingApprovalFlowTimelineModal.vue
|
||||
|
||||
-- author:GHT---date:20260608--for: 【审批注册中心】流转记录列新增查看审批节点(processForecast) -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkWorkflowService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingProcessForecastVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingProcessForecastNodeVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IMesXslApprovalTraceService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslApprovalTraceServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslApprovalTraceController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/DingApprovalForecastModal.vue
|
||||
|
||||
-- author:GHT---date:20260608--for: 【审批注册中心】processForecast携带MES发起approvers还原审批节点 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingApprovalLaunchParamBuilder.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingProcessForecastVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/vo/DingProcessForecastNodeVO.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/impl/MesXslApprovalTraceServiceImpl.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/DingApprovalForecastModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
||||
|
||||
-- author:GHT---date:20260609--for: 【钉钉Stream开发】第三方配置页可视化Stream接收节点 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_145__sys_third_app_config_stream_node.sql
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamNodeConfigService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/DingTalkStreamConfigController.java
|
||||
jeecgboot-vue3/src/views/system/appconfig/ThirdApp.data.ts
|
||||
jeecgboot-vue3/src/views/system/appconfig/ThirdApp.api.ts
|
||||
jeecgboot-vue3/src/views/system/appconfig/ThirdAppConfigModal.vue
|
||||
jeecgboot-vue3/src/views/system/appconfig/ThirdAppDingTalkConfigForm.vue
|
||||
|
||||
-- author:GHT---date:20260609--for: 【钉钉Stream开发】本机白名单仅指定电脑接收回调 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamProperties.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingApprovalReconcileScheduler.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamHealthMonitor.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev-local.yml.example
|
||||
jeecg-boot/.gitignore
|
||||
|
||||
-- author:GHT---date:20260609--for: 【钉钉Stream集群】Redis选主单节点建连+存活监控 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamProperties.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamLeaderElection.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamHealthMonitor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamSdkRunner.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
|
||||
|
||||
-- author:cursor---date:20260608--for: 【XSLMES-20260608-A01】混炼示方新增状态字段及列表查询条件 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_141__mes_xsl_mixing_spec_status.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/VisualActionEditor.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【MESToDing审批配置】钉钉模板列表操作列绑定审批流程弹窗 -----
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/BindApprovalFlowModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【MESToDing审批配置】模板名称 MES↔钉钉 双向同步 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【钉钉审批模板绑定】字段绑定支持原值/显示文本 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/vo/PrintBizFieldItemVO.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/print/util/PrintBizEntityFieldIntrospector.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingTplBindController.java
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplFieldValue.ts
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplBind.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/index.vue
|
||||
jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【审批流设计】节点内生成集成方案并配置动作 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/service/IntegrationPlanGenerator.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslIntegrationPlanController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslIntegrationPlan.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslIntegrationActionDrawer.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【审批注册中心】物理表名改为数据库表下拉选择 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/controller/MesXslBizDocRegistryController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslBizDocRegistry.data.ts
|
||||
|
||||
-- author:GHT---date:20260610--for: 【配合示方】审批进度展示改为关联痕迹表 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/advice/ApprovalTraceResponseAdvice.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/traceRecordHelper.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/components/MesXslFormulaSpecModal.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【混炼示方】页脚签章区展示痕迹表审批人/时间 -----
|
||||
jeecgboot-vue3/src/views/xslmes/approval/integration/traceRecordHelper.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【配合示方】移除手写痕迹列,改由 useListPage 统一注入 6 列 -----
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslFormulaSpec/MesXslFormulaSpec.data.ts
|
||||
|
||||
-- author:GHT---date:20260610--for: 【混炼示方】页脚起草人/变更人展示姓名 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpec.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】MES发起审批按钮按ding_tpl_bind路由匹配(与钉钉按钮一致) -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
|
||||
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】IM工作通知公众号(同事列表置顶+审批消息统一推送) -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_147__sys_im_work_notify_user.sql
|
||||
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/vo/SysImContactVO.java
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
|
||||
jeecgboot-vue3/src/views/system/im/ImChat.vue
|
||||
jeecgboot-vue3/src/views/system/im/imCache.ts
|
||||
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】审批待办IM发送修复(admin自审重复成员+独立事务防审批回滚) -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】IM卡片字段取值修复(下划线列名+valueMode与钉钉发起对齐) -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplBindFieldValueResolver.java
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】流转中实例支持补发IM审批卡片(审批台账入口) -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java
|
||||
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】发起人=处理人时审批待办改由admin代发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/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
|
||||
-- author:GHT---date:20260610--for: 【IM审批通用化】IM卡片复用钉钉模板字段+MES回调补齐stageKey走集成方案 -----
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/DingTplImCardBuilder.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/IMesXslDingTplBindService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/service/impl/MesXslDingTplBindServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
jeecgboot-vue3/src/views/system/im/imBizRecordMessage.ts
|
||||
jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue
|
||||
|
||||
-- author:GHT---date:20260610--for: 【混炼示方】TCU温度条件新增是否附加/重量字段 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_146__mes_xsl_mixing_spec_tcu_attach.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixingSpecTcu.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/MesXslMixingSpec.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslMixingSpec/components/MesXslMixingSpecModal.vue
|
||||
|
||||
-- author:jiangxh---date:20250602--for: 【MES】设备台账原设备编号改为自定义编号、新增001自增只读系统编号 ---
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_122__mes_xsl_equipment_ledger_ledger_no.sql
|
||||
jeecg-boot/db/mes-xsl-equipment-ledger.sql
|
||||
jeecg-boot/db/mes-xsl-equipment-ledger-menu-permission.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslEquipmentLedger.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslEquipmentLedgerMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslEquipmentLedgerService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslEquipmentLedgerServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslEquipmentLedgerController.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/components/MesXslEquipmentLedgerModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipInspectConfig/components/MesXslEquipmentLedgerSelectModal.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipInspectConfig/components/MesXslEquipmentLedgerMultiSelectModal.vue
|
||||
|
||||
-- author:jiangxh---date:20250602--for: 【MES】设备台账设备类别、设备类型必填 ---
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.data.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslEquipmentLedgerController.java
|
||||
|
||||
-- author:jiangxh---date:20250602--for: 【MES】设备台账 ledgerNo 显示名改为设备编号 ---
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.data.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslEquipmentLedger.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslEquipmentLedgerController.java
|
||||
|
||||
-- author:jiangxh---date:20250602--for: 【MES】设备台账 ledgerNo 显示名改为系统编号、equipmentCode 改为设备编号 ---
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipmentLedger/MesXslEquipmentLedger.data.ts
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslEquipmentLedger.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslEquipmentLedgerController.java
|
||||
|
||||
-- author:jiangxh---date:20250603--for: 【MES】设备对应部位:点检配置保存后按大部位+小部位去重生成,列表只读无新增 ---
|
||||
jeecg-boot/db/mes-xsl-equip-part-mapping-menu-permission.sql
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_123__mes_xsl_equip_part_mapping.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslEquipPartMapping.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslEquipPartMappingMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslEquipPartMappingService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslEquipPartMappingServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslEquipPartMappingController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslEquipInspectConfigServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/package-info.java
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipPartMapping/MesXslEquipPartMappingList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipPartMapping/MesXslEquipPartMapping.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslEquipPartMapping/MesXslEquipPartMapping.api.ts
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
# 审核集成功能 — 整体方案计划
|
||||
|
||||
> **版本:** V1.0
|
||||
> **日期:** 2026-06-05
|
||||
> **定位:** 在现有 MES 审批引擎之上,新增**审批后业务编排层**,实现「哪些单要审、审完自动生成/更新下游单」的可配置化集成。
|
||||
> **原则:** 审批归审批、编排归编排、复杂逻辑归代码;与 Online 增强模型对齐、与现有 `@ApprovalBizAction` / Gate 台账兼容。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [一、背景与目标](#一背景与目标)
|
||||
- [二、总体架构](#二总体架构)
|
||||
- [三、功能模块设计](#三功能模块设计)
|
||||
- [四、数据模型设计](#四数据模型设计)
|
||||
- [五、核心引擎设计](#五核心引擎设计)
|
||||
- [六、业务流程](#六业务流程)
|
||||
- [七、前端设计](#七前端设计)
|
||||
- [八、Online 表单集成策略](#八online-表单集成策略)
|
||||
- [九、与现有机制的关系](#九与现有机制的关系)
|
||||
- [十、分期实施计划](#十分期实施计划)
|
||||
- [十一、技术规范](#十一技术规范)
|
||||
- [十二、风险与应对](#十二风险与应对)
|
||||
- [十三、验收标准](#十三验收标准)
|
||||
- [十四、资源与依赖](#十四资源与依赖)
|
||||
- [十五、首个试点场景](#十五首个试点场景)
|
||||
- [十六、下一步行动](#十六下一步行动)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 1.1 现状
|
||||
|
||||
项目已具备较完整的审批底座:
|
||||
|
||||
| 能力 | 现有实现 |
|
||||
|-----|---------|
|
||||
| 审批流设计 | `MesXslApprovalFlow` + 前端 `ApprovalDesign` |
|
||||
| 审批办理 | `MesXslApprovalHandleServiceImpl` |
|
||||
| 业务回调 | `IApprovalBizCallback` / `@ApprovalBizAction` |
|
||||
| 跨通道门禁 | `MesXslApprovalGateService` + `MesXslApprovalRecord` |
|
||||
| 钉钉/OA | Stream 回调 + 流程模板 |
|
||||
|
||||
**缺口:** 下游单据生成、跨单字段回写仍依赖各模块硬编码 Callback,无法由实施/用户在界面配置。
|
||||
|
||||
### 1.2 建设目标
|
||||
|
||||
| 目标 | 说明 |
|
||||
|-----|------|
|
||||
| **G1 可配置** | 配置源单 → 审批策略 → 审后动作,减少重复 Java 开发 |
|
||||
| **G2 可编排** | 支持 CREATE / UPDATE / CALL_API / CALL_HANDLER 多动作顺序执行 |
|
||||
| **G3 可观测** | 每次编排有日志、幂等、失败可重试、台账可追踪 |
|
||||
| **G4 可兼容** | 不破坏现有 Callback;Online 表与 Codegen 表统一接入 |
|
||||
| **G5 可扩展** | 复杂业务(配方计算、BOM 展开)仍走 Java Handler |
|
||||
|
||||
### 1.3 不在本期范围(明确边界)
|
||||
|
||||
- 不替换现有审批流设计器(人怎么审仍用 Flow)
|
||||
- 不重做 Online BPM / Flowable 流程引擎
|
||||
- 不做通用 ETL / 消息中间件级集成平台
|
||||
- 不做前端 JS 编排引擎(审后动作必须服务端执行)
|
||||
|
||||
---
|
||||
|
||||
## 二、总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph L1["L1 配置层"]
|
||||
R[单据注册中心]
|
||||
P[审核策略 Policy]
|
||||
I[集成方案 Plan]
|
||||
A[集成动作 Action]
|
||||
end
|
||||
|
||||
subgraph L2["L2 审批层(已有)"]
|
||||
G[ApprovalGate 门禁]
|
||||
F[ApprovalFlow 审批流]
|
||||
REC[ApprovalRecord 台账]
|
||||
H[HandleService 办理引擎]
|
||||
end
|
||||
|
||||
subgraph L3["L3 编排层(新建)"]
|
||||
O[IntegrationOrchestrator]
|
||||
M[FieldMappingEngine]
|
||||
E[ActionExecutor 策略池]
|
||||
L[IntegrationLog 执行日志]
|
||||
end
|
||||
|
||||
subgraph L4["L4 执行通道"]
|
||||
SQL[SQL_UPDATE]
|
||||
API[CALL_API]
|
||||
HD[CALL_HANDLER]
|
||||
CR[CREATE/UPDATE Doc]
|
||||
ONL[Online Form API]
|
||||
end
|
||||
|
||||
R --> P --> G
|
||||
P --> I --> A
|
||||
G --> F --> H --> REC
|
||||
H -->|终态事件| O
|
||||
A --> O --> M --> E
|
||||
E --> SQL & API & HD & CR
|
||||
CR --> ONL
|
||||
O --> L
|
||||
```
|
||||
|
||||
### 三层职责划分
|
||||
|
||||
| 层 | 职责 | 配置主体 |
|
||||
|----|------|---------|
|
||||
| L1 配置层 | 定义单据元数据、审核策略、审后编排 | 实施/管理员 |
|
||||
| L2 审批层 | 谁审、几级审、MES/钉钉通道 | 已有 Flow 设计器 |
|
||||
| L3 编排层 | 审完自动生成/改数/调接口 | 集成方案设计器 |
|
||||
|
||||
---
|
||||
|
||||
## 三、功能模块设计
|
||||
|
||||
### 3.1 模块清单
|
||||
|
||||
| 模块 | 包路径建议 | 说明 |
|
||||
|-----|-----------|------|
|
||||
| 单据注册 | `approval.integration.registry` | 管理可参与集成的单据元数据 |
|
||||
| 审核策略 | `approval.integration.policy` | 哪些单、何时必须审、绑哪条流 |
|
||||
| 集成方案 | `approval.integration.plan` | 审后编排方案(含动作明细) |
|
||||
| 编排引擎 | `approval.integration.orchestrator` | 统一调度入口 |
|
||||
| 字段映射 | `approval.integration.mapping` | 变量解析、映射计算 |
|
||||
| 动作执行器 | `approval.integration.executor.*` | 各 action_type 实现 |
|
||||
| 执行日志 | `approval.integration.log` | 审计、重试、幂等 |
|
||||
| 管理 API | `approval.integration.controller` | CRUD + 测试预览 + 手动重试 |
|
||||
| 前端页面 | `views/xslmes/approval/integration/` | 4 个管理页 + 1 个设计器 |
|
||||
|
||||
### 3.2 与现有代码的挂接点(最小侵入)
|
||||
|
||||
在以下位置增加编排调用(**不替换**现有逻辑):
|
||||
|
||||
```
|
||||
MesXslApprovalHandleServiceImpl
|
||||
├─ 节点通过 → callbackDispatcher.fireNodeApproved()
|
||||
│ └─ + integrationOrchestrator.execute(ctx, NODE_APPROVED)
|
||||
├─ 最终通过 → callbackDispatcher.fireApproved()
|
||||
│ └─ + integrationOrchestrator.execute(ctx, APPROVED)
|
||||
└─ 驳回 → callbackDispatcher.fireRejected()
|
||||
└─ + integrationOrchestrator.execute(ctx, REJECTED)
|
||||
|
||||
DingBpmsEventProcessor(钉钉终态)
|
||||
└─ finishByExternalInstance 成功后
|
||||
└─ + integrationOrchestrator.executeByRecord(record, APPROVED/REJECTED)
|
||||
```
|
||||
|
||||
**执行顺序(同一事务内):**
|
||||
|
||||
1. 现有 `IApprovalBizCallback`(兼容老逻辑)
|
||||
2. 现有节点 `callbackActions`(HTTP 调用)
|
||||
3. **新增**集成编排(按 `exec_order` 顺序)
|
||||
|
||||
任一环节抛异常 → 整笔审批动作回滚(与现有 Callback 事务策略一致)。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据模型设计
|
||||
|
||||
### 4.1 表结构(6 张)
|
||||
|
||||
#### ① `mes_xsl_biz_doc_registry` — 单据注册
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| doc_code | varchar(64) | 业务编码,如 `formula_spec` |
|
||||
| table_name | varchar(128) | 物理表名 |
|
||||
| display_name | varchar(128) | 中文名 |
|
||||
| doc_source | varchar(16) | `online` / `codegen` / `manual` |
|
||||
| online_head_id | varchar(32) | Online 表 headId(可空) |
|
||||
| title_field | varchar(64) | 标题字段 |
|
||||
| status_field | varchar(64) | 状态字段 |
|
||||
| route_path | varchar(256) | 前端路由 |
|
||||
| is_master | tinyint | 是否主表 |
|
||||
| child_config | json | 子表 `[{table,fkField,docCode}]` |
|
||||
| field_schema | json | 字段缓存(可从 Online/实体同步) |
|
||||
| enabled | tinyint | 启用 |
|
||||
|
||||
#### ② `mes_xsl_approval_policy` — 审核策略
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| policy_code / policy_name | 策略编码/名称 |
|
||||
| source_doc_code | 源单据 |
|
||||
| enabled | 启用 |
|
||||
| trigger_event | `manual` / `on_submit` / `on_status_change` |
|
||||
| match_condition | 条件表达式(Online exp 语法) |
|
||||
| approval_channel | `mes` / `dingtalk` / `auto` |
|
||||
| flow_id | MES 审批流 ID |
|
||||
| ding_tpl_id | 钉钉模板 ID |
|
||||
| integration_plan_id | 绑定的集成方案 |
|
||||
| priority | 同单据多策略优先级 |
|
||||
| tenant_id | 租户 |
|
||||
|
||||
#### ③ `mes_xsl_approval_integration_plan` — 集成方案
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| plan_code / plan_name | 方案编码/名称 |
|
||||
| source_doc_code | 源单据 |
|
||||
| match_condition | 方案级条件(可空=默认) |
|
||||
| trigger_phase | `onApprove` / `onReject` / `onNodeApprove` |
|
||||
| version | 版本号 |
|
||||
| status | `0草稿 1已发布 2已停用` |
|
||||
| remark | 备注 |
|
||||
|
||||
#### ④ `mes_xsl_approval_integration_action` — 集成动作
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| plan_id | 所属方案 |
|
||||
| action_code / action_name | 动作编码/名称 |
|
||||
| action_type | 见下表 |
|
||||
| trigger_phase | 可覆盖方案级 phase |
|
||||
| target_doc_code | 目标单据 |
|
||||
| target_lookup | 定位已有单的 JSON 规则 |
|
||||
| field_mappings | 字段映射 JSON |
|
||||
| exec_config | 动作扩展配置 JSON |
|
||||
| exec_order | 执行顺序 |
|
||||
| on_fail | `stop` / `continue` |
|
||||
| idempotent_key | 幂等键表达式 |
|
||||
| enabled | 启用 |
|
||||
|
||||
**action_type 枚举:**
|
||||
|
||||
| 类型 | 说明 | 对标 Online |
|
||||
|-----|------|------------|
|
||||
| `SQL_UPDATE` | 执行 UPDATE SQL | SQL 增强 |
|
||||
| `UPDATE_DOC` | 结构化更新单据 | — |
|
||||
| `CREATE_DOC` | 生成主+子表 | Online form add |
|
||||
| `CALL_API` | HTTP 调业务接口 | Java http 增强 |
|
||||
| `CALL_HANDLER` | Spring Bean / Class | Java spring/class |
|
||||
| `DELEGATE_ONLINE` | 委托 Online 增强 | 复用已有增强 |
|
||||
|
||||
#### ⑤ `mes_xsl_approval_integration_log` — 执行日志
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| record_id | 审批台账 ID |
|
||||
| instance_id | MES 实例 ID(可空) |
|
||||
| plan_id / action_id | 方案/动作 |
|
||||
| idempotent_key | 幂等键 |
|
||||
| status | `success` / `failed` / `skipped` |
|
||||
| source_biz_id | 源单 ID |
|
||||
| target_biz_id | 目标单 ID |
|
||||
| request_snapshot | 映射后 payload |
|
||||
| response_snapshot | 执行结果 |
|
||||
| error_message | 错误信息 |
|
||||
| retry_count | 重试次数 |
|
||||
| exec_time_ms | 耗时 |
|
||||
|
||||
#### ⑥ `mes_xsl_approval_record` 扩展字段
|
||||
|
||||
| 新增字段 | 说明 |
|
||||
|---------|------|
|
||||
| integration_status | `0未执行 1成功 2部分失败 3失败` |
|
||||
| integration_remark | 编排摘要/错误 |
|
||||
|
||||
---
|
||||
|
||||
## 五、核心引擎设计
|
||||
|
||||
### 5.1 编排上下文 `IntegrationContext`
|
||||
|
||||
在现有 `ApprovalCallbackContext` 基础上扩展:
|
||||
|
||||
```java
|
||||
IntegrationContext {
|
||||
ApprovalCallbackContext approvalCtx; // 审批上下文
|
||||
MesXslApprovalRecord record; // 台账
|
||||
Map<String,Object> sourceRecord; // 源单主表数据
|
||||
Map<String,List<Map>> sourceChildren; // 源单子表
|
||||
Map<String,Object> vars; // 运行时变量池
|
||||
Map<String,String> actionResults; // 前序动作产出(如 targetId)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 变量体系(对齐 Online SQL 增强)
|
||||
|
||||
| 变量 | 含义 |
|
||||
|-----|------|
|
||||
| `#{source.id}` / `#{id}` | 源单 ID |
|
||||
| `#{source.字段名}` | 源单字段 |
|
||||
| `#{sys_user_code}` | 当前操作人 |
|
||||
| `#{sys_date}` / `#{sys_time}` | 系统日期时间 |
|
||||
| `#{approval.instance_id}` | 审批实例 |
|
||||
| `#{approval.apply_user}` | 发起人 |
|
||||
| `#{action.xxx.target_id}` | 前序动作生成的目标单 ID |
|
||||
|
||||
### 5.3 字段映射 JSON 规范
|
||||
|
||||
```json
|
||||
{
|
||||
"masterMappings": [
|
||||
{ "target": "spec_no", "type": "direct", "source": "formula_no" },
|
||||
{ "target": "status", "type": "constant", "value": "1" },
|
||||
{ "target": "approved_by", "type": "var", "value": "#{sys_user_code}" }
|
||||
],
|
||||
"childMappings": [
|
||||
{
|
||||
"targetChildTable": "mes_xsl_mixing_spec_line",
|
||||
"sourceChildTable": "mes_xsl_formula_spec_line",
|
||||
"fkField": "main_id",
|
||||
"rowFilter": "stage_no <= 3",
|
||||
"mappings": [
|
||||
{ "target": "material_code", "type": "direct", "source": "material_code" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**映射类型:** `direct` / `constant` / `var` / `expression` / `lookup` / `ref_action`
|
||||
|
||||
### 5.4 动作执行器接口
|
||||
|
||||
```java
|
||||
public interface IIntegrationActionExecutor {
|
||||
String supportActionType();
|
||||
IntegrationActionResult execute(IntegrationContext ctx, IntegrationAction action);
|
||||
}
|
||||
|
||||
// Handler 扩展(复杂场景)
|
||||
public interface IIntegrationActionHandler {
|
||||
String supportDocCode(); // "*" 通用
|
||||
IntegrationActionResult execute(IntegrationContext ctx, JSONObject config);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 幂等与重试
|
||||
|
||||
- **幂等键**:默认 `record_id + action_id`;CREATE 可配 `source_id + target_doc_code`
|
||||
- **已 success**:跳过(status=skipped)
|
||||
- **failed**:管理端可「手动重试」,retry_count+1
|
||||
- **钉钉/MES 双通道**:以 `record_id` 为统一键,避免重复生成
|
||||
|
||||
---
|
||||
|
||||
## 六、业务流程
|
||||
|
||||
### 6.1 发起审批
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant FE as 前端/Online JS
|
||||
participant G as ApprovalGate
|
||||
participant P as PolicyService
|
||||
participant L as LaunchController
|
||||
participant R as ApprovalRecord
|
||||
|
||||
U->>FE: 点击「提交审核」
|
||||
FE->>G: checkCanLaunch(bizTable, bizDataId)
|
||||
G->>R: 查最新台账
|
||||
G-->>FE: allowed / reason
|
||||
FE->>P: 匹配审核策略(可选)
|
||||
FE->>L: 发起审批(flowId / dingTpl)
|
||||
L->>R: createRunningRecord
|
||||
L->>H: enterFirstNode
|
||||
```
|
||||
|
||||
### 6.2 审批通过 → 编排执行
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as HandleService
|
||||
participant CB as CallbackDispatcher
|
||||
participant O as Orchestrator
|
||||
participant E as ActionExecutor
|
||||
participant L as IntegrationLog
|
||||
|
||||
H->>CB: fireApproved(ctx)
|
||||
H->>O: execute(ctx, APPROVED)
|
||||
O->>O: 匹配 Plan + Actions
|
||||
loop 按 exec_order
|
||||
O->>O: 检查幂等
|
||||
O->>E: execute(action)
|
||||
E-->>O: result / exception
|
||||
O->>L: 写日志
|
||||
end
|
||||
O-->>H: 全部成功 / 抛异常回滚
|
||||
```
|
||||
|
||||
### 6.3 配置发布流程
|
||||
|
||||
```
|
||||
草稿方案 → 测试预览(选源单ID,dry-run 不落库)→ 发布 → 生效
|
||||
↑ ↓
|
||||
└──────── 版本回滚 ← 停用旧版本 ←────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、前端设计
|
||||
|
||||
### 7.1 菜单结构
|
||||
|
||||
```
|
||||
MES 审批管理
|
||||
├── 审批流设计(已有)
|
||||
├── 审批台账(已有)
|
||||
├── 审核集成(新建)
|
||||
│ ├── 单据注册中心
|
||||
│ ├── 审核策略配置
|
||||
│ ├── 集成方案管理
|
||||
│ └── 集成执行日志
|
||||
```
|
||||
|
||||
### 7.2 页面说明
|
||||
|
||||
| 页面 | 核心功能 |
|
||||
|-----|---------|
|
||||
| **单据注册中心** | 列表 CRUD;从 Online 同步;从实体扫描;字段预览 |
|
||||
| **审核策略配置** | 选源单 → 条件 → 绑 Flow/钉钉模板 → 绑集成方案 |
|
||||
| **集成方案设计器** | 基本信息 → 动作列表 → 字段映射(主/子 Tab)→ 测试预览 → 发布 |
|
||||
| **集成执行日志** | 按台账/源单查;看 snapshot;失败重试 |
|
||||
|
||||
### 7.3 集成方案设计器(核心 UI)
|
||||
|
||||
**Step 1 — 基本信息**
|
||||
源单据、触发时机、匹配条件(可视化 exp 构建器)
|
||||
|
||||
**Step 2 — 动作列表**
|
||||
拖拽排序;增删动作;选 action_type
|
||||
|
||||
**Step 3 — 字段映射**
|
||||
左:源单字段树(来自 registry / Online API)
|
||||
右:目标单字段树
|
||||
中间:映射连线 + 类型选择
|
||||
|
||||
**Step 4 — 测试预览**
|
||||
输入源单 ID → 返回映射后的 JSON + SQL 预览 → 不执行
|
||||
|
||||
**Step 5 — 发布**
|
||||
版本号递增;旧版自动停用(同 source + phase 仅一个 published)
|
||||
|
||||
---
|
||||
|
||||
## 八、Online 表单集成策略
|
||||
|
||||
| 场景 | 做法 |
|
||||
|-----|------|
|
||||
| 源单是 Online 表 | 注册时 `doc_source=online`,字段从 `listByHeadId` 同步 |
|
||||
| 目标单是 Online 表 | `CREATE_DOC` 调 `POST /online/cgform/api/form/{code}` |
|
||||
| 已有 SQL/Java 增强 | `DELEGATE_ONLINE` 动作,填 buttonCode + event |
|
||||
| 发起端 | Online JS `beforeSubmit` 调 Gate API;自定义「提交审核」按钮 |
|
||||
| 与 Online BPM | **不混用**;Online 表统一走 MES Gate + Policy |
|
||||
|
||||
### Online 增强机制借鉴对照
|
||||
|
||||
| Online 增强 | 审核集成模块 | 现有审批 |
|
||||
|------------|-------------|---------|
|
||||
| `buttonCode` | `action_code` | `@ApprovalBizAction.name` |
|
||||
| `event: start/end` | `trigger_phase` | `phase: onApprove/onReject` |
|
||||
| SQL 增强 | `UPDATE_DOC` / `SQL_UPDATE` | 节点 callbackActions |
|
||||
| Java spring/class/http | `CALL_HANDLER` / `CALL_API` | Callback / HttpExecutor |
|
||||
| 自定义按钮 `exp` | `match_condition` | 流程条件分支 |
|
||||
| JS `beforeSubmit` | 发起审批前门禁 | ApprovalGate |
|
||||
| `onl_cgform_field` | 字段映射数据源 | Flow.titleField |
|
||||
|
||||
---
|
||||
|
||||
## 九、与现有机制的关系
|
||||
|
||||
```
|
||||
优先级(同一 trigger_phase):
|
||||
1. IApprovalBizCallback(代码 SPI,长期保留)
|
||||
2. Flow 节点 callbackActions(精细节点控制)
|
||||
3. IntegrationOrchestrator(配置化编排,新增)
|
||||
|
||||
迁移策略:
|
||||
- 新单据:优先配置集成方案
|
||||
- 老 Callback:逐步迁移,不强制一次性改
|
||||
- @ApprovalBizAction:作为 CALL_API 动作的数据源(复用 Registry)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、分期实施计划
|
||||
|
||||
### Phase 0 — 基础骨架(约 2 周)
|
||||
|
||||
**目标:** 跑通「审通过后 SQL 改字段」最小闭环
|
||||
|
||||
| 交付物 | 内容 |
|
||||
|-------|------|
|
||||
| Flyway | 6 张表 DDL + 字典项 |
|
||||
| 后端 | registry / policy / plan / action CRUD |
|
||||
| 引擎 | `IntegrationOrchestrator` + `SQL_UPDATE` 执行器 |
|
||||
| 挂接 | `HandleService` 终态调用编排 |
|
||||
| 日志 | 幂等 + integration_log |
|
||||
| 前端 | 策略列表 + 方案列表(JSON 编辑,暂无可视化映射) |
|
||||
| 试点 | 选 1 个简单单据:审批通过 UPDATE status |
|
||||
|
||||
**验收:** 配置一条方案 → 审批通过 → 源单字段被更新 → 日志可查 → 重复回调不重复执行
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — 单据编排(约 3 周)
|
||||
|
||||
**目标:** 支持 CREATE / UPDATE 主表 + 子表
|
||||
|
||||
| 交付物 | 内容 |
|
||||
|-------|------|
|
||||
| 引擎 | `FieldMappingEngine` + `CREATE_DOC` + `UPDATE_DOC` |
|
||||
| 注册 | Online 同步 + Codegen 手工注册 |
|
||||
| 执行 | Codegen 走 Service;Online 走 form API |
|
||||
| 前端 | 单据注册页 + 方案动作配置(表单式) |
|
||||
| 试点 | 1 条「源单 → 生成下游单」业务链 |
|
||||
|
||||
**验收:** 审批通过自动生成主子表下游单;源单回写下游 ID
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — 扩展与对接(约 2 周)
|
||||
|
||||
**目标:** 对接现有 `@ApprovalBizAction`、钉钉通道、Handler
|
||||
|
||||
| 交付物 | 内容 |
|
||||
|-------|------|
|
||||
| 执行器 | `CALL_API`(复用 ApprovalActionHttpExecutor) |
|
||||
| 执行器 | `CALL_HANDLER`(spring/class) |
|
||||
| 执行器 | `DELEGATE_ONLINE` |
|
||||
| 挂接 | DingBpmsEventProcessor 终态触发编排 |
|
||||
| 台账 | record.integration_status 展示 |
|
||||
| 前端 | 执行日志页 + 手动重试 |
|
||||
|
||||
**验收:** MES/钉钉双通道审通过后编排一致;失败可重试
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — 可视化设计器(约 3 周)
|
||||
|
||||
**目标:** 实施人员可自助配置,无需写 JSON
|
||||
|
||||
| 交付物 | 内容 |
|
||||
|-------|------|
|
||||
| 前端 | 字段映射可视化(双树连线) |
|
||||
| 前端 | 条件表达式构建器(Online exp 语法) |
|
||||
| 后端 | dry-run 测试预览 API |
|
||||
| 后端 | 方案版本管理 + 发布/回滚 |
|
||||
| 文档 | 实施配置手册 |
|
||||
|
||||
**验收:** 实施独立完成一条新集成链路,无需改 Java 代码
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — 增强与治理(持续)
|
||||
|
||||
| 内容 |
|
||||
|-----|
|
||||
| 监控告警(编排失败 IM/钉钉通知) |
|
||||
| 批量迁移老 Callback 到配置 |
|
||||
| expression / lookup 增强 |
|
||||
| 性能优化(大子表批量 insert) |
|
||||
| 权限:集成方案按角色授权 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、技术规范
|
||||
|
||||
### 11.1 代码规范
|
||||
|
||||
- 包路径:`org.jeecg.modules.xslmes.approval.integration.*`
|
||||
- 遵循 JeecgBoot 实体/Controller/Service 模式
|
||||
- 所有改动加 `update-begin/end` 注释(需 bug 号/需求号)
|
||||
- 表名/字段名白名单校验(复用现有 `IDENTIFIER` Pattern)
|
||||
|
||||
### 11.2 字典项(新增)
|
||||
|
||||
| 字典 | 值 |
|
||||
|-----|-----|
|
||||
| mes_xsl_integration_action_type | SQL_UPDATE / CREATE_DOC / UPDATE_DOC / CALL_API / CALL_HANDLER / DELEGATE_ONLINE |
|
||||
| mes_xsl_integration_trigger_phase | onApprove / onReject / onNodeApprove |
|
||||
| mes_xsl_integration_plan_status | 0草稿 / 1已发布 / 2已停用 |
|
||||
| mes_xsl_integration_log_status | success / failed / skipped |
|
||||
| mes_xsl_biz_doc_source | online / codegen / manual |
|
||||
|
||||
### 11.3 安全
|
||||
|
||||
- SQL_UPDATE 仅允许 UPDATE/INSERT(禁止 DROP/DELETE 全表)
|
||||
- 变量替换后 SQL 预编译或严格转义
|
||||
- CALL_API 仅允许内部白名单路径(复用 BizActionRegistry)
|
||||
- 编排执行默认用「系统机器人」账号,可配置为「最后审批人」
|
||||
|
||||
---
|
||||
|
||||
## 十二、风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
|-----|------|------|
|
||||
| 编排失败导致审批回滚 | 用户困惑 | 默认同事务;可选 afterCommit 异步 + 补偿(Phase 4) |
|
||||
| 主子表映射配置错误 | 脏数据 | dry-run 预览 + 发布前校验 + 日志 snapshot |
|
||||
| 与现有 Callback 重复执行 | 双写 | 迁移期文档明确;方案级开关 `skip_legacy_callback` |
|
||||
| Online 与 Codegen 执行路径不一致 | 行为差异 | 统一 DocExecutor 抽象,按 doc_source 路由 |
|
||||
| 大子表性能 | 超时 | 批量 insert + 异步(Phase 4) |
|
||||
| 实施配置门槛高 | 推广慢 | Phase 3 可视化 + 模板库(常用方案复制) |
|
||||
|
||||
---
|
||||
|
||||
## 十三、验收标准
|
||||
|
||||
1. **配置化**:至少 3 条不同业务链路通过界面配置完成,无新增 Java Callback
|
||||
2. **双通道**:MES 审批、钉钉审批各至少 1 条链路编排正常
|
||||
3. **幂等**:同一审批终态重复回调,下游单不重复生成
|
||||
4. **可观测**:台账 + 集成日志可定位每次执行 input/output/error
|
||||
5. **兼容**:现有 `RubberQuickTestStdApprovalCallback` 等老逻辑不受影响
|
||||
6. **回滚**:编排失败时审批状态与业务数据一致(不回滚一半)
|
||||
|
||||
---
|
||||
|
||||
## 十四、资源与依赖
|
||||
|
||||
| 角色 | 职责 |
|
||||
|-----|------|
|
||||
| 后端 1 | 引擎 + 执行器 + 挂接 |
|
||||
| 后端 0.5 | CRUD + Online 对接 |
|
||||
| 前端 1 | 4 管理页 + 设计器 |
|
||||
| 实施/业务 | 试点场景定义 + 验收 |
|
||||
| DBA | Flyway 评审 |
|
||||
|
||||
**外部依赖:** MySQL、Redis(可选缓存 registry)、Online 模块 API、现有审批模块稳定运行。
|
||||
|
||||
---
|
||||
|
||||
## 十五、首个试点场景
|
||||
|
||||
选一条**链路清晰、主子表、价值明显**的业务做 Phase 0~1 试点:
|
||||
|
||||
> **配合示方**(`mes_xsl_formula_spec`)审批通过 →
|
||||
> ① UPDATE 源单 `audit_status=已批准`
|
||||
> ② CREATE 下游**密炼示方**(主表 + 明细映射)
|
||||
> ③ UPDATE 源单 `approved_mixing_id=下游ID`
|
||||
|
||||
该场景覆盖 CREATE + UPDATE + 主子映射,且与现有审批流已打通,最适合验证整体方案。
|
||||
|
||||
---
|
||||
|
||||
## 十六、下一步行动
|
||||
|
||||
1. **确认试点业务**(配合示方 or 其他)及需求号/bug 号(用于 update-begin 注释)
|
||||
2. **评审本方案** — 确认 Phase 划分、表结构、是否与 Online BPM 边界
|
||||
3. **启动 Phase 0** — 先出 Flyway DDL + `IntegrationOrchestrator` 骨架 + 1 条 SQL_UPDATE 试点
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关现有代码索引
|
||||
|
||||
| 文件 | 说明 |
|
||||
|-----|------|
|
||||
| `approval/entity/MesXslApprovalFlow.java` | 审批流定义 |
|
||||
| `approval/entity/MesXslApprovalRecord.java` | 审批台账 |
|
||||
| `approval/callback/ApprovalCallbackContext.java` | 回调上下文 |
|
||||
| `approval/callback/IApprovalBizCallback.java` | 业务回调 SPI |
|
||||
| `approval/action/ApprovalBizAction.java` | 可发现业务动作注解 |
|
||||
| `approval/service/impl/MesXslApprovalHandleServiceImpl.java` | 审批办理引擎 |
|
||||
| `approval/service/impl/MesXslApprovalGateServiceImpl.java` | 跨通道门禁 |
|
||||
| `approval/callback/ApprovalActionHttpExecutor.java` | 节点回调 HTTP 执行器 |
|
||||
| `views/approval/flow/` | 前端审批流设计器 |
|
||||
|
||||
---
|
||||
|
||||
*文档维护:随 Phase 推进更新版本号与交付状态。*
|
||||
@@ -19,7 +19,13 @@
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<!-- 复用打印模板模块:打印机枚举、业务绑定、PDF 提交队列 -->
|
||||
<!-- dingtalk-stream SDK GHT 20260604 -->
|
||||
<dependency>
|
||||
<groupId>com.dingtalk.open</groupId>
|
||||
<artifactId>dingtalk-stream</artifactId>
|
||||
<version>1.3.12</version>
|
||||
</dependency>
|
||||
<!-- jeecg-module-print 打印模板 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-system-biz</artifactId>
|
||||
|
||||
@@ -75,6 +75,19 @@ public class ApprovalActionHttpExecutor {
|
||||
if (node == null || inst == null) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
run(node, phase, inst.getBizDataId(), token);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】钉钉后台线程无HTTP上下文,支持显式传入token-----
|
||||
/**
|
||||
* 以显式 token 执行节点回调配置的接口(供无 HTTP 上下文的后台线程使用,如钉钉 Stream 回调)。
|
||||
* 当 token 为空时降级跳过(与原有行为一致)。
|
||||
*/
|
||||
public void run(JSONObject node, String phase, String bizDataId, String token) {
|
||||
if (node == null || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return;
|
||||
@@ -87,7 +100,6 @@ public class ApprovalActionHttpExecutor {
|
||||
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) {
|
||||
@@ -98,14 +110,14 @@ public class ApprovalActionHttpExecutor {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
// 无登录态(自动流转/无人值守) -> 降级跳过
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, inst.getBizDataId());
|
||||
log.warn("[审批回调] 无登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, bizDataId);
|
||||
continue;
|
||||
}
|
||||
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
|
||||
invoke(method, url, action.getJSONObject("body"), inst.getBizDataId(), token);
|
||||
invoke(method, url, action.getJSONObject("body"), bizDataId, token);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】钉钉后台线程无HTTP上下文,支持显式传入token-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.experimental.Accessors;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审批回调上下文。
|
||||
@@ -27,7 +28,11 @@ public class ApprovalCallbackContext implements Serializable {
|
||||
/** 整个流程最终通过 */
|
||||
APPROVED,
|
||||
/** 被驳回(任一节点驳回即终止) */
|
||||
REJECTED
|
||||
REJECTED,
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增CANCELLED动作,支持撤销时触发业务回滚回调-----------
|
||||
/** 流程被撤销/终止(TERMINATED) */
|
||||
CANCELLED
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增CANCELLED动作,支持撤销时触发业务回滚回调-----------
|
||||
}
|
||||
|
||||
/** 回调动作 */
|
||||
@@ -60,12 +65,27 @@ public class ApprovalCallbackContext implements Serializable {
|
||||
/** 当前/刚处理的节点名称 */
|
||||
private String nodeName;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】新增stageKey区分关键环节节点与纯过路审批节点-----------
|
||||
/**
|
||||
* 节点绑定的审批环节(来自流程设计 props.stageKey)。
|
||||
* null = 节点未配置 stageKey(旧数据/手动添加),走降级启发式匹配。
|
||||
* "" = 节点显式设为「纯过路审批」,不触发任何集成动作。
|
||||
* 其他值 = 具体环节(proofread / audit / approve),直接作为集成方案匹配依据。
|
||||
*/
|
||||
private String stageKey;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】新增stageKey区分关键环节节点与纯过路审批节点-----------
|
||||
|
||||
/** 操作人 username(系统自动处理时为 null/system) */
|
||||
private String operatorUsername;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】环节同步使用实例tasks最新完成时间-----------
|
||||
/** 操作时间(钉钉回调时为 tasks 最新 finishTime) */
|
||||
private Date operatorTime;
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】环节同步使用实例tasks最新完成时间-----------
|
||||
|
||||
/** 审批意见 / 驳回理由 */
|
||||
private String comment;
|
||||
|
||||
@@ -77,4 +97,18 @@ public class ApprovalCallbackContext implements Serializable {
|
||||
|
||||
/** 完整审批实例(供业务读取租户、发起信息等) */
|
||||
private transient MesXslApprovalInstance instance;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【缺陷修复-D1/D2】新增token和activityId字段,支持钉钉回调时传递真实审批人身份及节点精确定位-----------
|
||||
/**
|
||||
* 操作人JWT Token(钉钉回调时为审批人真实身份Token;MES内部审批时为null)。
|
||||
* 供 ApprovalActionHttpExecutor 等需要身份的调用方使用。
|
||||
*/
|
||||
private transient String token;
|
||||
|
||||
/**
|
||||
* 钉钉任务节点ID(operationRecords[].activityId,仅钉钉通道有值)。
|
||||
* 可供集成引擎或业务回调按节点精确匹配。
|
||||
*/
|
||||
private String activityId;
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【缺陷修复-D1/D2】新增token和activityId字段,支持钉钉回调时传递真实审批人身份及节点精确定位-----------
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.jeecg.modules.xslmes.approval.callback;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamSdkRunner;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -28,6 +29,8 @@ public class ApprovalCallbackDispatcher {
|
||||
/** 监听所有业务表的通配符 */
|
||||
private static final String ANY_TABLE = "*";
|
||||
|
||||
private static final String DING_LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||
|
||||
private final ObjectProvider<List<IApprovalBizCallback>> callbacksProvider;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@@ -58,19 +61,51 @@ public class ApprovalCallbackDispatcher {
|
||||
dispatch(ctx);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增fireCancelled,审批撤销时通知业务回滚-----------
|
||||
/** 撤销(TERMINATED) */
|
||||
public void fireCancelled(ApprovalCallbackContext ctx) {
|
||||
ctx.setAction(ApprovalCallbackContext.Action.CANCELLED);
|
||||
ctx.setFinalResult(true);
|
||||
dispatch(ctx);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增fireCancelled,审批撤销时通知业务回滚-----------
|
||||
|
||||
private void dispatch(ApprovalCallbackContext ctx) {
|
||||
if (ctx == null || oConvertUtils.isEmpty(ctx.getBizTable())) {
|
||||
if (isDingTalkCallback(ctx)) {
|
||||
log.info("{} 分发跳过:ctx 或 bizTable 为空 action={}", DING_LOG_TAG,
|
||||
ctx == null ? null : ctx.getAction());
|
||||
}
|
||||
return;
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调分发器全量日志-----------
|
||||
List<IApprovalBizCallback> callbacks = matchedCallbacks(ctx.getBizTable());
|
||||
if (isDingTalkCallback(ctx)) {
|
||||
log.info("{} 开始分发 action={} recordId={} bizTable={} bizDataId={} nodeId={} nodeName={} "
|
||||
+ "finalResult={} callbackCount={} comment={}",
|
||||
DING_LOG_TAG, ctx.getAction(), ctx.getInstanceId(), ctx.getBizTable(), ctx.getBizDataId(),
|
||||
ctx.getNodeId(), ctx.getNodeName(), ctx.isFinalResult(), callbacks.size(), ctx.getComment());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调分发器全量日志-----------
|
||||
// 1) 强类型回调:按表路由 + 通配
|
||||
for (IApprovalBizCallback cb : matchedCallbacks(ctx.getBizTable())) {
|
||||
for (IApprovalBizCallback cb : callbacks) {
|
||||
invoke(cb, ctx);
|
||||
}
|
||||
// 2) 领域事件:松耦合监听(同步、同事务)
|
||||
try {
|
||||
eventPublisher.publishEvent(new ApprovalActionEvent(this, ctx));
|
||||
if (isDingTalkCallback(ctx)) {
|
||||
log.info("{} 分发完成 action={} bizTable={} bizDataId={}", DING_LOG_TAG,
|
||||
ctx.getAction(), ctx.getBizTable(), ctx.getBizDataId());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批领域事件处理失败 table={}, bizId={}, action={}", ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
if (isDingTalkCallback(ctx)) {
|
||||
log.error("{} 领域事件处理失败 action={} table={} bizId={}", DING_LOG_TAG,
|
||||
ctx.getAction(), ctx.getBizTable(), ctx.getBizDataId(), e);
|
||||
} else {
|
||||
log.error("审批领域事件处理失败 table={}, bizId={}, action={}",
|
||||
ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +126,12 @@ public class ApprovalCallbackDispatcher {
|
||||
}
|
||||
|
||||
private void invoke(IApprovalBizCallback cb, ApprovalCallbackContext ctx) {
|
||||
boolean dingTalk = isDingTalkCallback(ctx);
|
||||
String callbackName = cb.getClass().getSimpleName();
|
||||
if (dingTalk) {
|
||||
log.info("{} 执行业务回调 {} action={} bizTable={} bizDataId={}",
|
||||
DING_LOG_TAG, callbackName, ctx.getAction(), ctx.getBizTable(), ctx.getBizDataId());
|
||||
}
|
||||
try {
|
||||
switch (ctx.getAction()) {
|
||||
case NODE_APPROVED:
|
||||
@@ -102,14 +143,34 @@ public class ApprovalCallbackDispatcher {
|
||||
case REJECTED:
|
||||
cb.onRejected(ctx);
|
||||
break;
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【风险修复-R5】分发撤销回调-----------
|
||||
case CANCELLED:
|
||||
cb.onCancelled(ctx);
|
||||
break;
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【风险修复-R5】分发撤销回调-----------
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (dingTalk) {
|
||||
log.info("{} 业务回调完成 {} action={} bizTable={} bizDataId={}",
|
||||
DING_LOG_TAG, callbackName, ctx.getAction(), ctx.getBizTable(), ctx.getBizDataId());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.error("审批业务回调执行失败 callback={}, table={}, bizId={}, action={}",
|
||||
cb.getClass().getSimpleName(), ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
if (dingTalk) {
|
||||
log.error("{} 业务回调失败 {} action={} table={} bizId={}",
|
||||
DING_LOG_TAG, callbackName, ctx.getAction(), ctx.getBizTable(), ctx.getBizDataId(), e);
|
||||
} else {
|
||||
log.error("审批业务回调执行失败 callback={}, table={}, bizId={}, action={}",
|
||||
callbackName, ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
|
||||
}
|
||||
// 抛出以回滚整个审批动作,保证审批与业务数据一致
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别钉钉Stream来源回调-----------
|
||||
private boolean isDingTalkCallback(ApprovalCallbackContext ctx) {
|
||||
return ctx != null && "dingtalk".equals(ctx.getOperatorUsername());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别钉钉Stream来源回调-----------
|
||||
}
|
||||
|
||||
@@ -61,4 +61,14 @@ public interface IApprovalBizCallback {
|
||||
default void onRejected(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增撤销回调,TERMINATED时通知业务回滚中间态状态-----------
|
||||
/**
|
||||
* 审批被撤销/终止(TERMINATED)。适合回退业务状态(如置回「草稿」、释放占用等)。
|
||||
* 默认空实现,业务按需重写。
|
||||
*/
|
||||
default void onCancelled(ApprovalCallbackContext ctx) {
|
||||
// 默认不处理
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-08 for:【风险修复-R5】新增撤销回调,TERMINATED时通知业务回滚中间态状态-----------
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.jeecg.modules.xslmes.approval.constant;
|
||||
|
||||
/**
|
||||
* 审批台账常量
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public final class ApprovalRecordConstants {
|
||||
|
||||
private ApprovalRecordConstants() {
|
||||
}
|
||||
|
||||
/** MES 审批通道 */
|
||||
public static final String CHANNEL_MES = "MES";
|
||||
|
||||
/** 钉钉审批通道 */
|
||||
public static final String CHANNEL_DINGTALK = "DINGTALK";
|
||||
|
||||
/** 流转中 */
|
||||
public static final String STATUS_RUNNING = "0";
|
||||
|
||||
/** 审批通过 */
|
||||
public static final String STATUS_APPROVED = "1";
|
||||
|
||||
/** 审批拒绝 */
|
||||
public static final String STATUS_REJECTED = "2";
|
||||
|
||||
/** 已撤销 */
|
||||
public static final String STATUS_CANCELLED = "3";
|
||||
|
||||
/** 发起失败 */
|
||||
public static final String STATUS_LAUNCH_FAILED = "4";
|
||||
}
|
||||
@@ -18,6 +18,9 @@ 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.integration.engine.ApprovalStageResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -57,21 +60,13 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】流程设计环节改读审批注册中心-----
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService bizDocRegistryService;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】流程设计环节改读审批注册中心-----
|
||||
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 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审批流设计】当前页字段解析-----
|
||||
|
||||
/**
|
||||
@@ -209,7 +204,7 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
/**
|
||||
* 设计上下文:供全局"审批流程设计"悬浮按钮调用。
|
||||
* 1) 根据当前功能页路由反查绑定的业务表;
|
||||
* 2) 解析该表的字段,识别"校对/审核/审批/分发/抄送"等阶段字段(不存在不报错,存在即解析);
|
||||
* 2) 从审批注册中心读取该单据已启用的审批环节及对应人员字段;
|
||||
* 3) 取/建该业务表的草稿审批流,返回流程记录(含id)供直接进入设计器。
|
||||
*
|
||||
* @param routePath 当前功能页前端路由(如 /xslmes/mesXslFormulaSpec/MesXslFormulaSpecList)
|
||||
@@ -236,8 +231,8 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
String bizTableName = resolveBizTableName(table);
|
||||
data.put("bizTableName", bizTableName);
|
||||
|
||||
// 3) 解析阶段字段
|
||||
data.put("stages", parseStageFields(table));
|
||||
// 3) 从审批注册中心解析启用环节
|
||||
data.put("stages", parseRegistryStages(table));
|
||||
|
||||
// 4) 取/建草稿审批流
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
@@ -265,6 +260,18 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按业务表查询审批注册中心启用环节-----
|
||||
/**
|
||||
* 供审批流列表「设计」入口调用:按业务表返回注册中心已启用环节(供左侧候选面板点选追加)。
|
||||
*/
|
||||
@Operation(summary = "审批流设计-注册中心启用环节")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/registryStages")
|
||||
public Result<List<Map<String, Object>>> registryStages(@RequestParam(name = "bizTable") String bizTable) {
|
||||
return Result.OK(parseRegistryStages(bizTable));
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按业务表查询审批注册中心启用环节-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供设计器节点「回调接口」下拉选择。
|
||||
@@ -355,8 +362,12 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务表中文名:优先字典 mes_xsl_approval_biz_doc,其次表注释 */
|
||||
/** 业务表中文名:优先审批注册中心,其次字典 mes_xsl_approval_biz_doc,再次表注释 */
|
||||
private String resolveBizTableName(String table) {
|
||||
MesXslBizDocRegistry registry = bizDocRegistryService.findActiveByTableName(table);
|
||||
if (registry != null && oConvertUtils.isNotEmpty(registry.getDisplayName())) {
|
||||
return registry.getDisplayName();
|
||||
}
|
||||
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
|
||||
if (items != null) {
|
||||
for (DictModel item : items) {
|
||||
@@ -378,69 +389,38 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
return null;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】从审批注册中心解析启用环节-----
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】移除 byField 引用,操作人由痕迹表承载-----------
|
||||
/**
|
||||
* 解析表字段,识别审批阶段字段。每个阶段最多取一个字段(优先列注释含"人/员"的人员字段)。
|
||||
* 返回有序列表:[{stageKey, stageName, nodeType, field, fieldComment}]
|
||||
* 从审批注册中心读取已启用环节,映射为流程设计器候选节点。
|
||||
* 返回有序列表:[{stageKey, stageName, nodeType}]
|
||||
*/
|
||||
private List<Map<String, Object>> parseStageFields(String table) {
|
||||
private List<Map<String, Object>> parseRegistryStages(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);
|
||||
MesXslBizDocRegistry registry = bizDocRegistryService.findActiveByTableName(table);
|
||||
if (registry == null || oConvertUtils.isEmpty(registry.getEnabledStages())) {
|
||||
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);
|
||||
java.util.Set<String> enabled = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
String[][] ordered = new String[][]{
|
||||
{ApprovalStageResolver.STAGE_PROOFREAD, "校对"},
|
||||
{ApprovalStageResolver.STAGE_AUDIT, "审核"},
|
||||
{ApprovalStageResolver.STAGE_APPROVE, "批准"},
|
||||
};
|
||||
for (String[] item : ordered) {
|
||||
if (!enabled.contains(item[0])) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> stage = new LinkedHashMap<>();
|
||||
stage.put("stageKey", item[0]);
|
||||
stage.put("stageName", item[1]);
|
||||
stage.put("nodeType", "approver");
|
||||
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;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】移除 byField 引用,操作人由痕迹表承载-----------
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】从审批注册中心解析启用环节-----
|
||||
|
||||
/** 按业务表+租户查找审批流(取最近一条) */
|
||||
private MesXslApprovalFlow findFlowByTable(String table, Integer tenantId) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批门禁 API
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Tag(name = "MES审批门禁")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalGate")
|
||||
@Slf4j
|
||||
public class MesXslApprovalGateController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
|
||||
@Operation(summary = "检查是否允许发起审批")
|
||||
@GetMapping("/canLaunch")
|
||||
public Result<ApprovalGateVo> canLaunch(
|
||||
@RequestParam("bizTable") String bizTable,
|
||||
@RequestParam("bizDataId") String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("bizTable 与 bizDataId 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
ApprovalGateVo vo = approvalGateService.checkCanLaunch(bizTable.trim(), bizDataId.trim(), tenantId);
|
||||
return Result.OK(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量检查是否允许发起审批")
|
||||
@PostMapping("/canLaunchBatch")
|
||||
public Result<List<ApprovalGateVo>> canLaunchBatch(@RequestBody CanLaunchBatchRequest req) {
|
||||
if (req == null || oConvertUtils.isEmpty(req.getBizTable()) || req.getBizDataIds() == null || req.getBizDataIds().isEmpty()) {
|
||||
return Result.error("bizTable 与 bizDataIds 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
List<ApprovalGateVo> list = approvalGateService.checkCanLaunchBatch(req.getBizTable().trim(), req.getBizDataIds(), tenantId);
|
||||
return Result.OK(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询业务单据审批台账历史")
|
||||
@GetMapping("/history")
|
||||
public Result<List<MesXslApprovalRecord>> history(
|
||||
@RequestParam("bizTable") String bizTable,
|
||||
@RequestParam("bizDataId") String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("bizTable 与 bizDataId 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
return Result.OK(approvalGateService.listHistory(bizTable.trim(), bizDataId.trim(), tenantId));
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CanLaunchBatchRequest {
|
||||
private String bizTable;
|
||||
private List<String> bizDataIds;
|
||||
}
|
||||
}
|
||||
@@ -108,4 +108,20 @@ public class MesXslApprovalHandleController {
|
||||
return Result.OK(approvalHandleService.pendingList(user));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】补发IM审批卡片(历史流转中实例)-----------
|
||||
@Operation(summary = "审批办理-补发IM审批卡片(流转中)")
|
||||
@PostMapping("/resendCard")
|
||||
public Result<String> resendCard(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = body.get("instanceId") == null ? null : String.valueOf(body.get("instanceId"));
|
||||
String bizTable = body.get("bizTable") == null ? null : String.valueOf(body.get("bizTable"));
|
||||
String bizDataId = body.get("bizDataId") == null ? null : String.valueOf(body.get("bizDataId"));
|
||||
if (oConvertUtils.isEmpty(instanceId)
|
||||
&& (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId))) {
|
||||
return Result.error("请提供 instanceId 或 bizTable+bizDataId");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.resendCard(instanceId, bizTable, bizDataId, user);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】补发IM审批卡片(历史流转中实例)-----------
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -12,14 +11,18 @@ 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.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService;
|
||||
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.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -52,22 +55,45 @@ public class MesXslApprovalLaunchController {
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】MES发起按钮与钉钉绑定同路由匹配-----------
|
||||
@Autowired
|
||||
private IMesXslDingTplBindService dingTplBindService;
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】MES发起按钮与钉钉绑定同路由匹配-----------
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(按租户隔离),即"可发起的单据类型"。
|
||||
* 同时按"功能模块(单据表)"自动反查其菜单路由填入 routePath,供前端控制悬浮按钮仅在该功能页显示,无需手工配置。
|
||||
* routePath 有值时:与钉钉审批按钮一致,先按 mes_xsl_ding_tpl_bind 解析当前页绑定,再返回该页业务表下已发布审批流。
|
||||
*/
|
||||
@Operation(summary = "发起审批-已发布审批流列表")
|
||||
@GetMapping("/publishedList")
|
||||
public Result<List<MesXslApprovalFlow>> publishedList() {
|
||||
public Result<List<MesXslApprovalFlow>> publishedList(
|
||||
@RequestParam(name = "routePath", required = false) String routePath) {
|
||||
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
|
||||
qw.eq("status", "1");
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
if (tenantId != null) {
|
||||
qw.eq("tenant_id", tenantId);
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】MES发起按钮与钉钉绑定同路由匹配-----------
|
||||
if (oConvertUtils.isNotEmpty(routePath)) {
|
||||
if (dingTplBindService.resolveActiveByRoutePath(routePath.trim()) == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
String bizTable = resolveTableByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(bizTable) || !IDENTIFIER.matcher(bizTable).matches()) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
qw.eq("biz_table", bizTable);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】MES发起按钮与钉钉绑定同路由匹配-----------
|
||||
qw.orderByDesc("create_time");
|
||||
List<MesXslApprovalFlow> list = approvalFlowService.list(qw);
|
||||
// 未手工指定 route_path 时,按单据表名自动反查菜单路由
|
||||
@@ -84,6 +110,59 @@ public class MesXslApprovalLaunchController {
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List(如 mes_xsl_formula_spec -> MesXslFormulaSpecList),
|
||||
* 对应 sys_permission.component 形如 xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,取其 url 即路由。
|
||||
*/
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】按前端路由反查业务表(与钉钉绑定路由解析一致)-----------
|
||||
private String resolveTableByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
String component = resolveComponentByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(component)) {
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】按前端路由反查业务表(与钉钉绑定路由解析一致)-----------
|
||||
|
||||
private String resolveRoutePathByTable(String table) {
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return null;
|
||||
@@ -166,22 +245,22 @@ public class MesXslApprovalLaunchController {
|
||||
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审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(flow.getTenantId());
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
try {
|
||||
approvalGateService.assertCanLaunch(flow.getBizTable(), bizDataId, tenantId);
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalGateService.createRunningRecord(
|
||||
approvalGateService.buildMesDraft(flow.getBizTable(), flow.getBizTableName(), bizDataId, bizTitle,
|
||||
flow.getId(), flow.getFlowName(), inst.getId(), loginUser, tenantId));
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
syncApprovalRecordAfterLaunch(inst.getId());
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
return Result.OK("发起成功!");
|
||||
}
|
||||
|
||||
@@ -218,23 +297,25 @@ public class MesXslApprovalLaunchController {
|
||||
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) {
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】批量发起统一门禁与台账写入-----
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(flow.getTenantId());
|
||||
ApprovalGateVo gate = approvalGateService.checkCanLaunch(flow.getBizTable(), bizDataId, tenantId);
|
||||
if (gate.getAllowed() == null || !gate.getAllowed()) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalGateService.createRunningRecord(
|
||||
approvalGateService.buildMesDraft(flow.getBizTable(), flow.getBizTableName(), bizDataId, bizTitle,
|
||||
flow.getId(), flow.getFlowName(), inst.getId(), loginUser, tenantId));
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
syncApprovalRecordAfterLaunch(inst.getId());
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】批量发起统一门禁与台账写入-----
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
return Result.error(skipCount > 0 ? "所选单据均已有审批中的流程,无需重复发起" : "没有有效的单据数据");
|
||||
return Result.error(skipCount > 0 ? "所选单据均不允许发起审批(可能已有流转中或已通过流程)" : "没有有效的单据数据");
|
||||
}
|
||||
String msg = "已发起 " + count + " 条审批!";
|
||||
if (skipCount > 0) {
|
||||
@@ -265,4 +346,14 @@ public class MesXslApprovalLaunchController {
|
||||
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
|
||||
return inst;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起后同步台账终态(如无节点自动通过)-----
|
||||
private void syncApprovalRecordAfterLaunch(String instanceId) {
|
||||
MesXslApprovalInstance latest = approvalInstanceService.getById(instanceId);
|
||||
if (latest == null || "0".equals(latest.getStatus())) {
|
||||
return;
|
||||
}
|
||||
approvalGateService.finishByMesInstance(instanceId, latest.getStatus(), latest.getCurrentHandlersText());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起后同步台账终态(如无节点自动通过)-----
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* MES 审批台账(只读查询)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】台账列表菜单
|
||||
*/
|
||||
@Tag(name = "MES审批台账")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslApprovalRecord")
|
||||
@Slf4j
|
||||
public class MesXslApprovalRecordController extends JeecgController<MesXslApprovalRecord, IMesXslApprovalRecordService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService mesXslApprovalRecordService;
|
||||
|
||||
@Operation(summary = "MES审批台账-分页列表查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslApprovalRecord>> queryPageList(
|
||||
MesXslApprovalRecord model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslApprovalRecord> queryWrapper = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
queryWrapper.orderByDesc("apply_time").orderByDesc("create_time");
|
||||
Page<MesXslApprovalRecord> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslApprovalRecord> pageList = mesXslApprovalRecordService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@Operation(summary = "MES审批台账-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslApprovalRecord> queryById(@RequestParam(name = "id") String id) {
|
||||
MesXslApprovalRecord entity = mesXslApprovalRecordService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslApprovalRecord model) {
|
||||
return super.exportXls(request, model, MesXslApprovalRecord.class, "MES审批台账");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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 审批台账(跨 MES/钉钉 统一门禁)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_record")
|
||||
@Schema(description = "MES审批台账")
|
||||
public class MesXslApprovalRecord extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据中文名")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "业务编码(菜单permission.id)")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "业务单据展示标题")
|
||||
private String bizTitle;
|
||||
|
||||
@Schema(description = "审批通道 MES/DINGTALK")
|
||||
@Dict(dicCode = "mes_xsl_approval_channel")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "外部实例ID(MES实例ID或钉钉instanceId)")
|
||||
private String externalInstanceId;
|
||||
|
||||
@Schema(description = "MES审批流ID")
|
||||
private String flowId;
|
||||
|
||||
@Schema(description = "MES审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "钉钉审批模板ID")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "钉钉审批模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "同一业务单据第几次发起")
|
||||
private Integer launchNo;
|
||||
|
||||
@Schema(description = "状态 0流转中 1通过 2拒绝 3撤销 4发起失败")
|
||||
@Dict(dicCode = "mes_xsl_approval_record_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 = "办结时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date finishTime;
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】与 MesXslApprovalInstance 对齐,复用 isBizAtOriginStatus 逻辑-----
|
||||
@Schema(description = "业务单据状态字段名(发起时快照,驳回回写依据)")
|
||||
private String statusField;
|
||||
|
||||
@Schema(description = "发起审批时业务状态原值(驳回回写依据)")
|
||||
private String originStatus;
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】与 MesXslApprovalInstance 对齐,复用 isBizAtOriginStatus 逻辑-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁字段,记录已处理的节点回调数-----
|
||||
@Schema(description = "钉钉回调已处理节点数(幂等去重,勿用于业务逻辑)")
|
||||
private Integer processedOpCount;
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁字段,记录已处理的节点回调数-----
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】台账增加编排执行状态字段-----
|
||||
@Schema(description = "编排执行状态 0未执行 1成功 2部分失败 3失败")
|
||||
@Dict(dicCode = "mes_xsl_integration_orch_status")
|
||||
private String integrationStatus;
|
||||
|
||||
@Schema(description = "编排摘要/错误信息")
|
||||
private String integrationRemark;
|
||||
//update-end---author:GHT ---date:2026-06-05 for:【审核集成Phase0】台账增加编排执行状态字段-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R4】新增nodeActivityMap存储processForecast节点顺序映射-----------
|
||||
@Schema(description = "钉钉节点活动映射(processForecast结果, JSON数组, 含completionAt幂等边界, 会签/依次审批多人等待判断依据)")
|
||||
private String nodeActivityMap;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R4】新增nodeActivityMap存储processForecast节点顺序映射-----------
|
||||
|
||||
@Schema(description = "逻辑删除 0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.advice;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslApprovalTrace;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslApprovalTraceService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批痕迹自动注入增强器
|
||||
*
|
||||
* <p>当审批注册中心配置了 listApiPath 后,拦截匹配 URL 的列表响应,
|
||||
* 自动 LEFT JOIN mes_xsl_approval_trace,将痕迹字段(traceProofreadBy 等)
|
||||
* 注入到每条记录中,无需修改业务代码。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-08 for:【XSLMES-20260608-TRACE】审批痕迹响应自动注入
|
||||
*/
|
||||
//update-begin---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】审批痕迹响应自动注入-----------
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public class ApprovalTraceResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalTraceService traceService;
|
||||
|
||||
/** 路径缓存条目 */
|
||||
private static class CacheEntry {
|
||||
final String tableName;
|
||||
/** enabledStages 集合,如 {"proofread","audit","approve"} */
|
||||
final java.util.Set<String> enabledStages;
|
||||
CacheEntry(String tableName, java.util.Set<String> enabledStages) {
|
||||
this.tableName = tableName;
|
||||
this.enabledStages = enabledStages;
|
||||
}
|
||||
}
|
||||
|
||||
/** path → CacheEntry 缓存(1 分钟 TTL)*/
|
||||
private volatile Map<String, CacheEntry> pathToEntryCache = Collections.emptyMap();
|
||||
private volatile long cacheLoadTime = 0L;
|
||||
private static final long CACHE_TTL_MS = 60_000L;
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return Result.class.isAssignableFrom(returnType.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
if (!(body instanceof Result)) {
|
||||
return body;
|
||||
}
|
||||
String path = extractServletPath(request);
|
||||
CacheEntry entry = resolveEntry(path);
|
||||
if (entry == null) {
|
||||
entry = resolveEntryByQueryById(path);
|
||||
}
|
||||
if (entry == null) {
|
||||
return body;
|
||||
}
|
||||
|
||||
Result result = (Result) body;
|
||||
Object data = result.getResult();
|
||||
|
||||
List<?> records = null;
|
||||
IPage page = null;
|
||||
boolean singleEntity = false;
|
||||
if (data instanceof IPage) {
|
||||
page = (IPage) data;
|
||||
records = page.getRecords();
|
||||
} else if (data instanceof List) {
|
||||
records = (List<?>) data;
|
||||
} else if (data != null && extractId(data) != null) {
|
||||
records = Collections.singletonList(data);
|
||||
singleEntity = true;
|
||||
}
|
||||
|
||||
if (records == null || records.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
|
||||
List<String> ids = extractIds(records);
|
||||
Map<String, MesXslApprovalTrace> traceMap = Collections.emptyMap();
|
||||
if (!ids.isEmpty()) {
|
||||
try {
|
||||
traceMap = traceService.batchQueryByBizIds(entry.tableName, ids);
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批痕迹注入] 批量查询失败 table={} path={}: {}", entry.tableName, path, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> enriched = enrichRecords(records, traceMap, entry.enabledStages);
|
||||
|
||||
if (page != null) {
|
||||
((Page) page).setRecords(enriched);
|
||||
} else if (singleEntity) {
|
||||
result.setResult(enriched.get(0));
|
||||
} else {
|
||||
result.setResult(enriched);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private String extractServletPath(ServerHttpRequest request) {
|
||||
if (request instanceof ServletServerHttpRequest) {
|
||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
String path = servletRequest.getServletPath();
|
||||
return oConvertUtils.isNotEmpty(path) ? path : request.getURI().getPath();
|
||||
}
|
||||
return request.getURI().getPath();
|
||||
}
|
||||
|
||||
private CacheEntry resolveEntry(String path) {
|
||||
if (oConvertUtils.isEmpty(path)) {
|
||||
return null;
|
||||
}
|
||||
ensureCacheLoaded();
|
||||
return pathToEntryCache.get(path);
|
||||
}
|
||||
|
||||
/** queryById 与 list 同模块时,按 list 路径匹配注册中心配置 */
|
||||
private CacheEntry resolveEntryByQueryById(String path) {
|
||||
if (oConvertUtils.isEmpty(path) || !path.endsWith("/queryById")) {
|
||||
return null;
|
||||
}
|
||||
ensureCacheLoaded();
|
||||
String listPath = path.substring(0, path.length() - "/queryById".length()) + "/list";
|
||||
return pathToEntryCache.get(listPath);
|
||||
}
|
||||
|
||||
private String extractId(Object r) {
|
||||
if (r == null) {
|
||||
return null;
|
||||
}
|
||||
Object id = null;
|
||||
if (r instanceof Map) {
|
||||
id = ((Map<?, ?>) r).get("id");
|
||||
} else {
|
||||
try {
|
||||
id = r.getClass().getMethod("getId").invoke(r);
|
||||
} catch (Exception ignored) {
|
||||
// 无 getId 方法
|
||||
}
|
||||
}
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
String idStr = String.valueOf(id);
|
||||
return oConvertUtils.isNotEmpty(idStr) ? idStr : null;
|
||||
}
|
||||
|
||||
private void ensureCacheLoaded() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - cacheLoadTime > CACHE_TTL_MS) {
|
||||
synchronized (this) {
|
||||
if (now - cacheLoadTime > CACHE_TTL_MS) {
|
||||
reloadCache();
|
||||
cacheLoadTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadCache() {
|
||||
try {
|
||||
List<MesXslBizDocRegistry> registries = registryService.lambdaQuery()
|
||||
.eq(MesXslBizDocRegistry::getEnabled, 1)
|
||||
.isNotNull(MesXslBizDocRegistry::getListApiPath)
|
||||
.list();
|
||||
Map<String, CacheEntry> map = new HashMap<>();
|
||||
for (MesXslBizDocRegistry reg : registries) {
|
||||
if (oConvertUtils.isEmpty(reg.getListApiPath()) || oConvertUtils.isEmpty(reg.getTableName())) {
|
||||
continue;
|
||||
}
|
||||
java.util.Set<String> stages = parseStages(reg.getEnabledStages());
|
||||
CacheEntry entry = new CacheEntry(reg.getTableName(), stages);
|
||||
for (String p : reg.getListApiPath().split(",")) {
|
||||
String trimmed = p.trim();
|
||||
if (oConvertUtils.isNotEmpty(trimmed)) {
|
||||
map.put(trimmed, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
pathToEntryCache = map;
|
||||
log.debug("[审批痕迹注入] 路径缓存已刷新,共 {} 条路径映射", map.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批痕迹注入] 路径缓存刷新失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.Set<String> parseStages(String enabledStages) {
|
||||
java.util.Set<String> set = new java.util.LinkedHashSet<>();
|
||||
if (oConvertUtils.isEmpty(enabledStages)) {
|
||||
return set;
|
||||
}
|
||||
for (String s : enabledStages.split(",")) {
|
||||
String t = s.trim();
|
||||
if (oConvertUtils.isNotEmpty(t)) {
|
||||
set.add(t);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private List<String> extractIds(List<?> records) {
|
||||
List<String> ids = new ArrayList<>(records.size());
|
||||
for (Object r : records) {
|
||||
if (r == null) {
|
||||
continue;
|
||||
}
|
||||
Object id = null;
|
||||
if (r instanceof Map) {
|
||||
id = ((Map<?, ?>) r).get("id");
|
||||
} else {
|
||||
try {
|
||||
id = r.getClass().getMethod("getId").invoke(r);
|
||||
} catch (Exception ignored) {
|
||||
// 无 getId 方法时跳过
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
String idStr = String.valueOf(id);
|
||||
if (oConvertUtils.isNotEmpty(idStr)) {
|
||||
ids.add(idStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> enrichRecords(List<?> records,
|
||||
Map<String, MesXslApprovalTrace> traceMap,
|
||||
java.util.Set<String> enabledStages) {
|
||||
List<Map<String, Object>> enriched = new ArrayList<>(records.size());
|
||||
for (Object r : records) {
|
||||
if (r == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> map;
|
||||
if (r instanceof Map) {
|
||||
map = new LinkedHashMap<>((Map<String, Object>) r);
|
||||
} else {
|
||||
// 实体类转 Map(保留序列化配置如 @JsonFormat)
|
||||
map = new LinkedHashMap<>(JSON.parseObject(JSON.toJSONString(r), Map.class));
|
||||
}
|
||||
Object idObj = map.get("id");
|
||||
MesXslApprovalTrace trace = (idObj != null) ? traceMap.get(String.valueOf(idObj)) : null;
|
||||
// 对每个启用的环节,始终注入字段(无痕迹时为 null),使前端能感知注册了哪些列
|
||||
if (enabledStages.contains("proofread")) {
|
||||
map.put("traceProofreadBy", trace != null ? trace.getProofreadBy() : null);
|
||||
map.put("traceProofreadTime", trace != null ? trace.getProofreadTime() : null);
|
||||
}
|
||||
if (enabledStages.contains("audit")) {
|
||||
map.put("traceAuditBy", trace != null ? trace.getAuditBy() : null);
|
||||
map.put("traceAuditTime", trace != null ? trace.getAuditTime() : null);
|
||||
}
|
||||
if (enabledStages.contains("approve")) {
|
||||
map.put("traceApproveBy", trace != null ? trace.getApproveBy() : null);
|
||||
map.put("traceApproveTime", trace != null ? trace.getApproveTime() : null);
|
||||
}
|
||||
enriched.add(map);
|
||||
}
|
||||
return enriched;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】审批痕迹响应自动注入-----------
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
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.Logical;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
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.integration.entity.MesXslApprovalTrace;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslApprovalTraceService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessForecastVO;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessInstanceFlowVO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批痕迹明细
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【XSLMES-20260605-K8R2】审批痕迹查询
|
||||
*/
|
||||
@Tag(name = "审批痕迹")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslApprovalTrace")
|
||||
@Slf4j
|
||||
public class MesXslApprovalTraceController extends JeecgController<MesXslApprovalTrace, IMesXslApprovalTraceService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalTraceService traceService;
|
||||
|
||||
@Operation(summary = "审批痕迹-分页列表")
|
||||
@RequiresPermissions(value = {"xslmes:mes_xsl_approval_trace:list", "xslmes:mes_xsl_biz_doc_registry:trace"}, logical = Logical.OR)
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslApprovalTrace>> queryPageList(
|
||||
MesXslApprovalTrace model,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslApprovalTrace> qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
qw.orderByDesc("update_time").orderByDesc("create_time");
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
return Result.OK(traceService.pageWithDingInstanceId(new Page<>(pageNo, pageSize), qw));
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
}
|
||||
|
||||
@Operation(summary = "审批痕迹-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_trace:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslApprovalTrace> queryById(@RequestParam String id) {
|
||||
MesXslApprovalTrace entity = traceService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供前端关联展示-----------
|
||||
@Operation(summary = "审批痕迹-批量查询(bizTable + 单据ID列表,供前端或内部关联展示)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_trace:list")
|
||||
@PostMapping("/batchByBizIds")
|
||||
public Result<Map<String, MesXslApprovalTrace>> batchByBizIds(
|
||||
@RequestParam String bizTable,
|
||||
@RequestBody List<String> bizDataIds) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || bizDataIds == null || bizDataIds.isEmpty()) {
|
||||
return Result.error("bizTable 与 bizDataIds 不能为空");
|
||||
}
|
||||
return Result.OK(traceService.batchQueryByBizIds(bizTable, bizDataIds));
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供前端关联展示-----------
|
||||
|
||||
@Operation(summary = "审批痕迹-按业务表与单据ID查询(供业务页关联展示)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_trace:list")
|
||||
@GetMapping("/queryByBiz")
|
||||
public Result<MesXslApprovalTrace> queryByBiz(
|
||||
@RequestParam String bizTable,
|
||||
@RequestParam String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("业务表与单据ID不能为空");
|
||||
}
|
||||
MesXslApprovalTrace entity = traceService.getByBiz(bizTable, bizDataId);
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
@Operation(summary = "审批痕迹-钉钉审批流转记录(时间轴)")
|
||||
@RequiresPermissions(value = {"xslmes:mes_xsl_approval_trace:list", "xslmes:mes_xsl_biz_doc_registry:trace"}, logical = Logical.OR)
|
||||
@GetMapping("/dingFlowRecords")
|
||||
public Result<DingProcessInstanceFlowVO> dingFlowRecords(
|
||||
@RequestParam(required = false) String bizTable,
|
||||
@RequestParam(required = false) String bizDataId,
|
||||
@RequestParam(required = false) String processInstanceId) {
|
||||
if (oConvertUtils.isEmpty(processInstanceId)
|
||||
&& (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId))) {
|
||||
return Result.error("单据ID与钉钉审批流ID不能同时为空");
|
||||
}
|
||||
try {
|
||||
return Result.OK(traceService.getDingFlowRecords(bizTable, bizDataId, processInstanceId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("拉取钉钉审批流转记录失败 bizTable={} bizDataId={} processInstanceId={}: {}",
|
||||
bizTable, bizDataId, processInstanceId, e.getMessage(), e);
|
||||
return Result.error("拉取钉钉审批流转记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
@Operation(summary = "审批痕迹-钉钉审批节点(实例tasks解析)")
|
||||
@RequiresPermissions(value = {"xslmes:mes_xsl_approval_trace:list", "xslmes:mes_xsl_biz_doc_registry:trace"}, logical = Logical.OR)
|
||||
@GetMapping("/dingProcessForecast")
|
||||
public Result<DingProcessForecastVO> dingProcessForecast(
|
||||
@RequestParam(required = false) String bizTable,
|
||||
@RequestParam(required = false) String bizDataId,
|
||||
@RequestParam(required = false) String processInstanceId) {
|
||||
if (oConvertUtils.isEmpty(processInstanceId)
|
||||
&& (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId))) {
|
||||
return Result.error("单据ID与钉钉审批流ID不能同时为空");
|
||||
}
|
||||
try {
|
||||
return Result.OK(traceService.getDingProcessForecast(bizTable, bizDataId, processInstanceId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("获取钉钉审批节点失败 bizTable={} bizDataId={} processInstanceId={}: {}",
|
||||
bizTable, bizDataId, processInstanceId, e.getMessage(), e);
|
||||
return Result.error("获取钉钉审批节点失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
@Operation(summary = "审批痕迹-钉钉审批实例原始JSON")
|
||||
@RequiresPermissions(value = {"xslmes:mes_xsl_approval_trace:list", "xslmes:mes_xsl_biz_doc_registry:trace"}, logical = Logical.OR)
|
||||
@GetMapping("/dingProcessInstance")
|
||||
public Result<JSONObject> dingProcessInstance(
|
||||
@RequestParam(required = false) String bizTable,
|
||||
@RequestParam(required = false) String bizDataId,
|
||||
@RequestParam(required = false) String processInstanceId) {
|
||||
if (oConvertUtils.isEmpty(processInstanceId)
|
||||
&& (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId))) {
|
||||
return Result.error("单据ID与钉钉审批流ID不能同时为空");
|
||||
}
|
||||
try {
|
||||
return Result.OK(traceService.getDingProcessInstance(bizTable, bizDataId, processInstanceId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("拉取钉钉审批实例原始JSON失败 bizTable={} bizDataId={} processInstanceId={}: {}",
|
||||
bizTable, bizDataId, processInstanceId, e.getMessage(), e);
|
||||
return Result.error("拉取钉钉审批实例失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.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.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批注册中心
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】单据注册
|
||||
*/
|
||||
@Tag(name = "审批注册中心")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslBizDocRegistry")
|
||||
@Slf4j
|
||||
public class MesXslBizDocRegistryController extends JeecgController<MesXslBizDocRegistry, IMesXslBizDocRegistryService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService service;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Operation(summary = "审批注册-分页列表")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslBizDocRegistry>> queryPageList(
|
||||
MesXslBizDocRegistry model,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslBizDocRegistry> qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
qw.orderByAsc("doc_code");
|
||||
return Result.OK(service.page(new Page<>(pageNo, pageSize), qw));
|
||||
}
|
||||
|
||||
@Operation(summary = "审批注册-新增")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:add")
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody MesXslBizDocRegistry entity) {
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
service.normalizeBeforeSave(entity);
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
service.save(entity);
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "审批注册-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:edit")
|
||||
@PutMapping("/edit")
|
||||
public Result<String> edit(@RequestBody MesXslBizDocRegistry entity) {
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
service.normalizeBeforeSave(entity);
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
service.updateById(entity);
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "审批注册-通过id删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:delete")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<String> delete(@RequestParam String id) {
|
||||
service.removeById(id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "审批注册-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslBizDocRegistry> queryById(@RequestParam String id) {
|
||||
MesXslBizDocRegistry entity = service.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【审批注册中心】物理表名下拉选择,查询当前库表清单-----------
|
||||
@Operation(summary = "查询当前数据库物理表(供注册中心下拉选择)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_biz_doc_registry:list")
|
||||
@GetMapping("/dbTables")
|
||||
public Result<List<Map<String, String>>> listDbTables(
|
||||
@RequestParam(name = "keyword", required = false) String keyword) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT TABLE_NAME tableName, IFNULL(TABLE_COMMENT,'') tableComment "
|
||||
+ "FROM information_schema.tables "
|
||||
+ "WHERE table_schema = (SELECT DATABASE()) AND table_type = 'BASE TABLE' ");
|
||||
List<Object> args = new ArrayList<>();
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
String like = "%" + keyword.trim() + "%";
|
||||
sql.append("AND (TABLE_NAME LIKE ? OR TABLE_COMMENT LIKE ?) ");
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
}
|
||||
sql.append("ORDER BY TABLE_NAME");
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
|
||||
List<Map<String, String>> options = new ArrayList<>(rows.size());
|
||||
for (Map<String, Object> row : rows) {
|
||||
String tableName = String.valueOf(row.get("tableName"));
|
||||
String comment = String.valueOf(row.get("tableComment"));
|
||||
Map<String, String> opt = new LinkedHashMap<>();
|
||||
opt.put("value", tableName);
|
||||
opt.put("comment", comment);
|
||||
opt.put("label", comment.isBlank() ? tableName : tableName + "(" + comment + ")");
|
||||
options.add(opt);
|
||||
}
|
||||
return Result.OK(options);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【审批注册中心】物理表名下拉选择,查询当前库表清单-----------
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.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.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationLog;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 审核集成执行日志
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成执行日志
|
||||
*/
|
||||
@Tag(name = "审核集成执行日志")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslIntegrationLog")
|
||||
@Slf4j
|
||||
public class MesXslIntegrationLogController extends JeecgController<MesXslIntegrationLog, IMesXslIntegrationLogService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslIntegrationLogService logService;
|
||||
|
||||
@Operation(summary = "集成日志-分页列表")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_log:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslIntegrationLog>> queryPageList(
|
||||
MesXslIntegrationLog model,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslIntegrationLog> qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
return Result.OK(logService.page(new Page<>(pageNo, pageSize), qw));
|
||||
}
|
||||
|
||||
@Operation(summary = "集成日志-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_log:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslIntegrationLog> queryById(@RequestParam String id) {
|
||||
MesXslIntegrationLog entity = logService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
@Operation(summary = "集成日志-手动重试失败动作")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_log:retry")
|
||||
@PostMapping("/retry")
|
||||
public Result<String> retry(@RequestParam String id) {
|
||||
return logService.retry(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.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.util.oConvertUtils;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationActionService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IntegrationPlanGenerator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审核集成方案(含内嵌动作管理)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成方案管理
|
||||
*/
|
||||
@Tag(name = "审核集成方案")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslIntegrationPlan")
|
||||
@Slf4j
|
||||
public class MesXslIntegrationPlanController extends JeecgController<MesXslIntegrationPlan, IMesXslIntegrationPlanService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslIntegrationPlanService planService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationActionService actionService;
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
//update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增表字段元数据查询接口(可视化配置向导用)-----------
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Autowired
|
||||
private IntegrationPlanGenerator planGenerator;
|
||||
//update-end---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增表字段元数据查询接口(可视化配置向导用)-----------
|
||||
|
||||
@Operation(summary = "集成方案-分页列表")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslIntegrationPlan>> queryPageList(
|
||||
MesXslIntegrationPlan model,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslIntegrationPlan> qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
return Result.OK(planService.page(new Page<>(pageNo, pageSize), qw));
|
||||
}
|
||||
|
||||
@Operation(summary = "集成方案-新增")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:add")
|
||||
@PostMapping("/add")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
//update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】改为返回实体(前端向导保存动作时需要 planId)-----------
|
||||
public Result<MesXslIntegrationPlan> add(@RequestBody MesXslIntegrationPlan entity) {
|
||||
Result<String> validate = planService.normalizeAndValidate(entity);
|
||||
if (!validate.isSuccess()) {
|
||||
return Result.error(validate.getMessage());
|
||||
}
|
||||
planService.save(entity);
|
||||
return Result.OK("添加成功", entity);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-05 for:【审核集成Phase0】改为返回实体(前端向导保存动作时需要 planId)-----------
|
||||
|
||||
@Operation(summary = "集成方案-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PutMapping("/edit")
|
||||
public Result<String> edit(@RequestBody MesXslIntegrationPlan entity) {
|
||||
Result<String> validate = planService.normalizeAndValidate(entity);
|
||||
if (!validate.isSuccess()) {
|
||||
return validate;
|
||||
}
|
||||
planService.updateById(entity);
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "集成方案-删除(同时删除动作)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:delete")
|
||||
@DeleteMapping("/delete")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<String> delete(@RequestParam String id) {
|
||||
actionService.removeByPlanId(id);
|
||||
planService.removeById(id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "集成方案-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/queryById")
|
||||
public Result<MesXslIntegrationPlan> queryById(@RequestParam String id) {
|
||||
MesXslIntegrationPlan entity = planService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
@Operation(summary = "集成方案-发布")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:publish")
|
||||
@PostMapping("/publish")
|
||||
public Result<String> publish(@RequestParam String id) {
|
||||
return planService.publish(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "集成方案-停用")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:publish")
|
||||
@PostMapping("/disable")
|
||||
public Result<String> disable(@RequestParam String id) {
|
||||
return planService.disable(id);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按表名查询审批注册中心配置-----------
|
||||
@Operation(summary = "按表名查询审批注册中心(集成方案绑定环节用)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/registryByTable")
|
||||
public Result<MesXslBizDocRegistry> registryByTable(@RequestParam String tableName) {
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(tableName);
|
||||
return registry != null ? Result.OK(registry) : Result.error("该表未在审批注册中心启用");
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按表名查询审批注册中心配置-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按审批流程节点生成默认集成方案-----------
|
||||
@Operation(summary = "预览按流程生成的默认方案")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/previewDefaultFromFlow")
|
||||
public Result<Map<String, Object>> previewDefaultFromFlow(
|
||||
@RequestParam String sourceTable,
|
||||
@RequestParam(required = false) String flowId) {
|
||||
return planGenerator.preview(sourceTable, flowId);
|
||||
}
|
||||
|
||||
@Operation(summary = "按审批流程节点生成默认方案与动作")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PostMapping("/generateDefaultFromFlow")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generateDefaultFromFlow(@RequestBody Map<String, Object> body) {
|
||||
String sourceTable = body == null ? null : String.valueOf(body.get("sourceTable"));
|
||||
String flowId = body != null && body.get("flowId") != null ? String.valueOf(body.get("flowId")) : null;
|
||||
boolean overwriteDraft = body != null && Boolean.TRUE.equals(body.get("overwriteDraft"));
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】生成时支持手选识别环节-----------
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> nodeBindings = body != null
|
||||
? (List<Map<String, Object>>) body.get("nodeBindings") : null;
|
||||
return planGenerator.generate(sourceTable, flowId, overwriteDraft,
|
||||
IntegrationPlanGenerator.parseStageOverrides(nodeBindings));
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】生成时支持手选识别环节-----------
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案-----------
|
||||
@Operation(summary = "为单个审批节点生成集成方案(流程设计器内使用)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PostMapping("/generateForNode")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generateForNode(@RequestBody Map<String, Object> body) {
|
||||
if (body == null) {
|
||||
return Result.error("请求体不能为空");
|
||||
}
|
||||
String sourceTable = body.get("sourceTable") != null ? String.valueOf(body.get("sourceTable")) : null;
|
||||
String flowId = body.get("flowId") != null ? String.valueOf(body.get("flowId")) : null;
|
||||
String nodeId = body.get("nodeId") != null ? String.valueOf(body.get("nodeId")) : null;
|
||||
String stageKey = body.get("stageKey") != null ? String.valueOf(body.get("stageKey")) : null;
|
||||
String flowConfig = body.get("flowConfig") != null ? String.valueOf(body.get("flowConfig")) : null;
|
||||
boolean overwriteDraft = Boolean.TRUE.equals(body.get("overwriteDraft"));
|
||||
return planGenerator.generateForNode(sourceTable, flowId, nodeId, stageKey, flowConfig, overwriteDraft);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案-----------
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按审批流程节点生成默认集成方案-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增表字段元数据查询接口(可视化配置向导用)-----------
|
||||
@Operation(summary = "查询表字段元数据(可视化配置向导)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/tableColumns")
|
||||
public Result<List<Map<String, Object>>> getTableColumns(@RequestParam String tableName) {
|
||||
if (!tableName.matches("^[a-z][a-z0-9_]{0,63}$")) {
|
||||
return Result.error("非法表名");
|
||||
}
|
||||
List<Map<String, Object>> cols = jdbcTemplate.queryForList(
|
||||
"SELECT COLUMN_NAME columnName, DATA_TYPE dataType, COLUMN_COMMENT `comment`, " +
|
||||
"COLUMN_KEY columnKey " +
|
||||
"FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? " +
|
||||
"ORDER BY ORDINAL_POSITION",
|
||||
tableName
|
||||
);
|
||||
return Result.OK(cols);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增表字段元数据查询接口(可视化配置向导用)-----------
|
||||
|
||||
// ============ 动作(内嵌在方案下)============
|
||||
|
||||
@Operation(summary = "动作-按方案查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:list")
|
||||
@GetMapping("/action/listByPlanId")
|
||||
public Result<List<MesXslIntegrationAction>> listActions(@RequestParam String planId) {
|
||||
return Result.OK(actionService.listByPlanId(planId));
|
||||
}
|
||||
|
||||
@Operation(summary = "动作-新增")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PostMapping("/action/add")
|
||||
public Result<String> addAction(@RequestBody MesXslIntegrationAction action) {
|
||||
normalizeRegistryAction(action);
|
||||
actionService.save(action);
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "动作-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@PutMapping("/action/edit")
|
||||
public Result<String> editAction(@RequestBody MesXslIntegrationAction action) {
|
||||
normalizeRegistryAction(action);
|
||||
actionService.updateById(action);
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审核集成】环节同步/回退动作保存时清理无效SQL模板-----------
|
||||
/** REGISTRY 类动作不走 SQL_UPDATE,保存时强制清空 sql_template 避免脏数据 */
|
||||
private void normalizeRegistryAction(MesXslIntegrationAction action) {
|
||||
if (action == null || oConvertUtils.isEmpty(action.getActionType())) {
|
||||
return;
|
||||
}
|
||||
if ("REGISTRY_STAGE_SYNC".equals(action.getActionType())
|
||||
|| "REGISTRY_STAGE_REVERT".equals(action.getActionType())) {
|
||||
action.setSqlTemplate(null);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审核集成】环节同步/回退动作保存时清理无效SQL模板-----------
|
||||
|
||||
@Operation(summary = "动作-删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_integration_plan:edit")
|
||||
@DeleteMapping("/action/delete")
|
||||
public Result<String> deleteAction(@RequestParam String id) {
|
||||
actionService.removeById(id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 从钉钉审批实例 tasks 按 activityId 解析环节完成情况,并与 MES 审批流节点 stageKey / multiMode 对齐。
|
||||
*/
|
||||
@Component
|
||||
public class ApprovalInstanceStageExtractor {
|
||||
|
||||
private static final Set<String> TRACE_STAGES = Set.of(
|
||||
ApprovalStageResolver.STAGE_PROOFREAD,
|
||||
ApprovalStageResolver.STAGE_AUDIT,
|
||||
ApprovalStageResolver.STAGE_APPROVE);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public ApprovalInstanceStageExtractor(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】按MES multiMode解析节点状态与审批人-----------
|
||||
/**
|
||||
* 按 MES 审批流节点顺序与实例 tasks 的 activityId 顺序对齐,解析各环节已完成操作人及最新时间。
|
||||
*/
|
||||
public List<StageCompletion> resolveCompletedStages(JSONObject instance, String flowConfig) {
|
||||
List<StageCompletion> completions = new ArrayList<>();
|
||||
if (instance == null || oConvertUtils.isEmpty(flowConfig)) {
|
||||
return completions;
|
||||
}
|
||||
List<NodePair> pairs = alignMesNodesWithTasks(instance, flowConfig);
|
||||
for (NodePair pair : pairs) {
|
||||
String stageKey = resolveStageKey(pair.getMesNode());
|
||||
if (!isTraceStage(stageKey)) {
|
||||
continue;
|
||||
}
|
||||
NodeTaskDecision decision = evaluateNodeTasks(pair.getTaskList(), resolveApprovalMethod(pair.getMesNode()));
|
||||
if (!decision.isAgreed()) {
|
||||
continue;
|
||||
}
|
||||
StageCompletion completion = toStageCompletion(stageKey, pair.getActivityId(), decision);
|
||||
if (completion != null) {
|
||||
completions.add(completion);
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
public LinkedHashMap<String, List<JSONObject>> groupTasksByActivityId(JSONObject instance) {
|
||||
LinkedHashMap<String, List<JSONObject>> grouped = new LinkedHashMap<>();
|
||||
JSONArray tasks = instance.getJSONArray("tasks");
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return grouped;
|
||||
}
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
JSONObject task = tasks.getJSONObject(i);
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
String activityId = task.getString("activityId");
|
||||
if (oConvertUtils.isEmpty(activityId)) {
|
||||
continue;
|
||||
}
|
||||
grouped.computeIfAbsent(activityId, k -> new ArrayList<>()).add(task);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
public List<String> listOrderedActivityIds(JSONObject instance) {
|
||||
return new ArrayList<>(groupTasksByActivityId(instance).keySet());
|
||||
}
|
||||
|
||||
public int resolveStepIndexFromTasks(JSONObject instance, String activityId) {
|
||||
if (instance == null || oConvertUtils.isEmpty(activityId)) {
|
||||
return -1;
|
||||
}
|
||||
return listOrderedActivityIds(instance).indexOf(activityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取某 activityId 节点完成时的审批人及时间(按 MES multiMode 判定)。
|
||||
*/
|
||||
public StageCompletion extractActivityCompletion(JSONObject instance, String activityId, JSONObject mesNode) {
|
||||
if (instance == null || oConvertUtils.isEmpty(activityId)) {
|
||||
return null;
|
||||
}
|
||||
List<JSONObject> taskList = groupTasksByActivityId(instance).get(activityId);
|
||||
String approvalMethod = mesNode == null ? "NONE" : resolveApprovalMethod(mesNode);
|
||||
NodeTaskDecision decision = evaluateNodeTasks(taskList, approvalMethod);
|
||||
if (!decision.isAgreed()) {
|
||||
return null;
|
||||
}
|
||||
return toStageCompletion(null, activityId, decision);
|
||||
}
|
||||
|
||||
public List<JSONObject> loadMesApproverNodes(String flowConfig) {
|
||||
List<JSONObject> result = new ArrayList<>();
|
||||
if (oConvertUtils.isEmpty(flowConfig)) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
collectAllApproverNodes(JSONObject.parseObject(flowConfig), result);
|
||||
} catch (Exception ignored) {
|
||||
// 解析失败返回空列表
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<NodePair> alignMesNodesWithTasks(JSONObject instance, String flowConfig) {
|
||||
List<NodePair> pairs = new ArrayList<>();
|
||||
LinkedHashMap<String, List<JSONObject>> grouped = groupTasksByActivityId(instance);
|
||||
if (grouped.isEmpty()) {
|
||||
return pairs;
|
||||
}
|
||||
List<JSONObject> mesNodes = loadMesApproverNodes(flowConfig);
|
||||
if (mesNodes.isEmpty()) {
|
||||
return pairs;
|
||||
}
|
||||
List<String> activityOrder = new ArrayList<>(grouped.keySet());
|
||||
int pairCount = Math.min(mesNodes.size(), activityOrder.size());
|
||||
for (int i = 0; i < pairCount; i++) {
|
||||
NodePair pair = new NodePair();
|
||||
pair.setStepNo(i + 1);
|
||||
pair.setMesNode(mesNodes.get(i));
|
||||
pair.setActivityId(activityOrder.get(i));
|
||||
pair.setTaskList(grouped.get(activityOrder.get(i)));
|
||||
pairs.add(pair);
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
public String resolveStageKey(JSONObject mesNode) {
|
||||
if (mesNode == null) {
|
||||
return null;
|
||||
}
|
||||
JSONObject props = mesNode.getJSONObject("props");
|
||||
if (props == null) {
|
||||
return null;
|
||||
}
|
||||
String stageKey = props.getString("stageKey");
|
||||
return oConvertUtils.isEmpty(stageKey) ? null : stageKey.trim();
|
||||
}
|
||||
|
||||
/** 从 MES 审批流节点 props.multiMode 映射钉钉审批方式 */
|
||||
public String resolveApprovalMethod(JSONObject mesNode) {
|
||||
if (mesNode == null) {
|
||||
return "NONE";
|
||||
}
|
||||
JSONObject props = mesNode.getJSONObject("props");
|
||||
if (props == null) {
|
||||
return "NONE";
|
||||
}
|
||||
String multiMode = props.getString("multiMode");
|
||||
if (oConvertUtils.isEmpty(multiMode) || "none".equalsIgnoreCase(multiMode)) {
|
||||
return "NONE";
|
||||
}
|
||||
if ("or".equalsIgnoreCase(multiMode)) {
|
||||
return "OR";
|
||||
}
|
||||
if ("and".equalsIgnoreCase(multiMode)) {
|
||||
return "AND";
|
||||
}
|
||||
if ("sequence".equalsIgnoreCase(multiMode)) {
|
||||
return "ONE_BY_ONE";
|
||||
}
|
||||
return "NONE";
|
||||
}
|
||||
|
||||
public String approvalMethodText(String approvalMethod) {
|
||||
if (oConvertUtils.isEmpty(approvalMethod)) {
|
||||
return "单人审批";
|
||||
}
|
||||
return switch (approvalMethod.toUpperCase()) {
|
||||
case "AND" -> "会签";
|
||||
case "OR" -> "或签";
|
||||
case "ONE_BY_ONE" -> "依次审批";
|
||||
case "NONE" -> "单人审批";
|
||||
default -> approvalMethod;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按审批方式解析节点状态与应展示的审批人。
|
||||
* 或签:任一通过/拒绝即定论,只取实际操作人;会签:全部通过才完成,取全部通过人。
|
||||
*/
|
||||
public NodeTaskDecision evaluateNodeTasks(List<JSONObject> taskList, String approvalMethod) {
|
||||
NodeTaskDecision decision = new NodeTaskDecision();
|
||||
decision.setNodeStatus("UNKNOWN");
|
||||
if (taskList == null || taskList.isEmpty()) {
|
||||
decision.setNodeStatus("NEW");
|
||||
decision.setNodeStatusText(nodeStatusText("NEW"));
|
||||
return decision;
|
||||
}
|
||||
String method = normalizeApprovalMethod(approvalMethod);
|
||||
|
||||
JSONObject refuseTask = findFirstActedTask(taskList, "REFUSE");
|
||||
if (refuseTask != null) {
|
||||
decision.setNodeStatus("REFUSED");
|
||||
decision.setNodeStatusText(nodeStatusText("REFUSED"));
|
||||
decision.setRefused(true);
|
||||
decision.setActorUserIds(List.of(refuseTask.getString("userId")));
|
||||
decision.setOperatorTime(parseFinishTime(refuseTask.getString("finishTime")));
|
||||
return decision;
|
||||
}
|
||||
|
||||
if ("OR".equals(method)) {
|
||||
JSONObject agreeTask = findFirstActedTask(taskList, "AGREE");
|
||||
if (agreeTask != null) {
|
||||
decision.setNodeStatus("COMPLETED");
|
||||
decision.setNodeStatusText(nodeStatusText("COMPLETED"));
|
||||
decision.setAgreed(true);
|
||||
decision.setActorUserIds(List.of(agreeTask.getString("userId")));
|
||||
decision.setOperatorTime(parseFinishTime(agreeTask.getString("finishTime")));
|
||||
return decision;
|
||||
}
|
||||
return decisionFromPendingTasks(taskList);
|
||||
}
|
||||
|
||||
if ("AND".equals(method) || "ONE_BY_ONE".equals(method)) {
|
||||
if (hasRunningOrNew(taskList)) {
|
||||
decision.setNodeStatus("RUNNING");
|
||||
decision.setNodeStatusText(nodeStatusText("RUNNING"));
|
||||
decision.setActorUserIds(listAllAssigneeIds(taskList));
|
||||
return decision;
|
||||
}
|
||||
List<JSONObject> agreeTasks = findAllActedTasks(taskList, "AGREE");
|
||||
int activeCount = countActiveTasks(taskList);
|
||||
if (activeCount > 0 && agreeTasks.size() >= activeCount) {
|
||||
decision.setNodeStatus("COMPLETED");
|
||||
decision.setNodeStatusText(nodeStatusText("COMPLETED"));
|
||||
decision.setAgreed(true);
|
||||
decision.setActorUserIds(extractOrderedUserIds(agreeTasks));
|
||||
decision.setOperatorTime(latestFinishTime(agreeTasks));
|
||||
return decision;
|
||||
}
|
||||
return decisionFromPendingTasks(taskList);
|
||||
}
|
||||
|
||||
// 单人审批
|
||||
JSONObject agreeTask = findFirstActedTask(taskList, "AGREE");
|
||||
if (agreeTask != null) {
|
||||
decision.setNodeStatus("COMPLETED");
|
||||
decision.setNodeStatusText(nodeStatusText("COMPLETED"));
|
||||
decision.setAgreed(true);
|
||||
decision.setActorUserIds(List.of(agreeTask.getString("userId")));
|
||||
decision.setOperatorTime(parseFinishTime(agreeTask.getString("finishTime")));
|
||||
return decision;
|
||||
}
|
||||
if (hasRunningOrNew(taskList)) {
|
||||
decision.setNodeStatus("RUNNING");
|
||||
decision.setNodeStatusText(nodeStatusText("RUNNING"));
|
||||
decision.setActorUserIds(listAllAssigneeIds(taskList));
|
||||
return decision;
|
||||
}
|
||||
return decisionFromPendingTasks(taskList);
|
||||
}
|
||||
|
||||
public boolean isNodeCompleted(List<JSONObject> taskList, String approvalMethod) {
|
||||
NodeTaskDecision decision = evaluateNodeTasks(taskList, approvalMethod);
|
||||
return decision.isAgreed();
|
||||
}
|
||||
|
||||
/** 审批实例是否已拒绝或终止(此时不应反写已通过环节的痕迹) */
|
||||
public boolean isInstanceRejectedOrCancelled(JSONObject instance) {
|
||||
if (instance == null) {
|
||||
return false;
|
||||
}
|
||||
String result = instance.getString("result");
|
||||
if (oConvertUtils.isNotEmpty(result) && "refuse".equalsIgnoreCase(result.trim())) {
|
||||
return true;
|
||||
}
|
||||
String status = instance.getString("status");
|
||||
if (oConvertUtils.isEmpty(status)) {
|
||||
return false;
|
||||
}
|
||||
String normalized = status.trim().toUpperCase();
|
||||
return "TERMINATED".equals(normalized) || "CANCELED".equals(normalized) || "CANCELLED".equals(normalized);
|
||||
}
|
||||
|
||||
public String nodeStatusText(String nodeStatus) {
|
||||
if (oConvertUtils.isEmpty(nodeStatus)) {
|
||||
return "未知";
|
||||
}
|
||||
return switch (nodeStatus.toUpperCase()) {
|
||||
case "COMPLETED" -> "已完成";
|
||||
case "RUNNING" -> "进行中";
|
||||
case "REFUSED" -> "已拒绝";
|
||||
case "CANCELED" -> "已取消";
|
||||
case "NEW" -> "未启动";
|
||||
default -> nodeStatus;
|
||||
};
|
||||
}
|
||||
|
||||
public List<String> resolveActorNames(List<String> dtUserIds) {
|
||||
if (dtUserIds == null || dtUserIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, String> nameMap = batchResolveDtUserDisplayNames(dtUserIds);
|
||||
return dtUserIds.stream().map(id -> nameMap.getOrDefault(id, id)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private NodeTaskDecision decisionFromPendingTasks(List<JSONObject> taskList) {
|
||||
NodeTaskDecision decision = new NodeTaskDecision();
|
||||
if (hasRunningOrNew(taskList)) {
|
||||
decision.setNodeStatus("RUNNING");
|
||||
decision.setNodeStatusText(nodeStatusText("RUNNING"));
|
||||
decision.setActorUserIds(listAllAssigneeIds(taskList));
|
||||
return decision;
|
||||
}
|
||||
if (allCanceled(taskList)) {
|
||||
decision.setNodeStatus("CANCELED");
|
||||
decision.setNodeStatusText(nodeStatusText("CANCELED"));
|
||||
decision.setActorUserIds(listAllAssigneeIds(taskList));
|
||||
return decision;
|
||||
}
|
||||
decision.setNodeStatus("NEW");
|
||||
decision.setNodeStatusText(nodeStatusText("NEW"));
|
||||
decision.setActorUserIds(listAllAssigneeIds(taskList));
|
||||
return decision;
|
||||
}
|
||||
|
||||
private StageCompletion toStageCompletion(String stageKey, String activityId, NodeTaskDecision decision) {
|
||||
if (decision == null || !decision.isAgreed() || decision.getActorUserIds() == null
|
||||
|| decision.getActorUserIds().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<String> names = resolveActorNames(decision.getActorUserIds());
|
||||
StageCompletion completion = new StageCompletion();
|
||||
completion.setStage(stageKey);
|
||||
completion.setActivityId(activityId);
|
||||
completion.setOperatorBy(String.join("、", names));
|
||||
completion.setOperatorTime(decision.getOperatorTime() == null ? new Date() : decision.getOperatorTime());
|
||||
completion.setDtUserIds(decision.getActorUserIds());
|
||||
return completion;
|
||||
}
|
||||
|
||||
private String normalizeApprovalMethod(String approvalMethod) {
|
||||
return oConvertUtils.isEmpty(approvalMethod) ? "NONE" : approvalMethod.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private JSONObject findFirstActedTask(List<JSONObject> taskList, String result) {
|
||||
return taskList.stream()
|
||||
.filter(task -> task != null && "COMPLETED".equalsIgnoreCase(task.getString("status")))
|
||||
.filter(task -> result.equalsIgnoreCase(task.getString("result")))
|
||||
.min(Comparator.comparing(task -> {
|
||||
Date time = parseFinishTime(task.getString("finishTime"));
|
||||
return time == null ? new Date(Long.MAX_VALUE) : time;
|
||||
}))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<JSONObject> findAllActedTasks(List<JSONObject> taskList, String result) {
|
||||
List<JSONObject> list = new ArrayList<>();
|
||||
for (JSONObject task : taskList) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
if ("COMPLETED".equalsIgnoreCase(task.getString("status"))
|
||||
&& result.equalsIgnoreCase(task.getString("result"))) {
|
||||
list.add(task);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<String> extractOrderedUserIds(List<JSONObject> tasks) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (JSONObject task : tasks) {
|
||||
String uid = task.getString("userId");
|
||||
if (oConvertUtils.isNotEmpty(uid) && !ids.contains(uid)) {
|
||||
ids.add(uid);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<String> listAllAssigneeIds(List<JSONObject> taskList) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (JSONObject task : taskList) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
String uid = task.getString("userId");
|
||||
if (oConvertUtils.isNotEmpty(uid) && !ids.contains(uid)) {
|
||||
ids.add(uid);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private boolean hasRunningOrNew(List<JSONObject> taskList) {
|
||||
for (JSONObject task : taskList) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
String status = task.getString("status");
|
||||
if ("RUNNING".equalsIgnoreCase(status) || "NEW".equalsIgnoreCase(status)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean allCanceled(List<JSONObject> taskList) {
|
||||
for (JSONObject task : taskList) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
if (!"CANCELED".equalsIgnoreCase(task.getString("status"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int countActiveTasks(List<JSONObject> taskList) {
|
||||
int count = 0;
|
||||
for (JSONObject task : taskList) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
if (!"CANCELED".equalsIgnoreCase(task.getString("status"))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private Date latestFinishTime(List<JSONObject> tasks) {
|
||||
Date latest = null;
|
||||
for (JSONObject task : tasks) {
|
||||
Date time = parseFinishTime(task.getString("finishTime"));
|
||||
if (time != null && (latest == null || time.after(latest))) {
|
||||
latest = time;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
private boolean isTraceStage(String stageKey) {
|
||||
return oConvertUtils.isNotEmpty(stageKey) && TRACE_STAGES.contains(stageKey);
|
||||
}
|
||||
|
||||
private void collectAllApproverNodes(JSONObject node, List<JSONObject> out) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
if ("approver".equals(node.getString("type"))) {
|
||||
out.add(node);
|
||||
}
|
||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
||||
if (branches != null) {
|
||||
for (int i = 0; i < branches.size(); i++) {
|
||||
Object branch = branches.get(i);
|
||||
if (branch instanceof JSONObject branchObj) {
|
||||
collectAllApproverNodes(branchObj.getJSONObject("childNode"), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectAllApproverNodes(node.getJSONObject("childNode"), out);
|
||||
}
|
||||
|
||||
private Date parseFinishTime(String finishTime) {
|
||||
if (oConvertUtils.isEmpty(finishTime)) {
|
||||
return null;
|
||||
}
|
||||
String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"};
|
||||
for (String pattern : patterns) {
|
||||
try {
|
||||
return new SimpleDateFormat(pattern).parse(finishTime.trim());
|
||||
} catch (ParseException ignored) {
|
||||
// 尝试下一种格式
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, String> batchResolveDtUserDisplayNames(Collection<String> dtUserIds) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (dtUserIds == null || dtUserIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<String> ids = dtUserIds.stream().filter(oConvertUtils::isNotEmpty).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
String inClause = ids.stream().map(id -> "?").collect(Collectors.joining(","));
|
||||
try {
|
||||
List<Map<String, Object>> localRows = jdbcTemplate.queryForList(
|
||||
"SELECT ding_user_id, realname, username FROM sys_user "
|
||||
+ "WHERE ding_user_id IN (" + inClause + ") AND (del_flag=0 OR del_flag IS NULL)",
|
||||
ids.toArray());
|
||||
for (Map<String, Object> row : localRows) {
|
||||
String dtId = stringValue(row.get("ding_user_id"));
|
||||
if (oConvertUtils.isEmpty(dtId)) {
|
||||
continue;
|
||||
}
|
||||
result.put(dtId, pickDisplayName(stringValue(row.get("realname")), stringValue(row.get("username")), dtId));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 查询失败时降级保留钉钉ID
|
||||
}
|
||||
List<String> missing = ids.stream().filter(id -> !result.containsKey(id)).collect(Collectors.toList());
|
||||
if (!missing.isEmpty()) {
|
||||
String missingIn = missing.stream().map(id -> "?").collect(Collectors.joining(","));
|
||||
try {
|
||||
List<Map<String, Object>> thirdRows = jdbcTemplate.queryForList(
|
||||
"SELECT t.third_user_id, u.realname, u.username "
|
||||
+ "FROM sys_third_account t "
|
||||
+ "JOIN sys_user u ON u.id = t.sys_user_id "
|
||||
+ "WHERE t.third_type='dingtalk' AND t.third_user_id IN (" + missingIn + ") "
|
||||
+ "AND (t.del_flag=0 OR t.del_flag IS NULL) AND (u.del_flag=0 OR u.del_flag IS NULL)",
|
||||
missing.toArray());
|
||||
for (Map<String, Object> row : thirdRows) {
|
||||
String dtId = stringValue(row.get("third_user_id"));
|
||||
if (oConvertUtils.isEmpty(dtId) || result.containsKey(dtId)) {
|
||||
continue;
|
||||
}
|
||||
result.put(dtId, pickDisplayName(stringValue(row.get("realname")), stringValue(row.get("username")), dtId));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 查询失败时降级保留钉钉ID
|
||||
}
|
||||
}
|
||||
for (String id : ids) {
|
||||
result.putIfAbsent(id, id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String pickDisplayName(String realname, String username, String fallback) {
|
||||
if (oConvertUtils.isNotEmpty(realname)) {
|
||||
return realname;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
return username;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】按MES multiMode解析节点状态与审批人-----------
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class StageCompletion {
|
||||
private String stage;
|
||||
private String activityId;
|
||||
private String operatorBy;
|
||||
private Date operatorTime;
|
||||
private List<String> dtUserIds;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class NodePair {
|
||||
private int stepNo;
|
||||
private JSONObject mesNode;
|
||||
private String activityId;
|
||||
private List<JSONObject> taskList;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class NodeTaskDecision {
|
||||
private String nodeStatus;
|
||||
private String nodeStatusText;
|
||||
private List<String> actorUserIds = new ArrayList<>();
|
||||
private Date operatorTime;
|
||||
private boolean agreed;
|
||||
private boolean refused;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 审批环节解析与匹配(对接审批注册中心 enabled_stages)
|
||||
*/
|
||||
public final class ApprovalStageResolver {
|
||||
|
||||
public static final String STAGE_PROOFREAD = "proofread";
|
||||
public static final String STAGE_AUDIT = "audit";
|
||||
public static final String STAGE_APPROVE = "approve";
|
||||
|
||||
private ApprovalStageResolver() {
|
||||
}
|
||||
|
||||
public static Set<String> parseEnabledStages(String enabledStages) {
|
||||
if (oConvertUtils.isEmpty(enabledStages)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> set = new HashSet<>();
|
||||
for (String part : enabledStages.split(",")) {
|
||||
if (oConvertUtils.isNotEmpty(part)) {
|
||||
set.add(part.trim());
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
public static boolean containsStage(String enabledStages, String stage) {
|
||||
return parseEnabledStages(enabledStages).contains(stage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据回调上下文与注册配置,解析当前刚完成的审批环节。
|
||||
* <p>
|
||||
* 优先级:
|
||||
* 1. ctx.stageKey 不为 null → 直接采用(来自流程节点 props.stageKey,最权威);
|
||||
* 空串表示「纯过路审批」,返回 null,由编排引擎守卫跳过集成。
|
||||
* 2. APPROVED 终态 → STAGE_APPROVE
|
||||
* 3. REJECTED → 从源单 status 字段推断
|
||||
* 4. 兜底:源单 status 字段 → 节点名称关键字
|
||||
*/
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】stageKey最高优先,区分关键节点与纯过路审批-----------
|
||||
public static String resolveCurrentStage(ApprovalCallbackContext ctx,
|
||||
MesXslBizDocRegistry registry,
|
||||
Map<String, Object> sourceRecord) {
|
||||
if (ctx == null) {
|
||||
return null;
|
||||
}
|
||||
// 1. 节点显式绑定了 stageKey(流程设计 props.stageKey)
|
||||
// null → 节点未设置,走降级启发式(向后兼容)
|
||||
// "" → 纯过路审批,返回 null 让编排引擎跳过集成
|
||||
// 其他值 → 直接作为环节 key 返回
|
||||
String nodeStageKey = ctx.getStageKey();
|
||||
if (nodeStageKey != null) {
|
||||
return nodeStageKey.isEmpty() ? null : nodeStageKey;
|
||||
}
|
||||
// 2. 终态:全程通过
|
||||
if (ctx.getAction() == ApprovalCallbackContext.Action.APPROVED) {
|
||||
return STAGE_APPROVE;
|
||||
}
|
||||
// 3. 驳回:从源单 status 反推被驳回的环节
|
||||
if (ctx.getAction() == ApprovalCallbackContext.Action.REJECTED) {
|
||||
return resolveStageFromStatus(registry, sourceRecord);
|
||||
}
|
||||
// 4. 节点通过的降级启发式(stageKey 未设置的旧数据兼容路径)
|
||||
String fromStatus = resolveStageFromStatus(registry, sourceRecord);
|
||||
if (oConvertUtils.isNotEmpty(fromStatus)) {
|
||||
return fromStatus;
|
||||
}
|
||||
return resolveStageFromNodeName(ctx.getNodeName());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】stageKey最高优先,区分关键节点与纯过路审批-----------
|
||||
|
||||
public static String resolveStageFromStatus(MesXslBizDocRegistry registry, Map<String, Object> sourceRecord) {
|
||||
if (registry == null || sourceRecord == null || sourceRecord.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String statusField = oConvertUtils.isEmpty(registry.getStatusField()) ? "status" : registry.getStatusField();
|
||||
Object statusVal = sourceRecord.get(statusField);
|
||||
if (statusVal == null) {
|
||||
return null;
|
||||
}
|
||||
String status = String.valueOf(statusVal).trim();
|
||||
if (Arrays.asList(STAGE_PROOFREAD, STAGE_AUDIT, STAGE_APPROVE).contains(status)) {
|
||||
return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String resolveStageFromNodeName(String nodeName) {
|
||||
if (oConvertUtils.isEmpty(nodeName)) {
|
||||
return null;
|
||||
}
|
||||
if (nodeName.contains("校对")) {
|
||||
return STAGE_PROOFREAD;
|
||||
}
|
||||
if (nodeName.contains("审核")) {
|
||||
return STAGE_AUDIT;
|
||||
}
|
||||
if (nodeName.contains("批准") || nodeName.contains("审批")) {
|
||||
return STAGE_APPROVE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断方案绑定的环节是否与当前回调匹配。
|
||||
*/
|
||||
public static boolean matchesTriggerStage(String planTriggerPhase,
|
||||
String planTriggerStage,
|
||||
TriggerPhase currentPhase,
|
||||
String currentStage) {
|
||||
if (currentPhase == TriggerPhase.ON_APPROVE) {
|
||||
if (oConvertUtils.isEmpty(planTriggerStage)) {
|
||||
return true;
|
||||
}
|
||||
return STAGE_APPROVE.equals(planTriggerStage);
|
||||
}
|
||||
if (currentPhase == TriggerPhase.ON_REJECT) {
|
||||
if (oConvertUtils.isEmpty(planTriggerStage)) {
|
||||
return true;
|
||||
}
|
||||
return planTriggerStage.equals(currentStage);
|
||||
}
|
||||
if (currentPhase == TriggerPhase.ON_NODE_APPROVE) {
|
||||
if (oConvertUtils.isEmpty(planTriggerStage)) {
|
||||
return true;
|
||||
}
|
||||
return planTriggerStage.equals(currentStage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String stageLabel(String stage) {
|
||||
if (STAGE_PROOFREAD.equals(stage)) {
|
||||
return "校对";
|
||||
}
|
||||
if (STAGE_AUDIT.equals(stage)) {
|
||||
return "审核";
|
||||
}
|
||||
if (STAGE_APPROVE.equals(stage)) {
|
||||
return "批准";
|
||||
}
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
|
||||
/**
|
||||
* 集成动作 actionConfig 解析辅助。
|
||||
* 兼容向导扁平格式(stage/expectedFrom 顶层)与可视化编辑器嵌套格式(registryStage 对象)。
|
||||
*/
|
||||
public final class IntegrationActionConfigHelper {
|
||||
|
||||
private IntegrationActionConfigHelper() {
|
||||
}
|
||||
|
||||
public static String resolveStage(MesXslIntegrationAction action, MesXslIntegrationPlan plan) {
|
||||
if (action != null && oConvertUtils.isNotEmpty(action.getActionConfig())) {
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
String stage = cfg.getString("stage");
|
||||
if (oConvertUtils.isNotEmpty(stage)) {
|
||||
return stage.trim();
|
||||
}
|
||||
JSONObject registryStage = cfg.getJSONObject("registryStage");
|
||||
if (registryStage != null && oConvertUtils.isNotEmpty(registryStage.getString("stage"))) {
|
||||
return registryStage.getString("stage").trim();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
if (plan != null && oConvertUtils.isNotEmpty(plan.getTriggerStage())) {
|
||||
return plan.getTriggerStage();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String resolveExpectedFrom(MesXslIntegrationAction action, String stage) {
|
||||
if (action != null && oConvertUtils.isNotEmpty(action.getActionConfig())) {
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
if (cfg.containsKey("expectedFrom")) {
|
||||
String v = cfg.getString("expectedFrom");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
JSONObject registryStage = cfg.getJSONObject("registryStage");
|
||||
if (registryStage != null && registryStage.containsKey("expectedFrom")) {
|
||||
String v = registryStage.getString("expectedFrom");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
return RegistryStageFieldHelper.defaultExpectedFrom(stage);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批环节同步】通过后状态与审批环节解耦,业务表状态由 statusAfter 控制-----------
|
||||
/**
|
||||
* 解析环节通过后业务表应写入的状态值。
|
||||
* 未配置时回退为审批环节码(兼容旧数据)。
|
||||
*/
|
||||
public static String resolveStatusAfter(MesXslIntegrationAction action, String stage) {
|
||||
if (action != null && oConvertUtils.isNotEmpty(action.getActionConfig())) {
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
if (cfg.containsKey("statusAfter")) {
|
||||
String v = cfg.getString("statusAfter");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
JSONObject registryStage = cfg.getJSONObject("registryStage");
|
||||
if (registryStage != null && registryStage.containsKey("statusAfter")) {
|
||||
String v = registryStage.getString("statusAfter");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
return oConvertUtils.isNotEmpty(stage) ? stage.trim() : null;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批环节同步】通过后状态与审批环节解耦,业务表状态由 statusAfter 控制-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【驳回回退】targetStage 按 containsKey 解析字典键值(含 0)-----------
|
||||
/**
|
||||
* 解析驳回回退目标:取动作配置中「回退目标」下拉所选的字典 item_value,原样写入业务表 status。
|
||||
*/
|
||||
public static String resolveTargetStage(MesXslIntegrationAction action) {
|
||||
if (action != null && oConvertUtils.isNotEmpty(action.getActionConfig())) {
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
if (cfg.containsKey("targetStage")) {
|
||||
String v = cfg.getString("targetStage");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
JSONObject registryStage = cfg.getJSONObject("registryStage");
|
||||
if (registryStage != null && registryStage.containsKey("targetStage")) {
|
||||
String v = registryStage.getString("targetStage");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fallback null
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【驳回回退】targetStage 按 containsKey 解析字典键值(含 0)-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】解析 SQL_UPDATE 动作是否同步目标表痕迹-----------
|
||||
/** 关联表动作是否开启痕迹同步(actionConfig.syncTrace) */
|
||||
public static boolean resolveSyncTrace(MesXslIntegrationAction action) {
|
||||
if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
return cfg.getBooleanValue("syncTrace");
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 可视化配置中的目标表名 */
|
||||
public static String resolveTargetTable(MesXslIntegrationAction action) {
|
||||
if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
String table = cfg.getString("targetTable");
|
||||
return oConvertUtils.isEmpty(table) ? null : table.trim();
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 关联条件:触发表字段 */
|
||||
public static String resolveLinkSourceField(MesXslIntegrationAction action) {
|
||||
return resolveLinkField(action, "sourceField");
|
||||
}
|
||||
|
||||
/** 关联条件:目标表字段 */
|
||||
public static String resolveLinkTargetField(MesXslIntegrationAction action) {
|
||||
return resolveLinkField(action, "targetField");
|
||||
}
|
||||
|
||||
/** 状态修改动作的新状态值(驳回回退时作为痕迹清空目标) */
|
||||
public static String resolveStatusConfigNewValue(MesXslIntegrationAction action) {
|
||||
if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
JSONObject statusConfig = cfg.getJSONObject("statusConfig");
|
||||
if (statusConfig == null) {
|
||||
return null;
|
||||
}
|
||||
String v = statusConfig.getString("newValue");
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveLinkField(MesXslIntegrationAction action, String fieldKey) {
|
||||
if (action == null || oConvertUtils.isEmpty(action.getActionConfig())) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSONObject cfg = JSONObject.parseObject(action.getActionConfig());
|
||||
JSONObject link = cfg.getJSONObject("linkCondition");
|
||||
if (link == null) {
|
||||
return null;
|
||||
}
|
||||
String v = link.getString(fieldKey);
|
||||
return oConvertUtils.isEmpty(v) ? null : v.trim();
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】解析 SQL_UPDATE 动作是否同步目标表痕迹-----------
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||
import org.jeecg.modules.xslmes.approval.callback.IApprovalBizCallback;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamSdkRunner;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 审核集成编排回调。
|
||||
* 注册为 {@link IApprovalBizCallback}(支持所有表 "*"),
|
||||
* 在现有 Callback 之后自动触发集成编排,无需改动 HandleService。
|
||||
* <p>
|
||||
* Order(100):确保在业务 Callback(默认 Order)之后执行,
|
||||
* 编排是「审批后」动作,不应干扰业务回调的结果。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】自动触发集成编排
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(100)
|
||||
public class IntegrationBizCallback implements IApprovalBizCallback {
|
||||
|
||||
private static final String DING_LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||
|
||||
@Autowired
|
||||
private IntegrationOrchestrator orchestrator;
|
||||
|
||||
@Override
|
||||
public String supportTable() {
|
||||
return "*";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproved(ApprovalCallbackContext ctx) {
|
||||
dispatchWithLog(ctx, TriggerPhase.ON_APPROVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejected(ApprovalCallbackContext ctx) {
|
||||
dispatchWithLog(ctx, TriggerPhase.ON_REJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeApproved(ApprovalCallbackContext ctx) {
|
||||
dispatchWithLog(ctx, TriggerPhase.ON_NODE_APPROVE);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调集成编排入口日志-----------
|
||||
private void dispatchWithLog(ApprovalCallbackContext ctx, TriggerPhase phase) {
|
||||
boolean dingTalk = isDingTalkCallback(ctx);
|
||||
if (dingTalk) {
|
||||
log.info("{} 进入集成编排 phase={} bizTable={} bizDataId={} nodeName={} comment={}",
|
||||
DING_LOG_TAG, phase.getValue(), ctx.getBizTable(), ctx.getBizDataId(),
|
||||
ctx.getNodeName(), ctx.getComment());
|
||||
}
|
||||
try {
|
||||
orchestrator.dispatch(ctx, phase);
|
||||
if (dingTalk) {
|
||||
log.info("{} 集成编排派发完成 phase={} bizTable={} bizDataId={}",
|
||||
DING_LOG_TAG, phase.getValue(), ctx.getBizTable(), ctx.getBizDataId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (dingTalk) {
|
||||
log.error("{} 集成编排派发异常 phase={} table={} bizId={}",
|
||||
DING_LOG_TAG, phase.getValue(), ctx.getBizTable(), ctx.getBizDataId(), e);
|
||||
} else {
|
||||
log.error("[集成引擎] {} 分发异常 table={} bizId={}", phase.getValue(),
|
||||
ctx.getBizTable(), ctx.getBizDataId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDingTalkCallback(ApprovalCallbackContext ctx) {
|
||||
return ctx != null && "dingtalk".equals(ctx.getOperatorUsername());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调集成编排入口日志-----------
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 集成编排执行上下文,在 ApprovalCallbackContext 基础上扩展
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class IntegrationContext {
|
||||
|
||||
/** 原审批回调上下文 */
|
||||
private ApprovalCallbackContext approvalCtx;
|
||||
|
||||
/** 关联审批台账(可能为 null,钉钉通道时已填) */
|
||||
private MesXslApprovalRecord record;
|
||||
|
||||
/** 源单表名 */
|
||||
private String sourceBizTable;
|
||||
|
||||
/** 源单 ID */
|
||||
private String sourceBizId;
|
||||
|
||||
/** 触发时机 */
|
||||
private TriggerPhase triggerPhase;
|
||||
|
||||
/** 当前执行的集成方案(供环节同步等动作读取 triggerStage) */
|
||||
private MesXslIntegrationPlan plan;
|
||||
|
||||
/** 从 DB 加载的源单主表字段(懒加载) */
|
||||
private Map<String, Object> sourceRecord = new HashMap<>();
|
||||
|
||||
/** 前序动作输出结果(actionId → 产出值) */
|
||||
private Map<String, String> actionResults = new HashMap<>();
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
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.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.executor.IIntegrationActionExecutor;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationLog;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationActionService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationLogService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamSdkRunner;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审核集成编排引擎。
|
||||
* <p>
|
||||
* 执行流程:
|
||||
* <ol>
|
||||
* <li>根据 (source_table, trigger_phase) 查找已发布方案</li>
|
||||
* <li>加载源单主表字段到 IntegrationContext</li>
|
||||
* <li>按 exec_order 依次执行动作,幂等检查、写日志</li>
|
||||
* <li>更新审批台账 integration_status</li>
|
||||
* </ol>
|
||||
* exec_mode=async:在审批事务提交后异步执行,审批不因编排失败回滚。
|
||||
* exec_mode=sync :与审批同事务,编排失败回滚审批(慎用)。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成编排引擎
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class IntegrationOrchestrator {
|
||||
|
||||
private static final String DING_LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||
|
||||
@Autowired
|
||||
private IMesXslIntegrationPlanService planService;
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationActionService actionService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationLogService logService;
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService recordService;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Autowired
|
||||
private List<IIntegrationActionExecutor> executors;
|
||||
@Autowired
|
||||
private IntegrationRevertTargetResolver revertTargetResolver;
|
||||
|
||||
// ==================== 外部入口 ====================
|
||||
|
||||
/**
|
||||
* 由 IntegrationBizCallback 在审批回调时调用。
|
||||
* 自动按 exec_mode 决定同步还是异步执行。
|
||||
*/
|
||||
public void dispatch(ApprovalCallbackContext approvalCtx, TriggerPhase phase) {
|
||||
String bizTable = approvalCtx.getBizTable();
|
||||
String bizDataId = approvalCtx.getBizDataId();
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<MesXslIntegrationPlan> plans = planService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getSourceTable, bizTable)
|
||||
.eq(MesXslIntegrationPlan::getTriggerPhase, phase.getValue())
|
||||
.eq(MesXslIntegrationPlan::getStatus, "1")
|
||||
.list();
|
||||
|
||||
if (plans.isEmpty()) {
|
||||
if (isDingTalkCallback(approvalCtx)) {
|
||||
log.info("{} 集成引擎无已发布方案 table={} phase={}", DING_LOG_TAG, bizTable, phase.getValue());
|
||||
}
|
||||
log.info("[集成引擎] 无已发布方案 table={} phase={}", bizTable, phase.getValue());
|
||||
return;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按审批注册中心绑定环节过滤集成方案-----------
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(bizTable);
|
||||
Map<String, Object> sourceRecord = loadSourceRecord(bizTable, bizDataId);
|
||||
String currentStage = ApprovalStageResolver.resolveCurrentStage(approvalCtx, registry, sourceRecord);
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】stageKey显式为空串(纯过路审批节点),直接跳过所有集成-----------
|
||||
// stageKey="" → 用户在流程设计中明确标记「纯过路审批」,任何集成方案都不应执行
|
||||
// stageKey=null → 节点未配置(老数据),走原有启发式匹配(向后兼容)
|
||||
String nodeStageKey = approvalCtx.getStageKey();
|
||||
if (phase == TriggerPhase.ON_NODE_APPROVE && nodeStageKey != null && nodeStageKey.isEmpty()) {
|
||||
if (isDingTalkCallback(approvalCtx)) {
|
||||
log.info("{} 集成引擎跳过:节点 stageKey 显式为空(纯过路审批) nodeName={} table={} bizId={}",
|
||||
DING_LOG_TAG, approvalCtx.getNodeName(), bizTable, bizDataId);
|
||||
} else {
|
||||
log.info("[集成引擎] 跳过:纯过路审批节点 nodeName={} table={}", approvalCtx.getNodeName(), bizTable);
|
||||
}
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R3】stageKey显式为空串(纯过路审批节点),直接跳过所有集成-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】onNodeApprove 增加按源单status+expectedFrom 兜底匹配-----------
|
||||
plans = plans.stream()
|
||||
.filter(plan -> matchesPlan(plan, phase, currentStage, sourceRecord, registry))
|
||||
.toList();
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】onNodeApprove 增加按源单status+expectedFrom 兜底匹配-----------
|
||||
if (plans.isEmpty()) {
|
||||
if (isDingTalkCallback(approvalCtx)) {
|
||||
log.info("{} 集成引擎无匹配方案 table={} phase={} stage={} nodeName={}",
|
||||
DING_LOG_TAG, bizTable, phase.getValue(), currentStage, approvalCtx.getNodeName());
|
||||
}
|
||||
log.info("[集成引擎] 无匹配绑定环节的方案 table={} phase={} stage={}", bizTable, phase.getValue(), currentStage);
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按审批注册中心绑定环节过滤集成方案-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调集成引擎方案匹配日志-----------
|
||||
if (isDingTalkCallback(approvalCtx)) {
|
||||
log.info("{} 集成引擎命中方案 phase={} stage={} nodeName={} plans=[{}]",
|
||||
DING_LOG_TAG, phase.getValue(), currentStage, approvalCtx.getNodeName(),
|
||||
plans.stream().map(MesXslIntegrationPlan::getPlanCode).collect(Collectors.joining(",")));
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】钉钉回调集成引擎方案匹配日志-----------
|
||||
|
||||
// 查关联台账(MES通道通过instanceId)
|
||||
MesXslApprovalRecord record = findRecord(approvalCtx);
|
||||
|
||||
for (MesXslIntegrationPlan plan : plans) {
|
||||
if ("sync".equals(plan.getExecMode())) {
|
||||
// 同步:当前事务内执行
|
||||
executePlan(plan, approvalCtx, record);
|
||||
} else {
|
||||
// 异步(默认):事务提交后执行,捕获所有变量避免闭包延迟问题
|
||||
final MesXslIntegrationPlan finalPlan = plan;
|
||||
final ApprovalCallbackContext finalCtx = approvalCtx;
|
||||
final MesXslApprovalRecord finalRecord = record;
|
||||
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
try {
|
||||
executePlanInNewTx(finalPlan, finalCtx, finalRecord);
|
||||
} catch (Exception e) {
|
||||
log.error("[集成引擎] 异步执行方案失败 plan={} bizTable={} bizId={}",
|
||||
finalPlan.getPlanCode(), bizTable, bizDataId, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 无活跃事务(如钉钉 Stream 回调),直接在新事务执行
|
||||
executePlanInNewTx(plan, approvalCtx, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 执行方案(同步,当前事务) ====================
|
||||
|
||||
private void executePlan(MesXslIntegrationPlan plan, ApprovalCallbackContext approvalCtx, MesXslApprovalRecord record) {
|
||||
IntegrationContext ctx = buildContext(plan, approvalCtx, record);
|
||||
doExecute(plan, ctx, record);
|
||||
}
|
||||
|
||||
// ==================== 执行方案(新事务,异步场景) ====================
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
||||
public void executePlanInNewTx(MesXslIntegrationPlan plan, ApprovalCallbackContext approvalCtx, MesXslApprovalRecord record) {
|
||||
IntegrationContext ctx = buildContext(plan, approvalCtx, record);
|
||||
doExecute(plan, ctx, record);
|
||||
}
|
||||
|
||||
// ==================== 核心执行逻辑 ====================
|
||||
|
||||
private void doExecute(MesXslIntegrationPlan plan, IntegrationContext ctx, MesXslApprovalRecord record) {
|
||||
List<MesXslIntegrationAction> actions = actionService.listByPlanId(plan.getId());
|
||||
if (actions.isEmpty()) {
|
||||
log.info("[集成引擎] 方案 {} 无启用动作,跳过", plan.getPlanCode());
|
||||
return;
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
StringBuilder remarkBuf = new StringBuilder();
|
||||
|
||||
for (MesXslIntegrationAction action : actions) {
|
||||
String idempotentKey = buildIdempotentKey(ctx, action);
|
||||
String snapshot = JSON.toJSONString(Map.of(
|
||||
"sourceId", ctx.getSourceBizId(),
|
||||
"sourceTable", ctx.getSourceBizTable(),
|
||||
"phase", ctx.getTriggerPhase().getValue()));
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】多轮审批幂等:按台账隔离+回退未达目标强制重跑-----------
|
||||
// 幂等检查(REGISTRY_STAGE_REVERT 若源单仍未回到目标状态,忽略历史 success 重新执行)
|
||||
if (logService.isAlreadySuccess(idempotentKey) && !shouldBypassIdempotentSkip(ctx, action)) {
|
||||
log.info("[集成引擎] 幂等命中,跳过 action={} key={}", action.getActionName(), idempotentKey);
|
||||
writeLog(ctx, action, idempotentKey, "skipped", null, null, snapshot, null, 0L);
|
||||
continue;
|
||||
}
|
||||
if (logService.isAlreadySuccess(idempotentKey)) {
|
||||
log.info("[集成引擎] 幂等命中但源单未达目标,重新执行 action={} key={}", action.getActionName(), idempotentKey);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】多轮审批幂等:按台账隔离+回退未达目标强制重跑-----------
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
IIntegrationActionExecutor executor = findExecutor(action.getActionType());
|
||||
String response = executor.execute(ctx, action);
|
||||
long ms = System.currentTimeMillis() - t0;
|
||||
writeLog(ctx, action, idempotentKey, "success", null, response, snapshot, null, ms);
|
||||
successCount++;
|
||||
//update-begin---author:GHT ---date:20260608 for:【审核集成】动作成功后刷新源单快照,供后续动作使用最新字段-----------
|
||||
refreshSourceRecord(ctx);
|
||||
//update-end---author:GHT ---date:20260608 for:【审核集成】动作成功后刷新源单快照,供后续动作使用最新字段-----------
|
||||
} catch (Exception e) {
|
||||
long ms = System.currentTimeMillis() - t0;
|
||||
String errMsg = e.getMessage();
|
||||
log.error("[集成引擎] 动作执行失败 action={} plan={}", action.getActionName(), plan.getPlanCode(), e);
|
||||
writeLog(ctx, action, idempotentKey, "failed", errMsg, null, snapshot, null, ms);
|
||||
failCount++;
|
||||
remarkBuf.append("[").append(action.getActionName()).append("]").append(errMsg).append("; ");
|
||||
if ("stop".equals(action.getOnFail())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新台账 integration_status
|
||||
if (record != null && oConvertUtils.isNotEmpty(record.getId())) {
|
||||
String orchStatus;
|
||||
if (failCount == 0) {
|
||||
orchStatus = "1"; // 全部成功
|
||||
} else if (successCount > 0) {
|
||||
orchStatus = "2"; // 部分失败
|
||||
} else {
|
||||
orchStatus = "3"; // 全部失败
|
||||
}
|
||||
recordService.lambdaUpdate()
|
||||
.eq(MesXslApprovalRecord::getId, record.getId())
|
||||
.set(MesXslApprovalRecord::getIntegrationStatus, orchStatus)
|
||||
.set(MesXslApprovalRecord::getIntegrationRemark,
|
||||
remarkBuf.length() > 0 ? remarkBuf.toString() : null)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private IntegrationContext buildContext(MesXslIntegrationPlan plan,
|
||||
ApprovalCallbackContext approvalCtx,
|
||||
MesXslApprovalRecord record) {
|
||||
String bizTable = approvalCtx.getBizTable();
|
||||
String bizDataId = approvalCtx.getBizDataId();
|
||||
TriggerPhase phase = switch (plan.getTriggerPhase()) {
|
||||
case "onReject" -> TriggerPhase.ON_REJECT;
|
||||
case "onNodeApprove" -> TriggerPhase.ON_NODE_APPROVE;
|
||||
default -> TriggerPhase.ON_APPROVE;
|
||||
};
|
||||
|
||||
IntegrationContext ctx = new IntegrationContext()
|
||||
.setApprovalCtx(approvalCtx)
|
||||
.setRecord(record)
|
||||
.setPlan(plan)
|
||||
.setSourceBizTable(bizTable)
|
||||
.setSourceBizId(bizDataId)
|
||||
.setTriggerPhase(phase);
|
||||
|
||||
Map<String, Object> sourceRecord = loadSourceRecord(bizTable, bizDataId);
|
||||
if (sourceRecord != null) {
|
||||
ctx.setSourceRecord(sourceRecord);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private Map<String, Object> loadSourceRecord(String bizTable, String bizDataId) {
|
||||
try {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM `" + bizTable + "` WHERE id = ?", bizDataId);
|
||||
if (!rows.isEmpty()) {
|
||||
return rows.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[集成引擎] 加载源单字段失败 table={} id={}: {}", bizTable, bizDataId, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审核集成】多动作串行执行时刷新源单上下文-----------
|
||||
private void refreshSourceRecord(IntegrationContext ctx) {
|
||||
Map<String, Object> sourceRecord = loadSourceRecord(ctx.getSourceBizTable(), ctx.getSourceBizId());
|
||||
if (sourceRecord != null) {
|
||||
ctx.setSourceRecord(sourceRecord);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审核集成】多动作串行执行时刷新源单上下文-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】修复台账查找:兼容钉钉recordId与MES外部实例ID-----------
|
||||
private MesXslApprovalRecord findRecord(ApprovalCallbackContext approvalCtx) {
|
||||
try {
|
||||
String instanceId = approvalCtx.getInstanceId();
|
||||
if (oConvertUtils.isNotEmpty(instanceId)) {
|
||||
// MES 通道:instanceId = 审批实例ID,对应台账 external_instance_id
|
||||
MesXslApprovalRecord byExternal = recordService.lambdaQuery()
|
||||
.eq(MesXslApprovalRecord::getExternalInstanceId, instanceId)
|
||||
.orderByDesc(MesXslApprovalRecord::getCreateTime)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
if (byExternal != null) {
|
||||
return byExternal;
|
||||
}
|
||||
// 钉钉 Stream:instanceId = 台账主键 record.id
|
||||
MesXslApprovalRecord byId = recordService.getById(instanceId);
|
||||
if (byId != null && recordMatchesBiz(byId, approvalCtx)) {
|
||||
return byId;
|
||||
}
|
||||
}
|
||||
return findLatestRecordByBiz(approvalCtx);
|
||||
} catch (Exception e) {
|
||||
log.warn("[集成引擎] 查找台账失败 instanceId={}: {}", approvalCtx.getInstanceId(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private MesXslApprovalRecord findLatestRecordByBiz(ApprovalCallbackContext approvalCtx) {
|
||||
if (oConvertUtils.isEmpty(approvalCtx.getBizTable()) || oConvertUtils.isEmpty(approvalCtx.getBizDataId())) {
|
||||
return null;
|
||||
}
|
||||
return recordService.lambdaQuery()
|
||||
.eq(MesXslApprovalRecord::getBizTable, approvalCtx.getBizTable())
|
||||
.eq(MesXslApprovalRecord::getBizDataId, approvalCtx.getBizDataId())
|
||||
.orderByDesc(MesXslApprovalRecord::getCreateTime)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
private boolean recordMatchesBiz(MesXslApprovalRecord record, ApprovalCallbackContext approvalCtx) {
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(approvalCtx.getBizTable())
|
||||
&& !approvalCtx.getBizTable().equalsIgnoreCase(record.getBizTable())) {
|
||||
return false;
|
||||
}
|
||||
return oConvertUtils.isEmpty(approvalCtx.getBizDataId())
|
||||
|| approvalCtx.getBizDataId().equals(record.getBizDataId());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】修复台账查找:兼容钉钉recordId与MES外部实例ID-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】幂等键优先按审批台账recordId隔离各轮审批-----------
|
||||
private String buildIdempotentKey(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
if (oConvertUtils.isNotEmpty(action.getIdempotentKey())) {
|
||||
return VariableResolver.resolve(action.getIdempotentKey(), ctx);
|
||||
}
|
||||
// 默认:本轮审批台账 recordId + actionId(同一单据多轮审批互不干扰)
|
||||
String prefix = resolveRecordIdForIdempotent(ctx);
|
||||
return prefix + "_" + action.getId();
|
||||
}
|
||||
|
||||
private String resolveRecordIdForIdempotent(IntegrationContext ctx) {
|
||||
if (ctx.getRecord() != null && oConvertUtils.isNotEmpty(ctx.getRecord().getId())) {
|
||||
return ctx.getRecord().getId();
|
||||
}
|
||||
ApprovalCallbackContext approvalCtx = ctx.getApprovalCtx();
|
||||
if (approvalCtx != null && "dingtalk".equals(approvalCtx.getOperatorUsername())
|
||||
&& oConvertUtils.isNotEmpty(approvalCtx.getInstanceId())) {
|
||||
return approvalCtx.getInstanceId();
|
||||
}
|
||||
return ctx.getSourceBizId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 回退类动作:历史 success 但源单 status 仍未到 targetStage 时,允许再次执行。
|
||||
*/
|
||||
private boolean shouldBypassIdempotentSkip(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
if (!"REGISTRY_STAGE_REVERT".equals(action.getActionType())) {
|
||||
return false;
|
||||
}
|
||||
String targetStage = resolveRevertTargetStage(action);
|
||||
String currentStatus = readSourceStatus(ctx);
|
||||
if (oConvertUtils.isEmpty(currentStatus)) {
|
||||
return true;
|
||||
}
|
||||
return !targetStage.equals(currentStatus);
|
||||
}
|
||||
|
||||
private String resolveRevertTargetStage(MesXslIntegrationAction action) {
|
||||
String target = IntegrationActionConfigHelper.resolveTargetStage(action);
|
||||
if (oConvertUtils.isNotEmpty(target)) {
|
||||
return target;
|
||||
}
|
||||
if (action != null && oConvertUtils.isNotEmpty(action.getPlanId())) {
|
||||
MesXslIntegrationPlan plan = planService.getById(action.getPlanId());
|
||||
if (plan != null && oConvertUtils.isNotEmpty(plan.getSourceTable())) {
|
||||
return revertTargetResolver.resolveRevertTarget(plan.getSourceTable());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String readSourceStatus(IntegrationContext ctx) {
|
||||
Map<String, Object> sourceRecord = ctx.getSourceRecord();
|
||||
if (sourceRecord == null || sourceRecord.isEmpty()) {
|
||||
sourceRecord = loadSourceRecord(ctx.getSourceBizTable(), ctx.getSourceBizId());
|
||||
if (sourceRecord != null) {
|
||||
ctx.setSourceRecord(sourceRecord);
|
||||
}
|
||||
}
|
||||
if (sourceRecord == null) {
|
||||
return null;
|
||||
}
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(ctx.getSourceBizTable());
|
||||
String statusField = RegistryStageFieldHelper.statusField(registry);
|
||||
Object val = sourceRecord.get(statusField);
|
||||
return val == null ? null : String.valueOf(val).trim();
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】幂等键优先按审批台账recordId隔离各轮审批-----------
|
||||
|
||||
private IIntegrationActionExecutor findExecutor(String actionType) {
|
||||
return executors.stream()
|
||||
.filter(e -> e.supportActionType().equals(actionType))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new UnsupportedOperationException("不支持的动作类型: " + actionType));
|
||||
}
|
||||
|
||||
private void writeLog(IntegrationContext ctx, MesXslIntegrationAction action,
|
||||
String idempotentKey, String status, String error, String response,
|
||||
String requestSnapshot, String responseSnapshot, long execTimeMs) {
|
||||
try {
|
||||
MesXslIntegrationLog logEntry = new MesXslIntegrationLog()
|
||||
.setRecordId(ctx.getRecord() != null ? ctx.getRecord().getId() : null)
|
||||
.setPlanId(action.getPlanId())
|
||||
.setActionId(action.getId())
|
||||
.setIdempotentKey(idempotentKey)
|
||||
.setStatus(status)
|
||||
.setSourceBizId(ctx.getSourceBizId())
|
||||
.setSourceBizTable(ctx.getSourceBizTable())
|
||||
.setErrorMessage(error)
|
||||
.setRetryCount(0)
|
||||
.setExecTimeMs(execTimeMs)
|
||||
.setRequestSnapshot(requestSnapshot)
|
||||
.setResponseSnapshot(response)
|
||||
.setCreateTime(new Date());
|
||||
logService.save(logEntry);
|
||||
} catch (Exception e) {
|
||||
log.error("[集成引擎] 写执行日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案环节匹配增强(节点名+源单status)-----------
|
||||
/**
|
||||
* 方案是否匹配当前回调:优先节点名/环节解析;onNodeApprove 时兜底用「源单当前 status == 动作 expectedFrom」。
|
||||
*/
|
||||
private boolean matchesPlan(MesXslIntegrationPlan plan, TriggerPhase phase, String resolvedStage,
|
||||
Map<String, Object> sourceRecord, MesXslBizDocRegistry registry) {
|
||||
if (ApprovalStageResolver.matchesTriggerStage(
|
||||
plan.getTriggerPhase(), plan.getTriggerStage(), phase, resolvedStage)) {
|
||||
return true;
|
||||
}
|
||||
if (phase == TriggerPhase.ON_NODE_APPROVE) {
|
||||
return matchPlanBySourceExpectedFrom(plan, sourceRecord, registry);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 源单当前 status 等于方案动作 expectedFrom 时,视为本节点应执行的环节方案 */
|
||||
private boolean matchPlanBySourceExpectedFrom(MesXslIntegrationPlan plan,
|
||||
Map<String, Object> sourceRecord,
|
||||
MesXslBizDocRegistry registry) {
|
||||
if (registry == null || oConvertUtils.isEmpty(plan.getTriggerStage())) {
|
||||
return false;
|
||||
}
|
||||
String statusField = RegistryStageFieldHelper.statusField(registry);
|
||||
Object statusVal = sourceRecord != null ? sourceRecord.get(statusField) : null;
|
||||
String currentStatus = statusVal == null ? "" : String.valueOf(statusVal).trim();
|
||||
|
||||
List<MesXslIntegrationAction> actions = actionService.listByPlanId(plan.getId());
|
||||
if (actions == null || actions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String expectedFrom = resolveExpectedFromFromPlan(actions, plan.getTriggerStage());
|
||||
if (oConvertUtils.isEmpty(expectedFrom)) {
|
||||
return false;
|
||||
}
|
||||
return expectedFrom.equals(currentStatus);
|
||||
}
|
||||
|
||||
private String resolveExpectedFromFromPlan(List<MesXslIntegrationAction> actions, String triggerStage) {
|
||||
if (actions != null) {
|
||||
for (MesXslIntegrationAction action : actions) {
|
||||
String expectedFrom = IntegrationActionConfigHelper.resolveExpectedFrom(action, triggerStage);
|
||||
if (oConvertUtils.isNotEmpty(expectedFrom)) {
|
||||
return expectedFrom;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RegistryStageFieldHelper.defaultExpectedFrom(triggerStage);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案环节匹配增强(节点名+源单status)-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别钉钉Stream来源回调-----------
|
||||
private boolean isDingTalkCallback(ApprovalCallbackContext ctx) {
|
||||
return ctx != null && "dingtalk".equals(ctx.getOperatorUsername());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别钉钉Stream来源回调-----------
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationActionService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 驳回回退目标解析:优先读取已发布 onReject 集成方案中的 REGISTRY_STAGE_REVERT 配置。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IntegrationRevertTargetResolver {
|
||||
|
||||
private static final Pattern DICT_IN_COMMENT = Pattern.compile("字典[:\\s]?([a-zA-Z][a-zA-Z0-9_]*)");
|
||||
|
||||
private static final Map<String, String> TABLE_STATUS_DICT_FALLBACK = Map.of(
|
||||
"mes_xsl_mixer_ps_compile", "xslmes_mixer_ps_status",
|
||||
"mes_xsl_formula_spec", "xslmes_formula_spec_status",
|
||||
"mes_xsl_raw_material_entry", "xslmes_entry_status"
|
||||
);
|
||||
|
||||
@Autowired
|
||||
private IMesXslIntegrationPlanService planService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationActionService actionService;
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【驳回回退】从已发布 onReject 集成方案解析回退目标-----------
|
||||
/**
|
||||
* 解析业务表驳回时应回退到的 status 值。
|
||||
* 优先级:已发布 onReject 方案 REGISTRY_STAGE_REVERT.targetStage → 注册中心状态字典初始态 → compile。
|
||||
*/
|
||||
public String resolveRevertTarget(String sourceTable) {
|
||||
if (oConvertUtils.isEmpty(sourceTable)) {
|
||||
return "compile";
|
||||
}
|
||||
String fromPlan = resolveFromPublishedRejectPlan(sourceTable);
|
||||
if (oConvertUtils.isNotEmpty(fromPlan)) {
|
||||
return fromPlan;
|
||||
}
|
||||
String fromRegistry = resolveInitialStatusFromRegistry(sourceTable);
|
||||
if (oConvertUtils.isNotEmpty(fromRegistry)) {
|
||||
log.info("[集成引擎] 表 {} 未配置 onReject 回退目标,使用注册中心初始态={}", sourceTable, fromRegistry);
|
||||
return fromRegistry;
|
||||
}
|
||||
log.warn("[集成引擎] 表 {} 未解析到回退目标,回退 compile", sourceTable);
|
||||
return "compile";
|
||||
}
|
||||
|
||||
private String resolveFromPublishedRejectPlan(String sourceTable) {
|
||||
List<MesXslIntegrationPlan> plans = planService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getSourceTable, sourceTable)
|
||||
.eq(MesXslIntegrationPlan::getTriggerPhase, "onReject")
|
||||
.eq(MesXslIntegrationPlan::getStatus, "1")
|
||||
.orderByAsc(MesXslIntegrationPlan::getCreateTime)
|
||||
.list();
|
||||
for (MesXslIntegrationPlan plan : plans) {
|
||||
List<MesXslIntegrationAction> actions = actionService.listByPlanId(plan.getId());
|
||||
for (MesXslIntegrationAction action : actions) {
|
||||
if (!"REGISTRY_STAGE_REVERT".equals(action.getActionType())) {
|
||||
continue;
|
||||
}
|
||||
String target = IntegrationActionConfigHelper.resolveTargetStage(action);
|
||||
if (oConvertUtils.isNotEmpty(target)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveInitialStatusFromRegistry(String sourceTable) {
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(sourceTable);
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
List<StatusDictItem> chain = loadStatusChain(registry);
|
||||
if (chain.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<String> enabledStages = orderedEnabledStages(registry.getEnabledStages());
|
||||
return resolveInitialStatus(chain, enabledStages);
|
||||
}
|
||||
|
||||
private List<String> orderedEnabledStages(String enabledStages) {
|
||||
Set<String> enabled = ApprovalStageResolver.parseEnabledStages(enabledStages);
|
||||
List<String> ordered = new ArrayList<>();
|
||||
for (String key : new String[]{
|
||||
ApprovalStageResolver.STAGE_PROOFREAD,
|
||||
ApprovalStageResolver.STAGE_AUDIT,
|
||||
ApprovalStageResolver.STAGE_APPROVE}) {
|
||||
if (enabled.contains(key)) {
|
||||
ordered.add(key);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private String resolveInitialStatus(List<StatusDictItem> chain, List<String> enabledStages) {
|
||||
Set<String> enabledSet = new LinkedHashSet<>(enabledStages);
|
||||
int firstStageIdx = -1;
|
||||
for (int i = 0; i < chain.size(); i++) {
|
||||
if (enabledSet.contains(chain.get(i).value)) {
|
||||
firstStageIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstStageIdx > 0) {
|
||||
return chain.get(firstStageIdx - 1).value;
|
||||
}
|
||||
for (StatusDictItem item : chain) {
|
||||
if (!enabledSet.contains(item.value)) {
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
return chain.get(0).value;
|
||||
}
|
||||
|
||||
private List<StatusDictItem> loadStatusChain(MesXslBizDocRegistry registry) {
|
||||
String dictCode = resolveStatusDictCode(registry);
|
||||
if (oConvertUtils.isEmpty(dictCode)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT item_value AS value, item_text AS label, sort_order AS sortOrder "
|
||||
+ "FROM sys_dict_item WHERE dict_id=(SELECT id FROM sys_dict WHERE dict_code=?) "
|
||||
+ "AND status=1 ORDER BY sort_order ASC, item_value ASC",
|
||||
dictCode);
|
||||
List<StatusDictItem> chain = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
chain.add(new StatusDictItem(String.valueOf(row.get("value")), String.valueOf(row.get("label"))));
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
private String resolveStatusDictCode(MesXslBizDocRegistry registry) {
|
||||
String statusField = oConvertUtils.isEmpty(registry.getStatusField()) ? "status" : registry.getStatusField();
|
||||
String table = registry.getTableName();
|
||||
if (!table.matches("^[a-z][a-z0-9_]{0,63}$")) {
|
||||
return TABLE_STATUS_DICT_FALLBACK.getOrDefault(table, null);
|
||||
}
|
||||
try {
|
||||
List<String> comments = jdbcTemplate.queryForList(
|
||||
"SELECT COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS "
|
||||
+ "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=?",
|
||||
String.class, table, statusField);
|
||||
if (!comments.isEmpty()) {
|
||||
Matcher m = DICT_IN_COMMENT.matcher(comments.get(0));
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[集成引擎] 读取状态字典注释失败 table={} field={}", table, statusField, e);
|
||||
}
|
||||
return TABLE_STATUS_DICT_FALLBACK.getOrDefault(table, null);
|
||||
}
|
||||
|
||||
private record StatusDictItem(String value, String label) {
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【驳回回退】从已发布 onReject 集成方案解析回退目标-----------
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
|
||||
/**
|
||||
* 审批注册中心环节与业务表字段映射辅助
|
||||
*/
|
||||
public final class RegistryStageFieldHelper {
|
||||
|
||||
private RegistryStageFieldHelper() {
|
||||
}
|
||||
|
||||
public static String statusField(MesXslBizDocRegistry registry) {
|
||||
return oConvertUtils.isEmpty(registry.getStatusField()) ? "status" : registry.getStatusField();
|
||||
}
|
||||
|
||||
/** 环节默认前置状态:proofread←compile, audit←proofread, approve←audit */
|
||||
public static String defaultExpectedFrom(String stage) {
|
||||
switch (stage) {
|
||||
case ApprovalStageResolver.STAGE_PROOFREAD:
|
||||
return "compile";
|
||||
case ApprovalStageResolver.STAGE_AUDIT:
|
||||
return ApprovalStageResolver.STAGE_PROOFREAD;
|
||||
case ApprovalStageResolver.STAGE_APPROVE:
|
||||
return ApprovalStageResolver.STAGE_AUDIT;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertIdentifier(String name) {
|
||||
if (oConvertUtils.isEmpty(name) || !name.matches("^[a-z][a-z0-9_]{0,63}$")) {
|
||||
throw new IllegalArgumentException("非法字段名: " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 关联表 SQL_UPDATE 动作执行后的审批痕迹同步/清空。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RelatedTableTraceSyncHelper {
|
||||
|
||||
@Autowired
|
||||
private IApprovalTraceSyncService approvalTraceSyncService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL_UPDATE 成功后按动作配置同步目标表痕迹-----------
|
||||
/**
|
||||
* SQL_UPDATE 成功后,按动作配置将主表审批人/时间写入或清空关联表痕迹。
|
||||
*
|
||||
* @param affectedRows SQL 实际影响行数,零行时跳过
|
||||
*/
|
||||
public void syncAfterSqlUpdate(IntegrationContext ctx, MesXslIntegrationAction action, int affectedRows) {
|
||||
if (ctx == null || action == null || !IntegrationActionConfigHelper.resolveSyncTrace(action)) {
|
||||
return;
|
||||
}
|
||||
if (affectedRows <= 0) {
|
||||
log.info("[关联表痕迹] 跳过:SQL 零行更新 action={}", action.getActionName());
|
||||
return;
|
||||
}
|
||||
String targetTable = IntegrationActionConfigHelper.resolveTargetTable(action);
|
||||
String sourceField = IntegrationActionConfigHelper.resolveLinkSourceField(action);
|
||||
String targetField = IntegrationActionConfigHelper.resolveLinkTargetField(action);
|
||||
if (oConvertUtils.isEmpty(targetTable) || oConvertUtils.isEmpty(sourceField) || oConvertUtils.isEmpty(targetField)) {
|
||||
log.warn("[关联表痕迹] 跳过:未配置目标表或关联条件 action={}", action.getActionName());
|
||||
return;
|
||||
}
|
||||
RegistryStageFieldHelper.assertIdentifier(targetTable);
|
||||
RegistryStageFieldHelper.assertIdentifier(sourceField);
|
||||
RegistryStageFieldHelper.assertIdentifier(targetField);
|
||||
|
||||
String linkValue = resolveLinkValue(ctx, sourceField);
|
||||
if (oConvertUtils.isEmpty(linkValue)) {
|
||||
log.warn("[关联表痕迹] 跳过:触发表关联字段为空 action={} sourceField={}", action.getActionName(), sourceField);
|
||||
return;
|
||||
}
|
||||
List<String> targetIds = listTargetBizIds(targetTable, targetField, linkValue);
|
||||
if (targetIds.isEmpty()) {
|
||||
log.warn("[关联表痕迹] 跳过:未匹配到目标表记录 action={} table={} {}={}",
|
||||
action.getActionName(), targetTable, targetField, linkValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRejectLikePhase(ctx)) {
|
||||
syncRevertTrace(ctx, action, targetTable, targetIds);
|
||||
} else {
|
||||
syncPassTrace(ctx, action, targetTable, targetIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncPassTrace(IntegrationContext ctx, MesXslIntegrationAction action,
|
||||
String targetTable, List<String> targetIds) {
|
||||
String stage = resolveTraceStage(ctx, action);
|
||||
if (oConvertUtils.isEmpty(stage)) {
|
||||
log.warn("[关联表痕迹] 跳过:无法解析审批环节 action={}", action.getActionName());
|
||||
return;
|
||||
}
|
||||
String stageErr = approvalTraceSyncService.checkStageAllowed(targetTable, stage);
|
||||
if (stageErr != null) {
|
||||
log.warn("[关联表痕迹] 跳过:{} action={}", stageErr, action.getActionName());
|
||||
return;
|
||||
}
|
||||
String operator = resolveOperator(ctx);
|
||||
Date operatorTime = resolveOperatorTime(ctx);
|
||||
for (String targetId : targetIds) {
|
||||
approvalTraceSyncService.syncStage(targetTable, targetId, stage, operator, operatorTime);
|
||||
}
|
||||
log.info("[关联表痕迹] 写入完成 action={} table={} stage={} operator={} count={}",
|
||||
action.getActionName(), targetTable, stage, operator, targetIds.size());
|
||||
}
|
||||
|
||||
private void syncRevertTrace(IntegrationContext ctx, MesXslIntegrationAction action,
|
||||
String targetTable, List<String> targetIds) {
|
||||
// 驳回场景取状态修改动作的「新状态」,与 SQL SET 值一致,用于痕迹回退粒度对齐
|
||||
String revertTarget = IntegrationActionConfigHelper.resolveStatusConfigNewValue(action);
|
||||
if (oConvertUtils.isEmpty(revertTarget)) {
|
||||
log.warn("[关联表痕迹] 驳回清空跳过:状态修改未配置「新状态」action={}", action.getActionName());
|
||||
return;
|
||||
}
|
||||
for (String targetId : targetIds) {
|
||||
approvalTraceSyncService.revertToStage(targetTable, targetId, revertTarget);
|
||||
}
|
||||
log.info("[关联表痕迹] 驳回清空完成 action={} table={} targetStage={} count={}",
|
||||
action.getActionName(), targetTable, revertTarget, targetIds.size());
|
||||
}
|
||||
|
||||
private boolean isRejectLikePhase(IntegrationContext ctx) {
|
||||
if (ctx.getTriggerPhase() == TriggerPhase.ON_REJECT) {
|
||||
return true;
|
||||
}
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac == null || ac.getAction() == null) {
|
||||
return false;
|
||||
}
|
||||
return ac.getAction() == ApprovalCallbackContext.Action.REJECTED
|
||||
|| ac.getAction() == ApprovalCallbackContext.Action.CANCELLED;
|
||||
}
|
||||
|
||||
private String resolveTraceStage(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac != null && oConvertUtils.isNotEmpty(ac.getStageKey())) {
|
||||
return ac.getStageKey().trim();
|
||||
}
|
||||
MesXslIntegrationPlan plan = ctx.getPlan();
|
||||
if (plan != null && oConvertUtils.isNotEmpty(plan.getTriggerStage())) {
|
||||
return plan.getTriggerStage().trim();
|
||||
}
|
||||
return IntegrationActionConfigHelper.resolveStage(action, plan);
|
||||
}
|
||||
|
||||
private String resolveOperator(IntegrationContext ctx) {
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorName())) {
|
||||
return ac.getOperatorName();
|
||||
}
|
||||
if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorUsername())) {
|
||||
return ac.getOperatorUsername();
|
||||
}
|
||||
return "系统";
|
||||
}
|
||||
|
||||
private Date resolveOperatorTime(IntegrationContext ctx) {
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac != null && ac.getOperatorTime() != null) {
|
||||
return ac.getOperatorTime();
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
|
||||
private String resolveLinkValue(IntegrationContext ctx, String sourceField) {
|
||||
if ("id".equalsIgnoreCase(sourceField)) {
|
||||
return ctx.getSourceBizId();
|
||||
}
|
||||
Map<String, Object> rec = ctx.getSourceRecord();
|
||||
if (rec == null || !rec.containsKey(sourceField)) {
|
||||
return null;
|
||||
}
|
||||
Object v = rec.get(sourceField);
|
||||
return v == null ? null : String.valueOf(v).trim();
|
||||
}
|
||||
|
||||
private List<String> listTargetBizIds(String targetTable, String targetField, String linkValue) {
|
||||
String sql = "SELECT id FROM `" + targetTable + "` WHERE `" + targetField + "` = ?";
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, linkValue);
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
Object id = row.get("id");
|
||||
if (id != null && oConvertUtils.isNotEmpty(String.valueOf(id))) {
|
||||
ids.add(String.valueOf(id));
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL_UPDATE 成功后按动作配置同步目标表痕迹-----------
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
/**
|
||||
* 集成方案触发时机,与 ApprovalCallbackContext.Action 对应
|
||||
*/
|
||||
public enum TriggerPhase {
|
||||
ON_APPROVE("onApprove"),
|
||||
ON_REJECT("onReject"),
|
||||
ON_NODE_APPROVE("onNodeApprove");
|
||||
|
||||
private final String value;
|
||||
|
||||
TriggerPhase(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine;
|
||||
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 模板变量解析器。
|
||||
* 将 #{变量名} 替换为实际值,用于 SQL 模板、幂等键等场景。
|
||||
* <p>
|
||||
* 支持的变量:
|
||||
* <ul>
|
||||
* <li>#{source.id} / #{id} — 源单 ID</li>
|
||||
* <li>#{source.字段名} — 源单主表字段</li>
|
||||
* <li>#{sys_user_code} — 当前操作人 username</li>
|
||||
* <li>#{sys_date} — 当前日期 yyyy-MM-dd</li>
|
||||
* <li>#{sys_datetime} — 当前时间 yyyy-MM-dd HH:mm:ss</li>
|
||||
* <li>#{approval.instance_id} — 审批实例 ID</li>
|
||||
* <li>#{approval.apply_user} — 审批发起人</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class VariableResolver {
|
||||
|
||||
private static final Pattern PLACEHOLDER = Pattern.compile("#\\{([^}]+)}");
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private VariableResolver() {}
|
||||
|
||||
/**
|
||||
* 解析模板,返回替换后的字符串(不做 SQL 转义,纯文本场景用)。
|
||||
*/
|
||||
public static String resolve(String template, IntegrationContext ctx) {
|
||||
if (oConvertUtils.isEmpty(template)) {
|
||||
return template;
|
||||
}
|
||||
Matcher m = PLACEHOLDER.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String key = m.group(1).trim();
|
||||
String val = resolveKey(key, ctx);
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(val));
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 SQL 模板:字符串值自动加单引号并转义内部单引号。
|
||||
* 数值/null 不加引号。
|
||||
*/
|
||||
public static String resolveSql(String template, IntegrationContext ctx) {
|
||||
if (oConvertUtils.isEmpty(template)) {
|
||||
return template;
|
||||
}
|
||||
Matcher m = PLACEHOLDER.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String key = m.group(1).trim();
|
||||
String rawVal = resolveKey(key, ctx);
|
||||
String sqlVal = toSqlLiteral(rawVal);
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(sqlVal));
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String resolveKey(String key, IntegrationContext ctx) {
|
||||
// 系统变量
|
||||
switch (key) {
|
||||
case "sys_date":
|
||||
return LocalDate.now().format(DATE_FMT);
|
||||
case "sys_datetime":
|
||||
return LocalDateTime.now().format(DATETIME_FMT);
|
||||
case "sys_user_code":
|
||||
return safeStr(ctx.getApprovalCtx() != null ? ctx.getApprovalCtx().getOperatorUsername() : null);
|
||||
case "id":
|
||||
case "source.id":
|
||||
return safeStr(ctx.getSourceBizId());
|
||||
case "approval.instance_id":
|
||||
return safeStr(ctx.getApprovalCtx() != null ? ctx.getApprovalCtx().getInstanceId() : null);
|
||||
case "approval.apply_user":
|
||||
return safeStr(ctx.getApprovalCtx() != null ? ctx.getApprovalCtx().getApplyUser() : null);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// source.字段名
|
||||
if (key.startsWith("source.")) {
|
||||
String field = key.substring("source.".length());
|
||||
Map<String, Object> rec = ctx.getSourceRecord();
|
||||
if (rec != null && rec.containsKey(field)) {
|
||||
Object v = rec.get(field);
|
||||
return v == null ? "" : v.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// action.xxx.target_id(Phase1 预留)
|
||||
if (key.startsWith("action.")) {
|
||||
return safeStr(ctx.getActionResults().get(key));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 将解析值转为 SQL 字面量,字符串用单引号包裹并转义 */
|
||||
private static String toSqlLiteral(String raw) {
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return "NULL";
|
||||
}
|
||||
// 若值是纯数字,不加引号
|
||||
if (raw.matches("-?\\d+(\\.\\d+)?")) {
|
||||
return raw;
|
||||
}
|
||||
// 字符串:转义单引号后加引号
|
||||
return "'" + raw.replace("'", "''") + "'";
|
||||
}
|
||||
|
||||
private static String safeStr(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine.executor;
|
||||
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationContext;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
|
||||
/**
|
||||
* 集成动作执行器 SPI
|
||||
*/
|
||||
public interface IIntegrationActionExecutor {
|
||||
|
||||
/** 支持的 action_type */
|
||||
String supportActionType();
|
||||
|
||||
/**
|
||||
* 执行动作,返回结果描述(成功时)。
|
||||
* 失败时抛出 RuntimeException,由编排器捕获并写日志。
|
||||
*/
|
||||
String execute(IntegrationContext ctx, MesXslIntegrationAction action);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine.executor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationActionConfigHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationContext;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.RegistryStageFieldHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 审批驳回回退:按集成方案 targetStage 将源单 status 回退并清空环节痕迹。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RegistryStageRevertExecutor implements IIntegrationActionExecutor {
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
@Autowired
|
||||
private IApprovalTraceSyncService approvalTraceSyncService;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Override
|
||||
public String supportActionType() {
|
||||
return "REGISTRY_STAGE_REVERT";
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批注册中心环节回退执行器-----------
|
||||
@Override
|
||||
public String execute(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
String bizTable = ctx.getSourceBizTable();
|
||||
String bizId = ctx.getSourceBizId();
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizId)) {
|
||||
throw new IllegalArgumentException("缺少源单表名或ID");
|
||||
}
|
||||
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(bizTable);
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("业务表未在审批注册中心启用: " + bizTable);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【驳回回退】仅使用动作配置中「回退目标」所选字典键值-----------
|
||||
String targetStage = IntegrationActionConfigHelper.resolveTargetStage(action);
|
||||
if (oConvertUtils.isEmpty(targetStage)) {
|
||||
throw new IllegalStateException(
|
||||
"驳回回退动作未配置「回退目标」,请在集成方案动作编辑器中选择状态字典项并保存(actionConfig.targetStage)");
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【驳回回退】仅使用动作配置中「回退目标」所选字典键值-----------
|
||||
|
||||
String statusField = RegistryStageFieldHelper.statusField(registry);
|
||||
RegistryStageFieldHelper.assertIdentifier(statusField);
|
||||
RegistryStageFieldHelper.assertIdentifier(bizTable);
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】回退只重置业务表状态,操作人/时间由痕迹表承载-----------
|
||||
int affected = jdbcTemplate.update(
|
||||
"UPDATE `" + bizTable + "` SET `" + statusField + "`=? WHERE id=?",
|
||||
targetStage, bizId);
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】回退只重置业务表状态,操作人/时间由痕迹表承载-----------
|
||||
if (affected == 0) {
|
||||
throw new IllegalStateException("源单不存在或回退失败 id=" + bizId);
|
||||
}
|
||||
|
||||
approvalTraceSyncService.revertToStage(bizTable, bizId, targetStage);
|
||||
log.info("[集成引擎][REGISTRY_STAGE_REVERT] table={} id={} targetStage={}", bizTable, bizId, targetStage);
|
||||
return "环节回退成功: " + targetStage;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批注册中心环节回退执行器-----------
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine.executor;
|
||||
|
||||
import java.util.Date;
|
||||
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.integration.engine.ApprovalStageResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationActionConfigHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationContext;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.RegistryStageFieldHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 审批注册中心环节同步:无需手写 SQL,按注册配置更新源单 status/操作人/时间,并双写审批痕迹。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RegistryStageSyncExecutor implements IIntegrationActionExecutor {
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
@Autowired
|
||||
private IApprovalTraceSyncService approvalTraceSyncService;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public String supportActionType() {
|
||||
return "REGISTRY_STAGE_SYNC";
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批注册中心环节同步执行器-----------
|
||||
@Override
|
||||
public String execute(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
String bizTable = ctx.getSourceBizTable();
|
||||
String bizId = ctx.getSourceBizId();
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizId)) {
|
||||
throw new IllegalArgumentException("缺少源单表名或ID");
|
||||
}
|
||||
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(bizTable);
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("业务表未在审批注册中心启用: " + bizTable);
|
||||
}
|
||||
|
||||
String stage = resolveStage(ctx, action);
|
||||
String stageErr = approvalTraceSyncService.checkStageAllowed(bizTable, stage);
|
||||
if (stageErr != null) {
|
||||
throw new IllegalStateException(stageErr);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批环节同步】审批环节仅写痕迹,业务表状态由 statusAfter 控制-----------
|
||||
String statusAfter = resolveStatusAfter(action, stage);
|
||||
if (oConvertUtils.isEmpty(statusAfter)) {
|
||||
throw new IllegalArgumentException("动作未配置通过后状态(statusAfter),且无法从审批环节推断");
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批环节同步】审批环节仅写痕迹,业务表状态由 statusAfter 控制-----------
|
||||
|
||||
String expectedFrom = resolveExpectedFrom(action, stage);
|
||||
String statusField = RegistryStageFieldHelper.statusField(registry);
|
||||
RegistryStageFieldHelper.assertIdentifier(statusField);
|
||||
|
||||
String operator = resolveOperator(ctx);
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】环节同步使用实例tasks最新完成时间-----------
|
||||
Date now = resolveOperatorTime(ctx);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】环节同步使用实例tasks最新完成时间-----------
|
||||
|
||||
if (oConvertUtils.isNotEmpty(expectedFrom)) {
|
||||
Object current = jdbcTemplate.queryForObject(
|
||||
"SELECT `" + statusField + "` FROM `" + bizTable + "` WHERE id = ?",
|
||||
Object.class, bizId);
|
||||
String currentStr = current == null ? "" : String.valueOf(current).trim();
|
||||
if (!expectedFrom.equals(currentStr)) {
|
||||
return "跳过:当前状态=" + currentStr + ",期望=" + expectedFrom;
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】业务表只写状态,操作人/时间统一由痕迹表承载-----------
|
||||
StringBuilder sql = new StringBuilder("UPDATE `").append(bizTable).append("` SET `")
|
||||
.append(statusField).append("`=? WHERE id=?");
|
||||
java.util.List<Object> params = new java.util.ArrayList<>();
|
||||
params.add(statusAfter);
|
||||
params.add(bizId);
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】业务表只写状态,操作人/时间统一由痕迹表承载-----------
|
||||
|
||||
int affected = jdbcTemplate.update(sql.toString(), params.toArray());
|
||||
if (affected == 0) {
|
||||
throw new IllegalStateException("源单不存在或更新失败 id=" + bizId);
|
||||
}
|
||||
|
||||
approvalTraceSyncService.syncStage(bizTable, bizId, stage, operator, now);
|
||||
log.info("[集成引擎][REGISTRY_STAGE_SYNC] table={} id={} stage={} statusAfter={} operator={}",
|
||||
bizTable, bizId, stage, statusAfter, operator);
|
||||
return "环节同步成功: " + ApprovalStageResolver.stageLabel(stage) + " → 状态=" + statusAfter;
|
||||
}
|
||||
|
||||
private String resolveStage(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
String stage = IntegrationActionConfigHelper.resolveStage(action, ctx.getPlan());
|
||||
if (oConvertUtils.isNotEmpty(stage)) {
|
||||
return stage;
|
||||
}
|
||||
throw new IllegalArgumentException("动作未配置审批环节(stage),且方案未绑定 triggerStage");
|
||||
}
|
||||
|
||||
private String resolveExpectedFrom(MesXslIntegrationAction action, String stage) {
|
||||
return IntegrationActionConfigHelper.resolveExpectedFrom(action, stage);
|
||||
}
|
||||
|
||||
private String resolveStatusAfter(MesXslIntegrationAction action, String stage) {
|
||||
return IntegrationActionConfigHelper.resolveStatusAfter(action, stage);
|
||||
}
|
||||
|
||||
private String resolveOperator(IntegrationContext ctx) {
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorName())) {
|
||||
return ac.getOperatorName();
|
||||
}
|
||||
if (ac != null && oConvertUtils.isNotEmpty(ac.getOperatorUsername())) {
|
||||
return ac.getOperatorUsername();
|
||||
}
|
||||
return "系统";
|
||||
}
|
||||
|
||||
private Date resolveOperatorTime(IntegrationContext ctx) {
|
||||
ApprovalCallbackContext ac = ctx.getApprovalCtx();
|
||||
if (ac != null && ac.getOperatorTime() != null) {
|
||||
return ac.getOperatorTime();
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批注册中心环节同步执行器-----------
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.engine.executor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationContext;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.RelatedTableTraceSyncHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.VariableResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* SQL_UPDATE 动作执行器。
|
||||
* 支持 UPDATE / INSERT 语句,变量用 #{...} 占位。
|
||||
* 安全约束:
|
||||
* 1. SQL 必须以 UPDATE 或 INSERT 开头(不区分大小写)
|
||||
* 2. 禁止含 DROP / TRUNCATE / DELETE(无 WHERE 条件的批量删除风险)
|
||||
* 3. 变量值经过 SQL 字面量转义
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】SQL_UPDATE执行器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SqlUpdateActionExecutor implements IIntegrationActionExecutor {
|
||||
|
||||
private static final Pattern ALLOWED_START = Pattern.compile("^(UPDATE|INSERT)\\b", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DANGEROUS = Pattern.compile("\\b(DROP|TRUNCATE|DELETE\\s+FROM)\\b", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private RelatedTableTraceSyncHelper relatedTableTraceSyncHelper;
|
||||
|
||||
@Override
|
||||
public String supportActionType() {
|
||||
return "SQL_UPDATE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(IntegrationContext ctx, MesXslIntegrationAction action) {
|
||||
String template = action.getSqlTemplate();
|
||||
if (oConvertUtils.isEmpty(template)) {
|
||||
throw new IllegalArgumentException("动作 [" + action.getActionName() + "] sql_template 为空");
|
||||
}
|
||||
|
||||
// 变量替换
|
||||
String resolvedSql = VariableResolver.resolveSql(template.trim(), ctx);
|
||||
|
||||
// 安全校验
|
||||
validate(resolvedSql, action.getActionName());
|
||||
|
||||
log.info("[集成引擎][SQL_UPDATE] 执行 action={} sql={}", action.getActionName(), resolvedSql);
|
||||
int affected = jdbcTemplate.update(resolvedSql);
|
||||
//update-begin---author:GHT ---date:20260608 for:【审核集成】SQL_UPDATE零行时输出可诊断提示-----------
|
||||
String result = affected == 0
|
||||
? "影响行数: 0(未匹配记录,请检查关联字段、前置状态及方案绑定环节)"
|
||||
: "影响行数: " + affected;
|
||||
if (affected == 0) {
|
||||
log.warn("[集成引擎][SQL_UPDATE] 零行更新 action={} sql={}", action.getActionName(), resolvedSql);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审核集成】SQL_UPDATE零行时输出可诊断提示-----------
|
||||
log.info("[集成引擎][SQL_UPDATE] 完成 action={} {}", action.getActionName(), result);
|
||||
//update-begin---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL 成功后按动作配置写入/清空目标表痕迹-----------
|
||||
relatedTableTraceSyncHelper.syncAfterSqlUpdate(ctx, action, affected);
|
||||
//update-end---author:GHT ---date:20260610 for:【关联表痕迹同步】SQL 成功后按动作配置写入/清空目标表痕迹-----------
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validate(String sql, String actionName) {
|
||||
if (!ALLOWED_START.matcher(sql).find()) {
|
||||
throw new IllegalArgumentException(
|
||||
"集成动作 [" + actionName + "] SQL 必须以 UPDATE 或 INSERT 开头,实际: " + sql.substring(0, Math.min(50, sql.length())));
|
||||
}
|
||||
if (DANGEROUS.matcher(sql).find()) {
|
||||
throw new IllegalArgumentException(
|
||||
"集成动作 [" + actionName + "] SQL 含有危险关键字(DROP/TRUNCATE/DELETE FROM),已拒绝执行");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审批痕迹明细(每业务单据一行)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【XSLMES-20260605-K8R2】审批痕迹明细
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_trace")
|
||||
@Schema(description = "审批痕迹明细")
|
||||
public class MesXslApprovalTrace extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审批注册配置ID")
|
||||
private String registryId;
|
||||
|
||||
@Schema(description = "业务表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据ID")
|
||||
private String bizDataId;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "钉钉审批实例ID(来自审批台账)")
|
||||
private String externalInstanceId;
|
||||
|
||||
@Schema(description = "校对人")
|
||||
private String proofreadBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "校对时间")
|
||||
private Date proofreadTime;
|
||||
|
||||
@Schema(description = "审核人")
|
||||
private String auditBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "审核时间")
|
||||
private Date auditTime;
|
||||
|
||||
@Schema(description = "批准人")
|
||||
private String approveBy;
|
||||
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "批准时间")
|
||||
private Date approveTime;
|
||||
|
||||
@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,74 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.system.base.entity.JeecgEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批注册中心
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】单据注册
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_biz_doc_registry")
|
||||
@Schema(description = "审批注册中心")
|
||||
public class MesXslBizDocRegistry extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "业务编码,如 formula_spec")
|
||||
private String docCode;
|
||||
|
||||
@Schema(description = "物理表名")
|
||||
private String tableName;
|
||||
|
||||
@Schema(description = "中文名")
|
||||
private String displayName;
|
||||
|
||||
@Dict(dicCode = "yn")
|
||||
@Schema(description = "启用 0否 1是")
|
||||
private Integer enabled;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批环节与字段映射配置-----------
|
||||
@Dict(dicCode = "mes_xsl_approval_stage")
|
||||
@Schema(description = "启用环节(多选逗号分隔 proofread,audit,approve)")
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String enabledStages;
|
||||
|
||||
@Schema(description = "业务状态字段名,默认 status")
|
||||
private String statusField;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】审批环节与字段映射配置-----------
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】移除操作人字段配置,操作人/时间统一由痕迹表承载,业务表只需 statusField-----------
|
||||
// proofreadByField / proofreadTimeField / auditByField / auditTimeField / approveByField / approveTimeField 已移除
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】移除操作人字段配置,操作人/时间统一由痕迹表承载,业务表只需 statusField-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】列表接口路径,配置后自动注入审批痕迹字段-----------
|
||||
@Schema(description = "列表接口路径(多个逗号分隔),配置后自动注入审批痕迹字段到响应")
|
||||
private String listApiPath;
|
||||
//update-end---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】列表接口路径,配置后自动注入审批痕迹字段-----------
|
||||
|
||||
@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,66 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.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;
|
||||
|
||||
/**
|
||||
* 审核集成动作
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成动作
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_integration_action")
|
||||
@Schema(description = "审核集成动作")
|
||||
public class MesXslIntegrationAction extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "所属方案ID")
|
||||
private String planId;
|
||||
|
||||
@Schema(description = "动作名称")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "动作类型")
|
||||
@Dict(dicCode = "mes_xsl_integration_action_type")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "SQL模板(SQL_UPDATE动作用)")
|
||||
private String sqlTemplate;
|
||||
|
||||
@Schema(description = "执行顺序(升序)")
|
||||
private Integer execOrder;
|
||||
|
||||
@Schema(description = "失败策略 stop/continue")
|
||||
@Dict(dicCode = "mes_xsl_integration_on_fail")
|
||||
private String onFail;
|
||||
|
||||
@Schema(description = "幂等键表达式(空=record_id+action_id)")
|
||||
private String idempotentKey;
|
||||
|
||||
@Schema(description = "启用 0否 1是")
|
||||
private Integer enabled;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增可视化配置字段-----------
|
||||
@Schema(description = "可视化配置JSON(可视化编辑器专用)")
|
||||
private String actionConfig;
|
||||
//update-end---author:GHT ---date:2026-06-05 for:【审核集成Phase0】新增可视化配置字段-----------
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审核集成执行日志(只写不改,无 JeecgEntity 基类)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成执行日志
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_integration_log")
|
||||
@Schema(description = "审核集成执行日志")
|
||||
public class MesXslIntegrationLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "审批台账ID")
|
||||
private String recordId;
|
||||
|
||||
@Schema(description = "方案ID")
|
||||
private String planId;
|
||||
|
||||
@Schema(description = "动作ID")
|
||||
private String actionId;
|
||||
|
||||
@Schema(description = "幂等键")
|
||||
private String idempotentKey;
|
||||
|
||||
@Schema(description = "执行状态 success/failed/skipped")
|
||||
@Dict(dicCode = "mes_xsl_integration_log_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "源单ID")
|
||||
private String sourceBizId;
|
||||
|
||||
@Schema(description = "源单表名")
|
||||
private String sourceBizTable;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "重试次数")
|
||||
private Integer retryCount;
|
||||
|
||||
@Schema(description = "耗时ms")
|
||||
private Long execTimeMs;
|
||||
|
||||
@Schema(description = "执行前变量快照")
|
||||
private String requestSnapshot;
|
||||
|
||||
@Schema(description = "执行结果快照")
|
||||
private String responseSnapshot;
|
||||
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.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;
|
||||
|
||||
/**
|
||||
* 审核集成方案
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【审核集成Phase0】集成方案
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_integration_plan")
|
||||
@Schema(description = "审核集成方案")
|
||||
public class MesXslIntegrationPlan extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "方案编码(唯一)")
|
||||
private String planCode;
|
||||
|
||||
@Schema(description = "方案名称")
|
||||
private String planName;
|
||||
|
||||
@Schema(description = "源单表名")
|
||||
private String sourceTable;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
@Schema(description = "审批注册中心ID")
|
||||
private String registryId;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
|
||||
@Schema(description = "触发时机 onApprove/onReject/onNodeApprove")
|
||||
@Dict(dicCode = "mes_xsl_integration_trigger_phase")
|
||||
private String triggerPhase;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
@Schema(description = "绑定审批环节 proofread/audit/approve")
|
||||
@Dict(dicCode = "mes_xsl_approval_stage")
|
||||
private String triggerStage;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
|
||||
@Schema(description = "执行模式 sync/async")
|
||||
@Dict(dicCode = "mes_xsl_integration_exec_mode")
|
||||
private String execMode;
|
||||
|
||||
@Schema(description = "匹配条件(空=无条件)")
|
||||
private String matchCondition;
|
||||
|
||||
@Schema(description = "状态 0草稿 1已发布 2已停用")
|
||||
@Dict(dicCode = "mes_xsl_integration_plan_status")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslApprovalTrace;
|
||||
|
||||
/**
|
||||
* 审批痕迹明细
|
||||
*/
|
||||
public interface MesXslApprovalTraceMapper extends BaseMapper<MesXslApprovalTrace> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
|
||||
public interface MesXslBizDocRegistryMapper extends BaseMapper<MesXslBizDocRegistry> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
|
||||
public interface MesXslIntegrationActionMapper extends BaseMapper<MesXslIntegrationAction> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationLog;
|
||||
|
||||
public interface MesXslIntegrationLogMapper extends BaseMapper<MesXslIntegrationLog> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
|
||||
public interface MesXslIntegrationPlanMapper extends BaseMapper<MesXslIntegrationPlan> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.approval.integration.mapper.MesXslApprovalTraceMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.approval.integration.mapper.MesXslBizDocRegistryMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationActionMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationLogMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationPlanMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审批痕迹双写同步服务
|
||||
*/
|
||||
public interface IApprovalTraceSyncService {
|
||||
|
||||
/**
|
||||
* 校验业务表是否已启用指定审批环节;未注册配置时返回 null(不拦截业务)
|
||||
*/
|
||||
String checkStageAllowed(String bizTable, String stage);
|
||||
|
||||
/**
|
||||
* 环节通过后同步痕迹(upsert 每单据一行)
|
||||
*/
|
||||
void syncStage(String bizTable, String bizDataId, String stage, String operatorBy, Date operatorTime);
|
||||
|
||||
/**
|
||||
* 逆向回退时同步清空高于目标环节的痕迹字段
|
||||
*
|
||||
* @param targetStage 审批环节码(compile/proofread/audit)或业务 status 字典值(如 0)
|
||||
*/
|
||||
void revertToStage(String bizTable, String bizDataId, String targetStage);
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】按实例tasks反写审批痕迹明细-----------
|
||||
/**
|
||||
* 根据钉钉审批实例 tasks 与 MES 流程节点 stageKey,反写痕迹明细及源单操作人/时间字段
|
||||
*/
|
||||
void syncFromDingInstance(String bizTable, String bizDataId, String processInstanceId, String flowConfig);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】按实例tasks反写审批痕迹明细-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】拒绝/终止时清空源单与痕迹操作人-----------
|
||||
/**
|
||||
* 驳回/终止后按 onReject 集成方案回退目标重置业务表 status 并清空痕迹(兼容旧方法名)
|
||||
*/
|
||||
void revertToCompile(String bizTable, String bizDataId);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】拒绝/终止时清空源单与痕迹操作人-----------
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslApprovalTrace;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessForecastVO;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessInstanceFlowVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批痕迹明细
|
||||
*/
|
||||
public interface IMesXslApprovalTraceService extends IService<MesXslApprovalTrace> {
|
||||
|
||||
/**
|
||||
* 按业务表 + 单据ID 查询痕迹(供业务页关联展示)
|
||||
*/
|
||||
MesXslApprovalTrace getByBiz(String bizTable, String bizDataId);
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供响应增强器注入-----------
|
||||
/**
|
||||
* 按业务表 + 批量单据ID 查询痕迹,返回 bizDataId → trace 映射(供 ResponseBodyAdvice 批量注入)
|
||||
*/
|
||||
Map<String, MesXslApprovalTrace> batchQueryByBizIds(String bizTable, List<String> bizDataIds);
|
||||
//update-end---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供响应增强器注入-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
/**
|
||||
* 分页查询并补充钉钉审批实例ID
|
||||
*/
|
||||
IPage<MesXslApprovalTrace> pageWithDingInstanceId(IPage<MesXslApprovalTrace> page, Wrapper<MesXslApprovalTrace> wrapper);
|
||||
|
||||
/**
|
||||
* 批量补充钉钉审批实例ID
|
||||
*/
|
||||
void enrichExternalInstanceIds(List<MesXslApprovalTrace> traces);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
/**
|
||||
* 按业务单据或钉钉实例ID拉取审批流转操作记录
|
||||
*/
|
||||
DingProcessInstanceFlowVO getDingFlowRecords(String bizTable, String bizDataId, String processInstanceId);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
/**
|
||||
* 按业务单据或钉钉实例ID拉取审批实例,从 tasks 按 activityId 解析审批节点
|
||||
*/
|
||||
DingProcessForecastVO getDingProcessForecast(String bizTable, String bizDataId, String processInstanceId);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
/**
|
||||
* 按业务单据或钉钉实例ID拉取审批实例接口原始 JSON 响应
|
||||
*/
|
||||
JSONObject getDingProcessInstance(String bizTable, String bizDataId, String processInstanceId);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
|
||||
public interface IMesXslBizDocRegistryService extends IService<MesXslBizDocRegistry> {
|
||||
|
||||
/**
|
||||
* 保存前规范化字段映射与环节配置
|
||||
*/
|
||||
void normalizeBeforeSave(MesXslBizDocRegistry entity);
|
||||
|
||||
/**
|
||||
* 按物理表名查询已启用的审批注册配置
|
||||
*/
|
||||
MesXslBizDocRegistry findActiveByTableName(String tableName);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IMesXslIntegrationActionService extends IService<MesXslIntegrationAction> {
|
||||
|
||||
/** 按方案ID查询启用动作,按 exec_order 升序 */
|
||||
List<MesXslIntegrationAction> listByPlanId(String planId);
|
||||
|
||||
/** 删除方案下所有动作 */
|
||||
void removeByPlanId(String planId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationLog;
|
||||
|
||||
public interface IMesXslIntegrationLogService extends IService<MesXslIntegrationLog> {
|
||||
|
||||
/** 幂等检查:同一 idempotentKey 是否已 success */
|
||||
boolean isAlreadySuccess(String idempotentKey);
|
||||
|
||||
/** 手动重试失败日志(重新触发 Orchestrator 执行对应 action) */
|
||||
Result<String> retry(String logId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
|
||||
public interface IMesXslIntegrationPlanService extends IService<MesXslIntegrationPlan> {
|
||||
|
||||
/** 发布方案:status 0→1 */
|
||||
Result<String> publish(String planId);
|
||||
|
||||
/** 停用方案:status 1→2 */
|
||||
Result<String> disable(String planId);
|
||||
|
||||
/**
|
||||
* 保存前绑定注册中心并校验环节
|
||||
*/
|
||||
Result<String> normalizeAndValidate(MesXslIntegrationPlan plan);
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalStageResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationActionService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 按审批流程节点 + 业务状态字典,一键生成默认集成方案与动作。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【XSLMES-20260605-K8R2】按流程生成默认集成方案
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class IntegrationPlanGenerator {
|
||||
|
||||
private static final Pattern DICT_IN_COMMENT = Pattern.compile("字典[:\\s]?([a-zA-Z][a-zA-Z0-9_]*)");
|
||||
|
||||
private static final Map<String, String> TABLE_STATUS_DICT_FALLBACK = Map.of(
|
||||
"mes_xsl_mixer_ps_compile", "xslmes_mixer_ps_status",
|
||||
"mes_xsl_formula_spec", "xslmes_formula_spec_status"
|
||||
);
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService flowService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationPlanService planService;
|
||||
@Autowired
|
||||
private IMesXslIntegrationActionService actionService;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】预览按流程生成的默认方案-----------
|
||||
public Result<Map<String, Object>> preview(String sourceTable, String flowId) {
|
||||
return preview(sourceTable, flowId, null);
|
||||
}
|
||||
|
||||
public Result<Map<String, Object>> preview(String sourceTable, String flowId, Map<String, String> stageOverrides) {
|
||||
try {
|
||||
return Result.OK(buildPreview(sourceTable, flowId, stageOverrides));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】预览按流程生成的默认方案-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按流程生成默认集成方案与动作-----------
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generate(String sourceTable, String flowId, boolean overwriteDraft) {
|
||||
return generate(sourceTable, flowId, overwriteDraft, null);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generate(String sourceTable, String flowId, boolean overwriteDraft,
|
||||
Map<String, String> stageOverrides) {
|
||||
Map<String, Object> preview = buildPreview(sourceTable, flowId, stageOverrides);
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(sourceTable);
|
||||
String codePrefix = planCodePrefix(registry);
|
||||
|
||||
if (overwriteDraft) {
|
||||
removeDraftAutoPlans(sourceTable, codePrefix);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> planDefs = (List<Map<String, Object>>) preview.get("plans");
|
||||
int created = 0;
|
||||
int skipped = 0;
|
||||
List<String> planCodes = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> def : planDefs) {
|
||||
String planCode = String.valueOf(def.get("planCode"));
|
||||
if (planService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getPlanCode, planCode)
|
||||
.exists()) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
MesXslIntegrationPlan plan = new MesXslIntegrationPlan();
|
||||
plan.setPlanCode(planCode);
|
||||
plan.setPlanName(String.valueOf(def.get("planName")));
|
||||
plan.setSourceTable(sourceTable);
|
||||
plan.setRegistryId(registry.getId());
|
||||
plan.setTriggerPhase(String.valueOf(def.get("triggerPhase")));
|
||||
Object triggerStage = def.get("triggerStage");
|
||||
if (triggerStage != null && oConvertUtils.isNotEmpty(String.valueOf(triggerStage))) {
|
||||
plan.setTriggerStage(String.valueOf(triggerStage));
|
||||
}
|
||||
plan.setExecMode("async");
|
||||
plan.setStatus("0");
|
||||
plan.setRemark(String.valueOf(def.get("remark")));
|
||||
Result<String> validate = planService.normalizeAndValidate(plan);
|
||||
if (!validate.isSuccess()) {
|
||||
throw new IllegalStateException("方案校验失败[" + planCode + "]: " + validate.getMessage());
|
||||
}
|
||||
planService.save(plan);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> actionDef = (Map<String, Object>) def.get("action");
|
||||
MesXslIntegrationAction action = new MesXslIntegrationAction();
|
||||
action.setPlanId(plan.getId());
|
||||
action.setActionName(String.valueOf(actionDef.get("actionName")));
|
||||
action.setActionType(String.valueOf(actionDef.get("actionType")));
|
||||
action.setActionConfig(JSON.toJSONString(actionDef.get("actionConfig")));
|
||||
action.setExecOrder(1);
|
||||
action.setOnFail("stop");
|
||||
action.setEnabled(1);
|
||||
actionService.save(action);
|
||||
|
||||
created++;
|
||||
planCodes.add(planCode);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>(preview);
|
||||
result.put("created", created);
|
||||
result.put("skipped", skipped);
|
||||
result.put("planCodes", planCodes);
|
||||
return Result.OK("生成完成:新增 " + created + " 个方案,跳过 " + skipped + " 个已存在方案", result);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案并返回方案ID-----------
|
||||
/**
|
||||
* 为流程设计器中当前审批节点生成(或复用)集成方案,便于生成后直接配置动作。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> generateForNode(String sourceTable, String flowId, String nodeId,
|
||||
String stageKey, String flowConfigJson, boolean overwriteDraft) {
|
||||
if (oConvertUtils.isEmpty(sourceTable) || oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(nodeId)) {
|
||||
return Result.error("缺少业务表、审批流或节点信息");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(stageKey)) {
|
||||
return Result.error("请先在节点上绑定审批环节(校对/审核/批准)");
|
||||
}
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(sourceTable);
|
||||
if (registry == null) {
|
||||
return Result.error("业务表未在审批注册中心启用: " + sourceTable);
|
||||
}
|
||||
MesXslApprovalFlow flow = resolveFlow(sourceTable, flowId);
|
||||
String configJson = oConvertUtils.isNotEmpty(flowConfigJson) ? flowConfigJson : flow.getFlowConfig();
|
||||
if (oConvertUtils.isEmpty(configJson)) {
|
||||
return Result.error("审批流程未设计,请先保存或完成流程节点配置");
|
||||
}
|
||||
|
||||
List<String> enabledStages = orderedEnabledStages(registry);
|
||||
List<FlowNode> flowNodes = parseApproverNodes(configJson);
|
||||
boolean nodeFound = flowNodes.stream().anyMatch(n -> nodeId.equals(n.nodeId));
|
||||
if (!nodeFound) {
|
||||
return Result.error("当前节点不在流程配置中,请确认流程设计已包含该节点");
|
||||
}
|
||||
|
||||
List<StatusDictItem> statusChain = loadStatusChain(registry);
|
||||
String initialStatus = resolveInitialStatus(statusChain, enabledStages);
|
||||
Map<String, String> overrides = Map.of(nodeId, stageKey.trim());
|
||||
List<StageBinding> bindings = bindAllFlowNodes(flowNodes, registry, enabledStages, statusChain, initialStatus, overrides);
|
||||
StageBinding binding = bindings.stream()
|
||||
.filter(b -> nodeId.equals(b.nodeId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (binding == null || !binding.stageConfigured) {
|
||||
String reason = binding != null ? binding.unconfiguredReason : "节点环节未配置";
|
||||
return Result.error(reason);
|
||||
}
|
||||
|
||||
String codePrefix = planCodePrefix(registry);
|
||||
String displayName = oConvertUtils.isNotEmpty(registry.getDisplayName()) ? registry.getDisplayName() : sourceTable;
|
||||
String planCode = codePrefix + "_reg_" + binding.stage;
|
||||
String phase = "onNodeApprove";
|
||||
|
||||
MesXslIntegrationPlan existing = planService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getPlanCode, planCode)
|
||||
.one();
|
||||
if (existing != null) {
|
||||
if ("0".equals(existing.getStatus()) && overwriteDraft) {
|
||||
actionService.removeByPlanId(existing.getId());
|
||||
planService.removeById(existing.getId());
|
||||
} else {
|
||||
return buildNodeGenerateResult(existing, false, phase, binding);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> actionConfig = new LinkedHashMap<>();
|
||||
actionConfig.put("visualType", "REGISTRY_STAGE_SYNC");
|
||||
actionConfig.put("stage", binding.stage);
|
||||
actionConfig.put("expectedFrom", binding.expectedFrom);
|
||||
if (oConvertUtils.isNotEmpty(binding.statusAfter)) {
|
||||
actionConfig.put("statusAfter", binding.statusAfter);
|
||||
}
|
||||
|
||||
MesXslIntegrationPlan plan = new MesXslIntegrationPlan();
|
||||
plan.setPlanCode(planCode);
|
||||
plan.setPlanName(displayName + "-" + binding.stageLabel + "通过(流程生成)");
|
||||
plan.setSourceTable(sourceTable);
|
||||
plan.setRegistryId(registry.getId());
|
||||
plan.setTriggerPhase(phase);
|
||||
plan.setTriggerStage(binding.stage);
|
||||
plan.setExecMode("async");
|
||||
plan.setStatus("0");
|
||||
plan.setRemark("按审批流程节点「" + binding.nodeName + "」自动生成");
|
||||
Result<String> validate = planService.normalizeAndValidate(plan);
|
||||
if (!validate.isSuccess()) {
|
||||
return Result.error("方案校验失败: " + validate.getMessage());
|
||||
}
|
||||
planService.save(plan);
|
||||
|
||||
MesXslIntegrationAction action = new MesXslIntegrationAction();
|
||||
action.setPlanId(plan.getId());
|
||||
action.setActionName(binding.stageLabel + "环节同步");
|
||||
action.setActionType("REGISTRY_STAGE_SYNC");
|
||||
action.setActionConfig(JSON.toJSONString(actionConfig));
|
||||
action.setExecOrder(1);
|
||||
action.setOnFail("stop");
|
||||
action.setEnabled(1);
|
||||
actionService.save(action);
|
||||
|
||||
return buildNodeGenerateResult(plan, true, phase, binding);
|
||||
}
|
||||
|
||||
private Result<Map<String, Object>> buildNodeGenerateResult(MesXslIntegrationPlan plan, boolean created,
|
||||
String phase, StageBinding binding) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("planId", plan.getId());
|
||||
out.put("planCode", plan.getPlanCode());
|
||||
out.put("planName", plan.getPlanName());
|
||||
out.put("sourceTable", plan.getSourceTable());
|
||||
out.put("triggerPhase", phase);
|
||||
out.put("triggerStage", plan.getTriggerStage());
|
||||
out.put("status", plan.getStatus());
|
||||
out.put("created", created);
|
||||
out.put("nodeName", binding.nodeName);
|
||||
out.put("stageLabel", binding.stageLabel);
|
||||
String msg = created ? "已生成集成方案,请配置动作并发布" : "该环节已有集成方案,可直接配置动作";
|
||||
return Result.OK(msg, out);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-10 for:【审批流设计】单节点生成集成方案并返回方案ID-----------
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按流程生成默认集成方案与动作-----------
|
||||
|
||||
private Map<String, Object> buildPreview(String sourceTable, String flowId, Map<String, String> stageOverrides) {
|
||||
if (oConvertUtils.isEmpty(sourceTable)) {
|
||||
throw new IllegalArgumentException("请选择业务表");
|
||||
}
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(sourceTable);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("业务表未在审批注册中心启用: " + sourceTable);
|
||||
}
|
||||
|
||||
MesXslApprovalFlow flow = resolveFlow(sourceTable, flowId);
|
||||
if (flow == null || oConvertUtils.isEmpty(flow.getFlowConfig())) {
|
||||
throw new IllegalArgumentException("未找到已配置的审批流程,请先在审批流设计中保存流程");
|
||||
}
|
||||
|
||||
List<String> enabledStages = orderedEnabledStages(registry);
|
||||
|
||||
List<FlowNode> flowNodes = parseApproverNodes(flow.getFlowConfig());
|
||||
if (flowNodes.isEmpty()) {
|
||||
throw new IllegalArgumentException("审批流程中无审批人节点,请先设计流程");
|
||||
}
|
||||
|
||||
List<StatusDictItem> statusChain = loadStatusChain(registry);
|
||||
if (statusChain.isEmpty()) {
|
||||
throw new IllegalArgumentException("无法解析业务状态字典,请检查 status 字段注释或字典配置");
|
||||
}
|
||||
|
||||
String initialStatus = resolveInitialStatus(statusChain, enabledStages);
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按实际流程节点生成并标注环节配置状态-----------
|
||||
List<StageBinding> bindings = bindAllFlowNodes(flowNodes, registry, enabledStages, statusChain, initialStatus, stageOverrides);
|
||||
|
||||
List<StageBinding> configuredBindings = bindings.stream().filter(StageBinding::stageConfigured).toList();
|
||||
if (configuredBindings.isEmpty()) {
|
||||
throw new IllegalArgumentException("流程审批节点均未在审批注册中心配置对应环节,请先在注册中心启用环节并配置人员字段");
|
||||
}
|
||||
|
||||
String codePrefix = planCodePrefix(registry);
|
||||
String displayName = oConvertUtils.isNotEmpty(registry.getDisplayName()) ? registry.getDisplayName() : sourceTable;
|
||||
List<Map<String, Object>> plans = new ArrayList<>();
|
||||
List<Map<String, Object>> nodePreview = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
StageBinding b = bindings.get(i);
|
||||
boolean willGenerate = false;
|
||||
String phase = null;
|
||||
if (b.stageConfigured) {
|
||||
int cfgIdx = configuredBindings.indexOf(b);
|
||||
if (cfgIdx >= 0) {
|
||||
willGenerate = true;
|
||||
// 末节点也用 onNodeApprove,便于在流程设计器「本节点通过」下拉中绑定;终态 onApprove 由引擎按环节匹配兜底
|
||||
phase = "onNodeApprove";
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("nodeIndex", i + 1);
|
||||
node.put("nodeId", b.nodeId);
|
||||
node.put("nodeName", b.nodeName);
|
||||
node.put("nodeNameDisplay", b.nodeName + (b.stageConfigured ? "(已配置该环节)" : "(未配置该环节)"));
|
||||
node.put("stageConfigured", b.stageConfigured);
|
||||
node.put("configuredText", b.stageConfigured ? "已配置该环节" : "未配置该环节");
|
||||
node.put("stage", b.stage);
|
||||
node.put("suggestedStage", b.suggestedStage);
|
||||
node.put("stageLabel", oConvertUtils.isNotEmpty(b.stageLabel) ? b.stageLabel : "-");
|
||||
node.put("willGenerate", willGenerate);
|
||||
node.put("triggerPhase", phase);
|
||||
node.put("expectedFrom", b.expectedFrom);
|
||||
node.put("expectedFromLabel", oConvertUtils.isNotEmpty(b.expectedFrom) ? labelOf(statusChain, b.expectedFrom) : "-");
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批环节同步】预览与生成增加通过后状态-----------
|
||||
node.put("statusAfter", b.statusAfter);
|
||||
node.put("statusAfterLabel", oConvertUtils.isNotEmpty(b.statusAfter) ? labelOf(statusChain, b.statusAfter) : "-");
|
||||
//update-end---author:GHT ---date:20260609 for:【审批环节同步】预览与生成增加通过后状态-----------
|
||||
if (!b.stageConfigured && oConvertUtils.isNotEmpty(b.unconfiguredReason)) {
|
||||
node.put("unconfiguredReason", b.unconfiguredReason);
|
||||
}
|
||||
nodePreview.add(node);
|
||||
|
||||
if (!willGenerate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String planCode = codePrefix + "_reg_" + b.stage;
|
||||
String planName = displayName + "-" + b.stageLabel + "通过(流程生成)";
|
||||
|
||||
Map<String, Object> actionConfig = new LinkedHashMap<>();
|
||||
actionConfig.put("visualType", "REGISTRY_STAGE_SYNC");
|
||||
actionConfig.put("stage", b.stage);
|
||||
actionConfig.put("expectedFrom", b.expectedFrom);
|
||||
if (oConvertUtils.isNotEmpty(b.statusAfter)) {
|
||||
actionConfig.put("statusAfter", b.statusAfter);
|
||||
}
|
||||
|
||||
Map<String, Object> action = new LinkedHashMap<>();
|
||||
action.put("actionName", b.stageLabel + "环节同步");
|
||||
action.put("actionType", "REGISTRY_STAGE_SYNC");
|
||||
action.put("actionConfig", actionConfig);
|
||||
|
||||
Map<String, Object> plan = new LinkedHashMap<>();
|
||||
plan.put("planCode", planCode);
|
||||
plan.put("planName", planName);
|
||||
plan.put("triggerPhase", phase);
|
||||
plan.put("triggerStage", b.stage);
|
||||
plan.put("remark", "按审批流程节点「" + b.nodeName + "」自动生成");
|
||||
plan.put("action", action);
|
||||
plans.add(plan);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】按实际流程节点生成并标注环节配置状态-----------
|
||||
|
||||
String rejectCode = codePrefix + "_reg_reject";
|
||||
Map<String, Object> rejectActionConfig = new LinkedHashMap<>();
|
||||
rejectActionConfig.put("visualType", "REGISTRY_STAGE_REVERT");
|
||||
rejectActionConfig.put("targetStage", initialStatus);
|
||||
|
||||
Map<String, Object> rejectAction = new LinkedHashMap<>();
|
||||
rejectAction.put("actionName", "驳回回退" + labelOf(statusChain, initialStatus));
|
||||
rejectAction.put("actionType", "REGISTRY_STAGE_REVERT");
|
||||
rejectAction.put("actionConfig", rejectActionConfig);
|
||||
|
||||
Map<String, Object> rejectPlan = new LinkedHashMap<>();
|
||||
rejectPlan.put("planCode", rejectCode);
|
||||
rejectPlan.put("planName", displayName + "-驳回回退(流程生成)");
|
||||
rejectPlan.put("triggerPhase", "onReject");
|
||||
rejectPlan.put("triggerStage", null);
|
||||
rejectPlan.put("remark", "驳回时回退至初始状态「" + labelOf(statusChain, initialStatus) + "」并清空痕迹");
|
||||
rejectPlan.put("action", rejectAction);
|
||||
plans.add(rejectPlan);
|
||||
|
||||
Map<String, Object> preview = new LinkedHashMap<>();
|
||||
preview.put("sourceTable", sourceTable);
|
||||
preview.put("registryId", registry.getId());
|
||||
preview.put("flowId", flow.getId());
|
||||
preview.put("flowName", flow.getFlowName());
|
||||
preview.put("statusDictCode", resolveStatusDictCode(registry));
|
||||
preview.put("initialStatus", initialStatus);
|
||||
preview.put("initialStatusLabel", labelOf(statusChain, initialStatus));
|
||||
preview.put("statusChain", statusChain.stream().map(StatusDictItem::toMap).toList());
|
||||
preview.put("flowNodes", flowNodes.stream().map(FlowNode::toMap).toList());
|
||||
preview.put("nodeBindings", nodePreview);
|
||||
preview.put("flowNodeCount", flowNodes.size());
|
||||
preview.put("configuredNodeCount", configuredBindings.size());
|
||||
preview.put("unconfiguredNodeCount", flowNodes.size() - configuredBindings.size());
|
||||
preview.put("enabledStages", enabledStages);
|
||||
preview.put("stageOptions", buildStageOptions(statusChain));
|
||||
preview.put("stageMeta", buildStageMeta(registry, statusChain));
|
||||
preview.put("plans", plans);
|
||||
return preview;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别环节可选手选并支持环节元数据-----------
|
||||
private List<Map<String, Object>> buildStageOptions(List<StatusDictItem> statusChain) {
|
||||
List<Map<String, Object>> options = new ArrayList<>();
|
||||
for (String stage : new String[]{
|
||||
ApprovalStageResolver.STAGE_PROOFREAD,
|
||||
ApprovalStageResolver.STAGE_AUDIT,
|
||||
ApprovalStageResolver.STAGE_APPROVE}) {
|
||||
Map<String, Object> opt = new LinkedHashMap<>();
|
||||
opt.put("value", stage);
|
||||
opt.put("label", labelOf(statusChain, stage));
|
||||
options.add(opt);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildStageMeta(MesXslBizDocRegistry registry, List<StatusDictItem> statusChain) {
|
||||
Map<String, Object> meta = new LinkedHashMap<>();
|
||||
Set<String> enabled = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
for (String stage : new String[]{
|
||||
ApprovalStageResolver.STAGE_PROOFREAD,
|
||||
ApprovalStageResolver.STAGE_AUDIT,
|
||||
ApprovalStageResolver.STAGE_APPROVE}) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("label", labelOf(statusChain, stage));
|
||||
item.put("enabled", enabled.contains(stage));
|
||||
item.put("configured", isStageConfigured(registry, stage));
|
||||
meta.put(stage, item);
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
public static Map<String, String> parseStageOverrides(List<Map<String, Object>> nodeBindings) {
|
||||
if (nodeBindings == null || nodeBindings.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> overrides = new LinkedHashMap<>();
|
||||
for (Map<String, Object> binding : nodeBindings) {
|
||||
if (binding == null || binding.get("nodeId") == null) {
|
||||
continue;
|
||||
}
|
||||
String nodeId = String.valueOf(binding.get("nodeId"));
|
||||
Object stageVal = binding.get("stage");
|
||||
if (stageVal == null || oConvertUtils.isEmpty(String.valueOf(stageVal))) {
|
||||
overrides.put(nodeId, null);
|
||||
} else {
|
||||
overrides.put(nodeId, String.valueOf(stageVal).trim());
|
||||
}
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】识别环节可选手选并支持环节元数据-----------
|
||||
|
||||
private MesXslApprovalFlow resolveFlow(String sourceTable, String flowId) {
|
||||
if (oConvertUtils.isNotEmpty(flowId)) {
|
||||
MesXslApprovalFlow flow = flowService.getById(flowId);
|
||||
if (flow != null && sourceTable.equals(flow.getBizTable())) {
|
||||
return flow;
|
||||
}
|
||||
throw new IllegalArgumentException("审批流与业务表不匹配");
|
||||
}
|
||||
return flowService.lambdaQuery()
|
||||
.eq(MesXslApprovalFlow::getBizTable, sourceTable)
|
||||
.orderByDesc(MesXslApprovalFlow::getUpdateTime)
|
||||
.orderByDesc(MesXslApprovalFlow::getCreateTime)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
private List<String> orderedEnabledStages(MesXslBizDocRegistry registry) {
|
||||
Set<String> enabled = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
List<String> ordered = new ArrayList<>();
|
||||
for (String key : new String[]{
|
||||
ApprovalStageResolver.STAGE_PROOFREAD,
|
||||
ApprovalStageResolver.STAGE_AUDIT,
|
||||
ApprovalStageResolver.STAGE_APPROVE}) {
|
||||
if (enabled.contains(key)) {
|
||||
ordered.add(key);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private List<FlowNode> parseApproverNodes(String flowConfig) {
|
||||
List<FlowNode> nodes = new ArrayList<>();
|
||||
collectApproverNodes(JSONObject.parseObject(flowConfig), nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private void collectApproverNodes(JSONObject node, List<FlowNode> out) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
if ("approver".equals(node.getString("type"))) {
|
||||
JSONObject props = node.getJSONObject("props");
|
||||
if (props == null) {
|
||||
props = node.getJSONObject("properties");
|
||||
}
|
||||
if (props == null) {
|
||||
props = new JSONObject();
|
||||
}
|
||||
String name = props.getString("name");
|
||||
if (oConvertUtils.isEmpty(name)) {
|
||||
name = node.getString("name");
|
||||
}
|
||||
out.add(new FlowNode(
|
||||
oConvertUtils.isNotEmpty(name) ? name : "审批节点" + (out.size() + 1),
|
||||
node.getString("id"),
|
||||
props));
|
||||
}
|
||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
||||
if (branches != null && !branches.isEmpty()) {
|
||||
Object first = branches.get(0);
|
||||
if (first instanceof JSONObject branch) {
|
||||
collectApproverNodes(branch.getJSONObject("childNode"), out);
|
||||
}
|
||||
}
|
||||
collectApproverNodes(node.getJSONObject("childNode"), out);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】遍历全部流程节点解析环节并判断注册中心是否已配置-----------
|
||||
private List<StageBinding> bindAllFlowNodes(List<FlowNode> flowNodes,
|
||||
MesXslBizDocRegistry registry,
|
||||
List<String> enabledStages,
|
||||
List<StatusDictItem> statusChain,
|
||||
String initialStatus,
|
||||
Map<String, String> stageOverrides) {
|
||||
List<StageBinding> bindings = new ArrayList<>();
|
||||
for (int i = 0; i < flowNodes.size(); i++) {
|
||||
FlowNode node = flowNodes.get(i);
|
||||
String suggestedStage = resolveStageFromNode(node, registry, enabledStages, i);
|
||||
String stage = suggestedStage;
|
||||
if (stageOverrides != null && stageOverrides.containsKey(node.nodeId)) {
|
||||
stage = stageOverrides.get(node.nodeId);
|
||||
}
|
||||
boolean configured = isStageConfigured(registry, stage);
|
||||
String unconfiguredReason = null;
|
||||
if (!configured) {
|
||||
unconfiguredReason = buildUnconfiguredReason(registry, stage, enabledStages);
|
||||
}
|
||||
String stageLabel = "-";
|
||||
if (oConvertUtils.isNotEmpty(stage)) {
|
||||
stageLabel = labelOf(statusChain, stage);
|
||||
if (oConvertUtils.isEmpty(stageLabel) || stage.equals(stageLabel)) {
|
||||
stageLabel = ApprovalStageResolver.stageLabel(stage);
|
||||
}
|
||||
}
|
||||
bindings.add(new StageBinding(
|
||||
node.name, node.nodeId, stage, stageLabel, null, null, configured, unconfiguredReason, suggestedStage));
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
StageBinding b = bindings.get(i);
|
||||
String expectedFrom = b.stageConfigured
|
||||
? resolveExpectedFromForBinding(bindings, i, statusChain, initialStatus)
|
||||
: null;
|
||||
String statusAfter = b.stageConfigured
|
||||
? resolveStatusAfterForBinding(b, statusChain)
|
||||
: null;
|
||||
bindings.set(i, b.withExpectedFrom(expectedFrom).withStatusAfter(statusAfter));
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批环节同步】推断通过后业务状态(字典含环节码时自动填充)-----------
|
||||
private String resolveStatusAfterForBinding(StageBinding binding, List<StatusDictItem> statusChain) {
|
||||
if (oConvertUtils.isEmpty(binding.stage)) {
|
||||
return null;
|
||||
}
|
||||
if (indexOfValue(statusChain, binding.stage) >= 0) {
|
||||
return binding.stage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批环节同步】推断通过后业务状态(字典含环节码时自动填充)-----------
|
||||
|
||||
private String resolveStageFromNode(FlowNode node, MesXslBizDocRegistry registry,
|
||||
List<String> enabledStages, int nodeIndex) {
|
||||
JSONObject props = node.props;
|
||||
if (props != null) {
|
||||
String stageKey = props.getString("stageKey");
|
||||
if (oConvertUtils.isNotEmpty(stageKey)) {
|
||||
return stageKey.trim();
|
||||
}
|
||||
String fromField = mapFieldToStage(registry, props.getString("fieldName"));
|
||||
if (oConvertUtils.isNotEmpty(fromField)) {
|
||||
return fromField;
|
||||
}
|
||||
}
|
||||
String fromName = ApprovalStageResolver.resolveStageFromNodeName(node.name);
|
||||
if (oConvertUtils.isNotEmpty(fromName)) {
|
||||
return fromName;
|
||||
}
|
||||
if (nodeIndex < enabledStages.size()) {
|
||||
return enabledStages.get(nodeIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】移除 byField 引用,操作人由痕迹表承载-----------
|
||||
private String mapFieldToStage(MesXslBizDocRegistry registry, String fieldName) {
|
||||
// byField 已移除,节点 fieldName 不再映射环节,由 stageKey 或节点名称推断
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isStageConfigured(MesXslBizDocRegistry registry, String stage) {
|
||||
if (registry == null || oConvertUtils.isEmpty(stage)) {
|
||||
return false;
|
||||
}
|
||||
Set<String> enabled = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
return enabled.contains(stage);
|
||||
}
|
||||
|
||||
private String buildUnconfiguredReason(MesXslBizDocRegistry registry, String stage, List<String> enabledStages) {
|
||||
if (oConvertUtils.isEmpty(stage)) {
|
||||
return "未选择审批环节";
|
||||
}
|
||||
Set<String> enabled = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
if (!enabled.contains(stage)) {
|
||||
return "环节「" + ApprovalStageResolver.stageLabel(stage) + "」未在注册中心启用";
|
||||
}
|
||||
return "环节未完整配置";
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】移除 byField 引用,操作人由痕迹表承载-----------
|
||||
|
||||
private String resolveExpectedFromForBinding(List<StageBinding> bindings, int index,
|
||||
List<StatusDictItem> statusChain, String initialStatus) {
|
||||
StageBinding current = bindings.get(index);
|
||||
if (oConvertUtils.isEmpty(current.stage)) {
|
||||
return initialStatus;
|
||||
}
|
||||
int stageIdx = indexOfValue(statusChain, current.stage);
|
||||
if (stageIdx > 0) {
|
||||
return statusChain.get(stageIdx - 1).value;
|
||||
}
|
||||
for (int j = index - 1; j >= 0; j--) {
|
||||
StageBinding prev = bindings.get(j);
|
||||
if (prev.stageConfigured && oConvertUtils.isNotEmpty(prev.stage)) {
|
||||
return prev.stage;
|
||||
}
|
||||
}
|
||||
return initialStatus;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】遍历全部流程节点解析环节并判断注册中心是否已配置-----------
|
||||
|
||||
private String resolveInitialStatus(List<StatusDictItem> chain, List<String> enabledStages) {
|
||||
Set<String> enabledSet = new LinkedHashSet<>(enabledStages);
|
||||
int firstStageIdx = -1;
|
||||
for (int i = 0; i < chain.size(); i++) {
|
||||
if (enabledSet.contains(chain.get(i).value)) {
|
||||
firstStageIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstStageIdx > 0) {
|
||||
return chain.get(firstStageIdx - 1).value;
|
||||
}
|
||||
for (StatusDictItem item : chain) {
|
||||
if (!enabledSet.contains(item.value)) {
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
return chain.get(0).value;
|
||||
}
|
||||
|
||||
private List<StatusDictItem> loadStatusChain(MesXslBizDocRegistry registry) {
|
||||
String dictCode = resolveStatusDictCode(registry);
|
||||
if (oConvertUtils.isEmpty(dictCode)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT item_value AS value, item_text AS label, sort_order AS sortOrder "
|
||||
+ "FROM sys_dict_item WHERE dict_id=(SELECT id FROM sys_dict WHERE dict_code=?) "
|
||||
+ "AND status=1 ORDER BY sort_order ASC, item_value ASC",
|
||||
dictCode);
|
||||
List<StatusDictItem> chain = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
chain.add(new StatusDictItem(
|
||||
String.valueOf(row.get("value")),
|
||||
String.valueOf(row.get("label"))));
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
private String resolveStatusDictCode(MesXslBizDocRegistry registry) {
|
||||
String statusField = oConvertUtils.isEmpty(registry.getStatusField()) ? "status" : registry.getStatusField();
|
||||
String table = registry.getTableName();
|
||||
if (!table.matches("^[a-z][a-z0-9_]{0,63}$")) {
|
||||
return TABLE_STATUS_DICT_FALLBACK.getOrDefault(table, null);
|
||||
}
|
||||
try {
|
||||
List<String> comments = jdbcTemplate.queryForList(
|
||||
"SELECT COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS "
|
||||
+ "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=?",
|
||||
String.class, table, statusField);
|
||||
if (!comments.isEmpty()) {
|
||||
Matcher m = DICT_IN_COMMENT.matcher(comments.get(0));
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[集成方案生成] 读取字段注释失败 table={} field={}", table, statusField, e);
|
||||
}
|
||||
return TABLE_STATUS_DICT_FALLBACK.getOrDefault(table, null);
|
||||
}
|
||||
|
||||
private String planCodePrefix(MesXslBizDocRegistry registry) {
|
||||
if (oConvertUtils.isNotEmpty(registry.getDocCode())) {
|
||||
return registry.getDocCode().replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
}
|
||||
return registry.getTableName().replaceAll("^mes_xsl_", "").replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
}
|
||||
|
||||
private void removeDraftAutoPlans(String sourceTable, String codePrefix) {
|
||||
List<MesXslIntegrationPlan> drafts = planService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getSourceTable, sourceTable)
|
||||
.eq(MesXslIntegrationPlan::getStatus, "0")
|
||||
.likeRight(MesXslIntegrationPlan::getPlanCode, codePrefix + "_reg_")
|
||||
.list();
|
||||
for (MesXslIntegrationPlan plan : drafts) {
|
||||
actionService.removeByPlanId(plan.getId());
|
||||
planService.removeById(plan.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private static int indexOfValue(List<StatusDictItem> chain, String value) {
|
||||
for (int i = 0; i < chain.size(); i++) {
|
||||
if (value.equals(chain.get(i).value)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String labelOf(List<StatusDictItem> chain, String value) {
|
||||
for (StatusDictItem item : chain) {
|
||||
if (value.equals(item.value)) {
|
||||
return item.label;
|
||||
}
|
||||
}
|
||||
return ApprovalStageResolver.stageLabel(value);
|
||||
}
|
||||
|
||||
private record FlowNode(String name, String nodeId, JSONObject props) {
|
||||
Map<String, Object> toMap() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("nodeName", name);
|
||||
m.put("nodeId", nodeId);
|
||||
if (props != null && !props.isEmpty()) {
|
||||
m.put("fieldName", props.getString("fieldName"));
|
||||
m.put("stageKey", props.getString("stageKey"));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
private record StatusDictItem(String value, String label) {
|
||||
Map<String, Object> toMap() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("value", value);
|
||||
m.put("label", label);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
private record StageBinding(String nodeName, String nodeId, String stage, String stageLabel,
|
||||
String expectedFrom, String statusAfter, boolean stageConfigured,
|
||||
String unconfiguredReason, String suggestedStage) {
|
||||
StageBinding withExpectedFrom(String expectedFrom) {
|
||||
return new StageBinding(nodeName, nodeId, stage, stageLabel, expectedFrom, statusAfter,
|
||||
stageConfigured, unconfiguredReason, suggestedStage);
|
||||
}
|
||||
|
||||
StageBinding withStatusAfter(String statusAfter) {
|
||||
return new StageBinding(nodeName, nodeId, stage, stageLabel, expectedFrom, statusAfter,
|
||||
stageConfigured, unconfiguredReason, suggestedStage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalInstanceStageExtractor;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalInstanceStageExtractor.StageCompletion;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationRevertTargetResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.RegistryStageFieldHelper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslApprovalTrace;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslApprovalTraceMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkWorkflowService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 审批痕迹双写同步
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-05 for:【XSLMES-20260605-K8R2】审批痕迹双写
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ApprovalTraceSyncServiceImpl implements IApprovalTraceSyncService {
|
||||
|
||||
private static final String STAGE_PROOFREAD = "proofread";
|
||||
private static final String STAGE_AUDIT = "audit";
|
||||
private static final String STAGE_APPROVE = "approve";
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
|
||||
@Autowired
|
||||
private MesXslApprovalTraceMapper traceMapper;
|
||||
|
||||
@Autowired
|
||||
private DingTalkWorkflowService dingTalkWorkflowService;
|
||||
|
||||
@Autowired
|
||||
private ApprovalInstanceStageExtractor instanceStageExtractor;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private IntegrationRevertTargetResolver revertTargetResolver;
|
||||
|
||||
@Override
|
||||
public String checkStageAllowed(String bizTable, String stage) {
|
||||
MesXslBizDocRegistry registry = findActiveRegistry(bizTable);
|
||||
if (registry == null || oConvertUtils.isEmpty(registry.getEnabledStages())) {
|
||||
return null;
|
||||
}
|
||||
if (!containsStage(registry.getEnabledStages(), stage)) {
|
||||
return "业务表[" + registry.getDisplayName() + "]未启用「" + stageLabel(stage) + "」环节";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncStage(String bizTable, String bizDataId, String stage, String operatorBy, Date operatorTime) {
|
||||
MesXslBizDocRegistry registry = findActiveRegistry(bizTable);
|
||||
if (registry == null || !containsStage(registry.getEnabledStages(), stage)) {
|
||||
return;
|
||||
}
|
||||
MesXslApprovalTrace trace = findTraceByBiz(bizTable, bizDataId);
|
||||
if (trace == null) {
|
||||
trace = new MesXslApprovalTrace()
|
||||
.setRegistryId(registry.getId())
|
||||
.setBizTable(bizTable)
|
||||
.setBizDataId(bizDataId);
|
||||
}
|
||||
Date opTime = operatorTime == null ? new Date() : operatorTime;
|
||||
switch (stage) {
|
||||
case STAGE_PROOFREAD:
|
||||
trace.setProofreadBy(operatorBy);
|
||||
trace.setProofreadTime(opTime);
|
||||
break;
|
||||
case STAGE_AUDIT:
|
||||
trace.setAuditBy(operatorBy);
|
||||
trace.setAuditTime(opTime);
|
||||
break;
|
||||
case STAGE_APPROVE:
|
||||
trace.setApproveBy(operatorBy);
|
||||
trace.setApproveTime(opTime);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
saveOrUpdateTrace(trace);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void revertToStage(String bizTable, String bizDataId, String targetStage) {
|
||||
MesXslApprovalTrace trace = findTraceByBiz(bizTable, bizDataId);
|
||||
if (trace == null) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslApprovalTrace> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslApprovalTrace::getId, trace.getId());
|
||||
//update-begin---author:GHT ---date:20260609 for:【驳回回退】业务字典回退目标(如 0/待处理)清空全部环节痕迹-----------
|
||||
if (isFullTraceClearTarget(targetStage)) {
|
||||
wrapper.set(MesXslApprovalTrace::getProofreadBy, null)
|
||||
.set(MesXslApprovalTrace::getProofreadTime, null)
|
||||
.set(MesXslApprovalTrace::getAuditBy, null)
|
||||
.set(MesXslApprovalTrace::getAuditTime, null)
|
||||
.set(MesXslApprovalTrace::getApproveBy, null)
|
||||
.set(MesXslApprovalTrace::getApproveTime, null);
|
||||
} else if (STAGE_PROOFREAD.equals(targetStage)) {
|
||||
wrapper.set(MesXslApprovalTrace::getAuditBy, null)
|
||||
.set(MesXslApprovalTrace::getAuditTime, null)
|
||||
.set(MesXslApprovalTrace::getApproveBy, null)
|
||||
.set(MesXslApprovalTrace::getApproveTime, null);
|
||||
} else if (STAGE_AUDIT.equals(targetStage)) {
|
||||
wrapper.set(MesXslApprovalTrace::getApproveBy, null)
|
||||
.set(MesXslApprovalTrace::getApproveTime, null);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【驳回回退】业务字典回退目标(如 0/待处理)清空全部环节痕迹-----------
|
||||
traceMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】按实例tasks反写审批痕迹明细-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncFromDingInstance(String bizTable, String bizDataId, String processInstanceId, String flowConfig) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId) || oConvertUtils.isEmpty(processInstanceId)) {
|
||||
return;
|
||||
}
|
||||
MesXslBizDocRegistry registry = findActiveRegistry(bizTable);
|
||||
if (registry == null || oConvertUtils.isEmpty(flowConfig)) {
|
||||
return;
|
||||
}
|
||||
JSONObject instance = dingTalkWorkflowService.getProcessInstance(processInstanceId);
|
||||
if (instance == null) {
|
||||
log.warn("[审批痕迹反写] 拉取审批实例失败 table={} bizId={} instanceId={}", bizTable, bizDataId, processInstanceId);
|
||||
return;
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】拒绝/终止实例禁止反写已通过环节-----------
|
||||
if (instanceStageExtractor.isInstanceRejectedOrCancelled(instance)) {
|
||||
revertToCompile(bizTable, bizDataId);
|
||||
log.info("[审批痕迹反写] 实例已拒绝/终止,已清空痕迹 table={} bizId={} instanceId={}",
|
||||
bizTable, bizDataId, processInstanceId);
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】拒绝/终止实例禁止反写已通过环节-----------
|
||||
List<StageCompletion> completions = instanceStageExtractor.resolveCompletedStages(instance, flowConfig);
|
||||
if (completions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (StageCompletion completion : completions) {
|
||||
if (completion == null || oConvertUtils.isEmpty(completion.getStage())) {
|
||||
continue;
|
||||
}
|
||||
String stageErr = checkStageAllowed(bizTable, completion.getStage());
|
||||
if (stageErr != null) {
|
||||
continue;
|
||||
}
|
||||
syncStage(bizTable, bizDataId, completion.getStage(), completion.getOperatorBy(), completion.getOperatorTime());
|
||||
//update-begin---author:GHT ---date:20260608 for:【缺陷修复-会签集成】移除补偿路径对源单的直接修改,源单状态变更由集成方案动作(RegistryStageSyncExecutor)负责-----------
|
||||
// 此处不再调用 updateBizStageFields:源单状态字段必须经集成方案动作统一变更,
|
||||
// 补偿反写(backfillTraceFromDingInstances)只负责更新审批痕迹表,
|
||||
// 避免绕过集成方案导致第二条及后续动作(如关联表 SQL_UPDATE)无法执行。
|
||||
//update-end---author:GHT ---date:20260608 for:【缺陷修复-会签集成】移除补偿路径对源单的直接修改,源单状态变更由集成方案动作(RegistryStageSyncExecutor)负责-----------
|
||||
}
|
||||
log.info("[审批痕迹反写] 完成 table={} bizId={} instanceId={} stages={}",
|
||||
bizTable, bizDataId, processInstanceId,
|
||||
completions.stream().map(StageCompletion::getStage).reduce((a, b) -> a + "," + b).orElse(""));
|
||||
}
|
||||
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】按实例tasks反写审批痕迹明细-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【审批注册中心】拒绝/终止只重置业务表状态,操作人/时间由痕迹表承载-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void revertToCompile(String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260609 for:【驳回回退】补偿回退读取 onReject 集成方案 targetStage,不写死 compile-----------
|
||||
String targetStage = revertTargetResolver.resolveRevertTarget(bizTable);
|
||||
MesXslBizDocRegistry registry = findActiveRegistry(bizTable);
|
||||
if (registry != null) {
|
||||
String statusField = RegistryStageFieldHelper.statusField(registry);
|
||||
RegistryStageFieldHelper.assertIdentifier(statusField);
|
||||
RegistryStageFieldHelper.assertIdentifier(bizTable);
|
||||
jdbcTemplate.update(
|
||||
"UPDATE `" + bizTable + "` SET `" + statusField + "`=? WHERE id=?",
|
||||
targetStage, bizDataId);
|
||||
}
|
||||
revertToStage(bizTable, bizDataId, targetStage);
|
||||
log.info("[审批痕迹回退] table={} id={} targetStage={}", bizTable, bizDataId, targetStage);
|
||||
//update-end---author:GHT ---date:20260609 for:【驳回回退】补偿回退读取 onReject 集成方案 targetStage,不写死 compile-----------
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【审批注册中心】拒绝/终止只重置业务表状态,操作人/时间由痕迹表承载-----------
|
||||
|
||||
private MesXslApprovalTrace findTraceByBiz(String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return null;
|
||||
}
|
||||
return traceMapper.selectOne(new LambdaQueryWrapper<MesXslApprovalTrace>()
|
||||
.eq(MesXslApprovalTrace::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalTrace::getBizDataId, bizDataId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private void saveOrUpdateTrace(MesXslApprovalTrace trace) {
|
||||
if (oConvertUtils.isEmpty(trace.getId())) {
|
||||
traceMapper.insert(trace);
|
||||
} else {
|
||||
traceMapper.updateById(trace);
|
||||
}
|
||||
}
|
||||
|
||||
private MesXslBizDocRegistry findActiveRegistry(String bizTable) {
|
||||
if (oConvertUtils.isEmpty(bizTable)) {
|
||||
return null;
|
||||
}
|
||||
return registryService.lambdaQuery()
|
||||
.eq(MesXslBizDocRegistry::getTableName, bizTable)
|
||||
.eq(MesXslBizDocRegistry::getEnabled, 1)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
private boolean containsStage(String enabledStages, String stage) {
|
||||
if (oConvertUtils.isEmpty(enabledStages) || oConvertUtils.isEmpty(stage)) {
|
||||
return false;
|
||||
}
|
||||
Set<String> set = new HashSet<>(Arrays.asList(enabledStages.split(",")));
|
||||
return set.contains(stage.trim());
|
||||
}
|
||||
|
||||
private String stageLabel(String stage) {
|
||||
switch (stage) {
|
||||
case STAGE_PROOFREAD:
|
||||
return "校对";
|
||||
case STAGE_AUDIT:
|
||||
return "审核";
|
||||
case STAGE_APPROVE:
|
||||
return "批准";
|
||||
default:
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
|
||||
/** 回退到编制态或业务字典初始态时,清空全部审批环节痕迹 */
|
||||
private boolean isFullTraceClearTarget(String targetStage) {
|
||||
if (oConvertUtils.isEmpty(targetStage)) {
|
||||
return true;
|
||||
}
|
||||
return "compile".equals(targetStage)
|
||||
|| (!STAGE_PROOFREAD.equals(targetStage)
|
||||
&& !STAGE_AUDIT.equals(targetStage)
|
||||
&& !STAGE_APPROVE.equals(targetStage));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.constant.ApprovalRecordConstants;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalInstanceStageExtractor;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalInstanceStageExtractor.NodePair;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalInstanceStageExtractor.NodeTaskDecision;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslApprovalTrace;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslApprovalTraceMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IApprovalTraceSyncService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslApprovalTraceService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingOperationRecordVO;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessForecastNodeVO;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessForecastVO;
|
||||
import org.jeecg.modules.xslmes.approval.integration.vo.DingProcessInstanceFlowVO;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.DingApprovalLaunchParamBuilder;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkWorkflowService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审批痕迹明细
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MesXslApprovalTraceServiceImpl extends ServiceImpl<MesXslApprovalTraceMapper, MesXslApprovalTrace>
|
||||
implements IMesXslApprovalTraceService {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService approvalRecordService;
|
||||
|
||||
@Autowired
|
||||
private DingTalkWorkflowService dingTalkWorkflowService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private IMesXslDingProcessTplService dingProcessTplService;
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService approvalFlowService;
|
||||
|
||||
@Autowired
|
||||
private DingApprovalLaunchParamBuilder launchParamBuilder;
|
||||
|
||||
@Autowired
|
||||
private IApprovalTraceSyncService approvalTraceSyncService;
|
||||
|
||||
@Autowired
|
||||
private ApprovalInstanceStageExtractor instanceStageExtractor;
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供响应增强器注入-----------
|
||||
@Override
|
||||
public Map<String, MesXslApprovalTrace> batchQueryByBizIds(String bizTable, List<String> bizDataIds) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || bizDataIds == null || bizDataIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<String> ids = bizDataIds.stream()
|
||||
.filter(oConvertUtils::isNotEmpty)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<MesXslApprovalTrace> traces = lambdaQuery()
|
||||
.eq(MesXslApprovalTrace::getBizTable, bizTable)
|
||||
.in(MesXslApprovalTrace::getBizDataId, ids)
|
||||
.list();
|
||||
return traces.stream().collect(
|
||||
Collectors.toMap(MesXslApprovalTrace::getBizDataId, Function.identity(), (a, b) -> a));
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【XSLMES-20260608-TRACE】批量查询痕迹供响应增强器注入-----------
|
||||
|
||||
@Override
|
||||
public MesXslApprovalTrace getByBiz(String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return null;
|
||||
}
|
||||
return lambdaQuery()
|
||||
.eq(MesXslApprovalTrace::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalTrace::getBizDataId, bizDataId)
|
||||
.one();
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
@Override
|
||||
public IPage<MesXslApprovalTrace> pageWithDingInstanceId(IPage<MesXslApprovalTrace> page, Wrapper<MesXslApprovalTrace> wrapper) {
|
||||
IPage<MesXslApprovalTrace> result = page(page, wrapper);
|
||||
enrichExternalInstanceIds(result.getRecords());
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】列表补偿反写(主路径为钉钉回调→集成方案编排)-----------
|
||||
backfillTraceFromDingInstances(result.getRecords());
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】列表补偿反写(主路径为钉钉回调→集成方案编排)-----------
|
||||
return result;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】列表加载时按实例tasks反写痕迹-----------
|
||||
private void backfillTraceFromDingInstances(List<MesXslApprovalTrace> traces) {
|
||||
if (traces == null || traces.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (MesXslApprovalTrace trace : traces) {
|
||||
if (trace == null || oConvertUtils.isEmpty(trace.getExternalInstanceId())) {
|
||||
continue;
|
||||
}
|
||||
MesXslApprovalRecord dingRecord = findLatestDingRecord(trace.getBizTable(), trace.getBizDataId());
|
||||
if (dingRecord == null || oConvertUtils.isEmpty(dingRecord.getFlowId())) {
|
||||
continue;
|
||||
}
|
||||
MesXslApprovalFlow mesFlow = resolveMesFlow(dingRecord);
|
||||
if (mesFlow == null || oConvertUtils.isEmpty(mesFlow.getFlowConfig())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】台账已拒绝/终止时优先清空痕迹-----------
|
||||
if (ApprovalRecordConstants.STATUS_REJECTED.equals(dingRecord.getStatus())
|
||||
|| ApprovalRecordConstants.STATUS_CANCELLED.equals(dingRecord.getStatus())) {
|
||||
approvalTraceSyncService.revertToCompile(trace.getBizTable(), trace.getBizDataId());
|
||||
clearTraceFields(trace);
|
||||
continue;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】台账已拒绝/终止时优先清空痕迹-----------
|
||||
approvalTraceSyncService.syncFromDingInstance(
|
||||
trace.getBizTable(), trace.getBizDataId(), trace.getExternalInstanceId(), mesFlow.getFlowConfig());
|
||||
MesXslApprovalTrace latest = getByBiz(trace.getBizTable(), trace.getBizDataId());
|
||||
if (latest != null) {
|
||||
trace.setProofreadBy(latest.getProofreadBy());
|
||||
trace.setProofreadTime(latest.getProofreadTime());
|
||||
trace.setAuditBy(latest.getAuditBy());
|
||||
trace.setAuditTime(latest.getAuditTime());
|
||||
trace.setApproveBy(latest.getApproveBy());
|
||||
trace.setApproveTime(latest.getApproveTime());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批痕迹反写] 列表反写失败 table={} bizId={} instanceId={}: {}",
|
||||
trace.getBizTable(), trace.getBizDataId(), trace.getExternalInstanceId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearTraceFields(MesXslApprovalTrace trace) {
|
||||
if (trace == null) {
|
||||
return;
|
||||
}
|
||||
trace.setProofreadBy(null);
|
||||
trace.setProofreadTime(null);
|
||||
trace.setAuditBy(null);
|
||||
trace.setAuditTime(null);
|
||||
trace.setApproveBy(null);
|
||||
trace.setApproveTime(null);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】列表加载时按实例tasks反写痕迹-----------
|
||||
|
||||
@Override
|
||||
public void enrichExternalInstanceIds(List<MesXslApprovalTrace> traces) {
|
||||
if (traces == null || traces.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> bizDataIds = traces.stream()
|
||||
.map(MesXslApprovalTrace::getBizDataId)
|
||||
.filter(oConvertUtils::isNotEmpty)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
if (bizDataIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> bizTables = traces.stream()
|
||||
.map(MesXslApprovalTrace::getBizTable)
|
||||
.filter(oConvertUtils::isNotEmpty)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
if (bizTables.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<MesXslApprovalRecord> records = approvalRecordService.lambdaQuery()
|
||||
.in(MesXslApprovalRecord::getBizTable, bizTables)
|
||||
.in(MesXslApprovalRecord::getBizDataId, bizDataIds)
|
||||
.eq(MesXslApprovalRecord::getChannel, ApprovalRecordConstants.CHANNEL_DINGTALK)
|
||||
.orderByDesc(MesXslApprovalRecord::getLaunchNo)
|
||||
.orderByDesc(MesXslApprovalRecord::getApplyTime)
|
||||
.list();
|
||||
Map<String, String> instanceIdMap = new HashMap<>();
|
||||
for (MesXslApprovalRecord record : records) {
|
||||
if (record == null || oConvertUtils.isEmpty(record.getExternalInstanceId())) {
|
||||
continue;
|
||||
}
|
||||
String key = buildBizKey(record.getBizTable(), record.getBizDataId());
|
||||
instanceIdMap.putIfAbsent(key, record.getExternalInstanceId());
|
||||
}
|
||||
for (MesXslApprovalTrace trace : traces) {
|
||||
if (trace == null) {
|
||||
continue;
|
||||
}
|
||||
String key = buildBizKey(trace.getBizTable(), trace.getBizDataId());
|
||||
trace.setExternalInstanceId(instanceIdMap.get(key));
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】明细列表补充钉钉审批实例ID-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
@Override
|
||||
public DingProcessInstanceFlowVO getDingFlowRecords(String bizTable, String bizDataId, String processInstanceId) {
|
||||
String instanceId = processInstanceId;
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
MesXslApprovalRecord record = findLatestDingRecord(bizTable, bizDataId);
|
||||
if (record == null || oConvertUtils.isEmpty(record.getExternalInstanceId())) {
|
||||
throw new IllegalArgumentException("未找到绑定的钉钉审批实例,请确认该单据已通过钉钉发起审批");
|
||||
}
|
||||
instanceId = record.getExternalInstanceId();
|
||||
if (oConvertUtils.isEmpty(bizTable)) {
|
||||
bizTable = record.getBizTable();
|
||||
}
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
bizDataId = record.getBizDataId();
|
||||
}
|
||||
}
|
||||
JSONObject instance = dingTalkWorkflowService.getProcessInstance(instanceId);
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("拉取钉钉审批实例详情失败,请稍后重试");
|
||||
}
|
||||
DingProcessInstanceFlowVO vo = new DingProcessInstanceFlowVO();
|
||||
vo.setProcessInstanceId(instanceId);
|
||||
vo.setTitle(instance.getString("title"));
|
||||
vo.setStatus(instance.getString("status"));
|
||||
vo.setResult(instance.getString("result"));
|
||||
vo.setBizTable(bizTable);
|
||||
vo.setBizDataId(bizDataId);
|
||||
List<DingOperationRecordVO> operationRecords = parseOperationRecords(instance.getJSONArray("operationRecords"));
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】钉钉操作人ID映射本地用户姓名-----------
|
||||
enrichOperatorNames(operationRecords);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】钉钉操作人ID映射本地用户姓名-----------
|
||||
vo.setOperationRecords(operationRecords);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private MesXslApprovalRecord findLatestDingRecord(String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return null;
|
||||
}
|
||||
return approvalRecordService.lambdaQuery()
|
||||
.eq(MesXslApprovalRecord::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalRecord::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalRecord::getChannel, ApprovalRecordConstants.CHANNEL_DINGTALK)
|
||||
.orderByDesc(MesXslApprovalRecord::getLaunchNo)
|
||||
.orderByDesc(MesXslApprovalRecord::getApplyTime)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
private List<DingOperationRecordVO> parseOperationRecords(JSONArray records) {
|
||||
List<DingOperationRecordVO> list = new ArrayList<>();
|
||||
if (records == null || records.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
JSONObject rec = records.getJSONObject(i);
|
||||
if (rec == null) {
|
||||
continue;
|
||||
}
|
||||
DingOperationRecordVO item = new DingOperationRecordVO();
|
||||
item.setUserId(rec.getString("userId"));
|
||||
item.setDate(rec.getString("date"));
|
||||
item.setType(rec.getString("type"));
|
||||
item.setResult(rec.getString("result"));
|
||||
item.setRemark(rec.getString("remark"));
|
||||
item.setShowName(rec.getString("showName"));
|
||||
item.setActivityId(rec.getString("activityId"));
|
||||
JSONArray ccUserIds = rec.getJSONArray("ccUserIds");
|
||||
if (ccUserIds != null && !ccUserIds.isEmpty()) {
|
||||
List<String> ccList = new ArrayList<>();
|
||||
for (int j = 0; j < ccUserIds.size(); j++) {
|
||||
String cc = ccUserIds.getString(j);
|
||||
if (oConvertUtils.isNotEmpty(cc)) {
|
||||
ccList.add(cc);
|
||||
}
|
||||
}
|
||||
item.setCcUserIds(ccList);
|
||||
}
|
||||
JSONArray images = rec.getJSONArray("images");
|
||||
if (images != null && !images.isEmpty()) {
|
||||
List<String> imageList = new ArrayList<>();
|
||||
for (int j = 0; j < images.size(); j++) {
|
||||
String img = images.getString(j);
|
||||
if (oConvertUtils.isNotEmpty(img)) {
|
||||
imageList.add(img);
|
||||
}
|
||||
}
|
||||
item.setImages(imageList);
|
||||
}
|
||||
list.add(item);
|
||||
}
|
||||
list.sort(Comparator.comparing(DingOperationRecordVO::getDate, Comparator.nullsLast(String::compareTo)));
|
||||
return list;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】钉钉操作人ID映射本地用户姓名-----------
|
||||
/**
|
||||
* 将 operationRecords 中的钉钉 userId 映射为本地用户姓名。
|
||||
* 查询链:sys_user.ding_user_id → sys_third_account.third_user_id → 保留原钉钉ID。
|
||||
*/
|
||||
private void enrichOperatorNames(List<DingOperationRecordVO> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> dtUserIds = new HashSet<>();
|
||||
for (DingOperationRecordVO item : list) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(item.getUserId())) {
|
||||
dtUserIds.add(item.getUserId());
|
||||
}
|
||||
if (item.getCcUserIds() != null) {
|
||||
for (String cc : item.getCcUserIds()) {
|
||||
if (oConvertUtils.isNotEmpty(cc)) {
|
||||
dtUserIds.add(cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, String> nameMap = batchResolveDtUserDisplayNames(dtUserIds);
|
||||
for (DingOperationRecordVO item : list) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(item.getUserId())) {
|
||||
item.setUserName(nameMap.getOrDefault(item.getUserId(), item.getUserId()));
|
||||
}
|
||||
if (isUnknownShowName(item.getShowName())) {
|
||||
item.setShowName(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> batchResolveDtUserDisplayNames(Collection<String> dtUserIds) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (dtUserIds == null || dtUserIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<String> ids = dtUserIds.stream().filter(oConvertUtils::isNotEmpty).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
String inClause = ids.stream().map(id -> "?").collect(Collectors.joining(","));
|
||||
try {
|
||||
List<Map<String, Object>> localRows = jdbcTemplate.queryForList(
|
||||
"SELECT ding_user_id, realname, username FROM sys_user "
|
||||
+ "WHERE ding_user_id IN (" + inClause + ") AND (del_flag=0 OR del_flag IS NULL)",
|
||||
ids.toArray());
|
||||
for (Map<String, Object> row : localRows) {
|
||||
String dtId = stringValue(row.get("ding_user_id"));
|
||||
if (oConvertUtils.isEmpty(dtId)) {
|
||||
continue;
|
||||
}
|
||||
result.put(dtId, pickDisplayName(stringValue(row.get("realname")), stringValue(row.get("username")), dtId));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 查询失败时降级保留钉钉ID
|
||||
}
|
||||
List<String> missing = ids.stream().filter(id -> !result.containsKey(id)).collect(Collectors.toList());
|
||||
if (!missing.isEmpty()) {
|
||||
String missingIn = missing.stream().map(id -> "?").collect(Collectors.joining(","));
|
||||
try {
|
||||
List<Map<String, Object>> thirdRows = jdbcTemplate.queryForList(
|
||||
"SELECT t.third_user_id, u.realname, u.username "
|
||||
+ "FROM sys_third_account t "
|
||||
+ "JOIN sys_user u ON u.id = t.sys_user_id "
|
||||
+ "WHERE t.third_type='dingtalk' AND t.third_user_id IN (" + missingIn + ") "
|
||||
+ "AND (t.del_flag=0 OR t.del_flag IS NULL) AND (u.del_flag=0 OR u.del_flag IS NULL)",
|
||||
missing.toArray());
|
||||
for (Map<String, Object> row : thirdRows) {
|
||||
String dtId = stringValue(row.get("third_user_id"));
|
||||
if (oConvertUtils.isEmpty(dtId) || result.containsKey(dtId)) {
|
||||
continue;
|
||||
}
|
||||
result.put(dtId, pickDisplayName(stringValue(row.get("realname")), stringValue(row.get("username")), dtId));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 查询失败时降级保留钉钉ID
|
||||
}
|
||||
}
|
||||
for (String id : ids) {
|
||||
result.putIfAbsent(id, id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String pickDisplayName(String realname, String username, String fallback) {
|
||||
if (oConvertUtils.isNotEmpty(realname)) {
|
||||
return realname;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
return username;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private boolean isUnknownShowName(String showName) {
|
||||
if (oConvertUtils.isEmpty(showName)) {
|
||||
return true;
|
||||
}
|
||||
return "UNKNOWN".equalsIgnoreCase(showName.trim());
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】钉钉操作人ID映射本地用户姓名-----------
|
||||
|
||||
private String buildBizKey(String bizTable, String bizDataId) {
|
||||
return bizTable + "#" + bizDataId;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批流转记录-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
@Override
|
||||
public DingProcessForecastVO getDingProcessForecast(String bizTable, String bizDataId, String processInstanceId) {
|
||||
MesXslApprovalRecord dingRecord = null;
|
||||
String instanceId = processInstanceId;
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
dingRecord = findLatestDingRecord(bizTable, bizDataId);
|
||||
if (dingRecord == null || oConvertUtils.isEmpty(dingRecord.getExternalInstanceId())) {
|
||||
throw new IllegalArgumentException("未找到绑定的钉钉审批实例,请确认该单据已通过钉钉发起审批");
|
||||
}
|
||||
instanceId = dingRecord.getExternalInstanceId();
|
||||
} else {
|
||||
dingRecord = approvalRecordService.lambdaQuery()
|
||||
.eq(MesXslApprovalRecord::getExternalInstanceId, instanceId)
|
||||
.eq(MesXslApprovalRecord::getChannel, ApprovalRecordConstants.CHANNEL_DINGTALK)
|
||||
.orderByDesc(MesXslApprovalRecord::getLaunchNo)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
if (dingRecord == null && oConvertUtils.isNotEmpty(bizTable) && oConvertUtils.isNotEmpty(bizDataId)) {
|
||||
dingRecord = findLatestDingRecord(bizTable, bizDataId);
|
||||
}
|
||||
}
|
||||
JSONObject instance = dingTalkWorkflowService.getProcessInstance(instanceId);
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("拉取钉钉审批实例详情失败,请稍后重试");
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
MesXslApprovalFlow mesFlow = resolveMesFlow(dingRecord);
|
||||
List<DingProcessForecastNodeVO> nodes = parseInstanceTaskNodes(instance, dingRecord, mesFlow);
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
DingProcessForecastVO vo = new DingProcessForecastVO();
|
||||
vo.setProcessInstanceId(instanceId);
|
||||
vo.setProcessCode(resolveProcessCode(dingRecord));
|
||||
vo.setTemplateName(resolveTemplateName(dingRecord));
|
||||
vo.setMesFlowName(mesFlow != null ? mesFlow.getFlowName() : (dingRecord != null ? dingRecord.getFlowName() : null));
|
||||
vo.setNodeSource("审批实例tasks按activityId解析");
|
||||
vo.setNodes(nodes);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private MesXslApprovalFlow resolveMesFlow(MesXslApprovalRecord dingRecord) {
|
||||
if (dingRecord == null || oConvertUtils.isEmpty(dingRecord.getFlowId())) {
|
||||
return null;
|
||||
}
|
||||
return approvalFlowService.getById(dingRecord.getFlowId());
|
||||
}
|
||||
|
||||
private String resolveProcessCode(MesXslApprovalRecord dingRecord) {
|
||||
if (dingRecord == null || oConvertUtils.isEmpty(dingRecord.getTemplateId())) {
|
||||
return null;
|
||||
}
|
||||
MesXslDingProcessTpl tpl = dingProcessTplService.getById(dingRecord.getTemplateId());
|
||||
return tpl == null ? null : tpl.getProcessCode();
|
||||
}
|
||||
|
||||
private String resolveTemplateName(MesXslApprovalRecord dingRecord) {
|
||||
if (dingRecord == null) {
|
||||
return null;
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(dingRecord.getTemplateName())) {
|
||||
return dingRecord.getTemplateName();
|
||||
}
|
||||
if (oConvertUtils.isEmpty(dingRecord.getTemplateId())) {
|
||||
return null;
|
||||
}
|
||||
MesXslDingProcessTpl tpl = dingProcessTplService.getById(dingRecord.getTemplateId());
|
||||
return tpl == null ? null : tpl.getTplName();
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
/**
|
||||
* 从审批实例 tasks 按 activityId 分组解析实际审批节点,审批方式取自 MES 流程 multiMode。
|
||||
*/
|
||||
private List<DingProcessForecastNodeVO> parseInstanceTaskNodes(JSONObject instance,
|
||||
MesXslApprovalRecord dingRecord,
|
||||
MesXslApprovalFlow mesFlow) {
|
||||
String flowConfig = mesFlow == null ? null : mesFlow.getFlowConfig();
|
||||
if (oConvertUtils.isEmpty(flowConfig)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<NodePair> pairs = instanceStageExtractor.alignMesNodesWithTasks(instance, flowConfig);
|
||||
if (pairs.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, String> activityNameMap = buildActivityNameMap(instance);
|
||||
List<DingProcessForecastNodeVO> nodes = new ArrayList<>();
|
||||
for (NodePair pair : pairs) {
|
||||
JSONObject mesNode = pair.getMesNode();
|
||||
String activityId = pair.getActivityId();
|
||||
List<JSONObject> taskList = pair.getTaskList();
|
||||
String approvalMethod = instanceStageExtractor.resolveApprovalMethod(mesNode);
|
||||
NodeTaskDecision decision = instanceStageExtractor.evaluateNodeTasks(taskList, approvalMethod);
|
||||
String mesNodeName = mesNode == null ? null : mesNode.getString("name");
|
||||
String activityName = firstNonEmpty(activityNameMap.get(activityId), mesNodeName, "审批节点" + pair.getStepNo());
|
||||
List<String> actionerIds = decision.getActorUserIds() == null ? new ArrayList<>() : decision.getActorUserIds();
|
||||
List<String> actionerNames = instanceStageExtractor.resolveActorNames(actionerIds);
|
||||
DingProcessForecastNodeVO node = new DingProcessForecastNodeVO();
|
||||
node.setStepNo(pair.getStepNo());
|
||||
node.setActivityId(activityId);
|
||||
node.setActivityName(activityName);
|
||||
node.setMesNodeName(mesNodeName);
|
||||
node.setActivityType("target_approval");
|
||||
node.setApprovalMethod(approvalMethod);
|
||||
node.setApprovalMethodText(instanceStageExtractor.approvalMethodText(approvalMethod));
|
||||
node.setActionerUserIds(actionerIds);
|
||||
node.setActionerNames(actionerNames);
|
||||
node.setNodeStatus(decision.getNodeStatus());
|
||||
node.setNodeStatusText(decision.getNodeStatusText());
|
||||
nodes.add(node);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private Map<String, String> buildActivityNameMap(JSONObject instance) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
JSONArray records = instance.getJSONArray("operationRecords");
|
||||
if (records == null || records.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
JSONObject rec = records.getJSONObject(i);
|
||||
if (rec == null) {
|
||||
continue;
|
||||
}
|
||||
String activityId = rec.getString("activityId");
|
||||
String showName = rec.getString("showName");
|
||||
if (oConvertUtils.isNotEmpty(activityId) && oConvertUtils.isNotEmpty(showName)
|
||||
&& !isUnknownShowName(showName) && !map.containsKey(activityId)) {
|
||||
map.put(activityId, showName);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private String firstNonEmpty(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (oConvertUtils.isNotEmpty(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】审批节点改由实例tasks按activityId解析-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
@Override
|
||||
public JSONObject getDingProcessInstance(String bizTable, String bizDataId, String processInstanceId) {
|
||||
String instanceId = processInstanceId;
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
MesXslApprovalRecord record = findLatestDingRecord(bizTable, bizDataId);
|
||||
if (record == null || oConvertUtils.isEmpty(record.getExternalInstanceId())) {
|
||||
throw new IllegalArgumentException("未找到绑定的钉钉审批实例,请确认该单据已通过钉钉发起审批");
|
||||
}
|
||||
instanceId = record.getExternalInstanceId();
|
||||
}
|
||||
JSONObject raw = dingTalkWorkflowService.getProcessInstanceRaw(instanceId);
|
||||
if (raw == null) {
|
||||
throw new IllegalStateException("拉取钉钉审批实例详情失败,请稍后重试");
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260608 for:【审批注册中心】查看钉钉审批实例原始JSON-----------
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslBizDocRegistryMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 审批注册中心
|
||||
*/
|
||||
@Service
|
||||
public class MesXslBizDocRegistryServiceImpl extends ServiceImpl<MesXslBizDocRegistryMapper, MesXslBizDocRegistry>
|
||||
implements IMesXslBizDocRegistryService {
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
@Override
|
||||
public void normalizeBeforeSave(MesXslBizDocRegistry entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (entity.getEnabled() == null) {
|
||||
entity.setEnabled(1);
|
||||
}
|
||||
entity.setEnabledStages(normalizeStages(entity.getEnabledStages()));
|
||||
entity.setStatusField(defaultField(entity.getStatusField(), "status"));
|
||||
}
|
||||
|
||||
private String normalizeStages(String stages) {
|
||||
if (oConvertUtils.isEmpty(stages)) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = stages.split(",");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String part : parts) {
|
||||
if (oConvertUtils.isEmpty(part)) {
|
||||
continue;
|
||||
}
|
||||
String val = part.trim();
|
||||
if (sb.length() > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(val);
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
|
||||
private String defaultField(String value, String fallback) {
|
||||
return oConvertUtils.isEmpty(value) ? fallback : value.trim();
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】保存前规范化审批注册配置-----------
|
||||
|
||||
@Override
|
||||
public MesXslBizDocRegistry findActiveByTableName(String tableName) {
|
||||
if (oConvertUtils.isEmpty(tableName)) {
|
||||
return null;
|
||||
}
|
||||
return lambdaQuery()
|
||||
.eq(MesXslBizDocRegistry::getTableName, tableName)
|
||||
.eq(MesXslBizDocRegistry::getEnabled, 1)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationAction;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationActionMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationActionService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MesXslIntegrationActionServiceImpl extends ServiceImpl<MesXslIntegrationActionMapper, MesXslIntegrationAction>
|
||||
implements IMesXslIntegrationActionService {
|
||||
|
||||
@Override
|
||||
public List<MesXslIntegrationAction> listByPlanId(String planId) {
|
||||
return lambdaQuery()
|
||||
.eq(MesXslIntegrationAction::getPlanId, planId)
|
||||
.eq(MesXslIntegrationAction::getEnabled, 1)
|
||||
.orderByAsc(MesXslIntegrationAction::getExecOrder)
|
||||
.list();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByPlanId(String planId) {
|
||||
remove(new LambdaQueryWrapper<MesXslIntegrationAction>()
|
||||
.eq(MesXslIntegrationAction::getPlanId, planId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationLog;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationLogMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationLogService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MesXslIntegrationLogServiceImpl extends ServiceImpl<MesXslIntegrationLogMapper, MesXslIntegrationLog>
|
||||
implements IMesXslIntegrationLogService {
|
||||
|
||||
@Override
|
||||
public boolean isAlreadySuccess(String idempotentKey) {
|
||||
return lambdaQuery()
|
||||
.eq(MesXslIntegrationLog::getIdempotentKey, idempotentKey)
|
||||
.eq(MesXslIntegrationLog::getStatus, "success")
|
||||
.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<String> retry(String logId) {
|
||||
// Phase 0 先留钩子,Phase 2 完善手动重试
|
||||
return Result.error("手动重试功能将在 Phase 2 完善");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.integration.engine.ApprovalStageResolver;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslBizDocRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.mapper.MesXslIntegrationPlanMapper;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslBizDocRegistryService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class MesXslIntegrationPlanServiceImpl extends ServiceImpl<MesXslIntegrationPlanMapper, MesXslIntegrationPlan>
|
||||
implements IMesXslIntegrationPlanService {
|
||||
|
||||
@Autowired
|
||||
private IMesXslBizDocRegistryService registryService;
|
||||
|
||||
@Override
|
||||
public Result<String> publish(String planId) {
|
||||
MesXslIntegrationPlan plan = getById(planId);
|
||||
if (plan == null) {
|
||||
return Result.error("方案不存在");
|
||||
}
|
||||
if ("1".equals(plan.getStatus())) {
|
||||
return Result.error("方案已是发布状态");
|
||||
}
|
||||
Result<String> validate = normalizeAndValidate(plan);
|
||||
if (!validate.isSuccess()) {
|
||||
return validate;
|
||||
}
|
||||
boolean ok = lambdaUpdate()
|
||||
.eq(MesXslIntegrationPlan::getId, planId)
|
||||
.set(MesXslIntegrationPlan::getStatus, "1")
|
||||
.update();
|
||||
return ok ? Result.OK("发布成功") : Result.error("发布失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<String> disable(String planId) {
|
||||
MesXslIntegrationPlan plan = getById(planId);
|
||||
if (plan == null) {
|
||||
return Result.error("方案不存在");
|
||||
}
|
||||
boolean ok = lambdaUpdate()
|
||||
.eq(MesXslIntegrationPlan::getId, planId)
|
||||
.set(MesXslIntegrationPlan::getStatus, "2")
|
||||
.update();
|
||||
return ok ? Result.OK("已停用") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
@Override
|
||||
public Result<String> normalizeAndValidate(MesXslIntegrationPlan plan) {
|
||||
if (plan == null) {
|
||||
return Result.error("方案不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(plan.getSourceTable())) {
|
||||
return Result.error("请选择触发业务表");
|
||||
}
|
||||
MesXslBizDocRegistry registry = registryService.findActiveByTableName(plan.getSourceTable());
|
||||
if (registry == null) {
|
||||
return Result.error("源单表未在审批注册中心启用,请先在审批注册中心配置");
|
||||
}
|
||||
plan.setRegistryId(registry.getId());
|
||||
|
||||
Set<String> enabledStages = ApprovalStageResolver.parseEnabledStages(registry.getEnabledStages());
|
||||
if (enabledStages.isEmpty()) {
|
||||
return Result.error("审批注册中心未配置启用环节,无法绑定集成方案");
|
||||
}
|
||||
|
||||
String phase = plan.getTriggerPhase();
|
||||
if ("onApprove".equals(phase)) {
|
||||
if (oConvertUtils.isEmpty(plan.getTriggerStage())) {
|
||||
plan.setTriggerStage(ApprovalStageResolver.STAGE_APPROVE);
|
||||
}
|
||||
if (!enabledStages.contains(plan.getTriggerStage())) {
|
||||
return Result.error("绑定环节「" + ApprovalStageResolver.stageLabel(plan.getTriggerStage())
|
||||
+ "」未在审批注册中心启用");
|
||||
}
|
||||
} else if ("onNodeApprove".equals(phase)) {
|
||||
if (oConvertUtils.isEmpty(plan.getTriggerStage())) {
|
||||
return Result.error("节点通过时必须选择绑定的审批环节");
|
||||
}
|
||||
if (!enabledStages.contains(plan.getTriggerStage())) {
|
||||
return Result.error("绑定环节「" + ApprovalStageResolver.stageLabel(plan.getTriggerStage())
|
||||
+ "」未在审批注册中心启用");
|
||||
}
|
||||
} else if ("onReject".equals(phase)) {
|
||||
if (oConvertUtils.isNotEmpty(plan.getTriggerStage()) && !enabledStages.contains(plan.getTriggerStage())) {
|
||||
return Result.error("绑定环节「" + ApprovalStageResolver.stageLabel(plan.getTriggerStage())
|
||||
+ "」未在审批注册中心启用");
|
||||
}
|
||||
}
|
||||
return Result.OK("校验通过");
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】集成方案绑定审批注册中心环节-----------
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批实例操作记录(时间轴展示)
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "钉钉审批操作记录")
|
||||
public class DingOperationRecordVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "操作人钉钉userId")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "操作人姓名(本地用户映射)")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "操作时间(ISO8601)")
|
||||
private String date;
|
||||
|
||||
@Schema(description = "操作类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "操作结果")
|
||||
private String result;
|
||||
|
||||
@Schema(description = "备注/意见")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "节点展示名称")
|
||||
private String showName;
|
||||
|
||||
@Schema(description = "节点活动ID")
|
||||
private String activityId;
|
||||
|
||||
@Schema(description = "抄送人列表")
|
||||
private List<String> ccUserIds;
|
||||
|
||||
@Schema(description = "图片URL列表")
|
||||
private List<String> images;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉 processForecast 预测审批节点
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "钉钉预测审批节点")
|
||||
public class DingProcessForecastNodeVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "节点序号(从1开始)")
|
||||
private Integer stepNo;
|
||||
|
||||
@Schema(description = "节点活动ID")
|
||||
private String activityId;
|
||||
|
||||
@Schema(description = "节点名称")
|
||||
private String activityName;
|
||||
|
||||
@Schema(description = "MES审批流节点名称")
|
||||
private String mesNodeName;
|
||||
|
||||
@Schema(description = "节点类型(如target_select/target_approval)")
|
||||
private String activityType;
|
||||
|
||||
@Schema(description = "审批方式 NONE/AND/OR/ONE_BY_ONE")
|
||||
private String approvalMethod;
|
||||
|
||||
@Schema(description = "审批方式中文")
|
||||
private String approvalMethodText;
|
||||
|
||||
@Schema(description = "审批人钉钉userId列表")
|
||||
private List<String> actionerUserIds;
|
||||
|
||||
@Schema(description = "审批人姓名列表(本地映射)")
|
||||
private List<String> actionerNames;
|
||||
|
||||
@Schema(description = "节点状态(聚合tasks)")
|
||||
private String nodeStatus;
|
||||
|
||||
@Schema(description = "节点状态中文")
|
||||
private String nodeStatusText;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批实例 tasks 解析的审批节点结果
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "钉钉审批节点(实例tasks解析)")
|
||||
public class DingProcessForecastVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "钉钉审批实例ID")
|
||||
private String processInstanceId;
|
||||
|
||||
@Schema(description = "钉钉processCode")
|
||||
private String processCode;
|
||||
|
||||
@Schema(description = "钉钉模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "MES审批流名称")
|
||||
private String mesFlowName;
|
||||
|
||||
@Schema(description = "节点来源说明")
|
||||
private String nodeSource;
|
||||
|
||||
@Schema(description = "审批节点列表")
|
||||
private List<DingProcessForecastNodeVO> nodes;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.xslmes.approval.integration.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批实例流转详情(供前端时间轴展示)
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "钉钉审批实例流转详情")
|
||||
public class DingProcessInstanceFlowVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "钉钉审批实例ID")
|
||||
private String processInstanceId;
|
||||
|
||||
@Schema(description = "审批标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "实例状态 RUNNING/COMPLETED/TERMINATED 等")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "审批结果 agree/refuse 等")
|
||||
private String result;
|
||||
|
||||
@Schema(description = "业务单据ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "业务表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "操作记录列表(时间轴)")
|
||||
private List<DingOperationRecordVO> operationRecords;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
|
||||
/**
|
||||
* MES 审批台账 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface MesXslApprovalRecordMapper extends BaseMapper<MesXslApprovalRecord> {
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批门禁服务:统一判断能否发起审批,并维护审批台账
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface IMesXslApprovalGateService {
|
||||
|
||||
/**
|
||||
* 检查是否允许对指定业务单据发起审批(跨 MES/钉钉通道)
|
||||
*/
|
||||
ApprovalGateVo checkCanLaunch(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 批量检查是否允许发起审批
|
||||
*/
|
||||
List<ApprovalGateVo> checkCanLaunchBatch(String bizTable, List<String> bizDataIds, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 校验允许发起,不允许则抛出 IllegalStateException(供 Controller 转 Result.error)
|
||||
*/
|
||||
void assertCanLaunch(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 发起审批时写入台账(状态=流转中)
|
||||
*/
|
||||
MesXslApprovalRecord createRunningRecord(MesXslApprovalRecord draft);
|
||||
|
||||
/**
|
||||
* MES 审批实例办结时同步台账
|
||||
*/
|
||||
void finishByMesInstance(String mesInstanceId, String status, String remark);
|
||||
|
||||
/**
|
||||
* 按外部实例 ID 办结台账(预留钉钉回调等场景)。
|
||||
* 返回 true 表示本次调用实际更新了台账(状态从流转中变为终态);
|
||||
* 返回 false 表示台账已是终态,本次为重复调用。
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回是否实际更新,供调用方判断是否跳过后续业务回调-----
|
||||
boolean finishByExternalInstance(String channel, String externalInstanceId, String status, String remark);
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回是否实际更新,供调用方判断是否跳过后续业务回调-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁标记节点已处理-----
|
||||
/**
|
||||
* 用乐观锁尝试将台账的 processed_op_count 推进到 nodeIndex+1。
|
||||
* 返回 true 表示成功(本次是第一个处理该节点的线程);
|
||||
* 返回 false 表示已被其他线程/事件处理过,调用方应跳过。
|
||||
*/
|
||||
boolean tryMarkNodeProcessed(String recordId, int nodeIndex);
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁标记节点已处理-----
|
||||
|
||||
/**
|
||||
* 查询业务单据审批历史(按发起时间倒序)
|
||||
*/
|
||||
List<MesXslApprovalRecord> listHistory(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 构建 MES 通道台账草稿
|
||||
*/
|
||||
MesXslApprovalRecord buildMesDraft(String bizTable, String bizTableName, String bizDataId, String bizTitle,
|
||||
String flowId, String flowName, String mesInstanceId, LoginUser loginUser,
|
||||
Integer tenantId);
|
||||
|
||||
/**
|
||||
* 构建钉钉通道台账草稿
|
||||
*/
|
||||
MesXslApprovalRecord buildDingDraft(String bizTable, String bizTableName, String bizCode, String bizDataId,
|
||||
String bizTitle, String flowId, String flowName, String templateId,
|
||||
String templateName, String dingInstanceId, LoginUser loginUser,
|
||||
Integer tenantId);
|
||||
}
|
||||
@@ -57,6 +57,14 @@ public interface IMesXslApprovalHandleService {
|
||||
Result<String> urge(String instanceId, LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片-----------
|
||||
/**
|
||||
* 补发当前节点审批卡片(用于历史数据未收到 IM 消息等场景)。
|
||||
* instanceId 与 (bizTable+bizDataId) 二选一;仅审批中且 MES 通道实例可补发。
|
||||
*/
|
||||
Result<String> resendCard(String instanceId, String bizTable, String bizDataId, LoginUser user);
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片-----------
|
||||
|
||||
//update-begin---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.MesXslApprovalRecord;
|
||||
|
||||
/**
|
||||
* MES 审批台账 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface IMesXslApprovalRecordService extends IService<MesXslApprovalRecord> {
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.constant.ApprovalRecordConstants;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批门禁服务实现
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MesXslApprovalGateServiceImpl implements IMesXslApprovalGateService {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService recordService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public ApprovalGateVo checkCanLaunch(String bizTable, String bizDataId, Integer tenantId) {
|
||||
ApprovalGateVo vo = new ApprovalGateVo()
|
||||
.setBizTable(bizTable)
|
||||
.setBizDataId(bizDataId);
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return vo.setAllowed(false).setReason("业务单据信息不完整,无法判断审批状态");
|
||||
}
|
||||
MesXslApprovalRecord latest = findLatest(bizTable, bizDataId, tenantId);
|
||||
if (latest == null) {
|
||||
return vo.setAllowed(true).setReason("尚未发起过审批,允许发起");
|
||||
}
|
||||
fillLatestInfo(vo, latest);
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(latest.getStatus())) {
|
||||
String channelText = channelText(latest.getChannel());
|
||||
return vo.setAllowed(false).setReason("该单据已有" + channelText + "流程流转中,请勿重复发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_APPROVED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(false).setReason("该单据已审批通过,无需重复发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_REJECTED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次审批已被拒绝,允许重新发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_CANCELLED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次审批已撤销,允许重新发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_LAUNCH_FAILED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次发起失败,允许重新发起");
|
||||
}
|
||||
return vo.setAllowed(true).setReason("允许发起审批");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApprovalGateVo> checkCanLaunchBatch(String bizTable, List<String> bizDataIds, Integer tenantId) {
|
||||
List<ApprovalGateVo> list = new ArrayList<>();
|
||||
if (bizDataIds == null || bizDataIds.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
for (String bizDataId : bizDataIds) {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
list.add(checkCanLaunch(bizTable, bizDataId, tenantId));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertCanLaunch(String bizTable, String bizDataId, Integer tenantId) {
|
||||
ApprovalGateVo gate = checkCanLaunch(bizTable, bizDataId, tenantId);
|
||||
if (gate.getAllowed() == null || !gate.getAllowed()) {
|
||||
throw new IllegalStateException(oConvertUtils.getString(gate.getReason(), "当前不允许发起审批"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MesXslApprovalRecord createRunningRecord(MesXslApprovalRecord draft) {
|
||||
if (draft == null) {
|
||||
throw new IllegalArgumentException("台账数据不能为空");
|
||||
}
|
||||
assertCanLaunch(draft.getBizTable(), draft.getBizDataId(), draft.getTenantId());
|
||||
Date now = new Date();
|
||||
if (draft.getApplyTime() == null) {
|
||||
draft.setApplyTime(now);
|
||||
}
|
||||
draft.setStatus(ApprovalRecordConstants.STATUS_RUNNING);
|
||||
draft.setLaunchNo(nextLaunchNo(draft.getBizTable(), draft.getBizDataId(), draft.getTenantId()));
|
||||
recordService.save(draft);
|
||||
return draft;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void finishByMesInstance(String mesInstanceId, String status, String remark) {
|
||||
finishByExternalInstance(ApprovalRecordConstants.CHANNEL_MES, mesInstanceId, status, remark);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回实际影响行数,0表示台账已是终态-----
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean finishByExternalInstance(String channel, String externalInstanceId, String status, String remark) {
|
||||
if (oConvertUtils.isEmpty(externalInstanceId) || oConvertUtils.isEmpty(status)) {
|
||||
return false;
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(status)) {
|
||||
return false;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslApprovalRecord> uw = new LambdaUpdateWrapper<MesXslApprovalRecord>()
|
||||
.eq(MesXslApprovalRecord::getExternalInstanceId, externalInstanceId)
|
||||
.eq(MesXslApprovalRecord::getStatus, ApprovalRecordConstants.STATUS_RUNNING);
|
||||
if (oConvertUtils.isNotEmpty(channel)) {
|
||||
uw.eq(MesXslApprovalRecord::getChannel, channel);
|
||||
}
|
||||
uw.set(MesXslApprovalRecord::getStatus, status)
|
||||
.set(MesXslApprovalRecord::getFinishTime, new Date());
|
||||
if (oConvertUtils.isNotEmpty(remark)) {
|
||||
uw.set(MesXslApprovalRecord::getRemark, remark);
|
||||
}
|
||||
// getBaseMapper().update() 返回实际影响行数,0 = 台账已是终态(重复事件)
|
||||
int affected = recordService.getBaseMapper().update(null, uw);
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryMarkNodeProcessed(String recordId, int nodeIndex) {
|
||||
if (oConvertUtils.isEmpty(recordId)) return false;
|
||||
// 乐观锁:processed_op_count < nodeIndex+1 时才推进,保证同一节点只被处理一次
|
||||
// 两个并发线程同时到达时,MySQL 行锁保证只有一个 UPDATE 成功
|
||||
int affected = jdbcTemplate.update(
|
||||
"UPDATE mes_xsl_approval_record SET processed_op_count = ? " +
|
||||
"WHERE id = ? AND processed_op_count < ? AND del_flag = 0",
|
||||
nodeIndex + 1, recordId, nodeIndex + 1);
|
||||
return affected > 0;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回实际影响行数,0表示台账已是终态-----
|
||||
|
||||
@Override
|
||||
public List<MesXslApprovalRecord> listHistory(String bizTable, String bizDataId, Integer tenantId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return List.of();
|
||||
}
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
qw.orderByDesc(MesXslApprovalRecord::getApplyTime).orderByDesc(MesXslApprovalRecord::getCreateTime);
|
||||
return recordService.list(qw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MesXslApprovalRecord buildMesDraft(String bizTable, String bizTableName, String bizDataId, String bizTitle,
|
||||
String flowId, String flowName, String mesInstanceId, LoginUser loginUser,
|
||||
Integer tenantId) {
|
||||
MesXslApprovalRecord record = new MesXslApprovalRecord();
|
||||
record.setBizTable(bizTable);
|
||||
record.setBizTableName(bizTableName);
|
||||
record.setBizDataId(bizDataId);
|
||||
record.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
record.setChannel(ApprovalRecordConstants.CHANNEL_MES);
|
||||
record.setExternalInstanceId(mesInstanceId);
|
||||
record.setFlowId(flowId);
|
||||
record.setFlowName(flowName);
|
||||
record.setTenantId(tenantId);
|
||||
fillApplyUser(record, loginUser);
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MesXslApprovalRecord buildDingDraft(String bizTable, String bizTableName, String bizCode, String bizDataId,
|
||||
String bizTitle, String flowId, String flowName, String templateId,
|
||||
String templateName, String dingInstanceId, LoginUser loginUser,
|
||||
Integer tenantId) {
|
||||
MesXslApprovalRecord record = new MesXslApprovalRecord();
|
||||
record.setBizTable(bizTable);
|
||||
record.setBizTableName(bizTableName);
|
||||
record.setBizCode(bizCode);
|
||||
record.setBizDataId(bizDataId);
|
||||
record.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
record.setChannel(ApprovalRecordConstants.CHANNEL_DINGTALK);
|
||||
record.setExternalInstanceId(dingInstanceId);
|
||||
record.setFlowId(flowId);
|
||||
record.setFlowName(flowName);
|
||||
record.setTemplateId(templateId);
|
||||
record.setTemplateName(templateName);
|
||||
record.setTenantId(tenantId);
|
||||
fillApplyUser(record, loginUser);
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】快照业务状态原值,复用 isBizAtOriginStatus 逻辑-----
|
||||
snapshotOriginStatus(record, bizTable, bizDataId);
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】快照业务状态原值,复用 isBizAtOriginStatus 逻辑-----
|
||||
return record;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】发起时快照业务状态,驳回时供共用判断-----
|
||||
/**
|
||||
* 探测业务表是否存在 status 字段,若存在则读取当前值快照到台账。
|
||||
* 与 MesXslApprovalHandleServiceImpl.snapshotBizStatus() 逻辑对齐,
|
||||
* 使驳回时的 isBizAtOriginStatus 判断可跨 MES/钉钉两通道复用。
|
||||
*/
|
||||
private void snapshotOriginStatus(MesXslApprovalRecord record, String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) return;
|
||||
if (!bizTable.matches("^[A-Za-z0-9_]+$")) return;
|
||||
try {
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM information_schema.columns " +
|
||||
"WHERE table_schema=(SELECT DATABASE()) AND table_name=? AND column_name='status'",
|
||||
Integer.class, bizTable);
|
||||
if (cnt == null || cnt == 0) return;
|
||||
java.util.List<String> vals = jdbcTemplate.queryForList(
|
||||
"SELECT status FROM " + bizTable + " WHERE id=? LIMIT 1", String.class, bizDataId);
|
||||
if (!vals.isEmpty()) {
|
||||
record.setStatusField("status");
|
||||
record.setOriginStatus(vals.get(0));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[ApprovalGate] 快照业务状态失败 table={} id={}: {}", bizTable, bizDataId, e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】发起时快照业务状态,驳回时供共用判断-----
|
||||
|
||||
private MesXslApprovalRecord findLatest(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
qw.orderByDesc(MesXslApprovalRecord::getApplyTime).orderByDesc(MesXslApprovalRecord::getCreateTime).last("LIMIT 1");
|
||||
return recordService.getOne(qw, false);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<MesXslApprovalRecord> baseBizQuery(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = new LambdaQueryWrapper<>();
|
||||
qw.eq(MesXslApprovalRecord::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalRecord::getBizDataId, bizDataId);
|
||||
if (tenantId != null) {
|
||||
qw.eq(MesXslApprovalRecord::getTenantId, tenantId);
|
||||
}
|
||||
return qw;
|
||||
}
|
||||
|
||||
private int nextLaunchNo(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
long count = recordService.count(qw);
|
||||
return (int) count + 1;
|
||||
}
|
||||
|
||||
private void fillLatestInfo(ApprovalGateVo vo, MesXslApprovalRecord latest) {
|
||||
vo.setLatestRecordId(latest.getId())
|
||||
.setLatestStatus(latest.getStatus())
|
||||
.setLatestChannel(latest.getChannel())
|
||||
.setLatestChannelText(channelText(latest.getChannel()))
|
||||
.setLatestStatusText(statusText(latest.getStatus()));
|
||||
}
|
||||
|
||||
private void fillApplyUser(MesXslApprovalRecord record, LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return;
|
||||
}
|
||||
record.setApplyUser(loginUser.getUsername());
|
||||
record.setApplyUserName(loginUser.getRealname());
|
||||
record.setSysOrgCode(loginUser.getOrgCode());
|
||||
}
|
||||
|
||||
private String channelText(String channel) {
|
||||
if (ApprovalRecordConstants.CHANNEL_DINGTALK.equals(channel)) {
|
||||
return "钉钉审批";
|
||||
}
|
||||
if (ApprovalRecordConstants.CHANNEL_MES.equals(channel)) {
|
||||
return "MES审批";
|
||||
}
|
||||
return oConvertUtils.getString(channel, "审批");
|
||||
}
|
||||
|
||||
private String statusText(String status) {
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(status)) {
|
||||
return "流转中";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_APPROVED.equals(status)) {
|
||||
return "审批通过";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_REJECTED.equals(status)) {
|
||||
return "审批拒绝";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_CANCELLED.equals(status)) {
|
||||
return "已撤销";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_LAUNCH_FAILED.equals(status)) {
|
||||
return "发起失败";
|
||||
}
|
||||
return oConvertUtils.getString(status, "");
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,12 @@ import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackDispatcher;
|
||||
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.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.integration.entity.MesXslIntegrationPlan;
|
||||
import org.jeecg.modules.xslmes.approval.integration.service.IMesXslIntegrationPlanService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.DingTplImCardBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -68,6 +72,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
@Autowired
|
||||
private IMesXslApprovalInstanceService instanceService;
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@@ -87,6 +96,16 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
// 驳回统一回退:按业务表自动发现 @ApprovalBizAction(onReject) 动作,无需在每个流程节点配置
|
||||
@Autowired
|
||||
private ApprovalBizActionRegistry bizActionRegistry;
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】驳回回退改由集成方案 onReject 驱动-----------
|
||||
@Autowired
|
||||
private IMesXslIntegrationPlanService integrationPlanService;
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】驳回回退改由集成方案 onReject 驱动-----------
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】IM卡片字段与钉钉模板绑定对齐-----------
|
||||
@Autowired
|
||||
private DingTplImCardBuilder dingTplImCardBuilder;
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】IM卡片字段与钉钉模板绑定对齐-----------
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调-----
|
||||
|
||||
// ==================== 发起后进入首节点 ====================
|
||||
@@ -121,6 +140,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentNodeName("无审批节点");
|
||||
inst.setCurrentHandlersText("无审批节点,自动通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, "无审批节点,自动通过");
|
||||
// 无审批节点直接最终通过 -> 回调业务
|
||||
callbackDispatcher.fireApproved(buildContext(inst, inst.getCurrentNodeId(), "无审批节点", applyUser, "无审批节点,自动通过"));
|
||||
notifyApplicant(inst, applyUser == null ? null : applyUser.getUsername(),
|
||||
@@ -201,14 +221,17 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
// 节点完成 -> 先回调业务(节点通过,中间态),再推进
|
||||
inst.setNodeProgress(progress.toJSONString());
|
||||
callbackDispatcher.fireNodeApproved(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, comment));
|
||||
if (root != null) {
|
||||
actionHttpExecutor.run(findNodeById(root, progress.getString("nodeId")), "onNodeApprove", inst);
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
// if (root != null) {
|
||||
// actionHttpExecutor.run(findNodeById(root, progress.getString("nodeId")), "onNodeApprove", inst);
|
||||
// }
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
if (flow == null || root == null) {
|
||||
inst.setStatus("1");
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, comment);
|
||||
// 无后续流程,直接最终通过
|
||||
callbackDispatcher.fireApproved(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, comment));
|
||||
return Result.OK("审批通过");
|
||||
@@ -258,6 +281,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlersText("已驳回");
|
||||
inst.setNodeProgress(progress.toJSONString());
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, reason);
|
||||
|
||||
// 驳回 -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason));
|
||||
@@ -324,6 +348,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("已撤销");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, comment);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口-----
|
||||
// 撤销时单据仍处于发起前最初状态,只需把状态字段恢复到发起时快照(幂等),
|
||||
@@ -412,7 +437,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentNodeName(nodeName);
|
||||
// 该节点自动通过(中间态) -> 回调业务
|
||||
callbackDispatcher.fireNodeApproved(buildContext(inst, nodeId, nodeName, null, "审批人为空,自动通过"));
|
||||
actionHttpExecutor.run(node, "onNodeApprove", inst);
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
// actionHttpExecutor.run(node, "onNodeApprove", inst);
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
advanceAfter(inst, flow, root, nodeId, null);
|
||||
return;
|
||||
}
|
||||
@@ -423,6 +450,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批人为空,流程终止");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, "审批人为空,流程终止");
|
||||
// 流程终止(等同驳回) -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, nodeId, nodeName, null, "审批人为空,流程终止"));
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)-----
|
||||
@@ -516,9 +544,12 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, null);
|
||||
// 流程最终通过 -> 回调业务(终态)
|
||||
callbackDispatcher.fireApproved(buildContext(inst, currentNodeId, inst.getCurrentNodeName(), actingUser, null));
|
||||
actionHttpExecutor.run(findNodeById(root, currentNodeId), "onApprove", inst);
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
// actionHttpExecutor.run(findNodeById(root, currentNodeId), "onApprove", inst);
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】停用节点 HTTP 回调,改由集成方案编排-----------
|
||||
String actor = actingUser == null ? null : actingUser.getUsername();
|
||||
notifyApplicant(inst, actor, "您发起的「" + safeTitle(inst) + "」审批已全部通过。");
|
||||
}
|
||||
@@ -666,11 +697,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()), inst.getFlowName(), actionLabel);
|
||||
msgType = "text";
|
||||
}
|
||||
SysUser applicant = getUserSafely(inst.getApplyUser());
|
||||
String fromId = applicant == null ? null : applicant.getId();
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】待办卡片由系统账号发给处理人,支持发起人=处理人-----------
|
||||
for (String uname : handlerUsernames) {
|
||||
sendOne(fromId, uname, inst.getTenantId(), content, msgType);
|
||||
sendApprovalHandlerNotify(uname, inst.getTenantId(), content, msgType);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】待办卡片由系统账号发给处理人,支持发起人=处理人-----------
|
||||
}
|
||||
|
||||
/** 抄送通知(无办理按钮) */
|
||||
@@ -730,6 +761,24 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】审批待办IM通知-----------
|
||||
private void sendApprovalHandlerNotify(String toUsername, Integer tenantId, String content, String msgType) {
|
||||
String uname = toUsername == null ? "" : toUsername.trim();
|
||||
if (oConvertUtils.isEmpty(uname)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SysUser to = sysUserService.getUserByName(uname);
|
||||
if (to == null) {
|
||||
return;
|
||||
}
|
||||
sysImChatService.sendApprovalHandlerMessage(to.getId(), tenantId, content, msgType);
|
||||
} catch (Exception e) {
|
||||
log.warn("发送审批待办IM消息失败 to={}", uname, e);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】审批待办IM通知-----------
|
||||
|
||||
/** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */
|
||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片-----
|
||||
@@ -741,6 +790,15 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
*/
|
||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath, boolean approvalCard) {
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片-----
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】优先按钉钉模板绑定构建卡片字段-----------
|
||||
MesXslApprovalFlow flow = flowService.getById(inst.getFlowId());
|
||||
JSONObject dingPayload = dingTplImCardBuilder.buildCardPayload(
|
||||
inst, flow, actionLabel, canApprove, routePath, approvalCard);
|
||||
if (dingPayload != null) {
|
||||
return dingPayload.toJSONString();
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】优先按钉钉模板绑定构建卡片字段-----------
|
||||
|
||||
JSONArray fields = new JSONArray();
|
||||
addField(fields, "审批流", inst.getFlowName());
|
||||
addField(fields, "单据", safeTitle(inst));
|
||||
@@ -834,6 +892,12 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】单据仍在发起前状态时驳回:跳过业务「拒绝」接口,避免「无需拒绝」报错-----
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】已配置集成 onReject 方案时跳过 HTTP 业务拒绝-----------
|
||||
if (hasPublishedIntegrationPlan(inst.getBizTable(), "onReject")) {
|
||||
log.debug("[审批驳回] 表 {} 已配置集成方案 onReject,跳过 HTTP 业务拒绝接口", inst.getBizTable());
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】已配置集成 onReject 方案时跳过 HTTP 业务拒绝-----------
|
||||
List<ApprovalBizActionVo> rejectActions = bizActionRegistry.getByTableAndPhase(inst.getBizTable(), "onReject");
|
||||
if (rejectActions != null && !rejectActions.isEmpty()) {
|
||||
for (ApprovalBizActionVo action : rejectActions) {
|
||||
@@ -859,6 +923,20 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】判断业务单据是否仍处于发起前(最初)状态-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】判断业务表是否已发布集成方案-----------
|
||||
/** 业务表是否已配置并发布指定触发时机的集成方案 */
|
||||
private boolean hasPublishedIntegrationPlan(String bizTable, String triggerPhase) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(triggerPhase)) {
|
||||
return false;
|
||||
}
|
||||
return integrationPlanService.lambdaQuery()
|
||||
.eq(MesXslIntegrationPlan::getSourceTable, bizTable)
|
||||
.eq(MesXslIntegrationPlan::getTriggerPhase, triggerPhase)
|
||||
.eq(MesXslIntegrationPlan::getStatus, "1")
|
||||
.count() > 0;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】判断业务表是否已发布集成方案-----------
|
||||
|
||||
/**
|
||||
* 驳回/撤销/终止时,将业务单据状态字段直接回写为发起时原值。
|
||||
* 直接 UPDATE,不触发业务级联同步,确保单据回到“可重新提交”的初始态。
|
||||
@@ -1308,10 +1386,26 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
if (user != null) {
|
||||
ctx.setOperatorUsername(user.getUsername());
|
||||
ctx.setOperatorName(oConvertUtils.getString(user.getRealname(), user.getUsername()));
|
||||
ctx.setOperatorTime(new Date());
|
||||
} else {
|
||||
ctx.setOperatorUsername("system");
|
||||
ctx.setOperatorName("系统");
|
||||
}
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】MES通道补齐stageKey,与钉钉回调共用集成方案-----------
|
||||
if (oConvertUtils.isNotEmpty(nodeId) && oConvertUtils.isNotEmpty(inst.getFlowId())) {
|
||||
MesXslApprovalFlow flow = flowService.getById(inst.getFlowId());
|
||||
if (flow != null && oConvertUtils.isNotEmpty(flow.getFlowConfig())) {
|
||||
JSONObject root = safeParse(flow.getFlowConfig());
|
||||
JSONObject node = root == null ? null : findNodeById(root, nodeId);
|
||||
if (node != null) {
|
||||
JSONObject props = node.getJSONObject("props");
|
||||
if (props != null && props.containsKey("stageKey")) {
|
||||
ctx.setStageKey(props.getString("stageKey"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】MES通道补齐stageKey,与钉钉回调共用集成方案-----------
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -1410,7 +1504,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
if (oConvertUtils.isEmpty(uname)) {
|
||||
continue;
|
||||
}
|
||||
sendOne(user.getId(), uname, inst.getTenantId(),
|
||||
sendApprovalHandlerNotify(uname, inst.getTenantId(),
|
||||
"【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text");
|
||||
}
|
||||
urgeTimeMap.put(instanceId, System.currentTimeMillis());
|
||||
@@ -1418,6 +1512,77 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片-----------
|
||||
@Override
|
||||
public Result<String> resendCard(String instanceId, String bizTable, String bizDataId, LoginUser user) {
|
||||
if (user == null) {
|
||||
return Result.error("请先登录");
|
||||
}
|
||||
MesXslApprovalInstance inst = resolveRunningInstance(instanceId, bizTable, bizDataId);
|
||||
if (inst == null) {
|
||||
return Result.error("未找到审批中的 MES 审批实例");
|
||||
}
|
||||
if (!"0".equals(inst.getStatus())) {
|
||||
return Result.error("该审批已结束,无法补发卡片");
|
||||
}
|
||||
if (!canResendCard(user, inst)) {
|
||||
return Result.error("仅发起人或当前处理人可以补发审批卡片");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) {
|
||||
return Result.error("当前无待处理人,无法补发");
|
||||
}
|
||||
MesXslApprovalFlow flow = flowService.getById(inst.getFlowId());
|
||||
if (flow == null) {
|
||||
return Result.error("审批流不存在");
|
||||
}
|
||||
List<String> handlers = new ArrayList<>();
|
||||
for (String uname : inst.getCurrentHandlers().split(",")) {
|
||||
if (oConvertUtils.isNotEmpty(uname)) {
|
||||
handlers.add(uname.trim());
|
||||
}
|
||||
}
|
||||
if (handlers.isEmpty()) {
|
||||
return Result.error("当前无待处理人,无法补发");
|
||||
}
|
||||
String actionLabel = oConvertUtils.getString(inst.getCurrentNodeName(), "审批");
|
||||
sendApprovalCard(inst, flow, actionLabel, handlers);
|
||||
return Result.OK("已向 " + handlers.size() + " 位处理人补发审批卡片,请在 IM 中查看与 admin 的会话");
|
||||
}
|
||||
|
||||
private MesXslApprovalInstance resolveRunningInstance(String instanceId, String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isNotEmpty(instanceId)) {
|
||||
MesXslApprovalInstance inst = instanceService.getById(instanceId);
|
||||
return inst == null || !"0".equals(inst.getStatus()) ? null : inst;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)
|
||||
|| !IDENTIFIER.matcher(bizTable).matches()) {
|
||||
return null;
|
||||
}
|
||||
return instanceService.lambdaQuery()
|
||||
.eq(MesXslApprovalInstance::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0")
|
||||
.orderByDesc(MesXslApprovalInstance::getCreateTime)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
private boolean canResendCard(LoginUser user, MesXslApprovalInstance inst) {
|
||||
if (user.getUsername().equals(inst.getApplyUser())) {
|
||||
return true;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) {
|
||||
return false;
|
||||
}
|
||||
for (String uname : inst.getCurrentHandlers().split(",")) {
|
||||
if (user.getUsername().equals(uname == null ? "" : uname.trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//update-end---author:GHT ---date:20260610 for:【IM审批通用化】流转中实例补发IM审批卡片-----------
|
||||
|
||||
// ==================== 待办列表 ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例-----
|
||||
@@ -1571,4 +1736,13 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】全链路通知补全-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
private void syncApprovalRecord(MesXslApprovalInstance inst, String remark) {
|
||||
if (inst == null || oConvertUtils.isEmpty(inst.getId()) || "0".equals(inst.getStatus())) {
|
||||
return;
|
||||
}
|
||||
approvalGateService.finishByMesInstance(inst.getId(), inst.getStatus(), remark);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalRecordMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批台账 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalRecordServiceImpl extends ServiceImpl<MesXslApprovalRecordMapper, MesXslApprovalRecord>
|
||||
implements IMesXslApprovalRecordService {
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.xslmes.approval.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批门禁检查结果
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "审批门禁检查结果")
|
||||
public class ApprovalGateVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "是否允许发起审批")
|
||||
private Boolean allowed;
|
||||
|
||||
@Schema(description = "不允许时的原因说明")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "最新台账记录ID")
|
||||
private String latestRecordId;
|
||||
|
||||
@Schema(description = "最新台账状态")
|
||||
private String latestStatus;
|
||||
|
||||
@Schema(description = "最新台账通道 MES/DINGTALK")
|
||||
private String latestChannel;
|
||||
|
||||
@Schema(description = "最新台账通道展示名")
|
||||
private String latestChannelText;
|
||||
|
||||
@Schema(description = "最新台账状态展示名")
|
||||
private String latestStatusText;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslEquipPartMapping;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslEquipPartMappingService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* MES 设备对应部位(只读列表,数据由设备点检配置保存后自动生成)
|
||||
*/
|
||||
@Tag(name = "MES设备对应部位")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslEquipPartMapping")
|
||||
@Slf4j
|
||||
public class MesXslEquipPartMappingController
|
||||
extends JeecgController<MesXslEquipPartMapping, IMesXslEquipPartMappingService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslEquipPartMappingService mesXslEquipPartMappingService;
|
||||
|
||||
@Operation(summary = "MES设备对应部位-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslEquipPartMapping>> queryPageList(
|
||||
MesXslEquipPartMapping model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslEquipPartMapping> queryWrapper = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
//update-begin---author:jiangxh ---date:20250603 for:【MES】设备对应部位列表按设备名称、机台代号、大部位代码排序-----------
|
||||
queryWrapper.orderByAsc("equipment_name", "machine_code", "part_code", "sub_part_code");
|
||||
//update-end---author:jiangxh ---date:20250603 for:【MES】设备对应部位列表按设备名称、机台代号、大部位代码排序-----------
|
||||
Page<MesXslEquipPartMapping> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslEquipPartMapping> pageList = mesXslEquipPartMappingService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@RequiresPermissions("mes:mes_xsl_equip_part_mapping:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslEquipPartMapping model) {
|
||||
return super.exportXls(request, model, MesXslEquipPartMapping.class, "MES设备对应部位");
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,9 @@ public class MesXslEquipmentLedgerController extends JeecgController<MesXslEquip
|
||||
return Result.error(err);
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260518 for:【MES】设备台账保存前校验-----------
|
||||
//update-begin---author:jiangxh ---date:20250602 for:【MES】设备台账新增时系统编号由服务端生成-----------
|
||||
model.setLedgerNo(null);
|
||||
//update-end---author:jiangxh ---date:20250602 for:【MES】设备台账新增时系统编号由服务端生成-----------
|
||||
mesXslEquipmentLedgerService.save(model);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
@@ -131,6 +134,13 @@ public class MesXslEquipmentLedgerController extends JeecgController<MesXslEquip
|
||||
return Result.OK("该值可用!");
|
||||
}
|
||||
|
||||
@Operation(summary = "预览下一系统编号(001起)")
|
||||
@GetMapping(value = "/nextLedgerNo")
|
||||
public Result<String> nextLedgerNo() {
|
||||
MesXslEquipmentLedger ctx = new MesXslEquipmentLedger();
|
||||
return Result.OK(mesXslEquipmentLedgerService.generateNextLedgerNo(ctx));
|
||||
}
|
||||
|
||||
@Operation(summary = "校验设备名称是否重复")
|
||||
@GetMapping(value = "/checkEquipmentName")
|
||||
public Result<String> checkEquipmentName(
|
||||
@@ -225,6 +235,14 @@ public class MesXslEquipmentLedgerController extends JeecgController<MesXslEquip
|
||||
if (mesXslEquipmentLedgerService.isEquipmentCodeDuplicated(code, excludeId, model)) {
|
||||
return "设备编号不能重复";
|
||||
}
|
||||
//update-begin---author:jiangxh ---date:20250602 for:【MES】设备台账设备类别、设备类型必填-----------
|
||||
if (oConvertUtils.isEmpty(model.getEquipmentCategoryId())) {
|
||||
return "请选择设备类别";
|
||||
}
|
||||
if (oConvertUtils.isEmpty(model.getEquipmentTypeId())) {
|
||||
return "请选择设备类型";
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20250602 for:【MES】设备台账设备类别、设备类型必填-----------
|
||||
trimRelationNames(model);
|
||||
String status = model.getEquipmentStatus();
|
||||
if (oConvertUtils.isEmpty(status)) {
|
||||
@@ -258,6 +276,7 @@ public class MesXslEquipmentLedgerController extends JeecgController<MesXslEquip
|
||||
return "文件导入失败:第 " + rowNo + " 条设备编号不能为空";
|
||||
}
|
||||
row.setEquipmentCode(code);
|
||||
row.setLedgerNo(null);
|
||||
if (!codesInFile.add(code)) {
|
||||
return "文件导入失败:设备编号【" + code + "】在导入文件中重复";
|
||||
}
|
||||
@@ -279,6 +298,14 @@ public class MesXslEquipmentLedgerController extends JeecgController<MesXslEquip
|
||||
return "文件导入失败:第 " + rowNo + " 条设备名称【" + name + "】不能重复";
|
||||
}
|
||||
trimRelationNames(row);
|
||||
//update-begin---author:jiangxh ---date:20250602 for:【MES】设备台账导入设备类别、设备类型必填-----------
|
||||
if (oConvertUtils.isEmpty(row.getEquipmentCategoryId()) && oConvertUtils.isEmpty(row.getEquipmentCategoryName())) {
|
||||
return "文件导入失败:第 " + rowNo + " 条设备类别不能为空";
|
||||
}
|
||||
if (oConvertUtils.isEmpty(row.getEquipmentTypeId()) && oConvertUtils.isEmpty(row.getEquipmentTypeName())) {
|
||||
return "文件导入失败:第 " + rowNo + " 条设备类型不能为空";
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20250602 for:【MES】设备台账导入设备类别、设备类型必填-----------
|
||||
String status = row.getEquipmentStatus();
|
||||
if (oConvertUtils.isEmpty(status)) {
|
||||
row.setEquipmentStatus("0");
|
||||
|
||||
@@ -11,18 +11,15 @@ import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
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.common.system.query.QueryRuleEnum;
|
||||
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;
|
||||
@@ -44,6 +41,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
@Autowired
|
||||
private ISysDepartService sysDepartService;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
|
||||
@Operation(summary = "MES密炼PS编制-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslMixerPsCompile>> queryPageList(
|
||||
@@ -168,6 +168,11 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return super.importExcel(request, response, MesXslMixerPsCompile.class);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】密炼PS操作接口停用,改由集成方案无代码驱动-----------
|
||||
/*
|
||||
* 原校对/审核/批准/拒绝/撤回接口已停用(审批联动改由「集成方案」REGISTRY_STAGE_SYNC/REVERT 执行)。
|
||||
* 代码保留备后期可能恢复,勿删。
|
||||
*
|
||||
//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)
|
||||
@@ -215,8 +220,6 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
@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());
|
||||
@@ -255,6 +258,8 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return loginUser.getUsername();
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】校对/审核/批准-----------
|
||||
*/
|
||||
//update-end---author:GHT ---date:20260605 for:【XSLMES-20260605-K8R2】密炼PS操作接口停用,改由集成方案无代码驱动-----------
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】保存前校验与冗余回填-----------
|
||||
private String validateAndFill(MesXslMixerPsCompile model) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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 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.MesXslMixingProductionPlan;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMixingProductionPlanService;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanOrderOptionVO;
|
||||
import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanSaveAllVO;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "密炼生产计划维护")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslMixingProductionPlan")
|
||||
public class MesXslMixingProductionPlanController
|
||||
extends JeecgController<MesXslMixingProductionPlan, IMesXslMixingProductionPlanService> {
|
||||
|
||||
private final IMesXslMixingProductionPlanService mixingProductionPlanService;
|
||||
|
||||
public MesXslMixingProductionPlanController(
|
||||
IMesXslMixingProductionPlanService mixingProductionPlanService) {
|
||||
this.mixingProductionPlanService = mixingProductionPlanService;
|
||||
}
|
||||
|
||||
@Operation(summary = "密炼生产计划维护-分页列表查询")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslMixingProductionPlan>> queryPageList(
|
||||
MesXslMixingProductionPlan model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslMixingProductionPlan> queryWrapper =
|
||||
QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
queryWrapper.orderByAsc("sort_no").orderByAsc("create_time");
|
||||
IPage<MesXslMixingProductionPlan> pageList =
|
||||
mixingProductionPlanService.page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "密炼生产计划维护-整表保存")
|
||||
@Operation(summary = "密炼生产计划维护-整表保存")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixing_production_plan:saveAll")
|
||||
@PostMapping("/saveAll")
|
||||
public Result<String> saveAll(@RequestBody MesXslMixingProductionPlanSaveAllVO req) {
|
||||
mixingProductionPlanService.saveAllRows(req == null ? null : req.getRows());
|
||||
return Result.OK("保存成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "密炼生产计划维护-班次生产订单候选分页")
|
||||
@GetMapping("/orderOptionPage")
|
||||
public Result<IPage<MesXslMixingProductionPlanOrderOptionVO>> orderOptionPage(
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(name = "keyword", required = false) String keyword,
|
||||
@RequestParam(name = "machineId", required = false) String machineId,
|
||||
@RequestParam(name = "machineName", required = false) String machineName) {
|
||||
return Result.OK(
|
||||
mixingProductionPlanService.queryOrderOptions(
|
||||
pageNo, pageSize, keyword, machineId, machineName));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
@@ -97,9 +98,19 @@ public class MesXslProductionOrderController
|
||||
@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);
|
||||
public Result<List<MesXslMasterBatchPlan>> split(@RequestParam(name = "id", required = true) String id) {
|
||||
List<MesXslMasterBatchPlan> plans = mesXslProductionOrderService.splitToMasterBatchPlan(id);
|
||||
return Result.OK("拆分成功", plans);
|
||||
}
|
||||
|
||||
@AutoLog(value = "生产订单-批量拆分生成计划")
|
||||
@Operation(summary = "生产订单-批量拆分生成计划")
|
||||
@RequiresPermissions("xslmes:mes_xsl_production_order:split")
|
||||
@PostMapping("/splitBatch")
|
||||
public Result<Integer> splitBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idList = Arrays.asList(ids.split(","));
|
||||
int count = mesXslProductionOrderService.splitToMasterBatchPlanBatch(idList);
|
||||
return Result.OK("批量拆分成功", count);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_production_order:exportXls")
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
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.entity.MesXslRawMaterialDemandPlanSummary;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
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/mesXslRawMaterialDemandPlan")
|
||||
public class MesXslRawMaterialDemandPlanController {
|
||||
|
||||
@Autowired private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Operation(summary = "原材料需求计划-分页列表查询")
|
||||
@GetMapping("/list")
|
||||
public Result<IPage<MesXslRawMaterialDemandPlanSummary>> queryPageList(
|
||||
MesXslRawMaterialDemandPlanSummary query,
|
||||
@RequestParam(name = "groupByMachine", required = false, defaultValue = "0") Integer groupByMachine,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
|
||||
boolean groupedByMachine = groupByMachine != null && groupByMachine == 1;
|
||||
List<Object> params = new ArrayList<>();
|
||||
String groupedSql = buildGroupedSql(query, groupedByMachine, params);
|
||||
|
||||
String countSql = "SELECT COUNT(1) FROM (" + groupedSql + ") t";
|
||||
Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray());
|
||||
long totalCount = total == null ? 0L : total;
|
||||
|
||||
int offset = Math.max((pageNo - 1) * pageSize, 0);
|
||||
String pageSql = groupedSql + buildOrderBy(groupedByMachine) + " LIMIT ? OFFSET ?";
|
||||
List<Object> pageParams = new ArrayList<>(params);
|
||||
pageParams.add(pageSize);
|
||||
pageParams.add(offset);
|
||||
List<MesXslRawMaterialDemandPlanSummary> rows = queryRows(pageSql, pageParams, groupedByMachine);
|
||||
|
||||
Page<MesXslRawMaterialDemandPlanSummary> page = new Page<>(pageNo, pageSize, totalCount);
|
||||
page.setRecords(rows);
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_raw_material_demand_plan:exportXls")
|
||||
@RequestMapping("/exportXls")
|
||||
public ModelAndView exportXls(
|
||||
HttpServletRequest request,
|
||||
MesXslRawMaterialDemandPlanSummary query,
|
||||
@RequestParam(name = "groupByMachine", required = false, defaultValue = "0") Integer groupByMachine) {
|
||||
boolean groupedByMachine = groupByMachine != null && groupByMachine == 1;
|
||||
List<Object> params = new ArrayList<>();
|
||||
String groupedSql = buildGroupedSql(query, groupedByMachine, params) + buildOrderBy(groupedByMachine);
|
||||
List<MesXslRawMaterialDemandPlanSummary> exportList = queryRows(groupedSql, params, groupedByMachine);
|
||||
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||
mv.addObject(NormalExcelConstants.FILE_NAME, "原材料需求计划");
|
||||
mv.addObject(NormalExcelConstants.CLASS, MesXslRawMaterialDemandPlanSummary.class);
|
||||
mv.addObject(
|
||||
NormalExcelConstants.PARAMS,
|
||||
new ExportParams(
|
||||
"原材料需求计划",
|
||||
"导出人:" + (sysUser == null ? "admin" : sysUser.getRealname()),
|
||||
"原材料需求计划",
|
||||
ExcelType.XSSF));
|
||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
||||
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
|
||||
if (oConvertUtils.isNotEmpty(exportFields)) {
|
||||
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
|
||||
private String buildGroupedSql(
|
||||
MesXslRawMaterialDemandPlanSummary query, boolean groupedByMachine, List<Object> params) {
|
||||
String machineExpr = "COALESCE(NULLIF(TRIM(t.machine_name),''), '')";
|
||||
String erpCodeExpr = "COALESCE(NULLIF(TRIM(t.erp_code),''), '')";
|
||||
String rawMaterialExpr = "COALESCE(NULLIF(TRIM(t.raw_material_name),''), '')";
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT ");
|
||||
if (groupedByMachine) {
|
||||
sql.append(machineExpr).append(" AS machineName, ");
|
||||
} else {
|
||||
sql.append("'' AS machineName, ");
|
||||
}
|
||||
sql.append(erpCodeExpr)
|
||||
.append(" AS erpCode, ")
|
||||
.append(rawMaterialExpr)
|
||||
.append(" AS rawMaterialName, ")
|
||||
.append("SUM(COALESCE(t.demand_weight,0)) AS demandWeight, ")
|
||||
.append("SUM(COALESCE(t.standard_weight,0)) AS standardWeight, ")
|
||||
.append("SUM(COALESCE(t.actual_weight,0)) AS actualWeight ")
|
||||
.append("FROM mes_xsl_raw_material_demand_plan t ")
|
||||
.append("WHERE (t.del_flag = 0 OR t.del_flag IS NULL) ");
|
||||
|
||||
if (query != null && StringUtils.isNotBlank(query.getErpCode())) {
|
||||
sql.append("AND ").append(erpCodeExpr).append(" LIKE ? ");
|
||||
params.add("%" + query.getErpCode().trim() + "%");
|
||||
}
|
||||
if (query != null && StringUtils.isNotBlank(query.getRawMaterialName())) {
|
||||
sql.append("AND ").append(rawMaterialExpr).append(" LIKE ? ");
|
||||
params.add("%" + query.getRawMaterialName().trim() + "%");
|
||||
}
|
||||
if (groupedByMachine && query != null && StringUtils.isNotBlank(query.getMachineName())) {
|
||||
sql.append("AND ").append(machineExpr).append(" LIKE ? ");
|
||||
params.add("%" + query.getMachineName().trim() + "%");
|
||||
}
|
||||
|
||||
sql.append("GROUP BY ");
|
||||
if (groupedByMachine) {
|
||||
sql.append(machineExpr).append(", ");
|
||||
}
|
||||
sql.append(erpCodeExpr).append(", ").append(rawMaterialExpr);
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private List<MesXslRawMaterialDemandPlanSummary> queryRows(
|
||||
String sql, List<Object> params, boolean groupedByMachine) {
|
||||
return jdbcTemplate.query(
|
||||
sql,
|
||||
rs -> {
|
||||
List<MesXslRawMaterialDemandPlanSummary> list = new ArrayList<>();
|
||||
int seq = 1;
|
||||
while (rs.next()) {
|
||||
MesXslRawMaterialDemandPlanSummary row = new MesXslRawMaterialDemandPlanSummary();
|
||||
row.setMachineName(groupedByMachine ? trim(rs.getString("machineName")) : "");
|
||||
row.setErpCode(trim(rs.getString("erpCode")));
|
||||
row.setRawMaterialName(trim(rs.getString("rawMaterialName")));
|
||||
row.setDemandWeight(rs.getBigDecimal("demandWeight"));
|
||||
row.setStandardWeight(rs.getBigDecimal("standardWeight"));
|
||||
row.setActualWeight(rs.getBigDecimal("actualWeight"));
|
||||
row.setId(buildRowId(row, groupedByMachine, seq++));
|
||||
list.add(row);
|
||||
}
|
||||
return list;
|
||||
},
|
||||
params.toArray());
|
||||
}
|
||||
|
||||
private String buildOrderBy(boolean groupedByMachine) {
|
||||
if (groupedByMachine) {
|
||||
return " ORDER BY machineName, rawMaterialName, erpCode";
|
||||
}
|
||||
return " ORDER BY rawMaterialName, erpCode";
|
||||
}
|
||||
|
||||
private String buildRowId(MesXslRawMaterialDemandPlanSummary row, boolean groupedByMachine, int seq) {
|
||||
String machine = groupedByMachine ? StringUtils.defaultString(row.getMachineName()) : "ALL";
|
||||
return machine + "_" + StringUtils.defaultString(row.getErpCode()) + "_" + seq;
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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 jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
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.exception.JeecgBootException;
|
||||
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.entity.MesXslRubberSmallLockLog;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRubberSmallLockLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* MES 胶料小料锁定日志
|
||||
*/
|
||||
@Tag(name = "MES胶料小料锁定日志")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslRubberSmallLockLog")
|
||||
@Slf4j
|
||||
public class MesXslRubberSmallLockLogController
|
||||
extends JeecgController<MesXslRubberSmallLockLog, IMesXslRubberSmallLockLogService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslRubberSmallLockLogService mesXslRubberSmallLockLogService;
|
||||
|
||||
@Operation(summary = "MES胶料小料锁定日志-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslRubberSmallLockLog>> queryPageList(
|
||||
MesXslRubberSmallLockLog model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslRubberSmallLockLog> queryWrapper =
|
||||
QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
Page<MesXslRubberSmallLockLog> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslRubberSmallLockLog> pageList = mesXslRubberSmallLockLogService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES胶料小料锁定日志-添加")
|
||||
@Operation(summary = "MES胶料小料锁定日志-添加")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslRubberSmallLockLog model) {
|
||||
try {
|
||||
mesXslRubberSmallLockLogService.save(model);
|
||||
} catch (JeecgBootException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES胶料小料锁定日志-编辑")
|
||||
@Operation(summary = "MES胶料小料锁定日志-编辑")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslRubberSmallLockLog model) {
|
||||
try {
|
||||
mesXslRubberSmallLockLogService.updateById(model);
|
||||
} catch (JeecgBootException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES胶料小料锁定日志-删除")
|
||||
@Operation(summary = "MES胶料小料锁定日志-通过id删除")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
mesXslRubberSmallLockLogService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES胶料小料锁定日志-批量删除")
|
||||
@Operation(summary = "MES胶料小料锁定日志-批量删除")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
mesXslRubberSmallLockLogService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "MES胶料小料锁定日志-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslRubberSmallLockLog> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslRubberSmallLockLog entity = mesXslRubberSmallLockLogService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslRubberSmallLockLog model) {
|
||||
return super.exportXls(request, model, MesXslRubberSmallLockLog.class, "MES胶料小料锁定日志");
|
||||
}
|
||||
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_log:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, MesXslRubberSmallLockLog.class);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -18,6 +19,7 @@ import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRubberSmallLockReason;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRubberSmallLockReasonService;
|
||||
@@ -68,6 +70,9 @@ public class MesXslRubberSmallLockReasonController
|
||||
if (oConvertUtils.isEmpty(model.getBarcodeType())) {
|
||||
return Result.error("条码类型不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(model.getReasonDesc()) || model.getReasonDesc().trim().isEmpty()) {
|
||||
return Result.error("原因不能为空");
|
||||
}
|
||||
model.setReasonCode(null);
|
||||
try {
|
||||
mesXslRubberSmallLockReasonService.save(model);
|
||||
@@ -93,12 +98,16 @@ public class MesXslRubberSmallLockReasonController
|
||||
if (oConvertUtils.isEmpty(model.getBarcodeType())) {
|
||||
return Result.error("条码类型不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(model.getReasonDesc()) || model.getReasonDesc().trim().isEmpty()) {
|
||||
return Result.error("原因不能为空");
|
||||
}
|
||||
MesXslRubberSmallLockReason old = mesXslRubberSmallLockReasonService.getById(model.getId());
|
||||
if (old == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
old.setLockType(model.getLockType());
|
||||
old.setBarcodeType(model.getBarcodeType());
|
||||
old.setReasonDesc(model.getReasonDesc().trim());
|
||||
try {
|
||||
mesXslRubberSmallLockReasonService.updateById(old);
|
||||
} catch (JeecgBootException e) {
|
||||
@@ -143,6 +152,26 @@ public class MesXslRubberSmallLockReasonController
|
||||
return Result.OK(mesXslRubberSmallLockReasonService.generateNextReasonCode(ctx));
|
||||
}
|
||||
|
||||
@Operation(summary = "按条码类型查询锁定原因选项(日志表单联动)")
|
||||
@GetMapping(value = "/optionsByBarcodeType")
|
||||
public Result<List<MesXslRubberSmallLockReason>> optionsByBarcodeType(
|
||||
@RequestParam(name = "barcodeType", required = true) String barcodeType) {
|
||||
//update-begin---author:jiangxh ---date:20250602 for:【MES】锁定日志按条码类型加载原因选项-----------
|
||||
if (oConvertUtils.isEmpty(barcodeType)) {
|
||||
return Result.error("条码类型不能为空");
|
||||
}
|
||||
LambdaQueryWrapper<MesXslRubberSmallLockReason> w = new LambdaQueryWrapper<>();
|
||||
w.eq(MesXslRubberSmallLockReason::getBarcodeType, barcodeType.trim());
|
||||
w.and(
|
||||
q ->
|
||||
q.eq(MesXslRubberSmallLockReason::getDelFlag, CommonConstant.DEL_FLAG_0)
|
||||
.or()
|
||||
.isNull(MesXslRubberSmallLockReason::getDelFlag));
|
||||
w.orderByAsc(MesXslRubberSmallLockReason::getReasonCode);
|
||||
return Result.OK(mesXslRubberSmallLockReasonService.list(w));
|
||||
//update-end---author:jiangxh ---date:20250602 for:【MES】锁定日志按条码类型加载原因选项-----------
|
||||
}
|
||||
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_small_lock_reason:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslRubberSmallLockReason model) {
|
||||
@@ -179,6 +208,9 @@ public class MesXslRubberSmallLockReasonController
|
||||
if (oConvertUtils.isEmpty(row.getBarcodeType())) {
|
||||
return Result.error("文件导入失败:第 " + rowNo + " 条条码类型不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(row.getReasonDesc()) || row.getReasonDesc().trim().isEmpty()) {
|
||||
return Result.error("文件导入失败:第 " + rowNo + " 条原因不能为空");
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(row.getReasonCode())) {
|
||||
row.setReasonCode(row.getReasonCode().trim());
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.callback.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.entity.MesXslDingCallbackLog;
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.service.IMesXslDingCallbackLogService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
|
||||
/**
|
||||
* @Description: 钉钉回调日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-06-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "钉钉回调日志")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslDingCallbackLog")
|
||||
@Slf4j
|
||||
public class MesXslDingCallbackLogController extends JeecgController<MesXslDingCallbackLog, IMesXslDingCallbackLogService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslDingCallbackLogService mesXslDingCallbackLogService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*/
|
||||
@Operation(summary = "钉钉回调日志-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslDingCallbackLog>> queryPageList(MesXslDingCallbackLog mesXslDingCallbackLog,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslDingCallbackLog> queryWrapper = QueryGenerator.initQueryWrapper(mesXslDingCallbackLog, req.getParameterMap());
|
||||
Page<MesXslDingCallbackLog> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslDingCallbackLog> pageList = mesXslDingCallbackLogService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
@AutoLog(value = "钉钉回调日志-添加")
|
||||
@Operation(summary = "钉钉回调日志-添加")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslDingCallbackLog mesXslDingCallbackLog) {
|
||||
mesXslDingCallbackLogService.save(mesXslDingCallbackLog);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
@AutoLog(value = "钉钉回调日志-编辑")
|
||||
@Operation(summary = "钉钉回调日志-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslDingCallbackLog mesXslDingCallbackLog) {
|
||||
mesXslDingCallbackLogService.updateById(mesXslDingCallbackLog);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*/
|
||||
@AutoLog(value = "钉钉回调日志-通过id删除")
|
||||
@Operation(summary = "钉钉回调日志-通过id删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
mesXslDingCallbackLogService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
@AutoLog(value = "钉钉回调日志-批量删除")
|
||||
@Operation(summary = "钉钉回调日志-批量删除")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.mesXslDingCallbackLogService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*/
|
||||
@Operation(summary = "钉钉回调日志-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslDingCallbackLog> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
MesXslDingCallbackLog mesXslDingCallbackLog = mesXslDingCallbackLogService.getById(id);
|
||||
if (mesXslDingCallbackLog == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(mesXslDingCallbackLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*/
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslDingCallbackLog mesXslDingCallbackLog) {
|
||||
return super.exportXls(request, mesXslDingCallbackLog, MesXslDingCallbackLog.class, "钉钉回调日志");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*/
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_callback_log:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, MesXslDingCallbackLog.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.callback.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: 钉钉回调日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-06-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("mes_xsl_ding_callback_log")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "钉钉回调日志")
|
||||
public class MesXslDingCallbackLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Excel(name = "钉钉事件ID", width = 20)
|
||||
@Schema(description = "钉钉事件ID")
|
||||
private String eventId;
|
||||
|
||||
@Excel(name = "事件类型", width = 25)
|
||||
@Schema(description = "事件类型(bpms_instance_change/bpms_task_change)")
|
||||
private String eventType;
|
||||
|
||||
@Excel(name = "审批实例ID", width = 25)
|
||||
@Schema(description = "审批实例ID")
|
||||
private String processInstanceId;
|
||||
|
||||
@Excel(name = "原始推送数据", width = 50)
|
||||
@Schema(description = "原始推送数据JSON")
|
||||
private String rawData;
|
||||
|
||||
@Excel(name = "接收时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "接收时间")
|
||||
private Date receivedTime;
|
||||
|
||||
@Excel(name = "是否已处理", width = 10, dicCode = "yn")
|
||||
@Dict(dicCode = "yn")
|
||||
@Schema(description = "是否已处理集成方案(0否1是)")
|
||||
private Integer processed;
|
||||
|
||||
@Excel(name = "处理备注", width = 40)
|
||||
@Schema(description = "处理备注")
|
||||
private String processRemark;
|
||||
|
||||
@Excel(name = "关联业务表", width = 20)
|
||||
@Schema(description = "关联业务表")
|
||||
private String bizTable;
|
||||
|
||||
@Excel(name = "关联业务记录ID", width = 20)
|
||||
@Schema(description = "关联业务记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Excel(name = "关联审批台账ID", width = 20)
|
||||
@Schema(description = "关联审批台账ID")
|
||||
private String recordId;
|
||||
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/**创建日期*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
|
||||
/**更新人*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
|
||||
/**更新日期*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
|
||||
/**逻辑删除*/
|
||||
@TableLogic
|
||||
@Schema(description = "逻辑删除 0正常 1删除")
|
||||
private Integer delFlag;
|
||||
|
||||
/**租户ID*/
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.callback.mapper;
|
||||
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.entity.MesXslDingCallbackLog;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: 钉钉回调日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-06-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface MesXslDingCallbackLogMapper extends BaseMapper<MesXslDingCallbackLog> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.dingtalk.callback.mapper.MesXslDingCallbackLogMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.callback.service;
|
||||
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.entity.MesXslDingCallbackLog;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @Description: 钉钉回调日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-06-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IMesXslDingCallbackLogService extends IService<MesXslDingCallbackLog> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.callback.service.impl;
|
||||
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.entity.MesXslDingCallbackLog;
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.mapper.MesXslDingCallbackLogMapper;
|
||||
import org.jeecg.modules.xslmes.dingtalk.callback.service.IMesXslDingCallbackLogService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
/**
|
||||
* @Description: 钉钉回调日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-06-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
public class MesXslDingCallbackLogServiceImpl extends ServiceImpl<MesXslDingCallbackLogMapper, MesXslDingCallbackLog> implements IMesXslDingCallbackLogService {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamNodeConfigService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamSdkRunner;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 钉钉 Stream 节点配置辅助接口(供「第三方配置-钉钉集成」页面展示本机信息)。
|
||||
*/
|
||||
@Tag(name = "钉钉Stream配置")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/dingtalk/stream")
|
||||
public class DingTalkStreamConfigController {
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页展示本机节点信息-----------
|
||||
@Operation(summary = "获取本机 Stream 节点信息")
|
||||
@GetMapping("/nodeInfo")
|
||||
public Result<Map<String, Object>> nodeInfo() {
|
||||
Map<String, Object> data = new LinkedHashMap<>(nodeConfigService.buildNodeInfoSnapshot());
|
||||
DingTalkStreamSdkRunner.ConnectionSnapshot snap = DingTalkStreamSdkRunner.snapshot();
|
||||
data.put("streamRunning", snap.streamRunning());
|
||||
data.put("totalEvents", snap.totalEventCount());
|
||||
data.put("reconnectCount", snap.reconnectCount());
|
||||
return Result.OK(data);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页展示本机节点信息-----------
|
||||
}
|
||||
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