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/MesXslDesktopAnonController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java index 0937563b..7171e29d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java @@ -35,6 +35,7 @@ import org.jeecg.modules.xslmes.entity.MesXslVehicle; import org.jeecg.modules.xslmes.entity.MesXslWarehouse; import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea; import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestMethod; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd; @@ -50,11 +51,13 @@ import org.jeecg.modules.xslmes.service.IMesXslVehicleService; import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService; import org.jeecg.modules.xslmes.service.IMesXslWarehouseService; import org.jeecg.modules.xslmes.service.IMesXslMixingProductionPlanService; +import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestMethodService; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestRecordService; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService; import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService; import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; import org.jeecg.modules.xslmes.vo.MesXslRawMaterialCardBriefVO; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; @@ -100,6 +103,7 @@ public class MesXslDesktopAnonController { private final IMesXslMixingProductionPlanService mixingProductionPlanService; private final IMesXslRubberQuickTestStdService rubberQuickTestStdService; private final IMesXslRubberQuickTestRecordService rubberQuickTestRecordService; + private final IMesXslRubberQuickTestMethodService rubberQuickTestMethodService; // ═══════════════════════════ 车辆管理 ═══════════════════════════ @@ -965,6 +969,9 @@ public class MesXslDesktopAnonController { QueryWrapper qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap()); qw.orderByDesc("create_time"); IPage page = rubberQuickTestStdService.page(new Page<>(pageNo, pageSize), qw); + if (page.getRecords() != null) { + page.getRecords().forEach(this::fillStdQuickTestTypeFromMethod); + } return Result.OK(page); } @@ -976,6 +983,7 @@ public class MesXslDesktopAnonController { return Result.error("未找到对应数据"); } entity.setLineList(rubberQuickTestStdService.selectLinesByStdId(id)); + fillStdQuickTestTypeFromMethod(entity); return Result.OK(entity); } //update-end---author:jiangxh ---date:2026-06-17 for:【快检实验标准】桌面端只读列表与详情----------- @@ -1001,6 +1009,7 @@ public class MesXslDesktopAnonController { return Result.error("未找到胶料「" + name + "」对应的使用中且已批准的快检实验标准"); } std.setLineList(rubberQuickTestStdService.selectLinesByStdId(std.getId())); + fillStdQuickTestTypeFromMethod(std); return Result.OK(std); } @@ -1013,6 +1022,30 @@ public class MesXslDesktopAnonController { //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】桌面端胶料快检实验标准查询----------- //update-begin---author:jiangxh ---date:2026-06-17 for:【快检记录】桌面端胶料快检记录保存----------- + @Operation(summary = "胶料快检记录-免密分页列表") + @GetMapping("/xslmes/mesXslRubberQuickTestRecord/anon/list") + public Result> rubberQuickTestRecordAnonList( + MesXslRubberQuickTestRecord model, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "20") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap()); + qw.orderByDesc("create_time"); + IPage page = rubberQuickTestRecordService.page(new Page<>(pageNo, pageSize), qw); + return Result.OK(page); + } + + @Operation(summary = "胶料快检记录-免密通过id查询(含明细)") + @GetMapping("/xslmes/mesXslRubberQuickTestRecord/anon/queryById") + public Result rubberQuickTestRecordAnonQueryById(@RequestParam(name = "id") String id) { + MesXslRubberQuickTestRecord entity = rubberQuickTestRecordService.getById(id); + if (entity == null) { + return Result.error("未找到对应数据"); + } + fillRubberQuickTestRecordDetails(entity); + return Result.OK(entity); + } + @Operation(summary = "胶料快检记录-免密添加") @PostMapping("/xslmes/mesXslRubberQuickTestRecord/anon/add") public Result rubberQuickTestRecordAnonAdd(@RequestBody MesXslRubberQuickTestRecord record) { @@ -1022,21 +1055,57 @@ public class MesXslDesktopAnonController { if (oConvertUtils.isEmpty(record.getRubberMaterialName())) { return Result.error("胶料名称不能为空"); } - List lineList = record.getLineList(); - if (lineList == null || lineList.isEmpty()) { - return Result.error("请维护检验明细"); + if (CollectionUtils.isEmpty(record.getStdLineList())) { + return Result.error("请维护数据标准明细"); } + if (CollectionUtils.isEmpty(record.getRawLineList())) { + return Result.error("请维护试验结果明细"); + } + //update-begin---author:jiangxh ---date:20260617 for:【快检记录】桌面端同步须含曲线图数据----------- + if (CollectionUtils.isEmpty(record.getChartPointList())) { + return Result.error("请维护曲线图数据"); + } + //update-end---author:jiangxh ---date:20260617 for:【快检记录】桌面端同步须含曲线图数据----------- try { - rubberQuickTestRecordService.saveMain(record, lineList); + if (oConvertUtils.isEmpty(record.getRecordNo())) { + record.setRecordNo(rubberQuickTestRecordService.generateDesktopRecordNo(record)); + } + rubberQuickTestRecordService.fillStdAndTypeForRecord(record); + rubberQuickTestRecordService.saveMain(record, record.getLineList()); stompNotify.publishRubberQuickTestRecordChanged("add", record.getId()); - return Result.OK("添加成功!"); + return Result.OK(record.getRecordNo()); } catch (Exception e) { log.error(e.getMessage(), e); return Result.error(e.getMessage()); } } + + private void fillRubberQuickTestRecordDetails(MesXslRubberQuickTestRecord entity) { + if (entity == null || oConvertUtils.isEmpty(entity.getId())) { + return; + } + String id = entity.getId(); + entity.setStdLineList(rubberQuickTestRecordService.selectStdLinesByRecordId(id)); + entity.setRawLineList(rubberQuickTestRecordService.selectRawLinesByRecordId(id)); + entity.setChartPointList(rubberQuickTestRecordService.selectChartPointsByRecordId(id)); + entity.setLineList(rubberQuickTestRecordService.selectLinesByRecordId(id)); + } //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】桌面端胶料快检记录保存----------- + //update-begin---author:jiangxh ---date:20260618 for:【快检实验标准】桌面端回填实验方法关联实验类型----------- + private void fillStdQuickTestTypeFromMethod(MesXslRubberQuickTestStd std) { + if (std == null || oConvertUtils.isEmpty(std.getTestMethodId())) { + return; + } + MesXslRubberQuickTestMethod method = rubberQuickTestMethodService.getById(std.getTestMethodId()); + if (method == null) { + return; + } + std.setQuickTestTypeId(method.getQuickTestTypeId()); + std.setQuickTestTypeName(method.getQuickTestTypeName()); + } + //update-end---author:jiangxh ---date:20260618 for:【快检实验标准】桌面端回填实验方法关联实验类型----------- + // ─────────────────────────── 车辆私有辅助 ──────────────────────────── private void applyWeightNetAndBillType(MesXslWeightRecord record) { 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 { private final IMesXslMixingProductionPlanService mixingProductionPlanService; + private final MesXslStompNotifyService stompNotify; public MesXslMixingProductionPlanController( - IMesXslMixingProductionPlanService mixingProductionPlanService) { + IMesXslMixingProductionPlanService mixingProductionPlanService, + MesXslStompNotifyService stompNotify) { this.mixingProductionPlanService = mixingProductionPlanService; + this.stompNotify = stompNotify; } @Operation(summary = "密炼生产计划维护-分页列表查询") @@ -56,6 +60,9 @@ public class MesXslMixingProductionPlanController @PostMapping("/saveAll") public Result saveAll(@RequestBody MesXslMixingProductionPlanSaveAllVO req) { mixingProductionPlanService.saveAllRows(req == null ? null : req.getRows()); + //update-begin---author:jiangxh ---date:20260617 for:【密炼计划】整表保存后广播桌面端同步----------- + stompNotify.publishMixingProductionPlanChanged("saveAll", null); + //update-end---author:jiangxh ---date:20260617 for:【密炼计划】整表保存后广播桌面端同步----------- return Result.OK("保存成功"); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java index f523cd00..fd1449bb 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestRecordController.java @@ -23,8 +23,10 @@ import org.jeecg.modules.mes.material.service.IMesMaterialService; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.service.ISysUserService; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordChartPoint; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordRawLine; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordStdLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestType; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestRecordService; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestTypeService; @@ -32,6 +34,7 @@ import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordBatchFromMaterialV import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordPage; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; @@ -117,16 +120,9 @@ public class MesXslRubberQuickTestRecordController @RequiresPermissions("mes:mes_material:rubberQuickTestInspect") @PostMapping(value = "/batchFromMaterial") public Result> batchFromMaterial(@RequestBody MesXslRubberQuickTestRecordBatchFromMaterialVO vo) { - try { - //update-begin---author:jiangxh ---date:20260616 for:【MES】胶料快检记录批量生成默认带出当前登录检验人----------- - fillInspectorIfEmpty(vo); - //update-end---author:jiangxh ---date:20260616 for:【MES】胶料快检记录批量生成默认带出当前登录检验人----------- - List ids = mesXslRubberQuickTestRecordService.batchFromMaterial(vo); - return Result.OK("成功生成 " + ids.size() + " 条快检记录", ids); - } catch (Exception e) { - log.error(e.getMessage(), e); - return Result.error(e.getMessage()); - } + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】取消胶料列表批量生成,改由桌面端同步----------- + return Result.error("该功能已停用,请通过桌面端新增并同步胶料快检记录"); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】取消胶料列表批量生成,改由桌面端同步----------- } @AutoLog(value = "MES胶料快检记录-删除") @@ -154,6 +150,12 @@ public class MesXslRubberQuickTestRecordController if (entity == null) { return Result.error("未找到对应数据"); } + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】查询含数据标准/试验结果/曲线图明细----------- + entity.setStdLineList(mesXslRubberQuickTestRecordService.selectStdLinesByRecordId(id)); + entity.setRawLineList(mesXslRubberQuickTestRecordService.selectRawLinesByRecordId(id)); + entity.setChartPointList(mesXslRubberQuickTestRecordService.selectChartPointsByRecordId(id)); + entity.setLineList(mesXslRubberQuickTestRecordService.selectLinesByRecordId(id)); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】查询含数据标准/试验结果/曲线图明细----------- return Result.OK(entity); } @@ -173,6 +175,22 @@ public class MesXslRubberQuickTestRecordController } //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】查询原始数据明细----------- + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】查询数据标准明细与曲线图----------- + @Operation(summary = "MES胶料快检记录-查询数据标准明细") + @GetMapping(value = "/queryStdLineListByRecordId") + public Result> queryStdLineListByRecordId( + @RequestParam(name = "id", required = true) String id) { + return Result.OK(mesXslRubberQuickTestRecordService.selectStdLinesByRecordId(id)); + } + + @Operation(summary = "MES胶料快检记录-查询曲线图数据点") + @GetMapping(value = "/queryChartPointListByRecordId") + public Result> queryChartPointListByRecordId( + @RequestParam(name = "id", required = true) String id) { + return Result.OK(mesXslRubberQuickTestRecordService.selectChartPointsByRecordId(id)); + } + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】查询数据标准明细与曲线图----------- + @RequiresPermissions("mes:mes_xsl_rubber_quick_test_record:exportXls") @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, MesXslRubberQuickTestRecord model) { @@ -189,14 +207,22 @@ public class MesXslRubberQuickTestRecordController if (main == null) { return "参数不能为空"; } - if (oConvertUtils.isEmpty(main.getRubberMaterialId())) { - return "请选择胶料"; + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】Web保存校验支持桌面端三类明细----------- + if (oConvertUtils.isNotEmpty(main.getRubberMaterialId())) { + MesMaterial material = mesMaterialService.getById(main.getRubberMaterialId()); + if (material == null) { + return "所选胶料不存在"; + } + main.setRubberMaterialName(material.getMaterialName()); + } else if (oConvertUtils.isEmpty(main.getRubberMaterialName())) { + return "胶料名称不能为空"; } - MesMaterial material = mesMaterialService.getById(main.getRubberMaterialId()); - if (material == null) { - return "所选胶料不存在"; + + if (!CollectionUtils.isEmpty(main.getStdLineList()) && !CollectionUtils.isEmpty(main.getRawLineList())) { + resolveInspector(main); + return null; } - main.setRubberMaterialName(material.getMaterialName()); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】Web保存校验支持桌面端三类明细----------- if (oConvertUtils.isNotEmpty(main.getQuickTestTypeId())) { MesXslRubberQuickTestType type = mesXslRubberQuickTestTypeService.getById(main.getQuickTestTypeId()); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerAction.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerAction.java index 4cf67075..6ec73000 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerAction.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerAction.java @@ -38,6 +38,16 @@ public class MesXslMixerAction implements Serializable { @Schema(description = "设备名称冗余") private String equipmentName; + //update-begin---author:GHT ---date:20260617 for:【MES上辅机】密炼动作秒级采集-补全机台字段----------- + @Excel(name = "机台编号", width = 15) + @Schema(description = "机台编号(采集自中间表 EquipID)") + private String equipId; + + @Excel(name = "机台类型", width = 15) + @Schema(description = "机台类型(采集自中间表 EquipType)") + private String equipType; + //update-end---author:GHT ---date:20260617 for:【MES上辅机】密炼动作秒级采集-补全机台字段----------- + @Excel(name = "动作名称", width = 20) @Schema(description = "动作名称") private String actionName; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java index 694a3325..db1fe703 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecord.java @@ -32,8 +32,8 @@ public class MesXslRubberQuickTestRecord implements Serializable { @TableId(type = IdType.ASSIGN_ID) private String id; - @Excel(name = "单号", width = 16) - @Schema(description = "单号(JL+yyyyMMdd+4位流水)") + @Excel(name = "快检记录号", width = 16) + @Schema(description = "快检记录号(yyyyMMdd+4位流水+胶料名称)") private String recordNo; @Schema(description = "胶料ID mes_material.id") @@ -46,14 +46,25 @@ public class MesXslRubberQuickTestRecord implements Serializable { @Schema(description = "引用的实验标准ID") private String stdId; - @Schema(description = "生产机台ID") + @Excel(name = "实验标准", width = 20) + @Schema(description = "实验标准名称冗余") + private String stdName; + + @Schema(description = "实验方法ID") + private String testMethodId; + + @Excel(name = "实验方法", width = 20) + @Schema(description = "实验方法名称冗余") + private String testMethodName; + + @Schema(description = "炼机台ID") private String prodEquipmentLedgerId; - @Excel(name = "生产机台", width = 16) - @Schema(description = "生产机台名称冗余") + @Excel(name = "炼机台", width = 16) + @Schema(description = "炼机台名称冗余") private String prodEquipmentName; - @Excel(name = "生产日期", width = 12, format = "yyyy-MM-dd") + @Excel(name = "密炼日期", width = 12, format = "yyyy-MM-dd") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd") private Date productionDate; @@ -69,7 +80,7 @@ public class MesXslRubberQuickTestRecord implements Serializable { @Dict(dicCode = "xslmes_rubber_quick_test_work_team") private String workTeam; - @Excel(name = "检验次数", width = 10) + @Excel(name = "试验次数", width = 10) private Integer inspectTimes; @Excel(name = "检验时间", width = 18, format = "yyyy-MM-dd HH:mm:ss") @@ -86,17 +97,17 @@ public class MesXslRubberQuickTestRecord implements Serializable { @Excel(name = "检验人", width = 12) private String inspectorRealname; - @Schema(description = "检验类型ID") + @Schema(description = "实验类型ID") private String quickTestTypeId; - @Excel(name = "检验类型", width = 16) + @Excel(name = "实验类型", width = 16) private String quickTestTypeName; @Excel(name = "检验结果", width = 10, dicCode = "xslmes_rubber_quick_test_record_result") @Dict(dicCode = "xslmes_rubber_quick_test_record_result") private String inspectResult; - @Excel(name = "生产计划号", width = 16) + @Excel(name = "密炼计划", width = 16) private String productionPlanNo; @Schema(description = "检验机台ID") @@ -134,4 +145,14 @@ public class MesXslRubberQuickTestRecord implements Serializable { @TableField(exist = false) @Schema(description = "原始数据明细(试验结果全部检测值)") private List rawLineList; + + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图----------- + @TableField(exist = false) + @Schema(description = "数据标准明细(实验标准快照)") + private List stdLineList; + + @TableField(exist = false) + @Schema(description = "曲线图数据点") + private List chartPointList; + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordChartPoint.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordChartPoint.java new file mode 100644 index 00000000..306d3d2a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordChartPoint.java @@ -0,0 +1,59 @@ +package org.jeecg.modules.xslmes.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * MES 胶料快检记录曲线图数据点 + */ +@Data +@TableName("mes_xsl_rubber_quick_test_record_chart_point") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "MES胶料快检记录曲线图数据点") +public class MesXslRubberQuickTestRecordChartPoint implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + @Schema(description = "主表ID mes_xsl_rubber_quick_test_record.id") + private String recordId; + + @Excel(name = "时间(min)", width = 12, type = 10) + private BigDecimal timeMin; + + @Excel(name = "上模温度", width = 12, type = 10) + private BigDecimal upperTemp; + + @Excel(name = "下模温度", width = 12, type = 10) + private BigDecimal lowerTemp; + + @Excel(name = "S'(dNm)", width = 12, type = 10) + private BigDecimal torqueS; + + private Integer sortNo; + 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; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordStdLine.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordStdLine.java new file mode 100644 index 00000000..2f00f450 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestRecordStdLine.java @@ -0,0 +1,68 @@ +package org.jeecg.modules.xslmes.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * MES 胶料快检记录数据标准明细(实验标准快照) + */ +@Data +@TableName("mes_xsl_rubber_quick_test_record_std_line") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "MES胶料快检记录数据标准明细") +public class MesXslRubberQuickTestRecordStdLine implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + @Schema(description = "主表ID mes_xsl_rubber_quick_test_record.id") + private String recordId; + + @Schema(description = "数据点ID") + private String dataPointId; + + @Excel(name = "数据点", width = 18) + private String pointName; + + @Excel(name = "下限值", width = 12, type = 10) + private BigDecimal lowerLimit; + + @Excel(name = "上限值", width = 12, type = 10) + private BigDecimal upperLimit; + + @Excel(name = "下限预警", width = 12, type = 10) + private BigDecimal lowerWarn; + + @Excel(name = "上限预警", width = 12, type = 10) + private BigDecimal upperWarn; + + @Excel(name = "目标值", width = 12, type = 10) + private BigDecimal targetValue; + + private Integer sortNo; + 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; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestStd.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestStd.java index 743b95fa..6c738981 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestStd.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRubberQuickTestStd.java @@ -106,4 +106,12 @@ public class MesXslRubberQuickTestStd implements Serializable { @TableField(exist = false) @Schema(description = "标准明细") private List lineList; + + @TableField(exist = false) + @Schema(description = "实验类型ID(来自实验方法,桌面端展示)") + private String quickTestTypeId; + + @TableField(exist = false) + @Schema(description = "实验类型名称(来自实验方法,桌面端展示)") + private String quickTestTypeName; } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordChartPointMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordChartPointMapper.java new file mode 100644 index 00000000..6703258a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordChartPointMapper.java @@ -0,0 +1,6 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordChartPoint; + +public interface MesXslRubberQuickTestRecordChartPointMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordStdLineMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordStdLineMapper.java new file mode 100644 index 00000000..7293b267 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslRubberQuickTestRecordStdLineMapper.java @@ -0,0 +1,7 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordStdLine; + +public interface MesXslRubberQuickTestRecordStdLineMapper extends BaseMapper { +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java new file mode 100644 index 00000000..9e697588 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesXslMcsSyncConfigController.java @@ -0,0 +1,150 @@ +package org.jeecg.modules.xslmes.mcs.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.jeecg.common.api.vo.Result; +import org.jeecg.modules.xslmes.mcs.datasource.McsDataSourceManager; +import org.jeecg.modules.xslmes.mcs.entity.MesXslMcsSyncConfig; +import org.jeecg.modules.xslmes.mcs.mapper.McsMetaMapper; +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.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * MES上辅机 中间表采集配置(表/字段绑定 + 采集操作 + 元数据) + * + * @author GHT + * @date 2026-06-17 for:【MES上辅机】采集配置-表与字段绑定 + */ +@Tag(name = "MES上辅机采集配置") +@RestController +@RequestMapping("/xslmes/mcs/syncConfig") +public class MesXslMcsSyncConfigController { + + @Autowired + private IMesXslMcsSyncConfigService syncConfigService; + + @Autowired + private McsSyncScheduler syncScheduler; + + @Autowired + private McsMetaMapper metaMapper; + + @Autowired + private McsDataSourceManager mcsDataSourceManager; + + @Operation(summary = "采集配置-分页列表") + @GetMapping("/list") + public Result> 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/IMesXslRubberQuickTestRecordService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java index 8c5a0858..5fcc7983 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestRecordService.java @@ -5,8 +5,10 @@ import java.io.Serializable; import java.util.Collection; import java.util.List; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordChartPoint; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordRawLine; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordStdLine; import org.jeecg.modules.xslmes.vo.MesXslRubberQuickTestRecordBatchFromMaterialVO; public interface IMesXslRubberQuickTestRecordService extends IService { @@ -23,7 +25,24 @@ public interface IMesXslRubberQuickTestRecordService extends IService selectRawLinesByRecordId(String recordId); + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图查询----------- + List selectStdLinesByRecordId(String recordId); + + List selectChartPointsByRecordId(String recordId); + + /** 桌面端保存前:按实验标准回填标准名、方法名、实验类型 */ + void fillStdAndTypeForRecord(MesXslRubberQuickTestRecord record); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图查询----------- + String generateRecordNo(MesXslRubberQuickTestRecord context); + //update-begin---author:jiangxh ---date:20260618 for:【快检记录】桌面端快检记录号生成----------- + /** 桌面端快检记录号:yyyyMMdd + 4位流水 + 胶料名称,如 202606180001SA889 */ + String generateDesktopRecordNo(MesXslRubberQuickTestRecord context); + + /** 桌面端保存前:按实验标准关联实验方法回填检验类型 */ + void fillQuickTestTypeForRecord(MesXslRubberQuickTestRecord record); + //update-end---author:jiangxh ---date:20260618 for:【快检记录】桌面端快检记录号生成----------- + List batchFromMaterial(MesXslRubberQuickTestRecordBatchFromMaterialVO vo); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java index 6994c8f7..4aa9b8d5 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java @@ -99,6 +99,14 @@ public class MesXslStompNotifyService { } //update-end---author:jiangxh ---date:20260617 for:【快检实验标准】桌面端只读同步 STOMP----------- + //update-begin---author:jiangxh ---date:20260617 for:【密炼计划】桌面端只读同步 STOMP----------- + /** 广播密炼生产计划变更事件到 /topic/sync/mes-mixing-production-plans */ + public void publishMixingProductionPlanChanged(String action, String mixingProductionPlanId) { + publish("/topic/sync/mes-mixing-production-plans", "MIXING_PRODUCTION_PLAN_CHANGED", + "mixingProductionPlanId", mixingProductionPlanId, action); + } + //update-end---author:jiangxh ---date:20260617 for:【密炼计划】桌面端只读同步 STOMP----------- + // ─────────────────────────── 私有辅助 ──────────────────────────── private void publish(String topic, String cmd, String idKey, String idValue, String action) { 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-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java index c20d1797..3526e0d1 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestRecordServiceImpl.java @@ -19,14 +19,18 @@ import org.jeecg.modules.mes.material.service.IMesMaterialService; import org.jeecg.modules.xslmes.common.XslMesBizConstants; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestMethod; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecord; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordChartPoint; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordRawLine; +import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestRecordStdLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStdLine; import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestType; +import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordChartPointMapper; import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordLineMapper; import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordMapper; import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordRawLineMapper; +import org.jeecg.modules.xslmes.mapper.MesXslRubberQuickTestRecordStdLineMapper; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestMethodService; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestRecordService; import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService; @@ -50,6 +54,12 @@ public class MesXslRubberQuickTestRecordServiceImpl @Autowired private MesXslRubberQuickTestRecordRawLineMapper mesXslRubberQuickTestRecordRawLineMapper; + @Autowired + private MesXslRubberQuickTestRecordStdLineMapper mesXslRubberQuickTestRecordStdLineMapper; + + @Autowired + private MesXslRubberQuickTestRecordChartPointMapper mesXslRubberQuickTestRecordChartPointMapper; + @Autowired private IMesXslRubberQuickTestStdService mesXslRubberQuickTestStdService; @@ -73,6 +83,10 @@ public class MesXslRubberQuickTestRecordServiceImpl //update-begin---author:jiangxh ---date:2026-06-17 for:【快检记录】保存试验结果原始数据明细----------- insertRawLines(main.getId(), main.getRawLineList()); //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】保存试验结果原始数据明细----------- + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】保存数据标准明细与曲线图----------- + insertStdLines(main.getId(), main.getStdLineList()); + insertChartPoints(main.getId(), main.getChartPointList()); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】保存数据标准明细与曲线图----------- } @Override @@ -95,6 +109,16 @@ public class MesXslRubberQuickTestRecordServiceImpl .eq(MesXslRubberQuickTestRecordRawLine::getRecordId, main.getId())); insertRawLines(main.getId(), main.getRawLineList()); //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】更新时同步原始数据明细----------- + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】更新时同步数据标准与曲线图----------- + mesXslRubberQuickTestRecordStdLineMapper.delete( + new LambdaQueryWrapper() + .eq(MesXslRubberQuickTestRecordStdLine::getRecordId, main.getId())); + insertStdLines(main.getId(), main.getStdLineList()); + mesXslRubberQuickTestRecordChartPointMapper.delete( + new LambdaQueryWrapper() + .eq(MesXslRubberQuickTestRecordChartPoint::getRecordId, main.getId())); + insertChartPoints(main.getId(), main.getChartPointList()); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】更新时同步数据标准与曲线图----------- } private void insertLines(String recordId, List lineList) { @@ -128,6 +152,40 @@ public class MesXslRubberQuickTestRecordServiceImpl } //update-end---author:jiangxh ---date:2026-06-17 for:【快检记录】原始数据明细入库----------- + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图入库----------- + private void insertStdLines(String recordId, List stdLineList) { + if (CollectionUtils.isEmpty(stdLineList)) { + return; + } + int sort = 0; + for (MesXslRubberQuickTestRecordStdLine line : stdLineList) { + if (line == null) { + continue; + } + line.setId(null); + line.setRecordId(recordId); + line.setSortNo(sort++); + mesXslRubberQuickTestRecordStdLineMapper.insert(line); + } + } + + private void insertChartPoints(String recordId, List chartPointList) { + if (CollectionUtils.isEmpty(chartPointList)) { + return; + } + int sort = 0; + for (MesXslRubberQuickTestRecordChartPoint point : chartPointList) { + if (point == null) { + continue; + } + point.setId(null); + point.setRecordId(recordId); + point.setSortNo(sort++); + mesXslRubberQuickTestRecordChartPointMapper.insert(point); + } + } + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】数据标准明细与曲线图入库----------- + @Override @Transactional(rollbackFor = Exception.class) public void delMain(String id) { @@ -135,6 +193,10 @@ public class MesXslRubberQuickTestRecordServiceImpl new LambdaQueryWrapper().eq(MesXslRubberQuickTestRecordLine::getRecordId, id)); mesXslRubberQuickTestRecordRawLineMapper.delete( new LambdaQueryWrapper().eq(MesXslRubberQuickTestRecordRawLine::getRecordId, id)); + mesXslRubberQuickTestRecordStdLineMapper.delete( + new LambdaQueryWrapper().eq(MesXslRubberQuickTestRecordStdLine::getRecordId, id)); + mesXslRubberQuickTestRecordChartPointMapper.delete( + new LambdaQueryWrapper().eq(MesXslRubberQuickTestRecordChartPoint::getRecordId, id)); this.removeById(id); } @@ -162,6 +224,22 @@ public class MesXslRubberQuickTestRecordServiceImpl .orderByAsc(MesXslRubberQuickTestRecordRawLine::getSortNo)); } + @Override + public List selectStdLinesByRecordId(String recordId) { + return mesXslRubberQuickTestRecordStdLineMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslRubberQuickTestRecordStdLine::getRecordId, recordId) + .orderByAsc(MesXslRubberQuickTestRecordStdLine::getSortNo)); + } + + @Override + public List selectChartPointsByRecordId(String recordId) { + return mesXslRubberQuickTestRecordChartPointMapper.selectList( + new LambdaQueryWrapper() + .eq(MesXslRubberQuickTestRecordChartPoint::getRecordId, recordId) + .orderByAsc(MesXslRubberQuickTestRecordChartPoint::getSortNo)); + } + //update-begin---author:jiangxh ---date:20260528 for:【MES】胶料快检记录单号JL+日期+4位流水自动生成----------- @Override public String generateRecordNo(MesXslRubberQuickTestRecord context) { @@ -216,49 +294,92 @@ public class MesXslRubberQuickTestRecordServiceImpl } //update-end---author:jiangxh ---date:20260528 for:【MES】胶料快检记录单号JL+日期+4位流水自动生成----------- + //update-begin---author:jiangxh ---date:20260618 for:【快检记录】桌面端快检记录号 yyyyMMdd+流水+胶料号----------- + @Override + public String generateDesktopRecordNo(MesXslRubberQuickTestRecord context) { + String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date()); + if (context == null || oConvertUtils.isEmpty(context.getRubberMaterialName())) { + throw new JeecgBootException("胶料名称不能为空,无法生成快检记录号"); + } + String material = context.getRubberMaterialName().trim(); + int expectedLen = dateStr.length() + 4 + material.length(); + Integer tenantId = resolveTenantId(context); + + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.likeRight(MesXslRubberQuickTestRecord::getRecordNo, dateStr); + qw.and(q -> q.eq(MesXslRubberQuickTestRecord::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestRecord::getDelFlag)); + if (tenantId != null) { + qw.eq(MesXslRubberQuickTestRecord::getTenantId, tenantId); + } + qw.select(MesXslRubberQuickTestRecord::getRecordNo); + + int maxSeq = 0; + for (MesXslRubberQuickTestRecord row : this.list(qw)) { + String no = row.getRecordNo(); + if (oConvertUtils.isEmpty(no) || no.length() != expectedLen) { + continue; + } + if (!no.startsWith(dateStr) || !no.endsWith(material)) { + continue; + } + String seqPart = no.substring(dateStr.length(), dateStr.length() + 4); + try { + maxSeq = Math.max(maxSeq, Integer.parseInt(seqPart)); + } catch (NumberFormatException ignored) { + // ignore malformed historical numbers + } + } + return dateStr + String.format("%04d", maxSeq + 1) + material; + } + + @Override + public void fillQuickTestTypeForRecord(MesXslRubberQuickTestRecord record) { + fillStdAndTypeForRecord(record); + } + + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】回填实验标准/方法/类型----------- + @Override + public void fillStdAndTypeForRecord(MesXslRubberQuickTestRecord record) { + if (record == null) { + return; + } + String testMethodId = record.getTestMethodId(); + if (oConvertUtils.isNotEmpty(record.getStdId())) { + MesXslRubberQuickTestStd std = mesXslRubberQuickTestStdService.getById(record.getStdId()); + if (std != null) { + if (oConvertUtils.isEmpty(record.getStdName())) { + record.setStdName(std.getStdName()); + } + if (oConvertUtils.isEmpty(testMethodId)) { + testMethodId = std.getTestMethodId(); + record.setTestMethodId(testMethodId); + } + } + } + if (oConvertUtils.isNotEmpty(testMethodId) && oConvertUtils.isEmpty(record.getTestMethodName())) { + MesXslRubberQuickTestMethod method = mesXslRubberQuickTestMethodService.getById(testMethodId); + if (method != null) { + record.setTestMethodName(method.getMethodName()); + if (oConvertUtils.isEmpty(record.getQuickTestTypeId()) + && oConvertUtils.isNotEmpty(method.getQuickTestTypeId())) { + fillQuickTestType(record, method.getQuickTestTypeId()); + } + } + } + if (oConvertUtils.isNotEmpty(record.getQuickTestTypeId()) && oConvertUtils.isEmpty(record.getQuickTestTypeName())) { + fillQuickTestType(record, record.getQuickTestTypeId()); + } + } + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】回填实验标准/方法/类型----------- + //update-end---author:jiangxh ---date:20260618 for:【快检记录】桌面端快检记录号 yyyyMMdd+流水+胶料号----------- + //update-begin---author:jiangxh ---date:20260525 for:【MES】胶料信息多选按实验标准批量生成快检记录----------- @Override @Transactional(rollbackFor = Exception.class) public List batchFromMaterial(MesXslRubberQuickTestRecordBatchFromMaterialVO vo) { - if (vo == null || CollectionUtils.isEmpty(vo.getMaterialIds())) { - throw new JeecgBootException("请至少选择一条胶料信息"); - } - List createdIds = new ArrayList<>(); - for (String materialId : vo.getMaterialIds()) { - if (oConvertUtils.isEmpty(materialId)) { - continue; - } - String mid = materialId.trim(); - MesMaterial material = mesMaterialService.getById(mid); - if (material == null) { - throw new JeecgBootException("胶料不存在:" + mid); - } - MesXslRubberQuickTestStd std = findApprovedStdForMaterial(mid); - if (std == null) { - throw new JeecgBootException( - "胶料【" + material.getMaterialName() + "】未找到已启用且已批准的实验标准,请先在胶料快检实验标准中维护"); - } - List stdLines = mesXslRubberQuickTestStdService.selectLinesByStdId(std.getId()); - if (CollectionUtils.isEmpty(stdLines)) { - throw new JeecgBootException("胶料【" + material.getMaterialName() + "】关联的实验标准无明细数据"); - } - MesXslRubberQuickTestRecord main = buildMainFromMaterial(material, std, vo); - List recordLines = new ArrayList<>(); - for (MesXslRubberQuickTestStdLine stdLine : stdLines) { - MesXslRubberQuickTestRecordLine rl = new MesXslRubberQuickTestRecordLine(); - rl.setDataPointId(stdLine.getDataPointId()); - rl.setInspectItem(stdLine.getPointName()); - rl.setLowerLimit(stdLine.getLowerLimit()); - rl.setUpperLimit(stdLine.getUpperLimit()); - recordLines.add(rl); - } - saveMain(main, recordLines); - createdIds.add(main.getId()); - } - if (createdIds.isEmpty()) { - throw new JeecgBootException("未生成任何快检记录"); - } - return createdIds; + //update-begin---author:jiangxh ---date:2026-06-22 for:【快检记录】取消胶料列表批量生成,改由桌面端同步----------- + throw new JeecgBootException("胶料快检记录已改为由桌面端同步创建,请使用桌面端「胶料快检记录」功能"); + //update-end---author:jiangxh ---date:2026-06-22 for:【快检记录】取消胶料列表批量生成,改由桌面端同步----------- } private MesXslRubberQuickTestStd findApprovedStdForMaterial(String materialId) { 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/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_158__mes_xsl_rubber_quick_test_record_std_chart.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_158__mes_xsl_rubber_quick_test_record_std_chart.sql new file mode 100644 index 00000000..47d7f2f9 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_158__mes_xsl_rubber_quick_test_record_std_chart.sql @@ -0,0 +1,42 @@ +-- MES 胶料快检记录:主表扩展实验标准/方法字段;新增数据标准明细与曲线图明细 +SET NAMES utf8mb4; + +ALTER TABLE `mes_xsl_rubber_quick_test_record` + ADD COLUMN `std_name` varchar(200) DEFAULT NULL COMMENT '实验标准名称冗余' AFTER `std_id`, + ADD COLUMN `test_method_id` varchar(32) DEFAULT NULL COMMENT '实验方法ID' AFTER `std_name`, + ADD COLUMN `test_method_name` varchar(200) DEFAULT NULL COMMENT '实验方法名称冗余' AFTER `test_method_id`; + +CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record_std_line` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `record_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_record.id', + `data_point_id` varchar(32) DEFAULT NULL COMMENT '数据点ID', + `point_name` varchar(128) DEFAULT NULL COMMENT '数据点名称', + `lower_limit` decimal(18,6) DEFAULT NULL COMMENT '下限值', + `lower_warn` decimal(18,6) DEFAULT NULL COMMENT '下警告值', + `target_value` decimal(18,6) DEFAULT NULL COMMENT '目标值', + `upper_warn` decimal(18,6) DEFAULT NULL COMMENT '上警告值', + `upper_limit` decimal(18,6) DEFAULT NULL COMMENT '上限值', + `sort_no` int DEFAULT NULL COMMENT '排序号', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_rubber_quick_test_record_std_record_id` (`record_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录数据标准明细'; + +CREATE TABLE IF NOT EXISTS `mes_xsl_rubber_quick_test_record_chart_point` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `record_id` varchar(32) NOT NULL COMMENT '主表 mes_xsl_rubber_quick_test_record.id', + `time_min` decimal(10,4) DEFAULT NULL COMMENT '时间(min)', + `upper_temp` decimal(18,6) DEFAULT NULL COMMENT '上模温度', + `lower_temp` decimal(18,6) DEFAULT NULL COMMENT '下模温度', + `torque_s` decimal(18,6) DEFAULT NULL COMMENT 'S''(dNm)', + `sort_no` int DEFAULT NULL COMMENT '排序号', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_rubber_quick_test_record_chart_record_id` (`record_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES胶料快检记录曲线图数据点'; diff --git a/jeecg-boot/scan_mixing_plan.json b/jeecg-boot/scan_mixing_plan.json new file mode 100644 index 00000000..8b85cf67 --- /dev/null +++ b/jeecg-boot/scan_mixing_plan.json @@ -0,0 +1,374 @@ +{ + "scanKeyword": "MesXslMixingProductionPlan", + "entityClass": "MesXslMixingProductionPlan", + "tableName": "mes_xsl_mixing_production_plan", + "javaEntityFile": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\entity\\MesXslMixingProductionPlan.java", + "hasIzEnable": false, + "hasCodeUniqueness": false, + "uniquenessFields": [], + "backendArch": { + "unifiedAnonCtrl": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslDesktopAnonController.java", + "registeredInAnonCtrl": true, + "anonEndpoints": [ + "list" + ], + "stompNotifySvc": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\service\\MesXslStompNotifyService.java", + "registeredInStompSvc": false, + "bizCtrlFile": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslMixingProductionPlanController.java", + "bizCtrlUsesSharedNotify": false, + "bizCtrlHasPrivatePublish": false + }, + "wpfRegistrationStatus": { + "syncModuleService": false, + "syncModuleCoordinator": false, + "navigationView": false, + "stompSubscribe": false, + "menuRegistered": false, + "tenantMenuRegistered": false, + "syncModuleFilePath": "yy-admin-master\\YY.Admin\\Module\\SyncModule.cs", + "navExtFilePath": "yy-admin-master\\YY.Admin\\Module\\NavigationExtensions.cs", + "stompWsFilePath": "yy-admin-master\\YY.Admin\\Infrastructure\\Hubs\\StompWebSocketService.cs", + "menuSeedFilePath": "yy-admin-master\\YY.Admin.Core\\SeedData\\SysMenuSeedData.cs", + "summary": "✗ 待完成: SyncModule服务注册, SyncModule协调器注册, NavigationExtensions视图注册, STOMP订阅, 菜单注册" + }, + "menuSuggestion": { + "parentMenuId": 1300150000101, + "parentMenuTitle": "基础资料", + "nextMenuId": 1300150011401, + "nextOrderNo": 113, + "menuIdPattern": "130015001{N}01,N 每次 +1(1→101,2→201...)", + "alreadyExists": false, + "existingMenuId": null + }, + "apiPrefix": "/xslmes/mesXslMixingProductionPlan", + "stompCmd": "MIXING_PRODUCTION_PLAN_CHANGED", + "stompTopic": "/topic/sync/mes-mixing-production-plans", + "stompSubscriptionId": "sub-mes-xsl-mixing-production-plan", + "syncMode": "B", + "syncModeReason": "有/anon/免密端点,适合模式B", + "filterFields": [ + "machineId", + "machineName", + "planNo", + "planType", + "materialName" + ], + "fields": [ + { + "javaName": "sortNo", + "csName": "SortNo", + "sqlName": "sort_no", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "machineId", + "csName": "MachineId", + "sqlName": "machine_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": "id" + }, + { + "javaName": "machineName", + "csName": "MachineName", + "sqlName": "machine_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "shiftFlag", + "csName": "ShiftFlag", + "sqlName": "shift_flag", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planDate", + "csName": "PlanDate", + "sqlName": "plan_date", + "javaType": "Date", + "csType": "DateTime?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planNo", + "csName": "PlanNo", + "sqlName": "plan_no", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planId", + "csName": "PlanId", + "sqlName": "plan_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planType", + "csName": "PlanType", + "sqlName": "plan_type", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "sourceOrderId", + "csName": "SourceOrderId", + "sqlName": "source_order_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "materialId", + "csName": "MaterialId", + "sqlName": "material_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "materialName", + "csName": "MaterialName", + "sqlName": "material_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "orderNo", + "csName": "OrderNo", + "sqlName": "order_no", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "orderDate", + "csName": "OrderDate", + "sqlName": "order_date", + "javaType": "Date", + "csType": "DateTime?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "formulaName", + "csName": "FormulaName", + "sqlName": "formula_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planWeight", + "csName": "PlanWeight", + "sqlName": "plan_weight", + "javaType": "BigDecimal", + "csType": "double?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "plannedCarCount", + "csName": "PlannedCarCount", + "sqlName": "planned_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "scheduledCarCount", + "csName": "ScheduledCarCount", + "sqlName": "scheduled_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "finishedCarCount", + "csName": "FinishedCarCount", + "sqlName": "finished_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planCount", + "csName": "PlanCount", + "sqlName": "plan_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "remark", + "csName": "Remark", + "sqlName": "remark", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + } + ], + "pkField": { + "javaName": "id", + "csName": "Id", + "sqlName": "id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": true, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + "auditFields": [ + "TenantId", + "SysOrgCode", + "CreateBy", + "CreateTime", + "UpdateBy", + "UpdateTime", + "DelFlag" + ], + "dbConfig": { + "url": "jdbc:mysql://localhost:3306/jeecg-boot-dev?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai", + "username": "root", + "configFile": "jeecg-boot\\jeecg-boot-module\\jeecg-boot-module-airag\\src\\main\\resources\\application.yml" + }, + "dbColumns": [], + "csEntityStub": "public class MesXslMixingProductionPlan\n{\n public string? Id { get; set; }\n public int? SortNo { get; set; }\n public string? MachineId { get; set; } [Dict:id]\n public string? MachineName { get; set; }\n public int? ShiftFlag { get; set; }\n public DateTime? PlanDate { get; set; }\n public string? PlanNo { get; set; }\n public string? PlanId { get; set; }\n public string? PlanType { get; set; }\n public string? SourceOrderId { get; set; }\n public string? MaterialId { get; set; }\n public string? MaterialName { get; set; }\n public string? OrderNo { get; set; }\n public DateTime? OrderDate { get; set; }\n public string? FormulaName { get; set; }\n public double? PlanWeight { get; set; }\n public int? PlannedCarCount { get; set; }\n public int? ScheduledCarCount { get; set; }\n public int? FinishedCarCount { get; set; }\n public int? PlanCount { get; set; }\n public string? Remark { get; set; }\n public int? TenantId { get; set; }\n public string? SysOrgCode { get; set; }\n public string? CreateBy { get; set; }\n public DateTime? CreateTime { get; set; }\n public string? UpdateBy { get; set; }\n public DateTime? UpdateTime { get; set; }\n public int? DelFlag { get; set; }\n // 只读显示属性:\n // public string StatusText => Status == \"1\" ? \"停用\" : \"启用\";\n}", + "generationHints": { + "eventClassName": "MesXslMixingProductionPlanChangedEvent", + "serviceInterface": "IMixingProductionPlanService", + "serviceImpl": "MixingProductionPlanService", + "syncCoordinator": "MixingProductionPlanSyncCoordinator", + "listViewModel": "MixingProductionPlanListViewModel", + "editDialogViewModel": "MixingProductionPlanEditDialogViewModel", + "listView": "MixingProductionPlanListView", + "editDialogView": "MixingProductionPlanEditDialogView", + "pendingOpsFile": "mes-xsl-mixing-production-plan-pending-ops.json", + "cacheFile": "mes-xsl-mixing-production-plan-cache.json", + "nextMenuId": 1300150011401, + "nextMenuOrderNo": 113, + "backendFilesToModify": [ + "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\\MesXslStompNotifyService.java", + "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslMixingProductionPlanController.java", + "jeecg-boot-base-core/.../ShiroConfig.java" + ], + "wpfFilesToModify": [ + "yy-admin-master\\YY.Admin\\Module\\SyncModule.cs", + "yy-admin-master\\YY.Admin\\Module\\NavigationExtensions.cs", + "yy-admin-master\\YY.Admin\\Infrastructure\\Hubs\\StompWebSocketService.cs", + "yy-admin-master\\YY.Admin.Core\\SeedData\\SysMenuSeedData.cs", + "YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs" + ] + } +} \ No newline at end of file diff --git a/jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue b/jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue index ae8e0fa1..b6c74ff3 100644 --- a/jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue +++ b/jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue @@ -5,15 +5,6 @@ 新增 导出 导入 - - 检验 - + + 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 @@ -