Merge remote-tracking branch 'websoft/master'

This commit is contained in:
2026-09-21 22:33:12 +08:00
59 changed files with 2827 additions and 2073 deletions
+9
View File
@@ -22,6 +22,15 @@
<groupId>org.springblade</groupId> <groupId>org.springblade</groupId>
<artifactId>blade-starter-loadbalancer</artifactId> <artifactId>blade-starter-loadbalancer</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-log</artifactId>
<!-- providedLenientDateParser 编译期需要 ServiceException(位于 starter-log),
但 provided 不向下游传递——否则网关会经 common 拿到 blade-core-tool 的
BladeConverterConfigurationimplements WebMvcConfigurer),在无 spring-webmvc
的 WebFlux 网关里启动即崩。业务服务经 blade-core-boot 自带 starter-log,运行时不受影响。 -->
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springblade</groupId> <groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId> <artifactId>blade-core-auto</artifactId>
@@ -46,9 +46,11 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
/** /**
* 导入失败明细 Excel 导出工具类 * 导入失败明细 Excel 导出工具类
@@ -95,7 +97,7 @@ public class ImportFailureExcelUtil {
* @param excelClass 原导入 Excel 类型 * @param excelClass 原导入 Excel 类型
*/ */
public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) { 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, public static void exportFailureReasonOnly(HttpServletResponse response, String fileName, String sheetName,
List<?> data, Class<?> excelClass) { 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.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
@@ -118,7 +130,7 @@ public class ImportFailureExcelUtil {
List<List<Object>> rows = buildRows(data, excelFields); List<List<Object>> rows = buildRows(data, excelFields);
try { try {
FastExcel.write(response.getOutputStream()) FastExcel.write(response.getOutputStream())
.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields.size())) .registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows, markErrorColumns))
.head(head) .head(head)
.sheet(sheetName) .sheet(sheetName)
.doWrite(rows); .doWrite(rows);
@@ -204,15 +216,59 @@ public class ImportFailureExcelUtil {
throw new NoSuchFieldException(String.join(",", fieldNames)); 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 static class ImportFailureCellStyleHandler implements CellWriteHandler {
private final int failureReasonColumnIndex; 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> redStyleCache = new HashMap<>();
private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>(); private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
private final Map<Integer, Integer> columnWidthCache = new HashMap<>(); private final Map<Integer, Integer> columnWidthCache = new HashMap<>();
private ImportFailureCellStyleHandler(int failureReasonColumnIndex) { private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows, boolean markErrorColumns) {
this.failureReasonColumnIndex = failureReasonColumnIndex; 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 @Override
@@ -230,11 +286,83 @@ public class ImportFailureExcelUtil {
return; return;
} }
adjustColumnWidth(cell); adjustColumnWidth(cell);
if (cell.getColumnIndex() == failureReasonColumnIndex) { if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
return;
}
if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
markRed(cell); 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) { private void markRed(Cell cell) {
CellStyle currentStyle = cell.getCellStyle(); CellStyle currentStyle = cell.getCellStyle();
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> { 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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -1,263 +1,276 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development. * not liable for any claims arising from secondary or illegal development.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.system.controller; package org.springblade.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query; import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R; import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.excel.PortTerminalExcel; import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.system.excel.PortTerminalExportExcel; import org.springblade.system.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalImporter; import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.pojo.entity.PortTerminal; import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.pojo.vo.PortTerminalVO; import org.springblade.system.excel.PortTerminalImporter;
import org.springblade.system.service.IPortTerminalService; import org.springblade.system.pojo.entity.PortTerminal;
import org.springblade.system.wrapper.PortTerminalWrapper; import org.springblade.system.pojo.vo.PortTerminalVO;
import org.springframework.web.bind.annotation.GetMapping; import org.springblade.system.service.IPortTerminalService;
import org.springframework.web.bind.annotation.PostMapping; import org.springblade.system.wrapper.PortTerminalWrapper;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList; import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map; import java.util.ArrayList;
import java.util.List;
/** import java.util.Map;
* 港口码头主数据 控制器
* /**
* @author Chill * 港口码头主数据 控制器
*/ *
@NonDS * @author Chill
@RestController */
@AllArgsConstructor @NonDS
@PreAuth(menu = "port_terminal") @RestController
@RequestMapping("/port-terminal") @AllArgsConstructor
@Tag(name = "港口码头主数据", description = "港口码头主数据") @PreAuth(menu = "port_terminal")
public class PortTerminalController extends BladeController { @RequestMapping("/port-terminal")
@Tag(name = "港口码头主数据", description = "港口码头主数据")
private static final int DEFAULT_CURRENT = 1; public class PortTerminalController extends BladeController {
private static final int DEFAULT_SIZE = 10;
private static final int MAX_SIZE = 100; private static final int DEFAULT_CURRENT = 1;
private static final String SOURCE_INITIAL = "初始化录入"; private static final int DEFAULT_SIZE = 10;
private static final String SOURCE_INITIAL_IMPORT = "初始化导入"; private static final int MAX_SIZE = 100;
private static final String SOURCE_INITIAL_OLD = "初始"; private static final String SOURCE_INITIAL = "初始化录";
private static final String SOURCE_MANUAL = "手动录"; private static final String SOURCE_INITIAL_IMPORT = "初始化导";
private static final String SOURCE_MANUAL_OLD = "手工导入"; private static final String SOURCE_INITIAL_OLD = "初始导入";
private static final String SOURCE_MANUAL = "手动录入";
private final IPortTerminalService portTerminalService; private static final String SOURCE_MANUAL_OLD = "手工导入";
/** private final IPortTerminalService portTerminalService;
* 详情
*/ /**
@GetMapping("/detail") * 详情
@ApiOperationSupport(order = 1) */
@Operation(summary = "详情", description = "传入portTerminal") @GetMapping("/detail")
public R<PortTerminalVO> detail(PortTerminal portTerminal) { @ApiOperationSupport(order = 1)
if (Func.isEmpty(portTerminal.getId())) { @Operation(summary = "详情", description = "传入portTerminal")
return R.fail("主键不能为空"); public R<PortTerminalVO> detail(PortTerminal portTerminal) {
} if (Func.isEmpty(portTerminal.getId())) {
PortTerminal detail = portTerminalService.getById(portTerminal.getId()); return R.fail("主键不能为空");
if (Func.isEmpty(detail)) { }
return R.fail("港口码头不存在"); PortTerminal detail = portTerminalService.getById(portTerminal.getId());
} if (Func.isEmpty(detail)) {
detail.setDataSource(normalizeDataSource(detail.getDataSource())); return R.fail("港口码头不存在");
return R.data(PortTerminalWrapper.build().entityVO(detail)); }
} detail.setDataSource(normalizeDataSource(detail.getDataSource()));
return R.data(PortTerminalWrapper.build().entityVO(detail));
/** }
* 分页
*/ /**
@GetMapping("/list") * 分页
@ApiOperationSupport(order = 2) */
@Operation(summary = "分页", description = "传入portTerminal") @GetMapping("/list")
public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) { @ApiOperationSupport(order = 2)
IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal); @Operation(summary = "分页", description = "传入portTerminal")
return R.data(pages); public R<IPage<PortTerminalVO>> list(PortTerminalVO portTerminal, Query query) {
} IPage<PortTerminalVO> pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
return R.data(pages);
/** }
* 新增或修改
*/ /**
@PostMapping("/submit") * 新增或修改
@ApiOperationSupport(order = 3) */
@Operation(summary = "新增或修改", description = "传入portTerminal") @PostMapping("/submit")
public R submit(@Valid @RequestBody PortTerminal portTerminal) { @ApiOperationSupport(order = 3)
return R.status(portTerminalService.submit(portTerminal)); @Operation(summary = "新增或修改", description = "传入portTerminal")
} public R submit(@Valid @RequestBody PortTerminal portTerminal) {
return R.status(portTerminalService.submit(portTerminal));
/** }
* 删除
*/ /**
@PostMapping("/remove") * 删除
@ApiOperationSupport(order = 4) */
@Operation(summary = "逻辑删除", description = "传入ids") @PostMapping("/remove")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { @ApiOperationSupport(order = 4)
if (Func.isEmpty(ids)) { @Operation(summary = "逻辑删除", description = "传入ids")
return R.fail("主键不能为空"); public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
} if (Func.isEmpty(ids)) {
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids))); return R.fail("主键不能为空");
} }
return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
/** }
* 启用或停用
*/ /**
@PostMapping("/status") * 启用或停用
@ApiOperationSupport(order = 5) */
@Operation(summary = "启用或停用", description = "传入id和status") @PostMapping("/status")
public R status(@Parameter(description = "主键", required = true) @RequestParam Long id, @ApiOperationSupport(order = 5)
@Parameter(description = "状态", required = true) @RequestParam Integer status) { @Operation(summary = "启用或停用", description = "传入id和status")
return R.status(portTerminalService.changeStatus(id, status)); public R status(@Parameter(description = "主键", required = true) @RequestParam Long id,
} @Parameter(description = "状态", required = true) @RequestParam Integer status) {
return R.status(portTerminalService.changeStatus(id, status));
/** }
* 上级港口下拉数据源
*/ /**
@GetMapping("/port-select") * 上级港口下拉数据源
@ApiOperationSupport(order = 6) */
@Operation(summary = "上级港口下拉数据源") @GetMapping("/port-select")
public R<List<PortTerminal>> portSelect() { @ApiOperationSupport(order = 6)
List<PortTerminal> ports = portTerminalService.selectEnabledPorts(); @Operation(summary = "上级港口下拉数据源")
ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource()))); public R<List<PortTerminal>> portSelect() {
return R.data(ports); List<PortTerminal> ports = portTerminalService.selectEnabledPorts();
} ports.forEach(port -> port.setDataSource(normalizeDataSource(port.getDataSource())));
return R.data(ports);
/** }
* 导入港口码头主数据
*/ /**
@PostMapping("/import-port-terminal") * 导入港口码头主数据
@ApiOperationSupport(order = 7) */
@Operation(summary = "导入港口码头主数据", description = "传入excel") @PostMapping("/import-port-terminal")
public R importPortTerminal(MultipartFile file, HttpServletResponse response) { @ApiOperationSupport(order = 7)
if (file == null || file.isEmpty()) { @Operation(summary = "导入港口码头主数据", description = "传入excel")
return R.fail("上传文件不能为空"); public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
} if (file == null || file.isEmpty()) {
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); return R.fail("上传文件不能为空");
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { }
return R.fail("请上传 .xls,.xlsx 标准格式文件"); String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
} if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
List<PortTerminalExcel> failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class)); return R.fail("请上传 .xls,.xlsx 标准格式文件");
if (Func.isNotEmpty(failureList)) { }
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class); try {
return null; portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
} } catch (ImportFailureException exception) {
return R.success("操作成功"); // 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
} exportFailure(response, exception.getFailureList());
return null;
/** }
* 导出港口码头主数据 return R.success("操作成功");
*/ }
@GetMapping("/export-port-terminal")
@ApiOperationSupport(order = 8) /**
@Operation(summary = "导出港口码头主数据") * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) { * <p>
Object ids = portTerminal.remove("ids"); * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
Object dataSource = portTerminal.remove("dataSource"); */
portTerminal.remove("Blade-Auth"); private void exportFailure(HttpServletResponse response, List<?> failureList) {
portTerminal.remove("Authorization"); ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
portTerminal.remove("access_token"); }
normalizeRegionCodeCondition(portTerminal);
QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class); /**
applyDataSourceCondition(queryWrapper, dataSource); * 导出港口码头主数据
if (Func.isNotEmpty(ids)) { */
queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString())); @GetMapping("/export-port-terminal")
} @ApiOperationSupport(order = 8)
List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper); @Operation(summary = "导出港口码头主数据")
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class); public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map<String, Object> portTerminal, HttpServletResponse response) {
} Object ids = portTerminal.remove("ids");
Object dataSource = portTerminal.remove("dataSource");
/** portTerminal.remove("Blade-Auth");
* 导出模板 portTerminal.remove("Authorization");
*/ portTerminal.remove("access_token");
@GetMapping("/export-template") normalizeRegionCodeCondition(portTerminal);
@ApiOperationSupport(order = 9) QueryWrapper<PortTerminal> queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
@Operation(summary = "导出模板") applyDataSourceCondition(queryWrapper, dataSource);
public void exportTemplate(HttpServletResponse response) { if (Func.isNotEmpty(ids)) {
List<PortTerminalExcel> list = new ArrayList<>(); queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class); }
} List<PortTerminalExportExcel> list = portTerminalService.exportPortTerminal(queryWrapper);
ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
private Query normalizeQuery(Query query) { }
if (query == null) {
query = new Query(); /**
} * 导出模板
if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) { */
query.setCurrent(DEFAULT_CURRENT); @GetMapping("/export-template")
} @ApiOperationSupport(order = 9)
if (query.getSize() == null || query.getSize() <= 0) { @Operation(summary = "导出模板")
query.setSize(DEFAULT_SIZE); public void exportTemplate(HttpServletResponse response) {
} List<PortTerminalExcel> list = new ArrayList<>();
if (query.getSize() > MAX_SIZE) { ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
query.setSize(MAX_SIZE); }
}
return query; private Query normalizeQuery(Query query) {
} if (query == null) {
query = new Query();
private void normalizeRegionCodeCondition(Map<String, Object> params) { }
Object regionCode = params.remove("regionCode"); if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) { query.setCurrent(DEFAULT_CURRENT);
params.put("districtCode", regionCode); }
} if (query.getSize() == null || query.getSize() <= 0) {
} query.setSize(DEFAULT_SIZE);
}
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) { if (query.getSize() > MAX_SIZE) {
String value = Func.toStrWithEmpty(dataSource, ""); query.setSize(MAX_SIZE);
if (Func.isEmpty(value)) { }
return; return query;
} }
if (SOURCE_INITIAL.equals(value)) {
queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD); private void normalizeRegionCodeCondition(Map<String, Object> params) {
} else if (SOURCE_MANUAL.equals(value)) { Object regionCode = params.remove("regionCode");
queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD); if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
} else { params.put("districtCode", regionCode);
queryWrapper.eq("data_source", value); }
} }
}
private void applyDataSourceCondition(QueryWrapper<PortTerminal> queryWrapper, Object dataSource) {
private String normalizeDataSource(String dataSource) { String value = Func.toStrWithEmpty(dataSource, "");
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) { if (Func.isEmpty(value)) {
return SOURCE_INITIAL; return;
} }
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource; if (SOURCE_INITIAL.equals(value)) {
} queryWrapper.in("data_source", SOURCE_INITIAL, SOURCE_INITIAL_IMPORT, SOURCE_INITIAL_OLD);
} else if (SOURCE_MANUAL.equals(value)) {
} queryWrapper.in("data_source", SOURCE_MANUAL, SOURCE_MANUAL_OLD);
} else {
queryWrapper.eq("data_source", value);
}
}
private String normalizeDataSource(String dataSource) {
if (SOURCE_INITIAL_IMPORT.equals(dataSource) || SOURCE_INITIAL_OLD.equals(dataSource)) {
return SOURCE_INITIAL;
}
return SOURCE_MANUAL_OLD.equals(dataSource) ? SOURCE_MANUAL : dataSource;
}
}
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 币种汇率 Excel * 币种汇率 Excel
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("汇率") @ExcelProperty("汇率")
private BigDecimal exchangeRate; private BigDecimal exchangeRate;
@ExcelProperty("生效日期") @ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate effectiveDate; private String effectiveDate;
@ExcelProperty("状态") @ExcelProperty("状态")
private String statusName; private String statusName;
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("来源") @ExcelProperty("来源")
private String dataSource; private String dataSource;
@ExcelProperty("失效日期") @ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate expiryDate; private String expiryDate;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -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;
}
}
@@ -34,7 +34,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
/** /**
* UserExcel * UserExcel
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
private String postName; private String postName;
@ColumnWidth(20) @ColumnWidth(20)
@ExcelProperty("生日") @ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private Date birthday; private String birthday;
} }
@@ -95,7 +95,7 @@
</select> </select>
<select id="exportUser" resultType="org.springblade.system.excel.UserExcel"> <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>
<select id="selectCustomerOptions" resultType="java.util.HashMap"> <select id="selectCustomerOptions" resultType="java.util.HashMap">
@@ -125,6 +125,8 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
CurrencyExcel excel = data.get(index); CurrencyExcel excel = data.get(index);
try { try {
Currency currency = Objects.requireNonNull(BeanUtil.copyProperties(excel, Currency.class)); 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.setDataSource(SOURCE_BATCH);
currency.setStatus(STATUS_ENABLED); currency.setStatus(STATUS_ENABLED);
prepare(currency, SOURCE_BATCH); prepare(currency, SOURCE_BATCH);
@@ -689,6 +689,9 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
*/ */
private User buildImportUser(UserExcel userExcel, String tenantId) { private User buildImportUser(UserExcel userExcel, String tenantId) {
User user = Objects.requireNonNull(BeanUtil.copyProperties(userExcel, User.class)); 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.setTenantId(tenantId);
user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1)); user.setUserType(Func.toInt(DictCache.getKey(DictEnum.USER_TYPE, userExcel.getUserTypeName()), 1));
user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY)); user.setDeptId(Func.toStrWithEmpty(SysCache.getDeptIds(tenantId, userExcel.getDeptName()), StringPool.EMPTY));
@@ -6,14 +6,20 @@ package org.springblade.transport.config;
import io.minio.MinioClient; import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**
* 凭证图片 MinIO 客户端配置。 * 凭证图片 MinIO 客户端配置。
* <p>
* 连接参数由 Nacos 的 file.storage.minio 配置提供。 * 连接参数由 Nacos 的 file.storage.minio 配置提供。
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
*/ */
@Configuration @Configuration
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
public class VoucherMinioConfig { public class VoucherMinioConfig {
@Bean @Bean
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO; import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.IProcessConfigService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -83,20 +84,34 @@ public class ProcessConfigController extends BladeController {
private final VoucherFileMapper voucherFileMapper; private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper; private final VoucherImageMapper voucherImageMapper;
private final VoucherManageMapper voucherManageMapper; private final VoucherManageMapper voucherManageMapper;
private final MinioClient minioClient; private final ObjectProvider<MinioClient> minioClientProvider;
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName; private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper, public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper, VoucherManageMapper voucherManageMapper,
MinioClient minioClient) { ObjectProvider<MinioClient> minioClientProvider) {
this.processConfigService = processConfigService; this.processConfigService = processConfigService;
this.waybillMapper = waybillMapper; this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper; this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper; this.voucherImageMapper = voucherImageMapper;
this.voucherManageMapper = voucherManageMapper; 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") @GetMapping("/detail")
@@ -147,7 +162,7 @@ public class ProcessConfigController extends BladeController {
result.put("waybillNo", image.getWaybillNo()); result.put("waybillNo", image.getWaybillNo());
result.put("objectKey", image.getObjectKey()); result.put("objectKey", image.getObjectKey());
try { try {
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey()) .method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
.expiry(1, TimeUnit.HOURS).build())); .expiry(1, TimeUnit.HOURS).build()));
} catch (Exception e) { } catch (Exception e) {
@@ -270,7 +285,7 @@ public class ProcessConfigController extends BladeController {
result.put("objectKey", image.objectKey()); result.put("objectKey", image.objectKey());
result.put("matched", image.matched()); result.put("matched", image.matched());
try { try {
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.objectKey()) .method(Method.GET).bucket(minioBucketName).object(image.objectKey())
.expiry(1, TimeUnit.HOURS).build())); .expiry(1, TimeUnit.HOURS).build()));
} catch (Exception exception) { } catch (Exception exception) {
@@ -36,7 +36,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 事故记录 Excel * 事故记录 Excel
@@ -60,8 +59,8 @@ public class AccidentRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty("*事故发生日期") @ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate accidentDate; private String accidentDate;
@ExcelProperty("事故发生地点") @ExcelProperty("事故发生地点")
private String accidentLocation; private String accidentLocation;
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 事故记录导出 Excel * 事故记录导出 Excel
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 年检记录 Excel * 年检记录 Excel
@@ -59,11 +58,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty("*检测评定日期") @ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate inspectionAssessmentDate; private String inspectionAssessmentDate;
@ExcelProperty("*有效期截止日") @ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate validUntilDate; private String validUntilDate;
@ExcelProperty("*车辆技术等级") @ExcelProperty("*车辆技术等级")
private String vehicleTechnicalLevel; private String vehicleTechnicalLevel;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 年检记录导出 Excel * 年检记录导出 Excel
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -1,96 +1,99 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import lombok.Data; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serial;
import java.math.BigDecimal; import java.io.Serializable;
import java.util.Date; import java.math.BigDecimal;
import java.util.Date;
/**
* 常用地址导出 Excel /**
* * 常用地址导出 Excel
* @author Chill *
*/ * @author Chill
@Data */
@ColumnWidth(18) @Data
@HeadRowHeight(20) @ColumnWidth(18)
@ContentRowHeight(18) @HeadRowHeight(20)
public class CommonAddressExportExcel implements Serializable { @ContentRowHeight(18)
public class CommonAddressExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L; @Serial
private static final long serialVersionUID = 1L;
@ExcelIgnore
private Long id; @ExcelIgnore
private Long id;
@ExcelProperty("地址名称")
private String addressName; @ExcelProperty("地址名称")
private String addressName;
@ExcelProperty("地址编号")
private String addressCode; @ExcelProperty("地址编号")
private String addressCode;
@ExcelProperty("类型")
private String addressType; @ExcelProperty("类型")
private String addressType;
@ExcelProperty("站点编码")
private String siteCodeDisplay; @ExcelProperty("站点编码")
private String siteCodeDisplay;
@ExcelProperty("详细地址")
private String detailAddress; @ExcelProperty("详细地址")
private String detailAddress;
@ExcelProperty("经度")
private BigDecimal longitude; @ExcelProperty("经度")
private BigDecimal longitude;
@ExcelProperty("纬度")
private BigDecimal latitude; @ExcelProperty("纬度")
private BigDecimal latitude;
@ExcelProperty("行政区划")
private String regionName; @ExcelProperty("行政区划")
private String regionName;
@ExcelProperty("联系人")
private String contactName; @ExcelProperty("联系人")
private String contactName;
@ExcelProperty("联系方式")
private String contactPhone; @ExcelProperty("联系方式")
private String contactPhone;
@ExcelProperty("组织")
private String deptName; @ExcelProperty("组织")
private String deptName;
@ExcelProperty("备注")
private String remark; @ExcelProperty("备注")
private String remark;
@ExcelProperty("更新时间")
private Date updateTime; @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty("创建时间") private Date updateTime;
private Date createTime;
@ExcelProperty("创建时间")
} @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,101 +1,104 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.NumberFormat; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import lombok.Data; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serial;
import java.math.BigDecimal; import java.io.Serializable;
import java.util.Date; import java.math.BigDecimal;
import java.util.Date;
/**
* 常用货物导出 Excel /**
* * 常用货物导出 Excel
* @author Chill *
*/ * @author Chill
@Data */
@ColumnWidth(24) @Data
@HeadRowHeight(20) @ColumnWidth(24)
@ContentRowHeight(18) @HeadRowHeight(20)
public class CommonCargoExportExcel implements Serializable { @ContentRowHeight(18)
public class CommonCargoExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L; @Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("货物名称")
private String cargoName; @ExcelProperty("货物名称")
private String cargoName;
@ExcelProperty("货物编号后缀")
private String cargoCodeSuffix; @ExcelProperty("货物编号后缀")
private String cargoCodeSuffix;
@ExcelProperty("一级货物类型")
private String firstCargoTypeName; @ExcelProperty("一级货物类型")
private String firstCargoTypeName;
@ExcelProperty("二级货物类型")
private String secondCargoTypeName; @ExcelProperty("二级货物类型")
private String secondCargoTypeName;
@ExcelProperty("二级货物类型编码")
private String secondCargoTypeCode; @ExcelProperty("二级货物类型编码")
private String secondCargoTypeCode;
@ExcelProperty("包装")
private String packageType; @ExcelProperty("包装")
private String packageType;
@ExcelProperty("品牌")
private String brand; @ExcelProperty("品牌")
private String brand;
@ExcelProperty("规格")
private String specification; @ExcelProperty("规格")
private String specification;
@ExcelProperty("型号")
private String model; @ExcelProperty("型号")
private String model;
@ExcelProperty("单价")
@NumberFormat("0.00") @ExcelProperty("单价")
private BigDecimal cargoValue; @NumberFormat("0.00")
private BigDecimal cargoValue;
@ExcelProperty("计价单位")
private String priceUnit; @ExcelProperty("计价单位")
private String priceUnit;
@ExcelProperty("说明")
private String descriptionOne; @ExcelProperty("说明")
private String descriptionOne;
@ExcelProperty("备注")
private String remark; @ExcelProperty("备注")
private String remark;
@ExcelProperty("组织")
private String deptName; @ExcelProperty("组织")
private String deptName;
@ExcelProperty("更新时间")
private Date updateTime; @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty("创建时间") private Date updateTime;
private Date createTime;
@ExcelProperty("创建时间")
} @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,82 +1,85 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import lombok.Data; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serial;
import java.util.Date; import java.io.Serializable;
import java.util.Date;
/**
* 常用线路导出 Excel /**
* * 常用线路导出 Excel
* @author Chill *
*/ * @author Chill
@Data */
@ColumnWidth(22) @Data
@HeadRowHeight(20) @ColumnWidth(22)
@ContentRowHeight(18) @HeadRowHeight(20)
public class CommonRouteExportExcel implements Serializable { @ContentRowHeight(18)
public class CommonRouteExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L; @Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("线路编号")
private String routeCode; @ExcelProperty("线路编号")
@ExcelProperty("线路名称") private String routeCode;
private String routeName; @ExcelProperty("线路名称")
@ExcelProperty("发货地") private String routeName;
private String departureName; @ExcelProperty("发货地")
@ExcelProperty("发货地址") private String departureName;
private String departureAddress; @ExcelProperty("发货地址")
@ExcelProperty("发货联系人") private String departureAddress;
private String departureContact; @ExcelProperty("发货联系人")
@ExcelProperty("发货联系方式") private String departureContact;
private String departurePhone; @ExcelProperty("发货联系方式")
@ExcelProperty("收货地") private String departurePhone;
private String arrivalName; @ExcelProperty("收货地")
@ExcelProperty("收货地址") private String arrivalName;
private String arrivalAddress; @ExcelProperty("收货地址")
@ExcelProperty("收货联系人") private String arrivalAddress;
private String arrivalContact; @ExcelProperty("收货联系人")
@ExcelProperty("收货联系方式") private String arrivalContact;
private String arrivalPhone; @ExcelProperty("收货联系方式")
@ExcelProperty("所属组织") private String arrivalPhone;
private String deptName; @ExcelProperty("所属组织")
@ExcelProperty("备注") private String deptName;
private String remark; @ExcelProperty("备注")
@ExcelProperty("创建人") private String remark;
private String createUserName; @ExcelProperty("创建人")
@ExcelProperty("更新人") private String createUserName;
private String updateUserName; @ExcelProperty("更新人")
@ExcelProperty("更新时间") private String updateUserName;
private Date updateTime; @ExcelProperty("更新时间")
@ExcelProperty("创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date updateTime;
@ExcelProperty("创建时间")
} @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -1,118 +1,121 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.NumberFormat; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.format.NumberFormat;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import lombok.Data; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serial;
import java.math.BigDecimal; import java.io.Serializable;
import java.time.LocalDate; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDate;
import java.util.Date;
/**
* 合同管理 Excel /**
* * 合同管理 Excel
* @author Chill *
*/ * @author Chill
@Data */
@ColumnWidth(22) @Data
@HeadRowHeight(20) @ColumnWidth(22)
@ContentRowHeight(18) @HeadRowHeight(20)
public class ContractManageExcel implements Serializable { @ContentRowHeight(18)
public class ContractManageExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L; @Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("合同编号")
private String contractNo; @ExcelProperty("合同编号")
@ExcelProperty("合同名称") private String contractNo;
private String contractName; @ExcelProperty("合同名称")
@ExcelProperty("所属项目") private String contractName;
private String projectName; @ExcelProperty("所属项目")
@ExcelProperty("所属组织") private String projectName;
private String organizationName; @ExcelProperty("所属组织")
@ExcelProperty("合同类别") private String organizationName;
private String contractCategory; @ExcelProperty("合同类别")
@ExcelProperty("签约类型") private String contractCategory;
private String signType; @ExcelProperty("签约类型")
@ExcelProperty("甲方") private String signType;
private String partyA; @ExcelProperty("甲方")
@ExcelProperty("乙方") private String partyA;
private String partyB; @ExcelProperty("乙方")
@ExcelProperty("开始日期") private String partyB;
private LocalDate startDate; @ExcelProperty("开始日期")
@ExcelProperty("结束日期") private LocalDate startDate;
private LocalDate endDate; @ExcelProperty("结束日期")
@ExcelProperty("临时效力起") private LocalDate endDate;
private LocalDate temporaryStartDate; @ExcelProperty("临时效力起")
@ExcelProperty("临时效力止") private LocalDate temporaryStartDate;
private LocalDate temporaryEndDate; @ExcelProperty("临时效力止")
@ExcelProperty("经办人") private LocalDate temporaryEndDate;
private String handlerUserName; @ExcelProperty("经办人")
@ExcelProperty("合同阶段") private String handlerUserName;
private String contractStage; @ExcelProperty("合同阶段")
@ExcelProperty("审核状态") private String contractStage;
private String approvalStatus; @ExcelProperty("审核状态")
@ExcelProperty("归档状态") private String approvalStatus;
private String archiveStatus; @ExcelProperty("归档状态")
@ExcelProperty("结算币种") private String archiveStatus;
private String settlementCurrency; @ExcelProperty("结算币种")
@ExcelProperty("结算方式") private String settlementCurrency;
private String settlementMode; @ExcelProperty("结算方式")
@ExcelProperty("开票周期(天)") private String settlementMode;
private Integer invoiceCycle; @ExcelProperty("开票周期(天)")
@ExcelProperty("一式份数") private Integer invoiceCycle;
private Integer copyCount; @ExcelProperty("一式份数")
@ExcelProperty("回款账期(天)") private Integer copyCount;
private Integer paymentDays; @ExcelProperty("回款账期(天)")
@ExcelProperty("合同金额") private Integer paymentDays;
@NumberFormat("0.00") @ExcelProperty("合同金额")
private BigDecimal contractAmount; @NumberFormat("0.00")
@ExcelProperty("是否范本") private BigDecimal contractAmount;
private Integer templateFlag; @ExcelProperty("是否范本")
@ExcelProperty("原件合同编号") private Integer templateFlag;
private String originalContractNo; @ExcelProperty("原件合同编号")
@ExcelProperty("是否电子章") private String originalContractNo;
private Integer electronicSealFlag; @ExcelProperty("是否电子章")
@ExcelProperty("当前节点") private Integer electronicSealFlag;
private String currentNode; @ExcelProperty("当前节点")
@ExcelProperty("当前处理人") private String currentNode;
private String currentProcessor; @ExcelProperty("当前处理人")
@ExcelProperty("备注") private String currentProcessor;
private String remark; @ExcelProperty("备注")
@ExcelProperty("创建人") private String remark;
private String createUserName; @ExcelProperty("创建人")
@ExcelProperty("更新人") private String createUserName;
private String updateUserName; @ExcelProperty("更新人")
@ExcelProperty("创建时间") private String updateUserName;
private LocalDateTime createTime; @ExcelProperty("创建时间")
@ExcelProperty("更新时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date createTime;
@ExcelProperty("更新时间")
} @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -7,12 +7,10 @@ package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate;
/** /**
* 设备台账 Excel * 设备台账 Excel
@@ -29,7 +27,7 @@ public class EquipmentLedgerExcel implements Serializable {
@ExcelProperty("设备品牌") private String equipmentBrand; @ExcelProperty("设备品牌") private String equipmentBrand;
@ExcelProperty("设备类型") private String equipmentType; @ExcelProperty("设备类型") private String equipmentType;
@ExcelProperty("规格型号") private String specificationModel; @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; @ExcelProperty("备注") private String remark;
@ExcelIgnore private String errorMessage; @ExcelIgnore private String errorMessage;
} }
@@ -11,6 +11,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* ETC记录导出 Excel * ETC记录导出 Excel
@@ -50,14 +51,14 @@ public class EtcRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 保险记录 Excel * 保险记录 Excel
@@ -68,11 +67,11 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("*保单号") @ExcelProperty("*保单号")
private String policyNo; private String policyNo;
@ExcelProperty("*开始日期") @ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate startDate; private String startDate;
@ExcelProperty("*结束日期") @ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate endDate; private String endDate;
@ExcelProperty("保额") @ExcelProperty("保额")
private BigDecimal insuredAmount; private BigDecimal insuredAmount;
@@ -83,8 +82,8 @@ public class InsuranceRecordExcel implements Serializable {
@ExcelProperty("发票号") @ExcelProperty("发票号")
private String invoiceNo; private String invoiceNo;
@ExcelProperty("开票日期") @ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate invoiceDate; private String invoiceDate;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -36,7 +36,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 保险记录导出 Excel * 保险记录导出 Excel
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -33,22 +33,20 @@ import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
import cn.idev.excel.util.DateUtils; import cn.idev.excel.util.DateUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/** /**
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。 * 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
* <p>
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
* 文案带原值,与「日期格式无法识别」口径一致。
* *
* @author Chill * @author Chill
*/ */
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> { public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; 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 @Override
public Class<?> supportJavaTypeKey() { public Class<?> supportJavaTypeKey() {
@@ -71,16 +69,14 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
return null; return null;
} }
String normalizedValue = value.trim(); LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
try { if (parsed != null) {
return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER); return parsed;
} catch (DateTimeParseException ignored) {
try {
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
} catch (DateTimeParseException ignoredMinute) {
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
}
} }
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
value.trim() + " 日期格式无法识别");
} }
@Override @Override
@@ -11,7 +11,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.io.Serial; import java.io.Serial;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 车辆保养记录导出 Excel * 车辆保养记录导出 Excel
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
@ExcelProperty(value = "创建时间", index = 12) @ExcelProperty(value = "创建时间", index = 12)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty(value = "更新人", index = 13) @ExcelProperty(value = "更新人", index = 13)
private String updateUserName; private String updateUserName;
@ExcelProperty(value = "更新时间", index = 14) @ExcelProperty(value = "更新时间", index = 14)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
} }
@@ -38,6 +38,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 车辆维修记录 Excel * 车辆维修记录 Excel
@@ -95,16 +96,16 @@ public class MaintenanceRecordExcel implements Serializable {
@NumberFormat("0.00") @NumberFormat("0.00")
private BigDecimal mileage; private BigDecimal mileage;
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class) @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class) @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("里程单位") @ExcelProperty("里程单位")
private String mileageUnit; private String mileageUnit;
@@ -25,7 +25,6 @@
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -42,7 +41,4 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ExcelProperty(value = "导出失败原因", index = 17)
private String errorMessage;
} }
@@ -36,7 +36,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 里程记录导出 Excel * 里程记录导出 Excel
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -11,6 +11,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 油电记录导出 Excel * 油电记录导出 Excel
@@ -61,14 +62,14 @@ public class OilElectricRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -16,7 +16,6 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
/** /**
* 其他费用记录 Excel * 其他费用记录 Excel
@@ -40,8 +39,8 @@ public class OtherExpenseRecordExcel implements Serializable {
@ExcelProperty("*车牌号/船号") @ExcelProperty("*车牌号/船号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty("*费用日期") @ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate expenseDate; private String expenseDate;
@ExcelProperty("*费用类型") @ExcelProperty("*费用类型")
private String expenseType; private String expenseType;
@@ -11,7 +11,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 其他费用记录导出 Excel * 其他费用记录导出 Excel
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -1,76 +1,79 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import lombok.Data; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable; import java.io.Serial;
import java.util.Date; import java.io.Serializable;
import java.util.Date;
/**
* 过程配置导出 Excel /**
* * 过程配置导出 Excel
* @author Chill *
*/ * @author Chill
@Data */
@ColumnWidth(22) @Data
@HeadRowHeight(20) @ColumnWidth(22)
@ContentRowHeight(18) @HeadRowHeight(20)
public class ProcessConfigExportExcel implements Serializable { @ContentRowHeight(18)
public class ProcessConfigExportExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L; @Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("配置编号")
private String configCode; @ExcelProperty("配置编号")
@ExcelProperty("配置名称") private String configCode;
private String configName; @ExcelProperty("配置名称")
@ExcelProperty("项目ID集合") private String configName;
private String projectIds; @ExcelProperty("项目ID集合")
@ExcelProperty("项目") private String projectIds;
private String projectNames; @ExcelProperty("项目")
@ExcelProperty("包含过程节点") private String projectNames;
private String includedNodes; @ExcelProperty("包含过程节点")
@ExcelProperty("默认后台完成运输天数") private String includedNodes;
private Integer defaultFinishDays; @ExcelProperty("默认后台完成运输天数")
@ExcelProperty("状态") private Integer defaultFinishDays;
private Integer status; @ExcelProperty("状态")
@ExcelProperty("所属组织") private Integer status;
private String deptName; @ExcelProperty("所属组织")
@ExcelProperty("备注") private String deptName;
private String remark; @ExcelProperty("备注")
@ExcelProperty("创建人") private String remark;
private String createUserName; @ExcelProperty("创建人")
@ExcelProperty("更新人") private String createUserName;
private String updateUserName; @ExcelProperty("更新人")
@ExcelProperty("更新时间") private String updateUserName;
private Date updateTime; @ExcelProperty("更新时间")
@ExcelProperty("创建时间") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date updateTime;
@ExcelProperty("创建时间")
} @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -23,6 +23,7 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; 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.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -33,6 +34,7 @@ import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 项目立项 Excel * 项目立项 Excel
@@ -123,8 +125,10 @@ public class ProjectApplyExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
private LocalDateTime createTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
private LocalDateTime updateTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
} }
@@ -23,6 +23,7 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; 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.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -30,7 +31,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 发货模板 Excel * 发货模板 Excel
@@ -59,8 +60,10 @@ public class ShippingTemplateExcel implements Serializable {
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
private LocalDateTime updateTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
private LocalDateTime createTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
} }
@@ -23,6 +23,7 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; 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.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -32,7 +33,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 临时额度申请 Excel * 临时额度申请 Excel
@@ -81,8 +82,10 @@ public class TemporaryCreditLimitExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
private LocalDateTime createTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
private LocalDateTime updateTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
} }
@@ -38,6 +38,10 @@ import java.time.format.DateTimeFormatter;
/** /**
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。 * 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
* <p>
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
* 口径见根工作区 docs/import-spec.md)。
* *
* @author Chill * @author Chill
*/ */
@@ -37,7 +37,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 换胎记录导出 Excel * 换胎记录导出 Excel
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -28,7 +28,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 变更记录导出 Excel * 变更记录导出 Excel
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@@ -23,6 +23,7 @@
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; 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.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
@@ -31,7 +32,7 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.util.Date;
/** /**
* 运输计划 Excel * 运输计划 Excel
@@ -92,8 +93,10 @@ public class TransportPlanExcel implements Serializable {
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
private LocalDateTime createTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
private LocalDateTime updateTime; @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime;
} }
@@ -37,6 +37,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* 违章记录 Excel * 违章记录 Excel
@@ -95,14 +96,14 @@ public class ViolationRecordExcel implements Serializable {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private Date createTime;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private Date updateTime;
@ExcelIgnore @ExcelIgnore
private String errorMessage; private String errorMessage;
@@ -60,8 +60,11 @@ public class ViolationRecordImportExcel implements Serializable {
@ExcelProperty("*驾驶人") @ExcelProperty("*驾驶人")
private String driverName; private String driverName;
@ExcelProperty("*类型/事项") @ExcelProperty("*类型")
private String violationTypeOrItem; private String violationType;
@ExcelProperty("*事项")
private String violationItem;
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class) @ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@@ -1,163 +1,163 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.excel; package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.format.DateTimeFormat; import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth; import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight; import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight; import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Date; import java.util.Date;
/** /**
* 运单管理 Excel * 运单管理 Excel
* *
* @author Chill * @author Chill
*/ */
@Data @Data
@ColumnWidth(22) @ColumnWidth(22)
@HeadRowHeight(20) @HeadRowHeight(20)
@ContentRowHeight(18) @ContentRowHeight(18)
public class WaybillExcel implements Serializable { public class WaybillExcel implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ExcelProperty("运单号") @ExcelProperty("运单号")
private String waybillNo; private String waybillNo;
@ExcelProperty("项目") @ExcelProperty("项目")
private String projectName; private String projectName;
@ExcelProperty("客户合同") @ExcelProperty("客户合同")
private String contractName; private String contractName;
@ExcelProperty("客户名称") @ExcelProperty("客户名称")
private String customerName; private String customerName;
@ExcelProperty("运输类型") @ExcelProperty("运输类型")
private String transportType; private String transportType;
@ExcelProperty("货物名称") @ExcelProperty("货物名称")
private String cargoName; private String cargoName;
@ExcelProperty("货物类型") @ExcelProperty("货物类型")
private String cargoType; private String cargoType;
@ExcelProperty("规格") @ExcelProperty("规格")
private String specification; private String specification;
@ExcelProperty("型号") @ExcelProperty("型号")
private String model; private String model;
@ExcelProperty("数量") @ExcelProperty("数量")
private BigDecimal quantity; private BigDecimal quantity;
@ExcelProperty("数量单位") @ExcelProperty("数量单位")
private String quantityUnit; private String quantityUnit;
@ExcelProperty("发货地") @ExcelProperty("发货地")
private String departureName; private String departureName;
@ExcelProperty("发货地址") @ExcelProperty("发货地址")
private String departureAddress; private String departureAddress;
@ExcelProperty("发货联系人") @ExcelProperty("发货联系人")
private String departureContact; private String departureContact;
@ExcelProperty("发货联系方式") @ExcelProperty("发货联系方式")
private String departurePhone; private String departurePhone;
@ExcelProperty("收货地") @ExcelProperty("收货地")
private String arrivalName; private String arrivalName;
@ExcelProperty("收货地址") @ExcelProperty("收货地址")
private String arrivalAddress; private String arrivalAddress;
@ExcelProperty("收货联系人") @ExcelProperty("收货联系人")
private String arrivalContact; private String arrivalContact;
@ExcelProperty("收货联系方式") @ExcelProperty("收货联系方式")
private String arrivalPhone; private String arrivalPhone;
@ExcelProperty("任务录入模式") @ExcelProperty("任务录入模式")
private String taskEntryMode; private String taskEntryMode;
@ExcelProperty("承运类型") @ExcelProperty("承运类型")
private String carrierType; private String carrierType;
@ExcelProperty("承运商名称") @ExcelProperty("承运商名称")
private String carrierName; private String carrierName;
@ExcelProperty("司机姓名") @ExcelProperty("司机姓名")
private String driverName; private String driverName;
@ExcelProperty("司机手机号") @ExcelProperty("司机手机号")
private String driverPhone; private String driverPhone;
@ExcelProperty("车/船/航班/班列号") @ExcelProperty("车/船/航班/班列号")
private String vehicleNo; private String vehicleNo;
@ExcelProperty("挂车车牌号") @ExcelProperty("挂车车牌号")
private String trailerVehicleNo; private String trailerVehicleNo;
@ExcelProperty("押运人") @ExcelProperty("押运人")
private String escortName; private String escortName;
@ExcelProperty("押运人手机号") @ExcelProperty("押运人手机号")
private String escortPhone; private String escortPhone;
@ExcelProperty("里程(km)") @ExcelProperty("里程(km)")
private BigDecimal mileage; private BigDecimal mileage;
@ExcelProperty("预计发货日期") @ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate estimatedStartTime; private String estimatedStartTime;
@ExcelProperty("预计完成日期") @ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate estimatedEndTime; private String estimatedEndTime;
@ExcelProperty("单价") @ExcelProperty("单价")
private BigDecimal unitPrice; private BigDecimal unitPrice;
@ExcelProperty("计价单位") @ExcelProperty("计价单位")
private String priceUnit; private String priceUnit;
@ExcelProperty("其他费用合计") @ExcelProperty("其他费用合计")
private BigDecimal otherFeeTotal; private BigDecimal otherFeeTotal;
@ExcelProperty("任务备注") @ExcelProperty("任务备注")
private String taskRemark; private String taskRemark;
@ExcelProperty("原始单号") @ExcelProperty("原始单号")
private String originalNo; private String originalNo;
@ExcelProperty("业务状态") @ExcelProperty("业务状态")
private String businessStatus; private String businessStatus;
@ExcelProperty("数据来源") @ExcelProperty("数据来源")
private String dataSource; private String dataSource;
@ExcelProperty("开始日期") @ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate startDate; private String startDate;
@ExcelProperty("结束日期") @ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
private LocalDate endDate; private String endDate;
@ExcelProperty("计划名称") @ExcelProperty("计划名称")
private String planName; private String planName;
@ExcelProperty("多联总单") @ExcelProperty("多联总单")
private String masterNo; private String masterNo;
@ExcelProperty("配载单号") @ExcelProperty("配载单号")
private String loadingNo; private String loadingNo;
@ExcelProperty("运单批次号") @ExcelProperty("运单批次号")
private String batchNo; private String batchNo;
@ExcelProperty("关联单号") @ExcelProperty("关联单号")
private String relationNo; private String relationNo;
@ExcelProperty("当前过程节点") @ExcelProperty("当前过程节点")
private String currentProcessNode; private String currentProcessNode;
@ExcelProperty("所属组织") @ExcelProperty("所属组织")
private String deptName; private String deptName;
@ExcelProperty("备注") @ExcelProperty("备注")
private String remark; private String remark;
@ExcelProperty("创建人") @ExcelProperty("创建人")
private String createUserName; private String createUserName;
@ExcelProperty("更新人") @ExcelProperty("更新人")
private String updateUserName; private String updateUserName;
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date createTime; private Date createTime;
@ExcelProperty("更新时间") @ExcelProperty("更新时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date updateTime; private Date updateTime;
@ExcelIgnore @ExcelIgnore
private String errorMessage; private String errorMessage;
} }
@@ -94,6 +94,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
AccidentRecordExcel excel = data.get(index); AccidentRecordExcel excel = data.get(index);
try { try {
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class)); AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
prepare(accidentRecord); prepare(accidentRecord);
List<String> validationErrors = validateImportAccidentRecord(accidentRecord); List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
@@ -113,6 +113,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
AnnualInspectionRecordExcel excel = data.get(index); AnnualInspectionRecordExcel excel = data.get(index);
try { try {
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class)); 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); prepare(annualInspectionRecord);
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord); List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
if (Func.isNotEmpty(validationErrors)) { if (Func.isNotEmpty(validationErrors)) {
@@ -91,6 +91,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
EquipmentLedgerExcel excel = data.get(index); EquipmentLedgerExcel excel = data.get(index);
try { try {
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class)); EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
equipmentLedger.setFactoryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getFactoryDate(), "出厂日期"));
prepare(equipmentLedger); prepare(equipmentLedger);
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) { if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes)); equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
@@ -119,6 +119,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
InsuranceRecordExcel excel = data.get(index); InsuranceRecordExcel excel = data.get(index);
try { try {
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class)); 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); prepare(insuranceRecord);
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord); List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) { if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
@@ -174,6 +174,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo); throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
} }
Waybill first = waybills.get(0); Waybill first = waybills.get(0);
Waybill last = waybills.get(waybills.size() - 1);
LoadingManage loadingManage = new LoadingManage(); LoadingManage loadingManage = new LoadingManage();
loadingManage.setLoadingNo(normalizedLoadingNo); loadingManage.setLoadingNo(normalizedLoadingNo);
loadingManage.setLoadingSubNos(waybills.stream() loadingManage.setLoadingSubNos(waybills.stream()
@@ -197,7 +198,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setCarrierName(first.getCarrierName()); loadingManage.setCarrierName(first.getCarrierName());
loadingManage.setCarrierContractId(first.getCarrierContractId()); loadingManage.setCarrierContractId(first.getCarrierContractId());
loadingManage.setDepartureAddress(first.getDepartureAddress()); loadingManage.setDepartureAddress(first.getDepartureAddress());
loadingManage.setArrivalAddress(first.getArrivalAddress()); loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills));
loadingManage.setArrivalAddress(last.getArrivalAddress());
loadingManage.setOriginalNo(first.getOriginalNo()); loadingManage.setOriginalNo(first.getOriginalNo());
loadingManage.setDataSource("批量导入"); loadingManage.setDataSource("批量导入");
loadingManage.setStartDate(first.getStartDate()); loadingManage.setStartDate(first.getStartDate());
@@ -222,6 +224,39 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getLoadingNo, normalizedLoadingNo)); .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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public BusinessRemoveResultVO removeLoadingManage(String ids) { public BusinessRemoveResultVO removeLoadingManage(String ids) {
@@ -75,6 +75,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
OtherExpenseRecordExcel excel = data.get(index); OtherExpenseRecordExcel excel = data.get(index);
try { try {
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class)); OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
otherExpenseRecord.setExpenseDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpenseDate(), "费用日期"));
otherExpenseRecord.setDataSource("批量导入"); otherExpenseRecord.setDataSource("批量导入");
prepare(otherExpenseRecord); prepare(otherExpenseRecord);
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord); List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
@@ -43,8 +43,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; 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 REMARK_MAX_LENGTH = 500;
private static final int ATTACHMENTS_MAX_LENGTH = 1000; private static final int ATTACHMENTS_MAX_LENGTH = 1000;
private static final int MONEY_SCALE = 2; private static final int MONEY_SCALE = 2;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@Override @Override
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) { public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
@@ -144,9 +141,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
return null; return null;
} }
try { try {
return LocalDate.parse(normalizedValue, DATE_FORMATTER); return org.springblade.common.excel.LenientDateParser.parseDate(normalizedValue, "换胎时间");
} catch (DateTimeParseException exception) { } catch (Exception exception) {
validationErrors.add("换胎时间格式不正确,请使用yyyy-MM-dd格式并填写有效日期"); validationErrors.add(exception.getMessage());
return null; return null;
} }
} }
@@ -416,9 +416,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
// 2. 校验运输类型枚举值 // 2. 校验运输类型枚举值
String transportType = trimToEmpty(excel.getTransportType()); String transportType = trimToEmpty(excel.getTransportType());
if (Func.isNotEmpty(transportType)) { if (Func.isNotEmpty(transportType)) {
List<String> validTransportTypes = List.of("公路整车", "公路配载/零担", "铁路运输", "水路运输", "跨境海运", "航空运输"); List<String> validTransportTypes = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
if (!validTransportTypes.contains(transportType)) { if (!validTransportTypes.contains(transportType)) {
errors.add("运输类型需系统枚举值(公路整车、公路配载/零担、铁路运输、水路运输、跨境海运、航空运输)"); errors.add("运输类型需系统枚举值(公路运输、铁路运输、水路运输、航空运输)");
} }
} }
@@ -510,11 +510,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
if (Func.isEmpty(value)) { if (Func.isEmpty(value)) {
return null; return null;
} }
try { // 宽容解析:接受 2026-8-2、2026/8/2 等写法(口径见根工作区 docs/import-spec.md)。
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE); LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(value);
} catch (Exception exception) { if (date == null) {
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD"); throw new ServiceException(fieldName + " 日期格式无法识别:" + value.trim());
} }
return date;
} }
private void validateImportLength(String value, int maxLength, String fieldName) { private void validateImportLength(String value, int maxLength, String fieldName) {
@@ -60,7 +60,6 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -1346,14 +1345,12 @@ public class TransportReconciliationServiceImpl
private LocalDateTime parseTimeNullable(String value, String field) { private LocalDateTime parseTimeNullable(String value, String field) {
if (Func.isEmpty(value)) return null; 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", // 宽容解析:统一委托公共解析器,接受 2026-8-2 12:3 等写法(口径见根工作区 docs/import-spec.md)。
"yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/M/d HH:mm:ss", "yyyy/M/d HH:mm")) { LocalDateTime result = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { } 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")) { return result;
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");
} }
private String matchKey(TransportReconciliationInternal row) { private String matchKey(TransportReconciliationInternal row) {
@@ -1,310 +1,326 @@
/** /**
* BladeX Commercial License Agreement * BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p> * <p>
* Use of this software is governed by the Commercial License Agreement * Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX. * obtained after purchasing a license from BladeX.
* <p> * <p>
* 1. This software is for development use only under a valid license * 1. This software is for development use only under a valid license
* from BladeX. * from BladeX.
* <p> * <p>
* 2. Redistribution of this software's source code to any third party * 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited. * without a commercial license is strictly prohibited.
* <p> * <p>
* 3. Licensees may copyright their own code but cannot use segments * 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software * from this software for such purposes. Copyright of this software
* remains with BladeX. * remains with BladeX.
* <p> * <p>
* Using this software signifies agreement to this License, and the software * Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes. * must not be used for illegal purposes.
* <p> * <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development. * not liable for any claims arising from secondary or illegal development.
* <p> * <p>
* Author: Chill Zhuang (bladejava@qq.com) * Author: Chill Zhuang (bladejava@qq.com)
*/ */
package org.springblade.transport.service.impl; package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.ViolationRecordExcel; import org.springblade.transport.excel.ViolationRecordExcel;
import org.springblade.transport.excel.ViolationRecordImportExcel; import org.springblade.transport.excel.ViolationRecordImportExcel;
import org.springblade.transport.mapper.ViolationRecordMapper; import org.springblade.transport.mapper.ViolationRecordMapper;
import org.springblade.transport.pojo.entity.ViolationRecord; import org.springblade.transport.pojo.entity.ViolationRecord;
import org.springblade.transport.pojo.vo.ViolationRecordVO; import org.springblade.transport.pojo.vo.ViolationRecordVO;
import org.springblade.transport.service.IViolationRecordService; import org.springblade.transport.service.IViolationRecordService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
* 违章记录 服务实现类 * 违章记录 服务实现类
* *
* @author Chill * @author Chill
*/ */
@Service @Service
public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordMapper, ViolationRecord> implements IViolationRecordService { public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordMapper, ViolationRecord> implements IViolationRecordService {
private static final int VEHICLE_NO_MAX_LENGTH = 30; private static final int VEHICLE_NO_MAX_LENGTH = 30;
private static final int DRIVER_NAME_MAX_LENGTH = 20; private static final int DRIVER_NAME_MAX_LENGTH = 20;
private static final int TYPE_MAX_LENGTH = 50; private static final int TYPE_MAX_LENGTH = 50;
private static final int ITEM_MAX_LENGTH = 100; private static final int ITEM_MAX_LENGTH = 100;
private static final int LOCATION_MAX_LENGTH = 100; private static final int LOCATION_MAX_LENGTH = 100;
private static final int PENALTY_UNIT_MAX_LENGTH = 50; private static final int PENALTY_UNIT_MAX_LENGTH = 50;
private static final int DESCRIPTION_MAX_LENGTH = 500; private static final int DESCRIPTION_MAX_LENGTH = 500;
private static final int RESULT_MAX_LENGTH = 500; private static final int RESULT_MAX_LENGTH = 500;
private static final int ATTACHMENTS_MAX_LENGTH = 16000; private static final int ATTACHMENTS_MAX_LENGTH = 16000;
private static final int MAX_DEDUCT_POINTS = 15; private static final int MAX_DEDUCT_POINTS = 15;
private static final String PROCESSED = "已处理"; private static final String PROCESSED = "已处理";
private static final String UNPROCESSED = "未处理"; private static final String UNPROCESSED = "未处理";
private static final String CAR = "车辆";
@Override private static final String SHIP = "船舶";
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
List<ViolationRecordVO> records = baseMapper.selectViolationRecordPage(page, violationRecord); @Override
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser()))); public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
return page.setRecords(records); List<ViolationRecordVO> records = baseMapper.selectViolationRecordPage(page, violationRecord);
} records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
return page.setRecords(records);
@Override }
@Transactional(rollbackFor = Exception.class)
public boolean submit(ViolationRecord violationRecord) { @Override
prepare(violationRecord); @Transactional(rollbackFor = Exception.class)
validate(violationRecord); public boolean submit(ViolationRecord violationRecord) {
validateVehicleTypeImmutable(violationRecord); prepare(violationRecord);
return saveOrUpdate(violationRecord); validate(violationRecord);
} validateVehicleTypeImmutable(violationRecord);
clearIrrelevantField(violationRecord);
@Override return saveOrUpdate(violationRecord);
@Transactional(rollbackFor = Exception.class) }
public List<ViolationRecordImportExcel> importViolationRecord(List<ViolationRecordImportExcel> data) {
if (Func.isEmpty(data)) { @Override
throw new ServiceException("导入数据不能为空"); @Transactional(rollbackFor = Exception.class)
} public List<ViolationRecordImportExcel> importViolationRecord(List<ViolationRecordImportExcel> data) {
List<ViolationRecordImportExcel> errorList = new ArrayList<>(); if (Func.isEmpty(data)) {
List<ViolationRecord> violationRecordList = new ArrayList<>(); throw new ServiceException("导入数据不能为空");
for (int index = 0; index < data.size(); index++) { }
ViolationRecordImportExcel excel = data.get(index); List<ViolationRecordImportExcel> errorList = new ArrayList<>();
try { List<ViolationRecord> violationRecordList = new ArrayList<>();
ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class)); for (int index = 0; index < data.size(); index++) {
if ("船舶".equals(trimToEmpty(excel.getVehicleType()))) { ViolationRecordImportExcel excel = data.get(index);
violationRecord.setViolationItem(excel.getViolationTypeOrItem()); try {
} else { ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class));
violationRecord.setViolationType(excel.getViolationTypeOrItem()); prepare(violationRecord);
} List<String> validationErrors = validateImportViolationRecord(violationRecord);
prepare(violationRecord); if (Func.isNotEmpty(validationErrors)) {
List<String> validationErrors = validateImportViolationRecord(violationRecord); excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
if (Func.isNotEmpty(validationErrors)) { errorList.add(excel);
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors)); continue;
errorList.add(excel); }
continue; validateVehicleTypeImmutable(violationRecord);
} clearIrrelevantField(violationRecord);
validateVehicleTypeImmutable(violationRecord); violationRecordList.add(violationRecord);
violationRecordList.add(violationRecord); } catch (Exception exception) {
} catch (Exception exception) { String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败"; excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(message)));
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(message))); errorList.add(excel);
errorList.add(excel); }
} }
} if (Func.isNotEmpty(errorList)) {
if (Func.isNotEmpty(errorList)) { org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return errorList;
return errorList; }
} for (ViolationRecord violationRecord : violationRecordList) {
for (ViolationRecord violationRecord : violationRecordList) { if (!save(violationRecord)) {
if (!save(violationRecord)) { throw new ServiceException("违章记录保存失败");
throw new ServiceException("违章记录保存失败"); }
} }
} return errorList;
return errorList; }
}
private List<String> validateImportViolationRecord(ViolationRecord violationRecord) {
private List<String> validateImportViolationRecord(ViolationRecord violationRecord) { List<String> validationErrors = new ArrayList<>();
List<String> validationErrors = new ArrayList<>(); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleType()), "车船类型不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleType()), "车船类型不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getVehicleType()) && !"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType()), "车船类型不正确");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getVehicleType()) && !"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType()), "船类型不正确"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleNo()), "牌号/船号不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleNo()), "车牌号/船不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船不能为空");
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.getViolationType()), "类型不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空");
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, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem()), "车辆不适用于事项,该列应留");
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.getViolationTime()), "时间不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空"); 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.getProcessStatus()), "状态不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getProcessStatus()) && !PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus()), "状态值不正确"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessStatus()), "状态不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessDescription()), "过程描述不能为空"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getProcessStatus()) && !PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus()), "状态值不正确");
addImportNonNegativeError(validationErrors, violationRecord.getFineAmount(), "被罚金额"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessDescription()), "过程描述不能为空");
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getDeductPoints()) && (violationRecord.getDeductPoints() < 0 || violationRecord.getDeductPoints() > MAX_DEDUCT_POINTS), "被扣分数范围为0-15分"); addImportNonNegativeError(validationErrors, violationRecord.getFineAmount(), "被罚金额");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getDeductPoints()) && (violationRecord.getDeductPoints() < 0 || violationRecord.getDeductPoints() > MAX_DEDUCT_POINTS), "被扣分数范围为0-15分");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船不能超过20字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船不能超过30字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字"); org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
return validationErrors; org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
} return validationErrors;
}
private void addImportNonNegativeError(List<String> validationErrors, BigDecimal value, String fieldName) {
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0"); private void addImportNonNegativeError(List<String> validationErrors, BigDecimal value, String fieldName) {
} org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0");
}
@Override
public List<ViolationRecordExcel> exportViolationRecord(Wrapper<ViolationRecord> queryWrapper) { @Override
return list(queryWrapper).stream().map(violationRecord -> { public List<ViolationRecordExcel> exportViolationRecord(Wrapper<ViolationRecord> queryWrapper) {
ViolationRecordExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(violationRecord, ViolationRecordExcel.class)); return list(queryWrapper).stream().map(violationRecord -> {
excel.setFineAmount(nonNegative(violationRecord.getFineAmount())); ViolationRecordExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(violationRecord, ViolationRecordExcel.class));
excel.setDeductPoints(validDeductPoints(violationRecord.getDeductPoints())); excel.setFineAmount(nonNegative(violationRecord.getFineAmount()));
excel.setUpdateUserName(UserCache.getUserRealName(violationRecord.getUpdateUser())); excel.setDeductPoints(validDeductPoints(violationRecord.getDeductPoints()));
return excel; excel.setUpdateUserName(UserCache.getUserRealName(violationRecord.getUpdateUser()));
}).toList(); return excel;
} }).toList();
}
private void prepare(ViolationRecord violationRecord) {
violationRecord.setVehicleType(normalizeVehicleType(violationRecord.getVehicleType())); private void prepare(ViolationRecord violationRecord) {
violationRecord.setVehicleNo(trimToEmpty(violationRecord.getVehicleNo()).toUpperCase()); violationRecord.setVehicleType(normalizeVehicleType(violationRecord.getVehicleType()));
violationRecord.setDriverName(trimToEmpty(violationRecord.getDriverName())); violationRecord.setVehicleNo(trimToEmpty(violationRecord.getVehicleNo()).toUpperCase());
violationRecord.setViolationType(trimToNull(violationRecord.getViolationType())); violationRecord.setDriverName(trimToEmpty(violationRecord.getDriverName()));
violationRecord.setViolationItem(trimToNull(violationRecord.getViolationItem())); violationRecord.setViolationType(trimToNull(violationRecord.getViolationType()));
violationRecord.setLocation(trimToEmpty(violationRecord.getLocation())); violationRecord.setViolationItem(trimToNull(violationRecord.getViolationItem()));
violationRecord.setPenaltyUnit(trimToNull(violationRecord.getPenaltyUnit())); violationRecord.setLocation(trimToEmpty(violationRecord.getLocation()));
violationRecord.setProcessStatus(normalizeProcessStatus(violationRecord.getProcessStatus())); violationRecord.setPenaltyUnit(trimToNull(violationRecord.getPenaltyUnit()));
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription())); violationRecord.setProcessStatus(normalizeProcessStatus(violationRecord.getProcessStatus()));
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult())); violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments())); violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
if ("车辆".equals(violationRecord.getVehicleType())) { violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
violationRecord.setViolationItem(null); if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
} else { violationRecord.setProcessResult(null);
violationRecord.setViolationType(null); }
} }
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
violationRecord.setProcessResult(null); /**
} * 清空与车船类型不匹配的对侧字段
} * <p>
* 必须在 validate 之后执行:校验需要看到用户填了什么,
private void validate(ViolationRecord violationRecord) { * 若提前清空,误填的内容会被静默丢弃,用户无从察觉。
if (Func.isEmpty(violationRecord.getVehicleType())) { */
throw new ServiceException("车船类型不能为空"); private void clearIrrelevantField(ViolationRecord violationRecord) {
} if (CAR.equals(violationRecord.getVehicleType())) {
if (!"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType())) { violationRecord.setViolationItem(null);
throw new ServiceException("车船类型不正确"); } else {
} violationRecord.setViolationType(null);
if (Func.isEmpty(violationRecord.getVehicleNo())) { }
throw new ServiceException("车牌号/船号不能为空"); }
}
if (Func.isEmpty(violationRecord.getDriverName())) { private void validate(ViolationRecord violationRecord) {
throw new ServiceException("驾驶人/船长不能为空"); if (Func.isEmpty(violationRecord.getVehicleType())) {
} throw new ServiceException("车船类型不能为空");
if ("车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) { }
throw new ServiceException("类型不能为空"); if (!CAR.equals(violationRecord.getVehicleType()) && !SHIP.equals(violationRecord.getVehicleType())) {
} throw new ServiceException("车船类型不正确");
if ("船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) { }
throw new ServiceException("事项不能为空"); if (Func.isEmpty(violationRecord.getVehicleNo())) {
} throw new ServiceException("车牌号/船号不能为空");
if (Func.isEmpty(violationRecord.getViolationTime())) { }
throw new ServiceException("时间不能为空"); if (Func.isEmpty(violationRecord.getDriverName())) {
} throw new ServiceException("驾驶人/船长不能为空");
if (violationRecord.getViolationTime().isAfter(LocalDateTime.now())) { }
throw new ServiceException("时间不能超过当前时间"); if (CAR.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
} throw new ServiceException("类型不能为空");
if (Func.isEmpty(violationRecord.getLocation())) { }
throw new ServiceException("地址不能为空"); if (SHIP.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
} throw new ServiceException("事项不能为空");
if (Func.isEmpty(violationRecord.getProcessStatus())) { }
throw new ServiceException("状态不能为空"); if (SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType())) {
} throw new ServiceException("船舶不适用于类型,该列应留空");
if (!PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus())) { }
throw new ServiceException("状态值不正确"); if (CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem())) {
} throw new ServiceException("车辆不适用于事项,该列应留空");
if (Func.isEmpty(violationRecord.getProcessDescription())) { }
throw new ServiceException("过程描述不能为空"); if (Func.isEmpty(violationRecord.getViolationTime())) {
} throw new ServiceException("时间不能为空");
validateNonNegative(violationRecord.getFineAmount(), "被罚金额不能小于0"); }
validateDeductPoints(violationRecord.getDeductPoints()); if (violationRecord.getViolationTime().isAfter(LocalDateTime.now())) {
validateLength(violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字"); throw new ServiceException("时间不能超过当前时间");
validateLength(violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字"); }
validateLength(violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字"); if (Func.isEmpty(violationRecord.getLocation())) {
validateLength(violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字"); throw new ServiceException("地址不能为空");
validateLength(violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字"); }
validateLength(violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字"); if (Func.isEmpty(violationRecord.getProcessStatus())) {
validateLength(violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字"); throw new ServiceException("状态不能为空");
validateLength(violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字"); }
validateLength(violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字"); if (!PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus())) {
} throw new ServiceException("状态值不正确");
}
private void validateVehicleTypeImmutable(ViolationRecord violationRecord) { if (Func.isEmpty(violationRecord.getProcessDescription())) {
if (Func.isEmpty(violationRecord.getId())) { throw new ServiceException("过程描述不能为空");
return; }
} validateNonNegative(violationRecord.getFineAmount(), "被罚金额不能小于0");
ViolationRecord oldRecord = getById(violationRecord.getId()); validateDeductPoints(violationRecord.getDeductPoints());
if (oldRecord != null && Func.isNotEmpty(oldRecord.getVehicleType()) && !oldRecord.getVehicleType().equals(violationRecord.getVehicleType())) { validateLength(violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字");
throw new ServiceException("车船类型保存后不可修改"); validateLength(violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
} validateLength(violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
} validateLength(violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
validateLength(violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
private void validateLength(String value, int maxLength, String message) { validateLength(violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
if (Func.isNotEmpty(value) && value.length() > maxLength) { validateLength(violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
throw new ServiceException(message); validateLength(violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
} validateLength(violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
} }
private void validateNonNegative(BigDecimal value, String message) { private void validateVehicleTypeImmutable(ViolationRecord violationRecord) {
if (Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0) { if (Func.isEmpty(violationRecord.getId())) {
throw new ServiceException(message); return;
} }
} ViolationRecord oldRecord = getById(violationRecord.getId());
if (oldRecord != null && Func.isNotEmpty(oldRecord.getVehicleType()) && !oldRecord.getVehicleType().equals(violationRecord.getVehicleType())) {
private void validateDeductPoints(Integer value) { throw new ServiceException("车船类型保存后不可修改");
if (Func.isNotEmpty(value) && (value < 0 || value > MAX_DEDUCT_POINTS)) { }
throw new ServiceException("被扣分数范围为0-15分"); }
}
} private void validateLength(String value, int maxLength, String message) {
if (Func.isNotEmpty(value) && value.length() > maxLength) {
private BigDecimal nonNegative(BigDecimal value) { throw new ServiceException(message);
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) { }
return value; }
}
return BigDecimal.ZERO; private void validateNonNegative(BigDecimal value, String message) {
} if (Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException(message);
private Integer validDeductPoints(Integer value) { }
if (Func.isEmpty(value)) { }
return value;
} private void validateDeductPoints(Integer value) {
if (value < 0) { if (Func.isNotEmpty(value) && (value < 0 || value > MAX_DEDUCT_POINTS)) {
return 0; throw new ServiceException("被扣分数范围为0-15分");
} }
return Math.min(value, MAX_DEDUCT_POINTS); }
}
private BigDecimal nonNegative(BigDecimal value) {
private String normalizeVehicleType(String vehicleType) { if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
String value = trimToEmpty(vehicleType); return value;
return value.isEmpty() ? "车辆" : value; }
} return BigDecimal.ZERO;
}
private String normalizeProcessStatus(String processStatus) {
String value = trimToEmpty(processStatus); private Integer validDeductPoints(Integer value) {
return value.isEmpty() ? UNPROCESSED : value; if (Func.isEmpty(value)) {
} return value;
}
private String trimToEmpty(String value) { if (value < 0) {
return value == null ? "" : value.trim(); return 0;
} }
return Math.min(value, MAX_DEDUCT_POINTS);
private String trimToNull(String value) { }
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue; private String normalizeVehicleType(String vehicleType) {
} String value = trimToEmpty(vehicleType);
return value.isEmpty() ? CAR : value;
} }
private String normalizeProcessStatus(String processStatus) {
String value = trimToEmpty(processStatus);
return value.isEmpty() ? UNPROCESSED : value;
}
private String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}
private String trimToNull(String value) {
String trimValue = trimToEmpty(value);
return trimValue.isEmpty() ? null : trimValue;
}
}
@@ -42,6 +42,7 @@ import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs; import io.minio.RemoveObjectArgs;
import io.minio.http.Method; import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -79,14 +80,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final VoucherImageMapper voucherImageMapper; private final VoucherImageMapper voucherImageMapper;
private final VoucherFileMapper voucherFileMapper; private final VoucherFileMapper voucherFileMapper;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final MinioClient minioClient; private final ObjectProvider<MinioClient> minioClientProvider;
private final String minioBucketName; private final String minioBucketName;
private final String minioRootDirectory; private final String minioRootDirectory;
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService, public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService, IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper, 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.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) { @Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper; this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
@@ -95,7 +96,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.voucherImageMapper = voucherImageMapper; this.voucherImageMapper = voucherImageMapper;
this.voucherFileMapper = voucherFileMapper; this.voucherFileMapper = voucherFileMapper;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.minioClient = minioClient; this.minioClientProvider = minioClientProvider;
this.minioBucketName = minioBucketName; this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory; this.minioRootDirectory = minioRootDirectory;
} }
@@ -251,7 +252,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey) List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey)
? List.of(objectKey) : List.of(resolvedObjectKey, objectKey); ? List.of(objectKey) : List.of(resolvedObjectKey, objectKey);
for (String candidateObjectKey : candidateObjectKeys) { 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)) { OutputStream target = Files.newOutputStream(archivePath)) {
source.transferTo(target); source.transferTo(target);
return archivePath; return archivePath;
@@ -427,7 +428,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()); String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName); 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) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); VoucherFile file = new VoucherFile();
@@ -490,7 +491,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()); String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName); 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) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); VoucherFile file = new VoucherFile();
@@ -537,7 +538,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName; String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName;
String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName); String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName);
try (InputStream inputStream = imageFile.getInputStream()) { 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) .stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024)
.contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).build()); .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; 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -838,7 +839,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
String objectKey = buildObjectKey(voucher.getId(), waybillNo, String objectKey = buildObjectKey(voucher.getId(), waybillNo,
Func.isEmpty(plateNo) ? "root" : plateNo, entryName); Func.isEmpty(plateNo) ? "root" : plateNo, entryName);
String contentType = contentType(fileName); 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) .stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build()); .contentType(contentType).build());
VoucherFile file = new VoucherFile(); 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) { private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) {
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName); String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName);
if (Func.isEmpty(minioRootDirectory)) { if (Func.isEmpty(minioRootDirectory)) {
@@ -1127,7 +1142,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private String buildVoucherFileUrl(Long fileId, String objectKey) { private String buildVoucherFileUrl(Long fileId, String objectKey) {
try { 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()); .method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
} catch (Exception exception) { } catch (Exception exception) {
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception); log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
@@ -19,6 +19,7 @@ import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.WebUtil; 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.DictCache;
import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
@@ -51,10 +52,10 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -68,7 +69,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService { 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_DRAFT = "draft";
private static final String STATUS_COMPLETED = "completed"; 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<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<>(); List<Waybill> waybills = new ArrayList<>();
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>(); Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
for (int index = 0; index < rows.size(); index++) { 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 { 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); 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) { } catch (Exception exception) {
throw new ServiceException("" + (index + 1) + "" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage()); 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); loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
batch.setWaybillCount(waybills.size()); 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 @Override
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) { public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery() LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
@@ -386,11 +465,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
throw new ServiceException(fieldName + "不能为空"); throw new ServiceException(fieldName + "不能为空");
} }
String text = String.valueOf(value).trim(); String text = String.valueOf(value).trim();
try { // 宽容解析接受 2026-8-22026-9-1 8:0:0 等写法口径见根工作区 docs/import-spec.md
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER); LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
} catch (DateTimeParseException exception) { if (date == null) {
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD HH:mm:ss"); throw new ServiceException(fieldName + " 日期格式无法识别:" + text);
} }
return date;
} }
private BigDecimal defaultQuantity(BigDecimal quantity) { private BigDecimal defaultQuantity(BigDecimal quantity) {
@@ -523,6 +603,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
// 16. 同一运单标识号校验 // 16. 同一运单标识号校验
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors); validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
// 17. 同一运单标识号组内一致性校验地址/配载标识号/数量单位多货物合并的前提
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
errorMap.put(i, String.join("; ", errors)); errorMap.put(i, String.join("; ", errors));
} }
@@ -571,21 +654,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
if (!isRoadTransport(transportType)) { if (!isRoadTransport(transportType)) {
return; return;
} }
if (Func.isEmpty(vehicleNo)) { // 必填长度与格式统一为一条简短原因避免多条规则叠加导致失败原因过长
errors.add("公路运输时车牌号/航班号/船号/班列号不能为空"); // 文案保留车牌号字样导入失败明细仍能据此定位到车牌号列并标红
return; // VEHICLE_NO_PATTERN 已约束总长度为 7 8 故不再单独判断长度
} if (Func.isEmpty(vehicleNo)
if (vehicleNo.length() < 7) { || !VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()
errors.add("车牌号长度不能少于7位"); || !VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
} errors.add("车牌号校验不通过");
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位");
} }
} }
@@ -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) { private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
if (value == null || String.valueOf(value).isBlank()) { if (value == null || String.valueOf(value).isBlank()) {
return null; return null;
} }
String text = String.valueOf(value).trim(); String text = String.valueOf(value).trim();
try { // 宽容解析接受 2026-8-22026-9-1 8:0:0 等写法口径见根工作区 docs/import-spec.md
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER); LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
} catch (DateTimeParseException exception) { if (date == null) {
errors.add(fieldName + "格式必须为日期格式(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss"); errors.add(fieldName + " 日期格式无法识别:" + text);
return null; return null;
} }
return date;
} }
private Map<String, String> loadTransportTypeOptions() { private Map<String, String> loadTransportTypeOptions() {
@@ -861,8 +980,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
// 字典读取失败时使用默认值 // 字典读取失败时使用默认值
} }
if (options.isEmpty()) { if (options.isEmpty()) {
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输", // 运输方式仅支持公路铁路水路航空四种
"公路整车", "公路配载/零担", "铁路整车", "铁路零担", "水路", "航空", "多式联运", "管道运输", "其他"); List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
defaults.forEach(value -> options.put(value, value)); defaults.forEach(value -> options.put(value, value));
} }
return options; return options;
@@ -902,7 +902,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
WaybillExcel excel = data.get(index); WaybillExcel excel = data.get(index);
try { try {
Waybill waybill = Objects.requireNonNull(BeanUtil.copyProperties(excel, Waybill.class)); 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.setDataSource("批量导入");
waybill.setCreateTime(null);
waybill.setUpdateTime(null);
waybill.setCarrierJson(buildImportCarrierJson(waybill)); waybill.setCarrierJson(buildImportCarrierJson(waybill));
submit(waybill); submit(waybill);
} catch (Exception exception) { } catch (Exception exception) {