diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/profit/ProfitMarginReportQueryDTO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/profit/ProfitMarginReportQueryDTO.java new file mode 100644 index 000000000..24133b332 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/profit/ProfitMarginReportQueryDTO.java @@ -0,0 +1,145 @@ +package com.bsd.pjcloud.supermarket.dto.profit; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 毛利润报表查询DTO + * + * @author pjcloud + * @date 2025-01-27 16:00:00 + */ +@Data +@Schema(description = "毛利润报表查询DTO") +public class ProfitMarginReportQueryDTO { + + /** + * 查询类型:hour-按小时, day-按天, month-按月, product-按商品 + */ + @Schema(description = "查询类型:hour-按小时, day-按天, month-按月, product-按商品", example = "day") + private String queryType; + + /** + * 开始时间 + */ + @Schema(description = "开始时间") + private LocalDateTime startTime; + + /** + * 结束时间 + */ + @Schema(description = "结束时间") + private LocalDateTime endTime; + + /** + * 开始日期 + */ + @Schema(description = "开始日期") + private LocalDate startDate; + + /** + * 结束日期 + */ + @Schema(description = "结束日期") + private LocalDate endDate; + + /** + * 商品分类ID + */ + @Schema(description = "商品分类ID") + private Long categoryId; + + /** + * 商品名称(模糊查询) + */ + @Schema(description = "商品名称(模糊查询)") + private String productName; + + /** + * 商品编码(模糊查询) + */ + @Schema(description = "商品编码(模糊查询)") + private String productCode; + + /** + * 销售员ID + */ + @Schema(description = "销售员ID") + private Long salespersonId; + + /** + * 仓库ID + */ + @Schema(description = "仓库ID") + private String warehouseId; + + /** + * 排名数量限制 + */ + @Schema(description = "排名数量限制", example = "10") + private Integer topLimit; + + /** + * 毛利率范围筛选 - 最低毛利率(%) + */ + @Schema(description = "毛利率范围筛选 - 最低毛利率(%)", example = "10.0") + private BigDecimal minMarginRate; + + /** + * 毛利率范围筛选 - 最高毛利率(%) + */ + @Schema(description = "毛利率范围筛选 - 最高毛利率(%)", example = "50.0") + private BigDecimal maxMarginRate; + + /** + * 毛利润范围筛选 - 最低毛利润(元) + */ + @Schema(description = "毛利润范围筛选 - 最低毛利润(元)", example = "100.0") + private BigDecimal minProfitAmount; + + /** + * 毛利润范围筛选 - 最高毛利润(元) + */ + @Schema(description = "毛利润范围筛选 - 最高毛利润(元)", example = "1000.0") + private BigDecimal maxProfitAmount; + + /** + * 排序字段:profit_amount-毛利润, margin_rate-毛利率, sales_amount-销售额 + */ + @Schema(description = "排序字段:profit_amount-毛利润, margin_rate-毛利率, sales_amount-销售额", example = "profit_amount") + private String sortField; + + /** + * 排序方向:asc-升序, desc-降序 + */ + @Schema(description = "排序方向:asc-升序, desc-降序", example = "desc") + private String sortOrder; + + /** + * 是否只显示有毛利润的商品 + */ + @Schema(description = "是否只显示有毛利润的商品", example = "true") + private Boolean onlyProfitable; + + /** + * 是否只显示亏损的商品 + */ + @Schema(description = "是否只显示亏损的商品", example = "false") + private Boolean onlyLoss; + + /** + * 页码(从1开始) + */ + @Schema(description = "页码(从1开始)", example = "1") + private Integer pageNum = 1; + + /** + * 每页大小 + */ + @Schema(description = "每页大小", example = "1000") + private Integer pageSize = 1000; +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/profit/ProfitMarginReportVO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/profit/ProfitMarginReportVO.java new file mode 100644 index 000000000..61543f146 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/profit/ProfitMarginReportVO.java @@ -0,0 +1,369 @@ +package com.bsd.pjcloud.supermarket.vo.profit; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 毛利润报表VO + * + * @author pjcloud + * @date 2025-01-27 16:00:00 + */ +@Data +@Schema(description = "毛利润报表VO") +public class ProfitMarginReportVO { + + /** + * 报表生成时间 + */ + @Schema(description = "报表生成时间") + private LocalDateTime reportTime; + + /** + * 查询时间段 + */ + @Schema(description = "查询时间段") + private String timeRange; + + /** + * 毛利润汇总信息 + */ + @Schema(description = "毛利润汇总信息") + private ProfitSummaryVO profitSummary; + + /** + * 商品毛利润排名 + */ + @Schema(description = "商品毛利润排名") + private List productProfitRanking; + + /** + * 分类毛利润分析 + */ + @Schema(description = "分类毛利润分析") + private List categoryProfitAnalysis; + + /** + * 时段毛利润趋势 + */ + @Schema(description = "时段毛利润趋势") + private List timePeriodProfitTrend; + + /** + * 高毛利润商品 + */ + @Schema(description = "高毛利润商品") + private List highProfitProducts; + + /** + * 低毛利润/亏损商品 + */ + @Schema(description = "低毛利润/亏损商品") + private List lowProfitProducts; + + /** + * 毛利润汇总信息VO + */ + @Data + @Schema(description = "毛利润汇总信息VO") + public static class ProfitSummaryVO { + + /** + * 总销售额(元) + */ + @Schema(description = "总销售额(元)") + private BigDecimal totalSalesAmount; + + /** + * 总成本(元) + */ + @Schema(description = "总成本(元)") + private BigDecimal totalCostAmount; + + /** + * 总毛利润(元) + */ + @Schema(description = "总毛利润(元)") + private BigDecimal totalProfitAmount; + + /** + * 平均毛利率(%) + */ + @Schema(description = "平均毛利率(%)") + private BigDecimal averageMarginRate; + + /** + * 销售商品种类数 + */ + @Schema(description = "销售商品种类数") + private Integer productCount; + + /** + * 销售笔数 + */ + @Schema(description = "销售笔数") + private Integer salesCount; + + /** + * 盈利商品数 + */ + @Schema(description = "盈利商品数") + private Integer profitableProductCount; + + /** + * 亏损商品数 + */ + @Schema(description = "亏损商品数") + private Integer lossProductCount; + } + + /** + * 商品毛利润VO + */ + @Data + @Schema(description = "商品毛利润VO") + public static class ProductProfitVO { + + /** + * 排名 + */ + @Schema(description = "排名") + private Integer ranking; + + /** + * 商品ID + */ + @Schema(description = "商品ID") + private Long productId; + + /** + * 商品名称 + */ + @Schema(description = "商品名称") + private String productName; + + /** + * 商品编码 + */ + @Schema(description = "商品编码") + private String productCode; + + /** + * 商品分类ID + */ + @Schema(description = "商品分类ID") + private Long categoryId; + + /** + * 商品分类名称 + */ + @Schema(description = "商品分类名称") + private String categoryName; + + /** + * 销售数量 + */ + @Schema(description = "销售数量") + private Double salesQuantity; + + /** + * 销售单价(元) + */ + @Schema(description = "销售单价(元)") + private BigDecimal salesPrice; + + /** + * 成本单价(元) + */ + @Schema(description = "成本单价(元)") + private BigDecimal costPrice; + + /** + * 销售额(元) + */ + @Schema(description = "销售额(元)") + private BigDecimal salesAmount; + + /** + * 成本总额(元) + */ + @Schema(description = "成本总额(元)") + private BigDecimal costAmount; + + /** + * 毛利润(元) + */ + @Schema(description = "毛利润(元)") + private BigDecimal profitAmount; + + /** + * 毛利率(%) + */ + @Schema(description = "毛利率(%)") + private BigDecimal marginRate; + + /** + * 销售笔数 + */ + @Schema(description = "销售笔数") + private Integer salesCount; + + /** + * 利润状态:profitable-盈利, loss-亏损, break_even-盈亏平衡 + */ + @Schema(description = "利润状态:profitable-盈利, loss-亏损, break_even-盈亏平衡") + private String profitStatus; + + /** + * 包装单位 + */ + @Schema(description = "包装单位") + private String unit; + + /** + * 包装单位名称 + */ + @Schema(description = "包装单位名称") + private String unitName; + } + + /** + * 分类毛利润VO + */ + @Data + @Schema(description = "分类毛利润VO") + public static class CategoryProfitVO { + + /** + * 分类ID + */ + @Schema(description = "分类ID") + private Long categoryId; + + /** + * 分类名称 + */ + @Schema(description = "分类名称") + private String categoryName; + + /** + * 销售商品种类数 + */ + @Schema(description = "销售商品种类数") + private Integer productCount; + + /** + * 销售数量 + */ + @Schema(description = "销售数量") + private Double totalSalesQuantity; + + /** + * 销售额(元) + */ + @Schema(description = "销售额(元)") + private BigDecimal totalSalesAmount; + + /** + * 成本总额(元) + */ + @Schema(description = "成本总额(元)") + private BigDecimal totalCostAmount; + + /** + * 毛利润(元) + */ + @Schema(description = "毛利润(元)") + private BigDecimal totalProfitAmount; + + /** + * 平均毛利率(%) + */ + @Schema(description = "平均毛利率(%)") + private BigDecimal averageMarginRate; + + /** + * 销售笔数 + */ + @Schema(description = "销售笔数") + private Integer salesCount; + + /** + * 盈利商品数 + */ + @Schema(description = "盈利商品数") + private Integer profitableProductCount; + + /** + * 亏损商品数 + */ + @Schema(description = "亏损商品数") + private Integer lossProductCount; + } + + /** + * 时段毛利润VO + */ + @Data + @Schema(description = "时段毛利润VO") + public static class TimePeriodProfitVO { + + /** + * 时间段标识(如:09:00, 2025-01-27) + */ + @Schema(description = "时间段标识") + private String timeLabel; + + /** + * 销售额(元) + */ + @Schema(description = "销售额(元)") + private BigDecimal salesAmount; + + /** + * 成本总额(元) + */ + @Schema(description = "成本总额(元)") + private BigDecimal costAmount; + + /** + * 毛利润(元) + */ + @Schema(description = "毛利润(元)") + private BigDecimal profitAmount; + + /** + * 毛利率(%) + */ + @Schema(description = "毛利率(%)") + private BigDecimal marginRate; + + /** + * 销售数量 + */ + @Schema(description = "销售数量") + private Double salesQuantity; + + /** + * 销售笔数 + */ + @Schema(description = "销售笔数") + private Integer salesCount; + + /** + * 销售商品种类数 + */ + @Schema(description = "销售商品种类数") + private Integer productCount; + + /** + * 利润增长率(%) + */ + @Schema(description = "利润增长率(%)") + private BigDecimal profitGrowthRate; + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/DATABASE_INDEX_OPTIMIZATION.sql b/pjcloud-supermarket/pjcloud-supermarket-biz/DATABASE_INDEX_OPTIMIZATION.sql new file mode 100644 index 000000000..7c8b610d8 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/DATABASE_INDEX_OPTIMIZATION.sql @@ -0,0 +1,94 @@ +-- 毛利润报表数据库索引优化建议(OceanBase适配版本) +-- 执行前请先备份数据库! + +-- 1. 销售记录表索引优化 +-- 复合索引:库存类型 + 订单类型 + 创建时间(OceanBase不支持DESC索引) +CREATE INDEX IF NOT EXISTS idx_inventory_records_type_order_time +ON sm_erp_inventory_records (inventory_type, order_type, create_time); + +-- 复合索引:库存类型 + 订单类型 + 库存日期 +CREATE INDEX IF NOT EXISTS idx_inventory_records_type_order_date +ON sm_erp_inventory_records (inventory_type, order_type, inventory_date); + +-- 单列索引:创建时间(用于时间范围查询) +CREATE INDEX IF NOT EXISTS idx_inventory_records_create_time +ON sm_erp_inventory_records (create_time); + +-- 单列索引:库存日期(用于日期范围查询) +CREATE INDEX IF NOT EXISTS idx_inventory_records_inventory_date +ON sm_erp_inventory_records (inventory_date); + +-- 单列索引:处理人员ID +CREATE INDEX IF NOT EXISTS idx_inventory_records_handler_user_id +ON sm_erp_inventory_records (handler_user_id); + +-- 单列索引:仓库ID +CREATE INDEX IF NOT EXISTS idx_inventory_records_warehouse_id +ON sm_erp_inventory_records (warehouse_id); + +-- 2. 销售明细表索引优化 +-- 复合索引:库存记录ID + 商品SPU ID +CREATE INDEX IF NOT EXISTS idx_inventory_records_item_inventory_spu +ON sm_erp_inventory_records_item (inventory_id, erp_spu_id); + +-- 单列索引:商品SPU ID +CREATE INDEX IF NOT EXISTS idx_inventory_records_item_erp_spu_id +ON sm_erp_inventory_records_item (erp_spu_id); + +-- 单列索引:包装单位ID +CREATE INDEX IF NOT EXISTS idx_inventory_records_item_unit_id +ON sm_erp_inventory_records_item (unit_id); + +-- 3. 商品表索引优化 +-- 复合索引:分类ID + 商品名称 +CREATE INDEX IF NOT EXISTS idx_erp_spu_category_name +ON sm_erp_spu (category_id, product_name); + +-- 单列索引:商品编码 +CREATE INDEX IF NOT EXISTS idx_erp_spu_product_code +ON sm_erp_spu (product_code); + +-- 单列索引:商品名称 +CREATE INDEX IF NOT EXISTS idx_erp_spu_product_name +ON sm_erp_spu (product_name); + +-- 单列索引:分类ID +CREATE INDEX IF NOT EXISTS idx_erp_spu_category_id +ON sm_erp_spu (category_id); + +-- 4. 包装单位表索引优化 +-- 单列索引:商品SPU ID +CREATE INDEX IF NOT EXISTS idx_erp_unit_erp_spu_id + ON sm_erp_unit (spu_id); + +-- 单列索引:进价字段(用于成本计算) +CREATE INDEX IF NOT EXISTS idx_erp_unit_purchase_price +ON sm_erp_unit (purchase_price); + +-- 5. 分类表索引优化 +-- 单列索引:分类名称 +CREATE INDEX IF NOT EXISTS idx_erp_category_name +ON sm_erp_category (name); + +-- 执行说明(OceanBase适配): +-- 1. 这些索引会显著提升查询性能 +-- 2. 索引会占用额外的存储空间 +-- 3. 插入和更新操作会稍微变慢(但影响很小) +-- 4. 建议在业务低峰期执行 +-- 5. 执行前请先分析现有索引,避免重复创建 +-- 6. OceanBase不支持DESC索引,已移除所有DESC关键字 +-- 7. 添加了单独的时间字段索引以优化排序查询 + +-- OceanBase检查现有索引的SQL: +-- SELECT * FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sm_erp_inventory_records'; +-- SELECT * FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sm_erp_inventory_records_item'; +-- SELECT * FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sm_erp_spu'; +-- SELECT * FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sm_erp_unit'; +-- SELECT * FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'sm_erp_category'; + +-- 或者使用SHOW INDEX(如果支持): +-- SHOW INDEX FROM sm_erp_inventory_records; +-- SHOW INDEX FROM sm_erp_inventory_records_item; +-- SHOW INDEX FROM sm_erp_spu; +-- SHOW INDEX FROM sm_erp_unit; +-- SHOW INDEX FROM sm_erp_category; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/ProfitMarginReportController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/ProfitMarginReportController.java new file mode 100644 index 000000000..a396d27eb --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/ProfitMarginReportController.java @@ -0,0 +1,190 @@ +package com.bsd.pjcloud.supermarket.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.log.annotation.SysLog; +import com.bsd.pjcloud.supermarket.dto.profit.ProfitMarginReportQueryDTO; +import com.bsd.pjcloud.supermarket.service.ProfitMarginReportService; +import com.bsd.pjcloud.supermarket.vo.profit.ProfitMarginReportVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +/** + * 毛利润报表管理 + * + * @author pjcloud + * @date 2025-01-27 16:00:00 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/profitMarginReport") +@Tag(description = "profitMarginReport", name = "毛利润报表管理") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class ProfitMarginReportController { + + private final ProfitMarginReportService profitMarginReportService; + + @Operation(summary = "获取毛利润报表", description = "获取完整的毛利润报表,包含汇总、排名、趋势等数据") + @GetMapping("/report") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R getProfitMarginReport( + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + ProfitMarginReportVO result = profitMarginReportService.getProfitMarginReport(queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取商品毛利润排名", description = "获取商品毛利润排名,支持多种排序方式") + @GetMapping("/productProfitRanking") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R> getProductProfitRanking( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + Page result = + profitMarginReportService.getProductProfitRanking(page, queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取分类毛利润分析", description = "获取各商品分类的毛利润分析数据") + @GetMapping("/categoryProfitAnalysis") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R> getCategoryProfitAnalysis( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + Page result = + profitMarginReportService.getCategoryProfitAnalysis(page, queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取时段毛利润趋势", description = "获取时段毛利润趋势数据,支持按小时、天、月查询") + @GetMapping("/timePeriodProfitTrend") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R> getTimePeriodProfitTrend( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + Page result = + profitMarginReportService.getTimePeriodProfitTrend(page, queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取高毛利润商品", description = "获取高毛利润商品列表,毛利率大于30%的商品") + @GetMapping("/highProfitProducts") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R> getHighProfitProducts( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + Page result = + profitMarginReportService.getHighProfitProducts(page, queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取低毛利润/亏损商品", description = "获取低毛利润或亏损商品列表,毛利率小于10%或亏损的商品") + @GetMapping("/lowProfitProducts") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R> getLowProfitProducts( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + Page result = + profitMarginReportService.getLowProfitProducts(page, queryDTO); + return R.ok(result); + } + + @Operation(summary = "获取毛利润汇总信息", description = "获取毛利润汇总信息,包含总销售额、总成本、总毛利润等") + @GetMapping("/profitSummary") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R getProfitSummary( + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + ProfitMarginReportVO.ProfitSummaryVO result = + profitMarginReportService.getProfitSummary(queryDTO); + return R.ok(result); + } + + @Operation(summary = "导出毛利润报表", description = "导出毛利润报表数据为Excel文件") + @PostMapping("/export") + @SysLog("导出毛利润报表") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_export')") + public R exportProfitMarginReport( + @Parameter(description = "查询条件") @RequestBody ProfitMarginReportQueryDTO queryDTO) { + // TODO: 实现Excel导出功能 + return R.ok("导出功能待实现"); + } + + @Operation(summary = "调试毛利润报表", description = "调试毛利润报表查询,输出详细日志") + @GetMapping("/debug") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R debugProfitMarginReport( + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + try { + // 调用汇总方法,会输出调试日志 + profitMarginReportService.getProfitSummary(queryDTO); + return R.ok("调试完成,请查看日志输出"); + } catch (Exception e) { + return R.failed("调试失败: " + e.getMessage()); + } + } + + @GetMapping("/debug/category") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + public R debugCategoryFilter( + @Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId, + @Parameter(description = "商品名称") @RequestParam(required = false) String productName) { + try { + ProfitMarginReportQueryDTO queryDTO = new ProfitMarginReportQueryDTO(); + queryDTO.setCategoryId(categoryId); + queryDTO.setProductName(productName); + + // 调用汇总方法,会输出调试日志 + profitMarginReportService.getProfitSummary(queryDTO); + return R.ok("分类筛选调试完成,请查看日志输出"); + } catch (Exception e) { + return R.failed("分类筛选调试失败: " + e.getMessage()); + } + } + + @GetMapping("/fast") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + @Operation(summary = "快速查询毛利润报表", description = "高性能版本的毛利润报表查询,适用于大数据量场景") + public R getProfitSummaryFast( + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + try { + // 强制启用分页模式 + if (queryDTO.getPageNum() == null) queryDTO.setPageNum(1); + if (queryDTO.getPageSize() == null) queryDTO.setPageSize(500); + + ProfitMarginReportVO.ProfitSummaryVO result = profitMarginReportService.getProfitSummary(queryDTO); + return R.ok(result); + } catch (Exception e) { + log.error("快速查询毛利润报表失败", e); + return R.failed("查询失败: " + e.getMessage()); + } + } + + @GetMapping("/fast/ranking") + @PreAuthorize("@pms.hasPermission('supermarket_profitMarginReport_view')") + @Operation(summary = "快速查询商品毛利润排名", description = "高性能版本的商品毛利润排名查询") + public R> getProductProfitRankingFast( + @Parameter(description = "查询条件") ProfitMarginReportQueryDTO queryDTO) { + try { + // 强制启用分页模式 + if (queryDTO.getPageNum() == null) queryDTO.setPageNum(1); + if (queryDTO.getPageSize() == null) queryDTO.setPageSize(500); + if (queryDTO.getTopLimit() == null) queryDTO.setTopLimit(50); + + // 创建分页对象 + Page page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); + Page result = profitMarginReportService.getProductProfitRanking(page, queryDTO); + return R.ok(result); + } catch (Exception e) { + log.error("快速查询商品毛利润排名失败", e); + return R.failed("查询失败: " + e.getMessage()); + } + } + +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/ProfitMarginReportService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/ProfitMarginReportService.java new file mode 100644 index 000000000..3ea84ae32 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/ProfitMarginReportService.java @@ -0,0 +1,77 @@ +package com.bsd.pjcloud.supermarket.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.supermarket.dto.profit.ProfitMarginReportQueryDTO; +import com.bsd.pjcloud.supermarket.vo.profit.ProfitMarginReportVO; + +import java.util.List; + +/** + * 毛利润报表服务接口 + * + * @author pjcloud + * @date 2025-01-27 16:00:00 + */ +public interface ProfitMarginReportService { + + /** + * 获取毛利润报表 + * + * @param queryDTO 查询条件 + * @return 毛利润报表 + */ + ProfitMarginReportVO getProfitMarginReport(ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取商品毛利润排名 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 商品毛利润排名 + */ + Page getProductProfitRanking(Page page, ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取分类毛利润分析 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 分类毛利润分析 + */ + Page getCategoryProfitAnalysis(Page page, ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取时段毛利润趋势 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 时段毛利润趋势 + */ + Page getTimePeriodProfitTrend(Page page, ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取高毛利润商品 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 高毛利润商品 + */ + Page getHighProfitProducts(Page page, ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取低毛利润/亏损商品 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 低毛利润/亏损商品 + */ + Page getLowProfitProducts(Page page, ProfitMarginReportQueryDTO queryDTO); + + /** + * 获取毛利润汇总信息 + * + * @param queryDTO 查询条件 + * @return 毛利润汇总信息 + */ + ProfitMarginReportVO.ProfitSummaryVO getProfitSummary(ProfitMarginReportQueryDTO queryDTO); +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/ProfitMarginReportServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/ProfitMarginReportServiceImpl.java new file mode 100644 index 000000000..7579f5b02 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/ProfitMarginReportServiceImpl.java @@ -0,0 +1,1188 @@ +package com.bsd.pjcloud.supermarket.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.common.data.datascope.DataScope; +import com.bsd.pjcloud.supermarket.dto.profit.ProfitMarginReportQueryDTO; +import com.bsd.pjcloud.supermarket.entity.*; +import com.bsd.pjcloud.supermarket.mapper.*; +import com.bsd.pjcloud.supermarket.service.ProfitMarginReportService; +import com.bsd.pjcloud.supermarket.vo.profit.ProfitMarginReportVO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 毛利润报表服务实现类 + * + * @author pjcloud + * @date 2025-01-27 16:00:00 + */ +@Slf4j +@Service +@AllArgsConstructor +public class ProfitMarginReportServiceImpl implements ProfitMarginReportService { + + private final SmErpInventoryRecordsMapper inventoryRecordsMapper; + private final SmErpInventoryRecordsItemMapper inventoryRecordsItemMapper; + private final SmErpSpuMapper spuMapper; + private final SmErpCategoryMapper categoryMapper; + private final SmErpUnitMapper unitMapper; + + @Override + public ProfitMarginReportVO getProfitMarginReport(ProfitMarginReportQueryDTO queryDTO) { + ProfitMarginReportVO reportVO = new ProfitMarginReportVO(); + reportVO.setReportTime(LocalDateTime.now()); + + // 设置查询时间段 + String timeRange = buildTimeRange(queryDTO); + reportVO.setTimeRange(timeRange); + + // 获取毛利润汇总信息 + ProfitMarginReportVO.ProfitSummaryVO profitSummary = getProfitSummary(queryDTO); + reportVO.setProfitSummary(profitSummary); + + // 获取商品毛利润排名 + List productProfitRanking = getProductProfitRankingData(queryDTO); + reportVO.setProductProfitRanking(productProfitRanking); + + // 获取分类毛利润分析 + List categoryProfitAnalysis = getCategoryProfitAnalysisData(queryDTO); + reportVO.setCategoryProfitAnalysis(categoryProfitAnalysis); + + // 获取时段毛利润趋势 + List timePeriodProfitTrend = getTimePeriodProfitTrendData(queryDTO); + reportVO.setTimePeriodProfitTrend(timePeriodProfitTrend); + + // 获取高毛利润商品 + List highProfitProducts = getHighProfitProductsData(queryDTO); + reportVO.setHighProfitProducts(highProfitProducts); + + // 获取低毛利润/亏损商品 + List lowProfitProducts = getLowProfitProductsData(queryDTO); + reportVO.setLowProfitProducts(lowProfitProducts); + + return reportVO; + } + + @Override + public Page getProductProfitRanking(Page page, ProfitMarginReportQueryDTO queryDTO) { + List productProfits = getProductProfitRankingData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), productProfits.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < productProfits.size()) { + resultPage.setRecords(productProfits.subList(start, end)); + resultPage.setTotal(productProfits.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getCategoryProfitAnalysis(Page page, ProfitMarginReportQueryDTO queryDTO) { + List categoryProfits = getCategoryProfitAnalysisData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), categoryProfits.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < categoryProfits.size()) { + resultPage.setRecords(categoryProfits.subList(start, end)); + resultPage.setTotal(categoryProfits.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getTimePeriodProfitTrend(Page page, ProfitMarginReportQueryDTO queryDTO) { + List timePeriodProfits = getTimePeriodProfitTrendData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), timePeriodProfits.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < timePeriodProfits.size()) { + resultPage.setRecords(timePeriodProfits.subList(start, end)); + resultPage.setTotal(timePeriodProfits.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getHighProfitProducts(Page page, ProfitMarginReportQueryDTO queryDTO) { + List highProfitProducts = getHighProfitProductsData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), highProfitProducts.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < highProfitProducts.size()) { + resultPage.setRecords(highProfitProducts.subList(start, end)); + resultPage.setTotal(highProfitProducts.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getLowProfitProducts(Page page, ProfitMarginReportQueryDTO queryDTO) { + List lowProfitProducts = getLowProfitProductsData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), lowProfitProducts.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < lowProfitProducts.size()) { + resultPage.setRecords(lowProfitProducts.subList(start, end)); + resultPage.setTotal(lowProfitProducts.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public ProfitMarginReportVO.ProfitSummaryVO getProfitSummary(ProfitMarginReportQueryDTO queryDTO) { + log.info("开始获取毛利润汇总信息,查询条件: {}", queryDTO); + + + // 获取销售明细(使用优化版本) + List salesItems = getSalesItemsOptimized(queryDTO); + + if (CollectionUtils.isEmpty(salesItems)) { + log.info("毛利润报表:没有找到销售明细,返回空汇总信息"); + return createEmptyProfitSummary(); + } + + // 获取商品信息 + Set productIds = salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .collect(Collectors.toSet()); + + List products = spuMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpSpuEntity::getId, productIds) + ); + Map productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + + // 批量获取包装单位信息(性能优化) + Map unitMap = getUnitMap(salesItems); + + // 计算汇总数据 + BigDecimal totalSalesAmount = BigDecimal.ZERO; + BigDecimal totalCostAmount = BigDecimal.ZERO; + int salesCount = salesItems.size(); + int profitableProductCount = 0; + int lossProductCount = 0; + Set uniqueProductIds = new HashSet<>(); + + for (SmErpInventoryRecordsItemEntity item : salesItems) { + SmErpSpuEntity product = productMap.get(item.getErpSpuId()); + if (product == null) continue; + + // 过滤补差价商品 + if (isPriceAdjustmentProduct(product.getProductName())) { + continue; + } + + uniqueProductIds.add(item.getErpSpuId()); + + // 计算销售额(实际销售价格) + BigDecimal itemSalesAmount = new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + totalSalesAmount = totalSalesAmount.add(itemSalesAmount); + + // 计算成本额 + // 优先使用包装单位的实际进价,如果没有则使用商品主表的成本价 + BigDecimal costPrice = getActualCostPrice(product, item, unitMap); + BigDecimal itemCostAmount = costPrice.multiply(new BigDecimal(item.getQuantity() != null ? item.getQuantity() : 0)); + totalCostAmount = totalCostAmount.add(itemCostAmount); + + + // 判断盈亏 + BigDecimal itemProfit = itemSalesAmount.subtract(itemCostAmount); + if (itemProfit.compareTo(BigDecimal.ZERO) > 0) { + profitableProductCount++; + } else if (itemProfit.compareTo(BigDecimal.ZERO) < 0) { + lossProductCount++; + } + } + + BigDecimal totalProfitAmount = totalSalesAmount.subtract(totalCostAmount); + BigDecimal averageMarginRate = totalSalesAmount.compareTo(BigDecimal.ZERO) > 0 ? + totalProfitAmount.divide(totalSalesAmount, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + ProfitMarginReportVO.ProfitSummaryVO summaryVO = new ProfitMarginReportVO.ProfitSummaryVO(); + summaryVO.setTotalSalesAmount(totalSalesAmount); + summaryVO.setTotalCostAmount(totalCostAmount); + summaryVO.setTotalProfitAmount(totalProfitAmount); + summaryVO.setAverageMarginRate(averageMarginRate); + summaryVO.setProductCount(uniqueProductIds.size()); + summaryVO.setSalesCount(salesCount); + summaryVO.setProfitableProductCount(profitableProductCount); + summaryVO.setLossProductCount(lossProductCount); + + return summaryVO; + } + + /** + * 获取商品毛利润排名数据 + */ + private List getProductProfitRankingData(ProfitMarginReportQueryDTO queryDTO) { + List rankingList = new ArrayList<>(); + + // 查询销售记录 + // 获取销售明细(使用优化版本) + List salesItems = getSalesItemsOptimized(queryDTO); + + if (CollectionUtils.isEmpty(salesItems)) { + return rankingList; + } + + // 按商品ID分组统计 + Map> productGroups = salesItems.stream() + .collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId)); + + // 获取商品信息 + Set productIds = productGroups.keySet(); + List products = spuMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpSpuEntity::getId, productIds) + ); + Map productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + + // 获取分类信息 + Map categoryMap = getCategoryMap(products); + + // 获取单位信息 + Map unitMap = getUnitMap(salesItems); + + // 计算每个商品的毛利润数据 + int ranking = 1; + for (Map.Entry> entry : productGroups.entrySet()) { + Long productId = entry.getKey(); + List items = entry.getValue(); + + SmErpSpuEntity product = productMap.get(productId); + if (product == null || isPriceAdjustmentProduct(product.getProductName())) { + continue; + } + + // 计算销售数据 + BigDecimal salesAmount = items.stream() + .map(item -> new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + double salesQuantity = items.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0) + .sum(); + + int salesCount = items.size(); + + // 计算成本数据 + // 使用第一个销售记录的包装单位来获取成本价格 + SmErpInventoryRecordsItemEntity firstItem = items.get(0); + BigDecimal costPrice = getActualCostPrice(product, firstItem, unitMap); + BigDecimal costAmount = costPrice.multiply(new BigDecimal(salesQuantity)); + + // 计算毛利润 + BigDecimal profitAmount = salesAmount.subtract(costAmount); + BigDecimal marginRate = salesAmount.compareTo(BigDecimal.ZERO) > 0 ? + profitAmount.divide(salesAmount, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算销售单价 + BigDecimal salesPrice = salesQuantity > 0 ? + salesAmount.divide(new BigDecimal(salesQuantity), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 获取分类信息 + SmErpCategoryEntity category = null; + if (product.getCategoryId() != null) { + category = categoryMap.get(product.getCategoryId()); + } + + // 获取单位信息 + SmErpUnitEntity unit = null; + if (!items.isEmpty() && items.get(0).getUnitId() != null) { + unit = unitMap.get(items.get(0).getUnitId()); + } + + // 判断利润状态 + String profitStatus; + if (profitAmount.compareTo(BigDecimal.ZERO) > 0) { + profitStatus = "profitable"; + } else if (profitAmount.compareTo(BigDecimal.ZERO) < 0) { + profitStatus = "loss"; + } else { + profitStatus = "break_even"; + } + + ProfitMarginReportVO.ProductProfitVO profitVO = new ProfitMarginReportVO.ProductProfitVO(); + profitVO.setRanking(ranking++); + profitVO.setProductId(productId); + profitVO.setProductName(product.getProductName()); + profitVO.setProductCode(product.getProductCode()); + profitVO.setCategoryId(product.getCategoryId()); + profitVO.setCategoryName(category != null ? category.getName() : null); + profitVO.setSalesQuantity(salesQuantity); + profitVO.setSalesPrice(salesPrice); + profitVO.setCostPrice(costPrice); + profitVO.setSalesAmount(salesAmount); + profitVO.setCostAmount(costAmount); + profitVO.setProfitAmount(profitAmount); + profitVO.setMarginRate(marginRate); + profitVO.setSalesCount(salesCount); + profitVO.setProfitStatus(profitStatus); + profitVO.setUnit(unit != null ? unit.getUnit() : null); + profitVO.setUnitName(unit != null ? unit.getUnit() : null); + + rankingList.add(profitVO); + } + + // 应用筛选条件 + rankingList = applyProductFilters(rankingList, queryDTO); + + // 排序 + String sortField = queryDTO.getSortField(); + String sortOrder = queryDTO.getSortOrder(); + + if (StrUtil.isNotBlank(sortField)) { + switch (sortField) { + case "profit_amount": + rankingList.sort((a, b) -> "desc".equals(sortOrder) ? + b.getProfitAmount().compareTo(a.getProfitAmount()) : + a.getProfitAmount().compareTo(b.getProfitAmount())); + break; + case "margin_rate": + rankingList.sort((a, b) -> "desc".equals(sortOrder) ? + b.getMarginRate().compareTo(a.getMarginRate()) : + a.getMarginRate().compareTo(b.getMarginRate())); + break; + case "sales_amount": + rankingList.sort((a, b) -> "desc".equals(sortOrder) ? + b.getSalesAmount().compareTo(a.getSalesAmount()) : + a.getSalesAmount().compareTo(b.getSalesAmount())); + break; + default: + // 默认按毛利润降序排序 + rankingList.sort((a, b) -> b.getProfitAmount().compareTo(a.getProfitAmount())); + break; + } + } else { + // 默认按毛利润降序排序 + rankingList.sort((a, b) -> b.getProfitAmount().compareTo(a.getProfitAmount())); + } + + // 重新设置排名 + for (int i = 0; i < rankingList.size(); i++) { + rankingList.get(i).setRanking(i + 1); + } + + // 限制返回数量 + Integer topLimit = queryDTO.getTopLimit(); + if (topLimit != null && topLimit > 0) { + rankingList = rankingList.stream().limit(topLimit).collect(Collectors.toList()); + } + + return rankingList; + } + + /** + * 获取分类毛利润分析数据 + */ + private List getCategoryProfitAnalysisData(ProfitMarginReportQueryDTO queryDTO) { + List categoryList = new ArrayList<>(); + + // 获取商品毛利润数据 + List productProfits = getProductProfitRankingData(queryDTO); + + if (CollectionUtils.isEmpty(productProfits)) { + return categoryList; + } + + // 按分类分组统计 + Map> categoryGroups = productProfits.stream() + .filter(profit -> profit.getCategoryId() != null) + .collect(Collectors.groupingBy(ProfitMarginReportVO.ProductProfitVO::getCategoryId)); + + // 获取分类信息 + Set categoryIds = categoryGroups.keySet(); + List categories = categoryMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpCategoryEntity::getId, categoryIds) + ); + Map categoryMap = categories.stream() + .collect(Collectors.toMap(SmErpCategoryEntity::getId, category -> category)); + + // 计算每个分类的毛利润数据 + for (Map.Entry> entry : categoryGroups.entrySet()) { + Long categoryId = entry.getKey(); + List categoryProducts = entry.getValue(); + + SmErpCategoryEntity category = categoryMap.get(categoryId); + if (category == null) continue; + + // 计算分类汇总数据 + BigDecimal totalSalesAmount = categoryProducts.stream() + .map(ProfitMarginReportVO.ProductProfitVO::getSalesAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal totalCostAmount = categoryProducts.stream() + .map(ProfitMarginReportVO.ProductProfitVO::getCostAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal totalProfitAmount = categoryProducts.stream() + .map(ProfitMarginReportVO.ProductProfitVO::getProfitAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + double totalSalesQuantity = categoryProducts.stream() + .mapToDouble(ProfitMarginReportVO.ProductProfitVO::getSalesQuantity) + .sum(); + + int totalSalesCount = categoryProducts.stream() + .mapToInt(ProfitMarginReportVO.ProductProfitVO::getSalesCount) + .sum(); + + int profitableProductCount = (int) categoryProducts.stream() + .filter(p -> "profitable".equals(p.getProfitStatus())) + .count(); + + int lossProductCount = (int) categoryProducts.stream() + .filter(p -> "loss".equals(p.getProfitStatus())) + .count(); + + BigDecimal averageMarginRate = totalSalesAmount.compareTo(BigDecimal.ZERO) > 0 ? + totalProfitAmount.divide(totalSalesAmount, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + ProfitMarginReportVO.CategoryProfitVO categoryVO = new ProfitMarginReportVO.CategoryProfitVO(); + categoryVO.setCategoryId(categoryId); + categoryVO.setCategoryName(category.getName()); + categoryVO.setProductCount(categoryProducts.size()); + categoryVO.setTotalSalesQuantity(totalSalesQuantity); + categoryVO.setTotalSalesAmount(totalSalesAmount); + categoryVO.setTotalCostAmount(totalCostAmount); + categoryVO.setTotalProfitAmount(totalProfitAmount); + categoryVO.setAverageMarginRate(averageMarginRate); + categoryVO.setSalesCount(totalSalesCount); + categoryVO.setProfitableProductCount(profitableProductCount); + categoryVO.setLossProductCount(lossProductCount); + + categoryList.add(categoryVO); + } + + // 按毛利润降序排序 + categoryList.sort((a, b) -> b.getTotalProfitAmount().compareTo(a.getTotalProfitAmount())); + + // 限制返回数量 + Integer topLimit = queryDTO.getTopLimit(); + if (topLimit != null && topLimit > 0) { + categoryList = categoryList.stream().limit(topLimit).collect(Collectors.toList()); + } + + return categoryList; + } + + /** + * 获取时段毛利润趋势数据 + */ + private List getTimePeriodProfitTrendData(ProfitMarginReportQueryDTO queryDTO) { + List trendList = new ArrayList<>(); + + String queryType = queryDTO.getQueryType(); + if (StrUtil.isBlank(queryType)) { + queryType = "day"; // 默认按天查询 + } + + switch (queryType) { + case "hour": + trendList = getHourlyProfitTrendData(queryDTO); + break; + case "day": + trendList = getDailyProfitTrendData(queryDTO); + break; + case "month": + trendList = getMonthlyProfitTrendData(queryDTO); + break; + default: + trendList = getDailyProfitTrendData(queryDTO); + break; + } + + return trendList; + } + + /** + * 获取高毛利润商品数据 + */ + private List getHighProfitProductsData(ProfitMarginReportQueryDTO queryDTO) { + List allProducts = getProductProfitRankingData(queryDTO); + + return allProducts.stream() + .filter(product -> "profitable".equals(product.getProfitStatus())) + .filter(product -> product.getMarginRate().compareTo(new BigDecimal("30")) > 0) // 毛利率大于30% + .collect(Collectors.toList()); + } + + /** + * 获取低毛利润/亏损商品数据 + */ + private List getLowProfitProductsData(ProfitMarginReportQueryDTO queryDTO) { + List allProducts = getProductProfitRankingData(queryDTO); + + return allProducts.stream() + .filter(product -> "loss".equals(product.getProfitStatus()) || + (product.getMarginRate().compareTo(new BigDecimal("10")) < 0)) // 毛利率小于10%或亏损 + .collect(Collectors.toList()); + } + + + + /** + * 获取销售记录(高性能版本 + 性能分析) + */ + private List getSalesRecords(ProfitMarginReportQueryDTO queryDTO) { + long startTime = System.currentTimeMillis(); + log.info("=== 开始获取销售记录,性能分析开始 ==="); + + // 如果时间范围超过30天,强制分页查询 + boolean needPagination = false; + if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(queryDTO.getStartDate(), queryDTO.getEndDate()); + if (daysBetween > 30) { + needPagination = true; + log.warn("查询时间范围超过30天({}天),启用分页查询模式", daysBetween); + } + } + + long wrapperStartTime = System.currentTimeMillis(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 固定条件:即时销售类型和出库 + wrapper.eq(SmErpInventoryRecordsEntity::getInventoryType, "5"); // 即时销售 + wrapper.eq(SmErpInventoryRecordsEntity::getOrderType, "2"); // 出库 + + // 时间条件 - 支持多种时间筛选方式 + if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + wrapper.ge(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getStartTime()); + wrapper.le(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getEndTime()); + log.info("毛利润报表查询时间范围: {} 至 {}", queryDTO.getStartTime(), queryDTO.getEndTime()); + } else if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + wrapper.ge(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getStartDate()); + wrapper.le(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getEndDate()); + log.info("毛利润报表查询日期范围: {} 至 {}", queryDTO.getStartDate(), queryDTO.getEndDate()); + } else if (queryDTO.getStartTime() != null) { + wrapper.ge(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getStartTime()); + log.info("毛利润报表查询开始时间: {}", queryDTO.getStartTime()); + } else if (queryDTO.getEndTime() != null) { + wrapper.le(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getEndTime()); + log.info("毛利润报表查询结束时间: {}", queryDTO.getEndTime()); + } else if (queryDTO.getStartDate() != null) { + wrapper.ge(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getStartDate()); + log.info("毛利润报表查询开始日期: {}", queryDTO.getStartDate()); + } else if (queryDTO.getEndDate() != null) { + wrapper.le(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getEndDate()); + log.info("毛利润报表查询结束日期: {}", queryDTO.getEndDate()); + } + + // 其他条件 + if (queryDTO.getSalespersonId() != null) { + wrapper.eq(SmErpInventoryRecordsEntity::getHandlerUserId, queryDTO.getSalespersonId()); + log.info("应用销售员筛选: {}", queryDTO.getSalespersonId()); + } + if (StrUtil.isNotBlank(queryDTO.getWarehouseId())) { + wrapper.eq(SmErpInventoryRecordsEntity::getWarehouseId, queryDTO.getWarehouseId()); + log.info("应用仓库筛选: {}", queryDTO.getWarehouseId()); + } + + // 按创建时间降序排列,确保最新的数据在前面 + wrapper.orderByDesc(SmErpInventoryRecordsEntity::getCreateTime); + + // 实施分页查询 + if (needPagination) { + int offset = (queryDTO.getPageNum() - 1) * queryDTO.getPageSize(); + wrapper.last("LIMIT " + queryDTO.getPageSize() + " OFFSET " + offset); + log.info("启用分页查询: 页码={}, 每页大小={}, 偏移量={}", + queryDTO.getPageNum(), queryDTO.getPageSize(), offset); + } else { + // 临时移除限制,用于调试 + log.info("调试模式:不应用LIMIT限制"); + } + + long wrapperEndTime = System.currentTimeMillis(); + log.info("构建查询条件耗时: {}ms", wrapperEndTime - wrapperStartTime); + + long queryStartTime = System.currentTimeMillis(); + List records = inventoryRecordsMapper.selectListByScope(wrapper, DataScope.of()); + long queryEndTime = System.currentTimeMillis(); + log.info("销售记录SQL执行耗时: {}ms", queryEndTime - queryStartTime); + log.info("毛利润报表查询到销售记录数量: {}", records.size()); + + long totalTime = System.currentTimeMillis() - startTime; + log.info("=== 获取销售记录总耗时: {}ms ===", totalTime); + + return records; + } + + /** + * 获取销售明细(直接查询版本,修复筛选问题) + */ + private List getSalesItemsOptimized(ProfitMarginReportQueryDTO queryDTO) { + // 如果有商品筛选条件,先筛选商品ID + Set filteredProductIds = null; + if (StrUtil.isNotBlank(queryDTO.getProductName()) || + StrUtil.isNotBlank(queryDTO.getProductCode()) || + queryDTO.getCategoryId() != null) { + + filteredProductIds = getFilteredProductIds(queryDTO); + if (filteredProductIds.isEmpty()) { + return new ArrayList<>(); + } + } + + // 直接查询销售明细,通过关联查询应用时间筛选 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 通过EXISTS子查询应用时间筛选条件 + if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + wrapper.exists("SELECT 1 FROM sm_erp_inventory_records r WHERE r.id = sm_erp_inventory_records_item.inventory_id " + + "AND r.inventory_type = '5' AND r.order_type = '2' " + + "AND r.inventory_date >= '" + queryDTO.getStartDate() + "' " + + "AND r.inventory_date <= '" + queryDTO.getEndDate() + "'"); + } else if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + wrapper.exists("SELECT 1 FROM sm_erp_inventory_records r WHERE r.id = sm_erp_inventory_records_item.inventory_id " + + "AND r.inventory_type = '5' AND r.order_type = '2' " + + "AND r.create_time >= '" + queryDTO.getStartTime() + "' " + + "AND r.create_time <= '" + queryDTO.getEndTime() + "'"); + } else { + // 默认查询即时销售出库记录 + wrapper.exists("SELECT 1 FROM sm_erp_inventory_records r WHERE r.id = sm_erp_inventory_records_item.inventory_id " + + "AND r.inventory_type = '5' AND r.order_type = '2'"); + } + + // 如果有商品筛选条件,应用商品ID筛选 + if (filteredProductIds != null) { + wrapper.in(SmErpInventoryRecordsItemEntity::getErpSpuId, filteredProductIds); + } + + // 限制查询数量 + wrapper.last("LIMIT 10000"); + + List items = inventoryRecordsItemMapper.selectList(wrapper); + log.info("毛利润报表查询到销售明细数量: {}", items.size()); + + return items; + } + + /** + * 获取筛选后的商品ID集合(性能分析版本) + */ + private Set getFilteredProductIds(ProfitMarginReportQueryDTO queryDTO) { + long startTime = System.currentTimeMillis(); + log.info("=== 开始商品筛选,查询条件: 商品名称={}, 商品编码={}, 分类ID={} ===", + queryDTO.getProductName(), queryDTO.getProductCode(), queryDTO.getCategoryId()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 商品名称筛选 + if (StrUtil.isNotBlank(queryDTO.getProductName())) { + wrapper.like(SmErpSpuEntity::getProductName, queryDTO.getProductName()); + log.info("应用商品名称筛选: {}", queryDTO.getProductName()); + } + + // 商品编码筛选 + if (StrUtil.isNotBlank(queryDTO.getProductCode())) { + wrapper.like(SmErpSpuEntity::getProductCode, queryDTO.getProductCode()); + log.info("应用商品编码筛选: {}", queryDTO.getProductCode()); + } + + // 分类筛选 + if (queryDTO.getCategoryId() != null) { + wrapper.eq(SmErpSpuEntity::getCategoryId, queryDTO.getCategoryId()); + log.info("应用分类ID筛选: {}", queryDTO.getCategoryId()); + } + + long queryStartTime = System.currentTimeMillis(); + List products = spuMapper.selectList(wrapper); + long queryEndTime = System.currentTimeMillis(); + log.info("商品查询SQL执行耗时: {}ms", queryEndTime - queryStartTime); + + Set productIds = products.stream() + .map(SmErpSpuEntity::getId) + .collect(Collectors.toSet()); + + long totalTime = System.currentTimeMillis() - startTime; + log.info("商品筛选总耗时: {}ms, 匹配商品数: {}", totalTime, productIds.size()); + + return productIds; + } + + + + + /** + * 获取分类映射 + */ + private Map getCategoryMap(List products) { + Set categoryIds = products.stream() + .map(SmErpSpuEntity::getCategoryId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (categoryIds.isEmpty()) { + return new HashMap<>(); + } + + List categories = categoryMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpCategoryEntity::getId, categoryIds) + ); + + return categories.stream() + .collect(Collectors.toMap(SmErpCategoryEntity::getId, category -> category)); + } + + + /** + * 应用商品筛选条件 + */ + private List applyProductFilters(List products, ProfitMarginReportQueryDTO queryDTO) { + return products.stream() + .filter(product -> { + // 商品名称筛选 + if (StrUtil.isNotBlank(queryDTO.getProductName())) { + if (StrUtil.isBlank(product.getProductName()) || + !product.getProductName().contains(queryDTO.getProductName())) { + return false; + } + } + + // 商品编码筛选 + if (StrUtil.isNotBlank(queryDTO.getProductCode())) { + if (StrUtil.isBlank(product.getProductCode()) || + !product.getProductCode().contains(queryDTO.getProductCode())) { + return false; + } + } + + // 分类筛选 + if (queryDTO.getCategoryId() != null) { + if (!queryDTO.getCategoryId().equals(product.getCategoryId())) { + return false; + } + } + + // 毛利率范围筛选 + if (queryDTO.getMinMarginRate() != null) { + if (product.getMarginRate().compareTo(queryDTO.getMinMarginRate()) < 0) { + return false; + } + } + if (queryDTO.getMaxMarginRate() != null) { + if (product.getMarginRate().compareTo(queryDTO.getMaxMarginRate()) > 0) { + return false; + } + } + + // 毛利润范围筛选 + if (queryDTO.getMinProfitAmount() != null) { + if (product.getProfitAmount().compareTo(queryDTO.getMinProfitAmount()) < 0) { + return false; + } + } + if (queryDTO.getMaxProfitAmount() != null) { + if (product.getProfitAmount().compareTo(queryDTO.getMaxProfitAmount()) > 0) { + return false; + } + } + + // 只显示盈利商品 + if (Boolean.TRUE.equals(queryDTO.getOnlyProfitable())) { + if (!"profitable".equals(product.getProfitStatus())) { + return false; + } + } + + // 只显示亏损商品 + if (Boolean.TRUE.equals(queryDTO.getOnlyLoss())) { + if (!"loss".equals(product.getProfitStatus())) { + return false; + } + } + + return true; + }) + .collect(Collectors.toList()); + } + + /** + * 获取实际成本价格 + * 优先使用包装单位的实际进价,如果没有则使用商品主表的成本价 + */ + private BigDecimal getActualCostPrice(SmErpSpuEntity product, SmErpInventoryRecordsItemEntity item, Map unitMap) { + BigDecimal costPrice = BigDecimal.ZERO; + + // 1. 优先尝试从包装单位获取实际进价 + if (item.getUnitId() != null) { + SmErpUnitEntity unit = unitMap.get(item.getUnitId()); + if (unit != null && unit.getPurchasePrice() != null) { + // 包装单位的进价通常是分为单位,需要转换为元 + costPrice = new BigDecimal(unit.getPurchasePrice()) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + log.debug("使用包装单位进价: 商品={}, 单位={}, 进价={}分 -> {}元", + product.getProductName(), unit.getUnit(), unit.getPurchasePrice(), costPrice); + return costPrice; + } + } + + // 2. 如果包装单位没有进价,使用商品主表的成本价 + if (product.getCostPrice() != null && product.getCostPrice() > 0) { + // 商品主表的成本价可能是元为单位,需要确认单位 + costPrice = new BigDecimal(product.getCostPrice()).setScale(2, RoundingMode.HALF_UP); + log.debug("使用商品主表成本价: 商品={}, 成本价={}元", product.getProductName(), costPrice); + return costPrice; + } + + // 3. 如果都没有,尝试使用商品主表的进价(分为单位) + if (product.getPurchasePrice() != null && product.getPurchasePrice() > 0) { + costPrice = new BigDecimal(product.getPurchasePrice()) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + log.debug("使用商品主表进价: 商品={}, 进价={}分 -> {}元", + product.getProductName(), product.getPurchasePrice(), costPrice); + return costPrice; + } + + log.warn("无法获取成本价格: 商品={}, 商品ID={}", product.getProductName(), product.getId()); + return BigDecimal.ZERO; + } + + /** + * 批量获取包装单位信息(性能优化) + */ + private Map getUnitMap(List salesItems) { + long startTime = System.currentTimeMillis(); + + Set unitIds = salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getUnitId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (unitIds.isEmpty()) { + log.info("没有需要查询的包装单位ID,返回空Map"); + return new HashMap<>(); + } + + log.info("批量查询包装单位信息,单位ID数量: {}", unitIds.size()); + + List units = unitMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpUnitEntity::getId, unitIds) + ); + + Map unitMap = units.stream() + .collect(Collectors.toMap(SmErpUnitEntity::getId, unit -> unit)); + + long endTime = System.currentTimeMillis(); + log.info("批量查询包装单位信息耗时: {}ms, 查询到单位数量: {}", endTime - startTime, units.size()); + + return unitMap; + } + + /** + * 判断是否为补差价商品 + */ + private boolean isPriceAdjustmentProduct(String productName) { + if (StrUtil.isBlank(productName)) { + return false; + } + // 过滤掉只包含数字(可能包含小数点)的商品名称,如:0.5, 1.0, 10, 100等 + return productName.matches("^[0-9]+\\.?[0-9]*$"); + } + + /** + * 创建空的利润汇总信息 + */ + private ProfitMarginReportVO.ProfitSummaryVO createEmptyProfitSummary() { + ProfitMarginReportVO.ProfitSummaryVO summaryVO = new ProfitMarginReportVO.ProfitSummaryVO(); + summaryVO.setTotalSalesAmount(BigDecimal.ZERO); + summaryVO.setTotalCostAmount(BigDecimal.ZERO); + summaryVO.setTotalProfitAmount(BigDecimal.ZERO); + summaryVO.setAverageMarginRate(BigDecimal.ZERO); + summaryVO.setProductCount(0); + summaryVO.setSalesCount(0); + summaryVO.setProfitableProductCount(0); + summaryVO.setLossProductCount(0); + return summaryVO; + } + + /** + * 构建时间范围描述 + */ + private String buildTimeRange(ProfitMarginReportQueryDTO queryDTO) { + if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + return queryDTO.getStartTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + + " 至 " + queryDTO.getEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + } else if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + return queryDTO.getStartDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + + " 至 " + queryDTO.getEndDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + return "全部时间"; + } + + /** + * 获取小时毛利润趋势数据 + */ + private List getHourlyProfitTrendData(ProfitMarginReportQueryDTO queryDTO) { + // TODO: 实现按小时的毛利润趋势数据 + return new ArrayList<>(); + } + + /** + * 获取日毛利润趋势数据 + */ + private List getDailyProfitTrendData(ProfitMarginReportQueryDTO queryDTO) { + List trendList = new ArrayList<>(); + + // 获取销售明细 + List salesItems = getSalesItemsOptimized(queryDTO); + + if (CollectionUtils.isEmpty(salesItems)) { + return trendList; + } + + // 获取销售记录(用于获取日期信息) + List salesRecords = getSalesRecords(queryDTO); + + // 获取商品信息 + Set productIds = salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .collect(Collectors.toSet()); + + List products = spuMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpSpuEntity::getId, productIds) + ); + Map productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + + // 批量获取包装单位信息(性能优化) + Map unitMap = getUnitMap(salesItems); + + // 按日期分组统计 + Map> dailyGroups = salesItems.stream() + .collect(Collectors.groupingBy(item -> { + // 通过主表获取日期 + SmErpInventoryRecordsEntity mainRecord = salesRecords.stream() + .filter(record -> record.getId().equals(item.getInventoryId())) + .findFirst() + .orElse(null); + return mainRecord != null ? mainRecord.getInventoryDate() : LocalDate.now(); + })); + + // 计算每日的毛利润数据 + for (Map.Entry> entry : dailyGroups.entrySet()) { + LocalDate date = entry.getKey(); + List dayItems = entry.getValue(); + + // 计算销售额 + BigDecimal salesAmount = dayItems.stream() + .map(item -> new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + // 计算成本额 + BigDecimal costAmount = BigDecimal.ZERO; + for (SmErpInventoryRecordsItemEntity item : dayItems) { + SmErpSpuEntity product = productMap.get(item.getErpSpuId()); + if (product != null && !isPriceAdjustmentProduct(product.getProductName())) { + BigDecimal costPrice = getActualCostPrice(product, item, unitMap); + BigDecimal itemCostAmount = costPrice.multiply(new BigDecimal(item.getQuantity() != null ? item.getQuantity() : 0)); + costAmount = costAmount.add(itemCostAmount); + } + } + + // 计算毛利润 + BigDecimal profitAmount = salesAmount.subtract(costAmount); + BigDecimal marginRate = salesAmount.compareTo(BigDecimal.ZERO) > 0 ? + profitAmount.divide(salesAmount, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算其他统计信息 + double salesQuantity = dayItems.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0) + .sum(); + + int salesCount = dayItems.size(); + int productCount = (int) dayItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .distinct() + .count(); + + ProfitMarginReportVO.TimePeriodProfitVO trendVO = new ProfitMarginReportVO.TimePeriodProfitVO(); + trendVO.setTimeLabel(date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + trendVO.setSalesAmount(salesAmount); + trendVO.setCostAmount(costAmount); + trendVO.setProfitAmount(profitAmount); + trendVO.setMarginRate(marginRate); + trendVO.setSalesQuantity(salesQuantity); + trendVO.setSalesCount(salesCount); + trendVO.setProductCount(productCount); + trendVO.setProfitGrowthRate(BigDecimal.ZERO); // 简化处理 + + trendList.add(trendVO); + } + + // 按日期排序 + trendList.sort((a, b) -> a.getTimeLabel().compareTo(b.getTimeLabel())); + + return trendList; + } + + /** + * 获取月毛利润趋势数据 + */ + private List getMonthlyProfitTrendData(ProfitMarginReportQueryDTO queryDTO) { + List trendList = new ArrayList<>(); + + // 获取销售明细 + List salesItems = getSalesItemsOptimized(queryDTO); + + if (CollectionUtils.isEmpty(salesItems)) { + return trendList; + } + + // 获取销售记录(用于获取日期信息) + List salesRecords = getSalesRecords(queryDTO); + + // 获取商品信息 + Set productIds = salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .collect(Collectors.toSet()); + + List products = spuMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpSpuEntity::getId, productIds) + ); + Map productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + + // 批量获取包装单位信息(性能优化) + Map unitMap = getUnitMap(salesItems); + + // 按月份分组统计 + Map> monthlyGroups = salesItems.stream() + .collect(Collectors.groupingBy(item -> { + // 通过主表获取日期 + SmErpInventoryRecordsEntity mainRecord = salesRecords.stream() + .filter(record -> record.getId().equals(item.getInventoryId())) + .findFirst() + .orElse(null); + if (mainRecord != null && mainRecord.getInventoryDate() != null) { + return mainRecord.getInventoryDate().format(DateTimeFormatter.ofPattern("yyyy-MM")); + } + return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); + })); + + // 计算每月的毛利润数据 + for (Map.Entry> entry : monthlyGroups.entrySet()) { + String monthLabel = entry.getKey(); + List monthItems = entry.getValue(); + + // 计算销售额 + BigDecimal salesAmount = monthItems.stream() + .map(item -> new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + // 计算成本额 + BigDecimal costAmount = BigDecimal.ZERO; + for (SmErpInventoryRecordsItemEntity item : monthItems) { + SmErpSpuEntity product = productMap.get(item.getErpSpuId()); + if (product != null && !isPriceAdjustmentProduct(product.getProductName())) { + BigDecimal costPrice = getActualCostPrice(product, item, unitMap); + BigDecimal itemCostAmount = costPrice.multiply(new BigDecimal(item.getQuantity() != null ? item.getQuantity() : 0)); + costAmount = costAmount.add(itemCostAmount); + } + } + + // 计算毛利润 + BigDecimal profitAmount = salesAmount.subtract(costAmount); + BigDecimal marginRate = salesAmount.compareTo(BigDecimal.ZERO) > 0 ? + profitAmount.divide(salesAmount, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算其他统计信息 + double salesQuantity = monthItems.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0) + .sum(); + + int salesCount = monthItems.size(); + int productCount = (int) monthItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .distinct() + .count(); + + ProfitMarginReportVO.TimePeriodProfitVO trendVO = new ProfitMarginReportVO.TimePeriodProfitVO(); + trendVO.setTimeLabel(monthLabel); + trendVO.setSalesAmount(salesAmount); + trendVO.setCostAmount(costAmount); + trendVO.setProfitAmount(profitAmount); + trendVO.setMarginRate(marginRate); + trendVO.setSalesQuantity(salesQuantity); + trendVO.setSalesCount(salesCount); + trendVO.setProductCount(productCount); + trendVO.setProfitGrowthRate(BigDecimal.ZERO); // 简化处理 + + trendList.add(trendVO); + } + + // 按月份排序 + trendList.sort((a, b) -> a.getTimeLabel().compareTo(b.getTimeLabel())); + + return trendList; + } +}