2026-04-21 22:07:29 +08:00
|
|
|
package com.xuqm.tenant.service;
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
2026-04-29 17:35:52 +08:00
|
|
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
2026-04-28 16:08:07 +08:00
|
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
2026-04-21 22:07:29 +08:00
|
|
|
import com.xuqm.common.exception.BusinessException;
|
|
|
|
|
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
2026-04-24 16:46:38 +08:00
|
|
|
import com.xuqm.tenant.entity.ServiceActivationRequestEntity;
|
|
|
|
|
import com.xuqm.tenant.entity.ServiceActivationRequestEntity.Status;
|
2026-04-21 22:07:29 +08:00
|
|
|
import com.xuqm.tenant.repository.FeatureServiceRepository;
|
2026-04-24 16:46:38 +08:00
|
|
|
import com.xuqm.tenant.repository.ServiceActivationRequestRepository;
|
2026-04-21 22:07:29 +08:00
|
|
|
import org.springframework.stereotype.Service;
|
2026-04-24 16:46:38 +08:00
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
import java.time.LocalDateTime;
|
2026-04-29 12:33:25 +08:00
|
|
|
import java.util.ArrayList;
|
2026-04-21 22:07:29 +08:00
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
public class FeatureServiceManager {
|
|
|
|
|
|
|
|
|
|
private final FeatureServiceRepository repository;
|
2026-04-24 16:46:38 +08:00
|
|
|
private final ServiceActivationRequestRepository requestRepository;
|
2026-04-28 16:08:07 +08:00
|
|
|
private final ObjectMapper objectMapper;
|
2026-04-21 22:07:29 +08:00
|
|
|
|
2026-04-24 16:46:38 +08:00
|
|
|
public FeatureServiceManager(FeatureServiceRepository repository,
|
2026-04-28 16:08:07 +08:00
|
|
|
ServiceActivationRequestRepository requestRepository,
|
|
|
|
|
ObjectMapper objectMapper) {
|
2026-04-21 22:07:29 +08:00
|
|
|
this.repository = repository;
|
2026-04-24 16:46:38 +08:00
|
|
|
this.requestRepository = requestRepository;
|
2026-04-28 16:08:07 +08:00
|
|
|
this.objectMapper = objectMapper;
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public List<FeatureServiceEntity> listByApp(String appId) {
|
2026-04-29 12:33:25 +08:00
|
|
|
List<FeatureServiceEntity> services = repository.findByAppId(appId);
|
|
|
|
|
if (services.isEmpty()) {
|
|
|
|
|
return services;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<FeatureServiceEntity> normalized = new ArrayList<>();
|
2026-04-29 16:07:22 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
return normalized.isEmpty() ? services : normalized;
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:46:38 +08:00
|
|
|
/**
|
|
|
|
|
* Submit an activation request. Disabling is immediate; enabling requires ops approval.
|
2026-04-29 16:07:22 +08:00
|
|
|
* IM / PUSH / UPDATE are app-wide, so duplicate checks ignore platform.
|
2026-04-24 16:46:38 +08:00
|
|
|
*/
|
|
|
|
|
@Transactional
|
|
|
|
|
public ServiceActivationRequestEntity submitActivationRequest(
|
|
|
|
|
String appId,
|
|
|
|
|
FeatureServiceEntity.Platform platform,
|
|
|
|
|
FeatureServiceEntity.ServiceType serviceType,
|
|
|
|
|
String applyReason) {
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
if (isAppWideService(serviceType)) {
|
2026-04-29 12:33:25 +08:00
|
|
|
requestRepository.findFirstByAppIdAndServiceTypeOrderByCreatedAtDesc(appId, serviceType)
|
|
|
|
|
.ifPresent(req -> {
|
|
|
|
|
if (req.getStatus() == Status.PENDING) {
|
|
|
|
|
throw new BusinessException(400, "已有待审核的开通申请,请等待运营人员处理");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-24 16:46:38 +08:00
|
|
|
|
|
|
|
|
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) {
|
2026-04-29 16:07:22 +08:00
|
|
|
if (isAppWideService(serviceType)) {
|
2026-04-29 12:33:25 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
FeatureServiceEntity entity = repository
|
|
|
|
|
.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType)
|
2026-04-24 16:46:38 +08:00
|
|
|
.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);
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
if (isAppWideService(req.getServiceType())) {
|
2026-04-29 12:33:25 +08:00
|
|
|
List<FeatureServiceEntity> services = repository.findByAppIdAndServiceType(req.getAppId(), req.getServiceType());
|
|
|
|
|
if (services.isEmpty()) {
|
2026-04-29 16:07:22 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
} else {
|
|
|
|
|
services.forEach(service -> service.setEnabled(true));
|
|
|
|
|
repository.saveAll(services);
|
|
|
|
|
}
|
|
|
|
|
return req;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:46:38 +08:00
|
|
|
FeatureServiceEntity entity = repository
|
|
|
|
|
.findByAppIdAndPlatformAndServiceType(req.getAppId(), req.getPlatform(), req.getServiceType())
|
2026-04-21 22:07:29 +08:00
|
|
|
.orElseGet(() -> {
|
|
|
|
|
FeatureServiceEntity e = new FeatureServiceEntity();
|
|
|
|
|
e.setId(UUID.randomUUID().toString());
|
2026-04-24 16:46:38 +08:00
|
|
|
e.setAppId(req.getAppId());
|
|
|
|
|
e.setPlatform(req.getPlatform());
|
|
|
|
|
e.setServiceType(req.getServiceType());
|
2026-04-21 22:07:29 +08:00
|
|
|
e.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
return e;
|
|
|
|
|
});
|
2026-04-24 16:46:38 +08:00
|
|
|
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);
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FeatureServiceEntity getOrFail(String appId, FeatureServiceEntity.Platform platform,
|
|
|
|
|
FeatureServiceEntity.ServiceType serviceType) {
|
2026-04-29 12:33:25 +08:00
|
|
|
if (serviceType == FeatureServiceEntity.ServiceType.IM) {
|
|
|
|
|
return repository.findByAppIdAndServiceType(appId, serviceType)
|
|
|
|
|
.stream()
|
|
|
|
|
.findFirst()
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "服务未配置"));
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
return repository.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType)
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "服务未配置"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
@Transactional
|
|
|
|
|
public FeatureServiceEntity updateConfig(String appId,
|
|
|
|
|
FeatureServiceEntity.Platform platform,
|
|
|
|
|
FeatureServiceEntity.ServiceType serviceType,
|
|
|
|
|
String config) {
|
2026-04-29 12:33:25 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
FeatureServiceEntity entity = getOrFail(appId, platform, serviceType);
|
|
|
|
|
entity.setConfig(config);
|
|
|
|
|
return repository.save(entity);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 17:35:52 +08:00
|
|
|
public FeatureServiceEntity getByPlatform(String appId,
|
|
|
|
|
FeatureServiceEntity.Platform platform,
|
|
|
|
|
FeatureServiceEntity.ServiceType serviceType) {
|
|
|
|
|
return getOrFail(appId, platform, serviceType);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
public boolean allowStrangerMessage(String appId,
|
|
|
|
|
FeatureServiceEntity.Platform platform,
|
|
|
|
|
FeatureServiceEntity.ServiceType serviceType) {
|
2026-04-29 12:33:25 +08:00
|
|
|
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);
|
2026-04-28 16:08:07 +08:00
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
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);
|
2026-04-28 16:08:07 +08:00
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
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();
|
2026-04-28 16:08:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String buildAllowStrangerConfig(boolean allowStrangerMessage) {
|
|
|
|
|
ObjectNode node = objectMapper.createObjectNode();
|
|
|
|
|
node.put("allowStrangerMessage", allowStrangerMessage);
|
2026-04-29 12:33:25 +08:00
|
|
|
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);
|
2026-04-28 16:08:07 +08:00
|
|
|
return node.toString();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 17:35:52 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 17:35:52 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:25 +08:00
|
|
|
private String normalizeFriendRequestMode(String mode) {
|
|
|
|
|
String normalized = mode == null ? "" : mode.trim().toUpperCase();
|
|
|
|
|
return switch (normalized) {
|
|
|
|
|
case "DIRECT_ACCEPT", "DISALLOW" -> normalized;
|
|
|
|
|
default -> "REQUIRE_CONFIRM";
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 17:35:52 +08:00
|
|
|
private String normalizeReleaseMode(String mode) {
|
|
|
|
|
String normalized = mode == null ? "" : mode.trim().toUpperCase();
|
|
|
|
|
return switch (normalized) {
|
|
|
|
|
case "NOW", "SCHEDULED", "AUTO_REVIEW" -> normalized;
|
|
|
|
|
default -> "MANUAL";
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 16:07:22 +08:00
|
|
|
private boolean isAppWideService(FeatureServiceEntity.ServiceType serviceType) {
|
|
|
|
|
return serviceType == FeatureServiceEntity.ServiceType.IM
|
|
|
|
|
|| serviceType == FeatureServiceEntity.ServiceType.PUSH
|
|
|
|
|
|| serviceType == FeatureServiceEntity.ServiceType.UPDATE;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 12:33:25 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|