83 Commits

Author SHA1 Message Date
geht
5cb24c582d MES审批复用钉钉审批设置 2026-06-10 16:57:07 +08:00
geht
617d47a3db MES本地审批共用钉钉审批等配置 2026-06-10 16:33:44 +08:00
geht
c4447b91dd Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-06-10 15:43:15 +08:00
geht
39a9bd83f1 钉钉审批功能完善、混炼示方新增是否附加料 2026-06-10 15:41:02 +08:00
geht
de48bd2324 集群问题处理 2026-06-09 18:26:31 +08:00
geht
5b8bd2797a 钉钉回调事件处理 2026-06-09 17:52:33 +08:00
geht
fd5205e33e 钉钉审批配置优化 2026-06-08 19:05:29 +08:00
geht
1d0b4c9fbb 增强审批流管理能力,新增审批环节的 stageKey 区分关键环节与过路审批节点,完善钉钉回调日志记录,停用部分 HTTP 回调接口,改由集成方案驱动审批流,优化审批注册中心的查询逻辑。 2026-06-05 19:05:48 +08:00
geht
b9be88ae3f Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-06-05 10:45:42 +08:00
geht
fc4e3211ad 新增钉钉 Stream SDK 依赖,支持无 HTTP 上下文的后台线程显式传入 token 进行审批回调。同时,完善了 MES 审批台账功能,新增审批记录同步、批量发起审批时的门禁与台账写入逻辑,增强了系统的审批流管理能力。 2026-06-05 10:44:30 +08:00
geht
4785c55e52 新增钉钉审批模板配置功能,包括相关实体、控制器、服务及接口的实现,支持审批模板的增删改查及从钉钉同步模板,增强了系统的审批流管理能力。 2026-06-04 11:38:08 +08:00
457089e271 设备对应部位功能新增 2026-06-03 16:28:57 +08:00
geht
1c5cede957 Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-06-02 18:54:07 +08:00
geht
2d142dbc9c 新增多个控制器以支持密炼相关功能,包括报警记录、自动卸料日志、物料对应、称量校验日志、密炼机动作状态等,提供分页查询、通过ID查询及导出Excel功能,增强系统的可用性与数据管理能力。 2026-06-02 18:53:40 +08:00
a65ae7be60 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-06-02 16:55:53 +08:00
38f22ef8bd Merge branch 'main' into 生产及设备基础资料 2026-06-02 16:40:56 +08:00
69a60ca07b Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-06-02 16:38:48 +08:00
a08ca8985a 胶料小料锁定原因、锁定日志添加 2026-06-02 16:37:48 +08:00
29efd6694f 胶料维护规则 2026-06-02 16:32:10 +08:00
geht
e92cab555f 新增密炼物料皮重策略功能,包括相关实体、控制器、服务及接口的实现,支持桌面端免密CRUD操作,优化了打印记录的字段填充逻辑,提升了用户体验。 2026-06-02 16:30:46 +08:00
geht
fef7d25e3c 新增密炼物料皮重策略功能,包括相关实体、服务、控制器及接口,支持桌面端免密CRUD操作,优化打印记录与原料入场记录的衍生字段填充逻辑,提升用户体验。 2026-06-02 16:28:51 +08:00
b8b06a881a 胶料小料锁定原因新增 2026-06-02 15:29:16 +08:00
3f2c486f04 停机记录新增,设备管理查询条件完善 2026-06-02 14:11:35 +08:00
geht
37239e1b0a 更新混炼示方功能,优化胶料信息同步逻辑,新增胶料字段复制机制,提升胶料生成与更新的准确性与一致性。 2026-06-02 10:33:36 +08:00
geht
3586f86ea6 Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-06-01 11:30:44 +08:00
geht
bfb00804e6 更新混炼示方功能,优化胶料信息同步与删除逻辑,新增胶料类别名称匹配支持,调整选料弹窗以支持胶料查询,提升用户体验。 2026-06-01 11:30:07 +08:00
geht
767214b7db Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-05-29 18:57:49 +08:00
geht
0ff4a201b0 完善MES审批流设计功能,新增审批可选回调动作、发起人撤销及催办接口,支持审批状态恢复与联动回退,提升审批流程的灵活性与用户体验。 2026-05-29 18:57:09 +08:00
71f9dab1be Merge remote-tracking branch 'origin/20260519-3.9.2版本-葛昊天分支' 2026-05-29 15:51:29 +08:00
geht
aefa44b8a9 新增MES审批流设计功能,包括审批流定义、审批实例管理及审批办理接口,支持可视化设计与业务单据联动,提升审批流程的灵活性与用户体验。 2026-05-29 15:49:10 +08:00
4aa9952b26 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-05-29 15:49:02 +08:00
c8ce7a6fa3 母胶计划、终胶计划 2026-05-29 15:48:58 +08:00
geht
94132ea8da Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-05-29 10:50:42 +08:00
geht
e281f7fd92 新增IM聊天群管理接口,包括群聊详情、添加成员、移除成员、修改群名称、转让群主、退出群聊及解散群聊功能,提升群聊管理体验。 2026-05-29 10:49:11 +08:00
geht
22814cb1a7 增强IM聊天功能,支持群聊会话的打开与处理,优化消息过滤逻辑,提升用户体验。 2026-05-29 10:28:58 +08:00
geht
44a5868349 实现IM聊天群聊功能,包括群聊列表和创建群聊接口,优化消息处理和未读消息统计,增强用户体验。 2026-05-28 18:46:27 +08:00
71f6cfed3d Merge remote-tracking branch 'origin/生产及设备基础资料' 2026-05-28 17:16:45 +08:00
84821955c9 胶料快检记录新增 2026-05-28 17:15:20 +08:00
geht
a63cd6ad1a IM聊天功能优化 2026-05-28 17:08:34 +08:00
geht
3539eab924 新增IM聊天 2026-05-28 14:37:05 +08:00
geht
99e574f600 新增架子车数设定 2026-05-28 10:38:57 +08:00
geht
f3e0ffca4c 优化混炼示方日志记录逻辑,直接从入参构建快照以减少数据库查询,提高性能与效率。 2026-05-27 17:38:55 +08:00
geht
d2c1d4443b 优化配方日志查询功能,新增默认排序和参数处理逻辑,提升数据获取效率与用户体验。 2026-05-26 17:53:24 +08:00
geht
9e36435a72 新增配方日志查询功能,记录配方和混炼示方的创建、更新与删除操作,增强数据追溯能力。 2026-05-26 17:50:55 +08:00
geht
c70f7b2b90 新增混炼示方密炼PS审批联动同步审批人功能,优化混炼示方的编辑与删除权限控制,增强用户交互体验。 2026-05-26 11:15:00 +08:00
geht
7786369a63 Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-05-26 11:09:41 +08:00
geht
e6241c16c7 优化混炼示方中换算系数和配方参数的数值展示与输入解析,调整相关字段的小数精度,增强用户交互体验。 2026-05-26 10:26:53 +08:00
geht
51cac2c17a 新增混炼示方选择弹窗及相关功能,支持规格选择与参照新增,优化用户交互体验。 2026-05-26 10:21:56 +08:00
geht
a579f0e15c 新增参照历史混合步骤功能,包含混合步骤复制、状态解析及历史步骤选择弹窗,优化用户交互体验。 2026-05-26 10:10:28 +08:00
geht
41f8cef462 优化混炼示方 2026-05-26 09:52:42 +08:00
geht
72aeee0f10 优化种类生成逻辑 2026-05-25 20:42:13 +08:00
geht
441c19e87a 实现密炼物料种类配置关联解析功能,新增种类查找表加载与解析接口,优化选料弹窗层级与刷新功能,增强用户体验与系统稳定性。 2026-05-25 20:29:07 +08:00
geht
dc3f305303 优化混炼示方,新增种类配置 2026-05-25 19:44:14 +08:00
589961397c 快检实验标准新增 2026-05-25 16:01:14 +08:00
837a85f7ba Merge branch '生产及设备基础资料' 2026-05-25 11:02:18 +08:00
a6579f019a 快检实验方法新增 2026-05-25 11:01:26 +08:00
af8bf14b5e 胶料快检实验类型、胶料快检数据点、胶料快检实验方法新增 2026-05-25 11:01:00 +08:00
geht
c85657d199 新增混炼示方生成预览与批量创建功能,优化相关字段及用户交互,修复界面显示问题,增强系统稳定性和用户体验。 2026-05-22 19:43:41 +08:00
geht
f3e3a99ebc Merge remote-tracking branch 'origin/main' into 20260519-3.9.2版本-葛昊天分支 2026-05-22 16:35:11 +08:00
geht
d7fd9c6037 新增MES混炼示方模块,包括主表及子表结构、控制器、服务和映射器的实现,支持增删改查功能,优化数据验证和用户体验,增强系统稳定性。 2026-05-22 16:34:33 +08:00
geht
680eb6c54c 更新配合示方模块,新增密炼PS审批联动功能,支持状态与审批人同步,优化相关服务实现,调整数据库状态字典,增强系统数据一致性和用户体验。 2026-05-22 12:15:05 +08:00
b56bf74bb8 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-05-22 12:04:56 +08:00
467c49f432 密炼机动作维护、日罐物料对应信息、密炼机条件维护、生产订单、金蝶对接配置 2026-05-22 12:04:46 +08:00
fa1c5c9b42 Merge remote-tracking branch 'origin/生产及设备基础资料' 2026-05-22 09:58:43 +08:00
geht
442b31ad37 Merge branch '20260519-3.9.2版本-葛昊天分支' 2026-05-21 18:50:05 +08:00
geht
89407d1f1d 新增配合示方模块,包括主子表结构、控制器、服务及映射器的实现,支持增删改查功能,优化胶料代号生成逻辑及相关字段,增强数据验证和用户体验。 2026-05-21 18:49:20 +08:00
2496d05349 设备点检记录新增 2026-05-21 17:54:57 +08:00
874e513c90 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-05-21 10:04:24 +08:00
e678276aba 密炼机动作维护、日罐物料对应关系 2026-05-21 10:04:06 +08:00
geht
a10aae420a 更新胶料信息处理逻辑,优化字段描述及数据验证,增强系统稳定性和用户体验。 2026-05-20 19:06:58 +08:00
geht
9f37292eea 更新胶料编码为胶料别名,调整相关字段和描述,新增数据库字段以支持胶料信息补全 2026-05-20 16:12:38 +08:00
geht
1a4027086c Merge branch 'main' into 20260519-3.9.2版本-葛昊天分支 2026-05-20 15:59:44 +08:00
geht
031725de7e 新增MES密炼物料替代对应关系及开炼机参数维护模块,包括相关实体、控制器、服务和映射器的实现,支持增删改查及数据验证功能。 2026-05-20 15:56:27 +08:00
09c58f80eb 胶料信息更换分类字典 2026-05-20 15:55:54 +08:00
geht
cd3194d1a6 Merge remote-tracking branch 'origin/main' into 20260519-3.9.2版本-葛昊天分支 2026-05-20 15:49:36 +08:00
c09ca584c3 解决终端汉字乱码问题 2026-05-20 15:37:39 +08:00
0e6eba8cf4 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-05-20 15:30:46 +08:00
34b6ed4478 胶料信息 2026-05-20 15:30:42 +08:00
1b45d6124d 设备点检配置新增 2026-05-20 15:30:37 +08:00
b86c94add9 Merge remote-tracking branch 'origin/main' into 生产及设备基础资料 2026-05-20 11:35:54 +08:00
84286a6769 配置地址 2026-05-20 11:29:52 +08:00
9fe1da209d 点检及保养项目新增 2026-05-19 15:20:51 +08:00
d57cb6cb8c eclipse文件隐藏 2026-05-19 12:01:13 +08:00
1025 changed files with 109944 additions and 843 deletions

10
.gitignore vendored
View File

@@ -1,10 +1,20 @@
## ide
**/.idea
**/.project
**/.classpath
**/.factorypath
**/.settings/
*.iml
rebel.xml
.project
.classpath
.settings/
.factorypath
**/.settings/
## backend
**/target
**/bin/
**/logs
## front

20
.vscode/launch.json vendored
View File

@@ -1,6 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "jeecgboot-vue3: 调试前端 (Chrome)",
"url": "http://localhost:3100",
"webRoot": "${workspaceFolder}/jeecgboot-vue3",
"preLaunchTask": "jeecgboot-vue3: dev"
},
{
"type": "node",
"request": "launch",
"name": "jeecgboot-vue3: dev",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "dev"],
"cwd": "${workspaceFolder}/jeecgboot-vue3",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**", "**/node_modules/**"]
},
{
"type": "java",
"name": "JeecgSystemApplication (单体)",
@@ -8,7 +26,7 @@
"mainClass": "org.jeecg.JeecgSystemApplication",
"projectName": "jeecg-system-start",
"cwd": "${workspaceFolder}/jeecg-boot/jeecg-module-system/jeecg-system-start",
"vmArgs": "-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8"
"vmArgs": "-Dfile.encoding=UTF-8 -Dspring.main.banner-mode=log -Dspring.banner.charset=UTF-8 -Dlogging.charset.console=GBK"
}
]
}

15
.vscode/settings.json vendored
View File

@@ -4,6 +4,8 @@
"java.import.maven.enabled": true,
"java.configuration.updateBuildConfiguration": "automatic",
"java.autobuild.enabled": true,
"java.import.maven.offline.enabled": false,
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore",
"java.jdt.ls.java.home": "C:\\Program Files\\Java\\jdk-17",
"java.configuration.runtimes": [
{
@@ -12,6 +14,10 @@
"default": true
}
],
"java.maven.downloadSources": true,
"java.eclipse.downloadSources": true,
"java.project.importOnFirstTimeStartup": "automatic",
"java.configuration.checkProjectSettingsExclusions": false,
"java.import.exclusions": [
"**/jeecg-server-cloud/**",
"**/jeecg-boot-platform/**",
@@ -27,5 +33,12 @@
"jeecg-boot-platform"
],
"java.debug.settings.console": "integratedTerminal",
"java.debug.settings.vmArgs": "-Dspring.main.banner-mode=log -Dspring.banner.charset=UTF-8 -Dlogging.charset.console=GBK"
"java.debug.settings.vmArgs": "-Dfile.encoding=UTF-8 -Dspring.main.banner-mode=log -Dspring.banner.charset=UTF-8 -Dlogging.charset.console=GBK",
"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.profiles.windows": {
"PowerShell": {
"source": "PowerShell",
"args": ["-NoExit", "-Command", "chcp 936 | Out-Null"]
}
}
}

18
.vscode/tasks.json vendored
View File

@@ -54,6 +54,24 @@
"dependsOn": "YY.Admin: build",
"problemMatcher": []
},
{
"label": "jeecgboot-vue3: dev",
"type": "shell",
"command": "pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/jeecgboot-vue3"
},
"isBackground": true,
"problemMatcher": {
"owner": "vite",
"pattern": { "regexp": "^$" },
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "(Local:|ready in|http://localhost)"
}
}
},
{
"label": "YY.Admin: run (script)",
"type": "process",

View File

@@ -324,17 +324,26 @@ func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool,
queued := false
jobID := 0
sumatraDone := false
for {
select {
case err := <-cmdDone:
sumatraDone = true
if err != nil && !queued {
return fmt.Errorf("sumatra print failed: %v", err)
}
// Sumatra 已正常退出且 spooler 未出现新任务:部分驱动/打印机直接出纸,不经过队列
if err == nil && !queued {
return nil
}
default:
}
now := time.Now()
if !queued && sumatraDone {
return nil
}
if !queued && now.After(appearDeadline) {
return fmt.Errorf("print job not queued within %s", printQueueAppearTimeout)
}

View File

@@ -2,10 +2,18 @@
**/.idea
*.iml
rebel.xml
# VS Code/Cursor Java 扩展Eclipse JDT导入 Maven 时自动生成,勿提交
.project
.classpath
.settings/
.factorypath
**/.settings/
## backend
**/target
**/logs
# 开发者本机钉钉 Stream 接收配置(从 application-dev-local.yml.example 复制)
**/application-dev-local.yml
## front
**/*.lock

View File

@@ -4,6 +4,8 @@
"java.import.maven.enabled": true,
"java.configuration.updateBuildConfiguration": "automatic",
"java.autobuild.enabled": true,
"java.import.maven.offline.enabled": false,
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore",
"java.jdt.ls.java.home": "C:\\Program Files\\Java\\jdk-17",
"java.configuration.runtimes": [
{
@@ -12,6 +14,10 @@
"default": true
}
],
"java.maven.downloadSources": true,
"java.eclipse.downloadSources": true,
"java.project.importOnFirstTimeStartup": "automatic",
"java.configuration.checkProjectSettingsExclusions": false,
"java.import.exclusions": [
"**/jeecg-server-cloud/**",
"**/jeecg-boot-platform/**",

18
jeecg-boot/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Maven: 修复 Java Classpath",
"type": "shell",
"command": "mvn clean install -DskipTests -pl jeecg-boot-base-core,jeecg-boot-module/jeecg-module-print,jeecg-boot-module/jeecg-module-xslmes -am",
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "build",
"isDefault": false
},
"problemMatcher": []
}
]
}

View 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;

View File

@@ -11,9 +11,9 @@ menu_type=VALUES(menu_type), perms=VALUES(perms), perms_type=VALUES(perms_type),
is_route=VALUES(is_route), is_leaf=VALUES(is_leaf), hidden=VALUES(hidden), status=VALUES(status), del_flag=VALUES(del_flag),
always_show=VALUES(always_show), keep_alive=VALUES(keep_alive), internal_or_external=VALUES(internal_or_external);
-- 二级菜单料信息
-- 二级菜单料信息
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 ('1860000000000000011', '1860000000000000001', '料信息', '/mes/materialinfo', 'mes/materialinfo/index', 'MesMaterialList', 1, NULL, '1', 1, 1, 1, 0, '1', 0, 1, 0, 'admin', NOW())
VALUES ('1860000000000000011', '1860000000000000001', '料信息', '/mes/materialinfo', 'mes/materialinfo/index', 'MesMaterialList', 1, NULL, '1', 1, 1, 1, 0, '1', 0, 1, 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id=VALUES(parent_id), name=VALUES(name), url=VALUES(url), component=VALUES(component), component_name=VALUES(component_name),
menu_type=VALUES(menu_type), perms=VALUES(perms), perms_type=VALUES(perms_type), sort_no=VALUES(sort_no),

View File

@@ -0,0 +1,92 @@
-- 日罐物料对应信息菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099311', @mixer_parent_id, '日罐物料对应信息',
'/mes/daytankmaterialmapinfo',
'mes/daytankmaterialmapinfo/index',
'MesXslDayTankMaterialMapList', 1, NULL, '1', 31,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099312', '1860000000000099311', '新增', 2, 'xslmes:mes_xsl_day_tank_material_map:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099313', '1860000000000099311', '编辑', 2, 'xslmes:mes_xsl_day_tank_material_map:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099314', '1860000000000099311', '删除', 2, 'xslmes:mes_xsl_day_tank_material_map:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099315', '1860000000000099311', '批量删除', 2, 'xslmes:mes_xsl_day_tank_material_map:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099316', '1860000000000099311', '导出', 2, 'xslmes:mes_xsl_day_tank_material_map:exportXls', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099311',
'1860000000000099312', '1860000000000099313', '1860000000000099314', '1860000000000099315', '1860000000000099316'
)
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/daytankmaterialmapinfo',
component = 'mes/daytankmaterialmapinfo/index',
component_name = 'MesXslDayTankMaterialMapList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099311';

View File

@@ -0,0 +1,82 @@
-- MES 停机记录建表 + 菜单 + 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀mes:mes_xsl_downtime_record:*
-- 依赖mes_xsl_equipment_ledgermes_xsl_downtime_type父菜单 设备管理
-- FlywayV3.9.2_117__mes_xsl_downtime_record.sql
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_downtime_record` (
`id` varchar(32) NOT NULL COMMENT '主键',
`equipment_ledger_id` varchar(32) NOT NULL COMMENT '设备台账主键 mes_xsl_equipment_ledger.id',
`equipment_code` varchar(500) DEFAULT NULL COMMENT '设备编号冗余',
`equipment_name` varchar(500) DEFAULT NULL COMMENT '设备名称冗余',
`downtime_type_id` varchar(32) NOT NULL COMMENT '停机类型主键 mes_xsl_downtime_type.id',
`downtime_type_name` varchar(500) DEFAULT NULL COMMENT '停机类型名称冗余',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`equipment_part_id` varchar(32) DEFAULT NULL COMMENT '设备部位主键 mes_xsl_equipment_part.id',
`equipment_part_name` varchar(500) DEFAULT NULL COMMENT '设备部位名称冗余',
`inspect_maintain_item_id` varchar(32) DEFAULT NULL COMMENT '点检及保养项目主键 mes_xsl_inspect_maintain_item.id',
`inspect_maintain_item_name` varchar(500) DEFAULT NULL COMMENT '点检项目名称冗余',
`maintenance_result` varchar(500) DEFAULT NULL COMMENT '维修结果',
`maintainer_user_id` varchar(32) DEFAULT NULL COMMENT '维修人用户ID',
`maintainer_username` varchar(500) DEFAULT NULL COMMENT '维修人账号',
`maintainer_realname` varchar(500) DEFAULT NULL COMMENT '维修人姓名',
`maintenance_filled_flag` varchar(1) DEFAULT '0' COMMENT '是否已录入维修结果字典yn1是0否',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_mdr_equipment` (`equipment_ledger_id`),
KEY `idx_mdr_downtime_type` (`downtime_type_id`),
KEY `idx_mdr_start_time` (`start_time`)
) 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 ('1860000000000000201', @mes_equip_pid, '停机记录', '/xslmes/mesXslDowntimeRecord', 'xslmes/mesXslDowntimeRecord/MesXslDowntimeRecordList', 'MesXslDowntimeRecordList', 1, NULL, '1', 12, 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:history-outlined';
UPDATE `sys_permission` SET `icon` = 'ant-design:history-outlined' WHERE `id` = '1860000000000000201' 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
('1860000000000000202', '1860000000000000201', '新增', 2, 'mes:mes_xsl_downtime_record:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000203', '1860000000000000201', '编辑', 2, 'mes:mes_xsl_downtime_record:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000204', '1860000000000000201', '删除', 2, 'mes:mes_xsl_downtime_record:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000205', '1860000000000000201', '批量删除', 2, 'mes:mes_xsl_downtime_record:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000206', '1860000000000000201', '导出', 2, 'mes:mes_xsl_downtime_record:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000207', '1860000000000000201', '导入', 2, 'mes:mes_xsl_downtime_record:importExcel', '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 (
'1860000000000000201',
'1860000000000000202', '1860000000000000203', '1860000000000000204', '1860000000000000205',
'1860000000000000206', '1860000000000000207'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,91 @@
-- MES 设备点检配置主子表建表 + 菜单 + 按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_equip_inspect_config:*
-- 父菜单设备管理类型字典复用 xslmes_im_item_category须先有点检及保养项目功能
-- FlywayV3.9.2_78__mes_xsl_equip_inspect_config.sql
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_equip_inspect_config` (
`id` varchar(32) NOT NULL COMMENT '主键',
`equipment_ledger_id` varchar(32) NOT NULL COMMENT '设备台账主键 mes_xsl_equipment_ledger.id',
`equipment_name` varchar(500) DEFAULT NULL COMMENT '设备名称冗余',
`equipment_code` varchar(500) DEFAULT NULL COMMENT '设备编号冗余',
`config_type` varchar(500) NOT NULL COMMENT '配置类型字典xslmes_im_item_categoryinspect点检/maintain保养同设备同类型唯一',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_meic_tenant_equip_type` (`tenant_id`, `equipment_ledger_id`, `config_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES设备点检配置';
CREATE TABLE IF NOT EXISTS `mes_xsl_equip_inspect_config_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`config_id` varchar(32) NOT NULL COMMENT '主表主键 mes_xsl_equip_inspect_config.id',
`inspect_maintain_item_id` varchar(32) NOT NULL COMMENT '点检及保养项目主键 mes_xsl_inspect_maintain_item.id',
`item_code` varchar(500) DEFAULT NULL COMMENT '点检项目编号冗余',
`item_name` varchar(500) DEFAULT NULL COMMENT '项目名称冗余',
`item_category` varchar(500) DEFAULT NULL COMMENT '项目类别冗余',
`item_type` varchar(500) DEFAULT NULL COMMENT '项目类型冗余',
`equipment_part_name` varchar(500) DEFAULT NULL COMMENT '设备部位名称冗余',
`equipment_sub_part_name` varchar(500) DEFAULT NULL COMMENT '设备小部位名称冗余',
`inspect_method` varchar(500) DEFAULT NULL COMMENT '点检方式冗余',
`judgment_criteria` varchar(500) DEFAULT NULL COMMENT '判断基准冗余',
`sort_no` int DEFAULT '0' COMMENT '排序号',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_meicl_config` (`config_id`),
KEY `idx_meicl_item` (`inspect_maintain_item_id`),
UNIQUE KEY `uk_meicl_config_item` (`config_id`, `inspect_maintain_item_id`)
) 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 ('1860000000000000148', @mes_equip_pid, '设备点检配置', '/xslmes/mesXslEquipInspectConfig', 'xslmes/mesXslEquipInspectConfig/MesXslEquipInspectConfigList', NULL, 1, NULL, '1', 12, 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:control-outlined';
UPDATE `sys_permission` SET `icon` = 'ant-design:control-outlined' WHERE `id` = '1860000000000000148' 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
('1860000000000000149', '1860000000000000148', '新增', 2, 'mes:mes_xsl_equip_inspect_config:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000150', '1860000000000000148', '编辑', 2, 'mes:mes_xsl_equip_inspect_config:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000151', '1860000000000000148', '删除', 2, 'mes:mes_xsl_equip_inspect_config:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000152', '1860000000000000148', '批量删除', 2, 'mes:mes_xsl_equip_inspect_config:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000153', '1860000000000000148', '导出', 2, 'mes:mes_xsl_equip_inspect_config:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000154', '1860000000000000148', '导入', 2, 'mes:mes_xsl_equip_inspect_config:importExcel', '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 (
'1860000000000000148',
'1860000000000000149', '1860000000000000150', '1860000000000000151', '1860000000000000152',
'1860000000000000153', '1860000000000000154'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,138 @@
-- MES 点检/保养记录主子表建表 + 字典 + 菜单 + 按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_equip_inspect_record:*
-- 父菜单设备管理依赖设备点检配置设备台账
-- FlywayV3.9.2_79__mes_xsl_equip_inspect_record.sql
SET NAMES utf8mb4;
INSERT INTO `sys_dict`(`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES点检记录结果', 'xslmes_im_inspect_result', '合格/不合格', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_im_inspect_result' AND `del_flag` = 0);
INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.`id`, '合格', 'pass', '', 1, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_inspect_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.`id` AND i.`item_value` = 'pass');
INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.`id`, '不合格', 'fail', '', 2, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_inspect_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.`id` AND i.`item_value` = 'fail');
INSERT INTO `sys_dict`(`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES点检记录状态', 'xslmes_im_record_status', '待点检/已点检', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_im_record_status' AND `del_flag` = 0);
INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.`id`, '待点检', 'pending', '', 1, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_record_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.`id` AND i.`item_value` = 'pending');
INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.`id`, '已点检', 'done', '', 2, 1, 'admin', NOW()
FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_record_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.`id` AND i.`item_value` = 'done');
CREATE TABLE IF NOT EXISTS `mes_xsl_equip_inspect_record` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_no` varchar(32) NOT NULL COMMENT '记录编号EC+yyyyMMdd+4位流水租户内按日递增',
`plan_no` varchar(500) DEFAULT NULL COMMENT '计划单号',
`plan_id` varchar(32) DEFAULT NULL COMMENT '计划主键隐藏',
`equipment_ledger_id` varchar(32) NOT NULL COMMENT '设备台账主键 mes_xsl_equipment_ledger.id',
`equipment_code` varchar(500) DEFAULT NULL COMMENT '设备编码冗余',
`equipment_name` varchar(500) DEFAULT NULL COMMENT '设备名称冗余',
`equip_inspect_config_id` varchar(32) DEFAULT NULL COMMENT '设备点检配置主键 mes_xsl_equip_inspect_config.id',
`record_type` varchar(500) NOT NULL COMMENT '类型字典xslmes_im_item_categoryinspect点检/maintain保养',
`inspect_date` date DEFAULT NULL COMMENT '点检日期',
`inspector_user_id` varchar(32) DEFAULT NULL COMMENT '点检人用户ID',
`inspector_username` varchar(500) DEFAULT NULL COMMENT '点检人账号',
`inspector_realname` varchar(500) DEFAULT NULL COMMENT '点检人姓名',
`inspect_result` varchar(500) NOT NULL COMMENT '点检结果字典xslmes_im_inspect_resultpass合格/fail不合格',
`record_status` varchar(500) NOT NULL COMMENT '状态字典xslmes_im_record_statuspending待点检/done已点检',
`handled_flag` varchar(1) DEFAULT NULL COMMENT '是否已处理字典yn1是0否仅不合格记录使用',
`handler_user_id` varchar(32) DEFAULT NULL COMMENT '处理人用户ID',
`handler_username` varchar(500) DEFAULT NULL COMMENT '处理人账号',
`handler_realname` varchar(500) DEFAULT NULL COMMENT '处理人姓名',
`handle_time` datetime DEFAULT NULL COMMENT '处理时间',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_meir_tenant_record_no` (`tenant_id`, `record_no`),
KEY `idx_meir_equip_type` (`equipment_ledger_id`, `record_type`),
KEY `idx_meir_inspect_date` (`inspect_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES点检保养记录';
CREATE TABLE IF NOT EXISTS `mes_xsl_equip_inspect_record_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_id` varchar(32) NOT NULL COMMENT '主表主键 mes_xsl_equip_inspect_record.id',
`equip_inspect_config_line_id` varchar(32) NOT NULL COMMENT '设备点检配置明细主键 mes_xsl_equip_inspect_config_line.id',
`inspect_maintain_item_id` varchar(32) DEFAULT NULL COMMENT '点检及保养项目主键冗余',
`item_code` varchar(500) DEFAULT NULL COMMENT '点检项目编号冗余',
`item_name` varchar(500) DEFAULT NULL COMMENT '项目名称冗余',
`item_category` varchar(500) DEFAULT NULL COMMENT '项目类别冗余',
`item_type` varchar(500) DEFAULT NULL COMMENT '项目类型冗余',
`equipment_part_name` varchar(500) DEFAULT NULL COMMENT '设备部位冗余',
`equipment_sub_part_name` varchar(500) DEFAULT NULL COMMENT '设备小部位冗余',
`inspect_method` varchar(500) DEFAULT NULL COMMENT '点检方式冗余',
`judgment_criteria` varchar(500) DEFAULT NULL COMMENT '判断基准冗余',
`line_inspect_result` varchar(500) DEFAULT NULL COMMENT '明细点检结果文本',
`picture_files` varchar(2000) DEFAULT NULL COMMENT '图片上传路径逗号分隔',
`sort_no` int DEFAULT '0' COMMENT '排序号',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_meirl_record` (`record_id`),
KEY `idx_meirl_config_line` (`equip_inspect_config_line_id`)
) 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 ('1860000000000000155', @mes_equip_pid, '点检保养记录', '/xslmes/mesXslEquipInspectRecord', 'xslmes/mesXslEquipInspectRecord/MesXslEquipInspectRecordList', 'MesXslEquipInspectRecordList', 1, NULL, '1', 13, 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:file-done-outlined';
UPDATE `sys_permission` SET `icon` = 'ant-design:file-done-outlined' WHERE `id` = '1860000000000000155' 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
('1860000000000000156', '1860000000000000155', '新增', 2, 'mes:mes_xsl_equip_inspect_record:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000157', '1860000000000000155', '编辑', 2, 'mes:mes_xsl_equip_inspect_record:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000158', '1860000000000000155', '删除', 2, 'mes:mes_xsl_equip_inspect_record:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000159', '1860000000000000155', '批量删除', 2, 'mes:mes_xsl_equip_inspect_record:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000160', '1860000000000000155', '导出', 2, 'mes:mes_xsl_equip_inspect_record:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000161', '1860000000000000155', '导入', 2, 'mes:mes_xsl_equip_inspect_record:importExcel', '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 (
'1860000000000000155',
'1860000000000000156', '1860000000000000157', '1860000000000000158', '1860000000000000159',
'1860000000000000160', '1860000000000000161'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,65 @@
-- MES 设备对应部位建表 + 菜单 + 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀mes:mes_xsl_equip_part_mapping:*
-- 数据由设备点检配置保存后自动生成列表无手工新增
-- FlywayV3.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`
);

View File

@@ -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设备台账';

View File

@@ -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设备台账';

View File

@@ -0,0 +1,92 @@
-- 终胶计划菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099711', @mixer_parent_id, '终胶计划',
'/mes/finalbatchplaninfo',
'mes/finalbatchplaninfo/index',
'MesXslFinalBatchPlanList', 1, NULL, '1', 35,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099712', '1860000000000099711', '新增', 2, 'xslmes:mes_xsl_final_batch_plan:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099713', '1860000000000099711', '编辑', 2, 'xslmes:mes_xsl_final_batch_plan:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099714', '1860000000000099711', '删除', 2, 'xslmes:mes_xsl_final_batch_plan:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099715', '1860000000000099711', '批量删除', 2, 'xslmes:mes_xsl_final_batch_plan:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099716', '1860000000000099711', '导出', 2, 'xslmes:mes_xsl_final_batch_plan:exportXls', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099711',
'1860000000000099712', '1860000000000099713', '1860000000000099714', '1860000000000099715', '1860000000000099716'
)
AND NOT EXISTS (
SELECT 1
FROM sys_role_permission rp
WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
AND rp.permission_id = p.id
);
-- 强制修复确保菜单路由与组件路径正确
UPDATE sys_permission
SET
parent_id = @mixer_parent_id,
url = '/mes/finalbatchplaninfo',
component = 'mes/finalbatchplaninfo/index',
component_name = 'MesXslFinalBatchPlanList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099711';

View File

@@ -0,0 +1,30 @@
-- 终胶计划建表SQL
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_final_batch_plan` (
`id` varchar(32) NOT NULL COMMENT '主键',
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水',
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
`order_date` date DEFAULT NULL COMMENT '订单日期',
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编码',
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料信息',
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
`status` int DEFAULT 0 COMMENT '状态0未开始 1进行中 2已完成',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT 0 COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_mxfb_source_order` (`source_order_id`),
KEY `idx_mxfb_material_code` (`material_code`),
UNIQUE KEY `uk_mxfb_source_order_del` (`source_order_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES终胶计划';

View File

@@ -0,0 +1,118 @@
-- MES 点检及保养项目字典 + 建表 + 菜单 + 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀与 Controller前端 v-auth 一致mes:mes_xsl_inspect_maintain_item:*
-- 父菜单设备管理修改租户改 SET @mes_tenant_id
-- 新环境也可依赖 FlywayV3.9.2_77__mes_xsl_inspect_maintain_item.sql
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES点检保养项目类别', 'xslmes_im_item_category', 'inspect点检/maintain保养', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_im_item_category' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '点检', 'inspect', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_item_category' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'inspect');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '保养', 'maintain', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_item_category' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'maintain');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES点检保养项目类型', 'xslmes_im_item_type', 'mechanical机械类/electrical电气类', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_im_item_type' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '机械类', 'mechanical', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_item_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'mechanical');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '电气类', 'electrical', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_item_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'electrical');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES点检方式', 'xslmes_im_inspect_method', 'visual视觉/sight目测/hearing听觉', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_im_inspect_method' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '视觉', 'visual', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_inspect_method' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'visual');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '目测', 'sight', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_inspect_method' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'sight');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '听觉', 'hearing', 3, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_im_inspect_method' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'hearing');
CREATE TABLE IF NOT EXISTS `mes_xsl_inspect_maintain_item` (
`id` varchar(32) NOT NULL COMMENT '主键',
`item_name` varchar(500) NOT NULL COMMENT '项目名称同租户未删除数据中唯一',
`item_code` varchar(500) NOT NULL COMMENT '项目编号同租户未删除数据中唯一',
`equipment_category_id` varchar(32) NOT NULL COMMENT '设备类别主键 mes_xsl_equipment_category.id',
`equipment_category_name` varchar(500) DEFAULT NULL COMMENT '设备类别名称冗余',
`equipment_type_id` varchar(32) NOT NULL COMMENT '设备类型主键 mes_xsl_equipment_type.id',
`equipment_type_name` 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 '设备部位名称冗余',
`equipment_sub_part_id` varchar(32) NOT NULL COMMENT '设备小部位主键 mes_xsl_equipment_sub_part.id',
`equipment_sub_part_name` varchar(500) DEFAULT NULL COMMENT '设备小部位名称冗余',
`item_category` varchar(500) NOT NULL COMMENT '项目类别字典xslmes_im_item_categoryinspect点检/maintain保养',
`item_type` varchar(500) NOT NULL COMMENT '项目类型字典xslmes_im_item_typemechanical机械类/electrical电气类',
`inspect_method` varchar(500) NOT NULL COMMENT '点检方式字典xslmes_im_inspect_methodvisual视觉/sight目测/hearing听觉',
`judgment_criteria` varchar(500) NOT NULL COMMENT '判断基准',
`maintain_cycle_days` int DEFAULT NULL COMMENT '保养周期',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(500) DEFAULT NULL COMMENT '部门',
`create_by` varchar(500) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(500) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_mimi_tenant_name` (`tenant_id`, `item_name`),
KEY `idx_mimi_tenant_code` (`tenant_id`, `item_code`),
KEY `idx_mimi_category` (`equipment_category_id`),
KEY `idx_mimi_type` (`equipment_type_id`)
) 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 ('1860000000000000141', @mes_equip_pid, '点检及保养项目', '/xslmes/mesXslInspectMaintainItem', 'xslmes/mesXslInspectMaintainItem/MesXslInspectMaintainItemList', NULL, 1, NULL, '1', 11, 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:audit-outlined';
UPDATE `sys_permission` SET `icon` = 'ant-design:audit-outlined' WHERE `id` = '1860000000000000141' 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
('1860000000000000142', '1860000000000000141', '新增', 2, 'mes:mes_xsl_inspect_maintain_item:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000143', '1860000000000000141', '编辑', 2, 'mes:mes_xsl_inspect_maintain_item:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000144', '1860000000000000141', '删除', 2, 'mes:mes_xsl_inspect_maintain_item:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000145', '1860000000000000141', '批量删除', 2, 'mes:mes_xsl_inspect_maintain_item:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000146', '1860000000000000141', '导出', 2, 'mes:mes_xsl_inspect_maintain_item:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000147', '1860000000000000141', '导入', 2, 'mes:mes_xsl_inspect_maintain_item:importExcel', '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 (
'1860000000000000141',
'1860000000000000142', '1860000000000000143', '1860000000000000144', '1860000000000000145',
'1860000000000000146', '1860000000000000147'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,92 @@
-- 母胶计划菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099611', @mixer_parent_id, '母胶计划',
'/mes/masterbatchplaninfo',
'mes/masterbatchplaninfo/index',
'MesXslMasterBatchPlanList', 1, NULL, '1', 34,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099612', '1860000000000099611', '新增', 2, 'xslmes:mes_xsl_master_batch_plan:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099613', '1860000000000099611', '编辑', 2, 'xslmes:mes_xsl_master_batch_plan:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099614', '1860000000000099611', '删除', 2, 'xslmes:mes_xsl_master_batch_plan:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099615', '1860000000000099611', '批量删除', 2, 'xslmes:mes_xsl_master_batch_plan:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099616', '1860000000000099611', '导出', 2, 'xslmes:mes_xsl_master_batch_plan:exportXls', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099611',
'1860000000000099612', '1860000000000099613', '1860000000000099614', '1860000000000099615', '1860000000000099616'
)
AND NOT EXISTS (
SELECT 1
FROM sys_role_permission rp
WHERE rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
AND rp.permission_id = p.id
);
-- 强制修复确保菜单路由与组件路径正确
UPDATE sys_permission
SET
parent_id = @mixer_parent_id,
url = '/mes/masterbatchplaninfo',
component = 'mes/masterbatchplaninfo/index',
component_name = 'MesXslMasterBatchPlanList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099611';

View File

@@ -0,0 +1,30 @@
-- 母胶计划建表SQL
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_master_batch_plan` (
`id` varchar(32) NOT NULL COMMENT '主键',
`source_order_id` varchar(32) DEFAULT NULL COMMENT '来源生产订单ID',
`order_serial_no` varchar(500) DEFAULT NULL COMMENT '订单流水号',
`order_no` varchar(500) DEFAULT NULL COMMENT '订单编号',
`production_segment_count` int DEFAULT NULL COMMENT '生产段数',
`order_date` date DEFAULT NULL COMMENT '订单日期',
`material_code` varchar(500) DEFAULT NULL COMMENT '物料编号',
`mes_material_name` varchar(500) DEFAULT NULL COMMENT 'MES胶料名称',
`plan_weight` decimal(18,4) DEFAULT NULL COMMENT '计划重量',
`per_car_weight` decimal(18,4) DEFAULT NULL COMMENT '每车重量',
`planned_car_count` int DEFAULT 0 COMMENT '计划车数',
`scheduled_car_count` int DEFAULT 0 COMMENT '已排产车数',
`finished_car_count` int DEFAULT 0 COMMENT '完成车数',
`status` int DEFAULT 0 COMMENT '状态0未开始 1进行中 2已完成',
`tenant_id` int DEFAULT NULL COMMENT '租户',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT 0 COMMENT '删除标记0正常1删除',
PRIMARY KEY (`id`),
KEY `idx_mxmbp_source_order` (`source_order_id`),
KEY `idx_mxmbp_material_code` (`material_code`),
UNIQUE KEY `uk_mxmbp_source_order_del` (`source_order_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES母胶计划';

View File

@@ -0,0 +1,92 @@
-- 密炼机动作维护菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099211', @mixer_parent_id, '密炼机动作维护',
'/mes/mixeractioninfo',
'mes/mixeractioninfo/index',
'MesXslMixerActionList', 1, NULL, '1', 30,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099212', '1860000000000099211', '新增', 2, 'xslmes:mes_xsl_mixer_action:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099213', '1860000000000099211', '编辑', 2, 'xslmes:mes_xsl_mixer_action:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099214', '1860000000000099211', '删除', 2, 'xslmes:mes_xsl_mixer_action:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099215', '1860000000000099211', '批量删除', 2, 'xslmes:mes_xsl_mixer_action:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099216', '1860000000000099211', '导出', 2, 'xslmes:mes_xsl_mixer_action:exportXls', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099211',
'1860000000000099212', '1860000000000099213', '1860000000000099214', '1860000000000099215', '1860000000000099216'
)
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/mixeractioninfo',
component = 'mes/mixeractioninfo/index',
component_name = 'MesXslMixerActionList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099211';

View File

@@ -0,0 +1,92 @@
-- 密炼机条件维护菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099411', @mixer_parent_id, '密炼机条件维护',
'/mes/mixerconditioninfo',
'mes/mixerconditioninfo/index',
'MesXslMixerConditionList', 1, NULL, '1', 32,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099412', '1860000000000099411', '新增', 2, 'xslmes:mes_xsl_mixer_condition:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099413', '1860000000000099411', '编辑', 2, 'xslmes:mes_xsl_mixer_condition:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099414', '1860000000000099411', '删除', 2, 'xslmes:mes_xsl_mixer_condition:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099415', '1860000000000099411', '批量删除', 2, 'xslmes:mes_xsl_mixer_condition:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099416', '1860000000000099411', '导出', 2, 'xslmes:mes_xsl_mixer_condition:exportXls', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099411',
'1860000000000099412', '1860000000000099413', '1860000000000099414', '1860000000000099415', '1860000000000099416'
)
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/mixerconditioninfo',
component = 'mes/mixerconditioninfo/index',
component_name = 'MesXslMixerConditionList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099411';

View File

@@ -0,0 +1,93 @@
-- 生产订单菜单与权限挂到MES密炼工程目录
SET NAMES utf8mb4;
SET @mixer_parent_id = (
SELECT id
FROM sys_permission
WHERE name = 'MES密炼工程' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
SET @mixer_parent_id = IFNULL(@mixer_parent_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes' AND menu_type = 0 AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
SET @mixer_parent_id = IFNULL(@mixer_parent_id, '1860000000000000001');
INSERT INTO sys_permission(
id, parent_id, name, url, component, component_name, menu_type, perms, perms_type, sort_no,
is_route, is_leaf, hidden, status, del_flag, keep_alive, internal_or_external, create_by, create_time
)
VALUES (
'1860000000000099511', @mixer_parent_id, '生产订单',
'/mes/productionorderinfo',
'mes/productionorderinfo/index',
'MesXslProductionOrderList', 1, NULL, '1', 33,
1, 1, 0, '1', 0, 1, 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
url = VALUES(url),
component = VALUES(component),
component_name = VALUES(component_name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
sort_no = VALUES(sort_no),
is_route = VALUES(is_route),
is_leaf = VALUES(is_leaf),
hidden = VALUES(hidden),
status = VALUES(status),
del_flag = VALUES(del_flag),
keep_alive = VALUES(keep_alive),
internal_or_external = VALUES(internal_or_external);
INSERT INTO sys_permission(id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time) VALUES
('1860000000000099512', '1860000000000099511', '新增', 2, 'xslmes:mes_xsl_production_order:add', '1', '1', 0, 'admin', NOW()),
('1860000000000099513', '1860000000000099511', '编辑', 2, 'xslmes:mes_xsl_production_order:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000099514', '1860000000000099511', '删除', 2, 'xslmes:mes_xsl_production_order:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000099515', '1860000000000099511', '批量删除', 2, 'xslmes:mes_xsl_production_order:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000099516', '1860000000000099511', '导出', 2, 'xslmes:mes_xsl_production_order:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000099517', '1860000000000099511', '拆分', 2, 'xslmes:mes_xsl_production_order:split', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- admin 角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', p.id, NOW(), '127.0.0.1'
FROM sys_permission p
WHERE p.id IN (
'1860000000000099511',
'1860000000000099512', '1860000000000099513', '1860000000000099514', '1860000000000099515', '1860000000000099516', '1860000000000099517'
)
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/productionorderinfo',
component = 'mes/productionorderinfo/index',
component_name = 'MesXslProductionOrderList',
menu_type = 1,
is_route = 1,
is_leaf = 1,
hidden = 0,
status = '1',
del_flag = 0
WHERE id = '1860000000000099511';

View File

@@ -0,0 +1,64 @@
-- 生产订单拆分按钮权限补丁
SET NAMES utf8mb4;
-- 优先按组件路径定位生产订单菜单
SET @prod_menu_id = (
SELECT id
FROM sys_permission
WHERE component = 'mes/productionorderinfo/index'
AND menu_type = 1
AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
);
-- 兜底按URL定位
SET @prod_menu_id = IFNULL(@prod_menu_id, (
SELECT id
FROM sys_permission
WHERE url = '/mes/productionorderinfo'
AND menu_type = 1
AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
-- 再兜底按名称定位
SET @prod_menu_id = IFNULL(@prod_menu_id, (
SELECT id
FROM sys_permission
WHERE name = '生产订单'
AND menu_type = 1
AND del_flag = 0
ORDER BY create_time ASC
LIMIT 1
));
-- 若找不到页面菜单回退到约定ID你现有脚本中使用
SET @prod_menu_id = IFNULL(@prod_menu_id, '1860000000000099511');
-- 写入/修复拆分按钮权限
INSERT INTO sys_permission (
id, parent_id, name, menu_type, perms, perms_type, status, del_flag, create_by, create_time
) VALUES (
'1860000000000099517', @prod_menu_id, '拆分', 2, 'xslmes:mes_xsl_production_order:split', '1', '1', 0, 'admin', NOW()
)
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
menu_type = VALUES(menu_type),
perms = VALUES(perms),
perms_type = VALUES(perms_type),
status = VALUES(status),
del_flag = VALUES(del_flag);
-- 给admin角色授权
INSERT INTO sys_role_permission(id, role_id, permission_id, operate_date, operate_ip)
SELECT REPLACE(UUID(), '-', ''), 'f6817f48af4fb3af11b9e8bf182f618b', '1860000000000099517', NOW(), '127.0.0.1'
FROM dual
WHERE NOT EXISTS (
SELECT 1
FROM sys_role_permission
WHERE role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
AND permission_id = '1860000000000099517'
);

View File

@@ -0,0 +1,66 @@
-- MES 胶料快检数据点建表 + 菜单质量管理下+ 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀mes:mes_xsl_rubber_quick_test_data_point:*
-- 依赖mes_xsl_rubber_quick_test_type修改租户改 SET @mes_tenant_id
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_data_point` (
`id` varchar(32) NOT NULL COMMENT '主键',
`point_name` varchar(128) NOT NULL COMMENT '数据点名称同租户未删除唯一',
`quick_test_type_id` varchar(32) NOT NULL COMMENT '实验类型 mes_xsl_rubber_quick_test_type.id',
`quick_test_type_name` varchar(128) DEFAULT NULL COMMENT '实验类型名称冗余',
`unit_type` varchar(64) DEFAULT NULL COMMENT '单位类型手填',
`point_desc` 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`),
KEY `idx_mrqtdp_tenant_type` (`tenant_id`, `quick_test_type_id`),
UNIQUE KEY `uk_mrqtdp_tenant_name_del` (`tenant_id`, `point_name`, `del_flag`)
) 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 ('1860000000000000170', @mes_quality_pid, '胶料快检数据点', '/xslmes/mesXslRubberQuickTestDataPoint', 'xslmes/mesXslRubberQuickTestDataPoint/MesXslRubberQuickTestDataPointList', 'MesXslRubberQuickTestDataPointList', 1, NULL, '1', 2, 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` = 0, `hidden` = 0, `status` = '1', `del_flag` = 0,
`keep_alive` = VALUES(`keep_alive`), `internal_or_external` = VALUES(`internal_or_external`);
UPDATE `sys_permission` SET `icon` = 'ant-design:dot-chart-outlined' WHERE `id` = '1860000000000000170' 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
('1860000000000000171', '1860000000000000170', '新增', 2, 'mes:mes_xsl_rubber_quick_test_data_point:add', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000172', '1860000000000000170', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_data_point:edit', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000173', '1860000000000000170', '删除', 2, 'mes:mes_xsl_rubber_quick_test_data_point:delete', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000174', '1860000000000000170', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_data_point:deleteBatch', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000175', '1860000000000000170', '导出', 2, 'mes:mes_xsl_rubber_quick_test_data_point:exportXls', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000176', '1860000000000000170', '导入', 2, 'mes:mes_xsl_rubber_quick_test_data_point: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 (
'1860000000000000170',
'1860000000000000171', '1860000000000000172', '1860000000000000173', '1860000000000000174',
'1860000000000000175', '1860000000000000176'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,27 @@
-- 撤销胶料快检实验方法删除菜单权限字典业务表可整文件一次执行
-- 说明若仅未执行过 V3.9.2_99 / mes-xsl-rubber-quick-test-method-menu-permission.sql执行本脚本即可
-- Flyway 执行过 V3.9.2_99 的环境请同时执行 flyway V3.9.2_100 或本脚本内容一致
SET NAMES utf8mb4;
DELETE FROM `sys_role_permission`
WHERE `permission_id` IN (
'1860000000000000170',
'1860000000000000171', '1860000000000000172', '1860000000000000173',
'1860000000000000174', '1860000000000000175', '1860000000000000176'
);
DELETE FROM `sys_permission`
WHERE `id` IN (
'1860000000000000170',
'1860000000000000171', '1860000000000000172', '1860000000000000173',
'1860000000000000174', '1860000000000000175', '1860000000000000176'
);
DELETE di FROM `sys_dict_item` di
INNER JOIN `sys_dict` d ON di.`dict_id` = d.`id`
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_rotor_type' AND d.`del_flag` = 0;
DELETE FROM `sys_dict`
WHERE `dict_code` = 'xslmes_rubber_quick_test_rotor_type';
DROP TABLE IF EXISTS `mes_xsl_rubber_quick_test_method`;

View File

@@ -0,0 +1,294 @@
-- 胶料快检实验方法主子表字典 + 建表 + 菜单质量管理下+ 按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_rubber_quick_test_method:*
-- 菜单 ID 1860000000000000177与数据点 170 段区分
-- 可与 Flyway V3.9.2_102 重复执行ON DUPLICATE / IF NOT EXISTS
-- SET @mes_tenant_id多租户 admin 授权目标租户
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检转子类型', 'xslmes_rubber_quick_test_rotor_type', '1大转子2小转子', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_rotor_type' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '大转子', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_rotor_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '小转子', '2', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_rotor_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '2');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检扭矩单位', 'xslmes_rubber_quick_test_torque_unit', 'Ib.indNmkg.cmNmmdm', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_torque_unit' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT 'Ib.in' AS txt, 'Ib.in' AS val, 1 AS ord UNION ALL
SELECT 'dNm', 'dNm', 2 UNION ALL
SELECT 'kg.cm', 'kg.cm', 3 UNION ALL
SELECT 'Nm', 'Nm', 4 UNION ALL
SELECT 'mdm', 'mdm', 5
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_torque_unit'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检时间单位', 'xslmes_rubber_quick_test_time_unit', 'secminm:s', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_time_unit' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT 'sec' AS txt, 'sec' AS val, 1 AS ord UNION ALL
SELECT 'min', 'min', 2 UNION ALL
SELECT 'm:s', 'm:s', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_time_unit'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检门尼单位', 'xslmes_rubber_quick_test_mooney_unit', 'MU', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_mooney_unit' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, 'MU', 'MU', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_mooney_unit'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'MU');
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_method` (
`id` varchar(32) NOT NULL COMMENT '主键',
`method_code` varchar(16) NOT NULL COMMENT '方法编号租户内从001递增自动生成',
`method_name` varchar(128) NOT NULL COMMENT '实验方法名称同租户未删除唯一',
`quick_test_type_id` varchar(32) NOT NULL COMMENT '实验类型 mes_xsl_rubber_quick_test_type.id',
`quick_test_type_name` varchar(128) DEFAULT NULL COMMENT '实验类型名称冗余',
`test_temp_c` decimal(12,2) DEFAULT NULL COMMENT '实验温度°C',
`preheat_time_min` decimal(12,2) DEFAULT NULL COMMENT '预热时间min',
`test_time_min` decimal(12,2) DEFAULT NULL COMMENT '实验时间min',
`test_angle_deg` decimal(12,2) DEFAULT NULL COMMENT '实验角度Deg',
`test_freq_hz` decimal(12,2) DEFAULT NULL COMMENT '实验频率Hz',
`rotor_type` varchar(2) DEFAULT NULL COMMENT '转子类型字典xslmes_rubber_quick_test_rotor_type1大转子2小转子',
`rotor_speed_rpm` decimal(12,2) DEFAULT NULL COMMENT '转子速度rpm',
`method_desc` 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`),
KEY `idx_mrqtm_tenant_code` (`tenant_id`, `method_code`),
KEY `idx_mrqtm_type` (`quick_test_type_id`),
UNIQUE KEY `uk_mrqtm_tenant_name_del` (`tenant_id`, `method_name`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检实验方法';
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_method_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`method_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_method.id',
`data_point_id` varchar(32) NOT NULL COMMENT '数据点 mes_xsl_rubber_quick_test_data_point.id',
`point_name` varchar(128) DEFAULT NULL COMMENT '数据点名称冗余只读带出',
`unit_type` varchar(64) DEFAULT NULL COMMENT '单位类型冗余只读带出',
`torque_unit_type` varchar(32) DEFAULT NULL COMMENT '扭矩单位类型字典xslmes_rubber_quick_test_torque_unit',
`time_unit_type` varchar(32) DEFAULT NULL COMMENT '时间单位类型字典xslmes_rubber_quick_test_time_unit',
`mooney_unit_type` varchar(32) DEFAULT NULL COMMENT '门尼单位类型字典xslmes_rubber_quick_test_mooney_unit',
`sort_no` int 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 '更新时间',
PRIMARY KEY (`id`),
KEY `idx_mrqtml_method` (`method_id`),
UNIQUE KEY `uk_mrqtml_method_point` (`method_id`, `data_point_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 ('1860000000000000177', @mes_quality_pid, '胶料快检实验方法', '/xslmes/mesXslRubberQuickTestMethod', 'xslmes/mesXslRubberQuickTestMethod/MesXslRubberQuickTestMethodList', 'MesXslRubberQuickTestMethodList', 1, NULL, '1', 3, 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` = 0, `hidden` = 0, `status` = '1', `del_flag` = 0,
`keep_alive` = VALUES(`keep_alive`), `internal_or_external` = VALUES(`internal_or_external`);
UPDATE `sys_permission` SET `icon` = 'ant-design:profile-outlined' WHERE `id` = '1860000000000000177' 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
('1860000000000000178', '1860000000000000177', '新增', 2, 'mes:mes_xsl_rubber_quick_test_method:add', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000179', '1860000000000000177', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_method:edit', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000180', '1860000000000000177', '删除', 2, 'mes:mes_xsl_rubber_quick_test_method:delete', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000181', '1860000000000000177', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_method:deleteBatch', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000182', '1860000000000000177', '导出', 2, 'mes:mes_xsl_rubber_quick_test_method:exportXls', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000183', '1860000000000000177', '导入', 2, 'mes:mes_xsl_rubber_quick_test_method: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 (
'1860000000000000177',
'1860000000000000178', '1860000000000000179', '1860000000000000180', '1860000000000000181',
'1860000000000000182', '1860000000000000183'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,151 @@
-- 胶料快检记录主子表字典 + 建表 + 菜单质量管理下+ 按钮 + 胶料信息检验按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_rubber_quick_test_record:*
-- 菜单 ID 1860000000000000192
-- SET @mes_tenant_id多租户 admin 授权目标租户
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检记录检验结果', 'xslmes_rubber_quick_test_record_result', '1合格0不合格', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_record_result' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '合格', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '不合格', '0', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_record_result' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班次', 'xslmes_rubber_quick_test_work_shift', '班次', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_shift' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '早班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '中班', '2', 2 UNION ALL
SELECT '晚班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_shift'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检班组', 'xslmes_rubber_quick_test_work_team', '班组', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_work_team' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, v.txt, v.val, v.ord, 1, 'admin', NOW()
FROM `sys_dict` d
CROSS JOIN (
SELECT '甲班' AS txt, '1' AS val, 1 AS ord UNION ALL
SELECT '乙班', '2', 2 UNION ALL
SELECT '丙班', '3', 3
) v
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_work_team'
AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = v.val);
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_no` varchar(32) DEFAULT NULL COMMENT '单号JL+日期+4位流水如JL202605280001',
`rubber_material_id` varchar(32) DEFAULT NULL COMMENT '胶料 mes_material.id',
`rubber_material_name` varchar(128) DEFAULT NULL COMMENT '胶料名称冗余',
`std_id` varchar(32) DEFAULT NULL COMMENT '引用的实验标准 mes_xsl_rubber_quick_test_std.id',
`prod_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '生产机台 mes_xsl_equipment_ledger.id',
`prod_equipment_name` varchar(128) DEFAULT NULL COMMENT '生产机台名称冗余',
`production_date` date DEFAULT NULL COMMENT '生产日期',
`train_no` varchar(64) DEFAULT NULL COMMENT '车次编号',
`work_shift` varchar(8) DEFAULT NULL COMMENT '班次字典xslmes_rubber_quick_test_work_shift',
`work_team` varchar(8) DEFAULT NULL COMMENT '班组字典xslmes_rubber_quick_test_work_team',
`inspect_times` int DEFAULT NULL COMMENT '检验次数',
`inspect_time` datetime DEFAULT NULL COMMENT '检验时间',
`inspector_user_id` varchar(32) DEFAULT NULL COMMENT '检验人用户ID',
`inspector_username` varchar(64) DEFAULT NULL COMMENT '检验人账号冗余',
`inspector_realname` varchar(64) DEFAULT NULL COMMENT '检验人姓名冗余',
`quick_test_type_id` varchar(32) DEFAULT NULL COMMENT '检验类型 mes_xsl_rubber_quick_test_type.id',
`quick_test_type_name` varchar(128) DEFAULT NULL COMMENT '检验类型名称冗余',
`inspect_result` varchar(2) DEFAULT NULL COMMENT '检验结果字典xslmes_rubber_quick_test_record_result1合格0不合格',
`production_plan_no` varchar(100) DEFAULT NULL COMMENT '生产计划号',
`inspect_equipment_ledger_id` varchar(32) DEFAULT NULL COMMENT '检验机台 mes_xsl_equipment_ledger.id',
`inspect_equipment_name` varchar(128) DEFAULT NULL COMMENT '检验机台名称冗余',
`rubber_card_no` varchar(100) DEFAULT NULL COMMENT '胶料卡片号',
`rubber_batch_no` varchar(100) 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_mrqtr_record_no` (`record_no`),
KEY `idx_mrqtr_material` (`rubber_material_id`),
KEY `idx_mrqtr_std` (`std_id`),
KEY `idx_mrqtr_tenant` (`tenant_id`),
KEY `idx_mrqtr_inspect_time` (`inspect_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录';
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`record_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_record.id',
`data_point_id` varchar(32) DEFAULT NULL COMMENT '数据点 mes_xsl_rubber_quick_test_data_point.id',
`inspect_item` varchar(128) DEFAULT NULL COMMENT '检验项目数据点名称只读带出',
`lower_limit` decimal(18,6) DEFAULT NULL COMMENT '检验下限只读带出',
`inspect_value` decimal(18,6) DEFAULT NULL COMMENT '检验值',
`upper_limit` decimal(18,6) DEFAULT NULL COMMENT '检验上限只读带出',
`sort_no` int 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 '更新时间',
PRIMARY KEY (`id`),
KEY `idx_mrqtrl_record` (`record_id`),
UNIQUE KEY `uk_mrqtrl_record_point` (`record_id`, `data_point_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 ('1860000000000000192', @mes_quality_pid, '胶料快检记录', '/xslmes/mesXslRubberQuickTestRecord', 'xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecordList', 'MesXslRubberQuickTestRecordList', 1, NULL, '1', 5, 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` = VALUES(`is_leaf`), `keep_alive` = VALUES(`keep_alive`);
UPDATE `sys_permission` SET `icon` = 'ant-design:file-search-outlined' WHERE `id` = '1860000000000000192' 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
('1860000000000000193', '1860000000000000192', '新增', 2, 'mes:mes_xsl_rubber_quick_test_record:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000194', '1860000000000000192', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_record:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000195', '1860000000000000192', '删除', 2, 'mes:mes_xsl_rubber_quick_test_record:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000196', '1860000000000000192', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_record:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000197', '1860000000000000192', '导出', 2, 'mes:mes_xsl_rubber_quick_test_record:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000198', '1860000000000000192', '导入', 2, 'mes:mes_xsl_rubber_quick_test_record:importExcel', '1', '1', 0, 'admin', NOW()),
('1860000000000000199', '1860000000000000192', '从胶料生成', 2, 'mes:mes_xsl_rubber_quick_test_record:batchFromMaterial', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `status`, `del_flag`, `create_by`, `create_time`) VALUES
('1860000000000000200', '1860000000000000011', '胶料快检', 2, 'mes:mes_material:rubberQuickTestInspect', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
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 (
'1860000000000000192',
'1860000000000000193', '1860000000000000194', '1860000000000000195', '1860000000000000196',
'1860000000000000197', '1860000000000000198', '1860000000000000199',
'1860000000000000200'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
);

View File

@@ -0,0 +1,130 @@
-- 胶料快检实验标准主子表字典 + 建表 + 菜单质量管理下+ 按钮 + 租户 admin 授权
-- 权限前缀mes:mes_xsl_rubber_quick_test_std:*
-- 菜单 ID 1860000000000000184实验方法占用 177-183
-- 可与 Flyway V3.9.2_103 重复执行ON DUPLICATE / IF NOT EXISTS
-- SET @mes_tenant_id多租户 admin 授权目标租户
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检密炼机类型', 'xslmes_rubber_quick_test_mixer_type', '1普通密炼机2串密炼机', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_mixer_type' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '普通密炼机', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_mixer_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '串密炼机', '2', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_mixer_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '2');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检标准启用状态', 'xslmes_rubber_quick_test_std_enable_status', '1使用中0已停用', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_std_enable_status' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '使用中', '1', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_std_enable_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '已停用', '0', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_std_enable_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料快检标准审核状态', 'xslmes_rubber_quick_test_std_audit_status', '0草稿1已批准', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_quick_test_std_audit_status' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '草稿', '0', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_std_audit_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '0');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '已批准', '1', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_quick_test_std_audit_status' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = '1');
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_std` (
`id` varchar(32) NOT NULL COMMENT '主键',
`std_name` varchar(128) NOT NULL COMMENT '实验标准名称同租户未删除唯一',
`test_method_id` varchar(32) NOT NULL COMMENT '实验方法 mes_xsl_rubber_quick_test_method.id',
`test_method_name` varchar(128) DEFAULT NULL COMMENT '实验方法名称冗余',
`mixer_type` varchar(2) DEFAULT NULL COMMENT '密炼机类型字典xslmes_rubber_quick_test_mixer_type1普通密炼机2串密炼机',
`rubber_material_id` varchar(32) DEFAULT NULL COMMENT '胶料 mes_material.id',
`rubber_material_name` varchar(128) DEFAULT NULL COMMENT '胶料名称冗余',
`ps_compile_id` varchar(32) DEFAULT NULL COMMENT '密炼PS编制 mes_xsl_mixer_ps_compile.id',
`issue_number` varchar(100) DEFAULT NULL COMMENT '发行编号密炼PS编码冗余',
`issue_date` date DEFAULT NULL COMMENT '发行日期',
`issue_dept_id` varchar(32) DEFAULT NULL COMMENT '发行部门ID',
`issue_dept_name` varchar(200) DEFAULT NULL COMMENT '发行部门名称冗余',
`enable_status` varchar(2) DEFAULT '1' COMMENT '启用状态字典xslmes_rubber_quick_test_std_enable_status1使用中0已停用',
`audit_status` varchar(2) DEFAULT '0' COMMENT '审核状态字典xslmes_rubber_quick_test_std_audit_status0草稿1已批准',
`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_mrqts_method` (`test_method_id`),
KEY `idx_mrqts_material` (`rubber_material_id`),
KEY `idx_mrqts_tenant` (`tenant_id`),
UNIQUE KEY `uk_mrqts_tenant_name_del` (`tenant_id`, `std_name`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检实验标准';
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_std_line` (
`id` varchar(32) NOT NULL COMMENT '主键',
`std_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_std.id',
`data_point_id` varchar(32) NOT NULL COMMENT '数据点 mes_xsl_rubber_quick_test_data_point.id',
`point_name` varchar(128) DEFAULT NULL COMMENT '数据点名称冗余只读带出',
`lower_limit` decimal(18,6) DEFAULT NULL COMMENT '下限值',
`lower_warn` decimal(18,6) DEFAULT NULL COMMENT '下警告值',
`target_value` decimal(18,6) DEFAULT NULL COMMENT '目标值',
`upper_warn` decimal(18,6) DEFAULT NULL COMMENT '上警告值',
`upper_limit` decimal(18,6) DEFAULT NULL COMMENT '上限值',
`sort_no` int 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 '更新时间',
PRIMARY KEY (`id`),
KEY `idx_mrqtsl_std` (`std_id`),
UNIQUE KEY `uk_mrqtsl_std_point` (`std_id`, `data_point_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 ('1860000000000000184', @mes_quality_pid, '胶料快检实验标准', '/xslmes/mesXslRubberQuickTestStd', 'xslmes/mesXslRubberQuickTestStd/MesXslRubberQuickTestStdList', 'MesXslRubberQuickTestStdList', 1, NULL, '1', 4, 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` = VALUES(`is_leaf`), `keep_alive` = VALUES(`keep_alive`);
UPDATE `sys_permission` SET `icon` = 'ant-design:file-protect-outlined' WHERE `id` = '1860000000000000184' 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
('1860000000000000185', '1860000000000000184', '新增', 2, 'mes:mes_xsl_rubber_quick_test_std:add', '1', '1', 0, 'admin', NOW()),
('1860000000000000186', '1860000000000000184', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_std:edit', '1', '1', 0, 'admin', NOW()),
('1860000000000000187', '1860000000000000184', '删除', 2, 'mes:mes_xsl_rubber_quick_test_std:delete', '1', '1', 0, 'admin', NOW()),
('1860000000000000188', '1860000000000000184', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_std:deleteBatch', '1', '1', 0, 'admin', NOW()),
('1860000000000000189', '1860000000000000184', '导出', 2, 'mes:mes_xsl_rubber_quick_test_std:exportXls', '1', '1', 0, 'admin', NOW()),
('1860000000000000190', '1860000000000000184', '导入', 2, 'mes:mes_xsl_rubber_quick_test_std:importExcel', '1', '1', 0, 'admin', NOW()),
('1860000000000000191', '1860000000000000184', '启用停用', 2, 'mes:mes_xsl_rubber_quick_test_std:updateStatus', '1', '1', 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `name` = VALUES(`name`);
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 (
'1860000000000000184',
'1860000000000000185', '1860000000000000186', '1860000000000000187', '1860000000000000188',
'1860000000000000189', '1860000000000000190', '1860000000000000191'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
);

View File

@@ -0,0 +1,87 @@
-- MES 胶料快检实验类型建表 + 质量管理目录 + 子菜单 + 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀与 Controller前端 v-auth 一致mes:mes_xsl_rubber_quick_test_type:*
-- 修改租户 SET @mes_tenant_id
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_type` (
`id` varchar(32) NOT NULL COMMENT '主键',
`type_code` varchar(16) NOT NULL COMMENT '实验类型编号租户内从001递增自动生成',
`type_name` varchar(128) 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_mrqtt_tenant_code` (`tenant_id`, `type_code`),
KEY `idx_mrqtt_tenant_name` (`tenant_id`, `type_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检实验类型';
SET @mes_tenant_id = 1002;
SET @mes_root_parent = (
SELECT `parent_id` FROM `sys_permission`
WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = 'MES基础资料'
LIMIT 1
);
SET @mes_root_parent = IFNULL(@mes_root_parent, (
SELECT `parent_id` FROM `sys_permission`
WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` = 'MES资料'
LIMIT 1
));
SET @mes_root_parent = IFNULL(@mes_root_parent, '1860000000000000001');
SET @mes_quality_sort = IFNULL((
SELECT `sort_no` + 1 FROM `sys_permission`
WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` IN ('MES基础资料', 'MES资料', '设备管理')
ORDER BY `sort_no` DESC
LIMIT 1
), 52);
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 ('1860000000000000162', @mes_root_parent, '质量管理', '/xslmes/quality', 'layouts/RouteView', 'MesQualityLayout', 0, NULL, '1', @mes_quality_sort, 1, 0, 0, '1', 0, 0, 0, 'admin', NOW())
ON DUPLICATE KEY UPDATE
`parent_id` = VALUES(`parent_id`), `name` = VALUES(`name`), `url` = VALUES(`url`), `component` = VALUES(`component`),
`menu_type` = VALUES(`menu_type`), `is_leaf` = 0, `hidden` = 0, `status` = '1', `del_flag` = 0;
UPDATE `sys_permission` SET `icon` = 'ant-design:safety-certificate-outlined' WHERE `id` = '1860000000000000162' AND `del_flag` = 0;
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 ('1860000000000000163', '1860000000000000162', '胶料快检实验类型', '/xslmes/mesXslRubberQuickTestType', 'xslmes/mesXslRubberQuickTestType/MesXslRubberQuickTestTypeList', 'MesXslRubberQuickTestTypeList', 1, NULL, '1', 1, 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` = 0, `hidden` = 0, `status` = '1', `del_flag` = 0,
`keep_alive` = VALUES(`keep_alive`), `internal_or_external` = VALUES(`internal_or_external`);
UPDATE `sys_permission` SET `icon` = 'ant-design:experiment-outlined' WHERE `id` = '1860000000000000163' 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
('1860000000000000164', '1860000000000000163', '新增', 2, 'mes:mes_xsl_rubber_quick_test_type:add', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000165', '1860000000000000163', '编辑', 2, 'mes:mes_xsl_rubber_quick_test_type:edit', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000166', '1860000000000000163', '删除', 2, 'mes:mes_xsl_rubber_quick_test_type:delete', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000167', '1860000000000000163', '批量删除', 2, 'mes:mes_xsl_rubber_quick_test_type:deleteBatch', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000168', '1860000000000000163', '导出', 2, 'mes:mes_xsl_rubber_quick_test_type:exportXls', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000169', '1860000000000000163', '导入', 2, 'mes:mes_xsl_rubber_quick_test_type: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 (
'1860000000000000162',
'1860000000000000163',
'1860000000000000164', '1860000000000000165', '1860000000000000166', '1860000000000000167',
'1860000000000000168', '1860000000000000169'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -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`
);

View File

@@ -0,0 +1,87 @@
-- MES 胶料小料锁定原因字典 + 建表 + 菜单质量管理下+ 按钮 + 租户 admin 授权可整文件一次执行
-- 权限前缀mes:mes_xsl_rubber_small_lock_reason:*
-- 菜单 ID 1860000000000000208按钮 209-214
-- 修改租户 SET @mes_tenant_id
SET NAMES utf8mb4;
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料小料锁定类型', 'xslmes_rubber_small_lock_type', '锁定/解锁', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_small_lock_type' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '锁定', 'lock', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_small_lock_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'lock');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '解锁', 'unlock', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_small_lock_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'unlock');
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
SELECT REPLACE(UUID(), '-', ''), 'MES胶料小料锁定条码类型', 'xslmes_rubber_small_lock_barcode_type', '小料/胶料', 0, 'admin', NOW(), 0, 0
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_rubber_small_lock_barcode_type' AND `del_flag` = 0);
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '小料', 'small', 1, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_small_lock_barcode_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'small');
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
SELECT REPLACE(UUID(), '-', ''), d.id, '胶料', 'rubber', 2, 1, 'admin', NOW() FROM `sys_dict` d
WHERE d.`dict_code` = 'xslmes_rubber_small_lock_barcode_type' AND NOT EXISTS (SELECT 1 FROM `sys_dict_item` i WHERE i.`dict_id` = d.id AND i.`item_value` = 'rubber');
CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_small_lock_reason` (
`id` varchar(32) NOT NULL COMMENT '主键',
`reason_code` varchar(16) NOT NULL COMMENT '编号租户内从001递增自动生成只读',
`lock_type` varchar(16) NOT NULL COMMENT '类型字典xslmes_rubber_small_lock_typelock锁定unlock解锁',
`barcode_type` varchar(16) NOT NULL COMMENT '条码类型字典xslmes_rubber_small_lock_barcode_typesmall小料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 '创建人',
`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_mrslr_tenant_code` (`tenant_id`, `reason_code`),
KEY `idx_mrslr_tenant_lock` (`tenant_id`, `lock_type`, `barcode_type`)
) 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 ('1860000000000000208', @mes_quality_pid, '胶料小料锁定原因', '/xslmes/mesXslRubberSmallLockReason', 'xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLockReasonList', 'MesXslRubberSmallLockReasonList', 1, NULL, '1', 6, 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`), `sort_no` = VALUES(`sort_no`), `is_route` = VALUES(`is_route`), `is_leaf` = 0, `hidden` = 0, `status` = '1', `del_flag` = 0, `keep_alive` = VALUES(`keep_alive`);
UPDATE `sys_permission` SET `icon` = 'ant-design:lock-outlined' WHERE `id` = '1860000000000000208' 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
('1860000000000000209', '1860000000000000208', '新增', 2, 'mes:mes_xsl_rubber_small_lock_reason:add', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000210', '1860000000000000208', '编辑', 2, 'mes:mes_xsl_rubber_small_lock_reason:edit', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000211', '1860000000000000208', '删除', 2, 'mes:mes_xsl_rubber_small_lock_reason:delete', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000212', '1860000000000000208', '批量删除', 2, 'mes:mes_xsl_rubber_small_lock_reason:deleteBatch', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000213', '1860000000000000208', '导出', 2, 'mes:mes_xsl_rubber_small_lock_reason:exportXls', '1', 1, '1', 0, 'admin', NOW()),
('1860000000000000214', '1860000000000000208', '导入', 2, 'mes:mes_xsl_rubber_small_lock_reason: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 (
'1860000000000000208',
'1860000000000000209', '1860000000000000210', '1860000000000000211', '1860000000000000212',
'1860000000000000213', '1860000000000000214'
)
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp
WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -22,6 +22,7 @@ import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.*;
import java.sql.Date;
import java.util.*;
@@ -1046,7 +1047,7 @@ public class oConvertUtils {
BigDecimal bigDecimal = new BigDecimal(uploadCount);
//换算成MB
BigDecimal divide = bigDecimal.divide(new BigDecimal(1048576));
count = divide.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
count = divide.setScale(2, RoundingMode.HALF_UP).doubleValue();
return count;
}
return count;

View File

@@ -211,6 +211,10 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/xslmes/mesXslWarehouse/anon/**", "anon");
// MES库区管理免密接口供桌面端调用
filterChainDefinitionMap.put("/xslmes/mesXslWarehouseArea/anon/**", "anon");
// MES密炼物料皮重策略免密接口供桌面端调用
filterChainDefinitionMap.put("/xslmes/mesXslMixerMaterialTareStrategy/anon/**", "anon");
// MES单位只读免密接口供桌面端单位下拉调用
filterChainDefinitionMap.put("/xslmes/mesXslUnit/anon/**", "anon");
// MES密炼物料管理免密接口供桌面端调用
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
// 打印模板免密接口(供桌面端调用)

File diff suppressed because it is too large Load Diff

View File

@@ -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 可兼容** | 不破坏现有 CallbackOnline 表与 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 配置发布流程
```
草稿方案 → 测试预览选源单IDdry-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 走 ServiceOnline 走 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 推进更新版本号与交付状态。*

View File

@@ -17,8 +17,15 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<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>

View File

@@ -0,0 +1,38 @@
package org.jeecg.modules.xslmes.approval.action;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 审批联动业务动作标注。
*
* <p>把本注解打在业务 Controller 的处理方法上,即声明「该按钮/接口可被审批流程作为回调动作选择」。
* 系统启动时会反射扫描所有带本注解的接口方法,取其 {@code @RequestMapping} 真实路径与 HTTP 方法,
* 按 {@link #table()} 归类,供审批流设计器的节点「回调接口」下拉选择。</p>
*
* <p>因为基于 Spring 运行时反射 + 真实映射路径生产环境天然可用URL 绝对准确,无需任何源码解析。</p>
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批联动业务动作注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApprovalBizAction {
/** 动作展示名(如「批准」「反审核」「启用」),供设计器下拉显示 */
String name();
/** 所属业务表名(如 mes_xsl_rubber_quick_test_std与审批流绑定的 bizTable 对应 */
String table();
/**
* 适用触发时机,可多选;为空表示三种时机均可选用。
* 取值onNodeApprove(本节点通过) / onApprove(最终通过) / onReject(驳回)
*/
String[] phase() default {};
/** 排序,越小越靠前 */
int order() default 0;
}

View File

@@ -0,0 +1,154 @@
package org.jeecg.modules.xslmes.approval.action;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 审批联动业务动作注册表。
*
* <p>容器启动完成后,反射扫描所有带 {@link ApprovalBizAction} 的接口方法,
* 取真实映射路径 + HTTP 方法,按业务表归类缓存,供设计器查询选择。</p>
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批联动业务动作注册表
*/
@Slf4j
@Component
public class ApprovalBizActionRegistry {
@Resource
private ApplicationContext applicationContext;
/** 业务表 -> 动作列表 */
private final Map<String, List<ApprovalBizActionVo>> byTable = new ConcurrentHashMap<>();
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed() {
try {
scan();
} catch (Exception e) {
log.warn("[审批联动] 扫描 @ApprovalBizAction 失败", e);
}
}
private synchronized void scan() {
byTable.clear();
RequestMappingHandlerMapping mapping;
try {
mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
} catch (Exception e) {
log.warn("[审批联动] 未取到 RequestMappingHandlerMapping跳过扫描", e);
return;
}
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
HandlerMethod hm = entry.getValue();
ApprovalBizAction ann = hm.getMethodAnnotation(ApprovalBizAction.class);
if (ann == null || ann.table() == null || ann.table().isEmpty()) {
continue;
}
RequestMappingInfo info = entry.getKey();
String url = resolveUrl(info);
if (url == null || url.isEmpty()) {
continue;
}
ApprovalBizActionVo vo = new ApprovalBizActionVo();
vo.setName(ann.name());
vo.setTable(ann.table());
vo.setPhase(ann.phase());
vo.setOrder(ann.order());
vo.setUrl(url);
vo.setMethod(resolveMethod(info));
vo.setPerms(resolvePerms(hm));
byTable.computeIfAbsent(ann.table(), k -> new ArrayList<>()).add(vo);
}
// 排序
for (List<ApprovalBizActionVo> list : byTable.values()) {
list.sort(Comparator.comparingInt(ApprovalBizActionVo::getOrder));
}
int total = byTable.values().stream().mapToInt(List::size).sum();
log.info("[审批联动] 已注册业务动作 {} 个,覆盖 {} 张业务表", total, byTable.size());
}
/** 取第一个映射路径(不含 context-path) */
private String resolveUrl(RequestMappingInfo info) {
Set<String> patterns = info.getPatternValues();
if (patterns == null || patterns.isEmpty()) {
return null;
}
return patterns.iterator().next();
}
/** 取 HTTP 方法,默认 POST */
private String resolveMethod(RequestMappingInfo info) {
RequestMethodsRequestCondition cond = info.getMethodsCondition();
if (cond == null || cond.getMethods().isEmpty()) {
return "POST";
}
return cond.getMethods().iterator().next().name();
}
/** 取接口权限标识(@RequiresPermissions) */
private String resolvePerms(HandlerMethod hm) {
RequiresPermissions rp = hm.getMethodAnnotation(RequiresPermissions.class);
if (rp != null && rp.value().length > 0) {
return rp.value()[0];
}
return null;
}
/** 按业务表取可选动作 */
public List<ApprovalBizActionVo> getByTable(String table) {
if (table == null) {
return Collections.emptyList();
}
List<ApprovalBizActionVo> list = byTable.get(table);
return list == null ? Collections.emptyList() : new ArrayList<>(list);
}
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回统一回退按表+时机自动取业务动作-----------
/**
* 按业务表 + 时机取动作(如驳回统一执行 onReject 动作,无需在每个流程节点手动配置)。
*/
public List<ApprovalBizActionVo> getByTableAndPhase(String table, String phase) {
if (table == null || phase == null) {
return Collections.emptyList();
}
List<ApprovalBizActionVo> list = byTable.get(table);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
List<ApprovalBizActionVo> result = new ArrayList<>();
for (ApprovalBizActionVo vo : list) {
String[] phases = vo.getPhase();
if (phases == null) {
continue;
}
for (String p : phases) {
if (phase.equals(p)) {
result.add(vo);
break;
}
}
}
return result;
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回统一回退按表+时机自动取业务动作-----------
}

View File

@@ -0,0 +1,38 @@
package org.jeecg.modules.xslmes.approval.action;
import lombok.Data;
import java.io.Serializable;
/**
* 审批联动业务动作(供设计器节点「回调接口」下拉选择)。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批联动业务动作VO
*/
@Data
public class ApprovalBizActionVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 动作名(如「批准」) */
private String name;
/** 接口真实路径(取自 @RequestMapping不含 context-path如 /xslmes/xxx/updateStatus */
private String url;
/** HTTP 方法GET/POST/PUT/DELETE */
private String method;
/** 所属业务表 */
private String table;
/** 适用时机onNodeApprove/onApprove/onReject空表示均可 */
private String[] phase;
/** 接口权限标识(取自 @RequiresPermissions可空 */
private String perms;
/** 排序 */
private int order;
}

View File

@@ -0,0 +1,27 @@
package org.jeecg.modules.xslmes.approval.callback;
import org.springframework.context.ApplicationEvent;
/**
* 审批动作领域事件。
* 与 {@link IApprovalBizCallback} 等价的另一种接入方式:
* 偏好松耦合的业务可使用 {@code @EventListener} 监听本事件(同步、同事务)。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
public class ApprovalActionEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final ApprovalCallbackContext context;
public ApprovalActionEvent(Object source, ApprovalCallbackContext context) {
super(source);
this.context = context;
}
public ApprovalCallbackContext getContext() {
return context;
}
}

View File

@@ -0,0 +1,223 @@
package org.jeecg.modules.xslmes.approval.callback;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
/**
* 审批节点「回调接口」HTTP 执行器。
*
* 审批到对应时机时读取节点配置中录制好的业务接口url+method
* 以「当前审批处理人」的登录态(透传当前请求的 X-Access-Token内部调用该接口
* 自动带上单据ID覆盖 id 参数),从而真实执行业务页面按钮背后的逻辑。
*
* 限制:自动流转 / 无人值守节点(无登录态请求上下文)时降级跳过并记录日志。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】节点回调接口内部调用执行
*/
@Slf4j
@Component
public class ApprovalActionHttpExecutor {
@Value("${server.port:8080}")
private int serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 执行某节点在指定时机配置的所有回调接口。
*
* @param node 流程节点 JSON含 props.callbackActions
* @param phase 时机onNodeApprove / onApprove / onReject
* @param inst 审批实例
*/
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
/**
* 判断节点在指定时机是否配置了回调接口(用于决定是否由业务回调全权负责回退)。
*/
public boolean hasActions(JSONObject node, String phase) {
if (node == null) {
return false;
}
JSONObject propsObj = node.getJSONObject("props");
if (propsObj == null) {
return false;
}
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
if (callbackActions == null) {
return false;
}
JSONArray actions = callbackActions.getJSONArray(phase);
return actions != null && !actions.isEmpty();
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
if (node == null || inst == null) {
return;
}
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;
}
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
if (callbackActions == null) {
return;
}
JSONArray actions = callbackActions.getJSONArray(phase);
if (actions == null || actions.isEmpty()) {
return;
}
for (int i = 0; i < actions.size(); i++) {
JSONObject action = actions.getJSONObject(i);
if (action == null) {
continue;
}
String url = action.getString("url");
if (oConvertUtils.isEmpty(url)) {
continue;
}
if (oConvertUtils.isEmpty(token)) {
log.warn("[审批回调] 无登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, bizDataId);
continue;
}
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
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审批流设计】驳回统一回退按表注解自动调用业务接口-----------
/**
* 直接按 url+method 调用业务接口(用于驳回统一回退,动作来自 @ApprovalBizAction 注解而非节点配置)。
* 无当前处理人登录态时降级跳过。
*/
public void runByUrl(String method, String url, MesXslApprovalInstance inst) {
if (oConvertUtils.isEmpty(url) || inst == null) {
return;
}
String token = currentToken();
if (oConvertUtils.isEmpty(token)) {
log.warn("[审批回调] 无当前处理人登录态,跳过驳回业务回退 url={}, bizId={}", url, inst.getBizDataId());
return;
}
String httpMethod = oConvertUtils.getString(method, "POST").toUpperCase();
invoke(httpMethod, url, null, inst.getBizDataId(), token);
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回统一回退按表注解自动调用业务接口-----------
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
String fullUrl = buildFullUrl(url);
HttpMethod httpMethod = HttpMethod.valueOf(method);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
headers.add(HttpHeaders.AUTHORIZATION, token);
// 统一在 url 上附带单据ID的常见参数名兼容 @RequestParam(id/ids/dataId) 形式的接口
// 如密炼PS的 /proofread、/audit、/approve 用的是 @RequestParam ids
String idValue = oConvertUtils.getString(bizDataId, "");
String sep = fullUrl.contains("?") ? "&" : "?";
fullUrl = fullUrl + sep + "id=" + idValue + "&ids=" + idValue + "&dataId=" + idValue;
Object bodyToSend = null;
if (!("GET".equals(method) || "DELETE".equals(method))) {
// 写操作:合并录制的 body并用单据ID覆盖 id兼容 @RequestBody 形式)
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
body.put("id", bizDataId);
bodyToSend = body;
}
try {
HttpEntity<Object> entity = new HttpEntity<>(bodyToSend, headers);
ResponseEntity<String> resp = restTemplate.exchange(fullUrl, httpMethod, entity, String.class);
String respBody = resp.getBody();
if (!resp.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("回调接口返回非2xx" + resp.getStatusCode());
}
// Jeecg 统一返回 {success:false,...} 视为业务失败
if (oConvertUtils.isNotEmpty(respBody)) {
try {
JSONObject r = JSONObject.parseObject(respBody);
if (r != null && r.containsKey("success") && Boolean.FALSE.equals(r.getBoolean("success"))) {
throw new RuntimeException("回调接口业务失败:" + oConvertUtils.getString(r.getString("message"), "未知错误"));
}
} catch (RuntimeException re) {
throw re;
} catch (Exception ignore) {
// 非JSON响应不强校验
}
}
log.info("[审批回调] 已调用业务接口成功 {} {} bizId={}", method, fullUrl, bizDataId);
} catch (RuntimeException e) {
log.error("[审批回调] 调用业务接口失败 {} {} bizId={}", method, fullUrl, bizDataId, e);
// 抛出以回滚整个审批动作,保证审批与业务一致
throw e;
}
}
/** 构建内部调用绝对地址http://127.0.0.1:port + context-path + url */
private String buildFullUrl(String url) {
String ctx = oConvertUtils.getString(contextPath, "");
if (oConvertUtils.isNotEmpty(ctx) && !ctx.startsWith("/")) {
ctx = "/" + ctx;
}
String path = url.startsWith("/") ? url : "/" + url;
// 录制到的路径已含 context-path 时不重复拼接
if (oConvertUtils.isNotEmpty(ctx) && (path.equals(ctx) || path.startsWith(ctx + "/"))) {
ctx = "";
}
return "http://127.0.0.1:" + serverPort + ctx + path;
}
/** 取当前请求的登录 token处理人身份 */
private String currentToken() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return null;
}
HttpServletRequest request = attrs.getRequest();
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
if (oConvertUtils.isEmpty(token)) {
token = request.getHeader(HttpHeaders.AUTHORIZATION);
}
return token;
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,114 @@
package org.jeecg.modules.xslmes.approval.callback;
import lombok.Data;
import lombok.experimental.Accessors;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import java.io.Serializable;
import java.util.Date;
/**
* 审批回调上下文。
* 由审批引擎在「节点通过 / 最终通过 / 驳回」时构建并传给业务回调,
* 业务模块据此调用自身已有的审核/回写接口,实现审批与业务功能的统一联动。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
@Data
@Accessors(chain = true)
public class ApprovalCallbackContext implements Serializable {
private static final long serialVersionUID = 1L;
/** 回调动作类型 */
public enum Action {
/** 单个审批节点通过(中间态,每个节点都会触发一次) */
NODE_APPROVED,
/** 整个流程最终通过 */
APPROVED,
/** 被驳回(任一节点驳回即终止) */
REJECTED,
//update-begin---author:GHT ---date:2026-06-08 for【风险修复-R5】新增CANCELLED动作支持撤销时触发业务回滚回调-----------
/** 流程被撤销/终止TERMINATED */
CANCELLED
//update-end---author:GHT ---date:2026-06-08 for【风险修复-R5】新增CANCELLED动作支持撤销时触发业务回滚回调-----------
}
/** 回调动作 */
private Action action;
/** 审批实例ID */
private String instanceId;
/** 审批流定义ID */
private String flowId;
/** 审批流名称 */
private String flowName;
/** 业务单据表名 */
private String bizTable;
/** 业务单据中文名 */
private String bizTableName;
/** 业务单据记录ID业务表主键 */
private String bizDataId;
/** 业务单据展示标题 */
private String bizTitle;
/** 当前/刚处理的节点ID */
private String nodeId;
/** 当前/刚处理的节点名称 */
private String nodeName;
//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;
/** 发起人 username */
private String applyUser;
/** 是否为流程最终结束APPROVED/REJECTED 时为 true */
private boolean finalResult;
/** 完整审批实例(供业务读取租户、发起信息等) */
private transient MesXslApprovalInstance instance;
//update-begin---author:GHT ---date:2026-06-08 for【缺陷修复-D1/D2】新增token和activityId字段支持钉钉回调时传递真实审批人身份及节点精确定位-----------
/**
* 操作人JWT Token钉钉回调时为审批人真实身份TokenMES内部审批时为null
* 供 ApprovalActionHttpExecutor 等需要身份的调用方使用。
*/
private transient String token;
/**
* 钉钉任务节点IDoperationRecords[].activityId仅钉钉通道有值
* 可供集成引擎或业务回调按节点精确匹配。
*/
private String activityId;
//update-end---author:GHT ---date:2026-06-08 for【缺陷修复-D1/D2】新增token和activityId字段支持钉钉回调时传递真实审批人身份及节点精确定位-----------
}

View File

@@ -0,0 +1,176 @@
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;
import java.util.ArrayList;
import java.util.List;
/**
* 审批业务回调分发器。
* 统一在审批引擎流转关键点调用:
* <ol>
* <li>按业务表名路由到对应的 {@link IApprovalBizCallback} 实现(强类型,业务直接调用自身接口);</li>
* <li>同时发布 {@link ApprovalActionEvent} 供 {@code @EventListener} 松耦合监听。</li>
* </ol>
* 回调与审批状态变更同事务执行,回调异常将向上抛出以回滚整个审批动作。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
@Slf4j
@Component
public class ApprovalCallbackDispatcher {
/** 监听所有业务表的通配符 */
private static final String ANY_TABLE = "*";
private static final String DING_LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
private final ObjectProvider<List<IApprovalBizCallback>> callbacksProvider;
private final ApplicationEventPublisher eventPublisher;
public ApprovalCallbackDispatcher(ObjectProvider<List<IApprovalBizCallback>> callbacksProvider,
ApplicationEventPublisher eventPublisher) {
this.callbacksProvider = callbacksProvider;
this.eventPublisher = eventPublisher;
}
/** 节点通过(中间态) */
public void fireNodeApproved(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.NODE_APPROVED);
ctx.setFinalResult(false);
dispatch(ctx);
}
/** 流程最终通过 */
public void fireApproved(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.APPROVED);
ctx.setFinalResult(true);
dispatch(ctx);
}
/** 驳回 */
public void fireRejected(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.REJECTED);
ctx.setFinalResult(true);
dispatch(ctx);
}
//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 : 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) {
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;
}
}
private List<IApprovalBizCallback> matchedCallbacks(String bizTable) {
List<IApprovalBizCallback> all = callbacksProvider.getIfAvailable();
List<IApprovalBizCallback> matched = new ArrayList<>();
if (all == null) {
return matched;
}
for (IApprovalBizCallback cb : all) {
String support = cb.supportTable();
if (ANY_TABLE.equals(support) || (support != null && support.equalsIgnoreCase(bizTable))) {
matched.add(cb);
}
}
return matched;
}
private void invoke(IApprovalBizCallback cb, ApprovalCallbackContext ctx) {
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:
cb.onNodeApproved(ctx);
break;
case APPROVED:
cb.onApproved(ctx);
break;
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) {
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来源回调-----------
}

View File

@@ -0,0 +1,74 @@
package org.jeecg.modules.xslmes.approval.callback;
/**
* 业务审批回调扩展点SPI
*
* 各业务模块按需实现本接口并声明 {@link #supportTable()}(绑定的业务表名),
* 审批引擎会在审批流转的关键节点自动回调对应实现,业务模块在回调里
* 调用自己「已有的」审核/回写接口(如更新审核状态、扣减库存、生成下游单据等),
* 从而把审批流程与业务单据的功能统一串联起来。
*
* <p>事务说明:回调与审批状态变更处于同一事务内,
* 若回调抛出异常,整个审批动作回滚(审批失败),保证审批与业务数据一致。</p>
*
* <p>使用方式:实现类标注为 Spring Bean@Component / @Service即可被自动收集。</p>
*
* <pre>{@code
* @Component
* public class RubberStdApprovalCallback implements IApprovalBizCallback {
* @Override public String supportTable() { return "mes_xsl_rubber_quick_test_std"; }
* @Override public void onApproved(ApprovalCallbackContext ctx) {
* // 调用业务自身已有接口完成回写
* stdService.lambdaUpdate()
* .eq(MesXslRubberQuickTestStd::getId, ctx.getBizDataId())
* .set(MesXslRubberQuickTestStd::getAuditStatus, "1").update();
* }
* }
* }</pre>
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
public interface IApprovalBizCallback {
/**
* 绑定的业务表名(与审批流定义 bizTable 一致)。
* 返回 "*" 表示监听所有业务表。
*/
String supportTable();
/**
* 单个审批节点通过(中间态)。每经过一个审批节点通过都会触发一次。
* 适合更新中间状态(如「审核中」「已校对」等)。
* 默认空实现,业务按需重写。
*/
default void onNodeApproved(ApprovalCallbackContext ctx) {
// 默认不处理
}
/**
* 整个审批流程最终通过。适合执行终态业务(如置为「已批准」、生效、扣库存等)。
* 默认空实现,业务按需重写。
*/
default void onApproved(ApprovalCallbackContext ctx) {
// 默认不处理
}
/**
* 审批被驳回(流程终止)。适合回退业务状态(如置回「草稿」、释放占用等)。
* 默认空实现,业务按需重写。
*/
default void onRejected(ApprovalCallbackContext ctx) {
// 默认不处理
}
//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时通知业务回滚中间态状态-----------
}

View File

@@ -0,0 +1,59 @@
package org.jeecg.modules.xslmes.approval.callback.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
import org.jeecg.modules.xslmes.approval.callback.IApprovalBizCallback;
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService;
import org.springframework.stereotype.Component;
/**
* 胶料快检实验标准 审批回调示例。
* 演示如何把审批流转结果联动到业务单据已有功能:
* 审批最终通过 -> 审核状态置「已批准」;驳回 -> 回退「草稿」。
*
* 业务在回调里直接调用自身的 service此处用 lambdaUpdate 更新审核状态字段),
* 与原有「批准/反审核」逻辑保持统一。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调-示例
*/
@Slf4j
@Component
public class RubberQuickTestStdApprovalCallback implements IApprovalBizCallback {
private final IMesXslRubberQuickTestStdService stdService;
public RubberQuickTestStdApprovalCallback(IMesXslRubberQuickTestStdService stdService) {
this.stdService = stdService;
}
@Override
public String supportTable() {
return "mes_xsl_rubber_quick_test_std";
}
@Override
public void onApproved(ApprovalCallbackContext ctx) {
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_APPROVED);
log.info("[审批联动] 实验标准 {} 审批通过,审核状态置为已批准", ctx.getBizDataId());
}
@Override
public void onRejected(ApprovalCallbackContext ctx) {
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT);
log.info("[审批联动] 实验标准 {} 被驳回,审核状态回退为草稿", ctx.getBizDataId());
}
private void updateAuditStatus(String bizDataId, String auditStatus) {
if (oConvertUtils.isEmpty(bizDataId)) {
return;
}
stdService.lambdaUpdate()
.eq(MesXslRubberQuickTestStd::getId, bizDataId)
.set(MesXslRubberQuickTestStd::getAuditStatus, auditStatus)
.update();
}
}

View File

@@ -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";
}

View File

@@ -0,0 +1,438 @@
package org.jeecg.modules.xslmes.approval.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry;
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.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;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* MES 审批流设计
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Tag(name = "MES审批流设计")
@RestController
@RequestMapping("/xslmes/approvalFlow")
@Slf4j
public class MesXslApprovalFlowController extends JeecgController<MesXslApprovalFlow, IMesXslApprovalFlowService> {
@Autowired
private IMesXslApprovalFlowService mesXslApprovalFlowService;
@Autowired
private ISysBaseAPI sysBaseAPI;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批联动业务动作注册表-----
@Autowired
private ApprovalBizActionRegistry approvalBizActionRegistry;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批联动业务动作注册表-----
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】当前页字段解析-----
@Autowired
private JdbcTemplate jdbcTemplate;
//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_]+$");
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】当前页字段解析-----
/**
* 根据所选单据表名翻译字典,回填单据中文名
*/
private void fillBizTableName(MesXslApprovalFlow flow) {
if (oConvertUtils.isEmpty(flow.getBizTable())) {
return;
}
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
if (items != null) {
for (DictModel item : items) {
if (flow.getBizTable().equals(item.getValue())) {
flow.setBizTableName(item.getText());
break;
}
}
}
}
@Operation(summary = "审批流设计-分页列表查询")
@RequiresPermissions("approval:flow:list")
@GetMapping(value = "/list")
public Result<IPage<MesXslApprovalFlow>> queryPageList(
MesXslApprovalFlow mesXslApprovalFlow,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslApprovalFlow> queryWrapper = QueryGenerator.initQueryWrapper(mesXslApprovalFlow, req.getParameterMap());
// 列表查询不返回大字段 flowConfig避免传输冗余
queryWrapper.select(MesXslApprovalFlow.class, info -> !"flow_config".equals(info.getColumn()));
queryWrapper.orderByDesc("create_time");
Page<MesXslApprovalFlow> page = new Page<>(pageNo, pageSize);
IPage<MesXslApprovalFlow> pageList = mesXslApprovalFlowService.page(page, queryWrapper);
return Result.OK(pageList);
}
@AutoLog(value = "审批流设计-添加")
@Operation(summary = "审批流设计-添加")
@RequiresPermissions("approval:flow:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getFlowName())) {
return Result.error("审批流名称不能为空");
}
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getBizTable())) {
return Result.error("请先选择绑定单据");
}
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getStatus())) {
mesXslApprovalFlow.setStatus("0");
}
fillBizTableName(mesXslApprovalFlow);
mesXslApprovalFlowService.save(mesXslApprovalFlow);
return Result.OK("添加成功!");
}
@AutoLog(value = "审批流设计-编辑")
@Operation(summary = "审批流设计-编辑")
@RequiresPermissions("approval:flow:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
return Result.error("主键不能为空");
}
fillBizTableName(mesXslApprovalFlow);
mesXslApprovalFlowService.updateById(mesXslApprovalFlow);
return Result.OK("编辑成功!");
}
@AutoLog(value = "审批流设计-保存流程设计")
@Operation(summary = "审批流设计-保存流程设计")
@RequiresPermissions("approval:flow:design")
@PostMapping(value = "/saveDesign")
public Result<String> saveDesign(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
return Result.error("主键不能为空");
}
MesXslApprovalFlow update = new MesXslApprovalFlow();
update.setId(mesXslApprovalFlow.getId());
update.setFlowConfig(mesXslApprovalFlow.getFlowConfig());
// 设计保存时若仍为草稿则置为已发布
if (oConvertUtils.isNotEmpty(mesXslApprovalFlow.getStatus())) {
update.setStatus(mesXslApprovalFlow.getStatus());
}
mesXslApprovalFlowService.updateById(update);
return Result.OK("流程设计已保存!");
}
@AutoLog(value = "审批流设计-发布/停用")
@Operation(summary = "审批流设计-发布/停用")
@RequiresPermissions("approval:flow:design")
@PostMapping(value = "/updateStatus")
public Result<String> updateStatus(
@RequestParam(name = "id") String id,
@RequestParam(name = "status") String status) {
if (!"0".equals(status) && !"1".equals(status) && !"2".equals(status)) {
return Result.error("状态参数非法");
}
boolean ok = mesXslApprovalFlowService.lambdaUpdate()
.eq(MesXslApprovalFlow::getId, id)
.set(MesXslApprovalFlow::getStatus, status)
.update();
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
@AutoLog(value = "审批流设计-删除")
@Operation(summary = "审批流设计-删除")
@RequiresPermissions("approval:flow:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id") String id) {
mesXslApprovalFlowService.removeById(id);
return Result.OK("删除成功!");
}
@AutoLog(value = "审批流设计-批量删除")
@Operation(summary = "审批流设计-批量删除")
@RequiresPermissions("approval:flow:delete")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids") String ids) {
mesXslApprovalFlowService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
@Operation(summary = "审批流设计-通过id查询")
@GetMapping(value = "/queryById")
public Result<MesXslApprovalFlow> queryById(@RequestParam(name = "id") String id) {
MesXslApprovalFlow entity = mesXslApprovalFlowService.getById(id);
if (entity == null) {
return Result.error("未找到对应数据");
}
return Result.OK(entity);
}
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
/**
* 设计上下文:供全局"审批流程设计"悬浮按钮调用。
* 1) 根据当前功能页路由反查绑定的业务表;
* 2) 从审批注册中心读取该单据已启用的审批环节及对应人员字段;
* 3) 取/建该业务表的草稿审批流,返回流程记录(含id)供直接进入设计器。
*
* @param routePath 当前功能页前端路由(如 /xslmes/mesXslFormulaSpec/MesXslFormulaSpecList)
*/
@Operation(summary = "审批流设计-当前页设计上下文")
@RequiresPermissions("approval:flow:design")
@GetMapping(value = "/designContext")
public Result<Map<String, Object>> designContext(@RequestParam(name = "routePath") String routePath) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("routePath", routePath);
data.put("bizTable", null);
data.put("bizTableName", null);
data.put("stages", new ArrayList<>());
data.put("flow", null);
// 1) 路由 -> 业务表名(无法反查时不报错,返回空上下文,前端提示)
String table = resolveTableByRoutePath(routePath);
if (oConvertUtils.isEmpty(table) || !tableExists(table)) {
return Result.OK(data);
}
data.put("bizTable", table);
// 2) 业务表中文名:优先字典,其次表注释
String bizTableName = resolveBizTableName(table);
data.put("bizTableName", bizTableName);
// 3) 从审批注册中心解析启用环节
data.put("stages", parseRegistryStages(table));
// 4) 取/建草稿审批流
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
MesXslApprovalFlow flow = findFlowByTable(table, tenantId);
if (flow == null) {
flow = new MesXslApprovalFlow();
flow.setFlowName((oConvertUtils.isNotEmpty(bizTableName) ? bizTableName : table) + "审批流");
flow.setBizTable(table);
flow.setBizTableName(bizTableName);
flow.setRoutePath(routePath);
flow.setStatus("0");
if (tenantId != null) {
flow.setTenantId(tenantId);
}
mesXslApprovalFlowService.save(flow);
} else if (oConvertUtils.isEmpty(flow.getRoutePath())) {
// 历史数据未记录路由,回填一次便于后续发起按钮匹配
mesXslApprovalFlowService.lambdaUpdate()
.eq(MesXslApprovalFlow::getId, flow.getId())
.set(MesXslApprovalFlow::getRoutePath, routePath)
.update();
flow.setRoutePath(routePath);
}
data.put("flow", flow);
return Result.OK(data);
}
//update-begin---author:GHT ---date: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 的可选回调动作,供设计器节点「回调接口」下拉选择。
*
* @param table 业务表名(审批流绑定的 bizTable)
*/
@Operation(summary = "审批流设计-业务表可选回调动作")
@RequiresPermissions("approval:flow:design")
@GetMapping(value = "/bizActions")
public Result<List<ApprovalBizActionVo>> bizActions(@RequestParam(name = "table") String table) {
return Result.OK(approvalBizActionRegistry.getByTable(table));
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】按业务表查可选回调动作-----
/**
* 根据前端路由反查业务表名。
* 约定jeecg 代码生成的列表组件名为 表名驼峰 + Listsys_permission.component 形如
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecListurl 即路由。
* 反查url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
*/
/** 根据前端路由反查页面组件路径(sys_permission.component),用于前端按 key 取该页面按钮接口映射 */
private String resolveComponentByRoutePath(String routePath) {
if (oConvertUtils.isEmpty(routePath)) {
return null;
}
String path = routePath.trim().replaceAll("/+$", "");
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
+ "ORDER BY menu_type DESC LIMIT 1";
try {
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
return list.isEmpty() ? null : list.get(0);
} catch (Exception e) {
log.warn("反查菜单组件失败 routePath={}", routePath, e);
return null;
}
}
private String resolveTableByRoutePath(String routePath) {
if (oConvertUtils.isEmpty(routePath)) {
return null;
}
String component = resolveComponentByRoutePath(routePath);
if (oConvertUtils.isEmpty(component)) {
return null;
}
// 取组件路径末段xslmes/mesXslFormulaSpec/MesXslFormulaSpecList -> MesXslFormulaSpecList
String comp = component.contains("/") ? component.substring(component.lastIndexOf('/') + 1) : component;
if (comp.endsWith("List")) {
comp = comp.substring(0, comp.length() - "List".length());
}
return camelToUnderline(comp);
}
/** 驼峰转下划线小写MesXslFormulaSpec -> mes_xsl_formula_spec */
private String camelToUnderline(String camel) {
if (oConvertUtils.isEmpty(camel)) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < camel.length(); i++) {
char c = camel.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) {
sb.append('_');
}
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/** 校验表是否存在于当前库 */
private boolean tableExists(String table) {
if (!IDENTIFIER.matcher(table).matches()) {
return false;
}
try {
Integer cnt = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
Integer.class, table);
return cnt != null && cnt > 0;
} catch (Exception e) {
log.warn("校验表存在失败 table={}", table, e);
return false;
}
}
/** 业务表中文名:优先审批注册中心,其次字典 mes_xsl_approval_biz_doc再次表注释 */
private String resolveBizTableName(String table) {
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) {
if (table.equals(item.getValue())) {
return item.getText();
}
}
}
try {
List<String> comments = jdbcTemplate.queryForList(
"SELECT table_comment FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
String.class, table);
if (!comments.isEmpty() && oConvertUtils.isNotEmpty(comments.get(0))) {
return comments.get(0);
}
} catch (Exception e) {
log.warn("查询表注释失败 table={}", table, e);
}
return null;
}
//update-begin---author:GHT ---date:20260605 for【XSLMES-20260605-K8R2】从审批注册中心解析启用环节-----
//update-begin---author:GHT ---date:20260609 for【审批注册中心】移除 byField 引用,操作人由痕迹表承载-----------
/**
* 从审批注册中心读取已启用环节,映射为流程设计器候选节点。
* 返回有序列表:[{stageKey, stageName, nodeType}]
*/
private List<Map<String, Object>> parseRegistryStages(String table) {
List<Map<String, Object>> stages = new ArrayList<>();
MesXslBizDocRegistry registry = bizDocRegistryService.findActiveByTableName(table);
if (registry == null || oConvertUtils.isEmpty(registry.getEnabledStages())) {
return stages;
}
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;
}
//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) {
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
qw.eq("biz_table", table);
if (tenantId != null) {
qw.eq("tenant_id", tenantId);
}
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
return mesXslApprovalFlowService.getOne(qw, false);
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,127 @@
package org.jeecg.modules.xslmes.approval.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* MES 审批办理(运行时-流转)。
* 供 IM 审批卡片按钮调用:查看详情 / 审批通过 / 驳回。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批办理/流转
*/
@Tag(name = "MES审批办理")
@RestController
@RequestMapping("/xslmes/approvalHandle")
@Slf4j
public class MesXslApprovalHandleController {
@Autowired
private IMesXslApprovalHandleService approvalHandleService;
@Operation(summary = "审批办理-单据详情")
@GetMapping("/detail")
public Result<Map<String, Object>> detail(@RequestParam("instanceId") String instanceId) {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Map<String, Object> data = approvalHandleService.detail(instanceId, user);
if (data == null || data.isEmpty()) {
return Result.error("审批实例不存在");
}
return Result.OK(data);
}
@Operation(summary = "审批办理-实时状态(卡片置灰判断)")
@GetMapping("/status")
public Result<Map<String, Object>> status(@RequestParam("instanceId") String instanceId) {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Map<String, Object> data = approvalHandleService.statusInfo(instanceId, user);
return Result.OK(data);
}
@Operation(summary = "审批办理-通过")
@PostMapping("/approve")
public Result<String> approve(@RequestBody Map<String, Object> body) {
String instanceId = (String) body.get("instanceId");
String comment = body.get("comment") == null ? null : String.valueOf(body.get("comment"));
if (oConvertUtils.isEmpty(instanceId)) {
return Result.error("缺少审批实例ID");
}
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.approve(instanceId, comment, user);
}
@Operation(summary = "审批办理-驳回")
@PostMapping("/reject")
public Result<String> reject(@RequestBody Map<String, Object> body) {
String instanceId = (String) body.get("instanceId");
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
if (oConvertUtils.isEmpty(instanceId)) {
return Result.error("缺少审批实例ID");
}
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.reject(instanceId, reason, user);
}
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
@Operation(summary = "审批办理-撤销(发起人撤回)")
@PostMapping("/cancel")
public Result<String> cancel(@RequestBody Map<String, Object> body) {
String instanceId = (String) body.get("instanceId");
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
if (oConvertUtils.isEmpty(instanceId)) {
return Result.error("缺少审批实例ID");
}
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.cancel(instanceId, reason, user);
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起人撤销-----
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】催办接口-----
@Operation(summary = "审批办理-催办(发起人催处理人)")
@PostMapping("/urge")
public Result<String> urge(@RequestBody Map<String, Object> body) {
String instanceId = (String) body.get("instanceId");
if (oConvertUtils.isEmpty(instanceId)) {
return Result.error("缺少审批实例ID");
}
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.urge(instanceId, user);
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】催办接口-----
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】待办列表-----
@Operation(summary = "审批办理-我的待办列表")
@GetMapping("/pendingList")
public Result<List<Map<String, Object>>> pendingList() {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return Result.OK(approvalHandleService.pendingList(user));
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】待办列表-----
//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审批卡片(历史流转中实例)-----------
}

View File

@@ -0,0 +1,359 @@
package org.jeecg.modules.xslmes.approval.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
import org.jeecg.modules.xslmes.approval.service.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;
import java.util.regex.Pattern;
/**
* MES 审批发起(运行时-本期仅发起)
* 供全局"发起审批"悬浮按钮调用:选单据类型 -> 选单据记录 -> 发起。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Tag(name = "MES审批发起")
@RestController
@RequestMapping("/xslmes/approvalLaunch")
@Slf4j
public class MesXslApprovalLaunchController {
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
@Autowired
private IMesXslApprovalFlowService approvalFlowService;
@Autowired
private IMesXslApprovalInstanceService approvalInstanceService;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起改用流转引擎进入首节点-----
@Autowired
private IMesXslApprovalHandleService approvalHandleService;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起改用流转引擎进入首节点-----
//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 有值时:与钉钉审批按钮一致,先按 mes_xsl_ding_tpl_bind 解析当前页绑定,再返回该页业务表下已发布审批流。
*/
@Operation(summary = "发起审批-已发布审批流列表")
@GetMapping("/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 时,按单据表名自动反查菜单路由
for (MesXslApprovalFlow flow : list) {
if (oConvertUtils.isEmpty(flow.getRoutePath())) {
flow.setRoutePath(resolveRoutePathByTable(flow.getBizTable()));
}
}
return Result.OK(list);
}
/**
* 根据单据表名反查对应功能菜单的前端路由。
* 约定jeecg 代码生成的列表组件名为 表名驼峰 + List如 mes_xsl_formula_spec -> MesXslFormulaSpecList
* 对应 sys_permission.component 形如 xslmes/mesXslFormulaSpec/MesXslFormulaSpecList取其 url 即路由。
*/
//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;
}
StringBuilder comp = new StringBuilder();
for (String p : table.split("_")) {
if (oConvertUtils.isEmpty(p)) {
continue;
}
comp.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
}
comp.append("List");
String sql = "SELECT url FROM sys_permission WHERE menu_type IN (0,1) "
+ "AND (del_flag = 0 OR del_flag IS NULL) "
+ "AND (component LIKE ? OR component LIKE ?) "
+ "ORDER BY menu_type DESC LIMIT 1";
try {
List<String> urls = jdbcTemplate.queryForList(sql, String.class, "%/" + comp, "%" + comp);
return urls.isEmpty() ? null : urls.get(0);
} catch (Exception e) {
log.warn("反查菜单路由失败 table={}", table, e);
return null;
}
}
/**
* 根据审批流绑定的单据表查询业务单据记录id + 标题),供发起时选择
*/
@Operation(summary = "发起审批-业务单据记录列表")
@GetMapping("/bizRecords")
public Result<List<Map<String, Object>>> bizRecords(
@RequestParam("flowId") String flowId,
@RequestParam(value = "keyword", required = false) String keyword) {
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
String table = flow.getBizTable();
String titleField = flow.getTitleField();
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
return Result.error("单据表名非法");
}
boolean hasTitle = oConvertUtils.isNotEmpty(titleField) && IDENTIFIER.matcher(titleField).matches();
StringBuilder sql = new StringBuilder("SELECT id, ");
sql.append(hasTitle ? titleField : "id").append(" AS title FROM ").append(table);
List<Object> args = new ArrayList<>();
if (hasTitle && oConvertUtils.isNotEmpty(keyword)) {
sql.append(" WHERE ").append(titleField).append(" LIKE CONCAT('%', ?, '%')");
args.add(keyword);
}
// 主键一定存在,按 id 倒序近似最新;限制条数防全表
sql.append(" ORDER BY id DESC LIMIT 100");
try {
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), args.toArray());
return Result.OK(list);
} catch (Exception e) {
log.error("查询业务单据失败 table={}, field={}", table, titleField, e);
return Result.error("查询业务单据失败:" + e.getMessage());
}
}
/**
* 发起审批:根据审批流定义创建审批实例,解析首个审批节点处理人
*/
@Operation(summary = "发起审批-发起")
@PostMapping("/launch")
public Result<String> launch(@RequestBody Map<String, Object> body) {
String flowId = (String) body.get("flowId");
String bizDataId = (String) body.get("bizDataId");
String bizTitle = (String) body.get("bizTitle");
if (oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(bizDataId)) {
return Result.error("请选择审批流和单据");
}
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
if (!"1".equals(flow.getStatus())) {
return Result.error("该审批流未发布,无法发起");
}
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
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);
syncApprovalRecordAfterLaunch(inst.getId());
//update-end---author:GHT ---date:20260604 for【QH-MES审批台账】发起审批统一门禁与台账写入-----
return Result.OK("发起成功!");
}
/**
* 批量发起审批:用于列表多选后一次性发起
*/
@Operation(summary = "发起审批-批量发起")
@PostMapping("/launchBatch")
public Result<String> launchBatch(@RequestBody Map<String, Object> body) {
String flowId = (String) body.get("flowId");
Object itemsObj = body.get("items");
if (oConvertUtils.isEmpty(flowId) || !(itemsObj instanceof List) || ((List<?>) itemsObj).isEmpty()) {
return Result.error("请选择审批流和单据");
}
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
if (!"1".equals(flow.getStatus())) {
return Result.error("该审批流未发布,无法发起");
}
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
int count = 0;
int skipCount = 0;
for (Object o : (List<?>) itemsObj) {
if (!(o instanceof Map)) {
continue;
}
Map<?, ?> item = (Map<?, ?>) o;
String bizDataId = item.get("bizDataId") == null ? null : String.valueOf(item.get("bizDataId"));
String bizTitle = item.get("bizTitle") == null ? null : String.valueOf(item.get("bizTitle"));
if (oConvertUtils.isEmpty(bizDataId)) {
continue;
}
//update-begin---author:GHT ---date: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;
}
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 ? "所选单据均不允许发起审批(可能已有流转中或已通过流程)" : "没有有效的单据数据");
}
String msg = "已发起 " + count + " 条审批!";
if (skipCount > 0) {
msg += "" + skipCount + " 条已有审批中流程,已跳过)";
}
return Result.OK(msg);
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
}
/**
* 构建一条审批实例(基础字段;处理人解析/卡片由流转引擎完成)
*/
private MesXslApprovalInstance buildInstance(MesXslApprovalFlow flow, String bizDataId, String bizTitle, LoginUser loginUser) {
MesXslApprovalInstance inst = new MesXslApprovalInstance();
inst.setFlowId(flow.getId());
inst.setFlowName(flow.getFlowName());
inst.setBizTable(flow.getBizTable());
inst.setBizTableName(flow.getBizTableName());
inst.setBizDataId(bizDataId);
inst.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
inst.setStatus("0");
if (loginUser != null) {
inst.setApplyUser(loginUser.getUsername());
inst.setApplyUserName(loginUser.getRealname());
}
inst.setApplyTime(new Date());
inst.setTenantId(MesXslTenantUtils.resolveTenantId(flow.getTenantId()));
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
return inst;
}
//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审批台账】发起后同步台账终态(如无节点自动通过)-----
}

View File

@@ -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审批台账");
}
}

View File

@@ -0,0 +1,78 @@
package org.jeecg.modules.xslmes.approval.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import java.io.Serializable;
/**
* MES 审批流定义
* 钉钉式可视化审批流设计,绑定 MES 业务单据。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mes_xsl_approval_flow")
@Schema(description = "MES审批流定义")
public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审批流名称")
private String flowName;
@Schema(description = "绑定单据表名")
@Dict(dicCode = "mes_xsl_approval_biz_doc")
private String bizTable;
@Schema(description = "绑定单据中文名(冗余展示)")
private String bizTableName;
@Schema(description = "单据标题字段名(发起选单据时展示)")
private String titleField;
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
private String routePath;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回/撤销恢复初始状态-----
@Schema(description = "业务单据状态字段名(驳回/撤销时回写其发起时原值)")
private String statusField;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回/撤销恢复初始状态-----
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】超时提醒配置-----
@Schema(description = "超时提醒小时数(0=不提醒)默认24")
private Integer timeoutHours;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】超时提醒配置-----
@Schema(description = "流程设计JSON(钉钉式节点树)")
private String flowConfig;
@Schema(description = "状态0草稿 1已发布 2已停用")
@Dict(dicCode = "mes_xsl_approval_flow_status")
private String status;
@Schema(description = "排序")
private Double sortNo;
@Schema(description = "备注")
private String remark;
@Schema(description = "逻辑删除0正常 1已删除")
@TableLogic
private Integer delFlag;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "所属部门编码")
private String sysOrgCode;
}

View File

@@ -0,0 +1,109 @@
package org.jeecg.modules.xslmes.approval.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* MES 审批实例(本期仅记录发起,不含办理)
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mes_xsl_approval_instance")
@Schema(description = "MES审批实例")
public class MesXslApprovalInstance extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审批流定义ID")
private String flowId;
@Schema(description = "审批流名称")
private String flowName;
@Schema(description = "业务单据表名")
private String bizTable;
@Schema(description = "业务单据中文名")
private String bizTableName;
@Schema(description = "业务单据记录ID")
private String bizDataId;
@Schema(description = "业务单据展示标题")
private String bizTitle;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回/撤销恢复初始状态-----
@Schema(description = "业务单据状态字段名(发起时快照)")
private String statusField;
@Schema(description = "发起审批时业务状态原值(驳回/撤销回写)")
private String originStatus;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】驳回/撤销恢复初始状态-----
@Schema(description = "当前节点ID")
private String currentNodeId;
@Schema(description = "当前节点名称")
private String currentNodeName;
@Schema(description = "当前处理人(username逗号分隔)")
private String currentHandlers;
@Schema(description = "当前处理人展示文本")
private String currentHandlersText;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
@Schema(description = "当前节点处理进度JSON(nodeId/mode/tasks)")
private String nodeProgress;
@Schema(description = "审批历史JSON数组")
private String history;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
@Schema(description = "状态0审批中 1已通过 2已驳回 3已撤销")
@Dict(dicCode = "mes_xsl_approval_instance_status")
private String status;
@Schema(description = "发起人username")
private String applyUser;
@Schema(description = "发起人姓名")
private String applyUserName;
@Schema(description = "发起时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date applyTime;
@Schema(description = "备注")
private String remark;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】乐观锁防并发写-----
@com.baomidou.mybatisplus.annotation.Version
@Schema(description = "乐观锁版本号")
private Integer version;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流完善】乐观锁防并发写-----
@Schema(description = "逻辑删除0正常 1已删除")
@TableLogic
private Integer delFlag;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "所属部门编码")
private String sysOrgCode;
}

View File

@@ -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;
}

View File

@@ -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】审批痕迹响应自动注入-----------

View File

@@ -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-----------
}

View File

@@ -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【审批注册中心】物理表名下拉选择查询当前库表清单-----------
}

View File

@@ -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);
}
}

View File

@@ -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("删除成功");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 动作是否同步目标表痕迹-----------
}

View File

@@ -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】钉钉回调集成编排入口日志-----------
}

View File

@@ -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<>();
}

View File

@@ -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;
}
// 钉钉 StreaminstanceId = 台账主键 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来源回调-----------
}

View File

@@ -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 集成方案解析回退目标-----------
}

View File

@@ -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);
}
}
}

View File

@@ -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 成功后按动作配置同步目标表痕迹-----------
}

View File

@@ -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;
}
}

View File

@@ -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_idPhase1 预留)
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;
}
}

View File

@@ -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);
}

View File

@@ -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】审批注册中心环节回退执行器-----------
}

View File

@@ -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】审批注册中心环节同步执行器-----------
}

View File

@@ -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已拒绝执行");
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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【审批注册中心】拒绝/终止时清空源单与痕迹操作人-----------
}

View File

@@ -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-----------
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

Some files were not shown because too many files have changed in this diff Show More