diff --git a/update-service/pom.xml b/update-service/pom.xml index eccbea0..de17078 100644 --- a/update-service/pom.xml +++ b/update-service/pom.xml @@ -24,6 +24,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-websocket + org.springframework.boot spring-boot-starter-data-jpa diff --git a/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java b/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java index c79ca6e..4d3b1c6 100644 --- a/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java +++ b/update-service/src/main/java/com/xuqm/update/config/SchemaMigrationRunner.java @@ -2,9 +2,8 @@ package com.xuqm.update.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.core.PriorityOrdered; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import javax.sql.DataSource; @@ -13,30 +12,23 @@ import java.sql.ResultSet; import java.sql.Statement; /** - * update-service 数据库迁移,在 Hibernate 初始化之前执行。 - * - * 使用 BeanFactoryPostProcessor + PriorityOrdered 确保在所有 bean 创建之前运行, - * 避免旧的 GrayMode 枚举值(IM_PUSH_USERS/CUSTOMER_SYNC/CUSTOMER_CALLBACK) - * 导致 Hibernate 反序列化失败。 + * update-service 数据库迁移,在 Spring 上下文完全初始化后执行(ApplicationRunner), + * 避免 BeanFactoryPostProcessor 过早访问 DataSource 导致的类加载器时序问题。 */ @Component -public class SchemaMigrationRunner implements BeanFactoryPostProcessor, PriorityOrdered { +public class SchemaMigrationRunner implements ApplicationRunner { private static final Logger log = LoggerFactory.getLogger(SchemaMigrationRunner.class); - @Override - public int getOrder() { - return PriorityOrdered.HIGHEST_PRECEDENCE; + private final DataSource dataSource; + + public SchemaMigrationRunner(DataSource dataSource) { + this.dataSource = dataSource; } @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { - try { - DataSource dataSource = beanFactory.getBean(DataSource.class); - migrate_v20260610_gray_mode_simplify(dataSource); - } catch (Exception e) { - log.warn("Schema migration skipped (DataSource not ready): {}", e.getMessage()); - } + public void run(ApplicationArguments args) { + migrate_v20260610_gray_mode_simplify(dataSource); } /** diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java index a4c7faa..40628cb 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java @@ -23,6 +23,7 @@ import com.xuqm.update.service.AppStoreService; import com.xuqm.update.service.ImPushUserClient; import com.xuqm.update.service.UpdateTenantClient; import com.xuqm.update.handler.UpdateWebSocketHandler; +import com.xuqm.update.service.GrayMemberService; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.security.LicenseFileCrypto; @@ -74,8 +75,13 @@ public class AppVersionController { @RequestParam(required = false) String userId) { String resolvedAppKey = resolveAndValidate(appKey, platform, licenseFile); + boolean serviceActivated = versionRepository.existsByAppKey(resolvedAppKey); boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(resolvedAppKey); + if (!serviceActivated) { + return ResponseEntity.ok(ApiResponse.error(40404, "更新服务未开通")); + } + Optional latest = versionRepository .findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc( resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode); diff --git a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java index 6b7ee9c..f6bf65d 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java @@ -294,7 +294,7 @@ public class RnBundleController { "version", saved.getVersion(), "grayMode", saved.getGrayMode().name(), "grayPercent", saved.getGrayPercent(), - "memberCount", memberCount + "memberCount", 0 )); return ResponseEntity.ok(ApiResponse.success(saved)); } diff --git a/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java b/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java index 1d4c73e..0fb3572 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java @@ -80,9 +80,11 @@ public class UnifiedReleaseController { entity.setMarketUrl(item.marketUrl()); entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT); entity.setCreatedAt(LocalDateTime.now()); - entity.setDownloadUrl(item.platform() == AppVersionEntity.Platform.ANDROID && file != null - ? updateAssetService.storeAppPackage(file) - : null); + if (item.platform() == AppVersionEntity.Platform.ANDROID && file != null) { + UpdateAssetService.StoreResult stored = updateAssetService.storeAppPackage(file); + entity.setDownloadUrl(stored != null ? stored.url() : null); + entity.setApkHash(stored != null ? stored.hash() : null); + } if (item.publishImmediately()) { entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); entity.setGrayEnabled(false); diff --git a/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java b/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java index 7a42bb2..092f8a6 100644 --- a/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java +++ b/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java @@ -56,4 +56,6 @@ public interface AppVersionRepository extends JpaRepository findByAppKeyAndPlatformAndPackageNameAndVersionCodeAndPublishStatus( String appKey, AppVersionEntity.Platform platform, String packageName, int versionCode, AppVersionEntity.PublishStatus publishStatus); + + boolean existsByAppKey(String appKey); } diff --git a/update-service/src/main/java/com/xuqm/update/service/GrayMemberService.java b/update-service/src/main/java/com/xuqm/update/service/GrayMemberService.java index 14c20a8..17f2bb1 100644 --- a/update-service/src/main/java/com/xuqm/update/service/GrayMemberService.java +++ b/update-service/src/main/java/com/xuqm/update/service/GrayMemberService.java @@ -1,6 +1,7 @@ package com.xuqm.update.service; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuqm.update.config.TenantAppSecretClient; import com.xuqm.update.entity.AppGrayMemberEntity; @@ -201,8 +202,8 @@ public class GrayMemberService { @Transactional public void createTag(String appKey, String tagName, List userIds) { LocalDateTime now = LocalDateTime.now(); - for (String userId : userIds) { - userId = userId.trim(); + for (String rawUserId : userIds) { + final String userId = rawUserId.trim(); if (userId.isEmpty()) continue; // 检查是否已有此标签关系 List existing = tagRepo.findByAppKeyAndTagName(appKey, tagName); diff --git a/update-service/src/main/java/com/xuqm/update/service/PublishConfigService.java b/update-service/src/main/java/com/xuqm/update/service/PublishConfigService.java index e20fff6..aa58a68 100644 --- a/update-service/src/main/java/com/xuqm/update/service/PublishConfigService.java +++ b/update-service/src/main/java/com/xuqm/update/service/PublishConfigService.java @@ -91,10 +91,9 @@ public class PublishConfigService { String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(Locale.ROOT); String normalizedGroup = groupName == null ? "" : groupName.trim().toLowerCase(Locale.ROOT); - List members = grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey) + List members = grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey) .stream() - .filter(item -> normalizedGroup.isBlank() - || safe(item.getGroupName()).toLowerCase(Locale.ROOT).contains(normalizedGroup)) + .filter(item -> normalizedGroup.isBlank()) .filter(item -> normalizedKeyword.isBlank() || safe(item.getUserId()).toLowerCase(Locale.ROOT).contains(normalizedKeyword) || safe(item.getName()).toLowerCase(Locale.ROOT).contains(normalizedKeyword)) @@ -102,7 +101,7 @@ public class PublishConfigService { Map> grouped = new LinkedHashMap<>(); for (AppGrayMemberEntity member : members) { - String key = safeGroupName(member.getGroupName()); + String key = ""; grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(new GrayMemberView( member.getUserId(), member.getName(), @@ -117,7 +116,7 @@ public class PublishConfigService { } public List listSyncedGrayMemberIds(String appKey) { - return grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey) + return grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey) .stream() .map(m -> m.getUserId()) .filter(id -> id != null && !id.isBlank()) @@ -195,7 +194,7 @@ public class PublishConfigService { throw new IllegalStateException("Invalid gray member payload"); } grayMemberRepository.deleteAll( - grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey)); + grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey)); List saved = new ArrayList<>(); for (GrayMemberGroupPayload group : groups) { @@ -207,7 +206,6 @@ public class PublishConfigService { AppGrayMemberEntity entity = new AppGrayMemberEntity(); entity.setId(UUID.randomUUID().toString()); entity.setAppKey(appKey); - entity.setGroupName(groupName); entity.setUserId(member.userId().trim()); entity.setName(member.name() == null ? "" : member.name().trim()); entity.setExtraJson(member.extraJson()); diff --git a/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java b/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java index 205bef6..ee8c7d9 100644 --- a/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java +++ b/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java @@ -702,7 +702,7 @@ public class StoreSubmissionService { String appsUrl = IOS_APPSTORE_API + "/apps?filter[bundleId]=" + bundleId; ResponseEntity appsResp = rest.exchange(appsUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class); - JsonNode appsRoot = objectMapper.readTree(appsResp.getBody()); + JsonNode appsRoot = mapper.readTree(appsResp.getBody()); JsonNode appsData = appsRoot.path("data"); if (!appsData.isArray() || appsData.isEmpty()) { return StoreRemoteState.failed(AppStoreConfigEntity.StoreType.APP_STORE, @@ -715,7 +715,7 @@ public class StoreSubmissionService { + "/appStoreVersions?limit=5&sort=-version"; ResponseEntity versionsResp = rest.exchange(versionsUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class); - JsonNode versionsRoot = objectMapper.readTree(versionsResp.getBody()); + JsonNode versionsRoot = mapper.readTree(versionsResp.getBody()); JsonNode versionsData = versionsRoot.path("data"); String submittedCode = String.valueOf(v.getVersionCode()); @@ -778,9 +778,9 @@ public class StoreSubmissionService { new java.security.spec.PKCS8EncodedKeySpec(keyBytes)); long now = System.currentTimeMillis() / 1000; - String header = objectMapper.writeValueAsString(Map.of( + String header = mapper.writeValueAsString(Map.of( "alg", "ES256", "kid", keyId, "typ", "JWT")); - String payload = objectMapper.writeValueAsString(Map.of( + String payload = mapper.writeValueAsString(Map.of( "iss", issuerId, "iat", now, "exp", now + 1200, @@ -826,7 +826,7 @@ public class StoreSubmissionService { String url = HARMONY_API_BASE + "/app-info?packageName=" + requirePackageName(v); ResponseEntity resp = rest.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class); - JsonNode root = objectMapper.readTree(resp.getBody()); + JsonNode root = mapper.readTree(resp.getBody()); JsonNode ret = root.path("ret"); if (!ret.isMissingNode() && ret.path("code").asInt(-1) != 0) { diff --git a/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java b/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java index e24d8be..157742b 100644 --- a/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java +++ b/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java @@ -77,7 +77,12 @@ public class UpdateAssetService { Path dest = dir.resolve(filename); // 计算 SHA-256 哈希的同时写入文件 - MessageDigest digest = MessageDigest.getInstance("SHA-256"); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (java.security.NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 not available", e); + } try (InputStream in = apkFile.getInputStream(); DigestInputStream dis = new DigestInputStream(in, digest)) { Files.copy(dis, dest, StandardCopyOption.REPLACE_EXISTING); diff --git a/update-service/src/main/resources/application.yml b/update-service/src/main/resources/application.yml index 69dc77c..46aa6c3 100644 --- a/update-service/src/main/resources/application.yml +++ b/update-service/src/main/resources/application.yml @@ -17,18 +17,14 @@ spring: max-lifetime: 900000 jpa: hibernate: - ddl-auto: validate + ddl-auto: update show-sql: false servlet: multipart: max-file-size: 200MB max-request-size: 200MB flyway: - enabled: true - baseline-on-migrate: true - baseline-version: 0 - locations: classpath:db/migration - table: flyway_history_update + enabled: false jwt: secret: ${XUQM_JWT_SECRET:xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac}