package com.xuqm.tenant.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.xuqm.common.exception.BusinessException; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.entity.ServiceActivationRequestEntity; import com.xuqm.tenant.entity.ServiceActivationRequestEntity.Status; import com.xuqm.tenant.repository.FeatureServiceRepository; import com.xuqm.tenant.repository.ServiceActivationRequestRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @Service public class FeatureServiceManager { private final FeatureServiceRepository repository; private final ServiceActivationRequestRepository requestRepository; private final ObjectMapper objectMapper; public FeatureServiceManager(FeatureServiceRepository repository, ServiceActivationRequestRepository requestRepository, ObjectMapper objectMapper) { this.repository = repository; this.requestRepository = requestRepository; this.objectMapper = objectMapper; } public List listByApp(String appId) { List services = repository.findByAppId(appId); if (services.isEmpty()) { return services; } List normalized = new ArrayList<>(); services.stream() .filter(service -> service.getServiceType() == FeatureServiceEntity.ServiceType.IM) .findFirst() .ifPresent(normalized::add); services.stream() .filter(service -> service.getServiceType() != FeatureServiceEntity.ServiceType.IM) .forEach(normalized::add); return normalized.isEmpty() ? services : normalized; } /** * Submit an activation request. Disabling is immediate; enabling requires ops approval. * IM is app-wide, so duplicate checks ignore platform. */ @Transactional public ServiceActivationRequestEntity submitActivationRequest( String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType, String applyReason) { if (serviceType == FeatureServiceEntity.ServiceType.IM) { requestRepository.findFirstByAppIdAndServiceTypeOrderByCreatedAtDesc(appId, serviceType) .ifPresent(req -> { if (req.getStatus() == Status.PENDING) { throw new BusinessException(400, "已有待审核的开通申请,请等待运营人员处理"); } }); } else { requestRepository.findFirstByAppIdAndPlatformAndServiceTypeOrderByCreatedAtDesc(appId, platform, serviceType) .ifPresent(req -> { if (req.getStatus() == Status.PENDING) { throw new BusinessException(400, "已有待审核的开通申请,请等待运营人员处理"); } }); } ServiceActivationRequestEntity req = new ServiceActivationRequestEntity(); req.setId(UUID.randomUUID().toString()); req.setAppId(appId); req.setPlatform(platform); req.setServiceType(serviceType); req.setStatus(Status.PENDING); req.setApplyReason(applyReason); req.setCreatedAt(LocalDateTime.now()); return requestRepository.save(req); } /** * Disable a service immediately (no approval needed). */ @Transactional public FeatureServiceEntity disable(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { if (serviceType == FeatureServiceEntity.ServiceType.IM) { List services = repository.findByAppIdAndServiceType(appId, serviceType); if (services.isEmpty()) { throw new BusinessException(404, "服务未开通"); } services.forEach(service -> service.setEnabled(false)); repository.saveAll(services); return services.get(0); } FeatureServiceEntity entity = repository .findByAppIdAndPlatformAndServiceType(appId, platform, serviceType) .orElseThrow(() -> new BusinessException(404, "服务未开通")); entity.setEnabled(false); return repository.save(entity); } /** * Called by ops when approving an activation request. */ @Transactional public ServiceActivationRequestEntity approveRequest(String requestId, String reviewNote) { ServiceActivationRequestEntity req = requestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(404, "申请不存在")); if (req.getStatus() != Status.PENDING) { throw new BusinessException(400, "申请已处理"); } req.setStatus(Status.APPROVED); req.setReviewNote(reviewNote); req.setReviewedAt(LocalDateTime.now()); requestRepository.save(req); if (req.getServiceType() == FeatureServiceEntity.ServiceType.IM) { List services = repository.findByAppIdAndServiceType(req.getAppId(), req.getServiceType()); if (services.isEmpty()) { FeatureServiceEntity created = new FeatureServiceEntity(); created.setId(UUID.randomUUID().toString()); created.setAppId(req.getAppId()); created.setPlatform(req.getPlatform()); created.setServiceType(req.getServiceType()); created.setEnabled(true); created.setCreatedAt(LocalDateTime.now()); repository.save(created); } else { services.forEach(service -> service.setEnabled(true)); repository.saveAll(services); } return req; } FeatureServiceEntity entity = repository .findByAppIdAndPlatformAndServiceType(req.getAppId(), req.getPlatform(), req.getServiceType()) .orElseGet(() -> { FeatureServiceEntity e = new FeatureServiceEntity(); e.setId(UUID.randomUUID().toString()); e.setAppId(req.getAppId()); e.setPlatform(req.getPlatform()); e.setServiceType(req.getServiceType()); e.setCreatedAt(LocalDateTime.now()); return e; }); entity.setEnabled(true); repository.save(entity); return req; } /** * Called by ops when rejecting an activation request. */ @Transactional public ServiceActivationRequestEntity rejectRequest(String requestId, String reviewNote) { ServiceActivationRequestEntity req = requestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(404, "申请不存在")); if (req.getStatus() != Status.PENDING) { throw new BusinessException(400, "申请已处理"); } req.setStatus(Status.REJECTED); req.setReviewNote(reviewNote); req.setReviewedAt(LocalDateTime.now()); return requestRepository.save(req); } public List listRequestsByApp(String appId) { return requestRepository.findByAppIdOrderByCreatedAtDesc(appId); } public FeatureServiceEntity getOrFail(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { if (serviceType == FeatureServiceEntity.ServiceType.IM) { return repository.findByAppIdAndServiceType(appId, serviceType) .stream() .findFirst() .orElseThrow(() -> new BusinessException(404, "服务未配置")); } return repository.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType) .orElseThrow(() -> new BusinessException(404, "服务未配置")); } @Transactional public FeatureServiceEntity updateConfig(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType, String config) { if (serviceType == FeatureServiceEntity.ServiceType.IM) { List services = repository.findByAppIdAndServiceType(appId, serviceType); if (services.isEmpty()) { throw new BusinessException(404, "服务未配置"); } services.forEach(service -> service.setConfig(config)); repository.saveAll(services); return services.get(0); } FeatureServiceEntity entity = getOrFail(appId, platform, serviceType); entity.setConfig(config); return repository.save(entity); } public boolean allowStrangerMessage(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { return readConfigNode(appId, platform, serviceType).path("allowStrangerMessage").asBoolean(false); } public boolean allowFriendRequest(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { return readConfigNode(appId, platform, serviceType).path("allowFriendRequest").asBoolean(true); } public boolean allowGroupJoinRequest(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { return readConfigNode(appId, platform, serviceType).path("allowGroupJoinRequest").asBoolean(true); } public String friendRequestMode(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { String mode = readConfigNode(appId, platform, serviceType).path("friendRequestMode").asText(""); return switch (mode == null ? "" : mode.trim().toUpperCase()) { case "DIRECT_ACCEPT", "DISALLOW" -> mode.trim().toUpperCase(); default -> "REQUIRE_CONFIRM"; }; } public boolean blacklistSendSuccess(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { return readConfigNode(appId, platform, serviceType).path("blacklistSendSuccess").asBoolean(true); } public int messageRecallMinutes(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { int minutes = readConfigNode(appId, platform, serviceType).path("messageRecallMinutes").asInt(2); return Math.max(minutes, 0); } public int conversationPullLimit(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { int limit = readConfigNode(appId, platform, serviceType).path("conversationPullLimit").asInt(100); return Math.min(Math.max(limit, 1), 500); } public int historyRetentionDays(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { int days = readConfigNode(appId, platform, serviceType).path("historyRetentionDays").asInt(7); return Math.max(days, 1); } public String buildImConfig(String appId, FeatureServiceEntity.Platform platform, Boolean allowStrangerMessage, Boolean allowFriendRequest, String friendRequestMode, Boolean allowGroupJoinRequest, Boolean blacklistSendSuccess, Integer messageRecallMinutes, Integer historyRetentionDays, Integer conversationPullLimit, Boolean multiClientConversationDeleteSync) { ObjectNode node = readConfigNode(appId, platform, FeatureServiceEntity.ServiceType.IM).deepCopy(); if (!node.has("allowStrangerMessage")) { node.put("allowStrangerMessage", false); } if (!node.has("allowFriendRequest")) { node.put("allowFriendRequest", true); } if (!node.has("friendRequestMode")) { node.put("friendRequestMode", "REQUIRE_CONFIRM"); } if (!node.has("allowGroupJoinRequest")) { node.put("allowGroupJoinRequest", true); } if (!node.has("blacklistSendSuccess")) { node.put("blacklistSendSuccess", true); } if (!node.has("messageRecallMinutes")) { node.put("messageRecallMinutes", 2); } if (!node.has("historyRetentionDays")) { node.put("historyRetentionDays", 7); } if (!node.has("conversationPullLimit")) { node.put("conversationPullLimit", 100); } if (!node.has("multiClientConversationDeleteSync")) { node.put("multiClientConversationDeleteSync", false); } if (allowStrangerMessage != null) { node.put("allowStrangerMessage", allowStrangerMessage); } if (allowFriendRequest != null) { node.put("allowFriendRequest", allowFriendRequest); } String effectiveFriendRequestMode = null; if (friendRequestMode != null && !friendRequestMode.isBlank()) { effectiveFriendRequestMode = normalizeFriendRequestMode(friendRequestMode); } else if (allowFriendRequest != null && !allowFriendRequest) { effectiveFriendRequestMode = "DISALLOW"; } if (effectiveFriendRequestMode != null) { node.put("friendRequestMode", effectiveFriendRequestMode); if ("DISALLOW".equals(effectiveFriendRequestMode)) { node.put("allowFriendRequest", false); } } if (allowGroupJoinRequest != null) { node.put("allowGroupJoinRequest", allowGroupJoinRequest); } if (blacklistSendSuccess != null) { node.put("blacklistSendSuccess", blacklistSendSuccess); } if (messageRecallMinutes != null) { node.put("messageRecallMinutes", Math.max(messageRecallMinutes, 0)); } if (historyRetentionDays != null) { node.put("historyRetentionDays", Math.max(historyRetentionDays, 1)); } if (conversationPullLimit != null) { node.put("conversationPullLimit", Math.min(Math.max(conversationPullLimit, 1), 500)); } if (multiClientConversationDeleteSync != null) { node.put("multiClientConversationDeleteSync", multiClientConversationDeleteSync); } return node.toString(); } public String buildAllowStrangerConfig(boolean allowStrangerMessage) { ObjectNode node = objectMapper.createObjectNode(); node.put("allowStrangerMessage", allowStrangerMessage); node.put("allowFriendRequest", true); node.put("friendRequestMode", "REQUIRE_CONFIRM"); node.put("allowGroupJoinRequest", true); node.put("blacklistSendSuccess", true); node.put("messageRecallMinutes", 2); node.put("historyRetentionDays", 7); node.put("conversationPullLimit", 100); node.put("multiClientConversationDeleteSync", false); return node.toString(); } private String normalizeFriendRequestMode(String mode) { String normalized = mode == null ? "" : mode.trim().toUpperCase(); return switch (normalized) { case "DIRECT_ACCEPT", "DISALLOW" -> normalized; default -> "REQUIRE_CONFIRM"; }; } private JsonNode readConfigNode(String appId, FeatureServiceEntity.Platform platform, FeatureServiceEntity.ServiceType serviceType) { FeatureServiceEntity entity; if (serviceType == FeatureServiceEntity.ServiceType.IM) { entity = repository.findByAppIdAndServiceType(appId, serviceType) .stream() .findFirst() .orElse(null); } else { entity = repository.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType) .orElse(null); } if (entity == null || entity.getConfig() == null || entity.getConfig().isBlank()) { return objectMapper.createObjectNode(); } try { JsonNode node = objectMapper.readTree(entity.getConfig()); return node == null ? objectMapper.createObjectNode() : node; } catch (Exception e) { return objectMapper.createObjectNode(); } } }