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 pageTask(Page page, FinanceReconciliationTask query) { QueryWrapper 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 listDiff(Long taskId) { return diffMapper.selectList(new QueryWrapper() .eq("task_id", taskId).orderByAsc("id")); } @Override public List listExternal(Long taskId) { return externalMapper.selectList(new QueryWrapper() .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 incomes = incomeMapper.selectList(new QueryWrapper() .between("income_date", periodStart, periodEnd)); List expenses = expenseMapper.selectList(new QueryWrapper() .between("expense_date", periodStart, periodEnd)); // 2. 资金流水:优先关联主键精确匹配,否则金额+日期+往来单位模糊匹配 QueryWrapper flowWrapper = new QueryWrapper<>(); if (accountId != null) { flowWrapper.eq("account_id", accountId); } flowWrapper.between("account_date", periodStart, periodEnd); List flows = accountMapper.selectList(flowWrapper); List diffs = new ArrayList<>(); Set 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 flows, Set 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 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 externalList = externals == null ? new ArrayList<>() : externals; QueryWrapper wrapper = new QueryWrapper() .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 flows = accountMapper.selectList(wrapper); List diffs = new ArrayList<>(); Set 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 externals, Set 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; } }