feat: 车辆免收费登记表(逐车归属 + 车牌区间冲突)#54
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
package com.gxwebsoft.approval;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import com.gxwebsoft.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
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.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 车辆免收费登记表:一单多车、车辆归属逐车、车牌有效期区间冲突、节点链与明细导出。
|
||||
*
|
||||
* <p>不开启测试事务:提交与审批动作各自提交,便于验证跨库解析与节点推进;
|
||||
* 引擎表与车辆业务表由 {@link ApprovalTestData#cleanup()} 按实例清理。</p>
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalVehicleIT {
|
||||
|
||||
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 ARCHIVE = "approval:instance:archive";
|
||||
private static final String EXPORT = "approval:instance:export";
|
||||
private static final String FLOW = "vehicle";
|
||||
|
||||
/** 车辆归属编码(ADR 0010)。 */
|
||||
private static final String OWNER_GROUP = "GROUP_EMPLOYEE";
|
||||
private static final String OWNER_CLUB = "COOPERATING_CLUB_EMPLOYEE";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
@Autowired
|
||||
private ConfigProperties config;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private Integer reportOrgId;
|
||||
private ApprovalTestData.TestUser reportLeader;
|
||||
private ApprovalTestData.TestUser opsLeader;
|
||||
private ApprovalTestData.TestUser deputy;
|
||||
private ApprovalTestData.TestUser typist;
|
||||
private ApprovalTestData.TestUser archivist;
|
||||
|
||||
private final List<Path> exportedFiles = new ArrayList<>();
|
||||
private final List<Integer> userIds = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
String suffix = data.uniqueSuffix();
|
||||
reportOrgId = data.createOrganization("车辆登记部门" + suffix, 136);
|
||||
|
||||
reporter = createUser("车辆报送人" + suffix, reportOrgId);
|
||||
reportLeader = bindRole(data.leaderRoleCode(reportOrgId), "部门负责人",
|
||||
"车辆部门负责人" + suffix, reportOrgId);
|
||||
opsLeader = bindRole("dept_leader_APPROVAL_OPS", "运营管理部负责人",
|
||||
"车辆运营负责人" + suffix, reportOrgId);
|
||||
deputy = bindRole("dept_deputy_" + data.organizationCode(reportOrgId), "分管领导",
|
||||
"车辆分管领导" + suffix, reportOrgId);
|
||||
typist = bindRole(ApprovalConstants.ROLE_TYPIST, "录入员",
|
||||
"车辆录入员" + suffix, reportOrgId);
|
||||
archivist = bindRole(ApprovalConstants.ROLE_ARCHIVIST, "收文员",
|
||||
"车辆收文员" + suffix, reportOrgId);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
for (Path path : exportedFiles) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (Exception ignored) {
|
||||
// 文件清理失败不影响断言
|
||||
}
|
||||
}
|
||||
exportedFiles.clear();
|
||||
try {
|
||||
// 导出操作日志异步写入
|
||||
Thread.sleep(300);
|
||||
for (Integer userId : userIds) {
|
||||
data.server().update(
|
||||
"DELETE FROM sys_operation_record WHERE user_id = ? AND url LIKE '%/api/approval/export%'", userId);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 共享库清理尽力而为
|
||||
}
|
||||
userIds.clear();
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsEndToEndThroughTypistConfirmAndArchive() throws Exception {
|
||||
long instanceId = saveDraft(List.of(
|
||||
row("广西威宁集团", "张三 工号A001", "桂A12345", true, "13800000000",
|
||||
"2026-01-01", "2026-06-30", OWNER_GROUP),
|
||||
row("南宁大型活动主办方", "李四 身份证尾号1234", "桂AD67890", false, "13800000001",
|
||||
"2026-03-01", "2026-12-31", OWNER_CLUB)), null);
|
||||
|
||||
// 草稿:整单无业务字段,一车一行,车辆归属逐车
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
assertThat(data.queryMaster("SELECT flow_code FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("vehicle");
|
||||
assertThat(data.queryMaster("SELECT doc_type FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("vehicle");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_vehicle_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(2);
|
||||
assertThat(data.queryMaster("SELECT plate_no FROM approval_vehicle_item WHERE instance_id = ? AND line_no = 1",
|
||||
String.class, instanceId)).isEqualTo("桂A12345");
|
||||
assertThat(data.queryMaster("SELECT vehicle_owner_type FROM approval_vehicle_item "
|
||||
+ "WHERE instance_id = ? AND line_no = 2", String.class, instanceId)).isEqualTo(OWNER_CLUB);
|
||||
assertThat(data.queryMaster("SELECT new_energy FROM approval_vehicle_item "
|
||||
+ "WHERE instance_id = ? AND line_no = 1", Boolean.class, instanceId)).isTrue();
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.docTypeName").value("车辆免收费登记表"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("未提交"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns.length()").value(9))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[0].key").value("unitName"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[2].key").value("plateNo"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[8].key").value("vehicleOwnerType"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[8].type").value("ENUM"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[8].options.length()").value(8))
|
||||
.andExpect(jsonPath("$.data.document.lines.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].lineNo").value(1))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].plateNo").value("桂A12345"))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].newEnergy").value(true))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].vehicleOwnerType").value(OWNER_GROUP))
|
||||
.andExpect(jsonPath("$.data.document.lines[1].plateNo").value("桂AD67890"))
|
||||
.andExpect(jsonPath("$.data.document.lines[1].vehicleOwnerType").value(OWNER_CLUB))
|
||||
.andExpect(jsonPath("$.data.document.lineSummary.lineCount").value(2))
|
||||
.andExpect(jsonPath("$.data.document.lineSummary.amountTotal").doesNotExist())
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(true));
|
||||
|
||||
// 列表标题与检索文本含全部车牌(列表按车牌核实用)
|
||||
assertThat(data.queryMaster("SELECT title FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("车辆免收费登记(2 辆)");
|
||||
String searchText = data.queryMaster("SELECT search_text FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
assertThat(searchText).contains("桂A12345").contains("桂AD67890");
|
||||
|
||||
// 提交预检:至少一张附件(不要求分类)
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value("请至少上传一张附件"));
|
||||
uploadAttachment(instanceId, ApprovalConstants.CATEGORY_OTHER);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.bizNo").value(org.hamcrest.Matchers.matchesPattern("CL\\d{8}-\\d{4}")))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("报送部门负责人"));
|
||||
|
||||
String bizNo = bizNo(instanceId);
|
||||
assertThat(pendingTaskIds(instanceId)).contains(reportLeader.userId);
|
||||
// 节点链固定:报送部门负责人 → 运营管理部负责人(APPROVAL_OPS)→ 分管领导 → 录入员存档 → 归档
|
||||
String nodes = data.queryMaster("SELECT nodes_json FROM approval_flow_definition "
|
||||
+ "WHERE tenant_id = ? AND flow_code = 'vehicle' ORDER BY version DESC LIMIT 1", String.class, TENANT);
|
||||
assertThat(nodes).contains("\"resolver\": \"reporterOrgLeader\"")
|
||||
.contains("\"resolver\": \"orgLeaderByCode\"").contains("APPROVAL_OPS")
|
||||
.contains("\"resolver\": \"reporterOrgDeputy\"")
|
||||
.contains("\"resolver\": \"roleTypist\"").contains("\"resolver\": \"archivist\"");
|
||||
|
||||
// 报送部门负责人
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, reportLeader.userId)
|
||||
+ "/approve"), reportLeader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\"同意登记\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("运营管理部负责人"));
|
||||
|
||||
// 运营管理部负责人:按固定组织编码 APPROVAL_OPS 解析
|
||||
long opsTask = pendingTaskId(instanceId, opsLeader.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + opsTask + "/approve"), opsLeader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\"核验通过\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("分管领导"));
|
||||
|
||||
// 分管领导
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, deputy.userId)
|
||||
+ "/approve"), deputy, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\"同意\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("录入员存档"));
|
||||
|
||||
// 录入员:CONFIRM 存档确认,不派单;本单录入员按 approval_typist 角色成员解析
|
||||
long typistTask = pendingTaskId(instanceId, typist.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + typistTask + "/confirm"), typist, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
// 共享测试库里该角色可能还有其它成员:同节点同轮全部确认后才推进
|
||||
confirmAllPendingInNode(instanceId, "confirm");
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("APPROVED");
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("archive");
|
||||
|
||||
// 归档:归档节点或签,任务可能分配给角色内任一成员,按实际指派人操作
|
||||
long archiveTask = pendingTaskIdInNode(instanceId, "archive");
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/archive"),
|
||||
assigneeUser(archiveTask), ARCHIVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("已归档"));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("ARCHIVED");
|
||||
assertThat(bizNo(instanceId)).isEqualTo(bizNo);
|
||||
// 录入员角色在共享测试库可能有多名成员,确认记录数按实际确认人数至少 1 条
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'CONFIRM'", instanceId)).isGreaterThanOrEqualTo(1);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'ARCHIVE'", instanceId)).isEqualTo(1);
|
||||
// 明细不随审批推进丢失
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_vehicle_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void blocksOverlappingPlateAndAllowsAfterWithdraw() throws Exception {
|
||||
long first = saveDraft(List.of(row("合作俱乐部", "王五", "桂A55555", false, "13800000002",
|
||||
"2026-01-01", "2026-06-30", OWNER_CLUB)), null);
|
||||
uploadAttachment(first, ApprovalConstants.CATEGORY_OTHER);
|
||||
submit(first);
|
||||
String firstBizNo = bizNo(first);
|
||||
|
||||
// 第二单重叠区间:提交被拦,消息带出冲突单号与已登记有效期
|
||||
long overlapping = saveDraft(List.of(row("主办方", "赵六", "桂A55555", false, "13800000003",
|
||||
"2026-03-01", "2026-09-01", OWNER_CLUB)), null);
|
||||
uploadAttachment(overlapping, ApprovalConstants.CATEGORY_OTHER);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + overlapping + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("桂A55555")))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(firstBizNo)))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("2026-01-01")))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("2026-06-30")));
|
||||
assertThat(instanceStatus(overlapping)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(overlapping)).isNull();
|
||||
|
||||
// 第一单撤回后成为作废态,第二单可以提交(且不会撞上自己)
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + first + "/withdraw"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("WITHDRAWN"));
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + overlapping + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
||||
assertThat(bizNo(overlapping)).isNotBlank();
|
||||
|
||||
// 未重叠区间不受影响(第二单已占 2026-03-01 至 2026-09-01)
|
||||
long adjacent = saveDraft(List.of(row("合作俱乐部", "王五", "桂A55555", false, "13800000002",
|
||||
"2026-10-01", "2026-12-31", OWNER_CLUB)), null);
|
||||
uploadAttachment(adjacent, ApprovalConstants.CATEGORY_OTHER);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + adjacent + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsVehicleOwnerTypePerLineAndNormalizesPlate() throws Exception {
|
||||
long instanceId = saveDraft(List.of(
|
||||
row("威宁集团", "员工甲", " 桂a12345 ", true, "13800000004", "2026-01-01", "2026-12-31",
|
||||
OWNER_GROUP),
|
||||
row("大型活动主办方", "主办方乙", "桂A66666", false, "13800000005", "2026-02-01", "2026-05-31",
|
||||
"LARGE_EVENT_ORGANIZER")), null);
|
||||
|
||||
// 车牌去空格转大写后入库
|
||||
assertThat(plateNos(instanceId)).containsExactly("桂A12345", "桂A66666");
|
||||
// 同一单里两种归属并存,逐车存储编码而非整单枚举
|
||||
assertThat(ownerTypes(instanceId)).containsExactly(OWNER_GROUP, "LARGE_EVENT_ORGANIZER");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_vehicle_form WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesPlateFormatAndValidDateRange() throws Exception {
|
||||
// 有效期止早于起
|
||||
expectSaveFailure(List.of(row("单位", "员工", "桂A77777", false, "13800000006",
|
||||
"2026-06-30", "2026-01-01", OWNER_GROUP)), "第1行有效期止不能早于有效期起");
|
||||
// 车牌格式非法时消息带出该车牌
|
||||
expectSaveFailure(List.of(row("单位", "员工", "桂A123", false, "13800000006",
|
||||
"2026-01-01", "2026-06-30", OWNER_GROUP)), "车牌号「桂A123」格式不正确");
|
||||
expectSaveFailure(List.of(row("单位", "员工", "A12345", false, "13800000006",
|
||||
"2026-01-01", "2026-06-30", OWNER_GROUP)), "车牌号「A12345」格式不正确");
|
||||
// 必填列缺失与长度上限(共享明细校验器)
|
||||
expectSaveFailure(List.of(row("单位", "员工", "桂A88888", false, "13800000006",
|
||||
"2026-01-01", "2026-06-30", "")), "第1行车辆归属不能为空");
|
||||
expectSaveFailure(List.of(row("单位", "员工", "桂A88888", false, "13800000006",
|
||||
"2026-01-01", "2026-06-30", "NOT_A_TYPE")), "第1行车辆归属取值不正确");
|
||||
expectSaveFailure(List.of(row("单".repeat(101), "员工", "桂A88888", false, "13800000006",
|
||||
"2026-01-01", "2026-06-30", OWNER_GROUP)), "第1行单位名称不能超过100字");
|
||||
// 明细至少一行
|
||||
expectSaveFailure(List.of(), "请至少填写一行明细");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_vehicle_form")).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsByPlateKeywordsInPcListAndMineList() throws Exception {
|
||||
String uniquePlate = "桂A99999";
|
||||
long instanceId = saveDraft(List.of(row("检索单位", "员工丙", uniquePlate, false, "13800000007",
|
||||
"2026-01-01", "2026-12-31", OWNER_GROUP)), null);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/page").param("keywords", uniquePlate), reporter, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.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].flowCode").value("vehicle"))
|
||||
.andExpect(jsonPath("$.data.list[0].title").value("车辆免收费登记(1 辆)"));
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page").param("keywords", uniquePlate), reporter, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId));
|
||||
|
||||
// 别的车牌不命中本单
|
||||
mockMvc.perform(authed(get("/api/approval/instance/page").param("keywords", "桂A00000"), reporter, LIST))
|
||||
.andExpect(jsonPath("$.data.count").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportsLineCountInSummaryAndVehicleLines() throws Exception {
|
||||
String mark = "车辆导出" + data.uniqueSuffix();
|
||||
long instanceId = saveDraft(List.of(
|
||||
row(mark, "员工丁", "桂A10001", true, "13800000008", "2026-01-01", "2026-06-30", OWNER_GROUP),
|
||||
row(mark, "员工戊", "桂A10002", false, "13800000009", "2026-02-01", "2026-08-31", OWNER_CLUB)), null);
|
||||
String reportOrgName = data.server().queryForObject(
|
||||
"SELECT organization_name FROM sys_organization WHERE organization_id = ?", String.class, reportOrgId);
|
||||
|
||||
// 跨单据总表:明细行数 = 2,合计金额留空(无金额列)
|
||||
String response = mockMvc.perform(authed(post("/api/approval/export"), reporter, LIST, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"keywords\":" + quote("桂A10002") + "}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
File summary = exportFile(response);
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(summary))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertThat(cellText(sheet.getRow(0).getCell(16))).isEqualTo("明细行数");
|
||||
Row row = sheet.getRow(1);
|
||||
assertThat(cellText(row.getCell(1))).isEqualTo("车辆免收费登记表");
|
||||
assertThat(cellText(row.getCell(16))).isEqualTo("2");
|
||||
assertThat(cellText(row.getCell(17))).isEmpty();
|
||||
}
|
||||
|
||||
// 按类型明细导出:逐行车牌
|
||||
String lineResponse = mockMvc.perform(authed(post("/api/approval/export/" + FLOW + "/lines"),
|
||||
reporter, LIST, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"flowCode\":\"vehicle\",\"keywords\":" + quote("桂A10001") + "}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("共2条明细")))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
File lines = exportFile(lineResponse);
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(lines))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertThat(sheet.getSheetName()).isEqualTo("车辆免收费登记表");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(0))).isEqualTo("单号");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(3))).isEqualTo("报送人");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(4))).isEqualTo("单位名称");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(6))).isEqualTo("车牌号");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(12))).isEqualTo("车辆归属");
|
||||
|
||||
Row first = sheet.getRow(1);
|
||||
assertThat(cellText(first.getCell(1))).isEqualTo("未提交");
|
||||
assertThat(cellText(first.getCell(2))).isEqualTo(reportOrgName);
|
||||
assertThat(cellText(first.getCell(3))).isEqualTo(reporter.username);
|
||||
assertThat(cellText(first.getCell(4))).isEqualTo(mark);
|
||||
assertThat(cellText(first.getCell(6))).isEqualTo("桂A10001");
|
||||
assertThat(cellText(first.getCell(7))).isEqualTo("是");
|
||||
assertThat(cellText(first.getCell(9))).isEqualTo("2026-01-01");
|
||||
assertThat(cellText(first.getCell(10))).isEqualTo("2026-06-30");
|
||||
assertThat(cellText(first.getCell(12))).isEqualTo(OWNER_GROUP);
|
||||
|
||||
Row second = sheet.getRow(2);
|
||||
assertThat(cellText(second.getCell(6))).isEqualTo("桂A10002");
|
||||
assertThat(cellText(second.getCell(12))).isEqualTo(OWNER_CLUB);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private Map<String, Object> row(String unitName, String personInfo, String plateNo, boolean newEnergy,
|
||||
String contactPhone, String validStartDate, String validEndDate,
|
||||
String vehicleOwnerType) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("unitName", unitName);
|
||||
row.put("personInfo", personInfo);
|
||||
row.put("plateNo", plateNo);
|
||||
row.put("newEnergy", newEnergy);
|
||||
row.put("contactPhone", contactPhone);
|
||||
row.put("validStartDate", validStartDate);
|
||||
row.put("validEndDate", validEndDate);
|
||||
row.put("remark", "测试备注");
|
||||
row.put("vehicleOwnerType", vehicleOwnerType);
|
||||
return row;
|
||||
}
|
||||
|
||||
private long saveDraft(List<Map<String, Object>> lines, Long id) throws Exception {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
if (id != null) {
|
||||
body.put("id", id);
|
||||
}
|
||||
body.put("lines", lines);
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/" + FLOW), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(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);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private void expectSaveFailure(List<Map<String, Object>> lines, String expectedMessage) throws Exception {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("lines", lines);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + FLOW), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(expectedMessage)));
|
||||
}
|
||||
|
||||
private void submit(long instanceId) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private ApprovalTestData.TestUser bindRole(String roleCode, String roleName, String realName,
|
||||
Integer organizationId) {
|
||||
ApprovalTestData.TestUser member = createUser(realName, organizationId);
|
||||
data.bind(member.userId, ensureRole(roleCode, roleName));
|
||||
return member;
|
||||
}
|
||||
|
||||
/** 共享库可能已有同名角色(如运营管理部负责人/录入员),存在就复用,不存在才新建。 */
|
||||
private Integer ensureRole(String roleCode, String roleName) {
|
||||
List<Integer> ids = data.server().queryForList(
|
||||
"SELECT role_id FROM sys_role WHERE role_code = ? AND tenant_id = ? AND deleted = 0 "
|
||||
+ "ORDER BY role_id LIMIT 1", Integer.class, roleCode, TENANT);
|
||||
if (!ids.isEmpty()) {
|
||||
return ids.get(0);
|
||||
}
|
||||
return data.createRole(roleCode, roleName, TENANT).roleId;
|
||||
}
|
||||
|
||||
private ApprovalTestData.TestUser createUser(String realName, Integer organizationId) {
|
||||
ApprovalTestData.TestUser user = data.createUser(realName, organizationId, TENANT);
|
||||
userIds.add(user.userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void uploadAttachment(long instanceId, String category) 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", category)
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, reportOrgId,
|
||||
TENANT, SUBMIT))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
/** 同节点同轮的全部待办逐个确认(共享库里该角色可能有其它成员)。 */
|
||||
private void confirmAllPendingInNode(long instanceId, String nodeKey) throws Exception {
|
||||
for (int guard = 0; guard < 20; guard++) {
|
||||
List<Map<String, Object>> tasks = data.master().queryForList(
|
||||
"SELECT id, assignee_id, assignee_name FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = ? AND status = 'PENDING' ORDER BY id", instanceId, nodeKey);
|
||||
if (tasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Map<String, Object> task : tasks) {
|
||||
long taskId = ((Number) task.get("id")).longValue();
|
||||
String name = (String) task.get("assignee_name");
|
||||
ApprovalTestData.TestUser assignee =
|
||||
new ApprovalTestData.TestUser(((Number) task.get("assignee_id")).intValue(), name, name);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/confirm"), assignee, APPROVE))
|
||||
.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 long pendingTaskIdInNode(long instanceId, String nodeKey) {
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = ? AND status = 'PENDING' LIMIT 1", Long.class, instanceId, nodeKey);
|
||||
assertThat(taskId).as(nodeKey + "待办任务应存在").isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/** 任务实际指派人:归档/录入员这类岗位角色在共享库可能有多名成员。 */
|
||||
private ApprovalTestData.TestUser assigneeUser(long taskId) {
|
||||
Map<String, Object> row = data.master().queryForMap(
|
||||
"SELECT assignee_id, assignee_name FROM approval_task WHERE id = ?", taskId);
|
||||
String name = (String) row.get("assignee_name");
|
||||
return new ApprovalTestData.TestUser(((Number) row.get("assignee_id")).intValue(), name, name);
|
||||
}
|
||||
|
||||
private List<Integer> pendingTaskIds(long instanceId) {
|
||||
return data.master().queryForList("SELECT assignee_id FROM approval_task "
|
||||
+ "WHERE instance_id = ? AND status = 'PENDING' ORDER BY id", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private List<String> plateNos(long instanceId) {
|
||||
return data.master().queryForList("SELECT plate_no FROM approval_vehicle_item "
|
||||
+ "WHERE instance_id = ? ORDER BY line_no", String.class, instanceId);
|
||||
}
|
||||
|
||||
private List<String> ownerTypes(long instanceId) {
|
||||
return data.master().queryForList("SELECT vehicle_owner_type FROM approval_vehicle_item "
|
||||
+ "WHERE instance_id = ? ORDER BY line_no", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String instanceStatus(long instanceId) {
|
||||
return data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String bizNo(long instanceId) {
|
||||
return data.queryMaster("SELECT biz_no FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String currentNodeKey(long instanceId) {
|
||||
return data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
}
|
||||
|
||||
private File exportFile(String response) throws Exception {
|
||||
JsonNode node = objectMapper.readTree(response).path("data");
|
||||
File file = new File(node.path("path").asText());
|
||||
assertThat(file).exists();
|
||||
exportedFiles.add(file.toPath());
|
||||
return file;
|
||||
}
|
||||
|
||||
private String cellText(Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
}
|
||||
if (cell.getCellType() == CellType.NUMERIC) {
|
||||
return new java.math.BigDecimal(String.valueOf(cell.getNumericCellValue())).stripTrailingZeros()
|
||||
.toPlainString();
|
||||
}
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder,
|
||||
ApprovalTestData.TestUser user, String... authorities) {
|
||||
return builder.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, reportOrgId,
|
||||
TENANT, authorities)));
|
||||
}
|
||||
|
||||
private String quote(String value) {
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user