diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java new file mode 100644 index 0000000..e773aba --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java @@ -0,0 +1,98 @@ +package com.ruoyi.oa.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; +import com.ruoyi.oa.service.IOaApprovalService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 通用审批 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/oa/approval") +public class OaApprovalController extends BaseController { + + private final IOaApprovalService approvalService; + + // ===== 配置 ===== + @GetMapping("/config/list") + public R> configList() { + return R.ok(approvalService.listConfigs()); + } + + @GetMapping("/config/{businessType}") + public R getConfig(@PathVariable String businessType) { + return R.ok(approvalService.getConfig(businessType)); + } + + @PostMapping("/config") + public R saveConfig(@RequestBody OaApprovalConfigBo bo) { + return toAjax(approvalService.saveConfig(bo)); + } + + @DeleteMapping("/config/{id}") + public R deleteConfig(@PathVariable Long id) { + return toAjax(approvalService.deleteConfig(id)); + } + + // ===== 审批 ===== + @PostMapping("/act") + public R act(@Validated @RequestBody OaApprovalActionBo bo) { + return toAjax(approvalService.act(bo)); + } + + @PostMapping("/withdraw/{instanceId}") + public R withdraw(@PathVariable Long instanceId) { + return toAjax(approvalService.withdraw(instanceId)); + } + + // ===== 查询 ===== + @GetMapping("/detail/{instanceId}") + public R detail(@PathVariable Long instanceId) { + return R.ok(approvalService.getDetail(instanceId)); + } + + @GetMapping("/latest") + public R latest(@NotNull @RequestParam String businessType, + @NotNull @RequestParam Long businessId) { + return R.ok(approvalService.getLatestByBusiness(businessType, businessId)); + } + + @GetMapping("/mine/pending") + public TableDataInfo myPending(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMyPending(pageQuery, businessType); + } + + @GetMapping("/mine/done") + public TableDataInfo myDone(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMyDone(pageQuery, businessType); + } + + @GetMapping("/mine/submitted") + public TableDataInfo mySubmitted(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMySubmitted(pageQuery, businessType); + } + + @GetMapping("/list") + public TableDataInfo all(PageQuery pageQuery, + @RequestParam(required = false) String businessType, + @RequestParam(required = false) Integer status) { + return approvalService.pageAll(pageQuery, businessType, status); + } +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java new file mode 100644 index 0000000..d867e70 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java @@ -0,0 +1,40 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务审批配置 oa_approval_config + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("oa_approval_config") +public class OaApprovalConfig extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + /** 业务类型 key */ + private String businessType; + + /** 业务名称(展示用) */ + private String businessName; + + /** 审批人 user_id 列表,逗号分隔 */ + private String approverIds; + + /** 1或签 2会签 */ + private Integer signType; + + /** 是否启用:0停用 1启用 */ + private Integer enabled; + + private String remark; + + private Integer delFlag; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java new file mode 100644 index 0000000..adfa85d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java @@ -0,0 +1,42 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 审批单实例 oa_approval_instance + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("oa_approval_instance") +public class OaApprovalInstance extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + private String businessType; + private Long businessId; + private String businessTitle; + + private Long applyUserId; + private String applyUserName; + private Date applyTime; + + /** 快照:提交时的审批人 */ + private String approverIds; + /** 快照:1或签 2会签 */ + private Integer signType; + + /** 0待审 1通过 2驳回 3撤回 */ + private Integer status; + private Date finishTime; + + private Integer delFlag; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java new file mode 100644 index 0000000..9ac7cfd --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java @@ -0,0 +1,31 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 审批操作流水 oa_approval_record + */ +@Data +@TableName("oa_approval_record") +public class OaApprovalRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + private Long instanceId; + private Long approverId; + private String approverName; + + /** 1通过 2驳回 */ + private Integer action; + private String comment; + private Date opTime; + private Date createTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java new file mode 100644 index 0000000..47cf92d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java @@ -0,0 +1,21 @@ +package com.ruoyi.oa.domain.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +public class OaApprovalActionBo implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + private Long instanceId; + + /** 1通过 2驳回 */ + @NotNull + private Integer action; + + private String comment; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java new file mode 100644 index 0000000..b33002b --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java @@ -0,0 +1,22 @@ +package com.ruoyi.oa.domain.bo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class OaApprovalConfigBo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + /** 审批人 user_id 列表,逗号分隔 */ + private String approverIds; + /** 1或签 2会签 */ + private Integer signType; + /** 0停用 1启用 */ + private Integer enabled; + private String remark; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java new file mode 100644 index 0000000..2986df6 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java @@ -0,0 +1,24 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class OaApprovalConfigVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + private String approverIds; + /** 显示用:审批人昵称 列表 */ + private String approverNames; + private Integer signType; + private Integer enabled; + private String remark; + private Date createTime; + private Date updateTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java new file mode 100644 index 0000000..0645fff --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java @@ -0,0 +1,34 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +@Data +public class OaApprovalInstanceVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + private Long businessId; + private String businessTitle; + + private Long applyUserId; + private String applyUserName; + private Date applyTime; + + private String approverIds; + private String approverNames; + private Integer signType; + + /** 0待审 1通过 2驳回 3撤回 */ + private Integer status; + private Date finishTime; + + /** 审批流水 */ + private List records; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java new file mode 100644 index 0000000..da47d44 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java @@ -0,0 +1,20 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class OaApprovalRecordVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private Long instanceId; + private Long approverId; + private String approverName; + private Integer action; + private String comment; + private Date opTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java new file mode 100644 index 0000000..c7f325f --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalConfig; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; + +public interface OaApprovalConfigMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java new file mode 100644 index 0000000..41ed321 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalInstance; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; + +public interface OaApprovalInstanceMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java new file mode 100644 index 0000000..1496421 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalRecord; +import com.ruoyi.oa.domain.vo.OaApprovalRecordVo; + +public interface OaApprovalRecordMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java new file mode 100644 index 0000000..d8f56f5 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java @@ -0,0 +1,72 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 通用审批 Service + */ +public interface IOaApprovalService { + + // ===== 配置 ===== + List listConfigs(); + + OaApprovalConfigVo getConfig(String businessType); + + Boolean saveConfig(OaApprovalConfigBo bo); + + Boolean deleteConfig(Long id); + + // ===== 业务侧调用 ===== + + /** + * 业务提交触发审批,若已有未结束的实例则直接返回。 + * @return 审批单 id + */ + Long submit(String businessType, Long businessId, String businessTitle); + + /** + * 审批操作(通过/驳回),自动判定或签/会签终结状态。 + */ + Boolean act(OaApprovalActionBo bo); + + /** + * 撤回(仅申请人可在 status=0 时撤回) + */ + Boolean withdraw(Long instanceId); + + /** + * 查询某业务的最新审批单(用于业务详情展示) + */ + OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId); + + /** + * 批量查询业务最新审批状态:返回 businessId -> status + */ + Map batchLatestStatus(String businessType, Collection businessIds); + + // ===== 列表 ===== + + /** 我待办(审批人是我,status=0) */ + TableDataInfo pageMyPending(PageQuery pageQuery, String businessType); + + /** 我已办(出现在 record 里) */ + TableDataInfo pageMyDone(PageQuery pageQuery, String businessType); + + /** 我发起的 */ + TableDataInfo pageMySubmitted(PageQuery pageQuery, String businessType); + + /** 全部(管理员) */ + TableDataInfo pageAll(PageQuery pageQuery, String businessType, Integer status); + + /** 单条详情(带流水) */ + OaApprovalInstanceVo getDetail(Long instanceId); +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java new file mode 100644 index 0000000..614e5cc --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java @@ -0,0 +1,338 @@ +package com.ruoyi.oa.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.helper.LoginHelper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.oa.domain.OaApprovalConfig; +import com.ruoyi.oa.domain.OaApprovalInstance; +import com.ruoyi.oa.domain.OaApprovalRecord; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; +import com.ruoyi.oa.domain.vo.OaApprovalRecordVo; +import com.ruoyi.oa.mapper.OaApprovalConfigMapper; +import com.ruoyi.oa.mapper.OaApprovalInstanceMapper; +import com.ruoyi.oa.mapper.OaApprovalRecordMapper; +import com.ruoyi.oa.service.IOaApprovalService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.service.ISysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OaApprovalServiceImpl implements IOaApprovalService { + + private final OaApprovalConfigMapper configMapper; + private final OaApprovalInstanceMapper instanceMapper; + private final OaApprovalRecordMapper recordMapper; + private final ISysUserService userService; + + // ===================== 配置 ===================== + + @Override + public List listConfigs() { + List list = configMapper.selectVoList( + Wrappers.lambdaQuery().orderByAsc(OaApprovalConfig::getBusinessType)); + list.forEach(this::fillApproverNames); + return list; + } + + @Override + public OaApprovalConfigVo getConfig(String businessType) { + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType)); + if (cfg == null) return null; + OaApprovalConfigVo vo = BeanUtil.toBean(cfg, OaApprovalConfigVo.class); + fillApproverNames(vo); + return vo; + } + + @Override + public Boolean saveConfig(OaApprovalConfigBo bo) { + if (StringUtils.isBlank(bo.getBusinessType())) { + throw new ServiceException("业务类型不能为空"); + } + if (StringUtils.isBlank(bo.getApproverIds())) { + throw new ServiceException("审批人不能为空"); + } + if (bo.getSignType() == null) bo.setSignType(1); + if (bo.getEnabled() == null) bo.setEnabled(1); + + OaApprovalConfig exist = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, bo.getBusinessType())); + OaApprovalConfig entity = BeanUtil.toBean(bo, OaApprovalConfig.class); + if (exist != null) { + entity.setId(exist.getId()); + return configMapper.updateById(entity) > 0; + } + return configMapper.insert(entity) > 0; + } + + @Override + public Boolean deleteConfig(Long id) { + return configMapper.deleteById(id) > 0; + } + + // ===================== 业务侧 ===================== + + @Override + @Transactional(rollbackFor = Exception.class) + public Long submit(String businessType, Long businessId, String businessTitle) { + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType)); + if (cfg == null || cfg.getEnabled() != null && cfg.getEnabled() == 0) { + // 未配置 / 未启用:跳过审批,不报错 + return null; + } + if (StringUtils.isBlank(cfg.getApproverIds())) { + throw new ServiceException("业务【" + businessType + "】未配置审批人"); + } + + // 若已存在未结束实例则复用 + OaApprovalInstance pending = instanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .eq(OaApprovalInstance::getBusinessId, businessId) + .eq(OaApprovalInstance::getStatus, 0) + .last("LIMIT 1")); + if (pending != null) return pending.getId(); + + OaApprovalInstance inst = new OaApprovalInstance(); + inst.setBusinessType(businessType); + inst.setBusinessId(businessId); + inst.setBusinessTitle(businessTitle); + inst.setApplyUserId(LoginHelper.getUserId()); + inst.setApplyUserName(LoginHelper.getNickName()); + inst.setApplyTime(new Date()); + inst.setApproverIds(cfg.getApproverIds()); + inst.setSignType(cfg.getSignType()); + inst.setStatus(0); + instanceMapper.insert(inst); + return inst.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean act(OaApprovalActionBo bo) { + OaApprovalInstance inst = instanceMapper.selectById(bo.getInstanceId()); + if (inst == null) throw new ServiceException("审批单不存在"); + if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("审批已终结"); + + Long me = LoginHelper.getUserId(); + Set approvers = parseIds(inst.getApproverIds()); + if (!approvers.contains(me)) throw new ServiceException("无审批权限"); + + // 防重:同一人对同一单只能操作一次 + Long already = recordMapper.selectCount( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, inst.getId()) + .eq(OaApprovalRecord::getApproverId, me)); + if (already != null && already > 0) throw new ServiceException("您已审批过"); + + OaApprovalRecord rec = new OaApprovalRecord(); + rec.setInstanceId(inst.getId()); + rec.setApproverId(me); + rec.setApproverName(LoginHelper.getNickName()); + rec.setAction(bo.getAction()); + rec.setComment(bo.getComment()); + rec.setOpTime(new Date()); + recordMapper.insert(rec); + + // 终结判定 + int finalStatus = decideFinalStatus(inst, bo.getAction(), me, approvers); + if (finalStatus != 0) { + inst.setStatus(finalStatus); + inst.setFinishTime(new Date()); + instanceMapper.updateById(inst); + } + return true; + } + + private int decideFinalStatus(OaApprovalInstance inst, Integer action, Long me, Set approvers) { + // 驳回:无论或签会签都立即驳回 + if (action != null && action == 2) return 2; + // 通过场景 + if (inst.getSignType() != null && inst.getSignType() == 1) { + // 或签:任一人通过即通过 + return 1; + } + // 会签:所有人都通过才通过 + Long passCount = recordMapper.selectCount( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, inst.getId()) + .eq(OaApprovalRecord::getAction, 1)); + return passCount != null && passCount >= approvers.size() ? 1 : 0; + } + + @Override + public Boolean withdraw(Long instanceId) { + OaApprovalInstance inst = instanceMapper.selectById(instanceId); + if (inst == null) throw new ServiceException("审批单不存在"); + if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("已终结,无法撤回"); + if (!Objects.equals(inst.getApplyUserId(), LoginHelper.getUserId())) throw new ServiceException("仅申请人可撤回"); + inst.setStatus(3); + inst.setFinishTime(new Date()); + return instanceMapper.updateById(inst) > 0; + } + + @Override + public OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId) { + OaApprovalInstance inst = instanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .eq(OaApprovalInstance::getBusinessId, businessId) + .orderByDesc(OaApprovalInstance::getId) + .last("LIMIT 1")); + if (inst == null) return null; + OaApprovalInstanceVo vo = toInstanceVo(inst); + vo.setRecords(loadRecords(inst.getId())); + return vo; + } + + @Override + public Map batchLatestStatus(String businessType, Collection businessIds) { + if (businessIds == null || businessIds.isEmpty()) return Collections.emptyMap(); + List list = instanceMapper.selectList( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .in(OaApprovalInstance::getBusinessId, businessIds) + .orderByAsc(OaApprovalInstance::getId)); + // 后面的覆盖前面的,得到最新 + Map result = new HashMap<>(); + for (OaApprovalInstance i : list) { + result.put(i.getBusinessId(), i.getStatus()); + } + return result; + } + + // ===================== 列表 ===================== + + @Override + public TableDataInfo pageMyPending(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getStatus, 0) + .and(w -> w.like(OaApprovalInstance::getApproverIds, "," + me + ",") + .or().likeRight(OaApprovalInstance::getApproverIds, me + ",") + .or().likeLeft(OaApprovalInstance::getApproverIds, "," + me) + .or().eq(OaApprovalInstance::getApproverIds, String.valueOf(me))) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageMyDone(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + List instIds = recordMapper.selectObjs( + Wrappers.lambdaQuery() + .select(OaApprovalRecord::getInstanceId) + .eq(OaApprovalRecord::getApproverId, me)) + .stream().filter(Objects::nonNull).map(o -> Long.parseLong(o.toString())).distinct().collect(Collectors.toList()); + if (instIds.isEmpty()) return TableDataInfo.build(new ArrayList<>()); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .in(OaApprovalInstance::getId, instIds) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageMySubmitted(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getApplyUserId, me) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageAll(PageQuery pageQuery, String businessType, Integer status) { + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .eq(status != null, OaApprovalInstance::getStatus, status) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public OaApprovalInstanceVo getDetail(Long instanceId) { + OaApprovalInstance inst = instanceMapper.selectById(instanceId); + if (inst == null) return null; + OaApprovalInstanceVo vo = toInstanceVo(inst); + vo.setRecords(loadRecords(instanceId)); + return vo; + } + + // ===================== 内部 ===================== + + private TableDataInfo buildInstancePage(Page page) { + List vos = page.getRecords().stream().map(this::toInstanceVo).collect(Collectors.toList()); + Page p = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + p.setRecords(vos); + return TableDataInfo.build(p); + } + + private OaApprovalInstanceVo toInstanceVo(OaApprovalInstance inst) { + OaApprovalInstanceVo vo = BeanUtil.toBean(inst, OaApprovalInstanceVo.class); + vo.setApproverNames(resolveNames(inst.getApproverIds())); + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, inst.getBusinessType())); + if (cfg != null) vo.setBusinessName(cfg.getBusinessName()); + return vo; + } + + private List loadRecords(Long instanceId) { + return recordMapper.selectVoList( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, instanceId) + .orderByAsc(OaApprovalRecord::getOpTime)); + } + + private void fillApproverNames(OaApprovalConfigVo vo) { + vo.setApproverNames(resolveNames(vo.getApproverIds())); + } + + private String resolveNames(String csvIds) { + Set ids = parseIds(csvIds); + if (ids.isEmpty()) return ""; + List names = new ArrayList<>(); + for (Long id : ids) { + try { + SysUser u = userService.selectUserById(id); + names.add(u != null ? u.getNickName() : String.valueOf(id)); + } catch (Exception e) { + names.add(String.valueOf(id)); + } + } + return String.join(",", names); + } + + private Set parseIds(String csv) { + if (StringUtils.isBlank(csv)) return Collections.emptySet(); + Set set = new LinkedHashSet<>(); + for (String s : csv.split(",")) { + String t = s.trim(); + if (t.isEmpty()) continue; + try { set.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {} + } + return set; + } +} diff --git a/sql/oa_approval.sql b/sql/oa_approval.sql new file mode 100644 index 0000000..c6e1ff2 --- /dev/null +++ b/sql/oa_approval.sql @@ -0,0 +1,69 @@ +-- ============================================================ +-- 通用审批框架(轻量自建) +-- 1. oa_approval_config 业务-审批人配置(可改) +-- 2. oa_approval_instance 审批单实例(一次提交一条) +-- 3. oa_approval_record 审批操作流水(一人一条) +-- ============================================================ + +DROP TABLE IF EXISTS oa_approval_record; +DROP TABLE IF EXISTS oa_approval_instance; +DROP TABLE IF EXISTS oa_approval_config; + +CREATE TABLE oa_approval_config ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + business_type VARCHAR(64) NOT NULL COMMENT '业务类型 key(如 purchase_req/contract)', + business_name VARCHAR(128) NOT NULL COMMENT '业务名称(展示用)', + approver_ids VARCHAR(512) NOT NULL COMMENT '审批人 sys_user.user_id 列表,逗号分隔', + sign_type TINYINT NOT NULL DEFAULT 1 COMMENT '1或签 2会签', + enabled TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0停用 1启用', + remark VARCHAR(255) DEFAULT NULL COMMENT '备注', + create_by VARCHAR(64) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64) DEFAULT NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + del_flag TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_biz_type (business_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务审批配置'; + +CREATE TABLE oa_approval_instance ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + business_type VARCHAR(64) NOT NULL COMMENT '业务类型 key', + business_id BIGINT NOT NULL COMMENT '业务表主键', + business_title VARCHAR(255) DEFAULT NULL COMMENT '业务标题(冗余展示)', + apply_user_id BIGINT NOT NULL COMMENT '申请人 user_id', + apply_user_name VARCHAR(64) DEFAULT NULL, + apply_time DATETIME NOT NULL, + approver_ids VARCHAR(512) NOT NULL COMMENT '快照:提交时的审批人', + sign_type TINYINT NOT NULL COMMENT '快照:1或签 2会签', + status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回 3撤回', + finish_time DATETIME DEFAULT NULL, + remark VARCHAR(255) DEFAULT NULL, + create_by VARCHAR(64) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_by VARCHAR(64) DEFAULT NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + del_flag TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (id), + KEY idx_biz (business_type, business_id), + KEY idx_status (status), + KEY idx_apply_user (apply_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批单实例'; + +CREATE TABLE oa_approval_record ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + instance_id BIGINT NOT NULL COMMENT '审批单 id', + approver_id BIGINT NOT NULL COMMENT '审批人 user_id', + approver_name VARCHAR(64) DEFAULT NULL, + action TINYINT NOT NULL COMMENT '1通过 2驳回', + comment VARCHAR(512) DEFAULT NULL, + op_time DATETIME NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_inst (instance_id), + KEY idx_approver (approver_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批操作流水'; + +-- 采购需求 默认配置(先用 admin=1 占位,部署后改) +INSERT INTO oa_approval_config (business_type, business_name, approver_ids, sign_type, enabled, remark) +VALUES ('purchase_req', '采购需求', '1', 1, 1, '默认或签,请在「审批配置」修改审批人');