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>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>

查看文件

@ -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);
}
/**

查看文件

@ -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<AppVersionEntity> latest = versionRepository
.findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc(
resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);

查看文件

@ -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));
}

查看文件

@ -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);

查看文件

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

查看文件

@ -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<String> 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<AppGrayTagEntity> existing = tagRepo.findByAppKeyAndTagName(appKey, tagName);

查看文件

@ -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<AppGrayMemberEntity> members = grayMemberRepository.findByAppKeyOrderByGroupNameAscNameAscUserIdAsc(appKey)
List<AppGrayMemberEntity> 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<String, List<GrayMemberView>> 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<String> 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<AppGrayMemberEntity> 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());

查看文件

@ -702,7 +702,7 @@ public class StoreSubmissionService {
String appsUrl = IOS_APPSTORE_API + "/apps?filter[bundleId]=" + bundleId;
ResponseEntity<String> 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<String> 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<String> 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) {

查看文件

@ -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);

查看文件

@ -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}