5 天以前 1fccd0a7eb0b06e757f677de6279edab0bc81236
refactor(home): 重构首页财务统计功能实现

- 替换原有的占位逻辑为真实的财务数据计算方式
- 新增AccountStatementMapper依赖用于查询对账单数据
- 实现应收应付统计、月度收入、月度支出的真实财务口径计算
- 添加财务数据范围解析和金额汇总工具方法
- 创建财务统计相关的辅助计算函数
- 更新数据查询逻辑以支持新的财务统计规则
- 移除旧的临时计算代码并优化数据处理流程
已添加1个文件
已修改1个文件
392 ■■■■■ 文件已修改
doc/20260522_首页财务接口升级前端变更文档.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_Ê×Ò³²ÆÎñ½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# é¦–页财务接口升级前端变更文档
更新时间:2026-05-22
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,接口 URL、请求参数、返回字段保持不变。
主要变更是首页财务数据从占位/半成品逻辑,切换为按财务真实数据口径计算。
涉及接口:
1. `GET /home/statisticsReceivablePayable`
2. `GET /home/monthlyIncome`
3. `GET /home/monthlyExpenditure`
## 2. å‚数说明(无新增)
### 2.1 `GET /home/statisticsReceivablePayable`
- `type`:`1` æœ¬å‘¨ï¼Œ`2` æœ¬æœˆï¼Œ`3` æœ¬å­£åº¦ï¼ˆé»˜è®¤ `1`)
### 2.2 `GET /home/monthlyIncome`
- æ— å‚æ•°
### 2.3 `GET /home/monthlyExpenditure`
- æ— å‚æ•°
## 3. è¿”回字段口径变更
### 3.1 åº”收应付统计 `statisticsReceivablePayable`
返回字段不变:
- `receivableMoney`
- `payableMoney`
- `advanceMoney`
- `prepayMoney`
新口径:
- `receivableMoney = max(销售合同金额合计 - æ”¶æ¬¾é‡‘额合计, 0)`
- `payableMoney = max(采购合同金额合计 - ä»˜æ¬¾é‡‘额合计, 0)`
- `advanceMoney = æ”¶æ¬¾é‡‘额合计`
- `prepayMoney = ä»˜æ¬¾é‡‘额合计`
以上金额均按 `type` å¯¹åº”时间范围统计,保留两位小数。
返回示例:
```json
{
  "receivableMoney": 128000.00,
  "payableMoney": 76000.00,
  "advanceMoney": 42000.00,
  "prepayMoney": 31000.00
}
```
### 3.2 æœˆåº¦æ”¶å…¥ `monthlyIncome`
返回字段不变:
- `monthlyIncome`
- `collectionRate`
- `overdueNum`
- `overdueRate`
新口径:
- `monthlyIncome`:当月收款合计
- `collectionRate`:`当月收款合计 / å½“月销售合同金额合计 * 100`
- `overdueNum`:历史应收对账单(`account_statement.account_type=1`)中,早于当月且 `closing_balance > 0` çš„æ•°é‡
- `overdueRate`:`overdueNum / åŽ†å²åº”æ”¶å¯¹è´¦å•æ€»æ•° * 100`
返回示例:
```json
{
  "monthlyIncome": 89500.00,
  "collectionRate": "62.80",
  "overdueNum": 4,
  "overdueRate": "18.18"
}
```
### 3.3 æœˆåº¦æ”¯å‡º `monthlyExpenditure`
返回字段不变:
- `monthlyExpenditure`
- `paymentRate`
- `grossProfit`
- `profitMarginRate`
新口径:
- `monthlyExpenditure`:当月付款合计
- `paymentRate`:`当月付款合计 / å½“月采购合同金额合计 * 100`
- `grossProfit`:`当月收款合计 - å½“月付款合计`
- `profitMarginRate`:`grossProfit / å½“月收款合计 * 100`
返回示例:
```json
{
  "monthlyExpenditure": 73400.00,
  "paymentRate": "57.34",
  "grossProfit": 16100.00,
  "profitMarginRate": "17.99"
}
```
## 4. å‰ç«¯è”调说明
1. å‰ç«¯å­—段映射无需调整,可直接沿用现有解析逻辑。
2. ç™¾åˆ†æ¯”字段仍为不带 `%` çš„字符串,前端如需展示 `%` è¯·ç»§ç»­å‰ç«¯æ‹¼æŽ¥ã€‚
3. æœ¬æ¬¡åŽç«¯è¿”回值由真实财务数据驱动,建议重点回归卡片汇总与趋势图的数值联动。
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -3,8 +3,12 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.basic.mapper.CustomerMapper;
@@ -104,6 +108,7 @@
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountStatementMapper accountStatementMapper;
    private final ProductionAccountMapper productionAccountMapper;
@@ -394,43 +399,21 @@
     */
    @Override
    public StatisticsReceivablePayableDto statisticsReceivablePayable(Integer type) {
        StatisticsReceivablePayableDto statisticsReceivablePayableDto = new StatisticsReceivablePayableDto();
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
        switch (type) {
            case 1:
                // èŽ·å–æœ¬å‘¨å‘¨ä¸€
                startDate = today.with(DayOfWeek.MONDAY);
                // èŽ·å–æœ¬å‘¨å‘¨æ—¥
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3:
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
        StatisticsReceivablePayableDto dto = new StatisticsReceivablePayableDto();
        LocalDate[] range = resolveFinanceRange(type);
        LocalDate startDate = range[0];
        LocalDate endDate = range[1];
                startDate = today.withMonth(firstMonthOfQuarter.getValue())
                        .with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue())
                        .with(TemporalAdjusters.lastDayOfMonth());
                break;
        }
        // åº”æ”¶
        BigDecimal receivableBase = sumSalesContractAmount(startDate, endDate);
        BigDecimal payableBase = sumPurchaseContractAmount(startDate, endDate);
        BigDecimal advanceMoney = sumCollectionAmount(startDate, endDate);
        BigDecimal prepayMoney = sumPaymentAmount(startDate, endDate);
        // åº”付
        // é¢„æ”¶
        // é¢„付
        return statisticsReceivablePayableDto;
        dto.setReceivableMoney(scaleMoney(maxZero(receivableBase.subtract(advanceMoney))));
        dto.setPayableMoney(scaleMoney(maxZero(payableBase.subtract(prepayMoney))));
        dto.setAdvanceMoney(scaleMoney(advanceMoney));
        dto.setPrepayMoney(scaleMoney(prepayMoney));
        return dto;
    }
    public static <T> BigDecimal sumAmount(List<T> list, java.util.function.Function<T, BigDecimal> amountExtractor) {
@@ -1239,102 +1222,58 @@
    @Override
    public MonthlyIncomeDto monthlyIncome() {
        MonthlyIncomeDto dto = new MonthlyIncomeDto();
        LocalDate now = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(now);
        LocalDateTime startOfMonth = currentMonth.atDay(1).atStartOfDay();
        LocalDateTime endOfMonth = currentMonth.atEndOfMonth().atTime(23, 59, 59);
        LocalDate today = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(today);
        LocalDate startDate = currentMonth.atDay(1);
        LocalDate endDate = currentMonth.atEndOfMonth();
        LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SalesLedgerProduct::getType, 1);
        wrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        wrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        BigDecimal monthlyIncome = sumCollectionAmount(startDate, endDate);
        BigDecimal receivableBase = sumSalesContractAmount(startDate, endDate);
        String collectionRate = toRateString(monthlyIncome, receivableBase);
        List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(wrapper);
        String currentMonthText = currentMonth.toString();
        List<AccountStatement> receivableStatements = defaultList(accountStatementMapper.selectList(
                new LambdaQueryWrapper<AccountStatement>()
                        .eq(AccountStatement::getAccountType, 1)
                        .le(AccountStatement::getStatementMonth, currentMonthText)));
        if (CollectionUtils.isEmpty(products)) {
        long overdueNum = receivableStatements.stream()
                .filter(item -> item.getStatementMonth() != null && item.getStatementMonth().compareTo(currentMonthText) < 0)
                .filter(item -> defaultDecimal(item.getClosingBalance()).compareTo(BigDecimal.ZERO) > 0)
                .count();
        long totalReceivableCount = receivableStatements.size();
        String overdueRate = totalReceivableCount == 0
                ? "0.00"
                : BigDecimal.valueOf(overdueNum)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(totalReceivableCount), 2, RoundingMode.HALF_UP)
                .toString();
        dto.setMonthlyIncome(scaleMoney(monthlyIncome));
        dto.setCollectionRate(collectionRate);
        dto.setOverdueNum(BigDecimal.valueOf(overdueNum));
        dto.setOverdueRate(overdueRate);
            return dto;
        }
        return dto;
    }
    @Override
    public MonthlyExpenditureDto monthlyExpenditure() {
        MonthlyExpenditureDto dto = new MonthlyExpenditureDto();
        LocalDate today = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(today);
        LocalDate startDate = currentMonth.atDay(1);
        LocalDate endDate = currentMonth.atEndOfMonth();
        // å½“月时间范围
        LocalDate now = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(now);
        LocalDateTime startOfMonth = currentMonth.atDay(1).atStartOfDay();
        LocalDateTime endOfMonth = currentMonth.atEndOfMonth().atTime(23, 59, 59);
        BigDecimal monthlyExpenditure = sumPaymentAmount(startDate, endDate);
        BigDecimal payableBase = sumPurchaseContractAmount(startDate, endDate);
        BigDecimal monthlyIncome = sumCollectionAmount(startDate, endDate);
        BigDecimal grossProfit = monthlyIncome.subtract(monthlyExpenditure);
        // é‡‡è´­å°è´¦ï¼ˆtype = 2)
        LambdaQueryWrapper<SalesLedgerProduct> purchaseWrapper = new LambdaQueryWrapper<>();
        purchaseWrapper.eq(SalesLedgerProduct::getType, 2);
        purchaseWrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        purchaseWrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        List<SalesLedgerProduct> purchaseProducts = salesLedgerProductMapper.selectList(purchaseWrapper);
        BigDecimal rawMaterialCost = BigDecimal.ZERO; // åŽŸææ–™æˆæœ¬
        BigDecimal paidAmount = BigDecimal.ZERO; // å·²ä»˜æ¬¾é‡‘额
        BigDecimal pendingAmount = BigDecimal.ZERO; // å¾…付款金额
        if (!CollectionUtils.isEmpty(purchaseProducts)) {
            for (SalesLedgerProduct p : purchaseProducts) {
                if (p.getTaxInclusiveTotalPrice() != null) {
                    rawMaterialCost = rawMaterialCost.add(p.getTaxInclusiveTotalPrice());
                }
            }
        }
        // å…¶ä»–费用
        // æœˆåº¦æ€»æ”¯å‡º
        BigDecimal monthlyExpenditure = BigDecimal.ZERO;
        dto.setMonthlyExpenditure(monthlyExpenditure);
        // å·²ä»˜æ¬¾ Ã·ï¼ˆå·²ä»˜æ¬¾ + å¾…付款)
        BigDecimal totalPayable = paidAmount.add(pendingAmount);
        if (totalPayable.compareTo(BigDecimal.ZERO) > 0) {
            String paymentRate = paidAmount
                    .divide(totalPayable, 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setPaymentRate(paymentRate);
        }
        // å”®å°è´¦ï¼ˆtype = 1)
        LambdaQueryWrapper<SalesLedgerProduct> salesWrapper = new LambdaQueryWrapper<>();
        salesWrapper.eq(SalesLedgerProduct::getType, 1);
        salesWrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        salesWrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        List<SalesLedgerProduct> salesProducts = salesLedgerProductMapper.selectList(salesWrapper);
        BigDecimal revenue = BigDecimal.ZERO;
        // æ¯›åˆ©æ¶¦ & åˆ©æ¶¦çއ
        if (revenue.compareTo(BigDecimal.ZERO) > 0) {
            // æ¯›åˆ©æ¶¦ = é”€å”®æ”¶å…¥ - åŽŸææ–™æˆæœ¬
            BigDecimal grossProfit = revenue.subtract(rawMaterialCost);
            dto.setGrossProfit(grossProfit);
            // åˆ©æ¶¦çއ = (销售收入 - æœˆåº¦æ€»æ”¯å‡º) / é”€å”®æ”¶å…¥
            BigDecimal profit = revenue.subtract(monthlyExpenditure);
            String profitMarginRate = profit
                    .divide(revenue, 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setProfitMarginRate(profitMarginRate);
        }
        dto.setMonthlyExpenditure(scaleMoney(monthlyExpenditure));
        dto.setPaymentRate(toRateString(monthlyExpenditure, payableBase));
        dto.setGrossProfit(scaleMoney(grossProfit));
        dto.setProfitMarginRate(toRateString(grossProfit, monthlyIncome));
        return dto;
    }
@@ -2331,6 +2270,97 @@
        return productionOperationTaskMapper.calculateProductionStatistics(startDateTime, endDateTime, userId, processIds);
    }
    private LocalDate[] resolveFinanceRange(Integer type) {
        LocalDate today = LocalDate.now();
        int safeType = type == null ? 1 : type;
        LocalDate startDate;
        LocalDate endDate;
        switch (safeType) {
            case 1:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3:
                Month firstMonthOfQuarter = today.getMonth().firstMonthOfQuarter();
                startDate = LocalDate.of(today.getYear(), firstMonthOfQuarter, 1);
                endDate = startDate.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
                break;
            default:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
        }
        return new LocalDate[]{startDate, endDate};
    }
    private BigDecimal sumSalesContractAmount(LocalDate startDate, LocalDate endDate) {
        List<SalesLedger> salesLedgers = defaultList(salesLedgerMapper.selectList(new LambdaQueryWrapper<SalesLedger>()
                .ge(SalesLedger::getEntryDate, toDate(startDate))
                .lt(SalesLedger::getEntryDate, toExclusiveEndDate(endDate))));
        return sumAmount(salesLedgers, SalesLedger::getContractAmount);
    }
    private BigDecimal sumPurchaseContractAmount(LocalDate startDate, LocalDate endDate) {
        List<PurchaseLedger> purchaseLedgers = defaultList(purchaseLedgerMapper.selectList(new LambdaQueryWrapper<PurchaseLedger>()
                .ge(PurchaseLedger::getEntryDate, toDate(startDate))
                .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(endDate))));
        return sumAmount(purchaseLedgers, PurchaseLedger::getContractAmount);
    }
    private BigDecimal sumCollectionAmount(LocalDate startDate, LocalDate endDate) {
        List<AccountSalesCollection> collections = defaultList(accountSalesCollectionMapper.selectList(
                new LambdaQueryWrapper<AccountSalesCollection>()
                        .ge(AccountSalesCollection::getCollectionDate, startDate)
                        .le(AccountSalesCollection::getCollectionDate, endDate)));
        return sumAmount(collections, AccountSalesCollection::getCollectionAmount);
    }
    private BigDecimal sumPaymentAmount(LocalDate startDate, LocalDate endDate) {
        List<AccountPurchasePayment> payments = defaultList(accountPurchasePaymentMapper.selectList(
                new LambdaQueryWrapper<AccountPurchasePayment>()
                        .ge(AccountPurchasePayment::getPaymentDate, startDate)
                        .le(AccountPurchasePayment::getPaymentDate, endDate)));
        return sumAmount(payments, AccountPurchasePayment::getPaymentAmount);
    }
    private Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate localDate) {
        return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private String toRateString(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return "0.00";
        }
        return defaultDecimal(numerator)
                .divide(denominator, 4, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP)
                .toString();
    }
    private BigDecimal maxZero(BigDecimal value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        return value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private BigDecimal scaleMoney(BigDecimal value) {
        return defaultDecimal(value).setScale(2, RoundingMode.HALF_UP);
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }