package cn.iocoder.yudao.module.finance.service.ledger; import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerReqVO; import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerRespVO; import cn.iocoder.yudao.module.finance.dal.dataobject.ledger.FinLedgerEntryDO; import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO; import cn.iocoder.yudao.module.finance.dal.mysql.ledger.FinLedgerMapper; import cn.iocoder.yudao.module.finance.enums.FinBalanceDirectionEnum; import cn.iocoder.yudao.module.finance.enums.FinLedgerRowTypeEnum; import cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum; import cn.iocoder.yudao.module.finance.service.subject.FinAccountSubjectService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDate; import java.time.YearMonth; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_MONTH_RANGE_INVALID; import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_SUBJECT_NOT_EXISTS; /** * 总账 / 明细账 Service 实现类 * * 口径说明: * 1. 只统计已过账(posted)的凭证,未过账、已作废的凭证不计入账簿; * 2. 余额一律「借方正、贷方负」,期初余额 = 起始月份之前所有已过账分录的借贷净额; * 3. 科目的下级科目由 parentId 递归收集(不使用科目编码前缀匹配,避免把形近编码误算进来)。 * * @author 超级管理员 */ @Service @Validated public class FinLedgerServiceImpl implements FinLedgerService { @Resource private FinLedgerMapper ledgerMapper; @Resource private FinAccountSubjectService accountSubjectService; @Override public List getGeneralLedgerList(FinLedgerReqVO reqVO) { // 1. 校验科目与期间 FinAccountSubjectDO subject = validateSubject(reqVO); LocalDate startDate = reqVO.getStartMonth().atDay(1); LocalDate endDate = reqVO.getEndMonth().atEndOfMonth(); validateMonthRange(startDate, endDate); Set subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren()); // 2. 期初余额 List rows = new ArrayList<>(); BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate, FinVoucherStatusEnum.POSTED.getStatus())); rows.add(buildOpeningRow(subject, startDate, balance)); // 3. 按月汇总 List entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate, FinVoucherStatusEnum.POSTED.getStatus()); Map> monthMap = entries.stream() .collect(Collectors.groupingBy(entry -> YearMonth.from(entry.getVoucherDate()).toString())); BigDecimal totalDebit = BigDecimal.ZERO; BigDecimal totalCredit = BigDecimal.ZERO; YearMonth endMonth = YearMonth.from(endDate); for (YearMonth month = YearMonth.from(startDate); !month.isAfter(endMonth); month = month.plusMonths(1)) { List monthEntries = monthMap.getOrDefault(month.toString(), List.of()); BigDecimal debit = sum(monthEntries, true); BigDecimal credit = sum(monthEntries, false); balance = balance.add(debit).subtract(credit); totalDebit = totalDebit.add(debit); totalCredit = totalCredit.add(credit); LocalDate monthEndDate = minDate(month.atEndOfMonth(), endDate); rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, month.toString(), monthEndDate, subject, null, FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), debit, credit, balance)); } // 4. 本期累计 rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null, FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance)); return rows; } @Override public List getDetailLedgerList(FinLedgerReqVO reqVO) { // 1. 校验科目与期间 FinAccountSubjectDO subject = validateSubject(reqVO); LocalDate startDate = reqVO.getStartMonth().atDay(1); LocalDate endDate = reqVO.getEndMonth().atEndOfMonth(); validateMonthRange(startDate, endDate); Set subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren()); // 2. 期初余额 List rows = new ArrayList<>(); BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate, FinVoucherStatusEnum.POSTED.getStatus())); rows.add(buildOpeningRow(subject, startDate, balance)); // 3. 逐条明细,并对每条分录结转余额;每逢月末补一行「本月合计」 List entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate, FinVoucherStatusEnum.POSTED.getStatus()); BigDecimal totalDebit = BigDecimal.ZERO; BigDecimal totalCredit = BigDecimal.ZERO; BigDecimal monthDebit = BigDecimal.ZERO; BigDecimal monthCredit = BigDecimal.ZERO; YearMonth currentMonth = null; for (FinLedgerEntryDO entry : entries) { YearMonth month = YearMonth.from(entry.getVoucherDate()); // 3.1 跨月:先补上个月的本月合计 if (currentMonth != null && !currentMonth.equals(month)) { rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(), currentMonth.atEndOfMonth(), subject, null, FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), monthDebit, monthCredit, balance)); monthDebit = BigDecimal.ZERO; monthCredit = BigDecimal.ZERO; } currentMonth = month; // 3.2 明细行 BigDecimal debit = defaultIfNull(entry.getDebit()); BigDecimal credit = defaultIfNull(entry.getCredit()); balance = balance.add(debit).subtract(credit); monthDebit = monthDebit.add(debit); monthCredit = monthCredit.add(credit); totalDebit = totalDebit.add(debit); totalCredit = totalCredit.add(credit); rows.add(buildRow(FinLedgerRowTypeEnum.ENTRY, month.toString(), entry.getVoucherDate(), subject, entry.getVoucherNo(), entry.getSummary(), debit, credit, balance) .setSubjectCode(entry.getSubjectCode()).setSubjectName(entry.getSubjectName())); } // 4. 最后一个月的本月合计 if (currentMonth != null) { rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(), minDate(currentMonth.atEndOfMonth(), endDate), subject, null, FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), monthDebit, monthCredit, balance)); } // 5. 本期累计 rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null, FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance)); return rows; } private FinAccountSubjectDO validateSubject(FinLedgerReqVO reqVO) { FinAccountSubjectDO subject = accountSubjectService.getAccountSubjectByCode(reqVO.getSubjectCode()); if (subject == null) { throw exception(LEDGER_SUBJECT_NOT_EXISTS, reqVO.getSubjectCode()); } return subject; } private void validateMonthRange(LocalDate startDate, LocalDate endDate) { if (startDate.isAfter(endDate)) { throw exception(LEDGER_MONTH_RANGE_INVALID, YearMonth.from(startDate), YearMonth.from(endDate)); } } /** * 收集查询用的科目编码集合:默认包含所有下级科目 */ private Set buildSubjectCodeSet(FinAccountSubjectDO subject, Boolean includeChildren) { Set subjectCodes = new HashSet<>(); subjectCodes.add(subject.getSubjectCode()); if (!Boolean.TRUE.equals(includeChildren)) { return subjectCodes; } // 用 parentId 递归收集下级,visited 由 subjectCodes 的 add 结果兼作防护,避免脏数据成环时死循环 Map> childrenMap = accountSubjectService.getAccountSubjectList().stream() .filter(item -> item.getParentId() != null) .collect(Collectors.groupingBy(FinAccountSubjectDO::getParentId)); Deque queue = new ArrayDeque<>(); queue.add(subject.getId()); while (!queue.isEmpty()) { for (FinAccountSubjectDO child : childrenMap.getOrDefault(queue.poll(), List.of())) { if (subjectCodes.add(child.getSubjectCode())) { queue.add(child.getId()); } } } return subjectCodes; } private FinLedgerRespVO buildOpeningRow(FinAccountSubjectDO subject, LocalDate date, BigDecimal balance) { return buildRow(FinLedgerRowTypeEnum.OPENING, YearMonth.from(date).toString(), date, subject, null, FinLedgerRowTypeEnum.OPENING.getName(), null, null, balance); } private FinLedgerRespVO buildRow(FinLedgerRowTypeEnum rowType, String period, LocalDate date, FinAccountSubjectDO subject, String voucherNo, String summary, BigDecimal debit, BigDecimal credit, BigDecimal balance) { return new FinLedgerRespVO() .setRowType(rowType.getType()) .setRowTypeName(rowType.getName()) .setPeriod(period) .setDate(date) .setVoucherNo(voucherNo) .setSubjectCode(subject.getSubjectCode()) .setSubjectName(subject.getSubjectName()) .setSummary(summary) .setDebit(debit) .setCredit(credit) .setDirection(FinBalanceDirectionEnum.getByBalance(balance)) .setBalance(balance); } private static BigDecimal sum(List entries, boolean debit) { return entries.stream() .map(entry -> debit ? defaultIfNull(entry.getDebit()) : defaultIfNull(entry.getCredit())) .reduce(BigDecimal.ZERO, BigDecimal::add); } private static BigDecimal defaultIfNull(BigDecimal amount) { return amount == null ? BigDecimal.ZERO : amount; } private static LocalDate minDate(LocalDate date, LocalDate maxDate) { return date.isAfter(maxDate) ? maxDate : date; } }