diff --git a/pom.xml b/pom.xml index 7596720..9258f69 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 3.5.2 2.14.2 2.4.0 - 1.18.26 + 1.18.30 1.72 2.7.0 diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 77c9c48..8444dd5 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -110,10 +110,20 @@ com.aliyun dingtalk - 2.2.41 + 2.0.15 + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + + + commons-codec + commons-codec + 1.11 + 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 new file mode 100644 index 0000000..fde62da --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java @@ -0,0 +1,107 @@ +package com.ruoyi.web.controller.dingtalk; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.dingding.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.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/ding/talk") +public class DingTalkController { + /** + * 获取参数配置列表 + */ + @GetMapping("/list") + public R uploadMedia(String filePath) { + String appKey = "dingebfrpzqko25vo8w6"; + String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk"; + + // 如果未提供 filePath,使用默认值 + if (filePath == null || filePath.isEmpty()) { + filePath = "E:/新建文件夹 (2)/4eae3472-de71-4f4b-97cc-4212a88e17c8.png"; + } + + String accessToken = DingUtil.getAccessToken(appKey, appSecret); + // type 固定为 image,也可以根据文件名后缀判断 + String mediaId = DingUtil.uploadMedia(accessToken, "image", filePath); + + return R.ok("上传成功", mediaId); + } + + /** + * 发送互动卡片消息 (IM 1.0) + */ + @GetMapping("/sendCard") + public R sendCard() { + String appKey = "dingebfrpzqko25vo8w6"; + String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk"; + String robotCode = "dingebfrpzqko25vo8w6"; + String openConversationId = "cidCzwPP4a1xhnl9c8hrGXFuw=="; + String cardTemplateId = "2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema"; + + // 获取 AccessToken + String accessToken = DingUtil.getAccessToken(appKey, appSecret); + + // 生成唯一 outTrackId + String outTrackId = "group-card-" + System.currentTimeMillis(); + + // 构造卡片数据 + Map cardDataMap = new HashMap<>(); + cardDataMap.put("drawing_time", "2026/1/13 11:03"); + cardDataMap.put("production_order_no", "CP-026-054-023"); + cardDataMap.put("drawing_by", "毕敏霞"); + cardDataMap.put("bom_uptime", "2025/12/13 11:03"); + cardDataMap.put("drawing_type", "60R"); + cardDataMap.put("project_end_time", "2026/2/13 11:03"); + cardDataMap.put("route_uptime", "2025/12/13 11:03"); + cardDataMap.put("material_count", "2312"); + cardDataMap.put("route_count", "100"); + cardDataMap.put("plan_uptime", "2024/12/13 11:03"); + cardDataMap.put("bom_count", "121"); + cardDataMap.put("img_id", "@lALPM137gRk79qPNA9fNBT4"); + + String trackId = DingUtil.createAndDeliverCard( + accessToken, + cardTemplateId, + robotCode, + openConversationId, + outTrackId, + cardDataMap + ); + + return R.ok("发送卡片成功", trackId); + } + + /** + * 机器人发送群聊消息 + */ + @GetMapping("/sendRobotMessage") + public R sendRobotMessage() { + String appKey = "dingebfrpzqko25vo8w6"; + String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk"; + //机器人编码 + String robotCode = "dingebfrpzqko25vo8w6"; + //群回话的id + String openConversationId = "cidCzwPP4a1xhnl9c8hrGXFuw=="; + //消息类型 + String msgKey = "sampleText"; + + // 获取 AccessToken + String accessToken = DingUtil.getAccessToken(appKey, appSecret); + + Map msgContent = new HashMap<>(); + msgContent.put("content", "Hello"); + + // 调用机器人发送消息方法 + String processQueryKey = DingUtil.robotGroupSend(accessToken, msgContent, msgKey, openConversationId, robotCode, null); + + return R.ok("发送成功", processQueryKey); + } +} diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 74e4414..a1ca630 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -65,7 +65,7 @@ spring: sqlserver: type: com.zaxxer.hikari.HikariDataSource driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver - url: jdbc:sqlserver://192.168.5.8:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true + url: jdbc:sqlserver://192.168.5.8:14330;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true username: sa password: 1a! hikari: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 7708ec3..ac54a1f 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -68,7 +68,7 @@ spring: sqlserver: type: com.zaxxer.hikari.HikariDataSource driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver - url: jdbc:sqlserver://192.168.5.8:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true + url: jdbc:sqlserver://192.168.5.8:14330;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true username: sa password: 1a! # oracle: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 0d28e11..ecb5c6f 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -290,4 +290,4 @@ management: logfile: external-file: ./logs/sys-console.log dingtalk: - + diff --git a/ruoyi-admin/src/main/resources/kdwebapi.properties b/ruoyi-admin/src/main/resources/kdwebapi.properties index c6ba843..0914670 100644 --- a/ruoyi-admin/src/main/resources/kdwebapi.properties +++ b/ruoyi-admin/src/main/resources/kdwebapi.properties @@ -1,6 +1,5 @@ #??ID-PROD -X-KDApi-AcctID = 670768a85463de -#X-KDApi-AcctID = 6723465a38c722 +X-KDApi-AcctID = 695f86f96090b2 X-KDApi-UserName = Administrator #??IDID X-KDApi-AppID = 288012_Rc0C0zCG2lga0/Vs2Y4pzYSL6hQcWOko diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 0238aea..4b80798 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -266,6 +266,18 @@ commons-net 3.6 + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + compile + + + + com.aliyun + dingtalk + 2.0.15 + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java new file mode 100644 index 0000000..cf2ae65 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.dingding; + +import lombok.Data; + +/** + * 创建群聊返回参数DTO + */ +@Data +public class DingChatCreateResult { + /** + * 返回码 + */ + private Long errcode; + + /** + * 返回码描述 + */ + private String errmsg; + + /** + * 群会话的ID (旧版) + * 后续版本中chatid将不再使用,请将openConversationId作为群会话唯一标识 + */ + private String chatid; + + /** + * 群会话的ID + */ + private String openConversationId; + + /** + * 会话类型:2:企业群 + */ + private Long conversationTag; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java new file mode 100644 index 0000000..06c4267 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java @@ -0,0 +1,401 @@ +package com.ruoyi.common.dingding; + +import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders; +import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest; +import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardResponse; +import com.aliyun.dingtalkim_1_0.Client; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse; +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders; +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendRequest; +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse; +import com.aliyun.tea.TeaConverter; +import com.aliyun.tea.TeaPair; +import com.aliyun.teaopenapi.models.Config; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiChatCreateRequest; +import com.dingtalk.api.response.OapiChatCreateResponse; +import com.dingtalk.api.request.OapiMediaUploadRequest; +import com.dingtalk.api.response.OapiMediaUploadResponse; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.dingtalk.api.response.OapiRobotSendResponse; +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.util.Arrays; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DingUtil { + public static final String CUSTOM_ROBOT_TOKEN = "1f3711d72605f77e8ba8e3e3fe0d98989361a7bfb9c30b51a23520eeb783652e"; + + public static final String USER_ID = "4345285524672471"; + + public static final String SECRET = "SECae018c965ccba100318cc2cd5ef7df2e7a3d0379cf96ef39614fd9558209f18c"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + // 固定模板和群 ID + private static final String CARD_TEMPLATE_ID = "2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema"; + private static final String OPEN_CONVERSATION_ID = "cidCzwPP4a1xhnl9c8hrGXFuw=="; + /** + * 发送钉钉文本消息 + * + * @param content 消息内容 + * @param atUserIds 需要@的用户ID列表(可选,传null则不@特定人) + * @param isAtAll 是否@所有人 + */ + public static void sendText(String content, List atUserIds, boolean isAtAll) { + try { + Long timestamp = System.currentTimeMillis(); + String stringToSign = timestamp + "\n" + SECRET; + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(SECRET.getBytes("UTF-8"), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8")); + String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8"); + // sign字段和timestamp字段必须拼接到请求URL上 + String serverUrl = "https://oapi.dingtalk.com/robot/send?sign=" + sign + "×tamp=" + timestamp; + DingTalkClient client = new DefaultDingTalkClient(serverUrl); + OapiRobotSendRequest req = new OapiRobotSendRequest(); + // 设置消息类型 + req.setMsgtype("text"); + + // 定义文本内容 + OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text(); + text.setContent(content); + req.setText(text); + + // 定义 @ 对象 + OapiRobotSendRequest.At at = new OapiRobotSendRequest.At(); + if (atUserIds != null && !atUserIds.isEmpty()) { + at.setAtUserIds(atUserIds); + } + if (isAtAll) { + at.setIsAtAll(true); + } + req.setAt(at); + OapiRobotSendResponse rsp = client.execute(req, CUSTOM_ROBOT_TOKEN); + System.out.println("DingTalk Response: " + rsp.getBody()); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("发送钉钉消息失败", e); + } + } + + /** + * 使用 Token 初始化账号Client (IM) + * @return Client + * @throws Exception + */ + public static com.aliyun.dingtalkim_1_0.Client createImClient() throws Exception { + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkim_1_0.Client(config); + } + + /** + * 使用 Token 初始化账号Client (OAuth2) + * @return Client + * @throws Exception + */ + public static com.aliyun.dingtalkoauth2_1_0.Client createOauthClient() throws Exception { + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkoauth2_1_0.Client(config); + } + + /** + * 获取企业内部应用的accessToken + * @param appKey + * @param appSecret + * @return + */ + public static String getAccessToken(String appKey, String appSecret) { + try { + com.aliyun.dingtalkoauth2_1_0.Client client = createOauthClient(); + GetAccessTokenRequest getAccessTokenRequest = new GetAccessTokenRequest() + .setAppKey(appKey) + .setAppSecret(appSecret); + GetAccessTokenResponse response = client.getAccessToken(getAccessTokenRequest); + return response.getBody().getAccessToken(); + } catch (com.aliyun.tea.TeaException err) { + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + // err 中含有 code 和 message 属性,可帮助开发定位问题 + System.err.println("TeaException: " + err.code + ", " + err.message); + } + throw new RuntimeException(err); + + } catch (Exception _err) { + com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(_err.getMessage(), _err); + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + // err 中含有 code 和 message 属性,可帮助开发定位问题 + System.err.println("Exception: " + err.code + ", " + err.message); + } + throw new RuntimeException(_err); + } + } + + /** + * 上传媒体文件 + * @param accessToken + * @param type 媒体文件类型:image, voice, video, file + * @param filePath 文件路径 + * @return media_id + */ + public static String uploadMedia(String accessToken, String type, String filePath) { + try { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/media/upload"); + OapiMediaUploadRequest req = new OapiMediaUploadRequest(); + req.setType(type); + com.taobao.api.FileItem item = new com.taobao.api.FileItem(filePath); + req.setMedia(item); + OapiMediaUploadResponse rsp = client.execute(req, accessToken); + if (rsp.isSuccess()) { + return rsp.getMediaId(); + } else { + throw new RuntimeException("上传媒体文件失败: " + rsp.getErrmsg()); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("上传媒体文件异常", e); + } + } + + /** + * 创建群聊 + * @param accessToken + * @param name 群名称 + * @param owner 群主userid + * @param userIdList 群成员userid列表 + * @return DingChatCreateResult + */ + public static DingChatCreateResult createChat(String accessToken, String name, String owner, List userIdList) { + try { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/create"); + OapiChatCreateRequest req = new OapiChatCreateRequest(); + //群名称 + req.setName(name); + //群主的userId + req.setOwner(owner); + //群成员列表 + req.setUseridlist(userIdList); + /* + 新成员是否可查看100条历史消息:1:可查看 0:不可查看 + */ + req.setShowHistoryType(1L); + /* + 群是否可以被搜索:0(默认):不可搜索 1:可搜索 + */ + req.setSearchable(0L); + //入群是否需要验证 + req.setValidationType(0L); + //@all 使用范围 + req.setMentionAllAuthority(0L); + //群管理类型:1:仅群主可管理 + req.setManagementType(1L); + //是否开启群禁言 + req.setChatBannedType(0L); + OapiChatCreateResponse rsp = client.execute(req, accessToken); + if (rsp.isSuccess()) { + DingChatCreateResult result = new DingChatCreateResult(); + result.setErrcode(rsp.getErrcode()); + result.setErrmsg(rsp.getErrmsg()); + result.setChatid(rsp.getChatid()); + result.setOpenConversationId(rsp.getOpenConversationId()); + result.setConversationTag(rsp.getConversationTag()); + return result; + } else { + throw new RuntimeException("创建群聊失败: " + rsp.getErrmsg()); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("创建群聊异常", e); + } + } + + /** + * 机器人发送群聊消息(支持 Map 直接传入) + * @param accessToken AccessToken + * @param msgContent 消息内容 Map + * @param msgKey 消息模板 Key + * @param openConversationId 群 ID + * @param robotCode 机器人 Code + * @param coolAppCode 酷应用 Code (可选) + * @return processQueryKey + */ + public static String robotGroupSend( + String accessToken, + Map msgContent, + String msgKey, + String openConversationId, + String robotCode, + String coolAppCode) { + + try { + // 1. 初始化 SDK 客户端 + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); + config.protocol = "https"; + config.regionId = "central"; + com.aliyun.dingtalkrobot_1_0.Client client = new com.aliyun.dingtalkrobot_1_0.Client(config); + + // 2. 设置请求头 + OrgGroupSendHeaders headers = new OrgGroupSendHeaders(); + headers.xAcsDingtalkAccessToken = accessToken; + + // 3. Map 转 JSON 字符串 + String msgParam = OBJECT_MAPPER.writeValueAsString(msgContent); + + // 4. 构建消息请求对象 + OrgGroupSendRequest request = new OrgGroupSendRequest() + .setMsgParam(msgParam) + .setMsgKey(msgKey) + .setOpenConversationId(openConversationId) + .setRobotCode(robotCode); + + if (coolAppCode != null && !coolAppCode.isEmpty()) { + request.setCoolAppCode(coolAppCode); + } + + // 5. 发送消息 + OrgGroupSendResponse response = client.orgGroupSendWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + return response.getBody().getProcessQueryKey(); + + } catch (com.aliyun.tea.TeaException err) { + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + System.err.println("TeaException: " + err.code + ", " + err.message); + } + throw new RuntimeException(err); + } catch (Exception e) { + com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(e.getMessage(), e); + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + System.err.println("Exception: " + err.code + ", " + err.message); + } + throw new RuntimeException(e); + } + } + + /** + * 发送互动消息卡片 (IM 1.0) + * @param accessToken AccessToken + * @param cardTemplateId 卡片模板 ID + * @param robotCode 机器人 Code + * @param openConversationId 群 ID (可选,如果发送到群) + * @param outTrackId 外部跟踪 ID + * @param cardDataMap 卡片数据 Map (key-value) + * @return outTrackId + */ + public static String createAndDeliverCard(String accessToken, String cardTemplateId, String robotCode, + String openConversationId, String outTrackId, + Map cardDataMap) { + try { + com.aliyun.dingtalkim_1_0.Client client = createImClient(); + + SendInteractiveCardHeaders sendInteractiveCardHeaders = new SendInteractiveCardHeaders(); + sendInteractiveCardHeaders.xAcsDingtalkAccessToken = accessToken; + + SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions cardOptions = new SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions() + .setSupportForward(true); + + // 转换数据为 Tea 格式 Map (虽然直接传 Map 也是兼容的,但为了稳妥可以使用 buildMap) + // 这里直接使用传入的 cardDataMap + + SendInteractiveCardRequest.SendInteractiveCardRequestCardData cardData = new SendInteractiveCardRequest.SendInteractiveCardRequestCardData() + .setCardParamMap(cardDataMap); + + SendInteractiveCardRequest sendInteractiveCardRequest = new SendInteractiveCardRequest() + .setConversationType(1) // 1: 群聊 + .setCardData(cardData) + .setCardTemplateId(cardTemplateId) + .setUserIdType(1) + .setOutTrackId(outTrackId) + .setPullStrategy(false) + .setRobotCode(robotCode) + .setOpenConversationId(openConversationId) + .setCardOptions(cardOptions); + + client.sendInteractiveCardWithOptions(sendInteractiveCardRequest, sendInteractiveCardHeaders, new com.aliyun.teautil.models.RuntimeOptions()); + + return outTrackId; + + } catch (com.aliyun.tea.TeaException err) { + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + System.err.println("TeaException: " + err.code + ", " + err.message); + } + throw new RuntimeException(err); + } catch (Exception _err) { + com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(_err.getMessage(), _err); + if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { + System.err.println("Exception: " + err.code + ", " + err.message); + } + throw new RuntimeException(_err); + } + } + + /** + * @description 由于接口要求卡片数据的键值对均为 string 类型,需要对卡片数据进行预处理 + */ + public static Map convertJsonValuesToString(Map obj) { + Map result = new HashMap<>(); + + for (Map.Entry entry : obj.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + result.put(key, (String) value); + } else { + try { + result.put(key, OBJECT_MAPPER.writeValueAsString(value)); + } catch (JsonProcessingException e) { + result.put(key, ""); + } + } + } + + return result; + } + + /** + * 推送群卡片(高级版) + * @param cardParamMap 模板变量映射,例如 production_order_no、bom_count 等 + * @return outTrackId 返回卡片幂等 ID + */ + public static String pushGroupCard(Map cardParamMap) { + try { + // 获取 AccessToken + String accessToken = getAccessToken(CUSTOM_ROBOT_TOKEN, SECRET); // 注意:这里可能需要改为 AppKey/AppSecret + + // 生成唯一 outTrackId + String outTrackId = "track-" + System.currentTimeMillis(); + + // 调用已写好的 createAndDeliverCard 方法 + return createAndDeliverCard( + accessToken, + CARD_TEMPLATE_ID, + CUSTOM_ROBOT_TOKEN, // 这里的 robotCode 可能需要根据实际情况调整 + OPEN_CONVERSATION_ID, + outTrackId, + cardParamMap + ); + + } catch (Exception e) { + throw new RuntimeException("推送群卡片失败", e); + } + } + + + public static void main(String[] args) { + // 测试发送 + sendText("测试消息:刘开~~~,刘开~~~刘开~~~刘开~~~刘开~~~", Arrays.asList(USER_ID), false); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java index f6ccede..dd61af6 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java @@ -579,7 +579,7 @@ public class FtpUtil { } /** - * 下载FTP指定目录中的所有文件到本地 + * 下载FTP指定目录中的所有文件到本地(支持文件名过滤) * * @param ftpHost FTP服务器IP * @param ftpUserName FTP用户名 @@ -587,11 +587,12 @@ public class FtpUtil { * @param ftpPort FTP端口 * @param remoteDir 远程目录路径 * @param localDir 本地保存目录 + * @param fileNameFilter 文件名过滤字符串(只下载包含此字符串的文件),传null或空字符串则下载所有 * @return 下载结果 */ public static R downloadFtpDirectoryFiles(String ftpHost, String ftpUserName, String ftpPassword, int ftpPort, - String remoteDir, String localDir) { + String remoteDir, String localDir, String fileNameFilter) { FTPClient ftpClient = null; try { // 1. 连接FTP服务器 @@ -601,13 +602,18 @@ public class FtpUtil { } // 2. 设置FTP参数 - ftpClient.setControlEncoding("UTF-8"); + // 强制使用GBK编码,避免Windows FTP服务器UTF-8兼容性问题导致的文件名乱码或451错误 + ftpClient.setControlEncoding("GBK"); ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); ftpClient.enterLocalPassiveMode(); // 3. 切换到远程目录 if (!ftpClient.changeWorkingDirectory(remoteDir)) { - return R.fail("远程目录不存在: " + remoteDir); + // 如果切换失败,尝试用ISO-8859-1转码再试 + String isoPath = new String(remoteDir.getBytes("GBK"), "ISO-8859-1"); + if (!ftpClient.changeWorkingDirectory(isoPath)) { + return R.fail("远程目录不存在: " + remoteDir); + } } // 4. 创建本地目录 @@ -626,7 +632,7 @@ public class FtpUtil { // 6. 遍历下载文件 int successCount = 0; - int totalCount = files.length; + int totalCount = 0; // 仅统计符合过滤条件的文件 StringBuilder errorMsg = new StringBuilder(); for (FTPFile file : files) { @@ -636,6 +642,13 @@ public class FtpUtil { if (fileName.startsWith(".") || file.isDirectory()) { continue; } + + // 过滤文件名 + if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) { + continue; + } + + totalCount++; // 计入待下载总数 try { // 构建本地文件路径 @@ -644,19 +657,37 @@ public class FtpUtil { // 下载文件 try (OutputStream os = new FileOutputStream(localFile)) { + // 尝试直接下载 boolean downloadSuccess = ftpClient.retrieveFile(fileName, os); + + // 如果失败,尝试转码下载 (GBK -> ISO-8859-1) + if (!downloadSuccess) { + String isoFileName = new String(fileName.getBytes("GBK"), "ISO-8859-1"); + downloadSuccess = ftpClient.retrieveFile(isoFileName, os); + } + if (downloadSuccess) { successCount++; log.info("文件下载成功: {}", fileName); } else { - errorMsg.append("文件下载失败: ").append(fileName).append("; "); - log.error("文件下载失败: {}", fileName); + errorMsg.append("文件下载失败: ").append(fileName).append(" - ").append(ftpClient.getReplyString()).append("; "); + log.error("文件下载失败: {} - 响应: {}", fileName, ftpClient.getReplyString()); + // 删除下载失败的空文件 + os.close(); + if (localFile.exists()) { + localFile.delete(); + } } } } catch (Exception e) { errorMsg.append("文件下载异常: ").append(fileName).append(" - ").append(e.getMessage()).append("; "); log.error("文件下载异常: {}", fileName, e); + // 删除异常的空文件 + File localFile = new File(localDir + File.separator + fileName); + if (localFile.exists()) { + localFile.delete(); + } } } @@ -684,6 +715,15 @@ public class FtpUtil { } } + /** + * 下载FTP指定目录中的所有文件到本地(兼容旧方法调用,默认下载所有) + */ + public static R downloadFtpDirectoryFiles(String ftpHost, String ftpUserName, + String ftpPassword, int ftpPort, + String remoteDir, String localDir) { + return downloadFtpDirectoryFiles(ftpHost, ftpUserName, ftpPassword, ftpPort, remoteDir, localDir, null); + } + /** * 主方法 - 用于测试FTP目录文件下载功能 */ @@ -695,7 +735,7 @@ public class FtpUtil { int ftpPort = 21; // 远程目录和本地目录配置 - String remoteDir = "/FB-25-039-FS-01/40S-R-2720-T(FS039-25)-PDF"; // 远程目录路径 + String remoteDir = "2026/SH-26-001-LT/T40P1-L-1440-D-PDF"; // 远程目录路径 String localDir = "F:/DownloadedFiles"; // 本地保存目录 System.out.println("开始下载FTP文件..."); 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 7d19383..7ca590c 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 @@ -1701,10 +1701,10 @@ public class BomDetailsController extends BaseController { } subHeadEntity5.addProperty("FIssueType", "1"); - // 创建FPickStockId对象,并加入SubHeadEntity5 + /*// 创建FPickStockId对象,并加入SubHeadEntity5 JsonObject fPickStockId = new JsonObject(); fPickStockId.addProperty("FNumber", " 010"); - subHeadEntity1.add("FPickStockId", fPickStockId); + subHeadEntity1.add("FPickStockId", fPickStockId);*/ subHeadEntity5.addProperty("FOverControlMode", "1"); // 标准人员实作工时 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java index f6080cf..af5dfdb 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java @@ -11,10 +11,7 @@ import com.ruoyi.common.utils.JdUtils; import com.ruoyi.system.domain.ImMaterial; import com.ruoyi.system.domain.bo.ImMaterialBo; import com.ruoyi.system.domain.bo.ImProductionPlanBo; -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.*; import com.ruoyi.system.domain.vo.ImMaterialVo; import com.ruoyi.system.domain.vo.InventoryInfoVO; import com.ruoyi.system.runner.JdUtil; @@ -95,18 +92,27 @@ public class IndexController { .sum()) .orElse(0.0); }, executorService); + CompletableFuture salesOrderFuture = CompletableFuture.supplyAsync(() -> { + log.info("开始查询销售订单未入库数量: {}", materialCode); + return Optional.ofNullable(JdUtil.getSalesOrderList(materialCode)) + .map(list -> list.stream() + .mapToDouble(SalesOrderDTO::getFBaseRemainOutQty) + .sum()) + .orElse(0.0); + }, executorService); // 等待所有任务完成并获取结果 - CompletableFuture resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture) + CompletableFuture resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture,salesOrderFuture) .thenApply(v -> { try { double inventoryQty = inventoryFuture.get(); double fNoPickedQty = noPickedFuture.get(); double productionQty = productionFuture.get(); double purchaseQty = purchaseFuture.get(); + double salesOrder = salesOrderFuture.get(); - // 计算预计可用库存 即时库存+生产未入库+(采购申请-采购未入库)-子项未领料 - double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty; + // 计算预计可用库存 即时库存+生产未入库+(采购申请-采购未入库)-子项未领料-销售实际未出库数量 + double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty-salesOrder; InventoryInfoVO inventoryInfoVO = new InventoryInfoVO(); inventoryInfoVO.setMaterialCode(materialCode); inventoryInfoVO.setKucun(String.valueOf(inventoryQty)); @@ -114,6 +120,7 @@ public class IndexController { inventoryInfoVO.setMaterialName(String.valueOf(purchaseQty)); inventoryInfoVO.setStockName(String.valueOf(productionQty)); inventoryInfoVO.setStockUnit(String.valueOf(keyong)); + inventoryInfoVO.setSalesOrder(String.valueOf(salesOrder)); return inventoryInfoVO; } catch (Exception e) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java index 8f78421..a4ea6c0 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java @@ -456,29 +456,9 @@ public class KingdeeWorkCenterDataController extends BaseController { @PostMapping("/getKingdeeDelayData") public R> getKingdeeDelayData(@RequestParam(value = "workCenter") String workCenter) { try { - /* K3CloudApi client = new K3CloudApi(); - JsonObject parameter = new JsonObject(); - List kingdeeWorkCenterDataVos = new ArrayList<>(); - parameter.addProperty("FWorkCenterName", workCenter); - Object[] parameters = new Object[]{parameter.toString()}; - String execute = client.execute("Ljint.Kingdee.YiTe.KanBan.WebApi.ProduceWebApi.ExecuteService,Ljint.Kingdee.YiTe.KanBan.WebApi", parameters); - log.info("金蝶接口:" + workCenter + "===> 返回数据: {}", execute); - // 解析响应 - JSONObject response = JSONObject.parseObject(execute); - if (!"true".equals(response.getString("IsSuccess"))) { - String errorMsg = response.getString("Message"); - return R.fail("获取工段数据失败:" + errorMsg); - }*/ // 获取明天的日期字符串 (格式: yyyy-MM-dd) String yesterday = DateUtil.format(DateUtil.yesterday(), "yyyy-MM-dd"); - - /* // 获取数据数组 - JSONArray dataArray = response.getJSONArray("data"); - if (dataArray == null || dataArray.isEmpty()) { - return R.ok("无数据"); - } -*/ List kingdeeProduceData = mssqlQueryService.getKingdeeProduceData(workCenter); List kingdeeWorkCenterDataVos = new ArrayList<>(); for (KingdeeWorkCenterDataBo kingnum : kingdeeProduceData) { 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 8c3f83f..984a267 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 @@ -3,13 +3,20 @@ package com.ruoyi.system.controller; import java.io.*; import java.math.BigDecimal; import java.net.URLEncoder; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; import com.alibaba.excel.EasyExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.StringUtils; import com.ruoyi.common.utils.VersionComparator; import com.ruoyi.common.utils.file.SmbUtil; @@ -113,6 +120,11 @@ public class ProcessOrderProController extends BaseController { return R.ok(iProcessOrderProService.queryById(id)); } + /** + * 钉钉数据同步 Webhook 地址 + */ + private static final String DINGTALK_WEBHOOK_URL = "https://connector.dingtalk.com/webhook/flow/103694935fdc210503b10006"; + /** * 新增项目令号 */ @@ -121,7 +133,24 @@ public class ProcessOrderProController extends BaseController { @RepeatSubmit() @PostMapping() public R add(@Validated(AddGroup.class) @RequestBody ProcessOrderProBo bo) { - return toAjax(iProcessOrderProService.insertByBo(bo)); + boolean result = iProcessOrderProService.insertByBo(bo); + if (result) { + // 异步同步数据到钉钉 Webhook + CompletableFuture.runAsync(() -> { + try { + // 将业务对象转换为 JSON 字符串 + String jsonBody = JSONUtil.toJsonStr(bo); + System.out.println(jsonBody); + // 发送 POST 请求 + String response = HttpUtil.post(DINGTALK_WEBHOOK_URL, jsonBody); + System.out.println("钉钉Webhook响应: " + response); + } catch (Exception e) { + // 仅记录日志,不影响主流程 + e.printStackTrace(); + } + }); + } + return toAjax(result); } /** @@ -693,6 +722,8 @@ public class ProcessOrderProController extends BaseController { // 单重 vo.setBatchQuantity(getCellValueAsLong(row.getCell(18))); // 批次数量 vo.setUnitQuantity(getCellValueAsDouble(row.getCell(17))); // 批次数量 + vo.setXuEndTime(getCellValueAsDate(row.getCell(21))); // 批次数量 + vo.setXuStartTime(getCellValueAsDate(row.getCell(20))); // 批次数量 resultList.add(vo); } @@ -832,6 +863,42 @@ public class ProcessOrderProController extends BaseController { } } + /** + * 获取单元格的Date值 + */ + private Date getCellValueAsDate(XSSFCell cell) { + if (cell == null) { + return null; + } + + switch (cell.getCellType()) { + case NUMERIC: + if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } + return null; + case STRING: + try { + String dateStr = cell.getStringCellValue(); + if (dateStr == null || dateStr.trim().isEmpty()) { + return null; + } + // 尝试解析字符串日期 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + try { + return sdf.parse(dateStr); + } catch (ParseException e) { + sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(dateStr); + } + } catch (Exception e) { + return null; + } + default: + return null; + } + } + /** * 获取单元格的Long值 */ @@ -1672,17 +1739,70 @@ public class ProcessOrderProController extends BaseController { try { ProcessOrderPro orderPro = processOrderProMapper.selectById(id); // 下载Excel文件 - SmbUtil.downloadExcelFiles(orderPro.getProductionOrderNo()); + + // 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) {} + } + } + } + + // 2. 构建FTP路径和本地路径 + // 假设Excel文件在 /2026/SH-26-001-LT/ 目录下 + String ftpPath = "/" + year + "/" + productionCode; + String localPath = "D:\\file\\"; + + // 3. 下载文件 (只下载汇总表.xlsx) + FtpUtil.downloadFtpDirectoryFiles("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localPath, "汇总表.xlsx"); + // 构建文件路径 String excelName = "D:\\file\\" + orderPro.getProductionOrderNo() + "汇总表.xlsx"; String rawDataFile = "D:\\file\\RawDataTable.xlsx"; File file = new File(excelName); - if (!file.exists()) { - throw new ServiceException("项目 " + orderPro.getProductionOrderNo() + " 未出图"); + if (!file.exists() || file.length() == 0) { + // 如果文件不存在或为空(下载失败),抛出业务异常 + throw new ServiceException("项目 " + orderPro.getProductionOrderNo() + " 汇总表下载失败或不存在"); } // 1. 读取第一个sheet的数据list - 使用POI直接读取以保留空格 List allDataList = readExcelWithPOI(excelName,orderPro.getProductionOrderNo()); List routeList = readExcelPOIRoute(excelName,orderPro.getProductionOrderNo()); + List routeList2 = processRouteService.selectByProjectNumber(orderPro.getProductionOrderNo()); + + // 将routeList2中有但routeList中没有的记录添加到routeList + if (routeList2 != null && !routeList2.isEmpty()) { + if (routeList == null) { + routeList = new ArrayList<>(); + } + + // 使用 Set 存储 routeList 中已有的 materialCode,用于快速排重 + Set existingMaterialCodes = routeList.stream() + .map(ProcessRoute::getMaterialCode) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + + for (ProcessRoute route : routeList2) { + if (StringUtils.isNotBlank(route.getMaterialCode()) && !existingMaterialCodes.contains(route.getMaterialCode())) { + routeList.add(route); + existingMaterialCodes.add(route.getMaterialCode()); // 避免重复添加 + } + } + } + List routes = new ArrayList<>(); List> kingdeeBomRows = new ArrayList<>(); for (ProcessRoute base : routeList) { @@ -1702,6 +1822,8 @@ public class ProcessOrderProController extends BaseController { item.setDiscWeight(base.getDiscWeight()); item.setUnitQuantity(base.getUnitQuantity()); item.setBatchQuantity(base.getBatchQuantity()); + item.setXuEndTime(base.getXuEndTime()); + item.setXuStartTime(base.getXuStartTime()); routes.add(item); continue; } @@ -1715,6 +1837,8 @@ public class ProcessOrderProController extends BaseController { item.setDiscWeight(base.getDiscWeight()); item.setUnitQuantity(base.getUnitQuantity()); item.setBatchQuantity(base.getBatchQuantity()); + item.setXuEndTime(base.getXuEndTime()); + item.setXuStartTime(base.getXuStartTime()); routes.add(item); continue; } @@ -1740,6 +1864,8 @@ public class ProcessOrderProController extends BaseController { item.setProcessControl(r.getProcessControl()); item.setActivityDuration(r.getActivityDuration()); item.setActivityUnit(r.getActivityUnit()); + item.setXuStartTime(r.getXuStartTime()); + item.setXuEndTime(r.getXuEndTime()); routes.add(item); }); } else { @@ -1751,6 +1877,8 @@ public class ProcessOrderProController extends BaseController { item.setDiscWeight(base.getDiscWeight()); item.setUnitQuantity(base.getUnitQuantity()); item.setBatchQuantity(base.getBatchQuantity()); + item.setXuStartTime(base.getXuStartTime()); + item.setXuEndTime(base.getXuEndTime()); // 不写入BOM字段,保持纯工艺数据行 routes.add(item); } 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 4725aa1..e8a66b4 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 @@ -891,7 +891,7 @@ public class ProcessRouteController extends BaseController { * 更新生产订单仓库字段 */ @SaCheckPermission("system:route:updateProductionOrders") - @Log(title = "获取全部工艺路线和物料清单", businessType = BusinessType.OTHER) + @Log(title = "更新生产订单仓库", businessType = BusinessType.OTHER) @PostMapping("/updateProductionOrders") public R updateProductionOrders(@RequestParam("rooteProdet") String rooteProdet,@RequestParam("cangKuNum")String cangKuNum) throws Exception { //判断此项目是否有 @@ -900,10 +900,10 @@ public class ProcessRouteController extends BaseController { } /** - * 更新采购申请仓库字段 + * 更新采购申请入库仓库仓库 */ @SaCheckPermission("system:route:updateProductionOrders") - @Log(title = "获取全部工艺路线和物料清单", businessType = BusinessType.OTHER) + @Log(title = "更新采购申请单仓库", businessType = BusinessType.OTHER) @PostMapping("/updateCgOrders") public R updateCgOrders(@RequestParam("rooteProdet") String rooteProdet,@RequestParam("cangKuNum")String cangKuNum) throws Exception { //判断此项目是否有 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/CgDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/CgDTO.java index 55c717c..91e00da 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/CgDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/CgDTO.java @@ -9,4 +9,6 @@ public class CgDTO { private String orderBillNo; @JsonProperty("FMaterialId.FNumber") private String materialCode; + @JsonProperty("FMaterialName") + private String materialName; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlanPrcessNumDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlanPrcessNumDTO.java index b9d6e06..a91aeef 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlanPrcessNumDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PlanPrcessNumDTO.java @@ -33,6 +33,6 @@ public class PlanPrcessNumDTO { private Date FOperPlanFinishTime; @JsonProperty("FMONumber") private String FMONumber; - @JsonProperty("F_HBYT_RKCK.FName") + @JsonProperty("F_HBYT_RKCK") private String FRKCKFName; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PurchaseOrderDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PurchaseOrderDTO.java index 41abf4b..8836dec 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PurchaseOrderDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/PurchaseOrderDTO.java @@ -2,7 +2,11 @@ package com.ruoyi.system.domain.dto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; - +/** + * 采购订单未入库数量 + * @author tzy + * @date 2025-10-26 + */ @Data public class PurchaseOrderDTO { @JsonProperty("FMaterialId.FNumber") diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/SalesOrderDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/SalesOrderDTO.java new file mode 100644 index 0000000..99483b8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/SalesOrderDTO.java @@ -0,0 +1,14 @@ +package com.ruoyi.system.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SalesOrderDTO { + @JsonProperty("FMaterialId.FNumber") + private String FMaterialIdFNumber; + @JsonProperty("FMaterialName") + private String FMaterialName; + @JsonProperty("FBaseRemainOutQty") + private Double FBaseRemainOutQty; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InventoryInfoVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InventoryInfoVO.java index bfc6f14..38c9bdc 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InventoryInfoVO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/InventoryInfoVO.java @@ -10,4 +10,5 @@ public class InventoryInfoVO { private String stockName; // 仓库 private String stockUnit; // 单位 private String quantity; // 数量 + private String salesOrder; // 销售数量 } 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 8f52c22..6737055 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 @@ -21,6 +21,12 @@ public class FSubEntity { /* 入库仓库 */ - @JsonProperty("F_HBYT_RKCK.FName") - private String rkckName; + @JsonProperty("F_HBYT_RKCK") + private F_HBYT_RKCK F_HBYT_RKCK; + + @Data + public static class F_HBYT_RKCK { + @JsonProperty("FNUMBER") + private String 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 1d4707c..d47ddc2 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 @@ -11,7 +11,6 @@ import java.util.List; public class Model { @JsonProperty("FID") private int FID; - @JsonProperty("FPlanStartTime") private Date FPlanStartTime; @JsonProperty("FProcessId_number") // 添加 FProcessId_number 字段 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 5a7a8a4..cb6061b 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 @@ -3049,6 +3049,7 @@ public class JdUtil { pageJson.addProperty("StartRow", startRow); pageJson.addProperty("Limit", pageSize); String resultJson = String.valueOf(client.billQuery(pageJson.toString())); + System.out.println("0000000000000000"+ pageJson); JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); if (jsonArray == null || jsonArray.size() == 0) { break; @@ -3432,7 +3433,7 @@ public class JdUtil { // 请求参数,要求为json字符串 JsonObject json = new JsonObject(); json.addProperty("FormId", "PUR_Requisition"); - json.addProperty("FieldKeys", "FBillNo,FMaterialId.FNumber"); + json.addProperty("FieldKeys", "FBillNo,FMaterialId.FNumber,FMaterialName"); JsonArray filterString = new JsonArray(); JsonObject filterObject = new JsonObject(); filterObject.addProperty("FieldName", "F_UCHN_Text"); @@ -3473,7 +3474,8 @@ public class JdUtil { JsonArray needUpDateFields = new JsonArray(); needUpDateFields.add("FID"); - needUpDateFields.add("F_HBYT_RKCK.FName"); + needUpDateFields.add("FENTRYID"); + needUpDateFields.add("F_HBYT_RKCK"); needUpDateFields.add("FTreeEntity"); json.add("NeedUpDateFields", needUpDateFields); @@ -3484,7 +3486,11 @@ public class JdUtil { JsonArray fTreeEntity = new JsonArray(); JsonObject fTreeEntityItem = new JsonObject(); - fTreeEntityItem.addProperty("F_HBYT_RKCK.FName", cangKuNum); + // 修改:使用 FNUMBER 对象结构设置仓库 + JsonObject rkck = new JsonObject(); + rkck.addProperty("FNUMBER", cangKuNum); + fTreeEntityItem.add("F_HBYT_RKCK", rkck); + fTreeEntityItem.addProperty("FENTRYID", jdHuoZhu.getFTreeEntityFENTRYID()); fTreeEntity.add(fTreeEntityItem); model.add("FTreeEntity", fTreeEntity); @@ -3523,8 +3529,12 @@ public class JdUtil { JsonArray fTreeEntity = new JsonArray(); JsonObject fTreeEntityItem = new JsonObject(); - fTreeEntityItem.addProperty("F_HBYT_RKCK.FName", cangKuNum); - fTreeEntityItem.addProperty("FEntity_FEntryID", fidEntryIdDTO.getFEntryId()); + // 修改:使用 FNUMBER 对象结构设置仓库 + JsonObject rkck = new JsonObject(); + rkck.addProperty("FNUMBER", cangKuNum); + fTreeEntityItem.add("F_HBYT_RKCK", rkck); + + fTreeEntityItem.addProperty("FEntryID", fidEntryIdDTO.getFEntryId()); fTreeEntity.add(fTreeEntityItem); model.add("FEntity", fTreeEntity); json.add("Model", model); @@ -3539,9 +3549,9 @@ public class JdUtil { RepoRet sRet = gson.fromJson(result, RepoRet.class); if (sRet.isSuccessfully()) { - return "采购申请单保存成功"; + return gson.toJson(sRet.getResult()); } else { - return "采购申请单成功失败:" + gson.toJson(sRet.getResult()); + return gson.toJson(sRet.getResult()); } } @@ -3588,4 +3598,65 @@ public class JdUtil { } return Collections.emptyList(); } + + /** + * 销售订单的查询 + * @param materialCode + * @return + */ + public static List getSalesOrderList(String materialCode) { + + K3CloudApi client = new K3CloudApi(); + + // 请求参数,要求为json字符串 + JsonObject json = new JsonObject(); + json.addProperty("FormId", "SAL_SaleOrder"); + json.addProperty("FieldKeys", + "FMaterialId.FNumber,FMaterialName,FBaseRemainOutQty"); + // 创建过滤条件 + JsonArray filterString = new JsonArray(); + JsonObject filterObject = new JsonObject(); + filterObject.addProperty("FieldName", "FMaterialId.FNumber"); + filterObject.addProperty("Compare", "="); + filterObject.addProperty("Value", materialCode); + filterObject.addProperty("Left", ""); + filterObject.addProperty("Right", ""); + filterObject.addProperty("Logic", 0); + filterString.add(filterObject); + JsonObject filterObject1 = new JsonObject(); + filterObject1.addProperty("FieldName", "FCloseStatus"); + filterObject1.addProperty("Compare", "105"); + filterObject1.addProperty("Value", "A"); + filterObject1.addProperty("Left", ""); + filterObject1.addProperty("Right", ""); + filterObject1.addProperty("Logic", 0); + filterString.add(filterObject1); + json.add("FilterString", filterString); + json.addProperty("OrderString", ""); + json.addProperty("TopRowCount", 0); + json.addProperty("StartRow", 0); + json.addProperty("Limit", 2000); + json.addProperty("SubSystemId", ""); + + String jsonData = json.toString(); + List plannedProcessList = null; + try { + // 调用接口 + String resultJson = String.valueOf(client.billQuery(jsonData)); + JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class); + + // 使用 ObjectMapper 将 JsonArray 转换为 List + ObjectMapper objectMapper = new ObjectMapper(); + plannedProcessList = objectMapper.readValue(jsonArray.toString(), + new TypeReference>() { + + }); + + } catch (Exception e) { + e.printStackTrace(); // 输出异常日志 + } + + return plannedProcessList; // 返回结果 + } + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/updatePcessPlanConver.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/updatePcessPlanConver.java index 2e683a1..c755761 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/updatePcessPlanConver.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/updatePcessPlanConver.java @@ -126,7 +126,7 @@ public class updatePcessPlanConver { for (Model model : numDTO) { MainModel mainModel = new MainModel(); List needUpDateFields = Arrays.asList( - "FEntity", "FSubEntity", "F_HBYT_RKCK.FName" + "FEntity", "FSubEntity", "F_HBYT_RKCK" ); mainModel.setNeedUpDateFields(needUpDateFields); mainModel.setDeleteEntry(false); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBomDetailsService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBomDetailsService.java index 035ebc0..4389817 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IBomDetailsService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IBomDetailsService.java @@ -58,4 +58,5 @@ public interface IBomDetailsService { List selectByFNumberAndTotalWeight(String fnumber, String totalWeight); List selectByProjectNumber(String totalWeight); + List selectMaterialZH(String totalWeight,String partNumber); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessRouteService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessRouteService.java index 7ee9796..e814708 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessRouteService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IProcessRouteService.java @@ -119,6 +119,7 @@ public interface IProcessRouteService { boolean isAnTuDingGou(String materialCode); List getSelectProcessRoute(String materilCode); + List getSelectStandProcessRoute(String materilCode); //根据项目令号删除 材料bom 总装bom 工艺路线 R delByProjectCode(String productionOrderNo); 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 372f183..aadddba 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 @@ -26,6 +26,32 @@ public class MssqlQueryService { return jdbcTemplate.queryForList(sql); } + /** + * 根据物料编码查询是否是VMI业务 + * @param materialNumber 物料编码 + * @return true:是VMI业务, false:否 + */ + @DS("sqlserver") + public boolean checkIfVmiBusiness(String materialNumber) { + String sql = "SELECT p.FISVMIBUSINESS " + + " FROM T_BD_MATERIAL m " + + " INNER JOIN T_BD_MATERIALPURCHASE p ON m.FMATERIALID = p.FMATERIALID " + + " WHERE m.FNUMBER = ?"; + try { + List> list = jdbcTemplate.queryForList(sql, materialNumber); + if (list != null && !list.isEmpty()) { + Object val = list.get(0).get("FISVMIBUSINESS"); + if (val != null) { + String s = val.toString(); + return "1".equals(s) || "true".equalsIgnoreCase(s); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + @DS("sqlserver") public List getKingdeeProduceData(String workCenterName) { String sql = "select t8.FBillNo as MoBillNo,t8.F_HBYT_SCLH as MoOrderNo, \n" + 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 332ef8a..28678ba 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 @@ -246,6 +246,24 @@ public class BomDetailsServiceImpl implements IBomDetailsService { return baseMapper.selectList(bomDetailsLambdaQueryWrapper); } + /** + * 查询部件是否为组焊件 + * @param totalWeight 总权重/版本号 + * @param partNumber 部件编码 + * @return 如果是组焊件,返回包含该记录的列表;否则返回空列表 + */ + @Override + public List selectMaterialZH(String totalWeight, String partNumber) { + // 你的逻辑:fProcessIdNumber作为partNumber,totalWeight,Material=组焊件 + // 查询出数据 就代表他就是组焊件子件(或者说需要走CK025) + LambdaQueryWrapper bq = new LambdaQueryWrapper<>(); + bq.eq(BomDetails::getTotalWeight, totalWeight); + bq.eq(BomDetails::getPartNumber, partNumber); + bq.eq(BomDetails::getMaterial, "组焊件"); + + return baseMapper.selectList(bq); + } + public int loadMaterialPreservation(BomDetails bomDetails1) { K3CloudApi client = new K3CloudApi(); 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 2f44f3d..3db5c70 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 @@ -1004,14 +1004,16 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { String code = processOrderPro.getProductionOrderNo(); log.info("开始处理项目PDF文件,项目令号: {}", code); - // 3. 查询产品信息 + // 3. 查询产品信息/system/orderPro/uploadPDF + List fiseList = iFigureSaveService.selectByFid(id); if (fiseList == null || fiseList.isEmpty()) { return R.fail("项目下没有找到产品信息"); } // 4. 构建本地根目录 - String localBaseDir = "D:/2025/"; + // 修正:不再硬编码为 D:/2025/,而是根据 code 动态构建(与后续处理逻辑保持一致) + String localBaseDir = buildWorkDirectory(code); // 5. 清空本地目标文件夹 String targetDir = localBaseDir + code; File targetDirectory = new File(targetDir); @@ -1029,9 +1031,46 @@ 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 { + Integer.parseInt(yearShort); + 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 { + Integer.parseInt(yearShort); // 验证是否为数字 + year = "20" + yearShort; + } catch (NumberFormatException e) { + // 解析失败,保持默认 + } + } + } + } + // 拼接远程文件夹路径 - String ftpPath = "/" + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF"; + // 确保路径使用正斜杠,且不包含本地盘符 + 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"; @@ -1147,11 +1186,11 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { * 将 PDF 按工作中心分类复制到目标目录(如 zip/工作中心/) */ private void classifyPdfByWorkCenterToTarget(String productionOrderNo,String sourceDirPath, String code, String targetBaseDirPath) { - //按工作中心 分类时 查找是否有采购申请单 和生产订单 + /* //按工作中心 分类时 查找是否有采购申请单 和生产订单 //获取这个项目的所有生产令号 List orderList = JdUtil.getOrderNumberCollection(productionOrderNo); - List cgorderList = JdUtil.getCaiGouOrderNumberList(productionOrderNo); - + List cgorderList = JdUtil.getCaiGouOrderNumberList(productionOrderNo);*/ + File sourceDir = new File(sourceDirPath); if (!sourceDir.exists() || !sourceDir.isDirectory()) return; @@ -1166,16 +1205,16 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { if (materialCode == null) continue; String workCenter; - //如果这个文件的生产令号 在 生产令号列表 中 或者 采购申请单列表 中 ,则 可以分类到对应的文件夹中 + /* //如果这个文件的生产令号 在 生产令号列表 中 或者 采购申请单列表 中 ,则 可以分类到对应的文件夹中 // 由于物料名近似(如KP08和KP08.7),使用equals精确匹配,不使用contains if (!orderList.contains(materialCode) && !cgorderList.contains(materialCode)) { unmovedMaterials.add(materialCode); workCenter = "未匹配订单"; - } else { + } else {*/ workCenter = iProcessRouteService.getRouteCode(materialCode, code); if (iProductionOrderService.isPurchas(productionOrderNo, materialCode)) workCenter = "外购件"; if (workCenter == null || workCenter.isEmpty()) workCenter = "无工段"; - } + /*}*/ File workCenterDir = new File(targetBaseDirPath, workCenter); if (!workCenterDir.exists()) workCenterDir.mkdirs(); @@ -1188,7 +1227,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { log.warn("PDF {} 复制失败: {}", pdf.getName(), e.getMessage()); } } - + if (!unmovedMaterials.isEmpty()) { log.info("以下物料未匹配到生产订单或采购申请单,未进行分类移动: {}", unmovedMaterials); } 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 8af29da..1f1cce0 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 @@ -19,8 +19,6 @@ 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.exception.ServiceException; -import com.ruoyi.system.domain.dto.BOMItem; -import com.ruoyi.system.domain.dto.BOMUploadResult; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.controller.ProcessRouteController; import com.ruoyi.system.domain.*; @@ -55,7 +53,6 @@ import java.util.concurrent.*; import java.util.stream.Collectors; import static com.ruoyi.system.controller.BomDetailsController.*; -import static com.ruoyi.system.runner.JdUtil.getCaiGouOrderNumberList; import static com.ruoyi.system.runner.JdUtil.updateCgOrder1; import static com.ruoyi.system.runner.JsonConverter.createProcessModel; import static com.ruoyi.system.runner.updatePcessPlanConver.updatePcessPlan1; @@ -70,8 +67,9 @@ import static com.ruoyi.system.runner.updatePcessPlanConver.updatePcessPlan2; @RequiredArgsConstructor @Service public class ProcessRouteServiceImpl implements IProcessRouteService { - private final IMaterialTotalService iMaterialTotalService; + private final IImMaterialService imMaterialService; private final IBomDetailsService iBomDetailsService; + private final MssqlQueryService sqlQueryService; private final ProcessRouteMapper baseMapper; private final BomDetailsMapper bomDetailsMapper; private final IMaterialPropertiesService iMaterialPropertiesService; @@ -84,6 +82,17 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { private static final Logger log = LoggerFactory.getLogger(ProcessRouteController.class); private static final Logger logger = LoggerFactory.getLogger(IProcessRouteService.class); + // 全局线程池:核心线程10,最大线程50,存活60s,有界队列 + private static final ExecutorService GLOBAL_EXECUTOR = new ThreadPoolExecutor( + 10, 50, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(1000), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行,防止丢任务 + ); + + // 单据锁 Map + private static final ConcurrentHashMap BILL_LOCK = new ConcurrentHashMap<>(); + /** * 查询工艺路线 */ @@ -244,7 +253,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { * @return */ @Override - public List getProcessRouteGD(List list,String rooteProdet) { + public List getProcessRouteGD(List list, String rooteProdet) { List materialAndRouteList = new ArrayList<>(); for (ProcessRouteGetDTO processRoute : list) { // 跳过空行或解析为空的记录,避免空指针 @@ -327,15 +336,14 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ProcessRoute::getRouteDescription, productionOrderNo) .eq(ProcessRoute::getMaterialCode, materialCode) - .eq(ProcessRoute::getProcessNo,xu); + .eq(ProcessRoute::getProcessNo, xu); if (baseMapper.selectOne(wrapper) != null) { - return baseMapper.selectOne(wrapper); + return baseMapper.selectOne(wrapper); } return null; } - private String generateKey1(BomDetails bomDetail, ProcessRoute processRoute) { return String.format("%s:%s:%s", processRoute.getMaterialCode(), processRoute.getMaterialName(), bomDetail.getName(), bomDetail.getPartNumber()); } @@ -794,10 +802,10 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { } else if ("根".equals(unit) && quantity.contains("/")) { //写入工艺表时分数的体现 直接取分母 以"/"为分割符,取第二个字符串 materialBom.setQuantity(quantity); - } else if(!"根".equals(unit)&&quantity.contains("/") ){ + } else if (!"根".equals(unit) && quantity.contains("/")) { // throw new RuntimeException("单位不为根的时候不能为分数"); - }else { + } else { // 其他单位直接使用原值,保留2位小数 materialBom.setQuantity(String.valueOf(new BigDecimal(quantity).setScale(2, RoundingMode.HALF_UP))); } @@ -1264,7 +1272,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { } @Override - public R pushRouteBom(String rooteProdet,String groupName) { + public R pushRouteBom(String rooteProdet, String groupName) { List rawBomList = getProcessRoute(rooteProdet); List successfulRoutes = new ArrayList<>(); List failedRoutes = new ArrayList<>(); @@ -1281,14 +1289,14 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { if (isDifferent) { log.info("工艺路线不同,进行更新: " + processRouteXuDTO.getMaterialCode()); // 保存工艺路线 - LoadBomResult result = loadBillOfMaterialsPreservation(processRouteXuDTO,groupName); + LoadBomResult result = loadBillOfMaterialsPreservation(processRouteXuDTO, groupName); // 处理返回结果 if (result.isSuccess()) { log.info("工艺路线保存成功: " + processRouteXuDTO.getMaterialCode() + result.getResultData()); successfulRoutes.add(processRouteXuDTO); } else { log.info("工艺路线保存失败: " + processRouteXuDTO.getMaterialCode() + result.getMessage()); - processRouteXuDTO.setErrorMessage( result.getMessage()); + processRouteXuDTO.setErrorMessage(result.getMessage()); failedRoutes.add(processRouteXuDTO); } } else { @@ -1331,7 +1339,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { log.info("工艺路线不同,进行更新: " + processRouteXuDTO.getMaterialCode()); // 保存工艺路线 - LoadBomResult result = loadBillOfMaterialsPreservation(processRouteXuDTO,""); + LoadBomResult result = loadBillOfMaterialsPreservation(processRouteXuDTO, ""); // 处理返回结果 if (result.isSuccess()) { log.info("工艺路线保存成功: " + processRouteXuDTO.getMaterialCode() + result.getResultData()); @@ -1412,8 +1420,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { // 请求参数,要求为json字符串 JsonObject json = new JsonObject(); json.addProperty("FormId", "SFC_OperationPlanning"); - json.addProperty("FieldKeys", - "FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName"); + json.addProperty("FieldKeys", "FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName"); // 创建过滤条件 JsonArray filterString = new JsonArray(); JsonObject filterObject = new JsonObject(); @@ -1619,13 +1626,13 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { } // 工艺保存方法 - public LoadBomResult loadBillOfMaterialsPreservation(ProcessRouteXuDTO rawBomList,String groupName) { + public LoadBomResult loadBillOfMaterialsPreservation(ProcessRouteXuDTO rawBomList, String groupName) { // TODO: 实现加载和保存物料清单数据的逻辑 K3CloudApi client = new K3CloudApi(); - ProcessModel processModel = createProcessModel(rawBomList,groupName); + ProcessModel processModel = createProcessModel(rawBomList, groupName); String jsonStr = JSONUtil.toJsonStr(processModel); - log.debug("推送工艺报文=====》{}",JSONUtil.toJsonStr(jsonStr)); + log.debug("推送工艺报文=====》{}", JSONUtil.toJsonStr(jsonStr)); try { // 业务对象标识 String formId = "ENG_Route"; @@ -2031,7 +2038,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { json.addProperty("FieldKeys", "FID,FSubEntity_FDetailID,FProductId.FNumber,FOperNumber,FEntity_FEntryID,FProcessId.FName,FSeqNumber,FSeqName,FPlanStartTime,FPlanFinishTime," + - "FOperPlanStartTime,FOperPlanFinishTime,FMONumber,F_HBYT_RKCK.FName"); + "FOperPlanStartTime,FOperPlanFinishTime,FMONumber,F_HBYT_RKCK"); JsonArray filterString = new JsonArray(); JsonObject filterObject = new JsonObject(); @@ -2115,7 +2122,11 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { fSubEntity.setFOperNumber(dto.getFOperNumber()); fSubEntity.setFOperPlanStartTime((dto.getFOperPlanStartTime())); fSubEntity.setFOperPlanFinishTime(dto.getFOperPlanFinishTime()); - fSubEntity.setRkckName(dto.getFRKCKFName()); + + FSubEntity.F_HBYT_RKCK rkck = new FSubEntity.F_HBYT_RKCK(); + rkck.setFNUMBER(dto.getFRKCKFName()); + fSubEntity.setF_HBYT_RKCK(rkck); + fEntity.getFSubEntity().add(fSubEntity); } @@ -2601,55 +2612,198 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { */ @Override public List updateProductionOrders(String rooteProdet, String cangKuNum) throws Exception { - // 1. 更新生产订单 (PRD_MO) + // 创建一个线程安全的List来收集结果 + List results = Collections.synchronizedList(new ArrayList<>()); + List planOrderList = getSelectProceOrder1(rooteProdet); - for (PlanOrderVo planOrderVo : planOrderList) { - JdHuoZhu fidToProductionOrder = JdUtil.getFIDToProductionOrder(planOrderVo); - if (fidToProductionOrder != null) { - // 更新生产订单的仓库 - JdUtil.updateOrder2(fidToProductionOrder, cangKuNum); + List> futures = new ArrayList<>(); + + // 1. 更新生产订单 (PRD_MO) + if (planOrderList != null && !planOrderList.isEmpty()) { + // 按单据号分组,减少锁竞争和线程数 + Map> groupedByBillNo = planOrderList.stream() + .collect(Collectors.groupingBy(PlanOrderVo::getFBillNo)); + + for (Map.Entry> entry : groupedByBillNo.entrySet()) { + String billNo = entry.getKey(); + List orders = entry.getValue(); + + // 使用全局线程池,每个单据号一个任务 + Future future = GLOBAL_EXECUTOR.submit(() -> { + // 获取锁对象,防止跨请求并发 + Object lock = BILL_LOCK.computeIfAbsent(billNo, k -> new Object()); + + synchronized (lock) { + try { + for (PlanOrderVo planOrderVo : orders) { + String currentCangKuNum = cangKuNum; + // 判断是否为组焊件 + if (planOrderVo.getFmaterialidFnumber() != null) { + List bomDetails = iBomDetailsService.selectMaterialZH(rooteProdet, planOrderVo.getFmaterialidFnumber()); + if (bomDetails != null && !bomDetails.isEmpty()) { + currentCangKuNum = "CK025"; + } + } + + JdHuoZhu fidToProductionOrder = JdUtil.getFIDToProductionOrder(planOrderVo); + if (fidToProductionOrder != null) { + String result = JdUtil.updateOrder2(fidToProductionOrder, currentCangKuNum); + results.add("生产订单[" + planOrderVo.getFBillNo() + "]更新结果: " + result); + } + } + } catch (Exception e) { + log.error("更新生产订单失败: {}", billNo, e); + results.add("生产订单[" + billNo + "]更新失败: " + e.getMessage()); + } + } + }); + futures.add(future); } } // 2. 更新工序计划单 (SFC_OperationPlanning) - List numDTOS = getSelecPlan(rooteProdet); - boolean needUpdate = false; - for (Model numDTO : numDTOS) { - List fEntityList = numDTO.getFEntity(); - if (fEntityList != null && !fEntityList.isEmpty()) { - for (FEntity fEntity : fEntityList) { - List fSubEntityList = fEntity.getFSubEntity(); - if (fSubEntityList != null && !fSubEntityList.isEmpty()) { - for (FSubEntity fSubEntity : fSubEntityList) { - // 设置入库仓库 - fSubEntity.setRkckName(cangKuNum); - needUpdate = true; + // 将此逻辑也放入 Future 中,实现与生产订单更新的并行处理 + Future planUpdateFuture = GLOBAL_EXECUTOR.submit(() -> { + try { + List numDTOS = getSelecPlan(rooteProdet); + boolean needUpdate = false; + if (numDTOS != null) { + for (Model numDTO : numDTOS) { + String fProcessIdNumber = numDTO.getFProcessId_number(); + String finalCurrentCangKuNum = cangKuNum; + // 判断是否为组焊件 + List bomDetails = iBomDetailsService.selectMaterialZH(rooteProdet, fProcessIdNumber); + if (bomDetails != null && !bomDetails.isEmpty()) { + // 这里假设 selectMaterialZH 返回的是匹配的记录 + finalCurrentCangKuNum = "CK025"; + } + List fEntityList = numDTO.getFEntity(); + if (fEntityList != null && !fEntityList.isEmpty()) { + for (FEntity fEntity : fEntityList) { + List fSubEntityList = fEntity.getFSubEntity(); + if (fSubEntityList != null && !fSubEntityList.isEmpty()) { + for (FSubEntity fSubEntity : fSubEntityList) { + // 设置入库仓库 + FSubEntity.F_HBYT_RKCK rkck = new FSubEntity.F_HBYT_RKCK(); + rkck.setFNUMBER(finalCurrentCangKuNum); + fSubEntity.setF_HBYT_RKCK(rkck); + needUpdate = true; + } + } + } + } + } + if (needUpdate) { + RepoRet repoRet = updatePcessPlan2(numDTOS); + if (repoRet != null) { + results.add("工序计划单批量更新结果: " + (repoRet.isSuccessfully() ? "成功" : "失败")); } } } + } catch (Exception e) { + log.error("更新工序计划单失败", e); + results.add("工序计划单更新失败: " + e.getMessage()); + } + }); + futures.add(planUpdateFuture); + + // 等待所有任务(生产订单 + 工序计划单)完成 + for (Future future : futures) { + try { + future.get(); + } catch (Exception e) { + log.error("等待线程执行异常", e); } } + // 注意:全局线程池不需要shutdown - if (needUpdate) { - updatePcessPlan2(numDTOS); - } - - return Collections.emptyList(); + return results; } + /** * 更新采购申请仓库字段 */ public List updateCgOrders(String rooteProdet, String cangKuNum) throws Exception { List list = JdUtil.getCGOrderFBillNoList2(rooteProdet); - for (CgDTO orderBillNo : list) { - List fidEntryIdDTOS = JdUtil.queryFidAndEntryId(orderBillNo.getOrderBillNo()); - for (FidEntryIdDTO fidEntryIdDTO : fidEntryIdDTOS) { - String s = updateCgOrder1(fidEntryIdDTO, cangKuNum); - } - + if (list == null || list.isEmpty()) { + return Collections.emptyList(); } - return null; + // 创建一个线程安全的List来收集结果 + List results = Collections.synchronizedList(new ArrayList<>()); + List> futures = new ArrayList<>(); + + // 按单据号分组,减少锁竞争和线程数 + Map> groupedByBillNo = list.stream() + .collect(Collectors.groupingBy(CgDTO::getOrderBillNo)); + + for (Map.Entry> entry : groupedByBillNo.entrySet()) { + String billNo = entry.getKey(); + List cgList = entry.getValue(); + + // 使用全局线程池,每个单据号一个任务 + Future future = GLOBAL_EXECUTOR.submit(() -> { + // 获取锁对象,防止跨请求并发 + Object lock = BILL_LOCK.computeIfAbsent(billNo, k -> new Object()); + + synchronized (lock) { + try { + for (CgDTO cg : cgList) { + ImMaterial material = imMaterialService.selectByCodeAndName(cg.getMaterialCode(), cg.getMaterialName()); + List fidEntryIdDTOS = JdUtil.queryFidAndEntryId(cg.getOrderBillNo()); + + if (fidEntryIdDTOS != null && !fidEntryIdDTOS.isEmpty()) { + for (FidEntryIdDTO fidEntryIdDTO : fidEntryIdDTOS) { + + String currentCangKuNum = cangKuNum; + String materialCode = cg.getMaterialCode(); + + if (materialCode != null) { + // VMI库 + boolean result = sqlQueryService.checkIfVmiBusiness(materialCode); + if (material != null && result) { + continue; + } + // 015开头:原材料入库 + else if (materialCode.startsWith("015") || materialCode.startsWith("AB")) { + currentCangKuNum = "CK001"; + } + // 毛坯库 + else if (materialCode.startsWith("(M)")) { + currentCangKuNum = "CK002"; + } + // 电器入库 + /*else if (materialCode.startsWith("DK") || materialCode.startsWith("DC") || materialCode.startsWith("DP") || materialCode.startsWith("DT") + || materialCode.startsWith("DQ") || materialCode.startsWith("DD") || materialCode.startsWith("DF") || materialCode.startsWith("DX") + || materialCode.startsWith("DL") || materialCode.startsWith("DH") || materialCode.startsWith("DJ") || materialCode.startsWith("DZ") || materialCode.startsWith("DB")) { + currentCangKuNum = "CK006"; + }*/ + } + + String resultStr = updateCgOrder1(fidEntryIdDTO, currentCangKuNum); + results.add(resultStr); + } + } + } + } catch (Exception e) { + log.error("更新采购订单失败: {}", billNo, e); + results.add("更新失败: " + billNo + " - " + e.getMessage()); + } + } + }); + futures.add(future); + } + + // 等待所有任务完成 + for (Future future : futures) { + try { + future.get(); + } catch (Exception e) { + log.error("等待线程执行异常", e); + } + } + + return results; } }