Merge branch 'feat/approval-phase1' into feat/approval-t24
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
package com.gxwebsoft.approval.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||||
|
import com.gxwebsoft.approval.service.ApprovalExportService;
|
||||||
|
import com.gxwebsoft.approval.support.ApprovalExportResult;
|
||||||
|
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.common.system.entity.FileRecord;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PC 单据导出接口:与单据查询共用筛选参数与数据权限。
|
||||||
|
*/
|
||||||
|
@Api(tags = "审批单据导出")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/approval")
|
||||||
|
public class ApprovalExportController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalExportService exportService;
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('approval:instance:export')")
|
||||||
|
@OperationLog(module = "审批管理", comments = "导出单据查询结果")
|
||||||
|
@ApiOperation("导出单据")
|
||||||
|
@PostMapping("/export")
|
||||||
|
public ApiResult<FileRecord> export(@RequestBody ApprovalQueryParam param) throws IOException {
|
||||||
|
ApprovalExportResult result = exportService.export(param, getLoginUser());
|
||||||
|
return success("导出成功,共" + result.getRowCount() + "条", result.getFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.gxwebsoft.approval.mapper;
|
||||||
|
|
||||||
|
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||||
|
import com.gxwebsoft.approval.support.ApprovalExportRow;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出查询Mapper:独立的非分页 SQL,绕过分页插件上限,最多取 5001 行。
|
||||||
|
*
|
||||||
|
* <p>筛选条件与数据权限必须与 {@code ApprovalQueryMapper.selectInstancePage} 保持一致。</p>
|
||||||
|
*/
|
||||||
|
public interface ApprovalExportMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按筛选条件取导出数据,最多 5001 行(超出即代表需要拒绝导出)。
|
||||||
|
*/
|
||||||
|
List<ApprovalExportRow> selectExportRows(@Param("param") ApprovalQueryParam param,
|
||||||
|
@Param("userId") Integer userId,
|
||||||
|
@Param("allVisible") Integer allVisible);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.gxwebsoft.approval.mapper.ApprovalExportMapper">
|
||||||
|
|
||||||
|
<!-- 导出:数据权限与筛选条件同 ApprovalQueryMapper.selectInstancePage;LIMIT 5001 由 SQL 固定,不经过分页插件 -->
|
||||||
|
<select id="selectExportRows" resultType="com.gxwebsoft.approval.support.ApprovalExportRow">
|
||||||
|
SELECT i.id,
|
||||||
|
i.biz_no,
|
||||||
|
i.doc_type,
|
||||||
|
i.status,
|
||||||
|
i.reporter_name,
|
||||||
|
i.report_org_name,
|
||||||
|
i.accept_org_name,
|
||||||
|
i.current_node_key,
|
||||||
|
i.definition_id,
|
||||||
|
i.submit_time,
|
||||||
|
i.approve_time,
|
||||||
|
i.archive_time,
|
||||||
|
i.archivist_name,
|
||||||
|
f.name,
|
||||||
|
f.implement_org,
|
||||||
|
f.fee,
|
||||||
|
f.service_content,
|
||||||
|
co.title,
|
||||||
|
co.content,
|
||||||
|
co.remark
|
||||||
|
FROM approval_instance i
|
||||||
|
LEFT JOIN approval_completion_form f ON f.instance_id = i.id
|
||||||
|
LEFT JOIN approval_coordination_form co ON co.instance_id = i.id
|
||||||
|
WHERE (
|
||||||
|
i.reporter_id = #{userId}
|
||||||
|
OR (i.status != 'DRAFT' AND (
|
||||||
|
#{allVisible} = 1
|
||||||
|
OR EXISTS (SELECT 1 FROM approval_task t WHERE t.instance_id = i.id AND t.assignee_id = #{userId})
|
||||||
|
OR EXISTS (SELECT 1 FROM approval_record r WHERE r.instance_id = i.id AND r.operator_id = #{userId})
|
||||||
|
OR i.dispatch_target_id = #{userId}
|
||||||
|
))
|
||||||
|
)
|
||||||
|
<if test="param.docType != null and param.docType != ''">
|
||||||
|
AND i.doc_type = #{param.docType}
|
||||||
|
</if>
|
||||||
|
<if test="param.bizNo != null and param.bizNo != ''">
|
||||||
|
AND i.biz_no LIKE CONCAT('%', #{param.bizNo}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.reportOrgId != null">
|
||||||
|
AND i.report_org_id = #{param.reportOrgId}
|
||||||
|
</if>
|
||||||
|
<if test="param.acceptOrgId != null">
|
||||||
|
AND i.accept_org_id = #{param.acceptOrgId}
|
||||||
|
</if>
|
||||||
|
<if test="param.reporterId != null">
|
||||||
|
AND i.reporter_id = #{param.reporterId}
|
||||||
|
</if>
|
||||||
|
<if test="param.status != null and param.status != ''">
|
||||||
|
AND i.status = #{param.status}
|
||||||
|
</if>
|
||||||
|
<if test="param.keyword != null and param.keyword != ''">
|
||||||
|
AND (i.biz_no LIKE CONCAT('%', #{param.keyword}, '%')
|
||||||
|
OR f.name LIKE CONCAT('%', #{param.keyword}, '%')
|
||||||
|
OR co.title LIKE CONCAT('%', #{param.keyword}, '%')
|
||||||
|
OR f.implement_org LIKE CONCAT('%', #{param.keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="param.submitTimeStart != null and param.submitTimeStart != ''">
|
||||||
|
AND i.submit_time >= #{param.submitTimeStart}
|
||||||
|
</if>
|
||||||
|
<if test="param.submitTimeEnd != null and param.submitTimeEnd != ''">
|
||||||
|
AND i.submit_time <= #{param.submitTimeEnd}
|
||||||
|
</if>
|
||||||
|
<if test="param.archiveTimeStart != null and param.archiveTimeStart != ''">
|
||||||
|
AND i.archive_time >= #{param.archiveTimeStart}
|
||||||
|
</if>
|
||||||
|
<if test="param.archiveTimeEnd != null and param.archiveTimeEnd != ''">
|
||||||
|
AND i.archive_time <= #{param.archiveTimeEnd}
|
||||||
|
</if>
|
||||||
|
ORDER BY (i.submit_time IS NULL) ASC, i.submit_time DESC, i.id DESC
|
||||||
|
LIMIT 5001
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.gxwebsoft.approval.service;
|
||||||
|
|
||||||
|
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||||
|
import com.gxwebsoft.approval.mapper.ApprovalExportMapper;
|
||||||
|
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||||||
|
import com.gxwebsoft.approval.support.ApprovalExportResult;
|
||||||
|
import com.gxwebsoft.approval.support.ApprovalExportRow;
|
||||||
|
import com.gxwebsoft.approval.support.ApprovalNode;
|
||||||
|
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||||
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.common.system.entity.FileRecord;
|
||||||
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFRow;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFSheet;
|
||||||
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据导出:按列表同一套筛选与数据权限同步生成 xlsx,并写文件记录。
|
||||||
|
*
|
||||||
|
* <p>独立查询一次取 5001 行判断是否超限,不经过分页插件的 2000 条上限;
|
||||||
|
* 超过 5000 行直接拒绝,不生成文件。导出内容不含附件。</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ApprovalExportService {
|
||||||
|
|
||||||
|
/** 单次导出上限,超过即要求缩小筛选范围。 */
|
||||||
|
public static final int MAX_EXPORT_ROWS = 5000;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private static final String[] HEADERS = {
|
||||||
|
"单号", "单据类型", "标题或名称", "报送部门", "报送人", "受理部门", "实施单位", "费用(元)",
|
||||||
|
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人"
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 列宽(字符数),长文本列加宽但不截断内容。 */
|
||||||
|
private static final int[] COLUMN_WIDTHS = {
|
||||||
|
20, 16, 28, 20, 12, 20, 22, 12, 50, 28, 12, 16, 20, 20, 20, 12
|
||||||
|
};
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalExportMapper exportMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalFlowService flowService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalDirectoryService directoryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalFileService fileService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ConfigProperties config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出当前筛选结果。
|
||||||
|
*
|
||||||
|
* @throws BusinessException 结果超过 {@link #MAX_EXPORT_ROWS} 行时拒绝,不生成文件
|
||||||
|
*/
|
||||||
|
public ApprovalExportResult export(ApprovalQueryParam param, User user) throws IOException {
|
||||||
|
int allVisible = directoryService.hasAnyRole(user.getUserId(),
|
||||||
|
ApprovalConstants.ROLE_SUPER_ADMIN, ApprovalConstants.ROLE_ARCHIVIST) ? 1 : 0;
|
||||||
|
List<ApprovalExportRow> rows = exportMapper.selectExportRows(param, user.getUserId(), allVisible);
|
||||||
|
if (rows.size() > MAX_EXPORT_ROWS) {
|
||||||
|
throw new BusinessException("请缩小筛选范围");
|
||||||
|
}
|
||||||
|
String filename = "excel/审批单据-" + System.currentTimeMillis() + ".xlsx";
|
||||||
|
String path = config.getUploadPath() + filename;
|
||||||
|
File target = new File(path);
|
||||||
|
try {
|
||||||
|
File parent = target.getParentFile();
|
||||||
|
if (parent != null) {
|
||||||
|
Files.createDirectories(parent.toPath());
|
||||||
|
}
|
||||||
|
writeWorkbook(rows, target);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
// 生成失败时不留下半成品文件
|
||||||
|
Files.deleteIfExists(target.toPath());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRecord record = new FileRecord();
|
||||||
|
record.setName(filename);
|
||||||
|
record.setPath(path);
|
||||||
|
record.setLength(target.length());
|
||||||
|
record.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
record.setCreateUserId(user.getUserId());
|
||||||
|
record.setUrl(config.getServerUrl() + "/file/" + filename);
|
||||||
|
fileService.save(record);
|
||||||
|
return new ApprovalExportResult(record, rows.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeWorkbook(List<ApprovalExportRow> rows, File target) throws IOException {
|
||||||
|
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||||
|
try {
|
||||||
|
SXSSFSheet sheet = workbook.createSheet("审批单据");
|
||||||
|
for (int i = 0; i < COLUMN_WIDTHS.length; i++) {
|
||||||
|
sheet.setColumnWidth(i, COLUMN_WIDTHS[i] * 256);
|
||||||
|
}
|
||||||
|
SXSSFRow header = sheet.createRow(0);
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
|
header.createCell(i).setCellValue(HEADERS[i]);
|
||||||
|
}
|
||||||
|
Map<Long, List<ApprovalNode>> nodeCache = new HashMap<>();
|
||||||
|
int rowNum = 1;
|
||||||
|
for (ApprovalExportRow row : rows) {
|
||||||
|
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||||||
|
String[] values = toValues(row, nodeCache);
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
dataRow.createCell(i).setCellValue(values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (OutputStream output = Files.newOutputStream(target.toPath())) {
|
||||||
|
workbook.write(output);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
workbook.dispose();
|
||||||
|
workbook.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 固定列顺序:单号、单据类型、标题或名称、报送部门、报送人、受理部门、实施单位、费用(元)、
|
||||||
|
* 服务内容或协调内容、备注、状态、当前节点、提交时间、审批完成时间、归档时间、归档人。
|
||||||
|
*/
|
||||||
|
private String[] toValues(ApprovalExportRow row, Map<Long, List<ApprovalNode>> nodeCache) {
|
||||||
|
boolean completion = ApprovalConstants.DOC_COMPLETION.equals(row.getDocType());
|
||||||
|
return new String[]{
|
||||||
|
text(row.getBizNo()),
|
||||||
|
text(ApprovalConstants.docTypeName(row.getDocType())),
|
||||||
|
completion ? text(row.getName()) : text(row.getTitle()),
|
||||||
|
text(row.getReportOrgName()),
|
||||||
|
text(row.getReporterName()),
|
||||||
|
text(row.getAcceptOrgName()),
|
||||||
|
completion ? text(row.getImplementOrg()) : "",
|
||||||
|
completion ? fee(row.getFee()) : "",
|
||||||
|
completion ? text(row.getServiceContent()) : text(row.getContent()),
|
||||||
|
text(row.getRemark()),
|
||||||
|
text(ApprovalConstants.statusName(row.getStatus())),
|
||||||
|
text(nodeName(nodeCache, row.getDefinitionId(), row.getCurrentNodeKey())),
|
||||||
|
time(row.getSubmitTime()),
|
||||||
|
time(row.getApproveTime()),
|
||||||
|
time(row.getArchiveTime()),
|
||||||
|
text(row.getArchivistName())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nodeName(Map<Long, List<ApprovalNode>> cache, Long definitionId, String nodeKey) {
|
||||||
|
if (definitionId == null || nodeKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<ApprovalNode> nodes = cache.computeIfAbsent(definitionId, flowService::parseNodes);
|
||||||
|
return nodes.stream()
|
||||||
|
.filter(node -> nodeKey.equals(node.getKey()))
|
||||||
|
.map(ApprovalNode::getName)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fee(BigDecimal fee) {
|
||||||
|
return fee == null ? "" : fee.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String time(LocalDateTime time) {
|
||||||
|
return time == null ? "" : TIME_FORMAT.format(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.gxwebsoft.approval.support;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.system.entity.FileRecord;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出结果:返回给前端的文件记录 + 实际导出行数(用于提示与操作日志)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ApprovalExportResult {
|
||||||
|
|
||||||
|
private final FileRecord file;
|
||||||
|
|
||||||
|
private final int rowCount;
|
||||||
|
|
||||||
|
public ApprovalExportResult(FileRecord file, int rowCount) {
|
||||||
|
this.file = file;
|
||||||
|
this.rowCount = rowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.gxwebsoft.approval.support;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出查询数据库行:两类单据合并列,未命中的表单字段为空。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ApprovalExportRow implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String bizNo;
|
||||||
|
private String docType;
|
||||||
|
private String status;
|
||||||
|
private String reporterName;
|
||||||
|
private String reportOrgName;
|
||||||
|
private String acceptOrgName;
|
||||||
|
private String currentNodeKey;
|
||||||
|
private Long definitionId;
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
private LocalDateTime approveTime;
|
||||||
|
private LocalDateTime archiveTime;
|
||||||
|
private String archivistName;
|
||||||
|
|
||||||
|
/** 业务完成确认单:名称。 */
|
||||||
|
private String name;
|
||||||
|
/** 业务完成确认单:实施单位。 */
|
||||||
|
private String implementOrg;
|
||||||
|
/** 业务完成确认单:费用(元)。 */
|
||||||
|
private BigDecimal fee;
|
||||||
|
/** 业务完成确认单:服务内容。 */
|
||||||
|
private String serviceContent;
|
||||||
|
|
||||||
|
/** 内部协调单:标题。 */
|
||||||
|
private String title;
|
||||||
|
/** 内部协调单:协调内容。 */
|
||||||
|
private String content;
|
||||||
|
/** 内部协调单:备注。 */
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
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.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.nio.file.Paths;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
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.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据导出:权限、5000 行上限、列头与内容、数据权限与操作日志。
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||||
|
class ApprovalExportIT {
|
||||||
|
|
||||||
|
private static final int TENANT = TestAuthentication.TENANT_ID;
|
||||||
|
private static final String LIST = "approval:instance:list";
|
||||||
|
private static final String EXPORT = "approval:instance:export";
|
||||||
|
private static final String URL = "/api/approval/export";
|
||||||
|
private static final int EXPORT_LIMIT = 5000;
|
||||||
|
/** 多值 INSERT 每批行数,避免超出占位符上限。 */
|
||||||
|
private static final int SEED_CHUNK = 200;
|
||||||
|
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private static final String[] HEADERS = {
|
||||||
|
"单号", "单据类型", "标题或名称", "报送部门", "报送人", "受理部门", "实施单位", "费用(元)",
|
||||||
|
"服务内容或协调内容", "备注", "状态", "当前节点", "提交时间", "审批完成时间", "归档时间", "归档人"
|
||||||
|
};
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
@Autowired
|
||||||
|
private DataSource dataSource;
|
||||||
|
@Autowired
|
||||||
|
private ConfigProperties config;
|
||||||
|
|
||||||
|
private ApprovalTestData data;
|
||||||
|
private Integer reportOrgId;
|
||||||
|
private Integer acceptOrgId;
|
||||||
|
private ApprovalTestData.TestUser reporter;
|
||||||
|
/** 本次用例创建的导出文件,结束时删除。 */
|
||||||
|
private final List<Path> exportedFiles = new ArrayList<>();
|
||||||
|
/** 本次用例批量种子的关键字,结束时按单号前缀清理。 */
|
||||||
|
private final List<String> batchMarks = new ArrayList<>();
|
||||||
|
/** 本次用例创建的用户id,结束时清理其操作日志。 */
|
||||||
|
private final List<Integer> testUserIds = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
data = new ApprovalTestData(dataSource);
|
||||||
|
String suffix = data.uniqueSuffix();
|
||||||
|
reportOrgId = data.createOrganization("报送部门" + suffix, 136);
|
||||||
|
acceptOrgId = data.createOrganization("受理部门" + suffix, 136);
|
||||||
|
reporter = createUser("报送人" + suffix, reportOrgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
for (String mark : batchMarks) {
|
||||||
|
cleanBatch(mark);
|
||||||
|
}
|
||||||
|
batchMarks.clear();
|
||||||
|
for (Path path : exportedFiles) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 文件清理失败不影响断言结果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exportedFiles.clear();
|
||||||
|
try {
|
||||||
|
// 操作日志异步写入,稍等后再按测试用户清理
|
||||||
|
Thread.sleep(300);
|
||||||
|
for (Integer userId : testUserIds) {
|
||||||
|
data.server().update("DELETE FROM sys_operation_record WHERE url = ? AND user_id = ?", URL, userId);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 共享库清理尽力而为
|
||||||
|
}
|
||||||
|
testUserIds.clear();
|
||||||
|
data.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsWithoutExportAuthority() throws Exception {
|
||||||
|
mockMvc.perform(authed(post(URL), reporter, LIST)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsWhenResultExceedsLimitAndWritesNoFile() throws Exception {
|
||||||
|
String mark = "超限" + data.uniqueSuffix();
|
||||||
|
seedCompletionBatch(mark, EXPORT_LIMIT + 1);
|
||||||
|
List<Path> before = listExportFiles();
|
||||||
|
|
||||||
|
mockMvc.perform(authed(post(URL), reporter, LIST, EXPORT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"keyword\":" + quote(mark) + "}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("请缩小筛选范围")));
|
||||||
|
|
||||||
|
assertThat(listExportFiles()).isEqualTo(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportsExactlyLimitRowsWithFixedColumnsAndFullText() throws Exception {
|
||||||
|
String mark = "上限" + data.uniqueSuffix();
|
||||||
|
String specialName = mark + "特殊名称";
|
||||||
|
String longContent = repeat("场馆设施维修与验收", 200);
|
||||||
|
Map<String, Object> special = new LinkedHashMap<>();
|
||||||
|
special.put("name", specialName);
|
||||||
|
special.put("implementOrg", mark + "实施单位");
|
||||||
|
special.put("fee", "1234.56");
|
||||||
|
special.put("serviceContent", longContent);
|
||||||
|
LocalDateTime submitTime = LocalDateTime.now().withNano(0).minusDays(1);
|
||||||
|
LocalDateTime approveTime = submitTime.plusHours(2);
|
||||||
|
LocalDateTime archiveTime = submitTime.plusDays(1);
|
||||||
|
seedCompletionBatch(mark, EXPORT_LIMIT, special, submitTime, approveTime, archiveTime);
|
||||||
|
|
||||||
|
String response = mockMvc.perform(authed(post(URL), reporter, LIST, EXPORT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"keyword\":" + quote(mark) + "}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
JsonNode node = objectMapper.readTree(response).path("data");
|
||||||
|
assertThat(node.path("id").asInt()).isPositive();
|
||||||
|
String name = node.path("name").asText();
|
||||||
|
assertThat(name).startsWith("excel/审批单据-").endsWith(".xlsx");
|
||||||
|
assertThat(node.path("url").asText()).contains("/file/excel/审批单据-");
|
||||||
|
File file = new File(node.path("path").asText());
|
||||||
|
assertThat(file).exists();
|
||||||
|
exportedFiles.add(file.toPath());
|
||||||
|
assertThat(data.countServer("SELECT COUNT(*) FROM sys_file_record WHERE id = ? AND name = ?",
|
||||||
|
node.path("id").asInt(), name)).isEqualTo(1);
|
||||||
|
|
||||||
|
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(file))) {
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
assertThat(sheet.getLastRowNum() + 1).isEqualTo(EXPORT_LIMIT + 1);
|
||||||
|
Row header = sheet.getRow(0);
|
||||||
|
List<String> headerValues = new ArrayList<>();
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
|
headerValues.add(cellText(header.getCell(i)));
|
||||||
|
}
|
||||||
|
assertThat(headerValues).containsExactly(HEADERS);
|
||||||
|
|
||||||
|
Row matched = null;
|
||||||
|
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||||
|
Row row = sheet.getRow(i);
|
||||||
|
if (specialName.equals(cellText(row.getCell(2)))) {
|
||||||
|
matched = row;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(matched).as("应导出特殊名称行").isNotNull();
|
||||||
|
assertThat(cellText(matched.getCell(0))).isEqualTo(mark + "-0");
|
||||||
|
assertThat(cellText(matched.getCell(1))).isEqualTo("业务完成确认单");
|
||||||
|
assertThat(cellText(matched.getCell(3))).isEqualTo(orgName(reportOrgId));
|
||||||
|
assertThat(cellText(matched.getCell(4))).isEqualTo(reporter.realName);
|
||||||
|
assertThat(cellText(matched.getCell(5))).isEqualTo(orgName(acceptOrgId));
|
||||||
|
assertThat(cellText(matched.getCell(6))).isEqualTo(mark + "实施单位");
|
||||||
|
assertThat(cellText(matched.getCell(7))).isEqualTo("1234.56");
|
||||||
|
assertThat(cellText(matched.getCell(8))).isEqualTo(longContent);
|
||||||
|
assertThat(cellText(matched.getCell(9))).isEmpty();
|
||||||
|
assertThat(cellText(matched.getCell(10))).isEqualTo("已归档");
|
||||||
|
assertThat(cellText(matched.getCell(11))).isEqualTo("报送部门负责人");
|
||||||
|
assertThat(cellText(matched.getCell(12))).isEqualTo(TIME_FORMAT.format(submitTime));
|
||||||
|
assertThat(cellText(matched.getCell(13))).isEqualTo(TIME_FORMAT.format(approveTime));
|
||||||
|
assertThat(cellText(matched.getCell(14))).isEqualTo(TIME_FORMAT.format(archiveTime));
|
||||||
|
assertThat(cellText(matched.getCell(15))).isEqualTo("收文人" + mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesSameDataPermissionAsList() throws Exception {
|
||||||
|
String mark = "权限" + data.uniqueSuffix();
|
||||||
|
long draft = seedCompletion(mark + "草稿", "DRAFT", null);
|
||||||
|
long pending = seedCompletion(mark + "在途", "PENDING", LocalDateTime.now().minusHours(1));
|
||||||
|
ApprovalTestData.TestUser outsider = createUser("无关人员" + data.uniqueSuffix(), acceptOrgId);
|
||||||
|
ApprovalTestData.TestUser assignee = createUser("任务处理人" + data.uniqueSuffix(), acceptOrgId);
|
||||||
|
ApprovalTestData.TestUser archivist = createUser("收文员" + data.uniqueSuffix(), acceptOrgId);
|
||||||
|
data.master().update("INSERT INTO approval_task (tenant_id, instance_id, node_key, node_type, round, "
|
||||||
|
+ "task_type, assignee_id, status) VALUES (?, ?, 'deptLeader', 'APPROVE', 1, 'APPROVE', ?, 'PENDING')",
|
||||||
|
TENANT, pending, assignee.userId);
|
||||||
|
ApprovalTestData.TestRole archivistRole = data.createRole(ApprovalConstants.ROLE_ARCHIVIST, "收文员");
|
||||||
|
data.bind(archivist.userId, archivistRole.roleId);
|
||||||
|
|
||||||
|
assertThat(exportBizNos(reporter, mark)).containsExactlyInAnyOrder(bizNoOf(draft), bizNoOf(pending));
|
||||||
|
assertThat(exportBizNos(assignee, mark)).containsExactly(bizNoOf(pending));
|
||||||
|
assertThat(exportBizNos(outsider, mark)).isEmpty();
|
||||||
|
assertThat(exportBizNos(archivist, mark)).containsExactly(bizNoOf(pending));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void writesOperationLogWithFilterAndRowCount() throws Exception {
|
||||||
|
String mark = "日志" + data.uniqueSuffix();
|
||||||
|
seedCompletion(mark + "一", "PENDING", LocalDateTime.now().minusHours(2));
|
||||||
|
seedCompletion(mark + "二", "PENDING", LocalDateTime.now().minusHours(1));
|
||||||
|
|
||||||
|
mockMvc.perform(authed(post(URL), reporter, LIST, EXPORT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"keyword\":" + quote(mark) + "}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("共2条")));
|
||||||
|
|
||||||
|
Map<String, Object> record = awaitOperationRecord(mark);
|
||||||
|
assertThat(record).as("应记录导出操作日志").isNotNull();
|
||||||
|
assertThat(record.get("user_id")).isEqualTo(reporter.userId);
|
||||||
|
assertThat((String) record.get("params")).contains(mark);
|
||||||
|
assertThat((String) record.get("result")).contains("共2条");
|
||||||
|
assertThat(record.get("create_time")).isNotNull();
|
||||||
|
assertThat(((Number) record.get("spend_time")).longValue()).isNotNegative();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
|
||||||
|
private ApprovalTestData.TestUser createUser(String realName, Integer organizationId) {
|
||||||
|
ApprovalTestData.TestUser user = data.createUser(realName, organizationId);
|
||||||
|
testUserIds.add(user.userId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> awaitOperationRecord(String paramsMark) throws InterruptedException {
|
||||||
|
long deadline = System.currentTimeMillis() + 3000;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
List<Map<String, Object>> rows = data.server().queryForList(
|
||||||
|
"SELECT user_id, params, result, spend_time, create_time FROM sys_operation_record "
|
||||||
|
+ "WHERE url = ? AND params LIKE ? ORDER BY id DESC LIMIT 1",
|
||||||
|
URL, "%" + paramsMark + "%");
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
return rows.get(0);
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> exportBizNos(ApprovalTestData.TestUser user, String keyword) throws Exception {
|
||||||
|
String response = mockMvc.perform(authed(post(URL), user, LIST, EXPORT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"keyword\":" + quote(keyword) + "}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(0))
|
||||||
|
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
JsonNode node = objectMapper.readTree(response).path("data");
|
||||||
|
File file = new File(node.path("path").asText());
|
||||||
|
exportedFiles.add(file.toPath());
|
||||||
|
List<String> bizNos = new ArrayList<>();
|
||||||
|
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(file))) {
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||||
|
bizNos.add(cellText(sheet.getRow(i).getCell(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bizNos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long seedCompletion(String name, String status, LocalDateTime submitTime) {
|
||||||
|
long definitionId = completionDefinitionId();
|
||||||
|
String bizNo = "WG" + data.uniqueSuffix();
|
||||||
|
data.master().update("INSERT INTO approval_instance (tenant_id, biz_no, flow_code, definition_id, "
|
||||||
|
+ "definition_version, doc_type, biz_id, status, round, reporter_id, reporter_name, report_org_id, "
|
||||||
|
+ "report_org_name, accept_org_id, accept_org_name, current_node_key, current_node_type, submit_time) "
|
||||||
|
+ "VALUES (?, ?, 'completion', ?, 1, 'completion', 0, ?, 1, ?, ?, ?, ?, ?, ?, 'deptLeader', 'APPROVE', ?)",
|
||||||
|
TENANT, bizNo, definitionId, status, reporter.userId, reporter.realName, reportOrgId,
|
||||||
|
orgName(reportOrgId), acceptOrgId, orgName(acceptOrgId),
|
||||||
|
submitTime == null ? null : Timestamp.valueOf(submitTime));
|
||||||
|
Long instanceId = data.queryMaster("SELECT id FROM approval_instance WHERE tenant_id = ? AND biz_no = ?",
|
||||||
|
Long.class, TENANT, bizNo);
|
||||||
|
data.trackInstance(instanceId);
|
||||||
|
data.master().update("INSERT INTO approval_completion_form (tenant_id, instance_id, name, implement_org, "
|
||||||
|
+ "fee, service_content) VALUES (?, ?, ?, '广西体育中心实施部', 100.00, '完成场馆设施维修与验收')",
|
||||||
|
TENANT, instanceId, name);
|
||||||
|
Long formId = data.queryMaster("SELECT id FROM approval_completion_form WHERE instance_id = ?",
|
||||||
|
Long.class, instanceId);
|
||||||
|
data.master().update("UPDATE approval_instance SET biz_id = ? WHERE id = ?", formId, instanceId);
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bizNoOf(long instanceId) {
|
||||||
|
return data.queryMaster("SELECT biz_no FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量种子:count 条已归档确认单,单号前缀与名称都用 mark 标识,便于按前缀清理。
|
||||||
|
*
|
||||||
|
* <p>master 库连接未开启 rewriteBatchedStatements,逐条 batchUpdate 会产生上千次往返;
|
||||||
|
* 这里按 200 行一条多值 INSERT 落库,再一次性回填 biz_id。</p>
|
||||||
|
*/
|
||||||
|
private void seedCompletionBatch(String mark, int count) {
|
||||||
|
seedCompletionBatch(mark, count, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedCompletionBatch(String mark, int count, Map<String, Object> special,
|
||||||
|
LocalDateTime submitTime, LocalDateTime approveTime,
|
||||||
|
LocalDateTime archiveTime) {
|
||||||
|
batchMarks.add(mark);
|
||||||
|
long definitionId = completionDefinitionId();
|
||||||
|
String reportOrgName = orgName(reportOrgId);
|
||||||
|
String acceptOrgName = orgName(acceptOrgId);
|
||||||
|
Timestamp submit = Timestamp.valueOf(submitTime == null
|
||||||
|
? LocalDateTime.now().withNano(0).minusHours(1) : submitTime);
|
||||||
|
Timestamp approve = approveTime == null ? null : Timestamp.valueOf(approveTime);
|
||||||
|
Timestamp archive = archiveTime == null ? null : Timestamp.valueOf(archiveTime);
|
||||||
|
Integer archivistId = archive == null ? null : reporter.userId;
|
||||||
|
String archivistName = archive == null ? null : "收文人" + mark;
|
||||||
|
|
||||||
|
String instanceRow = "(?, ?, 'completion', ?, 1, 'completion', 0, 'ARCHIVED', 1, ?, ?, ?, ?, ?, ?, "
|
||||||
|
+ "'deptLeader', 'APPROVE', ?, ?, ?, ?, ?)";
|
||||||
|
for (int start = 0; start < count; start += SEED_CHUNK) {
|
||||||
|
int end = Math.min(start + SEED_CHUNK, count);
|
||||||
|
StringBuilder sql = new StringBuilder("INSERT INTO approval_instance (tenant_id, biz_no, flow_code, "
|
||||||
|
+ "definition_id, definition_version, doc_type, biz_id, status, round, reporter_id, reporter_name, "
|
||||||
|
+ "report_org_id, report_org_name, accept_org_id, accept_org_name, current_node_key, current_node_type, "
|
||||||
|
+ "submit_time, approve_time, archive_time, archivist_id, archivist_name) VALUES ");
|
||||||
|
List<Object> args = new ArrayList<>();
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (i > start) {
|
||||||
|
sql.append(',');
|
||||||
|
}
|
||||||
|
sql.append(instanceRow);
|
||||||
|
java.util.Collections.addAll(args, TENANT, mark + "-" + i, definitionId, reporter.userId,
|
||||||
|
reporter.realName, reportOrgId, reportOrgName, acceptOrgId, acceptOrgName, submit,
|
||||||
|
approve, archive, archivistId, archivistName);
|
||||||
|
}
|
||||||
|
data.master().update(sql.toString(), args.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> ids = data.master().queryForList(
|
||||||
|
"SELECT id FROM approval_instance WHERE tenant_id = ? AND biz_no LIKE ? ORDER BY id",
|
||||||
|
Long.class, TENANT, mark + "-%");
|
||||||
|
assertThat(ids).hasSize(count);
|
||||||
|
|
||||||
|
for (int start = 0; start < ids.size(); start += SEED_CHUNK) {
|
||||||
|
int end = Math.min(start + SEED_CHUNK, ids.size());
|
||||||
|
StringBuilder sql = new StringBuilder("INSERT INTO approval_completion_form (tenant_id, instance_id, "
|
||||||
|
+ "name, implement_org, fee, service_content) VALUES ");
|
||||||
|
List<Object> args = new ArrayList<>();
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
if (i > start) {
|
||||||
|
sql.append(',');
|
||||||
|
}
|
||||||
|
sql.append("(?, ?, ?, ?, ?, ?)");
|
||||||
|
boolean isSpecial = special != null && i == 0;
|
||||||
|
java.util.Collections.addAll(args, TENANT, ids.get(i),
|
||||||
|
isSpecial ? special.get("name") : mark + "-" + i,
|
||||||
|
isSpecial ? special.get("implementOrg") : "广西体育中心实施部",
|
||||||
|
new java.math.BigDecimal(isSpecial ? (String) special.get("fee") : "100.00"),
|
||||||
|
isSpecial ? special.get("serviceContent") : "完成场馆设施维修与验收");
|
||||||
|
}
|
||||||
|
data.master().update(sql.toString(), args.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
data.master().update("UPDATE approval_instance i JOIN approval_completion_form f "
|
||||||
|
+ "ON f.instance_id = i.id SET i.biz_id = f.id WHERE i.biz_no LIKE ?", mark + "-%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanBatch(String mark) {
|
||||||
|
try {
|
||||||
|
String[] tables = {"approval_attachment", "approval_record", "approval_task",
|
||||||
|
"approval_completion_form", "approval_coordination_form"};
|
||||||
|
for (String table : tables) {
|
||||||
|
data.master().update("DELETE t FROM " + table + " t JOIN approval_instance i "
|
||||||
|
+ "ON t.instance_id = i.id WHERE i.biz_no LIKE ?", mark + "-%");
|
||||||
|
}
|
||||||
|
data.master().update("DELETE FROM approval_instance WHERE biz_no LIKE ?", mark + "-%");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 清理失败不阻断测试报告
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long completionDefinitionId() {
|
||||||
|
List<Long> ids = data.master().queryForList("SELECT id FROM approval_flow_definition WHERE tenant_id = ? "
|
||||||
|
+ "AND flow_code = 'completion' AND version = 1 LIMIT 1", Long.class, TENANT);
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
data.master().update("INSERT INTO approval_flow_definition (tenant_id, flow_code, version, name, nodes_json) "
|
||||||
|
+ "VALUES (?, 'completion', 1, '业务完成确认单', ?)",
|
||||||
|
TENANT, ApprovalConstants.COMPLETION_NODES_JSON);
|
||||||
|
ids = data.master().queryForList("SELECT id FROM approval_flow_definition WHERE tenant_id = ? "
|
||||||
|
+ "AND flow_code = 'completion' AND version = 1 LIMIT 1", Long.class, TENANT);
|
||||||
|
}
|
||||||
|
return ids.isEmpty() ? 0L : ids.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Path> listExportFiles() throws Exception {
|
||||||
|
Path dir = Paths.get(config.getUploadPath(), "excel");
|
||||||
|
if (!Files.isDirectory(dir)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<Path> files = new ArrayList<>();
|
||||||
|
try (java.util.stream.Stream<Path> stream = Files.list(dir)) {
|
||||||
|
stream.filter(path -> path.getFileName().toString().startsWith("审批单据-"))
|
||||||
|
.forEach(files::add);
|
||||||
|
}
|
||||||
|
files.sort(Comparator.comparing(Path::toString));
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String orgName(Integer organizationId) {
|
||||||
|
return data.server().queryForObject("SELECT organization_name FROM sys_organization WHERE organization_id = ?",
|
||||||
|
String.class, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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("\"", "\\\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String repeat(String unit, int times) {
|
||||||
|
StringBuilder builder = new StringBuilder(unit.length() * times);
|
||||||
|
for (int i = 0; i < times; i++) {
|
||||||
|
builder.append(unit);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user