package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.approve.service.*;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.project.system.service.ISysNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
*
* 报销单主表 服务实现类
*
*
* @author 芯导软件(江苏)有限公司
* @since 2026-05-21 09:56:15
*/
@Service
@RequiredArgsConstructor
public class FinReimbursementServiceImpl extends ServiceImpl implements FinReimbursementService {
private static final String BILL_STATUS_DRAFT = "DRAFT";
private static final String BILL_STATUS_IN_APPROVAL = "IN_APPROVAL";
private static final String NODE_STATUS_WAITING = "WAITING";
private final ApprovalInstanceMapper approvalInstanceMapper;
private final ApprovalInstanceService approvalInstanceService;
private final ApprovalInstanceNodeService approvalInstanceNodeService;
private final ApprovalTaskService approvalTaskService;
private final ApprovalRecordService approvalRecordService;
private final FinReimbursementMapper finReimbursementMapper;
private final FinReimbursementTravelMapper finReimbursementTravelMapper;
private final FinReimbursementDetailMapper finReimbursementDetailMapper;
private final FileUtil fileUtil;
private final ISysNoticeService sysNoticeService;
@Override
public IPage listPage(FinReimbursementDto finReimbursementDto, Page page) {
IPage finReimbursementVoIPage = finReimbursementMapper.listPage(finReimbursementDto, page);
finReimbursementVoIPage.getRecords().forEach(vo -> {
vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, vo.getId()));
});
return finReimbursementVoIPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean add(FinReimbursementDto finReimbursementDto) {
String billStatus = validateAddParam(finReimbursementDto);
// 生成报销单号
String billNo = OrderUtils.countTodayByCreateTime(finReimbursementMapper, "BXD", "bill_no");
List details = finReimbursementDto.getDetails();
BigDecimal totalAmount = details.stream()
.map(FinReimbursementDetail::getAmount)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
FinReimbursement reimbursement = buildReimbursement(finReimbursementDto, billNo, totalAmount, billStatus);
// 保存报销单主表
boolean saved = this.save(reimbursement);
if (!saved || reimbursement.getId() == null) {
throw new ServiceException("新增报销单失败");
}
Long reimbursementId = reimbursement.getId();
// 保存差旅报销扩展信息(报销类型为差旅报销时)
FinReimbursementTravel travel = finReimbursementDto.getTravel();
if (isTravelReimbursement(finReimbursementDto.getReimbursementType())) {
travel.setReimbursementId(reimbursementId);
int travelRows = finReimbursementTravelMapper.insert(travel);
if (travelRows != 1) {
throw new ServiceException("新增差旅报销扩展信息失败");
}
}
// 保存报销单明细
for (int i = 0; i < details.size(); i++) {
FinReimbursementDetail detail = details.get(i);
detail.setId(null);
detail.setReimbursementId(reimbursementId);
detail.setRowNo(i + 1);
int detailRows = finReimbursementDetailMapper.insert(detail);
if (detailRows != 1) {
throw new ServiceException("新增报销单明细失败");
}
}
if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
startApproval(reimbursement, finReimbursementDto);
}
fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean update(FinReimbursementDto finReimbursementDto) {
String billStatus = validateUpdateParam(finReimbursementDto);
Long reimbursementId = finReimbursementDto.getId();
FinReimbursement existing = finReimbursementMapper.selectById(reimbursementId);
if (existing == null) {
throw new ServiceException("报销单不存在");
}
// 计算明细汇总金额
List details = finReimbursementDto.getDetails();
BigDecimal totalAmount = details.stream()
.map(FinReimbursementDetail::getAmount)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 更新主表
FinReimbursement reimbursement = buildReimbursement(
finReimbursementDto,
existing.getBillNo(),
totalAmount,
billStatus
);
reimbursement.setId(reimbursementId);
int mainRows = finReimbursementMapper.updateById(reimbursement);
if (mainRows != 1) {
throw new ServiceException("更新报销单主表失败");
}
// 查询数据库中已有的明细
List existingDetails = finReimbursementDetailMapper.selectList(
new LambdaQueryWrapper()
.eq(FinReimbursementDetail::getReimbursementId, reimbursementId));
Set existingDetailIds = existingDetails.stream()
.map(FinReimbursementDetail::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 新明细中有ID的 → 更新;无ID的 → 新增
Set submittedDetailIds = new HashSet<>();
for (int i = 0; i < details.size(); i++) {
FinReimbursementDetail detail = details.get(i);
detail.setReimbursementId(reimbursementId);
detail.setRowNo(i + 1);
if (detail.getId() != null && existingDetailIds.contains(detail.getId())) {
finReimbursementDetailMapper.updateById(detail);
submittedDetailIds.add(detail.getId());
} else {
detail.setId(null);
finReimbursementDetailMapper.insert(detail);
}
}
// 数据库中已有但新明细中没有的 → 删除
for (Long existingId : existingDetailIds) {
if (!submittedDetailIds.contains(existingId)) {
finReimbursementDetailMapper.deleteById(existingId);
}
}
// 差旅扩展:有则更新,无则新增
FinReimbursementTravel existingTravel = finReimbursementTravelMapper.selectOne(
new LambdaQueryWrapper()
.eq(FinReimbursementTravel::getReimbursementId, reimbursementId)
.last("LIMIT 1"));
FinReimbursementTravel travel = finReimbursementDto.getTravel();
if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && travel != null) {
travel.setReimbursementId(reimbursementId);
if (existingTravel != null) {
travel.setId(existingTravel.getId());
finReimbursementTravelMapper.updateById(travel);
} else {
travel.setId(null);
finReimbursementTravelMapper.insert(travel);
}
}
resetApprovalFlow(existing, reimbursementId);
if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
reimbursement.setApprovalInstanceId(null);
startApproval(reimbursement, finReimbursementDto);
}
fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
return true;
}
@Override
public FinReimbursementVo detail(Long id) {
if (id == null ) {
throw new ServiceException("报销单ID不能为空");
}
FinReimbursement reimbursement = finReimbursementMapper.selectById(id);
if (reimbursement == null) {
throw new ServiceException("报销单不存在");
}
FinReimbursementVo vo = new FinReimbursementVo();
vo.setId(reimbursement.getId());
vo.setBillNo(reimbursement.getBillNo());
vo.setReimbursementType(reimbursement.getReimbursementType());
vo.setExpenseType(reimbursement.getExpenseType());
vo.setApplicantId(reimbursement.getApplicantId());
vo.setApplicantCode(reimbursement.getApplicantCode());
vo.setApplicantName(reimbursement.getApplicantName());
vo.setApplicantDeptId(reimbursement.getApplicantDeptId());
vo.setApplicantDeptName(reimbursement.getApplicantDeptName());
vo.setReason(reimbursement.getReason());
vo.setApplyAmount(reimbursement.getApplyAmount());
vo.setDetailTotalAmount(reimbursement.getDetailTotalAmount());
vo.setPayeeName(reimbursement.getPayeeName());
vo.setPayeeAccount(reimbursement.getPayeeAccount());
vo.setPayeeBank(reimbursement.getPayeeBank());
vo.setApprovalInstanceId(reimbursement.getApprovalInstanceId());
vo.setApproveProcessId(reimbursement.getApproveProcessId());
vo.setBillStatus(reimbursement.getBillStatus());
vo.setApprovedTime(reimbursement.getApprovedTime());
vo.setPaidTime(reimbursement.getPaidTime());
vo.setAccountExpenseId(reimbursement.getAccountExpenseId());
vo.setRemark(reimbursement.getRemark());
vo.setTenantId(reimbursement.getTenantId());
vo.setCreateUser(reimbursement.getCreateUser());
vo.setCreateTime(reimbursement.getCreateTime());
vo.setUpdateUser(reimbursement.getUpdateUser());
vo.setUpdateTime(reimbursement.getUpdateTime());
vo.setDeptId(reimbursement.getDeptId());
vo.setDeleted(reimbursement.getDeleted());
vo.setDetails(finReimbursementDetailMapper.selectList(
new LambdaQueryWrapper()
.eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId())
.orderByAsc(FinReimbursementDetail::getRowNo)
));
if (isTravelReimbursement(reimbursement.getReimbursementType())) {
vo.setTravel(finReimbursementTravelMapper.selectOne(
new LambdaQueryWrapper()
.eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId())
.last("LIMIT 1")
));
}
vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId()));
//审批记录返回
vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId())));
vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId())));
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean delete(List ids) {
fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids);
//先删除明细
finReimbursementDetailMapper.delete(new LambdaQueryWrapper().in(FinReimbursementDetail::getReimbursementId, ids));
//删除差旅
finReimbursementTravelMapper.delete(new LambdaQueryWrapper().in(FinReimbursementTravel::getReimbursementId, ids));
//删除主表
int rows = finReimbursementMapper.delete(new LambdaQueryWrapper().in(FinReimbursement::getId, ids));
return rows == ids.size();
}
private String validateUpdateParam(FinReimbursementDto finReimbursementDto) {
if (finReimbursementDto == null || finReimbursementDto.getId() == null) {
throw new ServiceException("报销单ID不能为空");
}
if (finReimbursementDto.getReimbursementType() == null) {
throw new ServiceException("报销类型不能为空");
}
String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
if (billStatus == null) {
throw new ServiceException("单据状态只支持 DRAFT 或 IN_APPROVAL");
}
if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
validateApprovalNodes(finReimbursementDto.getNodes());
}
List details = finReimbursementDto.getDetails();
if (details == null || details.isEmpty()) {
throw new ServiceException("报销单明细不能为空");
}
for (FinReimbursementDetail detail : details) {
if (detail == null) {
throw new ServiceException("报销单明细不能为空");
}
if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("报销单明细金额必须大于0");
}
}
if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
throw new ServiceException("差旅报销必须填写差旅扩展信息");
}
if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
}
return billStatus;
}
private String validateAddParam(FinReimbursementDto finReimbursementDto) {
if (finReimbursementDto == null) {
throw new ServiceException("报销单数据不能为空");
}
if (finReimbursementDto.getReimbursementType() == null) {
throw new ServiceException("报销类型不能为空");
}
String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
if (billStatus == null) {
throw new ServiceException("单据状态只支持 DRAFT 或 IN_APPROVAL");
}
if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
validateApprovalNodes(finReimbursementDto.getNodes());
}
List details = finReimbursementDto.getDetails();
if (details == null || details.isEmpty()) {
throw new ServiceException("报销单明细不能为空");
}
for (FinReimbursementDetail detail : details) {
if (detail == null) {
throw new ServiceException("报销单明细不能为空");
}
if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("报销单明细金额必须大于0");
}
}
if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
throw new ServiceException("差旅报销必须填写差旅扩展信息");
}
if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
}
return billStatus;
}
private FinReimbursement buildReimbursement(FinReimbursementDto finReimbursementDto, String billNo, BigDecimal totalAmount, String billStatus) {
FinReimbursement reimbursement = new FinReimbursement();
reimbursement.setId(null);
reimbursement.setBillNo(billNo);
reimbursement.setReimbursementType(finReimbursementDto.getReimbursementType());
reimbursement.setExpenseType(finReimbursementDto.getExpenseType());
reimbursement.setApplicantId(finReimbursementDto.getApplicantId());
reimbursement.setApplicantCode(finReimbursementDto.getApplicantCode());
reimbursement.setApplicantName(finReimbursementDto.getApplicantName());
reimbursement.setApplicantDeptId(finReimbursementDto.getApplicantDeptId());
reimbursement.setApplicantDeptName(finReimbursementDto.getApplicantDeptName());
reimbursement.setReason(finReimbursementDto.getReason());
reimbursement.setApplyAmount(finReimbursementDto.getApplyAmount());
reimbursement.setDetailTotalAmount(totalAmount);
reimbursement.setPayeeName(finReimbursementDto.getPayeeName());
reimbursement.setPayeeAccount(finReimbursementDto.getPayeeAccount());
reimbursement.setPayeeBank(finReimbursementDto.getPayeeBank());
reimbursement.setRemark(finReimbursementDto.getRemark());
reimbursement.setTenantId(finReimbursementDto.getTenantId());
reimbursement.setApproveProcessId(null);
reimbursement.setBillStatus(billStatus);
return reimbursement;
}
private void startApproval(FinReimbursement reimbursement, FinReimbursementDto finReimbursementDto) {
Long businessType = resolveBusinessType(finReimbursementDto.getReimbursementType());
ApprovalInstanceDto approvalInstanceDto = new ApprovalInstanceDto();
approvalInstanceDto.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no"));
approvalInstanceDto.setBusinessId(reimbursement.getId());
approvalInstanceDto.setTemplateId(null);
approvalInstanceDto.setTemplateName(TypeEnums.getLabelByValue(businessType) + "审批");
approvalInstanceDto.setBusinessType(businessType);
approvalInstanceDto.setTitle("报销单号:" + reimbursement.getBillNo());
approvalInstanceDto.setApplicantId(reimbursement.getApplicantId() != null ? reimbursement.getApplicantId() : SecurityUtils.getUserId());
approvalInstanceDto.setApplicantName(reimbursement.getApplicantName() != null ? reimbursement.getApplicantName() : SecurityUtils.getLoginUser().getNickName());
approvalInstanceDto.setApplyTime(LocalDateTime.now());
approvalInstanceDto.setStatus("PENDING");
approvalInstanceDto.setCurrentLevel(1);
boolean approvalSaved = approvalInstanceService.save(approvalInstanceDto);
if (!approvalSaved || approvalInstanceDto.getId() == null) {
throw new ServiceException("发起审批失败");
}
List firstTasks = createApprovalNodes(approvalInstanceDto, finReimbursementDto.getNodes());
sendApproveNotice(approvalInstanceDto, firstTasks);
FinReimbursement update = new FinReimbursement();
update.setId(reimbursement.getId());
update.setApprovalInstanceId(approvalInstanceDto.getId());
update.setBillStatus(BILL_STATUS_IN_APPROVAL);
int rows = finReimbursementMapper.updateById(update);
if (rows != 1) {
throw new ServiceException("回填审批实例失败");
}
}
private List createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List nodes) {
List firstTasks = Collections.emptyList();
for (int i = 0; i < nodes.size(); i++) {
ApprovalTemplateNodeDto nodeDto = nodes.get(i);
ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
instanceNode.setInstanceId(approvalInstanceDto.getId());
instanceNode.setLevelNo(nodeDto.getLevelNo());
instanceNode.setApproveType(nodeDto.getApproveType());
instanceNode.setStatus(i == 0 ? "PENDING" : NODE_STATUS_WAITING);
instanceNode.setStartTime(i == 0 ? LocalDateTime.now() : null);
instanceNode.setDeleted((byte) 0);
approvalInstanceNodeService.save(instanceNode);
List tasks = nodeDto.getApprovers().stream().map(approver -> {
ApprovalTask task = new ApprovalTask();
task.setInstanceId(approvalInstanceDto.getId());
task.setNodeId(instanceNode.getId());
task.setLevelNo(instanceNode.getLevelNo());
task.setApproverId(approver.getApproverId());
task.setApproverName(approver.getApproverName());
task.setTaskStatus("PENDING");
task.setIsRead((byte) 0);
task.setDeleted((byte) 0);
return task;
}).collect(Collectors.toList());
approvalTaskService.saveBatch(tasks);
if (i == 0) {
firstTasks = tasks;
ApprovalRecord record = new ApprovalRecord();
record.setInstanceId(approvalInstanceDto.getId());
record.setNodeId(instanceNode.getId());
record.setOperatorId(approvalInstanceDto.getApplicantId());
record.setOperatorName(approvalInstanceDto.getApplicantName());
record.setAction("SUBMIT");
record.setComment("发起审批");
record.setDeleted((byte) 0);
approvalRecordService.save(record);
}
}
return firstTasks;
}
private void validateApprovalNodes(List nodes) {
if (nodes == null || nodes.isEmpty()) {
throw new ServiceException("提交审批时审批节点不能为空");
}
for (int i = 0; i < nodes.size(); i++) {
ApprovalTemplateNodeDto node = nodes.get(i);
if (node == null) {
throw new ServiceException("审批节点不能为空");
}
if (node.getLevelNo() == null) {
node.setLevelNo(i + 1);
}
if (!StringUtils.hasText(node.getApproveType())) {
throw new ServiceException("审批节点审批方式不能为空");
}
List approvers = node.getApprovers();
if (approvers == null || approvers.isEmpty()) {
throw new ServiceException("审批节点审批人不能为空");
}
for (ApprovalTemplateNodeApproverDto approver : approvers) {
if (approver == null || approver.getApproverId() == null) {
throw new ServiceException("审批人不能为空");
}
}
}
}
private void sendApproveNotice(ApprovalInstanceDto instance, List tasks) {
if (instance == null || tasks == null || tasks.isEmpty()) {
return;
}
List approverIds = tasks.stream()
.map(ApprovalTask::getApproverId)
.filter(id -> id != null && id > 0)
.distinct()
.collect(Collectors.toList());
if (approverIds.isEmpty()) {
return;
}
String title = "报销审批";
String message = "审批单号 " + instance.getInstanceNo() + " 需要您审批";
String jumpPath = "/approvalInstance?id=" + instance.getId();
sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
}
private void resetApprovalFlow(FinReimbursement existing, Long reimbursementId) {
if (existing == null || existing.getApprovalInstanceId() == null) {
return;
}
Long approvalInstanceId = existing.getApprovalInstanceId();
if (!"REJECTED".equals(existing.getBillStatus())) {
approvalInstanceService.delete(Collections.singletonList(approvalInstanceId));
}
clearApprovalBinding(reimbursementId);
}
private void clearApprovalBinding(Long reimbursementId) {
int rows = finReimbursementMapper.update(
null,
Wrappers.lambdaUpdate()
.eq(FinReimbursement::getId, reimbursementId)
.set(FinReimbursement::getApprovalInstanceId, null)
);
if (rows != 1) {
throw new ServiceException("重置审批流程失败");
}
}
private Long resolveBusinessType(Byte reimbursementType) {
return isTravelReimbursement(reimbursementType)
? TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode()
: TypeEnums.EXPENSE_APPROVAL.getCode();
}
private String normalizeBillStatus(String billStatus) {
if (billStatus == null) {
return BILL_STATUS_DRAFT;
}
String normalized = billStatus.trim().toUpperCase();
if (BILL_STATUS_DRAFT.equals(normalized) || BILL_STATUS_IN_APPROVAL.equals(normalized)) {
return normalized;
}
return null;
}
private boolean isTravelReimbursement(Byte reimbursementType) {
return Byte.valueOf((byte) 1).equals(reimbursementType);
}
}