package com.vincent.rsf.server.ai.service.provider; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult; import com.vincent.rsf.server.ai.model.AiPromptContext; import com.vincent.rsf.server.manager.entity.Loc; import com.vincent.rsf.server.manager.entity.LocItem; import com.vincent.rsf.server.manager.mapper.LocItemMapper; import com.vincent.rsf.server.manager.mapper.LocMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.*; @Service public class AiWarehouseSummaryService implements AiDiagnosticDataProvider { private static final String TOOL_CODE = "warehouse_summary"; private static final String TOOL_NAME = "库存摘要"; private static final Map LOC_STATUS_LABELS = new LinkedHashMap<>(); static { LOC_STATUS_LABELS.put("O", "空库"); LOC_STATUS_LABELS.put("D", "空板"); LOC_STATUS_LABELS.put("R", "预约出库"); LOC_STATUS_LABELS.put("S", "预约入库"); LOC_STATUS_LABELS.put("X", "禁用"); LOC_STATUS_LABELS.put("F", "在库"); } @Resource private LocMapper locMapper; @Resource private LocItemMapper locItemMapper; /** * 返回库存类内部工具的默认顺序。 */ @Override public int getOrder() { return 10; } /** * 返回库存工具编码。 */ @Override public String getToolCode() { return TOOL_CODE; } /** * 返回库存工具展示名。 */ @Override public String getToolName() { return TOOL_NAME; } /** * 返回库存工具默认说明。 */ @Override public String getDefaultToolPrompt() { return "结合库存摘要判断库位状态、库存结构与重点物料分布。"; } /** * 汇总库位与库存明细,生成库存摘要工具结果。 */ @Override public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) { return new AiDiagnosticToolResult() .setToolCode(getToolCode()) .setToolName(getToolName()) .setSeverity("INFO") .setSummaryText(buildWarehouseSummary(context)); } /** * 基于 man_loc 和 man_loc_item 生成库存概览、库位状态分布和 TOP 统计。 */ private String buildWarehouseSummary(AiPromptContext context) { List activeLocs = locMapper.selectList(new LambdaQueryWrapper() .select(Loc::getUseStatus) .eq(Loc::getStatus, 1)); long totalLoc = activeLocs.size(); List activeLocItems = locItemMapper.selectList(new LambdaQueryWrapper() .select(LocItem::getLocCode, LocItem::getMatnrCode, LocItem::getMaktx, LocItem::getAnfme) .eq(LocItem::getStatus, 1)); Map locStatusCounters = new LinkedHashMap<>(); for (Loc loc : activeLocs) { String useStatus = loc.getUseStatus(); locStatusCounters.put(useStatus, locStatusCounters.getOrDefault(useStatus, 0L) + 1); } long itemRows = activeLocItems.size(); Set locCodes = new HashSet<>(); Set materialCodes = new HashSet<>(); BigDecimal totalQty = BigDecimal.ZERO; Map locAggregates = new LinkedHashMap<>(); Map materialAggregates = new LinkedHashMap<>(); for (LocItem item : activeLocItems) { if (item.getLocCode() != null && !item.getLocCode().trim().isEmpty()) { locCodes.add(item.getLocCode()); } if (item.getMatnrCode() != null && !item.getMatnrCode().trim().isEmpty()) { materialCodes.add(item.getMatnrCode()); } totalQty = totalQty.add(toDecimal(item.getAnfme())); String locKey = item.getLocCode(); if (locKey != null && !locKey.trim().isEmpty()) { LocAggregate locAggregate = locAggregates.computeIfAbsent(locKey, key -> new LocAggregate()); locAggregate.totalQty = locAggregate.totalQty.add(toDecimal(item.getAnfme())); if (item.getMatnrCode() != null && !item.getMatnrCode().trim().isEmpty()) { locAggregate.materialCodes.add(item.getMatnrCode()); } } String materialKey = item.getMatnrCode(); if (materialKey != null && !materialKey.trim().isEmpty()) { MaterialAggregate materialAggregate = materialAggregates.computeIfAbsent(materialKey, key -> new MaterialAggregate()); materialAggregate.matnrCode = materialKey; materialAggregate.maktx = item.getMaktx(); materialAggregate.totalQty = materialAggregate.totalQty.add(toDecimal(item.getAnfme())); if (item.getLocCode() != null && !item.getLocCode().trim().isEmpty()) { materialAggregate.locCodes.add(item.getLocCode()); } } } List> topLocRows = new ArrayList<>(locAggregates.entrySet()); topLocRows.sort((left, right) -> right.getValue().totalQty.compareTo(left.getValue().totalQty)); if (topLocRows.size() > 5) { topLocRows = topLocRows.subList(0, 5); } List topMaterialRows = new ArrayList<>(materialAggregates.values()); topMaterialRows.sort((left, right) -> right.totalQty.compareTo(left.totalQty)); if (topMaterialRows.size() > 5) { topMaterialRows = topMaterialRows.subList(0, 5); } StringBuilder summary = new StringBuilder(); summary.append("以下是基于 man_loc 和 man_loc_item 的实时汇总,请优先依据这些数据回答;如果超出这两张表可推断的范围,请明确说明。"); summary.append("\n库位概况:总库位 ") .append(totalLoc) .append(" 个;状态分布:") .append(formatLocStatuses(locStatusCounters)) .append("。"); summary.append("\n库存概况:库存记录 ") .append(itemRows) .append(" 条,覆盖库位 ") .append(locCodes.size()) .append(" 个,涉及物料 ") .append(materialCodes.size()) .append(" 种,总数量 ") .append(formatDecimal(totalQty)) .append("。"); if (!topLocRows.isEmpty()) { summary.append("\n库存最多的库位 TOP5:") .append(formatTopLocs(topLocRows)) .append("。"); } if (!topMaterialRows.isEmpty()) { summary.append("\n库存最多的物料 TOP5:") .append(formatTopMaterials(topMaterialRows)) .append("。"); } return summary.toString(); } /** * 将库位状态计数格式化为可读文本。 */ private String formatLocStatuses(Map counters) { if (counters == null || counters.isEmpty()) { return "暂无数据"; } List parts = new ArrayList<>(); for (Map.Entry entry : LOC_STATUS_LABELS.entrySet()) { parts.add(entry.getValue() + " " + counters.getOrDefault(entry.getKey(), 0L) + " 个"); } return String.join(",", parts); } /** * 格式化库存最多的库位列表。 */ private String formatTopLocs(List> rows) { List parts = new ArrayList<>(); for (Map.Entry row : rows) { parts.add(row.getKey() + "(数量 " + formatDecimal(row.getValue().totalQty) + ",物料 " + row.getValue().materialCodes.size() + " 种)"); } return String.join(";", parts); } /** * 格式化库存最多的物料列表。 */ private String formatTopMaterials(List rows) { List parts = new ArrayList<>(); for (MaterialAggregate row : rows) { String matnrCode = row.matnrCode; String maktx = Objects.toString(row.maktx, ""); String title = maktx == null || maktx.trim().isEmpty() ? matnrCode : matnrCode + "/" + maktx; parts.add(title + "(数量 " + formatDecimal(row.totalQty) + ",分布库位 " + row.locCodes.size() + " 个)"); } return String.join(";", parts); } /** * 统一格式化数量值。 */ private String formatDecimal(Object value) { BigDecimal decimal = toDecimal(value); return decimal.stripTrailingZeros().toPlainString(); } /** * 将不同类型的数量字段统一转换为 BigDecimal。 */ private BigDecimal toDecimal(Object value) { if (value == null) { return BigDecimal.ZERO; } if (value instanceof BigDecimal) { return (BigDecimal) value; } if (value instanceof Number) { return BigDecimal.valueOf(((Number) value).doubleValue()); } return new BigDecimal(String.valueOf(value)); } private static class LocAggregate { private BigDecimal totalQty = BigDecimal.ZERO; private final Set materialCodes = new HashSet<>(); } private static class MaterialAggregate { private String matnrCode; private String maktx; private BigDecimal totalQty = BigDecimal.ZERO; private final Set locCodes = new HashSet<>(); } }