package com.talentcard.server.service; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.merchant.MerchantTypeCatalog; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.entity.BenefitFamilyCardApplication; import com.talentcard.server.entity.BenefitTalentCard; import com.talentcard.server.entity.MerchantDisplayPolicyEvent; import com.talentcard.server.mapper.BenefitFamilyCardApplicationMapper; import com.talentcard.server.mapper.BenefitTalentCardMapper; import com.talentcard.server.mapper.MerchantDisplayPolicyEventMapper; import com.talentcard.server.mapper.MerchantDisplayPolicyMerchantMapper; import com.talentcard.server.request.MerchantDisplayPolicyEventRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class MerchantDisplayPolicyPortalService { private final MerchantDisplayPolicyMerchantMapper merchantMapper; private final MerchantDisplayPolicyEventMapper eventMapper; private final BenefitTalentCardMapper cardMapper; private final BenefitFamilyCardApplicationMapper familyMapper; public MerchantDisplayPolicyPortalService( MerchantDisplayPolicyMerchantMapper merchantMapper, MerchantDisplayPolicyEventMapper eventMapper, BenefitTalentCardMapper cardMapper, BenefitFamilyCardApplicationMapper familyMapper ) { this.merchantMapper = merchantMapper; this.eventMapper = eventMapper; this.cardMapper = cardMapper; this.familyMapper = familyMapper; } @Transactional(readOnly = true) public PageResult> page(Long talentId, Long cardId, String merchantType, String keyword, int page, int size) { CardContext card = cardContext(talentId, cardId); int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); List aliases = cardLevelAliases(card.cardLevel()); List> records = merchantMapper.selectPortalPage( aliases, merchantTypeCode(merchantType), clean(keyword), safeSize, (safePage - 1) * safeSize ).stream().map(row -> portalView(row, card)).toList(); Long total = merchantMapper.countPortalPage(aliases, merchantTypeCode(merchantType), clean(keyword)); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional(readOnly = true) public Map detail(Long merchantId, Long talentId, Long cardId) { Long id = requireMerchantId(merchantId); CardContext card = cardContext(talentId, cardId); Map row = merchantMapper.selectPortalDetail(id, cardLevelAliases(card.cardLevel())); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "商户优惠政策不存在或当前人才卡类型不可用"); } return portalView(row, card); } private String merchantTypeCode(String value) { String cleaned = clean(value); return cleaned.isBlank() ? "" : MerchantTypeCatalog.canonicalCode(cleaned); } @Transactional public Map recordEvent(Long merchantId, MerchantDisplayPolicyEventRequest request) { Long id = requireMerchantId(merchantId); String eventType = normalizeEventType(request.eventType()); CardContext card = cardContext(request.talentId(), request.cardId()); Map row = merchantMapper.selectPortalDetail(id, cardLevelAliases(card.cardLevel())); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "商户优惠政策不存在或当前人才卡类型不可用"); } MerchantDisplayPolicyEvent event = new MerchantDisplayPolicyEvent(); event.setMerchantId(id); event.setTalentId(card.sourceTalentId()); event.setCardId(card.sourceCardId()); event.setPrimaryCardId(card.primaryCardId()); event.setCardLevel(card.cardLevel()); event.setEventType(eventType); event.setRemark(trimToLength(request.remark(), 255)); event.setCreatedAt(LocalDateTime.now()); event.setDeleted(0); if (eventMapper.insert(event) != 1) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "浏览/咨询记录保存失败"); } Map result = new LinkedHashMap<>(); result.put("id", event.getId()); result.put("merchantId", event.getMerchantId()); result.put("eventType", event.getEventType()); result.put("eventTypeName", "CONSULT".equals(event.getEventType()) ? "咨询" : "浏览"); result.put("talentId", event.getTalentId()); result.put("cardId", event.getCardId()); result.put("primaryCardId", event.getPrimaryCardId()); result.put("cardLevel", event.getCardLevel()); result.put("createdAt", event.getCreatedAt()); return result; } private Map portalView(Map row, CardContext card) { Map result = new LinkedHashMap<>(); result.put("merchantId", value(row, "merchantId")); result.put("merchantName", text(row, "merchantName")); result.put("merchantType", text(row, "merchantType")); result.put("contactName", text(row, "contactName")); result.put("contactMobile", text(row, "contactMobile")); result.put("logoUrl", text(row, "logoUrl")); result.put("coverUrl", text(row, "coverUrl")); result.put("address", text(row, "address")); result.put("longitude", value(row, "longitude")); result.put("latitude", value(row, "latitude")); result.put("policyId", value(row, "benefitId")); result.put("policyText", text(row, "policyText")); result.put("benefitText", text(row, "policyText")); result.put("rightsText", text(row, "rightsText")); result.put("handlingInstructions", text(row, "handlingInstructions")); result.put("serviceLabel", text(row, "serviceLabel")); result.put("cardLevel", text(row, "cardLevel")); result.put("cardName", text(row, "cardName")); result.put("theme", text(row, "theme")); result.put("available", intValue(value(row, "status")) == 1); result.put("sourceTalentId", card.sourceTalentId()); result.put("sourceCardId", card.sourceCardId()); result.put("sourceCardType", card.sourceCardType()); result.put("primaryCardId", card.primaryCardId()); result.put("primaryCardLevel", card.cardLevel()); result.put("familyCard", card.familyCard()); result.put("policyUpdatedAt", value(row, "policyUpdatedAt")); return result; } private CardContext cardContext(Long talentId, Long cardId) { if ((talentId == null || talentId <= 0) && (cardId == null || cardId <= 0)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "人才用户ID或人才卡ID不能为空"); } BenefitTalentCard source = cardId == null ? activeCardForTalent(talentId) : activeCard(cardId, talentId); String sourceType = clean(source.getCardType()).toUpperCase(Locale.ROOT); if (sourceType.isBlank() || "PERSONAL".equals(sourceType)) { return new CardContext(source.getTalentId(), source.getId(), sourceType, source.getId(), text(source.getCardLevel()), false); } BenefitFamilyCardApplication owner = familyMapper.selectOne(new QueryWrapper() .eq("deleted", 0) .eq("family_card_id", source.getId()) .isNotNull("talent_id") .orderByDesc("id") .last("limit 1")); if (owner == null) { throw new BusinessException(ErrorCode.CONFLICT, "亲属附属卡未找到绑定的主卡"); } BenefitTalentCard primary = activePersonalCard(owner.getTalentId()); return new CardContext(source.getTalentId(), source.getId(), sourceType, primary.getId(), text(primary.getCardLevel()), true); } private BenefitTalentCard activeCard(Long cardId, Long talentId) { QueryWrapper query = activeCardQuery() .eq("id", cardId); if (talentId != null && talentId > 0) { query.eq("talent_id", talentId); } BenefitTalentCard card = cardMapper.selectOne(query.last("limit 1")); if (card == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "有效人才卡不存在"); } return card; } private BenefitTalentCard activeCardForTalent(Long talentId) { BenefitTalentCard card = cardMapper.selectOne(activeCardQuery() .eq("talent_id", talentId) .last("order by case when card_type = 'PERSONAL' then 0 else 1 end, id desc limit 1")); if (card == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "当前用户未持有有效人才卡"); } return card; } private BenefitTalentCard activePersonalCard(Long talentId) { BenefitTalentCard card = cardMapper.selectOne(activeCardQuery() .eq("talent_id", talentId) .eq("card_type", "PERSONAL") .orderByDesc("id") .last("limit 1")); if (card == null) { throw new BusinessException(ErrorCode.CONFLICT, "未找到有效主卡"); } return card; } private QueryWrapper activeCardQuery() { LocalDate today = LocalDate.now(); LocalDateTime now = LocalDateTime.now(); return new QueryWrapper() .eq("deleted", 0) .eq("status", 1) .and(wrapper -> wrapper.isNull("valid_start").or().le("valid_start", today)) .and(wrapper -> wrapper.isNull("valid_end").or().ge("valid_end", today)) .and(wrapper -> wrapper.isNull("expire_time").or().gt("expire_time", now)); } private List cardLevelAliases(String cardLevel) { String normalized = normalizeCardLevel(cardLevel); if ("EXCELLENT".equals(normalized)) { return List.of("EXCELLENT", "卓越人才卡", "卓越"); } if ("OUTSTANDING".equals(normalized)) { return List.of("OUTSTANDING", "杰出人才卡", "杰出"); } if ("ELITE".equals(normalized)) { return List.of("ELITE", "菁英人才卡", "菁英"); } return List.of(normalized); } private String normalizeCardLevel(String value) { String raw = clean(value); String upper = raw.toUpperCase(Locale.ROOT); if (upper.contains("EXCELLENT") || raw.contains("卓越")) { return "EXCELLENT"; } if (upper.contains("OUTSTANDING") || raw.contains("杰出")) { return "OUTSTANDING"; } if (upper.contains("ELITE") || raw.contains("菁英")) { return "ELITE"; } return upper; } private String normalizeEventType(String value) { String raw = clean(value); String upper = raw.toUpperCase(Locale.ROOT); if ("VIEW".equals(upper) || "浏览".equals(raw)) { return MerchantDisplayPolicyEvent.EVENT_VIEW; } if ("CONSULT".equals(upper) || "咨询".equals(raw)) { return MerchantDisplayPolicyEvent.EVENT_CONSULT; } throw new BusinessException(ErrorCode.BAD_REQUEST, "事件类型只能为VIEW浏览或CONSULT咨询"); } private Long requireMerchantId(Long merchantId) { if (merchantId == null || merchantId <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, "商户ID不能为空"); } return merchantId; } private String trimToLength(String value, int maxLength) { String text = clean(value); return text.length() <= maxLength ? text : text.substring(0, maxLength); } private Object value(Map row, String key) { return row == null ? null : row.get(key); } private String text(Map row, String key) { return text(value(row, key)); } private String text(Object value) { return value == null ? "" : String.valueOf(value).trim(); } private int intValue(Object value) { return value instanceof Number number ? number.intValue() : 0; } private String clean(String value) { return value == null ? "" : value.trim(); } private record CardContext( Long sourceTalentId, Long sourceCardId, String sourceCardType, Long primaryCardId, String cardLevel, boolean familyCard ) { } }