diff --git a/pom.xml b/pom.xml
index 7596720..9258f69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
3.5.2
2.14.2
2.4.0
- 1.18.26
+ 1.18.30
1.72
2.7.0
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 77c9c48..8444dd5 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -110,10 +110,20 @@
com.aliyun
dingtalk
- 2.2.41
+ 2.0.15
+
+ com.aliyun
+ alibaba-dingtalk-service-sdk
+ 2.0.0
+
+
+ commons-codec
+ commons-codec
+ 1.11
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java
new file mode 100644
index 0000000..fde62da
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/dingtalk/DingTalkController.java
@@ -0,0 +1,107 @@
+package com.ruoyi.web.controller.dingtalk;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.dingding.DingUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/ding/talk")
+public class DingTalkController {
+ /**
+ * 获取参数配置列表
+ */
+ @GetMapping("/list")
+ public R uploadMedia(String filePath) {
+ String appKey = "dingebfrpzqko25vo8w6";
+ String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk";
+
+ // 如果未提供 filePath,使用默认值
+ if (filePath == null || filePath.isEmpty()) {
+ filePath = "E:/新建文件夹 (2)/4eae3472-de71-4f4b-97cc-4212a88e17c8.png";
+ }
+
+ String accessToken = DingUtil.getAccessToken(appKey, appSecret);
+ // type 固定为 image,也可以根据文件名后缀判断
+ String mediaId = DingUtil.uploadMedia(accessToken, "image", filePath);
+
+ return R.ok("上传成功", mediaId);
+ }
+
+ /**
+ * 发送互动卡片消息 (IM 1.0)
+ */
+ @GetMapping("/sendCard")
+ public R sendCard() {
+ String appKey = "dingebfrpzqko25vo8w6";
+ String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk";
+ String robotCode = "dingebfrpzqko25vo8w6";
+ String openConversationId = "cidCzwPP4a1xhnl9c8hrGXFuw==";
+ String cardTemplateId = "2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema";
+
+ // 获取 AccessToken
+ String accessToken = DingUtil.getAccessToken(appKey, appSecret);
+
+ // 生成唯一 outTrackId
+ String outTrackId = "group-card-" + System.currentTimeMillis();
+
+ // 构造卡片数据
+ Map cardDataMap = new HashMap<>();
+ cardDataMap.put("drawing_time", "2026/1/13 11:03");
+ cardDataMap.put("production_order_no", "CP-026-054-023");
+ cardDataMap.put("drawing_by", "毕敏霞");
+ cardDataMap.put("bom_uptime", "2025/12/13 11:03");
+ cardDataMap.put("drawing_type", "60R");
+ cardDataMap.put("project_end_time", "2026/2/13 11:03");
+ cardDataMap.put("route_uptime", "2025/12/13 11:03");
+ cardDataMap.put("material_count", "2312");
+ cardDataMap.put("route_count", "100");
+ cardDataMap.put("plan_uptime", "2024/12/13 11:03");
+ cardDataMap.put("bom_count", "121");
+ cardDataMap.put("img_id", "@lALPM137gRk79qPNA9fNBT4");
+
+ String trackId = DingUtil.createAndDeliverCard(
+ accessToken,
+ cardTemplateId,
+ robotCode,
+ openConversationId,
+ outTrackId,
+ cardDataMap
+ );
+
+ return R.ok("发送卡片成功", trackId);
+ }
+
+ /**
+ * 机器人发送群聊消息
+ */
+ @GetMapping("/sendRobotMessage")
+ public R sendRobotMessage() {
+ String appKey = "dingebfrpzqko25vo8w6";
+ String appSecret = "M_hoza71hmEqsbbeXM-7MBg67EINqi9C6mKj4zWKQysXN-68GCYYc5DZoV0hXVvk";
+ //机器人编码
+ String robotCode = "dingebfrpzqko25vo8w6";
+ //群回话的id
+ String openConversationId = "cidCzwPP4a1xhnl9c8hrGXFuw==";
+ //消息类型
+ String msgKey = "sampleText";
+
+ // 获取 AccessToken
+ String accessToken = DingUtil.getAccessToken(appKey, appSecret);
+
+ Map msgContent = new HashMap<>();
+ msgContent.put("content", "Hello");
+
+ // 调用机器人发送消息方法
+ String processQueryKey = DingUtil.robotGroupSend(accessToken, msgContent, msgKey, openConversationId, robotCode, null);
+
+ return R.ok("发送成功", processQueryKey);
+ }
+}
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 74e4414..a1ca630 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -65,7 +65,7 @@ spring:
sqlserver:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
- url: jdbc:sqlserver://192.168.5.8:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true
+ url: jdbc:sqlserver://192.168.5.8:14330;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true
username: sa
password: 1a!
hikari:
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 7708ec3..ac54a1f 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -68,7 +68,7 @@ spring:
sqlserver:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
- url: jdbc:sqlserver://192.168.5.8:1433;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true
+ url: jdbc:sqlserver://192.168.5.8:14330;databaseName=AIS20241010133631;encrypt=false;trustServerCertificate=true
username: sa
password: 1a!
# oracle:
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 0d28e11..ecb5c6f 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -290,4 +290,4 @@ management:
logfile:
external-file: ./logs/sys-console.log
dingtalk:
-
+
diff --git a/ruoyi-admin/src/main/resources/kdwebapi.properties b/ruoyi-admin/src/main/resources/kdwebapi.properties
index c6ba843..0914670 100644
--- a/ruoyi-admin/src/main/resources/kdwebapi.properties
+++ b/ruoyi-admin/src/main/resources/kdwebapi.properties
@@ -1,6 +1,5 @@
#??ID-PROD
-X-KDApi-AcctID = 670768a85463de
-#X-KDApi-AcctID = 6723465a38c722
+X-KDApi-AcctID = 695f86f96090b2
X-KDApi-UserName = Administrator
#??IDID
X-KDApi-AppID = 288012_Rc0C0zCG2lga0/Vs2Y4pzYSL6hQcWOko
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 0238aea..4b80798 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -266,6 +266,18 @@
commons-net
3.6
+
+ com.aliyun
+ alibaba-dingtalk-service-sdk
+ 2.0.0
+ compile
+
+
+
+ com.aliyun
+ dingtalk
+ 2.0.15
+
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java
new file mode 100644
index 0000000..cf2ae65
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingChatCreateResult.java
@@ -0,0 +1,35 @@
+package com.ruoyi.common.dingding;
+
+import lombok.Data;
+
+/**
+ * 创建群聊返回参数DTO
+ */
+@Data
+public class DingChatCreateResult {
+ /**
+ * 返回码
+ */
+ private Long errcode;
+
+ /**
+ * 返回码描述
+ */
+ private String errmsg;
+
+ /**
+ * 群会话的ID (旧版)
+ * 后续版本中chatid将不再使用,请将openConversationId作为群会话唯一标识
+ */
+ private String chatid;
+
+ /**
+ * 群会话的ID
+ */
+ private String openConversationId;
+
+ /**
+ * 会话类型:2:企业群
+ */
+ private Long conversationTag;
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java
new file mode 100644
index 0000000..06c4267
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/dingding/DingUtil.java
@@ -0,0 +1,401 @@
+package com.ruoyi.common.dingding;
+
+import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardHeaders;
+import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardRequest;
+import com.aliyun.dingtalkim_1_0.models.SendInteractiveCardResponse;
+import com.aliyun.dingtalkim_1_0.Client;
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest;
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendHeaders;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendRequest;
+import com.aliyun.dingtalkrobot_1_0.models.OrgGroupSendResponse;
+import com.aliyun.tea.TeaConverter;
+import com.aliyun.tea.TeaPair;
+import com.aliyun.teaopenapi.models.Config;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.DingTalkClient;
+import com.dingtalk.api.request.OapiChatCreateRequest;
+import com.dingtalk.api.response.OapiChatCreateResponse;
+import com.dingtalk.api.request.OapiMediaUploadRequest;
+import com.dingtalk.api.response.OapiMediaUploadResponse;
+import com.dingtalk.api.request.OapiRobotSendRequest;
+import com.dingtalk.api.response.OapiRobotSendResponse;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DingUtil {
+ public static final String CUSTOM_ROBOT_TOKEN = "1f3711d72605f77e8ba8e3e3fe0d98989361a7bfb9c30b51a23520eeb783652e";
+
+ public static final String USER_ID = "4345285524672471";
+
+ public static final String SECRET = "SECae018c965ccba100318cc2cd5ef7df2e7a3d0379cf96ef39614fd9558209f18c";
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ // 固定模板和群 ID
+ private static final String CARD_TEMPLATE_ID = "2b7bd7ab-0a20-43f2-902c-71198a8dd6a1.schema";
+ private static final String OPEN_CONVERSATION_ID = "cidCzwPP4a1xhnl9c8hrGXFuw==";
+ /**
+ * 发送钉钉文本消息
+ *
+ * @param content 消息内容
+ * @param atUserIds 需要@的用户ID列表(可选,传null则不@特定人)
+ * @param isAtAll 是否@所有人
+ */
+ public static void sendText(String content, List atUserIds, boolean isAtAll) {
+ try {
+ Long timestamp = System.currentTimeMillis();
+ String stringToSign = timestamp + "\n" + SECRET;
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(SECRET.getBytes("UTF-8"), "HmacSHA256"));
+ byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
+ String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
+ // sign字段和timestamp字段必须拼接到请求URL上
+ String serverUrl = "https://oapi.dingtalk.com/robot/send?sign=" + sign + "×tamp=" + timestamp;
+ DingTalkClient client = new DefaultDingTalkClient(serverUrl);
+ OapiRobotSendRequest req = new OapiRobotSendRequest();
+ // 设置消息类型
+ req.setMsgtype("text");
+
+ // 定义文本内容
+ OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
+ text.setContent(content);
+ req.setText(text);
+
+ // 定义 @ 对象
+ OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
+ if (atUserIds != null && !atUserIds.isEmpty()) {
+ at.setAtUserIds(atUserIds);
+ }
+ if (isAtAll) {
+ at.setIsAtAll(true);
+ }
+ req.setAt(at);
+ OapiRobotSendResponse rsp = client.execute(req, CUSTOM_ROBOT_TOKEN);
+ System.out.println("DingTalk Response: " + rsp.getBody());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("发送钉钉消息失败", e);
+ }
+ }
+
+ /**
+ * 使用 Token 初始化账号Client (IM)
+ * @return Client
+ * @throws Exception
+ */
+ public static com.aliyun.dingtalkim_1_0.Client createImClient() throws Exception {
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new com.aliyun.dingtalkim_1_0.Client(config);
+ }
+
+ /**
+ * 使用 Token 初始化账号Client (OAuth2)
+ * @return Client
+ * @throws Exception
+ */
+ public static com.aliyun.dingtalkoauth2_1_0.Client createOauthClient() throws Exception {
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new com.aliyun.dingtalkoauth2_1_0.Client(config);
+ }
+
+ /**
+ * 获取企业内部应用的accessToken
+ * @param appKey
+ * @param appSecret
+ * @return
+ */
+ public static String getAccessToken(String appKey, String appSecret) {
+ try {
+ com.aliyun.dingtalkoauth2_1_0.Client client = createOauthClient();
+ GetAccessTokenRequest getAccessTokenRequest = new GetAccessTokenRequest()
+ .setAppKey(appKey)
+ .setAppSecret(appSecret);
+ GetAccessTokenResponse response = client.getAccessToken(getAccessTokenRequest);
+ return response.getBody().getAccessToken();
+ } catch (com.aliyun.tea.TeaException err) {
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ System.err.println("TeaException: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(err);
+
+ } catch (Exception _err) {
+ com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(_err.getMessage(), _err);
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ System.err.println("Exception: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(_err);
+ }
+ }
+
+ /**
+ * 上传媒体文件
+ * @param accessToken
+ * @param type 媒体文件类型:image, voice, video, file
+ * @param filePath 文件路径
+ * @return media_id
+ */
+ public static String uploadMedia(String accessToken, String type, String filePath) {
+ try {
+ DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/media/upload");
+ OapiMediaUploadRequest req = new OapiMediaUploadRequest();
+ req.setType(type);
+ com.taobao.api.FileItem item = new com.taobao.api.FileItem(filePath);
+ req.setMedia(item);
+ OapiMediaUploadResponse rsp = client.execute(req, accessToken);
+ if (rsp.isSuccess()) {
+ return rsp.getMediaId();
+ } else {
+ throw new RuntimeException("上传媒体文件失败: " + rsp.getErrmsg());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("上传媒体文件异常", e);
+ }
+ }
+
+ /**
+ * 创建群聊
+ * @param accessToken
+ * @param name 群名称
+ * @param owner 群主userid
+ * @param userIdList 群成员userid列表
+ * @return DingChatCreateResult
+ */
+ public static DingChatCreateResult createChat(String accessToken, String name, String owner, List userIdList) {
+ try {
+ DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/create");
+ OapiChatCreateRequest req = new OapiChatCreateRequest();
+ //群名称
+ req.setName(name);
+ //群主的userId
+ req.setOwner(owner);
+ //群成员列表
+ req.setUseridlist(userIdList);
+ /*
+ 新成员是否可查看100条历史消息:1:可查看 0:不可查看
+ */
+ req.setShowHistoryType(1L);
+ /*
+ 群是否可以被搜索:0(默认):不可搜索 1:可搜索
+ */
+ req.setSearchable(0L);
+ //入群是否需要验证
+ req.setValidationType(0L);
+ //@all 使用范围
+ req.setMentionAllAuthority(0L);
+ //群管理类型:1:仅群主可管理
+ req.setManagementType(1L);
+ //是否开启群禁言
+ req.setChatBannedType(0L);
+ OapiChatCreateResponse rsp = client.execute(req, accessToken);
+ if (rsp.isSuccess()) {
+ DingChatCreateResult result = new DingChatCreateResult();
+ result.setErrcode(rsp.getErrcode());
+ result.setErrmsg(rsp.getErrmsg());
+ result.setChatid(rsp.getChatid());
+ result.setOpenConversationId(rsp.getOpenConversationId());
+ result.setConversationTag(rsp.getConversationTag());
+ return result;
+ } else {
+ throw new RuntimeException("创建群聊失败: " + rsp.getErrmsg());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("创建群聊异常", e);
+ }
+ }
+
+ /**
+ * 机器人发送群聊消息(支持 Map 直接传入)
+ * @param accessToken AccessToken
+ * @param msgContent 消息内容 Map
+ * @param msgKey 消息模板 Key
+ * @param openConversationId 群 ID
+ * @param robotCode 机器人 Code
+ * @param coolAppCode 酷应用 Code (可选)
+ * @return processQueryKey
+ */
+ public static String robotGroupSend(
+ String accessToken,
+ Map msgContent,
+ String msgKey,
+ String openConversationId,
+ String robotCode,
+ String coolAppCode) {
+
+ try {
+ // 1. 初始化 SDK 客户端
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ com.aliyun.dingtalkrobot_1_0.Client client = new com.aliyun.dingtalkrobot_1_0.Client(config);
+
+ // 2. 设置请求头
+ OrgGroupSendHeaders headers = new OrgGroupSendHeaders();
+ headers.xAcsDingtalkAccessToken = accessToken;
+
+ // 3. Map 转 JSON 字符串
+ String msgParam = OBJECT_MAPPER.writeValueAsString(msgContent);
+
+ // 4. 构建消息请求对象
+ OrgGroupSendRequest request = new OrgGroupSendRequest()
+ .setMsgParam(msgParam)
+ .setMsgKey(msgKey)
+ .setOpenConversationId(openConversationId)
+ .setRobotCode(robotCode);
+
+ if (coolAppCode != null && !coolAppCode.isEmpty()) {
+ request.setCoolAppCode(coolAppCode);
+ }
+
+ // 5. 发送消息
+ OrgGroupSendResponse response = client.orgGroupSendWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions());
+ return response.getBody().getProcessQueryKey();
+
+ } catch (com.aliyun.tea.TeaException err) {
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ System.err.println("TeaException: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(err);
+ } catch (Exception e) {
+ com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(e.getMessage(), e);
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ System.err.println("Exception: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 发送互动消息卡片 (IM 1.0)
+ * @param accessToken AccessToken
+ * @param cardTemplateId 卡片模板 ID
+ * @param robotCode 机器人 Code
+ * @param openConversationId 群 ID (可选,如果发送到群)
+ * @param outTrackId 外部跟踪 ID
+ * @param cardDataMap 卡片数据 Map (key-value)
+ * @return outTrackId
+ */
+ public static String createAndDeliverCard(String accessToken, String cardTemplateId, String robotCode,
+ String openConversationId, String outTrackId,
+ Map cardDataMap) {
+ try {
+ com.aliyun.dingtalkim_1_0.Client client = createImClient();
+
+ SendInteractiveCardHeaders sendInteractiveCardHeaders = new SendInteractiveCardHeaders();
+ sendInteractiveCardHeaders.xAcsDingtalkAccessToken = accessToken;
+
+ SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions cardOptions = new SendInteractiveCardRequest.SendInteractiveCardRequestCardOptions()
+ .setSupportForward(true);
+
+ // 转换数据为 Tea 格式 Map (虽然直接传 Map 也是兼容的,但为了稳妥可以使用 buildMap)
+ // 这里直接使用传入的 cardDataMap
+
+ SendInteractiveCardRequest.SendInteractiveCardRequestCardData cardData = new SendInteractiveCardRequest.SendInteractiveCardRequestCardData()
+ .setCardParamMap(cardDataMap);
+
+ SendInteractiveCardRequest sendInteractiveCardRequest = new SendInteractiveCardRequest()
+ .setConversationType(1) // 1: 群聊
+ .setCardData(cardData)
+ .setCardTemplateId(cardTemplateId)
+ .setUserIdType(1)
+ .setOutTrackId(outTrackId)
+ .setPullStrategy(false)
+ .setRobotCode(robotCode)
+ .setOpenConversationId(openConversationId)
+ .setCardOptions(cardOptions);
+
+ client.sendInteractiveCardWithOptions(sendInteractiveCardRequest, sendInteractiveCardHeaders, new com.aliyun.teautil.models.RuntimeOptions());
+
+ return outTrackId;
+
+ } catch (com.aliyun.tea.TeaException err) {
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ System.err.println("TeaException: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(err);
+ } catch (Exception _err) {
+ com.aliyun.tea.TeaException err = new com.aliyun.tea.TeaException(_err.getMessage(), _err);
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ System.err.println("Exception: " + err.code + ", " + err.message);
+ }
+ throw new RuntimeException(_err);
+ }
+ }
+
+ /**
+ * @description 由于接口要求卡片数据的键值对均为 string 类型,需要对卡片数据进行预处理
+ */
+ public static Map convertJsonValuesToString(Map obj) {
+ Map result = new HashMap<>();
+
+ for (Map.Entry entry : obj.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+
+ if (value instanceof String) {
+ result.put(key, (String) value);
+ } else {
+ try {
+ result.put(key, OBJECT_MAPPER.writeValueAsString(value));
+ } catch (JsonProcessingException e) {
+ result.put(key, "");
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 推送群卡片(高级版)
+ * @param cardParamMap 模板变量映射,例如 production_order_no、bom_count 等
+ * @return outTrackId 返回卡片幂等 ID
+ */
+ public static String pushGroupCard(Map cardParamMap) {
+ try {
+ // 获取 AccessToken
+ String accessToken = getAccessToken(CUSTOM_ROBOT_TOKEN, SECRET); // 注意:这里可能需要改为 AppKey/AppSecret
+
+ // 生成唯一 outTrackId
+ String outTrackId = "track-" + System.currentTimeMillis();
+
+ // 调用已写好的 createAndDeliverCard 方法
+ return createAndDeliverCard(
+ accessToken,
+ CARD_TEMPLATE_ID,
+ CUSTOM_ROBOT_TOKEN, // 这里的 robotCode 可能需要根据实际情况调整
+ OPEN_CONVERSATION_ID,
+ outTrackId,
+ cardParamMap
+ );
+
+ } catch (Exception e) {
+ throw new RuntimeException("推送群卡片失败", e);
+ }
+ }
+
+
+ public static void main(String[] args) {
+ // 测试发送
+ sendText("测试消息:刘开~~~,刘开~~~刘开~~~刘开~~~刘开~~~", Arrays.asList(USER_ID), false);
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java
index f6ccede..dd61af6 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/FtpUtil.java
@@ -579,7 +579,7 @@ public class FtpUtil {
}
/**
- * 下载FTP指定目录中的所有文件到本地
+ * 下载FTP指定目录中的所有文件到本地(支持文件名过滤)
*
* @param ftpHost FTP服务器IP
* @param ftpUserName FTP用户名
@@ -587,11 +587,12 @@ public class FtpUtil {
* @param ftpPort FTP端口
* @param remoteDir 远程目录路径
* @param localDir 本地保存目录
+ * @param fileNameFilter 文件名过滤字符串(只下载包含此字符串的文件),传null或空字符串则下载所有
* @return 下载结果
*/
public static R downloadFtpDirectoryFiles(String ftpHost, String ftpUserName,
String ftpPassword, int ftpPort,
- String remoteDir, String localDir) {
+ String remoteDir, String localDir, String fileNameFilter) {
FTPClient ftpClient = null;
try {
// 1. 连接FTP服务器
@@ -601,13 +602,18 @@ public class FtpUtil {
}
// 2. 设置FTP参数
- ftpClient.setControlEncoding("UTF-8");
+ // 强制使用GBK编码,避免Windows FTP服务器UTF-8兼容性问题导致的文件名乱码或451错误
+ ftpClient.setControlEncoding("GBK");
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
// 3. 切换到远程目录
if (!ftpClient.changeWorkingDirectory(remoteDir)) {
- return R.fail("远程目录不存在: " + remoteDir);
+ // 如果切换失败,尝试用ISO-8859-1转码再试
+ String isoPath = new String(remoteDir.getBytes("GBK"), "ISO-8859-1");
+ if (!ftpClient.changeWorkingDirectory(isoPath)) {
+ return R.fail("远程目录不存在: " + remoteDir);
+ }
}
// 4. 创建本地目录
@@ -626,7 +632,7 @@ public class FtpUtil {
// 6. 遍历下载文件
int successCount = 0;
- int totalCount = files.length;
+ int totalCount = 0; // 仅统计符合过滤条件的文件
StringBuilder errorMsg = new StringBuilder();
for (FTPFile file : files) {
@@ -636,6 +642,13 @@ public class FtpUtil {
if (fileName.startsWith(".") || file.isDirectory()) {
continue;
}
+
+ // 过滤文件名
+ if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) {
+ continue;
+ }
+
+ totalCount++; // 计入待下载总数
try {
// 构建本地文件路径
@@ -644,19 +657,37 @@ public class FtpUtil {
// 下载文件
try (OutputStream os = new FileOutputStream(localFile)) {
+ // 尝试直接下载
boolean downloadSuccess = ftpClient.retrieveFile(fileName, os);
+
+ // 如果失败,尝试转码下载 (GBK -> ISO-8859-1)
+ if (!downloadSuccess) {
+ String isoFileName = new String(fileName.getBytes("GBK"), "ISO-8859-1");
+ downloadSuccess = ftpClient.retrieveFile(isoFileName, os);
+ }
+
if (downloadSuccess) {
successCount++;
log.info("文件下载成功: {}", fileName);
} else {
- errorMsg.append("文件下载失败: ").append(fileName).append("; ");
- log.error("文件下载失败: {}", fileName);
+ errorMsg.append("文件下载失败: ").append(fileName).append(" - ").append(ftpClient.getReplyString()).append("; ");
+ log.error("文件下载失败: {} - 响应: {}", fileName, ftpClient.getReplyString());
+ // 删除下载失败的空文件
+ os.close();
+ if (localFile.exists()) {
+ localFile.delete();
+ }
}
}
} catch (Exception e) {
errorMsg.append("文件下载异常: ").append(fileName).append(" - ").append(e.getMessage()).append("; ");
log.error("文件下载异常: {}", fileName, e);
+ // 删除异常的空文件
+ File localFile = new File(localDir + File.separator + fileName);
+ if (localFile.exists()) {
+ localFile.delete();
+ }
}
}
@@ -684,6 +715,15 @@ public class FtpUtil {
}
}
+ /**
+ * 下载FTP指定目录中的所有文件到本地(兼容旧方法调用,默认下载所有)
+ */
+ public static R downloadFtpDirectoryFiles(String ftpHost, String ftpUserName,
+ String ftpPassword, int ftpPort,
+ String remoteDir, String localDir) {
+ return downloadFtpDirectoryFiles(ftpHost, ftpUserName, ftpPassword, ftpPort, remoteDir, localDir, null);
+ }
+
/**
* 主方法 - 用于测试FTP目录文件下载功能
*/
@@ -695,7 +735,7 @@ public class FtpUtil {
int ftpPort = 21;
// 远程目录和本地目录配置
- String remoteDir = "/FB-25-039-FS-01/40S-R-2720-T(FS039-25)-PDF"; // 远程目录路径
+ String remoteDir = "2026/SH-26-001-LT/T40P1-L-1440-D-PDF"; // 远程目录路径
String localDir = "F:/DownloadedFiles"; // 本地保存目录
System.out.println("开始下载FTP文件...");
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java
index 7d19383..7ca590c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/BomDetailsController.java
@@ -1701,10 +1701,10 @@ public class BomDetailsController extends BaseController {
}
subHeadEntity5.addProperty("FIssueType", "1");
- // 创建FPickStockId对象,并加入SubHeadEntity5
+ /*// 创建FPickStockId对象,并加入SubHeadEntity5
JsonObject fPickStockId = new JsonObject();
fPickStockId.addProperty("FNumber", " 010");
- subHeadEntity1.add("FPickStockId", fPickStockId);
+ subHeadEntity1.add("FPickStockId", fPickStockId);*/
subHeadEntity5.addProperty("FOverControlMode", "1");
// 标准人员实作工时
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java
index f6080cf..af5dfdb 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/IndexController.java
@@ -11,10 +11,7 @@ import com.ruoyi.common.utils.JdUtils;
import com.ruoyi.system.domain.ImMaterial;
import com.ruoyi.system.domain.bo.ImMaterialBo;
import com.ruoyi.system.domain.bo.ImProductionPlanBo;
-import com.ruoyi.system.domain.dto.JDInventoryDTO;
-import com.ruoyi.system.domain.dto.JDProductionDTO;
-import com.ruoyi.system.domain.dto.ProMoDTO;
-import com.ruoyi.system.domain.dto.PurchaseOrderDTO;
+import com.ruoyi.system.domain.dto.*;
import com.ruoyi.system.domain.vo.ImMaterialVo;
import com.ruoyi.system.domain.vo.InventoryInfoVO;
import com.ruoyi.system.runner.JdUtil;
@@ -95,18 +92,27 @@ public class IndexController {
.sum())
.orElse(0.0);
}, executorService);
+ CompletableFuture salesOrderFuture = CompletableFuture.supplyAsync(() -> {
+ log.info("开始查询销售订单未入库数量: {}", materialCode);
+ return Optional.ofNullable(JdUtil.getSalesOrderList(materialCode))
+ .map(list -> list.stream()
+ .mapToDouble(SalesOrderDTO::getFBaseRemainOutQty)
+ .sum())
+ .orElse(0.0);
+ }, executorService);
// 等待所有任务完成并获取结果
- CompletableFuture resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture)
+ CompletableFuture resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture,salesOrderFuture)
.thenApply(v -> {
try {
double inventoryQty = inventoryFuture.get();
double fNoPickedQty = noPickedFuture.get();
double productionQty = productionFuture.get();
double purchaseQty = purchaseFuture.get();
+ double salesOrder = salesOrderFuture.get();
- // 计算预计可用库存 即时库存+生产未入库+(采购申请-采购未入库)-子项未领料
- double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty;
+ // 计算预计可用库存 即时库存+生产未入库+(采购申请-采购未入库)-子项未领料-销售实际未出库数量
+ double keyong = inventoryQty + productionQty + purchaseQty - fNoPickedQty-salesOrder;
InventoryInfoVO inventoryInfoVO = new InventoryInfoVO();
inventoryInfoVO.setMaterialCode(materialCode);
inventoryInfoVO.setKucun(String.valueOf(inventoryQty));
@@ -114,6 +120,7 @@ public class IndexController {
inventoryInfoVO.setMaterialName(String.valueOf(purchaseQty));
inventoryInfoVO.setStockName(String.valueOf(productionQty));
inventoryInfoVO.setStockUnit(String.valueOf(keyong));
+ inventoryInfoVO.setSalesOrder(String.valueOf(salesOrder));
return inventoryInfoVO;
} catch (Exception e) {
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java
index 8f78421..a4ea6c0 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/KingdeeWorkCenterDataController.java
@@ -456,29 +456,9 @@ public class KingdeeWorkCenterDataController extends BaseController {
@PostMapping("/getKingdeeDelayData")
public R> getKingdeeDelayData(@RequestParam(value = "workCenter") String workCenter) {
try {
- /* K3CloudApi client = new K3CloudApi();
- JsonObject parameter = new JsonObject();
- List kingdeeWorkCenterDataVos = new ArrayList<>();
- parameter.addProperty("FWorkCenterName", workCenter);
- Object[] parameters = new Object[]{parameter.toString()};
- String execute = client.execute("Ljint.Kingdee.YiTe.KanBan.WebApi.ProduceWebApi.ExecuteService,Ljint.Kingdee.YiTe.KanBan.WebApi", parameters);
- log.info("金蝶接口:" + workCenter + "===> 返回数据: {}", execute);
- // 解析响应
- JSONObject response = JSONObject.parseObject(execute);
- if (!"true".equals(response.getString("IsSuccess"))) {
- String errorMsg = response.getString("Message");
- return R.fail("获取工段数据失败:" + errorMsg);
- }*/
// 获取明天的日期字符串 (格式: yyyy-MM-dd)
String yesterday = DateUtil.format(DateUtil.yesterday(), "yyyy-MM-dd");
-
- /* // 获取数据数组
- JSONArray dataArray = response.getJSONArray("data");
- if (dataArray == null || dataArray.isEmpty()) {
- return R.ok("无数据");
- }
-*/
List kingdeeProduceData = mssqlQueryService.getKingdeeProduceData(workCenter);
List kingdeeWorkCenterDataVos = new ArrayList<>();
for (KingdeeWorkCenterDataBo kingnum : kingdeeProduceData) {
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java
index 8c3f83f..984a267 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ProcessOrderProController.java
@@ -3,13 +3,20 @@ package com.ruoyi.system.controller;
import java.io.*;
import java.math.BigDecimal;
import java.net.URLEncoder;
+import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ruoyi.common.excel.DefaultExcelListener;
import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.FtpUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.VersionComparator;
import com.ruoyi.common.utils.file.SmbUtil;
@@ -113,6 +120,11 @@ public class ProcessOrderProController extends BaseController {
return R.ok(iProcessOrderProService.queryById(id));
}
+ /**
+ * 钉钉数据同步 Webhook 地址
+ */
+ private static final String DINGTALK_WEBHOOK_URL = "https://connector.dingtalk.com/webhook/flow/103694935fdc210503b10006";
+
/**
* 新增项目令号
*/
@@ -121,7 +133,24 @@ public class ProcessOrderProController extends BaseController {
@RepeatSubmit()
@PostMapping()
public R add(@Validated(AddGroup.class) @RequestBody ProcessOrderProBo bo) {
- return toAjax(iProcessOrderProService.insertByBo(bo));
+ boolean result = iProcessOrderProService.insertByBo(bo);
+ if (result) {
+ // 异步同步数据到钉钉 Webhook
+ CompletableFuture.runAsync(() -> {
+ try {
+ // 将业务对象转换为 JSON 字符串
+ String jsonBody = JSONUtil.toJsonStr(bo);
+ System.out.println(jsonBody);
+ // 发送 POST 请求
+ String response = HttpUtil.post(DINGTALK_WEBHOOK_URL, jsonBody);
+ System.out.println("钉钉Webhook响应: " + response);
+ } catch (Exception e) {
+ // 仅记录日志,不影响主流程
+ e.printStackTrace();
+ }
+ });
+ }
+ return toAjax(result);
}
/**
@@ -693,6 +722,8 @@ public class ProcessOrderProController extends BaseController {
// 单重
vo.setBatchQuantity(getCellValueAsLong(row.getCell(18))); // 批次数量
vo.setUnitQuantity(getCellValueAsDouble(row.getCell(17))); // 批次数量
+ vo.setXuEndTime(getCellValueAsDate(row.getCell(21))); // 批次数量
+ vo.setXuStartTime(getCellValueAsDate(row.getCell(20))); // 批次数量
resultList.add(vo);
}
@@ -832,6 +863,42 @@ public class ProcessOrderProController extends BaseController {
}
}
+ /**
+ * 获取单元格的Date值
+ */
+ private Date getCellValueAsDate(XSSFCell cell) {
+ if (cell == null) {
+ return null;
+ }
+
+ switch (cell.getCellType()) {
+ case NUMERIC:
+ if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
+ return cell.getDateCellValue();
+ }
+ return null;
+ case STRING:
+ try {
+ String dateStr = cell.getStringCellValue();
+ if (dateStr == null || dateStr.trim().isEmpty()) {
+ return null;
+ }
+ // 尝试解析字符串日期
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try {
+ return sdf.parse(dateStr);
+ } catch (ParseException e) {
+ sdf = new SimpleDateFormat("yyyy-MM-dd");
+ return sdf.parse(dateStr);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ default:
+ return null;
+ }
+ }
+
/**
* 获取单元格的Long值
*/
@@ -1672,17 +1739,70 @@ public class ProcessOrderProController extends BaseController {
try {
ProcessOrderPro orderPro = processOrderProMapper.selectById(id);
// 下载Excel文件
- SmbUtil.downloadExcelFiles(orderPro.getProductionOrderNo());
+
+ // 1. 解析年份
+ String productionCode = orderPro.getProductionOrderNo();
+ String year = "2025"; // 默认
+ if (productionCode != null) {
+ if (productionCode.startsWith("EY")) {
+ if (productionCode.length() >= 4) {
+ try {
+ Integer.parseInt(productionCode.substring(2, 4));
+ year = "20" + productionCode.substring(2, 4);
+ } catch (Exception e) {}
+ }
+ } else {
+ int firstDash = productionCode.indexOf("-");
+ if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) {
+ try {
+ Integer.parseInt(productionCode.substring(firstDash + 1, firstDash + 3));
+ year = "20" + productionCode.substring(firstDash + 1, firstDash + 3);
+ } catch (Exception e) {}
+ }
+ }
+ }
+
+ // 2. 构建FTP路径和本地路径
+ // 假设Excel文件在 /2026/SH-26-001-LT/ 目录下
+ String ftpPath = "/" + year + "/" + productionCode;
+ String localPath = "D:\\file\\";
+
+ // 3. 下载文件 (只下载汇总表.xlsx)
+ FtpUtil.downloadFtpDirectoryFiles("192.168.5.18", "admin", "hbyt2025", 21, ftpPath, localPath, "汇总表.xlsx");
+
// 构建文件路径
String excelName = "D:\\file\\" + orderPro.getProductionOrderNo() + "汇总表.xlsx";
String rawDataFile = "D:\\file\\RawDataTable.xlsx";
File file = new File(excelName);
- if (!file.exists()) {
- throw new ServiceException("项目 " + orderPro.getProductionOrderNo() + " 未出图");
+ if (!file.exists() || file.length() == 0) {
+ // 如果文件不存在或为空(下载失败),抛出业务异常
+ throw new ServiceException("项目 " + orderPro.getProductionOrderNo() + " 汇总表下载失败或不存在");
}
// 1. 读取第一个sheet的数据list - 使用POI直接读取以保留空格
List allDataList = readExcelWithPOI(excelName,orderPro.getProductionOrderNo());
List routeList = readExcelPOIRoute(excelName,orderPro.getProductionOrderNo());
+ List routeList2 = processRouteService.selectByProjectNumber(orderPro.getProductionOrderNo());
+
+ // 将routeList2中有但routeList中没有的记录添加到routeList
+ if (routeList2 != null && !routeList2.isEmpty()) {
+ if (routeList == null) {
+ routeList = new ArrayList<>();
+ }
+
+ // 使用 Set 存储 routeList 中已有的 materialCode,用于快速排重
+ Set existingMaterialCodes = routeList.stream()
+ .map(ProcessRoute::getMaterialCode)
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toSet());
+
+ for (ProcessRoute route : routeList2) {
+ if (StringUtils.isNotBlank(route.getMaterialCode()) && !existingMaterialCodes.contains(route.getMaterialCode())) {
+ routeList.add(route);
+ existingMaterialCodes.add(route.getMaterialCode()); // 避免重复添加
+ }
+ }
+ }
+
List routes = new ArrayList<>();
List