feat(wms/material-coil): 新增钢卷囤积统计与二维码详情查询功能

1. 新增囤积统计接口:计算已发货钢卷的平均囤积周期和平均囤积成本
   - 囤积周期 = 发货时间 - 二维码第一步创建时间
   - 囤积成本 = 囤积天数 × 钢卷净重 × 1元/吨/天
   - 支持与分页列表相同的查询条件筛选

2. 新增二维码详情查询接口:在分页查询基础上填充完整的二维码记录信息
   - 前端可通过 qrcodeRecord.content 获取二维码JSON内容
   - 采用批量查询避免N+1性能问题

调整前,钢卷查询无法获取二维码详细内容,也无法统计囤积相关指标;调整后,支持二维码内容查看和囤积成本分析,为仓储成本核算提供数据支持。
This commit is contained in:
2026-06-04 15:45:51 +08:00
parent 37d6830947
commit 5236500f04
3 changed files with 242 additions and 0 deletions

View File

@@ -99,6 +99,27 @@ public class WmsMaterialCoilController extends BaseController {
return iWmsMaterialCoilService.queryPageListWithRejudge(bo, pageQuery);
}
/**
* 查询钢卷物料表列表(包含二维码记录内容)
* 与list接口查询条件完全一致区别是会填充二维码content到qrcodeRecord字段
* 前端可通过qrcodeRecord.content获取二维码JSON内容
*/
@GetMapping("/listWithQrcode")
public TableDataInfo<WmsMaterialCoilVo> listWithQrcode(WmsMaterialCoilBo bo, PageQuery pageQuery) {
return iWmsMaterialCoilService.queryPageListWithQrcode(bo, pageQuery);
}
/**
* 统计已发货钢卷的平均囤积周期和平均囤积成本
* 使用与分页列表相同的查询条件,按发货时间筛选
* 囤积周期:发货时间 - 二维码中第一步的创建时间
* 囤积成本:囤积天数 * 净重(吨) * 1元/吨/天
*/
@PostMapping("/hoardingStatistics")
public R<Map<String, Object>> getHoardingStatistics(@RequestBody WmsMaterialCoilBo bo) {
return R.ok(iWmsMaterialCoilService.getHoardingStatistics(bo));
}
/**
* 统计筛选条件下的全量汇总数据
* 独立的统计接口,使用与分页列表相同的查询条件

View File

@@ -387,5 +387,26 @@ public interface IWmsMaterialCoilService {
* 根据入场钢卷号或当前钢卷号查询钢卷,供双机架计划绑定使用
*/
com.klp.domain.vo.WmsMaterialCoilVo queryByCoilNo(String coilNo);
/**
* 统计已发货钢卷的平均囤积周期和平均囤积成本
* 使用与分页列表相同的查询条件,按发货时间筛选已发货钢卷
* 囤积周期 = 发货时间 - 二维码中第一个步骤的创建时间
* 囤积成本 = 囤积天数 * 净重(吨) * 1元/吨/天
*
* @param bo 查询条件
* @return avgHoardingDays(平均囤积天数), avgHoardingCost(平均囤积成本), totalCount(已发货钢卷数量)
*/
Map<String, Object> getHoardingStatistics(WmsMaterialCoilBo bo);
/**
* 查询钢卷物料表列表(包含二维码记录信息)
* 与queryPageList查询条件完全一致区别是会填充qrcodeRecord字段WmsGenerateRecordVo
* 前端可通过qrcodeRecord.content获取二维码的JSON内容
*
* @param bo 查询条件
* @param pageQuery 分页参数
*/
TableDataInfo<WmsMaterialCoilVo> queryPageListWithQrcode(WmsMaterialCoilBo bo, PageQuery pageQuery);
}

View File

@@ -5849,5 +5849,205 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
return baseMapper.selectVoOne(lqw);
}
/**
* 获取材料卷的囤积统计信息
* 根据查询条件计算钢卷的平均囤积天数、平均囤积成本和符合条件的钢卷总数
* 囤积天数 = 发货时间 - 二维码中第一步操作时间
* 囤积成本 = 囤积天数 × 钢卷净重
* 注意: 此处是统计当前钢卷的重量,但是钢卷在加工的过程中重量会发生变化,如果想要更精确的数据需要对二维码进行一个更细致的拆分才行
* @param bo 查询条件对象,包含筛选钢卷的各种条件
* @return 包含平均囤积天数、平均囤积成本和总数的Map
* - avgHoardingDays: 平均囤积天数保留2位小数
* - avgHoardingCost: 平均囤积成本保留2位小数
* - totalCount: 符合条件的钢卷总数
*/
@Override
public Map<String, Object> getHoardingStatistics(WmsMaterialCoilBo bo) {
// 构建查询条件
QueryWrapper<WmsMaterialCoil> qw = buildQueryWrapperPlus(bo);
// 查询符合条件的钢卷列表
List<WmsMaterialCoilVo> list = baseMapper.selectVoListWithDynamicJoin(qw);
// 初始化返回结果,设置默认值
Map<String, Object> result = new HashMap<>();
result.put("avgHoardingDays", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
result.put("avgHoardingCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
result.put("totalCount", 0);
// 如果没有钢卷数据,直接返回默认值
if (list == null || list.isEmpty()) {
return result;
}
// 收集所有钢卷的二维码记录ID用于批量查询二维码信息
Set<Long> qrcodeIds = list.stream()
.map(WmsMaterialCoilVo::getQrcodeRecordId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 批量查询二维码记录建立ID到二维码对象的映射
Map<Long, WmsGenerateRecordVo> qrcodeMap = new HashMap<>();
if (!qrcodeIds.isEmpty()) {
List<WmsGenerateRecordVo> qrcodeList = generateRecordMapper.selectVoBatchIds(new ArrayList<>(qrcodeIds));
if (qrcodeList != null) {
for (WmsGenerateRecordVo qrcode : qrcodeList) {
if (qrcode != null && qrcode.getRecordId() != null) {
qrcodeMap.put(qrcode.getRecordId(), qrcode);
}
}
}
}
// 初始化统计变量
ObjectMapper objectMapper = new ObjectMapper();
BigDecimal totalHoardingDays = BigDecimal.ZERO;
BigDecimal totalHoardingCost = BigDecimal.ZERO;
int validCount = 0;
// 遍历每个钢卷,计算囤积天数和成本
for (WmsMaterialCoilVo vo : list) {
Date exportTime = vo.getExportTime();
if (exportTime == null) {
exportTime = vo.getUpdateTime(); // 没有发货时间则用更新时间
}
Long qrcodeRecordId = vo.getQrcodeRecordId();
if (qrcodeRecordId == null) {
continue; // 没有二维码记录,跳过
}
WmsGenerateRecordVo qrcode = qrcodeMap.get(qrcodeRecordId);
if (qrcode == null) {
continue; // 二维码记录不存在,跳过
}
try {
// 解析二维码内容,获取操作步骤
@SuppressWarnings("unchecked")
Map<String, Object> contentMap = objectMapper.readValue(qrcode.getContent(), Map.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> steps = (List<Map<String, Object>>) contentMap.get("steps");
if (steps == null || steps.isEmpty()) {
continue; // 没有操作步骤,跳过
}
// 获取第一步操作的时间(钢卷创建时间)
Object createTimeObj = steps.get(0).get("create_time");
if (createTimeObj == null) {
continue; // 没有创建时间,跳过
}
Date firstCreateTime = parseDateFromObject(createTimeObj);
if (firstCreateTime == null) {
continue; // 时间解析失败,跳过
}
// 计算囤积天数:发货时间 - 创建时间
long diffMs = exportTime.getTime() - firstCreateTime.getTime();
BigDecimal days = BigDecimal.valueOf(diffMs)
.divide(BigDecimal.valueOf(1000L * 60 * 60 * 24), 2, RoundingMode.HALF_UP);
totalHoardingDays = totalHoardingDays.add(days);
// 计算囤积成本:囤积天数 × 钢卷净重
BigDecimal netWeight = vo.getNetWeight() != null ? vo.getNetWeight() : BigDecimal.ZERO;
BigDecimal cost = days.multiply(netWeight);
totalHoardingCost = totalHoardingCost.add(cost);
validCount++; // 有效数据计数
} catch (Exception e) {
log.warn("计算囤积统计时解析二维码失败, coilId={}", vo.getCoilId(), e);
}
}
// 计算平均值
if (validCount > 0) {
result.put("avgHoardingDays", totalHoardingDays.divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP));
result.put("avgHoardingCost", totalHoardingCost.divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP));
result.put("totalCount", validCount);
}
return result;
}
/**
* 查询分页列表并填充二维码记录
* 在普通分页查询的基础上,额外填充每个钢卷的二维码详细信息
*
* @param bo 查询条件对象
* @param pageQuery 分页查询参数
* @return 包含二维码信息的分页结果
*/
@Override
public TableDataInfo<WmsMaterialCoilVo> queryPageListWithQrcode(WmsMaterialCoilBo bo, PageQuery pageQuery) {
// 先执行普通分页查询
TableDataInfo<WmsMaterialCoilVo> result = queryPageList(bo, pageQuery);
List<WmsMaterialCoilVo> records = result.getRows();
// 如果有数据,填充二维码记录
if (records != null && !records.isEmpty()) {
fillQrcodeRecords(records);
}
return result;
}
/**
* 批量填充钢卷列表的二维码记录
* 通过批量查询二维码记录避免N+1查询问题
*
* @param voList 钢卷列表
*/
private void fillQrcodeRecords(List<WmsMaterialCoilVo> voList) {
// 收集所有二维码记录ID
Set<Long> qrcodeRecordIds = voList.stream()
.map(WmsMaterialCoilVo::getQrcodeRecordId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (qrcodeRecordIds.isEmpty()) {
return; // 没有二维码记录ID直接返回
}
// 批量查询二维码记录
Map<Long, WmsGenerateRecordVo> qrcodeMap = new HashMap<>();
List<WmsGenerateRecordVo> qrcodeList = generateRecordMapper.selectVoBatchIds(new ArrayList<>(qrcodeRecordIds));
if (qrcodeList != null) {
for (WmsGenerateRecordVo qrcode : qrcodeList) {
if (qrcode != null && qrcode.getRecordId() != null) {
qrcodeMap.put(qrcode.getRecordId(), qrcode);
}
}
}
// 将二维码记录设置到对应的钢卷对象中
for (WmsMaterialCoilVo vo : voList) {
if (vo.getQrcodeRecordId() != null && qrcodeMap.containsKey(vo.getQrcodeRecordId())) {
vo.setQrcodeRecord(qrcodeMap.get(vo.getQrcodeRecordId()));
}
}
}
/**
* 将对象解析为Date类型
* 支持Number、Date和字符串格式的时间对象
*
* @param obj 时间对象可以是Number、Date或String
* @return 解析后的Date对象解析失败返回null
*/
private Date parseDateFromObject(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof Number) {
// 处理时间戳格式
return new Date(((Number) obj).longValue());
}
if (obj instanceof Date) {
// 已经是Date类型直接返回
return (Date) obj;
}
// 字符串格式,使用工具类解析
return DateUtils.parseDate(obj.toString());
}
}