Merge branch 'feat/approval-phase1' into feat/approval-t21
This commit is contained in:
@@ -34,7 +34,7 @@ public class ApprovalDirectoryController extends BaseController {
|
||||
return success(directoryService.listOrganizations());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:assign')")
|
||||
@PreAuthorize("hasAnyAuthority('approval:instance:list', 'approval:instance:assign')")
|
||||
@ApiOperation("启用用户搜索")
|
||||
@GetMapping("/users")
|
||||
public ApiResult<List<ApprovalUserVO>> users(
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.gxwebsoft.approval.controller;
|
||||
|
||||
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||
import com.gxwebsoft.approval.service.ApprovalQueryService;
|
||||
import com.gxwebsoft.approval.vo.ApprovalItemVO;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* PC 单据查询接口。
|
||||
*/
|
||||
@Api(tags = "审批单据查询")
|
||||
@RestController
|
||||
@RequestMapping("/api/approval")
|
||||
public class ApprovalQueryController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ApprovalQueryService queryService;
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:list')")
|
||||
@ApiOperation("PC 单据查询分页")
|
||||
@GetMapping("/instance/page")
|
||||
public ApiResult<PageResult<ApprovalItemVO>> page(ApprovalQueryParam param) {
|
||||
return success(queryService.page(param, getLoginUser()));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.gxwebsoft.approval.controller;
|
||||
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskActionParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
|
||||
import com.gxwebsoft.approval.service.ApprovalInstanceService;
|
||||
import com.gxwebsoft.approval.service.ApprovalTaskService;
|
||||
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalItemVO;
|
||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
@@ -104,4 +107,13 @@ public class ApprovalTaskController extends BaseController {
|
||||
ApprovalTaskService.Result result = taskService.withdraw(id, user);
|
||||
return success("撤回成功", instanceService.detail(result.getInstanceId(), user));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('approval:instance:assign')")
|
||||
@OperationLog(value = "改派审批待办", module = "审批管理")
|
||||
@ApiOperation("改派")
|
||||
@PostMapping("/task/{id}/assign")
|
||||
public ApiResult<ApprovalAssignVO> assign(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ApprovalTaskAssignParam param) {
|
||||
return success("改派成功", taskService.assign(id, param, getLoginUser()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.gxwebsoft.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalItemRow;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PC 单据查询Mapper:两类单据联表单,数据权限条件与详情一致。
|
||||
*/
|
||||
public interface ApprovalQueryMapper {
|
||||
|
||||
/**
|
||||
* 单据查询分页,默认提交时间倒序、同秒 id 倒序。
|
||||
*/
|
||||
List<ApprovalItemRow> selectInstancePage(@Param("page") IPage<ApprovalItemRow> page,
|
||||
@Param("param") ApprovalQueryParam param,
|
||||
@Param("userId") Integer userId,
|
||||
@Param("allVisible") Integer allVisible);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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.gxwebsoft.approval.mapper.ApprovalQueryMapper">
|
||||
|
||||
<!-- PC 单据查询:数据权限条件与详情保持一致(草稿仅发起人可见) -->
|
||||
<select id="selectInstancePage" resultType="com.gxwebsoft.approval.support.ApprovalItemRow">
|
||||
SELECT i.id,
|
||||
i.biz_no,
|
||||
i.doc_type,
|
||||
i.status,
|
||||
i.reporter_id,
|
||||
i.reporter_name,
|
||||
i.report_org_name,
|
||||
i.accept_org_name,
|
||||
i.archive_time,
|
||||
i.current_node_key,
|
||||
i.definition_id,
|
||||
i.submit_time,
|
||||
COALESCE(f.name, co.title) AS title
|
||||
FROM approval_instance i
|
||||
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||
WHERE (
|
||||
i.reporter_id = #{userId}
|
||||
OR (i.status != 'DRAFT' AND (
|
||||
#{allVisible} = 1
|
||||
OR EXISTS (SELECT 1 FROM approval_task t WHERE t.instance_id = i.id AND t.assignee_id = #{userId})
|
||||
OR EXISTS (SELECT 1 FROM approval_record r WHERE r.instance_id = i.id AND r.operator_id = #{userId})
|
||||
OR i.dispatch_target_id = #{userId}
|
||||
))
|
||||
)
|
||||
<if test="param.docType != null and param.docType != ''">
|
||||
AND i.doc_type = #{param.docType}
|
||||
</if>
|
||||
<if test="param.bizNo != null and param.bizNo != ''">
|
||||
AND i.biz_no LIKE CONCAT('%', #{param.bizNo}, '%')
|
||||
</if>
|
||||
<if test="param.reportOrgId != null">
|
||||
AND i.report_org_id = #{param.reportOrgId}
|
||||
</if>
|
||||
<if test="param.acceptOrgId != null">
|
||||
AND i.accept_org_id = #{param.acceptOrgId}
|
||||
</if>
|
||||
<if test="param.reporterId != null">
|
||||
AND i.reporter_id = #{param.reporterId}
|
||||
</if>
|
||||
<if test="param.status != null and param.status != ''">
|
||||
AND i.status = #{param.status}
|
||||
</if>
|
||||
<if test="param.keyword != null and param.keyword != ''">
|
||||
AND (i.biz_no LIKE CONCAT('%', #{param.keyword}, '%')
|
||||
OR f.name LIKE CONCAT('%', #{param.keyword}, '%')
|
||||
OR co.title LIKE CONCAT('%', #{param.keyword}, '%')
|
||||
OR f.implement_org LIKE CONCAT('%', #{param.keyword}, '%'))
|
||||
</if>
|
||||
<if test="param.submitTimeStart != null and param.submitTimeStart != ''">
|
||||
AND i.submit_time >= #{param.submitTimeStart}
|
||||
</if>
|
||||
<if test="param.submitTimeEnd != null and param.submitTimeEnd != ''">
|
||||
AND i.submit_time <= #{param.submitTimeEnd}
|
||||
</if>
|
||||
<if test="param.archiveTimeStart != null and param.archiveTimeStart != ''">
|
||||
AND i.archive_time >= #{param.archiveTimeStart}
|
||||
</if>
|
||||
<if test="param.archiveTimeEnd != null and param.archiveTimeEnd != ''">
|
||||
AND i.archive_time <= #{param.archiveTimeEnd}
|
||||
</if>
|
||||
ORDER BY (i.submit_time IS NULL) ASC, i.submit_time DESC, i.id DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.gxwebsoft.approval.param;
|
||||
|
||||
import com.gxwebsoft.common.core.web.BaseParam;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* PC 单据查询参数:两类单据统一检索,含归档单据。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ApiModel(description = "审批单据查询参数")
|
||||
public class ApprovalQueryParam extends BaseParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("单据类型 completion/coordination")
|
||||
private String docType;
|
||||
|
||||
@ApiModelProperty("单号")
|
||||
private String bizNo;
|
||||
|
||||
@ApiModelProperty("关键词:单号、名称或标题、实施单位")
|
||||
private String keyword;
|
||||
|
||||
@ApiModelProperty("报送部门id")
|
||||
private Integer reportOrgId;
|
||||
|
||||
@ApiModelProperty("受理部门id")
|
||||
private Integer acceptOrgId;
|
||||
|
||||
@ApiModelProperty("发起人id")
|
||||
private Integer reporterId;
|
||||
|
||||
@ApiModelProperty("单据状态")
|
||||
private String status;
|
||||
|
||||
@ApiModelProperty("提交时间起,yyyy-MM-dd HH:mm:ss")
|
||||
private String submitTimeStart;
|
||||
|
||||
@ApiModelProperty("提交时间止,yyyy-MM-dd HH:mm:ss")
|
||||
private String submitTimeEnd;
|
||||
|
||||
@ApiModelProperty("归档时间起,yyyy-MM-dd HH:mm:ss")
|
||||
private String archiveTimeStart;
|
||||
|
||||
@ApiModelProperty("归档时间止,yyyy-MM-dd HH:mm:ss")
|
||||
private String archiveTimeEnd;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gxwebsoft.approval.param;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 待办改派参数。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalTaskAssignParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("改派对象用户id")
|
||||
private Integer targetUserId;
|
||||
|
||||
@ApiModelProperty("改派原因;必填")
|
||||
private String reason;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gxwebsoft.approval.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalQueryMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalItemRow;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalItemVO;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PC 单据查询服务:两类单据统一分页检索,列表与详情共用同一套数据权限条件。
|
||||
*
|
||||
* <p>跨库约束:allVisible 角色判定走 core 专用连接,本方法不开启事务。</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApprovalQueryService {
|
||||
|
||||
private static final long DEFAULT_LIMIT = 10L;
|
||||
|
||||
@Resource
|
||||
private ApprovalQueryMapper queryMapper;
|
||||
|
||||
@Resource
|
||||
private ApprovalFlowService flowService;
|
||||
|
||||
@Resource
|
||||
private ApprovalDirectoryService directoryService;
|
||||
|
||||
/**
|
||||
* 单据查询分页,默认提交时间倒序、同秒按 id 倒序。
|
||||
*/
|
||||
public PageResult<ApprovalItemVO> page(ApprovalQueryParam param, User user) {
|
||||
long current = param.getPage() == null ? 1 : Math.max(1, param.getPage());
|
||||
long size = param.getLimit() == null ? DEFAULT_LIMIT : Math.max(1, param.getLimit());
|
||||
int allVisible = directoryService.hasAnyRole(user.getUserId(),
|
||||
ApprovalConstants.ROLE_SUPER_ADMIN, ApprovalConstants.ROLE_ARCHIVIST) ? 1 : 0;
|
||||
Page<ApprovalItemRow> page = new Page<>(current, size);
|
||||
List<ApprovalItemRow> rows = queryMapper.selectInstancePage(page, param, user.getUserId(), allVisible);
|
||||
Map<Long, List<ApprovalNode>> nodeCache = new HashMap<>();
|
||||
List<ApprovalItemVO> list = new ArrayList<>(rows.size());
|
||||
for (ApprovalItemRow row : rows) {
|
||||
list.add(toItem(row, nodeCache));
|
||||
}
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
private ApprovalItemVO toItem(ApprovalItemRow row, Map<Long, List<ApprovalNode>> nodeCache) {
|
||||
ApprovalItemVO item = new ApprovalItemVO();
|
||||
item.setId(row.getId());
|
||||
item.setBizNo(row.getBizNo());
|
||||
item.setDocType(row.getDocType());
|
||||
item.setDocTypeName(ApprovalConstants.docTypeName(row.getDocType()));
|
||||
item.setStatus(row.getStatus());
|
||||
item.setStatusName(ApprovalConstants.statusName(row.getStatus()));
|
||||
item.setTitle(row.getTitle());
|
||||
item.setSubmitTime(row.getSubmitTime());
|
||||
item.setReporterId(row.getReporterId());
|
||||
item.setReporterName(row.getReporterName());
|
||||
item.setReportOrgName(row.getReportOrgName());
|
||||
item.setAcceptOrgName(row.getAcceptOrgName());
|
||||
item.setArchiveTime(row.getArchiveTime());
|
||||
item.setCurrentNodeName(nodeName(nodeCache, row.getDefinitionId(), row.getCurrentNodeKey()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private String nodeName(Map<Long, List<ApprovalNode>> cache, Long definitionId, String nodeKey) {
|
||||
if (definitionId == null || nodeKey == null) {
|
||||
return null;
|
||||
}
|
||||
List<ApprovalNode> nodes = cache.computeIfAbsent(definitionId, flowService::parseNodes);
|
||||
return nodes.stream()
|
||||
.filter(node -> nodeKey.equals(node.getKey()))
|
||||
.map(ApprovalNode::getName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.entity.ApprovalTask;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalApprover;
|
||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
|
||||
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
@@ -151,6 +153,28 @@ public class ApprovalTaskService {
|
||||
return new Result(instanceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派:管理员把待处理任务换给同租户其他启用用户,原因必填并写 ASSIGN 记录。
|
||||
*
|
||||
* <p>改派不改变任务状态,只替换处理人;操作人不必是任务当前处理人。</p>
|
||||
*/
|
||||
public ApprovalAssignVO assign(Long taskId, ApprovalTaskAssignParam param, User user) {
|
||||
if (param == null || param.getTargetUserId() == null) {
|
||||
throw new BusinessException("请选择改派对象");
|
||||
}
|
||||
String reason = requireAssignReason(param.getReason());
|
||||
ApprovalTask task = requireAssignableTask(taskId);
|
||||
// 事务外解析目标用户:启用、未删除且属于当前租户(@DS("server") 查询显式带租户)
|
||||
List<ApprovalUserVO> targets =
|
||||
directoryService.listActiveUsersByIds(Collections.singletonList(param.getTargetUserId()));
|
||||
if (targets.isEmpty()) {
|
||||
throw new BusinessException("改派对象不存在或已停用");
|
||||
}
|
||||
ApprovalUserVO target = targets.get(0);
|
||||
String targetName = displayName(target);
|
||||
return inTransaction(() -> doAssign(taskId, task, target.getUserId(), targetName, reason, user));
|
||||
}
|
||||
|
||||
public Integer todoCount(User user) {
|
||||
return taskMapper.countPendingInstances(user.getUserId());
|
||||
}
|
||||
@@ -274,6 +298,58 @@ public class ApprovalTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务内改派:锁实例后条件更新处理人,并追加 ASSIGN 记录。
|
||||
*/
|
||||
private ApprovalAssignVO doAssign(Long taskId, ApprovalTask task, Integer targetUserId,
|
||||
String targetName, String reason, User user) {
|
||||
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
|
||||
ensureTaskHandleable(task, locked);
|
||||
int rows = taskMapper.update(null, new UpdateWrapper<ApprovalTask>()
|
||||
.eq("id", taskId)
|
||||
.eq("status", ApprovalConstants.TASK_PENDING)
|
||||
.set("assignee_id", targetUserId)
|
||||
.set("assignee_name", targetName));
|
||||
if (rows == 0) {
|
||||
throw new BusinessException("任务已处理");
|
||||
}
|
||||
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
|
||||
ApprovalConstants.ACTION_ASSIGN, reason);
|
||||
ApprovalAssignVO vo = new ApprovalAssignVO();
|
||||
vo.setInstanceId(locked.getId());
|
||||
vo.setTaskId(taskId);
|
||||
vo.setOriginalAssigneeId(task.getAssigneeId());
|
||||
vo.setOriginalAssigneeName(task.getAssigneeName());
|
||||
vo.setAssigneeId(targetUserId);
|
||||
vo.setAssigneeName(targetName);
|
||||
vo.setReason(reason);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派前的基本校验:任务存在、待处理且单据仍在审批中。
|
||||
*/
|
||||
private ApprovalTask requireAssignableTask(Long taskId) {
|
||||
if (taskId == null) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
ApprovalTask task = taskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
if (!ApprovalConstants.TASK_PENDING.equals(task.getStatus())) {
|
||||
throw new BusinessException("任务已处理");
|
||||
}
|
||||
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
|
||||
if (instance == null) {
|
||||
throw new BusinessException("单据不存在");
|
||||
}
|
||||
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
|
||||
throw new BusinessException("单据不在审批中,无法改派");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
|
||||
*/
|
||||
@@ -433,6 +509,14 @@ public class ApprovalTaskService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String requireAssignReason(String reason) {
|
||||
String normalized = normalizeOpinion(reason);
|
||||
if (normalized == null) {
|
||||
throw new BusinessException("请填写改派原因");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private <T> T inTransaction(Supplier<T> action) {
|
||||
return new TransactionTemplate(transactionManager).execute(status -> action.get());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gxwebsoft.approval.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 改派结果:原处理人、新处理人与原因,同时用于操作日志留痕。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "改派结果")
|
||||
public class ApprovalAssignVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long instanceId;
|
||||
|
||||
private Long taskId;
|
||||
|
||||
private Integer originalAssigneeId;
|
||||
|
||||
private String originalAssigneeName;
|
||||
|
||||
private Integer assigneeId;
|
||||
|
||||
private String assigneeName;
|
||||
|
||||
private String reason;
|
||||
}
|
||||
Reference in New Issue
Block a user