refactor(wms): 重构钢卷追溯组件以支持合卷展示

- 将原有的线性时间轴改造成支持线性段和合卷并排段的布局结构
- 新增 TraceStepBody 组件用于统一渲染追溯步骤内容
- 在后端服务中添加 traceLayout 字段用于返回前端展示结构
- 实现合卷场景下的多列并排展示功能
- 优化步骤排序逻辑,确保按存储顺序正确排列
- 添加合卷前各卷加工过程的并排展示界面
- 实现合卷汇聚节点的特殊展示效果
This commit is contained in:
2026-05-14 15:57:25 +08:00
parent 8075d76c11
commit c53dd4c97e
5 changed files with 505 additions and 131 deletions

View File

@@ -318,7 +318,9 @@ public class WmsMaterialCoilController extends BaseController {
/**
* 钢卷溯源查询
* 根据钢卷ID查询二维码解析content中的steps然后根据steps中的钢卷号反向查询数据库
* 根据钢卷ID查询二维码解析content中的steps然后根据steps中的钢卷号反向查询数据库
* 返回 data.steps 为按二维码存储顺序排列的全量步骤(合卷场景下勿依赖原 step 序号排序,已带 display_step
* data.traceLayout 为前端展示结构linear单时间线与 merge_join合卷前多列并排 + 合卷汇聚)交替。
*
* @param coilId 钢卷ID
* @param currentCoilNo 当前钢卷号(可选参数,用于查询特定子钢卷)

View File

@@ -99,7 +99,7 @@ public interface IWmsMaterialCoilService {
*
* @param coilId 钢卷ID
* @param currentCoilNo 当前钢卷号(可选,用于查询特定子钢卷)
* @return 溯源结果(包含二维码信息数据库记录)
* @return 溯源结果(包含二维码信息、按存储顺序排列的 steps、traceLayout 并排展示结构、数据库记录)
*/
Map<String, Object> queryTrace(Long coilId, String currentCoilNo);

View File

@@ -2095,10 +2095,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
// 合并所有参与合卷的原始钢卷的历史steps
List<Map<String, Object>> steps = new ArrayList<>();
// 从参与合卷的原始钢卷中获取二维码信息并合并
// 从参与合卷的原始钢卷中获取二维码信息并合并,并记录每个父卷携带的历史步数(用于溯源并排展示)
List<Integer> parentHistStepCounts = new ArrayList<>();
if (originalCoils != null && !originalCoils.isEmpty()) {
for (WmsMaterialCoilBo originalCoilBo : originalCoils) {
if (originalCoilBo.getCoilId() != null) {
int sizeBefore = steps.size();
// 查询原始钢卷的二维码信息
WmsMaterialCoil originalCoil = baseMapper.selectById(originalCoilBo.getCoilId());
if (originalCoil != null && originalCoil.getQrcodeRecordId() != null) {
@@ -2114,6 +2116,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
}
}
parentHistStepCounts.add(steps.size() - sizeBefore);
}
}
}
@@ -2139,6 +2142,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
mergeStep.put("parent_coil_nos", String.join(",", originalCoilNos));
mergeStep.put("parent_coil_ids", String.join(",", originalCoilIds));
mergeStep.put("parent_hist_step_counts", parentHistStepCounts);
mergeStep.put("new_current_coil_no", mergedCoilBo.getCurrentCoilNo());
mergeStep.put("operator", LoginHelper.getUsername()); // 操作者
steps.add(mergeStep);
@@ -2468,11 +2472,14 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
// 合卷会把多条产线的 steps 顺序拼接,各线局部 step 均为 1..n存在重复序号必须保留 JSON 数组顺序,不能按 step 字段排序
List<Map<String, Object>> orderedSteps = steps != null ? new ArrayList<>(steps) : new ArrayList<>();
Set<String> allCoilNos = new HashSet<>();
Set<String> operatorUsernames = new HashSet<>();
if (steps != null) {
for (Map<String, Object> step : steps) {
if (!orderedSteps.isEmpty()) {
for (Map<String, Object> step : orderedSteps) {
extractCoilNo(step, "current_coil_no", allCoilNos);
extractCoilNo(step, "new_current_coil_no", allCoilNos);
extractCoilNo(step, "old_current_coil_no", allCoilNos);
@@ -2493,19 +2500,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
Map<String, String> operatorNicknameMap = getOperatorNicknames(operatorUsernames);
List<Map<String, Object>> allSteps = new ArrayList<>(steps != null ? steps : new ArrayList<>());
allSteps.sort((a, b) -> {
Integer stepA = (Integer) a.get("step");
Integer stepB = (Integer) b.get("step");
if (stepA == null) stepA = 0;
if (stepB == null) stepB = 0;
return stepA.compareTo(stepB);
});
List<Map<String, Object>> allSteps = orderedSteps;
for (int i = 0; i < allSteps.size(); i++) {
allSteps.get(i).put("display_step", i + 1);
allSteps.get(i).put("original_step", allSteps.get(i).get("step"));
allSteps.get(i).put("storage_index", i);
allSteps.get(i).put("qrcode_serial", qrRecord.getSerialNumber());
allSteps.get(i).put("qrcode_id", qrRecord.getRecordId());
@@ -2517,6 +2517,8 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
}
List<Map<String, Object>> traceLayout = buildTraceLayout(allSteps);
Set<String> filteredCoilNos = allCoilNos;
if (currentCoilNo != null && !currentCoilNo.trim().isEmpty()) {
final String filterValue = currentCoilNo;
@@ -2567,6 +2569,7 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("qrcode", qrRecord);
resultMap.put("steps", allSteps);
resultMap.put("traceLayout", traceLayout);
resultMap.put("records", result);
return resultMap;
@@ -2643,6 +2646,181 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
}
private static int stepNumberValue(Map<String, Object> step) {
Object s = step.get("step");
if (s == null) {
return 0;
}
if (s instanceof Number) {
return ((Number) s).intValue();
}
try {
return Integer.parseInt(s.toString().trim());
} catch (NumberFormatException e) {
return 0;
}
}
private static boolean isMergeTraceStep(Map<String, Object> step) {
Object op = step.get("operation");
return op != null && "合卷".equals(op.toString());
}
private static List<String> splitCommaTokens(Object value) {
if (value == null) {
return Collections.emptyList();
}
return Arrays.stream(value.toString().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
private static List<Integer> readParentHistStepCounts(Map<String, Object> mergeStep) {
Object raw = mergeStep.get("parent_hist_step_counts");
if (!(raw instanceof List)) {
return null;
}
List<?> list = (List<?>) raw;
List<Integer> out = new ArrayList<>();
for (Object o : list) {
if (o instanceof Number) {
out.add(((Number) o).intValue());
} else if (o != null) {
try {
out.add(Integer.parseInt(o.toString().trim()));
} catch (NumberFormatException e) {
return null;
}
} else {
out.add(0);
}
}
return out.isEmpty() ? null : out;
}
private static List<Integer> inferLaneSizesByStepReset(List<Map<String, Object>> buffer) {
if (buffer == null || buffer.isEmpty()) {
return Collections.emptyList();
}
List<Integer> lengths = new ArrayList<>();
int runStart = 0;
Integer prev = null;
for (int i = 0; i < buffer.size(); i++) {
int cur = stepNumberValue(buffer.get(i));
if (prev != null && (cur < prev || (cur == 1 && prev > 1))) {
lengths.add(i - runStart);
runStart = i;
}
prev = cur;
}
lengths.add(buffer.size() - runStart);
return lengths;
}
private static List<Integer> distributeEvenly(int total, int parts) {
if (parts <= 0) {
return Collections.emptyList();
}
List<Integer> r = new ArrayList<>();
int base = total / parts;
int rem = total % parts;
for (int i = 0; i < parts; i++) {
r.add(base + (i < rem ? 1 : 0));
}
return r;
}
private List<List<Map<String, Object>>> sliceBufferIntoMergeLanes(List<Map<String, Object>> buffer,
Map<String, Object> mergeStep) {
List<String> parentIds = splitCommaTokens(mergeStep.get("parent_coil_ids"));
List<String> parentNos = splitCommaTokens(mergeStep.get("parent_coil_nos"));
int n = Math.max(parentIds.size(), parentNos.size());
if (n <= 0) {
n = 1;
}
if (buffer == null) {
buffer = Collections.emptyList();
}
int total = buffer.size();
List<Integer> counts = readParentHistStepCounts(mergeStep);
if (counts == null || counts.size() != n || counts.stream().mapToInt(Integer::intValue).sum() != total) {
List<Integer> inferred = inferLaneSizesByStepReset(buffer);
if (inferred.size() == n) {
counts = inferred;
} else {
counts = distributeEvenly(total, n);
}
}
List<List<Map<String, Object>>> lanes = new ArrayList<>();
int offset = 0;
for (int i = 0; i < n; i++) {
int len = i < counts.size() ? Math.max(0, counts.get(i)) : 0;
int end = Math.min(offset + len, total);
lanes.add(new ArrayList<>(buffer.subList(offset, end)));
offset = end;
}
if (offset < total) {
if (!lanes.isEmpty()) {
lanes.get(lanes.size() - 1).addAll(buffer.subList(offset, total));
} else {
lanes.add(new ArrayList<>(buffer.subList(0, total)));
}
}
return lanes;
}
private static List<String> buildMergeLaneLabels(Map<String, Object> mergeStep, int laneCount) {
List<String> nos = splitCommaTokens(mergeStep.get("parent_coil_nos"));
List<String> ids = splitCommaTokens(mergeStep.get("parent_coil_ids"));
List<String> labels = new ArrayList<>();
for (int i = 0; i < laneCount; i++) {
String no = i < nos.size() ? nos.get(i) : null;
String id = i < ids.size() ? ids.get(i) : null;
if (StringUtils.isNotBlank(no)) {
labels.add(no);
} else if (StringUtils.isNotBlank(id)) {
labels.add("ID:" + id);
} else {
labels.add("来源 " + (i + 1));
}
}
return labels;
}
/**
* 将按存储顺序排列的 steps 拆成「线性段 + 合卷并排段」,供前端展示。
*/
private List<Map<String, Object>> buildTraceLayout(List<Map<String, Object>> orderedSteps) {
List<Map<String, Object>> layout = new ArrayList<>();
if (orderedSteps == null || orderedSteps.isEmpty()) {
return layout;
}
List<Map<String, Object>> buffer = new ArrayList<>();
for (Map<String, Object> s : orderedSteps) {
if (isMergeTraceStep(s)) {
Map<String, Object> panel = new HashMap<>();
panel.put("type", "merge_join");
List<List<Map<String, Object>>> lanes = sliceBufferIntoMergeLanes(new ArrayList<>(buffer), s);
panel.put("lanes", lanes);
panel.put("laneLabels", buildMergeLaneLabels(s, lanes.size()));
panel.put("mergeStep", s);
layout.add(panel);
buffer.clear();
} else {
buffer.add(s);
}
}
if (!buffer.isEmpty()) {
Map<String, Object> linear = new HashMap<>();
linear.put("type", "linear");
linear.put("steps", new ArrayList<>(buffer));
layout.add(linear);
}
return layout;
}
/**
* 查询各个库区中不同类型的钢卷分布情况
* 按库区分组统计每种物品类型和物品ID的钢卷数量和重量