package com.talentcard.server.service; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.talentcard.server.business.PageResult; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.entity.MerchantCardBenefit; import com.talentcard.server.entity.MerchantDisplayPolicyMerchant; import com.talentcard.server.entity.ShoppingTopicMerchant; import com.talentcard.server.mapper.DisplayServiceMapper; import com.talentcard.server.mapper.MerchantCardBenefitMapper; import com.talentcard.server.mapper.ShoppingTopicMerchantMapper; import com.talentcard.server.request.DisplayServiceSaveRequest; import com.talentcard.server.request.DisplayServiceSortBatchRequest; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DisplayServiceAdminService { private static final List CATEGORIES = List.of( new Category("parking", "停车"), new Category("medical", "体检"), new Category("housing", "购房"), new Category("car", "购车"), new Category("appliance", "家电"), new Category("apartment", "公寓租赁") ); private static final Set CATEGORY_KEYS = Set.of("parking", "medical", "housing", "car", "appliance", "apartment"); private static final Set SHOPPING_CATEGORY_KEYS = Set.of("housing", "car", "appliance"); private static final Map CATEGORY_MERCHANT_TYPES = Map.of( "parking", "parking", "medical", "medical", "apartment", "apartment", "housing", "shopping", "car", "shopping", "appliance", "shopping" ); private final DisplayServiceMapper serviceMapper; private final MerchantCardBenefitMapper benefitMapper; private final ShoppingTopicMerchantMapper shoppingMapper; public DisplayServiceAdminService( DisplayServiceMapper serviceMapper, MerchantCardBenefitMapper benefitMapper, ShoppingTopicMerchantMapper shoppingMapper ) { this.serviceMapper = serviceMapper; this.benefitMapper = benefitMapper; this.shoppingMapper = shoppingMapper; } @Transactional(readOnly = true) public List> categories() { return CATEGORIES.stream().map(category -> { Map result = new LinkedHashMap<>(); result.put("key", category.key()); result.put("name", category.name()); return result; }).toList(); } @Transactional(readOnly = true) public PageResult> page(String category, String keyword, Integer status, int page, int size) { validateCategoryFilter(category); validateStatus(status, "上下架状态只能为1上架或0下架"); int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); List> records = serviceMapper.selectAdminPage( clean(category), clean(keyword), status, safeSize, (safePage - 1) * safeSize ).stream() .map(this::view) .toList(); Long total = serviceMapper.countAdminPage(clean(category), clean(keyword), status); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional(readOnly = true) public Map detail(Long serviceId) { Long id = requireId(serviceId, "展示服务ID不能为空"); Map row = serviceMapper.selectAdminDetail(id); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "展示服务不存在"); } List> policies = benefitMapper.selectList(new QueryWrapper() .eq("merchant_id", id) .orderByDesc("sort_no") .orderByDesc("id")) .stream() .map(this::policyView) .toList(); Map result = view(row); result.put("cardPolicies", policies); return result; } @Transactional public Map create(DisplayServiceSaveRequest request) { String category = validateCategory(request.category()); String merchantType = CATEGORY_MERCHANT_TYPES.get(category); MerchantDisplayPolicyMerchant merchant = new MerchantDisplayPolicyMerchant(); applyMerchant(merchant, request, merchantType); merchant.setDeleted(0); serviceMapper.insert(merchant); if (SHOPPING_CATEGORY_KEYS.contains(category)) { upsertShoppingMapping(merchant.getId(), category, merchant.getSortNo()); } upsertCardPolicies(merchant.getId(), request.cardPolicies()); return detail(merchant.getId()); } @Transactional public Map update(Long serviceId, DisplayServiceSaveRequest request) { Long id = requireId(serviceId, "展示服务ID不能为空"); requireMerchant(id); String category = validateCategory(request.category()); String merchantType = CATEGORY_MERCHANT_TYPES.get(category); int sortNo = request.sortNo() == null ? 0 : request.sortNo(); int status = request.status() == null ? 1 : request.status(); validateStatus(status, "上下架状态只能为1上架或0下架"); int updated = serviceMapper.update(null, new UpdateWrapper() .set("merchant_name", cleanRequired(request.merchantName(), "展示服务名称不能为空")) .set("merchant_type", merchantType) .set("contact_name", clean(request.contactName())) .set("contact_mobile", clean(request.contactMobile())) .set("logo_url", clean(request.logoUrl())) .set("cover_url", clean(request.coverUrl())) .set("address", clean(request.address())) .set("service_label", clean(request.serviceLabel())) .set("rights_text", clean(request.rightsText())) .set("usage_instructions", clean(request.handlingInstructions())) .set("sort_no", sortNo) .set("status", status) .set("recommend", request.recommend() == null ? 0 : request.recommend()) .eq("id", id) .eq("deleted", 0)); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "展示服务信息已变更,请刷新后重试"); } if (SHOPPING_CATEGORY_KEYS.contains(category)) { upsertShoppingMapping(id, category, sortNo); } else { removeShoppingMapping(id); } upsertCardPolicies(id, request.cardPolicies()); return detail(id); } @Transactional public Map updateStatus(Long serviceId, Integer status) { Long id = requireId(serviceId, "展示服务ID不能为空"); requireMerchant(id); validateStatus(status, "上下架状态只能为1上架或0下架"); int updated = serviceMapper.update(null, new UpdateWrapper() .set("status", status) .eq("id", id) .eq("deleted", 0)); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "展示服务状态已变更,请刷新后重试"); } return detail(id); } @Transactional public Map updateSort(Long serviceId, Integer sortNo) { Long id = requireId(serviceId, "展示服务ID不能为空"); requireMerchant(id); int sort = sortNo == null ? 0 : sortNo; int updated = serviceMapper.update(null, new UpdateWrapper() .set("sort_no", sort) .eq("id", id) .eq("deleted", 0)); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "展示服务状态已变更,请刷新后重试"); } shoppingMapper.update(null, new UpdateWrapper() .set("sort_no", sort) .eq("merchant_id", id) .eq("deleted", 0)); return detail(id); } @Transactional public Map updateSortBatch(DisplayServiceSortBatchRequest request) { if (request.items() == null || request.items().isEmpty()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "排序项列表不能为空"); } int updatedCount = 0; for (DisplayServiceSortBatchRequest.SortItem item : request.items()) { Long id = requireId(item.serviceId(), "展示服务ID不能为空"); requireMerchant(id); int sort = item.sortNo() == null ? 0 : item.sortNo(); int updated = serviceMapper.update(null, new UpdateWrapper() .set("sort_no", sort) .eq("id", id) .eq("deleted", 0)); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "展示服务状态已变更,请刷新后重试"); } shoppingMapper.update(null, new UpdateWrapper() .set("sort_no", sort) .eq("merchant_id", id) .eq("deleted", 0)); updatedCount++; } Map result = new LinkedHashMap<>(); result.put("updated", updatedCount); return result; } private void applyMerchant(MerchantDisplayPolicyMerchant merchant, DisplayServiceSaveRequest request, String merchantType) { merchant.setMerchantName(cleanRequired(request.merchantName(), "展示服务名称不能为空")); merchant.setMerchantType(merchantType); merchant.setContactName(clean(request.contactName())); merchant.setContactMobile(clean(request.contactMobile())); merchant.setLogoUrl(clean(request.logoUrl())); merchant.setCoverUrl(clean(request.coverUrl())); merchant.setAddress(clean(request.address())); merchant.setServiceLabel(clean(request.serviceLabel())); merchant.setRightsText(clean(request.rightsText())); merchant.setUsageInstructions(clean(request.handlingInstructions())); merchant.setSortNo(request.sortNo() == null ? 0 : request.sortNo()); int status = request.status() == null ? 1 : request.status(); validateStatus(status, "上下架状态只能为1上架或0下架"); merchant.setStatus(status); merchant.setRecommend(request.recommend() == null ? 0 : request.recommend()); } private void upsertShoppingMapping(Long merchantId, String categoryKey, int sortNo) { ShoppingTopicMerchant mapping = shoppingMapper.selectOne(new QueryWrapper() .eq("merchant_id", merchantId) .last("limit 1")); if (mapping == null) { ShoppingTopicMerchant entity = new ShoppingTopicMerchant(); entity.setMerchantId(merchantId); entity.setCategoryKey(categoryKey); entity.setSortNo(sortNo); entity.setDeleted(0); shoppingMapper.insert(entity); return; } int updated = shoppingMapper.update(null, new UpdateWrapper() .set("category_key", categoryKey) .set("sort_no", sortNo) .set("deleted", 0) .eq("id", mapping.getId())); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "服务分类映射已变更,请刷新后重试"); } } private void removeShoppingMapping(Long merchantId) { shoppingMapper.update(null, new UpdateWrapper() .set("deleted", 1) .eq("merchant_id", merchantId) .eq("deleted", 0)); } private void upsertCardPolicies(Long merchantId, List policies) { if (policies == null || policies.isEmpty()) { return; } if (policies.stream().anyMatch(Objects::isNull)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡级别政策不能为空"); } Set levels = new LinkedHashSet<>(); for (DisplayServiceSaveRequest.CardPolicy policy : policies) { String level = normalizeCardLevel(policy.cardLevel()); if (level.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡级别不能为空"); } levels.add(level); } if (levels.size() != policies.size()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "同一服务不能包含重复的人才卡级别"); } for (DisplayServiceSaveRequest.CardPolicy policy : policies) { saveCardPolicy(merchantId, policy); } } private void saveCardPolicy(Long merchantId, DisplayServiceSaveRequest.CardPolicy request) { String cardLevel = normalizeCardLevel(request.cardLevel()); String benefitText = clean(request.benefitText()); if (benefitText.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠政策内容不能为空"); } int status = request.status() == null ? 1 : request.status(); validateStatus(status, "政策状态只能为1启用或0停用"); MerchantCardBenefit current = benefitMapper.selectOne(new QueryWrapper() .eq("merchant_id", merchantId) .eq("card_level", cardLevel) .last("limit 1")); if (current == null) { MerchantCardBenefit benefit = new MerchantCardBenefit(); benefit.setMerchantId(merchantId); benefit.setCardLevel(cardLevel); benefit.setCardName(firstNonBlank(request.cardName(), defaultCardName(cardLevel))); benefit.setBenefitText(benefitText); benefit.setTheme(firstNonBlank(request.theme(), defaultTheme(cardLevel))); benefit.setSortNo(request.sortNo() == null ? defaultSortNo(cardLevel) : request.sortNo()); benefit.setStatus(status); try { benefitMapper.insert(benefit); } catch (DuplicateKeyException exception) { throw new BusinessException(ErrorCode.CONFLICT, "该服务的人才卡级别政策已存在,请刷新后重试"); } return; } int updated = benefitMapper.update(null, new UpdateWrapper() .set("card_name", firstNonBlank(request.cardName(), defaultCardName(cardLevel))) .set("benefit_text", benefitText) .set("theme", firstNonBlank(request.theme(), defaultTheme(cardLevel))) .set("sort_no", request.sortNo() == null ? current.getSortNo() : request.sortNo()) .set("status", status) .eq("id", current.getId())); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "政策配置已变更,请刷新后重试"); } } private MerchantDisplayPolicyMerchant requireMerchant(Long id) { MerchantDisplayPolicyMerchant merchant = serviceMapper.selectOne(new QueryWrapper() .eq("id", id) .eq("deleted", 0) .last("limit 1")); if (merchant == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "展示服务不存在"); } return merchant; } private Map view(Map row) { Map result = new LinkedHashMap<>(); result.put("serviceId", value(row, "merchantId")); result.put("merchantId", value(row, "merchantId")); result.put("serviceName", text(row, "merchantName")); result.put("merchantName", text(row, "merchantName")); result.put("category", text(row, "category")); result.put("categoryName", categoryName(text(row, "category"))); 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("businessStatus", text(row, "businessStatus")); result.put("longitude", value(row, "longitude")); result.put("latitude", value(row, "latitude")); result.put("serviceLabel", text(row, "serviceLabel")); result.put("rightsText", text(row, "rightsText")); result.put("handlingInstructions", text(row, "handlingInstructions")); result.put("recommend", intValue(value(row, "recommend"))); result.put("sortNo", intValue(value(row, "sortNo"))); result.put("status", intValue(value(row, "status"))); result.put("statusName", intValue(value(row, "status")) == 1 ? "上架" : "下架"); result.put("policyCount", intValue(value(row, "policyCount"))); result.put("cardPolicies", List.of()); result.put("createdAt", value(row, "createdAt")); result.put("updatedAt", value(row, "updatedAt")); return result; } private Map policyView(MerchantCardBenefit benefit) { Map result = new LinkedHashMap<>(); result.put("id", benefit.getId()); result.put("cardLevel", benefit.getCardLevel()); result.put("cardName", benefit.getCardName()); result.put("benefitText", benefit.getBenefitText()); result.put("theme", benefit.getTheme()); result.put("sortNo", benefit.getSortNo()); result.put("status", benefit.getStatus()); result.put("statusName", intValue(benefit.getStatus()) == 1 ? "启用" : "停用"); result.put("createdAt", benefit.getCreatedAt()); result.put("updatedAt", benefit.getUpdatedAt()); return result; } private String validateCategory(String category) { String value = clean(category); if (!CATEGORY_KEYS.contains(value)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "服务分类不正确,仅支持停车、体检、购房、购车、家电、公寓租赁"); } return value; } private void validateCategoryFilter(String category) { String value = clean(category); if (!value.isBlank() && !CATEGORY_KEYS.contains(value)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "服务分类不正确,仅支持停车、体检、购房、购车、家电、公寓租赁"); } } private void validateStatus(Integer status, String message) { if (status != null && status != 0 && status != 1) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } } private String categoryName(String key) { return CATEGORIES.stream() .filter(category -> category.key().equals(key)) .map(Category::name) .findFirst() .orElse(key); } 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 defaultCardName(String cardLevel) { return switch (cardLevel) { case "EXCELLENT" -> "卓越人才卡"; case "OUTSTANDING" -> "杰出人才卡"; case "ELITE" -> "精英人才卡"; default -> cardLevel; }; } private String defaultTheme(String cardLevel) { return switch (cardLevel) { case "EXCELLENT" -> "excellent"; case "OUTSTANDING" -> "outstanding"; case "ELITE" -> "elite"; default -> "default"; }; } private int defaultSortNo(String cardLevel) { return switch (cardLevel) { case "EXCELLENT" -> 30; case "OUTSTANDING" -> 20; case "ELITE" -> 10; default -> 0; }; } private Long requireId(Long id, String message) { if (id == null || id <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } return id; } private String cleanRequired(String value, String message) { String text = clean(value); if (text.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, message); } return text; } private Object value(Map row, String key) { return row == null ? null : row.get(key); } private String text(Map row, String key) { Object value = value(row, key); return value == null ? "" : String.valueOf(value).trim(); } private int intValue(Object value) { return value instanceof Number number ? number.intValue() : 0; } private String firstNonBlank(String first, String fallback) { String value = clean(first); return value.isBlank() ? fallback : value; } private String clean(String value) { return value == null ? "" : value.trim(); } private record Category(String key, String name) { } }