jindie xiangguangognneng

This commit is contained in:
tzy 2026-01-19 19:31:06 +08:00
parent a83e250c37
commit eafb53c54d
31 changed files with 1180 additions and 125 deletions

View File

@ -35,7 +35,7 @@
<dynamic-ds.version>3.5.2</dynamic-ds.version>
<alibaba-ttl.version>2.14.2</alibaba-ttl.version>
<xxl-job.version>2.4.0</xxl-job.version>
<lombok.version>1.18.26</lombok.version>
<lombok.version>1.18.30</lombok.version>
<bouncycastle.version>1.72</bouncycastle.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>

View File

@ -110,10 +110,20 @@
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.2.41</version>
<version>2.0.15</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>

View File

@ -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<String> 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<String> 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<String, String> 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<String> 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<String, Object> msgContent = new HashMap<>();
msgContent.put("content", "Hello");
// 调用机器人发送消息方法
String processQueryKey = DingUtil.robotGroupSend(accessToken, msgContent, msgKey, openConversationId, robotCode, null);
return R.ok("发送成功", processQueryKey);
}
}

View File

@ -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:

View File

@ -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:

View File

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

View File

@ -266,6 +266,18 @@
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.0.15</version>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -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<String> 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 + "&timestamp=" + 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<String> 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<String, Object> 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<String, String> 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<String, String> 也是兼容的但为了稳妥可以使用 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<String, String> convertJsonValuesToString(Map<String, Object> obj) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, Object> 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_nobom_count
* @return outTrackId 返回卡片幂等 ID
*/
public static String pushGroupCard(Map<String, String> 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);
}
}

View File

@ -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<String> 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,14 +602,19 @@ 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)) {
// 如果切换失败尝试用ISO-8859-1转码再试
String isoPath = new String(remoteDir.getBytes("GBK"), "ISO-8859-1");
if (!ftpClient.changeWorkingDirectory(isoPath)) {
return R.fail("远程目录不存在: " + remoteDir);
}
}
// 4. 创建本地目录
File localDirectory = new File(localDir);
@ -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) {
@ -637,6 +643,13 @@ public class FtpUtil {
continue;
}
// 过滤文件名
if (fileNameFilter != null && !fileNameFilter.isEmpty() && !fileName.contains(fileNameFilter)) {
continue;
}
totalCount++; // 计入待下载总数
try {
// 构建本地文件路径
String localFilePath = localDir + File.separator + fileName;
@ -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<String> 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文件...");

View File

@ -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");
// 标准人员实作工时

View File

@ -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<Double> 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<InventoryInfoVO> resultFuture = CompletableFuture.allOf(inventoryFuture, noPickedFuture, productionFuture, purchaseFuture)
CompletableFuture<InventoryInfoVO> 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) {

View File

@ -456,29 +456,9 @@ public class KingdeeWorkCenterDataController extends BaseController {
@PostMapping("/getKingdeeDelayData")
public R<List<KingdeeWorkCenterDataBo>> getKingdeeDelayData(@RequestParam(value = "workCenter") String workCenter) {
try {
/* K3CloudApi client = new K3CloudApi();
JsonObject parameter = new JsonObject();
List<KingdeeWorkCenterDataBo> 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<KingdeeWorkCenterDataBo> kingdeeProduceData = mssqlQueryService.getKingdeeProduceData(workCenter);
List<KingdeeWorkCenterDataBo> kingdeeWorkCenterDataVos = new ArrayList<>();
for (KingdeeWorkCenterDataBo kingnum : kingdeeProduceData) {

View File

@ -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<Void> 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<ProductionOrderVo> allDataList = readExcelWithPOI(excelName,orderPro.getProductionOrderNo());
List<ProcessRoute> routeList = readExcelPOIRoute(excelName,orderPro.getProductionOrderNo());
List<ProcessRoute> routeList2 = processRouteService.selectByProjectNumber(orderPro.getProductionOrderNo());
// 将routeList2中有但routeList中没有的记录添加到routeList
if (routeList2 != null && !routeList2.isEmpty()) {
if (routeList == null) {
routeList = new ArrayList<>();
}
// 使用 Set 存储 routeList 中已有的 materialCode用于快速排重
Set<String> 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<ProcessRoute> routes = new ArrayList<>();
List<Map<String, Object>> kingdeeBomRows = new ArrayList<>();
for (ProcessRoute base : routeList) {
@ -1702,6 +1822,8 @@ public class ProcessOrderProController extends BaseController {
item.setDiscWeight(base.getDiscWeight());
item.setUnitQuantity(base.getUnitQuantity());
item.setBatchQuantity(base.getBatchQuantity());
item.setXuEndTime(base.getXuEndTime());
item.setXuStartTime(base.getXuStartTime());
routes.add(item);
continue;
}
@ -1715,6 +1837,8 @@ public class ProcessOrderProController extends BaseController {
item.setDiscWeight(base.getDiscWeight());
item.setUnitQuantity(base.getUnitQuantity());
item.setBatchQuantity(base.getBatchQuantity());
item.setXuEndTime(base.getXuEndTime());
item.setXuStartTime(base.getXuStartTime());
routes.add(item);
continue;
}
@ -1740,6 +1864,8 @@ public class ProcessOrderProController extends BaseController {
item.setProcessControl(r.getProcessControl());
item.setActivityDuration(r.getActivityDuration());
item.setActivityUnit(r.getActivityUnit());
item.setXuStartTime(r.getXuStartTime());
item.setXuEndTime(r.getXuEndTime());
routes.add(item);
});
} else {
@ -1751,6 +1877,8 @@ public class ProcessOrderProController extends BaseController {
item.setDiscWeight(base.getDiscWeight());
item.setUnitQuantity(base.getUnitQuantity());
item.setBatchQuantity(base.getBatchQuantity());
item.setXuStartTime(base.getXuStartTime());
item.setXuEndTime(base.getXuEndTime());
// 不写入BOM字段保持纯工艺数据行
routes.add(item);
}

View File

@ -891,7 +891,7 @@ public class ProcessRouteController extends BaseController {
* 更新生产订单仓库字段
*/
@SaCheckPermission("system:route:updateProductionOrders")
@Log(title = "获取全部工艺路线和物料清单", businessType = BusinessType.OTHER)
@Log(title = "更新生产订单仓库", businessType = BusinessType.OTHER)
@PostMapping("/updateProductionOrders")
public R updateProductionOrders(@RequestParam("rooteProdet") String rooteProdet,@RequestParam("cangKuNum")String cangKuNum) throws Exception {
//判断此项目是否有
@ -900,10 +900,10 @@ public class ProcessRouteController extends BaseController {
}
/**
* 更新采购申请仓库字段
* 更新采购申请入库仓库仓库
*/
@SaCheckPermission("system:route:updateProductionOrders")
@Log(title = "获取全部工艺路线和物料清单", businessType = BusinessType.OTHER)
@Log(title = "更新采购申请单仓库", businessType = BusinessType.OTHER)
@PostMapping("/updateCgOrders")
public R updateCgOrders(@RequestParam("rooteProdet") String rooteProdet,@RequestParam("cangKuNum")String cangKuNum) throws Exception {
//判断此项目是否有

View File

@ -9,4 +9,6 @@ public class CgDTO {
private String orderBillNo;
@JsonProperty("FMaterialId.FNumber")
private String materialCode;
@JsonProperty("FMaterialName")
private String materialName;
}

View File

@ -33,6 +33,6 @@ public class PlanPrcessNumDTO {
private Date FOperPlanFinishTime;
@JsonProperty("FMONumber")
private String FMONumber;
@JsonProperty("F_HBYT_RKCK.FName")
@JsonProperty("F_HBYT_RKCK")
private String FRKCKFName;
}

View File

@ -2,7 +2,11 @@ package com.ruoyi.system.domain.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 采购订单未入库数量
* @author tzy
* @date 2025-10-26
*/
@Data
public class PurchaseOrderDTO {
@JsonProperty("FMaterialId.FNumber")

View File

@ -0,0 +1,14 @@
package com.ruoyi.system.domain.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class SalesOrderDTO {
@JsonProperty("FMaterialId.FNumber")
private String FMaterialIdFNumber;
@JsonProperty("FMaterialName")
private String FMaterialName;
@JsonProperty("FBaseRemainOutQty")
private Double FBaseRemainOutQty;
}

View File

@ -10,4 +10,5 @@ public class InventoryInfoVO {
private String stockName; // 仓库
private String stockUnit; // 单位
private String quantity; // 数量
private String salesOrder; // 销售数量
}

View File

@ -21,6 +21,12 @@ public class FSubEntity {
/*
入库仓库
*/
@JsonProperty("F_HBYT_RKCK.FName")
private String rkckName;
@JsonProperty("F_HBYT_RKCK")
private F_HBYT_RKCK F_HBYT_RKCK;
@Data
public static class F_HBYT_RKCK {
@JsonProperty("FNUMBER")
private String FNUMBER;
}
}

View File

@ -11,7 +11,6 @@ import java.util.List;
public class Model {
@JsonProperty("FID")
private int FID;
@JsonProperty("FPlanStartTime")
private Date FPlanStartTime;
@JsonProperty("FProcessId_number") // 添加 FProcessId_number 字段

View File

@ -3049,6 +3049,7 @@ public class JdUtil {
pageJson.addProperty("StartRow", startRow);
pageJson.addProperty("Limit", pageSize);
String resultJson = String.valueOf(client.billQuery(pageJson.toString()));
System.out.println("0000000000000000"+ pageJson);
JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class);
if (jsonArray == null || jsonArray.size() == 0) {
break;
@ -3432,7 +3433,7 @@ public class JdUtil {
// 请求参数要求为json字符串
JsonObject json = new JsonObject();
json.addProperty("FormId", "PUR_Requisition");
json.addProperty("FieldKeys", "FBillNo,FMaterialId.FNumber");
json.addProperty("FieldKeys", "FBillNo,FMaterialId.FNumber,FMaterialName");
JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "F_UCHN_Text");
@ -3473,7 +3474,8 @@ public class JdUtil {
JsonArray needUpDateFields = new JsonArray();
needUpDateFields.add("FID");
needUpDateFields.add("F_HBYT_RKCK.FName");
needUpDateFields.add("FENTRYID");
needUpDateFields.add("F_HBYT_RKCK");
needUpDateFields.add("FTreeEntity");
json.add("NeedUpDateFields", needUpDateFields);
@ -3484,7 +3486,11 @@ public class JdUtil {
JsonArray fTreeEntity = new JsonArray();
JsonObject fTreeEntityItem = new JsonObject();
fTreeEntityItem.addProperty("F_HBYT_RKCK.FName", cangKuNum);
// 修改使用 FNUMBER 对象结构设置仓库
JsonObject rkck = new JsonObject();
rkck.addProperty("FNUMBER", cangKuNum);
fTreeEntityItem.add("F_HBYT_RKCK", rkck);
fTreeEntityItem.addProperty("FENTRYID", jdHuoZhu.getFTreeEntityFENTRYID());
fTreeEntity.add(fTreeEntityItem);
model.add("FTreeEntity", fTreeEntity);
@ -3523,8 +3529,12 @@ public class JdUtil {
JsonArray fTreeEntity = new JsonArray();
JsonObject fTreeEntityItem = new JsonObject();
fTreeEntityItem.addProperty("F_HBYT_RKCK.FName", cangKuNum);
fTreeEntityItem.addProperty("FEntity_FEntryID", fidEntryIdDTO.getFEntryId());
// 修改使用 FNUMBER 对象结构设置仓库
JsonObject rkck = new JsonObject();
rkck.addProperty("FNUMBER", cangKuNum);
fTreeEntityItem.add("F_HBYT_RKCK", rkck);
fTreeEntityItem.addProperty("FEntryID", fidEntryIdDTO.getFEntryId());
fTreeEntity.add(fTreeEntityItem);
model.add("FEntity", fTreeEntity);
json.add("Model", model);
@ -3539,9 +3549,9 @@ public class JdUtil {
RepoRet sRet = gson.fromJson(result, RepoRet.class);
if (sRet.isSuccessfully()) {
return "采购申请单保存成功";
return gson.toJson(sRet.getResult());
} else {
return "采购申请单成功失败:" + gson.toJson(sRet.getResult());
return gson.toJson(sRet.getResult());
}
}
@ -3588,4 +3598,65 @@ public class JdUtil {
}
return Collections.emptyList();
}
/**
* 销售订单的查询
* @param materialCode
* @return
*/
public static List<SalesOrderDTO> getSalesOrderList(String materialCode) {
K3CloudApi client = new K3CloudApi();
// 请求参数要求为json字符串
JsonObject json = new JsonObject();
json.addProperty("FormId", "SAL_SaleOrder");
json.addProperty("FieldKeys",
"FMaterialId.FNumber,FMaterialName,FBaseRemainOutQty");
// 创建过滤条件
JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject();
filterObject.addProperty("FieldName", "FMaterialId.FNumber");
filterObject.addProperty("Compare", "=");
filterObject.addProperty("Value", materialCode);
filterObject.addProperty("Left", "");
filterObject.addProperty("Right", "");
filterObject.addProperty("Logic", 0);
filterString.add(filterObject);
JsonObject filterObject1 = new JsonObject();
filterObject1.addProperty("FieldName", "FCloseStatus");
filterObject1.addProperty("Compare", "105");
filterObject1.addProperty("Value", "A");
filterObject1.addProperty("Left", "");
filterObject1.addProperty("Right", "");
filterObject1.addProperty("Logic", 0);
filterString.add(filterObject1);
json.add("FilterString", filterString);
json.addProperty("OrderString", "");
json.addProperty("TopRowCount", 0);
json.addProperty("StartRow", 0);
json.addProperty("Limit", 2000);
json.addProperty("SubSystemId", "");
String jsonData = json.toString();
List<SalesOrderDTO> plannedProcessList = null;
try {
// 调用接口
String resultJson = String.valueOf(client.billQuery(jsonData));
JsonArray jsonArray = new Gson().fromJson(resultJson, JsonArray.class);
// 使用 ObjectMapper JsonArray 转换为 List<PlannedProcessVo>
ObjectMapper objectMapper = new ObjectMapper();
plannedProcessList = objectMapper.readValue(jsonArray.toString(),
new TypeReference<List<SalesOrderDTO>>() {
});
} catch (Exception e) {
e.printStackTrace(); // 输出异常日志
}
return plannedProcessList; // 返回结果
}
}

View File

@ -126,7 +126,7 @@ public class updatePcessPlanConver {
for (Model model : numDTO) {
MainModel mainModel = new MainModel();
List<String> needUpDateFields = Arrays.asList(
"FEntity", "FSubEntity", "F_HBYT_RKCK.FName"
"FEntity", "FSubEntity", "F_HBYT_RKCK"
);
mainModel.setNeedUpDateFields(needUpDateFields);
mainModel.setDeleteEntry(false);

View File

@ -58,4 +58,5 @@ public interface IBomDetailsService {
List<BomDetails> selectByFNumberAndTotalWeight(String fnumber, String totalWeight);
List<BomDetails> selectByProjectNumber(String totalWeight);
List<BomDetails> selectMaterialZH(String totalWeight,String partNumber);
}

View File

@ -119,6 +119,7 @@ public interface IProcessRouteService {
boolean isAnTuDingGou(String materialCode);
List<ProcessRouteSelectDTO> getSelectProcessRoute(String materilCode);
List<ProcessRouteSelectDTO> getSelectStandProcessRoute(String materilCode);
//根据项目令号删除 材料bom 总装bom 工艺路线
R<Void> delByProjectCode(String productionOrderNo);

View File

@ -26,6 +26,32 @@ public class MssqlQueryService {
return jdbcTemplate.queryForList(sql);
}
/**
* 根据物料编码查询是否是VMI业务
* @param materialNumber 物料编码
* @return true:是VMI业务, false:
*/
@DS("sqlserver")
public boolean checkIfVmiBusiness(String materialNumber) {
String sql = "SELECT p.FISVMIBUSINESS " +
" FROM T_BD_MATERIAL m " +
" INNER JOIN T_BD_MATERIALPURCHASE p ON m.FMATERIALID = p.FMATERIALID " +
" WHERE m.FNUMBER = ?";
try {
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, materialNumber);
if (list != null && !list.isEmpty()) {
Object val = list.get(0).get("FISVMIBUSINESS");
if (val != null) {
String s = val.toString();
return "1".equals(s) || "true".equalsIgnoreCase(s);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@DS("sqlserver")
public List<KingdeeWorkCenterDataBo> getKingdeeProduceData(String workCenterName) {
String sql = "select t8.FBillNo as MoBillNo,t8.F_HBYT_SCLH as MoOrderNo, \n" +

View File

@ -246,6 +246,24 @@ public class BomDetailsServiceImpl implements IBomDetailsService {
return baseMapper.selectList(bomDetailsLambdaQueryWrapper);
}
/**
* 查询部件是否为组焊件
* @param totalWeight 总权重/版本号
* @param partNumber 部件编码
* @return 如果是组焊件返回包含该记录的列表否则返回空列表
*/
@Override
public List<BomDetails> selectMaterialZH(String totalWeight, String partNumber) {
// 你的逻辑fProcessIdNumber作为partNumbertotalWeightMaterial=组焊件
// 查询出数据 就代表他就是组焊件子件或者说需要走CK025
LambdaQueryWrapper<BomDetails> bq = new LambdaQueryWrapper<>();
bq.eq(BomDetails::getTotalWeight, totalWeight);
bq.eq(BomDetails::getPartNumber, partNumber);
bq.eq(BomDetails::getMaterial, "组焊件");
return baseMapper.selectList(bq);
}
public int loadMaterialPreservation(BomDetails bomDetails1) {
K3CloudApi client = new K3CloudApi();

View File

@ -1004,14 +1004,16 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
String code = processOrderPro.getProductionOrderNo();
log.info("开始处理项目PDF文件项目令号: {}", code);
// 3. 查询产品信息
// 3. 查询产品信息/system/orderPro/uploadPDF
List<FigureSave> fiseList = iFigureSaveService.selectByFid(id);
if (fiseList == null || fiseList.isEmpty()) {
return R.fail("项目下没有找到产品信息");
}
// 4. 构建本地根目录
String localBaseDir = "D:/2025/";
// 修正不再硬编码为 D:/2025/而是根据 code 动态构建与后续处理逻辑保持一致
String localBaseDir = buildWorkDirectory(code);
// 5. 清空本地目标文件夹
String targetDir = localBaseDir + code;
File targetDirectory = new File(targetDir);
@ -1029,9 +1031,46 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
// 6. 下载FTP文件到本地
log.info("开始从FTP下载文件...");
for (FigureSave figureSave : fiseList) {
String productionCode = figureSave.getProductionCode();
//解析出年份 从左往右第一个- 后面就是 年份25 =2025 26 就是2026
//如果这个是EY开头的那就取EY后面的数字 25 26 27 作为年份 2025 2026 2027
try {
String year = "2025"; // 默认2025
if (productionCode != null) {
if (productionCode.startsWith("EY")) {
// EY开头取第2位开始的两位 (索引2和3)
if (productionCode.length() >= 4) {
String yearShort = productionCode.substring(2, 4);
try {
Integer.parseInt(yearShort);
year = "20" + yearShort;
} catch (NumberFormatException e) {
// 解析失败保持默认
}
}
} else {
// 原有逻辑取第一个横杠后的两位
int firstDash = productionCode.indexOf("-");
if (firstDash >= 0 && firstDash + 3 <= productionCode.length()) {
String yearShort = productionCode.substring(firstDash + 1, firstDash + 3);
try {
Integer.parseInt(yearShort); // 验证是否为数字
year = "20" + yearShort;
} catch (NumberFormatException e) {
// 解析失败保持默认
}
}
}
}
// 拼接远程文件夹路径
String ftpPath = "/" + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF";
// 确保路径使用正斜杠且不包含本地盘符
String ftpPath = year + "/" + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF";
ftpPath = ftpPath.replace("\\", "/"); // 强制替换反斜杠
if (!ftpPath.startsWith("/")) {
ftpPath = "/" + ftpPath; // 确保以/开头表示从FTP根目录开始
}
// 拼接本地存储路径
String localPath = localBaseDir + code + "/" + figureSave.getFigureNumber().replace("/", "-") + "-PDF";
@ -1147,10 +1186,10 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
* PDF 按工作中心分类复制到目标目录 zip/工作中心/
*/
private void classifyPdfByWorkCenterToTarget(String productionOrderNo,String sourceDirPath, String code, String targetBaseDirPath) {
//按工作中心 分类时 查找是否有采购申请单 和生产订单
/* //按工作中心 分类时 查找是否有采购申请单 和生产订单
//获取这个项目的所有生产令号
List<String> orderList = JdUtil.getOrderNumberCollection(productionOrderNo);
List<String> cgorderList = JdUtil.getCaiGouOrderNumberList(productionOrderNo);
List<String> cgorderList = JdUtil.getCaiGouOrderNumberList(productionOrderNo);*/
File sourceDir = new File(sourceDirPath);
@ -1166,16 +1205,16 @@ public class ProcessOrderProServiceImpl implements IProcessOrderProService {
if (materialCode == null) continue;
String workCenter;
//如果这个文件的生产令号 生产令号列表 或者 采购申请单列表 可以分类到对应的文件夹中
/* //如果这个文件的生产令号 生产令号列表 或者 采购申请单列表 可以分类到对应的文件夹中
// 由于物料名近似如KP08和KP08.7使用equals精确匹配不使用contains
if (!orderList.contains(materialCode) && !cgorderList.contains(materialCode)) {
unmovedMaterials.add(materialCode);
workCenter = "未匹配订单";
} else {
} else {*/
workCenter = iProcessRouteService.getRouteCode(materialCode, code);
if (iProductionOrderService.isPurchas(productionOrderNo, materialCode)) workCenter = "外购件";
if (workCenter == null || workCenter.isEmpty()) workCenter = "无工段";
}
/*}*/
File workCenterDir = new File(targetBaseDirPath, workCenter);
if (!workCenterDir.exists()) workCenterDir.mkdirs();

View File

@ -19,8 +19,6 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.dto.BOMItem;
import com.ruoyi.system.domain.dto.BOMUploadResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.controller.ProcessRouteController;
import com.ruoyi.system.domain.*;
@ -55,7 +53,6 @@ import java.util.concurrent.*;
import java.util.stream.Collectors;
import static com.ruoyi.system.controller.BomDetailsController.*;
import static com.ruoyi.system.runner.JdUtil.getCaiGouOrderNumberList;
import static com.ruoyi.system.runner.JdUtil.updateCgOrder1;
import static com.ruoyi.system.runner.JsonConverter.createProcessModel;
import static com.ruoyi.system.runner.updatePcessPlanConver.updatePcessPlan1;
@ -70,8 +67,9 @@ import static com.ruoyi.system.runner.updatePcessPlanConver.updatePcessPlan2;
@RequiredArgsConstructor
@Service
public class ProcessRouteServiceImpl implements IProcessRouteService {
private final IMaterialTotalService iMaterialTotalService;
private final IImMaterialService imMaterialService;
private final IBomDetailsService iBomDetailsService;
private final MssqlQueryService sqlQueryService;
private final ProcessRouteMapper baseMapper;
private final BomDetailsMapper bomDetailsMapper;
private final IMaterialPropertiesService iMaterialPropertiesService;
@ -84,6 +82,17 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
private static final Logger log = LoggerFactory.getLogger(ProcessRouteController.class);
private static final Logger logger = LoggerFactory.getLogger(IProcessRouteService.class);
// 全局线程池核心线程10最大线程50存活60s有界队列
private static final ExecutorService GLOBAL_EXECUTOR = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行防止丢任务
);
// 单据锁 Map
private static final ConcurrentHashMap<String, Object> BILL_LOCK = new ConcurrentHashMap<>();
/**
* 查询工艺路线
*/
@ -335,7 +344,6 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
}
private String generateKey1(BomDetails bomDetail, ProcessRoute processRoute) {
return String.format("%s:%s:%s", processRoute.getMaterialCode(), processRoute.getMaterialName(), bomDetail.getName(), bomDetail.getPartNumber());
}
@ -1412,8 +1420,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
// 请求参数要求为json字符串
JsonObject json = new JsonObject();
json.addProperty("FormId", "SFC_OperationPlanning");
json.addProperty("FieldKeys",
"FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName");
json.addProperty("FieldKeys", "FBillNo,FOperNumber,FProcessId.FName,FOptCtrlCodeId.FName,FActivity1BaseQty,FOperPlanStartTime,FOperPlanFinishTime,FOperQty,FDepartmentId.FName,FWorkCenterId.FName,FOperDescription,FPlanStartTime,FPlanFinishTime,FMOQty,FOperUnitId.FName");
// 创建过滤条件
JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject();
@ -2031,7 +2038,7 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
json.addProperty("FieldKeys",
"FID,FSubEntity_FDetailID,FProductId.FNumber,FOperNumber,FEntity_FEntryID,FProcessId.FName,FSeqNumber,FSeqName,FPlanStartTime,FPlanFinishTime,"
+
"FOperPlanStartTime,FOperPlanFinishTime,FMONumber,F_HBYT_RKCK.FName");
"FOperPlanStartTime,FOperPlanFinishTime,FMONumber,F_HBYT_RKCK");
JsonArray filterString = new JsonArray();
JsonObject filterObject = new JsonObject();
@ -2115,7 +2122,11 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
fSubEntity.setFOperNumber(dto.getFOperNumber());
fSubEntity.setFOperPlanStartTime((dto.getFOperPlanStartTime()));
fSubEntity.setFOperPlanFinishTime(dto.getFOperPlanFinishTime());
fSubEntity.setRkckName(dto.getFRKCKFName());
FSubEntity.F_HBYT_RKCK rkck = new FSubEntity.F_HBYT_RKCK();
rkck.setFNUMBER(dto.getFRKCKFName());
fSubEntity.setF_HBYT_RKCK(rkck);
fEntity.getFSubEntity().add(fSubEntity);
}
@ -2601,20 +2612,71 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
*/
@Override
public List<String> updateProductionOrders(String rooteProdet, String cangKuNum) throws Exception {
// 1. 更新生产订单 (PRD_MO)
// 创建一个线程安全的List来收集结果
List<String> results = Collections.synchronizedList(new ArrayList<>());
List<PlanOrderVo> planOrderList = getSelectProceOrder1(rooteProdet);
for (PlanOrderVo planOrderVo : planOrderList) {
List<Future<?>> futures = new ArrayList<>();
// 1. 更新生产订单 (PRD_MO)
if (planOrderList != null && !planOrderList.isEmpty()) {
// 按单据号分组减少锁竞争和线程数
Map<String, List<PlanOrderVo>> groupedByBillNo = planOrderList.stream()
.collect(Collectors.groupingBy(PlanOrderVo::getFBillNo));
for (Map.Entry<String, List<PlanOrderVo>> entry : groupedByBillNo.entrySet()) {
String billNo = entry.getKey();
List<PlanOrderVo> orders = entry.getValue();
// 使用全局线程池每个单据号一个任务
Future<?> future = GLOBAL_EXECUTOR.submit(() -> {
// 获取锁对象防止跨请求并发
Object lock = BILL_LOCK.computeIfAbsent(billNo, k -> new Object());
synchronized (lock) {
try {
for (PlanOrderVo planOrderVo : orders) {
String currentCangKuNum = cangKuNum;
// 判断是否为组焊件
if (planOrderVo.getFmaterialidFnumber() != null) {
List<BomDetails> bomDetails = iBomDetailsService.selectMaterialZH(rooteProdet, planOrderVo.getFmaterialidFnumber());
if (bomDetails != null && !bomDetails.isEmpty()) {
currentCangKuNum = "CK025";
}
}
JdHuoZhu fidToProductionOrder = JdUtil.getFIDToProductionOrder(planOrderVo);
if (fidToProductionOrder != null) {
// 更新生产订单的仓库
JdUtil.updateOrder2(fidToProductionOrder, cangKuNum);
String result = JdUtil.updateOrder2(fidToProductionOrder, currentCangKuNum);
results.add("生产订单[" + planOrderVo.getFBillNo() + "]更新结果: " + result);
}
}
} catch (Exception e) {
log.error("更新生产订单失败: {}", billNo, e);
results.add("生产订单[" + billNo + "]更新失败: " + e.getMessage());
}
}
});
futures.add(future);
}
}
// 2. 更新工序计划单 (SFC_OperationPlanning)
// 将此逻辑也放入 Future 实现与生产订单更新的并行处理
Future<?> planUpdateFuture = GLOBAL_EXECUTOR.submit(() -> {
try {
List<Model> numDTOS = getSelecPlan(rooteProdet);
boolean needUpdate = false;
if (numDTOS != null) {
for (Model numDTO : numDTOS) {
String fProcessIdNumber = numDTO.getFProcessId_number();
String finalCurrentCangKuNum = cangKuNum;
// 判断是否为组焊件
List<BomDetails> bomDetails = iBomDetailsService.selectMaterialZH(rooteProdet, fProcessIdNumber);
if (bomDetails != null && !bomDetails.isEmpty()) {
// 这里假设 selectMaterialZH 返回的是匹配的记录
finalCurrentCangKuNum = "CK025";
}
List<FEntity> fEntityList = numDTO.getFEntity();
if (fEntityList != null && !fEntityList.isEmpty()) {
for (FEntity fEntity : fEntityList) {
@ -2622,34 +2684,126 @@ public class ProcessRouteServiceImpl implements IProcessRouteService {
if (fSubEntityList != null && !fSubEntityList.isEmpty()) {
for (FSubEntity fSubEntity : fSubEntityList) {
// 设置入库仓库
fSubEntity.setRkckName(cangKuNum);
FSubEntity.F_HBYT_RKCK rkck = new FSubEntity.F_HBYT_RKCK();
rkck.setFNUMBER(finalCurrentCangKuNum);
fSubEntity.setF_HBYT_RKCK(rkck);
needUpdate = true;
}
}
}
}
}
if (needUpdate) {
updatePcessPlan2(numDTOS);
RepoRet repoRet = updatePcessPlan2(numDTOS);
if (repoRet != null) {
results.add("工序计划单批量更新结果: " + (repoRet.isSuccessfully() ? "成功" : "失败"));
}
}
}
} catch (Exception e) {
log.error("更新工序计划单失败", e);
results.add("工序计划单更新失败: " + e.getMessage());
}
});
futures.add(planUpdateFuture);
// 等待所有任务生产订单 + 工序计划单完成
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
log.error("等待线程执行异常", e);
}
}
// 注意全局线程池不需要shutdown
return results;
}
return Collections.emptyList();
}
/**
* 更新采购申请仓库字段
*/
public List<String> updateCgOrders(String rooteProdet, String cangKuNum) throws Exception {
List<CgDTO> list = JdUtil.getCGOrderFBillNoList2(rooteProdet);
for (CgDTO orderBillNo : list) {
List<FidEntryIdDTO> fidEntryIdDTOS = JdUtil.queryFidAndEntryId(orderBillNo.getOrderBillNo());
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
// 创建一个线程安全的List来收集结果
List<String> results = Collections.synchronizedList(new ArrayList<>());
List<Future<?>> futures = new ArrayList<>();
// 按单据号分组减少锁竞争和线程数
Map<String, List<CgDTO>> groupedByBillNo = list.stream()
.collect(Collectors.groupingBy(CgDTO::getOrderBillNo));
for (Map.Entry<String, List<CgDTO>> entry : groupedByBillNo.entrySet()) {
String billNo = entry.getKey();
List<CgDTO> cgList = entry.getValue();
// 使用全局线程池每个单据号一个任务
Future<?> future = GLOBAL_EXECUTOR.submit(() -> {
// 获取锁对象防止跨请求并发
Object lock = BILL_LOCK.computeIfAbsent(billNo, k -> new Object());
synchronized (lock) {
try {
for (CgDTO cg : cgList) {
ImMaterial material = imMaterialService.selectByCodeAndName(cg.getMaterialCode(), cg.getMaterialName());
List<FidEntryIdDTO> fidEntryIdDTOS = JdUtil.queryFidAndEntryId(cg.getOrderBillNo());
if (fidEntryIdDTOS != null && !fidEntryIdDTOS.isEmpty()) {
for (FidEntryIdDTO fidEntryIdDTO : fidEntryIdDTOS) {
String s = updateCgOrder1(fidEntryIdDTO, cangKuNum);
String currentCangKuNum = cangKuNum;
String materialCode = cg.getMaterialCode();
if (materialCode != null) {
// VMI库
boolean result = sqlQueryService.checkIfVmiBusiness(materialCode);
if (material != null && result) {
continue;
}
// 015开头原材料入库
else if (materialCode.startsWith("015") || materialCode.startsWith("AB")) {
currentCangKuNum = "CK001";
}
// 毛坯库
else if (materialCode.startsWith("(M)")) {
currentCangKuNum = "CK002";
}
// 电器入库
/*else if (materialCode.startsWith("DK") || materialCode.startsWith("DC") || materialCode.startsWith("DP") || materialCode.startsWith("DT")
|| materialCode.startsWith("DQ") || materialCode.startsWith("DD") || materialCode.startsWith("DF") || materialCode.startsWith("DX")
|| materialCode.startsWith("DL") || materialCode.startsWith("DH") || materialCode.startsWith("DJ") || materialCode.startsWith("DZ") || materialCode.startsWith("DB")) {
currentCangKuNum = "CK006";
}*/
}
String resultStr = updateCgOrder1(fidEntryIdDTO, currentCangKuNum);
results.add(resultStr);
}
}
}
} catch (Exception e) {
log.error("更新采购订单失败: {}", billNo, e);
results.add("更新失败: " + billNo + " - " + e.getMessage());
}
}
});
futures.add(future);
}
return null;
// 等待所有任务完成
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
log.error("等待线程执行异常", e);
}
}
return results;
}
}