liyong
5 天以前 2c18c7b8d1708fd028b8e5093d5f5832c389ed0e
feat(approve): 完善审批实例管理功能并新增报销单模块

- 修改ApprovalInstanceController使用update方法替代updateById
- 在ApprovalInstanceDto中添加storageBlobDTOs字段用于文件存储
- 新增FinReimbursement相关实体类、DTO、VO及控制器
- 实现报销单明细表FinReimbursementDetail的基础功能
- 添加审批实例的文件上传和存储功能
- 完善企业新闻和报销审批的状态同步处理逻辑
- 重构审批流程中的节点激活和任务分配逻辑
- 优化企业新闻读取权限范围的管理功能
- 新增FIN_REIMBURSEMENT和ENTERPRISE_NEWS记录类型枚举值
- 实现审批实例更新时的附件处理功能
- 完善各类审批类型的完成状态处理逻辑
已添加20个文件
已修改10个文件
1690 ■■■■■ 文件已修改
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java 537 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementMapper.xml 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java
@@ -1,7 +1,10 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalInstanceDto extends ApprovalInstance {
@@ -13,4 +16,6 @@
    private String createTimeEnd;
    private String createTimeStart;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class FinReimbursementDto extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    private FinReimbursementTravel  travel;
    private List<FinReimbursementDetail> details;
    private List<ApprovalTemplateNodeDto> nodes;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java
@@ -3,6 +3,7 @@
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.basic.dto.StorageBlobVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -22,4 +23,6 @@
    @Schema(description = "业务名称")
    private String businessName;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.approve.bean.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.dto.StorageBlobVO;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class FinReimbursementVo extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    private FinReimbursementTravel travel;
    private List<FinReimbursementDetail> details;
    //审批流程
    private List<ApprovalTask> tasks;
    //审批记录
    private List<ApprovalRecord>  records;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java
@@ -43,7 +43,7 @@
    @PutMapping("/update")
    @Operation(summary = "更新")
    public R update(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.updateById(approvalInstanceDto) ? R.ok() : R.fail();
        return approvalInstanceService.update(approvalInstanceDto) ? R.ok() : R.fail();
    }
    @DeleteMapping("/delete")
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.service.FinReimbursementService;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@RestController
@RequestMapping("/finReimbursement")
@Tag(name = "报销单主表", description = "报销单主表")
@AllArgsConstructor
public class FinReimbursementController {
    private final FinReimbursementService finReimbursementService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    public R listPage(Page<FinReimbursementVo> page, FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.listPage(finReimbursementDto, page));
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    public R save(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.add(finReimbursementDto));
    }
    @GetMapping("/detail")
    @Operation(summary = "详情")
    public R detail(Long id) {
        return R.ok(finReimbursementService.detail(id));
    }
    @PostMapping("/update")
    @Operation(summary = "修改")
    public R update(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.update(finReimbursementDto));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(finReimbursementService.delete(ids));
    }
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * æŠ¥é”€å•明细表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@RestController
@RequestMapping("/finReimbursementDetail")
public class FinReimbursementDetailController {
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å·®æ—…报销扩展表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@RestController
@RequestMapping("/finReimbursementTravel")
public class FinReimbursementTravelController {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * æŠ¥é”€å•明细表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Mapper
public interface FinReimbursementDetailMapper extends BaseMapper<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * æŠ¥é”€å•主表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Mapper
public interface FinReimbursementMapper extends BaseMapper<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(@Param("ew") FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å·®æ—…报销扩展表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Mapper
public interface FinReimbursementTravelMapper extends BaseMapper<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•主表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement")
@ApiModel(value = "FinReimbursement对象", description = "报销单主表")
public class FinReimbursement implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•号
     */
    @Schema(description = "报销单号")
    private String billNo;
    /**
     * æŠ¥é”€ç±»åž‹ï¼š1-差旅报销,2-费用报销
     */
    @Schema(description = "报销类型:1-差旅报销,2-费用报销")
    private Byte reimbursementType;
    /**
     * è´¹ç”¨ç±»åž‹ï¼šå·®æ—…è´¹/办公采购/业务招待/交通费/通讯费/其他
     */
    @Schema(description = "费用类型:差旅费/办公采购/业务招待/交通费/通讯费/其他")
    private String expenseType;
    /**
     * ç”³è¯·äººID
     */
    @Schema(description = "申请人ID")
    private Long applicantId;
    /**
     * å‘˜å·¥ç¼–号
     */
    @Schema(description = "员工编号")
    private String applicantCode;
    /**
     * å‘˜å·¥å§“名
     */
    @Schema(description = "员工姓名")
    private String applicantName;
    /**
     * ç”³è¯·éƒ¨é—¨ID
     */
    @Schema(description = "申请部门ID")
    private Long applicantDeptId;
    /**
     * ç”³è¯·éƒ¨é—¨åç§°
     */
    @Schema(description = "申请部门名称")
    private String applicantDeptName;
    /**
     * æŠ¥é”€åŽŸå› 
     */
    @Schema(description = "报销原因")
    private String reason;
    /**
     * ç”³è¯·é‡‘额
     */
    @Schema(description = "申请金额")
    private BigDecimal applyAmount;
    /**
     * æ˜Žç»†æ±‡æ€»é‡‘额
     */
    @Schema(description = "明细汇总金额")
    private BigDecimal detailTotalAmount;
    /**
     * æ”¶æ¬¾äºº
     */
    @Schema(description = "收款人")
    private String payeeName;
    /**
     * æ”¶æ¬¾è´¦å·
     */
    @Schema(description = "收款账号")
    private String payeeAccount;
    /**
     * å¼€æˆ·æ”¯è¡Œ
     */
    @Schema(description = "开户支行")
    private String payeeBank;
    /**
     * å®¡æ‰¹å®žä¾‹ID,对应 approval_instance.id
     */
    @Schema(description = "审批实例ID,对应 approval_instance.id")
    private Long approvalInstanceId;
    /**
     * å®¡æ‰¹æµç¨‹ID,对应 approve_process.id
     */
    @Schema(description = "审批流程ID,对应 approve_process.id")
    private Long approveProcessId;
    /**
     * å•据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款
     */
    @Schema(description = "单据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款")
    private String billStatus;
    /**
     * å®¡æ‰¹é€šè¿‡æ—¶é—´
     */
    @Schema(description = "审批通过时间")
    private LocalDateTime approvedTime;
    /**
     * ä»˜æ¬¾æ—¶é—´
     */
    @Schema(description = "付款时间")
    private LocalDateTime paidTime;
    /**
     * ç”Ÿæˆçš„财务支出记录ID,对应 account_expense.id
     */
    @Schema(description = "生成的财务支出记录ID,对应 account_expense.id")
    private Long accountExpenseId;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•明细表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_detail")
@ApiModel(value = "FinReimbursementDetail对象", description = "报销单明细表")
public class FinReimbursementDetail implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * æ˜Žç»†è¡Œå·
     */
    @Schema(description = "明细行号")
    private Integer rowNo;
    /**
     * å‘票日期
     */
    @Schema(description = "发票日期")
    private LocalDate invoiceDate;
    /**
     * è´¹ç”¨ç§‘ç›®
     */
    @Schema(description = "费用科目")
    private String expenseCategory;
    /**
     * é‡‘额
     */
    @Schema(description = "金额")
    private BigDecimal amount;
    /**
     * æè¿°
     */
    @Schema(description = "描述")
    private String description;
    /**
     * å‘票号码
     */
    @Schema(description = "发票号码")
    private String invoiceNo;
    /**
     * å‘票类型
     */
    @Schema(description = "发票类型")
    private String invoiceType;
    /**
     * ç¥¨é¢é‡‘额
     */
    @Schema(description = "票面金额")
    private BigDecimal invoiceAmount;
    /**
     * ç¨Žçއ
     */
    @Schema(description = "税率")
    private BigDecimal taxRate;
    /**
     * ç¨Žé¢
     */
    @Schema(description = "税额")
    private BigDecimal taxAmount;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * å·®æ—…报销扩展表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_travel")
@ApiModel(value = "FinReimbursementTravel对象", description = "差旅报销扩展表")
public class FinReimbursementTravel implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * å‡ºå·®å¼€å§‹æ—¶é—´
     */
    @Schema(description = "出差开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    /**
     * å‡ºå·®ç»“束时间
     */
    @Schema(description = "出差结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    /**
     * å‡ºå·®å¤©æ•°
     */
    @Schema(description = "出差天数")
    private BigDecimal travelDays;
    /**
     * å‡ºå·®åœ°/出发城市
     */
    @Schema(description = "出差地/出发城市")
    private String departureCity;
    /**
     * ç›®çš„地/目的城市
     */
    @Schema(description = "目的地/目的城市")
    private String destinationCity;
    /**
     * é…’店标准
     */
    @Schema(description = "酒店标准")
    private BigDecimal hotelStandard;
    /**
     * ä½å®¿å¤©æ•°
     */
    @Schema(description = "住宿天数")
    private BigDecimal lodgingDays;
    /**
     * ç”Ÿæ´»è¡¥è´´
     */
    @Schema(description = "生活补贴")
    private BigDecimal mealAllowance;
    /**
     * äº¤é€šè¡¥è´´
     */
    @Schema(description = "交通补贴")
    private BigDecimal transportAllowance;
    /**
     * ä½å®¿é™é¢
     */
    @Schema(description = "住宿限额")
    private BigDecimal lodgingLimit;
    /**
     * ç‰¹æ‰¹æ ‡è®°æ–‡æœ¬ï¼Œå¦‚在标准范围内/超标特批
     */
    @Schema(description = "特批标记文本,如在标准范围内/超标特批")
    private String standardTag;
    /**
     * æ˜¯å¦åœ¨æ ‡å‡†å†…:1-是,0-否
     */
    @Schema(description = "是否在标准内:1-是,0-否")
    private Byte withinStandard;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java
@@ -23,6 +23,8 @@
    Boolean add(ApprovalInstanceDto approvalInstanceDto);
    Boolean update(ApprovalInstanceDto approvalInstanceDto);
    Boolean delete(List<Long> ids);
    R approve(ApprovalInstanceDto approvalInstanceDto);
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
public interface FinReimbursementDetailService extends IService<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
public interface FinReimbursementService extends IService<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
    Boolean add(FinReimbursementDto finReimbursementDto);
    FinReimbursementVo detail(Long id);
    Boolean update(FinReimbursementDto finReimbursementDto);
    Boolean delete(List<Long> ids);
}
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
public interface FinReimbursementTravelService extends IService<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java
@@ -10,15 +10,20 @@
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.approve.service.*;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeDeptService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeUserService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsService;
import com.ruoyi.common.enums.*;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
@@ -40,6 +45,8 @@
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.staff.mapper.HolidayApplicationMapper;
import com.ruoyi.staff.pojo.HolidayApplication;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -69,7 +76,8 @@
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverService approvalTemplateNodeApproverService;
    private final FinReimbursementMapper finReimbursementMapper;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -77,12 +85,14 @@
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final QualityInspectHelper qualityInspectHelper;
    private final EnterpriseNewsService enterpriseNewsService;
    private final EnterpriseNewsScopeDeptService enterpriseNewsScopeDeptService;
    private final EnterpriseNewsScopeUserService enterpriseNewsScopeUserService;
    private final EnterpriseNewsScopeUserMapper enterpriseNewsScopeUserMapper;
    private final SysUserMapper sysUserMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final SysDeptMapper sysDeptMapper;
    private final HolidayApplicationMapper holidayApplicationMapper;
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    private final EnterpriseNewsScopeDeptMapper enterpriseNewsScopeDeptMapper;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    @Override
    public R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto) {
@@ -119,6 +129,7 @@
                vo.setIsApprove(approveProcessConfigNodeUtils.isCurrentApprover(vo.getId(), currentUserId));
                vo.setRecords(recordMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setTasks(taskMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.APPROVAL_INSTANCE, vo.getId()));
            }
        }
@@ -137,7 +148,22 @@
            return false;
        }
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(approvalInstanceDto);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        sendApproveNotice(approvalInstanceDto, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(ApprovalInstanceDto approvalInstanceDto) {
        if (approvalInstanceDto == null || approvalInstanceDto.getId() == null) {
            return false;
        }
        boolean updated = this.updateById(approvalInstanceDto);
        if (!updated) {
            return false;
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        return true;
    }
@@ -147,6 +173,7 @@
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, ids);
        int instanceRows = approvalInstanceMapper.update(
                null,
@@ -221,11 +248,10 @@
                approveAction,
                approvalInstanceDto.getApproveComment()
        );
        //审批拒绝的处理
        if ("REJECTED".equals(approveAction)) {
            return rejectCurrentNode(instance, currentNode, now);
        }
        if (!approveProcessConfigNodeUtils.canProceedToNextLevel(instance.getId(), currentNode.getApproveType())) {
            return R.ok("审批成功,等待其他审批人处理");
        }
@@ -278,6 +304,20 @@
        instance.setStatus("REJECTED");
        instance.setFinishTime(now);
        this.updateById(instance);
        // é©³å›žå¯¹åº”的企业新闻, å·®æ—…报销
        if (instance.getBusinessType().equals(TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode())) {
            enterpriseNewsMapper.update(
                    new LambdaUpdateWrapper<EnterpriseNews>()
                            .eq(EnterpriseNews::getId, instance.getBusinessId())
                            .set(EnterpriseNews::getStatus, "REJECTED")
            );
        }else if (instance.getBusinessType().equals(TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode())||instance.getBusinessType().equals(TypeEnums.EXPENSE_APPROVAL.getCode())) {
            finReimbursementMapper.update(
                    new LambdaUpdateWrapper<FinReimbursement>()
                            .eq(FinReimbursement::getId, instance.getBusinessId())
                            .set(FinReimbursement::getBillStatus, "REJECTED")
            );
        }
        return R.ok("审批已驳回");
    }
@@ -292,6 +332,33 @@
        closePendingTasks(instance.getId(), currentNode.getId());
        int nextLevel = currentNode.getLevelNo() + 1;
        ApprovalInstanceNode nextInstanceNode = approvalInstanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instance.getId())
                        .eq(ApprovalInstanceNode::getLevelNo, nextLevel)
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .orderByAsc(ApprovalInstanceNode::getId)
                        .last("LIMIT 1")
        );
        if (nextInstanceNode != null) {
            if (!activateNextInstanceNode(nextInstanceNode.getId(), now)) {
                return R.ok("下一审批节点已被激活,请刷新后重试");
            }
            instance.setCurrentLevel(nextLevel);
            instance.setStatus("PENDING");
            this.updateById(instance);
            List<ApprovalTask> nextTasks = approvalTaskService.list(
                    Wrappers.<ApprovalTask>lambdaQuery()
                            .eq(ApprovalTask::getInstanceId, instance.getId())
                            .eq(ApprovalTask::getNodeId, nextInstanceNode.getId())
                            .eq(ApprovalTask::getTaskStatus, "PENDING")
                            .eq(ApprovalTask::getDeleted, 0)
            );
            sendApproveNotice(instance, nextTasks);
            return R.ok("审批成功,已流转到下一节点");
        }
        ApprovalTemplateNode nextTemplateNode = approvalTemplateNodeService.getOne(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, instance.getTemplateId())
@@ -314,6 +381,17 @@
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(instance, false);
        sendApproveNotice(instance, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        return R.ok("审批成功,已流转到下一节点");
    }
    private boolean activateNextInstanceNode(Long nodeId, LocalDateTime now) {
        return approvalInstanceNodeService.update(
                Wrappers.<ApprovalInstanceNode>lambdaUpdate()
                        .eq(ApprovalInstanceNode::getId, nodeId)
                        .eq(ApprovalInstanceNode::getStatus, "WAITING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .set(ApprovalInstanceNode::getStatus, "PENDING")
                        .set(ApprovalInstanceNode::getStartTime, now)
        );
    }
    private boolean updateCurrentNodeStatus(Long nodeId, String targetStatus, LocalDateTime now) {
@@ -343,9 +421,40 @@
            handleShippingApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode().equals(businessType)
                || TypeEnums.EXPENSE_APPROVAL.getCode().equals(businessType)) {
            handleReimbursementApprovalFinished(instance, status);
            return;
        }
        // é©³å›žå¯¹åº”的企业新闻、加班申请、请假申请可以重新再提交
        if (TypeEnums.LEAVE_APPROVAL.getCode().equals(businessType)
                || TypeEnums.OVERTIME_APPROVAL.getCode().equals(businessType)) {
            handleHolidayApplicationApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode().equals(businessType)) {
            handleNewsApprovalFinished(instance, status);
        }
    }
    private void handleReimbursementApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        FinReimbursement reimbursement = new FinReimbursement();
        reimbursement.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            reimbursement.setBillStatus("APPROVED");
            reimbursement.setApprovedTime(instance.getFinishTime());
        } else if ("REJECTED".equals(status)) {
            reimbursement.setBillStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            reimbursement.setBillStatus("IN_APPROVAL");
        } else {
            return;
        }
        finReimbursementMapper.updateById(reimbursement);
    }
    private void handleNewsApprovalFinished(ApprovalInstance instance, String status) {
@@ -356,14 +465,32 @@
        enterpriseNews.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_PUBLISHED);
            enterpriseNewsService.updateById(enterpriseNews);
            enterpriseNewsMapper.updateById(enterpriseNews);
            sendEnterpriseNewsNotice(instance.getBusinessId());
            return;
        }
        if ("REJECTED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_REJECTED);
            enterpriseNewsService.updateById(enterpriseNews);
            enterpriseNewsMapper.updateById(enterpriseNews);
        }
    }
    private void handleHolidayApplicationApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        HolidayApplication holidayApplication = new HolidayApplication();
        holidayApplication.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            holidayApplication.setStatus("APPROVED");
        } else if ("REJECTED".equals(status)) {
            holidayApplication.setStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            holidayApplication.setStatus("PENDING");
        } else {
            return;
        }
        holidayApplicationMapper.updateById(holidayApplication);
    }
    private void handlePurchaseApprovalFinished(ApprovalInstance instance, String status) {
@@ -449,7 +576,7 @@
    }
    private List<ApprovalTask> createNodeAndTasks(ApprovalInstance instance, ApprovalTemplateNode templateNode) {
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverService.list(
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNodeApprover::getNodeId, templateNode.getId())
@@ -507,7 +634,7 @@
    }
    private void sendEnterpriseNewsNotice(Long newsId) {
        EnterpriseNews enterpriseNews = enterpriseNewsService.getById(newsId);
        EnterpriseNews enterpriseNews = enterpriseNewsMapper.selectById(newsId);
        if (enterpriseNews == null) {
            return;
        }
@@ -537,7 +664,7 @@
                    .collect(Collectors.toList());
        }
        if ("dept".equals(readScope)) {
            List<Long> deptIds = enterpriseNewsScopeDeptService.list(
            List<Long> deptIds = enterpriseNewsScopeDeptMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeDept>()
                                    .eq(EnterpriseNewsScopeDept::getNewsId, enterpriseNews.getId()))
                    .stream()
@@ -551,7 +678,7 @@
            return sysUserDeptMapper.selectDistinctUserIdsByDeptIds(collectDeptIdsWithChildren(deptIds));
        }
        if ("custom".equals(readScope)) {
            return enterpriseNewsScopeUserService.list(
            return enterpriseNewsScopeUserMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeUser>()
                                    .eq(EnterpriseNewsScopeUser::getNewsId, enterpriseNews.getId()))
                    .stream()
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java
@@ -107,7 +107,6 @@
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getDeleted, 0)
                        .eq(ApprovalTemplate::getEnabled, 1)
                        .eq(type != null, ApprovalTemplate::getTemplateType, type)
                        .orderByDesc(ApprovalTemplate::getTemplateType)
                        .orderByDesc(ApprovalTemplate::getId)
        );
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.service.FinReimbursementDetailService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Service
public class FinReimbursementDetailServiceImpl extends ServiceImpl<FinReimbursementDetailMapper, FinReimbursementDetail> implements FinReimbursementDetailService {
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,537 @@
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;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Service
@RequiredArgsConstructor
public class FinReimbursementServiceImpl extends ServiceImpl<FinReimbursementMapper, FinReimbursement> 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<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page) {
        IPage<FinReimbursementVo> 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<FinReimbursementDetail> 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<FinReimbursementDetail> 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<FinReimbursementDetail> existingDetails = finReimbursementDetailMapper.selectList(
                new LambdaQueryWrapper<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursementId));
        Set<Long> existingDetailIds = existingDetails.stream()
                .map(FinReimbursementDetail::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // æ–°æ˜Žç»†ä¸­æœ‰ID的 â†’ æ›´æ–°ï¼›æ— ID的 â†’ æ–°å¢ž
        Set<Long> 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<FinReimbursementTravel>()
                        .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.getApprovalInstanceId(), 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<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId())
                        .orderByAsc(FinReimbursementDetail::getRowNo)
        ));
        if (isTravelReimbursement(reimbursement.getReimbursementType())) {
            vo.setTravel(finReimbursementTravelMapper.selectOne(
                    new LambdaQueryWrapper<FinReimbursementTravel>()
                            .eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId())
                            .last("LIMIT 1")
            ));
        }
        vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId()));
        //审批记录返回
        vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper<ApprovalTask>().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId())));
        vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper<ApprovalRecord>().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId())));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids);
        //先删除明细
        finReimbursementDetailMapper.delete(new LambdaQueryWrapper<FinReimbursementDetail>().in(FinReimbursementDetail::getReimbursementId, ids));
        //删除差旅
        finReimbursementTravelMapper.delete(new LambdaQueryWrapper<FinReimbursementTravel>().in(FinReimbursementTravel::getReimbursementId, ids));
        //删除主表
        int rows = finReimbursementMapper.delete(new LambdaQueryWrapper<FinReimbursement>().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<FinReimbursementDetail> 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<FinReimbursementDetail> 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<ApprovalTask> 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<ApprovalTask> createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List<ApprovalTemplateNodeDto> nodes) {
        List<ApprovalTask> 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<ApprovalTask> 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<ApprovalTemplateNodeDto> 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<ApprovalTemplateNodeApproverDto> 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<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> 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(Long approvalInstanceId, Long reimbursementId) {
        if (approvalInstanceId == null) {
            return;
        }
        approvalInstanceService.delete(Collections.singletonList(approvalInstanceId));
        int rows = finReimbursementMapper.update(
                null,
                Wrappers.<FinReimbursement>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);
    }
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.service.FinReimbursementTravelService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Service
public class FinReimbursementTravelServiceImpl extends ServiceImpl<FinReimbursementTravelMapper, FinReimbursementTravel> implements FinReimbursementTravelService {
}
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
@@ -204,8 +204,10 @@
    SALES_REFUND_AMOUNT_ORDER("sales_refund_amount_order"),
    SALES_RECEIPT_RETURN("sales_receipt_return"),
    ACCOUNT_EXPENSE("account_expense"),
    FIN_REIMBURSEMENT("fin_reimbursement"),
    FIN_VOUCHER("fin_voucher"),
    ACCOUNT_FILE("account_file"),
    ENTERPRISE_NEWS("enterprise_news"),
    APPROVAL_INSTANCE("approval_instance");
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java
@@ -1,5 +1,6 @@
package com.ruoyi.collaborativeApproval.dto;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
@@ -19,4 +20,7 @@
    private String createTimeStart;
    private String createTimeEnd;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java
@@ -10,6 +10,9 @@
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
@@ -72,10 +75,15 @@
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApproveProcessConfigNodeUtils approveProcessConfigNodeUtils;
    private final ISysNoticeService sysNoticeService;
    private final FileUtil fileUtil;
    @Override
    public IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page, EnterpriseNewsDto enterpriseNewsDto) {
        return enterpriseNewsMapper.listPage(page, enterpriseNewsDto);
        IPage<EnterpriseNewsVo> enterpriseNewsVoIPage = enterpriseNewsMapper.listPage(page, enterpriseNewsDto);
        enterpriseNewsVoIPage.getRecords().forEach(enterpriseNewsVo -> {
            enterpriseNewsVo.setStorageBlobDTOs(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNewsVo.getId()));
        });
        return enterpriseNewsVoIPage;
    }
    @Override
@@ -107,6 +115,8 @@
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
        //添加附件
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        return true;
    }
@@ -148,7 +158,7 @@
        clearReadScopeRelations(enterpriseNews.getId());
        saveReadScopeRelations(enterpriseNews.getId(), readScope, deptIds, userIds);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java
@@ -1,10 +1,15 @@
package com.ruoyi.collaborativeApproval.vo;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
import java.util.List;
@Data
public class EnterpriseNewsVo extends EnterpriseNews {
    private String createUserName;
    private List<StorageBlobVO> storageBlobDTOs;
}
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementDetailMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursementDetail">
        <id column="id" property="id" />
        <result column="reimbursement_id" property="reimbursementId" />
        <result column="row_no" property="rowNo" />
        <result column="invoice_date" property="invoiceDate" />
        <result column="expense_category" property="expenseCategory" />
        <result column="amount" property="amount" />
        <result column="description" property="description" />
        <result column="invoice_no" property="invoiceNo" />
        <result column="invoice_type" property="invoiceType" />
        <result column="invoice_amount" property="invoiceAmount" />
        <result column="tax_rate" property="taxRate" />
        <result column="tax_amount" property="taxAmount" />
        <result column="remark" property="remark" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
        <result column="deleted" property="deleted" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/FinReimbursementMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursement">
        <id column="id" property="id" />
        <result column="bill_no" property="billNo" />
        <result column="reimbursement_type" property="reimbursementType" />
        <result column="expense_type" property="expenseType" />
        <result column="applicant_id" property="applicantId" />
        <result column="applicant_code" property="applicantCode" />
        <result column="applicant_name" property="applicantName" />
        <result column="applicant_dept_id" property="applicantDeptId" />
        <result column="applicant_dept_name" property="applicantDeptName" />
        <result column="reason" property="reason" />
        <result column="apply_amount" property="applyAmount" />
        <result column="detail_total_amount" property="detailTotalAmount" />
        <result column="payee_name" property="payeeName" />
        <result column="payee_account" property="payeeAccount" />
        <result column="payee_bank" property="payeeBank" />
        <result column="approval_instance_id" property="approvalInstanceId" />
        <result column="approve_process_id" property="approveProcessId" />
        <result column="bill_status" property="billStatus" />
        <result column="approved_time" property="approvedTime" />
        <result column="paid_time" property="paidTime" />
        <result column="account_expense_id" property="accountExpenseId" />
        <result column="remark" property="remark" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
        <result column="deleted" property="deleted" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.approve.bean.vo.FinReimbursementVo">
        select fin_reimbursement.*,
        fin_reimbursement_travel.start_time ,
        fin_reimbursement_travel.end_time
               from
                fin_reimbursement
            left join fin_reimbursement_travel on fin_reimbursement.id = fin_reimbursement_travel.reimbursement_id
        <where>
            <if test="ew.billNo != null and ew.billNo != ''">
                bill_no like concat('%',#{ew.billNo},'%')
            </if>
            <if test="ew.applicantName != null and ew.applicantName != ''">
                and applicant_name like concat('%',#{ew.applicantName},'%')
            </if>
            <if test="ew.createTimeStart != null and ew.createTimeStart !=''">
                and create_time between to_date(#{ew.createTimeStart}) and to_date(#{ew.createTimeEnd})
            </if>
        </where>
    </select>
</mapper>
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementTravelMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursementTravel">
        <id column="id" property="id" />
        <result column="reimbursement_id" property="reimbursementId" />
        <result column="start_time" property="startTime" />
        <result column="end_time" property="endTime" />
        <result column="travel_days" property="travelDays" />
        <result column="departure_city" property="departureCity" />
        <result column="destination_city" property="destinationCity" />
        <result column="hotel_standard" property="hotelStandard" />
        <result column="lodging_days" property="lodgingDays" />
        <result column="meal_allowance" property="mealAllowance" />
        <result column="transport_allowance" property="transportAllowance" />
        <result column="lodging_limit" property="lodgingLimit" />
        <result column="standard_tag" property="standardTag" />
        <result column="within_standard" property="withinStandard" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
    </resultMap>
</mapper>