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>
这个提交包含在:
父节点
8e041d50c1
当前提交
3cf5e294aa
@ -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}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户