feat: 体验券领用申请单(每类券一行 + 相关部门与发放人)#56
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
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.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
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.time.LocalDate;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 体验券领用申请单集成测试(ADR 0013):多券种草稿、每类券唯一、六审批节点
|
||||
* (部门负责人 → 相关部门 → 分管领导 → 总经理 → 发放人 → 存档)到归档、
|
||||
* 明细导出与「整单至少一张附件」。
|
||||
*
|
||||
* <p>独立租户:相关角色(approval_related_dept 等)按 tenant_id + role_code 解析,
|
||||
* 用专属租户(10052,测试库中无审批角色成员)避免与其它并行测试类共享角色成员。</p>
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalCouponIT {
|
||||
|
||||
private static final int TENANT = 10052;
|
||||
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 APPLY_REASON = "员工回馈活动领用体验券";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private ApprovalTestData.TestUser reportLeader;
|
||||
private ApprovalTestData.TestUser relatedDept;
|
||||
private ApprovalTestData.TestUser deputy;
|
||||
private ApprovalTestData.TestUser generalManager;
|
||||
private ApprovalTestData.TestUser issuer;
|
||||
private ApprovalTestData.TestUser archivist;
|
||||
private Integer organizationId;
|
||||
|
||||
/** 本次用例生成的文件。 */
|
||||
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();
|
||||
organizationId = data.createOrganization("体验券申请部门" + suffix, 136, TENANT);
|
||||
reporter = createUser("体验券申请人" + suffix, organizationId);
|
||||
|
||||
reportLeader = createUser("体验券部门负责人" + suffix, organizationId);
|
||||
data.bindTenant(reportLeader.userId,
|
||||
data.createRole(data.leaderRoleCode(organizationId), "部门负责人", TENANT).roleId, TENANT);
|
||||
|
||||
deputy = createUser("体验券分管领导" + suffix, organizationId);
|
||||
data.bindTenant(deputy.userId,
|
||||
data.createRole("dept_deputy_" + data.organizationCode(organizationId), "分管领导", TENANT).roleId,
|
||||
TENANT);
|
||||
|
||||
relatedDept = createUser("体验券相关部门" + suffix, organizationId);
|
||||
data.bindTenant(relatedDept.userId,
|
||||
data.createRole(ApprovalConstants.ROLE_RELATED_DEPT, "相关部门", TENANT).roleId, TENANT);
|
||||
|
||||
generalManager = createUser("体验券总经理" + suffix, organizationId);
|
||||
data.bindTenant(generalManager.userId,
|
||||
data.createRole(ApprovalConstants.ROLE_GENERAL_MANAGER, "总经理", TENANT).roleId, TENANT);
|
||||
|
||||
issuer = createUser("体验券发放人" + suffix, organizationId);
|
||||
data.bindTenant(issuer.userId,
|
||||
data.createRole(ApprovalConstants.ROLE_ISSUER, "发放人", TENANT).roleId, TENANT);
|
||||
|
||||
archivist = createUser("体验券收文员" + suffix, organizationId);
|
||||
data.bindTenant(archivist.userId,
|
||||
data.createRole(ApprovalConstants.ROLE_ARCHIVIST, "收文员", TENANT).roleId, TENANT);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// 体验券业务表与明细表已并入 ApprovalTestData.INSTANCE_CHILD_TABLES,随 cleanup 一并清理
|
||||
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 createsDraftWithMultipleCouponTypesAndShowsLines() throws Exception {
|
||||
long instanceId = saveDraft(reporter, APPLY_REASON, List.of(
|
||||
row("TENNIS_CENTER", 10, LocalDate.now().plusMonths(3)),
|
||||
row("SWIMMING_DIVING", 20, LocalDate.now().plusMonths(6)),
|
||||
row("GYM_MONTHLY", 5, LocalDate.now().plusMonths(1))));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
assertThat(data.queryMaster("SELECT flow_code FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("coupon");
|
||||
assertThat(data.queryMaster("SELECT doc_type FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("coupon");
|
||||
assertThat(data.queryMaster("SELECT reporter_name FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo(reporter.username);
|
||||
assertThat(data.queryMaster("SELECT apply_reason FROM approval_coupon_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isEqualTo(APPLY_REASON);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_coupon_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(3);
|
||||
assertThat(data.queryMaster("SELECT coupon_type FROM approval_coupon_item WHERE instance_id = ? AND line_no = 1",
|
||||
String.class, instanceId)).isEqualTo("TENNIS_CENTER");
|
||||
assertThat(data.queryMaster("SELECT quantity FROM approval_coupon_item WHERE instance_id = ? AND line_no = 2",
|
||||
Integer.class, instanceId)).isEqualTo(20);
|
||||
// 标题按申请事由生成,检索文本含券种中文名
|
||||
assertThat(data.queryMaster("SELECT title FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("体验券领用:" + APPLY_REASON);
|
||||
String searchText = data.queryMaster("SELECT search_text FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
assertThat(searchText).contains("网球中心").contains("游泳跳水馆单次").contains("威拓普健身馆月卡");
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.docType").value("coupon"))
|
||||
.andExpect(jsonPath("$.data.docTypeName").value("体验券领用申请单"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("未提交"))
|
||||
.andExpect(jsonPath("$.data.document.fields.length()").value(3))
|
||||
.andExpect(jsonPath("$.data.document.fields[0].key").value("applyReason"))
|
||||
.andExpect(jsonPath("$.data.document.fields[0].label").value("申请事由"))
|
||||
.andExpect(jsonPath("$.data.document.fields[0].value").value(APPLY_REASON))
|
||||
.andExpect(jsonPath("$.data.document.fields[1].key").value("reporterName"))
|
||||
.andExpect(jsonPath("$.data.document.fields[1].value").value(reporter.username))
|
||||
.andExpect(jsonPath("$.data.document.fields[2].key").value("reportOrgName"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns.length()").value(3))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[0].key").value("couponType"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[0].type").value("ENUM"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[0].options.length()").value(7))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[1].key").value("quantity"))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns[2].key").value("validUntil"))
|
||||
.andExpect(jsonPath("$.data.document.lines.length()").value(3))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].lineNo").value(1))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].couponType").value("TENNIS_CENTER"))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].quantity").value(10))
|
||||
.andExpect(jsonPath("$.data.document.lineSummary.lineCount").value(3))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(true));
|
||||
|
||||
// 列表标题即「体验券领用:<申请事由>」,类型名取流程名
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page"), reporter, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId))
|
||||
.andExpect(jsonPath("$.data.list[0].docTypeName").value("体验券领用申请单"))
|
||||
.andExpect(jsonPath("$.data.list[0].title").value("体验券领用:" + APPLY_REASON))
|
||||
.andExpect(jsonPath("$.data.list[0].statusName").value("未提交"));
|
||||
|
||||
// 草稿整表替换:改两行后子表只剩两行且行号重排
|
||||
long updated = saveDraft(reporter, "调整后的领用事由", List.of(
|
||||
row("TRAINING_HALL", 8, LocalDate.now().plusDays(30)),
|
||||
row("FIVE_A_SIDE", 4, LocalDate.now().plusDays(60))), instanceId);
|
||||
assertThat(updated).isEqualTo(instanceId);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_coupon_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(2);
|
||||
assertThat(data.queryMaster("SELECT coupon_type FROM approval_coupon_item WHERE instance_id = ? AND line_no = 1",
|
||||
String.class, instanceId)).isEqualTo("TRAINING_HALL");
|
||||
assertThat(data.queryMaster("SELECT title FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("体验券领用:调整后的领用事由");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateCouponTypeAndNonPositiveQuantity() throws Exception {
|
||||
Map<String, Object> repeat = new LinkedHashMap<>();
|
||||
repeat.put("applyReason", APPLY_REASON);
|
||||
repeat.put("lines", List.of(
|
||||
row("SWIMMING_DIVING", 2, LocalDate.now().plusDays(10)),
|
||||
row("TENNIS_CENTER", 2, LocalDate.now().plusDays(10)),
|
||||
row("SWIMMING_DIVING", 3, LocalDate.now().plusDays(10))));
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coupon"), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(repeat)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.allOf(
|
||||
org.hamcrest.Matchers.containsString("第3行"),
|
||||
org.hamcrest.Matchers.containsString("游泳跳水馆单次"),
|
||||
org.hamcrest.Matchers.containsString("第1行"),
|
||||
org.hamcrest.Matchers.containsString("每类券最多一行"))));
|
||||
|
||||
expectSaveFailure(List.of(row("SWIMMING_DIVING", 0, LocalDate.now().plusDays(10))),
|
||||
"第1行数量必须大于等于1");
|
||||
expectSaveFailure(List.of(row("SWIMMING_DIVING", -3, LocalDate.now().plusDays(10))),
|
||||
"第1行数量必须大于等于1");
|
||||
expectSaveFailure(List.of(row("NOT_A_COUPON", 1, LocalDate.now().plusDays(10))),
|
||||
"第1行券种取值不正确");
|
||||
expectSaveFailure(List.of(Map.of("couponType", "TENNIS_CENTER", "quantity", 1)),
|
||||
"第1行有效期至不能为空");
|
||||
expectSaveFailure(List.of(Map.of("quantity", 1, "validUntil", LocalDate.now().plusDays(10).toString())),
|
||||
"第1行券种不能为空");
|
||||
|
||||
// 申请事由必填且不超过 500 字,明细至少一行
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coupon"), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(Map.of("applyReason", " ", "lines",
|
||||
List.of(row("TENNIS_CENTER", 1, LocalDate.now().plusDays(10)))))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value("申请事由不能为空"));
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coupon"), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(Map.of("applyReason", "券".repeat(501), "lines",
|
||||
List.of(row("TENNIS_CENTER", 1, LocalDate.now().plusDays(10)))))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value("申请事由不能超过500字"));
|
||||
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_coupon_form")).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWithoutAnyAttachmentButDoesNotRequireCategory() throws Exception {
|
||||
long instanceId = saveDraft(reporter, APPLY_REASON,
|
||||
List.of(row("TENNIS_CENTER", 10, LocalDate.now().plusMonths(2))));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value("请至少上传一张附件"));
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
|
||||
// 任意分类一张即可,不要求签字件或凭证
|
||||
uploadAttachment(instanceId, ApprovalConstants.CATEGORY_OTHER);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("申请部门负责人"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsThroughSixApprovalNodesThenArchives() throws Exception {
|
||||
long instanceId = saveDraft(reporter, APPLY_REASON, List.of(
|
||||
row("SWIMMING_DIVING", 30, LocalDate.now().plusMonths(6)),
|
||||
row("TENNIS_CENTER", 20, LocalDate.now().plusMonths(6)),
|
||||
row("GYM_BIWEEKLY", 6, LocalDate.now().plusMonths(3))));
|
||||
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("TY\\d{8}-\\d{4}")))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("申请部门负责人"))
|
||||
.andExpect(jsonPath("$.data.document.lines.length()").value(3));
|
||||
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(reportLeader.userId);
|
||||
assertThat(approve(instanceId, reportLeader, "相关部门")).isEqualTo("relatedDept");
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(relatedDept.userId);
|
||||
assertThat(approve(instanceId, relatedDept, "分管领导")).isEqualTo("deputy");
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(deputy.userId);
|
||||
assertThat(approve(instanceId, deputy, "总经理")).isEqualTo("generalManager");
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(generalManager.userId);
|
||||
assertThat(approve(instanceId, generalManager, "发放人")).isEqualTo("issuer");
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(issuer.userId);
|
||||
// 发放人用「通过」而不是存档确认(ADR 0013)
|
||||
assertThat(approve(instanceId, issuer, "存档")).isEqualTo("confirm");
|
||||
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(reporter.userId);
|
||||
long confirmTask = pendingTaskId(instanceId, reporter.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + confirmTask + "/confirm"), reporter, APPROVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("归档"));
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(archivist.userId);
|
||||
|
||||
long archiveTask = pendingTaskId(instanceId, archivist.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/archive"), archivist, ARCHIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("已归档"))
|
||||
.andExpect(jsonPath("$.data.archivistName").value(archivist.username))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(false));
|
||||
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'APPROVE'",
|
||||
instanceId)).isEqualTo(5);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'CONFIRM'",
|
||||
instanceId)).isEqualTo(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_coupon_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportsLineCountInSummaryAndCouponLines() throws Exception {
|
||||
String mark = "券导出" + data.uniqueSuffix();
|
||||
long instanceId = saveDraft(reporter, mark, List.of(
|
||||
row("GYM_WEEKLY", 7, LocalDate.parse("2027-01-31")),
|
||||
row("GYM_MONTHLY", 2, LocalDate.parse("2027-06-30"))));
|
||||
String reportOrgName = data.server().queryForObject(
|
||||
"SELECT organization_name FROM sys_organization WHERE organization_id = ?", String.class, organizationId);
|
||||
|
||||
// 跨单据总表:明细行数 = 2,合计金额为空(券单无金额列)
|
||||
String response = mockMvc.perform(authed(post("/api/approval/export"), reporter, LIST, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"keywords\":" + quote(mark) + "}"))
|
||||
.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("明细行数");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(17))).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/coupon/lines"), reporter, LIST, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"flowCode\":\"coupon\",\"keywords\":" + quote(mark) + "}"))
|
||||
.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(cellText(sheet.getRow(0).getCell(0))).isEqualTo("单号");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(4))).isEqualTo("券种");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(5))).isEqualTo("数量");
|
||||
assertThat(cellText(sheet.getRow(0).getCell(6))).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("GYM_WEEKLY");
|
||||
assertThat(cellText(first.getCell(5))).isEqualTo("7");
|
||||
assertThat(cellText(first.getCell(6))).isEqualTo("2027-01-31");
|
||||
Row second = sheet.getRow(2);
|
||||
assertThat(cellText(second.getCell(4))).isEqualTo("GYM_MONTHLY");
|
||||
assertThat(cellText(second.getCell(5))).isEqualTo("2");
|
||||
assertThat(cellText(second.getCell(6))).isEqualTo("2027-06-30");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private ApprovalTestData.TestUser createUser(String realName, Integer organizationId) {
|
||||
ApprovalTestData.TestUser user = data.createUser(realName, organizationId, TENANT);
|
||||
userIds.add(user.userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
private long saveDraft(ApprovalTestData.TestUser user, String applyReason,
|
||||
List<Map<String, Object>> lines) throws Exception {
|
||||
return saveDraft(user, applyReason, lines, null);
|
||||
}
|
||||
|
||||
private long saveDraft(ApprovalTestData.TestUser user, String applyReason,
|
||||
List<Map<String, Object>> lines, Long id) throws Exception {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
if (id != null) {
|
||||
body.put("id", id);
|
||||
}
|
||||
body.put("applyReason", applyReason);
|
||||
body.put("lines", lines);
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/coupon"), user, 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("applyReason", APPLY_REASON);
|
||||
body.put("lines", lines);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coupon"), 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 Map<String, Object> row(String couponType, int quantity, LocalDate validUntil) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("couponType", couponType);
|
||||
row.put("quantity", quantity);
|
||||
row.put("validUntil", validUntil.toString());
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 逐个节点通过:断言推进到下一节点名称,并返回该节点 key 供顺序校验。 */
|
||||
private String approve(long instanceId, ApprovalTestData.TestUser approver, String nextNodeName)
|
||||
throws Exception {
|
||||
long taskId = pendingTaskId(instanceId, approver.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/approve"), approver, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value(nextNodeName));
|
||||
return data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
}
|
||||
|
||||
private void uploadAttachment(long instanceId, String category) throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "voucher.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, 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 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 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 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(org.apache.poi.ss.usermodel.Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
}
|
||||
if (cell.getCellType() == org.apache.poi.ss.usermodel.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, organizationId, TENANT,
|
||||
authorities)));
|
||||
}
|
||||
|
||||
private String quote(String value) {
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user