From 7b51cf282d32dd74d0b22e83a6aae73f67538174 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Mon, 16 Mar 2026 01:00:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(energy):=20=E6=B7=BB=E5=8A=A0=E4=B8=89?= =?UTF-8?q?=E6=AD=A5=E5=AF=BC=E5=85=A5=E6=8E=A5=E5=8F=A3=EF=BC=88=E9=A2=84?= =?UTF-8?q?=E8=A7=88/=E7=A1=AE=E8=AE=A4/=E8=BF=9B=E5=BA=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../energy/enums/ErrorCodeConstants.java | 1 + .../record/HydrogenRecordController.java | 27 ++ .../vo/HydrogenRecordImportPreviewVO.java | 47 ++++ .../vo/HydrogenRecordImportProgressVO.java | 15 ++ .../service/record/HydrogenRecordService.java | 5 + .../record/HydrogenRecordServiceImpl.java | 244 ++++++++++++++++++ 6 files changed, 339 insertions(+) create mode 100644 yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportPreviewVO.java create mode 100644 yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportProgressVO.java diff --git a/yudao-module-energy/yudao-module-energy-api/src/main/java/cn/iocoder/yudao/module/energy/enums/ErrorCodeConstants.java b/yudao-module-energy/yudao-module-energy-api/src/main/java/cn/iocoder/yudao/module/energy/enums/ErrorCodeConstants.java index cd62d83..0e39d49 100644 --- a/yudao-module-energy/yudao-module-energy-api/src/main/java/cn/iocoder/yudao/module/energy/enums/ErrorCodeConstants.java +++ b/yudao-module-energy/yudao-module-energy-api/src/main/java/cn/iocoder/yudao/module/energy/enums/ErrorCodeConstants.java @@ -7,6 +7,7 @@ public interface ErrorCodeConstants { ErrorCode HYDROGEN_RECORD_NOT_EXISTS = new ErrorCode(1_050_000_000, "加氢记录不存在"); ErrorCode HYDROGEN_RECORD_IMPORT_EMPTY = new ErrorCode(1_050_000_001, "导入数据为空"); ErrorCode HYDROGEN_RECORD_DUPLICATE = new ErrorCode(1_050_000_002, "存在重复导入记录"); + ErrorCode HYDROGEN_RECORD_IMPORT_PREVIEW_EXPIRED = new ErrorCode(1_050_000_003, "导入预览已过期或不存在,请重新上传文件"); // ========== 加氢明细 1-050-001-000 ========== ErrorCode HYDROGEN_DETAIL_NOT_EXISTS = new ErrorCode(1_050_001_000, "加氢明细不存在"); diff --git a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/HydrogenRecordController.java b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/HydrogenRecordController.java index 6064bfc..a618abd 100644 --- a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/HydrogenRecordController.java +++ b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/HydrogenRecordController.java @@ -102,6 +102,33 @@ public class HydrogenRecordController { return success(result); } + @PostMapping("/import-preview") + @Operation(summary = "导入预览") + @PreAuthorize("@ss.hasPermission('energy:hydrogen-record:import')") + public CommonResult importPreview( + @RequestParam("stationId") Long stationId, + @RequestParam("file") MultipartFile file) throws Exception { + List list = ExcelUtils.read(file, HydrogenRecordImportVO.class); + return success(hydrogenRecordService.importPreview(stationId, list)); + } + + @PostMapping("/import-confirm") + @Operation(summary = "确认导入") + @PreAuthorize("@ss.hasPermission('energy:hydrogen-record:import')") + public CommonResult> importConfirm( + @RequestParam("batchNo") String batchNo, + @RequestParam(value = "duplicateStrategy", defaultValue = "skip") String duplicateStrategy) { + return success(hydrogenRecordService.importConfirm(batchNo, duplicateStrategy)); + } + + @GetMapping("/import-progress") + @Operation(summary = "导入进度") + @PreAuthorize("@ss.hasPermission('energy:hydrogen-record:import')") + public CommonResult getImportProgress( + @RequestParam("batchNo") String batchNo) { + return success(hydrogenRecordService.getImportProgress(batchNo)); + } + @PutMapping("/match") @Operation(summary = "手动匹配车辆") @PreAuthorize("@ss.hasPermission('energy:hydrogen-record:update')") diff --git a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportPreviewVO.java b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportPreviewVO.java new file mode 100644 index 0000000..dd7d39a --- /dev/null +++ b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportPreviewVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.energy.controller.admin.record.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "管理后台 - 加氢记录导入预览 Response VO") +@Data +public class HydrogenRecordImportPreviewVO { + + @Schema(description = "批次号") + private String batchNo; + @Schema(description = "总行数") + private Integer totalCount; + @Schema(description = "有效行数") + private Integer validCount; + @Schema(description = "重复行数") + private Integer duplicateCount; + @Schema(description = "错误行数") + private Integer errorCount; + @Schema(description = "预览记录") + private List records; + @Schema(description = "重复记录") + private List duplicates; + @Schema(description = "错误明细") + private List errors; + + @Data + public static class RecordPreviewItem { + private Integer rowNum; + private String plateNumber; + private LocalDate hydrogenDate; + private BigDecimal hydrogenQuantity; + private BigDecimal unitPrice; + private BigDecimal amount; + private BigDecimal mileage; + private Boolean isDuplicate; + } + + @Data + public static class ImportErrorItem { + private Integer rowNum; + private String reason; + } +} diff --git a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportProgressVO.java b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportProgressVO.java new file mode 100644 index 0000000..526f110 --- /dev/null +++ b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/controller/admin/record/vo/HydrogenRecordImportProgressVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.energy.controller.admin.record.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 加氢记录导入进度 Response VO") +@Data +public class HydrogenRecordImportProgressVO { + @Schema(description = "当前进度") + private Integer current; + @Schema(description = "总数") + private Integer total; + @Schema(description = "状态: processing / completed / failed") + private String status; +} diff --git a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordService.java b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordService.java index a996dd1..973289b 100644 --- a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordService.java +++ b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordService.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.energy.service.record; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportPreviewVO; +import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportProgressVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordPageReqVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordSaveReqVO; @@ -17,4 +19,7 @@ public interface HydrogenRecordService { Map importRecords(Long stationId, List list, String batchNo); void matchRecord(Long id, Long vehicleId, Long customerId); Map batchMatch(); + HydrogenRecordImportPreviewVO importPreview(Long stationId, List list); + Map importConfirm(String batchNo, String duplicateStrategy); + HydrogenRecordImportProgressVO getImportProgress(String batchNo); } diff --git a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordServiceImpl.java b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordServiceImpl.java index 4af1397..527d43d 100644 --- a/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordServiceImpl.java +++ b/yudao-module-energy/yudao-module-energy-server/src/main/java/cn/iocoder/yudao/module/energy/service/record/HydrogenRecordServiceImpl.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.energy.service.record; +import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportPreviewVO; +import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportProgressVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordImportVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordPageReqVO; import cn.iocoder.yudao.module.energy.controller.admin.record.vo.HydrogenRecordSaveReqVO; @@ -15,10 +18,17 @@ import cn.iocoder.yudao.module.energy.event.RecordMatchedEvent; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.energy.enums.ErrorCodeConstants.*; @@ -28,10 +38,16 @@ import static cn.iocoder.yudao.module.energy.enums.ErrorCodeConstants.*; @Slf4j public class HydrogenRecordServiceImpl implements HydrogenRecordService { + private static final String REDIS_KEY_PREVIEW = "energy:import:preview:"; + private static final String REDIS_KEY_PROGRESS = "energy:import:progress:"; + private static final long PREVIEW_TTL_MINUTES = 15L; + @Resource private EnergyHydrogenRecordMapper hydrogenRecordMapper; @Resource private ApplicationEventPublisher eventPublisher; + @Resource + private StringRedisTemplate stringRedisTemplate; @Override public Long createRecord(HydrogenRecordSaveReqVO createReqVO) { @@ -144,6 +160,229 @@ public class HydrogenRecordServiceImpl implements HydrogenRecordService { return result; } + // ==================== 三步导入接口 ==================== + + @Override + public HydrogenRecordImportPreviewVO importPreview(Long stationId, List list) { + if (list == null || list.isEmpty()) { + throw exception(HYDROGEN_RECORD_IMPORT_EMPTY); + } + + String batchNo = generateBatchNo(); + + List validRecords = new ArrayList<>(); + List duplicateRecords = new ArrayList<>(); + List errors = new ArrayList<>(); + + // Build a set of keys for existing DB records (stationId+plateNumber+hydrogenDate) + // We'll check duplicates on-the-fly per record to avoid pulling too much data at once + Set seenInBatch = new HashSet<>(); + + int rowNum = 1; + for (HydrogenRecordImportVO importVO : list) { + // Validate required fields + if (importVO.getPlateNumber() == null || importVO.getPlateNumber().isBlank()) { + HydrogenRecordImportPreviewVO.ImportErrorItem err = new HydrogenRecordImportPreviewVO.ImportErrorItem(); + err.setRowNum(rowNum); + err.setReason("车牌号不能为空"); + errors.add(err); + rowNum++; + continue; + } + if (importVO.getHydrogenDate() == null) { + HydrogenRecordImportPreviewVO.ImportErrorItem err = new HydrogenRecordImportPreviewVO.ImportErrorItem(); + err.setRowNum(rowNum); + err.setReason("加氢日期不能为空"); + errors.add(err); + rowNum++; + continue; + } + if (importVO.getHydrogenQuantity() == null) { + HydrogenRecordImportPreviewVO.ImportErrorItem err = new HydrogenRecordImportPreviewVO.ImportErrorItem(); + err.setRowNum(rowNum); + err.setReason("加氢量不能为空"); + errors.add(err); + rowNum++; + continue; + } + + // Check duplicate: stationId + plateNumber + hydrogenDate + String dedupKey = stationId + ":" + importVO.getPlateNumber() + ":" + importVO.getHydrogenDate(); + boolean isDuplicateInBatch = seenInBatch.contains(dedupKey); + boolean isDuplicateInDb = false; + if (!isDuplicateInBatch) { + Long count = hydrogenRecordMapper.selectCount( + new LambdaQueryWrapperX() + .eq(EnergyHydrogenRecordDO::getStationId, stationId) + .eq(EnergyHydrogenRecordDO::getPlateNumber, importVO.getPlateNumber()) + .eq(EnergyHydrogenRecordDO::getHydrogenDate, importVO.getHydrogenDate())); + isDuplicateInDb = count != null && count > 0; + } + + HydrogenRecordImportPreviewVO.RecordPreviewItem item = new HydrogenRecordImportPreviewVO.RecordPreviewItem(); + item.setRowNum(rowNum); + item.setPlateNumber(importVO.getPlateNumber()); + item.setHydrogenDate(importVO.getHydrogenDate()); + item.setHydrogenQuantity(importVO.getHydrogenQuantity()); + item.setUnitPrice(importVO.getUnitPrice()); + item.setAmount(importVO.getAmount()); + item.setMileage(importVO.getMileage()); + item.setIsDuplicate(isDuplicateInBatch || isDuplicateInDb); + + if (isDuplicateInBatch || isDuplicateInDb) { + duplicateRecords.add(item); + } else { + validRecords.add(item); + seenInBatch.add(dedupKey); + } + + rowNum++; + } + + // Build cache payload + Map cachePayload = new HashMap<>(); + cachePayload.put("stationId", stationId); + cachePayload.put("validRecords", validRecords); + cachePayload.put("duplicateRecords", duplicateRecords); + + // Store in Redis with 15-minute TTL + stringRedisTemplate.opsForValue().set( + REDIS_KEY_PREVIEW + batchNo, + JSONUtil.toJsonStr(cachePayload), + PREVIEW_TTL_MINUTES, + TimeUnit.MINUTES); + + // Build response + HydrogenRecordImportPreviewVO result = new HydrogenRecordImportPreviewVO(); + result.setBatchNo(batchNo); + result.setTotalCount(list.size()); + result.setValidCount(validRecords.size()); + result.setDuplicateCount(duplicateRecords.size()); + result.setErrorCount(errors.size()); + // Return first 50 preview records + result.setRecords(validRecords.stream().limit(50).collect(Collectors.toList())); + result.setDuplicates(duplicateRecords.stream().limit(50).collect(Collectors.toList())); + result.setErrors(errors); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map importConfirm(String batchNo, String duplicateStrategy) { + // Read cached preview data + String cacheJson = stringRedisTemplate.opsForValue().get(REDIS_KEY_PREVIEW + batchNo); + if (cacheJson == null || cacheJson.isBlank()) { + throw exception(HYDROGEN_RECORD_IMPORT_PREVIEW_EXPIRED); + } + + cn.hutool.json.JSONObject payload = JSONUtil.parseObj(cacheJson); + Long stationId = payload.getLong("stationId"); + + List validRecords = + JSONUtil.toList(payload.getJSONArray("validRecords"), HydrogenRecordImportPreviewVO.RecordPreviewItem.class); + List duplicateRecords = + JSONUtil.toList(payload.getJSONArray("duplicateRecords"), HydrogenRecordImportPreviewVO.RecordPreviewItem.class); + + // Determine records to insert based on strategy + List toInsert = new ArrayList<>(validRecords); + if ("overwrite".equals(duplicateStrategy)) { + toInsert.addAll(duplicateRecords); + } + + int total = toInsert.size(); + + // Set initial progress + HydrogenRecordImportProgressVO progress = new HydrogenRecordImportProgressVO(); + progress.setCurrent(0); + progress.setTotal(total); + progress.setStatus("processing"); + stringRedisTemplate.opsForValue().set( + REDIS_KEY_PROGRESS + batchNo, + JSONUtil.toJsonStr(progress), + PREVIEW_TTL_MINUTES, + TimeUnit.MINUTES); + + int successCount = 0; + int failCount = 0; + List recordIds = new ArrayList<>(); + + for (int i = 0; i < toInsert.size(); i++) { + HydrogenRecordImportPreviewVO.RecordPreviewItem item = toInsert.get(i); + try { + if ("overwrite".equals(duplicateStrategy) && Boolean.TRUE.equals(item.getIsDuplicate())) { + // Delete existing record(s) with same key before inserting + hydrogenRecordMapper.delete( + new LambdaQueryWrapperX() + .eq(EnergyHydrogenRecordDO::getStationId, stationId) + .eq(EnergyHydrogenRecordDO::getPlateNumber, item.getPlateNumber()) + .eq(EnergyHydrogenRecordDO::getHydrogenDate, item.getHydrogenDate())); + } + EnergyHydrogenRecordDO record = EnergyHydrogenRecordDO.builder() + .stationId(stationId) + .plateNumber(item.getPlateNumber()) + .hydrogenDate(item.getHydrogenDate()) + .hydrogenQuantity(item.getHydrogenQuantity()) + .unitPrice(item.getUnitPrice()) + .amount(item.getAmount()) + .mileage(item.getMileage()) + .sourceType(SourceTypeEnum.EXCEL.getType()) + .matchStatus(MatchStatusEnum.UNMATCHED.getStatus()) + .uploadBatchNo(batchNo) + .build(); + hydrogenRecordMapper.insert(record); + recordIds.add(record.getId()); + successCount++; + } catch (Exception e) { + log.warn("[importConfirm] Failed to import record row {}: {}", item.getRowNum(), e.getMessage()); + failCount++; + } + + // Update progress + progress.setCurrent(i + 1); + stringRedisTemplate.opsForValue().set( + REDIS_KEY_PROGRESS + batchNo, + JSONUtil.toJsonStr(progress), + PREVIEW_TTL_MINUTES, + TimeUnit.MINUTES); + } + + // Mark progress as completed + progress.setStatus(failCount == 0 ? "completed" : "failed"); + stringRedisTemplate.opsForValue().set( + REDIS_KEY_PROGRESS + batchNo, + JSONUtil.toJsonStr(progress), + PREVIEW_TTL_MINUTES, + TimeUnit.MINUTES); + + // Publish event + if (!recordIds.isEmpty()) { + eventPublisher.publishEvent(new RecordImportedEvent(stationId, recordIds, batchNo)); + } + + // Clean up preview cache + stringRedisTemplate.delete(REDIS_KEY_PREVIEW + batchNo); + + Map result = new HashMap<>(); + result.put("successCount", successCount); + result.put("failCount", failCount); + return result; + } + + @Override + public HydrogenRecordImportProgressVO getImportProgress(String batchNo) { + String json = stringRedisTemplate.opsForValue().get(REDIS_KEY_PROGRESS + batchNo); + if (json == null || json.isBlank()) { + HydrogenRecordImportProgressVO vo = new HydrogenRecordImportProgressVO(); + vo.setCurrent(0); + vo.setTotal(0); + vo.setStatus("completed"); + return vo; + } + return JSONUtil.toBean(json, HydrogenRecordImportProgressVO.class); + } + + // ==================== 私有方法 ==================== + private EnergyHydrogenRecordDO validateRecordExists(Long id) { EnergyHydrogenRecordDO record = hydrogenRecordMapper.selectById(id); if (record == null) { @@ -151,4 +390,9 @@ public class HydrogenRecordServiceImpl implements HydrogenRecordService { } return record; } + + private String generateBatchNo() { + return "IMP" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + + UUID.randomUUID().toString().substring(0, 4).toUpperCase(); + } }