package com.vincent.rsf.server.ai.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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 AiPromptContextProvider { 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 boolean supports(AiPromptContext context) { return context != null && shouldSummarize(context.getQuestion()); } @Override public String buildContext(AiPromptContext context) { if (!supports(context)) { return ""; } 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 boolean shouldSummarize(String question) { if (question == null || question.trim().isEmpty()) { return false; } String normalized = question.toLowerCase(Locale.ROOT); return normalized.contains("loc") || normalized.contains("库位") || normalized.contains("货位") || normalized.contains("库区") || normalized.contains("库存") || normalized.contains("物料") || normalized.contains("巷道") || normalized.contains("储位"); } 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(); } 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<>(); } }