🐛 行政区划导入补事务实现整批回滚,缓存清理移到成功路径
问题:RegionServiceImpl.importRegion 是唯一没有 @Transactional 的导入板块, 循环内吞掉异常后正常 return,已落库行直接提交。且 cacheChanged 标志导致 「先清缓存、再提交事务」:循环内已有行 submit 成功 → 后面行失败 → 方法 正常返回 → 惰性树缓存被清/重算成失败批次的中间状态,库回滚后缓存脏掉。 改动: - 服务层:补 @Transactional(rollbackFor = Exception.class);错误收集完后 抛 ImportFailureException(org.springblade.common.excel)触发整批回滚。 - 缓存语义重构:submit 拆出 clearCache 参数(公开 submit(region) 行为不变, 手工新增仍即时清缓存);导入路径传 false 不逐行清缓存, 统一移到方法末尾成功路径上——异常抛出后不可达,失败批次缓存保持原状。 - 控制器:改 catch 异常导出(exportFailureReasonOnly)+ 补上传文件校验。 验证:mvn install -DskipTests 通过;行政区划回归 25/25 (tests/fixtures/region/),Redis 断言覆盖:成功路径缓存被清、 失败路径缓存键原样保留、基线行不被失败批次污染。
This commit is contained in:
+13
-3
@@ -46,6 +46,7 @@ import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tenant.annotation.NonDS;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.pojo.entity.Region;
|
||||
import org.springblade.system.excel.RegionExcel;
|
||||
import org.springblade.system.excel.RegionExportExcel;
|
||||
@@ -191,9 +192,18 @@ 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;
|
||||
}
|
||||
return R.success("操作成功");
|
||||
|
||||
+25
-5
@@ -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("导入数据不能为空");
|
||||
}
|
||||
// 全量校验:任何一行失败都整批回滚,因此先收集所有错误再统一抛出。
|
||||
// 注意不能只往 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user