feat: license 文件作为通用凭证支持所有服务 SDK 初始化
- LicenseFileCrypto 移至 common 模块并新增 decrypt() 方法 - LicenseFileCrypto.LicensePayload 携带 appKey / packageName / iosBundleId / harmonyBundleName,matchesPackageName() 支持三端包名任一匹配 - tenant-service downloadLicenseFile:去掉"License 服务已开通"限制,app 创建即可下载;payload 新增 iosBundleId / harmonyBundleName - im / push / update / license 四个服务 SDK 初始化端点均支持双模式: · licenseFile 模式:解密文件取 appKey,比对 packageName(无需调 tenant-service) · appKey 模式:调 tenant-service 取 platformInfo 比对 packageName(原有逻辑) - appKey 参数由必填改为可选(与 licenseFile 二选一) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
0a267c5f70
当前提交
8c9bfb6acd
@ -0,0 +1,112 @@
|
||||
package com.xuqm.common.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class LicenseFileCrypto {
|
||||
|
||||
private static final String MAGIC = "XUQM-LICENSE-V1";
|
||||
private static final String PASSPHRASE = "xuqm-license-file-v1.2026.internal";
|
||||
private static final int SALT_BYTES = 16;
|
||||
private static final int IV_BYTES = 12;
|
||||
private static final int KEY_BITS = 256;
|
||||
private static final int ITERATIONS = 120_000;
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private LicenseFileCrypto() {
|
||||
}
|
||||
|
||||
public static String encrypt(String plainText) {
|
||||
try {
|
||||
byte[] salt = randomBytes(SALT_BYTES);
|
||||
byte[] iv = randomBytes(IV_BYTES);
|
||||
SecretKeySpec key = deriveKey(salt);
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return String.join(".",
|
||||
MAGIC,
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(salt),
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(iv),
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(cipherText));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to encrypt license file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LicensePayload decrypt(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 4 || !MAGIC.equals(parts[0])) {
|
||||
throw new IllegalArgumentException("Invalid license file format");
|
||||
}
|
||||
byte[] salt = Base64.getUrlDecoder().decode(parts[1]);
|
||||
byte[] iv = Base64.getUrlDecoder().decode(parts[2]);
|
||||
byte[] cipherText = Base64.getUrlDecoder().decode(parts[3]);
|
||||
SecretKeySpec key = deriveKey(salt);
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||
String plainText = new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
|
||||
JsonNode node = MAPPER.readTree(plainText);
|
||||
return new LicensePayload(
|
||||
text(node, "appKey"),
|
||||
text(node, "appName"),
|
||||
text(node, "packageName"),
|
||||
text(node, "iosBundleId"),
|
||||
text(node, "harmonyBundleName"),
|
||||
text(node, "baseUrl"),
|
||||
text(node, "serverUrl"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Failed to decrypt license file: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKeySpec deriveKey(byte[] salt) throws Exception {
|
||||
PBEKeySpec spec = new PBEKeySpec(PASSPHRASE.toCharArray(), salt, ITERATIONS, KEY_BITS);
|
||||
byte[] encoded = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
return new SecretKeySpec(encoded, "AES");
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
RANDOM.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
JsonNode v = node.path(field);
|
||||
return v.isMissingNode() || v.isNull() ? null : v.asText(null);
|
||||
}
|
||||
|
||||
public record LicensePayload(
|
||||
String appKey,
|
||||
String appName,
|
||||
String packageName,
|
||||
String iosBundleId,
|
||||
String harmonyBundleName,
|
||||
String baseUrl,
|
||||
String serverUrl) {
|
||||
|
||||
public boolean matchesPackageName(String candidate) {
|
||||
if (candidate == null || candidate.isBlank()) return false;
|
||||
boolean anyConfigured = hasText(packageName) || hasText(iosBundleId) || hasText(harmonyBundleName);
|
||||
if (!anyConfigured) return true;
|
||||
return candidate.equals(packageName) || candidate.equals(iosBundleId) || candidate.equals(harmonyBundleName);
|
||||
}
|
||||
|
||||
private static boolean hasText(String s) { return s != null && !s.isBlank(); }
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package com.xuqm.im.controller;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.common.security.LicenseFileCrypto;
|
||||
import com.xuqm.im.service.ImAccountService;
|
||||
import com.xuqm.im.service.ImAppSecretClient;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@ -27,28 +28,36 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> login(
|
||||
@RequestParam @NotBlank String appKey,
|
||||
@RequestParam(required = false) String appKey,
|
||||
@RequestParam @NotBlank String userId,
|
||||
@RequestParam @NotBlank String userSig,
|
||||
@RequestParam @NotBlank String packageName) {
|
||||
if (userSig.isBlank()) {
|
||||
return ResponseEntity.status(401).body(ApiResponse.error(401, "Missing userSig"));
|
||||
}
|
||||
validatePackageName(appKey, packageName);
|
||||
ImAccountService.LoginResult result = accountService.loginWithUserSig(appKey, userId, userSig);
|
||||
@RequestParam @NotBlank String packageName,
|
||||
@RequestParam(required = false) String licenseFile) {
|
||||
String resolvedAppKey = resolveAndValidate(appKey, packageName, licenseFile);
|
||||
ImAccountService.LoginResult result = accountService.loginWithUserSig(resolvedAppKey, userId, userSig);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token(), "admin", result.admin())));
|
||||
}
|
||||
|
||||
private void validatePackageName(String appKey, String packageName) {
|
||||
private String resolveAndValidate(String appKey, String packageName, String licenseFile) {
|
||||
if (hasText(licenseFile)) {
|
||||
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
||||
if (!payload.matchesPackageName(packageName)) {
|
||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return payload.appKey();
|
||||
}
|
||||
if (!hasText(appKey)) {
|
||||
throw new BusinessException(400, "appKey 或 licenseFile 必须提供其中一个");
|
||||
}
|
||||
ImAppSecretClient.PlatformInfo info = appSecretClient.getPlatformInfo(appKey);
|
||||
String android = info.androidPackageName();
|
||||
String ios = info.iosBundleId();
|
||||
String harmony = info.harmonyBundleName();
|
||||
boolean anyConfigured = hasText(android) || hasText(ios) || hasText(harmony);
|
||||
boolean anyConfigured = hasText(info.androidPackageName()) || hasText(info.iosBundleId()) || hasText(info.harmonyBundleName());
|
||||
if (anyConfigured) {
|
||||
boolean matches = packageName.equals(android) || packageName.equals(ios) || packageName.equals(harmony);
|
||||
boolean matches = packageName.equals(info.androidPackageName())
|
||||
|| packageName.equals(info.iosBundleId())
|
||||
|| packageName.equals(info.harmonyBundleName());
|
||||
if (!matches) throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return appKey;
|
||||
}
|
||||
|
||||
private static boolean hasText(String s) { return s != null && !s.isBlank(); }
|
||||
|
||||
@ -3,7 +3,9 @@ package com.xuqm.license.controller;
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.common.security.LicenseFileCrypto;
|
||||
import com.xuqm.license.service.DeviceService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@ -27,8 +29,9 @@ public class LicensePublicController {
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
|
||||
String resolvedAppKey = resolveAppKey(req.appKey(), req.packageName(), req.licenseFile());
|
||||
DeviceService.RegisterResult result = deviceService.register(
|
||||
req.appKey(),
|
||||
resolvedAppKey,
|
||||
req.packageName(),
|
||||
req.deviceId(),
|
||||
req.deviceName(),
|
||||
@ -47,7 +50,8 @@ public class LicensePublicController {
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> verify(@Valid @RequestBody VerifyRequest req) {
|
||||
DeviceService.VerifyResult result = deviceService.verify(req.appKey(), req.packageName(), req.deviceId(), req.token(), req.userInfo());
|
||||
String resolvedAppKey = resolveAppKey(req.appKey(), req.packageName(), req.licenseFile());
|
||||
DeviceService.VerifyResult result = deviceService.verify(resolvedAppKey, req.packageName(), req.deviceId(), req.token(), req.userInfo());
|
||||
Map<String, Object> data = new java.util.LinkedHashMap<>();
|
||||
data.put("valid", result.valid());
|
||||
if (result.error() != null) {
|
||||
@ -56,9 +60,24 @@ public class LicensePublicController {
|
||||
return ResponseEntity.ok(ApiResponse.success(data));
|
||||
}
|
||||
|
||||
private static String resolveAppKey(String appKey, String packageName, String licenseFile) {
|
||||
if (licenseFile != null && !licenseFile.isBlank()) {
|
||||
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
||||
if (!payload.matchesPackageName(packageName)) {
|
||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return payload.appKey();
|
||||
}
|
||||
if (appKey == null || appKey.isBlank()) {
|
||||
throw new BusinessException(400, "appKey 或 licenseFile 必须提供其中一个");
|
||||
}
|
||||
return appKey;
|
||||
}
|
||||
|
||||
public record RegisterRequest(
|
||||
@NotBlank String appKey,
|
||||
String appKey,
|
||||
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
||||
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
||||
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
|
||||
@JsonProperty("deviceName") @JsonAlias("device_name") String deviceName,
|
||||
@JsonProperty("deviceModel") @JsonAlias("device_model") String deviceModel,
|
||||
@ -68,8 +87,9 @@ public class LicensePublicController {
|
||||
) {}
|
||||
|
||||
public record VerifyRequest(
|
||||
@NotBlank String appKey,
|
||||
String appKey,
|
||||
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
||||
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
||||
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
|
||||
@NotBlank String token,
|
||||
@JsonProperty("userInfo") @JsonAlias("user_info") JsonNode userInfo
|
||||
|
||||
@ -2,6 +2,7 @@ package com.xuqm.push.controller;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.common.security.LicenseFileCrypto;
|
||||
import com.xuqm.push.entity.DeviceTokenEntity;
|
||||
import com.xuqm.push.service.PushAccountService;
|
||||
import com.xuqm.push.service.PushAppSecretClient;
|
||||
@ -31,10 +32,11 @@ public class PushAuthController {
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> login(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam(required = false) String appKey,
|
||||
@RequestParam String userId,
|
||||
@RequestParam String userSig,
|
||||
@RequestParam String packageName,
|
||||
@RequestParam(required = false) String licenseFile,
|
||||
@RequestParam DeviceTokenEntity.Vendor vendor,
|
||||
@RequestParam String token,
|
||||
@RequestParam(required = false) String platform,
|
||||
@ -44,9 +46,9 @@ public class PushAuthController {
|
||||
@RequestParam(required = false) String osVersion,
|
||||
@RequestParam(required = false) String appVersion) {
|
||||
|
||||
validatePackageName(appKey, packageName);
|
||||
PushAccountService.LoginResult result = accountService.loginWithUserSig(appKey, userId, userSig);
|
||||
pushDispatcher.registerToken(appKey, userId, vendor, token, platform, deviceId, brand, model, osVersion, appVersion);
|
||||
String resolvedAppKey = resolveAndValidate(appKey, packageName, licenseFile);
|
||||
PushAccountService.LoginResult result = accountService.loginWithUserSig(resolvedAppKey, userId, userSig);
|
||||
pushDispatcher.registerToken(resolvedAppKey, userId, vendor, token, platform, deviceId, brand, model, osVersion, appVersion);
|
||||
|
||||
Map<String, Object> response = Map.of(
|
||||
"pushToken", result.pushToken(),
|
||||
@ -57,19 +59,29 @@ public class PushAuthController {
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
private void validatePackageName(String appKey, String packageName) {
|
||||
private String resolveAndValidate(String appKey, String packageName, String licenseFile) {
|
||||
if (hasText(licenseFile)) {
|
||||
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
||||
if (!payload.matchesPackageName(packageName)) {
|
||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return payload.appKey();
|
||||
}
|
||||
if (!hasText(appKey)) {
|
||||
throw new BusinessException(400, "appKey 或 licenseFile 必须提供其中一个");
|
||||
}
|
||||
if (packageName == null || packageName.isBlank()) {
|
||||
throw new BusinessException(403, "packageName is required");
|
||||
}
|
||||
PushAppSecretClient.PlatformInfo info = appSecretClient.getPlatformInfo(appKey);
|
||||
String android = info.androidPackageName();
|
||||
String ios = info.iosBundleId();
|
||||
String harmony = info.harmonyBundleName();
|
||||
boolean anyConfigured = hasText(android) || hasText(ios) || hasText(harmony);
|
||||
boolean anyConfigured = hasText(info.androidPackageName()) || hasText(info.iosBundleId()) || hasText(info.harmonyBundleName());
|
||||
if (anyConfigured) {
|
||||
boolean matches = packageName.equals(android) || packageName.equals(ios) || packageName.equals(harmony);
|
||||
boolean matches = packageName.equals(info.androidPackageName())
|
||||
|| packageName.equals(info.iosBundleId())
|
||||
|| packageName.equals(info.harmonyBundleName());
|
||||
if (!matches) throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return appKey;
|
||||
}
|
||||
|
||||
private static boolean hasText(String s) { return s != null && !s.isBlank(); }
|
||||
|
||||
@ -11,7 +11,7 @@ import com.xuqm.tenant.service.AppService;
|
||||
import com.xuqm.tenant.service.AppUserClient;
|
||||
import com.xuqm.tenant.service.EmailService;
|
||||
import com.xuqm.tenant.service.FeatureServiceManager;
|
||||
import com.xuqm.tenant.service.LicenseFileCrypto;
|
||||
import com.xuqm.common.security.LicenseFileCrypto;
|
||||
import com.xuqm.tenant.config.PrivateDeploymentProperties;
|
||||
import com.xuqm.tenant.service.OperationLogService;
|
||||
import jakarta.validation.Valid;
|
||||
@ -172,18 +172,17 @@ public class AppController {
|
||||
public ResponseEntity<byte[]> downloadLicenseFile(@PathVariable String appKey,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
AppEntity app = appService.getByAppKey(appKey, tenantId);
|
||||
boolean licenseEnabled = featureServiceManager.listByApp(appKey).stream()
|
||||
.anyMatch(service -> service.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE && service.isEnabled());
|
||||
if (!licenseEnabled) {
|
||||
throw new BusinessException(400, "License 服务未开通");
|
||||
}
|
||||
Map<String, Object> payload = new java.util.LinkedHashMap<>();
|
||||
payload.put("appKey", app.getAppKey());
|
||||
payload.put("appName", app.getName());
|
||||
payload.put("packageName", app.getPackageName());
|
||||
if (app.getIosBundleId() != null && !app.getIosBundleId().isBlank()) {
|
||||
payload.put("iosBundleId", app.getIosBundleId());
|
||||
}
|
||||
if (app.getHarmonyBundleName() != null && !app.getHarmonyBundleName().isBlank()) {
|
||||
payload.put("harmonyBundleName", app.getHarmonyBundleName());
|
||||
}
|
||||
payload.put("baseUrl", normalizeBaseUrl(licensePublicBaseUrl));
|
||||
// serverUrl is set only for private deployments; the SDK uses it to configure all service
|
||||
// endpoints automatically via XuqmSDK.autoInitialize(). Public deployments use default endpoints.
|
||||
if (deployProps.isPrivate()) {
|
||||
payload.put("serverUrl", normalizeBaseUrl(licensePublicBaseUrl));
|
||||
}
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class LicenseFileCrypto {
|
||||
|
||||
private static final String MAGIC = "XUQM-LICENSE-V1";
|
||||
private static final String PASSPHRASE = "xuqm-license-file-v1.2026.internal";
|
||||
private static final int SALT_BYTES = 16;
|
||||
private static final int IV_BYTES = 12;
|
||||
private static final int KEY_BITS = 256;
|
||||
private static final int ITERATIONS = 120_000;
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private LicenseFileCrypto() {
|
||||
}
|
||||
|
||||
public static String encrypt(String plainText) {
|
||||
try {
|
||||
byte[] salt = randomBytes(SALT_BYTES);
|
||||
byte[] iv = randomBytes(IV_BYTES);
|
||||
SecretKeySpec key = deriveKey(salt);
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return String.join(".",
|
||||
MAGIC,
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(salt),
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(iv),
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(cipherText));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to encrypt license file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKeySpec deriveKey(byte[] salt) throws Exception {
|
||||
PBEKeySpec spec = new PBEKeySpec(PASSPHRASE.toCharArray(), salt, ITERATIONS, KEY_BITS);
|
||||
byte[] encoded = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
return new SecretKeySpec(encoded, "AES");
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
RANDOM.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@ -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.common.exception.BusinessException;
|
||||
import com.xuqm.common.security.LicenseFileCrypto;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/updates")
|
||||
@ -56,21 +57,22 @@ public class AppVersionController {
|
||||
|
||||
@GetMapping("/app/check")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> checkUpdate(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam(required = false) String appKey,
|
||||
@RequestParam AppVersionEntity.Platform platform,
|
||||
@RequestParam int currentVersionCode,
|
||||
@RequestParam @jakarta.validation.constraints.NotBlank String packageName,
|
||||
@RequestParam(required = false) String licenseFile,
|
||||
@RequestParam(required = false) String userId) {
|
||||
|
||||
validatePackageName(appKey, platform, packageName);
|
||||
boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(appKey);
|
||||
String resolvedAppKey = resolveAndValidate(appKey, platform, packageName, licenseFile);
|
||||
boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(resolvedAppKey);
|
||||
|
||||
Optional<AppVersionEntity> latest = versionRepository
|
||||
.findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc(
|
||||
appKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);
|
||||
resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);
|
||||
Optional<AppVersionEntity> forcedHigher = versionRepository
|
||||
.findTopByAppKeyAndPlatformAndPublishStatusAndVersionCodeGreaterThanAndForceUpdateTrueOrderByVersionCodeDesc(
|
||||
appKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);
|
||||
resolvedAppKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode);
|
||||
|
||||
if (latest.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
|
||||
@ -78,8 +80,6 @@ public class AppVersionController {
|
||||
|
||||
AppVersionEntity v = latest.get();
|
||||
|
||||
// Gray release: userId is required when anonymous checks are disabled and version is gray-targeted.
|
||||
// Non-gray published versions are visible to all callers regardless of userId.
|
||||
if (v.isGrayEnabled()) {
|
||||
if (!allowAnonymousCheck && (userId == null || userId.isBlank())) {
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
|
||||
@ -91,16 +91,15 @@ public class AppVersionController {
|
||||
}
|
||||
}
|
||||
} else if (!allowAnonymousCheck && (userId == null || userId.isBlank())) {
|
||||
// App explicitly requires login to check for updates even without gray targeting.
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
|
||||
}
|
||||
|
||||
String appStoreJumpUrl = hasText(v.getAppStoreUrl())
|
||||
? v.getAppStoreUrl()
|
||||
: appStoreService.getStoreJumpUrl(appKey, com.xuqm.update.entity.AppStoreConfigEntity.StoreType.APP_STORE);
|
||||
: appStoreService.getStoreJumpUrl(resolvedAppKey, com.xuqm.update.entity.AppStoreConfigEntity.StoreType.APP_STORE);
|
||||
String harmonyJumpUrl = hasText(v.getMarketUrl())
|
||||
? v.getMarketUrl()
|
||||
: appStoreService.getStoreJumpUrl(appKey, com.xuqm.update.entity.AppStoreConfigEntity.StoreType.HARMONY_APP);
|
||||
: appStoreService.getStoreJumpUrl(resolvedAppKey, com.xuqm.update.entity.AppStoreConfigEntity.StoreType.HARMONY_APP);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"needsUpdate", true,
|
||||
"versionName", v.getVersionName(),
|
||||
@ -495,7 +494,17 @@ public class AppVersionController {
|
||||
return currentStatus == AppVersionEntity.PublishStatus.PUBLISHED ? "PUBLISH" : "SAVE_DRAFT";
|
||||
}
|
||||
|
||||
private void validatePackageName(String appKey, AppVersionEntity.Platform platform, String packageName) {
|
||||
private String resolveAndValidate(String appKey, AppVersionEntity.Platform platform, String packageName, String licenseFile) {
|
||||
if (hasText(licenseFile)) {
|
||||
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
||||
if (!payload.matchesPackageName(packageName)) {
|
||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return payload.appKey();
|
||||
}
|
||||
if (!hasText(appKey)) {
|
||||
throw new BusinessException(400, "appKey 或 licenseFile 必须提供其中一个");
|
||||
}
|
||||
UpdateTenantClient.PlatformInfo info = tenantClient.getPlatformInfo(appKey);
|
||||
String registered = switch (platform) {
|
||||
case IOS -> info.iosBundleId();
|
||||
@ -505,6 +514,7 @@ public class AppVersionController {
|
||||
if (hasText(registered) && !registered.equals(packageName)) {
|
||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
||||
}
|
||||
return appKey;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户