doc/20260508_²É¹º¶àÎļþ·ÖÎö¸½¼þ´æ´¢ÓëÀúÊ·»ØÏÔÁªµ÷˵Ã÷.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,149 @@ # éè´å¤æä»¶åæéä»¶åå¨ä¸åå²åæ¾èè°è¯´æ > æ´æ°æ¶é´ï¼2026-05-08 > éç¨èå´ï¼éè´æºè½ä½å¤æä»¶åæ + åå²ä¼è¯éä»¶åæ¾ ## 1. åæ´èæ¯ å端已补é½ä»¥ä¸è½åï¼ 1. `POST /purchase-ai/analyze-files` ä¸ä¼ æ¶å åéä»¶å°æå¡å¨ï¼å ¬å ±è®¿é®ï¼ã 2. ææä»¶ç±»åè¿åå¯è®¿é®è·¯å¾ï¼ - å¾ç / PDFï¼ä¼å é¢è§è·¯å¾ - å ¶å®æä»¶ï¼ä¼å ä¸è½½è·¯å¾ 3. ç¨æ·æé®ä¸éä»¶è·¯å¾åå¼åå ¥ Mongoã 4. å岿¶æ¯æ¥å£å¯æâæ¶æ¯ç»´åº¦âåä¼ éä»¶è·¯å¾ï¼å端å¯ç´æ¥åæ¾éä»¶å¡çã --- ## 2. æ¥å£è¡ä¸º ### 2.1 夿件忿¥å£ ```http POST /purchase-ai/analyze-files Content-Type: multipart/form-data ``` 请æ±åæ°ï¼ - `files`: `MultipartFile[]`ï¼å¿ å¡«ï¼ - `message`: `string`ï¼å¯éï¼ - `memoryId`: `string`ï¼å¯éï¼ å端å¤çæµç¨ï¼ 1. è°ç¨ `StorageBlobService.upload(files, true)` ä¸ä¼ å¹¶ä¿åéä»¶ã 2. çæé件访é®è·¯å¾ï¼é¢è§ / ä¸è½½ï¼ã 3. å°âæ¬æ¬¡æé® + æ¬æ¬¡éä»¶è·¯å¾å表âåå ¥ Mongoã 4. ç»§ç»æ§è¡åææä»¶è§£æå AI åææµç¨ã å¯è½éè¯¯ï¼æµå¼ææ¬ï¼ï¼ - `æä»¶ä¸ä¼ 失败` - `ä¼è¯æä»¶ä¿¡æ¯ä¿å失败` ### 2.2 å岿¶æ¯æ¥å£ï¼å端éç¹ï¼ ```http GET /purchase-ai/history/messages/{memoryId} ``` æ¶æ¯å¯¹è±¡æ°å¢å¯éåæ®µ `filePaths`ï¼ä» ç¨æ·æ¶æ¯å¯è½æå¼ï¼ï¼ ```json { "role": "user | assistant | system | tool | unknown", "content": "æ¶æ¯ææ¬", "filePaths": [ "/common/preview/xxx?publicKey=...", "/common/download/yyy?publicKey=..." ] } ``` 说æï¼ 1. `filePaths` å¯è½ç¼ºå¤±æä¸ºç©ºï¼èä¼è¯ / æ®é对è¯ï¼ã 2. åæ¡ç¨æ·æ¶æ¯å¯è½å å«å¤ä¸ªéä»¶è·¯å¾ã 3. è¯¥åæ®µå·²ææ¶æ¯ç»´åº¦å¯¹é½ï¼å¯ç´æ¥ç¨äºåå²åæ¾ã --- ## 3. å端æ¹é å»ºè®®ï¼æå½åå®ç°è½å°ï¼ ### 3.1 åå²ååºæ¨¡å 建议å¨å岿¶æ¯åå§ç»æä¸ä¿ç `filePaths`ï¼ ```ts type AiHistoryMessage = { role: string; content: string; filePaths?: string[]; }; ``` ### 3.2 UI æ¶æ¯æ¨¡åæ å° è¥å端页é¢ç¨çæ¯ `localUploadFiles` 渲æéä»¶å¡çï¼å¦ `AIChatSidebar` å½åå®ç°ï¼ï¼å岿¶æ¯éå䏿¬¡æ å°ï¼ ```ts type LocalUploadFileItem = { previewId: string; name: string; size: number; type: string; isImage: boolean; previewUrl: string; rawFile: null; }; ``` æ å°è§åå»ºè®®ï¼ 1. ä» `role === 'user'` ä¸ `filePaths?.length > 0` æ¶çæ `localUploadFiles`ã 2. `previewId` ç¨ `${memoryId}-${messageIndex}-${fileIndex}` çæç¨³å®å¼ã 3. `name` å¯ä» URL è·¯å¾è§£æï¼è§£æå¤±è´¥ç¨ `file-{n}`ã 4. `isImage` 坿æ©å±å夿ï¼`png/jpg/jpeg/gif/webp/bmp/svg`ï¼ã 5. å¾çç `previewUrl` ç´æ¥ä½¿ç¨è·¯å¾ï¼éå¾çå¯ç½®ç©ºå¹¶èµ°å¾æ å±ç¤ºã ### 3.3 æ¶æ¯æ¸²æ 对äºç¨æ·æ¶æ¯ï¼ 1. æ£å¸¸æ¸²æ `content`ã 2. è¥åå¨ `localUploadFiles`ï¼æç´æ¥ä½¿ç¨ `filePaths`ï¼ï¼å¨æ¶æ¯ä¸æ¹æ¸²æéä»¶å表/å¡çã 3. 龿¥ç´æ¥ä½¿ç¨å端è¿åè·¯å¾ï¼ä¸åæ¼æ¥æäºæ¬¡æ¹åã > åç«¯å·²å®æâé¢è§/ä¸è½½è·¯å¾âéæ©ï¼å端åªè´è´£å±ç¤ºä¸æå¼ã ### 3.4 å ¼å®¹è¦æ± - `filePaths` 缺失ï¼ä¸æ¥éï¼ä¸æ¸²æéä»¶åºåã - èä¼è¯ï¼ç»§ç»æ `role/content` å±ç¤ºï¼ä¸å½±ååå²è®°å½æ¥çã - å¤éä»¶ï¼ä¿æé¡ºåºå±ç¤ºï¼é¿å æä¹±ç¨æ·ä¸ä¼ 顺åºã --- ## 4. Mongo åæ®µè¯´æï¼å端已å®ç°ï¼ `chat_messages` ææ¡£æ°å¢/使ç¨åæ®µï¼ - `analyzeUserQuestions: string[]` - `analyzeFilePaths: string[]`ï¼å ¼å®¹æ§åæ®µï¼ - `analyzeFilePathGroups: string[][]`ï¼æ¨èï¼ææé®åç»ï¼ å岿¶æ¯åä¼ æ¶ï¼å端ä¼å 读å `analyzeFilePathGroups`ï¼å¹¶å ¼å®¹ `analyzeFilePaths`ã --- ## 5. èè°éªæ¶æ¸ å 1. æ°å»ºä¼è¯ï¼ä¸ä¼ 1 å¼ å¾ç + 1 个 Excelï¼åéåæè¯·æ±ã 2. è°ç¨ `GET /purchase-ai/history/messages/{memoryId}`ï¼ç¡®è®¤ç¨æ·æ¶æ¯å« `filePaths` 䏿°éæ£ç¡®ã 3. å·æ°é¡µé¢åéæ°è¿å ¥åä¸ä¼è¯ï¼ç¡®è®¤éä»¶å¡çå¯åæ¾ã 4. åå«éªè¯ï¼ - å¾ç/PDF å¯é¢è§è®¿é® - å ¶å®æä»¶å¯ä¸è½½è®¿é® 5. éªè¯èä¼è¯ï¼æ `filePaths`ï¼å¯æ£å¸¸å±ç¤ºï¼é¡µé¢ä¸æ¥éã src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
@@ -31,7 +31,7 @@ public class AccountSubjectController { private final AccountSubjectService accountSubjectService; @GetMapping("list") @GetMapping("/list") @Log(title = "æ»è´¦ç§ç®æ°æ®éå", businessType = BusinessType.OTHER) @Operation(summary = "æ»è´¦ç§ç®å页æ¥è¯¢") public R<IPage<AccountSubjectVo>> AccountSubjectDtoList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) { src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -11,8 +11,10 @@ import com.ruoyi.ai.service.AiChatSessionService; import com.ruoyi.ai.service.AiFileTextExtractor; import com.ruoyi.ai.store.MongoChatMemoryStore; import com.ruoyi.basic.dto.StorageBlobVO; import com.ruoyi.basic.mapper.SupplierManageMapper; import com.ruoyi.basic.pojo.SupplierManage; import com.ruoyi.basic.service.StorageBlobService; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.security.LoginUser; @@ -50,10 +52,14 @@ import org.springframework.web.multipart.MultipartFile; import reactor.core.publisher.Flux; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Base64; import java.util.Arrays; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -63,9 +69,11 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; import java.nio.file.Files; @Tag(name = "éè´æºè½ä½") @RestController @@ -87,6 +95,7 @@ private final IPurchaseLedgerService purchaseLedgerService; private final IPaymentRegistrationService paymentRegistrationService; private final PurchaseReturnOrdersService purchaseReturnOrdersService; private final StorageBlobService storageBlobService; private final SupplierManageMapper supplierManageMapper; private final StreamingChatLanguageModel purchaseVisionStreamingChatModel; @@ -96,12 +105,13 @@ MongoChatMemoryStore mongoChatMemoryStore, AiChatSessionService aiChatSessionService, AiFileTextExtractor aiFileTextExtractor, ObjectMapper objectMapper, IPurchaseLedgerService purchaseLedgerService, IPaymentRegistrationService paymentRegistrationService, PurchaseReturnOrdersService purchaseReturnOrdersService, SupplierManageMapper supplierManageMapper, @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) { ObjectMapper objectMapper, IPurchaseLedgerService purchaseLedgerService, IPaymentRegistrationService paymentRegistrationService, PurchaseReturnOrdersService purchaseReturnOrdersService, StorageBlobService storageBlobService, SupplierManageMapper supplierManageMapper, @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) { this.purchaseAgent = purchaseAgent; this.purchaseIntentExecutor = purchaseIntentExecutor; this.aiSessionUserContext = aiSessionUserContext; @@ -112,6 +122,7 @@ this.purchaseLedgerService = purchaseLedgerService; this.paymentRegistrationService = paymentRegistrationService; this.purchaseReturnOrdersService = purchaseReturnOrdersService; this.storageBlobService = storageBlobService; this.supplierManageMapper = supplierManageMapper; this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel; } @@ -172,6 +183,19 @@ ? message : "请åæè¿äºéè´æä»¶ï¼æåå¯ç¨äºä¸å¡å¤ççæ°æ®ï¼å¹¶æ´çæå¾ 客æ·ç¡®è®¤çæ ¼å¼"; List<String> filePaths; try { List<StorageBlobVO> uploadedFiles = storageBlobService.upload(copyFilesForUpload(files), true); filePaths = resolveFileAccessPaths(uploadedFiles); } catch (Exception ex) { return Flux.just("æä»¶ä¸ä¼ 失败"); } try { mongoChatMemoryStore.appendAnalyzeFileContext(finalMemoryId, finalMessage, filePaths); } catch (Exception ex) { return Flux.just("ä¼è¯æä»¶ä¿¡æ¯ä¿å失败"); } String fileContent; try { fileContent = buildMultiFileContent(files); @@ -189,7 +213,7 @@ aiChatSessionService.touchSession(finalMemoryId, loginUser, "éè´å¤æä»¶åæ: " + finalMessage); if (containsImageFile(files)) { return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files) return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files) .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)) .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); } @@ -282,8 +306,121 @@ return false; } private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) { private List<String> resolveFileAccessPaths(List<StorageBlobVO> uploadedFiles) { if (StringUtils.isEmpty(uploadedFiles)) { return Collections.emptyList(); } List<String> filePaths = new ArrayList<>(); for (StorageBlobVO uploadedFile : uploadedFiles) { if (uploadedFile == null) { continue; } String selectedPath; if (shouldUsePreviewPath(uploadedFile)) { selectedPath = StringUtils.hasText(uploadedFile.getPreviewURL()) ? uploadedFile.getPreviewURL() : uploadedFile.getDownloadURL(); } else { selectedPath = StringUtils.hasText(uploadedFile.getDownloadURL()) ? uploadedFile.getDownloadURL() : uploadedFile.getPreviewURL(); } if (StringUtils.hasText(selectedPath)) { filePaths.add(selectedPath); } } return filePaths; } private boolean shouldUsePreviewPath(StorageBlobVO uploadedFile) { String contentType = uploadedFile.getContentType(); if (StringUtils.hasText(contentType)) { String normalized = contentType.toLowerCase(Locale.ROOT); if (normalized.startsWith("image/") || "application/pdf".equals(normalized)) { return true; } } String filename = uploadedFile.getOriginalFilename(); if (!StringUtils.hasText(filename) || !filename.contains(".")) { return false; } String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT); return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp", "pdf"); } private List<MultipartFile> copyFilesForUpload(MultipartFile[] files) throws IOException { List<MultipartFile> copies = new ArrayList<>(); for (MultipartFile file : files) { copies.add(new InMemoryMultipartFile( file.getName(), file.getOriginalFilename(), file.getContentType(), file.getBytes() )); } return copies; } private static final class InMemoryMultipartFile implements MultipartFile { private final String name; private final String originalFilename; private final String contentType; private final byte[] bytes; private InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) { this.name = name; this.originalFilename = originalFilename; this.contentType = contentType; this.bytes = bytes == null ? new byte[0] : bytes; } @Override public String getName() { return name; } @Override public String getOriginalFilename() { return originalFilename; } @Override public String getContentType() { return contentType; } @Override public boolean isEmpty() { return bytes.length == 0; } @Override public long getSize() { return bytes.length; } @Override public byte[] getBytes() { return bytes.clone(); } @Override public InputStream getInputStream() { return new ByteArrayInputStream(bytes); } @Override public void transferTo(File dest) throws IOException, IllegalStateException { Files.write(dest.toPath(), bytes); } } private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userMessage, String userPrompt, MultipartFile[] files) { return Flux.create(sink -> { StringBuilder assistantReply = new StringBuilder(); try { List<Content> contents = new ArrayList<>(); contents.add(TextContent.from(userPrompt)); @@ -302,14 +439,21 @@ SystemMessage.from("ä½ æ¯éè´ä¸å¡æä»¶åæå©æãè¯·ä»ææ¬åå¾çä¸è¯å«éè´å°è´¦ãéè´äº§åæç»ã仿¬¾æéè´§ä¿¡æ¯ï¼åªè¾åºåæ³ JSONã"), UserMessage.from(contents) ); safeAppendMessages(memoryId, List.of(UserMessage.from("éè´å¤æä»¶åæ: " + userMessage))); purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() { @Override public void onPartialResponse(String partialResponse) { sink.next(partialResponse); if (partialResponse != null) { assistantReply.append(partialResponse); sink.next(partialResponse); } } @Override public void onCompleteResponse(ChatResponse completeResponse) { if (StringUtils.hasText(assistantReply.toString())) { safeAppendMessages(memoryId, List.of(AiMessage.from(assistantReply.toString()))); } sink.complete(); } @@ -325,6 +469,16 @@ }); } private void safeAppendMessages(String memoryId, List<ChatMessage> messages) { if (!StringUtils.hasText(memoryId) || StringUtils.isEmpty(messages)) { return; } try { mongoChatMemoryStore.appendMessages(memoryId, messages); } catch (Exception ignored) { } } private String resolveImageMimeType(MultipartFile file) { String contentType = file.getContentType(); if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) { src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
@@ -1,15 +1,28 @@ package com.ruoyi.ai.dto; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class AiChatMessageDto { private String role; private String content; private List<String> filePaths; public AiChatMessageDto(String role, String content) { this.role = role; this.content = content; } public AiChatMessageDto(String role, String content, List<String> filePaths) { this.role = role; this.content = content; this.filePaths = filePaths; } } src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -9,6 +9,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import java.util.Date; import java.util.List; @Data @AllArgsConstructor @@ -24,6 +25,21 @@ private String content; /** * 夿件åæç¨æ·æé®ä¿¡æ¯ï¼ä¸æä»¶è·¯å¾åå¼åå¨ï¼ */ private List<String> analyzeUserQuestions; /** * 夿件åæä¸ä¼ æä»¶è·¯å¾ï¼å¾çå pdf 使ç¨é¢è§å°åï¼å ¶ä»ä½¿ç¨ä¸è½½å°åï¼ */ private List<String> analyzeFilePaths; /** * 夿件忿¯æ¬¡æé®å¯¹åºçæä»¶è·¯å¾åç» */ private List<List<String>> analyzeFilePathGroups; private Date createTime; private Date updateTime; src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
@@ -114,7 +114,10 @@ return new LinkedList<>(); } List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId); return messages.stream().map(this::convertMessage).collect(Collectors.toList()); List<AiChatMessageDto> messageDtos = messages.stream().map(this::convertMessage).collect(Collectors.toList()); List<List<String>> analyzeFilePathGroups = mongoChatMemoryStore.getAnalyzeFilePathGroups(memoryId); attachAnalyzeFilePaths(messageDtos, analyzeFilePathGroups); return messageDtos; } @Override @@ -188,4 +191,22 @@ } return new AiChatMessageDto("unknown", String.valueOf(message)); } private void attachAnalyzeFilePaths(List<AiChatMessageDto> messages, List<List<String>> analyzeFilePathGroups) { if (StringUtils.isEmpty(messages) || StringUtils.isEmpty(analyzeFilePathGroups)) { return; } int analyzeIndex = 0; for (AiChatMessageDto message : messages) { if (!"user".equals(message.getRole()) || analyzeIndex >= analyzeFilePathGroups.size()) { continue; } List<String> filePaths = analyzeFilePathGroups.get(analyzeIndex); if (!StringUtils.isEmpty(filePaths)) { message.setFilePaths(filePaths); } analyzeIndex++; } } } src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -11,6 +11,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.util.Date; import java.util.LinkedList; @@ -24,8 +26,7 @@ @Override public List<ChatMessage> getMessages(Object memoryId) { Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId))); ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class); ChatMessages chatMessages = findChatMessages(memoryId); if (chatMessages == null || chatMessages.getContent() == null) { return new LinkedList<>(); } @@ -56,7 +57,75 @@ updateMessages(memoryId, messages); } public void appendAnalyzeFileContext(Object memoryId, String userQuestion, List<String> filePaths) { String memoryIdValue = memoryIdString(memoryId); if (!StringUtils.hasText(memoryIdValue)) { return; } List<String> validFilePaths = new LinkedList<>(); if (!CollectionUtils.isEmpty(filePaths)) { for (String filePath : filePaths) { if (StringUtils.hasText(filePath)) { validFilePaths.add(filePath); } } } if (!StringUtils.hasText(userQuestion) && validFilePaths.isEmpty()) { return; } Query query = Query.query(Criteria.where("memoryId").is(memoryIdValue)); Update update = new Update(); update.set("memoryId", memoryIdValue); update.set("updateTime", new Date()); update.setOnInsert("createTime", new Date()); if (StringUtils.hasText(userQuestion)) { update.push("analyzeUserQuestions", userQuestion); } if (!validFilePaths.isEmpty()) { update.push("analyzeFilePaths").each(validFilePaths.toArray()); update.push("analyzeFilePathGroups", validFilePaths); } mongoTemplate.upsert(query, update, ChatMessages.class); } public List<String> getAnalyzeUserQuestions(Object memoryId) { ChatMessages chatMessages = findChatMessages(memoryId); if (chatMessages == null || CollectionUtils.isEmpty(chatMessages.getAnalyzeUserQuestions())) { return new LinkedList<>(); } return new LinkedList<>(chatMessages.getAnalyzeUserQuestions()); } public List<List<String>> getAnalyzeFilePathGroups(Object memoryId) { ChatMessages chatMessages = findChatMessages(memoryId); if (chatMessages == null) { return new LinkedList<>(); } if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePathGroups())) { if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePaths())) { return new LinkedList<>(); } List<List<String>> fallback = new LinkedList<>(); fallback.add(new LinkedList<>(chatMessages.getAnalyzeFilePaths())); return fallback; } List<List<String>> groups = new LinkedList<>(); for (List<String> group : chatMessages.getAnalyzeFilePathGroups()) { if (CollectionUtils.isEmpty(group)) { groups.add(new LinkedList<>()); } else { groups.add(new LinkedList<>(group)); } } return groups; } private String memoryIdString(Object memoryId) { return memoryId == null ? "" : memoryId.toString(); } private ChatMessages findChatMessages(Object memoryId) { Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId))); return mongoTemplate.findOne(query, ChatMessages.class); } } src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -23,8 +23,9 @@ SALE_SHIP_STOCK_OUT("13", "éå®-åè´§åºåº"), RETURN_HE_IN("14", "éå®éè´§-åæ ¼å ¥åº"), RETURN_UNSTOCK_IN("15", "éå®éè´§-ä¸åæ ¼å ¥åº"), PICK_RETURN_IN("20", "éå®éè´§-åæ ¼å ¥åº"), PURCHASE_RETURN_STOCK_OUT("21", "éè´éè´§"); PICK_RETURN_IN("20", "颿éæ-åæ ¼å ¥åº"), PURCHASE_RETURN_STOCK_OUT("21", "éè´éè´§"), FEED_RETURN_IN("22", "ç产éæ-åæ ¼å ¥åº"); src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java
@@ -9,7 +9,9 @@ PRODUCTION_REPORT_STOCK_OUT("3", "ç产æ¥å·¥-åºåº"), SALE_STOCK_OUT("8", "éå®-åºåº"), PURCHASE_RETURN_STOCK_OUT("9", "éè´éè´§"), SALE_SHIP_STOCK_OUT("13", "éå®-åè´§åºåº"); SALE_SHIP_STOCK_OUT("13", "éå®-åè´§åºåº"), PICK_STOCK_OUT("14", "çäº§é¢æåºåº"), FEED_STOCK_OUT("15", "ç产补æåºåº"); private final String code; private final String value; src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -3,7 +3,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.enums.ReviewStatusEnum; import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum; import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.production.bean.dto.ProductionOrderPickDto; @@ -18,8 +20,12 @@ import com.ruoyi.production.service.ProductionOrderPickService; import com.ruoyi.stock.dto.StockInventoryDto; import com.ruoyi.stock.mapper.StockInventoryMapper; import com.ruoyi.stock.pojo.StockInRecord; import com.ruoyi.stock.pojo.StockInventory; import com.ruoyi.stock.pojo.StockOutRecord; import com.ruoyi.stock.service.StockInventoryService; import com.ruoyi.stock.service.StockInRecordService; import com.ruoyi.stock.service.StockOutRecordService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,12 +45,18 @@ private static final byte PICK_TYPE_NORMAL = 1; private static final byte PICK_TYPE_FEEDING = 2; private static final String PICK_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.PICK_STOCK_OUT.getCode(); private static final String FEED_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.FEED_STOCK_OUT.getCode(); private static final String PICK_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode(); private static final String FEED_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode(); private final ProductionOrderMapper productionOrderMapper; private final ProductionOperationTaskMapper productionOperationTaskMapper; private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper; private final StockInventoryMapper stockInventoryMapper; private final StockInventoryService stockInventoryService; private final StockInRecordService stockInRecordService; private final StockOutRecordService stockOutRecordService; @Override @Transactional(rollbackFor = Exception.class) @@ -52,8 +64,9 @@ // 颿æ°å¢æ»æµç¨ï¼ // 1) è§£æåç«¯è¡æ°æ®å¹¶éè¡åå¹¶åæ°ï¼ // 2) æ ¡éªåæ°ä¸æ¹æ¬¡ï¼ // 3) å æ£ååºåï¼åè½åºé¢æä¸»è®°å½ï¼ // 4) åå ¥é¢ææµæ°´ï¼è®°å½æ°éåå轨迹ã // 3) å ä¿åé¢æä¸»è®°å½ï¼ // 4) åèµ°âåºåºç³è¯· + 审æ¹éè¿â宿åºåæ£åï¼ // 5) åå ¥é¢ææµæ°´ï¼è®°å½æ°éåå轨迹ã List<ProductionOrderPickDto> pickItems = resolvePickItems(dto); // éè¡å¤çé¢ææ°æ®ï¼è¡å·ç¨äºæ¼è£ ç²¾ç¡®çæ¥éä¿¡æ¯ã for (int i = 0; i < pickItems.size(); i++) { @@ -66,7 +79,6 @@ List<String> batchNoList = resolveBatchNoList(resolvedDto); String inventoryBatchNo = pickInventoryBatchNo(batchNoList); String storedBatchNo = formatBatchNoStorage(batchNoList); subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo); // ä¿åé¢æä¸»è®°å½å¿«ç §ã ProductionOrderPick orderPick = new ProductionOrderPick(); @@ -82,6 +94,9 @@ orderPick.setReturned(false); // æ°å¢ä¸»è®°å½ã baseMapper.insert(orderPick); // å æ°å¢åºåºç³è¯·ï¼å审æ¹éè¿ï¼å®æåºåæ£åã subtractInventory(orderPick.getId(), resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE); // è®°å½æ¬æ¬¡é¢ææµæ°´ï¼before=0ï¼after=æ¬æ¬¡é¢æéï¼ã insertPickRecord(orderPick.getId(), @@ -201,7 +216,12 @@ } String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo()); BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity()); addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity); addInventory(existingPick.getId(), existingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); // å é¤å ³èé¢ææµæ°´ï¼é¿å éçæ ä¸»è®°å½ã productionOrderPickRecordMapper.delete( Wrappers.<ProductionOrderPickRecord>lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, existingPick.getId()) ); int affected = baseMapper.deleteById(deleteId); if (affected <= 0) { throw new ServiceException("å é¤é¢æè®°å½å¤±è´¥ï¼ID=" + deleteId); @@ -240,7 +260,12 @@ for (ProductionOrderPick missingPick : missingPickList) { String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo()); BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity()); addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity); addInventory(missingPick.getId(), missingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); // å é¤å ³èé¢ææµæ°´ï¼é¿å éçæ ä¸»è®°å½ã productionOrderPickRecordMapper.delete( Wrappers.<ProductionOrderPickRecord>lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, missingPick.getId()) ); int affected = baseMapper.deleteById(missingPick.getId()); if (affected <= 0) { throw new ServiceException("å 餿ªåä¼ é¢æè®°å½å¤±è´¥ï¼ID=" + missingPick.getId()); @@ -261,11 +286,11 @@ } private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) { // æ´æ°åºæ¯ä¸æ°å¢ä¸æ¡é¢æï¼æ£åºå -> æ°å¢ä¸»è®°å½ -> åæµæ°´ã // æ´æ°åºæ¯ä¸æ°å¢ä¸æ¡é¢æï¼ // æ°å¢ä¸»è®°å½ -> åºåºç³è¯·å¹¶å®¡æ¹ -> åæµæ°´ã List<String> batchNoList = resolveBatchNoList(dto); String inventoryBatchNo = pickInventoryBatchNo(batchNoList); String storedBatchNo = formatBatchNoStorage(batchNoList); subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo); ProductionOrderPick orderPick = new ProductionOrderPick(); orderPick.setProductionOrderId(dto.getProductionOrderId()); @@ -279,6 +304,9 @@ orderPick.setBom(dto.getBom()); orderPick.setReturned(false); baseMapper.insert(orderPick); // å æ°å¢åºåºç³è¯·ï¼å审æ¹éè¿ï¼å®æåºåæ£åã subtractInventory(orderPick.getId(), dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE); insertPickRecord(orderPick.getId(), dto.getProductionOrderId(), @@ -337,7 +365,7 @@ : formatBatchNoStorage(batchNoList); BigDecimal feedingQuantity = dto.getFeedingQuantity(); subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo); subtractInventory(oldPick.getId(), productModelId, inventoryBatchNo, feedingQuantity, rowNo, FEED_STOCK_OUT_RECORD_TYPE); // 计ç®è¡¥æååæ°éå¹¶åè¡¥ææµæ°´ã BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId()); @@ -393,19 +421,36 @@ } private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) { // éææ´æ°åªæ¹ä¸»é¢æè®°å½ä¸çéæå段ä¸å®é éã // éææ´æ°ï¼ // 1) returnQty æâæ¬æ¬¡éæéâå¤çï¼ // 2) æ¬æ¬¡éæéåè¡¥å°âç产éæå ¥åºâï¼ // 3) ç´¯å 主记å½éææ»éå¹¶éç®å®é éã BigDecimal oldReturnQty = defaultDecimal(oldPick.getReturnQty()); BigDecimal currentReturnQty = defaultDecimal(dto.getReturnQty()); BigDecimal totalReturnQty = oldReturnQty.add(currentReturnQty); if (currentReturnQty.compareTo(BigDecimal.ZERO) > 0) { String returnBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo()); addInventory(oldPick.getId(), oldPick.getProductModelId(), returnBatchNo, currentReturnQty, FEED_RETURN_IN_RECORD_TYPE); } BigDecimal actualQty = defaultDecimal(oldPick.getQuantity()) .add(defaultDecimal(oldPick.getFeedingQty())) .subtract(totalReturnQty); if (actualQty.compareTo(BigDecimal.ZERO) < 0) { throw new ServiceException("第" + rowNo + "è¡éæå¤±è´¥ï¼ç´¯è®¡éææ°éä¸è½å¤§äºå¯ç¨æ°é"); } ProductionOrderPick updatePick = new ProductionOrderPick(); updatePick.setId(oldPick.getId()); updatePick.setReturnQty(dto.getReturnQty()); updatePick.setActualQty(dto.getActualQty()); updatePick.setReturned(true); updatePick.setReturnQty(totalReturnQty); updatePick.setActualQty(actualQty); updatePick.setReturned(totalReturnQty.compareTo(BigDecimal.ZERO) > 0); int affected = baseMapper.updateById(updatePick); if (affected <= 0) { throw new ServiceException("第" + rowNo + "è¡éæå¤±è´¥ï¼æ´æ°é¢æä¸»è®°å½å¤±è´¥"); } oldPick.setReturnQty(updatePick.getReturnQty()); oldPick.setActualQty(updatePick.getActualQty()); oldPick.setReturned(true); oldPick.setReturned(updatePick.getReturned()); } private void updateExistingPick(ProductionOrderPickDto dto, @@ -431,21 +476,30 @@ String newStoredBatchNo = formatBatchNoStorage(newBatchNoList); BigDecimal newQuantity = dto.getPickQuantity(); // å¤æè§æ ¼+æ¹æ¬¡æ¯å¦ååï¼å³å®åºåå¤ççç¥ã // å¤æè§æ ¼+æ¹æ¬¡ææ°éæ¯å¦ååï¼å¹¶æåºæ¯å¤çåºåï¼ // 1) åè§æ ¼åæ¹æ¬¡ï¼æå·®å¼å¤çï¼å¢éæ£å / åéåéï¼ï¼ // 2) è§æ ¼ææ¹æ¬¡ååï¼å鿧颿ååéææ°é¢æã boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId) && Objects.equals(oldBatchNo, newBatchNo); boolean quantityChanged = oldQuantity.compareTo(newQuantity) != 0; boolean needReissuePickRecord = !sameStockKey || quantityChanged; if (sameStockKey) { // è§æ ¼ä¸æ¹æ¬¡ä¸åï¼åªæå·®å¼å¢ååºåã BigDecimal delta = newQuantity.subtract(oldQuantity); if (delta.compareTo(BigDecimal.ZERO) > 0) { subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo); } else if (delta.compareTo(BigDecimal.ZERO) < 0) { addInventory(oldProductModelId, oldBatchNo, delta.abs()); BigDecimal deltaQuantity = newQuantity.subtract(oldQuantity); if (deltaQuantity.compareTo(BigDecimal.ZERO) > 0) { // æ°éå¢å ï¼åªæ£åæ°å¢é¨åã subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, deltaQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE); } else if (deltaQuantity.compareTo(BigDecimal.ZERO) < 0) { // æ°éåå°ï¼åªåéå·®å¼é¨åã addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, deltaQuantity.abs(), PICK_RETURN_IN_RECORD_TYPE); } } else { // è§æ ¼ææ¹æ¬¡ååï¼å åè¡¥æ§åºåï¼åæ£åæ°åºåã addInventory(oldProductModelId, oldBatchNo, oldQuantity); subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo); // è§æ ¼ææ¹æ¬¡ååï¼å å ¨éå鿧颿ï¼åå ¨éæ£åæ°é¢æã addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, newQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE); } if (needReissuePickRecord) { // æ£å¸¸é¢ææµæ°´æâææ°é¢æéâé建ï¼é¿å ä¿çå岿§å¼ã deleteNormalPickRecord(oldPick.getId()); } oldPick.setProductModelId(newProductModelId); @@ -454,6 +508,9 @@ oldPick.setRemark(dto.getRemark()); oldPick.setOperationName(dto.getOperationName()); oldPick.setTechnologyOperationId(dto.getTechnologyOperationId()); // æ®éæ´æ°ä¹è¦åæ¥éç®å®é ç¨éï¼é¿å æ²¿ç¨æ§å¼ã // è§åï¼å®é ç¨é = 颿æ°é + è¡¥ææ°é - éææ°éã oldPick.setActualQty(calculateActualQty(oldPick, oldPick.getFeedingQty())); if (dto.getDemandedQuantity() != null) { oldPick.setDemandedQuantity(dto.getDemandedQuantity()); } @@ -465,16 +522,15 @@ throw new ServiceException("第" + rowNo + "è¡æ´æ°å¤±è´¥ï¼æ´æ°é¢æè®°å½å¤±è´¥"); } // åå ¥æ´æ°æµæ°´ï¼ä¿çæ¬æ¬¡æ°éåå轨迹ã BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity; if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) { // 妿åç颿éæï¼è¡¥å䏿¡æ°çæ£å¸¸é¢ææµæ°´ã if (needReissuePickRecord) { insertPickRecord(oldPick.getId(), dto.getProductionOrderId(), dto.getProductionOperationTaskId(), newProductModelId, newBatchNo, recordQuantity, oldQuantity, newQuantity, BigDecimal.ZERO, newQuantity, dto.getPickType(), dto.getRemark(), @@ -509,11 +565,28 @@ productionOrderPickRecordMapper.insert(pickRecord); } private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) { private void deleteNormalPickRecord(Long pickId) { // å é¤è¯¥é¢æååå²ä¸çâæ£å¸¸é¢æâæµæ°´ï¼ä¿çè¡¥æ/éææµæ°´ã if (pickId == null) { return; } productionOrderPickRecordMapper.delete( Wrappers.<ProductionOrderPickRecord>lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, pickId) .eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_NORMAL) ); } private void subtractInventory(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, int rowNo, String stockOutRecordType) { // æ£ååºåæ»æµç¨ï¼ // 1) è§£ææ¹æ¬¡åè¡¨ï¼ // 2) è®¡ç®æ¯ä¸ªæ¹æ¬¡å¯ç¨é䏿»å¯ç¨éï¼ // 3) ææ¹æ¬¡é¡ºåºéç¬æ£åï¼ç´å°æ£å®ç®æ æ°éï¼ // 3) ææ¹æ¬¡é¡ºåºéç¬âæ°å¢åºåºè®°å½å¹¶å®¡æ¹éè¿âï¼ç´å°æ£å®ç®æ æ°éï¼ // 4) 任䏿¥å¤±è´¥å³æéå¹¶åæ»äºå¡ã BigDecimal deductQuantity = defaultDecimal(quantity); // 颿æ°éå°äºçäº0æ¶ï¼ä¸éè¦æ§è¡åºåæ£åã @@ -562,14 +635,7 @@ continue; } BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity); StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(entry.getKey()); stockInventoryDto.setQualitity(currentDeductQuantity); int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto); if (affected <= 0) { throw new ServiceException("第" + rowNo + "è¡æ£ååºå失败ï¼åºåæ´æ°å¤±è´¥"); } createAndApproveStockOutRecord(recordId, productModelId, entry.getKey(), currentDeductQuantity, rowNo, stockOutRecordType); remainingQuantity = remainingQuantity.subtract(currentDeductQuantity); } @@ -578,19 +644,97 @@ } } private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) { // åè¡¥åºåï¼ç¨äºå é¤é¢æãæ¹å°é¢æãåæ¢æ¹æ¬¡çåºæ¯ï¼ã private void createAndApproveStockOutRecord(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, int rowNo, String stockOutRecordType) { // åºåæ£åæ¹ä¸ºä¸¤æ¥ï¼ // 1) å è°ç¨ addStockOutRecordOnly æ°å¢å¾ 审æ¹åºåºè®°å½ï¼ // 2) åè°ç¨åºåºå®¡æ¹ï¼å®¡æ¹ç¶æåºå®ä¼ 1ï¼éè¿ï¼ã try { StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setRecordId(recordId == null ? 0L : recordId); stockInventoryDto.setRecordType(stockOutRecordType); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(quantity); stockInventoryService.addStockOutRecordOnly(stockInventoryDto); LambdaQueryWrapper<StockOutRecord> recordWrapper = Wrappers.<StockOutRecord>lambdaQuery() .eq(StockOutRecord::getRecordId, stockInventoryDto.getRecordId()) .eq(StockOutRecord::getRecordType, stockOutRecordType) .eq(StockOutRecord::getProductModelId, productModelId) .eq(StockOutRecord::getType, "0") .orderByDesc(StockOutRecord::getId) .last("limit 1"); if (StringUtils.isEmpty(batchNo)) { recordWrapper.isNull(StockOutRecord::getBatchNo); } else { recordWrapper.eq(StockOutRecord::getBatchNo, batchNo); } StockOutRecord stockOutRecord = stockOutRecordService.getOne(recordWrapper, false); if (stockOutRecord == null || stockOutRecord.getId() == null) { throw new ServiceException("第" + rowNo + "è¡æ£ååºåå¤±è´¥ï¼æªæ¾å°å¯¹åºåºåºç³è¯·è®°å½"); } stockOutRecordService.batchApprove( Collections.singletonList(stockOutRecord.getId()), ReviewStatusEnum.APPROVED.getCode() ); } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException("第" + rowNo + "è¡æ£ååºå失败ï¼" + ex.getMessage()); } } private void addInventory(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, String stockInRecordType) { // åè¡¥åºåæ¹ä¸ºä¸¤æ¥ï¼ // 1) å æ°å¢å ¥åºç³è¯·ï¼ // 2) å审æ¹éè¿ï¼ç¡®ä¿åºåç«å»åè¡¥çæã BigDecimal addQuantity = defaultDecimal(quantity); if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) { return; } StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(addQuantity); stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode())); stockInventoryDto.setRecordId(0L); stockInventoryService.addStockInRecordOnly(stockInventoryDto); try { StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(addQuantity); stockInventoryDto.setRecordType(stockInRecordType); stockInventoryDto.setRecordId(recordId == null ? 0L : recordId); stockInventoryService.addStockInRecordOnly(stockInventoryDto); LambdaQueryWrapper<StockInRecord> recordWrapper = Wrappers.<StockInRecord>lambdaQuery() .eq(StockInRecord::getRecordId, stockInventoryDto.getRecordId()) .eq(StockInRecord::getRecordType, stockInventoryDto.getRecordType()) .eq(StockInRecord::getProductModelId, productModelId) .eq(StockInRecord::getType, "0") .orderByDesc(StockInRecord::getId) .last("limit 1"); if (StringUtils.isEmpty(batchNo)) { recordWrapper.isNull(StockInRecord::getBatchNo); } else { recordWrapper.eq(StockInRecord::getBatchNo, batchNo); } StockInRecord stockInRecord = stockInRecordService.getOne(recordWrapper, false); if (stockInRecord == null || stockInRecord.getId() == null) { throw new ServiceException("åè¡¥åºåå¤±è´¥ï¼æªæ¾å°å¯¹åºå ¥åºç³è¯·è®°å½"); } stockInRecordService.batchApprove( Collections.singletonList(stockInRecord.getId()), ReviewStatusEnum.APPROVED.getCode() ); } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException("åè¡¥åºå失败ï¼" + ex.getMessage()); } } private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) { @@ -769,7 +913,7 @@ } private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) { // æ ¡éªéæåæ°ï¼è®¢åã颿IDãéæéãå®é éï¼ã // æ ¡éªéæåæ°ï¼è®¢åã颿IDãéæéï¼ã if (dto.getProductionOrderId() == null) { throw new ServiceException("第" + rowNo + "è¡åæ°é误ï¼ç产订åIDä¸è½ä¸ºç©º"); } @@ -778,9 +922,6 @@ } 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"); } } src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -101,12 +101,13 @@ throw new BaseException("åå¹¶å¤±è´¥ï¼æéç产计åç产ååå·ä¸ä¸è´"); } // å·²ä¸åæé¨åä¸åç计åä¸å è®¸åæ¬¡åå¹¶ boolean hasIssuedPlan = planLists.stream() // ä» âå·²ä¸åâ计åä¸å è®¸åæ¬¡åä¸åå¹¶ä¸åï¼ // âå¾ ä¸å/é¨åä¸åâå 许继ç»ä¸åå©ä½æ°éã boolean hasFullyIssuedPlan = planLists.stream() .anyMatch(item -> item.getStatus() != null && (item.getStatus() == PLAN_STATUS_PARTIAL || item.getStatus() == PLAN_STATUS_ISSUED)); if (hasIssuedPlan) { throw new BaseException("åå¹¶å¤±è´¥ï¼æéç产计ååå¨å·²ä¸åæé¨åä¸åçæ°æ®"); && item.getStatus() == PLAN_STATUS_ISSUED); if (hasFullyIssuedPlan) { throw new BaseException("åå¹¶å¤±è´¥ï¼æéç产计ååå¨å·²ä¸åçæ°æ®"); } // è®¡ç®æ¬æ¬¡å¯ä¸åçå©ä½éæ±æ»é src/main/java/com/ruoyi/staff/dto/StaffLeaveDto.java
@@ -99,4 +99,9 @@ * 离èåå ææ¬ */ private String reasonText; /** * æ°æ */ private String nation; } src/main/resources/application-dev-pro.yml
@@ -71,9 +71,9 @@ druid: # ä¸»åºæ°æ®æº master: url: jdbc:mysql://localhost:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://1.15.17.182:9999/product-inventory-management-wtxxjc?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 password: xd@123456.. # ä»åºæ°æ®æº slave: # 仿°æ®æºå¼å ³/é»è®¤å ³é @@ -155,7 +155,7 @@ database: 0 # å¯ç # password: root2022! password: password: 123456 # è¿æ¥è¶ æ¶æ¶é´ timeout: 10s src/main/resources/application-dev.yml
@@ -254,8 +254,18 @@ # æ¯å¦å è®¸çææä»¶è¦çå°æ¬å°ï¼èªå®ä¹è·¯å¾ï¼ï¼é»è®¤ä¸å 许 allowOverwrite: false # æä»¶ä¸ä¼ é ç½® file: temp-dir: D:/ruoyi/temp/uploads # 临æ¶ç®å½ upload-dir: D:/ruoyi/prod/uploads # æ£å¼ç®å½ temp-dir: D:/ruoyi/temp/uploads # 临æ¶ç®å½ åæå é¤ upload-dir: D:/ruoyi/prod/uploads # æ£å¼ç®å½ åæå é¤ path: D:/ruoyi/prod/uploads # ä¸ä¼ ç®å½ urlPrefix: /common # 龿¥åç¼ domain: http://127.0.0.1:7005 # åååç¼ expired: 120 # è¿ææ¶é´(åä½:åé) useLimit: 10 # ä½¿ç¨æ¬¡æ° compress: true # æ¯å¦å缩 needCompressSize: 10MB # å缩éå¼ compressQuality: 0.5 # å缩质é(0.0-1.0) knowledge: one: D:\æ°ç大ç½ç´ ä¼ä¸äº§åä½ç³»è¯´æææ¡£.md src/main/resources/application-wtxxjc.yml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,266 @@ # 项ç®ç¸å ³é ç½® 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: YZFX # ä¸ªæ¨ 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: 9021 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://1.15.17.182:9999/product-inventory-management-wtxxjc?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: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ # ä»¤çæææï¼é»è®¤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: /javaWork/product-inventory-management/file/temp/uploads # 临æ¶ç®å½ upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ£å¼ç®å½ path: /javaWork/product-inventory-management/file # ä¸ä¼ ç®å½ urlPrefix: /prod-api/common # 龿¥åç¼ domain: http://1.15.17.182:9049 # åååç¼ expired: 120 # è¿ææ¶é´(åä½:åé) useLimit: 10 # ä½¿ç¨æ¬¡æ° compress: true # æ¯å¦å缩 needCompressSize: 10MB # å缩éå¼ compressQuality: 0.5 # å缩质é(0.0-1.0) src/main/resources/application.yml
@@ -3,7 +3,7 @@ main: allow-circular-references: true profiles: active: dev active: dev-pro langchain4j: mcp: # MCP æå¡ç«¯å°åï¼æ ¹æ®å®é é¨ç½²ç MCP æå¡è°æ´ï¼ src/main/resources/mapper/staff/StaffLeaveMapper.xml
@@ -9,6 +9,7 @@ soj.staff_no as staffNo, soj.sex as sex, soj.native_place as nativePlace, soj.nation as nation, soj.adress as adress, soj.first_study as firstStudy, soj.profession as profession, @@ -30,6 +31,9 @@ <if test="c.staffName != null and c.staffName != '' "> AND soj.staff_name LIKE CONCAT('%',#{c.staffName},'%') </if> <if test="c.nation != null and c.nation != '' "> AND soj.nation = #{c.nation} </if> </select> <select id="staffLeaveList" resultType="com.ruoyi.staff.dto.StaffLeaveDto"> SELECT src/main/resources/mapper/staff/StaffOnJobMapper.xml
@@ -17,6 +17,9 @@ <if test="staffOnJob.staffState != null"> AND staff_state = #{staffOnJob.staffState} </if> <if test="staffOnJob.nation != null"> AND nation = #{staffOnJob.nation} </if> <if test="staffOnJob.staffName != null and staffOnJob.staffName != '' "> AND staff_name LIKE CONCAT('%',#{staffOnJob.staffName},'%') </if>