package com.ruoyi.account.service.impl.financial; 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.bean.dto.financial.FinVoucherDto; import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto; import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto; import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo; import com.ruoyi.account.mapper.AccountSubjectMapper; import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper; import com.ruoyi.account.mapper.financial.FinVoucherMapper; import com.ruoyi.account.pojo.AccountSubject; import com.ruoyi.account.pojo.financial.FinVoucher; import com.ruoyi.account.pojo.financial.FinVoucherEntry; import com.ruoyi.account.service.financial.FinVoucherService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.stream.Collectors; /** * 凭证服务实现。 */ @Service @RequiredArgsConstructor public class FinVoucherServiceImpl extends ServiceImpl implements FinVoucherService { private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); private final FinVoucherEntryMapper finVoucherEntryMapper; private final AccountSubjectMapper accountSubjectMapper; @Override public IPage pageList(Page page, FinVoucherPageDto queryDto) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (queryDto != null && StringUtils.isNotEmpty(queryDto.getVoucherNo())) { wrapper.like(FinVoucher::getVoucherNo, queryDto.getVoucherNo()); } if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCreator())) { wrapper.eq(FinVoucher::getCreator, queryDto.getCreator()); } if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) { wrapper.eq(FinVoucher::getStatus, queryDto.getStatus()); } if (queryDto != null && queryDto.getStartDate() != null) { wrapper.ge(FinVoucher::getVoucherDate, queryDto.getStartDate()); } if (queryDto != null && queryDto.getEndDate() != null) { wrapper.le(FinVoucher::getVoucherDate, queryDto.getEndDate()); } wrapper.orderByDesc(FinVoucher::getVoucherDate).orderByDesc(FinVoucher::getId); return page(page, wrapper); } @Override @Transactional(rollbackFor = Exception.class) public Boolean addVoucher(FinVoucherDto dto) { validateVoucherBasicInfo(dto, false); List validEntries = buildAndValidateEntries(dto); FinVoucher voucher = new FinVoucher(); BeanUtils.copyProperties(dto, voucher); voucher.setStatus("unposted"); voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount()); BigDecimal totalDebit = calculateTotalDebit(validEntries); BigDecimal totalCredit = calculateTotalCredit(validEntries); voucher.setDebit(totalDebit); voucher.setCredit(totalCredit); if (StringUtils.isEmpty(voucher.getSummary())) { voucher.setSummary(findDefaultSummary(validEntries)); } save(voucher); saveEntries(voucher.getId(), validEntries); return true; } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateVoucher(FinVoucherDto dto) { validateVoucherBasicInfo(dto, true); FinVoucher existed = getById(dto.getId()); if (existed == null) { throw new ServiceException("修改失败,凭证不存在"); } if (!"unposted".equals(existed.getStatus())) { throw new ServiceException("仅未过账凭证允许修改"); } List validEntries = buildAndValidateEntries(dto); FinVoucher voucher = new FinVoucher(); BeanUtils.copyProperties(dto, voucher); voucher.setStatus(existed.getStatus()); voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount()); BigDecimal totalDebit = calculateTotalDebit(validEntries); BigDecimal totalCredit = calculateTotalCredit(validEntries); voucher.setDebit(totalDebit); voucher.setCredit(totalCredit); if (StringUtils.isEmpty(voucher.getSummary())) { voucher.setSummary(findDefaultSummary(validEntries)); } updateById(voucher); LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); deleteWrapper.eq(FinVoucherEntry::getVoucherId, voucher.getId()); finVoucherEntryMapper.delete(deleteWrapper); saveEntries(voucher.getId(), validEntries); return true; } @Override @Transactional(rollbackFor = Exception.class) public Boolean postVoucher(Long id) { FinVoucher voucher = getById(id); if (voucher == null) { throw new ServiceException("过账失败,凭证不存在"); } if (!"unposted".equals(voucher.getStatus())) { throw new ServiceException("仅未过账凭证允许过账"); } voucher.setStatus("posted"); return updateById(voucher); } @Override @Transactional(rollbackFor = Exception.class) public Boolean cancelVoucher(Long id) { FinVoucher voucher = getById(id); if (voucher == null) { throw new ServiceException("作废失败,凭证不存在"); } if (!"unposted".equals(voucher.getStatus())) { throw new ServiceException("仅未过账凭证允许作废"); } voucher.setStatus("cancelled"); return updateById(voucher); } @Override public FinVoucherDetailVo detail(Long id) { FinVoucher voucher = getById(id); if (voucher == null) { throw new ServiceException("查询失败,凭证不存在"); } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(FinVoucherEntry::getVoucherId, id) .orderByAsc(FinVoucherEntry::getRowNo) .orderByAsc(FinVoucherEntry::getId); List entries = finVoucherEntryMapper.selectList(wrapper); FinVoucherDetailVo vo = new FinVoucherDetailVo(); BeanUtils.copyProperties(voucher, vo); vo.setEntries(entries); return vo; } /** * 校验凭证主表字段、状态字段与唯一性。 */ private void validateVoucherBasicInfo(FinVoucherDto dto, boolean isUpdate) { if (dto == null) { throw new ServiceException("凭证数据不能为空"); } if (isUpdate && dto.getId() == null) { throw new ServiceException("修改失败,凭证ID不能为空"); } if (StringUtils.isEmpty(dto.getVoucherNo())) { throw new ServiceException("凭证字号不能为空"); } if (dto.getVoucherDate() == null) { throw new ServiceException("凭证日期不能为空"); } LambdaQueryWrapper uniqueWrapper = new LambdaQueryWrapper<>(); uniqueWrapper.eq(FinVoucher::getVoucherNo, dto.getVoucherNo()); if (isUpdate) { uniqueWrapper.ne(FinVoucher::getId, dto.getId()); } if (count(uniqueWrapper) > 0) { throw new ServiceException("凭证字号已存在,请勿重复提交"); } } /** * 过滤有效分录并执行借贷平衡校验。 */ private List buildAndValidateEntries(FinVoucherDto dto) { List rawEntries = dto.getEntries(); if (rawEntries == null || rawEntries.isEmpty()) { throw new ServiceException("分录不能为空,至少需要一条有效分录"); } List validEntries = new ArrayList<>(); int rowNo = 1; for (FinVoucherEntryDto entryDto : rawEntries) { if (entryDto == null || StringUtils.isEmpty(entryDto.getSubjectCode())) { continue; } BigDecimal debit = defaultMoney(entryDto.getDebit()); BigDecimal credit = defaultMoney(entryDto.getCredit()); if (debit.compareTo(BigDecimal.ZERO) <= 0 && credit.compareTo(BigDecimal.ZERO) <= 0) { continue; } if (debit.compareTo(BigDecimal.ZERO) > 0 && credit.compareTo(BigDecimal.ZERO) > 0) { throw new ServiceException("分录借方和贷方不能同时大于0"); } FinVoucherEntry entry = new FinVoucherEntry(); BeanUtils.copyProperties(entryDto, entry); entry.setDebit(debit); entry.setCredit(credit); entry.setRowNo(rowNo++); validEntries.add(entry); } if (validEntries.isEmpty()) { throw new ServiceException("分录至少需要一条有效行(科目不空,且借方或贷方大于0)"); } // 分录科目必须存在,避免脏科目编码入账。 Set subjectCodes = validEntries.stream() .map(FinVoucherEntry::getSubjectCode) .filter(StringUtils::isNotEmpty) .collect(Collectors.toSet()); if (subjectCodes.isEmpty()) { throw new ServiceException("分录科目不能为空"); } LambdaQueryWrapper subjectWrapper = new LambdaQueryWrapper<>(); subjectWrapper.in(AccountSubject::getSubjectCode, subjectCodes); List subjects = accountSubjectMapper.selectList(subjectWrapper); Map subjectMap = subjects.stream() .collect(Collectors.toMap(AccountSubject::getSubjectCode, it -> it, (a, b) -> a)); for (FinVoucherEntry entry : validEntries) { AccountSubject accountSubject = subjectMap.get(entry.getSubjectCode()); if (accountSubject == null) { throw new ServiceException("科目编码不存在:" + entry.getSubjectCode()); } if (StringUtils.isEmpty(entry.getSubjectName())) { entry.setSubjectName(accountSubject.getSubjectName()); } } BigDecimal totalDebit = calculateTotalDebit(validEntries); BigDecimal totalCredit = calculateTotalCredit(validEntries); if (totalDebit.compareTo(BigDecimal.ZERO) <= 0 || totalCredit.compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("借贷金额必须大于0"); } if (totalDebit.compareTo(totalCredit) != 0) { throw new ServiceException("借贷不平衡,禁止保存"); } return validEntries; } private void saveEntries(Long voucherId, List entries) { if (voucherId == null) { throw new ServiceException("凭证ID不能为空"); } for (FinVoucherEntry entry : entries) { entry.setVoucherId(voucherId); finVoucherEntryMapper.insert(entry); } } private String findDefaultSummary(List entries) { for (FinVoucherEntry entry : entries) { if (StringUtils.isNotEmpty(entry.getSummary())) { return entry.getSummary(); } } return ""; } private BigDecimal calculateTotalDebit(List entries) { BigDecimal total = BigDecimal.ZERO; for (FinVoucherEntry entry : entries) { total = total.add(defaultMoney(entry.getDebit())); } return total.setScale(2, RoundingMode.HALF_UP); } private BigDecimal calculateTotalCredit(List entries) { BigDecimal total = BigDecimal.ZERO; for (FinVoucherEntry entry : entries) { total = total.add(defaultMoney(entry.getCredit())); } return total.setScale(2, RoundingMode.HALF_UP); } private BigDecimal defaultMoney(BigDecimal value) { if (value == null) { return ZERO; } return value.setScale(2, RoundingMode.HALF_UP); } }