From 73a22b5ed9359c47634cb248a64d633913e31a3e Mon Sep 17 00:00:00 2001 From: geht <2947093423@qq.com> Date: Thu, 18 Jun 2026 10:55:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AD=E9=97=B4=E8=A1=A8=E9=87=87=E9=9B=86?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=87=87=E9=9B=86=E9=85=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=8F=AF=E8=A7=86=E5=8C=96=E5=8F=AF=E6=8E=A7?= =?UTF-8?q?=E9=87=87=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/QH-MES部署与热部署方案.md | 282 ++++++++++ .../jeecg-module-xslmes/doc/代码修改日志 | 70 +++ .../MesXslMixerActionController.java | 18 +- .../xslmes/entity/MesXslMixerAction.java | 10 + .../MesXslMcsSyncConfigController.java | 150 +++++ .../mcs/entity/MesXslMcsSyncConfig.java | 126 +++++ .../xslmes/mcs/entity/MesXslMcsSyncField.java | 75 +++ .../xslmes/mcs/mapper/McsMetaMapper.java | 58 ++ .../mcs/mapper/MesXslMcsSyncConfigMapper.java | 13 + .../mcs/mapper/MesXslMcsSyncFieldMapper.java | 13 + .../service/IMesXslMcsSyncConfigService.java | 40 ++ .../impl/MesXslMcsSyncConfigServiceImpl.java | 223 ++++++++ .../xslmes/mcs/sync/GenericMcsSyncEngine.java | 524 ++++++++++++++++++ .../xslmes/mcs/sync/McsSyncScheduler.java | 162 ++++++ .../service/IMesXslMixerActionService.java | 4 +- .../impl/MesXslMixerActionServiceImpl.java | 18 +- .../V3.9.2_153__mes_xsl_mcs_sync_config.sql | 73 +++ .../V3.9.2_154__mes_xsl_mcs_sync_field.sql | 85 +++ .../V3.9.2_155__mes_xsl_mcs_sync_mode.sql | 10 + ...2_156__mes_xsl_mcs_sync_flag_condition.sql | 4 + ...157__mes_xsl_mcs_sync_flag_match_value.sql | 3 + .../MesXslMixerAction.api.ts | 4 +- .../MesXslMixerAction.data.ts | 30 +- .../mcsSyncConfig/components/CollectModal.vue | 201 +++++++ .../components/SyncConfigModal.vue | 267 +++++++++ .../views/xslmesMcs/mcsSyncConfig/index.vue | 64 +++ .../mcsSyncConfig/mcsSyncConfig.api.ts | 44 ++ .../mcsSyncConfig/mcsSyncConfig.data.ts | 41 ++ .../views/xslmesMcs/mcsToMesMixAct/index.vue | 17 +- 29 files changed, 2602 insertions(+), 27 deletions(-) create mode 100644 docs/QH-MES部署与热部署方案.md create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncField.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/McsMetaMapper.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncConfigMapper.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncFieldMapper.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_153__mes_xsl_mcs_sync_config.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_154__mes_xsl_mcs_sync_field.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_155__mes_xsl_mcs_sync_mode.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_156__mes_xsl_mcs_sync_flag_condition.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_157__mes_xsl_mcs_sync_flag_match_value.sql create mode 100644 jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue create mode 100644 jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/SyncConfigModal.vue create mode 100644 jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/index.vue create mode 100644 jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts create mode 100644 jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts diff --git a/docs/QH-MES部署与热部署方案.md b/docs/QH-MES部署与热部署方案.md new file mode 100644 index 00000000..0232cd6b --- /dev/null +++ b/docs/QH-MES部署与热部署方案.md @@ -0,0 +1,282 @@ +# QH-MES 部署与热部署方案 + +> 本文档记录 QH-MES(jeecg-boot 3.9.2)在 Windows Server 上的部署方案,包含后端、前端的一键发版流程、关键配置、踩过的坑,以及未来上线客户正式服务器的规划。 +> 最后更新:2026-06-17 + +--- + +## 一、环境概况 + +### 测试服务器(当前) +- **系统**:Windows Server 2016 Standard +- **JDK**:17.0.11(`C:\Program Files\Java\jdk-17`,注意 `JAVA_HOME` 要指根目录,**不能带 `\bin`**) +- **Maven**:3.9.8(`D:\apache-maven-3.9.8`,从本地 Win11 拷过去的,因服务器网络受限无法在线下载) +- **Git**:已装在 `C:\Program Files\Git`,但不在 PATH(用完整路径 `"C:\Program Files\Git\cmd\git.exe"` 调用) +- **Gitea**:跑在**同一台机器**,裸仓库在 `D:\gitea\data\gitea-repositories\chenx\qhmes.git`,Web 地址 `http://27.223.88.102:33000/chenx/qhmes` +- **Nginx**:1.30.1,装在 `D:\qhmes\nginx-1.30.1\`,前端文件目录 `D:\qhmes\nginx-1.30.1\html\jeecg\` +- **Node/pnpm/npm**:**服务器上没装**(前端不在服务器构建) + +### 关键路径 +| 用途 | 路径 | +|------|------| +| 后端运行目录(jar + WinSW 服务) | `D:\qhmes\` | +| 后端运行 jar | `D:\qhmes\jeecg-system-start-3.9.2.jar` | +| 服务器源码工作副本 | `D:\qhmes-src\`(从本地裸仓库 clone,分支 `main`) | +| 前端 nginx 目录 | `D:\qhmes\nginx-1.30.1\html\jeecg\` | +| WinSW 服务程序/配置 | `D:\qhmes\qhmes-service.exe` / `qhmes-service.xml` | +| 后端发版脚本 | `D:\deploy-server.bat`(放源码目录外,避免 git pull 自我覆盖) | + +### 本地开发机(Win11) +- 项目路径:`D:\XSL-PROJECT\QH-MES\qhmes` +- 后端模块:`jeecg-boot/`,前端:`jeecgboot-vue3/`(同一个 git 仓库) +- 传文件到服务器的方式:**向日葵远程传输**(只能手动,不能脚本化) + +--- + +## 二、后端部署(已跑通 ✅) + +### 2.1 后端做成 Windows 服务(WinSW) +后端不再手动 `java -jar`(关窗口/断 RDP 就掉),改用 **WinSW** 注册成 Windows 服务,开机自启、崩溃自动重启。 + +- 服务 id:`qhmes`,名称:`QH-MES Backend` +- 配置文件 `D:\qhmes\qhmes-service.xml` 关键内容: + ```xml + java + -Xms1g -Xmx2g -jar "D:\qhmes\jeecg-system-start-3.9.2.jar" + D:\qhmes + D:\qhmes\logs + + ``` +- **注意**:`` 里**不加** `--spring.profiles.active`,因为 profile 已在打包时烤进 jar(见第四节)。 +- 常用命令(管理员): + ``` + D:\qhmes\qhmes-service.exe install # 安装服务(一次性) + D:\qhmes\qhmes-service.exe start # 启动 + D:\qhmes\qhmes-service.exe stop # 停止 + ``` +- 查日志: + ```powershell + Get-Content D:\qhmes\logs\qhmes-service.out.log -Wait -Tail 60 + ``` + 启动成功标志:`Started JeecgSystemApplication in xx seconds`,Tomcat 监听 8888。 + +### 2.2 一键发版脚本 `D:\deploy-server.bat` +流程:`git pull → mvn 打包(prod) → 停服务 → 换 jar → 启服务`。脚本自带 `git pull`,**双击即自动拉最新代码**。 + +```bat +@echo off +setlocal +set SRC=D:\qhmes-src +set GIT="C:\Program Files\Git\cmd\git.exe" +set DEPLOY_DIR=D:\qhmes +set JAR_NAME=jeecg-system-start-3.9.2.jar +set SVC=D:\qhmes\qhmes-service.exe +set BUILT_JAR=%SRC%\jeecg-boot\jeecg-module-system\jeecg-system-start\target\%JAR_NAME% + +echo [1/5] git pull ... +cd /d %SRC% +%GIT% pull +if errorlevel 1 ( echo [ERROR] git pull failed & pause & exit /b 1 ) + +echo [2/5] maven package with prod profile ... +cd /d %SRC%\jeecg-boot +call mvn clean package -pl jeecg-module-system/jeecg-system-start -am -DskipTests -P prod -T 1C +if errorlevel 1 ( echo [ERROR] build failed & pause & exit /b 1 ) +if not exist "%BUILT_JAR%" ( echo [ERROR] built jar not found & pause & exit /b 1 ) + +echo [3/5] stop service ... +"%SVC%" stop +timeout /t 6 /nobreak >nul + +echo [4/5] copy new jar ... +copy /Y "%BUILT_JAR%" "%DEPLOY_DIR%\%JAR_NAME%" +if errorlevel 1 ( echo [ERROR] copy failed, jar locked? & pause & exit /b 1 ) + +echo [5/5] start service ... +"%SVC%" start +echo ===== DEPLOY DONE ===== +endlocal +pause +``` + +### 2.3 后端发版流程 +1. 本地改代码 → `git push`(推到 `main`) +2. RDP/向日葵到服务器 → 双击 `D:\deploy-server.bat` +3. 看日志确认 `Started ... in xx seconds` + +> 首次构建会下载几百 MB 依赖(约 8 分钟),缓存在 `D:\maven-repo`,之后每次只需 1~3 分钟。 + +--- + +## 三、前端部署(脚本已就绪) + +前端 `jeecgboot-vue3` 用 **pnpm** 构建,产物 `dist/`,放到 nginx 的 `html\jeecg\`。 +因服务器没 Node 且网络受限,**前端不在服务器构建**,而是:**本地构建 → dist 走 git → 服务器拉取替换**。 + +> `jeecgboot-vue3/dist` 被 `.gitignore` 忽略,所以构建后复制到根目录 **`web-dist`** 文件夹(未被忽略)再提交。 + +### 3.1 本地构建脚本 `build-frontend.bat`(本地 Win11 双击) +核心构建步骤与官网一致:`pnpm install` + `pnpm run build`,之后自动复制 dist 到 web-dist 并 git push。 + +```bat +@echo off +setlocal +set REPO=%~dp0 +set WEBDIST=%REPO%web-dist + +echo [1/4] build frontend (pnpm install + build) ... +cd /d %REPO%jeecgboot-vue3 +call pnpm install +if errorlevel 1 ( echo [ERROR] pnpm install failed & pause & exit /b 1 ) +call pnpm run build +if errorlevel 1 ( echo [ERROR] frontend build failed & pause & exit /b 1 ) +if not exist "%REPO%jeecgboot-vue3\dist\index.html" ( echo [ERROR] dist/index.html not found & pause & exit /b 1 ) + +echo [2/4] refresh web-dist folder ... +if exist "%WEBDIST%" rmdir /S /Q "%WEBDIST%" +mkdir "%WEBDIST%" +xcopy "%REPO%jeecgboot-vue3\dist\*" "%WEBDIST%\" /E /Y /I >nul + +echo [3/4] git add and commit web-dist ... +cd /d %REPO% +git add web-dist +git commit -m "frontend build dist update" + +echo [4/4] git push ... +git push +if errorlevel 1 ( echo [ERROR] git push failed & pause & exit /b 1 ) +echo ===== FRONTEND BUILT AND PUSHED ===== +endlocal +pause +``` + +### 3.2 服务器部署脚本 `D:\deploy-frontend.bat`(服务器双击) +```bat +@echo off +setlocal +set SRC=D:\qhmes-src +set GIT="C:\Program Files\Git\cmd\git.exe" +set WEB=D:\qhmes\nginx-1.30.1\html\jeecg +set NGINX_DIR=D:\qhmes\nginx-1.30.1 + +echo [1/3] git pull ... +cd /d %SRC% +%GIT% pull +if errorlevel 1 ( echo [ERROR] git pull failed & pause & exit /b 1 ) +if not exist "%SRC%\web-dist\index.html" ( echo [ERROR] web-dist not found, run build-frontend.bat first & pause & exit /b 1 ) + +echo [2/3] replace nginx frontend files ... +if exist "%WEB%" rmdir /S /Q "%WEB%" +mkdir "%WEB%" +xcopy "%SRC%\web-dist\*" "%WEB%\" /E /Y /I >nul +if errorlevel 1 ( echo [ERROR] copy failed & pause & exit /b 1 ) + +echo [3/3] reload nginx (ignore error if not running) ... +cd /d %NGINX_DIR% +nginx.exe -s reload +echo ===== FRONTEND DEPLOY DONE ===== +endlocal +pause +``` +> **nginx 不用重启**:静态文件替换后自动生效。`nginx -s reload` 是平滑重载,**不要直接跑 `nginx.exe`**(会报端口占用)。 + +### 3.3 前端发版流程(两步双击) +1. 本地双击 `build-frontend.bat`(构建 + 推送 web-dist) +2. 服务器双击 `D:\deploy-frontend.bat`(拉取 + 替换 + reload) +3. 刷新浏览器 + +--- + +## 四、配置(profile)说明 —— 重点 + +### 4.1 哪个配置生效,由打包时的 Maven `-P` 决定 +`application.yml` 里 `spring.profiles.active: '@profile.name@'` 是 Maven 占位符,打包时填入: + +| 打包命令 | 生效配置 | +|---------|---------| +| `mvn package`(不带 -P) | **dev**(默认,`` 在 dev 上) | +| `mvn package -P prod` | **prod** | +| `mvn package -P test` | test | + +> 发版脚本用的是 **`-P prod`**,所以烤进 jar 的是 prod 配置,运行时无需再加 `--spring.profiles.active`。 + +### 4.2 判断一个 jar 用的哪个配置 +- 看启动日志:`The following 1 profile is active: "prod"` +- 解压 jar 看 `BOOT-INF/classes/application.yml` 里 `active` 的实际值 +- 运行时 `--spring.profiles.active=xxx` 可强制覆盖 + +### 4.3 dev vs prod 的区别(本项目) +| 项 | dev | prod(当前测试服务器用) | +|----|-----|------| +| 端口 | 8888 | 8888(已改成与现网一致) | +| MySQL | `xsl.qdxsl.top:50768`(公网绕一圈) | `127.0.0.1:3306`(本机直连) | +| MySQL 密码 | 123456 | 123456(已改,原为 root) | + +> **重要事实**:`xsl.qdxsl.top:50768` 与本机 `127.0.0.1:3306` 是**同一个生产库**(公网域名+端口转发到本机)。prod 走本机直连更快更稳。 +> prod 配置的修改在 `application-prod.yml`,已带 `update-begin/update-end` 痕迹注释。 + +--- + +## 五、踩过的坑(避免重复) + +1. **bat 文件乱码/无法执行**:bat 里写中文 + 存成 UTF-8,cmd 按 GBK 读会乱码,连 `@echo off` 都坏。→ **bat 只用英文 ASCII,不带 BOM**。 +2. **WinSW 报 `Invalid character in encoding`**:xml 里中文存成了 ANSI/GBK。→ 用 UTF-8 保存,或描述改英文。 +3. **服务用 prod 启动报 `Access denied for user 'root'`**:prod 密码原写的 `root`,实际应为 `123456`。 +4. **PowerShell 下载报 `未能创建 SSL/TLS 安全通道`**:Server 2016 默认 TLS 1.0。→ 先 `[Net.ServicePointManager]::SecurityProtocol = ... -bor 3072` 开启 TLS 1.2。 +5. **服务器下载被 `127.0.0.1:10080` 的本机 Apache 拦截 404**:服务器网络有本机代理/DNS 劫持,外网下载不通。→ Maven/Node 等**在本地下好,向日葵拷过去**。 +6. **`mvn` 报 `JAVA_HOME not defined correctly`**:`JAVA_HOME` 误设成了 `...\jdk-17\bin`。→ 应为根目录 `...\jdk-17`。 +7. **`git clone D:\gitea\...qhmes.git` 拿到的没有源码**:那是 Gitea 裸仓库(只有 git 底层数据)。→ `git clone` 它到 `D:\qhmes-src` 得到工作副本。 +8. **cmd 里 `cd D:\xxx` 不切盘**:要用 `cd /d D:\xxx`;`&` 是 PowerShell 语法,cmd 里不能用。 +9. **PowerShell 粘贴 here-string 卡在 `>>`**:终止符 `'@` 没识别。→ 大段内容改用**记事本**另存为(All Files + ANSI),或写进 git 拉取。 + +--- + +## 六、未来上线客户正式服务器的规划(待落地) + +**核心原则:测试服务器可以 git 拉源码+构建;客户正式服务器只部署"已构建的成品",不放源码、不装构建工具、不依赖我方 gitea。** + +### 6.1 目标流程 +``` +我方(测试服务器/开发机):打包出成品(jar + 前端dist)并测好 + ↓ 向日葵/U盘/网盘 传过去 +客户正式服务器:双击 deploy.bat → 停服务 → 换jar → 换前端 → 启服务 +``` + +### 6.2 必做改造:配置外置(同一 jar 走天下) +现在 prod 配置(数据库/IP/密码)烤死在 jar 里,是测试服务器的。客户的库不同,需把配置挪到 jar 外: +``` +D:\qhmes\ +├── jeecg-system-start-3.9.2.jar ← 所有环境通用,不用为每个客户重打 +└── config\ + └── application-prod.yml ← 本机专属:客户的数据库/IP/密码 +``` +Spring Boot 自动优先读 jar 同级 `config\` 目录的配置(外部 > jar 内 classpath)。 + +### 6.3 待办(上线时找 Claude 做) +1. 把 prod 配置从 jar 内挪到外部 `config\`,jar 变环境无关 +2. 写"打 release 包"脚本:一键产出 `jar + 前端dist + deploy.bat` 发布包 +3. 写客户服务器 `deploy.bat`:只"换文件+重启",不构建、不拉源码 +4. (建议)测试服务器也提前切到外置配置,与客户环境保持一致,上线零改动 + +--- + +## 七、快速命令速查 + +```powershell +# 后端发版 +D:\deploy-server.bat # 服务器双击 + +# 前端发版 +build-frontend.bat # 本地 Win11 双击 +D:\deploy-frontend.bat # 服务器双击 + +# 看后端日志 +Get-Content D:\qhmes\logs\qhmes-service.out.log -Wait -Tail 60 + +# 后端服务控制(管理员) +D:\qhmes\qhmes-service.exe stop|start|status + +# nginx 平滑重载(在 nginx 目录下) +cd /d D:\qhmes\nginx-1.30.1 +nginx.exe -s reload +``` diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 index 1590e8d4..4c6131e6 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 @@ -1110,3 +1110,73 @@ jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/ jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdService.cs + +-- author:GHT---date:20260617--for: 【MES上辅机】密炼动作秒级采集 + 通用中间表采集配置 --- +需求:密炼机动作维护数据从中间表 MCSToMES_MixAct 秒级采集(机台名称→设备名称、动作名称→动作名称、动作地址→动作代号), +在「密炼动作」页支持 启动/停止采集与设置时间间隔(默认1秒);采集配置落库为通用配置表(mes_xsl_mcs_sync_config)供后续功能复用。 +设计:新增通用采集配置表 + McsSyncHandler 扩展点 + McsSyncScheduler(ThreadPoolTaskScheduler 动态重排+启动加载); +MixActSyncHandler 增量 Upsert(按机台编号+动作代号唯一),保留手动维护数据;密炼机动作维护补全 equip_id/equip_type 字段, +唯一性由全局唯一改为(设备+动作代号)同设备内唯一,equipment_id 允许为空(采集未匹配台账时)。 +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_153__mes_xsl_mcs_sync_config.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncConfigMapper.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncHandler.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/handler/MixActSyncHandler.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerAction.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerActionService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerActionServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerActionController.java +jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/McsToMesMixAct.api.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.api.ts +-- author:GHT---date:20260617--for: 【MES上辅机】密炼动作秒级采集 + 通用中间表采集配置 --- + +-- author:GHT---date:20260617--for: 【MES上辅机】采集配置:通用表/字段绑定 + 配置驱动采集 --- +需求:在「MES上辅机数据」下新增「采集配置」,左选中间库表、右选MES表(mes_xsl_前缀),下方左带出中间库字段、右由用户选MES接收字段; +采集操作改为弹窗(是否采集+采集间隔),密炼动作页同样改为弹窗。 +设计:统一为配置驱动——删除硬编码 MixActSyncHandler/McsSyncHandler,新增 GenericMcsSyncEngine(JdbcTemplate跨库读源表→按"匹配键"Upsert写MES表, +自动填充 id/时间/租户/del_flag,纯字段拷贝);McsSyncScheduler 改为按 configId 调度;新增字段映射表 mes_xsl_mcs_sync_field 与配置头扩展(target_table/config_name等); +密炼动作(MIX_ACT)改造为预置配置+字段映射;新增 McsMetaMapper 查询SQLServer/MySQL表与字段元数据;采集配置CRUD/详情/采集操作接口。 +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_154__mes_xsl_mcs_sync_field.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncField.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncFieldMapper.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/McsMetaMapper.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java +(删除)jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncHandler.java +(删除)jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/handler/MixActSyncHandler.java +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/index.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/SyncConfigModal.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/McsToMesMixAct.api.ts +-- author:GHT---date:20260617--for: 【MES上辅机】采集配置:通用表/字段绑定 + 配置驱动采集 --- + +-- author:GHT---date:20260617--for: 【MES上辅机】采集模式:全量/时间/增量 + 批量增量写入(应对大表) --- +背景:原通用引擎每周期全表读源+全表读目标逐行Upsert,autocommit逐行往返,大表(上万~数十万)采集慢。 +优化:GenericMcsSyncEngine 改为「一次读现有建索引+内存比对+变更检测+batchUpdate分批」;并新增三种采集模式(采集操作弹窗可配): +FULL全量匹配(小表全量Upsert)、TIME时间匹配(按时间列取当天/最近七天再Upsert,目标侧按窗口匹配键定向IN读取)、 +INCR增量匹配(按增量列高水位>last_watermark、ORDER BY ASC取TOP N,仅追加并推进水位)。调度器落库 last_watermark。 +mes_xsl_mcs_sync_config 增加 sync_mode/incr_column/time_window/batch_limit/last_watermark。 +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_155__mes_xsl_mcs_sync_mode.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts +jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts +-- author:GHT---date:20260617--for: 【MES上辅机】采集模式:全量/时间/增量 + 批量增量写入(应对大表) --- diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerActionController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerActionController.java index 050af50f..2683e143 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerActionController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerActionController.java @@ -112,13 +112,14 @@ public class MesXslMixerActionController extends JeecgController checkActionName( + @RequestParam(name = "equipmentId", required = false) String equipmentId, @RequestParam(name = "actionName", required = true) String actionName, @RequestParam(name = "dataId", required = false) String dataId) { if (oConvertUtils.isEmpty(actionName) || actionName.trim().isEmpty()) { return Result.OK("该值可用!"); } - if (mesXslMixerActionService.isActionNameDuplicated(actionName, dataId)) { - return Result.error("动作名称不能重复"); + if (mesXslMixerActionService.isActionNameDuplicated(equipmentId, actionName, dataId)) { + return Result.error("同一设备下动作名称不能重复"); } return Result.OK("该值可用!"); } @@ -126,13 +127,14 @@ public class MesXslMixerActionController extends JeecgController checkActionCode( + @RequestParam(name = "equipmentId", required = false) String equipmentId, @RequestParam(name = "actionCode", required = true) String actionCode, @RequestParam(name = "dataId", required = false) String dataId) { if (oConvertUtils.isEmpty(actionCode) || actionCode.trim().isEmpty()) { return Result.OK("该值可用!"); } - if (mesXslMixerActionService.isActionCodeDuplicated(actionCode, dataId)) { - return Result.error("动作代号不能重复"); + if (mesXslMixerActionService.isActionCodeDuplicated(equipmentId, actionCode, dataId)) { + return Result.error("同一设备下动作代号不能重复"); } return Result.OK("该值可用!"); } @@ -152,15 +154,15 @@ public class MesXslMixerActionController extends JeecgController> list(MesXslMcsSyncConfig query, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) { + LambdaQueryWrapper qw = new LambdaQueryWrapper() + .eq(MesXslMcsSyncConfig::getDelFlag, 0) + .like(StringUtils.isNotBlank(query.getConfigName()), MesXslMcsSyncConfig::getConfigName, query.getConfigName()) + .like(StringUtils.isNotBlank(query.getSourceTable()), MesXslMcsSyncConfig::getSourceTable, query.getSourceTable()) + .orderByDesc(MesXslMcsSyncConfig::getUpdateTime); + IPage page = syncConfigService.page(new Page<>(pageNo, pageSize), qw); + page.getRecords().forEach(c -> c.setRunning(syncScheduler.isRunning(c.getId()))); + return Result.OK(page); + } + + @Operation(summary = "采集配置-详情(含字段映射)") + @GetMapping("/queryById") + public Result queryById(@RequestParam("id") String id) { + MesXslMcsSyncConfig cfg = syncConfigService.getDetail(id); + if (cfg == null) { + return Result.error("配置不存在"); + } + cfg.setRunning(syncScheduler.isRunning(id)); + return Result.OK(cfg); + } + + @Operation(summary = "采集配置-按业务类型获取(密炼动作页用)") + @GetMapping("/getByBizType") + public Result getByBizType(@RequestParam(name = "bizType", defaultValue = "MIX_ACT") String bizType) { + MesXslMcsSyncConfig cfg = syncConfigService.getByBizType(bizType); + if (cfg != null) { + cfg.setRunning(syncScheduler.isRunning(cfg.getId())); + } + return Result.OK(cfg); + } + + @Operation(summary = "采集配置-新增") + @RequiresPermissions("xslmes:mcsSyncConfig:add") + @PostMapping("/add") + public Result add(@RequestBody MesXslMcsSyncConfig config) { + config.setId(null); + return syncConfigService.saveConfig(config); + } + + @Operation(summary = "采集配置-编辑") + @RequiresPermissions("xslmes:mcsSyncConfig:edit") + @PostMapping("/edit") + public Result edit(@RequestBody MesXslMcsSyncConfig config) { + if (StringUtils.isBlank(config.getId())) { + return Result.error("缺少配置ID"); + } + return syncConfigService.saveConfig(config); + } + + @Operation(summary = "采集配置-删除") + @RequiresPermissions("xslmes:mcsSyncConfig:delete") + @DeleteMapping("/delete") + public Result delete(@RequestParam("id") String id) { + return syncConfigService.deleteConfig(id); + } + + @Operation(summary = "采集操作-是否采集+采集间隔") + @RequiresPermissions("xslmes:mcsSyncConfig:setting") + @PostMapping("/saveCollect") + public Result saveCollect(@RequestBody MesXslMcsSyncConfig body) { + return syncConfigService.saveCollect(body); + } + + // ===================== 元数据 ===================== + + @Operation(summary = "元数据-中间库表清单") + @GetMapping("/meta/sourceTables") + public Result>> sourceTables() { + if (!mcsDataSourceManager.isDbConfigActive()) { + return Result.error("中间库未连接,请先在「中间库连接配置」中启用"); + } + return Result.OK(metaMapper.listSourceTables()); + } + + @Operation(summary = "元数据-中间库表字段") + @GetMapping("/meta/sourceColumns") + public Result>> sourceColumns(@RequestParam("table") String table) { + if (!mcsDataSourceManager.isDbConfigActive()) { + return Result.error("中间库未连接,请先在「中间库连接配置」中启用"); + } + if (!table.matches("^[A-Za-z0-9_]+$")) { + return Result.error("非法表名"); + } + return Result.OK(metaMapper.listSourceColumns(table)); + } + + @Operation(summary = "元数据-MES业务表清单(mes_xsl_前缀)") + @GetMapping("/meta/targetTables") + public Result>> targetTables() { + return Result.OK(metaMapper.listTargetTables()); + } + + @Operation(summary = "元数据-MES表字段") + @GetMapping("/meta/targetColumns") + public Result>> targetColumns(@RequestParam("table") String table) { + if (!table.matches("^[A-Za-z0-9_]+$")) { + return Result.error("非法表名"); + } + return Result.OK(metaMapper.listTargetColumns(table)); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java new file mode 100644 index 00000000..32957fa2 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncConfig.java @@ -0,0 +1,126 @@ +package org.jeecg.modules.xslmes.mcs.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * MES上辅机 中间表采集配置(通用) + *

按 bizType 区分不同业务(密炼动作/报警/配方等),供秒级定时采集统一复用

+ * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】密炼动作秒级采集 + */ +@Data +@TableName("mes_xsl_mcs_sync_config") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "MES上辅机中间表采集配置") +public class MesXslMcsSyncConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + @Schema(description = "业务类型(采集任务唯一标识,如 MIX_ACT 密炼动作;通用配置可为空)") + private String bizType; + + @Schema(description = "配置名称") + private String configName; + + @Schema(description = "业务名称") + private String bizName; + + @Schema(description = "源中间表名") + private String sourceTable; + + @Schema(description = "源中间表注释") + private String sourceTableComment; + + @Schema(description = "MES目标表名") + private String targetTable; + + @Schema(description = "MES目标表注释") + private String targetTableComment; + + @Schema(description = "采集时间间隔(秒),默认1秒") + private Integer intervalSeconds; + + @Schema(description = "采集状态(0停止,1运行)") + private String status; + + @Schema(description = "采集模式(FULL全量匹配,TIME时间匹配,INCR增量匹配-标记位回写)") + private String syncMode; + + @Schema(description = "时间列/标记列(源表列名)。TIME模式=时间列;INCR模式=同步标记列(为空表示未采集,采集后回写'1')") + private String incrColumn; + + @Schema(description = "时间范围(TODAY当天,LAST7最近七天)") + private String timeWindow; + + @Schema(description = "每轮最大采集行数(INCR模式TOP N)") + private Integer batchLimit; + + @Schema(description = "增量采集高水位(INCR模式自动维护)") + private String lastWatermark; + + @Schema(description = "增量标记采集条件(IS_NULL为空,EQ_EMPTY等于匹配值,NE_EMPTY不等于匹配值),INCR模式用") + private String flagCondition; + + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】增量采集条件等于/不等于支持自定义匹配值----------- + @Schema(description = "增量标记采集条件比较值(EQ_EMPTY/NE_EMPTY 用,留空表示空字符串),INCR模式用") + private String flagMatchValue; + //update-end---author:GHT ---date:20260617 for:【MES上辅机】增量采集条件等于/不等于支持自定义匹配值----------- + + @Schema(description = "增量标记采集完成后回写值(默认1),INCR模式用") + private String flagWriteValue; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "最近一次采集时间") + private Date lastSyncTime; + + @Schema(description = "最近一次采集结果") + private String lastSyncResult; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "租户ID") + private Integer tenantId; + + private String createBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Integer delFlag; + + @TableField(exist = false) + @Schema(description = "字段映射明细(主子保存/详情用)") + private List fieldList; + + @TableField(exist = false) + @Schema(description = "采集任务是否运行中(运行态由调度器实时给出)") + private Boolean running; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncField.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncField.java new file mode 100644 index 00000000..436c305a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/entity/MesXslMcsSyncField.java @@ -0,0 +1,75 @@ +package org.jeecg.modules.xslmes.mcs.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * MES上辅机 采集字段映射(中间库源字段 → MES目标字段) + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +@Data +@TableName("mes_xsl_mcs_sync_field") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "MES上辅机采集字段映射") +public class MesXslMcsSyncField implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + @Schema(description = "采集配置ID") + private String configId; + + @Schema(description = "中间库源字段名") + private String sourceField; + + @Schema(description = "源字段注释") + private String sourceFieldComment; + + @Schema(description = "源字段类型") + private String sourceFieldType; + + @Schema(description = "MES目标字段名(接收字段)") + private String targetField; + + @Schema(description = "MES目标字段注释") + private String targetFieldComment; + + @Schema(description = "是否匹配键(0否,1是)") + private String matchKey; + + @Schema(description = "排序") + private Integer sortNo; + + @Schema(description = "租户ID") + private Integer tenantId; + + private String createBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private Integer delFlag; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/McsMetaMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/McsMetaMapper.java new file mode 100644 index 00000000..3e3480b9 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/McsMetaMapper.java @@ -0,0 +1,58 @@ +package org.jeecg.modules.xslmes.mcs.mapper; + +import com.baomidou.dynamic.datasource.annotation.DS; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 中间库(SQL Server) / MES(MySQL) 表与字段元数据查询。 + *

源表元数据走 sqlserver_mcs 数据源,目标表元数据走默认 MES 库。

+ * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +public interface McsMetaMapper { + + /** + * 中间库表清单(含表注释 MS_Description) + */ + @DS("sqlserver_mcs") + @Select("SELECT t.name AS tableName, CAST(ep.value AS NVARCHAR(200)) AS tableComment " + + "FROM sys.tables t " + + "LEFT JOIN sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.name = 'MS_Description' " + + "ORDER BY t.name") + List> listSourceTables(); + + /** + * 中间库表字段清单(含字段注释、类型) + */ + @DS("sqlserver_mcs") + @Select("SELECT c.name AS columnName, ty.name AS dataType, CAST(ep.value AS NVARCHAR(200)) AS columnComment " + + "FROM sys.columns c " + + "JOIN sys.types ty ON c.user_type_id = ty.user_type_id " + + "LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description' " + + "WHERE c.object_id = OBJECT_ID(#{table}) " + + "ORDER BY c.column_id") + List> listSourceColumns(@Param("table") String table); + + /** + * MES 业务表清单(仅 mes_xsl_ 前缀) + */ + @Select("SELECT table_name AS tableName, table_comment AS tableComment " + + "FROM information_schema.tables " + + "WHERE table_schema = (SELECT DATABASE()) AND table_name LIKE 'mes\\_xsl\\_%' " + + "ORDER BY table_name") + List> listTargetTables(); + + /** + * MES 表字段清单(含字段注释、类型) + */ + @Select("SELECT column_name AS columnName, data_type AS dataType, column_comment AS columnComment " + + "FROM information_schema.columns " + + "WHERE table_schema = (SELECT DATABASE()) AND table_name = #{table} " + + "ORDER BY ordinal_position") + List> listTargetColumns(@Param("table") String table); +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncConfigMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncConfigMapper.java new file mode 100644 index 00000000..8367f8a6 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncConfigMapper.java @@ -0,0 +1,13 @@ +package org.jeecg.modules.xslmes.mcs.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; + +/** + * MES上辅机 中间表采集配置 Mapper + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】密炼动作秒级采集 + */ +public interface MesXslMcsSyncConfigMapper extends BaseMapper { +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncFieldMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncFieldMapper.java new file mode 100644 index 00000000..689b057b --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/mapper/MesXslMcsSyncFieldMapper.java @@ -0,0 +1,13 @@ +package org.jeecg.modules.xslmes.mcs.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncField; + +/** + * MES上辅机 采集字段映射 Mapper + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +public interface MesXslMcsSyncFieldMapper extends BaseMapper { +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java new file mode 100644 index 00000000..5ebc2f0f --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/IMesXslMcsSyncConfigService.java @@ -0,0 +1,40 @@ +package org.jeecg.modules.xslmes.mcs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.jeecg.common.api.vo.Result; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; + +/** + * MES上辅机 中间表采集配置 Service(配置驱动:表/字段绑定 + 采集操作) + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +public interface IMesXslMcsSyncConfigService extends IService { + + /** + * 获取配置详情(含字段映射明细 fieldList) + */ + MesXslMcsSyncConfig getDetail(String id); + + /** + * 按业务类型获取最近配置(密炼动作页用,bizType=MIX_ACT) + */ + MesXslMcsSyncConfig getByBizType(String bizType); + + /** + * 保存配置(头 + 字段映射明细,主子整存) + */ + Result saveConfig(MesXslMcsSyncConfig config); + + /** + * 删除配置及其字段映射,并停止采集 + */ + Result deleteConfig(String id); + + /** + * 采集操作:维护是否采集、采集间隔、采集模式(全量/时间/增量)及其参数。 + * status='1' 启动并按间隔重排,'0' 停止。 + */ + Result saveCollect(MesXslMcsSyncConfig body); +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java new file mode 100644 index 00000000..cda7cbd1 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/service/impl/MesXslMcsSyncConfigServiceImpl.java @@ -0,0 +1,223 @@ +package org.jeecg.modules.xslmes.mcs.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncField; +import org.jeecg.modules.xslmes.mcs.mapper.MesXslMcsSyncConfigMapper; +import org.jeecg.modules.xslmes.mcs.mapper.MesXslMcsSyncFieldMapper; +import org.jeecg.modules.xslmes.mcs.service.IMesXslMcsSyncConfigService; +import org.jeecg.modules.xslmes.mcs.sync.McsSyncScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * MES上辅机 中间表采集配置 Service 实现 + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +@Slf4j +@Service +public class MesXslMcsSyncConfigServiceImpl extends ServiceImpl + implements IMesXslMcsSyncConfigService { + + @Autowired + private MesXslMcsSyncFieldMapper syncFieldMapper; + + @Autowired + private McsSyncScheduler syncScheduler; + + @Override + public MesXslMcsSyncConfig getDetail(String id) { + MesXslMcsSyncConfig cfg = getById(id); + if (cfg == null) { + return null; + } + cfg.setFieldList(listFields(id)); + return cfg; + } + + @Override + public MesXslMcsSyncConfig getByBizType(String bizType) { + return getOne(new LambdaQueryWrapper() + .eq(MesXslMcsSyncConfig::getBizType, bizType) + .eq(MesXslMcsSyncConfig::getDelFlag, 0) + .orderByDesc(MesXslMcsSyncConfig::getUpdateTime) + .last("LIMIT 1"), false); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Result saveConfig(MesXslMcsSyncConfig config) { + if (config == null) { + return Result.error("配置不能为空"); + } + if (StringUtils.isBlank(config.getSourceTable())) { + return Result.error("请选择中间库源表"); + } + if (StringUtils.isBlank(config.getTargetTable())) { + return Result.error("请选择MES目标表"); + } + List fields = config.getFieldList() != null ? config.getFieldList() : new ArrayList<>(); + // 至少一个有效映射 + boolean hasValid = fields.stream().anyMatch(f -> StringUtils.isNotBlank(f.getSourceField()) + && StringUtils.isNotBlank(f.getTargetField())); + if (!hasValid) { + return Result.error("请至少配置一个字段映射(源字段+接收字段)"); + } + + String username = currentUsername(); + Date now = new Date(); + if (config.getIntervalSeconds() == null || config.getIntervalSeconds() < 1) { + config.setIntervalSeconds(1); + } + if (config.getTenantId() == null) { + config.setTenantId(0); + } + config.setDelFlag(0); + config.setUpdateBy(username); + config.setUpdateTime(now); + + boolean isUpdate = StringUtils.isNotBlank(config.getId()); + if (isUpdate) { + MesXslMcsSyncConfig old = getById(config.getId()); + if (old == null) { + return Result.error("配置不存在"); + } + // 状态由采集操作维护,保存配置不改变运行状态 + config.setStatus(old.getStatus()); + updateById(config); + } else { + if (StringUtils.isBlank(config.getStatus())) { + config.setStatus("0"); + } + config.setCreateBy(username); + config.setCreateTime(now); + save(config); + } + + // 整存字段映射:先物理删除旧映射再插入 + syncFieldMapper.delete(new LambdaQueryWrapper() + .eq(MesXslMcsSyncField::getConfigId, config.getId())); + int sort = 0; + for (MesXslMcsSyncField f : fields) { + if (StringUtils.isBlank(f.getSourceField())) { + continue; + } + f.setId(null); + f.setConfigId(config.getId()); + f.setSortNo(sort++); + f.setTenantId(config.getTenantId()); + f.setDelFlag(0); + f.setCreateBy(username); + f.setCreateTime(now); + f.setUpdateBy(username); + f.setUpdateTime(now); + syncFieldMapper.insert(f); + } + + // 运行中则按新配置重排(间隔/映射即时生效) + if ("1".equals(config.getStatus())) { + syncScheduler.scheduleTask(getById(config.getId())); + } + return Result.OK(isUpdate ? "保存成功" : "新增成功"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Result deleteConfig(String id) { + MesXslMcsSyncConfig cfg = getById(id); + if (cfg == null) { + return Result.error("配置不存在"); + } + syncScheduler.cancelTask(id); + syncFieldMapper.delete(new LambdaQueryWrapper() + .eq(MesXslMcsSyncField::getConfigId, id)); + removeById(id); + return Result.OK("删除成功"); + } + + @Override + public Result saveCollect(MesXslMcsSyncConfig body) { + if (body == null || StringUtils.isBlank(body.getId())) { + return Result.error("缺少配置ID"); + } + MesXslMcsSyncConfig cfg = getById(body.getId()); + if (cfg == null) { + return Result.error("配置不存在"); + } + if (body.getIntervalSeconds() != null) { + if (body.getIntervalSeconds() < 1) { + return Result.error("采集间隔不能小于1秒"); + } + cfg.setIntervalSeconds(body.getIntervalSeconds()); + } + // 采集模式及参数 + String mode = StringUtils.isBlank(body.getSyncMode()) ? "FULL" : body.getSyncMode().trim().toUpperCase(); + cfg.setSyncMode(mode); + cfg.setIncrColumn(StringUtils.trimToNull(body.getIncrColumn())); + cfg.setTimeWindow(StringUtils.isBlank(body.getTimeWindow()) ? "TODAY" : body.getTimeWindow()); + if (body.getBatchLimit() != null && body.getBatchLimit() > 0) { + cfg.setBatchLimit(body.getBatchLimit()); + } + // INCR(标记回写):采集条件 + 回写值(可视化配置,回写值默认"1") + cfg.setFlagCondition(StringUtils.isBlank(body.getFlagCondition()) ? "IS_NULL" : body.getFlagCondition().trim().toUpperCase()); + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】增量采集条件等于/不等于支持自定义匹配值----------- + cfg.setFlagMatchValue(body.getFlagMatchValue()); + //update-end---author:GHT ---date:20260617 for:【MES上辅机】增量采集条件等于/不等于支持自定义匹配值----------- + cfg.setFlagWriteValue(StringUtils.isBlank(body.getFlagWriteValue()) ? "1" : body.getFlagWriteValue()); + if (("TIME".equals(mode) || "INCR".equals(mode)) && StringUtils.isBlank(cfg.getIncrColumn())) { + return Result.error("时间匹配/增量匹配需选择" + ("TIME".equals(mode) ? "时间列" : "标记列")); + } + + boolean on = "1".equals(body.getStatus()); + cfg.setStatus(on ? "1" : "0"); + cfg.setUpdateBy(currentUsername()); + cfg.setUpdateTime(new Date()); + updateById(cfg); + if (on) { + syncScheduler.scheduleTask(cfg); + return Result.OK("已启动采集(" + modeText(mode) + "),间隔 " + cfg.getIntervalSeconds() + " 秒"); + } + syncScheduler.cancelTask(cfg.getId()); + return Result.OK("已停止采集"); + } + + private String modeText(String mode) { + switch (mode) { + case "TIME": + return "时间匹配"; + case "INCR": + return "增量匹配"; + default: + return "全量匹配"; + } + } + + private List listFields(String configId) { + return syncFieldMapper.selectList(new LambdaQueryWrapper() + .eq(MesXslMcsSyncField::getConfigId, configId) + .eq(MesXslMcsSyncField::getDelFlag, 0) + .orderByAsc(MesXslMcsSyncField::getSortNo)); + } + + private String currentUsername() { + try { + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + return user != null ? user.getUsername() : "system"; + } catch (Exception e) { + return "system"; + } + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java new file mode 100644 index 00000000..9460efb6 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/GenericMcsSyncEngine.java @@ -0,0 +1,524 @@ +package org.jeecg.modules.xslmes.mcs.sync; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.modules.xslmes.mcs.datasource.McsDataSourceManager; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncField; +import org.jeecg.modules.xslmes.mcs.mapper.McsMetaMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 通用中间表采集引擎(配置驱动,纯字段拷贝)。 + *

支持三种采集模式,应对中间库不同规模的表:

+ *
    + *
  • FULL 全量匹配:全表读源+全表读目标→按匹配键 Upsert,仅写新增/变化行。适合小状态表、以更新为主。
  • + *
  • TIME 时间匹配:按时间列只取窗口内数据(当天/最近七天)→按匹配键 Upsert,目标侧按窗口匹配键定向读取。避免全表扫描。
  • + *
  • INCR 增量匹配(标记位回写):源表选一「同步标记列」,仅采集该列为空(NULL/'')的行(TOP N 限流), + * 按匹配键 Upsert 到 MES 后,回写源表该列为 {@code '1'},下轮不再重复采集。适合带 GUID 主键、无可靠递增列的流水表。
  • + *
+ * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +@Slf4j +@Component +public class GenericMcsSyncEngine { + + public static final String MODE_FULL = "FULL"; + public static final String MODE_TIME = "TIME"; + public static final String MODE_INCR = "INCR"; + + /** 合法标识符(表名/列名),防止 SQL 注入 */ + private static final Pattern IDENT = Pattern.compile("^[A-Za-z0-9_]+$"); + + /** 批量写入分批大小 */ + private static final int BATCH_SIZE = 500; + /** IN 查询分块大小 */ + private static final int IN_CHUNK = 1000; + /** INCR 默认每轮行数 */ + private static final int DEFAULT_BATCH_LIMIT = 2000; + /** INCR 标记位回写后的已同步标识值 */ + private static final String FLAG_SYNCED = "1"; + + @Autowired + private DataSource dataSource; + + @Autowired + private McsMetaMapper metaMapper; + + @Autowired + private org.jeecg.modules.xslmes.mcs.datasource.McsDataSourceManager mcsDataSourceManager; + + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】采集模式 全量/时间/增量----------- + public String sync(MesXslMcsSyncConfig cfg, List fields) { + String sourceTable = trim(cfg.getSourceTable()); + String targetTable = trim(cfg.getTargetTable()); + if (StringUtils.isBlank(sourceTable) || StringUtils.isBlank(targetTable)) { + return "未配置源表或目标表,跳过"; + } + validateIdent(sourceTable); + validateIdent(targetTable); + + List maps = fields == null ? List.of() : fields.stream() + .filter(f -> StringUtils.isNotBlank(f.getSourceField()) && StringUtils.isNotBlank(f.getTargetField())) + .collect(Collectors.toList()); + if (maps.isEmpty()) { + return "无有效字段映射,跳过"; + } + for (MesXslMcsSyncField f : maps) { + validateIdent(f.getSourceField()); + validateIdent(f.getTargetField()); + } + + String mode = StringUtils.isBlank(cfg.getSyncMode()) ? MODE_FULL : cfg.getSyncMode().trim().toUpperCase(); + JdbcTemplate sourceJt = new JdbcTemplate(getSourceDataSource()); + JdbcTemplate targetJt = new JdbcTemplate(dataSource); + + // 目标表标准字段探测 + 自动填充列 + Set targetCols = metaMapper.listTargetColumns(targetTable).stream() + .map(m -> String.valueOf(m.get("columnName")).toLowerCase()) + .collect(Collectors.toSet()); + boolean hasDel = targetCols.contains("del_flag"); + + List keyMaps = maps.stream().filter(f -> "1".equals(f.getMatchKey())).collect(Collectors.toList()); + Set keyTargetsLower = keyMaps.stream().map(k -> k.getTargetField().toLowerCase()).collect(Collectors.toSet()); + List nonKeyMaps = maps.stream() + .filter(f -> !keyTargetsLower.contains(f.getTargetField().toLowerCase())) + .collect(Collectors.toList()); + + int tenantId = cfg.getTenantId() != null ? cfg.getTenantId() : 0; + Timestamp now = new Timestamp(System.currentTimeMillis()); + + AutoCols auto = buildAutoCols(targetCols, maps, tenantId, now); + + // 1. 按模式读源数据 + LinkedHashSet srcCols = maps.stream().map(MesXslMcsSyncField::getSourceField) + .collect(Collectors.toCollection(LinkedHashSet::new)); + List> rows; + + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】增量匹配改为标记位回写----------- + // INCR(标记回写)模式:仅采集「标记列」为空的行,采完回写"1",下轮不再重复采集 + String flagCol = null; + boolean flagMode = MODE_INCR.equals(mode); + + if (flagMode) { + flagCol = requireIncrColumn(cfg); + if (keyMaps.isEmpty()) { + return "增量(标记)采集需在字段映射中勾选至少一个匹配键作为回写主键(如 GUID)"; + } + if (!mcsDataSourceManager.isWriteEnabled()) { + return "增量(标记)采集需开启中间库写入开关以回写同步标记,请在「中间库连接配置」开启写入"; + } + srcCols.add(flagCol); + int limit = cfg.getBatchLimit() != null && cfg.getBatchLimit() > 0 ? cfg.getBatchLimit() : DEFAULT_BATCH_LIMIT; + String predicate = flagPredicate(flagCol, cfg.getFlagCondition(), cfg.getFlagMatchValue()); + String sql = "SELECT TOP " + limit + " " + colList(srcCols) + " FROM [" + sourceTable + "]" + + " WHERE (" + predicate + ")"; + rows = sourceJt.queryForList(sql); + if (rows.isEmpty()) { + return "增量采集:无待采集数据"; + } + } else if (MODE_TIME.equals(mode)) { + String incrCol = requireIncrColumn(cfg); + Timestamp[] window = timeWindow(cfg.getTimeWindow(), now); + StringBuilder sql = new StringBuilder("SELECT ").append(colList(srcCols)) + .append(" FROM [").append(sourceTable).append("] WHERE [").append(incrCol).append("] >= ?"); + List args = new ArrayList<>(); + args.add(window[0]); + if (window[1] != null) { + sql.append(" AND [").append(incrCol).append("] < ?"); + args.add(window[1]); + } + rows = sourceJt.queryForList(sql.toString(), args.toArray()); + } else { + // FULL + rows = sourceJt.queryForList("SELECT " + colList(srcCols) + " FROM [" + sourceTable + "]"); + } + + if (rows.isEmpty()) { + return ("TIME".equals(mode) ? "时间匹配" : "全量匹配") + ":窗口/源表无数据,未更新"; + } + + // 无匹配键 → 整批追加 + if (keyMaps.isEmpty()) { + int ins = appendInsert(targetJt, targetTable, maps, auto, rows); + return String.format("采集完成(无匹配键,追加):新增%d,源%d条", ins, rows.size()); + } + + // 2. 加载现有目标数据(FULL 全量;TIME/INCR 仅按本批匹配键定向读取) + LinkedHashSet existCols = new LinkedHashSet<>(); + keyMaps.forEach(k -> existCols.add(k.getTargetField())); + maps.forEach(m -> existCols.add(m.getTargetField())); + Map> existingByKey = (MODE_TIME.equals(mode) || flagMode) + ? loadExistingByKeys(targetJt, targetTable, existCols, keyMaps, hasDel, rows) + : loadExistingAll(targetJt, targetTable, existCols, keyMaps, hasDel); + + // 3. 比对 → 批量 Upsert + List updateSetCols = nonKeyMaps.stream().map(MesXslMcsSyncField::getTargetField).collect(Collectors.toList()); + boolean updTime = targetCols.contains("update_time") && !mappedContains(maps, "update_time"); + boolean updBy = targetCols.contains("update_by") && !mappedContains(maps, "update_by"); + String updateSql = buildUpdateSql(targetTable, updateSetCols, keyMaps, updTime, updBy, hasDel); + String insertSql = buildInsertSql(targetTable, maps, auto); + + List insertArgs = new ArrayList<>(); + List updateArgs = new ArrayList<>(); + Set handled = new HashSet<>(); + int unchanged = 0; + + for (Map row : rows) { + Map rci = ci(row); + String key = buildKeyFromSource(keyMaps, rci); + if (!handled.add(key)) { + continue; + } + Map existing = existingByKey.get(key); + if (existing == null) { + insertArgs.add(buildInsertArgs(maps, rci, auto)); + } else if (updateSetCols.isEmpty()) { + unchanged++; + } else if (isChanged(nonKeyMaps, rci, existing)) { + updateArgs.add(buildUpdateArgs(nonKeyMaps, keyMaps, rci, updTime, updBy, now)); + } else { + unchanged++; + } + } + + int ins = batch(targetJt, insertSql, insertArgs); + int upd = updateSetCols.isEmpty() ? 0 : batch(targetJt, updateSql, updateArgs); + + // INCR(标记回写):对本批所有源行回写标记值,下轮不再采集 + if (flagMode) { + String writeValue = StringUtils.isBlank(cfg.getFlagWriteValue()) ? FLAG_SYNCED : cfg.getFlagWriteValue(); + int marked = writeBackFlag(sourceJt, sourceTable, flagCol, cfg.getFlagCondition(), cfg.getFlagMatchValue(), writeValue, keyMaps, rows); + return String.format("增量采集:新增%d,更新%d,未变%d,回写标记%d,源%d条", + ins, upd, unchanged, marked, rows.size()); + } + return String.format("%s:新增%d,更新%d,未变%d,源%d条", + "TIME".equals(mode) ? "时间匹配" : "全量匹配", ins, upd, unchanged, rows.size()); + //update-end---author:GHT ---date:20260617 for:【MES上辅机】增量匹配改为标记位回写----------- + } + + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】增量匹配改为标记位回写----------- + /** + * INCR 标记采集条件:根据配置构造源表标记列的判定谓词(SELECT 取数 + 回写守卫共用)。 + *
    + *
  • {@code IS_NULL} 为空:{@code [col] IS NULL}
  • + *
  • {@code EQ_EMPTY} 等于:{@code [col] = '<匹配值>'}(匹配值留空时退化为等于空串)
  • + *
  • {@code NE_EMPTY} 不等于:{@code [col] <> '<匹配值>'}(匹配值留空时退化为不等于空串)
  • + *
+ * @param matchValue EQ_EMPTY/NE_EMPTY 的比较值,由用户填写,留空表示空字符串 + */ + private String flagPredicate(String flagCol, String condition, String matchValue) { + String c = StringUtils.isBlank(condition) ? "IS_NULL" : condition.trim().toUpperCase(); + switch (c) { + case "EQ_EMPTY": + return "[" + flagCol + "] = '" + sqlLiteral(matchValue) + "'"; + case "NE_EMPTY": + return "[" + flagCol + "] <> '" + sqlLiteral(matchValue) + "'"; + case "IS_NULL": + default: + return "[" + flagCol + "] IS NULL"; + } + } + + /** 将用户填写的匹配值转义为 SQL 字符串字面量内容(单引号翻倍),防止注入。 */ + private String sqlLiteral(String value) { + return value == null ? "" : value.replace("'", "''"); + } + + /** + * INCR 标记回写:把本批读到的源行该标记列回写为配置的回写值。 + *

仅按匹配键精确定位本批读到的行(而非整列条件批量更新), + * 避免误标在本轮 SELECT 之后才进入中间库、尚未采集的新数据; + * 并以采集条件谓词做守卫,避开本轮已被其他进程改动的行。

+ */ + private int writeBackFlag(JdbcTemplate sourceJt, String sourceTable, String flagCol, String condition, + String matchValue, String writeValue, List keyMaps, List> rows) { + validateIdent(flagCol); + StringBuilder sql = new StringBuilder("UPDATE [").append(sourceTable).append("] SET [") + .append(flagCol).append("] = ? WHERE "); + sql.append(keyMaps.stream().map(k -> "[" + k.getSourceField() + "] = ?").collect(Collectors.joining(" AND "))); + sql.append(" AND (").append(flagPredicate(flagCol, condition, matchValue)).append(")"); + List argsList = new ArrayList<>(); + Set handled = new HashSet<>(); + for (Map row : rows) { + Map rci = ci(row); + String key = buildKeyFromSource(keyMaps, rci); + if (!handled.add(key)) { + continue; + } + List args = new ArrayList<>(keyMaps.size() + 1); + args.add(writeValue); + for (MesXslMcsSyncField k : keyMaps) { + args.add(rci.get(k.getSourceField())); + } + argsList.add(args.toArray()); + } + return batch(sourceJt, sql.toString(), argsList); + } + //update-end---author:GHT ---date:20260617 for:【MES上辅机】增量匹配改为标记位回写----------- + + // ---------------- 现有数据加载 ---------------- + + private Map> loadExistingAll(JdbcTemplate jt, String table, LinkedHashSet existCols, + List keyMaps, boolean hasDel) { + String sql = "SELECT " + colListBt(existCols) + " FROM `" + table + "`" + (hasDel ? " WHERE `del_flag` = 0" : ""); + Map> map = new HashMap<>(); + for (Map er : jt.queryForList(sql)) { + Map eci = ci(er); + map.put(buildKeyFromTarget(keyMaps, eci), eci); + } + return map; + } + + private Map> loadExistingByKeys(JdbcTemplate jt, String table, LinkedHashSet existCols, + List keyMaps, boolean hasDel, + List> rows) { + Map> map = new HashMap<>(); + MesXslMcsSyncField firstKey = keyMaps.get(0); + // 收集窗口内首匹配键去重值 + LinkedHashSet values = new LinkedHashSet<>(); + for (Map row : rows) { + Object v = ci(row).get(firstKey.getSourceField()); + if (v != null) { + values.add(v); + } + } + if (values.isEmpty()) { + return map; + } + List valueList = new ArrayList<>(values); + for (int i = 0; i < valueList.size(); i += IN_CHUNK) { + List part = valueList.subList(i, Math.min(i + IN_CHUNK, valueList.size())); + String ph = part.stream().map(x -> "?").collect(Collectors.joining(",")); + String sql = "SELECT " + colListBt(existCols) + " FROM `" + table + "` WHERE `" + + firstKey.getTargetField() + "` IN (" + ph + ")" + (hasDel ? " AND `del_flag` = 0" : ""); + for (Map er : jt.queryForList(sql, part.toArray())) { + Map eci = ci(er); + map.put(buildKeyFromTarget(keyMaps, eci), eci); + } + } + return map; + } + + // ---------------- 追加写入 ---------------- + + private int appendInsert(JdbcTemplate jt, String table, List maps, AutoCols auto, + List> rows) { + List insertArgs = new ArrayList<>(); + for (Map row : rows) { + insertArgs.add(buildInsertArgs(maps, ci(row), auto)); + } + return batch(jt, buildInsertSql(table, maps, auto), insertArgs); + } + + // ---------------- SQL 构建 ---------------- + + private String buildInsertSql(String table, List maps, AutoCols auto) { + List cols = new ArrayList<>(); + maps.forEach(m -> cols.add(m.getTargetField())); + if (auto.id) { + cols.add("id"); + } + cols.addAll(auto.cols); + String colSql = cols.stream().map(c -> "`" + c + "`").collect(Collectors.joining(",")); + String ph = cols.stream().map(c -> "?").collect(Collectors.joining(",")); + return "INSERT INTO `" + table + "` (" + colSql + ") VALUES (" + ph + ")"; + } + + private Object[] buildInsertArgs(List maps, Map ci, AutoCols auto) { + List args = new ArrayList<>(maps.size() + auto.vals.size() + 1); + for (MesXslMcsSyncField m : maps) { + args.add(ci.get(m.getSourceField())); + } + if (auto.id) { + args.add(IdWorker.getIdStr()); + } + args.addAll(auto.vals); + return args.toArray(); + } + + private String buildUpdateSql(String table, List setCols, List keyMaps, + boolean updTime, boolean updBy, boolean hasDel) { + if (setCols.isEmpty()) { + return null; + } + StringBuilder sql = new StringBuilder("UPDATE `").append(table).append("` SET "); + sql.append(setCols.stream().map(c -> "`" + c + "` = ?").collect(Collectors.joining(","))); + if (updTime) { + sql.append(", `update_time` = ?"); + } + if (updBy) { + sql.append(", `update_by` = ?"); + } + sql.append(" WHERE "); + sql.append(keyMaps.stream().map(k -> "`" + k.getTargetField() + "` = ?").collect(Collectors.joining(" AND "))); + if (hasDel) { + sql.append(" AND `del_flag` = 0"); + } + return sql.toString(); + } + + private Object[] buildUpdateArgs(List nonKeyMaps, List keyMaps, + Map ci, boolean updTime, boolean updBy, Timestamp now) { + List args = new ArrayList<>(); + for (MesXslMcsSyncField m : nonKeyMaps) { + args.add(ci.get(m.getSourceField())); + } + if (updTime) { + args.add(now); + } + if (updBy) { + args.add("mcs-sync"); + } + for (MesXslMcsSyncField k : keyMaps) { + args.add(ci.get(k.getSourceField())); + } + return args.toArray(); + } + + // ---------------- 工具 ---------------- + + /** 自动填充列汇总(id 单独标记,因每行不同) */ + private static class AutoCols { + boolean id; + final List cols = new ArrayList<>(); + final List vals = new ArrayList<>(); + } + + private AutoCols buildAutoCols(Set targetCols, List maps, int tenantId, Timestamp now) { + AutoCols a = new AutoCols(); + a.id = targetCols.contains("id") && !mappedContains(maps, "id"); + addAuto(a, targetCols, maps, "create_time", now); + addAuto(a, targetCols, maps, "update_time", now); + addAuto(a, targetCols, maps, "create_by", "mcs-sync"); + addAuto(a, targetCols, maps, "update_by", "mcs-sync"); + addAuto(a, targetCols, maps, "tenant_id", tenantId); + addAuto(a, targetCols, maps, "del_flag", 0); + return a; + } + + private void addAuto(AutoCols a, Set targetCols, List maps, String col, Object val) { + if (targetCols.contains(col) && !mappedContains(maps, col)) { + a.cols.add(col); + a.vals.add(val); + } + } + + /** 返回 [start, end],end 可为 null */ + private Timestamp[] timeWindow(String window, Timestamp now) { + String w = StringUtils.isBlank(window) ? "TODAY" : window.trim().toUpperCase(); + if ("LAST7".equals(w)) { + return new Timestamp[]{Timestamp.valueOf(LocalDateTime.now().minusDays(7)), null}; + } + // 默认当天 + Timestamp start = Timestamp.valueOf(LocalDate.now().atStartOfDay()); + Timestamp end = Timestamp.valueOf(LocalDate.now().plusDays(1).atStartOfDay()); + return new Timestamp[]{start, end}; + } + + private String requireIncrColumn(MesXslMcsSyncConfig cfg) { + String col = trim(cfg.getIncrColumn()); + if (StringUtils.isBlank(col)) { + throw new IllegalArgumentException("当前采集模式需指定标记列/时间列,请在采集操作中配置"); + } + validateIdent(col); + return col; + } + + private int batch(JdbcTemplate jt, String sql, List argsList) { + if (sql == null || argsList.isEmpty()) { + return 0; + } + int total = 0; + for (int i = 0; i < argsList.size(); i += BATCH_SIZE) { + List part = argsList.subList(i, Math.min(i + BATCH_SIZE, argsList.size())); + jt.batchUpdate(sql, part); + total += part.size(); + } + return total; + } + + private boolean isChanged(List nonKeyMaps, Map ci, Map existing) { + for (MesXslMcsSyncField m : nonKeyMaps) { + if (!normVal(ci.get(m.getSourceField())).equals(normVal(existing.get(m.getTargetField())))) { + return true; + } + } + return false; + } + + private String buildKeyFromSource(List keyMaps, Map ci) { + return keyMaps.stream().map(k -> normKey(ci.get(k.getSourceField()))).collect(Collectors.joining("||")); + } + + private String buildKeyFromTarget(List keyMaps, Map eci) { + return keyMaps.stream().map(k -> normKey(eci.get(k.getTargetField()))).collect(Collectors.joining("||")); + } + + private Map ci(Map row) { + Map m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + m.putAll(row); + return m; + } + + private String colList(LinkedHashSet cols) { + return cols.stream().map(c -> "[" + c + "]").collect(Collectors.joining(",")); + } + + private String colListBt(LinkedHashSet cols) { + return cols.stream().map(c -> "`" + c + "`").collect(Collectors.joining(",")); + } + + private String normKey(Object v) { + return v == null ? "" : String.valueOf(v).trim(); + } + + private String normVal(Object v) { + return v == null ? " " : String.valueOf(v); + } + + private boolean mappedContains(List maps, String targetCol) { + return maps.stream().anyMatch(m -> m.getTargetField() != null && m.getTargetField().equalsIgnoreCase(targetCol)); + } + + private DataSource getSourceDataSource() { + DynamicRoutingDataSource routing = (DynamicRoutingDataSource) dataSource; + DataSource src = routing.getDataSources().get(McsDataSourceManager.DS_KEY); + if (src == null) { + throw new IllegalStateException("中间库数据源 " + McsDataSourceManager.DS_KEY + " 未注册"); + } + return src; + } + + private void validateIdent(String name) { + if (name == null || !IDENT.matcher(name).matches()) { + throw new IllegalArgumentException("非法的表名或字段名: " + name); + } + } + + private String trim(String s) { + return s == null ? null : s.trim(); + } + //update-end---author:GHT ---date:20260617 for:【MES上辅机】采集模式 全量/时间/增量----------- +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java new file mode 100644 index 00000000..1799d1bb --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/sync/McsSyncScheduler.java @@ -0,0 +1,162 @@ +package org.jeecg.modules.xslmes.mcs.sync; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.modules.xslmes.mcs.datasource.McsDataSourceManager; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncField; +import org.jeecg.modules.xslmes.mcs.mapper.MesXslMcsSyncConfigMapper; +import org.jeecg.modules.xslmes.mcs.mapper.MesXslMcsSyncFieldMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import java.time.Duration; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +/** + * 中间表采集调度器(通用,配置驱动)。 + *

基于 {@link ThreadPoolTaskScheduler} 为每个运行中的采集配置维护一个可重排的定时任务, + * 支持秒级间隔、运行时改间隔、启动/停止。每次触发调用 {@link GenericMcsSyncEngine} 执行采集。

+ * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +@Slf4j +@Component +public class McsSyncScheduler { + + private static final String LOG_TAG = "[MCS采集]"; + + @Autowired + private MesXslMcsSyncConfigMapper syncConfigMapper; + + @Autowired + private MesXslMcsSyncFieldMapper syncFieldMapper; + + @Autowired + private McsDataSourceManager mcsDataSourceManager; + + @Autowired + private GenericMcsSyncEngine syncEngine; + + /** 运行中的定时任务,configId -> future */ + private final Map> runningTasks = new ConcurrentHashMap<>(); + + private ThreadPoolTaskScheduler taskScheduler; + + @PostConstruct + public void init() { + taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setPoolSize(4); + taskScheduler.setThreadNamePrefix("mcs-sync-"); + taskScheduler.setWaitForTasksToCompleteOnShutdown(true); + taskScheduler.setAwaitTerminationSeconds(10); + taskScheduler.initialize(); + log.info("{} 采集调度器初始化完成", LOG_TAG); + } + + @PreDestroy + public void destroy() { + runningTasks.values().forEach(f -> f.cancel(false)); + runningTasks.clear(); + if (taskScheduler != null) { + taskScheduler.shutdown(); + } + } + + /** + * 应用启动后,加载所有 status=1 的采集配置并启动定时任务 + */ + @EventListener(ApplicationReadyEvent.class) + public void loadOnStartup() { + try { + List configs = syncConfigMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslMcsSyncConfig::getDelFlag, 0) + .eq(MesXslMcsSyncConfig::getStatus, "1")); + configs.forEach(this::scheduleTask); + log.info("{} 启动加载采集任务完成,已启动={}", LOG_TAG, configs.size()); + } catch (Exception e) { + log.error("{} 启动加载采集任务失败: {}", LOG_TAG, e.getMessage(), e); + } + } + + public boolean isRunning(String configId) { + ScheduledFuture f = runningTasks.get(configId); + return f != null && !f.isCancelled(); + } + + /** + * (重新)按配置的间隔调度采集任务。已存在则先取消再重排,实现运行时改间隔。 + */ + public synchronized void scheduleTask(MesXslMcsSyncConfig config) { + if (config == null || config.getId() == null) { + return; + } + cancelTask(config.getId()); + long seconds = config.getIntervalSeconds() != null && config.getIntervalSeconds() > 0 + ? config.getIntervalSeconds() : 1L; + String configId = config.getId(); + ScheduledFuture future = taskScheduler.scheduleWithFixedDelay( + () -> runOnce(configId), Duration.ofSeconds(seconds)); + runningTasks.put(configId, future); + log.info("{} 采集任务已启动 configId={} 间隔={}s", LOG_TAG, configId, seconds); + } + + /** + * 取消采集任务(仅停内存定时,不改库) + */ + public synchronized void cancelTask(String configId) { + ScheduledFuture old = runningTasks.remove(configId); + if (old != null) { + old.cancel(false); + log.info("{} 采集任务已停止 configId={}", LOG_TAG, configId); + } + } + + /** + * 单次采集执行:连接/读取开关守卫 + 调用通用引擎 + 落库结果 + */ + private void runOnce(String configId) { + MesXslMcsSyncConfig cfg = syncConfigMapper.selectById(configId); + if (cfg == null || cfg.getDelFlag() != null && cfg.getDelFlag() == 1 || !"1".equals(cfg.getStatus())) { + return; + } + // 中间库未启用或读取开关关闭时安静跳过 + if (!mcsDataSourceManager.isDbConfigActive() || !mcsDataSourceManager.isReadEnabled()) { + log.debug("{} 中间库未就绪或读取关闭,跳过 configId={}", LOG_TAG, configId); + return; + } + try { + List fields = syncFieldMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslMcsSyncField::getConfigId, configId) + .eq(MesXslMcsSyncField::getDelFlag, 0) + .orderByAsc(MesXslMcsSyncField::getSortNo)); + String result = syncEngine.sync(cfg, fields); + // INCR 改为标记位回写后不再维护高水位,仅落库采集结果 + updateSyncResult(configId, result, null); + } catch (Exception e) { + log.error("{} 采集异常 configId={}: {}", LOG_TAG, configId, e.getMessage(), e); + updateSyncResult(configId, "采集失败:" + e.getMessage(), null); + } + } + + private void updateSyncResult(String id, String result, String watermark) { + MesXslMcsSyncConfig update = new MesXslMcsSyncConfig(); + update.setId(id); + update.setLastSyncTime(new Date()); + update.setLastSyncResult(result != null && result.length() > 480 ? result.substring(0, 480) : result); + update.setLastWatermark(watermark); + syncConfigMapper.updateById(update); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerActionService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerActionService.java index 10b4785a..9bf4a27d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerActionService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerActionService.java @@ -5,9 +5,9 @@ import org.jeecg.modules.xslmes.entity.MesXslMixerAction; public interface IMesXslMixerActionService extends IService { - boolean isActionNameDuplicated(String actionName, String excludeId); + boolean isActionNameDuplicated(String equipmentId, String actionName, String excludeId); - boolean isActionCodeDuplicated(String actionCode, String excludeId); + boolean isActionCodeDuplicated(String equipmentId, String actionCode, String excludeId); void fillEquipmentName(MesXslMixerAction model); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerActionServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerActionServiceImpl.java index 52879025..d090cd77 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerActionServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerActionServiceImpl.java @@ -17,13 +17,16 @@ public class MesXslMixerActionServiceImpl extends ServiceImpl wrapper = - new LambdaQueryWrapper().eq(MesXslMixerAction::getActionName, actionName.trim()); + new LambdaQueryWrapper() + .eq(MesXslMixerAction::getEquipmentId, equipmentId.trim()) + .eq(MesXslMixerAction::getActionName, actionName.trim()); if (StringUtils.isNotBlank(excludeId)) { wrapper.ne(MesXslMixerAction::getId, excludeId.trim()); } @@ -31,17 +34,20 @@ public class MesXslMixerActionServiceImpl extends ServiceImpl wrapper = - new LambdaQueryWrapper().eq(MesXslMixerAction::getActionCode, actionCode.trim()); + new LambdaQueryWrapper() + .eq(MesXslMixerAction::getEquipmentId, equipmentId.trim()) + .eq(MesXslMixerAction::getActionCode, actionCode.trim()); if (StringUtils.isNotBlank(excludeId)) { wrapper.ne(MesXslMixerAction::getId, excludeId.trim()); } return this.count(wrapper) > 0; } + //update-end---author:GHT ---date:20260617 for:【MES上辅机】密炼动作秒级采集-唯一性改为(设备+动作代号)同设备内唯一----------- @Override public void fillEquipmentName(MesXslMixerAction model) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_153__mes_xsl_mcs_sync_config.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_153__mes_xsl_mcs_sync_config.sql new file mode 100644 index 00000000..a681c3d9 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_153__mes_xsl_mcs_sync_config.sql @@ -0,0 +1,73 @@ +-- 【MES上辅机】密炼动作秒级采集 +-- 1. 通用中间表采集配置表(可被密炼动作/报警/配方等多功能复用,按 biz_type 区分) +-- 2. 密炼机动作维护表补全机台字段、放开台账关联、调整唯一键为(设备+动作代号) +-- 3. 密炼动作菜单下新增 启动采集/停止采集/采集设置 按钮权限 + +-- ===================== 1. 通用采集配置表 ===================== +CREATE TABLE IF NOT EXISTS `mes_xsl_mcs_sync_config` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `biz_type` varchar(50) NOT NULL COMMENT '业务类型(采集任务唯一标识,如 MIX_ACT 密炼动作)', + `biz_name` varchar(100) DEFAULT NULL COMMENT '业务名称', + `source_table` varchar(100) DEFAULT NULL COMMENT '源中间表名', + `interval_seconds` int NOT NULL DEFAULT '1' COMMENT '采集时间间隔(秒),默认1秒', + `status` varchar(1) NOT NULL DEFAULT '0' COMMENT '采集状态(0停止,1运行)', + `last_sync_time` datetime DEFAULT NULL COMMENT '最近一次采集时间', + `last_sync_result` varchar(500) DEFAULT NULL COMMENT '最近一次采集结果', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `tenant_id` int DEFAULT '0' COMMENT '租户', + `create_by` varchar(100) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(100) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` int DEFAULT '0' COMMENT '删除标记(0正常,1删除)', + PRIMARY KEY (`id`), + KEY `idx_mscfg_biz` (`biz_type`, `tenant_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES上辅机中间表采集配置(通用)'; + +-- 初始化密炼动作采集配置(默认间隔1秒、默认停止) +INSERT INTO `mes_xsl_mcs_sync_config` + (`id`, `biz_type`, `biz_name`, `source_table`, `interval_seconds`, `status`, `remark`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`) +SELECT '1900000000000000860', 'MIX_ACT', '密炼机动作', 'MCSToMES_MixAct', 1, '0', '密炼机动作维护数据采集', 0, 'admin', NOW(), 'admin', NOW(), 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `mes_xsl_mcs_sync_config` WHERE `biz_type` = 'MIX_ACT' AND `tenant_id` = 0); + +-- ===================== 2. 密炼机动作维护表补全字段 ===================== +-- 机台编号、机台类型(采集自中间表 EquipID/EquipType) +ALTER TABLE `mes_xsl_mixer_action` + ADD COLUMN `equip_id` varchar(50) DEFAULT NULL COMMENT '机台编号(采集自中间表 EquipID)' AFTER `equipment_name`, + ADD COLUMN `equip_type` varchar(50) DEFAULT NULL COMMENT '机台类型(采集自中间表 EquipType)' AFTER `equip_id`; + +-- 采集未匹配到台账时 equipment_id 允许为空 +ALTER TABLE `mes_xsl_mixer_action` + MODIFY COLUMN `equipment_id` varchar(32) DEFAULT NULL COMMENT '设备台账ID(mes_xsl_equipment_ledger.id),采集未匹配时为空'; + +-- 唯一性改为(设备+动作代号):按机台编号+动作代号建索引,便于采集 upsert +ALTER TABLE `mes_xsl_mixer_action` + ADD KEY `idx_mxma_equip_code` (`tenant_id`, `equip_id`, `action_code`, `del_flag`); + +-- ===================== 3. 密炼动作菜单按钮权限 ===================== +-- 父菜单:密炼动作 1900000000000000835 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000861', '1900000000000000835', '启动采集', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:start', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000861'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000862', '1900000000000000835', '停止采集', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:stop', '1', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000862'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000863', '1900000000000000835', '采集设置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:setting', '1', 3.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000863'); + +-- admin 角色授权 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1' +FROM `sys_role` r +JOIN ( + SELECT id FROM `sys_permission` + WHERE id IN ('1900000000000000861','1900000000000000862','1900000000000000863') +) p ON 1 = 1 +WHERE r.`role_code` = 'admin' + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_permission` rp + WHERE rp.`role_id` = r.id AND rp.`permission_id` = p.id + ); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_154__mes_xsl_mcs_sync_field.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_154__mes_xsl_mcs_sync_field.sql new file mode 100644 index 00000000..ac40de25 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_154__mes_xsl_mcs_sync_field.sql @@ -0,0 +1,85 @@ +-- 【MES上辅机】采集配置:通用表+字段映射(中间库表 ↔ MES表,配置驱动) +-- 1. 扩展采集配置头:目标表、配置名称、表注释 +-- 2. 新建字段映射表 mes_xsl_mcs_sync_field +-- 3. 将密炼动作(MIX_ACT)改造为配置驱动:补目标表+预置字段映射 +-- 4. 新增"采集配置"菜单及按钮权限 + +-- ===================== 1. 采集配置头扩展 ===================== +ALTER TABLE `mes_xsl_mcs_sync_config` + ADD COLUMN `config_name` varchar(100) DEFAULT NULL COMMENT '配置名称' AFTER `biz_type`, + ADD COLUMN `source_table_comment` varchar(200) DEFAULT NULL COMMENT '源中间表注释' AFTER `source_table`, + ADD COLUMN `target_table` varchar(100) DEFAULT NULL COMMENT 'MES目标表名' AFTER `source_table_comment`, + ADD COLUMN `target_table_comment` varchar(200) DEFAULT NULL COMMENT 'MES目标表注释' AFTER `target_table`; + +-- 密炼动作改造为配置驱动 +UPDATE `mes_xsl_mcs_sync_config` +SET `config_name` = '密炼机动作采集', + `source_table_comment` = '密炼机实时动作', + `target_table` = 'mes_xsl_mixer_action', + `target_table_comment` = 'MES密炼机动作维护' +WHERE `biz_type` = 'MIX_ACT'; + +-- ===================== 2. 字段映射表 ===================== +CREATE TABLE IF NOT EXISTS `mes_xsl_mcs_sync_field` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `config_id` varchar(32) NOT NULL COMMENT '采集配置ID(mes_xsl_mcs_sync_config.id)', + `source_field` varchar(100) NOT NULL COMMENT '中间库源字段名', + `source_field_comment` varchar(200) DEFAULT NULL COMMENT '源字段注释', + `source_field_type` varchar(50) DEFAULT NULL COMMENT '源字段类型', + `target_field` varchar(100) DEFAULT NULL COMMENT 'MES目标字段名(接收字段)', + `target_field_comment` varchar(200) DEFAULT NULL COMMENT 'MES目标字段注释', + `match_key` varchar(1) DEFAULT '0' COMMENT '是否匹配键(0否,1是),作为Upsert唯一键', + `sort_no` int DEFAULT '0' COMMENT '排序', + `tenant_id` int DEFAULT '0' COMMENT '租户', + `create_by` varchar(100) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(100) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `del_flag` int DEFAULT '0' COMMENT '删除标记(0正常,1删除)', + PRIMARY KEY (`id`), + KEY `idx_msf_config` (`config_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES上辅机采集字段映射'; + +-- 预置密炼动作字段映射(EquipName→equipment_name 等;匹配键=机台编号+动作代号) +INSERT INTO `mes_xsl_mcs_sync_field` + (`id`, `config_id`, `source_field`, `source_field_comment`, `source_field_type`, `target_field`, `target_field_comment`, `match_key`, `sort_no`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`) +SELECT * FROM ( + SELECT '1900000000000000870' id, '1900000000000000860' config_id, 'EquipName' sf, '机台名称' sfc, 'nvarchar' sft, 'equipment_name' tf, '设备名称' tfc, '0' mk, 1 sn, 0 tid, 'admin' cb, NOW() ct, 'admin' ub, NOW() ut, 0 df + UNION ALL SELECT '1900000000000000871', '1900000000000000860', 'EquipID', '机台编号', 'varchar', 'equip_id', '机台编号', '1', 2, 0, 'admin', NOW(), 'admin', NOW(), 0 + UNION ALL SELECT '1900000000000000872', '1900000000000000860', 'EquipType', '机台类型', 'nvarchar', 'equip_type', '机台类型', '0', 3, 0, 'admin', NOW(), 'admin', NOW(), 0 + UNION ALL SELECT '1900000000000000873', '1900000000000000860', 'MixActName', '动作名称', 'nvarchar', 'action_name', '动作名称', '0', 4, 0, 'admin', NOW(), 'admin', NOW(), 0 + UNION ALL SELECT '1900000000000000874', '1900000000000000860', 'MixActAddress', '动作地址', 'int', 'action_code', '动作代号', '1', 5, 0, 'admin', NOW(), 'admin', NOW(), 0 +) t +WHERE NOT EXISTS (SELECT 1 FROM `mes_xsl_mcs_sync_field` WHERE `config_id` = '1900000000000000860'); + +-- ===================== 3. 采集配置菜单 + 按钮权限 ===================== +-- 父菜单:MES上辅机数据 1900000000000000830 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000865', '1900000000000000830', '采集配置', '/xslmesMcs/mcsSyncConfig', 'xslmesMcs/mcsSyncConfig/index', 1, NULL, NULL, 0, NULL, '0', 0.50, 0, 'ant-design:sync-outlined', 1, 1, 0, 0, '中间表→MES表 采集配置与字段映射', 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000865'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000866', '1900000000000000865', '新增', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:add', '1', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000866'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000867', '1900000000000000865', '编辑', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:edit', '1', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000867'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) +SELECT '1900000000000000868', '1900000000000000865', '删除', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mcsSyncConfig:delete', '1', 3.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', NOW(), 'admin', NOW(), 0, 0, '1', 0 +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '1900000000000000868'); + +-- admin 授权 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1' +FROM `sys_role` r +JOIN ( + SELECT id FROM `sys_permission` + WHERE id IN ('1900000000000000865','1900000000000000866','1900000000000000867','1900000000000000868') +) p ON 1 = 1 +WHERE r.`role_code` = 'admin' + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_permission` rp + WHERE rp.`role_id` = r.id AND rp.`permission_id` = p.id + ); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_155__mes_xsl_mcs_sync_mode.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_155__mes_xsl_mcs_sync_mode.sql new file mode 100644 index 00000000..e8a4b174 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_155__mes_xsl_mcs_sync_mode.sql @@ -0,0 +1,10 @@ +-- 【MES上辅机】采集模式:全量匹配/时间匹配/增量匹配(应对中间库大表) +ALTER TABLE `mes_xsl_mcs_sync_config` + ADD COLUMN `sync_mode` varchar(20) NOT NULL DEFAULT 'FULL' COMMENT '采集模式(FULL全量匹配,TIME时间匹配,INCR增量匹配)' AFTER `status`, + ADD COLUMN `incr_column` varchar(100) DEFAULT NULL COMMENT '增量/时间列(源表列名,TIME/INCR模式用)' AFTER `sync_mode`, + ADD COLUMN `time_window` varchar(20) DEFAULT 'TODAY' COMMENT '时间范围(TODAY当天,LAST7最近七天),TIME模式用' AFTER `incr_column`, + ADD COLUMN `batch_limit` int DEFAULT '2000' COMMENT '每轮最大采集行数(INCR模式TOP N限流)' AFTER `time_window`, + ADD COLUMN `last_watermark` varchar(64) DEFAULT NULL COMMENT '增量采集已处理到的高水位(INCR模式自动维护)' AFTER `batch_limit`; + +-- 密炼动作为小状态表,保持全量匹配 +UPDATE `mes_xsl_mcs_sync_config` SET `sync_mode` = 'FULL' WHERE `biz_type` = 'MIX_ACT'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_156__mes_xsl_mcs_sync_flag_condition.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_156__mes_xsl_mcs_sync_flag_condition.sql new file mode 100644 index 00000000..1bf370d4 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_156__mes_xsl_mcs_sync_flag_condition.sql @@ -0,0 +1,4 @@ +-- 【MES上辅机】增量匹配改为标记位回写:可视化采集条件 + 可配置回写值 +ALTER TABLE `mes_xsl_mcs_sync_config` + ADD COLUMN `flag_condition` varchar(20) DEFAULT 'IS_NULL' COMMENT '增量标记采集条件(IS_NULL为空,EQ_EMPTY等于空串,NE_EMPTY不等于空串)' AFTER `last_watermark`, + ADD COLUMN `flag_write_value` varchar(64) DEFAULT '1' COMMENT '增量标记采集完成后回写值(默认1)' AFTER `flag_condition`; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_157__mes_xsl_mcs_sync_flag_match_value.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_157__mes_xsl_mcs_sync_flag_match_value.sql new file mode 100644 index 00000000..4323137c --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_157__mes_xsl_mcs_sync_flag_match_value.sql @@ -0,0 +1,3 @@ +-- 【MES上辅机】增量采集条件「等于/不等于」支持自定义匹配值(留空时退化为空字符串) +ALTER TABLE `mes_xsl_mcs_sync_config` + ADD COLUMN `flag_match_value` varchar(255) DEFAULT NULL COMMENT '增量标记采集条件比较值(EQ_EMPTY/NE_EMPTY用,留空表示空字符串)' AFTER `flag_condition`; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.api.ts index a9f342ba..4e1d8a24 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.api.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.api.ts @@ -14,10 +14,10 @@ enum Api { export const list = (params) => defHttp.get({ url: Api.list, params }); -export const checkActionName = (params: { actionName: string; dataId?: string }) => +export const checkActionName = (params: { equipmentId?: string; actionName: string; dataId?: string }) => defHttp.get({ url: Api.checkActionName, params }, { successMessageMode: 'none', errorMessageMode: 'none' }); -export const checkActionCode = (params: { actionCode: string; dataId?: string }) => +export const checkActionCode = (params: { equipmentId?: string; actionCode: string; dataId?: string }) => defHttp.get({ url: Api.checkActionCode, params }, { successMessageMode: 'none', errorMessageMode: 'none' }); export const deleteOne = (params, handleSuccess) => diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.data.ts index 816f0cd1..32a98dcb 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerAction/MesXslMixerAction.data.ts @@ -2,9 +2,11 @@ import { BasicColumn, FormSchema } from '/@/components/Table'; import { checkActionCode, checkActionName } from './MesXslMixerAction.api'; export const columns: BasicColumn[] = [ - { title: '设备名称', align: 'center', dataIndex: 'equipmentId_dictText', width: 180 }, + { title: '设备名称', align: 'center', dataIndex: 'equipmentName', width: 180 }, + { title: '机台编号', align: 'center', dataIndex: 'equipId', width: 110 }, + { title: '机台类型', align: 'center', dataIndex: 'equipType', width: 110 }, { title: '动作名称', align: 'center', dataIndex: 'actionName', width: 180 }, - { title: '动作代号', align: 'center', dataIndex: 'actionCode', width: 160 }, + { title: '动作代号', align: 'center', dataIndex: 'actionCode', width: 120 }, { title: '创建时间', align: 'center', dataIndex: 'createTime', width: 170 }, ]; @@ -16,6 +18,7 @@ export const searchFormSchema: FormSchema[] = [ componentProps: { dictCode: 'mes_xsl_equipment_ledger,equipment_name,id' }, colProps: { span: 6 }, }, + { label: '机台编号', field: 'equipId', component: 'Input', colProps: { span: 6 } }, { label: '动作名称', field: 'actionName', component: 'Input', colProps: { span: 6 } }, { label: '动作代号', field: 'actionCode', component: 'Input', colProps: { span: 6 } }, ]; @@ -29,6 +32,21 @@ export const formSchema: FormSchema[] = [ required: true, componentProps: { dictCode: 'mes_xsl_equipment_ledger,equipment_name,id', placeholder: '请选择设备台账中的设备' }, }, + // 采集冗余字段:仅采集数据有值,只读展示 + { + label: '机台编号', + field: 'equipId', + component: 'Input', + componentProps: { disabled: true }, + ifShow: ({ values }) => !!values.equipId, + }, + { + label: '机台类型', + field: 'equipType', + component: 'Input', + componentProps: { disabled: true }, + ifShow: ({ values }) => !!values.equipType, + }, { label: '动作名称', field: 'actionName', @@ -41,10 +59,10 @@ export const formSchema: FormSchema[] = [ const v = value == null ? '' : String(value).trim(); if (!v) return Promise.resolve(); try { - await checkActionName({ actionName: v, dataId: model?.id }); + await checkActionName({ equipmentId: model?.equipmentId, actionName: v, dataId: model?.id }); return Promise.resolve(); } catch (e: any) { - return Promise.reject(e?.response?.data?.message || e?.message || '动作名称不能重复'); + return Promise.reject(e?.response?.data?.message || e?.message || '同一设备下动作名称不能重复'); } }, trigger: 'blur', @@ -63,10 +81,10 @@ export const formSchema: FormSchema[] = [ const v = value == null ? '' : String(value).trim(); if (!v) return Promise.resolve(); try { - await checkActionCode({ actionCode: v, dataId: model?.id }); + await checkActionCode({ equipmentId: model?.equipmentId, actionCode: v, dataId: model?.id }); return Promise.resolve(); } catch (e: any) { - return Promise.reject(e?.response?.data?.message || e?.message || '动作代号不能重复'); + return Promise.reject(e?.response?.data?.message || e?.message || '同一设备下动作代号不能重复'); } }, trigger: 'blur', diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue new file mode 100644 index 00000000..44e2ba56 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/CollectModal.vue @@ -0,0 +1,201 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/SyncConfigModal.vue b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/SyncConfigModal.vue new file mode 100644 index 00000000..bb25c2d3 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/components/SyncConfigModal.vue @@ -0,0 +1,267 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/index.vue b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/index.vue new file mode 100644 index 00000000..525e04ec --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/index.vue @@ -0,0 +1,64 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts new file mode 100644 index 00000000..caf7397e --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.api.ts @@ -0,0 +1,44 @@ +import { defHttp } from '/@/utils/http/axios'; + +enum Api { + list = '/xslmes/mcs/syncConfig/list', + queryById = '/xslmes/mcs/syncConfig/queryById', + getByBizType = '/xslmes/mcs/syncConfig/getByBizType', + add = '/xslmes/mcs/syncConfig/add', + edit = '/xslmes/mcs/syncConfig/edit', + deleteOne = '/xslmes/mcs/syncConfig/delete', + saveCollect = '/xslmes/mcs/syncConfig/saveCollect', + sourceTables = '/xslmes/mcs/syncConfig/meta/sourceTables', + sourceColumns = '/xslmes/mcs/syncConfig/meta/sourceColumns', + targetTables = '/xslmes/mcs/syncConfig/meta/targetTables', + targetColumns = '/xslmes/mcs/syncConfig/meta/targetColumns', +} + +export const list = (params) => defHttp.get({ url: Api.list, params }); + +export const queryById = (id: string) => defHttp.get({ url: Api.queryById, params: { id } }); + +export const getByBizType = (bizType = 'MIX_ACT') => defHttp.get({ url: Api.getByBizType, params: { bizType } }); + +export const saveOrUpdate = (params, isUpdate: boolean) => defHttp.post({ url: isUpdate ? Api.edit : Api.add, params }); + +export const deleteOne = (id: string, handleSuccess) => + defHttp.delete({ url: Api.deleteOne, params: { id } }, { joinParamsToUrl: true }).then(() => handleSuccess()); + +// 采集操作:status '1'/'0' 表示是否采集;syncMode FULL/TIME/INCR +export const saveCollect = (params: { + id: string; + status: string; + intervalSeconds: number; + syncMode?: string; + incrColumn?: string; + timeWindow?: string; + batchLimit?: number; + flagCondition?: string; + flagWriteValue?: string; +}) => defHttp.post({ url: Api.saveCollect, params }); + +export const getSourceTables = () => defHttp.get({ url: Api.sourceTables }, { errorMessageMode: 'message' }); +export const getSourceColumns = (table: string) => defHttp.get({ url: Api.sourceColumns, params: { table } }, { errorMessageMode: 'message' }); +export const getTargetTables = () => defHttp.get({ url: Api.targetTables }); +export const getTargetColumns = (table: string) => defHttp.get({ url: Api.targetColumns, params: { table } }); diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts new file mode 100644 index 00000000..9ff52940 --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsSyncConfig/mcsSyncConfig.data.ts @@ -0,0 +1,41 @@ +import { BasicColumn, FormSchema } from '/@/components/Table'; + +export const columns: BasicColumn[] = [ + { title: '配置名称', align: 'center', dataIndex: 'configName', width: 160 }, + { + title: '中间库源表', + align: 'center', + dataIndex: 'sourceTable', + width: 200, + customRender: ({ record }) => (record.sourceTableComment ? `${record.sourceTable}(${record.sourceTableComment})` : record.sourceTable), + }, + { + title: 'MES目标表', + align: 'center', + dataIndex: 'targetTable', + width: 200, + customRender: ({ record }) => (record.targetTableComment ? `${record.targetTable}(${record.targetTableComment})` : record.targetTable), + }, + { + title: '采集模式', + align: 'center', + dataIndex: 'syncMode', + width: 100, + customRender: ({ record }) => ({ FULL: '全量匹配', TIME: '时间匹配', INCR: '增量匹配' }[record.syncMode] || '全量匹配'), + }, + { title: '采集间隔(秒)', align: 'center', dataIndex: 'intervalSeconds', width: 100 }, + { + title: '状态', + align: 'center', + dataIndex: 'running', + width: 90, + customRender: ({ record }) => (record.running ? '采集中' : '已停止'), + }, + { title: '最近采集时间', align: 'center', dataIndex: 'lastSyncTime', width: 160 }, + { title: '最近采集结果', align: 'center', dataIndex: 'lastSyncResult', width: 200 }, +]; + +export const searchFormSchema: FormSchema[] = [ + { label: '配置名称', field: 'configName', component: 'Input', colProps: { span: 6 } }, + { label: '中间库源表', field: 'sourceTable', component: 'Input', colProps: { span: 6 } }, +]; diff --git a/jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue b/jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue index 09e791d5..22cab30e 100644 --- a/jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue +++ b/jeecgboot-vue3/src/views/xslmesMcs/mcsToMesMixAct/index.vue @@ -1,8 +1,9 @@ -