更新,工序计划

This commit is contained in:
tzy 2026-10-27 17:37:34 +08:00
parent 5e091843b0
commit 9fb3090015
81 changed files with 5990 additions and 1519 deletions

35
.gitignore vendored
View File

@ -1,24 +1,51 @@
# Maven / Java
/.project /.project
*.class *.class
.classpath .classpath
.settings .settings
.factorypath .factorypath
.project .project
.logs
.log
pom.properties pom.properties
**/target/** **/target/**
mvnw mvnw
mvnw.cmd mvnw.cmd
maven-wrapper.properties maven-wrapper.properties
**/logs/
# Logs
.logs
.log
*.log
**/logs/
/logs/ /logs/
# IDE # IDE
.idea/ .idea/
*.iml *.iml
.vscode/ .vscode/
*.iws
*.ipr
out/
.cursor/
# 本地文档/工具(默认不进仓库;需要单独提交时再强制 add) # OS
.DS_Store
Thumbs.db
Desktop.ini
# Node(文档脚本等本地依赖)
**/node_modules/
package-lock.json
yarn.lock
# 本地文档/工具产物(默认不进仓库)
docs/ docs/
# 临时/导出文件
*.tmp
*.bak
*.swp
*.orig
*.xlsx
!**/src/**/*.xlsx
*.docx
!**/src/**/*.docx

12
pom.xml
View File

@ -36,7 +36,8 @@
<alibaba-ttl.version>2.14.2</alibaba-ttl.version> <alibaba-ttl.version>2.14.2</alibaba-ttl.version>
<xxl-job.version>2.4.0</xxl-job.version> <xxl-job.version>2.4.0</xxl-job.version>
<lombok.version>1.18.30</lombok.version> <lombok.version>1.18.30</lombok.version>
<bouncycastle.version>1.72</bouncycastle.version> <!-- 1.74 起仍有 DERApplicationSpecific;1.75+ 删除会导致 jcifs-ng 等报 NoClassDefFoundError -->
<bouncycastle.version>1.74</bouncycastle.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<!-- 临时修复 snakeyaml 漏洞 --> <!-- 临时修复 snakeyaml 漏洞 -->
@ -265,10 +266,15 @@
<version>${snakeyaml.version}</version> <version>${snakeyaml.version}</version>
</dependency> </dependency>
<!-- 加密包引入 --> <!-- 加密包:统一 BouncyCastle 版本,避免多版本冲突导致 JWT 登录失败 -->
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId> <artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version> <version>${bouncycastle.version}</version>
</dependency> </dependency>

View File

@ -103,12 +103,36 @@
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId> <artifactId>dingtalk</artifactId>
<version>2.0.15</version> <version>2.0.15</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId> <artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version> <version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -189,6 +213,10 @@
<configuration> <configuration>
<includeSystemScope>true</includeSystemScope> <!-- jar包需要加这个 --> <includeSystemScope>true</includeSystemScope> <!-- jar包需要加这个 -->
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 --> <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
<jvmArguments>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
</jvmArguments>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>

View File

@ -1,5 +1,6 @@
package com.ruoyi; package com.ruoyi;
import cn.hutool.crypto.SecureUtil;
import org.apache.poi.openxml4j.util.ZipSecureFile; import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@ -25,6 +26,8 @@ public class RuoYiApplication {
// 全局放宽,启动时执行一次即可 // 全局放宽,启动时执行一次即可
ZipSecureFile.setMaxFileCount(100000); ZipSecureFile.setMaxFileCount(100000);
ZipSecureFile.setMinInflateRatio(0.005); ZipSecureFile.setMinInflateRatio(0.005);
// Oracle JDK17 + Spring Boot fat jar 下 BC JCE 提供者会触发 zip file closed,JWT 用 JDK 自带 HMAC 即可
SecureUtil.disableBouncyCastle();
} }
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaIgnore;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.dingding.DingCallbackCrypto; import com.ruoyi.common.dingding.DingCallbackCrypto;
import com.ruoyi.common.dingding.robot.RobotGroupService; import com.ruoyi.common.dingding.robot.RobotGroupService;
@ -285,15 +286,26 @@ public class CallbackController {
*/ */
private void updateProcessOrderProStatus(String outTrackId, Long routeStatus, Long bomStatus) { private void updateProcessOrderProStatus(String outTrackId, Long routeStatus, Long bomStatus) {
try { try {
// 根据 notificationCardId 查询 // 根据审核卡片 Id 查询
ProcessOrderPro order = processOrderProMapper.selectOne(new LambdaQueryWrapper<ProcessOrderPro>().eq(ProcessOrderPro::getAuditCardId, outTrackId)); ProcessOrderPro order = processOrderProMapper.selectOne(new LambdaQueryWrapper<ProcessOrderPro>().eq(ProcessOrderPro::getAuditCardId, outTrackId));
if (order != null) { if (order != null) {
ProcessOrderPro update = new ProcessOrderPro(); // 幂等:已是目标审核状态则不再更新/通知(防钉钉回调重试连发)
update.setId(order.getId()); if (routeStatus != null && routeStatus.equals(order.getRouteStatus())) {
update.setRouteStatus(routeStatus); bizLogger.info("工艺审核状态已是目标值,跳过重复处理: id={}, routeStatus={}", order.getId(), routeStatus);
update.setBomStatus(bomStatus); return;
update.setUpdateBy("DingDing"); }
processOrderProMapper.updateById(update); // 条件更新:仅当 route_status 仍非目标值时更新,避免并发双回调都发通知
LambdaUpdateWrapper<ProcessOrderPro> uw = new LambdaUpdateWrapper<>();
uw.eq(ProcessOrderPro::getId, order.getId())
.and(w -> w.isNull(ProcessOrderPro::getRouteStatus).or().ne(ProcessOrderPro::getRouteStatus, routeStatus))
.set(ProcessOrderPro::getRouteStatus, routeStatus)
.set(ProcessOrderPro::getBomStatus, bomStatus)
.set(ProcessOrderPro::getUpdateBy, "DingDing");
int rows = processOrderProMapper.update(null, uw);
if (rows <= 0) {
bizLogger.info("工艺审核状态已被并发更新,跳过通知: id={}, routeStatus={}", order.getId(), routeStatus);
return;
}
bizLogger.info("更新 ProcessOrderPro 状态成功: id={}, routeStatus={}, bomStatus={}", bizLogger.info("更新 ProcessOrderPro 状态成功: id={}, routeStatus={}, bomStatus={}",
order.getId(), routeStatus, bomStatus); order.getId(), routeStatus, bomStatus);
@ -409,8 +421,8 @@ public class CallbackController {
// 其他事件(包括机器人消息) // 其他事件(包括机器人消息)
if (eventJson.containsKey("text")) { if (eventJson.containsKey("text")) {
String content = extractRobotText(eventJson); String content = extractRobotText(eventJson);
bizLogger.info("机器人收到文本消息: content={} conversationTitle={} robotCode={}", bizLogger.info("机器人收到文本消息: content={} conversationTitle={} conversationId={} robotCode={}",
content, eventJson.getString("conversationTitle"), eventJson.getString("robotCode")); content, eventJson.getString("conversationTitle"), eventJson.getString("conversationId"), eventJson.getString("robotCode"));
boolean isHandled = false; boolean isHandled = false;
@ -598,6 +610,24 @@ public class CallbackController {
}).start(); }).start();
} }
if (textContains(content, "群id") || textContains(content, "群ID")
|| textContains(content, "会话id") || textContains(content, "会话ID")
|| textContains(content, "群会话")) {
isHandled = true;
String sessionWebhook = eventJson.getString("sessionWebhook");
String senderId = eventJson.getString("senderStaffId");
String conversationId = eventJson.getString("conversationId");
String title = eventJson.getString("conversationTitle");
String reply = "群名称:" + (title == null ? "" : title)
+ "\n群会话ID:\n" + (conversationId == null ? "未获取到" : conversationId);
if (sessionWebhook != null && !sessionWebhook.isEmpty()) {
webhookClient.sendText(sessionWebhook, reply, Collections.singletonList(senderId), null);
bizLogger.info("回复群会话ID conversationId={} senderId={}", conversationId, senderId);
} else {
bizLogger.warn("未获取到 sessionWebhook,无法回复群会话ID");
}
}
// 默认回复 // 默认回复
if (!isHandled) { if (!isHandled) {
String sessionWebhook = eventJson.getString("sessionWebhook"); String sessionWebhook = eventJson.getString("sessionWebhook");
@ -605,7 +635,8 @@ public class CallbackController {
String conversationId = eventJson.getString("conversationId"); String conversationId = eventJson.getString("conversationId");
if (conversationId != null && !conversationId.isEmpty()) { if (conversationId != null && !conversationId.isEmpty()) {
String helpContent = "您好,我是数据提醒机器人。收到您的消息,但不知道你在讲什么?。\n" + "目前支持以下业务指令,请直接回复关键词:\n" + "1. 安全库存\n" + "2. 延期\n" + "3. 预警\n" + "4. 采购 / 采购申请\n" + "5. 工序汇报未质检 / 质检未入库\n" + "6. 未及时转移\n" + "7. 已转入未转出 / 转入但部分转出 / 部分转出\n" + "8. 开工未转入 / 开工未转"; String helpContent = "您好,我是数据提醒机器人。收到您的消息,但不知道你在讲什么?。\n" + "目前支持以下业务指令,请直接回复关键词:\n" + "1. 安全库存\n" + "2. 延期\n" + "3. 预警\n" + "4. 采购 / 采购申请\n" + "5. 工序汇报未质检 / 质检未入库\n" + "6. 未及时转移\n" + "7. 已转入未转出 / 转入但部分转出 / 部分转出\n" + "8. 开工未转入 / 开工未转\n" +
"\n本群会话ID(填到群预警推送页面):\n" + conversationId;
// 使用回调中的 sessionWebhook 直接回复 (这是最标准的回调回复方式) // 使用回调中的 sessionWebhook 直接回复 (这是最标准的回调回复方式)
if (sessionWebhook != null && !sessionWebhook.isEmpty()) { if (sessionWebhook != null && !sessionWebhook.isEmpty()) {

View File

@ -36,7 +36,7 @@ server:
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分 # 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512 buffer-size: 512
# 是否分配的直接内存 # 是否分配的直接内存\/system/kingdee/materialDocHub/board
direct-buffers: true direct-buffers: true
threads: threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
@ -162,6 +162,10 @@ security:
- /system/route/push/** - /system/route/push/**
- /DataBoard/ProduceKanBan/** - /DataBoard/ProduceKanBan/**
- /system/workCenterData/produceDataKanban - /system/workCenterData/produceDataKanban
# MES 图纸查询 / 下载(方法上另有 @SaIgnore + appKey 鉴权)
- /open/mes/**
# 北通道实验台设备状态看板
- /open/devstat/**
# MyBatisPlus配置 # MyBatisPlus配置
# https://baomidou.com/config/ # https://baomidou.com/config/
@ -320,21 +324,21 @@ dingtalk:
info-send-app-key: ding9bhxctsmq5jsf8pk info-send-app-key: ding9bhxctsmq5jsf8pk
info-send-app-secret: OBhuwZzf31kcVqqVwODtu59Aqmql21gc301V06Y8URdjZy_YmBJ47-AZ4nnLZVVm info-send-app-secret: OBhuwZzf31kcVqqVwODtu59Aqmql21gc301V06Y8URdjZy_YmBJ47-AZ4nnLZVVm
info-send-robot-code: ding9bhxctsmq5jsf8pk info-send-robot-code: ding9bhxctsmq5jsf8pk
info-send-user-ids: "665416234832653417,283633411029629036,0754423610937470" info-send-user-ids: "665416234832653417,283633411029629036,0754423610937470,01692556241068902"
#项目卡片模板ID #项目卡片模板ID
project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema
#工艺审核互动卡片模板ID #工艺审核互动卡片模板ID
route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema
#工艺审核互动卡片回调路由Key #工艺审核互动卡片回调路由Key
callback-route-key: update_route_status2 callback-route-key: update_route_status2
# 出图完成通知(钉钉 userId/staffId,多人逗号分隔;以 0 开头的须加引号) # 出图完成 / PLM图纸到达通知(钉钉 userId/staffId,多人逗号分隔;以 0 开头的须加引号)
notify-user-id: "173769650335444471,0108392663561185496,283633411029629036" notify-user-id: "293047444820124967,0108392663561185496,03610242692523410847"
# 工艺审核互动卡片 # 工艺审核互动卡片
route-approval-id: "01201038164023402380" route-approval-id: "0108392663561185496"
# 工艺计划推送用户 # 工艺计划推送用户;无需做工艺且出图完成后、PLM 总装BOM与工艺都到齐后通知生管
plan-user-id: "313514580637707245,02256761642526333139,192530665123549546,01201038164023402380" plan-user-id: "313514580637707245,02256761642526333139,192530665123549546,0108392663561185496,03361938481539892633,283633411029629036"
# 同步K3通知用户 # 同步K3通知用户
sync-k3-notify-user-ids: "313514580637707245,02256761642526333139,192530665123549546" sync-k3-notify-user-ids: "313514580637707245,02256761642526333139,192530665123549546,03361938481539892633"
#技术方案申请流程编码 #技术方案申请流程编码
scheme-application-process-code: PROC-81A79B4D-F0EA-4353-9A91-68FD7E3BCEBB scheme-application-process-code: PROC-81A79B4D-F0EA-4353-9A91-68FD7E3BCEBB
#技术方案申请流程同意后是否推送PLM #技术方案申请流程同意后是否推送PLM
@ -345,12 +349,41 @@ dingtalk:
scheme-bpms-plm-push-task-show-names: 总工程师 scheme-bpms-plm-push-task-show-names: 总工程师
#技术方案申请流程同意后推送PLM是否显示总工程师意见 #技术方案申请流程同意后推送PLM是否显示总工程师意见
scheme-bpms-plm-push-on-chief-opinion-filled: true scheme-bpms-plm-push-on-chief-opinion-filled: true
# 钉钉待办中转只放连接信息。哪个方法发、创建人、接收人、截止天数在表 ding_todo_scene(页面「钉钉待办」)
# 测试 http://192.168.5.232:8089 ,正式 http://192.168.5.121:8089
todo-base-url: http://192.168.5.121:8089
# adapter 登录账号,三项都填上才会真正调用创建待办
todo-username: mes
todo-password: 123456
todo-uuid: mes
# 页面未选类型时的默认:code=考勤工号,name=姓名
todo-recipient-type: name
# 登录返回的 token 放到这个请求头里再调 /ding/send
todo-token-header: Authorization
# 需要 Bearer 时写成 "Bearer ",对方直接收 token 就留空
todo-token-prefix:
# VMI 补货推送金蝶采购申请
vmi:
# 仓储部VMI负责人钉钉userId(逗号分隔);物料未单独配置 dingUserId 时用此默认
ding-user-ids: "696700331229897052,285404475726261431,283633411029629036"
kingdee:
# 申请人:取登录用户 sys_user.employee_number(金蝶员工工号),不再使用固定 applicant-number
# 申请部门:仓储部
application-dept: "009"
# 申请组织
application-org: "100"
# 图纸档案 FTP:同机 14.218,下载 21 / 上传 8022(8022 登录根目录=电子档案/1.标准图纸存放文件夹) # 图纸档案 FTP:同机 14.218,下载 21 / 上传 8022(8022 登录根目录=电子档案/1.标准图纸存放文件夹)
app: app:
drawing: drawing:
# DWG 上传在应用服务器上的暂存目录(须可写);实际上传目标走 FTP 8022 # DWG 上传在应用服务器上的暂存目录(须可写);实际上传目标走 FTP 8022
upload-staging-dir: D:/file/dwg-upload upload-staging-dir: D:/file/dwg-upload
# 图纸/模型文件服务。前端只调本系统,由后端转发下载
file-query:
base-url: http://192.168.5.20:28000
download-path: /api/FileQuery/download
read-timeout-ms: 300000
# 工艺计划表落盘目录(手动上传 / PLM 导出共用;下按生产令号分子目录) # 工艺计划表落盘目录(手动上传 / PLM 导出共用;下按生产令号分子目录)
plan-excel: plan-excel:
base-dir: D:/file/plan-excel base-dir: D:/file/plan-excel
@ -401,3 +434,9 @@ plm:
# 库内 DDAttachment 常为空时,推送前再从钉钉 getProcessInstance 拉附件 # 库内 DDAttachment 常为空时,推送前再从钉钉 getProcessInstance 拉附件
refresh-attachments-from-ding-on-push: true refresh-attachments-from-ding-on-push: true
log-payload-max-chars: 8000 log-payload-max-chars: 8000
# MES 图纸开放(/open/mes/drawing,appKey/appSecret 鉴权)
mes:
auth:
app-key: mes
app-secret: change-me-mes-secret

View File

@ -174,10 +174,10 @@
<artifactId>lock4j-redisson-spring-boot-starter</artifactId> <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
</dependency> </dependency>
<!-- 加密包引入 --> <!-- 加密包引入(与 dependencyManagement 统一版本) -->
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId> <artifactId>bcprov-jdk18on</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.hierynomus</groupId> <groupId>com.hierynomus</groupId>
@ -256,7 +256,13 @@
<dependency> <dependency>
<groupId>eu.agno3.jcifs</groupId> <groupId>eu.agno3.jcifs</groupId>
<artifactId>jcifs-ng</artifactId> <artifactId>jcifs-ng</artifactId>
<version>2.1.9</version> <version>2.1.10</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>com.github.xiaoymin</groupId>
@ -283,12 +289,36 @@
<artifactId>alibaba-dingtalk-service-sdk</artifactId> <artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version> <version>2.0.0</version>
<scope>compile</scope> <scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId> <artifactId>dingtalk</artifactId>
<version>2.0.15</version> <version>2.0.15</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -303,4 +333,17 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups combine.self="override"/>
<excludedGroups combine.self="override"/>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View File

@ -132,8 +132,8 @@ public class DingTalkProperties {
* 工艺卡片模版id * 工艺卡片模版id
*/ */
private String routeCardTemplateId; private String routeCardTemplateId;
/** /**
* 工艺计划接收人 * 工艺计划接收人;无需做工艺且出图完成后、PLM 总装BOM与工艺到齐后也通知这批人(逗号分隔)
*/ */
private String planUserId; private String planUserId;
@ -158,6 +158,41 @@ public class DingTalkProperties {
*/ */
private String messageRelayBaseUrl; private String messageRelayBaseUrl;
/**
* 待办中转根地址。测试 http://192.168.5.232:8089 ,正式 http://192.168.5.121:8089
*/
private String todoBaseUrl;
/**
* 待办中转登录账号
*/
private String todoUsername;
/**
* 待办中转登录密码
*/
private String todoPassword;
/**
* 待办中转登录 uuid(adapter 分配的权限标识)
*/
private String todoUuid;
/**
* 接收人类型:code 考勤工号,name 姓名。页面每一行可以单独选。
*/
private String todoRecipientType = "name";
/**
* 登录成功后调用 /ding/send 时携带 token 的请求头
*/
private String todoTokenHeader = "Authorization";
/**
* token 前缀,例如 {@code Bearer };为空则只传 token 本身
*/
private String todoTokenPrefix = "";
/** /**
* 技术方案申请表 OA 模板 processCode(如 PROC-xxx);为空则不处理 BPMS 审批事件 * 技术方案申请表 OA 模板 processCode(如 PROC-xxx);为空则不处理 BPMS 审批事件
*/ */

View File

@ -75,6 +75,12 @@ public class SysUser extends BaseEntity {
@Sensitive(strategy = SensitiveStrategy.PHONE) @Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber; private String phonenumber;
/**
* 员工编号(金蝶员工工号,如 00094)
*/
@Size(min = 0, max = 64, message = "员工编号长度不能超过{max}个字符")
private String employeeNumber;
/** /**
* 用户性别 * 用户性别
*/ */

View File

@ -8,12 +8,11 @@ import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.ResourceUtils;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -35,20 +34,10 @@ public class ExcelTemplateProc {
String exportFilePathAndName, String exportFilePathAndName,
Map<String, Object> staticDataMap, Map<String, Object> staticDataMap,
List<DynamicDataMapping> dynamicDataMappingList) throws IOException { List<DynamicDataMapping> dynamicDataMappingList) throws IOException {
try (InputStream inputStream = openTemplateInputStream(templateFileName)) {
InputStream inputStream; Workbook workbook = dealAllSheetsByTemplate(inputStream, staticDataMap, dynamicDataMappingList);
saveExportFile(workbook, exportFilePathAndName);
if (templateFileName.startsWith("/") || templateFileName.contains(":")) {
// 绝对路径 (Windows: D:/... Linux: /opt/...)
inputStream = Files.newInputStream(new File(templateFileName).toPath());
} else {
// classpath 路径
ClassPathResource resource = new ClassPathResource(templateFileName);
inputStream = resource.getInputStream();
} }
Workbook workbook = dealAllSheetsByTemplate(inputStream, staticDataMap, dynamicDataMappingList);
saveExportFile(workbook, exportFilePathAndName);
} }
/** /**
@ -60,12 +49,9 @@ public class ExcelTemplateProc {
if (sheetConfigs == null || sheetConfigs.isEmpty()) { if (sheetConfigs == null || sheetConfigs.isEmpty()) {
throw new IllegalArgumentException("sheetConfigs 不能为空"); throw new IllegalArgumentException("sheetConfigs 不能为空");
} }
InputStream inputStream = openTemplateInputStream(templateFileName);
XSSFWorkbook workbook; XSSFWorkbook workbook;
try { try (InputStream inputStream = openTemplateInputStream(templateFileName)) {
workbook = new XSSFWorkbook(inputStream); workbook = new XSSFWorkbook(inputStream);
} finally {
inputStream.close();
} }
while (workbook.getNumberOfSheets() > 1) { while (workbook.getNumberOfSheets() > 1) {
workbook.removeSheetAt(workbook.getNumberOfSheets() - 1); workbook.removeSheetAt(workbook.getNumberOfSheets() - 1);
@ -82,12 +68,105 @@ public class ExcelTemplateProc {
saveExportFile(workbook, exportFilePathAndName); saveExportFile(workbook, exportFilePathAndName);
} }
/**
* 打开 Excel 模板流。classpath 资源必须先完整读入内存再交给 POI:
* Spring Boot fat jar 中 ClassPathResource 返回的是嵌套 JAR 的 ZipInflaterInputStream,
* 直接 new XSSFWorkbook(stream) 会 ZipException: invalid block type。
*/
private static InputStream openTemplateInputStream(String templateFileName) throws IOException { private static InputStream openTemplateInputStream(String templateFileName) throws IOException {
byte[] bytes = readTemplateBytes(templateFileName);
return new ByteArrayInputStream(bytes);
}
private static byte[] readTemplateBytes(String templateFileName) throws IOException {
if (templateFileName.startsWith("/") || templateFileName.contains(":")) { if (templateFileName.startsWith("/") || templateFileName.contains(":")) {
return Files.newInputStream(new File(templateFileName).toPath()); File file = new File(templateFileName);
if (!file.exists() || !file.isFile()) {
throw new FileNotFoundException("Excel模板不存在: " + templateFileName);
}
return validateXlsxBytes(Files.readAllBytes(file.toPath()), templateFileName);
} }
ClassPathResource resource = new ClassPathResource(templateFileName);
return resource.getInputStream(); List<String> candidates = buildTemplateCandidates(templateFileName);
List<ClassLoader> loaders = new ArrayList<>();
// Spring Boot / 异步线程:优先用上下文 ClassLoader,再回退到本类加载器
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
if (ctx != null) {
loaders.add(ctx);
}
ClassLoader self = ExcelTemplateProc.class.getClassLoader();
if (self != null && self != ctx) {
loaders.add(self);
}
loaders.add(ClassLoader.getSystemClassLoader());
List<String> tried = new ArrayList<>();
for (String candidate : candidates) {
for (ClassLoader loader : loaders) {
if (loader == null) {
continue;
}
String mark = candidate + " @ " + loader.getClass().getName();
tried.add(mark);
byte[] bytes = readClasspathBytes(loader, candidate);
if (bytes != null) {
return validateXlsxBytes(bytes, candidate);
}
}
// 开发态:相对工作目录兜底
File local = new File(candidate);
if (local.isFile()) {
return validateXlsxBytes(Files.readAllBytes(local.toPath()), local.getAbsolutePath());
}
tried.add(candidate + " @ file:" + local.getAbsolutePath());
}
throw new FileNotFoundException("Excel模板不存在: " + templateFileName + ";已尝试: " + tried);
}
private static List<String> buildTemplateCandidates(String templateFileName) {
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add(templateFileName);
// 模版 / 模板 常见混用
if (templateFileName.contains("模版")) {
set.add(templateFileName.replace("模版", "模板"));
}
if (templateFileName.contains("模板")) {
set.add(templateFileName.replace("模板", "模版"));
}
if (templateFileName.startsWith("jpg/")) {
String name = templateFileName.substring("jpg/".length());
set.add("EXCEL模板/" + name);
if (name.contains("模版")) {
set.add("EXCEL模板/" + name.replace("模版", "模板"));
}
}
return new ArrayList<>(set);
}
private static byte[] readClasspathBytes(ClassLoader loader, String resourcePath) {
// 不用 ClassPathResource.exists():部分环境下对依赖 jar / 中文路径会误判不存在
try (InputStream in = loader.getResourceAsStream(resourcePath)) {
if (in == null) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
} catch (Exception e) {
return null;
}
}
private static byte[] validateXlsxBytes(byte[] bytes, String templateFileName) throws IOException {
if (bytes == null || bytes.length < 4 || bytes[0] != 0x50 || bytes[1] != 0x4B) {
throw new IOException("Excel模板不是有效的xlsx文件: " + templateFileName
+ ", size=" + (bytes == null ? -1 : bytes.length));
}
return bytes;
} }
private static String sanitizeSheetName(String name) { private static String sanitizeSheetName(String name) {

View File

@ -1,25 +1,21 @@
package com.ruoyi.common.utils; package com.ruoyi.common.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.SimpleCache;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.cglib.core.Converter;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* bean深拷贝工具(基于 cglib 性能优异) * bean深拷贝工具
* <p> * <p>
* 重点 cglib 不支持 拷贝到链式对象 * 重点 不支持 拷贝到链式对象
* 例如: 源对象 拷贝到 目标(链式对象) * 例如: 源对象 拷贝到 目标(链式对象)
* 请区分好`浅拷贝`和`深拷贝`再做使用 * 请区分好`浅拷贝`和`深拷贝`再做使用
* *
@ -60,8 +56,7 @@ public class BeanCopyUtils {
if (ObjectUtil.isNull(desc)) { if (ObjectUtil.isNull(desc)) {
return null; return null;
} }
BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(source.getClass(), desc.getClass(), null); BeanUtil.copyProperties(source, desc);
beanCopier.copy(source, desc, null);
return desc; return desc;
} }
@ -92,12 +87,11 @@ public class BeanCopyUtils {
* @param bean 数据来源实体 * @param bean 数据来源实体
* @return map对象 * @return map对象
*/ */
@SuppressWarnings("unchecked")
public static <T> Map<String, Object> copyToMap(T bean) { public static <T> Map<String, Object> copyToMap(T bean) {
if (ObjectUtil.isNull(bean)) { if (ObjectUtil.isNull(bean)) {
return null; return null;
} }
return BeanMap.create(bean); return BeanUtil.beanToMap(bean);
} }
/** /**
@ -132,7 +126,7 @@ public class BeanCopyUtils {
if (ObjectUtil.isNull(bean)) { if (ObjectUtil.isNull(bean)) {
return null; return null;
} }
BeanMap.create(bean).putAll(map); BeanUtil.fillBeanWithMap(map, bean, false);
return bean; return bean;
} }
@ -155,50 +149,4 @@ public class BeanCopyUtils {
return copyMap; return copyMap;
} }
/**
* BeanCopier属性缓存<br>
* 缓存用于防止多次反射造成的性能问题
*
* @author Looly
* @since 5.4.1
*/
public enum BeanCopierCache {
/**
* BeanCopier属性缓存单例
*/
INSTANCE;
private final SimpleCache<String, BeanCopier> cache = new SimpleCache<>();
/**
* 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param converter 转换器
* @return Map中对应的BeanCopier
*/
public BeanCopier get(Class<?> srcClass, Class<?> targetClass, Converter converter) {
final String key = genKey(srcClass, targetClass, converter);
return cache.get(key, () -> BeanCopier.create(srcClass, targetClass, converter != null));
}
/**
* 获得类与转换器生成的key
*
* @param srcClass 源Bean的类
* @param targetClass 目标Bean的类
* @param converter 转换器
* @return 属性名和Map映射的key
*/
private String genKey(Class<?> srcClass, Class<?> targetClass, Converter converter) {
final StringBuilder key = StrUtil.builder()
.append(srcClass.getName()).append('#').append(targetClass.getName());
if (null != converter) {
key.append('#').append(converter.getClass().getName());
}
return key.toString();
}
}
} }

View File

@ -16,6 +16,9 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Slf4j @Slf4j
public final class FtpUtil { public final class FtpUtil {
@ -138,6 +141,86 @@ public final class FtpUtil {
remoteDir, localDir, fileNameFilter); remoteDir, localDir, fileNameFilter);
} }
/**
* 在远程目录(含全部子目录)中查找并下载第一个匹配的文件。
* 目录不存在时返回失败(与整目录递归下载「视为空」不同)。
*/
public static R<String> downloadMatchingFileRecursive(DrawingFtpProperties.Endpoint ftp, String remoteDir,
String localDir, Predicate<String> fileNameMatcher) {
if (fileNameMatcher == null) {
return R.fail("文件匹配规则不能为空");
}
FTPClient ftpClient = null;
try {
ftpClient = getFTPClient(ftp.getHost(), ftp.getUsername(), ftp.getPassword(), ftp.getPort());
if (ftpClient == null || !ftpClient.isConnected()) {
return R.fail("FTP连接失败 " + ftp.getHost() + ":" + ftp.getPort());
}
String dir = normalizeRemotePath(remoteDir);
log.info("FTP 递归匹配下载 {}:{} dir={}", ftp.getHost(), ftp.getPort(), dir);
if (!changeWorkingDirectoryWithEncoding(ftpClient, dir)) {
return R.fail("远程目录不存在: " + dir + " (reply=" + ftpClient.getReplyString().trim() + ")");
}
File localDirectory = new File(localDir);
if (!localDirectory.exists() && !localDirectory.mkdirs()) {
return R.fail("无法创建本地目录: " + localDir);
}
String saved = findAndDownloadMatching(ftpClient, dir, localDirectory, fileNameMatcher);
if (saved == null) {
return R.fail("远程目录中未找到匹配文件 (dir=" + dir + ")");
}
return R.ok(saved);
} catch (Exception e) {
log.error("FTP递归匹配下载异常 dir={}", remoteDir, e);
return R.fail("下载失败: " + e.getMessage());
} finally {
disconnectQuietly(ftpClient);
}
}
private static String findAndDownloadMatching(FTPClient ftp, String remoteDir, File localDir,
Predicate<String> fileNameMatcher) throws IOException {
if (!changeWorkingDirectoryWithEncoding(ftp, remoteDir)) {
return null;
}
FTPFile[] files = ftp.listFiles();
if (files == null || files.length == 0) {
return null;
}
List<String> subDirs = new ArrayList<>();
for (FTPFile file : files) {
String name = file.getName();
if (name == null || ".".equals(name) || "..".equals(name)) {
continue;
}
if (file.isDirectory()) {
subDirs.add(name);
continue;
}
if (!fileNameMatcher.test(name)) {
continue;
}
File localFile = new File(localDir, name);
try (OutputStream os = Files.newOutputStream(localFile.toPath())) {
if (retrieveFileWithEncoding(ftp, name, os)) {
log.info("FTP匹配下载成功: {}/{} -> {}", remoteDir, name, localFile.getAbsolutePath());
return name;
}
Files.deleteIfExists(localFile.toPath());
log.warn("FTP匹配下载失败: {}/{} reply={}", remoteDir, name, ftp.getReplyString());
return null;
}
}
for (String name : subDirs) {
String childRemote = normalizeRemotePath(remoteDir + "/" + name);
String saved = findAndDownloadMatching(ftp, childRemote, localDir, fileNameMatcher);
if (saved != null) {
return saved;
}
}
return null;
}
/** /**
* 出图 Excel 远程目录:basePath(如 F:) + 年份 + 生产令号,与 SMB 路径 F:\2026\令号 一致。 * 出图 Excel 远程目录:basePath(如 F:) + 年份 + 生产令号,与 SMB 路径 F:\2026\令号 一致。
*/ */
@ -258,6 +341,9 @@ public final class FtpUtil {
FTPFile[] files = ftpClient.listFiles(); FTPFile[] files = ftpClient.listFiles();
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
if (fileNameFilter != null && !fileNameFilter.isEmpty()) {
return R.fail("远程目录为空,未找到文件: " + fileNameFilter + " (dir=" + remoteDir + ")");
}
return R.ok("远程目录为空,无需下载"); return R.ok("远程目录为空,无需下载");
} }
@ -270,7 +356,8 @@ public final class FtpUtil {
if (fileName.startsWith(".") || file.isDirectory()) { if (fileName.startsWith(".") || file.isDirectory()) {
continue; continue;
} }
if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) { if (fileNameFilter != null && !fileNameFilter.isEmpty()
&& !fileNameMatchesFilter(fileName, fileNameFilter)) {
continue; continue;
} }
totalCount++; totalCount++;
@ -312,6 +399,191 @@ public final class FtpUtil {
disconnectQuietly(ftpClient); disconnectQuietly(ftpClient);
} }
/**
* 递归下载远程目录(含全部子目录)到本地。远程目录不存在时返回成功(视为空)。
*/
public static R<String> downloadFtpDirectoryRecursive(DrawingFtpProperties.Endpoint ftp, String remoteDir,
String localDir) {
FTPClient ftpClient = null;
try {
ftpClient = getFTPClient(ftp.getHost(), ftp.getUsername(), ftp.getPassword(), ftp.getPort());
if (ftpClient == null || !ftpClient.isConnected()) {
return R.fail("FTP连接失败 " + ftp.getHost() + ":" + ftp.getPort());
}
return downloadFtpDirectoryRecursive(ftpClient, remoteDir, localDir);
} catch (Exception e) {
log.error("FTP递归下载异常 dir={}", remoteDir, e);
return R.fail("下载失败: " + e.getMessage());
} finally {
disconnectQuietly(ftpClient);
}
}
public static R<String> downloadFtpDirectoryRecursive(FTPClient ftpClient, String remoteDir, String localDir) {
if (ftpClient == null || !ftpClient.isConnected()) {
return R.fail("FTP连接不可用");
}
String dir = normalizeRemotePath(remoteDir);
try {
if (!changeWorkingDirectoryWithEncoding(ftpClient, dir)) {
log.info("FTP 远程目录不存在,跳过下载: {}", dir);
return R.ok("远程目录不存在,无需下载: " + dir);
}
int count = downloadTree(ftpClient, dir, localDir);
return R.ok("递归下载完成,文件数=" + count + " dir=" + dir);
} catch (Exception e) {
log.error("FTP递归下载异常 dir={}", dir, e);
return R.fail("下载失败: " + e.getMessage());
}
}
/**
* 删除远程目录内全部文件与子目录,保留该目录本身。目录不存在视为已清理。
*/
public static R<String> deleteRemoteDirectoryContents(DrawingFtpProperties.Endpoint ftp, String remoteDir) {
FTPClient ftpClient = null;
try {
ftpClient = getFTPClient(ftp.getHost(), ftp.getUsername(), ftp.getPassword(), ftp.getPort());
if (ftpClient == null || !ftpClient.isConnected()) {
return R.fail("FTP连接失败 " + ftp.getHost() + ":" + ftp.getPort());
}
return deleteRemoteDirectoryContents(ftpClient, remoteDir);
} catch (Exception e) {
log.error("FTP清理远程目录异常 dir={}", remoteDir, e);
return R.fail("清理远程目录失败: " + e.getMessage());
} finally {
disconnectQuietly(ftpClient);
}
}
public static R<String> deleteRemoteDirectoryContents(FTPClient ftpClient, String remoteDir) {
if (ftpClient == null || !ftpClient.isConnected()) {
return R.fail("FTP连接不可用");
}
String dir = normalizeRemotePath(remoteDir);
try {
if (!changeWorkingDirectoryWithEncoding(ftpClient, dir)) {
log.info("FTP 远程目录不存在,跳过删除: {}", dir);
return R.ok("远程目录不存在,无需删除: " + dir);
}
int count = deleteListedContents(ftpClient, dir);
return R.ok("已清理远程出图目录,删除 " + count + " 项: " + dir);
} catch (Exception e) {
log.error("FTP清理远程目录异常 dir={}", dir, e);
return R.fail("清理远程目录失败: " + e.getMessage());
}
}
private static int downloadTree(FTPClient ftp, String remoteDir, String localDir) throws IOException {
if (!changeWorkingDirectoryWithEncoding(ftp, remoteDir)) {
throw new IOException("远程目录不存在: " + remoteDir);
}
File local = new File(localDir);
if (!local.exists() && !local.mkdirs()) {
throw new IOException("无法创建本地目录: " + localDir);
}
FTPFile[] files = ftp.listFiles();
if (files == null || files.length == 0) {
return 0;
}
int count = 0;
List<String> subDirs = new ArrayList<>();
for (FTPFile file : files) {
String name = file.getName();
if (name == null || ".".equals(name) || "..".equals(name)) {
continue;
}
if (file.isDirectory()) {
subDirs.add(name);
continue;
}
File localFile = new File(local, name);
try (OutputStream os = Files.newOutputStream(localFile.toPath())) {
if (retrieveFileWithEncoding(ftp, name, os)) {
count++;
log.info("FTP递归下载成功: {}/{}", remoteDir, name);
} else {
Files.deleteIfExists(localFile.toPath());
log.warn("FTP递归下载失败: {}/{} reply={}", remoteDir, name, ftp.getReplyString());
}
}
}
for (String name : subDirs) {
String childRemote = normalizeRemotePath(remoteDir + "/" + name);
String childLocal = new File(local, name).getAbsolutePath();
count += downloadTree(ftp, childRemote, childLocal);
}
return count;
}
private static int deleteListedContents(FTPClient ftp, String absDir) throws IOException {
if (!changeWorkingDirectoryWithEncoding(ftp, absDir)) {
return 0;
}
FTPFile[] files = ftp.listFiles();
if (files == null || files.length == 0) {
return 0;
}
int count = 0;
List<String> subDirs = new ArrayList<>();
List<String> fileNames = new ArrayList<>();
for (FTPFile file : files) {
String name = file.getName();
if (name == null || ".".equals(name) || "..".equals(name)) {
continue;
}
if (file.isDirectory()) {
subDirs.add(name);
} else {
fileNames.add(name);
}
}
for (String name : fileNames) {
if (deleteFileWithEncoding(ftp, name)) {
count++;
log.info("FTP已删除文件: {}/{}", absDir, name);
} else {
log.warn("FTP删除文件失败: {}/{} reply={}", absDir, name, ftp.getReplyString());
}
}
for (String name : subDirs) {
String child = normalizeRemotePath(absDir + "/" + name);
count += deleteListedContents(ftp, child);
changeWorkingDirectoryWithEncoding(ftp, absDir);
if (removeDirectoryWithEncoding(ftp, name)) {
count++;
log.info("FTP已删除目录: {}", child);
} else {
log.warn("FTP删除目录失败: {} reply={}", child, ftp.getReplyString());
}
}
return count;
}
private static boolean deleteFileWithEncoding(FTPClient ftpClient, String fileName) throws IOException {
if (ftpClient.deleteFile(fileName)) {
return true;
}
String gbkIso = new String(fileName.getBytes("GBK"), SERVER_CHARSET);
if (ftpClient.deleteFile(gbkIso)) {
return true;
}
String utf8Iso = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
return ftpClient.deleteFile(utf8Iso);
}
private static boolean removeDirectoryWithEncoding(FTPClient ftpClient, String dir) throws IOException {
if (ftpClient.removeDirectory(dir)) {
return true;
}
String gbkIso = new String(dir.getBytes("GBK"), SERVER_CHARSET);
if (ftpClient.removeDirectory(gbkIso)) {
return true;
}
String utf8Iso = new String(dir.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
return ftpClient.removeDirectory(utf8Iso);
}
private static String combineRemotePath(String basePath, String filePath) { private static String combineRemotePath(String basePath, String filePath) {
String base = basePath == null ? "" : basePath.trim(); String base = basePath == null ? "" : basePath.trim();
String rel = filePath == null ? "" : filePath.trim(); String rel = filePath == null ? "" : filePath.trim();
@ -417,6 +689,16 @@ public final class FtpUtil {
return ftpClient.retrieveFile(utf8Iso, os); return ftpClient.retrieveFile(utf8Iso, os);
} }
/**
* 目录扫描按文件名精确匹配。不能用 contains,否则「图纸数量统计表.xlsx」会命中其它令号的「令号图纸数量统计表.xlsx」。
*/
private static boolean fileNameMatchesFilter(String fileName, String fileNameFilter) {
if (fileName == null || fileNameFilter == null) {
return false;
}
return fileName.equalsIgnoreCase(fileNameFilter);
}
private static boolean changeWorkingDirectoryWithEncoding(FTPClient ftpClient, String remoteDir) private static boolean changeWorkingDirectoryWithEncoding(FTPClient ftpClient, String remoteDir)
throws IOException { throws IOException {
if (ftpClient.changeWorkingDirectory(remoteDir)) { if (ftpClient.changeWorkingDirectory(remoteDir)) {

View File

@ -16,7 +16,7 @@ import java.util.stream.Collectors;
* WC000002 机二工段 → 006 机加车间 * WC000002 机二工段 → 006 机加车间
* WC000004 装一工段 → 007 装配车间 * WC000004 装一工段 → 007 装配车间
* WC000005 装二工段 → 007 装配车间 * WC000005 装二工段 → 007 装配车间
* WC000006 电钳工段 → 007 装配车间 * WC000006 电钳工段 → 023 电钳车间
* WC000007 下料工段 → 006 机加车间 * WC000007 下料工段 → 006 机加车间
* WC000008 委外中心 → 006 机加车间 * WC000008 委外中心 → 006 机加车间
* WC000010 下料工段 → 018 铆焊车间 * WC000010 下料工段 → 018 铆焊车间
@ -31,16 +31,21 @@ public final class KingdeeRouteDepartmentHelper {
public static final String DEPT_ASSEMBLY = "007"; public static final String DEPT_ASSEMBLY = "007";
/** 铆焊车间 */ /** 铆焊车间 */
public static final String DEPT_RIVET_WELD = "018"; public static final String DEPT_RIVET_WELD = "018";
/** 电钳车间 */
public static final String DEPT_ELECTRICIAN = "023";
/** 006 机加车间 */ /** 006 机加车间 */
private static final Set<String> MACHINE_WORKSHOP = setOf( private static final Set<String> MACHINE_WORKSHOP = setOf(
"机一工段", "机二工段", "机加委外"); "机一工段", "机二工段", "机加委外");
/** 007 装配车间 */ /** 007 装配车间 */
private static final Set<String> ASSEMBLY_WORKSHOP = setOf( private static final Set<String> ASSEMBLY_WORKSHOP = setOf(
"装一工段", "装二工段", "电钳工段","装配委外"); "装一工段", "装二工段", "装配委外");
/** 018 铆焊车间 */ /** 018 铆焊车间 */
private static final Set<String> RIVET_WELD_WORKSHOP = setOf( private static final Set<String> RIVET_WELD_WORKSHOP = setOf(
"下料工段", "焊接工段","铆焊委外"); "下料工段", "焊接工段", "铆焊委外");
/** 023 电钳车间 */
private static final Set<String> ELECTRICIAN_WORKSHOP = setOf(
"电钳工段");
public static final List<String> ALLOWED_WORK_CENTERS; public static final List<String> ALLOWED_WORK_CENTERS;
static { static {
@ -48,6 +53,7 @@ public final class KingdeeRouteDepartmentHelper {
all.addAll(MACHINE_WORKSHOP); all.addAll(MACHINE_WORKSHOP);
all.addAll(ASSEMBLY_WORKSHOP); all.addAll(ASSEMBLY_WORKSHOP);
all.addAll(RIVET_WELD_WORKSHOP); all.addAll(RIVET_WELD_WORKSHOP);
all.addAll(ELECTRICIAN_WORKSHOP);
ALLOWED_WORK_CENTERS = Collections.unmodifiableList(Arrays.asList(all.toArray(new String[0]))); ALLOWED_WORK_CENTERS = Collections.unmodifiableList(Arrays.asList(all.toArray(new String[0])));
} }
@ -72,6 +78,9 @@ public final class KingdeeRouteDepartmentHelper {
if (RIVET_WELD_WORKSHOP.contains(wc)) { if (RIVET_WELD_WORKSHOP.contains(wc)) {
return DEPT_RIVET_WELD; return DEPT_RIVET_WELD;
} }
if (ELECTRICIAN_WORKSHOP.contains(wc)) {
return DEPT_ELECTRICIAN;
}
throw new ServiceException("不支持的工作中心「" + wc + "」,仅允许:" throw new ServiceException("不支持的工作中心「" + wc + "」,仅允许:"
+ String.join("、", ALLOWED_WORK_CENTERS)); + String.join("、", ALLOWED_WORK_CENTERS));
} }
@ -95,6 +104,9 @@ public final class KingdeeRouteDepartmentHelper {
if (DEPT_RIVET_WELD.equals(deptFNumber)) { if (DEPT_RIVET_WELD.equals(deptFNumber)) {
return "铆焊车间"; return "铆焊车间";
} }
if (DEPT_ELECTRICIAN.equals(deptFNumber)) {
return "电钳车间";
}
return deptFNumber != null ? deptFNumber : ""; return deptFNumber != null ? deptFNumber : "";
} }

View File

@ -8,10 +8,12 @@ import com.itextpdf.text.pdf.PdfStamper;
import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.multipdf.PDFMergerUtility;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -132,7 +134,7 @@ public class PDFDocHelper {
} }
/** /**
* 在 PDF 第 1 页指定位置叠加文字(无图章),经临时文件替换原文件。 * 在 PDF 第 1 页指定位置叠加文字(无图章),就地覆盖原文件。
*/ */
public static void stampTextFirstPage(String pdfPath, List<Map<String, ?>> reviews, float[] pswX, float[] pswY, int fontSize) public static void stampTextFirstPage(String pdfPath, List<Map<String, ?>> reviews, float[] pswX, float[] pswY, int fontSize)
throws IOException, DocumentException { throws IOException, DocumentException {
@ -140,23 +142,88 @@ public class PDFDocHelper {
} }
/** /**
* 在 PDF 第 1 页指定位置叠加文字(无图章),经临时文件替换原文件。 * 在 PDF 第 1 页指定位置叠加文字(无图章),就地覆盖原文件。
* *
* @param rotation 逆时针旋转角度(伊特左栏竖排一般为 90) * @param rotation 逆时针旋转角度(伊特左栏竖排一般为 90)
*/ */
public static void stampTextFirstPage(String pdfPath, List<Map<String, ?>> reviews, float[] pswX, float[] pswY, public static void stampTextFirstPage(String pdfPath, List<Map<String, ?>> reviews, float[] pswX, float[] pswY,
int fontSize, float rotation) throws IOException, DocumentException { int fontSize, float rotation) throws IOException, DocumentException {
if (reviews == null || reviews.isEmpty()) { File src = new File(pdfPath);
File result = stampTextFirstPage(src, readAllBytesWithRetry(src.toPath()), reviews, pswX, pswY, fontSize, rotation);
if (isSameFile(result, src)) {
return; return;
} }
try {
if (!writeBytesWithRetry(src.toPath(), Files.readAllBytes(result.toPath()), 20)) {
throw new IOException("无法覆盖原文件: " + src.getAbsolutePath());
}
} finally {
Files.deleteIfExists(result.toPath());
}
}
/**
* 内存盖章后写回 {@code dest}。若 Windows 锁住原文件,则写入系统临时文件并返回该文件,
* 调用方用返回值继续出图,避免左栏标注被跳过。
*
* @return 后续流程应使用的 PDF(覆盖成功为 dest,否则为临时文件)
*/
public static File stampTextFirstPage(File dest, byte[] pdfBytes, List<Map<String, ?>> reviews,
float[] pswX, float[] pswY, int fontSize, float rotation)
throws IOException, DocumentException {
if (reviews == null || reviews.isEmpty()) {
return dest;
}
if (pswX == null || pswY == null || pswX.length < reviews.size() || pswY.length < reviews.size()) { if (pswX == null || pswY == null || pswX.length < reviews.size() || pswY.length < reviews.size()) {
throw new IllegalArgumentException("reviews 与坐标数组长度不一致"); throw new IllegalArgumentException("reviews 与坐标数组长度不一致");
} }
File src = new File(pdfPath); byte[] stamped = stampFirstPageToBytes(pdfBytes, reviews, pswX, pswY, fontSize, rotation);
File temp = new File(src.getParentFile(), "_plm_stamp_" + src.getName()); if (writeBytesWithRetry(dest.toPath(), stamped, 20)) {
PdfReader reader = new PdfReader(pdfPath); return dest;
FileOutputStream fout = new FileOutputStream(temp); }
PdfStamper stp = new PdfStamper(reader, fout); File sidecar = Files.createTempFile("plm_stamp_", ".pdf").toFile();
Files.write(sidecar.toPath(), stamped);
return sidecar;
}
public static boolean copyOverWithRetry(File from, File to) {
if (from == null || to == null || !from.isFile()) {
return false;
}
try {
return writeBytesWithRetry(to.toPath(), readAllBytesWithRetry(from.toPath()), 10);
} catch (IOException e) {
return false;
}
}
public static byte[] readAllBytesWithRetry(Path src) throws IOException {
IOException last = null;
for (int i = 1; i <= 20; i++) {
try {
return Files.readAllBytes(src);
} catch (IOException e) {
last = e;
if (i == 20 || Thread.currentThread().isInterrupted()) {
throw e;
}
try {
Thread.sleep(Math.min(1000L, 50L * (1L << Math.min(i - 1, 4))));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw e;
}
}
}
throw last != null ? last : new IOException("读取文件失败: " + src);
}
private static byte[] stampFirstPageToBytes(byte[] pdfBytes, List<Map<String, ?>> reviews,
float[] pswX, float[] pswY, int fontSize, float rotation)
throws IOException, DocumentException {
PdfReader reader = new PdfReader(pdfBytes);
ByteArrayOutputStream bout = new ByteArrayOutputStream(Math.max(8192, pdfBytes.length));
PdfStamper stp = new PdfStamper(reader, bout);
try { try {
PdfContentByte content = stp.getOverContent(1); PdfContentByte content = stp.getOverContent(1);
content.beginText(); content.beginText();
@ -168,10 +235,42 @@ public class PDFDocHelper {
content.endText(); content.endText();
} finally { } finally {
stp.close(); stp.close();
fout.close();
reader.close();
} }
Files.move(temp.toPath(), src.toPath(), StandardCopyOption.REPLACE_EXISTING); return bout.toByteArray();
}
/**
* Windows 上刚复制完的文件常被杀毒/索引短暂锁住,写回时重试。
*/
private static boolean writeBytesWithRetry(Path dest, byte[] data, int maxAttempts) {
for (int i = 1; i <= maxAttempts; i++) {
try {
Files.write(dest, data);
return true;
} catch (IOException e) {
if (i == maxAttempts || Thread.currentThread().isInterrupted()) {
return false;
}
try {
Thread.sleep(Math.min(1000L, 50L * (1L << Math.min(i - 1, 4))));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return false;
}
}
}
return false;
}
private static boolean isSameFile(File a, File b) {
if (a == null || b == null) {
return false;
}
try {
return a.getCanonicalFile().equals(b.getCanonicalFile());
} catch (IOException e) {
return a.getAbsoluteFile().equals(b.getAbsoluteFile());
}
} }
/** /**
@ -235,11 +334,12 @@ public class PDFDocHelper {
List<Map<String, ?>> reviews, float[] pswX, float[] pswY) throws IOException, DocumentException { List<Map<String, ?>> reviews, float[] pswX, float[] pswY) throws IOException, DocumentException {
PdfReader reader = new PdfReader(inputPath); PdfReader reader = new PdfReader(inputPath);
// 临时输出文件名,防止冲突 // 临时文件放到系统临时目录,避免 _temp.pdf 残留在 zip 输出目录被一起打包
String tempName = name.replace(".pdf", "_temp.pdf"); String baseName = name.toLowerCase().endsWith(".pdf") ? name.substring(0, name.length() - 4) : name;
String tempPath = outputPath + "/" + tempName; File tempFile = Files.createTempFile("pdf-wm-" + sanitizeTempToken(baseName) + "-", ".pdf").toFile();
String tempPath = tempFile.getAbsolutePath();
FileOutputStream foc = new FileOutputStream(tempPath); FileOutputStream foc = new FileOutputStream(tempFile);
PdfStamper stamper = new PdfStamper(reader, foc); PdfStamper stamper = new PdfStamper(reader, foc);
PdfGState gs1 = new PdfGState(); PdfGState gs1 = new PdfGState();
@ -268,28 +368,50 @@ public class PDFDocHelper {
e.printStackTrace(); e.printStackTrace();
return false; return false;
} finally { } finally {
stamper.close(); try {
foc.close(); stamper.close();
reader.close(); } catch (Exception ignore) {
}
try {
foc.close();
} catch (Exception ignore) {
}
try {
reader.close();
} catch (Exception ignore) {
}
} }
// 第二次处理 —— 添加文字水印 // 第二次处理 —— 添加文字水印到正式输出文件
try { try {
signSinglePsw(outputPath, tempName, outputPath, name, reviews, pswX, pswY); File outDir = new File(outputPath);
if (!outDir.exists() && !outDir.mkdirs()) {
throw new IOException("无法创建输出目录: " + outputPath);
}
signSinglePsw(tempFile.getParent(), tempFile.getName(), outputPath, name, reviews, pswX, pswY);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return false; return false;
} } finally {
// 删除临时文件(失败不阻塞主流程)
// 删除临时文件 try {
File tempFile = new File(tempPath); Files.deleteIfExists(tempFile.toPath());
if (tempFile.exists()) { } catch (Exception ignore) {
tempFile.delete(); tempFile.deleteOnExit();
}
} }
return true; return true;
} }
private static String sanitizeTempToken(String name) {
if (name == null || name.isEmpty()) {
return "file";
}
String cleaned = name.replaceAll("[\\\\/:*?\"<>|\\s]+", "_");
return cleaned.length() > 40 ? cleaned.substring(0, 40) : cleaned;
}
/** /**
* pdf合并 * pdf合并
* @param files * @param files

View File

@ -28,6 +28,7 @@ import org.springframework.expression.BeanResolver;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext; import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.common.TemplateParserContext;
import org.springframework.util.ClassUtils;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
@ -161,7 +162,7 @@ public class PlusDataPermissionHandler {
int index = sb.lastIndexOf("."); int index = sb.lastIndexOf(".");
String clazzName = sb.substring(0, index); String clazzName = sb.substring(0, index);
String methodName = sb.substring(index + 1, sb.length()); String methodName = sb.substring(index + 1, sb.length());
Class<?> clazz = ClassUtil.loadClass(clazzName); Class<?> clazz = loadMapperClass(clazzName);
List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz)) List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
.filter(method -> method.getName().equals(methodName)).collect(Collectors.toList()); .filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
DataPermission dataPermission; DataPermission dataPermission;
@ -196,4 +197,15 @@ public class PlusDataPermissionHandler {
public boolean isInvalid(String mappedStatementId) { public boolean isInvalid(String mappedStatementId) {
return invalidCacheSet.contains(mappedStatementId); return invalidCacheSet.contains(mappedStatementId);
} }
/**
* ForkJoin 等异步线程的 contextClassLoader 可能找不到 Mapper,回退到 Spring 类加载器。
*/
private Class<?> loadMapperClass(String clazzName) {
try {
return ClassUtils.forName(clazzName, getClass().getClassLoader());
} catch (ClassNotFoundException e) {
return ClassUtil.loadClass(clazzName);
}
}
} }

View File

@ -12,18 +12,21 @@ import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveItemDTO;
import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveSummary; import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveSummary;
import com.ruoyi.system.service.IPlmReceiveLogService; import com.ruoyi.system.service.IPlmReceiveLogService;
import com.ruoyi.system.service.IProductionOrderService; import com.ruoyi.system.service.IProductionOrderService;
import com.ruoyi.system.service.PlmPlanCompleteNotifyService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
/** /**
* PLM 总装配 BOM 推送接收入口(POST JSON 数组)。 * PLM 总装配 BOM 推送接收入口(POST JSON 数组)。
@ -39,9 +42,10 @@ public class AssemblyBomReceiveController extends BaseController {
private final IProductionOrderService productionOrderService; private final IProductionOrderService productionOrderService;
private final IPlmReceiveLogService plmReceiveLogService; private final IPlmReceiveLogService plmReceiveLogService;
private final PlmPlanCompleteNotifyService plmPlanCompleteNotifyService;
/** /**
* 接收总装配 BOM 行列表,按生产令号全量替换写入 production_order,并同步 bom_details(bomType=0)。 * 接收总装配 BOM 行列表。已审核且主产品图号相同则拒绝;主产品图号不同则追加。
* <p> * <p>
* POST /system/plm/assemblyBomReceive<br> * POST /system/plm/assemblyBomReceive<br>
* Content-Type: application/json; charset=UTF-8 * Content-Type: application/json; charset=UTF-8
@ -60,6 +64,7 @@ public class AssemblyBomReceiveController extends BaseController {
summaryMap.put("lineCount", summary.getLineCount()); summaryMap.put("lineCount", summary.getLineCount());
summaryMap.put("insertedCount", summary.getInsertedCount()); summaryMap.put("insertedCount", summary.getInsertedCount());
summaryMap.put("replaced", summary.isReplaced()); summaryMap.put("replaced", summary.isReplaced());
summaryMap.put("appended", summary.isAppended());
plmReceiveLogService.record( plmReceiveLogService.record(
IPlmReceiveLogService.BIZ_ASSEMBLY_BOM, IPlmReceiveLogService.BIZ_ASSEMBLY_BOM,
API_PATH, API_PATH,
@ -73,6 +78,10 @@ public class AssemblyBomReceiveController extends BaseController {
null, null,
clientIp clientIp
); );
// 工艺可能先到、总装后到:入库后再核实两边都有才发给 plan-user-id
final String productionOrder = summary.getProductionOrderNo();
CompletableFuture.runAsync(() ->
plmPlanCompleteNotifyService.notifyIfAssemblyAndRouteComplete(productionOrder, null));
return R.ok("接收成功", summary); return R.ok("接收成功", summary);
} catch (Exception e) { } catch (Exception e) {
plmReceiveLogService.record( plmReceiveLogService.record(
@ -92,6 +101,20 @@ public class AssemblyBomReceiveController extends BaseController {
} }
} }
/**
* 总装 BOM 与工艺都已入库后,按令号重发生管通知(工艺计划表 + Markdown)。
* 两边缺一则只返回原因,不发钉钉。
* <p>
* POST /system/plm/planCompleteNotify?productionOrderNo=令号
*/
@SaIgnore
@Log(title = "PLM工艺完成通知重发", businessType = BusinessType.OTHER)
@PostMapping("/planCompleteNotify")
public R<String> planCompleteNotify(@RequestParam("productionOrderNo") String productionOrderNo) {
String result = plmPlanCompleteNotifyService.notifyIfAssemblyAndRouteComplete(productionOrderNo, null);
return R.ok(result);
}
private static String resolveOrderNo(List<AssemblyBomReceiveItemDTO> body) { private static String resolveOrderNo(List<AssemblyBomReceiveItemDTO> body) {
if (body == null) { if (body == null) {
return null; return null;

View File

@ -22,6 +22,8 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.validate.AddGroup; import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.JsyyOrderNo;
import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ProcessRoute; import com.ruoyi.system.domain.ProcessRoute;
import com.ruoyi.system.domain.dto.*; import com.ruoyi.system.domain.dto.*;
@ -96,6 +98,9 @@ public class BomDetailsController extends BaseController {
private final DingTalkProperties dingTalkProperties; private final DingTalkProperties dingTalkProperties;
/** 电气 BOM 导入成功后发「无需做工艺」钉钉,并写入推送日志 */
private final SkipProcessNotifyService skipProcessNotifyService;
/** /**
* 查询bom明细列表 * 查询bom明细列表
*/ */
@ -437,6 +442,156 @@ public class BomDetailsController extends BaseController {
|| "PLM".equalsIgnoreCase(material.getUpdateBy()); || "PLM".equalsIgnoreCase(material.getUpdateBy());
} }
/**
* 电气 Excel BOM:bomType=1,且不是 PLM 项目、也不是 PLM 推送的材料 BOM。
*/
private static boolean isElectricalExcelBom(BomDetails material, ProcessOrderPro project) {
if (material == null || !"1".equals(material.getBomType())) {
return false;
}
if (project != null && ProcessOrderProSource.PLM.equals(project.getSource())) {
return false;
}
return !"PLM".equalsIgnoreCase(material.getCreateBy())
&& !"PLM".equalsIgnoreCase(material.getUpdateBy());
}
private static boolean containsElectricalExcelBom(List<BomDetails> materials, ProcessOrderPro project) {
if (materials == null) {
return false;
}
for (BomDetails material : materials) {
if (isElectricalExcelBom(material, project)) {
return true;
}
}
return false;
}
private static String firstNonBlankTrim(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (StringUtils.isNotBlank(value)) {
return value.trim();
}
}
return null;
}
/**
* 单台数量:{@code 1/4}、{@code 1/3} 拆成分子和分母;普通数字分母为 1。
*/
private static void applyElectricalQuantity(BomDetails bomDetails, String rawQuantity) {
bomDetails.setDenominator(1.0);
if (StringUtils.isBlank(rawQuantity)) {
bomDetails.setQuantity(null);
return;
}
String text = rawQuantity.trim().replace('/', '/');
int slash = text.indexOf('/');
if (slash > 0 && slash == text.lastIndexOf('/')) {
String numeratorText = text.substring(0, slash).trim();
String denominatorText = text.substring(slash + 1).trim();
Double numerator = parsePositiveNumber(numeratorText);
Double denominator = parsePositiveNumber(denominatorText);
if (numerator != null && denominator != null && denominator > 0) {
bomDetails.setQuantity(formatQuantityNumber(numerator));
bomDetails.setDenominator(denominator);
return;
}
}
Double number = parsePositiveNumber(text);
bomDetails.setQuantity(number == null ? text : formatQuantityNumber(number));
}
private static Double parsePositiveNumber(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
try {
double value = Double.parseDouble(text);
if (value < 0 || Double.isInfinite(value) || Double.isNaN(value)) {
return null;
}
return value;
} catch (NumberFormatException e) {
return null;
}
}
private static String formatQuantityNumber(double value) {
if (value == Math.rint(value) && value <= Long.MAX_VALUE) {
return String.valueOf((long) value);
}
return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
}
/** 电气 Excel「型号/规格型号」入库在 material;仅电气建档写入金蝶 FSpecification */
private static String resolveElectricalSpecification(BomDetails details) {
return details == null ? null : StringUtils.trim(details.getMaterial());
}
private static void writeKingdeeElectricalSpecification(JsonObject model, BomDetails details) {
String spec = resolveElectricalSpecification(details);
if (StringUtils.isNotBlank(spec)) {
model.addProperty("FSpecification", spec);
log.info("[电气建档] 写入规格型号 FNumber={} FSpecification={}",
details.getPartNumber(), spec);
} else {
log.warn("[电气建档] 规格型号为空,未写入 FSpecification, FNumber={}",
details == null ? null : details.getPartNumber());
}
}
/** 电气单位在 remarks,不能按品牌 wareHouse 去 switch(品牌为空会 NPE,建档失败规格也进不去) */
private static void fillElectricalKingdeeUnit(JsonObject target, String unitText) {
if (target == null || StringUtils.isBlank(unitText)) {
return;
}
switch (unitText.trim()) {
case "把":
target.addProperty("FNumber", "002");
break;
case "包":
target.addProperty("FNumber", "012");
break;
case "个":
target.addProperty("FNumber", "001");
break;
case "根":
target.addProperty("FNumber", "003");
break;
case "米":
target.addProperty("FNumber", "005");
break;
case "卷":
target.addProperty("FNumber", "020");
break;
case "盒":
target.addProperty("FNumber", "014");
break;
case "件":
target.addProperty("FNumber", "jian");
break;
case "盘":
target.addProperty("FNumber", "011");
break;
case "瓶":
target.addProperty("FNumber", "016");
break;
case "条":
target.addProperty("FNumber", "017");
break;
case "套":
target.addProperty("FNumber", "007");
break;
default:
break;
}
}
/* 触发金蝶物料清单按钮 */ /* 触发金蝶物料清单按钮 */
@Log(title = "金蝶物料清单更新") @Log(title = "金蝶物料清单更新")
@SaCheckPermission("system:details:updateMaterial") @SaCheckPermission("system:details:updateMaterial")
@ -473,9 +628,27 @@ public class BomDetailsController extends BaseController {
if (bomDetailParams == null || bomDetailParams.isEmpty()) { if (bomDetailParams == null || bomDetailParams.isEmpty()) {
return R.fail("请先选择要上传的物料"); return R.fail("请先选择要上传的物料");
} }
for (Map<String, String> param : bomDetailParams) {
String orderNo = param == null ? null : param.get("totalWeight");
String jsyyDeny = JsyyOrderNo.denyIfMatches(orderNo);
if (jsyyDeny != null) {
return R.fail(jsyyDeny);
}
if (StringUtils.isNotBlank(orderNo)) {
ProcessOrderPro orderForLock = iProcessOrderProService.selectByProjectNumber(orderNo);
if (orderForLock != null) {
String statusDeny = ProjectStatusLock.denyIfLocked(
orderForLock.getBomStatus(), ProjectStatusLock.Action.UPDATE_MATERIAL);
if (statusDeny != null) {
return R.fail(statusDeny);
}
}
}
}
List<BomDetails> bomDetailsList = new ArrayList<>(); List<BomDetails> bomDetailsList = new ArrayList<>();
List<KindegeeLogDTO> logDTOS = new ArrayList<>(); List<KindegeeLogDTO> logDTOS = new ArrayList<>();
List<String> failedMaterials = new ArrayList<>(); // 用于跟踪处理失败的物料 List<String> failedMaterials = new ArrayList<>(); // 用于跟踪处理失败的物料
Set<String> electricalOrderNos = new LinkedHashSet<>();
String totalWeight = ""; String totalWeight = "";
// 遍历前端传来的数据 // 遍历前端传来的数据
for (Map<String, String> param : bomDetailParams) { for (Map<String, String> param : bomDetailParams) {
@ -505,6 +678,9 @@ public class BomDetailsController extends BaseController {
failedMaterials.add(fnumber); failedMaterials.add(fnumber);
continue; continue;
} }
if (containsElectricalExcelBom(bomDetails, bo)) {
electricalOrderNos.add(bo.getProductionOrderNo());
}
// 第一步:处理所有物料的保存 // 第一步:处理所有物料的保存
for (BomDetails material : bomDetails) { for (BomDetails material : bomDetails) {
// 获取工艺表中的非委外工时 // 获取工艺表中的非委外工时
@ -513,7 +689,7 @@ public class BomDetailsController extends BaseController {
if (material.getPartNumber().startsWith("015") || material.getPartNumber().startsWith("009")) { if (material.getPartNumber().startsWith("015") || material.getPartNumber().startsWith("009")) {
return R.fail("015和009为管控物料,请先申请"); return R.fail("015和009为管控物料,请先申请");
} }
if (material.getRemarks() != null && material.getRemarks().contains("甲供")) { if (isOwnerSuppliedBom(material)) {
continue; continue;
} }
String state = determineState(material); String state = determineState(material);
@ -537,41 +713,46 @@ public class BomDetailsController extends BaseController {
} }
} }
// 第二步:BOM校验和上传(在循环外部) // 第二步:BOM校验和上传(过滤甲供件,不进金蝶 BOM)
ValidateBomResult validateResult = validateBOM(fnumber, bomDetails); List<BomDetails> uploadBomDetails = filterOwnerSuppliedFromBom(bomDetails);
boolean needUpload = !validateResult.isMatch(); if (uploadBomDetails.isEmpty()) {
if (needUpload) { log.info("过滤甲供件后无子项,跳过金蝶BOM上传, 物料编码: {}", fnumber);
try {
// 物料清单保存方法,判断是电气还是生产
BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(bomDetails, bo, groupName);
if (bomUploadResult.isSuccess()) {
KindegeeLogDTO logDTO = new KindegeeLogDTO();
logDTO.setProjectCode(bo.getProductionOrderNo());
logDTO.setMaterialCode(fnumber); // 使用物料编码而不是单个物料的编码
logDTO.setCode("200");
logDTO.setReason("成功上传" + "版本号:" + bomUploadResult.getNumber());
logDTOS.add(logDTO);
} else {
KindegeeLogDTO logDTO = new KindegeeLogDTO();
logDTO.setProjectCode(bo.getProductionOrderNo());
logDTO.setMaterialCode(fnumber);
logDTO.setCode("300");
logDTO.setReason(bomUploadResult.getErrorMessage());
logDTOS.add(logDTO);
}
bomDetailsList.addAll(bomDetails);
} catch (Exception e) {
log.error("保存BOM失败, 物料编码: {}, 错误: {}", fnumber, e.getMessage());
failedMaterials.add(fnumber);
}
} else { } else {
KindegeeLogDTO logDTO = new KindegeeLogDTO(); ValidateBomResult validateResult = validateBOM(fnumber, uploadBomDetails);
logDTO.setProjectCode(bo.getProductionOrderNo()); boolean needUpload = !validateResult.isMatch();
logDTO.setMaterialCode(fnumber); if (needUpload) {
logDTO.setCode("100"); try {
logDTO.setReason("BOM已存在且一致,版本号:" + validateResult.getVersion()); // 物料清单保存方法,判断是电气还是生产
logDTOS.add(logDTO); BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(uploadBomDetails, bo, groupName);
log.info("BOM已存在且一致,物料编码: {},跳过保存", fnumber); if (bomUploadResult.isSuccess()) {
KindegeeLogDTO logDTO = new KindegeeLogDTO();
logDTO.setProjectCode(bo.getProductionOrderNo());
logDTO.setMaterialCode(fnumber); // 使用物料编码而不是单个物料的编码
logDTO.setCode("200");
logDTO.setReason("成功上传" + "版本号:" + bomUploadResult.getNumber());
logDTOS.add(logDTO);
} else {
KindegeeLogDTO logDTO = new KindegeeLogDTO();
logDTO.setProjectCode(bo.getProductionOrderNo());
logDTO.setMaterialCode(fnumber);
logDTO.setCode("300");
logDTO.setReason(bomUploadResult.getErrorMessage());
logDTOS.add(logDTO);
}
bomDetailsList.addAll(bomDetails);
} catch (Exception e) {
log.error("保存BOM失败, 物料编码: {}, 错误: {}", fnumber, e.getMessage());
failedMaterials.add(fnumber);
}
} else {
KindegeeLogDTO logDTO = new KindegeeLogDTO();
logDTO.setProjectCode(bo.getProductionOrderNo());
logDTO.setMaterialCode(fnumber);
logDTO.setCode("100");
logDTO.setReason("BOM已存在且一致,版本号:" + validateResult.getVersion());
logDTOS.add(logDTO);
log.info("BOM已存在且一致,物料编码: {},跳过保存", fnumber);
}
} }
} }
} }
@ -584,7 +765,17 @@ public class BomDetailsController extends BaseController {
return R.fail("未找到生产令号对应的项目: " + totalWeight); return R.fail("未找到生产令号对应的项目: " + totalWeight);
} }
processOrderProBo.setDrawingType(JSONUtil.toJsonStr(logDTOS)); processOrderProBo.setDrawingType(JSONUtil.toJsonStr(logDTOS));
processOrderProBo.setBomStatus(2L); boolean electricalProject = electricalOrderNos.contains(processOrderProBo.getProductionOrderNo());
boolean productionDone = processOrderProBo.getBomStatus() != null
&& processOrderProBo.getBomStatus() == ProjectStatusLock.PRODUCTION_DONE;
if (electricalProject) {
// 电气项目:物料清单更新成功后进入开始生产;已生产完成不回退
if (!productionDone) {
processOrderProBo.setBomStatus(ProjectStatusLock.IN_PRODUCTION);
}
} else {
processOrderProBo.setBomStatus(2L);
}
processOrderProMapper.updateById(processOrderProBo); processOrderProMapper.updateById(processOrderProBo);
// 更新钉钉卡片数据 // 更新钉钉卡片数据
@ -633,7 +824,12 @@ public class BomDetailsController extends BaseController {
} }
// 返回处理结果 // 返回处理结果
return R.ok("成功", bomDetailsList); String resultMsg = electricalOrderNos.contains(processOrderProBo.getProductionOrderNo())
&& (processOrderProBo.getBomStatus() != null
&& processOrderProBo.getBomStatus() == ProjectStatusLock.IN_PRODUCTION)
? "成功,电气项目状态已改为开始生产"
: "成功";
return R.ok(resultMsg, bomDetailsList);
} }
@ -825,6 +1021,10 @@ public class BomDetailsController extends BaseController {
model.add("FTreeEntity", fTreeEntity); model.add("FTreeEntity", fTreeEntity);
ArrayList<JsonObject> fTreeEntityList = new ArrayList<>(); ArrayList<JsonObject> fTreeEntityList = new ArrayList<>();
for (BomDetails details : bomlist) { for (BomDetails details : bomlist) {
if (isOwnerSuppliedBom(details)) {
log.info("跳过甲供件,不写入金蝶BOM: {}", details.getPartNumber());
continue;
}
// 创建FTreeEntity对象,并加入FTreeEntity数组 // 创建FTreeEntity对象,并加入FTreeEntity数组
JsonObject fTreeEntityItem = new JsonObject(); JsonObject fTreeEntityItem = new JsonObject();
fTreeEntity.add(fTreeEntityItem); fTreeEntity.add(fTreeEntityItem);
@ -854,6 +1054,10 @@ public class BomDetailsController extends BaseController {
fTreeEntityItem.addProperty("FDENOMINATOR", details.getDenominator()); fTreeEntityItem.addProperty("FDENOMINATOR", details.getDenominator());
fTreeEntityList.add(fTreeEntityItem); fTreeEntityList.add(fTreeEntityItem);
} }
if (fTreeEntity.size() == 0) {
log.warn("过滤甲供件后无子项,跳过金蝶BOM保存, 父项: {}", bomDetails.getFNumber());
return 1;
}
String jsonData = json.toString(); String jsonData = json.toString();
log.debug("打印json:" + jsonData); log.debug("打印json:" + jsonData);
System.out.println(jsonData); System.out.println(jsonData);
@ -947,6 +1151,10 @@ public class BomDetailsController extends BaseController {
if (bomlist.isEmpty()) { if (bomlist.isEmpty()) {
return null; return null;
} }
if (isOwnerSuppliedBom(details)) {
log.info("跳过甲供件,不写入金蝶BOM: {}", details.getPartNumber());
continue;
}
// 创建FTreeEntity对象,并加入FTreeEntity数组 // 创建FTreeEntity对象,并加入FTreeEntity数组
JsonObject fTreeEntityItem = new JsonObject(); JsonObject fTreeEntityItem = new JsonObject();
fTreeEntity.add(fTreeEntityItem); fTreeEntity.add(fTreeEntityItem);
@ -1006,6 +1214,10 @@ public class BomDetailsController extends BaseController {
} }
fTreeEntityList.add(fTreeEntityItem); fTreeEntityList.add(fTreeEntityItem);
} }
if (fTreeEntity.size() == 0) {
log.warn("过滤甲供件后无子项,跳过金蝶BOM保存, 图号={}", bomDetails1.getFNumber());
return BOMUploadResult.success(null, "SKIP_OWNER_SUPPLY");
}
try { try {
String resultJson = new K3CloudApi().save("ENG_BOM", json.toString()); String resultJson = new K3CloudApi().save("ENG_BOM", json.toString());
// 直接用 Jackson 解析 // 直接用 Jackson 解析
@ -1049,6 +1261,33 @@ public class BomDetailsController extends BaseController {
return StringUtils.isNotBlank(text) && ("伊特".equals(text.trim()) || text.contains("伊特")); return StringUtils.isNotBlank(text) && ("伊特".equals(text.trim()) || text.contains("伊特"));
} }
/** 备注或材质含「甲供」视为甲供件,不进入金蝶 BOM */
private static boolean isOwnerSuppliedBom(BomDetails details) {
if (details == null) {
return false;
}
return containsOwnerSupply(details.getRemarks()) || containsOwnerSupply(details.getMaterial());
}
private static boolean containsOwnerSupply(String text) {
return StringUtils.isNotBlank(text) && text.contains("甲供");
}
/** 生成/校验/上传金蝶 BOM 前过滤甲供件 */
private static List<BomDetails> filterOwnerSuppliedFromBom(List<BomDetails> bomDetails) {
if (bomDetails == null || bomDetails.isEmpty()) {
return Collections.emptyList();
}
List<BomDetails> filtered = new ArrayList<>(bomDetails.size());
for (BomDetails detail : bomDetails) {
if (isOwnerSuppliedBom(detail)) {
continue;
}
filtered.add(detail);
}
return filtered;
}
/* /*
* 父级物料保存接口 * 父级物料保存接口
* 单位 是台 * 单位 是台
@ -1955,7 +2194,8 @@ public class BomDetailsController extends BaseController {
// 添加Model字段 // 添加Model字段
model.addProperty("FMATERIALID", 0); model.addProperty("FMATERIALID", 0);
model.addProperty("FSpecification", bomDetails1.getMaterial());// 规格 为存入的材质字段 // 电气规格型号:Excel「型号/规格型号」入库 material;生产建档仍走 loadMaterialPreservation,互不影响
writeKingdeeElectricalSpecification(model, bomDetails1);
model.addProperty("F_HBYT_PP", bomDetails1.getWareHouse());// 品牌 为存入的仓库字段 model.addProperty("F_HBYT_PP", bomDetails1.getWareHouse());// 品牌 为存入的仓库字段
writeKingdeeChildDanZhong(model, bomDetails1, "电气子项物料建档"); writeKingdeeChildDanZhong(model, bomDetails1, "电气子项物料建档");
model.addProperty("FNumber", bomDetails1.getPartNumber()); model.addProperty("FNumber", bomDetails1.getPartNumber());
@ -2006,46 +2246,10 @@ public class BomDetailsController extends BaseController {
// 创建FBaseUnitId对象,并加入SubHeadEntity // 创建FBaseUnitId对象,并加入SubHeadEntity
JsonObject fBaseUnitId = new JsonObject(); JsonObject fBaseUnitId = new JsonObject();
switch (bomDetails1.getWareHouse()) {// 单位 String electricalUnit = StringUtils.trim(bomDetails1.getRemarks());
case "把": fillElectricalKingdeeUnit(fBaseUnitId, electricalUnit);
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity.add("FBaseUnitId", fBaseUnitId); subHeadEntity.add("FBaseUnitId", fBaseUnitId);
if (states.equals("1")) { if ("1".equals(states)) {
subHeadEntity.addProperty("FIsPurchase", true); subHeadEntity.addProperty("FIsPurchase", true);
subHeadEntity.addProperty("FIsSale", true); subHeadEntity.addProperty("FIsSale", true);
subHeadEntity.addProperty("FIsInventory", true); subHeadEntity.addProperty("FIsInventory", true);
@ -2062,44 +2266,7 @@ public class BomDetailsController extends BaseController {
JsonObject subHeadEntity1 = new JsonObject(); JsonObject subHeadEntity1 = new JsonObject();
model.add("SubHeadEntity1", subHeadEntity1); model.add("SubHeadEntity1", subHeadEntity1);
JsonObject fStoreUnitId = new JsonObject(); JsonObject fStoreUnitId = new JsonObject();
switch (bomDetails1.getWareHouse()) { fillElectricalKingdeeUnit(fStoreUnitId, electricalUnit);
case "把":
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity1.add("FStoreUnitID", fStoreUnitId); subHeadEntity1.add("FStoreUnitID", fStoreUnitId);
subHeadEntity1.addProperty("FUnitConvertDir", "1"); subHeadEntity1.addProperty("FUnitConvertDir", "1");
@ -2120,44 +2287,7 @@ public class BomDetailsController extends BaseController {
subHeadEntity1.addProperty("FBoxStandardQty", 0.0); subHeadEntity1.addProperty("FBoxStandardQty", 0.0);
// 创建FPurchaseUnitId对象,并加入SubHeadEntity1 // 创建FPurchaseUnitId对象,并加入SubHeadEntity1
JsonObject fPurchaseUnitId = new JsonObject(); JsonObject fPurchaseUnitId = new JsonObject();
switch (bomDetails1.getWareHouse()) { fillElectricalKingdeeUnit(fPurchaseUnitId, electricalUnit);
case "把":
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity1.add("FPurchaseUnitId", fPurchaseUnitId); subHeadEntity1.add("FPurchaseUnitId", fPurchaseUnitId);
// 创建SubHeadEntity3对象,并加入Model // 创建SubHeadEntity3对象,并加入Model
@ -2166,44 +2296,7 @@ public class BomDetailsController extends BaseController {
// 创建FPurchasePriceUnitId对象,并加入SubHeadEntity3 // 创建FPurchasePriceUnitId对象,并加入SubHeadEntity3
JsonObject fPurchasePriceUnitId = new JsonObject(); JsonObject fPurchasePriceUnitId = new JsonObject();
switch (bomDetails1.getWareHouse()) { fillElectricalKingdeeUnit(fPurchasePriceUnitId, electricalUnit);
case "把":
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity3.add("FPurchasePriceUnitId", fPurchasePriceUnitId); subHeadEntity3.add("FPurchasePriceUnitId", fPurchasePriceUnitId);
subHeadEntity3.addProperty("FIsQuota", false); subHeadEntity3.addProperty("FIsQuota", false);
@ -2212,18 +2305,18 @@ public class BomDetailsController extends BaseController {
JsonObject subHeadEntity6 = new JsonObject(); JsonObject subHeadEntity6 = new JsonObject();
model.add("SubHeadEntity6", subHeadEntity6); model.add("SubHeadEntity6", subHeadEntity6);
// 不同的属性不同的检验方案 // 不同的属性不同的检验方案
if (states.equals("1")) { if ("1".equals(states)) {
// 外购 // 外购
subHeadEntity6.addProperty("FCheckIncoming", true); subHeadEntity6.addProperty("FCheckIncoming", true);
subHeadEntity6.addProperty("FCheckStock", true); subHeadEntity6.addProperty("FCheckStock", true);
} }
if (states.equals("2")) { if ("2".equals(states)) {
// 自制 // 自制
subHeadEntity6.addProperty("FCheckProduct", true); subHeadEntity6.addProperty("FCheckProduct", true);
subHeadEntity6.addProperty("FCheckStock", true); subHeadEntity6.addProperty("FCheckStock", true);
subHeadEntity6.addProperty("FCheckReturnMtrl", true); subHeadEntity6.addProperty("FCheckReturnMtrl", true);
} }
if (states.equals("3")) { if ("3".equals(states)) {
// 委外 // 委外
subHeadEntity6.addProperty("FCheckIncoming", true); subHeadEntity6.addProperty("FCheckIncoming", true);
subHeadEntity6.addProperty("FCheckProduct", true); subHeadEntity6.addProperty("FCheckProduct", true);
@ -2236,44 +2329,7 @@ public class BomDetailsController extends BaseController {
// 创建FProduceUnitId对象,并加入SubHeadEntity5 // 创建FProduceUnitId对象,并加入SubHeadEntity5
JsonObject fProduceUnitId = new JsonObject(); JsonObject fProduceUnitId = new JsonObject();
switch (bomDetails1.getWareHouse()) { fillElectricalKingdeeUnit(fProduceUnitId, electricalUnit);
case "把":
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity5.add("FProduceUnitId", fProduceUnitId); subHeadEntity5.add("FProduceUnitId", fProduceUnitId);
// 创建FProduceBillType对象,并加入SubHeadEntity5 // 创建FProduceBillType对象,并加入SubHeadEntity5
@ -2283,46 +2339,9 @@ public class BomDetailsController extends BaseController {
// 创建FBOMUnitId对象,并加入SubHeadEntity5 // 创建FBOMUnitId对象,并加入SubHeadEntity5
JsonObject fBOMUnitId = new JsonObject(); JsonObject fBOMUnitId = new JsonObject();
switch (bomDetails1.getRemarks()) { fillElectricalKingdeeUnit(fBOMUnitId, electricalUnit);
case "把":
fBaseUnitId.addProperty("FNumber", "002");
break;
case "包":
fBaseUnitId.addProperty("FNumber", "012");
break;
case "个":
fBaseUnitId.addProperty("FNumber", "001");
break;
case "根":
fBaseUnitId.addProperty("FNumber", "003");
break;
case "米":
fBaseUnitId.addProperty("FNumber", "005");
break;
case "卷":
fBaseUnitId.addProperty("FNumber", "020");
break;
case "盒":
fBaseUnitId.addProperty("FNumber", "014");
break;
case "件":
fBaseUnitId.addProperty("FNumber", "jian");
break;
case "盘":
fBaseUnitId.addProperty("FNumber", "011");
break;
case "瓶":
fBaseUnitId.addProperty("FNumber", "016");
break;
case "条":
fBaseUnitId.addProperty("FNumber", "017");
break;
case "套":
fBaseUnitId.addProperty("FNumber", "007");
break;
}
subHeadEntity5.add("FBOMUnitId", fBOMUnitId); subHeadEntity5.add("FBOMUnitId", fBOMUnitId);
subHeadEntity5.addProperty("FIsMainPrd", !states.equals("1")); subHeadEntity5.addProperty("FIsMainPrd", !"1".equals(states));
subHeadEntity5.addProperty("FIssueType", "1"); subHeadEntity5.addProperty("FIssueType", "1");
// 创建FPickStockId对象,并加入SubHeadEntity5 // 创建FPickStockId对象,并加入SubHeadEntity5
@ -2382,6 +2401,13 @@ public class BomDetailsController extends BaseController {
} }
} }
@Log(title = "电气BOM导入模板", businessType = BusinessType.EXPORT)
@SaCheckPermission("system:details:importElectricalBom")
@PostMapping("/importElectricalBomTemplate")
public void importElectricalBomTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(), "电气BOM", ElectricalMaterialBomVO.class, response);
}
/** /**
* 导入BOM * 导入BOM
* daorubaon * daorubaon
@ -2392,8 +2418,9 @@ public class BomDetailsController extends BaseController {
@Log(title = "导入电气bom", businessType = BusinessType.IMPORT) @Log(title = "导入电气bom", businessType = BusinessType.IMPORT)
@SaCheckPermission("system:details:importElectricalBom") @SaCheckPermission("system:details:importElectricalBom")
@PostMapping(value = "/importElectricalBom", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/importElectricalBom", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> importElectricalBom(@RequestPart("file") MultipartFile file) { public R<Map<String, Object>> importElectricalBom(
Map<String, Object> resp = new HashMap<>(); @RequestPart("file") MultipartFile file,
@RequestParam(value = "productionOrderNo", required = false) String productionOrderNo) {
try { try {
if (file == null || file.isEmpty() || StringUtils.isEmpty(file.getOriginalFilename())) { if (file == null || file.isEmpty() || StringUtils.isEmpty(file.getOriginalFilename())) {
return R.fail("请选择有效的Excel文件"); return R.fail("请选择有效的Excel文件");
@ -2404,12 +2431,37 @@ public class BomDetailsController extends BaseController {
return R.fail("仅支持Excel文件(xls/xlsx)"); return R.fail("仅支持Excel文件(xls/xlsx)");
} }
List<ElectricalMaterialBomVO> rows = ExcelUtil.importExcel(file.getInputStream(), ElectricalMaterialBomVO.class); List<ElectricalMaterialBomVO> rows;
try {
rows = ExcelUtil.importExcel(file.getInputStream(), ElectricalMaterialBomVO.class);
} catch (Exception parseEx) {
log.error("电气BOM Excel解析失败: {}", filename, parseEx);
return R.fail("Excel解析失败,请检查表头是否包含:生产令号、物料编码、物料名称、上级部件、上级部件图号");
}
int total = rows != null ? rows.size() : 0; int total = rows != null ? rows.size() : 0;
int skippedLingYong = 0;
int skippedEmpty = 0;
List<BomDetails> list = new ArrayList<>(); List<BomDetails> list = new ArrayList<>();
Set<String> orderNos = new LinkedHashSet<>();
if (StringUtils.isNotBlank(productionOrderNo)) {
orderNos.add(productionOrderNo.trim());
}
if (rows != null) { if (rows != null) {
for (ElectricalMaterialBomVO bomVO : rows) { for (ElectricalMaterialBomVO bomVO : rows) {
if (bomVO == null) {
skippedEmpty++;
continue;
}
if (StringUtils.isNotBlank(bomVO.getProductionOrderNo())) {
orderNos.add(bomVO.getProductionOrderNo().trim());
}
if (StringUtils.isBlank(bomVO.getDrawingNo()) && StringUtils.isBlank(bomVO.getDrawingName())) {
skippedEmpty++;
continue;
}
if (bomVO.getRemark() != null && bomVO.getRemark().contains("领用")) { if (bomVO.getRemark() != null && bomVO.getRemark().contains("领用")) {
skippedLingYong++;
continue; continue;
} }
BomDetails bomDetails = new BomDetails(); BomDetails bomDetails = new BomDetails();
@ -2418,7 +2470,8 @@ public class BomDetailsController extends BaseController {
bomDetails.setFName(bomVO.getParentPart()); bomDetails.setFName(bomVO.getParentPart());
bomDetails.setPartNumber(bomVO.getDrawingNo()); bomDetails.setPartNumber(bomVO.getDrawingNo());
bomDetails.setName(bomVO.getDrawingName()); bomDetails.setName(bomVO.getDrawingName());
bomDetails.setMaterial(bomVO.getModel()); String specification = firstNonBlankTrim(bomVO.getModel(), bomVO.getSpecificationModel());
bomDetails.setMaterial(specification);
bomDetails.setWareHouse(bomVO.getBrand()); bomDetails.setWareHouse(bomVO.getBrand());
String brand = bomVO.getBrand(); String brand = bomVO.getBrand();
if (brand != null && brand.contains("伊特")) { if (brand != null && brand.contains("伊特")) {
@ -2426,10 +2479,9 @@ public class BomDetailsController extends BaseController {
} else { } else {
bomDetails.setStats("外购"); bomDetails.setStats("外购");
} }
bomDetails.setQuantity(bomVO.getQuantity() == null ? null : bomVO.getQuantity().toString()); applyElectricalQuantity(bomDetails, bomVO.getQuantity());
bomDetails.setRemarks(bomVO.getUnit()); bomDetails.setRemarks(bomVO.getUnit());
bomDetails.setUpdateTime(new Date()); bomDetails.setUpdateTime(new Date());
bomDetails.setDenominator(1.0);
bomDetails.setBomType("1"); bomDetails.setBomType("1");
list.add(bomDetails); list.add(bomDetails);
} }
@ -2437,21 +2489,140 @@ public class BomDetailsController extends BaseController {
List<BomDetailsVo> vos = BeanUtil.copyToList(list, BomDetailsVo.class); List<BomDetailsVo> vos = BeanUtil.copyToList(list, BomDetailsVo.class);
List<BomDetails> toInsert = saveBomDetails(vos); List<BomDetails> toInsert = saveBomDetails(vos);
boolean saved = false; int matchedMaterial = 0;
if (!toInsert.isEmpty()) { int unmatchedMaterial = 0;
saved = bomDetailsMapper.insertBatch(toInsert); int nameMismatch = 0;
for (BomDetails item : toInsert) {
String flag = item.getUnitWeight();
if ("是".equals(flag)) {
matchedMaterial++;
} else if ("编码名称不符".equals(flag)) {
nameMismatch++;
} else {
unmatchedMaterial++;
}
} }
int savedCount = 0;
boolean saved = false;
if (!toInsert.isEmpty()) {
saved = Boolean.TRUE.equals(bomDetailsMapper.insertBatch(toInsert));
savedCount = saved ? toInsert.size() : 0;
}
Map<String, Object> resp = new HashMap<>();
resp.put("totalRows", total); resp.put("totalRows", total);
resp.put("parsedRows", list.size()); resp.put("parsedRows", list.size());
resp.put("savedRows", saved); resp.put("skippedLingYong", skippedLingYong);
resp.put("message", "导入完成"); resp.put("skippedEmpty", skippedEmpty);
return R.ok(resp); resp.put("savedCount", savedCount);
resp.put("matchedMaterial", matchedMaterial);
resp.put("unmatchedMaterial", unmatchedMaterial);
resp.put("nameMismatch", nameMismatch);
resp.put("bomStatus", ProjectStatusLock.DRAWING_DONE);
resp.put("needProcess", 0);
if (!saved) {
String failMsg;
if (total == 0) {
failMsg = "导入失败:Excel没有读到数据,请确认是否用对模板、表头是否在第一行";
} else if (list.isEmpty() && skippedLingYong > 0) {
failMsg = "导入失败:有效物料为0(领用行 " + skippedLingYong + " 条已跳过)";
} else if (list.isEmpty()) {
failMsg = "导入失败:没有可保存的物料行";
} else {
failMsg = "导入失败:物料解析了 " + list.size() + " 条,但写入数据库失败";
}
resp.put("success", false);
resp.put("resultType", "error");
resp.put("skipProcessOrders", Collections.emptyList());
resp.put("productionDoneOrders", Collections.emptyList());
resp.put("unmatchedOrderNos", Collections.emptyList());
resp.put("message", failMsg);
return R.fail(failMsg, resp);
}
List<ProcessOrderPro> electricalOrders = applyElectricalSkipProcess(orderNos, resp);
// 匹配到的令号发「电气BOM已上传」通知(含已生产完成、仅改 needProcess 的)
skipProcessNotifyService.notifyElectricalBomUploaded(electricalOrders, savedCount);
@SuppressWarnings("unchecked")
List<String> skipProcessOrders = (List<String>) resp.get("skipProcessOrders");
@SuppressWarnings("unchecked")
List<String> unmatchedOrderNos = (List<String>) resp.get("unmatchedOrderNos");
boolean statusUpdated = skipProcessOrders != null && !skipProcessOrders.isEmpty();
StringBuilder msg = new StringBuilder("导入成功,已保存 ").append(savedCount).append(" 条物料");
if (statusUpdated) {
msg.append(";令号 ").append(String.join("、", skipProcessOrders))
.append(" 已跳过工艺,状态改为设计出图完成");
} else if (unmatchedOrderNos != null && !unmatchedOrderNos.isEmpty()) {
msg.append(";未匹配到项目令号 ").append(String.join("、", unmatchedOrderNos))
.append(",状态未改");
}
String resultType = statusUpdated ? "success" : "warning";
resp.put("success", true);
resp.put("resultType", resultType);
resp.put("message", msg.toString());
return R.ok(msg.toString(), resp);
} catch (Exception e) { } catch (Exception e) {
return R.fail("导入失败: " + e.getMessage()); log.error("导入电气BOM失败", e);
String detail = e.getMessage();
if (StringUtils.isBlank(detail)) {
detail = e.getClass().getSimpleName();
}
return R.fail("导入失败:" + detail);
} }
} }
/**
* 电气 BOM 不走工艺审核:对应令号标记不做工艺,状态改为设计出图完成。
* 开始生产改在更新物料清单时,确认是电气项目后再写。已生产完成的令号不回退状态。
*
* @return 匹配到的项目,供钉钉通知使用(含未改状态的已完工令号)
*/
private List<ProcessOrderPro> applyElectricalSkipProcess(Set<String> orderNos, Map<String, Object> resp) {
List<String> updated = new ArrayList<>();
List<String> productionDone = new ArrayList<>();
List<ProcessOrderPro> matched = new ArrayList<>();
Set<String> found = new HashSet<>();
if (orderNos != null && !orderNos.isEmpty()) {
List<ProcessOrderPro> orders = processOrderProMapper.selectList(
new LambdaQueryWrapper<ProcessOrderPro>().in(ProcessOrderPro::getProductionOrderNo, orderNos));
for (ProcessOrderPro order : orders) {
if (order == null || order.getId() == null) {
continue;
}
found.add(order.getProductionOrderNo());
ProcessOrderPro patch = new ProcessOrderPro();
patch.setId(order.getId());
patch.setNeedProcess(0);
// 内存对象同步,供后续钉钉通知读最新标记
order.setNeedProcess(0);
if (order.getBomStatus() != null && order.getBomStatus() == ProjectStatusLock.PRODUCTION_DONE) {
// 已生产完成:只改 needProcess,不回退状态
productionDone.add(order.getProductionOrderNo());
} else {
patch.setBomStatus(ProjectStatusLock.DRAWING_DONE);
order.setBomStatus(ProjectStatusLock.DRAWING_DONE);
updated.add(order.getProductionOrderNo());
}
processOrderProMapper.updateById(patch);
matched.add(order);
}
}
List<String> unmatched = new ArrayList<>();
if (orderNos != null) {
for (String no : orderNos) {
if (StringUtils.isNotBlank(no) && !found.contains(no)) {
unmatched.add(no);
}
}
}
resp.put("skipProcessOrders", updated);
resp.put("productionDoneOrders", productionDone);
resp.put("unmatchedOrderNos", unmatched);
return matched;
}
// 保存到 BomDetails 表中 // 保存到 BomDetails 表中
//TODO: 使用本地库加速 //TODO: 使用本地库加速
private List<BomDetails> saveBomDetails(List<BomDetailsVo> bomDetailsVos) { private List<BomDetails> saveBomDetails(List<BomDetailsVo> bomDetailsVos) {

View File

@ -22,8 +22,10 @@ import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.excel.ExcelResult; import com.ruoyi.common.excel.ExcelResult;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.JdUtils; import com.ruoyi.common.utils.JdUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.PlmDrawingFileNames;
import com.ruoyi.common.utils.file.PlmPdfArchiveExtractor; import com.ruoyi.common.utils.file.PlmPdfArchiveExtractor;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.ImMaterial; import com.ruoyi.system.domain.ImMaterial;
@ -39,6 +41,7 @@ import com.ruoyi.system.mapper.ImMaterialMapper;
import com.ruoyi.system.service.IImMaterialService; import com.ruoyi.system.service.IImMaterialService;
import com.ruoyi.system.service.IPartCostService; import com.ruoyi.system.service.IPartCostService;
import com.ruoyi.system.service.IPlmPdfZipPushService; import com.ruoyi.system.service.IPlmPdfZipPushService;
import com.ruoyi.system.service.PlmDrawingArriveNotifyService;
import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -94,6 +97,7 @@ public class ImMaterialController extends BaseController {
private final IPartCostService iPartCostService; private final IPartCostService iPartCostService;
private final IPlmPdfZipPushService plmPdfZipPushService; private final IPlmPdfZipPushService plmPdfZipPushService;
private final PlmDrawingArriveNotifyService plmDrawingArriveNotifyService;
@Value("${plm.pdf.base-dir:D:/plm/pdf}") @Value("${plm.pdf.base-dir:D:/plm/pdf}")
private String plmPdfBaseDir; private String plmPdfBaseDir;
@ -222,6 +226,16 @@ public class ImMaterialController extends BaseController {
if (cleaned > 0) { if (cleaned > 0) {
resp.put("cleanedParentPdfCount", String.valueOf(cleaned)); resp.put("cleanedParentPdfCount", String.valueOf(cleaned));
} }
Map<String, String> notifyItem = new HashMap<>(8);
notifyItem.put("materialCode", materialCode);
notifyItem.put("materialName", materialName);
notifyItem.put("savedFileName", targetFile.getFileName().toString());
if (isDrawingChanged != null) {
notifyItem.put("isDrawingChanged", formatPlmBool(isDrawingChanged));
}
plmDrawingArriveNotifyService.notifyAsync(
productionOrderNo, "/system/material/plm/pdf", "单张",
null, null, Collections.singletonList(notifyItem));
return R.ok(resp); return R.ok(resp);
} catch (Exception e) { } catch (Exception e) {
log.error("接收PLM PDF图纸失败: productionOrderNo={}, materialCode={}", productionOrderNo, materialCode, e); log.error("接收PLM PDF图纸失败: productionOrderNo={}, materialCode={}", productionOrderNo, materialCode, e);
@ -324,6 +338,14 @@ public class ImMaterialController extends BaseController {
return R.fail("以下items声明的文件未上传: " + String.join(", ", missingFiles)); return R.fail("以下items声明的文件未上传: " + String.join(", ", missingFiles));
} }
Long processOrderProId;
try {
// 落盘前校验:令号必须已存在,不再自动建项目
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo, null);
} catch (ServiceException e) {
return R.fail(e.getMessage());
}
String safeProductionOrderNo = sanitizePathPart(productionOrderNo); String safeProductionOrderNo = sanitizePathPart(productionOrderNo);
String separator = resolvePlmPdfNameSeparator(); String separator = resolvePlmPdfNameSeparator();
Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo); Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo);
@ -357,11 +379,13 @@ public class ImMaterialController extends BaseController {
} }
Long pushRecordId = null; Long pushRecordId = null;
Long processOrderProId = null;
try { try {
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo); // 落盘后按推送明细补默认产品(项目已存在)
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo, saved);
pushRecordId = plmPdfZipPushService.savePushRecord( pushRecordId = plmPdfZipPushService.savePushRecord(
itemsJson, "batch", productionOrderNo, dir.toString(), saved); itemsJson, "batch", productionOrderNo, dir.toString(), saved);
} catch (ServiceException e) {
return R.fail(e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("[PLM-PDF] 批量推送记录入库失败 productionOrderNo={}", productionOrderNo, e); log.error("[PLM-PDF] 批量推送记录入库失败 productionOrderNo={}", productionOrderNo, e);
} }
@ -395,6 +419,9 @@ public class ImMaterialController extends BaseController {
if (cleaned > 0) { if (cleaned > 0) {
resp.put("cleanedParentPdfCount", cleaned); resp.put("cleanedParentPdfCount", cleaned);
} }
plmDrawingArriveNotifyService.notifyAsync(
productionOrderNo, "/system/material/plm/pdf/batch", "批量",
processOrderProId, pushRecordId, saved);
return R.ok(resp); return R.ok(resp);
} catch (Exception e) { } catch (Exception e) {
log.error("批量接收PLM PDF图纸失败: productionOrderNo={}", productionOrderNo, e); log.error("批量接收PLM PDF图纸失败: productionOrderNo={}", productionOrderNo, e);
@ -413,6 +440,15 @@ public class ImMaterialController extends BaseController {
if (parsed.failed()) { if (parsed.failed()) {
return R.fail(parsed.message); return R.fail(parsed.message);
} }
Long processOrderProId;
try {
// 落盘前校验:令号必须已存在,不再自动建项目
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(parsed.productionOrderNo, null);
} catch (ServiceException e) {
return R.fail(e.getMessage());
}
Path dir = Paths.get(plmPdfBaseDir, sanitizePathPart(parsed.productionOrderNo)); Path dir = Paths.get(plmPdfBaseDir, sanitizePathPart(parsed.productionOrderNo));
Path dwgDir = dir.resolve(PLM_DWG_SUBDIR); Path dwgDir = dir.resolve(PLM_DWG_SUBDIR);
String separator = resolvePlmPdfNameSeparator(); String separator = resolvePlmPdfNameSeparator();
@ -472,11 +508,13 @@ public class ImMaterialController extends BaseController {
} }
Long pushRecordId = null; Long pushRecordId = null;
Long processOrderProId = null;
try { try {
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(parsed.productionOrderNo); // 落盘后按推送明细补默认产品(项目已存在)
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(parsed.productionOrderNo, saved);
pushRecordId = plmPdfZipPushService.savePushRecord( pushRecordId = plmPdfZipPushService.savePushRecord(
manifestJson, parsed.archiveName, parsed.productionOrderNo, dir.toString(), saved); manifestJson, parsed.archiveName, parsed.productionOrderNo, dir.toString(), saved);
} catch (ServiceException e) {
return R.fail(e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("[PLM-PDF] 推送记录入库失败 productionOrderNo={}, archive={}", log.error("[PLM-PDF] 推送记录入库失败 productionOrderNo={}, archive={}",
parsed.productionOrderNo, parsed.archiveName, e); parsed.productionOrderNo, parsed.archiveName, e);
@ -516,6 +554,9 @@ public class ImMaterialController extends BaseController {
} }
log.info("[PLM-PDF] 接收完成 productionOrderNo={}, count={}, archive={}, pushRecordId={}, processOrderProId={}, cleanedParentPdfCount={}", log.info("[PLM-PDF] 接收完成 productionOrderNo={}, count={}, archive={}, pushRecordId={}, processOrderProId={}, cleanedParentPdfCount={}",
parsed.productionOrderNo, saved.size(), parsed.archiveName, pushRecordId, processOrderProId, cleaned); parsed.productionOrderNo, saved.size(), parsed.archiveName, pushRecordId, processOrderProId, cleaned);
plmDrawingArriveNotifyService.notifyAsync(
parsed.productionOrderNo, "/system/material/plm/pdf/zip", "压缩包",
processOrderProId, pushRecordId, saved);
return R.ok(resp); return R.ok(resp);
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
@ -719,6 +760,7 @@ public class ImMaterialController extends BaseController {
/** /**
* 同令号同图号覆盖:先删除目录内该图号的旧文件(含历史时间戳后缀),再落盘为标准名。 * 同令号同图号覆盖:先删除目录内该图号的旧文件(含历史时间戳后缀),再落盘为标准名。
* 按完整图号匹配,不会把 {@code 图号-MP} 等更长图号当成同一张图。
* *
* @param extension 不含点,如 pdf / dwg * @param extension 不含点,如 pdf / dwg
*/ */
@ -732,7 +774,7 @@ public class ImMaterialController extends BaseController {
} }
/** /**
* 删除目录中与指定图号匹配的图纸(标准名、时间戳后缀名均清理) * 删除目录中与指定图号匹配的图纸(标准名、历史时间戳后缀名均清理;不按「图号-」前缀误伤 -MP 等更长图号)
*/ */
private static void deleteExistingDrawingsByMaterialCode(Path dir, String materialCode, private static void deleteExistingDrawingsByMaterialCode(Path dir, String materialCode,
String separator, String extension) { String separator, String extension) {
@ -741,15 +783,12 @@ public class ImMaterialController extends BaseController {
} }
String code = sanitizeFileNamePart(materialCode); String code = sanitizeFileNamePart(materialCode);
String sep = StringUtils.isNotBlank(separator) ? separator : "_"; String sep = StringUtils.isNotBlank(separator) ? separator : "_";
String prefixWithSep = code + sep;
String ext = extension.startsWith(".") ? extension.toLowerCase(Locale.ROOT) String ext = extension.startsWith(".") ? extension.toLowerCase(Locale.ROOT)
: ("." + extension.toLowerCase(Locale.ROOT)); : ("." + extension.toLowerCase(Locale.ROOT));
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*" + ext)) { try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*" + ext)) {
for (Path file : stream) { for (Path file : stream) {
String name = file.getFileName().toString(); String name = file.getFileName().toString();
if (name.equalsIgnoreCase(code + ext) if (PlmDrawingFileNames.matchesMaterialCode(name, code, sep, extension)) {
|| name.regionMatches(true, 0, prefixWithSep, 0, prefixWithSep.length())
|| name.regionMatches(true, 0, code + "-", 0, code.length() + 1)) {
try { try {
Files.deleteIfExists(file); Files.deleteIfExists(file);
log.info("[PLM-PDF] 同令号同图号覆盖,已删除旧文件: {}", file); log.info("[PLM-PDF] 同令号同图号覆盖,已删除旧文件: {}", file);
@ -941,7 +980,7 @@ public class ImMaterialController extends BaseController {
} }
/** /**
* 查询物料检索列表 * 查询物料检索列表(实时金蝶物料)
*/ */
@SaCheckPermission("system:material:list") @SaCheckPermission("system:material:list")
@GetMapping("/list") @GetMapping("/list")

View File

@ -7,8 +7,7 @@ import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.JdUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ImMaterial;
import com.ruoyi.system.domain.bo.ImMaterialBo; import com.ruoyi.system.domain.bo.ImMaterialBo;
import com.ruoyi.system.domain.bo.ImProductionPlanBo; import com.ruoyi.system.domain.bo.ImProductionPlanBo;
import com.ruoyi.system.domain.dto.*; import com.ruoyi.system.domain.dto.*;
@ -17,15 +16,12 @@ import com.ruoyi.system.domain.vo.InventoryInfoVO;
import com.ruoyi.system.runner.JdUtil; import com.ruoyi.system.runner.JdUtil;
import com.ruoyi.system.service.IImMaterialService; import com.ruoyi.system.service.IImMaterialService;
import com.ruoyi.system.service.IImProductionPlanService; import com.ruoyi.system.service.IImProductionPlanService;
import io.netty.util.concurrent.CompleteFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.*;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.*; import java.util.concurrent.*;
@RequiredArgsConstructor @RequiredArgsConstructor
@ -33,6 +29,8 @@ import java.util.concurrent.*;
@RequestMapping("/index") @RequestMapping("/index")
@Slf4j @Slf4j
public class IndexController { public class IndexController {
private static final int MAX_MATERIAL_CODES = 50;
private final IImMaterialService iImMaterialService; private final IImMaterialService iImMaterialService;
private final IImProductionPlanService imProductionPlanService; private final IImProductionPlanService imProductionPlanService;
@ -42,111 +40,123 @@ public class IndexController {
} }
/** /**
* 金蝶chaxun * 金蝶库存查询(支持单个或多个物料编码)
* <p>
* 前端一次传入多个编码;后端对每个物料按原逻辑精确查询后汇总返回(金蝶侧不支持可靠的多码 IN 拼接)。
* 参数任选其一:
* - materialCode:单个物料编码(兼容旧调用)
* - materialCodes:多个物料编码,逗号分隔
*/ */
@GetMapping("/inventory") @GetMapping("/inventory")
public R<InventoryInfoVO> handleData(String materialCode) { public R<List<InventoryInfoVO>> handleData(
// 创建线程池 @RequestParam(value = "materialCode", required = false) String materialCode,
@RequestParam(value = "materialCodes", required = false) String materialCodes) {
List<String> codes = parseMaterialCodes(materialCode, materialCodes);
if (codes.isEmpty()) {
return R.fail("请指定物料编码");
}
if (codes.size() > MAX_MATERIAL_CODES) {
return R.fail("单次最多查询 " + MAX_MATERIAL_CODES + " 个物料");
}
int poolSize = Math.min(Math.max(codes.size(), 4), 10);
ExecutorService executorService = new ThreadPoolExecutor( ExecutorService executorService = new ThreadPoolExecutor(
4, // 线程池核心线程数 poolSize,
4, // 线程池最大线程数 poolSize,
0L, // 线程空闲时间 0L,
TimeUnit.MILLISECONDS, // 时间单位 TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10), // 线程队列 new LinkedBlockingQueue<>(Math.max(codes.size() * 2, 10)),
new ThreadPoolExecutor.DiscardPolicy() // 拒绝策略 new ThreadPoolExecutor.CallerRunsPolicy()
); );
try { try {
// 并行执行多个查询任务 List<CompletableFuture<InventoryInfoVO>> futures = new ArrayList<>(codes.size());
CompletableFuture<Double> inventoryFuture = CompletableFuture.supplyAsync(() -> { for (String code : codes) {
log.info("开始查询即时库存数量: {}", materialCode); futures.add(CompletableFuture.supplyAsync(() -> queryOne(code), executorService));
return Optional.ofNullable(JdUtil.getKuCun(materialCode)) }
.map(list -> list.stream() CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.mapToDouble(JDInventoryDTO::getFBaseQty) .get(Math.max(30, codes.size() * 10L), TimeUnit.SECONDS);
.sum())
.orElse(0.0);
}, executorService);
CompletableFuture<Double> noPickedFuture = CompletableFuture.supplyAsync(() -> {
log.info("开始查询生产用料清单子项未领料数量: {}", materialCode);
return Optional.ofNullable(JdUtil.getWeiLingliao(materialCode))
.map(list -> list.stream()
.mapToDouble(JDProductionDTO::getFNoPickedQty)
.sum())
.orElse(0.0);
}, executorService);
CompletableFuture<Double> productionFuture = CompletableFuture.supplyAsync(() -> {
log.info("开始查询生产订单未入库数量: {}", materialCode);
return Optional.ofNullable(JdUtil.getProMoList(materialCode))
.map(list -> list.stream()
.mapToDouble(ProMoDTO::getFQty)
.sum())
.orElse(0.0);
}, executorService);
CompletableFuture<Double> purchaseFuture = CompletableFuture.supplyAsync(() -> {
log.info("开始查询采购订单未入库数量: {}", materialCode);
return Optional.ofNullable(JdUtil.getPurchaseOrderList(materialCode))
.map(list -> list.stream()
.mapToDouble(PurchaseOrderDTO::getFRemainStockINQty)
.sum())
.orElse(0.0);
}, executorService);
CompletableFuture<Double> salesOrderFuture = CompletableFuture.supplyAsync(() -> {
log.info("开始查询销售订单未入库数量: {}", materialCode);
return Optional.ofNullable(JdUtil.getSalesOrderList(materialCode))
.map(list -> list.stream()
.mapToDouble(SalesOrderDTO::getFBaseRemainOutQty)
.sum())
.orElse(0.0);
}, executorService);
// 等待所有任务完成并获取结果
CompletableFuture<InventoryInfoVO> resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture,salesOrderFuture)
.thenApply(v -> {
try {
double inventoryQty = inventoryFuture.get();
double fNoPickedQty = noPickedFuture.get();
double productionQty = productionFuture.get();
double purchaseQty = purchaseFuture.get();
double salesOrder = salesOrderFuture.get();
// 计算预计可用库存 即时库存+生产未入库+(采购申请-采购未入库)-子项未领料-销售实际未出库数量
double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty-salesOrder;
InventoryInfoVO inventoryInfoVO = new InventoryInfoVO();
inventoryInfoVO.setMaterialCode(materialCode);
inventoryInfoVO.setKucun(String.valueOf(inventoryQty));
inventoryInfoVO.setQuantity(String.valueOf(fNoPickedQty));
inventoryInfoVO.setMaterialName(String.valueOf(purchaseQty));
inventoryInfoVO.setStockName(String.valueOf(productionQty));
inventoryInfoVO.setStockUnit(String.valueOf(keyong));
inventoryInfoVO.setSalesOrder(String.valueOf(salesOrder));
return inventoryInfoVO;
} catch (Exception e) {
log.error("处理库存数据时发生错误", e);
throw new CompletionException(e);
}
});
// 获取最终结果
return R.ok(resultFuture.get(30, TimeUnit.SECONDS));
List<InventoryInfoVO> result = new ArrayList<>(codes.size());
for (CompletableFuture<InventoryInfoVO> future : futures) {
result.add(future.get());
}
return R.ok(result);
} catch (Exception e) { } catch (Exception e) {
log.error("查询库存信息失败", e); log.error("查询库存信息失败", e);
return R.fail("查询库存信息失败:" + e.getMessage()); return R.fail("查询库存信息失败:" + e.getMessage());
} finally { } finally {
// 关闭线程池
executorService.shutdown(); executorService.shutdown();
} }
} }
/**
* 单个物料精确查询(与原先单码接口同一套金蝶条件),供多码并行汇总。
*/
private InventoryInfoVO queryOne(String materialCode) {
try {
double inventoryQty = Optional.ofNullable(JdUtil.getKuCun(materialCode))
.map(list -> list.stream().mapToDouble(JDInventoryDTO::getFBaseQty).sum())
.orElse(0.0);
double fNoPickedQty = Optional.ofNullable(JdUtil.getWeiLingliao(materialCode))
.map(list -> list.stream().mapToDouble(JDProductionDTO::getFNoPickedQty).sum())
.orElse(0.0);
double productionQty = Optional.ofNullable(JdUtil.getProMoList(materialCode))
.map(list -> list.stream().mapToDouble(ProMoDTO::getFQty).sum())
.orElse(0.0);
double purchaseQty = Optional.ofNullable(JdUtil.getPurchaseOrderList(materialCode))
.map(list -> list.stream().mapToDouble(PurchaseOrderDTO::getFRemainStockINQty).sum())
.orElse(0.0);
double salesOrder = Optional.ofNullable(JdUtil.getSalesOrderList(materialCode))
.map(list -> list.stream().mapToDouble(SalesOrderDTO::getFBaseRemainOutQty).sum())
.orElse(0.0);
double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty - salesOrder;
InventoryInfoVO vo = new InventoryInfoVO();
vo.setMaterialCode(materialCode);
vo.setKucun(String.valueOf(inventoryQty));
vo.setQuantity(String.valueOf(fNoPickedQty));
vo.setMaterialName(String.valueOf(purchaseQty));
vo.setStockName(String.valueOf(productionQty));
vo.setStockUnit(String.valueOf(keyong));
vo.setSalesOrder(String.valueOf(salesOrder));
return vo;
} catch (Exception e) {
log.error("查询单个物料库存失败: {}", materialCode, e);
InventoryInfoVO vo = new InventoryInfoVO();
vo.setMaterialCode(materialCode);
vo.setKucun("0.0");
vo.setQuantity("0.0");
vo.setMaterialName("0.0");
vo.setStockName("0.0");
vo.setStockUnit("0.0");
vo.setSalesOrder("0.0");
return vo;
}
}
private static List<String> parseMaterialCodes(String materialCode, String materialCodes) {
LinkedHashSet<String> set = new LinkedHashSet<>();
addCodes(set, materialCodes);
addCodes(set, materialCode);
return new ArrayList<>(set);
}
private static void addCodes(Set<String> set, String raw) {
if (StringUtils.isBlank(raw)) {
return;
}
for (String part : raw.split(",")) {
if (StringUtils.isNotBlank(part)) {
set.add(part.trim());
}
}
}
@Log(title = "生产计划") @Log(title = "生产计划")
@PostMapping("/JDList") @PostMapping("/JDList")
public void synchronize_lists() { public void synchronize_lists() {
K3CloudApi client = new K3CloudApi(); K3CloudApi client = new K3CloudApi();
//请求参数,要求为json字符串
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("FormId", "PRD_MO"); json.addProperty("FormId", "PRD_MO");
json.addProperty("FieldKeys", "FID,F_UCHN_Text,FBillType.FNUMBER,FMaterialId.FNumber,FMaterialName,FQty,FBillType.FNUMBER,FWorkShopID.FNumber,FDate,FProductType,FPickMtrlStatus"); json.addProperty("FieldKeys", "FID,F_UCHN_Text,FBillType.FNUMBER,FMaterialId.FNumber,FMaterialName,FQty,FBillType.FNUMBER,FWorkShopID.FNumber,FDate,FProductType,FPickMtrlStatus");
@ -168,19 +178,16 @@ public class IndexController {
String jsonData = json.toString(); String jsonData = json.toString();
try { try {
// 调用接口并获取结果
String input = String.valueOf(client.executeBillQuery(jsonData)); String input = String.valueOf(client.executeBillQuery(jsonData));
input = input.substring(2, input.length() - 2); input = input.substring(2, input.length() - 2);
String[] split = input.split("]\\, \\["); String[] split = input.split("]\\, \\[");
for (String imProductionn : split) { for (String imProductionn : split) {
ImProductionPlanBo imProductionPlan = new ImProductionPlanBo(); ImProductionPlanBo imProductionPlan = new ImProductionPlanBo();
String[] imPro= imProductionn.split(","); String[] imPro = imProductionn.split(",");
imProductionPlan.setFID(Long.valueOf(imPro[0])); imProductionPlan.setFID(Long.valueOf(imPro[0]));
imProductionPlan.setProductionCode(imPro[1]); imProductionPlan.setProductionCode(imPro[1]);
imProductionPlan.setTypeName(imPro[2]); imProductionPlan.setTypeName(imPro[2]);
imProductionPlan.setWorkShop(imPro[7]); imProductionPlan.setWorkShop(imPro[7]);
// 解析时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date parse = simpleDateFormat.parse(imPro[8]); Date parse = simpleDateFormat.parse(imPro[8]);
imProductionPlan.setDateF(parse); imProductionPlan.setDateF(parse);
@ -188,14 +195,13 @@ public class IndexController {
imProductionPlan.setProductTyp(imPro[10]); imProductionPlan.setProductTyp(imPro[10]);
imProductionPlan.setFigureNumber(imPro[3]); imProductionPlan.setFigureNumber(imPro[3]);
imProductionPlan.setFigureName(imPro[4]); imProductionPlan.setFigureName(imPro[4]);
// imProductionPlan.setOneNumber(imPro[5]);
imProductionPlan.setPickmtrlStatus(imPro[10]); imProductionPlan.setPickmtrlStatus(imPro[10]);
imProductionPlan.setUpdateTime(new Date()); imProductionPlan.setUpdateTime(new Date());
imProductionPlan.setSearchValue("1"); imProductionPlan.setSearchValue("1");
imProductionPlan.setUpdateBy("admin"); imProductionPlan.setUpdateBy("admin");
imProductionPlan.setCreateTime(new Date()); imProductionPlan.setCreateTime(new Date());
imProductionPlan.setCreateBy("admin"); imProductionPlan.setCreateBy("admin");
imProductionPlanService.insertByBo(imProductionPlan) ; imProductionPlanService.insertByBo(imProductionPlan);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();

View File

@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaIgnore;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.system.domain.bo.ProcessOrderProBo; import com.ruoyi.system.domain.bo.ProcessOrderProBo;
import com.ruoyi.system.domain.vo.ProcessOrderProVo; import com.ruoyi.system.domain.vo.ProcessOrderProVo;
import com.ruoyi.system.domain.vo.plm.PlmProductionOrderVo; import com.ruoyi.system.domain.vo.plm.PlmProductionOrderVo;
@ -45,8 +46,7 @@ public class PlmProductionOrderController extends BaseController {
public TableDataInfo<PlmProductionOrderVo> productionOrders(ProcessOrderProBo bo, PageQuery pageQuery) { public TableDataInfo<PlmProductionOrderVo> productionOrders(ProcessOrderProBo bo, PageQuery pageQuery) {
TableDataInfo<ProcessOrderProVo> page = processOrderProService.queryPageList(bo, pageQuery); TableDataInfo<ProcessOrderProVo> page = processOrderProService.queryPageList(bo, pageQuery);
List<ProcessOrderProVo> rows = page.getRows(); List<ProcessOrderProVo> rows = page.getRows();
List<PlmProductionOrderVo> mapped = rows == null ? Collections.emptyList() List<PlmProductionOrderVo> mapped = rows == null ? Collections.emptyList() : rows.stream().map(this::toPlmVo).collect(Collectors.toList());
: rows.stream().map(this::toPlmVo).collect(Collectors.toList());
TableDataInfo<PlmProductionOrderVo> result = new TableDataInfo<>(mapped, page.getTotal()); TableDataInfo<PlmProductionOrderVo> result = new TableDataInfo<>(mapped, page.getTotal());
result.setCode(page.getCode()); result.setCode(page.getCode());
result.setMsg(page.getMsg()); result.setMsg(page.getMsg());
@ -63,7 +63,7 @@ public class PlmProductionOrderController extends BaseController {
vo.setUnit(src.getUnit()); vo.setUnit(src.getUnit());
vo.setNeedProcess(src.getNeedProcess()); vo.setNeedProcess(src.getNeedProcess());
vo.setBomStatus(src.getBomStatus()); vo.setBomStatus(src.getBomStatus());
vo.setBomStatusLabel(resolveBomStatusLabel(src.getBomStatus())); vo.setBomStatusLabel(src.getBomStatus() == null ? null : ProjectStatusLock.statusLabel(src.getBomStatus()));
vo.setRouteStatus(src.getRouteStatus()); vo.setRouteStatus(src.getRouteStatus());
vo.setRouteStatusLabel(resolveRouteStatusLabel(src.getRouteStatus())); vo.setRouteStatusLabel(resolveRouteStatusLabel(src.getRouteStatus()));
vo.setPlanStartTime(src.getPlanStartTime()); vo.setPlanStartTime(src.getPlanStartTime());
@ -72,39 +72,6 @@ public class PlmProductionOrderController extends BaseController {
return vo; return vo;
} }
/**
* 项目状态:0方案设计完成 … 2工艺待审核 … 6开始生产 … 8工艺审核完成 … 9工艺审核拒绝
*/
private static String resolveBomStatusLabel(Long status) {
if (status == null) {
return null;
}
switch (status.intValue()) {
case 0:
return "方案设计完成";
case 1:
return "设计出图完成";
case 2:
return "工艺待审核";
case 3:
return "已推送BOM";
case 4:
return "已推送工艺";
case 5:
return "已核算MRP";
case 6:
return "开始生产";
case 7:
return "生产完成";
case 8:
return "工艺审核完成";
case 9:
return "工艺审核拒绝";
default:
return String.valueOf(status);
}
}
/** 工艺审核:0未审核 1已审核 2已拒绝 */ /** 工艺审核:0未审核 1已审核 2已拒绝 */
private static String resolveRouteStatusLabel(Long status) { private static String resolveRouteStatusLabel(Long status) {
if (status == null) { if (status == null) {

View File

@ -26,6 +26,7 @@ import com.ruoyi.common.poi.DynamicDataMapping;
import com.ruoyi.system.constant.ProcessOrderProSource; import com.ruoyi.system.constant.ProcessOrderProSource;
import com.ruoyi.system.domain.*; import com.ruoyi.system.domain.*;
import com.ruoyi.system.domain.bo.KingdeeOrderReceiveBatchBo; import com.ruoyi.system.domain.bo.KingdeeOrderReceiveBatchBo;
import com.ruoyi.system.domain.bo.OrderProBomStatusBo;
import com.ruoyi.system.domain.dto.KingdeePrdMoDTO; import com.ruoyi.system.domain.dto.KingdeePrdMoDTO;
import com.ruoyi.system.domain.dto.KingdeePurchaseOrderDTO; import com.ruoyi.system.domain.dto.KingdeePurchaseOrderDTO;
import com.ruoyi.system.domain.dto.MaterialUseDTO; import com.ruoyi.system.domain.dto.MaterialUseDTO;
@ -53,6 +54,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.annotation.RepeatSubmit;
@ -133,6 +135,28 @@ public class ProcessOrderProController extends BaseController {
ExcelUtil.exportExcel(exportList, "项目令号", ProcessOrderProExportVo.class, response); ExcelUtil.exportExcel(exportList, "项目令号", ProcessOrderProExportVo.class, response);
} }
/**
* 临时导入模板:生产令号 / 出图时间 / 项目完成时间 / 工艺完成时间 / 计划排产时间 / BOM/物料更新 / 设计人
*/
@Log(title = "预计出图时间导入模板", businessType = BusinessType.EXPORT)
@SaCheckPermission("system:orderPro:importDrawingByTemplate")
@PostMapping("/importDrawingByTemplate")
public void importDrawingByTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(), "项目时间导入", OrderProDrawingImportVo.class, response);
}
/**
* 临时导入:按生产令号更新已填写字段,空单元格不更新
*/
@Log(title = "导入预计出图时间", businessType = BusinessType.IMPORT)
@SaCheckPermission("system:orderPro:importDrawingBy")
@PostMapping(value = "/importDrawingBy", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> importDrawingBy(@RequestPart("file") MultipartFile file) {
Map<String, Object> result = iProcessOrderProService.importDrawingByExcel(file);
String message = result.get("message") != null ? String.valueOf(result.get("message")) : "导入完成";
return R.ok(message, result);
}
/** /**
* 获取项目令号详细信息 * 获取项目令号详细信息
* *
@ -170,6 +194,18 @@ public class ProcessOrderProController extends BaseController {
return toAjax(iProcessOrderProService.updateByBo(bo)); return toAjax(iProcessOrderProService.updateByBo(bo));
} }
/**
* 批量设置项目状态。
* 独立接口,避免走 PUT /system/orderPro 的 EditGroup(设计人必填)导致只改状态失败。
*/
@SaCheckPermission("system:orderPro:edit")
@Log(title = "设置项目状态", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/bomStatus")
public R<Void> updateBomStatus(@Validated @RequestBody OrderProBomStatusBo bo) {
return toAjax(iProcessOrderProService.updateBomStatus(bo.getIds(), bo.getBomStatus()));
}
/** /**
* 清空项目关联数据(保留项目) * 清空项目关联数据(保留项目)
* *
@ -258,6 +294,18 @@ public class ProcessOrderProController extends BaseController {
return R.ok(); return R.ok();
} }
/**
* 设置出图模式:normal 正常 / part 零部件
*/
@SaCheckPermission("system:orderPro:edit")
@Log(title = "设置出图模式", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/drawingMode/{id}")
public R<Void> updateDrawingMode(@PathVariable Long id, @RequestBody ProcessOrderProBo bo) {
String mode = bo == null ? null : bo.getDrawingMode();
return toAjax(iProcessOrderProService.updateDrawingMode(id, mode));
}
@SaCheckPermission("system:orderPro:executDrawing") @SaCheckPermission("system:orderPro:executDrawing")
@Log(title = "执行出图", businessType = BusinessType.INSERT) @Log(title = "执行出图", businessType = BusinessType.INSERT)
@RepeatSubmit() @RepeatSubmit()
@ -361,6 +409,7 @@ public class ProcessOrderProController extends BaseController {
os.flush(); os.flush();
} }
log.info("下载PDF ZIP 输出完成: id={}, file={}", id, file.getName()); log.info("下载PDF ZIP 输出完成: id={}, file={}", id, file.getName());
iProcessOrderProService.cleanupPdfRunDir(filePath);
} catch (Exception e) { } catch (Exception e) {
if (response.isCommitted()) { if (response.isCommitted()) {
@ -430,6 +479,7 @@ public class ProcessOrderProController extends BaseController {
} }
os.flush(); os.flush();
} }
iProcessOrderProService.cleanupPdfRunDir(zipFile.getAbsolutePath());
} catch (Exception e) { } catch (Exception e) {
if (response.isCommitted()) { if (response.isCommitted()) {
@ -1947,7 +1997,8 @@ public class ProcessOrderProController extends BaseController {
} }
/** /**
* 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId。 * 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId(工艺)。
* 项目「是否做工艺=否」时不推送。
*/ */
@SaCheckPermission("system:orderPro:sendProcessRoutePlanExcel") @SaCheckPermission("system:orderPro:sendProcessRoutePlanExcel")
@Log(title = "发送工艺计划表", businessType = BusinessType.OTHER) @Log(title = "发送工艺计划表", businessType = BusinessType.OTHER)
@ -1956,6 +2007,16 @@ public class ProcessOrderProController extends BaseController {
return iProcessOrderProService.sendProcessRoutePlanExcel(id); return iProcessOrderProService.sendProcessRoutePlanExcel(id);
} }
/**
* 设计人员确认本令号图纸已全部上传,确认后方可下载 PDF / PLM DWG。
*/
@SaCheckPermission("system:orderPro:issueDrawingQty")
@Log(title = "确认图纸已齐", businessType = BusinessType.UPDATE)
@PostMapping("/issueDrawingQty/{id}")
public R<Map<String, Object>> issueDrawingQty(@NotNull(message = "项目ID不能为空") @PathVariable Long id) {
return iProcessOrderProService.issueDrawingQty(id);
}
/** /**
* 查询工艺计划表钉钉发送成功次数(用于按钮文案) * 查询工艺计划表钉钉发送成功次数(用于按钮文案)
*/ */
@ -2821,6 +2882,11 @@ public class ProcessOrderProController extends BaseController {
writeJsonFail(response, "项目未配置生产令号"); writeJsonFail(response, "项目未配置生产令号");
return; return;
} }
if (ProcessOrderProSource.PLM.equals(orderPro.getSource())
&& !Integer.valueOf(1).equals(orderPro.getDrawingQtyIssued())) {
writeJsonFail(response, "请先点击「确认图纸已齐」,图纸全部上传完成后再下载图纸");
return;
}
List<File> dwgFiles = plmPdfZipPushService.listDownloadableDwgFiles(orderPro.getProductionOrderNo()); List<File> dwgFiles = plmPdfZipPushService.listDownloadableDwgFiles(orderPro.getProductionOrderNo());
String zipName = orderPro.getProductionOrderNo().trim() + "_PLM_DWG.zip"; String zipName = orderPro.getProductionOrderNo().trim() + "_PLM_DWG.zip";
String encoded = URLEncoder.encode(zipName, "UTF-8").replaceAll("\\+", "%20"); String encoded = URLEncoder.encode(zipName, "UTF-8").replaceAll("\\+", "%20");

View File

@ -1,6 +1,7 @@
package com.ruoyi.system.controller; package com.ruoyi.system.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.ExcelWriter;
@ -25,9 +26,12 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.helper.LoginHelper; import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.FtpUtil; import com.ruoyi.common.utils.FtpUtil;
import com.ruoyi.common.utils.KingdeeRouteDepartmentHelper; import com.ruoyi.common.utils.KingdeeRouteDepartmentHelper;
import com.ruoyi.common.utils.JsyyOrderNo;
import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.SmbUtil; import com.ruoyi.common.utils.file.SmbUtil;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.constant.DingTodoSceneCodes;
import com.ruoyi.system.domain.*; import com.ruoyi.system.domain.*;
import com.ruoyi.system.domain.bo.ProcessRouteBo; import com.ruoyi.system.domain.bo.ProcessRouteBo;
import com.ruoyi.system.domain.dto.*; import com.ruoyi.system.domain.dto.*;
@ -66,8 +70,6 @@ import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/** /**
* 工艺路线 * 工艺路线
@ -98,8 +100,13 @@ public class ProcessRouteController extends BaseController {
private ISysOssService iSysOssService; private ISysOssService iSysOssService;
@Autowired @Autowired
private IProcessOrderProService processOrderProService; private IProcessOrderProService processOrderProService;
/** 计划表落盘 + 按 plan-user-id 推送(与 PLM 导出共用) */ /**
* 计划表落盘 + 按 plan-user-id 推送(与 PLM 导出共用)
*/
private final ProcessPlanExcelPushService processPlanExcelPushService; private final ProcessPlanExcelPushService processPlanExcelPushService;
private final IDrawingRouteOperLogService drawingRouteOperLogService;
private final IDingTodoDispatchService dingTodoDispatchService;
private final DrawingFileQueryService drawingFileQueryService;
@Autowired @Autowired
private ISysUserService userService; private ISysUserService userService;
@ -175,9 +182,9 @@ public class ProcessRouteController extends BaseController {
} }
/** /**
* 查询工艺路线列表 * 查询工艺路线列表。项目令号「查看工艺」只分配 processlist 时也要能拉数。
*/ */
@SaCheckPermission("system:route:list") @SaCheckPermission(value = {"system:route:list", "system:orderPro:processlist"}, mode = SaMode.OR)
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo<ProcessRouteVo> list(ProcessRouteBo bo, PageQuery pageQuery) { public TableDataInfo<ProcessRouteVo> list(ProcessRouteBo bo, PageQuery pageQuery) {
try { try {
@ -400,7 +407,8 @@ public class ProcessRouteController extends BaseController {
@Log(title = "生产工艺计划表导入", businessType = BusinessType.IMPORT) @Log(title = "生产工艺计划表导入", businessType = BusinessType.IMPORT)
@SaCheckPermission("system:route:importRoute") @SaCheckPermission("system:route:importRoute")
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R importData(@RequestParam("file") MultipartFile file) throws Exception { public R importData(@RequestParam("file") MultipartFile file,
@RequestParam(value = "productionOrderNo", required = false) String expectedOrderNo) throws Exception {
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
log.info("读取文件名: " + originalFilename); log.info("读取文件名: " + originalFilename);
@ -410,23 +418,57 @@ public class ProcessRouteController extends BaseController {
// 读取总装部分sheet // 读取总装部分sheet
ExcelResult<ProductionOrderVo> result1 = ExcelUtil.importExcelSheet1(file.getInputStream(), ProductionOrderVo.class, true); ExcelResult<ProductionOrderVo> result1 = ExcelUtil.importExcelSheet1(file.getInputStream(), ProductionOrderVo.class, true);
List<ProductionOrderVo> list = result1.getList(); List<ProductionOrderVo> list = result1.getList();
String productionOrderNo = list1.get(0).getRouteDescription(); long started = System.currentTimeMillis();
List<BomDetails> bomDetails = iBomDetailsService.selectByProjectNumber(list1.get(0).getRouteDescription()); if (list1 == null || list1.isEmpty()) {
List<ProcessRoute> route = iProcessRouteService.selectByProjectNumber(list1.get(0).getRouteDescription()); recordRouteUpload(null, expectedOrderNo, originalFilename, 0,
if (list1.isEmpty()) { IDrawingRouteOperLogService.STATUS_FAIL, "工艺生产计划表为空", started);
return R.fail("项目 " + productionOrderNo + "工艺生产计划表为空,请检查此表或将sheet移动至第七个!"); return R.fail("工艺生产计划表为空,请检查此表或将sheet移动至第七个!");
} }
// 防呆:当前项目令号必须与文件内「生产令号」一致
String orderNoMismatch = validateImportOrderNo(expectedOrderNo, list1, list);
if (orderNoMismatch != null) {
ProcessOrderPro orderForMismatch = StringUtils.isNotBlank(expectedOrderNo)
? processOrderProService.selectByProjectNumber(expectedOrderNo.trim()) : null;
recordRouteUpload(orderForMismatch, expectedOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, orderNoMismatch, started);
return R.fail(orderNoMismatch);
}
String resolvedOrderNo = firstNonBlankRouteDescription(list1);
if (StringUtils.isBlank(resolvedOrderNo) && StringUtils.isNotBlank(expectedOrderNo)) {
resolvedOrderNo = expectedOrderNo.trim();
}
// 后面异步推送会捕获令号,必须是 effectively final
final String productionOrderNo = resolvedOrderNo;
ProcessOrderPro orderForLock = processOrderProService.selectByProjectNumber(productionOrderNo);
if (orderForLock != null) {
String statusDeny = ProjectStatusLock.denyIfLocked(orderForLock.getBomStatus(), ProjectStatusLock.Action.UPLOAD_ROUTE);
if (statusDeny != null) {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, statusDeny, started);
return R.fail(statusDeny);
}
}
List<BomDetails> bomDetails = iBomDetailsService.selectByProjectNumber(productionOrderNo);
List<ProcessRoute> route = iProcessRouteService.selectByProjectNumber(productionOrderNo);
if (!bomDetails.isEmpty()) { if (!bomDetails.isEmpty()) {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, "已导入过BOM,请先清空再上传", started);
return R.fail("项目 " + productionOrderNo + "已导入过BOM,请先清空再上传"); return R.fail("项目 " + productionOrderNo + "已导入过BOM,请先清空再上传");
} }
if (!route.isEmpty()) { if (!route.isEmpty()) {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, "已导入过工艺,请先清空再上传", started);
return R.fail("项目 " + productionOrderNo + "已导入过工艺 ,请先清空再上传"); return R.fail("项目 " + productionOrderNo + "已导入过工艺 ,请先清空再上传");
} }
String workCenterError = validateImportWorkCenters(list1); String workCenterError = validateImportWorkCenters(list1);
if (workCenterError != null) { if (workCenterError != null) {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, "工段校验失败", started);
return R.fail("生产工艺计划表工段校验失败:\n" + workCenterError); return R.fail("生产工艺计划表工段校验失败:\n" + workCenterError);
} }
if (iProcessRouteService.saveData(list1, list)) { if (iProcessRouteService.saveData(list1, list)) {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_SUCCESS, null, started);
// 计划表落到服务器固定目录(app.plan-excel.base-dir/令号/),保留不删 // 计划表落到服务器固定目录(app.plan-excel.base-dir/令号/),保留不删
File savedPlanFile = null; File savedPlanFile = null;
try { try {
@ -456,12 +498,38 @@ public class ProcessRouteController extends BaseController {
log.warn("获取当前登录用户昵称失败", e); log.warn("获取当前登录用户昵称失败", e);
} }
String todoProjectName = orderForLock == null ? null : orderForLock.getProductionName();
Date todoProjectEndTime = orderForLock == null ? null : orderForLock.getProjectEndTime();
String planUserIds = dingTalkProperties.getPlanUserId();
// 异步:按 plan-user-id 发文件 + 发工艺审核卡片(落盘文件保留) // 异步:按 plan-user-id 发文件 + 发工艺审核卡片(落盘文件保留)
String finalNickName = currentUserNickName; String finalNickName = currentUserNickName;
String operator = drawingRouteOperLogService.currentOperator();
String clientIp = drawingRouteOperLogService.currentClientIp();
Long projectId = orderForLock == null ? null : orderForLock.getId();
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
// 与 PLM 导出推送同一套接收人 Thread.currentThread().setContextClassLoader(contextClassLoader);
processPlanExcelPushService.sendToPlanUsers(uploadFile, originalFilename); long notifyStarted = System.currentTimeMillis();
ProcessPlanExcelPushService.FilePushResult filePush =
processPlanExcelPushService.sendToPlanUsers(uploadFile, originalFilename);
String todoError = dingTodoDispatchService.sendIfEnabled(
DingTodoSceneCodes.ROUTE_UPLOAD_PLAN,
productionOrderNo,
todoProjectName,
todoProjectEndTime,
uploadFile,
planUserIds);
if (StringUtils.isNotBlank(todoError) && filePush != null) {
filePush.setErrorMsg(StringUtils.isBlank(filePush.getErrorMsg())
? "待办: " + todoError
: filePush.getErrorMsg() + "; 待办: " + todoError);
if (filePush.getSuccessCount() > 0) {
filePush.setFailCount(filePush.getFailCount() + 1);
}
}
boolean cardOk = false;
String cardError = null;
String outTrackId = null;
try { try {
ProcessOrderPro order = processOrderProService.selectByProjectNumber(productionOrderNo); ProcessOrderPro order = processOrderProService.selectByProjectNumber(productionOrderNo);
if (order != null) { if (order != null) {
@ -476,7 +544,7 @@ public class ProcessRouteController extends BaseController {
cardDataMap.put("lastMessage", "请尽快处理"); cardDataMap.put("lastMessage", "请尽快处理");
cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批")); cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批"));
String outTrackId = "audit-card-" + System.currentTimeMillis(); outTrackId = "audit-card-" + System.currentTimeMillis();
// 调用发送给工艺负责人的方法 createAndDeliverCardToUser // 调用发送给工艺负责人的方法 createAndDeliverCardToUser
DingUtil.createAndDeliverCardToUser( DingUtil.createAndDeliverCardToUser(
accessToken, accessToken,
@ -497,17 +565,172 @@ public class ProcessRouteController extends BaseController {
// 更新审核状态为1(已审核/提交审核) // 更新审核状态为1(已审核/提交审核)
update.setRouteStatus(0L); update.setRouteStatus(0L);
proMapper.updateById(update); proMapper.updateById(update);
cardOk = true;
} else {
cardError = "项目不存在,未发送审核卡片";
} }
} catch (Exception e) { } catch (Exception e) {
cardError = e.getMessage();
log.error("发送钉钉卡片异常", e); log.error("发送钉钉卡片异常", e);
} }
recordRouteNotifyAfterUpload(productionOrderNo, projectId, originalFilename, finalNickName,
filePush, cardOk, cardError, outTrackId, operator, clientIp, notifyStarted);
}); });
return R.ok("上传物料成功"); return R.ok("上传物料成功");
} else { } else {
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
IDrawingRouteOperLogService.STATUS_FAIL, "导入失败", started);
return R.fail("导入失败"); return R.fail("导入失败");
} }
} }
/**
* 防呆:页面所选项目令号须与 Excel「生产令号」一致。
* 工艺 sheet(routeDescription)与总装 sheet(productionOrderNo)都会检查。
*
* @return 错误提示;通过则 null
*/
private String validateImportOrderNo(String expectedOrderNo, List<ProcessRouteVo> routes,
List<ProductionOrderVo> products) {
LinkedHashSet<String> fileNos = new LinkedHashSet<>();
if (routes != null) {
for (ProcessRouteVo row : routes) {
if (row != null && StringUtils.isNotBlank(row.getRouteDescription())) {
fileNos.add(row.getRouteDescription().trim());
}
}
}
if (products != null) {
for (ProductionOrderVo row : products) {
if (row != null && StringUtils.isNotBlank(row.getProductionOrderNo())) {
fileNos.add(row.getProductionOrderNo().trim());
}
}
}
if (fileNos.isEmpty()) {
return "导入文件未读到生产令号,请检查工艺计划表「生产令号」列";
}
// 大小写不同仍视为同一令号,避免 Excel 大小写差异误报多个令号
String fileNo = null;
for (String no : fileNos) {
if (fileNo == null) {
fileNo = no;
} else if (!fileNo.equalsIgnoreCase(no)) {
return "导入文件中存在多个生产令号:" + String.join("、", fileNos) + ",请确认是否导错文件";
}
}
if (StringUtils.isBlank(expectedOrderNo)) {
return "请先选择要导入的项目令号";
}
String expected = expectedOrderNo.trim();
if (!expected.equalsIgnoreCase(fileNo)) {
return "导入令号不一致:当前项目为「" + expected + "」,文件中为「" + fileNo + "」,请确认后重新导入";
}
return null;
}
/**
* 取工艺表中第一条非空生产令号。
*/
private static String firstNonBlankRouteDescription(List<ProcessRouteVo> routes) {
if (routes == null) {
return null;
}
for (ProcessRouteVo row : routes) {
if (row != null && StringUtils.isNotBlank(row.getRouteDescription())) {
return row.getRouteDescription().trim();
}
}
return null;
}
private void recordRouteUpload(ProcessOrderPro order, String productionOrderNo, String fileName,
int lineCount, String status, String errorMsg, long started) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("lineCount", lineCount);
summary.put("fileName", fileName);
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_UPLOAD, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_MANUAL);
row.setApiPath("/system/route/importData");
row.setProjectId(order == null ? null : order.getId());
row.setProductionOrderNo(productionOrderNo);
row.setFileName(fileName);
row.setStatus(status);
row.setErrorMsg(errorMsg);
row.setSummaryJson(JSONUtil.toJsonStr(summary));
row.setDurationMs(System.currentTimeMillis() - started);
});
}
private void recordRouteNotifyAfterUpload(String productionOrderNo, Long projectId, String fileName,
String nickName, ProcessPlanExcelPushService.FilePushResult filePush,
boolean cardOk, String cardError, String outTrackId,
String operator, String clientIp, long started) {
String fileStatus = filePush == null ? IDrawingRouteOperLogService.STATUS_FAIL : filePush.resolveStatus();
boolean fileOk = IDrawingRouteOperLogService.STATUS_SUCCESS.equals(fileStatus);
String status;
if (fileOk && cardOk) {
status = IDrawingRouteOperLogService.STATUS_SUCCESS;
} else if (!fileOk && !cardOk) {
status = IDrawingRouteOperLogService.STATUS_FAIL;
} else {
status = IDrawingRouteOperLogService.STATUS_PARTIAL;
}
StringBuilder error = new StringBuilder();
if (filePush != null && StringUtils.isNotBlank(filePush.getErrorMsg())) {
error.append("文件: ").append(filePush.getErrorMsg());
}
if (StringUtils.isNotBlank(cardError)) {
if (error.length() > 0) {
error.append("; ");
}
error.append("卡片: ").append(cardError);
}
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("fileStatus", fileStatus);
summary.put("fileSuccessCount", filePush == null ? 0 : filePush.getSuccessCount());
summary.put("fileFailCount", filePush == null ? 0 : filePush.getFailCount());
summary.put("cardOk", cardOk);
summary.put("outTrackId", outTrackId);
summary.put("submitter", nickName);
String notifyUsers = joinNotifyUsers(
filePush == null ? null : filePush.getUserIds(),
dingTalkProperties.getRouteApprovalId());
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_NOTIFY, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_MANUAL);
row.setApiPath("/system/route/importData");
row.setProjectId(projectId);
row.setProductionOrderNo(productionOrderNo);
row.setNotifyType(IDrawingRouteOperLogService.NOTIFY_FILE_AND_CARD);
row.setNotifyUserIds(notifyUsers);
row.setMessageTitle(nickName + "提交工艺审核");
row.setFileName(fileName);
row.setStatus(status);
row.setErrorMsg(error.length() == 0 ? null : error.toString());
row.setSummaryJson(JSONUtil.toJsonStr(summary));
row.setOperator(operator);
row.setClientIp(clientIp);
row.setDurationMs(System.currentTimeMillis() - started);
});
}
private static String joinNotifyUsers(String... parts) {
StringBuilder sb = new StringBuilder();
if (parts == null) {
return null;
}
for (String part : parts) {
if (StringUtils.isBlank(part)) {
continue;
}
if (sb.length() > 0) {
sb.append(",");
}
sb.append(part.trim());
}
return sb.length() == 0 ? null : sb.toString();
}
/** /**
* 查询组焊件集合 * 查询组焊件集合
*/ */
@ -549,9 +772,51 @@ public class ProcessRouteController extends BaseController {
@SaCheckPermission("system:route:pushRouteBom") @SaCheckPermission("system:route:pushRouteBom")
@PostMapping("/pushRouteBom") @PostMapping("/pushRouteBom")
public R<ProcessRoutePushResultDTO> pushRouteBom(@RequestParam String rooteProdet, @RequestParam String groupName) { public R<ProcessRoutePushResultDTO> pushRouteBom(@RequestParam String rooteProdet, @RequestParam String groupName) {
String jsyyDeny = JsyyOrderNo.denyIfMatches(rooteProdet);
if (jsyyDeny != null) {
return R.fail(jsyyDeny);
}
ProcessOrderPro orderForLock = processOrderProService.selectByProjectNumber(rooteProdet);
if (orderForLock != null) {
String statusDeny = ProjectStatusLock.denyIfLocked(orderForLock.getBomStatus(), ProjectStatusLock.Action.PUSH_ROUTE);
if (statusDeny != null) {
return R.fail(statusDeny);
}
}
return iProcessRouteService.pushRouteBom(rooteProdet, groupName); return iProcessRouteService.pushRouteBom(rooteProdet, groupName);
} }
/**
* 按物料推送工艺:只推所选物料,不改项目整单状态。
*/
@Log(title = "按物料推送工艺工序")
@SaCheckPermission("system:route:pushRouteBom")
@PostMapping("/pushRouteBomByMaterial")
public R<ProcessRoutePushResultDTO> pushRouteBomByMaterial(@RequestParam String rooteProdet,
@RequestParam String groupName,
@RequestParam String materialCodes) {
String jsyyDeny = JsyyOrderNo.denyIfMatches(rooteProdet);
if (jsyyDeny != null) {
return R.fail(jsyyDeny);
}
ProcessOrderPro orderForLock = processOrderProService.selectByProjectNumber(rooteProdet);
if (orderForLock != null) {
String statusDeny = ProjectStatusLock.denyIfLocked(orderForLock.getBomStatus(), ProjectStatusLock.Action.PUSH_ROUTE);
if (statusDeny != null) {
return R.fail(statusDeny);
}
}
if (StringUtils.isBlank(materialCodes)) {
return R.fail("请选择要推送的物料");
}
List<String> codes = Arrays.stream(materialCodes.split("[,,;;\\s]+"))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
return iProcessRouteService.pushRouteBomByMaterial(rooteProdet, groupName, codes);
}
@Log(title = "审核通过(发送钉钉通知,不推送金蝶)") @Log(title = "审核通过(发送钉钉通知,不推送金蝶)")
@SaCheckPermission("system:route:approveRoute") @SaCheckPermission("system:route:approveRoute")
@PostMapping("/approveRoute") @PostMapping("/approveRoute")
@ -583,41 +848,54 @@ public class ProcessRouteController extends BaseController {
return iProcessRouteService.getSelecPlanRouteList(rooteProdet); return iProcessRouteService.getSelecPlanRouteList(rooteProdet);
} }
@Log(title = "生成项目图纸PDF") @Log(title = "生成工序计划单PDF")
@SaCheckPermission("system:route:generatePDFs") @SaCheckPermission("system:route:generatePDFs")
@GetMapping("/generatePDFs") @GetMapping("/generatePDFs")
public void generatePDFs(String rooteProdet, HttpServletResponse response) throws IOException { public void generatePDFs(String rooteProdet,
// 调用服务层方法生成 ZIP 文件并获取其路径 @RequestParam(value = "materialCode", required = false) String materialCode,
String zipFilePath = iProcessRouteService.generatePDFs(rooteProdet); HttpServletResponse response) throws IOException {
ProcessOrderPro processOrderPro = proMapper.selectByProjectNumber(rooteProdet); if (StringUtils.isBlank(rooteProdet)) {
processOrderPro.setBomStatus(5L); throw new ServiceException("生产令号不能为空");
proMapper.updateById(processOrderPro);
// 读取文件为字节数组
File zipFile = new File(zipFilePath);
if (!zipFile.exists()) {
throw new FileNotFoundException("ZIP 文件未找到: " + zipFilePath);
} }
if (zipFile.length() <= 0) { // 有物料编码:单份 PDF;无物料编码:令号下全部 PDF 压缩包
throw new IOException("ZIP 文件为空: " + zipFilePath); String filePath = iProcessRouteService.generatePDFs(rooteProdet, materialCode);
File outFile = new File(filePath);
if (!outFile.exists()) {
throw new FileNotFoundException("文件未找到: " + filePath);
} }
try (ZipFile zf = new ZipFile(zipFile)) { if (outFile.length() <= 0) {
Enumeration<? extends ZipEntry> entries = zf.entries(); throw new IOException("文件为空: " + filePath);
if (entries == null || !entries.hasMoreElements()) { }
throw new IOException("ZIP 文件中没有PDF: " + zipFilePath); boolean isPdf = filePath.toLowerCase().endsWith(".pdf");
boolean isZip = filePath.toLowerCase().endsWith(".zip");
if (!isPdf && !isZip) {
throw new IOException("未生成PDF或压缩包: " + filePath);
}
response.setContentType(isPdf ? "application/pdf" : "application/zip");
String encoded = URLEncoder.encode(outFile.getName(), "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encoded);
try (FileInputStream fis = new FileInputStream(outFile);
OutputStream outputStreams = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
outputStreams.write(buffer, 0, len);
} }
outputStreams.flush();
} }
FileInputStream fis = new FileInputStream(zipFile); }
OutputStream outputStreams = response.getOutputStream();
response.setContentType("application/zip");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
outputStreams.write(buffer, 0, len);
}
outputStreams.flush();
fis.close(); /**
outputStreams.close(); * 下载 PLM 图纸/模型。后端校验项目来源后转发文件服务。
*/
@Log(title = "下载图纸模型", businessType = BusinessType.EXPORT)
@SaCheckPermission("system:route:downloadDrawingModel")
@GetMapping("/downloadDrawingModel")
public void downloadDrawingModel(@RequestParam String productionOrderNo,
@RequestParam String materialCode,
@RequestParam String fileType,
HttpServletResponse response) {
drawingFileQueryService.download(productionOrderNo, materialCode, fileType, response);
} }
@Log(title = "更新计划时间") @Log(title = "更新计划时间")
@ -719,7 +997,7 @@ public class ProcessRouteController extends BaseController {
log.info("读取文件名: " + originalFilename); log.info("读取文件名: " + originalFilename);
ExcelResult<ProcessRouteVo> result = ExcelUtil.importExcelSheet6(file.getInputStream(), ProcessRouteVo.class, true); ExcelResult<ProcessRouteVo> result = ExcelUtil.importExcelSheet6(file.getInputStream(), ProcessRouteVo.class, true);
List<ProcessRouteVo> list = result.getList(); List<ProcessRouteVo> list = result.getList();
List<ProcessRoute> list1 = iProcessRouteService.importDataTime(list); iProcessRouteService.importDataTime(list);
return R.ok("更新成功"); return R.ok("更新成功");
} }

View File

@ -13,8 +13,10 @@ import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushBo; import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushBo;
import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushNormalizer; import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushNormalizer;
import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.ProcessOrderProMapper;
import com.ruoyi.system.service.IDrawingRouteOperLogService;
import com.ruoyi.system.service.IPlmProcessRoutePushService; import com.ruoyi.system.service.IPlmProcessRoutePushService;
import com.ruoyi.system.service.IPlmReceiveLogService; import com.ruoyi.system.service.IPlmReceiveLogService;
import com.ruoyi.system.service.PlmPlanCompleteNotifyService;
import com.ruoyi.system.service.ProcessPlanExcelPushService; import com.ruoyi.system.service.ProcessPlanExcelPushService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -46,8 +48,11 @@ public class ProcessRoutePushReceiveController extends BaseController {
private final IPlmProcessRoutePushService plmProcessRoutePushService; private final IPlmProcessRoutePushService plmProcessRoutePushService;
private final IPlmReceiveLogService plmReceiveLogService; private final IPlmReceiveLogService plmReceiveLogService;
private final ProcessOrderProMapper processOrderProMapper; private final ProcessOrderProMapper processOrderProMapper;
/** 导出计划表落盘 + 按 plan-user-id 推送(与手动上传一致) */ /** 导出计划表落盘;总装已到才按 plan-user-id 推送 */
private final ProcessPlanExcelPushService processPlanExcelPushService; private final ProcessPlanExcelPushService processPlanExcelPushService;
/** 总装 BOM 与工艺都到齐后发给 plan-user-id */
private final PlmPlanCompleteNotifyService plmPlanCompleteNotifyService;
private final IDrawingRouteOperLogService drawingRouteOperLogService;
/** /**
* 接收一个生产令号下的物料、材料 BOM、工艺路线及工步数据。 * 接收一个生产令号下的物料、材料 BOM、工艺路线及工步数据。
@ -81,6 +86,8 @@ public class ProcessRoutePushReceiveController extends BaseController {
summaryMap.put("savedBomCount", summary.getSavedBomCount()); summaryMap.put("savedBomCount", summary.getSavedBomCount());
summaryMap.put("savedMaterialBomCount", summary.getSavedMaterialBomCount()); summaryMap.put("savedMaterialBomCount", summary.getSavedMaterialBomCount());
summaryMap.put("savedRouteCount", summary.getSavedRouteCount()); summaryMap.put("savedRouteCount", summary.getSavedRouteCount());
summaryMap.put("appended", summary.isAppended());
summaryMap.put("replaced", summary.isReplaced());
int saved = summary.getSavedBomCount() + summary.getSavedMaterialBomCount() + summary.getSavedRouteCount(); int saved = summary.getSavedBomCount() + summary.getSavedMaterialBomCount() + summary.getSavedRouteCount();
plmReceiveLogService.record( plmReceiveLogService.record(
IPlmReceiveLogService.BIZ_PROCESS_ROUTE, IPlmReceiveLogService.BIZ_PROCESS_ROUTE,
@ -95,8 +102,17 @@ public class ProcessRoutePushReceiveController extends BaseController {
null, null,
clientIp clientIp
); );
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_UPLOAD, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_PLM);
row.setApiPath(API_PATH);
row.setProductionOrderNo(summary.getProductionOrder());
row.setStatus(IDrawingRouteOperLogService.STATUS_SUCCESS);
row.setSummaryJson(JSON.toJSONString(summaryMap));
row.setOperator("PLM");
row.setClientIp(clientIp);
});
// 入库成功后异步:导出工艺计划表 → 落盘 → 推送给 plan-user-id(与手动上传同一套用户) // 入库成功后异步:导出落盘;有总装才发给 plan-user-id,没有就等总装到达
final String productionOrder = summary.getProductionOrder(); final String productionOrder = summary.getProductionOrder();
CompletableFuture.runAsync(() -> exportAndPushPlanExcel(productionOrder)); CompletableFuture.runAsync(() -> exportAndPushPlanExcel(productionOrder));
@ -119,18 +135,38 @@ public class ProcessRoutePushReceiveController extends BaseController {
e.getMessage(), e.getMessage(),
clientIp clientIp
); );
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_UPLOAD, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_PLM);
row.setApiPath(API_PATH);
row.setProductionOrderNo(orderNo);
row.setStatus(IDrawingRouteOperLogService.STATUS_FAIL);
row.setErrorMsg(e.getMessage());
row.setOperator("PLM");
row.setClientIp(clientIp);
});
throw e; throw e;
} }
} }
/** /**
* PLM 工艺入库后:按模板导出计划表,存到 app.plan-excel.base-dir,再按 plan-user-id 推送。 * PLM 工艺入库后:按模板导出计划表并落盘;总装已到才发给 plan-user-id。
*/ */
private void exportAndPushPlanExcel(String productionOrder) { private void exportAndPushPlanExcel(String productionOrder) {
long started = System.currentTimeMillis();
try { try {
ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder); ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder);
if (order == null || order.getId() == null) { if (order == null || order.getId() == null) {
log.warn("PLM 导出计划表跳过:项目不存在 productionOrder={}", productionOrder); log.warn("PLM 导出计划表跳过:项目不存在 productionOrder={}", productionOrder);
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_NOTIFY, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_PLM);
row.setApiPath(API_PATH);
row.setProductionOrderNo(productionOrder);
row.setNotifyType(IDrawingRouteOperLogService.NOTIFY_FILE);
row.setStatus(IDrawingRouteOperLogService.STATUS_FAIL);
row.setErrorMsg("项目不存在,跳过导出计划表");
row.setOperator("PLM");
row.setDurationMs(System.currentTimeMillis() - started);
});
return; return;
} }
// 复用现有导出(PLM 来源会走 generateRoute3ExcelFromPlm) // 复用现有导出(PLM 来源会走 generateRoute3ExcelFromPlm)
@ -139,12 +175,23 @@ public class ProcessRoutePushReceiveController extends BaseController {
File generated = new File(generatedPath); File generated = new File(generatedPath);
if (!generated.exists() || generated.length() <= 0) { if (!generated.exists() || generated.length() <= 0) {
log.warn("PLM 导出计划表失败:文件为空 path={}", generatedPath); log.warn("PLM 导出计划表失败:文件为空 path={}", generatedPath);
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_NOTIFY, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_PLM);
row.setApiPath(API_PATH);
row.setProjectId(order.getId());
row.setProductionOrderNo(productionOrder);
row.setNotifyType(IDrawingRouteOperLogService.NOTIFY_FILE);
row.setStatus(IDrawingRouteOperLogService.STATUS_FAIL);
row.setErrorMsg("导出计划表失败:文件为空");
row.setOperator("PLM");
row.setDurationMs(System.currentTimeMillis() - started);
});
return; return;
} }
// 落到固定目录,再推送;临时生成文件可删 // 落到固定目录;有总装才钉钉推送,没有就只落盘等总装
File saved = processPlanExcelPushService.savePlanExcel(productionOrder, generated, generated.getName()); File saved = processPlanExcelPushService.savePlanExcel(productionOrder, generated, generated.getName());
File toSend = saved != null ? saved : generated; File toSend = saved != null ? saved : generated;
processPlanExcelPushService.sendToPlanUsers(toSend, toSend.getName()); plmPlanCompleteNotifyService.notifyIfAssemblyAndRouteComplete(productionOrder, toSend);
if (saved != null && generated.exists() && !generated.getAbsolutePath().equals(saved.getAbsolutePath())) { if (saved != null && generated.exists() && !generated.getAbsolutePath().equals(saved.getAbsolutePath())) {
if (!generated.delete()) { if (!generated.delete()) {
log.warn("删除临时计划表失败: {}", generated.getAbsolutePath()); log.warn("删除临时计划表失败: {}", generated.getAbsolutePath());
@ -152,6 +199,16 @@ public class ProcessRoutePushReceiveController extends BaseController {
} }
} catch (Exception e) { } catch (Exception e) {
log.error("PLM 工艺推送后导出/推送计划表失败 productionOrder={}", productionOrder, e); log.error("PLM 工艺推送后导出/推送计划表失败 productionOrder={}", productionOrder, e);
drawingRouteOperLogService.record(IDrawingRouteOperLogService.BIZ_ROUTE_NOTIFY, row -> {
row.setChannel(IDrawingRouteOperLogService.CHANNEL_PLM);
row.setApiPath(API_PATH);
row.setProductionOrderNo(productionOrder);
row.setNotifyType(IDrawingRouteOperLogService.NOTIFY_FILE);
row.setStatus(IDrawingRouteOperLogService.STATUS_FAIL);
row.setErrorMsg(e.getMessage());
row.setOperator("PLM");
row.setDurationMs(System.currentTimeMillis() - started);
});
} }
} }

View File

@ -2,14 +2,20 @@ package com.ruoyi.system.controller;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO; import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO;
import com.ruoyi.system.domain.dto.ratio.RatioImportResultDTO;
import com.ruoyi.system.domain.dto.ratio.RatioModelOptionDTO;
import com.ruoyi.system.service.IRatioBomImportService;
import com.ruoyi.system.service.IRatioBuildCapacityService; import com.ruoyi.system.service.IRatioBuildCapacityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal; import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@ -17,11 +23,32 @@ import java.math.BigDecimal;
public class RatioBuildCapacityController { public class RatioBuildCapacityController {
private final IRatioBuildCapacityService ratioBuildCapacityService; private final IRatioBuildCapacityService ratioBuildCapacityService;
private final IRatioBomImportService ratioBomImportService;
@GetMapping("/ratios")
public R<List<String>> ratios(@RequestParam("seriesType") String seriesType) {
return R.ok(ratioBuildCapacityService.listRatios(seriesType));
}
@GetMapping("/models")
public R<List<RatioModelOptionDTO>> models(@RequestParam("seriesType") String seriesType,
@RequestParam("ratioValue") String ratioValue) {
return R.ok(ratioBuildCapacityService.listModels(seriesType, ratioValue));
}
@GetMapping("/buildCapacity") @GetMapping("/buildCapacity")
public R<RatioBuildCapacityDTO> buildCapacity(@RequestParam("ratioValue") BigDecimal ratioValue, public R<RatioBuildCapacityDTO> buildCapacity(@RequestParam("seriesType") String seriesType,
@RequestParam("ratioValue") String ratioValue,
@RequestParam("modelId") Long modelId,
@RequestParam("qty") Integer qty) { @RequestParam("qty") Integer qty) {
return R.ok(ratioBuildCapacityService.queryByRatio(ratioValue, qty)); return R.ok(ratioBuildCapacityService.queryByRatio(seriesType, ratioValue, modelId, qty));
}
@PostMapping("/import")
public R<RatioImportResultDTO> importBom(@RequestPart("file") MultipartFile file,
@RequestParam("seriesType") String seriesType) {
RatioImportResultDTO result = ratioBomImportService.importExcel(file, seriesType);
return R.ok(result.getMessage(), result);
} }
} }

View File

@ -37,6 +37,14 @@ public class ProcessOrderPro extends BaseEntity {
* 是否可以下载 * 是否可以下载
*/ */
private Integer isDownload; private Integer isDownload;
/**
* 图纸是否已齐:0否 1是。设计人员确认图纸上传完成后置1,方可下载 PDF;本批数量在下载时现算,不落库。
*/
private Integer drawingQtyIssued;
/**
* 出图模式:{@link com.ruoyi.system.constant.DrawingMode#NORMAL} / {@link com.ruoyi.system.constant.DrawingMode#PART};执行出图前必填
*/
private String drawingMode;
/** /**
* 是否做工艺:0否 1是 * 是否做工艺:0否 1是
*/ */
@ -89,7 +97,7 @@ public class ProcessOrderPro extends BaseEntity {
private String drawingPath; private String drawingPath;
/** /**
* 项目状态(0方案设计完成,1设计出图完成,2工艺待审核,3已推送BOM,4已推送工艺,5已核算MRP,6开始生产,7生产完成,8工艺审核完成,9工艺审核拒绝) * 项目状态(0方案设计完成,1设计出图完成,2工艺待审核,3已推送BOM,4已推送工艺,6开始生产,7生产完成,8工艺审核完成,9工艺审核拒绝)
*/ */
private Long bomStatus; private Long bomStatus;
/** /**
@ -129,7 +137,7 @@ public class ProcessOrderPro extends BaseEntity {
*/ */
private Date drawingTime; private Date drawingTime;
/** /**
* 出图人 * 设计人(字典 plan_proer,多人逗号分隔)
*/ */
private String drawingBy; private String drawingBy;
/** /**

View File

@ -7,6 +7,8 @@ import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@TableName("ratio_accessory") @TableName("ratio_accessory")
@ -17,16 +19,24 @@ public class RatioAccessory extends BaseEntity {
private Long ratioConfigId; private Long ratioConfigId;
/** 减速机型号ID,空表示该速比通用件 */
private Long ratioModelId;
private String materialCode; private String materialCode;
private String materialName; private String materialName;
private String danzhong; private String danzhong;
/** 材料 */
private String canzhi; private String canzhi;
/** 配件类别:通用件 / 轴套 / 行星架 / 法兰 */
private String partCategory; private String partCategory;
/** 单台用量 */
private BigDecimal bomQty;
private Integer status; private Integer status;
} }

View File

@ -17,7 +17,11 @@ public class RatioConfig extends BaseEntity {
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Long id; private Long id;
private BigDecimal ratioValue; /** 机型:KP08 / KP14 */
private String seriesType;
/** 速比值,例如 91B */
private String ratioValue;
private BigDecimal maxTorqueNm; private BigDecimal maxTorqueNm;

View File

@ -86,7 +86,7 @@ public class SfcOperationPlanningDetail extends BaseEntity {
*/ */
private String operator; private String operator;
/** /**
* 状态 * 工序状态(金蝶 FOperStatus 原始码:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭)
*/ */
private String operStatus; private String operStatus;
/** /**

View File

@ -66,4 +66,19 @@ public class SfcOperationPlanningMain extends BaseEntity {
*/ */
private Date planFinishTime; private Date planFinishTime;
/**
* 单据状态(来自生产订单 PRD_MO.FDocumentStatus):A创建 B审核中 C已审核 D重新审核 E暂存
*/
private String documentStatus;
/**
* 业务状态(来自生产订单 PRD_MO.FStatus):1计划 2计划确认 3下达 4开工 5完工 6结案 7结算
*/
private String billStatus;
/**
* 工序计划单据状态(SFC_OperationPlanning.FDocumentStatus)
*/
private String planDocumentStatus;
} }

View File

@ -81,31 +81,26 @@ public class FigureSaveBo extends BaseEntity {
/** /**
* 文件类型 * 文件类型
*/ */
@NotBlank(message = "文件类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String fileType; private String fileType;
/** /**
* 出图人 * 出图人
*/ */
@NotBlank(message = "出图人不能为空", groups = { AddGroup.class, EditGroup.class })
private String drawBy; private String drawBy;
/** /**
* 是否延期:0否 1是 * 是否延期:0否 1是
*/ */
@NotNull(message = "是否延期:0否 1是不能为空", groups = { AddGroup.class, EditGroup.class })
private Integer whetherDelay; private Integer whetherDelay;
/** /**
* 延期天数 * 延期天数
*/ */
@NotNull(message = "延期天数不能为空", groups = { AddGroup.class, EditGroup.class })
private Long delayDay; private Long delayDay;
/** /**
* 出图时间 * 出图时间(项目级维护,产品不再必填)
*/ */
@NotNull(message = "出图时间不能为空", groups = { AddGroup.class, EditGroup.class })
private Date drawTime; private Date drawTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

View File

@ -1,14 +1,18 @@
package com.ruoyi.system.domain.bo; package com.ruoyi.system.domain.bo;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.ruoyi.common.annotation.Translation; import com.ruoyi.common.annotation.Translation;
import com.ruoyi.common.constant.TransConstant; import com.ruoyi.common.constant.TransConstant;
import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.validate.AddGroup; import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.utils.StringUtils;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import javax.validation.constraints.*; import javax.validation.constraints.*;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -86,6 +90,16 @@ public class ProcessOrderProBo extends BaseEntity {
*/ */
private Integer isDownload; private Integer isDownload;
/**
* 图纸是否已齐:0否 1是
*/
private Integer drawingQtyIssued;
/**
* 出图模式:normal正常 / part零部件(执行出图前必填)
*/
private String drawingMode;
/** /**
* 是否做工艺:0否 1是 * 是否做工艺:0否 1是
*/ */
@ -131,14 +145,17 @@ public class ProcessOrderProBo extends BaseEntity {
/** /**
* 工艺上传更新时间 * 工艺上传更新时间
*/ */
@NotNull(message = "工艺上传更新不能为空", groups = { AddGroup.class })
private Date routeUptime; private Date routeUptime;
/** /**
* 排产计划更新时间 * 排产计划更新时间
*/ */
@NotNull(message = "排产计划更新不能为空", groups = { AddGroup.class })
private Date planUptime; private Date planUptime;
/** /**
* BOM/物料更新时间 * BOM/物料更新时间
*/ */
@NotNull(message = "BOM/物料更新不能为空", groups = { AddGroup.class })
private Date bomUptime; private Date bomUptime;
/** /**
* bom数量 * bom数量
@ -153,13 +170,64 @@ public class ProcessOrderProBo extends BaseEntity {
*/ */
private Long routeCount; private Long routeCount;
/** /**
* 出图时间 * 出图时间(项目整体;新增必填)
*/ */
@NotNull(message = "出图时间不能为空", groups = { AddGroup.class })
private Date drawingTime; private Date drawingTime;
/** /**
* 出图人 * 设计人(字典 plan_proer,多人逗号分隔)。
* 前端表单绑定的是 drawer 数组,提交时再折成本字段;校验看 drawingBy。
*/ */
@NotBlank(message = "设计人不能为空", groups = { AddGroup.class, EditGroup.class })
private String drawingBy; private String drawingBy;
/**
* JSON 反序列化设计人。
* {@code Nulls.SKIP}:请求体里 drawingBy 为 null 时不覆盖,避免把 drawer 已解析出的值冲掉。
*/
@JsonSetter(nulls = Nulls.SKIP)
public void setDrawingBy(String drawingBy) {
if (StringUtils.isNotBlank(drawingBy)) {
this.drawingBy = drawingBy.trim();
}
}
/**
* 兼容前端多选字段 drawer:仅当 drawingBy 仍为空时,把数组/字符串折成逗号分隔写入 drawingBy。
*/
@JsonSetter("drawer")
public void unpackDrawer(Object drawer) {
if (StringUtils.isNotBlank(this.drawingBy) || drawer == null) {
return;
}
String joined = joinDesignerValue(drawer);
if (StringUtils.isNotBlank(joined)) {
this.drawingBy = joined;
}
}
/** 将设计人数组或单个值规范为逗号分隔字符串;空值忽略。 */
private static String joinDesignerValue(Object value) {
if (value instanceof Collection) {
StringBuilder sb = new StringBuilder();
for (Object item : (Collection<?>) value) {
if (item == null) {
continue;
}
String s = String.valueOf(item).trim();
if (s.isEmpty() || "null".equalsIgnoreCase(s)) {
continue;
}
if (sb.length() > 0) {
sb.append(',');
}
sb.append(s);
}
return sb.length() == 0 ? null : sb.toString();
}
String s = String.valueOf(value).trim();
return s.isEmpty() || "null".equalsIgnoreCase(s) ? null : s;
}
/** /**
* 审核卡片Id * 审核卡片Id
*/ */

View File

@ -85,5 +85,13 @@ public class SfcOperationPlanningMainBo extends BaseEntity {
private String workCenterName; private String workCenterName;
/** 单据状态(生产订单 PRD_MO.FDocumentStatus) */
private String documentStatus;
/** 业务状态(生产订单 PRD_MO.FStatus) */
private String billStatus;
/** 工序计划单据状态(SFC_OperationPlanning.FDocumentStatus) */
private String planDocumentStatus;
} }

View File

@ -114,6 +114,10 @@ public final class PlmProcessRoutePushNormalizer {
private int savedMaterialBomCount; private int savedMaterialBomCount;
/** 实际写入 process_route 的行数(已过滤 PLM 占位工序) */ /** 实际写入 process_route 的行数(已过滤 PLM 占位工序) */
private int savedRouteCount; private int savedRouteCount;
/** 同令号已有工艺且本次总图号不同,追加写入 */
private boolean appended;
/** 同令号已有工艺且本次覆盖(同总图或无法比较总图) */
private boolean replaced;
private boolean received = true; private boolean received = true;
} }
} }

View File

@ -55,6 +55,36 @@ public class OperationPlanResultDTO {
*/ */
private Long fid; private Long fid;
/**
* 单据状态码(生产订单 PRD_MO.FDocumentStatus)
*/
private String documentStatus;
/**
* 单据状态名称
*/
private String documentStatusName;
/**
* 业务状态码(生产订单 PRD_MO.FStatus)
*/
private String billStatus;
/**
* 业务状态名称
*/
private String billStatusName;
/**
* 工序计划单据状态码(SFC_OperationPlanning.FDocumentStatus)
*/
private String planDocumentStatus;
/**
* 工序计划单据状态名称
*/
private String planDocumentStatusName;
/** /**
* 原生计划开始时间(内部排序用,不输出到JSON) * 原生计划开始时间(内部排序用,不输出到JSON)
*/ */

View File

@ -61,6 +61,12 @@ public class OperationPlanningDTO {
@JsonProperty("FBillNo") @JsonProperty("FBillNo")
private String fBillNo; private String fBillNo;
/**
* 工序计划单据状态:A创建 B审核中 C已审核 D重新审核 E暂存
*/
@JsonProperty("FDocumentStatus")
private String fDocumentStatus;
/** /**
* 工序号 * 工序号
*/ */

View File

@ -43,10 +43,18 @@ public class ProcessDetailDTO {
private Double ngQty; private Double ngQty;
/** /**
* 状态 * 工序状态码 FOperStatus:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭
*/
private String operStatus;
/**
* 业务展示状态(未开工/生产中/质检中/工序完成/已转出/已关闭)
*/ */
private String status; private String status;
/**
* 金蝶工序状态名称
*/
private String k3Status; private String k3Status;
/** /**
* 转出数量 * 转出数量

View File

@ -14,5 +14,9 @@ public class AssemblyBomReceiveSummary {
private int insertedCount; private int insertedCount;
/** 同令号已有数据且本次为全量/同主产品覆盖 */
private boolean replaced; private boolean replaced;
/** 同令号已有数据且本次主产品图号不同,追加写入 */
private boolean appended;
} }

View File

@ -10,7 +10,13 @@ public class RatioBuildCapacityDTO {
private Long ratioConfigId; private Long ratioConfigId;
private BigDecimal ratioValue; private String seriesType;
private String ratioValue;
private Long modelId;
private String modelCode;
private BigDecimal maxTorqueNm; private BigDecimal maxTorqueNm;
@ -18,6 +24,9 @@ public class RatioBuildCapacityDTO {
private Long maxBuildQty; private Long maxBuildQty;
/** 该型号没有轴套、行星架、法兰时,结果只有通用件。 */
private Boolean onlyCommonParts;
private List<RatioMaterialItemDTO> items; private List<RatioMaterialItemDTO> items;
} }

View File

@ -17,6 +17,9 @@ public class RatioMaterialItemDTO {
private String canzhi; private String canzhi;
/** Excel 中的单台数量 */
private Double bomQty;
private Double requiredQty; private Double requiredQty;
private Double onHandQty; private Double onHandQty;

View File

@ -1,13 +1,15 @@
package com.ruoyi.system.domain.vo; package com.ruoyi.system.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.convert.NullableDoubleConverter; import com.ruoyi.common.convert.NullableQuantityTextConverter;
import lombok.Data; import lombok.Data;
/** /**
* 电气物料BOM * 电气物料BOM
* 2025-09-25 * 2025-09-25
*/ */
@Data @Data
@ExcelIgnoreUnannotated
public class ElectricalMaterialBomVO { public class ElectricalMaterialBomVO {
/** /**
@ -33,22 +35,28 @@ public class ElectricalMaterialBomVO {
@ExcelProperty(value = "物料名称") @ExcelProperty(value = "物料名称")
private String drawingName; private String drawingName;
/** /**
* 型号 * 型号(部分模板表头为「规格型号」)
*/ */
@ExcelProperty(value = "型号") @ExcelProperty(value = "型号")
private String Model; private String model;
/**
* 规格型号(与「型号」二选一,导入时都会写入 material)
*/
@ExcelProperty(value = "规格型号")
private String specificationModel;
/** /**
* 品牌 * 品牌
*/ */
@ExcelProperty(value = "品牌") @ExcelProperty(value = "品牌")
private String Brand; private String brand;
/** /**
* 单台数量 * 单台数量。普通数字原样入库;文本分数如 1/4、1/3 在导入时拆成分子和分母。
*/ */
@ExcelProperty(value = "单台数量", converter = NullableDoubleConverter.class) @ExcelProperty(value = "单台数量", converter = NullableQuantityTextConverter.class)
private Double quantity; private String quantity;
/** /**
* 批次数量 * 批次数量
*/ */

View File

@ -3,8 +3,7 @@ package com.ruoyi.system.domain.vo;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.annotation.ExcelDictFormat; import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.common.convert.ExcelDictConvert;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import lombok.Data; import lombok.Data;
@ -43,8 +42,7 @@ public class ProcessOrderProExportVo {
@ExcelProperty(value = "生产令号") @ExcelProperty(value = "生产令号")
private String productionOrderNo; private String productionOrderNo;
@ExcelProperty(value = "项目状态", converter = ExcelDictConvert.class) @ExcelProperty(value = "项目状态")
@ExcelDictFormat(dictType = "process_status")
private String bomStatus; private String bomStatus;
@ExcelProperty(value = "项目名称") @ExcelProperty(value = "项目名称")
@ -101,7 +99,7 @@ public class ProcessOrderProExportVo {
return row; return row;
} }
row.setProductionOrderNo(vo.getProductionOrderNo()); row.setProductionOrderNo(vo.getProductionOrderNo());
row.setBomStatus(vo.getBomStatus() == null ? null : String.valueOf(vo.getBomStatus())); row.setBomStatus(vo.getBomStatus() == null ? null : ProjectStatusLock.statusLabel(vo.getBomStatus()));
row.setProductionName(vo.getProductionName()); row.setProductionName(vo.getProductionName());
row.setSource(formatSource(vo.getSource())); row.setSource(formatSource(vo.getSource()));
row.setWhetherDelayLabel(Integer.valueOf(1).equals(vo.getWhetherDelay()) ? "延期" : "正常"); row.setWhetherDelayLabel(Integer.valueOf(1).equals(vo.getWhetherDelay()) ? "延期" : "正常");

View File

@ -52,6 +52,18 @@ public class ProcessOrderProVo {
@ExcelProperty(value = "是否可以下载") @ExcelProperty(value = "是否可以下载")
private Integer isDownload; private Integer isDownload;
/**
* 图纸是否已齐:0否 1是
*/
@ExcelProperty(value = "图纸已齐")
private Integer drawingQtyIssued;
/**
* 出图模式:normal正常 / part零部件(执行出图前必填)
*/
@ExcelProperty(value = "出图模式")
private String drawingMode;
/** /**
* 是否做工艺:0否 1是 * 是否做工艺:0否 1是
*/ */
@ -151,8 +163,10 @@ public class ProcessOrderProVo {
*/ */
private Long routeCount; private Long routeCount;
/** /**
* 出图人 * 设计人(字典 plan_proer,多人逗号分隔)
*/ */
@ExcelProperty(value = "设计人", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "plan_proer")
private String drawingBy; private String drawingBy;
/** /**
* 出图时间 * 出图时间
@ -210,6 +224,18 @@ public class ProcessOrderProVo {
@ExcelIgnore @ExcelIgnore
private Boolean productionComplete; private Boolean productionComplete;
/**
* 设计出图进度(非库字段):进行中 / 延期 / 设计出图完成
*/
@ExcelIgnore
private String drawingProgress;
/**
* 等效 A4 合计(非库字段,来自图纸工作量)
*/
@ExcelProperty(value = "等效A4")
private Integer equivalentA4Total;
/** /**
* 列表/导出:金蝶订单状态摘要(非库字段) * 列表/导出:金蝶订单状态摘要(非库字段)
*/ */

View File

@ -86,5 +86,22 @@ public class SfcOperationPlanningMainVo {
@ExcelProperty(value = "计划结束时间") @ExcelProperty(value = "计划结束时间")
private Date planFinishTime; private Date planFinishTime;
/**
* 单据状态(生产订单 PRD_MO.FDocumentStatus)
*/
@ExcelProperty(value = "单据状态")
private String documentStatus;
/**
* 业务状态(生产订单 PRD_MO.FStatus)
*/
@ExcelProperty(value = "业务状态")
private String billStatus;
/**
* 工序计划单据状态(SFC_OperationPlanning.FDocumentStatus)
*/
@ExcelProperty(value = "工序计划单据状态")
private String planDocumentStatus;
} }

View File

@ -40,7 +40,6 @@ public class PlmProductionOrderVo {
* 2 工艺待审核 * 2 工艺待审核
* 3 已推送BOM * 3 已推送BOM
* 4 已推送工艺 * 4 已推送工艺
* 5 已核算MRP
* 6 开始生产 * 6 开始生产
* 7 生产完成 * 7 生产完成
* 8 工艺审核完成 * 8 工艺审核完成

View File

@ -1,8 +1,16 @@
package com.ruoyi.system.job; package com.ruoyi.system.job;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.helper.JobLoginContext;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.bo.SfcOperationPlanningMainBo; import com.ruoyi.system.domain.bo.SfcOperationPlanningMainBo;
import com.ruoyi.system.service.ISfcOperationPlanningMainService; import com.ruoyi.system.service.ISfcOperationPlanningMainService;
import com.ruoyi.system.service.ISysUserService;
import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -21,6 +29,7 @@ import org.springframework.stereotype.Component;
public class SfcOperationPlanningJob { public class SfcOperationPlanningJob {
private final ISfcOperationPlanningMainService iSfcOperationPlanningMainService; private final ISfcOperationPlanningMainService iSfcOperationPlanningMainService;
private final ISysUserService sysUserService;
/** /**
* 定时同步K3工序计划数据 * 定时同步K3工序计划数据
@ -30,8 +39,8 @@ public class SfcOperationPlanningJob {
public void syncOperationPlanningJobHandler() throws Exception { public void syncOperationPlanningJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, 开始同步K3工序计划数据..."); XxlJobHelper.log("XXL-JOB, 开始同步K3工序计划数据...");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
try { try {
loginAsAdmin();
Boolean result = iSfcOperationPlanningMainService.syncOperationPlanning(); Boolean result = iSfcOperationPlanningMainService.syncOperationPlanning();
if (result) { if (result) {
XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据成功!耗时: {}ms", (System.currentTimeMillis() - startTime)); XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据成功!耗时: {}ms", (System.currentTimeMillis() - startTime));
@ -44,9 +53,29 @@ public class SfcOperationPlanningJob {
log.error("同步K3工序计划数据异常", e); log.error("同步K3工序计划数据异常", e);
XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据异常: {}", e.getMessage()); XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据异常: {}", e.getMessage());
XxlJobHelper.handleFail("同步异常: " + e.getMessage()); XxlJobHelper.handleFail("同步异常: " + e.getMessage());
} finally {
JobLoginContext.logout();
} }
} }
/**
* 同步线程没有页面登录态,用管理员账号补上,创建人/更新人才能自动填充。
*/
private void loginAsAdmin() {
SysUser admin = sysUserService.selectUserById(UserConstants.ADMIN_ID);
if (admin == null || StringUtils.isBlank(admin.getUserName())) {
throw new ServiceException("未找到管理员账号,工序计划同步无法登录");
}
LoginUser loginUser = new LoginUser();
loginUser.setUserId(admin.getUserId());
loginUser.setDeptId(admin.getDeptId());
loginUser.setUsername(admin.getUserName());
loginUser.setUserType(StringUtils.isNotBlank(admin.getUserType())
? admin.getUserType() : UserType.SYS_USER.getUserType());
JobLoginContext.login(loginUser);
XxlJobHelper.log("XXL-JOB, 已以用户 {} 登录", loginUser.getUsername());
}
/** /**
* 项目进度推送:生成进度 Excel,用「信息发送」机器人私发给配置人员。 * 项目进度推送:生成进度 Excel,用「信息发送」机器人私发给配置人员。
* JobHandler:pushOperationPlanningProgress * JobHandler:pushOperationPlanningProgress

View File

@ -27,6 +27,10 @@ public class FileToZip {
private static void zipFileRecursive(File fileToZip, String fileName, ZipOutputStream zos) throws IOException { private static void zipFileRecursive(File fileToZip, String fileName, ZipOutputStream zos) throws IOException {
if (fileToZip.isHidden()) return; if (fileToZip.isHidden()) return;
// 跳过盖章残留的 *_temp.pdf,避免解压后出现双份
if (fileToZip.isFile() && fileToZip.getName().toLowerCase().endsWith("_temp.pdf")) {
return;
}
if (fileToZip.isDirectory()) { if (fileToZip.isDirectory()) {
if (!fileName.endsWith("/")) fileName += "/"; if (!fileName.endsWith("/")) fileName += "/";
zos.putNextEntry(new ZipEntry(fileName)); zos.putNextEntry(new ZipEntry(fileName));

View File

@ -14,7 +14,7 @@ public class CodeConverter {
//nameToCodeMap.put("焊接工段", "WC000003"); //nameToCodeMap.put("焊接工段", "WC000003");
nameToCodeMap.put("装一工段", "WC000004"); // 装配车间 nameToCodeMap.put("装一工段", "WC000004"); // 装配车间
nameToCodeMap.put("装二工段", "WC000005"); nameToCodeMap.put("装二工段", "WC000005");
nameToCodeMap.put("电钳工段", "WC000006"); nameToCodeMap.put("电钳工段", "WC000023");
nameToCodeMap.put("机加委外", "WC000008"); nameToCodeMap.put("机加委外", "WC000008");
nameToCodeMap.put("下料工段", "WC000010"); // 铆焊车间 nameToCodeMap.put("下料工段", "WC000010"); // 铆焊车间
nameToCodeMap.put("焊接工段", "WC000011"); nameToCodeMap.put("焊接工段", "WC000011");

View File

@ -16,6 +16,7 @@ import com.ruoyi.system.domain.BomDetails;
import com.ruoyi.system.domain.PartCost; import com.ruoyi.system.domain.PartCost;
import com.ruoyi.system.domain.dto.*; import com.ruoyi.system.domain.dto.*;
import com.ruoyi.system.domain.vo.*; import com.ruoyi.system.domain.vo.*;
import com.ruoyi.system.service.support.OperationPlanningResultAssembler;
import com.ruoyi.system.jdmain.rouplan.FidEntryIdDTO; import com.ruoyi.system.jdmain.rouplan.FidEntryIdDTO;
import com.ruoyi.system.jdmain.rouplan.Model; import com.ruoyi.system.jdmain.rouplan.Model;
import org.aspectj.bridge.MessageUtil; import org.aspectj.bridge.MessageUtil;
@ -854,7 +855,7 @@ public class JdUtil {
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject(); JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "FMaterialId.FNumber"); filterObject.addProperty("FieldName", "FMaterialId.FNumber");
filterObject.addProperty("Compare", "="); // 改为等号运算符 filterObject.addProperty("Compare", "=");
filterObject.addProperty("Value", materialCode); filterObject.addProperty("Value", materialCode);
filterObject.addProperty("Left", ""); filterObject.addProperty("Left", "");
filterObject.addProperty("Right", ""); filterObject.addProperty("Right", "");
@ -1125,20 +1126,21 @@ public class JdUtil {
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject(); JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "FMaterialId.FNumber"); filterObject.addProperty("FieldName", "FMaterialId.FNumber");
filterObject.addProperty("Compare", "="); // 改为等号运算符 filterObject.addProperty("Compare", "=");
filterObject.addProperty("Value", materialCode); filterObject.addProperty("Value", materialCode);
filterObject.addProperty("Left", ""); filterObject.addProperty("Left", "");
filterObject.addProperty("Right", ""); filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号 filterObject.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号
filterString.add(filterObject); filterString.add(filterObject);
JsonObject filterObject1 = new JsonObject(); // 整单未关闭
filterObject1.addProperty("FieldName", "FMRPCloseStatus"); JsonObject filterObjectClose = new JsonObject();
filterObject1.addProperty("Compare", "105"); // 改为等号运算符 filterObjectClose.addProperty("FieldName", "FCloseStatus");
filterObject1.addProperty("Value", "A"); filterObjectClose.addProperty("Compare", "105");
filterObject1.addProperty("Left", ""); filterObjectClose.addProperty("Value", "A");
filterObject1.addProperty("Right", ""); filterObjectClose.addProperty("Left", "");
filterObject1.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号 filterObjectClose.addProperty("Right", "");
filterString.add(filterObject1); filterObjectClose.addProperty("Logic", 0);
filterString.add(filterObjectClose);
json.add("FilterString", filterString); json.add("FilterString", filterString);
json.addProperty("OrderString", ""); json.addProperty("OrderString", "");
json.addProperty("TopRowCount", 0); json.addProperty("TopRowCount", 0);
@ -1194,14 +1196,15 @@ public class JdUtil {
filterObject.addProperty("Right", ""); filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 0); filterObject.addProperty("Logic", 0);
filterString.add(filterObject); filterString.add(filterObject);
JsonObject filterObject1 = new JsonObject(); // 整单未关闭
filterObject1.addProperty("FieldName", "FMRPCloseStatus"); JsonObject filterObjectClose = new JsonObject();
filterObject1.addProperty("Compare", "105"); filterObjectClose.addProperty("FieldName", "FCloseStatus");
filterObject1.addProperty("Value", "A"); filterObjectClose.addProperty("Compare", "105");
filterObject1.addProperty("Left", ""); filterObjectClose.addProperty("Value", "A");
filterObject1.addProperty("Right", ""); filterObjectClose.addProperty("Left", "");
filterObject1.addProperty("Logic", 0); filterObjectClose.addProperty("Right", "");
filterString.add(filterObject1); filterObjectClose.addProperty("Logic", 0);
filterString.add(filterObjectClose);
JsonObject filterObject2 = new JsonObject(); JsonObject filterObject2 = new JsonObject();
filterObject2.addProperty("FieldName", "F_UCHN_Text2"); filterObject2.addProperty("FieldName", "F_UCHN_Text2");
filterObject2.addProperty("Compare", "83"); filterObject2.addProperty("Compare", "83");
@ -4109,7 +4112,8 @@ public class JdUtil {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("FormId", "SFC_OperationPlanning"); json.addProperty("FormId", "SFC_OperationPlanning");
json.addProperty("FieldKeys", json.addProperty("FieldKeys",
"FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,F_HBYT_XMH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo,FOperNumber," + "FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,F_HBYT_XMH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo," +
"FDocumentStatus,FOperNumber," +
"FOperQty,FQualifiedQty,FTransOutQty,FUnqualifiedQty,FWastageQty,FTransInQty,FDepartmentId.FName,FWorkCenterId.FName," + "FOperQty,FQualifiedQty,FTransOutQty,FUnqualifiedQty,FWastageQty,FTransInQty,FDepartmentId.FName,FWorkCenterId.FName," +
"FProcessId.FName,F_HBYT_ZYRY,FOperStatus,FSeqPlanStartTime,FSeqPlanFinishTime,FOperPlanStartTime,FOperPlanFinishTime," + "FProcessId.FName,F_HBYT_ZYRY,FOperStatus,FSeqPlanStartTime,FSeqPlanFinishTime,FOperPlanStartTime,FOperPlanFinishTime," +
"FOptCtrlCodeId.FName,FKeyOper,FActivity1Name,FOperDescription,FPlanStartTime,FPlanFinishTime,F_HBYT_SB"); "FOptCtrlCodeId.FName,FKeyOper,FActivity1Name,FOperDescription,FPlanStartTime,FPlanFinishTime,F_HBYT_SB");
@ -4237,6 +4241,14 @@ public class JdUtil {
.filter(item -> item.getFid() != null) .filter(item -> item.getFid() != null)
.collect(java.util.stream.Collectors.groupingBy(OperationPlanningDTO::getFid)); .collect(java.util.stream.Collectors.groupingBy(OperationPlanningDTO::getFid));
// 生产订单单据状态/业务状态:均来自 PRD_MO(FDocumentStatus / FStatus)
Set<String> moNumbers = rawList.stream()
.map(OperationPlanningDTO::getFMoNumber)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
Map<String, String[]> moStatusMap = queryProductionOrderStatusMap(moNumbers);
List<OperationPlanResultDTO> resultList = new ArrayList<>(); List<OperationPlanResultDTO> resultList = new ArrayList<>();
for (Map.Entry<Long, List<OperationPlanningDTO>> entry : groupedMap.entrySet()) { for (Map.Entry<Long, List<OperationPlanningDTO>> entry : groupedMap.entrySet()) {
@ -4255,6 +4267,19 @@ public class JdUtil {
resultDTO.setPlanStartTime(firstItem.getFPlanStartTime()); resultDTO.setPlanStartTime(firstItem.getFPlanStartTime());
resultDTO.setPlanFinishTime(firstItem.getFPlanFinishTime()); resultDTO.setPlanFinishTime(firstItem.getFPlanFinishTime());
resultDTO.setFid(firstItem.getFid()); resultDTO.setFid(firstItem.getFid());
String[] moStatus = firstItem.getFMoNumber() == null
? null
: moStatusMap.get(firstItem.getFMoNumber().trim());
String moDocumentStatus = moStatus == null ? null : moStatus[0];
String billStatus = moStatus == null ? null : moStatus[1];
// 工序计划自身单据状态
resultDTO.setPlanDocumentStatus(firstItem.getFDocumentStatus());
resultDTO.setPlanDocumentStatusName(OperationPlanningResultAssembler.mapDocumentStatusName(firstItem.getFDocumentStatus()));
// 生产订单单据状态 / 业务状态
resultDTO.setDocumentStatus(moDocumentStatus);
resultDTO.setDocumentStatusName(OperationPlanningResultAssembler.mapDocumentStatusName(moDocumentStatus));
resultDTO.setBillStatus(billStatus);
resultDTO.setBillStatusName(OperationPlanningResultAssembler.mapBillStatusName(billStatus));
List<ProcessDetailDTO> processList = new ArrayList<>(); List<ProcessDetailDTO> processList = new ArrayList<>();
for (OperationPlanningDTO item : items) { for (OperationPlanningDTO item : items) {
@ -4284,26 +4309,9 @@ public class JdUtil {
detail.setK3DetailId(item.getFSubEntityFDetailId()); detail.setK3DetailId(item.getFSubEntityFDetailId());
detail.setEntityEntryId(item.getFEntityFEntryId()); detail.setEntityEntryId(item.getFEntityFEntryId());
// 状态映射:K3里4通常表示已完工 // 入库存原始 FOperStatus 码;展示名由 Assembler 统一映射
String statusStr = item.getFOperStatus(); detail.setOperStatus(item.getFOperStatus());
if ("0".equals(statusStr)) { OperationPlanningResultAssembler.enrichProcessDetailStatus(detail);
detail.setStatus("创建");
}
else if ("1".equals(statusStr)) {
detail.setStatus("计划");
} else if ("2".equals(statusStr)) {
detail.setStatus("计划确认");
} else if ("3".equals(statusStr)) {
detail.setStatus("下达");
} else if ("4".equals(statusStr)) {
detail.setStatus("开工");
} else if ("5".equals(statusStr)) {
detail.setStatus("完工");
} else if ("6".equals(statusStr)) {
detail.setStatus("关闭");
} else {
detail.setStatus(statusStr);
}
processList.add(detail); processList.add(detail);
} }
@ -4318,6 +4326,69 @@ public class JdUtil {
return resultList; return resultList;
} }
/**
* 按生产订单编号批量查 PRD_MO 的单据状态/业务状态。
*
* @return key=FBillNo,value=[FDocumentStatus, FStatus]
*/
private static Map<String, String[]> queryProductionOrderStatusMap(Collection<String> moNumbers) {
Map<String, String[]> result = new HashMap<>();
if (moNumbers == null || moNumbers.isEmpty()) {
return result;
}
List<String> list = moNumbers.stream()
.filter(StringUtils::isNotBlank)
.map(String::trim)
.distinct()
.collect(java.util.stream.Collectors.toList());
K3CloudApi client = new K3CloudApi();
ObjectMapper objectMapper = new ObjectMapper();
int batchSize = 200;
for (int i = 0; i < list.size(); i += batchSize) {
List<String> batch = list.subList(i, Math.min(i + batchSize, list.size()));
JsonObject json = new JsonObject();
json.addProperty("FormId", "PRD_MO");
json.addProperty("FieldKeys", "FBillNo,FDocumentStatus,FStatus");
JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "FBillNo");
filterObject.addProperty("Compare", "338");
filterObject.addProperty("Value", String.join(",", batch));
filterObject.addProperty("Left", "");
filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 0);
filterString.add(filterObject);
json.add("FilterString", filterString);
json.addProperty("OrderString", "");
json.addProperty("TopRowCount", 0);
json.addProperty("StartRow", 0);
json.addProperty("Limit", batch.size());
json.addProperty("SubSystemId", "");
try {
String resultJson = String.valueOf(client.billQuery(json.toString()));
JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class);
if (jsonArray == null || jsonArray.size() == 0) {
continue;
}
List<ProMoDTO> rows = objectMapper.readValue(jsonArray.toString(),
new TypeReference<List<ProMoDTO>>() {
});
if (rows == null) {
continue;
}
for (ProMoDTO row : rows) {
if (row == null || StringUtils.isBlank(row.getBillNo())) {
continue;
}
result.put(row.getBillNo().trim(), new String[]{row.getDocumentStatus(), row.getStatus()});
}
} catch (Exception e) {
log.warn("批量查询生产订单状态失败, size={}, err={}", batch.size(), e.getMessage());
}
}
return result;
}
// ===================== ND3 阶段性产品报表 · 金蝶查询 ===================== // ===================== ND3 阶段性产品报表 · 金蝶查询 =====================
@ -4555,7 +4626,7 @@ public class JdUtil {
"FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName"); "FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName");
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
filterString.add(nd3Filter("FMaterialId.FNumber", "=", materialCode)); filterString.add(nd3Filter("FMaterialId.FNumber", "=", materialCode));
filterString.add(nd3Filter("FMRPCloseStatus", "105", "A")); filterString.add(nd3Filter("FCloseStatus", "105", "A"));
filterString.add(nd3Filter("F_UCHN_Text2", "83", proCode)); filterString.add(nd3Filter("F_UCHN_Text2", "83", proCode));
json.add("FilterString", filterString); json.add("FilterString", filterString);
nd3BillQueryDefaults(json); nd3BillQueryDefaults(json);
@ -4573,7 +4644,7 @@ public class JdUtil {
"FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName"); "FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName");
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
filterString.add(nd3Filter("FMaterialId.FNumber", "=", materialCode)); filterString.add(nd3Filter("FMaterialId.FNumber", "=", materialCode));
filterString.add(nd3Filter("FMRPCloseStatus", "105", "A")); filterString.add(nd3Filter("FCloseStatus", "105", "A"));
json.add("FilterString", filterString); json.add("FilterString", filterString);
nd3BillQueryDefaults(json); nd3BillQueryDefaults(json);
return nd3ParseList(client, json, PurchaseOrderDTO.class, "PUR_PurchaseOrder", materialCode, null); return nd3ParseList(client, json, PurchaseOrderDTO.class, "PUR_PurchaseOrder", materialCode, null);

View File

@ -131,6 +131,14 @@ public class PDFGenerator {
} }
public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet, List<String> fontPaths) { public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet, List<String> fontPaths) {
return writeToPdf(combinedVoList, rooteProdet, fontPaths, null);
}
/**
* @param singlePdfFileName 非空时不打 zip,把本批 PDF 合成/直接返回该文件名(一个物料一份)
*/
public static String writeToPdf(List<CombinedDTO> combinedVoList, String rooteProdet, List<String> fontPaths,
String singlePdfFileName) {
String[] fontCandidates = (fontPaths == null || fontPaths.isEmpty()) String[] fontCandidates = (fontPaths == null || fontPaths.isEmpty())
? new String[] { ? new String[] {
"C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf" "C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf"
@ -380,6 +388,14 @@ public class PDFGenerator {
if (generatedPdfFiles.isEmpty()) { if (generatedPdfFiles.isEmpty()) {
throw new RuntimeException("目录中未找到已生成PDF: " + directoryPath); throw new RuntimeException("目录中未找到已生成PDF: " + directoryPath);
} }
// 指定了单物料文件名:一份物料一份 PDF,多张生产订单则合并
if (singlePdfFileName != null && !singlePdfFileName.trim().isEmpty()) {
try {
return packAsSinglePdf(directoryPath, generatedPdfFiles, singlePdfFileName.trim());
} catch (IOException e) {
throw new RuntimeException("合并单物料PDF失败: " + e.getMessage(), e);
}
}
// 生成ZIP文件 // 生成ZIP文件
String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip"; String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip";
try { try {
@ -395,6 +411,27 @@ public class PDFGenerator {
} }
/**
* 将已生成的多份 PDF 合成一份(同一物料多张金蝶生产订单时)。
*/
private static String packAsSinglePdf(String directoryPath, List<File> pdfFiles, String fileName) throws IOException {
File dest = new File(directoryPath, fileName);
if (pdfFiles.size() == 1) {
File src = pdfFiles.get(0);
if (!src.getCanonicalPath().equals(dest.getCanonicalPath())) {
Files.copy(src.toPath(), dest.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
return dest.getAbsolutePath();
}
PDFMergerUtility merger = new PDFMergerUtility();
for (File pdf : pdfFiles) {
merger.addSource(pdf);
}
merger.setDestinationFileName(dest.getAbsolutePath());
merger.mergeDocuments(null);
return dest.getAbsolutePath();
}
/** /**
* 创建ZIP文件 * 创建ZIP文件
* *

View File

@ -8,10 +8,20 @@ import com.ruoyi.system.domain.vo.DrawingWorkloadVo;
public interface IDrawingWorkloadService { public interface IDrawingWorkloadService {
/** /**
* 从 FTP 下载「{令号}图纸数量统计表.xlsx」,解析 Sheet1/Sheet2 后入库(按项目覆盖)。 * 从该令号对应目录下载「图纸数量统计表」并入库。目录中没有该表时工作量记为 0,不再用其它文件凑数。
*/ */
DrawingWorkloadVo syncFromFtp(Long orderProId); DrawingWorkloadVo syncFromFtp(Long orderProId);
/**
* 本机对应令号目录已有图纸数量统计表时解析入库;没有文件则返回 null。
*/
DrawingWorkloadVo syncFromLocalIfExists(Long orderProId);
/**
* 批量从 FTP 读取图纸工作量。不因单条失败中断。
*/
java.util.Map<String, Object> syncFromFtpBatch(java.util.Collection<Long> orderProIds);
/** /**
* 查询已入库的图纸工作量(含明细)。 * 查询已入库的图纸工作量(含明细)。
*/ */

View File

@ -16,12 +16,18 @@ public interface IPlmPdfZipPushService {
String savedDir, List<Map<String, String>> savedItems); String savedDir, List<Map<String, String>> savedItems);
/** /**
* 图纸 zip/批量推送时:若 process_order_pro 尚无该令号,则自动建项目(source=PLM)。 * 图纸 zip/批量推送:校验 process_order_pro 已存在该令号,否则抛「生产令号不存在」。
* 项目下尚无产品时,可补一条 figure_save(型号/名称/数量/类型/出图时间)。
* *
* @return 新建返回项目 id;已存在返回已有 id;令号为空返回 null * @return 已有项目 id
*/ */
Long ensureProcessOrderProForPlmPdf(String productionOrderNo); Long ensureProcessOrderProForPlmPdf(String productionOrderNo);
/**
* 同上;可用本次推送明细推断主产品型号/名称/数量(优先 .00 总装图)。
*/
Long ensureProcessOrderProForPlmPdf(String productionOrderNo, List<Map<String, String>> savedItems);
/** /**
* 清理总令号目录中与子令号重复的图纸(保留子令号侧): * 清理总令号目录中与子令号重复的图纸(保留子令号侧):
* <ul> * <ul>

View File

@ -9,7 +9,8 @@ import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushNormalizer;
public interface IPlmProcessRoutePushService { public interface IPlmProcessRoutePushService {
/** /**
* 将 PLM 推送的 BOM、材料 BOM、工艺路线写入本地库(同令号先删后插)。 * 将 PLM 推送的 BOM、材料 BOM、工艺路线写入本地库。
* 已审核且总图号(物料编码以 .00 结尾)相同则拒绝;总图号不同则追加;未审核且相同则覆盖该总图。
*/ */
PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary persist(PlmProcessRoutePushBo body); PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary persist(PlmProcessRoutePushBo body);
} }

View File

@ -17,6 +17,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -52,6 +53,11 @@ public interface IProcessOrderProService {
*/ */
Boolean updateByBo(ProcessOrderProBo bo); Boolean updateByBo(ProcessOrderProBo bo);
/**
* 批量设置项目状态(不校验设计人等整单字段)
*/
Boolean updateBomStatus(List<Long> ids, Long bomStatus);
/** /**
* 清空项目关联数据(保留项目本身) * 清空项目关联数据(保留项目本身)
*/ */
@ -72,6 +78,11 @@ public interface IProcessOrderProService {
void addProduct( RigidChainModelDTO orderPro); void addProduct( RigidChainModelDTO orderPro);
/**
* 设置项目出图模式(normal / part)。零部件模式下若已有产品且图纸路径不一致则拒绝。
*/
Boolean updateDrawingMode(Long id, String drawingMode);
String executDrawing(ProcessOrderProBo orderPro); String executDrawing(ProcessOrderProBo orderPro);
/** /**
@ -81,6 +92,16 @@ public interface IProcessOrderProService {
*/ */
R<String> uploadPDF(Long id); R<String> uploadPDF(Long id);
/**
* 下载流结束后清理本次出图临时目录(run_*),不影响其他人正在用的目录。
*/
void cleanupPdfRunDir(String zipPath);
/**
* 设计人员确认图纸已齐,允许下载 PDF(本批数量不落库,下载时按图纸推送明细取值)。
*/
R<Map<String, Object>> issueDrawingQty(Long id);
String uploadContractPDF(Integer id, String originalFilename, MultipartFile filePath); String uploadContractPDF(Integer id, String originalFilename, MultipartFile filePath);
List<OverdueProjectVo> getOverdueProjects(); List<OverdueProjectVo> getOverdueProjects();
@ -124,4 +145,9 @@ public interface IProcessOrderProService {
* 刷新并计算项目延期状态 * 刷新并计算项目延期状态
*/ */
void refreshDelayStatus(); void refreshDelayStatus();
/**
* 临时导入:按生产令号更新出图时间、项目完成时间、工艺完成时间、计划排产时间、BOM/物料更新、设计人
*/
Map<String, Object> importDrawingByExcel(MultipartFile file);
} }

View File

@ -58,16 +58,19 @@ public interface IProcessRouteService {
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid); Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
Boolean updateOrInsertRoute(List<ProcessRouteVo> bomDetailsVos); Boolean updateOrInsertRoute(List<ProcessRouteVo> bomDetailsVos);
/** /**
* 保存数据 * 保存数据
*/ */
boolean saveData(List<ProcessRouteVo> routeVoList, List<ProductionOrderVo> productionOrderVos); boolean saveData(List<ProcessRouteVo> routeVoList, List<ProductionOrderVo> productionOrderVos);
/** /**
* 查询组焊件集合 * 查询组焊件集合
*/ */
List<ProcessRoute> pushGroupWeldments(ProcessRoute processRoute); List<ProcessRoute> pushGroupWeldments(ProcessRoute processRoute);
/** /**
* 获取所有的项目令号 * 获取所有的项目令号
*/ */
@ -82,11 +85,19 @@ public interface IProcessRouteService {
* 查询工艺工序集合 * 查询工艺工序集合
*/ */
List<ProcessRouteXuDTO> getProcessRoute(String rooteProdet); List<ProcessRouteXuDTO> getProcessRoute(String rooteProdet);
/** /**
* 推送工艺路线 * 推送工艺路线
*/ */
R<ProcessRoutePushResultDTO> pushRouteBom(String rooteProdet,String groupName); R<ProcessRoutePushResultDTO> pushRouteBom(String rooteProdet, String groupName);
/**
* 按物料推送工艺路线(不改项目整单状态,不影响全量推送)。
*
* @param materialCodes 本项目下要推送的物料编码,不可空
*/
R<ProcessRoutePushResultDTO> pushRouteBomByMaterial(String rooteProdet, String groupName, Collection<String> materialCodes);
/** /**
* 获取所有的项目令号 * 获取所有的项目令号
@ -94,14 +105,18 @@ public interface IProcessRouteService {
List<CombinedDTO> getSelecPlanRouteList(String rooteProdet); List<CombinedDTO> getSelecPlanRouteList(String rooteProdet);
/** /**
* 生成这个项目的pdf * 生成工序计划单:物料编码为空时按令号打全部 PDF 压缩包;有物料编码时只生成该物料一份 PDF。
*
* @param materialCode 物料编码,可空
*/ */
String generatePDFs(String rooteProdet) throws IOException; String generatePDFs(String rooteProdet, String materialCode) throws IOException;
/** /**
* 更新计划时间 * 更新计划时间
*/ */
List<Model> updateProcessPlan(String rooteProdet) throws Exception; List<Model> updateProcessPlan(String rooteProdet) throws Exception;
/** /**
* 获取所有项目 * 获取所有项目
*/ */
@ -111,7 +126,7 @@ public interface IProcessRouteService {
R<Void> addRoute(List<ProcessRouteVo> list); R<Void> addRoute(List<ProcessRouteVo> list);
List<MaterialBom> getProcessMaterialList(String materialCode,String materialName, String productionOrderNo); List<MaterialBom> getProcessMaterialList(String materialCode, String materialName, String productionOrderNo);
List<ProcessRoute> deleteRoute(ProcessRoute processRoute); List<ProcessRoute> deleteRoute(ProcessRoute processRoute);
@ -122,18 +137,22 @@ public interface IProcessRouteService {
List<ProcessRouteSelectDTO> getSelectProcessRoute(String materilCode); List<ProcessRouteSelectDTO> getSelectProcessRoute(String materilCode);
List<ProcessRouteSelectDTO> getSelectStandProcessRoute(String materilCode); List<ProcessRouteSelectDTO> getSelectStandProcessRoute(String materilCode);
//根据项目令号删除 材料bom 总装bom 工艺路线 //根据项目令号删除 材料bom 总装bom 工艺路线
R<Void> delByProjectCode(String productionOrderNo); R<Void> delByProjectCode(String productionOrderNo);
List<ProcessRouteJdDTO> getProcessRouteList(String materialCode, String materialName, String productionOrderNo); List<ProcessRouteJdDTO> getProcessRouteList(String materialCode, String materialName, String productionOrderNo);
//获取物料的非委外的工作时长 //获取物料的非委外的工作时长
Double getFbWorkTime(BomDetails material); Double getFbWorkTime(BomDetails material);
Double getFaWorkTime(BomDetails material); Double getFaWorkTime(BomDetails material);
//根据令号和物料编码 查询工艺路线 //根据令号和物料编码 查询工艺路线
List<ProcessRoute> getProcessRoutesByOrder(String productionOrderNo, String materialCode); List<ProcessRoute> getProcessRoutesByOrder(String productionOrderNo, String materialCode);
//根据物料编码查询工艺路线 //根据物料编码查询工艺路线
List<ProcessRoute> getSingleWeght( String materialCode); List<ProcessRoute> getSingleWeght(String materialCode);
List<Model> updateProcesTime(String rooteProdet) throws Exception; List<Model> updateProcesTime(String rooteProdet) throws Exception;
@ -141,18 +160,19 @@ public interface IProcessRouteService {
List<MaterialBom> getProcessMaterialListAll(String routeDescription); List<MaterialBom> getProcessMaterialListAll(String routeDescription);
List<JDMaterialAndRoute> getProcessRouteGD(List<ProcessRouteGetDTO> list,String rooteProdet); List<JDMaterialAndRoute> getProcessRouteGD(List<ProcessRouteGetDTO> list, String rooteProdet);
//获取物料首个工序的工作中心 //获取物料首个工序的工作中心
String getRouteCode(String materialCode,String code); String getRouteCode(String materialCode, String code);
List<ProcessRoute> selectByProjectNumber(String productionOrderNo); List<ProcessRoute> selectByProjectNumber(String productionOrderNo);
R<String> viewGetBomUploadStatus(String rooteProdet); R<String> viewGetBomUploadStatus(String rooteProdet);
//根据令号和物料编码 查询工艺路线 //根据令号和物料编码 查询工艺路线
ProcessRoute getProcessRoutesXuTime(String productionOrderNo, String materialCode,String xu); ProcessRoute getProcessRoutesXuTime(String productionOrderNo, String materialCode, String xu);
List<String> updateProductionOrders(String rooteProdet,String cangKuNum) throws Exception; List<String> updateProductionOrders(String rooteProdet, String cangKuNum) throws Exception;
List<String> updateCgOrders(String rooteProdet, String cangKuNum) throws Exception; List<String> updateCgOrders(String rooteProdet, String cangKuNum) throws Exception;
} }

View File

@ -99,7 +99,8 @@ public interface IProductionOrderService {
Map<String, Double> resolveSingleWeightByMaterialCodes(Collection<String> materialCodes); Map<String, Double> resolveSingleWeightByMaterialCodes(Collection<String> materialCodes);
/** /**
* 接收 PLM 总装配 BOM JSON,按生产令号全量替换入库。 * 接收 PLM 总装配 BOM JSON。
* 已审核且主产品图号相同则拒绝;主产品图号不同则追加;未审核且相同则覆盖该主产品。
*/ */
AssemblyBomReceiveSummary receiveAssemblyBom(List<AssemblyBomReceiveItemDTO> items); AssemblyBomReceiveSummary receiveAssemblyBom(List<AssemblyBomReceiveItemDTO> items);

View File

@ -1,10 +1,16 @@
package com.ruoyi.system.service; package com.ruoyi.system.service;
import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO; import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO;
import com.ruoyi.system.domain.dto.ratio.RatioModelOptionDTO;
import java.math.BigDecimal; import java.util.List;
public interface IRatioBuildCapacityService { public interface IRatioBuildCapacityService {
RatioBuildCapacityDTO queryByRatio(BigDecimal ratioValue, Integer qty);
List<String> listRatios(String seriesType);
List<RatioModelOptionDTO> listModels(String seriesType, String ratioValue);
RatioBuildCapacityDTO queryByRatio(String seriesType, String ratioValue, Long modelId, Integer qty);
} }

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.dingding.robot.RobotGroupService; import com.ruoyi.common.dingding.robot.RobotGroupService;
import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.domain.KingdeePrdMo; import com.ruoyi.system.domain.KingdeePrdMo;
@ -202,6 +203,7 @@ public class KingdeeOrderReceiveService {
/** /**
* 金蝶推送入库后,回写项目令号:有生产/采购明细则对应标志置 1(已建)。 * 金蝶推送入库后,回写项目令号:有生产/采购明细则对应标志置 1(已建)。
* 只有本次含生产订单才把项目状态改为开始生产;仅采购订单不改状态。已生产完成的不回退。
*/ */
private void markProjectKingdeeOrderFlags(String productionOrderNo, int moCount, int poCount) { private void markProjectKingdeeOrderFlags(String productionOrderNo, int moCount, int poCount) {
if (moCount <= 0 && poCount <= 0) { if (moCount <= 0 && poCount <= 0) {
@ -209,7 +211,7 @@ public class KingdeeOrderReceiveService {
} }
ProcessOrderPro project = resolveProjectByProductionOrderNo(productionOrderNo); ProcessOrderPro project = resolveProjectByProductionOrderNo(productionOrderNo);
if (project == null) { if (project == null) {
log.warn("金蝶订单接收:未找到生产令号对应项目,跳过标志更新。推送令号={},请确认 process_order_pro.production_order_no 与金蝶 F_HBYT_SCLH 一致或为父令号", log.warn("金蝶订单接收:未找到生产令号对应项目,跳过标志与状态更新。推送令号={},请确认 process_order_pro.production_order_no 与金蝶令号完全一致",
productionOrderNo); productionOrderNo);
return; return;
} }
@ -221,42 +223,40 @@ public class KingdeeOrderReceiveService {
if (poCount > 0) { if (poCount > 0) {
uw.set(ProcessOrderPro::getNeedKingdeePurchaseOrder, 1); uw.set(ProcessOrderPro::getNeedKingdeePurchaseOrder, 1);
} }
boolean alreadyDone = project.getBomStatus() != null
&& project.getBomStatus() == ProjectStatusLock.PRODUCTION_DONE;
boolean startProduction = moCount > 0 && !alreadyDone;
if (startProduction) {
uw.set(ProcessOrderPro::getBomStatus, ProjectStatusLock.IN_PRODUCTION);
}
int rows = processOrderProMapper.update(null, uw); int rows = processOrderProMapper.update(null, uw);
if (rows <= 0) { if (rows <= 0) {
log.warn("金蝶订单接收:更新项目标志影响行数为 0,项目 id={},令号={}", log.warn("金蝶订单接收:更新项目标志影响行数为 0,项目 id={},令号={}",
project.getId(), project.getProductionOrderNo()); project.getId(), project.getProductionOrderNo());
return; return;
} }
if (!productionOrderNo.equals(project.getProductionOrderNo())) { log.info("金蝶订单接收:已更新项目 {}(id={})金蝶订单标志,生产 {} 条,采购 {} 条",
log.info("金蝶订单接收:推送令号 {} 已回写至父项目 {}(id={})", productionOrderNo, project.getId(), moCount, poCount);
productionOrderNo, project.getProductionOrderNo(), project.getId()); if (alreadyDone) {
log.info("金蝶订单接收:项目 {} 已是生产完成,不回退开始生产", project.getProductionOrderNo());
} else if (startProduction) {
log.info("金蝶订单接收:项目 {} 含生产订单,状态已更新为开始生产", project.getProductionOrderNo());
} else { } else {
log.info("金蝶订单接收:已更新项目 {} 金蝶订单标志,生产 {} 条,采购 {} 条", log.info("金蝶订单接收:项目 {} 仅采购订单,不更新为开始生产", project.getProductionOrderNo());
productionOrderNo, moCount, poCount);
} }
} }
/** /**
* 先精确匹配;若无则取「项目令号为金蝶令号前缀」的最长匹配(如 FB-26-038-FW-01 → FB-26-038-FW)。 * 仅按生产令号精确匹配项目,找不到则不更新。
*/ */
private ProcessOrderPro resolveProjectByProductionOrderNo(String productionOrderNo) { private ProcessOrderPro resolveProjectByProductionOrderNo(String productionOrderNo) {
if (StringUtils.isBlank(productionOrderNo)) { if (StringUtils.isBlank(productionOrderNo)) {
return null; return null;
} }
String key = productionOrderNo.trim(); return processOrderProMapper.selectOne(
ProcessOrderPro exact = processOrderProMapper.selectOne(
Wrappers.<ProcessOrderPro>lambdaQuery() Wrappers.<ProcessOrderPro>lambdaQuery()
.eq(ProcessOrderPro::getProductionOrderNo, key) .eq(ProcessOrderPro::getProductionOrderNo, productionOrderNo.trim())
.last("LIMIT 1")); .last("LIMIT 1"));
if (exact != null) {
return exact;
}
List<ProcessOrderPro> prefixMatches = processOrderProMapper.selectList(
Wrappers.<ProcessOrderPro>lambdaQuery()
.apply("{0} LIKE CONCAT(production_order_no, '%')", key)
.ne(ProcessOrderPro::getProductionOrderNo, "")
.last("ORDER BY LENGTH(production_order_no) DESC LIMIT 1"));
return prefixMatches.isEmpty() ? null : prefixMatches.get(0);
} }
private void notifyDingTalkGroup(List<OrderReceiveSummary> summaries, Date receiveTime) { private void notifyDingTalkGroup(List<OrderReceiveSummary> summaries, Date receiveTime) {
@ -275,13 +275,26 @@ public class KingdeeOrderReceiveService {
markdown.append("> **接收时间**:").append(sdf.format(receiveTime)).append("\n\n"); markdown.append("> **接收时间**:").append(sdf.format(receiveTime)).append("\n\n");
markdown.append("> **项目数量**:").append(summaries.size()).append(" 个\n\n"); markdown.append("> **项目数量**:").append(summaries.size()).append(" 个\n\n");
markdown.append("---\n\n"); markdown.append("---\n\n");
boolean hasProductionOrder = false;
boolean hasPurchaseOnly = false;
for (OrderReceiveSummary summary : summaries) { for (OrderReceiveSummary summary : summaries) {
markdown.append("> **生产令号**:").append(summary.productionOrderNo).append("\n\n"); markdown.append("> **生产令号**:").append(summary.productionOrderNo).append("\n\n");
markdown.append("> 生产订单 **").append(summary.moCount).append("** 条 · "); markdown.append("> 生产订单 **").append(summary.moCount).append("** 条 · ");
markdown.append("采购订单 **").append(summary.poCount).append("** 条\n\n"); markdown.append("采购订单 **").append(summary.poCount).append("** 条\n\n");
if (summary.moCount > 0) {
hasProductionOrder = true;
} else {
hasPurchaseOnly = true;
}
} }
markdown.append("---\n\n"); markdown.append("---\n\n");
markdown.append("金蝶生产、采购订单已审核并下达,请相关人员关注。"); if (hasProductionOrder && hasPurchaseOnly) {
markdown.append("含生产订单的项目状态已更新为开始生产;仅采购订单的项目状态不变。");
} else if (hasProductionOrder) {
markdown.append("金蝶生产订单已审核并下达,项目状态已更新为开始生产;请相关人员关注。");
} else {
markdown.append("金蝶采购订单已审核并下达;本次无生产订单,项目状态未改为开始生产。");
}
robotGroupService.sendMarkdown(conversationId, "金蝶订单接收通知", markdown.toString()); robotGroupService.sendMarkdown(conversationId, "金蝶订单接收通知", markdown.toString());
} catch (Exception e) { } catch (Exception e) {

View File

@ -7,10 +7,13 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ImMaterial;
import com.ruoyi.system.domain.bo.ImMaterialBo;
import com.ruoyi.system.domain.bo.KingdeeOrderTraceQueryBo; import com.ruoyi.system.domain.bo.KingdeeOrderTraceQueryBo;
import com.ruoyi.system.domain.bo.KingdeeWorkCenterDataBo; import com.ruoyi.system.domain.bo.KingdeeWorkCenterDataBo;
import com.ruoyi.system.domain.dto.JjSubcontractTransferDetailDTO; import com.ruoyi.system.domain.dto.JjSubcontractTransferDetailDTO;
import com.ruoyi.system.domain.dto.SubcontractOperationTransferRowDTO; import com.ruoyi.system.domain.dto.SubcontractOperationTransferRowDTO;
import com.ruoyi.system.domain.vo.ImMaterialVo;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@ -1222,5 +1225,238 @@ public class MssqlQueryService {
} }
return result; return result;
} }
private static final String KINGDEE_MATERIAL_FROM = ""
+ " FROM T_BD_MATERIAL m "
+ " LEFT JOIN T_BD_MATERIAL_L ml ON m.FMATERIALID = ml.FMATERIALID AND ml.FLOCALEID = 2052 "
+ " LEFT JOIN T_BD_MATERIALBASE b ON m.FMATERIALID = b.FMATERIALID "
+ " LEFT JOIN T_BD_UNIT_L ul ON b.FBASEUNITID = ul.FUNITID AND ul.FLOCALEID = 2052 "
+ " LEFT JOIN T_BD_MATERIALCATEGORY c ON b.FCATEGORYID = c.FCATEGORYID "
+ " LEFT JOIN T_BD_MATERIALPURCHASE p ON m.FMATERIALID = p.FMATERIALID "
+ " LEFT JOIN T_BAS_ASSISTANTDATAENTRY ass ON m.F_SVRI_ASSISTANT = ass.FENTRYID "
+ " LEFT JOIN T_BAS_ASSISTANTDATAENTRY_L assl ON ass.FENTRYID = assl.FENTRYID AND assl.FLOCALEID = 2052 ";
private static final String KINGDEE_MATERIAL_SELECT = ""
+ "SELECT m.FMATERIALID AS materialId, m.FNUMBER AS materialCode, ml.FNAME AS materialName, "
+ "COALESCE(NULLIF(LTRIM(RTRIM(ml.FSPECIFICATION)), ''), assl.FDATAVALUE) AS materialQuality, "
+ "m.F_HBYT_DZ AS singleWeight, ul.FNAME AS unit, "
+ "c.FNUMBER AS imCategory, b.FERPCLSID AS classificationNumber, "
+ "COALESCE(p.FISVMIBUSINESS, b.FISVMIBUSINESS) AS isVmi, "
+ "m.FMODIFYDATE AS modifyDate, m.FFORBIDSTATUS AS forbidStatus ";
/**
* 分页查询金蝶物料(T_BD_MATERIAL),仅未禁用。
*/
@DS("sqlserver")
public KingdeeMaterialPage queryMaterialPage(ImMaterialBo bo, int pageNum, int pageSize, boolean onlyEnabled) {
List<Object> args = new ArrayList<>();
String where = buildKingdeeMaterialWhere(bo, onlyEnabled, args);
Long total = jdbcTemplate.queryForObject(
"SELECT COUNT(1) " + KINGDEE_MATERIAL_FROM + where, args.toArray(), Long.class);
int offset = Math.max(pageNum - 1, 0) * pageSize;
List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(offset);
pageArgs.add(pageSize);
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM + where
+ " ORDER BY m.FMODIFYDATE DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY",
pageArgs.toArray());
KingdeeMaterialPage page = new KingdeeMaterialPage();
page.setTotal(total != null ? total : 0L);
page.setRecords(mapKingdeeMaterialVos(rows));
return page;
}
@DS("sqlserver")
public List<ImMaterialVo> queryMaterialList(ImMaterialBo bo, int limit, boolean onlyEnabled) {
List<Object> args = new ArrayList<>();
String where = buildKingdeeMaterialWhere(bo, onlyEnabled, args);
args.add(Math.max(limit, 1));
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM + where
+ " ORDER BY m.FMODIFYDATE DESC OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY",
args.toArray());
return mapKingdeeMaterialVos(rows);
}
@DS("sqlserver")
public KingdeeMaterialPage queryMaterialByKeyword(String keyword, int pageNum, int pageSize) {
List<Object> args = new ArrayList<>();
StringBuilder where = new StringBuilder(" WHERE m.FFORBIDSTATUS = 'A' ");
if (StringUtils.isNotBlank(keyword)) {
where.append(" AND (m.FNUMBER LIKE ? OR ml.FNAME LIKE ?) ");
String like = "%" + keyword.trim() + "%";
args.add(like);
args.add(like);
}
Long total = jdbcTemplate.queryForObject(
"SELECT COUNT(1) " + KINGDEE_MATERIAL_FROM + where, args.toArray(), Long.class);
int offset = Math.max(pageNum - 1, 0) * pageSize;
List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(offset);
pageArgs.add(pageSize);
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM + where
+ " ORDER BY m.FMODIFYDATE DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY",
pageArgs.toArray());
KingdeeMaterialPage page = new KingdeeMaterialPage();
page.setTotal(total != null ? total : 0L);
page.setRecords(mapKingdeeMaterialVos(rows));
return page;
}
@DS("sqlserver")
public ImMaterial findMaterialById(Long materialId) {
if (materialId == null) {
return null;
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM + " WHERE m.FMATERIALID = ?", materialId);
return rows.isEmpty() ? null : mapKingdeeMaterial(rows.get(0));
}
@DS("sqlserver")
public ImMaterial findMaterialByCode(String materialCode) {
if (StringUtils.isBlank(materialCode)) {
return null;
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM
+ " WHERE m.FNUMBER = ? ORDER BY CASE WHEN m.FFORBIDSTATUS = 'A' THEN 0 ELSE 1 END, m.FMODIFYDATE DESC",
materialCode.trim());
return rows.isEmpty() ? null : mapKingdeeMaterial(rows.get(0));
}
@DS("sqlserver")
public ImMaterial findMaterialByCodeAndName(String materialCode, String materialName) {
if (StringUtils.isBlank(materialCode) || StringUtils.isBlank(materialName)) {
return null;
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
KINGDEE_MATERIAL_SELECT + KINGDEE_MATERIAL_FROM
+ " WHERE m.FNUMBER = ? AND ml.FNAME = ?"
+ " ORDER BY CASE WHEN m.FFORBIDSTATUS = 'A' THEN 0 ELSE 1 END, m.FMODIFYDATE DESC",
materialCode.trim(), materialName.trim());
return rows.isEmpty() ? null : mapKingdeeMaterial(rows.get(0));
}
private String buildKingdeeMaterialWhere(ImMaterialBo bo, boolean onlyEnabled, List<Object> args) {
StringBuilder where = new StringBuilder(" WHERE 1=1 ");
if (onlyEnabled) {
where.append(" AND m.FFORBIDSTATUS = 'A' ");
}
if (bo == null) {
return where.toString();
}
appendLike(where, args, "m.FNUMBER", bo.getMaterialCode());
appendLike(where, args, "ml.FNAME", bo.getMaterialName());
if (StringUtils.isNotBlank(bo.getMaterialQuality())) {
where.append(" AND (ml.FSPECIFICATION LIKE ? OR assl.FDATAVALUE LIKE ?) ");
String qualityLike = "%" + bo.getMaterialQuality().trim() + "%";
args.add(qualityLike);
args.add(qualityLike);
}
appendLike(where, args, "ul.FNAME", bo.getUnit());
appendLike(where, args, "c.FNUMBER", bo.getImCategory());
if (bo.getSingleWeight() != null) {
where.append(" AND m.F_HBYT_DZ = ? ");
args.add(bo.getSingleWeight());
}
if (StringUtils.isNotBlank(bo.getClassificationName())) {
String vmi = bo.getClassificationName().trim();
if ("true".equalsIgnoreCase(vmi) || "1".equals(vmi)) {
where.append(" AND COALESCE(p.FISVMIBUSINESS, b.FISVMIBUSINESS) IN (1, '1', 'true') ");
} else if ("false".equalsIgnoreCase(vmi) || "0".equals(vmi)) {
where.append(" AND ISNULL(COALESCE(p.FISVMIBUSINESS, b.FISVMIBUSINESS), 0) IN (0, '0', 'false') ");
}
}
return where.toString();
}
private static void appendLike(StringBuilder where, List<Object> args, String column, String value) {
if (StringUtils.isBlank(value)) {
return;
}
where.append(" AND ").append(column).append(" LIKE ? ");
args.add("%" + value.trim() + "%");
}
private List<ImMaterialVo> mapKingdeeMaterialVos(List<Map<String, Object>> rows) {
List<ImMaterialVo> list = new ArrayList<>();
if (rows == null) {
return list;
}
for (Map<String, Object> row : rows) {
ImMaterial entity = mapKingdeeMaterial(row);
ImMaterialVo vo = new ImMaterialVo();
vo.setId(entity.getId());
vo.setMaterialCode(entity.getMaterialCode());
vo.setMaterialName(entity.getMaterialName());
vo.setMaterialQuality(entity.getMaterialQuality());
vo.setSingleWeight(entity.getSingleWeight());
vo.setUnit(entity.getUnit());
vo.setImCategory(entity.getImCategory());
vo.setClassificationName(entity.getClassificationName());
vo.setClassificationNumber(entity.getClassificationNumber());
list.add(vo);
}
return list;
}
private ImMaterial mapKingdeeMaterial(Map<String, Object> row) {
ImMaterial entity = new ImMaterial();
String materialId = emptyToNull(getString(row, "materialId"));
if (materialId != null) {
entity.setMaterialId(materialId);
try {
entity.setId(Long.valueOf(materialId));
} catch (NumberFormatException ignored) {
// 金蝶内码无法作为本地主键时忽略
}
}
entity.setMaterialCode(emptyToNull(getString(row, "materialCode")));
entity.setMaterialName(emptyToNull(getString(row, "materialName")));
entity.setMaterialQuality(emptyToNull(getString(row, "materialQuality")));
entity.setSingleWeight(getBigDecimalNullable(row, "singleWeight"));
entity.setUnit(emptyToNull(getString(row, "unit")));
entity.setImCategory(emptyToNull(getString(row, "imCategory")));
entity.setClassificationNumber(emptyToNull(getString(row, "classificationNumber")));
entity.setClassificationName(toVmiFlag(getCell(row, "isVmi")));
Object modifyDate = getCell(row, "modifyDate");
if (modifyDate != null) {
entity.setModifyDate(modifyDate.toString());
}
entity.setForbidStatus(emptyToNull(getString(row, "forbidStatus")));
return entity;
}
private BigDecimal getBigDecimalNullable(Map<String, Object> row, String key) {
Object val = getCell(row, key);
if (val == null) {
return null;
}
try {
return new BigDecimal(val.toString());
} catch (Exception e) {
return null;
}
}
private static String toVmiFlag(Object val) {
if (val == null) {
return "false";
}
String s = val.toString().trim();
if ("1".equals(s) || "true".equalsIgnoreCase(s)) {
return "true";
}
return "false";
}
@lombok.Data
public static class KingdeeMaterialPage {
private long total;
private List<ImMaterialVo> records;
}
} }

View File

@ -4,6 +4,7 @@ import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.dingding.media.MediaService; import com.ruoyi.common.dingding.media.MediaService;
import com.ruoyi.common.dingding.robot.RobotGroupService; import com.ruoyi.common.dingding.robot.RobotGroupService;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -19,7 +20,7 @@ import java.nio.file.StandardCopyOption;
/** /**
* 工艺计划表:落盘到服务器固定目录,并按 dingtalk.plan-user-id 推送钉钉文件。 * 工艺计划表:落盘到服务器固定目录,并按 dingtalk.plan-user-id 推送钉钉文件。
* <p> * <p>
* 手动上传计划表、PLM 推送工艺后导出,共用同一套目录与接收人。 * 手动上传计划表、PLM 总装与工艺到齐后导出,共用同一套目录与接收人。
*/ */
@Slf4j @Slf4j
@Service @Service
@ -61,19 +62,29 @@ public class ProcessPlanExcelPushService {
/** /**
* 按配置 dingtalk.plan-user-id(逗号分隔)私发文件,与手动上传计划表同一接收人逻辑。 * 按配置 dingtalk.plan-user-id(逗号分隔)私发文件,与手动上传计划表同一接收人逻辑。
*/ */
public void sendToPlanUsers(File file, String displayFileName) { public FilePushResult sendToPlanUsers(File file, String displayFileName) {
FilePushResult result = new FilePushResult();
result.setUserIds(dingTalkProperties.getPlanUserId());
if (file == null || !file.exists() || file.length() <= 0) { if (file == null || !file.exists() || file.length() <= 0) {
log.warn("推送计划表跳过:文件不存在或为空 file={}", file); log.warn("推送计划表跳过:文件不存在或为空 file={}", file);
return; result.setSkipped(true);
result.setSkipReason("文件不存在或为空");
result.setErrorMsg(result.getSkipReason());
return result;
} }
String planUserIds = dingTalkProperties.getPlanUserId(); String planUserIds = dingTalkProperties.getPlanUserId();
if (StringUtils.isBlank(planUserIds)) { if (StringUtils.isBlank(planUserIds)) {
log.warn("未配置钉钉工艺计划接收人(plan-user-id),跳过发送"); log.warn("未配置钉钉工艺计划接收人(plan-user-id),跳过发送");
return; result.setSkipped(true);
result.setSkipReason("未配置 plan-user-id");
result.setErrorMsg(result.getSkipReason());
return result;
} }
String name = StringUtils.isNotBlank(displayFileName) ? displayFileName : file.getName(); String name = StringUtils.isNotBlank(displayFileName) ? displayFileName : file.getName();
result.setFileName(name);
try { try {
String mediaId = mediaService.uploadMedia("file", file.getAbsolutePath()); String mediaId = mediaService.uploadMedia("file", file.getAbsolutePath());
result.setMediaId(mediaId);
for (String userId : planUserIds.split(",")) { for (String userId : planUserIds.split(",")) {
String cleanUserId = userId == null ? "" : userId.trim(); String cleanUserId = userId == null ? "" : userId.trim();
if (StringUtils.isBlank(cleanUserId)) { if (StringUtils.isBlank(cleanUserId)) {
@ -81,14 +92,92 @@ public class ProcessPlanExcelPushService {
} }
try { try {
robotGroupService.sendFileToUser(cleanUserId, mediaId, name); robotGroupService.sendFileToUser(cleanUserId, mediaId, name);
result.setSuccessCount(result.getSuccessCount() + 1);
log.info("工艺计划表已发送给用户: {} file={}", cleanUserId, name); log.info("工艺计划表已发送给用户: {} file={}", cleanUserId, name);
} catch (Exception e) { } catch (Exception e) {
result.setFailCount(result.getFailCount() + 1);
result.setErrorMsg(appendError(result.getErrorMsg(), cleanUserId + ": " + e.getMessage()));
log.error("发送工艺计划表给用户 {} 失败", cleanUserId, e); log.error("发送工艺计划表给用户 {} 失败", cleanUserId, e);
} }
} }
} catch (Exception e) { } catch (Exception e) {
result.setFailCount(result.getFailCount() + 1);
result.setErrorMsg(appendError(result.getErrorMsg(), e.getMessage()));
log.error("上传/推送工艺计划表失败 file={}", file.getAbsolutePath(), e); log.error("上传/推送工艺计划表失败 file={}", file.getAbsolutePath(), e);
} }
return result;
}
/**
* 按配置 dingtalk.notify-user-id(逗号分隔)私发文件,与出图完成发送计划表同一接收人。
*/
public FilePushResult sendToNotifyUsers(File file, String displayFileName) {
FilePushResult result = new FilePushResult();
result.setUserIds(dingTalkProperties.getNotifyUserId());
if (file == null || !file.exists() || file.length() <= 0) {
log.warn("推送计划表给 notify-user-id 跳过:文件不存在或为空 file={}", file);
result.setSkipped(true);
result.setSkipReason("文件不存在或为空");
result.setErrorMsg(result.getSkipReason());
return result;
}
String notifyUserIds = dingTalkProperties.getNotifyUserId();
if (StringUtils.isBlank(notifyUserIds)) {
log.warn("未配置钉钉出图完成接收人(notify-user-id),跳过发送");
result.setSkipped(true);
result.setSkipReason("未配置 notify-user-id");
result.setErrorMsg(result.getSkipReason());
return result;
}
String name = StringUtils.isNotBlank(displayFileName) ? displayFileName : file.getName();
result.setFileName(name);
try {
String mediaId = mediaService.uploadMedia("file", file.getAbsolutePath());
result.setMediaId(mediaId);
robotGroupService.sendFileToUser(notifyUserIds, mediaId, name);
result.setSuccessCount(1);
log.info("工艺计划表已发送给 notify-user-id file={}", name);
} catch (Exception e) {
result.setFailCount(1);
result.setErrorMsg(e.getMessage());
log.error("上传/推送工艺计划表给 notify-user-id 失败 file={}", file.getAbsolutePath(), e);
}
return result;
}
private static String appendError(String existing, String next) {
if (StringUtils.isBlank(next)) {
return existing;
}
if (StringUtils.isBlank(existing)) {
return next;
}
return existing + "; " + next;
}
@Data
public static class FilePushResult {
private boolean skipped;
private String skipReason;
private String fileName;
private String mediaId;
private String userIds;
private int successCount;
private int failCount;
private String errorMsg;
public String resolveStatus() {
if (skipped) {
return "FAIL";
}
if (failCount > 0 && successCount > 0) {
return "PARTIAL";
}
if (failCount > 0 || successCount <= 0) {
return "FAIL";
}
return "SUCCESS";
}
} }
public String getPlanExcelBaseDir() { public String getPlanExcelBaseDir() {

View File

@ -3,9 +3,12 @@ package com.ruoyi.system.service;
import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.controller.KingdeeWorkCenterDataController; import com.ruoyi.system.controller.KingdeeWorkCenterDataController;
import com.ruoyi.system.domain.WorkAlertGroupPushConfig;
import com.ruoyi.system.domain.WorkAlertPushConfig; import com.ruoyi.system.domain.WorkAlertPushConfig;
import com.ruoyi.system.mapper.WorkAlertGroupPushConfigMapper;
import com.ruoyi.system.mapper.WorkAlertPushConfigMapper; import com.ruoyi.system.mapper.WorkAlertPushConfigMapper;
import com.ruoyi.system.service.support.WorkAlertPushDeliveryHolder; import com.ruoyi.system.service.support.WorkAlertPushDeliveryHolder;
import com.ruoyi.system.service.support.WorkAlertPushRecipientRulesHelper; import com.ruoyi.system.service.support.WorkAlertPushRecipientRulesHelper;
@ -37,6 +40,7 @@ public class WorkAlertPushScheduleService {
private static final DateTimeFormatter HH_MM = DateTimeFormatter.ofPattern("HH:mm"); private static final DateTimeFormatter HH_MM = DateTimeFormatter.ofPattern("HH:mm");
private final WorkAlertPushConfigMapper configMapper; private final WorkAlertPushConfigMapper configMapper;
private final WorkAlertGroupPushConfigMapper groupConfigMapper;
private final KingdeeWorkCenterDataController kingdeeWorkCenterDataController; private final KingdeeWorkCenterDataController kingdeeWorkCenterDataController;
private final DingTalkProperties dingTalkProperties; private final DingTalkProperties dingTalkProperties;
@ -48,11 +52,34 @@ public class WorkAlertPushScheduleService {
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
List<WorkAlertPushConfig> configs = configMapper.selectList(null); List<WorkAlertPushConfig> configs = configMapper.selectList(null);
if (configs != null) {
for (WorkAlertPushConfig config : configs) {
if (!isEnabled(config)) {
continue;
}
if (!nowTime.equals(normalizePushTime(config.getPushTime()))) {
continue;
}
if (alreadyPushedToday(config.getLastPushDate(), today)) {
continue;
}
try {
executePush(config, false);
} catch (Exception e) {
log.error("定时推送失败 pushCode={} id={}", config.getPushCode(), config.getId(), e);
}
}
}
runDueGroupPushes(nowTime, today);
}
private void runDueGroupPushes(String nowTime, LocalDate today) {
List<WorkAlertGroupPushConfig> configs = groupConfigMapper.selectList(null);
if (configs == null || configs.isEmpty()) { if (configs == null || configs.isEmpty()) {
return; return;
} }
for (WorkAlertPushConfig config : configs) { for (WorkAlertGroupPushConfig config : configs) {
if (!isEnabled(config)) { if (!isEnabled(config.getEnabled())) {
continue; continue;
} }
if (!nowTime.equals(normalizePushTime(config.getPushTime()))) { if (!nowTime.equals(normalizePushTime(config.getPushTime()))) {
@ -62,13 +89,61 @@ public class WorkAlertPushScheduleService {
continue; continue;
} }
try { try {
executePush(config, false); executeGroupSchedule(config, false);
} catch (Exception e) { } catch (Exception e) {
log.error("定时推送失败 pushCode={} id={}", config.getPushCode(), config.getId(), e); log.error("指定群定时推送失败 id={} conversationId={}", config.getId(), config.getOpenConversationId(), e);
} }
} }
} }
/**
* 按保存的群定时配置推送(可多任务)。不走私发。
*/
public void executeGroupSchedule(WorkAlertGroupPushConfig config, boolean manual) {
if (config == null) {
throw new ServiceException("配置为空");
}
if (!manual && !isEnabled(config.getEnabled())) {
throw new ServiceException("配置未启用");
}
List<String> codes = parsePushCodes(config.getPushCodes());
if (codes.isEmpty()) {
throw new ServiceException("未配置推送任务");
}
log.info("开始指定群定时推送 id={} manual={} time={} conversationId={} codes={}",
config.getId(), manual, config.getPushTime(), config.getOpenConversationId(), codes.size());
int ok = 0;
int fail = 0;
StringBuilder failMsg = new StringBuilder();
for (String code : codes) {
try {
executeGroupPush(code, config.getOpenConversationId(), config.getRobotCode());
ok++;
} catch (Exception e) {
fail++;
log.error("指定群定时推送单项失败 id={} pushCode={}", config.getId(), code, e);
if (failMsg.length() > 0) {
failMsg.append(";");
}
failMsg.append(code).append(": ").append(e.getMessage());
}
}
if (ok > 0 || fail == 0) {
WorkAlertGroupPushConfig update = new WorkAlertGroupPushConfig();
update.setId(config.getId());
update.setLastPushDate(Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()));
groupConfigMapper.updateById(update);
}
log.info("指定群定时推送结束 id={} ok={} fail={}", config.getId(), ok, fail);
if (fail > 0) {
String msg = "推送完成:成功 " + ok + ",失败 " + fail + "。" + failMsg;
if (manual) {
throw new ServiceException(msg);
}
log.warn("指定群定时推送部分失败 id={} {}", config.getId(), msg);
}
}
public void executePush(WorkAlertPushConfig config, boolean manual) { public void executePush(WorkAlertPushConfig config, boolean manual) {
if (config == null) { if (config == null) {
throw new IllegalArgumentException("配置为空"); throw new IllegalArgumentException("配置为空");
@ -114,6 +189,39 @@ public class WorkAlertPushScheduleService {
log.info("预警推送完成 pushCode={} id={}", config.getPushCode(), config.getId()); log.info("预警推送完成 pushCode={} id={}", config.getPushCode(), config.getId());
} }
/**
* 将与预警推送配置相同的数据发到指定钉钉群。不走私发、不影响配置表上次推送日期。
*/
public void executeGroupPush(String pushCode, String conversationId, String robotCode) {
if (!WorkAlertPushTypeRegistry.isSupported(pushCode)) {
throw new ServiceException("不支持的推送编码:" + pushCode);
}
if (!WorkAlertPushTypeRegistry.supportsGroupConversation(pushCode)) {
throw new ServiceException("该任务不支持指定群会话ID:" + pushCode);
}
if (StringUtils.isBlank(conversationId)) {
throw new ServiceException("群会话ID不能为空");
}
String resolvedRobot = StringUtils.isNotBlank(robotCode)
? robotCode.trim()
: dingTalkProperties.getDefaultAlertPushRobotCode();
String cid = conversationId.trim();
log.info("指定群预警推送 pushCode={} conversationId={} robotCode={}", pushCode, cid, resolvedRobot);
try {
R<?> result = invokePush(pushCode, cid, null, resolvedRobot);
if (result == null || !R.isSuccess(result)) {
String msg = result == null ? "无返回" : result.getMsg();
throw new ServiceException("推送执行失败:" + msg);
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("指定群预警推送失败 pushCode={}", pushCode, e);
throw new ServiceException("推送执行失败:" + e.getMessage());
}
log.info("指定群预警推送完成 pushCode={}", pushCode);
}
private R<?> invokePush(String pushCode, String conversationId, String primaryUserId, String robotCode) { private R<?> invokePush(String pushCode, String conversationId, String primaryUserId, String robotCode) {
switch (pushCode) { switch (pushCode) {
case "getKuCun": case "getKuCun":
@ -222,8 +330,23 @@ public class WorkAlertPushScheduleService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
static List<String> parsePushCodes(String raw) {
if (StringUtils.isBlank(raw)) {
return new ArrayList<>();
}
return Arrays.stream(raw.split("[,,;;\\s]+"))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
private static boolean isEnabled(WorkAlertPushConfig config) { private static boolean isEnabled(WorkAlertPushConfig config) {
return config != null && "1".equals(StringUtils.trim(config.getEnabled())); return config != null && isEnabled(config.getEnabled());
}
private static boolean isEnabled(String enabled) {
return "1".equals(StringUtils.trim(enabled));
} }
private static String normalizePushTime(String pushTime) { private static String normalizePushTime(String pushTime) {

View File

@ -71,12 +71,74 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
} }
String productionCode = orderPro.getProductionOrderNo().trim(); String productionCode = orderPro.getProductionOrderNo().trim();
String localFile = downloadDrawingCountExcel(productionCode); String localFile = downloadDrawingCountExcel(productionCode);
if (StringUtils.isBlank(localFile)) {
log.info("令号 {} 对应目录未找到图纸数量统计表,工作量记为0", productionCode);
return saveEmptyWorkload(orderPro);
}
ParsedExcel parsed = parseExcel(localFile); ParsedExcel parsed = parseExcel(localFile);
DrawingWorkload saved = saveWorkload(orderPro, parsed); DrawingWorkload saved = saveWorkload(orderPro, parsed);
return toVo(saved, true); return toVo(saved, true);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public DrawingWorkloadVo syncFromLocalIfExists(Long orderProId) {
ProcessOrderPro orderPro = processOrderProMapper.selectById(orderProId);
if (orderPro == null || StringUtils.isBlank(orderPro.getProductionOrderNo())) {
return null;
}
File localFile = findLocalDrawingCountExcel(orderPro.getProductionOrderNo().trim());
if (localFile == null) {
return null;
}
ParsedExcel parsed = parseExcel(localFile.getAbsolutePath());
DrawingWorkload saved = saveWorkload(orderPro, parsed);
return toVo(saved, true);
}
@Override
public Map<String, Object> syncFromFtpBatch(java.util.Collection<Long> orderProIds) {
Map<String, Object> result = new HashMap<>();
int success = 0;
int fail = 0;
int skip = 0;
List<String> failDetails = new ArrayList<>();
if (orderProIds == null || orderProIds.isEmpty()) {
result.put("success", 0);
result.put("fail", 0);
result.put("skip", 0);
result.put("failDetails", failDetails);
result.put("message", "没有可读取的项目");
return result;
}
for (Long id : orderProIds) {
if (id == null) {
skip++;
continue;
}
try {
DrawingWorkloadVo local = syncFromLocalIfExists(id);
if (local != null && (local.getA4Count() != null || local.getEquivalentA4Total() != null)) {
success++;
continue;
}
syncFromFtp(id);
success++;
} catch (Exception e) {
fail++;
failDetails.add("项目ID " + id + ":" + e.getMessage());
log.warn("批量读取图纸工作量失败 orderProId={}", id, e);
}
}
result.put("success", success);
result.put("fail", fail);
result.put("skip", skip);
result.put("failDetails", failDetails);
result.put("message", "读取完成:成功" + success + "条,失败" + fail + "条");
return result;
}
@Override @Override
public DrawingWorkloadVo getByOrderProId(Long orderProId) { public DrawingWorkloadVo getByOrderProId(Long orderProId) {
DrawingWorkload row = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery() DrawingWorkload row = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery()
@ -92,39 +154,63 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
String year = ProductionOrderNoUtils.extractYear(productionCode); String year = ProductionOrderNoUtils.extractYear(productionCode);
String preferredName = productionCode + "图纸数量统计表.xlsx"; String preferredName = productionCode + "图纸数量统计表.xlsx";
String fallbackName = "图纸数量统计表.xlsx"; String fallbackName = "图纸数量统计表.xlsx";
String localPreferred = LOCAL_PATH + preferredName; File preferred = new File(LOCAL_PATH + preferredName);
File fallback = new File(LOCAL_PATH + fallbackName);
String ftpPath = FtpUtil.buildDrawingExcelRemoteDir(drawingBasePath, year, productionCode); String ftpPath = FtpUtil.buildDrawingExcelRemoteDir(drawingBasePath, year, productionCode);
log.info("图纸数量统计表 FTP 下载路径: {} (basePath={})", ftpPath, drawingBasePath); log.info("图纸数量统计表 FTP 下载路径: {} (basePath={})", ftpPath, drawingBasePath);
R<String> result = tryDownloadExcel(ftpPath, preferredName, preferredName); StringBuilder lastMsg = new StringBuilder();
if (R.isError(result)) { if (downloadOwnedExcel(ftpPath, preferredName, fallbackName, preferred, fallback, lastMsg)) {
result = tryDownloadExcel(ftpPath, fallbackName, preferredName); return preferred.getAbsolutePath();
} }
if (R.isError(result)) { String noDrivePath = FtpUtil.buildDrawingExcelRemoteDir("", year, productionCode);
String noDrivePath = FtpUtil.buildDrawingExcelRemoteDir("", year, productionCode); if (!noDrivePath.equals(ftpPath)) {
if (!noDrivePath.equals(ftpPath)) { log.info("尝试无盘符 FTP 路径: {}", noDrivePath);
log.info("尝试无盘符 FTP 路径: {}", noDrivePath); if (downloadOwnedExcel(noDrivePath, preferredName, fallbackName, preferred, fallback, lastMsg)) {
result = tryDownloadExcel(noDrivePath, preferredName, preferredName); return preferred.getAbsolutePath();
if (R.isError(result)) {
result = tryDownloadExcel(noDrivePath, fallbackName, preferredName);
}
if (R.isSuccess(result)) {
ftpPath = noDrivePath;
}
} }
ftpPath = noDrivePath;
}
log.info("项目 {} 对应目录未找到图纸数量统计表,FTP路径={},期望文件名={} 或 {},{}",
productionCode, ftpPath, preferredName, fallbackName, lastMsg);
return null;
}
/**
* 只接受本次从该令号目录真正下载到的文件,禁止把 D:\file 里其它令号的统计表改名冒充当前项目。
*/
private boolean downloadOwnedExcel(String ftpPath, String preferredName, String fallbackName,
File preferred, File fallback, StringBuilder lastMsg) {
Snapshot preferredSnap = snapshot(preferred);
R<String> result = tryDownloadExcel(ftpPath, preferredName, preferredName);
rememberMsg(lastMsg, result);
if (touchedSince(preferred, preferredSnap)) {
return true;
} }
// 目录扫描可能按远程原名落盘,统一落到 preferred 本地名 Snapshot fallbackSnap = snapshot(fallback);
ensureLocalPreferredName(preferredName, fallbackName); result = tryDownloadExcel(ftpPath, fallbackName, preferredName);
rememberMsg(lastMsg, result);
File file = new File(localPreferred); if (touchedSince(preferred, preferredSnap)) {
if (!file.exists() || file.length() == 0) { return true;
throw new ServiceException("项目 " + productionCode + " 图纸数量统计表下载失败或不存在,FTP路径="
+ ftpPath + ",期望文件名=" + preferredName + " 或 " + fallbackName + ","
+ (result == null ? "" : result.getMsg()));
} }
return file.getAbsolutePath(); if (touchedSince(fallback, fallbackSnap)) {
renameToPreferred(fallback, preferred);
return true;
}
if (R.isSuccess(result)) {
log.warn("FTP 返回成功但未拿到当前令号图纸数量统计表 path={} msg={}", ftpPath, result.getMsg());
}
return false;
}
private static void rememberMsg(StringBuilder lastMsg, R<String> result) {
if (lastMsg == null || result == null || StringUtils.isBlank(result.getMsg())) {
return;
}
lastMsg.setLength(0);
lastMsg.append(result.getMsg());
} }
private R<String> tryDownloadExcel(String ftpPath, String remoteName, String localName) { private R<String> tryDownloadExcel(String ftpPath, String remoteName, String localName) {
@ -138,40 +224,66 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
return result; return result;
} }
private void ensureLocalPreferredName(String preferredName, String fallbackName) { private void renameToPreferred(File source, File preferred) {
File preferred = new File(LOCAL_PATH + preferredName); if (preferred.exists() && !preferred.delete()) {
if (preferred.exists() && preferred.length() > 0) { throw new ServiceException("图纸数量统计表本地文件覆盖失败");
return;
} }
File fallback = new File(LOCAL_PATH + fallbackName); if (!source.renameTo(preferred)) {
if (fallback.exists() && fallback.length() > 0) { throw new ServiceException("图纸数量统计表本地文件重命名失败: " + source.getName());
if (preferred.exists()) {
//noinspection ResultOfMethodCallIgnored
preferred.delete();
}
if (!fallback.renameTo(preferred)) {
throw new ServiceException("图纸数量统计表本地文件重命名失败");
}
return;
} }
File dir = new File(LOCAL_PATH); }
File[] matches = dir.listFiles((d, name) ->
name != null && name.contains("图纸数量统计表") && name.toLowerCase().endsWith(".xlsx")); /**
if (matches == null || matches.length == 0) { * 只认该令号自己的目录里的表。D:\file 是所有项目共用缓存,不能当“共享盘上有这份表”的证据。
return; */
private File findLocalDrawingCountExcel(String productionCode) {
String preferredName = productionCode + "图纸数量统计表.xlsx";
String fallbackName = "图纸数量统计表.xlsx";
List<File> candidates = new ArrayList<>();
try {
String year = ProductionOrderNoUtils.extractYear(productionCode);
File shareDir = new File(drawingBasePath, year + File.separator + productionCode);
candidates.add(new File(shareDir, preferredName));
candidates.add(new File(shareDir, fallbackName));
File dDriveDir = new File("D:" + File.separator + year + File.separator + productionCode);
candidates.add(new File(dDriveDir, preferredName));
candidates.add(new File(dDriveDir, fallbackName));
} catch (Exception e) {
log.debug("令号 {} 无法解析年份,跳过本机图纸数量统计表查找", productionCode);
} }
File best = matches[0]; for (File file : candidates) {
for (File f : matches) { if (isUsable(file)) {
if (f.lastModified() > best.lastModified()) { return file;
best = f;
} }
} }
if (preferred.exists()) { return null;
//noinspection ResultOfMethodCallIgnored }
preferred.delete();
private static boolean isUsable(File file) {
return file != null && file.isFile() && file.length() > 0;
}
private static Snapshot snapshot(File file) {
if (!isUsable(file)) {
return new Snapshot(-1L, -1L);
} }
if (!best.getName().equals(preferredName) && !best.renameTo(preferred)) { return new Snapshot(file.lastModified(), file.length());
throw new ServiceException("图纸数量统计表本地文件重命名失败: " + best.getName()); }
private static boolean touchedSince(File file, Snapshot before) {
if (!isUsable(file)) {
return false;
}
return file.lastModified() > before.modified || file.length() != before.length;
}
private static final class Snapshot {
private final long modified;
private final long length;
private Snapshot(long modified, long length) {
this.modified = modified;
this.length = length;
} }
} }
@ -224,17 +336,17 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
} }
Map<String, Integer> col = mapHeaderColumns(header); Map<String, Integer> col = mapHeaderColumns(header);
DrawingWorkload row = new DrawingWorkload(); DrawingWorkload row = new DrawingWorkload();
row.setA4Count(readInt(data, col, "A4图纸数量")); row.setA4Count(readInt(data, col, "A4图纸数量", "A4图纸数", "A4数量"));
row.setA3Count(readInt(data, col, "A3图纸数量")); row.setA3Count(readInt(data, col, "A3图纸数量", "A3图纸数", "A3数量"));
row.setA3HalfCount(readInt(data, col, "A3+1/2图纸数量")); row.setA3HalfCount(readInt(data, col, "A3+1/2图纸数量", "A3+1/2图纸数"));
row.setA3Plus1Count(readInt(data, col, "A3+1图纸数量")); row.setA3Plus1Count(readInt(data, col, "A3+1图纸数量", "A3+1图纸数"));
row.setA2Count(readInt(data, col, "A2图纸数量")); row.setA2Count(readInt(data, col, "A2图纸数量", "A2图纸数"));
row.setA2HalfCount(readInt(data, col, "A2+1/2图纸数量")); row.setA2HalfCount(readInt(data, col, "A2+1/2图纸数量", "A2+1/2图纸数"));
row.setA1Count(readInt(data, col, "A1图纸数量")); row.setA1Count(readInt(data, col, "A1图纸数量", "A1图纸数"));
row.setA1HalfCount(readInt(data, col, "A1+1/2图纸数量")); row.setA1HalfCount(readInt(data, col, "A1+1/2图纸数量", "A1+1/2图纸数"));
row.setA0Count(readInt(data, col, "A0图纸数量")); row.setA0Count(readInt(data, col, "A0图纸数量", "A0图纸数"));
row.setA0HalfCount(readInt(data, col, "A0+1/2图纸数量")); row.setA0HalfCount(readInt(data, col, "A0+1/2图纸数量", "A0+1/2图纸数"));
row.setEquivalentA4Total(readInt(data, col, "等效A4图纸总数量")); row.setEquivalentA4Total(readInt(data, col, "等效A4图纸总数量", "等效A4图纸总量", "等效A4合计", "等效A4"));
return row; return row;
} }
@ -277,6 +389,7 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
private int findHeaderRow(Sheet sheet, String... keywords) { private int findHeaderRow(Sheet sheet, String... keywords) {
int last = Math.min(sheet.getLastRowNum(), 20); int last = Math.min(sheet.getLastRowNum(), 20);
int fallback = -1;
for (int r = 0; r <= last; r++) { for (int r = 0; r <= last; r++) {
Row row = sheet.getRow(r); Row row = sheet.getRow(r);
if (row == null) { if (row == null) {
@ -287,10 +400,10 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
for (int c = 0; c < lastCell; c++) { for (int c = 0; c < lastCell; c++) {
sb.append(cellText(row.getCell(c))).append('|'); sb.append(cellText(row.getCell(c))).append('|');
} }
String line = sb.toString(); String line = sb.toString().replace(" ", "");
boolean allMatch = true; boolean allMatch = true;
for (String kw : keywords) { for (String kw : keywords) {
if (!line.contains(kw)) { if (!line.contains(kw.replace(" ", ""))) {
allMatch = false; allMatch = false;
break; break;
} }
@ -298,8 +411,11 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
if (allMatch) { if (allMatch) {
return r; return r;
} }
if (fallback < 0 && (line.contains("等效A4") || line.contains("A4图纸数量") || line.contains("A4图纸数"))) {
fallback = r;
}
} }
return -1; return fallback;
} }
private Map<String, Integer> mapHeaderColumns(Row header) { private Map<String, Integer> mapHeaderColumns(Row header) {
@ -308,20 +424,44 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
for (int c = 0; c < last; c++) { for (int c = 0; c < last; c++) {
String text = cellText(header.getCell(c)); String text = cellText(header.getCell(c));
if (StringUtils.isNotBlank(text)) { if (StringUtils.isNotBlank(text)) {
map.put(text.trim(), c); String trimmed = text.trim();
map.put(trimmed, c);
map.put(trimmed.replace(" ", ""), c);
} }
} }
return map; return map;
} }
private Integer readInt(Row row, Map<String, Integer> col, String header) { private Integer readInt(Row row, Map<String, Integer> col, String... headers) {
Integer idx = col.get(header); Integer idx = findColumnIndex(col, headers);
if (idx == null) { if (idx == null) {
return null; return null;
} }
return parseInteger(row.getCell(idx)); return parseInteger(row.getCell(idx));
} }
private Integer findColumnIndex(Map<String, Integer> col, String... headers) {
for (String header : headers) {
if (col.containsKey(header)) {
return col.get(header);
}
String compact = header.replace(" ", "");
if (col.containsKey(compact)) {
return col.get(compact);
}
}
for (Map.Entry<String, Integer> entry : col.entrySet()) {
String key = entry.getKey() == null ? "" : entry.getKey().replace(" ", "");
for (String header : headers) {
String compact = header.replace(" ", "");
if (key.contains(compact) || compact.contains(key)) {
return entry.getValue();
}
}
}
return null;
}
private String readString(Row row, Map<String, Integer> col, String header) { private String readString(Row row, Map<String, Integer> col, String header) {
Integer idx = col.get(header); Integer idx = col.get(header);
if (idx == null) { if (idx == null) {
@ -360,6 +500,29 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
} }
} }
private DrawingWorkloadVo saveEmptyWorkload(ProcessOrderPro orderPro) {
ParsedExcel parsed = new ParsedExcel();
parsed.summary = emptySummary();
parsed.items = new ArrayList<>();
return toVo(saveWorkload(orderPro, parsed), true);
}
private static DrawingWorkload emptySummary() {
DrawingWorkload row = new DrawingWorkload();
row.setA4Count(0);
row.setA3Count(0);
row.setA3HalfCount(0);
row.setA3Plus1Count(0);
row.setA2Count(0);
row.setA2HalfCount(0);
row.setA1Count(0);
row.setA1HalfCount(0);
row.setA0Count(0);
row.setA0HalfCount(0);
row.setEquivalentA4Total(0);
return row;
}
private DrawingWorkload saveWorkload(ProcessOrderPro orderPro, ParsedExcel parsed) { private DrawingWorkload saveWorkload(ProcessOrderPro orderPro, ParsedExcel parsed) {
Date now = new Date(); Date now = new Date();
DrawingWorkload existing = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery() DrawingWorkload existing = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery()

View File

@ -7,9 +7,9 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.system.constant.DrawingMode;
import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.ProcessOrderProMapper;
import com.ruoyi.system.service.IProcessOrderProService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.bo.FigureSaveBo; import com.ruoyi.system.domain.bo.FigureSaveBo;
@ -17,6 +17,7 @@ import com.ruoyi.system.domain.vo.FigureSaveVo;
import com.ruoyi.system.domain.FigureSave; import com.ruoyi.system.domain.FigureSave;
import com.ruoyi.system.mapper.FigureSaveMapper; import com.ruoyi.system.mapper.FigureSaveMapper;
import com.ruoyi.system.service.IFigureSaveService; import com.ruoyi.system.service.IFigureSaveService;
import com.ruoyi.system.service.IOrderProDrawingDelayService;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -35,6 +36,7 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
private final FigureSaveMapper baseMapper; private final FigureSaveMapper baseMapper;
private final ProcessOrderProMapper processOrderProMapper; private final ProcessOrderProMapper processOrderProMapper;
private final IOrderProDrawingDelayService orderProDrawingDelayService;
/** /**
* 查询外购件临时 * 查询外购件临时
*/ */
@ -87,9 +89,12 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
public Boolean insertByBo(FigureSaveBo bo) { public Boolean insertByBo(FigureSaveBo bo) {
FigureSave add = BeanUtil.toBean(bo, FigureSave.class); FigureSave add = BeanUtil.toBean(bo, FigureSave.class);
validEntityBeforeSave(add); validEntityBeforeSave(add);
assertPartModeSameDrawPath(add.getPid(), add.getDrawPath(), null);
fillUploadTime(add);
boolean flag = baseMapper.insert(add) > 0; boolean flag = baseMapper.insert(add) > 0;
if (flag) { if (flag) {
bo.setId(add.getId()); bo.setId(add.getId());
orderProDrawingDelayService.refreshByProjectId(add.getPid());
} }
return flag; return flag;
} }
@ -101,7 +106,21 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
public Boolean updateByBo(FigureSaveBo bo) { public Boolean updateByBo(FigureSaveBo bo) {
FigureSave update = BeanUtil.toBean(bo, FigureSave.class); FigureSave update = BeanUtil.toBean(bo, FigureSave.class);
validEntityBeforeSave(update); validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0; Long pid = update.getPid();
if (pid == null && update.getId() != null) {
FigureSave existing = baseMapper.selectById(update.getId());
pid = existing != null ? existing.getPid() : null;
if (update.getPid() == null) {
update.setPid(pid);
}
}
assertPartModeSameDrawPath(pid, update.getDrawPath(), update.getId());
fillUploadTime(update);
boolean ok = baseMapper.updateById(update) > 0;
if (ok) {
orderProDrawingDelayService.refreshByProjectId(pid);
}
return ok;
} }
/** /**
@ -111,6 +130,48 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
//TODO 做一些数据校验,如唯一约束 //TODO 做一些数据校验,如唯一约束
} }
private void fillUploadTime(FigureSave entity) {
if (entity != null && StringUtils.isNotBlank(entity.getDrawPath()) && entity.getUploadTime() == null) {
entity.setUploadTime(new java.util.Date());
}
}
/**
* 零部件模式下:必须先选出图模式;图纸路径不能为空且须与已有产品一致。
*/
private void assertPartModeSameDrawPath(Long pid, String drawPath, Long excludeFigureId) {
if (pid == null) {
return;
}
ProcessOrderPro order = processOrderProMapper.selectById(pid);
if (order == null) {
throw new IllegalArgumentException("项目不存在");
}
DrawingMode.requireConfigured(order);
if (!DrawingMode.PART.equals(DrawingMode.normalize(order.getDrawingMode()))) {
return;
}
String normalized = DrawingMode.normalizeDrawPath(drawPath);
if (StringUtils.isBlank(normalized)) {
throw new IllegalArgumentException("零部件出图模式下图纸路径不能为空");
}
List<FigureSave> existing = baseMapper.selectList(
Wrappers.<FigureSave>lambdaQuery().eq(FigureSave::getPid, pid));
if (existing == null) {
return;
}
for (FigureSave fs : existing) {
if (excludeFigureId != null && excludeFigureId.equals(fs.getId())) {
continue;
}
String existingPath = DrawingMode.normalizeDrawPath(fs.getDrawPath());
if (StringUtils.isNotBlank(existingPath) && !existingPath.equals(normalized)) {
throw new IllegalArgumentException(
"零部件出图模式下所有产品必须使用同一图纸路径,已有路径:" + fs.getDrawPath());
}
}
}
/** /**
* 批量删除外购件临时 * 批量删除外购件临时
*/ */
@ -119,7 +180,18 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
if(isValid){ if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验 //TODO 做一些业务上的校验,判断是否需要校验
} }
return baseMapper.deleteBatchIds(ids) > 0; List<FigureSave> existing = ids == null || ids.isEmpty()
? Collections.emptyList()
: baseMapper.selectBatchIds(ids);
boolean ok = baseMapper.deleteBatchIds(ids) > 0;
if (ok && existing != null) {
existing.stream()
.map(FigureSave::getPid)
.filter(pid -> pid != null)
.distinct()
.forEach(orderProDrawingDelayService::refreshByProjectId);
}
return ok;
} }
/** /**
@ -158,10 +230,14 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
StringBuilder failureMsg = new StringBuilder(); StringBuilder failureMsg = new StringBuilder();
for (FigureSaveVo vo : dataList) { for (FigureSaveVo vo : dataList) {
try { try {
// 验证是否存在这个用户
FigureSave figureSave = BeanUtil.toBean(vo, FigureSave.class); FigureSave figureSave = BeanUtil.toBean(vo, FigureSave.class);
ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(figureSave.getProductionCode()); ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(figureSave.getProductionCode());
if (processOrderPro == null) {
throw new IllegalArgumentException("未找到生产令号:" + figureSave.getProductionCode());
}
figureSave.setPid(processOrderPro.getId()); figureSave.setPid(processOrderPro.getId());
assertPartModeSameDrawPath(figureSave.getPid(), figureSave.getDrawPath(), null);
fillUploadTime(figureSave);
baseMapper.insert(figureSave); baseMapper.insert(figureSave);
successNum++; successNum++;
successMsg.append("<br/>").append(successNum).append("、数据 ").append(vo.getFigureName()).append(" 导入成功"); successMsg.append("<br/>").append(successNum).append("、数据 ").append(vo.getFigureName()).append(" 导入成功");

View File

@ -39,6 +39,7 @@ import com.ruoyi.system.runner.JdUtil;
import com.ruoyi.system.runner.MaterialProperties; import com.ruoyi.system.runner.MaterialProperties;
import com.ruoyi.system.service.IImMaterialService; import com.ruoyi.system.service.IImMaterialService;
import com.ruoyi.system.service.IProductionOrderService; import com.ruoyi.system.service.IProductionOrderService;
import com.ruoyi.system.service.MssqlQueryService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock; import org.redisson.api.RLock;
import org.redisson.api.RedissonClient; import org.redisson.api.RedissonClient;
@ -70,6 +71,7 @@ public class ImMaterialServiceImpl implements IImMaterialService {
private final ImMaterialMapper baseMapper; private final ImMaterialMapper baseMapper;
private final IProductionOrderService productionOrderService; private final IProductionOrderService productionOrderService;
private final MssqlQueryService mssqlQueryService;
private static final Logger logger = LoggerFactory.getLogger(ImMaterialController.class); private static final Logger logger = LoggerFactory.getLogger(ImMaterialController.class);
@Autowired @Autowired
@ -181,26 +183,31 @@ public class ImMaterialServiceImpl implements IImMaterialService {
/** /**
* 查询物料检索列表 * 查询物料检索列表(实时查金蝶 T_BD_MATERIAL)
*/ */
@Override @Override
public TableDataInfo<ImMaterialVo> queryPageList(ImMaterialBo bo, PageQuery pageQuery) { public TableDataInfo<ImMaterialVo> queryPageList(ImMaterialBo bo, PageQuery pageQuery) {
try { try {
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() < 1 ? 1 : pageQuery.getPageNum();
LambdaQueryWrapper<ImMaterial> lqw = buildQueryWrapper(bo); Integer rawSize = pageQuery.getPageSize();
Page<ImMaterialVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); int pageSize;
if (CollectionUtils.isEmpty(result.getRecords())) { if (rawSize == null || rawSize <= 0 || rawSize == Integer.MAX_VALUE) {
return TableDataInfo.build(result); // 如果结果为空,直接返回 pageSize = 10;
} else {
pageSize = Math.min(rawSize, 200);
} }
// 调用库存查询方法,优化为批量查询而非逐个查询 MssqlQueryService.KingdeeMaterialPage kdPage =
Map<String, JsonObject> jsonMap = queryAllKuCun(result.getRecords()); mssqlQueryService.queryMaterialPage(bo, pageNum, pageSize, true);
Page<ImMaterialVo> result = new Page<>(pageNum, pageSize);
// 更新库存信息 result.setTotal(kdPage.getTotal());
updateRecordsWithKuCunInfo(jsonMap, result.getRecords()); List<ImMaterialVo> records = kdPage.getRecords() != null ? kdPage.getRecords() : Collections.emptyList();
// 更新数量,更新实体类 result.setRecords(records);
List<ImMaterialVo> records = result.getRecords(); if (CollectionUtils.isEmpty(records)) {
// 根据库存 in_inventory 降序排序 return TableDataInfo.build(result);
}
Map<String, JsonObject> jsonMap = queryAllKuCun(records);
updateRecordsWithKuCunInfo(jsonMap, records);
records.sort(Comparator.comparing( records.sort(Comparator.comparing(
r -> { r -> {
BigDecimal value = BigDecimal.ZERO; BigDecimal value = BigDecimal.ZERO;
@ -208,21 +215,18 @@ public class ImMaterialServiceImpl implements IImMaterialService {
try { try {
value = new BigDecimal(r.getInInventory()); value = new BigDecimal(r.getInInventory());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// 如果 in_inventory 不是数字,默认为 0
value = BigDecimal.ZERO; value = BigDecimal.ZERO;
} }
} }
// 保留三位小数,四舍五入
return value.setScale(3, RoundingMode.HALF_UP); return value.setScale(3, RoundingMode.HALF_UP);
}, },
Comparator.reverseOrder() Comparator.reverseOrder()
)); ));
result.setRecords(records); // 更新结果集*/ result.setRecords(records);
return TableDataInfo.build(result); return TableDataInfo.build(result);
} catch (Exception e) { } catch (Exception e) {
// 异常处理逻辑,可根据实际情况调整
log.error("查询页面列表异常", e); log.error("查询页面列表异常", e);
return new TableDataInfo<>(); // 返回空结果或自定义错误结果 return new TableDataInfo<>();
} }
} }
@ -331,22 +335,21 @@ public class ImMaterialServiceImpl implements IImMaterialService {
record.setInInventory("0.0"); record.setInInventory("0.0");
} }
// 使用keYong方法计算可用库存
ImMaterial material = BeanUtil.toBean(record, ImMaterial.class);
/* double availableStock = keYong(material);
record.setSingleWeight(BigDecimal.valueOf(availableStock));*/
// 更新记录
ImMaterial bean = BeanUtil.toBean(record, ImMaterial.class);
baseMapper.updateById(bean);
} }
} }
/** /**
* 查询物料检索 * 查询物料检索(优先金蝶内码,本地表兜底)
*/ */
@Override @Override
public ImMaterialVo queryById(Long id) { public ImMaterialVo queryById(Long id) {
try {
ImMaterial kingdee = mssqlQueryService.findMaterialById(id);
if (kingdee != null) {
return BeanUtil.toBean(kingdee, ImMaterialVo.class);
}
} catch (Exception e) {
}
return baseMapper.selectVoById(id); return baseMapper.selectVoById(id);
} }
/** /**
@ -392,12 +395,11 @@ public class ImMaterialServiceImpl implements IImMaterialService {
/** /**
* 查询物料检索列表 * 查询物料检索列表(实时查金蝶,导出上限 10000)
*/ */
@Override @Override
public List<ImMaterialVo> queryList(ImMaterialBo bo) { public List<ImMaterialVo> queryList(ImMaterialBo bo) {
LambdaQueryWrapper<ImMaterial> lqw = buildQueryWrapper(bo); return mssqlQueryService.queryMaterialList(bo, 10000, true);
return baseMapper.selectVoList(lqw);
} }
private LambdaQueryWrapper<ImMaterial> buildQueryWrapper(ImMaterialBo bo) { private LambdaQueryWrapper<ImMaterial> buildQueryWrapper(ImMaterialBo bo) {
@ -525,6 +527,13 @@ public class ImMaterialServiceImpl implements IImMaterialService {
@Override @Override
public ImMaterial selectByCodeAndName(String materialCode, String materialName) { public ImMaterial selectByCodeAndName(String materialCode, String materialName) {
try {
ImMaterial kingdee = mssqlQueryService.findMaterialByCodeAndName(materialCode, materialName);
if (kingdee != null) {
return kingdee;
}
} catch (Exception e) {
}
LambdaQueryWrapper<ImMaterial> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ImMaterial> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ImMaterial::getMaterialCode, materialCode) queryWrapper.eq(ImMaterial::getMaterialCode, materialCode)
.eq(ImMaterial::getMaterialName, materialName); .eq(ImMaterial::getMaterialName, materialName);
@ -639,8 +648,14 @@ public class ImMaterialServiceImpl implements IImMaterialService {
public ImMaterial getMaterialByCode(String materialCode) { public ImMaterial getMaterialByCode(String materialCode) {
try {
ImMaterial kingdee = mssqlQueryService.findMaterialByCode(materialCode);
if (kingdee != null) {
return kingdee;
}
} catch (Exception e) {
}
QueryWrapper<ImMaterial> wrapper = new QueryWrapper<>(); QueryWrapper<ImMaterial> wrapper = new QueryWrapper<>();
// 从数据库中获取ImMaterial,假设返回的是一个Optional<ImMaterial>
wrapper.eq("material_code", materialCode); wrapper.eq("material_code", materialCode);
return baseMapper.selectOne(wrapper); return baseMapper.selectOne(wrapper);
} }
@ -706,12 +721,16 @@ public class ImMaterialServiceImpl implements IImMaterialService {
} }
/** /**
* @param query * 按编码或名称模糊搜索金蝶物料
* @return
*/ */
@Override @Override
public TableDataInfo search(String query) { public TableDataInfo search(String query) {
return null; MssqlQueryService.KingdeeMaterialPage kdPage =
mssqlQueryService.queryMaterialByKeyword(query, 1, 50);
Page<ImMaterialVo> page = new Page<>(1, 50);
page.setTotal(kdPage.getTotal());
page.setRecords(kdPage.getRecords() != null ? kdPage.getRecords() : Collections.emptyList());
return TableDataInfo.build(page);
} }
/** /**

View File

@ -279,8 +279,6 @@ public class MrpResultCheckServiceImpl implements IMrpResultCheckService {
baseMapper.insert(vo); baseMapper.insert(vo);
} }
processOrderPro.setBomStatus(4L);
processOrderProMapper.updateById(processOrderPro);
} }

View File

@ -8,18 +8,23 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.constant.ProcessOrderProSource; import com.ruoyi.common.utils.file.PlmDrawingFileNames;
import com.ruoyi.system.domain.FigureSave;
import com.ruoyi.system.domain.PlmPdfZipPush; import com.ruoyi.system.domain.PlmPdfZipPush;
import com.ruoyi.system.domain.PlmPdfZipPushItem; import com.ruoyi.system.domain.PlmPdfZipPushItem;
import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.domain.ProductionOrder;
import com.ruoyi.system.domain.bo.PlmPdfZipPushBo; import com.ruoyi.system.domain.bo.PlmPdfZipPushBo;
import com.ruoyi.system.domain.vo.PlmPdfZipPushDetailVo; import com.ruoyi.system.domain.vo.PlmPdfZipPushDetailVo;
import com.ruoyi.system.domain.vo.PlmPdfZipPushItemVo; import com.ruoyi.system.domain.vo.PlmPdfZipPushItemVo;
import com.ruoyi.system.domain.vo.PlmPdfZipPushVo; import com.ruoyi.system.domain.vo.PlmPdfZipPushVo;
import com.ruoyi.system.mapper.FigureSaveMapper;
import com.ruoyi.system.mapper.PlmPdfZipPushItemMapper; import com.ruoyi.system.mapper.PlmPdfZipPushItemMapper;
import com.ruoyi.system.mapper.PlmPdfZipPushMapper; import com.ruoyi.system.mapper.PlmPdfZipPushMapper;
import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.ProcessOrderProMapper;
import com.ruoyi.system.mapper.ProductionOrderMapper;
import com.ruoyi.system.service.IPlmPdfZipPushService; import com.ruoyi.system.service.IPlmPdfZipPushService;
import com.ruoyi.system.utils.ProductionOrderNoUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -55,6 +60,8 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
private final PlmPdfZipPushMapper pushMapper; private final PlmPdfZipPushMapper pushMapper;
private final PlmPdfZipPushItemMapper pushItemMapper; private final PlmPdfZipPushItemMapper pushItemMapper;
private final ProcessOrderProMapper processOrderProMapper; private final ProcessOrderProMapper processOrderProMapper;
private final FigureSaveMapper figureSaveMapper;
private final ProductionOrderMapper productionOrderMapper;
private static final String PLM_DWG_SUBDIR = "dwg"; private static final String PLM_DWG_SUBDIR = "dwg";
@ -110,47 +117,185 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public Long ensureProcessOrderProForPlmPdf(String productionOrderNo) { public Long ensureProcessOrderProForPlmPdf(String productionOrderNo) {
String orderNo = StringUtils.trim(productionOrderNo); return ensureProcessOrderProForPlmPdf(productionOrderNo, null);
}
/**
* 图纸推送前校验:令号必须已在 process_order_pro 中存在,不再自动建项目。
*/
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public Long ensureProcessOrderProForPlmPdf(String productionOrderNo, List<Map<String, String>> savedItems) {
String orderNo = ProductionOrderNoUtils.stripSpaces(productionOrderNo);
if (StringUtils.isBlank(orderNo)) { if (StringUtils.isBlank(orderNo)) {
return null; throw new ServiceException("生产令号不能为空");
} }
ProcessOrderPro existing = processOrderProMapper.selectByProjectNumber(orderNo); ProcessOrderPro existing = processOrderProMapper.selectByProjectNumber(orderNo);
if (existing != null) { if (existing == null) {
return existing.getId(); throw new ServiceException("生产令号 " + orderNo + " 不存在");
} }
ensureDefaultFigureProduct(existing, savedItems);
return existing.getId();
}
/**
* 项目下尚无产品时自动建一条 figure_save,满足 PDF 下载前置校验。
* 型号/名称/数量:优先 production_order 主产品,其次推送明细(.00 总装),否则用令号。
* 类型:项目类型(非标/企标等);文件类型:二维;出图时间:项目出图时间或当前时间。
*/
private void ensureDefaultFigureProduct(ProcessOrderPro order, List<Map<String, String>> savedItems) {
if (order == null || order.getId() == null) {
return;
}
Long count = figureSaveMapper.selectCount(
Wrappers.<FigureSave>lambdaQuery().eq(FigureSave::getPid, order.getId()));
if (count != null && count > 0) {
return;
}
ProductHint hint = resolveProductHint(order.getProductionOrderNo(), savedItems);
Date drawTime = order.getDrawingTime() != null ? order.getDrawingTime() : new Date();
Date now = new Date(); Date now = new Date();
ProcessOrderPro add = new ProcessOrderPro(); FigureSave product = new FigureSave();
add.setProductionOrderNo(orderNo); product.setPid(order.getId());
add.setProductionName(orderNo); product.setProductionCode(order.getProductionOrderNo());
add.setSource(ProcessOrderProSource.PLM); product.setFigureNumber(hint.figureNumber);
add.setUnit("台"); product.setFigureName(hint.figureName);
add.setQuantity(0L); // 不保留零件图(前端 quantity:1是/0否) product.setFigureNum(hint.figureNum);
add.setBomStatus(1L); // 设计出图完成(图纸已推送) product.setProductType(StringUtils.isNotBlank(order.getIsEnterpriseStandard())
add.setRouteStatus(0L); ? order.getIsEnterpriseStandard() : resolveProjectTypeByOrderNo(order.getProductionOrderNo()));
add.setNeedProcess(1); product.setFileType("二维");
add.setIsDownload(1); product.setDrawTime(drawTime);
add.setNeedKingdeePrdMo(0); product.setWhetherDelay(0);
add.setNeedKingdeePurchaseOrder(0); product.setCreateBy("system-PLM");
add.setWhetherDelay(0); product.setUpdateBy("system-PLM");
add.setIsEnterpriseStandard(resolveProjectTypeByOrderNo(orderNo)); product.setCreateTime(now);
add.setDrawingTime(now); product.setUpdateTime(now);
add.setCreateBy("system-PLM"); figureSaveMapper.insert(product);
add.setUpdateBy("system-PLM"); log.info("[PLM-PDF] 自动新建产品: orderNo={}, pid={}, figureNumber={}, figureName={}, figureNum={}, productType={}, drawTime={}",
add.setCreateTime(now); order.getProductionOrderNo(), order.getId(), product.getFigureNumber(), product.getFigureName(),
add.setUpdateTime(now); product.getFigureNum(), product.getProductType(), product.getDrawTime());
try { }
processOrderProMapper.insert(add);
log.info("[PLM-PDF] 自动新建项目: productionOrderNo={}, id={}, source=PLM", orderNo, add.getId()); private ProductHint resolveProductHint(String orderNo, List<Map<String, String>> savedItems) {
return add.getId(); ProductHint hint = new ProductHint();
} catch (Exception e) { hint.figureNumber = orderNo;
// 并发首次推送:另一请求可能已插入,再查一次 hint.figureName = orderNo;
ProcessOrderPro again = processOrderProMapper.selectByProjectNumber(orderNo); hint.figureNum = 1L;
if (again != null) {
log.info("[PLM-PDF] 自动建项目遇并发,已存在: productionOrderNo={}, id={}", orderNo, again.getId()); List<ProductionOrder> assemblyRows = productionOrderMapper.selectList(
return again.getId(); Wrappers.<ProductionOrder>lambdaQuery()
.eq(ProductionOrder::getProductionOrderNo, orderNo)
.orderByAsc(ProductionOrder::getId));
if (!CollectionUtils.isEmpty(assemblyRows)) {
ProductionOrder preferred = null;
for (ProductionOrder row : assemblyRows) {
if (row == null) {
continue;
}
if (StringUtils.isNotBlank(row.getMainPart()) || StringUtils.isNotBlank(row.getMainName())) {
preferred = row;
break;
}
}
if (preferred == null) {
preferred = assemblyRows.get(0);
}
if (StringUtils.isNotBlank(preferred.getMainPart())) {
hint.figureNumber = preferred.getMainPart().trim();
} else if (StringUtils.isNotBlank(preferred.getDrawingNo())) {
hint.figureNumber = preferred.getDrawingNo().trim();
}
if (StringUtils.isNotBlank(preferred.getMainName())) {
hint.figureName = preferred.getMainName().trim();
} else if (StringUtils.isNotBlank(preferred.getDrawingName())) {
hint.figureName = preferred.getDrawingName().trim();
}
Long batch = parsePositiveLong(preferred.getBatchQuantity());
if (batch != null) {
hint.figureNum = batch;
} }
throw e;
} }
Map<String, String> item = pickMainPushItem(savedItems);
if (item != null) {
if (StringUtils.isBlank(hint.figureNumber) || Objects.equals(hint.figureNumber, orderNo)) {
String code = StringUtils.trim(item.get("materialCode"));
if (StringUtils.isNotBlank(code)) {
hint.figureNumber = code;
}
}
if (StringUtils.isBlank(hint.figureName) || Objects.equals(hint.figureName, orderNo)) {
String name = StringUtils.trim(item.get("materialName"));
if (StringUtils.isNotBlank(name)) {
hint.figureName = name;
}
}
if (hint.figureNum == null || hint.figureNum <= 1L) {
Long batchQty = parsePositiveLong(item.get("batchQty"));
if (batchQty == null) {
batchQty = parsePositiveLong(item.get("unitQty"));
}
if (batchQty != null) {
hint.figureNum = batchQty;
}
}
}
if (StringUtils.isBlank(hint.figureNumber)) {
hint.figureNumber = orderNo;
}
if (StringUtils.isBlank(hint.figureName)) {
hint.figureName = orderNo;
}
if (hint.figureNum == null || hint.figureNum < 1L) {
hint.figureNum = 1L;
}
return hint;
}
/** 优先选图号含 .00 的总装图纸,否则取第一条有物料编码的明细 */
private static Map<String, String> pickMainPushItem(List<Map<String, String>> savedItems) {
if (CollectionUtils.isEmpty(savedItems)) {
return null;
}
Map<String, String> first = null;
for (Map<String, String> item : savedItems) {
if (item == null) {
continue;
}
String code = StringUtils.trim(item.get("materialCode"));
if (StringUtils.isBlank(code)) {
continue;
}
if (first == null) {
first = item;
}
if (code.contains(".00")) {
return item;
}
}
return first;
}
private static Long parsePositiveLong(String raw) {
if (StringUtils.isBlank(raw)) {
return null;
}
try {
double v = Double.parseDouble(raw.trim());
if (v < 1D) {
return null;
}
return (long) Math.round(v);
} catch (Exception e) {
return null;
}
}
private static final class ProductHint {
private String figureNumber;
private String figureName;
private Long figureNum;
} }
/** 与手工建项一致:按令号前缀推断项目类型(FB/非标、QB/企标等) */ /** 与手工建项一致:按令号前缀推断项目类型(FB/非标、QB/企标等) */
@ -395,7 +540,6 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
} }
String sep = StringUtils.isNotBlank(plmPdfSeparator) ? plmPdfSeparator : "_"; String sep = StringUtils.isNotBlank(plmPdfSeparator) ? plmPdfSeparator : "_";
String code = StringUtils.trimToEmpty(materialCode); String code = StringUtils.trimToEmpty(materialCode);
String prefixWithSep = code + sep;
String ext = "." + extension.toLowerCase(Locale.ROOT); String ext = "." + extension.toLowerCase(Locale.ROOT);
int count = 0; int count = 0;
int scanned = 0; int scanned = 0;
@ -407,10 +551,7 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
if (!lower.endsWith(ext)) { if (!lower.endsWith(ext)) {
continue; continue;
} }
boolean matched = name.equalsIgnoreCase(code + ext) if (PlmDrawingFileNames.matchesMaterialCode(name, code, sep, extension)) {
|| name.regionMatches(true, 0, prefixWithSep, 0, prefixWithSep.length())
|| name.regionMatches(true, 0, code + "-", 0, code.length() + 1);
if (matched) {
log.info("[PLM-PDF][清理重复图] 文件名匹配: dir={}, materialCode={}, file={}", log.info("[PLM-PDF][清理重复图] 文件名匹配: dir={}, materialCode={}, file={}",
dir, materialCode, name); dir, materialCode, name);
count += deletePlmDrawingFile(file.toString(), deleted); count += deletePlmDrawingFile(file.toString(), deleted);

View File

@ -11,6 +11,7 @@ import com.ruoyi.system.domain.MaterialBom;
import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.domain.ProcessRoute; import com.ruoyi.system.domain.ProcessRoute;
import com.ruoyi.system.domain.ProductionOrder; import com.ruoyi.system.domain.ProductionOrder;
import com.ruoyi.system.domain.bo.plm.PlmGeneralDrawingUtils;
import com.ruoyi.system.domain.bo.plm.PlmMaterialBomBo; import com.ruoyi.system.domain.bo.plm.PlmMaterialBomBo;
import com.ruoyi.system.domain.bo.plm.PlmOrderMaterialMatchUtils; import com.ruoyi.system.domain.bo.plm.PlmOrderMaterialMatchUtils;
import com.ruoyi.system.domain.bo.plm.PlmProcessMaterialBo; import com.ruoyi.system.domain.bo.plm.PlmProcessMaterialBo;
@ -38,8 +39,10 @@ import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -75,26 +78,74 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
if (StringUtils.isBlank(productionOrder)) { if (StringUtils.isBlank(productionOrder)) {
throw new ServiceException("productionOrder 不能为空"); throw new ServiceException("productionOrder 不能为空");
} }
requireProductionOrderExists(productionOrder); ProcessOrderPro project = requireProductionOrderExists(productionOrder);
log.info("PLM 工艺推送 body JSON:\n{}", JSON.toJSONString(body, SerializerFeature.PrettyFormat)); log.info("PLM 工艺推送 body JSON:\n{}", JSON.toJSONString(body, SerializerFeature.PrettyFormat));
PlmProcessRoutePushValidator.validate(body); PlmProcessRoutePushValidator.validate(body);
// 总令号推送:入库前剔除子令号已有且图号归属匹配的物料(变更令 BG 跳过) // 总令号推送:入库前剔除子令号已有且图号归属匹配的物料(变更令 BG 跳过)
filterParentMaterialsDuplicatedInChild(body, productionOrder); filterParentMaterialsDuplicatedInChild(body, productionOrder);
// bom_details = 总装行(bomType=0,来自 production_order) + 材料BOM(bomType=1,来自本次推送) Set<String> incomingCodes = collectPushMaterialCodes(body);
List<BomDetails> assemblyBomRows = buildAssemblyBomDetailsFromProductionOrder(productionOrder); Set<String> incomingGeneral = PlmGeneralDrawingUtils.collectGeneralDrawingNos(incomingCodes);
Set<String> existingCodes = collectExistingProcessMaterialCodes(productionOrder);
Set<String> existingGeneral = PlmGeneralDrawingUtils.collectGeneralDrawingNos(existingCodes);
Set<String> overlap = PlmGeneralDrawingUtils.intersection(incomingGeneral, existingGeneral);
boolean hasExisting = !existingCodes.isEmpty();
boolean canCompare = !existingGeneral.isEmpty() && !incomingGeneral.isEmpty();
boolean append = hasExisting && canCompare && overlap.isEmpty();
boolean replaced = false;
if (hasExisting && !append) {
if (PlmGeneralDrawingUtils.isRouteApproved(project.getRouteStatus())) {
throw new ServiceException("生产令号 " + productionOrder + " 工艺状态已审核,不允许再推送");
}
replaced = true;
if (canCompare && !overlap.isEmpty()) {
Set<String> allGenerals = new LinkedHashSet<>(existingGeneral);
allGenerals.addAll(incomingGeneral);
Set<String> belonging = collectCodesBelongingToGenerals(productionOrder, overlap, allGenerals);
for (String code : incomingCodes) {
String owner = PlmGeneralDrawingUtils.resolveOwnerGeneralDrawing(code, allGenerals);
if (overlap.contains(code) || (owner != null && overlap.contains(owner))) {
belonging.add(code);
}
}
deleteProcessAndMaterialByCodes(productionOrder, belonging);
log.info("[PLM-工艺] 未审核同总图覆盖: order={}, general={}, codes={}",
productionOrder, overlap, belonging.size());
} else {
deleteAllProcessAndMaterialKeepAssembly(productionOrder);
log.info("[PLM-工艺] 未审核全量覆盖: order={}, existingGeneral={}, incomingGeneral={}",
productionOrder, existingGeneral, incomingGeneral);
}
} else if (append) {
log.info("[PLM-工艺] 总图号不同,追加写入: order={}, existing={}, incoming={}",
productionOrder, existingGeneral, incomingGeneral);
Set<String> mergedCodes = mergeSharedProcessBatchQuantities(body, productionOrder);
if (!mergedCodes.isEmpty()) {
List<PlmProcessMaterialBo> kept = new ArrayList<>();
for (PlmProcessMaterialBo material : body.getMaterialList()) {
if (material == null) {
continue;
}
String code = StringUtils.trimToEmpty(material.getMaterialCode());
if (mergedCodes.contains(code)) {
continue;
}
kept.add(material);
}
body.setMaterialList(kept);
log.info("[PLM-工艺] 共用件已合并本批数量,不再重复入库: order={}, merged={}, remain={}",
productionOrder, mergedCodes.size(), kept.size());
}
}
int assemblyCount = refreshAssemblyBomDetailsFromProductionOrder(productionOrder);
List<BomDetails> materialBomDetailRows = buildBomDetails(body, productionOrder); List<BomDetails> materialBomDetailRows = buildBomDetails(body, productionOrder);
List<BomDetails> bomRows = new ArrayList<>(assemblyBomRows.size() + materialBomDetailRows.size());
bomRows.addAll(assemblyBomRows);
bomRows.addAll(materialBomDetailRows);
List<MaterialBom> materialBomRows = buildMaterialBoms(body, productionOrder); List<MaterialBom> materialBomRows = buildMaterialBoms(body, productionOrder);
List<ProcessRoute> routeRows = buildProcessRoutes(body, productionOrder); List<ProcessRoute> routeRows = buildProcessRoutes(body, productionOrder);
bomDetailsMapper.deleteWithProCode(productionOrder); if (!materialBomDetailRows.isEmpty()) {
materialBomMapper.deleteWithProCode(productionOrder); bomDetailsMapper.insertBatch(materialBomDetailRows);
processRouteMapper.deleteByProCode(productionOrder);
if (!bomRows.isEmpty()) {
bomDetailsMapper.insertBatch(bomRows);
} }
if (!materialBomRows.isEmpty()) { if (!materialBomRows.isEmpty()) {
materialBomMapper.insertBatch(materialBomRows); materialBomMapper.insertBatch(materialBomRows);
@ -104,29 +155,30 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
} }
// 子令号推送:按本次归属匹配的图号,清理总令号侧重复工艺/BOM // 子令号推送:按本次归属匹配的图号,清理总令号侧重复工艺/BOM
cleanupParentProcessDuplicatedInChild(productionOrder, collectPushMaterialCodes(body)); cleanupParentProcessDuplicatedInChild(productionOrder, incomingCodes);
PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary summary = PlmProcessRoutePushNormalizer.summarize(body); PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary summary = PlmProcessRoutePushNormalizer.summarize(body);
summary.setSavedBomCount(bomRows.size()); summary.setSavedBomCount(assemblyCount + materialBomDetailRows.size());
summary.setSavedMaterialBomCount(materialBomRows.size()); summary.setSavedMaterialBomCount(materialBomRows.size());
summary.setSavedRouteCount(routeRows.size()); summary.setSavedRouteCount(routeRows.size());
summary.setAppended(append);
summary.setReplaced(replaced);
markProjectAfterPlmProcessRoute(productionOrder, summary); markProjectAfterPlmProcessRoute(productionOrder, summary);
log.info("PLM 工艺推送入库完成 productionOrder={} bom={}(assembly={} materialDetail={}) materialBom={} route={}", log.info("PLM 工艺推送入库完成 productionOrder={} bom={}(assembly={} materialDetail={}) materialBom={} route={} appended={} replaced={}",
productionOrder, bomRows.size(), assemblyBomRows.size(), materialBomDetailRows.size(), productionOrder, assemblyCount + materialBomDetailRows.size(), assemblyCount, materialBomDetailRows.size(),
materialBomRows.size(), routeRows.size()); materialBomRows.size(), routeRows.size(), append, replaced);
return summary; return summary;
} }
/** /**
* 总令号入库前:去掉已在子令号出现、且图号前缀归属本令号的物料。 * 总令号入库前:去掉子令号 process_route 中已存在的相同物料编码,并清理总令库中对应行。
*/ */
private void filterParentMaterialsDuplicatedInChild(PlmProcessRoutePushBo body, String productionOrder) { private void filterParentMaterialsDuplicatedInChild(PlmProcessRoutePushBo body, String productionOrder) {
if (body == null || CollectionUtils.isEmpty(body.getMaterialList())) { if (body == null || CollectionUtils.isEmpty(body.getMaterialList())) {
return; return;
} }
if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrder) if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrder)
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder) || PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)) {
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(productionOrder) == null) {
return; return;
} }
Set<String> childCodes = collectChildOrderMaterialCodes(productionOrder); Set<String> childCodes = collectChildOrderMaterialCodes(productionOrder);
@ -148,26 +200,26 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
kept.add(material); kept.add(material);
} }
body.setMaterialList(kept); body.setMaterialList(kept);
log.info("[PLM-工艺去重] 总令号过滤完成: parent={}, before={}, after={}, childCodes={}", int deleted = deleteProcessDataByMaterialCodes(productionOrder, childCodes);
productionOrder, before, kept.size(), childCodes.size()); log.info("[PLM-工艺去重] 总令号过滤完成: parent={}, before={}, after={}, childCodes={}, deletedRows={}",
productionOrder, before, kept.size(), childCodes.size(), deleted);
} }
/** /**
* 子令号入库后:按本次图号删除总令号侧重复工艺/材料BOM/bom_details。 * 子令号入库后:按本次推送的物料编码,删除总令号侧相同工艺/材料BOM/bom_details。
*/ */
private void cleanupParentProcessDuplicatedInChild(String productionOrder, Set<String> inputCodes) { private void cleanupParentProcessDuplicatedInChild(String productionOrder, Set<String> inputCodes) {
if (!PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)) { if (!PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)) {
return; return;
} }
String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrder); String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrder);
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent) if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)) {
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(parent) == null) {
log.info("[PLM-工艺去重] 子令号跳过清理总令: child={}, parent={}", productionOrder, parent); log.info("[PLM-工艺去重] 子令号跳过清理总令: child={}, parent={}", productionOrder, parent);
return; return;
} }
Set<String> codes = filterCodesBelongingToOrder(inputCodes, parent); Set<String> codes = normalizeMaterialCodes(inputCodes);
if (codes.isEmpty()) { if (codes.isEmpty()) {
log.info("[PLM-工艺去重] 子令号本次无归属匹配图号,不清理: child={}, parent={}", productionOrder, parent); log.info("[PLM-工艺去重] 子令号本次无有效物料编码,不清理: child={}, parent={}", productionOrder, parent);
return; return;
} }
int deleted = deleteProcessDataByMaterialCodes(parent, codes); int deleted = deleteProcessDataByMaterialCodes(parent, codes);
@ -175,7 +227,7 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
productionOrder, parent, codes, deleted); productionOrder, parent, codes, deleted);
} }
/** 汇总已接收子令号(末段纯数字、不含 BG)在 process_route 中的物料编码,且须归属总令图号前缀。 */ /** 汇总已接收子令号在 process_route 中的物料编码(物料编码完全相同即视为重复,不做图号前缀过滤)。 */
private Set<String> collectChildOrderMaterialCodes(String parentOrderNo) { private Set<String> collectChildOrderMaterialCodes(String parentOrderNo) {
Set<String> childOrderNos = findChildProductionOrderNos(parentOrderNo); Set<String> childOrderNos = findChildProductionOrderNos(parentOrderNo);
if (childOrderNos.isEmpty()) { if (childOrderNos.isEmpty()) {
@ -192,7 +244,7 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
continue; continue;
} }
String code = StringUtils.trimToEmpty(route.getMaterialCode()); String code = StringUtils.trimToEmpty(route.getMaterialCode());
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, parentOrderNo)) { if (StringUtils.isNotBlank(code)) {
codes.add(code); codes.add(code);
} }
} }
@ -235,17 +287,18 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
} }
private static Set<String> filterCodesBelongingToOrder(Set<String> codes, String productionOrderNo) { private static Set<String> normalizeMaterialCodes(Set<String> codes) {
Set<String> matched = new LinkedHashSet<>(); Set<String> normalized = new LinkedHashSet<>();
if (codes == null) { if (codes == null) {
return matched; return normalized;
} }
for (String code : codes) { for (String code : codes) {
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, productionOrderNo)) { String trimmed = StringUtils.trimToEmpty(code);
matched.add(StringUtils.trimToEmpty(code)); if (StringUtils.isNotBlank(trimmed)) {
normalized.add(trimmed);
} }
} }
return matched; return normalized;
} }
/** 按令号 + 物料编码精确删除工艺及相关 BOM(只动该令号)。 */ /** 按令号 + 物料编码精确删除工艺及相关 BOM(只动该令号)。 */
@ -268,20 +321,263 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
return n; return n;
} }
/** 从已入库的 production_order 回写总装行到 bom_details,避免工艺推送全删后总装丢失。 */ /** 从已入库的 production_order 回写总装行到 bom_details,避免工艺推送后总装丢失。 */
private List<BomDetails> buildAssemblyBomDetailsFromProductionOrder(String productionOrder) { private int refreshAssemblyBomDetailsFromProductionOrder(String productionOrder) {
bomDetailsMapper.delete(Wrappers.<BomDetails>lambdaQuery()
.eq(BomDetails::getTotalWeight, productionOrder)
.eq(BomDetails::getBomType, AssemblyBomToBomDetailsConverter.BOM_TYPE_ASSEMBLY));
List<ProductionOrder> orders = productionOrderMapper.selectList( List<ProductionOrder> orders = productionOrderMapper.selectList(
Wrappers.<ProductionOrder>lambdaQuery() Wrappers.<ProductionOrder>lambdaQuery()
.eq(ProductionOrder::getProductionOrderNo, productionOrder)); .eq(ProductionOrder::getProductionOrderNo, productionOrder));
return AssemblyBomToBomDetailsConverter.toBomDetailsList(orders); List<BomDetails> assemblyBomRows = AssemblyBomToBomDetailsConverter.toBomDetailsList(orders);
if (assemblyBomRows.isEmpty()) {
return 0;
}
bomDetailsMapper.insertBatch(assemblyBomRows);
return assemblyBomRows.size();
}
private Set<String> collectExistingProcessMaterialCodes(String productionOrder) {
Set<String> codes = new LinkedHashSet<>();
List<ProcessRoute> routes = processRouteMapper.selectList(
Wrappers.<ProcessRoute>lambdaQuery()
.eq(ProcessRoute::getRouteDescription, productionOrder)
.select(ProcessRoute::getMaterialCode));
if (routes != null) {
for (ProcessRoute route : routes) {
if (route == null) {
continue;
}
String code = StringUtils.trimToEmpty(route.getMaterialCode());
if (StringUtils.isNotBlank(code)) {
codes.add(code);
}
}
}
return codes;
}
/**
* 收集归属 overlap 总图号的已有物料编码;用 allGenerals 做最长匹配,避免误删另一总图下的件。
*/
private Set<String> collectCodesBelongingToGenerals(String productionOrder,
Set<String> overlapGenerals,
Set<String> allGenerals) {
Set<String> belonging = new LinkedHashSet<>();
if (StringUtils.isBlank(productionOrder) || CollectionUtils.isEmpty(overlapGenerals)) {
return belonging;
}
List<ProcessRoute> routes = processRouteMapper.selectList(
Wrappers.<ProcessRoute>lambdaQuery()
.eq(ProcessRoute::getRouteDescription, productionOrder)
.select(ProcessRoute::getMaterialCode));
if (routes != null) {
for (ProcessRoute route : routes) {
if (route == null) {
continue;
}
addIfBelongingToOverlap(belonging, route.getMaterialCode(), overlapGenerals, allGenerals);
}
}
List<MaterialBom> boms = materialBomMapper.selectList(
Wrappers.<MaterialBom>lambdaQuery()
.eq(MaterialBom::getProjectNumber, productionOrder)
.select(MaterialBom::getParentMaterialCode, MaterialBom::getMaterialCode));
if (boms != null) {
for (MaterialBom bom : boms) {
if (bom == null) {
continue;
}
String parent = StringUtils.trimToEmpty(bom.getParentMaterialCode());
String child = StringUtils.trimToEmpty(bom.getMaterialCode());
String parentOwner = PlmGeneralDrawingUtils.resolveOwnerGeneralDrawing(parent, allGenerals);
String childOwner = PlmGeneralDrawingUtils.resolveOwnerGeneralDrawing(child, allGenerals);
if ((parentOwner != null && overlapGenerals.contains(parentOwner))
|| (childOwner != null && overlapGenerals.contains(childOwner))) {
if (StringUtils.isNotBlank(parent)) {
belonging.add(parent);
}
if (StringUtils.isNotBlank(child)) {
belonging.add(child);
}
}
}
}
return belonging;
}
private static void addIfBelongingToOverlap(Set<String> belonging, String materialCode,
Set<String> overlapGenerals, Set<String> allGenerals) {
String code = StringUtils.trimToEmpty(materialCode);
if (StringUtils.isBlank(code)) {
return;
}
String owner = PlmGeneralDrawingUtils.resolveOwnerGeneralDrawing(code, allGenerals);
if (owner != null && overlapGenerals.contains(owner)) {
belonging.add(code);
}
}
/** 覆盖同总图时删除工艺/材料 BOM,保留总装行(bomType=0)。 */
private void deleteProcessAndMaterialByCodes(String orderNo, Set<String> materialCodes) {
if (StringUtils.isBlank(orderNo) || CollectionUtils.isEmpty(materialCodes)) {
return;
}
processRouteMapper.delete(Wrappers.<ProcessRoute>lambdaQuery()
.eq(ProcessRoute::getRouteDescription, orderNo)
.in(ProcessRoute::getMaterialCode, materialCodes));
materialBomMapper.delete(Wrappers.<MaterialBom>lambdaQuery()
.eq(MaterialBom::getProjectNumber, orderNo)
.and(w -> w.in(MaterialBom::getParentMaterialCode, materialCodes)
.or()
.in(MaterialBom::getMaterialCode, materialCodes)));
bomDetailsMapper.delete(Wrappers.<BomDetails>lambdaQuery()
.eq(BomDetails::getTotalWeight, orderNo)
.eq(BomDetails::getBomType, "1")
.and(w -> w.in(BomDetails::getFNumber, materialCodes)
.or()
.in(BomDetails::getPartNumber, materialCodes)));
}
/** 无法比较总图时全量覆盖工艺/材料 BOM,保留总装行。 */
private void deleteAllProcessAndMaterialKeepAssembly(String productionOrder) {
processRouteMapper.deleteByProCode(productionOrder);
materialBomMapper.deleteWithProCode(productionOrder);
bomDetailsMapper.delete(Wrappers.<BomDetails>lambdaQuery()
.eq(BomDetails::getTotalWeight, productionOrder)
.eq(BomDetails::getBomType, "1"));
}
/**
* 追加写入时:先比物料编码;再比有效工序条数;再逐道比工序号/名称/工作中心/描述。
* 全部相同才合并本批数量,返回已合并的物料编码。
*/
private Set<String> mergeSharedProcessBatchQuantities(PlmProcessRoutePushBo body, String productionOrder) {
Set<String> merged = new LinkedHashSet<>();
if (body == null || CollectionUtils.isEmpty(body.getMaterialList())) {
return merged;
}
List<ProcessRoute> existing = processRouteMapper.selectList(
Wrappers.<ProcessRoute>lambdaQuery()
.eq(ProcessRoute::getRouteDescription, productionOrder)
.select(ProcessRoute::getId, ProcessRoute::getMaterialCode,
ProcessRoute::getProcessNo, ProcessRoute::getProcessName,
ProcessRoute::getWorkCenter, ProcessRoute::getProcessDescription,
ProcessRoute::getBatchQuantity));
if (CollectionUtils.isEmpty(existing)) {
return merged;
}
Map<String, List<ProcessRoute>> byCode = new LinkedHashMap<>();
for (ProcessRoute row : existing) {
if (row == null || StringUtils.isBlank(row.getMaterialCode())) {
continue;
}
byCode.computeIfAbsent(row.getMaterialCode().trim(), k -> new ArrayList<>()).add(row);
}
Date now = new Date();
for (PlmProcessMaterialBo material : body.getMaterialList()) {
if (material == null || StringUtils.isBlank(material.getMaterialCode())) {
continue;
}
String code = material.getMaterialCode().trim();
List<ProcessRoute> existRows = byCode.get(code);
if (existRows == null || existRows.isEmpty()) {
continue;
}
if (!isSameProcessRoute(existRows, material, productionOrder, code)) {
continue;
}
long addQty = material.getBatchQuantity() == null ? 0L : material.getBatchQuantity().longValue();
Long oldQty = null;
for (ProcessRoute row : existRows) {
if (row.getBatchQuantity() != null) {
oldQty = row.getBatchQuantity();
break;
}
}
long baseQty = oldQty == null ? 0L : oldQty;
long newQty = baseQty + addQty;
processRouteMapper.update(null, Wrappers.<ProcessRoute>lambdaUpdate()
.eq(ProcessRoute::getRouteDescription, productionOrder)
.eq(ProcessRoute::getMaterialCode, code)
.set(ProcessRoute::getBatchQuantity, newQty)
.set(ProcessRoute::getUpdateBy, PLM_OPERATOR)
.set(ProcessRoute::getUpdateTime, now));
merged.add(code);
log.info("[PLM-工艺] 共用件合并本批数量: order={}, materialCode={}, {}+{}={}",
productionOrder, code, baseQty, addQty, newQty);
}
return merged;
}
/**
* 比较顺序:物料编码已相同 → 有效工序条数 → 逐道工序号/名称/工作中心/描述。
* 全部相同才允许合并本批数量。
*/
private static boolean isSameProcessRoute(List<ProcessRoute> existing, PlmProcessMaterialBo incoming,
String productionOrder, String materialCode) {
List<ProcessRoute> existSteps = collectExistingProcessSteps(existing);
List<PlmProcessRouteItemBo> incomingSteps = incoming == null || incoming.getProcessRoute() == null
? new ArrayList<>()
: collectValidRoutes(incoming.getProcessRoute());
if (existSteps.size() != incomingSteps.size()) {
log.info("[PLM-工艺] 物料编码相同但工序条数不同,不合并: order={}, materialCode={}, exist={}, incoming={}",
productionOrder, materialCode, existSteps.size(), incomingSteps.size());
return false;
}
for (int i = 0; i < existSteps.size(); i++) {
ProcessRoute exist = existSteps.get(i);
PlmProcessRouteItemBo in = incomingSteps.get(i);
Long existNo = exist.getProcessNo();
Long inNo = parseProcessNo(in.getProcessNo());
if (!Objects.equals(existNo, inNo)
|| !equalsTrim(exist.getProcessName(), in.getProcessName())
|| !equalsTrim(exist.getWorkCenter(), in.getWorkCenter())
|| !equalsProcessDesc(exist.getProcessDescription(), buildProcessDescription(in))) {
log.info("[PLM-工艺] 物料编码相同但第{}道工序不同,不合并: order={}, materialCode={}, existNo={}, incomingNo={}",
i + 1, productionOrder, materialCode, existNo, inNo);
return false;
}
}
return true;
}
private static List<ProcessRoute> collectExistingProcessSteps(List<ProcessRoute> rows) {
List<ProcessRoute> processes = new ArrayList<>();
if (rows != null) {
for (ProcessRoute row : rows) {
if (row == null || row.getProcessNo() == null || StringUtils.isBlank(row.getProcessName())) {
continue;
}
processes.add(row);
}
}
processes.sort(Comparator.comparing(ProcessRoute::getProcessNo));
return processes;
}
private static boolean equalsTrim(String left, String right) {
return StringUtils.trimToEmpty(left).equals(StringUtils.trimToEmpty(right));
}
private static boolean equalsProcessDesc(String stored, String incoming) {
return normalizeProcessDesc(stored).equals(normalizeProcessDesc(incoming));
}
private static String normalizeProcessDesc(String desc) {
if (desc == null) {
return "";
}
return desc.replace("\r\n", "\n").replace('\r', '\n').trim();
} }
/** 接收前校验:process_order_pro.production_order_no 必须已存在,否则拒绝接收。 */ /** 接收前校验:process_order_pro.production_order_no 必须已存在,否则拒绝接收。 */
private void requireProductionOrderExists(String productionOrder) { private ProcessOrderPro requireProductionOrderExists(String productionOrder) {
ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder.trim()); ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder.trim());
if (order == null) { if (order == null) {
throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在"); throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在");
} }
return order;
} }
/** /**
@ -299,6 +595,23 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在"); throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在");
} }
Date now = new Date(); Date now = new Date();
long bomCount = bomDetailsMapper.selectCount(
Wrappers.<BomDetails>lambdaQuery().eq(BomDetails::getTotalWeight, productionOrder));
long routeCount = processRouteMapper.selectCount(
Wrappers.<ProcessRoute>lambdaQuery().eq(ProcessRoute::getRouteDescription, productionOrder));
List<ProcessRoute> materialRows = processRouteMapper.selectList(
Wrappers.<ProcessRoute>lambdaQuery()
.eq(ProcessRoute::getRouteDescription, productionOrder)
.select(ProcessRoute::getMaterialCode));
long materialCount = 0L;
if (materialRows != null) {
materialCount = materialRows.stream()
.map(ProcessRoute::getMaterialCode)
.map(StringUtils::trimToEmpty)
.filter(StringUtils::isNotBlank)
.distinct()
.count();
}
ProcessOrderPro update = new ProcessOrderPro(); ProcessOrderPro update = new ProcessOrderPro();
update.setId(order.getId()); update.setId(order.getId());
update.setBomStatus(8L); update.setBomStatus(8L);
@ -306,16 +619,16 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
update.setRouteUptime(now); update.setRouteUptime(now);
update.setPlanUptime(now); update.setPlanUptime(now);
update.setBomUptime(now); update.setBomUptime(now);
update.setBomCount((long) summary.getSavedBomCount()); update.setBomCount(bomCount);
update.setMaterialCount((long) summary.getMaterialCount()); update.setMaterialCount(materialCount);
update.setRouteCount((long) summary.getSavedRouteCount()); update.setRouteCount(routeCount);
update.setSource(ProcessOrderProSource.PLM); update.setSource(ProcessOrderProSource.PLM);
update.setUpdateBy("PLM"); update.setUpdateBy("PLM");
update.setUpdateTime(now); update.setUpdateTime(now);
processOrderProMapper.updateById(update); processOrderProMapper.updateById(update);
log.info("PLM工艺计划已回写项目状态: 令号={}, id={}, bomStatus=8, routeStatus=1, bom={}, material={}, route={}", log.info("PLM工艺计划已回写项目状态: 令号={}, id={}, bomStatus=8, routeStatus=1, bom={}, material={}, route={}, appended={}, replaced={}",
productionOrder, order.getId(), productionOrder, order.getId(), bomCount, materialCount, routeCount,
summary.getSavedBomCount(), summary.getMaterialCount(), summary.getSavedRouteCount()); summary != null && summary.isAppended(), summary != null && summary.isReplaced());
} }
private List<BomDetails> buildBomDetails(PlmProcessRoutePushBo body, String productionOrder) { private List<BomDetails> buildBomDetails(PlmProcessRoutePushBo body, String productionOrder) {

View File

@ -22,6 +22,8 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.KingdeeMaterialCategoryHelper; import com.ruoyi.common.utils.KingdeeMaterialCategoryHelper;
import com.ruoyi.common.utils.KingdeeRouteDepartmentHelper; import com.ruoyi.common.utils.KingdeeRouteDepartmentHelper;
import com.ruoyi.common.utils.JsyyOrderNo;
import com.ruoyi.common.utils.ProjectStatusLock;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.controller.ProcessRouteController; import com.ruoyi.system.controller.ProcessRouteController;
import com.ruoyi.system.domain.*; import com.ruoyi.system.domain.*;
@ -907,7 +909,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
return bomDetails; return bomDetails;
} }
//图号以 009、AA、AB(、AC(、015、BC(、DIN、DNS、SF、两个空格开头,或图号含 (M),或备注含「外购件」
private boolean isOutsourced(String code) { private boolean isOutsourced(String code) {
return code.startsWith("009") || code.startsWith("AA") || code.startsWith("AB(") || code.startsWith("AC(") || code.startsWith("015") || code.contains("(M)") || code.startsWith("BC("); return code.startsWith("009") || code.startsWith("AA") || code.startsWith("AB(") || code.startsWith("AC(") || code.startsWith("015") || code.contains("(M)") || code.startsWith("BC(");
} }
@ -1133,10 +1135,39 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
@Override @Override
public R<ProcessRoutePushResultDTO> pushRouteBom(String rooteProdet, String groupName) { public R<ProcessRoutePushResultDTO> pushRouteBom(String rooteProdet, String groupName) {
return pushRouteBomInternal(rooteProdet, groupName, null, true);
}
@Override
public R<ProcessRoutePushResultDTO> pushRouteBomByMaterial(String rooteProdet, String groupName, Collection<String> materialCodes) {
if (CollUtil.isEmpty(materialCodes)) {
return R.fail("请选择要推送的物料");
}
return pushRouteBomInternal(rooteProdet, groupName, materialCodes, false);
}
/**
* @param materialCodes 为空则推送项目下全部物料;有值则只推这些物料
* @param updateProjectStatus 仅整单推送时回写项目状态与 routeLog
*/
private R<ProcessRoutePushResultDTO> pushRouteBomInternal(String rooteProdet, String groupName,
Collection<String> materialCodes,
boolean updateProjectStatus) {
String jsyyDeny = JsyyOrderNo.denyIfMatches(rooteProdet);
if (jsyyDeny != null) {
return R.fail(jsyyDeny);
}
//先查询此项目的工艺状态是否审核 //先查询此项目的工艺状态是否审核
LambdaQueryWrapper<ProcessOrderPro> wq = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ProcessOrderPro> wq = new LambdaQueryWrapper<>();
wq.eq(ProcessOrderPro::getProductionOrderNo, rooteProdet); wq.eq(ProcessOrderPro::getProductionOrderNo, rooteProdet);
ProcessOrderPro processOrderPro1 = processOrderProMapper.selectOne(wq); ProcessOrderPro processOrderPro1 = processOrderProMapper.selectOne(wq);
if (processOrderPro1 != null) {
String statusDeny = ProjectStatusLock.denyIfLocked(
processOrderPro1.getBomStatus(), ProjectStatusLock.Action.PUSH_ROUTE);
if (statusDeny != null) {
return R.fail(statusDeny);
}
}
if (processOrderPro1 != null && processOrderPro1.getRouteStatus() == 0) { if (processOrderPro1 != null && processOrderPro1.getRouteStatus() == 0) {
return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未审核,无法推送,请先审核"); return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未审核,无法推送,请先审核");
} }
@ -1144,6 +1175,29 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未通过,无法推送,请先审核"); return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未通过,无法推送,请先审核");
} }
List<ProcessRouteXuDTO> rawBomList = getProcessRoute(rooteProdet); List<ProcessRouteXuDTO> rawBomList = getProcessRoute(rooteProdet);
if (CollUtil.isNotEmpty(materialCodes)) {
Set<String> wanted = materialCodes.stream()
.filter(StringUtils::isNotBlank)
.map(s -> s.trim().toUpperCase(Locale.ROOT))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (wanted.isEmpty()) {
return R.fail("请选择要推送的物料");
}
rawBomList = rawBomList.stream()
.filter(r -> r != null && StringUtils.isNotBlank(r.getMaterialCode())
&& wanted.contains(r.getMaterialCode().trim().toUpperCase(Locale.ROOT)))
.collect(Collectors.toList());
if (rawBomList.isEmpty()) {
return R.fail("项目 " + rooteProdet + " 下未找到所选物料的可推送工艺");
}
Set<String> found = rawBomList.stream()
.map(r -> r.getMaterialCode().trim().toUpperCase(Locale.ROOT))
.collect(Collectors.toSet());
List<String> missing = wanted.stream().filter(c -> !found.contains(c)).collect(Collectors.toList());
if (!missing.isEmpty()) {
return R.fail("项目 " + rooteProdet + " 下未找到物料 " + String.join("、", missing) + " 的可推送工艺");
}
}
List<ProcessRouteXuDTO> successfulRoutes = new ArrayList<>(); List<ProcessRouteXuDTO> successfulRoutes = new ArrayList<>();
List<ProcessRouteXuDTO> failedRoutes = new ArrayList<>(); List<ProcessRouteXuDTO> failedRoutes = new ArrayList<>();
List<String> duplicateRoutes = new ArrayList<>(); List<String> duplicateRoutes = new ArrayList<>();
@ -1181,13 +1235,15 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
resultDTO.setSuccessfulRoutes(successfulRoutes); resultDTO.setSuccessfulRoutes(successfulRoutes);
resultDTO.setFailedRoutes(failedRoutes); resultDTO.setFailedRoutes(failedRoutes);
resultDTO.setDuplicateRoutes(duplicateRoutes); resultDTO.setDuplicateRoutes(duplicateRoutes);
String jsonStr = JSONUtil.toJsonStr(resultDTO); if (updateProjectStatus) {
ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(rooteProdet); String jsonStr = JSONUtil.toJsonStr(resultDTO);
//更新项目状态 推送工艺 ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(rooteProdet);
processOrderPro.setBomStatus(3L); //更新项目状态 推送工艺
processOrderPro.setRouteLog(jsonStr); processOrderPro.setBomStatus(3L);
log.info("<UNK>: " + jsonStr); processOrderPro.setRouteLog(jsonStr);
processOrderProMapper.updateById(processOrderPro); log.info("<UNK>: " + jsonStr);
processOrderProMapper.updateById(processOrderPro);
}
return R.ok(resultDTO); return R.ok(resultDTO);
} }
@ -1260,95 +1316,116 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
} }
/** /**
* 查询工艺路线单据体 * 查询工艺路线单据体:按生产令号查金蝶 PRD_MO,再拉工序计划与用料。
*
* @param rooteProdet
* @return
*/ */
@Override @Override
public List<CombinedDTO> getSelecPlanRouteList(String rooteProdet) { public List<CombinedDTO> getSelecPlanRouteList(String rooteProdet) {
// 根据生产令号查询生产订单编码,获取计划单据编号和工艺路线 return getSelecPlanRouteList(rooteProdet, null);
List<PlanOrderVo> planOrderList = getSelectProceOrder1(rooteProdet); }
log.info("获取计划单号集合: planOrderList=====>" + planOrderList); /**
* 用令号 + 物料编码查金蝶 PRD_MO,再组装工序/用料。
*/
public List<CombinedDTO> getSelecPlanRouteList(String rooteProdet, Collection<String> materialCodes) {
List<PlanOrderVo> planOrderList = getSelectProceOrder1(rooteProdet, materialCodes);
if (planOrderList == null || planOrderList.isEmpty()) {
return new ArrayList<>();
}
// 金蝶过滤后再按物料编码兜底筛一次,避免大小写/空格差异
if (CollUtil.isNotEmpty(materialCodes)) {
Set<String> wanted = materialCodes.stream()
.filter(StringUtils::isNotBlank)
.map(s -> s.trim().toUpperCase())
.collect(Collectors.toSet());
if (!wanted.isEmpty()) {
planOrderList = planOrderList.stream()
.filter(o -> o != null && StringUtils.isNotBlank(o.getFmaterialidFnumber())
&& wanted.contains(o.getFmaterialidFnumber().trim().toUpperCase()))
.collect(Collectors.toList());
}
}
log.info("获取计划单号集合(令号={}, 物料={}): planOrderList=====>{}", rooteProdet, materialCodes, planOrderList);
return buildCombinedFromPlanOrders(planOrderList);
}
// 初始化空列表 /** 按金蝶生产订单逐张拉取工序计划、用料清单并组装 CombinedDTO。 */
List<PlannedProcessVo> plannedProcesses = new ArrayList<>(); private List<CombinedDTO> buildCombinedFromPlanOrders(List<PlanOrderVo> planOrderList) {
List<CombinedDTO> combinedVoList = new ArrayList<>(); List<CombinedDTO> combinedVoList = new ArrayList<>();
if (planOrderList == null || planOrderList.isEmpty()) { if (planOrderList == null || planOrderList.isEmpty()) {
return combinedVoList; // 返回空列表避免空指针异常 return combinedVoList;
} }
K3CloudApi client = new K3CloudApi();
for (PlanOrderVo planOrder : planOrderList) { for (PlanOrderVo planOrder : planOrderList) {
// 请求参数,要求为json字符串
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("FormId", "SFC_OperationPlanning"); json.addProperty("FormId", "SFC_OperationPlanning");
json.addProperty("FieldKeys", "FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName"); json.addProperty("FieldKeys", "FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName");
// 创建过滤条件
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject(); JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "FMONumber"); filterObject.addProperty("FieldName", "FMONumber");
filterObject.addProperty("Compare", "="); // 改为等号运算符 filterObject.addProperty("Compare", "=");
filterObject.addProperty("Value", planOrder.getFBillNo()); filterObject.addProperty("Value", planOrder.getFBillNo());
filterObject.addProperty("Left", ""); filterObject.addProperty("Left", "");
filterObject.addProperty("Right", ""); filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号 filterObject.addProperty("Logic", 0);
filterString.add(filterObject); filterString.add(filterObject);
json.add("FilterString", filterString); json.add("FilterString", filterString);
json.addProperty("OrderString", ""); json.addProperty("OrderString", "");
json.addProperty("TopRowCount", 0); json.addProperty("TopRowCount", 0);
json.addProperty("StartRow", 0); json.addProperty("StartRow", 0);
json.addProperty("Limit", 2000); json.addProperty("Limit", 2000);
json.addProperty("SubSystemId", ""); json.addProperty("SubSystemId", "");
String jsonData = json.toString();
try { try {
// 调用接口 K3CloudApi client = new K3CloudApi();
String resultJson = String.valueOf(client.billQuery(jsonData)); String resultJson = String.valueOf(client.billQuery(json.toString()));
JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class);
// 使用 ObjectMapper 将 JsonArray 转换为 List<PlannedProcessVo>
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
List<PlannedProcessVo> plannedProcessList = objectMapper.readValue(jsonArray.toString(), new TypeReference<List<PlannedProcessVo>>() { List<PlannedProcessVo> plannedProcessList = objectMapper.readValue(jsonArray.toString(),
}); new TypeReference<List<PlannedProcessVo>>() {
});
// 检查 plannedProcessList 是否为 null,避免空指针异常
if (plannedProcessList != null && !plannedProcessList.isEmpty()) {
plannedProcesses.addAll(plannedProcessList); // 将非空列表添加到最终列表中
}
// 获取用料清单列表
List<MaterialUsageDTO> materialUseList = getSelectMaterialUseList(planOrder.getFBillNo()); List<MaterialUsageDTO> materialUseList = getSelectMaterialUseList(planOrder.getFBillNo());
// 4. 将 PlanOrderVo 和对应的 PlannedProcessVo 合并成一个对象
CombinedDTO combined = new CombinedDTO(); CombinedDTO combined = new CombinedDTO();
combined.setOrderNumber(planOrder.getFBillNo()); // 生产订单号 combined.setOrderNumber(planOrder.getFBillNo());
combined.setMaterialCode(planOrder.getFmaterialidFnumber()); // 物料编码 combined.setMaterialCode(planOrder.getFmaterialidFnumber());
combined.setMaterialName(planOrder.getFMaterialName()); // 物料名称 combined.setMaterialName(planOrder.getFMaterialName());
combined.setProcesses(plannedProcessList); combined.setProcesses(plannedProcessList);
combined.setMaterial(planOrder.getBasePropertyqtr()); combined.setMaterial(planOrder.getBasePropertyqtr());
combined.setMaterialUsageDTOList(materialUseList); combined.setMaterialUsageDTOList(materialUseList);
// 5. 添加到最终合并的列表中
combinedVoList.add(combined); combinedVoList.add(combined);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); // 输出异常日志 log.error("组装工序计划单失败 订单号={}", planOrder.getFBillNo(), e);
} }
} }
return combinedVoList;
return combinedVoList; // 返回结果
} }
/**
* 物料编码为空:令号下全部工序计划单打 zip;有物料编码:该物料一份 PDF。
*/
@Override @Override
public String generatePDFs(String rooteProdet) { public String generatePDFs(String rooteProdet, String materialCode) {
if (StringUtils.isNotBlank(materialCode)) {
String code = materialCode.trim();
List<CombinedDTO> selecPlanRouteList = getSelecPlanRouteList(rooteProdet, Collections.singletonList(code));
if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) {
throw new ServiceException("未查询到令号「" + rooteProdet + "」物料「" + code + "」的金蝶生产订单");
}
String singlePdfName = rooteProdet + "_" + code.replaceAll("[\\\\/:*?\"<>|]", "_") + "_工序计划单.pdf";
String path = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet,
Collections.singletonList(pdfFontPath), singlePdfName);
if (StringUtils.isBlank(path)) {
throw new ServiceException("PDF生成失败");
}
return path;
}
List<CombinedDTO> selecPlanRouteList = getSelecPlanRouteList(rooteProdet); List<CombinedDTO> selecPlanRouteList = getSelecPlanRouteList(rooteProdet);
if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) { if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) {
throw new ServiceException("未查询到可生成PDF的工艺数据"); throw new ServiceException("未查询到令号「" + rooteProdet + "」的金蝶生产订单");
} }
String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet, Collections.singletonList(pdfFontPath)); String path = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet,
if (StringUtils.isBlank(pathZIP)) { Collections.singletonList(pdfFontPath));
throw new ServiceException("PDF压缩包生成失败"); if (StringUtils.isBlank(path)) {
throw new ServiceException("PDF生成失败");
} }
return pathZIP; return path;
} }
public List<PlanOrderVo> getSelectProceOrder(String rooteProdet) { public List<PlanOrderVo> getSelectProceOrder(String rooteProdet) {
@ -1395,26 +1472,49 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
} }
public List<PlanOrderVo> getSelectProceOrder1(String rooteProdet) { public List<PlanOrderVo> getSelectProceOrder1(String rooteProdet) {
return getSelectProceOrder1(rooteProdet, null);
}
/**
* 查金蝶生产订单 PRD_MO:令号(项目号或生产令号)+ 可选物料编码。
*/
public List<PlanOrderVo> getSelectProceOrder1(String rooteProdet, Collection<String> materialCodes) {
K3CloudApi client = new K3CloudApi(); K3CloudApi client = new K3CloudApi();
// 第一次查询:使用 F_HBYT_SCLH return queryWithFieldName(client, "F_HBYT_XMH", rooteProdet, materialCodes);
List<PlanOrderVo> result = queryWithFieldName(client, "F_HBYT_XMH", rooteProdet);
return result;
} }
/* /*
根据生产令号查询 返回订单编号 物料编码名称 根据生产令号查询 返回订单编号 物料编码名称
*/ */
private List<PlanOrderVo> queryWithFieldName(K3CloudApi client, String fieldName, String rooteProdet) { private List<PlanOrderVo> queryWithFieldName(K3CloudApi client, String fieldName, String rooteProdet) {
return queryWithFieldName(client, fieldName, rooteProdet, null);
}
/**
* 金蝶 PRD_MO:条件为 (项目号 或 生产令号) ,有物料编码时再 AND 物料。
*/
private List<PlanOrderVo> queryWithFieldName(K3CloudApi client, String fieldName, String rooteProdet,
Collection<String> materialCodes) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("FormId", "PRD_MO"); json.addProperty("FormId", "PRD_MO");
json.addProperty("FieldKeys", "F_HBYT_XMH,F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr"); json.addProperty("FieldKeys", "F_HBYT_XMH,F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr");
List<String> codes = new ArrayList<>();
if (CollUtil.isNotEmpty(materialCodes)) {
for (String code : materialCodes) {
if (StringUtils.isNotBlank(code)) {
codes.add(code.trim());
}
}
}
boolean hasMaterial = !codes.isEmpty();
JsonArray filterString = new JsonArray(); JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject(); JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", fieldName); // 使用传入的 fieldName filterObject.addProperty("FieldName", fieldName); // 使用传入的 fieldName
filterObject.addProperty("Compare", "67"); filterObject.addProperty("Compare", "67");
filterObject.addProperty("Value", rooteProdet); filterObject.addProperty("Value", rooteProdet);
filterObject.addProperty("Left", ""); filterObject.addProperty("Left", hasMaterial ? "(" : "");
filterObject.addProperty("Right", ""); filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 1); filterObject.addProperty("Logic", 1);
filterString.add(filterObject); filterString.add(filterObject);
@ -1424,10 +1524,28 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
filterObject1.addProperty("Compare", "67"); filterObject1.addProperty("Compare", "67");
filterObject1.addProperty("Value", rooteProdet); filterObject1.addProperty("Value", rooteProdet);
filterObject1.addProperty("Left", ""); filterObject1.addProperty("Left", "");
filterObject1.addProperty("Right", ""); filterObject1.addProperty("Right", hasMaterial ? ")" : "");
filterObject1.addProperty("Logic", 1); // 有物料时括号内 OR 结束,再 AND 物料;无物料时保持原来的令号 OR
filterObject1.addProperty("Logic", hasMaterial ? 0 : 1);
filterString.add(filterObject1); filterString.add(filterObject1);
// 令号命中后再按物料编码收窄,一个物料对应金蝶里该物料的生产订单
if (hasMaterial) {
JsonObject matFilter = new JsonObject();
matFilter.addProperty("FieldName", "FMaterialId.FNumber");
if (codes.size() == 1) {
matFilter.addProperty("Compare", "=");
matFilter.addProperty("Value", codes.get(0));
} else {
matFilter.addProperty("Compare", "in");
matFilter.addProperty("Value", String.join(",", codes));
}
matFilter.addProperty("Left", "");
matFilter.addProperty("Right", "");
matFilter.addProperty("Logic", 0);
filterString.add(matFilter);
}
json.add("FilterString", filterString); json.add("FilterString", filterString);
json.addProperty("OrderString", ""); json.addProperty("OrderString", "");
json.addProperty("TopRowCount", 0); json.addProperty("TopRowCount", 0);

View File

@ -29,6 +29,7 @@ import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveSummary;
import com.ruoyi.system.domain.dto.productionorder.AssemblyBomToBomDetailsConverter; import com.ruoyi.system.domain.dto.productionorder.AssemblyBomToBomDetailsConverter;
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO; import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO;
import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO; import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO;
import com.ruoyi.system.domain.bo.plm.PlmGeneralDrawingUtils;
import com.ruoyi.system.domain.bo.plm.PlmOrderMaterialMatchUtils; import com.ruoyi.system.domain.bo.plm.PlmOrderMaterialMatchUtils;
import com.ruoyi.system.domain.vo.ProcessOrderProVo; import com.ruoyi.system.domain.vo.ProcessOrderProVo;
import com.ruoyi.system.mapper.FigureSaveMapper; import com.ruoyi.system.mapper.FigureSaveMapper;
@ -503,7 +504,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
throw new ServiceException("总装配 BOM 数据不能为空"); throw new ServiceException("总装配 BOM 数据不能为空");
} }
String productionOrderNo = AssemblyBomReceiveConverter.resolveProductionOrderNo(items); String productionOrderNo = AssemblyBomReceiveConverter.resolveProductionOrderNo(items);
requireProductionOrderExists(productionOrderNo); ProcessOrderPro project = requireProductionOrderExists(productionOrderNo);
// 外购件尾部 (2D)/(2D)去掉,避免上传金蝶一物两码 // 外购件尾部 (2D)/(2D)去掉,避免上传金蝶一物两码
AssemblyBomReceiveConverter.normalizeDrawingCodes(items); AssemblyBomReceiveConverter.normalizeDrawingCodes(items);
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
@ -528,11 +529,34 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
throw new ServiceException("总装配 BOM 无有效行数据"); throw new ServiceException("总装配 BOM 无有效行数据");
} }
LambdaQueryWrapper<ProductionOrder> existsQuery = Wrappers.lambdaQuery(); List<ProductionOrder> existingRows = baseMapper.selectList(
existsQuery.eq(ProductionOrder::getProductionOrderNo, productionOrderNo); Wrappers.<ProductionOrder>lambdaQuery()
boolean replaced = baseMapper.selectCount(existsQuery) > 0; .eq(ProductionOrder::getProductionOrderNo, productionOrderNo)
if (replaced) { .select(ProductionOrder::getId, ProductionOrder::getMainPart, ProductionOrder::getDrawingNo));
baseMapper.deleteWithProCode(productionOrderNo); boolean hasExisting = existingRows != null && !existingRows.isEmpty();
Set<String> existingKeys = collectAssemblyProductKeysFromOrders(existingRows);
Set<String> incomingKeys = collectAssemblyProductKeysFromItems(effectiveItems);
Set<String> overlap = PlmGeneralDrawingUtils.intersection(incomingKeys, existingKeys);
boolean canCompare = !existingKeys.isEmpty() && !incomingKeys.isEmpty();
boolean append = hasExisting && canCompare && overlap.isEmpty();
boolean replaced = false;
if (hasExisting && !append) {
if (PlmGeneralDrawingUtils.isRouteApproved(project.getRouteStatus())) {
throw new ServiceException("生产令号 " + productionOrderNo + " 工艺状态已审核,不允许再推送");
}
replaced = true;
if (canCompare && !overlap.isEmpty()) {
deleteAssemblyByProductKeys(productionOrderNo, overlap);
log.info("[PLM-总装] 未审核同主产品覆盖: order={}, mainPart={}", productionOrderNo, overlap);
} else {
baseMapper.deleteWithProCode(productionOrderNo);
log.info("[PLM-总装] 未审核全量覆盖: order={}, existingKeys={}, incomingKeys={}",
productionOrderNo, existingKeys, incomingKeys);
}
} else if (append) {
log.info("[PLM-总装] 主产品图号不同,追加写入: order={}, existing={}, incoming={}",
productionOrderNo, existingKeys, incomingKeys);
} }
int inserted = 0; int inserted = 0;
@ -546,7 +570,9 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
throw new ServiceException("总装配 BOM 入库失败"); throw new ServiceException("总装配 BOM 入库失败");
} }
int bomDetailsCount = syncAssemblyBomDetails(productionOrderNo, entities); List<ProductionOrder> allRows = baseMapper.selectList(
Wrappers.<ProductionOrder>lambdaQuery().eq(ProductionOrder::getProductionOrderNo, productionOrderNo));
int bomDetailsCount = syncAssemblyBomDetails(productionOrderNo, allRows);
// 子令号:按本次图号清理总令号侧重复总装行 // 子令号:按本次图号清理总令号侧重复总装行
Set<String> pushDrawingNos = effectiveItems.stream() Set<String> pushDrawingNos = effectiveItems.stream()
@ -562,14 +588,82 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
summary.setLineCount(effectiveItems.size()); summary.setLineCount(effectiveItems.size());
summary.setInsertedCount(inserted); summary.setInsertedCount(inserted);
summary.setReplaced(replaced); summary.setReplaced(replaced);
markProjectAfterPlmAssemblyBom(productionOrderNo, inserted); summary.setAppended(append);
log.info("总装配 BOM 接收完成 productionOrderNo={} lineCount={} inserted={} replaced={} bomDetails={}", int totalLines = allRows == null ? inserted : allRows.size();
productionOrderNo, effectiveItems.size(), inserted, replaced, bomDetailsCount); markProjectAfterPlmAssemblyBom(productionOrderNo, totalLines);
log.info("总装配 BOM 接收完成 productionOrderNo={} lineCount={} inserted={} replaced={} appended={} bomDetails={}",
productionOrderNo, effectiveItems.size(), inserted, replaced, append, bomDetailsCount);
return summary; return summary;
} }
private static Set<String> collectAssemblyProductKeysFromOrders(List<ProductionOrder> rows) {
Set<String> keys = new LinkedHashSet<>();
if (rows == null) {
return keys;
}
for (ProductionOrder row : rows) {
String key = resolveAssemblyProductKey(row);
if (StringUtils.isNotBlank(key)) {
keys.add(key);
}
}
return keys;
}
private static Set<String> collectAssemblyProductKeysFromItems(List<AssemblyBomReceiveItemDTO> items) {
Set<String> keys = new LinkedHashSet<>();
if (items == null) {
return keys;
}
for (AssemblyBomReceiveItemDTO item : items) {
String key = resolveAssemblyProductKey(item);
if (StringUtils.isNotBlank(key)) {
keys.add(key);
}
}
return keys;
}
/** 主产品图号优先;为空时回退图号结尾 .00 的总图号。 */
private static String resolveAssemblyProductKey(ProductionOrder row) {
if (row == null) {
return null;
}
if (StringUtils.isNotBlank(row.getMainPart())) {
return row.getMainPart().trim();
}
if (PlmGeneralDrawingUtils.isGeneralDrawingNo(row.getDrawingNo())) {
return row.getDrawingNo().trim();
}
return null;
}
private static String resolveAssemblyProductKey(AssemblyBomReceiveItemDTO item) {
if (item == null) {
return null;
}
if (StringUtils.isNotBlank(item.getProductDrawingNo())) {
return item.getProductDrawingNo().trim();
}
if (PlmGeneralDrawingUtils.isGeneralDrawingNo(item.getDrawingNo())) {
return item.getDrawingNo().trim();
}
return null;
}
private void deleteAssemblyByProductKeys(String productionOrderNo, Set<String> productKeys) {
if (StringUtils.isBlank(productionOrderNo) || productKeys == null || productKeys.isEmpty()) {
return;
}
baseMapper.delete(Wrappers.<ProductionOrder>lambdaQuery()
.eq(ProductionOrder::getProductionOrderNo, productionOrderNo)
.and(w -> w.in(ProductionOrder::getMainPart, productKeys)
.or()
.in(ProductionOrder::getDrawingNo, productKeys)));
}
/** /**
* 总令号总装入库前:去掉子令号已有、且图号前缀归属本令号的行。 * 总令号总装入库前:去掉子令号已有相同图号的行,并清理总令库中对应行。
*/ */
private List<AssemblyBomReceiveItemDTO> filterParentAssemblyDuplicatedInChild( private List<AssemblyBomReceiveItemDTO> filterParentAssemblyDuplicatedInChild(
List<AssemblyBomReceiveItemDTO> items, String productionOrderNo) { List<AssemblyBomReceiveItemDTO> items, String productionOrderNo) {
@ -577,8 +671,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
return items; return items;
} }
if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrderNo) if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrderNo)
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrderNo) || PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrderNo)) {
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(productionOrderNo) == null) {
return items; return items;
} }
Set<String> childCodes = collectChildAssemblyDrawingNos(productionOrderNo); Set<String> childCodes = collectChildAssemblyDrawingNos(productionOrderNo);
@ -597,8 +690,15 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
} }
kept.add(item); kept.add(item);
} }
log.info("[PLM-总装去重] 总令号过滤完成: parent={}, before={}, after={}", int deletedPo = baseMapper.delete(Wrappers.<ProductionOrder>lambdaQuery()
productionOrderNo, items.size(), kept.size()); .eq(ProductionOrder::getProductionOrderNo, productionOrderNo)
.in(ProductionOrder::getDrawingNo, childCodes));
int deletedBom = bomDetailsMapper.delete(Wrappers.<BomDetails>lambdaQuery()
.eq(BomDetails::getTotalWeight, productionOrderNo)
.eq(BomDetails::getBomType, AssemblyBomToBomDetailsConverter.BOM_TYPE_ASSEMBLY)
.in(BomDetails::getPartNumber, childCodes));
log.info("[PLM-总装去重] 总令号过滤完成: parent={}, before={}, after={}, childCodes={}, deletedPo={}, deletedBom={}",
productionOrderNo, items.size(), kept.size(), childCodes.size(), deletedPo, deletedBom);
return kept; return kept;
} }
@ -608,15 +708,15 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
return; return;
} }
String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrderNo); String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrderNo);
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent) if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)) {
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(parent) == null) {
return; return;
} }
Set<String> codes = new LinkedHashSet<>(); Set<String> codes = new LinkedHashSet<>();
if (drawingNos != null) { if (drawingNos != null) {
for (String code : drawingNos) { for (String code : drawingNos) {
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, parent)) { String trimmed = StringUtils.trimToEmpty(code);
codes.add(StringUtils.trimToEmpty(code)); if (StringUtils.isNotBlank(trimmed)) {
codes.add(trimmed);
} }
} }
} }
@ -666,7 +766,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
continue; continue;
} }
String drawingNo = StringUtils.trimToEmpty(row.getDrawingNo()); String drawingNo = StringUtils.trimToEmpty(row.getDrawingNo());
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(drawingNo, parentOrderNo)) { if (StringUtils.isNotBlank(drawingNo)) {
codes.add(drawingNo); codes.add(drawingNo);
} }
} }
@ -710,7 +810,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
} }
/** 接收前校验:process_order_pro.production_order_no 必须已存在,否则拒绝接收。 */ /** 接收前校验:process_order_pro.production_order_no 必须已存在,否则拒绝接收。 */
private void requireProductionOrderExists(String productionOrderNo) { private ProcessOrderPro requireProductionOrderExists(String productionOrderNo) {
if (StringUtils.isBlank(productionOrderNo)) { if (StringUtils.isBlank(productionOrderNo)) {
throw new ServiceException("生产令号不能为空"); throw new ServiceException("生产令号不能为空");
} }
@ -719,6 +819,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
if (order == null) { if (order == null) {
throw new ServiceException("生产令号 " + orderNo + " 不存在"); throw new ServiceException("生产令号 " + orderNo + " 不存在");
} }
return order;
} }
/** /**
@ -742,6 +843,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
update.setBomUptime(now); update.setBomUptime(now);
update.setMaterialCount((long) lineCount); update.setMaterialCount((long) lineCount);
update.setSource(ProcessOrderProSource.PLM); update.setSource(ProcessOrderProSource.PLM);
update.setDrawingQtyIssued(0);
update.setUpdateBy("PLM"); update.setUpdateBy("PLM");
update.setUpdateTime(now); update.setUpdateTime(now);
processOrderProMapper.updateById(update); processOrderProMapper.updateById(update);

View File

@ -2,22 +2,25 @@ package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.RatioAccessory; import com.ruoyi.system.domain.RatioAccessory;
import com.ruoyi.system.domain.RatioConfig; import com.ruoyi.system.domain.RatioConfig;
import com.ruoyi.system.domain.RatioModel;
import com.ruoyi.system.domain.dto.ProMoDTO; import com.ruoyi.system.domain.dto.ProMoDTO;
import com.ruoyi.system.domain.dto.PurchaseOrderDTO; import com.ruoyi.system.domain.dto.PurchaseOrderDTO;
import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO; import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO;
import com.ruoyi.system.domain.dto.ratio.RatioMaterialItemDTO; import com.ruoyi.system.domain.dto.ratio.RatioMaterialItemDTO;
import com.ruoyi.system.domain.dto.ratio.RatioModelOptionDTO;
import com.ruoyi.system.domain.dto.ratio.RatioTransitMoDTO; import com.ruoyi.system.domain.dto.ratio.RatioTransitMoDTO;
import com.ruoyi.system.domain.dto.ratio.RatioTransitPoDTO; import com.ruoyi.system.domain.dto.ratio.RatioTransitPoDTO;
import com.ruoyi.system.mapper.RatioAccessoryMapper; import com.ruoyi.system.mapper.RatioAccessoryMapper;
import com.ruoyi.system.mapper.RatioConfigMapper; import com.ruoyi.system.mapper.RatioConfigMapper;
import com.ruoyi.system.mapper.RatioModelMapper;
import com.ruoyi.system.runner.JdUtil; import com.ruoyi.system.runner.JdUtil;
import com.ruoyi.system.service.IRatioBuildCapacityService; import com.ruoyi.system.service.IRatioBuildCapacityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Collections; import java.util.Collections;
@ -31,33 +34,90 @@ import java.util.stream.Collectors;
public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService { public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService {
private final RatioConfigMapper ratioConfigMapper; private final RatioConfigMapper ratioConfigMapper;
private final RatioModelMapper ratioModelMapper;
private final RatioAccessoryMapper ratioAccessoryMapper; private final RatioAccessoryMapper ratioAccessoryMapper;
@Override @Override
public RatioBuildCapacityDTO queryByRatio(BigDecimal ratioValue, Integer qty) { public List<String> listRatios(String seriesType) {
// ratioValue: ratio_config.ratio_value(速比),qty: 页面输入的目标数量(本实现口径:BOM每个物料需求数量=qty) seriesType = normalizeSeries(seriesType);
if (ratioValue == null) { List<RatioConfig> configs = ratioConfigMapper.selectList(new LambdaQueryWrapper<RatioConfig>()
throw new ServiceException("速比不能为空"); .eq(RatioConfig::getSeriesType, seriesType)
.eq(RatioConfig::getStatus, 0));
List<String> ratios = new ArrayList<>();
if (configs == null) {
return ratios;
}
for (RatioConfig config : configs) {
if (config != null && StringUtils.isNotBlank(config.getRatioValue())) {
ratios.add(config.getRatioValue().trim());
}
}
ratios.sort(this::compareRatio);
return ratios;
}
@Override
public List<RatioModelOptionDTO> listModels(String seriesType, String ratioValue) {
RatioConfig config = findConfig(seriesType, ratioValue);
List<RatioModel> models = ratioModelMapper.selectList(new LambdaQueryWrapper<RatioModel>()
.eq(RatioModel::getRatioConfigId, config.getId())
.eq(RatioModel::getStatus, 0)
.orderByAsc(RatioModel::getModelCode));
List<RatioModelOptionDTO> options = new ArrayList<>();
if (models == null) {
return options;
}
for (RatioModel model : models) {
RatioModelOptionDTO option = new RatioModelOptionDTO();
option.setId(model.getId());
option.setModelCode(model.getModelCode());
options.add(option);
}
return options;
}
@Override
public RatioBuildCapacityDTO queryByRatio(String seriesType, String ratioValue, Long modelId, Integer qty) {
// seriesType: KP08 / KP14;ratioValue: 速比,如 91B;qty: 套数。需求数量 = Excel 单台数量 × 套数。
if (modelId == null) {
throw new ServiceException("请选择减速机型号");
} }
int inputQty = qty == null ? 0 : qty; int inputQty = qty == null ? 0 : qty;
if (inputQty <= 0) { if (inputQty <= 0) {
throw new ServiceException("数量必须大于0"); throw new ServiceException("数量必须大于0");
} }
// 1) 查速比主表 ratio_config,按速比值+启用状态获取一条配置(包含 maxTorqueNm 等扩展信息) RatioConfig config = findConfig(seriesType, ratioValue);
RatioConfig config = ratioConfigMapper.selectOne(new LambdaQueryWrapper<RatioConfig>() RatioModel model = ratioModelMapper.selectById(modelId);
.eq(RatioConfig::getRatioValue, ratioValue) if (model == null || !config.getId().equals(model.getRatioConfigId())) {
.eq(RatioConfig::getStatus, 0) throw new ServiceException("减速机型号不属于该速比");
.last("limit 1"));
if (config == null) {
throw new ServiceException("未找到对应速比配置");
} }
// 2) 查速比对应的BOM明细 ratio_accessory:每条代表一个需要齐套的物料(物料编码/名称/类别/单重/材质等) // 通用件挂在速比上。轴套、行星架、法兰只挂在所选型号上,没有的类别不补空行。
List<RatioAccessory> accessories = ratioAccessoryMapper.selectList(new LambdaQueryWrapper<RatioAccessory>() List<RatioAccessory> scoped = ratioAccessoryMapper.selectList(new LambdaQueryWrapper<RatioAccessory>()
.eq(RatioAccessory::getRatioConfigId, config.getId()) .eq(RatioAccessory::getRatioConfigId, config.getId())
.eq(RatioAccessory::getStatus, 0)); .eq(RatioAccessory::getStatus, 0)
if (accessories == null || accessories.isEmpty()) { .and(w -> w.isNull(RatioAccessory::getRatioModelId)
.or()
.eq(RatioAccessory::getRatioModelId, model.getId())));
List<RatioAccessory> accessories = new ArrayList<>();
boolean hasModelPart = false;
if (scoped != null) {
for (RatioAccessory accessory : scoped) {
if (accessory == null) {
continue;
}
if (accessory.getRatioModelId() == null || "通用件".equals(accessory.getPartCategory())) {
accessories.add(accessory);
continue;
}
if (modelPartKind(accessory.getPartCategory()) != null) {
accessories.add(accessory);
hasModelPart = true;
}
}
}
if (accessories.isEmpty()) {
throw new ServiceException("该速比未配置BOM明细"); throw new ServiceException("该速比未配置BOM明细");
} }
@ -69,8 +129,11 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
} }
String materialCode = a.getMaterialCode(); String materialCode = a.getMaterialCode();
// 3) requiredQty:该物料需求数量(当前按你的要求取“页面输入的数量”,没有乘BOM用量系数) double bomQty = a.getBomQty() == null ? 1D : a.getBomQty().doubleValue();
double requiredQty = inputQty; if (bomQty <= 0) {
bomQty = 1D;
}
double requiredQty = bomQty * inputQty;
// 4) 即时库存(onHandQty):金蝶 STK_Inventory 的基本单位库存汇总(JdUtil.getKuCun -> FBaseQty) // 4) 即时库存(onHandQty):金蝶 STK_Inventory 的基本单位库存汇总(JdUtil.getKuCun -> FBaseQty)
double onHandQty = Optional.ofNullable(JdUtil.getKuCun(materialCode)) double onHandQty = Optional.ofNullable(JdUtil.getKuCun(materialCode))
@ -99,8 +162,8 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
double availableQty = Math.max(0.0, onHandQty - moNotPickQty); double availableQty = Math.max(0.0, onHandQty - moNotPickQty);
// 9) 最终可用量 = 可用量 + 采购在途 + 生产在途 // 9) 最终可用量 = 可用量 + 采购在途 + 生产在途
double finalAvailableQty = availableQty + poNotInQty + moNotInQty; double finalAvailableQty = availableQty + poNotInQty + moNotInQty;
// 10) 可支持数量:按最终可用量取整向下(当前实现等价于“能做多少套”,因为 requiredQty=输入数量) // 10) 可支持套数:最终可用 ÷ Excel 单台数量,向下取整
long canSupportQty = (long) Math.floor(finalAvailableQty); long canSupportQty = (long) Math.floor(finalAvailableQty / bomQty);
maxBuildQty = Math.min(maxBuildQty, canSupportQty); maxBuildQty = Math.min(maxBuildQty, canSupportQty);
// 11) 补差数量:需求 - 最终可用(>0 表示缺多少) // 11) 补差数量:需求 - 最终可用(>0 表示缺多少)
double shortageQty = Math.max(0.0, requiredQty - finalAvailableQty); double shortageQty = Math.max(0.0, requiredQty - finalAvailableQty);
@ -111,6 +174,7 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
item.setMaterialName(a.getMaterialName()); item.setMaterialName(a.getMaterialName());
item.setDanzhong(a.getDanzhong()); item.setDanzhong(a.getDanzhong());
item.setCanzhi(a.getCanzhi()); item.setCanzhi(a.getCanzhi());
item.setBomQty(bomQty);
item.setRequiredQty(requiredQty); item.setRequiredQty(requiredQty);
item.setOnHandQty(onHandQty); item.setOnHandQty(onHandQty);
item.setMoNotPickQty(moNotPickQty); item.setMoNotPickQty(moNotPickQty);
@ -126,19 +190,125 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
} }
items = items.stream() items = items.stream()
.sorted(Comparator.comparing(RatioMaterialItemDTO::getMaterialCode, Comparator.nullsLast(String::compareTo))) .sorted(Comparator
.comparingInt((RatioMaterialItemDTO item) -> categoryOrder(item.getPartCategory()))
.thenComparing(RatioMaterialItemDTO::getMaterialCode, Comparator.nullsLast(String::compareTo)))
.collect(Collectors.toList()); .collect(Collectors.toList());
RatioBuildCapacityDTO dto = new RatioBuildCapacityDTO(); RatioBuildCapacityDTO dto = new RatioBuildCapacityDTO();
dto.setRatioConfigId(config.getId()); dto.setRatioConfigId(config.getId());
dto.setSeriesType(config.getSeriesType());
dto.setRatioValue(config.getRatioValue()); dto.setRatioValue(config.getRatioValue());
dto.setModelId(model.getId());
dto.setModelCode(model.getModelCode());
dto.setMaxTorqueNm(config.getMaxTorqueNm()); dto.setMaxTorqueNm(config.getMaxTorqueNm());
dto.setInputQty(inputQty); dto.setInputQty(inputQty);
dto.setMaxBuildQty(maxBuildQty == Long.MAX_VALUE ? 0L : maxBuildQty); dto.setMaxBuildQty(maxBuildQty == Long.MAX_VALUE ? 0L : maxBuildQty);
dto.setOnlyCommonParts(!hasModelPart);
dto.setItems(items); dto.setItems(items);
return dto; return dto;
} }
private String normalizeSeries(String seriesType) {
if (seriesType == null) {
throw new ServiceException("请选择型号 KP08 或 KP14");
}
seriesType = seriesType.trim().toUpperCase();
if (!"KP08".equals(seriesType) && !"KP14".equals(seriesType)) {
throw new ServiceException("型号只能是 KP08 或 KP14");
}
return seriesType;
}
/** 同一字母按数字从大到小,例如 160A、147A,再排到 B、C、D。 */
private int compareRatio(String left, String right) {
int letter = ratioLetter(left).compareTo(ratioLetter(right));
if (letter != 0) {
return letter;
}
return Integer.compare(ratioNumber(right), ratioNumber(left));
}
private String ratioLetter(String ratio) {
if (ratio == null) {
return "";
}
int i = 0;
while (i < ratio.length() && Character.isDigit(ratio.charAt(i))) {
i++;
}
return ratio.substring(i);
}
private int ratioNumber(String ratio) {
if (ratio == null) {
return -1;
}
int i = 0;
while (i < ratio.length() && Character.isDigit(ratio.charAt(i))) {
i++;
}
if (i == 0) {
return -1;
}
try {
return Integer.parseInt(ratio.substring(0, i));
} catch (NumberFormatException e) {
return -1;
}
}
private RatioConfig findConfig(String seriesType, String ratioValue) {
seriesType = normalizeSeries(seriesType);
if (StringUtils.isBlank(ratioValue)) {
throw new ServiceException("减速比不能为空");
}
ratioValue = ratioValue.trim();
RatioConfig config = ratioConfigMapper.selectOne(new LambdaQueryWrapper<RatioConfig>()
.eq(RatioConfig::getSeriesType, seriesType)
.eq(RatioConfig::getRatioValue, ratioValue)
.eq(RatioConfig::getStatus, 0)
.last("limit 1"));
if (config == null) {
throw new ServiceException("未找到 " + seriesType + " 速比 " + ratioValue + " 的配置");
}
return config;
}
/** 轴套、行星架(含行程架)、法兰。对不上的专用件不展示。 */
private String modelPartKind(String category) {
if (category == null) {
return null;
}
if (category.contains("轴套")) {
return "轴套";
}
if (category.contains("行星架") || category.contains("行程架")) {
return "行星架";
}
if (category.contains("法兰")) {
return "法兰";
}
return null;
}
private int categoryOrder(String category) {
String kind = modelPartKind(category);
if ("轴套".equals(kind)) {
return 0;
}
if ("行星架".equals(kind)) {
return 1;
}
if ("法兰".equals(kind)) {
return 2;
}
if ("通用件".equals(category)) {
return 3;
}
return 9;
}
private static List<RatioTransitPoDTO> toPoTransit(List<PurchaseOrderDTO> poList) { private static List<RatioTransitPoDTO> toPoTransit(List<PurchaseOrderDTO> poList) {
// 采购在途明细:从采购订单列表中过滤剩余未入库数量>0,按交期排序,最多返回50条(展示单号/交期/未入库数) // 采购在途明细:从采购订单列表中过滤剩余未入库数量>0,按交期排序,最多返回50条(展示单号/交期/未入库数)
if (poList == null || poList.isEmpty()) { if (poList == null || poList.isEmpty()) {

View File

@ -78,7 +78,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
private final DingTalkProperties dingTalkProperties; private final DingTalkProperties dingTalkProperties;
/** /**
* 同步K3工序计划数据(全量新增与更新) structuredList = {ArrayList@20034} size = 11284 * 同步K3工序计划数据(全量新增与更新)
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@ -109,6 +109,9 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
mainEntity.setProductCode(resultDTO.getProductCode()); mainEntity.setProductCode(resultDTO.getProductCode());
mainEntity.setProductName(resultDTO.getProductName()); mainEntity.setProductName(resultDTO.getProductName());
mainEntity.setProSpecification(resultDTO.getProSpecification()); mainEntity.setProSpecification(resultDTO.getProSpecification());
mainEntity.setDocumentStatus(resultDTO.getDocumentStatus());
mainEntity.setBillStatus(resultDTO.getBillStatus());
mainEntity.setPlanDocumentStatus(resultDTO.getPlanDocumentStatus());
if (StringUtils.isNotBlank(resultDTO.getPlanStartTime())) { if (StringUtils.isNotBlank(resultDTO.getPlanStartTime())) {
mainEntity.setPlanStartTime(DateUtil.parse(resultDTO.getPlanStartTime())); mainEntity.setPlanStartTime(DateUtil.parse(resultDTO.getPlanStartTime()));
} }
@ -152,7 +155,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
detailEntity.setWorkCenterName(detailDTO.getWorkCenter()); detailEntity.setWorkCenterName(detailDTO.getWorkCenter());
detailEntity.setProcessName(detailDTO.getProcessName()); detailEntity.setProcessName(detailDTO.getProcessName());
detailEntity.setOperator(detailDTO.getOperator()); detailEntity.setOperator(detailDTO.getOperator());
detailEntity.setOperStatus(detailDTO.getStatus()); // 存 FOperStatus 原始码(0~6),展示名由 Assembler 映射
detailEntity.setOperStatus(StringUtils.isNotBlank(detailDTO.getOperStatus())
? detailDTO.getOperStatus().trim()
: detailDTO.getStatus());
if (StringUtils.isNotBlank(detailDTO.getSeqPlanStartTime())) { if (StringUtils.isNotBlank(detailDTO.getSeqPlanStartTime())) {
detailEntity.setSeqPlanStartTime(DateUtil.parse(detailDTO.getSeqPlanStartTime())); detailEntity.setSeqPlanStartTime(DateUtil.parse(detailDTO.getSeqPlanStartTime()));
@ -530,7 +536,6 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
resultDTO.setWorkCenter(mainWorkCenter); resultDTO.setWorkCenter(mainWorkCenter);
resultDTO.setOverallPlanQty(totalPlanQty); resultDTO.setOverallPlanQty(totalPlanQty);
resultDTO.setOverallOkQty(totalOkQty); resultDTO.setOverallOkQty(totalOkQty);
OperationPlanningResultAssembler.applyOverallMetrics(resultDTO, resultDTO.getPlanFinishTime(), relatedDetails);
} }
} }
@ -657,6 +662,12 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
/** /**
* 构建工序进度多 Sheet Excel 临时文件。 * 构建工序进度多 Sheet Excel 临时文件。
* <p>推送/导出过滤:
* <ul>
* <li>生产订单单据状态、工序计划单据状态:仅已审核(C)</li>
* <li>生产订单业务状态:仅下达(3)、开工(4)</li>
* <li>工序 FOperStatus:仅下达(3)、开工(4)</li>
* </ul>
*/ */
private ProgressExcelFile buildCascadeProgressExcel(SfcOperationPlanningMainBo bo) throws Exception { private ProgressExcelFile buildCascadeProgressExcel(SfcOperationPlanningMainBo bo) throws Exception {
if (bo == null) { if (bo == null) {
@ -664,14 +675,22 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
} }
LambdaQueryWrapper<SfcOperationPlanningMain> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<SfcOperationPlanningMain> lqw = buildQueryWrapper(bo);
lqw.notLike(SfcOperationPlanningMain::getHbytSclh, "暂停"); lqw.notLike(SfcOperationPlanningMain::getHbytSclh, "暂停");
// 单据状态:生产订单 + 工序计划 均只要已审核;业务状态只要下达/开工
lqw.and(w -> w.eq(SfcOperationPlanningMain::getDocumentStatus, "C")
.or().eq(SfcOperationPlanningMain::getDocumentStatus, "已审核"));
lqw.and(w -> w.eq(SfcOperationPlanningMain::getPlanDocumentStatus, "C")
.or().eq(SfcOperationPlanningMain::getPlanDocumentStatus, "已审核"));
lqw.and(w -> w.in(SfcOperationPlanningMain::getBillStatus, "3", "4", "下达", "开工"));
List<SfcOperationPlanningMain> mainList = baseMapper.selectList(lqw); List<SfcOperationPlanningMain> mainList = baseMapper.selectList(lqw);
if (mainList == null || mainList.isEmpty()) { if (mainList == null || mainList.isEmpty()) {
throw new ServiceException("没有可导出的工序计划数据"); throw new ServiceException("没有符合状态条件的工序计划(订单/计划需已审核,业务状态需下达或开工)");
} }
List<Long> mainIds = mainList.stream().map(SfcOperationPlanningMain::getId).collect(Collectors.toList()); List<Long> mainIds = mainList.stream().map(SfcOperationPlanningMain::getId).collect(Collectors.toList());
var detailQuery = Wrappers.<SfcOperationPlanningDetail>lambdaQuery() var detailQuery = Wrappers.<SfcOperationPlanningDetail>lambdaQuery()
.in(SfcOperationPlanningDetail::getMainId, mainIds); .in(SfcOperationPlanningDetail::getMainId, mainIds)
// 工序只要下达、开工(兼容历史中文)
.and(w -> w.in(SfcOperationPlanningDetail::getOperStatus, "3", "4", "下达", "开工"));
if (StringUtils.isNotBlank(bo.getWorkCenterName())) { if (StringUtils.isNotBlank(bo.getWorkCenterName())) {
detailQuery.eq(SfcOperationPlanningDetail::getWorkCenterName, bo.getWorkCenterName().trim()); detailQuery.eq(SfcOperationPlanningDetail::getWorkCenterName, bo.getWorkCenterName().trim());
} }
@ -687,10 +706,13 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
if (plan == null || plan.getProcessList() == null || plan.getProcessList().isEmpty()) { if (plan == null || plan.getProcessList() == null || plan.getProcessList().isEmpty()) {
continue; continue;
} }
if (!isProgressPushMainAllowed(plan)) {
continue;
}
String sheetKey = resolveSheetKey(plan); String sheetKey = resolveSheetKey(plan);
List<OperationPlanningProgressExportVo> rows = sheetMap.computeIfAbsent(sheetKey, k -> new ArrayList<>()); List<OperationPlanningProgressExportVo> rows = sheetMap.computeIfAbsent(sheetKey, k -> new ArrayList<>());
for (ProcessDetailDTO process : plan.getProcessList()) { for (ProcessDetailDTO process : plan.getProcessList()) {
if (process == null || isFinishedOrClosed(process)) { if (process == null || !isProgressPushOperAllowed(process)) {
continue; continue;
} }
rows.add(toProgressExportRow(plan, process)); rows.add(toProgressExportRow(plan, process));
@ -698,7 +720,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
} }
sheetMap.entrySet().removeIf(e -> e.getValue() == null || e.getValue().isEmpty()); sheetMap.entrySet().removeIf(e -> e.getValue() == null || e.getValue().isEmpty());
if (sheetMap.isEmpty()) { if (sheetMap.isEmpty()) {
throw new ServiceException("过滤完工/关闭后没有可导出的工序"); throw new ServiceException("过滤后没有可导出的工序(需下达/开工且单据已审核)");
} }
String generatedAt = DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss"); String generatedAt = DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss");
@ -795,6 +817,43 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
return "未命名令号"; return "未命名令号";
} }
/**
* 推送/导出主表状态:生产订单与工序计划单据状态均需已审核;业务状态需下达或开工。
*/
private static boolean isProgressPushMainAllowed(OperationPlanResultDTO plan) {
return isApprovedDocumentStatus(plan.getDocumentStatus())
&& isApprovedDocumentStatus(plan.getPlanDocumentStatus())
&& isReleasedOrStartedBillStatus(plan.getBillStatus());
}
/**
* 推送/导出工序:FOperStatus 只要下达(3)、开工(4)。
*/
private static boolean isProgressPushOperAllowed(ProcessDetailDTO process) {
String oper = OperationPlanningResultAssembler.normalizeOperStatusCode(process.getOperStatus());
if ("3".equals(oper) || "4".equals(oper)) {
return true;
}
String k3 = process.getK3Status() == null ? "" : process.getK3Status().trim();
return "下达".equals(k3) || "开工".equals(k3) || "计划下达".equals(k3);
}
private static boolean isApprovedDocumentStatus(String status) {
if (StringUtils.isBlank(status)) {
return false;
}
String v = status.trim();
return "C".equalsIgnoreCase(v) || "已审核".equals(v);
}
private static boolean isReleasedOrStartedBillStatus(String status) {
if (StringUtils.isBlank(status)) {
return false;
}
String v = status.trim();
return "3".equals(v) || "4".equals(v) || "下达".equals(v) || "开工".equals(v);
}
/** /**
* 去除已完工 / 已关闭(含结案、结算、已转出、工序完成)。 * 去除已完工 / 已关闭(含结案、结算、已转出、工序完成)。
*/ */
@ -804,8 +863,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
} }
String status = process.getStatus() == null ? "" : process.getStatus().trim(); String status = process.getStatus() == null ? "" : process.getStatus().trim();
String k3 = process.getK3Status() == null ? "" : process.getK3Status().trim(); String k3 = process.getK3Status() == null ? "" : process.getK3Status().trim();
String oper = process.getOperStatus() == null ? "" : process.getOperStatus().trim();
return status.contains("完工") || status.contains("关闭") return status.contains("完工") || status.contains("关闭")
|| "完工".equals(k3) || "关闭".equals(k3); || "完工".equals(k3) || "关闭".equals(k3)
|| "5".equals(oper) || "6".equals(oper);
} }
private static OperationPlanningProgressExportVo toProgressExportRow( private static OperationPlanningProgressExportVo toProgressExportRow(
@ -818,8 +879,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
row.setProductName(plan.getProductName()); row.setProductName(plan.getProductName());
row.setProSpecification(plan.getProSpecification()); row.setProSpecification(plan.getProSpecification());
row.setBillNo(plan.getBillNo()); row.setBillNo(plan.getBillNo());
row.setOverallStatus(plan.getOverallStatus()); // 模板 {{ProgressData.overallStatus}} / overallProgress:工序状态、工序进度
row.setOverallProgress(scale2(plan.getOverallProgress())); Double operProgress = resolveExportOperProgress(process);
row.setOverallProgress(scale2(operProgress));
row.setOverallStatus(resolveStatusByProgress(operProgress));
row.setOperNo(process.getOperNo()); row.setOperNo(process.getOperNo());
row.setProcessName(process.getProcessName()); row.setProcessName(process.getProcessName());
row.setWorkCenter(process.getWorkCenter()); row.setWorkCenter(process.getWorkCenter());
@ -827,8 +890,8 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
row.setInQty(process.getInQty()); row.setInQty(process.getInQty());
row.setOkQty(process.getOkQty()); row.setOkQty(process.getOkQty());
row.setTransOutQty(process.getTransOutQty()); row.setTransOutQty(process.getTransOutQty());
row.setProgress(scale2(process.getProgress())); row.setProgress(scale2(operProgress));
row.setStatus(process.getStatus()); row.setStatus(resolveStatusByProgress(operProgress));
row.setOperPlanStartTime(process.getOperPlanStartTime()); row.setOperPlanStartTime(process.getOperPlanStartTime());
row.setOperPlanFinishTime(process.getOperPlanFinishTime()); row.setOperPlanFinishTime(process.getOperPlanFinishTime());
row.setOperator(process.getOperator()); row.setOperator(process.getOperator());
@ -836,6 +899,36 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
return row; return row;
} }
/**
* 工序状态按进度:0 未开工;&gt;0 生产中;100 已完工
*/
private static String resolveStatusByProgress(Double progress) {
double p = progress == null ? 0D : progress;
if (p <= 0D) {
return "未开工";
}
if (p >= 100D) {
return "已完工";
}
return "生产中";
}
/** 导出用工序进度:合格数量 / 工序数量 * 100 */
private static Double resolveExportOperProgress(ProcessDetailDTO process) {
if (process == null) {
return 0D;
}
Double planQty = process.getPlanQty();
if (planQty == null || planQty <= 0D) {
return 0D;
}
double ok = process.getOkQty() == null ? 0D : process.getOkQty();
return BigDecimal.valueOf(ok)
.multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(planQty), 2, RoundingMode.HALF_UP)
.doubleValue();
}
private static Double scale2(Double value) { private static Double scale2(Double value) {
if (value == null) { if (value == null) {
return null; return null;
@ -881,6 +974,9 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
lqw.eq(StringUtils.isNotBlank(bo.getProductCode()), SfcOperationPlanningMain::getProductCode, bo.getProductCode()); lqw.eq(StringUtils.isNotBlank(bo.getProductCode()), SfcOperationPlanningMain::getProductCode, bo.getProductCode());
lqw.like(StringUtils.isNotBlank(bo.getProductName()), SfcOperationPlanningMain::getProductName, bo.getProductName()); lqw.like(StringUtils.isNotBlank(bo.getProductName()), SfcOperationPlanningMain::getProductName, bo.getProductName());
lqw.eq(StringUtils.isNotBlank(bo.getProSpecification()), SfcOperationPlanningMain::getProSpecification, bo.getProSpecification()); lqw.eq(StringUtils.isNotBlank(bo.getProSpecification()), SfcOperationPlanningMain::getProSpecification, bo.getProSpecification());
lqw.eq(StringUtils.isNotBlank(bo.getDocumentStatus()), SfcOperationPlanningMain::getDocumentStatus, bo.getDocumentStatus());
lqw.eq(StringUtils.isNotBlank(bo.getBillStatus()), SfcOperationPlanningMain::getBillStatus, bo.getBillStatus());
lqw.eq(StringUtils.isNotBlank(bo.getPlanDocumentStatus()), SfcOperationPlanningMain::getPlanDocumentStatus, bo.getPlanDocumentStatus());
lqw.ge(bo.getPlanStartTime() != null, SfcOperationPlanningMain::getPlanStartTime, bo.getPlanStartTime()); lqw.ge(bo.getPlanStartTime() != null, SfcOperationPlanningMain::getPlanStartTime, bo.getPlanStartTime());
lqw.le(bo.getPlanFinishTime() != null, SfcOperationPlanningMain::getPlanFinishTime, bo.getPlanFinishTime()); lqw.le(bo.getPlanFinishTime() != null, SfcOperationPlanningMain::getPlanFinishTime, bo.getPlanFinishTime());
if (StringUtils.isNotBlank(bo.getWorkCenterName())) { if (StringUtils.isNotBlank(bo.getWorkCenterName())) {

View File

@ -39,7 +39,6 @@ public final class OperationPlanningResultAssembler {
if (mainList == null || mainList.isEmpty()) { if (mainList == null || mainList.isEmpty()) {
return resultList; return resultList;
} }
Date now = new Date();
for (SfcOperationPlanningMain main : mainList) { for (SfcOperationPlanningMain main : mainList) {
OperationPlanResultDTO resultDTO = new OperationPlanResultDTO(); OperationPlanResultDTO resultDTO = new OperationPlanResultDTO();
resultDTO.setMoNumber(main.getMoNumber()); resultDTO.setMoNumber(main.getMoNumber());
@ -52,6 +51,12 @@ public final class OperationPlanningResultAssembler {
resultDTO.setPlanStartTime(main.getPlanStartTime() != null ? DateUtil.formatDateTime(main.getPlanStartTime()) : null); resultDTO.setPlanStartTime(main.getPlanStartTime() != null ? DateUtil.formatDateTime(main.getPlanStartTime()) : null);
resultDTO.setPlanFinishTime(main.getPlanFinishTime() != null ? DateUtil.formatDateTime(main.getPlanFinishTime()) : null); resultDTO.setPlanFinishTime(main.getPlanFinishTime() != null ? DateUtil.formatDateTime(main.getPlanFinishTime()) : null);
resultDTO.setFid(main.getFid()); resultDTO.setFid(main.getFid());
resultDTO.setDocumentStatus(main.getDocumentStatus());
resultDTO.setDocumentStatusName(mapDocumentStatusName(main.getDocumentStatus()));
resultDTO.setBillStatus(main.getBillStatus());
resultDTO.setBillStatusName(mapBillStatusName(main.getBillStatus()));
resultDTO.setPlanDocumentStatus(main.getPlanDocumentStatus());
resultDTO.setPlanDocumentStatusName(mapDocumentStatusName(main.getPlanDocumentStatus()));
resultDTO.setRawPlanStartTime(main.getPlanStartTime()); resultDTO.setRawPlanStartTime(main.getPlanStartTime());
List<ProcessDetailDTO> processList = new ArrayList<>(); List<ProcessDetailDTO> processList = new ArrayList<>();
@ -78,11 +83,8 @@ public final class OperationPlanningResultAssembler {
resultDTO.setWorkCenter(mainWorkCenter); resultDTO.setWorkCenter(mainWorkCenter);
resultDTO.setOverallPlanQty(totalPlanQty); resultDTO.setOverallPlanQty(totalPlanQty);
resultDTO.setOverallOkQty(totalOkQty); resultDTO.setOverallOkQty(totalOkQty);
applyOverallMetrics(resultDTO, main.getPlanFinishTime(), processList, now);
} else {
resultDTO.setOverallProgress(0.0);
resultDTO.setOverallStatus(isOverdue(main.getPlanFinishTime(), now) ? STATUS_DELAYED : STATUS_NOT_STARTED);
} }
// 整体进度/整体状态不再计算
resultDTO.setProcessList(processList); resultDTO.setProcessList(processList);
resultList.add(resultDTO); resultList.add(resultDTO);
} }
@ -162,8 +164,10 @@ public final class OperationPlanningResultAssembler {
detailDTO.setWastageQty(detail.getWastageQty() != null ? detail.getWastageQty().doubleValue() : null); detailDTO.setWastageQty(detail.getWastageQty() != null ? detail.getWastageQty().doubleValue() : null);
detailDTO.setDepartment(detail.getDepartmentName()); detailDTO.setDepartment(detail.getDepartmentName());
detailDTO.setOperator(detail.getOperator()); detailDTO.setOperator(detail.getOperator());
if (detailDTO.getPlanQty() != null && detailDTO.getInQty() != null && detailDTO.getPlanQty() > 0) { // 工序进度 = 合格数量 / 工序数量 * 100
BigDecimal progress = BigDecimal.valueOf(detailDTO.getInQty()) if (detailDTO.getPlanQty() != null && detailDTO.getPlanQty() > 0) {
double ok = detailDTO.getOkQty() == null ? 0D : detailDTO.getOkQty();
BigDecimal progress = BigDecimal.valueOf(ok)
.multiply(BigDecimal.valueOf(100)) .multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(detailDTO.getPlanQty()), 1, RoundingMode.HALF_UP); .divide(BigDecimal.valueOf(detailDTO.getPlanQty()), 1, RoundingMode.HALF_UP);
if (progress.compareTo(BigDecimal.ZERO) < 0) { if (progress.compareTo(BigDecimal.ZERO) < 0) {
@ -174,40 +178,9 @@ public final class OperationPlanningResultAssembler {
detailDTO.setProgress(progress.doubleValue()); detailDTO.setProgress(progress.doubleValue());
} }
String statusStr = detail.getOperStatus(); // 兼容历史中文状态:优先按 FOperStatus 码解析
if ("1".equals(statusStr)) { detailDTO.setOperStatus(normalizeOperStatusCode(detail.getOperStatus()));
detailDTO.setK3Status("计划"); enrichProcessDetailStatus(detailDTO);
} else if ("2".equals(statusStr)) {
detailDTO.setK3Status("计划下达");
} else if ("3".equals(statusStr)) {
detailDTO.setK3Status("开工");
} else if ("4".equals(statusStr)) {
detailDTO.setK3Status("已完工");
} else if ("5".equals(statusStr)) {
detailDTO.setK3Status("结案");
} else if ("6".equals(statusStr)) {
detailDTO.setK3Status("结算");
} else {
detailDTO.setK3Status(statusStr);
}
double inQty = detailDTO.getInQty() == null ? 0D : detailDTO.getInQty();
double okQty = detailDTO.getOkQty() == null ? 0D : detailDTO.getOkQty();
double ngQty = detailDTO.getNgQty() == null ? 0D : detailDTO.getNgQty();
double transOutQty = detailDTO.getTransOutQty() == null ? 0D : detailDTO.getTransOutQty();
if ("5".equals(statusStr) || "6".equals(statusStr)) {
detailDTO.setStatus("已关闭");
} else if (inQty == 0D) {
detailDTO.setStatus("未开工");
} else if (transOutQty > 0D) {
detailDTO.setStatus("已转出");
} else if (okQty + ngQty >= inQty) {
detailDTO.setStatus("工序完成");
} else if (okQty + ngQty > 0D) {
detailDTO.setStatus("质检中");
} else {
detailDTO.setStatus("生产中");
}
detailDTO.setSeqPlanStartTime(detail.getSeqPlanStartTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanStartTime()) : null); detailDTO.setSeqPlanStartTime(detail.getSeqPlanStartTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanStartTime()) : null);
detailDTO.setSeqPlanFinishTime(detail.getSeqPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanFinishTime()) : null); detailDTO.setSeqPlanFinishTime(detail.getSeqPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanFinishTime()) : null);
@ -223,16 +196,184 @@ public final class OperationPlanningResultAssembler {
return detailDTO; return detailDTO;
} }
/**
* 根据 FOperStatus 原始码填充 k3Status / 业务 status。
* FOperStatus:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭
* <p>数量口径:工序数(planQty)=转入=合格=转出 时视为已转出完工。
*/
public static void enrichProcessDetailStatus(ProcessDetailDTO detailDTO) {
if (detailDTO == null) {
return;
}
String statusStr = normalizeOperStatusCode(detailDTO.getOperStatus());
detailDTO.setOperStatus(statusStr);
detailDTO.setK3Status(mapOperStatusName(statusStr));
double planQty = detailDTO.getPlanQty() == null ? 0D : detailDTO.getPlanQty();
double inQty = detailDTO.getInQty() == null ? 0D : detailDTO.getInQty();
double okQty = detailDTO.getOkQty() == null ? 0D : detailDTO.getOkQty();
double ngQty = detailDTO.getNgQty() == null ? 0D : detailDTO.getNgQty();
double transOutQty = detailDTO.getTransOutQty() == null ? 0D : detailDTO.getTransOutQty();
if ("5".equals(statusStr) || "6".equals(statusStr)
|| "完工".equals(statusStr) || "关闭".equals(statusStr)) {
detailDTO.setStatus("已关闭");
} else if (inQty == 0D) {
detailDTO.setStatus("未开工");
} else if (isQtyFullyTransferredOut(planQty, inQty, okQty, transOutQty)) {
// 工序数=转入=合格=转出 → 真正完工转出
detailDTO.setStatus("已转出");
} else if (okQty + ngQty >= inQty && inQty > 0D) {
detailDTO.setStatus("工序完成");
} else if (okQty + ngQty > 0D) {
detailDTO.setStatus("质检中");
} else if (transOutQty > 0D) {
// 部分转出,不算整道工序完工
detailDTO.setStatus("生产中");
} else {
detailDTO.setStatus("生产中");
}
}
/**
* 工序完工数量条件:工序数 = 转入 = 合格 = 转出,且均 &gt; 0。
*/
public static boolean isQtyFullyTransferredOut(Double planQty, Double inQty, Double okQty, Double transOutQty) {
double plan = planQty == null ? 0D : planQty;
double in = inQty == null ? 0D : inQty;
double ok = okQty == null ? 0D : okQty;
double out = transOutQty == null ? 0D : transOutQty;
if (plan <= 0D) {
return false;
}
return qtyEquals(plan, in) && qtyEquals(plan, ok) && qtyEquals(plan, out);
}
private static boolean qtyEquals(double a, double b) {
return BigDecimal.valueOf(a).subtract(BigDecimal.valueOf(b)).abs()
.compareTo(new BigDecimal("0.0001")) <= 0;
}
/** FDocumentStatus:A创建 B审核中 C已审核 D重新审核 E暂存 */
public static String mapDocumentStatusName(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
switch (code.trim().toUpperCase()) {
case "A":
return "创建";
case "B":
return "审核中";
case "C":
return "已审核";
case "D":
return "重新审核";
case "E":
case "Z":
return "暂存";
default:
return code;
}
}
/** FStatus:1计划 2计划确认 3下达 4开工 5完工 6结案 7结算 */
public static String mapBillStatusName(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
switch (code.trim()) {
case "1":
return "计划";
case "2":
return "计划确认";
case "3":
return "下达";
case "4":
return "开工";
case "5":
return "完工";
case "6":
return "结案";
case "7":
return "结算";
default:
return code;
}
}
/** FOperStatus:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭 */
public static String mapOperStatusName(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
switch (code.trim()) {
case "0":
return "创建";
case "1":
return "计划";
case "2":
return "计划确认";
case "3":
return "下达";
case "4":
return "开工";
case "5":
return "完工";
case "6":
return "关闭";
default:
return code;
}
}
/** 历史同步曾写入中文,读出时尽量还原为码值 */
public static String normalizeOperStatusCode(String raw) {
if (StringUtils.isBlank(raw)) {
return raw;
}
String v = raw.trim();
switch (v) {
case "创建":
return "0";
case "计划":
return "1";
case "计划确认":
return "2";
case "下达":
case "计划下达":
return "3";
case "开工":
return "4";
case "完工":
case "已完工":
return "5";
case "关闭":
case "结案":
case "结算":
case "已关闭":
return "6";
default:
return v;
}
}
public static boolean isProcessFinished(ProcessDetailDTO detail) { public static boolean isProcessFinished(ProcessDetailDTO detail) {
if (detail == null) { if (detail == null) {
return false; return false;
} }
String status = detail.getStatus(); // 金蝶工序状态完工/关闭
if ("已关闭".equals(status) || "已转出".equals(status) || "工序完成".equals(status)) { String oper = normalizeOperStatusCode(detail.getOperStatus());
if ("5".equals(oper) || "6".equals(oper)) {
return true; return true;
} }
String k3 = detail.getK3Status(); String k3 = detail.getK3Status();
return "已完工".equals(k3) || "结案".equals(k3) || "结算".equals(k3); if ("完工".equals(k3) || "关闭".equals(k3) || "已完工".equals(k3) || "结案".equals(k3) || "结算".equals(k3)) {
return true;
}
if ("已关闭".equals(detail.getStatus())) {
return true;
}
// 数量口径:工序数 = 转入 = 合格 = 转出 才算完工
return isQtyFullyTransferredOut(detail.getPlanQty(), detail.getInQty(), detail.getOkQty(), detail.getTransOutQty());
} }
static boolean isProcessStarted(ProcessDetailDTO detail) { static boolean isProcessStarted(ProcessDetailDTO detail) {

View File

@ -3,9 +3,11 @@ package com.ruoyi.system.service.support;
import lombok.Getter; import lombok.Getter;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -61,10 +63,24 @@ public final class WorkAlertPushTypeRegistry {
private static final Map<String, PushType> BY_CODE = TYPES.stream() private static final Map<String, PushType> BY_CODE = TYPES.stream()
.collect(Collectors.toMap(PushType::getCode, t -> t, (a, b) -> a, LinkedHashMap::new)); .collect(Collectors.toMap(PushType::getCode, t -> t, (a, b) -> a, LinkedHashMap::new));
/** 不走钉钉群会话(企业微信 / 固定私发 / 未接 conversationId) */
private static final Set<String> NON_GROUP_CODES = new HashSet<>(Arrays.asList(
"getMassageDelayDateSummary",
"getApplicationMessage",
"getConstructionDelay",
"getUninspectedData"
));
public static List<PushType> all() { public static List<PushType> all() {
return TYPES; return TYPES;
} }
public static List<PushType> groupConversationTypes() {
return TYPES.stream()
.filter(t -> supportsGroupConversation(t.getCode()))
.collect(Collectors.toList());
}
public static PushType find(String pushCode) { public static PushType find(String pushCode) {
if (pushCode == null) { if (pushCode == null) {
return null; return null;
@ -75,4 +91,8 @@ public final class WorkAlertPushTypeRegistry {
public static boolean isSupported(String pushCode) { public static boolean isSupported(String pushCode) {
return find(pushCode) != null; return find(pushCode) != null;
} }
public static boolean supportsGroupConversation(String pushCode) {
return isSupported(pushCode) && !NON_GROUP_CODES.contains(pushCode);
}
} }

View File

@ -25,6 +25,25 @@ public final class ProductionOrderNoUtils {
private ProductionOrderNoUtils() { private ProductionOrderNoUtils() {
} }
/**
* 入库前去掉令号中的空白(首尾、中间空格、全角空格、不间断空格),
* 避免从 Excel/网页复制时带入空格导致后续对不上。
*/
public static String stripSpaces(String productionOrderNo) {
if (productionOrderNo == null) {
return null;
}
StringBuilder sb = new StringBuilder(productionOrderNo.length());
for (int i = 0; i < productionOrderNo.length(); i++) {
char c = productionOrderNo.charAt(i);
if (Character.isWhitespace(c) || c == '\u00A0' || c == '\u3000') {
continue;
}
sb.append(c);
}
return sb.length() == 0 ? null : sb.toString();
}
public static String extractYear(String productionOrderNo) { public static String extractYear(String productionOrderNo) {
if (StringUtils.isBlank(productionOrderNo)) { if (StringUtils.isBlank(productionOrderNo)) {
throw new IllegalArgumentException("生产令号为空,无法识别年份"); throw new IllegalArgumentException("生产令号为空,无法识别年份");

View File

@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="quantity" column="quantity"/> <result property="quantity" column="quantity"/>
<result property="isEnterpriseStandard" column="is_enterprise_standard"/> <result property="isEnterpriseStandard" column="is_enterprise_standard"/>
<result property="isDownload" column="is_download"/> <result property="isDownload" column="is_download"/>
<result property="drawingQtyIssued" column="drawing_qty_issued"/>
<result property="needProcess" column="need_process"/> <result property="needProcess" column="need_process"/>
<result property="drawingPath" column="drawing_path"/> <result property="drawingPath" column="drawing_path"/>
<result property="bomStatus" column="bom_status"/> <result property="bomStatus" column="bom_status"/>

View File

@ -16,6 +16,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="proSpecification" column="pro_specification"/> <result property="proSpecification" column="pro_specification"/>
<result property="planStartTime" column="plan_start_time"/> <result property="planStartTime" column="plan_start_time"/>
<result property="planFinishTime" column="plan_finish_time"/> <result property="planFinishTime" column="plan_finish_time"/>
<result property="documentStatus" column="document_status"/>
<result property="billStatus" column="bill_status"/>
<result property="planDocumentStatus" column="plan_document_status"/>
<result property="createTime" column="create_time"/> <result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/> <result property="updateTime" column="update_time"/>
<result property="createBy" column="create_by"/> <result property="createBy" column="create_by"/>

View File

@ -12,6 +12,7 @@
<result property="userType" column="user_type"/> <result property="userType" column="user_type"/>
<result property="email" column="email"/> <result property="email" column="email"/>
<result property="phonenumber" column="phonenumber"/> <result property="phonenumber" column="phonenumber"/>
<result property="employeeNumber" column="employee_number"/>
<result property="sex" column="sex"/> <result property="sex" column="sex"/>
<result property="avatar" column="avatar"/> <result property="avatar" column="avatar"/>
<result property="password" column="password"/> <result property="password" column="password"/>
@ -56,6 +57,7 @@
u.email, u.email,
u.avatar, u.avatar,
u.phonenumber, u.phonenumber,
u.employee_number,
u.password, u.password,
u.sex, u.sex,
u.status, u.status,
@ -85,7 +87,7 @@
</sql> </sql>
<select id="selectPageUserList" resultMap="SysUserResult"> <select id="selectPageUserList" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.employee_number, u.sex,
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader
from sys_user u from sys_user u
left join sys_dept d on u.dept_id = d.dept_id left join sys_dept d on u.dept_id = d.dept_id
@ -93,7 +95,7 @@
</select> </select>
<select id="selectUserList" resultMap="SysUserResult"> <select id="selectUserList" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.employee_number, u.sex,
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader
from sys_user u from sys_user u
left join sys_dept d on u.dept_id = d.dept_id left join sys_dept d on u.dept_id = d.dept_id

View File

@ -15,7 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
a.main_id, a.main_id,
a.plan_qty, a.plan_qty,
a.ng_qty, a.ng_qty,
CASE WHEN COALESCE(d2.oper_status, '') = '4' THEN 1 ELSE 0 END AS finished_flag, CASE WHEN COALESCE(d2.oper_status, '') IN ('5', '6', '完工', '关闭', '已完工', '结案', '结算', '已关闭') THEN 1 ELSE 0 END AS finished_flag,
COALESCE(SUM(COALESCE(d2.qualified_qty, 0)), 0) AS finish_qty COALESCE(SUM(COALESCE(d2.qualified_qty, 0)), 0) AS finish_qty
FROM ( FROM (
SELECT SELECT
@ -74,10 +74,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SELECT SELECT
m.mo_number AS workOrderNo, m.mo_number AS workOrderNo,
CASE CASE
WHEN SUM(CASE WHEN COALESCE(d.oper_status, '') = '4' THEN 1 ELSE 0 END) = COUNT(1) THEN '已完工' WHEN SUM(CASE WHEN COALESCE(d.oper_status, '') IN ('5', '6', '完工', '关闭', '已完工', '结案', '结算', '已关闭') THEN 1 ELSE 0 END) = COUNT(1) THEN '已完工'
WHEN SUM(COALESCE(d.trans_in_qty, 0)) > 0 WHEN SUM(COALESCE(d.trans_in_qty, 0)) > 0
OR SUM(COALESCE(d.qualified_qty, 0) + COALESCE(d.unqualified_qty, 0)) > 0 OR SUM(COALESCE(d.qualified_qty, 0) + COALESCE(d.unqualified_qty, 0)) > 0
OR SUM(CASE WHEN COALESCE(d.oper_status, '') = '3' THEN 1 ELSE 0 END) > 0 OR SUM(CASE WHEN COALESCE(d.oper_status, '') IN ('4', '开工') THEN 1 ELSE 0 END) > 0
THEN '已开工' THEN '已开工'
ELSE '未开工' ELSE '未开工'
END AS status, END AS status,

View File

@ -1,2 +0,0 @@
1753766028406|1|statement|connection 0|url jdbc:mysql://192.168.5.121:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true|SELECT oss_config_id, config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, status, ext1, remark, access_policy, create_by, create_time, update_by, update_time FROM sys_oss_config|SELECT oss_config_id, config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, status, ext1, remark, access_policy, create_by, create_time, update_by, update_time FROM sys_oss_config
1759114213618|1|statement|connection 0|url jdbc:mysql://192.168.5.121:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true|SELECT oss_config_id, config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, status, ext1, remark, access_policy, create_by, create_time, update_by, update_time FROM sys_oss_config|SELECT oss_config_id, config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, status, ext1, remark, access_policy, create_by, create_time, update_by, update_time FROM sys_oss_config