package com.zy.asrs.service.impl; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.plugins.Page; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.core.common.Cools; import com.core.common.SnowflakeIdWorker; import com.core.exception.CoolException; import com.zy.asrs.entity.MonthlySettle; import com.zy.asrs.entity.MonthlySettleDetail; import com.zy.asrs.entity.result.MaterialInOutRawDTO; import com.zy.asrs.entity.result.MaterialInOutStatDTO; import com.zy.asrs.entity.result.MonthlySettleResultVO; import com.zy.asrs.entity.result.MonthlySettleStatisticsVO; import com.zy.asrs.entity.result.PreviousSettleEndingQtyDTO; import com.zy.asrs.mapper.MonthlySettleDetailMapper; import com.zy.asrs.mapper.MonthlySettleMapper; import com.zy.asrs.service.ManLocDetlService; import com.zy.asrs.service.MonthlySettleService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service("monthlySettleService") public class MonthlySettleServiceImpl extends ServiceImpl implements MonthlySettleService { @Autowired private MonthlySettleDetailMapper monthlySettleDetailMapper; @Autowired private ManLocDetlService manLocDetlService; @Autowired private SnowflakeIdWorker snowflakeIdWorker; @Override public MonthlySettle getLatestSettle() { return this.baseMapper.selectLatestSettle(); } @Override public Date getNextStartDate() { MonthlySettle latestSettle = getLatestSettle(); if (latestSettle == null) { // 如果没有月结记录,返回null,由前端选择起始日期 return null; } // 返回最晚月结记录结束日期的下一天(起始日期应该是结束日期+1天的00:00:00) Calendar cal = Calendar.getInstance(); cal.setTime(latestSettle.getEndDate()); // 先设置为当天的00:00:00,然后加1天,确保返回的是下一天的00:00:00 cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); cal.add(Calendar.DAY_OF_MONTH, 1); return cal.getTime(); } @Override public Date getLatestEndDate() { MonthlySettle latestSettle = getLatestSettle(); if (latestSettle == null) { return null; } // 返回最晚月结记录的结束日期(格式化为当天的00:00:00,用于显示) Calendar cal = Calendar.getInstance(); cal.setTime(latestSettle.getEndDate()); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } @Override public boolean hasUnfinishedOrders(Date startDate, Date endDate) { // 结束日期+23:59:59 Calendar cal = Calendar.getInstance(); cal.setTime(endDate); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59); cal.set(Calendar.MILLISECOND, 999); endDate = cal.getTime(); // 注意:order_time 是 varchar 类型,需要格式化为完整的日期时间字符串进行比较 SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 起始日期设置为当天的 00:00:00 Calendar startCal = Calendar.getInstance(); startCal.setTime(startDate); startCal.set(Calendar.HOUR_OF_DAY, 0); startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.SECOND, 0); startCal.set(Calendar.MILLISECOND, 0); String startDateStr = dateTimeFormat.format(startCal.getTime()); String endDateStr = dateTimeFormat.format(endDate); // 分两次查询:入库和出库 int pakinCount = this.baseMapper.countUnfinishedOrdersInRangePakin(startDateStr, endDateStr); int pakoutCount = this.baseMapper.countUnfinishedOrdersInRangePakout(startDateStr, endDateStr); return (pakinCount + pakoutCount) > 0; } @Override @Transactional public MonthlySettleResultVO startSettle(Date startDate, Date endDate, Long userId) { // 结束日期+23:59:59 Calendar cal = Calendar.getInstance(); cal.setTime(endDate); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59); cal.set(Calendar.MILLISECOND, 999); endDate = cal.getTime(); // 检查起始日期必须大于最晚月结记录的结束日期 MonthlySettle latestSettle = getLatestSettle(); if (latestSettle != null) { Calendar startCal = Calendar.getInstance(); startCal.setTime(startDate); startCal.set(Calendar.HOUR_OF_DAY, 0); startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.SECOND, 0); startCal.set(Calendar.MILLISECOND, 0); Calendar latestEndCal = Calendar.getInstance(); latestEndCal.setTime(latestSettle.getEndDate()); latestEndCal.set(Calendar.HOUR_OF_DAY, 0); latestEndCal.set(Calendar.MINUTE, 0); latestEndCal.set(Calendar.SECOND, 0); latestEndCal.set(Calendar.MILLISECOND, 0); // 起始日期必须大于最晚结束日期,不能等于或小于 if (!startCal.getTime().after(latestEndCal.getTime())) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); throw new CoolException("起始日期必须大于最晚月结记录的结束日期:" + sdf.format(latestSettle.getEndDate())); } } // 检查是否有未完成的订单 if (hasUnfinishedOrders(startDate, endDate)) { throw new CoolException("月结时间范围内存在未完成的订单,无法进行月结"); } // 统计物料出入库数量(分别查询两个表,在Java代码中合并) // 注意:order_time 是 varchar 类型,需要格式化为完整的日期时间字符串进行比较 SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 起始日期设置为当天的 00:00:00 Calendar startCal = Calendar.getInstance(); startCal.setTime(startDate); startCal.set(Calendar.HOUR_OF_DAY, 0); startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.SECOND, 0); startCal.set(Calendar.MILLISECOND, 0); String startDateStr = dateTimeFormat.format(startCal.getTime()); String endDateStr = dateTimeFormat.format(endDate); // 分别查询入库表和出库表 List pakinDataList = this.baseMapper.statisticsMaterialInOutFromPakin(startDateStr, endDateStr); List pakoutDataList = this.baseMapper.statisticsMaterialInOutFromPakout(startDateStr, endDateStr); // 合并两个表的数据 List rawDataList = new java.util.ArrayList<>(); if (pakinDataList != null && !pakinDataList.isEmpty()) { rawDataList.addAll(pakinDataList); } if (pakoutDataList != null && !pakoutDataList.isEmpty()) { rawDataList.addAll(pakoutDataList); } // 检查是否有出入库历史订单 if (rawDataList == null || rawDataList.isEmpty()) { throw new CoolException("月结时间范围内没有出入库历史订单,无法进行月结"); } // 在Java代码中处理分类统计逻辑 // 1. 处理NULL值(batch、brand、maktx) // 2. 根据物料编码、批次、品牌分组 // 3. 根据 pakin_pakout_status 区分入库和出库 Map materialStatsMap = new HashMap<>(); Map maktxMap = new HashMap<>(); // 存储物料名称(取第一个非空值) for (MaterialInOutRawDTO raw : rawDataList) { // 处理NULL值 String matnr = raw.getMatnr(); String batch = raw.getBatch() != null ? raw.getBatch() : ""; String brand = raw.getBrand() != null ? raw.getBrand() : ""; String maktx = raw.getMaktx() != null ? raw.getMaktx() : ""; String key = matnr + "_" + batch + "_" + brand; // 保存物料名称(取第一个非空值) if (maktx != null && !maktx.isEmpty() && !maktxMap.containsKey(key)) { maktxMap.put(key, maktx); } MaterialInOutStatDTO stat = materialStatsMap.get(key); if (stat == null) { stat = new MaterialInOutStatDTO(); stat.setMatnr(matnr); stat.setMaktx(maktxMap.getOrDefault(key, "")); stat.setBatch(batch); stat.setBrand(brand); stat.setInQty(BigDecimal.ZERO); stat.setOutQty(BigDecimal.ZERO); materialStatsMap.put(key, stat); } // 根据 pakin_pakout_status 分类统计 // 1 = 入库,2 = 出库,0 = 未知(已在SQL中过滤) BigDecimal qty = raw.getQty() != null ? raw.getQty() : BigDecimal.ZERO; Integer status = raw.getPakinPakoutStatus(); if (status != null && qty.compareTo(BigDecimal.ZERO) > 0) { if (status == 1) { // 入库 stat.setInQty(stat.getInQty().add(qty)); } else if (status == 2) { // 出库 stat.setOutQty(stat.getOutQty().add(qty)); } } } // 转换为List List materialStats = new java.util.ArrayList<>(materialStatsMap.values()); // 获取上一个月结记录(用于计算期初库存和承接上一期的物料) MonthlySettle previousSettle = getLatestSettle(); Map previousEndingQtyMap = new HashMap<>(); Map previousDetailMap = new HashMap<>(); // 存储上一期的完整明细信息 if (previousSettle != null) { List previousDetails = this.baseMapper.getPreviousSettleEndingQty(previousSettle.getId()); for (PreviousSettleEndingQtyDTO detail : previousDetails) { String key = detail.getMatnr() + "_" + (detail.getBatch() != null ? detail.getBatch() : "") + "_" + (detail.getBrand() != null ? detail.getBrand() : ""); BigDecimal endingQty = detail.getEndingQty() != null ? detail.getEndingQty() : BigDecimal.ZERO; previousEndingQtyMap.put(key, endingQty); previousDetailMap.put(key, detail); // 保存完整信息,用于后续创建明细 } } // 生成月结编号 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String settleNo = "MS" + sdf.format(new Date()) + String.format("%04d", snowflakeIdWorker.nextId() % 10000); // 创建月结主记录 MonthlySettle monthlySettle = new MonthlySettle(); monthlySettle.setSettleNo(settleNo); monthlySettle.setStartDate(startDate); monthlySettle.setEndDate(endDate); monthlySettle.setStatus(1); // 已月结 monthlySettle.setIsDeleted(0); // 未删除 monthlySettle.setCreateBy(userId); monthlySettle.setCreateTime(new Date()); this.insert(monthlySettle); BigDecimal totalInQty = BigDecimal.ZERO; BigDecimal totalOutQty = BigDecimal.ZERO; int materialCount = 0; // 创建 Map 存储本期有出入库的物料(用于判断是否需要从上一期继承) Map currentMaterialMap = new HashMap<>(); for (MaterialInOutStatDTO stat : materialStats) { String matnr = stat.getMatnr(); String batch = stat.getBatch() != null ? stat.getBatch() : ""; String brand = stat.getBrand() != null ? stat.getBrand() : ""; String key = matnr + "_" + batch + "_" + brand; currentMaterialMap.put(key, stat); } // 收集所有明细记录,用于批量插入 List detailList = new java.util.ArrayList<>(); // 1. 处理本期有出入库的物料 for (MaterialInOutStatDTO stat : materialStats) { // 1. 提取基础信息 String matnr = stat.getMatnr(); String batch = stat.getBatch() != null ? stat.getBatch() : ""; String brand = stat.getBrand() != null ? stat.getBrand() : ""; String maktx = stat.getMaktx(); // 2. 提取数量信息 BigDecimal inQty = stat.getInQty() != null ? stat.getInQty() : BigDecimal.ZERO; BigDecimal outQty = stat.getOutQty() != null ? stat.getOutQty() : BigDecimal.ZERO; // 3. 计算库存相关数量 BigDecimal beginningQty = getBeginningQty(matnr, batch, brand, previousEndingQtyMap); BigDecimal endingQty = calculateEndingQty(beginningQty, inQty, outQty); // 差异数量 = 期末库存 - 期初库存(期末大于期初时为正数) BigDecimal diffQty = calculateDiffQty(beginningQty, endingQty); // 4. 创建明细记录(暂不插入) MonthlySettleDetail detail = buildMonthlySettleDetail( monthlySettle.getId(), settleNo, matnr, batch, maktx, brand, beginningQty, inQty, outQty, endingQty, diffQty ); detail.setIsDeleted(0); // 未删除 detailList.add(detail); // 5. 累计统计 totalInQty = totalInQty.add(inQty); totalOutQty = totalOutQty.add(outQty); materialCount++; } // 2. 处理上一期存在但本期没有出入库的物料(需要承接显示) if (previousSettle != null && !previousDetailMap.isEmpty()) { for (Map.Entry entry : previousDetailMap.entrySet()) { String key = entry.getKey(); // 如果本期没有出入库,但上一期有期末库存(且不为0),则需要创建一条记录 if (!currentMaterialMap.containsKey(key)) { PreviousSettleEndingQtyDTO previousDetail = entry.getValue(); BigDecimal previousEndingQty = previousDetail.getEndingQty() != null ? previousDetail.getEndingQty() : BigDecimal.ZERO; // 如果上一期的期末库存不为0,或者即使为0也要显示(用于对比) // 这里我们总是创建记录,即使上一期期末库存为0,因为用户要求"如果上一期存在过,本期库存归零也要显示一条为0的记录" String matnr = previousDetail.getMatnr(); String batch = previousDetail.getBatch() != null ? previousDetail.getBatch() : ""; String brand = previousDetail.getBrand() != null ? previousDetail.getBrand() : ""; String maktx = previousDetail.getMaktx() != null ? previousDetail.getMaktx() : ""; // 本期没有出入库,所以入库和出库数量都为0 BigDecimal inQty = BigDecimal.ZERO; BigDecimal outQty = BigDecimal.ZERO; // 期初库存 = 上一期的期末库存 BigDecimal beginningQty = previousEndingQty; // 期末库存 = 期初 + 入库 - 出库 = 期初(因为本期没有出入库) BigDecimal endingQty = beginningQty; // 差异数量 = 期初库存 - 期末库存(期初大于期末时为正数) BigDecimal diffQty = calculateDiffQty(beginningQty, endingQty); // 创建明细记录(暂不插入) MonthlySettleDetail detail = buildMonthlySettleDetail( monthlySettle.getId(), settleNo, matnr, batch, maktx, brand, beginningQty, inQty, outQty, endingQty, diffQty ); detail.setIsDeleted(0); // 未删除 detailList.add(detail); // 累计统计(虽然入库和出库为0,但也要计入物料种类数) materialCount++; } } } // 分批插入,每批1000条,避免一次性插入过多数据 if (!detailList.isEmpty()) { int batchSize = 1000; for (int i = 0; i < detailList.size(); i += batchSize) { int end = Math.min(i + batchSize, detailList.size()); List batch = detailList.subList(i, end); for (MonthlySettleDetail detail : batch) { if (monthlySettleDetailMapper.insert(detail) <= 0) { throw new CoolException("插入月结明细失败"); } } } } // 更新月结主记录 monthlySettle.setTotalInQty(totalInQty); monthlySettle.setTotalOutQty(totalOutQty); monthlySettle.setTotalMaterials(materialCount); monthlySettle.setStatus(1); // 已月结 monthlySettle.setUpdateBy(userId); monthlySettle.setUpdateTime(new Date()); this.updateById(monthlySettle); // 更新订单的月结信息(入库和出库都要更新) this.baseMapper.updateOrderSettleInfo(monthlySettle.getId(), settleNo, startDateStr, endDateStr); this.baseMapper.updateOrderSettleInfoPakout(monthlySettle.getId(), settleNo, startDateStr, endDateStr); MonthlySettleResultVO result = new MonthlySettleResultVO(); result.setSettleId(monthlySettle.getId()); result.setSettleNo(settleNo); result.setTotalInQty(totalInQty); result.setTotalOutQty(totalOutQty); result.setTotalMaterials(materialCount); return result; } @Override public MonthlySettleStatisticsVO getSettleStatistics(Long settleId) { MonthlySettle settle = this.selectById(settleId); if (settle == null || (settle.getIsDeleted() != null && settle.getIsDeleted() == 1)) { throw new CoolException("月结记录不存在"); } // 关联物料表查询明细 List details = monthlySettleDetailMapper.selectDetailWithMat(settleId); MonthlySettleStatisticsVO result = new MonthlySettleStatisticsVO(); result.setSettle(settle); result.setDetails(details); return result; } @Override public Page getPage(Page page) { Map condition = page.getCondition(); EntityWrapper wrapper = new EntityWrapper<>(); wrapper.eq("is_deleted", 0); if (condition != null) { if (condition.get("settleNo") != null) { wrapper.like("settle_no", condition.get("settleNo").toString()); } if (condition.get("status") != null) { wrapper.eq("status", condition.get("status")); } if (condition.get("startDate") != null && condition.get("endDate") != null) { wrapper.ge("start_date", condition.get("startDate")); wrapper.le("end_date", condition.get("endDate")); } } wrapper.orderBy("create_time", false); List list = this.selectList(wrapper); EntityWrapper countWrapper = new EntityWrapper<>(); countWrapper.eq("is_deleted", 0); if (condition != null) { if (condition.get("settleNo") != null) { countWrapper.like("settle_no", condition.get("settleNo").toString()); } if (condition.get("status") != null) { countWrapper.eq("status", condition.get("status")); } if (condition.get("startDate") != null && condition.get("endDate") != null) { countWrapper.ge("start_date", condition.get("startDate")); countWrapper.le("end_date", condition.get("endDate")); } } page.setRecords(list); page.setTotal(this.selectCount(countWrapper)); return page; } /** * 获取期初库存(上期结余) */ private BigDecimal getBeginningQty(String matnr, String batch, String brand, Map previousEndingQtyMap) { String key = matnr + "_" + batch + "_" + brand; return previousEndingQtyMap.getOrDefault(key, BigDecimal.ZERO); } /** * 计算期末库存(期初+入库-出库) */ private BigDecimal calculateEndingQty(BigDecimal beginningQty, BigDecimal inQty, BigDecimal outQty) { return beginningQty.add(inQty).subtract(outQty); } /** * 获取当前实际库存数量(已废弃,改用批量查询) * @deprecated 使用批量查询方法替代,避免N+1查询问题 */ @Deprecated private BigDecimal getCurrentStockQty(String matnr, String batch) { Double stockQty = manLocDetlService.queryStockAnfme(matnr, batch); return stockQty != null ? BigDecimal.valueOf(stockQty) : BigDecimal.ZERO; } /** * 计算差异数量(期末库存-期初库存) * 期末大于期初时为正数,表示库存增加 */ private BigDecimal calculateDiffQty(BigDecimal beginningQty, BigDecimal endingQty) { return endingQty.subtract(beginningQty); } /** * 构建月结明细对象 */ private MonthlySettleDetail buildMonthlySettleDetail( Long settleId, String settleNo, String matnr, String batch, String maktx, String brand, BigDecimal beginningQty, BigDecimal inQty, BigDecimal outQty, BigDecimal endingQty, BigDecimal diffQty) { MonthlySettleDetail detail = new MonthlySettleDetail(); // 基本信息 detail.setSettleId(settleId); detail.setSettleNo(settleNo); detail.setMatnr(matnr); detail.setBatch(batch); detail.setMaktx(maktx); detail.setBrand(brand); // 数量信息 detail.setBeginningQty(beginningQty); detail.setInQty(inQty); detail.setOutQty(outQty); detail.setEndingQty(endingQty); // stock_qty 字段已废弃,实际库存等于期末库存,不再单独存储 // detail.setStockQty(endingQty); detail.setDiffQty(diffQty); // 时间信息 detail.setCreateTime(new Date()); return detail; } @Override @Transactional public void deleteSettle(Long settleId) { MonthlySettle settle = this.selectById(settleId); if (settle == null || (settle.getIsDeleted() != null && settle.getIsDeleted() == 1)) { throw new CoolException("月结记录不存在"); } // 清除出入库订单的月结信息 this.baseMapper.clearOrderSettleInfo(settleId); this.baseMapper.clearOrderSettleInfoPakout(settleId); // 逻辑删除月结明细 EntityWrapper detailWrapper = new EntityWrapper<>(); detailWrapper.eq("settle_id", settleId); detailWrapper.eq("is_deleted", 0); List details = monthlySettleDetailMapper.selectList(detailWrapper); if (details != null && !details.isEmpty()) { for (MonthlySettleDetail detail : details) { detail.setIsDeleted(1); monthlySettleDetailMapper.updateById(detail); } } // 逻辑删除月结主记录 settle.setIsDeleted(1); settle.setUpdateTime(new Date()); this.updateById(settle); } @Override public boolean isOrderTimeInSettledRange(String orderTime) { if (Cools.isEmpty(orderTime)) { return false; } try { // 解析订单业务时间 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date orderDate = sdf.parse(orderTime); // 查询所有已月结的记录(status=1且未删除) EntityWrapper wrapper = new EntityWrapper<>(); wrapper.eq("status", 1); wrapper.eq("is_deleted", 0); List settledList = this.selectList(wrapper); if (settledList == null || settledList.isEmpty()) { return false; } // 检查订单时间是否在任何已月结的区间内 for (MonthlySettle settle : settledList) { Date startDate = settle.getStartDate(); Date endDate = settle.getEndDate(); // 确保startDate是当天的00:00:00 Calendar startCal = Calendar.getInstance(); startCal.setTime(startDate); startCal.set(Calendar.HOUR_OF_DAY, 0); startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.SECOND, 0); startCal.set(Calendar.MILLISECOND, 0); // 确保endDate是当天的23:59:59.999 Calendar endCal = Calendar.getInstance(); endCal.setTime(endDate); endCal.set(Calendar.HOUR_OF_DAY, 23); endCal.set(Calendar.MINUTE, 59); endCal.set(Calendar.SECOND, 59); endCal.set(Calendar.MILLISECOND, 999); // 判断订单时间是否在区间内:[startDate的00:00:00, endDate的23:59:59.999] if (!orderDate.before(startCal.getTime()) && !orderDate.after(endCal.getTime())) { return true; } } return false; } catch (Exception e) { log.error("检查订单时间是否在已月结区间内失败", e); return false; } } }