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> 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 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 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> coreMetrics = List.of( metric("计划完成率", toRate(planCompleted, planTotal)), metric("工单完成率", toRate(workOrderCompleted, workOrderTotal)), metric("质量异常率", toRate(qualityNgCount, qualityInspectTotal)), metric("低库存占比", toRate(lowStockCount, materialSkuCount)) ); Map 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 = "根据用户问题输出可执行的办理动作建议,包括目标业务接口、必填字段和示例。") public String planActions(@ToolMemoryId String memoryId, @P("用户诉求原文") String userQuery) { LoginUser loginUser = currentLoginUser(memoryId); List> 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", "空压机A-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 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 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 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> 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 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> 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 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 pendingRepairMap = pendingRepairCountByDevice(loginUser); List> 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 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 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> 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 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> 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 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> 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 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> 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 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 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 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 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 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 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 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 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 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 wrapper = new LambdaQueryWrapper<>(); applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId); return stockInventoryMapper.selectCount(wrapper); } private long countLowStock(LoginUser loginUser) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId); wrapper.isNotNull(StockInventory::getWarnNum); List stocks = defaultList(stockInventoryMapper.selectList(wrapper)); return stocks.stream() .filter(this::isLowStock) .count(); } private long countExceptionRecords(LoginUser loginUser, DateRange range) { LambdaQueryWrapper 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 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 wrapper = new LambdaQueryWrapper<>(); applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId); wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2); return productionOperationTaskMapper.selectCount(wrapper); } private Map pendingRepairCountByDevice(LoginUser loginUser) { LambdaQueryWrapper 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 toPlanItem(ProductionPlan item) { Map 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 toWorkOrderItem(ProductionOperationTask item) { Map 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 toDeviceItem(DeviceLedger item, long pendingRepairCount) { Map 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 toDeviceRepairItem(DeviceRepair item) { Map 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 toQualityItem(QualityUnqualified item) { Map 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 toMaterialItem(StockInventory item) { Map 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 toExceptionItem(ProcurementExceptionRecord item) { Map 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 warningItem(String level, String title, long count, String detail) { Map map = new LinkedHashMap<>(); map.put("level", level); map.put("title", title); map.put("count", count); map.put("detail", detail); return map; } private Map actionCard(String code, String name, String method, String targetApi, List requiredFields, Map examplePayload, String description) { Map 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 metric(String label, String value) { Map map = new LinkedHashMap<>(); map.put("label", label); map.put("value", value); return map; } private Map rangeSummary(DateRange range, int count, String keyword) { Map 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 buildDomainBarOption(Map summary) { List xData = List.of("计划", "工单", "设备", "质量", "物料", "异常"); List 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 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 buildQualityPieOption(long inspectTotal, long ngCount) { long passCount = Math.max(inspectTotal - ngCount, 0); List> data = List.of( Map.of("name", "不合格", "value", ngCount), Map.of("name", "非不合格", "value", passCount) ); Map 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 findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) { if (!StringUtils.hasText(keyword)) { return List.of(); } LambdaQueryWrapper 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 void applyTenantFilter(LambdaQueryWrapper wrapper, Long tenantId, SFunction field) { if (tenantId != null) { wrapper.eq(field, tenantId); } } private void applyDeptFilter(LambdaQueryWrapper wrapper, Long deptId, SFunction 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 List defaultList(List list) { return list == null ? List.of() : list; } private String jsonResponse(boolean success, String type, String description, Map summary, Map data, Map charts) { Map 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) { } }