提交审批能力部分代码

This commit is contained in:
2026-06-16 15:37:35 +08:00
parent d294c7b5cd
commit 44949287e0
15 changed files with 835 additions and 0 deletions

View File

@@ -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<List<OaApprovalConfigVo>> configList() {
return R.ok(approvalService.listConfigs());
}
@GetMapping("/config/{businessType}")
public R<OaApprovalConfigVo> getConfig(@PathVariable String businessType) {
return R.ok(approvalService.getConfig(businessType));
}
@PostMapping("/config")
public R<Void> saveConfig(@RequestBody OaApprovalConfigBo bo) {
return toAjax(approvalService.saveConfig(bo));
}
@DeleteMapping("/config/{id}")
public R<Void> deleteConfig(@PathVariable Long id) {
return toAjax(approvalService.deleteConfig(id));
}
// ===== 审批 =====
@PostMapping("/act")
public R<Void> act(@Validated @RequestBody OaApprovalActionBo bo) {
return toAjax(approvalService.act(bo));
}
@PostMapping("/withdraw/{instanceId}")
public R<Void> withdraw(@PathVariable Long instanceId) {
return toAjax(approvalService.withdraw(instanceId));
}
// ===== 查询 =====
@GetMapping("/detail/{instanceId}")
public R<OaApprovalInstanceVo> detail(@PathVariable Long instanceId) {
return R.ok(approvalService.getDetail(instanceId));
}
@GetMapping("/latest")
public R<OaApprovalInstanceVo> latest(@NotNull @RequestParam String businessType,
@NotNull @RequestParam Long businessId) {
return R.ok(approvalService.getLatestByBusiness(businessType, businessId));
}
@GetMapping("/mine/pending")
public TableDataInfo<OaApprovalInstanceVo> myPending(PageQuery pageQuery,
@RequestParam(required = false) String businessType) {
return approvalService.pageMyPending(pageQuery, businessType);
}
@GetMapping("/mine/done")
public TableDataInfo<OaApprovalInstanceVo> myDone(PageQuery pageQuery,
@RequestParam(required = false) String businessType) {
return approvalService.pageMyDone(pageQuery, businessType);
}
@GetMapping("/mine/submitted")
public TableDataInfo<OaApprovalInstanceVo> mySubmitted(PageQuery pageQuery,
@RequestParam(required = false) String businessType) {
return approvalService.pageMySubmitted(pageQuery, businessType);
}
@GetMapping("/list")
public TableDataInfo<OaApprovalInstanceVo> all(PageQuery pageQuery,
@RequestParam(required = false) String businessType,
@RequestParam(required = false) Integer status) {
return approvalService.pageAll(pageQuery, businessType, status);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<OaApprovalRecordVo> records;
}

View File

@@ -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;
}

View File

@@ -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<OaApprovalConfigMapper, OaApprovalConfig, OaApprovalConfigVo> {
}

View File

@@ -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<OaApprovalInstanceMapper, OaApprovalInstance, OaApprovalInstanceVo> {
}

View File

@@ -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<OaApprovalRecordMapper, OaApprovalRecord, OaApprovalRecordVo> {
}

View File

@@ -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<OaApprovalConfigVo> 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<Long, Integer> batchLatestStatus(String businessType, Collection<Long> businessIds);
// ===== 列表 =====
/** 我待办审批人是我status=0 */
TableDataInfo<OaApprovalInstanceVo> pageMyPending(PageQuery pageQuery, String businessType);
/** 我已办(出现在 record 里) */
TableDataInfo<OaApprovalInstanceVo> pageMyDone(PageQuery pageQuery, String businessType);
/** 我发起的 */
TableDataInfo<OaApprovalInstanceVo> pageMySubmitted(PageQuery pageQuery, String businessType);
/** 全部(管理员) */
TableDataInfo<OaApprovalInstanceVo> pageAll(PageQuery pageQuery, String businessType, Integer status);
/** 单条详情(带流水) */
OaApprovalInstanceVo getDetail(Long instanceId);
}

View File

@@ -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<OaApprovalConfigVo> listConfigs() {
List<OaApprovalConfigVo> list = configMapper.selectVoList(
Wrappers.<OaApprovalConfig>lambdaQuery().orderByAsc(OaApprovalConfig::getBusinessType));
list.forEach(this::fillApproverNames);
return list;
}
@Override
public OaApprovalConfigVo getConfig(String businessType) {
OaApprovalConfig cfg = configMapper.selectOne(
Wrappers.<OaApprovalConfig>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.<OaApprovalConfig>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.<OaApprovalConfig>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.<OaApprovalInstance>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<Long> approvers = parseIds(inst.getApproverIds());
if (!approvers.contains(me)) throw new ServiceException("无审批权限");
// 防重:同一人对同一单只能操作一次
Long already = recordMapper.selectCount(
Wrappers.<OaApprovalRecord>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<Long> approvers) {
// 驳回:无论或签会签都立即驳回
if (action != null && action == 2) return 2;
// 通过场景
if (inst.getSignType() != null && inst.getSignType() == 1) {
// 或签:任一人通过即通过
return 1;
}
// 会签:所有人都通过才通过
Long passCount = recordMapper.selectCount(
Wrappers.<OaApprovalRecord>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.<OaApprovalInstance>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<Long, Integer> batchLatestStatus(String businessType, Collection<Long> businessIds) {
if (businessIds == null || businessIds.isEmpty()) return Collections.emptyMap();
List<OaApprovalInstance> list = instanceMapper.selectList(
Wrappers.<OaApprovalInstance>lambdaQuery()
.eq(OaApprovalInstance::getBusinessType, businessType)
.in(OaApprovalInstance::getBusinessId, businessIds)
.orderByAsc(OaApprovalInstance::getId));
// 后面的覆盖前面的,得到最新
Map<Long, Integer> result = new HashMap<>();
for (OaApprovalInstance i : list) {
result.put(i.getBusinessId(), i.getStatus());
}
return result;
}
// ===================== 列表 =====================
@Override
public TableDataInfo<OaApprovalInstanceVo> pageMyPending(PageQuery pageQuery, String businessType) {
Long me = LoginHelper.getUserId();
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>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<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
return buildInstancePage(page);
}
@Override
public TableDataInfo<OaApprovalInstanceVo> pageMyDone(PageQuery pageQuery, String businessType) {
Long me = LoginHelper.getUserId();
List<Long> instIds = recordMapper.selectObjs(
Wrappers.<OaApprovalRecord>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<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
.in(OaApprovalInstance::getId, instIds)
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
.orderByDesc(OaApprovalInstance::getId);
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
return buildInstancePage(page);
}
@Override
public TableDataInfo<OaApprovalInstanceVo> pageMySubmitted(PageQuery pageQuery, String businessType) {
Long me = LoginHelper.getUserId();
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
.eq(OaApprovalInstance::getApplyUserId, me)
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
.orderByDesc(OaApprovalInstance::getId);
Page<OaApprovalInstance> page = instanceMapper.selectPage(pageQuery.build(), qw);
return buildInstancePage(page);
}
@Override
public TableDataInfo<OaApprovalInstanceVo> pageAll(PageQuery pageQuery, String businessType, Integer status) {
LambdaQueryWrapper<OaApprovalInstance> qw = Wrappers.<OaApprovalInstance>lambdaQuery()
.eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType)
.eq(status != null, OaApprovalInstance::getStatus, status)
.orderByDesc(OaApprovalInstance::getId);
Page<OaApprovalInstance> 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<OaApprovalInstanceVo> buildInstancePage(Page<OaApprovalInstance> page) {
List<OaApprovalInstanceVo> vos = page.getRecords().stream().map(this::toInstanceVo).collect(Collectors.toList());
Page<OaApprovalInstanceVo> 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.<OaApprovalConfig>lambdaQuery().eq(OaApprovalConfig::getBusinessType, inst.getBusinessType()));
if (cfg != null) vo.setBusinessName(cfg.getBusinessName());
return vo;
}
private List<OaApprovalRecordVo> loadRecords(Long instanceId) {
return recordMapper.selectVoList(
Wrappers.<OaApprovalRecord>lambdaQuery()
.eq(OaApprovalRecord::getInstanceId, instanceId)
.orderByAsc(OaApprovalRecord::getOpTime));
}
private void fillApproverNames(OaApprovalConfigVo vo) {
vo.setApproverNames(resolveNames(vo.getApproverIds()));
}
private String resolveNames(String csvIds) {
Set<Long> ids = parseIds(csvIds);
if (ids.isEmpty()) return "";
List<String> 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<Long> parseIds(String csv) {
if (StringUtils.isBlank(csv)) return Collections.emptySet();
Set<Long> 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;
}
}

69
sql/oa_approval.sql Normal file
View File

@@ -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, '默认或签,请在「审批配置」修改审批人');