更新项目工作流

This commit is contained in:
tzy 2026-03-23 13:40:50 +08:00
parent 27308be393
commit 84fb4742d3
41 changed files with 1646 additions and 228 deletions

View File

@ -6,7 +6,7 @@ import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.config.DingTalkProperties; import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.dingding.DingCallbackCrypto; import com.ruoyi.common.dingding.DingCallbackCrypto;
import com.ruoyi.common.dingding.DingUtil; import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.system.controller.KingdeeWorkCenterDataController; import com.ruoyi.system.controller.KingdeeWorkCenterDataController;
import com.ruoyi.system.domain.ProcessOrderPro; import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.mapper.ProcessOrderProMapper; import com.ruoyi.system.mapper.ProcessOrderProMapper;
@ -39,6 +39,13 @@ public class CallbackController {
private DingTalkProperties dingTalkProperties; private DingTalkProperties dingTalkProperties;
@Autowired @Autowired
private KingdeeWorkCenterDataController kingdeeWorkCenterDataController; 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") @PostMapping("/receive")
public Map<String, String> callBack(@RequestBody(required = false) Map<String, Object> json) { public Map<String, String> callBack(@RequestBody(required = false) Map<String, Object> json) {
@ -166,7 +173,7 @@ public class CallbackController {
// 调用 DingUtil 更新卡片 // 调用 DingUtil 更新卡片
try { try {
DingUtil.updateInteractiveCard(accessToken, outTrackId, cardDataMap); cardService.updateInteractiveCard(accessToken, outTrackId, cardDataMap);
bizLogger.info("成功更新卡片状态: outTrackId={}, action={}", outTrackId, action); bizLogger.info("成功更新卡片状态: outTrackId={}, action={}", outTrackId, action);
} catch (Exception e) { } catch (Exception e) {
bizLogger.error("更新卡片失败", e); bizLogger.error("更新卡片失败", e);
@ -230,10 +237,8 @@ public class CallbackController {
String replyContent = "你好 @" + senderId; String replyContent = "你好 @" + senderId;
// 使用 Text 消息格式 // 使用 Text 消息格式
Map<String, Object> textParam = new HashMap<>();
textParam.put("content", replyContent);
try { try {
DingUtil.robotGroupSend(accessToken, textParam, "sampleText", conversationId, targetAppKey, null); robotGroupService.sendText(conversationId, replyContent, Collections.singletonList(senderId));
bizLogger.info("已回复你好(Text),并@用户: {}", senderId); bizLogger.info("已回复你好(Text),并@用户: {}", senderId);
} catch (Exception e) { } catch (Exception e) {
bizLogger.error("回复你好失败", e); bizLogger.error("回复你好失败", e);
@ -344,7 +349,7 @@ public class CallbackController {
// 使用回调中的 sessionWebhook 直接回复 (这是最标准的回调回复方式) // 使用回调中的 sessionWebhook 直接回复 (这是最标准的回调回复方式)
if (sessionWebhook != null && !sessionWebhook.isEmpty()) { if (sessionWebhook != null && !sessionWebhook.isEmpty()) {
DingUtil.sendSessionText(sessionWebhook, helpContent, Collections.singletonList(senderId)); webhookClient.sendText(sessionWebhook, helpContent, Collections.singletonList(senderId), null);
bizLogger.info("通过Webhook回复帮助信息给用户: {}", senderId); bizLogger.info("通过Webhook回复帮助信息给用户: {}", senderId);
} else { } else {
@ -356,36 +361,31 @@ public class CallbackController {
} }
} }
@Autowired
private com.ruoyi.common.dingding.token.AccessTokenManager accessTokenManager; // 注入 AccessTokenManager
/** /**
* 根据回调中的 robotCode 获取对应的 AccessToken * 根据回调中的 robotCode 获取对应的 AccessToken
*/ */
private String getAccessToken(JSONObject eventJson) { private String getAccessToken(JSONObject eventJson) {
String currentRobotCode = eventJson.getString("robotCode"); if (eventJson.containsKey("robotCode")) {
String currentAppKey = dingTalkProperties.getAppKey(); String robotCode = eventJson.getString("robotCode");
String currentAppSecret = dingTalkProperties.getAppSecret(); // 如果是安全库存机器人
if (SAFETY_STOCK_ROBOT_CODE.equals(robotCode) ||
// 判定是否为安全库存机器人 (dingTalkProperties.getSafetyStockAppKey() != null && dingTalkProperties.getSafetyStockAppKey().equals(robotCode))) {
boolean isSafetyRobot = false; String appKey = dingTalkProperties.getSafetyStockAppKey();
String safetyKey = dingTalkProperties.getSafetyStockAppKey(); String appSecret = dingTalkProperties.getSafetyStockAppSecret();
if (currentRobotCode != null) { // 兜底如果配置为空使用常量
if (currentRobotCode.equals(SAFETY_STOCK_ROBOT_CODE)) { if (appKey == null || appKey.isEmpty()) {
isSafetyRobot = true; appKey = SAFETY_STOCK_ROBOT_CODE;
} else if (currentRobotCode.equals(safetyKey)) { }
isSafetyRobot = true; // 动态获取 Token
bizLogger.info("使用安全库存机器人Token: robotCode={}", robotCode);
return DingUtil.getAccessToken(appKey, appSecret);
} }
} }
// 默认情况使用主应用的 Token
if (isSafetyRobot) { return accessTokenManager.getAccessToken();
// 优先使用配置的 Key/Secret
if (safetyKey != null && !safetyKey.isEmpty()) {
currentAppKey = safetyKey;
} else {
currentAppKey = SAFETY_STOCK_ROBOT_CODE;
}
currentAppSecret = dingTalkProperties.getSafetyStockAppSecret();
}
return DingUtil.getAccessToken(currentAppKey, currentAppSecret);
} }
} }

View File

@ -2,7 +2,7 @@ package com.ruoyi.web.controller.dingtalk;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.dingding.DingUtil; import com.ruoyi.common.dingding.util.DingUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;

View File

@ -29,7 +29,7 @@ server:
port: 8033 port: 8033
servlet: servlet:
# 应用的访问路径 # 应用的访问路径
context-path: / context-path: /dev-api
undertow: undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的 # HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1 max-http-post-size: -1
@ -301,3 +301,5 @@ dingtalk:
project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema project-card-template-id: 2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema
route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema route-card-template-id: 58244f90-b92d-4ab9-b619-8e83cef67d0c.schema
callback-route-key: update_route_status2 callback-route-key: update_route_status2
notify-user-id: 0754423610937470
route-approval-id: 01201038164023402380

View File

@ -4,11 +4,14 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Primary;
/** /**
* 钉钉配置属性 * 钉钉配置属性
*/ */
@Data @Data
@Component @Component
@Primary
@ConfigurationProperties(prefix = "dingtalk") @ConfigurationProperties(prefix = "dingtalk")
public class DingTalkProperties { public class DingTalkProperties {
/** /**
@ -57,8 +60,41 @@ public class DingTalkProperties {
/**
* 接收工艺表格个人用户ID
*/
private String notifyUserId;
/**
* 接收工艺审批用户ID
*/
private String routeApprovalId;
/** /**
* 回调路由 Key * 回调路由 Key
*/ */
private String callbackRouteKey; 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;
}
} }

View File

@ -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<String, String> 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<String, String> 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);
}
}
}

View File

@ -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 {
}

View File

@ -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<String> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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消息");
}
}

View File

@ -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);
}
}
}

View File

@ -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<String> 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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.dingtalkcard_1_0.models.*;
import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders; 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.dingtalk.api.response.OapiV2UserGetResponse;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.dingding.DingChatCreateResult;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac; import javax.crypto.Mac;
@ -28,6 +29,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class DingUtil { public class DingUtil {
public static final String CUSTOM_ROBOT_TOKEN = "1f3711d72605f77e8ba8e3e3fe0d98989361a7bfb9c30b51a23520eeb783652e"; 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, public static String createAndDeliverCard(String accessToken, String cardTemplateId, String robotCode,
String openConversationId, String outTrackId, Map<String, String> cardDataMap, String callbackRouteKey) { String openConversationId, String outTrackId, Map<String, String> cardDataMap, String callbackRouteKey) {
// 修改为 1 (群聊)原本为 0 (单聊) 导致接口报错
return createAndDeliverCardInternal(accessToken, cardTemplateId, robotCode, 1, openConversationId, null, outTrackId, cardDataMap, callbackRouteKey); return createAndDeliverCardInternal(accessToken, cardTemplateId, robotCode, 1, openConversationId, null, outTrackId, cardDataMap, callbackRouteKey);
} }

View File

@ -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<String> atUserIds, List<String> 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);
}
}
}

View File

@ -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
}
}
}
}
/** /**
* 列出远程目录中的文件 * 列出远程目录中的文件
* *

View File

@ -53,6 +53,18 @@ public class ExcelUtil {
} }
/**
* 同步导入(适用于小数据量)
*
* @param is 输入流
* @param clazz 对象类型
* @param charset 字符集
* @return 转换后集合
*/
public static <T> List<T> importExcel(InputStream is, Class<T> clazz, String charset) {
return EasyExcel.read(is).head(clazz).charset(java.nio.charset.Charset.forName(charset)).autoCloseStream(false).sheet().doReadSync();
}
/** /**
* 使用校验监听器 异步导入 同步返回 * 使用校验监听器 异步导入 同步返回
* *

View File

@ -26,7 +26,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ProcessRoute; import com.ruoyi.system.domain.ProcessRoute;
import com.ruoyi.system.domain.dto.*; import com.ruoyi.system.domain.dto.*;
import com.ruoyi.common.config.DingTalkProperties; 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.JdUtils;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.BomDetails; import com.ruoyi.system.domain.BomDetails;

View File

@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -107,4 +108,25 @@ public class FigureSaveController extends BaseController {
@PathVariable Long[] ids) { @PathVariable Long[] ids) {
return toAjax(iFigureSaveService.deleteWithValidByIds(Arrays.asList(ids), true)); return toAjax(iFigureSaveService.deleteWithValidByIds(Arrays.asList(ids), true));
} }
/**
* 导入产品
*/
@Log(title = "导入产品", businessType = BusinessType.IMPORT)
@SaCheckPermission("system:save:import")
@PostMapping("/importData")
public R<String> importData(MultipartFile file, boolean updateSupport) throws Exception {
String filename = file.getOriginalFilename();
List<FigureSaveVo> 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);
}
} }

View File

@ -21,7 +21,7 @@ import com.google.gson.JsonObject;
import com.kingdee.bos.webapi.sdk.K3CloudApi; import com.kingdee.bos.webapi.sdk.K3CloudApi;
import com.ruoyi.common.utils.HttpRequestUtil; import com.ruoyi.common.utils.HttpRequestUtil;
import com.ruoyi.common.utils.WxRobotUtil; 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.config.DingTalkProperties;
import com.ruoyi.common.poi.ExcelTemplateProc; import com.ruoyi.common.poi.ExcelTemplateProc;
import com.ruoyi.common.poi.DynamicDataMapping; 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.getCurrentStock())).append("\n")
.append("- 即时库存: ").append(String.format("%.2f", safetyStock.getSecAvbqty())).append("\n") .append("- 即时库存: ").append(String.format("%.2f", safetyStock.getSecAvbqty())).append("\n")
.append("- 最大库存: ").append(String.format("%.2f", safetyStock.getMaxsafetyStock())).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"); // 添加分隔线 .append("---\n"); // 添加分隔线
} }
// 添加总数汇总 // 添加总数汇总

View File

@ -56,6 +56,7 @@ import com.ruoyi.system.domain.bo.ProcessOrderProBo;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 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)); return toAjax(iProcessOrderProService.updateByBo(bo));
} }
/**
* 清空项目关联数据保留项目
*
* @param ids 主键串
*/
@SaCheckPermission("system:orderPro:clear")
@Log(title = "清空项目数据", businessType = BusinessType.CLEAN)
@DeleteMapping("/clear/{ids}")
public R<Void> 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) @Log(title = "项目令号", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}") @DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return iProcessOrderProService.deleteWithValidByIds(Arrays.asList(ids), true); return iProcessOrderProService.deleteWithValidByIds(Arrays.asList(ids), true);
} }
/**
* 推送钉钉卡片
*/
@SaCheckPermission("system:orderPro:pushDingTalkCard")
@Log(title = "推送钉钉卡片", businessType = BusinessType.UPDATE)
@PostMapping("/pushDingTalkCard/{id}")
public R<Void> pushDingTalkCard(@PathVariable Long id) {
iProcessOrderProService.pushDingTalkCard(id);
return R.ok();
}
@SaCheckPermission("system:orderPro:getExcel") @SaCheckPermission("system:orderPro:getExcel")
@Log(title = "获取项目生产数据表", businessType = BusinessType.INSERT) @Log(title = "获取项目生产数据表", businessType = BusinessType.INSERT)
@RepeatSubmit() @RepeatSubmit()
@ -371,6 +394,16 @@ public class ProcessOrderProController extends BaseController {
return iProcessOrderProService.getOverdueProjects(); return iProcessOrderProService.getOverdueProjects();
} }
/**
* 刷新并计算项目延期状态
*/
@SaCheckPermission("system:orderPro:list")
@GetMapping("/refreshDelayStatus")
public R<Void> refreshDelayStatus() {
iProcessOrderProService.refreshDelayStatus();
return R.ok();
}
@SaCheckPermission("system:orderPro:geMRPResults") @SaCheckPermission("system:orderPro:geMRPResults")
@Log(title = "获取MRP复核结果", businessType = BusinessType.OTHER) @Log(title = "获取MRP复核结果", businessType = BusinessType.OTHER)
@PostMapping("/getMRPResults/{id}") @PostMapping("/getMRPResults/{id}")
@ -762,9 +795,7 @@ public class ProcessOrderProController extends BaseController {
map.put("processDescription", route.getProcessDescription()); map.put("processDescription", route.getProcessDescription());
map.put("productionOrderNo", orderPro.getProductionOrderNo()); map.put("productionOrderNo", orderPro.getProductionOrderNo());
map.put("productionName", orderPro.getProductionName()); map.put("productionName", orderPro.getProductionName());
map.put("drawingNo", orderPro.getDrawingNo());
map.put("mainProducts", orderPro.getDrawingName());
map.put("mainProductsName", orderPro.getDrawingName());
mapList.add(map); mapList.add(map);
} }
return mapList; 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<Long> ids, HttpServletResponse response) {
try {
R<String> 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);
}
}
}
} }

View File

@ -16,7 +16,7 @@ import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.validate.AddGroup; import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup; import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.dingding.DingUtil; import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.excel.DefaultExcelListener; import com.ruoyi.common.excel.DefaultExcelListener;
import com.ruoyi.common.excel.ExcelResult; import com.ruoyi.common.excel.ExcelResult;
@ -439,12 +439,22 @@ public class ProcessRouteController extends BaseController {
cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批")); cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批"));
String outTrackId = "audit-card-" + System.currentTimeMillis(); 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 // 更新数据库中的AuditCardId
ProcessOrderPro update = new ProcessOrderPro(); ProcessOrderPro update = new ProcessOrderPro();
update.setId(order.getId()); update.setId(order.getId());
update.setUpdateBy("Dingding"); update.setUpdateBy("Dingding");
update.setAuditCardId(outTrackId1); update.setAuditCardId(fullOutTrackId);
update.setBomStatus(2L); update.setBomStatus(2L);
// 更新审核状态为1已审核/提交审核 // 更新审核状态为1已审核/提交审核
update.setRouteStatus(0L); update.setRouteStatus(0L);

View File

@ -3,10 +3,13 @@ package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.Date;
/** /**
* 项目产品对象 figure_save * 项目产品对象 figure_save
* *
@ -65,4 +68,25 @@ public class FigureSave extends BaseEntity {
* 箱体类型 * 箱体类型
*/ */
private String boxType; 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;
} }

View File

@ -28,6 +28,17 @@ public class ProcessOrderPro extends BaseEntity {
*/ */
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Long id; 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 productionName;
/**
* 图号
*/
private String drawingNo;
/**
* 名称
*/
private String drawingName;
/** /**
* 计划结束时间 * 计划结束时间
@ -72,11 +75,10 @@ public class ProcessOrderPro extends BaseEntity {
/** /**
* 是否企标 (0-, 1-) * 是否企标 (0-, 1-)
*/ */
private Integer isEnterpriseStandard; private String isEnterpriseStandard;
/** /**
* 图纸路径 * 图纸路径
*/ */
@Translation(type = TransConstant.OSS_ID_TO_URL)
private String drawingPath; private String drawingPath;
/** /**

View File

@ -6,6 +6,7 @@ import com.ruoyi.common.core.validate.EditGroup;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import javax.validation.constraints.*; import javax.validation.constraints.*;
import java.util.Date;
/** /**
@ -19,9 +20,7 @@ import javax.validation.constraints.*;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class FigureSaveBo extends BaseEntity { public class FigureSaveBo extends BaseEntity {
/**
*
*/
private Long id; private Long id;
/** /**
@ -80,5 +79,33 @@ public class FigureSaveBo extends BaseEntity {
*/ */
private String boxType; 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;
} }

View File

@ -22,6 +22,7 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class ProcessOrderProBo extends BaseEntity { public class ProcessOrderProBo extends BaseEntity {
/** /**
@ -39,15 +40,7 @@ public class ProcessOrderProBo extends BaseEntity {
*/ */
private String productionName; private String productionName;
/**
* 项目图号
*/
private String drawingNo;
/**
* 产品名称
*/
private String drawingName;
/** /**
* 计划结束时间 * 计划结束时间
*/ */
@ -81,7 +74,13 @@ public class ProcessOrderProBo extends BaseEntity {
/** /**
* 是否企标 (0-, 1-) * 是否企标 (0-, 1-)
*/ */
private Integer isEnterpriseStandard; private String isEnterpriseStandard;
/**
* 是否企标 (查询多选使用)
*/
private List<String> isEnterpriseStandardList;
private String drawingPath; private String drawingPath;
/** /**
@ -151,4 +150,13 @@ public class ProcessOrderProBo extends BaseEntity {
* 通知卡片Id * 通知卡片Id
*/ */
private String notificationCardId; private String notificationCardId;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
} }

View File

@ -1,7 +1,10 @@
package com.ruoyi.system.domain.dto.excuteDrawing; package com.ruoyi.system.domain.dto.excuteDrawing;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data; import lombok.Data;
import java.util.Date;
@Data @Data
public class RigidChainModelDTO { public class RigidChainModelDTO {
@ -28,5 +31,29 @@ public class RigidChainModelDTO {
private String figureNumber; private String figureNumber;
private String productName; private String productName;
private String figureName; private String figureName;
/**
* 文件类型
*/
private String fileType;
/**
* 出图人
*/
private String drawBy;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
/**
* 出图时间
*/
private Date drawTime;
} }

View File

@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data; import lombok.Data;
import java.util.Date;
/** /**
@ -21,42 +22,41 @@ public class FigureSaveVo {
/** /**
* *
*/ */
@ExcelProperty(value = "")
private Long id; 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; private String figureName;
/**
* 生产令号
*/
@ExcelProperty(value = "生产令号")
private String productionCode;
/**
* 产品型号
*/
@ExcelProperty(value = "产品型号")
private String figureNumber;
/** /**
* 数量 * 数量
*/ */
@ExcelProperty(value = "数量") @ExcelProperty(value = "数量", index = 3)
private Long figureNum; private Long figureNum;
/** /**
* 行程 * 行程
*/ */
@ExcelProperty(value = "行程") @ExcelProperty(value = "行程", index = 4)
private Long jdInventory; private Long jdInventory;
/** /**
* 图纸路径 * 图纸路径
*/ */
@ExcelProperty(value = "图纸路径") @ExcelProperty(value = "图纸路径", index = 5)
private String drawPath; private String drawPath;
/** /**
* 类型 * 类型
@ -67,17 +67,41 @@ public class FigureSaveVo {
/** /**
* 关联项目表 * 关联项目表
*/ */
@ExcelProperty(value = "关联项目表")
private Long pid; private Long pid;
/** /**
* 轴向 * 轴向
*/ */
@ExcelProperty(value = "轴向")
private String axialType; private String axialType;
/** /**
* 箱体类型 * 箱体类型
*/ */
@ExcelProperty(value = "箱体类型")
private String boxType; 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;
} }

View File

@ -44,17 +44,7 @@ public class ProcessOrderProVo {
@ExcelProperty(value = "项目名称") @ExcelProperty(value = "项目名称")
private String productionName; private String productionName;
/**
* 图号
*/
@ExcelProperty(value = "图号")
private String drawingNo;
/**
* 名称
*/
@ExcelProperty(value = "名称")
private String drawingName;
/** /**
* 计划结束时间 * 计划结束时间
*/ */
@ -94,7 +84,7 @@ public class ProcessOrderProVo {
* 是否企标 (0-, 1-) * 是否企标 (0-, 1-)
*/ */
@ExcelProperty(value = "是否企标 (0-否, 1-是)") @ExcelProperty(value = "是否企标 (0-否, 1-是)")
private Integer isEnterpriseStandard; private String isEnterpriseStandard;
/** /**
* bom状态(0,未完成 1,完成 2已上传) * bom状态(0,未完成 1,完成 2已上传)
*/ */
@ -115,6 +105,21 @@ public class ProcessOrderProVo {
@Translation(mapper = "drawingPath", type = TransConstant.OSS_ID_TO_URL) @Translation(mapper = "drawingPath", type = TransConstant.OSS_ID_TO_URL)
private String drawingPathUrl; 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; private Date createTime;
/** /**
* 创建人 * 创建人
@ -170,4 +175,14 @@ public class ProcessOrderProVo {
@ExcelProperty(value = "通知卡片Id") @ExcelProperty(value = "通知卡片Id")
private String notificationCardId; private String notificationCardId;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
} }

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.BomDetails; import com.ruoyi.system.domain.BomDetails;
import com.ruoyi.system.domain.vo.BomDetailsVo; import com.ruoyi.system.domain.vo.BomDetailsVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus; import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/** /**
* bom明细Mapper接口 * bom明细Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-04-08 * @date 2024-04-08
*/ */
public interface BomDetailsMapper extends BaseMapperPlus<BomDetailsMapper, BomDetails, BomDetailsVo> { public interface BomDetailsMapper extends BaseMapperPlus<BomDetailsMapper, BomDetails, BomDetailsVo> {
@Delete("DELETE FROM bom_details where total_weight = #{productionOrderNo}")
void deleteWithProCode(String productionOrderNo);
} }

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.FigureSave; import com.ruoyi.system.domain.FigureSave;
import com.ruoyi.system.domain.vo.FigureSaveVo; import com.ruoyi.system.domain.vo.FigureSaveVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus; import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/** /**
* 外购件临时Mapper接口 * 外购件临时Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-05-28 * @date 2024-05-28
*/ */
public interface FigureSaveMapper extends BaseMapperPlus<FigureSaveMapper, FigureSave, FigureSaveVo> { public interface FigureSaveMapper extends BaseMapperPlus<FigureSaveMapper, FigureSave, FigureSaveVo> {
@Delete("DELETE FROM figure_save where production_code = #{productionOrderNo}")
void deleteWithProCode(String productionOrderNo);
} }

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.MaterialBom; import com.ruoyi.system.domain.MaterialBom;
import com.ruoyi.system.domain.vo.MaterialBomVo; import com.ruoyi.system.domain.vo.MaterialBomVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus; import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/** /**
* bomaterialMapper接口 * bomaterialMapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-11-08 * @date 2024-11-08
*/ */
public interface MaterialBomMapper extends BaseMapperPlus<MaterialBomMapper, MaterialBom, MaterialBomVo> { public interface MaterialBomMapper extends BaseMapperPlus<MaterialBomMapper, MaterialBom, MaterialBomVo> {
@Delete("DELETE FROM material_bom where project_number = #{productionOrderNo}")
void deleteWithProCode(String productionOrderNo);
} }

View File

@ -25,7 +25,7 @@ public interface ProcessRouteMapper extends BaseMapperPlus<ProcessRouteMapper, P
@Select("SELECT COUNT(1) FROM process_route WHERE process_no = #{processNo} AND material_code = #{materialCode} AND route_description = #{routeDescription}") @Select("SELECT COUNT(1) FROM process_route WHERE process_no = #{processNo} AND material_code = #{materialCode} AND route_description = #{routeDescription}")
boolean existsByProcessNoAndMaterialCode(@Param("processNo") Long processNo, @Param("materialCode") String materialCode); boolean existsByProcessNoAndMaterialCode(@Param("processNo") Long processNo, @Param("materialCode") String materialCode);
@Delete("DELETE FROM process_route where route_description = #{productionOrderNo}") @Delete("DELETE FROM process_route where route_description = #{routeDescription}")
Boolean deleteByProCode(@Param("routeDescription") String productionOrderNo); Boolean deleteByProCode(@Param("routeDescription") String productionOrderNo);

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.ProductionOrder; import com.ruoyi.system.domain.ProductionOrder;
import com.ruoyi.system.domain.vo.ProductionOrderVo; import com.ruoyi.system.domain.vo.ProductionOrderVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus; import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/** /**
* 生产订单Mapper接口 * 生产订单Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-10-20 * @date 2024-10-20
*/ */
public interface ProductionOrderMapper extends BaseMapperPlus<ProductionOrderMapper, ProductionOrder, ProductionOrderVo> { public interface ProductionOrderMapper extends BaseMapperPlus<ProductionOrderMapper, ProductionOrder, ProductionOrderVo> {
@Delete("DELETE FROM production_order where production_order_no = #{productionOrderNo}")
void deleteWithProCode(String productionOrderNo);
} }

View File

@ -55,4 +55,9 @@ public interface IFigureSaveService {
List<FigureSave> selectByFid(Long id); List<FigureSave> selectByFid(Long id);
List<FigureSave> selectByProCode(String totalWeight); List<FigureSave> selectByProCode(String totalWeight);
/**
* 导入数据
*/
String importData(List<FigureSaveVo> dataList, boolean updateSupport, String operName);
} }

View File

@ -56,6 +56,11 @@ public interface IProcessOrderProService {
*/ */
Boolean updateByBo(ProcessOrderProBo bo); Boolean updateByBo(ProcessOrderProBo bo);
/**
* 清空项目关联数据保留项目本身
*/
R<Void> clearProjectData(Collection<Long> ids);
/** /**
* 校验并批量删除项目令号信息 * 校验并批量删除项目令号信息
*/ */
@ -89,4 +94,17 @@ public interface IProcessOrderProService {
R<String> getRouteLog(Long id) throws JsonProcessingException; R<String> getRouteLog(Long id) throws JsonProcessingException;
SseEmitter startDrawingSse(Long id); SseEmitter startDrawingSse(Long id);
R<String> downloadDwg(List<Long> ids);
/**
* 推送钉钉卡片
* @param id 项目ID
*/
void pushDingTalkCard(Long id);
/**
* 刷新并计算项目延期状态
*/
void refreshDelayStatus();
} }

View File

@ -7,6 +7,9 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.mapper.ProcessOrderProMapper;
import com.ruoyi.system.service.IProcessOrderProService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.bo.FigureSaveBo; import com.ruoyi.system.domain.bo.FigureSaveBo;
@ -31,7 +34,7 @@ import java.util.Collection;
public class FigureSaveServiceImpl implements IFigureSaveService { public class FigureSaveServiceImpl implements IFigureSaveService {
private final FigureSaveMapper baseMapper; 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(bo.getJdInventory() != null, FigureSave::getJdInventory, bo.getJdInventory());
lqw.eq(StringUtils.isNotBlank(bo.getDrawPath()), FigureSave::getDrawPath, bo.getDrawPath()); 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.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; return lqw;
} }
@ -137,4 +145,39 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
return baseMapper.selectList(wa); return baseMapper.selectList(wa);
} }
@Override
public String importData(List<FigureSaveVo> 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("<br/>").append(successNum).append("、数据 ").append(vo.getFigureName()).append(" 导入成功");
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + 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();
}
} }

View File

@ -17,7 +17,10 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo; 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.helper.LoginHelper;
import com.ruoyi.common.utils.FtpUtil; import com.ruoyi.common.utils.FtpUtil;
import com.ruoyi.common.utils.HttpUtils; import com.ruoyi.common.utils.HttpUtils;
@ -91,11 +94,23 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
private final DeviceSpec60rMapper deviceSpec60rMapper; private final DeviceSpec60rMapper deviceSpec60rMapper;
private final DeviceSpec80rMapper deviceSpec80rMapper; private final DeviceSpec80rMapper deviceSpec80rMapper;
private final DeviceSpec100rMapper deviceSpec100rMapper; private final DeviceSpec100rMapper deviceSpec100rMapper;
private final IFigureSaveService iFigureSaveService;
private final SafetyStockMapper safetyStockMapper; private final SafetyStockMapper safetyStockMapper;
private final IProcessRouteService iProcessRouteService; private final IProcessRouteService iProcessRouteService;
private final IProductionOrderService iProductionOrderService; private final IProductionOrderService iProductionOrderService;
private final ISysUserService userService; 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 @Autowired
private ProcessRouteMapper processRouteMapper; private ProcessRouteMapper processRouteMapper;
@Autowired @Autowired
@ -111,8 +126,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
@Value("${app.drawing.status-url:http://192.168.5.18:9000/getstate}") @Value("${app.drawing.status-url:http://192.168.5.18:9000/getstate}")
private String statusUrl; private String statusUrl;
@Autowired @Autowired
private IFigureSaveService iFigureSaveService; private IFigureSaveService iFigureSaveSservice;
private Map<String, SpecStrategy> specStrategies; private Map<String, SpecStrategy> specStrategies;
@Autowired
private BomDetailsMapper bomDetailsMapper;
@Autowired
private MaterialBomMapper materialBomMapper;
public static String[] getNullPropertyNames(Object source) { public static String[] getNullPropertyNames(Object source) {
BeanWrapper src = new BeanWrapperImpl(source); BeanWrapper src = new BeanWrapperImpl(source);
@ -234,11 +253,71 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
return baseMapper.selectVoById(id); return baseMapper.selectVoById(id);
} }
/**
* 刷新并计算项目延期状态
*/
@Override
public void refreshDelayStatus() {
// 1. 查询所有未完成的项目状态为 0
LambdaQueryWrapper<ProcessOrderPro> wrapper = new LambdaQueryWrapper<>();
wrapper.in(ProcessOrderPro::getBomStatus, 0L);
// 确保计划结束时间不为空
wrapper.isNotNull(ProcessOrderPro::getDrawingTime);
List<ProcessOrderPro> list = baseMapper.selectList(wrapper);
if (list == null || list.isEmpty()) {
return;
}
Date now = new Date();
List<ProcessOrderPro> updates = new ArrayList<>();
for (ProcessOrderPro vo : list) {
Integer oldWhetherDelay = vo.getWhetherDelay();
Long oldDelayDay = vo.getDelayDay();
// 检查子产品是否有图纸
boolean hasProductDrawing = false;
List<FigureSave> 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 @Override
public TableDataInfo<ProcessOrderProVo> queryPageList(ProcessOrderProBo bo, PageQuery pageQuery) { public TableDataInfo<ProcessOrderProVo> queryPageList(ProcessOrderProBo bo, PageQuery pageQuery) {
// 在查询前先刷新数据库中的延期状态
// refreshDelayStatus();
LambdaQueryWrapper<ProcessOrderPro> lqw = buildQueryWrapper(bo); LambdaQueryWrapper<ProcessOrderPro> lqw = buildQueryWrapper(bo);
Page<ProcessOrderProVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); Page<ProcessOrderProVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result); return TableDataInfo.build(result);
@ -258,8 +337,6 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
LambdaQueryWrapper<ProcessOrderPro> lqw = Wrappers.lambdaQuery(); LambdaQueryWrapper<ProcessOrderPro> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProcessOrderPro::getProductionOrderNo, bo.getProductionOrderNo()); lqw.like(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProcessOrderPro::getProductionOrderNo, bo.getProductionOrderNo());
lqw.like(StringUtils.isNotBlank(bo.getProductionName()), ProcessOrderPro::getProductionName, bo.getProductionName()); 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.getPlanEndTime() != null, ProcessOrderPro::getPlanEndTime, bo.getPlanEndTime());
lqw.eq(bo.getPlanStartTime() != null, ProcessOrderPro::getPlanStartTime, bo.getPlanStartTime()); lqw.eq(bo.getPlanStartTime() != null, ProcessOrderPro::getPlanStartTime, bo.getPlanStartTime());
lqw.eq(StringUtils.isNotBlank(bo.getDrawingType()), ProcessOrderPro::getDrawingType, bo.getDrawingType()); 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.getQuantity() != null, ProcessOrderPro::getQuantity, bo.getQuantity());
lqw.eq(bo.getBomStatus() != null, ProcessOrderPro::getBomStatus, bo.getBomStatus()); lqw.eq(bo.getBomStatus() != null, ProcessOrderPro::getBomStatus, bo.getBomStatus());
lqw.eq(bo.getIsEnterpriseStandard() != null, ProcessOrderPro::getIsEnterpriseStandard, bo.getIsEnterpriseStandard()); lqw.eq(bo.getIsEnterpriseStandard() != null, ProcessOrderPro::getIsEnterpriseStandard, bo.getIsEnterpriseStandard());
// isEnterpriseStandardList 多选处理
List<String> 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.getDrawingPath() != null, ProcessOrderPro::getDrawingPath, bo.getDrawingPath());
lqw.eq(bo.getWhetherDelay() != null, ProcessOrderPro::getWhetherDelay, bo.getWhetherDelay());
//按照创建时间排序最新的在顶端 //按照创建时间排序最新的在顶端
lqw.orderByDesc(true, ProcessOrderPro::getCreateTime); lqw.orderByDesc(true, ProcessOrderPro::getCreateTime);
return lqw; return lqw;
} }
// 项目类型常量定义
private static final Map<String, String> 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()); add.setCreateBy(user.getNickName());
} }
} }
// 如果出图时间为空默认为当前时间
if (add.getDrawingTime() == null) { // 对项目令号的判断根据前缀设置DrawingType
add.setDrawingTime(new Date()); String productionOrderNo = bo.getProductionOrderNo();
bo.setDrawingTime(add.getDrawingTime()); 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.setRouteStatus(0L);
add.setBomStatus(0L); add.setBomStatus(0L);
@ -303,17 +414,38 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
boolean flag = baseMapper.insert(add) > 0; boolean flag = baseMapper.insert(add) > 0;
if (flag) { if (flag) {
bo.setId(add.getId()); bo.setId(add.getId());
}
return flag;
}
/**
* 推送钉钉卡片
*
* @param id 项目ID
*/
@Override
public void pushDingTalkCard(Long id) {
ProcessOrderPro processOrderPro = baseMapper.selectById(id);
List<FigureSave> 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(() -> { // CompletableFuture.runAsync(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
try { try {
// 获取 AccessToken // 获取 AccessToken
String accessToken = DingUtil.getAccessToken(dingTalkProperties.getAppKey(), dingTalkProperties.getAppSecret()); String accessToken = accessTokenManager.getAccessToken();
// 处理图片上传 // 处理图片上传
String imgMediaId = ""; // 默认图片 String imgMediaId = ""; // 默认图片
if (StringUtils.isNotEmpty(bo.getDrawingPath())) { if (StringUtils.isNotEmpty(bo.getDrawingPath())) {
try { try {
Long ossId = Long.parseLong(bo.getDrawingPath()); String[] paths = bo.getDrawingPath().split(",");
Long ossId = Long.parseLong(paths[0]);
SysOssVo ossVo = iSysOssService.getById(ossId); SysOssVo ossVo = iSysOssService.getById(ossId);
if (ossVo != null) { if (ossVo != null) {
String suffix = FileUtil.getSuffix(ossVo.getOriginalName()); String suffix = FileUtil.getSuffix(ossVo.getOriginalName());
@ -321,7 +453,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) { try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) {
IoUtil.copy(is, os); IoUtil.copy(is, os);
} }
imgMediaId = DingUtil.uploadMedia(accessToken, "image", tempFile.getAbsolutePath()); imgMediaId = mediaService.uploadMedia("image", tempFile.getAbsolutePath());
FileUtil.del(tempFile); FileUtil.del(tempFile);
} }
} catch (Exception e) { } catch (Exception e) {
@ -329,14 +461,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
} }
} }
// 构造卡片数据 // 构造卡片数据
Map<String, String> cardDataMap = new HashMap<>(); Map<String, String> cardDataMap = new HashMap<>();
cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定"); cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定");
cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), "")); cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), ""));
cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), "")); cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), ""));
cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(bo.getDrawingBy(), "暂无")); cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(drawingBy, "暂无"));
cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传"); cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传");
cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), "")); cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), ""));
cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定"); cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定");
@ -347,46 +477,51 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0"); cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0");
cardDataMap.put("img_id", imgMediaId); cardDataMap.put("img_id", imgMediaId);
String outTrackId = "project-card-" + System.currentTimeMillis(); String outTrackId = "project-card-" + System.currentTimeMillis();
String outTrackId1 = DingUtil.createAndDeliverCard(accessToken, dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey()); String outTrackId1 = cardService.createAndDeliverCard(dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey());
//同时发送给设计人员 //同时发送给设计人员
List<String> designers = StringUtils.splitList(bo.getDrawingBy()); List<String> designers = StringUtils.splitList(bo.getDrawingBy());
// 收集手机号用于@ // 收集手机号用于@
List<String> receiverMobiles = new ArrayList<>(); List<String> receiverMobiles = new ArrayList<>();
//拿到用户id,这里将用户id存入email字段
for (String designer : designers) { for (String designer : designers) {
SysUser sysUser = userService.selectUserByUserName(designer); SysUser sysUser = userService.selectUserByUserName(designer);
if (sysUser != null && StringUtils.isNotEmpty(sysUser.getPhonenumber())) { if (sysUser != null && StringUtils.isNotEmpty(sysUser.getEmail())) {
receiverMobiles.add(sysUser.getPhonenumber()); receiverMobiles.add(sysUser.getEmail());
log.info("添加发送消息用户 {}: {}", sysUser.getNickName(), sysUser.getPhonenumber()); 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()) { if (!receiverMobiles.isEmpty()) {
// 去重 // 去重
receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList()); receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList());
try { try {
log.info("发送群消息并@用户(手机号): {}", receiverMobiles); log.info("发送群消息并@用户(手机号): {}", receiverMobiles);
DingUtil.sendSessionText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450", // 使用 WebhookClient 发送消息
bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles); webhookClient.sendText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450", bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles);
} catch (Exception e) { } catch (Exception e) {
log.error("发送@消息失败", e); log.error("发送@消息失败", e);
} }
} }
// 更新数据库中的 auditCardId // 更新数据库中的 auditCardId
ProcessOrderPro update = new ProcessOrderPro(); ProcessOrderPro update = new ProcessOrderPro();
update.setId(add.getId()); update.setId(processOrderPro.getId());
update.setDrawingBy(drawingBy);
if (latest != null) {
update.setDrawingTime(latest.getDrawTime());
}
update.setUpdateBy("Dingding"); update.setUpdateBy("Dingding");
update.setNotificationCardId(outTrackId1); update.setNotificationCardId(outTrackId1);
log.info("更新项目令号 {} 卡片id {}", add.getProductionOrderNo(), outTrackId1); log.info("更新项目令号 {} 卡片id {}", processOrderPro.getProductionOrderNo(), outTrackId1);
baseMapper.updateById(update); baseMapper.updateById(update);
bo.setAuditCardId(outTrackId1); bo.setAuditCardId(outTrackId1);
} catch (Exception e) { } catch (Exception e) {
log.error("发送钉钉卡片异常", e); log.error("发送钉钉卡片异常", e);
} }
}); // });
}
return flag;
} }
/** /**
@ -413,6 +548,24 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
} }
} }
@Override
public R<Void> clearProjectData(Collection<Long> ids) {
for (Long id : ids) {
ProcessOrderProVo processOrderProVo = baseMapper.selectVoById(id);
if (processOrderProVo == null) {
log.warn("未找到对应的 ProcessOrderProVoID: {}", 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; continue;
} }
String productionOrderNo = processOrderProVo.getProductionOrderNo(); String productionOrderNo = processOrderProVo.getProductionOrderNo();
// 调用 selectByProjectCode 方法并检查结果 // 调用 selectByProjectCode 方法并检查结果
return iProcessRouteService.delByProjectCode(productionOrderNo); return iProcessRouteService.delByProjectCode(productionOrderNo);
} }
@ -443,7 +595,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
log.error("删除 ID 列表失败: {}", ids); log.error("删除 ID 列表失败: {}", ids);
} }
return R.fail(); return R.ok();
} }
@Override @Override
@ -564,6 +716,11 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
figureSave.setFigureName(orderPro.getFigureName()); figureSave.setFigureName(orderPro.getFigureName());
figureSave.setAxialType(axial); figureSave.setAxialType(axial);
figureSave.setBoxType(box); figureSave.setBoxType(box);
figureSave.setFileType(orderPro.getFileType());
figureSave.setDrawBy(orderPro.getDrawBy());
figureSave.setDelayDay(orderPro.getDelayDay());
figureSave.setDrawTime(orderPro.getDrawTime());
try { try {
figureSave.setJdInventory(Long.valueOf(journey)); figureSave.setJdInventory(Long.valueOf(journey));
} catch (Exception ignore) { } catch (Exception ignore) {
@ -620,12 +777,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
ProcessOrderPro po = processOrderPro; ProcessOrderPro po = processOrderPro;
SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd"); SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");
if (po != null) { 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 planStart = po.getPlanStartTime() == null ? "" : sdfDate.format(po.getPlanStartTime());
String planEnd = po.getPlanEndTime() == null ? "" : sdfDate.format(po.getPlanEndTime()); String planEnd = po.getPlanEndTime() == null ? "" : sdfDate.format(po.getPlanEndTime());
if (StringUtils.isNotBlank(planStart) || StringUtils.isNotBlank(planEnd)) { if (StringUtils.isNotBlank(planStart) || StringUtils.isNotBlank(planEnd)) {
@ -637,7 +789,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
} }
if (po.getIsEnterpriseStandard() != null) { 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; String bomStatus = null;
@ -667,8 +819,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
markdownText.append("\n请相关人员及时处理。"); markdownText.append("\n请相关人员及时处理。");
// 发送Markdown消息 // 发送Markdown消息给个人
DingUtil.sendOrgGroupMarkdown(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), "生产项目创建通知", markdownText.toString()); if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) {
robotGroupService.sendMarkdownToUser(dingTalkProperties.getNotifyUserId(), "生产项目创建通知", markdownText.toString());
} else {
log.warn("未配置钉钉通知个人ID(notifyUserId)跳过发送Markdown消息");
}
// 下载并发送Excel文件 // 下载并发送Excel文件
log.info("开始下载并发送Excel文件: {}", orderPro.getProductionOrderNo()); log.info("开始下载并发送Excel文件: {}", orderPro.getProductionOrderNo());
@ -712,10 +868,14 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
File excelFile = new File(filePath); File excelFile = new File(filePath);
if (excelFile.exists() && excelFile.length() > 0) { if (excelFile.exists() && excelFile.length() > 0) {
// Upload media // Upload media
String mediaId = DingUtil.uploadMedia(accessToken, "file", filePath); String mediaId = mediaService.uploadMedia("file", filePath);
// Send file // Send file给个人
DingUtil.sendOrgGroupFile(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), mediaId, excelFile.getName(), "sampleFile"); if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) {
robotGroupService.sendFileToUser(dingTalkProperties.getNotifyUserId(), mediaId, excelFile.getName());
log.info("Excel文件发送成功"); log.info("Excel文件发送成功");
} else {
log.warn("未配置钉钉通知个人ID(notifyUserId)跳过发送Excel文件");
}
} else { } else {
log.warn("Excel文件未找到或为空: {}", filePath); log.warn("Excel文件未找到或为空: {}", filePath);
} }
@ -1194,6 +1354,84 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
return emitter; return emitter;
} }
/**
* 下载DWG文件
*
* @param ids FigureSaveVo ID列表
* @return 结果
*/
@Override
public R<String> downloadDwg(List<Long> 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 * @param id
*/ */

View File

@ -848,10 +848,16 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
} }
//将带工艺的bom 存表 bomdetail //将带工艺的bom 存表 bomdetail
if (routeVoList != null && !routeVoList.isEmpty()) { 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) { if (processRouteVo.getRawMaterialCode() != null && processRouteVo.getRawMaterialName() != null) {
try {
// 处理表中的材料bom // 处理表中的材料bom
bomDetailsVos.add(createBomDetails(processRouteVo)); bomDetailsVos.add(createBomDetails(processRouteVo));
} catch (Exception e) {
throw new ServiceException("" + (i + 3) + "行, " + e.getMessage());
}
System.out.println("<UNK>BOM<UNK>" + processRouteVo.getRawMaterialCode());
} }
} }
} }
@ -879,6 +885,10 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
bomDetails.setSingleWeghit(processRoute.getBomDanZhong()); bomDetails.setSingleWeghit(processRoute.getBomDanZhong());
bomDetails.setMaterial(processRoute.getMaterial()); bomDetails.setMaterial(processRoute.getMaterial());
if (StringUtils.isEmpty(processRoute.getDiscUsage())) {
throw new ServiceException("材料BOM物料信息不完整: 物料编码[" + processRoute.getRawMaterialCode() + "], 物料名称[" + processRoute.getRawMaterialName() + "] 的用量为空");
}
// 处理单位换算 // 处理单位换算
if ("mm".equals(processRoute.getBomUnit())) { if ("mm".equals(processRoute.getBomUnit())) {
bomDetails.setQuantity(Double.parseDouble(processRoute.getDiscUsage()) / 1000.0); // 转换为米 bomDetails.setQuantity(Double.parseDouble(processRoute.getDiscUsage()) / 1000.0); // 转换为米

View File

@ -207,7 +207,6 @@ public class WorkProcedureServiceImpl implements IWorkProcedureService {
parentTask.setId(pro.getId()); parentTask.setId(pro.getId());
parentTask.setText(pro.getProductionOrderNo()); parentTask.setText(pro.getProductionOrderNo());
parentTask.setOpen(false); parentTask.setOpen(false);
parentTask.setToolTipsTxt(pro.getDrawingNo());
/* parentTask.setProgress(1L);*/ /* parentTask.setProgress(1L);*/
parentTask.setStatus("yellow"); parentTask.setStatus("yellow");
parentTask.setParent(null); parentTask.setParent(null);

View File

@ -20,6 +20,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="pid" column="pid"/> <result property="pid" column="pid"/>
<result property="axialType" column="axial_type"/> <result property="axialType" column="axial_type"/>
<result property="boxType" column="box_type"/> <result property="boxType" column="box_type"/>
<result property="fileType" column="file_type"/>
<result property="drawBy" column="draw_by"/>
<result property="whetherDelay" column="whether_delay"/>
<result property="delayDay" column="delay_day"/>
<result property="drawTime" column="draw_time"/>
</resultMap> </resultMap>
</mapper> </mapper>

View File

@ -8,8 +8,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="id" column="id"/> <result property="id" column="id"/>
<result property="productionOrderNo" column="production_order_no"/> <result property="productionOrderNo" column="production_order_no"/>
<result property="productionName" column="production_name"/> <result property="productionName" column="production_name"/>
<result property="drawingNo" column="drawing_no"/> <result property="whetherDelay" column="whether_delay"/>
<result property="drawingName" column="drawing_name"/> <result property="delayDay" column="delay_day"/>
<result property="createBy" column="create_by"/> <result property="createBy" column="create_by"/>
<result property="updateBy" column="update_by"/> <result property="updateBy" column="update_by"/>
<result property="createTime" column="create_time"/> <result property="createTime" column="create_time"/>
@ -38,7 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<!-- 根据项目编号查询 --> <!-- 根据项目编号查询 -->
<select id="selectByProjectNumber" resultType="com.ruoyi.system.domain.ProcessOrderPro"> <select id="selectByProjectNumber" resultType="com.ruoyi.system.domain.ProcessOrderPro">
SELECT id, production_order_no, production_name, drawing_no, drawing_name,drawing_type, SELECT id, production_order_no, production_name, drawing_type,
plan_end_time, plan_start_time, create_by, create_time, update_by, update_time plan_end_time, plan_start_time, create_by, create_time, update_by, update_time
FROM process_order_pro FROM process_order_pro
WHERE production_order_no = #{routeDescription} WHERE production_order_no = #{routeDescription}
@ -48,8 +48,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
UPDATE process_order_pro UPDATE process_order_pro
SET production_order_no = #{productionOrderNo}, SET production_order_no = #{productionOrderNo},
production_name = #{productionName}, production_name = #{productionName},
drawing_no = #{drawingNo},
drawing_name = #{drawingName},
plan_end_time = #{planEndTime}, plan_end_time = #{planEndTime},
plan_start_time = #{planStartTime}, plan_start_time = #{planStartTime},
create_by = #{createBy}, create_by = #{createBy},