diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java b/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java new file mode 100644 index 00000000..7b038ad0 --- /dev/null +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportCellReadException.java @@ -0,0 +1,93 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.common.excel; + +import org.springblade.core.log.exception.ServiceException; + +import java.io.Serial; + +/** + * 单元格读取失败异常 —— 用于「异常不是 fastexcel 抛出、但同样发生在读取阶段」的场景。 + *

+ * fastexcel 自身解析失败时抛 {@code ExcelDataConvertException},位置信息齐全, + * {@link ImportFailureExcelUtil#exportReadFailure} 可以直接定位到行与列。但有些模块 + * 在读取阶段有自己的解析逻辑(例如运输对账的动态费用列:模板列不固定,由 + * {@code VehicleReconciliationFeeReader} 自行按表头解析),这些代码如果只是 + * {@code catch (NumberFormatException)} 后返回 {@code BigDecimal.ZERO}, + * 脏数据就会静默入库(费用 0.00 还会参与结算金额与自动匹配)。 + *

+ * 正确做法是改抛本异常,把位置信息补齐,让导出工具能把原因挂到正确的行、标红正确的列。 + *

+ * 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。 + * + * @author Chill + */ +public class ImportCellReadException extends ServiceException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 出错的单元格所在的 Excel 行号,绝对 0 基(表头占 0,第一个数据行为 1)。 + * 与 {@code ExcelDataConvertException.getRowIndex()} 保持同一口径, + * 便于导出工具统一换算。 + */ + private final transient Integer rowIndex; + + /** + * 出错的列名(导入模板表头原文),会写进原因文案里,导出工具靠它标红对应列。 + */ + private final transient String columnName; + + public ImportCellReadException(Integer rowIndex, String columnName, String cellValue) { + super(buildMessage(rowIndex, columnName, cellValue)); + this.rowIndex = rowIndex; + this.columnName = columnName; + } + + /** + * 文案格式与 {@link ImportFailureExcelUtil#describeReadFailure} 保持一致: + * 「第N行「列名」格式不正确:<原值>」。列名必须包在「」里,导出工具才能标红该列。 + */ + private static String buildMessage(Integer rowIndex, String columnName, String cellValue) { + String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行"; + String columnText = columnName == null || columnName.isBlank() ? "该列" : "「" + columnName + "」"; + StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确"); + if (cellValue != null && !cellValue.isBlank()) { + message.append(":").append(cellValue); + } + return message.toString(); + } + + public Integer getRowIndex() { + return rowIndex; + } + + public String getColumnName() { + return columnName; + } + +} diff --git a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java index 6b0ad40d..0fc7d2f1 100644 --- a/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureExcelUtil.java @@ -45,7 +45,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -80,6 +82,443 @@ public class ImportFailureExcelUtil { addValidationError(validationErrors, value != null && value.length() > maxLength, message); } + /** + * 把「读取 Excel 阶段」的转换异常转成可读的失败原因。 + *

+ * 背景:单元格里填了无法转换的内容(如金额列填「一万二千元」)时,EasyExcel 会在 + * {@code ExcelUtil.read(...)} 阶段就抛 {@link cn.idev.excel.exception.ExcelDataConvertException}, + * 根本走不到行级校验;若不兜底,用户看到的是 + * 「Convert data ... to class java.math.BigDecimal error」这类技术文案。 + *

+ * 本方法据异常里的行列位置与目标类型,生成「第N行「列名」格式不正确:<原值>」这样 + * 用户能看懂、能定位的文案;列名靠表头反射拿 {@code @ExcelProperty},拿不到则退回列序号。 + * + * @param exception 读取阶段抛出的异常 + * @param excelClass 导入模型类(用于按列序号反查表头名) + * @return 可读的失败原因;非转换类异常返回 null(交由上层按原样处理) + */ + public static String describeReadFailure(Throwable exception, Class excelClass) { + cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, + cn.idev.excel.exception.ExcelDataConvertException.class); + if (convertException == null) { + // 不是 fastexcel 抛的转换异常:模块自带的读取期解析逻辑(如运输对账的动态费用列) + // 用 ImportCellReadException 补齐了位置信息,直接透传它已经拼好的原因文案。 + ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class); + return cellReadException == null ? null : cellReadException.getMessage(); + } + Integer rowIndex = convertException.getRowIndex(); + Integer columnIndex = convertException.getColumnIndex(); + String columnName = columnName(excelClass, columnIndex); + // Excel 行号:getRowIndex() 是绝对 0 基行号(表头占 0),故 Excel 行号 = rowIndex + 1 + String rowText = rowIndex == null ? "第?行" : "第" + (rowIndex + 1) + "行"; + String columnText = columnName == null ? ("第" + (columnIndex == null ? "?" : columnIndex + 1) + "列") : "「" + columnName + "」"; + String value = cellValue(convertException); + StringBuilder message = new StringBuilder(rowText).append(columnText).append("格式不正确"); + if (value != null && !value.isBlank()) { + message.append(":").append(value); + } + return message.toString(); + } + + /** + * 在异常链里查找指定类型的 cause + */ + private static T findCause(Throwable throwable, Class type) { + for (Throwable current = throwable; current != null; current = current.getCause()) { + if (type.isInstance(current)) { + return type.cast(current); + } + if (current.getCause() == current) { + break; + } + } + return null; + } + + /** + * 按列序号反查表头名(@ExcelProperty 的 value,取不到则用字段名) + */ + private static String columnName(Class excelClass, Integer columnIndex) { + if (excelClass == null || columnIndex == null) { + return null; + } + List fields = excelFields(excelClass); + if (columnIndex < 0 || columnIndex >= fields.size()) { + return null; + } + Field field = fields.get(columnIndex); + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty == null) { + return field.getName(); + } + String[] value = excelProperty.value(); + String columnName = value.length == 0 ? field.getName() : value[0]; + return columnName.replace("*", ""); + } + + /** + * 取触发转换失败的单元格原值 + */ + private static String cellValue(cn.idev.excel.exception.ExcelDataConvertException exception) { + try { + cn.idev.excel.metadata.data.CellData cellData = exception.getCellData(); + if (cellData == null) { + return null; + } + Object data = cellData.getData(); + if (data == null) { + data = cellData.getStringValue(); + } + return data == null ? null : String.valueOf(data).trim(); + } catch (Exception ignored) { + return null; + } + } + + /** + * 以「原始单元格文本」方式重读导入文件,得到的行用于构造失败明细。 + *

+ * 优先使用 Excel 文件真实表头(含动态费用列等模型类未声明的列),保证失败明细 + * 与原导入模板列一致,用户修正后可直接用该文件重新导入。 + *

+ * 用途:当 {@code ExcelUtil.read(file, XxxExcel.class)} 因单元格无法转换而失败时, + * 已解析的实体列表是不完整的(异常中断了整批读取)。此时用本方法重读一次: + * 不做类型转换(直接读成 Map),因此绝不会再抛转换异常,可拿到**原表全部行**, + * 供导出「原表全量 + 仅出错行带原因」的失败明细。 + * + * @param file 上传的 Excel 文件 + * @param excelClass 导入模型类(表头缺失时按列序号反查表头名) + * @return 按行组织的明细数据:每行是「表头名 → 单元格文本」的有序 Map; + * 读不出内容时返回空列表(调用方按空明细处理) + */ + public static List> readRawRows(java.io.InputStream file, Class excelClass) { + List> rawRows = new ArrayList<>(); + Map headerByIndex = new LinkedHashMap<>(); + try { + cn.idev.excel.read.builder.ExcelReaderBuilder builder = cn.idev.excel.FastExcel + .read(file, new cn.idev.excel.event.AnalysisEventListener>() { + @Override + public void invokeHeadMap(Map headMap, cn.idev.excel.context.AnalysisContext context) { + if (headMap == null) { + return; + } + headMap.forEach((index, header) -> { + if (index == null) { + return; + } + String name = header == null ? "" : header.trim(); + // 历史失败明细再导入时忽略旧的失败原因列,导出时重新追加 + if (!name.isEmpty() && !FAILURE_REASON.equals(name)) { + headerByIndex.put(index, name); + } + }); + } + + @Override + public void invoke(Map data, cn.idev.excel.context.AnalysisContext context) { + rawRows.add(data == null ? new LinkedHashMap<>() : new LinkedHashMap<>(data)); + } + + @Override + public void doAfterAllAnalysed(cn.idev.excel.context.AnalysisContext context) { + // 无需处理 + } + }); + builder.headRowNumber(1).sheet().doRead(); + } catch (Exception ignored) { + // 重读本身失败时退回空明细,由调用方按「无法读取」提示 + return new ArrayList<>(); + } + List> rows = new ArrayList<>(); + for (Map rawRow : rawRows) { + Map row = new LinkedHashMap<>(); + Set columnIndexes = new LinkedHashSet<>(headerByIndex.keySet()); + if (rawRow != null) { + columnIndexes.addAll(rawRow.keySet()); + } + List sortedIndexes = new ArrayList<>(columnIndexes); + Collections.sort(sortedIndexes); + for (Integer columnIndex : sortedIndexes) { + if (columnIndex == null) { + continue; + } + String columnName = headerByIndex.get(columnIndex); + if (columnName == null || columnName.isBlank()) { + columnName = columnName(excelClass, columnIndex); + } + if (columnName == null || columnName.isBlank()) { + columnName = "第" + (columnIndex + 1) + "列"; + } + if (FAILURE_REASON.equals(columnName)) { + continue; + } + row.put(columnName, rawRow == null ? null : rawRow.get(columnIndex)); + } + rows.add(row); + } + return rows; + } + + /** + * 按原上传文件真实表头导出失败明细(含动态列),并追加「导入失败原因」。 + *

+ * 适用于运输对账等「模板列 = 固定列 + 动态费用列」场景:按实体字段反射导出 + * 会丢掉动态列,用户无法用失败文件修正后重新导入。本方法重读原文件全量列, + * 再按 failureList 行序挂上失败原因。 + * + * @param response 响应 + * @param fileName 文件名 + * @param sheetName 工作表名 + * @param file 原导入文件 + * @param failureList 校验失败列表(与原表行序一致,成功行 errorMessage 可为空) + * @param excelClass 导入模型类(原文件重读失败时回退到按实体导出) + */ + public static void exportOriginalWithFailures(HttpServletResponse response, String fileName, String sheetName, + org.springframework.web.multipart.MultipartFile file, + List failureList, Class excelClass) { + List> rawRows = new ArrayList<>(); + if (file != null && !file.isEmpty()) { + try (java.io.InputStream inputStream = file.getInputStream()) { + rawRows = readRawRows(inputStream, excelClass); + } catch (IOException ignored) { + rawRows = new ArrayList<>(); + } + } + if (rawRows.isEmpty()) { + // 原文件读不出时回退实体导出,至少保证失败原因可下载 + if (failureList != null && !failureList.isEmpty()) { + export(response, fileName, sheetName, failureList, excelClass); + } + return; + } + Map failureMessageByRow = new HashMap<>(); + if (failureList != null) { + for (int index = 0; index < failureList.size(); index++) { + Object message = errorMessage(failureList.get(index)); + if (message != null && !String.valueOf(message).isBlank()) { + failureMessageByRow.put(index, String.valueOf(message)); + } + } + } + exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow); + } + + /** + * 导出「读取阶段失败」明细:原表全量行 + 末尾「导入失败原因」列。 + *

+ * 与 {@link #export} 的区别:本方法直接按重读得到的「表头名 → 单元格文本」写出, + * 不依赖导入模型的字段反射,因此适用于「单元格无法转换为实体字段」的场景 + * (此时实体列表不完整,无法走常规导出)。 + * + * @param response 响应 + * @param fileName 文件名 + * @param sheetName 工作表名 + * @param rawRows {@link #readRawRows} 得到的原表全部行 + * @param failureMessageByRow 行号(0 基,对应 rawRows 下标)→ 该行失败原因;为 null 表示该行无错 + */ + public static void exportRawFailure(HttpServletResponse response, String fileName, String sheetName, + List> rawRows, Map failureMessageByRow) { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + response.setHeader("Content-disposition", "attachment;filename=" + encodeFileName + ".xlsx"); + List headers = new ArrayList<>(); + for (Map row : rawRows) { + for (String key : row.keySet()) { + if (!headers.contains(key)) { + headers.add(key); + } + } + } + headers.add(FAILURE_REASON); + List> head = new ArrayList<>(); + for (String header : headers) { + head.add(List.of(header)); + } + List> rows = new ArrayList<>(); + Map> redColumnsByRow = new HashMap<>(); + List> headerKeywords = new ArrayList<>(); + for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) { + headerKeywords.add(headerKeywords(headers.get(columnIndex))); + } + for (int rowIndex = 0; rowIndex < rawRows.size(); rowIndex++) { + Map rawRow = rawRows.get(rowIndex); + List row = new ArrayList<>(); + String failureMessage = failureMessageByRow == null ? null : failureMessageByRow.get(rowIndex); + for (int columnIndex = 0; columnIndex < headers.size() - 1; columnIndex++) { + row.add(rawRow.get(headers.get(columnIndex))); + } + row.add(failureMessage == null ? "" : failureMessage); + rows.add(row); + redColumnsByRow.put(rowIndex, resolveRawRedColumns(failureMessage, headerKeywords, headers.size() - 1)); + } + try { + FastExcel.write(response.getOutputStream()) + .registerWriteHandler(new RawFailureCellStyleHandler(rows, redColumnsByRow)) + .head(head) + .sheet(sheetName) + .doWrite(rows); + } catch (IOException exception) { + throw new IllegalStateException("导出导入失败明细失败", exception); + } + } + + /** + * 读取阶段失败明细的单元格样式处理器:统一列宽 + 标红失败原因列与出错列 + */ + private static class RawFailureCellStyleHandler implements CellWriteHandler { + + private final int failureReasonColumnIndex; + private final Map> redColumnsByRow; + private final Map redStyleCache = new HashMap<>(); + private final Map noWrapStyleCache = new HashMap<>(); + private final Map columnWidthCache = new HashMap<>(); + + private RawFailureCellStyleHandler(List> rows, Map> redColumnsByRow) { + this.failureReasonColumnIndex = rows.isEmpty() ? 0 : rows.get(0).size() - 1; + this.redColumnsByRow = redColumnsByRow; + } + + @Override + public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, + List> cellDataList, Cell cell, cn.idev.excel.metadata.Head head, + Integer relativeRowIndex, Boolean isHead) { + if (Boolean.TRUE.equals(isHead)) { + adjustColumnWidth(cell); + markNoWrap(cell); + return; + } + adjustColumnWidth(cell); + if (relativeRowIndex == null || relativeRowIndex < 0) { + return; + } + Set redColumns = redColumnsByRow.get(relativeRowIndex); + if (redColumns != null && redColumns.contains(cell.getColumnIndex())) { + markRed(cell); + } + } + + private void markRed(Cell cell) { + CellStyle currentStyle = cell.getCellStyle(); + CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { + Workbook workbook = cell.getSheet().getWorkbook(); + CellStyle newStyle = workbook.createCellStyle(); + newStyle.cloneStyleFrom(currentStyle); + Font font = workbook.createFont(); + font.setColor(IndexedColors.RED.getIndex()); + newStyle.setFont(font); + newStyle.setWrapText(true); + return newStyle; + }); + cell.setCellStyle(redStyle); + } + + private void markNoWrap(Cell cell) { + CellStyle currentStyle = cell.getCellStyle(); + CellStyle noWrapStyle = noWrapStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { + CellStyle newStyle = cell.getSheet().getWorkbook().createCellStyle(); + newStyle.cloneStyleFrom(currentStyle); + newStyle.setWrapText(false); + return newStyle; + }); + cell.setCellStyle(noWrapStyle); + } + + private void adjustColumnWidth(Cell cell) { + int columnIndex = cell.getColumnIndex(); + int columnWidth = Math.min(Math.max(displayWidth(cell.toString()) + 4, 12), 80) * 256; + Integer currentWidth = columnWidthCache.get(columnIndex); + if (currentWidth == null || columnWidth > currentWidth) { + columnWidthCache.put(columnIndex, columnWidth); + cell.getSheet().setColumnWidth(columnIndex, columnWidth); + } + } + + private int displayWidth(String value) { + int width = 0; + for (int index = 0; index < value.length(); index++) { + width += value.charAt(index) > 255 ? 2 : 1; + } + return width; + } + } + + /** + * 读取阶段异常的一站式兜底:重读原表 → 导出「全量行 + 出错行原因」失败明细。 + *

+ * 控制器用法:在导入的 try 块末尾追加 + *

+	 * } catch (Exception exception) {
+	 *     if (ImportFailureExcelUtil.exportReadFailure(response, fileName, sheetName, file, XxxExcel.class, exception)) {
+	 *         return null;
+	 *     }
+	 *     throw exception;
+	 * }
+	 * 
+ * + * @return true 表示已识别为「单元格转换失败」并已写出明细(调用方直接 return null); + * false 表示不是转换类异常(调用方应继续抛出,保持原有处理) + */ + public static boolean exportReadFailure(HttpServletResponse response, String fileName, String sheetName, + org.springframework.web.multipart.MultipartFile file, + Class excelClass, Throwable exception) { + String failureMessage = describeReadFailure(exception, excelClass); + if (failureMessage == null) { + return false; + } + List> rawRows; + try (java.io.InputStream inputStream = file.getInputStream()) { + rawRows = readRawRows(inputStream, excelClass); + } catch (IOException ioException) { + rawRows = new ArrayList<>(); + } + Map failureMessageByRow = new HashMap<>(); + if (rawRows.isEmpty()) { + // 重读也拿不到内容时,退化为「仅失败原因」一行,至少让用户看到可读原因 + Map single = new java.util.LinkedHashMap<>(); + single.put(FAILURE_REASON, failureMessage); + rawRows.add(single); + failureMessageByRow.put(0, failureMessage); + exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow); + return true; + } + // 异常里的数据行下标(0 基)用于把原因挂到对应行 + Integer rowIndex = readFailureRowIndex(exception); + int targetRow = rowIndex == null ? 0 : Math.max(0, Math.min(rowIndex, rawRows.size() - 1)); + failureMessageByRow.put(targetRow, failureMessage); + exportRawFailure(response, fileName, sheetName, rawRows, failureMessageByRow); + return true; + } + + /** + * 取读取阶段异常中「失败数据行」的下标(0 基,只对数据行计数),供挂失败原因使用。 + *

+ * 无论哪种异常,源信息都是绝对 0 基行号(表头占 0,第一个数据行为 1), + * 而这里要的下标是相对数据行的,故统一减 1。 + * 不减的话原因会整体下移一行:错误恰好落在最后一行时会因调用处的 clamping + * 侥幸正确,落在首行/中间行就会标到不该标的行上,误导用户去改正确的那一行。 + */ + private static Integer readFailureRowIndex(Throwable exception) { + Integer excelRowIndex = readFailureExcelRowIndex(exception); + return excelRowIndex == null ? null : Math.max(0, excelRowIndex - 1); + } + + /** + * 取读取阶段异常的绝对 0 基行号(表头 = 0)。 + *

+ * 两条来源统一到这里:fastexcel 自带的 {@code ExcelDataConvertException}, + * 以及模块自行解析时抛出的 {@link ImportCellReadException}。 + */ + private static Integer readFailureExcelRowIndex(Throwable exception) { + cn.idev.excel.exception.ExcelDataConvertException convertException = findCause(exception, + cn.idev.excel.exception.ExcelDataConvertException.class); + if (convertException != null) { + return convertException.getRowIndex(); + } + ImportCellReadException cellReadException = findCause(exception, ImportCellReadException.class); + return cellReadException == null ? null : cellReadException.getRowIndex(); + } + private static final String FAILURE_REASON = "导入失败原因"; private static final String ERROR_MESSAGE_FIELD = "errorMessage"; private static final String FAILURE_REASON_FIELD = "failureReason"; @@ -247,6 +686,69 @@ public class ImportFailureExcelUtil { .toList(); } + /** + * 按真实表头生成定位关键词(用于原表失败明细标红) + */ + private static List headerKeywords(String header) { + if (header == null || header.isBlank()) { + return List.of(); + } + List keywords = new ArrayList<>(); + keywords.add(header); + keywords.addAll(Arrays.asList(header.replace("*", "").split("[//、()()\\s]+"))); + return keywords.stream() + .map(ImportFailureExcelUtil::normalize) + .filter(keyword -> keyword.length() >= 2) + .distinct() + .toList(); + } + + /** + * 按失败原因命中表头关键词,计算需要标红的列(含失败原因列) + */ + private static Set resolveRawRedColumns(String failureMessage, List> headerKeywords, + int failureReasonColumnIndex) { + Set redColumns = new LinkedHashSet<>(); + redColumns.add(failureReasonColumnIndex); + String normalizedReason = normalize(failureMessage); + if (normalizedReason.isEmpty()) { + return redColumns; + } + List matches = new ArrayList<>(); + for (int column = 0; column < headerKeywords.size(); column++) { + for (String keyword : headerKeywords.get(column)) { + int fromIndex = 0; + while (true) { + int index = normalizedReason.indexOf(keyword, fromIndex); + if (index < 0) { + break; + } + matches.add(new int[]{index, index + keyword.length(), column}); + fromIndex = index + 1; + } + } + } + for (int[] match : matches) { + if (!isRawMatchSubsumed(match, matches)) { + redColumns.add(match[2]); + } + } + return redColumns; + } + + private static boolean isRawMatchSubsumed(int[] match, List matches) { + int matchLength = match[1] - match[0]; + for (int[] other : matches) { + if (other[2] == match[2]) { + continue; + } + if ((other[1] - other[0]) > matchLength && other[0] <= match[0] && other[1] >= match[1]) { + return true; + } + } + return false; + } + private static class ImportFailureCellStyleHandler implements CellWriteHandler { private final int failureReasonColumnIndex; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java similarity index 72% rename from blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java rename to blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java index 0277ec01..6065ce0f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java +++ b/blade-common/src/main/java/org/springblade/common/excel/ImportFailureException.java @@ -23,7 +23,7 @@ *

* Author: Chill Zhuang (bladejava@qq.com) */ -package org.springblade.system.excel; +package org.springblade.common.excel; import org.springblade.core.log.exception.ServiceException; @@ -33,8 +33,14 @@ import java.util.List; /** * 导入失败异常,携带失败明细用于导出原表并标注错误。 *

- * 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚, - * 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。 + * 批量导入采用「任一行失败即整批回滚」语义:循环内先收集完全部错误明细, + * 再抛出本异常触发事务回滚。明细在抛异常前已收集完毕,因此回滚不影响明细内容。 + *

+ * 注意:仅标注 {@code @Transactional} 并不足够。若循环内把异常吞掉(catch 后 + * 只往 errorList 塞数据并正常 return),方法会正常返回,Spring 会照常提交事务。 + * 必须抛出异常才能让事务真正回滚。 + *

+ * 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。 * * @author Chill */ diff --git a/blade-gateway/src/main/resources/bootstrap.yml b/blade-gateway/src/main/resources/bootstrap.yml index 0f5092a9..e4fa3a81 100644 --- a/blade-gateway/src/main/resources/bootstrap.yml +++ b/blade-gateway/src/main/resources/bootstrap.yml @@ -2,6 +2,9 @@ server: port: 80 spring: + codec: + # 运单批量导入等大 JSON / 失败明细导出场景,默认 256KB 会触发网关 500 + max-in-memory-size: 20MB cloud: gateway: server: diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java index ecbdf0c6..4cf979bb 100644 --- a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/feign/IBusinessProcessClient.java @@ -7,6 +7,7 @@ import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO; import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO; import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springblade.process.pojo.vo.MkFormBindVO; import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; import org.springblade.process.pojo.vo.ProcessTodoVO; import org.springframework.cloud.openfeign.FeignClient; @@ -40,6 +41,8 @@ public interface IBusinessProcessClient { String QUERY_APPROVED_RECORD_LIST = API_PREFIX + "/queryApprovedRecordsNoAttachments"; String GET_CURRENT_NODES = API_PREFIX + "/getCurrentNodes"; String GET_PROCESS_INFO = API_PREFIX + "/getProcessInfo"; + String RESOLVE_BY_FORM_INSTANCE_ID = API_PREFIX + "/resolveByFormInstanceId"; + String MARK_PROCESS_FINISHED = API_PREFIX + "/markProcessFinished"; /** * 提交业务流程 @@ -126,4 +129,24 @@ public interface IBusinessProcessClient { @GetMapping(GET_PROCESS_INFO) FR getProcessInfo(@RequestParam("processInstanceId") String processInstanceId, @RequestParam(value = "loginName", required = false) String loginName); + + /** + * 按表单实例id解析业务绑定 + * + * @param formInstanceId 表单实例id + * @return 绑定信息(含业务主键) + */ + @GetMapping(RESOLVE_BY_FORM_INSTANCE_ID) + FR resolveByFormInstanceId(@RequestParam("formInstanceId") Long formInstanceId); + + /** + * 公开页流程结束时标记业务流程通过/驳回并完结 + * + * @param processInstanceId 流程实例id + * @param approved true=通过 false=驳回 + * @return 是否更新成功 + */ + @PostMapping(MARK_PROCESS_FINISHED) + FR markProcessFinished(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam("approved") boolean approved); } diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java index bac2329d..45329958 100644 --- a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/BusinessProcess.java @@ -65,6 +65,12 @@ public class BusinessProcess implements Serializable { */ @Schema(description = "业务id") private Long bizId; + /** + * MK表单实例id + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "MK表单实例id") + private Long formInstanceId; /** * 流程实例id */ diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/MkFormBind.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/MkFormBind.java new file mode 100644 index 00000000..03499c83 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/entity/MkFormBind.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.pojo.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * MK表单实例绑定表 实体类 + * + * @author BladeX + */ +@Data +@TableName("blade_mk_form_bind") +@Schema(description = "MkFormBind对象") +public class MkFormBind implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + + @Schema(description = "租户ID") + private String tenantId; + + @Schema(description = "业务类型") + private String bizType; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "业务主键") + private Long bizId; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "MK表单实例id") + private Long formInstanceId; + + @Schema(description = "MK流程实例id") + private String processInstanceId; + + @Schema(description = "是否当前生效(1是0否)") + private Integer isCurrent; + + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "创建时间", hidden = true) + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "更新时间", hidden = true) + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} diff --git a/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/MkFormBindVO.java b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/MkFormBindVO.java new file mode 100644 index 00000000..a1623972 --- /dev/null +++ b/blade-service-api/blade-process-api/src/main/java/org/springblade/process/pojo/vo/MkFormBindVO.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.pojo.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * MK表单实例绑定 VO + * + * @author BladeX + */ +@Data +@Schema(description = "MkFormBindVO") +public class MkFormBindVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "业务主键") + private Long bizId; + + @Schema(description = "业务类型") + private String bizType; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "MK表单实例id") + private Long formInstanceId; + + @Schema(description = "MK流程实例id") + private String processInstanceId; +} diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java index 932e41a7..18338ed0 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java @@ -138,6 +138,12 @@ public class Dept extends TenantEntity { @Schema(description = "部门编码") private String deptCode; + /** + * 金蝶部门代码 + */ + @Schema(description = "金蝶部门代码") + private String kingdeeDeptCode; + /** * 组织简称 */ diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/AirportMasterVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/AirportMasterVO.java index 28c7da60..f445c719 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/AirportMasterVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/AirportMasterVO.java @@ -52,6 +52,13 @@ public class AirportMasterVO extends AirportMaster { @Schema(description = "导入错误信息") private String errorMessage; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/FeeItemVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/FeeItemVO.java index b45d4580..7b77ee32 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/FeeItemVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/FeeItemVO.java @@ -49,6 +49,10 @@ public class FeeItemVO extends FeeItem { @Schema(description = "组织") private String createDeptName; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人") private String updateUserName; diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/PortTerminalVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/PortTerminalVO.java index 78474b07..b08a4c8a 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/PortTerminalVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/PortTerminalVO.java @@ -52,6 +52,13 @@ public class PortTerminalVO extends PortTerminal { @Schema(description = "导入错误信息") private String errorMessage; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/RailwayStationVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/RailwayStationVO.java index c28a15fc..17a50ba7 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/RailwayStationVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/RailwayStationVO.java @@ -52,6 +52,13 @@ public class RailwayStationVO extends RailwayStation { @Schema(description = "导入错误信息") private String errorMessage; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java index c346165c..e445395e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillImportBatchRequest.java @@ -33,4 +33,6 @@ public class WaybillImportBatchRequest { private String planName; private String remark; private List> rows; + /** 分批确认导入时,是否追加到已有批次(首批为 false,后续批为 true)。 */ + private Boolean append; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java index 6393fc0a..e037a882 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java @@ -56,7 +56,7 @@ public class LoadingManage extends TenantEntity { @Schema(description = "货物名称") private String cargoName; - @Schema(description = "车/船/航班/班列") + @Schema(description = "车牌号/船班号/船次号/班次号") private String vehicleNo; @Schema(description = "挂车车牌号") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index 81c481d9..dec1a0b5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -162,7 +162,7 @@ public class Waybill extends TenantEntity { @TableField(updateStrategy = FieldStrategy.ALWAYS) private String driverRejectReason; - @Schema(description = "车/船/航班/班列号") + @Schema(description = "车牌号/船班号/船次号/班次号") private String vehicleNo; @Schema(description = "船长") @@ -234,6 +234,9 @@ public class Waybill extends TenantEntity { @Schema(description = "配载单号") private String loadingNo; + @Schema(description = "配载标识号") + private String loadingIdentifier; + @Schema(description = "运单批次号") private String batchNo; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AccidentRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AccidentRecordVO.java index e06dc084..427bf362 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AccidentRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AccidentRecordVO.java @@ -80,6 +80,13 @@ public class AccidentRecordVO extends AccidentRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java index b76896a3..24606c29 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AnnualInspectionRecordVO.java @@ -68,6 +68,10 @@ public class AnnualInspectionRecordVO extends AnnualInspectionRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java index 802ed9ed..5e924aec 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java @@ -58,6 +58,10 @@ public class CustomerArchiveVO extends CustomerArchive { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "联系人") private List contacts = new ArrayList<>(); diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVO.java index 856575c6..5b22c7ac 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverVO.java @@ -69,4 +69,8 @@ public class DriverVO extends Driver { @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate warningDate; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EquipmentLedgerVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EquipmentLedgerVO.java index 6df81ad4..72def95e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EquipmentLedgerVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EquipmentLedgerVO.java @@ -25,5 +25,8 @@ public class EquipmentLedgerVO extends EquipmentLedger { @Serial private static final long serialVersionUID = 1L; @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) private String updateUserName; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EtcRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EtcRecordVO.java index cef2656d..9c636dcd 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EtcRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/EtcRecordVO.java @@ -26,6 +26,10 @@ public class EtcRecordVO extends EtcRecord { @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceRecordVO.java index 3af9b3b0..0aeeb030 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InsuranceRecordVO.java @@ -63,6 +63,13 @@ public class InsuranceRecordVO extends InsuranceRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenancePlanVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenancePlanVO.java index bb4d225d..35306e67 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenancePlanVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenancePlanVO.java @@ -62,6 +62,13 @@ public class MaintenancePlanVO extends MaintenancePlan { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenanceRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenanceRecordVO.java index 260f0946..bfacf9f3 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenanceRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MaintenanceRecordVO.java @@ -62,6 +62,13 @@ public class MaintenanceRecordVO extends MaintenanceRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java index 490607ab..8ef58199 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java @@ -41,6 +41,11 @@ public class MasterOrderVO extends MasterOrder { private List boundWaybills; @TableField(exist = false) private BigDecimal totalQuantity; + /** + * 各品类已调度量(按货物名称+类型聚合,取各分段已调度量的最大值) + */ + @TableField(exist = false) + private Map dispatchedGoods; @TableField(exist = false) private String createUserName; @TableField(exist = false) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MileageRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MileageRecordVO.java index 4c39754a..963e809c 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MileageRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MileageRecordVO.java @@ -66,6 +66,10 @@ public class MileageRecordVO extends MileageRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OilElectricRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OilElectricRecordVO.java index 94b35024..d3a08860 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OilElectricRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OilElectricRecordVO.java @@ -76,6 +76,10 @@ public class OilElectricRecordVO extends OilElectricRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OtherExpenseRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OtherExpenseRecordVO.java index 0c6db1e5..1639fa43 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OtherExpenseRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/OtherExpenseRecordVO.java @@ -49,6 +49,10 @@ public class OtherExpenseRecordVO extends OtherExpenseRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TireReplacementRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TireReplacementRecordVO.java index f9140956..eaf4b1c4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TireReplacementRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TireReplacementRecordVO.java @@ -63,6 +63,13 @@ public class TireReplacementRecordVO extends TireReplacementRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportChangeRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportChangeRecordVO.java index 2ae66b51..60f904b6 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportChangeRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportChangeRecordVO.java @@ -57,6 +57,10 @@ public class TransportChangeRecordVO extends TransportChangeRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + @TableField(exist = false) @Schema(description = "更新人姓名") private String updateUserName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportShipVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportShipVO.java index 76fb12af..27e06308 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportShipVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportShipVO.java @@ -62,4 +62,8 @@ public class TransportShipVO extends TransportShip { @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate warningDate; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java index 7536d193..bd4ab362 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportVehicleVO.java @@ -76,4 +76,8 @@ public class TransportVehicleVO extends TransportVehicle { @Schema(description = "绑定司机") private String boundDriver; + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ViolationRecordVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ViolationRecordVO.java index 98b7569c..0e462908 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ViolationRecordVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ViolationRecordVO.java @@ -63,6 +63,13 @@ public class ViolationRecordVO extends ViolationRecord { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTimeEnd; + /** + * 创建人姓名 + */ + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + /** * 更新人姓名 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VoucherFolderVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VoucherFolderVO.java index 1f70c945..6c9ca5a5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VoucherFolderVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/VoucherFolderVO.java @@ -25,6 +25,7 @@ public class VoucherFolderVO implements Serializable { private String folderName; private Long waybillId; private String waybillNo; + private String loadingNo; private Integer matched; private String processStatus; private String fileName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportRowErrorVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportRowErrorVO.java new file mode 100644 index 00000000..2fa95b19 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportRowErrorVO.java @@ -0,0 +1,26 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** 运单批量导入单行校验错误。 */ +@Data +@Schema(description = "运单批量导入单行校验错误") +public class WaybillImportRowErrorVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "行号(从0开始,相对当前批次)") + private Integer rowIndex; + + @Schema(description = "错误信息") + private String errorMessage; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportValidateResultVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportValidateResultVO.java new file mode 100644 index 00000000..193c5ee7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportValidateResultVO.java @@ -0,0 +1,28 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** 运单批量导入校验结果。 */ +@Data +@Schema(description = "运单批量导入校验结果") +public class WaybillImportValidateResultVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "是否全部通过") + private Boolean success; + + @Schema(description = "校验错误列表") + private List errors = new ArrayList<>(); +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java index cea37c38..ebba5343 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java @@ -66,6 +66,10 @@ public class WaybillVO extends Waybill { @Schema(description = "业务状态名称") private String businessStatusName; + @TableField(exist = false) + @Schema(description = "运输方式名称") + private String transportTypeName; + @TableField(exist = false) @Schema(description = "是否需要司机确认接单(过程配置接单节点)") private Boolean requireAccept; diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java index 6f83dd9f..f65cc45e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/controller/BusinessProcessController.java @@ -13,6 +13,7 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.StringUtil; import org.springblade.process.pojo.dto.ApprovalDTO; import org.springblade.process.pojo.vo.ApprovalVO; import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; @@ -55,15 +56,27 @@ public class BusinessProcessController extends BladeController { @PostMapping("/processSubmit") @ApiOperationSupport(order = 2) - @Operation(summary = "提交MK审核流", description = "调用mk processSubmit,传入templateCode/submitIdentity/formInstanceId") + @Operation(summary = "提交MK审核流", description = "传入bizId/bizType/templateCode,后端生成或复用formInstanceId后调用mk processSubmit") public R processSubmit(@Validated @RequestBody MKProcessCreateDTO param) { return R.data(businessProcessService.processSubmit(param)); } + @GetMapping("/hasMkProcessInstance") + @ApiOperationSupport(order = 8) + @Operation(summary = "查询MK流程实例是否存在", description = "传入业务主键bizId,有数据则无需删除/重新提交") + public R hasMkProcessInstance(@Valid @NotBlank(message = "业务id不能为空") String bizId) { + return R.data(businessProcessService.hasMkProcessInstance(bizId)); + } + @PostMapping("/processDelete") @ApiOperationSupport(order = 7) + @Operation(summary = "删除MK审核流", description = "传入bizId,删除当前业务对应MK流程实例") public R processDelete(@RequestBody MKProcessCreateDTO param) { - return R.data(businessProcessService.processDelete(param == null ? null : param.getFormInstanceId())); + String bizId = null; + if (param != null) { + bizId = StringUtil.isNotBlank(param.getBizId()) ? param.getBizId() : param.getFormInstanceId(); + } + return R.data(businessProcessService.processDelete(bizId)); } @GetMapping("/isEditView") diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java index fa87f975..6319b680 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/feign/BusinessProcessClient.java @@ -11,6 +11,7 @@ import org.springblade.process.pojo.dto.BusinessProcessDeleteDTO; import org.springblade.process.pojo.dto.BusinessProcessSubmitDTO; import org.springblade.process.pojo.dto.BusinessProcessUpdateDTO; import org.springblade.process.pojo.vo.BusinessProcessVO; +import org.springblade.process.pojo.vo.MkFormBindVO; import org.springblade.process.pojo.vo.ProcessApprovedRecordVO; import org.springblade.process.pojo.vo.ProcessTodoVO; import org.springblade.process.service.IBusinessProcessService; @@ -94,4 +95,19 @@ public class BusinessProcessClient implements IBusinessProcessClient { @RequestParam(value = "loginName", required = false) String loginName) { return FR.data(businessProcessService.getProcessInfo(processInstanceId, loginName)); } + + @PreAuth(AuthConstant.PERMIT_ALL) + @GetMapping(RESOLVE_BY_FORM_INSTANCE_ID) + @Override + public FR resolveByFormInstanceId(@RequestParam("formInstanceId") Long formInstanceId) { + return FR.data(businessProcessService.resolveByFormInstanceId(formInstanceId)); + } + + @PreAuth(AuthConstant.PERMIT_ALL) + @PostMapping(MARK_PROCESS_FINISHED) + @Override + public FR markProcessFinished(@RequestParam("processInstanceId") String processInstanceId, + @RequestParam("approved") boolean approved) { + return FR.data(businessProcessService.markProcessFinished(processInstanceId, approved)); + } } diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml index 47b45b79..4aa5df53 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/BusinessProcessMapper.xml @@ -6,6 +6,7 @@ + diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.java b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.java new file mode 100644 index 00000000..23e701f4 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.java @@ -0,0 +1,38 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.process.pojo.entity.MkFormBind; + +/** + * MK表单实例绑定表 Mapper 接口 + * + * @author BladeX + */ +public interface MkFormBindMapper extends BaseMapper { + +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.xml new file mode 100644 index 00000000..b19c1df2 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/mapper/MkFormBindMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java index e3b7834a..36b15ce0 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/IBusinessProcessService.java @@ -29,18 +29,43 @@ public interface IBusinessProcessService extends IService { /** * 直接调用 MK processSubmit 提交流程 * - * @param param MK 流程创建参数 + * @param param MK 流程创建参数(传 bizId+bizType,后端生成/复用 formInstanceId) * @return 流程实例 id */ String processSubmit(MKProcessCreateDTO param); /** - * 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用) + * 按业务 id 查询 MK 流程实例是否仍有数据(本地有 processInstanceId 且 MK getProcessInfo 有返回) * - * @param formInstanceId 业务表单实例 id + * @param bizId 业务主键 + * @return true=流程实例仍存在 + */ + boolean hasMkProcessInstance(String bizId); + + /** + * 按业务主键删除 MK 流程(不删除本地业务流程记录与表单绑定,供驳回后重新提交使用) + * + * @param bizId 业务主键 * @return 是否成功 */ - boolean processDelete(String formInstanceId); + boolean processDelete(String bizId); + + /** + * 按表单实例id解析业务绑定 + * + * @param formInstanceId 表单实例id + * @return 绑定信息 + */ + MkFormBindVO resolveByFormInstanceId(Long formInstanceId); + + /** + * 公开页流程结束时标记业务流程通过/驳回并完结 + * + * @param processInstanceId 流程实例id + * @param approved true=通过 false=驳回 + * @return 是否更新成功 + */ + boolean markProcessFinished(String processInstanceId, boolean approved); /** * 获取流程当前节点详情 diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/IMkFormBindService.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/IMkFormBindService.java new file mode 100644 index 00000000..5bd9fe2b --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/IMkFormBindService.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.springblade.process.pojo.entity.MkFormBind; +import org.springblade.process.pojo.vo.MkFormBindVO; + +/** + * MK表单实例绑定表 服务类 + * + * @author BladeX + */ +public interface IMkFormBindService extends IService { + + /** + * 查询业务当前生效的表单实例绑定 + * + * @param bizType 业务类型(可空,空时仅按业务id查) + * @param bizId 业务主键 + * @return 当前绑定,不存在返回 null + */ + MkFormBind getCurrentBind(String bizType, Long bizId); + + /** + * 按表单实例id查询绑定 + * + * @param formInstanceId 表单实例id + * @return 绑定记录 + */ + MkFormBind getByFormInstanceId(Long formInstanceId); + + /** + * 按表单实例id解析业务主键等信息 + * + * @param formInstanceId 表单实例id + * @return 绑定 VO + */ + MkFormBindVO resolveByFormInstanceId(Long formInstanceId); + + /** + * 解析或生成表单实例id:无当前绑定则新建;强制新建/当前流程已通过或已完结则新建;否则复用 + * + * @param bizType 业务类型 + * @param bizId 业务主键 + * @param forceNew 是否强制生成新表单实例(审核通过后再次提交) + * @return 本次应使用的表单实例id + */ + Long resolveOrCreateFormInstanceId(String bizType, Long bizId, boolean forceNew); + + /** + * 回写流程实例id到当前绑定 + * + * @param formInstanceId 表单实例id + * @param processInstanceId 流程实例id + */ + void updateProcessInstanceId(Long formInstanceId, String processInstanceId); +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java index 088c83d5..fed83a70 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/BusinessProcessServiceImpl.java @@ -25,6 +25,7 @@ import org.springblade.process.pojo.entity.BusinessProcess; import org.springblade.process.pojo.enums.TodoStatus; import org.springblade.process.pojo.vo.*; import org.springblade.process.service.IBusinessProcessService; +import org.springblade.process.service.IMkFormBindService; import org.springblade.system.pojo.entity.User; import org.springblade.system.service.IUserService; import org.springblade.thirdparty.mk.config.MKProperties; @@ -72,6 +73,7 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + .orderByDesc(BusinessProcess::getUpdateTime) + .last("limit 1") + ); + boolean forceNew = Boolean.TRUE.equals(param.getForceNewFormInstance()) + || shouldForceNewFormInstance(businessProcess, loginName); + Long formInstanceId = mkFormBindService.resolveOrCreateFormInstanceId(bizType, bizId, forceNew); + String formInstanceIdText = String.valueOf(formInstanceId); + param.setFormInstanceId(formInstanceIdText); + + // 已有流程实例且 MK 仍能查到详情时,不再删除、不再重新创建提交(强制新建或流程已结束时跳过短路) + if (!forceNew && businessProcess != null && StringUtil.isNotBlank(businessProcess.getProcessInstanceId()) + && Objects.equals(formInstanceId, businessProcess.getFormInstanceId())) { + String existingProcessInstanceId = businessProcess.getProcessInstanceId(); + Object existingProcessInfo = this.getProcessInfo(existingProcessInstanceId, loginName); + if (hasProcessInfoData(existingProcessInfo) && !isMkProcessFinished(existingProcessInfo)) { + log.info("MK流程实例已存在,跳过创建并提交流程 bizId={} formInstanceId={} processInstanceId={}", + bizId, formInstanceIdText, existingProcessInstanceId); + businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue()); + if (StringUtil.isNotBlank(param.getSubject())) { + businessProcess.setSubject(param.getSubject()); + } + businessProcess.setPromoterLoginName(loginName); + businessProcess.setFormInstanceId(formInstanceId); + this.updateById(businessProcess); + this.getCurrentNodes(existingProcessInstanceId, loginName); + this.syncBizFromProcessInfo(bizType, String.valueOf(bizId), existingProcessInfo); + return existingProcessInstanceId; + } + // 已结束但仍能查到详情:继续走新建提交流程 + forceNew = true; + formInstanceId = mkFormBindService.resolveOrCreateFormInstanceId(bizType, bizId, true); + formInstanceIdText = String.valueOf(formInstanceId); + param.setFormInstanceId(formInstanceIdText); + } + MKProcessCreateDTO mkParam = new MKProcessCreateDTO(); - mkParam.setFormInstanceId(param.getFormInstanceId()); + mkParam.setFormInstanceId(formInstanceIdText); mkParam.setSubject(param.getSubject()); mkParam.setSubmitIdentity(loginName); mkParam.setLoginName(loginName); @@ -149,15 +189,6 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() - .eq(BusinessProcess::getBizId, bizId) - ); if (businessProcess == null) { businessProcess = new BusinessProcess(); businessProcess.setBizId(bizId); @@ -177,30 +208,151 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() + .eq(BusinessProcess::getBizId, id) + .orderByDesc(BusinessProcess::getUpdateTime) + .last("limit 1") + ); + if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) { + return false; + } + // 审批通过 / 已完结:允许再次提交新表单实例 + if (Objects.equals(ApproveStatusEnum.APPROVED.getValue(), businessProcess.getApproveStatus()) + || (businessProcess.getIsCompleted() != null && businessProcess.getIsCompleted() == 1)) { + return false; + } + String loginName = businessProcess.getPromoterLoginName(); + if (StringUtil.isBlank(loginName)) { + loginName = resolveCurrentUserPhone(); + } + if (StringUtil.isBlank(loginName)) { + log.warn("查询MK流程实例跳过,登录名为空 bizId={} processInstanceId={}", + bizId, businessProcess.getProcessInstanceId()); + return false; + } + Object processInfo = this.getProcessInfo(businessProcess.getProcessInstanceId(), loginName); + // MK 流程已结束(fdProcessStatus=30)时视为无有效待办,需重新提交新实例 + if (isMkProcessFinished(processInfo)) { + return false; + } + return hasProcessInfoData(processInfo); + } + + /** + * MK getProcessInfo 是否为已结束流程 + */ + private boolean isMkProcessFinished(Object processInfo) { + if (!(processInfo instanceof Map map)) { + return false; + } + Object status = map.get("fdProcessStatus"); + return status != null && "30".equals(String.valueOf(status)); + } + + /** + * MK getProcessInfo 是否返回有效数据 + */ + private boolean hasProcessInfoData(Object processInfo) { + if (processInfo == null) { + return false; + } + if (processInfo instanceof Map map) { + return !map.isEmpty(); + } + if (processInfo instanceof Collection collection) { + return !collection.isEmpty(); + } + if (processInfo instanceof CharSequence text) { + return StringUtil.isNotBlank(text.toString()); + } + return true; + } + @Transactional(rollbackFor = Exception.class) @Override public boolean processDelete(String formInstanceId) { - AssertUtils.notBlank(formInstanceId, "表单实例id不能为空"); + AssertUtils.notBlank(formInstanceId, "业务id不能为空"); Long bizId; try { bizId = Long.valueOf(formInstanceId); } catch (NumberFormatException e) { - throw new ServiceException("表单实例id格式不正确"); + throw new ServiceException("业务id格式不正确"); } BusinessProcess businessProcess = this.getOne(Wrappers.lambdaQuery() .eq(BusinessProcess::getBizId, bizId) + .orderByDesc(BusinessProcess::getUpdateTime) + .last("limit 1") ); if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) { - log.warn("客商驳回后删除流程跳过,未找到流程实例 formInstanceId={}", formInstanceId); + log.warn("驳回后删除流程跳过,未找到流程实例 bizId={}", bizId); return true; } String loginName = businessProcess.getPromoterLoginName(); @@ -208,8 +360,8 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() + .eq(BusinessProcess::getBizId, formInstanceId) + .last("limit 1") + ); + if (businessProcess == null) { + businessProcess = this.getOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getFormInstanceId, formInstanceId) + .last("limit 1") + ); + } + if (businessProcess == null) { + return null; + } + MkFormBindVO fallback = new MkFormBindVO(); + fallback.setBizId(businessProcess.getBizId()); + fallback.setFormInstanceId(businessProcess.getFormInstanceId() != null + ? businessProcess.getFormInstanceId() : businessProcess.getBizId()); + fallback.setProcessInstanceId(businessProcess.getProcessInstanceId()); + return fallback; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean markProcessFinished(String processInstanceId, boolean approved) { + AssertUtils.notBlank(processInstanceId, "流程实例id不能为空"); + BusinessProcess businessProcess = this.getBusinessProcessByProcessInstanceId(processInstanceId); + if (businessProcess == null) { + log.warn("标记流程完结跳过,未找到业务流程 processInstanceId={}", processInstanceId); + return false; + } + String approveStatus = approved + ? ApproveStatusEnum.APPROVED.getValue() + : ApproveStatusEnum.REJECTED.getValue(); + businessProcess.setApproveStatus(approveStatus); + businessProcess.setIsCompleted(1); + businessProcess.setCompleteTime(new Date()); + businessProcess.setCurrentNodeIds(null); + businessProcess.setCurrentNodeNames(null); + businessProcess.setCurrentHandlers(null); + boolean updated = this.updateById(businessProcess); + log.info("标记流程完结 processInstanceId={} approved={} approveStatus={} result={}", + processInstanceId, approved, approveStatus, updated); + return updated; + } + @Override public List getCurrentNodes(String processInstanceId, String loginName) { if (StringUtils.isBlank(processInstanceId)) { @@ -358,17 +563,20 @@ public class BusinessProcessServiceImpl extends ServiceImpllambdaQuery() .eq(BusinessProcess::getBizId, bizId) + .orderByDesc(BusinessProcess::getUpdateTime) .last("limit 1") ); AssertUtils.notNull(businessProcess, "业务流程不存在"); processInstanceId = businessProcess.getProcessInstanceId(); } try { - String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, AuthUtil.getUserAccount()); + String loginName = resolveCurrentUserPhone(); + AssertUtils.notBlank(loginName, "当前用户手机号为空,无法获取审核跳转地址"); + String mkApprovalUrl = mkService.getMKApprovalUrl(processInstanceId, loginName); AssertUtils.notBlank(mkApprovalUrl, "获取mk审批页面链接异常"); return mkApprovalUrl; } catch (MKException e) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/MkFormBindServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/MkFormBindServiceImpl.java new file mode 100644 index 00000000..2e9c4346 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/process/service/impl/MkFormBindServiceImpl.java @@ -0,0 +1,177 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.process.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.log.utils.AssertUtils; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.process.mapper.BusinessProcessMapper; +import org.springblade.process.mapper.MkFormBindMapper; +import org.springblade.process.pojo.entity.BusinessProcess; +import org.springblade.process.pojo.entity.MkFormBind; +import org.springblade.process.pojo.enums.ApproveStatusEnum; +import org.springblade.process.pojo.vo.MkFormBindVO; +import org.springblade.process.service.IMkFormBindService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.Objects; + +/** + * MK表单实例绑定表 服务实现类 + * + * @author BladeX + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class MkFormBindServiceImpl extends ServiceImpl implements IMkFormBindService { + + private final BusinessProcessMapper businessProcessMapper; + + @Override + public MkFormBind getCurrentBind(String bizType, Long bizId) { + AssertUtils.notNull(bizId, "业务id不能为空"); + MkFormBind bind = null; + if (StringUtil.isNotBlank(bizType)) { + bind = this.getOne(Wrappers.lambdaQuery() + .eq(MkFormBind::getBizType, bizType) + .eq(MkFormBind::getBizId, bizId) + .eq(MkFormBind::getIsCurrent, 1) + .orderByDesc(MkFormBind::getCreateTime) + .last("limit 1") + ); + } + if (bind == null) { + // 兼容历史迁移数据 biz_type 为空,或未传 bizType + bind = this.getOne(Wrappers.lambdaQuery() + .eq(MkFormBind::getBizId, bizId) + .eq(MkFormBind::getIsCurrent, 1) + .orderByDesc(MkFormBind::getCreateTime) + .last("limit 1") + ); + } + return bind; + } + + @Override + public MkFormBind getByFormInstanceId(Long formInstanceId) { + AssertUtils.notNull(formInstanceId, "表单实例id不能为空"); + return this.getOne(Wrappers.lambdaQuery() + .eq(MkFormBind::getFormInstanceId, formInstanceId) + .last("limit 1") + ); + } + + @Override + public MkFormBindVO resolveByFormInstanceId(Long formInstanceId) { + MkFormBind bind = this.getByFormInstanceId(formInstanceId); + if (bind == null) { + return null; + } + MkFormBindVO vo = new MkFormBindVO(); + vo.setBizId(bind.getBizId()); + vo.setBizType(bind.getBizType()); + vo.setFormInstanceId(bind.getFormInstanceId()); + vo.setProcessInstanceId(bind.getProcessInstanceId()); + return vo; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Long resolveOrCreateFormInstanceId(String bizType, Long bizId, boolean forceNew) { + AssertUtils.notNull(bizId, "业务id不能为空"); + MkFormBind current = this.getCurrentBind(bizType, bizId); + if (current == null) { + return this.createNewBind(bizType, bizId); + } + BusinessProcess businessProcess = businessProcessMapper.selectOne(Wrappers.lambdaQuery() + .eq(BusinessProcess::getBizId, bizId) + .orderByDesc(BusinessProcess::getUpdateTime) + .last("limit 1") + ); + boolean approvedPass = businessProcess != null + && Objects.equals(ApproveStatusEnum.APPROVED.getValue(), businessProcess.getApproveStatus()); + boolean completed = businessProcess != null + && businessProcess.getIsCompleted() != null + && businessProcess.getIsCompleted() == 1; + if (forceNew || approvedPass || completed) { + log.info("生成新表单实例 forceNew={} approvedPass={} completed={} bizType={} bizId={} oldFormInstanceId={}", + forceNew, approvedPass, completed, bizType, bizId, current.getFormInstanceId()); + this.lambdaUpdate() + .eq(MkFormBind::getBizId, bizId) + .eq(MkFormBind::getIsCurrent, 1) + .set(MkFormBind::getIsCurrent, 0) + .set(MkFormBind::getUpdateTime, new Date()) + .update(); + return this.createNewBind(bizType, bizId); + } + log.info("复用当前表单实例 bizType={} bizId={} formInstanceId={}", + bizType, bizId, current.getFormInstanceId()); + if (StringUtil.isNotBlank(bizType) && StringUtil.isBlank(current.getBizType())) { + current.setBizType(bizType); + current.setUpdateTime(new Date()); + this.updateById(current); + } + return current.getFormInstanceId(); + } + + @Override + public void updateProcessInstanceId(Long formInstanceId, String processInstanceId) { + AssertUtils.notNull(formInstanceId, "表单实例id不能为空"); + this.lambdaUpdate() + .eq(MkFormBind::getFormInstanceId, formInstanceId) + .set(MkFormBind::getProcessInstanceId, processInstanceId) + .set(MkFormBind::getUpdateTime, new Date()) + .update(); + } + + private Long createNewBind(String bizType, Long bizId) { + Long formInstanceId = IdWorker.getId(); + MkFormBind bind = new MkFormBind(); + bind.setId(IdWorker.getId()); + bind.setTenantId(StringUtil.isBlank(AuthUtil.getTenantId()) ? "000000" : AuthUtil.getTenantId()); + bind.setBizType(StringUtil.isBlank(bizType) ? "" : bizType); + bind.setBizId(bizId); + bind.setFormInstanceId(formInstanceId); + bind.setIsCurrent(1); + bind.setCreateTime(new Date()); + bind.setUpdateTime(new Date()); + boolean saved = this.save(bind); + if (!saved) { + throw new ServiceException("保存表单实例绑定失败"); + } + log.info("创建表单实例绑定 bizType={} bizId={} formInstanceId={}", bizType, bizId, formInstanceId); + return formInstanceId; + } +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java index 59950862..355aaee9 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/AirportMasterController.java @@ -157,10 +157,21 @@ public class AirportMasterController extends BladeController { if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { return R.fail("请上传 .xls,.xlsx 标准格式文件"); } - List failureList = airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "空港机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AirportMasterExcel.class); + try { + airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AirportMasterExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", file, AirportMasterExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java index 745968b5..cf73d38c 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CargoTypeController.java @@ -46,9 +46,9 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.common.excel.ImportFailureException; import org.springblade.system.excel.CargoTypeExcel; import org.springblade.system.excel.CargoTypeExportExcel; -import org.springblade.system.excel.CargoTypeImportFailureExcel; import org.springblade.system.pojo.entity.CargoType; import org.springblade.system.pojo.vo.CargoTypeVO; import org.springblade.system.service.ICargoTypeService; @@ -156,15 +156,41 @@ public class CargoTypeController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入货物类型", description = "传入excel") public R importCargoType(MultipartFile file, HttpServletResponse response) { - List data = ExcelUtil.read(file, CargoTypeExcel.class); - List failureList = cargoTypeService.importCargoType(data); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class); + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); + if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return R.fail("请上传 .xls,.xlsx 标准格式文件"); + } + try { + cargoTypeService.importCargoType(ExcelUtil.read(file, CargoTypeExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + exportFailure(response, exception.getFailureList()); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoTypeExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } + /** + * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。 + *

+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。 + */ + private void exportFailure(HttpServletResponse response, List failureList) { + ImportFailureExcelUtil.exportFailureReasonOnly(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class); + } + /** * 导出货物类型 */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java index 4bc43d95..8b6b6c44 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/CurrencyController.java @@ -149,10 +149,28 @@ public class CurrencyController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "导入币种汇率", description = "传入excel") public R importCurrency(MultipartFile file, HttpServletResponse response) { - List failureList = currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CurrencyExcel.class); + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); + if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return R.fail("请上传 .xls,.xlsx 标准格式文件"); + } + try { + currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CurrencyExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", file, CurrencyExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 180dc550..67dbf494 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -44,11 +44,10 @@ import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; import org.springblade.core.tool.support.Kv; import org.springblade.core.tool.utils.Func; -import org.springblade.system.cache.DictCache; +import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.User; -import org.springblade.system.pojo.enums.DictEnum; import org.springblade.system.pojo.vo.DeptVO; import org.springblade.system.pojo.vo.OaOrgSyncPageVO; import org.springblade.system.pojo.vo.UserVO; @@ -156,7 +155,7 @@ public class DeptController extends BladeController { CacheUtil.clear(SYS_CACHE, Boolean.FALSE); // 返回懒加载树更新节点所需字段 Kv kv = Kv.create().set("id", String.valueOf(dept.getId())).set("tenantId", dept.getTenantId()) - .set("deptCategoryName", DictCache.getValue(DictEnum.ORG_CATEGORY, dept.getDeptCategory())); + .set("deptCategoryName", DictBizCache.getValue("organization_type", dept.getDeptCategory())); return R.data(kv); } return R.fail("操作失败"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java index b8eae386..d70330bd 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/FeeItemController.java @@ -36,6 +36,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.common.excel.ImportFailureException; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; @@ -47,7 +48,6 @@ import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.excel.FeeItemExcel; import org.springblade.system.excel.FeeItemExportExcel; -import org.springblade.system.excel.FeeItemImportFailureExcel; import org.springblade.system.pojo.entity.FeeItem; import org.springblade.system.pojo.vo.FeeItemVO; import org.springblade.system.service.IFeeItemService; @@ -138,11 +138,28 @@ public class FeeItemController extends BladeController { @ApiOperationSupport(order = 6) @Operation(summary = "导入费用项", description = "传入excel") public R importFeeItem(MultipartFile file, HttpServletResponse response) { - List data = ExcelUtil.read(file, FeeItemExcel.class); - List failureList = feeItemService.importFeeItem(data); - if (Func.isNotEmpty(failureList)) { - ImportFailureExcelUtil.export(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", failureList, FeeItemExcel.class); + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); + if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return R.fail("请上传 .xls,.xlsx 标准格式文件"); + } + try { + feeItemService.importFeeItem(ExcelUtil.read(file, FeeItemExcel.class)); + } catch (ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + ImportFailureExcelUtil.exportFailureReasonOnly(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), FeeItemExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", file, FeeItemExcel.class, readException)) { + return null; + } + throw readException; } return R.success("导入数据成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java index 0639b52d..8c19eedc 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java @@ -44,7 +44,7 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.common.excel.ImportFailureExcelUtil; -import org.springblade.system.excel.ImportFailureException; +import org.springblade.common.excel.ImportFailureException; import org.springblade.system.excel.PortTerminalExcel; import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.excel.PortTerminalImporter; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java index 7bc1f8c2..34149e9d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RailwayStationController.java @@ -160,10 +160,21 @@ public class RailwayStationController extends BladeController { if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { return R.fail("请上传 .xls,.xlsx 标准格式文件"); } - List failureList = railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class)); - if (Func.isNotEmpty(failureList)) { - org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "铁路车站主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RailwayStationExcel.class); + try { + railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。 + org.springblade.common.excel.ImportFailureExcelUtil.export(response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RailwayStationExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", file, RailwayStationExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java index 0de1e680..9f0ec270 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/RegionController.java @@ -46,6 +46,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; import org.springblade.system.pojo.entity.Region; import org.springblade.system.excel.RegionExcel; import org.springblade.system.excel.RegionExportExcel; @@ -191,10 +192,28 @@ public class RegionController extends BladeController { @ApiOperationSupport(order = 10) @Operation(summary = "导入行政区划", description = "传入excel") public R importRegion(MultipartFile file, Integer isCovered, HttpServletResponse response) { - List failureList = regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1)); - if (!failureList.isEmpty()) { - org.springblade.common.excel.ImportFailureExcelUtil.export(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RegionExcel.class); + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); + if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return R.fail("请上传 .xls,.xlsx 标准格式文件"); + } + try { + regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1)); + } catch (org.springblade.common.excel.ImportFailureException exception) { + // 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。 + org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RegionExcel.class); return null; + } catch (Exception readException) { + // 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时, + // EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。 + // 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。 + if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure( + response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", file, RegionExcel.class, readException)) { + return null; + } + throw readException; } return R.success("操作成功"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExportExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExportExcel.java index e237be36..d2918d23 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExportExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/AirportMasterExportExcel.java @@ -102,6 +102,9 @@ public class AirportMasterExportExcel implements Serializable { @ExcelProperty("备注") private String remark; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("更新人") private String updateUserName; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java index b59573d5..998f3aac 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java @@ -76,7 +76,7 @@ public class CurrencyExcel implements Serializable { @ExcelProperty("备注") private String remark; - @ExcelProperty + @ExcelIgnore private String errorMessage; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/FeeItemExportExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/FeeItemExportExcel.java index aedffeb5..593992ad 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/FeeItemExportExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/FeeItemExportExcel.java @@ -50,6 +50,9 @@ public class FeeItemExportExcel implements Serializable { @ExcelProperty("组织") private String createDeptName; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("更新人") private String updateUserName; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java index b1c6471b..5454526d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/PortTerminalExportExcel.java @@ -80,6 +80,9 @@ public class PortTerminalExportExcel implements Serializable { @ExcelProperty("备注") private String remark; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("更新人") private String updateUserName; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExportExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExportExcel.java index b745a80c..ddaf8230 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExportExcel.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/RailwayStationExportExcel.java @@ -92,6 +92,9 @@ public class RailwayStationExportExcel implements Serializable { @ExcelProperty("备注") private String remark; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("更新人") private String updateUserName; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.java index 55661a6a..6176e472 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.java @@ -26,6 +26,7 @@ package org.springblade.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.vo.DeptVO; @@ -66,6 +67,18 @@ public interface DeptMapper extends BaseMapper { */ List lazyTree(String tenantId, Long parentId); + /** + * 按组织 ID 集合查询树节点(用于数据权限过滤后的根节点回显) + * + * @param tenantId 租户ID,空表示不限 + * @param ids 需要返回的组织 ID + * @param childIds 计算 hasChildren 时允许计入的子组织 ID;为空则 hasChildren 恒为 false + * @return 树节点列表 + */ + List treeNodesByIds(@Param("tenantId") String tenantId, + @Param("ids") List ids, + @Param("childIds") List childIds); + /** * 获取部门名 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml index baef84dc..e125b43b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/DeptMapper.xml @@ -12,6 +12,7 @@ + @@ -32,6 +33,7 @@ + @@ -115,6 +117,47 @@ ORDER BY dept.sort + +