Merge remote-tracking branch 'refs/remotes/origin/master' into feature/payment-apply-bfi-sync
# Conflicts: # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.common.excel;
|
||||
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 单元格读取失败异常 —— 用于「异常不是 fastexcel 抛出、但同样发生在读取阶段」的场景。
|
||||
* <p>
|
||||
* fastexcel 自身解析失败时抛 {@code ExcelDataConvertException},位置信息齐全,
|
||||
* {@link ImportFailureExcelUtil#exportReadFailure} 可以直接定位到行与列。但有些模块
|
||||
* 在读取阶段有自己的解析逻辑(例如运输对账的动态费用列:模板列不固定,由
|
||||
* {@code VehicleReconciliationFeeReader} 自行按表头解析),这些代码如果只是
|
||||
* {@code catch (NumberFormatException)} 后返回 {@code BigDecimal.ZERO},
|
||||
* 脏数据就会静默入库(费用 0.00 还会参与结算金额与自动匹配)。
|
||||
* <p>
|
||||
* 正确做法是改抛本异常,把位置信息补齐,让导出工具能把原因挂到正确的行、标红正确的列。
|
||||
* <p>
|
||||
* 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class ImportCellReadException extends ServiceException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 出错的单元格所在的 Excel 行号,<b>绝对 0 基</b>(表头占 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 阶段」的转换异常转成可读的失败原因。
|
||||
* <p>
|
||||
* 背景:单元格里填了无法转换的内容(如金额列填「一万二千元」)时,EasyExcel 会在
|
||||
* {@code ExcelUtil.read(...)} 阶段就抛 {@link cn.idev.excel.exception.ExcelDataConvertException},
|
||||
* 根本走不到行级校验;若不兜底,用户看到的是
|
||||
* 「Convert data ... to class java.math.BigDecimal error」这类技术文案。
|
||||
* <p>
|
||||
* 本方法据异常里的行列位置与目标类型,生成「第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 extends Throwable> T findCause(Throwable throwable, Class<T> 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<Field> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以「原始单元格文本」方式重读导入文件,得到的行用于构造失败明细。
|
||||
* <p>
|
||||
* 优先使用 Excel 文件真实表头(含动态费用列等模型类未声明的列),保证失败明细
|
||||
* 与原导入模板列一致,用户修正后可直接用该文件重新导入。
|
||||
* <p>
|
||||
* 用途:当 {@code ExcelUtil.read(file, XxxExcel.class)} 因单元格无法转换而失败时,
|
||||
* 已解析的实体列表是不完整的(异常中断了整批读取)。此时用本方法重读一次:
|
||||
* 不做类型转换(直接读成 Map),因此绝不会再抛转换异常,可拿到**原表全部行**,
|
||||
* 供导出「原表全量 + 仅出错行带原因」的失败明细。
|
||||
*
|
||||
* @param file 上传的 Excel 文件
|
||||
* @param excelClass 导入模型类(表头缺失时按列序号反查表头名)
|
||||
* @return 按行组织的明细数据:每行是「表头名 → 单元格文本」的有序 Map;
|
||||
* 读不出内容时返回空列表(调用方按空明细处理)
|
||||
*/
|
||||
public static List<Map<String, String>> readRawRows(java.io.InputStream file, Class<?> excelClass) {
|
||||
List<Map<Integer, String>> rawRows = new ArrayList<>();
|
||||
Map<Integer, String> headerByIndex = new LinkedHashMap<>();
|
||||
try {
|
||||
cn.idev.excel.read.builder.ExcelReaderBuilder builder = cn.idev.excel.FastExcel
|
||||
.read(file, new cn.idev.excel.event.AnalysisEventListener<Map<Integer, String>>() {
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> 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<Integer, String> 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<Map<String, String>> rows = new ArrayList<>();
|
||||
for (Map<Integer, String> rawRow : rawRows) {
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
Set<Integer> columnIndexes = new LinkedHashSet<>(headerByIndex.keySet());
|
||||
if (rawRow != null) {
|
||||
columnIndexes.addAll(rawRow.keySet());
|
||||
}
|
||||
List<Integer> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按原上传文件真实表头导出失败明细(含动态列),并追加「导入失败原因」。
|
||||
* <p>
|
||||
* 适用于运输对账等「模板列 = 固定列 + 动态费用列」场景:按实体字段反射导出
|
||||
* 会丢掉动态列,用户无法用失败文件修正后重新导入。本方法重读原文件全量列,
|
||||
* 再按 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<Map<String, String>> 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<Integer, String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出「读取阶段失败」明细:原表全量行 + 末尾「导入失败原因」列。
|
||||
* <p>
|
||||
* 与 {@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<Map<String, String>> rawRows, Map<Integer, String> 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<String> headers = new ArrayList<>();
|
||||
for (Map<String, String> row : rawRows) {
|
||||
for (String key : row.keySet()) {
|
||||
if (!headers.contains(key)) {
|
||||
headers.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.add(FAILURE_REASON);
|
||||
List<List<String>> head = new ArrayList<>();
|
||||
for (String header : headers) {
|
||||
head.add(List.of(header));
|
||||
}
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||
List<List<String>> 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<String, String> rawRow = rawRows.get(rowIndex);
|
||||
List<Object> 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<Integer, Set<Integer>> redColumnsByRow;
|
||||
private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
|
||||
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
|
||||
private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
|
||||
|
||||
private RawFailureCellStyleHandler(List<List<Object>> rows, Map<Integer, Set<Integer>> redColumnsByRow) {
|
||||
this.failureReasonColumnIndex = rows.isEmpty() ? 0 : rows.get(0).size() - 1;
|
||||
this.redColumnsByRow = redColumnsByRow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
|
||||
List<WriteCellData<?>> 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<Integer> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取阶段异常的一站式兜底:重读原表 → 导出「全量行 + 出错行原因」失败明细。
|
||||
* <p>
|
||||
* 控制器用法:在导入的 try 块末尾追加
|
||||
* <pre>
|
||||
* } catch (Exception exception) {
|
||||
* if (ImportFailureExcelUtil.exportReadFailure(response, fileName, sheetName, file, XxxExcel.class, exception)) {
|
||||
* return null;
|
||||
* }
|
||||
* throw exception;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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<Map<String, String>> rawRows;
|
||||
try (java.io.InputStream inputStream = file.getInputStream()) {
|
||||
rawRows = readRawRows(inputStream, excelClass);
|
||||
} catch (IOException ioException) {
|
||||
rawRows = new ArrayList<>();
|
||||
}
|
||||
Map<Integer, String> failureMessageByRow = new HashMap<>();
|
||||
if (rawRows.isEmpty()) {
|
||||
// 重读也拿不到内容时,退化为「仅失败原因」一行,至少让用户看到可读原因
|
||||
Map<String, String> 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 基,只对数据行计数),供挂失败原因使用。
|
||||
* <p>
|
||||
* 无论哪种异常,源信息都是<b>绝对 0 基行号</b>(表头占 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)。
|
||||
* <p>
|
||||
* 两条来源统一到这里: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<String> headerKeywords(String header) {
|
||||
if (header == null || header.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> 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<Integer> resolveRawRedColumns(String failureMessage, List<List<String>> headerKeywords,
|
||||
int failureReasonColumnIndex) {
|
||||
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||
redColumns.add(failureReasonColumnIndex);
|
||||
String normalizedReason = normalize(failureMessage);
|
||||
if (normalizedReason.isEmpty()) {
|
||||
return redColumns;
|
||||
}
|
||||
List<int[]> 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<int[]> 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;
|
||||
|
||||
+9
-3
@@ -23,7 +23,7 @@
|
||||
* <p>
|
||||
* 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;
|
||||
/**
|
||||
* 导入失败异常,携带失败明细用于导出原表并标注错误。
|
||||
* <p>
|
||||
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
|
||||
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
|
||||
* 批量导入采用「任一行失败即整批回滚」语义:循环内先收集完全部错误明细,
|
||||
* 再抛出本异常触发事务回滚。明细在抛异常前已收集完毕,因此回滚不影响明细内容。
|
||||
* <p>
|
||||
* 注意:仅标注 {@code @Transactional} 并不足够。若循环内把异常吞掉(catch 后
|
||||
* 只往 errorList 塞数据并正常 return),方法会正常返回,Spring 会照常提交事务。
|
||||
* 必须抛出异常才能让事务真正回滚。
|
||||
* <p>
|
||||
* 本类位于 blade-common,供 blade-system 与 blade-transport 等模块共用。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -2,6 +2,9 @@ server:
|
||||
port: 80
|
||||
|
||||
spring:
|
||||
codec:
|
||||
# 运单批量导入等大 JSON / 失败明细导出场景,默认 256KB 会触发网关 500
|
||||
max-in-memory-size: 20MB
|
||||
cloud:
|
||||
gateway:
|
||||
server:
|
||||
|
||||
+23
@@ -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<Object> getProcessInfo(@RequestParam("processInstanceId") String processInstanceId,
|
||||
@RequestParam(value = "loginName", required = false) String loginName);
|
||||
|
||||
/**
|
||||
* 按表单实例id解析业务绑定
|
||||
*
|
||||
* @param formInstanceId 表单实例id
|
||||
* @return 绑定信息(含业务主键)
|
||||
*/
|
||||
@GetMapping(RESOLVE_BY_FORM_INSTANCE_ID)
|
||||
FR<MkFormBindVO> resolveByFormInstanceId(@RequestParam("formInstanceId") Long formInstanceId);
|
||||
|
||||
/**
|
||||
* 公开页流程结束时标记业务流程通过/驳回并完结
|
||||
*
|
||||
* @param processInstanceId 流程实例id
|
||||
* @param approved true=通过 false=驳回
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
@PostMapping(MARK_PROCESS_FINISHED)
|
||||
FR<Boolean> markProcessFinished(@RequestParam("processInstanceId") String processInstanceId,
|
||||
@RequestParam("approved") boolean approved);
|
||||
}
|
||||
|
||||
+6
@@ -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
|
||||
*/
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
+6
@@ -138,6 +138,12 @@ public class Dept extends TenantEntity {
|
||||
@Schema(description = "部门编码")
|
||||
private String deptCode;
|
||||
|
||||
/**
|
||||
* 金蝶部门代码
|
||||
*/
|
||||
@Schema(description = "金蝶部门代码")
|
||||
private String kingdeeDeptCode;
|
||||
|
||||
/**
|
||||
* 组织简称
|
||||
*/
|
||||
|
||||
+7
@@ -52,6 +52,13 @@ public class AirportMasterVO extends AirportMaster {
|
||||
@Schema(description = "导入错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 创建人姓名
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+7
@@ -52,6 +52,13 @@ public class PortTerminalVO extends PortTerminal {
|
||||
@Schema(description = "导入错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 创建人姓名
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+7
@@ -52,6 +52,13 @@ public class RailwayStationVO extends RailwayStation {
|
||||
@Schema(description = "导入错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 创建人姓名
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+2
@@ -33,4 +33,6 @@ public class WaybillImportBatchRequest {
|
||||
private String planName;
|
||||
private String remark;
|
||||
private List<Map<String, Object>> rows;
|
||||
/** 分批确认导入时,是否追加到已有批次(首批为 false,后续批为 true)。 */
|
||||
private Boolean append;
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class LoadingManage extends TenantEntity {
|
||||
@Schema(description = "货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@Schema(description = "车/船/航班/班列")
|
||||
@Schema(description = "车牌号/船班号/船次号/班次号")
|
||||
private String vehicleNo;
|
||||
|
||||
@Schema(description = "挂车车牌号")
|
||||
|
||||
+4
-1
@@ -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;
|
||||
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+4
@@ -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<CustomerContactVO> contacts = new ArrayList<>();
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -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;
|
||||
}
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+5
@@ -41,6 +41,11 @@ public class MasterOrderVO extends MasterOrder {
|
||||
private List<Waybill> boundWaybills;
|
||||
@TableField(exist = false)
|
||||
private BigDecimal totalQuantity;
|
||||
/**
|
||||
* 各品类已调度量(按货物名称+类型聚合,取各分段已调度量的最大值)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Map<String, BigDecimal> dispatchedGoods;
|
||||
@TableField(exist = false)
|
||||
private String createUserName;
|
||||
@TableField(exist = false)
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -76,4 +76,8 @@ public class TransportVehicleVO extends TransportVehicle {
|
||||
@Schema(description = "绑定司机")
|
||||
private String boundDriver;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "创建人姓名")
|
||||
private String createUserName;
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
/**
|
||||
* 更新人姓名
|
||||
*/
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+26
@@ -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;
|
||||
}
|
||||
+28
@@ -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<WaybillImportRowErrorVO> errors = new ArrayList<>();
|
||||
}
|
||||
+4
@@ -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;
|
||||
|
||||
+15
-2
@@ -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<String> processSubmit(@Validated @RequestBody MKProcessCreateDTO param) {
|
||||
return R.data(businessProcessService.processSubmit(param));
|
||||
}
|
||||
|
||||
@GetMapping("/hasMkProcessInstance")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "查询MK流程实例是否存在", description = "传入业务主键bizId,有数据则无需删除/重新提交")
|
||||
public R<Boolean> 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<Boolean> 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")
|
||||
|
||||
+16
@@ -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<MkFormBindVO> resolveByFormInstanceId(@RequestParam("formInstanceId") Long formInstanceId) {
|
||||
return FR.data(businessProcessService.resolveByFormInstanceId(formInstanceId));
|
||||
}
|
||||
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@PostMapping(MARK_PROCESS_FINISHED)
|
||||
@Override
|
||||
public FR<Boolean> markProcessFinished(@RequestParam("processInstanceId") String processInstanceId,
|
||||
@RequestParam("approved") boolean approved) {
|
||||
return FR.data(businessProcessService.markProcessFinished(processInstanceId, approved));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
<resultMap id="businessProcessResultMap" type="org.springblade.process.pojo.entity.BusinessProcess">
|
||||
<result column="id" property="id"/>
|
||||
<result column="biz_id" property="bizId"/>
|
||||
<result column="form_instance_id" property="formInstanceId"/>
|
||||
<result column="process_instance_id" property="processInstanceId"/>
|
||||
<result column="process_type" property="processType"/>
|
||||
<result column="doc_code" property="docCode"/>
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<MkFormBind> {
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.springblade.process.mapper.MkFormBindMapper">
|
||||
|
||||
<resultMap id="mkFormBindResultMap" type="org.springblade.process.pojo.entity.MkFormBind">
|
||||
<result column="id" property="id"/>
|
||||
<result column="tenant_id" property="tenantId"/>
|
||||
<result column="biz_type" property="bizType"/>
|
||||
<result column="biz_id" property="bizId"/>
|
||||
<result column="form_instance_id" property="formInstanceId"/>
|
||||
<result column="process_instance_id" property="processInstanceId"/>
|
||||
<result column="is_current" property="isCurrent"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
</mapper>
|
||||
+29
-4
@@ -29,18 +29,43 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
|
||||
/**
|
||||
* 直接调用 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);
|
||||
|
||||
/**
|
||||
* 获取流程当前节点详情
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<MkFormBind> {
|
||||
|
||||
/**
|
||||
* 查询业务当前生效的表单实例绑定
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
+235
-24
@@ -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 ServiceImpl<BusinessProcessMappe
|
||||
private final IUserService userService;
|
||||
private final ICustomerArchiveClient customerArchiveClient;
|
||||
private final IMkProcessClient mkProcessClient;
|
||||
private final IMkFormBindService mkFormBindService;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
@@ -129,7 +131,6 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
@Override
|
||||
public String processSubmit(MKProcessCreateDTO param) {
|
||||
AssertUtils.notNull(param, "提交流程参数不能为空");
|
||||
AssertUtils.notBlank(param.getFormInstanceId(), "表单实例id不能为空");
|
||||
AssertUtils.notBlank(param.getTemplateCode(), "模板编码不能为空");
|
||||
// 前端拿到的手机号可能经 @Sensitive 脱敏(如 137****8880),这里从库取真实手机号覆盖
|
||||
String loginName = resolveCurrentUserPhone();
|
||||
@@ -137,8 +138,47 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
param.setSubmitIdentity(loginName);
|
||||
param.setLoginName(loginName);
|
||||
String bizType = param.getBizType();
|
||||
|
||||
Long bizId = resolveBizIdFromSubmitParam(param);
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.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 ServiceImpl<BusinessProcessMappe
|
||||
String processInstanceId = mkService.processSubmit(mkParam);
|
||||
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
|
||||
|
||||
Long bizId;
|
||||
try {
|
||||
bizId = Long.valueOf(param.getFormInstanceId());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ServiceException("表单实例id格式不正确");
|
||||
}
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.eq(BusinessProcess::getBizId, bizId)
|
||||
);
|
||||
if (businessProcess == null) {
|
||||
businessProcess = new BusinessProcess();
|
||||
businessProcess.setBizId(bizId);
|
||||
@@ -177,30 +208,151 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
businessProcess.setSubmitTime(new Date());
|
||||
}
|
||||
}
|
||||
businessProcess.setFormInstanceId(formInstanceId);
|
||||
businessProcess.setProcessInstanceId(processInstanceId);
|
||||
businessProcess.setApproveStatus(ApproveStatusEnum.APPROVING.getValue());
|
||||
businessProcess.setIsCompleted(0);
|
||||
businessProcess.setCompleteTime(null);
|
||||
this.saveOrUpdate(businessProcess);
|
||||
mkFormBindService.updateProcessInstanceId(formInstanceId, processInstanceId);
|
||||
this.getCurrentNodes(processInstanceId, loginName);
|
||||
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
|
||||
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
|
||||
this.syncBizFromProcessInfo(bizType, String.valueOf(bizId), processInfo);
|
||||
return processInstanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从提交参数解析业务主键:优先 bizId,兼容旧调用把业务id放在 formInstanceId
|
||||
*/
|
||||
private Long resolveBizIdFromSubmitParam(MKProcessCreateDTO param) {
|
||||
String bizIdText = param.getBizId();
|
||||
if (StringUtil.isBlank(bizIdText)) {
|
||||
bizIdText = param.getFormInstanceId();
|
||||
}
|
||||
AssertUtils.notBlank(bizIdText, "业务id不能为空");
|
||||
try {
|
||||
return Long.valueOf(bizIdText);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ServiceException("业务id格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要强制新建表单实例:本地已通过/已完结,或 MK 流程已结束
|
||||
*/
|
||||
private boolean shouldForceNewFormInstance(BusinessProcess businessProcess, String loginName) {
|
||||
if (businessProcess == null) {
|
||||
return false;
|
||||
}
|
||||
if (Objects.equals(ApproveStatusEnum.APPROVED.getValue(), businessProcess.getApproveStatus())) {
|
||||
return true;
|
||||
}
|
||||
if (businessProcess.getIsCompleted() != null && businessProcess.getIsCompleted() == 1) {
|
||||
return true;
|
||||
}
|
||||
if (StringUtil.isBlank(businessProcess.getProcessInstanceId())) {
|
||||
return false;
|
||||
}
|
||||
String resolvedLoginName = loginName;
|
||||
if (StringUtil.isBlank(resolvedLoginName)) {
|
||||
resolvedLoginName = businessProcess.getPromoterLoginName();
|
||||
}
|
||||
if (StringUtil.isBlank(resolvedLoginName)) {
|
||||
resolvedLoginName = resolveCurrentUserPhone();
|
||||
}
|
||||
if (StringUtil.isBlank(resolvedLoginName)) {
|
||||
return false;
|
||||
}
|
||||
Object processInfo = this.getProcessInfo(businessProcess.getProcessInstanceId(), resolvedLoginName);
|
||||
return isMkProcessFinished(processInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMkProcessInstance(String bizId) {
|
||||
AssertUtils.notBlank(bizId, "业务id不能为空");
|
||||
Long id;
|
||||
try {
|
||||
id = Long.valueOf(bizId);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ServiceException("业务id格式不正确");
|
||||
}
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.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.<BusinessProcess>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 ServiceImpl<BusinessProcessMappe
|
||||
loginName = resolveCurrentUserPhone();
|
||||
}
|
||||
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
|
||||
log.info("调用mk processDelete processInstanceId={} loginName={} formInstanceId={}",
|
||||
businessProcess.getProcessInstanceId(), loginName, formInstanceId);
|
||||
log.info("调用mk processDelete processInstanceId={} loginName={} bizId={} formInstanceId={}",
|
||||
businessProcess.getProcessInstanceId(), loginName, bizId, businessProcess.getFormInstanceId());
|
||||
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
|
||||
if (!deleted) {
|
||||
throw new ServiceException("删除MK流程失败");
|
||||
@@ -217,6 +369,59 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MkFormBindVO resolveByFormInstanceId(Long formInstanceId) {
|
||||
AssertUtils.notNull(formInstanceId, "表单实例id不能为空");
|
||||
MkFormBindVO bindVO = mkFormBindService.resolveByFormInstanceId(formInstanceId);
|
||||
if (bindVO != null) {
|
||||
return bindVO;
|
||||
}
|
||||
// 兼容未迁移历史:formInstanceId 曾等于 bizId
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.eq(BusinessProcess::getBizId, formInstanceId)
|
||||
.last("limit 1")
|
||||
);
|
||||
if (businessProcess == null) {
|
||||
businessProcess = this.getOne(Wrappers.<BusinessProcess>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 ServiceImpl<BusinessProcessMappe
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前登录用户实体读取真实手机号(绕过接口返回脱敏)
|
||||
* 从当前登录用户实体读取真实手机号(库内 SM2 密文先解密,绕过接口脱敏)
|
||||
*/
|
||||
private String resolveCurrentUserPhone() {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (userId != null) {
|
||||
User user = userService.getById(userId);
|
||||
if (user != null && StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
|
||||
return user.getPhone().trim();
|
||||
}
|
||||
if (user != null && StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
|
||||
return user.getAccount().trim();
|
||||
if (user != null) {
|
||||
userService.decryptPhone(user);
|
||||
if (StringUtil.isNotBlank(user.getPhone()) && !user.getPhone().contains("*")) {
|
||||
return user.getPhone().trim();
|
||||
}
|
||||
if (StringUtil.isNotBlank(user.getAccount()) && user.getAccount().matches("^1\\d{10}$")) {
|
||||
return user.getAccount().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
String account = AuthUtil.getUserAccount();
|
||||
@@ -488,13 +696,16 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
|
||||
if (processInstanceIdBlank) {
|
||||
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
|
||||
.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) {
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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<MkFormBindMapper, MkFormBind> 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.<MkFormBind>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.<MkFormBind>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.<MkFormBind>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.<BusinessProcess>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;
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -157,10 +157,21 @@ public class AirportMasterController extends BladeController {
|
||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||
}
|
||||
List<AirportMasterExcel> 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("操作成功");
|
||||
}
|
||||
|
||||
+31
-5
@@ -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<CargoTypeExcel> data = ExcelUtil.read(file, CargoTypeExcel.class);
|
||||
List<CargoTypeImportFailureExcel> 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("导入数据成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
|
||||
* <p>
|
||||
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
|
||||
*/
|
||||
private void exportFailure(HttpServletResponse response, List<?> failureList) {
|
||||
ImportFailureExcelUtil.exportFailureReasonOnly(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出货物类型
|
||||
*/
|
||||
|
||||
+21
-3
@@ -149,10 +149,28 @@ public class CurrencyController extends BladeController {
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导入币种汇率", description = "传入excel")
|
||||
public R importCurrency(MultipartFile file, HttpServletResponse response) {
|
||||
List<CurrencyExcel> 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("操作成功");
|
||||
}
|
||||
|
||||
+2
-3
@@ -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("操作失败");
|
||||
|
||||
+22
-5
@@ -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<FeeItemExcel> data = ExcelUtil.read(file, FeeItemExcel.class);
|
||||
List<FeeItemImportFailureExcel> 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("导入数据成功");
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+14
-3
@@ -160,10 +160,21 @@ public class RailwayStationController extends BladeController {
|
||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||
}
|
||||
List<RailwayStationExcel> 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("操作成功");
|
||||
}
|
||||
|
||||
+22
-3
@@ -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<RegionExcel> 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("操作成功");
|
||||
}
|
||||
|
||||
+3
@@ -102,6 +102,9 @@ public class AirportMasterExportExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public class CurrencyExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -50,6 +50,9 @@ public class FeeItemExportExcel implements Serializable {
|
||||
@ExcelProperty("组织")
|
||||
private String createDeptName;
|
||||
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
|
||||
+3
@@ -80,6 +80,9 @@ public class PortTerminalExportExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
|
||||
+3
@@ -92,6 +92,9 @@ public class RailwayStationExportExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
|
||||
+13
@@ -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<Dept> {
|
||||
*/
|
||||
List<DeptVO> lazyTree(String tenantId, Long parentId);
|
||||
|
||||
/**
|
||||
* 按组织 ID 集合查询树节点(用于数据权限过滤后的根节点回显)
|
||||
*
|
||||
* @param tenantId 租户ID,空表示不限
|
||||
* @param ids 需要返回的组织 ID
|
||||
* @param childIds 计算 hasChildren 时允许计入的子组织 ID;为空则 hasChildren 恒为 false
|
||||
* @return 树节点列表
|
||||
*/
|
||||
List<DeptVO> treeNodesByIds(@Param("tenantId") String tenantId,
|
||||
@Param("ids") List<Long> ids,
|
||||
@Param("childIds") List<Long> childIds);
|
||||
|
||||
/**
|
||||
* 获取部门名
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<result column="leader_id" property="leaderId"/>
|
||||
<result column="dept_category" property="deptCategory"/>
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
@@ -32,6 +33,7 @@
|
||||
<result column="leader_id" property="leaderId"/>
|
||||
<result column="dept_category" property="deptCategory"/>
|
||||
<result column="dept_code" property="deptCode"/>
|
||||
<result column="kingdee_dept_code" property="kingdeeDeptCode"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="pinyin_mnemonic" property="pinyinMnemonic"/>
|
||||
<result column="mnemonic_code" property="mnemonicCode"/>
|
||||
@@ -115,6 +117,47 @@
|
||||
ORDER BY dept.sort
|
||||
</select>
|
||||
|
||||
<select id="treeNodesByIds" resultMap="treeNodeResultMap">
|
||||
SELECT
|
||||
dept.id,
|
||||
dept.parent_id,
|
||||
dept.dept_name AS title,
|
||||
dept.id AS "value",
|
||||
dept.id AS "key",
|
||||
(
|
||||
SELECT
|
||||
CASE WHEN count(1) > 0 THEN 1 ELSE 0 END
|
||||
FROM
|
||||
blade_dept child
|
||||
WHERE
|
||||
child.parent_id = dept.id
|
||||
AND child.is_deleted = 0
|
||||
<choose>
|
||||
<when test="childIds != null and childIds.size() > 0">
|
||||
AND child.id IN
|
||||
<foreach collection="childIds" item="childId" open="(" close=")" separator=",">
|
||||
#{childId}
|
||||
</foreach>
|
||||
</when>
|
||||
<otherwise>
|
||||
AND 1 = 0
|
||||
</otherwise>
|
||||
</choose>
|
||||
) AS "has_children"
|
||||
FROM
|
||||
blade_dept dept
|
||||
WHERE
|
||||
dept.is_deleted = 0
|
||||
AND dept.id IN
|
||||
<foreach collection="ids" item="id" open="(" close=")" separator=",">
|
||||
#{id}
|
||||
</foreach>
|
||||
<if test="tenantId != null and tenantId != ''">
|
||||
AND dept.tenant_id = #{tenantId}
|
||||
</if>
|
||||
ORDER BY dept.sort, dept.id
|
||||
</select>
|
||||
|
||||
<select id="getDeptNames" resultType="java.lang.String">
|
||||
SELECT
|
||||
dept_name
|
||||
|
||||
+5
-3
@@ -68,10 +68,10 @@ public interface IDeptService extends IService<Dept> {
|
||||
List<DeptVO> lazyList(String tenantId, Long parentId, Map<String, Object> param);
|
||||
|
||||
/**
|
||||
* 树形结构(含超管判定 + 当前租户隔离)
|
||||
* 树形结构(含超管判定 + 当前租户隔离 + 用户组织数据权限)
|
||||
* <p>
|
||||
* 仅超级管理员可指定任意 tenantId 查询;其他用户传入的 tenantId 一律被忽略,
|
||||
* 强制使用当前会话租户。
|
||||
* 强制使用当前会话租户,并按用户「数据权限范围」「数据层级范围」过滤可见组织。
|
||||
*
|
||||
* @param tenantId 入参 tenantId(仅超管生效)
|
||||
* @return 部门树
|
||||
@@ -79,9 +79,11 @@ public interface IDeptService extends IService<Dept> {
|
||||
List<DeptVO> tree(String tenantId);
|
||||
|
||||
/**
|
||||
* 懒加载树形结构(含超管判定 + 当前租户隔离)
|
||||
* 懒加载树形结构(含超管判定 + 当前租户隔离 + 用户组织数据权限)
|
||||
* <p>
|
||||
* 仅超级管理员可指定任意 tenantId,空值表示查询全部;其他用户强制使用当前会话租户。
|
||||
* 非超管按用户「数据权限范围」「数据层级范围」过滤可见组织:
|
||||
* 仅所属组织 / 自定义组织,并可选择是否包含无限下级。
|
||||
*
|
||||
* @param tenantId 入参 tenantId(仅超管生效)
|
||||
* @param parentId 父级部门ID
|
||||
|
||||
+5
-3
@@ -127,7 +127,7 @@ public class AirportMasterServiceImpl extends BaseServiceImpl<AirportMasterMappe
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AirportMasterExcel> importAirportMaster(List<AirportMasterExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
List<AirportMasterExcel> errorList = new ArrayList<>();
|
||||
List<AirportMaster> airportMasterList = new ArrayList<>();
|
||||
@@ -160,8 +160,9 @@ public class AirportMasterServiceImpl extends BaseServiceImpl<AirportMasterMappe
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return errorList;
|
||||
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new org.springblade.common.excel.ImportFailureException(data);
|
||||
}
|
||||
for (AirportMaster airportMaster : airportMasterList) {
|
||||
// 编码命中逻辑删除记录时复用原主键,恢复后更新,避免唯一索引冲突。
|
||||
@@ -322,6 +323,7 @@ public class AirportMasterServiceImpl extends BaseServiceImpl<AirportMasterMappe
|
||||
excel.setLatitude(scaleCoordinate(airportMaster.getLatitude(), MIN_LATITUDE, MAX_LATITUDE));
|
||||
excel.setDataSource(normalizeDataSource(airportMaster.getDataSource()));
|
||||
excel.setStatusName(Objects.equals(airportMaster.getStatus(), STATUS_ENABLED) ? "启用" : "停用");
|
||||
excel.setCreateUserName(UserCache.getUserRealName(airportMaster.getCreateUser()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(airportMaster.getUpdateUser()));
|
||||
return excel;
|
||||
}).toList();
|
||||
|
||||
+11
-1
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.springblade.common.excel.ImportFailureException;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -134,10 +135,14 @@ public class CargoTypeServiceImpl extends BaseServiceImpl<CargoTypeMapper, Cargo
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CargoTypeImportFailureExcel> importCargoType(List<CargoTypeExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
|
||||
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
|
||||
List<CargoTypeImportFailureExcel> failureList = new ArrayList<>();
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
CargoTypeExcel excel = data.get(index);
|
||||
@@ -152,6 +157,11 @@ public class CargoTypeServiceImpl extends BaseServiceImpl<CargoTypeMapper, Cargo
|
||||
failureList.add(toImportFailureExcel(excel, message));
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
// 抛出携带失败明细的异常,触发事务回滚。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new ImportFailureException(data);
|
||||
}
|
||||
return failureList;
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -115,10 +115,16 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CurrencyExcel> importCurrency(List<CurrencyExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
// 注意:循环内除 save 外还有 rebuildEnabledValidity 这个派生写——
|
||||
// 若吞掉异常正常 return,主表数据和派生的有效期区间都会落库;
|
||||
// 必须抛异常让两者一起回滚,否则会出现「主表回滚了、有效期区间
|
||||
// 却按失败批次算过」的脏数据。
|
||||
List<CurrencyExcel> errorList = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
@@ -140,6 +146,11 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
|
||||
errorList.add(excel);
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
// 抛出携带失败明细的异常,触发事务回滚(含 rebuildEnabledValidity 的派生写)。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new org.springblade.common.excel.ImportFailureException(data);
|
||||
}
|
||||
return errorList;
|
||||
}
|
||||
|
||||
|
||||
+154
-2
@@ -44,8 +44,10 @@ import org.springblade.core.tool.node.ForestNodeMerger;
|
||||
import org.springblade.core.tool.utils.*;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.mapper.DeptMapper;
|
||||
import org.springblade.system.mapper.UserDataScopeMapper;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.entity.User;
|
||||
import org.springblade.system.pojo.entity.UserDataScope;
|
||||
import org.springblade.system.pojo.enums.DeptCategory;
|
||||
import org.springblade.system.pojo.vo.DeptVO;
|
||||
import org.springblade.system.pojo.vo.UserVO;
|
||||
@@ -90,8 +92,19 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
private static final String IAM_SYNC_TENANT_ID = "000000";
|
||||
private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L;
|
||||
private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
/** 数据权限范围:仅所属组织 */
|
||||
private static final int DATA_SCOPE_OWN = 1;
|
||||
/** 数据权限范围:全部组织 */
|
||||
private static final int DATA_SCOPE_ALL = 2;
|
||||
/** 数据权限范围:自定义 */
|
||||
private static final int DATA_SCOPE_CUSTOM = 3;
|
||||
/** 数据层级范围:包含下级 */
|
||||
private static final int DATA_LEVEL_INCLUDE_CHILDREN = 1;
|
||||
/** 数据层级范围:仅本级 */
|
||||
private static final int DATA_LEVEL_CURRENT_ONLY = 2;
|
||||
|
||||
private final IUserService userService;
|
||||
private final UserDataScopeMapper userDataScopeMapper;
|
||||
private final IamSyncProperties iamSyncProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -144,7 +157,11 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
String resolvedTenantId = AuthUtil.isAdministrator()
|
||||
? Func.toStrWithEmpty(tenantId, AuthUtil.getTenantId())
|
||||
: AuthUtil.getTenantId();
|
||||
return ForestNodeMerger.merge(baseMapper.tree(resolvedTenantId));
|
||||
DeptDataScopeFilter scopeFilter = resolveDeptDataScopeFilter();
|
||||
if (scopeFilter == null || scopeFilter.allDept()) {
|
||||
return ForestNodeMerger.merge(baseMapper.tree(resolvedTenantId));
|
||||
}
|
||||
return buildScopedDeptTree(resolvedTenantId, scopeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,7 +170,142 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
String resolvedTenantId = AuthUtil.isAdministrator()
|
||||
? Func.toStr(tenantId, StringPool.EMPTY)
|
||||
: AuthUtil.getTenantId();
|
||||
return ForestNodeMerger.merge(baseMapper.lazyTree(resolvedTenantId, parentId));
|
||||
Long resolvedParentId = parentId == null ? BladeConstant.TOP_PARENT_ID : parentId;
|
||||
DeptDataScopeFilter scopeFilter = resolveDeptDataScopeFilter();
|
||||
if (scopeFilter == null || scopeFilter.allDept()) {
|
||||
return ForestNodeMerger.merge(baseMapper.lazyTree(resolvedTenantId, resolvedParentId));
|
||||
}
|
||||
Set<Long> allowedIds = scopeFilter.allowedDeptIds();
|
||||
if (Func.isEmpty(allowedIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
boolean includeChildren = scopeFilter.includeChildren();
|
||||
List<Long> childIdsForHasChildren = includeChildren ? new ArrayList<>(allowedIds) : Collections.emptyList();
|
||||
// 根层:返回「可见根」——权限集合中父节点不在集合内的组织,避免用户看不到深层权限节点。
|
||||
if (Objects.equals(resolvedParentId, BladeConstant.TOP_PARENT_ID)) {
|
||||
List<Long> visibleRootIds = resolveVisibleRootIds(allowedIds);
|
||||
if (Func.isEmpty(visibleRootIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return ForestNodeMerger.merge(baseMapper.treeNodesByIds(resolvedTenantId, visibleRootIds, childIdsForHasChildren));
|
||||
}
|
||||
// 非根层:无权限的父节点不允许展开;仅本级时不允许继续下钻。
|
||||
if (!includeChildren || !allowedIds.contains(resolvedParentId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<DeptVO> children = baseMapper.lazyTree(resolvedTenantId, resolvedParentId);
|
||||
List<Long> filteredChildIds = children.stream()
|
||||
.map(DeptVO::getId)
|
||||
.filter(allowedIds::contains)
|
||||
.toList();
|
||||
if (Func.isEmpty(filteredChildIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return ForestNodeMerger.merge(baseMapper.treeNodesByIds(resolvedTenantId, filteredChildIds, childIdsForHasChildren));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数据权限构建完整组织树(用于 /dept/tree)。
|
||||
*/
|
||||
private List<DeptVO> buildScopedDeptTree(String tenantId, DeptDataScopeFilter scopeFilter) {
|
||||
Set<Long> allowedIds = scopeFilter.allowedDeptIds();
|
||||
if (Func.isEmpty(allowedIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> childIdsForHasChildren = scopeFilter.includeChildren()
|
||||
? new ArrayList<>(allowedIds)
|
||||
: Collections.emptyList();
|
||||
return ForestNodeMerger.merge(baseMapper.treeNodesByIds(tenantId, new ArrayList<>(allowedIds), childIdsForHasChildren));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算懒加载首层可见根:父级不在权限集合内的组织。
|
||||
*/
|
||||
private List<Long> resolveVisibleRootIds(Set<Long> allowedIds) {
|
||||
return listByIds(allowedIds).stream()
|
||||
.filter(dept -> dept.getParentId() == null
|
||||
|| Objects.equals(dept.getParentId(), BladeConstant.TOP_PARENT_ID)
|
||||
|| !allowedIds.contains(dept.getParentId()))
|
||||
.sorted(Comparator.comparing(Dept::getSort, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(Dept::getId, Comparator.nullsLast(Long::compareTo)))
|
||||
.map(Dept::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前登录用户的组织树数据权限。
|
||||
* <p>
|
||||
* 超管或「全部组织」返回 allDept=true;其余按所属组织/自定义组织,并结合「包含下级/仅本级」展开。
|
||||
*
|
||||
* @return 过滤上下文;无法识别用户时按空权限处理
|
||||
*/
|
||||
private DeptDataScopeFilter resolveDeptDataScopeFilter() {
|
||||
if (AuthUtil.isAdministrator()) {
|
||||
return DeptDataScopeFilter.all();
|
||||
}
|
||||
Long userId = AuthUtil.getUserId();
|
||||
if (Func.isEmpty(userId) || userId <= 0) {
|
||||
return DeptDataScopeFilter.empty();
|
||||
}
|
||||
User user = userService.getById(userId);
|
||||
if (Func.isEmpty(user)) {
|
||||
return DeptDataScopeFilter.empty();
|
||||
}
|
||||
Integer dataScopeRange = user.getDataScopeRange() == null ? DATA_SCOPE_OWN : user.getDataScopeRange();
|
||||
Integer dataLevelRange = user.getDataLevelRange() == null ? DATA_LEVEL_INCLUDE_CHILDREN : user.getDataLevelRange();
|
||||
if (Objects.equals(dataScopeRange, DATA_SCOPE_ALL)) {
|
||||
return DeptDataScopeFilter.all();
|
||||
}
|
||||
List<Long> baseDeptIds;
|
||||
if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) {
|
||||
baseDeptIds = userDataScopeMapper.selectList(Wrappers.<UserDataScope>lambdaQuery()
|
||||
.eq(UserDataScope::getUserId, userId))
|
||||
.stream()
|
||||
.map(UserDataScope::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
} else {
|
||||
// 仅所属组织:优先用户档案中的所属组织,其次当前会话组织。
|
||||
baseDeptIds = Func.toLongList(user.getDeptId());
|
||||
if (Func.isEmpty(baseDeptIds)) {
|
||||
baseDeptIds = Func.toLongList(AuthUtil.getDeptId());
|
||||
}
|
||||
}
|
||||
if (Func.isEmpty(baseDeptIds)) {
|
||||
return DeptDataScopeFilter.empty();
|
||||
}
|
||||
boolean includeChildren = !Objects.equals(dataLevelRange, DATA_LEVEL_CURRENT_ONLY);
|
||||
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
|
||||
for (Long deptId : baseDeptIds) {
|
||||
if (Func.isEmpty(deptId) || deptId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (includeChildren) {
|
||||
List<Long> childIds = SysCache.getDeptChildIds(deptId);
|
||||
if (Func.isNotEmpty(childIds)) {
|
||||
allowedDeptIds.addAll(childIds);
|
||||
} else {
|
||||
allowedDeptIds.add(deptId);
|
||||
}
|
||||
} else {
|
||||
allowedDeptIds.add(deptId);
|
||||
}
|
||||
}
|
||||
return new DeptDataScopeFilter(false, includeChildren, allowedDeptIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织懒加载树的数据权限过滤上下文。
|
||||
*/
|
||||
private record DeptDataScopeFilter(boolean allDept, boolean includeChildren, Set<Long> allowedDeptIds) {
|
||||
private static DeptDataScopeFilter all() {
|
||||
return new DeptDataScopeFilter(true, true, Collections.emptySet());
|
||||
}
|
||||
|
||||
private static DeptDataScopeFilter empty() {
|
||||
return new DeptDataScopeFilter(false, false, Collections.emptySet());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-1
@@ -106,8 +106,11 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<FeeItemImportFailureExcel> importFeeItem(List<FeeItemExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
// 注意不能只往 failureList 里塞数据就 return——异常被吞掉后方法会正常返回,
|
||||
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
|
||||
List<FeeItemImportFailureExcel> failureList = new ArrayList<>();
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
FeeItemExcel excel = data.get(index);
|
||||
@@ -118,9 +121,18 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
|
||||
save(feeItem);
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
// 除收集到 failureList 外,还必须把原因写回原行对象:
|
||||
// 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因,
|
||||
// 只写在副本上会导致失败明细里错误行的原因为空。
|
||||
excel.setErrorMessage("第" + (index + 2) + "行:" + message);
|
||||
failureList.add(toImportFailureExcel(excel, "第" + (index + 2) + "行:" + message));
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
// 抛出携带失败明细的异常,触发事务回滚。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new org.springblade.common.excel.ImportFailureException(data);
|
||||
}
|
||||
return failureList;
|
||||
}
|
||||
|
||||
@@ -216,6 +228,7 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
|
||||
excel.setCreateDeptName(
|
||||
Func.isEmpty(feeItem.getCreateDept()) ? "" : SysCache.getDeptName(feeItem.getCreateDept())
|
||||
);
|
||||
excel.setCreateUserName(UserCache.getUserRealName(feeItem.getCreateUser()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(feeItem.getUpdateUser()));
|
||||
excel.setStatusName(Objects.equals(feeItem.getStatus(), STATUS_ENABLED) ? "启用" : "停用");
|
||||
return excel;
|
||||
|
||||
+3
-2
@@ -34,8 +34,8 @@ import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.common.excel.ImportFailureException;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.excel.ImportFailureException;
|
||||
import org.springblade.system.excel.PortTerminalExcel;
|
||||
import org.springblade.system.excel.PortTerminalExportExcel;
|
||||
import org.springblade.system.mapper.PortTerminalMapper;
|
||||
@@ -140,7 +140,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<PortTerminalExcel> importPortTerminal(List<PortTerminalExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
List<PortTerminalExcel> failureList = new ArrayList<>();
|
||||
@@ -357,6 +357,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
excel.setLatitude(scaleCoordinate(portTerminal.getLatitude(), MIN_LATITUDE, MAX_LATITUDE));
|
||||
excel.setDataSource(normalizeDataSource(portTerminal.getDataSource()));
|
||||
excel.setStatusName(Objects.equals(portTerminal.getStatus(), STATUS_ENABLED) ? "启用" : "停用");
|
||||
excel.setCreateUserName(UserCache.getUserRealName(portTerminal.getCreateUser()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(portTerminal.getUpdateUser()));
|
||||
return excel;
|
||||
}).toList();
|
||||
|
||||
+5
-3
@@ -129,7 +129,7 @@ public class RailwayStationServiceImpl extends BaseServiceImpl<RailwayStationMap
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<RailwayStationExcel> importRailwayStation(List<RailwayStationExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
List<RailwayStationExcel> errorList = new ArrayList<>();
|
||||
List<RailwayStation> railwayStationList = new ArrayList<>();
|
||||
@@ -165,8 +165,9 @@ public class RailwayStationServiceImpl extends BaseServiceImpl<RailwayStationMap
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return errorList;
|
||||
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new org.springblade.common.excel.ImportFailureException(data);
|
||||
}
|
||||
for (RailwayStation railwayStation : railwayStationList) {
|
||||
prepareImportTarget(railwayStation);
|
||||
@@ -348,6 +349,7 @@ public class RailwayStationServiceImpl extends BaseServiceImpl<RailwayStationMap
|
||||
excel.setLatitude(formatCoordinate(railwayStation.getLatitude(), MIN_LATITUDE, MAX_LATITUDE));
|
||||
excel.setDataSource(normalizeDataSource(railwayStation.getDataSource()));
|
||||
excel.setStatusName(Objects.equals(railwayStation.getStatus(), STATUS_ENABLED) ? "启用" : "停用");
|
||||
excel.setCreateUserName(UserCache.getUserRealName(railwayStation.getCreateUser()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(railwayStation.getUpdateUser()));
|
||||
return excel;
|
||||
}).toList();
|
||||
|
||||
+29
-6
@@ -41,6 +41,7 @@ import org.springblade.system.mapper.RegionMapper;
|
||||
import org.springblade.system.service.IRegionService;
|
||||
import org.springblade.system.pojo.vo.RegionVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@@ -60,6 +61,17 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
|
||||
|
||||
@Override
|
||||
public boolean submit(Region region) {
|
||||
return submit(region, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交区划,clearCache 控制是否在保存成功后立即清理惰性树缓存。
|
||||
* <p>
|
||||
* 批量导入路径传 false:导入内部会收集全部错误,只有整批成功才统一清缓存;
|
||||
* 若逐行清缓存,失败批次中已成功行会把缓存清掉/重算成中间状态,
|
||||
* 与随后的事务回滚形成「库回滚了、缓存却是脏的」。
|
||||
*/
|
||||
private boolean submit(Region region, boolean clearCache) {
|
||||
Date now = new Date();
|
||||
Long currentUserId = AuthUtil.getUserId();
|
||||
boolean isNew = StringUtil.isBlank(region.getOriginalCode());
|
||||
@@ -98,6 +110,9 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
|
||||
validateUniqueCode(region);
|
||||
// 设置祖区划编号
|
||||
Region parent = getByCode(region.getParentCode());
|
||||
if (level != null && level > COUNTRY_LEVEL && (Func.isEmpty(parent) || Func.isEmpty(parent.getCode()))) {
|
||||
throw new ServiceException("父区划不存在,请重新选择父区划");
|
||||
}
|
||||
if (Func.isNotEmpty(parent) && Func.isNotEmpty(parent.getCode())) {
|
||||
String ancestors = parent.getRegionLevel() != null && parent.getRegionLevel() == COUNTRY_LEVEL
|
||||
? parent.getCode()
|
||||
@@ -138,7 +153,7 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
|
||||
region.setVillageName(name);
|
||||
}
|
||||
boolean result = StringUtil.isNotBlank(region.getOriginalCode()) ? this.updateById(region) : this.save(region);
|
||||
if (result) {
|
||||
if (result && clearCache) {
|
||||
clearLazyTree();
|
||||
}
|
||||
return result;
|
||||
@@ -189,12 +204,15 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<RegionExcel> importRegion(List<RegionExcel> data, Boolean isCovered) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
|
||||
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
|
||||
List<RegionExcel> errorList = new ArrayList<>();
|
||||
boolean cacheChanged = false;
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
RegionExcel excel = data.get(index);
|
||||
try {
|
||||
@@ -210,15 +228,20 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
|
||||
if (Boolean.TRUE.equals(isCovered) && this.getById(region.getCode()) != null) {
|
||||
region.setOriginalCode(region.getCode());
|
||||
}
|
||||
cacheChanged = this.submit(region) || cacheChanged;
|
||||
// 导入路径不逐行清缓存,缓存只在整批成功后统一清一次。
|
||||
this.submit(region, false);
|
||||
} catch (Exception exception) {
|
||||
excel.setErrorMessage(exception.getMessage());
|
||||
errorList.add(excel);
|
||||
}
|
||||
}
|
||||
if (cacheChanged) {
|
||||
clearLazyTree();
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
// 抛出携带失败明细的异常,触发事务回滚。缓存清理在异常抛出后不可达,
|
||||
// 只会发生在成功路径上——避免「库回滚了、惰性树缓存却按失败批次
|
||||
// 的中间状态被清掉/重算」的脏缓存。
|
||||
throw new org.springblade.common.excel.ImportFailureException(data);
|
||||
}
|
||||
clearLazyTree();
|
||||
return errorList;
|
||||
}
|
||||
|
||||
|
||||
+12
-12
@@ -1130,22 +1130,22 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
|
||||
@Override
|
||||
public void decryptPhone(IPhone user) {
|
||||
// if (user == null) {
|
||||
// return;
|
||||
// }
|
||||
// if (user.getPhone() != null && user.getPhone().length() > 20) {
|
||||
// user.setPhone(SM2Util.decrypt(user.getPhone(), properties.getPublicKey(), properties.getPrivateKey()));
|
||||
// }
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
if (user.getPhone() != null && user.getPhone().length() > 20) {
|
||||
user.setPhone(SM2Util.decrypt(user.getPhone(), properties.getPublicKey(), properties.getPrivateKey()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptPhone(IPhone user) {
|
||||
// if (user == null) {
|
||||
// return;
|
||||
// }
|
||||
// if(StringUtil.isNotBlank(user.getPhone())) {
|
||||
// user.setPhone(Hex.toHexString(SM2Util.encrypt(user.getPhone(),properties.getPublicKey())));
|
||||
// }
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isNotBlank(user.getPhone())) {
|
||||
user.setPhone(Hex.toHexString(SM2Util.encrypt(user.getPhone(), properties.getPublicKey())));
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePlainPassword(String password) {
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ public class AirportMasterWrapper extends BaseEntityWrapper<AirportMaster, Airpo
|
||||
public AirportMasterVO entityVO(AirportMaster airportMaster) {
|
||||
AirportMasterVO airportMasterVO = Objects.requireNonNull(BeanUtil.copyProperties(airportMaster, AirportMasterVO.class));
|
||||
airportMasterVO.setRegionCode(airportMaster.getDistrictCode());
|
||||
airportMasterVO.setCreateUserName(UserCache.getUserRealName(airportMaster.getCreateUser()));
|
||||
airportMasterVO.setUpdateUserName(UserCache.getUserRealName(airportMaster.getUpdateUser()));
|
||||
return airportMasterVO;
|
||||
}
|
||||
|
||||
+9
-5
@@ -30,10 +30,9 @@ import org.springblade.core.tool.constant.BladeConstant;
|
||||
import org.springblade.core.tool.node.ForestNodeMerger;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.system.pojo.enums.DictEnum;
|
||||
import org.springblade.system.pojo.vo.DeptVO;
|
||||
|
||||
import java.util.List;
|
||||
@@ -47,6 +46,11 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public class DeptWrapper extends BaseEntityWrapper<Dept, DeptVO> {
|
||||
|
||||
/**
|
||||
* 组织类型业务字典编码
|
||||
*/
|
||||
private static final String ORGANIZATION_TYPE_CODE = "organization_type";
|
||||
|
||||
public static DeptWrapper build() {
|
||||
return new DeptWrapper();
|
||||
}
|
||||
@@ -60,7 +64,7 @@ public class DeptWrapper extends BaseEntityWrapper<Dept, DeptVO> {
|
||||
Dept parent = SysCache.getDept(dept.getParentId());
|
||||
deptVO.setParentName(parent.getDeptName());
|
||||
}
|
||||
String category = DictCache.getValue(DictEnum.ORG_CATEGORY, dept.getDeptCategory());
|
||||
String category = DictBizCache.getValue(ORGANIZATION_TYPE_CODE, dept.getDeptCategory());
|
||||
deptVO.setDeptCategoryName(category);
|
||||
return deptVO;
|
||||
}
|
||||
@@ -69,7 +73,7 @@ public class DeptWrapper extends BaseEntityWrapper<Dept, DeptVO> {
|
||||
public List<DeptVO> listNodeVO(List<Dept> list) {
|
||||
List<DeptVO> collect = list.stream().map(dept -> {
|
||||
DeptVO deptVO = BeanUtil.copyProperties(dept, DeptVO.class);
|
||||
String category = DictCache.getValue(DictEnum.ORG_CATEGORY, dept.getDeptCategory());
|
||||
String category = DictBizCache.getValue(ORGANIZATION_TYPE_CODE, dept.getDeptCategory());
|
||||
Objects.requireNonNull(deptVO).setDeptCategoryName(category);
|
||||
return deptVO;
|
||||
}).collect(Collectors.toList());
|
||||
@@ -78,7 +82,7 @@ public class DeptWrapper extends BaseEntityWrapper<Dept, DeptVO> {
|
||||
|
||||
public List<DeptVO> listNodeLazyVO(List<DeptVO> list) {
|
||||
List<DeptVO> collect = list.stream().peek(dept -> {
|
||||
String category = DictCache.getValue(DictEnum.ORG_CATEGORY, dept.getDeptCategory());
|
||||
String category = DictBizCache.getValue(ORGANIZATION_TYPE_CODE, dept.getDeptCategory());
|
||||
Objects.requireNonNull(dept).setDeptCategoryName(category);
|
||||
}).collect(Collectors.toList());
|
||||
return ForestNodeMerger.merge(collect);
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ public class FeeItemWrapper extends BaseEntityWrapper<FeeItem, FeeItemVO> {
|
||||
feeItemVO.setCreateDeptName(
|
||||
Func.isEmpty(feeItem.getCreateDept()) ? "" : SysCache.getDeptName(feeItem.getCreateDept())
|
||||
);
|
||||
feeItemVO.setCreateUserName(UserCache.getUserRealName(feeItem.getCreateUser()));
|
||||
feeItemVO.setUpdateUserName(UserCache.getUserRealName(feeItem.getUpdateUser()));
|
||||
return feeItemVO;
|
||||
}
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ public class PortTerminalWrapper extends BaseEntityWrapper<PortTerminal, PortTer
|
||||
public PortTerminalVO entityVO(PortTerminal portTerminal) {
|
||||
PortTerminalVO portTerminalVO = Objects.requireNonNull(BeanUtil.copyProperties(portTerminal, PortTerminalVO.class));
|
||||
portTerminalVO.setRegionCode(portTerminal.getDistrictCode());
|
||||
portTerminalVO.setCreateUserName(UserCache.getUserRealName(portTerminal.getCreateUser()));
|
||||
portTerminalVO.setUpdateUserName(UserCache.getUserRealName(portTerminal.getUpdateUser()));
|
||||
return portTerminalVO;
|
||||
}
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ public class RailwayStationWrapper extends BaseEntityWrapper<RailwayStation, Rai
|
||||
public RailwayStationVO entityVO(RailwayStation railwayStation) {
|
||||
RailwayStationVO railwayStationVO = Objects.requireNonNull(BeanUtil.copyProperties(railwayStation, RailwayStationVO.class));
|
||||
railwayStationVO.setRegionCode(railwayStation.getDistrictCode());
|
||||
railwayStationVO.setCreateUserName(UserCache.getUserRealName(railwayStation.getCreateUser()));
|
||||
railwayStationVO.setUpdateUserName(UserCache.getUserRealName(railwayStation.getUpdateUser()));
|
||||
return railwayStationVO;
|
||||
}
|
||||
|
||||
+21
-3
@@ -128,10 +128,28 @@ public class AccidentRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入事故记录", description = "传入excel")
|
||||
public R importAccidentRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<AccidentRecordExcel> failureList = accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AccidentRecordExcel.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 {
|
||||
accidentRecordService.importAccidentRecord(ExcelUtil.read(file, AccidentRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AccidentRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "事故记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AccidentRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -125,10 +125,28 @@ public class AnnualInspectionRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "导入年检记录", description = "传入excel")
|
||||
public R importAnnualInspectionRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<AnnualInspectionRecordExcel> failureList = annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AnnualInspectionRecordExcel.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 {
|
||||
annualInspectionRecordService.importAnnualInspectionRecord(ExcelUtil.read(file, AnnualInspectionRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AnnualInspectionRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "年检记录导入失败明细" + DateUtil.time(), "导入失败明细", file, AnnualInspectionRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+4
-20
@@ -33,11 +33,9 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -46,6 +44,7 @@ import org.springblade.transport.pojo.entity.CommonAddress;
|
||||
import org.springblade.transport.pojo.vo.CommonAddressRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.CommonAddressVO;
|
||||
import org.springblade.transport.service.ICommonAddressService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -54,7 +53,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 常用地址 控制器
|
||||
@@ -164,15 +162,9 @@ public class CommonAddressController extends BladeController {
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
queryWrapper.in(CommonAddress::getId, Func.toLongList(ids));
|
||||
}
|
||||
if (Objects.equals(commonAddress.getAllDept(), 1)) {
|
||||
if (!AuthUtil.isAdministrator()) {
|
||||
throw new ServiceException("无权导出全部组织常用地址");
|
||||
}
|
||||
} else {
|
||||
queryWrapper.eq(CommonAddress::getDeptId, currentDeptId());
|
||||
}
|
||||
if (Objects.equals(commonAddress.getAllDept(), 1) && Func.isNotEmpty(commonAddress.getDeptId())) {
|
||||
queryWrapper.eq(CommonAddress::getDeptId, commonAddress.getDeptId());
|
||||
List<Long> deptIdList = TransportBusinessSupport.resolveDeptFilterIds(commonAddress.getAllDept(), commonAddress.getDeptId(), "常用地址");
|
||||
if (Func.isNotEmpty(deptIdList)) {
|
||||
queryWrapper.eq(CommonAddress::getDeptId, deptIdList.get(0));
|
||||
}
|
||||
if (Func.isNotEmpty(commonAddress.getAddressName())) {
|
||||
queryWrapper.like(CommonAddress::getAddressName, commonAddress.getAddressName().trim());
|
||||
@@ -198,12 +190,4 @@ public class CommonAddressController extends BladeController {
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
private Long currentDeptId() {
|
||||
Long deptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
if (Func.isEmpty(deptId) || deptId <= 0) {
|
||||
throw new ServiceException("当前用户所属组织为空,无法导出常用地址");
|
||||
}
|
||||
return deptId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
-4
@@ -38,9 +38,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.transport.excel.CommonCargoExcel;
|
||||
import org.springblade.transport.excel.CommonCargoExportExcel;
|
||||
import org.springblade.transport.excel.CommonCargoImportFailureExcel;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springblade.transport.pojo.entity.CommonCargo;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
@@ -110,14 +110,41 @@ public class CommonCargoController extends BladeController {
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "导入常用货物", description = "传入excel")
|
||||
public R importCommonCargo(MultipartFile file, HttpServletResponse response) {
|
||||
List<CommonCargoImportFailureExcel> failureList = commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
ImportFailureExcelUtil.export(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoImportFailureExcel.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 {
|
||||
commonCargoService.importCommonCargo(ExcelUtil.read(file, CommonCargoExcel.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, CommonCargoExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("导入数据成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
|
||||
* <p>
|
||||
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
|
||||
*/
|
||||
private void exportFailure(HttpServletResponse response, List<?> failureList) {
|
||||
ImportFailureExcelUtil.exportFailureReasonOnly(response, "常用货物导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CommonCargoExcel.class);
|
||||
}
|
||||
|
||||
@GetMapping("/export-template")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导出模板")
|
||||
|
||||
+23
-6
@@ -38,6 +38,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.common.excel.ImportFailureException;
|
||||
import org.springblade.transport.excel.CommonRouteExportExcel;
|
||||
import org.springblade.transport.excel.CommonRouteImportExcel;
|
||||
import org.springblade.transport.pojo.entity.CommonRoute;
|
||||
@@ -109,18 +110,34 @@ public class CommonRouteController extends BladeController {
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "导入常用线路", description = "传入excel")
|
||||
public R importCommonRoute(MultipartFile file, HttpServletResponse response) {
|
||||
List<CommonRouteImportExcel> failureList = commonRouteService.importCommonRoute(
|
||||
ExcelUtil.read(file, CommonRouteImportExcel.class)
|
||||
);
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
ImportFailureExcelUtil.export(
|
||||
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 {
|
||||
commonRouteService.importCommonRoute(ExcelUtil.read(file, CommonRouteImportExcel.class));
|
||||
} catch (ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
|
||||
ImportFailureExcelUtil.exportFailureReasonOnly(
|
||||
response,
|
||||
"常用线路导入失败明细" + DateUtil.time(),
|
||||
"导入失败明细",
|
||||
failureList,
|
||||
exception.getFailureList(),
|
||||
CommonRouteImportExcel.class
|
||||
);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "常用线路导入失败明细" + DateUtil.time(), "导入失败明细", file, CommonRouteImportExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("导入数据成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -138,10 +138,28 @@ public class CreditScoreQuantificationController extends BladeController {
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导入评分量化表", description = "传入excel")
|
||||
public R importCreditScoreQuantification(MultipartFile file, HttpServletResponse response) {
|
||||
List<CreditScoreQuantificationExcel> failureList = creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CreditScoreQuantificationExcel.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 {
|
||||
creditScoreQuantificationService.importCreditScoreQuantification(ExcelUtil.read(file, CreditScoreQuantificationExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一分组失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CreditScoreQuantificationExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "评分量化表导入失败明细" + DateUtil.time(), "导入失败明细", file, CreditScoreQuantificationExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+7
-4
@@ -38,6 +38,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -220,10 +221,12 @@ public class CustomerArchiveController extends BladeController {
|
||||
LambdaQueryWrapper<CustomerArchive> queryWrapper = Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.eq(CustomerArchive::getIsDeleted, 0)
|
||||
.orderByDesc(CustomerArchive::getCreateTime);
|
||||
Long userId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
|
||||
+ " AND bucs.customer_id = blade_customer_archive.id"));
|
||||
if (!TransportBusinessSupport.isAdministrator()) {
|
||||
Long userId = AuthUtil.getUserId();
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.exists("SELECT 1 FROM blade_user_customer_scope bucs WHERE bucs.user_id = " + userId
|
||||
+ " AND bucs.customer_id = blade_customer_archive.id"));
|
||||
}
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
queryWrapper.in(CustomerArchive::getId, Func.toLongList(ids));
|
||||
}
|
||||
|
||||
+26
-3
@@ -30,12 +30,16 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.process.feign.IBusinessProcessClient;
|
||||
import org.springblade.process.pojo.vo.MkFormBindVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
@@ -65,15 +69,34 @@ public class CustomerArchivePublicController {
|
||||
|
||||
private final ICustomerArchiveService customerArchiveService;
|
||||
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
|
||||
private final IBusinessProcessClient businessProcessClient;
|
||||
|
||||
/**
|
||||
* 公开详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "公开详情", description = "传入id,无需登录")
|
||||
public R<CustomerArchiveVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(customerArchiveService.publicDetail(id));
|
||||
@Operation(summary = "公开详情", description = "传入表单实例id,无需登录;内部反查业务主键后加载详情")
|
||||
public R<CustomerArchiveVO> detail(@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
|
||||
return R.data(customerArchiveService.publicDetail(resolveBizId(id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
|
||||
*/
|
||||
private Long resolveBizId(Long formInstanceId) {
|
||||
if (formInstanceId == null) {
|
||||
throw new ServiceException("表单实例id不能为空");
|
||||
}
|
||||
try {
|
||||
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
|
||||
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
|
||||
return result.getData().getBizId();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("按表单实例id解析客商业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
|
||||
}
|
||||
return formInstanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+21
-3
@@ -90,10 +90,28 @@ public class EquipmentLedgerController extends BladeController {
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "导入设备台账")
|
||||
public R importEquipmentLedger(MultipartFile file, HttpServletResponse response) {
|
||||
List<EquipmentLedgerExcel> failureList = equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EquipmentLedgerExcel.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 {
|
||||
equipmentLedgerService.importEquipmentLedger(ExcelUtil.read(file, EquipmentLedgerExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EquipmentLedgerExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "设备台账导入失败明细" + DateUtil.time(), "导入失败明细", file, EquipmentLedgerExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -94,10 +94,28 @@ public class EtcRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入ETC记录", description = "传入excel")
|
||||
public R importEtcRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<EtcRecordExcel> failureList = etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, EtcRecordExcel.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 {
|
||||
etcRecordService.importEtcRecord(ExcelUtil.read(file, EtcRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), EtcRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "ETC记录导入失败明细" + DateUtil.time(), "导入失败明细", file, EtcRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -129,10 +129,28 @@ public class InsuranceRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入保险记录", description = "传入excel")
|
||||
public R importInsuranceRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<InsuranceRecordExcel> failureList = insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, InsuranceRecordExcel.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 {
|
||||
insuranceRecordService.importInsuranceRecord(ExcelUtil.read(file, InsuranceRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), InsuranceRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "保险记录导入失败明细" + DateUtil.time(), "导入失败明细", file, InsuranceRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -128,10 +128,28 @@ public class MaintenancePlanController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入保养记录", description = "传入excel")
|
||||
public R importMaintenancePlan(MultipartFile file, HttpServletResponse response) {
|
||||
List<MaintenancePlanExcel> failureList = maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenancePlanExcel.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 {
|
||||
maintenancePlanService.importMaintenancePlan(ExcelUtil.read(file, MaintenancePlanExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenancePlanExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "保养计划导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenancePlanExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -128,10 +128,28 @@ public class MaintenanceRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入维修记录", description = "传入excel")
|
||||
public R importMaintenanceRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<MaintenanceRecordExcel> failureList = maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MaintenanceRecordExcel.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 {
|
||||
maintenanceRecordService.importMaintenanceRecord(ExcelUtil.read(file, MaintenanceRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MaintenanceRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "维修记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MaintenanceRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -116,10 +116,28 @@ public class MileageRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入里程记录", description = "传入excel")
|
||||
public R importMileageRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<MileageRecordExcel> failureList = mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, MileageRecordExcel.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 {
|
||||
mileageRecordService.importMileageRecord(ExcelUtil.read(file, MileageRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), MileageRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "里程记录导入失败明细" + DateUtil.time(), "导入失败明细", file, MileageRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+27
-3
@@ -7,10 +7,14 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.process.feign.IBusinessProcessClient;
|
||||
import org.springblade.process.pojo.vo.MkFormBindVO;
|
||||
import org.springblade.transport.mk.MkProcessMessageService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -35,13 +39,33 @@ import java.util.Map;
|
||||
public class MkProcessPublicController {
|
||||
|
||||
private final MkProcessMessageService mkProcessMessageService;
|
||||
private final IBusinessProcessClient businessProcessClient;
|
||||
|
||||
@GetMapping("/{bizType}/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "公开详情", description = "无需登录")
|
||||
@Operation(summary = "公开详情", description = "无需登录;id 为 MK 表单实例id,内部反查业务主键后加载详情")
|
||||
public R detail(@PathVariable String bizType,
|
||||
@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
|
||||
@Parameter(description = "表单实例id", required = true) @RequestParam Long id) {
|
||||
Long bizId = resolveBizId(id);
|
||||
return R.data(mkProcessMessageService.handler(bizType).publicDetail(bizId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开页传入的 id 为 formInstanceId,反查业务主键;查不到时兼容历史(id 即 bizId)
|
||||
*/
|
||||
private Long resolveBizId(Long formInstanceId) {
|
||||
if (formInstanceId == null) {
|
||||
throw new ServiceException("表单实例id不能为空");
|
||||
}
|
||||
try {
|
||||
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
|
||||
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
|
||||
return result.getData().getBizId();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("按表单实例id解析业务主键失败,回退按业务主键查询 formInstanceId={}", formInstanceId, e);
|
||||
}
|
||||
return formInstanceId;
|
||||
}
|
||||
|
||||
@PostMapping("/{bizType}/process-message")
|
||||
|
||||
+21
-3
@@ -94,10 +94,28 @@ public class OilElectricRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入油电记录", description = "传入excel")
|
||||
public R importOilElectricRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<OilElectricRecordExcel> failureList = oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OilElectricRecordExcel.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 {
|
||||
oilElectricRecordService.importOilElectricRecord(ExcelUtil.read(file, OilElectricRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OilElectricRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "油电记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OilElectricRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -94,10 +94,28 @@ public class OtherExpenseRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入其他费用记录", description = "传入excel")
|
||||
public R importOtherExpenseRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<OtherExpenseRecordExcel> failureList = otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, OtherExpenseRecordExcel.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 {
|
||||
otherExpenseRecordService.importOtherExpenseRecord(ExcelUtil.read(file, OtherExpenseRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), OtherExpenseRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "其他费用记录导入失败明细" + DateUtil.time(), "导入失败明细", file, OtherExpenseRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+23
-1
@@ -51,7 +51,9 @@ import org.springblade.transport.pojo.entity.VoucherManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
||||
import org.springblade.transport.pojo.vo.VoucherFolderVO;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IVoucherManageService;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -80,6 +82,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public class ProcessConfigController extends BladeController {
|
||||
|
||||
private final IProcessConfigService processConfigService;
|
||||
private final IVoucherManageService voucherManageService;
|
||||
private final WaybillMapper waybillMapper;
|
||||
private final VoucherFileMapper voucherFileMapper;
|
||||
private final VoucherImageMapper voucherImageMapper;
|
||||
@@ -88,11 +91,13 @@ public class ProcessConfigController extends BladeController {
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
||||
private String minioBucketName;
|
||||
|
||||
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
||||
public ProcessConfigController(IProcessConfigService processConfigService, IVoucherManageService voucherManageService,
|
||||
WaybillMapper waybillMapper,
|
||||
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
||||
VoucherManageMapper voucherManageMapper,
|
||||
ObjectProvider<MinioClient> minioClientProvider) {
|
||||
this.processConfigService = processConfigService;
|
||||
this.voucherManageService = voucherManageService;
|
||||
this.waybillMapper = waybillMapper;
|
||||
this.voucherFileMapper = voucherFileMapper;
|
||||
this.voucherImageMapper = voucherImageMapper;
|
||||
@@ -128,6 +133,23 @@ public class ProcessConfigController extends BladeController {
|
||||
return R.data(detail);
|
||||
}
|
||||
|
||||
@GetMapping("/voucher-folders")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "查询运单执行凭证文件夹")
|
||||
public R<List<VoucherFolderVO>> voucherFolders(
|
||||
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
|
||||
return R.data(voucherManageService.listWaybillFolders(waybillId));
|
||||
}
|
||||
|
||||
@GetMapping("/voucher-folder-detail")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "查询运单执行凭证文件夹详情")
|
||||
public R<VoucherFolderVO> voucherFolderDetail(
|
||||
@Parameter(description = "凭证批次主键", required = true) @RequestParam Long voucherId,
|
||||
@Parameter(description = "车牌号", required = true) @RequestParam String plateNo) {
|
||||
return R.data(voucherManageService.folderDetail(voucherId, plateNo));
|
||||
}
|
||||
|
||||
@GetMapping("/voucher-images")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "查询运单已关联凭证图片")
|
||||
|
||||
+21
-3
@@ -128,10 +128,28 @@ public class TireReplacementRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入换胎记录", description = "传入excel")
|
||||
public R importTireReplacementRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<TireReplacementRecordExcel> failureList = tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TireReplacementRecordExcel.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 {
|
||||
tireReplacementRecordService.importTireReplacementRecord(ExcelUtil.read(file, TireReplacementRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TireReplacementRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "换胎记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TireReplacementRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -113,10 +113,28 @@ public class TransportChangeRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入变更记录", description = "传入excel")
|
||||
public R importTransportChangeRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<TransportChangeRecordExcel> failureList = transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "变更记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportChangeRecordExcel.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 {
|
||||
transportChangeRecordService.importTransportChangeRecord(ExcelUtil.read(file, TransportChangeRecordExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportChangeRecordExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "运输变更记录导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportChangeRecordExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
+22
-4
@@ -147,11 +147,29 @@ public class TransportPlanController extends BladeController {
|
||||
public R importTransportPlan(MultipartFile file, @RequestParam Long projectId, @RequestParam String projectName,
|
||||
@RequestParam Long contractId, @RequestParam String contractName, @RequestParam String customerName,
|
||||
HttpServletResponse response) {
|
||||
List<TransportPlanImportExcel> failureList = transportPlanService.importTransportPlan(
|
||||
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportPlanImportExcel.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 {
|
||||
transportPlanService.importTransportPlan(
|
||||
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
|
||||
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), TransportPlanImportExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", file, TransportPlanImportExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("导入数据成功");
|
||||
}
|
||||
|
||||
+51
-12
@@ -19,6 +19,7 @@ import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.excel.util.ExcelUtil;
|
||||
import org.springblade.transport.excel.CargoReconciliationExcel;
|
||||
import org.springblade.transport.excel.CargoReconciliationFeeReader;
|
||||
@@ -126,13 +127,32 @@ public class TransportReconciliationController extends BladeController {
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "导入整车总额外部账单")
|
||||
public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
|
||||
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
|
||||
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
|
||||
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
|
||||
List<VehicleReconciliationFailureExcel> failures = reconciliationService.importVehicles(id, rows);
|
||||
if (!failures.isEmpty()) {
|
||||
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.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 {
|
||||
List<VehicleReconciliationExcel> rows = ExcelUtil.read(file, VehicleReconciliationExcel.class);
|
||||
List<Map<String, BigDecimal>> feeItems = VehicleReconciliationFeeReader.read(file);
|
||||
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
|
||||
reconciliationService.importVehicles(id, rows);
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
|
||||
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
|
||||
"导入失败明细", file, exception.getFailureList(), VehicleReconciliationExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, VehicleReconciliationExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("导入数据成功");
|
||||
}
|
||||
@@ -141,13 +161,32 @@ public class TransportReconciliationController extends BladeController {
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "导入货物明细外部账单")
|
||||
public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) {
|
||||
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
|
||||
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
|
||||
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
|
||||
List<CargoReconciliationFailureExcel> failures = reconciliationService.importCargoes(id, rows);
|
||||
if (!failures.isEmpty()) {
|
||||
ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.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 {
|
||||
List<CargoReconciliationExcel> rows = ExcelUtil.read(file, CargoReconciliationExcel.class);
|
||||
List<Map<String, BigDecimal>> feeItems = CargoReconciliationFeeReader.read(file);
|
||||
for (int index = 0; index < rows.size() && index < feeItems.size(); index++) rows.get(index).setFeeItems(feeItems.get(index));
|
||||
reconciliationService.importCargoes(id, rows);
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 按原表真实表头导出全量列(含动态费用),修正后可直接重新导入
|
||||
ImportFailureExcelUtil.exportOriginalWithFailures(response, "运输对账导入失败明细" + DateUtil.time(),
|
||||
"导入失败明细", file, exception.getFailureList(), CargoReconciliationExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoReconciliationExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("导入数据成功");
|
||||
}
|
||||
|
||||
+21
-3
@@ -127,10 +127,28 @@ public class ViolationRecordController extends BladeController {
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "导入违章记录", description = "传入excel")
|
||||
public R importViolationRecord(MultipartFile file, HttpServletResponse response) {
|
||||
List<ViolationRecordImportExcel> failureList = violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", failureList, ViolationRecordImportExcel.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 {
|
||||
violationRecordService.importViolationRecord(ExcelUtil.read(file, ViolationRecordImportExcel.class));
|
||||
} catch (org.springblade.common.excel.ImportFailureException exception) {
|
||||
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红),用户修正后重新导入。
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), ViolationRecordImportExcel.class);
|
||||
return null;
|
||||
} catch (Exception readException) {
|
||||
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
|
||||
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
|
||||
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
|
||||
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
|
||||
response, "违章记录导入失败明细" + DateUtil.time(), "导入失败明细", file, ViolationRecordImportExcel.class, readException)) {
|
||||
return null;
|
||||
}
|
||||
throw readException;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user