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;
|
}
|
}
|