Files
klp-oa/klp-wms/src/main/java/com/klp/service/impl/WmsCostCoilDailyServiceImpl.java
2025-12-03 16:29:00 +08:00

950 lines
40 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

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

package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.klp.domain.WmsCostCoilDaily;
import com.klp.domain.bo.WmsCostCoilDailyBo;
import com.klp.domain.vo.WmsCostCoilDailyVo;
import com.klp.domain.vo.WmsCostStandardConfigVo;
import com.klp.mapper.WmsCostCoilDailyMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.klp.domain.WmsMaterialCoil;
import com.klp.mapper.WmsMaterialCoilMapper;
import com.klp.service.IWmsCostCoilDailyService;
import com.klp.service.IWmsCostStandardConfigService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
/**
* 钢卷日成本记录表Service业务层处理
*
* @author klp
* @date 2025-11-25
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class WmsCostCoilDailyServiceImpl implements IWmsCostCoilDailyService {
private final WmsCostCoilDailyMapper baseMapper;
private final WmsMaterialCoilMapper coilMapper;
private final IWmsCostStandardConfigService costStandardConfigService;
/**
* 查询钢卷日成本记录表
*/
@Override
public WmsCostCoilDailyVo queryById(Long costId) {
return baseMapper.selectVoById(costId);
}
/**
* 查询钢卷日成本记录表列表
*/
@Override
public TableDataInfo<WmsCostCoilDailyVo> queryPageList(WmsCostCoilDailyBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<WmsCostCoilDaily> lqw = buildQueryWrapper(bo);
Page<WmsCostCoilDailyVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询钢卷日成本记录表列表
*/
@Override
public List<WmsCostCoilDailyVo> queryList(WmsCostCoilDailyBo bo) {
LambdaQueryWrapper<WmsCostCoilDaily> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<WmsCostCoilDaily> buildQueryWrapper(WmsCostCoilDailyBo bo) {
LambdaQueryWrapper<WmsCostCoilDaily> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getCostId() != null, WmsCostCoilDaily::getCostId, bo.getCostId());
lqw.eq(bo.getCoilId() != null, WmsCostCoilDaily::getCoilId, bo.getCoilId());
lqw.like(bo.getCurrentCoilNo() != null, WmsCostCoilDaily::getCurrentCoilNo, bo.getCurrentCoilNo());
lqw.eq(bo.getCalcDate() != null, WmsCostCoilDaily::getCalcDate, bo.getCalcDate());
lqw.eq(bo.getWarehouseId() != null, WmsCostCoilDaily::getWarehouseId, bo.getWarehouseId());
lqw.eq(bo.getItemType() != null, WmsCostCoilDaily::getItemType, bo.getItemType());
lqw.eq(bo.getMaterialType() != null, WmsCostCoilDaily::getMaterialType, bo.getMaterialType());
if (bo.getStartDate() != null) {
lqw.ge(WmsCostCoilDaily::getCalcDate, bo.getStartDate());
}
if (bo.getEndDate() != null) {
lqw.le(WmsCostCoilDaily::getCalcDate, bo.getEndDate());
}
lqw.orderByDesc(WmsCostCoilDaily::getCalcDate);
lqw.orderByDesc(WmsCostCoilDaily::getCostId);
return lqw;
}
/**
* 新增钢卷日成本记录表
*/
@Override
public Boolean insertByBo(WmsCostCoilDailyBo bo) {
WmsCostCoilDaily add = BeanUtil.toBean(bo, WmsCostCoilDaily.class);
validEntityBeforeSave(add);
return baseMapper.insert(add) > 0;
}
/**
* 修改钢卷日成本记录表
*/
@Override
public Boolean updateByBo(WmsCostCoilDailyBo bo) {
WmsCostCoilDaily update = BeanUtil.toBean(bo, WmsCostCoilDaily.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(WmsCostCoilDaily entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
* 校验并批量删除钢卷日成本记录表信息
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 实时计算指定钢卷的成本
*/
@Override
public Map<String, Object> calculateCost(Long coilId, LocalDateTime calcTime) {
Map<String, Object> result = new HashMap<>();
// 1. 查询钢卷信息
WmsMaterialCoil coil = coilMapper.selectById(coilId);
if (coil == null) {
result.put("error", "钢卷不存在");
return result;
}
// 2. 验证是否为计算对象data_type=1, export_time IS NULL
if (coil.getDataType() == null || coil.getDataType() != 1) {
result.put("error", "该钢卷不是现存数据,不在计算范围内");
return result;
}
if (coil.getExportTime() != null) {
result.put("error", "该钢卷已发货,不在计算范围内");
return result;
}
WeightContext weightContext = resolveWeightContext(coil);
if (!weightContext.isValid()) {
result.put("error", "钢卷毛重与净重均为空或为0无法计算成本");
return result;
}
// 3. 计算在库天数
LocalDate startDate = coil.getCreateTime() != null
? coil.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
: LocalDate.now();
LocalDate endDate = calcTime != null ? calcTime.toLocalDate() : LocalDate.now();
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (days < 1) {
days = 1; // 不足一天按一天计算
}
// 4. 获取成本标准(使用入库日期的成本标准)
WmsCostStandardConfigVo costStandard = costStandardConfigService.queryEffectiveByDate(startDate);
if (costStandard == null) {
// 如果没有找到对应日期的标准,使用当前有效的标准
costStandard = costStandardConfigService.queryCurrentEffective();
}
if (costStandard == null || costStandard.getUnitCost() == null) {
result.put("error", "未找到有效的成本标准配置");
return result;
}
BigDecimal unitCost = costStandard.getUnitCost();
// 5. 计算成本(按毛重优先)
BigDecimal dailyCost = weightContext.getCostTon().multiply(unitCost).setScale(2, RoundingMode.HALF_UP);
BigDecimal totalCost = dailyCost.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP);
// 6. 返回结果
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("netWeight", weightContext.getNetTon()); // 吨
result.put("netWeightKg", weightContext.getNetKg()); // 千克
result.put("grossWeightKg", weightContext.getGrossKg());
result.put("grossWeightTon", weightContext.getGrossTon());
result.put("costWeightTon", weightContext.getCostTon());
result.put("weightBasis", weightContext.useGross() ? "gross" : "net");
result.put("storageDays", days);
result.put("unitCost", unitCost);
result.put("dailyCost", dailyCost);
result.put("totalCost", totalCost);
result.put("warehouseId", coil.getWarehouseId());
result.put("itemType", coil.getItemType());
result.put("materialType", coil.getMaterialType());
result.put("createTime", coil.getCreateTime());
return result;
}
/**
* 批量计算多个钢卷的成本
*/
@Override
public List<Map<String, Object>> batchCalculateCost(List<Long> coilIds, LocalDateTime calcTime) {
List<Map<String, Object>> results = new ArrayList<>();
if (coilIds == null || coilIds.isEmpty()) {
return results;
}
// 批量查询钢卷信息
QueryWrapper<WmsMaterialCoil> queryWrapper = new QueryWrapper<>();
queryWrapper.in("coil_id", coilIds);
List<WmsMaterialCoil> coils = coilMapper.selectList(queryWrapper);
if (coils == null || coils.isEmpty()) {
return results;
}
// 获取成本标准(统一使用当前有效的标准,因为批量计算时可能跨多个日期)
LocalDate calcDate = calcTime != null ? calcTime.toLocalDate() : LocalDate.now();
WmsCostStandardConfigVo costStandard = costStandardConfigService.queryEffectiveByDate(calcDate);
if (costStandard == null) {
costStandard = costStandardConfigService.queryCurrentEffective();
}
BigDecimal unitCost = null;
if (costStandard != null && costStandard.getUnitCost() != null) {
unitCost = costStandard.getUnitCost();
}
// 遍历计算每个钢卷的成本
for (WmsMaterialCoil coil : coils) {
Map<String, Object> result = new HashMap<>();
// 验证是否为计算对象data_type=1, export_time IS NULL
if (coil.getDataType() == null || coil.getDataType() != 1) {
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("error", "该钢卷不是现存数据,不在计算范围内");
results.add(result);
continue;
}
if (coil.getExportTime() != null) {
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("error", "该钢卷已发货,不在计算范围内");
results.add(result);
continue;
}
WeightContext weightContext = resolveWeightContext(coil);
if (!weightContext.isValid()) {
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("error", "钢卷毛重与净重均为空或为0无法计算成本");
results.add(result);
continue;
}
// 计算在库天数
LocalDate startDate = coil.getCreateTime() != null
? coil.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
: LocalDate.now();
LocalDate endDate = calcTime != null ? calcTime.toLocalDate() : LocalDate.now();
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (days < 1) {
days = 1; // 不足一天按一天计算
}
// 如果统一标准为空,尝试使用入库日期的标准
BigDecimal finalUnitCost = unitCost;
if (finalUnitCost == null) {
WmsCostStandardConfigVo startDateStandard = costStandardConfigService.queryEffectiveByDate(startDate);
if (startDateStandard != null && startDateStandard.getUnitCost() != null) {
finalUnitCost = startDateStandard.getUnitCost();
} else {
WmsCostStandardConfigVo currentStandard = costStandardConfigService.queryCurrentEffective();
if (currentStandard != null && currentStandard.getUnitCost() != null) {
finalUnitCost = currentStandard.getUnitCost();
}
}
}
if (finalUnitCost == null) {
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("error", "未找到有效的成本标准配置");
results.add(result);
continue;
}
// 计算成本(毛重优先)
BigDecimal dailyCost = weightContext.getCostTon().multiply(finalUnitCost).setScale(2, RoundingMode.HALF_UP);
BigDecimal totalCost = dailyCost.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP);
// 返回结果
result.put("coilId", coil.getCoilId());
result.put("coilNo", coil.getCurrentCoilNo());
result.put("netWeight", weightContext.getNetTon()); // 吨
result.put("netWeightKg", weightContext.getNetKg()); // 千克
result.put("grossWeightKg", weightContext.getGrossKg());
result.put("grossWeightTon", weightContext.getGrossTon());
result.put("costWeightTon", weightContext.getCostTon());
result.put("weightBasis", weightContext.useGross() ? "gross" : "net");
result.put("storageDays", days);
result.put("unitCost", finalUnitCost);
result.put("dailyCost", dailyCost);
result.put("totalCost", totalCost);
result.put("warehouseId", coil.getWarehouseId());
result.put("itemType", coil.getItemType());
result.put("materialType", coil.getMaterialType());
result.put("createTime", coil.getCreateTime());
results.add(result);
}
return results;
}
/**
* 批量计算钢卷成本(定时任务使用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int calculateDailyCost(LocalDate calcDate) {
log.info("开始计算日期 {} 的钢卷成本", calcDate);
// 0. 防止重复记录,先清理当日历史
int deleted = baseMapper.deleteByCalcDate(calcDate);
log.info("已删除日期 {} 的历史成本记录 {} 条", calcDate, deleted);
// 1. 查询所有现存且未发货的钢卷
QueryWrapper<WmsMaterialCoil> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("data_type", 1) // 现存数据
.isNull("export_time") // 未发货export_time IS NULL
.eq("del_flag", 0); // 未删除
List<WmsMaterialCoil> coils = coilMapper.selectList(queryWrapper);
log.info("找到 {} 个需要计算成本的钢卷", coils.size());
// 2. 获取成本标准
WmsCostStandardConfigVo costStandard = costStandardConfigService.queryEffectiveByDate(calcDate);
if (costStandard == null) {
costStandard = costStandardConfigService.queryCurrentEffective();
}
if (costStandard == null || costStandard.getUnitCost() == null) {
log.error("未找到日期 {} 的有效成本标准配置", calcDate);
return 0;
}
BigDecimal unitCost = costStandard.getUnitCost();
int successCount = 0;
// 3. 遍历计算每个钢卷的成本
for (WmsMaterialCoil coil : coils) {
try {
// 计算在库天数(从入库到计算日期)
LocalDate createDate = coil.getCreateTime() != null
? coil.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
: LocalDate.now();
long days = ChronoUnit.DAYS.between(createDate, calcDate) + 1;
if (days < 1) {
days = 1;
}
// 计算成本(毛重优先)
WeightContext weightContext = resolveWeightContext(coil);
if (!weightContext.isValid()) {
log.warn("钢卷 {} 缺少有效重量,跳过", coil.getCurrentCoilNo());
continue;
}
BigDecimal dailyCost = weightContext.getCostTon().multiply(unitCost).setScale(2, RoundingMode.HALF_UP);
BigDecimal totalCost = dailyCost.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP);
// 4. 保存成本记录
WmsCostCoilDaily costRecord = new WmsCostCoilDaily();
costRecord.setCoilId(coil.getCoilId());
costRecord.setCurrentCoilNo(coil.getCurrentCoilNo());
costRecord.setCalcDate(calcDate);
costRecord.setNetWeight(weightContext.getCostTon());
costRecord.setUnitCost(unitCost);
costRecord.setDailyCost(dailyCost);
costRecord.setStorageDays((int) days);
costRecord.setTotalCost(totalCost);
costRecord.setWarehouseId(coil.getWarehouseId());
costRecord.setItemType(coil.getItemType());
costRecord.setMaterialType(coil.getMaterialType());
baseMapper.insert(costRecord);
successCount++;
} catch (Exception e) {
log.error("计算钢卷 {} 的成本时发生错误: {}", coil.getCurrentCoilNo(), e.getMessage(), e);
}
}
log.info("完成计算日期 {} 的钢卷成本,成功计算 {} 条记录", calcDate, successCount);
return successCount;
}
/**
* 查询成本统计报表
*/
@Override
public Map<String, Object> queryCostSummary(LocalDate startDate, LocalDate endDate, String groupBy, Long warehouseId) {
Map<String, Object> result = new HashMap<>();
// 查询汇总数据
Map<String, Object> summary = baseMapper.selectCostSummary(startDate, endDate, warehouseId, null, null);
result.put("summary", summary);
// 根据分组维度查询明细
List<Map<String, Object>> details = new ArrayList<>();
if ("warehouse".equals(groupBy)) {
details = baseMapper.selectCostByWarehouse(startDate, endDate);
} else if ("itemType".equals(groupBy)) {
details = baseMapper.selectCostByItemType(startDate, endDate);
}
result.put("details", details);
return result;
}
/**
* 查询成本趋势分析
*/
@Override
public List<Map<String, Object>> queryCostTrend(LocalDate startDate, LocalDate endDate) {
LocalDate effectiveStart = startDate != null ? startDate : LocalDate.now().minusDays(30);
LocalDate effectiveEnd = endDate != null ? endDate : LocalDate.now();
return baseMapper.selectCostTrend(effectiveStart, effectiveEnd);
}
/**
* 按入场钢卷号维度计算成本
*/
@Override
public Map<String, Object> calculateCostByEnterCoilNo(String enterCoilNo, LocalDate calcDate) {
Map<String, Object> result = new HashMap<>();
result.put("enterCoilNo", enterCoilNo);
if (calcDate == null) {
calcDate = LocalDate.now();
}
// 1. 查询该入场钢卷号下的所有钢卷(包括已发货和未发货的)
QueryWrapper<WmsMaterialCoil> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("enter_coil_no", enterCoilNo)
.eq("data_type", 1) // 现存数据
.eq("del_flag", 0); // 未删除
List<WmsMaterialCoil> coils = coilMapper.selectList(queryWrapper);
if (coils.isEmpty()) {
result.put("error", "未找到入场钢卷号 " + enterCoilNo + " 的相关钢卷");
return result;
}
List<Map<String, Object>> coilDetails = new ArrayList<>();
BigDecimal totalCost = BigDecimal.ZERO;
BigDecimal totalNetWeight = BigDecimal.ZERO;
BigDecimal totalGrossWeight = BigDecimal.ZERO;
int unshippedCount = 0;
int shippedCount = 0;
// 2. 遍历每个钢卷计算成本
for (WmsMaterialCoil coil : coils) {
Map<String, Object> coilDetail = new HashMap<>();
coilDetail.put("coilId", coil.getCoilId());
coilDetail.put("currentCoilNo", coil.getCurrentCoilNo());
coilDetail.put("isShipped", coil.getExportTime() != null);
// 计算在库天数
LocalDate startDate = coil.getCreateTime() != null
? coil.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
: LocalDate.now();
// 确定结束日期:已发货的计算到发货前一天,未发货的计算到当日
LocalDate endDate;
if (coil.getExportTime() != null) {
// 已发货:计算到发货前一天
endDate = coil.getExportTime().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDate()
.minusDays(1);
shippedCount++;
} else {
// 未发货:计算到当日
endDate = calcDate;
unshippedCount++;
}
// 确保结束日期不早于开始日期
if (endDate.isBefore(startDate)) {
endDate = startDate;
}
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (days < 1) {
days = 1;
}
// 获取成本标准
WmsCostStandardConfigVo costStandard = costStandardConfigService.queryEffectiveByDate(startDate);
if (costStandard == null) {
costStandard = costStandardConfigService.queryCurrentEffective();
}
if (costStandard == null || costStandard.getUnitCost() == null) {
coilDetail.put("error", "未找到有效的成本标准配置");
coilDetails.add(coilDetail);
continue;
}
BigDecimal unitCost = costStandard.getUnitCost();
// 计算成本(毛重优先)
WeightContext weightContext = resolveWeightContext(coil);
if (!weightContext.isValid()) {
coilDetail.put("error", "钢卷缺少有效重量,无法计算");
coilDetails.add(coilDetail);
continue;
}
BigDecimal dailyCost = weightContext.getCostTon().multiply(unitCost).setScale(2, RoundingMode.HALF_UP);
BigDecimal coilTotalCost = dailyCost.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP);
coilDetail.put("netWeightTon", weightContext.getNetTon());
coilDetail.put("grossWeightTon", weightContext.getGrossTon());
coilDetail.put("weightBasis", weightContext.useGross() ? "gross" : "net");
coilDetail.put("storageDays", days);
coilDetail.put("unitCost", unitCost);
coilDetail.put("dailyCost", dailyCost);
coilDetail.put("totalCost", coilTotalCost);
coilDetail.put("startDate", startDate);
coilDetail.put("endDate", endDate);
coilDetail.put("exportTime", coil.getExportTime());
coilDetails.add(coilDetail);
// 累计总成本和总净重
totalCost = totalCost.add(coilTotalCost);
totalNetWeight = totalNetWeight.add(weightContext.getNetTon());
totalGrossWeight = totalGrossWeight.add(weightContext.getGrossTon());
}
// 3. 汇总结果
result.put("coilDetails", coilDetails);
result.put("totalCoils", coils.size());
result.put("shippedCount", shippedCount);
result.put("unshippedCount", unshippedCount);
result.put("totalNetWeight", totalNetWeight.setScale(3, RoundingMode.HALF_UP));
result.put("totalGrossWeight", totalGrossWeight.setScale(3, RoundingMode.HALF_UP));
result.put("totalCost", totalCost.setScale(2, RoundingMode.HALF_UP));
result.put("calcDate", calcDate);
return result;
}
/**
* 批量按入场钢卷号维度计算成本(定时任务使用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int calculateDailyCostByEnterCoilNo(LocalDate calcDate) {
if (calcDate == null) {
calcDate = LocalDate.now().minusDays(1);
}
log.info("开始按入场钢卷号维度计算日期 {} 的成本", calcDate);
// 清理当日历史记录,防止重复计算
int deleted = baseMapper.deleteByCalcDate(calcDate);
log.info("已删除日期 {} 的历史成本记录 {} 条(按入场卷号维度重算)", calcDate, deleted);
// 1. 查询所有需要计算的入场钢卷号(去重)
QueryWrapper<WmsMaterialCoil> queryWrapper = new QueryWrapper<>();
queryWrapper.select("DISTINCT enter_coil_no")
.eq("data_type", 1)
.eq("del_flag", 0)
.isNotNull("enter_coil_no");
List<WmsMaterialCoil> distinctCoils = coilMapper.selectList(queryWrapper);
log.info("找到 {} 个需要计算成本的入场钢卷号", distinctCoils.size());
int successCount = 0;
// 2. 遍历每个入场钢卷号计算成本
for (WmsMaterialCoil distinctCoil : distinctCoils) {
try {
String enterCoilNo = distinctCoil.getEnterCoilNo();
if (enterCoilNo == null || enterCoilNo.trim().isEmpty()) {
continue;
}
// 计算该入场钢卷号的成本
Map<String, Object> costResult = calculateCostByEnterCoilNo(enterCoilNo, calcDate);
if (costResult.containsKey("error")) {
log.warn("计算入场钢卷号 {} 的成本时发生错误: {}", enterCoilNo, costResult.get("error"));
continue;
}
// 获取该入场钢卷号下的所有钢卷
QueryWrapper<WmsMaterialCoil> coilQueryWrapper = new QueryWrapper<>();
coilQueryWrapper.eq("enter_coil_no", enterCoilNo)
.eq("data_type", 1)
.eq("del_flag", 0);
List<WmsMaterialCoil> coils = coilMapper.selectList(coilQueryWrapper);
// 为每个钢卷生成成本记录
for (WmsMaterialCoil coil : coils) {
// 确定计算结束日期
LocalDate startDate = coil.getCreateTime() != null
? coil.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
: LocalDate.now();
LocalDate endDate;
if (coil.getExportTime() != null) {
// 已发货:计算到发货前一天
endDate = coil.getExportTime().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDate()
.minusDays(1);
} else {
// 未发货:计算到计算日期
endDate = calcDate;
}
// 对于已发货的钢卷,只计算到发货前一天,如果计算日期在发货日期之后,跳过
if (coil.getExportTime() != null) {
LocalDate exportDate = coil.getExportTime().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDate();
// 如果计算日期在发货日期之后,不需要计算
if (calcDate.isAfter(exportDate)) {
continue;
}
// 如果计算日期等于发货日期,计算到前一天
if (calcDate.isEqual(exportDate)) {
endDate = exportDate.minusDays(1);
}
}
// 确保结束日期不早于开始日期
if (endDate.isBefore(startDate)) {
endDate = startDate;
}
// 计算在库天数(到结束日期)
long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (days < 1) {
days = 1;
}
// 获取成本标准
WmsCostStandardConfigVo costStandard = costStandardConfigService.queryEffectiveByDate(startDate);
if (costStandard == null) {
costStandard = costStandardConfigService.queryCurrentEffective();
}
if (costStandard == null || costStandard.getUnitCost() == null) {
log.warn("未找到入场钢卷号 {} 钢卷 {} 的有效成本标准", enterCoilNo, coil.getCurrentCoilNo());
continue;
}
BigDecimal unitCost = costStandard.getUnitCost();
// 计算成本
WeightContext weightContext = resolveWeightContext(coil);
if (!weightContext.isValid()) {
log.warn("入场卷号 {} 下的钢卷 {} 缺少有效重量,跳过", enterCoilNo, coil.getCurrentCoilNo());
continue;
}
BigDecimal dailyCost = weightContext.getCostTon().multiply(unitCost).setScale(2, RoundingMode.HALF_UP);
BigDecimal totalCost = dailyCost.multiply(BigDecimal.valueOf(days)).setScale(2, RoundingMode.HALF_UP);
// 保存成本记录
WmsCostCoilDaily costRecord = new WmsCostCoilDaily();
costRecord.setCoilId(coil.getCoilId());
costRecord.setCurrentCoilNo(coil.getCurrentCoilNo());
costRecord.setCalcDate(calcDate);
costRecord.setNetWeight(weightContext.getCostTon());
costRecord.setUnitCost(unitCost);
costRecord.setDailyCost(dailyCost);
costRecord.setStorageDays((int) days);
costRecord.setTotalCost(totalCost);
costRecord.setWarehouseId(coil.getWarehouseId());
costRecord.setItemType(coil.getItemType());
costRecord.setMaterialType(coil.getMaterialType());
baseMapper.insert(costRecord);
}
successCount++;
} catch (Exception e) {
log.error("按入场钢卷号维度计算成本时发生错误: {}", e.getMessage(), e);
}
}
log.info("完成按入场钢卷号维度计算日期 {} 的成本,成功计算 {} 个入场钢卷号", calcDate, successCount);
return successCount;
}
@Override
public Map<String, Object> searchMaterialCost(String enterCoilNo, LocalDate calcDate, int pageNum, int pageSize) {
Map<String, Object> result = new HashMap<>();
int safePageNum = pageNum > 0 ? pageNum : 1;
int safePageSize = pageSize > 0 ? Math.min(pageSize, 100) : 20;
LocalDate effectiveCalcDate = calcDate != null ? calcDate : LocalDate.now();
String trimmedEnterCoilNo = StringUtils.isNotBlank(enterCoilNo) ? enterCoilNo.trim() : null;
result.put("enterCoilNo", trimmedEnterCoilNo);
result.put("calcDate", effectiveCalcDate);
result.put("pageNum", safePageNum);
result.put("pageSize", safePageSize);
result.put("records", Collections.emptyList());
result.put("total", 0L);
Map<String, Object> summary = buildEmptySearchSummary(trimmedEnterCoilNo, effectiveCalcDate);
if (trimmedEnterCoilNo == null) {
result.put("summary", summary);
return result;
}
long total = baseMapper.countMaterialCostCards(trimmedEnterCoilNo, effectiveCalcDate);
if (total > 0) {
Map<String, Object> dbSummary = baseMapper.selectMaterialCostSummary(trimmedEnterCoilNo, effectiveCalcDate);
summary = normalizeMaterialCostSummary(dbSummary, trimmedEnterCoilNo, effectiveCalcDate);
int offset = (safePageNum - 1) * safePageSize;
List<Map<String, Object>> cards = baseMapper.selectMaterialCostCards(trimmedEnterCoilNo, effectiveCalcDate, offset, safePageSize);
result.put("records", cards);
result.put("total", total);
} else {
result.put("total", 0L);
}
result.put("summary", summary);
return result;
}
/**
* 查询按入场钢卷号统计的成本报表
*/
@Override
public List<Map<String, Object>> queryCostByEnterCoilNo(LocalDate startDate, LocalDate endDate, String enterCoilNo) {
return baseMapper.selectCostByEnterCoilNo(startDate, endDate, enterCoilNo);
}
/**
* 囤积成本页数据(后台统一计算)
*/
@Override
public Map<String, Object> queryStockpileCostList(String enterCoilNo, String currentCoilNo, PageQuery pageQuery) {
Map<String, Object> result = new HashMap<>();
// 使用 SQL 直接按入场钢卷号聚合,避免在 Service 层做大循环
LocalDate calcDate = LocalDate.now();
int pageNum = pageQuery.getPageNum() != null ? pageQuery.getPageNum() : 1;
int pageSize = pageQuery.getPageSize() != null ? pageQuery.getPageSize() : 50;
int offset = (pageNum - 1) * pageSize;
String trimmedEnterCoilNo = StringUtils.isNotBlank(enterCoilNo) ? enterCoilNo.trim() : null;
List<Map<String, Object>> rows = baseMapper.selectStockpileByEnterCoilNo(calcDate, trimmedEnterCoilNo, offset, pageSize);
long total = baseMapper.countStockpileByEnterCoilNo(calcDate, trimmedEnterCoilNo);
Map<String, Object> summary = baseMapper.selectStockpileSummaryByEnterCoilNo(calcDate, trimmedEnterCoilNo);
if (summary == null) {
summary = new HashMap<>();
summary.put("totalNetWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
summary.put("totalGrossWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
summary.put("totalCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
summary.put("avgStorageDays", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
summary.put("todayCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
}
// 兼容前端字段命名
summary.putIfAbsent("totalCoils", total);
result.put("rows", rows);
result.put("total", total);
result.put("summary", summary);
return result;
}
/**
* 成本模块首页概览数据
* 统计当前「现存且未发货」钢卷的总成本、总净重、总毛重以及平均在库天数
*/
@Override
public Map<String, Object> queryOverview() {
Map<String, Object> db = baseMapper.selectOverviewFromMaterialCoil();
Map<String, Object> result = new HashMap<>();
if (db == null || db.isEmpty()) {
result.put("totalCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
result.put("todayCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
result.put("totalNetWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
result.put("totalGrossWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
result.put("avgStorageDays", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
result.put("totalCoils", 0);
return result;
}
BigDecimal totalCost = toBigDecimal(db.get("totalCost"), 2);
BigDecimal todayCost = toBigDecimal(db.get("todayCost"), 2);
BigDecimal totalNetWeight = toBigDecimal(db.get("totalNetWeight"), 3);
BigDecimal totalGrossWeight = toBigDecimal(db.get("totalGrossWeight"), 3);
BigDecimal avgStorageDays = toBigDecimal(db.get("avgStorageDays"), 2);
LocalDate calcDate = LocalDate.now();
Long totalCoils = baseMapper.countStockpileByEnterCoilNo(calcDate,null);
result.put("totalCost", totalCost);
result.put("todayCost", todayCost);
result.put("totalNetWeight", totalNetWeight);
result.put("totalGrossWeight", totalGrossWeight);
result.put("avgStorageDays", avgStorageDays);
result.put("totalCoils", totalCoils);
return result;
}
private BigDecimal toBigDecimal(Object value, int scale) {
if (value == null) {
return BigDecimal.ZERO.setScale(scale, RoundingMode.HALF_UP);
}
if (value instanceof BigDecimal) {
return ((BigDecimal) value).setScale(scale, RoundingMode.HALF_UP);
}
if (value instanceof Number) {
return BigDecimal.valueOf(((Number) value).doubleValue()).setScale(scale, RoundingMode.HALF_UP);
}
try {
return new BigDecimal(value.toString()).setScale(scale, RoundingMode.HALF_UP);
} catch (Exception e) {
return BigDecimal.ZERO.setScale(scale, RoundingMode.HALF_UP);
}
}
private Map<String, Object> buildEmptySearchSummary(String enterCoilNo, LocalDate calcDate) {
Map<String, Object> summary = new HashMap<>();
summary.put("enterCoilNo", enterCoilNo);
summary.put("calcDate", calcDate);
summary.put("totalCoils", 0);
summary.put("shippedCount", 0);
summary.put("unshippedCount", 0);
summary.put("totalCost", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
summary.put("totalNetWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
summary.put("totalGrossWeight", BigDecimal.ZERO.setScale(3, RoundingMode.HALF_UP));
summary.put("avgStorageDays", BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
return summary;
}
private Map<String, Object> normalizeMaterialCostSummary(Map<String, Object> db, String enterCoilNo, LocalDate calcDate) {
Map<String, Object> summary = buildEmptySearchSummary(enterCoilNo, calcDate);
if (db == null || db.isEmpty()) {
return summary;
}
summary.put("totalCoils", toBigDecimal(db.get("totalCoils"), 0).intValue());
summary.put("shippedCount", toBigDecimal(db.get("shippedCount"), 0).intValue());
summary.put("unshippedCount", toBigDecimal(db.get("unshippedCount"), 0).intValue());
summary.put("totalCost", toBigDecimal(db.get("totalCost"), 2));
summary.put("totalNetWeight", toBigDecimal(db.get("totalNetWeight"), 3));
summary.put("totalGrossWeight", toBigDecimal(db.get("totalGrossWeight"), 3));
summary.put("avgStorageDays", toBigDecimal(db.get("avgStorageDays"), 2));
return summary;
}
private WeightContext resolveWeightContext(WmsMaterialCoil coil) {
BigDecimal netWeightKg = coil.getNetWeight() == null ? BigDecimal.ZERO : coil.getNetWeight();
BigDecimal grossWeightKg = coil.getGrossWeight() == null ? BigDecimal.ZERO : coil.getGrossWeight();
BigDecimal costWeightKg = grossWeightKg.compareTo(BigDecimal.ZERO) > 0 ? grossWeightKg : netWeightKg;
return new WeightContext(netWeightKg, grossWeightKg, costWeightKg);
}
private static class WeightContext {
private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000);
private final BigDecimal netKg;
private final BigDecimal grossKg;
private final BigDecimal costKg;
private final BigDecimal netTon;
private final BigDecimal grossTon;
private final BigDecimal costTon;
WeightContext(BigDecimal netKg, BigDecimal grossKg, BigDecimal costKg) {
this.netKg = netKg;
this.grossKg = grossKg;
this.costKg = costKg;
this.netTon = convertToTon(netKg);
this.grossTon = convertToTon(grossKg);
this.costTon = convertToTon(costKg);
}
private BigDecimal convertToTon(BigDecimal kg) {
if (kg == null || kg.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
return kg.divide(THOUSAND, 3, RoundingMode.HALF_UP);
}
boolean isValid() {
return costKg != null && costKg.compareTo(BigDecimal.ZERO) > 0;
}
boolean useGross() {
return grossKg != null && grossKg.compareTo(BigDecimal.ZERO) > 0;
}
BigDecimal getNetKg() {
return netKg;
}
BigDecimal getGrossKg() {
return grossKg;
}
BigDecimal getNetTon() {
return netTon;
}
BigDecimal getGrossTon() {
return grossTon;
}
BigDecimal getCostTon() {
return costTon;
}
}
}