package com.xuqm.tenant.service; import com.xuqm.common.exception.BusinessException; import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.entity.TenantEntity; import com.xuqm.tenant.repository.AppRepository; import com.xuqm.tenant.repository.FeatureServiceRepository; import com.xuqm.tenant.repository.TenantRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.Base64; import java.util.List; import java.util.UUID; @Service public class SdkAppProvisioningService { private static final SecureRandom RANDOM = new SecureRandom(); private final AppRepository appRepository; private final TenantRepository tenantRepository; private final FeatureServiceRepository featureServiceRepository; private final PasswordEncoder passwordEncoder; @Value("${sdk.bootstrap-app-key:ak_demo_chat}") private String bootstrapAppKey; @Value("${sdk.bootstrap-app-name:Demo Chat}") private String bootstrapAppName; @Value("${sdk.bootstrap-app-package:com.xuqm.demo}") private String bootstrapAppPackage; @Value("${sdk.bootstrap-app-description:XuqmGroup demo app}") private String bootstrapAppDescription; public SdkAppProvisioningService(AppRepository appRepository, TenantRepository tenantRepository, FeatureServiceRepository featureServiceRepository, PasswordEncoder passwordEncoder) { this.appRepository = appRepository; this.tenantRepository = tenantRepository; this.featureServiceRepository = featureServiceRepository; this.passwordEncoder = passwordEncoder; } @Transactional public AppEntity ensureBootstrapApp() { return ensureApp(bootstrapAppKey, true); } @Transactional public AppEntity resolveApp(String appId) { return appRepository.findByAppKey(appId) .or(() -> appRepository.findById(appId)) .orElseGet(() -> { if (!bootstrapAppKey.equals(appId)) { throw new BusinessException(404, "App not found: " + appId); } return ensureApp(appId, true); }); } @Transactional public AppEntity ensureApp(String appKey, boolean bootstrapDefaults) { AppEntity existing = appRepository.findByAppKey(appKey).orElse(null); if (existing != null) { if (bootstrapDefaults) { ensureFeatureDefaults(existing); } return existing; } TenantEntity owner = resolveOwnerTenant(); AppEntity app = new AppEntity(); app.setId(UUID.randomUUID().toString()); app.setTenantId(owner.getId()); app.setPackageName(bootstrapAppPackage); app.setName(bootstrapAppName); app.setDescription(bootstrapAppDescription); app.setIconUrl(null); app.setAppKey(appKey); app.setAppSecret(generateSecret()); app.setCreatedAt(LocalDateTime.now()); app = appRepository.save(app); if (bootstrapDefaults) { ensureFeatureDefaults(app); } return app; } private TenantEntity resolveOwnerTenant() { return tenantRepository.findFirstByOrderByCreatedAtAsc() .orElseGet(this::createBootstrapTenant); } private TenantEntity createBootstrapTenant() { TenantEntity tenant = new TenantEntity(); tenant.setId(UUID.randomUUID().toString()); tenant.setUsername("system"); tenant.setPassword(passwordEncoder.encode(generateSecret())); tenant.setEmail("system@xuqinmin.com"); tenant.setNickname("System"); tenant.setPhone(null); tenant.setType(TenantEntity.Type.MAIN); tenant.setStatus(TenantEntity.Status.ACTIVE); tenant.setParentId(null); tenant.setCreatedAt(LocalDateTime.now()); return tenantRepository.save(tenant); } private void ensureFeatureDefaults(AppEntity app) { featureServiceRepository.findByAppIdAndServiceType(app.getAppKey(), FeatureServiceEntity.ServiceType.IM) .stream() .findFirst() .orElseGet(() -> { FeatureServiceEntity feature = new FeatureServiceEntity(); feature.setId(UUID.randomUUID().toString()); feature.setAppId(app.getAppKey()); feature.setPlatform(FeatureServiceEntity.Platform.ANDROID); feature.setServiceType(FeatureServiceEntity.ServiceType.IM); feature.setEnabled(true); feature.setConfig(""" {"allowStrangerMessage":false,"allowFriendRequest":true,"friendRequestMode":"REQUIRE_CONFIRM","allowGroupJoinRequest":true,"blacklistSendSuccess":true,"messageRecallMinutes":2,"historyRetentionDays":7,"conversationPullLimit":100,"multiClientConversationDeleteSync":false} """.trim()); feature.setCreatedAt(LocalDateTime.now()); return featureServiceRepository.save(feature); }); for (FeatureServiceEntity.Platform platform : List.of( FeatureServiceEntity.Platform.ANDROID, FeatureServiceEntity.Platform.IOS, FeatureServiceEntity.Platform.HARMONY)) { for (FeatureServiceEntity.ServiceType serviceType : List.of( FeatureServiceEntity.ServiceType.PUSH, FeatureServiceEntity.ServiceType.UPDATE)) { featureServiceRepository.findByAppIdAndPlatformAndServiceType(app.getAppKey(), platform, serviceType) .orElseGet(() -> { FeatureServiceEntity feature = new FeatureServiceEntity(); feature.setId(UUID.randomUUID().toString()); feature.setAppId(app.getAppKey()); feature.setPlatform(platform); feature.setServiceType(serviceType); feature.setEnabled(true); feature.setConfig(null); feature.setCreatedAt(LocalDateTime.now()); return featureServiceRepository.save(feature); }); } } } private String generateSecret() { byte[] bytes = new byte[32]; RANDOM.nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } }