XuqmGroup-Server/tenant-service/src/main/java/com/xuqm/tenant/service/FeatureServiceManager.java
XuqmGroup 32b0e49e61 docs(deploy): 添加生产环境部署配置示例和部署文档
- 新增 .env.production.example 环境变量配置模板
- 添加 compose.production.yaml Docker Compose 部署配置
- 创建 web.Dockerfile 前端构建部署文件
- 编写详细的 README.md 部署文档,涵盖架构、配置、步骤等内容
- 添加离线推送架构设计文档
- 更新 IM 多平台进度跟踪文档
2026-04-30 09:49:05 +08:00

590 行
26 KiB
Java

package com.xuqm.tenant.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
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<FeatureServiceEntity> listByApp(String appId) {
List<FeatureServiceEntity> services = repository.findByAppId(appId);
if (services.isEmpty()) {
return services;
}
List<FeatureServiceEntity> normalized = new ArrayList<>();
for (FeatureServiceEntity.ServiceType serviceType : List.of(
FeatureServiceEntity.ServiceType.IM,
FeatureServiceEntity.ServiceType.PUSH,
FeatureServiceEntity.ServiceType.UPDATE)) {
services.stream()
.filter(service -> service.getServiceType() == serviceType)
.findFirst()
.ifPresent(normalized::add);
}
return normalized.isEmpty() ? services : normalized;
}
/**
* Submit an activation request. Disabling is immediate; enabling requires ops approval.
* IM / PUSH / UPDATE are app-wide, so duplicate checks ignore platform.
*/
@Transactional
public ServiceActivationRequestEntity submitActivationRequest(
String appId,
FeatureServiceEntity.Platform platform,
FeatureServiceEntity.ServiceType serviceType,
String applyReason) {
if (isAppWideService(serviceType)) {
requestRepository.findFirstByAppIdAndServiceTypeOrderByCreatedAtDesc(appId, 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 (isAppWideService(serviceType)) {
List<FeatureServiceEntity> 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 (isAppWideService(req.getServiceType())) {
List<FeatureServiceEntity> services = repository.findByAppIdAndServiceType(req.getAppId(), req.getServiceType());
if (services.isEmpty()) {
for (FeatureServiceEntity.Platform platform : FeatureServiceEntity.Platform.values()) {
FeatureServiceEntity created = new FeatureServiceEntity();
created.setId(UUID.randomUUID().toString());
created.setAppId(req.getAppId());
created.setPlatform(platform);
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<ServiceActivationRequestEntity> 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<FeatureServiceEntity> 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 FeatureServiceEntity getByPlatform(String appId,
FeatureServiceEntity.Platform platform,
FeatureServiceEntity.ServiceType serviceType) {
return getOrFail(appId, platform, serviceType);
}
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();
}
public String buildUpdateConfig(String appId,
FeatureServiceEntity.Platform platform,
List<String> defaultStoreTargets,
String defaultPublishMode,
Boolean defaultPublishImmediately,
String defaultScheduledPublishAt,
Boolean defaultAutoPublishAfterReview,
String defaultWebhookUrl,
Boolean defaultForceUpdate,
Boolean defaultGrayEnabled,
Integer defaultGrayPercent,
String defaultPackageName,
String defaultAppStoreUrl,
String defaultMarketUrl) {
ObjectNode node = readConfigNode(appId, platform, FeatureServiceEntity.ServiceType.UPDATE).deepCopy();
if (!node.has("defaultStoreTargets")) {
node.putArray("defaultStoreTargets");
}
if (!node.has("defaultPublishMode")) {
node.put("defaultPublishMode", "MANUAL");
}
if (!node.has("defaultPublishImmediately")) {
node.put("defaultPublishImmediately", false);
}
if (!node.has("defaultScheduledPublishAt")) {
node.put("defaultScheduledPublishAt", "");
}
if (!node.has("defaultAutoPublishAfterReview")) {
node.put("defaultAutoPublishAfterReview", false);
}
if (!node.has("defaultWebhookUrl")) {
node.put("defaultWebhookUrl", "");
}
if (!node.has("defaultForceUpdate")) {
node.put("defaultForceUpdate", false);
}
if (!node.has("defaultGrayEnabled")) {
node.put("defaultGrayEnabled", false);
}
if (!node.has("defaultGrayPercent")) {
node.put("defaultGrayPercent", 0);
}
if (!node.has("defaultPackageName")) {
node.put("defaultPackageName", "");
}
if (!node.has("defaultAppStoreUrl")) {
node.put("defaultAppStoreUrl", "");
}
if (!node.has("defaultMarketUrl")) {
node.put("defaultMarketUrl", "");
}
if (defaultStoreTargets != null) {
node.remove("defaultStoreTargets");
ArrayNode array = node.putArray("defaultStoreTargets");
defaultStoreTargets.stream()
.filter(v -> v != null && !v.isBlank())
.map(v -> v.trim().toUpperCase())
.forEach(array::add);
}
if (defaultPublishMode != null && !defaultPublishMode.isBlank()) {
node.put("defaultPublishMode", normalizeReleaseMode(defaultPublishMode));
}
if (defaultPublishImmediately != null) {
node.put("defaultPublishImmediately", defaultPublishImmediately);
}
if (defaultScheduledPublishAt != null) {
node.put("defaultScheduledPublishAt", defaultScheduledPublishAt.trim());
}
if (defaultAutoPublishAfterReview != null) {
node.put("defaultAutoPublishAfterReview", defaultAutoPublishAfterReview);
}
if (defaultWebhookUrl != null) {
node.put("defaultWebhookUrl", defaultWebhookUrl.trim());
}
if (defaultForceUpdate != null) {
node.put("defaultForceUpdate", defaultForceUpdate);
}
if (defaultGrayEnabled != null) {
node.put("defaultGrayEnabled", defaultGrayEnabled);
}
if (defaultGrayPercent != null) {
node.put("defaultGrayPercent", Math.min(Math.max(defaultGrayPercent, 0), 100));
}
if (defaultPackageName != null) {
node.put("defaultPackageName", defaultPackageName.trim());
}
if (defaultAppStoreUrl != null) {
node.put("defaultAppStoreUrl", defaultAppStoreUrl.trim());
}
if (defaultMarketUrl != null) {
node.put("defaultMarketUrl", defaultMarketUrl.trim());
}
return node.toString();
}
public String buildPushConfig(String appId,
FeatureServiceEntity.Platform platform,
String huaweiAppId,
String huaweiAppSecret,
String xiaomiAppId,
String xiaomiAppKey,
String xiaomiAppSecret,
String oppoAppId,
String oppoAppKey,
String oppoMasterSecret,
String vivoAppId,
String vivoAppKey,
String vivoAppSecret,
String honorAppId,
String honorClientId,
String honorClientSecret,
String apnsTeamId,
String apnsKeyId,
String apnsBundleId,
String apnsKeyPath,
boolean apnsSandbox,
String fcmServiceAccountJson) {
ObjectNode node = readConfigNode(appId, platform, FeatureServiceEntity.ServiceType.PUSH).deepCopy();
ensureObjectNode(node, "huawei");
ensureObjectNode(node, "xiaomi");
ensureObjectNode(node, "oppo");
ensureObjectNode(node, "vivo");
ensureObjectNode(node, "honor");
ensureObjectNode(node, "apns");
ensureObjectNode(node, "fcm");
putText(node.with("huawei"), "appId", huaweiAppId);
putText(node.with("huawei"), "appSecret", huaweiAppSecret);
putText(node.with("xiaomi"), "appId", xiaomiAppId);
putText(node.with("xiaomi"), "appKey", xiaomiAppKey);
putText(node.with("xiaomi"), "appSecret", xiaomiAppSecret);
putText(node.with("oppo"), "appId", oppoAppId);
putText(node.with("oppo"), "appKey", oppoAppKey);
putText(node.with("oppo"), "masterSecret", oppoMasterSecret);
putText(node.with("vivo"), "appId", vivoAppId);
putText(node.with("vivo"), "appKey", vivoAppKey);
putText(node.with("vivo"), "appSecret", vivoAppSecret);
putText(node.with("honor"), "appId", honorAppId);
putText(node.with("honor"), "clientId", honorClientId);
putText(node.with("honor"), "clientSecret", honorClientSecret);
putText(node.with("apns"), "teamId", apnsTeamId);
putText(node.with("apns"), "keyId", apnsKeyId);
putText(node.with("apns"), "bundleId", apnsBundleId);
putText(node.with("apns"), "keyPath", apnsKeyPath);
node.with("apns").put("sandbox", apnsSandbox);
putText(node.with("fcm"), "serviceAccountJson", fcmServiceAccountJson);
return node.toString();
}
public List<String> parseStoreTargets(String json) {
if (json == null || json.isBlank()) {
return List.of();
}
try {
return objectMapper.readValue(json, new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {});
} catch (Exception e) {
return List.of();
}
}
private String normalizeFriendRequestMode(String mode) {
String normalized = mode == null ? "" : mode.trim().toUpperCase();
return switch (normalized) {
case "DIRECT_ACCEPT", "DISALLOW" -> normalized;
default -> "REQUIRE_CONFIRM";
};
}
private String normalizeReleaseMode(String mode) {
String normalized = mode == null ? "" : mode.trim().toUpperCase();
return switch (normalized) {
case "NOW", "SCHEDULED", "AUTO_REVIEW" -> normalized;
default -> "MANUAL";
};
}
private boolean isAppWideService(FeatureServiceEntity.ServiceType serviceType) {
return serviceType == FeatureServiceEntity.ServiceType.IM
|| serviceType == FeatureServiceEntity.ServiceType.PUSH
|| serviceType == FeatureServiceEntity.ServiceType.UPDATE;
}
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();
}
}
private void ensureObjectNode(ObjectNode root, String field) {
if (!root.has(field) || !root.get(field).isObject()) {
root.putObject(field);
}
}
private void putText(ObjectNode node, String field, String value) {
if (value != null) {
node.put(field, value.trim());
}
}
}