d5001c981c
- ApprovalDocumentType SPI + 注册表:新增单据不再改引擎 - ApprovalApproverResolver:会签组、orgLeaderByCode/orgDeputyByCode、租户岗位解析器 - 会签:任务带 group_key/group_name/group_seq,一人多组一次通过,只取消同组待办 - 回填:节点 writableFields、BACKFILL 动作、approval_field_change 留痕 - 附件:新增 VOUCHER 分类并按单据类型校验必传 - 详情:document.fields/lines/lineColumns/groupProgress + permissions.writableFields - 列表/导出:title/search_text 落库、flowCode 筛选、明细行数/合计金额/会签进度列、明细导出 - sql/05-approval_phase2.sql(可重复执行)
375 lines
15 KiB
Java
375 lines
15 KiB
Java
package com.gxwebsoft.approval.service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||
import com.gxwebsoft.approval.mapper.ApprovalExportMapper;
|
||
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
|
||
import com.gxwebsoft.approval.param.ApprovalQueryParam;
|
||
import com.gxwebsoft.approval.support.ApprovalExportResult;
|
||
import com.gxwebsoft.approval.support.ApprovalExportRow;
|
||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||
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.Menu;
|
||
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.ArrayList;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 单据导出:跨单据总表与按单据类型的明细导出。
|
||
*
|
||
* <p>总表保持「一行单据一行」,只多「明细行数」「合计金额」与「会签进度」三列(ADR 0004);
|
||
* 逐行数据另做按 flow_code 的明细导出,供对账与成本归集。导出内容不含附件。</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");
|
||
|
||
/** 一期 16 列顺序保持不变,二期三列追加在末尾(ADR 0004 / 0003)。 */
|
||
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, 10, 14, 18
|
||
};
|
||
|
||
@Resource
|
||
private ApprovalExportMapper exportMapper;
|
||
@Resource
|
||
private ApprovalInstanceMapper instanceMapper;
|
||
@Resource
|
||
private ApprovalDocumentRegistry documentRegistry;
|
||
@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;
|
||
int assignVisible = hasAuthority(user, ApprovalConstants.PERMISSION_ASSIGN) ? 1 : 0;
|
||
List<ApprovalExportRow> rows = exportMapper.selectExportRows(param, user.getUserId(), allVisible,
|
||
assignVisible);
|
||
if (rows.size() > MAX_EXPORT_ROWS) {
|
||
throw new BusinessException("请缩小筛选范围");
|
||
}
|
||
String filename = "excel/审批单据-" + System.currentTimeMillis() + ".xlsx";
|
||
return write(user, rows.size(), filename, sheet -> fillSummarySheet(sheet, rows));
|
||
}
|
||
|
||
/**
|
||
* 按单据类型的明细导出:明细列 + 单号/报送部门/状态,含逐行数据。
|
||
*/
|
||
public ApprovalExportResult exportLines(String flowCode, ApprovalQueryParam param, User user)
|
||
throws IOException {
|
||
ApprovalDocumentType type = documentRegistry.require(flowCode);
|
||
List<ApprovalLineSpec> columns = type.lineColumns();
|
||
if (columns == null || columns.isEmpty()) {
|
||
throw new BusinessException("该单据类型没有明细行");
|
||
}
|
||
int allVisible = directoryService.hasAnyRole(user.getUserId(),
|
||
ApprovalConstants.ROLE_SUPER_ADMIN, ApprovalConstants.ROLE_ARCHIVIST) ? 1 : 0;
|
||
int assignVisible = hasAuthority(user, ApprovalConstants.PERMISSION_ASSIGN) ? 1 : 0;
|
||
List<ApprovalExportRow> instances = exportMapper.selectExportRows(param, user.getUserId(), allVisible,
|
||
assignVisible);
|
||
if (instances.size() > MAX_EXPORT_ROWS) {
|
||
throw new BusinessException("请缩小筛选范围");
|
||
}
|
||
List<ExportLine> lines = new ArrayList<>();
|
||
for (ApprovalExportRow row : instances) {
|
||
if (!flowCode.equals(row.getFlowCode())) {
|
||
continue;
|
||
}
|
||
ApprovalInstance instance = instanceMapper.selectById(row.getId());
|
||
if (instance == null) {
|
||
continue;
|
||
}
|
||
for (Map<String, Object> line : type.lines(instance)) {
|
||
lines.add(new ExportLine(row, line));
|
||
}
|
||
}
|
||
String filename = "excel/审批单据明细-" + flowCode + "-" + System.currentTimeMillis() + ".xlsx";
|
||
return write(user, lines.size(), filename, sheet -> fillLineSheet(sheet, type, columns, lines));
|
||
}
|
||
|
||
/** 明细导出的一行:所属单据 + 明细行。 */
|
||
private static final class ExportLine {
|
||
private final ApprovalExportRow instance;
|
||
private final Map<String, Object> line;
|
||
|
||
private ExportLine(ApprovalExportRow instance, Map<String, Object> line) {
|
||
this.instance = instance;
|
||
this.line = line;
|
||
}
|
||
}
|
||
|
||
private ApprovalExportResult write(User user, int rowCount, String filename,
|
||
WorkbookFiller filler) throws IOException {
|
||
String path = config.getUploadPath() + filename;
|
||
File target = new File(path);
|
||
try {
|
||
File parent = target.getParentFile();
|
||
if (parent != null) {
|
||
Files.createDirectories(parent.toPath());
|
||
}
|
||
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||
try {
|
||
filler.fill(workbook);
|
||
try (OutputStream output = Files.newOutputStream(target.toPath())) {
|
||
workbook.write(output);
|
||
}
|
||
} finally {
|
||
workbook.dispose();
|
||
workbook.close();
|
||
}
|
||
} 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/download/" + filename);
|
||
record.setDownloadUrl(record.getUrl());
|
||
fileService.save(record);
|
||
return new ApprovalExportResult(record, rowCount);
|
||
}
|
||
|
||
private void fillSummarySheet(SXSSFWorkbook workbook, List<ApprovalExportRow> rows) {
|
||
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<>();
|
||
// 批量取实例,避免逐行往返(5000 行导出时 N+1 会拖到分钟级)
|
||
Map<Long, ApprovalInstance> instances = new HashMap<>();
|
||
List<Long> instanceIds = new ArrayList<>();
|
||
for (ApprovalExportRow row : rows) {
|
||
instanceIds.add(row.getId());
|
||
}
|
||
if (!instanceIds.isEmpty()) {
|
||
for (ApprovalInstance instance : instanceMapper.selectBatchIds(instanceIds)) {
|
||
instances.put(instance.getId(), instance);
|
||
}
|
||
}
|
||
Map<String, int[]> lineCountCache = new HashMap<>();
|
||
Map<String, BigDecimal> amountCache = new HashMap<>();
|
||
Map<String, String> progressCache = new HashMap<>();
|
||
int rowNum = 1;
|
||
for (ApprovalExportRow row : rows) {
|
||
ApprovalInstance instance = instances.get(row.getId());
|
||
ApprovalDocumentType type = instance == null ? null
|
||
: documentRegistry.find(instance.getFlowCode());
|
||
String cacheKey = type == null ? "" : type.flowCode() + ":" + row.getId();
|
||
int lineCount = type == null ? 0
|
||
: lineCountCache.computeIfAbsent(cacheKey, k -> new int[]{documentRegistry.lineCount(type, instance)})[0];
|
||
BigDecimal amountTotal = type == null ? null : amountCache.computeIfAbsent(cacheKey,
|
||
k -> documentRegistry.lineAmountTotal(type, instance));
|
||
String progress = instance == null ? "" : progressCache.computeIfAbsent(cacheKey,
|
||
k -> countersignProgress(instance));
|
||
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||
String[] values = toValues(row, nodeCache, lineCount, amountTotal, progress);
|
||
for (int i = 0; i < values.length; i++) {
|
||
dataRow.createCell(i).setCellValue(values[i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void fillLineSheet(SXSSFWorkbook workbook, ApprovalDocumentType type,
|
||
List<ApprovalLineSpec> columns, List<ExportLine> lines) {
|
||
SXSSFSheet sheet = workbook.createSheet(type.flowName());
|
||
List<String> headers = new ArrayList<>();
|
||
headers.add("单号");
|
||
headers.add("状态");
|
||
headers.add("报送部门");
|
||
headers.add("报送人");
|
||
for (ApprovalLineSpec column : columns) {
|
||
headers.add(column.getLabel());
|
||
}
|
||
for (int i = 0; i < headers.size(); i++) {
|
||
sheet.setColumnWidth(i, 18 * 256);
|
||
}
|
||
SXSSFRow header = sheet.createRow(0);
|
||
for (int i = 0; i < headers.size(); i++) {
|
||
header.createCell(i).setCellValue(headers.get(i));
|
||
}
|
||
int rowNum = 1;
|
||
for (ExportLine exportLine : lines) {
|
||
SXSSFRow dataRow = sheet.createRow(rowNum++);
|
||
int col = 0;
|
||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getBizNo()));
|
||
dataRow.createCell(col++).setCellValue(text(ApprovalConstants.statusName(exportLine.instance.getStatus())));
|
||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getReportOrgName()));
|
||
dataRow.createCell(col++).setCellValue(text(exportLine.instance.getReporterName()));
|
||
for (ApprovalLineSpec column : columns) {
|
||
dataRow.createCell(col++).setCellValue(lineValue(exportLine.line.get(column.getKey())));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 会签进度列:仅会签节点有组时填「已签 N/M」,其余为空。
|
||
*/
|
||
private String countersignProgress(ApprovalInstance instance) {
|
||
List<ApprovalNode> nodes = flowService.parseNodes(instance.getDefinitionId());
|
||
List<String> parts = new ArrayList<>();
|
||
for (ApprovalNode node : nodes) {
|
||
if (node.getGroups() == null || node.getGroups().isEmpty()) {
|
||
continue;
|
||
}
|
||
// 用详情装配同一套进度,避免导出与详情口径分叉
|
||
com.gxwebsoft.approval.vo.ApprovalDetail detail = new com.gxwebsoft.approval.vo.ApprovalDetail();
|
||
documentService.fillGroupProgress(instance, detail);
|
||
List<com.gxwebsoft.approval.vo.ApprovalGroupProgressVO> progress = detail.getGroupProgress();
|
||
int signedGroups = 0;
|
||
int totalGroups = 0;
|
||
for (com.gxwebsoft.approval.vo.ApprovalGroupProgressVO group : progress) {
|
||
if (!node.getKey().equals(group.getNodeKey())) {
|
||
continue;
|
||
}
|
||
totalGroups++;
|
||
if (group.getSigned() >= group.getTotal() && group.getTotal() > 0) {
|
||
signedGroups++;
|
||
}
|
||
}
|
||
if (totalGroups > 0) {
|
||
parts.add(node.getName() + " 已签 " + signedGroups + "/" + totalGroups);
|
||
}
|
||
}
|
||
return String.join(";", parts);
|
||
}
|
||
|
||
private String[] toValues(ApprovalExportRow row, Map<Long, List<ApprovalNode>> nodeCache,
|
||
int lineCount, BigDecimal amountTotal, String progress) {
|
||
boolean completion = ApprovalConstants.DOC_COMPLETION.equals(row.getDocType());
|
||
String flowName = flowService.flowName(row.getDefinitionId(), row.getFlowCode());
|
||
return new String[]{
|
||
text(row.getBizNo()),
|
||
text(flowName == null ? ApprovalConstants.docTypeName(row.getDocType()) : flowName),
|
||
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()),
|
||
lineCount > 0 ? String.valueOf(lineCount) : "",
|
||
amountTotal == null ? "" : fee(amountTotal),
|
||
text(progress)
|
||
};
|
||
}
|
||
|
||
private String lineValue(Object value) {
|
||
if (value == null) {
|
||
return "";
|
||
}
|
||
if (value instanceof BigDecimal) {
|
||
return ((BigDecimal) value).stripTrailingZeros().toPlainString();
|
||
}
|
||
if (value instanceof LocalDateTime) {
|
||
return TIME_FORMAT.format((LocalDateTime) value);
|
||
}
|
||
if (value instanceof java.time.LocalDate) {
|
||
return value.toString();
|
||
}
|
||
if (value instanceof Boolean) {
|
||
return (Boolean) value ? "是" : "否";
|
||
}
|
||
return String.valueOf(value);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
private boolean hasAuthority(User user, String authority) {
|
||
if (user.getAuthorities() == null) {
|
||
return false;
|
||
}
|
||
return user.getAuthorities().stream()
|
||
.map(Menu::getAuthority)
|
||
.anyMatch(authority::equals);
|
||
}
|
||
|
||
/** 工作簿填充回调。 */
|
||
private interface WorkbookFiller {
|
||
void fill(SXSSFWorkbook workbook);
|
||
}
|
||
|
||
@Resource
|
||
private ApprovalDocumentService documentService;
|
||
}
|