| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.temporal.TemporalAdjusters; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Collections; |
| | | import java.util.Locale; |
| | | |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.business.merchant.MerchantTypeCatalog; |
| | | import com.talentcard.server.business.merchant.MerchantTypeCatalog.MerchantType; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import org.jsoup.Jsoup; |
| | |
| | | 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, sort_no 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(""" |
| | |
| | | limit 8 |
| | | """)); |
| | | result.put("recommendedMerchants", query(""" |
| | | select id, merchant_name, merchant_type, logo_url, cover_url, address |
| | | select id, merchant_name, merchant_type_code as merchant_type, logo_url, cover_url, address |
| | | from merchant |
| | | where deleted = 0 and status = 1 and is_recommend = 1 |
| | | order by sort_no desc, id desc |
| | | limit 6 |
| | | """)); |
| | | Map<String, Object> homeSettings = new LinkedHashMap<>(); |
| | | for (Map<String, Object> row : query(""" |
| | | select setting_key, setting_value from admin_setting |
| | | where setting_key in ('homeVideoUrl', 'miniProgramUrl') |
| | | """)) { |
| | | homeSettings.put(String.valueOf(row.get("settingKey")), row.get("settingValue")); |
| | | } |
| | | result.put("homeVideoUrl", String.valueOf(homeSettings.getOrDefault("homeVideoUrl", ""))); |
| | | result.put("miniProgramUrl", String.valueOf(homeSettings.getOrDefault("miniProgramUrl", ""))); |
| | | if (talentId != null) { |
| | | result.put("cards", query(""" |
| | | select id, talent_id, card_no, card_type, card_level, valid_start, valid_end, status |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> news(int page, int size) { |
| | | return page(""" |
| | | select id, title, summary, cover_url, source, view_count, is_top, publish_time |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | 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, sort_no 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); |
| | | item.put("contentPreview", htmlToText((String) item.get("content"))); |
| | | item.remove("content"); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> policies(int page, int size) { |
| | | return page(""" |
| | | select id, policy_no, title, subtitle, category_name, cover_url, tags, author, source, view_count, is_top, publish_time |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, policy_no, title, subtitle, category_name, cover_url, tags, author, source, content, view_count, is_top, publish_time |
| | | from policy_article |
| | | where deleted = 0 and status = 1 |
| | | order by is_top desc, sort_no desc, publish_time desc, id desc |
| | | """, "select count(*) from policy_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); |
| | | item.put("contentPreview", htmlToText((String) item.get("content"))); |
| | | item.remove("content"); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | String mode, |
| | | String sortField, |
| | | String sortOrder, |
| | | Long talentId, |
| | | int page, |
| | | int size |
| | | ) { |
| | |
| | | } |
| | | } |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | is_recommend, sort_no, longitude, latitude, business_status, |
| | | 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, 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 |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size, args.toArray()); |
| | | List<Map<String, Object>> records = result.records().stream().map(this::enrichMerchantSearchRow).toList(); |
| | | 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", 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()); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> foodMerchants(int page, int size) { |
| | | String merchantWhere = " where deleted = 0 and merchant_type in ('food', '餐饮服务')"; |
| | | public PageResult<Map<String, Object>> foodMerchants(int page, int size, Long talentId) { |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = 'food'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, |
| | | (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 |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(this::enrichMerchantSearchRow).toList(); |
| | | 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("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> travelMerchants(int page, int size) { |
| | | String merchantWhere = " where deleted = 0 and merchant_type = 'scenic'"; |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = 'scenic'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, |
| | | (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 |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(this::enrichMerchantSearchRow).toList(); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", "scenic"); |
| | | return enrichMerchantSearchRow(normalized); |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> stayMerchants(String type, int page, int size) { |
| | | String requestedType = type == null ? "" : type.trim(); |
| | | String merchantType = switch (requestedType) { |
| | | case "hotel", "酒店服务" -> "酒店服务"; |
| | | case "apartment", "公寓租赁服务" -> "公寓租赁服务"; |
| | | return stayMerchants(type, page, size, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> stayMerchants(String type, int page, int size, Long talentId) { |
| | | String requestedType = MerchantTypeCatalog.canonicalCode(type); |
| | | boolean apartment = switch (requestedType) { |
| | | case "hotel" -> false; |
| | | case "apartment" -> true; |
| | | default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的住宿服务类型"); |
| | | }; |
| | | String responseMerchantType = merchantType.equals("公寓租赁服务") ? "apartment" : "hotel"; |
| | | String merchantWhere = " where deleted = 0 and merchant_type = ?"; |
| | | String responseMerchantType = apartment ? "apartment" : "hotel"; |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = '" + responseMerchantType + "'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, group_buying_enabled, merchant_rights_config, |
| | | (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 |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size, merchantType); |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", responseMerchantType); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | Map<String, Object> right = matchingMerchantRight(merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, activeTalentSourceAreaType(talentId), true); |
| | | normalized.put("benefitTags", stayBenefitTags(right, row)); |
| | | return enrichMerchantSearchRow(normalized); |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | private List<String> stayBenefitTags(Map<String, Object> right, Map<String, Object> merchant) { |
| | | Object merchantId = merchant.get("id"); |
| | | 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"), ""); |
| | | if (!times.isBlank()) tags.add("免费" + times + "次"); |
| | | } |
| | | if (booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) { |
| | | String type = textOrDefault(right.get("consumeType"), ""); |
| | | 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; |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> entertainmentMerchants(int page, int size) { |
| | | String merchantWhere = " where deleted = 0 and merchant_type in ('cinema', 'theatre', '电影院')"; |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = 'cinema'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | 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 |
| | |
| | | "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()); |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> transportMerchants(String type, int page, int size) { |
| | | if (!List.of("fuel", "parking", "carWash").contains(type)) { |
| | | 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, "不支持的行服务类型"); |
| | | } |
| | | String merchantWhere = " where deleted = 0 and merchant_type = ?"; |
| | | if (type.equals("parking")) { |
| | | type = "停车服务"; |
| | | } |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = '" + requestedType + "'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | service_label, status, sort_no, longitude, latitude, business_status, |
| | | 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, merchant_rights_config, |
| | | (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 |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size, type); |
| | | "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("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享"))); |
| | | item.put("merchantType", requestedType); |
| | | if ("carWash".equals(requestedType)) { |
| | | 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; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> shoppingMerchants(String categoryId, int page, int size) { |
| | | return shoppingMerchants(categoryId, page, size, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> shoppingMerchants(String categoryId, int page, int size, Long talentId) { |
| | | if (categoryId == null || categoryId.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择购物服务分类"); |
| | | int categoryCount = jdbcTemplate.queryForObject(""" |
| | | select count(*) from shopping_topic_category |
| | | where category_key = ? and enabled = 1 and deleted = 0 |
| | | """, Integer.class, categoryId); |
| | | if (categoryCount == 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的购物服务分类"); |
| | | List<String> merchantTypeCodes = MerchantTypeCatalog.codesForShoppingCategory(categoryId); |
| | | if (merchantTypeCodes.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "该购物分类未配置商家类型"); |
| | | String placeholders = String.join(",", java.util.Collections.nCopies(merchantTypeCodes.size(), "?")); |
| | | String merchantWhere = """ |
| | | where m.deleted = 0 and m.merchant_type = 'shopping' |
| | | and mapping.deleted = 0 and mapping.category_key = ? |
| | | """; |
| | | where m.deleted = 0 |
| | | and m.merchant_type_code in (%s) |
| | | """.formatted(placeholders); |
| | | List<Object> args = new ArrayList<>(); |
| | | args.add(categoryId); |
| | | args.addAll(merchantTypeCodes); |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile, m.logo_url, m.cover_url, m.address, |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, m.business_status |
| | | select m.id, m.merchant_name, m.merchant_type_code as merchant_type, m.contact_name, m.contact_mobile, m.logo_url, m.cover_url, m.address, |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, m.business_status, m.merchant_rights_config |
| | | from merchant m |
| | | join shopping_topic_merchant mapping on mapping.merchant_id = m.id |
| | | left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? |
| | | """ + merchantWhere + " order by mapping.sort_no asc, m.sort_no asc, m.id asc", |
| | | "select count(*) from merchant m join shopping_topic_merchant mapping on mapping.merchant_id = m.id " + merchantWhere, |
| | | page, size, categoryId); |
| | | "select count(*) from merchant m left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? " + merchantWhere, |
| | | page, size, args.toArray()); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "人才专享")); |
| | | List<Map<String, Object>> rights = merchantRightsConfigs(row.get("merchantRightsConfig")); |
| | | Map<String, Object> configuredRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, true); |
| | | Map<String, Object> levelRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, false); |
| | | Long merchantId = row.get("id") instanceof Number number ? number.longValue() : null; |
| | | item.put("couponAvailable", merchantId != null && hasActiveMerchantCoupon(merchantId)); |
| | | item.put("storeConsumeAvailable", configuredRight != null |
| | | && booleanValue(configuredRight.get("storeConsume")) |
| | | && booleanValue(configuredRight.get("consumeEnabled"))); |
| | | if (configuredRight != null) { |
| | | item.put("discountText", merchantRightDiscountText(configuredRight)); |
| | | item.put("rightsAvailable", true); |
| | | } else if (levelRight != null || !rights.isEmpty()) { |
| | | item.put("discountText", levelRight == null ? "" : merchantRightDiscountText(levelRight)); |
| | | item.put("rightsAvailable", false); |
| | | item.put("available", false); |
| | | item.put("unavailableReason", levelRight == null ? "当前人才卡暂未配置该权益" : "当前人才卡暂未开通该权益"); |
| | | } else { |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "")); |
| | | item.put("rightsAvailable", true); |
| | | } |
| | | item.remove("merchantRightsConfig"); |
| | | 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) |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> premiumServices(String categoryId, int page, int size) { |
| | | public PageResult<Map<String, Object>> premiumServices(String type, int page, int size) { |
| | | String categoryId = jdbcTemplate.query("select category_key from premium_topic_category where category_name = ? and enabled = 1 and deleted = 0 limit 1", rs -> rs.next() ? rs.getString("category_key") : null, type == null ? "" : type.trim()); |
| | | if (categoryId == null || categoryId.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择臻享服务分类"); |
| | | Integer categoryCount = jdbcTemplate.queryForObject(""" |
| | | select count(*) from premium_topic_category |
| | | where category_key = ? and enabled = 1 and deleted = 0 |
| | | """, Integer.class, categoryId); |
| | | if (categoryCount == null || categoryCount == 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的臻享服务分类"); |
| | | String serviceWhere = """ |
| | | where m.deleted = 0 and mapping.deleted = 0 and mapping.category_key = ? |
| | | """; |
| | | MerchantType merchantType = MerchantTypeCatalog.forPremiumCategory(categoryId) |
| | | .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "该臻享分类未配置商家类型")); |
| | | String serviceWhere = " where m.deleted = 0 and m.merchant_type_code = ?"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select m.id, m.merchant_name, m.merchant_type, m.contact_mobile, m.cover_url, m.address, |
| | | select m.id, m.merchant_name, m.merchant_type_code as merchant_type, m.contact_mobile, m.cover_url, m.address, |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, |
| | | mapping.category_key, mapping.business_type |
| | | mapping.sort_no as topic_sort_no |
| | | from merchant m |
| | | join premium_topic_merchant mapping on mapping.merchant_id = m.id |
| | | left join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? |
| | | """ + serviceWhere + " order by mapping.sort_no asc, m.sort_no asc, m.id asc", |
| | | "select count(*) from merchant m join premium_topic_merchant mapping on mapping.merchant_id = m.id " + serviceWhere, |
| | | page, size, categoryId); |
| | | "select count(*) from merchant m left join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? " + serviceWhere, |
| | | page, size, categoryId, merchantType.code()); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("businessType", row.get("businessType")); |
| | | item.put("targetAction", row.get("businessType")); |
| | | item.put("businessType", merchantType.premiumBusinessType()); |
| | | item.put("targetAction", merchantType.premiumBusinessType()); |
| | | item.put("benefitText", textOrDefault(row.get("serviceLabel"), "人才专享")); |
| | | item.put("discountText", textOrDefault(row.get("serviceLabel"), "人才专享")); |
| | | return item; |
| | |
| | | } |
| | | |
| | | private String categoryName(String merchantType) { |
| | | return switch (merchantType) { |
| | | case "food", "餐饮服务" -> "暖心食享"; |
| | | case "hotel", "apartment" -> "舒心安居"; |
| | | case "cinema", "theatre" -> "怡乐生活"; |
| | | case "scenic" -> "悦览花城"; |
| | | case "fuel", "parking", "carWash" -> "畅行无忧"; |
| | | case "shopping" -> "惠享好物"; |
| | | case "medical", "finance" -> "臻享礼遇"; |
| | | default -> "其他权益"; |
| | | }; |
| | | return MerchantTypeCatalog.find(merchantType).map(MerchantType::topicName).orElse("其他权益"); |
| | | } |
| | | |
| | | private String appendMerchantTypeFilter(String merchantWhere, List<String> merchantTypes, List<Object> args) { |
| | | if (merchantTypes.size() == 1) { |
| | | args.add(merchantTypes.get(0)); |
| | | return merchantWhere + " and merchant_type = ?"; |
| | | return merchantWhere + " and merchant_type_code = ?"; |
| | | } |
| | | args.addAll(merchantTypes); |
| | | String placeholders = String.join(",", Collections.nCopies(merchantTypes.size(), "?")); |
| | | return merchantWhere + " and merchant_type in (" + placeholders + ")"; |
| | | return merchantWhere + " and merchant_type_code in (" + placeholders + ")"; |
| | | } |
| | | |
| | | private List<String> merchantTypesForRequestedValue(String value) { |
| | | List<String> categoryTypes = merchantTypesForCategory(value); |
| | | return categoryTypes.isEmpty() ? List.of(value) : categoryTypes; |
| | | List<String> types = MerchantTypeCatalog.codesForRequestedValue(value); |
| | | return types.isEmpty() ? List.of("other") : types; |
| | | } |
| | | |
| | | private List<String> merchantTypesForCategory(String value) { |
| | | return switch (value) { |
| | | case "暖心食享", "餐饮服务", "food" -> List.of("food", "餐饮服务"); |
| | | case "舒心安居", "hotel" -> List.of("hotel"); |
| | | case "怡乐生活", "cinema", "theatre" -> List.of("cinema", "theatre"); |
| | | case "悦览花城", "scenic" -> List.of("scenic"); |
| | | case "畅行无忧" -> List.of("fuel", "parking", "carWash"); |
| | | case "fuel" -> List.of("fuel"); |
| | | case "parking" -> List.of("parking"); |
| | | case "carWash" -> List.of("carWash"); |
| | | case "惠享好物", "shopping" -> List.of("shopping"); |
| | | case "臻享礼遇" -> List.of("medical", "finance"); |
| | | case "medical" -> List.of("medical"); |
| | | case "finance" -> List.of("finance"); |
| | | default -> List.of(); |
| | | }; |
| | | return MerchantTypeCatalog.codesForRequestedValue(value); |
| | | } |
| | | |
| | | private String detailType(String merchantType) { |
| | | return switch (merchantType) { |
| | | case "cinema" -> "CINEMA"; |
| | | case "theatre" -> "THEATRE"; |
| | | case "shopping" -> "SHOPPING"; |
| | | case "medical" -> "MEDICAL"; |
| | | case "finance" -> "FINANCE"; |
| | | case "carWash" -> "CAR_WASH"; |
| | | case "fuel" -> "FUEL"; |
| | | case "parking" -> "PARKING"; |
| | | default -> "MERCHANT"; |
| | | }; |
| | | return MerchantTypeCatalog.find(merchantType).map(MerchantType::detailType).orElse("MERCHANT"); |
| | | } |
| | | |
| | | private String targetAction(String merchantType) { |
| | |
| | | } |
| | | |
| | | private String discountText(Object salePrice, Object originalPrice) { |
| | | if (salePrice instanceof Number sale && originalPrice instanceof Number original |
| | | && original.doubleValue() > 0 && sale.doubleValue() == 0) { |
| | | return "免费"; |
| | | } |
| | | if (salePrice instanceof Number sale && originalPrice instanceof Number original |
| | | && sale.doubleValue() > 0 && original.doubleValue() > sale.doubleValue()) { |
| | | double discount = Math.round(sale.doubleValue() / original.doubleValue() * 100) / 10.0; |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchantDetail(Long id) { |
| | | return merchantDetail(id, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchantDetail(Long id, Long talentId) { |
| | | Map<String, Object> merchant = one(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | business_status, longitude, latitude, group_buying_enabled, usage_instructions, rights_text, |
| | | service_label, status, is_recommend, sort_no, |
| | | (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 |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | business_status, longitude, latitude, group_buying_enabled, usage_instructions, rights_text, merchant_rights_config, |
| | | service_label, status, is_recommend, sort_no |
| | | from merchant |
| | | where deleted = 0 and id = ? |
| | | """, id); |
| | | merchant.put("available", ((Number) merchant.getOrDefault("status", 0)).intValue() == 1); |
| | | merchant.put("groupBuyingEnabled", ((Number) merchant.getOrDefault("groupBuyingEnabled", 1)).intValue() == 1); |
| | | merchant.put("discountText", discountText(merchant.get("salePrice"), merchant.get("originalPrice"))); |
| | | merchant.put("couponAvailable", hasActiveMerchantCoupon(id)); |
| | | merchant.put("discountText", merchantLowestTalentDiscount(id, talentId)); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | merchant.put("cardLevel", cardLevel); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | | merchant.put("sourceAreaType", sourceAreaType); |
| | | List<Map<String, Object>> rightsConfigs = merchantRightsConfigs(merchant.get("merchantRightsConfig")); |
| | | Map<String, Object> configuredRight = matchingMerchantRight(rightsConfigs, cardLevel, sourceAreaType, true); |
| | | Map<String, Object> levelRight = matchingMerchantRight(rightsConfigs, cardLevel, sourceAreaType, false); |
| | | boolean freeVoucherEnabled = false; |
| | | String freeVoucherTimes = ""; |
| | | String freeVoucherCycle = ""; |
| | | String consumeType = ""; |
| | | String consumeDiscount = ""; |
| | | String consumeCoupon = ""; |
| | | boolean storeConsume = false; |
| | | boolean consumeEnabled = false; |
| | | merchant.put("rightsAvailable", configuredRight != null); |
| | | if (configuredRight != null) { |
| | | merchant.put("rightsTag", textOrDefault(configuredRight.get("tag"), "")); |
| | | merchant.put("rightsText", textOrDefault(configuredRight.get("detail"), textOrDefault(merchant.get("rightsText"), ""))); |
| | | merchant.put("applicableCards", textOrDefault(configuredRight.get("tag"), "")); |
| | | merchant.put("rightsStatus", "AVAILABLE"); |
| | | freeVoucherEnabled = booleanValue(configuredRight.get("freeVoucherEnabled")); |
| | | freeVoucherTimes = textOrDefault(configuredRight.get("freeVoucherTimes"), ""); |
| | | freeVoucherCycle = textOrDefault(configuredRight.get("freeVoucherCycle"), ""); |
| | | consumeType = textOrDefault(configuredRight.get("consumeType"), ""); |
| | | consumeDiscount = textOrDefault(configuredRight.get("consumeDiscount"), ""); |
| | | consumeCoupon = textOrDefault(configuredRight.get("consumeCoupon"), ""); |
| | | storeConsume = booleanValue(configuredRight.get("storeConsume")); |
| | | consumeEnabled = booleanValue(configuredRight.get("consumeEnabled")); |
| | | } else if (levelRight != null) { |
| | | merchant.put("rightsTag", textOrDefault(levelRight.get("tag"), "")); |
| | | merchant.put("rightsText", "当前人才卡暂未开通该权益"); |
| | | merchant.put("applicableCards", textOrDefault(levelRight.get("tag"), "")); |
| | | merchant.put("rightsStatus", "DISABLED"); |
| | | } else if (!rightsConfigs.isEmpty()) { |
| | | merchant.put("rightsTag", ""); |
| | | merchant.put("rightsText", "当前人才卡暂未配置该权益"); |
| | | merchant.put("applicableCards", ""); |
| | | merchant.put("rightsStatus", "MISSING"); |
| | | } else { |
| | | merchant.put("rightsStatus", "UNCONFIGURED"); |
| | | } |
| | | merchant.put("freeVoucherEnabled", freeVoucherEnabled); |
| | | merchant.put("freeVoucherTimes", freeVoucherTimes); |
| | | merchant.put("freeVoucherCycle", freeVoucherCycle); |
| | | merchant.put("consumeType", consumeType); |
| | | merchant.put("consumeDiscount", consumeDiscount); |
| | | merchant.put("consumeCoupon", consumeCoupon); |
| | | merchant.put("storeConsume", storeConsume); |
| | | merchant.put("consumeEnabled", consumeEnabled); |
| | | merchant.put("usageInstructions", merchantUsageInstructions( |
| | | id, merchant.get("merchantRightsConfig"), cardLevel, merchant.get("usageInstructions"))); |
| | | List<String> heroImages = new ArrayList<>(); |
| | | String coverUrl = textOrDefault(merchant.get("coverUrl"), ""); |
| | | if (!coverUrl.isBlank()) heroImages.add(coverUrl); |
| | | for (Map<String, Object> product : query(""" |
| | | select cover_url |
| | | from merchant_product |
| | | where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2 |
| | | and cover_url is not null and cover_url <> '' |
| | | order by sort_no desc, id desc |
| | | limit 8 |
| | | """, id)) { |
| | | String imageUrl = textOrDefault(product.get("coverUrl"), ""); |
| | | if (!imageUrl.isBlank() && !heroImages.contains(imageUrl)) heroImages.add(imageUrl); |
| | | } |
| | | merchant.put("heroImages", heroImages); |
| | | return merchant; |
| | | } |
| | | |
| | | private Map<String, Object> matchingMerchantRight(List<Map<String, Object>> rights, String cardLevel, String sourceAreaType, boolean requireEnabled) { |
| | | if (cardLevel == null) return null; |
| | | List<String> candidates = merchantRightCandidates(cardLevel, sourceAreaType); |
| | | for (String candidate : candidates) { |
| | | for (Map<String, Object> right : rights) { |
| | | if (requireEnabled && !booleanValue(right.get("enabled"))) continue; |
| | | if (candidate.equals(textOrDefault(right.get("level"), ""))) return right; |
| | | if (sameCardLevel(right.get("level"), candidate)) return right; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private List<String> merchantRightCandidates(String cardLevel, String sourceAreaType) { |
| | | String level = normalizedCardLevel(cardLevel); |
| | | String area = normalizedSourceAreaType(sourceAreaType); |
| | | if ("EXCELLENT".equals(level)) { |
| | | if ("LOCAL".equals(area)) return List.of("卓越人才卡(本地)", "卓越人才卡"); |
| | | if ("OUTSIDE".equals(area)) return List.of("卓越人才卡(外地)", "卓越人才卡"); |
| | | return List.of("卓越人才卡"); |
| | | } |
| | | if ("OUTSTANDING".equals(level)) return List.of("杰出人才卡"); |
| | | if ("ELITE".equals(level)) return List.of("菁英人才卡"); |
| | | return cardLevel == null || cardLevel.isBlank() ? List.of() : List.of(cardLevel); |
| | | } |
| | | |
| | | private String cardLevelText(String cardLevel) { |
| | | return switch (normalizedCardLevel(cardLevel)) { |
| | | case "EXCELLENT" -> "卓越卡"; |
| | | case "OUTSTANDING" -> "杰出卡"; |
| | | case "ELITE" -> "菁英卡"; |
| | | default -> "人才卡"; |
| | | }; |
| | | } |
| | | |
| | | private String merchantUsageInstructions(Long merchantId, Object rightsConfig, String cardLevel, Object fallback) { |
| | | if (cardLevel != null) { |
| | | for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) { |
| | | if (!booleanValue(right.get("enabled")) || !booleanValue(right.get("consumeEnabled")) |
| | | || !booleanValue(right.get("storeConsume")) || !sameCardLevel(right.get("level"), cardLevel)) continue; |
| | | String description = textOrDefault(right.get("consumeDesc"), ""); |
| | | if (!description.isBlank()) return description; |
| | | } |
| | | List<Map<String, Object>> benefits = query(""" |
| | | select card_level, benefit_text |
| | | from merchant_card_benefit |
| | | where merchant_id = ? and status = 1 |
| | | order by sort_no desc, id desc |
| | | """, merchantId); |
| | | for (Map<String, Object> benefit : benefits) { |
| | | if (sameCardLevel(benefit.get("cardLevel"), cardLevel)) { |
| | | String description = textOrDefault(benefit.get("benefitText"), ""); |
| | | if (!description.isBlank()) return description; |
| | | } |
| | | } |
| | | } |
| | | return textOrDefault(fallback, "以当前商户到店消费规则为准"); |
| | | } |
| | | |
| | | private List<Map<String, Object>> merchantRightsConfigs(Object value) { |
| | | if (value == null || String.valueOf(value).isBlank()) return List.of(); |
| | | try { |
| | | return OBJECT_MAPPER.readValue(String.valueOf(value), new TypeReference<List<Map<String, Object>>>() {}); |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | private String carWashExternalLinkType(List<Map<String, Object>> rights) { |
| | | for (Map<String, Object> right : rights) { |
| | | if (right.containsKey("externalLinkType") |
| | | || right.containsKey("externalMiniProgramShortLink") |
| | | || right.containsKey("externalMiniProgramPath") |
| | | || right.containsKey("externalWebUrl")) { |
| | | return textOrDefault(right.get("externalLinkType"), ""); |
| | | } |
| | | } |
| | | 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; |
| | | String text = value == null ? "" : String.valueOf(value).trim(); |
| | | return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); |
| | | } |
| | | |
| | | private boolean sameCardLevel(Object configured, String actual) { |
| | | return normalizedCardLevel(configured).equals(normalizedCardLevel(actual)); |
| | | } |
| | | |
| | | private String normalizedSourceAreaType(Object value) { |
| | | String raw = textOrDefault(value, "").toUpperCase(Locale.ROOT); |
| | | if (raw.contains("OUTSIDE") || raw.contains("外地")) return "OUTSIDE"; |
| | | if (raw.contains("LOCAL") || raw.contains("本地")) return "LOCAL"; |
| | | return ""; |
| | | } |
| | | |
| | | private String normalizedCardLevel(Object value) { |
| | | String level = value == null ? "" : String.valueOf(value).trim().toUpperCase(Locale.ROOT); |
| | | if (level.contains("卓越") || "EXCELLENT".equals(level)) return "EXCELLENT"; |
| | | if (level.contains("杰出") || "OUTSTANDING".equals(level)) return "OUTSTANDING"; |
| | | if (level.contains("菁英") || "ELITE".equals(level)) return "ELITE"; |
| | | return level; |
| | | } |
| | | |
| | | private boolean hasActiveMerchantCoupon(Long merchantId) { |
| | | Long count = jdbcTemplate.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) |
| | | """, Long.class, merchantId); |
| | | return count != null && count > 0; |
| | | } |
| | | |
| | | private String merchantLowestTalentDiscount(Long merchantId, Long talentId) { |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | if (cardLevel == null) return ""; |
| | | BigDecimal lowestRate = null; |
| | | for (Map<String, Object> product : query(""" |
| | | select id, original_price, sale_price, rights_config |
| | | from merchant_product |
| | | where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2 |
| | | """, merchantId)) { |
| | | ProductPriceCalculator.ProductRight right = ProductPriceCalculator.productRight( |
| | | product.get("rightsConfig"), cardLevel); |
| | | if (right == null || right.discount().signum() <= 0) continue; |
| | | BigDecimal rate = right.discount().divide(BigDecimal.TEN, 6, RoundingMode.HALF_UP); |
| | | if (lowestRate == null || rate.compareTo(lowestRate) < 0) { |
| | | lowestRate = rate; |
| | | } |
| | | } |
| | | if (lowestRate == null || lowestRate.compareTo(BigDecimal.ONE) >= 0) return ""; |
| | | double discount = Math.round(lowestRate.doubleValue() * 100) / 10.0; |
| | | return discount + "折"; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> productDetail(Long id) { |
| | | return productDetail(id, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> productDetail(Long id, Long talentId) { |
| | | Map<String, Object> product = one(""" |
| | | select id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, stock, sales_count, detail_text, benefit_desc, |
| | |
| | | && ((Number) product.getOrDefault("auditStatus", 0)).intValue() == 2; |
| | | product.put("available", available); |
| | | product.put("soldOut", stock == 0); |
| | | BigDecimal configuredOriginalPrice = decimal(product.get("originalPrice")); |
| | | if (configuredOriginalPrice.signum() <= 0) { |
| | | configuredOriginalPrice = decimal(product.get("salePrice")); |
| | | product.put("originalPrice", configuredOriginalPrice); |
| | | } |
| | | BigDecimal configuredSalePrice = decimal(product.get("salePrice")); |
| | | BigDecimal personalizedPrice = personalizedProductPrice(product, talentId); |
| | | product.put("salePrice", personalizedPrice); |
| | | product.put("personalized", talentId != null && personalizedPrice.compareTo(configuredSalePrice) != 0); |
| | | Long merchantId = ((Number) product.get("merchantId")).longValue(); |
| | | product.put("couponAvailable", hasActiveMerchantCoupon(merchantId)); |
| | | product.put("discountText", merchantLowestTalentDiscount(merchantId, talentId)); |
| | | return product; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> availableOrderCoupons(Long merchantId, Long productId, String orderId) { |
| | | return availableOrderCoupons(merchantId, productId, orderId, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> availableOrderCoupons(Long merchantId, Long productId, String orderId, Long talentId) { |
| | | Map<String, Object> product = orderCouponProduct(merchantId, productId, orderId); |
| | | BigDecimal productAmount = decimal(product.get("salePrice")); |
| | | BigDecimal productAmount = personalizedProductPrice(product, talentId); |
| | | String cardLevel = talentId == null ? null : activeCouponCardLevel(talentId); |
| | | return query(""" |
| | | select c.id, c.coupon_name, c.condition_type, c.condition_amount, c.coupon_amount, |
| | | c.valid_start, c.valid_end |
| | |
| | | where c.deleted = 0 and c.status = 1 and c.merchant_id = ? |
| | | and (c.valid_start is null or c.valid_start <= current_date) |
| | | and (c.valid_end is null or c.valid_end >= current_date) |
| | | and (c.condition_type = 'NONE' or c.condition_amount is null or c.condition_amount <= ?) |
| | | and (c.product_scope = 'ALL' or exists ( |
| | | select 1 from merchant_coupon_product cp |
| | | where cp.coupon_id = c.id and cp.product_id = ? |
| | | )) |
| | | order by c.coupon_amount desc, c.id desc |
| | | """, merchantId, productAmount, productId).stream() |
| | | and (c.condition_type = 'NONE' or c.condition_amount is null or c.condition_amount <= ?) |
| | | and (? is null or coalesce(c.talent_levels, '') = '' or find_in_set(?, replace(c.talent_levels, '、', ',')) > 0) |
| | | and (c.product_scope = 'ALL' or exists ( |
| | | select 1 from merchant_coupon_product cp |
| | | where cp.coupon_id = c.id and cp.product_id = ? |
| | | )) |
| | | and (? is null or not exists ( |
| | | select 1 from benefit_order o |
| | | where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0 |
| | | and (o.expires_at is null or o.expires_at > current_timestamp) |
| | | )) |
| | | order by c.coupon_amount desc, c.id desc |
| | | """, merchantId, productAmount, cardLevel, cardLevel, productId, talentId, talentId).stream() |
| | | .map(this::couponListItem) |
| | | .toList(); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> orderCouponQuote(Long merchantId, Long productId, String orderId, Long couponId) { |
| | | return orderCouponQuote(merchantId, productId, orderId, couponId, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> orderCouponQuote(Long merchantId, Long productId, String orderId, Long couponId, Long talentId) { |
| | | Map<String, Object> product = orderCouponProduct(merchantId, productId, orderId); |
| | | BigDecimal originalAmount = decimal(product.get("originalPrice")); |
| | | BigDecimal productAmount = decimal(product.get("salePrice")); |
| | | if (originalAmount.signum() <= 0) originalAmount = decimal(product.get("salePrice")); |
| | | BigDecimal productAmount = personalizedProductPrice(product, talentId); |
| | | String cardLevel = talentId == null ? null : activeCouponCardLevel(talentId); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("orderId", orderId); |
| | | result.put("merchantId", merchantId); |
| | |
| | | result.put("couponAmount", BigDecimal.ZERO); |
| | | result.put("discountAmount", originalAmount.subtract(productAmount).max(BigDecimal.ZERO)); |
| | | result.put("payAmount", productAmount); |
| | | result.put("subsidyAmount", originalAmount.subtract(productAmount).max(BigDecimal.ZERO)); |
| | | return result; |
| | | } |
| | | |
| | |
| | | where c.id = ? and c.deleted = 0 and c.status = 1 and c.merchant_id = ? |
| | | and (c.valid_start is null or c.valid_start <= current_date) |
| | | and (c.valid_end is null or c.valid_end >= current_date) |
| | | and (c.condition_type = 'NONE' or c.condition_amount is null or c.condition_amount <= ?) |
| | | and (c.product_scope = 'ALL' or exists ( |
| | | select 1 from merchant_coupon_product cp |
| | | where cp.coupon_id = c.id and cp.product_id = ? |
| | | )) |
| | | """, couponId, merchantId, productAmount, productId); |
| | | and (c.condition_type = 'NONE' or c.condition_amount is null or c.condition_amount <= ?) |
| | | and (? is null or coalesce(c.talent_levels, '') = '' or find_in_set(?, replace(c.talent_levels, '、', ',')) > 0) |
| | | and (c.product_scope = 'ALL' or exists ( |
| | | select 1 from merchant_coupon_product cp |
| | | where cp.coupon_id = c.id and cp.product_id = ? |
| | | )) |
| | | and (? is null or not exists ( |
| | | select 1 from benefit_order o |
| | | where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0 |
| | | and (o.expires_at is null or o.expires_at > current_timestamp) |
| | | )) |
| | | """, couponId, merchantId, productAmount, cardLevel, cardLevel, productId, talentId, talentId); |
| | | if (coupons.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "该优惠券当前不可用,请重新选择"); |
| | | } |
| | |
| | | result.put("couponAmount", couponAmount); |
| | | result.put("discountAmount", originalAmount.subtract(productAmount).max(BigDecimal.ZERO).add(couponAmount)); |
| | | result.put("payAmount", productAmount.subtract(couponAmount).max(BigDecimal.ZERO)); |
| | | result.put("subsidyAmount", originalAmount.subtract(productAmount).max(BigDecimal.ZERO).add(couponAmount)); |
| | | return result; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> products(Long merchantId, String type, int page, int size) { |
| | | public PageResult<Map<String, Object>> products(Long merchantId, String type, Long talentId, int page, int size) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, stock, sales_count, benefit_desc |
| | | original_price, sale_price, subsidy_amount, stock, sales_count, benefit_desc, rights_config |
| | | from merchant_product |
| | | where deleted = 0 and status = 1 and audit_status = 2 |
| | | """); |
| | |
| | | args.add(type); |
| | | } |
| | | sql.append(" order by sort_no desc, id desc"); |
| | | return page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | List<Map<String, Object>> records = result.records().stream().map(product -> { |
| | | Map<String, Object> item = new LinkedHashMap<>(product); |
| | | BigDecimal configuredSalePrice = decimal(product.get("salePrice")); |
| | | BigDecimal configuredOriginalPrice = decimal(product.get("originalPrice")); |
| | | BigDecimal originalPrice = decimal(item.get("originalPrice")); |
| | | if (originalPrice.signum() <= 0) originalPrice = decimal(item.get("salePrice")); |
| | | BigDecimal personalizedPrice = personalizedProductPrice(item, talentId); |
| | | item.put("configuredSalePrice", configuredSalePrice); |
| | | item.put("configuredOriginalPrice", configuredOriginalPrice.signum() > 0 ? configuredOriginalPrice : originalPrice); |
| | | item.put("originalPrice", originalPrice); |
| | | item.put("salePrice", personalizedPrice); |
| | | item.put("personalized", talentId != null && personalizedPrice.compareTo(decimal(product.get("salePrice"))) != 0); |
| | | item.put("discountText", discountText(personalizedPrice, originalPrice)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | private Map<String, Object> orderCouponProduct(Long merchantId, Long productId, String orderId) { |
| | |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "订单、商户和商品信息不能为空"); |
| | | } |
| | | Map<String, Object> product = one(""" |
| | | select id, merchant_id, product_name, cover_url, original_price, sale_price, stock, status, audit_status |
| | | select id, merchant_id, product_name, cover_url, original_price, sale_price, rights_config, stock, status, audit_status |
| | | from merchant_product |
| | | where deleted = 0 and id = ? and merchant_id = ? |
| | | """, productId, merchantId); |
| | |
| | | throw new BusinessException(ErrorCode.CONFLICT, "商品当前不可购买"); |
| | | } |
| | | return product; |
| | | } |
| | | |
| | | private BigDecimal personalizedProductPrice(Map<String, Object> product, Long talentId) { |
| | | if (talentId == null) return decimal(product.get("salePrice")); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | if (cardLevel == null) return decimal(product.get("salePrice")); |
| | | return ProductPriceCalculator.calculate(product.get("originalPrice"), product.get("salePrice"), product.get("rightsConfig"), cardLevel, |
| | | usedFreeCount(talentId, product.get("id"), product.get("rightsConfig"), cardLevel)).salePrice(); |
| | | } |
| | | |
| | | private String activeProductCardLevel(Long talentId) { |
| | | if (talentId == null) return null; |
| | | List<Map<String, Object>> cards = query("select card_level from talent_card where deleted = 0 and status = 1 and talent_id = ? and (card_type = 'PERSONAL' or card_type is null) order by id desc limit 1", talentId); |
| | | Object cardLevel = cards.isEmpty() ? null : cards.get(0).get("cardLevel"); |
| | | if (cardLevel == null) { |
| | | List<Map<String, Object>> talents = query("select talent_level from talent_user where deleted = 0 and status = 1 and id = ? limit 1", talentId); |
| | | if (!talents.isEmpty()) cardLevel = talents.get(0).get("talentLevel"); |
| | | } |
| | | return cardLevel == null ? null : String.valueOf(cardLevel); |
| | | } |
| | | |
| | | private String activeTalentSourceAreaType(Long talentId) { |
| | | if (talentId == null) return ""; |
| | | List<Map<String, Object>> rows = query(""" |
| | | select source_area_type |
| | | from talent_user |
| | | where deleted = 0 and status = 1 and id = ? |
| | | limit 1 |
| | | """, talentId); |
| | | if (rows.isEmpty()) return ""; |
| | | return normalizedSourceAreaType(rows.get(0).get("sourceAreaType")); |
| | | } |
| | | |
| | | private int usedFreeCount(Long talentId, Object productId, Object rightsConfig, Object cardLevel) { |
| | | ProductPriceCalculator.ProductRight right = ProductPriceCalculator.productRight(rightsConfig, cardLevel); |
| | | if (right == null || !right.free() || !(productId instanceof Number number)) return 0; |
| | | Long count = jdbcTemplate.queryForObject(""" |
| | | select count(*) |
| | | from benefit_order |
| | | where deleted = 0 and talent_id = ? and product_id = ? and pay_amount = 0 and order_status = 1 |
| | | and created_at >= ? |
| | | """, Long.class, talentId, number.longValue(), freeWindowStart(right.resetCycle())); |
| | | return count == null ? 0 : count.intValue(); |
| | | } |
| | | |
| | | private LocalDateTime freeWindowStart(String resetCycle) { |
| | | String cycle = resetCycle == null ? "" : resetCycle.trim().toUpperCase(Locale.ROOT); |
| | | LocalDate today = LocalDate.now(); |
| | | if (cycle.isBlank() || "NONE".equals(cycle)) return LocalDate.of(1970, 1, 1).atStartOfDay(); |
| | | if (cycle.contains("DAY") || cycle.contains("天") || cycle.contains("日")) return today.atStartOfDay(); |
| | | if (cycle.contains("WEEK") || cycle.contains("周") || cycle.contains("星期")) return today.minusDays(today.getDayOfWeek().getValue() - 1L).atStartOfDay(); |
| | | if (cycle.contains("MONTH") || cycle.contains("月")) return today.withDayOfMonth(1).atStartOfDay(); |
| | | return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay(); |
| | | } |
| | | |
| | | private Map<String, Object> couponListItem(Map<String, Object> coupon) { |
| | |
| | | return value.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | private String activeCouponCardLevel(Long talentId) { |
| | | List<Map<String, Object>> cards = query("select card_level from talent_card where talent_id=? and deleted=0 and status=1 and card_type='PERSONAL' and (valid_end is null or valid_end >= current_date) order by id desc limit 1", talentId); |
| | | if (cards.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前暂无有效人才卡"); |
| | | return String.valueOf(cards.get(0).get("cardLevel")); |
| | | } |
| | | |
| | | private List<Map<String, Object>> query(String sql, Object... args) { |
| | | return DatabaseRows.normalize(jdbcTemplate.queryForList(sql, args)); |
| | | } |