package com.talentcard.server.service; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.talentcard.server.business.PageResult; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.entity.TransportParkingPlateRegistration; import com.talentcard.server.mapper.TransportParkingPlateRegistrationMapper; import com.talentcard.server.request.ParkingPlateStatusUpdateRequest; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class ParkingPlateSubmissionAdminService { private static final String STATUS_SUBMITTED = "SUBMITTED"; private static final String STATUS_ACTIVE = "ACTIVE"; private static final String STATUS_REJECTED = "REJECTED"; private static final String STATUS_CANCELLED = "CANCELLED"; private final TransportParkingPlateRegistrationMapper registrationMapper; public ParkingPlateSubmissionAdminService(TransportParkingPlateRegistrationMapper registrationMapper) { this.registrationMapper = registrationMapper; } @Transactional(readOnly = true) public PageResult> page(Long merchantId, Long parkingLotId, String keyword, String serviceKeyword, String cardLevel, String status, String submissionType, String submittedStart, String submittedEnd, int page, int size) { int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); LocalDateTime start = startTime(submittedStart, "提交开始日期格式不正确"); LocalDateTime end = endTime(submittedEnd, "提交结束日期格式不正确"); String normalizedStatus = normalizeFilterStatus(status); String normalizedSubmissionType = normalizeSubmissionType(submissionType, false); Long total = registrationMapper.countAdminPage(merchantId, parkingLotId, clean(keyword), clean(serviceKeyword), clean(cardLevel), normalizedStatus, normalizedSubmissionType, start, end); List> records = registrationMapper.selectAdminPage( merchantId, parkingLotId, clean(keyword), clean(serviceKeyword), clean(cardLevel), normalizedStatus, normalizedSubmissionType, start, end, safeSize, (safePage - 1) * safeSize) .stream() .map(this::listView) .toList(); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional(readOnly = true) public Map detail(Long id) { Map row = detailRow(id); Map result = listView(row); Map talent = new LinkedHashMap<>(); talent.put("talentId", value(row, "talentId", "talent_id")); talent.put("talentName", text(row, "talentName", "talent_name")); talent.put("talentMobile", text(row, "talentMobile", "talent_mobile")); talent.put("cardId", value(row, "cardId", "card_id")); talent.put("cardNo", text(row, "cardNo", "card_no")); talent.put("cardLevel", text(row, "cardLevel", "card_level")); result.put("talentInfo", talent); Map submission = new LinkedHashMap<>(); submission.put("plateNumber", text(row, "plateNumber", "plate_number")); submission.put("merchantId", value(row, "merchantId", "merchant_id")); submission.put("merchantName", text(row, "merchantName", "merchant_name")); submission.put("parkingLotId", value(row, "parkingLotId", "parking_lot_id")); submission.put("parkingLotName", text(row, "parkingLotName", "parking_lot_name")); submission.put("parkingLotAddress", text(row, "parkingLotAddress", "parking_lot_address")); submission.put("submissionType", text(row, "submissionType", "submission_type")); submission.put("submittedAt", value(row, "submittedAt", "submitted_at")); result.put("submissionInfo", submission); Map statusInfo = new LinkedHashMap<>(); statusInfo.put("status", text(row, "status")); statusInfo.put("statusName", statusName(text(row, "status"))); statusInfo.put("handledAt", value(row, "handledAt", "handled_at")); statusInfo.put("handledBy", value(row, "handledBy", "handled_by")); statusInfo.put("handledByName", first(text(row, "handledByName", "handled_by_name"), handlerFallback(value(row, "handledBy", "handled_by")))); statusInfo.put("handleRemark", text(row, "handleRemark", "handle_remark")); result.put("statusInfo", statusInfo); return result; } @Transactional public Map updateStatus(Long id, ParkingPlateStatusUpdateRequest request, Long operatorId) { Long registrationId = requireId(id); TransportParkingPlateRegistration current = registrationMapper.selectByIdForUpdate(registrationId); if (current == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "车牌提交记录不存在"); } String nextStatus = normalizeStatus(request.status()); String remark = clean(request.remark()); LocalDateTime handledAt = STATUS_SUBMITTED.equals(nextStatus) ? null : LocalDateTime.now(); UpdateWrapper update = new UpdateWrapper() .set("status", nextStatus) .set("handled_by", handledAt == null ? null : operatorId) .set("handled_at", handledAt) .set("handle_remark", remark.isBlank() ? null : remark) .eq("id", registrationId) .eq("deleted", 0); int updated = registrationMapper.update(null, update); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "车牌提交记录状态已变化,请刷新后重试"); } return detail(registrationId); } @Transactional(readOnly = true) public byte[] exportExcel(Long merchantId, Long parkingLotId, String keyword, String serviceKeyword, String cardLevel, String status, String submissionType, String submittedStart, String submittedEnd) { LocalDateTime start = startTime(submittedStart, "提交开始日期格式不正确"); LocalDateTime end = endTime(submittedEnd, "提交结束日期格式不正确"); List> records = registrationMapper.selectAdminPage( merchantId, parkingLotId, clean(keyword), clean(serviceKeyword), clean(cardLevel), normalizeFilterStatus(status), normalizeSubmissionType(submissionType, false), start, end, 200000, 0); String[] headers = { "序号", "提交单号", "持卡人名称", "持卡人电话", "人才等级", "人才卡号", "车牌号", "停车商家", "停车场", "提交时间", "提交类型", "状态", "处理人", "处理时间", "处理备注" }; try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { Sheet sheet = workbook.createSheet("车牌提交"); sheet.createFreezePane(0, 1); CellStyle headerStyle = headerStyle(workbook); Row headerRow = sheet.createRow(0); for (int index = 0; index < headers.length; index++) { headerRow.createCell(index).setCellValue(headers[index]); headerRow.getCell(index).setCellStyle(headerStyle); } int rowIndex = 1; for (Map record : records) { Row row = sheet.createRow(rowIndex); List values = exportValues(rowIndex, record); for (int index = 0; index < values.size(); index++) { row.createCell(index).setCellValue(values.get(index)); } rowIndex++; } int[] widths = { 8, 24, 16, 18, 16, 20, 16, 28, 28, 24, 12, 12, 16, 24, 36 }; for (int index = 0; index < widths.length; index++) { sheet.setColumnWidth(index, widths[index] * 256); } workbook.write(output); return output.toByteArray(); } catch (IOException exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "车牌提交导出失败"); } } private Map listView(Map row) { Map result = new LinkedHashMap<>(); String status = text(row, "status"); result.put("id", value(row, "id")); result.put("registrationNo", text(row, "registrationNo", "registration_no")); result.put("talentName", text(row, "talentName", "talent_name")); result.put("talentMobile", text(row, "talentMobile", "talent_mobile")); result.put("cardLevel", text(row, "cardLevel", "card_level")); result.put("cardNo", text(row, "cardNo", "card_no")); result.put("plateNumber", text(row, "plateNumber", "plate_number")); result.put("merchantId", value(row, "merchantId", "merchant_id")); result.put("merchantName", text(row, "merchantName", "merchant_name")); result.put("parkingLotId", value(row, "parkingLotId", "parking_lot_id")); result.put("parkingLotName", text(row, "parkingLotName", "parking_lot_name")); result.put("submittedAt", value(row, "submittedAt", "submitted_at")); result.put("submissionType", text(row, "submissionType", "submission_type")); result.put("status", status); result.put("statusName", statusName(status)); result.put("handledAt", value(row, "handledAt", "handled_at")); result.put("handledByName", first(text(row, "handledByName", "handled_by_name"), handlerFallback(value(row, "handledBy", "handled_by")))); return result; } private List exportValues(int index, Map row) { List values = new ArrayList<>(); values.add(String.valueOf(index)); values.add(text(row, "registrationNo", "registration_no")); values.add(text(row, "talentName", "talent_name")); values.add(text(row, "talentMobile", "talent_mobile")); values.add(text(row, "cardLevel", "card_level")); values.add(text(row, "cardNo", "card_no")); values.add(text(row, "plateNumber", "plate_number")); values.add(text(row, "merchantName", "merchant_name")); values.add(text(row, "parkingLotName", "parking_lot_name")); values.add(string(value(row, "submittedAt", "submitted_at"))); values.add(text(row, "submissionType", "submission_type")); values.add(statusName(text(row, "status"))); values.add(first(text(row, "handledByName", "handled_by_name"), handlerFallback(value(row, "handledBy", "handled_by")))); values.add(string(value(row, "handledAt", "handled_at"))); values.add(text(row, "handleRemark", "handle_remark")); return values; } private Map detailRow(Long id) { Long registrationId = requireId(id); Map row = registrationMapper.selectAdminDetail(registrationId); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "车牌提交记录不存在"); } return row; } private String normalizeFilterStatus(String raw) { String value = clean(raw); return value.isBlank() ? "" : normalizeStatus(value); } private String normalizeStatus(String raw) { String value = clean(raw).toUpperCase(Locale.ROOT).replace("-", "_"); return switch (value) { case "SUBMITTED", "PENDING", "待处理", "待审核" -> STATUS_SUBMITTED; case "ACTIVE", "PROCESSED", "PASSED", "APPROVED", "已处理", "处理完成", "通过", "已通过" -> STATUS_ACTIVE; case "REJECTED", "REFUSED", "DENIED", "已拒绝", "拒绝", "驳回", "已驳回" -> STATUS_REJECTED; case "CANCELLED", "CANCELED", "已取消", "取消" -> STATUS_CANCELLED; default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "处理状态不正确"); }; } private String normalizeSubmissionType(String raw, boolean required) { String value = clean(raw); if (value.isBlank()) { if (required) { throw new BusinessException(ErrorCode.BAD_REQUEST, "提交类型不能为空"); } return ""; } String upper = value.toUpperCase(Locale.ROOT); return switch (upper) { case "CREATE", "ADD", "NEW", "新增" -> "新增"; case "UPDATE", "MODIFY", "EDIT", "修改" -> "修改"; default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "提交类型不正确"); }; } private String statusName(String status) { return switch (clean(status).toUpperCase(Locale.ROOT)) { case STATUS_ACTIVE -> "已处理"; case STATUS_REJECTED -> "已拒绝"; case STATUS_CANCELLED -> "已取消"; default -> "待处理"; }; } private LocalDateTime startTime(String value, String message) { LocalDate date = localDate(value, message); return date == null ? null : date.atStartOfDay(); } private LocalDateTime endTime(String value, String message) { LocalDate date = localDate(value, message); return date == null ? null : date.plusDays(1).atStartOfDay(); } private LocalDate localDate(String value, String message) { String text = clean(value); if (text.isBlank()) { return null; } try { return LocalDate.parse(text); } catch (DateTimeParseException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } } private CellStyle headerStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); return style; } private Long requireId(Long id) { if (id == null || id <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, "车牌提交记录ID不能为空"); } return id; } private Object value(Map row, String... keys) { for (String key : keys) { if (row.containsKey(key)) { return row.get(key); } } return null; } private String text(Map row, String... keys) { Object value = value(row, keys); return value == null ? "" : String.valueOf(value).trim(); } private String first(String... values) { for (String value : values) { String text = clean(value); if (!text.isBlank()) { return text; } } return ""; } private String handlerFallback(Object value) { return value instanceof Number number ? "管理员" + number.longValue() : ""; } private String string(Object value) { return value == null ? "" : String.valueOf(value); } private String clean(String value) { return value == null ? "" : value.trim(); } }