From 84fb4742d33b4516e8a159e0598e6980b3bb4d86 Mon Sep 17 00:00:00 2001 From: tzy Date: Mon, 23 Mar 2026 13:40:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dingtalk/CallbackController.java | 60 +-- .../dingtalk/DingTalkController.java | 2 +- .../src/main/resources/application.yml | 4 +- .../common/config/DingTalkProperties.java | 36 ++ .../common/dingding/card/CardService.java | 132 +++++ .../config/DingAutoConfiguration.java | 18 + .../dingding/corp/CorpMessageService.java | 67 +++ .../dingding/exception/DingException.java | 29 ++ .../ruoyi/common/dingding/im/ImService.java | 36 ++ .../common/dingding/media/MediaService.java | 59 +++ .../dingding/robot/RobotGroupService.java | 223 +++++++++ .../dingding/token/AccessTokenManager.java | 71 +++ .../common/dingding/{ => util}/DingUtil.java | 5 +- .../dingding/webhook/WebhookClient.java | 69 +++ .../java/com/ruoyi/common/utils/FtpUtil.java | 104 +++- .../com/ruoyi/common/utils/poi/ExcelUtil.java | 12 + .../controller/BomDetailsController.java | 2 +- .../controller/FigureSaveController.java | 36 +- .../KingdeeWorkCenterDataController.java | 4 +- .../controller/ProcessOrderProController.java | 88 +++- .../controller/ProcessRouteController.java | 16 +- .../com/ruoyi/system/domain/FigureSave.java | 24 + .../ruoyi/system/domain/ProcessOrderPro.java | 22 +- .../ruoyi/system/domain/bo/FigureSaveBo.java | 33 +- .../system/domain/bo/ProcessOrderProBo.java | 26 +- .../dto/excuteDrawing/RigidChainModelDTO.java | 27 ++ .../ruoyi/system/domain/vo/FigureSaveVo.java | 64 ++- .../system/domain/vo/ProcessOrderProVo.java | 37 +- .../ruoyi/system/mapper/BomDetailsMapper.java | 4 +- .../ruoyi/system/mapper/FigureSaveMapper.java | 4 +- .../system/mapper/MaterialBomMapper.java | 4 +- .../system/mapper/ProcessRouteMapper.java | 2 +- .../system/mapper/ProductionOrderMapper.java | 4 +- .../system/service/IFigureSaveService.java | 7 +- .../service/IProcessOrderProService.java | 18 + .../service/impl/FigureSaveServiceImpl.java | 45 +- .../impl/ProcessOrderProServiceImpl.java | 450 +++++++++++++----- .../service/impl/ProcessRouteServiceImpl.java | 16 +- .../impl/WorkProcedureServiceImpl.java | 1 - .../mapper/system/FigureSaveMapper.xml | 5 + .../mapper/system/ProcessOrderProMapper.xml | 8 +- 41 files changed, 1646 insertions(+), 228 deletions(-) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/card/CardService.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/config/DingAutoConfiguration.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/corp/CorpMessageService.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/exception/DingException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/im/ImService.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/media/MediaService.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/robot/RobotGroupService.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/token/AccessTokenManager.java rename ruoyi-common/src/main/java/com/ruoyi/common/dingding/{ => util}/DingUtil.java (99%) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/dingding/webhook/WebhookClient.java 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 3af98ef..83e6014 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 @@ -6,7 +6,7 @@ import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.dingding.DingCallbackCrypto; -import com.ruoyi.common.dingding.DingUtil; +import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.system.controller.KingdeeWorkCenterDataController; import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.mapper.ProcessOrderProMapper; @@ -39,6 +39,13 @@ public class CallbackController { private DingTalkProperties dingTalkProperties; @Autowired private KingdeeWorkCenterDataController kingdeeWorkCenterDataController; + @Autowired + private com.ruoyi.common.dingding.card.CardService cardService; + + @Autowired + private com.ruoyi.common.dingding.robot.RobotGroupService robotGroupService; // 注入 RobotGroupService + @Autowired + private com.ruoyi.common.dingding.webhook.WebhookClient webhookClient; // 注入 WebhookClient @PostMapping("/receive") public Map callBack(@RequestBody(required = false) Map json) { @@ -166,7 +173,7 @@ public class CallbackController { // 调用 DingUtil 更新卡片 try { - DingUtil.updateInteractiveCard(accessToken, outTrackId, cardDataMap); + cardService.updateInteractiveCard(accessToken, outTrackId, cardDataMap); bizLogger.info("成功更新卡片状态: outTrackId={}, action={}", outTrackId, action); } catch (Exception e) { bizLogger.error("更新卡片失败", e); @@ -230,10 +237,8 @@ public class CallbackController { String replyContent = "你好 @" + senderId; // 使用 Text 消息格式 - Map textParam = new HashMap<>(); - textParam.put("content", replyContent); try { - DingUtil.robotGroupSend(accessToken, textParam, "sampleText", conversationId, targetAppKey, null); + robotGroupService.sendText(conversationId, replyContent, Collections.singletonList(senderId)); bizLogger.info("已回复你好(Text),并@用户: {}", senderId); } catch (Exception e) { bizLogger.error("回复你好失败", e); @@ -344,7 +349,7 @@ public class CallbackController { // 使用回调中的 sessionWebhook 直接回复 (这是最标准的回调回复方式) if (sessionWebhook != null && !sessionWebhook.isEmpty()) { - DingUtil.sendSessionText(sessionWebhook, helpContent, Collections.singletonList(senderId)); + webhookClient.sendText(sessionWebhook, helpContent, Collections.singletonList(senderId), null); bizLogger.info("通过Webhook回复帮助信息给用户: {}", senderId); } else { @@ -356,36 +361,31 @@ public class CallbackController { } } + @Autowired + private com.ruoyi.common.dingding.token.AccessTokenManager accessTokenManager; // 注入 AccessTokenManager + /** * 根据回调中的 robotCode 获取对应的 AccessToken */ private String getAccessToken(JSONObject eventJson) { - String currentRobotCode = eventJson.getString("robotCode"); - String currentAppKey = dingTalkProperties.getAppKey(); - String currentAppSecret = dingTalkProperties.getAppSecret(); - - // 判定是否为安全库存机器人 - boolean isSafetyRobot = false; - String safetyKey = dingTalkProperties.getSafetyStockAppKey(); - if (currentRobotCode != null) { - if (currentRobotCode.equals(SAFETY_STOCK_ROBOT_CODE)) { - isSafetyRobot = true; - } else if (currentRobotCode.equals(safetyKey)) { - isSafetyRobot = true; + if (eventJson.containsKey("robotCode")) { + String robotCode = eventJson.getString("robotCode"); + // 如果是安全库存机器人 + if (SAFETY_STOCK_ROBOT_CODE.equals(robotCode) || + (dingTalkProperties.getSafetyStockAppKey() != null && dingTalkProperties.getSafetyStockAppKey().equals(robotCode))) { + String appKey = dingTalkProperties.getSafetyStockAppKey(); + String appSecret = dingTalkProperties.getSafetyStockAppSecret(); + // 兜底:如果配置为空,使用常量 + if (appKey == null || appKey.isEmpty()) { + appKey = SAFETY_STOCK_ROBOT_CODE; + } + // 动态获取 Token + bizLogger.info("使用安全库存机器人Token: robotCode={}", robotCode); + return DingUtil.getAccessToken(appKey, appSecret); } } - - if (isSafetyRobot) { - // 优先使用配置的 Key/Secret - if (safetyKey != null && !safetyKey.isEmpty()) { - currentAppKey = safetyKey; - } else { - currentAppKey = SAFETY_STOCK_ROBOT_CODE; - } - currentAppSecret = dingTalkProperties.getSafetyStockAppSecret(); - } - - return DingUtil.getAccessToken(currentAppKey, currentAppSecret); + // 默认情况,使用主应用的 Token + return accessTokenManager.getAccessToken(); } } 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 634ce8c..dc0251b 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,7 +2,7 @@ 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 com.ruoyi.common.dingding.util.DingUtil; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index bedf9ba..7f69b2a 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -29,7 +29,7 @@ server: port: 8033 servlet: # 应用的访问路径 - context-path: / + context-path: /dev-api undertow: # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 max-http-post-size: -1 @@ -301,3 +301,5 @@ dingtalk: project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema callback-route-key: update_route_status2 + notify-user-id: 0754423610937470 + route-approval-id: 01201038164023402380 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 64b8cf0..6e6eae2 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 @@ -4,11 +4,14 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Primary; + /** * 钉钉配置属性 */ @Data @Component +@Primary @ConfigurationProperties(prefix = "dingtalk") public class DingTalkProperties { /** @@ -57,8 +60,41 @@ public class DingTalkProperties { + /** + * 接收工艺表格个人用户ID + */ + private String notifyUserId; + /** + * 接收工艺审批用户ID + */ + private String routeApprovalId; + /** * 回调路由 Key */ private String callbackRouteKey; + /** + * Webhook相关配置 + */ + private DingTalkProperties.Webhook webhook = new DingTalkProperties.Webhook(); + + @Data + public static class Robot { + /** + * 机器人Code + */ + private String code; + } + + @Data + public static class Webhook { + /** + * 默认Token + */ + private String token; + /** + * 默认Secret + */ + private String secret; + } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/card/CardService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/card/CardService.java new file mode 100644 index 0000000..5f71684 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/card/CardService.java @@ -0,0 +1,132 @@ +package com.ruoyi.common.dingding.card; + +import com.aliyun.dingtalkcard_1_0.models.UpdateCardHeaders; +import com.aliyun.dingtalkcard_1_0.models.UpdateCardRequest; +import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest; +import com.aliyun.teaopenapi.models.Config; +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.exception.DingException; +import com.ruoyi.common.dingding.token.AccessTokenManager; +import com.ruoyi.common.dingding.util.DingUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import com.aliyun.dingtalkcard_1_0.Client; +import java.util.Map; + +/** + * 互动卡片服务 + * + * @author tzy + */ +@Service +public class CardService { + + private static final Logger log = LoggerFactory.getLogger(CardService.class); + + private final AccessTokenManager tokenManager; + private final DingTalkProperties dingTalkProperties; + public CardService(AccessTokenManager tokenManager, DingTalkProperties dingTalkProperties) { + this.tokenManager = tokenManager; + this.dingTalkProperties = dingTalkProperties; + } + + /** + * 使用 Token 初始化账号Client (Card) + * @return Client + * @throws Exception + */ + public static Client createCardClient() throws Exception { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + return new Client(config); + } + + /** + * 创建并投放互动卡片 (IM 1.0) + * + * @param templateId 卡片模板ID (Standard Card ID) + * @param openConversationId 投放的目标会话ID + * @param outTrackId 唯一跟踪ID (业务侧生成) + * @param cardDataMap 卡片初始数据 + * @param callbackRouteKey 回调路由Key (可选) + * @return outTrackId (成功时返回) + */ + public String createAndDeliverCard(String templateId, String openConversationId, String outTrackId, Map cardDataMap, String callbackRouteKey) { + try { + com.aliyun.dingtalkim_1_0.Client client = createImClient(); + com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders headers = new com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions cardOptions = new com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions() + .setSupportForward(true); + + com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest.SendInteractiveCardRequestCardData cardData = new com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest.SendInteractiveCardRequestCardData() + .setCardParamMap(cardDataMap); + + com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest request = new com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest() + .setCardTemplateId(templateId) + .setOpenConversationId(openConversationId) + .setConversationType(1) + .setOutTrackId(outTrackId) + .setRobotCode(dingTalkProperties.getRobotCode()) + .setCallbackRouteKey(callbackRouteKey) + .setCardData(cardData) + .setUserIdType(1) + .setPullStrategy(false) + .setCardOptions(cardOptions); + + // 发送卡片 + client.sendInteractiveCardWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("互动卡片发送成功, outTrackId: {}", outTrackId); + return outTrackId; + + } catch (Exception e) { + log.error("互动卡片发送失败", e); + throw new DingException("互动卡片发送失败", e); + } + } + + /** + * 创建 IM 1.0 Client + */ + private static com.aliyun.dingtalkim_1_0.Client createImClient() throws Exception { + return DingUtil.createImClient(); + } + + /** + * 更新互动卡片 (IM 1.0) + * + * @param accessToken AccessToken + * @param outTrackId 外部跟踪 ID (创建卡片时生成的唯一ID) + * @param cardDataMap 需要更新的卡片数据 (key-value) + */ + public void updateInteractiveCard(String accessToken, String outTrackId, Map cardDataMap) { + try { + Client client = createCardClient(); + + UpdateCardHeaders updateCardHeaders = new UpdateCardHeaders(); + updateCardHeaders.xAcsDingtalkAccessToken = accessToken; + + UpdateCardRequest.UpdateCardRequestCardData cardData = new UpdateCardRequest.UpdateCardRequestCardData() + .setCardParamMap(cardDataMap); + + UpdateCardRequest.UpdateCardRequestCardUpdateOptions cardUpdateOptions = new UpdateCardRequest.UpdateCardRequestCardUpdateOptions() + .setUpdateCardDataByKey(true) + .setUpdatePrivateDataByKey(false); + + UpdateCardRequest updateCardRequest = new UpdateCardRequest() + .setOutTrackId(outTrackId) + .setCardData(cardData) + .setCardUpdateOptions(cardUpdateOptions); + + client.updateCardWithOptions(updateCardRequest, updateCardHeaders, new com.aliyun.teautil.models.RuntimeOptions()); + + } catch (Exception e) { + e.printStackTrace(); + throw new DingException("更新钉钉互动卡片失败", e); + } + } +} + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/config/DingAutoConfiguration.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/config/DingAutoConfiguration.java new file mode 100644 index 0000000..77643b5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/config/DingAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.dingding.config; + +import com.ruoyi.common.config.DingTalkProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 钉钉自动配置 + * + * @author tzy + */ +@Configuration +@EnableConfigurationProperties(DingTalkProperties.class) +@ComponentScan(basePackages = "com.ruoyi.common.dingding") +public class DingAutoConfiguration { + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/corp/CorpMessageService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/corp/CorpMessageService.java new file mode 100644 index 0000000..a95ebf4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/corp/CorpMessageService.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.dingding.corp; + +import com.aliyun.dingtalkrobot_1_0.Client; +import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOHeaders; +import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTORequest; +import com.aliyun.teaopenapi.models.Config; +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.exception.DingException; +import com.ruoyi.common.dingding.token.AccessTokenManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 工作通知服务 + * + * @author tzy + */ +@Service +public class CorpMessageService { + + private static final Logger log = LoggerFactory.getLogger(CorpMessageService.class); + + private final AccessTokenManager tokenManager; + private final DingTalkProperties dingTalkProperties; + + public CorpMessageService(AccessTokenManager tokenManager, DingTalkProperties dingTalkProperties) { + this.tokenManager = tokenManager; + this.dingTalkProperties = dingTalkProperties; + } + + /** + * 发送工作通知 (批量单发) + * + * @param userIds 用户ID列表 + * @param content 消息内容 + * @return 任务ID + */ + public String sendText(List userIds, String content) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(dingTalkProperties.getRobotCode()) + .setUserIds(userIds) + .setMsgKey("sampleText") + .setMsgParam("{\"content\":\"" + content + "\"}"); + + client.batchSendOTOWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("工作通知发送成功, userIds: {}", userIds); + return "success"; + } catch (Exception e) { + log.error("工作通知发送失败", e); + throw new DingException("工作通知发送失败", e); + } + } + + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/exception/DingException.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/exception/DingException.java new file mode 100644 index 0000000..3823aad --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/exception/DingException.java @@ -0,0 +1,29 @@ +package com.ruoyi.common.dingding.exception; + +/** + * 钉钉业务异常 + * + * @author tzy + */ +public class DingException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private String code; + + public DingException(String message) { + super(message); + } + + public DingException(String code, String message) { + super(message); + this.code = code; + } + + public DingException(String message, Throwable cause) { + super(message, cause); + } + + public String getCode() { + return code; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/im/ImService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/im/ImService.java new file mode 100644 index 0000000..1b94025 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/im/ImService.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.dingding.im; + +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.token.AccessTokenManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * IM 2.0 服务 + * + * @author tzy + */ +@Service +public class ImService { + + private static final Logger log = LoggerFactory.getLogger(ImService.class); + + private final AccessTokenManager tokenManager; + private final DingTalkProperties dingTalkProperties; + + public ImService(AccessTokenManager tokenManager, DingTalkProperties dingTalkProperties) { + this.tokenManager = tokenManager; + this.dingTalkProperties = dingTalkProperties; + } + + /** + * 发送IM消息 (示例) + */ + public void sendMessage() { + // TODO: 实现IM 2.0发送逻辑 + log.info("发送IM消息"); + } + + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/media/MediaService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/media/MediaService.java new file mode 100644 index 0000000..c5f4a86 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/media/MediaService.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.dingding.media; + +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiMediaUploadRequest; +import com.dingtalk.api.response.OapiMediaUploadResponse; +import com.ruoyi.common.dingding.exception.DingException; +import com.ruoyi.common.dingding.token.AccessTokenManager; +import com.taobao.api.FileItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * 媒体文件服务 + * + * @author tzy + */ +@Service +public class MediaService { + + private static final Logger log = LoggerFactory.getLogger(MediaService.class); + + private final AccessTokenManager tokenManager; + + public MediaService(AccessTokenManager tokenManager) { + this.tokenManager = tokenManager; + } + + /** + * 上传媒体文件 (旧版API) + * + * @param type 媒体类型: image, voice, file, video + * @param filePath 本地文件路径 + * @return mediaId + */ + public String uploadMedia(String type, String filePath) { + try { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/media/upload"); + OapiMediaUploadRequest req = new OapiMediaUploadRequest(); + req.setType(type); + req.setMedia(new FileItem(filePath)); + + String accessToken = tokenManager.getAccessToken(); + OapiMediaUploadResponse rsp = client.execute(req, accessToken); + + if (rsp.isSuccess()) { + log.info("媒体文件上传成功, type: {}, mediaId: {}", type, rsp.getMediaId()); + return rsp.getMediaId(); + } else { + throw new DingException("媒体文件上传失败: " + rsp.getErrmsg()); + } + } catch (Exception e) { + log.error("媒体文件上传异常", e); + throw new DingException("媒体文件上传异常", e); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/robot/RobotGroupService.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/robot/RobotGroupService.java new file mode 100644 index 0000000..059bb3b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/robot/RobotGroupService.java @@ -0,0 +1,223 @@ +package com.ruoyi.common.dingding.robot; + +import com.aliyun.dingtalkrobot_1_0.Client; +import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTOHeaders; +import com.aliyun.dingtalkrobot_1_0.models.BatchSendOTORequest; +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders; +import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendRequest; +import com.aliyun.teaopenapi.models.Config; +import com.aliyun.teautil.models.RuntimeOptions; +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.exception.DingException; +import com.ruoyi.common.dingding.token.AccessTokenManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 企业机器人服务 (OpenAPI) + * + * @author tzy + */ +@Service +public class RobotGroupService { + + private static final Logger log = LoggerFactory.getLogger(RobotGroupService.class); + + private final AccessTokenManager tokenManager; + private final DingTalkProperties properties; + + public RobotGroupService(AccessTokenManager tokenManager, DingTalkProperties dingTalkProperties) { + this.tokenManager = tokenManager; + this.properties = dingTalkProperties; + } + + /** + * 发送Markdown消息到群 + * + * @param openConversationId 群会话ID + * @param title 标题 + * @param text Markdown内容 + * @return success + */ + public String sendMarkdown(String openConversationId, String title, String text) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + OrgGroupSendHeaders headers = new OrgGroupSendHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + // 构造 Markdown 消息参数 JSON + // 格式参考钉钉官方文档: {"title": "标题", "text": "内容"} + String msgParam = String.format("{\"title\":\"%s\",\"text\":\"%s\"}", + title.replace("\"", "\\\""), + text.replace("\"", "\\\"").replace("\n", "\\n")); + + OrgGroupSendRequest request = new OrgGroupSendRequest() + .setRobotCode(properties.getRobotCode()) + .setOpenConversationId(openConversationId) + .setMsgKey("sampleMarkdown") // 对应Markdown消息的 msgKey + .setMsgParam(msgParam); + + client.orgGroupSendWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人Markdown消息发送成功, conversationId: {}, title: {}", openConversationId, title); + return "success"; + } catch (Exception e) { + log.error("机器人Markdown消息发送失败", e); + throw new DingException("机器人Markdown消息发送失败", e); + } + } + + /** + * 发送文件到群 (需先上传媒体文件获取 mediaId) + * + * @param openConversationId 群会话ID + * @param mediaId 媒体文件ID + * @param fileName 文件名 + * @return success + */ + public String sendFile(String openConversationId, String mediaId, String fileName) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + OrgGroupSendHeaders headers = new OrgGroupSendHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + // 构造 File 消息参数 JSON + String msgParam = String.format("{\"mediaId\":\"%s\",\"fileName\":\"%s\"}", + mediaId, fileName); + + OrgGroupSendRequest request = new OrgGroupSendRequest() + .setRobotCode(properties.getRobotCode()) + .setOpenConversationId(openConversationId) + .setMsgKey("sampleFile") // 对应文件消息的 msgKey + .setMsgParam(msgParam); + + client.orgGroupSendWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人文件发送成功, conversationId: {}, fileName: {}", openConversationId, fileName); + return "success"; + } catch (Exception e) { + log.error("机器人文件发送失败", e); + throw new DingException("机器人文件发送失败", e); + } + } + + /** + * 发送Markdown消息给个人 (批量单聊) + * + * @param userId 接收人userId + * @param title 标题 + * @param text Markdown内容 + * @return success + */ + public String sendMarkdownToUser(String userId, String title, String text) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"title\":\"%s\",\"text\":\"%s\"}", + title.replace("\"", "\\\""), + text.replace("\"", "\\\"").replace("\n", "\\n")); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Collections.singletonList(userId)) + .setMsgKey("sampleMarkdown") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new RuntimeOptions()); + log.info("机器人个人Markdown消息发送成功, userId: {}, title: {}", userId, title); + return "success"; + } catch (Exception e) { + log.error("机器人个人Markdown消息发送失败", e); + throw new DingException("机器人个人Markdown消息发送失败", e); + } + } + + /** + * 发送文件给个人 (批量单聊) + * + * @param userId 接收人userId + * @param mediaId 媒体文件ID + * @param fileName 文件名 + * @return success + */ + public String sendFileToUser(String userId, String mediaId, String fileName) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"mediaId\":\"%s\",\"fileName\":\"%s\"}", + mediaId, fileName); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Arrays.asList(userId)) + .setMsgKey("sampleFile") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人个人文件发送成功, userId: {}, fileName: {}", userId, fileName); + return "success"; + } catch (Exception e) { + log.error("机器人个人文件发送失败", e); + throw new DingException("机器人个人文件发送失败", e); + } + } + + /** + * 发送文本消息到群 + * + * @param openConversationId 群会话ID + * @param content 消息内容 + * @param atUserIds @的用户ID列表 + * @return processQueryKey 消息唯一标识 + */ + public String sendText(String openConversationId, String content, List atUserIds) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + OrgGroupSendHeaders headers = new OrgGroupSendHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + OrgGroupSendRequest request = new OrgGroupSendRequest() + .setRobotCode(properties.getRobotCode()) + .setOpenConversationId(openConversationId) + .setMsgKey("sampleText") // 对应文本消息的 msgKey + .setMsgParam("{\"content\":\"" + content + "\"}"); + + // 注意: atUserIds 在sampleText模板中可能需要特殊处理,或者使用自定义模板 + // 这里仅作示例,实际使用可能需要更复杂的JSON构建 + + client.orgGroupSendWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人消息发送成功, conversationId: {}", openConversationId); + return "success"; + } catch (Exception e) { + log.error("机器人消息发送失败", e); + throw new DingException("机器人消息发送失败", e); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/token/AccessTokenManager.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/token/AccessTokenManager.java new file mode 100644 index 0000000..7f71d37 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/token/AccessTokenManager.java @@ -0,0 +1,71 @@ +package com.ruoyi.common.dingding.token; + +import com.aliyun.dingtalkoauth2_1_0.Client; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse; +import com.aliyun.teaopenapi.models.Config; +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.exception.DingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * AccessToken管理器 + * 支持自动刷新和线程安全 + * + * @author ruoyi + */ +@Component +public class AccessTokenManager { + + private static final Logger log = LoggerFactory.getLogger(AccessTokenManager.class); + + private final DingTalkProperties properties; + private volatile String accessToken; + private volatile long expireTime; + + public AccessTokenManager(DingTalkProperties properties) { + this.properties = properties; + } + + /** + * 获取AccessToken + * + * @return accessToken + */ + public synchronized String getAccessToken() { + if (accessToken == null || System.currentTimeMillis() > expireTime) { + refresh(); + } + return accessToken; + } + + /** + * 刷新AccessToken + */ + private void refresh() { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + GetAccessTokenRequest request = new GetAccessTokenRequest() + .setAppKey(properties.getAppKey()) + .setAppSecret(properties.getAppSecret()); + + GetAccessTokenResponse response = client.getAccessToken(request); + if (response.getBody() != null) { + this.accessToken = response.getBody().getAccessToken(); + // 设置过期时间,提前100秒刷新 + this.expireTime = System.currentTimeMillis() + (response.getBody().getExpireIn() * 1000) - 100000; + log.info("钉钉AccessToken刷新成功,过期时间: {}", this.expireTime); + } else { + throw new DingException("刷新AccessToken失败,返回为空"); + } + } catch (Exception e) { + log.error("刷新钉钉AccessToken失败", e); + throw new DingException("刷新钉钉AccessToken失败: " + e.getMessage(), e); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java similarity index 99% rename from ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java rename to ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java index 8942706..2f730cd 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/util/DingUtil.java @@ -1,4 +1,4 @@ -package com.ruoyi.common.dingding; +package com.ruoyi.common.dingding.util; import com.aliyun.dingtalkcard_1_0.models.*; import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders; @@ -18,6 +18,7 @@ import com.dingtalk.api.response.OapiRobotSendResponse; import com.dingtalk.api.response.OapiV2UserGetResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.dingding.DingChatCreateResult; import org.apache.commons.codec.binary.Base64; import javax.crypto.Mac; @@ -28,6 +29,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; + public class DingUtil { public static final String CUSTOM_ROBOT_TOKEN = "1f3711d72605f77e8ba8e3e3fe0d98989361a7bfb9c30b51a23520eeb783652e"; @@ -643,6 +645,7 @@ public class DingUtil { */ public static String createAndDeliverCard(String accessToken, String cardTemplateId, String robotCode, String openConversationId, String outTrackId, Map cardDataMap, String callbackRouteKey) { + // 修改为 1 (群聊),原本为 0 (单聊) 导致接口报错 return createAndDeliverCardInternal(accessToken, cardTemplateId, robotCode, 1, openConversationId, null, outTrackId, cardDataMap, callbackRouteKey); } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/webhook/WebhookClient.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/webhook/WebhookClient.java new file mode 100644 index 0000000..e30c42f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/webhook/WebhookClient.java @@ -0,0 +1,69 @@ +package com.ruoyi.common.dingding.webhook; + +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.ruoyi.common.config.DingTalkProperties; +import com.ruoyi.common.dingding.exception.DingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import com.ruoyi.common.utils.StringUtils; + +import java.util.List; + +/** + * Webhook客户端(群机器人) + * + * @author ruoyi + */ +@Service +public class WebhookClient { + + private static final Logger log = LoggerFactory.getLogger(WebhookClient.class); + private final DingTalkProperties properties; + + public WebhookClient(DingTalkProperties properties) { + this.properties = properties; + } + + /** + * 发送文本消息 + * + * @param webhookUrl Webhook URL (如果为空则使用配置中的默认值) + * @param content 消息内容 + * @param atUserIds @的用户ID列表 + * @param atMobiles @的手机号列表 + */ + public void sendText(String webhookUrl, String content, List atUserIds, List atMobiles) { + String url = StringUtils.isEmpty(webhookUrl) ? + "https://oapi.dingtalk.com/robot/send?access_token=" + properties.getWebhook().getToken() : + webhookUrl; + + try { + DingTalkClient client = new DefaultDingTalkClient(url); + 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 (atMobiles != null && !atMobiles.isEmpty()) { + at.setAtMobiles(atMobiles); + } + if ((atUserIds != null && !atUserIds.isEmpty()) || (atMobiles != null && !atMobiles.isEmpty())) { + at.setIsAtAll(false); + } + req.setAt(at); + client.execute(req); + log.info("Webhook消息发送成功: {}", content); + } catch (Exception e) { + log.error("Webhook消息发送失败", e); + throw new DingException("Webhook消息发送失败", e); + } + } +} 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 dd61af6..8c1d7a0 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 @@ -520,6 +520,104 @@ public class FtpUtil { } } + /** + * 下载单个文件 + * + * @param ftpHost FTP服务器IP + * @param ftpUserName FTP用户名 + * @param ftpPassword FTP密码 + * @param ftpPort FTP端口 + * @param remotePath 远程文件完整路径 (包含文件名) + * @param localPath 本地保存完整路径 (包含文件名) + * @return 是否成功 + */ + public static boolean downloadSingleFile(String ftpHost, String ftpUserName, String ftpPassword, int ftpPort, + String remotePath, String localPath) { + FTPClient ftpClient = null; + try { + ftpClient = getFTPClient(ftpHost, ftpUserName, ftpPassword, ftpPort); + if (ftpClient == null || !ftpClient.isConnected()) { + log.error("FTP连接失败"); + return false; + } + + // 默认优先使用GBK,适配大多数Windows FTP服务器 + ftpClient.setControlEncoding("GBK"); + ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); + ftpClient.enterLocalPassiveMode(); + + File localFile = new File(localPath); + File parentDir = localFile.getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + + try (OutputStream os = new FileOutputStream(localFile)) { + // 尝试直接下载 (GBK) + boolean success = ftpClient.retrieveFile(remotePath, os); + + if (!success) { + // 如果GBK失败,尝试开启UTF-8重试 + if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) { + ftpClient.setControlEncoding("UTF-8"); + // UTF-8下无需手动转码,直接使用原路径 + success = ftpClient.retrieveFile(remotePath, os); + } + } + + // 如果还失败,尝试 ISO-8859-1 (兼容某些特殊配置) + if (!success) { + String isoPath = new String(remotePath.getBytes("GBK"), "ISO-8859-1"); + success = ftpClient.retrieveFile(isoPath, os); + } + + if (success) { + log.info("文件下载成功: {}", remotePath); + return true; + } else { + log.error("文件下载失败: {} - 响应: {}", remotePath, ftpClient.getReplyString()); + // 尝试切换目录再下载文件名 + // 有些FTP服务器不支持直接 retrieveFile("/path/to/file") + int lastSlash = remotePath.lastIndexOf("/"); + if (lastSlash >= 0) { + String dir = remotePath.substring(0, lastSlash); + String name = remotePath.substring(lastSlash + 1); + + if (ftpClient.changeWorkingDirectory(dir) || + ftpClient.changeWorkingDirectory(new String(dir.getBytes("GBK"), "ISO-8859-1"))) { + + success = ftpClient.retrieveFile(name, os); + if (!success) { + success = ftpClient.retrieveFile(new String(name.getBytes("GBK"), "ISO-8859-1"), os); + } + } + } + + if (success) return true; + } + } + + // 如果失败,删除空文件 + if (localFile.exists() && localFile.length() == 0) { + localFile.delete(); + } + return false; + + } catch (Exception e) { + log.error("FTP下载异常: {}", remotePath, e); + return false; + } finally { + if (ftpClient != null && ftpClient.isConnected()) { + try { + ftpClient.logout(); + ftpClient.disconnect(); + } catch (IOException e) { + // ignore + } + } + } + } + /** * 列出远程目录中的文件 * @@ -642,12 +740,12 @@ public class FtpUtil { if (fileName.startsWith(".") || file.isDirectory()) { continue; } - + // 过滤文件名 if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) { continue; } - + totalCount++; // 计入待下载总数 try { @@ -659,7 +757,7 @@ 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"); diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java index 9a185d9..3f6aabb 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -53,6 +53,18 @@ public class ExcelUtil { } + /** + * 同步导入(适用于小数据量) + * + * @param is 输入流 + * @param clazz 对象类型 + * @param charset 字符集 + * @return 转换后集合 + */ + public static List importExcel(InputStream is, Class clazz, String charset) { + return EasyExcel.read(is).head(clazz).charset(java.nio.charset.Charset.forName(charset)).autoCloseStream(false).sheet().doReadSync(); + } + /** * 使用校验监听器 异步导入 同步返回 * 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 403376d..59d037b 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 @@ -26,7 +26,7 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.ProcessRoute; import com.ruoyi.system.domain.dto.*; import com.ruoyi.common.config.DingTalkProperties; -import com.ruoyi.common.dingding.DingUtil; +import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.utils.JdUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.domain.BomDetails; diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/FigureSaveController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/FigureSaveController.java index 20e9ded..23b0be8 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/FigureSaveController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/FigureSaveController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; import java.util.Arrays; import java.util.List; @@ -87,13 +88,13 @@ public class FigureSaveController extends BaseController { @SaCheckPermission("system:save:edit") @Log(title = "外购件临时", businessType = BusinessType.UPDATE) @RepeatSubmit() - @PutMapping() - public R edit(@RequestBody @Validated FigureSaveBo bo) { - if (bo.getId() == null) { - return toAjax(iFigureSaveService.insertByBo(bo)); - } - return toAjax(iFigureSaveService.updateByBo(bo)); - } + @PutMapping() + public R edit(@RequestBody @Validated FigureSaveBo bo) { + if (bo.getId() == null) { + return toAjax(iFigureSaveService.insertByBo(bo)); + } + return toAjax(iFigureSaveService.updateByBo(bo)); + } /** * 删除外购件临时 @@ -107,4 +108,25 @@ public class FigureSaveController extends BaseController { @PathVariable Long[] ids) { return toAjax(iFigureSaveService.deleteWithValidByIds(Arrays.asList(ids), true)); } + + /** + * 导入产品 + */ + @Log(title = "导入产品", businessType = BusinessType.IMPORT) + @SaCheckPermission("system:save:import") + @PostMapping("/importData") + public R importData(MultipartFile file, boolean updateSupport) throws Exception { + String filename = file.getOriginalFilename(); + List list; + // 如果是CSV文件,尝试使用GBK编码读取(兼容Windows下生成的CSV) + if (filename != null && (filename.endsWith(".csv") || filename.endsWith(".CSV"))) { + list = ExcelUtil.importExcel(file.getInputStream(), FigureSaveVo.class, "GBK"); + } else { + // list = ExcelUtil.importExcel(file.getInputStream(), FigureSaveVo.class); + list = ExcelUtil.importExcelSheet1(file.getInputStream(), FigureSaveVo.class, true).getList(); + } + String operName = getLoginUser().getUsername(); + String message = iFigureSaveService.importData(list, updateSupport, operName); + return R.ok(message); + } } 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 c891b11..4b6e336 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 @@ -21,7 +21,7 @@ import com.google.gson.JsonObject; import com.kingdee.bos.webapi.sdk.K3CloudApi; import com.ruoyi.common.utils.HttpRequestUtil; import com.ruoyi.common.utils.WxRobotUtil; -import com.ruoyi.common.dingding.DingUtil; +import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.poi.ExcelTemplateProc; import com.ruoyi.common.poi.DynamicDataMapping; @@ -775,7 +775,7 @@ public class KingdeeWorkCenterDataController extends BaseController { .append("- 预计可用量: ").append(String.format("%.2f", safetyStock.getCurrentStock())).append("\n") .append("- 即时库存: ").append(String.format("%.2f", safetyStock.getSecAvbqty())).append("\n") .append("- 最大库存: ").append(String.format("%.2f", safetyStock.getMaxsafetyStock())).append("\n") - .append("- 创建时间: ").append(DateUtil.formatDateTime(safetyStock.getCreateTime())).append("\n") + .append("- 创建时间: ").append(DateUtil.formatDateTime(new Date())).append("\n") .append("---\n"); // 添加分隔线 } // 添加总数汇总 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 dacd3ea..6b8b720 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 @@ -56,6 +56,7 @@ import com.ruoyi.system.domain.bo.ProcessOrderProBo; import com.ruoyi.common.core.page.TableDataInfo; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import shade.com.alibaba.fastjson2.JSONObject; /** * 生产项目 @@ -143,6 +144,18 @@ public class ProcessOrderProController extends BaseController { return toAjax(iProcessOrderProService.updateByBo(bo)); } + /** + * 清空项目关联数据(保留项目) + * + * @param ids 主键串 + */ + @SaCheckPermission("system:orderPro:clear") + @Log(title = "清空项目数据", businessType = BusinessType.CLEAN) + @DeleteMapping("/clear/{ids}") + public R clear(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return iProcessOrderProService.clearProjectData(Arrays.asList(ids)); + } + /** * 删除项目令号 * @@ -152,10 +165,20 @@ public class ProcessOrderProController extends BaseController { @Log(title = "项目令号", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { - return iProcessOrderProService.deleteWithValidByIds(Arrays.asList(ids), true); } + /** + * 推送钉钉卡片 + */ + @SaCheckPermission("system:orderPro:pushDingTalkCard") + @Log(title = "推送钉钉卡片", businessType = BusinessType.UPDATE) + @PostMapping("/pushDingTalkCard/{id}") + public R pushDingTalkCard(@PathVariable Long id) { + iProcessOrderProService.pushDingTalkCard(id); + return R.ok(); + } + @SaCheckPermission("system:orderPro:getExcel") @Log(title = "获取项目生产数据表", businessType = BusinessType.INSERT) @RepeatSubmit() @@ -371,6 +394,16 @@ public class ProcessOrderProController extends BaseController { return iProcessOrderProService.getOverdueProjects(); } + /** + * 刷新并计算项目延期状态 + */ + @SaCheckPermission("system:orderPro:list") + @GetMapping("/refreshDelayStatus") + public R refreshDelayStatus() { + iProcessOrderProService.refreshDelayStatus(); + return R.ok(); + } + @SaCheckPermission("system:orderPro:geMRPResults") @Log(title = "获取MRP复核结果", businessType = BusinessType.OTHER) @PostMapping("/getMRPResults/{id}") @@ -762,9 +795,7 @@ public class ProcessOrderProController extends BaseController { map.put("processDescription", route.getProcessDescription()); map.put("productionOrderNo", orderPro.getProductionOrderNo()); map.put("productionName", orderPro.getProductionName()); - map.put("drawingNo", orderPro.getDrawingNo()); - map.put("mainProducts", orderPro.getDrawingName()); - map.put("mainProductsName", orderPro.getDrawingName()); + mapList.add(map); } return mapList; @@ -2136,4 +2167,53 @@ public class ProcessOrderProController extends BaseController { } } + + + + @SaCheckPermission("system:orderPro:downloadZip") + @Log(title = "下载项目DWG", businessType = BusinessType.OTHER) + @GetMapping("/downloadDwg/{id}") + public void downloadDwg(@PathVariable("id") List ids, HttpServletResponse response) { + try { + R result = iProcessOrderProService.downloadDwg(ids); + if (result.getCode() == 200) { + String filePath = result.getData(); + File downloadFile = new File(filePath); + + if (downloadFile.exists()) { + // 设置响应头 + response.setContentType("application/octet-stream"); + response.setCharacterEncoding("utf-8"); + String fileName = URLEncoder.encode(downloadFile.getName(), "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName); + + // 写入响应流 + try (FileInputStream fis = new FileInputStream(downloadFile); + OutputStream os = response.getOutputStream()) { + byte[] buffer = new byte[8192]; + int length; + while ((length = fis.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + os.flush(); + } + // 删除临时文件 + downloadFile.delete(); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件生成失败"); + } + } else { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write(JSONObject.toJSONString(result)); + } + } catch (Exception e) { + log.error("下载DWG失败", e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "下载失败: " + e.getMessage()); + } catch (IOException ex) { + log.error("发送错误响应失败", ex); + } + } + } + } 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 ab7bd7a..5ba1299 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 @@ -16,7 +16,7 @@ import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.validate.AddGroup; import com.ruoyi.common.core.validate.EditGroup; -import com.ruoyi.common.dingding.DingUtil; +import com.ruoyi.common.dingding.util.DingUtil; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.excel.DefaultExcelListener; import com.ruoyi.common.excel.ExcelResult; @@ -439,12 +439,22 @@ public class ProcessRouteController extends BaseController { cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批")); String outTrackId = "audit-card-" + System.currentTimeMillis(); - String outTrackId1 = DingUtil.createAndDeliverCard(accessToken, dingTalkProperties.getRouteCardTemplateId(), dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey()); + // 调用发送给工艺负责人的方法 createAndDeliverCardToUser + DingUtil.createAndDeliverCardToUser( + accessToken, + dingTalkProperties.getRouteCardTemplateId(), + Collections.singletonList(dingTalkProperties.getNotifyUserId()), + outTrackId, + cardDataMap, + dingTalkProperties.getCallbackRouteKey() + ); + // 统一存入带 userId 后缀的完整 outTrackId,以便和钉钉回调保持一致 + String fullOutTrackId = outTrackId + "_" + dingTalkProperties.getNotifyUserId(); // 更新数据库中的AuditCardId ProcessOrderPro update = new ProcessOrderPro(); update.setId(order.getId()); update.setUpdateBy("Dingding"); - update.setAuditCardId(outTrackId1); + update.setAuditCardId(fullOutTrackId); update.setBomStatus(2L); // 更新审核状态为1(已审核/提交审核) update.setRouteStatus(0L); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/FigureSave.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/FigureSave.java index a3ed058..07d71bd 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/FigureSave.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/FigureSave.java @@ -3,10 +3,13 @@ package com.ruoyi.system.domain; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.common.core.domain.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.Date; + /** * 项目产品对象 figure_save * @@ -65,4 +68,25 @@ public class FigureSave extends BaseEntity { * 箱体类型 */ private String boxType; + /** + * 文件类型 + */ + private String fileType; + /** + * 出图人 + */ + private String drawBy; + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + /** + * 延期天数 + */ + private Long delayDay; + /** + * 出图时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date drawTime; } 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 62e44aa..fe6c1b0 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 @@ -28,6 +28,17 @@ public class ProcessOrderPro extends BaseEntity { */ @TableId(value = "id", type = IdType.AUTO) private Long id; + + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + + /** + * 延期天数 + */ + private Long delayDay; + /** * 生产令号 */ @@ -36,14 +47,6 @@ public class ProcessOrderPro extends BaseEntity { * 项目名称 */ private String productionName; - /** - * 图号 - */ - private String drawingNo; - /** - * 名称 - */ - private String drawingName; /** * 计划结束时间 @@ -72,11 +75,10 @@ public class ProcessOrderPro extends BaseEntity { /** * 是否企标 (0-否, 1-是) */ - private Integer isEnterpriseStandard; + private String isEnterpriseStandard; /** * 图纸路径 */ - @Translation(type = TransConstant.OSS_ID_TO_URL) private String drawingPath; /** diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/FigureSaveBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/FigureSaveBo.java index 9973f35..d661442 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/FigureSaveBo.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/FigureSaveBo.java @@ -6,6 +6,7 @@ import com.ruoyi.common.core.validate.EditGroup; import lombok.Data; import lombok.EqualsAndHashCode; import javax.validation.constraints.*; +import java.util.Date; /** @@ -19,9 +20,7 @@ import javax.validation.constraints.*; @EqualsAndHashCode(callSuper = true) public class FigureSaveBo extends BaseEntity { - /** - * - */ + private Long id; /** @@ -80,5 +79,33 @@ public class FigureSaveBo extends BaseEntity { */ private String boxType; + /** + * 文件类型 + */ + @NotBlank(message = "文件类型不能为空", groups = { AddGroup.class, EditGroup.class }) + private String fileType; + /** + * 出图人 + */ + @NotBlank(message = "出图人不能为空", groups = { AddGroup.class, EditGroup.class }) + private String drawBy; + + /** + * 是否延期:0否 1是 + */ + @NotNull(message = "是否延期:0否 1是不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer whetherDelay; + + /** + * 延期天数 + */ + @NotNull(message = "延期天数不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long delayDay; + + /** + * 出图时间 + */ + @NotNull(message = "出图时间不能为空", groups = { AddGroup.class, EditGroup.class }) + private Date drawTime; } 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 412e511..a3fe3b1 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 @@ -22,6 +22,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) + public class ProcessOrderProBo extends BaseEntity { /** @@ -39,15 +40,7 @@ public class ProcessOrderProBo extends BaseEntity { */ private String productionName; - /** - * 项目图号 - */ - private String drawingNo; - /** - * 产品名称 - */ - private String drawingName; /** * 计划结束时间 */ @@ -81,7 +74,13 @@ public class ProcessOrderProBo extends BaseEntity { /** * 是否企标 (0-否, 1-是) */ - private Integer isEnterpriseStandard; + private String isEnterpriseStandard; + + /** + * 是否企标 (查询多选使用) + */ + private List isEnterpriseStandardList; + private String drawingPath; /** @@ -151,4 +150,13 @@ public class ProcessOrderProBo extends BaseEntity { * 通知卡片Id */ private String notificationCardId; + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + + /** + * 延期天数 + */ + private Long delayDay; } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/RigidChainModelDTO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/RigidChainModelDTO.java index 584c7b9..5d0aacb 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/RigidChainModelDTO.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/excuteDrawing/RigidChainModelDTO.java @@ -1,7 +1,10 @@ package com.ruoyi.system.domain.dto.excuteDrawing; +import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; +import java.util.Date; + @Data public class RigidChainModelDTO { @@ -28,5 +31,29 @@ public class RigidChainModelDTO { private String figureNumber; private String productName; private String figureName; + /** + * 文件类型 + */ + private String fileType; + + /** + * 出图人 + */ + private String drawBy; + + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + + /** + * 延期天数 + */ + private Long delayDay; + + /** + * 出图时间 + */ + private Date drawTime; } 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 38a4cf7..e0d051e 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 @@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; +import java.util.Date; /** @@ -21,42 +22,41 @@ public class FigureSaveVo { /** * */ - @ExcelProperty(value = "") private Long id; + /** + * 生产令号 + */ + @ExcelProperty(value = "生产令号", index = 0) + private String productionCode; + + /** + * 产品编码 + */ + @ExcelProperty(value = "产品编码", index = 1) + private String figureNumber; /** * 产品名称 */ - @ExcelProperty(value = "产品名称") + @ExcelProperty(value = "产品名称", index = 2) private String figureName; - /** - * 生产令号 - */ - @ExcelProperty(value = "生产令号") - private String productionCode; - - /** - * 产品型号 - */ - @ExcelProperty(value = "产品型号") - private String figureNumber; - /** * 数量 */ - @ExcelProperty(value = "数量") + @ExcelProperty(value = "数量", index = 3) private Long figureNum; /** * 行程 */ - @ExcelProperty(value = "行程") + @ExcelProperty(value = "行程", index = 4) private Long jdInventory; + /** * 图纸路径 */ - @ExcelProperty(value = "图纸路径") + @ExcelProperty(value = "图纸路径", index = 5) private String drawPath; /** * 类型 @@ -67,17 +67,41 @@ public class FigureSaveVo { /** * 关联项目表 */ - @ExcelProperty(value = "关联项目表") private Long pid; /** * 轴向 */ - @ExcelProperty(value = "轴向") private String axialType; /** * 箱体类型 */ - @ExcelProperty(value = "箱体类型") private String boxType; + /** + * 文件类型 + */ + @ExcelProperty(value = "文件类型") + private String fileType; + + /** + * 出图人 + */ + @ExcelProperty(value = "出图人") + private String drawBy; + + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + + /** + * 延期天数 + */ + private Long delayDay; + + /** + * 出图时间 + */ + @ExcelProperty(value = "出图时间") + private Date drawTime; } 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 be86482..d06a21f 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,17 +44,7 @@ public class ProcessOrderProVo { @ExcelProperty(value = "项目名称") private String productionName; - /** - * 图号 - */ - @ExcelProperty(value = "图号") - private String drawingNo; - /** - * 名称 - */ - @ExcelProperty(value = "名称") - private String drawingName; /** * 计划结束时间 */ @@ -94,7 +84,7 @@ public class ProcessOrderProVo { * 是否企标 (0-否, 1-是) */ @ExcelProperty(value = "是否企标 (0-否, 1-是)") - private Integer isEnterpriseStandard; + private String isEnterpriseStandard; /** * bom状态(0,未完成 1,完成 2已上传) */ @@ -114,6 +104,21 @@ public class ProcessOrderProVo { @Translation(mapper = "drawingPath", type = TransConstant.OSS_ID_TO_URL) private String drawingPathUrl; + + /** + * 图纸路径列表(逗号分隔转数组) + */ + public String[] getDrawingPathList() { + // 优先使用 drawingPathUrl (已翻译的 URL) + if (drawingPathUrl != null && !drawingPathUrl.isEmpty()) { + return drawingPathUrl.split(","); + } + // 如果 drawingPathUrl 为空,尝试直接使用 drawingPath (可能是未翻译的 ID 或直接存的 URL) + if (drawingPath != null && !drawingPath.isEmpty()) { + return drawingPath.split(","); + } + return new String[0]; + } private Date createTime; /** @@ -170,4 +175,14 @@ public class ProcessOrderProVo { @ExcelProperty(value = "通知卡片Id") private String notificationCardId; + /** + * 是否延期:0否 1是 + */ + private Integer whetherDelay; + + /** + * 延期天数 + */ + private Long delayDay; + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BomDetailsMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BomDetailsMapper.java index 80eb1b0..f38e509 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BomDetailsMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/BomDetailsMapper.java @@ -3,6 +3,7 @@ package com.ruoyi.system.mapper; import com.ruoyi.system.domain.BomDetails; import com.ruoyi.system.domain.vo.BomDetailsVo; import com.ruoyi.common.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Delete; /** * bom明细Mapper接口 @@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus; * @date 2024-04-08 */ public interface BomDetailsMapper extends BaseMapperPlus { - + @Delete("DELETE FROM bom_details where total_weight = #{productionOrderNo}") + void deleteWithProCode(String productionOrderNo); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/FigureSaveMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/FigureSaveMapper.java index e77802f..accf22e 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/FigureSaveMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/FigureSaveMapper.java @@ -3,6 +3,7 @@ package com.ruoyi.system.mapper; import com.ruoyi.system.domain.FigureSave; import com.ruoyi.system.domain.vo.FigureSaveVo; import com.ruoyi.common.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Delete; /** * 外购件临时Mapper接口 @@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus; * @date 2024-05-28 */ public interface FigureSaveMapper extends BaseMapperPlus { - + @Delete("DELETE FROM figure_save where production_code = #{productionOrderNo}") + void deleteWithProCode(String productionOrderNo); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/MaterialBomMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/MaterialBomMapper.java index 449e7bd..07da6f5 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/MaterialBomMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/MaterialBomMapper.java @@ -3,6 +3,7 @@ package com.ruoyi.system.mapper; import com.ruoyi.system.domain.MaterialBom; import com.ruoyi.system.domain.vo.MaterialBomVo; import com.ruoyi.common.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Delete; /** * bomaterialMapper接口 @@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus; * @date 2024-11-08 */ public interface MaterialBomMapper extends BaseMapperPlus { - + @Delete("DELETE FROM material_bom where project_number = #{productionOrderNo}") + void deleteWithProCode(String productionOrderNo); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProcessRouteMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProcessRouteMapper.java index 866198f..747ceac 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProcessRouteMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ProcessRouteMapper.java @@ -25,7 +25,7 @@ public interface ProcessRouteMapper extends BaseMapperPlus { - + @Delete("DELETE FROM production_order where production_order_no = #{productionOrderNo}") + void deleteWithProCode(String productionOrderNo); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IFigureSaveService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IFigureSaveService.java index c4ac4e5..5a052d5 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IFigureSaveService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IFigureSaveService.java @@ -54,5 +54,10 @@ public interface IFigureSaveService { */ List selectByFid(Long id); - List selectByProCode(String totalWeight); + List selectByProCode(String totalWeight); + + /** + * 导入数据 + */ + String importData(List dataList, boolean updateSupport, String operName); } 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 6df141d..9a6f9a4 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 @@ -56,6 +56,11 @@ public interface IProcessOrderProService { */ Boolean updateByBo(ProcessOrderProBo bo); + /** + * 清空项目关联数据(保留项目本身) + */ + R clearProjectData(Collection ids); + /** * 校验并批量删除项目令号信息 */ @@ -89,4 +94,17 @@ public interface IProcessOrderProService { R getRouteLog(Long id) throws JsonProcessingException; SseEmitter startDrawingSse(Long id); + + R downloadDwg(List ids); + + /** + * 推送钉钉卡片 + * @param id 项目ID + */ + void pushDingTalkCard(Long id); + + /** + * 刷新并计算项目延期状态 + */ + void refreshDelayStatus(); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FigureSaveServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FigureSaveServiceImpl.java index 3f92b35..cd47f54 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FigureSaveServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FigureSaveServiceImpl.java @@ -7,6 +7,9 @@ import com.ruoyi.common.core.domain.PageQuery; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.system.domain.ProcessOrderPro; +import com.ruoyi.system.mapper.ProcessOrderProMapper; +import com.ruoyi.system.service.IProcessOrderProService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import com.ruoyi.system.domain.bo.FigureSaveBo; @@ -31,7 +34,7 @@ import java.util.Collection; public class FigureSaveServiceImpl implements IFigureSaveService { private final FigureSaveMapper baseMapper; - + private final ProcessOrderProMapper processOrderProMapper; /** * 查询外购件临时 */ @@ -69,6 +72,11 @@ public class FigureSaveServiceImpl implements IFigureSaveService { lqw.eq(bo.getJdInventory() != null, FigureSave::getJdInventory, bo.getJdInventory()); lqw.eq(StringUtils.isNotBlank(bo.getDrawPath()), FigureSave::getDrawPath, bo.getDrawPath()); lqw.eq(StringUtils.isNotBlank(bo.getProductionCode()), FigureSave::getProductionCode, bo.getProductionCode()); + lqw.eq(StringUtils.isNotBlank(bo.getFileType()), FigureSave::getFileType, bo.getFileType()); + lqw.eq(StringUtils.isNotBlank(bo.getDrawBy()), FigureSave::getDrawBy, bo.getDrawBy()); + lqw.eq(bo.getWhetherDelay() != null, FigureSave::getWhetherDelay, bo.getWhetherDelay()); + lqw.eq(bo.getDelayDay() != null, FigureSave::getDelayDay, bo.getDelayDay()); + lqw.eq(bo.getDrawTime() != null, FigureSave::getDrawTime, bo.getDrawTime()); return lqw; } @@ -137,4 +145,39 @@ public class FigureSaveServiceImpl implements IFigureSaveService { return baseMapper.selectList(wa); } + + @Override + public String importData(List dataList, boolean updateSupport, String operName) { + if (dataList.isEmpty()) { + throw new RuntimeException("导入数据不能为空!"); + } + + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (FigureSaveVo vo : dataList) { + try { + // 验证是否存在这个用户 + FigureSave figureSave = BeanUtil.toBean(vo, FigureSave.class); + ProcessOrderPro processOrderPro = processOrderProMapper.selectByProjectNumber(figureSave.getProductionCode()); + figureSave.setPid(processOrderPro.getId()); + baseMapper.insert(figureSave); + successNum++; + successMsg.append("
").append(successNum).append("、数据 ").append(vo.getFigureName()).append(" 导入成功"); + } catch (Exception e) { + failureNum++; + String msg = "
" + failureNum + "、数据 " + vo.getFigureName() + " 导入失败:"; + failureMsg.append(msg).append(e.getMessage()); + } + } + if (failureNum > 0) { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + // throw new ServiceException(failureMsg.toString()); + return failureMsg.toString(); + } else { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } } 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 bc2160e..dc92afb 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 @@ -17,7 +17,10 @@ import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.dingding.DingUtil; +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; import com.ruoyi.common.utils.HttpUtils; @@ -91,11 +94,23 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { private final DeviceSpec60rMapper deviceSpec60rMapper; private final DeviceSpec80rMapper deviceSpec80rMapper; private final DeviceSpec100rMapper deviceSpec100rMapper; - + private final IFigureSaveService iFigureSaveService; private final SafetyStockMapper safetyStockMapper; private final IProcessRouteService iProcessRouteService; private final IProductionOrderService iProductionOrderService; private final ISysUserService userService; + private final ProductionOrderMapper productionOrderMapper; + @Autowired + private AccessTokenManager accessTokenManager; + @Autowired + private RobotGroupService robotGroupService; + @Autowired + private com.ruoyi.common.dingding.media.MediaService mediaService; + @Autowired + private com.ruoyi.common.dingding.card.CardService cardService; + @Autowired + private WebhookClient webhookClient; + @Autowired private ProcessRouteMapper processRouteMapper; @Autowired @@ -111,8 +126,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { @Value("${app.drawing.status-url:http://192.168.5.18:9000/getstate}") private String statusUrl; @Autowired - private IFigureSaveService iFigureSaveService; + private IFigureSaveService iFigureSaveSservice; private Map specStrategies; + @Autowired + private BomDetailsMapper bomDetailsMapper; + @Autowired + private MaterialBomMapper materialBomMapper; public static String[] getNullPropertyNames(Object source) { BeanWrapper src = new BeanWrapperImpl(source); @@ -234,11 +253,71 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { return baseMapper.selectVoById(id); } + /** + * 刷新并计算项目延期状态 + */ + @Override + public void refreshDelayStatus() { + // 1. 查询所有未完成的项目(状态为 0) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(ProcessOrderPro::getBomStatus, 0L); + // 确保计划结束时间不为空 + wrapper.isNotNull(ProcessOrderPro::getDrawingTime); + + List list = baseMapper.selectList(wrapper); + if (list == null || list.isEmpty()) { + return; + } + + Date now = new Date(); + List updates = new ArrayList<>(); + + for (ProcessOrderPro vo : list) { + Integer oldWhetherDelay = vo.getWhetherDelay(); + Long oldDelayDay = vo.getDelayDay(); + + // 检查子产品是否有图纸 + boolean hasProductDrawing = false; + List figureSaves = iFigureSaveService.selectByFid(vo.getId()); + if (figureSaves != null && !figureSaves.isEmpty()) { + hasProductDrawing = figureSaves.stream().anyMatch(f -> StringUtils.isNotBlank(f.getDrawPath())); + } + + // 如果项目或产品已经上传了图纸,则认为没有延期 + if (hasProductDrawing) { + vo.setWhetherDelay(0); + vo.setDelayDay(0L); + } else { + // 如果都没有上传图纸,则计算是否超过出图时间 + long diff = now.getTime() - vo.getDrawingTime().getTime(); + if (diff > 0) { + vo.setWhetherDelay(1); + vo.setDelayDay(diff / (1000 * 60 * 60 * 24)); + } else { + vo.setWhetherDelay(0); + vo.setDelayDay(0L); + } + } + // 检查是否发生变化 + if (!Objects.equals(oldWhetherDelay, vo.getWhetherDelay()) || !Objects.equals(oldDelayDay, vo.getDelayDay())) { + updates.add(vo); + } + } + // 批量更新 + if (!updates.isEmpty()) { + baseMapper.updateBatchById(updates); + log.info("已刷新 {} 条项目的延期状态", updates.size()); + } + } + /** * 查询项目令号列表 */ @Override public TableDataInfo queryPageList(ProcessOrderProBo bo, PageQuery pageQuery) { + // 在查询前先刷新数据库中的延期状态 + // refreshDelayStatus(); + LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); return TableDataInfo.build(result); @@ -258,8 +337,6 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProcessOrderPro::getProductionOrderNo, bo.getProductionOrderNo()); lqw.like(StringUtils.isNotBlank(bo.getProductionName()), ProcessOrderPro::getProductionName, bo.getProductionName()); - lqw.eq(StringUtils.isNotBlank(bo.getDrawingNo()), ProcessOrderPro::getDrawingNo, bo.getDrawingNo()); - lqw.like(StringUtils.isNotBlank(bo.getDrawingName()), ProcessOrderPro::getDrawingName, bo.getDrawingName()); lqw.eq(bo.getPlanEndTime() != null, ProcessOrderPro::getPlanEndTime, bo.getPlanEndTime()); lqw.eq(bo.getPlanStartTime() != null, ProcessOrderPro::getPlanStartTime, bo.getPlanStartTime()); lqw.eq(StringUtils.isNotBlank(bo.getDrawingType()), ProcessOrderPro::getDrawingType, bo.getDrawingType()); @@ -268,12 +345,34 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { 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()); + + // isEnterpriseStandardList 多选处理 + List list = bo.getIsEnterpriseStandardList(); + if (list != null && !list.isEmpty()) { + lqw.in(ProcessOrderPro::getIsEnterpriseStandard, list); + } + lqw.eq(bo.getDrawingPath() != null, ProcessOrderPro::getDrawingPath, bo.getDrawingPath()); + lqw.eq(bo.getWhetherDelay() != null, ProcessOrderPro::getWhetherDelay, bo.getWhetherDelay()); //按照创建时间排序,最新的在顶端 lqw.orderByDesc(true, ProcessOrderPro::getCreateTime); return lqw; } + // 项目类型常量定义 + private static final Map PROJECT_TYPE_MAP = new HashMap<>(); + + static { + PROJECT_TYPE_MAP.put("CP", "产品"); + PROJECT_TYPE_MAP.put("FB", "非标"); + PROJECT_TYPE_MAP.put("BG", "变更"); + PROJECT_TYPE_MAP.put("QB", "企标"); + PROJECT_TYPE_MAP.put("EY", "研发"); + PROJECT_TYPE_MAP.put("GZ", "固资"); + PROJECT_TYPE_MAP.put("ZP", "展品"); + PROJECT_TYPE_MAP.put("KC", "库存"); + } + /** * 新增项目令号 */ @@ -292,10 +391,22 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { add.setCreateBy(user.getNickName()); } } - // 如果出图时间为空,默认为当前时间 - if (add.getDrawingTime() == null) { - add.setDrawingTime(new Date()); - bo.setDrawingTime(add.getDrawingTime()); + + // 对项目令号的判断,根据前缀设置DrawingType + String productionOrderNo = bo.getProductionOrderNo(); + if (StringUtils.isNotBlank(productionOrderNo)) { + String isEnterpriseStandard = null; + if (productionOrderNo.toUpperCase().contains("BG")) { + isEnterpriseStandard = PROJECT_TYPE_MAP.get("BG"); + } else { + // 提取前缀 + String prefix = productionOrderNo.length() >= 2 ? productionOrderNo.substring(0, 2).toUpperCase() : ""; + isEnterpriseStandard = PROJECT_TYPE_MAP.get(prefix); + } + if (isEnterpriseStandard != null) { + add.setIsEnterpriseStandard(isEnterpriseStandard); + bo.setIsEnterpriseStandard(isEnterpriseStandard); + } } add.setRouteStatus(0L); add.setBomStatus(0L); @@ -303,92 +414,116 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { boolean flag = baseMapper.insert(add) > 0; if (flag) { bo.setId(add.getId()); - // 异步发送钉钉卡片 - CompletableFuture.runAsync(() -> { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - try { - // 获取 AccessToken - String accessToken = DingUtil.getAccessToken(dingTalkProperties.getAppKey(), dingTalkProperties.getAppSecret()); - // 处理图片上传 - String imgMediaId = ""; // 默认图片 - if (StringUtils.isNotEmpty(bo.getDrawingPath())) { - try { - Long ossId = Long.parseLong(bo.getDrawingPath()); - SysOssVo ossVo = iSysOssService.getById(ossId); - if (ossVo != null) { - String suffix = FileUtil.getSuffix(ossVo.getOriginalName()); - File tempFile = FileUtil.createTempFile("ding_img", StringUtils.isNotEmpty(suffix) ? "." + suffix : ".jpg", true); - try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) { - IoUtil.copy(is, os); - } - imgMediaId = DingUtil.uploadMedia(accessToken, "image", tempFile.getAbsolutePath()); - FileUtil.del(tempFile); - } - } catch (Exception e) { - log.error("上传图片到钉钉失败", e); - } - } - - - // 构造卡片数据 - Map cardDataMap = new HashMap<>(); - - cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定"); - cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), "")); - cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), "")); - cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(bo.getDrawingBy(), "暂无")); - cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传"); - cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), "")); - cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定"); - cardDataMap.put("route_uptime", bo.getRouteUptime() != null ? sdf.format(bo.getRouteUptime()) : "未上传"); - cardDataMap.put("material_count", bo.getMaterialCount() != null ? String.valueOf(bo.getMaterialCount()) : "0"); - cardDataMap.put("route_count", bo.getRouteCount() != null ? String.valueOf(bo.getRouteCount()) : "0"); - cardDataMap.put("plan_uptime", bo.getPlanUptime() != null ? sdf.format(bo.getPlanUptime()) : "未上传"); - cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0"); - cardDataMap.put("img_id", imgMediaId); - String outTrackId = "project-card-" + System.currentTimeMillis(); - String outTrackId1 = DingUtil.createAndDeliverCard(accessToken, dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey()); - //同时发送给设计人员 - List designers = StringUtils.splitList(bo.getDrawingBy()); - // 收集手机号用于@ - List receiverMobiles = new ArrayList<>(); - - for (String designer : designers) { - SysUser sysUser = userService.selectUserByUserName(designer); - if (sysUser != null && StringUtils.isNotEmpty(sysUser.getPhonenumber())) { - receiverMobiles.add(sysUser.getPhonenumber()); - log.info("添加发送消息用户 {}: {}", sysUser.getNickName(), sysUser.getPhonenumber()); - } - } - - if (!receiverMobiles.isEmpty()) { - // 去重 - receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList()); - try { - log.info("发送群消息并@用户(手机号): {}", receiverMobiles); - DingUtil.sendSessionText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450", - bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles); - } catch (Exception e) { - log.error("发送@消息失败", e); - } - } - // 更新数据库中的 auditCardId - ProcessOrderPro update = new ProcessOrderPro(); - update.setId(add.getId()); - update.setUpdateBy("Dingding"); - update.setNotificationCardId(outTrackId1); - log.info("更新项目令号 {} 卡片id {}", add.getProductionOrderNo(), outTrackId1); - baseMapper.updateById(update); - - bo.setAuditCardId(outTrackId1); - } catch (Exception e) { - log.error("发送钉钉卡片异常", e); - } - }); } return flag; } + /** + * 推送钉钉卡片 + * + * @param id 项目ID + */ + @Override + public void pushDingTalkCard(Long id) { + ProcessOrderPro processOrderPro = baseMapper.selectById(id); + List figureSaves = iFigureSaveService.selectByFid(id); + if (processOrderPro == null) { + throw new RuntimeException("项目不存在"); + } + ProcessOrderProBo bo = new ProcessOrderProBo(); + BeanUtils.copyProperties(processOrderPro, bo); + // 收集所有设计人员 + String drawingBy = figureSaves.stream().map(FigureSave::getDrawBy).filter(StringUtils::isNotEmpty).distinct().collect(Collectors.joining(",")); + // 异步发送钉钉卡片 + // CompletableFuture.runAsync(() -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + try { + // 获取 AccessToken + String accessToken = accessTokenManager.getAccessToken(); + // 处理图片上传 + String imgMediaId = ""; // 默认图片 + if (StringUtils.isNotEmpty(bo.getDrawingPath())) { + try { + String[] paths = bo.getDrawingPath().split(","); + Long ossId = Long.parseLong(paths[0]); + SysOssVo ossVo = iSysOssService.getById(ossId); + if (ossVo != null) { + String suffix = FileUtil.getSuffix(ossVo.getOriginalName()); + File tempFile = FileUtil.createTempFile("ding_img", StringUtils.isNotEmpty(suffix) ? "." + suffix : ".jpg", true); + try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) { + IoUtil.copy(is, os); + } + imgMediaId = mediaService.uploadMedia("image", tempFile.getAbsolutePath()); + FileUtil.del(tempFile); + } + } catch (Exception e) { + log.error("上传图片到钉钉失败", e); + } + } + + // 构造卡片数据 + Map cardDataMap = new HashMap<>(); + cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定"); + cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), "")); + cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), "")); + cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(drawingBy, "暂无")); + cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传"); + cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), "")); + cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定"); + cardDataMap.put("route_uptime", bo.getRouteUptime() != null ? sdf.format(bo.getRouteUptime()) : "未上传"); + cardDataMap.put("material_count", bo.getMaterialCount() != null ? String.valueOf(bo.getMaterialCount()) : "0"); + cardDataMap.put("route_count", bo.getRouteCount() != null ? String.valueOf(bo.getRouteCount()) : "0"); + cardDataMap.put("plan_uptime", bo.getPlanUptime() != null ? sdf.format(bo.getPlanUptime()) : "未上传"); + cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0"); + cardDataMap.put("img_id", imgMediaId); + String outTrackId = "project-card-" + System.currentTimeMillis(); + String outTrackId1 = cardService.createAndDeliverCard(dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey()); + //同时发送给设计人员 + List designers = StringUtils.splitList(bo.getDrawingBy()); + // 收集手机号用于@ + List receiverMobiles = new ArrayList<>(); + //拿到用户id,这里将用户id存入email字段 + for (String designer : designers) { + SysUser sysUser = userService.selectUserByUserName(designer); + if (sysUser != null && StringUtils.isNotEmpty(sysUser.getEmail())) { + receiverMobiles.add(sysUser.getEmail()); + log.info("添加发送消息用户 {}: {}", sysUser.getNickName(), sysUser.getEmail()); + } + } + //获取最晚的出图时间 + FigureSave latest = figureSaves.stream().filter(e -> e.getDrawTime() != null).max(Comparator.comparing(FigureSave::getDrawTime)).orElse(null); + + + if (!receiverMobiles.isEmpty()) { + // 去重 + receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList()); + try { + log.info("发送群消息并@用户(手机号): {}", receiverMobiles); + // 使用 WebhookClient 发送消息 + webhookClient.sendText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450", bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles); + } catch (Exception e) { + log.error("发送@消息失败", e); + } + } + // 更新数据库中的 auditCardId + ProcessOrderPro update = new ProcessOrderPro(); + update.setId(processOrderPro.getId()); + update.setDrawingBy(drawingBy); + if (latest != null) { + update.setDrawingTime(latest.getDrawTime()); + } + update.setUpdateBy("Dingding"); + update.setNotificationCardId(outTrackId1); + log.info("更新项目令号 {} 卡片id {}", processOrderPro.getProductionOrderNo(), outTrackId1); + baseMapper.updateById(update); + + bo.setAuditCardId(outTrackId1); + } catch (Exception e) { + log.error("发送钉钉卡片异常", e); + } + // }); + } + /** * 修改项目令号 */ @@ -413,6 +548,24 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } } + @Override + public R clearProjectData(Collection ids) { + for (Long id : ids) { + ProcessOrderProVo processOrderProVo = baseMapper.selectVoById(id); + if (processOrderProVo == null) { + log.warn("未找到对应的 ProcessOrderProVo,ID: {}", id); + continue; + } + String productionOrderNo = processOrderProVo.getProductionOrderNo(); + // 清空关联表数据 + processRouteMapper.deleteByProCode(productionOrderNo); + productionOrderMapper.deleteWithProCode(productionOrderNo); + materialBomMapper.deleteWithProCode(productionOrderNo); + bomDetailsMapper.deleteWithProCode(productionOrderNo); + } + return R.ok(); + } + /** * 批量删除项目令号 */ @@ -425,7 +578,6 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { continue; } String productionOrderNo = processOrderProVo.getProductionOrderNo(); - // 调用 selectByProjectCode 方法并检查结果 return iProcessRouteService.delByProjectCode(productionOrderNo); } @@ -443,7 +595,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { log.error("删除 ID 列表失败: {}", ids); } - return R.fail(); + return R.ok(); } @Override @@ -564,6 +716,11 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { figureSave.setFigureName(orderPro.getFigureName()); figureSave.setAxialType(axial); figureSave.setBoxType(box); + figureSave.setFileType(orderPro.getFileType()); + figureSave.setDrawBy(orderPro.getDrawBy()); + figureSave.setDelayDay(orderPro.getDelayDay()); + figureSave.setDrawTime(orderPro.getDrawTime()); + try { figureSave.setJdInventory(Long.valueOf(journey)); } catch (Exception ignore) { @@ -620,12 +777,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { ProcessOrderPro po = processOrderPro; SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd"); if (po != null) { - if (StringUtils.isNotBlank(po.getDrawingNo())) { - markdownText.append("> **图号**: ").append(po.getDrawingNo()).append("\n\n"); - } - if (StringUtils.isNotBlank(po.getDrawingName())) { - markdownText.append("> **图纸名称**: ").append(po.getDrawingName()).append("\n\n"); - } + 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)) { @@ -637,7 +789,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { } if (po.getIsEnterpriseStandard() != null) { - markdownText.append("> **是否企标**: ").append(po.getIsEnterpriseStandard() == 1 ? "是" : "否").append("\n\n"); + markdownText.append("> **是否企标**: ").append(po.getIsEnterpriseStandard() == "1" ? "是" : "否").append("\n\n"); } String bomStatus = null; @@ -667,8 +819,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { markdownText.append("\n请相关人员及时处理。"); - // 发送Markdown消息 - DingUtil.sendOrgGroupMarkdown(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), "生产项目创建通知", markdownText.toString()); + // 发送Markdown消息给个人 + if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) { + robotGroupService.sendMarkdownToUser(dingTalkProperties.getNotifyUserId(), "生产项目创建通知", markdownText.toString()); + } else { + log.warn("未配置钉钉通知个人ID(notifyUserId),跳过发送Markdown消息"); + } // 下载并发送Excel文件 log.info("开始下载并发送Excel文件: {}", orderPro.getProductionOrderNo()); @@ -712,10 +868,14 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { File excelFile = new File(filePath); if (excelFile.exists() && excelFile.length() > 0) { // Upload media - String mediaId = DingUtil.uploadMedia(accessToken, "file", filePath); - // Send file - DingUtil.sendOrgGroupFile(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), mediaId, excelFile.getName(), "sampleFile"); - log.info("Excel文件发送成功"); + 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文件"); + } } else { log.warn("Excel文件未找到或为空: {}", filePath); } @@ -1194,6 +1354,84 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService { return emitter; } + /** + * 下载DWG文件 + * + * @param ids FigureSaveVo ID列表 + * @return 结果 + */ + @Override + public R downloadDwg(List ids) { + if (ids == null || ids.isEmpty()) { + return R.fail("ID列表为空"); + } + + // 只下载第一个文件 + FigureSave figureSave = figureSaveMapper.selectById(ids.get(0)); + if (figureSave == null) { + return R.fail("未找到对应的产品信息"); + } + + // 创建临时目录 + String tempBaseDir = "D:\\dwg_temp\\" + System.currentTimeMillis(); + File tempDir = new File(tempBaseDir); + if (!tempDir.exists() && !tempDir.mkdirs()) { + return R.fail("创建临时目录失败"); + } + + try { + String productionCode = figureSave.getProductionCode(); + String figureName = figureSave.getFigureName(); + + if (StringUtils.isEmpty(productionCode) || StringUtils.isEmpty(figureName)) { + return R.fail("产品信息不完整"); + } + // 1. 解析年份 + String year = "2025"; // 默认 + if (productionCode.startsWith("EY")) { + if (productionCode.length() >= 4) { + try { + Integer.parseInt(productionCode.substring(2, 4)); + year = "20" + productionCode.substring(2, 4); + } catch (Exception ignore) { + } + } + } 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 ignore) { + } + } + } + // 2. 构建FTP路径和本地路径 + // FTP路径: /{year}/{productionCode}/{figureName}.dwg + String ftpPath = "/" + year + "/" + productionCode + "/" + figureName + ".dwg"; + String fileName = figureName + ".dwg"; + // 本地存放路径 + String localFilePath = tempBaseDir + File.separator + fileName; + log.info("开始下载DWG: ftpPath={}, localFilePath={}", ftpPath, localFilePath); + // 3. 下载文件 + boolean downloadSuccess = FtpUtil.downloadSingleFile("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localFilePath); + if (downloadSuccess) { + File downloadedFile = new File(localFilePath); + if (downloadedFile.exists() && downloadedFile.length() > 0) { + // 直接返回文件路径,无需打包 + return R.ok("操作成功", localFilePath); + } else { + return R.fail("文件下载失败:文件为空"); + } + } else { + return R.fail("文件下载失败"); + } + } catch (Exception e) { + log.error("下载DWG过程中发生异常", e); + return R.fail("下载异常: " + e.getMessage()); + } + } + /** * @param id */ 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 5354efc..df603b0 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 @@ -848,10 +848,16 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { } //将带工艺的bom 存表 bomdetail if (routeVoList != null && !routeVoList.isEmpty()) { - for (ProcessRouteVo processRouteVo : routeVoList) { + for (int i = 0; i < routeVoList.size(); i++) { + ProcessRouteVo processRouteVo = routeVoList.get(i); if (processRouteVo.getRawMaterialCode() != null && processRouteVo.getRawMaterialName() != null) { - // 处理表中的材料bom - bomDetailsVos.add(createBomDetails(processRouteVo)); + try { + // 处理表中的材料bom + bomDetailsVos.add(createBomDetails(processRouteVo)); + } catch (Exception e) { + throw new ServiceException("第" + (i + 3) + "行, " + e.getMessage()); + } + System.out.println("BOM" + processRouteVo.getRawMaterialCode()); } } } @@ -879,6 +885,10 @@ public class ProcessRouteServiceImpl implements IProcessRouteService { bomDetails.setSingleWeghit(processRoute.getBomDanZhong()); bomDetails.setMaterial(processRoute.getMaterial()); + if (StringUtils.isEmpty(processRoute.getDiscUsage())) { + throw new ServiceException("材料BOM物料信息不完整: 物料编码[" + processRoute.getRawMaterialCode() + "], 物料名称[" + processRoute.getRawMaterialName() + "] 的用量为空"); + } + // 处理单位换算 if ("mm".equals(processRoute.getBomUnit())) { bomDetails.setQuantity(Double.parseDouble(processRoute.getDiscUsage()) / 1000.0); // 转换为米 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WorkProcedureServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WorkProcedureServiceImpl.java index f92d76a..095f9cf 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WorkProcedureServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WorkProcedureServiceImpl.java @@ -207,7 +207,6 @@ public class WorkProcedureServiceImpl implements IWorkProcedureService { parentTask.setId(pro.getId()); parentTask.setText(pro.getProductionOrderNo()); parentTask.setOpen(false); - parentTask.setToolTipsTxt(pro.getDrawingNo()); /* parentTask.setProgress(1L);*/ parentTask.setStatus("yellow"); parentTask.setParent(null); diff --git a/ruoyi-system/src/main/resources/mapper/system/FigureSaveMapper.xml b/ruoyi-system/src/main/resources/mapper/system/FigureSaveMapper.xml index b7ad7d3..5ecb665 100644 --- a/ruoyi-system/src/main/resources/mapper/system/FigureSaveMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/FigureSaveMapper.xml @@ -20,6 +20,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + diff --git a/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml index e5407ad..9a19f7a 100644 --- a/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/ProcessOrderProMapper.xml @@ -8,8 +8,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - - + + @@ -38,7 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"