package com.klp.wms; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.klp.common.core.controller.BaseController; import com.klp.common.core.domain.R; import com.klp.domain.WmsCoilPendingAction; import com.klp.domain.WmsProductionLine; import com.klp.domain.bo.WmsMaterialCoilBo; import com.klp.domain.vo.WmsCoilPendingActionVo; import com.klp.domain.vo.WmsMaterialCoilVo; import com.klp.domain.vo.WmsProcessSpecVersionVo; import com.klp.framework.service.SqlServerApiBusinessService; import com.klp.mapper.WmsCoilPendingActionMapper; import com.klp.mapper.WmsProductionLineMapper; import com.klp.service.IWmsMaterialCoilService; import com.klp.service.IWmsProcessSpecVersionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; /** * 规程同步接口(跨模块:同时依赖 klp-admin 的 L2 服务和 klp-wms 的规程/钢卷服务) */ @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/wms/specSync") public class WmsSpecSyncController extends BaseController { private final SqlServerApiBusinessService sqlServerApiBusinessService; private final IWmsProcessSpecVersionService specVersionService; private final IWmsMaterialCoilService materialCoilService; private final WmsProductionLineMapper productionLineMapper; private final WmsCoilPendingActionMapper pendingActionMapper; /** * 根据热卷号匹配最佳规程版本(供 typing.vue 在 L2 填入后调用) */ @GetMapping("/matchBest") public R matchBest(@RequestParam String hotCoilId, @RequestParam(required = false) Long lineId) { SqlServerApiBusinessService.PlanDetailView plan = sqlServerApiBusinessService.getPlanByHotCoilId(hotCoilId); if (plan == null || plan.getFirstRow().isEmpty()) { return R.ok(null); } Map row = plan.getFirstRow(); BigDecimal entryThick = toBigDecimal(row, "ENTRY_THICK", "entry_thick"); BigDecimal exitThick = toBigDecimal(row, "EXIT_THICK", "exit_thick"); BigDecimal entryWidth = toBigDecimal(row, "ENTRY_WIDTH", "entry_width"); BigDecimal exitWidth = toBigDecimal(row, "EXIT_WIDTH", "exit_width"); String grade = toStr(row, "GRADE", "grade"); WmsProcessSpecVersionVo best = specVersionService.matchBestVersion( entryThick, exitThick, entryWidth, exitWidth, grade, lineId); return R.ok(best); } /** * 规程同步分页列表。 * 以 L3(WMS MySQL)为主维度:只展示在 wms_coil_pending_action.processed_coil_ids * 中出现过的钢卷(即生产后处理过的),再按 enter_coil_no = HOT_COILID 从 L2(Oracle) * 批量富化计划维度(入口/出口厚宽、钢种等)及上线/下线时间。 */ @GetMapping("/pageList") public R> pageList( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "40") int pageSize, @RequestParam(required = false) String enterCoilNo, @RequestParam(required = false) String currentCoilNo, @RequestParam(required = false) String material, @RequestParam(required = false) String qualityStatus, @RequestParam(required = false) String specCode, @RequestParam(required = false) String syncStatus, @RequestParam(required = false) Long lineId) { // ── 1. 预加载规程版本 ────────────────────────────────────────────────── // 活跃版本:仅用于推荐候选匹配 List allActive = specVersionService.queryActiveVersionsEnriched(null); List globalCandidates = lineId != null ? allActive.stream().filter(v -> lineId.equals(v.getLineId())).collect(Collectors.toList()) : allActive; // 全部版本(含历史版本):用于展示已绑定规程名称,避免旧版本查不到 Map allVersionById = specVersionService.queryAllVersionsEnriched().stream() .collect(Collectors.toMap(WmsProcessSpecVersionVo::getVersionId, v -> v, (a, b) -> a)); // specCode 过滤 → specId 集合(后置内存过滤,从全部版本里搜) final Set filterSpecIds; if (StringUtils.hasText(specCode)) { final String kw = specCode.trim().toLowerCase(); filterSpecIds = allVersionById.values().stream() .filter(v -> (v.getSpecCode() != null && v.getSpecCode().toLowerCase().contains(kw)) || (v.getSpecName() != null && v.getSpecName().toLowerCase().contains(kw))) .map(WmsProcessSpecVersionVo::getSpecId) .collect(Collectors.toSet()); } else { filterSpecIds = null; } // ── 2. 预加载产线表 → actionType(int) → Set ─────────────────── // 有效产线:wms_production_line.action_type IS NOT NULL(即配置了操作类型的产线) List allLines = productionLineMapper.selectList( Wrappers.lambdaQuery() .isNotNull(WmsProductionLine::getActionType)); Map> actionTypeToLineIds = new HashMap<>(); for (WmsProductionLine pl : allLines) { if (!StringUtils.hasText(pl.getActionType())) continue; for (String part : pl.getActionType().split(",")) { try { int at = Integer.parseInt(part.trim()); actionTypeToLineIds.computeIfAbsent(at, k -> new HashSet<>()).add(pl.getLineId()); } catch (NumberFormatException ignore) { /* 跳过非数字 */ } } } // ── 3. 展开已录入(action_status=2)待操作的 processed_coil_ids → 输出钢卷 ID ── Set validActionTypes = actionTypeToLineIds.keySet(); Set typedCoilIds = new LinkedHashSet<>(); Map allCoilIdToActionType = new HashMap<>(); if (!validActionTypes.isEmpty()) { List allPaList = pendingActionMapper.selectAllProcessedCoilIdsAndActionStatus(); for (WmsCoilPendingAction pa : allPaList) { if (pa.getActionType() == null || !validActionTypes.contains(pa.getActionType())) continue; if (!StringUtils.hasText(pa.getProcessedCoilIds())) continue; for (String idStr : pa.getProcessedCoilIds().split(",")) { try { long pid = Long.parseLong(idStr.trim()); typedCoilIds.add(pid); allCoilIdToActionType.putIfAbsent(pid, pa.getActionType()); } catch (NumberFormatException ignore) { /* 跳过非数字 */ } } } } final Map coilIdToActionType = allCoilIdToActionType; // ── 4. 计数分页(仅已录入钢卷,按规程绑定状态过滤)──────────────────────── final String effectiveSyncStatus; if ("synced".equals(syncStatus)) effectiveSyncStatus = "synced"; else if ("unsynced".equals(syncStatus)) effectiveSyncStatus = "unsynced"; else effectiveSyncStatus = null; if (typedCoilIds.isEmpty()) { Map emptyResult = new LinkedHashMap<>(); emptyResult.put("rows", Collections.emptyList()); emptyResult.put("total", 0L); return R.ok(emptyResult); } // ── 4a. 整体汇总统计(忽略 syncStatus 过滤,体现全量分布) ───────────────── Map overallStats = materialCoilService.getOverallSyncStats( typedCoilIds, enterCoilNo, currentCoilNo, material, qualityStatus, filterSpecIds); int offset = (pageNum - 1) * pageSize; long total = materialCoilService.countByProcessedCoilIds( typedCoilIds, enterCoilNo, currentCoilNo, material, qualityStatus, effectiveSyncStatus, filterSpecIds); List l3Coils = materialCoilService.queryByProcessedCoilIds( typedCoilIds, enterCoilNo, currentCoilNo, material, qualityStatus, effectiveSyncStatus, filterSpecIds, offset, pageSize); if (l3Coils.isEmpty()) { Map emptyResult = new LinkedHashMap<>(); emptyResult.put("rows", Collections.emptyList()); emptyResult.put("total", total); emptyResult.put("overallStats", overallStats); return R.ok(emptyResult); } // ── 5. L2 富化:批量按 enter_coil_no(= HOT_COILID)查询计划维度 ────────── List enterCoilNos = new ArrayList<>(); Set seenNos = new HashSet<>(); for (WmsMaterialCoilVo c : l3Coils) { String primary = primaryEnterCoilNo(c.getEnterCoilNo()); if (primary != null && seenNos.add(primary)) enterCoilNos.add(primary); } Map> l2DimMap = sqlServerApiBusinessService.getPlanDimsByHotCoilIds(enterCoilNos); // ── 6. 上线/下线时间:PLTCM_PDO_EXCOIL.EXCOILID = PLTCM_PDI_PLAN.COILID ── List l2CoilIds = new ArrayList<>(); for (Map dims : l2DimMap.values()) { String cid = toStr(dims, "COILID", "coilid"); if (StringUtils.hasText(cid)) l2CoilIds.add(cid); } Map> excoilTimeMap = sqlServerApiBusinessService.getExcoilTimesByCoilIds(l2CoilIds); // ── 7. 组装行 ────────────────────────────────────────────────────────────── List> rows = new ArrayList<>(); for (WmsMaterialCoilVo coil : l3Coils) { Map row = new LinkedHashMap<>(); String primaryNo = primaryEnterCoilNo(coil.getEnterCoilNo()); // ── L3 基础字段 ── row.put("wmsCoilId", coil.getCoilId()); row.put("enterCoilNo", primaryNo); row.put("currentCoilNo", coil.getCurrentCoilNo()); row.put("supplierCoilNo", coil.getSupplierCoilNo()); row.put("netWeight", coil.getNetWeight()); row.put("actualThickness", coil.getActualThickness()); row.put("actualWidth", coil.getActualWidth()); row.put("qualityStatus", coil.getQualityStatus()); row.put("specification", coil.getSpecification()); row.put("material", coil.getMaterial()); // 是否已流转(data_type=0) boolean movedOn = Integer.valueOf(0).equals(coil.getDataType()); row.put("movedOn", movedOn); row.put("dataType", coil.getDataType()); // ── L2 数据有则富化,无则留空(不跳过,避免破坏分页偏移)── Map l2row = primaryNo != null ? l2DimMap.get(primaryNo) : null; BigDecimal entryThick = null, exitThick = null, entryWidth = null, exitWidth = null; String grade = null, l2CoilId = null; if (l2row != null) { entryThick = toBigDecimal(l2row, "ENTRY_THICK", "entry_thick"); exitThick = toBigDecimal(l2row, "EXIT_THICK", "exit_thick"); entryWidth = toBigDecimal(l2row, "ENTRY_WIDTH", "entry_width"); exitWidth = toBigDecimal(l2row, "EXIT_WIDTH", "exit_width"); grade = toStr(l2row, "GRADE", "grade"); l2CoilId = toStr(l2row, "COILID", "coilid"); row.put("entryThick", entryThick); row.put("exitThick", exitThick); row.put("entryWidth", entryWidth); row.put("exitWidth", exitWidth); row.put("grade", grade); row.put("processCode", toStr(l2row, "PROCESS_CODE", "process_code")); row.put("l2CoilId", l2CoilId); row.put("l2Found", true); } // 上线/下线时间 Map excoilTimes = StringUtils.hasText(l2CoilId) ? excoilTimeMap.get(l2CoilId) : null; if (excoilTimes != null) { row.put("onlineTime", excoilTimes.getOrDefault("START_DATE", excoilTimes.get("start_date"))); row.put("offlineTime", excoilTimes.getOrDefault("END_DATE", excoilTimes.get("end_date"))); } // ── 规程绑定状态 ── if (coil.getVersionId() != null) { WmsProcessSpecVersionVo bound = allVersionById.get(coil.getVersionId()); if (bound != null) { row.put("specCode", bound.getSpecCode()); row.put("specName", bound.getSpecName()); row.put("versionCode", bound.getVersionCode()); putMatchConds(row, bound); } row.put("syncStatus", "synced"); } else { row.put("syncStatus", "unsynced"); // 按产线限定候选规程(lineId=null 的规程不限定产线,始终纳入) Integer pendingAt = coilIdToActionType.get(coil.getCoilId()); Set allowedLineIds = pendingAt != null ? actionTypeToLineIds.getOrDefault(pendingAt, Collections.emptySet()) : null; // 优先用同产线规程;若过滤后为空则兜底到全量活跃版本 List candidates; if (allowedLineIds == null || allowedLineIds.isEmpty()) { candidates = globalCandidates; } else { candidates = globalCandidates.stream() .filter(v -> v.getLineId() == null || allowedLineIds.contains(v.getLineId())) .collect(Collectors.toList()); if (candidates.isEmpty()) { candidates = globalCandidates; // 兜底:产线无匹配规程时展示全部 } } WmsProcessSpecVersionVo best = null; int bestScore = -1; for (WmsProcessSpecVersionVo v : candidates) { int s = scoreVersion(v, entryThick, exitThick, entryWidth, exitWidth, grade); if (s > bestScore) { bestScore = s; best = v; } } // bestScore >= 0:只要存在候选规程就展示推荐(0 = 无维度条件匹配但仍有版本可绑) if (best != null && bestScore >= 0) { row.put("candidateSpecCode", best.getSpecCode()); row.put("candidateSpecName", best.getSpecName()); row.put("candidateVersionCode", best.getVersionCode()); row.put("candidateVersionId", best.getVersionId()); row.put("candidateSpecId", best.getSpecId()); putMatchConds(row, best); } } rows.add(row); } Map result = new LinkedHashMap<>(); result.put("rows", rows); result.put("total", total); result.put("overallStats", overallStats); return R.ok(result); } // ── 私有工具方法 ───────────────────────────────────────────────────────────── /** * enter_coil_no 字段可能存储了多个逗号分隔的热卷号(如 "26032550,26032550")。 * 取第一个非空值作为主键,用于展示和 L2 关联查询。 */ private String primaryEnterCoilNo(String raw) { if (!StringUtils.hasText(raw)) return null; String first = raw.split(",")[0].trim(); return first.isEmpty() ? null : first; } private int scoreVersion(WmsProcessSpecVersionVo v, BigDecimal entryThick, BigDecimal exitThick, BigDecimal entryWidth, BigDecimal exitWidth, String grade) { int score = 0; if (entryThick != null && v.getMatchEntryThickMin() != null && v.getMatchEntryThickMax() != null && entryThick.compareTo(v.getMatchEntryThickMin()) >= 0 && entryThick.compareTo(v.getMatchEntryThickMax()) <= 0) score++; if (exitThick != null && v.getMatchExitThickMin() != null && v.getMatchExitThickMax() != null && exitThick.compareTo(v.getMatchExitThickMin()) >= 0 && exitThick.compareTo(v.getMatchExitThickMax()) <= 0) score++; if (entryWidth != null && v.getMatchEntryWidthMin() != null && v.getMatchEntryWidthMax() != null && entryWidth.compareTo(v.getMatchEntryWidthMin()) >= 0 && entryWidth.compareTo(v.getMatchEntryWidthMax()) <= 0) score++; if (exitWidth != null && v.getMatchExitWidthMin() != null && v.getMatchExitWidthMax() != null && exitWidth.compareTo(v.getMatchExitWidthMin()) >= 0 && exitWidth.compareTo(v.getMatchExitWidthMax()) <= 0) score++; if (StringUtils.hasText(grade) && StringUtils.hasText(v.getMatchSteelGrade()) && grade.toLowerCase().contains(v.getMatchSteelGrade().toLowerCase())) score++; return score; } private void putMatchConds(Map row, WmsProcessSpecVersionVo v) { row.put("matchEntryThickMin", v.getMatchEntryThickMin()); row.put("matchEntryThickMax", v.getMatchEntryThickMax()); row.put("matchExitThickMin", v.getMatchExitThickMin()); row.put("matchExitThickMax", v.getMatchExitThickMax()); row.put("matchEntryWidthMin", v.getMatchEntryWidthMin()); row.put("matchEntryWidthMax", v.getMatchEntryWidthMax()); row.put("matchExitWidthMin", v.getMatchExitWidthMin()); row.put("matchExitWidthMax", v.getMatchExitWidthMax()); row.put("matchSteelGrade", v.getMatchSteelGrade()); } /** * 待录入核查分页列表。 * 数据源:wms_production_line 有效产线对应的待操作记录中 action_status != 2(尚未完成 typing)的条目。 * 额外按入场卷号从 L2(Oracle)查询计划/实绩维度,供核查人员判断二级是否已有实绩。 */ @GetMapping("/untypedPageList") public R> untypedPageList( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "40") int pageSize, @RequestParam(required = false) String enterCoilNo, @RequestParam(required = false) String currentCoilNo, @RequestParam(required = false) String operatorName) { // ── 1. 有效产线 actionType ────────────────────────────────────────────── List lines = productionLineMapper.selectList( Wrappers.lambdaQuery().isNotNull(WmsProductionLine::getActionType)); Set validAts = new HashSet<>(); for (WmsProductionLine pl : lines) { if (!StringUtils.hasText(pl.getActionType())) continue; for (String part : pl.getActionType().split(",")) { try { validAts.add(Integer.parseInt(part.trim())); } catch (NumberFormatException ignore) {} } } if (validAts.isEmpty()) { Map empty = new LinkedHashMap<>(); empty.put("rows", Collections.emptyList()); empty.put("total", 0L); return R.ok(empty); } // ── 2. 分页查询未录入待操作 ──────────────────────────────────────────────── // 条件1:action_status IN (0,1) —— 0=待处理/1=进行中,2=已完成/3=已取消均排除 // 条件2:processed_coil_ids 为空 —— 只有未写入产出卷 ID 的记录才是尚未录入的 QueryWrapper qw = new QueryWrapper<>(); qw.eq("wcpa.del_flag", 0); qw.in("wcpa.action_type", validAts); qw.in("wcpa.action_status", java.util.Arrays.asList(0, 1)); qw.and(w -> w.isNull("wcpa.processed_coil_ids") .or().eq("wcpa.processed_coil_ids", "")); if (StringUtils.hasText(enterCoilNo)) qw.like("wmc.enter_coil_no", enterCoilNo); if (StringUtils.hasText(currentCoilNo)) qw.like("wcpa.current_coil_no", currentCoilNo); if (StringUtils.hasText(operatorName)) qw.like("wcpa.operator_name", operatorName); qw.orderByDesc("wcpa.scan_time"); Page voPage = pendingActionMapper.selectVoPagePlus(Page.of(pageNum, pageSize), qw); List paList = voPage.getRecords(); if (paList.isEmpty()) { Map empty = new LinkedHashMap<>(); empty.put("rows", Collections.emptyList()); empty.put("total", voPage.getTotal()); return R.ok(empty); } // ── 3. L2 富化 ───────────────────────────────────────────────────────── List nos = new ArrayList<>(); Set seen = new HashSet<>(); for (WmsCoilPendingActionVo pa : paList) { String primary = primaryEnterCoilNo(pa.getEnterCoilNo()); if (primary != null && seen.add(primary)) nos.add(primary); } Map> l2DimMap = nos.isEmpty() ? Collections.emptyMap() : sqlServerApiBusinessService.getPlanDimsByHotCoilIds(nos); // ── 4. 组装行 ────────────────────────────────────────────────────────── List> rows = new ArrayList<>(); for (WmsCoilPendingActionVo pa : paList) { String primaryNo = primaryEnterCoilNo(pa.getEnterCoilNo()); Map l2row = primaryNo != null ? l2DimMap.get(primaryNo) : null; Map row = new LinkedHashMap<>(); row.put("actionId", pa.getActionId()); row.put("coilId", pa.getCoilId()); row.put("actionType", pa.getActionType()); row.put("actionStatus", pa.getActionStatus()); row.put("scanTime", pa.getScanTime()); row.put("createTime", pa.getCreateTime()); row.put("operatorName", pa.getOperatorName()); row.put("warehouseName", pa.getWarehouseName()); row.put("enterCoilNo", primaryNo); row.put("currentCoilNo", pa.getCurrentCoilNo()); row.put("supplierCoilNo", pa.getSupplierCoilNo()); row.put("material", pa.getMaterial()); row.put("specification", pa.getSpecification()); row.put("itemName", pa.getItemName()); if (l2row != null) { row.put("entryThick", toBigDecimal(l2row, "ENTRY_THICK", "entry_thick")); row.put("exitThick", toBigDecimal(l2row, "EXIT_THICK", "exit_thick")); row.put("entryWidth", toBigDecimal(l2row, "ENTRY_WIDTH", "entry_width")); row.put("exitWidth", toBigDecimal(l2row, "EXIT_WIDTH", "exit_width")); row.put("grade", toStr(l2row, "GRADE", "grade")); row.put("processCode", toStr(l2row, "PROCESS_CODE", "process_code")); row.put("l2Found", true); } else { row.put("l2Found", false); } rows.add(row); } Map result = new LinkedHashMap<>(); result.put("rows", rows); result.put("total", voPage.getTotal()); return R.ok(result); } /** * 批量同步规程绑定。 * 请求体:{ "bindings": [{ "coilId": 123, "specId": 456, "versionId": 789 }, ...] } * 前端已完成匹配计算,直接写入指定的 specId/versionId,不重新跑匹配算法。 */ @PostMapping("/syncSpec") @SuppressWarnings("unchecked") public R syncSpec(@RequestBody Map body) { List> bindings = (List>) body.get("bindings"); if (bindings == null || bindings.isEmpty()) { return R.fail("bindings 不能为空"); } for (Map b : bindings) { try { Long coilId = toLong(b, "coilId"); Long specId = toLong(b, "specId"); Long versionId = toLong(b, "versionId"); if (coilId == 0L || specId == 0L || versionId == 0L) continue; WmsMaterialCoilBo bo = new WmsMaterialCoilBo(); bo.setCoilId(coilId); bo.setSpecId(specId); bo.setVersionId(versionId); materialCoilService.updateSimple(bo); } catch (Exception e) { log.warn("规程同步失败 binding={}", b, e); } } return R.ok(); } private BigDecimal toBigDecimal(Map row, String upperKey, String lowerKey) { Object val = row.getOrDefault(upperKey, row.get(lowerKey)); if (val == null) return null; try { return new BigDecimal(val.toString()); } catch (NumberFormatException e) { return null; } } private String toStr(Map row, String upperKey, String lowerKey) { Object val = row.getOrDefault(upperKey, row.get(lowerKey)); return val == null ? null : val.toString().trim(); } private long toLong(Map row, String key) { if (row == null) return 0L; Object v = row.get(key); if (v == null) return 0L; try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return 0L; } } }