diff --git a/klp-job/src/main/java/com/klp/job/service/SampleService.java b/klp-job/src/main/java/com/klp/job/service/SampleService.java index bbc260f2..75e0a8f8 100644 --- a/klp-job/src/main/java/com/klp/job/service/SampleService.java +++ b/klp-job/src/main/java/com/klp/job/service/SampleService.java @@ -249,4 +249,101 @@ public class SampleService { } + /** + * 6、成本日计算任务 + * 每天凌晨 2 点由调度中心触发,负责调用业务系统接口,计算前一日成本日报数据。 + * + * 调度中心建议配置: + * - JobHandler:costDailyCalculateJob + * - Cron:0 0 2 * * ? + * - 执行参数(可选):baseUrl=http://127.0.0.1:8080 + */ + @XxlJob("costDailyCalculateJob") + public void costDailyCalculateJob() throws Exception { + // 1. 计算前一日日期 + java.time.LocalDate calcDate = java.time.LocalDate.now().minusDays(1); + String dateStr = calcDate.toString(); // yyyy-MM-dd + + // 2. 解析调度参数,获取业务系统基础地址 + String param = XxlJobHelper.getJobParam(); + String baseUrl = "http://127.0.0.1:8080"; // 默认本机 + if (param != null && !param.trim().isEmpty()) { + // 简单解析形式:baseUrl=http://host:port + for (String line : param.split("\n")) { + line = line.trim(); + if (line.startsWith("baseUrl=")) { + baseUrl = line.substring("baseUrl=".length()).trim(); + } + } + } + + // 3. 构造两个调用地址 + String urlDaily = baseUrl + "/wms/cost/coil/batchCalculate?calcDate=" + dateStr; + String urlByEnterCoilNo = baseUrl + "/wms/cost/coil/batchCalculateByEnterCoilNo?calcDate=" + dateStr; + + XxlJobHelper.log("开始成本日计算,calcDate={}, baseUrl={}", dateStr, baseUrl); + + // 4. 依次调用两个接口 + try { + doPost(urlDaily); + XxlJobHelper.log("调用 batchCalculate 成功: {}", urlDaily); + } catch (Exception e) { + XxlJobHelper.log(e); + XxlJobHelper.handleFail("调用 batchCalculate 失败: " + e.getMessage()); + return; + } + + try { + doPost(urlByEnterCoilNo); + XxlJobHelper.log("调用 batchCalculateByEnterCoilNo 成功: {}", urlByEnterCoilNo); + } catch (Exception e) { + XxlJobHelper.log(e); + XxlJobHelper.handleFail("调用 batchCalculateByEnterCoilNo 失败: " + e.getMessage()); + return; + } + + // 默认成功 + } + + /** + * 简单的 POST 请求工具,复用 JDK HttpURLConnection。 + */ + private void doPost(String url) throws Exception { + HttpURLConnection connection = null; + BufferedReader bufferedReader = null; + try { + URL realUrl = new URL(url); + connection = (HttpURLConnection) realUrl.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setReadTimeout(30 * 1000); + connection.setConnectTimeout(10 * 1000); + connection.setRequestProperty("connection", "Keep-Alive"); + + connection.connect(); + + int statusCode = connection.getResponseCode(); + if (statusCode != 200) { + throw new RuntimeException("HTTP " + url + " StatusCode(" + statusCode + ") Invalid."); + } + + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + XxlJobHelper.log("调用 {} 返回: {}", url, result.toString()); + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (connection != null) { + connection.disconnect(); + } + } + } + } diff --git a/klp-ui/src/views/wms/cost/dashboard/index.vue b/klp-ui/src/views/wms/cost/dashboard/index.vue new file mode 100644 index 00000000..943d30e0 --- /dev/null +++ b/klp-ui/src/views/wms/cost/dashboard/index.vue @@ -0,0 +1,388 @@ + + + + + + diff --git a/klp-ui/src/views/wms/cost/detail/index.vue b/klp-ui/src/views/wms/cost/detail/index.vue new file mode 100644 index 00000000..5056e9c1 --- /dev/null +++ b/klp-ui/src/views/wms/cost/detail/index.vue @@ -0,0 +1,568 @@ + + + + + + diff --git a/klp-ui/src/views/wms/cost/standard/index.vue b/klp-ui/src/views/wms/cost/standard/index.vue new file mode 100644 index 00000000..e509d418 --- /dev/null +++ b/klp-ui/src/views/wms/cost/standard/index.vue @@ -0,0 +1,363 @@ + + + + + + diff --git a/klp-ui/src/views/wms/cost/stockpile/index.vue b/klp-ui/src/views/wms/cost/stockpile/index.vue index 68864004..ac0e9ef5 100644 --- a/klp-ui/src/views/wms/cost/stockpile/index.vue +++ b/klp-ui/src/views/wms/cost/stockpile/index.vue @@ -1,5 +1,422 @@ \ No newline at end of file + + + + + diff --git a/klp-wms/src/main/java/com/klp/controller/CostCoilDailyController.java b/klp-wms/src/main/java/com/klp/controller/CostCoilDailyController.java new file mode 100644 index 00000000..f18524ce --- /dev/null +++ b/klp-wms/src/main/java/com/klp/controller/CostCoilDailyController.java @@ -0,0 +1,272 @@ +package com.klp.controller; + +import java.util.List; +import java.util.Map; +import java.util.Arrays; + +import lombok.RequiredArgsConstructor; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.klp.common.annotation.RepeatSubmit; +import com.klp.common.annotation.Log; +import com.klp.common.core.controller.BaseController; +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.domain.R; +import com.klp.common.core.validate.AddGroup; +import com.klp.common.core.validate.EditGroup; +import com.klp.common.enums.BusinessType; +import com.klp.common.utils.poi.ExcelUtil; +import com.klp.domain.vo.CostCoilDailyVo; +import com.klp.domain.bo.CostCoilDailyBo; +import com.klp.service.ICostCoilDailyService; +import com.klp.common.core.page.TableDataInfo; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * 钢卷日成本记录表 + * + * @author klp + * @date 2025-11-25 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/wms/cost/coil") +public class CostCoilDailyController extends BaseController { + + private final ICostCoilDailyService iCostCoilDailyService; + + /** + * 查询钢卷日成本记录表列表 + */ + @GetMapping("/list") + public TableDataInfo list(CostCoilDailyBo bo, PageQuery pageQuery) { + return iCostCoilDailyService.queryPageList(bo, pageQuery); + } + + /** + * 导出钢卷日成本记录表列表 + */ + @Log(title = "钢卷日成本记录表", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CostCoilDailyBo bo, HttpServletResponse response) { + List list = iCostCoilDailyService.queryList(bo); + ExcelUtil.exportExcel(list, "钢卷日成本记录表", CostCoilDailyVo.class, response); + } + + /** + * 获取钢卷日成本记录表详细信息 + * + * @param costId 主键 + */ + @GetMapping("/{costId}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long costId) { + return R.ok(iCostCoilDailyService.queryById(costId)); + } + + /** + * 新增钢卷日成本记录表 + */ + @Log(title = "钢卷日成本记录表", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CostCoilDailyBo bo) { + return toAjax(iCostCoilDailyService.insertByBo(bo)); + } + + /** + * 修改钢卷日成本记录表 + */ + @Log(title = "钢卷日成本记录表", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CostCoilDailyBo bo) { + return toAjax(iCostCoilDailyService.updateByBo(bo)); + } + + /** + * 删除钢卷日成本记录表 + * + * @param costIds 主键串 + */ + @Log(title = "钢卷日成本记录表", businessType = BusinessType.DELETE) + @DeleteMapping("/{costIds}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] costIds) { + return toAjax(iCostCoilDailyService.deleteWithValidByIds(Arrays.asList(costIds), true)); + } + + /** + * 实时计算指定钢卷的成本 + * + * @param coilId 钢卷ID(可选) + * @param calcTime 计算时间点(可选,默认当前时间) + */ + @PostMapping("/calculate") + public R> calculateCost(@RequestParam(required = false) Long coilId, + @RequestParam(required = false) LocalDateTime calcTime) { + Map result = iCostCoilDailyService.calculateCost(coilId, calcTime); + if (result.containsKey("error")) { + return R.fail(result.get("error").toString()); + } + return R.ok(result); + } + + /** + * 批量计算多个钢卷的成本 + * + * @param coilIds 钢卷ID列表(支持逗号分隔的字符串或数组) + * @param calcTime 计算时间点(可选,默认当前时间) + */ + @PostMapping("/batchCalculateCost") + public R>> batchCalculateCost(@RequestParam String coilIds, + @RequestParam(required = false) LocalDateTime calcTime) { + // 解析coilIds字符串为Long列表 + List coilIdList = new java.util.ArrayList<>(); + if (coilIds != null && !coilIds.trim().isEmpty()) { + String[] ids = coilIds.split(","); + for (String id : ids) { + try { + Long coilId = Long.parseLong(id.trim()); + coilIdList.add(coilId); + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + } + + List> results = iCostCoilDailyService.batchCalculateCost(coilIdList, calcTime); + return R.ok(results); + } + + /** + * 批量计算钢卷成本(定时任务使用) + * + * @param calcDate 计算日期(可选,默认前一日) + */ + @PostMapping("/batchCalculate") + @Log(title = "批量计算钢卷成本", businessType = BusinessType.OTHER) + public R batchCalculate(@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate calcDate) { + if (calcDate == null) { + calcDate = LocalDate.now().minusDays(1); + } + int count = iCostCoilDailyService.calculateDailyCost(calcDate); + return R.ok(count); + } + + /** + * 查询成本统计报表 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param groupBy 分组维度(warehouse/itemType/materialType) + * @param warehouseId 库区ID(可选) + */ + @GetMapping("/report/summary") + public R> queryCostSummary(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate, + @RequestParam(required = false) String groupBy, + @RequestParam(required = false) Long warehouseId) { + Map result = iCostCoilDailyService.queryCostSummary(startDate, endDate, groupBy, warehouseId); + return R.ok(result); + } + + /** + * 查询成本趋势分析 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + */ + @GetMapping("/report/trend") + public R>> queryCostTrend(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) { + List> result = iCostCoilDailyService.queryCostTrend(startDate, endDate); + return R.ok(result); + } + + /** + * 按入场钢卷号维度计算成本 + * + * @param enterCoilNo 入场钢卷号 + * @param calcDate 计算日期(可选,默认当前日期) + */ + @PostMapping("/calculateByEnterCoilNo") + public R> calculateCostByEnterCoilNo(@RequestParam String enterCoilNo, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate calcDate) { + Map result = iCostCoilDailyService.calculateCostByEnterCoilNo(enterCoilNo, calcDate); + if (result.containsKey("error")) { + return R.fail(result.get("error").toString()); + } + return R.ok(result); + } + + /** + * 现算成本检索(基于 wms_material_coil) + */ + @GetMapping("/search/material") + public R> searchMaterialCost(@RequestParam(required = false) String enterCoilNo, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate calcDate, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + Map result = iCostCoilDailyService.searchMaterialCost(enterCoilNo, calcDate, pageNum, pageSize); + return R.ok(result); + } + + /** + * 批量按入场钢卷号维度计算成本(定时任务使用) + * + * @param calcDate 计算日期(可选,默认前一日) + */ + @PostMapping("/batchCalculateByEnterCoilNo") + @Log(title = "批量按入场钢卷号计算成本", businessType = BusinessType.OTHER) + public R batchCalculateByEnterCoilNo(@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate calcDate) { + if (calcDate == null) { + calcDate = LocalDate.now().minusDays(1); + } + int count = iCostCoilDailyService.calculateDailyCostByEnterCoilNo(calcDate); + return R.ok(count); + } + + /** + * 查询按入场钢卷号统计的成本报表 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param enterCoilNo 入场钢卷号(可选) + */ + @GetMapping("/report/byEnterCoilNo") + public R>> queryCostByEnterCoilNo(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate, + @RequestParam(required = false) String enterCoilNo) { + List> result = iCostCoilDailyService.queryCostByEnterCoilNo(startDate, endDate, enterCoilNo); + return R.ok(result); + } + + /** + * 囤积成本页数据(分页 + 汇总) + */ + @GetMapping("/stockpile") + public R> queryStockpile(@RequestParam(required = false) String enterCoilNo, + @RequestParam(required = false) String currentCoilNo, + PageQuery pageQuery) { + Map result = iCostCoilDailyService.queryStockpileCostList(enterCoilNo, currentCoilNo, pageQuery); + return R.ok(result); + } + + /** + * 成本模块首页概览 + * 统计当前「现存且未发货」钢卷的总成本、总净重、总毛重以及平均在库天数 + */ + @GetMapping("/overview") + public R> overview() { + Map result = iCostCoilDailyService.queryOverview(); + return R.ok(result); + } +} + diff --git a/klp-wms/src/main/java/com/klp/controller/CostStandardConfigController.java b/klp-wms/src/main/java/com/klp/controller/CostStandardConfigController.java new file mode 100644 index 00000000..005f5748 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/controller/CostStandardConfigController.java @@ -0,0 +1,108 @@ +package com.klp.controller; + +import java.util.List; +import java.util.Arrays; + +import lombok.RequiredArgsConstructor; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.klp.common.annotation.RepeatSubmit; +import com.klp.common.annotation.Log; +import com.klp.common.core.controller.BaseController; +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.domain.R; +import com.klp.common.core.validate.AddGroup; +import com.klp.common.core.validate.EditGroup; +import com.klp.common.enums.BusinessType; +import com.klp.common.utils.poi.ExcelUtil; +import com.klp.domain.vo.CostStandardConfigVo; +import com.klp.domain.bo.CostStandardConfigBo; +import com.klp.service.ICostStandardConfigService; +import com.klp.common.core.page.TableDataInfo; + +/** + * 成本标准配置表 + * + * @author klp + * @date 2025-11-25 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/wms/cost/standard") +public class CostStandardConfigController extends BaseController { + + private final ICostStandardConfigService iCostStandardConfigService; + + /** + * 查询成本标准配置表列表 + */ + @GetMapping("/list") + public TableDataInfo list(CostStandardConfigBo bo, PageQuery pageQuery) { + return iCostStandardConfigService.queryPageList(bo, pageQuery); + } + + /** + * 导出成本标准配置表列表 + */ + @Log(title = "成本标准配置表", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CostStandardConfigBo bo, HttpServletResponse response) { + List list = iCostStandardConfigService.queryList(bo); + ExcelUtil.exportExcel(list, "成本标准配置表", CostStandardConfigVo.class, response); + } + + /** + * 获取成本标准配置表详细信息 + * + * @param configId 主键 + */ + @GetMapping("/{configId}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long configId) { + return R.ok(iCostStandardConfigService.queryById(configId)); + } + + /** + * 查询当前有效的成本标准 + */ + @GetMapping("/current") + public R getCurrent() { + return R.ok(iCostStandardConfigService.queryCurrentEffective()); + } + + /** + * 新增成本标准配置表 + */ + @Log(title = "成本标准配置表", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CostStandardConfigBo bo) { + return toAjax(iCostStandardConfigService.insertByBo(bo)); + } + + /** + * 修改成本标准配置表 + */ + @Log(title = "成本标准配置表", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CostStandardConfigBo bo) { + return toAjax(iCostStandardConfigService.updateByBo(bo)); + } + + /** + * 删除成本标准配置表 + * + * @param configIds 主键串 + */ + @Log(title = "成本标准配置表", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] configIds) { + return toAjax(iCostStandardConfigService.deleteWithValidByIds(Arrays.asList(configIds), true)); + } +} + diff --git a/klp-wms/src/main/java/com/klp/domain/CostCoilDaily.java b/klp-wms/src/main/java/com/klp/domain/CostCoilDaily.java new file mode 100644 index 00000000..bd3a02fc --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/CostCoilDaily.java @@ -0,0 +1,85 @@ +package com.klp.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 钢卷日成本记录表对象 cost_coil_daily + * + * @author klp + * @date 2025-11-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cost_coil_daily") +public class CostCoilDaily extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "cost_id") + private Long costId; + + /** + * 钢卷ID(关联wms_material_coil.coil_id) + */ + private Long coilId; + + /** + * 当前钢卷号 + */ + private String currentCoilNo; + + /** + * 计算日期 + */ + private LocalDate calcDate; + + /** + * 净重(吨) + */ + private BigDecimal netWeight; + + /** + * 单位成本(元/吨/天) + */ + private BigDecimal unitCost; + + /** + * 日成本(元) + */ + private BigDecimal dailyCost; + + /** + * 累计在库天数 + */ + private Integer storageDays; + + /** + * 累计成本(元) + */ + private BigDecimal totalCost; + + /** + * 所在库区ID + */ + private Long warehouseId; + + /** + * 物品类型(raw_material/product) + */ + private String itemType; + + /** + * 材料类型 + */ + private String materialType; +} + diff --git a/klp-wms/src/main/java/com/klp/domain/CostStandardConfig.java b/klp-wms/src/main/java/com/klp/domain/CostStandardConfig.java new file mode 100644 index 00000000..32c2a721 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/CostStandardConfig.java @@ -0,0 +1,61 @@ +package com.klp.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 成本标准配置表对象 cost_standard_config + * + * @author klp + * @date 2025-11-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cost_standard_config") +public class CostStandardConfig extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "config_id") + private Long configId; + + /** + * 单位成本(元/吨/天) + */ + private BigDecimal unitCost; + + /** + * 生效日期 + */ + private LocalDate effectiveDate; + + /** + * 失效日期(NULL表示当前有效) + */ + private LocalDate expireDate; + + /** + * 状态(0=失效,1=有效) + */ + private Integer status; + + /** + * 删除标志(0=正常,1=已删除) + */ + @TableLogic + private Integer delFlag; + + /** + * 备注说明 + */ + private String remark; +} + diff --git a/klp-wms/src/main/java/com/klp/domain/bo/CostCoilDailyBo.java b/klp-wms/src/main/java/com/klp/domain/bo/CostCoilDailyBo.java new file mode 100644 index 00000000..6310946c --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/bo/CostCoilDailyBo.java @@ -0,0 +1,90 @@ +package com.klp.domain.bo; + +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 钢卷日成本记录表业务对象 cost_coil_daily + * + * @author klp + * @date 2025-11-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CostCoilDailyBo extends BaseEntity { + + /** + * 主键ID + */ + private Long costId; + + /** + * 钢卷ID(关联wms_material_coil.coil_id) + */ + private Long coilId; + + /** + * 当前钢卷号 + */ + private String currentCoilNo; + + /** + * 计算日期 + */ + private LocalDate calcDate; + + /** + * 净重(吨) + */ + private BigDecimal netWeight; + + /** + * 单位成本(元/吨/天) + */ + private BigDecimal unitCost; + + /** + * 日成本(元) + */ + private BigDecimal dailyCost; + + /** + * 累计在库天数 + */ + private Integer storageDays; + + /** + * 累计成本(元) + */ + private BigDecimal totalCost; + + /** + * 所在库区ID + */ + private Long warehouseId; + + /** + * 物品类型(raw_material/product) + */ + private String itemType; + + /** + * 材料类型 + */ + private String materialType; + + /** + * 开始日期(用于查询) + */ + private LocalDate startDate; + + /** + * 结束日期(用于查询) + */ + private LocalDate endDate; +} + diff --git a/klp-wms/src/main/java/com/klp/domain/bo/CostStandardConfigBo.java b/klp-wms/src/main/java/com/klp/domain/bo/CostStandardConfigBo.java new file mode 100644 index 00000000..041cbff5 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/bo/CostStandardConfigBo.java @@ -0,0 +1,50 @@ +package com.klp.domain.bo; + +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 成本标准配置表业务对象 cost_standard_config + * + * @author klp + * @date 2025-11-25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CostStandardConfigBo extends BaseEntity { + + /** + * 主键ID + */ + private Long configId; + + /** + * 单位成本(元/吨/天) + */ + private BigDecimal unitCost; + + /** + * 生效日期 + */ + private LocalDate effectiveDate; + + /** + * 失效日期(NULL表示当前有效) + */ + private LocalDate expireDate; + + /** + * 状态(0=失效,1=有效) + */ + private Integer status; + + /** + * 备注说明 + */ + private String remark; +} + diff --git a/klp-wms/src/main/java/com/klp/domain/vo/CostCoilDailyVo.java b/klp-wms/src/main/java/com/klp/domain/vo/CostCoilDailyVo.java new file mode 100644 index 00000000..b96fde77 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/vo/CostCoilDailyVo.java @@ -0,0 +1,98 @@ +package com.klp.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 钢卷日成本记录表视图对象 cost_coil_daily + * + * @author klp + * @date 2025-11-25 + */ +@Data +@ExcelIgnoreUnannotated +public class CostCoilDailyVo { + + /** + * 主键ID + */ + @ExcelProperty(value = "成本记录ID") + private Long costId; + + /** + * 钢卷ID(关联wms_material_coil.coil_id) + */ + @ExcelProperty(value = "钢卷ID") + private Long coilId; + + /** + * 当前钢卷号 + */ + @ExcelProperty(value = "钢卷号") + private String currentCoilNo; + + /** + * 计算日期 + */ + @ExcelProperty(value = "计算日期") + private LocalDate calcDate; + + /** + * 净重(吨) + */ + @ExcelProperty(value = "净重(吨)") + private BigDecimal netWeight; + + /** + * 单位成本(元/吨/天) + */ + @ExcelProperty(value = "单位成本(元/吨/天)") + private BigDecimal unitCost; + + /** + * 日成本(元) + */ + @ExcelProperty(value = "日成本(元)") + private BigDecimal dailyCost; + + /** + * 累计在库天数 + */ + @ExcelProperty(value = "累计在库天数") + private Integer storageDays; + + /** + * 累计成本(元) + */ + @ExcelProperty(value = "累计成本(元)") + private BigDecimal totalCost; + + /** + * 所在库区ID + */ + @ExcelProperty(value = "库区ID") + private Long warehouseId; + + /** + * 库区名称 + */ + @ExcelProperty(value = "库区名称") + private String warehouseName; + + /** + * 物品类型(raw_material/product) + */ + @ExcelProperty(value = "物品类型") + private String itemType; + + /** + * 材料类型 + */ + @ExcelProperty(value = "材料类型") + private String materialType; +} + diff --git a/klp-wms/src/main/java/com/klp/domain/vo/CostStandardConfigVo.java b/klp-wms/src/main/java/com/klp/domain/vo/CostStandardConfigVo.java new file mode 100644 index 00000000..e1058442 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/vo/CostStandardConfigVo.java @@ -0,0 +1,80 @@ +package com.klp.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 成本标准配置表视图对象 cost_standard_config + * + * @author klp + * @date 2025-11-25 + */ +@Data +@ExcelIgnoreUnannotated +public class CostStandardConfigVo { + + /** + * 主键ID + */ + @ExcelProperty(value = "配置ID") + private Long configId; + + /** + * 单位成本(元/吨/天) + */ + @ExcelProperty(value = "单位成本(元/吨/天)") + private BigDecimal unitCost; + + /** + * 生效日期 + */ + @ExcelProperty(value = "生效日期") + private LocalDate effectiveDate; + + /** + * 失效日期(NULL表示当前有效) + */ + @ExcelProperty(value = "失效日期") + private LocalDate expireDate; + + /** + * 状态(0=失效,1=有效) + */ + @ExcelProperty(value = "状态") + private Integer status; + + /** + * 备注说明 + */ + @ExcelProperty(value = "备注") + private String remark; + + /** + * 创建人 + */ + @ExcelProperty(value = "创建人") + private String createBy; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private java.util.Date createTime; + + /** + * 更新人 + */ + @ExcelProperty(value = "更新人") + private String updateBy; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private java.util.Date updateTime; +} + diff --git a/klp-wms/src/main/java/com/klp/mapper/CostCoilDailyMapper.java b/klp-wms/src/main/java/com/klp/mapper/CostCoilDailyMapper.java new file mode 100644 index 00000000..660bef12 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/mapper/CostCoilDailyMapper.java @@ -0,0 +1,150 @@ +package com.klp.mapper; + +import com.klp.common.core.mapper.BaseMapperPlus; +import com.klp.domain.CostCoilDaily; +import com.klp.domain.vo.CostCoilDailyVo; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 钢卷日成本记录表Mapper接口 + * + * @author klp + * @date 2025-11-25 + */ +public interface CostCoilDailyMapper extends BaseMapperPlus { + + /** + * 查询成本统计汇总 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param warehouseId 库区ID(可选) + * @param itemType 物品类型(可选) + * @param materialType 材料类型(可选) + * @return 统计结果 + */ + Map selectCostSummary(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("warehouseId") Long warehouseId, + @Param("itemType") String itemType, + @Param("materialType") String materialType); + + /** + * 按库区统计成本 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 统计结果列表 + */ + List> selectCostByWarehouse(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 按物品类型统计成本 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 统计结果列表 + */ + List> selectCostByItemType(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 检查指定日期和钢卷的成本记录是否存在 + * + * @param coilId 钢卷ID + * @param calcDate 计算日期 + * @return 记录数量 + */ + int countByCoilIdAndDate(@Param("coilId") Long coilId, @Param("calcDate") LocalDate calcDate); + + /** + * 按入场钢卷号统计成本 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param enterCoilNo 入场钢卷号(可选) + * @return 统计结果列表 + */ + List> selectCostByEnterCoilNo(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("enterCoilNo") String enterCoilNo); + + /** + * 删除指定日期的成本记录 + * + * @param calcDate 计算日期 + * @return 删除数量 + */ + int deleteByCalcDate(@Param("calcDate") LocalDate calcDate); + + /** + * 囤积成本(按入场钢卷号聚合)列表查询 + * + * @param calcDate 计算日期 + * @param enterCoilNo 入场钢卷号(可选,支持精确匹配或前缀匹配,可在 SQL 中自行处理) + * @param offset 分页偏移量 + * @param pageSize 分页大小 + * @return 聚合后的列表 + */ + List> selectStockpileByEnterCoilNo(@Param("calcDate") LocalDate calcDate, + @Param("enterCoilNo") String enterCoilNo, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** + * 囤积成本(按入场钢卷号聚合)列表总数 + * + * @param calcDate 计算日期 + * @param enterCoilNo 入场钢卷号(可选) + * @return 入场钢卷号分组条数 + */ + long countStockpileByEnterCoilNo(@Param("calcDate") LocalDate calcDate, + @Param("enterCoilNo") String enterCoilNo); + + /** + * 囤积成本(按入场钢卷号聚合)汇总 + * + * @param calcDate 计算日期 + * @param enterCoilNo 入场钢卷号(可选) + * @return 汇总数据 + */ + Map selectStockpileSummaryByEnterCoilNo(@Param("calcDate") LocalDate calcDate, + @Param("enterCoilNo") String enterCoilNo); + + /** + * 首页概览:直接基于 wms_material_coil 现算当日成本 + */ + Map selectOverviewFromMaterialCoil(); + + /** + * 成本趋势(按日汇总) + */ + List> selectCostTrend(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 成本检索(基于 wms_material_coil 实时计算)- 列表 + */ + List> selectMaterialCostCards(@Param("enterCoilNo") String enterCoilNo, + @Param("calcDate") LocalDate calcDate, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** + * 成本检索(基于 wms_material_coil 实时计算)- 总数 + */ + long countMaterialCostCards(@Param("enterCoilNo") String enterCoilNo, + @Param("calcDate") LocalDate calcDate); + + /** + * 成本检索(基于 wms_material_coil 实时计算)- 汇总 + */ + Map selectMaterialCostSummary(@Param("enterCoilNo") String enterCoilNo, + @Param("calcDate") LocalDate calcDate); +} + diff --git a/klp-wms/src/main/java/com/klp/mapper/CostStandardConfigMapper.java b/klp-wms/src/main/java/com/klp/mapper/CostStandardConfigMapper.java new file mode 100644 index 00000000..7dc73770 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/mapper/CostStandardConfigMapper.java @@ -0,0 +1,33 @@ +package com.klp.mapper; + +import com.klp.domain.CostStandardConfig; +import com.klp.domain.vo.CostStandardConfigVo; +import com.klp.common.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; + +/** + * 成本标准配置表Mapper接口 + * + * @author klp + * @date 2025-11-25 + */ +public interface CostStandardConfigMapper extends BaseMapperPlus { + + /** + * 查询指定日期的有效成本标准 + * + * @param date 日期 + * @return 成本标准配置 + */ + CostStandardConfigVo selectEffectiveByDate(@Param("date") LocalDate date); + + /** + * 查询当前有效的成本标准 + * + * @return 成本标准配置 + */ + CostStandardConfigVo selectCurrentEffective(); +} + diff --git a/klp-wms/src/main/java/com/klp/service/ICostCoilDailyService.java b/klp-wms/src/main/java/com/klp/service/ICostCoilDailyService.java new file mode 100644 index 00000000..795aa955 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/ICostCoilDailyService.java @@ -0,0 +1,156 @@ +package com.klp.service; + +import com.klp.domain.vo.CostCoilDailyVo; +import com.klp.domain.bo.CostCoilDailyBo; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.core.domain.PageQuery; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 钢卷日成本记录表Service接口 + * + * @author klp + * @date 2025-11-25 + */ +public interface ICostCoilDailyService { + + /** + * 查询钢卷日成本记录表 + */ + CostCoilDailyVo queryById(Long costId); + + /** + * 查询钢卷日成本记录表列表 + */ + TableDataInfo queryPageList(CostCoilDailyBo bo, PageQuery pageQuery); + + /** + * 查询钢卷日成本记录表列表 + */ + List queryList(CostCoilDailyBo bo); + + /** + * 新增钢卷日成本记录表 + */ + Boolean insertByBo(CostCoilDailyBo bo); + + /** + * 修改钢卷日成本记录表 + */ + Boolean updateByBo(CostCoilDailyBo bo); + + /** + * 校验并批量删除钢卷日成本记录表信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 实时计算指定钢卷的成本 + * + * @param coilId 钢卷ID(可选,不传则计算所有现存钢卷) + * @param calcTime 计算时间点(可选,默认当前时间) + * @return 成本计算结果 + */ + Map calculateCost(Long coilId, java.time.LocalDateTime calcTime); + + /** + * 批量计算多个钢卷的成本 + * + * @param coilIds 钢卷ID列表 + * @param calcTime 计算时间点(可选,默认当前时间) + * @return 成本计算结果列表,每个元素包含一个钢卷的成本信息 + */ + List> batchCalculateCost(List coilIds, java.time.LocalDateTime calcTime); + + /** + * 批量计算钢卷成本(定时任务使用) + * + * @param calcDate 计算日期(前一日) + * @return 计算成功的数量 + */ + int calculateDailyCost(LocalDate calcDate); + + /** + * 查询成本统计报表 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param groupBy 分组维度(warehouse/itemType/materialType) + * @param warehouseId 库区ID(可选) + * @return 统计结果 + */ + Map queryCostSummary(LocalDate startDate, LocalDate endDate, String groupBy, Long warehouseId); + + /** + * 查询成本趋势分析 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 趋势数据 + */ + List> queryCostTrend(LocalDate startDate, LocalDate endDate); + + /** + * 按入场钢卷号维度计算成本 + * 同一个入场钢卷号可能被分卷成多个钢卷,需要汇总计算 + * 未发货的:计算到当日;已发货的:计算到发货前一天 + * + * @param enterCoilNo 入场钢卷号 + * @param calcDate 计算日期(可选,默认当前日期) + * @return 成本计算结果(包含各子卷明细和汇总) + */ + Map calculateCostByEnterCoilNo(String enterCoilNo, LocalDate calcDate); + + /** + * 批量按入场钢卷号维度计算成本(定时任务使用) + * 按入场钢卷号分组,计算每个入场钢卷号的总成本 + * + * @param calcDate 计算日期(可选,默认前一日) + * @return 计算成功的入场钢卷号数量 + */ + int calculateDailyCostByEnterCoilNo(LocalDate calcDate); + + /** + * 现算成本检索(基于 wms_material_coil) + * + * @param enterCoilNo 入场钢卷号(支持前缀匹配) + * @param calcDate 计算日期(可选,默认当前日期) + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 汇总与分页明细 + */ + Map searchMaterialCost(String enterCoilNo, LocalDate calcDate, int pageNum, int pageSize); + + /** + * 查询按入场钢卷号统计的成本报表 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param enterCoilNo 入场钢卷号(可选,用于精确查询) + * @return 统计结果列表 + */ + List> queryCostByEnterCoilNo(LocalDate startDate, LocalDate endDate, String enterCoilNo); + + /** + * 成本模块首页概览数据 + * 统计当前「现存且未发货」钢卷的总成本、总净重、总毛重以及平均在库天数 + * + * @return 概览数据:totalCost、totalNetWeight、totalGrossWeight、avgStorageDays、totalCoils + */ + Map queryOverview(); + + /** + * 囤积成本页数据(后台统一计算) + * + * @param enterCoilNo 入场钢卷号(可选) + * @param currentCoilNo 当前钢卷号(可选) + * @param pageQuery 分页参数 + * @return rows+total+summary 结果 + */ + Map queryStockpileCostList(String enterCoilNo, String currentCoilNo, PageQuery pageQuery); +} + diff --git a/klp-wms/src/main/java/com/klp/service/ICostStandardConfigService.java b/klp-wms/src/main/java/com/klp/service/ICostStandardConfigService.java new file mode 100644 index 00000000..e11bad90 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/ICostStandardConfigService.java @@ -0,0 +1,65 @@ +package com.klp.service; + +import com.klp.domain.vo.CostStandardConfigVo; +import com.klp.domain.bo.CostStandardConfigBo; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.core.domain.PageQuery; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +/** + * 成本标准配置表Service接口 + * + * @author klp + * @date 2025-11-25 + */ +public interface ICostStandardConfigService { + + /** + * 查询成本标准配置表 + */ + CostStandardConfigVo queryById(Long configId); + + /** + * 查询成本标准配置表列表 + */ + TableDataInfo queryPageList(CostStandardConfigBo bo, PageQuery pageQuery); + + /** + * 查询成本标准配置表列表 + */ + List queryList(CostStandardConfigBo bo); + + /** + * 新增成本标准配置表 + */ + Boolean insertByBo(CostStandardConfigBo bo); + + /** + * 修改成本标准配置表 + */ + Boolean updateByBo(CostStandardConfigBo bo); + + /** + * 校验并批量删除成本标准配置表信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 查询当前有效的成本标准 + * + * @return 成本标准配置 + */ + CostStandardConfigVo queryCurrentEffective(); + + /** + * 查询指定日期的有效成本标准 + * + * @param date 日期 + * @return 成本标准配置 + */ + CostStandardConfigVo queryEffectiveByDate(LocalDate date); +} + diff --git a/klp-wms/src/main/java/com/klp/service/impl/CostCoilDailyServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/CostCoilDailyServiceImpl.java new file mode 100644 index 00000000..2e5968d9 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/impl/CostCoilDailyServiceImpl.java @@ -0,0 +1,946 @@ +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.klp.domain.bo.CostCoilDailyBo; +import com.klp.domain.vo.CostCoilDailyVo; +import com.klp.domain.CostCoilDaily; +import com.klp.domain.WmsMaterialCoil; +import com.klp.domain.vo.CostStandardConfigVo; +import com.klp.mapper.CostCoilDailyMapper; +import com.klp.mapper.WmsMaterialCoilMapper; +import com.klp.service.ICostCoilDailyService; +import com.klp.service.ICostStandardConfigService; + +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 CostCoilDailyServiceImpl implements ICostCoilDailyService { + + private final CostCoilDailyMapper baseMapper; + private final WmsMaterialCoilMapper coilMapper; + private final ICostStandardConfigService costStandardConfigService; + + /** + * 查询钢卷日成本记录表 + */ + @Override + public CostCoilDailyVo queryById(Long costId) { + return baseMapper.selectVoById(costId); + } + + /** + * 查询钢卷日成本记录表列表 + */ + @Override + public TableDataInfo queryPageList(CostCoilDailyBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询钢卷日成本记录表列表 + */ + @Override + public List queryList(CostCoilDailyBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(CostCoilDailyBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getCostId() != null, CostCoilDaily::getCostId, bo.getCostId()); + lqw.eq(bo.getCoilId() != null, CostCoilDaily::getCoilId, bo.getCoilId()); + lqw.like(bo.getCurrentCoilNo() != null, CostCoilDaily::getCurrentCoilNo, bo.getCurrentCoilNo()); + lqw.eq(bo.getCalcDate() != null, CostCoilDaily::getCalcDate, bo.getCalcDate()); + lqw.eq(bo.getWarehouseId() != null, CostCoilDaily::getWarehouseId, bo.getWarehouseId()); + lqw.eq(bo.getItemType() != null, CostCoilDaily::getItemType, bo.getItemType()); + lqw.eq(bo.getMaterialType() != null, CostCoilDaily::getMaterialType, bo.getMaterialType()); + if (bo.getStartDate() != null) { + lqw.ge(CostCoilDaily::getCalcDate, bo.getStartDate()); + } + if (bo.getEndDate() != null) { + lqw.le(CostCoilDaily::getCalcDate, bo.getEndDate()); + } + lqw.orderByDesc(CostCoilDaily::getCalcDate); + lqw.orderByDesc(CostCoilDaily::getCostId); + return lqw; + } + + /** + * 新增钢卷日成本记录表 + */ + @Override + public Boolean insertByBo(CostCoilDailyBo bo) { + CostCoilDaily add = BeanUtil.toBean(bo, CostCoilDaily.class); + validEntityBeforeSave(add); + return baseMapper.insert(add) > 0; + } + + /** + * 修改钢卷日成本记录表 + */ + @Override + public Boolean updateByBo(CostCoilDailyBo bo) { + CostCoilDaily update = BeanUtil.toBean(bo, CostCoilDaily.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CostCoilDaily entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除钢卷日成本记录表信息 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 实时计算指定钢卷的成本 + */ + @Override + public Map calculateCost(Long coilId, LocalDateTime calcTime) { + Map 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. 获取成本标准(使用入库日期的成本标准) + CostStandardConfigVo 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> batchCalculateCost(List coilIds, LocalDateTime calcTime) { + List> results = new ArrayList<>(); + + if (coilIds == null || coilIds.isEmpty()) { + return results; + } + + // 批量查询钢卷信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("coil_id", coilIds); + List coils = coilMapper.selectList(queryWrapper); + + if (coils == null || coils.isEmpty()) { + return results; + } + + // 获取成本标准(统一使用当前有效的标准,因为批量计算时可能跨多个日期) + LocalDate calcDate = calcTime != null ? calcTime.toLocalDate() : LocalDate.now(); + CostStandardConfigVo 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 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) { + CostStandardConfigVo startDateStandard = costStandardConfigService.queryEffectiveByDate(startDate); + if (startDateStandard != null && startDateStandard.getUnitCost() != null) { + finalUnitCost = startDateStandard.getUnitCost(); + } else { + CostStandardConfigVo 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 queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("data_type", 1) // 现存数据 + .isNull("export_time") // 未发货(export_time IS NULL) + .eq("del_flag", 0); // 未删除 + + List coils = coilMapper.selectList(queryWrapper); + log.info("找到 {} 个需要计算成本的钢卷", coils.size()); + + // 2. 获取成本标准 + CostStandardConfigVo 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. 保存成本记录 + CostCoilDaily costRecord = new CostCoilDaily(); + 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 queryCostSummary(LocalDate startDate, LocalDate endDate, String groupBy, Long warehouseId) { + Map result = new HashMap<>(); + + // 查询汇总数据 + Map summary = baseMapper.selectCostSummary(startDate, endDate, warehouseId, null, null); + result.put("summary", summary); + + // 根据分组维度查询明细 + List> 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> 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 calculateCostByEnterCoilNo(String enterCoilNo, LocalDate calcDate) { + Map result = new HashMap<>(); + result.put("enterCoilNo", enterCoilNo); + + if (calcDate == null) { + calcDate = LocalDate.now(); + } + + // 1. 查询该入场钢卷号下的所有钢卷(包括已发货和未发货的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("enter_coil_no", enterCoilNo) + .eq("data_type", 1) // 现存数据 + .eq("del_flag", 0); // 未删除 + + List coils = coilMapper.selectList(queryWrapper); + if (coils.isEmpty()) { + result.put("error", "未找到入场钢卷号 " + enterCoilNo + " 的相关钢卷"); + return result; + } + + List> 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 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; + } + + // 获取成本标准 + CostStandardConfigVo 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 queryWrapper = new QueryWrapper<>(); + queryWrapper.select("DISTINCT enter_coil_no") + .eq("data_type", 1) + .eq("del_flag", 0) + .isNotNull("enter_coil_no"); + + List 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 costResult = calculateCostByEnterCoilNo(enterCoilNo, calcDate); + + if (costResult.containsKey("error")) { + log.warn("计算入场钢卷号 {} 的成本时发生错误: {}", enterCoilNo, costResult.get("error")); + continue; + } + + // 获取该入场钢卷号下的所有钢卷 + QueryWrapper coilQueryWrapper = new QueryWrapper<>(); + coilQueryWrapper.eq("enter_coil_no", enterCoilNo) + .eq("data_type", 1) + .eq("del_flag", 0); + + List 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; + } + + // 获取成本标准 + CostStandardConfigVo 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); + + // 保存成本记录 + CostCoilDaily costRecord = new CostCoilDaily(); + 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 searchMaterialCost(String enterCoilNo, LocalDate calcDate, int pageNum, int pageSize) { + Map 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 summary = buildEmptySearchSummary(trimmedEnterCoilNo, effectiveCalcDate); + if (trimmedEnterCoilNo == null) { + result.put("summary", summary); + return result; + } + + long total = baseMapper.countMaterialCostCards(trimmedEnterCoilNo, effectiveCalcDate); + if (total > 0) { + Map dbSummary = baseMapper.selectMaterialCostSummary(trimmedEnterCoilNo, effectiveCalcDate); + summary = normalizeMaterialCostSummary(dbSummary, trimmedEnterCoilNo, effectiveCalcDate); + + int offset = (safePageNum - 1) * safePageSize; + List> 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> queryCostByEnterCoilNo(LocalDate startDate, LocalDate endDate, String enterCoilNo) { + return baseMapper.selectCostByEnterCoilNo(startDate, endDate, enterCoilNo); + } + + /** + * 囤积成本页数据(后台统一计算) + */ + @Override + public Map queryStockpileCostList(String enterCoilNo, String currentCoilNo, PageQuery pageQuery) { + Map 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> rows = baseMapper.selectStockpileByEnterCoilNo(calcDate, trimmedEnterCoilNo, offset, pageSize); + long total = baseMapper.countStockpileByEnterCoilNo(calcDate, trimmedEnterCoilNo); + Map 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.putIfAbsent("totalCoils", total); + + result.put("rows", rows); + result.put("total", total); + result.put("summary", summary); + return result; + } + + /** + * 成本模块首页概览数据 + * 统计当前「现存且未发货」钢卷的总成本、总净重、总毛重以及平均在库天数 + */ + @Override + public Map queryOverview() { + Map db = baseMapper.selectOverviewFromMaterialCoil(); + Map result = new HashMap<>(); + + if (db == null || db.isEmpty()) { + result.put("totalCost", 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 totalNetWeight = toBigDecimal(db.get("totalNetWeight"), 3); + BigDecimal totalGrossWeight = toBigDecimal(db.get("totalGrossWeight"), 3); + BigDecimal avgStorageDays = toBigDecimal(db.get("avgStorageDays"), 2); + Number totalCoils = db.get("totalCoils") instanceof Number ? (Number) db.get("totalCoils") : 0; + + result.put("totalCost", totalCost); + result.put("totalNetWeight", totalNetWeight); + result.put("totalGrossWeight", totalGrossWeight); + result.put("avgStorageDays", avgStorageDays); + result.put("totalCoils", totalCoils.intValue()); + + 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 buildEmptySearchSummary(String enterCoilNo, LocalDate calcDate) { + Map 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 normalizeMaterialCostSummary(Map db, String enterCoilNo, LocalDate calcDate) { + Map 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; + } + } +} + diff --git a/klp-wms/src/main/java/com/klp/service/impl/CostStandardConfigServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/CostStandardConfigServiceImpl.java new file mode 100644 index 00000000..26ee1bc6 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/impl/CostStandardConfigServiceImpl.java @@ -0,0 +1,142 @@ +package com.klp.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.core.domain.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.klp.domain.bo.CostStandardConfigBo; +import com.klp.domain.vo.CostStandardConfigVo; +import com.klp.domain.CostStandardConfig; +import com.klp.mapper.CostStandardConfigMapper; +import com.klp.service.ICostStandardConfigService; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +/** + * 成本标准配置表Service业务层处理 + * + * @author klp + * @date 2025-11-25 + */ +@RequiredArgsConstructor +@Service +public class CostStandardConfigServiceImpl implements ICostStandardConfigService { + + private final CostStandardConfigMapper baseMapper; + + /** + * 查询成本标准配置表 + */ + @Override + public CostStandardConfigVo queryById(Long configId) { + return baseMapper.selectVoById(configId); + } + + /** + * 查询成本标准配置表列表 + */ + @Override + public TableDataInfo queryPageList(CostStandardConfigBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询成本标准配置表列表 + */ + @Override + public List queryList(CostStandardConfigBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(CostStandardConfigBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getConfigId() != null, CostStandardConfig::getConfigId, bo.getConfigId()); + lqw.eq(bo.getUnitCost() != null, CostStandardConfig::getUnitCost, bo.getUnitCost()); + lqw.eq(bo.getEffectiveDate() != null, CostStandardConfig::getEffectiveDate, bo.getEffectiveDate()); + lqw.eq(bo.getStatus() != null, CostStandardConfig::getStatus, bo.getStatus()); + lqw.orderByDesc(CostStandardConfig::getEffectiveDate); + return lqw; + } + + /** + * 新增成本标准配置表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(CostStandardConfigBo bo) { + // 如果新标准立即生效,需要将当前有效的标准标记为失效 + if (bo.getEffectiveDate() != null && !bo.getEffectiveDate().isAfter(LocalDate.now())) { + // 将当前有效的标准标记为失效 + CostStandardConfigVo current = queryCurrentEffective(); + if (current != null) { + CostStandardConfig update = new CostStandardConfig(); + update.setConfigId(current.getConfigId()); + update.setExpireDate(LocalDate.now().minusDays(1)); + update.setStatus(0); + baseMapper.updateById(update); + } + } + + CostStandardConfig add = BeanUtil.toBean(bo, CostStandardConfig.class); + if (add.getStatus() == null) { + add.setStatus(1); // 默认有效 + } + validEntityBeforeSave(add); + return baseMapper.insert(add) > 0; + } + + /** + * 修改成本标准配置表 + */ + @Override + public Boolean updateByBo(CostStandardConfigBo bo) { + CostStandardConfig update = BeanUtil.toBean(bo, CostStandardConfig.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CostStandardConfig entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除成本标准配置表信息 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 查询当前有效的成本标准 + */ + @Override + public CostStandardConfigVo queryCurrentEffective() { + return baseMapper.selectCurrentEffective(); + } + + /** + * 查询指定日期的有效成本标准 + */ + @Override + public CostStandardConfigVo queryEffectiveByDate(LocalDate date) { + return baseMapper.selectEffectiveByDate(date); + } +} + diff --git a/klp-wms/src/main/java/com/klp/task/CostCoilDailyTask.java b/klp-wms/src/main/java/com/klp/task/CostCoilDailyTask.java new file mode 100644 index 00000000..f228ac87 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/task/CostCoilDailyTask.java @@ -0,0 +1,54 @@ +package com.klp.task; + +import com.klp.service.ICostCoilDailyService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +/** + * 成本日统计定时任务 + * + * 每天凌晨 2 点执行: + * 1. 按钢卷维度生成前一日的日成本记录; + * 2. 按入场钢卷号维度生成前一日的日成本记录。 + * + * 说明: + * - 具体的计算逻辑在 {@link ICostCoilDailyService#calculateDailyCost(LocalDate)} 和 + * {@link ICostCoilDailyService#calculateDailyCostByEnterCoilNo(LocalDate)} 中实现。 + * - 当前任务只负责在固定时间触发这两个方法。 + */ +@Slf4j +@RequiredArgsConstructor +@Component +public class CostCoilDailyTask { + + private final ICostCoilDailyService costCoilDailyService; + + /** + * 每天凌晨 2:00 计算前一日钢卷日成本(按钢卷维度) + */ + @Scheduled(cron = "0 0 2 * * ?") + public void calculateDailyCostByCoil() { + LocalDate calcDate = LocalDate.now().minusDays(1); + log.info("[成本定时任务] 开始按钢卷维度计算日期 {} 的成本", calcDate); + int count = costCoilDailyService.calculateDailyCost(calcDate); + log.info("[成本定时任务] 按钢卷维度计算日期 {} 的成本完成,成功 {} 条", calcDate, count); + } + + /** + * 每天凌晨 2:10 计算前一日钢卷日成本(按入场钢卷号维度) + * 与上一个任务错开几分钟,避免同时占用资源。 + */ + @Scheduled(cron = "0 10 2 * * ?") + public void calculateDailyCostByEnterCoilNo() { + LocalDate calcDate = LocalDate.now().minusDays(1); + log.info("[成本定时任务] 开始按入场钢卷号维度计算日期 {} 的成本", calcDate); + int count = costCoilDailyService.calculateDailyCostByEnterCoilNo(calcDate); + log.info("[成本定时任务] 按入场钢卷号维度计算日期 {} 的成本完成,成功 {} 个入场钢卷号", calcDate, count); + } +} + + diff --git a/klp-wms/src/main/resources/mapper/klp/CostCoilDailyMapper.xml b/klp-wms/src/main/resources/mapper/klp/CostCoilDailyMapper.xml new file mode 100644 index 00000000..53325526 --- /dev/null +++ b/klp-wms/src/main/resources/mapper/klp/CostCoilDailyMapper.xml @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + m.enter_coil_no, + m.coil_id, + m.current_coil_no, + IFNULL(m.net_weight, 0) / 1000 AS net_weight_ton, + IFNULL(m.gross_weight, 0) / 1000 AS gross_weight_ton, + CASE + WHEN IFNULL(m.gross_weight, 0) > 0 THEN 'gross' + ELSE 'net' + END AS weight_basis, + CASE + WHEN IFNULL(m.gross_weight, 0) > 0 THEN IFNULL(m.gross_weight, 0) + ELSE IFNULL(m.net_weight, 0) + END / 1000 AS charge_weight_ton, + DATE(m.create_time) AS start_date, + CASE + WHEN m.export_time IS NOT NULL THEN DATE_SUB(DATE(m.export_time), INTERVAL 1 DAY) + WHEN #{calcDate} IS NOT NULL THEN #{calcDate} + ELSE CURDATE() + END AS raw_end_date, + CASE + WHEN m.export_time IS NULL THEN 0 + ELSE 1 + END AS is_shipped, + IFNULL(cs.unit_cost, 0) AS unit_cost, + m.warehouse_id, + COALESCE(w.warehouse_name, '-') AS warehouse_name + FROM wms_material_coil m + LEFT JOIN cost_standard_config cs + ON cs.status = 1 + AND DATE(m.create_time) >= cs.effective_date + AND (cs.expire_date IS NULL OR DATE(m.create_time) <= cs.expire_date) + LEFT JOIN wms_warehouse w ON w.warehouse_id = m.warehouse_id AND w.del_flag = 0 + WHERE m.data_type = 1 + AND m.del_flag = 0 + AND m.enter_coil_no IS NOT NULL + + AND m.enter_coil_no LIKE CONCAT(#{enterCoilNo}, '%') + + + + + SELECT + base.*, + CASE + WHEN base.raw_end_date < base.start_date THEN base.start_date + ELSE base.raw_end_date + END AS end_date, + CASE + WHEN DATEDIFF( + CASE + WHEN base.raw_end_date < base.start_date THEN base.start_date + ELSE base.raw_end_date + END, + base.start_date + ) < 0 THEN 1 + ELSE DATEDIFF( + CASE + WHEN base.raw_end_date < base.start_date THEN base.start_date + ELSE base.raw_end_date + END, + base.start_date + ) + 1 + END AS storage_days + FROM ( + + ) base + + + + + + + + + + + DELETE FROM cost_coil_daily + WHERE calc_date = #{calcDate} + + + + diff --git a/klp-wms/src/main/resources/mapper/klp/CostStandardConfigMapper.xml b/klp-wms/src/main/resources/mapper/klp/CostStandardConfigMapper.xml new file mode 100644 index 00000000..77c3afaa --- /dev/null +++ b/klp-wms/src/main/resources/mapper/klp/CostStandardConfigMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +