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<String> 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<String, Object> options(Long talentId, Long cardId) {
|
Map<String, Object> card = activeCard(talentId, cardId);
|
String cardLevel = text(card, "cardLevel", "card_level");
|
List<Map<String, Object>> rights = rightMapper.selectAvailableForCard(cardLevel, null);
|
Map<String, Object> 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<String, Object> submit(ParkingPlateSubmitRequest request, String headerIdempotencyKey) {
|
String idempotencyKey = clean(first(headerIdempotencyKey, request.idempotencyKey()));
|
if (!idempotencyKey.isBlank()) {
|
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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());
|
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) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "原车牌提交记录不存在");
|
}
|
if (!Objects.equals(original.getTalentId(), talentId) || !Objects.equals(original.getMerchantId(), merchantId)) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "原车牌提交记录与当前持卡人或停车商家不匹配");
|
}
|
if (!OCCUPIED_STATUSES.contains(original.getStatus())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "原车牌提交记录当前状态不能修改");
|
}
|
submissionType = "修改";
|
}
|
|
long occupied = occupiedCount(talentId, cardId, 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<TransportParkingPlateRegistration>()
|
.set("status", STATUS_CANCELLED)
|
.set("handled_at", now)
|
.set("handle_remark", "用户修改车牌重新提交")
|
.eq("id", original.getId())
|
.eq("deleted", 0));
|
}
|
|
String registrationNo = BusinessIdGenerator.next("PR");
|
TransportParkingPlateRegistration registration = new TransportParkingPlateRegistration();
|
registration.setRegistrationNo(registrationNo);
|
registration.setMerchantId(merchantId);
|
registration.setBenefitId(benefitId);
|
registration.setParkingLotId(lot.getId());
|
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_SUBMITTED);
|
registration.setIdempotencyKey(idempotencyKey);
|
registration.setDeleted(0);
|
registrationMapper.insert(registration);
|
return submittedView(registration.getId(), registrationNo, STATUS_SUBMITTED);
|
}
|
|
private Map<String, Object> merchantOptionView(Map<String, Object> right) {
|
Long merchantId = longValue(right, "merchantId", "merchant_id");
|
Map<String, Object> 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"));
|
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, "benefitDetail", "benefit_detail"));
|
result.put("chargeType", normalizeChargeType(text(right, "chargeType", "charge_type")));
|
String selectionMode = normalizeSelectionMode(text(right, "selectionMode", "selection_mode"));
|
result.put("selectionMode", selectionMode);
|
result.put("selectionModeName", selectionModeName(selectionMode));
|
result.put("storeConsumeEnabled", intValue(right, "storeConsumeEnabled", "store_consume_enabled") == 1);
|
result.put("discountText", text(right, "discountText", "discount_text"));
|
result.put("governmentSubsidy", intValue(right, "governmentSubsidy", "government_subsidy") == 1);
|
result.put("consumeUsageText", text(right, "consumeUsageText", "consume_usage_text"));
|
result.put("parkingLots", lots(merchantId).stream().map(this::lotView).toList());
|
return result;
|
}
|
|
private TransportParkingLot requireParkingLot(Long merchantId, Long parkingLotId) {
|
List<TransportParkingLot> 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<TransportParkingLot> lots(Long merchantId) {
|
return lotMapper.selectList(new QueryWrapper<TransportParkingLot>()
|
.eq("merchant_id", merchantId)
|
.eq("deleted", 0)
|
.eq("enabled", 1)
|
.orderByAsc("sort_no", "id"));
|
}
|
|
private Map<String, Object> lotView(TransportParkingLot lot) {
|
Map<String, Object> 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<TransportParkingPlateRegistration> wrapper = new QueryWrapper<TransportParkingPlateRegistration>()
|
.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<String, Object> activeCard(Long talentId, Long cardId) {
|
if ((talentId == null || talentId <= 0) && (cardId == null || cardId <= 0)) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "持卡人ID或人才卡ID不能为空");
|
}
|
Map<String, Object> 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<String, Object> submittedView(Object id, String registrationNo, String status) {
|
Map<String, Object> 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) || "SELECT_ONE".equals(text) ? "ONE" : "ALL";
|
}
|
|
private String selectionModeName(String value) {
|
return "ONE".equals(value) ? "停车场仅选择其一" : "停车场全部可选";
|
}
|
|
private String statusName(String status) {
|
return switch (clean(status).toUpperCase(Locale.ROOT)) {
|
case STATUS_ACTIVE -> "已处理";
|
case "REJECTED" -> "已拒绝";
|
case STATUS_CANCELLED -> "已取消";
|
default -> "待处理";
|
};
|
}
|
|
private Long requirePositive(Long value, String message) {
|
if (value == null || value <= 0) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, message);
|
}
|
return value;
|
}
|
|
private String first(String... values) {
|
for (String value : values) {
|
String text = clean(value);
|
if (!text.isBlank()) {
|
return text;
|
}
|
}
|
return "";
|
}
|
|
private Object value(Map<String, Object> row, String... keys) {
|
for (String key : keys) {
|
if (row.containsKey(key)) {
|
return row.get(key);
|
}
|
}
|
return null;
|
}
|
|
private String text(Map<String, Object> row, String... keys) {
|
Object value = value(row, keys);
|
return value == null ? "" : String.valueOf(value).trim();
|
}
|
|
private int intValue(Map<String, Object> row, String... keys) {
|
Object value = value(row, keys);
|
return value instanceof Number number ? number.intValue() : 0;
|
}
|
|
private Long longValue(Map<String, Object> row, String... keys) {
|
Object value = value(row, keys);
|
return value instanceof Number number ? number.longValue() : null;
|
}
|
|
private String clean(String value) {
|
return value == null ? "" : value.trim();
|
}
|
}
|