diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 8444dd5..c56e9d3 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -94,7 +94,7 @@ ruoyi-job - + com.ruoyi ruoyi-oss @@ -161,6 +161,22 @@ 1.39.0 compile + + cn.dev33 + sa-token-core + 1.39.0 + compile + + + cn.dev33 + sa-token-core + 1.39.0 + compile + + + org.springframework + spring-context + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/CallbackController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/CallbackController.java index 301cdb7..ff4565e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/CallbackController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/CallbackController.java @@ -9,6 +9,7 @@ import com.ruoyi.common.dingding.DingCallbackCrypto; import com.ruoyi.common.dingding.robot.RobotGroupService; import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.dingding.webhook.WebhookClient; +import com.ruoyi.common.dingding.workflow.SchemeApplicationBpmsEventHandler; import com.ruoyi.system.controller.KingdeeWorkCenterDataController; import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.mapper.ProcessOrderProMapper; @@ -49,6 +50,9 @@ public class CallbackController { @Autowired private WebhookClient webhookClient; // 注入 WebhookClient + @Autowired + private SchemeApplicationBpmsEventHandler schemeApplicationBpmsEventHandler; + @PostMapping("/receive") public Map callBack(@RequestBody(required = false) Map json) { @@ -216,6 +220,8 @@ public class CallbackController { eventType = "robot_message"; } bizLogger.info("事件类型: {}", eventType); + //只处理「技术方案表」且「同意」场景 + schemeApplicationBpmsEventHandler.handleEventPayload(eventType, eventJson.toJSONString()); // 4. 根据EventType分类处理 if ("check_url".equals(eventType)) { bizLogger.info("测试回调url的正确性"); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java index dc0251b..5d6124d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java @@ -2,12 +2,14 @@ package com.ruoyi.web.controller.dingtalk; import cn.dev33.satoken.annotation.SaCheckPermission; import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.dingding.relay.DingMessageRelayService; import com.ruoyi.common.dingding.util.DingUtil; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -18,6 +20,8 @@ import com.dingtalk.api.response.OapiV2UserGetResponse; @RequestMapping("/ding/talk") public class DingTalkController { + private final DingMessageRelayService dingMessageRelayService; + /** * 查询用户详情 */ @@ -136,4 +140,28 @@ public class DingTalkController { return R.ok("发送成功", processQueryKey); } + + /** + * 示例:经消息中转平台发送 Markdown 单聊(需启动中转服务并配置 dingtalk.message-relay-base-url) + */ + @GetMapping("/testRelayMarkdown") + public R testRelayMarkdown() { + StringBuilder markdownMsg = new StringBuilder(); + markdownMsg.append("### 安全库存提醒\n\n") + .append("今日暂无安全库存预警数据。\n\n") + .append("**物料编号: 123321**\n") + .append("- 物料名称: 测试测试567\n") + .append("- 预计可用量: 200\n") + .append("- 即时库存: 100\n") + .append("- 最大库存: 50\n") + .append("---\n") + .append("**总数汇总**\n") + .append("- 今日安全库存预警数据总数: **50**\n"); + + String resp = dingMessageRelayService.sendMarkdownToUser( + "test测试", + markdownMsg.toString(), + Arrays.asList("史国臣", "田志阳")); + return R.ok("中转返回", resp); + } } diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 7e86632..4f4f697 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -49,8 +49,10 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562 # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题) - #url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true - 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 + # 本机双实例:5.7 常用 3306,8.0 常用其它端口(示例 3307);内网库另见下行注释 + #url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + #url: jdbc:mysql://localhost:3307/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + 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&allowPublicKeyRetrieval=true username: root password: root # 从库数据源 @@ -58,11 +60,13 @@ spring: lazy: true type: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver - #url: jdbc:mysql://localhost:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true - url: jdbc:mysql://192.168.5.121:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true + #url: jdbc:mysql://localhost:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + #url: jdbc:mysql://localhost:3307/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + url: jdbc:mysql://192.168.5.121:3306/item_retrieval_salve?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true username: root password: root sqlserver: + lazy: true type: com.zaxxer.hikari.HikariDataSource driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://192.168.5.9:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index f2d985f..840db38 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -30,7 +30,6 @@ server: servlet: # 应用的访问路径 context-path: / - undertow: # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 max-http-post-size: -1 # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 @@ -71,7 +70,7 @@ spring: # 国际化资源文件路径 basename: i18n/messages profiles: - active: '@profiles.active@' + active: ${SPRING_PROFILES_ACTIVE:dev} # 文件上传 servlet: multipart: @@ -99,6 +98,15 @@ spring: # 允许对象忽略json中不存在的属性 fail_on_unknown_properties: false +plm: + pdf: + base-dir: D:/plm/pdf + separator: "" + auth: + app-key: plm + app-secret: change-me + username: plm_bot + # Sa-Token配置 sa-token: # token名称 (同时也是cookie名称) @@ -296,6 +304,8 @@ management: logfile: external-file: ./logs/sys-console.log dingtalk: + # 钉钉消息中转服务地址(本服务 POST {base}/ding/send) + message-relay-base-url: http://192.168.5.121:8089 app-key: dingebfrpzqko25vo8w6 app-secret: M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk robot-code: dingebfrpzqko25vo8w6 @@ -310,3 +320,15 @@ dingtalk: route-approval-id: 01201038164023402380 plan-user-id: 313514580637707245,192530665123549546 sync-k3-notify-user-ids: 313514580637707245,351225426738742993,192530665123549546 + # 技术方案申请表 OA 模板 processCode(钉钉后台复制);留空则不处理 BPMS 事件 + scheme-application-process-code: PROC-81A79B4D-F0EA-4353-9A91-68FD7E3BCEBB + # 整单审批结束且 result=agree 时拉取表单(推荐,与 event.json 顶层 result=agree 一致) + scheme-bpms-on-instance-agree: true + # 单审批任务结束且 result=agree 时拉取(会多次触发;任务节点可能出现 audit 等非 agree) + scheme-bpms-on-task-agree: false + # 日志中只输出这些表单控件名称(逗号分隔);留空则输出全部非空字段 + scheme-application-log-field-names: 方案提交日期,是否收到方案,任务详情 + +pdf: + # PDF字体(支持 classpath: 前缀) + font-path: classpath:font/arial unicode ms.ttf diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/DingTalkProperties.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/DingTalkProperties.java index c169f62..5046ec5 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/config/DingTalkProperties.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/DingTalkProperties.java @@ -78,6 +78,31 @@ public class DingTalkProperties { */ private String syncK3NotifyUserIds; + /** + * 消息中转服务根地址(如 http://host:8089,不要带尾斜杠也可),为空则不调用中转发送 + */ + private String messageRelayBaseUrl; + + /** + * 技术方案申请表 OA 模板 processCode(如 PROC-xxx);为空则不处理 BPMS 审批事件 + */ + private String schemeApplicationProcessCode = ""; + + /** + * 是否在 bpms_instance_change 整单结束且 result=agree 时拉取审批详情(推荐) + */ + private boolean schemeBpmsOnInstanceAgree = true; + + /** + * 是否在 bpms_task_change 单任务结束且 result=agree 时拉取审批详情(会多次触发,默认关闭) + */ + private boolean schemeBpmsOnTaskAgree = false; + + /** + * 拉取详情后日志中输出的控件名称(逗号分隔);为空则输出全部非空表单字段 + */ + private String schemeApplicationLogFieldNames = ""; + /** * 回调路由 Key */ diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingTalkStreamService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingTalkStreamService.java index 1df5d51..465d90e 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingTalkStreamService.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingTalkStreamService.java @@ -11,15 +11,19 @@ import org.springframework.stereotype.Component; import shade.com.alibaba.fastjson2.JSONObject; import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; /** * 钉钉 Stream 模式事件推送服务 */ @Component +@RequiredArgsConstructor public class DingTalkStreamService { private static final Logger log = LoggerFactory.getLogger(DingTalkStreamService.class); + private final com.ruoyi.common.dingding.workflow.SchemeApplicationBpmsEventHandler schemeApplicationBpmsEventHandler; + // 钉钉应用的 AppKey private static final String CLIENT_ID = "dingebfrpzqko25vo8w6"; // 钉钉应用的 AppSecret @@ -57,6 +61,7 @@ public class DingTalkStreamService { log.info("BornTime: {}", bornTime); log.info("BizData: {}", bizData != null ? bizData.toString() : "null"); + schemeApplicationBpmsEventHandler.handleEventPayload(eventType, bizData != null ? bizData.toJSONString() : "{}"); //处理事件 process(bizData); //消费成功 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java index 2f730cd..204ebed 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java @@ -934,7 +934,14 @@ public class DingUtil { } } - - + /** + * 使用 Token 初始化账号 Client(OA 审批 / workflow) + */ + public static com.aliyun.dingtalkworkflow_1_0.Client createWorkflowClient() throws Exception { + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkworkflow_1_0.Client(config); + } } diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml index 9efa0bb..a67dc08 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml +++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml @@ -4,7 +4,7 @@ spring: application: name: ruoyi-monitor-admin profiles: - active: @profiles.active@ + active: ${SPRING_PROFILES_ACTIVE:dev} logging: config: classpath:logback-plus.xml diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application-dev.yml b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application-dev.yml index 78e390f..a77aa81 100644 --- a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application-dev.yml +++ b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application-dev.yml @@ -9,12 +9,14 @@ spring.boot.admin.client: username: ruoyi password: 123456 ---- # 数据库配置 +--- # 数据库配置(与 ruoyi-admin dev 同一实例;本机 5.7 多为 3306,MySQL8 多为 3307) spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai + #url: jdbc:mysql://localhost:3306/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + url: jdbc:mysql://localhost:3307/item_retrieval-evo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true + #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&allowPublicKeyRetrieval=true username: root password: root hikari: diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml index 6b53fbe..d9d438b 100644 --- a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml +++ b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: application: name: ruoyi-xxl-job-admin profiles: - active: @profiles.active@ + active: ${SPRING_PROFILES_ACTIVE:dev} mvc: servlet: load-on-startup: 0 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java index 0636a5c..a58068f 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java @@ -84,6 +84,8 @@ public class BomDetailsController extends BaseController { private final IProcessOrderProService iProcessOrderProService; + private final IProductionOrderService iProductionOrderService; + private final ProcessOrderProMapper processOrderProMapper; private final BomDetailsMapper bomDetailsMapper; @@ -390,12 +392,17 @@ public class BomDetailsController extends BaseController { if (bomDetails != null && !bomDetails.isEmpty()) { //TODO 处理父级物料 ProcessOrderPro bo = iProcessOrderProService.selectByProjectNumber(totalWeight); - // 第一步:处理所有物料的保存 for (BomDetails material : bomDetails) { // 获取工艺表中的非委外工时 Double fbWorkTime = iProcessRouteService.getFbWorkTime(material); if (material.getPartNumber() != null && material.getName() != null && material.getUnitWeight().equals("否")) { + if (material.getPartNumber().startsWith("015") || material.getPartNumber().startsWith("009")) { + return R.fail("015和009为管控物料,请先申请"); + } + if (material.getRemarks() != null && material.getRemarks().contains("甲供")) { + continue; + } String state = determineState(material); //判断bom类型是生产还是电气 if (material.getBomType().equals("0")) { @@ -437,13 +444,13 @@ public class BomDetailsController extends BaseController { if (needUpload) { try { // 物料清单保存方法,判断是电气还是生产 - BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(bomDetails, bo,groupName); + BOMUploadResult bomUploadResult = FBloadBillOfMaterialsPreservation(bomDetails, bo, groupName); if (bomUploadResult.isSuccess()) { KindegeeLogDTO logDTO = new KindegeeLogDTO(); logDTO.setProjectCode(bo.getProductionOrderNo()); logDTO.setMaterialCode(fnumber); // 使用物料编码而不是单个物料的编码 logDTO.setCode("200"); - logDTO.setReason("成功上传"+"版本号:"+bomUploadResult.getNumber()); + logDTO.setReason("成功上传" + "版本号:" + bomUploadResult.getNumber()); logDTOS.add(logDTO); } else { KindegeeLogDTO logDTO = new KindegeeLogDTO(); @@ -498,8 +505,8 @@ public class BomDetailsController extends BaseController { } //查询工艺路线数量 LambdaQueryWrapper wqroute = new LambdaQueryWrapper<>(); - wqroute.eq(ProcessRoute::getRouteDescription,totalWeight) - .eq(ProcessRoute::getProcessNo,"10"); + wqroute.eq(ProcessRoute::getRouteDescription, totalWeight) + .eq(ProcessRoute::getProcessNo, "10"); Long routeCount = routeMapper.selectCount(wqroute); // 计算统计数据 int bomCount = fNumberCount + partdiagramCodeCount; @@ -771,7 +778,7 @@ public class BomDetailsController extends BaseController { } // FBOM物料清单保存 - public BOMUploadResult FBloadBillOfMaterialsPreservation(List bomlist, ProcessOrderPro bo,String groupName) { + public BOMUploadResult FBloadBillOfMaterialsPreservation(List bomlist, ProcessOrderPro bo, String groupName) { BomDetails bomDetails1 = bomlist.get(0); int verification = isMaterialVerification(bomDetails1.getFNumber(), bomDetails1.getFName()); @@ -928,7 +935,20 @@ public class BomDetailsController extends BaseController { JsonObject FSVRIAssistant = new JsonObject(); FSVRIAssistant.addProperty("FNumber", materialProperties.getMaterialAttributeId()); model.add("F_SVRI_Assistant", FSVRIAssistant); - + // 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null + Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetails.getPartNumber()); + if (dz == null) { + if (bomDetails.getDanZhong() != null && bomDetails.getSingleWeghit() != null) { + dz = bomDetails.getSingleWeghit(); + } else if (bomDetails.getDanZhong() != null) { + dz = bomDetails.getDanZhong(); + } else if (bomDetails.getSingleWeghit() != null) { + dz = bomDetails.getSingleWeghit(); + } + } + if (dz != null) { + model.addProperty("F_HBYT_DZ", dz); + } // 创建FMaterialGroup对象,并加入Model JsonObject fMaterialGroup = new JsonObject(); fMaterialGroup.addProperty("FNumber", "XM-100"); @@ -946,6 +966,9 @@ public class BomDetailsController extends BaseController { // 创建FCategoryID对象,并加入SubHeadEntity JsonObject fCategoryID = new JsonObject(); + if(bomDetails.getFNumber().contains("-MP")){ + fCategoryID.addProperty("FNumber", "007"); + } fCategoryID.addProperty("FNumber", "CHLB05_SYS"); subHeadEntity.add("FCategoryID", fCategoryID); @@ -1094,7 +1117,7 @@ public class BomDetailsController extends BaseController { /* * FB父级物料保存接口 */ - public static int loadFBMaterialPreservation(BomDetails bomDetail) { + public int loadFBMaterialPreservation(BomDetails bomDetail) { K3CloudApi client = new K3CloudApi(); // 创建一个空的JsonObject JsonObject json = new JsonObject(); @@ -1117,7 +1140,20 @@ public class BomDetailsController extends BaseController { model.add("FMaterialGroup", fMaterialGroup); model.addProperty("FIsHandleReserve", true); - + // 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null + Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetail.getPartNumber()); + if (dz == null) { + if (bomDetail.getDanZhong() != null && bomDetail.getSingleWeghit() != null) { + dz = bomDetail.getSingleWeghit(); + } else if (bomDetail.getDanZhong() != null) { + dz = bomDetail.getDanZhong(); + } else if (bomDetail.getSingleWeghit() != null) { + dz = bomDetail.getSingleWeghit(); + } + } + if (dz != null) { + model.addProperty("F_HBYT_DZ", dz); + } // 创建SubHeadEntity对象,并加入Model JsonObject subHeadEntity = new JsonObject(); model.add("SubHeadEntity", subHeadEntity); @@ -1127,6 +1163,9 @@ public class BomDetailsController extends BaseController { // 创建FCategoryID对象,并加入SubHeadEntity JsonObject fCategoryID = new JsonObject(); + if (bomDetail.getFNumber().contains("-MP")){ + fCategoryID.addProperty("FNumber", "007");// + } fCategoryID.addProperty("FNumber", "CHLB05_SYS");// 产成品 subHeadEntity.add("FCategoryID", fCategoryID); @@ -1512,11 +1551,19 @@ public class BomDetailsController extends BaseController { json.add("Model", model); model.addProperty("FMATERIALID", 0); - //单重 - if (!(bomDetails1.getDanZhong() == null) && !(bomDetails1.getSingleWeghit() == null)) { - model.addProperty("F_HBYT_DZ", bomDetails1.getSingleWeghit()); - } else { - model.addProperty("F_HBYT_DZ", bomDetails1.getDanZhong()); + // 单重:与 Excel 批量匹配逻辑一致(production_order.drawing_no → process_route.material_code);图号用物料编码 partNumber;库中未查到则为 null + Double dz = iProductionOrderService.resolveSingleWeightByMaterialCode(bomDetails1.getPartNumber()); + if (dz == null) { + if (bomDetails1.getDanZhong() != null && bomDetails1.getSingleWeghit() != null) { + dz = bomDetails1.getSingleWeghit(); + } else if (bomDetails1.getDanZhong() != null) { + dz = bomDetails1.getDanZhong(); + } else if (bomDetails1.getSingleWeghit() != null) { + dz = bomDetails1.getSingleWeghit(); + } + } + if (dz != null) { + model.addProperty("F_HBYT_DZ", dz); } model.addProperty("FSpecification", bomDetails1.getRemarks()); model.addProperty("FNumber", bomDetails1.getPartNumber()); @@ -1544,9 +1591,9 @@ public class BomDetailsController extends BaseController { subHeadEntity.addProperty("FErpClsID", states); subHeadEntity.addProperty("FFeatureItem", "1"); - + //存货类别 JsonObject fCategoryID = new JsonObject(); - if (states.equals("2") || states.equals("3")) { + if (states.equals("2") || states.equals("3")||bomDetails1.getPartNumber().contains("-MP")) { fCategoryID.addProperty("FNumber", "007"); } else { fCategoryID.addProperty("FNumber", "006"); @@ -1834,7 +1881,7 @@ public class BomDetailsController extends BaseController { // 创建SubHeadEntity对象,并加入Model JsonObject subHeadEntity = new JsonObject(); model.add("SubHeadEntity", subHeadEntity); - if (bomDetails1.getWareHouse().contains("伊特")){ + if (bomDetails1.getWareHouse().contains("伊特")) { subHeadEntity.addProperty("FErpClsID", "2"); } subHeadEntity.addProperty("FErpClsID", "1"); @@ -2260,7 +2307,7 @@ public class BomDetailsController extends BaseController { List list = new ArrayList<>(); if (rows != null) { for (ElectricalMaterialBomVO bomVO : rows) { - if(bomVO.getRemark().equals("研发已领用")|| bomVO.getRemark().contains("领用")){ + if (bomVO.getRemark() != null && bomVO.getRemark().contains("领用")) { continue; } BomDetails bomDetails = new BomDetails(); @@ -2271,6 +2318,9 @@ public class BomDetailsController extends BaseController { bomDetails.setName(bomVO.getDrawingName()); bomDetails.setMaterial(bomVO.getModel()); bomDetails.setWareHouse(bomVO.getBrand()); + if(bomVO.getBrand().contains("伊特")){ + bomDetails.setStats("自制"); + } bomDetails.setStats("外购"); bomDetails.setQuantity(bomVO.getQuantity() == null ? null : bomVO.getQuantity().toString()); bomDetails.setRemarks(bomVO.getUnit()); @@ -2320,6 +2370,7 @@ public class BomDetailsController extends BaseController { } return materialsToAdd; } + @Log(title = "查看BOM上传日志") @SaCheckPermission("system:route:viewGetBomUploadStatus") @PostMapping("/viewGetBomUploadStatus") diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/EleMaterialsController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/EleMaterialsController.java index 408d1a8..fce4ed5 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/EleMaterialsController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/EleMaterialsController.java @@ -13,9 +13,13 @@ import java.util.stream.Collectors; import cn.hutool.core.date.DateUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.kingdee.bos.webapi.entity.SuccessEntity; import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.excel.ExcelResult; import com.ruoyi.common.utils.JdUtils; import com.ruoyi.system.domain.EleMaterials; @@ -453,43 +457,103 @@ public class EleMaterialsController extends BaseController { } - @Log(title = "更新物料单重", businessType = BusinessType.IMPORT) + @Log(title = "更新物料单重", businessType = BusinessType.UPDATE) @SaCheckPermission("system:route:updateDanzhong") - @PostMapping(value = "/updateDanzhong", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public R updateDanzhong(@RequestParam("file") MultipartFile file) throws Exception { - String originalFilename = file.getOriginalFilename(); - log.info("读取文件名: " + originalFilename); - ExcelResult result = ExcelUtil.importExcelSheet1(file.getInputStream(), JdDanZhong.class, true); - List list = result.getList(); + @PostMapping("/updateDanzhong") + public R updateDanzhong() { + final int pageSize = 500; + int pageNum = 1; + long totalRows = 0; + long skippedDisabled = 0; + long skippedNoWeight = 0; + long submitted = 0; + int failedTasks = 0; - // 创建固定大小的线程池 ExecutorService executor = Executors.newFixedThreadPool(10); + List> futures = new ArrayList<>(); - list.forEach(vmi -> executor.submit(() -> { - List entryID = JdUtil.getEntryID2(vmi.getMaterialCode()); - entryID.forEach(jdEntryVmi -> { - int fmaterialid = jdEntryVmi.getFMATERIALID(); - - try { - log.info("=====================>开始更新 " + vmi.getMaterialCode() + " 的单重"); - JdUtil.updateDanzhong(fmaterialid,vmi.getDanzhong() ); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - })); - - // 关闭线程池并等待所有任务完成 - executor.shutdown(); try { - if (!executor.awaitTermination(60, TimeUnit.MINUTES)) { - executor.shutdownNow(); // 超时后强制关闭 + while (true) { + LambdaQueryWrapper qw = Wrappers.lambdaQuery(); + qw.isNotNull(ImMaterial::getMaterialCode); + qw.ne(ImMaterial::getMaterialCode, ""); + qw.and(w -> w.isNull(ImMaterial::getForbidStatus).or().ne(ImMaterial::getForbidStatus, "B")); + qw.orderByAsc(ImMaterial::getId); + Page page = materialMapper.selectPage(new Page<>(pageNum, pageSize), qw); + List rows = page.getRecords(); + if (rows.isEmpty()) { + break; + } + for (ImMaterial m : rows) { + totalRows++; + if (m == null || StringUtils.isBlank(m.getMaterialCode())) { + continue; + } + if (StringUtils.isNotBlank(m.getForbidStatus()) && "B".equalsIgnoreCase(m.getForbidStatus().trim())) { + skippedDisabled++; + continue; + } + if (m.getSingleWeight() == null) { + skippedNoWeight++; + continue; + } + final String materialCode = m.getMaterialCode().trim(); + final String danzhongStr = m.getSingleWeight().stripTrailingZeros().toPlainString(); + + futures.add(executor.submit(() -> { + List entryID = JdUtil.getEntryID2(materialCode); + if (entryID == null || entryID.isEmpty()) { + log.warn("金蝶未查到物料编码: {}", materialCode); + return null; + } + for (JdEntryVmi jdEntryVmi : entryID) { + int fmaterialid = jdEntryVmi.getFMATERIALID(); + try { + log.info("=====================>开始更新 {} 的单重(来自 im_material)", materialCode); + JdUtil.updateDanzhong(fmaterialid, danzhongStr); + } catch (Exception e) { + log.error("更新单重失败 materialCode={}, fmaterialid={}", materialCode, fmaterialid, e); + throw new RuntimeException(e); + } + } + return null; + })); + submitted++; + } + if (rows.size() < pageSize) { + break; + } + pageNum++; + } + + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException e) { + failedTasks++; + log.error("更新物料单重任务异常: {}", e.getCause() != null ? e.getCause().getMessage() : e.getMessage(), e); + } } } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); // 保留中断状态 + Thread.currentThread().interrupt(); + log.error("更新物料单重被中断", e); + return R.fail("任务被中断"); + } finally { + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.MINUTES)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } } + log.info("updateDanzhong 完成: 扫描行={}, 提交金蝶任务={}, 跳过禁用={}, 跳过无单重={}, 失败任务={}", totalRows, submitted, skippedDisabled, skippedNoWeight, failedTasks); + if (failedTasks > 0) { + return R.fail(String.format("部分失败: 失败任务数=%d,详见日志", failedTasks)); + } return R.ok("更新成功"); } @@ -542,8 +606,6 @@ public class EleMaterialsController extends BaseController { log.info("读取文件名: " + originalFilename); ExcelResult result = ExcelUtil.importExcelSheet1(file.getInputStream(), JdDanZhong.class, true); List list = result.getList(); - - return R.ok("更新成功"); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ImMaterialController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ImMaterialController.java index fbfb98d..6ce04ac 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ImMaterialController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ImMaterialController.java @@ -21,17 +21,26 @@ import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.excel.ExcelResult; import com.ruoyi.common.utils.JdUtils; +import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.domain.ImMaterial; import com.ruoyi.system.domain.bo.ImMaterialBo; +import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO; +import com.ruoyi.system.domain.dto.buildcapacity.CalcInfo; +import com.ruoyi.system.domain.dto.buildcapacity.MaterialItem; +import com.ruoyi.system.domain.dto.buildcapacity.StockInfo; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO; +import com.ruoyi.system.domain.vo.BuildCapacityMaterialExportVo; import com.ruoyi.system.domain.vo.ImMaterialVo; import com.ruoyi.system.mapper.ImMaterialMapper; import com.ruoyi.system.service.IImMaterialService; import com.ruoyi.system.service.IPartCostService; import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.Data; import lombok.RequiredArgsConstructor; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -43,8 +52,15 @@ import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import static com.ruoyi.common.core.mapper.BaseMapperPlus.log; @@ -72,6 +88,12 @@ public class ImMaterialController extends BaseController { private final ImMaterialMapper imMaterialMapper; private final IPartCostService iPartCostService; + + @Value("${plm.pdf.base-dir:D:/plm/pdf}") + private String plmPdfBaseDir; + + @Value("${plm.pdf.separator:}") + private String plmPdfSeparator; /** * 金蝶物料保存 → 自动推送到此接口 * SaIgnore 注解用于开放此接口,无需 token 验证 @@ -114,6 +136,426 @@ public class ImMaterialController extends BaseController { } } + @SaIgnore + @PostMapping(value = "/plm/pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R> receivePlmPdfDrawing( + @RequestParam("productionOrderNo") String productionOrderNo, + @RequestParam("materialCode") String materialCode, + @RequestParam("materialName") String materialName, + @RequestParam(value = "pdfFileName", required = false) String pdfFileName, + @RequestPart("file") MultipartFile file + ) { + if (StringUtils.isBlank(productionOrderNo)) { + return R.fail("生产令号不能为空"); + } + if (StringUtils.isBlank(materialCode)) { + return R.fail("物料编码不能为空"); + } + if (StringUtils.isBlank(materialName)) { + return R.fail("物料名称不能为空"); + } + if (file == null || file.isEmpty()) { + return R.fail("PDF文件不能为空"); + } + + String originalName = file.getOriginalFilename(); + String finalPdfFileName = StringUtils.isNotBlank(pdfFileName) ? pdfFileName : originalName; + if (!isPdfFileName(finalPdfFileName)) { + return R.fail("仅支持上传PDF文件"); + } + + String safeProductionOrderNo = sanitizePathPart(productionOrderNo); + String safeMaterialCode = sanitizeFileNamePart(materialCode); + String safeMaterialName = sanitizeFileNamePart(materialName); + String separator = StringUtils.trimToEmpty(plmPdfSeparator); + + String targetBaseName = safeMaterialCode + separator + safeMaterialName; + Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo); + try { + Files.createDirectories(dir); + Path targetFile = resolveAvailablePdfPath(dir, targetBaseName); + try (InputStream in = file.getInputStream()) { + Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + + Map resp = new HashMap<>(8); + resp.put("productionOrderNo", productionOrderNo); + resp.put("materialCode", materialCode); + resp.put("materialName", materialName); + resp.put("pdfFileName", finalPdfFileName); + resp.put("savedFileName", targetFile.getFileName().toString()); + resp.put("savedDir", dir.toString()); + resp.put("savedPath", targetFile.toString()); + return R.ok(resp); + } catch (Exception e) { + log.error("接收PLM PDF图纸失败: productionOrderNo={}, materialCode={}", productionOrderNo, materialCode, e); + return R.fail("接收PLM PDF图纸失败: " + e.getMessage()); + } + } + + @SaIgnore + @PostMapping(value = "/plm/pdf/batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R> receivePlmPdfDrawingsBatch( + @RequestParam("productionOrderNo") String productionOrderNo, + @RequestPart("items") String itemsJson, + @RequestPart("files") MultipartFile[] files + ) { + if (StringUtils.isBlank(productionOrderNo)) { + return R.fail("生产令号不能为空"); + } + if (StringUtils.isBlank(itemsJson)) { + return R.fail("items不能为空"); + } + if (files == null || files.length == 0) { + return R.fail("files不能为空"); + } + + List items; + try { + items = new ObjectMapper().readValue(itemsJson, new TypeReference>() {}); + } catch (JsonProcessingException e) { + return R.fail("items不是合法JSON: " + e.getOriginalMessage()); + } + if (items == null || items.isEmpty()) { + return R.fail("items不能为空"); + } + + Map metaByFileName = new HashMap<>(items.size() * 2); + for (PlmPdfBatchItem item : items) { + if (item == null) { + continue; + } + String pdfName = normalizeOriginalName(item.getPdfFileName()); + if (StringUtils.isBlank(pdfName)) { + return R.fail("items中pdfFileName不能为空"); + } + if (StringUtils.isBlank(item.getMaterialCode())) { + return R.fail("items中materialCode不能为空: " + pdfName); + } + if (StringUtils.isBlank(item.getMaterialName())) { + return R.fail("items中materialName不能为空: " + pdfName); + } + if (!isPdfFileName(pdfName)) { + return R.fail("items中仅支持PDF文件名: " + pdfName); + } + if (metaByFileName.containsKey(pdfName)) { + return R.fail("items中存在重复pdfFileName: " + pdfName); + } + metaByFileName.put(pdfName, item); + } + + Set fileNames = new HashSet<>(files.length * 2); + List duplicateFiles = new ArrayList<>(); + List filesWithoutMeta = new ArrayList<>(); + for (MultipartFile f : files) { + if (f == null || f.isEmpty()) { + continue; + } + String fn = normalizeOriginalName(f.getOriginalFilename()); + if (StringUtils.isBlank(fn)) { + return R.fail("files中存在无文件名的上传项"); + } + if (!isPdfFileName(fn)) { + return R.fail("仅支持上传PDF文件: " + fn); + } + if (!fileNames.add(fn)) { + duplicateFiles.add(fn); + } + if (!metaByFileName.containsKey(fn)) { + filesWithoutMeta.add(fn); + } + } + if (!duplicateFiles.isEmpty()) { + return R.fail("files中存在重复文件名: " + String.join(", ", duplicateFiles)); + } + if (!filesWithoutMeta.isEmpty()) { + return R.fail("以下文件未在items中声明: " + String.join(", ", filesWithoutMeta)); + } + + List missingFiles = new ArrayList<>(); + for (String metaFileName : metaByFileName.keySet()) { + if (!fileNames.contains(metaFileName)) { + missingFiles.add(metaFileName); + } + } + if (!missingFiles.isEmpty()) { + return R.fail("以下items声明的文件未上传: " + String.join(", ", missingFiles)); + } + + String safeProductionOrderNo = sanitizePathPart(productionOrderNo); + String separator = StringUtils.trimToEmpty(plmPdfSeparator); + Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo); + + List> saved = new ArrayList<>(files.length); + try { + Files.createDirectories(dir); + for (MultipartFile f : files) { + if (f == null || f.isEmpty()) { + continue; + } + String fn = normalizeOriginalName(f.getOriginalFilename()); + PlmPdfBatchItem item = metaByFileName.get(fn); + String safeMaterialCode = sanitizeFileNamePart(item.getMaterialCode()); + String safeMaterialName = sanitizeFileNamePart(item.getMaterialName()); + String targetBaseName = safeMaterialCode + separator + safeMaterialName; + Path targetFile = resolveAvailablePdfPath(dir, targetBaseName); + try (InputStream in = f.getInputStream()) { + Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + Map row = new HashMap<>(6); + row.put("pdfFileName", fn); + row.put("materialCode", item.getMaterialCode()); + row.put("materialName", item.getMaterialName()); + row.put("savedFileName", targetFile.getFileName().toString()); + row.put("savedDir", dir.toString()); + row.put("savedPath", targetFile.toString()); + saved.add(row); + } + + Map resp = new HashMap<>(6); + resp.put("productionOrderNo", productionOrderNo); + resp.put("savedDir", dir.toString()); + resp.put("count", saved.size()); + resp.put("items", saved); + return R.ok(resp); + } catch (Exception e) { + log.error("批量接收PLM PDF图纸失败: productionOrderNo={}", productionOrderNo, e); + return R.fail("批量接收PLM PDF图纸失败: " + e.getMessage()); + } + } + + @SaIgnore + @PostMapping(value = "/plm/pdf/zip", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R> receivePlmPdfDrawingsZip( + @RequestPart("manifest") String manifestJson, + @RequestPart("zip") MultipartFile zipFile + ) { + if (StringUtils.isBlank(manifestJson)) { + return R.fail("manifest不能为空"); + } + if (zipFile == null || zipFile.isEmpty()) { + return R.fail("zip不能为空"); + } + + PlmPdfManifest manifest; + try { + manifest = new ObjectMapper().readValue(manifestJson, PlmPdfManifest.class); + } catch (JsonProcessingException e) { + return R.fail("manifest不是合法JSON: " + e.getOriginalMessage()); + } + if (manifest == null || StringUtils.isBlank(manifest.getProductionOrderNo())) { + return R.fail("manifest.productionOrderNo不能为空"); + } + if (manifest.getItems() == null || manifest.getItems().isEmpty()) { + return R.fail("manifest.items不能为空"); + } + + String productionOrderNo = manifest.getProductionOrderNo(); + String safeProductionOrderNo = sanitizePathPart(productionOrderNo); + String separator = StringUtils.trimToEmpty(plmPdfSeparator); + Path dir = Paths.get(plmPdfBaseDir, safeProductionOrderNo); + + Map itemByPdfPath = new LinkedHashMap<>(manifest.getItems().size() * 2); + for (PlmPdfManifestItem item : manifest.getItems()) { + if (item == null) { + continue; + } + String pdfPath = normalizeZipEntryName(item.getPdfPath()); + if (StringUtils.isBlank(pdfPath)) { + return R.fail("manifest.items[].pdfPath不能为空"); + } + if (!isPdfFileName(pdfPath)) { + return R.fail("manifest.items[].pdfPath仅支持PDF: " + pdfPath); + } + if (StringUtils.isBlank(item.getMaterialCode())) { + return R.fail("manifest.items[].materialCode不能为空: " + pdfPath); + } + if (StringUtils.isBlank(item.getMaterialName())) { + return R.fail("manifest.items[].materialName不能为空: " + pdfPath); + } + if (itemByPdfPath.containsKey(pdfPath)) { + return R.fail("manifest中存在重复pdfPath: " + pdfPath); + } + itemByPdfPath.put(pdfPath, item); + } + + Path tempZip = null; + try { + Files.createDirectories(dir); + tempZip = Files.createTempFile("plm-pdf-", ".zip"); + try (InputStream in = zipFile.getInputStream()) { + Files.copy(in, tempZip, StandardCopyOption.REPLACE_EXISTING); + } + + Map pdfEntryByPath = new HashMap<>(itemByPdfPath.size() * 2); + Set extraPdfFiles = new LinkedHashSet<>(); + try (ZipFile zf = new ZipFile(tempZip.toFile())) { + Enumeration entries = zf.entries(); + while (entries.hasMoreElements()) { + ZipEntry ze = entries.nextElement(); + if (ze == null || ze.isDirectory()) { + continue; + } + String name = normalizeZipEntryName(ze.getName()); + if (StringUtils.isBlank(name)) { + continue; + } + if (!isPdfFileName(name)) { + continue; + } + if (pdfEntryByPath.containsKey(name)) { + return R.fail("zip中存在重复文件路径: " + name); + } + pdfEntryByPath.put(name, ze); + if (!itemByPdfPath.containsKey(name)) { + extraPdfFiles.add(name); + } + } + + List missing = new ArrayList<>(); + for (String pdfPath : itemByPdfPath.keySet()) { + if (!pdfEntryByPath.containsKey(pdfPath)) { + missing.add(pdfPath); + } + } + if (!missing.isEmpty()) { + return R.fail("zip缺少以下PDF文件: " + String.join(", ", missing)); + } + if (!extraPdfFiles.isEmpty()) { + return R.fail("zip包含未在manifest声明的PDF文件: " + String.join(", ", extraPdfFiles)); + } + + List> saved = new ArrayList<>(itemByPdfPath.size()); + for (Map.Entry kv : itemByPdfPath.entrySet()) { + String pdfPath = kv.getKey(); + PlmPdfManifestItem item = kv.getValue(); + ZipEntry ze = pdfEntryByPath.get(pdfPath); + String safeMaterialCode = sanitizeFileNamePart(item.getMaterialCode()); + String safeMaterialName = sanitizeFileNamePart(item.getMaterialName()); + String targetBaseName = safeMaterialCode + separator + safeMaterialName; + Path targetFile = resolveAvailablePdfPath(dir, targetBaseName); + try (InputStream in = zf.getInputStream(ze)) { + Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + + Map row = new HashMap<>(7); + row.put("pdfPath", pdfPath); + row.put("materialCode", item.getMaterialCode()); + row.put("materialName", item.getMaterialName()); + row.put("savedFileName", targetFile.getFileName().toString()); + row.put("savedDir", dir.toString()); + row.put("savedPath", targetFile.toString()); + saved.add(row); + } + + Map resp = new HashMap<>(6); + resp.put("productionOrderNo", productionOrderNo); + resp.put("savedDir", dir.toString()); + resp.put("count", saved.size()); + resp.put("items", saved); + return R.ok(resp); + } + } catch (Exception e) { + log.error("接收PLM PDF压缩包失败: productionOrderNo={}", manifest.getProductionOrderNo(), e); + return R.fail("接收PLM PDF压缩包失败: " + e.getMessage()); + } finally { + if (tempZip != null) { + try { + Files.deleteIfExists(tempZip); + } catch (Exception ignored) { + } + } + } + } + + private static boolean isPdfFileName(String fileName) { + if (StringUtils.isBlank(fileName)) { + return false; + } + return fileName.trim().toLowerCase(Locale.ROOT).endsWith(".pdf"); + } + + private static String normalizeOriginalName(String fileName) { + String s = StringUtils.trimToEmpty(fileName); + if (StringUtils.isBlank(s)) { + return s; + } + s = s.replace("\\", "/"); + int idx = s.lastIndexOf('/'); + if (idx >= 0 && idx + 1 < s.length()) { + s = s.substring(idx + 1); + } + return s.trim(); + } + + private static String normalizeZipEntryName(String entryName) { + String s = StringUtils.trimToEmpty(entryName); + if (StringUtils.isBlank(s)) { + return s; + } + s = s.replace("\\", "/"); + while (s.startsWith("/")) { + s = s.substring(1); + } + while (s.startsWith("./")) { + s = s.substring(2); + } + if (s.contains("..")) { + throw new IllegalArgumentException("非法zip路径: " + entryName); + } + return s.trim(); + } + + private static String sanitizePathPart(String input) { + String s = StringUtils.trimToEmpty(input); + s = s.replace("\\", "_").replace("/", "_"); + return sanitizeFileNamePart(s); + } + + private static String sanitizeFileNamePart(String input) { + String s = StringUtils.trimToEmpty(input); + s = s.replaceAll("[\\\\/:*?\"<>|]", "_"); + s = s.replaceAll("\\s+", " "); + s = s.trim(); + if (s.length() > 120) { + s = s.substring(0, 120); + } + if (StringUtils.isBlank(s)) { + return "_"; + } + return s; + } + + private static Path resolveAvailablePdfPath(Path dir, String targetBaseName) { + String base = sanitizeFileNamePart(targetBaseName); + Path p = dir.resolve(base + ".pdf"); + if (!Files.exists(p)) { + return p; + } + String suffix = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + return dir.resolve(base + "-" + suffix + ".pdf"); + } + + @Data + private static class PlmPdfBatchItem { + private String pdfFileName; + private String materialCode; + private String materialName; + } + + @Data + private static class PlmPdfManifest { + private String productionOrderNo; + private List items; + } + + @Data + private static class PlmPdfManifestItem { + private String pdfPath; + private String materialCode; + private String materialName; + } + /** * 查询物料检索列表 */ @@ -186,6 +628,16 @@ public class ImMaterialController extends BaseController { return toAjax(iImMaterialService.deleteWithValidByIds(Arrays.asList(ids), true)); } + /** + * 手动触发:全量分页刷新 im_material 单重(按物料编码解析);仅单重变化时更新 updateTime、modify_date + */ + @SaCheckPermission("system:material:edit") + @Log(title = "刷新物料单重", businessType = BusinessType.UPDATE) + @PostMapping("/refreshSingleWeightFromOrders") + public R> refreshSingleWeightFromOrders() { + return iImMaterialService.refreshImMaterialSingleWeightFromOrders(); + } + @PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) { ExcelUtil.exportExcel(new ArrayList<>(), "用户数据", ImMaterialBo.class, response); @@ -485,6 +937,91 @@ public class ImMaterialController extends BaseController { public TableDataInfo search(@RequestParam(value = "query", required = false) String query) { return iImMaterialService.search(query); } + + /** + * 齐套物料查询 + */ + @SaCheckPermission("system:material:buildCapacity") + @Log(title = "齐套物料", businessType = BusinessType.EXPORT) + @GetMapping("/buildCapacity") + public R buildCapacity(@RequestParam("productionOrderNo") String productionOrderNo, + @RequestParam("productCode") String productCode, + @RequestParam(value = "productName", required = false) String productName) { + return R.ok(iImMaterialService.queryBuildCapacity(productionOrderNo, productCode, productName)); + } + + @SaCheckPermission("system:material:exportBuildCapacity") + @Log(title = "齐套物料导出", businessType = BusinessType.EXPORT) + @PostMapping("/buildCapacity/export") + public void exportBuildCapacity(@RequestParam("productionOrderNo") String productionOrderNo, + @RequestParam("productCode") String productCode, + @RequestParam(value = "productName", required = false) String productName, + HttpServletResponse response) { + BuildCapacityDTO dto = iImMaterialService.queryBuildCapacity(productionOrderNo, productCode, productName); + List rows = new ArrayList<>(); + if (dto != null) { + appendBuildCapacityRows(rows, productionOrderNo, dto, "自制", dto.getSelfMadeMaterials()); + appendBuildCapacityRows(rows, productionOrderNo, dto, "采购", dto.getPurchaseMaterials()); + } + ExcelUtil.exportExcel(rows, "齐套物料", BuildCapacityMaterialExportVo.class, response); + } + + @GetMapping("/buildCapacityByProductionOrderNo") + public R> buildCapacityByProductionOrderNo(@RequestParam("productionOrderNo") String productionOrderNo) { + return R.ok(iImMaterialService.queryBuildCapacityByProductionOrderNo(productionOrderNo)); + } + + @GetMapping("/productionOrderMaterials") + public R productionOrderMaterials(@RequestParam("productionOrderNo") String productionOrderNo, + @RequestParam("productCode") String productCode) { + return R.ok(iImMaterialService.queryProductionOrderMaterials(productionOrderNo, productCode)); + } + + private static void appendBuildCapacityRows(List out, + String productionOrderNo, + BuildCapacityDTO dto, + String materialType, + List items) { + if (items == null || items.isEmpty()) { + return; + } + for (MaterialItem item : items) { + if (item == null) { + continue; + } + BuildCapacityMaterialExportVo row = new BuildCapacityMaterialExportVo(); + row.setProductionOrderNo(productionOrderNo); + row.setProductCode(dto.getProductCode()); + row.setProductName(dto.getProductName()); + row.setMaxBuildQty(dto.getMaxBuildQty()); + row.setMaterialType(materialType); + row.setMaterialCode(item.getMaterialCode()); + row.setMaterialName(item.getMaterialName()); + row.setRequiredQty(item.getRequiredQty()); + row.setMaterial(item.getMaterial()); + row.setSingleWeight(item.getSingleWeight()); + row.setTotalWeight(item.getTotalWeight()); + row.setRemark(item.getRemark()); + row.setBatchQuantity(item.getBatchQuantity()); + + StockInfo stock = item.getStock(); + if (stock != null) { + row.setOnHandQty(stock.getOnHandQty()); + row.setPoNotInQty(stock.getPoNotInQty()); + row.setMoNotInQty(stock.getMoNotInQty()); + row.setMoNotPickQty(stock.getMoNotPickQty()); + } + CalcInfo calc = item.getCalc(); + if (calc != null) { + row.setAvailableQty(calc.getAvailableQty()); + row.setFinalAvailableQty(calc.getFinalAvailableQty()); + row.setCanSupportQty(calc.getCanSupportQty()); + } + row.setStatus(item.getStatus() != null ? item.getStatus().name() : null); + out.add(row); + } + } + @SaIgnore @PostMapping("/getObtainPartData2") public R getObtainPartData() { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java index 79bbb36..5af4c27 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.ruoyi.common.excel.DefaultExcelListener; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.FtpUtil; +import com.ruoyi.common.utils.HttpUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.VersionComparator; import com.ruoyi.common.utils.file.SmbUtil; @@ -32,6 +33,7 @@ import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.SafetyStockMapper; import com.ruoyi.system.runner.JdUtil; import com.ruoyi.system.service.*; +import com.ruoyi.system.utils.ProductionOrderNoUtils; import lombok.RequiredArgsConstructor; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; @@ -40,6 +42,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaIgnore; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -244,7 +247,14 @@ public class ProcessOrderProController extends BaseController { String s = iProcessOrderProService.executDrawing(orderPro); return R.ok(s); } - + @GetMapping("/getstate") + public R getPlotState() { + String result = HttpUtils.sendGet("http://192.168.5.18:9000/getstate", "UTF-8"); + if (StringUtils.isBlank(result)) { + return R.ok("操作成功", null); + } + return R.ok("操作成功", JSON.parseObject(result)); + } @GetMapping("/executDrawing/progress/{id}") public SseEmitter executDrawingProgress(@PathVariable Long id) { return iProcessOrderProService.startDrawingSse(id); @@ -933,8 +943,8 @@ public class ProcessOrderProController extends BaseController { vo.setBatchQuantity(getCellValueAsString(row.getCell(8))); // 批次数量 vo.setParentPart(getCellValueAsString(row.getCell(9))); // 上级部件 vo.setParentDrawingNo(getCellValueAsString(row.getCell(10))); // 上级部件图号 - vo.setMainProducts(getCellValueAsString(row.getCell(11))); // 主产品图号 - vo.setMainProductsName(getCellValueAsString(row.getCell(12))); // 主产品名称 + vo.setMainPart(getCellValueAsString(row.getCell(11))); // 主产品图号 + vo.setMainName(getCellValueAsString(row.getCell(12))); // 主产品名称 resultList.add(vo); } @@ -1095,8 +1105,8 @@ public class ProcessOrderProController extends BaseController { } map.put("batchQuantityInt", batchQuantityInt); - map.put("mainProducts", item.getMainProducts()); - map.put("mainProductsName", item.getMainProductsName()); + map.put("mainProducts", item.getMainPart()); + map.put("mainProductsName", item.getMainName()); mapList.add(map); } return mapList; @@ -1882,6 +1892,25 @@ public class ProcessOrderProController extends BaseController { return mapList; } + /** + * 出图完成后由前端触发:先推送「出图完成」Markdown,再生成工艺计划表 Excel 并发至钉钉 notifyUserId。 + */ + @SaCheckPermission("system:orderPro:sendProcessRoutePlanExcel") + @Log(title = "发送工艺计划表", businessType = BusinessType.OTHER) + @PostMapping("/sendProcessRoutePlanExcel/{id}") + public R sendProcessRoutePlanExcel(@NotNull(message = "项目ID不能为空") @PathVariable Long id) { + return iProcessOrderProService.sendProcessRoutePlanExcel(id); + } + + /** + * 查询工艺计划表钉钉发送成功次数(用于按钮文案) + */ + @SaIgnore + @GetMapping("/routePlanSendCount/{id}") + public R getRoutePlanSendCount(@NotNull(message = "项目ID不能为空") @PathVariable Long id) { + return iProcessOrderProService.getRoutePlanSendCount(id); + } + @SaCheckPermission("system:orderPro:exportRoute3") @Log(title = "下载工艺生产表3", businessType = BusinessType.EXPORT) @GetMapping ("/exportRoute3") @@ -1925,30 +1954,28 @@ public class ProcessOrderProController extends BaseController { return generateRoute3Excel(id, false); } + /** 去掉 Windows 文件名非法字符,避免生成或钉钉展示异常 */ + private static String sanitizeFileNameSegment(String name) { + if (StringUtils.isBlank(name)) { + return "unknown"; + } + String trimmed = name.trim(); + String illegal = "\\/:*?\"<>|"; + StringBuilder sb = new StringBuilder(trimmed.length()); + for (int i = 0; i < trimmed.length(); i++) { + char c = trimmed.charAt(i); + sb.append(illegal.indexOf(c) >= 0 ? '_' : c); + } + String out = sb.toString(); + return out.isEmpty() ? "unknown" : out; + } + public String generateRoute3Excel(Long id, boolean isDrawing) throws Exception { ProcessOrderPro orderPro = processOrderProMapper.selectById(id); // 下载Excel文件 // 1. 解析年份 String productionCode = orderPro.getProductionOrderNo(); - String year = "2025"; // 默认 - if (productionCode != null) { - if (productionCode.startsWith("EY")) { - if (productionCode.length() >= 4) { - try { - Integer.parseInt(productionCode.substring(2, 4)); - year = "20" + productionCode.substring(2, 4); - } catch (Exception e) {} - } - } else { - int firstDash = productionCode.indexOf("-"); - if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) { - try { - Integer.parseInt(productionCode.substring(firstDash + 1, firstDash + 3)); - year = "20" + productionCode.substring(firstDash + 1, firstDash + 3); - } catch (Exception e) {} - } - } - } + String year = ProductionOrderNoUtils.extractYear(productionCode); // 2. 构建FTP路径和本地路径 String ftpPath = "/" + year + "/" + productionCode; @@ -2313,7 +2340,11 @@ public class ProcessOrderProController extends BaseController { // 使用Excel模板文件 String templatePath = "jpg/生产及工艺计划模版.xlsx"; - String outputPath = "D:/file/" + orderPro.getProductionOrderNo() + "生产及工艺计划表.xlsx"; + String safeOrderNo = sanitizeFileNameSegment(orderPro.getProductionOrderNo()); + // 钉钉发送场景用「令号+工艺表」,避免过长中文后缀在客户端显示异常;浏览器导出仍为「生产及工艺计划表」 + String outputPath = isDrawing + ? ("D:/file/" + safeOrderNo + "工艺表.xlsx") + : ("D:/file/" + safeOrderNo + "生产及工艺计划表.xlsx"); // 准备模板数据 Map staticDataMap = new HashMap<>(); @@ -2393,7 +2424,7 @@ public class ProcessOrderProController extends BaseController { return; } // 检查 quantity 字段(假设你的数量字段名是 quantity 或者 unitQuantity 等,这里假设是 getQuantity(),如果不对请调整为对应的 get 方法) - if (orderPro.getQuantity() != null && orderPro.getQuantity().intValue() == 0) { + if (orderPro.getIsDownload() != null && orderPro.getIsDownload().intValue() == 0) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write(JSONObject.toJSONString(R.fail("当前图纸数量不允许下载"))); return; diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessRouteController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessRouteController.java index 0907acf..5b8c4de 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessRouteController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessRouteController.java @@ -40,6 +40,8 @@ import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.ProcessRouteMapper; import com.ruoyi.system.runner.JdUtil; import com.ruoyi.system.service.*; +import com.ruoyi.system.service.support.OperationPlanningWorkshopUpdateService; +import com.ruoyi.system.service.support.OperationReportWorkshopUpdateService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +64,8 @@ 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; /** * 工艺路线 @@ -76,6 +80,8 @@ import java.util.stream.Collectors; public class ProcessRouteController extends BaseController { private final ProcessRouteMapper baseMapper; private final IProcessRouteService iProcessRouteService; + private final OperationPlanningWorkshopUpdateService operationPlanningWorkshopUpdateService; + private final OperationReportWorkshopUpdateService operationReportWorkshopUpdateService; private final IBomDetailsService iBomDetailsService; private final ProcessOrderProMapper proMapper; private final IImMaterialService materialService; @@ -603,6 +609,15 @@ public class ProcessRouteController extends BaseController { if (!zipFile.exists()) { throw new FileNotFoundException("ZIP 文件未找到: " + zipFilePath); } + if (zipFile.length() <= 0) { + throw new IOException("ZIP 文件为空: " + zipFilePath); + } + try (ZipFile zf = new ZipFile(zipFile)) { + Enumeration entries = zf.entries(); + if (entries == null || !entries.hasMoreElements()) { + throw new IOException("ZIP 文件中没有PDF: " + zipFilePath); + } + } FileInputStream fis = new FileInputStream(zipFile); OutputStream outputStreams = response.getOutputStream(); response.setContentType("application/zip"); @@ -624,6 +639,46 @@ public class ProcessRouteController extends BaseController { return iProcessRouteService.updateProcessPlan(rooteProdet); } + /** + * 按车间+物料默认条件拉取工序计划,回填工艺时间,保存时部门统一为 018(与 {@link OperationPlanningWorkshopUpdateService} 一致)。 + */ + @Log(title = "按车间物料更新工序计划(默认条件)") + @SaCheckPermission("system:route:updateProcessPlan") + @PostMapping("/updateProcessPlanByWorkshop") + public R> updateProcessPlanByWorkshop() throws Exception { + return R.ok(operationPlanningWorkshopUpdateService.updateProcessPlanByWorkshopFilter()); + } + + /** + * 同上,BillQuery 过滤条件可自定义(部门 Compare/Value、物料 Compare/Value)。 + */ + @Log(title = "按车间物料更新工序计划(自定义条件)") + @SaCheckPermission("system:route:updateProcessPlan") + @PostMapping("/updateProcessPlanByWorkshopCustom") + public R> updateProcessPlanByWorkshopCustom( + @RequestParam String departmentCompare, + @RequestParam String departmentNumber, + @RequestParam String productCompare, + @RequestParam String productFilterValue) throws Exception { + return R.ok(operationPlanningWorkshopUpdateService.updateProcessPlanByWorkshopFilter( + departmentCompare, departmentNumber, productCompare, productFilterValue)); + } + + @Log(title = "按车间物料更新工序汇报单(默认条件)") + @SaCheckPermission("system:route:updateProcessPlan") + @PostMapping("/updateOperationReportByWorkshop") + public R> updateOperationReportByWorkshop() throws Exception { + return R.ok(operationReportWorkshopUpdateService.updateOperationReportByWorkshopFilter()); + } + + @Log(title = "按车间物料更新工序汇报单(固定条件67/007/17/-MP)") + @SaCheckPermission("system:route:updateProcessPlan") + @PostMapping("/updateOperationReportByWorkshopCustom") + public R> updateOperationReportByWorkshopCustom() throws Exception { + return R.ok(operationReportWorkshopUpdateService.updateOperationReportByWorkshopFilter( + "67", "007", "17", "-MP")); + } + /** * 更新生产订单的时间 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionOrderController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionOrderController.java index 1b4bef5..d76dda6 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionOrderController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionOrderController.java @@ -3,6 +3,8 @@ package com.ruoyi.system.controller; import java.util.*; import com.ruoyi.system.domain.ProductionOrder; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO; import com.ruoyi.system.domain.dto.PurchaseOrderExcelDTO; import com.ruoyi.system.domain.dto.PurchaseRequestExcelDTO; import com.ruoyi.system.runner.JdUtil; @@ -138,6 +140,19 @@ public class ProductionOrderController extends BaseController { return R.ok(null); } + /** + * 测试方法:导入物料编码后批量匹配单重并导出 + * Excel列:物料编码、物料名称、单重 + */ + @Log(title = "物料单重匹配导出", businessType = BusinessType.IMPORT) + @SaCheckPermission("system:order:import") + @PostMapping(value = "/testExportWeightByMaterialCode", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public void testExportWeightByMaterialCode(@RequestPart("file") MultipartFile file, HttpServletResponse response) throws Exception { + List importRows = ExcelUtil.importExcel(file.getInputStream(), ProductionOrderWeightImportDTO.class); + List exportRows = iProductionOrderService.matchSingleWeightByMaterialCode(importRows); + ExcelUtil.exportExcel(exportRows, "物料单重匹配结果", ProductionOrderWeightExportDTO.class, response); + } + /** * 获取组焊件bom列表 * @param productionOrderVo diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionPlanController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionPlanController.java index 650b377..89d6a27 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionPlanController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProductionPlanController.java @@ -12,6 +12,7 @@ import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.domain.FigureSave; +import com.ruoyi.system.domain.ProductionOrder; import com.ruoyi.system.domain.ProductionPlan; import com.ruoyi.system.domain.bo.ProductionPlanBo; import com.ruoyi.system.domain.vo.ProductionPlanVo; @@ -20,6 +21,7 @@ import com.ruoyi.system.mapper.FigureSaveMapper; import com.ruoyi.system.mapper.ProductionPlanMapper; import com.ruoyi.system.runner.JdUtil; import com.ruoyi.system.service.IFigureSaveService; +import com.ruoyi.system.service.IProductionOrderService; import com.ruoyi.system.service.IProductionPlanService; import lombok.RequiredArgsConstructor; import org.apache.poi.ss.usermodel.*; @@ -51,6 +53,7 @@ import java.util.List; public class ProductionPlanController extends BaseController { private final IFigureSaveService iFigureSaveService; private final IProductionPlanService iProductionPlanService; + private final IProductionOrderService iProductionOrderService; @Resource private ProductionPlanMapper productionPlanMapper; @Resource @@ -210,6 +213,26 @@ public class ProductionPlanController extends BaseController { return R.ok("状态更新成功"); } + @SaCheckPermission("system:plan:productionOrderNoOptions") + @Log(title = "查询生产令号及主产品,预配套使用", businessType = BusinessType.UPDATE) + @GetMapping("/productionOrderNoOptions") + public R> productionOrderNoOptions(@RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "limit", required = false) Integer limit) { + return R.ok("ok", iProductionOrderService.selectProductionOrderNoOptions(keyword, limit)); + } + + /** + * 根据生产令号查询所有的装配bom + * + * @param productionOrderNo 生产令号 + */ + @SaCheckPermission("system:plan:selectByProCode") + @Log(title = "排产计划", businessType = BusinessType.UPDATE) + @PostMapping("/selectByProCode") + public R> selectByProCode(@RequestParam("productionOrderNo") String productionOrderNo) { + List productionOrders = iProductionOrderService.selectByProCode(productionOrderNo.trim()); + return R.ok("ok", productionOrders); + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/SfcOperationPlanningMainController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/SfcOperationPlanningMainController.java index 2b17766..c0c0c5e 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/SfcOperationPlanningMainController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/SfcOperationPlanningMainController.java @@ -46,8 +46,7 @@ public class SfcOperationPlanningMainController extends BaseController { /** * 同步工序计划数据 */ - @SaIgnore - //@SaCheckPermission("system:operationPlanningMain:sync") + @SaCheckPermission("system:operationPlanningMain:sync") @Log(title = "工序计划数据同步", businessType = BusinessType.INSERT) @PostMapping("/sync") public R syncData() { @@ -57,7 +56,7 @@ public class SfcOperationPlanningMainController extends BaseController { /** * 直接从K3查询工序计划数据(不入库测试) */ - @SaIgnore + @SaCheckPermission("system:operationPlanningMain:queryK3") @GetMapping("/queryFromK3") public R> queryFromK3() { return R.ok(iSfcOperationPlanningMainService.queryFromK3Directly()); @@ -66,13 +65,13 @@ public class SfcOperationPlanningMainController extends BaseController { /** * 分页查询工序计划主及明细级联数据 */ - @SaIgnore + @SaCheckPermission("system:operationPlanningMain:list") @GetMapping("/cascadeList") public TableDataInfo cascadeList(SfcOperationPlanningMainBo bo, PageQuery pageQuery) { return iSfcOperationPlanningMainService.queryCascadePageList(bo, pageQuery); } - @SaIgnore + @SaCheckPermission("system:operationPlanningMain:updateOperators") @Log(title = "工序作业人员/设备更新", businessType = BusinessType.UPDATE) @PutMapping("/updateOperators") public R updateOperators(@Validated @RequestBody OperationPlanningUpdateOperatorsReq req) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProcessOrderPro.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProcessOrderPro.java index fe6c1b0..7c586e8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProcessOrderPro.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProcessOrderPro.java @@ -33,7 +33,10 @@ public class ProcessOrderPro extends BaseEntity { * 是否延期:0否 1是 */ private Integer whetherDelay; - + /** + * 是否可以下载 + */ + private Integer isDownload; /** * 延期天数 */ @@ -134,4 +137,9 @@ public class ProcessOrderPro extends BaseEntity { */ private String notificationCardId; + /** + * 工艺计划表(钉钉)发送次数,每次发送成功 +1 + */ + private Integer routePlanSendCount; + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProductionOrder.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProductionOrder.java index 7c2c60e..eb8ed19 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProductionOrder.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ProductionOrder.java @@ -66,6 +66,14 @@ public class ProductionOrder extends BaseEntity { * 部件图号 */ private String parentDrawingNo; + /** + * 主产品名称 + */ + private String mainName; + /** + * 主产品图号 + */ + private String mainPart; /** * 批次数量 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SfcOperationPlanningMain.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SfcOperationPlanningMain.java index 92a2d2d..9365b28 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SfcOperationPlanningMain.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SfcOperationPlanningMain.java @@ -42,6 +42,9 @@ public class SfcOperationPlanningMain extends BaseEntity { * 生产令号 */ private String hbytSclh; + + @TableField("hbyt_xmh") + private String fHbytXmh; /** * 产品编码 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProcessOrderProBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProcessOrderProBo.java index a3fe3b1..69f99d0 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProcessOrderProBo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProcessOrderProBo.java @@ -81,6 +81,10 @@ public class ProcessOrderProBo extends BaseEntity { */ private List isEnterpriseStandardList; + /** + * 是否可以下载 + */ + private Integer isDownload; private String drawingPath; /** diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProductionOrderBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProductionOrderBo.java index e5394f3..ac10a86 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProductionOrderBo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/ProductionOrderBo.java @@ -64,4 +64,14 @@ public class ProductionOrderBo extends BaseEntity { * 批次数量 */ private String batchQuantity; + + /** + * 主产品名称 + */ + private String mainName; + + /** + * 主产品图号 + */ + private String mainPart; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SfcOperationPlanningMainBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SfcOperationPlanningMainBo.java index 3b8f61a..6c58664 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SfcOperationPlanningMainBo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SfcOperationPlanningMainBo.java @@ -1,5 +1,6 @@ package com.ruoyi.system.domain.bo; +import com.fasterxml.jackson.annotation.JsonProperty; import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.validate.AddGroup; import com.ruoyi.common.core.validate.EditGroup; @@ -50,7 +51,8 @@ public class SfcOperationPlanningMainBo extends BaseEntity { */ @NotBlank(message = "生产令号不能为空", groups = { AddGroup.class, EditGroup.class }) private String hbytSclh; - + @NotBlank(message = "项目号不能为空", groups = { AddGroup.class, EditGroup.class }) + private String fHbytXmh; /** * 产品编码 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ConstructionDelayDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ConstructionDelayDTO.java index f6d3fcc..102eae2 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ConstructionDelayDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ConstructionDelayDTO.java @@ -8,7 +8,7 @@ import java.util.Date; @Data public class ConstructionDelayDTO { //生产令号 - @JsonProperty("F_HBYT_SCLH") + @JsonProperty("F_HBYT_XMH") private String F_HBYT_SCLH; //单据编码 @JsonProperty("FBillNo") diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanResultDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanResultDTO.java index d6edebf..ae8efe2 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanResultDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanResultDTO.java @@ -18,6 +18,8 @@ public class OperationPlanResultDTO { */ private String batchNo; + private String projectNo; + /** * 产品编码 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanningDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanningDTO.java index be2414b..d8ce7ec 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanningDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/OperationPlanningDTO.java @@ -25,7 +25,11 @@ public class OperationPlanningDTO { * 生产令号 */ @JsonProperty("F_HBYT_SCLH") - private String fHbytSclh; + private String fHbytSclh;/** + * 生产令号 + */ + @JsonProperty("F_HBYT_XMH") + private String fHbytXmh; /** * 生产订单编号 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ProcessDetailDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ProcessDetailDTO.java index d7597e0..5554d0e 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ProcessDetailDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ProcessDetailDTO.java @@ -58,6 +58,60 @@ public class ProcessDetailDTO { */ private Double wastageQty; + /** + * 综合看板:统计日(请求参数 boardReportDay,默认服务器当天)内金蝶工序汇报汇总与本工序号匹配的完工数量合计。 + */ + private Double dayReportFinishQty; + + /** 同上:合格数量合计 */ + private Double dayReportQuaQty; + + /** 同上:废品/不良数量合计 */ + private Double dayReportFailQty; + + /** + * 综合看板:生产入库数量。当前追溯数据未按工序拆分,仅在该工序计划主档下工序号最大的一道工序上回填本生产订单入库数量合计,其余工序为 null。 + */ + private Double operationInstockQty; + + // ---------- 综合看板:金蝶「工序委外转移」报表并入工序明细(按 boardReportDay 切分) ---------- + + /** + * 单据日期 < boardReportDay 的委外发出数量累计(多行合并)。 + * 仅当本工序被识别为委外/外协相关且匹配到报表行时有值,否则为 null。 + */ + private Double subcontractLeaveQtyBeforeReportDay; + + /** + * 单据日期 < boardReportDay 的委外接收数量累计。 + */ + private Double subcontractEnterQtyBeforeReportDay; + + /** + * 单据日期 < boardReportDay 的委外合格数量累计。 + */ + private Double subcontractQualifiedQtyBeforeReportDay; + + /** + * 单据日期 = boardReportDay 当天的委外发出合计(可多笔合并)。 + */ + private Double subcontractLeaveQtyOnReportDay; + + /** + * 单据日期 = boardReportDay 当天的委外接收合计。 + */ + private Double subcontractEnterQtyOnReportDay; + + /** + * 单据日期 = boardReportDay 当天的委外合格合计。 + */ + private Double subcontractQualifiedQtyOnReportDay; + + /** + * 统计日当天委外发出 + 接收之和(便于前端单列展示;等于 subcontractLeaveQtyOnReportDay + subcontractEnterQtyOnReportDay,无当日数据时为 null)。 + */ + private Double subcontractLeaveEnterSumOnReportDay; + /** * 加工车间 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BottleneckMaterial.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BottleneckMaterial.java new file mode 100644 index 0000000..d04a2ea --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BottleneckMaterial.java @@ -0,0 +1,13 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +import lombok.Data; + +@Data +public class BottleneckMaterial { + + private String materialCode; + + private String materialName; + + private Integer canSupportQty; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BuildCapacityDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BuildCapacityDTO.java new file mode 100644 index 0000000..cb8245c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/BuildCapacityDTO.java @@ -0,0 +1,34 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +import lombok.Data; + +import java.util.List; + +@Data +public class BuildCapacityDTO { + + /** + * 成品编码(前端选中的成品/产品编码) + */ + private String productCode; + + /** + * 成品名称(可选,仅用于展示) + */ + private String productName; + + /** + * 最大可生产套数(取所有物料行 calc.canSupportQty 的最小值) + */ + private Integer maxBuildQty; + + /** + * 自制物料明细(物料编码非 009 开头) + */ + private List selfMadeMaterials; + + /** + * 采购物料明细(物料编码以 009 开头) + */ + private List purchaseMaterials; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/CalcInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/CalcInfo.java new file mode 100644 index 0000000..9225847 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/CalcInfo.java @@ -0,0 +1,13 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +import lombok.Data; + +@Data +public class CalcInfo { + + private Integer availableQty; + + private Integer finalAvailableQty; + + private Integer canSupportQty; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/MaterialItem.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/MaterialItem.java new file mode 100644 index 0000000..4d75889 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/MaterialItem.java @@ -0,0 +1,29 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +import lombok.Data; + +@Data +public class MaterialItem { + + private String materialCode; + + private String materialName; + + private Integer requiredQty; + + private String material; + + private Double singleWeight; + + private Double totalWeight; + + private String remark; + + private String batchQuantity; + + private StockInfo stock; + + private CalcInfo calc; + + private StockStatus status; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockInfo.java new file mode 100644 index 0000000..595c97f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockInfo.java @@ -0,0 +1,27 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +import lombok.Data; + +@Data +public class StockInfo { + + /** + * 即时库存(当前可用库存台账数量) + */ + private Integer onHandQty; + + /** + * 采购未入库数量 + */ + private Integer poNotInQty; + + /** + * 生产订单未入库数量 + */ + private Integer moNotInQty; + + /** + * 生产订单未领料数量 + */ + private Integer moNotPickQty; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockStatus.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockStatus.java new file mode 100644 index 0000000..d10ad8f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/buildcapacity/StockStatus.java @@ -0,0 +1,6 @@ +package com.ruoyi.system.domain.dto.buildcapacity; + +public enum StockStatus { + OK, + SHORTAGE +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/ProductInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/ProductInfo.java index e5e1d6a..97bda91 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/ProductInfo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/ProductInfo.java @@ -1,6 +1,8 @@ package com.ruoyi.system.domain.dto.excuteDrawing; import lombok.Data; +import java.util.List; + @Data public class ProductInfo { //型号名称 @@ -11,4 +13,8 @@ public class ProductInfo { private String number; //变量信息 private DataInfo vars; + //是否保留零件图 + private boolean preservepartdrawing; + //企标图子件信息 + private List subparts; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/FigureSaveVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/FigureSaveVo.java index a6b06a0..a05cb64 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/FigureSaveVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/FigureSaveVo.java @@ -108,4 +108,16 @@ public class FigureSaveVo { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date uploadTime; + + + /** + * 创建人 + */ + private String createBy; + + + /** + * 更新人 + */ + private String updateBy; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PlanOrderVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PlanOrderVo.java index 58e5ac4..799d107 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PlanOrderVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PlanOrderVo.java @@ -32,6 +32,12 @@ public class PlanOrderVo { @ExcelProperty(value = "生产令号") @JsonProperty("F_HBYT_SCLH") private String fHbytSclh; + /** + * 生产令号 + */ + @ExcelProperty(value = "生产令号") + @JsonProperty("F_HBYT_XMH") + private String fHbytXmh; /** * 生产订单编号 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProcessOrderProVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProcessOrderProVo.java index d06a21f..b7ebab7 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProcessOrderProVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProcessOrderProVo.java @@ -44,7 +44,11 @@ public class ProcessOrderProVo { @ExcelProperty(value = "项目名称") private String productionName; - + /** + * 是否可以下载 + */ + @ExcelProperty(value = "是否可以下载") + private Integer isDownload; /** * 计划结束时间 */ @@ -185,4 +189,10 @@ public class ProcessOrderProVo { */ private Long delayDay; + /** + * 工艺计划表(钉钉)发送次数 + */ + @ExcelProperty(value = "工艺计划表发送次数") + private Integer routePlanSendCount; + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProductionOrderVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProductionOrderVo.java index 7caefdc..7a95de2 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProductionOrderVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ProductionOrderVo.java @@ -86,16 +86,16 @@ public class ProductionOrderVo { @ExcelProperty(value = "上级部件图号") private String parentDrawingNo; - /** - * 主产品图号 - */ - @ExcelProperty(value = "主产品图号") - private String mainProducts; - /** * 主产品名称 */ @ExcelProperty(value = "主产品名称") - private String mainProductsName; + private String mainName; + + /** + * 主产品图号 + */ + @ExcelProperty(value = "主产品图号") + private String mainPart; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SfcOperationPlanningMainVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SfcOperationPlanningMainVo.java index e904f12..d1374e5 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SfcOperationPlanningMainVo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SfcOperationPlanningMainVo.java @@ -50,6 +50,11 @@ public class SfcOperationPlanningMainVo { */ @ExcelProperty(value = "生产令号") private String hbytSclh; + /** + * 项目号 + */ + @ExcelProperty(value = "项目号") + private String fHbytXmh;; /** * 产品编码 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/FSubEntity.java b/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/FSubEntity.java index 395f5f3..18670f5 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/FSubEntity.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/FSubEntity.java @@ -19,6 +19,13 @@ public class FSubEntity { @JsonProperty("FOperNumber") private Long FOperNumber; + /** + * 加工车间(部门),与 {@link F_HBYT_RKCK} 相同写法:JSON 为 + * {@code "FDepartmentId": { "FNumber": "018" }},对应金蝶字段路径 {@code FDepartmentId.FNumber}。 + */ + @JsonProperty("FWorkShopID") + private FWorkShopID FWorkShopID; + @JsonProperty("F_HBYT_ZYRY") private String F_HBYT_ZYRY; @@ -31,6 +38,12 @@ public class FSubEntity { @JsonProperty("F_HBYT_RKCK") private F_HBYT_RKCK F_HBYT_RKCK; + @Data + public static class FWorkShopID { + @JsonProperty("FNUMBER") + private String FNUMBER; + } + @Data public static class F_HBYT_RKCK { @JsonProperty("FNUMBER") diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/Model.java b/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/Model.java index d47ddc2..6b4a97b 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/Model.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/jdmain/rouplan/Model.java @@ -20,9 +20,10 @@ public class Model { @JsonProperty("FPlanFinishTime") private Date FPlanFinishTime; + @JsonProperty("FEntity") private List FEntity = new ArrayList<>(); - // getters and setters + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProductionOrderMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProductionOrderMapper.java index 1791778..cd66d79 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProductionOrderMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProductionOrderMapper.java @@ -4,6 +4,10 @@ import com.ruoyi.system.domain.ProductionOrder; import com.ruoyi.system.domain.vo.ProductionOrderVo; import com.ruoyi.common.core.mapper.BaseMapperPlus; import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; /** * 生产订单Mapper接口 @@ -14,4 +18,23 @@ import org.apache.ibatis.annotations.Delete; public interface ProductionOrderMapper extends BaseMapperPlus { @Delete("DELETE FROM production_order where production_order_no = #{productionOrderNo}") void deleteWithProCode(String productionOrderNo); + + @Select("SELECT DISTINCT production_order_no " + + "FROM production_order " + + "WHERE production_order_no IS NOT NULL " + + " AND production_order_no <> '' " + + " AND (#{keyword} IS NULL OR #{keyword} = '' OR production_order_no LIKE CONCAT('%', #{keyword}, '%')) " + + "ORDER BY production_order_no DESC " + + "LIMIT #{limit}") + List selectDistinctProductionOrderNos(@Param("keyword") String keyword, @Param("limit") Integer limit); + + /** + * 按图号批量查询单重 + */ + List selectWeightByDrawingNos(@Param("drawingNos") List drawingNos); + + /** + * 按物料编码批量查询工艺路线单重 + */ + List selectDiscWeightByMaterialCodes(@Param("materialCodes") List materialCodes); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java index 67ef168..e16d7fe 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JdUtil.java @@ -1,5 +1,6 @@ package com.ruoyi.system.runner; +import cn.hutool.core.date.DateUtil; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.core.type.TypeReference; @@ -541,6 +542,298 @@ public class JdUtil { return 0.0; } + /** + * 金蝶报表部分数值列以字符串返回,且可能含千分位逗号(如 1,176),需规范化后再转 BigDecimal。 + */ + private static String normalizeKingdeeNumericString(String raw) { + if (raw == null) { + return ""; + } + return raw.trim() + .replace(",", "") + .replace(",", "") + .replace("\u00A0", "") + .trim(); + } + + private static BigDecimal getBigDecimalFromJsonArray(JsonArray array, int index) { + if (array.size() <= index || array.get(index).isJsonNull()) { + return null; + } + JsonElement el = array.get(index); + String raw; + if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isNumber()) { + raw = el.getAsJsonPrimitive().getAsString(); + } else { + raw = el.getAsString(); + } + if (raw == null || raw.isEmpty()) { + return null; + } + String value = normalizeKingdeeNumericString(raw); + if (value.isEmpty()) { + return null; + } + try { + return new BigDecimal(value); + } catch (NumberFormatException e) { + log.warn("报表数量解析失败(规范化后仍非法): raw={} normalized={}", raw, value); + return null; + } + } + + /** + * 工序汇报汇总报表(系统报表 GetSysReportData)。 + * FieldKeys 顺序需与 {@link KingdeeOperationReportSumRowDTO} 字段一致(默认 11 列)。 + * + * @param formId 报表 FormId(金蝶报表属性中查看) + * @param fieldKeys 输出列,逗号分隔 + * @param beginDateTime FBeginDate,如 2026-04-10 00:00:00 + * @param endDateTime FEndDate,如 2026-05-10 23:59:59 + * @param processOrgNumber 加工组织 FNumber,如 100 + * @param timeUnit Model.FTimeUnit,如 B 按日 + * @param moBillNo 可选,单个生产订单号过滤 FMOBillNo + * @param limitPerPage 每页 Limit,内部会分页累加 + */ + public static List getOperationReportSumRpt( + String formId, + String fieldKeys, + String beginDateTime, + String endDateTime, + String processOrgNumber, + String timeUnit, + String moBillNo, + int limitPerPage) { + List all = new ArrayList<>(); + if (formId == null || formId.isEmpty()) { + log.warn("工序汇报汇总报表 FormId 为空"); + return all; + } + int limit = Math.min(Math.max(limitPerPage, 1), 10000); + K3CloudApi client = new K3CloudApi(); + Gson gson = new Gson(); + + JsonObject template = new JsonObject(); + template.addProperty("FieldKeys", fieldKeys != null && !fieldKeys.isEmpty() ? fieldKeys + : "FDEPARTMENTID,FDATE,FMOBillNo,FOpOper,FWORKCENTERID,FPROCESSID,FPRODUCTNUMBER,FMATERIALNAME,FPRDFINISHQTY,FPRDQUAQTY,FPRDFAILQTY"); + template.addProperty("SchemeId", ""); + template.addProperty("IsVerifyBaseDataField", "true"); + + JsonObject model = new JsonObject(); + JsonObject fProcessOrgId = new JsonObject(); + fProcessOrgId.addProperty("FNumber", processOrgNumber != null && !processOrgNumber.isEmpty() ? processOrgNumber : "100"); + model.add("FProcessOrgId", fProcessOrgId); + model.addProperty("FBeginDate", beginDateTime); + model.addProperty("FEndDate", endDateTime); + model.addProperty("FTimeUnit", timeUnit != null && !timeUnit.isEmpty() ? timeUnit : "B"); + template.add("Model", model); + + JsonArray filterString = new JsonArray(); + if (moBillNo != null && !moBillNo.trim().isEmpty()) { + JsonObject f = new JsonObject(); + f.addProperty("Left", ""); + f.addProperty("FieldName", "FMOBillNo"); + f.addProperty("Compare", "67"); + f.addProperty("Value", moBillNo.trim()); + f.addProperty("Right", ""); + f.addProperty("Logic", 0); + filterString.add(f); + } + template.add("FilterString", filterString); + + int startRow = 0; + try { + while (true) { + JsonObject page = gson.fromJson(template.toString(), JsonObject.class); + page.addProperty("StartRow", startRow); + page.addProperty("Limit", limit); + + String resultJson = client.getSysReportData(formId, page.toString()); + if (resultJson == null || resultJson.isEmpty()) { + log.warn("工序汇报汇总报表返回空 formId={} startRow={}", formId, startRow); + break; + } + JsonObject root = new JsonParser().parse(resultJson).getAsJsonObject(); + if (!root.has("Result")) { + log.warn("工序汇报汇总报表无 Result 节点: {}", trunc(resultJson)); + break; + } + JsonObject result = root.getAsJsonObject("Result"); + if (result.has("ResponseStatus")) { + JsonObject rs = result.getAsJsonObject("ResponseStatus"); + if (rs.has("IsSuccess") && !rs.get("IsSuccess").getAsBoolean()) { + log.error("工序汇报汇总报表业务失败: {}", trunc(resultJson)); + break; + } + } + if (!result.has("Rows")) { + break; + } + JsonArray rows = result.getAsJsonArray("Rows"); + if (rows == null || rows.size() == 0) { + break; + } + for (JsonElement rowElement : rows) { + if (rowElement == null || rowElement.isJsonNull() || !rowElement.isJsonArray()) { + continue; + } + JsonArray row = rowElement.getAsJsonArray(); + KingdeeOperationReportSumRowDTO dto = mapOperationReportSumRow(row); + all.add(dto); + } + if (rows.size() < limit) { + break; + } + startRow += limit; + } + } catch (Exception e) { + log.error("工序汇报汇总报表调用异常 formId={}", formId, e); + } + return all; + } + + private static KingdeeOperationReportSumRowDTO mapOperationReportSumRow(JsonArray row) { + KingdeeOperationReportSumRowDTO dto = new KingdeeOperationReportSumRowDTO(); + dto.setDepartmentId(getStringFromJsonArray(row, 0)); + dto.setReportDate(getStringFromJsonArray(row, 1)); + dto.setMoBillNo(getStringFromJsonArray(row, 2)); + dto.setOpOper(getStringFromJsonArray(row, 3)); + dto.setWorkCenterId(getStringFromJsonArray(row, 4)); + dto.setProcessId(getStringFromJsonArray(row, 5)); + dto.setProductNumber(getStringFromJsonArray(row, 6)); + dto.setMaterialName(getStringFromJsonArray(row, 7)); + dto.setFinishQty(getBigDecimalFromJsonArray(row, 8)); + dto.setQuaQty(getBigDecimalFromJsonArray(row, 9)); + dto.setFailQty(getBigDecimalFromJsonArray(row, 10)); + return dto; + } + + private static String trunc(String s) { + if (s == null) { + return ""; + } + return s.length() > 500 ? s.substring(0, 500) + "..." : s; + } + + /** + * 工序委外转移报表(系统报表 GetSysReportData)。 + * Model:FProOrgId、FStartDate、FFinishDate、FDocumentStatus。 + * Result 可能含顶层 {@code IsSuccess}(与 ResponseStatus 并存)。 + */ + public static List getSubcontractTransferRpt( + String formId, + String fieldKeys, + String startDateTime, + String finishDateTime, + String proOrgNumber, + String documentStatus, + String productOrderCode, + int limitPerPage) { + List all = new ArrayList<>(); + if (formId == null || formId.isEmpty()) { + log.warn("工序委外转移报表 FormId 为空"); + return all; + } + int limit = Math.min(Math.max(limitPerPage, 1), 10000); + K3CloudApi client = new K3CloudApi(); + Gson gson = new Gson(); + + JsonObject template = new JsonObject(); + template.addProperty("FieldKeys", fieldKeys != null && !fieldKeys.isEmpty() ? fieldKeys + : "FProductNumber,FProductName,FProviderName,FProductOrderCodes,FDates,FShiftTaskName,FLeaveQty,FEnterQty,FQualifiedQty"); + template.addProperty("SchemeId", ""); + template.addProperty("IsVerifyBaseDataField", "true"); + + JsonObject model = new JsonObject(); + JsonObject fProOrgId = new JsonObject(); + fProOrgId.addProperty("FNumber", proOrgNumber != null && !proOrgNumber.isEmpty() ? proOrgNumber : "100"); + model.add("FProOrgId", fProOrgId); + model.addProperty("FStartDate", startDateTime); + model.addProperty("FFinishDate", finishDateTime); + model.addProperty("FDocumentStatus", documentStatus != null && !documentStatus.isEmpty() ? documentStatus : "C"); + template.add("Model", model); + + JsonArray filterString = new JsonArray(); + if (productOrderCode != null && !productOrderCode.trim().isEmpty()) { + JsonObject f = new JsonObject(); + f.addProperty("Left", ""); + f.addProperty("FieldName", "FProductOrderCodes"); + f.addProperty("Compare", "67"); + f.addProperty("Value", productOrderCode.trim()); + f.addProperty("Right", ""); + f.addProperty("Logic", 0); + filterString.add(f); + } + template.add("FilterString", filterString); + + int startRow = 0; + try { + while (true) { + JsonObject page = gson.fromJson(template.toString(), JsonObject.class); + page.addProperty("StartRow", startRow); + page.addProperty("Limit", limit); + + String resultJson = client.getSysReportData(formId, page.toString()); + if (resultJson == null || resultJson.isEmpty()) { + log.warn("工序委外转移报表返回空 formId={} startRow={}", formId, startRow); + break; + } + JsonObject root = new JsonParser().parse(resultJson).getAsJsonObject(); + if (!root.has("Result")) { + log.warn("工序委外转移报表无 Result: {}", trunc(resultJson)); + break; + } + JsonObject result = root.getAsJsonObject("Result"); + if (result.has("IsSuccess") && !result.get("IsSuccess").getAsBoolean()) { + log.error("工序委外转移报表 IsSuccess=false: {}", trunc(resultJson)); + break; + } + if (result.has("ResponseStatus")) { + JsonObject rs = result.getAsJsonObject("ResponseStatus"); + if (rs.has("IsSuccess") && !rs.get("IsSuccess").getAsBoolean()) { + log.error("工序委外转移报表 ResponseStatus 失败: {}", trunc(resultJson)); + break; + } + } + if (!result.has("Rows")) { + break; + } + JsonArray rows = result.getAsJsonArray("Rows"); + if (rows == null || rows.size() == 0) { + break; + } + for (JsonElement rowElement : rows) { + if (rowElement == null || rowElement.isJsonNull() || !rowElement.isJsonArray()) { + continue; + } + JsonArray row = rowElement.getAsJsonArray(); + all.add(mapSubcontractTransferRow(row)); + } + if (rows.size() < limit) { + break; + } + startRow += limit; + } + } catch (Exception e) { + log.error("工序委外转移报表调用异常 formId={}", formId, e); + } + return all; + } + + private static KingdeeSubcontractTransferRowDTO mapSubcontractTransferRow(JsonArray row) { + KingdeeSubcontractTransferRowDTO dto = new KingdeeSubcontractTransferRowDTO(); + dto.setProductNumber(getStringFromJsonArray(row, 0)); + dto.setProductName(getStringFromJsonArray(row, 1)); + dto.setProviderName(getStringFromJsonArray(row, 2)); + dto.setProductOrderCodes(getStringFromJsonArray(row, 3)); + dto.setDocumentDate(getStringFromJsonArray(row, 4)); + dto.setShiftTaskName(getStringFromJsonArray(row, 5)); + dto.setLeaveQty(getBigDecimalFromJsonArray(row, 6)); + dto.setEnterQty(getBigDecimalFromJsonArray(row, 7)); + dto.setQualifiedQty(getBigDecimalFromJsonArray(row, 8)); + return dto; + } + /** * 查询生产订单单据体 * 查询 @@ -1597,167 +1890,6 @@ public class JdUtil { } } - //新增父级物料 - public static int loadChengPinMaterialPreservation(BomDetails bomDetail, Double fbWorkTime) { - K3CloudApi client = new K3CloudApi(); - // 创建一个空的JsonObject - JsonObject json = new JsonObject(); - - // 添加IsAutoSubmitAndAudit字段 - json.addProperty("IsAutoSubmitAndAudit", "true"); - - // 创建Model对象,并加入JsonObject - JsonObject model = new JsonObject(); - json.add("Model", model); - - // 添加Model字段 - model.addProperty("FMATERIALID", 0); - if (!(bomDetail.getDanZhong() == null)) { - model.addProperty("F_HBYT_DZ", bomDetail.getDanZhong()); - } - model.addProperty("FNumber", bomDetail.getFNumber()); - model.addProperty("FName", bomDetail.getFName()); - - // 创建FMaterialGroup对象,并加入Model - JsonObject fMaterialGroup = new JsonObject(); - fMaterialGroup.addProperty("FNumber", "A000106"); - model.add("FMaterialGroup", fMaterialGroup); - - model.addProperty("FIsHandleReserve", true); - - // 创建SubHeadEntity对象,并加入Model - JsonObject subHeadEntity = new JsonObject(); - model.add("SubHeadEntity", subHeadEntity); - - subHeadEntity.addProperty("FErpClsID", "2"); - subHeadEntity.addProperty("FFeatureItem", "1"); - - // 创建FCategoryID对象,并加入SubHeadEntity - JsonObject fCategoryID = new JsonObject(); - fCategoryID.addProperty("FNumber", "CHLB05_SYS");// 产成品 - subHeadEntity.add("FCategoryID", fCategoryID); - - // 创建FTaxRateId对象,并加入SubHeadEntity - JsonObject fTaxRateId = new JsonObject(); - fTaxRateId.addProperty("FNUMBER", "SL02_SYS"); - subHeadEntity.add("FTaxRateId", fTaxRateId); - - // 创建FBaseUnitId对象,并加入SubHeadEntity - JsonObject fBaseUnitId = new JsonObject(); - fBaseUnitId.addProperty("FNumber", "008"); - subHeadEntity.add("FBaseUnitId", fBaseUnitId); - - subHeadEntity.addProperty("FIsPurchase", true); - subHeadEntity.addProperty("FIsInventory", true); - // 成品不允许委外 - // subHeadEntity.addProperty("FIsSubContract", true); - subHeadEntity.addProperty("FIsSale", true); - subHeadEntity.addProperty("FIsProduce", true); - - // 创建SubHeadEntity1对象,并加入Model - JsonObject subHeadEntity1 = new JsonObject(); - model.add("SubHeadEntity1", subHeadEntity1); - JsonObject fStoreUnitId = new JsonObject(); - fStoreUnitId.addProperty("FNumber", "008"); - subHeadEntity1.add("FStoreUnitID", fStoreUnitId); - subHeadEntity1.addProperty("FUnitConvertDir", "1"); - - // 创建FStockId对象,并加入SubHeadEntity1 - JsonObject fStockId = new JsonObject(); - // 判断是产成品还是企标 - String cangKu = "CK011"; - fStockId.addProperty("FNumber", cangKu); - subHeadEntity1.add("FStockId", fStockId); - - // 创建FCurrencyId对象,并加入SubHeadEntity1 - JsonObject fCurrencyId = new JsonObject(); - fCurrencyId.addProperty("FNumber", "PRE001"); - subHeadEntity1.add("FCurrencyId", fCurrencyId); - - subHeadEntity1.addProperty("FIsSNPRDTracy", false); - subHeadEntity1.addProperty("FSNManageType", "1"); - subHeadEntity1.addProperty("FSNGenerateTime", "1"); - subHeadEntity1.addProperty("FBoxStandardQty", 0.0); - // 创建FPurchaseUnitId对象,并加入SubHeadEntity1 - JsonObject fPurchaseUnitId = new JsonObject(); - fPurchaseUnitId.addProperty("FNumber", "008"); - subHeadEntity1.add("FPurchaseUnitId", fPurchaseUnitId); - - // 创建SubHeadEntity3对象,并加入Model - JsonObject subHeadEntity3 = new JsonObject(); - model.add("SubHeadEntity3", subHeadEntity3); - - - // 创建FPurchasePriceUnitId对象,并加入SubHeadEntity3 - JsonObject fPurchasePriceUnitId = new JsonObject(); - fPurchasePriceUnitId.addProperty("FNumber", "008"); - subHeadEntity3.add("FPurchasePriceUnitId", fPurchasePriceUnitId); - - subHeadEntity3.addProperty("FIsQuota", false); - subHeadEntity3.addProperty("FQuotaType", "1"); - - // 创建SubHeadEntity6对象,并加入Model 检验项 - JsonObject subHeadEntity6 = new JsonObject(); - model.add("SubHeadEntity6", subHeadEntity6); - subHeadEntity6.addProperty("FCheckProduct", true); - subHeadEntity6.addProperty("FCheckReturn", true); - - // 创建SubHeadEntity5对象,并加入Model - JsonObject subHeadEntity5 = new JsonObject(); - model.add("SubHeadEntity5", subHeadEntity5); - - // 创建FProduceUnitId对象,并加入SubHeadEntity5 - JsonObject fProduceUnitId = new JsonObject(); - fProduceUnitId.addProperty("FNumber", "008"); - subHeadEntity5.add("FProduceUnitId", fProduceUnitId); - - // 创建FProduceBillType对象,并加入SubHeadEntity5 - JsonObject fProduceBillType = new JsonObject(); - fProduceBillType.addProperty("FNUMBER", "SCDD05_SYS"); - subHeadEntity5.add("FProduceBillType", fProduceBillType); - - // 创建FBOMUnitId对象,并加入SubHeadEntity5 - JsonObject fBOMUnitId = new JsonObject(); - fBOMUnitId.addProperty("FNumber", "008"); - subHeadEntity5.add("FBOMUnitId", fBOMUnitId); - - subHeadEntity5.addProperty("FIsMainPrd", true); - subHeadEntity5.addProperty("FIssueType", "1"); - - // 创建FPickStockId对象,并加入SubHeadEntity5 - JsonObject fPickStockId = new JsonObject(); - fPickStockId.addProperty("FNumber", "CK012"); - subHeadEntity1.add("FPickStockId", fPickStockId); - - subHeadEntity5.addProperty("FOverControlMode", "1"); - subHeadEntity5.addProperty("FStdLaborProcessTime", fbWorkTime); - subHeadEntity5.addProperty("FStandHourUnitId", "60"); - subHeadEntity5.addProperty("FBackFlushType", "1"); - String jsonData = json.toString(); - System.out.println(jsonData); - try { - // 业务对象标识 - String formId = "BD_MATERIAL"; - // 调用接口 - String resultJson = client.save(formId, jsonData); - // 用于记录结果 - Gson gson = new Gson(); - // 对返回结果进行解析和校验 - RepoRet repoRet = gson.fromJson(resultJson, RepoRet.class); - if (repoRet.getResult().getResponseStatus().isIsSuccess()) { - log.debug("接口返回结果: %s%n" + gson.toJson(repoRet.getResult())); - return 1; - } else { - MessageUtil.fail("接口返回结果: " + gson.toJson(repoRet.getResult().getResponseStatus())); - log.error("接口返回结果: " + gson.toJson(repoRet.getResult().getResponseStatus())); - return 0; - } - } catch (Exception e) { - MessageUtil.fail(e.getMessage()); - return 0; - } - - } //查询物料 作用于更新到物料的 仓位 是否启用VMI public static List getEntryID2(String materialCode) { @@ -3671,7 +3803,7 @@ public class JdUtil { JsonObject json = new JsonObject(); json.addProperty("FormId", "SFC_OperationPlanning"); json.addProperty("FieldKeys", - "FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo,FOperNumber," + + "FID,FENTITY_FEntryID,FSubEntity_FDetailID,F_HBYT_SCLH,F_HBYT_XMH,FMONumber,FProductId.FNumber,FProductName,FProSpecification,FBillNo,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"); @@ -3688,7 +3820,7 @@ public class JdUtil { JsonObject filterObject1 = new JsonObject(); filterObject1.addProperty("FieldName", "FPlanFinishTime"); filterObject1.addProperty("Compare", "39"); - filterObject1.addProperty("Value", "2026-04-15 00:00:00"); + filterObject1.addProperty("Value", "2026-04-16 00:00:00"); filterObject1.addProperty("Left", ""); filterObject1.addProperty("Right", ""); filterObject1.addProperty("Logic", 0); @@ -3810,6 +3942,7 @@ public class JdUtil { OperationPlanResultDTO resultDTO = new OperationPlanResultDTO(); resultDTO.setMoNumber(moNumber); resultDTO.setBatchNo(firstItem.getFHbytSclh()); // 生产令号 + resultDTO.setProjectNo(firstItem.getFHbytXmh()); resultDTO.setProductCode(firstItem.getFProductIdFNumber()); resultDTO.setProductName(firstItem.getFProductName()); resultDTO.setBillNo(firstItem.getFBillNo()); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JsonConverter.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JsonConverter.java index 30aebb0..1a3f9e8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/JsonConverter.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/JsonConverter.java @@ -85,7 +85,10 @@ public class JsonConverter { processRouteDTO.getWorkCenter().equals("铆焊工段") || processRouteDTO.getWorkCenter().equals("委外中心")) { department.setFNUMBER("006"); - } else { + } else if (processRouteDTO.getWorkCenter().equals("下料工段")|| + processRouteDTO.getWorkCenter().equals("焊接工段")){ + department.setFNUMBER("018"); + }else{ department.setFNUMBER("007"); } subEntity.setFDepartmentId(department); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/PDFGenerator.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/PDFGenerator.java index b7c3f6b..c077338 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/PDFGenerator.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/PDFGenerator.java @@ -19,10 +19,10 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.time.format.DateTimeFormatter; import java.util.*; @@ -35,10 +35,25 @@ public class PDFGenerator { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd"); static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + /** + * PDFBox 分页时在方法内 {@code close()} 旧流并新建流后,必须把新流回传给调用方; + * 否则调用方仍握着已关闭的流,而新流永不关闭,save 时会报 Cannot read while there is an open stream writer。 + */ + private static final class PdfDrawContext { + PDPageContentStream stream; + PDPage page; + + PdfDrawContext(PDPageContentStream stream, PDPage page) { + this.stream = stream; + this.page = page; + } + } + // 绘制表格行的方法 - private static PDPageContentStream drawTableRow(PDPageContentStream contentStream, PDType0Font font, float margin, float yStart, - float tableWidth, float[] colWidths, String[] cells, PDDocument document, - PDPage page, float rowHeight, float pageHeight) throws IOException { + private static void drawTableRow(PdfDrawContext ctx, PDType0Font font, float margin, float yStart, + float tableWidth, float[] colWidths, String[] cells, PDDocument document, + float rowHeight, float pageHeight) throws IOException { + PDPageContentStream contentStream = ctx.stream; float nextX = margin; float nextY = yStart; @@ -47,10 +62,11 @@ public class PDFGenerator { // 检查是否需要分页 if (nextY - rowHeight < margin) { contentStream.close(); - // 创建新页面 - page = new PDPage(); - document.addPage(page); - contentStream = new PDPageContentStream(document, page); + PDPage newPage = new PDPage(); + document.addPage(newPage); + contentStream = new PDPageContentStream(document, newPage); + ctx.stream = contentStream; + ctx.page = newPage; // 重置Y坐标为新页面顶部 nextY = pageHeight - margin; } @@ -73,8 +89,6 @@ public class PDFGenerator { nextX += colWidths[i]; } - - return contentStream; // 返回可能更新后的contentStream } // 生成二维码 @@ -94,8 +108,19 @@ public class PDFGenerator { return LosslessFactory.createFromImage(document, bufferedImage); } - public static String writeToPdf(List combinedVoList, String rooteProdet) throws IOException { - String fontPath = "C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf"; + public static String writeToPdf(List combinedVoList, String rooteProdet) { + return writeToPdf(combinedVoList, rooteProdet, Arrays.asList( + "C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf" + )); + } + + public static String writeToPdf(List combinedVoList, String rooteProdet, List fontPaths) { + String[] fontCandidates = (fontPaths == null || fontPaths.isEmpty()) + ? new String[] { + "C:\\Users\\Administrator\\Desktop\\arial unicode ms.ttf" + } + : fontPaths.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toArray(String[]::new); + List pdfPaths = new ArrayList<>(); String directoryPath = "D:\\上传BOM\\" + rooteProdet; // 检查目录是否存在,如果不存在则创建 File directory = new File(directoryPath); @@ -111,572 +136,249 @@ public class PDFGenerator { if (java.nio.file.Files.exists(root)) { try (java.util.stream.Stream walk = java.nio.file.Files.walk(root)) { walk.filter(p -> !p.equals(root)) - .sorted(java.util.Comparator.reverseOrder()) // 先删子路径再删父路径 - .forEach(p -> { - try { - java.nio.file.Files.deleteIfExists(p); - } catch (IOException e) { - System.err.println("删除失败: " + p.toString() + ": " + e.getMessage()); - } - }); + .sorted(java.util.Comparator.reverseOrder()) // 先删子路径再删父路径 + .forEach(p -> { + try { + java.nio.file.Files.deleteIfExists(p); + } catch (IOException e) { + System.err.println("删除失败: " + p.toString() + ": " + e.getMessage()); + } + }); } } } catch (IOException e) { throw new RuntimeException("清理目录失败: " + directoryPath + ", " + e.getMessage(), e); } } - Map> groupMap = new LinkedHashMap<>(); - if (combinedVoList != null) { - for (CombinedDTO dto : combinedVoList) { - if (dto == null) { - continue; + for (CombinedDTO combinedVo : combinedVoList) { + if (!combinedVoList.isEmpty()) { + // 获取物料编码 + String materialCode1 = combinedVo.getMaterialCode(); + String fProcessName; + String productionOrderNumber; + Date starttime; + Date endtime; + String materialCode; + Long fmoQty; + String fDepartmentName; + String OrderNumber = ""; + // 获取第一序的工序名称 + List processes = combinedVo.getProcesses(); + if (!processes.isEmpty()) { + fProcessName = processes.get(0).getFProcessName(); + OrderNumber = combinedVo.getOrderNumber(); + productionOrderNumber = String.valueOf(combinedVo.getProcesses().get(0).getFBillNo()); + starttime = combinedVo.getProcesses().get(0).getFPlanStartTime(); + endtime = combinedVo.getProcesses().get(0).getFPlanFinishTime(); + List usageList = combinedVo.getMaterialUsageDTOList(); + materialCode = (usageList != null && !usageList.isEmpty() && usageList.get(0) != null) + ? String.valueOf(usageList.get(0).getBillNumber()) + : combinedVo.getOrderNumber(); + fmoQty = combinedVo.getProcesses().get(0).getFMOQty(); + fDepartmentName = combinedVo.getProcesses().get(0).getFWorkCenterName(); + // 其他代码... + } else { + // 处理列表为空的情况 + fProcessName = "A"; + productionOrderNumber = "A"; + starttime = new Date(); + endtime = new Date(); + materialCode = "0"; + fmoQty = 0L; + fDepartmentName = ""; + } + + String pdfFileName = fDepartmentName + "_" + materialCode1.replace("/", "_") + "_独立" + ".pdf"; + + // 1. 获取文件夹名 + String departmentFolderName = fDepartmentName; + if (departmentFolderName == null || departmentFolderName.trim().isEmpty()) { + // 兜底:从文件名取前四个字或以'_'分割第一个元素 + String[] parts = pdfFileName.split("_"); + departmentFolderName = parts.length > 0 ? parts[0] + : pdfFileName.substring(0, Math.min(4, pdfFileName.length())); + } + + // 2. 创建文件夹 + File departmentFolder = new File(directoryPath, departmentFolderName); + if (!departmentFolder.exists()) { + departmentFolder.mkdirs(); + } + + // 获取当前时间的毫秒时间戳 + long timestamp = System.currentTimeMillis(); + String pdfPath = departmentFolder.getAbsolutePath() + "\\" + fDepartmentName + "_" + materialCode1.replace("/", "_") + "_" + productionOrderNumber + ".pdf"; + PDDocument document = null; + try { + // 创建 PDF 文件并写入内容 + document = new PDDocument(); + PDPage page = new PDPage(); + document.addPage(page); + PDType0Font font = loadAvailableFont(document, fontCandidates); + + PDPageContentStream contentStream = new PDPageContentStream(document, page); + PdfDrawContext drawCtx = new PdfDrawContext(contentStream, page); + + float margin = 15; + float yStart = 740; + float rowHeight = 20f; + float pageHeight = page.getMediaBox().getHeight(); + float tableWidth = page.getMediaBox().getWidth() - 2 * margin; + + // 添加标题和右上角的二维码 + drawCtx.stream.beginText(); + drawCtx.stream.setFont(font, 14); + drawCtx.stream.newLineAtOffset(220, 780); + drawCtx.stream.showText("工序计划单"); + drawCtx.stream.endText(); + + PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, 90, 90); + drawCtx.stream.drawImage(qrCode1, drawCtx.page.getMediaBox().getWidth() - 110, 710); + SimpleDateFormat formatte11r = new SimpleDateFormat("yyyy-MM-dd"); + // 绘制生产订单信息表格 + float[] colWidths = { 60, 95, 110, 110, 60, 60, 55 }; + drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths, + new String[] { "生产令号", rooteProdet, + "开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) }, + document, rowHeight, pageHeight); + + yStart -= rowHeight; + drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths, + new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, document, + rowHeight, pageHeight); + + yStart -= rowHeight; + drawTableRow(drawCtx, font, margin, yStart, tableWidth, colWidths, new String[] { + productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(), + combinedVo.getMaterialCode(), + String.valueOf(fmoQty), fDepartmentName, "1", "" }, document, rowHeight, pageHeight); + + // 绘制工序表格 + float processTableStartY = yStart - 2 * rowHeight - 10; + float[] processColWidths = { 35, 78, 35, 55, 45, 170, 55, 60, 60 }; + yStart = processTableStartY; + + // 使用支持换行的方法绘制表头 + yStart = drawTableRowWithWrap(drawCtx, font, margin, yStart, tableWidth, processColWidths, new String[] { + "工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" }, document, + rowHeight, pageHeight); + + for (PlannedProcessVo process : combinedVo.getProcesses()) { + SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd"); + // 使用支持换行的方法绘制数据行,特别是工序说明字段 + yStart = drawTableRowWithWrap(drawCtx, font, margin, yStart, tableWidth, processColWidths, new String[] { + String.valueOf(process.getFOperNumber()), process.getFProcessName(), + String.valueOf(process.getFOperQty()), + process.getFWorkCenterName(), String.format("%.2f", process.getFActivity1BaseQty()), + process.getFOperDescription(), + process.getFOptCtrlCodeIFName(), + process.getFSeqPlanStartTime() != null + ? formatte1r.format(process.getFSeqPlanStartTime()) + : "", + process.getFSeqPlanFinishTime() != null + ? (formatte1r.format(process.getFSeqPlanFinishTime())) + : "" + }, document, rowHeight, pageHeight); + } + + // 添加生产用料清单标题 + yStart = yStart - 2 * rowHeight - 40; + + drawCtx.stream.beginText(); + drawCtx.stream.setFont(font, 12); + drawCtx.stream.newLineAtOffset(margin, yStart); + drawCtx.stream.showText("生产用料清单:"); + drawCtx.stream.showText(materialCode); + drawCtx.stream.endText(); + + // 在用料清单标题右侧添加二维码,与右上角二维码对齐 + PDImageXObject qrCode2 = generateQRCode(document, materialCode, 90, 90); + drawCtx.stream.drawImage(qrCode2, drawCtx.page.getMediaBox().getWidth() - 110, yStart - 31); + + yStart -= rowHeight; + + // 用料清单表头 + float[] materialColWidths = { 25, 130, 110, 50, 35, 40, 40, 80, 50 }; + drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths, new String[] { + "序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注" + }, document, rowHeight, pageHeight); + + List materialUsageDTOList = combinedVo.getMaterialUsageDTOList(); + if (materialUsageDTOList != null && !materialUsageDTOList.isEmpty()) { + for (int i = 0; i < materialUsageDTOList.size(); i++) { + yStart -= rowHeight; + + // 如果空间不足,则新建页面 + if (yStart - rowHeight < margin) { + drawCtx.stream.close(); + PDPage newPage = new PDPage(); + document.addPage(newPage); + drawCtx.stream = new PDPageContentStream(document, newPage); + drawCtx.page = newPage; + yStart = pageHeight - margin; + + // 绘制用料清单表头 + drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths, + new String[] { + "序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注" + }, document, rowHeight, pageHeight); + yStart -= rowHeight; + } + + MaterialUsageDTO material = materialUsageDTOList.get(i); + SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd"); + drawTableRow(drawCtx, font, margin, yStart, tableWidth, materialColWidths, + new String[] { + String.valueOf(i + 1), material.getMaterialCode(), + material.getMaterialName(), + material.getSpecification(), material.getUnit(), + String.valueOf(material.getRequiredQty()), + String.valueOf(material.getPickedQty()), + + material.getStockName(), + material.getRemarks() != null ? material.getRemarks() : "" + }, document, rowHeight, pageHeight); + } + } + + drawCtx.stream.close(); + document.save(pdfPath); + pdfPaths.add(pdfPath); + } catch (IOException | WriterException e) { + throw new RuntimeException("生成PDF失败: " + pdfPath + ", " + e.getMessage(), e); + } finally { + if (document != null) { + try { + document.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } } - String prefix = getMaterialPrefix(dto.getMaterialCode()); - groupMap.computeIfAbsent(prefix, k -> new ArrayList<>()).add(dto); } } - - for (Map.Entry> entry : groupMap.entrySet()) { - List groupList = entry.getValue(); - if (groupList == null || groupList.isEmpty()) { - continue; - } - if (groupList.size() == 1) { - createSinglePdf(groupList.get(0), rooteProdet, directoryPath, fontPath); - continue; - } - - if (groupList.size() == 2) { - generateTwoInOnePage(groupList.get(0), groupList.get(1), rooteProdet, directoryPath, fontPath, entry.getKey()); - continue; - } - - List groupPdfPaths = new ArrayList<>(); - for (CombinedDTO combinedVo : groupList) { - String pdfPath = createSinglePdf(combinedVo, rooteProdet, directoryPath, fontPath); - if (pdfPath != null && !pdfPath.trim().isEmpty()) { - groupPdfPaths.add(pdfPath); - } - } - - if (groupPdfPaths.size() > 1) { - String mergedPath = buildMergedPdfPath(groupList.get(0), directoryPath, entry.getKey()); - Files.move(Paths.get(groupPdfPaths.get(0)), Paths.get(mergedPath), StandardCopyOption.REPLACE_EXISTING); - for (int i = 1; i < groupPdfPaths.size(); i++) { - String p = groupPdfPaths.get(i); - mergePdfToExisting(p, mergedPath); - Files.deleteIfExists(Paths.get(p)); - } - } + if (pdfPaths.isEmpty()) { + throw new RuntimeException("未生成任何PDF文件,请检查工艺数据或字体配置"); + } + List generatedPdfFiles = collectPdfFiles(new File(directoryPath)); + if (generatedPdfFiles.isEmpty()) { + throw new RuntimeException("目录中未找到已生成PDF: " + directoryPath); } // 生成ZIP文件 String zipFilePath = directoryPath + "\\" + rooteProdet + "_工序计划单.zip"; try { - createZipFile(directoryPath, zipFilePath); // 传入整个目录 + createZipFile(directoryPath, zipFilePath); + File zipFile = new File(zipFilePath); + if (!zipFile.exists() || zipFile.length() <= 0) { + throw new IOException("ZIP文件为空或不存在: " + zipFilePath); + } return zipFilePath; } catch (IOException e) { - e.printStackTrace(); - return null; + throw new RuntimeException("生成ZIP失败: " + zipFilePath + ", " + e.getMessage(), e); } } - private static String getMaterialPrefix(String materialCode) { - if (materialCode == null) { - return ""; - } - int index = materialCode.indexOf("-"); - if (index > 0) { - return materialCode.substring(0, index); - } - return materialCode; - } - - private static String getDepartmentName(CombinedDTO combinedVo) { - List processes = combinedVo.getProcesses(); - if (processes != null && !processes.isEmpty()) { - String d = processes.get(0).getFWorkCenterName(); - return d != null ? d : ""; - } - return ""; - } - - private static String buildMergedPdfPath(CombinedDTO anyVo, String directoryPath, String prefix) { - String dept = getDepartmentName(anyVo); - String safePrefix = prefix != null ? prefix.replace("/", "_") : ""; - String fileName = (safePrefix.isEmpty() ? ("UNKNOWN_" + System.currentTimeMillis()) : safePrefix) + ".pdf"; - - String folderName = dept; - if (folderName == null || folderName.trim().isEmpty()) { - String[] parts = fileName.split("_"); - folderName = parts.length > 0 ? parts[0] : "merge"; - } - - File departmentFolder = new File(directoryPath, folderName); - if (!departmentFolder.exists()) { - departmentFolder.mkdirs(); - } - return departmentFolder.getAbsolutePath() + "\\" + fileName; - } - - private static String generateTwoInOnePage(CombinedDTO dto1, CombinedDTO dto2, String rooteProdet, String directoryPath, String fontPath, String prefix) { - if (dto1 == null || dto2 == null) { - return null; - } - String outputPath = buildMergedPdfPath(dto1, directoryPath, prefix); - PDDocument document = null; - PDPageContentStream contentStream = null; - try { - document = new PDDocument(); - PDPage page = new PDPage(); - document.addPage(page); - - PDType0Font font = PDType0Font.load(document, new File(fontPath)); - contentStream = new PDPageContentStream(document, page); - - float topStart = 780f; - float bottomStart = 380f; - drawPlanIntoArea(dto1, document, page, contentStream, font, rooteProdet, topStart, 400f); - drawPlanIntoArea(dto2, document, page, contentStream, font, rooteProdet, bottomStart, 15f); - - contentStream.close(); - contentStream = null; - document.save(outputPath); - return outputPath; - } catch (Exception e) { - e.printStackTrace(); - return null; - } finally { - if (contentStream != null) { - try { - contentStream.close(); - } catch (IOException ignored) { - } - } - if (document != null) { - try { - document.close(); - } catch (IOException ignored) { - } - } - } - } - - private static void drawPlanIntoArea(CombinedDTO combinedVo, PDDocument document, PDPage page, PDPageContentStream contentStream, - PDType0Font font, String rooteProdet, float titleY, float minY) throws IOException, WriterException { - float margin = 15f; - float rowHeight = 14f; - float fontSize = 8f; - float pageWidth = page.getMediaBox().getWidth(); - float qrSize = 90f; - float qrGap = 5f; - float tableWidth = pageWidth - 2 * margin - (qrSize + qrGap); - float qrX = pageWidth - margin - qrSize; - - List processes = combinedVo.getProcesses(); - String productionOrderNumber; - Date starttime; - Date endtime; - Long fmoQty; - String fDepartmentName; - if (processes != null && !processes.isEmpty()) { - productionOrderNumber = String.valueOf(processes.get(0).getFBillNo()); - starttime = processes.get(0).getFPlanStartTime(); - endtime = processes.get(0).getFPlanFinishTime(); - fmoQty = processes.get(0).getFMOQty(); - fDepartmentName = processes.get(0).getFWorkCenterName(); - } else { - productionOrderNumber = "A"; - starttime = new Date(); - endtime = new Date(); - fmoQty = 0L; - fDepartmentName = ""; - } - - String materialCodeQr; - if (combinedVo.getMaterialUsageDTOList() != null && !combinedVo.getMaterialUsageDTOList().isEmpty()) { - materialCodeQr = String.valueOf(combinedVo.getMaterialUsageDTOList().get(0).getBillNumber()); - } else { - materialCodeQr = "0"; - } - - if (titleY - qrSize >= minY) { - PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, (int) qrSize, (int) qrSize); - contentStream.drawImage(qrCode1, qrX, titleY - qrSize + 5); - } - - SimpleDateFormat formatte11r = new SimpleDateFormat("yyyy-MM-dd"); - float[] colWidths = scaleWidths(new float[] { 60, 95, 110, 110, 60, 60, 55 }, tableWidth); - float yStart = titleY - 5; - - Object[] r1 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, colWidths, - new String[] { "生产令号", rooteProdet, "开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) }, - rowHeight, minY); - yStart = (Float) r1[1]; - - Object[] r2 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, colWidths, - new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, rowHeight, minY); - yStart = (Float) r2[1]; - - Object[] r3 = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, colWidths, new String[] { - productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(), combinedVo.getMaterialCode(), - String.valueOf(fmoQty), fDepartmentName, "1", "" }, rowHeight, minY); - yStart = (Float) r3[1]; - - float[] processColWidths = scaleWidths(new float[] { 35, 78, 35, 55, 45, 170, 55, 60, 60 }, tableWidth); - yStart = yStart - 6; - Object[] headerResult = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, processColWidths, - new String[] { "工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" }, - rowHeight, minY); - yStart = (Float) headerResult[1]; - - if (processes != null) { - SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd"); - for (PlannedProcessVo process : processes) { - yStart = yStart - rowHeight; - Object[] rowResult = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, processColWidths, new String[] { - String.valueOf(process.getFOperNumber()), process.getFProcessName(), - String.valueOf(process.getFOperQty()), - process.getFWorkCenterName(), String.format("%.2f", process.getFActivity1BaseQty()), - process.getFOperDescription(), - process.getFOptCtrlCodeIFName(), - process.getFSeqPlanStartTime() != null ? formatte1r.format(process.getFSeqPlanStartTime()) : "", - process.getFSeqPlanFinishTime() != null ? formatte1r.format(process.getFSeqPlanFinishTime()) : "" - }, rowHeight, minY); - float nextY = (Float) rowResult[1]; - if (nextY == yStart || nextY <= minY) { - break; - } - yStart = nextY; - } - } - - yStart = yStart - 15; - if (yStart >= minY) { - contentStream.beginText(); - contentStream.setFont(font, 10); - contentStream.newLineAtOffset(margin, yStart); - contentStream.showText("生产用料清单:" + materialCodeQr); - contentStream.endText(); - - PDImageXObject qrCode2 = generateQRCode(document, materialCodeQr, (int) qrSize, (int) qrSize); - float qrTopY = yStart + 10; - if (qrTopY - qrSize >= minY - 20) { - contentStream.drawImage(qrCode2, qrX, qrTopY - qrSize); - } - yStart = yStart - 12; - } - - yStart = yStart - 2; - float[] materialColWidths = scaleWidths(new float[] { 25, 130, 110, 50, 35, 40, 40, 80, 50 }, tableWidth); - Object[] mHeader = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart, tableWidth, materialColWidths, new String[] { - "序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注" - }, rowHeight, minY); - yStart = (Float) mHeader[1]; - - List materialUsageDTOList = combinedVo.getMaterialUsageDTOList(); - if (materialUsageDTOList != null && !materialUsageDTOList.isEmpty()) { - for (int i = 0; i < materialUsageDTOList.size(); i++) { - MaterialUsageDTO material = materialUsageDTOList.get(i); - Object[] mr = drawTableRowWithWrapNoPageBreak(contentStream, font, fontSize, margin, yStart - 2, tableWidth, materialColWidths, new String[] { - String.valueOf(i + 1), material.getMaterialCode(), - material.getMaterialName(), - material.getSpecification(), material.getUnit(), - String.valueOf(material.getRequiredQty()), - String.valueOf(material.getPickedQty()), - material.getStockName(), - material.getRemarks() != null ? material.getRemarks() : "" - }, rowHeight, minY); - float nextY = (Float) mr[1]; - if (nextY == yStart - 2 || nextY <= minY) { - break; - } - yStart = nextY; - } - } - } - - private static float[] scaleWidths(float[] widths, float targetTotal) { - float sum = 0f; - for (float w : widths) { - sum += w; - } - if (sum <= 0f || targetTotal <= 0f) { - return widths; - } - float scale = targetTotal / sum; - float[] scaled = new float[widths.length]; - for (int i = 0; i < widths.length; i++) { - scaled[i] = widths[i] * scale; - } - return scaled; - } - - private static float drawTableRowNoPageBreak(PDPageContentStream contentStream, PDType0Font font, float fontSize, float margin, - float yStart, float tableWidth, float[] colWidths, String[] cells, float rowHeight, float minY) throws IOException { - if (yStart - rowHeight < minY) { - return yStart; - } - - float nextX = margin; - float nextY = yStart; - int maxColumns = Math.min(cells.length, colWidths.length); - - for (int i = 0; i < maxColumns; i++) { - contentStream.beginText(); - contentStream.setFont(font, fontSize); - contentStream.newLineAtOffset(nextX + 4, nextY - (fontSize + 4)); - String cellText = cells[i] != null ? cells[i].replace("\n", "") : ""; - contentStream.showText(cellText); - contentStream.endText(); - - contentStream.moveTo(nextX, nextY); - contentStream.lineTo(nextX, nextY - rowHeight); - contentStream.lineTo(nextX + colWidths[i], nextY - rowHeight); - contentStream.lineTo(nextX + colWidths[i], nextY); - contentStream.closeAndStroke(); - - nextX += colWidths[i]; - } - - return yStart - rowHeight; - } - - private static Object[] drawTableRowWithWrapNoPageBreak(PDPageContentStream contentStream, PDType0Font font, float fontSize, - float margin, float yStart, float tableWidth, float[] colWidths, String[] cells, float baseRowHeight, float minY) - throws IOException { - float nextX = margin; - float nextY = yStart; - int maxColumns = Math.min(cells.length, colWidths.length); - - List> allCellLines = new ArrayList>(); - int maxLines = 1; - - for (int i = 0; i < maxColumns; i++) { - String cellText = cells[i] != null ? cells[i] : ""; - List lines = wrapText(cellText, font, fontSize, colWidths[i]); - allCellLines.add(lines); - maxLines = Math.max(maxLines, lines.size()); - } - - float actualRowHeight = baseRowHeight * maxLines; - if (nextY - actualRowHeight < minY) { - return new Object[]{contentStream, yStart}; - } - - nextX = margin; - for (int i = 0; i < maxColumns; i++) { - List lines = allCellLines.get(i); - - contentStream.moveTo(nextX, nextY); - contentStream.lineTo(nextX, nextY - actualRowHeight); - contentStream.lineTo(nextX + colWidths[i], nextY - actualRowHeight); - contentStream.lineTo(nextX + colWidths[i], nextY); - contentStream.closeAndStroke(); - - for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) { - String line = lines.get(lineIndex); - if (!line.isEmpty()) { - contentStream.beginText(); - contentStream.setFont(font, fontSize); - float textY = nextY - (fontSize + 4) - (lineIndex * (fontSize + 1)); - contentStream.newLineAtOffset(nextX + 4, textY); - contentStream.showText(line); - contentStream.endText(); - } - } - - nextX += colWidths[i]; - } - - return new Object[]{contentStream, nextY - actualRowHeight}; - } - - private static String createSinglePdf(CombinedDTO combinedVo, String rooteProdet, String directoryPath, String fontPath) { - if (combinedVo == null) { - return null; - } - - String materialCode1 = combinedVo.getMaterialCode(); - String productionOrderNumber; - Date starttime; - Date endtime; - String materialCode; - Long fmoQty; - String fDepartmentName; - - List processes = combinedVo.getProcesses(); - if (processes != null && !processes.isEmpty()) { - productionOrderNumber = String.valueOf(processes.get(0).getFBillNo()); - starttime = processes.get(0).getFPlanStartTime(); - endtime = processes.get(0).getFPlanFinishTime(); - if (combinedVo.getMaterialUsageDTOList() != null && !combinedVo.getMaterialUsageDTOList().isEmpty()) { - materialCode = String.valueOf(combinedVo.getMaterialUsageDTOList().get(0).getBillNumber()); - } else { - materialCode = "0"; - } - fmoQty = processes.get(0).getFMOQty(); - fDepartmentName = processes.get(0).getFWorkCenterName(); - } else { - productionOrderNumber = "A"; - starttime = new Date(); - endtime = new Date(); - materialCode = "0"; - fmoQty = 0L; - fDepartmentName = ""; - } - - String pdfFileName = fDepartmentName + "_" + (materialCode1 != null ? materialCode1.replace("/", "_") : "") + "_独立" + ".pdf"; - - String departmentFolderName = fDepartmentName; - if (departmentFolderName == null || departmentFolderName.trim().isEmpty()) { - String[] parts = pdfFileName.split("_"); - departmentFolderName = parts.length > 0 ? parts[0] : pdfFileName.substring(0, Math.min(4, pdfFileName.length())); - } - - File departmentFolder = new File(directoryPath, departmentFolderName); - if (!departmentFolder.exists()) { - departmentFolder.mkdirs(); - } - - String pdfPath = departmentFolder.getAbsolutePath() + "\\" + fDepartmentName + "_" + (materialCode1 != null ? materialCode1.replace("/", "_") : "") + "_" + productionOrderNumber + ".pdf"; - PDDocument document = null; - try { - document = new PDDocument(); - PDPage page = new PDPage(); - document.addPage(page); - PDType0Font font; - - try { - font = PDType0Font.load(document, new File(fontPath)); - } catch (IllegalArgumentException e) { - throw new RuntimeException("Font file not found", e); - } - - PDPageContentStream contentStream = new PDPageContentStream(document, page); - - float margin = 15; - float yStart = 740; - float rowHeight = 20f; - float pageHeight = page.getMediaBox().getHeight(); - float pageWidth = page.getMediaBox().getWidth(); - float qrSize = 90f; - float qrGap = 5f; - float tableWidth = pageWidth - 2 * margin - (qrSize + qrGap); - float qrX = pageWidth - margin - qrSize; - - PDImageXObject qrCode1 = generateQRCode(document, productionOrderNumber, (int)qrSize, (int)qrSize); - contentStream.drawImage(qrCode1, qrX, yStart - qrSize + 25); - - SimpleDateFormat formatte11r = new SimpleDateFormat("yyyy-MM-dd"); - float[] colWidths = scaleWidths(new float[] { 60, 95, 110, 110, 60, 60, 55 }, tableWidth); - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths, - new String[] { "生产令号", rooteProdet, "开始时间", formatte11r.format(starttime), "结束时间", formatte11r.format(endtime) }, - document, page, rowHeight, pageHeight); - - yStart -= rowHeight; - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths, - new String[] { "单据编号", "生产订单编号", "产品名称", "产品编码", "数量", "生产车间", "次数" }, document, page, - rowHeight, pageHeight); - - yStart -= rowHeight; - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, colWidths, new String[] { - productionOrderNumber, combinedVo.getOrderNumber(), combinedVo.getMaterialName(), combinedVo.getMaterialCode(), - String.valueOf(fmoQty), fDepartmentName, "1", "" }, document, page, rowHeight, pageHeight); - - float processTableStartY = yStart - 2 * rowHeight - 10; - float[] processColWidths = scaleWidths(new float[] { 35, 78, 35, 55, 45, 170, 55, 60, 60 }, tableWidth); - yStart = processTableStartY; - - Object[] result = drawTableRowWithWrap(contentStream, font, margin, yStart, tableWidth, processColWidths, new String[] { - "工序号", "工序名称", "数量", "工作中心", "工时(分)", "工序说明", "控制码", "开始时间", "结束时间" }, document, page, - rowHeight, pageHeight); - contentStream = (PDPageContentStream) result[0]; - yStart = (Float) result[1]; - - if (processes != null) { - for (PlannedProcessVo process : processes) { - SimpleDateFormat formatte1r = new SimpleDateFormat("yyyy-MM-dd"); - Object[] processResult = drawTableRowWithWrap(contentStream, font, margin, yStart, tableWidth, processColWidths, new String[] { - String.valueOf(process.getFOperNumber()), process.getFProcessName(), - String.valueOf(process.getFOperQty()), - process.getFWorkCenterName(), String.format("%.2f", process.getFActivity1BaseQty()), - process.getFOperDescription(), - process.getFOptCtrlCodeIFName(), - process.getFSeqPlanStartTime() != null ? formatte1r.format(process.getFSeqPlanStartTime()) : "", - process.getFSeqPlanFinishTime() != null ? formatte1r.format(process.getFSeqPlanFinishTime()) : "" - }, document, page, rowHeight, pageHeight); - contentStream = (PDPageContentStream) processResult[0]; - yStart = (Float) processResult[1]; - } - } - - yStart = yStart - 25; - - contentStream.beginText(); - contentStream.setFont(font, 12); - contentStream.newLineAtOffset(margin, yStart); - contentStream.showText("生产用料清单:"); - contentStream.showText(materialCode); - contentStream.endText(); - - PDImageXObject qrCode2 = generateQRCode(document, materialCode, (int)qrSize, (int)qrSize); - float qrTopY = yStart + 10; - contentStream.drawImage(qrCode2, qrX, qrTopY - qrSize); - - yStart = yStart - 15; - - float[] materialColWidths = scaleWidths(new float[] { 25, 130, 110, 50, 35, 40, 40, 80, 50 }, tableWidth); - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths, new String[] { - "序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注" - }, document, page, rowHeight, pageHeight); - - List materialUsageDTOList = combinedVo.getMaterialUsageDTOList(); - if (materialUsageDTOList != null && !materialUsageDTOList.isEmpty()) { - for (int i = 0; i < materialUsageDTOList.size(); i++) { - yStart -= rowHeight; - if (yStart - rowHeight < margin) { - contentStream.close(); - page = new PDPage(); - document.addPage(page); - contentStream = new PDPageContentStream(document, page); - yStart = pageHeight - margin; - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths, new String[] { - "序号", "物料编码", "物料名称", "规格型号", "单位", "应发数", "已领数", "仓库", "备注" - }, document, page, rowHeight, pageHeight); - yStart -= rowHeight; - } - MaterialUsageDTO material = materialUsageDTOList.get(i); - contentStream = drawTableRow(contentStream, font, margin, yStart, tableWidth, materialColWidths, - new String[] { - String.valueOf(i + 1), material.getMaterialCode(), - material.getMaterialName(), - material.getSpecification(), material.getUnit(), - String.valueOf(material.getRequiredQty()), - String.valueOf(material.getPickedQty()), - material.getStockName(), - material.getRemarks() != null ? material.getRemarks() : "" - }, document, page, rowHeight, pageHeight); - } - } - - contentStream.close(); - document.save(pdfPath); - return pdfPath; - } catch (IOException | WriterException e) { - e.printStackTrace(); - return null; - } finally { - if (document != null) { - try { - document.close(); - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - } - } - /** * 创建ZIP文件 * @@ -690,36 +392,96 @@ public class PDFGenerator { zipFile.getParentFile().mkdirs(); } - Set fileNamesInZip = new HashSet<>(); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFilePath)))) { // 设置压缩级别 zipOut.setLevel(ZipOutputStream.DEFLATED); - - File sourceDir = new File(sourceDirPath); - zipDirectory(sourceDir, sourceDir.getName() + "/", zipOut); - } - } - - private static void zipDirectory(File folder, String parentFolder, ZipOutputStream zipOut) throws IOException { - for (File file : Objects.requireNonNull(folder.listFiles())) { - if (file.isDirectory()) { - zipDirectory(file, parentFolder + file.getName() + "/", zipOut); - } else { + Path rootPath = Paths.get(sourceDirPath).toAbsolutePath().normalize(); + List filesToZip = collectPdfFiles(new File(sourceDirPath)); + if (filesToZip.isEmpty()) { + throw new IOException("目录中未找到可打包PDF: " + sourceDirPath); + } + Set entryNames = new HashSet<>(); + for (File file : filesToZip) { + Path filePath = file.toPath().toAbsolutePath().normalize(); + String entryName = rootPath.relativize(filePath).toString().replace("\\", "/"); + if (entryName.trim().isEmpty() || !entryNames.add(entryName)) { + continue; + } try (FileInputStream fis = new FileInputStream(file)) { - ZipEntry zipEntry = new ZipEntry(parentFolder + file.getName()); + ZipEntry zipEntry = new ZipEntry(entryName); zipOut.putNextEntry(zipEntry); - byte[] bytes = new byte[1024]; int length; while ((length = fis.read(bytes)) >= 0) { zipOut.write(bytes, 0, length); } + zipOut.closeEntry(); } } } } + private static List collectPdfFiles(File root) { + List result = new ArrayList<>(); + if (root == null || !root.exists()) { + return result; + } + if (root.isFile()) { + if (root.getName().toLowerCase().endsWith(".pdf")) { + result.add(root); + } + return result; + } + File[] children = root.listFiles(); + if (children == null) { + return result; + } + for (File child : children) { + result.addAll(collectPdfFiles(child)); + } + return result; + } + + private static PDType0Font loadAvailableFont(PDDocument document, String[] fontCandidates) throws IOException { + Exception last = null; + StringBuilder triedPaths = new StringBuilder(); + for (String fontPath : fontCandidates) { + if (fontPath == null || fontPath.trim().isEmpty()) { + continue; + } + if (triedPaths.length() > 0) { + triedPaths.append(", "); + } + triedPaths.append(fontPath); + try { + if (fontPath.startsWith("classpath:")) { + String classpathLocation = fontPath.substring("classpath:".length()); + if (classpathLocation.startsWith("/")) { + classpathLocation = classpathLocation.substring(1); + } + try (InputStream in = PDFGenerator.class.getClassLoader().getResourceAsStream(classpathLocation)) { + if (in == null) { + continue; + } + return PDType0Font.load(document, in, true); + } + } else { + File file = new File(fontPath); + if (!file.exists() || !file.isFile()) { + continue; + } + return PDType0Font.load(document, file); + } + } catch (Exception e) { + last = e; + } + } + if (last instanceof IOException) { + throw new IOException("字体加载失败,请检查字体文件: " + triedPaths, last); + } + throw new IOException("未找到可用中文字体,请检查字体文件路径配置: " + triedPaths); + } + // 文件搜索方法:根据物料编码在文件夹中查找文件名包含该编码的PDF文件 public static File findFileWithMaterialCode(String directoryPath, String materialCode) { File directory = new File(directoryPath); @@ -735,7 +497,7 @@ public class PDFGenerator { // 合并PDF,将生成的PDF插入到已有PDF的末尾 public static void mergePdfToExisting(String generatedPdfPath, String existingPdfPath) throws IOException { try (PDDocument existingDoc = PDDocument.load(new File(existingPdfPath)); - PDDocument generatedDoc = PDDocument.load(new File(generatedPdfPath))) { + PDDocument generatedDoc = PDDocument.load(new File(generatedPdfPath))) { PDFMergerUtility merger = new PDFMergerUtility(); merger.appendDocument(existingDoc, generatedDoc); @@ -885,10 +647,11 @@ public class PDFGenerator { } // Java 8兼容的改进表格绘制方法,支持自动换行 - private static Object[] drawTableRowWithWrap(PDPageContentStream contentStream, PDType0Font font, float margin, - float yStart, float tableWidth, float[] colWidths, String[] cells, PDDocument document, - PDPage page, float baseRowHeight, float pageHeight) throws IOException { + private static float drawTableRowWithWrap(PdfDrawContext ctx, PDType0Font font, float margin, + float yStart, float tableWidth, float[] colWidths, String[] cells, PDDocument document, + float baseRowHeight, float pageHeight) throws IOException { + PDPageContentStream contentStream = ctx.stream; float nextX = margin; float nextY = yStart; float fontSize = 10; @@ -910,10 +673,11 @@ public class PDFGenerator { // 检查是否需要分页 if (nextY - actualRowHeight < margin) { contentStream.close(); - // 创建新页面 - page = new PDPage(); - document.addPage(page); - contentStream = new PDPageContentStream(document, page); + PDPage newPage = new PDPage(); + document.addPage(newPage); + contentStream = new PDPageContentStream(document, newPage); + ctx.stream = contentStream; + ctx.page = newPage; // 重置Y坐标为新页面顶部 nextY = pageHeight - margin; } @@ -946,7 +710,7 @@ public class PDFGenerator { nextX += colWidths[i]; } - return new Object[]{contentStream, nextY - actualRowHeight}; // 返回contentStream和下一行的Y坐标 + return nextY - actualRowHeight; // 返回下一行的Y坐标 } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IImMaterialService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IImMaterialService.java index 07374da..3b51d90 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IImMaterialService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IImMaterialService.java @@ -6,9 +6,13 @@ import com.ruoyi.system.domain.vo.ImMaterialVo; import com.ruoyi.system.domain.bo.ImMaterialBo; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO; import java.util.Collection; import java.util.List; +import java.util.Map; /** * 物料检索Service接口 @@ -66,4 +70,16 @@ public interface IImMaterialService { Boolean insertJDMaterial(List materialList); Boolean updateByFMid(List imMaterials); + + BuildCapacityDTO queryBuildCapacity(String productionOrderNo, String productCode, String productName); + + List queryBuildCapacityByProductionOrderNo(String productionOrderNo); + + ProductionOrderMaterialsDTO queryProductionOrderMaterials(String productionOrderNo, String productCode); + + /** + * 分页扫描 im_material,按物料编码批量解析单重(production_order → process_route)并写回; + * 仅解析到单重且与库中值不同时更新 single_weight、modify_date、update_time。 + */ + R> refreshImMaterialSingleWeightFromOrders(); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessOrderProService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessOrderProService.java index dc9c273..91e6483 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessOrderProService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessOrderProService.java @@ -99,6 +99,20 @@ public interface IProcessOrderProService { */ void pushDingTalkCard(Long id); + /** + * 生成工艺计划表 Excel(出图场景模板)并发送至钉钉机器人配置的接收人 + * + * @param id 项目令号主键 ID(与列表/详情中的 id 一致) + */ + R sendProcessRoutePlanExcel(Long id); + + /** + * 查询工艺计划表(钉钉)已成功发送次数,供前端按钮等展示 + * + * @param id 项目令号主键 ID + */ + R getRoutePlanSendCount(Long id); + /** * 刷新并计算项目延期状态 */ diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProductionOrderService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProductionOrderService.java index 3d4ee71..4d29341 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProductionOrderService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProductionOrderService.java @@ -1,65 +1,89 @@ -package com.ruoyi.system.service; - -import com.ruoyi.system.domain.ProductionOrder; -import com.ruoyi.system.domain.bo.FigureSaveBo; -import com.ruoyi.system.domain.bo.ProcessOrderProBo; -import com.ruoyi.system.domain.vo.ProductionOrderVo; -import com.ruoyi.system.domain.bo.ProductionOrderBo; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.core.domain.PageQuery; - -import java.util.Collection; -import java.util.List; - -/** - * 生产订单Service接口 - * - * @author 田志阳 - * @date 2024-10-20 - */ -public interface IProductionOrderService { - - /** - * 查询生产订单 - */ - ProductionOrderVo queryById(Long id); - - /** - * 查询生产订单列表 - */ - TableDataInfo queryPageList(ProductionOrderBo bo, PageQuery pageQuery); - - /** - * 查询生产订单列表 - */ - List queryList(ProductionOrderBo bo); - - /** - * 新增生产订单 - */ - Boolean insertByBo(ProductionOrderBo bo); - - /** - * 修改生产订单 - */ - Boolean updateByBo(ProductionOrderBo bo); - - /** - * 校验并批量删除生产订单信息 - */ - Boolean deleteWithValidByIds(Collection ids, Boolean isValid); - - void loadProductionOrder(List productionOrderVoList); - - List getGroupWeld(ProductionOrderVo productionOrderVo); - - List getGroupWeldSubset(ProductionOrderVo productionOrderVo); - - Boolean executDrawing(ProcessOrderProBo orderPro); - - List selectByProCode(String productionOrderNo); - /** - * 是否属于外购件 - */ - Boolean isPurchas(String proCode,String materialCode); -} +package com.ruoyi.system.service; + +import com.ruoyi.system.domain.ProductionOrder; +import com.ruoyi.system.domain.bo.FigureSaveBo; +import com.ruoyi.system.domain.bo.ProcessOrderProBo; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO; +import com.ruoyi.system.domain.vo.ProductionOrderVo; +import com.ruoyi.system.domain.bo.ProductionOrderBo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 生产订单Service接口 + * + * @author 田志阳 + * @date 2024-10-20 + */ +public interface IProductionOrderService { + + /** + * 查询生产订单 + */ + ProductionOrderVo queryById(Long id); + + /** + * 查询生产订单列表 + */ + TableDataInfo queryPageList(ProductionOrderBo bo, PageQuery pageQuery); + + /** + * 查询生产订单列表 + */ + List queryList(ProductionOrderBo bo); + + /** + * 新增生产订单 + */ + Boolean insertByBo(ProductionOrderBo bo); + + /** + * 修改生产订单 + */ + Boolean updateByBo(ProductionOrderBo bo); + + /** + * 校验并批量删除生产订单信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + void loadProductionOrder(List productionOrderVoList); + + List getGroupWeld(ProductionOrderVo productionOrderVo); + + List getGroupWeldSubset(ProductionOrderVo productionOrderVo); + + Boolean executDrawing(ProcessOrderProBo orderPro); + + List selectByProCode(String productionOrderNo); + /** + * 是否属于外购件 + */ + Boolean isPurchas(String proCode,String materialCode); + + /** + * 查询生产令号列表(去重,用于下拉选择) + */ + List selectProductionOrderNoOptions(String keyword, Integer limit); + + /** + * 通过物料编码匹配系统单重(未匹配数据会跳过) + */ + List matchSingleWeightByMaterialCode(List rows); + + /** + * 按物料图号/编码解析单重:先 production_order.drawing_no(最新一条且 single_weight 非空),再 process_route.material_code 的 disc_weight。 + * 未查到返回 null。 + */ + Double resolveSingleWeightByMaterialCode(String materialCode); + + /** + * 批量解析单重(内部 IN 分批),仅返回能查到单重的编码;查不到的编码不出现在 Map 中。 + */ + Map resolveSingleWeightByMaterialCodes(Collection materialCodes); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/MssqlQueryService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/MssqlQueryService.java index aadddba..17143f6 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/MssqlQueryService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/MssqlQueryService.java @@ -1,16 +1,26 @@ package com.ruoyi.system.service; import com.baomidou.dynamic.datasource.annotation.DS; +import com.ruoyi.common.exception.ServiceException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.bo.KingdeeOrderTraceQueryBo; import com.ruoyi.system.domain.bo.KingdeeWorkCenterDataBo; +import com.ruoyi.system.domain.vo.KingdeeMoTraceVo; +import com.ruoyi.system.domain.vo.KingdeeOrderTraceVo; +import com.ruoyi.system.domain.vo.KingdeePoTraceVo; + import java.math.BigDecimal; import java.math.RoundingMode; +import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; @Service public class MssqlQueryService { @@ -26,6 +36,208 @@ public class MssqlQueryService { return jdbcTemplate.queryForList(sql); } + /** + * 按生产令号、采购单号、生产订单号、产品编码(均可选,至少填一项)查询金蝶采购与生产追溯数据。 + */ + @DS("sqlserver") + public KingdeeOrderTraceVo queryOrderTrace(KingdeeOrderTraceQueryBo bo) { + if (bo == null) { + bo = new KingdeeOrderTraceQueryBo(); + } + List sclhList = normalizeSclhList(bo.getSclh()); + String purchaseBillNo = blankToNull(bo.getPurchaseBillNo()); + String moBillNo = blankToNull(bo.getMoBillNo()); + String materialNumber = blankToNull(bo.getMaterialNumber()); + if (sclhList.isEmpty() && purchaseBillNo == null && moBillNo == null && materialNumber == null) { + throw new ServiceException("请至少填写生产令号、采购订单、生产订单或产品编码之一"); + } + + KingdeeOrderTraceVo vo = new KingdeeOrderTraceVo(); + vo.setPurchaseList(queryPurchaseTrace(sclhList, purchaseBillNo, materialNumber)); + vo.setProductionList(queryProductionTrace(sclhList, moBillNo, materialNumber)); + return vo; + } + + private List queryPurchaseTrace(List sclhList, String purchaseBillNo, String materialNumber) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT "); + sql.append("PO.F_UCHN_TEXT2 AS sclh, PO.FBILLNO AS purchaseBillNo, PO.FDATE AS purchaseDate, "); + sql.append("MAT.FNUMBER AS materialNumber, MAT_L.FNAME AS materialName, PO_E.FQty AS purchaseQty, "); + sql.append("INSTOCK.FBILLNO AS instockBillNo, INSTOCK.FDATE AS instockDate, INSTOCK_E.FREALQTY AS instockQty "); + sql.append("FROM dbo.T_PUR_POORDER PO "); + sql.append("INNER JOIN dbo.T_PUR_POORDERENTRY PO_E ON PO.FID = PO_E.FID "); + sql.append("LEFT JOIN dbo.T_BD_MATERIAL MAT ON PO_E.FMATERIALID = MAT.FMATERIALID "); + sql.append("LEFT JOIN dbo.T_BD_MATERIAL_L MAT_L ON MAT.FMATERIALID = MAT_L.FMATERIALID AND MAT_L.FLOCALEID = 2052 "); + sql.append("LEFT JOIN dbo.T_STK_INSTOCKENTRY INSTOCK_E ON PO_E.FENTRYID = INSTOCK_E.FPOORDERENTRYID "); + sql.append("LEFT JOIN dbo.T_STK_INSTOCK INSTOCK ON INSTOCK_E.FID = INSTOCK.FID "); + sql.append("WHERE 1 = 1 "); + List args = new ArrayList<>(); + appendIn(sql, args, "PO.F_UCHN_TEXT2", sclhList); + appendEq(sql, args, "PO.FBILLNO", purchaseBillNo); + appendEq(sql, args, "MAT.FNUMBER", materialNumber); + sql.append("ORDER BY PO.FDATE DESC"); + return mapPoRows(jdbcTemplate.queryForList(sql.toString(), args.toArray())); + } + + private List queryProductionTrace(List sclhList, String moBillNo, String materialNumber) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT "); + sql.append("MO.F_HBYT_SCLH AS sclh, MO.FBILLNO AS moBillNo, MAT.FNUMBER AS materialNumber, MAT_L.FNAME AS materialName, "); + sql.append("MO_E.FQty AS qty, MO.FDATE AS orderDate, OP.FBILLNO AS operPlanningBillNo, OP.FID AS operPlanningId, "); + sql.append("MO_E.FPlanStartDate AS planStartDate, MO_E.FPlanFinishDate AS planFinishDate, "); + sql.append("INSTOCK.FBILLNO AS instockBillNo, INSTOCK.FDATE AS instockDate, INSTOCK_E.FREALQTY AS instockQty "); + sql.append("FROM dbo.T_PRD_MO MO "); + sql.append("INNER JOIN dbo.T_PRD_MOENTRY MO_E ON MO.FID = MO_E.FID "); + sql.append("LEFT JOIN dbo.T_BD_MATERIAL MAT ON MO_E.FMATERIALID = MAT.FMATERIALID "); + sql.append("LEFT JOIN dbo.T_BD_MATERIAL_L MAT_L ON MAT.FMATERIALID = MAT_L.FMATERIALID AND MAT_L.FLOCALEID = 2052 "); + sql.append("LEFT JOIN dbo.T_SFC_OPERPLANNING OP ON MO.FID = OP.FMOID AND MO_E.FENTRYID = OP.FMOENTRYID "); + sql.append("LEFT JOIN dbo.T_PRD_INSTOCKENTRY INSTOCK_E ON MO.FID = INSTOCK_E.FMOID AND MO_E.FENTRYID = INSTOCK_E.FMOENTRYID "); + sql.append("LEFT JOIN dbo.T_PRD_INSTOCK INSTOCK ON INSTOCK_E.FID = INSTOCK.FID "); + sql.append("WHERE 1 = 1 "); + List args = new ArrayList<>(); + appendIn(sql, args, "MO.F_HBYT_SCLH", sclhList); + appendEq(sql, args, "MO.FBILLNO", moBillNo); + appendEq(sql, args, "MAT.FNUMBER", materialNumber); + sql.append("ORDER BY MO.FDATE DESC"); + return mapMoRows(jdbcTemplate.queryForList(sql.toString(), args.toArray())); + } + + private static void appendEq(StringBuilder sql, List args, String column, String value) { + if (value != null) { + sql.append(" AND ").append(column).append(" = ? "); + args.add(value); + } + } + + /** + * 生产令号 IN 查询;列表为空则不追加条件。 + */ + private static void appendIn(StringBuilder sql, List args, String column, List values) { + if (values == null || values.isEmpty()) { + return; + } + sql.append(" AND ").append(column).append(" IN ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + sql.append("?"); + args.add(values.get(i)); + } + sql.append(") "); + } + + /** + * 合并多个传参及逗号分隔项,去重保序。 + */ + private static List normalizeSclhList(List raw) { + if (raw == null || raw.isEmpty()) { + return new ArrayList<>(); + } + Set seen = new LinkedHashSet<>(); + for (String s : raw) { + if (StringUtils.isBlank(s)) { + continue; + } + String t = s.trim(); + if (t.contains(",")) { + for (String part : t.split(",")) { + String p = part.trim(); + if (StringUtils.isNotBlank(p)) { + seen.add(p); + } + } + } else { + seen.add(t); + } + } + return new ArrayList<>(seen); + } + + private static String blankToNull(String s) { + if (StringUtils.isBlank(s)) { + return null; + } + return s.trim(); + } + + private List mapPoRows(List> rows) { + List list = new ArrayList<>(); + for (Map row : rows) { + KingdeePoTraceVo v = new KingdeePoTraceVo(); + v.setSclh(getString(row, "sclh")); + v.setPurchaseBillNo(getString(row, "purchaseBillNo")); + v.setPurchaseDate(toUtilDate(getCell(row, "purchaseDate"))); + v.setMaterialNumber(getString(row, "materialNumber")); + v.setMaterialName(getString(row, "materialName")); + v.setPurchaseQty(getBigDecimal(row, "purchaseQty")); + v.setInstockBillNo(getString(row, "instockBillNo")); + v.setInstockDate(toUtilDate(getCell(row, "instockDate"))); + v.setInstockQty(getBigDecimal(row, "instockQty")); + list.add(v); + } + return list; + } + + private List mapMoRows(List> rows) { + List list = new ArrayList<>(); + for (Map row : rows) { + KingdeeMoTraceVo v = new KingdeeMoTraceVo(); + v.setSclh(getString(row, "sclh")); + v.setMoBillNo(getString(row, "moBillNo")); + v.setMaterialNumber(getString(row, "materialNumber")); + v.setMaterialName(getString(row, "materialName")); + v.setQty(getBigDecimal(row, "qty")); + v.setOrderDate(toUtilDate(getCell(row, "orderDate"))); + v.setOperPlanningBillNo(getString(row, "operPlanningBillNo")); + v.setOperPlanningId(getLong(row, "operPlanningId")); + v.setPlanStartDate(toUtilDate(getCell(row, "planStartDate"))); + v.setPlanFinishDate(toUtilDate(getCell(row, "planFinishDate"))); + v.setInstockBillNo(getString(row, "instockBillNo")); + v.setInstockDate(toUtilDate(getCell(row, "instockDate"))); + v.setInstockQty(getBigDecimal(row, "instockQty")); + list.add(v); + } + return list; + } + + private Object getCell(Map row, String key) { + if (row.containsKey(key)) { + return row.get(key); + } + for (Map.Entry e : row.entrySet()) { + if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) { + return e.getValue(); + } + } + return null; + } + + private Date toUtilDate(Object val) { + if (val == null) { + return null; + } + if (val instanceof Date) { + return (Date) val; + } + if (val instanceof Timestamp) { + return new Date(((Timestamp) val).getTime()); + } + return null; + } + + private Long getLong(Map row, String key) { + Object val = getCell(row, key); + if (val == null) { + return null; + } + try { + return Long.parseLong(val.toString()); + } catch (NumberFormatException e) { + return null; + } + } + /** * 根据物料编码查询是否是VMI业务 * @param materialNumber 物料编码 @@ -147,13 +359,13 @@ public class MssqlQueryService { } private String getString(Map row, String key) { - Object val = row.get(key); + Object val = getCell(row, key); return val != null ? val.toString() : ""; } private BigDecimal getBigDecimal(Map row, String key) { - Object val = row.get(key); + Object val = getCell(row, key); if (val == null) return BigDecimal.ZERO; try { return new BigDecimal(val.toString()).setScale(2, RoundingMode.HALF_UP); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java index 928904d..32d7fa4 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java @@ -16,6 +16,7 @@ import com.ruoyi.common.core.domain.model.XcxLoginUser; import com.ruoyi.common.enums.DeviceType; import com.ruoyi.common.enums.LoginType; import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.user.CaptchaException; import com.ruoyi.common.exception.user.CaptchaExpireException; import com.ruoyi.common.exception.user.UserException; @@ -34,6 +35,8 @@ import org.springframework.stereotype.Service; import java.time.Duration; import java.util.List; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.function.Supplier; /** @@ -56,6 +59,15 @@ public class SysLoginService { @Value("${user.password.lockTime}") private Integer lockTime; + @Value("${plm.auth.app-key:}") + private String plmAuthAppKey; + + @Value("${plm.auth.app-secret:}") + private String plmAuthAppSecret; + + @Value("${plm.auth.username:}") + private String plmAuthUsername; + /** * 登录验证 * @@ -135,6 +147,27 @@ public class SysLoginService { recordLoginInfo(user.getUserId(), user.getUserName()); return StpUtil.getTokenValue(); } + + public String plmToken(String appKey, String appSecret) { + if (StringUtils.isBlank(plmAuthAppKey) || StringUtils.isBlank(plmAuthAppSecret)) { + throw new ServiceException("PLM鉴权未配置"); + } + if (!safeEquals(plmAuthAppKey, StringUtils.trimToEmpty(appKey)) + || !safeEquals(plmAuthAppSecret, StringUtils.trimToEmpty(appSecret))) { + recordLogininfor("plm", Constants.LOGIN_FAIL, "plm auth failed"); + throw new ServiceException("PLM鉴权失败"); + } + if (StringUtils.isBlank(plmAuthUsername)) { + throw new ServiceException("PLM鉴权用户未配置"); + } + + SysUser user = loadUserByUsername(plmAuthUsername); + LoginUser loginUser = buildLoginUser(user); + LoginHelper.loginByDevice(loginUser, DeviceType.PC); + recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS, "plm token issued"); + recordLoginInfo(user.getUserId(), user.getUserName()); + return StpUtil.getTokenValue(); + } /** * 退出登录 @@ -330,4 +363,11 @@ public class SysLoginService { // 登录成功 清空错误次数 RedisUtils.deleteObject(errorKey); } + + private static boolean safeEquals(String a, String b) { + if (a == null || b == null) { + return false; + } + return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8)); + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BomDetailsServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BomDetailsServiceImpl.java index 28678ba..e218717 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BomDetailsServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BomDetailsServiceImpl.java @@ -218,7 +218,7 @@ public class BomDetailsServiceImpl implements IBomDetailsService { )); for (BomDetails bomDetail : bomDetails) { - String key = bomDetail.getPartNumber() + "-" + bomDetail.getName(); + String key = bomDetail.getPartNumber() + "-" + bomDetail.getName()+System.currentTimeMillis(); MaterialBom matchedMaterialBom = materialBomMap.get(key); if (matchedMaterialBom != null) { if (matchedMaterialBom.getUnit().equals("根")) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ImMaterialServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ImMaterialServiceImpl.java index c549c66..07cfd3f 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ImMaterialServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ImMaterialServiceImpl.java @@ -15,23 +15,31 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.kingdee.bos.webapi.sdk.K3CloudApi; 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.BeanCopyUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.controller.ImMaterialController; import com.ruoyi.system.domain.ImMaterial; +import com.ruoyi.system.domain.ProductionOrder; import com.ruoyi.system.domain.bo.ImMaterialBo; import com.ruoyi.system.domain.dto.JDInventoryDTO; import com.ruoyi.system.domain.dto.JDProductionDTO; import com.ruoyi.system.domain.dto.ProMoDTO; import com.ruoyi.system.domain.dto.PurchaseOrderDTO; +import com.ruoyi.system.domain.dto.buildcapacity.BuildCapacityDTO; +import com.ruoyi.system.domain.dto.buildcapacity.CalcInfo; +import com.ruoyi.system.domain.dto.buildcapacity.MaterialItem; +import com.ruoyi.system.domain.dto.buildcapacity.StockInfo; +import com.ruoyi.system.domain.dto.buildcapacity.StockStatus; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialItemDTO; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderMaterialsDTO; import com.ruoyi.system.domain.vo.ImMaterialVo; import com.ruoyi.system.mapper.ImMaterialMapper; 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 lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.math.NumberUtils; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.slf4j.Logger; @@ -42,6 +50,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; +import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -60,6 +69,7 @@ import static com.ruoyi.common.core.mapper.BaseMapperPlus.log; public class ImMaterialServiceImpl implements IImMaterialService { private final ImMaterialMapper baseMapper; + private final IProductionOrderService productionOrderService; private static final Logger logger = LoggerFactory.getLogger(ImMaterialController.class); @Autowired @@ -878,4 +888,300 @@ public class ImMaterialServiceImpl implements IImMaterialService { } + @Override + public BuildCapacityDTO queryBuildCapacity(String productionOrderNo, String productCode, String productName) { + // 齐套查询:不查金蝶 BOM,仅使用本地 production_order 中的物料行作为“清单来源” + BuildCapacityDTO result = new BuildCapacityDTO(); + result.setProductCode(productCode); + result.setProductName(productName); + result.setSelfMadeMaterials(new ArrayList<>()); + result.setPurchaseMaterials(new ArrayList<>()); + result.setMaxBuildQty(0); + if (StringUtils.isBlank(productionOrderNo) || StringUtils.isBlank(productCode)) { + return result; + } + + String proNo = productionOrderNo.trim(); + String proCode = productCode.trim(); + // 生产令号下的所有行(包含成品行/子项行) + List productionOrderList = productionOrderService.selectByProCode(proNo); + if (CollectionUtils.isEmpty(productionOrderList)) { + return result; + } + + if (StringUtils.isBlank(result.getProductName())) { + // 未传 productName 时,从本地数据中回填成品名称 + result.setProductName(productionOrderList.stream() + .filter(p -> p != null && StringUtils.isNotBlank(p.getDrawingNo()) && proCode.equals(p.getDrawingNo().trim())) + .map(ProductionOrder::getDrawingName) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse(null)); + } + + // 逐行计算:productionOrderList 有多少条,就计算多少条(不做父子级过滤/不做按编码合并) + List allItems = new ArrayList<>(); + int minCanSupport = Integer.MAX_VALUE; + + for (ProductionOrder p : productionOrderList) { + if (p == null || StringUtils.isBlank(p.getDrawingNo())) { + continue; + } + String materialCode = p.getDrawingNo().trim(); + int requiredQty = normalizeQtyToInt(p.getQuantity()); + if (requiredQty <= 0) { + requiredQty = 1; + } + + int onHandQty = normalizeToInt(Optional.ofNullable(JdUtil.getKuCun(materialCode)) + .map(list -> list.stream().mapToDouble(JDInventoryDTO::getFBaseQty).sum()) + .orElse(0.0)); + int moNotPickQty = normalizeToInt(Optional.ofNullable(JdUtil.getWeiLingliao(materialCode)) + .map(list -> list.stream().mapToDouble(JDProductionDTO::getFNoPickedQty).sum()) + .orElse(0.0)); + int moNotInQty = normalizeToInt(Optional.ofNullable(JdUtil.getProMoList(materialCode)) + .map(list -> list.stream().mapToDouble(ProMoDTO::getFQty).sum()) + .orElse(0.0)); + int poNotInQty = normalizeToInt(Optional.ofNullable(JdUtil.getPurchaseOrderList(materialCode)) + .map(list -> list.stream().mapToDouble(PurchaseOrderDTO::getFRemainStockINQty).sum()) + .orElse(0.0)); + + int availableQty = onHandQty - moNotPickQty; + int finalAvailableQty = availableQty + poNotInQty + moNotInQty; + int canSupportQty = Math.max(0, finalAvailableQty / requiredQty); + + StockInfo stock = new StockInfo(); + stock.setOnHandQty(onHandQty); + stock.setMoNotPickQty(moNotPickQty); + stock.setMoNotInQty(moNotInQty); + stock.setPoNotInQty(poNotInQty); + + CalcInfo calc = new CalcInfo(); + calc.setAvailableQty(availableQty); + calc.setFinalAvailableQty(finalAvailableQty); + calc.setCanSupportQty(canSupportQty); + + MaterialItem item = new MaterialItem(); + item.setMaterialCode(materialCode); + item.setMaterialName(p.getDrawingName()); + item.setRequiredQty(requiredQty); + item.setMaterial(p.getMaterial()); + item.setSingleWeight(p.getSingleWeight()); + item.setTotalWeight(p.getTotalWeight()); + item.setRemark(p.getRemark()); + item.setBatchQuantity(p.getBatchQuantity()); + item.setStock(stock); + item.setCalc(calc); + item.setStatus(finalAvailableQty >= requiredQty ? StockStatus.OK : StockStatus.SHORTAGE); + allItems.add(item); + + minCanSupport = Math.min(minCanSupport, canSupportQty); + } + + if (minCanSupport == Integer.MAX_VALUE) { + minCanSupport = 0; + } + + List purchaseMaterials = new ArrayList<>(); + List selfMadeMaterials = new ArrayList<>(); + for (MaterialItem item : allItems) { + String code = item.getMaterialCode(); + if (StringUtils.isNotBlank(code) && code.startsWith("009")) { + purchaseMaterials.add(item); + } else { + selfMadeMaterials.add(item); + } + } + + result.setPurchaseMaterials(purchaseMaterials); + result.setSelfMadeMaterials(selfMadeMaterials); + result.setMaxBuildQty(minCanSupport); + return result; + } + @Override + public List queryBuildCapacityByProductionOrderNo(String productionOrderNo) { + if (StringUtils.isBlank(productionOrderNo)) { + return Collections.emptyList(); + } + String proNo = productionOrderNo.trim(); + List productionOrderList = productionOrderService.selectByProCode(proNo); + if (CollectionUtils.isEmpty(productionOrderList)) { + return Collections.emptyList(); + } + + List topLevel = productionOrderList.stream() + .filter(p -> StringUtils.isBlank(p.getParentDrawingNo())) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(topLevel)) { + topLevel = productionOrderList; + } + + Map productMap = new LinkedHashMap<>(); + for (ProductionOrder p : topLevel) { + if (p == null || StringUtils.isBlank(p.getDrawingNo())) { + continue; + } + productMap.putIfAbsent(p.getDrawingNo().trim(), p); + } + + List list = new ArrayList<>(); + for (ProductionOrder p : productMap.values()) { + list.add(queryBuildCapacity(proNo, p.getDrawingNo(), p.getDrawingName())); + } + return list; + } + + private static int normalizeQtyToInt(Double value) { + if (value == null) { + return 0; + } + if (value <= 0) { + return 0; + } + return (int) Math.ceil(value); + } + + private static int safeInt(Integer v) { + return v == null ? 0 : v; + } + + private static double safeDouble(Double v) { + return v == null ? 0D : v; + } + + @Override + public ProductionOrderMaterialsDTO queryProductionOrderMaterials(String productionOrderNo, String productCode) { + ProductionOrderMaterialsDTO dto = new ProductionOrderMaterialsDTO(); + dto.setProductionOrderNo(productionOrderNo); + dto.setProductCode(productCode); + dto.setMaterials(Collections.emptyList()); + if (StringUtils.isBlank(productionOrderNo) || StringUtils.isBlank(productCode)) { + return dto; + } + + String proNo = productionOrderNo.trim(); + String proCode = productCode.trim(); + List productionOrderList = productionOrderService.selectByProCode(proNo); + if (CollectionUtils.isEmpty(productionOrderList)) { + return dto; + } + + dto.setProductName(productionOrderList.stream() + .filter(p -> p != null && StringUtils.isNotBlank(p.getDrawingNo()) && proCode.equals(p.getDrawingNo().trim())) + .map(ProductionOrder::getDrawingName) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse(null)); + + List materials = productionOrderList.stream() + .filter(p -> p != null) + .filter(p -> StringUtils.isNotBlank(p.getParentDrawingNo()) && proCode.equals(p.getParentDrawingNo().trim())) + .map(p -> { + ProductionOrderMaterialItemDTO item = new ProductionOrderMaterialItemDTO(); + item.setMaterialCode(p.getDrawingNo()); + item.setMaterialName(p.getDrawingName()); + item.setQty(p.getQuantity()); + item.setMaterial(p.getMaterial()); + item.setSingleWeight(p.getSingleWeight()); + item.setTotalWeight(p.getTotalWeight()); + item.setRemark(p.getRemark()); + item.setParentDrawingNo(p.getParentDrawingNo()); + item.setParentPart(p.getParentPart()); + item.setBatchQuantity(p.getBatchQuantity()); + return item; + }) + .sorted(Comparator.comparing(ProductionOrderMaterialItemDTO::getMaterialCode, Comparator.nullsLast(String::compareTo))) + .collect(Collectors.toList()); + + dto.setMaterials(materials); + return dto; + } + + @Override + public R> refreshImMaterialSingleWeightFromOrders() { + final int pageSize = 1000; + long scanned = 0; + long updated = 0; + long unchanged = 0; + long noResolved = 0; + int pageNum = 1; + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.isNotNull(ImMaterial::getMaterialCode); + wrapper.ne(ImMaterial::getMaterialCode, ""); + wrapper.orderByAsc(ImMaterial::getId); + + Date now = new Date(); + String modifyDay = new SimpleDateFormat("yyyy-MM-dd").format(now); + + while (true) { + Page pageResult = baseMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); + List records = pageResult.getRecords(); + if (records.isEmpty()) { + break; + } + scanned += records.size(); + List codes = records.stream() + .map(ImMaterial::getMaterialCode) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + Map weightMap = productionOrderService.resolveSingleWeightByMaterialCodes(codes); + for (ImMaterial row : records) { + if (StringUtils.isBlank(row.getMaterialCode())) { + continue; + } + String code = row.getMaterialCode().trim(); + Double resolved = weightMap.get(code); + if (resolved == null) { + noResolved++; + continue; + } + BigDecimal newBd = BigDecimal.valueOf(resolved).stripTrailingZeros(); + if (singleWeightUnchanged(row.getSingleWeight(), newBd)) { + unchanged++; + continue; + } + row.setSingleWeight(newBd); + row.setUpdateTime(now); + row.setModifyDate(modifyDay); + baseMapper.updateById(row); + updated++; + } + if (records.size() < pageSize) { + break; + } + pageNum++; + } + Map data = new LinkedHashMap<>(); + data.put("scanned", scanned); + data.put("updated", updated); + data.put("unchanged", unchanged); + data.put("noResolvedWeight", noResolved); + return R.ok("同步完成", data); + } + + private static boolean singleWeightUnchanged(BigDecimal oldBd, BigDecimal newBd) { + if (oldBd == null && newBd == null) { + return true; + } + if (oldBd == null || newBd == null) { + return false; + } + return oldBd.stripTrailingZeros().compareTo(newBd.stripTrailingZeros()) == 0; + } + + private static double toRequiredQty(Double fenzi, Integer fenmu) { + double numerator = fenzi == null ? 0D : fenzi; + int denominator = fenmu == null || fenmu == 0 ? 1 : fenmu; + return numerator / denominator; + } + + private static int normalizeToInt(double value) { + if (value <= 0) { + return 0; + } + return (int) Math.floor(value); + } + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessOrderProServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessOrderProServiceImpl.java index a0f5543..2957b7c 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessOrderProServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessOrderProServiceImpl.java @@ -6,6 +6,7 @@ import cn.hutool.core.io.IoUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.JsonProcessingException; @@ -19,7 +20,6 @@ import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.dingding.robot.RobotGroupService; import com.ruoyi.common.dingding.token.AccessTokenManager; -import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.dingding.webhook.WebhookClient; import com.ruoyi.common.helper.LoginHelper; import com.ruoyi.common.utils.FtpUtil; @@ -37,6 +37,7 @@ import com.ruoyi.system.domain.dto.excuteDrawing.DataInfo; import com.ruoyi.system.domain.dto.excuteDrawing.ProductInfo; import com.ruoyi.system.domain.dto.excuteDrawing.PwProductionBill; import com.ruoyi.system.domain.dto.excuteDrawing.RigidChainModelDTO; +import com.ruoyi.system.domain.dto.excuteDrawing.Subparts; import com.ruoyi.system.domain.vo.OverdueProjectVo; import com.ruoyi.system.domain.vo.ProcessOrderProVo; import com.ruoyi.system.domain.vo.ProductionOrderVo; @@ -45,6 +46,7 @@ import com.ruoyi.system.listener.FileToZip; import com.ruoyi.system.listener.SmbUtils; import com.ruoyi.system.mapper.*; import com.ruoyi.system.service.*; +import com.ruoyi.system.utils.ProductionOrderNoUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; @@ -66,7 +68,6 @@ import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -102,6 +103,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { private final IProductionOrderService iProductionOrderService; private final ISysUserService userService; private final ProductionOrderMapper productionOrderMapper; + private final ImMaterialMapper mapper; @Autowired private AccessTokenManager accessTokenManager; @Autowired @@ -135,6 +137,9 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { @Autowired private MaterialBomMapper materialBomMapper; + /** PDF 重命名:原文件名主体 与 「图号+名称」后缀之间的分隔(Windows 合法;后缀中的 {@code @} 会在 sanitize 中去掉) */ + private static final String PDF_ORIGINAL_TAIL_SEP = "--"; + public static String[] getNullPropertyNames(Object source) { BeanWrapper src = new BeanWrapperImpl(source); java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); @@ -344,6 +349,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { lqw.eq(StringUtils.isNotBlank(bo.getDrawingType()), ProcessOrderPro::getDrawingType, bo.getDrawingType()); lqw.eq(bo.getProjectEndTime() != null, ProcessOrderPro::getProjectEndTime, bo.getProjectEndTime()); lqw.eq(StringUtils.isNotBlank(bo.getUnit()), ProcessOrderPro::getUnit, bo.getUnit()); + lqw.eq(bo.getIsDownload() != null, ProcessOrderPro::getIsDownload, bo.getIsDownload()); lqw.eq(bo.getQuantity() != null, ProcessOrderPro::getQuantity, bo.getQuantity()); lqw.eq(bo.getBomStatus() != null, ProcessOrderPro::getBomStatus, bo.getBomStatus()); lqw.eq(bo.getIsEnterpriseStandard() != null, ProcessOrderPro::getIsEnterpriseStandard, bo.getIsEnterpriseStandard()); @@ -741,8 +747,8 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { // 1. 参数验证 validateOrderPro(orderPro); - // 2. 查询生产令号是否重复 - ProcessOrderPro processOrderPro = checkProductionOrderExists(orderPro.getProductionOrderNo()); + // 2. 查询生产令号(日志) + checkProductionOrderExists(orderPro.getProductionOrderNo()); // 2.1 检查出图服务是否繁忙 try { @@ -753,7 +759,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { throw new RuntimeException("出图服务忙,有其他人在出图中,请稍后再试"); } } catch (Exception e) { - if (e instanceof RuntimeException && e.getMessage().contains("出图服务忙")) { + if (e.getMessage().contains("出图服务忙")) { throw (RuntimeException) e; } log.warn("检查出图服务状态失败,继续执行: {}", e.getMessage()); @@ -773,108 +779,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { String apiResult = null; try { apiResult = callDrawingApi(pwProductionBill, orderPro); - - // 只有成功才发送通知和文件 - CompletableFuture.runAsync(() -> { - try { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - // 获取 AccessToken - String accessToken = DingUtil.getAccessToken(dingTalkProperties.getAppKey(), dingTalkProperties.getAppSecret()); - - // 构造Markdown消息内容 - StringBuilder markdownText = new StringBuilder(); - markdownText.append("**项目创建出图完成,请及时填写工艺**\n\n"); - markdownText.append("> **时间**: ").append(sdf.format(new Date())).append("\n\n"); - markdownText.append("> **项目令号**: ").append(StringUtils.defaultIfEmpty(orderPro.getProductionOrderNo(), "")).append("\n\n"); - markdownText.append("> **项目名称**: ").append(StringUtils.defaultIfEmpty(orderPro.getProductionName(), "")).append("\n\n"); - - // 补充更多实体信息(若库中尚无记录,processOrderPro 可能为 null) - ProcessOrderPro po = processOrderPro; - SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd"); - if (po != null) { - - String planStart = po.getPlanStartTime() == null ? "" : sdfDate.format(po.getPlanStartTime()); - String planEnd = po.getPlanEndTime() == null ? "" : sdfDate.format(po.getPlanEndTime()); - if (StringUtils.isNotBlank(planStart) || StringUtils.isNotBlank(planEnd)) { - markdownText.append("> **计划周期**: ").append(planStart).append(" ~ ").append(planEnd).append("\n\n"); - } - - if (StringUtils.isNotBlank(po.getUnit()) || po.getQuantity() != null) { - markdownText.append("> **单位/数量**: ").append(StringUtils.defaultIfEmpty(po.getUnit(), "")).append(StringUtils.isNotBlank(po.getUnit()) && po.getQuantity() != null ? " / " : "").append(po.getQuantity() == null ? "" : po.getQuantity()).append("\n\n"); - } - - if (po.getIsEnterpriseStandard() != null) { - markdownText.append("> **是否企标**: ").append(Objects.equals(po.getIsEnterpriseStandard(), "1") ? "是" : "否").append("\n\n"); - } - - String bomStatus = null; - if (po.getBomStatus() != null) { - if (po.getBomStatus() == 0) bomStatus = "未完成"; - else if (po.getBomStatus() == 1) bomStatus = "完成"; - else if (po.getBomStatus() == 2) bomStatus = "已上传"; - else bomStatus = String.valueOf(po.getBomStatus()); - } - String routeStatus = null; - if (po.getRouteStatus() != null) { - if (po.getRouteStatus() == 0) routeStatus = "未审核"; - else if (po.getRouteStatus() == 1) routeStatus = "已审核"; - else routeStatus = String.valueOf(po.getRouteStatus()); - } - if (StringUtils.isNotBlank(bomStatus) || StringUtils.isNotBlank(routeStatus)) { - markdownText.append("> **BOM/工艺状态**: ").append(StringUtils.defaultIfEmpty(bomStatus, "-")).append(" / ").append(StringUtils.defaultIfEmpty(routeStatus, "-")).append("\n\n"); - } - - if (po.getDrawingTime() != null) { - markdownText.append("> **出图时间**: ").append(sdf.format(po.getDrawingTime())).append("\n\n"); - } - if (StringUtils.isNotBlank(po.getDrawingBy())) { - markdownText.append("> **出图人**: ").append(po.getDrawingBy()).append("\n\n"); - } - } - - markdownText.append("\n请相关人员及时处理。"); - - // 发送Markdown消息给个人 - if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) { - robotGroupService.sendMarkdownToUser(dingTalkProperties.getNotifyUserId(), "生产项目创建通知", markdownText.toString()); - } else { - log.warn("未配置钉钉通知个人ID(notifyUserId),跳过发送Markdown消息"); - } - - // 下载并发送Excel文件 - log.info("开始生成并发送Excel模板文件: {}", orderPro.getProductionOrderNo()); - - String filePath = null; - try { - ProcessOrderProController controller = SpringUtils.getBean(ProcessOrderProController.class); - filePath = controller.generateRoute3Excel(orderPro.getId(), true); - } catch (Exception e) { - log.error("生成工艺计划表模板Excel失败", e); - } - - if (filePath != null) { - File excelFile = new File(filePath); - if (excelFile.exists() && excelFile.length() > 0) { - // Upload media - String mediaId = mediaService.uploadMedia("file", filePath); - // Send file给个人 - if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) { - robotGroupService.sendFileToUser(dingTalkProperties.getNotifyUserId(), mediaId, excelFile.getName()); - log.info("Excel模板文件发送成功"); - } else { - log.warn("未配置钉钉通知个人ID(notifyUserId),跳过发送Excel文件"); - } - // 发送完毕后删除生成的临时文件 - excelFile.delete(); - } else { - log.warn("Excel文件未找到或为空: {}", filePath); - } - } - - } catch (Exception ex) { - log.error("发送出图通知/文件失败", ex); - } - }); + // 出图成功后不再自动发钉钉;由前端调用 sendProcessRoutePlanExcel 一并发送 Markdown 通知与工艺计划表 Excel return apiResult; } catch (Exception e) { @@ -883,6 +788,152 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } } + @Override + public R sendProcessRoutePlanExcel(Long id) { + if (id == null) { + return R.fail("项目ID不能为空"); + } + ProcessOrderPro entity = baseMapper.selectById(id); + if (entity == null) { + return R.fail("项目不存在"); + } + return sendProcessRoutePlanExcelInternal(entity); + } + + @Override + public R getRoutePlanSendCount(Long id) { + if (id == null) { + return R.fail("项目ID不能为空"); + } + ProcessOrderPro entity = baseMapper.selectById(id); + if (entity == null) { + return R.fail("项目不存在"); + } + Integer n = entity.getRoutePlanSendCount(); + return R.ok(n != null ? n : 0); + } + + /** 向 notifyUserId 发送与原先出图完成一致的 Markdown 提醒(不含 Excel)。 */ + private void sendDrawingCompleteMarkdownNotify(ProcessOrderPro po) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + StringBuilder markdownText = new StringBuilder(); + markdownText.append("**项目创建出图完成,请及时填写工艺**\n\n"); + markdownText.append("> **时间**: ").append(sdf.format(new Date())).append("\n\n"); + markdownText.append("> **项目令号**: ").append(StringUtils.defaultIfEmpty(po.getProductionOrderNo(), "")).append("\n\n"); + markdownText.append("> **项目名称**: ").append(StringUtils.defaultIfEmpty(po.getProductionName(), "")).append("\n\n"); + + SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd"); + String planStart = po.getPlanStartTime() == null ? "" : sdfDate.format(po.getPlanStartTime()); + String planEnd = po.getPlanEndTime() == null ? "" : sdfDate.format(po.getPlanEndTime()); + if (StringUtils.isNotBlank(planStart) || StringUtils.isNotBlank(planEnd)) { + markdownText.append("> **计划周期**: ").append(planStart).append(" ~ ").append(planEnd).append("\n\n"); + } + + if (StringUtils.isNotBlank(po.getUnit()) || po.getQuantity() != null) { + markdownText.append("> **单位/数量**: ").append(StringUtils.defaultIfEmpty(po.getUnit(), "")).append(StringUtils.isNotBlank(po.getUnit()) && po.getQuantity() != null ? " / " : "").append(po.getQuantity() == null ? "" : po.getQuantity()).append("\n\n"); + } + + if (po.getIsEnterpriseStandard() != null) { + markdownText.append("> **是否企标**: ").append(Objects.equals(po.getIsEnterpriseStandard(), "1") ? "是" : "否").append("\n\n"); + } + + String bomStatus = null; + if (po.getBomStatus() != null) { + if (po.getBomStatus() == 0) bomStatus = "未完成"; + else if (po.getBomStatus() == 1) bomStatus = "完成"; + else if (po.getBomStatus() == 2) bomStatus = "已上传"; + else bomStatus = String.valueOf(po.getBomStatus()); + } + String routeStatus = null; + if (po.getRouteStatus() != null) { + if (po.getRouteStatus() == 0) routeStatus = "未审核"; + else if (po.getRouteStatus() == 1) routeStatus = "已审核"; + else routeStatus = String.valueOf(po.getRouteStatus()); + } + if (StringUtils.isNotBlank(bomStatus) || StringUtils.isNotBlank(routeStatus)) { + markdownText.append("> **BOM/工艺状态**: ").append(StringUtils.defaultIfEmpty(bomStatus, "-")).append(" / ").append(StringUtils.defaultIfEmpty(routeStatus, "-")).append("\n\n"); + } + + if (po.getDrawingTime() != null) { + markdownText.append("> **出图时间**: ").append(sdf.format(po.getDrawingTime())).append("\n\n"); + } + if (StringUtils.isNotBlank(po.getDrawingBy())) { + markdownText.append("> **出图人**: ").append(po.getDrawingBy()).append("\n\n"); + } + + markdownText.append("\n请相关人员及时处理。"); + robotGroupService.sendMarkdownToUser(dingTalkProperties.getNotifyUserId(), "生产项目创建通知", markdownText.toString()); + } + + /** + * 发送「出图完成」Markdown 通知,并生成工艺计划表 Excel(generateRoute3Excel(id, true))上传钉钉媒体发给 notifyUserId。 + */ + private R sendProcessRoutePlanExcelInternal(ProcessOrderPro orderPro) { + Long id = orderPro.getId(); + String productionOrderNo = orderPro.getProductionOrderNo(); + log.info("开始发送出图完成通知并下发工艺计划表Excel,令号: {}", productionOrderNo); + + if (StringUtils.isBlank(dingTalkProperties.getNotifyUserId())) { + log.warn("未配置钉钉通知个人ID(notifyUserId),无法发送工艺计划表文件"); + return R.fail("未配置钉钉通知用户(notifyUserId),无法发送文件"); + } + + try { + sendDrawingCompleteMarkdownNotify(orderPro); + } catch (Exception e) { + log.error("发送出图完成钉钉Markdown通知失败, id={}", id, e); + return R.fail("发送钉钉通知失败: " + e.getMessage()); + } + + String filePath; + try { + ProcessOrderProController controller = SpringUtils.getBean(ProcessOrderProController.class); + filePath = controller.generateRoute3Excel(id, true); + } catch (Exception e) { + log.error("生成工艺计划表Excel失败, id={}", id, e); + return R.fail("生成Excel失败: " + e.getMessage()); + } + + if (StringUtils.isBlank(filePath)) { + return R.fail("生成Excel失败:未返回文件路径"); + } + + File excelFile = new File(filePath); + if (!excelFile.exists() || excelFile.length() <= 0) { + return R.fail("Excel文件不存在或为空: " + filePath); + } + + try { + String mediaId = mediaService.uploadMedia("file", filePath); + robotGroupService.sendFileToUser(dingTalkProperties.getNotifyUserId(), mediaId, excelFile.getName()); + log.info("工艺计划表Excel已发送至钉钉用户,令号: {}", productionOrderNo); + } catch (Exception e) { + log.error("钉钉发送工艺计划表文件失败, id={}", id, e); + if (excelFile.exists() && !excelFile.delete()) { + log.warn("临时Excel删除失败: {}", excelFile.getAbsolutePath()); + } + return R.fail("钉钉发送失败: " + e.getMessage()); + } + + if (excelFile.exists() && !excelFile.delete()) { + log.warn("临时Excel删除失败: {}", excelFile.getAbsolutePath()); + } + + try { + LambdaUpdateWrapper uw = Wrappers.lambdaUpdate(); + uw.eq(ProcessOrderPro::getId, id) + .setSql("route_plan_send_count = IFNULL(route_plan_send_count, 0) + 1, update_time = NOW()"); + int rows = baseMapper.update(null, uw); + if (rows != 1) { + log.warn("发送次数累加影响行数异常, id={}, rows={}", id, rows); + } + } catch (Exception ex) { + log.error("记录工艺计划表发送次数失败, id={}", id, ex); + } + + return R.ok("发送成功"); + } + private void validateOrderPro(ProcessOrderProBo orderPro) { if (orderPro == null || StringUtils.isBlank(orderPro.getProductionOrderNo())) { throw new IllegalArgumentException("生产令号不能为空"); @@ -909,10 +960,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } private String extractYearPart(String code) { - if (code.length() < 5 || !code.contains("-")) { - throw new IllegalArgumentException("生产令号格式不正确"); - } - return "-".equals(code.substring(2, 3)) ? code.substring(3, 5) : code.substring(2, 4); + return ProductionOrderNoUtils.extractYearPart(code); } private void createDirectoryIfNotExists(String dirPath) { @@ -929,7 +977,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } private PwProductionBill buildProductionBill(ProcessOrderProBo orderPro, String dirPath) { - String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); PwProductionBill pwProductionBill = new PwProductionBill(); pwProductionBill.setDestpath(dirPath); pwProductionBill.setProducitonorder(orderPro.getProductionOrderNo()); @@ -939,13 +987,118 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } private List buildProductInfos(Long orderId) { + ProcessOrderPro processOrderPro = baseMapper.selectById(orderId); List figureSaveList = figureSaveMapper.selectList(new LambdaQueryWrapper().eq(FigureSave::getPid, orderId)); + if (figureSaveList == null) { + figureSaveList = Collections.emptyList(); + } + boolean preservePartDrawing = processOrderPro != null && Long.valueOf(1).equals(processOrderPro.getQuantity()); + String productionCode = processOrderPro != null ? processOrderPro.getProductionOrderNo() : null; + if (StringUtils.isBlank(productionCode) && !figureSaveList.isEmpty()) { + productionCode = figureSaveList.get(0).getProductionCode(); + } + String normalizedProductionCode = productionCode == null ? "" : productionCode.trim().toUpperCase(Locale.ROOT); + //判断是企标 + boolean isQbProject = normalizedProductionCode.startsWith("QB"); + if (isQbProject) { + if (figureSaveList.isEmpty()) { + return Collections.emptyList(); + } + // 同一图纸路径下可有多个子件;不同路径各自一个 ProductInfo,sourcefile 与子件与路径一致 + Map> byDrawPath = new LinkedHashMap<>(); + for (FigureSave fs : figureSaveList) { + byDrawPath.computeIfAbsent(qbDrawPathGroupKey(fs), k -> new ArrayList<>()).add(fs); + } + List qbProducts = new ArrayList<>(byDrawPath.size()); + int qbSeq = 1; + for (List pathGroup : byDrawPath.values()) { + qbProducts.add(buildQbProductInfoForPathGroup(pathGroup, preservePartDrawing, qbSeq, productionCode)); + qbSeq++; + } + return qbProducts; + } return figureSaveList.stream().map(fs -> { SpecStrategy st = getStrategyForType(fs.getProductType()); ProductInfo pi = st != null ? st.build(fs) : null; - return pi != null ? pi : buildProductInfoFallback(fs); + ProductInfo resolved = pi != null ? pi : buildProductInfoFallback(fs); + resolved.setPreservepartdrawing(preservePartDrawing); + return resolved; }).collect(Collectors.toList()); } + + /** 企标按图纸路径分组:空路径按行拆开,避免误合并。 */ + private String qbDrawPathGroupKey(FigureSave fs) { + String raw = fs.getDrawPath(); + if (StringUtils.isBlank(raw)) { + return "__row__" + (fs.getId() != null ? fs.getId() : System.identityHashCode(fs)); + } + return raw.trim().replace('\\', '/'); + } + + private boolean isQbProductionOrderNo(String orderProductionNo) { + if (StringUtils.isBlank(orderProductionNo)) { + return false; + } + return orderProductionNo.trim().toUpperCase(Locale.ROOT).startsWith("QB"); + } + + /** + * 与 {@link #buildProductInfos} 企标分组、assembledrawing(生产令号-序号)规则一致,得到 FTP/本地 PDF 目录名(如 QBxx-1-PDF)。 + */ + private List qbPdfDirectoryNamesOrdered(String orderProductionNo, List figureSaves) { + if (figureSaves == null || figureSaves.isEmpty()) { + return Collections.emptyList(); + } + Map> byDrawPath = new LinkedHashMap<>(); + for (FigureSave fs : figureSaves) { + byDrawPath.computeIfAbsent(qbDrawPathGroupKey(fs), k -> new ArrayList<>()).add(fs); + } + String base = orderProductionNo == null ? "" : orderProductionNo.trim(); + if (StringUtils.isBlank(base)) { + String fromRow = figureSaves.get(0).getProductionCode(); + base = fromRow == null ? "" : fromRow.trim(); + } + List names = new ArrayList<>(byDrawPath.size()); + int seq = 1; + for (List ignored : byDrawPath.values()) { + String pdfFolderName = base + "-" + seq; + pdfFolderName = pdfFolderName.replace("/", "-").replace("\\", "-"); + names.add(pdfFolderName + "-PDF"); + seq++; + } + return names; + } + + private ProductInfo buildQbProductInfoForPathGroup(List pathGroup, boolean preservePartDrawing, int qbAssemblySeq, String productionCodeBase) { + FigureSave first = pathGroup.get(0); + List subparts = pathGroup.stream().map(fs -> { + Subparts sp = new Subparts(); + sp.setPartname(fs.getFigureNumber()); + sp.setNumber(fs.getFigureNum() == null ? null : String.valueOf(fs.getFigureNum())); + return sp; + }).collect(Collectors.toList()); + SpecStrategy st = getStrategyForType(first.getProductType()); + ProductInfo pi = st != null ? st.build(first) : null; + ProductInfo resolved = pi != null ? pi : buildProductInfoFallback(first); + resolved.setPreservepartdrawing(preservePartDrawing); + resolved.setSubparts(subparts); + normalizeProductInfoSourcefile(resolved, first.getDrawPath()); + String base = productionCodeBase == null ? "" : productionCodeBase.trim(); + if (StringUtils.isBlank(base)) { + String fromRow = first.getProductionCode(); + base = fromRow == null ? "" : fromRow.trim(); + } + resolved.setAssembledrawing(base + "-" + qbAssemblySeq); + return resolved; + } + + private void normalizeProductInfoSourcefile(ProductInfo info, String drawPath) { + if (info == null) { + return; + } + String dp = drawPath; + info.setSourcefile(dp == null ? "" : dp.replace("/", "\\")); + } /* private ProductInfo buildProductInfo(FigureSave figureSave, PcRigidChain rigidChain) { ProductInfo productInfo = new ProductInfo(); @@ -961,10 +1114,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { //出图时如果不是标准的产品,而是零件图的话,变量会为空 private ProductInfo buildProductInfoFallback(FigureSave figureSave) { ProductInfo productInfo = new ProductInfo(); - productInfo.setAssembledrawing(figureSave.getFigureNumber()); productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); - productInfo.setSourcefile(figureSave.getDrawPath().replace("/", "\\")); + String dp = figureSave.getDrawPath(); + productInfo.setSourcefile(dp == null ? "" : dp.replace("/", "\\")); productInfo.setVars(new DataInfo()); + productInfo.setAssembledrawing(StringUtils.defaultString(figureSave.getFigureNumber())); + return productInfo; } @@ -972,7 +1127,8 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { ProductInfo productInfo = new ProductInfo(); productInfo.setAssembledrawing(figureSave.getFigureNumber()); productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); - productInfo.setSourcefile(figureSave.getDrawPath().replace("/", "\\")); + String dp = figureSave.getDrawPath(); + productInfo.setSourcefile(dp == null ? "" : dp.replace("/", "\\")); log.info("项目生产令号为:{}查询设备参数: {}+ 产品名称: {}", figureSave.getProductionCode(), figureSave.getFigureNumber(), figureSave.getFigureName()); DataInfo datainfo = buildDataInfo(device); productInfo.setVars(datainfo); @@ -983,7 +1139,8 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { ProductInfo productInfo = new ProductInfo(); productInfo.setAssembledrawing(figureSave.getFigureNumber()); productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); - productInfo.setSourcefile(figureSave.getDrawPath().replace("/", "\\")); + String dp = figureSave.getDrawPath(); + productInfo.setSourcefile(dp == null ? "" : dp.replace("/", "\\")); log.info("项目生产令号为:{}查询设备参数: {}+ 产品名称: {}", figureSave.getProductionCode(), figureSave.getFigureNumber(), figureSave.getFigureName()); DataInfo datainfo = buildDataInfo30S(device); productInfo.setVars(datainfo); @@ -994,7 +1151,8 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { ProductInfo productInfo = new ProductInfo(); productInfo.setAssembledrawing(figureSave.getFigureNumber()); productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); - productInfo.setSourcefile(figureSave.getDrawPath().replace("/", "\\")); + String dp = figureSave.getDrawPath(); + productInfo.setSourcefile(dp == null ? "" : dp.replace("/", "\\")); DataInfo datainfo = buildDataInfoGeneric(device); productInfo.setVars(datainfo); return productInfo; @@ -1368,47 +1526,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { return R.fail("产品信息中生产令号为空"); } - // 解析年份 - String year = "2025"; // 默认 - if (productionCode.startsWith("EY")) { - if (productionCode.length() >= 4) { - String yearShort = productionCode.substring(2, 4); - try { - int y = Integer.parseInt(yearShort); - if (y > 10 && y < 50) { - year = "20" + yearShort; - } - } catch (NumberFormatException e) { - // 解析失败,保持默认 - } - } - } else { - int firstDash = productionCode.indexOf("-"); - if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) { - String yearShort = productionCode.substring(firstDash + 1, firstDash + 3); - try { - int yearInt = Integer.parseInt(yearShort); - if (yearInt > 10 && yearInt < 50) { - year = "20" + yearShort; - } else { - if (firstDash >= 4 && productionCode.startsWith("EY")) { - String altYearShort = productionCode.substring(2, 4); - try { - Integer.parseInt(altYearShort); - year = "20" + altYearShort; - } catch (Exception ex) {} - } else if (firstDash >= 2) { - String altYearShort = productionCode.substring(firstDash - 2, firstDash); - try { - Integer.parseInt(altYearShort); - year = "20" + altYearShort; - } catch (Exception ex) {} - } - } - } catch (Exception ignore) { - } - } - } + String year = ProductionOrderNoUtils.extractYear(productionCode); // 生成到本地的路径: 根据项目号生成文件夹 String localBaseDir = buildWorkDirectory(productionCode); @@ -1435,8 +1553,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { String safeFigureNumber = figureNumber.replace("/", "-"); String dwgFileName = safeFigureNumber + ".dwg"; - // 修正:有些 DWG 文件就放在根目录下,所以直接用生产令号作为远程目录 - // 或者有可能 DWG 放在 ` /{year}/{productionCode}/dwg ` 下 + String rootRemoteDir = "/" + year + "/" + productionCode; log.info("尝试下载DWG文件, 远程目录: {}, 本地目录: {}, 文件名: {}", rootRemoteDir, targetDir, dwgFileName); @@ -1477,6 +1594,183 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } } + /** + * DWG 文件名主体 → 库表图号:去掉开头「数字序号-」;{@code --} 表示 {@code /};其余单个 {@code -} 转为 {@code /}。 + * 例:{@code 5-80R.13.0-1440--D} → {@code 80R.13.0/1440/D} + */ + static String figureFilenameBaseToDrawingNo(String fileBaseName) { + if (fileBaseName == null) { + return ""; + } + String s = fileBaseName.trim(); + if (s.isEmpty()) { + return ""; + } + s = s.replaceFirst("^\\d+-", ""); + final String ph = "@@DOUBLE_HYPHEN@@"; + s = s.replace("--", ph); + s = s.replace("-", "/"); + s = s.replace(ph, "/"); + return s.trim(); + } + + private static String sanitizeForFileNamePart(String name) { + if (name == null) { + return "unnamed"; + } + String s = name.trim(); + s = s.replaceAll("[\\\\/:*?\"<>|@]", "_"); + s = s.replaceAll("\\s+", "_"); + // 避免前缀里的 '-' 早于原图号文件名里的序号横线,干扰物料码截取 + s = s.replace('-', '_'); + if (s.length() > 120) { + s = s.substring(0, 120); + } + return s.isEmpty() ? "unnamed" : s; + } + + /** 从「原文件名@@…」整名中取 FTP 原文件名主体(不含 .pdf) */ + private static String extractPdfOriginalBaseName(String fileName) { + if (fileName == null) { + return fileName; + } + int sep = fileName.indexOf(PDF_ORIGINAL_TAIL_SEP); + if (sep < 0) { + int dot = fileName.toLowerCase().lastIndexOf(".pdf"); + return dot > 0 ? fileName.substring(0, dot) : fileName; + } + return fileName.substring(0, sep); + } + + /** FTP 下载到本地后的 PDF 目录:按图号查 production_order,重命名为「原文件名@@图号@@名称.pdf」(未查到则不重命名) */ + private void renamePdfFilesInDirectoryRecursive(File root, String productionOrderNo) { + if (root == null || !root.exists()) { + return; + } + List pdfs = new ArrayList<>(); + collectPdfFilesRecursively(root, pdfs); + if (pdfs.isEmpty()) { + return; + } + for (File f : pdfs) { + tryRenamePdfForProductionOrder(f, productionOrderNo); + } + } + + private void tryRenamePdfForProductionOrder(File pdfFile, String productionOrderNo) { + if (pdfFile == null || !pdfFile.isFile()) { + return; + } + String fullName = pdfFile.getName(); + if (!fullName.toLowerCase().endsWith(".pdf")) { + return; + } + if (fullName.contains(PDF_ORIGINAL_TAIL_SEP)) { + return; + } + String baseName = fullName.substring(0, fullName.length() - 4); + String drawingKey = figureFilenameBaseToDrawingNo(baseName); + if (StringUtils.isBlank(drawingKey)) { + log.warn("PDF 文件名无法转换为查询用图号,跳过重命名: {}", fullName); + return; + } + DrawingNameLookupResult hit = lookupDrawingDisplayByOrderAndFigureNo(productionOrderNo, drawingKey); + String display = hit != null ? hit.displayName : null; + if (StringUtils.isBlank(display)) { + log.info("未在生产订单中匹配到图号名称,保留 PDF 文件名: {} (drawingKey={})", fullName, drawingKey); + return; + } + String matchedNo = hit != null ? hit.matchedDrawingNo : null; + String partName = sanitizeForFileNamePart(display); + String partNo = StringUtils.isNotBlank(matchedNo) ? sanitizeForFileNamePart(matchedNo) : ""; + // 原文件名主体 + -- + 名称 + String newBase = baseName + PDF_ORIGINAL_TAIL_SEP + partName; + String newName = newBase + ".pdf"; + if (newName.equals(fullName)) { + return; + } + File dest = new File(pdfFile.getParentFile(), newName); + if (dest.exists() && !dest.getAbsolutePath().equals(pdfFile.getAbsolutePath())) { + if (!dest.delete()) { + log.warn("无法删除已存在的目标文件,放弃 PDF 重命名: {}", dest.getAbsolutePath()); + return; + } + } + if (!pdfFile.renameTo(dest)) { + log.warn("PDF 重命名失败: {} -> {}", fullName, newName); + } else { + log.info("PDF 已重命名: {} -> {}", fullName, newName); + } + } + + /** + * 按生产令号 + 图号类 key 查名称:依次等价于 + * drawing_no → drawing_name;parent_drawing_no → parent_part;main_part → main_name。 + */ + private DrawingNameLookupResult lookupDrawingDisplayByOrderAndFigureNo(String productionOrderNo, String drawingNoKey) { + if (StringUtils.isAnyBlank(productionOrderNo, drawingNoKey)) { + return null; + } + String orderNo = productionOrderNo.trim(); + String key = drawingNoKey.trim(); + + LambdaQueryWrapper q1 = Wrappers.lambdaQuery(); + q1.eq(ProductionOrder::getProductionOrderNo, orderNo); + q1.eq(ProductionOrder::getDrawingNo, key); + q1.last("LIMIT 1"); + ProductionOrder r1 = productionOrderMapper.selectOne(q1); + if (r1 != null) { + String name = r1.getDrawingName() == null ? null : r1.getDrawingName().trim(); + String no = r1.getDrawingNo() == null ? null : r1.getDrawingNo().trim(); + return new DrawingNameLookupResult(name, StringUtils.isNotBlank(no) ? no : key); + } + + LambdaQueryWrapper q2 = Wrappers.lambdaQuery(); + q2.eq(ProductionOrder::getProductionOrderNo, orderNo); + q2.eq(ProductionOrder::getParentDrawingNo, key); + q2.last("LIMIT 1"); + ProductionOrder r2 = productionOrderMapper.selectOne(q2); + if (r2 != null) { + String name = r2.getParentPart() == null ? null : r2.getParentPart().trim(); + String no = r2.getParentDrawingNo() == null ? null : r2.getParentDrawingNo().trim(); + return new DrawingNameLookupResult(name, StringUtils.isNotBlank(no) ? no : key); + } + + LambdaQueryWrapper q3 = Wrappers.lambdaQuery(); + q3.eq(ProductionOrder::getProductionOrderNo, orderNo); + q3.eq(ProductionOrder::getMainPart, key); + q3.last("LIMIT 1"); + ProductionOrder r3 = productionOrderMapper.selectOne(q3); + if (r3 != null) { + String name = r3.getMainName() == null ? null : r3.getMainName().trim(); + String no = r3.getMainPart() == null ? null : r3.getMainPart().trim(); + return new DrawingNameLookupResult(name, StringUtils.isNotBlank(no) ? no : key); + } + + LambdaQueryWrapper q4 = Wrappers.lambdaQuery(); + q4.eq(ImMaterial::getMaterialCode, key); + q4.last("LIMIT 1"); + ImMaterial r4 = mapper.selectOne(q4); + if (r4 != null) { + String name = r4.getMaterialName() == null ? null : r4.getMaterialName().trim(); + String no = r4.getMaterialCode() == null ? null : r4.getMaterialCode().trim(); + return new DrawingNameLookupResult(name, StringUtils.isNotBlank(no) ? no : key); + } + + + return null; + } + + private static final class DrawingNameLookupResult { + final String displayName; + final String matchedDrawingNo; + + DrawingNameLookupResult(String displayName, String matchedDrawingNo) { + this.displayName = displayName; + this.matchedDrawingNo = matchedDrawingNo; + } + } + /** * @param id */ @@ -1523,99 +1817,103 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { // 6. 下载FTP文件到本地 log.info("开始从FTP下载文件..."); - for (FigureSave figureSave : fiseList) { - String productionCode = figureSave.getProductionCode(); - //解析出年份 从左往右第一个- 后面就是 年份25 =2025 26 就是2026 - //如果这个是EY开头的那就取EY后面的数字 25 26 27 作为年份 2025 2026 2027 - try { - String year = "2025"; // 默认2025 - - if (productionCode != null) { - if (productionCode.startsWith("EY")) { - // EY开头,取第2位开始的两位 (索引2和3) - if (productionCode.length() >= 4) { - String yearShort = productionCode.substring(2, 4); - try { - int y = Integer.parseInt(yearShort); - if (y > 10 && y < 50) { - year = "20" + yearShort; - } - } catch (NumberFormatException e) { - // 解析失败,保持默认 - } + Set downloadedDirectories = new HashSet<>(); + if (isQbProductionOrderNo(code)) { + String yearForFtp = ProductionOrderNoUtils.extractYear(code); + for (String pdfDirectoryName : qbPdfDirectoryNamesOrdered(code, fiseList)) { + if (!downloadedDirectories.add(pdfDirectoryName)) { + log.info("目录已下载,跳过重复下载: {}", pdfDirectoryName); + continue; + } + try { + String ftpPath = yearForFtp + "/" + code + "/" + pdfDirectoryName; + ftpPath = ftpPath.replace("\\", "/"); + if (!ftpPath.startsWith("/")) { + ftpPath = "/" + ftpPath; + } + String localPath = localBaseDir + code + "/" + pdfDirectoryName; + log.info("下载路径(企标按路径组与出图 assembledrawing 一致): {} -> {}", ftpPath, localPath); + File localDir = new File(localPath); + if (!localDir.exists()) { + if (!localDir.mkdirs()) { + log.error("无法创建本地目录: {}", localPath); + return R.fail("无法创建本地目录: " + localPath); } + } + R downloadResult = FtpUtil.downloadFtpDirectoryFiles("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localPath); + if (downloadResult.getCode() != 200) { + log.error("FTP下载失败: {}", downloadResult.getMsg()); + return R.fail("FTP下载失败: " + downloadResult.getMsg()); + } + File localDownloadedDir = new File(localPath); + int pdfCount = countPdfFilesRecursively(localDownloadedDir); + if (pdfCount == 0) { + log.warn("FTP下载完成但目录中未发现PDF文件: {}, msg: {}", localPath, downloadResult.getMsg()); } else { - // 原有逻辑:取第一个横杠后的两位 - int firstDash = productionCode.indexOf("-"); - if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) { - String yearShort = productionCode.substring(firstDash + 1, firstDash + 3); - try { - // 简单的年份合理性校验,比如 > 10 且 < 50 - int yearInt = Integer.parseInt(yearShort); - if (yearInt > 10 && yearInt < 50) { - year = "20" + yearShort; - } else { - // 如果提取出来的像 00,可能格式是 EY2609-002,横杠前其实是年份 26 - if (firstDash >= 4 && productionCode.startsWith("EY")) { - String altYearShort = productionCode.substring(2, 4); - try { - Integer.parseInt(altYearShort); - year = "20" + altYearShort; - } catch (Exception ex) { - // ignore - } - } else if (firstDash >= 2) { - // 比如 XX26-001,可能是字母加数字 - String altYearShort = productionCode.substring(firstDash - 2, firstDash); - try { - Integer.parseInt(altYearShort); - year = "20" + altYearShort; - } catch (Exception ex) { - // ignore - } - } - } - } catch (NumberFormatException e) { - // 解析失败,保持默认 - } + log.info("FTP下载成功,PDF数量: {}, msg: {}", pdfCount, downloadResult.getMsg()); + renamePdfFilesInDirectoryRecursive(localDownloadedDir, code); + } + } catch (Exception e) { + log.error("下载企标目录失败: {}", pdfDirectoryName, e); + return R.fail("下载产品文件失败: " + pdfDirectoryName + " - " + e.getMessage()); + } + } + } else { + for (FigureSave figureSave : fiseList) { + String productionCode = figureSave.getProductionCode(); + //解析出年份 从左往右第一个- 后面就是 年份25 =2025 26 就是2026 + //如果这个是EY开头的那就取EY后面的数字 25 26 27 作为年份 2025 2026 2027 + try { + String year = ProductionOrderNoUtils.extractYear(productionCode); + + String pdfFolderName = StringUtils.defaultIfBlank(figureSave.getFigureNumber(), productionCode); + pdfFolderName = StringUtils.defaultIfBlank(pdfFolderName, "UNKNOWN"); + pdfFolderName = pdfFolderName.replace("/", "-").replace("\\", "-"); + + String pdfDirectoryName = pdfFolderName + "-PDF"; + + String ftpPath = year + "/" + code + "/" + pdfDirectoryName; + ftpPath = ftpPath.replace("\\", "/"); + if (!ftpPath.startsWith("/")) { + ftpPath = "/" + ftpPath; + } + String localPath = localBaseDir + code + "/" + pdfDirectoryName; + + if (!downloadedDirectories.add(pdfDirectoryName)) { + log.info("目录已下载,跳过重复下载: {}", pdfDirectoryName); + continue; + } + + log.info("下载路径: {} -> {}", ftpPath, localPath); + + File localDir = new File(localPath); + if (!localDir.exists()) { + if (!localDir.mkdirs()) { + log.error("无法创建本地目录: {}", localPath); + return R.fail("无法创建本地目录: " + localPath); } } - } - // 拼接远程文件夹路径 - // 确保路径使用正斜杠,且不包含本地盘符 - String ftpPath = year + "/" + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF"; - ftpPath = ftpPath.replace("\\", "/"); // 强制替换反斜杠 - if (!ftpPath.startsWith("/")) { - ftpPath = "/" + ftpPath; // 确保以/开头,表示从FTP根目录开始 - } - // 拼接本地存储路径 - String localPath = localBaseDir + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF"; + R downloadResult = FtpUtil.downloadFtpDirectoryFiles("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localPath); - log.info("下载路径: {} -> {}", ftpPath, localPath); - - // 创建本地文件夹 - File localDir = new File(localPath); - if (!localDir.exists()) { - if (!localDir.mkdirs()) { - log.error("无法创建本地目录: {}", localPath); - return R.fail("无法创建本地目录: " + localPath); + if (downloadResult.getCode() != 200) { + log.error("FTP下载失败: {}", downloadResult.getMsg()); + return R.fail("FTP下载失败: " + downloadResult.getMsg()); } + + File localDownloadedDir = new File(localPath); + int pdfCount = countPdfFilesRecursively(localDownloadedDir); + if (pdfCount == 0) { + log.warn("FTP下载完成但目录中未发现PDF文件: {}, msg: {}", localPath, downloadResult.getMsg()); + } else { + log.info("FTP下载成功,PDF数量: {}, msg: {}", pdfCount, downloadResult.getMsg()); + renamePdfFilesInDirectoryRecursive(localDownloadedDir, code); + } + + } catch (Exception e) { + log.error("下载产品文件失败: {}", figureSave.getFigureNumber(), e); + return R.fail("下载产品文件失败: " + figureSave.getFigureNumber() + " - " + e.getMessage()); } - - // 下载对应文件夹 - R downloadResult = FtpUtil.downloadFtpDirectoryFiles("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localPath); - - if (downloadResult.getCode() != 200) { - log.error("FTP下载失败: {}", downloadResult.getMsg()); - return R.fail("FTP下载失败: " + downloadResult.getMsg()); - } - - log.info("FTP下载成功: {}", downloadResult.getData()); - - } catch (Exception e) { - log.error("下载产品文件失败: {}", figureSave.getFigureNumber(), e); - return R.fail("下载产品文件失败: " + figureSave.getFigureNumber() + " - " + e.getMessage()); } } @@ -1755,14 +2053,10 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { * 构建工作目录路径 */ private String buildWorkDirectory(String code) { - String arr = ""; - if ("-".equals(code.substring(2, 3))) { - arr += "D:/20" + code.substring(3, 5); - } else { - arr += "D:/20" + code.substring(2, 4); - } - arr += "/"; - return arr; + String year = ProductionOrderNoUtils.extractYear(code); + String projectName = ProductionOrderNoUtils.resolveProjectName(code); + log.info("生产令号:{} 识别项目:{} 年份:{}", code, projectName, year); + return "D:/" + year + "/"; } /** @@ -1825,43 +2119,87 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } } - // 遍历所有产品,将PDF文件复制到allpdf文件夹 - for (FigureSave figureSave : fiseList) { - //获取文件路径 - String filePDF = figureSave.getFigureNumber().replace("/", "-") + "-PDF"; - String sourceDir = arr + code + "/" + filePDF; - File sourceFolder = new File(sourceDir); - - if (!sourceFolder.exists()) { - log.warn("源PDF目录不存在: {}", sourceDir); - continue; - } - - File[] pdfFiles = sourceFolder.listFiles((dir, name) -> name.toLowerCase().endsWith(".pdf")); - if (pdfFiles == null || pdfFiles.length == 0) { - log.warn("源PDF目录为空: {}", sourceDir); - continue; - } - - // 复制所有PDF文件到allpdf文件夹 - for (File pdfFile : pdfFiles) { - File targetFile = new File(allPdfDir, pdfFile.getName()); - - // 如果目标文件已存在,先删除 - if (targetFile.exists()) { - if (!targetFile.delete()) { - log.warn("无法删除已存在的目标文件: {}", targetFile.getAbsolutePath()); - continue; + // 遍历所有产品,将PDF文件复制到allpdf文件夹(企标目录名与 FTP 下载、出图 assembledrawing 一致) + Set processedSourceDirs = new HashSet<>(); + if (isQbProductionOrderNo(code)) { + for (String filePDF : qbPdfDirectoryNamesOrdered(code, fiseList)) { + String sourceDir = arr + code + "/" + filePDF; + if (!processedSourceDirs.add(sourceDir)) { + log.info("源PDF目录已处理,跳过重复处理: {}", sourceDir); + continue; + } + File sourceFolder = new File(sourceDir); + if (!sourceFolder.exists()) { + log.warn("源PDF目录不存在: {}", sourceDir); + continue; + } + List pdfFiles = new ArrayList<>(); + collectPdfFilesRecursively(sourceFolder, pdfFiles); + if (pdfFiles.isEmpty()) { + log.warn("源PDF目录为空: {}", sourceDir); + continue; + } + for (File pdfFile : pdfFiles) { + File targetFile = new File(allPdfDir, pdfFile.getName()); + if (targetFile.exists()) { + if (!targetFile.delete()) { + log.warn("无法删除已存在的目标文件: {}", targetFile.getAbsolutePath()); + continue; + } + } + try { + copyFile(pdfFile, targetFile); + log.info("成功复制PDF文件: {} -> {}", pdfFile.getName(), targetFile.getAbsolutePath()); + } catch (IOException e) { + log.error("复制PDF文件失败: {}", pdfFile.getName(), e); + return false; } } + } + } else { + for (FigureSave figureSave : fiseList) { + String productionCode = figureSave.getProductionCode(); + String pdfFolderName = StringUtils.defaultIfBlank(figureSave.getFigureNumber(), productionCode); + pdfFolderName = StringUtils.defaultIfBlank(pdfFolderName, "UNKNOWN"); + pdfFolderName = pdfFolderName.replace("/", "-").replace("\\", "-"); + String filePDF = pdfFolderName + "-PDF"; + String sourceDir = arr + code + "/" + filePDF; - // 复制文件而不是移动 - try { - copyFile(pdfFile, targetFile); - log.info("成功复制PDF文件: {} -> {}", pdfFile.getName(), targetFile.getAbsolutePath()); - } catch (IOException e) { - log.error("复制PDF文件失败: {}", pdfFile.getName(), e); - return false; + if (!processedSourceDirs.add(sourceDir)) { + log.info("源PDF目录已处理,跳过重复处理: {}", sourceDir); + continue; + } + File sourceFolder = new File(sourceDir); + + if (!sourceFolder.exists()) { + log.warn("源PDF目录不存在: {}", sourceDir); + continue; + } + + List pdfFiles = new ArrayList<>(); + collectPdfFilesRecursively(sourceFolder, pdfFiles); + if (pdfFiles.isEmpty()) { + log.warn("源PDF目录为空: {}", sourceDir); + continue; + } + + for (File pdfFile : pdfFiles) { + File targetFile = new File(allPdfDir, pdfFile.getName()); + + if (targetFile.exists()) { + if (!targetFile.delete()) { + log.warn("无法删除已存在的目标文件: {}", targetFile.getAbsolutePath()); + continue; + } + } + + try { + copyFile(pdfFile, targetFile); + log.info("成功复制PDF文件: {} -> {}", pdfFile.getName(), targetFile.getAbsolutePath()); + } catch (IOException e) { + log.error("复制PDF文件失败: {}", pdfFile.getName(), e); + return false; + } } } } @@ -1899,8 +2237,16 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { log.info("处理PDF文件=====>输出到zip目录: {}", name); PDFDocHelper.imageWatermark(fs.getAbsolutePath(), zipDir, name, "D:/java/tuzhizhang.png", reviews, pawX, pawY); - // 生成三维BOM版本 - String threeDName = name.substring(name.indexOf("-") + 1); + // 生成三维BOM版本(若已按「原名@@图号@@名称」重命名,则仍按原文件名主体解析) + String threeDName; + if (name.contains(PDF_ORIGINAL_TAIL_SEP)) { + String orig = extractPdfOriginalBaseName(name); + threeDName = orig + ".pdf"; + } else if (name.indexOf("-") >= 0) { + threeDName = name.substring(name.indexOf("-") + 1); + } else { + threeDName = name; + } PDFDocHelper.imageWatermark(fs.getAbsolutePath(), arr + code + "/bomThree/" + threeDName, "D:/java/tuzhizhang.png"); log.info("PDF处理成功: {}", name); @@ -1941,11 +2287,39 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } } + private int countPdfFilesRecursively(File dir) { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return 0; + } + List pdfFiles = new ArrayList<>(); + collectPdfFilesRecursively(dir, pdfFiles); + return pdfFiles.size(); + } + + private void collectPdfFilesRecursively(File dir, List pdfFiles) { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return; + } + File[] files = dir.listFiles(); + if (files == null || files.length == 0) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + collectPdfFilesRecursively(file, pdfFiles); + } else if (file.getName().toLowerCase().endsWith(".pdf")) { + pdfFiles.add(file); + } + } + } + private String extractMaterialCode(String fileName) { - int firstDash = fileName.indexOf("-"); - int dotPdf = fileName.lastIndexOf(".pdf"); + String origBase = extractPdfOriginalBaseName(fileName); + String work = origBase + ".pdf"; + int firstDash = work.indexOf("-"); + int dotPdf = work.lastIndexOf(".pdf"); if (firstDash >= 0 && dotPdf > firstDash) { - String code = fileName.substring(firstDash + 1, dotPdf); + String code = work.substring(firstDash + 1, dotPdf); return code.replace("--", "/"); } return null; diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessRouteServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessRouteServiceImpl.java index ae83355..cb6a2f9 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessRouteServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProcessRouteServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.commons.collections4.ListUtils; import org.aspectj.bridge.MessageUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -86,6 +87,8 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { private final ProcessInfoMapper processInfoMapper; private final ProductionRouteTwoMapper productionRouteTwoMapper; private final ProcessOrderProMapper processOrderProMapper; + @Value("${pdf.font-path:classpath:font/arial unicode ms.ttf}") + private String pdfFontPath; public static JdHuoZhu getFIDToProductionOrder(PlanOrderVo planOrder) { K3CloudApi client = new K3CloudApi(); @@ -816,8 +819,8 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { for (ProductionOrderVo productionOrderVo : productionOrderVos) { BomDetailsVo bomDetails = new BomDetailsVo(); bomDetails.setTotalWeight(productionOrderVo.getProductionOrderNo()); - bomDetails.setFNumber(productionOrderVo.getMainProducts()); - bomDetails.setFName(productionOrderVo.getMainProductsName()); + bomDetails.setFNumber(productionOrderVo.getMainPart()); + bomDetails.setFName(productionOrderVo.getMainName()); bomDetails.setPartdiagramCode(productionOrderVo.getParentDrawingNo()); bomDetails.setPartdiagramName(productionOrderVo.getParentPart()); bomDetails.setPartNumber(productionOrderVo.getDrawingNo()); @@ -1444,7 +1447,13 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { @Override public String generatePDFs(String rooteProdet) throws IOException { List selecPlanRouteList = getSelecPlanRouteList(rooteProdet); - String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet); + if (selecPlanRouteList == null || selecPlanRouteList.isEmpty()) { + throw new ServiceException("未查询到可生成PDF的工艺数据"); + } + String pathZIP = PDFGenerator.writeToPdf(selecPlanRouteList, rooteProdet, Collections.singletonList(pdfFontPath)); + if (StringUtils.isBlank(pathZIP)) { + throw new ServiceException("PDF压缩包生成失败"); + } return pathZIP; } @@ -1494,7 +1503,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { public List getSelectProceOrder1(String rooteProdet) { K3CloudApi client = new K3CloudApi(); // 第一次查询:使用 F_HBYT_SCLH - List result = queryWithFieldName(client, "F_HBYT_SCLH", rooteProdet); + List result = queryWithFieldName(client, "F_HBYT_XMH", rooteProdet); return result; } @@ -1504,7 +1513,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { private List queryWithFieldName(K3CloudApi client, String fieldName, String rooteProdet) { JsonObject json = new JsonObject(); json.addProperty("FormId", "PRD_MO"); - json.addProperty("FieldKeys", "F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr"); + json.addProperty("FieldKeys", "F_HBYT_XMH,F_HBYT_SCLH,FBillNo ,FMaterialId.FNumber,FMaterialName, F_UCHN_BaseProperty_qtr"); JsonArray filterString = new JsonArray(); JsonObject filterObject = new JsonObject(); @@ -1513,9 +1522,18 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { filterObject.addProperty("Value", rooteProdet); filterObject.addProperty("Left", ""); filterObject.addProperty("Right", ""); - filterObject.addProperty("Logic", 0); + filterObject.addProperty("Logic", 1); filterString.add(filterObject); + JsonObject filterObject1 = new JsonObject(); + filterObject1.addProperty("FieldName", "F_HBYT_SCLH"); // 使用传入的 fieldName + filterObject1.addProperty("Compare", "67"); + filterObject1.addProperty("Value", rooteProdet); + filterObject1.addProperty("Left", ""); + filterObject1.addProperty("Right", ""); + filterObject1.addProperty("Logic", 1); + filterString.add(filterObject1); + json.add("FilterString", filterString); json.addProperty("OrderString", ""); json.addProperty("TopRowCount", 0); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProductionOrderServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProductionOrderServiceImpl.java index a2c1ad3..92bec9d 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProductionOrderServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ProductionOrderServiceImpl.java @@ -1,346 +1,449 @@ -package com.ruoyi.system.service.impl; - -import cn.hutool.core.bean.BeanUtil; -import com.alibaba.fastjson.JSONObject; -import com.ruoyi.common.core.page.TableDataInfo; -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.common.utils.HttpUtils; -import com.ruoyi.system.domain.FigureSave; -import com.ruoyi.system.domain.PcRigidChain; -import com.ruoyi.system.domain.ProcessRoute; -import com.ruoyi.system.domain.bo.FigureSaveBo; -import com.ruoyi.system.domain.bo.ProcessOrderProBo; -import com.ruoyi.system.domain.bo.ProcessRouteBo; -import com.ruoyi.system.domain.dto.excuteDrawing.DataInfo; -import com.ruoyi.system.domain.dto.excuteDrawing.ProductInfo; -import com.ruoyi.system.domain.dto.excuteDrawing.PwProductionBill; -import com.ruoyi.system.domain.vo.ProcessOrderProVo; -import com.ruoyi.system.mapper.FigureSaveMapper; -import com.ruoyi.system.service.IPcRigidChainService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import com.ruoyi.system.domain.bo.ProductionOrderBo; -import com.ruoyi.system.domain.vo.ProductionOrderVo; -import com.ruoyi.system.domain.ProductionOrder; -import com.ruoyi.system.mapper.ProductionOrderMapper; -import com.ruoyi.system.service.IProductionOrderService; -import com.ruoyi.common.utils.StringUtils; - -import java.io.File; -import java.nio.file.Paths; -import java.sql.Array; -import java.util.*; - -/** - * 生产订单Service业务层处理 - * - * @author 田志阳 - * @date 2024-10-20 - */ -@RequiredArgsConstructor -@Service -@Slf4j -public class ProductionOrderServiceImpl implements IProductionOrderService { - - private final ProductionOrderMapper baseMapper; - private final FigureSaveMapper figureSaveMapper; - @Autowired - private IPcRigidChainService pcRigidChainService; - /** - * 查询生产订单 - */ - @Override - public ProductionOrderVo queryById(Long id){ - return baseMapper.selectVoById(id); - } - - /** - * 查询生产订单列表 - */ - @Override - public TableDataInfo queryPageList(ProductionOrderBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - return TableDataInfo.build(result); - } - - /** - * 查询生产订单列表 - */ - @Override - public List queryList(ProductionOrderBo bo) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); - } - - private LambdaQueryWrapper buildQueryWrapper(ProductionOrderBo bo) { - Map params = bo.getParams(); - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProductionOrder::getProductionOrderNo, bo.getProductionOrderNo()); - lqw.like(StringUtils.isNotBlank(bo.getDrawingNo()), ProductionOrder::getDrawingNo, bo.getDrawingNo()); - lqw.like(StringUtils.isNotBlank(bo.getDrawingName()), ProductionOrder::getDrawingName, bo.getDrawingName()); - lqw.eq(bo.getQuantity() != null, ProductionOrder::getQuantity, bo.getQuantity()); - lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), ProductionOrder::getMaterial, bo.getMaterial()); - lqw.eq(bo.getSingleWeight() != null, ProductionOrder::getSingleWeight, bo.getSingleWeight()); - lqw.eq(bo.getTotalWeight() != null, ProductionOrder::getTotalWeight, bo.getTotalWeight()); - lqw.eq(StringUtils.isNotBlank(bo.getParentPart()), ProductionOrder::getParentPart, bo.getParentPart()); - lqw.eq(StringUtils.isNotBlank(bo.getParentDrawingNo()), ProductionOrder::getParentDrawingNo, bo.getParentDrawingNo()); - lqw.orderByDesc(ProductionOrder::getParentPart); - return lqw; - } - - /** - * 新增生产订单 - */ - @Override - public Boolean insertByBo(ProductionOrderBo bo) { - ProductionOrder add = BeanUtil.toBean(bo, ProductionOrder.class); - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; - if (flag) { - bo.setId(add.getId()); - } - return flag; - } - - /** - * 修改生产订单 - */ - @Override - public Boolean updateByBo(ProductionOrderBo bo) { - ProductionOrder update = BeanUtil.toBean(bo, ProductionOrder.class); - validEntityBeforeSave(update); - return baseMapper.updateById(update) > 0; - } - - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(ProductionOrder entity){ - //TODO 做一些数据校验,如唯一约束 - } - - /** - * 批量删除生产订单 - */ - @Override - public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if(isValid){ - //TODO 做一些业务上的校验,判断是否需要校验 - } - return baseMapper.deleteBatchIds(ids) > 0; - } - - @Override - public void loadProductionOrder(List productionOrderVoList) { - - List beanList = BeanUtil.copyToList(productionOrderVoList, ProductionOrder.class); - for (ProductionOrder productionOrder : beanList) { - productionOrder.setProductionOrderNo("Tset-001-24"); - baseMapper.insert(productionOrder); - } - - } - - @Override - public List getGroupWeld(ProductionOrderVo productionOrderVo) { - BeanUtil.toBean(productionOrderVo, ProductionOrder.class); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ProductionOrder::getMaterial, "组焊件"); - return baseMapper.selectList(wrapper); - } - - /** - * 拿到材质为组焊件的集合,再筛选出parentDrawingNo和DrawingNo相等的集合 - * @param productionOrderVo - * @return - */ - @Override - public List getGroupWeldSubset(ProductionOrderVo productionOrderVo) { - List groupWeld = getGroupWeld(productionOrderVo); - // TODO: 拿到材质为组焊件的集合,再筛选出parentDrawingNo和DrawingNo相等的集合 - List subsetList = new ArrayList<>(); - for (ProductionOrder productionOrder : groupWeld) { - if (Objects.equals(productionOrder.getParentDrawingNo(), productionOrder.getDrawingNo())){ - subsetList.add(productionOrder); - } - } - return subsetList; - } - - - @Override - public Boolean executDrawing(ProcessOrderProBo orderPro) { - //查询是否有此令号 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ProductionOrder::getProductionOrderNo, orderPro.getProductionOrderNo()); - ProductionOrder productionOrder = baseMapper.selectOne(wrapper); - - log.info("查询生产令号:{} 是否重复...", orderPro.getProductionOrderNo()); - if (productionOrder != null) { - new Throwable("令号重复"); - return false; - } - PwProductionBill pwProductionBill = new PwProductionBill(); - String code = orderPro.getProductionOrderNo(); - String yearPart; - if ("-".equals(code.substring(2, 3))) { - yearPart = code.substring(3, 5); - } else { - yearPart = code.substring(2, 4); - } - String dirPath = Paths.get("F:", "20" + yearPart).toString(); - log.info("生产令号:{} 生成路径:{}", orderPro.getProductionOrderNo(), dirPath); - // pwProductionBill.setDestpath(dirPath.replace(File.separator, "\\")); - pwProductionBill.setDestpath("D:\\EVO_TECH\\EVO程序输出路径"); - // 创建目录(多级) - File file = new File(dirPath); - if (!file.exists()) { - file.mkdirs(); - } - //生产令号 - pwProductionBill.setProducitonorder(orderPro.getProductionOrderNo()); - //出图时间 - pwProductionBill.setReleasedate(String.valueOf(orderPro.getProjectEndTime())); - //产品集合 - List productInfos = new ArrayList<>(); - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(FigureSave::getPid, orderPro.getId()); - List productionOrders = figureSaveMapper.selectList(queryWrapper); - ProductInfo productInfo =null; - for (FigureSave figureSave : productionOrders) { - productInfo.setAssembledrawing(figureSave.getFigureNumber()); - // productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); - productInfo.setNumber(String.valueOf(2)); - productInfo.setSourcefile("D:\\EVO_TECH\\EVO程序图\\40S程序图.dwg"); - //设置变量信息 - log.info("项目生产令号为:{}查询产品型号信息: {}+ 产品名称: {}",figureSave.getProductionCode(), figureSave.getFigureNumber(), figureSave.getFigureName()); - PcRigidChain rigidChain = pcRigidChainService.selectPcRigidChainByTypeName(figureSave.getFigureNumber()); - DataInfo datainfo = new DataInfo(); - if(null!=rigidChain){ - datainfo = new DataInfo(); - datainfo.setV1(rigidChain.getVOne().toString()); - datainfo.setV2(rigidChain.getVTwo().toString()); - datainfo.setV3(rigidChain.getVThree().toString()); - datainfo.setV4(rigidChain.getVFour().toString()); - datainfo.setV5(rigidChain.getVFive().toString()); - datainfo.setV6(rigidChain.getVSix().toString()); - datainfo.setV7(rigidChain.getVSeven().toString()); - datainfo.setV8(rigidChain.getVEight().toString()); - datainfo.setV9(rigidChain.getVNine().toString()); - datainfo.setV10(rigidChain.getVTen().toString()); - datainfo.setV11(rigidChain.getVEleven().toString()); - datainfo.setV12(rigidChain.getVTwelve().toString()); - datainfo.setV13(rigidChain.getVThirteen().toString()); - datainfo.setV14(rigidChain.getVFourteen().toString()); - datainfo.setV15(rigidChain.getVFifteen().toString()); - datainfo.setV16(rigidChain.getVSixteen().toString()); - datainfo.setV17(rigidChain.getVSeveteen().toString()); - datainfo.setV18(rigidChain.getVEighteen().toString()); - datainfo.setV19(rigidChain.getVNineteen().toString()); - datainfo.setV20(rigidChain.getVTwenty().toString()); - datainfo.setV21(rigidChain.getVTwentyOne().toString()); - datainfo.setV22(rigidChain.getVTwentyTwo().toString()); - datainfo.setV23(rigidChain.getVTwentyThree().toString()); - datainfo.setV24(rigidChain.getVTwentyFour().toString()); - datainfo.setV25(rigidChain.getVTwentyFive().toString()); - datainfo.setV26(rigidChain.getVTwentySix().toString()); - datainfo.setV27(rigidChain.getVTwentySeven().toString()); - datainfo.setV28(rigidChain.getVTwentyEight().toString()); - datainfo.setV29(rigidChain.getVTwentyNine().toString()); - datainfo.setV30(rigidChain.getVThirty().toString()); - datainfo.setV31(rigidChain.getVThirtyOne().toString()); - datainfo.setV32(rigidChain.getVThirtyTwo().toString()); - datainfo.setV33(rigidChain.getVThirtyThree().toString()); - datainfo.setV34(rigidChain.getVThirtyFour().toString()); - datainfo.setV35(rigidChain.getVThirtyFive().toString()); - datainfo.setV36(rigidChain.getVThirtySix().toString()); - datainfo.setV37(rigidChain.getVThirtySeven().toString()); - datainfo.setV38(rigidChain.getVThirtyEight().toString()); - datainfo.setV39(rigidChain.getVThirtyNine().toString()); - datainfo.setV40(rigidChain.getVForty().toString()); - datainfo.setV41(rigidChain.getVFortyOne().toString()); - datainfo.setV42(rigidChain.getVFortyTwo().toString()); - datainfo.setV43(rigidChain.getVFortyThree().toString()); - datainfo.setV44(rigidChain.getVFortyFour().toString()); - datainfo.setV45(rigidChain.getVFortyFive().toString()); - datainfo.setV46(rigidChain.getVFortySix().toString()); - datainfo.setV47(rigidChain.getVFortySeven().toString()); - datainfo.setV48(rigidChain.getVFortyEight().toString()); - datainfo.setV49(rigidChain.getVFortyNine().toString()); - datainfo.setV50(rigidChain.getVFifty().toString()); - datainfo.setV51(rigidChain.getVFiftyOne().toString()); - datainfo.setV52(rigidChain.getVFiftyTwo().toString()); - datainfo.setV53(rigidChain.getVFiftyThree().toString()); - - datainfo.setG1(rigidChain.getGOne().toString()); - datainfo.setG2(rigidChain.getGTwo().toString()); - datainfo.setG3(rigidChain.getGThree().toString()); - datainfo.setG4(rigidChain.getGFour().toString()); - datainfo.setG5(rigidChain.getGFive().toString()); - datainfo.setG6(rigidChain.getGSix().toString()); - datainfo.setG7(rigidChain.getGSeven().toString()); - datainfo.setG8(rigidChain.getGEight().toString()); - datainfo.setG9(rigidChain.getGNine().toString()); - datainfo.setG10(rigidChain.getGTen().toString()); - datainfo.setG11(rigidChain.getGEleven().toString()); - datainfo.setG12(rigidChain.getGTwelve().toString()); - datainfo.setG13(rigidChain.getGThirteen().toString()); - datainfo.setG14(rigidChain.getGFifteen().toString()); - datainfo.setG15(rigidChain.getGFifteen().toString()); - datainfo.setG16(rigidChain.getGSixteen().toString()); - datainfo.setG17(rigidChain.getGSeveteen().toString()); - datainfo.setG18(rigidChain.getGEighteen().toString()); - datainfo.setG19(rigidChain.getGNineteen().toString()); - datainfo.setG20(rigidChain.getGTwenty().toString()); - } - productInfo.setVars(datainfo); - productInfos.add(productInfo); - } - pwProductionBill.setProduct(productInfos); - String dawingDate = JSONObject.toJSONString(pwProductionBill); - String dataResult = HttpUtils.sendPost("http://192.168.5.23:9000/generatedrawingscompatible", dawingDate); - - return null; - } - - /** - * @param productionOrderNo - * @return - */ - @Override - public List selectByProCode(String productionOrderNo) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ProductionOrder::getProductionOrderNo,productionOrderNo); - return baseMapper.selectList(wrapper); - } - - /** - * 判断如果是外购产品,返回true - * @param proCode - * @param materialCode - * @return - */ - @Override - public Boolean isPurchas(String proCode, String materialCode) { - if (proCode == null || materialCode == null) { - return false; - } - String p = proCode.trim(); - String m = materialCode.trim(); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ProductionOrder::getProductionOrderNo, p) - .eq(ProductionOrder::getDrawingNo, m) - .isNotNull(ProductionOrder::getRemark) - .like(ProductionOrder::getRemark, "外购"); - Long count = baseMapper.selectCount(wrapper); - return count != null && count > 0; - } - - -} +package com.ruoyi.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.alibaba.fastjson.JSONObject; +import com.ruoyi.common.core.page.TableDataInfo; +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.common.utils.HttpUtils; +import com.ruoyi.system.domain.FigureSave; +import com.ruoyi.system.domain.PcRigidChain; +import com.ruoyi.system.domain.ProcessRoute; +import com.ruoyi.system.domain.bo.FigureSaveBo; +import com.ruoyi.system.domain.bo.ProcessOrderProBo; +import com.ruoyi.system.domain.bo.ProcessRouteBo; +import com.ruoyi.system.domain.dto.excuteDrawing.DataInfo; +import com.ruoyi.system.domain.dto.excuteDrawing.ProductInfo; +import com.ruoyi.system.domain.dto.excuteDrawing.PwProductionBill; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightExportDTO; +import com.ruoyi.system.domain.dto.productionorder.ProductionOrderWeightImportDTO; +import com.ruoyi.system.domain.vo.ProcessOrderProVo; +import com.ruoyi.system.mapper.FigureSaveMapper; +import com.ruoyi.system.service.IPcRigidChainService; +import com.ruoyi.system.utils.ProductionOrderNoUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.bo.ProductionOrderBo; +import com.ruoyi.system.domain.vo.ProductionOrderVo; +import com.ruoyi.system.domain.ProductionOrder; +import com.ruoyi.system.mapper.ProductionOrderMapper; +import com.ruoyi.system.service.IProductionOrderService; +import com.ruoyi.common.utils.StringUtils; + +import java.io.File; +import java.nio.file.Paths; +import java.sql.Array; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 生产订单Service业务层处理 + * + * @author 田志阳 + * @date 2024-10-20 + */ +@RequiredArgsConstructor +@Service +@Slf4j +public class ProductionOrderServiceImpl implements IProductionOrderService { + + private final ProductionOrderMapper baseMapper; + private final FigureSaveMapper figureSaveMapper; + @Autowired + private IPcRigidChainService pcRigidChainService; + /** + * 查询生产订单 + */ + @Override + public ProductionOrderVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 查询生产订单列表 + */ + @Override + public TableDataInfo queryPageList(ProductionOrderBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询生产订单列表 + */ + @Override + public List queryList(ProductionOrderBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(ProductionOrderBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProductionOrder::getProductionOrderNo, bo.getProductionOrderNo()); + lqw.like(StringUtils.isNotBlank(bo.getDrawingNo()), ProductionOrder::getDrawingNo, bo.getDrawingNo()); + lqw.like(StringUtils.isNotBlank(bo.getDrawingName()), ProductionOrder::getDrawingName, bo.getDrawingName()); + lqw.eq(bo.getQuantity() != null, ProductionOrder::getQuantity, bo.getQuantity()); + lqw.eq(StringUtils.isNotBlank(bo.getMaterial()), ProductionOrder::getMaterial, bo.getMaterial()); + lqw.eq(bo.getSingleWeight() != null, ProductionOrder::getSingleWeight, bo.getSingleWeight()); + lqw.like(StringUtils.isNotBlank(bo.getMainName()), ProductionOrder::getMainName, bo.getMainName()); + lqw.eq(StringUtils.isNotBlank(bo.getMainPart()), ProductionOrder::getMainPart, bo.getMainPart()); + lqw.eq(bo.getTotalWeight() != null, ProductionOrder::getTotalWeight, bo.getTotalWeight()); + lqw.eq(StringUtils.isNotBlank(bo.getParentPart()), ProductionOrder::getParentPart, bo.getParentPart()); + lqw.eq(StringUtils.isNotBlank(bo.getParentDrawingNo()), ProductionOrder::getParentDrawingNo, bo.getParentDrawingNo()); + + lqw.orderByDesc(ProductionOrder::getParentPart); + return lqw; + } + + /** + * 新增生产订单 + */ + @Override + public Boolean insertByBo(ProductionOrderBo bo) { + ProductionOrder add = BeanUtil.toBean(bo, ProductionOrder.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改生产订单 + */ + @Override + public Boolean updateByBo(ProductionOrderBo bo) { + ProductionOrder update = BeanUtil.toBean(bo, ProductionOrder.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(ProductionOrder entity){ + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除生产订单 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + @Override + public void loadProductionOrder(List productionOrderVoList) { + + List beanList = BeanUtil.copyToList(productionOrderVoList, ProductionOrder.class); + for (ProductionOrder productionOrder : beanList) { + productionOrder.setProductionOrderNo("Tset-001-24"); + baseMapper.insert(productionOrder); + } + + } + + @Override + public List getGroupWeld(ProductionOrderVo productionOrderVo) { + BeanUtil.toBean(productionOrderVo, ProductionOrder.class); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ProductionOrder::getMaterial, "组焊件"); + return baseMapper.selectList(wrapper); + } + + /** + * 拿到材质为组焊件的集合,再筛选出parentDrawingNo和DrawingNo相等的集合 + * @param productionOrderVo + * @return + */ + @Override + public List getGroupWeldSubset(ProductionOrderVo productionOrderVo) { + List groupWeld = getGroupWeld(productionOrderVo); + // TODO: 拿到材质为组焊件的集合,再筛选出parentDrawingNo和DrawingNo相等的集合 + List subsetList = new ArrayList<>(); + for (ProductionOrder productionOrder : groupWeld) { + if (Objects.equals(productionOrder.getParentDrawingNo(), productionOrder.getDrawingNo())){ + subsetList.add(productionOrder); + } + } + return subsetList; + } + + + @Override + public Boolean executDrawing(ProcessOrderProBo orderPro) { + //查询是否有此令号 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ProductionOrder::getProductionOrderNo, orderPro.getProductionOrderNo()); + ProductionOrder productionOrder = baseMapper.selectOne(wrapper); + + log.info("查询生产令号:{} 是否重复...", orderPro.getProductionOrderNo()); + if (productionOrder != null) { + new Throwable("令号重复"); + return false; + } + PwProductionBill pwProductionBill = new PwProductionBill(); + String code = orderPro.getProductionOrderNo(); + String yearPart = ProductionOrderNoUtils.extractYearPart(code); + String dirPath = Paths.get("F:", "20" + yearPart).toString(); + log.info("生产令号:{} 生成路径:{}", orderPro.getProductionOrderNo(), dirPath); + // pwProductionBill.setDestpath(dirPath.replace(File.separator, "\\")); + pwProductionBill.setDestpath("D:\\EVO_TECH\\EVO程序输出路径"); + // 创建目录(多级) + File file = new File(dirPath); + if (!file.exists()) { + file.mkdirs(); + } + //生产令号 + pwProductionBill.setProducitonorder(orderPro.getProductionOrderNo()); + //出图时间 + pwProductionBill.setReleasedate(String.valueOf(orderPro.getProjectEndTime())); + //产品集合 + List productInfos = new ArrayList<>(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FigureSave::getPid, orderPro.getId()); + List productionOrders = figureSaveMapper.selectList(queryWrapper); + ProductInfo productInfo =null; + for (FigureSave figureSave : productionOrders) { + productInfo.setAssembledrawing(figureSave.getFigureNumber()); + // productInfo.setNumber(String.valueOf(figureSave.getFigureNum())); + productInfo.setNumber(String.valueOf(2)); + productInfo.setSourcefile("D:\\EVO_TECH\\EVO程序图\\40S程序图.dwg"); + //设置变量信息 + log.info("项目生产令号为:{}查询产品型号信息: {}+ 产品名称: {}",figureSave.getProductionCode(), figureSave.getFigureNumber(), figureSave.getFigureName()); + PcRigidChain rigidChain = pcRigidChainService.selectPcRigidChainByTypeName(figureSave.getFigureNumber()); + DataInfo datainfo = new DataInfo(); + if(null!=rigidChain){ + datainfo = new DataInfo(); + datainfo.setV1(rigidChain.getVOne().toString()); + datainfo.setV2(rigidChain.getVTwo().toString()); + datainfo.setV3(rigidChain.getVThree().toString()); + datainfo.setV4(rigidChain.getVFour().toString()); + datainfo.setV5(rigidChain.getVFive().toString()); + datainfo.setV6(rigidChain.getVSix().toString()); + datainfo.setV7(rigidChain.getVSeven().toString()); + datainfo.setV8(rigidChain.getVEight().toString()); + datainfo.setV9(rigidChain.getVNine().toString()); + datainfo.setV10(rigidChain.getVTen().toString()); + datainfo.setV11(rigidChain.getVEleven().toString()); + datainfo.setV12(rigidChain.getVTwelve().toString()); + datainfo.setV13(rigidChain.getVThirteen().toString()); + datainfo.setV14(rigidChain.getVFourteen().toString()); + datainfo.setV15(rigidChain.getVFifteen().toString()); + datainfo.setV16(rigidChain.getVSixteen().toString()); + datainfo.setV17(rigidChain.getVSeveteen().toString()); + datainfo.setV18(rigidChain.getVEighteen().toString()); + datainfo.setV19(rigidChain.getVNineteen().toString()); + datainfo.setV20(rigidChain.getVTwenty().toString()); + datainfo.setV21(rigidChain.getVTwentyOne().toString()); + datainfo.setV22(rigidChain.getVTwentyTwo().toString()); + datainfo.setV23(rigidChain.getVTwentyThree().toString()); + datainfo.setV24(rigidChain.getVTwentyFour().toString()); + datainfo.setV25(rigidChain.getVTwentyFive().toString()); + datainfo.setV26(rigidChain.getVTwentySix().toString()); + datainfo.setV27(rigidChain.getVTwentySeven().toString()); + datainfo.setV28(rigidChain.getVTwentyEight().toString()); + datainfo.setV29(rigidChain.getVTwentyNine().toString()); + datainfo.setV30(rigidChain.getVThirty().toString()); + datainfo.setV31(rigidChain.getVThirtyOne().toString()); + datainfo.setV32(rigidChain.getVThirtyTwo().toString()); + datainfo.setV33(rigidChain.getVThirtyThree().toString()); + datainfo.setV34(rigidChain.getVThirtyFour().toString()); + datainfo.setV35(rigidChain.getVThirtyFive().toString()); + datainfo.setV36(rigidChain.getVThirtySix().toString()); + datainfo.setV37(rigidChain.getVThirtySeven().toString()); + datainfo.setV38(rigidChain.getVThirtyEight().toString()); + datainfo.setV39(rigidChain.getVThirtyNine().toString()); + datainfo.setV40(rigidChain.getVForty().toString()); + datainfo.setV41(rigidChain.getVFortyOne().toString()); + datainfo.setV42(rigidChain.getVFortyTwo().toString()); + datainfo.setV43(rigidChain.getVFortyThree().toString()); + datainfo.setV44(rigidChain.getVFortyFour().toString()); + datainfo.setV45(rigidChain.getVFortyFive().toString()); + datainfo.setV46(rigidChain.getVFortySix().toString()); + datainfo.setV47(rigidChain.getVFortySeven().toString()); + datainfo.setV48(rigidChain.getVFortyEight().toString()); + datainfo.setV49(rigidChain.getVFortyNine().toString()); + datainfo.setV50(rigidChain.getVFifty().toString()); + datainfo.setV51(rigidChain.getVFiftyOne().toString()); + datainfo.setV52(rigidChain.getVFiftyTwo().toString()); + datainfo.setV53(rigidChain.getVFiftyThree().toString()); + + datainfo.setG1(rigidChain.getGOne().toString()); + datainfo.setG2(rigidChain.getGTwo().toString()); + datainfo.setG3(rigidChain.getGThree().toString()); + datainfo.setG4(rigidChain.getGFour().toString()); + datainfo.setG5(rigidChain.getGFive().toString()); + datainfo.setG6(rigidChain.getGSix().toString()); + datainfo.setG7(rigidChain.getGSeven().toString()); + datainfo.setG8(rigidChain.getGEight().toString()); + datainfo.setG9(rigidChain.getGNine().toString()); + datainfo.setG10(rigidChain.getGTen().toString()); + datainfo.setG11(rigidChain.getGEleven().toString()); + datainfo.setG12(rigidChain.getGTwelve().toString()); + datainfo.setG13(rigidChain.getGThirteen().toString()); + datainfo.setG14(rigidChain.getGFifteen().toString()); + datainfo.setG15(rigidChain.getGFifteen().toString()); + datainfo.setG16(rigidChain.getGSixteen().toString()); + datainfo.setG17(rigidChain.getGSeveteen().toString()); + datainfo.setG18(rigidChain.getGEighteen().toString()); + datainfo.setG19(rigidChain.getGNineteen().toString()); + datainfo.setG20(rigidChain.getGTwenty().toString()); + } + productInfo.setVars(datainfo); + productInfos.add(productInfo); + } + pwProductionBill.setProduct(productInfos); + String dawingDate = JSONObject.toJSONString(pwProductionBill); + String dataResult = HttpUtils.sendPost("http://192.168.5.23:9000/generatedrawingscompatible", dawingDate); + + return null; + } + + /** + * @param productionOrderNo + * @return + */ + @Override + public List selectByProCode(String productionOrderNo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ProductionOrder::getProductionOrderNo,productionOrderNo); + return baseMapper.selectList(wrapper); + } + + /** + * 判断如果是外购产品,返回true + * @param proCode + * @param materialCode + * @return + */ + @Override + public Boolean isPurchas(String proCode, String materialCode) { + if (proCode == null || materialCode == null) { + return false; + } + String p = proCode.trim(); + String m = materialCode.trim(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ProductionOrder::getProductionOrderNo, p) + .eq(ProductionOrder::getDrawingNo, m) + .isNotNull(ProductionOrder::getRemark) + .like(ProductionOrder::getRemark, "外购"); + Long count = baseMapper.selectCount(wrapper); + return count != null && count > 0; + } + + @Override + public List selectProductionOrderNoOptions(String keyword, Integer limit) { + int safeLimit = limit == null ? 200 : limit; + if (safeLimit <= 0) { + safeLimit = 200; + } + if (safeLimit > 1000) { + safeLimit = 1000; + } + String kw = StringUtils.isBlank(keyword) ? null : keyword.trim(); + return baseMapper.selectDistinctProductionOrderNos(kw, safeLimit); + } + + private Map buildSingleWeightLookupMap(List drawingNos) { + if (drawingNos == null || drawingNos.isEmpty()) { + return Collections.emptyMap(); + } + final int batchSize = 500; + Map weightMap = new HashMap<>(); + for (int i = 0; i < drawingNos.size(); i += batchSize) { + int end = Math.min(i + batchSize, drawingNos.size()); + List batch = drawingNos.subList(i, end); + List dbRows = baseMapper.selectWeightByDrawingNos(batch); + for (ProductionOrder dbRow : dbRows) { + if (StringUtils.isNotBlank(dbRow.getDrawingNo()) && dbRow.getSingleWeight() != null) { + weightMap.put(dbRow.getDrawingNo().trim(), dbRow.getSingleWeight().doubleValue()); + } + } + } + List missingCodes = drawingNos.stream() + .filter(code -> !weightMap.containsKey(code)) + .collect(Collectors.toList()); + for (int i = 0; i < missingCodes.size(); i += batchSize) { + int end = Math.min(i + batchSize, missingCodes.size()); + List batch = missingCodes.subList(i, end); + List routeRows = baseMapper.selectDiscWeightByMaterialCodes(batch); + for (ProductionOrder routeRow : routeRows) { + if (StringUtils.isNotBlank(routeRow.getDrawingNo()) && routeRow.getSingleWeight() != null) { + weightMap.put(routeRow.getDrawingNo().trim(), routeRow.getSingleWeight().doubleValue()); + } + } + } + return weightMap; + } + + @Override + public Map resolveSingleWeightByMaterialCodes(Collection materialCodes) { + if (materialCodes == null || materialCodes.isEmpty()) { + return Collections.emptyMap(); + } + List drawingNos = materialCodes.stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + if (drawingNos.isEmpty()) { + return Collections.emptyMap(); + } + return buildSingleWeightLookupMap(drawingNos); + } + + @Override + public List matchSingleWeightByMaterialCode(List rows) { + if (rows == null || rows.isEmpty()) { + return Collections.emptyList(); + } + List drawingNos = rows.stream() + .map(ProductionOrderWeightImportDTO::getMaterialCode) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + if (drawingNos.isEmpty()) { + return Collections.emptyList(); + } + Map weightMap = buildSingleWeightLookupMap(drawingNos); + List result = new ArrayList<>(); + for (ProductionOrderWeightImportDTO row : rows) { + if (row == null || StringUtils.isBlank(row.getMaterialCode())) { + continue; + } + String code = row.getMaterialCode().trim(); + Double dbWeight = weightMap.get(code); + ProductionOrderWeightExportDTO dto = new ProductionOrderWeightExportDTO(); + dto.setMaterialCode(code); + dto.setMaterialName(row.getMaterialName()); + dto.setSingleWeight(dbWeight); + result.add(dto); + } + return result; + } + + @Override + public Double resolveSingleWeightByMaterialCode(String materialCode) { + if (StringUtils.isBlank(materialCode)) { + return null; + } + String code = materialCode.trim(); + return buildSingleWeightLookupMap(Collections.singletonList(code)).get(code); + } + + +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SfcOperationPlanningMainServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SfcOperationPlanningMainServiceImpl.java index 95c68f9..e685477 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SfcOperationPlanningMainServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SfcOperationPlanningMainServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.dingding.relay.DingMessageRelayService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -30,18 +31,19 @@ import com.ruoyi.system.runner.updatePcessPlanConver; import com.kingdee.bos.webapi.entity.RepoRet; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.math.RoundingMode; import cn.hutool.core.date.DateUtil; import java.util.Date; import com.ruoyi.system.service.ISfcOperationPlanningMainService; +import com.ruoyi.system.service.support.OperationPlanningResultAssembler; import com.ruoyi.common.exception.ServiceException; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Collection; +import java.util.Set; +import java.util.LinkedHashSet; import java.util.stream.Collectors; -import java.util.Comparator; import java.util.LinkedHashMap; /** @@ -57,9 +59,10 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin private final SfcOperationPlanningMainMapper baseMapper; private final SfcOperationPlanningDetailMapper detailMapper; + private final DingMessageRelayService dingMessageRelayService; /** - * 同步K3工序计划数据 + * 同步K3工序计划数据(全量新增与更新) */ @Override @Transactional(rollbackFor = Exception.class) @@ -81,7 +84,12 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin mainEntity.setFid(resultDTO.getFid()); mainEntity.setBillNo(resultDTO.getBillNo()); mainEntity.setMoNumber(resultDTO.getMoNumber()); - mainEntity.setHbytSclh(resultDTO.getBatchNo()); + if (StringUtils.isNotBlank(resultDTO.getBatchNo())) { + mainEntity.setHbytSclh(resultDTO.getBatchNo()); + } else if (mainEntity.getId() == null && StringUtils.isNotBlank(resultDTO.getProjectNo())) { + mainEntity.setHbytSclh(resultDTO.getProjectNo()); + } + mainEntity.setFHbytXmh(resultDTO.getProjectNo()); mainEntity.setProductCode(resultDTO.getProductCode()); mainEntity.setProductName(resultDTO.getProductName()); mainEntity.setProSpecification(resultDTO.getProSpecification()); @@ -104,19 +112,8 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin List processList = resultDTO.getProcessList(); if (processList != null && !processList.isEmpty()) { for (ProcessDetailDTO detailDTO : processList) { - // 查询是否已经存在该明细记录 (根据 fid 和 entity_entry_id 和 oper_number) - SfcOperationPlanningDetail detailEntity; - if (detailDTO.getK3DetailId() != null) { - detailEntity = detailMapper.selectOne(Wrappers.lambdaQuery() - .eq(SfcOperationPlanningDetail::getFid, resultDTO.getFid()) - .eq(SfcOperationPlanningDetail::getK3DetailId, detailDTO.getK3DetailId())); - } else { - detailEntity = detailMapper.selectOne(Wrappers.lambdaQuery() - .eq(SfcOperationPlanningDetail::getFid, resultDTO.getFid()) - .eq(SfcOperationPlanningDetail::getEntityEntryId, detailDTO.getEntityEntryId()) - .eq(detailDTO.getOperNo() != null, SfcOperationPlanningDetail::getOperNumber, detailDTO.getOperNo())); - } - + // 先按 k3_detail_id 再按 entity_entry_id(+工序号)兜底,避免金蝶字段空/有切换时漏查导致重复 insert 触发唯一约束 + SfcOperationPlanningDetail detailEntity = findDetailForSync(resultDTO.getFid(), detailDTO); if (detailEntity == null) { detailEntity = new SfcOperationPlanningDetail(); } @@ -159,7 +156,6 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin detailEntity.setActivityName(detailDTO.getActivity1Name()); detailEntity.setOperDescription(detailDTO.getOperDescription()); detailEntity.setEquipment(detailDTO.getEquipment()); - detailEntity.setK3DetailId(detailDTO.getK3DetailId()); if (detailEntity.getId() == null) { detailMapper.insert(detailEntity); @@ -172,6 +168,51 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin return true; } + /** + * 同步时定位已有明细行:优先 k3_detail_id,未命中再用 entity_entry_id(及工序号)匹配,避免互斥分支漏查。 + */ + private SfcOperationPlanningDetail findDetailForSync(Long fid, ProcessDetailDTO detailDTO) { + if (fid == null) { + return null; + } + if (detailDTO.getK3DetailId() != null) { + List byK3 = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(SfcOperationPlanningDetail::getFid, fid) + .eq(SfcOperationPlanningDetail::getK3DetailId, detailDTO.getK3DetailId()) + .last("LIMIT 2")); + if (!byK3.isEmpty()) { + if (byK3.size() > 1) { + log.warn("工序计划明细 k3_detail_id 重复 fid={} k3DetailId={} 使用首条", fid, detailDTO.getK3DetailId()); + } + return byK3.get(0); + } + } + if (detailDTO.getEntityEntryId() != null) { + LambdaQueryWrapper w = Wrappers.lambdaQuery() + .eq(SfcOperationPlanningDetail::getFid, fid) + .eq(SfcOperationPlanningDetail::getEntityEntryId, detailDTO.getEntityEntryId()); + if (detailDTO.getOperNo() != null) { + w.eq(SfcOperationPlanningDetail::getOperNumber, detailDTO.getOperNo().longValue()); + } + List list = detailMapper.selectList(w); + if (list.isEmpty()) { + return null; + } + if (list.size() > 1) { + if (detailDTO.getK3DetailId() != null) { + for (SfcOperationPlanningDetail row : list) { + if (detailDTO.getK3DetailId().equals(row.getK3DetailId())) { + return row; + } + } + } + log.warn("工序计划明细命中多条 fid={} entityEntryId={} operNo={} 使用首条", fid, detailDTO.getEntityEntryId(), detailDTO.getOperNo()); + } + return list.get(0); + } + return null; + } + @Override @Transactional(rollbackFor = Exception.class) public Boolean updateOperators(OperationPlanningUpdateOperatorsReq req) { @@ -290,9 +331,95 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin detail.setEquipment(item.getEquipment()); // 同步更新设备字段 detailMapper.updateById(detail); } + sendDispatchNotice(main, details, updateMap); return true; } + private void sendDispatchNotice(SfcOperationPlanningMain main, + List details, + Map updateMap) { + List receivers = resolveDispatchReceivers(updateMap); + if (receivers.isEmpty()) { + log.warn("派工通知未发送:本次派工未提供有效人员名字"); + return; + } + + String title = "工序派工通知"; + String markdown = buildDispatchMarkdown(main, details, updateMap); + try { + dingMessageRelayService.sendMarkdownToUser(title, markdown, receivers); + } catch (Exception e) { + // 通知发送失败不影响派工更新主流程 + log.error("派工通知发送失败, fid={}", main == null ? null : main.getFid(), e); + } + } + + private List resolveDispatchReceivers(Map updateMap) { + if (updateMap == null || updateMap.isEmpty()) { + return new ArrayList<>(); + } + Set users = new LinkedHashSet<>(); + for (OperationPlanningUpdateOperatorItem item : updateMap.values()) { + if (item == null || StringUtils.isBlank(item.getOperator())) { + continue; + } + String normalized = item.getOperator().replace(",", ",").trim(); + if (StringUtils.isBlank(normalized)) { + continue; + } + String[] parts = normalized.split("、"); + for (String part : parts) { + String user = part == null ? null : part.trim(); + if (StringUtils.isNotBlank(user)) { + users.add(user); + } + } + } + return new ArrayList<>(users); + } + + private String buildDispatchMarkdown(SfcOperationPlanningMain main, + List details, + Map updateMap) { + StringBuilder sb = new StringBuilder(512); + sb.append("### 工序派工通知\n\n") + .append("- 派工时间: ").append(DateUtil.formatDateTime(new Date())).append("\n") + .append("- 生产执行单号: ").append(main.getHbytSclh() == null ? "-" : main.getHbytSclh()).append("\n") + .append("- 项目号: ").append(main.getFHbytXmh() == null ? "-" : main.getFHbytXmh()).append("\n") + .append("- 产品编码: ").append(main.getProductCode() == null ? "-" : main.getProductCode()).append("\n") + .append("- 产品名称: ").append(main.getProductName() == null ? "-" : main.getProductName()).append("\n") + .append("---\n"); + + for (SfcOperationPlanningDetail detail : details) { + OperationPlanningUpdateOperatorItem item = updateMap.get(detail.getK3DetailId()); + if (item == null) { + continue; + } + String operator = item.getOperator(); + if (operator != null) { + operator = operator.trim(); + } + sb.append("**工序: ").append(detail.getProcessName() == null ? "-" : detail.getProcessName()) + .append("**\n") + .append("- 工序号: ").append(detail.getOperNumber() == null ? "-" : detail.getOperNumber()).append("\n") + .append("- 工序数量: ").append(detail.getOperQty() == null ? "-" : detail.getOperQty()).append("\n") + .append("- 转入数量: ").append(detail.getTransInQty() == null ? "-" : detail.getTransInQty()).append("\n") + .append("- 车间/工作中心: ") + .append(detail.getWorkCenterName() == null ? "-" : detail.getWorkCenterName()).append("\n") + .append("- 作业: ").append(detail.getProcessName() == null ? "-" : detail.getProcessName()).append("\n") + .append("- 作业人员: ").append(StringUtils.isBlank(operator) ? "-" : operator).append("\n") + .append("- 工序控制码: ").append(StringUtils.isBlank(detail.getOptCtrlCode()) ? "-" : detail.getOptCtrlCode()).append("\n") + .append("- 工序说明: ").append(StringUtils.isBlank(detail.getOperDescription()) ? "-" : detail.getOperDescription()).append("\n") + .append("- 设备/厂家: ").append(StringUtils.isBlank(item.getEquipment()) ? "-" : item.getEquipment().trim()).append("\n") + .append("- 工序计划开始时间: ") + .append(detail.getSeqPlanStartTime() == null ? "-" : DateUtil.formatDateTime(detail.getSeqPlanStartTime())).append("\n") + .append("- 工序计划完成时间: ") + .append(detail.getSeqPlanFinishTime() == null ? "-" : DateUtil.formatDateTime(detail.getSeqPlanFinishTime())).append("\n") + .append("---\n"); + } + return sb.toString(); + } + /** * 查询工序计划主 */ @@ -412,137 +539,7 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin .collect(Collectors.groupingBy(SfcOperationPlanningDetail::getMainId)); // 6. 组装返回给前端的层级结构 - List resultList = new ArrayList<>(); - for (SfcOperationPlanningMain main : mainList) { - OperationPlanResultDTO resultDTO = new OperationPlanResultDTO(); - resultDTO.setMoNumber(main.getMoNumber()); - resultDTO.setBatchNo(main.getHbytSclh()); - resultDTO.setProductCode(main.getProductCode()); - resultDTO.setProductName(main.getProductName()); - resultDTO.setBillNo(main.getBillNo()); - resultDTO.setProSpecification(main.getProSpecification()); - resultDTO.setPlanStartTime(main.getPlanStartTime() != null ? DateUtil.formatDateTime(main.getPlanStartTime()) : null); - resultDTO.setPlanFinishTime(main.getPlanFinishTime() != null ? DateUtil.formatDateTime(main.getPlanFinishTime()) : null); - resultDTO.setFid(main.getFid()); - - // 获取原生Date以便于后续排序使用 - resultDTO.setRawPlanStartTime(main.getPlanStartTime()); - - // 组装关联的子表数据 - List processList = new ArrayList<>(); - List relatedDetails = detailMap.get(main.getId()); - - double totalPlanQty = 0.0; - double totalOkQty = 0.0; - int finishedCount = 0; - String mainWorkCenter = ""; - - if (relatedDetails != null) { - // 计算该单据所有工段的总进度数据 - mainWorkCenter = relatedDetails.get(0).getWorkCenterName(); - for (SfcOperationPlanningDetail detail : relatedDetails) { - if (detail.getOperQty() != null) totalPlanQty += detail.getOperQty().doubleValue(); - if (detail.getQualifiedQty() != null) totalOkQty += detail.getQualifiedQty().doubleValue(); - if ("4".equals(detail.getOperStatus())) { - finishedCount++; - } - } - - for (SfcOperationPlanningDetail detail : relatedDetails) { - ProcessDetailDTO detailDTO = new ProcessDetailDTO(); - detailDTO.setOperNo(detail.getOperNumber() != null ? detail.getOperNumber().intValue() : null); - detailDTO.setProcessName(detail.getProcessName()); - detailDTO.setWorkCenter(detail.getWorkCenterName()); - detailDTO.setPlanQty(detail.getOperQty() != null ? detail.getOperQty().doubleValue() : null); - detailDTO.setInQty(detail.getTransInQty() != null ? detail.getTransInQty().doubleValue() : null); - detailDTO.setOkQty(detail.getQualifiedQty() != null ? detail.getQualifiedQty().doubleValue() : null); - detailDTO.setNgQty(detail.getUnqualifiedQty() != null ? detail.getUnqualifiedQty().doubleValue() : null); - detailDTO.setTransOutQty(detail.getTransOutQty() != null ? detail.getTransOutQty().doubleValue() : null); - 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()) - .multiply(BigDecimal.valueOf(100)) - .divide(BigDecimal.valueOf(detailDTO.getPlanQty()), 1, RoundingMode.HALF_UP); - if (progress.compareTo(BigDecimal.ZERO) < 0) { - progress = BigDecimal.ZERO; - } else if (progress.compareTo(BigDecimal.valueOf(100)) > 0) { - progress = BigDecimal.valueOf(100); - } - 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("生产中"); - } - - detailDTO.setSeqPlanStartTime(detail.getSeqPlanStartTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanStartTime()) : null); - detailDTO.setSeqPlanFinishTime(detail.getSeqPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getSeqPlanFinishTime()) : null); - detailDTO.setOperPlanStartTime(detail.getOperPlanStartTime() != null ? DateUtil.formatDateTime(detail.getOperPlanStartTime()) : null); - detailDTO.setOperPlanFinishTime(detail.getOperPlanFinishTime() != null ? DateUtil.formatDateTime(detail.getOperPlanFinishTime()) : null); - detailDTO.setOptCtrlCode(detail.getOptCtrlCode()); - detailDTO.setKeyOper(detail.getKeyOper()); - detailDTO.setActivity1Name(detail.getActivityName()); - detailDTO.setOperDescription(detail.getOperDescription()); - detailDTO.setEquipment(detail.getEquipment()); - detailDTO.setK3DetailId(detail.getK3DetailId()); - detailDTO.setEntityEntryId(detail.getEntityEntryId()); - - processList.add(detailDTO); - } - - resultDTO.setWorkCenter(mainWorkCenter); - resultDTO.setOverallPlanQty(totalPlanQty); - resultDTO.setOverallOkQty(totalOkQty); - - double progress = 0.0; - if (totalPlanQty > 0) { - progress = (totalOkQty / totalPlanQty) * 100; - } - resultDTO.setOverallProgress(Math.min(100.0, Math.max(0.0, progress))); - - // 计算总体状态 - if (relatedDetails.isEmpty() || (finishedCount == 0 && totalOkQty == 0)) { - resultDTO.setOverallStatus("未开工"); - } else if (finishedCount == relatedDetails.size()) { - resultDTO.setOverallStatus("已完工"); - } else { - resultDTO.setOverallStatus("生产中"); - } - } - resultDTO.setProcessList(processList); - resultList.add(resultDTO); - } - - // 对当前页结果进行排序:先按生产令号升序,再按计划开始时间升序 - resultList.sort(Comparator - .comparing(OperationPlanResultDTO::getBatchNo, Comparator.nullsLast(String::compareTo)) - .thenComparing(OperationPlanResultDTO::getRawPlanStartTime, Comparator.nullsLast(Date::compareTo))); + List resultList = OperationPlanningResultAssembler.assemble(mainList, detailMap); // 构造返回带 total 的 TableDataInfo TableDataInfo rspData = new TableDataInfo<>(); @@ -559,7 +556,13 @@ public class SfcOperationPlanningMainServiceImpl implements ISfcOperationPlannin lqw.eq(bo.getFid() != null, SfcOperationPlanningMain::getFid, bo.getFid()); lqw.eq(StringUtils.isNotBlank(bo.getBillNo()), SfcOperationPlanningMain::getBillNo, bo.getBillNo()); lqw.eq(StringUtils.isNotBlank(bo.getMoNumber()), SfcOperationPlanningMain::getMoNumber, bo.getMoNumber()); - lqw.eq(StringUtils.isNotBlank(bo.getHbytSclh()), SfcOperationPlanningMain::getHbytSclh, bo.getHbytSclh()); + if (StringUtils.isNotBlank(bo.getHbytSclh())) { + String keyword = bo.getHbytSclh().trim(); + lqw.and(w -> w.eq(SfcOperationPlanningMain::getHbytSclh, keyword) + .or() + .like(SfcOperationPlanningMain::getFHbytXmh, keyword)); + } + lqw.like(StringUtils.isNotBlank(bo.getFHbytXmh()), SfcOperationPlanningMain::getFHbytXmh, bo.getFHbytXmh()); 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()); diff --git a/ruoyi-system/src/main/resources/font/arial unicode ms.ttf b/ruoyi-system/src/main/resources/font/arial unicode ms.ttf new file mode 100644 index 0000000..d97f076 Binary files /dev/null and b/ruoyi-system/src/main/resources/font/arial unicode ms.ttf differ diff --git a/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml index 9a19f7a..2c08ccc 100644 --- a/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml @@ -21,6 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -35,6 +36,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + select p.drawing_no as drawingNo, + p.single_weight as singleWeight + from production_order p + inner join ( + select drawing_no, max(id) as max_id + from production_order + where drawing_no in + + #{drawingNo} + + group by drawing_no + ) t on p.id = t.max_id + where p.single_weight is not null + + + diff --git a/ruoyi-system/src/main/resources/mapper/system/SfcOperationPlanningMainMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SfcOperationPlanningMainMapper.xml index 27a9fc1..643d005 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SfcOperationPlanningMainMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SfcOperationPlanningMainMapper.xml @@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + diff --git a/ruoyi-system/src/main/resources/mapper/system/WorkbenchMapper.xml b/ruoyi-system/src/main/resources/mapper/system/WorkbenchMapper.xml index 190deca..37b4ed8 100644 --- a/ruoyi-system/src/main/resources/mapper/system/WorkbenchMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/WorkbenchMapper.xml @@ -94,7 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND d.work_center_name = #{workCenterName} GROUP BY m.mo_number - ORDER BY m.plan_start_time DESC, progress DESC + ORDER BY MAX(m.plan_start_time) DESC, progress DESC LIMIT #{topN}