更新项目工作流

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

View File

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

View File

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

View File

@ -4,11 +4,14 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Primary;
/**
* 钉钉配置属性
*/
@Data
@Component
@Primary
@ConfigurationProperties(prefix = "dingtalk")
public class DingTalkProperties {
/**
@ -57,8 +60,41 @@ public class DingTalkProperties {
/**
* 接收工艺表格个人用户ID
*/
private String notifyUserId;
/**
* 接收工艺审批用户ID
*/
private String routeApprovalId;
/**
* 回调路由 Key
*/
private String callbackRouteKey;
/**
* Webhook相关配置
*/
private DingTalkProperties.Webhook webhook = new DingTalkProperties.Webhook();
@Data
public static class Robot {
/**
* 机器人Code
*/
private String code;
}
@Data
public static class Webhook {
/**
* 默认Token
*/
private String token;
/**
* 默认Secret
*/
private String secret;
}
}

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

View File

@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
@ -107,4 +108,25 @@ public class FigureSaveController extends BaseController {
@PathVariable Long[] ids) {
return toAjax(iFigureSaveService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 导入产品
*/
@Log(title = "导入产品", businessType = BusinessType.IMPORT)
@SaCheckPermission("system:save:import")
@PostMapping("/importData")
public R<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.ruoyi.common.utils.HttpRequestUtil;
import com.ruoyi.common.utils.WxRobotUtil;
import com.ruoyi.common.dingding.DingUtil;
import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.common.config.DingTalkProperties;
import com.ruoyi.common.poi.ExcelTemplateProc;
import com.ruoyi.common.poi.DynamicDataMapping;
@ -775,7 +775,7 @@ public class KingdeeWorkCenterDataController extends BaseController {
.append("- 预计可用量: ").append(String.format("%.2f", safetyStock.getCurrentStock())).append("\n")
.append("- 即时库存: ").append(String.format("%.2f", safetyStock.getSecAvbqty())).append("\n")
.append("- 最大库存: ").append(String.format("%.2f", safetyStock.getMaxsafetyStock())).append("\n")
.append("- 创建时间: ").append(DateUtil.formatDateTime(safetyStock.getCreateTime())).append("\n")
.append("- 创建时间: ").append(DateUtil.formatDateTime(new Date())).append("\n")
.append("---\n"); // 添加分隔线
}
// 添加总数汇总

View File

@ -56,6 +56,7 @@ import com.ruoyi.system.domain.bo.ProcessOrderProBo;
import com.ruoyi.common.core.page.TableDataInfo;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import shade.com.alibaba.fastjson2.JSONObject;
/**
* 生产项目
@ -143,6 +144,18 @@ public class ProcessOrderProController extends BaseController {
return toAjax(iProcessOrderProService.updateByBo(bo));
}
/**
* 清空项目关联数据保留项目
*
* @param ids 主键串
*/
@SaCheckPermission("system:orderPro:clear")
@Log(title = "清空项目数据", businessType = BusinessType.CLEAN)
@DeleteMapping("/clear/{ids}")
public R<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)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return iProcessOrderProService.deleteWithValidByIds(Arrays.asList(ids), true);
}
/**
* 推送钉钉卡片
*/
@SaCheckPermission("system:orderPro:pushDingTalkCard")
@Log(title = "推送钉钉卡片", businessType = BusinessType.UPDATE)
@PostMapping("/pushDingTalkCard/{id}")
public R<Void> pushDingTalkCard(@PathVariable Long id) {
iProcessOrderProService.pushDingTalkCard(id);
return R.ok();
}
@SaCheckPermission("system:orderPro:getExcel")
@Log(title = "获取项目生产数据表", businessType = BusinessType.INSERT)
@RepeatSubmit()
@ -371,6 +394,16 @@ public class ProcessOrderProController extends BaseController {
return iProcessOrderProService.getOverdueProjects();
}
/**
* 刷新并计算项目延期状态
*/
@SaCheckPermission("system:orderPro:list")
@GetMapping("/refreshDelayStatus")
public R<Void> refreshDelayStatus() {
iProcessOrderProService.refreshDelayStatus();
return R.ok();
}
@SaCheckPermission("system:orderPro:geMRPResults")
@Log(title = "获取MRP复核结果", businessType = BusinessType.OTHER)
@PostMapping("/getMRPResults/{id}")
@ -762,9 +795,7 @@ public class ProcessOrderProController extends BaseController {
map.put("processDescription", route.getProcessDescription());
map.put("productionOrderNo", orderPro.getProductionOrderNo());
map.put("productionName", orderPro.getProductionName());
map.put("drawingNo", orderPro.getDrawingNo());
map.put("mainProducts", orderPro.getDrawingName());
map.put("mainProductsName", orderPro.getDrawingName());
mapList.add(map);
}
return mapList;
@ -2136,4 +2167,53 @@ public class ProcessOrderProController extends BaseController {
}
}
@SaCheckPermission("system:orderPro:downloadZip")
@Log(title = "下载项目DWG", businessType = BusinessType.OTHER)
@GetMapping("/downloadDwg/{id}")
public void downloadDwg(@PathVariable("id") List<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.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.dingding.DingUtil;
import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.excel.DefaultExcelListener;
import com.ruoyi.common.excel.ExcelResult;
@ -439,12 +439,22 @@ public class ProcessRouteController extends BaseController {
cardDataMap.put("status", StringUtils.defaultIfEmpty("待审批", "待审批"));
String outTrackId = "audit-card-" + System.currentTimeMillis();
String outTrackId1 = DingUtil.createAndDeliverCard(accessToken, dingTalkProperties.getRouteCardTemplateId(), dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey());
// 调用发送给工艺负责人的方法 createAndDeliverCardToUser
DingUtil.createAndDeliverCardToUser(
accessToken,
dingTalkProperties.getRouteCardTemplateId(),
Collections.singletonList(dingTalkProperties.getNotifyUserId()),
outTrackId,
cardDataMap,
dingTalkProperties.getCallbackRouteKey()
);
// 统一存入带 userId 后缀的完整 outTrackId以便和钉钉回调保持一致
String fullOutTrackId = outTrackId + "_" + dingTalkProperties.getNotifyUserId();
// 更新数据库中的AuditCardId
ProcessOrderPro update = new ProcessOrderPro();
update.setId(order.getId());
update.setUpdateBy("Dingding");
update.setAuditCardId(outTrackId1);
update.setAuditCardId(fullOutTrackId);
update.setBomStatus(2L);
// 更新审核状态为1已审核/提交审核
update.setRouteStatus(0L);

View File

@ -3,10 +3,13 @@ package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 项目产品对象 figure_save
*
@ -65,4 +68,25 @@ public class FigureSave extends BaseEntity {
* 箱体类型
*/
private String boxType;
/**
* 文件类型
*/
private String fileType;
/**
* 出图人
*/
private String drawBy;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
/**
* 出图时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date drawTime;
}

View File

@ -28,6 +28,17 @@ public class ProcessOrderPro extends BaseEntity {
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
/**
* 生产令号
*/
@ -36,14 +47,6 @@ public class ProcessOrderPro extends BaseEntity {
* 项目名称
*/
private String productionName;
/**
* 图号
*/
private String drawingNo;
/**
* 名称
*/
private String drawingName;
/**
* 计划结束时间
@ -72,11 +75,10 @@ public class ProcessOrderPro extends BaseEntity {
/**
* 是否企标 (0-, 1-)
*/
private Integer isEnterpriseStandard;
private String isEnterpriseStandard;
/**
* 图纸路径
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private String drawingPath;
/**

View File

@ -6,6 +6,7 @@ import com.ruoyi.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
import java.util.Date;
/**
@ -19,9 +20,7 @@ import javax.validation.constraints.*;
@EqualsAndHashCode(callSuper = true)
public class FigureSaveBo extends BaseEntity {
/**
*
*/
private Long id;
/**
@ -80,5 +79,33 @@ public class FigureSaveBo extends BaseEntity {
*/
private String boxType;
/**
* 文件类型
*/
@NotBlank(message = "文件类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String fileType;
/**
* 出图人
*/
@NotBlank(message = "出图人不能为空", groups = { AddGroup.class, EditGroup.class })
private String drawBy;
/**
* 是否延期0否 1是
*/
@NotNull(message = "是否延期0否 1是不能为空", groups = { AddGroup.class, EditGroup.class })
private Integer whetherDelay;
/**
* 延期天数
*/
@NotNull(message = "延期天数不能为空", groups = { AddGroup.class, EditGroup.class })
private Long delayDay;
/**
* 出图时间
*/
@NotNull(message = "出图时间不能为空", groups = { AddGroup.class, EditGroup.class })
private Date drawTime;
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
@ -21,42 +22,41 @@ public class FigureSaveVo {
/**
*
*/
@ExcelProperty(value = "")
private Long id;
/**
* 生产令号
*/
@ExcelProperty(value = "生产令号", index = 0)
private String productionCode;
/**
* 产品编码
*/
@ExcelProperty(value = "产品编码", index = 1)
private String figureNumber;
/**
* 产品名称
*/
@ExcelProperty(value = "产品名称")
@ExcelProperty(value = "产品名称", index = 2)
private String figureName;
/**
* 生产令号
*/
@ExcelProperty(value = "生产令号")
private String productionCode;
/**
* 产品型号
*/
@ExcelProperty(value = "产品型号")
private String figureNumber;
/**
* 数量
*/
@ExcelProperty(value = "数量")
@ExcelProperty(value = "数量", index = 3)
private Long figureNum;
/**
* 行程
*/
@ExcelProperty(value = "行程")
@ExcelProperty(value = "行程", index = 4)
private Long jdInventory;
/**
* 图纸路径
*/
@ExcelProperty(value = "图纸路径")
@ExcelProperty(value = "图纸路径", index = 5)
private String drawPath;
/**
* 类型
@ -67,17 +67,41 @@ public class FigureSaveVo {
/**
* 关联项目表
*/
@ExcelProperty(value = "关联项目表")
private Long pid;
/**
* 轴向
*/
@ExcelProperty(value = "轴向")
private String axialType;
/**
* 箱体类型
*/
@ExcelProperty(value = "箱体类型")
private String boxType;
/**
* 文件类型
*/
@ExcelProperty(value = "文件类型")
private String fileType;
/**
* 出图人
*/
@ExcelProperty(value = "出图人")
private String drawBy;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
/**
* 出图时间
*/
@ExcelProperty(value = "出图时间")
private Date drawTime;
}

View File

@ -44,17 +44,7 @@ public class ProcessOrderProVo {
@ExcelProperty(value = "项目名称")
private String productionName;
/**
* 图号
*/
@ExcelProperty(value = "图号")
private String drawingNo;
/**
* 名称
*/
@ExcelProperty(value = "名称")
private String drawingName;
/**
* 计划结束时间
*/
@ -94,7 +84,7 @@ public class ProcessOrderProVo {
* 是否企标 (0-, 1-)
*/
@ExcelProperty(value = "是否企标 (0-否, 1-是)")
private Integer isEnterpriseStandard;
private String isEnterpriseStandard;
/**
* bom状态(0,未完成 1,完成 2已上传)
*/
@ -115,6 +105,21 @@ public class ProcessOrderProVo {
@Translation(mapper = "drawingPath", type = TransConstant.OSS_ID_TO_URL)
private String drawingPathUrl;
/**
* 图纸路径列表逗号分隔转数组
*/
public String[] getDrawingPathList() {
// 优先使用 drawingPathUrl (已翻译的 URL)
if (drawingPathUrl != null && !drawingPathUrl.isEmpty()) {
return drawingPathUrl.split(",");
}
// 如果 drawingPathUrl 为空尝试直接使用 drawingPath (可能是未翻译的 ID 或直接存的 URL)
if (drawingPath != null && !drawingPath.isEmpty()) {
return drawingPath.split(",");
}
return new String[0];
}
private Date createTime;
/**
* 创建人
@ -170,4 +175,14 @@ public class ProcessOrderProVo {
@ExcelProperty(value = "通知卡片Id")
private String notificationCardId;
/**
* 是否延期0否 1是
*/
private Integer whetherDelay;
/**
* 延期天数
*/
private Long delayDay;
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.BomDetails;
import com.ruoyi.system.domain.vo.BomDetailsVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/**
* bom明细Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-04-08
*/
public interface BomDetailsMapper extends BaseMapperPlus<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.vo.FigureSaveVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/**
* 外购件临时Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-05-28
*/
public interface FigureSaveMapper extends BaseMapperPlus<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.vo.MaterialBomVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/**
* bomaterialMapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-11-08
*/
public interface MaterialBomMapper extends BaseMapperPlus<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}")
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);

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.ProductionOrder;
import com.ruoyi.system.domain.vo.ProductionOrderVo;
import com.ruoyi.common.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Delete;
/**
* 生产订单Mapper接口
@ -11,5 +12,6 @@ import com.ruoyi.common.core.mapper.BaseMapperPlus;
* @date 2024-10-20
*/
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> 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);
/**
* 清空项目关联数据保留项目本身
*/
R<Void> clearProjectData(Collection<Long> ids);
/**
* 校验并批量删除项目令号信息
*/
@ -89,4 +94,17 @@ public interface IProcessOrderProService {
R<String> getRouteLog(Long id) throws JsonProcessingException;
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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.system.domain.ProcessOrderPro;
import com.ruoyi.system.mapper.ProcessOrderProMapper;
import com.ruoyi.system.service.IProcessOrderProService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.bo.FigureSaveBo;
@ -31,7 +34,7 @@ import java.util.Collection;
public class FigureSaveServiceImpl implements IFigureSaveService {
private final FigureSaveMapper baseMapper;
private final ProcessOrderProMapper processOrderProMapper;
/**
* 查询外购件临时
*/
@ -69,6 +72,11 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
lqw.eq(bo.getJdInventory() != null, FigureSave::getJdInventory, bo.getJdInventory());
lqw.eq(StringUtils.isNotBlank(bo.getDrawPath()), FigureSave::getDrawPath, bo.getDrawPath());
lqw.eq(StringUtils.isNotBlank(bo.getProductionCode()), FigureSave::getProductionCode, bo.getProductionCode());
lqw.eq(StringUtils.isNotBlank(bo.getFileType()), FigureSave::getFileType, bo.getFileType());
lqw.eq(StringUtils.isNotBlank(bo.getDrawBy()), FigureSave::getDrawBy, bo.getDrawBy());
lqw.eq(bo.getWhetherDelay() != null, FigureSave::getWhetherDelay, bo.getWhetherDelay());
lqw.eq(bo.getDelayDay() != null, FigureSave::getDelayDay, bo.getDelayDay());
lqw.eq(bo.getDrawTime() != null, FigureSave::getDrawTime, bo.getDrawTime());
return lqw;
}
@ -137,4 +145,39 @@ public class FigureSaveServiceImpl implements IFigureSaveService {
return baseMapper.selectList(wa);
}
@Override
public String importData(List<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.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.dingding.DingUtil;
import com.ruoyi.common.dingding.robot.RobotGroupService;
import com.ruoyi.common.dingding.token.AccessTokenManager;
import com.ruoyi.common.dingding.util.DingUtil;
import com.ruoyi.common.dingding.webhook.WebhookClient;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.FtpUtil;
import com.ruoyi.common.utils.HttpUtils;
@ -91,11 +94,23 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
private final DeviceSpec60rMapper deviceSpec60rMapper;
private final DeviceSpec80rMapper deviceSpec80rMapper;
private final DeviceSpec100rMapper deviceSpec100rMapper;
private final IFigureSaveService iFigureSaveService;
private final SafetyStockMapper safetyStockMapper;
private final IProcessRouteService iProcessRouteService;
private final IProductionOrderService iProductionOrderService;
private final ISysUserService userService;
private final ProductionOrderMapper productionOrderMapper;
@Autowired
private AccessTokenManager accessTokenManager;
@Autowired
private RobotGroupService robotGroupService;
@Autowired
private com.ruoyi.common.dingding.media.MediaService mediaService;
@Autowired
private com.ruoyi.common.dingding.card.CardService cardService;
@Autowired
private WebhookClient webhookClient;
@Autowired
private ProcessRouteMapper processRouteMapper;
@Autowired
@ -111,8 +126,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
@Value("${app.drawing.status-url:http://192.168.5.18:9000/getstate}")
private String statusUrl;
@Autowired
private IFigureSaveService iFigureSaveService;
private IFigureSaveService iFigureSaveSservice;
private Map<String, SpecStrategy> specStrategies;
@Autowired
private BomDetailsMapper bomDetailsMapper;
@Autowired
private MaterialBomMapper materialBomMapper;
public static String[] getNullPropertyNames(Object source) {
BeanWrapper src = new BeanWrapperImpl(source);
@ -234,11 +253,71 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
return baseMapper.selectVoById(id);
}
/**
* 刷新并计算项目延期状态
*/
@Override
public void refreshDelayStatus() {
// 1. 查询所有未完成的项目状态为 0
LambdaQueryWrapper<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
public TableDataInfo<ProcessOrderProVo> queryPageList(ProcessOrderProBo bo, PageQuery pageQuery) {
// 在查询前先刷新数据库中的延期状态
// refreshDelayStatus();
LambdaQueryWrapper<ProcessOrderPro> lqw = buildQueryWrapper(bo);
Page<ProcessOrderProVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
@ -258,8 +337,6 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
LambdaQueryWrapper<ProcessOrderPro> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getProductionOrderNo()), ProcessOrderPro::getProductionOrderNo, bo.getProductionOrderNo());
lqw.like(StringUtils.isNotBlank(bo.getProductionName()), ProcessOrderPro::getProductionName, bo.getProductionName());
lqw.eq(StringUtils.isNotBlank(bo.getDrawingNo()), ProcessOrderPro::getDrawingNo, bo.getDrawingNo());
lqw.like(StringUtils.isNotBlank(bo.getDrawingName()), ProcessOrderPro::getDrawingName, bo.getDrawingName());
lqw.eq(bo.getPlanEndTime() != null, ProcessOrderPro::getPlanEndTime, bo.getPlanEndTime());
lqw.eq(bo.getPlanStartTime() != null, ProcessOrderPro::getPlanStartTime, bo.getPlanStartTime());
lqw.eq(StringUtils.isNotBlank(bo.getDrawingType()), ProcessOrderPro::getDrawingType, bo.getDrawingType());
@ -268,12 +345,34 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
lqw.eq(bo.getQuantity() != null, ProcessOrderPro::getQuantity, bo.getQuantity());
lqw.eq(bo.getBomStatus() != null, ProcessOrderPro::getBomStatus, bo.getBomStatus());
lqw.eq(bo.getIsEnterpriseStandard() != null, ProcessOrderPro::getIsEnterpriseStandard, bo.getIsEnterpriseStandard());
// isEnterpriseStandardList 多选处理
List<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.getWhetherDelay() != null, ProcessOrderPro::getWhetherDelay, bo.getWhetherDelay());
//按照创建时间排序最新的在顶端
lqw.orderByDesc(true, ProcessOrderPro::getCreateTime);
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());
}
}
// 如果出图时间为空默认为当前时间
if (add.getDrawingTime() == null) {
add.setDrawingTime(new Date());
bo.setDrawingTime(add.getDrawingTime());
// 对项目令号的判断根据前缀设置DrawingType
String productionOrderNo = bo.getProductionOrderNo();
if (StringUtils.isNotBlank(productionOrderNo)) {
String isEnterpriseStandard = null;
if (productionOrderNo.toUpperCase().contains("BG")) {
isEnterpriseStandard = PROJECT_TYPE_MAP.get("BG");
} else {
// 提取前缀
String prefix = productionOrderNo.length() >= 2 ? productionOrderNo.substring(0, 2).toUpperCase() : "";
isEnterpriseStandard = PROJECT_TYPE_MAP.get(prefix);
}
if (isEnterpriseStandard != null) {
add.setIsEnterpriseStandard(isEnterpriseStandard);
bo.setIsEnterpriseStandard(isEnterpriseStandard);
}
}
add.setRouteStatus(0L);
add.setBomStatus(0L);
@ -303,92 +414,116 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
// 异步发送钉钉卡片
CompletableFuture.runAsync(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
try {
// 获取 AccessToken
String accessToken = DingUtil.getAccessToken(dingTalkProperties.getAppKey(), dingTalkProperties.getAppSecret());
// 处理图片上传
String imgMediaId = ""; // 默认图片
if (StringUtils.isNotEmpty(bo.getDrawingPath())) {
try {
Long ossId = Long.parseLong(bo.getDrawingPath());
SysOssVo ossVo = iSysOssService.getById(ossId);
if (ossVo != null) {
String suffix = FileUtil.getSuffix(ossVo.getOriginalName());
File tempFile = FileUtil.createTempFile("ding_img", StringUtils.isNotEmpty(suffix) ? "." + suffix : ".jpg", true);
try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) {
IoUtil.copy(is, os);
}
imgMediaId = DingUtil.uploadMedia(accessToken, "image", tempFile.getAbsolutePath());
FileUtil.del(tempFile);
}
} catch (Exception e) {
log.error("上传图片到钉钉失败", e);
}
}
// 构造卡片数据
Map<String, String> cardDataMap = new HashMap<>();
cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定");
cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), ""));
cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), ""));
cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(bo.getDrawingBy(), "暂无"));
cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传");
cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), ""));
cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定");
cardDataMap.put("route_uptime", bo.getRouteUptime() != null ? sdf.format(bo.getRouteUptime()) : "未上传");
cardDataMap.put("material_count", bo.getMaterialCount() != null ? String.valueOf(bo.getMaterialCount()) : "0");
cardDataMap.put("route_count", bo.getRouteCount() != null ? String.valueOf(bo.getRouteCount()) : "0");
cardDataMap.put("plan_uptime", bo.getPlanUptime() != null ? sdf.format(bo.getPlanUptime()) : "未上传");
cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0");
cardDataMap.put("img_id", imgMediaId);
String outTrackId = "project-card-" + System.currentTimeMillis();
String outTrackId1 = DingUtil.createAndDeliverCard(accessToken, dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey());
//同时发送给设计人员
List<String> designers = StringUtils.splitList(bo.getDrawingBy());
// 收集手机号用于@
List<String> receiverMobiles = new ArrayList<>();
for (String designer : designers) {
SysUser sysUser = userService.selectUserByUserName(designer);
if (sysUser != null && StringUtils.isNotEmpty(sysUser.getPhonenumber())) {
receiverMobiles.add(sysUser.getPhonenumber());
log.info("添加发送消息用户 {}: {}", sysUser.getNickName(), sysUser.getPhonenumber());
}
}
if (!receiverMobiles.isEmpty()) {
// 去重
receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList());
try {
log.info("发送群消息并@用户(手机号): {}", receiverMobiles);
DingUtil.sendSessionText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450",
bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles);
} catch (Exception e) {
log.error("发送@消息失败", e);
}
}
// 更新数据库中的 auditCardId
ProcessOrderPro update = new ProcessOrderPro();
update.setId(add.getId());
update.setUpdateBy("Dingding");
update.setNotificationCardId(outTrackId1);
log.info("更新项目令号 {} 卡片id {}", add.getProductionOrderNo(), outTrackId1);
baseMapper.updateById(update);
bo.setAuditCardId(outTrackId1);
} catch (Exception e) {
log.error("发送钉钉卡片异常", e);
}
});
}
return flag;
}
/**
* 推送钉钉卡片
*
* @param id 项目ID
*/
@Override
public void pushDingTalkCard(Long id) {
ProcessOrderPro processOrderPro = baseMapper.selectById(id);
List<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(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
try {
// 获取 AccessToken
String accessToken = accessTokenManager.getAccessToken();
// 处理图片上传
String imgMediaId = ""; // 默认图片
if (StringUtils.isNotEmpty(bo.getDrawingPath())) {
try {
String[] paths = bo.getDrawingPath().split(",");
Long ossId = Long.parseLong(paths[0]);
SysOssVo ossVo = iSysOssService.getById(ossId);
if (ossVo != null) {
String suffix = FileUtil.getSuffix(ossVo.getOriginalName());
File tempFile = FileUtil.createTempFile("ding_img", StringUtils.isNotEmpty(suffix) ? "." + suffix : ".jpg", true);
try (InputStream is = OssFactory.instance().getObjectContent(ossVo.getUrl()); OutputStream os = Files.newOutputStream(tempFile.toPath())) {
IoUtil.copy(is, os);
}
imgMediaId = mediaService.uploadMedia("image", tempFile.getAbsolutePath());
FileUtil.del(tempFile);
}
} catch (Exception e) {
log.error("上传图片到钉钉失败", e);
}
}
// 构造卡片数据
Map<String, String> cardDataMap = new HashMap<>();
cardDataMap.put("drawing_time", bo.getDrawingTime() != null ? sdf.format(bo.getDrawingTime()) : "待定");
cardDataMap.put("production_order_no", StringUtils.defaultIfEmpty(bo.getProductionOrderNo(), ""));
cardDataMap.put("production_name", StringUtils.defaultIfEmpty(bo.getProductionName(), ""));
cardDataMap.put("drawing_by", StringUtils.defaultIfEmpty(drawingBy, "暂无"));
cardDataMap.put("bom_uptime", bo.getBomUptime() != null ? sdf.format(bo.getBomUptime()) : "未上传");
cardDataMap.put("drawing_type", StringUtils.defaultIfEmpty(bo.getDrawingType(), ""));
cardDataMap.put("project_end_time", bo.getProjectEndTime() != null ? sdf.format(bo.getProjectEndTime()) : "待定");
cardDataMap.put("route_uptime", bo.getRouteUptime() != null ? sdf.format(bo.getRouteUptime()) : "未上传");
cardDataMap.put("material_count", bo.getMaterialCount() != null ? String.valueOf(bo.getMaterialCount()) : "0");
cardDataMap.put("route_count", bo.getRouteCount() != null ? String.valueOf(bo.getRouteCount()) : "0");
cardDataMap.put("plan_uptime", bo.getPlanUptime() != null ? sdf.format(bo.getPlanUptime()) : "未上传");
cardDataMap.put("bom_count", bo.getBomCount() != null ? String.valueOf(bo.getBomCount()) : "0");
cardDataMap.put("img_id", imgMediaId);
String outTrackId = "project-card-" + System.currentTimeMillis();
String outTrackId1 = cardService.createAndDeliverCard(dingTalkProperties.getProjectCardTemplateId(), dingTalkProperties.getOpenConversationId(), outTrackId, cardDataMap, dingTalkProperties.getCallbackRouteKey());
//同时发送给设计人员
List<String> designers = StringUtils.splitList(bo.getDrawingBy());
// 收集手机号用于@
List<String> receiverMobiles = new ArrayList<>();
//拿到用户id,这里将用户id存入email字段
for (String designer : designers) {
SysUser sysUser = userService.selectUserByUserName(designer);
if (sysUser != null && StringUtils.isNotEmpty(sysUser.getEmail())) {
receiverMobiles.add(sysUser.getEmail());
log.info("添加发送消息用户 {}: {}", sysUser.getNickName(), sysUser.getEmail());
}
}
//获取最晚的出图时间
FigureSave latest = figureSaves.stream().filter(e -> e.getDrawTime() != null).max(Comparator.comparing(FigureSave::getDrawTime)).orElse(null);
if (!receiverMobiles.isEmpty()) {
// 去重
receiverMobiles = receiverMobiles.stream().distinct().collect(Collectors.toList());
try {
log.info("发送群消息并@用户(手机号): {}", receiverMobiles);
// 使用 WebhookClient 发送消息
webhookClient.sendText("https://oapi.dingtalk.com/robot/send?access_token=61525f1eccca9dde871df334918b1aac47f08e4ef71b8c689b46a091b3d45450", bo.getProductionOrderNo() + " 项目已创建", null, receiverMobiles);
} catch (Exception e) {
log.error("发送@消息失败", e);
}
}
// 更新数据库中的 auditCardId
ProcessOrderPro update = new ProcessOrderPro();
update.setId(processOrderPro.getId());
update.setDrawingBy(drawingBy);
if (latest != null) {
update.setDrawingTime(latest.getDrawTime());
}
update.setUpdateBy("Dingding");
update.setNotificationCardId(outTrackId1);
log.info("更新项目令号 {} 卡片id {}", processOrderPro.getProductionOrderNo(), outTrackId1);
baseMapper.updateById(update);
bo.setAuditCardId(outTrackId1);
} catch (Exception e) {
log.error("发送钉钉卡片异常", e);
}
// });
}
/**
* 修改项目令号
*/
@ -413,6 +548,24 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
}
}
@Override
public R<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;
}
String productionOrderNo = processOrderProVo.getProductionOrderNo();
// 调用 selectByProjectCode 方法并检查结果
return iProcessRouteService.delByProjectCode(productionOrderNo);
}
@ -443,7 +595,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
log.error("删除 ID 列表失败: {}", ids);
}
return R.fail();
return R.ok();
}
@Override
@ -564,6 +716,11 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
figureSave.setFigureName(orderPro.getFigureName());
figureSave.setAxialType(axial);
figureSave.setBoxType(box);
figureSave.setFileType(orderPro.getFileType());
figureSave.setDrawBy(orderPro.getDrawBy());
figureSave.setDelayDay(orderPro.getDelayDay());
figureSave.setDrawTime(orderPro.getDrawTime());
try {
figureSave.setJdInventory(Long.valueOf(journey));
} catch (Exception ignore) {
@ -620,12 +777,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
ProcessOrderPro po = processOrderPro;
SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");
if (po != null) {
if (StringUtils.isNotBlank(po.getDrawingNo())) {
markdownText.append("> **图号**: ").append(po.getDrawingNo()).append("\n\n");
}
if (StringUtils.isNotBlank(po.getDrawingName())) {
markdownText.append("> **图纸名称**: ").append(po.getDrawingName()).append("\n\n");
}
String planStart = po.getPlanStartTime() == null ? "" : sdfDate.format(po.getPlanStartTime());
String planEnd = po.getPlanEndTime() == null ? "" : sdfDate.format(po.getPlanEndTime());
if (StringUtils.isNotBlank(planStart) || StringUtils.isNotBlank(planEnd)) {
@ -637,7 +789,7 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
}
if (po.getIsEnterpriseStandard() != null) {
markdownText.append("> **是否企标**: ").append(po.getIsEnterpriseStandard() == 1 ? "" : "").append("\n\n");
markdownText.append("> **是否企标**: ").append(po.getIsEnterpriseStandard() == "1" ? "" : "").append("\n\n");
}
String bomStatus = null;
@ -667,8 +819,12 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
markdownText.append("\n请相关人员及时处理。");
// 发送Markdown消息
DingUtil.sendOrgGroupMarkdown(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), "生产项目创建通知", markdownText.toString());
// 发送Markdown消息给个人
if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) {
robotGroupService.sendMarkdownToUser(dingTalkProperties.getNotifyUserId(), "生产项目创建通知", markdownText.toString());
} else {
log.warn("未配置钉钉通知个人ID(notifyUserId)跳过发送Markdown消息");
}
// 下载并发送Excel文件
log.info("开始下载并发送Excel文件: {}", orderPro.getProductionOrderNo());
@ -712,10 +868,14 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
File excelFile = new File(filePath);
if (excelFile.exists() && excelFile.length() > 0) {
// Upload media
String mediaId = DingUtil.uploadMedia(accessToken, "file", filePath);
// Send file
DingUtil.sendOrgGroupFile(accessToken, dingTalkProperties.getRobotCode(), dingTalkProperties.getOpenConversationId(), mediaId, excelFile.getName(), "sampleFile");
log.info("Excel文件发送成功");
String mediaId = mediaService.uploadMedia("file", filePath);
// Send file给个人
if (StringUtils.isNotBlank(dingTalkProperties.getNotifyUserId())) {
robotGroupService.sendFileToUser(dingTalkProperties.getNotifyUserId(), mediaId, excelFile.getName());
log.info("Excel文件发送成功");
} else {
log.warn("未配置钉钉通知个人ID(notifyUserId)跳过发送Excel文件");
}
} else {
log.warn("Excel文件未找到或为空: {}", filePath);
}
@ -1194,6 +1354,84 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
return emitter;
}
/**
* 下载DWG文件
*
* @param ids FigureSaveVo ID列表
* @return 结果
*/
@Override
public R<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
*/

View File

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

View File

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

View File

@ -20,6 +20,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="pid" column="pid"/>
<result property="axialType" column="axial_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>
</mapper>

View File

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