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}