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 <= 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 >= DATE_FORMAT(#{dateQueryDto.entryDateStart},'%Y-%m-%d') </if> <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != ''"> AND ae.expense_date <= 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 >= DATE_FORMAT(#{dateQueryDto.entryDateStart},'%Y-%m-%d') </if> <if test="dateQueryDto.entryDateEnd != null and dateQueryDto.entryDateEnd != ''"> AND ae.expense_date <= 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 <= 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 >= DATE_FORMAT(#{c.entryDateStart},'%Y-%m-%d') </if> <if test="c.entryDateEnd != null and c.entryDateEnd != ''"> and a.account_date <= 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 > 0 THEN a.amount ELSE 0 END) AS income, SUM(CASE WHEN a.direction < 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 >= DATE_FORMAT(#{start},'%Y-%m-%d') </if> <if test="end != null and end != ''"> AND a.account_date <= DATE_FORMAT(#{end},'%Y-%m-%d') </if> GROUP BY DATE_FORMAT(a.account_date, '%Y-%m-%d') ORDER BY `date` ASC </select> </mapper>