gongchunyi
13 小时以前 cdf97cbb3d3ccdd34b04296b920e42eab03f29c1
feat: BI大屏接口
已添加6个文件
已修改3个文件
666 ■■■■■ 文件已修改
src/main/java/com/ruoyi/home/controller/HomeController.java 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/CustomerContributionRankingDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/CustomerRevenueAnalysisDto.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/DeptStaffDistributionDto.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/HomeSummaryDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/ProductCategoryDistributionDto.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/SupplierPurchaseRankingDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/HomeService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 445 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -1,7 +1,5 @@
package com.ruoyi.home.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
@@ -9,9 +7,6 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.home.dto.*;
import com.ruoyi.home.service.HomeService;
import com.ruoyi.production.dto.ProductOrderDto;
import com.ruoyi.production.dto.ProductWorkOrderDto;
import com.ruoyi.production.dto.SalesLedgerWorkDto;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -59,6 +54,47 @@
        return AjaxResult.success(count);
    }
    @GetMapping("/deptStaffDistribution")
    @ApiOperation("各部门人员分布")
    public AjaxResult deptStaffDistribution() {
        DeptStaffDistributionDto dto = homeService.deptStaffDistribution();
        return AjaxResult.success(dto);
    }
    @GetMapping("/summaryStatistics")
    @ApiOperation("员工-客户-供应商总数")
    public AjaxResult summaryStatistics() {
        HomeSummaryDto homeSummaryDto = homeService.summaryStatistics();
        return AjaxResult.success(homeSummaryDto);
    }
    @GetMapping("/supplierPurchaseRanking")
    @ApiOperation("供应商采购排名")
    public AjaxResult supplierPurchaseRanking(@RequestParam(value = "type", defaultValue = "0") Integer type) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type);
        return AjaxResult.success(list);
    }
    @GetMapping("/customerRevenueAnalysis")
    @ApiOperation("客户营收贡献数值分析")
    public AjaxResult customerRevenueAnalysis(@RequestParam("customerId") Long customerId, @RequestParam(value = "type", defaultValue = "0") Integer type) {
        CustomerRevenueAnalysisDto dto = homeService.customerRevenueAnalysis(customerId, type);
        return AjaxResult.success(dto);
    }
    @GetMapping("/productCategoryDistribution")
    @ApiOperation("产品大类分布")
    public AjaxResult productCategoryDistribution() {
        ProductCategoryDistributionDto dto = homeService.productCategoryDistribution();
        return AjaxResult.success(dto);
    }
    @GetMapping("/customerContributionRanking")
    @ApiOperation("客户金额贡献排名")
    public AjaxResult customerContributionRanking(@RequestParam(value = "type", defaultValue = "1") Integer type) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type);
        return AjaxResult.success(list);
    }
    /********************************************************营销采购类**************************************************/
    @GetMapping("/business")
src/main/java/com/ruoyi/home/dto/CustomerContributionRankingDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.home.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
 * å®¢æˆ·é‡‘额贡献排名DTO
 */
@Data
@ApiModel("客户金额贡献排名")
public class CustomerContributionRankingDto {
    @ApiModelProperty("客户名称")
    private String customerName;
    @ApiModelProperty("合同总金额")
    private BigDecimal totalAmount;
}
src/main/java/com/ruoyi/home/dto/CustomerRevenueAnalysisDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.home.dto;
import com.ruoyi.dto.MapDto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * å®¢æˆ·è¥æ”¶è´¡çŒ®æ•°å€¼åˆ†æžDTO
 */
@Data
@ApiModel("客户营收贡献数值分析")
public class CustomerRevenueAnalysisDto {
    @ApiModelProperty("分析条目列表")
    private List<MapDto> items;
}
src/main/java/com/ruoyi/home/dto/DeptStaffDistributionDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.home.dto;
import com.ruoyi.dto.MapDto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * éƒ¨é—¨äººå‘˜åˆ†å¸ƒç»Ÿè®¡DTO
 */
@Data
@ApiModel("部门人员分布统计")
public class DeptStaffDistributionDto {
    @ApiModelProperty("部门总人数")
    private Long total;
    @ApiModelProperty("部门分布列表")
    private List<MapDto> items;
}
src/main/java/com/ruoyi/home/dto/HomeSummaryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.home.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * é¦–页汇总统计DTO
 */
@Data
@ApiModel("首页汇总统计")
public class HomeSummaryDto {
    @ApiModelProperty("总工作人员")
    private Long totalStaff;
    @ApiModelProperty("总工作人员同比增长率")
    private String staffGrowthRate;
    @ApiModelProperty("总客户数")
    private Long totalCustomer;
    @ApiModelProperty("总客户同比增长率")
    private String customerGrowthRate;
    @ApiModelProperty("总供应商数")
    private Long totalSupplier;
    @ApiModelProperty("总供应商同比增长率")
    private String supplierGrowthRate;
}
src/main/java/com/ruoyi/home/dto/ProductCategoryDistributionDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.home.dto;
import com.ruoyi.dto.MapDto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * äº§å“å¤§ç±»åˆ†å¸ƒç»Ÿè®¡DTO
 */
@Data
@ApiModel("产品大类分布统计")
public class ProductCategoryDistributionDto {
    @ApiModelProperty("大类分布列表")
    private List<MajorCategoryDto> items;
    @Data
    public static class MajorCategoryDto {
        @ApiModelProperty("大类名称")
        private String name;
        @ApiModelProperty("库存总数")
        private String value;
        @ApiModelProperty("占比")
        private String rate;
        @ApiModelProperty("小类分布详情")
        private List<MinorCategoryDto> children;
    }
    @Data
    public static class MinorCategoryDto {
        @ApiModelProperty("小类名称")
        private String name;
        @ApiModelProperty("库存数量")
        private String value;
        @ApiModelProperty("占比")
        private String rate;
        @ApiModelProperty("型号分布详情")
        private List<MapDto> children;
    }
}
src/main/java/com/ruoyi/home/dto/SupplierPurchaseRankingDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.home.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
 * ä¾›åº”商采购排名DTO
 */
@Data
@ApiModel("供应商采购排名")
public class SupplierPurchaseRankingDto {
    @ApiModelProperty("供应商名称")
    private String supplierName;
    @ApiModelProperty("采购总金额")
    private BigDecimal totalAmount;
}
src/main/java/com/ruoyi/home/service/HomeService.java
@@ -33,4 +33,16 @@
    ProductionProgressDto productionProgress();
    ProductionTurnoverDto workInProcessTurnover();
    DeptStaffDistributionDto deptStaffDistribution();
    HomeSummaryDto summaryStatistics();
    List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type);
    CustomerRevenueAnalysisDto customerRevenueAnalysis(Long customerId, Integer type);
    ProductCategoryDistributionDto productCategoryDistribution();
    List<CustomerContributionRankingDto> customerContributionRanking(Integer type);
}
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -1,45 +1,40 @@
package com.ruoyi.home.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.collaborativeApproval.mapper.NoticeMapper;
import com.ruoyi.collaborativeApproval.pojo.Notice;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.dto.MapDto;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.home.dto.*;
import com.ruoyi.home.service.HomeService;
import com.ruoyi.lavorissue.mapper.LavorIssueMapper;
import com.ruoyi.lavorissue.pojo.LaborIssue;
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.dto.ProductOrderDto;
import com.ruoyi.production.dto.ProductWorkOrderDto;
import com.ruoyi.production.dto.ProductionProductMainDto;
import com.ruoyi.production.mapper.ProductOrderMapper;
import com.ruoyi.production.mapper.ProductProcessMapper;
import com.ruoyi.production.mapper.ProductWorkOrderMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductOrder;
import com.ruoyi.production.pojo.ProductProcess;
import com.ruoyi.production.pojo.ProductWorkOrder;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUserDept;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PaymentRegistration;
@@ -49,9 +44,13 @@
import com.ruoyi.sales.mapper.ReceiptPaymentMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.sales.pojo.ReceiptPayment;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.staff.mapper.StaffOnJobMapper;
import com.ruoyi.staff.pojo.StaffOnJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -60,13 +59,11 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
@@ -87,13 +84,7 @@
    private SalesLedgerProductMapper salesLedgerProductMapper;
    @Autowired
    private ProcurementRecordOutMapper procurementRecordOutMapper;
    @Autowired
    private ProcurementRecordMapper procurementRecordStorageMapper;
    @Autowired
    private CustomStorageMapper customStorageMapper;
    @Autowired
    private QualityInspectMapper qualityStatisticsMapper;
@@ -108,9 +99,6 @@
    private PaymentRegistrationMapper paymentRegistrationMapper;
    @Autowired
    private LavorIssueMapper lavorIssueMapper;
    @Autowired
    private SysDeptMapper sysDeptMapper;
    @Autowired
@@ -118,13 +106,24 @@
    @Autowired
    private ProductOrderMapper productOrderMapper;
    @Autowired
    private ProductProcessMapper productProcessMapper;
    @Autowired
    private ProductWorkOrderMapper productWorkOrderMapper;
    @Autowired
    private ProductModelMapper productModelMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private StockUtils stockUtils;
    @Autowired
    private StaffOnJobMapper staffOnJobMapper;
    @Autowired
    private CustomerMapper customerMapper;
    @Autowired
    private SupplierManageMapper supplierManageMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysUserDeptMapper sysUserDeptMapper;
    @Override
    public HomeBusinessDto business() {
        // æž„建结果
@@ -581,6 +580,398 @@
        productionTurnoverDto.setProcessDetails(strings);
        productionTurnoverDto.setProcessQuantityDetails(processQuantityDetails);
        return productionTurnoverDto;
    }
    @Override
    public DeptStaffDistributionDto deptStaffDistribution() {
        DeptStaffDistributionDto dto = new DeptStaffDistributionDto();
        List<MapDto> items = new ArrayList<>();
        // æŸ¥è¯¢æ‰€æœ‰æ­£å¸¸ä¸”未删除的部门
        List<SysDept> depts = sysDeptMapper.selectDeptList(new SysDept());
        if (CollectionUtils.isEmpty(depts)) {
            dto.setItems(items);
            return dto;
        }
        long totalUsers = 0;
        List<Map<String, Object>> countsByDept = new ArrayList<>();
        for (SysDept dept : depts) {
            if ("0".equals(dept.getStatus()) && "0".equals(dept.getDelFlag())) {
                Long count = sysUserDeptMapper.selectCount(new LambdaQueryWrapper<SysUserDept>()
                        .eq(SysUserDept::getDeptId, dept.getDeptId()));
                if (count > 0) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("name", dept.getDeptName());
                    map.put("count", count);
                    countsByDept.add(map);
                    totalUsers += count;
                }
            }
        }
        if (totalUsers > 0) {
            BigDecimal total = BigDecimal.valueOf(totalUsers);
            for (Map<String, Object> map : countsByDept) {
                MapDto mapDto = new MapDto();
                mapDto.setName((String) map.get("name"));
                Long count = (Long) map.get("count");
                mapDto.setValue(count.toString());
                mapDto.setRate(BigDecimal.valueOf(count).multiply(new BigDecimal("100"))
                        .divide(total, 2, RoundingMode.HALF_UP).toString());
                items.add(mapDto);
            }
        }
        dto.setTotal(totalUsers);
        dto.setItems(items);
        return dto;
    }
    @Override
    public HomeSummaryDto summaryStatistics() {
        HomeSummaryDto dto = new HomeSummaryDto();
        LocalDate now = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(now);
        YearMonth prevMonth = currentMonth.minusMonths(1);
        LocalDateTime currentMonthEnd = currentMonth.atEndOfMonth().atTime(23, 59, 59);
        LocalDateTime prevMonthEnd = prevMonth.atEndOfMonth().atTime(23, 59, 59);
        //  æ€»å·¥ä½œäººå‘˜
        Long currentStaff = countStaff(currentMonthEnd);
        Long prevStaff = countStaff(prevMonthEnd);
        dto.setTotalStaff(currentStaff);
        dto.setStaffGrowthRate(calculateMoM(currentStaff, prevStaff));
        //  æ€»å®¢æˆ·æ•°
        Long currentCustomers = countCustomers(currentMonthEnd);
        Long prevCustomers = countCustomers(prevMonthEnd);
        dto.setTotalCustomer(currentCustomers);
        dto.setCustomerGrowthRate(calculateMoM(currentCustomers, prevCustomers));
        //  æ€»ä¾›åº”商数
        Long currentSuppliers = countSuppliers(currentMonthEnd);
        Long prevSuppliers = countSuppliers(prevMonthEnd);
        dto.setTotalSupplier(currentSuppliers);
        dto.setSupplierGrowthRate(calculateMoM(currentSuppliers, prevSuppliers));
        return dto;
    }
    private Long countStaff(LocalDateTime dateTime) {
        Long sysUserCount = sysUserMapper.selectCount(new LambdaQueryWrapper<SysUser>()
                .eq(SysUser::getDelFlag, "0")
                .le(SysUser::getCreateTime, dateTime));
        Long staffCountItem = staffOnJobMapper.selectCount(new LambdaQueryWrapper<StaffOnJob>()
                .le(StaffOnJob::getCreateTime, dateTime));
        return sysUserCount + staffCountItem;
    }
    private Long countCustomers(LocalDateTime dateTime) {
        return customerMapper.selectCount(new LambdaQueryWrapper<Customer>()
                .le(Customer::getMaintenanceTime, dateTime.toLocalDate()));
    }
    private Long countSuppliers(LocalDateTime dateTime) {
        return supplierManageMapper.selectCount(new LambdaQueryWrapper<SupplierManage>()
                .le(SupplierManage::getCreateTime, dateTime));
    }
    private String calculateMoM(Number current, Number prev) {
        BigDecimal curVal = new BigDecimal(current.toString());
        BigDecimal prevVal = new BigDecimal(prev.toString());
        if (prevVal.compareTo(BigDecimal.ZERO) == 0) {
            return curVal.compareTo(BigDecimal.ZERO) > 0 ? "100.00" : "0.00";
        }
        return curVal.subtract(prevVal)
                .divide(prevVal, 4, RoundingMode.HALF_UP)
                .multiply(new BigDecimal("100"))
                .setScale(2, RoundingMode.HALF_UP)
                .toString();
    }
    @Override
    public List<SupplierPurchaseRankingDto> supplierPurchaseRanking(Integer type) {
        LocalDate today = LocalDate.now();
        LocalDate startDate;
        LocalDate endDate;
        switch (type) {
            case 0: // å‘¨
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 1: // æœˆ
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 2: // å­£åº¦
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                break;
            default:
                return new ArrayList<>();
        }
        QueryWrapper<PurchaseLedger> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("supplier_name", "SUM(contract_amount) as total_amount")
                .ge("entry_date", startDate)
                .le("entry_date", endDate)
//                .ne("approval_status", 3)
                .groupBy("supplier_name")
                .orderByDesc("total_amount")
                .last("LIMIT 5");
        List<Map<String, Object>> maps = purchaseLedgerMapper.selectMaps(queryWrapper);
        return maps.stream().map(map -> {
            SupplierPurchaseRankingDto dto = new SupplierPurchaseRankingDto();
            dto.setSupplierName(map.get("supplier_name") != null ? map.get("supplier_name").toString() : "");
            Object amount = map.get("total_amount");
            dto.setTotalAmount(amount != null ? new BigDecimal(amount.toString()) : BigDecimal.ZERO);
            return dto;
        }).collect(Collectors.toList());
    }
    @Override
    public CustomerRevenueAnalysisDto customerRevenueAnalysis(Long customerId, Integer type) {
        CustomerRevenueAnalysisDto dto = new CustomerRevenueAnalysisDto();
        List<MapDto> items = new ArrayList<>();
        LocalDate today = LocalDate.now();
        LocalDate start;
        LocalDate end;
        boolean groupByMonth = false;
        switch (type) {
            case 0: // å‘¨
                start = today.with(DayOfWeek.MONDAY);
                end = today.with(DayOfWeek.SUNDAY);
                break;
            case 1: // æœˆ
                start = today.with(TemporalAdjusters.firstDayOfMonth());
                end = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 2: // å­£åº¦
                Month firstMonthOfQuarter = today.getMonth().firstMonthOfQuarter();
                start = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                end = today.withMonth(firstMonthOfQuarter.plus(2).getValue()).with(TemporalAdjusters.lastDayOfMonth());
                groupByMonth = true;
                break;
            default:
                dto.setItems(items);
                return dto;
        }
        List<SalesLedger> list = salesLedgerMapper.selectList(new LambdaQueryWrapper<SalesLedger>()
                .eq(SalesLedger::getCustomerId, customerId)
                .ge(SalesLedger::getEntryDate, start)
                .le(SalesLedger::getEntryDate, end));
        if (groupByMonth) {
            for (int i = 0; i < 3; i++) {
                LocalDate m = start.plusMonths(i);
                String monthName = m.getMonthValue() + "月";
                BigDecimal sum = list.stream()
                        .filter(l -> l.getEntryDate() != null)
                        .filter(l -> {
                            LocalDate ld = l.getEntryDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
                            return ld.getMonth() == m.getMonth() && ld.getYear() == m.getYear();
                        })
                        .map(SalesLedger::getContractAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                MapDto mapDto = new MapDto();
                mapDto.setName(monthName);
                mapDto.setValue(sum.setScale(2, RoundingMode.HALF_UP).toString());
                items.add(mapDto);
            }
        } else {
            long days = ChronoUnit.DAYS.between(start, end) + 1;
            for (int i = 0; i < days; i++) {
                LocalDate d = start.plusDays(i);
                String dayName = d.getMonthValue() + "/" + d.getDayOfMonth();
                BigDecimal sum = list.stream()
                        .filter(l -> l.getEntryDate() != null)
                        .filter(l -> l.getEntryDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(d))
                        .map(SalesLedger::getContractAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                MapDto mapDto = new MapDto();
                mapDto.setName(dayName);
                mapDto.setValue(sum.setScale(2, RoundingMode.HALF_UP).toString());
                items.add(mapDto);
            }
        }
        dto.setItems(items);
        return dto;
    }
    @Override
    public ProductCategoryDistributionDto productCategoryDistribution() {
        ProductCategoryDistributionDto dto = new ProductCategoryDistributionDto();
        List<ProductCategoryDistributionDto.MajorCategoryDto> majorItems = new ArrayList<>();
        //  æ‰€æœ‰çš„产品大类和小类
        List<Product> allProducts = productMapper.selectList(new LambdaQueryWrapper<Product>());
        if (CollectionUtils.isEmpty(allProducts)) {
            dto.setItems(majorItems);
            return dto;
        }
        List<Product> majorCategories = allProducts.stream().filter(p -> p.getParentId() == null).collect(Collectors.toList());
        List<Product> minorCategories = allProducts.stream().filter(p -> p.getParentId() != null).collect(Collectors.toList());
        //  ä»Ž sales_ledger_product æ‹¿åˆ°æ¯ä¸ªäº§å“çš„型号库存
        // éœ€åŒ…含规格型号以支持三级分布
        List<Map<String, Object>> quantityMaps = salesLedgerProductMapper.selectMaps(new QueryWrapper<SalesLedgerProduct>()
                .select("product_id", "specification_model", "type", "SUM(quantity) as sum_qty")
                .isNotNull("product_id")
                .groupBy("product_id", "specification_model", "type"));
        Map<Long, Map<String, Map<Integer, BigDecimal>>> modelStockGroups = new HashMap<>();
        for (Map<String, Object> map : quantityMaps) {
            Long productId = Long.parseLong(map.get("product_id").toString());
            String model = map.get("specification_model") != null ? map.get("specification_model").toString() : "未知型号";
            Integer type = Integer.parseInt(map.get("type").toString());
            BigDecimal sum = map.get("sum_qty") != null ? new BigDecimal(map.get("sum_qty").toString()) : BigDecimal.ZERO;
            modelStockGroups.computeIfAbsent(productId, k -> new HashMap<>())
                    .computeIfAbsent(model, k -> new HashMap<>())
                    .put(type, sum);
        }
        //  åº“存并汇总
        Map<Long, List<MapDto>> productModelsMap = new HashMap<>();
        Map<Long, BigDecimal> productTotalStockMap = new HashMap<>();
        for (Long pid : modelStockGroups.keySet()) {
            Map<String, Map<Integer, BigDecimal>> models = modelStockGroups.get(pid);
            BigDecimal productStock = BigDecimal.ZERO;
            List<MapDto> modelDtos = new ArrayList<>();
            for (String modelName : models.keySet()) {
                Map<Integer, BigDecimal> types = models.get(modelName);
                BigDecimal procurement = types.getOrDefault(2, BigDecimal.ZERO);
                BigDecimal sales = types.getOrDefault(1, BigDecimal.ZERO);
                BigDecimal stock = procurement.subtract(sales);
                if (stock.compareTo(BigDecimal.ZERO) < 0) stock = BigDecimal.ZERO;
                MapDto modelDto = new MapDto();
                modelDto.setName(modelName);
                modelDto.setValue(stock.stripTrailingZeros().toPlainString());
                modelDtos.add(modelDto);
                productStock = productStock.add(stock);
            }
            productModelsMap.put(pid, modelDtos);
            productTotalStockMap.put(pid, productStock);
        }
        BigDecimal totalInventory = productTotalStockMap.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        //  åž‹å·å æ¯”
        if (totalInventory.compareTo(BigDecimal.ZERO) > 0) {
            for (List<MapDto> dtos : productModelsMap.values()) {
                for (MapDto m : dtos) {
                    BigDecimal val = new BigDecimal(m.getValue());
                    m.setRate(val.multiply(new BigDecimal("100"))
                            .divide(totalInventory, 2, RoundingMode.HALF_UP).toString());
                }
            }
        }
        //  åˆ†å±‚数据
        for (Product major : majorCategories) {
            ProductCategoryDistributionDto.MajorCategoryDto majorDto = new ProductCategoryDistributionDto.MajorCategoryDto();
            majorDto.setName(major.getProductName());
            List<ProductCategoryDistributionDto.MinorCategoryDto> minorDtos = new ArrayList<>();
            BigDecimal majorStock = BigDecimal.ZERO;
            for (Product minor : minorCategories) {
                if (major.getId().equals(minor.getParentId())) {
                    BigDecimal stock = productTotalStockMap.getOrDefault(minor.getId(), BigDecimal.ZERO);
                    ProductCategoryDistributionDto.MinorCategoryDto minorDto = new ProductCategoryDistributionDto.MinorCategoryDto();
                    minorDto.setName(minor.getProductName());
                    minorDto.setValue(stock.stripTrailingZeros().toPlainString());
                    if (totalInventory.compareTo(BigDecimal.ZERO) > 0) {
                        minorDto.setRate(stock.multiply(new BigDecimal("100"))
                                .divide(totalInventory, 2, RoundingMode.HALF_UP).toString());
                    } else {
                        minorDto.setRate("0.00");
                    }
                    minorDto.setChildren(productModelsMap.getOrDefault(minor.getId(), new ArrayList<>()));
                    minorDtos.add(minorDto);
                    majorStock = majorStock.add(stock);
                }
            }
            majorDto.setValue(majorStock.stripTrailingZeros().toPlainString());
            if (totalInventory.compareTo(BigDecimal.ZERO) > 0) {
                majorDto.setRate(majorStock.multiply(new BigDecimal("100"))
                        .divide(totalInventory, 2, RoundingMode.HALF_UP).toString());
            } else {
                majorDto.setRate("0.00");
            }
            majorDto.setChildren(minorDtos);
            majorItems.add(majorDto);
        }
        dto.setItems(majorItems);
        return dto;
    }
    @Override
    public List<CustomerContributionRankingDto> customerContributionRanking(Integer type) {
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
        switch (type) {
            case 0:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 1:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 2:
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
                startDate = today.withMonth(firstMonthOfQuarter.getValue()).with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue()).with(TemporalAdjusters.lastDayOfMonth());
                break;
        }
        QueryWrapper<SalesLedger> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("customer_name", "SUM(contract_amount) as total_amount")
                .isNotNull("customer_name")
                .groupBy("customer_name")
                .orderByDesc("total_amount")
                .last("LIMIT 5");
        if (startDate != null && endDate != null) {
            queryWrapper.between("entry_date", startDate, endDate);
        }
        List<Map<String, Object>> maps = salesLedgerMapper.selectMaps(queryWrapper);
        List<CustomerContributionRankingDto> result = new ArrayList<>();
        for (Map<String, Object> map : maps) {
            CustomerContributionRankingDto rankingDto = new CustomerContributionRankingDto();
            rankingDto.setCustomerName(map.get("customer_name").toString());
            rankingDto.setTotalAmount(map.get("total_amount") != null ? new BigDecimal(map.get("total_amount").toString()) : BigDecimal.ZERO);
            result.add(rankingDto);
        }
        return result;
    }
}