package com.xuqm.tenant.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.tenant.config.PrivateDeploymentProperties; import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.repository.FeatureServiceRepository; import com.xuqm.tenant.service.SdkAppProvisioningService; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/sdk") public class SdkConfigController { private final FeatureServiceRepository featureServiceRepository; private final SdkAppProvisioningService sdkAppProvisioningService; private final ObjectMapper objectMapper; private final PrivateDeploymentProperties deployProps; @Value("${sdk.im-ws-url:wss://im.dev.xuqinmin.com/ws/im}") private String imWsUrl; @Value("${sdk.file-service-url:https://file.dev.xuqinmin.com}") private String fileServiceUrl; @Value("${sdk.im-api-url:https://im.dev.xuqinmin.com}") private String imApiUrl; public SdkConfigController(FeatureServiceRepository featureServiceRepository, SdkAppProvisioningService sdkAppProvisioningService, ObjectMapper objectMapper, PrivateDeploymentProperties deployProps) { this.featureServiceRepository = featureServiceRepository; this.sdkAppProvisioningService = sdkAppProvisioningService; this.objectMapper = objectMapper; this.deployProps = deployProps; } /** * GET /api/sdk/config?appKey=XXX&platform=ANDROID — public, no auth required. * * Returns SDK configuration URLs and enabled feature flags for the given appKey/appKey. * The demo app (`ak_demo_chat`) is auto-provisioned if it does not exist. * For update releases, the platform-specific UPDATE row drives both the enabled flag and * the default release configuration. */ @GetMapping("/config") public ResponseEntity> getConfig( @RequestParam String appKey, @RequestParam String packageName, @RequestParam(required = false, defaultValue = "ANDROID") FeatureServiceEntity.Platform platform) { AppEntity app = sdkAppProvisioningService.resolveApp(appKey); validatePackageName(app, platform, packageName); List features = featureServiceRepository.findByAppKey(app.getAppKey()); // In private deployments, intersect DB feature flags with deployment-level service availability boolean imEnabled = deployProps.isEnableIm() && features.stream() .anyMatch(f -> f.getServiceType() == FeatureServiceEntity.ServiceType.IM && f.isEnabled()); boolean pushEnabled = deployProps.isEnablePush() && features.stream() .anyMatch(f -> f.getServiceType() == FeatureServiceEntity.ServiceType.PUSH && f.isEnabled()); JsonNode updateConfig = featureServiceRepository .findByAppKeyAndPlatformAndServiceType(app.getAppKey(), platform, FeatureServiceEntity.ServiceType.UPDATE) .map(feature -> parseConfig(feature.getConfig())) .orElseGet(objectMapper::createObjectNode); JsonNode pushConfig = featureServiceRepository .findByAppKeyAndPlatformAndServiceType(app.getAppKey(), platform, FeatureServiceEntity.ServiceType.PUSH) .map(feature -> parseConfig(feature.getConfig())) .orElseGet(objectMapper::createObjectNode); boolean updateEnabled = deployProps.isEnableUpdate() && featureServiceRepository .findByAppKeyAndPlatformAndServiceType(app.getAppKey(), platform, FeatureServiceEntity.ServiceType.UPDATE) .map(FeatureServiceEntity::isEnabled) .orElse(false); SdkConfigResponse response = new SdkConfigResponse( imWsUrl, fileServiceUrl, imApiUrl, Map.of( "im", imEnabled, "push", pushEnabled, "update", updateEnabled ), updateEnabled, updateConfig.path("defaultPublishMode").asText("MANUAL"), updateConfig.path("defaultPublishImmediately").asBoolean(false), updateConfig.path("defaultScheduledPublishAt").asText(""), updateConfig.path("defaultAutoPublishAfterReview").asBoolean(false), updateConfig.path("defaultWebhookUrl").asText(""), csv(updateConfig.path("defaultStoreTargets")), updateConfig.path("defaultForceUpdate").asBoolean(false), updateConfig.path("defaultGrayEnabled").asBoolean(false), updateConfig.path("defaultGrayPercent").asInt(0), updateConfig.path("defaultPackageName").asText(""), updateConfig.path("defaultAppStoreUrl").asText(""), updateConfig.path("defaultMarketUrl").asText(""), pushConfig ); return ResponseEntity.ok(ApiResponse.success(response)); } public record SdkConfigResponse( String imWsUrl, String fileServiceUrl, String imApiUrl, Map features, boolean updateEnabled, String updateDefaultPublishMode, boolean updateDefaultPublishImmediately, String updateDefaultScheduledPublishAt, boolean updateDefaultAutoPublishAfterReview, String updateDefaultWebhookUrl, String updateDefaultStoreTargets, boolean updateDefaultForceUpdate, boolean updateDefaultGrayEnabled, int updateDefaultGrayPercent, String updateDefaultPackageName, String updateDefaultAppStoreUrl, String updateDefaultMarketUrl, JsonNode pushConfig ) {} private void validatePackageName(AppEntity app, FeatureServiceEntity.Platform platform, String packageName) { String registered = switch (platform) { case IOS -> app.getIosBundleId(); case HARMONY -> app.getHarmonyBundleName(); default -> app.getPackageName(); }; if (registered != null && !registered.isBlank() && !registered.equals(packageName)) { throw new BusinessException(403, "包名与应用配置不匹配"); } } private JsonNode parseConfig(String config) { if (config == null || config.isBlank()) { return objectMapper.createObjectNode(); } try { JsonNode node = objectMapper.readTree(config); return node == null ? objectMapper.createObjectNode() : node; } catch (Exception e) { return objectMapper.createObjectNode(); } } private String csv(JsonNode node) { if (node == null || !node.isArray()) { return ""; } StringBuilder sb = new StringBuilder(); for (JsonNode item : node) { String value = item.asText(null); if (value == null || value.isBlank()) { continue; } if (!sb.isEmpty()) { sb.append(','); } sb.append(value.trim()); } return sb.toString(); } }