yeg
2026-09-18 70ea0911ce7519ca802266078624e9fed3559a2c
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java
@@ -1,5 +1,7 @@
package com.talentcard.server.business.portal;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -30,11 +32,13 @@
      Map<String, Object> benefit = parkingBenefit(merchantId);
      String cardLevel = talentCardId == null ? "" : talentCardLevel(talentCardId);
      List<Map<String, Object>> rights = parseList(merchant.get("merchantRightsConfig"));
      Map<String, Object> right = matchingRight(rights, cardLevel);
      boolean hasCardRightsConfig = !rights.isEmpty() && !cardLevel.isBlank();
      Map<String, Object> legacyRight = matchingRight(rights, cardLevel);
      Map<String, Object> configuredRight = parkingCardLevelRight(merchantId, cardLevel);
      Map<String, Object> right = mergeRight(legacyRight, configuredRight);
      boolean hasCardRightsConfig = (!rights.isEmpty() || configuredRight != null) && !cardLevel.isBlank();
      boolean available = number(merchant.get("status")) == 1
            && (!hasCardRightsConfig || right != null)
            && (right == null || booleanValue(right.get("enabled")));
            && (right == null || (booleanValue(right.get("enabled")) && rightStatusEnabled(right)));
      Map<String, Object> result = new LinkedHashMap<>();
      result.put("merchantId", merchant.get("id"));
@@ -47,18 +51,29 @@
      result.put("longitude", merchant.get("longitude"));
      result.put("available", available);
      result.put("unavailableReason", available ? "" : unavailableReason(merchant, right));
      result.put("serviceLabel", text(merchant.get("serviceLabel"), "停车服务"));
      // 游客没有人才卡等级,不具备可展示的权益标签,避免把默认权益当成该用户的标签。
      boolean hasCardLevel = !cardLevel.isBlank();
      List<String> benefitTags = hasCardLevel ? parkingBenefitTags(merchantId, right) : List.of();
      String displayTag = benefitTags.isEmpty() ? "" : benefitTags.get(0);
      result.put("serviceLabel", displayTag);
      result.put("discountText", displayTag);
      result.put("benefitTags", benefitTags);
      result.put("benefitText", benefitText(merchant, right, benefit));
      result.put("applicableCards", applicableCards(right, benefit));
      result.put("usageInstructions", text(merchant.get("usageInstructions"), "请咨询商户"));
      result.put("applicableCards", hasCardLevel ? applicableCards(right, benefit) : "");
      result.put("usageInstructions", usageInstructions(merchant, right));
      result.put("rightsText", rightsText(merchant, right));
      result.put("actionMode", text(benefit.get("actionMode"), "plate"));
      result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple"));
      result.put("benefit", benefitView(benefit));
      String parkingType = text(right == null ? null : right.get("parkingType"), "paid");
      // 付费权益到店出示人才码,免费权益先登记车牌。
      result.put("actionMode", "paid".equalsIgnoreCase(parkingType) ? "talentCode" : "plate");
      // 车牌表单模式以商家权益里配的「停车场选择范围」为准:
      // 全部可选(all)只要填车牌;只选其一(selected)才需要挑停车场。
      String plateFormMode = effectivePlateFormMode(right, benefit);
      result.put("plateFormMode", plateFormMode);
      result.put("benefit", benefitView(benefit, plateFormMode));
      result.put("applicableStores", parkingStores(merchant.get("usageInstructions")));
      result.put("merchantRightsConfig", rights);
      result.put("merchantRight", right == null ? Map.of() : right);
      result.put("parkingType", text(right == null ? null : right.get("parkingType"), "paid"));
      result.put("parkingType", parkingType);
      result.put("parkingScope", text(right == null ? null : right.get("parkingScope"), "all"));
      result.put("consumeDesc", text(right == null ? null : right.get("consumeDesc"), ""));
      return result;
@@ -103,15 +118,25 @@
            where id = ? and merchant_id = ? and deleted = 0
            """, request.benefitId(), merchantId);
      if (number(benefit.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "停车权益当前不可用");
      if (!"plate".equals(benefit.get("actionMode"))) throw new BusinessException(ErrorCode.CONFLICT, "当前停车权益无需登记车牌");
      if ("withParking".equals(benefit.get("plateFormMode"))) {
         if (request.parkingLotId() == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择停车场");
         boolean matchedLot = parkingStores(merchant.get("usageInstructions")).stream()
               .anyMatch(item -> String.valueOf(request.parkingLotId()).equals(String.valueOf(item.get("id"))));
         if (!matchedLot) throw new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用");
      Map<String, Object> card = activeTalentCard(request.talentId());
      String cardLevel = text(card.get("cardLevel"), "");
      Map<String, Object> right = mergeRight(
            matchingRight(parseList(merchant.get("merchantRightsConfig")), cardLevel),
            parkingCardLevelRight(merchantId, cardLevel));
      if (right == null || !booleanValue(right.get("enabled")) || !rightStatusEnabled(right)) {
         throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡暂无可用停车权益");
      }
      Integer talentCount = jdbc.queryForObject("select count(*) from talent_user where id = ? and deleted = 0", Integer.class, request.talentId());
      if (talentCount == null || talentCount == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "人才用户不存在");
      if (!"free".equalsIgnoreCase(text(right.get("parkingType"), "paid"))) {
         throw new BusinessException(ErrorCode.CONFLICT, "付费停车权益无需登记车牌");
      }
      Map<String, Object> selectedParkingLot = null;
      if (requiresParkingLotSelection(right, benefit)) {
         if (request.parkingLotId() == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择停车场");
         selectedParkingLot = parkingStores(merchant.get("usageInstructions")).stream()
               .filter(item -> String.valueOf(request.parkingLotId()).equals(String.valueOf(item.get("id"))))
               .findFirst()
               .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用"));
      }
      Integer duplicateCount = jdbc.queryForObject("""
            select count(*) from transport_parking_plate_registration
            where merchant_id = ? and talent_id = ? and plate_number = ? and deleted = 0
@@ -121,9 +146,16 @@
      String registrationNo = BusinessIdGenerator.next("PR");
      jdbc.update("""
            insert into transport_parking_plate_registration(registration_no, merchant_id, benefit_id, parking_lot_id, talent_id, plate_number, status, idempotency_key)
            values (?, ?, ?, ?, ?, ?, 'SUBMITTED', ?)
            """, registrationNo, merchantId, request.benefitId(), request.parkingLotId(), request.talentId(), request.plateNumber(), idempotencyKey);
            insert into transport_parking_plate_registration(
                registration_no, merchant_id, benefit_id, parking_lot_id,
                parking_lot_name_snapshot, parking_lot_address_snapshot,
                talent_id, plate_number, status, idempotency_key
            )
            values (?, ?, ?, ?, ?, ?, ?, ?, 'SUBMITTED', ?)
            """, registrationNo, merchantId, request.benefitId(), request.parkingLotId(),
            selectedParkingLot == null ? null : parkingLotName(selectedParkingLot),
            selectedParkingLot == null ? null : text(selectedParkingLot.get("address"), ""),
            request.talentId(), request.plateNumber(), idempotencyKey);
      return registrationView(one("""
            select id, registration_no, status
            from transport_parking_plate_registration
@@ -136,7 +168,7 @@
            select id, merchant_name, contact_mobile, address, cover_url, service_label, usage_instructions,
                   rights_text, merchant_rights_config, status, latitude, longitude
            from merchant
            where id = ? and merchant_type in ('parking', '停车服务') and deleted = 0
            where id = ? and merchant_type_code = 'parking' and deleted = 0
            """, merchantId);
   }
@@ -148,6 +180,61 @@
            order by id asc limit 1
            """, merchantId);
      return rows.isEmpty() ? new LinkedHashMap<>() : rows.get(0);
   }
   private Map<String, Object> parkingCardLevelRight(Long merchantId, String cardLevel) {
      if (cardLevel == null || cardLevel.isBlank()) return null;
      List<Map<String, Object>> rows = query("""
            select card_level as level, enabled, label as tag, benefit_detail as detail,
                   charge_type as chargeType, selection_mode as selectionMode,
                   store_consume_enabled as storeConsumeEnabled, discount_text as discountText,
                   government_subsidy as governmentSubsidy, consume_usage_text as consumeDesc,
                   status as rightStatus
            from transport_parking_card_level_right
            where merchant_id = ? and card_level = ? and deleted = 0
            limit 1
            """, merchantId, cardLevel);
      if (rows.isEmpty()) return null;
      Map<String, Object> right = rows.get(0);
      String chargeType = text(right.get("chargeType"), "FREE");
      right.put("parkingType", "PAID".equalsIgnoreCase(chargeType) ? "paid" : "free");
      String selectionMode = text(right.get("selectionMode"), "ALL");
      right.put("parkingScope", "ONE".equalsIgnoreCase(selectionMode) ? "selected" : "all");
      // 付费权益的到店消费使用描述就是详情页的使用说明。
      if ("paid".equals(right.get("parkingType")) && text(right.get("consumeDesc"), "").isBlank()) {
         right.put("consumeDesc", text(right.get("detail"), ""));
      }
      return right;
   }
   private Map<String, Object> mergeRight(Map<String, Object> legacyRight, Map<String, Object> configuredRight) {
      if (legacyRight == null && configuredRight == null) return null;
      Map<String, Object> result = new LinkedHashMap<>();
      if (legacyRight != null) result.putAll(legacyRight);
      if (configuredRight != null) {
         for (Map.Entry<String, Object> entry : configuredRight.entrySet()) {
            Object value = entry.getValue();
            if (value != null && (!(value instanceof String text) || !text.isBlank())) result.put(entry.getKey(), value);
         }
      }
      String parkingType = text(result.get("parkingType"), "free");
      if ("paid".equalsIgnoreCase(parkingType) && !text(result.get("consumeDesc"), "").isBlank()) {
         result.put("detail", text(result.get("consumeDesc"), ""));
      }
      return result;
   }
   private boolean rightStatusEnabled(Map<String, Object> right) {
      Object status = right.get("rightStatus");
      return status == null || number(status) != 0;
   }
   private String usageInstructions(Map<String, Object> merchant, Map<String, Object> right) {
      if (right != null && "paid".equalsIgnoreCase(text(right.get("parkingType"), ""))) {
         String consumeDesc = text(right.get("consumeDesc"), text(right.get("detail"), ""));
         if (!consumeDesc.isBlank()) return consumeDesc;
      }
      return text(merchant.get("usageInstructions"), "请咨询商户");
   }
   private List<Map<String, Object>> parkingStores(Object usageInstructions) {
@@ -165,13 +252,37 @@
      return stores;
   }
   private Map<String, Object> benefitView(Map<String, Object> benefit) {
   private String parkingLotName(Map<String, Object> parkingLot) {
      return text(parkingLot.get("parkingLotName"),
            text(parkingLot.get("storeName"), text(parkingLot.get("name"), "")));
   }
   private Map<String, Object> benefitView(Map<String, Object> benefit, String plateFormMode) {
      Map<String, Object> result = new LinkedHashMap<>();
      result.put("benefitId", benefit.get("id"));
      result.put("actionMode", text(benefit.get("actionMode"), "plate"));
      result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple"));
      result.put("plateFormMode", plateFormMode);
      result.put("validEnd", benefit.get("validEnd"));
      return result;
   }
   /**
    * 是否需要小程序里挑一个停车场。
    * 以商家权益(merchant_rights_config)里的 parkingScope 为准:selected/ONE=只选其一,all=全部可选;
    * 权益里没配范围的旧数据,继续沿用停车权益表的 plate_form_mode。
    */
   private boolean requiresParkingLotSelection(Map<String, Object> right, Map<String, Object> benefit) {
      if (right != null) {
         String scope = text(right.get("parkingScope"), "").trim().toUpperCase(Locale.ROOT);
         if (!scope.isBlank()) {
            return "SELECTED".equals(scope) || "ONE".equals(scope) || scope.contains("只选") || scope.contains("其一");
         }
      }
      return "withParking".equalsIgnoreCase(text(benefit.get("plateFormMode"), ""));
   }
   private String effectivePlateFormMode(Map<String, Object> right, Map<String, Object> benefit) {
      return requiresParkingLotSelection(right, benefit) ? "withParking" : "simple";
   }
   private Map<String, Object> registrationView(Map<String, Object> row) {
@@ -196,7 +307,7 @@
   }
   private String applicableCards(Map<String, Object> right, Map<String, Object> benefit) {
      return text(right == null ? null : right.get("level"), text(benefit.get("talentLevels"), "请咨询商户"));
      return text(right == null ? null : right.get("tag"), "");
   }
   private String rightsText(Map<String, Object> merchant, Map<String, Object> right) {
@@ -219,6 +330,42 @@
         if (!tag.isBlank()) return tag;
      }
      return text(benefit.get("benefitText"), text(merchant.get("rightsText"), "免费停车"));
   }
   private List<String> parkingBenefitTags(Long merchantId, Map<String, Object> right) {
      if (right == null || !booleanValue(right.get("enabled")) || !rightStatusEnabled(right)) return List.of();
      String parkingType = text(right.get("parkingType"), "free");
      if ("free".equalsIgnoreCase(parkingType)) return List.of("免费");
      List<String> tags = new ArrayList<>();
      if (hasActiveMerchantCoupon(merchantId)) tags.add("券");
      boolean hasStoredConsumeSetting = right.containsKey("storeConsumeEnabled");
      boolean storeConsume = hasStoredConsumeSetting
            ? booleanValue(right.get("storeConsumeEnabled"))
            : booleanValue(right.get("storeConsume"));
      if (storeConsume) {
         String rawDiscount = text(right.get("discount"), text(right.get("discountText"), ""))
               .replace("折", "").trim();
         try {
            BigDecimal discount = new BigDecimal(rawDiscount).stripTrailingZeros();
            if (discount.compareTo(BigDecimal.ZERO) > 0 && discount.compareTo(BigDecimal.TEN) <= 0) {
               tags.add(discount.toPlainString() + "折");
            }
         } catch (NumberFormatException ignored) {
            // Invalid or missing discounts do not produce a display tag.
         }
      }
      return tags;
   }
   private boolean hasActiveMerchantCoupon(Long merchantId) {
      Integer count = jdbc.queryForObject("""
            select count(*) from merchant_coupon
            where merchant_id = ? and deleted = 0 and status = 1
              and (valid_start is null or valid_start <= current_date)
              and (valid_end is null or valid_end >= current_date)
            """, Integer.class, merchantId);
      return count != null && count > 0;
   }
   private String unavailableReason(Map<String, Object> merchant, Map<String, Object> right) {
@@ -252,6 +399,17 @@
      return rows.isEmpty() ? "" : text(rows.get(0).get("cardLevel"), "");
   }
   private Map<String, Object> activeTalentCard(Long talentId) {
      List<Map<String, Object>> rows = query("""
            select id, card_level
            from talent_card
            where talent_id = ? and card_type = 'PERSONAL' and status = 1 and deleted = 0
            order by id desc limit 1
            """, talentId);
      if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "人才用户或有效人才卡不存在");
      return rows.get(0);
   }
   private String normalizedCardLevel(String value) {
      String raw = value == null ? "" : value.trim();
      String upper = raw.toUpperCase(Locale.ROOT);