shinnosuke
2026-09-10 65b57875700bf42ebd17eafa01a265e5690ce271
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -49,7 +49,7 @@
            select id, title, summary, cover_url, source, view_count, is_top, publish_time
            from news_article
            where deleted = 0 and status = 1
            order by is_top desc, case when is_top = 1 then top_time end desc, publish_time desc, id desc
            order by is_top desc, case when is_top = 1 then updated_at end desc, publish_time desc, id desc
            limit 4
            """));
      result.put("policies", query("""
@@ -99,7 +99,7 @@
            select id, title, summary, cover_url, source, content, view_count, is_top, publish_time
            from news_article
            where deleted = 0 and status = 1
            order by is_top desc, case when is_top = 1 then top_time end desc, publish_time desc, id desc
            order by is_top desc, case when is_top = 1 then updated_at end desc, publish_time desc, id desc
            """, "select count(*) from news_article where deleted = 0 and status = 1", page, size);
      List<Map<String, Object>> records = result.records().stream().map(row -> {
         Map<String, Object> item = new LinkedHashMap<>(row);
@@ -229,7 +229,19 @@
      }
      PageResult<Map<String, Object>> result = page("""
            select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address,
                   is_recommend, sort_no, longitude, latitude, business_status,
                   is_recommend, sort_no, longitude, latitude, business_status, service_label, merchant_rights_config,
                   (exists(select 1 from merchant_coupon c
                           where c.merchant_id = merchant.id and c.deleted = 0 and c.status = 1
                             and (c.valid_start is null or c.valid_start <= current_date)
                             and (c.valid_end is null or c.valid_end >= current_date))
                    or (merchant.merchant_type_code = 'cinema' and exists(select 1 from ticket_package tp
                           where tp.deleted = 0 and tp.status = 1
                             and (tp.valid_start is null or tp.valid_start <= current_date)
                             and (tp.valid_end is null or tp.valid_end >= current_date)
                             and (tp.merchant_id = merchant.id or exists (
                                 select 1 from ticket_package_merchant tpm
                                 where tpm.package_id = tp.id and tpm.merchant_id = merchant.id and tpm.deleted = 0
                             ))))) as coupon_available,
                   (select min(p.sale_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as sale_price,
                   (select min(p.original_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as original_price
            from merchant
@@ -238,8 +250,10 @@
      List<Map<String, Object>> records = result.records().stream().map(row -> {
         Map<String, Object> item = enrichMerchantSearchRow(row);
         Long merchantId = item.get("id") instanceof Number id ? id.longValue() : null;
         item.put("couponAvailable", merchantId != null && hasActiveMerchantCoupon(merchantId));
         item.put("couponAvailable", booleanValue(row.get("couponAvailable")));
         item.put("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId));
         item.put("benefitTags", merchantListBenefitTags(item, row, talentId));
         item.remove("merchantRightsConfig");
         return item;
      }).toList();
      return new PageResult<>(records, result.page(), result.size(), result.total());
@@ -371,9 +385,14 @@
   }
   private List<String> stayBenefitTags(Map<String, Object> right, Map<String, Object> merchant) {
      List<String> tags = new ArrayList<>();
      Object merchantId = merchant.get("id");
      if (merchantId instanceof Number id && hasActiveMerchantCoupon(id.longValue())) tags.add("券");
      boolean couponAvailable = merchantId instanceof Number id && hasActiveMerchantCoupon(id.longValue());
      return stayBenefitTags(right, couponAvailable);
   }
   private List<String> stayBenefitTags(Map<String, Object> right, boolean couponAvailable) {
      List<String> tags = new ArrayList<>();
      if (couponAvailable) tags.add("券");
      if (right == null) return tags;
      if (booleanValue(right.get("freeVoucherEnabled"))) {
         String times = textOrDefault(right.get("freeVoucherTimes"), "");
@@ -384,6 +403,61 @@
         String discount = textOrDefault(right.get("consumeDiscount"), "");
         if ("折扣".equals(type) && !discount.isBlank()) tags.add(discount + "折");
      }
      return tags;
   }
   private List<String> merchantListBenefitTags(Map<String, Object> item, Map<String, Object> row, Long talentId) {
      String merchantType = MerchantTypeCatalog.canonicalCode(textOrDefault(item.get("merchantType"), ""));
      boolean couponAvailable = booleanValue(item.get("couponAvailable"));
      String discountText = textOrDefault(item.get("discountText"), "").trim();
      String serviceLabel = textOrDefault(row.get("serviceLabel"), "").trim();
      List<String> tags = new ArrayList<>();
      if ("food".equals(merchantType)) {
         if (couponAvailable) tags.add("券");
         if (!discountText.isBlank()) tags.add(discountText);
         return tags;
      }
      if ("cinema".equals(merchantType)) {
         if (couponAvailable) tags.add("券");
         return tags;
      }
      // The travel module displays the calculated discount, not the merchant's
      // service label, so search results must use the same source.
      if ("scenic".equals(merchantType)) {
         if (!discountText.isBlank()) tags.add(discountText);
         return tags;
      }
      String cardLevel = activeProductCardLevel(talentId);
      String sourceAreaType = activeTalentSourceAreaType(talentId);
      Map<String, Object> right = matchingMerchantRight(
            merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, sourceAreaType, true);
      if ("hotel".equals(merchantType) || "apartment".equals(merchantType)) {
         tags.addAll(stayBenefitTags(right, couponAvailable));
         if (tags.isEmpty()) tags.add(!discountText.isBlank() ? discountText : "人才专享");
         return tags;
      }
      boolean shoppingType = MerchantTypeCatalog.find(merchantType)
            .map(type -> type.shoppingCategoryKey() != null)
            .orElse(false);
      if (shoppingType) {
         if (couponAvailable) tags.add("券");
         if ("culture".equals(merchantType)) {
            if (right != null && booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) {
               tags.add("到店消费");
            }
         } else if (right != null) {
            String configuredDiscount = merchantRightDiscountText(right);
            if (!configuredDiscount.isBlank()) tags.add(configuredDiscount);
         }
         return tags;
      }
      String categoryTag = !serviceLabel.isBlank() ? serviceLabel : discountText;
      if (categoryTag.isBlank()) categoryTag = "人才专享";
      tags.add(categoryTag);
      return tags;
   }
@@ -410,6 +484,18 @@
      PageResult<Map<String, Object>> result = page("""
            select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address,
                   service_label, status, sort_no, longitude, latitude, business_status,
                   (exists(select 1 from merchant_coupon c
                           where c.merchant_id = merchant.id and c.deleted = 0 and c.status = 1
                             and (c.valid_start is null or c.valid_start <= current_date)
                             and (c.valid_end is null or c.valid_end >= current_date))
                    or exists(select 1 from ticket_package p
                              where p.deleted = 0 and p.status = 1
                                and (p.valid_start is null or p.valid_start <= current_date)
                                and (p.valid_end is null or p.valid_end >= current_date)
                                and (p.merchant_id = merchant.id or exists (
                                    select 1 from ticket_package_merchant pm
                                    where pm.package_id = p.id and pm.merchant_id = merchant.id and pm.deleted = 0
                                )))) as coupon_available,
                   (select min(p.sale_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as sale_price,
                   (select min(p.original_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as original_price
            from merchant
@@ -417,7 +503,9 @@
            "select count(*) from merchant" + merchantWhere, page, size);
      List<Map<String, Object>> records = result.records().stream().map(row -> {
         Map<String, Object> item = enrichMerchantSearchRow(row);
         item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享")));
         boolean couponAvailable = booleanValue(item.get("couponAvailable"));
         item.put("couponAvailable", couponAvailable);
         item.put("discountText", couponAvailable ? "券" : "");
         return item;
      }).toList();
      return new PageResult<>(records, result.page(), result.size(), result.total());
@@ -443,7 +531,7 @@
   }
   @Transactional(readOnly = true)
   public PageResult<Map<String, Object>> transportMerchants(String type, int page, int size) {
   public PageResult<Map<String, Object>> transportMerchants(String type, int page, int size, Long talentId) {
      String requestedType = MerchantTypeCatalog.canonicalCode(type);
      if (!List.of("fuel", "parking", "carWash").contains(requestedType)) {
         throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的行服务类型");
@@ -457,12 +545,21 @@
            from merchant
            """ + merchantWhere + " order by sort_no asc, id asc",
            "select count(*) from merchant" + merchantWhere, page, size);
      String cardLevel = "carWash".equals(requestedType) ? activeProductCardLevel(talentId) : null;
      String sourceAreaType = "carWash".equals(requestedType) ? activeTalentSourceAreaType(talentId) : "";
      List<Map<String, Object>> records = result.records().stream().map(row -> {
         Map<String, Object> item = enrichMerchantSearchRow(row);
         item.put("merchantType", requestedType);
         item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享")));
         if ("carWash".equals(requestedType)) {
            item.put("externalLinkType", carWashExternalLinkType(item.get("merchantRightsConfig")));
            List<Map<String, Object>> rights = merchantRightsConfigs(item.get("merchantRightsConfig"));
            String externalLinkType = carWashExternalLinkType(rights);
            Map<String, Object> right = matchingMerchantRight(rights, cardLevel, sourceAreaType, true);
            String discountText = carWashListDiscountText(externalLinkType, right);
            item.put("externalLinkType", externalLinkType);
            item.put("discountText", discountText);
            item.put("benefitTagVisible", !discountText.isBlank());
         } else {
            item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享")));
         }
         item.remove("merchantRightsConfig");
         return item;
@@ -538,10 +635,10 @@
               && booleanValue(configuredRight.get("storeConsume"))
               && booleanValue(configuredRight.get("consumeEnabled")));
         if (configuredRight != null) {
            item.put("discountText", textOrDefault(configuredRight.get("tag"), textOrDefault(item.get("serviceLabel"), "")));
            item.put("discountText", merchantRightDiscountText(configuredRight));
            item.put("rightsAvailable", true);
         } else if (levelRight != null || !rights.isEmpty()) {
            item.put("discountText", levelRight == null ? "" : textOrDefault(levelRight.get("tag"), ""));
            item.put("discountText", levelRight == null ? "" : merchantRightDiscountText(levelRight));
            item.put("rightsAvailable", false);
            item.put("available", false);
            item.put("unavailableReason", levelRight == null ? "当前人才卡暂未配置该权益" : "当前人才卡暂未开通该权益");
@@ -553,6 +650,19 @@
         return item;
      }).toList();
      return new PageResult<>(records, result.page(), result.size(), result.total());
   }
   private String merchantRightDiscountText(Map<String, Object> right) {
      if (!booleanValue(right.get("storeConsume")) || !booleanValue(right.get("consumeEnabled"))) return "";
      String discount = textOrDefault(right.get("discount"), "").trim();
      if (discount.isBlank()) return "";
      try {
         BigDecimal value = new BigDecimal(discount).stripTrailingZeros();
         if (value.compareTo(BigDecimal.ZERO) <= 0 || value.compareTo(BigDecimal.TEN) > 0) return "";
         return value.toPlainString() + "折";
      } catch (NumberFormatException ignored) {
         return "";
      }
   }
   @Transactional(readOnly = true)
@@ -825,8 +935,8 @@
      }
   }
   private String carWashExternalLinkType(Object rightsConfig) {
      for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) {
   private String carWashExternalLinkType(List<Map<String, Object>> rights) {
      for (Map<String, Object> right : rights) {
         if (right.containsKey("externalLinkType")
               || right.containsKey("externalMiniProgramShortLink")
               || right.containsKey("externalMiniProgramPath")
@@ -837,6 +947,11 @@
      return "";
   }
   private String carWashListDiscountText(String externalLinkType, Map<String, Object> right) {
      if ("miniProgram".equals(externalLinkType) || right == null) return "";
      return merchantRightDiscountText(right);
   }
   private boolean booleanValue(Object value) {
      if (value instanceof Boolean bool) return bool;
      if (value instanceof Number number) return number.intValue() != 0;