Compare commits

...

2 Commits

Author SHA1 Message Date
cd099f2e6b Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
# Conflicts:
#	klp-admin/src/main/resources/application-prod.yml
2026-05-23 19:35:22 +08:00
35ad50a79d 同步规程同步代码和录入监测代码 2026-05-23 19:34:52 +08:00
28 changed files with 2355 additions and 327 deletions

16
docs/spec-match-ddl.sql Normal file
View File

@@ -0,0 +1,16 @@
-- 规程版本新增匹配条件字段
ALTER TABLE wms_process_spec_version
ADD COLUMN match_entry_thick_min DECIMAL(8,3) NULL COMMENT '来料厚度下限(mm)',
ADD COLUMN match_entry_thick_max DECIMAL(8,3) NULL COMMENT '来料厚度上限(mm)',
ADD COLUMN match_exit_thick_min DECIMAL(8,3) NULL COMMENT '出口厚度下限(mm)',
ADD COLUMN match_exit_thick_max DECIMAL(8,3) NULL COMMENT '出口厚度上限(mm)',
ADD COLUMN match_entry_width_min DECIMAL(8,2) NULL COMMENT '来料宽度下限(mm)',
ADD COLUMN match_entry_width_max DECIMAL(8,2) NULL COMMENT '来料宽度上限(mm)',
ADD COLUMN match_exit_width_min DECIMAL(8,2) NULL COMMENT '出口宽度下限(mm)',
ADD COLUMN match_exit_width_max DECIMAL(8,2) NULL COMMENT '出口宽度上限(mm)',
ADD COLUMN match_steel_grade VARCHAR(100) NULL COMMENT '钢种关键字(模糊匹配)';
-- 钢卷主表新增规程绑定字段
ALTER TABLE wms_material_coil
ADD COLUMN spec_id BIGINT NULL COMMENT '绑定的规程ID',
ADD COLUMN version_id BIGINT NULL COMMENT '绑定的规程版本ID';

128
docs/spec-sample-data.sql Normal file
View File

@@ -0,0 +1,128 @@
-- ============================================================
-- 规程匹配测试样本数据
-- 执行前请确认SELECT spec_id, spec_code FROM wms_process_spec;
-- SELECT * FROM wms_production_line WHERE line_id = 1;
-- ============================================================
-- 先查看现有规程,避免重复
-- SELECT * FROM wms_process_spec WHERE line_id = 1;
-- SELECT * FROM wms_process_spec_version WHERE is_active = 1;
-- ─────────────────────────────────────────────────
-- 规程1酸轧通用规程宽范围覆盖大多数钢卷
-- ─────────────────────────────────────────────────
INSERT INTO wms_process_spec (
spec_code, spec_name, spec_type, line_id,
product_type, is_enabled, del_flag,
remark, create_by, create_time, update_by, update_time
) VALUES (
'ACL-STD-001', '酸轧通用规程', 'PROCESS', 1,
'CR', 1, 0,
'通用匹配规程,覆盖常规酸轧来料范围', 'admin', NOW(), 'admin', NOW()
);
-- 获取刚插入的 spec_id
SET @spec_id_std = LAST_INSERT_ID();
-- 版本V1生效版本宽范围
INSERT INTO wms_process_spec_version (
spec_id, version_code, is_active, status, del_flag,
match_entry_thick_min, match_entry_thick_max,
match_exit_thick_min, match_exit_thick_max,
match_entry_width_min, match_entry_width_max,
match_exit_width_min, match_exit_width_max,
match_steel_grade,
remark, create_by, create_time, update_by, update_time
) VALUES (
@spec_id_std, 'V1.0', 1, 1, 0,
1.500, 8.000, -- 来料厚度范围 mm热卷厚度典型值 2~6mm留余量
0.200, 4.000, -- 出口厚度范围 mm冷轧成品典型值 0.3~2mm
700.00, 1700.00, -- 来料宽度范围 mm
700.00, 1700.00, -- 出口宽度范围 mm
NULL, -- 钢种为空 = 不限钢种
'通用版本,宽范围覆盖', 'admin', NOW(), 'admin', NOW()
);
-- ─────────────────────────────────────────────────
-- 规程2酸轧薄规格规程出口厚度 ≤ 1.0mm
-- ─────────────────────────────────────────────────
INSERT INTO wms_process_spec (
spec_code, spec_name, spec_type, line_id,
product_type, is_enabled, del_flag,
remark, create_by, create_time, update_by, update_time
) VALUES (
'ACL-THIN-001', '酸轧薄规格规程', 'PROCESS', 1,
'CR', 1, 0,
'薄规格产品专用出口厚度不超过1.0mm', 'admin', NOW(), 'admin', NOW()
);
SET @spec_id_thin = LAST_INSERT_ID();
-- 版本V1生效版本出口厚度 ≤ 1.0mm
INSERT INTO wms_process_spec_version (
spec_id, version_code, is_active, status, del_flag,
match_entry_thick_min, match_entry_thick_max,
match_exit_thick_min, match_exit_thick_max,
match_entry_width_min, match_entry_width_max,
match_exit_width_min, match_exit_width_max,
match_steel_grade,
remark, create_by, create_time, update_by, update_time
) VALUES (
@spec_id_thin, 'V1.0', 1, 1, 0,
2.000, 5.000, -- 来料厚度
0.200, 1.000, -- 出口厚度(薄规格)
800.00, 1500.00, -- 来料宽度
800.00, 1500.00, -- 出口宽度
NULL, -- 不限钢种
'薄规格专用版本', 'admin', NOW(), 'admin', NOW()
);
-- ─────────────────────────────────────────────────
-- 规程3高强钢规程含钢种匹配
-- ─────────────────────────────────────────────────
INSERT INTO wms_process_spec (
spec_code, spec_name, spec_type, line_id,
product_type, is_enabled, del_flag,
remark, create_by, create_time, update_by, update_time
) VALUES (
'ACL-HSS-001', '酸轧高强钢规程', 'PROCESS', 1,
'CR', 1, 0,
'高强度结构钢专用规程', 'admin', NOW(), 'admin', NOW()
);
SET @spec_id_hss = LAST_INSERT_ID();
INSERT INTO wms_process_spec_version (
spec_id, version_code, is_active, status, del_flag,
match_entry_thick_min, match_entry_thick_max,
match_exit_thick_min, match_exit_thick_max,
match_entry_width_min, match_entry_width_max,
match_exit_width_min, match_exit_width_max,
match_steel_grade,
remark, create_by, create_time, update_by, update_time
) VALUES (
@spec_id_hss, 'V1.0', 1, 1, 0,
2.500, 7.000,
0.500, 2.500,
900.00, 1600.00,
900.00, 1600.00,
'Q', -- 匹配钢种含 "Q" 的Q235、Q345、Q420 等)
'高强钢版本钢种含Q', 'admin', NOW(), 'admin', NOW()
);
-- ─────────────────────────────────────────────────
-- 验证查询
-- ─────────────────────────────────────────────────
SELECT
s.spec_id, s.spec_code, s.spec_name, s.line_id,
v.version_id, v.version_code, v.is_active,
v.match_entry_thick_min, v.match_entry_thick_max,
v.match_exit_thick_min, v.match_exit_thick_max,
v.match_entry_width_min, v.match_entry_width_max,
v.match_steel_grade
FROM wms_process_spec s
JOIN wms_process_spec_version v ON s.spec_id = v.spec_id
WHERE s.line_id = 1
AND s.del_flag = 0
AND v.del_flag = 0
ORDER BY s.spec_id, v.version_code;

View File

@@ -361,6 +361,59 @@ public class SqlServerApiClient {
);
}
public ExecuteSqlResponse queryPlanListByHotCoilIdLike(String hotCoilId, int page, int pageSize) {
int endRow = page * pageSize;
int startRow = endRow - pageSize;
Map<String, Object> params = new java.util.HashMap<>();
params.put("startRow", startRow);
params.put("endRow", endRow);
params.put("hotCoilId", "%" + hotCoilId + "%");
return executeSql(
"oracle",
"select * from (select t.*, ROWNUM rn from (select * from JXPLTCM.PLTCM_PDI_PLAN where HOT_COILID LIKE :hotCoilId order by INSDATE desc) t where ROWNUM <= :endRow) where rn > :startRow",
params
);
}
public ExecuteSqlResponse queryPlanCountByHotCoilIdLike(String hotCoilId) {
return executeSql(
"oracle",
"select count(*) as total from JXPLTCM.PLTCM_PDI_PLAN where HOT_COILID LIKE :hotCoilId",
singletonParam("hotCoilId", "%" + hotCoilId + "%")
);
}
/**
* 批量按 EXCOILID 查询出口卷上线/下线时间PLTCM_PDO_EXCOIL
*/
public ExecuteSqlResponse queryExcoilTimesByCoilIds(List<String> coilIds) {
if (coilIds == null || coilIds.isEmpty()) {
return new ExecuteSqlResponse();
}
String inList = coilIds.stream()
.filter(id -> id != null && !id.trim().isEmpty())
.map(id -> "'" + id.replace("'", "''") + "'")
.collect(java.util.stream.Collectors.joining(", "));
if (inList.isEmpty()) return new ExecuteSqlResponse();
String sql = "SELECT EXCOILID, START_DATE, END_DATE FROM JXPLTCM.PLTCM_PDO_EXCOIL WHERE EXCOILID IN (" + inList + ")";
return executeSql("oracle", sql, emptyParams());
}
/**
* 批量按 HOT_COILID 查询计划数据IN 子句,一次 Oracle 调用获取整页数据)
*/
public ExecuteSqlResponse queryPlanDimsByHotCoilIds(List<String> hotCoilIds) {
if (hotCoilIds == null || hotCoilIds.isEmpty()) {
return new ExecuteSqlResponse();
}
String inList = hotCoilIds.stream()
.map(id -> "'" + id.replace("'", "''") + "'")
.collect(java.util.stream.Collectors.joining(", "));
String sql = "SELECT HOT_COILID, ENTRY_THICK, EXIT_THICK, ENTRY_WIDTH, EXIT_WIDTH, GRADE, PROCESS_CODE, COILID " +
"FROM JXPLTCM.PLTCM_PDI_PLAN WHERE HOT_COILID IN (" + inList + ")";
return executeSql("oracle", sql, emptyParams());
}
public ExecuteSqlResponse queryProSegByEncoilId(String encoilId) {
return executeSql(
"oracle",

View File

@@ -66,6 +66,51 @@ public class SqlServerApiBusinessService {
return PlanListView.fromExecuteSqlResponse(client.queryPlanListByStatus(status));
}
public PlanListView getPlanListByHotCoilIdLike(String hotCoilId, int page, int pageSize) {
return PlanListView.fromExecuteSqlResponse(client.queryPlanListByHotCoilIdLike(hotCoilId, page, pageSize));
}
public long getPlanCountByHotCoilIdLike(String hotCoilId) {
List<Map<String, Object>> rows = asRowList(client.queryPlanCountByHotCoilIdLike(hotCoilId));
if (rows.isEmpty()) return 0L;
Object total = rows.get(0).get("total");
Number n = asNumber(total);
return n == null ? 0L : n.longValue();
}
/**
* 批量获取 L2 计划维度数据(来回料厚/宽、钢种),一次 Oracle 调用覆盖整页。
* 返回 Map&lt;hotCoilId, firstRow&gt;,同一 HOT_COILID 只保留第一行。
*/
/**
* 批量查询出口卷上线/下线时间,返回 Map&lt;excoilId, {START_DATE, END_DATE}&gt;
*/
public Map<String, Map<String, Object>> getExcoilTimesByCoilIds(List<String> coilIds) {
if (coilIds == null || coilIds.isEmpty()) return Collections.emptyMap();
List<Map<String, Object>> rows = asRowList(client.queryExcoilTimesByCoilIds(coilIds));
Map<String, Map<String, Object>> result = new LinkedHashMap<>();
for (Map<String, Object> row : rows) {
Object id = row.getOrDefault("EXCOILID", row.get("excoilid"));
if (id != null) result.putIfAbsent(id.toString().trim(), row);
}
return result;
}
public Map<String, Map<String, Object>> getPlanDimsByHotCoilIds(List<String> hotCoilIds) {
if (hotCoilIds == null || hotCoilIds.isEmpty()) {
return Collections.emptyMap();
}
List<Map<String, Object>> rows = asRowList(client.queryPlanDimsByHotCoilIds(hotCoilIds));
Map<String, Map<String, Object>> result = new LinkedHashMap<>();
for (Map<String, Object> row : rows) {
Object hcId = row.getOrDefault("HOT_COILID", row.get("hot_coilid"));
if (hcId != null) {
result.putIfAbsent(hcId.toString().trim(), row);
}
}
return result;
}
/**
* 计划详情:按成品卷号查询单条计划。
*/

View File

@@ -0,0 +1,502 @@
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; }
}
}

View File

@@ -44,3 +44,36 @@ export function delProcessSpecVersion(versionId) {
method: 'delete'
})
}
export function matchBestSpecVersion(hotCoilId, lineId) {
return request({
url: '/wms/specSync/matchBest',
method: 'get',
params: { hotCoilId, lineId }
})
}
// bindings: [{coilId, specId, versionId}, ...]
export function syncCoilSpec(bindings) {
return request({
url: '/wms/specSync/syncSpec',
method: 'post',
data: { bindings }
})
}
export function getCoilSyncPageList(params) {
return request({
url: '/wms/specSync/pageList',
method: 'get',
params
})
}
export function getUntypedPageList(params) {
return request({
url: '/wms/specSync/untypedPageList',
method: 'get',
params
})
}

View File

@@ -198,92 +198,12 @@
<el-button size="mini" @click="handleFindReset">重置</el-button>
</div>
<!-- 规程关联 -->
<div class="spec-bind-block">
<div class="panel-title">规程关联</div>
<template v-if="selectedRow">
<div class="bind-status">
<span class="bind-label">已关联版本</span>
<span v-if="coilBindingLoading" class="bind-val muted">加载中</span>
<el-tag v-else-if="coilBinding" type="success" size="mini" effect="plain">
{{ coilBinding.versionCode }}
</el-tag>
<span v-else class="bind-val muted">未关联</span>
</div>
<el-button
size="mini"
:type="coilBinding ? 'default' : 'primary'"
icon="el-icon-link"
style="width:100%;margin-top:8px"
@click="openSpecBindDialog"
>{{ coilBinding ? '重新关联' : '关联规程版本' }}</el-button>
</template>
<span v-else class="bind-val muted" style="font-size:12px">请先选择钢卷</span>
</div>
</div>
</div>
</div>
<quality-report-dialog ref="qualityReport" />
<!-- 规程版本选择弹窗 -->
<el-dialog
title="关联规程版本"
:visible.sync="specBindDialog"
width="580px"
append-to-body
@closed="specBindSelectedId = null"
>
<div class="spec-bind-toolbar">
<el-switch
v-model="specBindShowAll"
active-text="全部版本"
inactive-text="仅生效版本"
style="margin-right:8px"
/>
<span class="spec-bind-hint"> {{ specVersionsForDialog.length }} </span>
</div>
<el-table
v-loading="specVersionLoading"
:data="specVersionsForDialog"
size="mini"
border
highlight-current-row
max-height="340"
style="margin-top:10px"
@row-click="row => specBindSelectedId = row.versionId"
>
<el-table-column width="36" align="center">
<template slot-scope="{ row }">
<i
v-if="specBindSelectedId === row.versionId"
class="el-icon-check"
style="color:#409eff;font-weight:700"
/>
</template>
</el-table-column>
<el-table-column label="规程名称" prop="specName" min-width="140" show-overflow-tooltip />
<el-table-column label="版本号" prop="versionCode" width="90" />
<el-table-column label="状态" align="center" width="80">
<template slot-scope="{ row }">
<el-tag v-if="row.isActive === 1" type="success" size="mini" effect="dark">生效中</el-tag>
<el-tag v-else-if="row.status === 'PUBLISHED'" size="mini" effect="plain">已发布</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">草稿</el-tag>
</template>
</el-table-column>
<el-table-column label="规程编码" prop="specCode" width="130" show-overflow-tooltip />
</el-table>
<div slot="footer">
<el-button size="small" @click="specBindDialog = false">取消</el-button>
<el-button
size="small"
type="primary"
:disabled="!specBindSelectedId"
:loading="specBindLoading"
@click="confirmSpecBind"
>确认关联</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -299,12 +219,6 @@ import {
getTimingRealtimeData,
getPresetSetupByCoilId
} from '@/api/l2/timing'
import { listProcessSpecVersion, getProcessSpecVersion } from '@/api/wms/processSpecVersion'
import { listProcessCoilRecord, upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
import { listProcessSpec } from '@/api/wms/processSpec'
import { listProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam } from '@/api/wms/processPlanParam'
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
const TREND_GROUPS = [
@@ -492,32 +406,6 @@ export default {
topTableHeight: 'calc(40vh - 80px)',
chartInstances: [],
resizeHandler: null,
// ── 规程关联 ──
coilBinding: null, // 当前选中钢卷已有的关联记录 { versionId, versionCode, ... }
coilBindingLoading: false,
specBindDialog: false,
specVersionLoading: false,
specVersionRawList: [], // 所有 wms_process_spec_version
specList: [], // 所有 wms_process_spec用于显示规程名
specBindSelectedId: null, // 弹窗内选中的 versionId
specBindShowAll: false, // true=显示全部版本false=只显示生效版本
specBindLoading: false
}
},
computed: {
/** 弹窗中展示的版本列表(含规程名,支持只看生效版本) */
specVersionsForDialog() {
const specMap = {}
this.specList.forEach(s => { specMap[s.specId] = s })
let list = this.specVersionRawList.map(v => ({
...v,
specCode: specMap[v.specId] ? specMap[v.specId].specCode : '—',
specName: specMap[v.specId] ? specMap[v.specId].specName : '—',
}))
if (!this.specBindShowAll) {
list = list.filter(v => v.isActive === 1)
}
return list
}
},
created() {
@@ -558,9 +446,7 @@ export default {
this.shapeRows = null
this.presetData = null
this.selectedTrendParam = null
this.coilBinding = null
this.disposeAllCharts()
this.loadCoilBinding()
const encoilId = row.ENCOILID || row.encoilid
const excoilId = row.EXCOILID || row.excoilid
@@ -979,7 +865,6 @@ export default {
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.coilBinding = null
this.disposeAllCharts()
this.pagination.page = 1
this.loadExcoilCount()
@@ -997,177 +882,6 @@ export default {
this.$refs.qualityReport.open(row, segData, gaugeRows, shapeRows, presetData)
},
// ── 规程关联 ─────────────────────────────────────────
/** 加载当前选中钢卷的已有规程关联记录 */
async loadCoilBinding() {
if (!this.selectedRow) return
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
if (!coilId) return
this.coilBindingLoading = true
try {
const res = await listProcessCoilRecord({ coilId, pageNum: 1, pageSize: 1 })
const rec = (res.rows || [])[0]
if (rec) {
// 补充版本号显示
try {
const verRes = await getProcessSpecVersion(rec.versionId)
this.coilBinding = { ...rec, versionCode: verRes.data?.versionCode || String(rec.versionId) }
} catch {
this.coilBinding = { ...rec, versionCode: String(rec.versionId) }
}
} else {
this.coilBinding = null
}
} catch {
this.coilBinding = null
} finally {
this.coilBindingLoading = false
}
},
/** 打开选择规程版本的弹窗 */
async openSpecBindDialog() {
this.specBindSelectedId = this.coilBinding ? this.coilBinding.versionId : null
this.specBindShowAll = !this.coilBinding // 若已关联默认显示全部;否则只看生效版本
this.specBindDialog = true
this.specVersionLoading = true
try {
const [verRes, specRes] = await Promise.all([
listProcessSpecVersion({ pageNum: 1, pageSize: 500 }),
listProcessSpec({ pageNum: 1, pageSize: 200 })
])
this.specVersionRawList = verRes.rows || []
this.specList = specRes.rows || []
} finally {
this.specVersionLoading = false
}
},
/** 确认关联,并自动检测 L1 实绩与规程参数限值的偏差,将异常落库 */
async confirmSpecBind() {
if (!this.specBindSelectedId) return
this.specBindLoading = true
try {
const coilId = this.selectedRow.EXCOILID || this.selectedRow.excoilid
const enCoilId = this.selectedRow.ENCOILID || this.selectedRow.encoilid || null
const versionId = this.specBindSelectedId
// 1. 先写入基础关联记录(异常数稍后更新)
await upsertProcessCoilRecord({
versionId,
coilId,
enCoilId,
hasAnomaly: 0,
anomalyCnt: 0,
processTime: new Date().toISOString()
})
// 2. 异常检测(仅当存在 SEG 数据时执行)
let anomalyCnt = 0
if (this.segData) {
try {
// 2.1 获取该版本下所有方案点位
const planRes = await listProcessPlan({ versionId })
const plans = planRes.rows || []
// 2.2 汇总所有点位的参数
const allParams = []
for (const plan of plans) {
const paramRes = await listProcessPlanParam({ planId: plan.planId })
;(paramRes.rows || []).forEach(p => allParams.push({ ...p, planId: plan.planId }))
}
// 2.3 逐参数比对,构建异常列表
const anomalies = []
for (const param of allParams) {
const col = (param.paramCode || '').toUpperCase()
if (!col) continue
// 从 SEG 系列数据取段内最大/最小值数组,再求全卷极值
const maxArr = this.seg(col + 'MAX').filter(v => v != null)
const minArr = this.seg(col + 'MIN').filter(v => v != null)
if (!maxArr.length && !minArr.length) continue
const actualMax = maxArr.length ? Math.max(...maxArr) : null
const actualMin = minArr.length ? Math.min(...minArr) : null
// 从预设数据取实际设定值(如有对应映射)
let actualTarget = null
const presetCol = TREND_PRESET_MAP[col]
if (presetCol && this.presetData) {
const raw = this.presetData[presetCol] !== undefined
? this.presetData[presetCol]
: this.presetData[presetCol.toLowerCase()]
if (raw != null) actualTarget = parseFloat(Number(raw).toFixed(4))
}
const upper = param.upperLimit != null ? Number(param.upperLimit) : null
const lower = param.lowerLimit != null ? Number(param.lowerLimit) : null
if (upper == null && lower == null) continue // 未配置限值,跳过
const overMax = upper != null && actualMax != null && actualMax > upper
const underMin = lower != null && actualMin != null && actualMin < lower
if (!overMax && !underMin) continue
const anomalyType = (overMax && underMin) ? 'BOTH' : (overMax ? 'OVER_MAX' : 'UNDER_MIN')
anomalies.push({
versionId,
planId: param.planId,
paramId: param.paramId,
coilId,
enCoilId,
paramCode: param.paramCode,
paramName: param.paramName,
unit: param.unit || TREND_UNIT_MAP[col] || null,
anomalyType,
storedTarget: param.targetValue != null ? Number(param.targetValue) : null,
storedUpper: upper,
storedLower: lower,
actualTarget,
actualMax,
actualMin,
deviationMax: overMax ? parseFloat((actualMax - upper).toFixed(4)) : null,
deviationMin: underMin ? parseFloat((actualMin - lower).toFixed(4)) : null,
detectedAt: new Date().toISOString()
})
}
anomalyCnt = anomalies.length
// 2.4 批量写入异常记录
if (anomalies.length > 0) {
await batchAddProcessAnomaly(anomalies)
}
// 2.5 若有异常则更新关联记录的计数字段
if (anomalyCnt > 0) {
await upsertProcessCoilRecord({
versionId,
coilId,
enCoilId,
hasAnomaly: 1,
anomalyCnt,
processTime: new Date().toISOString()
})
}
} catch (e) {
console.warn('[规程关联] 异常检测失败,跳过', e)
}
}
const msg = anomalyCnt > 0
? `关联成功,检测到 ${anomalyCnt} 个参数异常`
: '关联成功,无异常'
this.$message[anomalyCnt > 0 ? 'warning' : 'success'](msg)
this.specBindDialog = false
await this.loadCoilBinding()
} catch {
this.$message.error('关联失败,请重试')
} finally {
this.specBindLoading = false
}
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
@@ -1389,45 +1103,5 @@ export default {
font-size: 13px;
}
/* ── 规程关联区块 ── */
.spec-bind-block {
border-top: 1px solid #ebeef5;
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.bind-status {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.bind-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
}
.bind-val {
font-size: 12px;
color: #303133;
font-weight: 500;
&.muted { color: #c0c4cc; font-weight: normal; }
}
/* ── 弹窗内工具栏 ── */
.spec-bind-toolbar {
display: flex;
align-items: center;
gap: 12px;
}
.spec-bind-hint {
font-size: 12px;
color: #909399;
}
</style>

View File

@@ -31,6 +31,12 @@
</div>
</div>
<div v-if="matchedSpec" style="margin-bottom:10px">
<el-tag type="success" size="small">
<i class="el-icon-s-order" style="margin-right:4px" />
已匹配规程{{ matchedSpec.specCode }}{{ matchedSpec.specName ? ' - ' + matchedSpec.specName : '' }} / {{ matchedSpec.versionCode }}
</el-tag>
</div>
<el-form ref="updateForm" :model="updateForm" :rules="rules" label-width="86px" size="small">
<el-row>
<el-col :span="16">
@@ -305,6 +311,7 @@
<script>
import { getMaterialCoil, updateMaterialCoil, getFirstHeatCoilMaterial } from '@/api/wms/coil';
import { matchBestSpecVersion } from '@/api/wms/processSpecVersion';
import { completeAction, getPendingAction } from '@/api/wms/pendingAction';
import { saveCoilCache, getCoilCacheByCoilId, delCoilCache } from '@/api/wms/coilCache';
import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect";
@@ -381,7 +388,10 @@ export default {
productionEndTime: '',
productionDuration: '',
formattedDuration: '',
specId: null,
versionId: null,
},
matchedSpec: null,
rules: {
currentCoilNo: [
{ required: true, message: '请输入当前钢卷号', trigger: 'blur' },
@@ -576,6 +586,18 @@ export default {
else if (t === '0') this.$set(this.updateForm, 'trimmingRequirement', '毛边料')
}
this.$message.success('L2 数据已写入表单')
const ACID_ROLL_ACTION_TYPES = [11, 200, 520]
const ACID_LINE_ID = 1
if (this.l2HotCoilId && ACID_ROLL_ACTION_TYPES.includes(this.actionType)) {
matchBestSpecVersion(this.l2HotCoilId, ACID_LINE_ID).then(res => {
const v = res.data
if (v) {
this.$set(this.updateForm, 'specId', v.specId)
this.$set(this.updateForm, 'versionId', v.versionId)
this.matchedSpec = v
}
}).catch(() => {})
}
},
// 处理材料类型变化

View File

@@ -0,0 +1,571 @@
<template>
<div class="page-wrap">
<!-- 搜索栏 -->
<div class="search-bar">
<el-form :model="query" inline size="small">
<el-form-item label="入场卷号">
<el-input v-model="query.enterCoilNo" placeholder="模糊查询" clearable style="width:150px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="query.currentCoilNo" placeholder="模糊查询" clearable style="width:150px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材料">
<el-input v-model="query.material" placeholder="模糊查询" clearable style="width:120px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="规程">
<el-input v-model="query.specCode" placeholder="编码或名称" clearable style="width:130px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="钢种">
<el-input v-model="query.grade" placeholder="当前页筛选" clearable style="width:110px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="质量状态">
<el-select v-model="query.qualityStatus" style="width:110px" clearable>
<el-option label="合格" value="合格" />
<el-option label="待判" value="待判" />
<el-option label="不合格" value="不合格" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 整体统计条 -->
<div class="stat-strip" v-if="overallStats.total > 0 || list.length">
<div class="stat-tile">
<span class="st-num">{{ overallStats.synced }}</span>
<span class="st-label">已同步</span>
</div>
<div class="stat-div" />
<div class="stat-tile">
<span class="st-num warn">{{ overallStats.unsynced }}</span>
<span class="st-label">未同步</span>
</div>
<div class="stat-div" />
<div class="stat-tile">
<span class="st-num muted">{{ overallStats.movedOn }}</span>
<span class="st-label">已流转</span>
</div>
<div class="stat-div" />
<div class="stat-tile">
<span class="st-num info">{{ overallStats.total }}</span>
<span class="st-label">总计</span>
</div>
<div class="stat-tip">当前页 {{ list.length }} / {{ total }} </div>
</div>
<!-- 表格 -->
<div class="table-area">
<!-- Tab 切换 + 批量同步按钮 -->
<div class="toolbar">
<div class="sync-tabs">
<span
v-for="tab in syncTabs" :key="tab.value"
class="sync-tab"
:class="{ active: query.syncStatus === tab.value }"
@click="switchTab(tab.value)">
{{ tab.label }}
</span>
</div>
<el-button type="primary" size="small" :disabled="!batchable.length"
:loading="batchLoading" @click="handleBatchSync" style="margin-left:auto">
<i class="el-icon-s-promotion" /> 批量同步
<span v-if="batchable.length" class="btn-badge">{{ batchable.length }}</span>
</el-button>
</div>
<el-table v-loading="loading" :data="filteredList" size="small" border
:header-cell-style="{ background: '#fafafa', color: '#606266', fontWeight: '500' }"
style="width:100%;margin-top:10px"
@selection-change="onSelect">
<el-table-column type="selection" width="40" align="center" :selectable="canSelect" />
<!-- L3 WMS 字段 -->
<el-table-column label="入场卷号" prop="enterCoilNo" min-width="148" fixed show-overflow-tooltip>
<template slot-scope="{ row }">
<span class="coil-no">{{ row.enterCoilNo || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" prop="currentCoilNo" min-width="135" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.currentCoilNo || '—' }}</template>
</el-table-column>
<el-table-column label="供应商卷号" prop="supplierCoilNo" min-width="130" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.supplierCoilNo">{{ row.supplierCoilNo }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="净重(kg)" prop="netWeight" width="88" align="right">
<template slot-scope="{ row }">
<span v-if="row.netWeight != null">{{ row.netWeight }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="实测厚(mm)" prop="actualThickness" width="95" align="right">
<template slot-scope="{ row }">
<span v-if="row.actualThickness != null">{{ row.actualThickness }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="实测宽(mm)" prop="actualWidth" width="95" align="right">
<template slot-scope="{ row }">
<span v-if="row.actualWidth != null">{{ row.actualWidth }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="质量状态" prop="qualityStatus" width="88" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row.qualityStatus" size="mini" :type="qualityTagType(row.qualityStatus)" effect="plain">
{{ row.qualityStatus }}
</el-tag>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="材质" prop="material" width="90" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.material">{{ row.material }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="规格" prop="specification" width="110" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.specification">{{ row.specification }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="上线时间" prop="onlineTime" width="148" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.onlineTime" class="time-val">{{ fmtTime(row.onlineTime) }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="下线时间" prop="offlineTime" width="148" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.offlineTime" class="time-val">{{ fmtTime(row.offlineTime) }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- L2 Oracle 字段 -->
<el-table-column label="工艺代码" prop="processCode" width="108" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.processCode" class="process-code">{{ row.processCode }}</span>
<span v-else-if="!row.l2Found" class="dim">无L2</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="钢种" prop="grade" width="100" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.grade" class="grade-tag">{{ row.grade }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="来料厚×宽(mm)" width="135" align="center">
<template slot-scope="{ row }">
<template v-if="row.entryThick != null || row.entryWidth != null">
<span :class="dimClass(row.entryThick, row.matchEntryThickMin, row.matchEntryThickMax)">{{ fmt(row.entryThick) }}</span>
<span class="spec-x">×</span>
<span :class="dimClass(row.entryWidth, row.matchEntryWidthMin, row.matchEntryWidthMax)">{{ fmt(row.entryWidth) }}</span>
</template>
<span v-else-if="!row.l2Found" class="dim">无L2</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="出口厚×宽(mm)" width="135" align="center">
<template slot-scope="{ row }">
<template v-if="row.exitThick != null || row.exitWidth != null">
<span :class="dimClass(row.exitThick, row.matchExitThickMin, row.matchExitThickMax)">{{ fmt(row.exitThick) }}</span>
<span class="spec-x">×</span>
<span :class="dimClass(row.exitWidth, row.matchExitWidthMin, row.matchExitWidthMax)">{{ fmt(row.exitWidth) }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 已绑规程 -->
<el-table-column label="已绑规程" min-width="180" show-overflow-tooltip>
<template slot-scope="{ row }">
<template v-if="row.syncStatus === 'synced'">
<i v-if="isDeviated(row)" class="el-icon-warning deviation-icon" title="实际值超出规程设定范围" />
<span class="spec-name bound">{{ row.specName || row.specCode || '—' }}</span>
<span v-if="row.versionCode" class="ver-tag">{{ row.versionCode }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 推荐规程 -->
<el-table-column label="推荐规程" min-width="200" show-overflow-tooltip>
<template slot-scope="{ row }">
<template v-if="row.syncStatus !== 'synced' && row.candidateSpecCode">
<span class="rec-badge">推荐</span>
<span class="spec-name candidate">{{ row.candidateSpecName || row.candidateSpecCode }}</span>
<span v-if="row.candidateVersionCode" class="ver-tag candidate-ver">{{ row.candidateVersionCode }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<el-table-column label="规程状态" width="100" align="center">
<template slot-scope="{ row }">
<div style="display:flex;flex-direction:column;align-items:center;gap:3px">
<el-tag v-if="row.syncStatus === 'synced'" type="success" size="mini" effect="plain">已同步</el-tag>
<el-tag v-else-if="row.candidateVersionId" type="warning" size="mini" effect="plain">可同步</el-tag>
<el-tag v-else type="info" size="mini" effect="plain">无规程</el-tag>
<el-tag v-if="row.movedOn" type="info" size="mini" effect="dark"
style="font-size:10px;padding:0 4px;height:14px;line-height:14px">已流转</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="64" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button v-if="row.syncStatus === 'unsynced' && row.candidateVersionId"
type="text" size="mini" style="color:#e6a23c"
:loading="row._syncing" @click="handleSyncOne(row)">同步</el-button>
<span v-else class="dim"></span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total"
:page.sync="query.pageNum" :limit.sync="query.pageSize"
:page-sizes="[20, 40, 100]"
@pagination="loadData" />
</div>
</div>
</template>
<script>
import { getCoilSyncPageList, syncCoilSpec } from '@/api/wms/processSpecVersion'
export default {
name: 'CoilSpecSync',
data() {
return {
loading: false,
batchLoading: false,
list: [],
total: 0,
selected: [],
overallStats: { total: 0, synced: 0, unsynced: 0, movedOn: 0 },
syncTabs: [
{ label: '未同步', value: 'unsynced' },
{ label: '全部', value: '' },
{ label: '已同步', value: 'synced' }
],
query: {
pageNum: 1,
pageSize: 40,
enterCoilNo: '',
currentCoilNo: '',
material: '',
specCode: '',
grade: '',
qualityStatus: '',
syncStatus: 'unsynced'
}
}
},
computed: {
filteredList() {
const grade = (this.query.grade || '').trim().toLowerCase()
if (!grade) return this.list
return this.list.filter(r => r.grade && r.grade.toLowerCase().includes(grade))
},
batchable() {
return this.selected.filter(r => r.syncStatus === 'unsynced' && r.candidateVersionId)
}
},
created() { this.loadData() },
methods: {
loadData() {
this.loading = true
getCoilSyncPageList({
pageNum: this.query.pageNum,
pageSize: this.query.pageSize,
enterCoilNo: this.query.enterCoilNo || undefined,
currentCoilNo: this.query.currentCoilNo || undefined,
material: this.query.material || undefined,
specCode: this.query.specCode || undefined,
qualityStatus: this.query.qualityStatus || undefined,
syncStatus: this.query.syncStatus || undefined
}).then(res => {
const d = res.data || {}
this.list = (d.rows || []).map(r => ({ ...r, _syncing: false }))
this.total = d.total || 0
const os = d.overallStats || {}
this.overallStats = {
total: os.total || 0,
synced: os.synced || 0,
unsynced: os.unsynced || 0,
movedOn: os.movedOn || 0
}
}).finally(() => { this.loading = false })
},
handleQuery() { this.query.pageNum = 1; this.loadData() },
switchTab(val) {
if (this.query.syncStatus === val) return
this.query.syncStatus = val
this.query.pageNum = 1
this.loadData()
},
handleReset() {
this.query = { pageNum: 1, pageSize: 40, enterCoilNo: '', currentCoilNo: '', material: '', specCode: '', grade: '', qualityStatus: '', syncStatus: 'unsynced' }
this.loadData()
},
onSelect(sel) { this.selected = sel },
canSelect(row) { return row.syncStatus === 'unsynced' && !!row.candidateVersionId },
isDeviated(row) {
if (!row.l2Found) return false
return [
[row.entryThick, row.matchEntryThickMin, row.matchEntryThickMax],
[row.exitThick, row.matchExitThickMin, row.matchExitThickMax],
[row.entryWidth, row.matchEntryWidthMin, row.matchEntryWidthMax],
[row.exitWidth, row.matchExitWidthMin, row.matchExitWidthMax]
].some(([v, mn, mx]) => v != null && mn != null && mx != null && (+v < +mn || +v > +mx))
},
dimClass(val, min, max) {
if (val == null || min == null || max == null) return 'dv'
return +val < +min || +val > +max ? 'dv out' : 'dv ok'
},
qualityTagType(qs) {
if (!qs) return 'info'
if (qs.includes('合格') || qs === 'PASS') return 'success'
if (qs.includes('待判') || qs.includes('HOLD')) return 'warning'
return 'danger'
},
handleSyncOne(row) {
if (!row.wmsCoilId) { this.$message.warning('该钢卷在WMS中无记录'); return }
if (!row.candidateSpecId || !row.candidateVersionId) { this.$message.warning('无可同步规程'); return }
this.$set(row, '_syncing', true)
syncCoilSpec([{ coilId: row.wmsCoilId, specId: row.candidateSpecId, versionId: row.candidateVersionId }])
.then(() => {
this.$message.success('同步成功')
// 原地更新行状态,不刷新整页(避免因 unsynced 过滤器导致该行消失)
this.$set(row, 'syncStatus', 'synced')
this.$set(row, 'specCode', row.candidateSpecCode)
this.$set(row, 'specName', row.candidateSpecName)
this.$set(row, 'versionCode', row.candidateVersionCode)
this.$set(row, 'candidateSpecCode', null)
this.$set(row, 'candidateSpecName', null)
this.$set(row, 'candidateVersionCode', null)
this.$set(row, 'candidateVersionId', null)
this.$set(row, 'candidateSpecId', null)
})
.catch(() => this.$message.error('同步失败'))
.finally(() => this.$set(row, '_syncing', false))
},
handleBatchSync() {
const bindings = this.batchable
.filter(r => r.wmsCoilId && r.candidateSpecId && r.candidateVersionId)
.map(r => ({ coilId: r.wmsCoilId, specId: r.candidateSpecId, versionId: r.candidateVersionId }))
if (!bindings.length) return
this.batchLoading = true
syncCoilSpec(bindings)
.then(() => { this.$message.success(`批量同步完成,共 ${bindings.length}`); this.selected = []; this.loadData() })
.catch(() => this.$message.error('批量同步失败'))
.finally(() => { this.batchLoading = false })
},
fmt(val) { return val == null ? '—' : Number(val).toFixed(2) },
fmtTime(val) {
if (!val) return '—'
const d = new Date(val)
if (isNaN(d.getTime())) return String(val)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
}
}
</script>
<style scoped>
.page-wrap {
padding: 16px;
background: #f4f6f9;
min-height: 100%;
box-sizing: border-box;
}
/* 搜索栏 */
.search-bar {
background: #fff;
border-radius: 6px;
padding: 14px 20px 4px;
margin-bottom: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
/* 当前页统计条 */
.stat-strip {
display: flex;
align-items: center;
background: #fff;
border-radius: 6px;
padding: 8px 20px;
margin-bottom: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
font-size: 13px;
gap: 4px;
}
.stat-tile {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
}
.st-num {
font-size: 20px;
font-weight: 700;
color: #67c23a;
line-height: 1;
}
.st-num.warn { color: #e6a23c; }
.st-num.muted { color: #909399; }
.st-num.danger { color: #f56c6c; }
.st-num.info { color: #409eff; }
.st-label { font-size: 12px; color: #606266; }
.stat-div { width: 1px; background: #ebeef5; height: 24px; margin: 0 4px; }
.stat-tip { margin-left: auto; font-size: 12px; color: #c0c4cc; }
/* 时间字段 */
.time-val { font-size: 12px; color: #606266; font-variant-numeric: tabular-nums; }
/* 表格区 */
.table-area {
background: #fff;
border-radius: 6px;
padding: 16px 20px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.toolbar { display: flex; align-items: center; gap: 0; }
/* Tab 切换 */
.sync-tabs {
display: flex;
align-items: center;
background: #f0f2f5;
border-radius: 6px;
padding: 3px;
gap: 2px;
}
.sync-tab {
padding: 4px 18px;
font-size: 13px;
color: #606266;
border-radius: 4px;
cursor: pointer;
transition: all .18s;
user-select: none;
white-space: nowrap;
}
.sync-tab:hover { color: #303133; background: rgba(255,255,255,.6); }
.sync-tab.active {
background: #fff;
color: #409eff;
font-weight: 600;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
}
.btn-badge {
display: inline-block;
background: rgba(255,255,255,.3);
border-radius: 10px;
padding: 0 6px;
font-size: 11px;
margin-left: 4px;
}
/* 卷号 */
.coil-no {
font-family: 'Courier New', monospace;
font-size: 12.5px;
letter-spacing: .3px;
}
/* 规格维度 */
.dv { font-variant-numeric: tabular-nums; font-size: 12.5px; color: #303133; }
.dv.ok { color: #67c23a; }
.dv.out { color: #f56c6c; font-weight: 600; }
.spec-x { color: #c0c4cc; margin: 0 2px; font-size: 11px; }
/* 工艺代码 */
.process-code {
font-family: 'Courier New', monospace;
font-size: 11.5px;
color: #606266;
background: #f5f7fa;
padding: 1px 4px;
border-radius: 3px;
}
/* 钢种 */
.grade-tag {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
background: #ecf5ff;
color: #409eff;
font-size: 11px;
border: 1px solid #d9ecff;
}
/* 规程 / 版本 */
.spec-name { font-weight: 500; font-size: 12.5px; }
.spec-name.bound { color: #67c23a; }
.spec-name.candidate { color: #e6a23c; }
.ver-tag {
display: inline-block;
margin-left: 5px;
padding: 0 5px;
height: 18px;
line-height: 18px;
border-radius: 2px;
background: #f0f9eb;
color: #67c23a;
font-size: 11px;
border: 1px solid #c2e7b0;
}
.ver-tag.candidate-ver {
background: #fdf6ec;
color: #e6a23c;
border-color: #f5dab1;
}
.deviation-icon { color: #f56c6c; margin-right: 4px; font-size: 13px; }
.rec-badge {
display: inline-block;
margin-right: 5px;
padding: 0 5px;
height: 16px;
line-height: 16px;
border-radius: 2px;
background: #fdf6ec;
color: #e6a23c;
font-size: 10px;
border: 1px solid #f5dab1;
vertical-align: middle;
}
.dim { color: #c0c4cc; font-size: 12px; }
</style>

View File

@@ -0,0 +1,327 @@
<template>
<div class="page-wrap">
<!-- 搜索栏 -->
<div class="search-bar">
<el-form :model="query" inline size="small">
<el-form-item label="入场卷号">
<el-input v-model="query.enterCoilNo" placeholder="模糊查询" clearable style="width:150px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="query.currentCoilNo" placeholder="模糊查询" clearable style="width:150px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="操作员">
<el-input v-model="query.operatorName" placeholder="模糊查询" clearable style="width:110px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="二级实绩">
<el-select v-model="query.l2Filter" style="width:100px" clearable>
<el-option label="全部" value="" />
<el-option label="已有实绩" value="hasL2" />
<el-option label="暂无实绩" value="noL2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计条 -->
<div class="stat-strip" v-if="list.length">
<div class="stat-tile">
<span class="st-num danger">{{ pageStats.hasL2 }}</span>
<span class="st-label">二级已实绩</span>
</div>
<div class="stat-div" />
<div class="stat-tile">
<span class="st-num muted">{{ pageStats.noL2 }}</span>
<span class="st-label">暂无实绩</span>
</div>
<div class="stat-tip">当前页 {{ list.length }} {{ total }} 条待录入</div>
</div>
<!-- 表格 -->
<div class="table-area">
<el-table v-loading="loading" :data="filteredList" size="small" border
:header-cell-style="{ background: '#fafafa', color: '#606266', fontWeight: '500' }"
style="width:100%;margin-top:0">
<!-- 入场卷号 -->
<el-table-column label="入场卷号" prop="enterCoilNo" min-width="148" fixed show-overflow-tooltip>
<template slot-scope="{ row }">
<span class="coil-no">{{ row.enterCoilNo || '—' }}</span>
</template>
</el-table-column>
<!-- 当前卷号 -->
<el-table-column label="当前钢卷号" prop="currentCoilNo" min-width="135" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.currentCoilNo || '—' }}</template>
</el-table-column>
<!-- 供应商卷号 -->
<el-table-column label="供应商卷号" prop="supplierCoilNo" min-width="130" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.supplierCoilNo">{{ row.supplierCoilNo }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 物料名称 -->
<el-table-column label="物料名称" prop="itemName" min-width="140" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.itemName">{{ row.itemName }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 材质 -->
<el-table-column label="材质" prop="material" width="90" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.material">{{ row.material }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 规格 -->
<el-table-column label="规格" prop="specification" width="110" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.specification">{{ row.specification }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 仓库 -->
<el-table-column label="仓库" prop="warehouseName" width="110" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.warehouseName">{{ row.warehouseName }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 扫描时间 -->
<el-table-column label="扫描时间" prop="scanTime" width="148" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.scanTime" class="time-val">{{ fmtTime(row.scanTime) }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 创建时间 -->
<el-table-column label="创建时间" prop="createTime" width="148" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.createTime" class="time-val">{{ fmtTime(row.createTime) }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 操作员 -->
<el-table-column label="操作员" prop="operatorName" width="90" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.operatorName">{{ row.operatorName }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 待操作状态 -->
<el-table-column label="待操作状态" width="95" align="center">
<template slot-scope="{ row }">
<el-tag :type="actionStatusType(row.actionStatus)" size="mini" effect="plain">
{{ actionStatusLabel(row.actionStatus) }}
</el-tag>
</template>
</el-table-column>
<!-- 二级实绩 -->
<el-table-column label="二级实绩" width="90" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row.l2Found" type="danger" size="mini" effect="dark">已有实绩</el-tag>
<span v-else class="dim">暂无</span>
</template>
</el-table-column>
<!-- 工艺代码 -->
<el-table-column label="工艺代码" prop="processCode" width="108" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.processCode" class="process-code">{{ row.processCode }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 钢种 -->
<el-table-column label="钢种" prop="grade" width="100" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.grade" class="grade-tag">{{ row.grade }}</span>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 来料厚× -->
<el-table-column label="来料厚×宽(mm)" width="135" align="center">
<template slot-scope="{ row }">
<template v-if="row.entryThick != null || row.entryWidth != null">
<span class="dv">{{ fmt(row.entryThick) }}</span>
<span class="spec-x">×</span>
<span class="dv">{{ fmt(row.entryWidth) }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
<!-- 出口厚× -->
<el-table-column label="出口厚×宽(mm)" width="135" align="center">
<template slot-scope="{ row }">
<template v-if="row.exitThick != null || row.exitWidth != null">
<span class="dv">{{ fmt(row.exitThick) }}</span>
<span class="spec-x">×</span>
<span class="dv">{{ fmt(row.exitWidth) }}</span>
</template>
<span v-else class="dim"></span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total"
:page.sync="query.pageNum" :limit.sync="query.pageSize"
:page-sizes="[20, 40, 100]"
@pagination="loadData" />
</div>
</div>
</template>
<script>
import { getUntypedPageList } from '@/api/wms/processSpecVersion'
export default {
name: 'CoilTypingCheck',
data() {
return {
loading: false,
list: [],
total: 0,
query: {
pageNum: 1,
pageSize: 40,
enterCoilNo: '',
currentCoilNo: '',
operatorName: '',
l2Filter: ''
}
}
},
computed: {
filteredList() {
if (!this.query.l2Filter) return this.list
if (this.query.l2Filter === 'hasL2') return this.list.filter(r => r.l2Found)
if (this.query.l2Filter === 'noL2') return this.list.filter(r => !r.l2Found)
return this.list
},
pageStats() {
let hasL2 = 0, noL2 = 0
this.list.forEach(r => { r.l2Found ? hasL2++ : noL2++ })
return { hasL2, noL2 }
}
},
created() { this.loadData() },
methods: {
loadData() {
this.loading = true
getUntypedPageList({
pageNum: this.query.pageNum,
pageSize: this.query.pageSize,
enterCoilNo: this.query.enterCoilNo || undefined,
currentCoilNo: this.query.currentCoilNo || undefined,
operatorName: this.query.operatorName || undefined
}).then(res => {
const d = res.data || {}
this.list = d.rows || []
this.total = d.total || 0
}).finally(() => { this.loading = false })
},
handleQuery() { this.query.pageNum = 1; this.loadData() },
handleReset() {
this.query = { pageNum: 1, pageSize: 40, enterCoilNo: '', currentCoilNo: '', operatorName: '', l2Filter: '' }
this.loadData()
},
actionStatusType(s) {
if (s === 0) return 'info'
if (s === 1) return 'warning'
return ''
},
actionStatusLabel(s) {
if (s === 0) return '待处理'
if (s === 1) return '处理中'
return String(s ?? '—')
},
fmt(val) { return val == null ? '—' : Number(val).toFixed(2) },
fmtTime(val) {
if (!val) return '—'
const d = new Date(val)
if (isNaN(d.getTime())) return String(val)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
}
}
</script>
<style scoped>
.page-wrap {
padding: 16px;
background: #f4f6f9;
min-height: 100%;
box-sizing: border-box;
}
.search-bar {
background: #fff;
border-radius: 6px;
padding: 14px 20px 4px;
margin-bottom: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.stat-strip {
display: flex;
align-items: center;
background: #fff;
border-radius: 6px;
padding: 8px 20px;
margin-bottom: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
font-size: 13px;
gap: 4px;
}
.stat-tile { display: flex; align-items: center; gap: 6px; padding: 0 12px; }
.st-num { font-size: 20px; font-weight: 700; color: #67c23a; line-height: 1; }
.st-num.danger { color: #f56c6c; }
.st-num.muted { color: #909399; }
.st-label { font-size: 12px; color: #606266; }
.stat-div { width: 1px; background: #ebeef5; height: 24px; margin: 0 4px; }
.stat-tip { margin-left: auto; font-size: 12px; color: #c0c4cc; }
.table-area {
background: #fff;
border-radius: 6px;
padding: 16px 20px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.coil-no { font-family: 'Courier New', monospace; font-size: 12.5px; letter-spacing: .3px; }
.time-val { font-size: 12px; color: #606266; font-variant-numeric: tabular-nums; }
.process-code {
font-family: 'Courier New', monospace; font-size: 11.5px; color: #606266;
background: #f5f7fa; padding: 1px 4px; border-radius: 3px;
}
.grade-tag {
display: inline-block; padding: 1px 6px; border-radius: 3px;
background: #ecf5ff; color: #409eff; font-size: 11px; border: 1px solid #d9ecff;
}
.dv { font-variant-numeric: tabular-nums; font-size: 12.5px; color: #303133; }
.spec-x { color: #c0c4cc; margin: 0 2px; font-size: 11px; }
.dim { color: #c0c4cc; font-size: 12px; }
</style>

View File

@@ -85,6 +85,88 @@
>模板导入</el-button>
</div>
<!-- 匹配条件面板 -->
<div class="match-cond-panel" v-if="versionId">
<div class="match-cond-header">
<span class="match-cond-title"><i class="el-icon-aim" />匹配条件</span>
<div class="match-cond-actions">
<template v-if="matchCondEditing">
<el-button size="mini" @click="cancelMatchCond">取消</el-button>
<el-button size="mini" type="primary" :loading="matchCondSaving" @click="saveMatchCond">保存</el-button>
</template>
<el-button v-else size="mini" icon="el-icon-edit" @click="matchCondEditing = true">编辑</el-button>
</div>
</div>
<div class="match-cond-body">
<!-- 只读模式 -->
<template v-if="!matchCondEditing">
<div class="match-cond-item">
<span class="mci-label">来料厚度</span>
<span class="mci-val">{{ fmtRange(matchCondForm.matchEntryThickMin, matchCondForm.matchEntryThickMax, 'mm') }}</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">出口厚度</span>
<span class="mci-val">{{ fmtRange(matchCondForm.matchExitThickMin, matchCondForm.matchExitThickMax, 'mm') }}</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">来料宽度</span>
<span class="mci-val">{{ fmtRange(matchCondForm.matchEntryWidthMin, matchCondForm.matchEntryWidthMax, 'mm') }}</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">出口宽度</span>
<span class="mci-val">{{ fmtRange(matchCondForm.matchExitWidthMin, matchCondForm.matchExitWidthMax, 'mm') }}</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">钢种关键字</span>
<span class="mci-val">{{ matchCondForm.matchSteelGrade || '—' }}</span>
</div>
</template>
<!-- 编辑模式 -->
<template v-else>
<div class="match-cond-item">
<span class="mci-label">来料厚度</span>
<el-input v-model="matchCondForm.matchEntryThickMin" size="small" class="mci-input" placeholder="下限" />
<span class="mci-sep"></span>
<el-input v-model="matchCondForm.matchEntryThickMax" size="small" class="mci-input" placeholder="上限" />
<span class="mci-unit">mm</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">出口厚度</span>
<el-input v-model="matchCondForm.matchExitThickMin" size="small" class="mci-input" placeholder="下限" />
<span class="mci-sep"></span>
<el-input v-model="matchCondForm.matchExitThickMax" size="small" class="mci-input" placeholder="上限" />
<span class="mci-unit">mm</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">来料宽度</span>
<el-input v-model="matchCondForm.matchEntryWidthMin" size="small" class="mci-input" placeholder="下限" />
<span class="mci-sep"></span>
<el-input v-model="matchCondForm.matchEntryWidthMax" size="small" class="mci-input" placeholder="上限" />
<span class="mci-unit">mm</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">出口宽度</span>
<el-input v-model="matchCondForm.matchExitWidthMin" size="small" class="mci-input" placeholder="下限" />
<span class="mci-sep"></span>
<el-input v-model="matchCondForm.matchExitWidthMax" size="small" class="mci-input" placeholder="上限" />
<span class="mci-unit">mm</span>
</div>
<div class="match-cond-divider" />
<div class="match-cond-item">
<span class="mci-label">钢种关键字</span>
<el-input v-model="matchCondForm.matchSteelGrade" size="small" style="width:160px" placeholder="如SPCC、Q345" />
</div>
</template>
</div>
</div>
<!-- DR 道次参数同步提示 -->
<el-alert
v-if="drSyncLoading"
@@ -292,6 +374,7 @@ import * as XLSX from 'xlsx'
import { listProcessPlan, addProcessPlan, updateProcessPlan, delProcessPlan } from '@/api/wms/processPlan'
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam, delProcessPlanParam } from '@/api/wms/processPlanParam'
import { listDrRecipe, listDrRecipeVersions, getDrRecipeVersionDetail } from '@/api/wms/drMill'
import { getProcessSpecVersion, updateProcessSpecVersion } from '@/api/wms/processSpecVersion'
/** 表单内可选段类型(新建/编辑仍支持全部枚举) */
const SEGMENT_FORM_OPTIONS = [
@@ -367,7 +450,11 @@ export default {
importStatus: 'idle', // idle, processing, finished, error
importErrorMsg: '',
validateLoading: false,
importLoading: false
importLoading: false,
matchCondForm: {},
matchCondSaving: false,
matchCondEditing: false,
_matchCondSnapshot: {}
}
},
computed: {
@@ -514,6 +601,7 @@ export default {
this._drSyncAttempted = false // 路由切换时重置,避免重复同步
if (this.versionId) {
this.loadPlans()
this.openMatchCond()
}
},
goBack() { this.$router.go(-1) },
@@ -1155,6 +1243,45 @@ export default {
this.importErrorMsg = message
this.validateLoading = false
this.importLoading = false
},
openMatchCond() {
getProcessSpecVersion(this.versionId).then(res => {
const v = res.data || {}
this.matchCondForm = {
versionId: this.versionId,
versionCode: v.versionCode || '',
specId: v.specId || undefined,
matchEntryThickMin: v.matchEntryThickMin ?? null,
matchEntryThickMax: v.matchEntryThickMax ?? null,
matchExitThickMin: v.matchExitThickMin ?? null,
matchExitThickMax: v.matchExitThickMax ?? null,
matchEntryWidthMin: v.matchEntryWidthMin ?? null,
matchEntryWidthMax: v.matchEntryWidthMax ?? null,
matchExitWidthMin: v.matchExitWidthMin ?? null,
matchExitWidthMax: v.matchExitWidthMax ?? null,
matchSteelGrade: v.matchSteelGrade || ''
}
this._matchCondSnapshot = { ...this.matchCondForm }
this.matchCondEditing = false
})
},
cancelMatchCond() {
this.matchCondForm = { ...this._matchCondSnapshot }
this.matchCondEditing = false
},
saveMatchCond() {
this.matchCondSaving = true
updateProcessSpecVersion(this.matchCondForm).then(() => {
this._matchCondSnapshot = { ...this.matchCondForm }
this.matchCondEditing = false
this.$modal.msgSuccess('匹配条件保存成功')
}).catch(e => console.error(e)).finally(() => { this.matchCondSaving = false })
},
fmtRange(min, max, unit) {
if (min == null && max == null) return '—'
const a = min != null ? min : '∞'
const b = max != null ? max : '∞'
return `${a} ${b} ${unit}`
}
}
}
@@ -1347,6 +1474,78 @@ export default {
.seg-process { background: #f0f9eb; color: #3a7a2a; border: 1px solid #b3e19d; }
.seg-outlet { background: #fdf6ec; color: #a86a00; border: 1px solid #f5dab1; }
/* ── 匹配条件面板 ── */
.match-cond-panel {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid #e4e9f0;
border-radius: 6px;
background: #f8fafd;
margin-bottom: 12px;
overflow: hidden;
}
.match-cond-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
background: #edf2f8;
border-bottom: 1px solid #e4e9f0;
}
.match-cond-title {
font-size: 12px;
font-weight: 600;
color: #4a6585;
letter-spacing: .3px;
}
.match-cond-title i { margin-right: 5px; }
.match-cond-actions { display: flex; gap: 6px; }
.mci-val {
font-size: 12px;
color: #303133;
font-variant-numeric: tabular-nums;
}
.match-cond-body {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 8px 12px;
gap: 0;
}
.match-cond-item {
display: flex;
align-items: center;
gap: 5px;
padding: 2px 0;
}
.mci-label {
font-size: 12px;
color: #606266;
white-space: nowrap;
min-width: 60px;
}
.mci-input {
width: 82px !important;
}
.mci-sep {
color: #c0c4cc;
font-size: 13px;
flex-shrink: 0;
}
.mci-unit {
font-size: 11px;
color: #909399;
flex-shrink: 0;
}
.match-cond-divider {
width: 1px;
height: 20px;
background: #e4e9f0;
margin: 0 12px;
flex-shrink: 0;
}
/* 导入对话框样式 */
.import-container {

View File

@@ -210,5 +210,8 @@ public class WmsMaterialCoil extends BaseEntity {
* 调拨类型
*/
private String transferType;
private Long specId;
private Long versionId;
}

View File

@@ -7,6 +7,8 @@ import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 规程版本对象 wms_process_spec_version
*
@@ -46,4 +48,14 @@ public class WmsProcessSpecVersion extends BaseEntity {
private Integer delFlag;
private String remark;
private BigDecimal matchEntryThickMin;
private BigDecimal matchEntryThickMax;
private BigDecimal matchExitThickMin;
private BigDecimal matchExitThickMax;
private BigDecimal matchEntryWidthMin;
private BigDecimal matchEntryWidthMax;
private BigDecimal matchExitWidthMin;
private BigDecimal matchExitWidthMax;
private String matchSteelGrade;
}

View File

@@ -50,4 +50,9 @@ public class WmsProductionLine extends BaseEntity {
*/
private String remark;
/**
* 产线支持的操作类型逗号分隔1=分卷2=合卷3=更新)
*/
private String actionType;
}

View File

@@ -378,5 +378,8 @@ public class WmsMaterialCoilBo extends BaseEntity {
*/
@TableField(exist = false)
private Boolean onlyEmptyPackingStatus;
private Long specId;
private Long versionId;
}

View File

@@ -8,6 +8,7 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 规程版本业务对象 wms_process_spec_version
@@ -38,4 +39,14 @@ public class WmsProcessSpecVersionBo extends BaseEntity {
private String status;
private String remark;
private BigDecimal matchEntryThickMin;
private BigDecimal matchEntryThickMax;
private BigDecimal matchExitThickMin;
private BigDecimal matchExitThickMax;
private BigDecimal matchEntryWidthMin;
private BigDecimal matchEntryWidthMax;
private BigDecimal matchExitWidthMin;
private BigDecimal matchExitWidthMax;
private String matchSteelGrade;
}

View File

@@ -334,5 +334,10 @@ public class WmsMaterialCoilVo extends BaseEntity {
* 关联的订单列表通过wms_coil_contract_rel中间表JOIN crm_order
*/
private List<com.klp.domain.vo.WmsCoilContractRelVo> orderList;
private Long specId;
private Long versionId;
private String specCode;
private String versionCode;
}

View File

@@ -5,6 +5,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -41,4 +42,18 @@ public class WmsProcessSpecVersionVo {
@ExcelProperty(value = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
private BigDecimal matchEntryThickMin;
private BigDecimal matchEntryThickMax;
private BigDecimal matchExitThickMin;
private BigDecimal matchExitThickMax;
private BigDecimal matchEntryWidthMin;
private BigDecimal matchEntryWidthMax;
private BigDecimal matchExitWidthMin;
private BigDecimal matchExitWidthMax;
private String matchSteelGrade;
private String specCode;
private String specName;
private Long lineId;
}

View File

@@ -64,6 +64,11 @@ public class WmsProductionLineVo {
@ExcelProperty(value = "备注")
private String remark;
/**
* 产线支持的操作类型逗号分隔1=分卷2=合卷3=更新)
*/
private String actionType;
/**
* 关联的排产计划明细数量
*/

View File

@@ -7,6 +7,7 @@ import com.klp.domain.WmsCoilPendingAction;
import com.klp.domain.vo.WmsCoilPendingActionVo;
import com.klp.domain.vo.WmsCoilPendingActionIdCoilVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 钢卷待操作Mapper接口
@@ -44,5 +45,29 @@ public interface WmsCoilPendingActionMapper extends BaseMapperPlus<WmsCoilPendin
* @return 待操作记录
*/
WmsCoilPendingAction selectByActionIdAndDelFlag(@Param("actionId") Long actionId, @Param("delFlag") Integer delFlag);
/**
* 批量查询:找出 processed_coil_ids 中包含给定 coilId 列表中任意一个的记录,
* 只返回 action_type 和 processed_coil_ids 两列(供规程同步线路匹配用)。
*/
List<WmsCoilPendingAction> selectActionTypeByProcessedCoilIds(@Param("coilIds") List<Long> coilIds);
/**
* 获取所有有效待操作记录的 processed_coil_ids 字符串列表(供规程同步主键展开用)。
*/
List<String> selectAllProcessedCoilIdStrings();
/**
* 获取所有有效待操作记录的 processed_coil_ids + action_status + action_type
* 供规程同步页面同时构建"已处理钢卷 ID 集"和"录入状态"映射。
*/
List<WmsCoilPendingAction> selectAllProcessedCoilIdsAndActionStatus();
/**
* 规程同步:按 actionType 集合查询全部待操作记录(含未录入和已录入)。
* 返回 action_id / coil_id / action_type / action_status / processed_coil_ids 五列。
*/
List<WmsCoilPendingAction> selectPendingByActionTypes(
@Param("actionTypes") java.util.Collection<Integer> actionTypes);
}

View File

@@ -47,6 +47,8 @@ public interface WmsMaterialCoilMapper extends BaseMapperPlus<WmsMaterialCoilMap
List<WmsMaterialCoilVo> selectVoListWithDynamicJoin(@Param("ew")QueryWrapper<WmsMaterialCoil> lqw);
Map<String, Object> selectCountForSpecSync(@Param("ew") QueryWrapper<WmsMaterialCoil> qw);
List<Map<String, Object>> getDistributionByActualWarehouse(@Param("itemType") String itemType, @Param("itemId") Long itemId);
List<Map<String, Object>> getDistributionByActualItemType(@Param("itemType")String itemType,@Param("itemId") Long itemId);

View File

@@ -55,6 +55,44 @@ public interface IWmsMaterialCoilService {
*/
List<WmsMaterialCoilVo> queryList(WmsMaterialCoilBo bo);
/**
* 按入场卷号批量查询供规程同步分页列表使用IN 查询避免 N 次单查)
*/
List<WmsMaterialCoilVo> queryByEnterCoilNos(List<String> enterCoilNos);
/**
* 规程同步专用DB 层分页查询(避免全表加载),支持 material LIKE 过滤和 syncStatus 过滤。
* specIds 不为空时追加 mc.spec_id IN (...) 条件。
*/
List<WmsMaterialCoilVo> queryPageForSpecSync(WmsMaterialCoilBo bo, int pageNum, int pageSize,
String syncStatus, String material, java.util.Set<Long> specIds);
/**
* 规程同步专用:一次 SQL 返回 total/synced/unsynced 三个计数。
*/
Map<String, Object> countForSpecSync(WmsMaterialCoilBo bo, String material, java.util.Set<Long> specIds);
/**
* 规程同步专用:以 processed_coil_ids 展开后的 coilId 集合为主,分页查 L3 钢卷(含 item join
*/
List<WmsMaterialCoilVo> queryByProcessedCoilIds(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
String syncStatus, java.util.Set<Long> filterSpecIds, int offset, int pageSize);
/**
* 规程同步专用:以 processed_coil_ids 展开后的 coilId 集合为主,计算满足 L3 过滤条件的总数。
*/
long countByProcessedCoilIds(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
String syncStatus, java.util.Set<Long> filterSpecIds);
/**
* 规程同步专用:不区分 syncStatus一次查询返回 total/synced/unsynced/movedOn 整体汇总(用于统计条)。
*/
java.util.Map<String, Long> getOverallSyncStats(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
java.util.Set<Long> filterSpecIds);
/**
* 统计筛选条件下的全量汇总数据高性能只查sum/count
* 独立的统计接口,不影响分页查询

View File

@@ -5,6 +5,7 @@ import com.klp.common.core.page.TableDataInfo;
import com.klp.domain.bo.WmsProcessSpecVersionBo;
import com.klp.domain.vo.WmsProcessSpecVersionVo;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
@@ -31,4 +32,25 @@ public interface IWmsProcessSpecVersionService {
* 将指定版本设为当前规程下唯一生效版本
*/
Boolean activateVersion(Long versionId);
/**
* 根据L2实绩参数匹配最佳规程版本。
* 对所有 isActive=1 的版本评分命中条件最多者胜出返回匹配的版本VO含specCode/specName无匹配返回null。
* lineId 不为空时,只在属于该产线的规程版本中匹配。
*/
WmsProcessSpecVersionVo matchBestVersion(BigDecimal entryThick, BigDecimal exitThick,
BigDecimal entryWidth, BigDecimal exitWidth,
String grade, Long lineId);
/**
* 加载所有生效版本并批量填充 specCode/specName供分页列表一次性预加载使用。
* lineId 不为空时只返回属于该产线的版本。
*/
List<WmsProcessSpecVersionVo> queryActiveVersionsEnriched(Long lineId);
/**
* 加载全部版本(含非生效版本)并批量填充 specCode/specName/lineId。
* 用于展示已绑定规程信息,避免钢卷绑定旧版本后查不到名称。
*/
List<WmsProcessSpecVersionVo> queryAllVersionsEnriched();
}

View File

@@ -1181,6 +1181,122 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
return list;
}
@Override
public List<WmsMaterialCoilVo> queryByEnterCoilNos(List<String> enterCoilNos) {
if (enterCoilNos == null || enterCoilNos.isEmpty()) {
return Collections.emptyList();
}
QueryWrapper<WmsMaterialCoil> qw = new QueryWrapper<>();
qw.in("mc.enter_coil_no", enterCoilNos).eq("mc.del_flag", 0);
return baseMapper.selectVoListWithDynamicJoin(qw);
}
@Override
public List<WmsMaterialCoilVo> queryPageForSpecSync(WmsMaterialCoilBo bo, int pageNum, int pageSize,
String syncStatus, String material, java.util.Set<Long> specIds) {
QueryWrapper<WmsMaterialCoil> qw = buildQueryWrapperPlus(bo);
applySpecSyncExtras(qw, syncStatus, material, specIds);
int offset = Math.max(0, (pageNum - 1) * pageSize);
qw.last("LIMIT " + offset + ", " + pageSize);
return baseMapper.selectVoListWithDynamicJoin(qw);
}
@Override
public Map<String, Object> countForSpecSync(WmsMaterialCoilBo bo, String material, java.util.Set<Long> specIds) {
QueryWrapper<WmsMaterialCoil> qw = buildQueryWrapperPlus(bo);
applySpecSyncExtras(qw, null, material, specIds);
return baseMapper.selectCountForSpecSync(qw);
}
private void applySpecSyncExtras(QueryWrapper<WmsMaterialCoil> qw, String syncStatus,
String material, java.util.Set<Long> specIds) {
if (StringUtils.isNotBlank(material)) {
String like = "%" + material.toLowerCase() + "%";
qw.apply("LOWER(CASE WHEN mc.item_type = 'raw_material' THEN COALESCE(rm.material,'') "
+ "ELSE COALESCE(p.material,'') END) LIKE {0}", like);
}
if (specIds != null && !specIds.isEmpty()) {
qw.in("mc.spec_id", specIds);
}
if ("synced".equals(syncStatus)) qw.isNotNull("mc.spec_id");
else if ("unsynced".equals(syncStatus)) qw.isNull("mc.spec_id");
}
@Override
public List<WmsMaterialCoilVo> queryByProcessedCoilIds(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
String syncStatus, java.util.Set<Long> filterSpecIds, int offset, int pageSize) {
if (coilIds == null || coilIds.isEmpty()) return Collections.emptyList();
QueryWrapper<WmsMaterialCoil> qw = buildProcessedCoilQw(coilIds, enterCoilNo, currentCoilNo,
material, qualityStatus, syncStatus, filterSpecIds);
qw.last("LIMIT " + offset + ", " + pageSize);
return baseMapper.selectVoListWithDynamicJoin(qw);
}
@Override
public long countByProcessedCoilIds(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
String syncStatus, java.util.Set<Long> filterSpecIds) {
if (coilIds == null || coilIds.isEmpty()) return 0L;
QueryWrapper<WmsMaterialCoil> qw = buildProcessedCoilQw(coilIds, enterCoilNo, currentCoilNo,
material, qualityStatus, syncStatus, filterSpecIds);
Map<String, Object> result = baseMapper.selectCountForSpecSync(qw);
if (result == null) return 0L;
Object total = result.get("total");
if (total == null) return 0L;
try { return Long.parseLong(total.toString()); } catch (NumberFormatException e) { return 0L; }
}
@Override
public java.util.Map<String, Long> getOverallSyncStats(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
java.util.Set<Long> filterSpecIds) {
java.util.Map<String, Long> stats = new java.util.LinkedHashMap<>();
stats.put("total", 0L);
stats.put("synced", 0L);
stats.put("unsynced", 0L);
stats.put("movedOn", 0L);
if (coilIds == null || coilIds.isEmpty()) return stats;
// syncStatus=null → 不加 version_id 过滤,一次查出全量分组
QueryWrapper<WmsMaterialCoil> qw = buildProcessedCoilQw(
coilIds, enterCoilNo, currentCoilNo, material, qualityStatus, null, filterSpecIds);
Map<String, Object> raw = baseMapper.selectCountForSpecSync(qw);
if (raw == null) return stats;
stats.put("total", safeLong(raw, "total"));
stats.put("synced", safeLong(raw, "synced"));
stats.put("unsynced", safeLong(raw, "unsynced"));
stats.put("movedOn", safeLong(raw, "movedOn"));
return stats;
}
private long safeLong(Map<String, Object> map, String key) {
Object v = map.get(key);
if (v == null) return 0L;
try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return 0L; }
}
private QueryWrapper<WmsMaterialCoil> buildProcessedCoilQw(java.util.Collection<Long> coilIds,
String enterCoilNo, String currentCoilNo, String material, String qualityStatus,
String syncStatus, java.util.Set<Long> filterSpecIds) {
QueryWrapper<WmsMaterialCoil> qw = new QueryWrapper<>();
// 不过滤 data_typedata_type=0已流转/历史)和 data_type=1当前均展示
// movedOn 状态仅作页面标记,不阻断规程绑定
qw.in("mc.coil_id", coilIds).eq("mc.del_flag", 0);
if (StringUtils.isNotBlank(enterCoilNo)) qw.like("mc.enter_coil_no", enterCoilNo);
if (StringUtils.isNotBlank(currentCoilNo)) qw.like("mc.current_coil_no", currentCoilNo);
if (StringUtils.isNotBlank(qualityStatus)) qw.eq("mc.quality_status", qualityStatus);
if (StringUtils.isNotBlank(material)) {
qw.apply("LOWER(CASE WHEN mc.item_type = 'raw_material' THEN COALESCE(rm.material,'') "
+ "ELSE COALESCE(p.material,'') END) LIKE {0}", "%" + material.toLowerCase() + "%");
}
if (filterSpecIds != null && !filterSpecIds.isEmpty()) qw.in("mc.spec_id", filterSpecIds);
// 用 version_id 判断是否已绑规程,与控制层 coil.getVersionId()!=null 的逻辑保持一致
// spec_id 有时可能为空(老数据只写了 version_id用 version_id 更可靠
if ("synced".equals(syncStatus)) qw.isNotNull("mc.version_id");
if ("unsynced".equals(syncStatus)) qw.isNull("mc.version_id");
return qw;
}
@Override
public String queryQualityStatusByWarehouseIdAndCurrentCoilNo(Long warehouseId, String currentCoilNo) {
if (warehouseId == null || StringUtils.isBlank(currentCoilNo)) {

View File

@@ -24,8 +24,12 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 规程版本Service实现
@@ -139,6 +143,135 @@ public class WmsProcessSpecVersionServiceImpl implements IWmsProcessSpecVersionS
return baseMapper.updateById(one) > 0;
}
@Override
public WmsProcessSpecVersionVo matchBestVersion(BigDecimal entryThick, BigDecimal exitThick,
BigDecimal entryWidth, BigDecimal exitWidth,
String grade, Long lineId) {
LambdaQueryWrapper<WmsProcessSpecVersion> lqw = Wrappers.lambdaQuery();
lqw.eq(WmsProcessSpecVersion::getIsActive, 1);
List<WmsProcessSpecVersionVo> versions = baseMapper.selectVoList(lqw);
if (versions.isEmpty()) {
return null;
}
// 按产线ID过滤只匹配属于指定产线的规程版本
if (lineId != null) {
List<Long> specIds = versions.stream()
.map(WmsProcessSpecVersionVo::getSpecId).distinct().collect(Collectors.toList());
LambdaQueryWrapper<WmsProcessSpec> slqw = Wrappers.lambdaQuery();
slqw.in(WmsProcessSpec::getSpecId, specIds);
slqw.eq(WmsProcessSpec::getLineId, lineId);
Set<Long> validSpecIds = wmsProcessSpecMapper.selectList(slqw)
.stream().map(WmsProcessSpec::getSpecId).collect(Collectors.toSet());
versions = versions.stream()
.filter(v -> validSpecIds.contains(v.getSpecId())).collect(Collectors.toList());
if (versions.isEmpty()) {
return null;
}
}
WmsProcessSpecVersionVo best = null;
int bestScore = -1;
for (WmsProcessSpecVersionVo v : versions) {
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.isNotBlank(grade) && StringUtils.isNotBlank(v.getMatchSteelGrade())
&& grade.toLowerCase().contains(v.getMatchSteelGrade().toLowerCase())) {
score++;
}
if (score > bestScore) {
bestScore = score;
best = v;
}
}
if (best == null || bestScore == 0) {
return null;
}
WmsProcessSpec spec = wmsProcessSpecMapper.selectById(best.getSpecId());
if (spec != null) {
best.setSpecCode(spec.getSpecCode());
best.setSpecName(spec.getSpecName());
}
return best;
}
@Override
public List<WmsProcessSpecVersionVo> queryActiveVersionsEnriched(Long lineId) {
LambdaQueryWrapper<WmsProcessSpecVersion> lqw = Wrappers.lambdaQuery();
lqw.eq(WmsProcessSpecVersion::getIsActive, 1);
List<WmsProcessSpecVersionVo> versions = baseMapper.selectVoList(lqw);
if (versions.isEmpty()) {
return versions;
}
if (lineId != null) {
List<Long> specIds = versions.stream()
.map(WmsProcessSpecVersionVo::getSpecId).distinct().collect(Collectors.toList());
LambdaQueryWrapper<WmsProcessSpec> slqw = Wrappers.lambdaQuery();
slqw.in(WmsProcessSpec::getSpecId, specIds);
slqw.eq(WmsProcessSpec::getLineId, lineId);
Set<Long> validSpecIds = wmsProcessSpecMapper.selectList(slqw)
.stream().map(WmsProcessSpec::getSpecId).collect(Collectors.toSet());
versions = versions.stream()
.filter(v -> validSpecIds.contains(v.getSpecId())).collect(Collectors.toList());
if (versions.isEmpty()) {
return versions;
}
}
enrichVersionsWithSpec(versions);
return versions;
}
@Override
public List<WmsProcessSpecVersionVo> queryAllVersionsEnriched() {
// 不过滤 isActive获取全部版本含历史版本用于展示已绑定规程名称
List<WmsProcessSpecVersionVo> versions = baseMapper.selectVoList(Wrappers.lambdaQuery());
if (versions.isEmpty()) return versions;
enrichVersionsWithSpec(versions);
return versions;
}
/** 批量填充 specCode / specName / lineId公共逻辑 */
private void enrichVersionsWithSpec(List<WmsProcessSpecVersionVo> versions) {
List<Long> specIds = versions.stream()
.map(WmsProcessSpecVersionVo::getSpecId).distinct().collect(Collectors.toList());
LambdaQueryWrapper<WmsProcessSpec> sq = Wrappers.lambdaQuery();
sq.in(WmsProcessSpec::getSpecId, specIds);
Map<Long, WmsProcessSpec> specMap = wmsProcessSpecMapper.selectList(sq).stream()
.collect(Collectors.toMap(WmsProcessSpec::getSpecId, s -> s, (a, b) -> a));
for (WmsProcessSpecVersionVo v : versions) {
WmsProcessSpec spec = specMap.get(v.getSpecId());
if (spec != null) {
v.setSpecCode(spec.getSpecCode());
v.setSpecName(spec.getSpecName());
v.setLineId(spec.getLineId());
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {

View File

@@ -182,7 +182,47 @@
WHERE action_id = #{actionId}
</update>
<!-- 规程同步:获取全部有效记录的 processed_coil_ids 字符串(用于 Java 层展开) -->
<select id="selectAllProcessedCoilIdStrings" resultType="java.lang.String">
SELECT processed_coil_ids
FROM wms_coil_pending_action
WHERE del_flag = 0
AND processed_coil_ids IS NOT NULL
AND processed_coil_ids != ''
</select>
<!-- 规程同步:同时返回 processed_coil_ids 和 action_status供 Java 层展开并构建录入状态映射 -->
<select id="selectAllProcessedCoilIdsAndActionStatus" resultMap="WmsCoilPendingActionResult">
SELECT action_type, action_status, processed_coil_ids
FROM wms_coil_pending_action
WHERE del_flag = 0
AND processed_coil_ids IS NOT NULL
AND processed_coil_ids != ''
</select>
<!-- 规程同步:按 actionType 集合查询全部待操作(含未录入 action_status!=2用于构建规程同步主数据源 -->
<select id="selectPendingByActionTypes" resultMap="WmsCoilPendingActionResult">
SELECT action_id, coil_id, action_type, action_status, processed_coil_ids
FROM wms_coil_pending_action
WHERE del_flag = 0
AND action_type IN
<foreach collection="actionTypes" item="at" open="(" separator="," close=")">
#{at}
</foreach>
</select>
<!-- 规程同步:批量查找 processed_coil_ids 中包含给定 coilId 的记录(每页最多 40 个 FIND_IN_SET 条件) -->
<select id="selectActionTypeByProcessedCoilIds" resultMap="WmsCoilPendingActionResult">
SELECT action_id, action_type, processed_coil_ids
FROM wms_coil_pending_action
WHERE del_flag = 0
AND processed_coil_ids IS NOT NULL
AND (
<foreach collection="coilIds" item="id" separator=" OR ">
FIND_IN_SET(#{id}, processed_coil_ids)
</foreach>
)
</select>
</mapper>

View File

@@ -40,6 +40,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="saleName" column="sale_name"/>
<result property="coilSurfaceTreatment" column="coil_surface_treatment"/>
<result property="transferType" column="transfer_type"/>
<result property="specId" column="spec_id"/>
<result property="versionId" column="version_id"/>
</resultMap>
@@ -1097,5 +1099,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
LIMIT 1
</select>
<!-- 规程同步页面:一次 SQL 返回 total/synced/unsynced 三个计数 -->
<select id="selectCountForSpecSync" resultType="java.util.Map">
SELECT COUNT(*) AS total,
SUM(CASE WHEN mc.version_id IS NOT NULL THEN 1 ELSE 0 END) AS synced,
SUM(CASE WHEN mc.version_id IS NULL THEN 1 ELSE 0 END) AS unsynced,
SUM(CASE WHEN mc.data_type = 0 THEN 1 ELSE 0 END) AS movedOn
FROM wms_material_coil mc
LEFT JOIN wms_raw_material rm ON mc.item_type = 'raw_material' AND mc.item_id = rm.raw_material_id AND rm.del_flag = 0
LEFT JOIN wms_product p ON mc.item_type = 'product' AND mc.item_id = p.product_id AND p.del_flag = 0
${ew.customSqlSegment}
</select>
</mapper>

View File

@@ -14,6 +14,15 @@
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="remark" column="remark"/>
<result property="matchEntryThickMin" column="match_entry_thick_min"/>
<result property="matchEntryThickMax" column="match_entry_thick_max"/>
<result property="matchExitThickMin" column="match_exit_thick_min"/>
<result property="matchExitThickMax" column="match_exit_thick_max"/>
<result property="matchEntryWidthMin" column="match_entry_width_min"/>
<result property="matchEntryWidthMax" column="match_entry_width_max"/>
<result property="matchExitWidthMin" column="match_exit_width_min"/>
<result property="matchExitWidthMax" column="match_exit_width_max"/>
<result property="matchSteelGrade" column="match_steel_grade"/>
</resultMap>
</mapper>