merge: t21 存档确认与归档闭环
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -80,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")
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -367,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) {
|
||||
|
||||
@@ -77,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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回:意见必填,同轮待办全部取消,实例回到报送人。
|
||||
*/
|
||||
@@ -103,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("当前状态不可撤回");
|
||||
}
|
||||
@@ -155,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);
|
||||
@@ -254,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())
|
||||
@@ -364,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;
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
package com.gxwebsoft.approval;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 存档确认与归档闭环集成测试:全员存档、部分存档、归档或签、归档后只读、
|
||||
* 归档缺配阻断、收文员与 superAdmin 全租户可见。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalConfirmArchiveIT {
|
||||
|
||||
/**
|
||||
* 独立租户:收文员解析按 (tenant_id, role_code) 全局匹配,使用专属租户可避免
|
||||
* 与并行执行的其它审批测试类互相污染角色成员。
|
||||
*/
|
||||
private static final int TENANT = 10024;
|
||||
private static final String LIST = "approval:instance:list";
|
||||
private static final String SUBMIT = "approval:instance:submit";
|
||||
private static final String APPROVE = "approval:instance:approve";
|
||||
private static final String ARCHIVE = "approval:instance:archive";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private ApprovalTestData.TestUser leader;
|
||||
private ApprovalTestData.TestUser archivist;
|
||||
private ApprovalTestData.TestUser secondArchivist;
|
||||
private ApprovalTestData.TestUser superAdmin;
|
||||
private ApprovalTestData.TestUser outsider;
|
||||
private Integer organizationId;
|
||||
private Integer archivistRoleId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
organizationId = data.createOrganization("归档部门" + data.uniqueSuffix(), 136, TENANT);
|
||||
reporter = data.createUser("归档报送人" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
ApprovalTestData.TestRole leaderRole =
|
||||
data.createRole("dept_leader_" + organizationId, "部门负责人", TENANT);
|
||||
leader = data.createUser("归档负责人" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
data.bindTenant(leader.userId, leaderRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole archivistRole = data.createRole("approval_archivist", "收文员", TENANT);
|
||||
archivistRoleId = archivistRole.roleId;
|
||||
archivist = data.createUser("归档收文员" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
secondArchivist = data.createUser("归档收文员B" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
data.bindTenant(archivist.userId, archivistRoleId, TENANT);
|
||||
data.bindTenant(secondArchivist.userId, archivistRoleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole superAdminRole = data.createRole("superAdmin", "超级管理员", TENANT);
|
||||
superAdmin = data.createUser("归档超管" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
data.bindTenant(superAdmin.userId, superAdminRole.roleId, TENANT);
|
||||
|
||||
outsider = data.createUser("归档路人" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsOnlyAfterAllConfirmTasksDone() throws Exception {
|
||||
long instanceId = createAndSubmit("全员存档");
|
||||
approveByLeader(instanceId);
|
||||
long reporterTask = pendingTaskId(instanceId, reporter.userId);
|
||||
|
||||
// 存档节点缺配前先验证:CONFIRM 任务不能驳回,也不需要意见
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + reporterTask + "/reject"),
|
||||
reporter, organizationId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"不同意\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不支持驳回")));
|
||||
|
||||
// 追加一名并存档人(协调单场景:报送人 + 派单对象)
|
||||
ApprovalTestData.TestUser secondConfirmer = data.createUser("归档存档人B" + data.uniqueSuffix(), organizationId);
|
||||
long secondTask = insertConfirmTask(instanceId, secondConfirmer);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), secondConfirmer, organizationId, LIST))
|
||||
.andExpect(jsonPath("$.data.confirms.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.confirms[1].status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.confirms[1].statusName").value("待确认"))
|
||||
.andExpect(jsonPath("$.data.permissions.canConfirm").value(true));
|
||||
|
||||
// 部分存档:仍等待其他人
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + reporterTask + "/confirm"), reporter, organizationId, APPROVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("存档"))
|
||||
.andExpect(jsonPath("$.data.confirms[0].status").value("DONE"))
|
||||
.andExpect(jsonPath("$.data.confirms[0].statusName").value("已确认"))
|
||||
.andExpect(jsonPath("$.data.confirms[0].handleTime").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.confirms[1].status").value("PENDING"));
|
||||
assertThat(data.queryMaster("SELECT approve_time FROM approval_instance WHERE id = ?",
|
||||
Object.class, instanceId)).isNull();
|
||||
|
||||
// 全员存档完成 → APPROVED,并为全部收文员生成或签归档任务
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + secondTask + "/confirm"),
|
||||
secondConfirmer, organizationId, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("审批完成"))
|
||||
.andExpect(jsonPath("$.data.approveTime").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("归档"))
|
||||
.andExpect(jsonPath("$.data.confirms[1].status").value("DONE"));
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_type = 'ARCHIVE' AND status = 'PENDING'", instanceId)).isEqualTo(2);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'CONFIRM'",
|
||||
instanceId)).isEqualTo(2);
|
||||
// 存档记录不带意见
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'CONFIRM' AND opinion IS NOT NULL", instanceId)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void archivesOnceAndCancelsOtherArchiveTasks() throws Exception {
|
||||
long instanceId = createAndSubmit("归档或签");
|
||||
approveByLeader(instanceId);
|
||||
confirmReporterTask(instanceId);
|
||||
|
||||
long firstArchive = pendingTaskId(instanceId, archivist.userId);
|
||||
long secondArchive = pendingTaskId(instanceId, secondArchivist.userId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + firstArchive + "/archive"), archivist, organizationId, ARCHIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("已归档"))
|
||||
.andExpect(jsonPath("$.data.archiveTime").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.archivistId").value(archivist.userId))
|
||||
.andExpect(jsonPath("$.data.archivistName").value(archivist.username));
|
||||
|
||||
assertThat(data.queryMaster("SELECT status FROM approval_task WHERE id = ?", String.class, secondArchive))
|
||||
.isEqualTo("CANCELLED");
|
||||
assertThat(data.queryMaster("SELECT handle_time FROM approval_task WHERE id = ?", Object.class, firstArchive))
|
||||
.isNotNull();
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'ARCHIVE'",
|
||||
instanceId)).isEqualTo(1);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + secondArchive + "/archive"),
|
||||
secondArchivist, organizationId, ARCHIVE))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("已归档")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEveryWriteAfterArchive() throws Exception {
|
||||
long instanceId = createAndSubmit("归档只读");
|
||||
long attachmentId = data.queryMaster("SELECT id FROM approval_attachment WHERE instance_id = ? LIMIT 1",
|
||||
Long.class, instanceId);
|
||||
approveByLeader(instanceId);
|
||||
long confirmTask = pendingTaskId(instanceId, reporter.userId);
|
||||
confirmReporterTask(instanceId);
|
||||
long archiveTask = pendingTaskId(instanceId, archivist.userId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/archive"), archivist, organizationId, ARCHIVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canSubmit").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canWithdraw").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canConfirm").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canArchive").value(false))
|
||||
.andExpect(jsonPath("$.data.myTask").doesNotExist());
|
||||
|
||||
String archived = "单据已归档,不能修改";
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, organizationId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(archived));
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/withdraw"), reporter, organizationId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(archived));
|
||||
mockMvc.perform(authed(post("/api/approval/instance/completion"), reporter, organizationId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"id\":" + instanceId + ",\"name\":\"改归档单据\","
|
||||
+ "\"implementOrg\":\"广西体育中心实施部\",\"fee\":1.00,"
|
||||
+ "\"serviceContent\":\"尝试修改已归档单据\"}"))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(archived));
|
||||
mockMvc.perform(authed(delete("/api/approval/attachment/" + attachmentId), reporter, organizationId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(archived));
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + confirmTask + "/confirm"), reporter, organizationId, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(archived));
|
||||
|
||||
assertThat(data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("ARCHIVED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void blocksWhenArchiveNodeHasNoApprover() throws Exception {
|
||||
long instanceId = createAndSubmit("归档缺配");
|
||||
approveByLeader(instanceId);
|
||||
|
||||
// 提交后收文员角色被清空,存档完成时归档节点缺配
|
||||
data.server().update("DELETE FROM sys_user_role WHERE role_id = ?", archivistRoleId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, reporter.userId) + "/confirm"),
|
||||
reporter, organizationId, APPROVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("缺少审批人")))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("归档"))
|
||||
.andExpect(jsonPath("$.data.approveTime").doesNotExist());
|
||||
|
||||
assertThat(data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("PENDING");
|
||||
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("archive");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_type = 'ARCHIVE'", instanceId)).isZero();
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'CONFIRM'",
|
||||
instanceId)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void letsArchivistAndSuperAdminViewAllTenantInstances() throws Exception {
|
||||
long instanceId = createAndSubmit("全租户可见");
|
||||
approveByLeader(instanceId);
|
||||
confirmReporterTask(instanceId);
|
||||
long archiveTask = pendingTaskId(instanceId, archivist.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/archive"), archivist, organizationId, ARCHIVE))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
// 收文员与 superAdmin 非参与人也可见完整详情(含归档人/时间与附件)
|
||||
ApprovalTestData.TestUser otherArchivist =
|
||||
data.createUser("归档外聘收文员" + data.uniqueSuffix(), organizationId, TENANT);
|
||||
data.bindTenant(otherArchivist.userId, archivistRoleId, TENANT);
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), otherArchivist, organizationId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.archivistName").value(archivist.username))
|
||||
.andExpect(jsonPath("$.data.attachments.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.records.length()").isNotEmpty());
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), superAdmin, organizationId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value((int) instanceId));
|
||||
|
||||
// 普通参与人之外的用户仍被拒绝
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), outsider, organizationId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权查看该单据")));
|
||||
// 参与人(报送人)可见
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, organizationId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private long createAndSubmit(String name) throws Exception {
|
||||
String body = "{\"name\":" + quote(name)
|
||||
+ ",\"implementOrg\":\"广西体育中心实施部\",\"fee\":123.45,"
|
||||
+ "\"serviceContent\":\"完成场馆设施维修与验收\"}";
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/completion"),
|
||||
reporter, organizationId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
long instanceId = objectMapper.readTree(response).path("data").asLong();
|
||||
data.trackInstance(instanceId);
|
||||
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private void approveByLeader(long instanceId) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, leader.userId) + "/approve"),
|
||||
leader, organizationId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意办理\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private void confirmReporterTask(long instanceId) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, reporter.userId) + "/confirm"),
|
||||
reporter, organizationId, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"));
|
||||
}
|
||||
|
||||
private long insertConfirmTask(long instanceId, ApprovalTestData.TestUser assignee) {
|
||||
Integer round = data.queryMaster("SELECT round FROM approval_instance WHERE id = ?", Integer.class, instanceId);
|
||||
data.master().update("INSERT INTO approval_task (tenant_id, instance_id, node_key, node_type, round, "
|
||||
+ "task_type, assignee_id, assignee_name, status) VALUES (?, ?, 'confirm', 'CONFIRM', ?, 'CONFIRM', ?, ?, 'PENDING')",
|
||||
TENANT, instanceId, round, assignee.userId, assignee.realName);
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? AND assignee_id = ? "
|
||||
+ "AND status = 'PENDING'", Long.class, instanceId, assignee.userId);
|
||||
assertThat(taskId).as("并存档任务应存在").isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private void uploadAttachment(long instanceId) throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sign.png", "image/png",
|
||||
new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A});
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload")
|
||||
.file(file)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.param("category", "SIGNED")
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username,
|
||||
organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private long pendingTaskId(long instanceId, int userId) {
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? AND status = 'PENDING' LIMIT 1", Long.class, instanceId, userId);
|
||||
assertThat(taskId).as("待办任务应存在").isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder,
|
||||
ApprovalTestData.TestUser user, Integer org,
|
||||
String... authorities) {
|
||||
return builder.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, org, TENANT, authorities)));
|
||||
}
|
||||
|
||||
private String quote(String value) {
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user