From 0268f58a18bfcf061389387ef5322bf11aece154 Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期二, 28 四月 2026 14:22:54 +0800
Subject: [PATCH] Merge branch 'dev_New_pro' of http://114.132.189.42:9002/r/product-inventory-management-after into dev_New_pro
---
src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml | 21
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java | 85 +
src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java | 24
src/main/resources/application-new-pro.yml | 268 +++++++
pom.xml | 4
src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java | 19
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java | 41
src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java | 32
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java | 12
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java | 12
src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java | 5
doc/20260428_create_table_ai_chat_session.sql | 16
src/main/resources/application-dev.yml | 2
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java | 15
src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java | 7
src/main/java/com/ruoyi/ai/service/AiChatSessionService.java | 22
src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java | 3
src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java | 6
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java | 11
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java | 794 +++++++++++++-------
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java | 17
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java | 17
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java | 191 +++++
src/main/resources/application-dev-pro.yml | 9
src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java | 1
src/main/java/com/ruoyi/ai/pojo/AiChatSession.java | 34
src/main/resources/mapper/production/ProductionOrderPickMapper.xml | 5
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java | 136 +++
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java | 35
src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java | 5
src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java | 13
src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java | 117 +++
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java | 248 ++++++
src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java | 19
src/main/resources/application.yml | 3
35 files changed, 1,883 insertions(+), 366 deletions(-)
diff --git a/doc/20260428_create_table_ai_chat_session.sql b/doc/20260428_create_table_ai_chat_session.sql
new file mode 100644
index 0000000..f490376
--- /dev/null
+++ b/doc/20260428_create_table_ai_chat_session.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS `ai_chat_session` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+ `memory_id` varchar(64) NOT NULL COMMENT '浼氳瘽ID',
+ `user_id` bigint NOT NULL COMMENT '鐢ㄦ埛ID',
+ `tenant_id` bigint DEFAULT NULL COMMENT '绉熸埛ID',
+ `title` varchar(128) DEFAULT NULL COMMENT '浼氳瘽鏍囬',
+ `last_message` varchar(512) DEFAULT NULL COMMENT '鏈�鍚庝竴鏉℃秷鎭�',
+ `message_count` int NOT NULL DEFAULT 0 COMMENT '娑堟伅鏁伴噺',
+ `last_chat_time` datetime DEFAULT NULL COMMENT '鏈�鍚庤亰澶╂椂闂�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_ai_chat_session_memory_id` (`memory_id`),
+ KEY `idx_ai_chat_session_user_tenant` (`user_id`, `tenant_id`),
+ KEY `idx_ai_chat_session_last_chat_time` (`last_chat_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI鍘嗗彶浼氳瘽鍏冩暟鎹〃';
diff --git a/pom.xml b/pom.xml
index b645d4c..e840acd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,6 +157,10 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-reactor</artifactId>
+ </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
index 6e1a948..daaaf74 100644
--- a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -16,6 +16,8 @@
private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{8,}\\b");
private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?(\\d{1,2})鏉�");
+ private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
+ private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
private final ApproveTodoTools approveTodoTools;
@@ -23,7 +25,7 @@
this.approveTodoTools = approveTodoTools;
}
- public String tryExecute(String message) {
+ public String tryExecute(String memoryId, String message) {
if (!StringUtils.hasText(message)) {
return null;
}
@@ -32,46 +34,52 @@
String approveId = extractApproveId(text);
if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮")) {
- return approveTodoTools.getTodoStats();
+ return approveTodoTools.getTodoStats(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ extractTimeRange(text)
+ );
}
if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.getTodoProgress(approveId)
+ ? approveTodoTools.getTodoProgress(memoryId, approveId)
: missingApproveId("todo_progress", "鏌ヨ瀹℃壒杩涘害闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "璇︽儏", "鏄庣粏") && !containsAny(text, "鍒楄〃")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.getTodoDetail(approveId)
+ ? approveTodoTools.getTodoDetail(memoryId, approveId)
: missingApproveId("todo_detail", "鏌ヨ瀹℃壒璇︽儏闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.cancelReviewTodo(approveId, extractTail(text, "鍘熷洜"))
+ ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractTail(text, "鍘熷洜"))
: missingApproveId("cancel_review_action", "鍙栨秷瀹℃牳闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "鍒犻櫎")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.deleteTodo(approveId)
+ ? approveTodoTools.deleteTodo(memoryId, approveId)
: missingApproveId("delete_action", "鍒犻櫎瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "椹冲洖", "鎷掔粷")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.reviewTodo(approveId, "reject", extractTail(text, "鍘熷洜"))
+ ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractTail(text, "鍘熷洜"))
: missingApproveId("review_action", "椹冲洖瀹℃壒闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "瀹℃牳閫氳繃", "瀹℃壒閫氳繃", "閫氳繃瀹℃壒", "鍚屾剰瀹℃壒", "瀹℃壒鍚屾剰")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"))
+ ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"))
: missingApproveId("review_action", "瀹℃壒閫氳繃闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (StringUtils.hasText(approveId)
&& containsAny(text, "閫氳繃", "鍚屾剰")
&& !containsAny(text, "鏈�氳繃", "閫氳繃鐜�", "瀹℃壒閫氳繃鐜�", "瀹℃牳閫氳繃鐜�")) {
- return approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"));
+ return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"));
}
if (containsAny(text, "淇敼")) {
return StringUtils.hasText(approveId)
? approveTodoTools.updateTodo(
+ memoryId,
approveId,
extractValue(text, "鏍囬"),
extractDateValue(text, "寮�濮嬫棩鏈�"),
@@ -84,6 +92,7 @@
}
if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒")) {
return approveTodoTools.listTodos(
+ memoryId,
extractStatus(text),
extractApproveType(text),
extractKeyword(text),
@@ -165,33 +174,75 @@
.replace("寰呭姙", "")
.replace("鍒楄〃", "")
.replace("鍓�10鏉�", "")
- .replace("鍓�20鏉�", "")
+ .replace("鏈�杩�10鏉�", "")
.trim();
return cleaned.length() >= 2 ? cleaned : null;
}
private String extractValue(String text, String fieldName) {
- Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)([^锛屻��,锛�;\\s]+)").matcher(text);
+ Pattern pattern = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗鏄�)?[:锛歖?[\\s]*([^,锛屻�傦紱;\\s]+)");
+ Matcher matcher = pattern.matcher(text);
return matcher.find() ? matcher.group(2) : null;
}
private String extractDateValue(String text, String fieldName) {
- Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{4}-\\d{2}-\\d{2})").matcher(text);
- return matcher.find() ? matcher.group(2) : null;
+ int index = text.indexOf(fieldName);
+ if (index < 0) {
+ return null;
+ }
+ Matcher matcher = DATE_PATTERN.matcher(text.substring(index));
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ private String extractStartDate(String text) {
+ Matcher matcher = DATE_PATTERN.matcher(text);
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ private String extractEndDate(String text) {
+ Matcher matcher = DATE_PATTERN.matcher(text);
+ if (!matcher.find()) {
+ return null;
+ }
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ private String extractTimeRange(String text) {
+ if (containsAny(text, "浠婂ぉ", "鏄ㄦ棩", "鏄ㄥぉ", "鏈懆", "涓婂懆", "鏈湀", "涓婃湀", "鏈勾", "浠婂勾", "鍘诲勾")) {
+ return text;
+ }
+ if (Pattern.compile("杩慭\d+(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text).find()) {
+ return text;
+ }
+ if (Pattern.compile("鏈�杩慭\d+(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text).find()) {
+ return text;
+ }
+ if (text.contains("鍒�") || text.contains("鑷�")) {
+ return text;
+ }
+ return null;
}
private Integer extractIntegerValue(String text, String fieldName) {
- Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{1,2})").matcher(text);
+ if (!text.contains(fieldName)) {
+ return null;
+ }
+ Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗鏄�)?[:锛歖?[\\s]*(\\d{1,2})").matcher(text);
return matcher.find() ? Integer.parseInt(matcher.group(2)) : null;
}
private BigDecimal extractBigDecimalValue(String text, String fieldName) {
- Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d+(\\.\\d+)?)").matcher(text);
- return matcher.find() ? new BigDecimal(matcher.group(2)) : null;
+ int index = text.indexOf(fieldName);
+ if (index < 0) {
+ return null;
+ }
+ Matcher matcher = NUMBER_PATTERN.matcher(text.substring(index));
+ return matcher.find() ? new BigDecimal(matcher.group(1)) : null;
}
private String extractTail(String text, String key) {
- Matcher matcher = Pattern.compile(key + "(鏄瘄涓簗锛殀:)?(.+)").matcher(text);
+ Pattern pattern = Pattern.compile(key + "(鏄瘄涓�)?[:锛歖?[\\s]*(.+)");
+ Matcher matcher = pattern.matcher(text);
return matcher.find() ? matcher.group(2).trim() : null;
}
diff --git a/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java b/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java
new file mode 100644
index 0000000..131a9b7
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java
@@ -0,0 +1,24 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.spring.AiService;
+import reactor.core.publisher.Flux;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+
+@AiService(
+ wiringMode = EXPLICIT,
+ streamingChatModel = "qwenStreamingChatModel",
+ chatMemoryProvider = "chatMemoryProvider"
+)
+public interface FileAnalyzeAgent {
+
+ @SystemMessage("""
+ 浣犳槸浼佷笟鏂囨。鍒嗘瀽鍔╂墜銆�
+ 璇蜂弗鏍煎熀浜庣敤鎴锋彁渚涚殑鏂囦欢鍐呭杩涜鍒嗘瀽锛岃緭鍑鸿缁撴瀯鍖栥�佸噯纭�佺畝娲併��
+ 鑻ユ枃浠跺唴瀹逛笉瓒充互鏀寔缁撹锛屾槑纭寚鍑衡�滀笉瓒充俊鎭�濆苟缁欏嚭闇�瑕佽ˉ鍏呯殑鏁版嵁椤广��
+ """)
+ Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
diff --git a/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java b/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
index f266a17..03d933e 100644
--- a/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
+++ b/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
@@ -1,15 +1,24 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
+import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
+import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+
+import java.util.Collections;
+import java.util.List;
/**
* @author :yys
@@ -26,8 +35,8 @@
@Autowired
private EmbeddingModel embeddingModel;
-// @Value("${knowledge.one}")
-// private String one;
+ @Value("${knowledge.one}")
+ private String one;
//
// @Value("${knowledge.two}")
// private String two;
@@ -48,12 +57,12 @@
// ContentRetriever contentRetrieverXiaozhi() {
// //浣跨敤FileSystemDocumentLoader璇诲彇鎸囧畾鐩綍涓嬬殑鐭ヨ瘑搴撴枃妗�
// //骞朵娇鐢ㄩ粯璁ょ殑鏂囨。瑙f瀽鍣ㄥ鏂囨。杩涜瑙f瀽
-//// Document document1 = FileSystemDocumentLoader.loadDocument(one);
+// Document document1 = FileSystemDocumentLoader.loadDocument(one);
//// Document document2 = FileSystemDocumentLoader.loadDocument(two);
//// Document document3 = FileSystemDocumentLoader.loadDocument(three);
//// List<Document> documents = Arrays.asList(document1, document2, document3);
//
-//// List<Document> documents = Collections.singletonList(document1);
+// List<Document> documents = Collections.singletonList(document1);
//// 2. 灏嗘暟鎹簱鏁版嵁杞负LangChain4j鐨凞ocument瀵硅薄
//// List<Document> documents = new ArrayList<>();
//
diff --git a/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java b/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
new file mode 100644
index 0000000..3a5877b
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
@@ -0,0 +1,35 @@
+package com.ruoyi.ai.context;
+
+import com.ruoyi.framework.security.LoginUser;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class AiSessionUserContext {
+
+ private final Map<String, LoginUser> loginUserByMemoryId = new ConcurrentHashMap<>();
+
+ public void bind(String memoryId, LoginUser loginUser) {
+ if (!StringUtils.hasText(memoryId) || loginUser == null) {
+ return;
+ }
+ loginUserByMemoryId.put(memoryId, loginUser);
+ }
+
+ public LoginUser get(String memoryId) {
+ if (!StringUtils.hasText(memoryId)) {
+ return null;
+ }
+ return loginUserByMemoryId.get(memoryId);
+ }
+
+ public void remove(String memoryId) {
+ if (!StringUtils.hasText(memoryId)) {
+ return;
+ }
+ loginUserByMemoryId.remove(memoryId);
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
index e1223cc..eac43b2 100644
--- a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -2,35 +2,161 @@
import com.ruoyi.ai.assistant.ApproveTodoAgent;
import com.ruoyi.ai.assistant.ApproveTodoIntentExecutor;
+import com.ruoyi.ai.assistant.FileAnalyzeAgent;
import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.service.AiFileTextExtractor;
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.framework.web.controller.BaseController;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.UUID;
@Tag(name = "鍗忓悓鍔炲叕鍔╂墜")
@RestController
@RequestMapping("/xiaozhi")
-public class XiaozhiController {
+public class XiaozhiController extends BaseController {
+
+ private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::";
private final ApproveTodoAgent approveTodoAgent;
private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
+ private final FileAnalyzeAgent fileAnalyzeAgent;
+ private final AiSessionUserContext aiSessionUserContext;
+ private final MongoChatMemoryStore mongoChatMemoryStore;
+ private final AiFileTextExtractor aiFileTextExtractor;
+ private final AiChatSessionService aiChatSessionService;
- public XiaozhiController(ApproveTodoAgent approveTodoAgent, ApproveTodoIntentExecutor approveTodoIntentExecutor) {
+ public XiaozhiController(ApproveTodoAgent approveTodoAgent,
+ ApproveTodoIntentExecutor approveTodoIntentExecutor,
+ FileAnalyzeAgent fileAnalyzeAgent,
+ AiSessionUserContext aiSessionUserContext,
+ MongoChatMemoryStore mongoChatMemoryStore,
+ AiFileTextExtractor aiFileTextExtractor,
+ AiChatSessionService aiChatSessionService) {
this.approveTodoAgent = approveTodoAgent;
this.approveTodoIntentExecutor = approveTodoIntentExecutor;
+ this.fileAnalyzeAgent = fileAnalyzeAgent;
+ this.aiSessionUserContext = aiSessionUserContext;
+ this.mongoChatMemoryStore = mongoChatMemoryStore;
+ this.aiFileTextExtractor = aiFileTextExtractor;
+ this.aiChatSessionService = aiChatSessionService;
}
@Operation(summary = "瀵硅瘽")
@PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
public Flux<String> chat(@RequestBody ChatForm chatForm) {
- String directResponse = approveTodoIntentExecutor.tryExecute(chatForm.getMessage());
- if (directResponse != null) {
+ if (!StringUtils.hasText(chatForm.getMemoryId())) {
+ return Flux.just("memoryId涓嶈兘涓虹┖");
+ }
+ if (!StringUtils.hasText(chatForm.getMessage())) {
+ return Flux.just("message涓嶈兘涓虹┖");
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ String memoryId = chatForm.getMemoryId();
+ String userMessage = chatForm.getMessage();
+
+ aiSessionUserContext.bind(memoryId, loginUser);
+ aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
+
+ String directResponse = approveTodoIntentExecutor.tryExecute(memoryId, userMessage);
+ if (StringUtils.isNotEmpty(directResponse)) {
+ mongoChatMemoryStore.appendMessages(
+ memoryId,
+ List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
+ );
+ aiChatSessionService.refreshSessionStats(memoryId, loginUser);
return Flux.just(directResponse);
}
- return approveTodoAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
+
+ return approveTodoAgent.chat(memoryId, userMessage)
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
+ }
+
+ @Operation(summary = "涓婁紶鏂囦欢鍒嗘瀽")
+ @PostMapping(value = "/analyze-file", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8")
+ public Flux<String> analyzeFile(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "message", required = false) String message,
+ @RequestParam(value = "memoryId", required = false) String memoryId) {
+ String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
+ String finalMemoryId = rawMemoryId.startsWith(FILE_ANALYZE_MEMORY_PREFIX)
+ ? rawMemoryId
+ : FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
+
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ aiSessionUserContext.bind(finalMemoryId, loginUser);
+
+ String finalMessage = StringUtils.hasText(message) ? message : "璇峰垎鏋愯繖涓枃浠剁殑鏍稿績鍐呭骞剁粰鍑烘�荤粨";
+ String fileText;
+ try {
+ fileText = aiFileTextExtractor.extractText(file);
+ } catch (IllegalArgumentException ex) {
+ return Flux.just(ex.getMessage());
+ } catch (IOException ex) {
+ return Flux.just("鏂囦欢璇诲彇澶辫触");
+ }
+
+ if (!StringUtils.hasText(fileText)) {
+ return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
+ }
+
+ String limitedContent = fileText.length() > 12000 ? fileText.substring(0, 12000) : fileText;
+ String userPrompt = "浣犲皢鍒嗘瀽鐢ㄦ埛涓婁紶鐨勬枃浠跺唴瀹广�俓n"
+ + "鏂囦欢鍚�: " + file.getOriginalFilename() + "\n"
+ + "鐢ㄦ埛闂: " + finalMessage + "\n"
+ + "鏂囦欢鍐呭濡備笅:\n"
+ + limitedContent;
+
+ aiChatSessionService.touchSession(finalMemoryId, loginUser, "鏂囦欢鍒嗘瀽: " + finalMessage);
+
+ return Flux.defer(() -> fileAnalyzeAgent.chat(finalMemoryId, userPrompt))
+ .onErrorResume(NoSuchElementException.class, ex -> {
+ // DashScope 鍦ㄥ巻鍙叉秷鎭吋瀹瑰満鏅笅鍙兘鎶� NoSuchElementException锛屾竻绌哄悗閲嶈瘯涓�娆�
+ mongoChatMemoryStore.deleteMessages(finalMemoryId);
+ return fileAnalyzeAgent.chat(finalMemoryId, userPrompt);
+ })
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ }
+
+ @Operation(summary = "浼氳瘽鍒楄〃")
+ @GetMapping("/history/sessions")
+ public AjaxResult listSessions() {
+ return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
+ }
+
+ @Operation(summary = "浼氳瘽娑堟伅")
+ @GetMapping("/history/messages/{memoryId}")
+ public AjaxResult listMessages(@PathVariable String memoryId) {
+ return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
+ }
+
+ @Operation(summary = "鍒犻櫎浼氳瘽")
+ @DeleteMapping("/history/{memoryId}")
+ public AjaxResult deleteSession(@PathVariable String memoryId) {
+ aiSessionUserContext.remove(memoryId);
+ return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
}
}
diff --git a/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java b/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
new file mode 100644
index 0000000..9242b3e
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
@@ -0,0 +1,15 @@
+package com.ruoyi.ai.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiChatMessageDto {
+
+ private String role;
+
+ private String content;
+}
diff --git a/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java b/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java
new file mode 100644
index 0000000..f62f3b5
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java
@@ -0,0 +1,19 @@
+package com.ruoyi.ai.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AiChatSessionDto {
+
+ private String memoryId;
+
+ private String title;
+
+ private String lastMessage;
+
+ private Integer messageCount;
+
+ private Date lastChatTime;
+}
diff --git a/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java b/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java
new file mode 100644
index 0000000..8983396
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java
@@ -0,0 +1,7 @@
+package com.ruoyi.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.ai.pojo.AiChatSession;
+
+public interface AiChatSessionMapper extends BaseMapper<AiChatSession> {
+}
diff --git a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
index 90f1e02..0a239d8 100644
--- a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
+++ b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -5,23 +5,26 @@
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
-/**
- * @author :yys
- * @date : 2025/5/2 19:13
- */
+import java.util.Date;
+
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("chat_messages")
public class ChatMessages {
- //鍞竴鏍囪瘑锛屾槧灏勫埌 MongoDB 鏂囨。鐨� _id 瀛楁
+
@Id
private ObjectId id;
- private String messageId;
+ @Indexed(unique = true)
+ private String memoryId;
- private String content; //瀛樺偍褰撳墠鑱婂ぉ璁板綍鍒楄〃鐨刯son瀛楃涓�
+ private String content;
+ private Date createTime;
+
+ private Date updateTime;
}
diff --git a/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java b/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java
new file mode 100644
index 0000000..cc289f8
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java
@@ -0,0 +1,34 @@
+package com.ruoyi.ai.pojo;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("ai_chat_session")
+public class AiChatSession {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String memoryId;
+
+ private Long userId;
+
+ private Long tenantId;
+
+ private String title;
+
+ private String lastMessage;
+
+ private Integer messageCount;
+
+ private Date lastChatTime;
+
+ private Date createTime;
+
+ private Date updateTime;
+}
diff --git a/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java b/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java
new file mode 100644
index 0000000..ef47e03
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java
@@ -0,0 +1,22 @@
+package com.ruoyi.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.ai.dto.AiChatMessageDto;
+import com.ruoyi.ai.dto.AiChatSessionDto;
+import com.ruoyi.ai.pojo.AiChatSession;
+import com.ruoyi.framework.security.LoginUser;
+
+import java.util.List;
+
+public interface AiChatSessionService extends IService<AiChatSession> {
+
+ void touchSession(String memoryId, LoginUser loginUser, String userMessage);
+
+ void refreshSessionStats(String memoryId, LoginUser loginUser);
+
+ List<AiChatSessionDto> listCurrentUserSessions(LoginUser loginUser);
+
+ List<AiChatMessageDto> listCurrentUserMessages(String memoryId, LoginUser loginUser);
+
+ boolean deleteCurrentUserSession(String memoryId, LoginUser loginUser);
+}
diff --git a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
new file mode 100644
index 0000000..82cf1eb
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
@@ -0,0 +1,117 @@
+package com.ruoyi.ai.service;
+
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@Component
+public class AiFileTextExtractor {
+
+ private static final long MAX_FILE_SIZE = 10L * 1024 * 1024;
+
+ public String extractText(MultipartFile file) throws IOException {
+ if (file == null || file.isEmpty()) {
+ throw new IllegalArgumentException("鏂囦欢涓嶈兘涓虹┖");
+ }
+ if (file.getSize() > MAX_FILE_SIZE) {
+ throw new IllegalArgumentException("鏂囦欢杩囧ぇ锛岃鎺у埗鍦�10MB浠ュ唴");
+ }
+
+ String filename = file.getOriginalFilename();
+ String ext = getExtension(filename);
+ byte[] bytes = file.getBytes();
+
+ if (isPlainText(ext)) {
+ return decodeText(bytes);
+ }
+ if ("docx".equals(ext)) {
+ return extractDocx(bytes);
+ }
+ if ("xlsx".equals(ext)) {
+ return extractXlsx(bytes);
+ }
+ if ("xls".equals(ext)) {
+ return extractXls(bytes);
+ }
+ throw new IllegalArgumentException("鏆備笉鏀寔璇ユ枃浠剁被鍨�: " + ext);
+ }
+
+ private String extractDocx(byte[] bytes) throws IOException {
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ XWPFDocument document = new XWPFDocument(inputStream);
+ XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
+ return extractor.getText();
+ }
+ }
+
+ private String extractXlsx(byte[] bytes) throws IOException {
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
+ return extractWorkbook(workbook);
+ }
+ }
+
+ private String extractXls(byte[] bytes) throws IOException {
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ HSSFWorkbook workbook = new HSSFWorkbook(inputStream)) {
+ return extractWorkbook(workbook);
+ }
+ }
+
+ private String extractWorkbook(org.apache.poi.ss.usermodel.Workbook workbook) {
+ StringBuilder text = new StringBuilder();
+ DataFormatter formatter = new DataFormatter();
+ for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
+ Sheet sheet = workbook.getSheetAt(i);
+ text.append("Sheet: ").append(sheet.getSheetName()).append("\n");
+ for (Row row : sheet) {
+ short lastCellNum = row.getLastCellNum();
+ if (lastCellNum <= 0) {
+ text.append("\n");
+ continue;
+ }
+ for (int c = 0; c < lastCellNum; c++) {
+ String cellText = formatter.formatCellValue(row.getCell(c));
+ text.append(cellText);
+ if (c < lastCellNum - 1) {
+ text.append('\t');
+ }
+ }
+ text.append('\n');
+ }
+ }
+ return text.toString();
+ }
+
+ private String decodeText(byte[] bytes) {
+ String utf8 = new String(bytes, StandardCharsets.UTF_8);
+ if (utf8.contains("锟�")) {
+ return new String(bytes, java.nio.charset.Charset.forName("GBK"));
+ }
+ return utf8;
+ }
+
+ private String getExtension(String filename) {
+ if (!StringUtils.hasText(filename) || !filename.contains(".")) {
+ return "";
+ }
+ return filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+ }
+
+ private boolean isPlainText(String ext) {
+ return StringUtils.inStringIgnoreCase(ext,
+ "txt", "md", "markdown", "json", "xml", "yaml", "yml", "csv", "log", "properties",
+ "java", "js", "ts", "vue", "html", "css", "sql", "py", "go", "sh", "bat");
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java b/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
new file mode 100644
index 0000000..6d8c945
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
@@ -0,0 +1,191 @@
+package com.ruoyi.ai.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.ai.dto.AiChatMessageDto;
+import com.ruoyi.ai.dto.AiChatSessionDto;
+import com.ruoyi.ai.mapper.AiChatSessionMapper;
+import com.ruoyi.ai.pojo.AiChatSession;
+import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.data.message.UserMessage;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AiChatSessionServiceImpl extends ServiceImpl<AiChatSessionMapper, AiChatSession> implements AiChatSessionService {
+
+ private static final int TITLE_MAX_LENGTH = 40;
+ private static final int LAST_MESSAGE_MAX_LENGTH = 300;
+
+ private final MongoChatMemoryStore mongoChatMemoryStore;
+
+ @Override
+ public void touchSession(String memoryId, LoginUser loginUser, String userMessage) {
+ if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+ return;
+ }
+ Date now = new Date();
+ AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+ if (session == null) {
+ AiChatSession add = new AiChatSession();
+ add.setMemoryId(memoryId);
+ add.setUserId(loginUser.getUserId());
+ add.setTenantId(loginUser.getTenantId());
+ add.setTitle(buildTitle(userMessage));
+ add.setLastMessage(trimText(userMessage, LAST_MESSAGE_MAX_LENGTH));
+ add.setMessageCount(0);
+ add.setLastChatTime(now);
+ add.setCreateTime(now);
+ add.setUpdateTime(now);
+ save(add);
+ return;
+ }
+
+ AiChatSession update = new AiChatSession();
+ update.setId(session.getId());
+ if (!StringUtils.hasText(session.getTitle())) {
+ update.setTitle(buildTitle(userMessage));
+ }
+ update.setLastMessage(trimText(userMessage, LAST_MESSAGE_MAX_LENGTH));
+ update.setLastChatTime(now);
+ update.setUpdateTime(now);
+ updateById(update);
+ }
+
+ @Override
+ public void refreshSessionStats(String memoryId, LoginUser loginUser) {
+ if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+ return;
+ }
+ AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+ if (session == null) {
+ return;
+ }
+ List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
+ AiChatSession update = new AiChatSession();
+ update.setId(session.getId());
+ update.setMessageCount(messages.size());
+ update.setLastMessage(trimText(lastMessageText(messages), LAST_MESSAGE_MAX_LENGTH));
+ update.setLastChatTime(new Date());
+ update.setUpdateTime(new Date());
+ updateById(update);
+ }
+
+ @Override
+ public List<AiChatSessionDto> listCurrentUserSessions(LoginUser loginUser) {
+ if (loginUser == null || loginUser.getUserId() == null) {
+ return new LinkedList<>();
+ }
+ LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+ .eq(AiChatSession::getUserId, loginUser.getUserId())
+ .orderByDesc(AiChatSession::getLastChatTime);
+ applyTenantCondition(queryWrapper, loginUser.getTenantId());
+ return list(queryWrapper).stream().map(item -> {
+ AiChatSessionDto dto = new AiChatSessionDto();
+ dto.setMemoryId(item.getMemoryId());
+ dto.setTitle(item.getTitle());
+ dto.setLastMessage(item.getLastMessage());
+ dto.setMessageCount(item.getMessageCount());
+ dto.setLastChatTime(item.getLastChatTime());
+ return dto;
+ }).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<AiChatMessageDto> listCurrentUserMessages(String memoryId, LoginUser loginUser) {
+ if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+ return new LinkedList<>();
+ }
+ AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+ if (session == null) {
+ return new LinkedList<>();
+ }
+ List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
+ return messages.stream().map(this::convertMessage).collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean deleteCurrentUserSession(String memoryId, LoginUser loginUser) {
+ if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+ return false;
+ }
+ LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+ .eq(AiChatSession::getMemoryId, memoryId)
+ .eq(AiChatSession::getUserId, loginUser.getUserId());
+ applyTenantCondition(queryWrapper, loginUser.getTenantId());
+ boolean removed = remove(queryWrapper);
+ mongoChatMemoryStore.deleteMessages(memoryId);
+ return removed;
+ }
+
+ private AiChatSession getSession(String memoryId, Long userId, Long tenantId) {
+ LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+ .eq(AiChatSession::getMemoryId, memoryId)
+ .eq(AiChatSession::getUserId, userId);
+ applyTenantCondition(queryWrapper, tenantId);
+ return getOne(queryWrapper, false);
+ }
+
+ private void applyTenantCondition(LambdaQueryWrapper<AiChatSession> queryWrapper, Long tenantId) {
+ if (tenantId == null) {
+ queryWrapper.isNull(AiChatSession::getTenantId);
+ return;
+ }
+ queryWrapper.eq(AiChatSession::getTenantId, tenantId);
+ }
+
+ private String buildTitle(String userMessage) {
+ if (!StringUtils.hasText(userMessage)) {
+ return "鏂颁細璇�";
+ }
+ return trimText(userMessage, TITLE_MAX_LENGTH);
+ }
+
+ private String trimText(String text, int maxLength) {
+ if (!StringUtils.hasText(text)) {
+ return "";
+ }
+ String source = text.trim();
+ if (source.length() <= maxLength) {
+ return source;
+ }
+ return source.substring(0, maxLength) + "...";
+ }
+
+ private String lastMessageText(List<ChatMessage> messages) {
+ if (messages == null || messages.isEmpty()) {
+ return "";
+ }
+ ChatMessage lastMessage = messages.get(messages.size() - 1);
+ return convertMessage(lastMessage).getContent();
+ }
+
+ private AiChatMessageDto convertMessage(ChatMessage message) {
+ if (message instanceof UserMessage userMessage) {
+ return new AiChatMessageDto("user", userMessage.singleText());
+ }
+ if (message instanceof AiMessage aiMessage) {
+ return new AiChatMessageDto("assistant", aiMessage.text());
+ }
+ if (message instanceof SystemMessage systemMessage) {
+ return new AiChatMessageDto("system", systemMessage.text());
+ }
+ if (message instanceof ToolExecutionResultMessage toolExecutionResultMessage) {
+ return new AiChatMessageDto("tool", toolExecutionResultMessage.text());
+ }
+ return new AiChatMessageDto("unknown", String.valueOf(message));
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
index e94bf73..e88f0e9 100644
--- a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
+++ b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -5,47 +5,58 @@
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
+import java.util.Date;
import java.util.LinkedList;
import java.util.List;
-/**
- * @author :yys
- * @date : 2025/5/2 19:18
- */
@Component
+@RequiredArgsConstructor
public class MongoChatMemoryStore implements ChatMemoryStore {
- @Autowired
- private MongoTemplate mongoTemplate;
+ private final MongoTemplate mongoTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
- Criteria criteria = Criteria.where("memoryId").is(memoryId);
- Query query = new Query(criteria);
+ Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
- if(chatMessages == null) return new LinkedList<>();
+ if (chatMessages == null || chatMessages.getContent() == null) {
+ return new LinkedList<>();
+ }
return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
}
+
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
- Criteria criteria = Criteria.where("memoryId").is(memoryId);
- Query query = new Query(criteria);
+ String memoryIdValue = memoryIdString(memoryId);
+ Query query = Query.query(Criteria.where("memoryId").is(memoryIdValue));
Update update = new Update();
+ update.set("memoryId", memoryIdValue);
update.set("content", ChatMessageSerializer.messagesToJson(messages));
- //鏍规嵁query鏉′欢鑳芥煡璇㈠嚭鏂囨。锛屽垯淇敼鏂囨。锛涘惁鍒欐柊澧炴枃妗�
+ update.set("updateTime", new Date());
+ update.setOnInsert("createTime", new Date());
mongoTemplate.upsert(query, update, ChatMessages.class);
}
+
@Override
public void deleteMessages(Object memoryId) {
- Criteria criteria = Criteria.where("memoryId").is(memoryId);
- Query query = new Query(criteria);
+ Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
mongoTemplate.remove(query, ChatMessages.class);
}
+
+ public void appendMessages(Object memoryId, List<ChatMessage> appendList) {
+ List<ChatMessage> messages = new LinkedList<>(getMessages(memoryId));
+ messages.addAll(appendList);
+ updateMessages(memoryId, messages);
+ }
+
+ private String memoryIdString(Object memoryId) {
+ return memoryId == null ? "" : memoryId.toString();
+ }
}
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
index 431b848..8215ce8 100644
--- a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -2,25 +2,43 @@
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.approve.mapper.ApproveLogMapper;
import com.ruoyi.approve.mapper.ApproveNodeMapper;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveLog;
import com.ruoyi.approve.pojo.ApproveNode;
import com.ruoyi.approve.pojo.ApproveProcess;
+import com.ruoyi.approve.service.IApproveNodeService;
+import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.security.LoginUser;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
+import java.io.IOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.regex.Matcher;
import java.util.stream.Collectors;
@Component
@@ -34,93 +52,103 @@
private final ApproveProcessMapper approveProcessMapper;
private final ApproveNodeMapper approveNodeMapper;
private final ApproveLogMapper approveLogMapper;
+ private final IApproveNodeService approveNodeService;
+ private final ApproveProcessServiceImpl approveProcessService;
+ private final AiSessionUserContext aiSessionUserContext;
- public ApproveTodoTools(
- ApproveProcessMapper approveProcessMapper,
- ApproveNodeMapper approveNodeMapper,
- ApproveLogMapper approveLogMapper) {
+ public ApproveTodoTools(ApproveProcessMapper approveProcessMapper,
+ ApproveNodeMapper approveNodeMapper,
+ ApproveLogMapper approveLogMapper,
+ IApproveNodeService approveNodeService,
+ ApproveProcessServiceImpl approveProcessService,
+ AiSessionUserContext aiSessionUserContext) {
this.approveProcessMapper = approveProcessMapper;
this.approveNodeMapper = approveNodeMapper;
this.approveLogMapper = approveLogMapper;
+ this.approveNodeService = approveNodeService;
+ this.approveProcessService = approveProcessService;
+ this.aiSessionUserContext = aiSessionUserContext;
}
- @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ瀹℃壒寰呭姙锛屾敮鎸佹寜鐘舵�併�佺被鍨嬨�佸叧閿瓧杩囨护锛岃繑鍥� Markdown 琛ㄦ牸銆�")
- public String listTodos(
- @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
- @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
- @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+ @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧杩囨护銆�")
+ public String listTodos(@ToolMemoryId String memoryId,
+ @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
+ @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
+ @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+
+ LoginUser loginUser = currentLoginUser(memoryId);
+ Long userId = loginUser.getUserId();
+ Integer statusCode = parseStatus(status);
LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(ApproveProcess::getApproveDelete, 0);
+ wrapper.eq(ApproveProcess::getApproveDelete, 0)
+ .ne(ApproveProcess::getApproveStatus, 2);
- Integer statusCode = parseStatus(status);
- if (statusCode != null) {
- wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
- }
if (approveType != null) {
wrapper.eq(ApproveProcess::getApproveType, approveType);
}
- if (StringUtils.hasText(keyword)) {
- wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
- .or().like(ApproveProcess::getApproveReason, keyword)
- .or().like(ApproveProcess::getApproveUserName, keyword)
- .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+// if (StringUtils.hasText(keyword)) {
+// wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
+// .or().like(ApproveProcess::getApproveReason, keyword)
+// .or().like(ApproveProcess::getApproveUserName, keyword)
+// .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+// }
+ if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+ wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+ } else {
+ wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId)
+ .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
+ .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId));
}
- wrapper.orderByDesc(ApproveProcess::getCreateTime);
- wrapper.last("limit " + normalizeLimit(limit));
+ wrapper.orderByDesc(ApproveProcess::getCreateTime)
+ .last("limit " + normalizeLimit(limit));
- List<ApproveProcess> processes = approveProcessMapper.selectList(wrapper);
- if (processes == null || processes.isEmpty()) {
- return jsonResponse(
- true,
- "todo_list",
- "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勫鎵瑰緟鍔炪��",
+ List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(wrapper));
+ if (processes.isEmpty()) {
+ return jsonResponse(true, "todo_list", "鏈煡璇㈠埌褰撳墠鐢ㄦ埛绗﹀悎鏉′欢鐨勫鎵瑰緟鍔炪��",
Map.of("count", 0),
- Map.of(
- "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
- "items", List.of()
- ),
- Map.of()
- );
+ Map.of("columns", todoColumns(), "items", List.of()),
+ Map.of());
}
- List<Map<String, Object>> items = processes.stream().map(process -> {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("approveId", process.getApproveId());
- item.put("approveType", approveTypeName(process.getApproveType()));
- item.put("approveUserName", process.getApproveUserName());
- item.put("approveUserCurrentName", process.getApproveUserCurrentName());
- item.put("approveReason", process.getApproveReason());
- item.put("approveStatus", approveStatusName(process.getApproveStatus()));
- item.put("createTime", formatDateTime(process.getCreateTime()));
- return item;
- }).collect(Collectors.toList());
+ List<Map<String, Object>> items = processes.stream()
+ .filter(process -> canView(process, userId))
+ .sorted(Comparator
+ .comparing((ApproveProcess process) -> !Objects.equals(process.getApproveUserCurrentId(), userId))
+ .thenComparing(ApproveProcess::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
+ .map(process -> {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("approveId", process.getApproveId());
+ item.put("approveType", approveTypeName(process.getApproveType()));
+ item.put("approveUserName", safe(process.getApproveUserName()));
+ item.put("approveUserCurrentName", safe(process.getApproveUserCurrentName()));
+ item.put("approveReason", safe(process.getApproveReason()));
+ item.put("approveStatus", approveStatusName(process.getApproveStatus()));
+ item.put("createTime", formatDateTime(process.getCreateTime()));
+ item.put("relation", relationName(process, userId));
+ return item;
+ })
+ .collect(Collectors.toList());
- return jsonResponse(
- true,
- "todo_list",
- "宸茶繑鍥炲鎵瑰緟鍔炲垪琛紝鍙洿鎺ユ覆鏌撹〃鏍兼垨鍗$墖銆�",
+ return jsonResponse(true, "todo_list", "宸茶繑鍥炲綋鍓嶇敤鎴风浉鍏冲鎵瑰垪琛ㄣ��",
Map.of(
"count", items.size(),
- "statusFilter", status == null ? "all" : status,
+ "statusFilter", StringUtils.hasText(status) ? status : "all",
"approveType", approveType == null ? "" : approveType,
"keyword", keyword == null ? "" : keyword
),
- Map.of(
- "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
- "items", items
- ),
- Map.of()
- );
+ Map.of("columns", todoColumns(), "items", items),
+ Map.of());
}
- @Tool(name = "鏌ヨ瀹℃壒寰呭姙璇︽儏", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ鍗曟潯瀹℃壒寰呭姙璇︽儏锛岃繑鍥炵粨鏋勫寲鏂囨湰銆�")
- public String getTodoDetail(@P("娴佺▼缂栧彿 approveId") String approveId) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Tool(name = "鏌ヨ瀹℃壒寰呭姙璇︽儏", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ褰撳墠鐧诲綍浜哄彲瑙佺殑瀹℃壒璇︽儏銆�")
+ public String getTodoDetail(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�";
+ return "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冩煡鐪嬭娴佺▼銆�";
}
StringJoiner detail = new StringJoiner("\n");
@@ -139,255 +167,231 @@
detail.add("閲戦: " + (process.getPrice() == null ? "" : process.getPrice().toPlainString()));
detail.add("澶囨敞: " + safe(process.getApproveRemark()));
detail.add("鍒涘缓鏃堕棿: " + formatDateTime(process.getCreateTime()));
+ detail.add("涓庡綋鍓嶇敤鎴峰叧绯�: " + relationName(process, currentUserId(memoryId)));
return detail.toString();
}
- @Tool(name = "鏌ヨ瀹℃壒娴佽浆璁板綍", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ瀹℃壒鑺傜偣鍜屽鎵规棩蹇楋紝閫傚悎鍥炵瓟杩涘害銆佸崱鍦ㄥ摢涓妭鐐广�佽皝澶勭悊杩囥��")
- public String getTodoProgress(@P("娴佺▼缂栧彿 approveId") String approveId) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Tool(name = "鏌ヨ瀹℃壒娴佽浆璁板綍", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ瀹℃壒鑺傜偣鍜屽鎵规棩蹇楋紝鐢ㄤ簬鍥炵瓟杩涘害銆佸綋鍓嶅崱鐐瑰拰鍘嗗彶澶勭悊璁板綍銆�")
+ public String getTodoProgress(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return jsonResponse(
- false,
- "todo_progress",
- "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�",
- Map.of("approveId", approveId == null ? "" : approveId),
+ return jsonResponse(false, "todo_progress", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冩煡鐪嬭娴佺▼銆�",
+ Map.of("approveId", safe(approveId)),
Map.of(),
- Map.of()
- );
+ Map.of());
}
List<ApproveNode> nodes = listNodes(process);
List<ApproveLog> logs = listLogs(process.getId());
+ ApproveNode currentNode = findCurrentNode(nodes);
+
List<Map<String, Object>> nodeItems = nodes.stream().map(node -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("approveNodeOrder", node.getApproveNodeOrder());
- item.put("approveNodeUser", node.getApproveNodeUser());
+ item.put("approveNodeUser", safe(node.getApproveNodeUser()));
+ item.put("approveNodeUserId", node.getApproveNodeUserId());
item.put("approveNodeStatus", approveNodeStatusName(node.getApproveNodeStatus()));
item.put("approveNodeTime", formatDate(node.getApproveNodeTime()));
- item.put("approveNodeReason", node.getApproveNodeReason());
- item.put("approveNodeRemark", node.getApproveNodeRemark());
+ item.put("approveNodeReason", safe(node.getApproveNodeReason()));
+ item.put("approveNodeRemark", safe(node.getApproveNodeRemark()));
+ item.put("isCurrent", currentNode != null && Objects.equals(currentNode.getId(), node.getId()));
return item;
}).collect(Collectors.toList());
+
List<Map<String, Object>> logItems = logs.stream().map(log -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("approveNodeOrder", log.getApproveNodeOrder());
item.put("approveUser", log.getApproveUser());
item.put("approveStatus", approveStatusName(log.getApproveStatus()));
item.put("approveTime", formatDate(log.getApproveTime()));
- item.put("approveRemark", log.getApproveRemark());
+ item.put("approveRemark", safe(log.getApproveRemark()));
return item;
}).collect(Collectors.toList());
- return jsonResponse(
- true,
- "todo_progress",
- "宸茶繑鍥炲鎵规祦杞褰曪紝鍙覆鏌撴椂闂寸嚎銆佹楠ゆ潯鍜屾棩蹇楄〃鏍笺��",
+
+ return jsonResponse(true, "todo_progress", "宸茶繑鍥炲鎵规祦杞褰曘��",
Map.of(
- "approveId", process.getApproveId(),
+ "approveId", safe(process.getApproveId()),
"currentStatus", approveStatusName(process.getApproveStatus()),
"currentApprover", safe(process.getApproveUserCurrentName()),
+ "currentNodeOrder", currentNode == null ? "" : currentNode.getApproveNodeOrder(),
"nodeCount", nodeItems.size(),
"logCount", logItems.size()
),
- Map.of(
- "nodes", nodeItems,
- "logs", logItems
- ),
- Map.of()
- );
+ Map.of("nodes", nodeItems, "logs", logItems),
+ Map.of());
}
- @Tool(name = "缁熻瀹℃壒寰呭姙鏁版嵁", value = "杩斿洖涓撲笟鍖栫粺璁$粨鏋滐紝鍖呭惈鎽樿鎸囨爣鍜屽彲鐩存帴鐢ㄤ簬 ECharts 鐨勫浘琛� option銆�")
- public String getTodoStats() {
- List<ApproveProcess> processes = approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
- .eq(ApproveProcess::getApproveDelete, 0));
- if (processes == null || processes.isEmpty()) {
- return jsonResponse(
- true,
- "todo_stats",
- "褰撳墠娌℃湁瀹℃壒寰呭姙鏁版嵁銆�",
- Map.of("total", 0),
+ @Tool(name = "缁熻瀹℃壒寰呭姙鏁版嵁", value = "鎸夌敤鎴锋寚瀹氱殑鏃堕棿鑼冨洿缁熻褰撳墠鐧诲綍浜虹浉鍏冲鎵圭殑鐘舵�佸垎甯冦�佺被鍨嬪垎甯冨拰瓒嬪娍锛涙湭鎸囧畾鏃堕粯璁よ繎7澶┿��")
+ public String getTodoStats(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡� 浠婂ぉ銆佹湰鏈堛�佽繎30澶┿��2026-04-01鍒�2026-04-27", required = false) String timeRange) {
+ Long userId = currentUserId(memoryId);
+ List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
+ .eq(ApproveProcess::getApproveDelete, 0)
+ .and(w -> w.eq(ApproveProcess::getApproveUser, userId)
+ .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
+ .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId))));
+
+ DateRange dateRange = resolveDateRange(startDate, endDate, timeRange);
+ List<ApproveProcess> filteredProcesses = processes.stream()
+ .filter(process -> withinDateRange(process.getCreateTime(), dateRange))
+ .collect(Collectors.toList());
+
+ if (filteredProcesses.isEmpty()) {
+ return jsonResponse(true, "todo_stats", "褰撳墠鐢ㄦ埛娌℃湁鐩稿叧瀹℃壒鏁版嵁銆�",
+ Map.of(
+ "total", 0,
+ "startDate", dateRange.start().toString(),
+ "endDate", dateRange.end().toString(),
+ "timeRange", dateRange.label()
+ ),
Map.of(
"statusDistribution", Map.of(),
"typeDistribution", Map.of(),
- "recent7DayTrend", List.of(),
- "tips", List.of()
+ "trend", List.of()
),
- Map.of()
- );
+ Map.of());
}
- Map<String, Long> statusStats = processes.stream()
+ Map<String, Long> statusStats = filteredProcesses.stream()
.collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting()));
- Map<String, Long> typeStats = processes.stream()
+ Map<String, Long> typeStats = filteredProcesses.stream()
.collect(Collectors.groupingBy(p -> approveTypeName(p.getApproveType()), LinkedHashMap::new, Collectors.counting()));
- long pendingCount = countByStatus(processes, 0);
- long processingCount = countByStatus(processes, 1);
- long approvedCount = countByStatus(processes, 2);
- long rejectedCount = countByStatus(processes, 3);
- long resubmittedCount = countByStatus(processes, 4);
+ long pendingCount = countByStatus(filteredProcesses, 0);
+ long processingCount = countByStatus(filteredProcesses, 1);
+ long approvedCount = countByStatus(filteredProcesses, 2);
+ long rejectedCount = countByStatus(filteredProcesses, 3);
+ long resubmittedCount = countByStatus(filteredProcesses, 4);
- List<String> recentDates = buildRecentDates(7);
- List<Long> trendValues = recentDates.stream()
- .map(date -> processes.stream()
- .filter(process -> process.getCreateTime() != null)
- .filter(process -> process.getCreateTime().toLocalDate().equals(LocalDate.parse(date)))
- .count())
- .collect(Collectors.toList());
+ TrendRange trendRange = buildTrendRange(dateRange.start(), dateRange.end(), filteredProcesses);
Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("total", processes.size());
+ summary.put("total", filteredProcesses.size());
summary.put("pending", pendingCount);
summary.put("processing", processingCount);
summary.put("approved", approvedCount);
summary.put("rejected", rejectedCount);
summary.put("resubmitted", resubmittedCount);
- summary.put("approvalCompletionRate", calculateRate(approvedCount, processes.size()));
- summary.put("rejectionRate", calculateRate(rejectedCount, processes.size()));
+ summary.put("approvalCompletionRate", calculateRate(approvedCount, filteredProcesses.size()));
+ summary.put("rejectionRate", calculateRate(rejectedCount, filteredProcesses.size()));
+ summary.put("startDate", dateRange.start().toString());
+ summary.put("endDate", dateRange.end().toString());
+ summary.put("timeRange", dateRange.label());
+ summary.put("trendGranularity", trendRange.granularity());
Map<String, Object> charts = new LinkedHashMap<>();
charts.put("statusBarOption", buildStatusBarOption(statusStats));
charts.put("typePieOption", buildTypePieOption(typeStats));
- charts.put("recentTrendLineOption", buildRecentTrendLineOption(recentDates, trendValues));
+ charts.put("trendLineOption", buildTrendLineOption(trendRange.labels(), trendRange.values(), trendRange.label()));
- return jsonResponse(
- true,
- "todo_stats",
- "宸茶繑鍥炲鎵圭粺璁℃瑙堜笌鍥捐〃閰嶇疆锛屽墠绔彲鐩存帴浣跨敤 charts 涓殑 ECharts option 娓叉煋銆�",
+ return jsonResponse(true, "todo_stats", "宸茶繑鍥炲綋鍓嶇敤鎴风浉鍏冲鎵圭粺璁°��",
summary,
Map.of(
"statusDistribution", statusStats,
"typeDistribution", typeStats,
- "recent7DayTrend", toTrendItems(recentDates, trendValues),
- "tips", List.of(
- "statusBarOption 閫傚悎灞曠ず鍚勫鎵圭姸鎬佹暟閲忓姣�",
- "typePieOption 閫傚悎灞曠ず鍚勫鎵圭被鍨嬪崰姣�",
- "recentTrendLineOption 閫傚悎灞曠ず鏈�杩�7澶╂柊澧炲鎵硅秼鍔�"
- )
+ "trend", toTrendItems(trendRange.labels(), trendRange.values())
),
- charts
- );
+ charts);
}
- @Transactional
- @Tool(name = "瀹℃壒寰呭姙", value = "鎵ц瀹℃壒鍔ㄤ綔銆俛ction 鍙敮鎸� approve 鎴� reject銆俛pprove 琛ㄧず閫氳繃锛宺eject 琛ㄧず椹冲洖銆�")
- public String reviewTodo(
- @P("娴佺▼缂栧彿 approveId") String approveId,
- @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") String action,
- @P(value = "瀹℃壒澶囨敞锛屽彲涓嶄紶", required = false) String remark) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Transactional(rollbackFor = Exception.class)
+ @Tool(name = "瀹℃壒寰呭姙", value = "鎵ц瀹℃壒鍔ㄤ綔锛宎ction 浠呮敮鎸� approve 鎴� reject锛屼笖鍙兘澶勭悊褰撳墠鐧诲綍浜鸿嚜宸辩殑寰呭鑺傜偣銆�")
+ public String reviewTodo(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") String action,
+ @P(value = "瀹℃壒澶囨敞锛屽彲涓嶄紶", required = false) String remark) {
+
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return actionResult(false, "review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ return actionResult(false, "review_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
}
- if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
- return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡鍒犻櫎锛屼笉鑳藉啀瀹℃牳銆�", approveId, null);
+ if (!canOperate(process, currentUserId(memoryId))) {
+ return actionResult(false, "review_action", "褰撳墠鐧诲綍浜轰笉鏄瀹℃壒鐨勫綋鍓嶅鐞嗕汉銆�", approveId, null);
}
if (process.getApproveStatus() != null && (process.getApproveStatus() == 2 || process.getApproveStatus() == 3)) {
- return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡缁撴潫锛屼笉鑳介噸澶嶅鏍搞��", approveId, null);
+ return actionResult(false, "review_action", "璇ュ鎵瑰凡缁撴潫锛屼笉鑳介噸澶嶅鐞嗐��", approveId, null);
}
List<ApproveNode> nodes = listNodes(process);
ApproveNode currentNode = findCurrentNode(nodes);
- if (currentNode == null) {
- return actionResult(false, "review_action", "鏈壘鍒板彲瀹℃牳鐨勫綋鍓嶈妭鐐广��", approveId, null);
+ if (currentNode == null || !Objects.equals(currentNode.getApproveNodeUserId(), currentUserId(memoryId))) {
+ return actionResult(false, "review_action", "鏈壘鍒板綋鍓嶇敤鎴峰彲澶勭悊鐨勫鎵硅妭鐐广��", approveId, null);
}
String normalizedAction = action == null ? "" : action.trim().toLowerCase();
- Date now = new Date();
- currentNode.setApproveNodeTime(now);
+ currentNode.setApproveNodeRemark(remark);
+ currentNode.setApproveNodeReason("reject".equals(normalizedAction) ? remark : null);
+ currentNode.setUpdateUser(currentUserId(memoryId));
currentNode.setUpdateTime(LocalDateTime.now());
+ currentNode.setIsLast(isLastNode(nodes, currentNode));
- ApproveLog log = new ApproveLog();
- log.setApproveId(process.getId());
- log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
- log.setApproveUser(currentNode.getApproveNodeUserId());
- log.setApproveTime(now);
- log.setApproveRemark(remark);
-
- if ("approve".equals(normalizedAction)) {
- currentNode.setApproveNodeStatus(1);
- currentNode.setApproveNodeReason(null);
- approveNodeMapper.updateById(currentNode);
-
- ApproveNode nextNode = findNextNode(nodes, currentNode.getApproveNodeOrder());
- if (nextNode == null) {
- process.setApproveStatus(2);
- process.setApproveOverTime(now);
- process.setApproveUserCurrentId(null);
- process.setApproveUserCurrentName(null);
- } else {
- process.setApproveStatus(1);
- process.setApproveUserCurrentId(nextNode.getApproveNodeUserId());
- process.setApproveUserCurrentName(nextNode.getApproveNodeUser());
+ try {
+ switch (normalizedAction) {
+ case "approve" -> currentNode.setApproveNodeStatus(1);
+ case "reject" -> currentNode.setApproveNodeStatus(2);
+ default -> {
+ return actionResult(false, "review_action", "action 鍙敮鎸� approve 鎴� reject銆�", approveId, null);
+ }
}
- process.setApproveRemark(remark);
- approveProcessMapper.updateById(process);
-
- log.setApproveStatus(nextNode == null ? 2 : 1);
- approveLogMapper.insert(log);
- return actionResult(true, "review_action",
- nextNode == null ? "瀹℃壒宸查�氳繃锛屼笖璇ユ祦绋嬪凡鍏ㄩ儴瀹屾垚銆�" : "瀹℃壒宸查�氳繃锛屾祦绋嬪凡娴佽浆鍒颁笅涓�瀹℃壒鑺傜偣銆�",
- approveId,
- Map.of(
- "action", "approve",
- "currentStatus", approveStatusName(process.getApproveStatus()),
- "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
- "remark", safe(remark)
- ));
+ approveNodeService.updateApproveNode(currentNode);
+ } catch (IOException e) {
+ throw new RuntimeException("瀹℃壒澶勭悊澶辫触", e);
}
- if ("reject".equals(normalizedAction)) {
- currentNode.setApproveNodeStatus(2);
- currentNode.setApproveNodeReason(remark);
- currentNode.setApproveNodeRemark(remark);
- approveNodeMapper.updateById(currentNode);
+ ApproveProcess refreshed = getProcessByApproveId(approveId);
+ writeApproveLog(memoryId, refreshed, currentNode, remark);
+ ApproveNode nextNode = refreshed == null ? null : findCurrentNode(listNodes(refreshed));
- process.setApproveStatus(3);
- process.setApproveOverTime(now);
- process.setApproveUserCurrentId(null);
- process.setApproveUserCurrentName(null);
- process.setApproveRemark(remark);
- approveProcessMapper.updateById(process);
-
- log.setApproveStatus(3);
- approveLogMapper.insert(log);
- return actionResult(true, "review_action", "瀹℃壒宸查┏鍥炪��", approveId, Map.of(
- "action", "reject",
- "currentStatus", approveStatusName(process.getApproveStatus()),
- "remark", safe(remark)
- ));
- }
-
- return actionResult(false, "review_action", "action 鍙敮鎸� approve 鎴� reject銆�", approveId, null);
+ return actionResult(true, "review_action",
+ "approve".equals(normalizedAction) ? "瀹℃壒宸查�氳繃銆�" : "瀹℃壒宸查┏鍥炪��",
+ approveId,
+ Map.of(
+ "action", normalizedAction,
+ "currentStatus", refreshed == null ? "" : approveStatusName(refreshed.getApproveStatus()),
+ "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
+ "remark", safe(remark)
+ ));
}
- @Transactional
- @Tool(name = "鍙栨秷瀹℃壒寰呭姙瀹℃牳", value = "鎾ら攢鏈�杩戜竴娆″鏍哥粨鏋滐紝灏嗘渶杩戝鏍歌妭鐐规仮澶嶄负鏈鏍革紝骞跺洖婊氭祦绋嬬姸鎬併��")
- public String cancelReviewTodo(
- @P("娴佺▼缂栧彿 approveId") String approveId,
- @P(value = "鍙栨秷鍘熷洜锛屽彲涓嶄紶", required = false) String reason) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Transactional(rollbackFor = Exception.class)
+ @Tool(name = "鍙栨秷瀹℃壒寰呭姙瀹℃牳", value = "鎾ら攢鏈�杩戜竴娆″鏍哥粨鏋滐紝浠呭厑璁告渶杩戜竴娆″鏍镐汉鎴栫敵璇蜂汉鎿嶄綔銆�")
+ public String cancelReviewTodo(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P(value = "鍙栨秷鍘熷洜锛屽彲涓嶄紶", required = false) String reason) {
+
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return actionResult(false, "cancel_review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ return actionResult(false, "cancel_review_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
}
+
List<ApproveNode> nodes = listNodes(process);
ApproveNode lastReviewedNode = nodes.stream()
.filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() != 0)
.max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
.orElse(null);
if (lastReviewedNode == null) {
- return actionResult(false, "cancel_review_action", "褰撳墠娴佺▼娌℃湁鍙彇娑堢殑瀹℃牳璁板綍銆�", approveId, null);
+ return actionResult(false, "cancel_review_action", "褰撳墠娴佺▼娌℃湁鍙挙閿�鐨勫鏍歌褰曘��", approveId, null);
+ }
+
+ Long userId = currentUserId(memoryId);
+ if (!isAdmin(userId)
+ && !Objects.equals(process.getApproveUser(), userId)
+ && !Objects.equals(lastReviewedNode.getApproveNodeUserId(), userId)) {
+ return actionResult(false, "cancel_review_action", "鍙湁鐢宠浜恒�佹渶杩戜竴娆″鏍镐汉鎴栫鐞嗗憳鍙互鎾ら攢銆�", approveId, null);
}
lastReviewedNode.setApproveNodeStatus(0);
lastReviewedNode.setApproveNodeTime(null);
lastReviewedNode.setApproveNodeReason(null);
lastReviewedNode.setApproveNodeRemark(reason);
+ lastReviewedNode.setUpdateUser(userId);
lastReviewedNode.setUpdateTime(LocalDateTime.now());
approveNodeMapper.updateById(lastReviewedNode);
- List<ApproveLog> logs = listLogs(process.getId());
- ApproveLog latestLog = logs.stream()
+ ApproveLog latestLog = listLogs(process.getId()).stream()
.max(Comparator.comparing(ApproveLog::getApproveNodeOrder)
.thenComparing(ApproveLog::getApproveTime, Comparator.nullsLast(Date::compareTo)))
.orElse(null);
@@ -395,20 +399,14 @@
approveLogMapper.deleteById(latestLog.getId());
}
- ApproveNode previousReviewedNode = nodes.stream()
- .filter(node -> !node.getId().equals(lastReviewedNode.getId()))
- .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 1)
- .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
- .orElse(null);
-
process.setApproveOverTime(null);
process.setApproveRemark(reason);
- process.setApproveStatus(previousReviewedNode == null ? 0 : 1);
+ process.setApproveStatus(lastReviewedNode.getApproveNodeOrder() == null || lastReviewedNode.getApproveNodeOrder() <= 1 ? 0 : 1);
process.setApproveUserCurrentId(lastReviewedNode.getApproveNodeUserId());
process.setApproveUserCurrentName(lastReviewedNode.getApproveNodeUser());
approveProcessMapper.updateById(process);
- return actionResult(true, "cancel_review_action", "鏈�杩戜竴娆″鏍稿凡鍙栨秷锛屾祦绋嬪凡鍥為��鍒板搴斿鎵硅妭鐐广��", approveId, Map.of(
+ return actionResult(true, "cancel_review_action", "鏈�杩戜竴娆″鏍稿凡鎾ら攢銆�", approveId, Map.of(
"rollbackNodeOrder", lastReviewedNode.getApproveNodeOrder(),
"currentStatus", approveStatusName(process.getApproveStatus()),
"currentApprover", safe(process.getApproveUserCurrentName()),
@@ -416,27 +414,36 @@
));
}
- @Transactional
- @Tool(name = "淇敼瀹℃壒寰呭姙", value = "淇敼瀹℃壒鍗曞熀纭�淇℃伅銆傛敮鎸佷慨鏀规爣棰樸�佸紑濮嬫棩鏈熴�佺粨鏉熸棩鏈熴�侀噾棰濄�佸湴鐐广�佺被鍨嬪拰澶囨敞銆傛棩鏈熸牸寮忓繀椤绘槸 yyyy-MM-dd銆�")
- public String updateTodo(
- @P("娴佺▼缂栧彿 approveId") String approveId,
- @P(value = "鏂扮殑鏍囬锛屽彲涓嶄紶", required = false) String approveReason,
- @P(value = "鏂扮殑寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
- @P(value = "鏂扮殑缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
- @P(value = "鏂扮殑閲戦锛屽彲涓嶄紶", required = false) BigDecimal price,
- @P(value = "鏂扮殑鍦扮偣锛屽彲涓嶄紶", required = false) String location,
- @P(value = "鏂扮殑瀹℃壒绫诲瀷锛屽彲涓嶄紶", required = false) Integer approveType,
- @P(value = "鏂扮殑澶囨敞锛屽彲涓嶄紶", required = false) String approveRemark) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Transactional(rollbackFor = Exception.class)
+ @Tool(name = "淇敼瀹℃壒寰呭姙", value = "淇敼瀹℃壒鍗曞熀纭�淇℃伅锛屼粎鍏佽鐢宠浜轰慨鏀癸紱涓嶆敮鎸侀�氳繃 AI 鍙樻洿瀹℃壒绫诲瀷銆�")
+ public String updateTodo(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P(value = "鏂扮殑鏍囬锛屽彲涓嶄紶", required = false) String approveReason,
+ @P(value = "鏂扮殑寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "鏂扮殑缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏂扮殑閲戦锛屽彲涓嶄紶", required = false) BigDecimal price,
+ @P(value = "鏂扮殑鍦扮偣锛屽彲涓嶄紶", required = false) String location,
+ @P(value = "鏂扮殑瀹℃壒绫诲瀷锛屽彲涓嶄紶", required = false) Integer approveType,
+ @P(value = "鏂扮殑澶囨敞锛屽彲涓嶄紶", required = false) String approveRemark) {
+
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return actionResult(false, "update_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ return actionResult(false, "update_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
+ }
+ if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
+ return actionResult(false, "update_action", "鍙湁鐢宠浜烘垨绠$悊鍛樺彲浠ヤ慨鏀瑰鎵瑰崟銆�", approveId, null);
+ }
+ if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
+ return actionResult(false, "update_action", "瀹℃壒澶勭悊涓垨宸插畬鎴愭椂锛屼笉鍏佽閫氳繃 AI 淇敼銆�", approveId, null);
+ }
+ if (approveType != null && !Objects.equals(approveType, process.getApproveType())) {
+ return actionResult(false, "update_action", "AI 鍔╂墜鏆備笉鏀寔鐩存帴鍙樻洿瀹℃壒绫诲瀷锛岄伩鍏嶈妭鐐归厤缃け鐪熴��", approveId, null);
}
if (!StringUtils.hasText(approveReason)
&& !StringUtils.hasText(startDate)
&& !StringUtils.hasText(endDate)
&& price == null
&& !StringUtils.hasText(location)
- && approveType == null
&& !StringUtils.hasText(approveRemark)) {
return actionResult(false, "update_action", "娌℃湁妫�娴嬪埌鍙洿鏂扮殑瀛楁銆�", approveId, null);
}
@@ -456,9 +463,6 @@
if (StringUtils.hasText(location)) {
process.setLocation(location);
}
- if (approveType != null) {
- process.setApproveType(approveType);
- }
if (StringUtils.hasText(approveRemark)) {
process.setApproveRemark(approveRemark);
}
@@ -475,30 +479,34 @@
));
}
- @Transactional
- @Tool(name = "鍒犻櫎瀹℃壒寰呭姙", value = "鍒犻櫎瀹℃壒娴佺▼銆傞�昏緫鍒犻櫎娴佺▼璁板綍锛屽苟鍚屾閫昏緫鍒犻櫎瀹℃壒鑺傜偣銆�")
- public String deleteTodo(@P("娴佺▼缂栧彿 approveId") String approveId) {
- ApproveProcess process = getProcessByApproveId(approveId);
+ @Transactional(rollbackFor = Exception.class)
+ @Tool(name = "鍒犻櫎瀹℃壒寰呭姙", value = "鍒犻櫎瀹℃壒娴佺▼锛屼粎鍏佽鐢宠浜哄垹闄ゆ湭瀹屾垚鐨勬祦绋嬨��")
+ public String deleteTodo(@ToolMemoryId String memoryId,
+ @P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getAccessibleProcess(memoryId, approveId);
if (process == null) {
- return actionResult(false, "delete_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ return actionResult(false, "delete_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
}
- if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
- return actionResult(false, "delete_action", "璇ュ鎵规祦绋嬪凡缁忔槸鍒犻櫎鐘舵�併��", approveId, null);
+ if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
+ return actionResult(false, "delete_action", "鍙湁鐢宠浜烘垨绠$悊鍛樺彲浠ュ垹闄ゅ鎵瑰崟銆�", approveId, null);
+ }
+ if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
+ return actionResult(false, "delete_action", "瀹℃壒澶勭悊涓垨宸插畬鎴愮殑娴佺▼涓嶅厑璁搁�氳繃 AI 鍒犻櫎銆�", approveId, null);
}
- process.setApproveDelete(1);
- approveProcessMapper.updateById(process);
-
- List<ApproveNode> nodes = listNodes(process);
- for (ApproveNode node : nodes) {
- node.setDeleteFlag(1);
- node.setUpdateTime(LocalDateTime.now());
- approveNodeMapper.updateById(node);
- }
+ approveProcessService.delByIds(Collections.singletonList(process.getId()));
return actionResult(true, "delete_action", "瀹℃壒娴佺▼宸插垹闄ゃ��", approveId, Map.of(
- "deletedNodeCount", nodes.size(),
+ "deletedProcessId", process.getId(),
"approveStatus", approveStatusName(process.getApproveStatus())
));
+ }
+
+ private ApproveProcess getAccessibleProcess(String memoryId, String approveId) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return null;
+ }
+ return canView(process, currentUserId(memoryId)) ? process : null;
}
private ApproveProcess getProcessByApproveId(String approveId) {
@@ -507,29 +515,31 @@
}
return approveProcessMapper.selectOne(new LambdaQueryWrapper<ApproveProcess>()
.eq(ApproveProcess::getApproveId, approveId)
+ .eq(ApproveProcess::getApproveDelete, 0)
.last("limit 1"));
}
private List<ApproveNode> listNodes(ApproveProcess process) {
- List<ApproveNode> nodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+ if (process == null) {
+ return List.of();
+ }
+ List<ApproveNode> nodes = defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
.eq(ApproveNode::getDeleteFlag, 0)
.eq(ApproveNode::getApproveProcessId, process.getApproveId())
- .orderByAsc(ApproveNode::getApproveNodeOrder));
- if (nodes != null && !nodes.isEmpty()) {
+ .orderByAsc(ApproveNode::getApproveNodeOrder)));
+ if (!nodes.isEmpty()) {
return nodes;
}
- List<ApproveNode> fallbackNodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+ return defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
.eq(ApproveNode::getDeleteFlag, 0)
.eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId()))
- .orderByAsc(ApproveNode::getApproveNodeOrder));
- return fallbackNodes == null ? List.of() : fallbackNodes;
+ .orderByAsc(ApproveNode::getApproveNodeOrder)));
}
private List<ApproveLog> listLogs(Long processId) {
- List<ApproveLog> logs = approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
+ return defaultList(approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
.eq(ApproveLog::getApproveId, processId)
- .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime));
- return logs == null ? List.of() : logs;
+ .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime)));
}
private ApproveNode findCurrentNode(List<ApproveNode> nodes) {
@@ -539,12 +549,72 @@
.orElse(null);
}
- private ApproveNode findNextNode(List<ApproveNode> nodes, Integer currentOrder) {
- return nodes.stream()
- .filter(node -> node.getApproveNodeOrder() != null && currentOrder != null)
- .filter(node -> node.getApproveNodeOrder() > currentOrder)
- .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+ private boolean isLastNode(List<ApproveNode> nodes, ApproveNode currentNode) {
+ Integer maxOrder = nodes.stream()
+ .map(ApproveNode::getApproveNodeOrder)
+ .filter(Objects::nonNull)
+ .max(Integer::compareTo)
.orElse(null);
+ return maxOrder != null && Objects.equals(maxOrder, currentNode.getApproveNodeOrder());
+ }
+
+ private void writeApproveLog(String memoryId, ApproveProcess process, ApproveNode currentNode, String remark) {
+ if (process == null || currentNode == null) {
+ return;
+ }
+ ApproveLog log = new ApproveLog();
+ log.setApproveId(process.getId());
+ log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
+ log.setApproveUser(currentUserId(memoryId));
+ log.setApproveTime(new Date());
+ log.setApproveStatus(process.getApproveStatus());
+ log.setApproveRemark(remark);
+ approveLogMapper.insert(log);
+ }
+
+ private boolean canView(ApproveProcess process, Long userId) {
+ if (process == null || userId == null) {
+ return false;
+ }
+ return isAdmin(userId)
+ || Objects.equals(process.getApproveUser(), userId)
+ || Objects.equals(process.getApproveUserCurrentId(), userId)
+ || containsUserId(process.getApproveUserIds(), userId);
+ }
+
+ private boolean canOperate(ApproveProcess process, Long userId) {
+ return process != null && userId != null && Objects.equals(process.getApproveUserCurrentId(), userId);
+ }
+
+ private boolean containsUserId(String csv, Long userId) {
+ if (!StringUtils.hasText(csv) || userId == null) {
+ return false;
+ }
+ String target = String.valueOf(userId);
+ for (String item : csv.split(",")) {
+ if (target.equals(item.trim())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String relationName(ApproveProcess process, Long userId) {
+ if (Objects.equals(process.getApproveUserCurrentId(), userId)) {
+ return "褰撳墠瀹℃壒浜�";
+ }
+ if (Objects.equals(process.getApproveUser(), userId)) {
+ return "鐢宠浜�";
+ }
+ if (containsUserId(process.getApproveUserIds(), userId)) {
+ return "瀹℃壒閾炬垚鍛�";
+ }
+ return "鍙";
+ }
+
+ private List<String> todoColumns() {
+ return List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName",
+ "approveReason", "approveStatus", "createTime", "relation");
}
private int normalizeLimit(Integer limit) {
@@ -640,17 +710,7 @@
if (total <= 0) {
return "0.00%";
}
- double rate = part * 100.0 / total;
- return String.format("%.2f%%", rate);
- }
-
- private List<String> buildRecentDates(int days) {
- List<String> dates = new ArrayList<>();
- LocalDate today = LocalDate.now();
- for (int i = days - 1; i >= 0; i--) {
- dates.add(today.minusDays(i).toString());
- }
- return dates;
+ return String.format("%.2f%%", part * 100.0 / total);
}
private List<Map<String, Object>> toTrendItems(List<String> dates, List<Long> values) {
@@ -702,9 +762,9 @@
return option;
}
- private Map<String, Object> buildRecentTrendLineOption(List<String> dates, List<Long> values) {
+ private Map<String, Object> buildTrendLineOption(List<String> dates, List<Long> values, String label) {
Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "鏈�杩�7澶╁鎵规柊澧炶秼鍔�", "left", "center"));
+ option.put("title", Map.of("text", label + "瀹℃壒鏂板瓒嬪娍", "left", "center"));
option.put("tooltip", Map.of("trigger", "axis"));
option.put("xAxis", Map.of("type", "category", "data", dates));
option.put("yAxis", Map.of("type", "value"));
@@ -726,19 +786,147 @@
}
}
+ private DateRange resolveDateRange(String startDateText, String endDateText, String timeRange) {
+ LocalDate today = LocalDate.now();
+ LocalDate explicitStart = parseLocalDate(startDateText);
+ LocalDate explicitEnd = parseLocalDate(endDateText);
+ if (explicitStart != null || explicitEnd != null) {
+ LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
+ LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
+ if (start.isAfter(end)) {
+ LocalDate temp = start;
+ start = end;
+ end = temp;
+ }
+ return new DateRange(start, end, start + "鑷�" + end);
+ }
+ if (!StringUtils.hasText(timeRange)) {
+ return new DateRange(today.minusDays(6), today, "杩�7澶�");
+ }
+
+ String text = timeRange.trim();
+ if (text.contains("浠婂ぉ")) {
+ return new DateRange(today, today, "浠婂ぉ");
+ }
+ if (text.contains("鏄ㄥぉ") || text.contains("鏄ㄦ棩")) {
+ LocalDate day = today.minusDays(1);
+ return new DateRange(day, day, "鏄ㄥぉ");
+ }
+ if (text.contains("鏈懆")) {
+ LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ return new DateRange(start, today, "鏈懆");
+ }
+ if (text.contains("涓婂懆")) {
+ LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ LocalDate start = thisWeekStart.minusWeeks(1);
+ LocalDate end = thisWeekStart.minusDays(1);
+ return new DateRange(start, end, "涓婂懆");
+ }
+ if (text.contains("鏈湀")) {
+ LocalDate start = today.withDayOfMonth(1);
+ return new DateRange(start, today, "鏈湀");
+ }
+ if (text.contains("涓婃湀")) {
+ YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
+ return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "涓婃湀");
+ }
+ if (text.contains("鏈勾") || text.contains("浠婂勾")) {
+ LocalDate start = today.withDayOfYear(1);
+ return new DateRange(start, today, "鏈勾");
+ }
+ if (text.contains("鍘诲勾")) {
+ LocalDate start = today.minusYears(1).withDayOfYear(1);
+ LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31);
+ return new DateRange(start, end, "鍘诲勾");
+ }
+
+ Matcher relativeMatcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+ if (relativeMatcher.find()) {
+ int amount = Integer.parseInt(relativeMatcher.group(2));
+ String unit = relativeMatcher.group(3);
+ LocalDate start = switch (unit) {
+ case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
+ case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
+ case "涓湀", "鏈�" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
+ case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
+ default -> today.minusDays(6);
+ };
+ return new DateRange(start, today, "杩�" + amount + unit);
+ }
+
+ Matcher dateMatcher = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2})").matcher(text);
+ if (dateMatcher.find()) {
+ LocalDate start = LocalDate.parse(dateMatcher.group(1));
+ LocalDate end = dateMatcher.find() ? LocalDate.parse(dateMatcher.group(1)) : start;
+ if (start.isAfter(end)) {
+ LocalDate temp = start;
+ start = end;
+ end = temp;
+ }
+ return new DateRange(start, end, start + "鑷�" + end);
+ }
+
+ return new DateRange(today.minusDays(6), today, "杩�7澶�");
+ }
+
+ private boolean withinDateRange(LocalDateTime createTime, DateRange dateRange) {
+ if (createTime == null) {
+ return false;
+ }
+ LocalDate date = createTime.toLocalDate();
+ return !date.isBefore(dateRange.start()) && !date.isAfter(dateRange.end());
+ }
+
+ private TrendRange buildTrendRange(LocalDate start, LocalDate end, List<ApproveProcess> processes) {
+ long days = ChronoUnit.DAYS.between(start, end) + 1;
+ if (days <= 31) {
+ List<String> labels = new ArrayList<>();
+ List<Long> values = new ArrayList<>();
+ for (LocalDate cursor = start; !cursor.isAfter(end); cursor = cursor.plusDays(1)) {
+ LocalDate current = cursor;
+ labels.add(current.toString());
+ values.add(processes.stream()
+ .filter(process -> process.getCreateTime() != null)
+ .filter(process -> process.getCreateTime().toLocalDate().equals(current))
+ .count());
+ }
+ return new TrendRange(labels, values, "day", start + "鑷�" + end);
+ }
+
+ List<String> labels = new ArrayList<>();
+ List<Long> values = new ArrayList<>();
+ YearMonth startMonth = YearMonth.from(start);
+ YearMonth endMonth = YearMonth.from(end);
+ for (YearMonth cursor = startMonth; !cursor.isAfter(endMonth); cursor = cursor.plusMonths(1)) {
+ YearMonth current = cursor;
+ labels.add(current.toString());
+ values.add(processes.stream()
+ .filter(process -> process.getCreateTime() != null)
+ .filter(process -> YearMonth.from(process.getCreateTime()).equals(current))
+ .count());
+ }
+ return new TrendRange(labels, values, "month", start + "鑷�" + end);
+ }
+
+ private LocalDate parseLocalDate(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ return LocalDate.parse(text.trim());
+ }
+
private String actionResult(boolean success, String type, String description, String approveId, Map<String, Object> data) {
Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("approveId", approveId == null ? "" : approveId);
+ summary.put("approveId", safe(approveId));
return jsonResponse(success, type, description, summary, data == null ? Map.of() : data, Map.of());
}
- private String jsonResponse(
- boolean success,
- String type,
- String description,
- Map<String, Object> summary,
- Map<String, Object> data,
- Map<String, Object> charts) {
+ private String jsonResponse(boolean success,
+ String type,
+ String description,
+ Map<String, Object> summary,
+ Map<String, Object> data,
+ Map<String, Object> charts) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", success);
result.put("type", type);
@@ -748,4 +936,30 @@
result.put("charts", charts == null ? Map.of() : charts);
return JSON.toJSONString(result);
}
+
+ private LoginUser currentLoginUser(String memoryId) {
+ LoginUser loginUser = aiSessionUserContext.get(memoryId);
+ if (loginUser != null) {
+ return loginUser;
+ }
+ return SecurityUtils.getLoginUser();
+ }
+
+ private Long currentUserId(String memoryId) {
+ return currentLoginUser(memoryId).getUserId();
+ }
+
+ private boolean isAdmin(Long userId) {
+ return SecurityUtils.isAdmin(userId);
+ }
+
+ private <T> List<T> defaultList(List<T> list) {
+ return list == null ? List.of() : list;
+ }
+
+ private record DateRange(LocalDate start, LocalDate end, String label) {
+ }
+
+ private record TrendRange(List<String> labels, List<Long> values, String granularity, String label) {
+ }
}
diff --git a/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java b/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
index f2e1d08..9f6a258 100644
--- a/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
+++ b/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
@@ -37,6 +37,7 @@
IGNORE_TABLES.add("sys_notice");
IGNORE_TABLES.add("sys_user_client");
IGNORE_TABLES.add("product_model");
+ IGNORE_TABLES.add("ai_chat_session");
IGNORE_TABLES.add("product");
}
diff --git a/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
index 410d049..93d4c77 100644
--- a/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
+++ b/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
@@ -22,23 +22,35 @@
* @author ruoyi
*/
@Component
-public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
-{
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
+{
@Autowired
private TokenService tokenService;
@Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
- throws ServletException, IOException
- {
- LoginUser loginUser = tokenService.getLoginUser(request);
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException
+ {
+ LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
- }
- chain.doFilter(request, response);
- }
-}
+ }
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ protected boolean shouldNotFilterAsyncDispatch()
+ {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldNotFilterErrorDispatch()
+ {
+ return false;
+ }
+}
diff --git a/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java b/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java
index 0c7f5cc..1b226ad 100644
--- a/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java
+++ b/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickDto.java
@@ -33,6 +33,12 @@
@Schema(description = "澶囨敞")
private String remark;
+ @Schema(description = "琛ユ枡鍘熷洜")
+ private String feedingReason;
+
+ @Schema(description = "鏈琛ユ枡鏁伴噺")
+ private BigDecimal feedingQuantity;
+
@Schema(description = "棰嗘枡鏄庣粏鍒楄〃")
@JsonAlias({"dto", "productionOrderPickDto"})
private List<ProductionOrderPickDto> pickList;
diff --git a/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java b/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java
new file mode 100644
index 0000000..6954c4a
--- /dev/null
+++ b/src/main/java/com/ruoyi/production/bean/dto/ProductionOrderPickRecordDto.java
@@ -0,0 +1,12 @@
+package com.ruoyi.production.bean.dto;
+
+import com.ruoyi.production.pojo.ProductionOrderPickRecord;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(name = "ProductionOrderPickRecordDto", description = "棰嗘枡璁板綍鏌ヨ鍙傛暟")
+public class ProductionOrderPickRecordDto extends ProductionOrderPickRecord {
+}
diff --git a/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java
index b88aa8a..aea1c85 100644
--- a/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java
+++ b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderPickRecordVo.java
@@ -1,9 +1,12 @@
package com.ruoyi.production.bean.vo;
+import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@@ -21,5 +24,11 @@
@Schema(description = "鍗曚綅")
private String unit;
-}
+ @Schema(description = "琛ユ枡浜�")
+ private String supplementUserName;
+
+ @Schema(description = "琛ユ枡鏃ユ湡")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime supplementTime;
+}
diff --git a/src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java b/src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java
index a266c8e..05e1a50 100644
--- a/src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java
+++ b/src/main/java/com/ruoyi/production/controller/ProductionOrderPickRecordController.java
@@ -1,7 +1,17 @@
package com.ruoyi.production.controller;
+import com.ruoyi.framework.web.domain.R;
+import com.ruoyi.production.bean.dto.ProductionOrderPickRecordDto;
+import com.ruoyi.production.bean.vo.ProductionOrderPickRecordVo;
+import com.ruoyi.production.service.ProductionOrderPickRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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.List;
/**
* <p>
@@ -13,6 +23,15 @@
*/
@RestController
@RequestMapping("/productionOrderPickRecord")
+@Tag(name = "鐢熶骇璁㈠崟棰嗘枡璁板綍")
+@RequiredArgsConstructor
public class ProductionOrderPickRecordController {
+ private final ProductionOrderPickRecordService productionOrderPickRecordService;
+
+ @GetMapping("/feeding")
+ @Operation(summary = "鏌ヨ琛ユ枡璁板綍")
+ public R<List<ProductionOrderPickRecordVo>> listFeedingRecord(ProductionOrderPickRecordDto dto) {
+ return R.ok(productionOrderPickRecordService.listFeedingRecord(dto));
+ }
}
diff --git a/src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java b/src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java
index dc909cd..6b18e1c 100644
--- a/src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java
+++ b/src/main/java/com/ruoyi/production/mapper/ProductionOrderPickRecordMapper.java
@@ -1,6 +1,8 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.production.bean.dto.ProductionOrderPickRecordDto;
+import com.ruoyi.production.bean.vo.ProductionOrderPickRecordVo;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import org.apache.ibatis.annotations.Mapper;
@@ -20,5 +22,6 @@
public interface ProductionOrderPickRecordMapper extends BaseMapper<ProductionOrderPickRecord> {
List<ProductionOrderPickVo> listPickedDetailByOrderId(@Param("productionOrderId") Long productionOrderId);
-}
+ List<ProductionOrderPickRecordVo> listFeedingRecord(ProductionOrderPickRecordDto dto);
+}
diff --git a/src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java b/src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java
index a15270c..61c5d19 100644
--- a/src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java
+++ b/src/main/java/com/ruoyi/production/pojo/ProductionOrderPick.java
@@ -66,6 +66,19 @@
@Schema(description = "闇�姹傛暟閲�")
private BigDecimal demandedQuantity;
+ @Schema(description = "琛ユ枡鎬婚噺")
+ private BigDecimal feedingQty;
+
+ @Schema(description = "閫�鏂欐暟閲�")
+ private BigDecimal returnQty;
+
+ @Schema(description = "瀹為檯鏁伴噺")
+ private BigDecimal actualQty;
+
+ @Schema(description = "鏄惁宸查��鏂�")
+ @TableField("is_returned")
+ private Boolean returned;
+
@Schema(description = "鏄惁bom棰嗘枡")
@TableField("is_bom")
private Boolean bom;
diff --git a/src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java b/src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java
index 75bf484..4f0dc49 100644
--- a/src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java
+++ b/src/main/java/com/ruoyi/production/pojo/ProductionOrderPickRecord.java
@@ -57,6 +57,9 @@
@Schema(description = "澶囨敞")
private String remark;
+ @Schema(description = "琛ユ枡鍘熷洜")
+ private String feedingReason;
+
@Schema(description = "鍒涘缓鏃堕棿")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
diff --git a/src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java b/src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java
index 4787929..ebd6e27 100644
--- a/src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java
+++ b/src/main/java/com/ruoyi/production/service/ProductionOrderPickRecordService.java
@@ -1,7 +1,11 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.production.bean.dto.ProductionOrderPickRecordDto;
+import com.ruoyi.production.bean.vo.ProductionOrderPickRecordVo;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
+
+import java.util.List;
/**
* <p>
@@ -13,4 +17,5 @@
*/
public interface ProductionOrderPickRecordService extends IService<ProductionOrderPickRecord> {
+ List<ProductionOrderPickRecordVo> listFeedingRecord(ProductionOrderPickRecordDto dto);
}
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java
index e852999..1cf7c1e 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java
@@ -1,10 +1,15 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.production.bean.dto.ProductionOrderPickRecordDto;
+import com.ruoyi.production.bean.vo.ProductionOrderPickRecordVo;
import com.ruoyi.production.mapper.ProductionOrderPickRecordMapper;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
import com.ruoyi.production.service.ProductionOrderPickRecordService;
import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
/**
* <p>
@@ -17,4 +22,11 @@
@Service
public class ProductionOrderPickRecordServiceImpl extends ServiceImpl<ProductionOrderPickRecordMapper, ProductionOrderPickRecord> implements ProductionOrderPickRecordService {
+ @Override
+ public List<ProductionOrderPickRecordVo> listFeedingRecord(ProductionOrderPickRecordDto dto) {
+ if (dto == null || dto.getProductionOrderId() == null || dto.getPickId() == null) {
+ return Collections.emptyList();
+ }
+ return baseMapper.listFeedingRecord(dto);
+ }
}
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
index 8f2f728..beedbb5 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -39,6 +39,9 @@
@RequiredArgsConstructor
public class ProductionOrderPickServiceImpl extends ServiceImpl<ProductionOrderPickMapper, ProductionOrderPick> implements ProductionOrderPickService {
+ private static final byte PICK_TYPE_NORMAL = 1;
+ private static final byte PICK_TYPE_FEEDING = 2;
+
private final ProductionOrderMapper productionOrderMapper;
private final ProductionOperationTaskMapper productionOperationTaskMapper;
private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
@@ -68,6 +71,7 @@
orderPick.setTechnologyOperationId(resolvedDto.getTechnologyOperationId());
orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity());
orderPick.setBom(resolvedDto.getBom());
+ orderPick.setReturned(false);
baseMapper.insert(orderPick);
insertPickRecord(orderPick.getId(),
@@ -79,7 +83,8 @@
BigDecimal.ZERO,
resolvedDto.getPickQuantity(),
resolvedDto.getPickType(),
- resolvedDto.getRemark());
+ resolvedDto.getRemark(),
+ resolvedDto.getFeedingReason());
}
return true;
}
@@ -105,6 +110,15 @@
Map<Long, ProductionOrderPick> existingPickMap = existingPickList.stream()
.filter(item -> item.getId() != null)
.collect(Collectors.toMap(ProductionOrderPick::getId, Function.identity(), (a, b) -> a));
+
+ if (isFeedingRequest(dto)) {
+ processFeedingPickItems(dto, existingPickMap, productionOrderId);
+ return true;
+ }
+ if (isReturnRequest(dto)) {
+ processReturnPickItems(dto, existingPickMap, productionOrderId);
+ return true;
+ }
processDeletePickIds(dto, existingPickMap, productionOrderId);
@@ -174,7 +188,8 @@
oldQuantity,
BigDecimal.ZERO,
rootDto.getPickType(),
- rootDto.getRemark());
+ rootDto.getRemark(),
+ rootDto.getFeedingReason());
existingPickMap.remove(deleteId);
}
}
@@ -191,7 +206,7 @@
.filter(item -> item.getId() != null)
.filter(item -> Objects.equals(item.getProductionOrderId(), productionOrderId))
.filter(item -> !keepPickIdSet.contains(item.getId()))
- .collect(Collectors.toList());
+ .toList();
for (ProductionOrderPick missingPick : missingPickList) {
String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo());
BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity());
@@ -209,7 +224,8 @@
oldQuantity,
BigDecimal.ZERO,
rootDto.getPickType(),
- rootDto.getRemark());
+ rootDto.getRemark(),
+ rootDto.getFeedingReason());
existingPickMap.remove(missingPick.getId());
}
}
@@ -230,6 +246,7 @@
orderPick.setTechnologyOperationId(dto.getTechnologyOperationId());
orderPick.setDemandedQuantity(dto.getDemandedQuantity());
orderPick.setBom(dto.getBom());
+ orderPick.setReturned(false);
baseMapper.insert(orderPick);
insertPickRecord(orderPick.getId(),
@@ -241,7 +258,111 @@
BigDecimal.ZERO,
dto.getPickQuantity(),
dto.getPickType(),
- dto.getRemark());
+ dto.getRemark(),
+ dto.getFeedingReason());
+ }
+
+ private void processFeedingPickItems(ProductionOrderPickDto rootDto,
+ Map<Long, ProductionOrderPick> existingPickMap,
+ Long productionOrderId) {
+ List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
+ for (int i = 0; i < pickItems.size(); i++) {
+ int rowNo = i + 1;
+ ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
+ if (isEmptyUpdateItem(resolvedDto)) {
+ continue;
+ }
+ if (!isFeedingPick(resolvedDto)) {
+ throw new ServiceException("琛ユ枡璇锋眰涓殑棰嗘枡绫诲瀷蹇呴』鍏ㄩ儴涓�2");
+ }
+ if (resolvedDto.getProductionOrderId() == null) {
+ resolvedDto.setProductionOrderId(productionOrderId);
+ }
+ validateFeedingParam(resolvedDto, rowNo);
+
+ ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
+ if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢鏂欒褰曚笉瀛樺湪鎴栦笉灞炰簬褰撳墠璁㈠崟");
+ }
+ addFeedingPick(resolvedDto, oldPick, rowNo);
+ }
+ }
+
+ private void addFeedingPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
+ if (dto.getProductModelId() != null && !Objects.equals(dto.getProductModelId(), oldPick.getProductModelId())) {
+ throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欎骇鍝佽鏍间笌棰嗘枡璁板綍涓嶄竴鑷�");
+ }
+ Long productModelId = oldPick.getProductModelId();
+ List<String> batchNoList = resolveBatchNoList(dto);
+ String inventoryBatchNo = batchNoList.isEmpty()
+ ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo())
+ : pickInventoryBatchNo(batchNoList);
+ BigDecimal feedingQuantity = dto.getFeedingQuantity();
+
+ subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
+
+ BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId());
+ BigDecimal afterFeedingQty = beforeFeedingQty.add(feedingQuantity);
+ insertPickRecord(oldPick.getId(),
+ dto.getProductionOrderId(),
+ dto.getProductionOperationTaskId(),
+ productModelId,
+ inventoryBatchNo,
+ feedingQuantity,
+ beforeFeedingQty,
+ afterFeedingQty,
+ PICK_TYPE_FEEDING,
+ dto.getRemark(),
+ dto.getFeedingReason());
+
+ ProductionOrderPick updatePick = new ProductionOrderPick();
+ updatePick.setId(oldPick.getId());
+ updatePick.setFeedingQty(afterFeedingQty);
+ updatePick.setActualQty(calculateActualQty(oldPick, afterFeedingQty));
+ int affected = baseMapper.updateById(updatePick);
+ if (affected <= 0) {
+ throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欐�婚噺鏇存柊澶辫触");
+ }
+ oldPick.setFeedingQty(afterFeedingQty);
+ oldPick.setActualQty(updatePick.getActualQty());
+ }
+
+ private void processReturnPickItems(ProductionOrderPickDto rootDto,
+ Map<Long, ProductionOrderPick> existingPickMap,
+ Long productionOrderId) {
+ List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
+ for (int i = 0; i < pickItems.size(); i++) {
+ int rowNo = i + 1;
+ ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
+ if (isEmptyUpdateItem(resolvedDto)) {
+ continue;
+ }
+ if (resolvedDto.getProductionOrderId() == null) {
+ resolvedDto.setProductionOrderId(productionOrderId);
+ }
+ validateReturnParam(resolvedDto, rowNo);
+
+ ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
+ if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢鏂欒褰曚笉瀛樺湪鎴栦笉灞炰簬褰撳墠璁㈠崟");
+ }
+ updateReturnPick(resolvedDto, oldPick, rowNo);
+ }
+ }
+
+ private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
+ ProductionOrderPick updatePick = new ProductionOrderPick();
+ updatePick.setId(oldPick.getId());
+ updatePick.setReturnQty(dto.getReturnQty());
+ updatePick.setActualQty(dto.getActualQty());
+ updatePick.setReturned(true);
+ int affected = baseMapper.updateById(updatePick);
+ if (affected <= 0) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢��鏂欎俊鎭洿鏂板け璐�");
+ }
+ oldPick.setReturnQty(updatePick.getReturnQty());
+ oldPick.setActualQty(updatePick.getActualQty());
+ oldPick.setReturned(true);
}
private void updateExistingPick(ProductionOrderPickDto dto,
@@ -304,7 +425,8 @@
oldQuantity,
newQuantity,
dto.getPickType(),
- dto.getRemark());
+ dto.getRemark(),
+ dto.getFeedingReason());
}
}
@@ -317,7 +439,8 @@
BigDecimal beforeQuantity,
BigDecimal afterQuantity,
Byte pickType,
- String remark) {
+ String remark,
+ String feedingReason) {
ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord();
pickRecord.setPickId(pickId);
pickRecord.setProductionOrderId(productionOrderId);
@@ -327,8 +450,9 @@
pickRecord.setPickQuantity(defaultDecimal(pickQuantity));
pickRecord.setBeforeQuantity(defaultDecimal(beforeQuantity));
pickRecord.setAfterQuantity(defaultDecimal(afterQuantity));
- pickRecord.setPickType(pickType == null ? (byte) 1 : pickType);
+ pickRecord.setPickType(pickType == null ? PICK_TYPE_NORMAL : pickType);
pickRecord.setRemark(remark);
+ pickRecord.setFeedingReason(feedingReason);
productionOrderPickRecordMapper.insert(pickRecord);
}
@@ -405,6 +529,11 @@
&& StringUtils.isEmpty(dto.getBatchNo())
&& (dto.getBatchNoList() == null || dto.getBatchNoList().isEmpty())
&& dto.getPickType() == null
+ && dto.getFeedingQuantity() == null
+ && StringUtils.isEmpty(dto.getFeedingReason())
+ && dto.getReturnQty() == null
+ && dto.getActualQty() == null
+ && dto.getReturned() == null
&& dto.getProductionOperationTaskId() == null
&& dto.getTechnologyOperationId() == null
&& StringUtils.isEmpty(dto.getOperationName())
@@ -440,10 +569,15 @@
merged.setPickQuantity(itemDto.getPickQuantity());
merged.setPickType(itemDto.getPickType());
merged.setRemark(itemDto.getRemark());
+ merged.setFeedingReason(itemDto.getFeedingReason());
+ merged.setFeedingQuantity(itemDto.getFeedingQuantity());
merged.setTechnologyOperationId(itemDto.getTechnologyOperationId());
merged.setOperationName(itemDto.getOperationName());
merged.setDemandedQuantity(itemDto.getDemandedQuantity());
merged.setBom(itemDto.getBom());
+ merged.setReturnQty(itemDto.getReturnQty());
+ merged.setActualQty(itemDto.getActualQty());
+ merged.setReturned(itemDto.getReturned());
}
if (merged.getId() == null) {
merged.setId(rootDto.getId());
@@ -472,6 +606,12 @@
if (merged.getRemark() == null) {
merged.setRemark(rootDto.getRemark());
}
+ if (merged.getFeedingReason() == null) {
+ merged.setFeedingReason(rootDto.getFeedingReason());
+ }
+ if (merged.getFeedingQuantity() == null) {
+ merged.setFeedingQuantity(rootDto.getFeedingQuantity());
+ }
if (merged.getTechnologyOperationId() == null) {
merged.setTechnologyOperationId(rootDto.getTechnologyOperationId());
}
@@ -483,6 +623,15 @@
}
if (merged.getBom() == null) {
merged.setBom(rootDto.getBom());
+ }
+ if (merged.getReturnQty() == null) {
+ merged.setReturnQty(rootDto.getReturnQty());
+ }
+ if (merged.getActualQty() == null) {
+ merged.setActualQty(rootDto.getActualQty());
+ }
+ if (merged.getReturned() == null) {
+ merged.setReturned(rootDto.getReturned());
}
return merged;
}
@@ -497,9 +646,89 @@
if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐暟閲忓繀椤诲ぇ浜�0");
}
- if (dto.getPickType() != null && dto.getPickType() != 1 && dto.getPickType() != 2) {
+ if (dto.getPickType() != null && dto.getPickType() != PICK_TYPE_NORMAL && dto.getPickType() != PICK_TYPE_FEEDING) {
throw new ServiceException("绗�" + rowNo + "鏉¢鏂欑被鍨嬪彧鑳芥槸1鎴�2");
}
+ }
+
+ private void validateFeedingParam(ProductionOrderPickDto dto, int rowNo) {
+ if (dto.getProductionOrderId() == null) {
+ throw new ServiceException("绗�" + rowNo + "鏉$敓浜ц鍗旾D涓嶈兘涓虹┖");
+ }
+ if (dto.getId() == null) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢鏂橧D涓嶈兘涓虹┖");
+ }
+ if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new ServiceException("绗�" + rowNo + "鏉℃湰娆¤ˉ鏂欐暟閲忓繀椤诲ぇ浜�0");
+ }
+ if (!isFeedingPick(dto)) {
+ throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欑被鍨嬪繀椤讳负2");
+ }
+ }
+
+ private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) {
+ if (dto.getProductionOrderId() == null) {
+ throw new ServiceException("绗�" + rowNo + "鏉$敓浜ц鍗旾D涓嶈兘涓虹┖");
+ }
+ if (dto.getId() == null) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢鏂橧D涓嶈兘涓虹┖");
+ }
+ if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("绗�" + rowNo + "鏉¢��鏂欐暟閲忎笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+ }
+ if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("绗�" + rowNo + "鏉″疄闄呮暟閲忎笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+ }
+ }
+
+ private boolean isFeedingRequest(ProductionOrderPickDto dto) {
+ if (isFeedingPick(dto)) {
+ return true;
+ }
+ if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
+ return false;
+ }
+ return dto.getPickList().stream()
+ .filter(Objects::nonNull)
+ .anyMatch(this::isFeedingPick);
+ }
+
+ private boolean isFeedingPick(ProductionOrderPickDto dto) {
+ return dto != null && Objects.equals(dto.getPickType(), PICK_TYPE_FEEDING);
+ }
+
+ private boolean isReturnRequest(ProductionOrderPickDto dto) {
+ if (isReturnPick(dto)) {
+ return true;
+ }
+ if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
+ return false;
+ }
+ return dto.getPickList().stream()
+ .filter(Objects::nonNull)
+ .anyMatch(this::isReturnPick);
+ }
+
+ private boolean isReturnPick(ProductionOrderPickDto dto) {
+ return dto != null && Boolean.TRUE.equals(dto.getReturned());
+ }
+
+ private BigDecimal sumFeedingQuantity(Long productionOrderId, Long pickId) {
+ List<ProductionOrderPickRecord> feedingRecords = productionOrderPickRecordMapper.selectList(
+ Wrappers.<ProductionOrderPickRecord>lambdaQuery()
+ .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)
+ .eq(ProductionOrderPickRecord::getPickId, pickId)
+ .eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_FEEDING));
+ return feedingRecords.stream()
+ .map(ProductionOrderPickRecord::getPickQuantity)
+ .map(this::defaultDecimal)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ private BigDecimal calculateActualQty(ProductionOrderPick pick, BigDecimal feedingQty) {
+ return defaultDecimal(pick.getQuantity())
+ .add(defaultDecimal(feedingQty))
+ .subtract(defaultDecimal(pick.getReturnQty()));
}
private String normalizeBatchNo(String batchNo) {
@@ -654,4 +883,3 @@
return value == null ? BigDecimal.ZERO : value;
}
}
-
diff --git a/src/main/resources/application-dev-pro.yml b/src/main/resources/application-dev-pro.yml
index f94aaa2..1552370 100644
--- a/src/main/resources/application-dev-pro.yml
+++ b/src/main/resources/application-dev-pro.yml
@@ -142,14 +142,15 @@
enabled: false
# redis 閰嶇疆
data:
+ mongodb:
+ uri: mongodb://114.132.189.42:9028/chat_memory_db
+ # redis 閰嶇疆
redis:
# 鍦板潃
- # host: 127.0.0.1
- host: 47.114.74.44
+ host: 127.0.0.1
# host: 172.17.0.1
# 绔彛锛岄粯璁や负6379
- # port: 6379
- port: 6399
+ port: 6379
# 鏁版嵁搴撶储寮�
database: 0
# 瀵嗙爜
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index c545068..ef003c9 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -257,3 +257,5 @@
file:
temp-dir: D:/ruoyi/temp/uploads # 涓存椂鐩綍
upload-dir: D:/ruoyi/prod/uploads # 姝e紡鐩綍
+knowledge:
+ one: D:\鏂扮枂澶х綏绱犱紒涓氫骇鍝佷綋绯昏鏄庢枃妗�.md
diff --git a/src/main/resources/application-new-pro.yml b/src/main/resources/application-new-pro.yml
new file mode 100644
index 0000000..925ea91
--- /dev/null
+++ b/src/main/resources/application-new-pro.yml
@@ -0,0 +1,268 @@
+# 椤圭洰鐩稿叧閰嶇疆
+ruoyi:
+ # 鍚嶇О
+ name: RuoYi
+ # 鐗堟湰
+ version: 3.8.9
+ # 鐗堟潈骞翠唤
+ copyrightYear: 2025
+ # 鏂囦欢璺緞 绀轰緥锛� Windows閰嶇疆D:/ruoyi/uploadPath锛孡inux閰嶇疆 /home/ruoyi/uploadPath锛�
+ profile: /javaWork/product-inventory-management/file
+
+ # 鑾峰彇ip鍦板潃寮�鍏�
+ addressEnabled: false
+ # 楠岃瘉鐮佺被鍨� math 鏁板瓧璁$畻 char 瀛楃楠岃瘉
+ captchaType: math
+ # 鍗忓悓瀹℃壒缂栧彿鍓嶇紑(閰嶇疆鏂囦欢鍚庣紑鍛藉悕)
+ approvalNumberPrefix: NEW
+
+ # 涓帹 Unipush 閰嶇疆
+ getui:
+ appId: PfjyAAE0FK64FaO1w2CMb1
+ appKey: zTMb831OEL6J4GK1uE3Ob4
+ masterSecret: K1GFtsv42v61tXGnF7SGE5
+ domain: https://restapi.getui.cn/v2/
+ # 绂荤嚎鎺ㄩ�佷娇鐢ㄧ殑鍖呭悕/缁勪欢鍚�
+ intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
+
+# 寮�鍙戠幆澧冮厤缃�
+server:
+ # 鏈嶅姟鍣ㄧ殑HTTP绔彛锛岄粯璁や负8080
+ port: 9003
+ servlet:
+ # 搴旂敤鐨勮闂矾寰�
+ context-path: /
+ tomcat:
+ # tomcat鐨刄RI缂栫爜
+ uri-encoding: UTF-8
+ # 杩炴帴鏁版弧鍚庣殑鎺掗槦鏁帮紝榛樿涓�100
+ accept-count: 1000
+ threads:
+ # tomcat鏈�澶х嚎绋嬫暟锛岄粯璁や负200
+ max: 800
+ # Tomcat鍚姩鍒濆鍖栫殑绾跨▼鏁帮紝榛樿鍊�10
+ min-spare: 100
+
+# 鏃ュ織閰嶇疆
+logging:
+ level:
+ com.ruoyi: warn
+ org.springframework: warn
+
+minio:
+ endpoint: http://114.132.189.42/
+ port: 7019
+ secure: false
+ accessKey: admin
+ secretKey: 12345678
+ preview-expiry: 24 # 棰勮鍦板潃榛樿24灏忔椂
+ default-bucket: jxc
+# 鐢ㄦ埛閰嶇疆
+user:
+ password:
+ # 瀵嗙爜鏈�澶ч敊璇鏁�
+ maxRetryCount: 5
+ # 瀵嗙爜閿佸畾鏃堕棿锛堥粯璁�10鍒嗛挓锛�
+ lockTime: 10
+
+# Spring閰嶇疆
+spring:
+ datasource:
+ type: com.alibaba.druid.pool.DruidDataSource
+ driverClassName: com.mysql.cj.jdbc.Driver
+ druid:
+ # 涓诲簱鏁版嵁婧�
+ master:
+ url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: root
+ password: xd@123456..
+ # 浠庡簱鏁版嵁婧�
+ slave:
+ # 浠庢暟鎹簮寮�鍏�/榛樿鍏抽棴
+ enabled: false
+ url:
+ username:
+ password:
+ # 鍒濆杩炴帴鏁�
+ initialSize: 5
+ # 鏈�灏忚繛鎺ユ睜鏁伴噺
+ minIdle: 10
+ # 鏈�澶ц繛鎺ユ睜鏁伴噺
+ maxActive: 20
+ # 閰嶇疆鑾峰彇杩炴帴绛夊緟瓒呮椂鐨勬椂闂�
+ maxWait: 60000
+ # 閰嶇疆杩炴帴瓒呮椂鏃堕棿
+ connectTimeout: 30000
+ # 閰嶇疆缃戠粶瓒呮椂鏃堕棿
+ socketTimeout: 60000
+ # 閰嶇疆闂撮殧澶氫箙鎵嶈繘琛屼竴娆℃娴嬶紝妫�娴嬮渶瑕佸叧闂殑绌洪棽杩炴帴锛屽崟浣嶆槸姣
+ timeBetweenEvictionRunsMillis: 60000
+ # 閰嶇疆涓�涓繛鎺ュ湪姹犱腑鏈�灏忕敓瀛樼殑鏃堕棿锛屽崟浣嶆槸姣
+ minEvictableIdleTimeMillis: 300000
+ # 閰嶇疆涓�涓繛鎺ュ湪姹犱腑鏈�澶х敓瀛樼殑鏃堕棿锛屽崟浣嶆槸姣
+ maxEvictableIdleTimeMillis: 900000
+ # 閰嶇疆妫�娴嬭繛鎺ユ槸鍚︽湁鏁�
+ validationQuery: SELECT 1 FROM DUAL
+ testWhileIdle: true
+ testOnBorrow: false
+ testOnReturn: false
+ webStatFilter:
+ enabled: true
+ statViewServlet:
+ enabled: true
+ # 璁剧疆鐧藉悕鍗曪紝涓嶅~鍒欏厑璁告墍鏈夎闂�
+ allow:
+ url-pattern: /druid/*
+ # 鎺у埗鍙扮鐞嗙敤鎴峰悕鍜屽瘑鐮�
+ login-username: ruoyi
+ login-password: 123456
+ filter:
+ stat:
+ enabled: true
+ # 鎱QL璁板綍
+ log-slow-sql: true
+ slow-sql-millis: 1000
+ merge-sql: true
+ wall:
+ config:
+ multi-statement-allow: true
+ # 璧勬簮淇℃伅
+ messages:
+ # 鍥介檯鍖栬祫婧愭枃浠惰矾寰�
+ basename: i18n/messages
+ # 鏂囦欢涓婁紶
+ servlet:
+ multipart:
+ # 鍗曚釜鏂囦欢澶у皬
+ max-file-size: 1GB
+ # 璁剧疆鎬讳笂浼犵殑鏂囦欢澶у皬
+ max-request-size: 2GB
+ # 鏈嶅姟妯″潡
+ devtools:
+ restart:
+ # 鐑儴缃插紑鍏�
+ enabled: false
+ # redis 閰嶇疆
+ data:
+ mongodb:
+ uri: mongodb://114.132.189.42:9028/chat_memory_db
+ # redis 閰嶇疆
+ redis:
+ # 鍦板潃
+# host: 127.0.0.1
+ host: 172.17.0.1
+ # 绔彛锛岄粯璁や负6379
+ port: 6379
+ # 鏁版嵁搴撶储寮�
+ database: 0
+ # 瀵嗙爜
+ # password: root2022!
+ password:
+
+ # 杩炴帴瓒呮椂鏃堕棿
+ timeout: 10s
+ lettuce:
+ pool:
+ # 杩炴帴姹犱腑鐨勬渶灏忕┖闂茶繛鎺�
+ min-idle: 0
+ # 杩炴帴姹犱腑鐨勬渶澶х┖闂茶繛鎺�
+ max-idle: 8
+ # 杩炴帴姹犵殑鏈�澶ф暟鎹簱杩炴帴鏁�
+ max-active: 8
+ # #杩炴帴姹犳渶澶ч樆濉炵瓑寰呮椂闂达紙浣跨敤璐熷�艰〃绀烘病鏈夐檺鍒讹級
+ max-wait: -1ms
+
+ # Quartz瀹氭椂浠诲姟閰嶇疆锛堟柊澧為儴鍒嗭級
+ quartz:
+ job-store-type: jdbc # 浣跨敤鏁版嵁搴撳瓨鍌�
+ jdbc:
+ initialize-schema: never # 棣栨杩愯鏃惰嚜鍔ㄥ垱寤鸿〃缁撴瀯锛屾垚鍔熷悗鏀逛负never
+ schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql # MySQL琛ㄧ粨鏋勮剼鏈�
+ properties:
+ org:
+ quartz:
+ scheduler:
+ instanceName: RuoYiScheduler
+ instanceId: AUTO
+ jobStore:
+ class: org.quartz.impl.jdbcjobstore.JobStoreTX
+ driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQL閫傞厤
+ tablePrefix: qrtz_ # 琛ㄥ悕鍓嶇紑锛屼笌鑴氭湰涓�鑷�
+ isClustered: false # 鍗曡妭鐐规ā寮忥紙闆嗙兢闇�鏀逛负true锛�
+ clusterCheckinInterval: 10000
+ txIsolationLevelSerializable: true
+ threadPool:
+ class: org.quartz.simpl.SimpleThreadPool
+ threadCount: 10 # 绾跨▼姹犲ぇ灏�
+ threadPriority: 5
+ makeThreadsDaemons: true
+ updateCheck: false # 鍏抽棴鐗堟湰妫�鏌�
+# token閰嶇疆
+token:
+ # 浠ょ墝鑷畾涔夋爣璇�
+ header: Authorization
+ # 浠ょ墝瀵嗛挜
+ secret: abcdefghijklmnopqrstuvwxyz
+ # 浠ょ墝鏈夋晥鏈燂紙榛樿30鍒嗛挓锛�
+ expireTime: 450
+
+# MyBatis Plus閰嶇疆
+mybatis-plus:
+ # 鎼滅储鎸囧畾鍖呭埆鍚� 鏍规嵁鑷繁鐨勯」鐩潵
+ typeAliasesPackage: com.ruoyi.**.pojo
+ # 閰嶇疆mapper鐨勬壂鎻忥紝鎵惧埌鎵�鏈夌殑mapper.xml鏄犲皠鏂囦欢
+ mapperLocations: classpath*:mapper/**/*Mapper.xml
+ # 鍔犺浇鍏ㄥ眬鐨勯厤缃枃浠�
+ configLocation: classpath:mybatis/mybatis-config.xml
+ global-config:
+ enable-sql-runner: true
+ db-config:
+ id-type: auto
+
+# PageHelper鍒嗛〉鎻掍欢
+pagehelper:
+ helperDialect: mysql
+ supportMethodsArguments: true
+ params: count=countSql
+
+# Swagger閰嶇疆
+swagger:
+ # 鏄惁寮�鍚痵wagger
+ enabled: true
+ # 璇锋眰鍓嶇紑
+ pathMapping: /dev-api
+
+# 闃叉XSS鏀诲嚮
+xss:
+ # 杩囨护寮�鍏�
+ enabled: true
+ # 鎺掗櫎閾炬帴锛堝涓敤閫楀彿鍒嗛殧锛�
+ excludes: /system/notice
+ # 鍖归厤閾炬帴
+ urlPatterns: /system/*,/monitor/*,/tool/*
+
+# 浠g爜鐢熸垚
+gen:
+ # 浣滆��
+ author: ruoyi
+ # 榛樿鐢熸垚鍖呰矾寰� system 闇�鏀规垚鑷繁鐨勬ā鍧楀悕绉� 濡� system monitor tool
+ packageName: com.ruoyi.project.system
+ # 鑷姩鍘婚櫎琛ㄥ墠缂�锛岄粯璁ゆ槸true
+ autoRemovePre: false
+ # 琛ㄥ墠缂�锛堢敓鎴愮被鍚嶄笉浼氬寘鍚〃鍓嶇紑锛屽涓敤閫楀彿鍒嗛殧锛�
+ tablePrefix: sys_
+ # 鏄惁鍏佽鐢熸垚鏂囦欢瑕嗙洊鍒版湰鍦帮紙鑷畾涔夎矾寰勶級锛岄粯璁や笉鍏佽
+ allowOverwrite: false
+
+# 鏂囦欢涓婁紶閰嶇疆
+file:
+ temp-dir: D:/ruoyi/temp/uploads # 涓存椂鐩綍
+ upload-dir: D:/ruoyi/prod/uploads # 姝e紡鐩綍
+ path: C:/Users/12631/Desktop/download/uploads # 涓婁紶鐩綍
+ urlPrefix: /common # 閾炬帴鍓嶇紑
+ domain: http://127.0.0.1:7003 # 鍩熷悕鍓嶇紑
+ expired: 120 # 杩囨湡鏃堕棿(鍗曚綅:鍒嗛挓)
+ useLimit: 10 # 浣跨敤娆℃暟
+ compress: true # 鏄惁鍘嬬缉
+ needCompressSize: 10MB # 鍘嬬缉闃堝��
+ compressQuality: 0.5 # 鍘嬬缉璐ㄩ噺(0.0-1.0)
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 70324c6..87fceb2 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -38,5 +38,4 @@
model-name: "deepseek-r1:1.5b"
log-requests: true
log-responses: true
-knowledge:
- one: D:\鏂扮枂澶х綏绱犱紒涓氫骇鍝佷綋绯昏鏄庢枃妗�.md
+
diff --git a/src/main/resources/mapper/production/ProductionOrderPickMapper.xml b/src/main/resources/mapper/production/ProductionOrderPickMapper.xml
index 7dd8a1f..8b0a0d7 100644
--- a/src/main/resources/mapper/production/ProductionOrderPickMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOrderPickMapper.xml
@@ -17,12 +17,17 @@
<result column="operation_name" property="operationName" />
<result column="technology_operation_id" property="technologyOperationId" />
<result column="demanded_quantity" property="demandedQuantity" />
+ <result column="feeding_qty" property="feedingQty" />
+ <result column="return_qty" property="returnQty" />
+ <result column="actual_qty" property="actualQty" />
+ <result column="is_returned" property="returned" />
<result column="is_bom" property="bom" />
</resultMap>
<select id="listPickedDetailByOrderId" resultType="com.ruoyi.production.bean.vo.ProductionOrderPickVo">
select pop.*,
pop.is_bom as bom,
+ pop.is_returned as returned,
pop.quantity as pickQuantity,
p.product_name as productName,
pm.model as model,
diff --git a/src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml b/src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml
index 818f4d9..4614ef7 100644
--- a/src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOrderPickRecordMapper.xml
@@ -15,6 +15,7 @@
<result column="after_quantity" property="afterQuantity" />
<result column="pick_type" property="pickType" />
<result column="remark" property="remark" />
+ <result column="feeding_reason" property="feedingReason" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="create_user" property="createUser" />
@@ -36,4 +37,24 @@
order by popr.create_time desc, popr.id desc
</select>
+ <select id="listFeedingRecord" resultType="com.ruoyi.production.bean.vo.ProductionOrderPickRecordVo">
+ select popr.*,
+ poro.operation_name as operationName,
+ p.product_name as productName,
+ pm.model as model,
+ pm.unit as unit,
+ coalesce(su.nick_name, su.user_name) as supplementUserName,
+ popr.create_time as supplementTime
+ from production_order_pick_record popr
+ left join production_operation_task pot on popr.production_operation_task_id = pot.id
+ left join production_order_routing_operation poro on pot.technology_routing_operation_id = poro.id
+ left join product_model pm on popr.product_model_id = pm.id
+ left join product p on pm.product_id = p.id
+ left join sys_user su on popr.create_user = su.user_id
+ where popr.production_order_id = #{productionOrderId}
+ and popr.pick_id = #{pickId}
+ and popr.pick_type = 2
+ order by popr.create_time desc, popr.id desc
+ </select>
+
</mapper>
--
Gitblit v1.9.3