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<String> 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> detail(MerchantPrincipal principal, Long id) {
|
requireParkingMerchant(principal.merchantId());
|
Map<String, Object> row = detailRow(principal, id);
|
Map<String, Object> result = listView(row);
|
|
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> create(MerchantPrincipal principal,
|
MerchantParkingPlateRegistrationRequest request,
|
String headerIdempotencyKey) {
|
Map<String, Object> 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<String, Object> existing = registrationMapper.selectByIdempotencyKey(idempotencyKey);
|
if (existing != null && !existing.isEmpty()) {
|
Long existingId = longValue(existing, "id");
|
Map<String, Object> existingDetail = registrationMapper.selectMerchantDetail(existingId, principal.merchantId());
|
if (existingDetail == null || existingDetail.isEmpty()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "提交凭证已被其他商家使用");
|
}
|
return detail(principal, existingId);
|
}
|
|
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");
|
Map<String, Object> 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<TransportParkingPlateRegistration>()
|
.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<String, Object> repeated = registrationMapper.selectByIdempotencyKey(idempotencyKey);
|
if (repeated != null && !repeated.isEmpty()) {
|
Long repeatedId = longValue(repeated, "id");
|
Map<String, Object> 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<String, Object> 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<TransportParkingPlateRegistration> update = new UpdateWrapper<TransportParkingPlateRegistration>()
|
.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<String, Object> listView(Map<String, Object> row) {
|
Map<String, Object> 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<String, Object> detailRow(MerchantPrincipal principal, Long id) {
|
Long registrationId = requirePositive(id, "车牌登记ID不能为空");
|
Map<String, Object> row = registrationMapper.selectMerchantDetail(registrationId, principal.merchantId());
|
if (row == null || row.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "车牌登记不存在");
|
}
|
return row;
|
}
|
|
private Map<String, Object> requireParkingMerchant(Long merchantId) {
|
Map<String, Object> merchant = registrationMapper.selectParkingMerchant(merchantId);
|
if (merchant == null || merchant.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "停车商家不存在");
|
}
|
return merchant;
|
}
|
|
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 TransportParkingLot requireParkingLot(Long merchantId, Long parkingLotId) {
|
List<TransportParkingLot> lots = lotMapper.selectList(new QueryWrapper<TransportParkingLot>()
|
.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<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 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<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 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();
|
}
|
}
|