From e7c60afd56e8a46dac2111283108b718398c7d2a Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 25 五月 2026 16:27:47 +0800
Subject: [PATCH] refactor(approve): 重构审批业务状态同步逻辑

---
 src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java | 1035 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 1,035 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java b/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
new file mode 100644
index 0000000..1ff96c1
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
@@ -0,0 +1,1035 @@
+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.ai.context.AiSessionUserContext;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.device.mapper.DeviceDefectRecordMapper;
+import com.ruoyi.device.mapper.DeviceLedgerMapper;
+import com.ruoyi.device.mapper.DeviceRepairMapper;
+import com.ruoyi.device.pojo.DeviceDefectRecord;
+import com.ruoyi.device.pojo.DeviceLedger;
+import com.ruoyi.device.pojo.DeviceRepair;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
+import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
+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.pojo.ProductionOperationTask;
+import com.ruoyi.production.pojo.ProductionOrder;
+import com.ruoyi.production.pojo.ProductionPlan;
+import com.ruoyi.production.pojo.ProductionProductMain;
+import com.ruoyi.quality.mapper.QualityInspectMapper;
+import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
+import com.ruoyi.quality.pojo.QualityInspect;
+import com.ruoyi.quality.pojo.QualityUnqualified;
+import com.ruoyi.stock.mapper.StockInventoryMapper;
+import com.ruoyi.stock.pojo.StockInventory;
+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.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+public class ManufacturingAgentTools {
+
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final int DEFAULT_LIMIT = 10;
+    private static final int MAX_LIMIT = 30;
+    private static final int DEVICE_REPAIR_STATUS_PENDING = 0;
+
+    private final ProductionPlanMapper productionPlanMapper;
+    private final ProductionOrderMapper productionOrderMapper;
+    private final ProductionOperationTaskMapper productionOperationTaskMapper;
+    private final ProductionProductMainMapper productionProductMainMapper;
+    private final DeviceLedgerMapper deviceLedgerMapper;
+    private final DeviceRepairMapper deviceRepairMapper;
+    private final DeviceDefectRecordMapper deviceDefectRecordMapper;
+    private final QualityInspectMapper qualityInspectMapper;
+    private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
+    private final StockInventoryMapper stockInventoryMapper;
+    private final ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
+    private final AiSessionUserContext aiSessionUserContext;
+
+    public ManufacturingAgentTools(ProductionPlanMapper productionPlanMapper,
+                                   ProductionOrderMapper productionOrderMapper,
+                                   ProductionOperationTaskMapper productionOperationTaskMapper,
+                                   ProductionProductMainMapper productionProductMainMapper,
+                                   DeviceLedgerMapper deviceLedgerMapper,
+                                   DeviceRepairMapper deviceRepairMapper,
+                                   DeviceDefectRecordMapper deviceDefectRecordMapper,
+                                   QualityInspectMapper qualityInspectMapper,
+                                   QualityUnqualifiedMapper qualityUnqualifiedMapper,
+                                   StockInventoryMapper stockInventoryMapper,
+                                   ProcurementExceptionRecordMapper procurementExceptionRecordMapper,
+                                   AiSessionUserContext aiSessionUserContext) {
+        this.productionPlanMapper = productionPlanMapper;
+        this.productionOrderMapper = productionOrderMapper;
+        this.productionOperationTaskMapper = productionOperationTaskMapper;
+        this.productionProductMainMapper = productionProductMainMapper;
+        this.deviceLedgerMapper = deviceLedgerMapper;
+        this.deviceRepairMapper = deviceRepairMapper;
+        this.deviceDefectRecordMapper = deviceDefectRecordMapper;
+        this.qualityInspectMapper = qualityInspectMapper;
+        this.qualityUnqualifiedMapper = qualityUnqualifiedMapper;
+        this.stockInventoryMapper = stockInventoryMapper;
+        this.procurementExceptionRecordMapper = procurementExceptionRecordMapper;
+        this.aiSessionUserContext = aiSessionUserContext;
+    }
+
+    @Tool(name = "鏌ヨ鍒堕�犱笟鍔″煙鏁版嵁", value = "鎸変笟鍔″煙鏌ヨ鐢熶骇鐜板満銆佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗙浉鍏虫暟鎹��")
+    public String queryDomain(@ToolMemoryId String memoryId,
+                              @P(value = "涓氬姟鍩燂紝site/plan/workorder/device/quality/material/exception") String domain,
+                              @P(value = "鍏抽敭瀛楋紝鍙笉浼�", required = false) String keyword,
+                              @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit,
+                              @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);
+        int finalLimit = normalizeLimit(limit);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+        boolean hasTimeConstraint = hasTimeConstraint(startDate, endDate, timeRange);
+        String normalizedDomain = normalizeDomain(domain);
+
+        return switch (normalizedDomain) {
+            case "site" -> siteSnapshot(loginUser, range);
+            case "plan" -> listProductionPlans(loginUser, keyword, finalLimit, range);
+            case "workorder" -> listWorkOrders(loginUser, keyword, finalLimit, range);
+            case "device" -> isRepairIntent(keyword, timeRange)
+                    ? listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint)
+                    : listDevices(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit);
+            case "repair" -> listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint);
+            case "quality" -> listQualityIssues(loginUser, keyword, finalLimit, range);
+            case "material" -> listMaterialInventory(loginUser, keyword, finalLimit);
+            case "exception" -> listExceptions(loginUser, keyword, finalLimit, range);
+            default -> jsonResponse(false, "manufacturing_query", "涓嶆敮鎸佺殑涓氬姟鍩�: " + safe(domain), Map.of(), Map.of(), Map.of());
+        };
+    }
+
+    @Tool(name = "鍒堕�犻璀︾湅鏉�", value = "璁$畻璁″垝銆佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗙殑棰勮淇℃伅銆�")
+    public String getWarningBoard(@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);
+        LocalDate today = LocalDate.now();
+
+        long overduePlanCount = countOverduePlans(loginUser, today);
+        long overdueWorkOrderCount = countOverdueWorkOrders(loginUser, today);
+        long pendingRepairCount = countPendingRepairs(loginUser);
+        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
+        long lowStockCount = countLowStock(loginUser);
+        long exceptionCount = countExceptionRecords(loginUser, range);
+
+        List<Map<String, Object>> warningItems = new ArrayList<>();
+        if (overduePlanCount > 0) {
+            warningItems.add(warningItem("high", "璁″垝閫炬湡", overduePlanCount, "鏈夌敓浜ц鍒掕秴杩囬渶姹傛棩鏈熶粛鏈畬鎴�"));
+        }
+        if (overdueWorkOrderCount > 0) {
+            warningItems.add(warningItem("high", "宸ュ崟閫炬湡", overdueWorkOrderCount, "鏈夊伐鍗曡鍒掔粨鏉熸棩鏈熷凡杩囦粛鏈畬宸�"));
+        }
+        if (pendingRepairCount > 0) {
+            warningItems.add(warningItem("medium", "璁惧寰呯淮淇�", pendingRepairCount, "瀛樺湪寰呯淮淇�/缁翠慨涓殑璁惧"));
+        }
+        if (qualityOpenCount > 0) {
+            warningItems.add(warningItem("high", "璐ㄩ噺鏈棴鐜�", qualityOpenCount, "瀛樺湪鏈鐞嗗畬鎴愮殑涓嶅悎鏍艰褰�"));
+        }
+        if (lowStockCount > 0) {
+            warningItems.add(warningItem("medium", "鐗╂枡浣庡簱瀛�", lowStockCount, "搴撳瓨鏁伴噺浣庝簬鎴栫瓑浜庨璀﹂槇鍊�"));
+        }
+        if (exceptionCount > 0) {
+            warningItems.add(warningItem("medium", "寮傚父璁板綍", exceptionCount, "鏃堕棿鑼冨洿鍐呭瓨鍦ㄥ紓甯稿鐞嗚褰�"));
+        }
+
+        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("warningCount", warningItems.size());
+        summary.put("overduePlanCount", overduePlanCount);
+        summary.put("overdueWorkOrderCount", overdueWorkOrderCount);
+        summary.put("pendingRepairCount", pendingRepairCount);
+        summary.put("qualityOpenCount", qualityOpenCount);
+        summary.put("lowStockCount", lowStockCount);
+        summary.put("exceptionCount", exceptionCount);
+
+        return jsonResponse(true, "manufacturing_warning", "宸茶繑鍥炲埗閫犻璀︾湅鏉裤��", summary,
+                Map.of("items", warningItems), Map.of());
+    }
+
+    @Tool(name = "鍒堕�犵粡钀ュ垎鏋�", value = "鎸夋椂闂磋寖鍥磋緭鍑哄埗閫犲叧閿寚鏍囷紝鏀寔鏌ャ�侀棶銆佸垎鏋愬満鏅��")
+    public String analyzeFactory(@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);
+
+        long planTotal = countPlans(loginUser, range);
+        long planCompleted = countPlansByStatus(loginUser, range, 2);
+        long workOrderTotal = countWorkOrders(loginUser, range);
+        long workOrderCompleted = countWorkOrdersByStatus(loginUser, range, 2);
+        long workOrderInProgress = countWorkOrdersByStatus(loginUser, range, 1);
+
+        long outputCount = countOutputs(loginUser, range);
+        long deviceTotal = countDevices(loginUser);
+        long pendingRepairCount = countPendingRepairs(loginUser);
+        long qualityInspectTotal = countQualityInspect(loginUser, range);
+        long qualityNgCount = countOpenQualityIssues(loginUser, range);
+        long materialSkuCount = countInventorySku(loginUser);
+        long lowStockCount = countLowStock(loginUser);
+        long exceptionCount = countExceptionRecords(loginUser, range);
+
+        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("planTotal", planTotal);
+        summary.put("planCompleted", planCompleted);
+        summary.put("planCompletionRate", toRate(planCompleted, planTotal));
+        summary.put("workOrderTotal", workOrderTotal);
+        summary.put("workOrderCompleted", workOrderCompleted);
+        summary.put("workOrderInProgress", workOrderInProgress);
+        summary.put("workOrderCompletionRate", toRate(workOrderCompleted, workOrderTotal));
+        summary.put("outputCount", outputCount);
+        summary.put("deviceTotal", deviceTotal);
+        summary.put("pendingRepairCount", pendingRepairCount);
+        summary.put("qualityInspectTotal", qualityInspectTotal);
+        summary.put("qualityNgCount", qualityNgCount);
+        summary.put("qualityIssueRate", toRate(qualityNgCount, qualityInspectTotal));
+        summary.put("materialSkuCount", materialSkuCount);
+        summary.put("lowStockCount", lowStockCount);
+        summary.put("exceptionCount", exceptionCount);
+
+        List<Map<String, Object>> coreMetrics = List.of(
+                metric("璁″垝瀹屾垚鐜�", toRate(planCompleted, planTotal)),
+                metric("宸ュ崟瀹屾垚鐜�", toRate(workOrderCompleted, workOrderTotal)),
+                metric("璐ㄩ噺寮傚父鐜�", toRate(qualityNgCount, qualityInspectTotal)),
+                metric("浣庡簱瀛樺崰姣�", toRate(lowStockCount, materialSkuCount))
+        );
+
+        Map<String, Object> charts = new LinkedHashMap<>();
+        charts.put("domainBarOption", buildDomainBarOption(summary));
+        charts.put("qualityPieOption", buildQualityPieOption(qualityInspectTotal, qualityNgCount));
+
+        return jsonResponse(true, "manufacturing_analysis", "宸茶繑鍥炲埗閫犲垎鏋愮粨鏋溿��", summary,
+                Map.of("coreMetrics", coreMetrics), charts);
+    }
+
+    @Tool(name = "鐢熸垚鍒堕�犲姙鐞嗗缓璁�", value = "鏍规嵁鐢ㄦ埛闂杈撳嚭鍙墽琛岀殑鍔炵悊鍔ㄤ綔寤鸿锛屽寘鎷洰鏍囦笟鍔℃帴鍙c�佸繀濉瓧娈靛拰绀轰緥銆�")
+    public String planActions(@ToolMemoryId String memoryId,
+                              @P("鐢ㄦ埛璇夋眰鍘熸枃") String userQuery) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        List<Map<String, Object>> actionCards = new ArrayList<>();
+
+        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "宸ュ崟", "娲惧伐", "浣滀笟")) {
+            actionCards.add(actionCard(
+                    "workorder_assign",
+                    "宸ュ崟娲惧伐",
+                    "POST",
+                    "/productionOperationTask/assign",
+                    List.of("id", "userIds"),
+                    Map.of("id", 10001, "userIds", "12,13"),
+                    "灏嗗伐鍗曞垎閰嶇粰鎸囧畾浜哄憳锛岄�傜敤浜庣幇鍦鸿皟搴︺��"));
+        }
+        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "璁惧", "缁翠慨", "鏁呴殰")) {
+            actionCards.add(actionCard(
+                    "device_repair_create",
+                    "鍒涘缓璁惧缁翠慨鍗�",
+                    "POST",
+                    "/device/repair",
+                    List.of("deviceLedgerId", "deviceName", "repairName", "remark"),
+                    Map.of("deviceLedgerId", 1001, "deviceName", "绌哄帇鏈篈-01", "repairName", "寮犱笁", "remark", "寮傚搷骞朵即闅忔俯鍗�"),
+                    "鏂板缓缁翠慨鍗曪紝杩涘叆璁惧寮傚父澶勭悊闂幆銆�"));
+        }
+        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "璐ㄩ噺", "涓嶅悎鏍�", "闂幆")) {
+            actionCards.add(actionCard(
+                    "quality_unqualified_deal",
+                    "澶勭悊涓嶅悎鏍煎崟",
+                    "POST",
+                    "/quality/qualityUnqualified/deal",
+                    List.of("id", "dealResult", "dealName"),
+                    Map.of("id", 3001, "dealResult", "杩斿伐鍚庡妫�", "dealName", "鏉庡洓"),
+                    "瀵逛笉鍚堟牸璁板綍鎵ц澶勭疆骞堕棴鐜��"));
+        }
+        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "鐗╂枡", "搴撳瓨", "琛ユ枡")) {
+            actionCards.add(actionCard(
+                    "material_inbound",
+                    "琛ュ厖搴撳瓨",
+                    "POST",
+                    "/stockInventory/addstockInventory",
+                    List.of("productModelId", "batchNo", "qualitity"),
+                    Map.of("productModelId", 5001, "batchNo", "B2026051601", "qualitity", 120),
+                    "褰撲綆搴撳瓨棰勮瑙﹀彂鏃讹紝澧炲姞搴撳瓨鏁伴噺銆�"));
+        }
+        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "寮傚父", "閲囪喘寮傚父", "鏉ユ枡寮傚父")) {
+            actionCards.add(actionCard(
+                    "procurement_exception_add",
+                    "鐧昏寮傚父璁板綍",
+                    "POST",
+                    "/procurementExceptionRecord/add",
+                    List.of("purchaseLedgerId", "exceptionReason", "exceptionNum"),
+                    Map.of("purchaseLedgerId", 888, "exceptionReason", "鍒版枡鐭己", "exceptionNum", 24),
+                    "鐧昏閲囪喘/鏉ユ枡寮傚父锛屼究浜庡悗缁拷韪拰鍒嗘瀽銆�"));
+        }
+
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put("actionCount", actionCards.size());
+        summary.put("userId", loginUser.getUserId());
+        summary.put("tenantId", loginUser.getTenantId());
+
+        return jsonResponse(true, "manufacturing_action_plan", "宸茬敓鎴愬姙鐞嗗缓璁紝璇峰墠绔紩瀵肩敤鎴风‘璁ゅ悗璋冪敤鐩爣涓氬姟鎺ュ彛銆�",
+                summary, Map.of("actionCards", actionCards), Map.of());
+    }
+
+    private String siteSnapshot(LoginUser loginUser, DateRange range) {
+        long planTotal = countPlans(loginUser, range);
+        long workOrderTotal = countWorkOrders(loginUser, range);
+        long outputCount = countOutputs(loginUser, range);
+        long deviceTotal = countDevices(loginUser);
+        long pendingRepairCount = countPendingRepairs(loginUser);
+        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
+        long lowStockCount = countLowStock(loginUser);
+        long exceptionCount = countExceptionRecords(loginUser, range);
+
+        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("planTotal", planTotal);
+        summary.put("workOrderTotal", workOrderTotal);
+        summary.put("outputCount", outputCount);
+        summary.put("deviceTotal", deviceTotal);
+        summary.put("pendingRepairCount", pendingRepairCount);
+        summary.put("qualityOpenCount", qualityOpenCount);
+        summary.put("lowStockCount", lowStockCount);
+        summary.put("exceptionCount", exceptionCount);
+
+        return jsonResponse(true, "manufacturing_site_snapshot", "宸茶繑鍥炵敓浜х幇鍦烘瑙堛��", summary, Map.of(), Map.of());
+    }
+
+    private String listProductionPlans(LoginUser loginUser, String keyword, int limit, DateRange range) {
+        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(ProductionPlan::getMpsNo, keyword)
+                    .or().like(ProductionPlan::getRemark, keyword)
+                    .or().like(ProductionPlan::getSource, keyword));
+        }
+        wrapper.orderByDesc(ProductionPlan::getRequiredDate, ProductionPlan::getId).last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(productionPlanMapper.selectList(wrapper)).stream()
+                .map(this::toPlanItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_plan_list", "宸茶繑鍥炵敓浜ц鍒掑垪琛ㄣ��",
+                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+    }
+
+    private String listWorkOrders(LoginUser loginUser, String keyword, int limit, DateRange range) {
+        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+                .le(ProductionOperationTask::getPlanEndTime, range.end());
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(ProductionOperationTask::getWorkOrderNo, keyword)
+                    .or().like(ProductionOperationTask::getUserIds, keyword));
+        }
+        wrapper.orderByDesc(ProductionOperationTask::getPlanEndTime, ProductionOperationTask::getId)
+                .last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(productionOperationTaskMapper.selectList(wrapper)).stream()
+                .map(this::toWorkOrderItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_workorder_list", "宸茶繑鍥炲伐鍗曞垪琛ㄣ��",
+                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+    }
+
+    private String listDevices(LoginUser loginUser, String keyword, int limit) {
+        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
+                    .or().like(DeviceLedger::getDeviceModel, keyword)
+                    .or().like(DeviceLedger::getDeviceBrand, keyword));
+        }
+        wrapper.orderByDesc(DeviceLedger::getId).last("limit " + limit);
+
+        Map<Long, Long> pendingRepairMap = pendingRepairCountByDevice(loginUser);
+        List<Map<String, Object>> items = defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
+                .map(item -> toDeviceItem(item, pendingRepairMap.getOrDefault(item.getId(), 0L)))
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_device_list", "宸茶繑鍥炶澶囧垪琛ㄣ��",
+                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
+    }
+
+    private String listDeviceRepairs(LoginUser loginUser, String keyword, int limit, DateRange range, boolean hasTimeConstraint) {
+        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+        Long currentDeptId = loginUser.getCurrentDeptId();
+        if (currentDeptId != null) {
+            wrapper.and(w -> w.eq(DeviceRepair::getDeptId, currentDeptId).or().isNull(DeviceRepair::getDeptId));
+        }
+        if (hasTimeConstraint) {
+            wrapper.ge(DeviceRepair::getCreateTime, range.start().atStartOfDay())
+                    .lt(DeviceRepair::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        }
+        if (StringUtils.hasText(keyword)) {
+            List<Long> matchedDeviceIds = findDeviceLedgerIdsByKeyword(loginUser, keyword);
+            wrapper.and(w -> {
+                w.like(DeviceRepair::getDeviceName, keyword)
+                        .or().like(DeviceRepair::getDeviceModel, keyword)
+                        .or().like(DeviceRepair::getRemark, keyword)
+                        .or().like(DeviceRepair::getRepairName, keyword)
+                        .or().like(DeviceRepair::getMaintenanceName, keyword);
+                if (!matchedDeviceIds.isEmpty()) {
+                    w.or().in(DeviceRepair::getDeviceLedgerId, matchedDeviceIds);
+                }
+            });
+        }
+        wrapper.orderByDesc(DeviceRepair::getCreateTime, DeviceRepair::getId).last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(deviceRepairMapper.selectList(wrapper)).stream()
+                .map(this::toDeviceRepairItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_device_repair_list", "宸茶繑鍥炶澶囩淮淇褰曘��",
+                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+    }
+
+    private String listQualityIssues(LoginUser loginUser, String keyword, int limit, DateRange range) {
+        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
+        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
+                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()));
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(QualityUnqualified::getProductName, keyword)
+                    .or().like(QualityUnqualified::getDefectivePhenomena, keyword)
+                    .or().like(QualityUnqualified::getDealResult, keyword));
+        }
+        wrapper.orderByDesc(QualityUnqualified::getCheckTime, QualityUnqualified::getId).last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(qualityUnqualifiedMapper.selectList(wrapper)).stream()
+                .map(this::toQualityItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_quality_list", "宸茶繑鍥炶川閲忓紓甯稿垪琛ㄣ��",
+                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+    }
+
+    private String listMaterialInventory(LoginUser loginUser, String keyword, int limit) {
+        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(StockInventory::getBatchNo, keyword)
+                    .or().like(StockInventory::getProductModelId, keyword));
+        }
+        wrapper.orderByDesc(StockInventory::getId).last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(stockInventoryMapper.selectList(wrapper)).stream()
+                .map(this::toMaterialItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_material_list", "宸茶繑鍥炵墿鏂欏簱瀛樺垪琛ㄣ��",
+                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
+    }
+
+    private String listExceptions(LoginUser loginUser, String keyword, int limit, DateRange range) {
+        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
+        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
+                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        if (StringUtils.hasText(keyword)) {
+            wrapper.like(ProcurementExceptionRecord::getExceptionReason, keyword);
+        }
+        wrapper.orderByDesc(ProcurementExceptionRecord::getCreateTime, ProcurementExceptionRecord::getId)
+                .last("limit " + limit);
+
+        List<Map<String, Object>> items = defaultList(procurementExceptionRecordMapper.selectList(wrapper)).stream()
+                .map(this::toExceptionItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "manufacturing_exception_list", "宸茶繑鍥炲紓甯稿鐞嗗垪琛ㄣ��",
+                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+    }
+
+    private long countPlans(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
+        return productionPlanMapper.selectCount(wrapper);
+    }
+
+    private long countPlansByStatus(LoginUser loginUser, DateRange range, int status) {
+        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+        wrapper.ge(ProductionPlan::getRequiredDate, range.start())
+                .le(ProductionPlan::getRequiredDate, range.end())
+                .eq(ProductionPlan::getStatus, status);
+        return productionPlanMapper.selectCount(wrapper);
+    }
+
+    private long countWorkOrders(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+                .le(ProductionOperationTask::getPlanEndTime, range.end());
+        return productionOperationTaskMapper.selectCount(wrapper);
+    }
+
+    private long countWorkOrdersByStatus(LoginUser loginUser, DateRange range, int status) {
+        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+                .le(ProductionOperationTask::getPlanEndTime, range.end())
+                .eq(ProductionOperationTask::getStatus, status);
+        return productionOperationTaskMapper.selectCount(wrapper);
+    }
+
+    private long countOutputs(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<ProductionProductMain> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
+        wrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay())
+                .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        return productionProductMainMapper.selectCount(wrapper);
+    }
+
+    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 countPendingRepairs(LoginUser loginUser) {
+        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
+        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
+        return deviceRepairMapper.selectCount(wrapper);
+    }
+
+    private long countQualityInspect(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<QualityInspect> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityInspect::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityInspect::getDeptId);
+        wrapper.ge(QualityInspect::getCheckTime, toDate(range.start()))
+                .lt(QualityInspect::getCheckTime, toExclusiveEndDate(range.end()));
+        return qualityInspectMapper.selectCount(wrapper);
+    }
+
+    private long countOpenQualityIssues(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
+        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
+                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()))
+                .ne(QualityUnqualified::getInspectState, 2);
+        return qualityUnqualifiedMapper.selectCount(wrapper);
+    }
+
+    private long countInventorySku(LoginUser loginUser) {
+        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+        return stockInventoryMapper.selectCount(wrapper);
+    }
+
+    private long countLowStock(LoginUser loginUser) {
+        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+        wrapper.isNotNull(StockInventory::getWarnNum);
+        List<StockInventory> stocks = defaultList(stockInventoryMapper.selectList(wrapper));
+        return stocks.stream()
+                .filter(this::isLowStock)
+                .count();
+    }
+
+    private long countExceptionRecords(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
+        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
+                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        return procurementExceptionRecordMapper.selectCount(wrapper);
+    }
+
+    private long countOverduePlans(LoginUser loginUser, LocalDate today) {
+        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+        wrapper.lt(ProductionPlan::getRequiredDate, today).ne(ProductionPlan::getStatus, 2);
+        return productionPlanMapper.selectCount(wrapper);
+    }
+
+    private long countOverdueWorkOrders(LoginUser loginUser, LocalDate today) {
+        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+        wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2);
+        return productionOperationTaskMapper.selectCount(wrapper);
+    }
+
+    private Map<Long, Long> pendingRepairCountByDevice(LoginUser loginUser) {
+        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
+        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
+        return defaultList(deviceRepairMapper.selectList(wrapper)).stream()
+                .filter(item -> item.getDeviceLedgerId() != null)
+                .collect(Collectors.groupingBy(DeviceRepair::getDeviceLedgerId, Collectors.counting()));
+    }
+
+    private Map<String, Object> toPlanItem(ProductionPlan item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("mpsNo", safe(item.getMpsNo()));
+        map.put("requiredDate", formatDate(item.getRequiredDate()));
+        map.put("promisedDeliveryDate", formatDate(item.getPromisedDeliveryDate()));
+        map.put("qtyRequired", item.getQtyRequired());
+        map.put("quantityIssued", item.getQuantityIssued());
+        map.put("status", item.getStatus());
+        map.put("source", safe(item.getSource()));
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private Map<String, Object> toWorkOrderItem(ProductionOperationTask item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("workOrderNo", safe(item.getWorkOrderNo()));
+        map.put("productionOrderId", item.getProductionOrderId());
+        map.put("planStartTime", formatDate(item.getPlanStartTime()));
+        map.put("planEndTime", formatDate(item.getPlanEndTime()));
+        map.put("actualStartTime", formatDate(item.getActualStartTime()));
+        map.put("actualEndTime", formatDate(item.getActualEndTime()));
+        map.put("planQuantity", item.getPlanQuantity());
+        map.put("completeQuantity", item.getCompleteQuantity());
+        map.put("status", item.getStatus());
+        map.put("userIds", safe(item.getUserIds()));
+        return map;
+    }
+
+    private Map<String, Object> toDeviceItem(DeviceLedger item, long pendingRepairCount) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("deviceName", safe(item.getDeviceName()));
+        map.put("deviceModel", safe(item.getDeviceModel()));
+        map.put("deviceBrand", safe(item.getDeviceBrand()));
+        map.put("status", safe(item.getStatus()));
+        map.put("storageLocation", safe(item.getStorageLocation()));
+        map.put("supplierName", safe(item.getSupplierName()));
+        map.put("pendingRepairCount", pendingRepairCount);
+        return map;
+    }
+
+    private Map<String, Object> toDeviceRepairItem(DeviceRepair item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("deviceLedgerId", item.getDeviceLedgerId());
+        map.put("deviceName", safe(item.getDeviceName()));
+        map.put("deviceModel", safe(item.getDeviceModel()));
+        map.put("repairTime", formatDate(item.getRepairTime()));
+        map.put("repairName", safe(item.getRepairName()));
+        map.put("maintenanceName", safe(item.getMaintenanceName()));
+        map.put("maintenanceTime", formatDateTime(item.getMaintenanceTime()));
+        map.put("maintenanceResult", safe(item.getMaintenanceResult()));
+        map.put("acceptanceName", safe(item.getAcceptanceName()));
+        map.put("acceptanceTime", formatDateTime(item.getAcceptanceTime()));
+        map.put("status", item.getStatus());
+        map.put("remark", safe(item.getRemark()));
+        map.put("createTime", formatDateTime(item.getCreateTime()));
+        return map;
+    }
+
+    private Map<String, Object> toQualityItem(QualityUnqualified item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("checkTime", formatDate(item.getCheckTime()));
+        map.put("inspectState", item.getInspectState());
+        map.put("productId", item.getProductId());
+        map.put("productName", safe(item.getProductName()));
+        map.put("model", safe(item.getModel()));
+        map.put("quantity", item.getQuantity());
+        map.put("defectivePhenomena", safe(item.getDefectivePhenomena()));
+        map.put("dealResult", safe(item.getDealResult()));
+        map.put("dealName", safe(item.getDealName()));
+        map.put("dealTime", formatDate(item.getDealTime()));
+        return map;
+    }
+
+    private Map<String, Object> toMaterialItem(StockInventory item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("productModelId", item.getProductModelId());
+        map.put("batchNo", safe(item.getBatchNo()));
+        map.put("qualitity", item.getQualitity());
+        map.put("lockedQuantity", item.getLockedQuantity());
+        map.put("warnNum", item.getWarnNum());
+        map.put("lowStock", isLowStock(item));
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private Map<String, Object> toExceptionItem(ProcurementExceptionRecord item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
+        map.put("exceptionReason", safe(item.getExceptionReason()));
+        map.put("exceptionNum", item.getExceptionNum());
+        map.put("createTime", formatDateTime(item.getCreateTime()));
+        return map;
+    }
+
+    private boolean isLowStock(StockInventory item) {
+        BigDecimal quantity = item.getQualitity();
+        BigDecimal warnNum = item.getWarnNum();
+        if (quantity == null || warnNum == null) {
+            return false;
+        }
+        return quantity.compareTo(warnNum) <= 0;
+    }
+
+    private Map<String, Object> warningItem(String level, String title, long count, String detail) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("level", level);
+        map.put("title", title);
+        map.put("count", count);
+        map.put("detail", detail);
+        return map;
+    }
+
+    private Map<String, Object> actionCard(String code,
+                                           String name,
+                                           String method,
+                                           String targetApi,
+                                           List<String> requiredFields,
+                                           Map<String, Object> examplePayload,
+                                           String description) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("code", code);
+        map.put("name", name);
+        map.put("method", method);
+        map.put("targetApi", targetApi);
+        map.put("requiredFields", requiredFields);
+        map.put("examplePayload", examplePayload);
+        map.put("description", description);
+        return map;
+    }
+
+    private Map<String, Object> metric(String label, String value) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("label", label);
+        map.put("value", value);
+        return map;
+    }
+
+    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 Map<String, Object> buildDomainBarOption(Map<String, Object> summary) {
+        List<String> xData = List.of("璁″垝", "宸ュ崟", "璁惧", "璐ㄩ噺", "鐗╂枡", "寮傚父");
+        List<Number> yData = List.of(
+                numberValue(summary.get("planTotal")),
+                numberValue(summary.get("workOrderTotal")),
+                numberValue(summary.get("deviceTotal")),
+                numberValue(summary.get("qualityNgCount")),
+                numberValue(summary.get("lowStockCount")),
+                numberValue(summary.get("exceptionCount"))
+        );
+        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> buildQualityPieOption(long inspectTotal, long ngCount) {
+        long passCount = Math.max(inspectTotal - ngCount, 0);
+        List<Map<String, Object>> data = List.of(
+                Map.of("name", "涓嶅悎鏍�", "value", ngCount),
+                Map.of("name", "闈炰笉鍚堟牸", "value", passCount)
+        );
+        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 int numberValue(Object value) {
+        if (value instanceof Number number) {
+            return number.intValue();
+        }
+        return 0;
+    }
+
+    private String toRate(long numerator, long denominator) {
+        if (denominator <= 0) {
+            return "0.00%";
+        }
+        BigDecimal rate = new BigDecimal(numerator)
+                .multiply(new BigDecimal("100"))
+                .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
+        return rate.toPlainString() + "%";
+    }
+
+    private String normalizeDomain(String domain) {
+        if (!StringUtils.hasText(domain)) {
+            return "";
+        }
+        String value = domain.trim().toLowerCase();
+        return switch (value) {
+            case "鐢熶骇鐜板満", "site", "factory", "workshop" -> "site";
+            case "璁″垝", "plan", "schedule" -> "plan";
+            case "宸ュ崟", "workorder", "work_order", "task" -> "workorder";
+            case "璁惧", "device", "equipment" -> "device";
+            case "缁翠慨", "repair", "maintenance" -> "repair";
+            case "璐ㄩ噺", "quality", "qc" -> "quality";
+            case "鐗╂枡", "material", "inventory", "stock" -> "material";
+            case "寮傚父", "exception", "abnormal" -> "exception";
+            default -> value;
+        };
+    }
+
+    private boolean isRepairIntent(String keyword, String userQuery) {
+        String query = safe(userQuery);
+        return containsAny(safe(keyword), "缁翠慨", "鎶ヤ慨", "妫�淇�", "缁存姢")
+                || containsAny(query, "缁翠慨", "鎶ヤ慨", "妫�淇�", "缁存姢");
+    }
+
+    private String normalizeDeviceQueryKeyword(String keyword, String userQuery) {
+        String source = StringUtils.hasText(keyword) ? keyword : userQuery;
+        if (!StringUtils.hasText(source)) {
+            return null;
+        }
+        String cleaned = source
+                .replace("鏌ヨ", "")
+                .replace("鏌ョ湅", "")
+                .replace("甯垜", "")
+                .replace("璇�", "")
+                .replace("鏌�", "")
+                .replace("璁惧", "")
+                .replace("缁翠慨璁板綍", "")
+                .replace("缁翠慨鎯呭喌", "")
+                .replace("鎶ヤ慨璁板綍", "")
+                .replace("鎶ヤ慨鎯呭喌", "")
+                .replace("缁翠慨", "")
+                .replace("鎶ヤ慨", "")
+                .replace("鎯呭喌", "")
+                .replace("璁板綍", "")
+                .replace("淇℃伅", "")
+                .replace("鐨�", "")
+                .replace("涓�涓�", "")
+                .trim();
+        return cleaned.length() >= 2 ? cleaned : null;
+    }
+
+    private List<Long> findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return List.of();
+        }
+        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
+        Long currentDeptId = loginUser.getCurrentDeptId();
+        if (currentDeptId != null) {
+            wrapper.and(w -> w.eq(DeviceLedger::getDeptId, currentDeptId).or().isNull(DeviceLedger::getDeptId));
+        }
+        wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
+                .or().like(DeviceLedger::getDeviceModel, keyword)
+                .or().like(DeviceLedger::getDeviceBrand, keyword));
+        wrapper.orderByDesc(DeviceLedger::getId).last("limit 200");
+        return defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
+                .map(DeviceLedger::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    private boolean hasTimeConstraint(String startDate, String endDate, String userQuery) {
+        if (StringUtils.hasText(startDate) || StringUtils.hasText(endDate)) {
+            return true;
+        }
+        if (!StringUtils.hasText(userQuery)) {
+            return false;
+        }
+        String text = userQuery.trim();
+        return containsAny(text, "浠婂ぉ", "鏄ㄥぉ", "鏈懆", "涓婂懆", "鏈湀", "涓婃湀", "浠婂勾", "鍘诲勾", "杩�", "鏈�杩�");
+    }
+
+    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
+        LocalDate today = LocalDate.now();
+        LocalDate start = parseLocalDate(startDate);
+        LocalDate end = parseLocalDate(endDate);
+        if (start != null || end != null) {
+            LocalDate s = start != null ? start : end;
+            LocalDate e = end != null ? end : start;
+            if (s.isAfter(e)) {
+                LocalDate temp = s;
+                s = e;
+                e = temp;
+            }
+            return new DateRange(s, e, s + "鑷�" + e);
+        }
+        if (!StringUtils.hasText(timeRange)) {
+            return new DateRange(today.minusDays(29), today, "杩�30澶�");
+        }
+        String text = timeRange.trim();
+        if (text.contains("浠婂ぉ")) {
+            return new DateRange(today, today, "浠婂ぉ");
+        }
+        if (text.contains("鏈懆")) {
+            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+            return new DateRange(startOfWeek, today, "鏈懆");
+        }
+        if (text.contains("鏈湀")) {
+            return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
+        }
+        if (text.contains("鏈勾") || text.contains("浠婂勾")) {
+            return new DateRange(today.withDayOfYear(1), today, "浠婂勾");
+        }
+        if (text.contains("鍘诲勾")) {
+            LocalDate firstDay = today.minusYears(1).withDayOfYear(1);
+            LocalDate lastDay = today.minusYears(1).withMonth(12).withDayOfMonth(31);
+            return new DateRange(firstDay, lastDay, "鍘诲勾");
+        }
+        if (text.contains("涓婃湀")) {
+            LocalDate startOfLastMonth = today.minusMonths(1).withDayOfMonth(1);
+            return new DateRange(startOfLastMonth, startOfLastMonth.withDayOfMonth(startOfLastMonth.lengthOfMonth()), "涓婃湀");
+        }
+        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+        if (matcher.find()) {
+            int amount = Integer.parseInt(matcher.group(2));
+            String unit = matcher.group(3);
+            LocalDate relativeStart = 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(relativeStart, today, "杩�" + amount + unit);
+        }
+        return new DateRange(today.minusDays(29), today, "杩�30澶�");
+    }
+
+    private LocalDate parseLocalDate(String text) {
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        return LocalDate.parse(text.trim(), DATE_FMT);
+    }
+
+    private Date toDate(LocalDate date) {
+        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
+    }
+
+    private Date toExclusiveEndDate(LocalDate date) {
+        return Date.from(date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+    }
+
+    private String formatDate(LocalDate date) {
+        return date == null ? "" : DATE_FMT.format(date);
+    }
+
+    private String formatDate(Date date) {
+        if (date == null) {
+            return "";
+        }
+        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+    }
+
+    private String formatDateTime(LocalDateTime time) {
+        if (time == null) {
+            return "";
+        }
+        return time.truncatedTo(ChronoUnit.SECONDS).toString().replace('T', ' ');
+    }
+
+    private int normalizeLimit(Integer limit) {
+        if (limit == null || limit <= 0) {
+            return DEFAULT_LIMIT;
+        }
+        return Math.min(limit, MAX_LIMIT);
+    }
+
+    private boolean containsAny(String text, String... values) {
+        for (String value : values) {
+            if (text.contains(value)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    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 LoginUser currentLoginUser(String memoryId) {
+        LoginUser loginUser = aiSessionUserContext.get(memoryId);
+        if (loginUser != null) {
+            return loginUser;
+        }
+        return SecurityUtils.getLoginUser();
+    }
+
+    private String safe(Object value) {
+        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
+    }
+
+    private <T> List<T> defaultList(List<T> list) {
+        return list == null ? List.of() : list;
+    }
+
+    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) {
+    }
+}

--
Gitblit v1.9.3