package com.talentcard.server.service; import java.net.URI; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; 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.TransportCarWashService; import com.talentcard.server.mapper.TransportCarWashServiceMapper; import com.talentcard.server.request.CarWashJumpConfigRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class CarWashJumpConfigAdminService { private final TransportCarWashServiceMapper serviceMapper; public CarWashJumpConfigAdminService(TransportCarWashServiceMapper serviceMapper) { this.serviceMapper = serviceMapper; } @Transactional(readOnly = true) public PageResult> page(Long merchantId, String keyword, int page, int size) { int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); String cleanedKeyword = clean(keyword); var records = serviceMapper.selectConfigPage(merchantId, cleanedKeyword, safeSize, (safePage - 1) * safeSize) .stream() .map(this::view) .toList(); Long total = serviceMapper.countConfigPage(merchantId, cleanedKeyword); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional(readOnly = true) public Map detail(Long merchantId) { Map row = serviceMapper.selectConfigDetail(requireMerchantId(merchantId)); if (row == null || row.isEmpty()) { throw new BusinessException(ErrorCode.NOT_FOUND, "洗车商家不存在"); } return view(row); } @Transactional public Map save(Long merchantId, CarWashJumpConfigRequest request) { Long id = requireMerchantId(merchantId); Long merchantCount = serviceMapper.countCarWashMerchant(id); if (merchantCount == null || merchantCount == 0) { throw new BusinessException(ErrorCode.NOT_FOUND, "洗车商家不存在"); } JumpConfig config = validate(request); TransportCarWashService current = serviceMapper.selectOne(new QueryWrapper() .eq("merchant_id", id) .eq("deleted", 0) .last("limit 1 for update")); if (current == null) { current = new TransportCarWashService(); current.setMerchantId(id); current.setTalentLevels("卓越人才卡、杰出人才卡、菁英人才卡"); current.setServicePointText("请咨询商户"); current.setTipText("到店后按现场指引操作,可直接享受洗车权益。"); current.setDeleted(0); apply(current, config, request.status()); serviceMapper.insert(current); } else { int updated = serviceMapper.update(null, new UpdateWrapper() .set("external_link_type", config.type()) .set("external_mini_program_app_id", config.appId()) .set("external_mini_program_path", config.path()) .set("external_web_url", config.webUrl()) .set("status", normalizeStatus(request.status())) .eq("id", current.getId()) .eq("deleted", 0)); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "洗车跳转配置已变化,请刷新后重试"); } } return detail(id); } private void apply(TransportCarWashService service, JumpConfig config, Integer status) { service.setExternalLinkType(config.type()); service.setExternalMiniProgramAppId(config.appId()); service.setExternalMiniProgramPath(config.path()); service.setExternalWebUrl(config.webUrl()); service.setStatus(normalizeStatus(status)); } private JumpConfig validate(CarWashJumpConfigRequest request) { String type = normalizeType(request.externalLinkType()); if ("miniProgram".equals(type)) { String appId = clean(request.externalMiniProgramAppId()); if (!appId.matches("^wx[a-zA-Z0-9]{16}$")) { throw new BusinessException(ErrorCode.BAD_REQUEST, "第三方小程序AppId格式不正确"); } return new JumpConfig(type, appId, clean(request.externalMiniProgramPath()), ""); } if ("web".equals(type)) { String webUrl = clean(request.externalWebUrl()); if (!validHttpsUrl(webUrl)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "第三方网页地址必须是有效的HTTPS地址"); } return new JumpConfig(type, "", "", webUrl); } return new JumpConfig("", "", "", ""); } private String normalizeType(String raw) { String value = clean(raw).toLowerCase(Locale.ROOT).replace("_", "").replace("-", ""); return switch (value) { case "miniprogram", "mini", "wechatminiprogram", "小程序" -> "miniProgram"; case "web", "h5", "网页" -> "web"; case "none", "no", "off", "关闭", "不跳转" -> ""; default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "跳转类型仅支持第三方小程序、网页或不跳转"); }; } private boolean validHttpsUrl(String value) { if (value.isBlank()) { return false; } try { URI uri = URI.create(value); return "https".equalsIgnoreCase(uri.getScheme()) && uri.getHost() != null && uri.getUserInfo() == null; } catch (IllegalArgumentException exception) { return false; } } private int normalizeStatus(Integer status) { if (status == null) { return 1; } if (status != 0 && status != 1) { throw new BusinessException(ErrorCode.BAD_REQUEST, "服务状态只能为0停用或1启用"); } return status; } private Long requireMerchantId(Long merchantId) { if (merchantId == null || merchantId <= 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, "洗车商家ID不能为空"); } return merchantId; } private Map view(Map row) { Map result = new LinkedHashMap<>(); result.put("merchantId", value(row, "merchantId", "merchant_id")); result.put("merchantName", text(row, "merchantName", "merchant_name")); result.put("contactMobile", text(row, "contactMobile", "contact_mobile")); result.put("address", text(row, "address")); result.put("coverUrl", text(row, "coverUrl", "cover_url")); result.put("merchantStatus", intValue(row, "merchantStatus", "merchant_status")); result.put("configId", value(row, "configId", "config_id")); result.put("externalLinkType", text(row, "externalLinkType", "external_link_type")); result.put("externalMiniProgramAppId", text(row, "externalMiniProgramAppId", "external_mini_program_app_id")); result.put("externalMiniProgramPath", text(row, "externalMiniProgramPath", "external_mini_program_path")); result.put("externalWebUrl", text(row, "externalWebUrl", "external_web_url")); result.put("serviceStatus", intValue(row, "serviceStatus", "service_status")); result.put("updatedAt", value(row, "updatedAt", "updated_at")); return result; } private Object value(Map row, String... keys) { for (String key : keys) { if (row.containsKey(key)) { return row.get(key); } } return null; } private String text(Map row, String... keys) { Object value = value(row, keys); return value == null ? "" : String.valueOf(value).trim(); } private int intValue(Map row, String... keys) { Object value = value(row, keys); return value instanceof Number number ? number.intValue() : 0; } private String clean(String value) { return value == null ? "" : value.trim(); } private record JumpConfig(String type, String appId, String path, String webUrl) { } }