package com.talentcard.server.service; import java.time.LocalDateTime; 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.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.ParkingPlateSubmitRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class ParkingPlateSubmissionService { private static final String STATUS_SUBMITTED = "SUBMITTED"; private static final String STATUS_ACTIVE = "ACTIVE"; 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 ParkingPlateSubmissionService( TransportParkingPlateRegistrationMapper registrationMapper, TransportParkingCardLevelRightMapper rightMapper, TransportParkingLotMapper lotMapper ) { this.registrationMapper = registrationMapper; this.rightMapper = rightMapper; this.lotMapper = lotMapper; } @Transactional(readOnly = true) public Map options(Long talentId, Long cardId) { Map card = activeCard(talentId, cardId); String cardLevel = text(card, "cardLevel", "card_level"); List> rights = rightMapper.selectAvailableForCard(cardLevel, null); Map result = new LinkedHashMap<>(); result.put("talentId", value(card, "talentId", "talent_id")); result.put("talentName", text(card, "talentName", "talent_name")); result.put("talentMobile", text(card, "talentMobile", "talent_mobile")); result.put("cardId", value(card, "cardId", "card_id")); result.put("cardNo", text(card, "cardNo", "card_no")); result.put("cardLevel", cardLevel); result.put("merchants", rights.stream().map(this::merchantOptionView).toList()); return result; } @Transactional public Map submit(ParkingPlateSubmitRequest request, String headerIdempotencyKey) { String idempotencyKey = clean(first(headerIdempotencyKey, request.idempotencyKey())); if (!idempotencyKey.isBlank()) { Map existing = registrationMapper.selectByIdempotencyKey(idempotencyKey); if (existing != null && !existing.isEmpty()) { return submittedView(value(existing, "id"), text(existing, "registrationNo", "registration_no"), text(existing, "status")); } } else { idempotencyKey = BusinessIdGenerator.next("PPI"); } 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"); Long merchantId = requirePositive(request.merchantId(), "停车商家ID不能为空"); Map merchant = registrationMapper.selectParkingMerchant(merchantId); if (merchant == null || merchant.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "停车商家不存在"); } if (intValue(merchant, "merchantStatus", "merchant_status") != 1) { throw new BusinessException(ErrorCode.CONFLICT, "停车商家当前不可用"); } Map right = rightMapper.selectAvailableForCard(cardLevel, 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(merchantId, request.parkingLotId()); long occupiedCount = occupiedCount(talentId, cardId, merchantId, lot.getId(), selectionMode, null); if (occupiedCount > 0) { throw new BusinessException(ErrorCode.CONFLICT, "该车牌已在当前停车商家提交,请勿重复提交"); } LocalDateTime now = LocalDateTime.now(); String registrationNo = BusinessIdGenerator.next("PPR"); TransportParkingPlateRegistration registration = new TransportParkingPlateRegistration(); registration.setRegistrationNo(registrationNo); registration.setMerchantId(merchantId); registration.setBenefitId(benefitId); registration.setParkingLotId(lot.getId()); registration.setParkingLotNameSnapshot(lot.getParkingLotName()); registration.setParkingLotAddressSnapshot(lot.getAddress()); registration.setTalentId(talentId); registration.setCardId(cardId); registration.setCardNo(text(card, "cardNo", "card_no")); registration.setCardLevel(cardLevel); registration.setPlateNumber(request.plateNumber()); registration.setSubmittedAt(now); registration.setStatus(STATUS_SUBMITTED); registration.setIdempotencyKey(idempotencyKey); registration.setDeleted(0); registrationMapper.insert(registration); return submittedView(registration.getId(), registrationNo, STATUS_SUBMITTED); } private Map merchantOptionView(Map right) { Long merchantId = longValue(right, "merchantId", "merchant_id"); Map result = new LinkedHashMap<>(); result.put("merchantId", merchantId); result.put("merchantName", text(right, "merchantName", "merchant_name")); result.put("contactMobile", text(right, "contactMobile", "contact_mobile")); result.put("address", text(right, "address", "usageInstructions", "usage_instructions")); result.put("usageInstructions", text(right, "usageInstructions", "usage_instructions")); result.put("merchantRightsConfig", text(right, "merchantRightsConfig", "merchant_rights_config")); result.put("coverUrl", text(right, "coverUrl", "cover_url")); result.put("rightId", value(right, "rightId", "right_id")); result.put("label", text(right, "label")); result.put("benefitDetail", text(right, "merchantRightsConfig", "merchant_rights_config")); result.put("chargeType", "FREE"); result.put("selectionMode", "ALL"); result.put("selectionModeName", "停车场全部可选"); result.put("storeConsumeEnabled", false); result.put("discountText", ""); result.put("governmentSubsidy", false); result.put("consumeUsageText", ""); result.put("parkingLots", lots(merchantId).stream().map(this::lotView).toList()); return result; } private TransportParkingLot requireParkingLot(Long merchantId, Long parkingLotId) { List lots = lots(merchantId); 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 List lots(Long merchantId) { return lotMapper.selectList(new QueryWrapper() .eq("merchant_id", merchantId) .eq("deleted", 0) .eq("enabled", 1) .orderByAsc("sort_no", "id")); } private Map lotView(TransportParkingLot lot) { Map result = new LinkedHashMap<>(); result.put("parkingLotId", lot.getId()); result.put("parkingLotName", lot.getParkingLotName()); result.put("address", lot.getAddress()); result.put("latitude", lot.getLatitude()); result.put("longitude", lot.getLongitude()); result.put("sortNo", lot.getSortNo()); return result; } 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 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 Map submittedView(Object id, String registrationNo, String status) { Map result = new LinkedHashMap<>(); result.put("registrationId", id); result.put("registrationNo", registrationNo); result.put("status", status); result.put("statusName", statusName(status)); return result; } private String normalizeChargeType(String value) { String text = clean(value).toUpperCase(Locale.ROOT); return "PAID".equals(text) ? "PAID" : "FREE"; } private String normalizeSelectionMode(String value) { String text = clean(value).toUpperCase(Locale.ROOT); return "ONE".equals(text) ? "ONE" : "ALL"; } private String selectionModeName(String selectionMode) { return "ONE".equals(selectionMode) ? "停车场仅选择其一" : "停车场全部可选"; } private String statusName(String status) { if (STATUS_SUBMITTED.equals(status)) { return "待提交"; } if (STATUS_ACTIVE.equals(status)) { return "已生效"; } if (STATUS_CANCELLED.equals(status)) { return "已取消"; } return status; } private Long requirePositive(Long value, String message) { if (value == null || value <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } return value; } private Long longValue(Map row, String... keys) { Object value = value(row, keys); return value instanceof Number number ? number.longValue() : null; } 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 String clean(String value) { return value == null ? "" : value.trim(); } private String first(String first, String second) { return clean(first).isBlank() ? clean(second) : clean(first); } }