package com.xuqm.tenant.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuqm.common.model.ApiResponse; 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; @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) { this.featureServiceRepository = featureServiceRepository; this.sdkAppProvisioningService = sdkAppProvisioningService; this.objectMapper = objectMapper; } /** * 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(required = false, defaultValue = "ANDROID") FeatureServiceEntity.Platform platform) { AppEntity app = sdkAppProvisioningService.resolveApp(appKey); List features = featureServiceRepository.findByAppKey(app.getAppKey()); boolean imEnabled = features.stream() .anyMatch(f -> f.getServiceType() == FeatureServiceEntity.ServiceType.IM && f.isEnabled()); boolean pushEnabled = 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 = 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 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(); } }