feat(approval): 单据查询导出 xlsx (#26)
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;
|
||||
}
|
||||
Reference in New Issue
Block a user