From 78be20a0887fd6eddcd703fcc0dc863b700b0613 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 28 五月 2026 16:30:40 +0800
Subject: [PATCH] fix: 去除AI与个推
---
/dev/null | 43 ---------------------
src/main/resources/application-jhy.yml | 5 +-
src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java | 4 +-
pom.xml | 47 -----------------------
4 files changed, 5 insertions(+), 94 deletions(-)
diff --git a/pom.xml b/pom.xml
index d2a7ca5..2dcdd01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,25 +52,11 @@
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<jsqlparser.version>4.9</jsqlparser.version>
<thumbnailator.version>0.4.20</thumbnailator.version>
- <langchain4j.version>1.0.0-beta3</langchain4j.version>
+
</properties>
<dependencyManagement>
<dependencies>
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-bom</artifactId>
- <version>${langchain4j.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-community-bom</artifactId>
- <version>${langchain4j.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
</dependencies>
</dependencyManagement>
@@ -139,38 +125,7 @@
<artifactId>commons-pool2</artifactId>
</dependency>
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-spring-boot-starter</artifactId>
- </dependency>
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-pinecone</artifactId>
- </dependency>
- <dependency>
- <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>
- <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
- </dependency>
-
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
- </dependency>
-
- <dependency>
- <groupId>dev.langchain4j</groupId>
- <artifactId>langchain4j-mcp</artifactId>
- </dependency>
<!-- Mysql椹卞姩鍖� -->
<dependency>
diff --git a/src/main/java/com/ruoyi/ai/assistant/FinancialAgent.java b/src/main/java/com/ruoyi/ai/assistant/FinancialAgent.java
deleted file mode 100644
index 875b98d..0000000
--- a/src/main/java/com/ruoyi/ai/assistant/FinancialAgent.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.ruoyi.ai.assistant;
-
-import dev.langchain4j.service.MemoryId;
-import dev.langchain4j.service.SystemMessage;
-import dev.langchain4j.service.UserMessage;
-import dev.langchain4j.service.V;
-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 = "chatMemoryProviderFinancial",
- tools = "financialAgentTools"
-)
-public interface FinancialAgent {
-
- @SystemMessage(fromResource = "financial-agent-prompt.txt")
- Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
-}
diff --git a/src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
deleted file mode 100644
index 9afadff..0000000
--- a/src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
+++ /dev/null
@@ -1,266 +0,0 @@
-package com.ruoyi.ai.assistant;
-
-import com.ruoyi.ai.tools.FinancialAgentTools;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-import java.time.LocalDate;
-import java.time.YearMonth;
-import java.time.format.DateTimeFormatter;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-@Component
-public class FinancialIntentExecutor {
-
- private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?\\s*(\\d{1,2})\\s*鏉�");
- private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
- private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(杩憒鏈�杩�)?\\s*(\\d{1,3})\\s*澶�");
-
- private final FinancialAgentTools financialAgentTools;
-
- public FinancialIntentExecutor(FinancialAgentTools financialAgentTools) {
- this.financialAgentTools = financialAgentTools;
- }
-
- public String tryExecute(String memoryId, String message) {
- if (!StringUtils.hasText(message)) {
- return null;
- }
- String text = message.trim();
-
- String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
- if (StringUtils.hasText(quickPromptResponse)) {
- return quickPromptResponse;
- }
-
- DateRange dateRange = extractDateRange(text);
- Integer limit = extractLimit(text);
- String keyword = extractKeyword(text);
- String startDate = dateRange.startDate();
- String endDate = dateRange.endDate();
- String timeRange = dateRange.label();
-
- if (containsAny(text, "鎴愭湰鏍哥畻", "浜у搧鎴愭湰", "宸ュ簭鎴愭湰", "浜哄伐鎴愭湰", "鎶樻棫", "鏉愭枡鎹熻��")) {
- return financialAgentTools.calculateIntelligentCost(memoryId, startDate, endDate, timeRange, keyword, limit);
- }
- if (containsAny(text, "鍒╂鼎鍒嗘瀽", "璁㈠崟鍒╂鼎", "浜忔崯璁㈠崟", "浣庡埄娑�",
- "鏈�璧氶挶瀹㈡埛", "鍝釜瀹㈡埛鏈�璧氶挶", "瀹㈡埛鏈�璧氶挶", "鍒╂鼎鏈�楂樺鎴�", "鍒╂鼎璐$尞鏈�楂樺鎴�", "鍒╂鼎涓嬮檷")) {
- return financialAgentTools.analyzeOrderProfit(memoryId, startDate, endDate, timeRange, keyword, limit);
- }
- if (containsAny(text, "搴撳瓨璧勯噾", "搴撳瓨绉帇", "鍛嗘粸搴撳瓨", "璧勯噾鍗犵敤", "鍛ㄨ浆鐜�", "搴撳瓨鍛ㄨ浆")) {
- return financialAgentTools.analyzeInventoryCapital(memoryId, startDate, endDate, timeRange, keyword, limit);
- }
- if (containsAny(text, "鐜伴噾娴�", "鍥炴椋庨櫓", "浠樻鍘嬪姏", "璧勯噾缂哄彛", "搴旀敹", "搴斾粯", "鍥炴棰勬祴")) {
- return financialAgentTools.forecastCashFlow(memoryId, startDate, endDate, timeRange, limit);
- }
- if (containsAny(text, "寮傚父棰勮", "缁忚惀寮傚父", "椋庨櫓棰勮", "鎴愭湰寮傚父", "鍒╂鼎寮傚父", "鍥炴寮傚父", "璁㈠崟椋庨櫓")) {
- return financialAgentTools.detectBusinessAnomalies(memoryId, startDate, endDate, timeRange, limit);
- }
- if (containsAny(text, "椹鹃┒鑸�", "缁忚惀鐪嬫澘", "缁忚惀鎬昏", "缁忚惀浠〃鐩�", "缁忚惀澶х洏")) {
- return financialAgentTools.getBusinessCockpit(memoryId, startDate, endDate, timeRange);
- }
- if (containsAny(text, "鏃ユ姤", "鍛ㄦ姤", "缁忚惀鎶ュ憡", "鍒嗘瀽鎶ュ憡")) {
- return financialAgentTools.generateOperationReport(memoryId, startDate, endDate, timeRange,
- containsAny(text, "鍛ㄦ姤") ? "weekly" : "daily");
- }
- if (containsAny(text, "涓氳储铻嶅悎", "涓氳储鑱斿姩", "鍙e緞", "鎸囨爣瑙i噴", "涓轰粈涔�")) {
- return financialAgentTools.retrieveFinancialKnowledge(memoryId, text);
- }
- return null;
- }
-
- private String tryExecuteQuickPrompt(String memoryId, String text) {
- String normalized = normalizeForMatch(text);
- if ("鏌ョ湅鏈湀缁忚惀椹鹃┒鑸�".equals(normalized) || "鏌ョ湅缁忚惀椹鹃┒鑸�".equals(normalized)) {
- DateRange range = monthRange();
- return financialAgentTools.getBusinessCockpit(memoryId, range.startDate(), range.endDate(), range.label());
- }
- if ("鏌ヨ杩�30澶╀簭鎹熻鍗�".equals(normalized) || "鍝釜璁㈠崟浜忔崯".equals(normalized)) {
- DateRange range = recentDaysRange(30);
- return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
- }
- if ("鐢熸垚鏈懆缁忚惀鍛ㄦ姤".equals(normalized) || "鐢熸垚鍛ㄦ姤".equals(normalized)) {
- DateRange range = weekRange();
- return financialAgentTools.generateOperationReport(memoryId, range.startDate(), range.endDate(), range.label(), "weekly");
- }
- if ("涓轰粈涔堝埄娑︿笅闄�".equals(normalized)) {
- DateRange range = monthRange();
- return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
- }
- if ("鍝釜瀹㈡埛鏈�璧氶挶".equals(normalized)
- || "鏈�杩戝摢涓鎴锋渶璧氶挶".equals(normalized)
- || "鏈湀鍝釜瀹㈡埛鏈�璧氶挶".equals(normalized)
- || "杩�30澶╁摢涓鎴锋渶璧氶挶".equals(normalized)
- || "鍝釜瀹㈡埛鍒╂鼎鏈�楂�".equals(normalized)
- || "鍝釜瀹㈡埛鍒╂鼎璐$尞鏈�楂�".equals(normalized)) {
- DateRange range = extractDateRange(text);
- return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
- }
- return null;
- }
-
- private boolean containsAny(String text, String... keywords) {
- for (String keyword : keywords) {
- if (text.contains(keyword)) {
- return true;
- }
- }
- return false;
- }
-
- private Integer extractLimit(String text) {
- Matcher matcher = LIMIT_PATTERN.matcher(text);
- return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
- }
-
- private DateRange extractDateRange(String text) {
- Matcher matcher = DATE_PATTERN.matcher(text);
- if (matcher.find()) {
- String first = matcher.group(1);
- String second = matcher.find() ? matcher.group(1) : first;
- return buildDateRange(first, second, first + "鑷�" + second);
- }
- if (text.contains("鏈湀")) {
- return monthRange();
- }
- if (text.contains("涓婃湀")) {
- return lastMonthRange();
- }
- if (text.contains("鏈勾") || text.contains("浠婂勾")) {
- return yearRange();
- }
- if (text.contains("鏈懆")) {
- return weekRange();
- }
- Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text);
- if (relativeDayMatcher.find()) {
- int days = Integer.parseInt(relativeDayMatcher.group(2));
- return recentDaysRange(days);
- }
- return new DateRange(null, null, "杩�30澶�");
- }
-
- private DateRange buildDateRange(String start, String end, String label) {
- LocalDate startDate = parseDate(start);
- LocalDate endDate = parseDate(end);
- if (startDate == null || endDate == null) {
- return new DateRange(null, null, "杩�30澶�");
- }
- if (startDate.isAfter(endDate)) {
- LocalDate temp = startDate;
- startDate = endDate;
- endDate = temp;
- }
- return new DateRange(formatDate(startDate), formatDate(endDate), label);
- }
-
- private DateRange recentDaysRange(int days) {
- LocalDate end = LocalDate.now();
- int safeDays = Math.max(days, 1);
- LocalDate start = end.minusDays(safeDays - 1L);
- return new DateRange(formatDate(start), formatDate(end), "杩�" + safeDays + "澶�");
- }
-
- private DateRange monthRange() {
- LocalDate today = LocalDate.now();
- return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today), "鏈湀");
- }
-
- private DateRange weekRange() {
- LocalDate today = LocalDate.now();
- LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
- return new DateRange(formatDate(start), formatDate(today), "鏈懆");
- }
-
- private DateRange lastMonthRange() {
- YearMonth lastMonth = YearMonth.now().minusMonths(1);
- return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth()), "涓婃湀");
- }
-
- private DateRange yearRange() {
- LocalDate today = LocalDate.now();
- return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today), "浠婂勾");
- }
-
- private LocalDate parseDate(String text) {
- try {
- return LocalDate.parse(text, DATE_FMT);
- } catch (Exception ignored) {
- return null;
- }
- }
-
- private String formatDate(LocalDate date) {
- return date == null ? null : date.format(DATE_FMT);
- }
-
- private String normalizeForMatch(String text) {
- if (!StringUtils.hasText(text)) {
- return "";
- }
- return text.replace("锛�", "")
- .replace(",", "")
- .replace("銆�", "")
- .replace(".", "")
- .replace("锛�", "")
- .replace("!", "")
- .replace("锛�", "")
- .replace("?", "")
- .replace("锛�", "")
- .replace(":", "")
- .replace("锛�", "")
- .replace(";", "")
- .replace(" ", "")
- .trim();
- }
-
- private String extractKeyword(String text) {
- String cleaned = text
- .replace("鏌ヨ", "")
- .replace("鏌ョ湅", "")
- .replace("鐪嬩笅", "")
- .replace("鐪嬬湅", "")
- .replace("甯垜", "")
- .replace("璇�", "")
- .replace("涓�涓�", "")
- .replace("涓轰粈涔�", "")
- .replace("鍝釜瀹㈡埛鏈�璧氶挶", "")
- .replace("鏈�杩戝摢涓鎴锋渶璧氶挶", "")
- .replace("鏈湀鍝釜瀹㈡埛鏈�璧氶挶", "")
- .replace("杩�30澶╁摢涓鎴锋渶璧氶挶", "")
- .replace("鏈�璧氶挶瀹㈡埛", "")
- .replace("瀹㈡埛鏈�璧氶挶", "")
- .replace("鍝釜瀹㈡埛鍒╂鼎鏈�楂�", "")
- .replace("鍒╂鼎鏈�楂樺鎴�", "")
- .replace("鍝釜瀹㈡埛鍒╂鼎璐$尞鏈�楂�", "")
- .replace("鍒╂鼎璐$尞鏈�楂樺鎴�", "")
- .replace("鏈湀", "")
- .replace("鏈懆", "")
- .replace("鏈勾", "")
- .replace("浠婂勾", "")
- .replace("涓婃湀", "")
- .replace("杩�30澶�", "")
- .replace("杩�7澶�", "")
- .replace("杩�90澶�", "")
- .replace("鍓�10鏉�", "")
- .replace("鏈�杩�10鏉�", "")
- .replace("鍓�20鏉�", "")
- .replace("鏈�杩�20鏉�", "")
- .replace("璁㈠崟鍒╂鼎鍒嗘瀽", "")
- .replace("鍒╂鼎鍒嗘瀽", "")
- .replace("搴撳瓨璧勯噾鍒嗘瀽", "")
- .replace("鐜伴噾娴侀娴�", "")
- .replace("缁忚惀椹鹃┒鑸�", "")
- .replace("鏃ユ姤", "")
- .replace("鍛ㄦ姤", "")
- .replace("寮傚父棰勮", "")
- .replace("鏉�", "")
- .trim();
- return cleaned.length() >= 2 ? cleaned : null;
- }
-
- private record DateRange(String startDate, String endDate, String label) {
- }
-}
diff --git a/src/main/java/com/ruoyi/ai/config/FinancialAgentConfig.java b/src/main/java/com/ruoyi/ai/config/FinancialAgentConfig.java
deleted file mode 100644
index 0706e74..0000000
--- a/src/main/java/com/ruoyi/ai/config/FinancialAgentConfig.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.ruoyi.ai.config;
-
-import com.ruoyi.ai.store.MongoChatMemoryStore;
-import dev.langchain4j.memory.chat.ChatMemoryProvider;
-import dev.langchain4j.memory.chat.MessageWindowChatMemory;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class FinancialAgentConfig {
-
- @Bean
- ChatMemoryProvider chatMemoryProviderFinancial(MongoChatMemoryStore mongoChatMemoryStore) {
- return memoryId -> MessageWindowChatMemory.builder()
- .id(memoryId)
- .maxMessages(40)
- .chatMemoryStore(mongoChatMemoryStore)
- .build();
- }
-}
diff --git a/src/main/java/com/ruoyi/ai/controller/FinancialAiController.java b/src/main/java/com/ruoyi/ai/controller/FinancialAiController.java
deleted file mode 100644
index 0680713..0000000
--- a/src/main/java/com/ruoyi/ai/controller/FinancialAiController.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.ruoyi.ai.controller;
-
-import com.ruoyi.ai.assistant.FinancialAgent;
-import com.ruoyi.ai.assistant.FinancialIntentExecutor;
-import com.ruoyi.ai.bean.ChatForm;
-import com.ruoyi.ai.context.AiSessionUserContext;
-import com.ruoyi.ai.service.AiChatSessionService;
-import com.ruoyi.ai.store.MongoChatMemoryStore;
-import com.ruoyi.common.utils.SecurityUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.framework.aspectj.lang.annotation.Log;
-import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
-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.RestController;
-import reactor.core.publisher.Flux;
-
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-
-@Tag(name = "璐㈠姟鏅鸿兘浣�")
-@RestController
-@RequestMapping("/financial-ai")
-public class FinancialAiController extends BaseController {
-
- private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
-
- private final FinancialAgent financialAgent;
- private final FinancialIntentExecutor financialIntentExecutor;
- private final AiSessionUserContext aiSessionUserContext;
- private final MongoChatMemoryStore mongoChatMemoryStore;
- private final AiChatSessionService aiChatSessionService;
-
- public FinancialAiController(FinancialAgent financialAgent,
- FinancialIntentExecutor financialIntentExecutor,
- AiSessionUserContext aiSessionUserContext,
- MongoChatMemoryStore mongoChatMemoryStore,
- AiChatSessionService aiChatSessionService) {
- this.financialAgent = financialAgent;
- this.financialIntentExecutor = financialIntentExecutor;
- this.aiSessionUserContext = aiSessionUserContext;
- this.mongoChatMemoryStore = mongoChatMemoryStore;
- this.aiChatSessionService = aiChatSessionService;
- }
-
- @Operation(summary = "璐㈠姟鏅鸿兘浣撳璇�")
- @Log(title = "璐㈠姟鏅鸿兘浣撳璇�", businessType = BusinessType.OTHER)
- @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
- public Flux<String> chat(@RequestBody ChatForm chatForm) {
- 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 = financialIntentExecutor.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 financialAgent.chat(memoryId, userMessage, currentDateForPrompt())
- .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
- .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, 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 = "鍒犻櫎璐㈠姟鏅鸿兘浣撲細璇�")
- @Log(title = "鍒犻櫎璐㈠姟鏅鸿兘浣撲細璇�", businessType = BusinessType.DELETE)
- @DeleteMapping("/history/{memoryId}")
- public AjaxResult deleteSession(@PathVariable String memoryId) {
- aiSessionUserContext.remove(memoryId);
- return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
- }
-
- private String currentDateForPrompt() {
- return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
- }
-}
diff --git a/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java b/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
deleted file mode 100644
index 900242b..0000000
--- a/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
+++ /dev/null
@@ -1,2226 +0,0 @@
-package com.ruoyi.ai.tools;
-
-import com.alibaba.fastjson2.JSON;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
-import com.ruoyi.account.mapper.AccountStatementMapper;
-import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
-import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
-import com.ruoyi.account.pojo.AccountStatement;
-import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
-import com.ruoyi.account.pojo.sales.AccountSalesCollection;
-import com.ruoyi.account.service.impl.AccountingServiceImpl;
-import com.ruoyi.ai.context.AiSessionUserContext;
-import com.ruoyi.basic.mapper.CustomerMapper;
-import com.ruoyi.basic.mapper.ProductMapper;
-import com.ruoyi.basic.mapper.ProductModelMapper;
-import com.ruoyi.basic.mapper.SupplierManageMapper;
-import com.ruoyi.basic.pojo.Customer;
-import com.ruoyi.basic.pojo.Product;
-import com.ruoyi.basic.pojo.ProductModel;
-import com.ruoyi.basic.pojo.SupplierManage;
-import com.ruoyi.common.utils.SecurityUtils;
-import com.ruoyi.device.mapper.DeviceLedgerMapper;
-import com.ruoyi.device.mapper.DeviceRepairMapper;
-import com.ruoyi.device.pojo.DeviceLedger;
-import com.ruoyi.device.pojo.DeviceRepair;
-import com.ruoyi.framework.security.LoginUser;
-import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
-import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
-import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
-import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
-import com.ruoyi.production.mapper.ProductionAccountMapper;
-import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
-import com.ruoyi.production.mapper.ProductionOrderMapper;
-import com.ruoyi.production.mapper.ProductionPlanMapper;
-import com.ruoyi.production.mapper.ProductionProductMainMapper;
-import com.ruoyi.production.mapper.ProductionProductOutputMapper;
-import com.ruoyi.production.pojo.ProductionAccount;
-import com.ruoyi.production.pojo.ProductionOperationTask;
-import com.ruoyi.production.pojo.ProductionOrder;
-import com.ruoyi.production.pojo.ProductionPlan;
-import com.ruoyi.production.pojo.ProductionProductMain;
-import com.ruoyi.production.pojo.ProductionProductOutput;
-import com.ruoyi.sales.mapper.SalesLedgerMapper;
-import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
-import com.ruoyi.sales.pojo.SalesLedger;
-import com.ruoyi.sales.pojo.SalesLedgerProduct;
-import com.ruoyi.stock.mapper.StockInventoryMapper;
-import com.ruoyi.stock.pojo.StockInventory;
-import com.ruoyi.technology.mapper.TechnologyOperationMapper;
-import com.ruoyi.technology.pojo.TechnologyOperation;
-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.util.StringUtils;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.YearMonth;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-@Component
-public class FinancialAgentTools {
-
- private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- private static final Pattern RELATIVE_PATTERN = Pattern.compile("(杩憒鏈�杩�)?\\s*(\\d+)\\s*(澶﹟鍛▅涓湀|鏈坾骞�)");
- private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
- private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
- private static final int DEFAULT_LIMIT = 10;
- private static final int MAX_LIMIT = 50;
- private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.65");
-
- private final SalesLedgerMapper salesLedgerMapper;
- private final SalesLedgerProductMapper salesLedgerProductMapper;
- private final ProductionAccountMapper productionAccountMapper;
- private final ProductionProductMainMapper productionProductMainMapper;
- private final ProductionOperationTaskMapper productionOperationTaskMapper;
- private final ProductionOrderMapper productionOrderMapper;
- private final ProductionPlanMapper productionPlanMapper;
- private final ProductionProductOutputMapper productionProductOutputMapper;
- private final TechnologyOperationMapper technologyOperationMapper;
- private final DeviceLedgerMapper deviceLedgerMapper;
- private final DeviceRepairMapper deviceRepairMapper;
- private final ProcurementRecordMapper procurementRecordMapper;
- private final ProcurementRecordOutMapper procurementRecordOutMapper;
- private final StockInventoryMapper stockInventoryMapper;
- private final AccountSalesCollectionMapper accountSalesCollectionMapper;
- private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
- private final AccountStatementMapper accountStatementMapper;
- private final CustomerMapper customerMapper;
- private final SupplierManageMapper supplierManageMapper;
- private final ProductModelMapper productModelMapper;
- private final ProductMapper productMapper;
- private final AiSessionUserContext aiSessionUserContext;
-
- public FinancialAgentTools(SalesLedgerMapper salesLedgerMapper,
- SalesLedgerProductMapper salesLedgerProductMapper,
- ProductionAccountMapper productionAccountMapper,
- ProductionProductMainMapper productionProductMainMapper,
- ProductionOperationTaskMapper productionOperationTaskMapper,
- ProductionOrderMapper productionOrderMapper,
- ProductionPlanMapper productionPlanMapper,
- ProductionProductOutputMapper productionProductOutputMapper,
- TechnologyOperationMapper technologyOperationMapper,
- DeviceLedgerMapper deviceLedgerMapper,
- DeviceRepairMapper deviceRepairMapper,
- ProcurementRecordMapper procurementRecordMapper,
- ProcurementRecordOutMapper procurementRecordOutMapper,
- StockInventoryMapper stockInventoryMapper,
- AccountSalesCollectionMapper accountSalesCollectionMapper,
- AccountPurchasePaymentMapper accountPurchasePaymentMapper,
- AccountStatementMapper accountStatementMapper,
- CustomerMapper customerMapper,
- SupplierManageMapper supplierManageMapper,
- ProductModelMapper productModelMapper,
- ProductMapper productMapper,
- AiSessionUserContext aiSessionUserContext) {
- this.salesLedgerMapper = salesLedgerMapper;
- this.salesLedgerProductMapper = salesLedgerProductMapper;
- this.productionAccountMapper = productionAccountMapper;
- this.productionProductMainMapper = productionProductMainMapper;
- this.productionOperationTaskMapper = productionOperationTaskMapper;
- this.productionOrderMapper = productionOrderMapper;
- this.productionPlanMapper = productionPlanMapper;
- this.productionProductOutputMapper = productionProductOutputMapper;
- this.technologyOperationMapper = technologyOperationMapper;
- this.deviceLedgerMapper = deviceLedgerMapper;
- this.deviceRepairMapper = deviceRepairMapper;
- this.procurementRecordMapper = procurementRecordMapper;
- this.procurementRecordOutMapper = procurementRecordOutMapper;
- this.stockInventoryMapper = stockInventoryMapper;
- this.accountSalesCollectionMapper = accountSalesCollectionMapper;
- this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
- this.accountStatementMapper = accountStatementMapper;
- this.customerMapper = customerMapper;
- this.supplierManageMapper = supplierManageMapper;
- this.productModelMapper = productModelMapper;
- this.productMapper = productMapper;
- this.aiSessionUserContext = aiSessionUserContext;
- }
-
- @Tool(name = "璐㈠姟鐭ヨ瘑妫�绱�", value = "鎸夎储鍔$粡钀ラ棶棰樻绱笟璐㈣瀺鍚堢煡璇嗙墖娈典笌鎸囨爣鍙e緞锛屼綔涓篟AG涓婁笅鏂囥��")
- public String retrieveFinancialKnowledge(@ToolMemoryId String memoryId,
- @P(value = "闂鎴栧叧閿瘝锛屼緥濡傚埄娑︿笅闄嶃�佸簱瀛樺懆杞�佽祫閲戠己鍙�") String question) {
- List<KnowledgeDoc> knowledgeDocs = financeKnowledgeBase();
- String normalized = normalizeForMatch(question);
- List<KnowledgeDoc> ranked = knowledgeDocs.stream()
- .sorted(Comparator.comparingInt((KnowledgeDoc doc) -> keywordHitCount(doc.keywords(), normalized)).reversed())
- .filter(doc -> keywordHitCount(doc.keywords(), normalized) > 0 || !StringUtils.hasText(normalized))
- .limit(5)
- .toList();
-
- List<Map<String, Object>> items = ranked.stream().map(doc -> {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("topic", doc.topic());
- map.put("knowledge", doc.knowledge());
- map.put("relatedTables", doc.relatedTables());
- map.put("suggestedQuestions", doc.suggestedQuestions());
- return map;
- }).toList();
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("question", safe(question));
- summary.put("hitCount", items.size());
- summary.put("retrievalMode", "keyword_rag");
- return jsonResponse(true, "financial_rag_knowledge", "宸茶繑鍥炶储鍔$煡璇嗘绱㈢粨鏋�", summary, Map.of("items", items), Map.of());
- }
-
- @Tool(name = "鏅鸿兘鎴愭湰鏍哥畻", value = "鑷姩鏍哥畻浜у搧鎴愭湰銆佸伐搴忔垚鏈�佷汉宸ユ垚鏈�佽澶囨姌鏃с�佹潗鏂欐崯鑰椾笌璁㈠崟鍒╂鼎銆�")
- public String calculateIntelligentCost(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽鏈湀銆佽繎30澶�", required = false) String timeRange,
- @P(value = "鍏抽敭璇嶏紝鍙尮閰嶅悎鍚屽彿/瀹㈡埛/椤圭洰", required = false) String keyword,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
- AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("orderCount", bundle.orderMetrics().size());
- summary.put("totalRevenue", bundle.totalRevenue());
- summary.put("totalMaterialCost", bundle.totalMaterialCost());
- summary.put("totalLaborCost", bundle.totalLaborCost());
- summary.put("totalDepreciationCost", bundle.totalDepreciationCost());
- summary.put("totalScrapCost", bundle.totalScrapCost());
- summary.put("totalCost", bundle.totalCost());
- summary.put("totalProfit", bundle.totalProfit());
- summary.put("profitRate", toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())));
-
- List<Map<String, Object>> orderItems = bundle.orderMetrics().stream()
- .map(this::toOrderCostItem)
- .toList();
- List<Map<String, Object>> processItems = bundle.processCostRanking().entrySet().stream()
- .map(entry -> {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("processName", entry.getKey());
- map.put("cost", entry.getValue());
- return map;
- }).toList();
-
- List<Map<String, Object>> topCustomerItems = buildCustomerProfitTop(bundle.orderMetrics(), 5);
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("costCompositionPieOption",
- buildCostCompositionPie(bundle.totalMaterialCost(), bundle.totalLaborCost(), bundle.totalDepreciationCost(), bundle.totalScrapCost()));
- charts.put("orderProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
- charts.put("processCostBarOption", buildProcessCostBar(bundle.processCostRanking()));
-
- return jsonResponse(true, "financial_cost_accounting", "宸插畬鎴愭櫤鑳芥垚鏈牳绠�", summary,
- Map.of(
- "orders", orderItems,
- "processCostRanking", processItems,
- "topCustomers", topCustomerItems
- ),
- charts
- );
- }
-
- @Tool(name = "璁㈠崟鍒╂鼎鍒嗘瀽", value = "璇嗗埆浣庡埄娑�/浜忔崯璁㈠崟锛岃緭鍑哄師鍥犲垎鏋愬拰浼樺寲寤鸿銆�")
- public String analyzeOrderProfit(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽鏈湀銆佽繎30澶�", required = false) String timeRange,
- @P(value = "鍏抽敭璇嶏紝鍙尮閰嶅悎鍚屽彿/瀹㈡埛/椤圭洰", required = false) String keyword,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
- AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
- List<OrderProfitMetric> metrics = bundle.orderMetrics();
-
- List<OrderProfitMetric> riskyOrders = metrics.stream()
- .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(new BigDecimal("0.08")) < 0)
- .sorted(Comparator.comparing(OrderProfitMetric::profitRate)
- .thenComparing(OrderProfitMetric::profit))
- .toList();
-
- Map<String, BigDecimal> customerProfitMap = new LinkedHashMap<>();
- for (OrderProfitMetric metric : metrics) {
- customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
- }
- Map.Entry<String, BigDecimal> topCustomer = customerProfitMap.entrySet().stream()
- .max(Map.Entry.comparingByValue())
- .orElse(Map.entry("鏆傛棤鏁版嵁", BigDecimal.ZERO));
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("orderCount", metrics.size());
- summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count());
- summary.put("lowProfitOrderCount", riskyOrders.size());
- summary.put("avgProfitRate", toPercent(avgRate(metrics)));
- summary.put("topCustomerByProfit", topCustomer.getKey());
- summary.put("topCustomerProfit", topCustomer.getValue());
-
- List<Map<String, Object>> riskyItems = riskyOrders.stream()
- .limit(normalizeLimit(limit))
- .map(this::toRiskOrderItem)
- .toList();
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("profitDistributionOption", buildProfitDistributionBar(metrics));
- charts.put("lossOrderTrendOption", buildLossOrderTrendLine(metrics));
- charts.put("customerProfitTopOption", buildCustomerProfitBar(customerProfitMap));
-
- return jsonResponse(true, "financial_order_profit_analysis", "宸插畬鎴愯鍗曞埄娑﹀垎鏋�", summary,
- Map.of(
- "riskOrders", riskyItems,
- "allOrders", metrics.stream().map(this::toOrderCostItem).toList(),
- "customerProfitTop", buildCustomerProfitTop(metrics, 10)
- ),
- charts
- );
- }
-
- @Tool(name = "搴撳瓨璧勯噾鍒嗘瀽", value = "鍒嗘瀽搴撳瓨绉帇銆佸憜婊炲簱瀛樸�佽祫閲戝崰鐢ㄤ笌鍛ㄨ浆鐜囥��")
- public String analyzeInventoryCapital(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽鏈湀銆佽繎30澶�", required = false) String timeRange,
- @P(value = "鍏抽敭璇嶏紝鍙尮閰嶄骇鍝佸悕绉�/鍨嬪彿", required = false) String keyword,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
- int finalLimit = normalizeLimit(limit);
-
- List<StockInventory> inventoryRows = queryStockInventory(loginUser);
- if (inventoryRows.isEmpty()) {
- return jsonResponse(true, "financial_inventory_capital_analysis", "褰撳墠鏃犲簱瀛樻暟鎹�",
- rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
- }
-
- Set<Long> modelIds = inventoryRows.stream()
- .map(StockInventory::getProductModelId)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- Map<Long, ProductModel> productModelMap = queryProductModels(modelIds);
- Map<Long, Product> productMap = queryProducts(productModelMap.values());
- Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, modelIds);
- OutboundStats outboundStats = queryOutboundStats(loginUser, modelIds, range);
-
- List<InventoryMetric> metrics = buildInventoryMetrics(inventoryRows, productModelMap, productMap, avgUnitCostByModelId, outboundStats)
- .stream()
- .filter(metric -> matchInventoryKeyword(metric, keyword))
- .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
- .toList();
-
- BigDecimal totalInventoryValue = metrics.stream().map(InventoryMetric::inventoryValue).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal stagnantValue = metrics.stream()
- .filter(metric -> metric.stagnantDays() >= 90)
- .map(InventoryMetric::inventoryValue)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- long stagnantCount = metrics.stream().filter(metric -> metric.stagnantDays() >= 90).count();
- long overstockCount = metrics.stream().filter(InventoryMetric::overstock).count();
- BigDecimal totalOutboundCost = outboundStats.totalOutboundCost();
- BigDecimal turnoverDays = totalOutboundCost.compareTo(BigDecimal.ZERO) > 0
- ? totalInventoryValue.multiply(BigDecimal.valueOf(daysBetween(range.start(), range.end()) + 1L))
- .divide(totalOutboundCost, 2, RoundingMode.HALF_UP)
- : BigDecimal.ZERO;
-
- List<Map<String, Object>> items = metrics.stream()
- .limit(finalLimit)
- .map(this::toInventoryItem)
- .toList();
-
- Map<String, Object> summary = rangeSummary(range, metrics.size(), keyword);
- summary.put("totalInventoryValue", totalInventoryValue);
- summary.put("stagnantValue", stagnantValue);
- summary.put("stagnantCount", stagnantCount);
- summary.put("overstockCount", overstockCount);
- summary.put("turnoverDays", turnoverDays);
- summary.put("capitalOccupation", totalInventoryValue);
- summary.put("totalOutboundCost", totalOutboundCost);
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("inventoryValueTopOption", buildInventoryTopBar(metrics));
- charts.put("inventoryAgingPieOption", buildInventoryAgingPie(metrics));
- charts.put("inventoryTurnoverGauge", buildTurnoverGauge(turnoverDays));
-
- return jsonResponse(true, "financial_inventory_capital_analysis", "宸插畬鎴愬簱瀛樿祫閲戝垎鏋�", summary, Map.of("items", items), charts);
- }
-
- @Tool(name = "搴旀敹搴斾粯涓庣幇閲戞祦棰勬祴", value = "棰勬祴鏈潵鐜伴噾娴併�佸洖娆鹃闄┿�佷粯娆惧帇鍔涗笌璧勯噾缂哄彛銆�")
- public String forecastCashFlow(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽杩�90澶┿�佹湰骞�", required = false) String timeRange,
- @P(value = "棰勬祴鏈堜唤鏁帮紝榛樿3锛屾渶澶�6", required = false) Integer forecastMonths) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�90澶�");
- int months = forecastMonths == null || forecastMonths <= 0 ? 3 : Math.min(forecastMonths, 6);
-
- List<AccountSalesCollection> collections = queryCollections(loginUser, range);
- List<AccountPurchasePayment> payments = queryPayments(loginUser, range);
- List<MonthlyCashFlow> monthlyActual = buildMonthlyCashFlow(range, collections, payments);
- List<MonthlyCashFlow> monthlyForecast = forecastMonthlyCashFlow(monthlyActual, months);
-
- StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
- BigDecimal receivableTotal = snapshot.receivableTotal();
- BigDecimal payableTotal = snapshot.payableTotal();
- BigDecimal forecastNetSum = monthlyForecast.stream().map(MonthlyCashFlow::netFlow).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal coverage = receivableTotal.add(maxZero(forecastNetSum));
- BigDecimal fundGap = maxZero(payableTotal.subtract(coverage));
-
- Map<String, String> customerNameMap = queryCustomerNameMap(snapshot.receivableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
- Map<String, String> supplierNameMap = querySupplierNameMap(snapshot.payableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
-
- List<Map<String, Object>> receivableRiskItems = snapshot.receivableTop().stream().map(item -> toStatementRiskItem(item, customerNameMap, "customer")).toList();
- List<Map<String, Object>> payablePressureItems = snapshot.payableTop().stream().map(item -> toStatementRiskItem(item, supplierNameMap, "supplier")).toList();
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
- summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
- summary.put("receivableBalance", receivableTotal);
- summary.put("payableBalance", payableTotal);
- summary.put("forecastNetSum", forecastNetSum);
- summary.put("fundGap", fundGap);
- summary.put("forecastMonths", months);
- summary.put("collectionRiskLevel", riskLevelByAmount(receivableTotal));
- summary.put("paymentPressureLevel", riskLevelByAmount(payableTotal));
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("cashFlowTrendOption", buildCashflowTrend(monthlyActual, monthlyForecast));
- charts.put("receivablePayableBarOption", buildReceivablePayableBar(receivableTotal, payableTotal));
- charts.put("fundGapGaugeOption", buildFundGapGauge(fundGap));
-
- return jsonResponse(true, "financial_cashflow_forecast", "宸插畬鎴愬簲鏀跺簲浠樹笌鐜伴噾娴侀娴�", summary,
- Map.of(
- "actualMonthly", monthlyActual.stream().map(this::toMonthlyCashFlowItem).toList(),
- "forecastMonthly", monthlyForecast.stream().map(this::toMonthlyCashFlowItem).toList(),
- "receivableRiskTop", receivableRiskItems,
- "payablePressureTop", payablePressureItems
- ),
- charts
- );
- }
-
- @Tool(name = "缁忚惀寮傚父棰勮", value = "璇嗗埆鎴愭湰寮傚父銆佸埄娑﹀紓甯搞�佸洖娆惧紓甯搞�佽鍗曢闄┿�佸簱瀛樺紓甯搞��")
- public String detectBusinessAnomalies(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽杩�30澶�", required = false) String timeRange,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
- int finalLimit = normalizeLimit(limit);
-
- AnalysisBundle currentBundle = buildOrderProfitBundle(loginUser, range, null, Math.max(finalLimit, 30));
- DateRange prevRange = previousSameLengthRange(range);
- AnalysisBundle prevBundle = buildOrderProfitBundle(loginUser, prevRange, null, 50);
-
- BigDecimal currentCostRate = rate(currentBundle.totalCost(), currentBundle.totalRevenue());
- BigDecimal prevCostRate = rate(prevBundle.totalCost(), prevBundle.totalRevenue());
- BigDecimal costRateDiff = currentCostRate.subtract(prevCostRate);
-
- StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
- List<InventoryMetric> inventoryMetrics = buildInventoryMetrics(
- queryStockInventory(loginUser),
- queryProductModels(Collections.emptySet()),
- Map.of(),
- queryAverageUnitCostByModel(loginUser, Collections.emptySet()),
- queryOutboundStats(loginUser, Collections.emptySet(), range)
- );
-
- List<Map<String, Object>> anomalyItems = new ArrayList<>();
- if (costRateDiff.compareTo(new BigDecimal("0.10")) > 0) {
- anomalyItems.add(anomalyItem("high", "鎴愭湰寮傚父", "鍗曚綅鏀跺叆鎴愭湰鐜囪緝涓婂懆鏈熶笂鍗囪秴杩�10%", Map.of(
- "currentCostRate", toPercent(currentCostRate),
- "previousCostRate", toPercent(prevCostRate),
- "delta", toPercent(costRateDiff)
- )));
- }
- long lossCount = currentBundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
- if (lossCount > 0) {
- anomalyItems.add(anomalyItem("high", "鍒╂鼎寮傚父", "妫�娴嬪埌浜忔崯璁㈠崟", Map.of("lossOrderCount", lossCount)));
- }
- if (snapshot.receivableTotal().compareTo(snapshot.payableTotal().multiply(new BigDecimal("1.2"))) > 0) {
- anomalyItems.add(anomalyItem("medium", "鍥炴寮傚父", "搴旀敹浣欓鏄捐憲楂樹簬搴斾粯锛屽洖娆惧帇鍔涘亸澶�", Map.of(
- "receivableBalance", snapshot.receivableTotal(),
- "payableBalance", snapshot.payableTotal()
- )));
- }
- long overdueOrderCount = currentBundle.orderMetrics().stream()
- .filter(item -> item.deliveryDate() != null && item.deliveryDate().isBefore(LocalDate.now()) && item.profitRate().compareTo(new BigDecimal("0.10")) < 0)
- .count();
- if (overdueOrderCount > 0) {
- anomalyItems.add(anomalyItem("medium", "璁㈠崟椋庨櫓", "瀛樺湪浣庡埄娑︿笖浜や粯宸查�炬湡璁㈠崟", Map.of("overdueRiskOrderCount", overdueOrderCount)));
- }
- long stagnantCount = inventoryMetrics.stream().filter(item -> item.stagnantDays() >= 90).count();
- if (stagnantCount > 0) {
- anomalyItems.add(anomalyItem("medium", "搴撳瓨寮傚父", "瀛樺湪瓒呰繃90澶╂湭鍛ㄨ浆搴撳瓨", Map.of("stagnantCount", stagnantCount)));
- }
-
- List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList();
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("anomalyCount", topAnomalies.size());
- summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count());
- summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count());
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("anomalyLevelPieOption", buildAnomalyLevelPie(topAnomalies));
- charts.put("anomalyTypeBarOption", buildAnomalyTypeBar(topAnomalies));
- return jsonResponse(true, "financial_business_anomaly_warning", "宸插畬鎴愮粡钀ュ紓甯搁璀﹀垎鏋�", summary,
- Map.of("items", topAnomalies), charts);
- }
-
- @Tool(name = "AI缁忚惀椹鹃┒鑸�", value = "瀹炴椂灞曠ず浜у�笺�佸埄娑︺�佸簱瀛樸�佸洖娆俱�佽澶囧埄鐢ㄧ巼銆佽鍗曞埄娑︾巼绛夋牳蹇冪粡钀ユ寚鏍囥��")
- public String getBusinessCockpit(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽鏈湀銆佽繎30澶�", required = false) String timeRange) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange, "鏈湀");
-
- AnalysisBundle profitBundle = buildOrderProfitBundle(loginUser, range, null, 30);
- StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
- List<StockInventory> inventories = queryStockInventory(loginUser);
- BigDecimal inventoryValue = estimateInventoryValue(loginUser, inventories);
-
- long deviceTotal = countDevices(loginUser);
- long repairingCount = countRepairingDevices(loginUser);
- BigDecimal deviceUtilization = deviceTotal > 0
- ? new BigDecimal(deviceTotal - repairingCount).multiply(ONE_HUNDRED).divide(new BigDecimal(deviceTotal), 2, RoundingMode.HALF_UP)
- : BigDecimal.ZERO;
-
- BigDecimal outputValue = profitBundle.totalRevenue();
- BigDecimal profitRate = rate(profitBundle.totalProfit(), profitBundle.totalRevenue());
- BigDecimal collectionRate = snapshot.receivableTotal().compareTo(BigDecimal.ZERO) > 0
- ? ONE_HUNDRED.subtract(rate(snapshot.receivableTotal(), snapshot.receivableTotal().add(snapshot.payableTotal())).multiply(ONE_HUNDRED))
- : BigDecimal.ZERO;
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("outputValue", outputValue);
- summary.put("profit", profitBundle.totalProfit());
- summary.put("profitRate", toPercent(profitRate));
- summary.put("inventoryValue", inventoryValue);
- summary.put("receivableBalance", snapshot.receivableTotal());
- summary.put("payableBalance", snapshot.payableTotal());
- summary.put("collectionRate", toPercent(collectionRate.divide(ONE_HUNDRED, 4, RoundingMode.HALF_UP)));
- summary.put("deviceUtilizationRate", deviceUtilization + "%");
- summary.put("orderProfitRate", toPercent(avgRate(profitBundle.orderMetrics())));
-
- Map<String, Object> indicators = new LinkedHashMap<>();
- indicators.put("浜у��", outputValue);
- indicators.put("鍒╂鼎", profitBundle.totalProfit());
- indicators.put("搴撳瓨璧勯噾鍗犵敤", inventoryValue);
- indicators.put("搴旀敹浣欓", snapshot.receivableTotal());
- indicators.put("璁惧鍒╃敤鐜�", deviceUtilization + "%");
- indicators.put("璁㈠崟骞冲潎鍒╂鼎鐜�", toPercent(avgRate(profitBundle.orderMetrics())));
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("kpiCardData", indicators);
- charts.put("profitTrendOption", buildOrderProfitBar(profitBundle.orderMetrics()));
- charts.put("receivablePayableBarOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
- charts.put("inventoryProfitGaugeOption", buildInventoryProfitGauge(inventoryValue, profitBundle.totalProfit()));
-
- return jsonResponse(true, "financial_business_cockpit", "宸茬敓鎴怉I缁忚惀椹鹃┒鑸辨暟鎹�", summary,
- Map.of(
- "orderProfitTop", profitBundle.orderMetrics().stream()
- .sorted(Comparator.comparing(OrderProfitMetric::profit).reversed())
- .limit(10)
- .map(this::toOrderCostItem)
- .toList(),
- "indicators", indicators
- ),
- charts
- );
- }
-
- @Tool(name = "鏃ユ姤鍛ㄦ姤鑷姩鐢熸垚", value = "鑷姩杈撳嚭缁忚惀鍒嗘瀽鏃ユ姤/鍛ㄦ姤涓庨闄╁缓璁��")
- public String generateOperationReport(@ToolMemoryId String memoryId,
- @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
- @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
- @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屽浠婂ぉ銆佹湰鍛�", required = false) String timeRange,
- @P(value = "鎶ュ憡绫诲瀷 daily/weekly", required = false) String reportType) {
- LoginUser loginUser = currentLoginUser(memoryId);
- DateRange range = resolveDateRange(startDate, endDate, timeRange,
- "weekly".equalsIgnoreCase(reportType) ? "鏈懆" : "浠婂ぉ");
- String type = "weekly".equalsIgnoreCase(reportType) ? "weekly" : "daily";
-
- AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, null, 30);
- StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
- BigDecimal inventoryValue = estimateInventoryValue(loginUser, queryStockInventory(loginUser));
- long lossCount = bundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
-
- List<String> conclusions = new ArrayList<>();
- conclusions.add("钀ユ敹" + bundle.totalRevenue() + "锛屽埄娑�" + bundle.totalProfit() + "锛屽埄娑︾巼" + toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())) + "銆�");
- conclusions.add("搴旀敹浣欓" + snapshot.receivableTotal() + "锛屽簲浠樹綑棰�" + snapshot.payableTotal() + "锛屽簱瀛樿祫閲戝崰鐢�" + inventoryValue + "銆�");
- if (lossCount > 0) {
- conclusions.add("鍙戠幇浜忔崯璁㈠崟" + lossCount + "涓紝寤鸿浼樺厛澶嶆牳鏉愭枡鎹熻�楀拰宸ュ簭浜哄伐鏁堢巼銆�");
- } else {
- conclusions.add("褰撳墠鏈彂鐜颁簭鎹熻鍗曪紝寤鸿鎸佺画璺熻釜浣庝簬8%鍒╂鼎鐜囪鍗曘��");
- }
- if (snapshot.receivableTotal().compareTo(snapshot.payableTotal()) > 0) {
- conclusions.add("鍥炴鍘嬪姏鍋忛珮锛屽缓璁拡瀵归珮搴旀敹瀹㈡埛鎵ц鍒嗗眰鍌敹涓庤处鏈熶紭鍖栥��");
- } else {
- conclusions.add("璧勯噾鍘嬪姏鍙帶锛屽缓璁繚鎸佷粯娆捐鍒掍笌閲囪喘鑺傚鑱斿姩銆�");
- }
-
- List<Map<String, Object>> riskSuggestions = new ArrayList<>();
- if (lossCount > 0) {
- riskSuggestions.add(riskSuggestion("鍒╂鼎椋庨櫓", "楂�", "澶嶆牳浜忔崯璁㈠崟BOM鍜屽伐搴忓伐璧勫畾棰濓紝蹇呰鏃惰皟鏁存姤浠蜂笌浜や粯鑺傚銆�"));
- }
- if (snapshot.receivableTotal().compareTo(new BigDecimal("1000000")) > 0) {
- riskSuggestions.add(riskSuggestion("鍥炴椋庨櫓", "涓�", "瀵瑰簲鏀禩OP瀹㈡埛寤虹珛鍛ㄥ害鍥炴璁″垝锛屽苟璁剧疆棰勮闃堝�笺��"));
- }
- if (inventoryValue.compareTo(new BigDecimal("3000000")) > 0) {
- riskSuggestions.add(riskSuggestion("搴撳瓨椋庨櫓", "涓�", "瀵归珮閲戦鍛嗘粸搴撳瓨鎵ц闄嶄环銆佹浛浠e拰鐢熶骇娑堣�楃瓥鐣ャ��"));
- }
-
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("reportType", type);
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("orderCount", bundle.orderMetrics().size());
- summary.put("lossOrderCount", lossCount);
- summary.put("riskSuggestionCount", riskSuggestions.size());
-
- Map<String, Object> data = new LinkedHashMap<>();
- data.put("headline", "weekly".equals(type) ? "缁忚惀鍛ㄦ姤" : "缁忚惀鏃ユ姤");
- data.put("conclusions", conclusions);
- data.put("riskSuggestions", riskSuggestions);
- data.put("orderProfitTop", bundle.orderMetrics().stream()
- .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
- .limit(10)
- .map(this::toRiskOrderItem)
- .toList());
-
- Map<String, Object> charts = new LinkedHashMap<>();
- charts.put("reportProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
- charts.put("reportReceivablePayableOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
- return jsonResponse(true, "financial_operation_report", "宸茶嚜鍔ㄧ敓鎴愮粡钀ュ垎鏋愭姤鍛�", summary, data, charts);
- }
-
- private AnalysisBundle buildOrderProfitBundle(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
- List<SalesLedger> ledgers = querySalesLedgers(loginUser, range, keyword, limit);
- if (ledgers.isEmpty()) {
- return AnalysisBundle.empty();
- }
-
- List<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).toList();
- List<SalesLedgerProduct> ledgerProducts = queryLedgerProducts(loginUser, ledgerIds);
- Map<Long, List<SalesLedgerProduct>> productsByLedgerId = ledgerProducts.stream()
- .collect(Collectors.groupingBy(SalesLedgerProduct::getSalesLedgerId));
-
- MaterialCostResult materialCostResult = calculateMaterialCost(loginUser, range, ledgerProducts);
- ProductionCostContext productionCostContext = calculateProductionCost(loginUser, range, ledgers, ledgerProducts, materialCostResult.avgUnitCostByModelId());
- BigDecimal totalDepreciation = calculateTotalDepreciation(loginUser);
-
- BigDecimal totalRevenue = ledgers.stream()
- .map(SalesLedger::getContractAmount)
- .filter(Objects::nonNull)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- Map<Long, BigDecimal> depreciationCostByLedger = allocateDepreciation(ledgers, totalDepreciation, totalRevenue);
-
- List<OrderProfitMetric> metrics = new ArrayList<>();
- for (SalesLedger ledger : ledgers) {
- BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
- BigDecimal materialCost = materialCostResult.materialCostByLedgerId().getOrDefault(ledger.getId(), fallbackMaterialCost(productsByLedgerId.get(ledger.getId()), revenue));
- BigDecimal laborCost = productionCostContext.laborCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
- BigDecimal scrapCost = productionCostContext.scrapCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
- BigDecimal depreciationCost = depreciationCostByLedger.getOrDefault(ledger.getId(), BigDecimal.ZERO);
- BigDecimal totalCost = materialCost.add(laborCost).add(scrapCost).add(depreciationCost);
- BigDecimal profit = revenue.subtract(totalCost);
- BigDecimal profitRate = rate(profit, revenue);
- String riskLevel = profit.compareTo(BigDecimal.ZERO) < 0
- ? "high"
- : (profitRate.compareTo(new BigDecimal("0.08")) < 0 ? "medium" : "low");
- List<String> reasons = buildProfitReasons(revenue, materialCost, laborCost, scrapCost, profit, profitRate);
- String suggestion = buildProfitSuggestion(riskLevel, reasons);
-
- metrics.add(new OrderProfitMetric(
- ledger.getId(),
- safe(ledger.getSalesContractNo()),
- safe(ledger.getCustomerName()),
- safe(ledger.getProjectName()),
- toLocalDate(ledger.getEntryDate()),
- ledger.getDeliveryDate(),
- revenue,
- materialCost,
- laborCost,
- depreciationCost,
- scrapCost,
- totalCost,
- profit,
- profitRate,
- riskLevel,
- reasons,
- suggestion
- ));
- }
-
- metrics.sort(Comparator.comparing(OrderProfitMetric::entryDate, Comparator.nullsLast(Comparator.reverseOrder()))
- .thenComparing(OrderProfitMetric::ledgerId, Comparator.nullsLast(Comparator.reverseOrder())));
- BigDecimal totalMaterialCost = metrics.stream().map(OrderProfitMetric::materialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal totalLaborCost = metrics.stream().map(OrderProfitMetric::laborCost).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal totalScrapCost = metrics.stream().map(OrderProfitMetric::scrapCost).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal totalDepreciationCost = metrics.stream().map(OrderProfitMetric::depreciationCost).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal totalCost = metrics.stream().map(OrderProfitMetric::totalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
- BigDecimal totalProfit = metrics.stream().map(OrderProfitMetric::profit).reduce(BigDecimal.ZERO, BigDecimal::add);
-
- return new AnalysisBundle(
- metrics,
- productionCostContext.processCostRanking(),
- totalRevenue,
- totalMaterialCost,
- totalLaborCost,
- totalDepreciationCost,
- totalScrapCost,
- totalCost,
- totalProfit
- );
- }
-
- private MaterialCostResult calculateMaterialCost(LoginUser loginUser, DateRange range, List<SalesLedgerProduct> ledgerProducts) {
- if (ledgerProducts.isEmpty()) {
- return new MaterialCostResult(Map.of(), Map.of());
- }
- List<Long> ledgerProductIds = ledgerProducts.stream().map(SalesLedgerProduct::getId).filter(Objects::nonNull).toList();
- Set<Long> productModelIds = ledgerProducts.stream().map(SalesLedgerProduct::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
- Map<Long, Long> productIdToLedgerId = ledgerProducts.stream()
- .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
- .collect(Collectors.toMap(SalesLedgerProduct::getId, SalesLedgerProduct::getSalesLedgerId, (a, b) -> a));
-
- Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, productModelIds);
- LambdaQueryWrapper<ProcurementRecordOut> outWrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
- applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
- outWrapper.eq(ProcurementRecordOut::getType, 2)
- .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds)
- .ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
- .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
- List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper));
-
- Set<Integer> storageIds = outList.stream()
- .map(ProcurementRecordOut::getProcurementRecordStorageId)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
- ? Map.of()
- : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
- .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
-
- Map<Long, BigDecimal> materialCostByLedgerId = new HashMap<>();
- for (ProcurementRecordOut out : outList) {
- Long ledgerId = productIdToLedgerId.get(out.getSalesLedgerProductId());
- if (ledgerId == null) {
- continue;
- }
- ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
- BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
- BigDecimal quantity = defaultDecimal(out.getInboundNum());
- BigDecimal cost = quantity.multiply(unitPrice);
- materialCostByLedgerId.merge(ledgerId, cost, BigDecimal::add);
- }
- return new MaterialCostResult(materialCostByLedgerId, avgUnitCostByModelId);
- }
-
- private ProductionCostContext calculateProductionCost(LoginUser loginUser,
- DateRange range,
- List<SalesLedger> ledgers,
- List<SalesLedgerProduct> ledgerProducts,
- Map<Long, BigDecimal> avgUnitCostByModelId) {
- if (ledgers.isEmpty()) {
- return ProductionCostContext.empty();
- }
-
- Set<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toSet());
- Map<Long, Set<Long>> productModelToLedgerIds = new HashMap<>();
- for (SalesLedgerProduct product : ledgerProducts) {
- if (product.getProductModelId() == null || product.getSalesLedgerId() == null) {
- continue;
- }
- productModelToLedgerIds.computeIfAbsent(product.getProductModelId(), key -> new HashSet<>()).add(product.getSalesLedgerId());
- }
-
- LambdaQueryWrapper<ProductionPlan> planWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(planWrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
- planWrapper.in(ProductionPlan::getSalesLedgerId, ledgerIds);
- List<ProductionPlan> plans = defaultList(productionPlanMapper.selectList(planWrapper));
- Map<Long, Long> planIdToLedgerId = plans.stream()
- .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
- .collect(Collectors.toMap(ProductionPlan::getId, ProductionPlan::getSalesLedgerId, (a, b) -> a));
-
- LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId);
- orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
- .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
- List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper));
-
- Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>();
- for (ProductionOrder order : orders) {
- Set<Long> orderLedgers = new HashSet<>();
- for (Long planId : parseIdList(order.getProductionPlanIds())) {
- Long ledgerId = planIdToLedgerId.get(planId);
- if (ledgerId != null) {
- orderLedgers.add(ledgerId);
- }
- }
- if (orderLedgers.isEmpty() && order.getProductModelId() != null) {
- orderLedgers.addAll(productModelToLedgerIds.getOrDefault(order.getProductModelId(), Set.of()));
- }
- if (!orderLedgers.isEmpty()) {
- orderIdToLedgerIds.put(order.getId(), orderLedgers);
- }
- }
- if (orderIdToLedgerIds.isEmpty()) {
- return ProductionCostContext.empty();
- }
-
- LambdaQueryWrapper<ProductionOperationTask> taskWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(taskWrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
- taskWrapper.in(ProductionOperationTask::getProductionOrderId, orderIdToLedgerIds.keySet());
- List<ProductionOperationTask> tasks = defaultList(productionOperationTaskMapper.selectList(taskWrapper));
- Map<Long, Long> taskIdToOrderId = tasks.stream()
- .filter(item -> item.getId() != null && item.getProductionOrderId() != null)
- .collect(Collectors.toMap(ProductionOperationTask::getId, ProductionOperationTask::getProductionOrderId, (a, b) -> a));
- if (taskIdToOrderId.isEmpty()) {
- return ProductionCostContext.empty();
- }
-
- LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
- mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet())
- .ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
- .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
- List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper));
- Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>();
- for (ProductionProductMain main : mainList) {
- Long orderId = taskIdToOrderId.get(main.getProductionOperationTaskId());
- if (orderId == null) {
- continue;
- }
- Set<Long> ledgerSet = orderIdToLedgerIds.get(orderId);
- if (ledgerSet == null || ledgerSet.isEmpty()) {
- continue;
- }
- mainIdToLedgers.put(main.getId(), ledgerSet);
- }
- if (mainIdToLedgers.isEmpty()) {
- return ProductionCostContext.empty();
- }
-
- LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId);
- accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet())
- .ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
- .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
- List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper));
-
- Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>()
- .select(TechnologyOperation::getName, TechnologyOperation::getSalaryQuota)))
- .stream()
- .filter(item -> StringUtils.hasText(item.getName()))
- .collect(Collectors.toMap(TechnologyOperation::getName, item -> defaultDecimal(item.getSalaryQuota()), (a, b) -> a));
-
- Map<Long, BigDecimal> laborCostByLedger = new HashMap<>();
- Map<String, BigDecimal> processCostMap = new HashMap<>();
- for (ProductionAccount account : accountList) {
- Set<Long> ledgerSet = mainIdToLedgers.get(account.getProductionProductMainId());
- if (ledgerSet == null || ledgerSet.isEmpty()) {
- continue;
- }
- BigDecimal cost = estimateLaborCost(account, salaryQuotaByOperation);
- if (cost.compareTo(BigDecimal.ZERO) <= 0) {
- continue;
- }
- BigDecimal split = cost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
- for (Long ledgerId : ledgerSet) {
- laborCostByLedger.merge(ledgerId, split, BigDecimal::add);
- }
- processCostMap.merge(safe(account.getTechnologyOperationName()), cost, BigDecimal::add);
- }
-
- LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId);
- outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet())
- .ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
- .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
- List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper));
- Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>();
- for (ProductionProductOutput output : outputList) {
- Set<Long> ledgerSet = mainIdToLedgers.get(output.getProductionProductMainId());
- if (ledgerSet == null || ledgerSet.isEmpty()) {
- continue;
- }
- BigDecimal scrapQty = defaultDecimal(output.getScrapQty());
- if (scrapQty.compareTo(BigDecimal.ZERO) <= 0) {
- continue;
- }
- BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(output.getProductModelId(), BigDecimal.ZERO);
- BigDecimal scrapCost = scrapQty.multiply(unitCost);
- BigDecimal split = scrapCost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
- for (Long ledgerId : ledgerSet) {
- scrapCostByLedger.merge(ledgerId, split, BigDecimal::add);
- }
- }
-
- Map<String, BigDecimal> processCostRanking = processCostMap.entrySet().stream()
- .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
- .limit(10)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
-
- return new ProductionCostContext(laborCostByLedger, scrapCostByLedger, processCostRanking);
- }
-
- private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
- LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
- if (StringUtils.hasText(keyword)) {
- wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword)
- .or().like(SalesLedger::getCustomerContractNo, keyword)
- .or().like(SalesLedger::getCustomerName, keyword)
- .or().like(SalesLedger::getProjectName, keyword)
- .or().like(SalesLedger::getSalesman, keyword));
- }
- wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
- .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()))
- .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId)
- .last("limit " + normalizeLimit(limit));
- return defaultList(salesLedgerMapper.selectList(wrapper));
- }
-
- private List<SalesLedgerProduct> queryLedgerProducts(LoginUser loginUser, List<Long> ledgerIds) {
- if (ledgerIds == null || ledgerIds.isEmpty()) {
- return List.of();
- }
- LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedgerProduct::getDeptId);
- wrapper.in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)
- .eq(SalesLedgerProduct::getType, 1);
- return defaultList(salesLedgerProductMapper.selectList(wrapper));
- }
-
- private List<StockInventory> queryStockInventory(LoginUser loginUser) {
- LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
- return defaultList(stockInventoryMapper.selectList(wrapper));
- }
-
- private Map<Long, ProductModel> queryProductModels(Set<Long> modelIds) {
- if (modelIds == null || modelIds.isEmpty()) {
- return defaultList(productModelMapper.selectList(null)).stream()
- .filter(item -> item.getId() != null)
- .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
- }
- LambdaQueryWrapper<ProductModel> wrapper = new LambdaQueryWrapper<>();
- wrapper.in(ProductModel::getId, modelIds);
- return defaultList(productModelMapper.selectList(wrapper)).stream()
- .filter(item -> item.getId() != null)
- .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
- }
-
- private Map<Long, Product> queryProducts(Collection<ProductModel> models) {
- Set<Long> productIds = models == null ? Set.of() : models.stream()
- .map(ProductModel::getProductId)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- if (productIds.isEmpty()) {
- return Map.of();
- }
- LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
- wrapper.in(Product::getId, productIds);
- return defaultList(productMapper.selectList(wrapper)).stream()
- .filter(item -> item.getId() != null)
- .collect(Collectors.toMap(Product::getId, item -> item, (a, b) -> a));
- }
-
- private Map<Long, BigDecimal> queryAverageUnitCostByModel(LoginUser loginUser, Set<Long> productModelIds) {
- LambdaQueryWrapper<ProcurementRecordStorage> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordStorage::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordStorage::getDeptId);
- wrapper.in(ProcurementRecordStorage::getType, List.of(1, 2));
- if (productModelIds != null && !productModelIds.isEmpty()) {
- wrapper.in(ProcurementRecordStorage::getProductModelId, productModelIds);
- }
- List<ProcurementRecordStorage> rows = defaultList(procurementRecordMapper.selectList(wrapper));
- Map<Long, BigDecimal> amountByModel = new HashMap<>();
- Map<Long, BigDecimal> qtyByModel = new HashMap<>();
- for (ProcurementRecordStorage row : rows) {
- if (row.getProductModelId() == null) {
- continue;
- }
- BigDecimal qty = defaultDecimal(row.getInboundNum());
- if (qty.compareTo(BigDecimal.ZERO) <= 0) {
- continue;
- }
- BigDecimal amount = defaultDecimal(row.getUnitPrice()).multiply(qty);
- amountByModel.merge(row.getProductModelId(), amount, BigDecimal::add);
- qtyByModel.merge(row.getProductModelId(), qty, BigDecimal::add);
- }
- Map<Long, BigDecimal> result = new HashMap<>();
- for (Map.Entry<Long, BigDecimal> entry : amountByModel.entrySet()) {
- BigDecimal qty = qtyByModel.get(entry.getKey());
- if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
- continue;
- }
- result.put(entry.getKey(), entry.getValue().divide(qty, 6, RoundingMode.HALF_UP));
- }
- return result;
- }
-
- private OutboundStats queryOutboundStats(LoginUser loginUser, Set<Long> productModelIds, DateRange range) {
- LambdaQueryWrapper<ProcurementRecordOut> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
- if (productModelIds != null && !productModelIds.isEmpty()) {
- wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds);
- }
- wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
- .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
- List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper));
- if (outList.isEmpty()) {
- return OutboundStats.empty();
- }
- Set<Integer> storageIds = outList.stream()
- .map(ProcurementRecordOut::getProcurementRecordStorageId)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
- ? Map.of()
- : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
- .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
-
- Map<Long, BigDecimal> outboundQtyByModel = new HashMap<>();
- Map<Long, LocalDateTime> lastOutboundTimeByModel = new HashMap<>();
- BigDecimal totalOutboundCost = BigDecimal.ZERO;
- for (ProcurementRecordOut out : outList) {
- Long modelId = out.getProductModelId();
- if (modelId == null) {
- continue;
- }
- BigDecimal qty = defaultDecimal(out.getInboundNum());
- outboundQtyByModel.merge(modelId, qty, BigDecimal::add);
- if (out.getCreateTime() != null) {
- LocalDateTime existing = lastOutboundTimeByModel.get(modelId);
- if (existing == null || out.getCreateTime().isAfter(existing)) {
- lastOutboundTimeByModel.put(modelId, out.getCreateTime());
- }
- }
- ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
- BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
- totalOutboundCost = totalOutboundCost.add(unitPrice.multiply(qty));
- }
- return new OutboundStats(outboundQtyByModel, lastOutboundTimeByModel, totalOutboundCost);
- }
-
- private List<InventoryMetric> buildInventoryMetrics(List<StockInventory> inventoryRows,
- Map<Long, ProductModel> productModelMap,
- Map<Long, Product> productMap,
- Map<Long, BigDecimal> avgUnitCostByModelId,
- OutboundStats outboundStats) {
- Map<Long, InventoryMetricBuilder> metricBuilderByModel = new HashMap<>();
- for (StockInventory row : inventoryRows) {
- if (row.getProductModelId() == null) {
- continue;
- }
- InventoryMetricBuilder builder = metricBuilderByModel.computeIfAbsent(row.getProductModelId(), InventoryMetricBuilder::new);
- builder.addQuantity(maxZero(defaultDecimal(row.getQualitity()).subtract(defaultDecimal(row.getLockedQuantity()))));
- builder.addLockedQuantity(defaultDecimal(row.getLockedQuantity()));
- builder.addWarnNum(defaultDecimal(row.getWarnNum()));
- if (row.getCreateTime() != null) {
- builder.updateFirstInTime(row.getCreateTime());
- }
- }
-
- List<InventoryMetric> result = new ArrayList<>();
- LocalDate today = LocalDate.now();
- for (InventoryMetricBuilder builder : metricBuilderByModel.values()) {
- Long modelId = builder.modelId();
- ProductModel model = productModelMap.get(modelId);
- Product product = model == null ? null : productMap.get(model.getProductId());
- BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(modelId, BigDecimal.ZERO);
- BigDecimal value = builder.quantity().multiply(unitCost);
- LocalDateTime lastOutTime = outboundStats.lastOutboundTimeByModel().get(modelId);
- long stagnantDays;
- if (lastOutTime != null) {
- stagnantDays = daysBetween(lastOutTime.toLocalDate(), today);
- } else if (builder.firstInTime() != null) {
- stagnantDays = daysBetween(builder.firstInTime().toLocalDate(), today);
- } else {
- stagnantDays = 0;
- }
- BigDecimal outQty = outboundStats.outboundQtyByModel().getOrDefault(modelId, BigDecimal.ZERO);
- boolean overstock = builder.warnNum().compareTo(BigDecimal.ZERO) > 0
- && builder.quantity().compareTo(builder.warnNum().multiply(new BigDecimal("3"))) > 0;
- result.add(new InventoryMetric(
- modelId,
- product == null ? "鏈煡浜у搧" : safe(product.getProductName()),
- model == null ? "鏈煡鍨嬪彿" : safe(model.getModel()),
- builder.quantity(),
- builder.lockedQuantity(),
- unitCost,
- value,
- outQty,
- stagnantDays,
- overstock
- ));
- }
- return result;
- }
-
- private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
- LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
- wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
- .le(AccountSalesCollection::getCollectionDate, range.end())
- .orderByAsc(AccountSalesCollection::getCollectionDate);
- return defaultList(accountSalesCollectionMapper.selectList(wrapper));
- }
-
- private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
- LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
- wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
- .le(AccountPurchasePayment::getPaymentDate, range.end())
- .orderByAsc(AccountPurchasePayment::getPaymentDate);
- return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
- }
-
- private List<MonthlyCashFlow> buildMonthlyCashFlow(DateRange range,
- List<AccountSalesCollection> collections,
- List<AccountPurchasePayment> payments) {
- Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>();
- Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>();
- YearMonth startMonth = YearMonth.from(range.start());
- YearMonth endMonth = YearMonth.from(range.end());
- for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
- incomeByMonth.put(month, BigDecimal.ZERO);
- expenseByMonth.put(month, BigDecimal.ZERO);
- }
-
- for (AccountSalesCollection row : collections) {
- if (row.getCollectionDate() == null) {
- continue;
- }
- YearMonth month = YearMonth.from(row.getCollectionDate());
- if (incomeByMonth.containsKey(month)) {
- incomeByMonth.put(month, incomeByMonth.get(month).add(defaultDecimal(row.getCollectionAmount())));
- }
- }
- for (AccountPurchasePayment row : payments) {
- if (row.getPaymentDate() == null) {
- continue;
- }
- YearMonth month = YearMonth.from(row.getPaymentDate());
- if (expenseByMonth.containsKey(month)) {
- expenseByMonth.put(month, expenseByMonth.get(month).add(defaultDecimal(row.getPaymentAmount())));
- }
- }
-
- List<MonthlyCashFlow> result = new ArrayList<>();
- for (YearMonth month : incomeByMonth.keySet()) {
- BigDecimal income = incomeByMonth.get(month);
- BigDecimal expense = expenseByMonth.getOrDefault(month, BigDecimal.ZERO);
- result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense)));
- }
- return result;
- }
-
- private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) {
- if (actual.isEmpty()) {
- List<MonthlyCashFlow> defaults = new ArrayList<>();
- YearMonth now = YearMonth.now();
- for (int i = 1; i <= forecastMonths; i++) {
- YearMonth month = now.plusMonths(i);
- defaults.add(new MonthlyCashFlow(month.toString(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
- }
- return defaults;
- }
- List<BigDecimal> series = actual.stream().map(MonthlyCashFlow::netFlow).toList();
- BigDecimal avg = series.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
- .divide(new BigDecimal(series.size()), 4, RoundingMode.HALF_UP);
- BigDecimal slope = BigDecimal.ZERO;
- if (series.size() > 1) {
- slope = series.get(series.size() - 1).subtract(series.get(0))
- .divide(new BigDecimal(series.size() - 1), 4, RoundingMode.HALF_UP);
- }
- YearMonth lastMonth = YearMonth.parse(actual.get(actual.size() - 1).month());
- List<MonthlyCashFlow> forecast = new ArrayList<>();
- for (int i = 1; i <= forecastMonths; i++) {
- YearMonth month = lastMonth.plusMonths(i);
- BigDecimal net = avg.add(slope.multiply(new BigDecimal(i))).setScale(2, RoundingMode.HALF_UP);
- BigDecimal income = net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO;
- BigDecimal expense = net.compareTo(BigDecimal.ZERO) >= 0 ? BigDecimal.ZERO : net.abs();
- forecast.add(new MonthlyCashFlow(month.toString(), income, expense, net));
- }
- return forecast;
- }
-
- private StatementSnapshot buildStatementSnapshot(LoginUser loginUser) {
- LambdaQueryWrapper<AccountStatement> wrapper = new LambdaQueryWrapper<>();
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountStatement::getDeptId);
- wrapper.orderByDesc(AccountStatement::getStatementMonth);
- List<AccountStatement> rows = defaultList(accountStatementMapper.selectList(wrapper));
- if (rows.isEmpty()) {
- return StatementSnapshot.empty();
- }
-
- Map<String, AccountStatement> latestByEntity = new HashMap<>();
- for (AccountStatement row : rows) {
- if (row.getAccountType() == null || row.getCustomerId() == null || !StringUtils.hasText(row.getStatementMonth())) {
- continue;
- }
- String key = row.getAccountType() + "::" + row.getCustomerId();
- AccountStatement existing = latestByEntity.get(key);
- if (existing == null || row.getStatementMonth().compareTo(existing.getStatementMonth()) > 0) {
- latestByEntity.put(key, row);
- }
- }
-
- BigDecimal receivableTotal = BigDecimal.ZERO;
- BigDecimal payableTotal = BigDecimal.ZERO;
- List<StatementMetric> receivableMetrics = new ArrayList<>();
- List<StatementMetric> payableMetrics = new ArrayList<>();
- for (AccountStatement row : latestByEntity.values()) {
- BigDecimal closing = defaultDecimal(row.getClosingBalance());
- if (Objects.equals(row.getAccountType(), 1)) {
- receivableTotal = receivableTotal.add(closing);
- receivableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
- defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
- } else if (Objects.equals(row.getAccountType(), 2)) {
- payableTotal = payableTotal.add(closing);
- payableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
- defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
- }
- }
- receivableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
- payableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
-
- return new StatementSnapshot(
- receivableTotal,
- payableTotal,
- receivableMetrics.stream().limit(10).toList(),
- payableMetrics.stream().limit(10).toList()
- );
- }
-
- private BigDecimal calculateTotalDepreciation(LoginUser loginUser) {
- LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
- wrapper.eq(DeviceLedger::getIsDepr, 1);
- List<DeviceLedger> devices = defaultList(deviceLedgerMapper.selectList(wrapper));
- BigDecimal total = BigDecimal.ZERO;
- for (DeviceLedger device : devices) {
- total = total.add(defaultDecimal(AccountingServiceImpl.calculatePreciseDepreciation(device)));
- }
- return total;
- }
-
- private Map<Long, BigDecimal> allocateDepreciation(List<SalesLedger> ledgers, BigDecimal totalDepreciation, BigDecimal totalRevenue) {
- if (ledgers.isEmpty() || totalDepreciation.compareTo(BigDecimal.ZERO) <= 0) {
- return Map.of();
- }
- Map<Long, BigDecimal> result = new HashMap<>();
- if (totalRevenue.compareTo(BigDecimal.ZERO) <= 0) {
- BigDecimal avg = totalDepreciation.divide(new BigDecimal(ledgers.size()), 4, RoundingMode.HALF_UP);
- for (SalesLedger ledger : ledgers) {
- result.put(ledger.getId(), avg);
- }
- return result;
- }
- for (SalesLedger ledger : ledgers) {
- BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
- BigDecimal ratio = revenue.divide(totalRevenue, 6, RoundingMode.HALF_UP);
- result.put(ledger.getId(), totalDepreciation.multiply(ratio));
- }
- return result;
- }
-
- private BigDecimal fallbackMaterialCost(List<SalesLedgerProduct> products, BigDecimal revenue) {
- if (products != null && !products.isEmpty()) {
- BigDecimal productAmount = products.stream()
- .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
- .filter(Objects::nonNull)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- if (productAmount.compareTo(BigDecimal.ZERO) > 0) {
- return productAmount;
- }
- }
- return revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
- }
-
- private Map<String, String> queryCustomerNameMap(Set<String> idSet) {
- if (idSet == null || idSet.isEmpty()) {
- return Map.of();
- }
- Set<Long> ids = idSet.stream()
- .map(this::toLongOrNull)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- if (ids.isEmpty()) {
- return Map.of();
- }
- LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
- wrapper.in(Customer::getId, ids);
- return defaultList(customerMapper.selectList(wrapper)).stream()
- .collect(Collectors.toMap(item -> String.valueOf(item.getId()), Customer::getCustomerName, (a, b) -> a));
- }
-
- private Map<String, String> querySupplierNameMap(Set<String> idSet) {
- if (idSet == null || idSet.isEmpty()) {
- return Map.of();
- }
- Set<Long> ids = idSet.stream()
- .map(this::toLongOrNull)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
- if (ids.isEmpty()) {
- return Map.of();
- }
- LambdaQueryWrapper<SupplierManage> wrapper = new LambdaQueryWrapper<>();
- wrapper.in(SupplierManage::getId, ids);
- return defaultList(supplierManageMapper.selectList(wrapper)).stream()
- .collect(Collectors.toMap(item -> String.valueOf(item.getId()), SupplierManage::getSupplierName, (a, b) -> a));
- }
-
- private String riskLevelByAmount(BigDecimal amount) {
- if (amount.compareTo(new BigDecimal("5000000")) >= 0) {
- return "high";
- }
- if (amount.compareTo(new BigDecimal("1000000")) >= 0) {
- return "medium";
- }
- return "low";
- }
-
- private long countDevices(LoginUser loginUser) {
- LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
- return deviceLedgerMapper.selectCount(wrapper);
- }
-
- private long countRepairingDevices(LoginUser loginUser) {
- LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
- applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
- applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
- wrapper.in(DeviceRepair::getStatus, List.of(0, 3));
- return deviceRepairMapper.selectCount(wrapper);
- }
-
- private BigDecimal estimateInventoryValue(LoginUser loginUser, List<StockInventory> inventories) {
- if (inventories == null || inventories.isEmpty()) {
- return BigDecimal.ZERO;
- }
- Set<Long> modelIds = inventories.stream().map(StockInventory::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
- Map<Long, BigDecimal> costMap = queryAverageUnitCostByModel(loginUser, modelIds);
- BigDecimal total = BigDecimal.ZERO;
- for (StockInventory inventory : inventories) {
- BigDecimal qty = maxZero(defaultDecimal(inventory.getQualitity()).subtract(defaultDecimal(inventory.getLockedQuantity())));
- BigDecimal unit = costMap.getOrDefault(inventory.getProductModelId(), BigDecimal.ZERO);
- total = total.add(qty.multiply(unit));
- }
- return total;
- }
-
- private DateRange previousSameLengthRange(DateRange range) {
- long days = daysBetween(range.start(), range.end()) + 1L;
- LocalDate prevEnd = range.start().minusDays(1);
- LocalDate prevStart = prevEnd.minusDays(days - 1L);
- return new DateRange(prevStart, prevEnd, prevStart + "鑷�" + prevEnd);
- }
-
- private List<String> buildProfitReasons(BigDecimal revenue,
- BigDecimal materialCost,
- BigDecimal laborCost,
- BigDecimal scrapCost,
- BigDecimal profit,
- BigDecimal profitRate) {
- List<String> reasons = new ArrayList<>();
- BigDecimal materialRate = rate(materialCost, revenue);
- if (materialRate.compareTo(new BigDecimal("0.70")) >= 0) {
- reasons.add("鏉愭枡鎴愭湰鍗犳瘮瓒呰繃70%");
- } else if (materialRate.compareTo(new BigDecimal("0.55")) >= 0) {
- reasons.add("鏉愭枡鎴愭湰鍗犳瘮鍋忛珮");
- }
- BigDecimal laborRate = rate(laborCost, revenue);
- if (laborRate.compareTo(new BigDecimal("0.20")) >= 0) {
- reasons.add("浜哄伐鎴愭湰鍗犳瘮瓒呰繃20%");
- } else if (laborRate.compareTo(new BigDecimal("0.12")) >= 0) {
- reasons.add("浜哄伐鎴愭湰澧為暱鍋忓揩");
- }
- BigDecimal scrapRate = rate(scrapCost, revenue);
- if (scrapRate.compareTo(new BigDecimal("0.05")) >= 0) {
- reasons.add("鎶ュ簾鎹熻�楀崰姣斿亸楂�");
- }
- if (profit.compareTo(BigDecimal.ZERO) < 0) {
- reasons.add("璁㈠崟澶勪簬浜忔崯鐘舵��");
- } else if (profitRate.compareTo(new BigDecimal("0.08")) < 0) {
- reasons.add("鍒╂鼎鐜囦綆浜�8%");
- }
- if (reasons.isEmpty()) {
- reasons.add("鎴愭湰缁撴瀯澶勪簬鍚堢悊鍖洪棿");
- }
- return reasons;
- }
-
- private String buildProfitSuggestion(String riskLevel, List<String> reasons) {
- if ("high".equals(riskLevel)) {
- return "浼樺厛澶嶆牳BOM鐢ㄩ噺涓庡伐搴忓畾棰濓紝蹇呰鏃惰皟鏁存姤浠峰拰浠樻鏉℃锛屽苟闄愬埗瓒呰处鏈熶氦浠樸��";
- }
- if ("medium".equals(riskLevel)) {
- return "寤鸿浼樺寲閲囪喘鎵规鍜屽伐搴忔帓浜э紝鎻愬崌涓�娆″悎鏍肩巼骞跺悓姝ユ墽琛屾瘺鍒╅璀︺��";
- }
- if (reasons.stream().anyMatch(item -> item.contains("鏉愭枡"))) {
- return "淇濇寔鏉愭枡閲囪喘鎴愭湰鐪嬫澘锛屾寜鍛ㄨ窡韪富瑕佹潗鏂欏崟浠锋尝鍔ㄣ��";
- }
- return "缁存寔褰撳墠缁忚惀鑺傚锛岀户缁窡韪鍗曞埄娑︾巼鍜屽洖娆炬晥鐜囥��";
- }
-
- private Map<String, Object> toOrderCostItem(OrderProfitMetric metric) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("ledgerId", metric.ledgerId());
- item.put("salesContractNo", metric.salesContractNo());
- item.put("customerName", metric.customerName());
- item.put("projectName", metric.projectName());
- item.put("entryDate", formatDate(metric.entryDate()));
- item.put("deliveryDate", formatDate(metric.deliveryDate()));
- item.put("revenue", metric.revenue());
- item.put("materialCost", metric.materialCost());
- item.put("laborCost", metric.laborCost());
- item.put("depreciationCost", metric.depreciationCost());
- item.put("scrapCost", metric.scrapCost());
- item.put("totalCost", metric.totalCost());
- item.put("profit", metric.profit());
- item.put("profitRate", toPercent(metric.profitRate()));
- item.put("riskLevel", metric.riskLevel());
- item.put("reasons", metric.reasons());
- item.put("suggestion", metric.suggestion());
- return item;
- }
-
- private Map<String, Object> toRiskOrderItem(OrderProfitMetric metric) {
- Map<String, Object> map = toOrderCostItem(metric);
- map.put("priority", "high".equals(metric.riskLevel()) ? "high" : ("medium".equals(metric.riskLevel()) ? "medium" : "low"));
- return map;
- }
-
- private List<Map<String, Object>> buildCustomerProfitTop(List<OrderProfitMetric> metrics, int topN) {
- Map<String, BigDecimal> customerProfitMap = new HashMap<>();
- Map<String, BigDecimal> customerRevenueMap = new HashMap<>();
- for (OrderProfitMetric metric : metrics) {
- customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
- customerRevenueMap.merge(metric.customerName(), metric.revenue(), BigDecimal::add);
- }
- return customerProfitMap.entrySet().stream()
- .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
- .limit(topN)
- .map(entry -> {
- Map<String, Object> map = new LinkedHashMap<>();
- BigDecimal revenue = customerRevenueMap.getOrDefault(entry.getKey(), BigDecimal.ZERO);
- map.put("customerName", entry.getKey());
- map.put("profit", entry.getValue());
- map.put("revenue", revenue);
- map.put("profitRate", toPercent(rate(entry.getValue(), revenue)));
- return map;
- })
- .toList();
- }
-
- private Map<String, Object> toInventoryItem(InventoryMetric metric) {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("productModelId", metric.modelId());
- map.put("productName", metric.productName());
- map.put("model", metric.modelName());
- map.put("quantity", metric.quantity());
- map.put("lockedQuantity", metric.lockedQuantity());
- map.put("avgUnitCost", metric.avgUnitCost());
- map.put("inventoryValue", metric.inventoryValue());
- map.put("outboundQuantity", metric.outboundQuantity());
- map.put("stagnantDays", metric.stagnantDays());
- map.put("overstock", metric.overstock());
- map.put("riskLevel", metric.stagnantDays() >= 90 ? "high" : (metric.stagnantDays() >= 30 ? "medium" : "low"));
- return map;
- }
-
- private boolean matchInventoryKeyword(InventoryMetric metric, String keyword) {
- if (!StringUtils.hasText(keyword)) {
- return true;
- }
- return metric.productName().contains(keyword.trim()) || metric.modelName().contains(keyword.trim());
- }
-
- private Map<String, Object> toMonthlyCashFlowItem(MonthlyCashFlow flow) {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("month", flow.month());
- map.put("income", flow.income());
- map.put("expense", flow.expense());
- map.put("netFlow", flow.netFlow());
- return map;
- }
-
- private Map<String, Object> toStatementRiskItem(StatementMetric metric, Map<String, String> nameMap, String type) {
- BigDecimal actualRate = rate(metric.actualAmount(), metric.planAmount());
- Map<String, Object> map = new LinkedHashMap<>();
- map.put(type + "Id", metric.entityId());
- map.put(type + "Name", safe(nameMap.get(metric.entityId())));
- map.put("statementMonth", metric.statementMonth());
- map.put("closingBalance", metric.closingBalance());
- map.put("planAmount", metric.planAmount());
- map.put("actualAmount", metric.actualAmount());
- map.put("actualRate", toPercent(actualRate));
- map.put("riskLevel", metric.closingBalance().compareTo(new BigDecimal("1000000")) > 0 || actualRate.compareTo(new BigDecimal("0.50")) < 0 ? "high" : "medium");
- return map;
- }
-
- private Map<String, Object> anomalyItem(String level, String type, String message, Map<String, Object> detail) {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("riskLevel", level);
- map.put("type", type);
- map.put("message", message);
- map.put("detail", detail);
- return map;
- }
-
- private Map<String, Object> riskSuggestion(String type, String level, String suggestion) {
- Map<String, Object> map = new LinkedHashMap<>();
- map.put("type", type);
- map.put("level", level);
- map.put("suggestion", suggestion);
- return map;
- }
-
- private Map<String, Object> buildCostCompositionPie(BigDecimal material, BigDecimal labor, BigDecimal depreciation, BigDecimal scrap) {
- List<Map<String, Object>> data = List.of(
- Map.of("name", "鏉愭枡鎴愭湰", "value", material),
- Map.of("name", "浜哄伐鎴愭湰", "value", labor),
- Map.of("name", "鎶樻棫鎴愭湰", "value", depreciation),
- Map.of("name", "鎹熻�楁垚鏈�", "value", scrap)
- );
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "鎴愭湰鏋勬垚", "left", "center"));
- option.put("tooltip", Map.of("trigger", "item"));
- option.put("series", List.of(Map.of("name", "鎴愭湰鏋勬垚", "type", "pie", "radius", "60%", "data", data)));
- return option;
- }
-
- private Map<String, Object> buildOrderProfitBar(List<OrderProfitMetric> metrics) {
- List<OrderProfitMetric> top = metrics.stream()
- .sorted(Comparator.comparing(OrderProfitMetric::profit))
- .limit(10)
- .toList();
- List<String> xData = top.stream().map(OrderProfitMetric::salesContractNo).toList();
- List<BigDecimal> yData = top.stream().map(OrderProfitMetric::profit).toList();
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "璁㈠崟鍒╂鼎鍒嗗竷", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", xData));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "鍒╂鼎", "type", "bar", "data", yData)));
- return option;
- }
-
- private Map<String, Object> buildProcessCostBar(Map<String, BigDecimal> processCosts) {
- List<String> xData = new ArrayList<>(processCosts.keySet());
- List<BigDecimal> yData = new ArrayList<>(processCosts.values());
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "宸ュ簭鎴愭湰鎺掑悕", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", xData));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "鎴愭湰", "type", "bar", "data", yData)));
- return option;
- }
-
- private Map<String, Object> buildProfitDistributionBar(List<OrderProfitMetric> metrics) {
- List<OrderProfitMetric> sorted = metrics.stream()
- .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
- .limit(15)
- .toList();
- List<String> xData = sorted.stream().map(OrderProfitMetric::salesContractNo).toList();
- List<BigDecimal> yData = sorted.stream().map(metric -> metric.profitRate().multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP)).toList();
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "璁㈠崟鍒╂鼎鐜囧垎甯�", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", xData));
- option.put("yAxis", Map.of("type", "value", "name", "%"));
- option.put("series", List.of(Map.of("name", "鍒╂鼎鐜�", "type", "bar", "data", yData)));
- return option;
- }
-
- private Map<String, Object> buildLossOrderTrendLine(List<OrderProfitMetric> metrics) {
- Map<String, Long> lossByDate = new LinkedHashMap<>();
- List<OrderProfitMetric> sorted = metrics.stream()
- .filter(metric -> metric.entryDate() != null)
- .sorted(Comparator.comparing(OrderProfitMetric::entryDate))
- .toList();
- for (OrderProfitMetric metric : sorted) {
- String day = formatDate(metric.entryDate());
- long inc = metric.profit().compareTo(BigDecimal.ZERO) < 0 ? 1L : 0L;
- lossByDate.merge(day, inc, Long::sum);
- }
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "浜忔崯璁㈠崟瓒嬪娍", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(lossByDate.keySet())));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "浜忔崯璁㈠崟鏁�", "type", "line", "smooth", true, "data", new ArrayList<>(lossByDate.values()))));
- return option;
- }
-
- private Map<String, Object> buildCustomerProfitBar(Map<String, BigDecimal> customerProfitMap) {
- List<Map.Entry<String, BigDecimal>> top = customerProfitMap.entrySet().stream()
- .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
- .limit(10)
- .toList();
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "瀹㈡埛鍒╂鼎璐$尞TOP10", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", top.stream().map(Map.Entry::getKey).toList()));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "鍒╂鼎", "type", "bar", "data", top.stream().map(Map.Entry::getValue).toList())));
- return option;
- }
-
- private Map<String, Object> buildInventoryTopBar(List<InventoryMetric> metrics) {
- List<InventoryMetric> top = metrics.stream()
- .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
- .limit(10)
- .toList();
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "搴撳瓨璧勯噾鍗犵敤TOP10", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", top.stream().map(item -> item.productName() + "/" + item.modelName()).toList()));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "璧勯噾鍗犵敤", "type", "bar", "data", top.stream().map(InventoryMetric::inventoryValue).toList())));
- return option;
- }
-
- private Map<String, Object> buildInventoryAgingPie(List<InventoryMetric> metrics) {
- long normal = metrics.stream().filter(item -> item.stagnantDays() < 30).count();
- long slow = metrics.stream().filter(item -> item.stagnantDays() >= 30 && item.stagnantDays() < 90).count();
- long stagnant = metrics.stream().filter(item -> item.stagnantDays() >= 90).count();
- List<Map<String, Object>> data = List.of(
- Map.of("name", "姝e父", "value", normal),
- Map.of("name", "缂撴參", "value", slow),
- Map.of("name", "鍛嗘粸", "value", stagnant)
- );
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "搴撳瓨搴撻緞鍒嗗竷", "left", "center"));
- option.put("tooltip", Map.of("trigger", "item"));
- option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", data)));
- return option;
- }
-
- private Map<String, Object> buildTurnoverGauge(BigDecimal turnoverDays) {
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "搴撳瓨鍛ㄨ浆澶╂暟", "left", "center"));
- option.put("series", List.of(Map.of(
- "type", "gauge",
- "min", 0,
- "max", 180,
- "detail", Map.of("formatter", "{value}澶�"),
- "data", List.of(Map.of("value", turnoverDays, "name", "鍛ㄨ浆澶╂暟"))
- )));
- return option;
- }
-
- private Map<String, Object> buildCashflowTrend(List<MonthlyCashFlow> actual, List<MonthlyCashFlow> forecast) {
- List<String> labels = new ArrayList<>();
- List<BigDecimal> netActual = new ArrayList<>();
- List<BigDecimal> netForecast = new ArrayList<>();
- for (MonthlyCashFlow point : actual) {
- labels.add(point.month());
- netActual.add(point.netFlow());
- netForecast.add(null);
- }
- for (MonthlyCashFlow point : forecast) {
- labels.add(point.month());
- netActual.add(null);
- netForecast.add(point.netFlow());
- }
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "鐜伴噾娴佽秼鍔匡紙瀹為檯+棰勬祴锛�", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", labels));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(
- Map.of("name", "瀹為檯鍑�鐜伴噾娴�", "type", "line", "smooth", true, "data", netActual),
- Map.of("name", "棰勬祴鍑�鐜伴噾娴�", "type", "line", "smooth", true, "data", netForecast)
- ));
- return option;
- }
-
- private Map<String, Object> buildReceivablePayableBar(BigDecimal receivable, BigDecimal payable) {
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "搴旀敹搴斾粯浣欓瀵规瘮", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", List.of("搴旀敹", "搴斾粯")));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "浣欓", "type", "bar", "data", List.of(receivable, payable))));
- return option;
- }
-
- private Map<String, Object> buildFundGapGauge(BigDecimal fundGap) {
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "璧勯噾缂哄彛", "left", "center"));
- option.put("series", List.of(Map.of(
- "type", "gauge",
- "min", 0,
- "max", 10000000,
- "detail", Map.of("formatter", "{value}"),
- "data", List.of(Map.of("value", fundGap, "name", "璧勯噾缂哄彛"))
- )));
- return option;
- }
-
- private Map<String, Object> buildAnomalyLevelPie(List<Map<String, Object>> anomalies) {
- long high = anomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count();
- long medium = anomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count();
- long low = anomalies.stream().filter(item -> "low".equals(item.get("riskLevel"))).count();
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "寮傚父绛夌骇鍒嗗竷", "left", "center"));
- option.put("tooltip", Map.of("trigger", "item"));
- option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", List.of(
- Map.of("name", "楂橀闄�", "value", high),
- Map.of("name", "涓闄�", "value", medium),
- Map.of("name", "浣庨闄�", "value", low)
- ))));
- return option;
- }
-
- private Map<String, Object> buildAnomalyTypeBar(List<Map<String, Object>> anomalies) {
- Map<String, Long> countByType = new LinkedHashMap<>();
- for (Map<String, Object> anomaly : anomalies) {
- countByType.merge(String.valueOf(anomaly.get("type")), 1L, Long::sum);
- }
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "寮傚父绫诲瀷鍒嗗竷", "left", "center"));
- option.put("tooltip", Map.of("trigger", "axis"));
- option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(countByType.keySet())));
- option.put("yAxis", Map.of("type", "value"));
- option.put("series", List.of(Map.of("name", "寮傚父鏁�", "type", "bar", "data", new ArrayList<>(countByType.values()))));
- return option;
- }
-
- private Map<String, Object> buildInventoryProfitGauge(BigDecimal inventoryValue, BigDecimal profit) {
- BigDecimal ratio = inventoryValue.compareTo(BigDecimal.ZERO) <= 0
- ? BigDecimal.ZERO
- : profit.divide(inventoryValue, 4, RoundingMode.HALF_UP).multiply(ONE_HUNDRED);
- Map<String, Object> option = new LinkedHashMap<>();
- option.put("title", Map.of("text", "鍒╂鼎/搴撳瓨璧勯噾姣�", "left", "center"));
- option.put("series", List.of(Map.of(
- "type", "gauge",
- "min", -100,
- "max", 100,
- "detail", Map.of("formatter", "{value}%"),
- "data", List.of(Map.of("value", ratio.setScale(2, RoundingMode.HALF_UP), "name", "鍒╂鼎璧勯噾姣�"))
- )));
- return option;
- }
-
- private int normalizeLimit(Integer limit) {
- if (limit == null || limit <= 0) {
- return DEFAULT_LIMIT;
- }
- return Math.min(limit, MAX_LIMIT);
- }
-
- private DateRange resolveDateRange(String startDate, String endDate, String timeRange, String defaultLabel) {
- LocalDate today = LocalDate.now();
- LocalDate explicitStart = parseLocalDate(startDate);
- LocalDate explicitEnd = parseLocalDate(endDate);
- 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)) {
- if ("浠婂ぉ".equals(defaultLabel)) {
- return new DateRange(today, today, "浠婂ぉ");
- }
- if ("鏈懆".equals(defaultLabel)) {
- LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
- return new DateRange(start, today, "鏈懆");
- }
- if ("鏈湀".equals(defaultLabel)) {
- return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
- }
- if ("杩�90澶�".equals(defaultLabel)) {
- return new DateRange(today.minusDays(89), today, "杩�90澶�");
- }
- return new DateRange(today.minusDays(29), today, defaultLabel);
- }
-
- 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("鏈湀")) {
- return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
- }
- if (text.contains("涓婃湀")) {
- YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
- return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "涓婃湀");
- }
- if (text.contains("浠婂勾") || text.contains("鏈勾")) {
- return new DateRange(today.withDayOfYear(1), today, "浠婂勾");
- }
- Matcher relativeMatcher = RELATIVE_PATTERN.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(29);
- };
- return new DateRange(start, today, "杩�" + amount + unit);
- }
- Matcher dateMatcher = DATE_PATTERN.matcher(text);
- if (dateMatcher.find()) {
- LocalDate start = parseLocalDate(dateMatcher.group(1));
- LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start;
- if (start != null && end != null) {
- if (start.isAfter(end)) {
- LocalDate temp = start;
- start = end;
- end = temp;
- }
- return new DateRange(start, end, start + "鑷�" + end);
- }
- }
- return new DateRange(today.minusDays(29), today, "杩�30澶�");
- }
-
- private LocalDate parseLocalDate(String text) {
- if (!StringUtils.hasText(text)) {
- return null;
- }
- try {
- return LocalDate.parse(text.trim(), DATE_FMT);
- } catch (Exception ignored) {
- return null;
- }
- }
-
- private Date toDate(LocalDate localDate) {
- return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
- }
-
- private Date toExclusiveEndDate(LocalDate localDate) {
- return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
- }
-
- private LocalDate toLocalDate(Date date) {
- if (date == null) {
- return null;
- }
- return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
- }
-
- private String formatDate(LocalDate date) {
- return date == null ? "" : date.format(DATE_FMT);
- }
-
- private long daysBetween(LocalDate start, LocalDate end) {
- if (start == null || end == null || start.isAfter(end)) {
- return 0;
- }
- return end.toEpochDay() - start.toEpochDay();
- }
-
- private BigDecimal defaultDecimal(BigDecimal value) {
- return value == null ? BigDecimal.ZERO : value;
- }
-
- private BigDecimal maxZero(BigDecimal value) {
- return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
- }
-
- private BigDecimal rate(BigDecimal numerator, BigDecimal denominator) {
- if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
- return BigDecimal.ZERO;
- }
- return defaultDecimal(numerator).divide(denominator, 6, RoundingMode.HALF_UP);
- }
-
- private String toPercent(BigDecimal decimal) {
- if (decimal == null) {
- return "0.00%";
- }
- BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP);
- return rate.toPlainString() + "%";
- }
-
- private BigDecimal avgRate(List<OrderProfitMetric> metrics) {
- if (metrics == null || metrics.isEmpty()) {
- return BigDecimal.ZERO;
- }
- BigDecimal sum = metrics.stream().map(OrderProfitMetric::profitRate).reduce(BigDecimal.ZERO, BigDecimal::add);
- return sum.divide(new BigDecimal(metrics.size()), 6, RoundingMode.HALF_UP);
- }
-
- private BigDecimal estimateLaborCost(ProductionAccount account, Map<String, BigDecimal> salaryQuotaByOperation) {
- BigDecimal salaryQuota = salaryQuotaByOperation.getOrDefault(safe(account.getTechnologyOperationName()), BigDecimal.ZERO);
- BigDecimal finishedNum = defaultDecimal(account.getFinishedNum());
- BigDecimal workHours = defaultDecimal(account.getWorkHours());
- if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && finishedNum.compareTo(BigDecimal.ZERO) > 0) {
- return finishedNum.multiply(salaryQuota);
- }
- if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && workHours.compareTo(BigDecimal.ZERO) > 0) {
- return workHours.multiply(salaryQuota);
- }
- if (workHours.compareTo(BigDecimal.ZERO) > 0) {
- return workHours;
- }
- return finishedNum;
- }
-
- private List<Long> parseIdList(String raw) {
- if (!StringUtils.hasText(raw)) {
- return List.of();
- }
- String text = raw.replace("[", "").replace("]", "").replace(" ", "");
- if (!StringUtils.hasText(text)) {
- return List.of();
- }
- List<Long> result = new ArrayList<>();
- for (String part : text.split(",")) {
- if (!StringUtils.hasText(part)) {
- continue;
- }
- try {
- result.add(Long.parseLong(part.trim()));
- } catch (Exception ignored) {
- }
- }
- return result;
- }
-
- private int keywordHitCount(List<String> keywords, String question) {
- if (!StringUtils.hasText(question) || keywords == null) {
- return 0;
- }
- int count = 0;
- for (String keyword : keywords) {
- if (question.contains(keyword)) {
- count++;
- }
- }
- return count;
- }
-
- private String normalizeForMatch(String text) {
- if (!StringUtils.hasText(text)) {
- return "";
- }
- return text.replace("锛�", "")
- .replace(",", "")
- .replace("銆�", "")
- .replace(".", "")
- .replace("锛�", "")
- .replace("!", "")
- .replace("锛�", "")
- .replace("?", "")
- .replace("锛�", "")
- .replace(":", "")
- .replace("锛�", "")
- .replace(";", "")
- .replace(" ", "")
- .trim();
- }
-
- private String safe(Object value) {
- return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
- }
-
- private LoginUser currentLoginUser(String memoryId) {
- LoginUser loginUser = aiSessionUserContext.get(memoryId);
- if (loginUser != null) {
- return loginUser;
- }
- return SecurityUtils.getLoginUser();
- }
-
- private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
- Map<String, Object> summary = new LinkedHashMap<>();
- summary.put("timeRange", range.label());
- summary.put("startDate", range.start().toString());
- summary.put("endDate", range.end().toString());
- summary.put("count", count);
- summary.put("keyword", safe(keyword));
- return summary;
- }
-
- private Long toLongOrNull(String value) {
- if (!StringUtils.hasText(value)) {
- return null;
- }
- try {
- return Long.valueOf(value.trim());
- } catch (Exception ignored) {
- return null;
- }
- }
-
- private <T> List<T> defaultList(List<T> list) {
- return list == null ? List.of() : list;
- }
-
- private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
- if (tenantId != null) {
- wrapper.eq(field, tenantId);
- }
- }
-
- private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
- if (deptId != null) {
- wrapper.eq(field, deptId);
- }
- }
-
- private List<KnowledgeDoc> financeKnowledgeBase() {
- return List.of(
- new KnowledgeDoc(
- "鍒╂鼎涓嬮檷鍒嗘瀽妗嗘灦",
- List.of("鍒╂鼎涓嬮檷", "浜忔崯璁㈠崟", "姣涘埄鐜�", "鍑�鍒╃巼"),
- "鍏堢湅鏀跺叆绔紙璁㈠崟缁撴瀯銆佸崟浠枫�佷氦浠樺欢杩燂級锛屽啀鐪嬫垚鏈锛堟潗鏂欍�佷汉宸ャ�佹姌鏃с�佹崯鑰楋級锛屾渶鍚庣湅鐜伴噾绔紙鍥炴銆佽处鏈熴�佸潖璐﹂闄╋級銆�",
- List.of("sales_ledger", "sales_ledger_product", "production_account", "device_ledger", "account_statement"),
- List.of("涓轰粈涔堟湰鏈堝埄娑︿笅闄嶏紵", "鍝簺璁㈠崟浜忔崯鏈�涓ラ噸锛�", "鎴愭湰涓婂崌鏉ヨ嚜鍝釜宸ュ簭锛�")
- ),
- new KnowledgeDoc(
- "搴撳瓨璧勯噾鍗犵敤璇婃柇",
- List.of("搴撳瓨绉帇", "鍛嗘粸搴撳瓨", "鍛ㄨ浆鐜�", "璧勯噾鍗犵敤"),
- "搴撳瓨璧勯噾璇婃柇閲嶇偣鐪嬶細搴撳瓨浠峰�笺�佽繎30澶╁嚭搴撴垚鏈�佸憜婊炲ぉ鏁般�佽秴鍌ㄦ瘮渚嬶紝褰㈡垚鍘诲簱瀛樹笌閲囪喘鑺傚鑱斿姩绛栫暐銆�",
- List.of("stock_inventory", "procurement_record_storage", "procurement_record_out"),
- List.of("鍝簺鐗╂枡璧勯噾鍗犵敤鏈�楂橈紵", "鍝簺搴撳瓨瓒呰繃90澶╂湭鍛ㄨ浆锛�", "搴撳瓨鍛ㄨ浆澶╂暟鏄惁寮傚父锛�")
- ),
- new KnowledgeDoc(
- "鐜伴噾娴佷笌璐︽椋庨櫓",
- List.of("鐜伴噾娴�", "搴旀敹", "搴斾粯", "鍥炴", "璧勯噾缂哄彛"),
- "鐜伴噾娴佸垽鏂缁撳悎鏀舵銆佷粯娆俱�佸簲鏀跺簲浠樹綑棰濅笌棰勬祴鍑�娴侀噺锛岄噸鐐瑰叧娉ㄩ珮浣欓瀹㈡埛鍜岄珮闆嗕腑浠樻渚涘簲鍟嗐��",
- List.of("account_sales_collection", "account_purchase_payment", "account_statement"),
- List.of("鏈潵涓変釜鏈堟槸鍚︽湁璧勯噾缂哄彛锛�", "鍝釜瀹㈡埛鍥炴椋庨櫓鏈�楂橈紵", "浠樻鍘嬪姏鏈�澶х殑鏄摢浜涗緵搴斿晢锛�")
- ),
- new KnowledgeDoc(
- "涓氳储涓�浣撳寲鍙e緞",
- List.of("涓氳储铻嶅悎", "涓氳储鑱斿姩", "鍙e緞", "椹鹃┒鑸�"),
- "璁㈠崟鍒╂鼎鍙e緞=閿�鍞敹鍏�-鏉愭枡鎴愭湰-浜哄伐鎴愭湰-璁惧鎶樻棫-鎹熻�楁垚鏈紱缁忚惀椹鹃┒鑸辫仈鍔ㄨ鍗曘�佺敓浜с�佸簱瀛樸�佽澶囥�佽处娆炬暟鎹��",
- List.of("sales_ledger", "production_operation_task", "production_product_main", "device_ledger", "stock_inventory", "account_statement"),
- List.of("璁㈠崟鍒╂鼎鐜囧浣曡绠楋紵", "缁忚惀椹鹃┒鑸辨牳蹇冩寚鏍囨湁鍝簺锛�")
- )
- );
- }
-
- 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);
- result.put("description", description);
- result.put("summary", summary == null ? Map.of() : summary);
- result.put("data", data == null ? Map.of() : data);
- result.put("charts", charts == null ? Map.of() : charts);
- return JSON.toJSONString(result);
- }
-
- private record DateRange(LocalDate start, LocalDate end, String label) {
- }
-
- private record OrderProfitMetric(Long ledgerId,
- String salesContractNo,
- String customerName,
- String projectName,
- LocalDate entryDate,
- LocalDate deliveryDate,
- BigDecimal revenue,
- BigDecimal materialCost,
- BigDecimal laborCost,
- BigDecimal depreciationCost,
- BigDecimal scrapCost,
- BigDecimal totalCost,
- BigDecimal profit,
- BigDecimal profitRate,
- String riskLevel,
- List<String> reasons,
- String suggestion) {
- }
-
- private record AnalysisBundle(List<OrderProfitMetric> orderMetrics,
- Map<String, BigDecimal> processCostRanking,
- BigDecimal totalRevenue,
- BigDecimal totalMaterialCost,
- BigDecimal totalLaborCost,
- BigDecimal totalDepreciationCost,
- BigDecimal totalScrapCost,
- BigDecimal totalCost,
- BigDecimal totalProfit) {
- private static AnalysisBundle empty() {
- return new AnalysisBundle(List.of(), Map.of(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
- BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
- }
- }
-
- private record MaterialCostResult(Map<Long, BigDecimal> materialCostByLedgerId,
- Map<Long, BigDecimal> avgUnitCostByModelId) {
- }
-
- private record ProductionCostContext(Map<Long, BigDecimal> laborCostByLedgerId,
- Map<Long, BigDecimal> scrapCostByLedgerId,
- Map<String, BigDecimal> processCostRanking) {
- private static ProductionCostContext empty() {
- return new ProductionCostContext(Map.of(), Map.of(), Map.of());
- }
- }
-
- private record InventoryMetric(Long modelId,
- String productName,
- String modelName,
- BigDecimal quantity,
- BigDecimal lockedQuantity,
- BigDecimal avgUnitCost,
- BigDecimal inventoryValue,
- BigDecimal outboundQuantity,
- long stagnantDays,
- boolean overstock) {
- }
-
- private static class InventoryMetricBuilder {
- private final Long modelId;
- private BigDecimal quantity = BigDecimal.ZERO;
- private BigDecimal lockedQuantity = BigDecimal.ZERO;
- private BigDecimal warnNum = BigDecimal.ZERO;
- private LocalDateTime firstInTime;
-
- private InventoryMetricBuilder(Long modelId) {
- this.modelId = modelId;
- }
-
- private void addQuantity(BigDecimal quantity) {
- this.quantity = this.quantity.add(quantity);
- }
-
- private void addLockedQuantity(BigDecimal lockedQuantity) {
- this.lockedQuantity = this.lockedQuantity.add(lockedQuantity);
- }
-
- private void addWarnNum(BigDecimal warnNum) {
- this.warnNum = this.warnNum.max(warnNum);
- }
-
- private void updateFirstInTime(LocalDateTime createTime) {
- if (this.firstInTime == null || createTime.isBefore(this.firstInTime)) {
- this.firstInTime = createTime;
- }
- }
-
- private Long modelId() {
- return modelId;
- }
-
- private BigDecimal quantity() {
- return quantity;
- }
-
- private BigDecimal lockedQuantity() {
- return lockedQuantity;
- }
-
- private BigDecimal warnNum() {
- return warnNum;
- }
-
- private LocalDateTime firstInTime() {
- return firstInTime;
- }
- }
-
- private record OutboundStats(Map<Long, BigDecimal> outboundQtyByModel,
- Map<Long, LocalDateTime> lastOutboundTimeByModel,
- BigDecimal totalOutboundCost) {
- private static OutboundStats empty() {
- return new OutboundStats(Map.of(), Map.of(), BigDecimal.ZERO);
- }
- }
-
- private record MonthlyCashFlow(String month, BigDecimal income, BigDecimal expense, BigDecimal netFlow) {
- }
-
- private record StatementMetric(String entityId,
- BigDecimal closingBalance,
- BigDecimal planAmount,
- BigDecimal actualAmount,
- String statementMonth) {
- }
-
- private record StatementSnapshot(BigDecimal receivableTotal,
- BigDecimal payableTotal,
- List<StatementMetric> receivableTop,
- List<StatementMetric> payableTop) {
- private static StatementSnapshot empty() {
- return new StatementSnapshot(BigDecimal.ZERO, BigDecimal.ZERO, List.of(), List.of());
- }
- }
-
- private record KnowledgeDoc(String topic,
- List<String> keywords,
- String knowledge,
- List<String> relatedTables,
- List<String> suggestedQuestions) {
- }
-}
diff --git a/src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java b/src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java
index 6d69b7b..81066d6 100644
--- a/src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java
+++ b/src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java
@@ -3,7 +3,7 @@
import com.ruoyi.basic.pojo.CustomerReturnVisit;
import com.ruoyi.basic.service.CustomerReturnVisitService;
import com.ruoyi.framework.redis.RedisCache;
-import com.ruoyi.project.system.service.SysUserClientService;
+
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
@@ -31,7 +31,7 @@
private final CustomerReturnVisitService customerReturnVisitService;
- private final SysUserClientService userClientService;
+
@SuppressWarnings("unchecked")
@Scheduled(fixedDelay = 60000)
diff --git a/src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java b/src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java
deleted file mode 100644
index fe1b2f6..0000000
--- a/src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.ruoyi.project.system.controller;
-
-import com.ruoyi.framework.web.controller.BaseController;
-import com.ruoyi.framework.web.domain.AjaxResult;
-import com.ruoyi.project.system.domain.SysUserClient;
-import com.ruoyi.project.system.service.SysUserClientService;
-import com.ruoyi.common.utils.SecurityUtils;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
-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.RestController;
-
-/**
- * 鐢ㄦ埛瀹夊崜璁惧绠$悊鎺у埗灞�
- *
- * @author deslrey
- * @version 1.0
- * @since 2026/2/9
- */
-@Tag(name = "鐢ㄦ埛璁惧缁戝畾")
-@RestController
-@RequestMapping("/system/client")
-@AllArgsConstructor
-public class SysUserClientController extends BaseController {
-
- private SysUserClientService sysUserClientService;
-
- /**
- * 娣诲姞/鏇存柊鐢ㄦ埛cid
- */
- @PostMapping("/addOrUpdateClientId")
- @Operation(summary = "娣诲姞/鏇存柊鐢ㄦ埛cid")
- public AjaxResult addOrUpdateClientId(@RequestBody SysUserClient sysUserClient) {
- Long userId = SecurityUtils.getUserId();
- sysUserClient.setUserId(userId);
- boolean result = sysUserClientService.addOrUpdateClientId(sysUserClient);
- return result ? success() : error("璁惧缁戝畾澶辫触");
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/project/system/domain/SysUserClient.java b/src/main/java/com/ruoyi/project/system/domain/SysUserClient.java
deleted file mode 100644
index 24dc336..0000000
--- a/src/main/java/com/ruoyi/project/system/domain/SysUserClient.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.ruoyi.project.system.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.AllArgsConstructor;
-
-import java.io.Serializable;
-import java.util.Date;
-
-/**
- * <br>
- * 鐢ㄦ埛瀹夊崜璁惧鍏宠仈瀵硅薄 sys_user_client
- * </br>
- *
- * @author deslrey
- * @version 1.0
- * @since 2026/2/9
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@TableName("sys_user_client")
-public class SysUserClient implements Serializable {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * 鐢ㄦ埛ID
- */
- @TableId(type = IdType.INPUT)
- private Long userId;
-
- /**
- * 涓帹璁惧鏍囪瘑 (CID)
- */
- private String cid;
-
- /**
- * 鏈�鍚庢椿璺冩椂闂�
- */
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private Date updateTime;
-}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java b/src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java
deleted file mode 100644
index b48e2e7..0000000
--- a/src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.ruoyi.project.system.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.ruoyi.project.system.domain.SysUserClient;
-
-/**
- * <br>
- * 鐢ㄦ埛瀹夊崜璁惧鍏宠仈mapper
- * </br>
- *
- * @author deslrey
- * @version 1.0
- * @since 2026/2/9
- */
-
-public interface SysUserClientMapper extends BaseMapper<SysUserClient> {
-
-}
diff --git a/src/main/java/com/ruoyi/project/system/service/SysUserClientService.java b/src/main/java/com/ruoyi/project/system/service/SysUserClientService.java
deleted file mode 100644
index 0ff0009..0000000
--- a/src/main/java/com/ruoyi/project/system/service/SysUserClientService.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.ruoyi.project.system.service;
-
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.ruoyi.project.system.domain.SysUserClient;
-
-/**
- * <br>
- * 鐢ㄦ埛瀹夊崜璁惧鍏宠仈鎺ュ彛
- * </br>
- *
- * @author deslrey
- * @version 1.0
- * @since 2026/2/9
- */
-
-public interface SysUserClientService extends IService<SysUserClient> {
-
- boolean addOrUpdateClientId(SysUserClient sysUserClient);
-}
diff --git a/src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java b/src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java
deleted file mode 100644
index 7130bf4..0000000
--- a/src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.ruoyi.project.system.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.project.system.domain.SysUserClient;
-import com.ruoyi.project.system.mapper.SysUserClientMapper;
-import com.ruoyi.project.system.service.SysUserClientService;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.Date;
-
-/**
- * 鐢ㄦ埛瀹夊崜璁惧鍏宠仈鎺ュ彛瀹炵幇绫�
- *
- * @author deslrey
- * @version 1.0
- * @since 2026/2/9
- */
-@Service
-public class SysUserClientServiceImpl extends ServiceImpl<SysUserClientMapper, SysUserClient> implements SysUserClientService {
-
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean addOrUpdateClientId(SysUserClient sysUserClient) {
- if (sysUserClient == null || sysUserClient.getUserId() == null || StringUtils.isEmpty(sysUserClient.getCid())) {
- return false;
- }
-
- String cid = sysUserClient.getCid();
- Long userId = sysUserClient.getUserId();
-
- remove(new LambdaQueryWrapper<SysUserClient>().eq(SysUserClient::getCid, cid).ne(SysUserClient::getUserId, userId));
-
- SysUserClient userClient = new SysUserClient();
- userClient.setUserId(userId);
- userClient.setCid(cid);
- userClient.setUpdateTime(new Date());
-
- return saveOrUpdate(userClient);
- }
-}
\ No newline at end of file
diff --git a/src/main/resources/application-jhy.yml b/src/main/resources/application-jhy.yml
index beeda1b..013902e 100644
--- a/src/main/resources/application-jhy.yml
+++ b/src/main/resources/application-jhy.yml
@@ -1,4 +1,4 @@
-锘�# 椤圭洰鐩稿叧閰嶇疆
+# 椤圭洰鐩稿叧閰嶇疆
ruoyi:
# 鍚嶇О
name: RuoYi
@@ -133,8 +133,7 @@
enabled: false
# redis 閰嶇疆
data:
-# mongodb:
-# uri: mongodb://114.132.189.42:9028/chat_memory_db
+
# redis 閰嶇疆
redis:
# 鍦板潃
--
Gitblit v1.9.3