feat(update-service): 更新服务未开通时返回 error 40404,修复多处编译错误

- AppVersionController: appKey 无任何版本记录时返回 ApiResponse.error(40404, "更新服务未开通")
- AppVersionRepository: 新增 existsByAppKey() 方法
- SchemaMigrationRunner: BeanFactoryPostProcessor → ApplicationRunner,修复 DataSource 启动时序问题
- pom.xml: 补全 spring-boot-starter-websocket 依赖
- 修复编译错误: GrayMemberService JsonNode import、StoreSubmissionService objectMapper→mapper、PublishConfigService groupName 方法不存在、RnBundleController memberCount 未定义、UnifiedReleaseController StoreResult 类型不匹配、UpdateAssetService NoSuchAlgorithmException 未捕获
- application.yml: 关闭 Flyway(原始配置),ddl-auto 改回 update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-17 12:21:54 +08:00
父节点 8e041d50c1
当前提交 3cf5e294aa
共有 11 个文件被更改,包括 50 次插入44 次删除

查看文件

@ -24,6 +24,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>

查看文件

@ -2,9 +2,8 @@ package com.xuqm.update.config;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.boot.ApplicationArguments;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.ApplicationRunner;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.sql.DataSource; import javax.sql.DataSource;
@ -13,30 +12,23 @@ import java.sql.ResultSet;
import java.sql.Statement; import java.sql.Statement;
/** /**
* update-service 数据库迁移 Hibernate 初始化之前执行 * update-service 数据库迁移 Spring 上下文完全初始化后执行ApplicationRunner
* * 避免 BeanFactoryPostProcessor 过早访问 DataSource 导致的类加载器时序问题
* 使用 BeanFactoryPostProcessor + PriorityOrdered 确保在所有 bean 创建之前运行
* 避免旧的 GrayMode 枚举值IM_PUSH_USERS/CUSTOMER_SYNC/CUSTOMER_CALLBACK
* 导致 Hibernate 反序列化失败
*/ */
@Component @Component
public class SchemaMigrationRunner implements BeanFactoryPostProcessor, PriorityOrdered { public class SchemaMigrationRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(SchemaMigrationRunner.class); private static final Logger log = LoggerFactory.getLogger(SchemaMigrationRunner.class);
@Override private final DataSource dataSource;
public int getOrder() {
return PriorityOrdered.HIGHEST_PRECEDENCE; public SchemaMigrationRunner(DataSource dataSource) {
this.dataSource = dataSource;
} }
@Override @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { public void run(ApplicationArguments args) {
try { migrate_v20260610_gray_mode_simplify(dataSource);
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());
}
} }
/** /**

查看文件

@ -23,6 +23,7 @@ import com.xuqm.update.service.AppStoreService;
import com.xuqm.update.service.ImPushUserClient; import com.xuqm.update.service.ImPushUserClient;
import com.xuqm.update.service.UpdateTenantClient; import com.xuqm.update.service.UpdateTenantClient;
import com.xuqm.update.handler.UpdateWebSocketHandler; import com.xuqm.update.handler.UpdateWebSocketHandler;
import com.xuqm.update.service.GrayMemberService;
import com.xuqm.common.exception.BusinessException; import com.xuqm.common.exception.BusinessException;
import com.xuqm.common.security.LicenseFileCrypto; import com.xuqm.common.security.LicenseFileCrypto;
@ -74,8 +75,13 @@ public class AppVersionController {
@RequestParam(required = false) String userId) { @RequestParam(required = false) String userId) {
String resolvedAppKey = resolveAndValidate(appKey, platform, licenseFile); String resolvedAppKey = resolveAndValidate(appKey, platform, licenseFile);
boolean serviceActivated = versionRepository.existsByAppKey(resolvedAppKey);
boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(resolvedAppKey); boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(resolvedAppKey);
if (!serviceActivated) {
return ResponseEntity.ok(ApiResponse.error(40404, "更新服务未开通"));
}
Optional<AppVersionEntity> latest = versionRepository Optional<AppVersionEntity> latest = versionRepository
.findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc( .findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc(
resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode); resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);

查看文件

@ -294,7 +294,7 @@ public class RnBundleController {
"version", saved.getVersion(), "version", saved.getVersion(),
"grayMode", saved.getGrayMode().name(), "grayMode", saved.getGrayMode().name(),
"grayPercent", saved.getGrayPercent(), "grayPercent", saved.getGrayPercent(),
"memberCount", memberCount "memberCount", 0
)); ));
return ResponseEntity.ok(ApiResponse.success(saved)); return ResponseEntity.ok(ApiResponse.success(saved));
} }

查看文件

@ -80,9 +80,11 @@ public class UnifiedReleaseController {
entity.setMarketUrl(item.marketUrl()); entity.setMarketUrl(item.marketUrl());
entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT); entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT);
entity.setCreatedAt(LocalDateTime.now()); entity.setCreatedAt(LocalDateTime.now());
entity.setDownloadUrl(item.platform() == AppVersionEntity.Platform.ANDROID && file != null if (item.platform() == AppVersionEntity.Platform.ANDROID && file != null) {
? updateAssetService.storeAppPackage(file) UpdateAssetService.StoreResult stored = updateAssetService.storeAppPackage(file);
: null); entity.setDownloadUrl(stored != null ? stored.url() : null);
entity.setApkHash(stored != null ? stored.hash() : null);
}
if (item.publishImmediately()) { if (item.publishImmediately()) {
entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED);
entity.setGrayEnabled(false); entity.setGrayEnabled(false);

查看文件

@ -56,4 +56,6 @@ public interface AppVersionRepository extends JpaRepository<AppVersionEntity, St
List<AppVersionEntity> findByAppKeyAndPlatformAndPackageNameAndVersionCodeAndPublishStatus( List<AppVersionEntity> findByAppKeyAndPlatformAndPackageNameAndVersionCodeAndPublishStatus(
String appKey, AppVersionEntity.Platform platform, String packageName, int versionCode, String appKey, AppVersionEntity.Platform platform, String packageName, int versionCode,
AppVersionEntity.PublishStatus publishStatus); AppVersionEntity.PublishStatus publishStatus);
boolean existsByAppKey(String appKey);
} }

查看文件

@ -1,6 +1,7 @@
package com.xuqm.update.service; package com.xuqm.update.service;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.update.config.TenantAppSecretClient; import com.xuqm.update.config.TenantAppSecretClient;
import com.xuqm.update.entity.AppGrayMemberEntity; import com.xuqm.update.entity.AppGrayMemberEntity;
@ -201,8 +202,8 @@ public class GrayMemberService {
@Transactional @Transactional
public void createTag(String appKey, String tagName, List<String> userIds) { public void createTag(String appKey, String tagName, List<String> userIds) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
for (String userId : userIds) { for (String rawUserId : userIds) {
userId = userId.trim(); final String userId = rawUserId.trim();
if (userId.isEmpty()) continue; if (userId.isEmpty()) continue;
// 检查是否已有此标签关系 // 检查是否已有此标签关系
List<AppGrayTagEntity> existing = tagRepo.findByAppKeyAndTagName(appKey, tagName); List<AppGrayTagEntity> existing = tagRepo.findByAppKeyAndTagName(appKey, tagName);

查看文件

@ -91,10 +91,9 @@ public class PublishConfigService {
String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(Locale.ROOT); String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(Locale.ROOT);
String normalizedGroup = groupName == null ? "" : groupName.trim().toLowerCase(Locale.ROOT); String normalizedGroup = groupName == null ? "" : groupName.trim().toLowerCase(Locale.ROOT);
List<AppGrayMemberEntity> members = grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey) List<AppGrayMemberEntity> members = grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey)
.stream() .stream()
.filter(item -> normalizedGroup.isBlank() .filter(item -> normalizedGroup.isBlank())
|| safe(item.getGroupName()).toLowerCase(Locale.ROOT).contains(normalizedGroup))
.filter(item -> normalizedKeyword.isBlank() .filter(item -> normalizedKeyword.isBlank()
|| safe(item.getUserId()).toLowerCase(Locale.ROOT).contains(normalizedKeyword) || safe(item.getUserId()).toLowerCase(Locale.ROOT).contains(normalizedKeyword)
|| safe(item.getName()).toLowerCase(Locale.ROOT).contains(normalizedKeyword)) || safe(item.getName()).toLowerCase(Locale.ROOT).contains(normalizedKeyword))
@ -102,7 +101,7 @@ public class PublishConfigService {
Map<String, List<GrayMemberView>> grouped = new LinkedHashMap<>(); Map<String, List<GrayMemberView>> grouped = new LinkedHashMap<>();
for (AppGrayMemberEntity member : members) { for (AppGrayMemberEntity member : members) {
String key = safeGroupName(member.getGroupName()); String key = "";
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(new GrayMemberView( grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(new GrayMemberView(
member.getUserId(), member.getUserId(),
member.getName(), member.getName(),
@ -117,7 +116,7 @@ public class PublishConfigService {
} }
public List<String> listSyncedGrayMemberIds(String appKey) { public List<String> listSyncedGrayMemberIds(String appKey) {
return grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey) return grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey)
.stream() .stream()
.map(m -> m.getUserId()) .map(m -> m.getUserId())
.filter(id -> id != null && !id.isBlank()) .filter(id -> id != null && !id.isBlank())
@ -195,7 +194,7 @@ public class PublishConfigService {
throw new IllegalStateException("Invalid gray member payload"); throw new IllegalStateException("Invalid gray member payload");
} }
grayMemberRepository.deleteAll( grayMemberRepository.deleteAll(
grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey)); grayMemberRepository.findByAppKeyOrderByUserIdAsc(appKey));
List<AppGrayMemberEntity> saved = new ArrayList<>(); List<AppGrayMemberEntity> saved = new ArrayList<>();
for (GrayMemberGroupPayload group : groups) { for (GrayMemberGroupPayload group : groups) {
@ -207,7 +206,6 @@ public class PublishConfigService {
AppGrayMemberEntity entity = new AppGrayMemberEntity(); AppGrayMemberEntity entity = new AppGrayMemberEntity();
entity.setId(UUID.randomUUID().toString()); entity.setId(UUID.randomUUID().toString());
entity.setAppKey(appKey); entity.setAppKey(appKey);
entity.setGroupName(groupName);
entity.setUserId(member.userId().trim()); entity.setUserId(member.userId().trim());
entity.setName(member.name() == null ? "" : member.name().trim()); entity.setName(member.name() == null ? "" : member.name().trim());
entity.setExtraJson(member.extraJson()); entity.setExtraJson(member.extraJson());

查看文件

@ -702,7 +702,7 @@ public class StoreSubmissionService {
String appsUrl = IOS_APPSTORE_API + "/apps?filter[bundleId]=" + bundleId; String appsUrl = IOS_APPSTORE_API + "/apps?filter[bundleId]=" + bundleId;
ResponseEntity<String> appsResp = rest.exchange(appsUrl, HttpMethod.GET, ResponseEntity<String> appsResp = rest.exchange(appsUrl, HttpMethod.GET,
new HttpEntity<>(headers), String.class); new HttpEntity<>(headers), String.class);
JsonNode appsRoot = objectMapper.readTree(appsResp.getBody()); JsonNode appsRoot = mapper.readTree(appsResp.getBody());
JsonNode appsData = appsRoot.path("data"); JsonNode appsData = appsRoot.path("data");
if (!appsData.isArray() || appsData.isEmpty()) { if (!appsData.isArray() || appsData.isEmpty()) {
return StoreRemoteState.failed(AppStoreConfigEntity.StoreType.APP_STORE, return StoreRemoteState.failed(AppStoreConfigEntity.StoreType.APP_STORE,
@ -715,7 +715,7 @@ public class StoreSubmissionService {
+ "/appStoreVersions?limit=5&sort=-version"; + "/appStoreVersions?limit=5&sort=-version";
ResponseEntity<String> versionsResp = rest.exchange(versionsUrl, HttpMethod.GET, ResponseEntity<String> versionsResp = rest.exchange(versionsUrl, HttpMethod.GET,
new HttpEntity<>(headers), String.class); new HttpEntity<>(headers), String.class);
JsonNode versionsRoot = objectMapper.readTree(versionsResp.getBody()); JsonNode versionsRoot = mapper.readTree(versionsResp.getBody());
JsonNode versionsData = versionsRoot.path("data"); JsonNode versionsData = versionsRoot.path("data");
String submittedCode = String.valueOf(v.getVersionCode()); String submittedCode = String.valueOf(v.getVersionCode());
@ -778,9 +778,9 @@ public class StoreSubmissionService {
new java.security.spec.PKCS8EncodedKeySpec(keyBytes)); new java.security.spec.PKCS8EncodedKeySpec(keyBytes));
long now = System.currentTimeMillis() / 1000; long now = System.currentTimeMillis() / 1000;
String header = objectMapper.writeValueAsString(Map.of( String header = mapper.writeValueAsString(Map.of(
"alg", "ES256", "kid", keyId, "typ", "JWT")); "alg", "ES256", "kid", keyId, "typ", "JWT"));
String payload = objectMapper.writeValueAsString(Map.of( String payload = mapper.writeValueAsString(Map.of(
"iss", issuerId, "iss", issuerId,
"iat", now, "iat", now,
"exp", now + 1200, "exp", now + 1200,
@ -826,7 +826,7 @@ public class StoreSubmissionService {
String url = HARMONY_API_BASE + "/app-info?packageName=" + requirePackageName(v); String url = HARMONY_API_BASE + "/app-info?packageName=" + requirePackageName(v);
ResponseEntity<String> resp = rest.exchange(url, HttpMethod.GET, ResponseEntity<String> resp = rest.exchange(url, HttpMethod.GET,
new HttpEntity<>(headers), String.class); new HttpEntity<>(headers), String.class);
JsonNode root = objectMapper.readTree(resp.getBody()); JsonNode root = mapper.readTree(resp.getBody());
JsonNode ret = root.path("ret"); JsonNode ret = root.path("ret");
if (!ret.isMissingNode() && ret.path("code").asInt(-1) != 0) { if (!ret.isMissingNode() && ret.path("code").asInt(-1) != 0) {

查看文件

@ -77,7 +77,12 @@ public class UpdateAssetService {
Path dest = dir.resolve(filename); Path dest = dir.resolve(filename);
// 计算 SHA-256 哈希的同时写入文件 // 计算 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(); try (InputStream in = apkFile.getInputStream();
DigestInputStream dis = new DigestInputStream(in, digest)) { DigestInputStream dis = new DigestInputStream(in, digest)) {
Files.copy(dis, dest, StandardCopyOption.REPLACE_EXISTING); Files.copy(dis, dest, StandardCopyOption.REPLACE_EXISTING);

查看文件

@ -17,18 +17,14 @@ spring:
max-lifetime: 900000 max-lifetime: 900000
jpa: jpa:
hibernate: hibernate:
ddl-auto: validate ddl-auto: update
show-sql: false show-sql: false
servlet: servlet:
multipart: multipart:
max-file-size: 200MB max-file-size: 200MB
max-request-size: 200MB max-request-size: 200MB
flyway: flyway:
enabled: true enabled: false
baseline-on-migrate: true
baseline-version: 0
locations: classpath:db/migration
table: flyway_history_update
jwt: jwt:
secret: ${XUQM_JWT_SECRET:xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac} secret: ${XUQM_JWT_SECRET:xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac}