| | |
| | | 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; |
| | |
| | | Map<String, Object> merchant = parkingMerchant(merchantId); |
| | | Map<String, Object> benefit = parkingBenefit(merchantId); |
| | | String cardLevel = talentCardId == null ? "" : talentCardLevel(talentCardId); |
| | | Map<String, Object> right = matchingRight(merchant.get("merchantRightsConfig"), cardLevel); |
| | | boolean available = number(merchant.get("status")) == 1 && (right == null || booleanValue(right.get("enabled"))); |
| | | List<Map<String, Object>> rights = parseList(merchant.get("merchantRightsConfig")); |
| | | 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")) && rightStatusEnabled(right))); |
| | | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("merchantId", merchant.get("id")); |
| | |
| | | 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", parkingType); |
| | | result.put("parkingScope", text(right == null ? null : right.get("parkingScope"), "all")); |
| | | result.put("consumeDesc", text(right == null ? null : right.get("consumeDesc"), "")); |
| | | return result; |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | |
| | | 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 |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> matchingRight(Object rightsConfig, String cardLevel) { |
| | | List<Map<String, Object>> rights = parseList(rightsConfig); |
| | | private Map<String, Object> matchingRight(List<Map<String, Object>> rights, String cardLevel) { |
| | | if (rights.isEmpty()) return null; |
| | | String normalizedCardLevel = normalizedCardLevel(cardLevel); |
| | | if (normalizedCardLevel.isBlank()) { |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | private String talentCardLevel(Long talentCardId) { |
| | | List<Map<String, Object>> rows = query("select card_level from talent_card where id = ? and deleted = 0 limit 1", talentCardId); |
| | | 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 text = String.valueOf(value).trim(); |
| | | return text.isEmpty() ? fallback : text; |
| | | } |
| | | } |
| | | } |