feat(approval): 内部协调单表单、提交预检与双部门负责人审批 (#23)

This commit is contained in:
2026-10-30 21:07:35 +08:00
parent ddc0deb289
commit 7f26fafba3
5 changed files with 775 additions and 5 deletions
@@ -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")
@@ -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;
}
@@ -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);
@@ -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 "单据正在审批中,不能重复提交";