package com.talentcard.server.service; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.entity.TransportParkingLot; import com.talentcard.server.entity.TransportParkingPlateRegistration; import com.talentcard.server.mapper.TransportParkingCardLevelRightMapper; import com.talentcard.server.mapper.TransportParkingLotMapper; import com.talentcard.server.mapper.TransportParkingPlateRegistrationMapper; import com.talentcard.server.request.MerchantParkingPlateRegistrationRequest; import com.talentcard.server.request.ParkingPlateStatusUpdateRequest; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class ParkingPlateMerchantService { 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 static final List OCCUPIED_STATUSES = List.of(STATUS_SUBMITTED, STATUS_ACTIVE); private final TransportParkingPlateRegistrationMapper registrationMapper; private final TransportParkingCardLevelRightMapper rightMapper; private final TransportParkingLotMapper lotMapper; public ParkingPlateMerchantService( TransportParkingPlateRegistrationMapper registrationMapper, TransportParkingCardLevelRightMapper rightMapper, TransportParkingLotMapper lotMapper ) { this.registrationMapper = registrationMapper; this.rightMapper = rightMapper; this.lotMapper = lotMapper; } @Transactional(readOnly = true) public PageResult> page(MerchantPrincipal principal, Long parkingLotId, String keyword, String cardLevel, String status, String submissionType, String submittedStart, String submittedEnd, int page, int size) { requireParkingMerchant(principal.merchantId()); 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.countMerchantPage(principal.merchantId(), parkingLotId, clean(keyword), clean(cardLevel), normalizedStatus, normalizedSubmissionType, start, end); List> records = registrationMapper.selectMerchantPage(principal.merchantId(), parkingLotId, clean(keyword), 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(MerchantPrincipal principal, Long id) { requireParkingMerchant(principal.merchantId()); Map row = detailRow(principal, 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 create(MerchantPrincipal principal, MerchantParkingPlateRegistrationRequest request, String headerIdempotencyKey) { Map merchant = requireParkingMerchant(principal.merchantId()); if (intValue(merchant, "merchantStatus", "merchant_status") != 1) { throw new BusinessException(ErrorCode.CONFLICT, "停车商家当前不可用"); } String idempotencyKey = clean(first(headerIdempotencyKey, request.idempotencyKey())); if (idempotencyKey.isBlank()) { idempotencyKey = BusinessIdGenerator.next("PPI"); } Map existing = registrationMapper.selectByIdempotencyKey(idempotencyKey); if (existing != null && !existing.isEmpty()) { Long existingId = longValue(existing, "id"); Map existingDetail = registrationMapper.selectMerchantDetail(existingId, principal.merchantId()); if (existingDetail == null || existingDetail.isEmpty()) { throw new BusinessException(ErrorCode.CONFLICT, "提交凭证已被其他商家使用"); } return detail(principal, existingId); } Map card = activeCard(request.talentId(), request.cardId()); Long talentId = longValue(card, "talentId", "talent_id"); Long cardId = longValue(card, "cardId", "card_id"); String cardLevel = text(card, "cardLevel", "card_level"); Map right = rightMapper.selectAvailableForCard(cardLevel, principal.merchantId()).stream().findFirst() .orElseThrow(() -> new BusinessException(ErrorCode.CONFLICT, "当前人才卡不适用该停车商家")); Long benefitId = longValue(right, "benefitId", "benefit_id"); if (benefitId == null || benefitId <= 0) { throw new BusinessException(ErrorCode.NOT_FOUND, "停车权益配置不存在"); } String selectionMode = normalizeSelectionMode(text(right, "selectionMode", "selection_mode")); TransportParkingLot lot = requireParkingLot(principal.merchantId(), request.parkingLotId()); String plateNumber = request.plateNumber().trim().toUpperCase(Locale.ROOT); TransportParkingPlateRegistration original = null; String submissionType = "新增"; if (request.registrationId() != null) { original = registrationMapper.selectByIdForUpdate(requirePositive(request.registrationId(), "原车牌登记ID不能为空")); if (original == null || !Objects.equals(original.getMerchantId(), principal.merchantId())) { throw new BusinessException(ErrorCode.NOT_FOUND, "原车牌登记不存在"); } if (!Objects.equals(original.getTalentId(), talentId)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "原车牌登记与当前持卡人不匹配"); } if (!OCCUPIED_STATUSES.contains(original.getStatus())) { throw new BusinessException(ErrorCode.CONFLICT, "原车牌登记当前状态不能修改"); } submissionType = "修改"; } long occupied = occupiedCount(talentId, cardId, principal.merchantId(), lot.getId(), selectionMode, original == null ? null : original.getId()); if (occupied > 0) { if ("ALL".equals(selectionMode)) { throw new BusinessException(ErrorCode.CONFLICT, "该停车场已登记车牌,如需修改请传入原记录ID"); } throw new BusinessException(ErrorCode.CONFLICT, "该停车商家仅能选择一个停车场录入车牌,如需修改请传入原记录ID"); } LocalDateTime now = LocalDateTime.now(); if (original != null) { registrationMapper.update(null, new UpdateWrapper() .set("status", STATUS_CANCELLED) .set("handled_by", principal.operatorId()) .set("handled_at", now) .set("handle_remark", "商户端修改车牌重新录入") .eq("id", original.getId()) .eq("merchant_id", principal.merchantId()) .eq("deleted", 0)); } TransportParkingPlateRegistration registration = new TransportParkingPlateRegistration(); registration.setRegistrationNo(BusinessIdGenerator.next("PR")); registration.setMerchantId(principal.merchantId()); registration.setBenefitId(benefitId); registration.setParkingLotId(lot.getId()); registration.setParkingLotNameSnapshot(lot.getParkingLotName()); registration.setParkingLotAddressSnapshot(lot.getAddress()); registration.setTalentId(talentId); registration.setCardId(cardId); registration.setTalentName(text(card, "talentName", "talent_name")); registration.setTalentMobile(text(card, "talentMobile", "talent_mobile")); registration.setCardNo(text(card, "cardNo", "card_no")); registration.setCardLevel(cardLevel); registration.setPlateNumber(plateNumber); registration.setSubmissionType(submissionType); registration.setSubmittedAt(now); registration.setStatus(STATUS_ACTIVE); registration.setIdempotencyKey(idempotencyKey); registration.setHandledBy(principal.operatorId()); registration.setHandledAt(now); registration.setHandleRemark(first(request.remark(), "商户端录入并处理")); registration.setDeleted(0); try { registrationMapper.insert(registration); } catch (DuplicateKeyException exception) { Map repeated = registrationMapper.selectByIdempotencyKey(idempotencyKey); if (repeated != null && !repeated.isEmpty()) { Long repeatedId = longValue(repeated, "id"); Map repeatedDetail = registrationMapper.selectMerchantDetail(repeatedId, principal.merchantId()); if (repeatedDetail != null && !repeatedDetail.isEmpty()) { return detail(principal, repeatedId); } } throw new BusinessException(ErrorCode.CONFLICT, "车牌登记已提交,请稍后查询结果"); } return detail(principal, registration.getId()); } @Transactional public Map updateStatus(MerchantPrincipal principal, Long id, ParkingPlateStatusUpdateRequest request) { requireParkingMerchant(principal.merchantId()); Long registrationId = requirePositive(id, "车牌登记ID不能为空"); TransportParkingPlateRegistration current = registrationMapper.selectByIdForUpdate(registrationId); if (current == null || !Objects.equals(current.getMerchantId(), principal.merchantId())) { 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 : principal.operatorId()) .set("handled_at", handledAt) .set("handle_remark", remark.isBlank() ? null : remark) .eq("id", registrationId) .eq("merchant_id", principal.merchantId()) .eq("deleted", 0); int updated = registrationMapper.update(null, update); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "车牌登记状态已变化,请刷新后重试"); } return detail(principal, registrationId); } 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 Map detailRow(MerchantPrincipal principal, Long id) { Long registrationId = requirePositive(id, "车牌登记ID不能为空"); Map row = registrationMapper.selectMerchantDetail(registrationId, principal.merchantId()); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "车牌登记不存在"); } return row; } private Map requireParkingMerchant(Long merchantId) { Map merchant = registrationMapper.selectParkingMerchant(merchantId); if (merchant == null || merchant.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "停车商家不存在"); } return merchant; } private Map activeCard(Long talentId, Long cardId) { if ((talentId == null || talentId <= 0) && (cardId == null || cardId <= 0)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "持卡人ID或人才卡ID不能为空"); } Map card = registrationMapper.selectActiveCardSnapshot(talentId, cardId); if (card == null || card.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "有效人才卡不存在"); } String cardLevel = text(card, "cardLevel", "card_level"); if (cardLevel.isBlank() || "普通游客".equals(cardLevel)) { throw new BusinessException(ErrorCode.CONFLICT, "当前用户无停车权益"); } return card; } private TransportParkingLot requireParkingLot(Long merchantId, Long parkingLotId) { List lots = lotMapper.selectList(new QueryWrapper() .eq("merchant_id", merchantId) .eq("deleted", 0) .eq("enabled", 1) .orderByAsc("sort_no", "id")); if (lots.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "停车场配置不存在"); } if (parkingLotId == null) { if (lots.size() == 1) { return lots.get(0); } throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择停车场"); } return lots.stream() .filter(lot -> Objects.equals(lot.getId(), parkingLotId)) .findFirst() .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用")); } private long occupiedCount(Long talentId, Long cardId, Long merchantId, Long parkingLotId, String selectionMode, Long excludeId) { QueryWrapper wrapper = new QueryWrapper() .eq("deleted", 0) .eq("talent_id", talentId) .eq("merchant_id", merchantId) .in("status", OCCUPIED_STATUSES); if (cardId != null) { wrapper.and(query -> query.eq("card_id", cardId).or().isNull("card_id")); } if ("ALL".equals(selectionMode)) { wrapper.eq("parking_lot_id", parkingLotId); } if (excludeId != null) { wrapper.ne("id", excludeId); } Long count = registrationMapper.selectCount(wrapper); return count == null ? 0 : count; } private String normalizeSelectionMode(String value) { String text = clean(value).toUpperCase(Locale.ROOT); return "ONE".equals(text) || "SELECT_ONE".equals(text) ? "ONE" : "ALL"; } 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 Long requirePositive(Long value, String message) { if (value == null || value <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } return value; } 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 int intValue(Map row, String... keys) { Object value = value(row, keys); return value instanceof Number number ? number.intValue() : 0; } private Long longValue(Map row, String... keys) { Object value = value(row, keys); return value instanceof Number number ? number.longValue() : null; } 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 clean(String value) { return value == null ? "" : value.trim(); } }