Merge branch 'feat/approval-phase1' into feat/approval-t26

This commit is contained in:
2026-10-30 21:21:17 +08:00
13 changed files with 1808 additions and 9 deletions
@@ -163,6 +163,13 @@ public final class ApprovalConstants {
return CATEGORY_NAMES.getOrDefault(category, category);
}
/**
* 归档后单据为只读终态:任何写动作统一返回该提示。
*/
public static String writeBlockedMessage(String status) {
return STATUS_ARCHIVED.equals(status) ? "单据已归档,不能修改" : "当前状态不可修改";
}
public static String departmentLeaderRoleCode(Integer organizationId) {
return organizationId == null ? null : "dept_leader_" + organizationId;
}
@@ -1,6 +1,7 @@
package com.gxwebsoft.approval.controller;
import com.gxwebsoft.approval.param.ApprovalCompletionParam;
import com.gxwebsoft.approval.param.ApprovalCoordinationParam;
import com.gxwebsoft.approval.param.ApprovalMineParam;
import com.gxwebsoft.approval.service.ApprovalInstanceService;
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
@@ -40,6 +41,14 @@ public class ApprovalInstanceController extends BaseController {
return success("保存成功", instanceService.saveCompletion(param, user));
}
@PreAuthorize("hasAuthority('approval:instance:submit')")
@ApiOperation("新建或保存内部协调单草稿")
@PostMapping("/instance/coordination")
public ApiResult<Long> saveCoordination(@RequestBody ApprovalCoordinationParam param) {
User user = getLoginUser();
return success("保存成功", instanceService.saveCoordination(param, user));
}
@PreAuthorize("hasAuthority('approval:instance:submit')")
@ApiOperation("提交或重提")
@PostMapping("/instance/{id}/submit")
@@ -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;
@@ -77,6 +80,25 @@ public class ApprovalTaskController extends BaseController {
return success("驳回成功", instanceService.detail(result.getInstanceId(), user));
}
@PreAuthorize("hasAuthority('approval:instance:approve')")
@ApiOperation("存档确认")
@PostMapping("/task/{id}/confirm")
public ApiResult<ApprovalInstanceVO> confirm(@PathVariable("id") Long id) {
User user = getLoginUser();
ApprovalTaskService.Result result = taskService.confirm(id, user);
return success(result.getWarning() == null ? "存档成功" : result.getWarning(),
instanceService.detail(result.getInstanceId(), user));
}
@PreAuthorize("hasAuthority('approval:instance:archive')")
@ApiOperation("归档")
@PostMapping("/task/{id}/archive")
public ApiResult<ApprovalInstanceVO> archive(@PathVariable("id") Long id) {
User user = getLoginUser();
ApprovalTaskService.Result result = taskService.archive(id, user);
return success("归档成功", instanceService.detail(result.getInstanceId(), user));
}
@PreAuthorize("hasAuthority('approval:instance:submit')")
@ApiOperation("撤回")
@PostMapping("/instance/{id}/withdraw")
@@ -85,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()));
}
}
@@ -32,7 +32,7 @@
i.reporter_id,
i.reporter_name,
i.report_org_name,
i.accept_org_name,
COALESCE(i.accept_org_name, co.accept_org_name) AS accept_org_name,
i.archive_time,
i.current_node_key,
i.definition_id,
@@ -0,0 +1,31 @@
package com.gxwebsoft.approval.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 内部协调单草稿参数;id 为审批实例id,更新草稿时传入。
*
* <p>报送部门与经办人由服务端按登录态带出,不在参数中接收。</p>
*/
@Data
public class ApprovalCoordinationParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("审批实例id(更新草稿时传入)")
private Long id;
@ApiModelProperty("受理部门id,必须是启用组织")
private Integer acceptOrgId;
@ApiModelProperty("标题")
private String title;
@ApiModelProperty("内容")
private String content;
@ApiModelProperty("备注")
private String remark;
}
@@ -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;
}
@@ -162,9 +162,11 @@ public class ApprovalAttachmentService {
}
private void checkEditable(ApprovalInstance instance, User user) {
if (user == null || !instance.getReporterId().equals(user.getUserId())
|| !ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException("当前状态不可修改");
if (user == null || !instance.getReporterId().equals(user.getUserId())) {
throw new BusinessException("无权操作该单据");
}
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException(ApprovalConstants.writeBlockedMessage(instance.getStatus()));
}
}
@@ -16,6 +16,7 @@ import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
import com.gxwebsoft.approval.mapper.ApprovalRecordMapper;
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
import com.gxwebsoft.approval.param.ApprovalCompletionParam;
import com.gxwebsoft.approval.param.ApprovalCoordinationParam;
import com.gxwebsoft.approval.param.ApprovalMineParam;
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
import com.gxwebsoft.approval.support.ApprovalApprover;
@@ -99,6 +100,30 @@ public class ApprovalInstanceService {
return inTransaction(() -> createDraft(param, user, definition, organizationId, organizationName));
}
/**
* 新建或更新内部协调单草稿,返回实例id。
*
* <p>报送部门与经办人取登录态;受理部门必须是启用组织,保存时即校验。</p>
*/
public Long saveCoordination(ApprovalCoordinationParam param, User user) {
validateCoordination(param);
ApprovalOrganizationVO acceptOrganization = directoryService.getOrganization(param.getAcceptOrgId());
if (acceptOrganization == null) {
throw new BusinessException("受理部门不存在或已停用");
}
Integer organizationId = user.getOrganizationId();
ApprovalOrganizationVO organization = directoryService.getOrganization(organizationId);
String organizationName = organization == null ? null : organization.getOrganizationName();
if (param.getId() != null) {
return inTransaction(() ->
updateCoordinationDraft(param, user, organizationId, organizationName, acceptOrganization));
}
ApprovalFlowDefinition definition = flowService.getOrCreate(ApprovalConstants.FLOW_COORDINATION_INTERNAL);
return inTransaction(() ->
createCoordinationDraft(param, user, definition, organizationId, organizationName, acceptOrganization));
}
/**
* 提交或重提,返回节点缺配提示;正常提交返回 null。
*/
@@ -113,7 +138,14 @@ public class ApprovalInstanceService {
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException(submitBlockedMessage(instance.getStatus()));
}
if (completionForm(instanceId) == null) {
boolean coordination = ApprovalConstants.DOC_COORDINATION.equals(instance.getDocType());
ApprovalCoordinationForm coordinationForm = null;
if (coordination) {
coordinationForm = coordinationForm(instanceId);
if (coordinationForm == null) {
throw new BusinessException("单据内容不存在");
}
} else if (completionForm(instanceId) == null) {
throw new BusinessException("单据内容不存在");
}
@@ -130,6 +162,10 @@ public class ApprovalInstanceService {
if (leaders.isEmpty()) {
throw new BusinessException("报送部门负责人角色缺配");
}
ApprovalOrganizationVO acceptOrganization = null;
if (coordination) {
acceptOrganization = preflightAcceptOrganization(coordinationForm);
}
if (directoryService.listRoleMembers(ApprovalConstants.ROLE_ARCHIVIST).isEmpty()) {
throw new BusinessException("收文员角色缺配");
}
@@ -141,11 +177,30 @@ public class ApprovalInstanceService {
.map(this::toApprover)
.collect(Collectors.toList());
return inTransaction(() -> doSubmit(instanceId, user, organization, approvers));
ApprovalOrganizationVO acceptOrg = acceptOrganization;
return inTransaction(() -> doSubmit(instanceId, user, organization, acceptOrg, approvers));
}
/**
* 协调单提交预检:受理部门负责人角色、受理部门停用。
*/
private ApprovalOrganizationVO preflightAcceptOrganization(ApprovalCoordinationForm form) {
Integer acceptOrgId = form.getAcceptOrgId();
if (acceptOrgId == null || acceptOrgId <= 0) {
throw new BusinessException("请选择受理部门");
}
if (directoryService.listRoleMembers(ApprovalConstants.departmentLeaderRoleCode(acceptOrgId)).isEmpty()) {
throw new BusinessException("受理部门负责人角色缺配");
}
ApprovalOrganizationVO acceptOrganization = directoryService.getOrganization(acceptOrgId);
if (acceptOrganization == null) {
throw new BusinessException("受理部门已停用");
}
return acceptOrganization;
}
private String doSubmit(Long instanceId, User user, ApprovalOrganizationVO organization,
List<ApprovalApprover> approvers) {
ApprovalOrganizationVO acceptOrganization, List<ApprovalApprover> approvers) {
ApprovalInstance locked = engineService.lockInstance(instanceId);
if (!locked.getReporterId().equals(user.getUserId())
|| !ApprovalConstants.EDITABLE_STATUSES.contains(locked.getStatus())) {
@@ -154,8 +209,15 @@ public class ApprovalInstanceService {
boolean firstSubmit = StrUtil.isBlank(locked.getBizNo());
locked.setReportOrgId(organization.getOrganizationId());
locked.setReportOrgName(organization.getOrganizationName());
if (acceptOrganization != null) {
locked.setAcceptOrgId(acceptOrganization.getOrganizationId());
locked.setAcceptOrgName(acceptOrganization.getOrganizationName());
}
if (firstSubmit) {
locked.setBizNo(noService.nextNo(ApprovalConstants.NO_PREFIX_COMPLETION));
boolean coordination = ApprovalConstants.DOC_COORDINATION.equals(locked.getDocType());
locked.setBizNo(noService.nextNo(coordination
? ApprovalConstants.NO_PREFIX_COORDINATION
: ApprovalConstants.NO_PREFIX_COMPLETION));
}
locked.setStatus(ApprovalConstants.STATUS_PENDING);
locked.setRound((locked.getRound() == null ? 0 : locked.getRound()) + 1);
@@ -305,7 +367,7 @@ public class ApprovalInstanceService {
throw new BusinessException("无权操作该单据");
}
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException("当前状态不可修改");
throw new BusinessException(ApprovalConstants.writeBlockedMessage(instance.getStatus()));
}
ApprovalCompletionForm form = completionForm(instance.getId());
if (form == null) {
@@ -323,6 +385,79 @@ public class ApprovalInstanceService {
return instance.getId();
}
private Long createCoordinationDraft(ApprovalCoordinationParam param, User user,
ApprovalFlowDefinition definition,
Integer organizationId, String organizationName,
ApprovalOrganizationVO acceptOrganization) {
ApprovalInstance instance = new ApprovalInstance();
instance.setFlowCode(ApprovalConstants.FLOW_COORDINATION_INTERNAL);
instance.setDefinitionId(definition.getId());
instance.setDefinitionVersion(definition.getVersion());
instance.setDocType(ApprovalConstants.DOC_COORDINATION);
instance.setBizId(0L);
instance.setStatus(ApprovalConstants.STATUS_DRAFT);
instance.setRound(0);
instance.setReporterId(user.getUserId());
instance.setReporterName(displayName(user));
instance.setReportOrgId(organizationId);
instance.setReportOrgName(organizationName);
instanceMapper.insert(instance);
ApprovalCoordinationForm form = new ApprovalCoordinationForm();
form.setInstanceId(instance.getId());
form.setAcceptOrgId(acceptOrganization.getOrganizationId());
form.setAcceptOrgName(acceptOrganization.getOrganizationName());
form.setTitle(param.getTitle().trim());
form.setContent(param.getContent().trim());
form.setRemark(normalizeRemark(param.getRemark()));
coordinationFormMapper.insert(form);
instance.setBizId(form.getId());
instanceMapper.updateById(instance);
return instance.getId();
}
private Long updateCoordinationDraft(ApprovalCoordinationParam param, User user,
Integer organizationId, String organizationName,
ApprovalOrganizationVO acceptOrganization) {
ApprovalInstance instance = instanceMapper.selectById(param.getId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
if (!ApprovalConstants.DOC_COORDINATION.equals(instance.getDocType())) {
throw new BusinessException("单据类型不匹配");
}
if (!instance.getReporterId().equals(user.getUserId())) {
throw new BusinessException("无权操作该单据");
}
if (!ApprovalConstants.EDITABLE_STATUSES.contains(instance.getStatus())) {
throw new BusinessException("当前状态不可修改");
}
ApprovalCoordinationForm form = coordinationForm(instance.getId());
if (form == null) {
throw new BusinessException("单据内容不存在");
}
form.setAcceptOrgId(acceptOrganization.getOrganizationId());
form.setAcceptOrgName(acceptOrganization.getOrganizationName());
form.setTitle(param.getTitle().trim());
form.setContent(param.getContent().trim());
form.setRemark(normalizeRemark(param.getRemark()));
coordinationFormMapper.updateById(form);
instance.setReportOrgId(organizationId);
instance.setReportOrgName(organizationName);
instanceMapper.updateById(instance);
return instance.getId();
}
private String normalizeRemark(String remark) {
if (remark == null) {
return null;
}
String trimmed = remark.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private ApprovalInstanceVO baseVO(ApprovalInstance instance) {
ApprovalInstanceVO vo = new ApprovalInstanceVO();
vo.setId(instance.getId());
@@ -502,6 +637,33 @@ public class ApprovalInstanceService {
}
}
private void validateCoordination(ApprovalCoordinationParam param) {
if (param == null) {
throw new BusinessException("参数不能为空");
}
if (param.getAcceptOrgId() == null || param.getAcceptOrgId() <= 0) {
throw new BusinessException("请选择受理部门");
}
String title = param.getTitle() == null ? "" : param.getTitle().trim();
if (title.isEmpty()) {
throw new BusinessException("标题不能为空");
}
if (title.length() > 100) {
throw new BusinessException("标题不能超过100字");
}
String content = param.getContent() == null ? "" : param.getContent().trim();
if (content.isEmpty()) {
throw new BusinessException("内容不能为空");
}
if (content.length() > 2000) {
throw new BusinessException("内容不能超过2000字");
}
String remark = normalizeRemark(param.getRemark());
if (remark != null && remark.length() > 500) {
throw new BusinessException("备注不能超过500字");
}
}
private String submitBlockedMessage(String status) {
if (ApprovalConstants.STATUS_PENDING.equals(status)) {
return "单据正在审批中,不能重复提交";
@@ -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;
@@ -75,6 +77,45 @@ public class ApprovalTaskService {
return new Result(instance.getId(), warning);
}
/**
* 存档确认:CONFIRM 任务只能确认(无意见),同节点同轮全部完成后实例进入审批完成,
* 并激活归档节点;归档节点缺配时实例停在审批中并提示管理员补配置。
*/
public Result confirm(Long taskId, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_CONFIRM.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持存档确认");
}
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
List<ApprovalApprover> approvers =
next == null ? Collections.emptyList() : resolveApprovers(instance, next);
String warning = inTransaction(() -> doConfirm(taskId, task, user, next, approvers));
return new Result(instance.getId(), warning);
}
/**
* 归档:ARCHIVE 任务或签,处理后取消同节点其余待办,实例进入只读终态。
*/
public Result archive(Long taskId, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_ARCHIVE.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持归档");
}
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
inTransaction(() -> doArchive(taskId, task, user));
return new Result(instance.getId(), null);
}
/**
* 驳回:意见必填,同轮待办全部取消,实例回到报送人。
*/
@@ -101,6 +142,9 @@ public class ApprovalTaskService {
if (user == null || !instance.getReporterId().equals(user.getUserId())) {
throw new BusinessException("无权操作该单据");
}
if (ApprovalConstants.STATUS_ARCHIVED.equals(instance.getStatus())) {
throw new BusinessException("单据已归档,不能修改");
}
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
throw new BusinessException("当前状态不可撤回");
}
@@ -109,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());
}
@@ -131,6 +197,67 @@ public class ApprovalTaskService {
return created == 0 ? "当前节点缺少审批人,请联系管理员补配置" : null;
}
private String doConfirm(Long taskId, ApprovalTask task, User user,
ApprovalNode next, List<ApprovalApprover> approvers) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
ensureTaskHandleable(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, null) == 0) {
throw new BusinessException("任务已处理");
}
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_CONFIRM, null);
Integer remaining = taskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
.eq(ApprovalTask::getInstanceId, locked.getId())
.eq(ApprovalTask::getRound, locked.getRound())
.eq(ApprovalTask::getNodeKey, task.getNodeKey())
.eq(ApprovalTask::getStatus, ApprovalConstants.TASK_PENDING));
if (remaining != null && remaining > 0) {
return null;
}
if (next == null) {
return "当前节点缺少审批人,请联系管理员补配置";
}
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
if (created == 0) {
return "当前节点缺少审批人,请联系管理员补配置";
}
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_PENDING)
.set("status", ApprovalConstants.STATUS_APPROVED)
.set("approve_time", LocalDateTime.now()));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
return null;
}
private void doArchive(Long taskId, ApprovalTask task, User user) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
if (!ApprovalConstants.STATUS_APPROVED.equals(locked.getStatus())) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
ensureTaskCurrent(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, null) == 0) {
throw new BusinessException("任务已处理");
}
engineService.cancelPendingTasks(locked.getId(), locked.getRound(), taskId);
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_ARCHIVE, null);
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_APPROVED)
.set("status", ApprovalConstants.STATUS_ARCHIVED)
.set("archive_time", LocalDateTime.now())
.set("archivist_id", user.getUserId())
.set("archivist_name", displayName(user)));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
}
private void doReject(Long taskId, ApprovalTask task, String opinion, User user) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureTaskHandleable(task, locked);
@@ -171,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;
}
/**
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
*/
@@ -178,12 +357,22 @@ public class ApprovalTaskService {
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
ensureTaskCurrent(task, instance);
}
private void ensureTaskCurrent(ApprovalTask task, ApprovalInstance instance) {
if (!task.getRound().equals(instance.getRound())
|| !task.getNodeKey().equals(instance.getCurrentNodeKey())) {
throw new BusinessException("任务已处理");
}
}
private void ensureNotArchived(ApprovalInstance instance) {
if (ApprovalConstants.STATUS_ARCHIVED.equals(instance.getStatus())) {
throw new BusinessException("单据已归档,不能修改");
}
}
private void ensureNoHandledTask(ApprovalInstance instance) {
Integer handled = taskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
.eq(ApprovalTask::getInstanceId, instance.getId())
@@ -288,6 +477,16 @@ public class ApprovalTaskService {
return user.getUsername();
}
private String displayName(User user) {
if (StrUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StrUtil.isNotBlank(user.getNickname())) {
return user.getNickname();
}
return user.getUsername();
}
private String normalizeOpinion(String opinion) {
if (opinion == null) {
return null;
@@ -310,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;
}