redColumns = redColumnsByRow.get(rowIndex);
+ return redColumns != null && redColumns.contains(columnIndex);
+ }
+
+ /**
+ * 标红单元格
+ *
+ * 统一只改字体颜色,不加底纹;空单元格的红色字体在屏幕上不可见,
+ * 这类错误依靠失败原因列的文字定位。
+ */
private void markRed(Cell cell) {
CellStyle currentStyle = cell.getCellStyle();
CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java
new file mode 100644
index 0000000..750c101
--- /dev/null
+++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateParser.java
@@ -0,0 +1,198 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.common.excel;
+
+import 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)。
+ *
+ * 解析规则:
+ *
+ * - 分隔符 {@code -}、{@code /}、{@code .} 均接受;补零与否均可(2026-08-02 ≡ 2026-8-2)
+ * - 日期时间:日期部分同上,时间 HH:mm:ss,可省略秒或秒+分(2026-8-2 12:3 可解析)
+ * - 拒绝:两位年份(26-8-2)、日在前(2/8/2026)、无分隔符(20260802)
+ *
+ *
+ * @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;
+ }
+
+}
diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java
new file mode 100644
index 0000000..970dde7
--- /dev/null
+++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateStringConverter.java
@@ -0,0 +1,75 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.common.excel;
+
+import 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 文本,文本原样透传。
+ *
+ * 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
+ * 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
+ * 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
+ *
+ * @author Chill
+ */
+public class LenientDateStringConverter implements Converter {
+
+ @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);
+ }
+
+}
diff --git a/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java b/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java
new file mode 100644
index 0000000..bcb070a
--- /dev/null
+++ b/blade-common/src/main/java/org/springblade/common/excel/LenientDateTimeStringConverter.java
@@ -0,0 +1,82 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.common.excel;
+
+import 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} 文本,文本原样透传。
+ *
+ * 文本的宽容解析由服务层调用 {@link LenientDateParser} 完成,不在此处抛错——
+ * 转换器阶段抛出的异常会被 fastexcel 包装成 ExcelDataConvertException 直接中断读取,
+ * 无法进入导入失败明细流程。口径见根工作区 docs/import-spec.md。
+ *
+ * @author Chill
+ */
+public class LenientDateTimeStringConverter implements Converter {
+
+ /**
+ * 数值日期序列号转文本的输出格式,与导入模板展示格式保持一致。
+ */
+ 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);
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
index 3979b3c..0639b52 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/PortTerminalController.java
@@ -1,263 +1,276 @@
-/**
- * BladeX Commercial License Agreement
- * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
- *
- * Use of this software is governed by the Commercial License Agreement
- * obtained after purchasing a license from BladeX.
- *
- * 1. This software is for development use only under a valid license
- * from BladeX.
- *
- * 2. Redistribution of this software's source code to any third party
- * without a commercial license is strictly prohibited.
- *
- * 3. Licensees may copyright their own code but cannot use segments
- * from this software for such purposes. Copyright of this software
- * remains with BladeX.
- *
- * Using this software signifies agreement to this License, and the software
- * must not be used for illegal purposes.
- *
- * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
- * not liable for any claims arising from secondary or illegal development.
- *
- * Author: Chill Zhuang (bladejava@qq.com)
- */
-package org.springblade.system.controller;
-
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
-import lombok.AllArgsConstructor;
-import org.springblade.core.boot.ctrl.BladeController;
-import org.springblade.core.excel.util.ExcelUtil;
-import org.springblade.core.mp.support.Condition;
-import org.springblade.core.mp.support.Query;
-import org.springblade.core.secure.annotation.PreAuth;
-import org.springblade.core.tenant.annotation.NonDS;
-import org.springblade.core.tool.api.R;
-import org.springblade.core.tool.utils.DateUtil;
-import org.springblade.core.tool.utils.Func;
-import org.springblade.system.excel.PortTerminalExcel;
-import org.springblade.system.excel.PortTerminalExportExcel;
-import org.springblade.system.excel.PortTerminalImporter;
-import org.springblade.system.pojo.entity.PortTerminal;
-import org.springblade.system.pojo.vo.PortTerminalVO;
-import org.springblade.system.service.IPortTerminalService;
-import org.springblade.system.wrapper.PortTerminalWrapper;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 港口码头主数据 控制器
- *
- * @author Chill
- */
-@NonDS
-@RestController
-@AllArgsConstructor
-@PreAuth(menu = "port_terminal")
-@RequestMapping("/port-terminal")
-@Tag(name = "港口码头主数据", description = "港口码头主数据")
-public class PortTerminalController extends BladeController {
-
- private static final int DEFAULT_CURRENT = 1;
- private static final int DEFAULT_SIZE = 10;
- private static final int MAX_SIZE = 100;
- private static final String SOURCE_INITIAL = "初始化录入";
- private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
- private static final String SOURCE_INITIAL_OLD = "初始导入";
- private static final String SOURCE_MANUAL = "手动录入";
- private static final String SOURCE_MANUAL_OLD = "手工导入";
-
- private final IPortTerminalService portTerminalService;
-
- /**
- * 详情
- */
- @GetMapping("/detail")
- @ApiOperationSupport(order = 1)
- @Operation(summary = "详情", description = "传入portTerminal")
- public R detail(PortTerminal portTerminal) {
- if (Func.isEmpty(portTerminal.getId())) {
- return R.fail("主键不能为空");
- }
- PortTerminal detail = portTerminalService.getById(portTerminal.getId());
- if (Func.isEmpty(detail)) {
- return R.fail("港口码头不存在");
- }
- detail.setDataSource(normalizeDataSource(detail.getDataSource()));
- return R.data(PortTerminalWrapper.build().entityVO(detail));
- }
-
- /**
- * 分页
- */
- @GetMapping("/list")
- @ApiOperationSupport(order = 2)
- @Operation(summary = "分页", description = "传入portTerminal")
- public R> list(PortTerminalVO portTerminal, Query query) {
- IPage pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
- return R.data(pages);
- }
-
- /**
- * 新增或修改
- */
- @PostMapping("/submit")
- @ApiOperationSupport(order = 3)
- @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")
- public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
- if (Func.isEmpty(ids)) {
- return R.fail("主键不能为空");
- }
- return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
- }
-
- /**
- * 启用或停用
- */
- @PostMapping("/status")
- @ApiOperationSupport(order = 5)
- @Operation(summary = "启用或停用", description = "传入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 = "上级港口下拉数据源")
- public R> portSelect() {
- List 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")
- public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
- if (file == null || file.isEmpty()) {
- return R.fail("上传文件不能为空");
- }
- String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
- if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
- return R.fail("请上传 .xls,.xlsx 标准格式文件");
- }
- List failureList = portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
- if (Func.isNotEmpty(failureList)) {
- org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
- return null;
- }
- return R.success("操作成功");
- }
-
- /**
- * 导出港口码头主数据
- */
- @GetMapping("/export-port-terminal")
- @ApiOperationSupport(order = 8)
- @Operation(summary = "导出港口码头主数据")
- public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map portTerminal, HttpServletResponse response) {
- Object ids = portTerminal.remove("ids");
- Object dataSource = portTerminal.remove("dataSource");
- portTerminal.remove("Blade-Auth");
- portTerminal.remove("Authorization");
- portTerminal.remove("access_token");
- normalizeRegionCodeCondition(portTerminal);
- QueryWrapper queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
- applyDataSourceCondition(queryWrapper, dataSource);
- if (Func.isNotEmpty(ids)) {
- queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
- }
- List list = portTerminalService.exportPortTerminal(queryWrapper);
- ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
- }
-
- /**
- * 导出模板
- */
- @GetMapping("/export-template")
- @ApiOperationSupport(order = 9)
- @Operation(summary = "导出模板")
- public void exportTemplate(HttpServletResponse response) {
- List list = new ArrayList<>();
- ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
- }
-
- private Query normalizeQuery(Query query) {
- if (query == null) {
- query = new Query();
- }
- if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
- query.setCurrent(DEFAULT_CURRENT);
- }
- if (query.getSize() == null || query.getSize() <= 0) {
- query.setSize(DEFAULT_SIZE);
- }
- if (query.getSize() > MAX_SIZE) {
- query.setSize(MAX_SIZE);
- }
- return query;
- }
-
- private void normalizeRegionCodeCondition(Map params) {
- Object regionCode = params.remove("regionCode");
- if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
- params.put("districtCode", regionCode);
- }
- }
-
- private void applyDataSourceCondition(QueryWrapper queryWrapper, Object dataSource) {
- String value = Func.toStrWithEmpty(dataSource, "");
- if (Func.isEmpty(value)) {
- return;
- }
- 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;
- }
-
-}
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.excel.util.ExcelUtil;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.annotation.PreAuth;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.common.excel.ImportFailureExcelUtil;
+import org.springblade.system.excel.ImportFailureException;
+import org.springblade.system.excel.PortTerminalExcel;
+import org.springblade.system.excel.PortTerminalExportExcel;
+import org.springblade.system.excel.PortTerminalImporter;
+import org.springblade.system.pojo.entity.PortTerminal;
+import org.springblade.system.pojo.vo.PortTerminalVO;
+import org.springblade.system.service.IPortTerminalService;
+import org.springblade.system.wrapper.PortTerminalWrapper;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 港口码头主数据 控制器
+ *
+ * @author Chill
+ */
+@NonDS
+@RestController
+@AllArgsConstructor
+@PreAuth(menu = "port_terminal")
+@RequestMapping("/port-terminal")
+@Tag(name = "港口码头主数据", description = "港口码头主数据")
+public class PortTerminalController extends BladeController {
+
+ private static final int DEFAULT_CURRENT = 1;
+ private static final int DEFAULT_SIZE = 10;
+ private static final int MAX_SIZE = 100;
+ private static final String SOURCE_INITIAL = "初始化录入";
+ private static final String SOURCE_INITIAL_IMPORT = "初始化导入";
+ private static final String SOURCE_INITIAL_OLD = "初始导入";
+ private static final String SOURCE_MANUAL = "手动录入";
+ private static final String SOURCE_MANUAL_OLD = "手工导入";
+
+ private final IPortTerminalService portTerminalService;
+
+ /**
+ * 详情
+ */
+ @GetMapping("/detail")
+ @ApiOperationSupport(order = 1)
+ @Operation(summary = "详情", description = "传入portTerminal")
+ public R detail(PortTerminal portTerminal) {
+ if (Func.isEmpty(portTerminal.getId())) {
+ return R.fail("主键不能为空");
+ }
+ PortTerminal detail = portTerminalService.getById(portTerminal.getId());
+ if (Func.isEmpty(detail)) {
+ return R.fail("港口码头不存在");
+ }
+ detail.setDataSource(normalizeDataSource(detail.getDataSource()));
+ return R.data(PortTerminalWrapper.build().entityVO(detail));
+ }
+
+ /**
+ * 分页
+ */
+ @GetMapping("/list")
+ @ApiOperationSupport(order = 2)
+ @Operation(summary = "分页", description = "传入portTerminal")
+ public R> list(PortTerminalVO portTerminal, Query query) {
+ IPage pages = portTerminalService.selectPortTerminalPage(Condition.getPage(normalizeQuery(query)), portTerminal);
+ return R.data(pages);
+ }
+
+ /**
+ * 新增或修改
+ */
+ @PostMapping("/submit")
+ @ApiOperationSupport(order = 3)
+ @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")
+ public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
+ if (Func.isEmpty(ids)) {
+ return R.fail("主键不能为空");
+ }
+ return R.status(portTerminalService.deleteLogic(Func.toLongList(ids)));
+ }
+
+ /**
+ * 启用或停用
+ */
+ @PostMapping("/status")
+ @ApiOperationSupport(order = 5)
+ @Operation(summary = "启用或停用", description = "传入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 = "上级港口下拉数据源")
+ public R> portSelect() {
+ List 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")
+ public R importPortTerminal(MultipartFile file, HttpServletResponse response) {
+ if (file == null || file.isEmpty()) {
+ return R.fail("上传文件不能为空");
+ }
+ String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
+ if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
+ return R.fail("请上传 .xls,.xlsx 标准格式文件");
+ }
+ try {
+ portTerminalService.importPortTerminal(ExcelUtil.read(file, PortTerminalExcel.class));
+ } catch (ImportFailureException exception) {
+ // 全失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
+ exportFailure(response, exception.getFailureList());
+ return null;
+ }
+ return R.success("操作成功");
+ }
+
+ /**
+ * 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
+ *
+ * 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
+ */
+ private void exportFailure(HttpServletResponse response, List> failureList) {
+ ImportFailureExcelUtil.exportFailureReasonOnly(response, "港口码头主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, PortTerminalExcel.class);
+ }
+
+ /**
+ * 导出港口码头主数据
+ */
+ @GetMapping("/export-port-terminal")
+ @ApiOperationSupport(order = 8)
+ @Operation(summary = "导出港口码头主数据")
+ public void exportPortTerminal(@Parameter(hidden = true) @RequestParam Map portTerminal, HttpServletResponse response) {
+ Object ids = portTerminal.remove("ids");
+ Object dataSource = portTerminal.remove("dataSource");
+ portTerminal.remove("Blade-Auth");
+ portTerminal.remove("Authorization");
+ portTerminal.remove("access_token");
+ normalizeRegionCodeCondition(portTerminal);
+ QueryWrapper queryWrapper = Condition.getQueryWrapper(portTerminal, PortTerminal.class);
+ applyDataSourceCondition(queryWrapper, dataSource);
+ if (Func.isNotEmpty(ids)) {
+ queryWrapper.lambda().in(PortTerminal::getId, Func.toLongList(ids.toString()));
+ }
+ List list = portTerminalService.exportPortTerminal(queryWrapper);
+ ExcelUtil.export(response, "港口码头主数据" + DateUtil.time(), "港口码头主数据表", list, PortTerminalExportExcel.class);
+ }
+
+ /**
+ * 导出模板
+ */
+ @GetMapping("/export-template")
+ @ApiOperationSupport(order = 9)
+ @Operation(summary = "导出模板")
+ public void exportTemplate(HttpServletResponse response) {
+ List list = new ArrayList<>();
+ ExcelUtil.export(response, "港口码头主数据模板", "港口码头主数据表", list, PortTerminalExcel.class);
+ }
+
+ private Query normalizeQuery(Query query) {
+ if (query == null) {
+ query = new Query();
+ }
+ if (query.getCurrent() == null || query.getCurrent() < DEFAULT_CURRENT) {
+ query.setCurrent(DEFAULT_CURRENT);
+ }
+ if (query.getSize() == null || query.getSize() <= 0) {
+ query.setSize(DEFAULT_SIZE);
+ }
+ if (query.getSize() > MAX_SIZE) {
+ query.setSize(MAX_SIZE);
+ }
+ return query;
+ }
+
+ private void normalizeRegionCodeCondition(Map params) {
+ Object regionCode = params.remove("regionCode");
+ if (Func.isNotEmpty(regionCode) && Func.isEmpty(params.get("districtCode"))) {
+ params.put("districtCode", regionCode);
+ }
+ }
+
+ private void applyDataSourceCondition(QueryWrapper queryWrapper, Object dataSource) {
+ String value = Func.toStrWithEmpty(dataSource, "");
+ if (Func.isEmpty(value)) {
+ return;
+ }
+ 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;
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
index 941b7eb..b59573d 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/CurrencyExcel.java
@@ -35,7 +35,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
-import java.time.LocalDate;
/**
* 币种汇率 Excel
@@ -62,8 +61,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("汇率")
private BigDecimal exchangeRate;
- @ExcelProperty("生效日期")
- private LocalDate effectiveDate;
+ @ExcelProperty(value = "生效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
+ private String effectiveDate;
@ExcelProperty("状态")
private String statusName;
@@ -71,8 +70,8 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("来源")
private String dataSource;
- @ExcelProperty("失效日期")
- private LocalDate expiryDate;
+ @ExcelProperty(value = "失效日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
+ private String expiryDate;
@ExcelProperty("备注")
private String remark;
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java
new file mode 100644
index 0000000..0277ec0
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/ImportFailureException.java
@@ -0,0 +1,60 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.excel;
+
+import org.springblade.core.log.exception.ServiceException;
+
+import java.io.Serial;
+import java.util.List;
+
+/**
+ * 导入失败异常,携带失败明细用于导出原表并标注错误。
+ *
+ * 批量导入采用「全失败即整批回滚」语义:任一行校验失败都会抛出本异常触发事务回滚,
+ * 失败明细在抛异常前已收集完毕,因此回滚不影响明细的内容。
+ *
+ * @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;
+ }
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
index 1dd71ac..306e144 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/excel/UserExcel.java
@@ -34,7 +34,6 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
-import java.util.Date;
/**
* UserExcel
@@ -102,7 +101,7 @@ public class UserExcel implements Serializable {
private String postName;
@ColumnWidth(20)
- @ExcelProperty("生日")
- private Date birthday;
+ @ExcelProperty(value = "生日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
+ private String birthday;
}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
index 6a4dd47..70dd2e9 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/UserMapper.xml
@@ -95,7 +95,7 @@