Merge remote-tracking branch 'websoft/master'
This commit is contained in:
@@ -22,6 +22,15 @@
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-starter-log</artifactId>
|
||||
<!-- provided:LenientDateParser 编译期需要 ServiceException(位于 starter-log),
|
||||
但 provided 不向下游传递——否则网关会经 common 拿到 blade-core-tool 的
|
||||
BladeConverterConfiguration(implements WebMvcConfigurer),在无 spring-webmvc
|
||||
的 WebFlux 网关里启动即崩。业务服务经 blade-core-boot 自带 starter-log,运行时不受影响。 -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-core-auto</artifactId>
|
||||
|
||||
+133
-5
@@ -46,9 +46,11 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 导入失败明细 Excel 导出工具类
|
||||
@@ -95,7 +97,7 @@ public class ImportFailureExcelUtil {
|
||||
* @param excelClass 原导入 Excel 类型
|
||||
*/
|
||||
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
|
||||
exportFailureReasonOnly(response, fileName, sheetName, data, excelClass);
|
||||
export(response, fileName, sheetName, data, excelClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +111,16 @@ public class ImportFailureExcelUtil {
|
||||
*/
|
||||
public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
|
||||
List<?> data, Class<?> excelClass) {
|
||||
export(response, fileName, sheetName, data, excelClass, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细
|
||||
*
|
||||
* @param markErrorColumns 是否按失败原因里出现的列名,标红对应字段单元格
|
||||
*/
|
||||
private static void export(HttpServletResponse response, String fileName, String sheetName,
|
||||
List<?> data, Class<?> excelClass, boolean markErrorColumns) {
|
||||
response.setContentType("application/vnd.ms-excel");
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||
@@ -118,7 +130,7 @@ public class ImportFailureExcelUtil {
|
||||
List<List<Object>> rows = buildRows(data, excelFields);
|
||||
try {
|
||||
FastExcel.write(response.getOutputStream())
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size()))
|
||||
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
|
||||
.head(head)
|
||||
.sheet(sheetName)
|
||||
.doWrite(rows);
|
||||
@@ -204,15 +216,59 @@ public class ImportFailureExcelUtil {
|
||||
throw new NoSuchFieldException(String.join(",", fieldNames));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本归一化:去掉空白、星号与中英文标点并统一小写
|
||||
* <p>
|
||||
* 目的是让「失败原因里写的列名」与「表头列名」能直接做包含匹配,
|
||||
* 不受「*」「/」「:」等写法差异影响。
|
||||
*/
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.replaceAll("[\\s*_::,,。;;()()\\[\\]【】<>《》//、-]", "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某一列用于定位的候选关键词
|
||||
* <p>
|
||||
* 包含表头列名、字段名,以及列名按分隔符拆出的片段
|
||||
* (如「车牌号/船号」拆出「车牌号」「船号」,失败原因只写其中一段时也能定位)。
|
||||
*/
|
||||
private static List<String> columnKeywords(Field field) {
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
String[] value = excelProperty.value();
|
||||
String columnName = value.length == 0 ? field.getName() : value[0];
|
||||
List<String> keywords = new ArrayList<>();
|
||||
keywords.add(columnName);
|
||||
keywords.add(field.getName());
|
||||
keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[//、()()\\s]+")));
|
||||
return keywords.stream()
|
||||
.map(ImportFailureExcelUtil::normalize)
|
||||
.filter(keyword -> keyword.length() >= 2)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static class ImportFailureCellStyleHandler implements CellWriteHandler {
|
||||
|
||||
private final int failureReasonColumnIndex;
|
||||
private final boolean markErrorColumns;
|
||||
private final List<List<Object>> rows;
|
||||
private final Map<Integer, Set<Integer>> redColumnsByRow = new HashMap<>();
|
||||
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 ImportFailureCellStyleHandler(int failureReasonColumnIndex) {
|
||||
this.failureReasonColumnIndex = failureReasonColumnIndex;
|
||||
private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
|
||||
this.failureReasonColumnIndex = excelFields.size();
|
||||
this.markErrorColumns = markErrorColumns;
|
||||
this.rows = rows;
|
||||
if (markErrorColumns) {
|
||||
List<List<String>> columnKeywords = excelFields.stream()
|
||||
.map(ImportFailureExcelUtil::columnKeywords)
|
||||
.toList();
|
||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
||||
redColumnsByRow.put(rowIndex, resolveRedColumns(rows.get(rowIndex), columnKeywords));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,11 +286,83 @@ public class ImportFailureExcelUtil {
|
||||
return;
|
||||
}
|
||||
adjustColumnWidth(cell);
|
||||
if (cell.getColumnIndex() == failureReasonColumnIndex) {
|
||||
if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
|
||||
return;
|
||||
}
|
||||
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
|
||||
markRed(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某一行需要标红的列
|
||||
* <p>
|
||||
* 失败原因里出现的列名即视为出错列;但当某个命中片段被另一列更长的命中
|
||||
* 片段完全覆盖时(如原因「车船类型不能为空」同时命中「车船类型」和它的
|
||||
* 子串「类型」),只保留更长的那一列,避免把无关列一起标红。
|
||||
*/
|
||||
private Set<Integer> resolveRedColumns(List<Object> row, List<List<String>> columnKeywords) {
|
||||
Set<Integer> redColumns = new LinkedHashSet<>();
|
||||
redColumns.add(failureReasonColumnIndex);
|
||||
String failureReason = normalize(String.valueOf(row.get(failureReasonColumnIndex)));
|
||||
if (failureReason.isEmpty()) {
|
||||
return redColumns;
|
||||
}
|
||||
List<int[]> matches = new ArrayList<>();
|
||||
for (int column = 0; column < columnKeywords.size(); column++) {
|
||||
for (String keyword : columnKeywords.get(column)) {
|
||||
int fromIndex = 0;
|
||||
while (true) {
|
||||
int index = failureReason.indexOf(keyword, fromIndex);
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
matches.add(new int[]{index, index + keyword.length(), column});
|
||||
fromIndex = index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int[] match : matches) {
|
||||
if (!isSubsumed(match, matches)) {
|
||||
redColumns.add(match[2]);
|
||||
}
|
||||
}
|
||||
return redColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断该命中片段是否被另一列更长的命中片段完全覆盖
|
||||
*/
|
||||
private boolean isSubsumed(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 boolean shouldMarkRed(int rowIndex, int columnIndex) {
|
||||
if (columnIndex == failureReasonColumnIndex) {
|
||||
return true;
|
||||
}
|
||||
if (!markErrorColumns) {
|
||||
return false;
|
||||
}
|
||||
Set<Integer> redColumns = redColumnsByRow.get(rowIndex);
|
||||
return redColumns != null && redColumns.contains(columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标红单元格
|
||||
* <p>
|
||||
* 统一只改字体颜色,不加底纹;空单元格的红色字体在屏幕上不可见,
|
||||
* 这类错误依靠失败原因列的文字定位。
|
||||
*/
|
||||
private void markRed(Cell cell) {
|
||||
CellStyle currentStyle = cell.getCellStyle();
|
||||
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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 java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期解析器(口径见根工作区 docs/import-spec.md)。
|
||||
* <p>
|
||||
* 解析规则:
|
||||
* <ul>
|
||||
* <li>分隔符 {@code -}、{@code /}、{@code .} 均接受;补零与否均可(2026-08-02 ≡ 2026-8-2)</li>
|
||||
* <li>日期时间:日期部分同上,时间 HH:mm:ss,可省略秒或秒+分(2026-8-2 12:3 可解析)</li>
|
||||
* <li>拒绝:两位年份(26-8-2)、日在前(2/8/2026)、无分隔符(20260802)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public final class LenientDateParser {
|
||||
|
||||
/**
|
||||
* 日期部分:4 位年 + 分隔符 + 1~2 位月 + 同一分隔符 + 1~2 位日(年在前,拒绝两位年份与日在前)。
|
||||
*/
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4})([-/.])(\\d{1,2})\\2(\\d{1,2})");
|
||||
|
||||
/**
|
||||
* 时间部分:1~2 位时[:1~2 位分[:1~2 位秒]],逐级可省略。
|
||||
*/
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{1,2})(?::(\\d{1,2})(?::(\\d{1,2}))?)?");
|
||||
|
||||
private LenientDateParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本,失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDate parseDateOrNull(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = DATE_PATTERN.matcher(normalized);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.of(Integer.parseInt(matcher.group(1)),
|
||||
Integer.parseInt(matcher.group(3)), Integer.parseInt(matcher.group(4)));
|
||||
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本;允许携带合法时间部分并截断(如 2026-9-1 8:0:0 按 2026-09-01 解析,
|
||||
* 与 fastexcel 内置 LocalDate 转换的既有宽容度保持一致),失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDate parseDateLenientlyOrNull(String value) {
|
||||
LocalDate date = parseDateOrNull(value);
|
||||
if (date != null) {
|
||||
return date;
|
||||
}
|
||||
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||
return dateTime == null ? null : dateTime.toLocalDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间文本;纯日期按当日零点处理,失败返回 {@code null}。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @return 日期时间;无法识别时返回 null
|
||||
*/
|
||||
public static LocalDateTime parseDateTimeOrNull(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
// 拆出日期与时间两部分;中间允许 1 个及以上空白或小写 t(2026-8-2t12:3 亦接受)。
|
||||
String[] parts = normalized.split("[ \\t]+|(?<=\\d)t", 2);
|
||||
if (parts.length == 0) {
|
||||
return null;
|
||||
}
|
||||
LocalDate date = parseDateOrNull(parts[0]);
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
if (parts.length == 1) {
|
||||
return date.atStartOfDay();
|
||||
}
|
||||
Matcher matcher = TIME_PATTERN.matcher(parts[1]);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int hour = Integer.parseInt(matcher.group(1));
|
||||
int minute = matcher.group(2) == null ? 0 : Integer.parseInt(matcher.group(2));
|
||||
int second = matcher.group(3) == null ? 0 : Integer.parseInt(matcher.group(3));
|
||||
return LocalDateTime.of(date, LocalTime.of(hour, minute, second));
|
||||
} catch (NumberFormatException | java.time.DateTimeException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||
* @return 日期
|
||||
*/
|
||||
public static LocalDate parseDate(String value, String columnName) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
LocalDate date = parseDateLenientlyOrNull(value);
|
||||
if (date == null) {
|
||||
throw unrecognizedDate(value, columnName);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间文本,失败抛出携带口径文案的 {@link org.springblade.core.log.exception.ServiceException}。
|
||||
* 空白与 {@code null} 返回 {@code null}(可选字段由业务校验决定是否必填)。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名(用于失败原因文案与失败明细标红定位)
|
||||
* @return 日期时间
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String value, String columnName) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
LocalDateTime dateTime = parseDateTimeOrNull(value);
|
||||
if (dateTime == null) {
|
||||
throw unrecognizedDate(value, columnName);
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按口径构造「日期格式无法识别」错误。
|
||||
*
|
||||
* @param value 单元格原始文本
|
||||
* @param columnName 导入模板列名
|
||||
* @return 业务异常
|
||||
*/
|
||||
public static org.springblade.core.log.exception.ServiceException unrecognizedDate(String value, String columnName) {
|
||||
String displayName = columnName == null || columnName.isBlank() ? "日期" : columnName;
|
||||
return new org.springblade.core.log.exception.ServiceException(
|
||||
displayName + " 日期格式无法识别:" + (value == null ? "" : value.trim()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化输入:去首尾空白,跳过空白值。
|
||||
*/
|
||||
private static String normalize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期转换器(String 承载):Excel 数值日期(序列号)转 ISO 文本,文本原样透传。
|
||||
* <p>
|
||||
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class LenientDateStringConverter implements Converter<String> {
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||
LocalDate date = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||
globalConfiguration.getUse1904windowing()).toLocalDate();
|
||||
return date.toString();
|
||||
}
|
||||
return cellData.getStringValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 批量导入宽容日期时间转换器(String 承载):Excel 数值日期(序列号)转
|
||||
* {@code yyyy-MM-dd HH:mm:ss} 文本,文本原样透传。
|
||||
* <p>
|
||||
* 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
|
||||
* 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
|
||||
* 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class LenientDateTimeStringConverter implements Converter<String> {
|
||||
|
||||
/**
|
||||
* 数值日期序列号转文本的输出格式,与导入模板展示格式保持一致。
|
||||
*/
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
|
||||
LocalDateTime dateTime = DateUtils.getLocalDateTime(cellData.getNumberValue().doubleValue(),
|
||||
globalConfiguration.getUse1904windowing());
|
||||
return dateTime.format(DATE_TIME_FORMATTER);
|
||||
}
|
||||
return cellData.getStringValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
}
|
||||
+16
-3
@@ -43,6 +43,8 @@ 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.common.excel.ImportFailureExcelUtil;
|
||||
import org.springblade.system.excel.ImportFailureException;
|
||||
import org.springblade.system.excel.PortTerminalExcel;
|
||||
import org.springblade.system.excel.PortTerminalExportExcel;
|
||||
import org.springblade.system.excel.PortTerminalImporter;
|
||||
@@ -175,14 +177,25 @@ public class PortTerminalController extends BladeController {
|
||||
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
|
||||
return R.fail("请上传 .xls,.xlsx 标准格式文件");
|
||||
}
|
||||
List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
||||
try {
|
||||
portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
|
||||
} catch (ImportFailureException exception) {
|
||||
// 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
|
||||
exportFailure(response, exception.getFailureList());
|
||||
return null;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
|
||||
* <p>
|
||||
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
|
||||
*/
|
||||
private void exportFailure(HttpServletResponse response, List<?> failureList) {
|
||||
ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出港口码头主数据
|
||||
*/
|
||||
|
||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 币种汇率 Excel
|
||||
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
|
||||
@ExcelProperty("汇率")
|
||||
private BigDecimal exchangeRate;
|
||||
|
||||
@ExcelProperty("生效日期")
|
||||
private LocalDate effectiveDate;
|
||||
@ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String effectiveDate;
|
||||
|
||||
@ExcelProperty("状态")
|
||||
private String statusName;
|
||||
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
|
||||
@ExcelProperty("来源")
|
||||
private String dataSource;
|
||||
|
||||
@ExcelProperty("失效日期")
|
||||
private LocalDate expiryDate;
|
||||
@ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String expiryDate;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.system.excel;
|
||||
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 导入失败异常,携带失败明细用于导出原表并标注错误。
|
||||
* <p>
|
||||
* 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
|
||||
* 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class ImportFailureException extends ServiceException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 失败明细,包含原表全部数据,错误行已标注错误原因。
|
||||
*/
|
||||
private final transient List<?> failureList;
|
||||
|
||||
public ImportFailureException(List<?> failureList) {
|
||||
super("导入失败,已回滚全部数据");
|
||||
this.failureList = failureList;
|
||||
}
|
||||
|
||||
public List<?> getFailureList() {
|
||||
return failureList;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -34,7 +34,6 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* UserExcel
|
||||
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
|
||||
private String postName;
|
||||
|
||||
@ColumnWidth(20)
|
||||
@ExcelProperty("生日")
|
||||
private Date birthday;
|
||||
@ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String birthday;
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@
|
||||
</select>
|
||||
|
||||
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel">
|
||||
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
|
||||
SELECT id, tenant_id, user_type, account, name, real_name, email, phone, DATE_FORMAT(birthday, '%Y-%m-%d') AS birthday, role_id, dept_id, post_id FROM blade_user ${ew.customSqlSegment}
|
||||
</select>
|
||||
|
||||
<select id="selectCustomerOptions" resultType="java.util.HashMap">
|
||||
|
||||
+2
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
|
||||
CurrencyExcel excel = data.get(index);
|
||||
try {
|
||||
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class));
|
||||
currency.setEffectiveDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEffectiveDate(), "生效日期"));
|
||||
currency.setExpiryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpiryDate(), "失效日期"));
|
||||
currency.setDataSource(SOURCE_BATCH);
|
||||
currency.setStatus(STATUS_ENABLED);
|
||||
prepare(currency, SOURCE_BATCH);
|
||||
|
||||
+241
-295
@@ -35,6 +35,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
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;
|
||||
@@ -45,17 +46,19 @@ import org.springblade.system.service.IPortTerminalService;
|
||||
import org.springblade.system.service.IRegionService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@@ -69,12 +72,10 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
|
||||
private static final String CATEGORY_PORT = "港口";
|
||||
private static final String CATEGORY_TERMINAL = "码头";
|
||||
private static final String SOURCE_INITIAL = "初始化录入";
|
||||
private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
|
||||
private static final String SOURCE_INITIAL = "初始化导入";
|
||||
private static final String SOURCE_INITIAL_OLD = "初始导入";
|
||||
private static final String SOURCE_BATCH = "批量导入";
|
||||
private static final String SOURCE_MANUAL = "手动录入";
|
||||
private static final String SOURCE_MANUAL_OLD = "手工导入";
|
||||
private static final String SOURCE_MANUAL = "手工导入";
|
||||
private static final int STATUS_ENABLED = 1;
|
||||
private static final int STATUS_DISABLED = 2;
|
||||
private static final int CODE_MAX_LENGTH = 30;
|
||||
@@ -141,270 +142,209 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
Map<Integer, PortTerminalExcel> errorMap = new TreeMap<>();
|
||||
Map<String, Integer> codeCountMap = buildImportCodeCountMap(data);
|
||||
importPortTerminalByCategory(data, codeCountMap, errorMap, true);
|
||||
importPortTerminalByCategory(data, codeCountMap, errorMap, false);
|
||||
if (Func.isNotEmpty(errorMap)) {
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
List<PortTerminalExcel> failureList = new ArrayList<>();
|
||||
// 批内已占用的编码,用于识别文件内重复数据。
|
||||
Map<String, Integer> occupiedCodeMap = new HashMap<>();
|
||||
// 本文件声明的一级数据(港口)编码集合。
|
||||
// 用于区分两种情况:父港口"漏填"与"父行自身校验失败"。
|
||||
// 后者说明父行已被标红、用户只需改那一行,因此不再连带给码头行报错。
|
||||
Set<String> declaredPortCodeSet = new HashSet<>();
|
||||
// 两阶段导入:先落库一级数据(港口),再落库二级数据(码头),消除对 Excel 行序的依赖。
|
||||
Map<String, PortTerminal> batchPortMap = new LinkedHashMap<>();
|
||||
List<Integer> portIndexList = new ArrayList<>();
|
||||
List<Integer> terminalIndexList = new ArrayList<>();
|
||||
splitByCategory(data, portIndexList, terminalIndexList, occupiedCodeMap, declaredPortCodeSet, failureList);
|
||||
importPorts(data, portIndexList, batchPortMap, failureList);
|
||||
importTerminals(data, terminalIndexList, batchPortMap, declaredPortCodeSet, failureList);
|
||||
if (Func.isNotEmpty(failureList)) {
|
||||
// 抛出携带失败明细的异常,触发事务回滚。
|
||||
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
|
||||
throw new ImportFailureException(data);
|
||||
}
|
||||
return new ArrayList<>(errorMap.values());
|
||||
return failureList;
|
||||
}
|
||||
|
||||
private void importPortTerminalByCategory(List<PortTerminalExcel> data, Map<String, Integer> codeCountMap,
|
||||
Map<Integer, PortTerminalExcel> errorMap, boolean importPort) {
|
||||
/**
|
||||
* 按类型拆分数据行,并完成与阶段无关的基础校验(类型、编码格式、批内重复)。
|
||||
*
|
||||
* @param data 导入数据
|
||||
* @param portIndexList 港口行下标
|
||||
* @param terminalIndexList 码头行下标
|
||||
* @param occupiedCodeMap 编码占用情况,值为首次出现的下标
|
||||
* @param declaredPortCodeSet 本文件声明的港口编码(含编码格式不合法的行)
|
||||
* @param failureList 失败明细
|
||||
*/
|
||||
private void splitByCategory(List<PortTerminalExcel> data, List<Integer> portIndexList, List<Integer> terminalIndexList,
|
||||
Map<String, Integer> occupiedCodeMap, Set<String> declaredPortCodeSet,
|
||||
List<PortTerminalExcel> failureList) {
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
PortTerminalExcel excel = data.get(index);
|
||||
boolean isPort = CATEGORY_PORT.equals(trimToEmpty(excel.getCategory()));
|
||||
if (isPort != importPort) {
|
||||
String category = trimToEmpty(excel.getCategory());
|
||||
boolean categoryValid = CATEGORY_PORT.equals(category) || CATEGORY_TERMINAL.equals(category);
|
||||
if (!categoryValid) {
|
||||
failureList.add(buildFailure(data, index, "类型只能为港口或码头"));
|
||||
continue;
|
||||
}
|
||||
PortTerminal portTerminal = buildImportPortTerminal(excel);
|
||||
portTerminal.setDataSource(SOURCE_BATCH);
|
||||
portTerminal.setStatus(STATUS_ENABLED);
|
||||
normalizeImportPortTerminal(portTerminal);
|
||||
List<String> validationErrors = validateImportPortTerminal(excel, portTerminal, codeCountMap);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
|
||||
errorMap.put(index, excel);
|
||||
// 码头行的「港口编码」既是编号前缀,也是"所属港口"的来源(见 importTerminals)。
|
||||
// 缺失时下游解析不到父港口,而该异常又会被"父行已声明"的抑制逻辑吞掉,
|
||||
// 导致码头被静默丢弃、接口却返回成功——所以必须在这里明确拦下。
|
||||
if (CATEGORY_TERMINAL.equals(category) && Func.isEmpty(trimToEmpty(excel.getPortCode()))) {
|
||||
failureList.add(buildFailure(data, index, "码头必须填写所属港口编码"));
|
||||
continue;
|
||||
}
|
||||
// 只要类型是港口就登记为"本文件已声明",即使它的编码格式不合法:
|
||||
// 这样引用它的码头行不会被连带报错,用户只需修正这一个港口行。
|
||||
if (CATEGORY_PORT.equals(category)) {
|
||||
String declaredCode = resolveImportCode(excel);
|
||||
if (Func.isNotEmpty(declaredCode)) {
|
||||
declaredPortCodeSet.add(declaredCode);
|
||||
}
|
||||
}
|
||||
String code = resolveImportCode(excel);
|
||||
boolean codeValid = CATEGORY_PORT.equals(category)
|
||||
? PORT_CODE_PATTERN.matcher(code).matches()
|
||||
: TERMINAL_CODE_PATTERN.matcher(code).matches();
|
||||
if (!codeValid) {
|
||||
String message = CATEGORY_PORT.equals(category) ? "港口编码为5位大写字母" : "码头编码格式为港口编码-码头标识";
|
||||
failureList.add(buildFailure(data, index, message));
|
||||
continue;
|
||||
}
|
||||
// 批内重复:两行都需要标记,由用户决定保留哪一行。
|
||||
if (occupiedCodeMap.containsKey(code)) {
|
||||
markFailure(data, occupiedCodeMap.get(code), "编码 " + code + " 在文件中重复出现");
|
||||
failureList.add(buildFailure(data, index, "编码 " + code + " 在文件中重复出现"));
|
||||
continue;
|
||||
}
|
||||
occupiedCodeMap.put(code, index);
|
||||
if (CATEGORY_PORT.equals(category)) {
|
||||
portIndexList.add(index);
|
||||
} else {
|
||||
terminalIndexList.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段一:导入港口(一级数据),同时登记到批次内存映射,供码头引用。
|
||||
*/
|
||||
private void importPorts(List<PortTerminalExcel> data, List<Integer> portIndexList, Map<String, PortTerminal> batchPortMap,
|
||||
List<PortTerminalExcel> failureList) {
|
||||
for (Integer index : portIndexList) {
|
||||
PortTerminalExcel excel = data.get(index);
|
||||
try {
|
||||
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
|
||||
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码。
|
||||
portTerminal.setCode(resolveImportCode(excel));
|
||||
portTerminal.setDataSource(SOURCE_BATCH);
|
||||
portTerminal.setStatus(STATUS_ENABLED);
|
||||
prepare(portTerminal, SOURCE_BATCH);
|
||||
validate(portTerminal);
|
||||
prepareImportTarget(portTerminal);
|
||||
if (!saveOrUpdate(portTerminal)) {
|
||||
throw new ServiceException("港口码头保存失败");
|
||||
save(portTerminal);
|
||||
batchPortMap.put(portTerminal.getCode(), portTerminal);
|
||||
} catch (Exception exception) {
|
||||
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段二:导入码头(二级数据),上级港口优先取本批次新增的港口,其次回查数据库。
|
||||
*
|
||||
* @param declaredPortCodeSet 本文件声明的港口编码,用于避免连带误报
|
||||
*/
|
||||
private void importTerminals(List<PortTerminalExcel> data, List<Integer> terminalIndexList, Map<String, PortTerminal> batchPortMap,
|
||||
Set<String> declaredPortCodeSet, List<PortTerminalExcel> failureList) {
|
||||
for (Integer index : terminalIndexList) {
|
||||
PortTerminalExcel excel = data.get(index);
|
||||
try {
|
||||
PortTerminal portTerminal = Objects.requireNonNull(BeanUtil.copyProperties(excel, PortTerminal.class));
|
||||
// 导入模板按"港口编码/码头编码"两列填写,需归并为实体编码;上级港口取本行的港口编码列。
|
||||
portTerminal.setCode(resolveImportCode(excel));
|
||||
portTerminal.setParentCode(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT));
|
||||
portTerminal.setDataSource(SOURCE_BATCH);
|
||||
portTerminal.setStatus(STATUS_ENABLED);
|
||||
prepareTerminal(portTerminal, batchPortMap);
|
||||
validate(portTerminal);
|
||||
save(portTerminal);
|
||||
} catch (ParentPortNotFoundException exception) {
|
||||
// 上级港口就声明在本文件里,只是那一行自己校验失败(已被标红)。
|
||||
// 此时码头行本身没有问题,不再连带报错,避免用户看到"两行都错"的假象。
|
||||
// 注意这里必须与上面 setParentCode 取同一列(港口编码);
|
||||
// 若改用「上级港口编码」,父港口取不到时会被误判为"已声明"而吞掉异常。
|
||||
if (!declaredPortCodeSet.contains(trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT))) {
|
||||
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
|
||||
errorMap.put(index, excel);
|
||||
failureList.add(buildFailure(data, index, resolveMessage(exception)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareImportTarget(PortTerminal portTerminal) {
|
||||
if (Func.isNotEmpty(portTerminal.getId())) {
|
||||
/**
|
||||
* 码头导入:上级港口优先匹配本批次新增的港口,其次由 prepare 回查数据库。
|
||||
*/
|
||||
private void prepareTerminal(PortTerminal portTerminal, Map<String, PortTerminal> batchPortMap) {
|
||||
String parentCode = trimToEmpty(portTerminal.getParentCode()).toUpperCase(Locale.ROOT);
|
||||
PortTerminal parent = Func.isEmpty(parentCode) ? null : batchPortMap.get(parentCode);
|
||||
if (Func.isNotEmpty(parent)) {
|
||||
applyParent(portTerminal, parent);
|
||||
// 父信息已由本批次港口回填,无需再回查数据库。
|
||||
prepare(portTerminal, SOURCE_BATCH, true);
|
||||
return;
|
||||
}
|
||||
PortTerminal existingPortTerminal = baseMapper.selectByCodeIncludingDeleted(portTerminal.getCode());
|
||||
if (existingPortTerminal == null) {
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(existingPortTerminal.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("该编码已存在");
|
||||
}
|
||||
baseMapper.restoreById(existingPortTerminal.getId());
|
||||
portTerminal.setId(existingPortTerminal.getId());
|
||||
portTerminal.setIsDeleted(0);
|
||||
portTerminal.setParentCode(parentCode);
|
||||
prepare(portTerminal, SOURCE_BATCH);
|
||||
}
|
||||
|
||||
private String formatImportErrorMessage(List<String> validationErrors) {
|
||||
StringBuilder errorMessage = new StringBuilder();
|
||||
for (int index = 0; index < validationErrors.size(); index++) {
|
||||
if (index > 0) {
|
||||
errorMessage.append(System.lineSeparator());
|
||||
}
|
||||
errorMessage.append(index + 1).append(". ").append(validationErrors.get(index));
|
||||
/**
|
||||
* 将上级港口信息回填到码头,保证码头不会出现没有港口的数据。
|
||||
*/
|
||||
private void applyParent(PortTerminal portTerminal, PortTerminal parent) {
|
||||
portTerminal.setParentId(parent.getId());
|
||||
portTerminal.setParentCode(parent.getCode());
|
||||
portTerminal.setParentName(parent.getName());
|
||||
// 与回查数据库保持一致:父港口区域信息为空时保留码头自身填写的值。
|
||||
if (Func.isNotEmpty(parent.getCountry())) {
|
||||
portTerminal.setCountry(parent.getCountry());
|
||||
}
|
||||
return errorMessage.toString();
|
||||
}
|
||||
|
||||
private Map<String, Integer> buildImportCodeCountMap(List<PortTerminalExcel> data) {
|
||||
Map<String, Integer> codeCountMap = new HashMap<>();
|
||||
for (PortTerminalExcel excel : data) {
|
||||
String code = resolveImportCode(excel);
|
||||
if (Func.isNotEmpty(code)) {
|
||||
codeCountMap.merge(code, 1, Integer::sum);
|
||||
}
|
||||
if (Func.isNotEmpty(parent.getCity())) {
|
||||
portTerminal.setCity(parent.getCity());
|
||||
}
|
||||
return codeCountMap;
|
||||
}
|
||||
|
||||
private PortTerminal buildImportPortTerminal(PortTerminalExcel excel) {
|
||||
PortTerminal portTerminal = new PortTerminal();
|
||||
portTerminal.setCode(resolveImportCode(excel));
|
||||
portTerminal.setName(excel.getName());
|
||||
portTerminal.setCategory(excel.getCategory());
|
||||
portTerminal.setParentName(excel.getParentName());
|
||||
portTerminal.setParentCode(CATEGORY_TERMINAL.equals(trimToEmpty(excel.getCategory())) ? excel.getPortCode() : excel.getParentCode());
|
||||
portTerminal.setCountry(excel.getCountry());
|
||||
portTerminal.setProvinceName(excel.getProvinceName());
|
||||
portTerminal.setCity(excel.getCity());
|
||||
portTerminal.setDistrictName(excel.getDistrictName());
|
||||
portTerminal.setDetailAddress(excel.getDetailAddress());
|
||||
portTerminal.setLongitude(excel.getLongitude());
|
||||
portTerminal.setLatitude(excel.getLatitude());
|
||||
portTerminal.setRemark(excel.getRemark());
|
||||
return portTerminal;
|
||||
}
|
||||
|
||||
private String resolveImportCode(PortTerminalExcel excel) {
|
||||
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
||||
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
||||
if (Func.isEmpty(terminalCode)) {
|
||||
return portCode;
|
||||
if (Func.isNotEmpty(parent.getDistrictCode())) {
|
||||
portTerminal.setDistrictCode(parent.getDistrictCode());
|
||||
portTerminal.setRegionCode(parent.getDistrictCode());
|
||||
} else if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
|
||||
portTerminal.setRegionCode(portTerminal.getDistrictCode());
|
||||
}
|
||||
return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
|
||||
}
|
||||
|
||||
private void normalizeImportPortTerminal(PortTerminal portTerminal) {
|
||||
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
|
||||
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
|
||||
portTerminal.setName(trimToEmpty(portTerminal.getName()));
|
||||
portTerminal.setParentCode(trimToNull(portTerminal.getParentCode()));
|
||||
if (Func.isNotEmpty(portTerminal.getParentCode())) {
|
||||
portTerminal.setParentCode(portTerminal.getParentCode().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
portTerminal.setParentName(trimToNull(portTerminal.getParentName()));
|
||||
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
|
||||
portTerminal.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
|
||||
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
|
||||
portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
|
||||
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
|
||||
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
|
||||
portTerminal.setRegionCode(trimToNull(portTerminal.getRegionCode()));
|
||||
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getRegionCode())) {
|
||||
portTerminal.setDistrictCode(portTerminal.getRegionCode());
|
||||
}
|
||||
portTerminal.setDetailAddress(trimToNull(portTerminal.getDetailAddress()));
|
||||
portTerminal.setRemark(trimToNull(portTerminal.getRemark()));
|
||||
}
|
||||
|
||||
private List<String> validateImportPortTerminal(PortTerminalExcel excel, PortTerminal portTerminal,
|
||||
Map<String, Integer> codeCountMap) {
|
||||
List<String> validationErrors = new ArrayList<>();
|
||||
if (!CATEGORY_PORT.equals(portTerminal.getCategory()) && !CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
|
||||
addValidationError(validationErrors, "类型只能为港口或码头");
|
||||
}
|
||||
validateImportCodeColumns(excel, portTerminal, validationErrors);
|
||||
if (Func.isEmpty(portTerminal.getCode())) {
|
||||
addValidationError(validationErrors, "编码不能为空");
|
||||
} else {
|
||||
validateImportLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字", validationErrors);
|
||||
if (CATEGORY_PORT.equals(portTerminal.getCategory()) && !PORT_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) {
|
||||
addValidationError(validationErrors, "港口编码为5位大写字母");
|
||||
}
|
||||
if (CATEGORY_TERMINAL.equals(portTerminal.getCategory()) && !TERMINAL_CODE_PATTERN.matcher(portTerminal.getCode()).matches()) {
|
||||
addValidationError(validationErrors, "码头编码格式为港口编码-码头标识");
|
||||
}
|
||||
if (codeCountMap.getOrDefault(portTerminal.getCode(), 0) > 1) {
|
||||
addValidationError(validationErrors, "编码在本次导入中重复");
|
||||
}
|
||||
if (count(Wrappers.<PortTerminal>lambdaQuery()
|
||||
.eq(PortTerminal::getCode, portTerminal.getCode())
|
||||
.eq(PortTerminal::getIsDeleted, 0)) > 0L) {
|
||||
addValidationError(validationErrors, "该编码已存在");
|
||||
}
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getName())) {
|
||||
addValidationError(validationErrors, "港口/码头名称不能为空");
|
||||
}
|
||||
validateImportLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字", validationErrors);
|
||||
validateImportLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字", validationErrors);
|
||||
validateImportLength(portTerminal.getProvinceName(), REGION_MAX_LENGTH, "所属省份不能超过50字", validationErrors);
|
||||
validateImportLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字", validationErrors);
|
||||
validateImportLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字", validationErrors);
|
||||
validateImportLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字", validationErrors);
|
||||
if (Func.isEmpty(portTerminal.getDetailAddress())) {
|
||||
addValidationError(validationErrors, "详细地址不能为空");
|
||||
}
|
||||
validateImportLength(portTerminal.getDetailAddress(), DETAIL_ADDRESS_MAX_LENGTH, "详细地址不能超过255字", validationErrors);
|
||||
validateImportLength(portTerminal.getRemark(), REMARK_MAX_LENGTH, "备注不能超过200个字", validationErrors);
|
||||
if (Func.isEmpty(portTerminal.getLongitude())) {
|
||||
addValidationError(validationErrors, "经度不能为空");
|
||||
} else if (!validRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE)) {
|
||||
addValidationError(validationErrors, "经度范围为 -180 到 180");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getLatitude())) {
|
||||
addValidationError(validationErrors, "纬度不能为空");
|
||||
} else if (!validRange(portTerminal.getLatitude(), MIN_LATITUDE, MAX_LATITUDE)) {
|
||||
addValidationError(validationErrors, "纬度范围为 -90 到 90");
|
||||
}
|
||||
if (CATEGORY_PORT.equals(portTerminal.getCategory())) {
|
||||
validateImportPortRegion(portTerminal, validationErrors);
|
||||
}
|
||||
if (CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
|
||||
validateImportParentPort(portTerminal, validationErrors);
|
||||
}
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
private void validateImportCodeColumns(PortTerminalExcel excel, PortTerminal portTerminal,
|
||||
List<String> validationErrors) {
|
||||
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
||||
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
||||
String parentCode = trimToEmpty(excel.getParentCode()).toUpperCase(Locale.ROOT);
|
||||
if (Func.isEmpty(portCode)) {
|
||||
addValidationError(validationErrors, "港口编码不能为空");
|
||||
}
|
||||
if (!CATEGORY_TERMINAL.equals(portTerminal.getCategory())) {
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(terminalCode)) {
|
||||
addValidationError(validationErrors, "码头编码不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getParentName())) {
|
||||
addValidationError(validationErrors, "上级港口不能为空");
|
||||
}
|
||||
if (Func.isEmpty(parentCode)) {
|
||||
addValidationError(validationErrors, "上级港口编码不能为空");
|
||||
} else if (Func.isNotEmpty(portCode) && !portCode.equals(parentCode) && !portCode.endsWith(parentCode)) {
|
||||
addValidationError(validationErrors, "港口编码与上级港口编码不匹配");
|
||||
if (Func.isNotEmpty(parent.getDistrictName())) {
|
||||
portTerminal.setDistrictName(parent.getDistrictName());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImportPortRegion(PortTerminal portTerminal, List<String> validationErrors) {
|
||||
if (Func.isEmpty(portTerminal.getCountry())) {
|
||||
addValidationError(validationErrors, "国家不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getCity())) {
|
||||
addValidationError(validationErrors, "城市不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getProvinceName())) {
|
||||
addValidationError(validationErrors, "所属省份不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isEmpty(portTerminal.getDistrictName())) {
|
||||
addValidationError(validationErrors, "区县不能为空");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fillRegion(portTerminal);
|
||||
} catch (ServiceException exception) {
|
||||
addValidationError(validationErrors, exception.getMessage());
|
||||
/**
|
||||
* 构造失败明细,行号按 Excel 中的实际行号(表头占第 1 行)推算。
|
||||
*/
|
||||
private PortTerminalExcel buildFailure(List<PortTerminalExcel> data, int index, String message) {
|
||||
PortTerminalExcel excel = data.get(index);
|
||||
markFailure(data, index, message);
|
||||
return excel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅标注错误原因,不重复加入失败明细集合。
|
||||
*/
|
||||
private void markFailure(List<PortTerminalExcel> data, int index, String message) {
|
||||
if (index >= 0 && index < data.size()) {
|
||||
data.get(index).setErrorMessage("第" + (index + 2) + "行:" + message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImportParentPort(PortTerminal portTerminal, List<String> validationErrors) {
|
||||
if (Func.isEmpty(portTerminal.getParentCode())) {
|
||||
addValidationError(validationErrors, "上级港口编码不能为空");
|
||||
return;
|
||||
}
|
||||
PortTerminal parentPort = getOne(Wrappers.<PortTerminal>lambdaQuery()
|
||||
.eq(PortTerminal::getCode, portTerminal.getParentCode())
|
||||
.eq(PortTerminal::getCategory, CATEGORY_PORT)
|
||||
.eq(PortTerminal::getIsDeleted, 0), false);
|
||||
if (Func.isEmpty(parentPort)) {
|
||||
addValidationError(validationErrors, "上级港口编码对应的港口不存在");
|
||||
} else if (Func.isNotEmpty(portTerminal.getParentName()) && !Objects.equals(portTerminal.getParentName(), parentPort.getName())) {
|
||||
addValidationError(validationErrors, "上级港口与上级港口编码不匹配");
|
||||
}
|
||||
if (Func.isNotEmpty(portTerminal.getCode()) && !portTerminal.getCode().startsWith(portTerminal.getParentCode() + "-")) {
|
||||
addValidationError(validationErrors, "码头编码必须以上级港口编码开头");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImportLength(String value, int maxLength, String message, List<String> validationErrors) {
|
||||
if (Func.isNotEmpty(value) && value.length() > maxLength) {
|
||||
addValidationError(validationErrors, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void addValidationError(List<String> validationErrors, String message) {
|
||||
if (Func.isNotEmpty(message) && !validationErrors.contains(message)) {
|
||||
validationErrors.add(message);
|
||||
}
|
||||
/**
|
||||
* 解析异常信息,非业务异常统一提示导入失败。
|
||||
*/
|
||||
private String resolveMessage(Exception exception) {
|
||||
return exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -430,12 +370,21 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
}
|
||||
|
||||
private void prepare(PortTerminal portTerminal, String defaultDataSource) {
|
||||
prepare(portTerminal, defaultDataSource, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理并补全港口码头数据。
|
||||
*
|
||||
* @param portTerminal 港口码头
|
||||
* @param defaultDataSource 默认数据来源
|
||||
* @param parentResolved 上级港口是否已确定(批量导入时由批次内存匹配得到,无需回查数据库)
|
||||
*/
|
||||
private void prepare(PortTerminal portTerminal, String defaultDataSource, boolean parentResolved) {
|
||||
portTerminal.setCode(trimToEmpty(portTerminal.getCode()).toUpperCase(Locale.ROOT));
|
||||
portTerminal.setCategory(trimToEmpty(portTerminal.getCategory()));
|
||||
portTerminal.setName(trimToEmpty(portTerminal.getName()));
|
||||
portTerminal.setCountry(trimToEmpty(portTerminal.getCountry()));
|
||||
portTerminal.setProvinceCode(trimToNull(portTerminal.getProvinceCode()));
|
||||
portTerminal.setProvinceName(trimToNull(portTerminal.getProvinceName()));
|
||||
portTerminal.setCity(trimToEmpty(portTerminal.getCity()));
|
||||
portTerminal.setDistrictCode(trimToNull(portTerminal.getDistrictCode()));
|
||||
portTerminal.setDistrictName(trimToNull(portTerminal.getDistrictName()));
|
||||
@@ -456,6 +405,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
portTerminal.setParentName(null);
|
||||
return;
|
||||
}
|
||||
if (parentResolved) {
|
||||
return;
|
||||
}
|
||||
fillParentPort(portTerminal);
|
||||
}
|
||||
|
||||
@@ -471,7 +423,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
.eq(PortTerminal::getIsDeleted, 0));
|
||||
}
|
||||
if (Func.isEmpty(parent)) {
|
||||
throw new ServiceException("码头必须选择上级港口");
|
||||
// 用专用异常类型:调用方需要区分"父港口真的漏填"与"父行自身失败",
|
||||
// 后者不应连带给码头行报错(见 importTerminals)。
|
||||
throw new ParentPortNotFoundException("码头必须选择上级港口");
|
||||
}
|
||||
if (!CATEGORY_PORT.equals(parent.getCategory())) {
|
||||
throw new ServiceException("上级港口类型不正确");
|
||||
@@ -479,31 +433,25 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
portTerminal.setParentId(parent.getId());
|
||||
portTerminal.setParentCode(parent.getCode());
|
||||
portTerminal.setParentName(parent.getName());
|
||||
portTerminal.setCountry(inheritParentValue(parent.getCountry(), portTerminal.getCountry()));
|
||||
portTerminal.setProvinceCode(inheritParentValue(parent.getProvinceCode(), portTerminal.getProvinceCode()));
|
||||
portTerminal.setProvinceName(inheritParentValue(parent.getProvinceName(), portTerminal.getProvinceName()));
|
||||
portTerminal.setCity(inheritParentValue(parent.getCity(), portTerminal.getCity()));
|
||||
portTerminal.setDistrictCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getDistrictCode()));
|
||||
portTerminal.setDistrictName(inheritParentValue(parent.getDistrictName(), portTerminal.getDistrictName()));
|
||||
portTerminal.setRegionCode(inheritParentValue(parent.getDistrictCode(), portTerminal.getRegionCode()));
|
||||
if (Func.isEmpty(portTerminal.getDistrictCode()) && Func.isNotEmpty(portTerminal.getDistrictName())) {
|
||||
fillRegion(portTerminal);
|
||||
// 父港口区域信息为空时保留码头自身填写的值,避免历史数据(区县为空)导致码头无法导入。
|
||||
if (Func.isNotEmpty(parent.getCountry())) {
|
||||
portTerminal.setCountry(parent.getCountry());
|
||||
}
|
||||
if (Func.isNotEmpty(parent.getCity())) {
|
||||
portTerminal.setCity(parent.getCity());
|
||||
}
|
||||
if (Func.isNotEmpty(parent.getDistrictCode())) {
|
||||
portTerminal.setDistrictCode(parent.getDistrictCode());
|
||||
portTerminal.setRegionCode(parent.getDistrictCode());
|
||||
} else if (Func.isNotEmpty(portTerminal.getRegionCode())) {
|
||||
portTerminal.setDistrictCode(portTerminal.getRegionCode());
|
||||
}
|
||||
if (Func.isNotEmpty(parent.getDistrictName())) {
|
||||
portTerminal.setDistrictName(parent.getDistrictName());
|
||||
}
|
||||
}
|
||||
|
||||
private void fillRegion(PortTerminal portTerminal) {
|
||||
Region selectedProvince = null;
|
||||
if (Func.isNotEmpty(portTerminal.getProvinceCode())) {
|
||||
selectedProvince = regionService.getById(portTerminal.getProvinceCode());
|
||||
}
|
||||
if (Func.isEmpty(selectedProvince) && Func.isNotEmpty(portTerminal.getProvinceName())) {
|
||||
selectedProvince = regionService.getOne(Wrappers.<Region>lambdaQuery()
|
||||
.eq(Region::getName, portTerminal.getProvinceName())
|
||||
.eq(Region::getRegionLevel, 1), false);
|
||||
}
|
||||
if (Func.isEmpty(selectedProvince)) {
|
||||
throw new ServiceException("请选择所属省份");
|
||||
}
|
||||
Region district = null;
|
||||
if (Func.isNotEmpty(portTerminal.getDistrictCode())) {
|
||||
district = regionService.getById(portTerminal.getDistrictCode());
|
||||
@@ -512,7 +460,6 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
if (Func.isNotEmpty(portTerminal.getCity())) {
|
||||
List<Region> cityList = regionService.list(Wrappers.<Region>lambdaQuery()
|
||||
.eq(Region::getName, portTerminal.getCity())
|
||||
.eq(Region::getParentCode, selectedProvince.getCode())
|
||||
.eq(Region::getRegionLevel, 2));
|
||||
for (Region city : cityList) {
|
||||
district = regionService.getOne(Wrappers.<Region>lambdaQuery()
|
||||
@@ -536,26 +483,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
if (Func.isEmpty(city)) {
|
||||
throw new ServiceException("区县所属城市不存在");
|
||||
}
|
||||
Region province = regionService.getById(city.getParentCode());
|
||||
if (Func.isEmpty(province)) {
|
||||
throw new ServiceException("城市所属省份不存在");
|
||||
}
|
||||
if (Func.isNotEmpty(portTerminal.getProvinceCode())
|
||||
&& !Objects.equals(portTerminal.getProvinceCode(), province.getCode())) {
|
||||
throw new ServiceException("城市与省份不匹配");
|
||||
}
|
||||
if (Func.isNotEmpty(portTerminal.getProvinceName())
|
||||
&& !Objects.equals(portTerminal.getProvinceName(), province.getName())) {
|
||||
throw new ServiceException("城市与省份不匹配");
|
||||
}
|
||||
if (!Objects.equals(selectedProvince.getCode(), province.getCode())) {
|
||||
throw new ServiceException("城市与省份不匹配");
|
||||
}
|
||||
if (Func.isNotEmpty(portTerminal.getCity()) && !Objects.equals(portTerminal.getCity(), city.getName())) {
|
||||
throw new ServiceException("区县与城市不匹配");
|
||||
}
|
||||
portTerminal.setProvinceCode(province.getCode());
|
||||
portTerminal.setProvinceName(province.getName());
|
||||
portTerminal.setCity(city.getName());
|
||||
portTerminal.setDistrictCode(district.getCode());
|
||||
portTerminal.setDistrictName(district.getName());
|
||||
@@ -575,7 +505,6 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
validateLength(portTerminal.getCode(), CODE_MAX_LENGTH, "编码不能超过30字");
|
||||
validateLength(portTerminal.getName(), NAME_MAX_LENGTH, "港口/码头名称不能超过100字");
|
||||
validateLength(portTerminal.getCountry(), REGION_MAX_LENGTH, "国家不能超过50字");
|
||||
validateLength(portTerminal.getProvinceName(), REGION_MAX_LENGTH, "所属省份不能超过50字");
|
||||
validateLength(portTerminal.getCity(), REGION_MAX_LENGTH, "城市不能超过50字");
|
||||
validateLength(portTerminal.getDistrictName(), REGION_MAX_LENGTH, "区县不能超过50字");
|
||||
validateLength(portTerminal.getRegionCode(), REGION_CODE_MAX_LENGTH, "行政区划编码不能超过32字");
|
||||
@@ -596,15 +525,9 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
if (Func.isEmpty(portTerminal.getCity())) {
|
||||
throw new ServiceException("城市不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getProvinceCode())) {
|
||||
throw new ServiceException("请选择所属省份");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getDistrictCode())) {
|
||||
throw new ServiceException("区县不能为空");
|
||||
}
|
||||
if (Func.isEmpty(portTerminal.getDetailAddress())) {
|
||||
throw new ServiceException("详细地址不能为空");
|
||||
}
|
||||
validateDataSource(portTerminal.getDataSource());
|
||||
validateStatus(portTerminal.getStatus());
|
||||
validateRange(portTerminal.getLongitude(), MIN_LONGITUDE, MAX_LONGITUDE, "经度范围为 -180 到 180");
|
||||
@@ -655,12 +578,21 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
return Func.isEmpty(value) || (value.compareTo(min) >= 0 && value.compareTo(max) <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归并导入行编码:仅填港口编码时取港口编码,填了码头编码时拼接为"港口编码-码头编码"。
|
||||
*/
|
||||
private String resolveImportCode(PortTerminalExcel excel) {
|
||||
String portCode = trimToEmpty(excel.getPortCode()).toUpperCase(Locale.ROOT);
|
||||
String terminalCode = trimToEmpty(excel.getTerminalCode()).toUpperCase(Locale.ROOT);
|
||||
if (Func.isEmpty(terminalCode)) {
|
||||
return portCode;
|
||||
}
|
||||
return Func.isEmpty(portCode) ? terminalCode : portCode + "-" + terminalCode;
|
||||
}
|
||||
|
||||
private String normalizeDataSource(String dataSource) {
|
||||
String value = trimToEmpty(dataSource);
|
||||
if (SOURCE_INITIAL_IMPORT.equals(value) || SOURCE_INITIAL_OLD.equals(value)) {
|
||||
return SOURCE_INITIAL;
|
||||
}
|
||||
return SOURCE_MANUAL_OLD.equals(value) ? SOURCE_MANUAL : value;
|
||||
return SOURCE_INITIAL_OLD.equals(value) ? SOURCE_INITIAL : value;
|
||||
}
|
||||
|
||||
private void validateEnabledTerminal(Long parentId) {
|
||||
@@ -686,9 +618,23 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
|
||||
return trimValue.isEmpty() ? null : trimValue;
|
||||
}
|
||||
|
||||
private String inheritParentValue(String parentValue, String currentValue) {
|
||||
String normalizedParentValue = trimToNull(parentValue);
|
||||
return normalizedParentValue == null ? currentValue : normalizedParentValue;
|
||||
/**
|
||||
* 上级港口找不到时抛出。
|
||||
* <p>
|
||||
* 与普通业务异常区分开,是因为调用方需要判断:这个父港口到底是"用户漏填了",
|
||||
* 还是"父港口那一行就在本文件里、只是它自己校验失败"。后者不该连带给码头行报错。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
private static class ParentPortNotFoundException extends ServiceException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
ParentPortNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -689,6 +689,9 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
|
||||
*/
|
||||
private User buildImportUser(UserExcel userExcel, String tenantId) {
|
||||
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class));
|
||||
// 宽容解析生日文本(2026-8-2 等写法),User.birthday 为 java.util.Date 需转换
|
||||
java.time.LocalDate birthday = org.springblade.common.excel.LenientDateParser.parseDate(userExcel.getBirthday(), "生日");
|
||||
user.setBirthday(birthday == null ? null : java.sql.Date.valueOf(birthday));
|
||||
user.setTenantId(tenantId);
|
||||
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
|
||||
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
|
||||
|
||||
+6
@@ -6,14 +6,20 @@ package org.springblade.transport.config;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 凭证图片 MinIO 客户端配置。
|
||||
* <p>
|
||||
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
||||
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
|
||||
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
|
||||
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
|
||||
public class VoucherMinioConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
+20
-5
@@ -52,6 +52,7 @@ 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.service.IProcessConfigService;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -83,20 +84,34 @@ public class ProcessConfigController extends BladeController {
|
||||
private final VoucherFileMapper voucherFileMapper;
|
||||
private final VoucherImageMapper voucherImageMapper;
|
||||
private final VoucherManageMapper voucherManageMapper;
|
||||
private final MinioClient minioClient;
|
||||
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
||||
private String minioBucketName;
|
||||
|
||||
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
||||
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
||||
VoucherManageMapper voucherManageMapper,
|
||||
MinioClient minioClient) {
|
||||
ObjectProvider<MinioClient> minioClientProvider) {
|
||||
this.processConfigService = processConfigService;
|
||||
this.waybillMapper = waybillMapper;
|
||||
this.voucherFileMapper = voucherFileMapper;
|
||||
this.voucherImageMapper = voucherImageMapper;
|
||||
this.voucherManageMapper = voucherManageMapper;
|
||||
this.minioClient = minioClient;
|
||||
this.minioClientProvider = minioClientProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MinIO 客户端。
|
||||
* <p>
|
||||
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
|
||||
* 此时凭证预览地址无法生成,抛出明确提示而非启动即失败。
|
||||
*/
|
||||
private MinioClient minioClient() {
|
||||
MinioClient minioClient = minioClientProvider.getIfAvailable();
|
||||
if (minioClient == null) {
|
||||
throw new IllegalStateException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
|
||||
}
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@@ -147,7 +162,7 @@ public class ProcessConfigController extends BladeController {
|
||||
result.put("waybillNo", image.getWaybillNo());
|
||||
result.put("objectKey", image.getObjectKey());
|
||||
try {
|
||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
|
||||
.expiry(1, TimeUnit.HOURS).build()));
|
||||
} catch (Exception e) {
|
||||
@@ -270,7 +285,7 @@ public class ProcessConfigController extends BladeController {
|
||||
result.put("objectKey", image.objectKey());
|
||||
result.put("matched", image.matched());
|
||||
try {
|
||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(image.objectKey())
|
||||
.expiry(1, TimeUnit.HOURS).build()));
|
||||
} catch (Exception exception) {
|
||||
|
||||
+2
-3
@@ -36,7 +36,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 事故记录 Excel
|
||||
@@ -60,8 +59,8 @@ public class AccidentRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*事故发生日期")
|
||||
private LocalDate accidentDate;
|
||||
@ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String accidentDate;
|
||||
|
||||
@ExcelProperty("事故发生地点")
|
||||
private String accidentLocation;
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 事故记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 年检记录 Excel
|
||||
@@ -59,11 +58,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*检测评定日期")
|
||||
private LocalDate inspectionAssessmentDate;
|
||||
@ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String inspectionAssessmentDate;
|
||||
|
||||
@ExcelProperty("*有效期截止日")
|
||||
private LocalDate validUntilDate;
|
||||
@ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String validUntilDate;
|
||||
|
||||
@ExcelProperty("*车辆技术等级")
|
||||
private String vehicleTechnicalLevel;
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 年检记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
@@ -24,6 +24,7 @@ package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -88,9 +89,11 @@ public class CommonAddressExportExcel implements Serializable {
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -24,6 +24,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
@@ -93,9 +94,11 @@ public class CommonCargoExportExcel implements Serializable {
|
||||
private String deptName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -75,8 +76,10 @@ public class CommonRouteExportExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
@@ -33,7 +34,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 合同管理 Excel
|
||||
@@ -111,8 +112,10 @@ public class ContractManageExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+1
-3
@@ -7,12 +7,10 @@ package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 设备台账 Excel
|
||||
@@ -29,7 +27,7 @@ public class EquipmentLedgerExcel implements Serializable {
|
||||
@ExcelProperty("设备品牌") private String equipmentBrand;
|
||||
@ExcelProperty("设备类型") private String equipmentType;
|
||||
@ExcelProperty("规格型号") private String specificationModel;
|
||||
@ExcelProperty("出厂日期") @DateTimeFormat("yyyy-MM-dd") private LocalDate factoryDate;
|
||||
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
|
||||
@ExcelProperty("备注") private String remark;
|
||||
@ExcelIgnore private String errorMessage;
|
||||
}
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* ETC记录导出 Excel
|
||||
@@ -50,14 +51,14 @@ public class EtcRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+6
-7
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 保险记录 Excel
|
||||
@@ -68,11 +67,11 @@ public class InsuranceRecordExcel implements Serializable {
|
||||
@ExcelProperty("*保单号")
|
||||
private String policyNo;
|
||||
|
||||
@ExcelProperty("*开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String startDate;
|
||||
|
||||
@ExcelProperty("*结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String endDate;
|
||||
|
||||
@ExcelProperty("保额")
|
||||
private BigDecimal insuredAmount;
|
||||
@@ -83,8 +82,8 @@ public class InsuranceRecordExcel implements Serializable {
|
||||
@ExcelProperty("发票号")
|
||||
private String invoiceNo;
|
||||
|
||||
@ExcelProperty("开票日期")
|
||||
private LocalDate invoiceDate;
|
||||
@ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String invoiceDate;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 保险记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+11
-15
@@ -33,22 +33,20 @@ import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
/**
|
||||
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
|
||||
* <p>
|
||||
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
|
||||
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
|
||||
* 文案带原值,与「日期格式无法识别」口径一致。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
|
||||
|
||||
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DATE_TIME_MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
@@ -71,16 +69,14 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String normalizedValue = value.trim();
|
||||
try {
|
||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
try {
|
||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
|
||||
} catch (DateTimeParseException ignoredMinute) {
|
||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
|
||||
}
|
||||
LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException,
|
||||
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
|
||||
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
|
||||
value.trim() + " 日期格式无法识别");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 车辆保养记录导出 Excel
|
||||
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
|
||||
|
||||
@ExcelProperty(value = "创建时间", index = 12)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty(value = "更新人", index = 13)
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty(value = "更新时间", index = 14)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
|
||||
+5
-4
@@ -38,6 +38,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 车辆维修记录 Excel
|
||||
@@ -95,16 +96,16 @@ public class MaintenanceRecordExcel implements Serializable {
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("里程单位")
|
||||
private String mileageUnit;
|
||||
|
||||
-4
@@ -25,7 +25,6 @@
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -42,7 +41,4 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty(value = "导出失败原因", index = 17)
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 里程记录导出 Excel
|
||||
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 油电记录导出 Excel
|
||||
@@ -61,14 +62,14 @@ public class OilElectricRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+2
-3
@@ -16,7 +16,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 其他费用记录 Excel
|
||||
@@ -40,8 +39,8 @@ public class OtherExpenseRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*费用日期")
|
||||
private LocalDate expenseDate;
|
||||
@ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String expenseDate;
|
||||
|
||||
@ExcelProperty("*费用类型")
|
||||
private String expenseType;
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 其他费用记录导出 Excel
|
||||
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -69,8 +70,10 @@ public class ProcessConfigExportExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -33,6 +34,7 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 项目立项 Excel
|
||||
@@ -123,8 +125,10 @@ public class ProjectApplyExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -30,7 +31,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 发货模板 Excel
|
||||
@@ -59,8 +60,10 @@ public class ShippingTemplateExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -32,7 +33,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 临时额度申请 Excel
|
||||
@@ -81,8 +82,10 @@ public class TemporaryCreditLimitExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -38,6 +38,10 @@ import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
|
||||
* <p>
|
||||
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
|
||||
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
|
||||
* 口径见根工作区 docs/import-spec.md)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 换胎记录导出 Excel
|
||||
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 变更记录导出 Excel
|
||||
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -31,7 +32,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 运输计划 Excel
|
||||
@@ -92,8 +93,10 @@ public class TransportPlanExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,6 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 违章记录 Excel
|
||||
@@ -95,14 +96,14 @@ public class ViolationRecordExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
+5
-2
@@ -60,8 +60,11 @@ public class ViolationRecordImportExcel implements Serializable {
|
||||
@ExcelProperty("*驾驶人")
|
||||
private String driverName;
|
||||
|
||||
@ExcelProperty("*类型/事项")
|
||||
private String violationTypeOrItem;
|
||||
@ExcelProperty("*类型")
|
||||
private String violationType;
|
||||
|
||||
@ExcelProperty("*事项")
|
||||
private String violationItem;
|
||||
|
||||
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
+8
-8
@@ -108,10 +108,10 @@ public class WaybillExcel implements Serializable {
|
||||
private String escortPhone;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("预计发货日期")
|
||||
private LocalDate estimatedStartTime;
|
||||
@ExcelProperty("预计完成日期")
|
||||
private LocalDate estimatedEndTime;
|
||||
@ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String estimatedStartTime;
|
||||
@ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String estimatedEndTime;
|
||||
@ExcelProperty("单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("计价单位")
|
||||
@@ -126,10 +126,10 @@ public class WaybillExcel implements Serializable {
|
||||
private String businessStatus;
|
||||
@ExcelProperty("数据来源")
|
||||
private String dataSource;
|
||||
@ExcelProperty("开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty("结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String startDate;
|
||||
@ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String endDate;
|
||||
@ExcelProperty("计划名称")
|
||||
private String planName;
|
||||
@ExcelProperty("多联总单")
|
||||
|
||||
+1
@@ -94,6 +94,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
|
||||
AccidentRecordExcel excel = data.get(index);
|
||||
try {
|
||||
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
|
||||
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
|
||||
prepare(accidentRecord);
|
||||
List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
|
||||
+2
@@ -113,6 +113,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
|
||||
AnnualInspectionRecordExcel excel = data.get(index);
|
||||
try {
|
||||
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class));
|
||||
annualInspectionRecord.setInspectionAssessmentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInspectionAssessmentDate(), "检测评定日期"));
|
||||
annualInspectionRecord.setValidUntilDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getValidUntilDate(), "有效期截止日"));
|
||||
prepare(annualInspectionRecord);
|
||||
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
|
||||
EquipmentLedgerExcel excel = data.get(index);
|
||||
try {
|
||||
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
|
||||
equipmentLedger.setFactoryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getFactoryDate(), "出厂日期"));
|
||||
prepare(equipmentLedger);
|
||||
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
|
||||
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
|
||||
|
||||
+3
@@ -119,6 +119,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
InsuranceRecordExcel excel = data.get(index);
|
||||
try {
|
||||
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class));
|
||||
insuranceRecord.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
|
||||
insuranceRecord.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
|
||||
insuranceRecord.setInvoiceDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInvoiceDate(), "开票日期"));
|
||||
prepare(insuranceRecord);
|
||||
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
|
||||
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
|
||||
|
||||
+36
-1
@@ -174,6 +174,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
||||
}
|
||||
Waybill first = waybills.get(0);
|
||||
Waybill last = waybills.get(waybills.size() - 1);
|
||||
LoadingManage loadingManage = new LoadingManage();
|
||||
loadingManage.setLoadingNo(normalizedLoadingNo);
|
||||
loadingManage.setLoadingSubNos(waybills.stream()
|
||||
@@ -197,7 +198,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setCarrierName(first.getCarrierName());
|
||||
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
||||
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
||||
loadingManage.setArrivalAddress(first.getArrivalAddress());
|
||||
loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills));
|
||||
loadingManage.setArrivalAddress(last.getArrivalAddress());
|
||||
loadingManage.setOriginalNo(first.getOriginalNo());
|
||||
loadingManage.setDataSource("批量导入");
|
||||
loadingManage.setStartDate(first.getStartDate());
|
||||
@@ -222,6 +224,39 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据导入运单构建配载单的途经地:
|
||||
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
|
||||
* 最终形成“首票发货地 → 途经点 → 末票到货地”。
|
||||
*/
|
||||
private String buildImportedRouteTransitAddress(List<Waybill> waybills) {
|
||||
// 按顺序收集全部节点:首票发货地、每票到货地;节点与相邻前一点相同则重合跳过
|
||||
List<String> nodes = new ArrayList<>();
|
||||
for (Waybill waybill : waybills) {
|
||||
String departure = TransportBusinessSupport.trimToNull(waybill.getDepartureAddress());
|
||||
String arrival = TransportBusinessSupport.trimToNull(waybill.getArrivalAddress());
|
||||
appendRouteNode(nodes, departure);
|
||||
appendRouteNode(nodes, arrival);
|
||||
}
|
||||
// 途经点 = 去掉首尾(首票发货地、末票到货地)后的中间节点
|
||||
List<String> transitNodes = nodes.size() > 2 ? nodes.subList(1, nodes.size() - 1) : List.of();
|
||||
if (Func.isEmpty(transitNodes)) {
|
||||
return null;
|
||||
}
|
||||
return String.join(" - ", transitNodes);
|
||||
}
|
||||
|
||||
private void appendRouteNode(List<String> nodes, String address) {
|
||||
if (Func.isEmpty(address)) {
|
||||
return;
|
||||
}
|
||||
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1).equals(address)) {
|
||||
// 与上一节点相同视为同一地点,重合不重复
|
||||
return;
|
||||
}
|
||||
nodes.add(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
|
||||
OtherExpenseRecordExcel excel = data.get(index);
|
||||
try {
|
||||
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
|
||||
otherExpenseRecord.setExpenseDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpenseDate(), "费用日期"));
|
||||
otherExpenseRecord.setDataSource("批量导入");
|
||||
prepare(otherExpenseRecord);
|
||||
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
|
||||
|
||||
+3
-6
@@ -43,8 +43,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -64,7 +62,6 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
||||
private static final int REMARK_MAX_LENGTH = 500;
|
||||
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
|
||||
private static final int MONEY_SCALE = 2;
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
@Override
|
||||
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
|
||||
@@ -144,9 +141,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
validationErrors.add("换胎时间格式不正确,请使用yyyy-MM-dd格式并填写有效日期");
|
||||
return org.springblade.common.excel.LenientDateParser.parseDate(normalizedValue, "换胎时间");
|
||||
} catch (Exception exception) {
|
||||
validationErrors.add(exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -416,9 +416,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
// 2. 校验运输类型枚举值
|
||||
String transportType = trimToEmpty(excel.getTransportType());
|
||||
if (Func.isNotEmpty(transportType)) {
|
||||
List<String> validTransportTypes = List.of("公路整车", "公路配载/零担", "铁路运输", "水路运输", "跨境海运", "航空运输");
|
||||
List<String> validTransportTypes = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
if (!validTransportTypes.contains(transportType)) {
|
||||
errors.add("运输类型需系统枚举值(公路整车、公路配载/零担、铁路运输、水路运输、跨境海运、航空运输)");
|
||||
errors.add("运输类型需系统枚举值(公路运输、铁路运输、水路运输、航空运输)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,11 +510,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
if (Func.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD");
|
||||
// 宽容解析:接受 2026-8-2、2026/8/2 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(value);
|
||||
if (date == null) {
|
||||
throw new ServiceException(fieldName + " 日期格式无法识别:" + value.trim());
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private void validateImportLength(String value, int maxLength, String fieldName) {
|
||||
|
||||
+5
-8
@@ -60,7 +60,6 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -1346,14 +1345,12 @@ public class TransportReconciliationServiceImpl
|
||||
|
||||
private LocalDateTime parseTimeNullable(String value, String field) {
|
||||
if (Func.isEmpty(value)) return null;
|
||||
for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-M-d HH:mm:ss", "yyyy-M-d HH:mm",
|
||||
"yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/M/d HH:mm:ss", "yyyy/M/d HH:mm")) {
|
||||
try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { }
|
||||
// 宽容解析:统一委托公共解析器,接受 2026-8-2 12:3 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDateTime result = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
|
||||
if (result == null) {
|
||||
throw new ServiceException(field + " 日期格式无法识别:" + value.trim());
|
||||
}
|
||||
for (String pattern : List.of("yyyy-MM-dd", "yyyy-M-d", "yyyy/MM/dd", "yyyy/M/d")) {
|
||||
try { return LocalDate.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); } catch (DateTimeParseException ignored) { }
|
||||
}
|
||||
throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd");
|
||||
return result;
|
||||
}
|
||||
|
||||
private String matchKey(TransportReconciliationInternal row) {
|
||||
|
||||
+29
-13
@@ -67,6 +67,8 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
private static final int MAX_DEDUCT_POINTS = 15;
|
||||
private static final String PROCESSED = "已处理";
|
||||
private static final String UNPROCESSED = "未处理";
|
||||
private static final String CAR = "车辆";
|
||||
private static final String SHIP = "船舶";
|
||||
|
||||
@Override
|
||||
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
|
||||
@@ -81,6 +83,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
prepare(violationRecord);
|
||||
validate(violationRecord);
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
clearIrrelevantField(violationRecord);
|
||||
return saveOrUpdate(violationRecord);
|
||||
}
|
||||
|
||||
@@ -96,11 +99,6 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
ViolationRecordImportExcel excel = data.get(index);
|
||||
try {
|
||||
ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class));
|
||||
if ("船舶".equals(trimToEmpty(excel.getVehicleType()))) {
|
||||
violationRecord.setViolationItem(excel.getViolationTypeOrItem());
|
||||
} else {
|
||||
violationRecord.setViolationType(excel.getViolationTypeOrItem());
|
||||
}
|
||||
prepare(violationRecord);
|
||||
List<String> validationErrors = validateImportViolationRecord(violationRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
@@ -109,6 +107,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
continue;
|
||||
}
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
clearIrrelevantField(violationRecord);
|
||||
violationRecordList.add(violationRecord);
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
@@ -136,6 +135,8 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType()), "船舶不适用于类型,该列应留空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem()), "车辆不适用于事项,该列应留空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空");
|
||||
@@ -183,21 +184,30 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
|
||||
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
|
||||
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
|
||||
if ("车辆".equals(violationRecord.getVehicleType())) {
|
||||
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
violationRecord.setProcessResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空与车船类型不匹配的对侧字段
|
||||
* <p>
|
||||
* 必须在 validate 之后执行:校验需要看到用户填了什么,
|
||||
* 若提前清空,误填的内容会被静默丢弃,用户无从察觉。
|
||||
*/
|
||||
private void clearIrrelevantField(ViolationRecord violationRecord) {
|
||||
if (CAR.equals(violationRecord.getVehicleType())) {
|
||||
violationRecord.setViolationItem(null);
|
||||
} else {
|
||||
violationRecord.setViolationType(null);
|
||||
}
|
||||
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
violationRecord.setProcessResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(ViolationRecord violationRecord) {
|
||||
if (Func.isEmpty(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不能为空");
|
||||
}
|
||||
if (!"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType())) {
|
||||
if (!CAR.equals(violationRecord.getVehicleType()) && !SHIP.equals(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不正确");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getVehicleNo())) {
|
||||
@@ -206,12 +216,18 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
if (Func.isEmpty(violationRecord.getDriverName())) {
|
||||
throw new ServiceException("驾驶人/船长不能为空");
|
||||
}
|
||||
if ("车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
|
||||
if (CAR.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
|
||||
throw new ServiceException("类型不能为空");
|
||||
}
|
||||
if ("船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
|
||||
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
|
||||
throw new ServiceException("事项不能为空");
|
||||
}
|
||||
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType())) {
|
||||
throw new ServiceException("船舶不适用于类型,该列应留空");
|
||||
}
|
||||
if (CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem())) {
|
||||
throw new ServiceException("车辆不适用于事项,该列应留空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getViolationTime())) {
|
||||
throw new ServiceException("时间不能为空");
|
||||
}
|
||||
@@ -290,7 +306,7 @@ public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordM
|
||||
|
||||
private String normalizeVehicleType(String vehicleType) {
|
||||
String value = trimToEmpty(vehicleType);
|
||||
return value.isEmpty() ? "车辆" : value;
|
||||
return value.isEmpty() ? CAR : value;
|
||||
}
|
||||
|
||||
private String normalizeProcessStatus(String processStatus) {
|
||||
|
||||
+25
-10
@@ -42,6 +42,7 @@ import io.minio.PutObjectArgs;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.http.Method;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -79,14 +80,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
private final VoucherImageMapper voucherImageMapper;
|
||||
private final VoucherFileMapper voucherFileMapper;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final MinioClient minioClient;
|
||||
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||
private final String minioBucketName;
|
||||
private final String minioRootDirectory;
|
||||
|
||||
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
|
||||
IWaybillService waybillService,
|
||||
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
|
||||
ApplicationEventPublisher eventPublisher, MinioClient minioClient,
|
||||
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
|
||||
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
|
||||
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
|
||||
@@ -95,7 +96,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
this.voucherImageMapper = voucherImageMapper;
|
||||
this.voucherFileMapper = voucherFileMapper;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.minioClient = minioClient;
|
||||
this.minioClientProvider = minioClientProvider;
|
||||
this.minioBucketName = minioBucketName;
|
||||
this.minioRootDirectory = minioRootDirectory;
|
||||
}
|
||||
@@ -251,7 +252,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey)
|
||||
? List.of(objectKey) : List.of(resolvedObjectKey, objectKey);
|
||||
for (String candidateObjectKey : candidateObjectKeys) {
|
||||
try (InputStream source = minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build());
|
||||
try (InputStream source = minioClient().getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build());
|
||||
OutputStream target = Files.newOutputStream(archivePath)) {
|
||||
source.transferTo(target);
|
||||
return archivePath;
|
||||
@@ -427,7 +428,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -490,7 +491,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -537,7 +538,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||
String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName);
|
||||
try (InputStream inputStream = imageFile.getInputStream()) {
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).build());
|
||||
}
|
||||
@@ -606,7 +607,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo;
|
||||
}
|
||||
|
||||
private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||
private void deleteObjectQuietly(String objectKey) { try { minioClient().removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -838,7 +839,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo,
|
||||
Func.isEmpty(plateNo) ? "root" : plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -1052,6 +1053,20 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MinIO 客户端。
|
||||
* <p>
|
||||
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
|
||||
* 此处给出与 {@link #validateMinioConfig()} 一致的明确提示。
|
||||
*/
|
||||
private MinioClient minioClient() {
|
||||
MinioClient minioClient = minioClientProvider.getIfAvailable();
|
||||
if (minioClient == null) {
|
||||
throw new ServiceException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
|
||||
}
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) {
|
||||
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName);
|
||||
if (Func.isEmpty(minioRootDirectory)) {
|
||||
@@ -1127,7 +1142,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
|
||||
private String buildVoucherFileUrl(Long fileId, String objectKey) {
|
||||
try {
|
||||
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
return minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
|
||||
} catch (Exception exception) {
|
||||
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
|
||||
|
||||
+152
-33
@@ -19,6 +19,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
@@ -51,10 +52,10 @@ import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -68,7 +69,6 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
/** 批量导入状态仅保留草稿与导入完成两种。 */
|
||||
private static final String STATUS_DRAFT = "draft";
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
@@ -184,20 +184,36 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
|
||||
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
||||
|
||||
// 逐行构建运单(未落库),构建失败时报出对应行号
|
||||
List<Waybill> rowWaybills = new ArrayList<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
try {
|
||||
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
// 同一运单标识号的多货物行合并为一条运单,各行转为货物明细
|
||||
Map<Integer, Waybill> waybillByRow = new HashMap<>();
|
||||
Set<Integer> skipRows = new HashSet<>();
|
||||
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
|
||||
|
||||
List<Waybill> waybills = new ArrayList<>();
|
||||
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
if (skipRows.contains(index)) continue;
|
||||
Waybill waybill = waybillByRow.getOrDefault(index, rowWaybills.get(index));
|
||||
// 配载标识号必须取自行数据:运单落库(prepareForSave)会把新建运单的 loadingNo 置空
|
||||
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
||||
try {
|
||||
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
||||
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
|
||||
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
|
||||
waybills.add(waybill);
|
||||
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
||||
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||
}
|
||||
waybills.add(waybill);
|
||||
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
||||
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
||||
}
|
||||
}
|
||||
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
|
||||
batch.setWaybillCount(waybills.size());
|
||||
@@ -218,6 +234,69 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将同一运单标识号的多货物行合并为一条运单:
|
||||
* 首行作为主行(数值字段累加各行),后续行转为货物明细追加到主行货物信息中;
|
||||
* 被 merge 的行记入 skipRows,落库时跳过。
|
||||
*/
|
||||
private void mergeMultiCargoRows(List<Map<String, Object>> rows, List<Waybill> rowWaybills,
|
||||
Map<Integer, Waybill> waybillByRow, Set<Integer> skipRows) {
|
||||
Map<String, List<Integer>> groups = new LinkedHashMap<>();
|
||||
for (int index = 0; index < rowWaybills.size(); index++) {
|
||||
String relationNo = rowWaybills.get(index).getRelationNo();
|
||||
if (Func.isEmpty(relationNo)) continue;
|
||||
groups.computeIfAbsent(relationNo, key -> new ArrayList<>()).add(index);
|
||||
}
|
||||
for (List<Integer> groupRows : groups.values()) {
|
||||
if (groupRows.size() <= 1) continue;
|
||||
Waybill primary = rowWaybills.get(groupRows.get(0));
|
||||
List<Map<String, Object>> goodsList = new ArrayList<>();
|
||||
for (Integer rowIndex : groupRows) {
|
||||
Waybill current = rowWaybills.get(rowIndex);
|
||||
if (!rowIndex.equals(groupRows.get(0))) {
|
||||
// 主行字段保留首行值,数量等数值字段累加同组其余各行
|
||||
mergeNumericFields(primary, current);
|
||||
skipRows.add(rowIndex);
|
||||
waybillByRow.remove(rowIndex);
|
||||
}
|
||||
goodsList.add(buildGoodsItem(rows.get(rowIndex), current));
|
||||
}
|
||||
primary.setGoodsJson(JsonUtil.toJson(goodsList));
|
||||
}
|
||||
}
|
||||
|
||||
/** 合并多货物行时累加数量、其他费用等可加数值字段;为空的字段跳过。 */
|
||||
private void mergeNumericFields(Waybill primary, Waybill current) {
|
||||
primary.setQuantity(sumNullable(primary.getQuantity(), current.getQuantity()));
|
||||
primary.setOtherFeeTotal(sumNullable(primary.getOtherFeeTotal(), current.getOtherFeeTotal()));
|
||||
}
|
||||
|
||||
private BigDecimal sumNullable(BigDecimal first, BigDecimal second) {
|
||||
if (first == null) return second;
|
||||
if (second == null) return first;
|
||||
return first.add(second);
|
||||
}
|
||||
|
||||
/** 由导入行构建一条货物明细,字段与运单详情货物编辑结构保持一致。 */
|
||||
private Map<String, Object> buildGoodsItem(Map<String, Object> row, Waybill waybill) {
|
||||
Map<String, Object> goods = new LinkedHashMap<>();
|
||||
putIfNotEmpty(goods, "cargoName", waybill.getCargoName());
|
||||
putIfNotEmpty(goods, "cargoType", waybill.getCargoType());
|
||||
putIfNotEmpty(goods, "specification", waybill.getSpecification());
|
||||
putIfNotEmpty(goods, "model", waybill.getModel());
|
||||
putIfNotEmpty(goods, "packageType", stringValue(row, "packageType", "包装"));
|
||||
if (waybill.getQuantity() != null) {
|
||||
goods.put("quantity", waybill.getQuantity().toPlainString());
|
||||
}
|
||||
putIfNotEmpty(goods, "quantityUnit", waybill.getQuantityUnit());
|
||||
putIfNotEmpty(goods, "remark", waybill.getRemark());
|
||||
return goods;
|
||||
}
|
||||
|
||||
private void putIfNotEmpty(Map<String, Object> goods, String key, String value) {
|
||||
if (Func.isNotEmpty(value)) goods.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
|
||||
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
|
||||
@@ -386,11 +465,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
throw new ServiceException(fieldName + "不能为空");
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
try {
|
||||
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD HH:mm:ss");
|
||||
// 宽容解析:接受 2026-8-2、2026-9-1 8:0:0 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
|
||||
if (date == null) {
|
||||
throw new ServiceException(fieldName + " 日期格式无法识别:" + text);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private BigDecimal defaultQuantity(BigDecimal quantity) {
|
||||
@@ -523,6 +603,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
// 16. 同一运单标识号校验
|
||||
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
// 17. 同一运单标识号组内一致性校验(地址/配载标识号/数量单位,多货物合并的前提)
|
||||
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errorMap.put(i, String.join("; ", errors));
|
||||
}
|
||||
@@ -571,21 +654,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
if (!isRoadTransport(transportType)) {
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(vehicleNo)) {
|
||||
errors.add("公路运输时车牌号/航班号/船号/班列号不能为空");
|
||||
return;
|
||||
}
|
||||
if (vehicleNo.length() < 7) {
|
||||
errors.add("车牌号长度不能少于7位");
|
||||
}
|
||||
if (vehicleNo.length() > 8) {
|
||||
errors.add("车牌号长度不能超过8位");
|
||||
}
|
||||
if (!VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()) {
|
||||
errors.add("车牌号首位必须是省份简称,第二位必须是英文字母");
|
||||
}
|
||||
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
|
||||
errors.add("车牌号格式不正确,应为首位省份简称、次位英文字母、总长度7或8位");
|
||||
// 必填、长度与格式统一为一条简短原因,避免多条规则叠加导致失败原因过长;
|
||||
// 文案保留“车牌号”字样,导入失败明细仍能据此定位到车牌号列并标红。
|
||||
// VEHICLE_NO_PATTERN 已约束总长度为 7 或 8 位,故不再单独判断长度。
|
||||
if (Func.isEmpty(vehicleNo)
|
||||
|| !VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()
|
||||
|| !VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
|
||||
errors.add("车牌号校验不通过");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,18 +901,62 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一运单标识号组内的发货地址、到货地址、数量单位、配载标识号必须一致:
|
||||
* 多货物行会合并为一条运单,上述字段不一致时无法合并(数量累加也要求单位相同)。
|
||||
*/
|
||||
private void validateWaybillIdentifierConsistency(Map<String, Object> row, int rowIndex,
|
||||
Map<String, List<Integer>> waybillIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
|
||||
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
|
||||
if (Func.isEmpty(waybillIdentifier)) {
|
||||
return;
|
||||
}
|
||||
List<Integer> sameIdentifierRows = waybillIdentifierMap.get(waybillIdentifier);
|
||||
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "departureAddress", "发货地址", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "arrivalAddress", "到货地址", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "quantityUnit", "数量单位", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "loadingIdentifier", "配载标识号", errors);
|
||||
}
|
||||
|
||||
/** 比较同一运单标识号组内其余行的字段值,不一致时记录错误;空值不参与比较。 */
|
||||
private void compareGroupValue(int rowIndex, List<Integer> sameIdentifierRows, List<Map<String, Object>> allRows,
|
||||
String field, String fieldName, List<String> errors) {
|
||||
String currentValue = normalizeCompareValue(stringValue(allRows.get(rowIndex), field, fieldName));
|
||||
if (Func.isEmpty(currentValue)) {
|
||||
return;
|
||||
}
|
||||
for (Integer otherRowIndex : sameIdentifierRows) {
|
||||
if (otherRowIndex <= rowIndex) {
|
||||
continue;
|
||||
}
|
||||
String otherValue = normalizeCompareValue(stringValue(allRows.get(otherRowIndex), field, fieldName));
|
||||
if (Func.isNotEmpty(otherValue) && !currentValue.equals(otherValue)) {
|
||||
errors.add("同一运单标识号下," + fieldName + "必须一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeCompareValue(String value) {
|
||||
return Func.isEmpty(value) ? null : value.replaceAll("\\s+", "");
|
||||
}
|
||||
|
||||
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = String.valueOf(value).trim();
|
||||
try {
|
||||
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
errors.add(fieldName + "格式必须为日期格式(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)");
|
||||
// 宽容解析:接受 2026-8-2、2026-9-1 8:0:0 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
|
||||
if (date == null) {
|
||||
errors.add(fieldName + " 日期格式无法识别:" + text);
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private Map<String, String> loadTransportTypeOptions() {
|
||||
@@ -861,8 +980,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
// 字典读取失败时使用默认值。
|
||||
}
|
||||
if (options.isEmpty()) {
|
||||
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输",
|
||||
"公路整车", "公路配载/零担", "铁路整车", "铁路零担", "水路", "航空", "多式联运", "管道运输", "其他");
|
||||
// 运输方式仅支持公路、铁路、水路、航空四种
|
||||
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
defaults.forEach(value -> options.put(value, value));
|
||||
}
|
||||
return options;
|
||||
|
||||
+6
@@ -902,7 +902,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
WaybillExcel excel = data.get(index);
|
||||
try {
|
||||
Waybill waybill = Objects.requireNonNull(BeanUtil.copyProperties(excel, Waybill.class));
|
||||
waybill.setEstimatedStartTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedStartTime(), "预计发货日期"));
|
||||
waybill.setEstimatedEndTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedEndTime(), "预计完成日期"));
|
||||
waybill.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
|
||||
waybill.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
|
||||
waybill.setDataSource("批量导入");
|
||||
waybill.setCreateTime(null);
|
||||
waybill.setUpdateTime(null);
|
||||
waybill.setCarrierJson(buildImportCarrierJson(waybill));
|
||||
submit(waybill);
|
||||
} catch (Exception exception) {
|
||||
|
||||
Reference in New Issue
Block a user