feat(energy): 添加三步导入接口(预览/确认/进度)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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, "加氢明细不存在");
|
||||
|
||||
@@ -102,6 +102,33 @@ public class HydrogenRecordController {
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/import-preview")
|
||||
@Operation(summary = "导入预览")
|
||||
@PreAuthorize("@ss.hasPermission('energy:hydrogen-record:import')")
|
||||
public CommonResult<HydrogenRecordImportPreviewVO> importPreview(
|
||||
@RequestParam("stationId") Long stationId,
|
||||
@RequestParam("file") MultipartFile file) throws Exception {
|
||||
List<HydrogenRecordImportVO> 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<Map<String, Integer>> 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<HydrogenRecordImportProgressVO> getImportProgress(
|
||||
@RequestParam("batchNo") String batchNo) {
|
||||
return success(hydrogenRecordService.getImportProgress(batchNo));
|
||||
}
|
||||
|
||||
@PutMapping("/match")
|
||||
@Operation(summary = "手动匹配车辆")
|
||||
@PreAuthorize("@ss.hasPermission('energy:hydrogen-record:update')")
|
||||
|
||||
@@ -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<RecordPreviewItem> records;
|
||||
@Schema(description = "重复记录")
|
||||
private List<RecordPreviewItem> duplicates;
|
||||
@Schema(description = "错误明细")
|
||||
private List<ImportErrorItem> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String, Integer> importRecords(Long stationId, List<HydrogenRecordImportVO> list, String batchNo);
|
||||
void matchRecord(Long id, Long vehicleId, Long customerId);
|
||||
Map<String, Integer> batchMatch();
|
||||
HydrogenRecordImportPreviewVO importPreview(Long stationId, List<HydrogenRecordImportVO> list);
|
||||
Map<String, Integer> importConfirm(String batchNo, String duplicateStrategy);
|
||||
HydrogenRecordImportProgressVO getImportProgress(String batchNo);
|
||||
}
|
||||
|
||||
@@ -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<HydrogenRecordImportVO> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw exception(HYDROGEN_RECORD_IMPORT_EMPTY);
|
||||
}
|
||||
|
||||
String batchNo = generateBatchNo();
|
||||
|
||||
List<HydrogenRecordImportPreviewVO.RecordPreviewItem> validRecords = new ArrayList<>();
|
||||
List<HydrogenRecordImportPreviewVO.RecordPreviewItem> duplicateRecords = new ArrayList<>();
|
||||
List<HydrogenRecordImportPreviewVO.ImportErrorItem> 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<String> 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<EnergyHydrogenRecordDO>()
|
||||
.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<String, Object> 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<String, Integer> 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<HydrogenRecordImportPreviewVO.RecordPreviewItem> validRecords =
|
||||
JSONUtil.toList(payload.getJSONArray("validRecords"), HydrogenRecordImportPreviewVO.RecordPreviewItem.class);
|
||||
List<HydrogenRecordImportPreviewVO.RecordPreviewItem> duplicateRecords =
|
||||
JSONUtil.toList(payload.getJSONArray("duplicateRecords"), HydrogenRecordImportPreviewVO.RecordPreviewItem.class);
|
||||
|
||||
// Determine records to insert based on strategy
|
||||
List<HydrogenRecordImportPreviewVO.RecordPreviewItem> 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<Long> 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<EnergyHydrogenRecordDO>()
|
||||
.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<String, Integer> 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user