gongchunyi
5 天以前 b3712bb9c618d3cb74b6a200fd83c2ba0577ac3d
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
已添加4个文件
已修改13个文件
538 ■■■■■ 文件已修改
doc/20260522_财务助手提问优化前端变更文档.md 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/AccountReportVo.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountingController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/financial-agent-prompt.txt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountStatementMapper.xml 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_²ÆÎñÖúÊÖÌáÎÊÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
# è´¢åŠ¡åŠ©æ‰‹æé—®ä¼˜åŒ–å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-22
适用模块:财务智能助手(`/financial-ai`)
## 1. èƒŒæ™¯
当前首页财务助手快捷提问为:
1. `生成本周经营周报`
2. `为什么利润下降`
3. `哪个客户最赚钱`
问题点:
- ç¬¬ 3 æ¡é—®æ³•在部分场景下意图命中不稳定,容易走普通文本回答,导致图表链接以原始 Markdown æ–‡æœ¬å±•示(如 `![...](https://local/generate_chart?options=...)`)。
- å¿«æ·æé—®ç¼ºå°‘时间范围和分析目标,结果稳定性与可解释性较弱。
## 2. å‰ç«¯å¿«æ·æé—®æ–‡æ¡ˆä¼˜åŒ–(必改)
建议将默认三条快捷提问调整为:
1. `生成本周经营周报(利润与现金流)`
2. `分析本月利润下降原因`
3. `近30天哪个客户利润贡献最高`
说明:
- ä¸‰æ¡é—®æ³•均带时间范围或分析目标,后端命中更稳定。
- ç¬¬ 3 æ¡ä¸Žâ€œæœ€èµšé’±å®¢æˆ·â€è¯­ä¹‰ä¸€è‡´ï¼Œä½†â€œåˆ©æ¶¦è´¡çŒ®æœ€é«˜â€æ›´æ˜Žç¡®ï¼Œé€‚合直接驱动利润分析结果页。
## 3. ä¸ŽåŽç«¯èƒ½åŠ›æ˜ å°„
| å¿«æ·æé—® | é¢„期命中能力 | é¢„期 `type` |
| --- | --- | --- |
| ç”Ÿæˆæœ¬å‘¨ç»è¥å‘¨æŠ¥ï¼ˆåˆ©æ¶¦ä¸ŽçŽ°é‡‘æµï¼‰ | ç»è¥æŠ¥å‘Šç”Ÿæˆ | `financial_operation_report` |
| åˆ†æžæœ¬æœˆåˆ©æ¶¦ä¸‹é™åŽŸå›  | è®¢å•利润分析 | `financial_order_profit_analysis` |
| è¿‘30天哪个客户利润贡献最高 | è®¢å•利润分析 | `financial_order_profit_analysis` |
后端已同步增强“最赚钱客户/客户利润最高/利润贡献最高”等同义问法识别,前端按以上文案改造后可直接联调。
## 4. èŠå¤©å†…容渲染兜底(建议改)
针对聊天返回文本中出现的图表 Markdown é“¾æŽ¥ï¼ˆ`https://local/generate_chart?options=...`),建议前端增加兜底处理:
1. è¯†åˆ« Markdown å›¾ç‰‡è¯­æ³•中的 `local/generate_chart` é“¾æŽ¥ã€‚
2. è§£æž `options` å‚数并转换为 ECharts `option` åŽæ¸²æŸ“图表组件。
3. è§£æžå¤±è´¥æ—¶ä¸å±•示原始长链接文本,展示统一空态提示。
## 5. è”调回归清单
1. ç‚¹å‡»å¿«æ·æé—® `生成本周经营周报(利润与现金流)`
   - æ ¡éªŒè¿”回 `type=financial_operation_report`,并正常渲染摘要/建议/图表。
2. ç‚¹å‡»å¿«æ·æé—® `分析本月利润下降原因`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,并展示亏损订单与客户利润排行。
3. ç‚¹å‡»å¿«æ·æé—® `近30天哪个客户利润贡献最高`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,`summary.topCustomerByProfit` æœ‰å€¼ã€‚
4. æ‰‹å·¥è¾“å…¥ `哪个客户最赚钱`、`哪个客户利润最高`
   - æ ¡éªŒä»å‘½ä¸­ `financial_order_profit_analysis`,不再出现原始图表 Markdown é“¾æŽ¥ç›´å‡ºã€‚
src/main/java/com/ruoyi/account/bean/vo/AccountReportVo.java
@@ -4,6 +4,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@@ -16,13 +17,50 @@
    @Schema(description = "总支出")
    private BigDecimal totalExpense;
    @Schema(description = "总收入笔数")
    private Integer incomeNumber;
    @Schema(description = "应收账款")
    private BigDecimal accountsReceivable;
    @Schema(description = "总支出笔数")
    private Integer expenseNumber;
    @Schema(description = "应付账款")
    private BigDecimal accountsPayable;
    @Schema(description = "净收入")
    private BigDecimal netRevenue;
    // --- æŠ˜çº¿å›¾ï¼šæœˆåº¦è¶‹åŠ¿æ•°æ® ---
    @Schema(description = "月度趋势数据列表")
    private List<MonthlyTrendVO> monthlyTrendList;
    // --- æŸ±çŠ¶å›¾ï¼šåº”æ”¶åº”ä»˜æœˆåº¦æ•°æ® ---
    @Schema(description = "应收应付月度数据列表")
    private List<ReceivablePayableVO> receivablePayableList;
    @Data
    @Schema(description = "月度趋势VO(折线图用)")
    public static class MonthlyTrendVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "当月营收")
        private BigDecimal income;
        @Schema(description = "当月支出")
        private BigDecimal expense;
        @Schema(description = "当月净利润")
        private BigDecimal profit;
    }
    @Data
    @Schema(description = "应收应付月度VO(柱状图用)")
    public static class ReceivablePayableVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "应收账款金额")
        private BigDecimal receivable;
        @Schema(description = "应付账款金额")
        private BigDecimal payable;
    }
}
src/main/java/com/ruoyi/account/controller/AccountingController.java
@@ -50,8 +50,8 @@
    /*****************************************财务报表******************************************************************************/
    @GetMapping("/accountStatementDetailsByMonth")
    @Log(title = "统计总应收+总支出+总收入笔数+总支出笔数+净收入", businessType = BusinessType.OTHER)
    @Operation(summary = "财务报表--统计总应收+总支出+总收入笔数+总支出笔数+净收入")
    @Log(title = "财务报表", businessType = BusinessType.OTHER)
    @Operation(summary = "财务报表")
    public R getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        return R.ok(accountingService.getAccountStatementDetailsByMonth(accountReportDto));
    }
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java
@@ -6,6 +6,7 @@
import com.ruoyi.account.bean.dto.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.purchase.dto.VatDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -22,4 +23,6 @@
    IPage<StatementAccountVo> listPageAccountStatement(Page page, @Param("req") StatementAccountDto statementAccountDto);
    IPage<VatDto> selectVatDtoPage(Page page, @Param("month") String month);
}
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java
@@ -8,7 +8,15 @@
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.bean.dto.DeviceTypeDetail;
import com.ruoyi.account.bean.dto.DeviceTypeDistributionVO;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.AccountReportVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
@@ -20,16 +28,22 @@
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
@@ -49,6 +63,10 @@
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    @Override
@@ -285,20 +303,155 @@
    @Override
    public AccountReportVo getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        AccountReportVo accountReportVo = new AccountReportVo();
        //总营收=收款单总金额
        //总收入笔数=收款单总笔数
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(Wrappers.<AccountSalesCollection>lambdaQuery()
                .between(AccountSalesCollection::getCollectionDate, accountReportDto.getEntryDateStart(), accountReportDto.getEntryDateEnd()));
        accountReportVo.setTotalIncome(accountSalesCollections.stream().map(AccountSalesCollection::getCollectionAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        accountReportVo.setIncomeNumber(accountSalesCollections.size());
        //总支出=付款单总金额
        //总支出笔数=付款单总笔数
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(Wrappers.<AccountPurchasePayment>lambdaQuery()
                .between(AccountPurchasePayment::getPaymentDate, accountReportDto.getEntryDateStart(), accountReportDto.getEntryDateEnd()));
        accountReportVo.setTotalExpense(accountPurchasePayments.stream().map(AccountPurchasePayment::getPaymentAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        accountReportVo.setExpenseNumber(accountPurchasePayments.size());
        //净利润=总营收-总支出
        accountReportVo.setNetRevenue(accountReportVo.getTotalIncome().subtract(accountReportVo.getTotalExpense()));
        LocalDate start = accountReportDto.getEntryDateStart();
        LocalDate end = accountReportDto.getEntryDateEnd();
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
        // ========== 1. é¡¶éƒ¨å¡ç‰‡æ•°æ® ==========
        // 1.1 æ€»è¥æ”¶ = æ”¶æ¬¾å•总金额
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(
                Wrappers.<AccountSalesCollection>lambdaQuery()
                        .between(AccountSalesCollection::getCollectionDate, start, end)
        );
        BigDecimal totalIncome = Optional.of(
                accountSalesCollections.stream()
                        .map(AccountSalesCollection::getCollectionAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalIncome(totalIncome);
        // 1.2 æ€»æ”¯å‡º = ä»˜æ¬¾å•总金额
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(
                Wrappers.<AccountPurchasePayment>lambdaQuery()
                        .between(AccountPurchasePayment::getPaymentDate, start, end)
        );
        BigDecimal totalExpense = Optional.of(
                accountPurchasePayments.stream()
                        .map(AccountPurchasePayment::getPaymentAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalExpense(totalExpense);
        // 1.3 åº”收账款 = é”€å”®å‡ºåº“金额合计 - é”€å”®é€€è´§é‡‘额合计
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setStartDate(accountReportDto.getEntryDateStart());
        salesOutboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        BigDecimal salesOutAmount = Optional.of(
                salesOutboundVos.stream()
                        .map(SalesOutboundVo::getOutboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        salesReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        BigDecimal salesReturnAmount = Optional.of(
                salesReturnVos.stream()
                        .map(SalesReturnVo::getRefundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsReceivable(salesOutAmount.subtract(salesReturnAmount));
        // 1.4 åº”付账款 = é‡‡è´­å…¥åº“金额合计 - é‡‡è´­é€€è´§é‡‘额合计
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseInboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        BigDecimal purchaseInAmount = Optional.of(
                purchaseInboundVos.stream()
                        .map(PurchaseInboundVo::getInboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        BigDecimal purchaseReturnAmount = Optional.of(
                purchaseReturnVos.stream()
                        .map(PurchaseReturnVo::getTotalAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsPayable(purchaseInAmount.subtract(purchaseReturnAmount));
        // 1.5 å‡€åˆ©æ¶¦ = æ€»è¥æ”¶ - æ€»æ”¯å‡º
        BigDecimal netProfit = totalIncome.subtract(totalExpense);
        accountReportVo.setNetRevenue(netProfit);
        // ========== 2. æŠ˜çº¿å›¾ï¼šæœˆåº¦è¥æ”¶/支出/净利润趋势 ==========
        Map<String, BigDecimal> monthIncomeMap = new HashMap<>();
        Map<String, BigDecimal> monthExpenseMap = new HashMap<>();
        // æœˆåº¦è¥æ”¶
        accountSalesCollections.forEach(item -> {
            String month = item.getCollectionDate().format(monthFormatter);
            monthIncomeMap.put(month, monthIncomeMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getCollectionAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦æ”¯å‡º
        accountPurchasePayments.forEach(item -> {
            String month = item.getPaymentDate().format(monthFormatter);
            monthExpenseMap.put(month, monthExpenseMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getPaymentAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç”Ÿæˆè¿žç»­æœˆä»½åˆ—表
        List<String> monthList = new ArrayList<>();
        LocalDate current = start.withDayOfMonth(1);
        while (!current.isAfter(end.withDayOfMonth(1))) {
            monthList.add(current.format(monthFormatter));
            current = current.plusMonths(1);
        }
        // ç»„装趋势数据
        List<AccountReportVo.MonthlyTrendVO> trendList = new ArrayList<>();
        for (String month : monthList) {
            BigDecimal income = monthIncomeMap.getOrDefault(month, BigDecimal.ZERO);
            BigDecimal expense = monthExpenseMap.getOrDefault(month, BigDecimal.ZERO);
            AccountReportVo.MonthlyTrendVO trend = new AccountReportVo.MonthlyTrendVO();
            trend.setMonth(month);
            trend.setIncome(income);
            trend.setExpense(expense);
            trend.setProfit(income.subtract(expense));
            trendList.add(trend);
        }
        accountReportVo.setMonthlyTrendList(trendList);
        // ========== 3. æŸ±çŠ¶å›¾ï¼šæœˆåº¦åº”æ”¶/应付数据 ==========
        Map<String, BigDecimal> monthReceivableMap = new HashMap<>();
        Map<String, BigDecimal> monthPayableMap = new HashMap<>();
        // æœˆåº¦åº”收(销售出库-退货)
        salesOutboundVos.forEach(item -> {
            String month = item.getShippingDate().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getOutboundAmount()).orElse(BigDecimal.ZERO)));
        });
        salesReturnVos.forEach(item -> {
            String month = item.getMakeTime().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getRefundAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦åº”付(采购入库-退货)
        purchaseInboundVos.forEach(item -> {
            String month = item.getInboundDate().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getInboundAmount()).orElse(BigDecimal.ZERO)));
        });
        purchaseReturnVos.forEach(item -> {
            String month = item.getPreparedAt().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getTotalAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç»„装应收应付数据
        List<AccountReportVo.ReceivablePayableVO> rpList = new ArrayList<>();
        for (String month : monthList) {
            AccountReportVo.ReceivablePayableVO rp = new AccountReportVo.ReceivablePayableVO();
            rp.setMonth(month);
            rp.setReceivable(monthReceivableMap.getOrDefault(month, BigDecimal.ZERO));
            rp.setPayable(monthPayableMap.getOrDefault(month, BigDecimal.ZERO));
            rpList.add(rp);
        }
        accountReportVo.setReceivablePayableList(rpList);
        return accountReportVo;
    }
}
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
@@ -45,7 +45,8 @@
        if (containsAny(text, "成本核算", "产品成本", "工序成本", "人工成本", "折旧", "材料损耗")) {
            return financialAgentTools.calculateIntelligentCost(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "利润分析", "订单利润", "亏损订单", "低利润", "最赚钱客户", "利润下降")) {
        if (containsAny(text, "利润分析", "订单利润", "亏损订单", "低利润",
                "最赚钱客户", "哪个客户最赚钱", "客户最赚钱", "利润最高客户", "利润贡献最高客户", "利润下降")) {
            return financialAgentTools.analyzeOrderProfit(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "库存资金", "库存积压", "呆滞库存", "资金占用", "周转率", "库存周转")) {
@@ -86,6 +87,15 @@
        }
        if ("为什么利润下降".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
        }
        if ("哪个客户最赚钱".equals(normalized)
                || "最近哪个客户最赚钱".equals(normalized)
                || "本月哪个客户最赚钱".equals(normalized)
                || "近30天哪个客户最赚钱".equals(normalized)
                || "哪个客户利润最高".equals(normalized)
                || "哪个客户利润贡献最高".equals(normalized)) {
            DateRange range = extractDateRange(text);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
        }
        return null;
@@ -216,6 +226,16 @@
                .replace("请", "")
                .replace("一下", "")
                .replace("为什么", "")
                .replace("哪个客户最赚钱", "")
                .replace("最近哪个客户最赚钱", "")
                .replace("本月哪个客户最赚钱", "")
                .replace("近30天哪个客户最赚钱", "")
                .replace("最赚钱客户", "")
                .replace("客户最赚钱", "")
                .replace("哪个客户利润最高", "")
                .replace("利润最高客户", "")
                .replace("哪个客户利润贡献最高", "")
                .replace("利润贡献最高客户", "")
                .replace("本月", "")
                .replace("本周", "")
                .replace("本年", "")
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java
@@ -2,10 +2,13 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.service.PurchaseReportService;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
@@ -15,6 +18,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Tag(name = "采购报表")
@RequestMapping("/purchase/report")
@@ -22,31 +27,38 @@
public class AccountingReportController {
    private final ISupplierService supplierService;
    private final PurchaseReportService purchaseReportService;
    @GetMapping("/list")
    @Log(title = "采购报表-项目利润", businessType = BusinessType.OTHER)
    public AjaxResult list(Page page) {
        return AjaxResult.success();
    public R list(Page page, String customerName) {
        return R.ok(purchaseReportService.list(page,customerName));
    }
    @Log(title = "采购报表-项目利润导出", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @Operation(summary = "采购报表-项目利润导出")
    public void export(HttpServletResponse response) {
    public void export(HttpServletResponse response, String customerName) {
        List<PurchaseReportVo> list = purchaseReportService.list(new Page(1,-1),customerName).getRecords();
        ExcelUtil<PurchaseReportVo> util = new ExcelUtil<>(PurchaseReportVo.class);
        util.exportExcel(response, list , "项目利润");
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.OTHER)
    @GetMapping("/listVat")
    public AjaxResult listVat(Page page,String month) {
        return AjaxResult.success();
    public R listVat(Page page,String month) {
        return R.ok(purchaseReportService.listVat(page,month));
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.EXPORT)
    @PostMapping("/exportTwo")
    @Operation(summary = "采购报表-增值税比对")
    public void exportTwo(HttpServletResponse response) {
    public void exportTwo(HttpServletResponse response,String month) {
        List<VatDto> list = purchaseReportService.listVat(new Page(1,-1),month).getRecords();
        ExcelUtil<VatDto> util = new ExcelUtil<>(VatDto.class);
        util.exportExcel(response, list , "增值税比对");
    }
    @GetMapping("/supplierTransactions")
src/main/java/com/ruoyi/purchase/dto/VatDto.java
@@ -1,27 +1,30 @@
package com.ruoyi.purchase.dto;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Schema(name = "VatDto", description = "管理驾驶舱--增值税比对参数")
@ExcelIgnoreUnannotated
public class VatDto {
    //月份
    @Excel(name = "月份")
    @Schema(description = "月份")
    private String month ;
    //进项税
    @Excel(name = "进项税额")
    @Excel(name = "销项税额")
    @Schema(description = "销项税额")
    private BigDecimal jTaxAmount;
    //销项税
    @Excel(name = "销项税额")
    @Excel(name = "进项税额")
    @Schema(description = "进项税额")
    private BigDecimal xTaxAmount;
    @Excel(name = "销-进")
    @Schema(description = "销-进")
    private BigDecimal taxAmount;
}
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.purchase.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.vo.PurchaseReportVo;
public interface PurchaseReportService {
    IPage<PurchaseReportVo> list(Page page, String customerName);
    IPage<VatDto> listVat(Page page, String month);
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.purchase.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.service.PurchaseReportService;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class PurchaseReportServiceImpl implements PurchaseReportService {
    private final SalesLedgerMapper salesLedgerMapper;
    private final AccountStatementMapper accountStatementMapper;
    @Override
    public IPage<PurchaseReportVo> list(Page page, String customerName) {
        return salesLedgerMapper.selectPurchaseReportVoPage(page, customerName);
    }
    @Override
    public IPage<VatDto> listVat(Page page, String month) {
        return accountStatementMapper.selectVatDtoPage(page, month);
    }
}
src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.purchase.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(name = "PurchaseReportVo", description = "管理驾驶舱--项目利润参数")
@ExcelIgnoreUnannotated
public class PurchaseReportVo {
    @Schema(description = "销售合同号")
    @Excel(name = "销售合同号")
    private String customerContractNo;
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "项目名称")
    @Excel(name = "项目名称")
    private String projectName;
    @Excel(name = "合同金额")
    @Schema(description = "合同金额")
    private BigDecimal contractAmount;
    @Excel(name = "采购金额")
    @Schema(description = "采购金额")
    private BigDecimal purchaseAmount;
    @Schema(description = "利润")
    @Excel(name = "利润")
    private BigDecimal balance;
    @Schema(description = "利润率")
    @Excel(name = "利润率")
    private BigDecimal balanceRatio;
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
@@ -6,6 +6,7 @@
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.home.dto.IncomeExpenseAnalysisDto;
import com.ruoyi.purchase.vo.PurchaseReportVo;
import com.ruoyi.sales.dto.SalesLedgerDto;
import com.ruoyi.sales.dto.SalesTrendDto;
import com.ruoyi.sales.dto.StatisticsTableDto;
@@ -86,4 +87,7 @@
    List<SalesTrendDto> statisticsTable(@Param("statisticsTableDto")StatisticsTableDto statisticsTableDto);
    IPage<SalesLedgerDto> listSalesLedgerAndShipped(Page page, @Param("ew") SalesLedgerDto salesLedgerDto);
    IPage<PurchaseReportVo> selectPurchaseReportVoPage(Page page, @Param("customerName") String customerName);
}
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -17,6 +17,8 @@
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockInRecordExportData;
import com.ruoyi.production.mapper.ProductionOrderPickMapper;
import com.ruoyi.production.pojo.ProductionOrderPick;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockUninventoryMapper;
@@ -30,6 +32,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
@Service
@@ -39,6 +42,7 @@
    private StockInRecordMapper stockInRecordMapper;
    private StockInventoryMapper stockInventoryMapper;
    private StockUninventoryMapper stockUninventoryMapper;
    private ProductionOrderPickMapper productionOrderPickMapper;
    @Override
    public IPage<StockInRecordDto> listPage(Page page, StockInRecordDto stockInRecordDto) {
@@ -186,6 +190,33 @@
            }
            stockInRecord.setApprovalStatus(approvalStatus);
            stockInRecordMapper.updateById(stockInRecord);
            // å®¡æ‰¹é©³å›žæ—¶ï¼Œå¦‚果是生产退料入库,需要回滚领料记录的退料数量
            if (ReviewStatusEnum.REJECTED.getCode().equals(approvalStatus) &&
                StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode().equals(stockInRecord.getRecordType())) {
                // æ‰¾åˆ°å¯¹åº”的领料记录
                ProductionOrderPick productionOrderPick = productionOrderPickMapper.selectById(stockInRecord.getRecordId());
                if (productionOrderPick != null) {
                    // å›žæ»šé€€æ–™æ•°é‡
                    BigDecimal returnQty = productionOrderPick.getReturnQty();
                    if (returnQty == null) {
                        returnQty = BigDecimal.ZERO;
                    }
                    BigDecimal newReturnQty = returnQty.subtract(stockInRecord.getStockInNum());
                    if (newReturnQty.compareTo(BigDecimal.ZERO) < 0) {
                        newReturnQty = BigDecimal.ZERO;
                    }
                    productionOrderPick.setReturnQty(newReturnQty);
                    // é‡æ–°è®¡ç®—实际用量
                    BigDecimal actualQty = productionOrderPick.getQuantity().add(
                        productionOrderPick.getFeedingQty() != null ? productionOrderPick.getFeedingQty() : BigDecimal.ZERO)
                        .subtract(newReturnQty);
                    productionOrderPick.setActualQty(actualQty);
                    productionOrderPick.setReturned(newReturnQty.compareTo(BigDecimal.ZERO) > 0);
                    productionOrderPickMapper.updateById(productionOrderPick);
                }
            }
            // å®¡æ‰¹é€šè¿‡æ—¶ï¼Œåº“存增加
            if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)) {
                if ("0".equals(stockInRecord.getType())) {
src/main/resources/application.yml
@@ -3,7 +3,7 @@
  main:
    allow-circular-references: true
  profiles:
    active: dev
    active: dev-pro
langchain4j:
  mcp:
    # MCP æœåŠ¡ç«¯åœ°å€ï¼ˆæ ¹æ®å®žé™…éƒ¨ç½²çš„ MCP æœåŠ¡è°ƒæ•´ï¼‰
src/main/resources/financial-agent-prompt.txt
@@ -6,6 +6,6 @@
2. å‘½ä¸­æˆæœ¬ã€åˆ©æ¶¦ã€åº“存资金、现金流、预警、驾驶舱、日报周报场景时,优先调用对应工具。
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,也不要在前后追加解释文本。
4. å½“用户问题缺少时间范围时,默认使用工具内置口径(如近30天、本月、近90天),并在后续可提醒用户补充范围。
5. ç”¨æˆ·é—®â€œä¸ºä»€ä¹ˆåˆ©æ¶¦ä¸‹é™â€â€œå“ªä¸ªè®¢å•亏损”“哪个客户最赚钱”“哪个车间/工序成本最高”等问题时,优先基于订单利润与工序成本分析工具作答。
5. ç”¨æˆ·é—®â€œä¸ºä»€ä¹ˆåˆ©æ¶¦ä¸‹é™â€â€œå“ªä¸ªè®¢å•亏损”“哪个客户最赚钱”“哪个客户利润贡献最高”“哪个车间/工序成本最高”等问题时,优先基于订单利润与工序成本分析工具作答。
6. å›žç­”必须使用中文;若数据不足以得出结论,明确指出缺少哪些关键字段或筛选条件。
7. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
src/main/resources/mapper/account/AccountStatementMapper.xml
@@ -40,4 +40,39 @@
            </if>
    ORDER BY lj.statement_month DESC
    </select>
    <select id="selectVatDtoPage" resultType="com.ruoyi.purchase.dto.VatDto">
    SELECT
        month,
        jTaxAmount,
        xTaxAmount,
        (jTaxAmount - xTaxAmount) AS taxAmount
    FROM (
        SELECT
            month,
            SUM(IF(type = 'purchase', tax_price, 0)) AS xTaxAmount,
            SUM(IF(type = 'sales', tax_price, 0)) AS jTaxAmount
        FROM (
            SELECT
                DATE_FORMAT(issue_date, '%Y-%m') AS month,
                tax_price,
                'sales' AS type
            FROM account_sales_invoice
            WHERE status != 1
            UNION ALL
            SELECT
                DATE_FORMAT(issue_date, '%Y-%m') AS month,
                tax_price,
                'purchase' AS type
            FROM account_purchase_invoice
            WHERE status != 1
        ) AS all_data
    GROUP BY month
    ) AS TT
     <where>
            <if test="month != null">
                and TT.month = #{month}
            </if>
     </where>
    ORDER BY TT.month
    </select>
</mapper>
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -121,5 +121,21 @@
        </if>
        order by sl.execution_date desc
    </select>
    <select id="selectPurchaseReportVoPage" resultType="com.ruoyi.purchase.vo.PurchaseReportVo">
        select sl.sales_contract_no customerContractNo,
               c.customer_name,
               sl.project_name,
               sl.contract_amount contractAmount,
               pl.contract_amount purchaseAmount,
               sl.contract_amount-pl.contract_amount balance,
               (sl.contract_amount-pl.contract_amount)/sl.contract_amount balanceRatio
        from sales_ledger sl
        left join purchase_ledger pl on sl.id = pl.sales_ledger_id
        left join customer c on sl.customer_id = c.id
        where 1=1
        <if test="customerName != null and customerName != '' ">
            and c.customer_name like concat('%',#{customerName},'%')
        </if>
    </select>
</mapper>