From 1fccd0a7eb0b06e757f677de6279edab0bc81236 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 22 五月 2026 13:08:34 +0800
Subject: [PATCH] refactor(home): 重构首页财务统计功能实现

---
 doc/20260522_首页财务接口升级前端变更文档.md                                 |  120 +++++++++++++++++
 src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java |  270 +++++++++++++++++++++-----------------
 2 files changed, 270 insertions(+), 120 deletions(-)

diff --git "a/doc/20260522_\351\246\226\351\241\265\350\264\242\345\212\241\346\216\245\345\217\243\345\215\207\347\272\247\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md" "b/doc/20260522_\351\246\226\351\241\265\350\264\242\345\212\241\346\216\245\345\217\243\345\215\207\347\272\247\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..532eba7
--- /dev/null
+++ "b/doc/20260522_\351\246\226\351\241\265\350\264\242\345\212\241\346\216\245\345\217\243\345\215\207\347\272\247\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.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. 杩斿洖瀛楁鍙e緞鍙樻洿
+
+### 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. 鍓嶇瀛楁鏄犲皠鏃犻渶璋冩暣锛屽彲鐩存帴娌跨敤鐜版湁瑙f瀽閫昏緫銆�  
+2. 鐧惧垎姣斿瓧娈典粛涓轰笉甯� `%` 鐨勫瓧绗︿覆锛屽墠绔闇�灞曠ず `%` 璇风户缁墠绔嫾鎺ャ��  
+3. 鏈鍚庣杩斿洖鍊肩敱鐪熷疄璐㈠姟鏁版嵁椹卞姩锛屽缓璁噸鐐瑰洖褰掑崱鐗囨眹鎬讳笌瓒嬪娍鍥剧殑鏁板�艰仈鍔ㄣ��  
diff --git a/src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java b/src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
index a17c012..fac571b 100644
--- a/src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
+++ b/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)) {
-            return dto;
-        }
+        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;
     }
 
+    @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);
 
-        // 閲囪喘鍙拌处锛坱ype = 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;
     }

--
Gitblit v1.9.3