更新,工序计划
This commit is contained in:
parent
5e091843b0
commit
9fb3090015
35
.gitignore
vendored
35
.gitignore
vendored
@ -1,24 +1,51 @@
|
||||
# Maven / Java
|
||||
/.project
|
||||
*.class
|
||||
.classpath
|
||||
.settings
|
||||
.factorypath
|
||||
.project
|
||||
.logs
|
||||
.log
|
||||
pom.properties
|
||||
**/target/**
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
maven-wrapper.properties
|
||||
**/logs/
|
||||
|
||||
# Logs
|
||||
.logs
|
||||
.log
|
||||
*.log
|
||||
**/logs/
|
||||
/logs/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
.cursor/
|
||||
|
||||
# 本地文档/工具(默认不进仓库;需要单独提交时再强制 add)
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Node(文档脚本等本地依赖)
|
||||
**/node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 本地文档/工具产物(默认不进仓库)
|
||||
docs/
|
||||
|
||||
# 临时/导出文件
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*.orig
|
||||
*.xlsx
|
||||
!**/src/**/*.xlsx
|
||||
*.docx
|
||||
!**/src/**/*.docx
|
||||
|
||||
12
pom.xml
12
pom.xml
@ -36,7 +36,8 @@
|
||||
<alibaba-ttl.version>2.14.2</alibaba-ttl.version>
|
||||
<xxl-job.version>2.4.0</xxl-job.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地址定位库 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<!-- 临时修复 snakeyaml 漏洞 -->
|
||||
@ -265,10 +266,15 @@
|
||||
<version>${snakeyaml.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 加密包引入 -->
|
||||
<!-- 加密包:统一 BouncyCastle 版本,避免多版本冲突导致 JWT 登录失败 -->
|
||||
<dependency>
|
||||
<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>
|
||||
</dependency>
|
||||
|
||||
|
||||
@ -103,12 +103,36 @@
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dingtalk</artifactId>
|
||||
<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>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
|
||||
<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>
|
||||
@ -189,6 +213,10 @@
|
||||
<configuration>
|
||||
<includeSystemScope>true</includeSystemScope> <!-- jar包需要加这个 -->
|
||||
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
|
||||
<jvmArguments>
|
||||
--add-opens java.base/java.lang=ALL-UNNAMED
|
||||
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
|
||||
</jvmArguments>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.ruoyi;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import org.apache.poi.openxml4j.util.ZipSecureFile;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@ -25,6 +26,8 @@ public class RuoYiApplication {
|
||||
// 全局放宽,启动时执行一次即可
|
||||
ZipSecureFile.setMaxFileCount(100000);
|
||||
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) {
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
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.dingding.DingCallbackCrypto;
|
||||
import com.ruoyi.common.dingding.robot.RobotGroupService;
|
||||
@ -285,15 +286,26 @@ public class CallbackController {
|
||||
*/
|
||||
private void updateProcessOrderProStatus(String outTrackId, Long routeStatus, Long bomStatus) {
|
||||
try {
|
||||
// 根据 notificationCardId 查询
|
||||
// 根据审核卡片 Id 查询
|
||||
ProcessOrderPro order = processOrderProMapper.selectOne(new LambdaQueryWrapper<ProcessOrderPro>().eq(ProcessOrderPro::getAuditCardId, outTrackId));
|
||||
if (order != null) {
|
||||
ProcessOrderPro update = new ProcessOrderPro();
|
||||
update.setId(order.getId());
|
||||
update.setRouteStatus(routeStatus);
|
||||
update.setBomStatus(bomStatus);
|
||||
update.setUpdateBy("DingDing");
|
||||
processOrderProMapper.updateById(update);
|
||||
// 幂等:已是目标审核状态则不再更新/通知(防钉钉回调重试连发)
|
||||
if (routeStatus != null && routeStatus.equals(order.getRouteStatus())) {
|
||||
bizLogger.info("工艺审核状态已是目标值,跳过重复处理: id={}, routeStatus={}", order.getId(), routeStatus);
|
||||
return;
|
||||
}
|
||||
// 条件更新:仅当 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={}",
|
||||
order.getId(), routeStatus, bomStatus);
|
||||
|
||||
@ -409,8 +421,8 @@ public class CallbackController {
|
||||
// 其他事件(包括机器人消息)
|
||||
if (eventJson.containsKey("text")) {
|
||||
String content = extractRobotText(eventJson);
|
||||
bizLogger.info("机器人收到文本消息: content={} conversationTitle={} robotCode={}",
|
||||
content, eventJson.getString("conversationTitle"), eventJson.getString("robotCode"));
|
||||
bizLogger.info("机器人收到文本消息: content={} conversationTitle={} conversationId={} robotCode={}",
|
||||
content, eventJson.getString("conversationTitle"), eventJson.getString("conversationId"), eventJson.getString("robotCode"));
|
||||
|
||||
boolean isHandled = false;
|
||||
|
||||
@ -598,6 +610,24 @@ public class CallbackController {
|
||||
}).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) {
|
||||
String sessionWebhook = eventJson.getString("sessionWebhook");
|
||||
@ -605,7 +635,8 @@ public class CallbackController {
|
||||
String conversationId = eventJson.getString("conversationId");
|
||||
|
||||
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 直接回复 (这是最标准的回调回复方式)
|
||||
if (sessionWebhook != null && !sessionWebhook.isEmpty()) {
|
||||
|
||||
@ -36,7 +36,7 @@ server:
|
||||
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
|
||||
# 每块buffer的空间大小,越小的空间被利用越充分
|
||||
buffer-size: 512
|
||||
# 是否分配的直接内存
|
||||
# 是否分配的直接内存\/system/kingdee/materialDocHub/board
|
||||
direct-buffers: true
|
||||
threads:
|
||||
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
|
||||
@ -162,6 +162,10 @@ security:
|
||||
- /system/route/push/**
|
||||
- /DataBoard/ProduceKanBan/**
|
||||
- /system/workCenterData/produceDataKanban
|
||||
# MES 图纸查询 / 下载(方法上另有 @SaIgnore + appKey 鉴权)
|
||||
- /open/mes/**
|
||||
# 北通道实验台设备状态看板
|
||||
- /open/devstat/**
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
@ -320,21 +324,21 @@ dingtalk:
|
||||
info-send-app-key: ding9bhxctsmq5jsf8pk
|
||||
info-send-app-secret: OBhuwZzf31kcVqqVwODtu59Aqmql21gc301V06Y8URdjZy_YmBJ47-AZ4nnLZVVm
|
||||
info-send-robot-code: ding9bhxctsmq5jsf8pk
|
||||
info-send-user-ids: "665416234832653417,283633411029629036,0754423610937470"
|
||||
info-send-user-ids: "665416234832653417,283633411029629036,0754423610937470,01692556241068902"
|
||||
#项目卡片模板ID
|
||||
project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema
|
||||
#工艺审核互动卡片模板ID
|
||||
route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema
|
||||
#工艺审核互动卡片回调路由Key
|
||||
callback-route-key: update_route_status2
|
||||
# 出图完成通知(钉钉 userId/staffId,多人逗号分隔;以 0 开头的须加引号)
|
||||
notify-user-id: "173769650335444471,0108392663561185496,283633411029629036"
|
||||
# 出图完成 / PLM图纸到达通知(钉钉 userId/staffId,多人逗号分隔;以 0 开头的须加引号)
|
||||
notify-user-id: "293047444820124967,0108392663561185496,03610242692523410847"
|
||||
# 工艺审核互动卡片
|
||||
route-approval-id: "01201038164023402380"
|
||||
# 工艺计划推送用户
|
||||
plan-user-id: "313514580637707245,02256761642526333139,192530665123549546,01201038164023402380"
|
||||
route-approval-id: "0108392663561185496"
|
||||
# 工艺计划推送用户;无需做工艺且出图完成后、PLM 总装BOM与工艺都到齐后通知生管
|
||||
plan-user-id: "313514580637707245,02256761642526333139,192530665123549546,0108392663561185496,03361938481539892633,283633411029629036"
|
||||
# 同步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
|
||||
#技术方案申请流程同意后是否推送PLM
|
||||
@ -345,12 +349,41 @@ dingtalk:
|
||||
scheme-bpms-plm-push-task-show-names: 总工程师
|
||||
#技术方案申请流程同意后推送PLM是否显示总工程师意见
|
||||
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.标准图纸存放文件夹)
|
||||
app:
|
||||
drawing:
|
||||
# DWG 上传在应用服务器上的暂存目录(须可写);实际上传目标走 FTP 8022
|
||||
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 导出共用;下按生产令号分子目录)
|
||||
plan-excel:
|
||||
base-dir: D:/file/plan-excel
|
||||
@ -401,3 +434,9 @@ plm:
|
||||
# 库内 DDAttachment 常为空时,推送前再从钉钉 getProcessInstance 拉附件
|
||||
refresh-attachments-from-ding-on-push: true
|
||||
log-payload-max-chars: 8000
|
||||
|
||||
# MES 图纸开放(/open/mes/drawing,appKey/appSecret 鉴权)
|
||||
mes:
|
||||
auth:
|
||||
app-key: mes
|
||||
app-secret: change-me-mes-secret
|
||||
|
||||
@ -174,10 +174,10 @@
|
||||
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 加密包引入 -->
|
||||
<!-- 加密包引入(与 dependencyManagement 统一版本) -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.hierynomus</groupId>
|
||||
@ -256,7 +256,13 @@
|
||||
<dependency>
|
||||
<groupId>eu.agno3.jcifs</groupId>
|
||||
<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>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
@ -283,12 +289,36 @@
|
||||
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<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>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dingtalk</artifactId>
|
||||
<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>
|
||||
@ -303,4 +333,17 @@
|
||||
</dependency>
|
||||
</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>
|
||||
|
||||
@ -133,7 +133,7 @@ public class DingTalkProperties {
|
||||
*/
|
||||
private String routeCardTemplateId;
|
||||
/**
|
||||
* 工艺计划接收人
|
||||
* 工艺计划接收人;无需做工艺且出图完成后、PLM 总装BOM与工艺到齐后也通知这批人(逗号分隔)
|
||||
*/
|
||||
private String planUserId;
|
||||
|
||||
@ -158,6 +158,41 @@ public class DingTalkProperties {
|
||||
*/
|
||||
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 审批事件
|
||||
*/
|
||||
|
||||
@ -75,6 +75,12 @@ public class SysUser extends BaseEntity {
|
||||
@Sensitive(strategy = SensitiveStrategy.PHONE)
|
||||
private String phonenumber;
|
||||
|
||||
/**
|
||||
* 员工编号(金蝶员工工号,如 00094)
|
||||
*/
|
||||
@Size(min = 0, max = 64, message = "员工编号长度不能超过{max}个字符")
|
||||
private String employeeNumber;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*/
|
||||
|
||||
@ -8,12 +8,11 @@ import org.apache.poi.xssf.usermodel.XSSFCell;
|
||||
import org.apache.poi.xssf.usermodel.XSSFRow;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -35,21 +34,11 @@ public class ExcelTemplateProc {
|
||||
String exportFilePathAndName,
|
||||
Map<String, Object> staticDataMap,
|
||||
List<DynamicDataMapping> dynamicDataMappingList) throws IOException {
|
||||
|
||||
InputStream inputStream;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
try (InputStream inputStream = openTemplateInputStream(templateFileName)) {
|
||||
Workbook workbook = dealAllSheetsByTemplate(inputStream, staticDataMap, dynamicDataMappingList);
|
||||
saveExportFile(workbook, exportFilePathAndName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一模板多 Sheet 导出:按 sheetConfigs 顺序复制模板页并分别填充,Sheet 名取自 sheetName(通常为工段名)。
|
||||
@ -60,12 +49,9 @@ public class ExcelTemplateProc {
|
||||
if (sheetConfigs == null || sheetConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("sheetConfigs 不能为空");
|
||||
}
|
||||
InputStream inputStream = openTemplateInputStream(templateFileName);
|
||||
XSSFWorkbook workbook;
|
||||
try {
|
||||
try (InputStream inputStream = openTemplateInputStream(templateFileName)) {
|
||||
workbook = new XSSFWorkbook(inputStream);
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
while (workbook.getNumberOfSheets() > 1) {
|
||||
workbook.removeSheetAt(workbook.getNumberOfSheets() - 1);
|
||||
@ -82,12 +68,105 @@ public class ExcelTemplateProc {
|
||||
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 {
|
||||
if (templateFileName.startsWith("/") || templateFileName.contains(":")) {
|
||||
return Files.newInputStream(new File(templateFileName).toPath());
|
||||
byte[] bytes = readTemplateBytes(templateFileName);
|
||||
return new ByteArrayInputStream(bytes);
|
||||
}
|
||||
ClassPathResource resource = new ClassPathResource(templateFileName);
|
||||
return resource.getInputStream();
|
||||
|
||||
private static byte[] readTemplateBytes(String templateFileName) throws IOException {
|
||||
if (templateFileName.startsWith("/") || templateFileName.contains(":")) {
|
||||
File file = new File(templateFileName);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
throw new FileNotFoundException("Excel模板不存在: " + templateFileName);
|
||||
}
|
||||
return validateXlsxBytes(Files.readAllBytes(file.toPath()), templateFileName);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.SimpleCache;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AccessLevel;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* bean深拷贝工具(基于 cglib 性能优异)
|
||||
* bean深拷贝工具
|
||||
* <p>
|
||||
* 重点 cglib 不支持 拷贝到链式对象
|
||||
* 重点 不支持 拷贝到链式对象
|
||||
* 例如: 源对象 拷贝到 目标(链式对象)
|
||||
* 请区分好`浅拷贝`和`深拷贝`再做使用
|
||||
*
|
||||
@ -60,8 +56,7 @@ public class BeanCopyUtils {
|
||||
if (ObjectUtil.isNull(desc)) {
|
||||
return null;
|
||||
}
|
||||
BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(source.getClass(), desc.getClass(), null);
|
||||
beanCopier.copy(source, desc, null);
|
||||
BeanUtil.copyProperties(source, desc);
|
||||
return desc;
|
||||
}
|
||||
|
||||
@ -92,12 +87,11 @@ public class BeanCopyUtils {
|
||||
* @param bean 数据来源实体
|
||||
* @return map对象
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Map<String, Object> copyToMap(T bean) {
|
||||
if (ObjectUtil.isNull(bean)) {
|
||||
return null;
|
||||
}
|
||||
return BeanMap.create(bean);
|
||||
return BeanUtil.beanToMap(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,7 +126,7 @@ public class BeanCopyUtils {
|
||||
if (ObjectUtil.isNull(bean)) {
|
||||
return null;
|
||||
}
|
||||
BeanMap.create(bean).putAll(map);
|
||||
BeanUtil.fillBeanWithMap(map, bean, false);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@ -155,50 +149,4 @@ public class BeanCopyUtils {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,6 +16,9 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Slf4j
|
||||
public final class FtpUtil {
|
||||
@ -138,6 +141,86 @@ public final class FtpUtil {
|
||||
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\令号 一致。
|
||||
*/
|
||||
@ -258,6 +341,9 @@ public final class FtpUtil {
|
||||
|
||||
FTPFile[] files = ftpClient.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
if (fileNameFilter != null && !fileNameFilter.isEmpty()) {
|
||||
return R.fail("远程目录为空,未找到文件: " + fileNameFilter + " (dir=" + remoteDir + ")");
|
||||
}
|
||||
return R.ok("远程目录为空,无需下载");
|
||||
}
|
||||
|
||||
@ -270,7 +356,8 @@ public final class FtpUtil {
|
||||
if (fileName.startsWith(".") || file.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) {
|
||||
if (fileNameFilter != null && !fileNameFilter.isEmpty()
|
||||
&& !fileNameMatchesFilter(fileName, fileNameFilter)) {
|
||||
continue;
|
||||
}
|
||||
totalCount++;
|
||||
@ -312,6 +399,191 @@ public final class FtpUtil {
|
||||
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) {
|
||||
String base = basePath == null ? "" : basePath.trim();
|
||||
String rel = filePath == null ? "" : filePath.trim();
|
||||
@ -417,6 +689,16 @@ public final class FtpUtil {
|
||||
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)
|
||||
throws IOException {
|
||||
if (ftpClient.changeWorkingDirectory(remoteDir)) {
|
||||
|
||||
@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
||||
* WC000002 机二工段 → 006 机加车间
|
||||
* WC000004 装一工段 → 007 装配车间
|
||||
* WC000005 装二工段 → 007 装配车间
|
||||
* WC000006 电钳工段 → 007 装配车间
|
||||
* WC000006 电钳工段 → 023 电钳车间
|
||||
* WC000007 下料工段 → 006 机加车间
|
||||
* WC000008 委外中心 → 006 机加车间
|
||||
* WC000010 下料工段 → 018 铆焊车间
|
||||
@ -31,16 +31,21 @@ public final class KingdeeRouteDepartmentHelper {
|
||||
public static final String DEPT_ASSEMBLY = "007";
|
||||
/** 铆焊车间 */
|
||||
public static final String DEPT_RIVET_WELD = "018";
|
||||
/** 电钳车间 */
|
||||
public static final String DEPT_ELECTRICIAN = "023";
|
||||
|
||||
/** 006 机加车间 */
|
||||
private static final Set<String> MACHINE_WORKSHOP = setOf(
|
||||
"机一工段", "机二工段", "机加委外");
|
||||
/** 007 装配车间 */
|
||||
private static final Set<String> ASSEMBLY_WORKSHOP = setOf(
|
||||
"装一工段", "装二工段", "电钳工段","装配委外");
|
||||
"装一工段", "装二工段", "装配委外");
|
||||
/** 018 铆焊车间 */
|
||||
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;
|
||||
static {
|
||||
@ -48,6 +53,7 @@ public final class KingdeeRouteDepartmentHelper {
|
||||
all.addAll(MACHINE_WORKSHOP);
|
||||
all.addAll(ASSEMBLY_WORKSHOP);
|
||||
all.addAll(RIVET_WELD_WORKSHOP);
|
||||
all.addAll(ELECTRICIAN_WORKSHOP);
|
||||
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)) {
|
||||
return DEPT_RIVET_WELD;
|
||||
}
|
||||
if (ELECTRICIAN_WORKSHOP.contains(wc)) {
|
||||
return DEPT_ELECTRICIAN;
|
||||
}
|
||||
throw new ServiceException("不支持的工作中心「" + wc + "」,仅允许:"
|
||||
+ String.join("、", ALLOWED_WORK_CENTERS));
|
||||
}
|
||||
@ -95,6 +104,9 @@ public final class KingdeeRouteDepartmentHelper {
|
||||
if (DEPT_RIVET_WELD.equals(deptFNumber)) {
|
||||
return "铆焊车间";
|
||||
}
|
||||
if (DEPT_ELECTRICIAN.equals(deptFNumber)) {
|
||||
return "电钳车间";
|
||||
}
|
||||
return deptFNumber != null ? deptFNumber : "";
|
||||
}
|
||||
|
||||
|
||||
@ -8,10 +8,12 @@ import com.itextpdf.text.pdf.PdfStamper;
|
||||
import org.apache.pdfbox.io.MemoryUsageSetting;
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
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)
|
||||
throws IOException, DocumentException {
|
||||
@ -140,23 +142,88 @@ public class PDFDocHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 PDF 第 1 页指定位置叠加文字(无图章),经临时文件替换原文件。
|
||||
* 在 PDF 第 1 页指定位置叠加文字(无图章),就地覆盖原文件。
|
||||
*
|
||||
* @param rotation 逆时针旋转角度(伊特左栏竖排一般为 90)
|
||||
*/
|
||||
public static void stampTextFirstPage(String pdfPath, List<Map<String, ?>> reviews, float[] pswX, float[] pswY,
|
||||
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;
|
||||
}
|
||||
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()) {
|
||||
throw new IllegalArgumentException("reviews 与坐标数组长度不一致");
|
||||
}
|
||||
File src = new File(pdfPath);
|
||||
File temp = new File(src.getParentFile(), "_plm_stamp_" + src.getName());
|
||||
PdfReader reader = new PdfReader(pdfPath);
|
||||
FileOutputStream fout = new FileOutputStream(temp);
|
||||
PdfStamper stp = new PdfStamper(reader, fout);
|
||||
byte[] stamped = stampFirstPageToBytes(pdfBytes, reviews, pswX, pswY, fontSize, rotation);
|
||||
if (writeBytesWithRetry(dest.toPath(), stamped, 20)) {
|
||||
return dest;
|
||||
}
|
||||
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 {
|
||||
PdfContentByte content = stp.getOverContent(1);
|
||||
content.beginText();
|
||||
@ -168,10 +235,42 @@ public class PDFDocHelper {
|
||||
content.endText();
|
||||
} finally {
|
||||
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 {
|
||||
PdfReader reader = new PdfReader(inputPath);
|
||||
|
||||
// 临时输出文件名,防止冲突
|
||||
String tempName = name.replace(".pdf", "_temp.pdf");
|
||||
String tempPath = outputPath + "/" + tempName;
|
||||
// 临时文件放到系统临时目录,避免 _temp.pdf 残留在 zip 输出目录被一起打包
|
||||
String baseName = name.toLowerCase().endsWith(".pdf") ? name.substring(0, name.length() - 4) : name;
|
||||
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);
|
||||
|
||||
PdfGState gs1 = new PdfGState();
|
||||
@ -268,28 +368,50 @@ public class PDFDocHelper {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
stamper.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
foc.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
reader.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
// 第二次处理 —— 添加文字水印
|
||||
// 第二次处理 —— 添加文字水印到正式输出文件
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
// 删除临时文件(失败不阻塞主流程)
|
||||
try {
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
} catch (Exception ignore) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
|
||||
// 删除临时文件
|
||||
File tempFile = new File(tempPath);
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
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合并
|
||||
* @param files
|
||||
|
||||
@ -28,6 +28,7 @@ import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
@ -161,7 +162,7 @@ public class PlusDataPermissionHandler {
|
||||
int index = sb.lastIndexOf(".");
|
||||
String clazzName = sb.substring(0, index);
|
||||
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))
|
||||
.filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
|
||||
DataPermission dataPermission;
|
||||
@ -196,4 +197,15 @@ public class PlusDataPermissionHandler {
|
||||
public boolean isInvalid(String 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,18 +12,21 @@ import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveItemDTO;
|
||||
import com.ruoyi.system.domain.dto.productionorder.AssemblyBomReceiveSummary;
|
||||
import com.ruoyi.system.service.IPlmReceiveLogService;
|
||||
import com.ruoyi.system.service.IProductionOrderService;
|
||||
import com.ruoyi.system.service.PlmPlanCompleteNotifyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* PLM 总装配 BOM 推送接收入口(POST JSON 数组)。
|
||||
@ -39,9 +42,10 @@ public class AssemblyBomReceiveController extends BaseController {
|
||||
|
||||
private final IProductionOrderService productionOrderService;
|
||||
private final IPlmReceiveLogService plmReceiveLogService;
|
||||
private final PlmPlanCompleteNotifyService plmPlanCompleteNotifyService;
|
||||
|
||||
/**
|
||||
* 接收总装配 BOM 行列表,按生产令号全量替换写入 production_order,并同步 bom_details(bomType=0)。
|
||||
* 接收总装配 BOM 行列表。已审核且主产品图号相同则拒绝;主产品图号不同则追加。
|
||||
* <p>
|
||||
* POST /system/plm/assemblyBomReceive<br>
|
||||
* Content-Type: application/json; charset=UTF-8
|
||||
@ -60,6 +64,7 @@ public class AssemblyBomReceiveController extends BaseController {
|
||||
summaryMap.put("lineCount", summary.getLineCount());
|
||||
summaryMap.put("insertedCount", summary.getInsertedCount());
|
||||
summaryMap.put("replaced", summary.isReplaced());
|
||||
summaryMap.put("appended", summary.isAppended());
|
||||
plmReceiveLogService.record(
|
||||
IPlmReceiveLogService.BIZ_ASSEMBLY_BOM,
|
||||
API_PATH,
|
||||
@ -73,6 +78,10 @@ public class AssemblyBomReceiveController extends BaseController {
|
||||
null,
|
||||
clientIp
|
||||
);
|
||||
// 工艺可能先到、总装后到:入库后再核实两边都有才发给 plan-user-id
|
||||
final String productionOrder = summary.getProductionOrderNo();
|
||||
CompletableFuture.runAsync(() ->
|
||||
plmPlanCompleteNotifyService.notifyIfAssemblyAndRouteComplete(productionOrder, null));
|
||||
return R.ok("接收成功", summary);
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
if (body == null) {
|
||||
return null;
|
||||
|
||||
@ -22,6 +22,8 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
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.system.domain.ProcessRoute;
|
||||
import com.ruoyi.system.domain.dto.*;
|
||||
@ -96,6 +98,9 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
private final DingTalkProperties dingTalkProperties;
|
||||
|
||||
/** 电气 BOM 导入成功后发「无需做工艺」钉钉,并写入推送日志 */
|
||||
private final SkipProcessNotifyService skipProcessNotifyService;
|
||||
|
||||
/**
|
||||
* 查询bom明细列表
|
||||
*/
|
||||
@ -437,6 +442,156 @@ public class BomDetailsController extends BaseController {
|
||||
|| "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 = "金蝶物料清单更新")
|
||||
@SaCheckPermission("system:details:updateMaterial")
|
||||
@ -473,9 +628,27 @@ public class BomDetailsController extends BaseController {
|
||||
if (bomDetailParams == null || bomDetailParams.isEmpty()) {
|
||||
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<KindegeeLogDTO> logDTOS = new ArrayList<>();
|
||||
List<String> failedMaterials = new ArrayList<>(); // 用于跟踪处理失败的物料
|
||||
Set<String> electricalOrderNos = new LinkedHashSet<>();
|
||||
String totalWeight = "";
|
||||
// 遍历前端传来的数据
|
||||
for (Map<String, String> param : bomDetailParams) {
|
||||
@ -505,6 +678,9 @@ public class BomDetailsController extends BaseController {
|
||||
failedMaterials.add(fnumber);
|
||||
continue;
|
||||
}
|
||||
if (containsElectricalExcelBom(bomDetails, bo)) {
|
||||
electricalOrderNos.add(bo.getProductionOrderNo());
|
||||
}
|
||||
// 第一步:处理所有物料的保存
|
||||
for (BomDetails material : bomDetails) {
|
||||
// 获取工艺表中的非委外工时
|
||||
@ -513,7 +689,7 @@ public class BomDetailsController extends BaseController {
|
||||
if (material.getPartNumber().startsWith("015") || material.getPartNumber().startsWith("009")) {
|
||||
return R.fail("015和009为管控物料,请先申请");
|
||||
}
|
||||
if (material.getRemarks() != null && material.getRemarks().contains("甲供")) {
|
||||
if (isOwnerSuppliedBom(material)) {
|
||||
continue;
|
||||
}
|
||||
String state = determineState(material);
|
||||
@ -537,13 +713,17 @@ public class BomDetailsController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:BOM校验和上传(在循环外部)
|
||||
ValidateBomResult validateResult = validateBOM(fnumber, bomDetails);
|
||||
// 第二步:BOM校验和上传(过滤甲供件,不进金蝶 BOM)
|
||||
List<BomDetails> uploadBomDetails = filterOwnerSuppliedFromBom(bomDetails);
|
||||
if (uploadBomDetails.isEmpty()) {
|
||||
log.info("过滤甲供件后无子项,跳过金蝶BOM上传, 物料编码: {}", fnumber);
|
||||
} else {
|
||||
ValidateBomResult validateResult = validateBOM(fnumber, uploadBomDetails);
|
||||
boolean needUpload = !validateResult.isMatch();
|
||||
if (needUpload) {
|
||||
try {
|
||||
// 物料清单保存方法,判断是电气还是生产
|
||||
BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(bomDetails, bo, groupName);
|
||||
BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(uploadBomDetails, bo, groupName);
|
||||
if (bomUploadResult.isSuccess()) {
|
||||
KindegeeLogDTO logDTO = new KindegeeLogDTO();
|
||||
logDTO.setProjectCode(bo.getProductionOrderNo());
|
||||
@ -575,6 +755,7 @@ public class BomDetailsController extends BaseController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isBlank(totalWeight)) {
|
||||
return R.fail("生产令号不能为空");
|
||||
}
|
||||
@ -584,7 +765,17 @@ public class BomDetailsController extends BaseController {
|
||||
return R.fail("未找到生产令号对应的项目: " + totalWeight);
|
||||
}
|
||||
processOrderProBo.setDrawingType(JSONUtil.toJsonStr(logDTOS));
|
||||
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);
|
||||
|
||||
// 更新钉钉卡片数据
|
||||
@ -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);
|
||||
ArrayList<JsonObject> fTreeEntityList = new ArrayList<>();
|
||||
for (BomDetails details : bomlist) {
|
||||
if (isOwnerSuppliedBom(details)) {
|
||||
log.info("跳过甲供件,不写入金蝶BOM: {}", details.getPartNumber());
|
||||
continue;
|
||||
}
|
||||
// 创建FTreeEntity对象,并加入FTreeEntity数组
|
||||
JsonObject fTreeEntityItem = new JsonObject();
|
||||
fTreeEntity.add(fTreeEntityItem);
|
||||
@ -854,6 +1054,10 @@ public class BomDetailsController extends BaseController {
|
||||
fTreeEntityItem.addProperty("FDENOMINATOR", details.getDenominator());
|
||||
fTreeEntityList.add(fTreeEntityItem);
|
||||
}
|
||||
if (fTreeEntity.size() == 0) {
|
||||
log.warn("过滤甲供件后无子项,跳过金蝶BOM保存, 父项: {}", bomDetails.getFNumber());
|
||||
return 1;
|
||||
}
|
||||
String jsonData = json.toString();
|
||||
log.debug("打印json:" + jsonData);
|
||||
System.out.println(jsonData);
|
||||
@ -947,6 +1151,10 @@ public class BomDetailsController extends BaseController {
|
||||
if (bomlist.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (isOwnerSuppliedBom(details)) {
|
||||
log.info("跳过甲供件,不写入金蝶BOM: {}", details.getPartNumber());
|
||||
continue;
|
||||
}
|
||||
// 创建FTreeEntity对象,并加入FTreeEntity数组
|
||||
JsonObject fTreeEntityItem = new JsonObject();
|
||||
fTreeEntity.add(fTreeEntityItem);
|
||||
@ -1006,6 +1214,10 @@ public class BomDetailsController extends BaseController {
|
||||
}
|
||||
fTreeEntityList.add(fTreeEntityItem);
|
||||
}
|
||||
if (fTreeEntity.size() == 0) {
|
||||
log.warn("过滤甲供件后无子项,跳过金蝶BOM保存, 图号={}", bomDetails1.getFNumber());
|
||||
return BOMUploadResult.success(null, "SKIP_OWNER_SUPPLY");
|
||||
}
|
||||
try {
|
||||
String resultJson = new K3CloudApi().save("ENG_BOM", json.toString());
|
||||
// 直接用 Jackson 解析
|
||||
@ -1049,6 +1261,33 @@ public class BomDetailsController extends BaseController {
|
||||
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.addProperty("FMATERIALID", 0);
|
||||
model.addProperty("FSpecification", bomDetails1.getMaterial());// 规格 为存入的材质字段
|
||||
// 电气规格型号:Excel「型号/规格型号」入库 material;生产建档仍走 loadMaterialPreservation,互不影响
|
||||
writeKingdeeElectricalSpecification(model, bomDetails1);
|
||||
model.addProperty("F_HBYT_PP", bomDetails1.getWareHouse());// 品牌 为存入的仓库字段
|
||||
writeKingdeeChildDanZhong(model, bomDetails1, "电气子项物料建档");
|
||||
model.addProperty("FNumber", bomDetails1.getPartNumber());
|
||||
@ -2006,46 +2246,10 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FBaseUnitId对象,并加入SubHeadEntity
|
||||
JsonObject fBaseUnitId = new JsonObject();
|
||||
switch (bomDetails1.getWareHouse()) {// 单位
|
||||
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;
|
||||
}
|
||||
String electricalUnit = StringUtils.trim(bomDetails1.getRemarks());
|
||||
fillElectricalKingdeeUnit(fBaseUnitId, electricalUnit);
|
||||
subHeadEntity.add("FBaseUnitId", fBaseUnitId);
|
||||
if (states.equals("1")) {
|
||||
if ("1".equals(states)) {
|
||||
subHeadEntity.addProperty("FIsPurchase", true);
|
||||
subHeadEntity.addProperty("FIsSale", true);
|
||||
subHeadEntity.addProperty("FIsInventory", true);
|
||||
@ -2062,44 +2266,7 @@ public class BomDetailsController extends BaseController {
|
||||
JsonObject subHeadEntity1 = new JsonObject();
|
||||
model.add("SubHeadEntity1", subHeadEntity1);
|
||||
JsonObject fStoreUnitId = new JsonObject();
|
||||
switch (bomDetails1.getWareHouse()) {
|
||||
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;
|
||||
}
|
||||
fillElectricalKingdeeUnit(fStoreUnitId, electricalUnit);
|
||||
subHeadEntity1.add("FStoreUnitID", fStoreUnitId);
|
||||
subHeadEntity1.addProperty("FUnitConvertDir", "1");
|
||||
|
||||
@ -2120,44 +2287,7 @@ public class BomDetailsController extends BaseController {
|
||||
subHeadEntity1.addProperty("FBoxStandardQty", 0.0);
|
||||
// 创建FPurchaseUnitId对象,并加入SubHeadEntity1
|
||||
JsonObject fPurchaseUnitId = new JsonObject();
|
||||
switch (bomDetails1.getWareHouse()) {
|
||||
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;
|
||||
}
|
||||
fillElectricalKingdeeUnit(fPurchaseUnitId, electricalUnit);
|
||||
subHeadEntity1.add("FPurchaseUnitId", fPurchaseUnitId);
|
||||
|
||||
// 创建SubHeadEntity3对象,并加入Model
|
||||
@ -2166,44 +2296,7 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FPurchasePriceUnitId对象,并加入SubHeadEntity3
|
||||
JsonObject fPurchasePriceUnitId = new JsonObject();
|
||||
switch (bomDetails1.getWareHouse()) {
|
||||
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;
|
||||
}
|
||||
fillElectricalKingdeeUnit(fPurchasePriceUnitId, electricalUnit);
|
||||
subHeadEntity3.add("FPurchasePriceUnitId", fPurchasePriceUnitId);
|
||||
|
||||
subHeadEntity3.addProperty("FIsQuota", false);
|
||||
@ -2212,18 +2305,18 @@ public class BomDetailsController extends BaseController {
|
||||
JsonObject subHeadEntity6 = new JsonObject();
|
||||
model.add("SubHeadEntity6", subHeadEntity6);
|
||||
// 不同的属性不同的检验方案
|
||||
if (states.equals("1")) {
|
||||
if ("1".equals(states)) {
|
||||
// 外购
|
||||
subHeadEntity6.addProperty("FCheckIncoming", true);
|
||||
subHeadEntity6.addProperty("FCheckStock", true);
|
||||
}
|
||||
if (states.equals("2")) {
|
||||
if ("2".equals(states)) {
|
||||
// 自制
|
||||
subHeadEntity6.addProperty("FCheckProduct", true);
|
||||
subHeadEntity6.addProperty("FCheckStock", true);
|
||||
subHeadEntity6.addProperty("FCheckReturnMtrl", true);
|
||||
}
|
||||
if (states.equals("3")) {
|
||||
if ("3".equals(states)) {
|
||||
// 委外
|
||||
subHeadEntity6.addProperty("FCheckIncoming", true);
|
||||
subHeadEntity6.addProperty("FCheckProduct", true);
|
||||
@ -2236,44 +2329,7 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FProduceUnitId对象,并加入SubHeadEntity5
|
||||
JsonObject fProduceUnitId = new JsonObject();
|
||||
switch (bomDetails1.getWareHouse()) {
|
||||
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;
|
||||
}
|
||||
fillElectricalKingdeeUnit(fProduceUnitId, electricalUnit);
|
||||
subHeadEntity5.add("FProduceUnitId", fProduceUnitId);
|
||||
|
||||
// 创建FProduceBillType对象,并加入SubHeadEntity5
|
||||
@ -2283,46 +2339,9 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
// 创建FBOMUnitId对象,并加入SubHeadEntity5
|
||||
JsonObject fBOMUnitId = new JsonObject();
|
||||
switch (bomDetails1.getRemarks()) {
|
||||
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;
|
||||
}
|
||||
fillElectricalKingdeeUnit(fBOMUnitId, electricalUnit);
|
||||
subHeadEntity5.add("FBOMUnitId", fBOMUnitId);
|
||||
subHeadEntity5.addProperty("FIsMainPrd", !states.equals("1"));
|
||||
subHeadEntity5.addProperty("FIsMainPrd", !"1".equals(states));
|
||||
subHeadEntity5.addProperty("FIssueType", "1");
|
||||
|
||||
// 创建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
|
||||
* daorubaon
|
||||
@ -2392,8 +2418,9 @@ public class BomDetailsController extends BaseController {
|
||||
@Log(title = "导入电气bom", businessType = BusinessType.IMPORT)
|
||||
@SaCheckPermission("system:details:importElectricalBom")
|
||||
@PostMapping(value = "/importElectricalBom", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<Map<String, Object>> importElectricalBom(@RequestPart("file") MultipartFile file) {
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
public R<Map<String, Object>> importElectricalBom(
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestParam(value = "productionOrderNo", required = false) String productionOrderNo) {
|
||||
try {
|
||||
if (file == null || file.isEmpty() || StringUtils.isEmpty(file.getOriginalFilename())) {
|
||||
return R.fail("请选择有效的Excel文件");
|
||||
@ -2404,12 +2431,37 @@ public class BomDetailsController extends BaseController {
|
||||
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 skippedLingYong = 0;
|
||||
int skippedEmpty = 0;
|
||||
List<BomDetails> list = new ArrayList<>();
|
||||
Set<String> orderNos = new LinkedHashSet<>();
|
||||
if (StringUtils.isNotBlank(productionOrderNo)) {
|
||||
orderNos.add(productionOrderNo.trim());
|
||||
}
|
||||
if (rows != null) {
|
||||
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("领用")) {
|
||||
skippedLingYong++;
|
||||
continue;
|
||||
}
|
||||
BomDetails bomDetails = new BomDetails();
|
||||
@ -2418,7 +2470,8 @@ public class BomDetailsController extends BaseController {
|
||||
bomDetails.setFName(bomVO.getParentPart());
|
||||
bomDetails.setPartNumber(bomVO.getDrawingNo());
|
||||
bomDetails.setName(bomVO.getDrawingName());
|
||||
bomDetails.setMaterial(bomVO.getModel());
|
||||
String specification = firstNonBlankTrim(bomVO.getModel(), bomVO.getSpecificationModel());
|
||||
bomDetails.setMaterial(specification);
|
||||
bomDetails.setWareHouse(bomVO.getBrand());
|
||||
String brand = bomVO.getBrand();
|
||||
if (brand != null && brand.contains("伊特")) {
|
||||
@ -2426,10 +2479,9 @@ public class BomDetailsController extends BaseController {
|
||||
} else {
|
||||
bomDetails.setStats("外购");
|
||||
}
|
||||
bomDetails.setQuantity(bomVO.getQuantity() == null ? null : bomVO.getQuantity().toString());
|
||||
applyElectricalQuantity(bomDetails, bomVO.getQuantity());
|
||||
bomDetails.setRemarks(bomVO.getUnit());
|
||||
bomDetails.setUpdateTime(new Date());
|
||||
bomDetails.setDenominator(1.0);
|
||||
bomDetails.setBomType("1");
|
||||
list.add(bomDetails);
|
||||
}
|
||||
@ -2437,19 +2489,138 @@ public class BomDetailsController extends BaseController {
|
||||
|
||||
List<BomDetailsVo> vos = BeanUtil.copyToList(list, BomDetailsVo.class);
|
||||
List<BomDetails> toInsert = saveBomDetails(vos);
|
||||
boolean saved = false;
|
||||
if (!toInsert.isEmpty()) {
|
||||
saved = bomDetailsMapper.insertBatch(toInsert);
|
||||
int matchedMaterial = 0;
|
||||
int unmatchedMaterial = 0;
|
||||
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("parsedRows", list.size());
|
||||
resp.put("savedRows", saved);
|
||||
resp.put("message", "导入完成");
|
||||
return R.ok(resp);
|
||||
} catch (Exception e) {
|
||||
return R.fail("导入失败: " + e.getMessage());
|
||||
resp.put("skippedLingYong", skippedLingYong);
|
||||
resp.put("skippedEmpty", skippedEmpty);
|
||||
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) {
|
||||
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 表中
|
||||
|
||||
@ -22,8 +22,10 @@ import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.JdUtils;
|
||||
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.poi.ExcelUtil;
|
||||
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.IPartCostService;
|
||||
import com.ruoyi.system.service.IPlmPdfZipPushService;
|
||||
import com.ruoyi.system.service.PlmDrawingArriveNotifyService;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -94,6 +97,7 @@ public class ImMaterialController extends BaseController {
|
||||
|
||||
private final IPartCostService iPartCostService;
|
||||
private final IPlmPdfZipPushService plmPdfZipPushService;
|
||||
private final PlmDrawingArriveNotifyService plmDrawingArriveNotifyService;
|
||||
|
||||
@Value("${plm.pdf.base-dir:D:/plm/pdf}")
|
||||
private String plmPdfBaseDir;
|
||||
@ -222,6 +226,16 @@ public class ImMaterialController extends BaseController {
|
||||
if (cleaned > 0) {
|
||||
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);
|
||||
} catch (Exception 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));
|
||||
}
|
||||
|
||||
Long processOrderProId;
|
||||
try {
|
||||
// 落盘前校验:令号必须已存在,不再自动建项目
|
||||
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo, null);
|
||||
} catch (ServiceException e) {
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
String safeProductionOrderNo = sanitizePathPart(productionOrderNo);
|
||||
String separator = resolvePlmPdfNameSeparator();
|
||||
Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo);
|
||||
@ -357,11 +379,13 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
|
||||
Long pushRecordId = null;
|
||||
Long processOrderProId = null;
|
||||
try {
|
||||
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo);
|
||||
// 落盘后按推送明细补默认产品(项目已存在)
|
||||
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(productionOrderNo, saved);
|
||||
pushRecordId = plmPdfZipPushService.savePushRecord(
|
||||
itemsJson, "batch", productionOrderNo, dir.toString(), saved);
|
||||
} catch (ServiceException e) {
|
||||
return R.fail(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("[PLM-PDF] 批量推送记录入库失败 productionOrderNo={}", productionOrderNo, e);
|
||||
}
|
||||
@ -395,6 +419,9 @@ public class ImMaterialController extends BaseController {
|
||||
if (cleaned > 0) {
|
||||
resp.put("cleanedParentPdfCount", cleaned);
|
||||
}
|
||||
plmDrawingArriveNotifyService.notifyAsync(
|
||||
productionOrderNo, "/system/material/plm/pdf/batch", "批量",
|
||||
processOrderProId, pushRecordId, saved);
|
||||
return R.ok(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("批量接收PLM PDF图纸失败: productionOrderNo={}", productionOrderNo, e);
|
||||
@ -413,6 +440,15 @@ public class ImMaterialController extends BaseController {
|
||||
if (parsed.failed()) {
|
||||
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 dwgDir = dir.resolve(PLM_DWG_SUBDIR);
|
||||
String separator = resolvePlmPdfNameSeparator();
|
||||
@ -472,11 +508,13 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
|
||||
Long pushRecordId = null;
|
||||
Long processOrderProId = null;
|
||||
try {
|
||||
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(parsed.productionOrderNo);
|
||||
// 落盘后按推送明细补默认产品(项目已存在)
|
||||
processOrderProId = plmPdfZipPushService.ensureProcessOrderProForPlmPdf(parsed.productionOrderNo, saved);
|
||||
pushRecordId = plmPdfZipPushService.savePushRecord(
|
||||
manifestJson, parsed.archiveName, parsed.productionOrderNo, dir.toString(), saved);
|
||||
} catch (ServiceException e) {
|
||||
return R.fail(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("[PLM-PDF] 推送记录入库失败 productionOrderNo={}, archive={}",
|
||||
parsed.productionOrderNo, parsed.archiveName, e);
|
||||
@ -516,6 +554,9 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
log.info("[PLM-PDF] 接收完成 productionOrderNo={}, count={}, archive={}, pushRecordId={}, processOrderProId={}, cleanedParentPdfCount={}",
|
||||
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);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
@ -719,6 +760,7 @@ public class ImMaterialController extends BaseController {
|
||||
|
||||
/**
|
||||
* 同令号同图号覆盖:先删除目录内该图号的旧文件(含历史时间戳后缀),再落盘为标准名。
|
||||
* 按完整图号匹配,不会把 {@code 图号-MP} 等更长图号当成同一张图。
|
||||
*
|
||||
* @param extension 不含点,如 pdf / dwg
|
||||
*/
|
||||
@ -732,7 +774,7 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录中与指定图号匹配的图纸(标准名、时间戳后缀名均清理)
|
||||
* 删除目录中与指定图号匹配的图纸(标准名、历史时间戳后缀名均清理;不按「图号-」前缀误伤 -MP 等更长图号)
|
||||
*/
|
||||
private static void deleteExistingDrawingsByMaterialCode(Path dir, String materialCode,
|
||||
String separator, String extension) {
|
||||
@ -741,15 +783,12 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
String code = sanitizeFileNamePart(materialCode);
|
||||
String sep = StringUtils.isNotBlank(separator) ? separator : "_";
|
||||
String prefixWithSep = code + sep;
|
||||
String ext = extension.startsWith(".") ? extension.toLowerCase(Locale.ROOT)
|
||||
: ("." + extension.toLowerCase(Locale.ROOT));
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*" + ext)) {
|
||||
for (Path file : stream) {
|
||||
String name = file.getFileName().toString();
|
||||
if (name.equalsIgnoreCase(code + ext)
|
||||
|| name.regionMatches(true, 0, prefixWithSep, 0, prefixWithSep.length())
|
||||
|| name.regionMatches(true, 0, code + "-", 0, code.length() + 1)) {
|
||||
if (PlmDrawingFileNames.matchesMaterialCode(name, code, sep, extension)) {
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
log.info("[PLM-PDF] 同令号同图号覆盖,已删除旧文件: {}", file);
|
||||
@ -941,7 +980,7 @@ public class ImMaterialController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询物料检索列表
|
||||
* 查询物料检索列表(实时金蝶物料)
|
||||
*/
|
||||
@SaCheckPermission("system:material:list")
|
||||
@GetMapping("/list")
|
||||
|
||||
@ -7,8 +7,7 @@ import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.JdUtils;
|
||||
import com.ruoyi.system.domain.ImMaterial;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.bo.ImMaterialBo;
|
||||
import com.ruoyi.system.domain.bo.ImProductionPlanBo;
|
||||
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.service.IImMaterialService;
|
||||
import com.ruoyi.system.service.IImProductionPlanService;
|
||||
import io.netty.util.concurrent.CompleteFuture;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@ -33,6 +29,8 @@ import java.util.concurrent.*;
|
||||
@RequestMapping("/index")
|
||||
@Slf4j
|
||||
public class IndexController {
|
||||
private static final int MAX_MATERIAL_CODES = 50;
|
||||
|
||||
private final IImMaterialService iImMaterialService;
|
||||
private final IImProductionPlanService imProductionPlanService;
|
||||
|
||||
@ -42,111 +40,123 @@ public class IndexController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶chaxun
|
||||
* 金蝶库存查询(支持单个或多个物料编码)
|
||||
* <p>
|
||||
* 前端一次传入多个编码;后端对每个物料按原逻辑精确查询后汇总返回(金蝶侧不支持可靠的多码 IN 拼接)。
|
||||
* 参数任选其一:
|
||||
* - materialCode:单个物料编码(兼容旧调用)
|
||||
* - materialCodes:多个物料编码,逗号分隔
|
||||
*/
|
||||
@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(
|
||||
4, // 线程池核心线程数
|
||||
4, // 线程池最大线程数
|
||||
0L, // 线程空闲时间
|
||||
TimeUnit.MILLISECONDS, // 时间单位
|
||||
new LinkedBlockingQueue<>(10), // 线程队列
|
||||
new ThreadPoolExecutor.DiscardPolicy() // 拒绝策略
|
||||
poolSize,
|
||||
poolSize,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(Math.max(codes.size() * 2, 10)),
|
||||
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||
);
|
||||
try {
|
||||
// 并行执行多个查询任务
|
||||
CompletableFuture<Double> inventoryFuture = CompletableFuture.supplyAsync(() -> {
|
||||
log.info("开始查询即时库存数量: {}", materialCode);
|
||||
return Optional.ofNullable(JdUtil.getKuCun(materialCode))
|
||||
.map(list -> list.stream()
|
||||
.mapToDouble(JDInventoryDTO::getFBaseQty)
|
||||
.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);
|
||||
List<CompletableFuture<InventoryInfoVO>> futures = new ArrayList<>(codes.size());
|
||||
for (String code : codes) {
|
||||
futures.add(CompletableFuture.supplyAsync(() -> queryOne(code), executorService));
|
||||
}
|
||||
});
|
||||
|
||||
// 获取最终结果
|
||||
return R.ok(resultFuture.get(30, TimeUnit.SECONDS));
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.get(Math.max(30, codes.size() * 10L), 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) {
|
||||
log.error("查询库存信息失败", e);
|
||||
return R.fail("查询库存信息失败:" + e.getMessage());
|
||||
} finally {
|
||||
// 关闭线程池
|
||||
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 = "生产计划")
|
||||
@PostMapping("/JDList")
|
||||
public void synchronize_lists() {
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
|
||||
//请求参数,要求为json字符串
|
||||
JsonObject json = new JsonObject();
|
||||
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");
|
||||
@ -168,8 +178,6 @@ public class IndexController {
|
||||
|
||||
String jsonData = json.toString();
|
||||
try {
|
||||
|
||||
// 调用接口并获取结果
|
||||
String input = String.valueOf(client.executeBillQuery(jsonData));
|
||||
input = input.substring(2, input.length() - 2);
|
||||
String[] split = input.split("]\\, \\[");
|
||||
@ -180,7 +188,6 @@ public class IndexController {
|
||||
imProductionPlan.setProductionCode(imPro[1]);
|
||||
imProductionPlan.setTypeName(imPro[2]);
|
||||
imProductionPlan.setWorkShop(imPro[7]);
|
||||
// 解析时间
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||
Date parse = simpleDateFormat.parse(imPro[8]);
|
||||
imProductionPlan.setDateF(parse);
|
||||
@ -188,7 +195,6 @@ public class IndexController {
|
||||
imProductionPlan.setProductTyp(imPro[10]);
|
||||
imProductionPlan.setFigureNumber(imPro[3]);
|
||||
imProductionPlan.setFigureName(imPro[4]);
|
||||
// imProductionPlan.setOneNumber(imPro[5]);
|
||||
imProductionPlan.setPickmtrlStatus(imPro[10]);
|
||||
imProductionPlan.setUpdateTime(new Date());
|
||||
imProductionPlan.setSearchValue("1");
|
||||
|
||||
@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
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.vo.ProcessOrderProVo;
|
||||
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) {
|
||||
TableDataInfo<ProcessOrderProVo> page = processOrderProService.queryPageList(bo, pageQuery);
|
||||
List<ProcessOrderProVo> rows = page.getRows();
|
||||
List<PlmProductionOrderVo> mapped = rows == null ? Collections.emptyList()
|
||||
: rows.stream().map(this::toPlmVo).collect(Collectors.toList());
|
||||
List<PlmProductionOrderVo> mapped = rows == null ? Collections.emptyList() : rows.stream().map(this::toPlmVo).collect(Collectors.toList());
|
||||
TableDataInfo<PlmProductionOrderVo> result = new TableDataInfo<>(mapped, page.getTotal());
|
||||
result.setCode(page.getCode());
|
||||
result.setMsg(page.getMsg());
|
||||
@ -63,7 +63,7 @@ public class PlmProductionOrderController extends BaseController {
|
||||
vo.setUnit(src.getUnit());
|
||||
vo.setNeedProcess(src.getNeedProcess());
|
||||
vo.setBomStatus(src.getBomStatus());
|
||||
vo.setBomStatusLabel(resolveBomStatusLabel(src.getBomStatus()));
|
||||
vo.setBomStatusLabel(src.getBomStatus() == null ? null : ProjectStatusLock.statusLabel(src.getBomStatus()));
|
||||
vo.setRouteStatus(src.getRouteStatus());
|
||||
vo.setRouteStatusLabel(resolveRouteStatusLabel(src.getRouteStatus()));
|
||||
vo.setPlanStartTime(src.getPlanStartTime());
|
||||
@ -72,39 +72,6 @@ public class PlmProductionOrderController extends BaseController {
|
||||
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已拒绝 */
|
||||
private static String resolveRouteStatusLabel(Long status) {
|
||||
if (status == null) {
|
||||
|
||||
@ -26,6 +26,7 @@ import com.ruoyi.common.poi.DynamicDataMapping;
|
||||
import com.ruoyi.system.constant.ProcessOrderProSource;
|
||||
import com.ruoyi.system.domain.*;
|
||||
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.KingdeePurchaseOrderDTO;
|
||||
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.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||
@ -133,6 +135,28 @@ public class ProcessOrderProController extends BaseController {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置项目状态。
|
||||
* 独立接口,避免走 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置出图模式: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")
|
||||
@Log(title = "执行出图", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@ -361,6 +409,7 @@ public class ProcessOrderProController extends BaseController {
|
||||
os.flush();
|
||||
}
|
||||
log.info("下载PDF ZIP 输出完成: id={}, file={}", id, file.getName());
|
||||
iProcessOrderProService.cleanupPdfRunDir(filePath);
|
||||
|
||||
} catch (Exception e) {
|
||||
if (response.isCommitted()) {
|
||||
@ -430,6 +479,7 @@ public class ProcessOrderProController extends BaseController {
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
iProcessOrderProService.cleanupPdfRunDir(zipFile.getAbsolutePath());
|
||||
|
||||
} catch (Exception e) {
|
||||
if (response.isCommitted()) {
|
||||
@ -1947,7 +1997,8 @@ public class ProcessOrderProController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId。
|
||||
* 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId(工艺)。
|
||||
* 项目「是否做工艺=否」时不推送。
|
||||
*/
|
||||
@SaCheckPermission("system:orderPro:sendProcessRoutePlanExcel")
|
||||
@Log(title = "发送工艺计划表", businessType = BusinessType.OTHER)
|
||||
@ -1956,6 +2007,16 @@ public class ProcessOrderProController extends BaseController {
|
||||
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, "项目未配置生产令号");
|
||||
return;
|
||||
}
|
||||
if (ProcessOrderProSource.PLM.equals(orderPro.getSource())
|
||||
&& !Integer.valueOf(1).equals(orderPro.getDrawingQtyIssued())) {
|
||||
writeJsonFail(response, "请先点击「确认图纸已齐」,图纸全部上传完成后再下载图纸");
|
||||
return;
|
||||
}
|
||||
List<File> dwgFiles = plmPdfZipPushService.listDownloadableDwgFiles(orderPro.getProductionOrderNo());
|
||||
String zipName = orderPro.getProductionOrderNo().trim() + "_PLM_DWG.zip";
|
||||
String encoded = URLEncoder.encode(zipName, "UTF-8").replaceAll("\\+", "%20");
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.ruoyi.system.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
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.utils.FtpUtil;
|
||||
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.file.SmbUtil;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.constant.DingTodoSceneCodes;
|
||||
import com.ruoyi.system.domain.*;
|
||||
import com.ruoyi.system.domain.bo.ProcessRouteBo;
|
||||
import com.ruoyi.system.domain.dto.*;
|
||||
@ -66,8 +70,6 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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;
|
||||
@Autowired
|
||||
private IProcessOrderProService processOrderProService;
|
||||
/** 计划表落盘 + 按 plan-user-id 推送(与 PLM 导出共用) */
|
||||
/**
|
||||
* 计划表落盘 + 按 plan-user-id 推送(与 PLM 导出共用)
|
||||
*/
|
||||
private final ProcessPlanExcelPushService processPlanExcelPushService;
|
||||
private final IDrawingRouteOperLogService drawingRouteOperLogService;
|
||||
private final IDingTodoDispatchService dingTodoDispatchService;
|
||||
private final DrawingFileQueryService drawingFileQueryService;
|
||||
@Autowired
|
||||
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")
|
||||
public TableDataInfo<ProcessRouteVo> list(ProcessRouteBo bo, PageQuery pageQuery) {
|
||||
try {
|
||||
@ -400,7 +407,8 @@ public class ProcessRouteController extends BaseController {
|
||||
@Log(title = "生产工艺计划表导入", businessType = BusinessType.IMPORT)
|
||||
@SaCheckPermission("system:route:importRoute")
|
||||
@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();
|
||||
|
||||
log.info("读取文件名: " + originalFilename);
|
||||
@ -410,23 +418,57 @@ public class ProcessRouteController extends BaseController {
|
||||
// 读取总装部分sheet
|
||||
ExcelResult<ProductionOrderVo> result1 = ExcelUtil.importExcelSheet1(file.getInputStream(), ProductionOrderVo.class, true);
|
||||
List<ProductionOrderVo> list = result1.getList();
|
||||
String productionOrderNo = list1.get(0).getRouteDescription();
|
||||
List<BomDetails> bomDetails = iBomDetailsService.selectByProjectNumber(list1.get(0).getRouteDescription());
|
||||
List<ProcessRoute> route = iProcessRouteService.selectByProjectNumber(list1.get(0).getRouteDescription());
|
||||
if (list1.isEmpty()) {
|
||||
return R.fail("项目 " + productionOrderNo + "工艺生产计划表为空,请检查此表或将sheet移动至第七个!");
|
||||
long started = System.currentTimeMillis();
|
||||
if (list1 == null || list1.isEmpty()) {
|
||||
recordRouteUpload(null, expectedOrderNo, originalFilename, 0,
|
||||
IDrawingRouteOperLogService.STATUS_FAIL, "工艺生产计划表为空", started);
|
||||
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()) {
|
||||
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
|
||||
IDrawingRouteOperLogService.STATUS_FAIL, "已导入过BOM,请先清空再上传", started);
|
||||
return R.fail("项目 " + productionOrderNo + "已导入过BOM,请先清空再上传");
|
||||
}
|
||||
if (!route.isEmpty()) {
|
||||
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
|
||||
IDrawingRouteOperLogService.STATUS_FAIL, "已导入过工艺,请先清空再上传", started);
|
||||
return R.fail("项目 " + productionOrderNo + "已导入过工艺 ,请先清空再上传");
|
||||
}
|
||||
String workCenterError = validateImportWorkCenters(list1);
|
||||
if (workCenterError != null) {
|
||||
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
|
||||
IDrawingRouteOperLogService.STATUS_FAIL, "工段校验失败", started);
|
||||
return R.fail("生产工艺计划表工段校验失败:\n" + workCenterError);
|
||||
}
|
||||
if (iProcessRouteService.saveData(list1, list)) {
|
||||
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
|
||||
IDrawingRouteOperLogService.STATUS_SUCCESS, null, started);
|
||||
// 计划表落到服务器固定目录(app.plan-excel.base-dir/令号/),保留不删
|
||||
File savedPlanFile = null;
|
||||
try {
|
||||
@ -456,12 +498,38 @@ public class ProcessRouteController extends BaseController {
|
||||
log.warn("获取当前登录用户昵称失败", e);
|
||||
}
|
||||
|
||||
String todoProjectName = orderForLock == null ? null : orderForLock.getProductionName();
|
||||
Date todoProjectEndTime = orderForLock == null ? null : orderForLock.getProjectEndTime();
|
||||
String planUserIds = dingTalkProperties.getPlanUserId();
|
||||
// 异步:按 plan-user-id 发文件 + 发工艺审核卡片(落盘文件保留)
|
||||
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(() -> {
|
||||
// 与 PLM 导出推送同一套接收人
|
||||
Thread.currentThread().setContextClassLoader(contextClassLoader);
|
||||
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 {
|
||||
ProcessOrderPro order = processOrderProService.selectByProjectNumber(productionOrderNo);
|
||||
if (order != null) {
|
||||
@ -476,7 +544,7 @@ public class ProcessRouteController extends BaseController {
|
||||
cardDataMap.put("lastMessage", "请尽快处理");
|
||||
cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批"));
|
||||
|
||||
String outTrackId = "audit-card-" + System.currentTimeMillis();
|
||||
outTrackId = "audit-card-" + System.currentTimeMillis();
|
||||
// 调用发送给工艺负责人的方法 createAndDeliverCardToUser
|
||||
DingUtil.createAndDeliverCardToUser(
|
||||
accessToken,
|
||||
@ -497,17 +565,172 @@ public class ProcessRouteController extends BaseController {
|
||||
// 更新审核状态为1(已审核/提交审核)
|
||||
update.setRouteStatus(0L);
|
||||
proMapper.updateById(update);
|
||||
cardOk = true;
|
||||
} else {
|
||||
cardError = "项目不存在,未发送审核卡片";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
cardError = e.getMessage();
|
||||
log.error("发送钉钉卡片异常", e);
|
||||
}
|
||||
recordRouteNotifyAfterUpload(productionOrderNo, projectId, originalFilename, finalNickName,
|
||||
filePush, cardOk, cardError, outTrackId, operator, clientIp, notifyStarted);
|
||||
});
|
||||
return R.ok("上传物料成功");
|
||||
} else {
|
||||
recordRouteUpload(orderForLock, productionOrderNo, originalFilename, list1.size(),
|
||||
IDrawingRouteOperLogService.STATUS_FAIL, "导入失败", started);
|
||||
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")
|
||||
@PostMapping("/pushRouteBom")
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按物料推送工艺:只推所选物料,不改项目整单状态。
|
||||
*/
|
||||
@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 = "审核通过(发送钉钉通知,不推送金蝶)")
|
||||
@SaCheckPermission("system:route:approveRoute")
|
||||
@PostMapping("/approveRoute")
|
||||
@ -583,41 +848,54 @@ public class ProcessRouteController extends BaseController {
|
||||
return iProcessRouteService.getSelecPlanRouteList(rooteProdet);
|
||||
}
|
||||
|
||||
@Log(title = "生成项目图纸PDF")
|
||||
@Log(title = "生成工序计划单PDF")
|
||||
@SaCheckPermission("system:route:generatePDFs")
|
||||
@GetMapping("/generatePDFs")
|
||||
public void generatePDFs(String rooteProdet, HttpServletResponse response) throws IOException {
|
||||
// 调用服务层方法生成 ZIP 文件并获取其路径
|
||||
String zipFilePath = iProcessRouteService.generatePDFs(rooteProdet);
|
||||
ProcessOrderPro processOrderPro = proMapper.selectByProjectNumber(rooteProdet);
|
||||
processOrderPro.setBomStatus(5L);
|
||||
proMapper.updateById(processOrderPro);
|
||||
// 读取文件为字节数组
|
||||
File zipFile = new File(zipFilePath);
|
||||
if (!zipFile.exists()) {
|
||||
throw new FileNotFoundException("ZIP 文件未找到: " + zipFilePath);
|
||||
public void generatePDFs(String rooteProdet,
|
||||
@RequestParam(value = "materialCode", required = false) String materialCode,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (StringUtils.isBlank(rooteProdet)) {
|
||||
throw new ServiceException("生产令号不能为空");
|
||||
}
|
||||
if (zipFile.length() <= 0) {
|
||||
throw new IOException("ZIP 文件为空: " + zipFilePath);
|
||||
// 有物料编码:单份 PDF;无物料编码:令号下全部 PDF 压缩包
|
||||
String filePath = iProcessRouteService.generatePDFs(rooteProdet, materialCode);
|
||||
File outFile = new File(filePath);
|
||||
if (!outFile.exists()) {
|
||||
throw new FileNotFoundException("文件未找到: " + filePath);
|
||||
}
|
||||
try (ZipFile zf = new ZipFile(zipFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
if (entries == null || !entries.hasMoreElements()) {
|
||||
throw new IOException("ZIP 文件中没有PDF: " + zipFilePath);
|
||||
if (outFile.length() <= 0) {
|
||||
throw new IOException("文件为空: " + filePath);
|
||||
}
|
||||
boolean isPdf = filePath.toLowerCase().endsWith(".pdf");
|
||||
boolean isZip = filePath.toLowerCase().endsWith(".zip");
|
||||
if (!isPdf && !isZip) {
|
||||
throw new IOException("未生成PDF或压缩包: " + filePath);
|
||||
}
|
||||
FileInputStream fis = new FileInputStream(zipFile);
|
||||
OutputStream outputStreams = response.getOutputStream();
|
||||
response.setContentType("application/zip");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 = "更新计划时间")
|
||||
@ -719,7 +997,7 @@ public class ProcessRouteController extends BaseController {
|
||||
log.info("读取文件名: " + originalFilename);
|
||||
ExcelResult<ProcessRouteVo> result = ExcelUtil.importExcelSheet6(file.getInputStream(), ProcessRouteVo.class, true);
|
||||
List<ProcessRouteVo> list = result.getList();
|
||||
List<ProcessRoute> list1 = iProcessRouteService.importDataTime(list);
|
||||
iProcessRouteService.importDataTime(list);
|
||||
return R.ok("更新成功");
|
||||
}
|
||||
|
||||
|
||||
@ -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.PlmProcessRoutePushNormalizer;
|
||||
import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
import com.ruoyi.system.service.IDrawingRouteOperLogService;
|
||||
import com.ruoyi.system.service.IPlmProcessRoutePushService;
|
||||
import com.ruoyi.system.service.IPlmReceiveLogService;
|
||||
import com.ruoyi.system.service.PlmPlanCompleteNotifyService;
|
||||
import com.ruoyi.system.service.ProcessPlanExcelPushService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -46,8 +48,11 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
private final IPlmProcessRoutePushService plmProcessRoutePushService;
|
||||
private final IPlmReceiveLogService plmReceiveLogService;
|
||||
private final ProcessOrderProMapper processOrderProMapper;
|
||||
/** 导出计划表落盘 + 按 plan-user-id 推送(与手动上传一致) */
|
||||
/** 导出计划表落盘;总装已到才按 plan-user-id 推送 */
|
||||
private final ProcessPlanExcelPushService processPlanExcelPushService;
|
||||
/** 总装 BOM 与工艺都到齐后发给 plan-user-id */
|
||||
private final PlmPlanCompleteNotifyService plmPlanCompleteNotifyService;
|
||||
private final IDrawingRouteOperLogService drawingRouteOperLogService;
|
||||
|
||||
/**
|
||||
* 接收一个生产令号下的物料、材料 BOM、工艺路线及工步数据。
|
||||
@ -81,6 +86,8 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
summaryMap.put("savedBomCount", summary.getSavedBomCount());
|
||||
summaryMap.put("savedMaterialBomCount", summary.getSavedMaterialBomCount());
|
||||
summaryMap.put("savedRouteCount", summary.getSavedRouteCount());
|
||||
summaryMap.put("appended", summary.isAppended());
|
||||
summaryMap.put("replaced", summary.isReplaced());
|
||||
int saved = summary.getSavedBomCount() + summary.getSavedMaterialBomCount() + summary.getSavedRouteCount();
|
||||
plmReceiveLogService.record(
|
||||
IPlmReceiveLogService.BIZ_PROCESS_ROUTE,
|
||||
@ -95,8 +102,17 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
null,
|
||||
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();
|
||||
CompletableFuture.runAsync(() -> exportAndPushPlanExcel(productionOrder));
|
||||
|
||||
@ -119,18 +135,38 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
e.getMessage(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PLM 工艺入库后:按模板导出计划表,存到 app.plan-excel.base-dir,再按 plan-user-id 推送。
|
||||
* PLM 工艺入库后:按模板导出计划表并落盘;总装已到才发给 plan-user-id。
|
||||
*/
|
||||
private void exportAndPushPlanExcel(String productionOrder) {
|
||||
long started = System.currentTimeMillis();
|
||||
try {
|
||||
ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder);
|
||||
if (order == null || order.getId() == null) {
|
||||
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;
|
||||
}
|
||||
// 复用现有导出(PLM 来源会走 generateRoute3ExcelFromPlm)
|
||||
@ -139,12 +175,23 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
File generated = new File(generatedPath);
|
||||
if (!generated.exists() || generated.length() <= 0) {
|
||||
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;
|
||||
}
|
||||
// 落到固定目录,再推送;临时生成文件可删
|
||||
// 落到固定目录;有总装才钉钉推送,没有就只落盘等总装
|
||||
File saved = processPlanExcelPushService.savePlanExcel(productionOrder, generated, generated.getName());
|
||||
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 (!generated.delete()) {
|
||||
log.warn("删除临时计划表失败: {}", generated.getAbsolutePath());
|
||||
@ -152,6 +199,16 @@ public class ProcessRoutePushReceiveController extends BaseController {
|
||||
}
|
||||
} catch (Exception 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,14 +2,20 @@ package com.ruoyi.system.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@ -17,11 +23,32 @@ import java.math.BigDecimal;
|
||||
public class RatioBuildCapacityController {
|
||||
|
||||
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")
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,14 @@ public class ProcessOrderPro extends BaseEntity {
|
||||
* 是否可以下载
|
||||
*/
|
||||
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是
|
||||
*/
|
||||
@ -89,7 +97,7 @@ public class ProcessOrderPro extends BaseEntity {
|
||||
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;
|
||||
/**
|
||||
@ -129,7 +137,7 @@ public class ProcessOrderPro extends BaseEntity {
|
||||
*/
|
||||
private Date drawingTime;
|
||||
/**
|
||||
* 出图人
|
||||
* 设计人(字典 plan_proer,多人逗号分隔)
|
||||
*/
|
||||
private String drawingBy;
|
||||
/**
|
||||
|
||||
@ -7,6 +7,8 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("ratio_accessory")
|
||||
@ -17,16 +19,24 @@ public class RatioAccessory extends BaseEntity {
|
||||
|
||||
private Long ratioConfigId;
|
||||
|
||||
/** 减速机型号ID,空表示该速比通用件 */
|
||||
private Long ratioModelId;
|
||||
|
||||
private String materialCode;
|
||||
|
||||
private String materialName;
|
||||
|
||||
private String danzhong;
|
||||
|
||||
/** 材料 */
|
||||
private String canzhi;
|
||||
|
||||
/** 配件类别:通用件 / 轴套 / 行星架 / 法兰 */
|
||||
private String partCategory;
|
||||
|
||||
/** 单台用量 */
|
||||
private BigDecimal bomQty;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,11 @@ public class RatioConfig extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private BigDecimal ratioValue;
|
||||
/** 机型:KP08 / KP14 */
|
||||
private String seriesType;
|
||||
|
||||
/** 速比值,例如 91B */
|
||||
private String ratioValue;
|
||||
|
||||
private BigDecimal maxTorqueNm;
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@ public class SfcOperationPlanningDetail extends BaseEntity {
|
||||
*/
|
||||
private String operator;
|
||||
/**
|
||||
* 状态
|
||||
* 工序状态(金蝶 FOperStatus 原始码:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭)
|
||||
*/
|
||||
private String operStatus;
|
||||
/**
|
||||
|
||||
@ -66,4 +66,19 @@ public class SfcOperationPlanningMain extends BaseEntity {
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -81,31 +81,26 @@ public class FigureSaveBo extends BaseEntity {
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
@NotBlank(message = "文件类型不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String fileType;
|
||||
|
||||
/**
|
||||
* 出图人
|
||||
*/
|
||||
@NotBlank(message = "出图人不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String drawBy;
|
||||
|
||||
/**
|
||||
* 是否延期:0否 1是
|
||||
*/
|
||||
@NotNull(message = "是否延期:0否 1是不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Integer whetherDelay;
|
||||
|
||||
/**
|
||||
* 延期天数
|
||||
*/
|
||||
@NotNull(message = "延期天数不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long delayDay;
|
||||
|
||||
/**
|
||||
* 出图时间
|
||||
* 出图时间(项目级维护,产品不再必填)
|
||||
*/
|
||||
@NotNull(message = "出图时间不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Date drawTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
package com.ruoyi.system.domain.bo;
|
||||
|
||||
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.constant.TransConstant;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import javax.validation.constraints.*;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@ -86,6 +90,16 @@ public class ProcessOrderProBo extends BaseEntity {
|
||||
*/
|
||||
private Integer isDownload;
|
||||
|
||||
/**
|
||||
* 图纸是否已齐:0否 1是
|
||||
*/
|
||||
private Integer drawingQtyIssued;
|
||||
|
||||
/**
|
||||
* 出图模式:normal正常 / part零部件(执行出图前必填)
|
||||
*/
|
||||
private String drawingMode;
|
||||
|
||||
/**
|
||||
* 是否做工艺:0否 1是
|
||||
*/
|
||||
@ -131,14 +145,17 @@ public class ProcessOrderProBo extends BaseEntity {
|
||||
/**
|
||||
* 工艺上传更新时间
|
||||
*/
|
||||
@NotNull(message = "工艺上传更新不能为空", groups = { AddGroup.class })
|
||||
private Date routeUptime;
|
||||
/**
|
||||
* 排产计划更新时间
|
||||
*/
|
||||
@NotNull(message = "排产计划更新不能为空", groups = { AddGroup.class })
|
||||
private Date planUptime;
|
||||
/**
|
||||
* BOM/物料更新时间
|
||||
*/
|
||||
@NotNull(message = "BOM/物料更新不能为空", groups = { AddGroup.class })
|
||||
private Date bomUptime;
|
||||
/**
|
||||
* bom数量
|
||||
@ -153,13 +170,64 @@ public class ProcessOrderProBo extends BaseEntity {
|
||||
*/
|
||||
private Long routeCount;
|
||||
/**
|
||||
* 出图时间
|
||||
* 出图时间(项目整体;新增必填)
|
||||
*/
|
||||
@NotNull(message = "出图时间不能为空", groups = { AddGroup.class })
|
||||
private Date drawingTime;
|
||||
/**
|
||||
* 出图人
|
||||
* 设计人(字典 plan_proer,多人逗号分隔)。
|
||||
* 前端表单绑定的是 drawer 数组,提交时再折成本字段;校验看 drawingBy。
|
||||
*/
|
||||
@NotBlank(message = "设计人不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
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
|
||||
*/
|
||||
|
||||
@ -85,5 +85,13 @@ public class SfcOperationPlanningMainBo extends BaseEntity {
|
||||
|
||||
private String workCenterName;
|
||||
|
||||
/** 单据状态(生产订单 PRD_MO.FDocumentStatus) */
|
||||
private String documentStatus;
|
||||
|
||||
/** 业务状态(生产订单 PRD_MO.FStatus) */
|
||||
private String billStatus;
|
||||
|
||||
/** 工序计划单据状态(SFC_OperationPlanning.FDocumentStatus) */
|
||||
private String planDocumentStatus;
|
||||
|
||||
}
|
||||
|
||||
@ -114,6 +114,10 @@ public final class PlmProcessRoutePushNormalizer {
|
||||
private int savedMaterialBomCount;
|
||||
/** 实际写入 process_route 的行数(已过滤 PLM 占位工序) */
|
||||
private int savedRouteCount;
|
||||
/** 同令号已有工艺且本次总图号不同,追加写入 */
|
||||
private boolean appended;
|
||||
/** 同令号已有工艺且本次覆盖(同总图或无法比较总图) */
|
||||
private boolean replaced;
|
||||
private boolean received = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +55,36 @@ public class OperationPlanResultDTO {
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
|
||||
@ -61,6 +61,12 @@ public class OperationPlanningDTO {
|
||||
@JsonProperty("FBillNo")
|
||||
private String fBillNo;
|
||||
|
||||
/**
|
||||
* 工序计划单据状态:A创建 B审核中 C已审核 D重新审核 E暂存
|
||||
*/
|
||||
@JsonProperty("FDocumentStatus")
|
||||
private String fDocumentStatus;
|
||||
|
||||
/**
|
||||
* 工序号
|
||||
*/
|
||||
|
||||
@ -43,10 +43,18 @@ public class ProcessDetailDTO {
|
||||
private Double ngQty;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
* 工序状态码 FOperStatus:0创建 1计划 2计划确认 3下达 4开工 5完工 6关闭
|
||||
*/
|
||||
private String operStatus;
|
||||
|
||||
/**
|
||||
* 业务展示状态(未开工/生产中/质检中/工序完成/已转出/已关闭)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 金蝶工序状态名称
|
||||
*/
|
||||
private String k3Status;
|
||||
/**
|
||||
* 转出数量
|
||||
|
||||
@ -14,5 +14,9 @@ public class AssemblyBomReceiveSummary {
|
||||
|
||||
private int insertedCount;
|
||||
|
||||
/** 同令号已有数据且本次为全量/同主产品覆盖 */
|
||||
private boolean replaced;
|
||||
|
||||
/** 同令号已有数据且本次主产品图号不同,追加写入 */
|
||||
private boolean appended;
|
||||
}
|
||||
|
||||
@ -10,7 +10,13 @@ public class RatioBuildCapacityDTO {
|
||||
|
||||
private Long ratioConfigId;
|
||||
|
||||
private BigDecimal ratioValue;
|
||||
private String seriesType;
|
||||
|
||||
private String ratioValue;
|
||||
|
||||
private Long modelId;
|
||||
|
||||
private String modelCode;
|
||||
|
||||
private BigDecimal maxTorqueNm;
|
||||
|
||||
@ -18,6 +24,9 @@ public class RatioBuildCapacityDTO {
|
||||
|
||||
private Long maxBuildQty;
|
||||
|
||||
/** 该型号没有轴套、行星架、法兰时,结果只有通用件。 */
|
||||
private Boolean onlyCommonParts;
|
||||
|
||||
private List<RatioMaterialItemDTO> items;
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,9 @@ public class RatioMaterialItemDTO {
|
||||
|
||||
private String canzhi;
|
||||
|
||||
/** Excel 中的单台数量 */
|
||||
private Double bomQty;
|
||||
|
||||
private Double requiredQty;
|
||||
|
||||
private Double onHandQty;
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
package com.ruoyi.system.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.convert.NullableDoubleConverter;
|
||||
import com.ruoyi.common.convert.NullableQuantityTextConverter;
|
||||
import lombok.Data;
|
||||
/**
|
||||
* 电气物料BOM
|
||||
* 2025-09-25
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class ElectricalMaterialBomVO {
|
||||
|
||||
/**
|
||||
@ -33,22 +35,28 @@ public class ElectricalMaterialBomVO {
|
||||
@ExcelProperty(value = "物料名称")
|
||||
private String drawingName;
|
||||
/**
|
||||
* 型号
|
||||
* 型号(部分模板表头为「规格型号」)
|
||||
*/
|
||||
@ExcelProperty(value = "型号")
|
||||
private String Model;
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 规格型号(与「型号」二选一,导入时都会写入 material)
|
||||
*/
|
||||
@ExcelProperty(value = "规格型号")
|
||||
private String specificationModel;
|
||||
|
||||
/**
|
||||
* 品牌
|
||||
*/
|
||||
@ExcelProperty(value = "品牌")
|
||||
private String Brand;
|
||||
private String brand;
|
||||
|
||||
/**
|
||||
* 单台数量
|
||||
* 单台数量。普通数字原样入库;文本分数如 1/4、1/3 在导入时拆成分子和分母。
|
||||
*/
|
||||
@ExcelProperty(value = "单台数量", converter = NullableDoubleConverter.class)
|
||||
private Double quantity;
|
||||
@ExcelProperty(value = "单台数量", converter = NullableQuantityTextConverter.class)
|
||||
private String quantity;
|
||||
/**
|
||||
* 批次数量
|
||||
*/
|
||||
|
||||
@ -3,8 +3,7 @@ package com.ruoyi.system.domain.vo;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.convert.ExcelDictConvert;
|
||||
import com.ruoyi.common.utils.ProjectStatusLock;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import lombok.Data;
|
||||
|
||||
@ -43,8 +42,7 @@ public class ProcessOrderProExportVo {
|
||||
@ExcelProperty(value = "生产令号")
|
||||
private String productionOrderNo;
|
||||
|
||||
@ExcelProperty(value = "项目状态", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "process_status")
|
||||
@ExcelProperty(value = "项目状态")
|
||||
private String bomStatus;
|
||||
|
||||
@ExcelProperty(value = "项目名称")
|
||||
@ -101,7 +99,7 @@ public class ProcessOrderProExportVo {
|
||||
return row;
|
||||
}
|
||||
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.setSource(formatSource(vo.getSource()));
|
||||
row.setWhetherDelayLabel(Integer.valueOf(1).equals(vo.getWhetherDelay()) ? "延期" : "正常");
|
||||
|
||||
@ -52,6 +52,18 @@ public class ProcessOrderProVo {
|
||||
@ExcelProperty(value = "是否可以下载")
|
||||
private Integer isDownload;
|
||||
|
||||
/**
|
||||
* 图纸是否已齐:0否 1是
|
||||
*/
|
||||
@ExcelProperty(value = "图纸已齐")
|
||||
private Integer drawingQtyIssued;
|
||||
|
||||
/**
|
||||
* 出图模式:normal正常 / part零部件(执行出图前必填)
|
||||
*/
|
||||
@ExcelProperty(value = "出图模式")
|
||||
private String drawingMode;
|
||||
|
||||
/**
|
||||
* 是否做工艺:0否 1是
|
||||
*/
|
||||
@ -151,8 +163,10 @@ public class ProcessOrderProVo {
|
||||
*/
|
||||
private Long routeCount;
|
||||
/**
|
||||
* 出图人
|
||||
* 设计人(字典 plan_proer,多人逗号分隔)
|
||||
*/
|
||||
@ExcelProperty(value = "设计人", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "plan_proer")
|
||||
private String drawingBy;
|
||||
/**
|
||||
* 出图时间
|
||||
@ -210,6 +224,18 @@ public class ProcessOrderProVo {
|
||||
@ExcelIgnore
|
||||
private Boolean productionComplete;
|
||||
|
||||
/**
|
||||
* 设计出图进度(非库字段):进行中 / 延期 / 设计出图完成
|
||||
*/
|
||||
@ExcelIgnore
|
||||
private String drawingProgress;
|
||||
|
||||
/**
|
||||
* 等效 A4 合计(非库字段,来自图纸工作量)
|
||||
*/
|
||||
@ExcelProperty(value = "等效A4")
|
||||
private Integer equivalentA4Total;
|
||||
|
||||
/**
|
||||
* 列表/导出:金蝶订单状态摘要(非库字段)
|
||||
*/
|
||||
|
||||
@ -86,5 +86,22 @@ public class SfcOperationPlanningMainVo {
|
||||
@ExcelProperty(value = "计划结束时间")
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ public class PlmProductionOrderVo {
|
||||
* 2 工艺待审核
|
||||
* 3 已推送BOM
|
||||
* 4 已推送工艺
|
||||
* 5 已核算MRP
|
||||
* 6 开始生产
|
||||
* 7 生产完成
|
||||
* 8 工艺审核完成
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
package com.ruoyi.system.job;
|
||||
|
||||
import com.ruoyi.common.constant.UserConstants;
|
||||
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.service.ISfcOperationPlanningMainService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import com.xxl.job.core.context.XxlJobHelper;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -21,6 +29,7 @@ import org.springframework.stereotype.Component;
|
||||
public class SfcOperationPlanningJob {
|
||||
|
||||
private final ISfcOperationPlanningMainService iSfcOperationPlanningMainService;
|
||||
private final ISysUserService sysUserService;
|
||||
|
||||
/**
|
||||
* 定时同步K3工序计划数据
|
||||
@ -30,8 +39,8 @@ public class SfcOperationPlanningJob {
|
||||
public void syncOperationPlanningJobHandler() throws Exception {
|
||||
XxlJobHelper.log("XXL-JOB, 开始同步K3工序计划数据...");
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
loginAsAdmin();
|
||||
Boolean result = iSfcOperationPlanningMainService.syncOperationPlanning();
|
||||
if (result) {
|
||||
XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据成功!耗时: {}ms", (System.currentTimeMillis() - startTime));
|
||||
@ -44,9 +53,29 @@ public class SfcOperationPlanningJob {
|
||||
log.error("同步K3工序计划数据异常", e);
|
||||
XxlJobHelper.log("XXL-JOB, 同步K3工序计划数据异常: {}", 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,用「信息发送」机器人私发给配置人员。
|
||||
* JobHandler:pushOperationPlanningProgress
|
||||
|
||||
@ -27,6 +27,10 @@ public class FileToZip {
|
||||
|
||||
private static void zipFileRecursive(File fileToZip, String fileName, ZipOutputStream zos) throws IOException {
|
||||
if (fileToZip.isHidden()) return;
|
||||
// 跳过盖章残留的 *_temp.pdf,避免解压后出现双份
|
||||
if (fileToZip.isFile() && fileToZip.getName().toLowerCase().endsWith("_temp.pdf")) {
|
||||
return;
|
||||
}
|
||||
if (fileToZip.isDirectory()) {
|
||||
if (!fileName.endsWith("/")) fileName += "/";
|
||||
zos.putNextEntry(new ZipEntry(fileName));
|
||||
|
||||
@ -14,7 +14,7 @@ public class CodeConverter {
|
||||
//nameToCodeMap.put("焊接工段", "WC000003");
|
||||
nameToCodeMap.put("装一工段", "WC000004"); // 装配车间
|
||||
nameToCodeMap.put("装二工段", "WC000005");
|
||||
nameToCodeMap.put("电钳工段", "WC000006");
|
||||
nameToCodeMap.put("电钳工段", "WC000023");
|
||||
nameToCodeMap.put("机加委外", "WC000008");
|
||||
nameToCodeMap.put("下料工段", "WC000010"); // 铆焊车间
|
||||
nameToCodeMap.put("焊接工段", "WC000011");
|
||||
|
||||
@ -16,6 +16,7 @@ import com.ruoyi.system.domain.BomDetails;
|
||||
import com.ruoyi.system.domain.PartCost;
|
||||
import com.ruoyi.system.domain.dto.*;
|
||||
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.Model;
|
||||
import org.aspectj.bridge.MessageUtil;
|
||||
@ -854,7 +855,7 @@ public class JdUtil {
|
||||
JsonArray filterString = new JsonArray();
|
||||
JsonObject filterObject = new JsonObject();
|
||||
filterObject.addProperty("FieldName", "FMaterialId.FNumber");
|
||||
filterObject.addProperty("Compare", "="); // 改为等号运算符
|
||||
filterObject.addProperty("Compare", "=");
|
||||
filterObject.addProperty("Value", materialCode);
|
||||
filterObject.addProperty("Left", "");
|
||||
filterObject.addProperty("Right", "");
|
||||
@ -1125,20 +1126,21 @@ public class JdUtil {
|
||||
JsonArray filterString = new JsonArray();
|
||||
JsonObject filterObject = new JsonObject();
|
||||
filterObject.addProperty("FieldName", "FMaterialId.FNumber");
|
||||
filterObject.addProperty("Compare", "="); // 改为等号运算符
|
||||
filterObject.addProperty("Compare", "=");
|
||||
filterObject.addProperty("Value", materialCode);
|
||||
filterObject.addProperty("Left", "");
|
||||
filterObject.addProperty("Right", "");
|
||||
filterObject.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号
|
||||
filterString.add(filterObject);
|
||||
JsonObject filterObject1 = new JsonObject();
|
||||
filterObject1.addProperty("FieldName", "FMRPCloseStatus");
|
||||
filterObject1.addProperty("Compare", "105"); // 改为等号运算符
|
||||
filterObject1.addProperty("Value", "A");
|
||||
filterObject1.addProperty("Left", "");
|
||||
filterObject1.addProperty("Right", "");
|
||||
filterObject1.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号
|
||||
filterString.add(filterObject1);
|
||||
// 整单未关闭
|
||||
JsonObject filterObjectClose = new JsonObject();
|
||||
filterObjectClose.addProperty("FieldName", "FCloseStatus");
|
||||
filterObjectClose.addProperty("Compare", "105");
|
||||
filterObjectClose.addProperty("Value", "A");
|
||||
filterObjectClose.addProperty("Left", "");
|
||||
filterObjectClose.addProperty("Right", "");
|
||||
filterObjectClose.addProperty("Logic", 0);
|
||||
filterString.add(filterObjectClose);
|
||||
json.add("FilterString", filterString);
|
||||
json.addProperty("OrderString", "");
|
||||
json.addProperty("TopRowCount", 0);
|
||||
@ -1194,14 +1196,15 @@ public class JdUtil {
|
||||
filterObject.addProperty("Right", "");
|
||||
filterObject.addProperty("Logic", 0);
|
||||
filterString.add(filterObject);
|
||||
JsonObject filterObject1 = new JsonObject();
|
||||
filterObject1.addProperty("FieldName", "FMRPCloseStatus");
|
||||
filterObject1.addProperty("Compare", "105");
|
||||
filterObject1.addProperty("Value", "A");
|
||||
filterObject1.addProperty("Left", "");
|
||||
filterObject1.addProperty("Right", "");
|
||||
filterObject1.addProperty("Logic", 0);
|
||||
filterString.add(filterObject1);
|
||||
// 整单未关闭
|
||||
JsonObject filterObjectClose = new JsonObject();
|
||||
filterObjectClose.addProperty("FieldName", "FCloseStatus");
|
||||
filterObjectClose.addProperty("Compare", "105");
|
||||
filterObjectClose.addProperty("Value", "A");
|
||||
filterObjectClose.addProperty("Left", "");
|
||||
filterObjectClose.addProperty("Right", "");
|
||||
filterObjectClose.addProperty("Logic", 0);
|
||||
filterString.add(filterObjectClose);
|
||||
JsonObject filterObject2 = new JsonObject();
|
||||
filterObject2.addProperty("FieldName", "F_UCHN_Text2");
|
||||
filterObject2.addProperty("Compare", "83");
|
||||
@ -4109,7 +4112,8 @@ public class JdUtil {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("FormId", "SFC_OperationPlanning");
|
||||
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," +
|
||||
"FProcessId.FName,F_HBYT_ZYRY,FOperStatus,FSeqPlanStartTime,FSeqPlanFinishTime,FOperPlanStartTime,FOperPlanFinishTime," +
|
||||
"FOptCtrlCodeId.FName,FKeyOper,FActivity1Name,FOperDescription,FPlanStartTime,FPlanFinishTime,F_HBYT_SB");
|
||||
@ -4237,6 +4241,14 @@ public class JdUtil {
|
||||
.filter(item -> item.getFid() != null)
|
||||
.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<>();
|
||||
|
||||
for (Map.Entry<Long, List<OperationPlanningDTO>> entry : groupedMap.entrySet()) {
|
||||
@ -4255,6 +4267,19 @@ public class JdUtil {
|
||||
resultDTO.setPlanStartTime(firstItem.getFPlanStartTime());
|
||||
resultDTO.setPlanFinishTime(firstItem.getFPlanFinishTime());
|
||||
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<>();
|
||||
for (OperationPlanningDTO item : items) {
|
||||
@ -4284,26 +4309,9 @@ public class JdUtil {
|
||||
detail.setK3DetailId(item.getFSubEntityFDetailId());
|
||||
detail.setEntityEntryId(item.getFEntityFEntryId());
|
||||
|
||||
// 状态映射:K3里4通常表示已完工
|
||||
String statusStr = item.getFOperStatus();
|
||||
if ("0".equals(statusStr)) {
|
||||
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);
|
||||
}
|
||||
// 入库存原始 FOperStatus 码;展示名由 Assembler 统一映射
|
||||
detail.setOperStatus(item.getFOperStatus());
|
||||
OperationPlanningResultAssembler.enrichProcessDetailStatus(detail);
|
||||
|
||||
processList.add(detail);
|
||||
}
|
||||
@ -4318,6 +4326,69 @@ public class JdUtil {
|
||||
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 阶段性产品报表 · 金蝶查询 =====================
|
||||
|
||||
@ -4555,7 +4626,7 @@ public class JdUtil {
|
||||
"FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName");
|
||||
JsonArray filterString = new JsonArray();
|
||||
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));
|
||||
json.add("FilterString", filterString);
|
||||
nd3BillQueryDefaults(json);
|
||||
@ -4573,7 +4644,7 @@ public class JdUtil {
|
||||
"FBillNo,F_UCHN_Text2,FDate,FDeliveryDate,FQty,FRemainStockINQty,FMaterialId.FNumber,FMaterialName");
|
||||
JsonArray filterString = new JsonArray();
|
||||
filterString.add(nd3Filter("FMaterialId.FNumber", "=", materialCode));
|
||||
filterString.add(nd3Filter("FMRPCloseStatus", "105", "A"));
|
||||
filterString.add(nd3Filter("FCloseStatus", "105", "A"));
|
||||
json.add("FilterString", filterString);
|
||||
nd3BillQueryDefaults(json);
|
||||
return nd3ParseList(client, json, PurchaseOrderDTO.class, "PUR_PurchaseOrder", materialCode, null);
|
||||
|
||||
@ -131,6 +131,14 @@ public class PDFGenerator {
|
||||
}
|
||||
|
||||
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())
|
||||
? new String[] {
|
||||
"C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf"
|
||||
@ -380,6 +388,14 @@ public class PDFGenerator {
|
||||
if (generatedPdfFiles.isEmpty()) {
|
||||
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文件
|
||||
String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip";
|
||||
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文件
|
||||
*
|
||||
|
||||
@ -8,10 +8,20 @@ import com.ruoyi.system.domain.vo.DrawingWorkloadVo;
|
||||
public interface IDrawingWorkloadService {
|
||||
|
||||
/**
|
||||
* 从 FTP 下载「{令号}图纸数量统计表.xlsx」,解析 Sheet1/Sheet2 后入库(按项目覆盖)。
|
||||
* 从该令号对应目录下载「图纸数量统计表」并入库。目录中没有该表时工作量记为 0,不再用其它文件凑数。
|
||||
*/
|
||||
DrawingWorkloadVo syncFromFtp(Long orderProId);
|
||||
|
||||
/**
|
||||
* 本机对应令号目录已有图纸数量统计表时解析入库;没有文件则返回 null。
|
||||
*/
|
||||
DrawingWorkloadVo syncFromLocalIfExists(Long orderProId);
|
||||
|
||||
/**
|
||||
* 批量从 FTP 读取图纸工作量。不因单条失败中断。
|
||||
*/
|
||||
java.util.Map<String, Object> syncFromFtpBatch(java.util.Collection<Long> orderProIds);
|
||||
|
||||
/**
|
||||
* 查询已入库的图纸工作量(含明细)。
|
||||
*/
|
||||
|
||||
@ -16,12 +16,18 @@ public interface IPlmPdfZipPushService {
|
||||
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);
|
||||
|
||||
/**
|
||||
* 同上;可用本次推送明细推断主产品型号/名称/数量(优先 .00 总装图)。
|
||||
*/
|
||||
Long ensureProcessOrderProForPlmPdf(String productionOrderNo, List<Map<String, String>> savedItems);
|
||||
|
||||
/**
|
||||
* 清理总令号目录中与子令号重复的图纸(保留子令号侧):
|
||||
* <ul>
|
||||
|
||||
@ -9,7 +9,8 @@ import com.ruoyi.system.domain.bo.plm.PlmProcessRoutePushNormalizer;
|
||||
public interface IPlmProcessRoutePushService {
|
||||
|
||||
/**
|
||||
* 将 PLM 推送的 BOM、材料 BOM、工艺路线写入本地库(同令号先删后插)。
|
||||
* 将 PLM 推送的 BOM、材料 BOM、工艺路线写入本地库。
|
||||
* 已审核且总图号(物料编码以 .00 结尾)相同则拒绝;总图号不同则追加;未审核且相同则覆盖该总图。
|
||||
*/
|
||||
PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary persist(PlmProcessRoutePushBo body);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@ -52,6 +53,11 @@ public interface IProcessOrderProService {
|
||||
*/
|
||||
Boolean updateByBo(ProcessOrderProBo bo);
|
||||
|
||||
/**
|
||||
* 批量设置项目状态(不校验设计人等整单字段)
|
||||
*/
|
||||
Boolean updateBomStatus(List<Long> ids, Long bomStatus);
|
||||
|
||||
/**
|
||||
* 清空项目关联数据(保留项目本身)
|
||||
*/
|
||||
@ -72,6 +78,11 @@ public interface IProcessOrderProService {
|
||||
|
||||
void addProduct( RigidChainModelDTO orderPro);
|
||||
|
||||
/**
|
||||
* 设置项目出图模式(normal / part)。零部件模式下若已有产品且图纸路径不一致则拒绝。
|
||||
*/
|
||||
Boolean updateDrawingMode(Long id, String drawingMode);
|
||||
|
||||
String executDrawing(ProcessOrderProBo orderPro);
|
||||
|
||||
/**
|
||||
@ -81,6 +92,16 @@ public interface IProcessOrderProService {
|
||||
*/
|
||||
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);
|
||||
|
||||
List<OverdueProjectVo> getOverdueProjects();
|
||||
@ -124,4 +145,9 @@ public interface IProcessOrderProService {
|
||||
* 刷新并计算项目延期状态
|
||||
*/
|
||||
void refreshDelayStatus();
|
||||
|
||||
/**
|
||||
* 临时导入:按生产令号更新出图时间、项目完成时间、工艺完成时间、计划排产时间、BOM/物料更新、设计人
|
||||
*/
|
||||
Map<String, Object> importDrawingByExcel(MultipartFile file);
|
||||
}
|
||||
|
||||
@ -58,16 +58,19 @@ public interface IProcessRouteService {
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
Boolean updateOrInsertRoute(List<ProcessRouteVo> bomDetailsVos);
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
*/
|
||||
|
||||
boolean saveData(List<ProcessRouteVo> routeVoList, List<ProductionOrderVo> productionOrderVos);
|
||||
|
||||
/**
|
||||
* 查询组焊件集合
|
||||
*/
|
||||
|
||||
List<ProcessRoute> pushGroupWeldments(ProcessRoute processRoute);
|
||||
|
||||
/**
|
||||
* 获取所有的项目令号
|
||||
*/
|
||||
@ -82,26 +85,38 @@ public interface IProcessRouteService {
|
||||
* 查询工艺工序集合
|
||||
*/
|
||||
List<ProcessRouteXuDTO> getProcessRoute(String rooteProdet);
|
||||
|
||||
/**
|
||||
* 推送工艺路线
|
||||
*/
|
||||
|
||||
R<ProcessRoutePushResultDTO> pushRouteBom(String rooteProdet, String groupName);
|
||||
|
||||
/**
|
||||
* 按物料推送工艺路线(不改项目整单状态,不影响全量推送)。
|
||||
*
|
||||
* @param materialCodes 本项目下要推送的物料编码,不可空
|
||||
*/
|
||||
R<ProcessRoutePushResultDTO> pushRouteBomByMaterial(String rooteProdet, String groupName, Collection<String> materialCodes);
|
||||
|
||||
/**
|
||||
* 获取所有的项目令号
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 获取所有项目
|
||||
*/
|
||||
@ -122,16 +137,20 @@ public interface IProcessRouteService {
|
||||
List<ProcessRouteSelectDTO> getSelectProcessRoute(String materilCode);
|
||||
|
||||
List<ProcessRouteSelectDTO> getSelectStandProcessRoute(String materilCode);
|
||||
|
||||
//根据项目令号删除 材料bom 总装bom 工艺路线
|
||||
R<Void> delByProjectCode(String productionOrderNo);
|
||||
|
||||
List<ProcessRouteJdDTO> getProcessRouteList(String materialCode, String materialName, String productionOrderNo);
|
||||
|
||||
//获取物料的非委外的工作时长
|
||||
Double getFbWorkTime(BomDetails material);
|
||||
|
||||
Double getFaWorkTime(BomDetails material);
|
||||
|
||||
//根据令号和物料编码 查询工艺路线
|
||||
List<ProcessRoute> getProcessRoutesByOrder(String productionOrderNo, String materialCode);
|
||||
|
||||
//根据物料编码查询工艺路线
|
||||
List<ProcessRoute> getSingleWeght(String materialCode);
|
||||
|
||||
@ -142,6 +161,7 @@ public interface IProcessRouteService {
|
||||
List<MaterialBom> getProcessMaterialListAll(String routeDescription);
|
||||
|
||||
List<JDMaterialAndRoute> getProcessRouteGD(List<ProcessRouteGetDTO> list, String rooteProdet);
|
||||
|
||||
//获取物料首个工序的工作中心
|
||||
String getRouteCode(String materialCode, String code);
|
||||
|
||||
|
||||
@ -99,7 +99,8 @@ public interface IProductionOrderService {
|
||||
Map<String, Double> resolveSingleWeightByMaterialCodes(Collection<String> materialCodes);
|
||||
|
||||
/**
|
||||
* 接收 PLM 总装配 BOM JSON,按生产令号全量替换入库。
|
||||
* 接收 PLM 总装配 BOM JSON。
|
||||
* 已审核且主产品图号相同则拒绝;主产品图号不同则追加;未审核且相同则覆盖该主产品。
|
||||
*/
|
||||
AssemblyBomReceiveSummary receiveAssemblyBom(List<AssemblyBomReceiveItemDTO> items);
|
||||
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.common.config.DingTalkProperties;
|
||||
import com.ruoyi.common.dingding.robot.RobotGroupService;
|
||||
import com.ruoyi.common.utils.ProjectStatusLock;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.ProcessOrderPro;
|
||||
import com.ruoyi.system.domain.KingdeePrdMo;
|
||||
@ -202,6 +203,7 @@ public class KingdeeOrderReceiveService {
|
||||
|
||||
/**
|
||||
* 金蝶推送入库后,回写项目令号:有生产/采购明细则对应标志置 1(已建)。
|
||||
* 只有本次含生产订单才把项目状态改为开始生产;仅采购订单不改状态。已生产完成的不回退。
|
||||
*/
|
||||
private void markProjectKingdeeOrderFlags(String productionOrderNo, int moCount, int poCount) {
|
||||
if (moCount <= 0 && poCount <= 0) {
|
||||
@ -209,7 +211,7 @@ public class KingdeeOrderReceiveService {
|
||||
}
|
||||
ProcessOrderPro project = resolveProjectByProductionOrderNo(productionOrderNo);
|
||||
if (project == null) {
|
||||
log.warn("金蝶订单接收:未找到生产令号对应项目,跳过标志更新。推送令号={},请确认 process_order_pro.production_order_no 与金蝶 F_HBYT_SCLH 一致或为父令号",
|
||||
log.warn("金蝶订单接收:未找到生产令号对应项目,跳过标志与状态更新。推送令号={},请确认 process_order_pro.production_order_no 与金蝶令号完全一致",
|
||||
productionOrderNo);
|
||||
return;
|
||||
}
|
||||
@ -221,42 +223,40 @@ public class KingdeeOrderReceiveService {
|
||||
if (poCount > 0) {
|
||||
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);
|
||||
if (rows <= 0) {
|
||||
log.warn("金蝶订单接收:更新项目标志影响行数为 0,项目 id={},令号={}",
|
||||
project.getId(), project.getProductionOrderNo());
|
||||
return;
|
||||
}
|
||||
if (!productionOrderNo.equals(project.getProductionOrderNo())) {
|
||||
log.info("金蝶订单接收:推送令号 {} 已回写至父项目 {}(id={})",
|
||||
productionOrderNo, project.getProductionOrderNo(), project.getId());
|
||||
log.info("金蝶订单接收:已更新项目 {}(id={})金蝶订单标志,生产 {} 条,采购 {} 条",
|
||||
productionOrderNo, project.getId(), moCount, poCount);
|
||||
if (alreadyDone) {
|
||||
log.info("金蝶订单接收:项目 {} 已是生产完成,不回退开始生产", project.getProductionOrderNo());
|
||||
} else if (startProduction) {
|
||||
log.info("金蝶订单接收:项目 {} 含生产订单,状态已更新为开始生产", project.getProductionOrderNo());
|
||||
} else {
|
||||
log.info("金蝶订单接收:已更新项目 {} 金蝶订单标志,生产 {} 条,采购 {} 条",
|
||||
productionOrderNo, moCount, poCount);
|
||||
log.info("金蝶订单接收:项目 {} 仅采购订单,不更新为开始生产", project.getProductionOrderNo());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 先精确匹配;若无则取「项目令号为金蝶令号前缀」的最长匹配(如 FB-26-038-FW-01 → FB-26-038-FW)。
|
||||
* 仅按生产令号精确匹配项目,找不到则不更新。
|
||||
*/
|
||||
private ProcessOrderPro resolveProjectByProductionOrderNo(String productionOrderNo) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
return null;
|
||||
}
|
||||
String key = productionOrderNo.trim();
|
||||
ProcessOrderPro exact = processOrderProMapper.selectOne(
|
||||
return processOrderProMapper.selectOne(
|
||||
Wrappers.<ProcessOrderPro>lambdaQuery()
|
||||
.eq(ProcessOrderPro::getProductionOrderNo, key)
|
||||
.eq(ProcessOrderPro::getProductionOrderNo, productionOrderNo.trim())
|
||||
.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) {
|
||||
@ -275,13 +275,26 @@ public class KingdeeOrderReceiveService {
|
||||
markdown.append("> **接收时间**:").append(sdf.format(receiveTime)).append("\n\n");
|
||||
markdown.append("> **项目数量**:").append(summaries.size()).append(" 个\n\n");
|
||||
markdown.append("---\n\n");
|
||||
boolean hasProductionOrder = false;
|
||||
boolean hasPurchaseOnly = false;
|
||||
for (OrderReceiveSummary summary : summaries) {
|
||||
markdown.append("> **生产令号**:").append(summary.productionOrderNo).append("\n\n");
|
||||
markdown.append("> 生产订单 **").append(summary.moCount).append("** 条 · ");
|
||||
markdown.append("采购订单 **").append(summary.poCount).append("** 条\n\n");
|
||||
if (summary.moCount > 0) {
|
||||
hasProductionOrder = true;
|
||||
} else {
|
||||
hasPurchaseOnly = true;
|
||||
}
|
||||
}
|
||||
markdown.append("---\n\n");
|
||||
markdown.append("金蝶生产、采购订单已审核并下达,请相关人员关注。");
|
||||
if (hasProductionOrder && hasPurchaseOnly) {
|
||||
markdown.append("含生产订单的项目状态已更新为开始生产;仅采购订单的项目状态不变。");
|
||||
} else if (hasProductionOrder) {
|
||||
markdown.append("金蝶生产订单已审核并下达,项目状态已更新为开始生产;请相关人员关注。");
|
||||
} else {
|
||||
markdown.append("金蝶采购订单已审核并下达;本次无生产订单,项目状态未改为开始生产。");
|
||||
}
|
||||
|
||||
robotGroupService.sendMarkdown(conversationId, "金蝶订单接收通知", markdown.toString());
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -7,10 +7,13 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
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.KingdeeWorkCenterDataBo;
|
||||
import com.ruoyi.system.domain.dto.JjSubcontractTransferDetailDTO;
|
||||
import com.ruoyi.system.domain.dto.SubcontractOperationTransferRowDTO;
|
||||
import com.ruoyi.system.domain.vo.ImMaterialVo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@ -1222,5 +1225,238 @@ public class MssqlQueryService {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import com.ruoyi.common.config.DingTalkProperties;
|
||||
import com.ruoyi.common.dingding.media.MediaService;
|
||||
import com.ruoyi.common.dingding.robot.RobotGroupService;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -19,7 +20,7 @@ import java.nio.file.StandardCopyOption;
|
||||
/**
|
||||
* 工艺计划表:落盘到服务器固定目录,并按 dingtalk.plan-user-id 推送钉钉文件。
|
||||
* <p>
|
||||
* 手动上传计划表、PLM 推送工艺后导出,共用同一套目录与接收人。
|
||||
* 手动上传计划表、PLM 总装与工艺到齐后导出,共用同一套目录与接收人。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -61,19 +62,29 @@ public class ProcessPlanExcelPushService {
|
||||
/**
|
||||
* 按配置 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) {
|
||||
log.warn("推送计划表跳过:文件不存在或为空 file={}", file);
|
||||
return;
|
||||
result.setSkipped(true);
|
||||
result.setSkipReason("文件不存在或为空");
|
||||
result.setErrorMsg(result.getSkipReason());
|
||||
return result;
|
||||
}
|
||||
String planUserIds = dingTalkProperties.getPlanUserId();
|
||||
if (StringUtils.isBlank(planUserIds)) {
|
||||
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();
|
||||
result.setFileName(name);
|
||||
try {
|
||||
String mediaId = mediaService.uploadMedia("file", file.getAbsolutePath());
|
||||
result.setMediaId(mediaId);
|
||||
for (String userId : planUserIds.split(",")) {
|
||||
String cleanUserId = userId == null ? "" : userId.trim();
|
||||
if (StringUtils.isBlank(cleanUserId)) {
|
||||
@ -81,14 +92,92 @@ public class ProcessPlanExcelPushService {
|
||||
}
|
||||
try {
|
||||
robotGroupService.sendFileToUser(cleanUserId, mediaId, name);
|
||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||
log.info("工艺计划表已发送给用户: {} file={}", cleanUserId, name);
|
||||
} catch (Exception e) {
|
||||
result.setFailCount(result.getFailCount() + 1);
|
||||
result.setErrorMsg(appendError(result.getErrorMsg(), cleanUserId + ": " + e.getMessage()));
|
||||
log.error("发送工艺计划表给用户 {} 失败", cleanUserId, e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.setFailCount(result.getFailCount() + 1);
|
||||
result.setErrorMsg(appendError(result.getErrorMsg(), e.getMessage()));
|
||||
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() {
|
||||
|
||||
@ -3,9 +3,12 @@ package com.ruoyi.system.service;
|
||||
import com.ruoyi.common.config.DingTalkProperties;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.dingding.util.DingUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.controller.KingdeeWorkCenterDataController;
|
||||
import com.ruoyi.system.domain.WorkAlertGroupPushConfig;
|
||||
import com.ruoyi.system.domain.WorkAlertPushConfig;
|
||||
import com.ruoyi.system.mapper.WorkAlertGroupPushConfigMapper;
|
||||
import com.ruoyi.system.mapper.WorkAlertPushConfigMapper;
|
||||
import com.ruoyi.system.service.support.WorkAlertPushDeliveryHolder;
|
||||
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 final WorkAlertPushConfigMapper configMapper;
|
||||
private final WorkAlertGroupPushConfigMapper groupConfigMapper;
|
||||
private final KingdeeWorkCenterDataController kingdeeWorkCenterDataController;
|
||||
private final DingTalkProperties dingTalkProperties;
|
||||
|
||||
@ -48,9 +52,7 @@ public class WorkAlertPushScheduleService {
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
List<WorkAlertPushConfig> configs = configMapper.selectList(null);
|
||||
if (configs == null || configs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (configs != null) {
|
||||
for (WorkAlertPushConfig config : configs) {
|
||||
if (!isEnabled(config)) {
|
||||
continue;
|
||||
@ -68,6 +70,79 @@ public class WorkAlertPushScheduleService {
|
||||
}
|
||||
}
|
||||
}
|
||||
runDueGroupPushes(nowTime, today);
|
||||
}
|
||||
|
||||
private void runDueGroupPushes(String nowTime, LocalDate today) {
|
||||
List<WorkAlertGroupPushConfig> configs = groupConfigMapper.selectList(null);
|
||||
if (configs == null || configs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (WorkAlertGroupPushConfig config : configs) {
|
||||
if (!isEnabled(config.getEnabled())) {
|
||||
continue;
|
||||
}
|
||||
if (!nowTime.equals(normalizePushTime(config.getPushTime()))) {
|
||||
continue;
|
||||
}
|
||||
if (alreadyPushedToday(config.getLastPushDate(), today)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
executeGroupSchedule(config, false);
|
||||
} catch (Exception 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) {
|
||||
if (config == null) {
|
||||
@ -114,6 +189,39 @@ public class WorkAlertPushScheduleService {
|
||||
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) {
|
||||
switch (pushCode) {
|
||||
case "getKuCun":
|
||||
@ -222,8 +330,23 @@ public class WorkAlertPushScheduleService {
|
||||
.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) {
|
||||
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) {
|
||||
|
||||
@ -71,12 +71,74 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
}
|
||||
String productionCode = orderPro.getProductionOrderNo().trim();
|
||||
String localFile = downloadDrawingCountExcel(productionCode);
|
||||
if (StringUtils.isBlank(localFile)) {
|
||||
log.info("令号 {} 对应目录未找到图纸数量统计表,工作量记为0", productionCode);
|
||||
return saveEmptyWorkload(orderPro);
|
||||
}
|
||||
|
||||
ParsedExcel parsed = parseExcel(localFile);
|
||||
DrawingWorkload saved = saveWorkload(orderPro, parsed);
|
||||
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
|
||||
public DrawingWorkloadVo getByOrderProId(Long orderProId) {
|
||||
DrawingWorkload row = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery()
|
||||
@ -92,39 +154,63 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
String year = ProductionOrderNoUtils.extractYear(productionCode);
|
||||
String preferredName = productionCode + "图纸数量统计表.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);
|
||||
log.info("图纸数量统计表 FTP 下载路径: {} (basePath={})", ftpPath, drawingBasePath);
|
||||
|
||||
R<String> result = tryDownloadExcel(ftpPath, preferredName, preferredName);
|
||||
if (R.isError(result)) {
|
||||
result = tryDownloadExcel(ftpPath, fallbackName, preferredName);
|
||||
StringBuilder lastMsg = new StringBuilder();
|
||||
if (downloadOwnedExcel(ftpPath, preferredName, fallbackName, preferred, fallback, lastMsg)) {
|
||||
return preferred.getAbsolutePath();
|
||||
}
|
||||
if (R.isError(result)) {
|
||||
String noDrivePath = FtpUtil.buildDrawingExcelRemoteDir("", year, productionCode);
|
||||
if (!noDrivePath.equals(ftpPath)) {
|
||||
log.info("尝试无盘符 FTP 路径: {}", noDrivePath);
|
||||
result = tryDownloadExcel(noDrivePath, preferredName, preferredName);
|
||||
if (R.isError(result)) {
|
||||
result = tryDownloadExcel(noDrivePath, fallbackName, preferredName);
|
||||
if (downloadOwnedExcel(noDrivePath, preferredName, fallbackName, preferred, fallback, lastMsg)) {
|
||||
return preferred.getAbsolutePath();
|
||||
}
|
||||
if (R.isSuccess(result)) {
|
||||
ftpPath = noDrivePath;
|
||||
}
|
||||
}
|
||||
log.info("项目 {} 对应目录未找到图纸数量统计表,FTP路径={},期望文件名={} 或 {},{}",
|
||||
productionCode, ftpPath, preferredName, fallbackName, lastMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 目录扫描可能按远程原名落盘,统一落到 preferred 本地名
|
||||
ensureLocalPreferredName(preferredName, fallbackName);
|
||||
|
||||
File file = new File(localPreferred);
|
||||
if (!file.exists() || file.length() == 0) {
|
||||
throw new ServiceException("项目 " + productionCode + " 图纸数量统计表下载失败或不存在,FTP路径="
|
||||
+ ftpPath + ",期望文件名=" + preferredName + " 或 " + fallbackName + ","
|
||||
+ (result == null ? "" : result.getMsg()));
|
||||
/**
|
||||
* 只接受本次从该令号目录真正下载到的文件,禁止把 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;
|
||||
}
|
||||
return file.getAbsolutePath();
|
||||
|
||||
Snapshot fallbackSnap = snapshot(fallback);
|
||||
result = tryDownloadExcel(ftpPath, fallbackName, preferredName);
|
||||
rememberMsg(lastMsg, result);
|
||||
if (touchedSince(preferred, preferredSnap)) {
|
||||
return true;
|
||||
}
|
||||
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) {
|
||||
@ -138,40 +224,66 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ensureLocalPreferredName(String preferredName, String fallbackName) {
|
||||
File preferred = new File(LOCAL_PATH + preferredName);
|
||||
if (preferred.exists() && preferred.length() > 0) {
|
||||
return;
|
||||
private void renameToPreferred(File source, File preferred) {
|
||||
if (preferred.exists() && !preferred.delete()) {
|
||||
throw new ServiceException("图纸数量统计表本地文件覆盖失败");
|
||||
}
|
||||
File fallback = new File(LOCAL_PATH + fallbackName);
|
||||
if (fallback.exists() && fallback.length() > 0) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
File best = matches[0];
|
||||
for (File f : matches) {
|
||||
if (f.lastModified() > best.lastModified()) {
|
||||
best = f;
|
||||
if (!source.renameTo(preferred)) {
|
||||
throw new ServiceException("图纸数量统计表本地文件重命名失败: " + source.getName());
|
||||
}
|
||||
}
|
||||
if (preferred.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
preferred.delete();
|
||||
|
||||
/**
|
||||
* 只认该令号自己的目录里的表。D:\file 是所有项目共用缓存,不能当“共享盘上有这份表”的证据。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
if (!best.getName().equals(preferredName) && !best.renameTo(preferred)) {
|
||||
throw new ServiceException("图纸数量统计表本地文件重命名失败: " + best.getName());
|
||||
for (File file : candidates) {
|
||||
if (isUsable(file)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return new Snapshot(file.lastModified(), file.length());
|
||||
}
|
||||
|
||||
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);
|
||||
DrawingWorkload row = new DrawingWorkload();
|
||||
row.setA4Count(readInt(data, col, "A4图纸数量"));
|
||||
row.setA3Count(readInt(data, col, "A3图纸数量"));
|
||||
row.setA3HalfCount(readInt(data, col, "A3+1/2图纸数量"));
|
||||
row.setA3Plus1Count(readInt(data, col, "A3+1图纸数量"));
|
||||
row.setA2Count(readInt(data, col, "A2图纸数量"));
|
||||
row.setA2HalfCount(readInt(data, col, "A2+1/2图纸数量"));
|
||||
row.setA1Count(readInt(data, col, "A1图纸数量"));
|
||||
row.setA1HalfCount(readInt(data, col, "A1+1/2图纸数量"));
|
||||
row.setA0Count(readInt(data, col, "A0图纸数量"));
|
||||
row.setA0HalfCount(readInt(data, col, "A0+1/2图纸数量"));
|
||||
row.setEquivalentA4Total(readInt(data, col, "等效A4图纸总数量"));
|
||||
row.setA4Count(readInt(data, col, "A4图纸数量", "A4图纸数", "A4数量"));
|
||||
row.setA3Count(readInt(data, col, "A3图纸数量", "A3图纸数", "A3数量"));
|
||||
row.setA3HalfCount(readInt(data, col, "A3+1/2图纸数量", "A3+1/2图纸数"));
|
||||
row.setA3Plus1Count(readInt(data, col, "A3+1图纸数量", "A3+1图纸数"));
|
||||
row.setA2Count(readInt(data, col, "A2图纸数量", "A2图纸数"));
|
||||
row.setA2HalfCount(readInt(data, col, "A2+1/2图纸数量", "A2+1/2图纸数"));
|
||||
row.setA1Count(readInt(data, col, "A1图纸数量", "A1图纸数"));
|
||||
row.setA1HalfCount(readInt(data, col, "A1+1/2图纸数量", "A1+1/2图纸数"));
|
||||
row.setA0Count(readInt(data, col, "A0图纸数量", "A0图纸数"));
|
||||
row.setA0HalfCount(readInt(data, col, "A0+1/2图纸数量", "A0+1/2图纸数"));
|
||||
row.setEquivalentA4Total(readInt(data, col, "等效A4图纸总数量", "等效A4图纸总量", "等效A4合计", "等效A4"));
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -277,6 +389,7 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
|
||||
private int findHeaderRow(Sheet sheet, String... keywords) {
|
||||
int last = Math.min(sheet.getLastRowNum(), 20);
|
||||
int fallback = -1;
|
||||
for (int r = 0; r <= last; r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) {
|
||||
@ -287,10 +400,10 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
for (int c = 0; c < lastCell; c++) {
|
||||
sb.append(cellText(row.getCell(c))).append('|');
|
||||
}
|
||||
String line = sb.toString();
|
||||
String line = sb.toString().replace(" ", "");
|
||||
boolean allMatch = true;
|
||||
for (String kw : keywords) {
|
||||
if (!line.contains(kw)) {
|
||||
if (!line.contains(kw.replace(" ", ""))) {
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
@ -298,8 +411,11 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
if (allMatch) {
|
||||
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) {
|
||||
@ -308,20 +424,44 @@ public class DrawingWorkloadServiceImpl implements IDrawingWorkloadService {
|
||||
for (int c = 0; c < last; c++) {
|
||||
String text = cellText(header.getCell(c));
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
map.put(text.trim(), c);
|
||||
String trimmed = text.trim();
|
||||
map.put(trimmed, c);
|
||||
map.put(trimmed.replace(" ", ""), c);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private Integer readInt(Row row, Map<String, Integer> col, String header) {
|
||||
Integer idx = col.get(header);
|
||||
private Integer readInt(Row row, Map<String, Integer> col, String... headers) {
|
||||
Integer idx = findColumnIndex(col, headers);
|
||||
if (idx == null) {
|
||||
return null;
|
||||
}
|
||||
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) {
|
||||
Integer idx = col.get(header);
|
||||
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) {
|
||||
Date now = new Date();
|
||||
DrawingWorkload existing = workloadMapper.selectOne(Wrappers.<DrawingWorkload>lambdaQuery()
|
||||
|
||||
@ -7,9 +7,9 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.system.constant.DrawingMode;
|
||||
import com.ruoyi.system.domain.ProcessOrderPro;
|
||||
import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
import com.ruoyi.system.service.IProcessOrderProService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.mapper.FigureSaveMapper;
|
||||
import com.ruoyi.system.service.IFigureSaveService;
|
||||
import com.ruoyi.system.service.IOrderProDrawingDelayService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -35,6 +36,7 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
|
||||
|
||||
private final FigureSaveMapper baseMapper;
|
||||
private final ProcessOrderProMapper processOrderProMapper;
|
||||
private final IOrderProDrawingDelayService orderProDrawingDelayService;
|
||||
/**
|
||||
* 查询外购件临时
|
||||
*/
|
||||
@ -87,9 +89,12 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
|
||||
public Boolean insertByBo(FigureSaveBo bo) {
|
||||
FigureSave add = BeanUtil.toBean(bo, FigureSave.class);
|
||||
validEntityBeforeSave(add);
|
||||
assertPartModeSameDrawPath(add.getPid(), add.getDrawPath(), null);
|
||||
fillUploadTime(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
orderProDrawingDelayService.refreshByProjectId(add.getPid());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
@ -101,7 +106,21 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
|
||||
public Boolean updateByBo(FigureSaveBo bo) {
|
||||
FigureSave update = BeanUtil.toBean(bo, FigureSave.class);
|
||||
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 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
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){
|
||||
//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();
|
||||
for (FigureSaveVo vo : dataList) {
|
||||
try {
|
||||
// 验证是否存在这个用户
|
||||
FigureSave figureSave = BeanUtil.toBean(vo, FigureSave.class);
|
||||
ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(figureSave.getProductionCode());
|
||||
if (processOrderPro == null) {
|
||||
throw new IllegalArgumentException("未找到生产令号:" + figureSave.getProductionCode());
|
||||
}
|
||||
figureSave.setPid(processOrderPro.getId());
|
||||
assertPartModeSameDrawPath(figureSave.getPid(), figureSave.getDrawPath(), null);
|
||||
fillUploadTime(figureSave);
|
||||
baseMapper.insert(figureSave);
|
||||
successNum++;
|
||||
successMsg.append("<br/>").append(successNum).append("、数据 ").append(vo.getFigureName()).append(" 导入成功");
|
||||
|
||||
@ -39,6 +39,7 @@ import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.runner.MaterialProperties;
|
||||
import com.ruoyi.system.service.IImMaterialService;
|
||||
import com.ruoyi.system.service.IProductionOrderService;
|
||||
import com.ruoyi.system.service.MssqlQueryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
@ -70,6 +71,7 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
private final ImMaterialMapper baseMapper;
|
||||
private final IProductionOrderService productionOrderService;
|
||||
private final MssqlQueryService mssqlQueryService;
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImMaterialController.class);
|
||||
|
||||
@Autowired
|
||||
@ -181,26 +183,31 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询物料检索列表
|
||||
* 查询物料检索列表(实时查金蝶 T_BD_MATERIAL)
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ImMaterialVo> queryPageList(ImMaterialBo bo, PageQuery pageQuery) {
|
||||
|
||||
try {
|
||||
|
||||
LambdaQueryWrapper<ImMaterial> lqw = buildQueryWrapper(bo);
|
||||
Page<ImMaterialVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
if (CollectionUtils.isEmpty(result.getRecords())) {
|
||||
return TableDataInfo.build(result); // 如果结果为空,直接返回
|
||||
int pageNum = pageQuery.getPageNum() == null || pageQuery.getPageNum() < 1 ? 1 : pageQuery.getPageNum();
|
||||
Integer rawSize = pageQuery.getPageSize();
|
||||
int pageSize;
|
||||
if (rawSize == null || rawSize <= 0 || rawSize == Integer.MAX_VALUE) {
|
||||
pageSize = 10;
|
||||
} else {
|
||||
pageSize = Math.min(rawSize, 200);
|
||||
}
|
||||
// 调用库存查询方法,优化为批量查询而非逐个查询
|
||||
Map<String, JsonObject> jsonMap = queryAllKuCun(result.getRecords());
|
||||
|
||||
// 更新库存信息
|
||||
updateRecordsWithKuCunInfo(jsonMap, result.getRecords());
|
||||
// 更新数量,更新实体类
|
||||
List<ImMaterialVo> records = result.getRecords();
|
||||
// 根据库存 in_inventory 降序排序
|
||||
MssqlQueryService.KingdeeMaterialPage kdPage =
|
||||
mssqlQueryService.queryMaterialPage(bo, pageNum, pageSize, true);
|
||||
Page<ImMaterialVo> result = new Page<>(pageNum, pageSize);
|
||||
result.setTotal(kdPage.getTotal());
|
||||
List<ImMaterialVo> records = kdPage.getRecords() != null ? kdPage.getRecords() : Collections.emptyList();
|
||||
result.setRecords(records);
|
||||
if (CollectionUtils.isEmpty(records)) {
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
Map<String, JsonObject> jsonMap = queryAllKuCun(records);
|
||||
updateRecordsWithKuCunInfo(jsonMap, records);
|
||||
records.sort(Comparator.comparing(
|
||||
r -> {
|
||||
BigDecimal value = BigDecimal.ZERO;
|
||||
@ -208,21 +215,18 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
try {
|
||||
value = new BigDecimal(r.getInInventory());
|
||||
} catch (NumberFormatException e) {
|
||||
// 如果 in_inventory 不是数字,默认为 0
|
||||
value = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
// 保留三位小数,四舍五入
|
||||
return value.setScale(3, RoundingMode.HALF_UP);
|
||||
},
|
||||
Comparator.reverseOrder()
|
||||
));
|
||||
result.setRecords(records); // 更新结果集*/
|
||||
result.setRecords(records);
|
||||
return TableDataInfo.build(result);
|
||||
} catch (Exception e) {
|
||||
// 异常处理逻辑,可根据实际情况调整
|
||||
log.error("查询页面列表异常", e);
|
||||
return new TableDataInfo<>(); // 返回空结果或自定义错误结果
|
||||
return new TableDataInfo<>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,22 +335,21 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
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
|
||||
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);
|
||||
}
|
||||
/**
|
||||
@ -392,12 +395,11 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询物料检索列表
|
||||
* 查询物料检索列表(实时查金蝶,导出上限 10000)
|
||||
*/
|
||||
@Override
|
||||
public List<ImMaterialVo> queryList(ImMaterialBo bo) {
|
||||
LambdaQueryWrapper<ImMaterial> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
return mssqlQueryService.queryMaterialList(bo, 10000, true);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ImMaterial> buildQueryWrapper(ImMaterialBo bo) {
|
||||
@ -525,6 +527,13 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
@Override
|
||||
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<>();
|
||||
queryWrapper.eq(ImMaterial::getMaterialCode, materialCode)
|
||||
.eq(ImMaterial::getMaterialName, materialName);
|
||||
@ -639,8 +648,14 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
|
||||
|
||||
public ImMaterial getMaterialByCode(String materialCode) {
|
||||
try {
|
||||
ImMaterial kingdee = mssqlQueryService.findMaterialByCode(materialCode);
|
||||
if (kingdee != null) {
|
||||
return kingdee;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
QueryWrapper<ImMaterial> wrapper = new QueryWrapper<>();
|
||||
// 从数据库中获取ImMaterial,假设返回的是一个Optional<ImMaterial>
|
||||
wrapper.eq("material_code", materialCode);
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
@ -706,12 +721,16 @@ public class ImMaterialServiceImpl implements IImMaterialService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param query
|
||||
* @return
|
||||
* 按编码或名称模糊搜索金蝶物料
|
||||
*/
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -279,8 +279,6 @@ public class MrpResultCheckServiceImpl implements IMrpResultCheckService {
|
||||
baseMapper.insert(vo);
|
||||
|
||||
}
|
||||
processOrderPro.setBomStatus(4L);
|
||||
processOrderProMapper.updateById(processOrderPro);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -8,18 +8,23 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
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.PlmPdfZipPushItem;
|
||||
import com.ruoyi.system.domain.ProcessOrderPro;
|
||||
import com.ruoyi.system.domain.ProductionOrder;
|
||||
import com.ruoyi.system.domain.bo.PlmPdfZipPushBo;
|
||||
import com.ruoyi.system.domain.vo.PlmPdfZipPushDetailVo;
|
||||
import com.ruoyi.system.domain.vo.PlmPdfZipPushItemVo;
|
||||
import com.ruoyi.system.domain.vo.PlmPdfZipPushVo;
|
||||
import com.ruoyi.system.mapper.FigureSaveMapper;
|
||||
import com.ruoyi.system.mapper.PlmPdfZipPushItemMapper;
|
||||
import com.ruoyi.system.mapper.PlmPdfZipPushMapper;
|
||||
import com.ruoyi.system.mapper.ProcessOrderProMapper;
|
||||
import com.ruoyi.system.mapper.ProductionOrderMapper;
|
||||
import com.ruoyi.system.service.IPlmPdfZipPushService;
|
||||
import com.ruoyi.system.utils.ProductionOrderNoUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -55,6 +60,8 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
|
||||
private final PlmPdfZipPushMapper pushMapper;
|
||||
private final PlmPdfZipPushItemMapper pushItemMapper;
|
||||
private final ProcessOrderProMapper processOrderProMapper;
|
||||
private final FigureSaveMapper figureSaveMapper;
|
||||
private final ProductionOrderMapper productionOrderMapper;
|
||||
|
||||
private static final String PLM_DWG_SUBDIR = "dwg";
|
||||
|
||||
@ -110,47 +117,185 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
||||
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)) {
|
||||
return null;
|
||||
throw new ServiceException("生产令号不能为空");
|
||||
}
|
||||
ProcessOrderPro existing = processOrderProMapper.selectByProjectNumber(orderNo);
|
||||
if (existing != null) {
|
||||
if (existing == null) {
|
||||
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();
|
||||
ProcessOrderPro add = new ProcessOrderPro();
|
||||
add.setProductionOrderNo(orderNo);
|
||||
add.setProductionName(orderNo);
|
||||
add.setSource(ProcessOrderProSource.PLM);
|
||||
add.setUnit("台");
|
||||
add.setQuantity(0L); // 不保留零件图(前端 quantity:1是/0否)
|
||||
add.setBomStatus(1L); // 设计出图完成(图纸已推送)
|
||||
add.setRouteStatus(0L);
|
||||
add.setNeedProcess(1);
|
||||
add.setIsDownload(1);
|
||||
add.setNeedKingdeePrdMo(0);
|
||||
add.setNeedKingdeePurchaseOrder(0);
|
||||
add.setWhetherDelay(0);
|
||||
add.setIsEnterpriseStandard(resolveProjectTypeByOrderNo(orderNo));
|
||||
add.setDrawingTime(now);
|
||||
add.setCreateBy("system-PLM");
|
||||
add.setUpdateBy("system-PLM");
|
||||
add.setCreateTime(now);
|
||||
add.setUpdateTime(now);
|
||||
FigureSave product = new FigureSave();
|
||||
product.setPid(order.getId());
|
||||
product.setProductionCode(order.getProductionOrderNo());
|
||||
product.setFigureNumber(hint.figureNumber);
|
||||
product.setFigureName(hint.figureName);
|
||||
product.setFigureNum(hint.figureNum);
|
||||
product.setProductType(StringUtils.isNotBlank(order.getIsEnterpriseStandard())
|
||||
? order.getIsEnterpriseStandard() : resolveProjectTypeByOrderNo(order.getProductionOrderNo()));
|
||||
product.setFileType("二维");
|
||||
product.setDrawTime(drawTime);
|
||||
product.setWhetherDelay(0);
|
||||
product.setCreateBy("system-PLM");
|
||||
product.setUpdateBy("system-PLM");
|
||||
product.setCreateTime(now);
|
||||
product.setUpdateTime(now);
|
||||
figureSaveMapper.insert(product);
|
||||
log.info("[PLM-PDF] 自动新建产品: orderNo={}, pid={}, figureNumber={}, figureName={}, figureNum={}, productType={}, drawTime={}",
|
||||
order.getProductionOrderNo(), order.getId(), product.getFigureNumber(), product.getFigureName(),
|
||||
product.getFigureNum(), product.getProductType(), product.getDrawTime());
|
||||
}
|
||||
|
||||
private ProductHint resolveProductHint(String orderNo, List<Map<String, String>> savedItems) {
|
||||
ProductHint hint = new ProductHint();
|
||||
hint.figureNumber = orderNo;
|
||||
hint.figureName = orderNo;
|
||||
hint.figureNum = 1L;
|
||||
|
||||
List<ProductionOrder> assemblyRows = productionOrderMapper.selectList(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
processOrderProMapper.insert(add);
|
||||
log.info("[PLM-PDF] 自动新建项目: productionOrderNo={}, id={}, source=PLM", orderNo, add.getId());
|
||||
return add.getId();
|
||||
double v = Double.parseDouble(raw.trim());
|
||||
if (v < 1D) {
|
||||
return null;
|
||||
}
|
||||
return (long) Math.round(v);
|
||||
} catch (Exception e) {
|
||||
// 并发首次推送:另一请求可能已插入,再查一次
|
||||
ProcessOrderPro again = processOrderProMapper.selectByProjectNumber(orderNo);
|
||||
if (again != null) {
|
||||
log.info("[PLM-PDF] 自动建项目遇并发,已存在: productionOrderNo={}, id={}", orderNo, again.getId());
|
||||
return again.getId();
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
private static final class ProductHint {
|
||||
private String figureNumber;
|
||||
private String figureName;
|
||||
private Long figureNum;
|
||||
}
|
||||
|
||||
/** 与手工建项一致:按令号前缀推断项目类型(FB/非标、QB/企标等) */
|
||||
@ -395,7 +540,6 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
|
||||
}
|
||||
String sep = StringUtils.isNotBlank(plmPdfSeparator) ? plmPdfSeparator : "_";
|
||||
String code = StringUtils.trimToEmpty(materialCode);
|
||||
String prefixWithSep = code + sep;
|
||||
String ext = "." + extension.toLowerCase(Locale.ROOT);
|
||||
int count = 0;
|
||||
int scanned = 0;
|
||||
@ -407,10 +551,7 @@ public class PlmPdfZipPushServiceImpl implements IPlmPdfZipPushService {
|
||||
if (!lower.endsWith(ext)) {
|
||||
continue;
|
||||
}
|
||||
boolean matched = name.equalsIgnoreCase(code + ext)
|
||||
|| name.regionMatches(true, 0, prefixWithSep, 0, prefixWithSep.length())
|
||||
|| name.regionMatches(true, 0, code + "-", 0, code.length() + 1);
|
||||
if (matched) {
|
||||
if (PlmDrawingFileNames.matchesMaterialCode(name, code, sep, extension)) {
|
||||
log.info("[PLM-PDF][清理重复图] 文件名匹配: dir={}, materialCode={}, file={}",
|
||||
dir, materialCode, name);
|
||||
count += deletePlmDrawingFile(file.toString(), deleted);
|
||||
|
||||
@ -11,6 +11,7 @@ import com.ruoyi.system.domain.MaterialBom;
|
||||
import com.ruoyi.system.domain.ProcessOrderPro;
|
||||
import com.ruoyi.system.domain.ProcessRoute;
|
||||
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.PlmOrderMaterialMatchUtils;
|
||||
import com.ruoyi.system.domain.bo.plm.PlmProcessMaterialBo;
|
||||
@ -38,8 +39,10 @@ import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@ -75,26 +78,74 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
if (StringUtils.isBlank(productionOrder)) {
|
||||
throw new ServiceException("productionOrder 不能为空");
|
||||
}
|
||||
requireProductionOrderExists(productionOrder);
|
||||
ProcessOrderPro project = requireProductionOrderExists(productionOrder);
|
||||
log.info("PLM 工艺推送 body JSON:\n{}", JSON.toJSONString(body, SerializerFeature.PrettyFormat));
|
||||
PlmProcessRoutePushValidator.validate(body);
|
||||
|
||||
// 总令号推送:入库前剔除子令号已有且图号归属匹配的物料(变更令 BG 跳过)
|
||||
filterParentMaterialsDuplicatedInChild(body, productionOrder);
|
||||
|
||||
// bom_details = 总装行(bomType=0,来自 production_order) + 材料BOM(bomType=1,来自本次推送)
|
||||
List<BomDetails> assemblyBomRows = buildAssemblyBomDetailsFromProductionOrder(productionOrder);
|
||||
Set<String> incomingCodes = collectPushMaterialCodes(body);
|
||||
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> bomRows = new ArrayList<>(assemblyBomRows.size() + materialBomDetailRows.size());
|
||||
bomRows.addAll(assemblyBomRows);
|
||||
bomRows.addAll(materialBomDetailRows);
|
||||
List<MaterialBom> materialBomRows = buildMaterialBoms(body, productionOrder);
|
||||
List<ProcessRoute> routeRows = buildProcessRoutes(body, productionOrder);
|
||||
bomDetailsMapper.deleteWithProCode(productionOrder);
|
||||
materialBomMapper.deleteWithProCode(productionOrder);
|
||||
processRouteMapper.deleteByProCode(productionOrder);
|
||||
if (!bomRows.isEmpty()) {
|
||||
bomDetailsMapper.insertBatch(bomRows);
|
||||
if (!materialBomDetailRows.isEmpty()) {
|
||||
bomDetailsMapper.insertBatch(materialBomDetailRows);
|
||||
}
|
||||
if (!materialBomRows.isEmpty()) {
|
||||
materialBomMapper.insertBatch(materialBomRows);
|
||||
@ -104,29 +155,30 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
}
|
||||
|
||||
// 子令号推送:按本次归属匹配的图号,清理总令号侧重复工艺/BOM
|
||||
cleanupParentProcessDuplicatedInChild(productionOrder, collectPushMaterialCodes(body));
|
||||
cleanupParentProcessDuplicatedInChild(productionOrder, incomingCodes);
|
||||
|
||||
PlmProcessRoutePushNormalizer.PlmProcessRoutePushSummary summary = PlmProcessRoutePushNormalizer.summarize(body);
|
||||
summary.setSavedBomCount(bomRows.size());
|
||||
summary.setSavedBomCount(assemblyCount + materialBomDetailRows.size());
|
||||
summary.setSavedMaterialBomCount(materialBomRows.size());
|
||||
summary.setSavedRouteCount(routeRows.size());
|
||||
summary.setAppended(append);
|
||||
summary.setReplaced(replaced);
|
||||
markProjectAfterPlmProcessRoute(productionOrder, summary);
|
||||
log.info("PLM 工艺推送入库完成 productionOrder={} bom={}(assembly={} materialDetail={}) materialBom={} route={}",
|
||||
productionOrder, bomRows.size(), assemblyBomRows.size(), materialBomDetailRows.size(),
|
||||
materialBomRows.size(), routeRows.size());
|
||||
log.info("PLM 工艺推送入库完成 productionOrder={} bom={}(assembly={} materialDetail={}) materialBom={} route={} appended={} replaced={}",
|
||||
productionOrder, assemblyCount + materialBomDetailRows.size(), assemblyCount, materialBomDetailRows.size(),
|
||||
materialBomRows.size(), routeRows.size(), append, replaced);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 总令号入库前:去掉已在子令号出现、且图号前缀归属本令号的物料。
|
||||
* 总令号入库前:去掉子令号 process_route 中已存在的相同物料编码,并清理总令库中对应行。
|
||||
*/
|
||||
private void filterParentMaterialsDuplicatedInChild(PlmProcessRoutePushBo body, String productionOrder) {
|
||||
if (body == null || CollectionUtils.isEmpty(body.getMaterialList())) {
|
||||
return;
|
||||
}
|
||||
if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrder)
|
||||
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)
|
||||
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(productionOrder) == null) {
|
||||
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)) {
|
||||
return;
|
||||
}
|
||||
Set<String> childCodes = collectChildOrderMaterialCodes(productionOrder);
|
||||
@ -148,26 +200,26 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
kept.add(material);
|
||||
}
|
||||
body.setMaterialList(kept);
|
||||
log.info("[PLM-工艺去重] 总令号过滤完成: parent={}, before={}, after={}, childCodes={}",
|
||||
productionOrder, before, kept.size(), childCodes.size());
|
||||
int deleted = deleteProcessDataByMaterialCodes(productionOrder, childCodes);
|
||||
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) {
|
||||
if (!PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrder)) {
|
||||
return;
|
||||
}
|
||||
String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrder);
|
||||
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)
|
||||
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(parent) == null) {
|
||||
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)) {
|
||||
log.info("[PLM-工艺去重] 子令号跳过清理总令: child={}, parent={}", productionOrder, parent);
|
||||
return;
|
||||
}
|
||||
Set<String> codes = filterCodesBelongingToOrder(inputCodes, parent);
|
||||
Set<String> codes = normalizeMaterialCodes(inputCodes);
|
||||
if (codes.isEmpty()) {
|
||||
log.info("[PLM-工艺去重] 子令号本次无归属匹配图号,不清理: child={}, parent={}", productionOrder, parent);
|
||||
log.info("[PLM-工艺去重] 子令号本次无有效物料编码,不清理: child={}, parent={}", productionOrder, parent);
|
||||
return;
|
||||
}
|
||||
int deleted = deleteProcessDataByMaterialCodes(parent, codes);
|
||||
@ -175,7 +227,7 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
productionOrder, parent, codes, deleted);
|
||||
}
|
||||
|
||||
/** 汇总已接收子令号(末段纯数字、不含 BG)在 process_route 中的物料编码,且须归属总令图号前缀。 */
|
||||
/** 汇总已接收子令号在 process_route 中的物料编码(物料编码完全相同即视为重复,不做图号前缀过滤)。 */
|
||||
private Set<String> collectChildOrderMaterialCodes(String parentOrderNo) {
|
||||
Set<String> childOrderNos = findChildProductionOrderNos(parentOrderNo);
|
||||
if (childOrderNos.isEmpty()) {
|
||||
@ -192,7 +244,7 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
continue;
|
||||
}
|
||||
String code = StringUtils.trimToEmpty(route.getMaterialCode());
|
||||
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, parentOrderNo)) {
|
||||
if (StringUtils.isNotBlank(code)) {
|
||||
codes.add(code);
|
||||
}
|
||||
}
|
||||
@ -235,17 +287,18 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private static Set<String> filterCodesBelongingToOrder(Set<String> codes, String productionOrderNo) {
|
||||
Set<String> matched = new LinkedHashSet<>();
|
||||
private static Set<String> normalizeMaterialCodes(Set<String> codes) {
|
||||
Set<String> normalized = new LinkedHashSet<>();
|
||||
if (codes == null) {
|
||||
return matched;
|
||||
return normalized;
|
||||
}
|
||||
for (String code : codes) {
|
||||
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, productionOrderNo)) {
|
||||
matched.add(StringUtils.trimToEmpty(code));
|
||||
String trimmed = StringUtils.trimToEmpty(code);
|
||||
if (StringUtils.isNotBlank(trimmed)) {
|
||||
normalized.add(trimmed);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** 按令号 + 物料编码精确删除工艺及相关 BOM(只动该令号)。 */
|
||||
@ -268,20 +321,263 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 从已入库的 production_order 回写总装行到 bom_details,避免工艺推送全删后总装丢失。 */
|
||||
private List<BomDetails> buildAssemblyBomDetailsFromProductionOrder(String productionOrder) {
|
||||
/** 从已入库的 production_order 回写总装行到 bom_details,避免工艺推送后总装丢失。 */
|
||||
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(
|
||||
Wrappers.<ProductionOrder>lambdaQuery()
|
||||
.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 必须已存在,否则拒绝接收。 */
|
||||
private void requireProductionOrderExists(String productionOrder) {
|
||||
private ProcessOrderPro requireProductionOrderExists(String productionOrder) {
|
||||
ProcessOrderPro order = processOrderProMapper.selectByProjectNumber(productionOrder.trim());
|
||||
if (order == null) {
|
||||
throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -299,6 +595,23 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
throw new ServiceException("生产令号 " + productionOrder.trim() + " 不存在");
|
||||
}
|
||||
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();
|
||||
update.setId(order.getId());
|
||||
update.setBomStatus(8L);
|
||||
@ -306,16 +619,16 @@ public class PlmProcessRoutePushServiceImpl implements IPlmProcessRoutePushServi
|
||||
update.setRouteUptime(now);
|
||||
update.setPlanUptime(now);
|
||||
update.setBomUptime(now);
|
||||
update.setBomCount((long) summary.getSavedBomCount());
|
||||
update.setMaterialCount((long) summary.getMaterialCount());
|
||||
update.setRouteCount((long) summary.getSavedRouteCount());
|
||||
update.setBomCount(bomCount);
|
||||
update.setMaterialCount(materialCount);
|
||||
update.setRouteCount(routeCount);
|
||||
update.setSource(ProcessOrderProSource.PLM);
|
||||
update.setUpdateBy("PLM");
|
||||
update.setUpdateTime(now);
|
||||
processOrderProMapper.updateById(update);
|
||||
log.info("PLM工艺计划已回写项目状态: 令号={}, id={}, bomStatus=8, routeStatus=1, bom={}, material={}, route={}",
|
||||
productionOrder, order.getId(),
|
||||
summary.getSavedBomCount(), summary.getMaterialCount(), summary.getSavedRouteCount());
|
||||
log.info("PLM工艺计划已回写项目状态: 令号={}, id={}, bomStatus=8, routeStatus=1, bom={}, material={}, route={}, appended={}, replaced={}",
|
||||
productionOrder, order.getId(), bomCount, materialCount, routeCount,
|
||||
summary != null && summary.isAppended(), summary != null && summary.isReplaced());
|
||||
}
|
||||
|
||||
private List<BomDetails> buildBomDetails(PlmProcessRoutePushBo body, String productionOrder) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,8 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.KingdeeMaterialCategoryHelper;
|
||||
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.system.controller.ProcessRouteController;
|
||||
import com.ruoyi.system.domain.*;
|
||||
@ -907,7 +909,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
return bomDetails;
|
||||
}
|
||||
|
||||
|
||||
//图号以 009、AA、AB(、AC(、015、BC(、DIN、DNS、SF、两个空格开头,或图号含 (M),或备注含「外购件」
|
||||
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(");
|
||||
}
|
||||
@ -1133,10 +1135,39 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
|
||||
@Override
|
||||
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<>();
|
||||
wq.eq(ProcessOrderPro::getProductionOrderNo, rooteProdet);
|
||||
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) {
|
||||
return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未审核,无法推送,请先审核");
|
||||
}
|
||||
@ -1144,6 +1175,29 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
return R.fail("项目:" + processOrderPro1.getProductionOrderNo() + "工艺未通过,无法推送,请先审核");
|
||||
}
|
||||
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> failedRoutes = new ArrayList<>();
|
||||
List<String> duplicateRoutes = new ArrayList<>();
|
||||
@ -1181,6 +1235,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
resultDTO.setSuccessfulRoutes(successfulRoutes);
|
||||
resultDTO.setFailedRoutes(failedRoutes);
|
||||
resultDTO.setDuplicateRoutes(duplicateRoutes);
|
||||
if (updateProjectStatus) {
|
||||
String jsonStr = JSONUtil.toJsonStr(resultDTO);
|
||||
ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(rooteProdet);
|
||||
//更新项目状态 推送工艺
|
||||
@ -1188,6 +1243,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
processOrderPro.setRouteLog(jsonStr);
|
||||
log.info("<UNK>: " + jsonStr);
|
||||
processOrderProMapper.updateById(processOrderPro);
|
||||
}
|
||||
return R.ok(resultDTO);
|
||||
}
|
||||
|
||||
@ -1260,95 +1316,116 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工艺路线单据体
|
||||
*
|
||||
* @param rooteProdet
|
||||
* @return
|
||||
* 查询工艺路线单据体:按生产令号查金蝶 PRD_MO,再拉工序计划与用料。
|
||||
*/
|
||||
@Override
|
||||
public List<CombinedDTO> getSelecPlanRouteList(String rooteProdet) {
|
||||
// 根据生产令号查询生产订单编码,获取计划单据编号和工艺路线
|
||||
List<PlanOrderVo> planOrderList = getSelectProceOrder1(rooteProdet);
|
||||
|
||||
log.info("获取计划单号集合: planOrderList=====>" + planOrderList);
|
||||
|
||||
// 初始化空列表
|
||||
List<PlannedProcessVo> plannedProcesses = new ArrayList<>();
|
||||
List<CombinedDTO> combinedVoList = new ArrayList<>();
|
||||
if (planOrderList == null || planOrderList.isEmpty()) {
|
||||
return combinedVoList; // 返回空列表避免空指针异常
|
||||
return getSelecPlanRouteList(rooteProdet, null);
|
||||
}
|
||||
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
/**
|
||||
* 用令号 + 物料编码查金蝶 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。 */
|
||||
private List<CombinedDTO> buildCombinedFromPlanOrders(List<PlanOrderVo> planOrderList) {
|
||||
List<CombinedDTO> combinedVoList = new ArrayList<>();
|
||||
if (planOrderList == null || planOrderList.isEmpty()) {
|
||||
return combinedVoList;
|
||||
}
|
||||
for (PlanOrderVo planOrder : planOrderList) {
|
||||
// 请求参数,要求为json字符串
|
||||
JsonObject json = new JsonObject();
|
||||
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");
|
||||
// 创建过滤条件
|
||||
JsonArray filterString = new JsonArray();
|
||||
JsonObject filterObject = new JsonObject();
|
||||
filterObject.addProperty("FieldName", "FMONumber");
|
||||
filterObject.addProperty("Compare", "="); // 改为等号运算符
|
||||
filterObject.addProperty("Compare", "=");
|
||||
filterObject.addProperty("Value", planOrder.getFBillNo());
|
||||
filterObject.addProperty("Left", "");
|
||||
filterObject.addProperty("Right", "");
|
||||
filterObject.addProperty("Logic", 0);// 从 PlanOrderVo 获取生产令号
|
||||
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", 2000);
|
||||
json.addProperty("SubSystemId", "");
|
||||
|
||||
String jsonData = json.toString();
|
||||
try {
|
||||
// 调用接口
|
||||
String resultJson = String.valueOf(client.billQuery(jsonData));
|
||||
K3CloudApi client = new K3CloudApi();
|
||||
String resultJson = String.valueOf(client.billQuery(json.toString()));
|
||||
JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class);
|
||||
// 使用 ObjectMapper 将 JsonArray 转换为 List<PlannedProcessVo>
|
||||
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());
|
||||
// 4. 将 PlanOrderVo 和对应的 PlannedProcessVo 合并成一个对象
|
||||
CombinedDTO combined = new CombinedDTO();
|
||||
combined.setOrderNumber(planOrder.getFBillNo()); // 生产订单号
|
||||
combined.setMaterialCode(planOrder.getFmaterialidFnumber()); // 物料编码
|
||||
combined.setMaterialName(planOrder.getFMaterialName()); // 物料名称
|
||||
combined.setOrderNumber(planOrder.getFBillNo());
|
||||
combined.setMaterialCode(planOrder.getFmaterialidFnumber());
|
||||
combined.setMaterialName(planOrder.getFMaterialName());
|
||||
combined.setProcesses(plannedProcessList);
|
||||
combined.setMaterial(planOrder.getBasePropertyqtr());
|
||||
combined.setMaterialUsageDTOList(materialUseList);
|
||||
// 5. 添加到最终合并的列表中
|
||||
combinedVoList.add(combined);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 输出异常日志
|
||||
log.error("组装工序计划单失败 订单号={}", planOrder.getFBillNo(), e);
|
||||
}
|
||||
}
|
||||
return combinedVoList;
|
||||
}
|
||||
|
||||
return combinedVoList; // 返回结果
|
||||
}
|
||||
|
||||
/**
|
||||
* 物料编码为空:令号下全部工序计划单打 zip;有物料编码:该物料一份 PDF。
|
||||
*/
|
||||
@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);
|
||||
if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) {
|
||||
throw new ServiceException("未查询到可生成PDF的工艺数据");
|
||||
throw new ServiceException("未查询到令号「" + rooteProdet + "」的金蝶生产订单");
|
||||
}
|
||||
String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet, Collections.singletonList(pdfFontPath));
|
||||
if (StringUtils.isBlank(pathZIP)) {
|
||||
throw new ServiceException("PDF压缩包生成失败");
|
||||
String path = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet,
|
||||
Collections.singletonList(pdfFontPath));
|
||||
if (StringUtils.isBlank(path)) {
|
||||
throw new ServiceException("PDF生成失败");
|
||||
}
|
||||
return pathZIP;
|
||||
return path;
|
||||
}
|
||||
|
||||
public List<PlanOrderVo> getSelectProceOrder(String rooteProdet) {
|
||||
@ -1395,26 +1472,49 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
}
|
||||
|
||||
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();
|
||||
// 第一次查询:使用 F_HBYT_SCLH
|
||||
List<PlanOrderVo> result = queryWithFieldName(client, "F_HBYT_XMH", rooteProdet);
|
||||
return result;
|
||||
return queryWithFieldName(client, "F_HBYT_XMH", rooteProdet, materialCodes);
|
||||
}
|
||||
|
||||
/*
|
||||
根据生产令号查询 返回订单编号 物料编码名称
|
||||
*/
|
||||
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();
|
||||
json.addProperty("FormId", "PRD_MO");
|
||||
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();
|
||||
JsonObject filterObject = new JsonObject();
|
||||
filterObject.addProperty("FieldName", fieldName); // 使用传入的 fieldName
|
||||
filterObject.addProperty("Compare", "67");
|
||||
filterObject.addProperty("Value", rooteProdet);
|
||||
filterObject.addProperty("Left", "");
|
||||
filterObject.addProperty("Left", hasMaterial ? "(" : "");
|
||||
filterObject.addProperty("Right", "");
|
||||
filterObject.addProperty("Logic", 1);
|
||||
filterString.add(filterObject);
|
||||
@ -1424,10 +1524,28 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
|
||||
filterObject1.addProperty("Compare", "67");
|
||||
filterObject1.addProperty("Value", rooteProdet);
|
||||
filterObject1.addProperty("Left", "");
|
||||
filterObject1.addProperty("Right", "");
|
||||
filterObject1.addProperty("Logic", 1);
|
||||
filterObject1.addProperty("Right", hasMaterial ? ")" : "");
|
||||
// 有物料时括号内 OR 结束,再 AND 物料;无物料时保持原来的令号 OR
|
||||
filterObject1.addProperty("Logic", hasMaterial ? 0 : 1);
|
||||
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.addProperty("OrderString", "");
|
||||
json.addProperty("TopRowCount", 0);
|
||||
|
||||
@ -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.ProductionOrderWeightExportDTO;
|
||||
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.vo.ProcessOrderProVo;
|
||||
import com.ruoyi.system.mapper.FigureSaveMapper;
|
||||
@ -503,7 +504,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
throw new ServiceException("总装配 BOM 数据不能为空");
|
||||
}
|
||||
String productionOrderNo = AssemblyBomReceiveConverter.resolveProductionOrderNo(items);
|
||||
requireProductionOrderExists(productionOrderNo);
|
||||
ProcessOrderPro project = requireProductionOrderExists(productionOrderNo);
|
||||
// 外购件尾部 (2D)/(2D)去掉,避免上传金蝶一物两码
|
||||
AssemblyBomReceiveConverter.normalizeDrawingCodes(items);
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
@ -528,11 +529,34 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
throw new ServiceException("总装配 BOM 无有效行数据");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<ProductionOrder> existsQuery = Wrappers.lambdaQuery();
|
||||
existsQuery.eq(ProductionOrder::getProductionOrderNo, productionOrderNo);
|
||||
boolean replaced = baseMapper.selectCount(existsQuery) > 0;
|
||||
if (replaced) {
|
||||
List<ProductionOrder> existingRows = baseMapper.selectList(
|
||||
Wrappers.<ProductionOrder>lambdaQuery()
|
||||
.eq(ProductionOrder::getProductionOrderNo, productionOrderNo)
|
||||
.select(ProductionOrder::getId, ProductionOrder::getMainPart, ProductionOrder::getDrawingNo));
|
||||
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;
|
||||
@ -546,7 +570,9 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
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()
|
||||
@ -562,14 +588,82 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
summary.setLineCount(effectiveItems.size());
|
||||
summary.setInsertedCount(inserted);
|
||||
summary.setReplaced(replaced);
|
||||
markProjectAfterPlmAssemblyBom(productionOrderNo, inserted);
|
||||
log.info("总装配 BOM 接收完成 productionOrderNo={} lineCount={} inserted={} replaced={} bomDetails={}",
|
||||
productionOrderNo, effectiveItems.size(), inserted, replaced, bomDetailsCount);
|
||||
summary.setAppended(append);
|
||||
int totalLines = allRows == null ? inserted : allRows.size();
|
||||
markProjectAfterPlmAssemblyBom(productionOrderNo, totalLines);
|
||||
log.info("总装配 BOM 接收完成 productionOrderNo={} lineCount={} inserted={} replaced={} appended={} bomDetails={}",
|
||||
productionOrderNo, effectiveItems.size(), inserted, replaced, append, bomDetailsCount);
|
||||
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(
|
||||
List<AssemblyBomReceiveItemDTO> items, String productionOrderNo) {
|
||||
@ -577,8 +671,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
return items;
|
||||
}
|
||||
if (PlmOrderMaterialMatchUtils.isChangeOrder(productionOrderNo)
|
||||
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrderNo)
|
||||
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(productionOrderNo) == null) {
|
||||
|| PlmOrderMaterialMatchUtils.isChildProductionOrderNo(productionOrderNo)) {
|
||||
return items;
|
||||
}
|
||||
Set<String> childCodes = collectChildAssemblyDrawingNos(productionOrderNo);
|
||||
@ -597,8 +690,15 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
}
|
||||
kept.add(item);
|
||||
}
|
||||
log.info("[PLM-总装去重] 总令号过滤完成: parent={}, before={}, after={}",
|
||||
productionOrderNo, items.size(), kept.size());
|
||||
int deletedPo = baseMapper.delete(Wrappers.<ProductionOrder>lambdaQuery()
|
||||
.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;
|
||||
}
|
||||
|
||||
@ -608,15 +708,15 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
return;
|
||||
}
|
||||
String parent = PlmOrderMaterialMatchUtils.resolveParentProductionOrderNo(productionOrderNo);
|
||||
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)
|
||||
|| PlmOrderMaterialMatchUtils.resolveMaterialCodePrefix(parent) == null) {
|
||||
if (StringUtils.isBlank(parent) || PlmOrderMaterialMatchUtils.isChangeOrder(parent)) {
|
||||
return;
|
||||
}
|
||||
Set<String> codes = new LinkedHashSet<>();
|
||||
if (drawingNos != null) {
|
||||
for (String code : drawingNos) {
|
||||
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(code, parent)) {
|
||||
codes.add(StringUtils.trimToEmpty(code));
|
||||
String trimmed = StringUtils.trimToEmpty(code);
|
||||
if (StringUtils.isNotBlank(trimmed)) {
|
||||
codes.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -666,7 +766,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
continue;
|
||||
}
|
||||
String drawingNo = StringUtils.trimToEmpty(row.getDrawingNo());
|
||||
if (PlmOrderMaterialMatchUtils.materialBelongsToOrder(drawingNo, parentOrderNo)) {
|
||||
if (StringUtils.isNotBlank(drawingNo)) {
|
||||
codes.add(drawingNo);
|
||||
}
|
||||
}
|
||||
@ -710,7 +810,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
}
|
||||
|
||||
/** 接收前校验:process_order_pro.production_order_no 必须已存在,否则拒绝接收。 */
|
||||
private void requireProductionOrderExists(String productionOrderNo) {
|
||||
private ProcessOrderPro requireProductionOrderExists(String productionOrderNo) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
throw new ServiceException("生产令号不能为空");
|
||||
}
|
||||
@ -719,6 +819,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
if (order == null) {
|
||||
throw new ServiceException("生产令号 " + orderNo + " 不存在");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -742,6 +843,7 @@ public class ProductionOrderServiceImpl implements IProductionOrderService {
|
||||
update.setBomUptime(now);
|
||||
update.setMaterialCount((long) lineCount);
|
||||
update.setSource(ProcessOrderProSource.PLM);
|
||||
update.setDrawingQtyIssued(0);
|
||||
update.setUpdateBy("PLM");
|
||||
update.setUpdateTime(now);
|
||||
processOrderProMapper.updateById(update);
|
||||
|
||||
@ -2,22 +2,25 @@ package com.ruoyi.system.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.RatioAccessory;
|
||||
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.PurchaseOrderDTO;
|
||||
import com.ruoyi.system.domain.dto.ratio.RatioBuildCapacityDTO;
|
||||
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.RatioTransitPoDTO;
|
||||
import com.ruoyi.system.mapper.RatioAccessoryMapper;
|
||||
import com.ruoyi.system.mapper.RatioConfigMapper;
|
||||
import com.ruoyi.system.mapper.RatioModelMapper;
|
||||
import com.ruoyi.system.runner.JdUtil;
|
||||
import com.ruoyi.system.service.IRatioBuildCapacityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
@ -31,33 +34,90 @@ import java.util.stream.Collectors;
|
||||
public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService {
|
||||
|
||||
private final RatioConfigMapper ratioConfigMapper;
|
||||
private final RatioModelMapper ratioModelMapper;
|
||||
private final RatioAccessoryMapper ratioAccessoryMapper;
|
||||
|
||||
@Override
|
||||
public RatioBuildCapacityDTO queryByRatio(BigDecimal ratioValue, Integer qty) {
|
||||
// ratioValue: ratio_config.ratio_value(速比),qty: 页面输入的目标数量(本实现口径:BOM每个物料需求数量=qty)
|
||||
if (ratioValue == null) {
|
||||
throw new ServiceException("速比不能为空");
|
||||
public List<String> listRatios(String seriesType) {
|
||||
seriesType = normalizeSeries(seriesType);
|
||||
List<RatioConfig> configs = ratioConfigMapper.selectList(new LambdaQueryWrapper<RatioConfig>()
|
||||
.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;
|
||||
if (inputQty <= 0) {
|
||||
throw new ServiceException("数量必须大于0");
|
||||
}
|
||||
|
||||
// 1) 查速比主表 ratio_config,按速比值+启用状态获取一条配置(包含 maxTorqueNm 等扩展信息)
|
||||
RatioConfig config = ratioConfigMapper.selectOne(new LambdaQueryWrapper<RatioConfig>()
|
||||
.eq(RatioConfig::getRatioValue, ratioValue)
|
||||
.eq(RatioConfig::getStatus, 0)
|
||||
.last("limit 1"));
|
||||
if (config == null) {
|
||||
throw new ServiceException("未找到对应速比配置");
|
||||
RatioConfig config = findConfig(seriesType, ratioValue);
|
||||
RatioModel model = ratioModelMapper.selectById(modelId);
|
||||
if (model == null || !config.getId().equals(model.getRatioConfigId())) {
|
||||
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::getStatus, 0));
|
||||
if (accessories == null || accessories.isEmpty()) {
|
||||
.eq(RatioAccessory::getStatus, 0)
|
||||
.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明细");
|
||||
}
|
||||
|
||||
@ -69,8 +129,11 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
|
||||
}
|
||||
|
||||
String materialCode = a.getMaterialCode();
|
||||
// 3) requiredQty:该物料需求数量(当前按你的要求取“页面输入的数量”,没有乘BOM用量系数)
|
||||
double requiredQty = inputQty;
|
||||
double bomQty = a.getBomQty() == null ? 1D : a.getBomQty().doubleValue();
|
||||
if (bomQty <= 0) {
|
||||
bomQty = 1D;
|
||||
}
|
||||
double requiredQty = bomQty * inputQty;
|
||||
|
||||
// 4) 即时库存(onHandQty):金蝶 STK_Inventory 的基本单位库存汇总(JdUtil.getKuCun -> FBaseQty)
|
||||
double onHandQty = Optional.ofNullable(JdUtil.getKuCun(materialCode))
|
||||
@ -99,8 +162,8 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
|
||||
double availableQty = Math.max(0.0, onHandQty - moNotPickQty);
|
||||
// 9) 最终可用量 = 可用量 + 采购在途 + 生产在途
|
||||
double finalAvailableQty = availableQty + poNotInQty + moNotInQty;
|
||||
// 10) 可支持数量:按最终可用量取整向下(当前实现等价于“能做多少套”,因为 requiredQty=输入数量)
|
||||
long canSupportQty = (long) Math.floor(finalAvailableQty);
|
||||
// 10) 可支持套数:最终可用 ÷ Excel 单台数量,向下取整
|
||||
long canSupportQty = (long) Math.floor(finalAvailableQty / bomQty);
|
||||
maxBuildQty = Math.min(maxBuildQty, canSupportQty);
|
||||
// 11) 补差数量:需求 - 最终可用(>0 表示缺多少)
|
||||
double shortageQty = Math.max(0.0, requiredQty - finalAvailableQty);
|
||||
@ -111,6 +174,7 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
|
||||
item.setMaterialName(a.getMaterialName());
|
||||
item.setDanzhong(a.getDanzhong());
|
||||
item.setCanzhi(a.getCanzhi());
|
||||
item.setBomQty(bomQty);
|
||||
item.setRequiredQty(requiredQty);
|
||||
item.setOnHandQty(onHandQty);
|
||||
item.setMoNotPickQty(moNotPickQty);
|
||||
@ -126,19 +190,125 @@ public class RatioBuildCapacityServiceImpl implements IRatioBuildCapacityService
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
RatioBuildCapacityDTO dto = new RatioBuildCapacityDTO();
|
||||
dto.setRatioConfigId(config.getId());
|
||||
dto.setSeriesType(config.getSeriesType());
|
||||
dto.setRatioValue(config.getRatioValue());
|
||||
dto.setModelId(model.getId());
|
||||
dto.setModelCode(model.getModelCode());
|
||||
dto.setMaxTorqueNm(config.getMaxTorqueNm());
|
||||
dto.setInputQty(inputQty);
|
||||
dto.setMaxBuildQty(maxBuildQty == Long.MAX_VALUE ? 0L : maxBuildQty);
|
||||
dto.setOnlyCommonParts(!hasModelPart);
|
||||
dto.setItems(items);
|
||||
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) {
|
||||
// 采购在途明细:从采购订单列表中过滤剩余未入库数量>0,按交期排序,最多返回50条(展示单号/交期/未入库数)
|
||||
if (poList == null || poList.isEmpty()) {
|
||||
|
||||
@ -78,7 +78,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
private final DingTalkProperties dingTalkProperties;
|
||||
|
||||
/**
|
||||
* 同步K3工序计划数据(全量新增与更新)
|
||||
structuredList = {ArrayList@20034} size = 11284 * 同步K3工序计划数据(全量新增与更新)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@ -109,6 +109,9 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
mainEntity.setProductCode(resultDTO.getProductCode());
|
||||
mainEntity.setProductName(resultDTO.getProductName());
|
||||
mainEntity.setProSpecification(resultDTO.getProSpecification());
|
||||
mainEntity.setDocumentStatus(resultDTO.getDocumentStatus());
|
||||
mainEntity.setBillStatus(resultDTO.getBillStatus());
|
||||
mainEntity.setPlanDocumentStatus(resultDTO.getPlanDocumentStatus());
|
||||
if (StringUtils.isNotBlank(resultDTO.getPlanStartTime())) {
|
||||
mainEntity.setPlanStartTime(DateUtil.parse(resultDTO.getPlanStartTime()));
|
||||
}
|
||||
@ -152,7 +155,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
detailEntity.setWorkCenterName(detailDTO.getWorkCenter());
|
||||
detailEntity.setProcessName(detailDTO.getProcessName());
|
||||
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())) {
|
||||
detailEntity.setSeqPlanStartTime(DateUtil.parse(detailDTO.getSeqPlanStartTime()));
|
||||
@ -530,7 +536,6 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
resultDTO.setWorkCenter(mainWorkCenter);
|
||||
resultDTO.setOverallPlanQty(totalPlanQty);
|
||||
resultDTO.setOverallOkQty(totalOkQty);
|
||||
OperationPlanningResultAssembler.applyOverallMetrics(resultDTO, resultDTO.getPlanFinishTime(), relatedDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@ -657,6 +662,12 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
|
||||
/**
|
||||
* 构建工序进度多 Sheet Excel 临时文件。
|
||||
* <p>推送/导出过滤:
|
||||
* <ul>
|
||||
* <li>生产订单单据状态、工序计划单据状态:仅已审核(C)</li>
|
||||
* <li>生产订单业务状态:仅下达(3)、开工(4)</li>
|
||||
* <li>工序 FOperStatus:仅下达(3)、开工(4)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private ProgressExcelFile buildCascadeProgressExcel(SfcOperationPlanningMainBo bo) throws Exception {
|
||||
if (bo == null) {
|
||||
@ -664,14 +675,22 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
}
|
||||
LambdaQueryWrapper<SfcOperationPlanningMain> lqw = buildQueryWrapper(bo);
|
||||
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);
|
||||
if (mainList == null || mainList.isEmpty()) {
|
||||
throw new ServiceException("没有可导出的工序计划数据");
|
||||
throw new ServiceException("没有符合状态条件的工序计划(订单/计划需已审核,业务状态需下达或开工)");
|
||||
}
|
||||
|
||||
List<Long> mainIds = mainList.stream().map(SfcOperationPlanningMain::getId).collect(Collectors.toList());
|
||||
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())) {
|
||||
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()) {
|
||||
continue;
|
||||
}
|
||||
if (!isProgressPushMainAllowed(plan)) {
|
||||
continue;
|
||||
}
|
||||
String sheetKey = resolveSheetKey(plan);
|
||||
List<OperationPlanningProgressExportVo> rows = sheetMap.computeIfAbsent(sheetKey, k -> new ArrayList<>());
|
||||
for (ProcessDetailDTO process : plan.getProcessList()) {
|
||||
if (process == null || isFinishedOrClosed(process)) {
|
||||
if (process == null || !isProgressPushOperAllowed(process)) {
|
||||
continue;
|
||||
}
|
||||
rows.add(toProgressExportRow(plan, process));
|
||||
@ -698,7 +720,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
}
|
||||
sheetMap.entrySet().removeIf(e -> e.getValue() == null || e.getValue().isEmpty());
|
||||
if (sheetMap.isEmpty()) {
|
||||
throw new ServiceException("过滤完工/关闭后没有可导出的工序");
|
||||
throw new ServiceException("过滤后没有可导出的工序(需下达/开工且单据已审核)");
|
||||
}
|
||||
|
||||
String generatedAt = DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss");
|
||||
@ -795,6 +817,43 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
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 k3 = process.getK3Status() == null ? "" : process.getK3Status().trim();
|
||||
String oper = process.getOperStatus() == null ? "" : process.getOperStatus().trim();
|
||||
return status.contains("完工") || status.contains("关闭")
|
||||
|| "完工".equals(k3) || "关闭".equals(k3);
|
||||
|| "完工".equals(k3) || "关闭".equals(k3)
|
||||
|| "5".equals(oper) || "6".equals(oper);
|
||||
}
|
||||
|
||||
private static OperationPlanningProgressExportVo toProgressExportRow(
|
||||
@ -818,8 +879,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
row.setProductName(plan.getProductName());
|
||||
row.setProSpecification(plan.getProSpecification());
|
||||
row.setBillNo(plan.getBillNo());
|
||||
row.setOverallStatus(plan.getOverallStatus());
|
||||
row.setOverallProgress(scale2(plan.getOverallProgress()));
|
||||
// 模板 {{ProgressData.overallStatus}} / overallProgress:工序状态、工序进度
|
||||
Double operProgress = resolveExportOperProgress(process);
|
||||
row.setOverallProgress(scale2(operProgress));
|
||||
row.setOverallStatus(resolveStatusByProgress(operProgress));
|
||||
row.setOperNo(process.getOperNo());
|
||||
row.setProcessName(process.getProcessName());
|
||||
row.setWorkCenter(process.getWorkCenter());
|
||||
@ -827,8 +890,8 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
row.setInQty(process.getInQty());
|
||||
row.setOkQty(process.getOkQty());
|
||||
row.setTransOutQty(process.getTransOutQty());
|
||||
row.setProgress(scale2(process.getProgress()));
|
||||
row.setStatus(process.getStatus());
|
||||
row.setProgress(scale2(operProgress));
|
||||
row.setStatus(resolveStatusByProgress(operProgress));
|
||||
row.setOperPlanStartTime(process.getOperPlanStartTime());
|
||||
row.setOperPlanFinishTime(process.getOperPlanFinishTime());
|
||||
row.setOperator(process.getOperator());
|
||||
@ -836,6 +899,36 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工序状态按进度:0 未开工;>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) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@ -881,6 +974,9 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProductCode()), SfcOperationPlanningMain::getProductCode, bo.getProductCode());
|
||||
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.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.le(bo.getPlanFinishTime() != null, SfcOperationPlanningMain::getPlanFinishTime, bo.getPlanFinishTime());
|
||||
if (StringUtils.isNotBlank(bo.getWorkCenterName())) {
|
||||
|
||||
@ -39,7 +39,6 @@ public final class OperationPlanningResultAssembler {
|
||||
if (mainList == null || mainList.isEmpty()) {
|
||||
return resultList;
|
||||
}
|
||||
Date now = new Date();
|
||||
for (SfcOperationPlanningMain main : mainList) {
|
||||
OperationPlanResultDTO resultDTO = new OperationPlanResultDTO();
|
||||
resultDTO.setMoNumber(main.getMoNumber());
|
||||
@ -52,6 +51,12 @@ public final class OperationPlanningResultAssembler {
|
||||
resultDTO.setPlanStartTime(main.getPlanStartTime() != null ? DateUtil.formatDateTime(main.getPlanStartTime()) : null);
|
||||
resultDTO.setPlanFinishTime(main.getPlanFinishTime() != null ? DateUtil.formatDateTime(main.getPlanFinishTime()) : null);
|
||||
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());
|
||||
|
||||
List<ProcessDetailDTO> processList = new ArrayList<>();
|
||||
@ -78,11 +83,8 @@ public final class OperationPlanningResultAssembler {
|
||||
resultDTO.setWorkCenter(mainWorkCenter);
|
||||
resultDTO.setOverallPlanQty(totalPlanQty);
|
||||
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);
|
||||
resultList.add(resultDTO);
|
||||
}
|
||||
@ -162,8 +164,10 @@ public final class OperationPlanningResultAssembler {
|
||||
detailDTO.setWastageQty(detail.getWastageQty() != null ? detail.getWastageQty().doubleValue() : null);
|
||||
detailDTO.setDepartment(detail.getDepartmentName());
|
||||
detailDTO.setOperator(detail.getOperator());
|
||||
if (detailDTO.getPlanQty() != null && detailDTO.getInQty() != null && detailDTO.getPlanQty() > 0) {
|
||||
BigDecimal progress = BigDecimal.valueOf(detailDTO.getInQty())
|
||||
// 工序进度 = 合格数量 / 工序数量 * 100
|
||||
if (detailDTO.getPlanQty() != null && detailDTO.getPlanQty() > 0) {
|
||||
double ok = detailDTO.getOkQty() == null ? 0D : detailDTO.getOkQty();
|
||||
BigDecimal progress = BigDecimal.valueOf(ok)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.divide(BigDecimal.valueOf(detailDTO.getPlanQty()), 1, RoundingMode.HALF_UP);
|
||||
if (progress.compareTo(BigDecimal.ZERO) < 0) {
|
||||
@ -174,40 +178,9 @@ public final class OperationPlanningResultAssembler {
|
||||
detailDTO.setProgress(progress.doubleValue());
|
||||
}
|
||||
|
||||
String statusStr = detail.getOperStatus();
|
||||
if ("1".equals(statusStr)) {
|
||||
detailDTO.setK3Status("计划");
|
||||
} 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("生产中");
|
||||
}
|
||||
// 兼容历史中文状态:优先按 FOperStatus 码解析
|
||||
detailDTO.setOperStatus(normalizeOperStatusCode(detail.getOperStatus()));
|
||||
enrichProcessDetailStatus(detailDTO);
|
||||
|
||||
detailDTO.setSeqPlanStartTime(detail.getSeqPlanStartTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanStartTime()) : null);
|
||||
detailDTO.setSeqPlanFinishTime(detail.getSeqPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanFinishTime()) : null);
|
||||
@ -223,16 +196,184 @@ public final class OperationPlanningResultAssembler {
|
||||
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("生产中");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工序完工数量条件:工序数 = 转入 = 合格 = 转出,且均 > 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) {
|
||||
if (detail == null) {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -3,9 +3,11 @@ package com.ruoyi.system.service.support;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -61,10 +63,24 @@ public final class WorkAlertPushTypeRegistry {
|
||||
private static final Map<String, PushType> BY_CODE = TYPES.stream()
|
||||
.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() {
|
||||
return TYPES;
|
||||
}
|
||||
|
||||
public static List<PushType> groupConversationTypes() {
|
||||
return TYPES.stream()
|
||||
.filter(t -> supportsGroupConversation(t.getCode()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static PushType find(String pushCode) {
|
||||
if (pushCode == null) {
|
||||
return null;
|
||||
@ -75,4 +91,8 @@ public final class WorkAlertPushTypeRegistry {
|
||||
public static boolean isSupported(String pushCode) {
|
||||
return find(pushCode) != null;
|
||||
}
|
||||
|
||||
public static boolean supportsGroupConversation(String pushCode) {
|
||||
return isSupported(pushCode) && !NON_GROUP_CODES.contains(pushCode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,25 @@ public final class 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) {
|
||||
if (StringUtils.isBlank(productionOrderNo)) {
|
||||
throw new IllegalArgumentException("生产令号为空,无法识别年份");
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="quantity" column="quantity"/>
|
||||
<result property="isEnterpriseStandard" column="is_enterprise_standard"/>
|
||||
<result property="isDownload" column="is_download"/>
|
||||
<result property="drawingQtyIssued" column="drawing_qty_issued"/>
|
||||
<result property="needProcess" column="need_process"/>
|
||||
<result property="drawingPath" column="drawing_path"/>
|
||||
<result property="bomStatus" column="bom_status"/>
|
||||
|
||||
@ -16,6 +16,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="proSpecification" column="pro_specification"/>
|
||||
<result property="planStartTime" column="plan_start_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="updateTime" column="update_time"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<result property="userType" column="user_type"/>
|
||||
<result property="email" column="email"/>
|
||||
<result property="phonenumber" column="phonenumber"/>
|
||||
<result property="employeeNumber" column="employee_number"/>
|
||||
<result property="sex" column="sex"/>
|
||||
<result property="avatar" column="avatar"/>
|
||||
<result property="password" column="password"/>
|
||||
@ -56,6 +57,7 @@
|
||||
u.email,
|
||||
u.avatar,
|
||||
u.phonenumber,
|
||||
u.employee_number,
|
||||
u.password,
|
||||
u.sex,
|
||||
u.status,
|
||||
@ -85,7 +87,7 @@
|
||||
</sql>
|
||||
|
||||
<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
|
||||
from sys_user u
|
||||
left join sys_dept d on u.dept_id = d.dept_id
|
||||
@ -93,7 +95,7 @@
|
||||
</select>
|
||||
|
||||
<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
|
||||
from sys_user u
|
||||
left join sys_dept d on u.dept_id = d.dept_id
|
||||
|
||||
@ -15,7 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
a.main_id,
|
||||
a.plan_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
|
||||
FROM (
|
||||
SELECT
|
||||
@ -74,10 +74,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
SELECT
|
||||
m.mo_number AS workOrderNo,
|
||||
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
|
||||
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 '已开工'
|
||||
ELSE '未开工'
|
||||
END AS status,
|
||||
|
||||
2
spy.log
2
spy.log
@ -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
|
||||
Loading…
Reference in New Issue
Block a user