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åå²ä¼è¯å æ°æ®è¡¨'; 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> 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; } 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); } 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读åæå®ç®å½ä¸çç¥è¯åºææ¡£ // //并使ç¨é»è®¤çææ¡£è§£æå¨å¯¹ææ¡£è¿è¡è§£æ //// 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çDocument对象 //// List<Document> documents = new ArrayList<>(); // 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); } } 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())); } } 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; } 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; } 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> { } 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; //åå¨å½åè天记å½å表çjsonå符串 private String content; private Date createTime; private Date updateTime; } 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; } 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); } 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"); } } 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)); } } 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(); } } 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ãpendingãprocessingãapprovedãrejectedãresubmitted", 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ãpendingãprocessingãapprovedãrejectedãresubmitted", 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 "æªæ¾å°å¯¹åºçå®¡æ¹æµç¨ï¼è¯·ç¡®è®¤æµç¨ç¼å·æ¯å¦æ£ç¡®ã"; 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", "æªæ¾å°å¯¹åºçå®¡æ¹æµç¨ï¼è¯·ç¡®è®¤æµç¨ç¼å·æ¯å¦æ£ç¡®ã", 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 = "æ§è¡å®¡æ¹å¨ä½ãaction åªæ¯æ approve æ rejectãapprove 表示éè¿ï¼reject 表示驳åã") public String reviewTodo( @P("æµç¨ç¼å· approveId") String approveId, @P("å¨ä½ï¼approve=éè¿ï¼reject=驳å") String action, @P(value = "审æ¹å¤æ³¨ï¼å¯ä¸ä¼ ", required = false) String remark) { ApproveProcess process = getProcessByApproveId(approveId); @Transactional(rollbackFor = Exception.class) @Tool(name = "审æ¹å¾ å", value = "æ§è¡å®¡æ¹å¨ä½ï¼action ä» æ¯æ approve æ rejectï¼ä¸åªè½å¤çå½åç»å½äººèªå·±çå¾ å®¡èç¹ã") public String reviewTodo(@ToolMemoryId String memoryId, @P("æµç¨ç¼å· approveId") String approveId, @P("å¨ä½ï¼approve=éè¿ï¼reject=驳å") 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) { } } 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"); } 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; } } 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; 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 { } 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; } 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)); } } 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); } 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; 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; 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); } 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); } } 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 + "æ¡ç产订åIDä¸è½ä¸ºç©º"); } if (dto.getId() == null) { throw new ServiceException("第" + rowNo + "æ¡é¢æIDä¸è½ä¸ºç©º"); } 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 + "æ¡ç产订åIDä¸è½ä¸ºç©º"); } if (dto.getId() == null) { throw new ServiceException("第" + rowNo + "æ¡é¢æIDä¸è½ä¸ºç©º"); } 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; } } 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 # å¯ç src/main/resources/application-dev.yml
@@ -257,3 +257,5 @@ file: temp-dir: D:/ruoyi/temp/uploads # 临æ¶ç®å½ upload-dir: D:/ruoyi/prod/uploads # æ£å¼ç®å½ knowledge: one: D:\æ°ç大ç½ç´ ä¼ä¸äº§åä½ç³»è¯´æææ¡£.md src/main/resources/application-new-pro.yml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,268 @@ # 项ç®ç¸å ³é ç½® ruoyi: # åç§° name: RuoYi # çæ¬ version: 3.8.9 # çæå¹´ä»½ copyrightYear: 2025 # æä»¶è·¯å¾ 示ä¾ï¼ Windowsé ç½®D:/ruoyi/uploadPathï¼Linuxé ç½® /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çURIç¼ç 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 # æ ¢SQLè®°å½ 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: # æ¯å¦å¼å¯swagger enabled: true # 请æ±åç¼ pathMapping: /dev-api # 鲿¢XSSæ»å» xss: # è¿æ»¤å¼å ³ enabled: true # æé¤é¾æ¥ï¼å¤ä¸ªç¨éå·åéï¼ excludes: /system/notice # å¹é 龿¥ urlPatterns: /system/*,/monitor/*,/tool/* # 代ç çæ 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 # æ£å¼ç®å½ 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) 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 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, 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>