merge: t22 PC 待办改派接口与集成测试
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package com.gxwebsoft.approval.controller;
|
package com.gxwebsoft.approval.controller;
|
||||||
|
|
||||||
import com.gxwebsoft.approval.param.ApprovalTaskActionParam;
|
import com.gxwebsoft.approval.param.ApprovalTaskActionParam;
|
||||||
|
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
|
||||||
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
|
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
|
||||||
import com.gxwebsoft.approval.service.ApprovalInstanceService;
|
import com.gxwebsoft.approval.service.ApprovalInstanceService;
|
||||||
import com.gxwebsoft.approval.service.ApprovalTaskService;
|
import com.gxwebsoft.approval.service.ApprovalTaskService;
|
||||||
|
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
|
||||||
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
|
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
|
||||||
import com.gxwebsoft.approval.vo.ApprovalItemVO;
|
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.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
@@ -85,4 +88,13 @@ public class ApprovalTaskController extends BaseController {
|
|||||||
ApprovalTaskService.Result result = taskService.withdraw(id, user);
|
ApprovalTaskService.Result result = taskService.withdraw(id, user);
|
||||||
return success("撤回成功", instanceService.detail(result.getInstanceId(), 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,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;
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ import com.gxwebsoft.approval.entity.ApprovalInstance;
|
|||||||
import com.gxwebsoft.approval.entity.ApprovalTask;
|
import com.gxwebsoft.approval.entity.ApprovalTask;
|
||||||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||||||
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
|
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
|
||||||
|
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
|
||||||
import com.gxwebsoft.approval.support.ApprovalApprover;
|
import com.gxwebsoft.approval.support.ApprovalApprover;
|
||||||
import com.gxwebsoft.approval.support.ApprovalNode;
|
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||||
|
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
|
||||||
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
import com.gxwebsoft.approval.vo.ApprovalUserVO;
|
||||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
@@ -109,6 +111,28 @@ public class ApprovalTaskService {
|
|||||||
return new Result(instanceId, null);
|
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) {
|
public Integer todoCount(User user) {
|
||||||
return taskMapper.countPendingInstances(user.getUserId());
|
return taskMapper.countPendingInstances(user.getUserId());
|
||||||
}
|
}
|
||||||
@@ -171,6 +195,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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
|
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
|
||||||
*/
|
*/
|
||||||
@@ -310,6 +386,14 @@ public class ApprovalTaskService {
|
|||||||
return normalized;
|
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) {
|
private <T> T inTransaction(Supplier<T> action) {
|
||||||
return new TransactionTemplate(transactionManager).execute(status -> action.get());
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
package com.gxwebsoft.approval;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PC 待办审批与改派的端到端行为。
|
||||||
|
*
|
||||||
|
* <p>覆盖:改派成功与待办归属、目标用户校验、非待办任务、原因必填、改派权限 403、
|
||||||
|
* ASSIGN 处理记录、操作日志以及改派候选用户查询。</p>
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||||
|
class ApprovalPcTodoIT {
|
||||||
|
|
||||||
|
private static final int TENANT = TestAuthentication.TENANT_ID;
|
||||||
|
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 ASSIGN = "approval:instance:assign";
|
||||||
|
|
||||||
|
@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 admin;
|
||||||
|
private Integer organizationId;
|
||||||
|
private final List<String> assignUrls = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
data = new ApprovalTestData(dataSource);
|
||||||
|
organizationId = data.createOrganization("PC待办部门" + data.uniqueSuffix(), 136);
|
||||||
|
reporter = data.createUser("PC报送人" + data.uniqueSuffix(), organizationId);
|
||||||
|
ApprovalTestData.TestRole leaderRole =
|
||||||
|
data.createRole("dept_leader_" + organizationId, "部门负责人");
|
||||||
|
leader = data.createUser("PC负责人" + data.uniqueSuffix(), organizationId);
|
||||||
|
data.bind(leader.userId, leaderRole.roleId);
|
||||||
|
ApprovalTestData.TestRole archivistRole = data.createRole("approval_archivist", "收文员");
|
||||||
|
ApprovalTestData.TestUser archivist = data.createUser("PC收文员" + data.uniqueSuffix(), organizationId);
|
||||||
|
data.bind(archivist.userId, archivistRole.roleId);
|
||||||
|
admin = data.createUser("PC管理员" + data.uniqueSuffix(), organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
for (String url : assignUrls) {
|
||||||
|
data.server().update("DELETE FROM sys_operation_record WHERE user_id = ? AND url = ?",
|
||||||
|
admin.userId, url);
|
||||||
|
}
|
||||||
|
assignUrls.clear();
|
||||||
|
data.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void assignsPendingTaskAndMovesTodoToTargetUser() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("改派单据");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
ApprovalTestData.TestUser target = data.createUser("改派目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
String reason = "原处理人休假" + data.uniqueSuffix();
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, target.userId, reason))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.instanceId").value((int) instanceId))
|
||||||
|
.andExpect(jsonPath("$.data.taskId").value((int) taskId))
|
||||||
|
.andExpect(jsonPath("$.data.originalAssigneeId").value(leader.userId))
|
||||||
|
.andExpect(jsonPath("$.data.assigneeId").value(target.userId))
|
||||||
|
.andExpect(jsonPath("$.data.assigneeName").value(target.realName))
|
||||||
|
.andExpect(jsonPath("$.data.reason").value(reason));
|
||||||
|
|
||||||
|
// 任务仍为待处理,归属已换人
|
||||||
|
assertThat(data.queryMaster("SELECT status FROM approval_task WHERE id = ?", String.class, taskId))
|
||||||
|
.isEqualTo("PENDING");
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?", Integer.class, taskId))
|
||||||
|
.isEqualTo(target.userId);
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_name FROM approval_task WHERE id = ?", String.class, taskId))
|
||||||
|
.isEqualTo(target.realName);
|
||||||
|
|
||||||
|
// 处理记录留痕:改派人与原因
|
||||||
|
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||||
|
+ "AND action = 'ASSIGN' AND operator_id = ? AND opinion = ?", instanceId, admin.userId, reason))
|
||||||
|
.isEqualTo(1);
|
||||||
|
|
||||||
|
// 原处理人待办消失、新处理人待办出现
|
||||||
|
mockMvc.perform(authed(get("/api/approval/todo/page"), leader, organizationId, LIST))
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.count").value(0));
|
||||||
|
mockMvc.perform(authed(get("/api/approval/todo/page"), target, organizationId, LIST))
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.count").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId))
|
||||||
|
.andExpect(jsonPath("$.data.list[0].currentNodeName").value("报送部门负责人"));
|
||||||
|
|
||||||
|
// 新处理人可正常处理改派后的任务
|
||||||
|
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/approve"), target, organizationId, APPROVE)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\"接办\"}"))
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.currentNodeName").value("存档"));
|
||||||
|
|
||||||
|
// 原处理人已无权处理该任务
|
||||||
|
long second = createAndSubmit("改派后原处理人");
|
||||||
|
long secondTask = pendingTaskId(second, leader.userId);
|
||||||
|
mockMvc.perform(assign(secondTask, admin, ASSIGN, target.userId, "转办" + data.uniqueSuffix()))
|
||||||
|
.andExpect(jsonPath("$.code").value(0));
|
||||||
|
mockMvc.perform(authed(post("/api/approval/task/" + secondTask + "/approve"), leader, organizationId, APPROVE)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content("{}"))
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权处理该任务")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAssignToUnknownOrInactiveUser() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("改派无效目标");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
|
||||||
|
ApprovalTestData.TestUser inactive = data.createUser("停用目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
data.server().update("UPDATE sys_user SET status = 1 WHERE user_id = ?", inactive.userId);
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, inactive.userId, "停用目标改派"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("改派对象不存在或已停用")));
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, 99999999, "不存在用户改派"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("改派对象不存在或已停用")));
|
||||||
|
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?", Integer.class, taskId))
|
||||||
|
.isEqualTo(leader.userId);
|
||||||
|
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'ASSIGN'",
|
||||||
|
instanceId)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiresAssignReason() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("改派无原因");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
ApprovalTestData.TestUser target = data.createUser("改派目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, target.userId, " "))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("请填写改派原因")));
|
||||||
|
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?", Integer.class, taskId))
|
||||||
|
.isEqualTo(leader.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAssignForHandledTask() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("改派已处理任务");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
ApprovalTestData.TestUser target = data.createUser("改派目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
|
||||||
|
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/approve"), leader, organizationId, APPROVE)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content("{}"))
|
||||||
|
.andExpect(jsonPath("$.code").value(0));
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, target.userId, "已处理后改派"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("任务已处理")));
|
||||||
|
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?", Integer.class, taskId))
|
||||||
|
.isEqualTo(leader.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAssignWithoutAuthority() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("无权限改派");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
ApprovalTestData.TestUser target = data.createUser("改派目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, APPROVE, target.userId, "越权改派"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(403));
|
||||||
|
|
||||||
|
assertThat(data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?", Integer.class, taskId))
|
||||||
|
.isEqualTo(leader.userId);
|
||||||
|
|
||||||
|
mockMvc.perform(authed(get("/api/approval/directory/users").param("keywords", "PC"),
|
||||||
|
admin, organizationId, APPROVE))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void writesOperationLogWithOriginalAndNewAssignee() throws Exception {
|
||||||
|
long instanceId = createAndSubmit("改派操作日志");
|
||||||
|
long taskId = pendingTaskId(instanceId, leader.userId);
|
||||||
|
ApprovalTestData.TestUser target = data.createUser("改派目标" + data.uniqueSuffix(), organizationId);
|
||||||
|
String reason = "操作日志原因" + data.uniqueSuffix();
|
||||||
|
String url = "/api/approval/task/" + taskId + "/assign";
|
||||||
|
assignUrls.add(url);
|
||||||
|
|
||||||
|
mockMvc.perform(assign(taskId, admin, ASSIGN, target.userId, reason))
|
||||||
|
.andExpect(jsonPath("$.code").value(0));
|
||||||
|
|
||||||
|
JsonNode record = awaitOperationRecord(url, reason);
|
||||||
|
assertThat(record).as("改派操作日志应写入 sys_operation_record").isNotNull();
|
||||||
|
assertThat(record.path("userId").asInt()).isEqualTo(admin.userId);
|
||||||
|
assertThat(record.path("description").asText()).contains("改派");
|
||||||
|
assertThat(record.path("params").asText()).contains(reason);
|
||||||
|
assertThat(record.path("result").asText())
|
||||||
|
.contains(leader.realName)
|
||||||
|
.contains(target.realName)
|
||||||
|
.contains(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsOnlyActiveUsersForAssignCandidates() throws Exception {
|
||||||
|
ApprovalTestData.TestUser active = data.createUser("候选人" + data.uniqueSuffix(), organizationId);
|
||||||
|
ApprovalTestData.TestUser inactive = data.createUser("停用候选人" + data.uniqueSuffix(), organizationId);
|
||||||
|
data.server().update("UPDATE sys_user SET status = 1 WHERE user_id = ?", inactive.userId);
|
||||||
|
|
||||||
|
String body = mockMvc.perform(authed(get("/api/approval/directory/users")
|
||||||
|
.param("keywords", active.realName), admin, organizationId, ASSIGN))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
JsonNode users = objectMapper.readTree(body).path("data");
|
||||||
|
assertThat(users.isArray()).isTrue();
|
||||||
|
assertThat(users.findValuesAsText("userId")).contains(String.valueOf(active.userId));
|
||||||
|
assertThat(users.findValuesAsText("userId")).doesNotContain(String.valueOf(inactive.userId));
|
||||||
|
JsonNode matched = null;
|
||||||
|
for (JsonNode user : users) {
|
||||||
|
if (user.path("userId").asInt() == active.userId) {
|
||||||
|
matched = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(matched).isNotNull();
|
||||||
|
assertThat(matched.path("realName").asText()).isEqualTo(active.realName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
|
||||||
|
private MockHttpServletRequestBuilder assign(long taskId, ApprovalTestData.TestUser operator,
|
||||||
|
String authority, int targetUserId, String reason)
|
||||||
|
throws Exception {
|
||||||
|
return authed(post("/api/approval/task/" + taskId + "/assign"), operator, organizationId, authority)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"targetUserId\":" + targetUserId + ",\"reason\":" + quote(reason) + "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 操作日志异步写入,最多等待 2 秒。 */
|
||||||
|
private JsonNode awaitOperationRecord(String url, String paramsMark) throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis() + 2000;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
List<java.util.Map<String, Object>> rows = data.server().queryForList(
|
||||||
|
"SELECT user_id AS userId, description, params, result FROM sys_operation_record "
|
||||||
|
+ "WHERE url = ? AND params LIKE ? ORDER BY id DESC LIMIT 1",
|
||||||
|
url, "%" + paramsMark + "%");
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
return objectMapper.valueToTree(rows.get(0));
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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