Files
klp-oa/klp-admin/src/main/java/com/klp/wms/WmsSpecSyncController.java

503 lines
27 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<WmsProcessSpecVersionVo> 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<String, Object> 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);
}
/**
* 规程同步分页列表。
* 以 L3WMS MySQL为主维度只展示在 wms_coil_pending_action.processed_coil_ids
* 中出现过的钢卷(即生产后处理过的),再按 enter_coil_no = HOT_COILID 从 L2Oracle
* 批量富化计划维度(入口/出口厚宽、钢种等)及上线/下线时间。
*/
@GetMapping("/pageList")
public R<Map<String, Object>> 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<WmsProcessSpecVersionVo> allActive = specVersionService.queryActiveVersionsEnriched(null);
List<WmsProcessSpecVersionVo> globalCandidates = lineId != null
? allActive.stream().filter(v -> lineId.equals(v.getLineId())).collect(Collectors.toList())
: allActive;
// 全部版本(含历史版本):用于展示已绑定规程名称,避免旧版本查不到
Map<Long, WmsProcessSpecVersionVo> allVersionById = specVersionService.queryAllVersionsEnriched().stream()
.collect(Collectors.toMap(WmsProcessSpecVersionVo::getVersionId, v -> v, (a, b) -> a));
// specCode 过滤 → specId 集合(后置内存过滤,从全部版本里搜)
final Set<Long> 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<lineId> ───────────────────
// 有效产线wms_production_line.action_type IS NOT NULL即配置了操作类型的产线
List<WmsProductionLine> allLines = productionLineMapper.selectList(
Wrappers.<WmsProductionLine>lambdaQuery()
.isNotNull(WmsProductionLine::getActionType));
Map<Integer, Set<Long>> 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<Integer> validActionTypes = actionTypeToLineIds.keySet();
Set<Long> typedCoilIds = new LinkedHashSet<>();
Map<Long, Integer> allCoilIdToActionType = new HashMap<>();
if (!validActionTypes.isEmpty()) {
List<WmsCoilPendingAction> 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<Long, Integer> 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<String, Object> emptyResult = new LinkedHashMap<>();
emptyResult.put("rows", Collections.emptyList());
emptyResult.put("total", 0L);
return R.ok(emptyResult);
}
// ── 4a. 整体汇总统计(忽略 syncStatus 过滤,体现全量分布) ─────────────────
Map<String, Long> 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<WmsMaterialCoilVo> l3Coils = materialCoilService.queryByProcessedCoilIds(
typedCoilIds, enterCoilNo, currentCoilNo, material, qualityStatus,
effectiveSyncStatus, filterSpecIds, offset, pageSize);
if (l3Coils.isEmpty()) {
Map<String, Object> 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<String> enterCoilNos = new ArrayList<>();
Set<String> seenNos = new HashSet<>();
for (WmsMaterialCoilVo c : l3Coils) {
String primary = primaryEnterCoilNo(c.getEnterCoilNo());
if (primary != null && seenNos.add(primary)) enterCoilNos.add(primary);
}
Map<String, Map<String, Object>> l2DimMap =
sqlServerApiBusinessService.getPlanDimsByHotCoilIds(enterCoilNos);
// ── 6. 上线/下线时间PLTCM_PDO_EXCOIL.EXCOILID = PLTCM_PDI_PLAN.COILID ──
List<String> l2CoilIds = new ArrayList<>();
for (Map<String, Object> dims : l2DimMap.values()) {
String cid = toStr(dims, "COILID", "coilid");
if (StringUtils.hasText(cid)) l2CoilIds.add(cid);
}
Map<String, Map<String, Object>> excoilTimeMap =
sqlServerApiBusinessService.getExcoilTimesByCoilIds(l2CoilIds);
// ── 7. 组装行 ──────────────────────────────────────────────────────────────
List<Map<String, Object>> rows = new ArrayList<>();
for (WmsMaterialCoilVo coil : l3Coils) {
Map<String, Object> 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<String, Object> 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<String, Object> 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<Long> allowedLineIds = pendingAt != null
? actionTypeToLineIds.getOrDefault(pendingAt, Collections.emptySet())
: null;
// 优先用同产线规程;若过滤后为空则兜底到全量活跃版本
List<WmsProcessSpecVersionVo> 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<String, Object> 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<String, Object> 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的条目。
* 额外按入场卷号从 L2Oracle查询计划/实绩维度,供核查人员判断二级是否已有实绩。
*/
@GetMapping("/untypedPageList")
public R<Map<String, Object>> 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<WmsProductionLine> lines = productionLineMapper.selectList(
Wrappers.<WmsProductionLine>lambdaQuery().isNotNull(WmsProductionLine::getActionType));
Set<Integer> 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<String, Object> empty = new LinkedHashMap<>();
empty.put("rows", Collections.emptyList());
empty.put("total", 0L);
return R.ok(empty);
}
// ── 2. 分页查询未录入待操作 ────────────────────────────────────────────────
// 条件1action_status IN (0,1) —— 0=待处理/1=进行中2=已完成/3=已取消均排除
// 条件2processed_coil_ids 为空 —— 只有未写入产出卷 ID 的记录才是尚未录入的
QueryWrapper<WmsCoilPendingAction> 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<WmsCoilPendingActionVo> voPage =
pendingActionMapper.selectVoPagePlus(Page.of(pageNum, pageSize), qw);
List<WmsCoilPendingActionVo> paList = voPage.getRecords();
if (paList.isEmpty()) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("rows", Collections.emptyList());
empty.put("total", voPage.getTotal());
return R.ok(empty);
}
// ── 3. L2 富化 ─────────────────────────────────────────────────────────
List<String> nos = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (WmsCoilPendingActionVo pa : paList) {
String primary = primaryEnterCoilNo(pa.getEnterCoilNo());
if (primary != null && seen.add(primary)) nos.add(primary);
}
Map<String, Map<String, Object>> l2DimMap = nos.isEmpty()
? Collections.emptyMap()
: sqlServerApiBusinessService.getPlanDimsByHotCoilIds(nos);
// ── 4. 组装行 ──────────────────────────────────────────────────────────
List<Map<String, Object>> rows = new ArrayList<>();
for (WmsCoilPendingActionVo pa : paList) {
String primaryNo = primaryEnterCoilNo(pa.getEnterCoilNo());
Map<String, Object> l2row = primaryNo != null ? l2DimMap.get(primaryNo) : null;
Map<String, Object> 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<String, Object> 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<Void> syncSpec(@RequestBody Map<String, Object> body) {
List<Map<String, Object>> bindings = (List<Map<String, Object>>) body.get("bindings");
if (bindings == null || bindings.isEmpty()) {
return R.fail("bindings 不能为空");
}
for (Map<String, Object> 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<String, Object> 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<String, Object> row, String upperKey, String lowerKey) {
Object val = row.getOrDefault(upperKey, row.get(lowerKey));
return val == null ? null : val.toString().trim();
}
private long toLong(Map<String, Object> 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; }
}
}