Merge remote-tracking branch 'websoft/fix/issue-2-10-import-batch-rollback'

This commit is contained in:
2026-09-27 13:03:12 +08:00
60 changed files with 1268 additions and 192 deletions
@@ -157,10 +157,21 @@ public class AirportMasterController extends BladeController {
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
List<AirportMasterExcel> failureList = airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "空港机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, AirportMasterExcel.class);
try {
airportMasterService.importAirportMaster(ExcelUtil.read(file, AirportMasterExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), AirportMasterExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "机场主数据导入失败明细" + DateUtil.time(), "导入失败明细", file, AirportMasterExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -46,9 +46,9 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.system.excel.CargoTypeExcel;
import org.springblade.system.excel.CargoTypeExportExcel;
import org.springblade.system.excel.CargoTypeImportFailureExcel;
import org.springblade.system.pojo.entity.CargoType;
import org.springblade.system.pojo.vo.CargoTypeVO;
import org.springblade.system.service.ICargoTypeService;
@@ -156,15 +156,41 @@ public class CargoTypeController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入货物类型", description = "传入excel")
public R importCargoType(MultipartFile file, HttpServletResponse response) {
List<CargoTypeExcel> data = ExcelUtil.read(file, CargoTypeExcel.class);
List<CargoTypeImportFailureExcel> failureList = cargoTypeService.importCargoType(data);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
cargoTypeService.importCargoType(ExcelUtil.read(file, CargoTypeExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
exportFailure(response, exception.getFailureList());
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", file, CargoTypeExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
/**
* 导出导入失败明细,内容为原表全部数据并在末尾追加失败原因列。
* <p>
* 失败数据仅标红出错单元格与失败原因列,表头保持默认样式。
*/
private void exportFailure(HttpServletResponse response, List<?> failureList) {
ImportFailureExcelUtil.exportFailureReasonOnly(response, "货物类型导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CargoTypeExcel.class);
}
/**
* 导出货物类型
*/
@@ -149,10 +149,28 @@ public class CurrencyController extends BladeController {
@ApiOperationSupport(order = 7)
@Operation(summary = "导入币种汇率", description = "传入excel")
public R importCurrency(MultipartFile file, HttpServletResponse response) {
List<CurrencyExcel> failureList = currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", failureList, CurrencyExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
currencyService.importCurrency(ExcelUtil.read(file, CurrencyExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), CurrencyExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "币种汇率导入失败明细" + DateUtil.time(), "导入失败明细", file, CurrencyExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -36,6 +36,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
@@ -47,7 +48,6 @@ import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.excel.FeeItemExcel;
import org.springblade.system.excel.FeeItemExportExcel;
import org.springblade.system.excel.FeeItemImportFailureExcel;
import org.springblade.system.pojo.entity.FeeItem;
import org.springblade.system.pojo.vo.FeeItemVO;
import org.springblade.system.service.IFeeItemService;
@@ -138,11 +138,28 @@ public class FeeItemController extends BladeController {
@ApiOperationSupport(order = 6)
@Operation(summary = "导入费用项", description = "传入excel")
public R importFeeItem(MultipartFile file, HttpServletResponse response) {
List<FeeItemExcel> data = ExcelUtil.read(file, FeeItemExcel.class);
List<FeeItemImportFailureExcel> failureList = feeItemService.importFeeItem(data);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", failureList, FeeItemExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
feeItemService.importFeeItem(ExcelUtil.read(file, FeeItemExcel.class));
} catch (ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
ImportFailureExcelUtil.exportFailureReasonOnly(response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), FeeItemExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "费用项导入失败明细" + DateUtil.time(), "导入失败明细", file, FeeItemExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("导入数据成功");
}
@@ -44,7 +44,7 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.system.excel.ImportFailureException;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.excel.PortTerminalImporter;
@@ -160,10 +160,21 @@ public class RailwayStationController extends BladeController {
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
List<RailwayStationExcel> failureList = railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class));
if (Func.isNotEmpty(failureList)) {
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "铁路车站主数据导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RailwayStationExcel.class);
try {
railwayStationService.importRailwayStation(ExcelUtil.read(file, RailwayStationExcel.class));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误(含按列标红)。
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RailwayStationExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "铁路站点导入失败明细" + DateUtil.time(), "导入失败明细", file, RailwayStationExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -46,6 +46,7 @@ import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.pojo.entity.Region;
import org.springblade.system.excel.RegionExcel;
import org.springblade.system.excel.RegionExportExcel;
@@ -191,10 +192,28 @@ public class RegionController extends BladeController {
@ApiOperationSupport(order = 10)
@Operation(summary = "导入行政区划", description = "传入excel")
public R importRegion(MultipartFile file, Integer isCovered, HttpServletResponse response) {
List<RegionExcel> failureList = regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1));
if (!failureList.isEmpty()) {
org.springblade.common.excel.ImportFailureExcelUtil.export(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, RegionExcel.class);
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
regionService.importRegion(ExcelUtil.read(file, RegionExcel.class), Objects.equals(isCovered, 1));
} catch (org.springblade.common.excel.ImportFailureException exception) {
// 任一行失败即整批回滚,导出原表全部数据并标注错误,用户修正后重新导入。
org.springblade.common.excel.ImportFailureExcelUtil.exportFailureReasonOnly(response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", exception.getFailureList(), RegionExcel.class);
return null;
} catch (Exception readException) {
// 读取阶段异常兜底:单元格内容无法转换(如金额列填「一万二千元」)时,
// EasyExcel 在 ExcelUtil.read 阶段即抛异常,走不到行级校验。
// 此处用原始文本重读原表,导出「全量行 + 出错行原因」,避免用户看到 500 技术文案。
if (org.springblade.common.excel.ImportFailureExcelUtil.exportReadFailure(
response, "行政区划导入失败明细" + DateUtil.time(), "导入失败明细", file, RegionExcel.class, readException)) {
return null;
}
throw readException;
}
return R.success("操作成功");
}
@@ -76,7 +76,7 @@ public class CurrencyExcel implements Serializable {
@ExcelProperty("备注")
private String remark;
@ExcelProperty
@ExcelIgnore
private String errorMessage;
}
@@ -1,60 +0,0 @@
/**
* 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;
}
}
@@ -127,7 +127,7 @@ public class AirportMasterServiceImpl extends BaseServiceImpl<AirportMasterMappe
@Transactional(rollbackFor = Exception.class)
public List<AirportMasterExcel> importAirportMaster(List<AirportMasterExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<AirportMasterExcel> errorList = new ArrayList<>();
List<AirportMaster> airportMasterList = new ArrayList<>();
@@ -160,8 +160,9 @@ public class AirportMasterServiceImpl extends BaseServiceImpl<AirportMasterMappe
}
}
if (Func.isNotEmpty(errorList)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (AirportMaster airportMaster : airportMasterList) {
// 编码命中逻辑删除记录时复用原主键,恢复后更新,避免唯一索引冲突。
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.Func;
@@ -134,10 +135,14 @@ public class CargoTypeServiceImpl extends BaseServiceImpl<CargoTypeMapper, Cargo
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<CargoTypeImportFailureExcel> importCargoType(List<CargoTypeExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
List<CargoTypeImportFailureExcel> failureList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
CargoTypeExcel excel = data.get(index);
@@ -152,6 +157,11 @@ public class CargoTypeServiceImpl extends BaseServiceImpl<CargoTypeMapper, Cargo
failureList.add(toImportFailureExcel(excel, message));
}
}
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new ImportFailureException(data);
}
return failureList;
}
@@ -115,10 +115,16 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<CurrencyExcel> importCurrency(List<CurrencyExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意:循环内除 save 外还有 rebuildEnabledValidity 这个派生写——
// 若吞掉异常正常 return,主表数据和派生的有效期区间都会落库;
// 必须抛异常让两者一起回滚,否则会出现「主表回滚了、有效期区间
// 却按失败批次算过」的脏数据。
List<CurrencyExcel> errorList = new ArrayList<>();
int successCount = 0;
for (int index = 0; index < data.size(); index++) {
@@ -140,6 +146,11 @@ public class CurrencyServiceImpl extends BaseServiceImpl<CurrencyMapper, Currenc
errorList.add(excel);
}
}
if (Func.isNotEmpty(errorList)) {
// 抛出携带失败明细的异常,触发事务回滚(含 rebuildEnabledValidity 的派生写)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
return errorList;
}
@@ -106,8 +106,11 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
@Transactional(rollbackFor = Exception.class)
public List<FeeItemImportFailureExcel> importFeeItem(List<FeeItemExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意不能只往 failureList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
List<FeeItemImportFailureExcel> failureList = new ArrayList<>();
for (int index = 0; index < data.size(); index++) {
FeeItemExcel excel = data.get(index);
@@ -118,9 +121,18 @@ public class FeeItemServiceImpl extends BaseServiceImpl<FeeItemMapper, FeeItem>
save(feeItem);
} catch (Exception exception) {
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
// 除收集到 failureList 外,还必须把原因写回原行对象:
// 抛出异常时携带的是原表 data,导出工具从行对象的 errorMessage 字段取原因,
// 只写在副本上会导致失败明细里错误行的原因为空。
excel.setErrorMessage("第" + (index + 2) + "行:" + message);
failureList.add(toImportFailureExcel(excel, "第" + (index + 2) + "行:" + message));
}
}
if (Func.isNotEmpty(failureList)) {
// 抛出携带失败明细的异常,触发事务回滚。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
return failureList;
}
@@ -34,8 +34,8 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.system.cache.UserCache;
import org.springblade.system.excel.ImportFailureException;
import org.springblade.system.excel.PortTerminalExcel;
import org.springblade.system.excel.PortTerminalExportExcel;
import org.springblade.system.mapper.PortTerminalMapper;
@@ -140,7 +140,7 @@ public class PortTerminalServiceImpl extends BaseServiceImpl<PortTerminalMapper,
@Transactional(rollbackFor = Exception.class)
public List<PortTerminalExcel> importPortTerminal(List<PortTerminalExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
List<PortTerminalExcel> failureList = new ArrayList<>();
@@ -129,7 +129,7 @@ public class RailwayStationServiceImpl extends BaseServiceImpl<RailwayStationMap
@Transactional(rollbackFor = Exception.class)
public List<RailwayStationExcel> importRailwayStation(List<RailwayStationExcel> data) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
List<RailwayStationExcel> errorList = new ArrayList<>();
List<RailwayStation> railwayStationList = new ArrayList<>();
@@ -165,8 +165,9 @@ public class RailwayStationServiceImpl extends BaseServiceImpl<RailwayStationMap
}
}
if (Func.isNotEmpty(errorList)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return errorList;
// 抛出携带失败明细的异常,触发事务回滚(替代旧的 setRollbackOnly)。
// 明细为原表全部行(未出错行仅无错误原因),用户可对照原表修正后重新导入。
throw new org.springblade.common.excel.ImportFailureException(data);
}
for (RailwayStation railwayStation : railwayStationList) {
prepareImportTarget(railwayStation);
@@ -41,6 +41,7 @@ import org.springblade.system.mapper.RegionMapper;
import org.springblade.system.service.IRegionService;
import org.springblade.system.pojo.vo.RegionVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
@@ -60,6 +61,17 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
@Override
public boolean submit(Region region) {
return submit(region, true);
}
/**
* 提交区划,clearCache 控制是否在保存成功后立即清理惰性树缓存。
* <p>
* 批量导入路径传 false:导入内部会收集全部错误,只有整批成功才统一清缓存;
* 若逐行清缓存,失败批次中已成功行会把缓存清掉/重算成中间状态,
* 与随后的事务回滚形成「库回滚了、缓存却是脏的」。
*/
private boolean submit(Region region, boolean clearCache) {
Date now = new Date();
Long currentUserId = AuthUtil.getUserId();
boolean isNew = StringUtil.isBlank(region.getOriginalCode());
@@ -138,7 +150,7 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
region.setVillageName(name);
}
boolean result = StringUtil.isNotBlank(region.getOriginalCode()) ? this.updateById(region) : this.save(region);
if (result) {
if (result && clearCache) {
clearLazyTree();
}
return result;
@@ -189,12 +201,15 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<RegionExcel> importRegion(List<RegionExcel> data, Boolean isCovered) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
throw new ServiceException("未读取到数据行,请确认表头下方已填写内容");
}
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
// 注意不能只往 errorList 里塞数据就 return——异常被吞掉后方法会正常返回,
// Spring 会照常提交事务,@Transactional 起不到回滚作用。
List<RegionExcel> errorList = new ArrayList<>();
boolean cacheChanged = false;
for (int index = 0; index < data.size(); index++) {
RegionExcel excel = data.get(index);
try {
@@ -210,15 +225,20 @@ public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> impleme
if (Boolean.TRUE.equals(isCovered) && this.getById(region.getCode()) != null) {
region.setOriginalCode(region.getCode());
}
cacheChanged = this.submit(region) || cacheChanged;
// 导入路径不逐行清缓存,缓存只在整批成功后统一清一次。
this.submit(region, false);
} catch (Exception exception) {
excel.setErrorMessage(exception.getMessage());
errorList.add(excel);
}
}
if (cacheChanged) {
clearLazyTree();
if (Func.isNotEmpty(errorList)) {
// 抛出携带失败明细的异常,触发事务回滚。缓存清理在异常抛出后不可达,
// 只会发生在成功路径上——避免「库回滚了、惰性树缓存却按失败批次
// 的中间状态被清掉/重算」的脏缓存。
throw new org.springblade.common.excel.ImportFailureException(data);
}
clearLazyTree();
return errorList;
}