feat(wms): 添加钢卷加工链追溯功能

- 在IWmsMaterialCoilService中新增queryCoilChain方法实现双向追溯
- 在WmsMaterialCoilController中添加/chain/all/{coilId}接口
- 在WmsMaterialCoilMapper中新增selectByParentCoilIds批量查询方法
- 在Mapper XML中实现FIND_IN_SET匹配逗号分隔的parent_coil_id查询
- 实现完整的双向追溯逻辑:向上追溯祖先向下查找后代支持合卷场景
- 创建CoilChainVo数据传输对象包含追溯结果和节点关系信息
- 实现BFS算法构建完整的加工链父子关系映射和深度计算
This commit is contained in:
2026-06-17 16:49:19 +08:00
parent 605f7b85a1
commit 585017873c
6 changed files with 343 additions and 0 deletions

View File

@@ -369,6 +369,24 @@ public class WmsMaterialCoilController extends BaseController {
return R.ok(iWmsMaterialCoilService.queryById(coilId));
}
/**
* 钢卷加工链追溯查询
* 根据钢卷ID双向追溯完整加工链条
* - 向上沿parentCoilId一直查到根节点
* - 向下:查找所有后代钢卷
*
* @param coilId 钢卷ID
*/
@GetMapping("/chain/all/{coilId}")
public R<com.klp.domain.vo.CoilChainVo> getChain(@NotNull(message = "主键不能为空")
@PathVariable("coilId") Long coilId) {
com.klp.domain.vo.CoilChainVo result = iWmsMaterialCoilService.queryCoilChain(coilId);
if (result == null) {
return R.fail("钢卷不存在");
}
return R.ok(result);
}
/**
* 新增钢卷物料表
*/

View File

@@ -0,0 +1,55 @@
package com.klp.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 钢卷加工链追溯结果
*
* @author Joshi
* @date 2026-06-17
*/
@Data
public class CoilChainVo {
/**
* 当前查询的钢卷
*/
private WmsMaterialCoilVo self;
/**
* 祖先钢卷列表(从直接父级 → 根,按追溯顺序排列)
*/
private List<CoilChainNode> ancestors;
/**
* 后代钢卷列表(所有子孙节点)
*/
private List<CoilChainNode> descendants;
/**
* 加工链扁平列表(按加工顺序排列:根 → ... → 当前 → ... → 叶子)
* 每个节点标注 depth0=根,正数向下)和 relation
*/
private List<CoilChainNode> traceList;
/**
* 链条上钢卷总数
*/
private int totalCount;
/**
* 加工链节点
*/
@Data
public static class CoilChainNode {
/** 钢卷信息 */
private WmsMaterialCoilVo coil;
/** 相对当前钢卷的深度:负数=祖先0=自身,正数=后代 */
private int depth;
/** 节点关系类型 */
private String relation;
/** 此节点的直接父级ID列表逗号分隔时会有多个 */
private String parentCoilId;
}
}

View File

@@ -163,5 +163,14 @@ public interface WmsMaterialCoilMapper extends BaseMapperPlus<WmsMaterialCoilMap
* @return 材质信息
*/
String selectEarliestHotRolledMaterial(@Param("enterCoilNo") String enterCoilNo);
/**
* 根据父级钢卷ID列表批量查询子钢卷
* 使用FIND_IN_SET匹配逗号分隔的parent_coil_id字段
*
* @param coilIds 父级钢卷ID列表
* @return 子钢卷列表
*/
List<WmsMaterialCoil> selectByParentCoilIds(@Param("coilIds") java.util.Collection<Long> coilIds);
}

View File

@@ -417,5 +417,17 @@ public interface IWmsMaterialCoilService {
* @param pageQuery 分页参数
*/
TableDataInfo<WmsMaterialCoilVo> queryPageListWithQrcode(WmsMaterialCoilBo bo, PageQuery pageQuery);
/**
* 钢卷加工链追溯查询
* 根据钢卷ID双向追溯完整加工链条
* - 向上追溯沿parentCoilId一直查到根节点无父级为止
* - 向下追溯:查找所有以当前钢卷为父级的后代钢卷
* 支持合卷场景parentCoilId逗号分隔多父级
*
* @param coilId 钢卷ID
* @return 加工链追溯结果包含祖先、自身、后代及扁平化traceList
*/
com.klp.domain.vo.CoilChainVo queryCoilChain(Long coilId);
}

View File

@@ -105,6 +105,242 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
return vo;
}
/**
* 钢卷加工链追溯查询
* 双向追溯向上沿parentCoilId查至根节点向下查所有后代节点
*/
@Override
public CoilChainVo queryCoilChain(Long coilId) {
// 1. 查询起始钢卷
WmsMaterialCoil startEntity = baseMapper.selectById(coilId);
if (startEntity == null) {
return null;
}
// 所有已查询到的钢卷coilId -> entity
Map<Long, WmsMaterialCoil> allCoils = new LinkedHashMap<>();
allCoils.put(coilId, startEntity);
Set<Long> ancestorIds = new LinkedHashSet<>(); // 祖先ID集合
List<Long> ancestorOrder = new ArrayList<>(); // 追溯顺序:直接父级→根
// 2. 向上追溯祖先沿parentCoilId链
Set<Long> parentIds = parseParentCoilIds(startEntity.getParentCoilId());
int maxDepth = 50; // 防止无限循环
while (!parentIds.isEmpty() && maxDepth-- > 0) {
Set<Long> nextParents = new LinkedHashSet<>();
for (Long pid : parentIds) {
if (allCoils.containsKey(pid)) {
continue;
}
WmsMaterialCoil parent = baseMapper.selectById(pid);
if (parent != null) {
allCoils.put(pid, parent);
ancestorIds.add(pid);
ancestorOrder.add(pid);
nextParents.addAll(parseParentCoilIds(parent.getParentCoilId()));
}
}
parentIds = nextParents;
}
// 3. 向下追溯后代BFS用FIND_IN_SET批量查子级
Set<Long> descendantIds = new LinkedHashSet<>();
Map<Long, List<Long>> parentToChildren = new LinkedHashMap<>(); // parentId -> childIds
// BFS队列从当前钢卷开始
Set<Long> currentLevel = new LinkedHashSet<>();
currentLevel.add(coilId);
// 祖先也要参与BFS它们可能也有其他后代分支
for (Long aid : ancestorOrder) {
currentLevel.add(aid);
}
Set<Long> bfsVisited = new LinkedHashSet<>(allCoils.keySet());
maxDepth = 50;
while (!currentLevel.isEmpty() && maxDepth-- > 0) {
List<Long> levelList = new ArrayList<>(currentLevel);
List<WmsMaterialCoil> children = baseMapper.selectByParentCoilIds(levelList);
currentLevel.clear();
if (children != null && !children.isEmpty()) {
for (WmsMaterialCoil child : children) {
Long cid = child.getCoilId();
// 确定子节点属于哪个父节点
Set<Long> childParents = parseParentCoilIds(child.getParentCoilId());
Long matchedParent = null;
for (Long cp : childParents) {
if (levelList.contains(cp)) {
matchedParent = cp;
break;
}
}
if (matchedParent != null) {
parentToChildren
.computeIfAbsent(matchedParent, k -> new ArrayList<>())
.add(cid);
}
if (bfsVisited.add(cid)) {
allCoils.put(cid, child);
descendantIds.add(cid);
currentLevel.add(cid);
}
}
}
}
// 3.1 重建完整的 parentToChildren 映射清空BFS阶段的部分映射基于allCoils完整重建
parentToChildren.clear();
for (Map.Entry<Long, WmsMaterialCoil> entry : allCoils.entrySet()) {
Long cid = entry.getKey();
WmsMaterialCoil coil = entry.getValue();
Set<Long> parents = parseParentCoilIds(coil.getParentCoilId());
for (Long pid : parents) {
if (allCoils.containsKey(pid)) {
parentToChildren
.computeIfAbsent(pid, k -> new ArrayList<>())
.add(cid);
}
}
}
// 4. 计算每个节点相对当前钢卷的深度
// 先找所有根节点parentCoilId为空或父级不在allCoils中的节点
Set<Long> roots = new LinkedHashSet<>();
for (Map.Entry<Long, WmsMaterialCoil> entry : allCoils.entrySet()) {
Long cid = entry.getKey();
WmsMaterialCoil coil = entry.getValue();
Set<Long> parents = parseParentCoilIds(coil.getParentCoilId());
boolean hasParentInChain = false;
for (Long pid : parents) {
if (allCoils.containsKey(pid)) {
hasParentInChain = true;
break;
}
}
if (!hasParentInChain) {
roots.add(cid);
}
}
// BFS从根节点开始计算绝对深度
Map<Long, Integer> absoluteDepth = new LinkedHashMap<>();
Queue<Long> depthQueue = new LinkedList<>();
Set<Long> depthVisited = new LinkedHashSet<>();
for (Long rootId : roots) {
depthQueue.offer(rootId);
absoluteDepth.put(rootId, 0);
depthVisited.add(rootId);
}
while (!depthQueue.isEmpty()) {
Long current = depthQueue.poll();
int currDepth = absoluteDepth.get(current);
List<Long> children = parentToChildren.get(current);
if (children != null) {
for (Long childId : children) {
if (depthVisited.add(childId)) {
absoluteDepth.put(childId, currDepth + 1);
depthQueue.offer(childId);
}
}
}
}
// 转为相对于self的深度
int selfAbsoluteDepth = absoluteDepth.getOrDefault(coilId, 0);
Map<Long, Integer> finalDepth = new LinkedHashMap<>();
for (Map.Entry<Long, Integer> entry : absoluteDepth.entrySet()) {
finalDepth.put(entry.getKey(), entry.getValue() - selfAbsoluteDepth);
}
// 确保self深度为0
finalDepth.put(coilId, 0);
// 5. 构建响应所有entity转VO并批量填充关联对象
List<WmsMaterialCoilVo> allVos = new ArrayList<>();
Map<Long, WmsMaterialCoilVo> voMap = new LinkedHashMap<>();
for (WmsMaterialCoil entity : allCoils.values()) {
WmsMaterialCoilVo vo = new WmsMaterialCoilVo();
BeanUtil.copyProperties(entity, vo);
allVos.add(vo);
voMap.put(entity.getCoilId(), vo);
}
fillRelatedObjectsBatch(allVos);
// 6. 组装CoilChainVo
CoilChainVo result = new CoilChainVo();
result.setSelf(voMap.get(coilId));
// ancestors: 从直接父级→根
List<CoilChainVo.CoilChainNode> ancestorNodes = new ArrayList<>();
for (Long aid : ancestorOrder) {
CoilChainVo.CoilChainNode node = new CoilChainVo.CoilChainNode();
node.setCoil(voMap.get(aid));
node.setDepth(finalDepth.getOrDefault(aid, 0));
node.setRelation("ancestor");
node.setParentCoilId(allCoils.get(aid).getParentCoilId());
ancestorNodes.add(node);
}
result.setAncestors(ancestorNodes);
// descendants: 所有后代BFS顺序
List<CoilChainVo.CoilChainNode> descendantNodes = new ArrayList<>();
for (Long did : descendantIds) {
CoilChainVo.CoilChainNode node = new CoilChainVo.CoilChainNode();
node.setCoil(voMap.get(did));
node.setDepth(finalDepth.getOrDefault(did, 0));
node.setRelation("descendant");
node.setParentCoilId(allCoils.get(did).getParentCoilId());
descendantNodes.add(node);
}
result.setDescendants(descendantNodes);
// traceList: 按深度排序的扁平列表根→self→叶子
List<CoilChainVo.CoilChainNode> traceNodes = new ArrayList<>();
for (Long cid : allCoils.keySet()) {
CoilChainVo.CoilChainNode node = new CoilChainVo.CoilChainNode();
node.setCoil(voMap.get(cid));
int d = finalDepth.getOrDefault(cid, 0);
node.setDepth(d);
if (cid.equals(coilId)) {
node.setRelation("self");
} else if (ancestorIds.contains(cid)) {
node.setRelation("ancestor");
} else {
node.setRelation("descendant");
}
node.setParentCoilId(allCoils.get(cid).getParentCoilId());
traceNodes.add(node);
}
// 按深度排序祖先负→自身0→后代正
traceNodes.sort(Comparator.comparingInt(CoilChainVo.CoilChainNode::getDepth));
result.setTraceList(traceNodes);
result.setTotalCount(allCoils.size());
return result;
}
/**
* 解析parentCoilId字段支持逗号分隔的多父级合卷场景
*/
private Set<Long> parseParentCoilIds(String parentCoilId) {
Set<Long> ids = new LinkedHashSet<>();
if (StringUtils.isBlank(parentCoilId)) {
return ids;
}
for (String s : parentCoilId.split(",")) {
String trimmed = s.trim();
if (StringUtils.isNotBlank(trimmed)) {
try {
ids.add(Long.parseLong(trimmed));
} catch (NumberFormatException ignored) {
// 忽略非法格式
}
}
}
return ids;
}
/**
* 批量填充关联对象信息优化版本避免N+1查询
*/

View File

@@ -1101,5 +1101,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
${ew.customSqlSegment}
</select>
<!-- 根据父级钢卷ID列表批量查询子钢卷使用FIND_IN_SET匹配逗号分隔的parent_coil_id -->
<select id="selectByParentCoilIds" resultMap="WmsMaterialCoilResult">
SELECT * FROM wms_material_coil
WHERE del_flag = 0
AND parent_coil_id IS NOT NULL
AND parent_coil_id != ''
AND (
<foreach collection="coilIds" item="id" separator=" OR ">
FIND_IN_SET(#{id}, parent_coil_id) > 0
</foreach>
)
</select>
</mapper>