liding
5 天以前 db628292cb9b69b901b44b146ac88c5f68e38389
财务管理系统模块优化
已修改12个文件
已添加21个文件
1697 ■■■■■ 文件已修改
src/main/java/com/ruoyi/account/controller/AccountController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountExpenseController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/FinanceAccountController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/FinanceCashflowPlanController.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/FinanceReconciliationController.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/AccountTransferDto.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountExpenseMapper.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/FinanceAccountMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/FinanceCashflowPlanMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationDiffMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationExternalMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationTaskMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/Account.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountExpense.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/FinanceAccount.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/FinanceCashflowPlan.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationDiff.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationExternal.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationTask.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountExpenseService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/FinanceAccountService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/FinanceCashflowPlanService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/FinanceReconciliationService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountExpenseServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountServiceImpl.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/FinanceAccountServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/FinanceCashflowPlanServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/FinanceReconciliationServiceImpl.java 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountExpenseMapper.xml 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountIncomeMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/FinanceAccountMapper.xml 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountController.java
@@ -1,6 +1,7 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.dto.AccountTransferDto;
import com.ruoyi.account.pojo.Account;
import com.ruoyi.account.pojo.AccountIncome;
import com.ruoyi.account.service.AccountIncomeService;
@@ -31,14 +32,25 @@
    /**
     * æ–°å¢ž
     * æ–°å¢žæµæ°´(自动补全收支方向)
     * @param account
     * @return
     */
    @PostMapping()
    @ApiOperation("新增")
    @PostMapping("/add")
    @ApiOperation("新增流水")
    public AjaxResult add(@RequestBody Account account) {
        return AjaxResult.success(accountService.save(account));
        return AjaxResult.success(accountService.saveFlow(account));
    }
    /**
     * è½¬è´¦(生成成对流水)
     * @param dto
     * @return
     */
    @PostMapping("/transfer")
    @ApiOperation("转账")
    public AjaxResult transfer(@RequestBody AccountTransferDto dto) {
        return AjaxResult.success(accountService.transfer(dto));
    }
    /**
@@ -52,7 +64,10 @@
        if(CollectionUtils.isEmpty(ids)){
            return AjaxResult.error("请选择至少一条数据");
        }
        //删除检验单
        if (accountService.listByIds(ids).stream().anyMatch(item -> "2".equals(item.getFlowType()))) {
            return AjaxResult.error("转账成对流水不可单独删除");
        }
        // åˆ é™¤æ™®é€šèµ„金流水;转账流水必须保持成对完整
        return AjaxResult.success(accountService.removeBatchByIds(ids));
    }
@@ -64,7 +79,7 @@
    @PutMapping()
    @ApiOperation("修改")
    public AjaxResult update(@RequestBody Account account) {
        return AjaxResult.success(accountService.updateById(account));
        return AjaxResult.success(accountService.updateFlow(account));
    }
    /**
src/main/java/com/ruoyi/account/controller/AccountExpenseController.java
@@ -131,6 +131,12 @@
        return AjaxResult.success(accountExpenseService.analysis());
    }
    @GetMapping("/report/category")
    @ApiOperation("支出大类占比与趋势")
    public AjaxResult categoryReport(DateQueryDto dateQueryDto) {
        return AjaxResult.success(accountExpenseService.categoryReport(dateQueryDto));
    }
    /**
     * è´¢åŠ¡æŠ¥è¡¨å›¾è¡¨æ”¶å…¥å¹´åº¦æŸ¥è¯¢
     * @param
src/main/java/com/ruoyi/account/controller/FinanceAccountController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.FinanceAccount;
import com.ruoyi.account.service.FinanceAccountService;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
/**
 * è´¢åŠ¡ç®¡ç†--资金账户
 */
@RestController
@RequestMapping("/finance/account")
@Api(tags = "财务管理--资金账户")
public class FinanceAccountController {
    @Resource
    private FinanceAccountService financeAccountService;
    /**
     * æ–°å¢žè´¦æˆ·
     */
    @PostMapping
    @ApiOperation("新增账户")
    public AjaxResult add(@RequestBody FinanceAccount account) {
        if (account.getOpeningBalance() == null) {
            account.setOpeningBalance(BigDecimal.ZERO);
        }
        if (account.getStatus() == null) {
            account.setStatus("0");
        }
        return AjaxResult.success(financeAccountService.save(account));
    }
    /**
     * åˆ é™¤
     */
    @DeleteMapping
    @ApiOperation("删除账户")
    public AjaxResult del(@RequestBody List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        return AjaxResult.success(financeAccountService.removeBatchByIds(ids));
    }
    /**
     * ä¿®æ”¹
     */
    @PutMapping
    @ApiOperation("修改账户")
    public AjaxResult update(@RequestBody FinanceAccount account) {
        return AjaxResult.success(financeAccountService.updateById(account));
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢(含当前余额)
     */
    @GetMapping("/page")
    @ApiOperation("分页查询(含余额)")
    public AjaxResult page(Page page, FinanceAccount account) {
        return AjaxResult.success(financeAccountService.pageWithBalance(page, account));
    }
    /**
     * å¯ç”¨è´¦æˆ·åˆ—表(下拉用)
     */
    @GetMapping("/list")
    @ApiOperation("启用账户列表(下拉用)")
    public AjaxResult list() {
        FinanceAccount q = new FinanceAccount();
        q.setStatus("0");
        return AjaxResult.success(financeAccountService.pageWithBalance(new Page(1, 1000), q).getRecords());
    }
    /**
     * æŸ¥è¯¢è´¦æˆ·ä½™é¢
     */
    @GetMapping("/balance/{accountId}")
    @ApiOperation("查询账户余额")
    public AjaxResult balance(@PathVariable("accountId") Long accountId) {
        return AjaxResult.success(financeAccountService.getBalance(accountId));
    }
    /**
     * èµ„金流趋势
     */
    @GetMapping("/trend")
    @ApiOperation("资金流趋势")
    public AjaxResult trend(@RequestParam(required = false) Long accountId,
                            @RequestParam(required = false) String start,
                            @RequestParam(required = false) String end) {
        return AjaxResult.success(financeAccountService.trend(accountId, start, end));
    }
}
src/main/java/com/ruoyi/account/controller/FinanceCashflowPlanController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.FinanceCashflowPlan;
import com.ruoyi.account.service.FinanceCashflowPlanService;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/finance/cashflowPlan")
@Api(tags = "财务管理--收支计划")
public class FinanceCashflowPlanController {
    @Resource
    private FinanceCashflowPlanService service;
    @GetMapping("/page")
    public AjaxResult page(Page page, FinanceCashflowPlan query) {
        QueryWrapper<FinanceCashflowPlan> wrapper = new QueryWrapper<>();
        wrapper.eq(query.getPlanType() != null && !query.getPlanType().isEmpty(), "plan_type", query.getPlanType())
                .eq(query.getStatus() != null && !query.getStatus().isEmpty(), "status", query.getStatus())
                .like(query.getCounterparty() != null && !query.getCounterparty().isEmpty(), "counterparty", query.getCounterparty())
                .orderByAsc("plan_date").orderByDesc("id");
        return AjaxResult.success(service.page(page, wrapper));
    }
    @PostMapping
    @ApiOperation("新增收支计划")
    public AjaxResult add(@RequestBody FinanceCashflowPlan plan) {
        if (plan.getStatus() == null) plan.setStatus("0");
        return AjaxResult.success(service.save(plan));
    }
    @PutMapping
    public AjaxResult update(@RequestBody FinanceCashflowPlan plan) {
        return AjaxResult.success(service.updateById(plan));
    }
    @DeleteMapping
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return AjaxResult.success(service.removeBatchByIds(ids));
    }
    @GetMapping("/forecast")
    @ApiOperation("未来30天资金预测")
    public AjaxResult forecast(@RequestParam(required = false) Integer days,
                               @RequestParam(required = false) BigDecimal safetyLine) {
        return AjaxResult.success(service.forecast(days, safetyLine));
    }
}
src/main/java/com/ruoyi/account/controller/FinanceReconciliationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.account.pojo.FinanceReconciliationTask;
import com.ruoyi.account.pojo.FinanceReconciliationExternal;
import com.ruoyi.account.pojo.FinanceReconciliationDiff;
import com.ruoyi.account.service.FinanceReconciliationService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;
/**
 * è´¢åŠ¡ç®¡ç†--财务对账(任务/差异/内部自动对账)
 */
@RestController
@RequestMapping("/finance/reconciliation")
@Api(tags = "财务管理--财务对账")
public class FinanceReconciliationController {
    @Resource
    private FinanceReconciliationService financeReconciliationService;
    /**
     * å¯¹è´¦ä»»åŠ¡åˆ†é¡µ
     */
    @GetMapping("/task/page")
    @ApiOperation("对账任务分页")
    public AjaxResult taskPage(Page page, FinanceReconciliationTask query) {
        return AjaxResult.success(financeReconciliationService.pageTask(page, query));
    }
    /**
     * å¯¹è´¦ä»»åŠ¡è¯¦æƒ…
     */
    @GetMapping("/task/{id}")
    @ApiOperation("对账任务详情")
    public AjaxResult taskDetail(@PathVariable("id") Long id) {
        return AjaxResult.success(financeReconciliationService.getTask(id));
    }
    /**
     * å·®å¼‚明细
     */
    @GetMapping("/task/{id}/diff")
    @ApiOperation("对账差异明细")
    public AjaxResult taskDiff(@PathVariable("id") Long id) {
        return AjaxResult.success(financeReconciliationService.listDiff(id));
    }
    /**
     * å‘起内部对账(收支记录 vs èµ„金流水 è‡ªåŠ¨æ¯”å¯¹)
     */
    @PostMapping("/task/internal")
    @ApiOperation("发起内部对账")
    public AjaxResult internal(@RequestBody InternalReconReq req) {
        return AjaxResult.success(
                financeReconciliationService.generateInternal(req.getPeriodStart(), req.getPeriodEnd(), req.getAccountId()));
    }
    /** å‘起内部/外部对账;外部对账时同时提交手工录入的 externalList */
    @PostMapping("/task")
    @ApiOperation("发起对账")
    public AjaxResult createTask(@RequestBody TaskReq req) {
        FinanceReconciliationTask task = new FinanceReconciliationTask();
        task.setReconType(req.getReconType());
        task.setPeriodStart(req.getPeriodStart());
        task.setPeriodEnd(req.getPeriodEnd());
        task.setAccountId(req.getAccountId());
        task.setTarget(req.getTarget());
        return AjaxResult.success(financeReconciliationService.generateExternal(task, req.getExternalList()));
    }
    @GetMapping("/task/{id}/external")
    @ApiOperation("外部凭证列表")
    public AjaxResult external(@PathVariable("id") Long id) {
        return AjaxResult.success(financeReconciliationService.listExternal(id));
    }
    /**
     * å¡«å†™å¤„理方案/归档
     */
    @PutMapping("/task/{id}/handle")
    @ApiOperation("填写处理方案/归档")
    public AjaxResult handle(@PathVariable("id") Long id, @RequestBody HandleReq req) {
        return AjaxResult.success(financeReconciliationService.updateHandle(id, req.getHandleNote(), req.getStatus()));
    }
    @PutMapping("/task/{id}/archive")
    @ApiOperation("归档对账任务")
    public AjaxResult archive(@PathVariable("id") Long id) {
        return AjaxResult.success(financeReconciliationService.updateHandle(id, null, "2"));
    }
    @PostMapping("/export")
    @ApiOperation("导出差异报告")
    public void export(HttpServletResponse response, @RequestBody ExportReq req) {
        List<FinanceReconciliationDiff> rows = financeReconciliationService.listDiff(req.getTaskId());
        new ExcelUtil<FinanceReconciliationDiff>(FinanceReconciliationDiff.class)
                .exportExcel(response, rows, "财务对账差异报告");
    }
    @Data
    public static class InternalReconReq {
        @JsonFormat(pattern = "yyyy-MM-dd")
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date periodStart;
        @JsonFormat(pattern = "yyyy-MM-dd")
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date periodEnd;
        private Long accountId;
    }
    @Data
    public static class HandleReq {
        private String handleNote;
        private String status;
    }
    @Data
    public static class TaskReq extends InternalReconReq {
        private String reconType;
        private String target;
        private List<FinanceReconciliationExternal> externalList;
    }
    @Data
    public static class ExportReq {
        private Long taskId;
    }
}
src/main/java/com/ruoyi/account/dto/AccountTransferDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.account.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
 * è´¢åŠ¡ç®¡ç†--资金转账(成对流水)
 */
@Data
public class AccountTransferDto implements Serializable {
    @ApiModelProperty("转出账户ID")
    @NotNull(message = "转出账户不能为空")
    private Long fromAccountId;
    @ApiModelProperty("转入账户ID")
    @NotNull(message = "转入账户不能为空")
    private Long toAccountId;
    @ApiModelProperty("转账金额")
    @NotNull(message = "转账金额不能为空")
    private BigDecimal amount;
    @ApiModelProperty("转账日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date transferDate;
    @ApiModelProperty("备注")
    private String remark;
}
src/main/java/com/ruoyi/account/mapper/AccountExpenseMapper.java
@@ -24,4 +24,8 @@
    List<AccountDto2> report(@Param("dateQueryDto") DateQueryDto dateQueryDto);
    BigDecimal report1(@Param("dateQueryDto") DateQueryDto dateQueryDto, @Param("dictValue") String dictValue);
    List<Map<String, Object>> reportByCategory(@Param("dateQueryDto") DateQueryDto dateQueryDto);
    List<Map<String, Object>> trendByCategory(@Param("dateQueryDto") DateQueryDto dateQueryDto);
}
src/main/java/com/ruoyi/account/mapper/FinanceAccountMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.FinanceAccount;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Mapper
public interface FinanceAccountMapper extends BaseMapper<FinanceAccount> {
    /** åˆ†é¡µæŸ¥è¯¢(带当前余额) */
    IPage<FinanceAccount> selectAccountPageWithBalance(Page page, @Param("c") FinanceAccount account);
    /** è®¡ç®—某账户当前余额:期初 + Î£(方向×金额) */
    BigDecimal selectBalance(@Param("accountId") Long accountId);
    /** æ‰€æœ‰å¯ç”¨è´¦æˆ·å½“前余额合计 */
    BigDecimal selectTotalBalance();
    BigDecimal selectTotalSafetyLine();
    /** èµ„金流趋势:按日聚合(收入/支出/净额) */
    List<Map<String, Object>> trend(@Param("accountId") Long accountId,
                                    @Param("start") String start,
                                    @Param("end") String end);
}
src/main/java/com/ruoyi/account/mapper/FinanceCashflowPlanMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.FinanceCashflowPlan;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FinanceCashflowPlanMapper extends BaseMapper<FinanceCashflowPlan> {
}
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationDiffMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.FinanceReconciliationDiff;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FinanceReconciliationDiffMapper extends BaseMapper<FinanceReconciliationDiff> {
}
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationExternalMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.FinanceReconciliationExternal;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FinanceReconciliationExternalMapper extends BaseMapper<FinanceReconciliationExternal> {
}
src/main/java/com/ruoyi/account/mapper/FinanceReconciliationTaskMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.FinanceReconciliationTask;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FinanceReconciliationTaskMapper extends BaseMapper<FinanceReconciliationTask> {
}
src/main/java/com/ruoyi/account/pojo/Account.java
@@ -38,10 +38,40 @@
    private Date accountDate;
    /**
     * æ‰€å±žèµ„金账户ID(finance_account.id)
     */
    private Long accountId;
    /**
     * æµæ°´ç±»åž‹(字典 finance_flow_type):0收入 1支出 2转账 3存入 4取出
     */
    @Excel(name = "流水类型", readConverterExp = "0=收入,1=支出,2=转账,3=存入,4=取出")
    private String flowType;
    /**
     * å…³è”记录类型:Income/Expense(可空)
     */
    private String linkType;
    /**
     * å…³è”记录主键ID(可空)
     */
    private Long linkId;
    /**
     * æ”¶æ”¯æ–¹å‘:+1 å¢žåŠ ä½™é¢ -1 å‡å°‘余额
     */
    private Integer direction;
    /**
     * è½¬è´¦å¯¹æ‰‹è´¦æˆ·ID(预留)
     */
    private Long counterAccountId;
    /**
     * ç±»åž‹
     */
    @Excel(name = "类型",readConverterExp = "0=销售收入,1=服务收入,2=其他收入")
    @NotBlank(message = "类型不能为空!!")
    private String typeText;
    /**
@@ -90,5 +120,18 @@
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    /**
     * æ‰€å±žè´¦æˆ·åç§°(展示用,不入库)
     */
    @TableField(exist = false)
    private String accountName;
    @ApiModelProperty(value = "开始时间(查询)")
    @TableField(exist = false)
    private String entryDateStart;
    @ApiModelProperty(value = "结束时间(查询)")
    @TableField(exist = false)
    private String entryDateEnd;
}
src/main/java/com/ruoyi/account/pojo/AccountExpense.java
@@ -38,6 +38,12 @@
    private Date expenseDate;
    /**
     * æ”¯å‡ºå¤§ç±»(采购支出,费用支出,薪资支出,其他) å­—å…¸:finance_expense_category
     */
    @Excel(name = "支出大类", readConverterExp = "0=采购支出,1=费用支出,2=薪资支出,3=其他")
    private String expenseCategory;
    /**
     * æ”¯å‡ºç±»åž‹(办公用品,员工工资,差旅费,设备费用,其他)
     */
    @Excel(name = "支出类型",readConverterExp = "0=办公用品,1=员工工资,2=差旅费,3=设备费用,4=其他")
src/main/java/com/ruoyi/account/pojo/FinanceAccount.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * è´¢åŠ¡ç®¡ç†--资金账户主档
 * finance_account
 */
@TableName(value = "finance_account")
@Data
public class FinanceAccount implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * åºå·
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * è´¦æˆ·åç§°
     */
    @Excel(name = "账户名称")
    @NotBlank(message = "账户名称不能为空!!")
    private String accountName;
    /**
     * è´¦æˆ·ç±»åž‹(字典 finance_account_type):0银行账户 1现金账户
     */
    @Excel(name = "账户类型", readConverterExp = "0=银行账户,1=现金账户")
    private String accountType;
    /**
     * å¼€æˆ·è¡Œ
     */
    @Excel(name = "开户行")
    private String bankName;
    /**
     * é“¶è¡Œè´¦å·
     */
    @Excel(name = "银行账号")
    private String bankNo;
    /**
     * æœŸåˆä½™é¢
     */
    @Excel(name = "期初余额")
    private BigDecimal openingBalance;
    /** èµ„金预测安全线 */
    private BigDecimal safetyLine;
    /**
     * çŠ¶æ€:0启用 1停用
     */
    @Excel(name = "状态", readConverterExp = "0=启用,1=停用")
    private String status;
    /**
     * å¤‡æ³¨
     */
    @Excel(name = "备注")
    private String remark;
    /**
     * å½“前余额(动态计算,不入库)
     */
    @TableField(exist = false)
    private BigDecimal currentBalance;
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty(value = "创建用户")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "修改用户")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @ApiModelProperty(value = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/account/pojo/FinanceCashflowPlan.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
@Data
@TableName("finance_cashflow_plan")
public class FinanceCashflowPlan implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    @Excel(name = "计划类型", readConverterExp = "0=计划收入,1=计划支出")
    private String planType;
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "计划日期", dateFormat = "yyyy-MM-dd")
    private Date planDate;
    @Excel(name = "预计金额")
    private BigDecimal expectedAmount;
    @Excel(name = "分类")
    private String category;
    @Excel(name = "往来单位")
    private String counterparty;
    private Long relatedId;
    @Excel(name = "状态", readConverterExp = "0=待发生,1=已发生,2=已取消")
    private String status;
    @ApiModelProperty("备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationDiff.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * è´¢åŠ¡ç®¡ç†--对账差异明细
 * finance_reconciliation_diff
 */
@TableName(value = "finance_reconciliation_diff")
@Data
public class FinanceReconciliationDiff implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * åºå·
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * å¯¹è´¦ä»»åŠ¡ID
     */
    private Long taskId;
    /**
     * å·®å¼‚类型(字典 finance_diff_type):0系统有外部无 1外部有系统无 2金额不符 3日期不符
     */
    private String diffType;
    /**
     * ä¸šåŠ¡ç±»åž‹:Income/Expense/Flow
     */
    private String bizType;
    /**
     * ä¸šåŠ¡è®°å½•ID
     */
    private Long bizId;
    /**
     * ä¸šåŠ¡æ—¥æœŸ
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "业务日期", dateFormat = "yyyy-MM-dd")
    private Date bizDate;
    /**
     * å¾€æ¥å•位
     */
    @Excel(name = "往来单位")
    private String counterparty;
    /**
     * ç³»ç»Ÿé‡‘额
     */
    @Excel(name = "系统金额")
    private BigDecimal systemAmount;
    /**
     * å¯¹æ–¹é‡‘额
     */
    @Excel(name = "外部金额")
    private BigDecimal externalAmount;
    /**
     * å¤‡æ³¨
     */
    @Excel(name = "差异说明")
    private String remark;
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty(value = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationExternal.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
@Data
@TableName("finance_reconciliation_external")
public class FinanceReconciliationExternal implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long taskId;
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "外部日期", dateFormat = "yyyy-MM-dd")
    private Date extDate;
    @Excel(name = "往来单位")
    private String extCounterparty;
    @Excel(name = "金额")
    private BigDecimal amount;
    @Excel(name = "方向", readConverterExp = "1=收入,-1=支出")
    private Integer direction;
    @Excel(name = "凭证号")
    private String voucherNo;
    @Excel(name = "备注")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/account/pojo/FinanceReconciliationTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * è´¢åŠ¡ç®¡ç†--对账任务
 * finance_reconciliation_task
 */
@TableName(value = "finance_reconciliation_task")
@Data
public class FinanceReconciliationTask implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * åºå·
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * å¯¹è´¦ç±»åž‹(字典 finance_recon_type):0内部对账 1银行对账 2供应商对账 3客户对账
     */
    @Excel(name = "对账类型", readConverterExp = "0=内部对账,1=银行对账,2=供应商对账,3=客户对账")
    @NotBlank(message = "对账类型不能为空")
    private String reconType;
    /**
     * å¯¹è´¦æœŸé—´å¼€å§‹
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "期间开始", width = 30, dateFormat = "yyyy-MM-dd")
    private Date periodStart;
    /**
     * å¯¹è´¦æœŸé—´ç»“束
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "期间结束", width = 30, dateFormat = "yyyy-MM-dd")
    private Date periodEnd;
    /**
     * å¯¹è´¦å¯¹è±¡(账户/供应商/客户名称)
     */
    @Excel(name = "对账对象")
    private String target;
    /**
     * å¯¹è´¦è´¦æˆ·ID
     */
    private Long accountId;
    /**
     * ç³»ç»Ÿé‡‘额(收支净额)
     */
    @Excel(name = "系统金额")
    private BigDecimal systemAmount;
    /**
     * æµæ°´/外部金额
     */
    @Excel(name = "流水金额")
    private BigDecimal externalAmount;
    /**
     * å·®é¢
     */
    @Excel(name = "差额")
    private BigDecimal diffAmount;
    /**
     * ç»“æžœ(字典 finance_recon_result):0一致 1有差异
     */
    @Excel(name = "结果", readConverterExp = "0=一致,1=有差异")
    private String result;
    /**
     * çŠ¶æ€(字典 finance_recon_status):0待处理 1处理中 2已归档
     */
    @Excel(name = "状态", readConverterExp = "0=待处理,1=处理中,2=已归档")
    private String status;
    /**
     * å¤„理方案
     */
    @Excel(name = "处理方案")
    private String handleNote;
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty(value = "创建用户")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "修改用户")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @ApiModelProperty(value = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}
src/main/java/com/ruoyi/account/service/AccountExpenseService.java
@@ -26,6 +26,8 @@
    Map<String,List<String>> analysis();
    Map<String, Object> categoryReport(DateQueryDto dateQueryDto);
    AccountExpense getByInvoiceNumber(String purchaseContractNumber);
    List<AccountExpense> getByInvoiceNumberList(String purchaseContractNumber);
src/main/java/com/ruoyi/account/service/AccountService.java
@@ -3,7 +3,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.dto.AccountDto3;
import com.ruoyi.account.dto.AccountTransferDto;
import com.ruoyi.account.pojo.Account;
import com.ruoyi.account.pojo.AccountIncome;
@@ -14,4 +14,12 @@
    IPage<Account> accountListPage(Page page, Account account);
    /** ä¿å­˜æµæ°´(自动补全收支方向) */
    boolean saveFlow(Account account);
    boolean updateFlow(Account account);
    /** è½¬è´¦:生成成对流水 */
    boolean transfer(AccountTransferDto dto);
}
src/main/java/com/ruoyi/account/service/FinanceAccountService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.pojo.FinanceAccount;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public interface FinanceAccountService extends IService<FinanceAccount> {
    IPage<FinanceAccount> pageWithBalance(Page page, FinanceAccount account);
    BigDecimal getBalance(Long accountId);
    /** èµ„金流趋势:按日聚合收入/支出/净额 */
    List<Map<String, Object>> trend(Long accountId, String start, String end);
}
src/main/java/com/ruoyi/account/service/FinanceCashflowPlanService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.pojo.FinanceCashflowPlan;
import java.math.BigDecimal;
import java.util.Map;
public interface FinanceCashflowPlanService extends IService<FinanceCashflowPlan> {
    Map<String, Object> forecast(Integer days, BigDecimal safetyLine);
}
src/main/java/com/ruoyi/account/service/FinanceReconciliationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.pojo.FinanceReconciliationDiff;
import com.ruoyi.account.pojo.FinanceReconciliationExternal;
import com.ruoyi.account.pojo.FinanceReconciliationTask;
import java.util.Date;
import java.util.List;
public interface FinanceReconciliationService {
    IPage<FinanceReconciliationTask> pageTask(Page page, FinanceReconciliationTask query);
    FinanceReconciliationTask getTask(Long id);
    List<FinanceReconciliationDiff> listDiff(Long taskId);
    List<FinanceReconciliationExternal> listExternal(Long taskId);
    /** å‘起内部对账:比对 æ”¶æ”¯è®°å½• vs èµ„金流水,自动生成任务与差异 */
    FinanceReconciliationTask generateInternal(Date periodStart, Date periodEnd, Long accountId);
    FinanceReconciliationTask generateExternal(FinanceReconciliationTask task, List<FinanceReconciliationExternal> externals);
    boolean updateHandle(Long taskId, String handleNote, String status);
}
src/main/java/com/ruoyi/account/service/impl/AccountExpenseServiceImpl.java
@@ -64,9 +64,10 @@
        accountDto.setIncomeNumber(incomeNumber);
        BigDecimal totalIncome = accountIncomes.stream().map(AccountDto2::getAccount).reduce(BigDecimal.ZERO, BigDecimal::add);
        accountDto.setTotalIncome(totalIncome);
        accountIncomes.stream().forEach(accountDto2 -> {
            accountDto2.setProportion(accountDto2.getAccount().divide(totalIncome,2,BigDecimal.ROUND_HALF_UP));
        });
        accountIncomes.forEach(accountDto2 -> accountDto2.setProportion(
                totalIncome.compareTo(BigDecimal.ZERO) == 0
                        ? BigDecimal.ZERO
                        : accountDto2.getAccount().divide(totalIncome, 2, BigDecimal.ROUND_HALF_UP)));
        accountDto.setIncomeType(accountIncomes);
        //获取该段时间内的所有支出
        List<AccountDto2> accountExpenses =accountExpenseMapper.report(dateQueryDto);
@@ -76,9 +77,10 @@
        accountDto.setExpenseNumber(expenseNumber);
        BigDecimal totalExpense = accountExpenses.stream().map(AccountDto2::getAccount).reduce(BigDecimal.ZERO, BigDecimal::add);
        accountDto.setTotalExpense(totalExpense);
        accountExpenses.stream().forEach(accountDto2 -> {
            accountDto2.setProportion(accountDto2.getAccount().divide(totalExpense,2,BigDecimal.ROUND_HALF_UP));
        });
        accountExpenses.forEach(accountDto2 -> accountDto2.setProportion(
                totalExpense.compareTo(BigDecimal.ZERO) == 0
                        ? BigDecimal.ZERO
                        : accountDto2.getAccount().divide(totalExpense, 2, BigDecimal.ROUND_HALF_UP)));
        accountDto.setExpenseType(accountExpenses);
        //净收入
        BigDecimal netRevenue = totalIncome.subtract(totalExpense);
@@ -152,6 +154,14 @@
    }
    @Override
    public Map<String, Object> categoryReport(DateQueryDto dateQueryDto) {
        Map<String, Object> result = new HashMap<>();
        result.put("share", accountExpenseMapper.reportByCategory(dateQueryDto));
        result.put("trend", accountExpenseMapper.trendByCategory(dateQueryDto));
        return result;
    }
    @Override
    public AccountExpense getByInvoiceNumber(String purchaseContractNumber) {
        return accountExpenseMapper.selectOne(Wrappers.<AccountExpense>lambdaQuery()
                .eq(AccountExpense::getInvoiceNumber, purchaseContractNumber));
src/main/java/com/ruoyi/account/service/impl/AccountServiceImpl.java
@@ -1,30 +1,19 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.dto.AccountDto3;
import com.ruoyi.account.mapper.AccountIncomeMapper;
import com.ruoyi.account.dto.AccountTransferDto;
import com.ruoyi.account.mapper.AccountMapper;
import com.ruoyi.account.pojo.Account;
import com.ruoyi.account.pojo.AccountIncome;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.account.service.AccountService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.dto.DateQueryDto;
import com.ruoyi.project.system.domain.SysDictData;
import com.ruoyi.project.system.mapper.SysDictDataMapper;
import com.ruoyi.common.exception.ServiceException;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Date;
@AllArgsConstructor
@Service
@@ -34,6 +23,102 @@
    @Override
    public IPage<Account> accountListPage(Page page, Account account) {
        return accountMapper.accountListPage(page,account);
        return accountMapper.accountListPage(page, account);
    }
    /**
     * æµæ°´ç±»åž‹ -> æ”¶æ”¯æ–¹å‘
     * 0收入 +1, 1支出 -1, 3存入 +1, 4取出 -1, 2转账由 transfer() å¤„理
     */
    private Integer resolveDirection(String flowType) {
        if (flowType == null) {
            return null;
        }
        switch (flowType) {
            case "0":
            case "3":
                return 1;
            case "1":
            case "4":
                return -1;
            default:
                return null;
        }
    }
    @Override
    public boolean saveFlow(Account account) {
        if (account.getAccountId() == null) {
            throw new ServiceException("资金账户不能为空");
        }
        if (account.getAmount() == null || account.getAmount().signum() <= 0) {
            throw new ServiceException("流水金额必须大于0");
        }
        if ("2".equals(account.getFlowType())) {
            throw new ServiceException("转账请使用转账接口,以确保生成成对流水");
        }
        if (account.getDirection() == null) {
            account.setDirection(resolveDirection(account.getFlowType()));
        }
        if (account.getDirection() == null) {
            throw new ServiceException("请选择有效的流水类型");
        }
        if (account.getAccountDate() == null) {
            account.setAccountDate(new Date());
        }
        return this.save(account);
    }
    @Override
    public boolean updateFlow(Account account) {
        if (account.getId() == null) throw new ServiceException("流水ID不能为空");
        Account existing = this.getById(account.getId());
        if (existing == null) throw new ServiceException("资金流水不存在");
        if ("2".equals(existing.getFlowType()) || "2".equals(account.getFlowType())) {
            throw new ServiceException("转账成对流水不可单独编辑");
        }
        if (account.getAccountId() == null) throw new ServiceException("资金账户不能为空");
        if (account.getAmount() == null || account.getAmount().signum() <= 0) throw new ServiceException("流水金额必须大于0");
        Integer direction = resolveDirection(account.getFlowType());
        if (direction == null) throw new ServiceException("请选择有效的流水类型");
        account.setDirection(direction);
        return this.updateById(account);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean transfer(AccountTransferDto dto) {
        if (dto.getFromAccountId() == null || dto.getToAccountId() == null) {
            throw new ServiceException("转出/转入账户不能为空");
        }
        if (dto.getFromAccountId().equals(dto.getToAccountId())) {
            throw new ServiceException("转出与转入账户不能相同");
        }
        if (dto.getAmount() == null || dto.getAmount().signum() <= 0) {
            throw new ServiceException("转账金额必须大于0");
        }
        Date date = dto.getTransferDate() == null ? new Date() : dto.getTransferDate();
        Account out = new Account();
        out.setAccountDate(date);
        out.setAccountId(dto.getFromAccountId());
        out.setFlowType("2");
        out.setDirection(-1);
        out.setCounterAccountId(dto.getToAccountId());
        out.setAmount(dto.getAmount());
        out.setRemark(dto.getRemark());
        Account in = new Account();
        in.setAccountDate(date);
        in.setAccountId(dto.getToAccountId());
        in.setFlowType("2");
        in.setDirection(1);
        in.setCounterAccountId(dto.getFromAccountId());
        in.setAmount(dto.getAmount());
        in.setRemark(dto.getRemark());
        this.save(out);
        this.save(in);
        return true;
    }
}
src/main/java/com/ruoyi/account/service/impl/FinanceAccountServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.FinanceAccountMapper;
import com.ruoyi.account.pojo.FinanceAccount;
import com.ruoyi.account.service.FinanceAccountService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
@Service
public class FinanceAccountServiceImpl extends ServiceImpl<FinanceAccountMapper, FinanceAccount> implements FinanceAccountService {
    private FinanceAccountMapper financeAccountMapper;
    @Override
    public IPage<FinanceAccount> pageWithBalance(Page page, FinanceAccount account) {
        return financeAccountMapper.selectAccountPageWithBalance(page, account);
    }
    @Override
    public BigDecimal getBalance(Long accountId) {
        BigDecimal balance = financeAccountMapper.selectBalance(accountId);
        return balance == null ? BigDecimal.ZERO : balance;
    }
    @Override
    public List<Map<String, Object>> trend(Long accountId, String start, String end) {
        return financeAccountMapper.trend(accountId, start, end);
    }
}
src/main/java/com/ruoyi/account/service/impl/FinanceCashflowPlanServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.mapper.FinanceAccountMapper;
import com.ruoyi.account.mapper.FinanceCashflowPlanMapper;
import com.ruoyi.account.pojo.FinanceCashflowPlan;
import com.ruoyi.account.service.FinanceCashflowPlanService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class FinanceCashflowPlanServiceImpl extends ServiceImpl<FinanceCashflowPlanMapper, FinanceCashflowPlan>
        implements FinanceCashflowPlanService {
    @Resource
    private FinanceAccountMapper financeAccountMapper;
    @Override
    public Map<String, Object> forecast(Integer days, BigDecimal safetyLine) {
        int window = days == null ? 30 : Math.max(1, Math.min(days, 30));
        BigDecimal threshold = safetyLine == null ? financeAccountMapper.selectTotalSafetyLine() : safetyLine;
        if (threshold == null) threshold = BigDecimal.ZERO;
        LocalDate today = LocalDate.now();
        LocalDate lastDay = today.plusDays(window - 1L);
        Date begin = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
        Date end = Date.from(lastDay.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
        List<FinanceCashflowPlan> plans = list(new QueryWrapper<FinanceCashflowPlan>()
                .eq("status", "0").ge("plan_date", begin).lt("plan_date", end).orderByAsc("plan_date"));
        BigDecimal currentBalance = financeAccountMapper.selectTotalBalance();
        if (currentBalance == null) currentBalance = BigDecimal.ZERO;
        List<Map<String, Object>> points = new ArrayList<>();
        BigDecimal balance = currentBalance;
        for (int offset = 0; offset < window; offset++) {
            LocalDate date = today.plusDays(offset);
            BigDecimal income = BigDecimal.ZERO;
            BigDecimal expense = BigDecimal.ZERO;
            for (FinanceCashflowPlan plan : plans) {
                LocalDate planDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
                if (!date.equals(planDate)) continue;
                BigDecimal amount = plan.getExpectedAmount() == null ? BigDecimal.ZERO : plan.getExpectedAmount();
                if ("0".equals(plan.getPlanType())) income = income.add(amount); else expense = expense.add(amount);
            }
            balance = balance.add(income).subtract(expense);
            Map<String, Object> point = new LinkedHashMap<>();
            point.put("date", date.toString());
            point.put("income", income);
            point.put("expense", expense);
            point.put("balance", balance);
            point.put("signal", balance.signum() < 0 ? "gap" : (balance.compareTo(threshold) > 0 ? "surplus" : "safe"));
            points.add(point);
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("currentBalance", currentBalance);
        result.put("safetyLine", threshold);
        result.put("days", window);
        result.put("points", points);
        result.put("finalBalance", balance);
        result.put("signal", balance.signum() < 0 ? "gap" : (balance.compareTo(threshold) > 0 ? "surplus" : "safe"));
        return result;
    }
}
src/main/java/com/ruoyi/account/service/impl/FinanceReconciliationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,349 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.mapper.AccountExpenseMapper;
import com.ruoyi.account.mapper.AccountIncomeMapper;
import com.ruoyi.account.mapper.AccountMapper;
import com.ruoyi.account.mapper.FinanceReconciliationDiffMapper;
import com.ruoyi.account.mapper.FinanceReconciliationExternalMapper;
import com.ruoyi.account.mapper.FinanceReconciliationTaskMapper;
import com.ruoyi.account.pojo.Account;
import com.ruoyi.account.pojo.AccountExpense;
import com.ruoyi.account.pojo.AccountIncome;
import com.ruoyi.account.pojo.FinanceReconciliationDiff;
import com.ruoyi.account.pojo.FinanceReconciliationExternal;
import com.ruoyi.account.pojo.FinanceReconciliationTask;
import com.ruoyi.account.service.FinanceReconciliationService;
import com.ruoyi.common.exception.ServiceException;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * è´¢åŠ¡ç®¡ç†--财务对账(内部自动比对:收支记录 vs èµ„金流水)
 */
@AllArgsConstructor
@Service
public class FinanceReconciliationServiceImpl implements FinanceReconciliationService {
    private final FinanceReconciliationTaskMapper taskMapper;
    private final FinanceReconciliationDiffMapper diffMapper;
    private final FinanceReconciliationExternalMapper externalMapper;
    private final AccountIncomeMapper incomeMapper;
    private final AccountExpenseMapper expenseMapper;
    private final AccountMapper accountMapper;
    @Override
    public IPage<FinanceReconciliationTask> pageTask(Page page, FinanceReconciliationTask query) {
        QueryWrapper<FinanceReconciliationTask> w = new QueryWrapper<>();
        if (query != null) {
            w.eq(query.getReconType() != null && !query.getReconType().isEmpty(), "recon_type", query.getReconType());
            w.eq(query.getResult() != null && !query.getResult().isEmpty(), "result", query.getResult());
            w.eq(query.getStatus() != null && !query.getStatus().isEmpty(), "status", query.getStatus());
            String target = query.getTarget();
            if (target != null && !target.trim().isEmpty()) {
                w.like("target", target.trim());
            }
        }
        w.orderByDesc("id");
        return taskMapper.selectPage(page, w);
    }
    @Override
    public FinanceReconciliationTask getTask(Long id) {
        return taskMapper.selectById(id);
    }
    @Override
    public List<FinanceReconciliationDiff> listDiff(Long taskId) {
        return diffMapper.selectList(new QueryWrapper<FinanceReconciliationDiff>()
                .eq("task_id", taskId).orderByAsc("id"));
    }
    @Override
    public List<FinanceReconciliationExternal> listExternal(Long taskId) {
        return externalMapper.selectList(new QueryWrapper<FinanceReconciliationExternal>()
                .eq("task_id", taskId).orderByAsc("ext_date", "id"));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FinanceReconciliationTask generateInternal(Date periodStart, Date periodEnd, Long accountId) {
        if (periodStart == null || periodEnd == null) {
            throw new ServiceException("请选择对账期间");
        }
        if (periodStart.after(periodEnd)) {
            throw new ServiceException("开始日期不能晚于结束日期");
        }
        // 1. æ”¶æ”¯è®°å½•(系统侧)
        List<AccountIncome> incomes = incomeMapper.selectList(new QueryWrapper<AccountIncome>()
                .between("income_date", periodStart, periodEnd));
        List<AccountExpense> expenses = expenseMapper.selectList(new QueryWrapper<AccountExpense>()
                .between("expense_date", periodStart, periodEnd));
        // 2. èµ„金流水:优先关联主键精确匹配,否则金额+日期+往来单位模糊匹配
        QueryWrapper<Account> flowWrapper = new QueryWrapper<>();
        if (accountId != null) {
            flowWrapper.eq("account_id", accountId);
        }
        flowWrapper.between("account_date", periodStart, periodEnd);
        List<Account> flows = accountMapper.selectList(flowWrapper);
        List<FinanceReconciliationDiff> diffs = new ArrayList<>();
        Set<Long> matchedFlowIds = new HashSet<>();
        BigDecimal systemNet = BigDecimal.ZERO;   // æ”¶æ”¯å‡€é¢
        BigDecimal flowNet = BigDecimal.ZERO;      // åŒ¹é…åˆ°çš„æµæ°´å‡€é¢
        // 3. æ”¶å…¥æ¯”对
        for (AccountIncome in : incomes) {
            BigDecimal sys = in.getIncomeMoney() == null ? BigDecimal.ZERO : in.getIncomeMoney();
            systemNet = systemNet.add(sys);
            Account flow = findInternalFlow(flows, matchedFlowIds, "Income", in.getId(), 1,
                    in.getIncomeDate(), in.getCustomerName(), sys);
            if (flow == null) {
                diffs.add(buildDiff("0", "Income", in.getId(), in.getIncomeDate(),
                        in.getCustomerName(), sys, null, "收入记录缺少对应资金流水"));
            } else {
                matchedFlowIds.add(flow.getId());
                BigDecimal flowAmt = flow.getAmount() == null ? BigDecimal.ZERO : flow.getAmount();
                flowNet = flowNet.add(signedAmount(flowAmt, flow.getDirection()));
                if (flowAmt.compareTo(sys) != 0) {
                    diffs.add(buildDiff("2", "Income", in.getId(), in.getIncomeDate(),
                            in.getCustomerName(), sys, flowAmt, "收入流水金额不符"));
                }
            }
        }
        // 4. æ”¯å‡ºæ¯”对
        for (AccountExpense ex : expenses) {
            BigDecimal sys = ex.getExpenseMoney() == null ? BigDecimal.ZERO : ex.getExpenseMoney();
            systemNet = systemNet.subtract(sys);
            Account flow = findInternalFlow(flows, matchedFlowIds, "Expense", ex.getId(), -1,
                    ex.getExpenseDate(), ex.getSupplierName(), sys);
            if (flow == null) {
                diffs.add(buildDiff("0", "Expense", ex.getId(), ex.getExpenseDate(),
                        ex.getSupplierName(), sys, null, "支出记录缺少对应资金流水"));
            } else {
                matchedFlowIds.add(flow.getId());
                BigDecimal flowAmt = flow.getAmount() == null ? BigDecimal.ZERO : flow.getAmount();
                flowNet = flowNet.add(signedAmount(flowAmt, flow.getDirection()));
                if (flowAmt.compareTo(sys) != 0) {
                    diffs.add(buildDiff("2", "Expense", ex.getId(), ex.getExpenseDate(),
                            ex.getSupplierName(), sys, flowAmt, "支出流水金额不符"));
                }
            }
        }
        // æµæ°´ä¾§å­˜åœ¨ã€æ”¶æ”¯ä¾§ä¸å­˜åœ¨
        for (Account flow : flows) {
            if (matchedFlowIds.contains(flow.getId()) || flow.getDirection() == null || "2".equals(flow.getFlowType())) continue;
            flowNet = flowNet.add(signedAmount(flow.getAmount(), flow.getDirection()));
            diffs.add(buildDiff("1", "Flow", flow.getId(), flow.getAccountDate(), flow.getCounterparty(),
                    null, flow.getAmount(), "资金流水缺少对应收支记录"));
        }
        // 5. ç”Ÿæˆå¯¹è´¦ä»»åŠ¡ä¸Žå·®å¼‚æ˜Žç»†
        FinanceReconciliationTask task = new FinanceReconciliationTask();
        task.setReconType("0");
        task.setPeriodStart(periodStart);
        task.setPeriodEnd(periodEnd);
        task.setAccountId(accountId);
        task.setTarget("收支记录 vs èµ„金流水");
        task.setSystemAmount(systemNet);
        task.setExternalAmount(flowNet);
        task.setDiffAmount(systemNet.subtract(flowNet));
        task.setResult(diffs.isEmpty() ? "0" : "1");
        task.setStatus("0");
        taskMapper.insert(task);
        for (FinanceReconciliationDiff d : diffs) {
            d.setTaskId(task.getId());
            diffMapper.insert(d);
        }
        return task;
    }
    private FinanceReconciliationDiff buildDiff(String diffType, String bizType, Long bizId,
                                                Date bizDate, String counterparty,
                                                BigDecimal sysAmt, BigDecimal extAmt, String remark) {
        FinanceReconciliationDiff d = new FinanceReconciliationDiff();
        d.setDiffType(diffType);
        d.setBizType(bizType);
        d.setBizId(bizId);
        d.setBizDate(bizDate);
        d.setCounterparty(counterparty);
        d.setSystemAmount(sysAmt);
        d.setExternalAmount(extAmt);
        d.setRemark(remark);
        return d;
    }
    private Account findInternalFlow(List<Account> flows, Set<Long> matchedIds, String linkType, Long linkId,
                                     Integer direction, Date bizDate, String counterparty, BigDecimal amount) {
        for (Account flow : flows) {
            if (!matchedIds.contains(flow.getId()) && linkType.equals(flow.getLinkType()) && linkId.equals(flow.getLinkId())) {
                return flow;
            }
        }
        for (Account flow : flows) {
            if (matchedIds.contains(flow.getId()) || !direction.equals(flow.getDirection())) continue;
            BigDecimal flowAmount = flow.getAmount() == null ? BigDecimal.ZERO : flow.getAmount();
            if (flowAmount.compareTo(amount) == 0 && sameDay(bizDate, flow.getAccountDate())
                    && sameText(counterparty, flow.getCounterparty())) {
                return flow;
            }
        }
        return null;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FinanceReconciliationTask generateExternal(FinanceReconciliationTask request,
                                                       List<FinanceReconciliationExternal> externals) {
        if (request == null || request.getPeriodStart() == null || request.getPeriodEnd() == null) {
            throw new ServiceException("请选择对账期间");
        }
        if (request.getPeriodStart().after(request.getPeriodEnd())) {
            throw new ServiceException("开始日期不能晚于结束日期");
        }
        if ("0".equals(request.getReconType())) {
            return generateInternal(request.getPeriodStart(), request.getPeriodEnd(), request.getAccountId());
        }
        List<FinanceReconciliationExternal> externalList = externals == null
                ? new ArrayList<>() : externals;
        QueryWrapper<Account> wrapper = new QueryWrapper<Account>()
                .between("account_date", request.getPeriodStart(), request.getPeriodEnd());
        if (request.getAccountId() != null) wrapper.eq("account_id", request.getAccountId());
        if (request.getTarget() != null && !request.getTarget().trim().isEmpty()) {
            wrapper.like("counterparty", request.getTarget().trim());
        }
        List<Account> flows = accountMapper.selectList(wrapper);
        List<FinanceReconciliationDiff> diffs = new ArrayList<>();
        Set<Integer> matched = new HashSet<>();
        BigDecimal systemAmount = BigDecimal.ZERO;
        BigDecimal externalAmount = BigDecimal.ZERO;
        for (Account flow : flows) {
            BigDecimal signedFlow = signedAmount(flow.getAmount(), flow.getDirection());
            systemAmount = systemAmount.add(signedFlow);
            int index = findExternal(flow, externalList, matched);
            if (index < 0) {
                diffs.add(buildDiff("0", "Flow", flow.getId(), flow.getAccountDate(), flow.getCounterparty(),
                        signedFlow, null, "系统流水在外部凭证中未找到"));
            } else {
                matched.add(index);
                FinanceReconciliationExternal ext = externalList.get(index);
                BigDecimal signedExternal = signedAmount(ext.getAmount(), ext.getDirection());
                externalAmount = externalAmount.add(signedExternal);
                if (signedFlow.compareTo(signedExternal) != 0) {
                    diffs.add(buildDiff("2", "Flow", flow.getId(), flow.getAccountDate(), flow.getCounterparty(),
                            signedFlow, signedExternal, "系统与外部凭证金额不符"));
                }
            }
        }
        for (int i = 0; i < externalList.size(); i++) {
            if (matched.contains(i)) continue;
            FinanceReconciliationExternal ext = externalList.get(i);
            BigDecimal signedExternal = signedAmount(ext.getAmount(), ext.getDirection());
            externalAmount = externalAmount.add(signedExternal);
            diffs.add(buildDiff("1", "External", null, ext.getExtDate(), ext.getExtCounterparty(),
                    null, signedExternal, "外部凭证在系统流水中未找到"));
        }
        FinanceReconciliationTask task = new FinanceReconciliationTask();
        task.setReconType(request.getReconType());
        task.setPeriodStart(request.getPeriodStart());
        task.setPeriodEnd(request.getPeriodEnd());
        task.setAccountId(request.getAccountId());
        task.setTarget(request.getTarget());
        task.setSystemAmount(systemAmount);
        task.setExternalAmount(externalAmount);
        task.setDiffAmount(systemAmount.subtract(externalAmount));
        task.setResult(diffs.isEmpty() ? "0" : "1");
        task.setStatus("0");
        taskMapper.insert(task);
        for (FinanceReconciliationDiff diff : diffs) {
            diff.setTaskId(task.getId());
            diffMapper.insert(diff);
        }
        for (FinanceReconciliationExternal ext : externalList) {
            ext.setId(null);
            ext.setTaskId(task.getId());
            externalMapper.insert(ext);
        }
        return task;
    }
    private int findExternal(Account flow, List<FinanceReconciliationExternal> externals, Set<Integer> matched) {
        int amountMismatchCandidate = -1;
        for (int i = 0; i < externals.size(); i++) {
            if (matched.contains(i)) continue;
            FinanceReconciliationExternal ext = externals.get(i);
            if (sameDay(flow.getAccountDate(), ext.getExtDate())
                    && sameText(flow.getCounterparty(), ext.getExtCounterparty())) {
                if (signedAmount(flow.getAmount(), flow.getDirection())
                        .compareTo(signedAmount(ext.getAmount(), ext.getDirection())) == 0) {
                    return i;
                }
                if (amountMismatchCandidate < 0) amountMismatchCandidate = i;
            }
        }
        return amountMismatchCandidate;
    }
    private BigDecimal signedAmount(BigDecimal amount, Integer direction) {
        return (amount == null ? BigDecimal.ZERO : amount)
                .multiply(BigDecimal.valueOf(direction == null ? 1 : direction));
    }
    private boolean sameDay(Date left, Date right) {
        if (left == null || right == null) return false;
        Calendar a = Calendar.getInstance();
        Calendar b = Calendar.getInstance();
        a.setTime(left);
        b.setTime(right);
        return a.get(Calendar.ERA) == b.get(Calendar.ERA)
                && a.get(Calendar.YEAR) == b.get(Calendar.YEAR)
                && a.get(Calendar.DAY_OF_YEAR) == b.get(Calendar.DAY_OF_YEAR);
    }
    private boolean sameText(String left, String right) {
        if (left == null || left.trim().isEmpty()) return right == null || right.trim().isEmpty();
        return right != null && left.trim().equalsIgnoreCase(right.trim());
    }
    @Override
    public boolean updateHandle(Long taskId, String handleNote, String status) {
        FinanceReconciliationTask task = taskMapper.selectById(taskId);
        if (task == null) {
            throw new ServiceException("对账任务不存在");
        }
        if ("2".equals(task.getStatus())) {
            throw new ServiceException("已归档的对账任务不可修改");
        }
        if (handleNote != null) {
            task.setHandleNote(handleNote);
        }
        if (status != null && !status.isEmpty()) {
            if ("2".equals(status) && "1".equals(task.getResult())
                    && (task.getHandleNote() == null || task.getHandleNote().trim().isEmpty())) {
                throw new ServiceException("存在对账差异,请先填写处理方案再归档");
            }
            task.setStatus(status);
        }
        return taskMapper.updateById(task) > 0;
    }
}
src/main/resources/mapper/account/AccountExpenseMapper.xml
@@ -16,6 +16,9 @@
        <if test="accountExpense.expenseType != null and accountExpense.expenseType != '' ">
            AND expense_type = #{accountExpense.expenseType}
        </if>
        <if test="accountExpense.expenseCategory != null and accountExpense.expenseCategory != '' ">
            AND expense_category = #{accountExpense.expenseCategory}
        </if>
        <if test="accountExpense.expenseMethod != null and accountExpense.expenseMethod != '' ">
            AND expense_method = #{accountExpense.expenseMethod}
        </if>
@@ -35,6 +38,9 @@
        <if test="accountExpense.expenseType != null and accountExpense.expenseType != '' ">
            AND expense_type = #{accountExpense.expenseType}
        </if>
        <if test="accountExpense.expenseCategory != null and accountExpense.expenseCategory != '' ">
            AND expense_category = #{accountExpense.expenseCategory}
        </if>
        <if test="accountExpense.expenseMethod != null and accountExpense.expenseMethod != '' ">
            AND expense_method = #{accountExpense.expenseMethod}
        </if>
@@ -53,7 +59,7 @@
        <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != '' ">
            AND expense_date &lt;= DATE_FORMAT(#{dateQueryDto.entryDateEnd},'%Y-%m-%d')
        </if>
        group by expense_type
        group by ae.expense_type, sdd.dict_label
    </select>
    <select id="report1" resultType="java.math.BigDecimal">
@@ -70,4 +76,40 @@
        </if>
    </select>
    <select id="reportByCategory" resultType="java.util.Map">
        SELECT COALESCE(sdd.dict_label, '未分类') AS categoryName,
               ae.expense_category AS categoryValue,
               SUM(ae.expense_money) AS amount
        FROM account_expense ae
        LEFT JOIN sys_dict_data sdd ON ae.expense_category = sdd.dict_value
          AND sdd.dict_type = 'finance_expense_category'
        WHERE 1=1
        <if test="dateQueryDto.entryDateStart != null and dateQueryDto.entryDateStart != ''">
            AND ae.expense_date &gt;= DATE_FORMAT(#{dateQueryDto.entryDateStart},'%Y-%m-%d')
        </if>
        <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != ''">
            AND ae.expense_date &lt;= DATE_FORMAT(#{dateQueryDto.entryDateEnd},'%Y-%m-%d')
        </if>
        GROUP BY ae.expense_category, sdd.dict_label
    </select>
    <select id="trendByCategory" resultType="java.util.Map">
        SELECT DATE_FORMAT(ae.expense_date, '%Y-%m') AS month,
               ae.expense_category AS categoryValue,
               COALESCE(sdd.dict_label, '未分类') AS categoryName,
               SUM(ae.expense_money) AS amount
        FROM account_expense ae
        LEFT JOIN sys_dict_data sdd ON ae.expense_category = sdd.dict_value
          AND sdd.dict_type = 'finance_expense_category'
        WHERE 1=1
        <if test="dateQueryDto.entryDateStart != null and dateQueryDto.entryDateStart != ''">
            AND ae.expense_date &gt;= DATE_FORMAT(#{dateQueryDto.entryDateStart},'%Y-%m-%d')
        </if>
        <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != ''">
            AND ae.expense_date &lt;= DATE_FORMAT(#{dateQueryDto.entryDateEnd},'%Y-%m-%d')
        </if>
        GROUP BY DATE_FORMAT(ae.expense_date, '%Y-%m'), ae.expense_category, sdd.dict_label
        ORDER BY month, ae.expense_category
    </select>
</mapper>
src/main/resources/mapper/account/AccountIncomeMapper.xml
@@ -54,7 +54,7 @@
        <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != '' ">
            AND income_date &lt;= DATE_FORMAT(#{dateQueryDto.entryDateEnd},'%Y-%m-%d')
        </if>
        group by income_type
        group by ai.income_type, sdd.dict_label
    </select>
    <select id="report1" resultType="java.math.BigDecimal">
        SELECT
src/main/resources/mapper/account/AccountMapper.xml
@@ -2,13 +2,28 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.ruoyi.account.mapper.AccountMapper">
    <select id="accountListPage" resultType="com.ruoyi.account.pojo.Account">
        select * from account
        select a.*, fa.account_name as accountName
        from account a
        left join finance_account fa on a.account_id = fa.id
        where 1=1
        <if test="c.counterparty !='' and c.counterparty!=null">
            and counterparty = #{c.counterparty}
            and a.counterparty like concat('%', #{c.counterparty}, '%')
        </if>
        <if test="c.typeText !='' and c.typeText!=null">
            and type_text = #{c.typeText}
            and a.type_text = #{c.typeText}
        </if>
        <if test="c.accountId != null">
            and a.account_id = #{c.accountId}
        </if>
        <if test="c.flowType !='' and c.flowType!=null">
            and a.flow_type = #{c.flowType}
        </if>
        <if test="c.entryDateStart != null and c.entryDateStart != ''">
            and a.account_date &gt;= DATE_FORMAT(#{c.entryDateStart},'%Y-%m-%d')
        </if>
        <if test="c.entryDateEnd != null and c.entryDateEnd != ''">
            and a.account_date &lt;= DATE_FORMAT(#{c.entryDateEnd},'%Y-%m-%d')
        </if>
        order by a.account_date desc, a.id desc
    </select>
</mapper>
src/main/resources/mapper/account/FinanceAccountMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.ruoyi.account.mapper.FinanceAccountMapper">
    <select id="selectAccountPageWithBalance" resultType="com.ruoyi.account.pojo.FinanceAccount">
        SELECT fa.*,
               (fa.opening_balance + IFNULL(
                    (SELECT SUM(a.direction * a.amount) FROM account a WHERE a.account_id = fa.id), 0)
               ) AS currentBalance
        FROM finance_account fa
        WHERE 1=1
        <if test="c.accountName != null and c.accountName != ''">
            AND fa.account_name LIKE concat('%', #{c.accountName}, '%')
        </if>
        <if test="c.accountType != null and c.accountType != ''">
            AND fa.account_type = #{c.accountType}
        </if>
        <if test="c.status != null and c.status != ''">
            AND fa.status = #{c.status}
        </if>
        ORDER BY fa.id DESC
    </select>
    <select id="selectBalance" resultType="java.math.BigDecimal">
        SELECT IFNULL(
            (SELECT opening_balance FROM finance_account WHERE id = #{accountId}), 0)
            + IFNULL(
            (SELECT SUM(a.direction * a.amount) FROM account a WHERE a.account_id = #{accountId}), 0)
    </select>
    <select id="selectTotalBalance" resultType="java.math.BigDecimal">
        SELECT IFNULL(SUM(fa.opening_balance), 0) + IFNULL(
            (SELECT SUM(a.direction * a.amount)
             FROM account a INNER JOIN finance_account x ON x.id = a.account_id
             WHERE x.status = '0'), 0)
        FROM finance_account fa
        WHERE fa.status = '0'
    </select>
    <select id="selectTotalSafetyLine" resultType="java.math.BigDecimal">
        SELECT IFNULL(SUM(safety_line), 0) FROM finance_account WHERE status = '0'
    </select>
    <select id="trend" resultType="java.util.Map">
        SELECT
            DATE_FORMAT(a.account_date, '%Y-%m-%d') AS `date`,
            SUM(CASE WHEN a.direction &gt; 0 THEN a.amount ELSE 0 END) AS income,
            SUM(CASE WHEN a.direction &lt; 0 THEN a.amount ELSE 0 END) AS expense,
            SUM(a.direction * a.amount) AS net
        FROM account a
        WHERE 1=1
        <if test="accountId != null">
            AND a.account_id = #{accountId}
        </if>
        <if test="start != null and start != ''">
            AND a.account_date &gt;= DATE_FORMAT(#{start},'%Y-%m-%d')
        </if>
        <if test="end != null and end != ''">
            AND a.account_date &lt;= DATE_FORMAT(#{end},'%Y-%m-%d')
        </if>
        GROUP BY DATE_FORMAT(a.account_date, '%Y-%m-%d')
        ORDER BY `date` ASC
    </select>
</mapper>