From 0385b2010adbc557cdaeaa938f64e8016b4cc6fb Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Fri, 8 May 2026 12:00:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(update):=20=E6=B7=BB=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=9B=B4=E6=96=B0=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=94=A8=E6=88=B7ID=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在UpdateApi接口中新增可选的userId查询参数 - 新增UpdateSDK对象用于统一管理应用更新逻辑 - 实现应用版本检查、下载安装和APK文件处理功能 - 添加下载URL规范化处理逻辑 - 在Flutter SDK中新增update模块实现跨平台更新功能 - 在iOS SDK中新增UpdateSDK类提供应用更新检查接口 - 支持Android和iOS平台的应用商店跳转功能 - 添加React Native SDK的更新检查和插件注册功能 - 实现RN Bundle的检查、下载和缓存机制 --- docs/API_ACCESS.md | 9 ++-- .../xuqm/tenant/controller/OpsController.java | 3 +- .../java/com/xuqm/tenant/dto/AppSummary.java | 14 ++++++ .../xuqm/tenant/dto/ServiceRequestView.java | 19 ++++++++ .../com/xuqm/tenant/dto/TenantSummary.java | 13 +++++ .../com/xuqm/tenant/service/OpsService.java | 47 +++++++++++++++++-- .../controller/AppVersionController.java | 14 ++++-- .../controller/PublishConfigController.java | 15 ++++-- .../update/controller/RnBundleController.java | 27 +++++++++-- .../update/service/PublishConfigService.java | 6 ++- 10 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/dto/AppSummary.java create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/dto/ServiceRequestView.java create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/dto/TenantSummary.java diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index 466e7f2..1c81ac8 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -176,6 +176,7 @@ - `GET /api/sdk/config` 主要给 `update-sdk` 脚本使用,脚本侧直接传 `appKey`。接口会根据 `platform` 返回当前平台的 UPDATE 默认配置和开关状态。 - 应用商店配置页分成两个 tab:`凭据配置` 和 `应用配置指引`。App Store / 鸿蒙只保留 `marketUrl` 跳转页,且该字段是可选项;Android 市场继续保留审核凭据。审核通知使用单独的 `REVIEW_WEBHOOK` 配置,只保存一次,所有市场共用,并会在保存时先做连通性校验。 - 发布配置页保存灰度默认模式、成员目录同步回调和成员选择回调,两个回调都支持单独配置 `secret`,调用时会带 `X-Xuqm-Callback-Secret`。 +- 发布配置里新增 `allowAnonymousUpdateCheck` 开关,默认关闭。关闭时更新检查仍要求登录,且灰度发布可正常使用;开启后允许未登录设备检查更新,但所有灰度相关能力都会被禁用,服务端和租户平台都不会再允许操作灰度配置。 - `POST /api/im/auth/login` 还要求 demo-service 通过 AppSecret 生成签名头再转发给 IM 服务。 - 发版上传建议走两段式:先调 `POST /api/file/upload` 拿到 `url`,再把这个 `url` 作为 `apkUrl` 传给 `POST /api/v1/updates/app/upload` 和 `POST /api/v1/updates/app/inspect`。 - 如果远程包地址暂时不可读,`inspect` 会返回 `detected=false`,发版页可以继续走手动填写 `versionName/versionCode` 的流程,不会因为解析失败直接中断。 @@ -203,6 +204,7 @@ Android SDK 的整包发版脚本支持通过“检查版本 -> 打包 -> 上传 - `xuqm.publishImmediately` - `xuqm.scheduledPublishAt` - `xuqm.autoPublishAfterReview` +- `xuqm.allowAnonymousUpdateCheck` - `xuqm.webhookUrl` - `xuqm.forceUpdate` - `xuqm.grayEnabled` @@ -215,9 +217,10 @@ Android SDK 的整包发版脚本支持通过“检查版本 -> 打包 -> 上传 2. 如果服务未开通或配置缺失,则进入 dry-run 或提示补齐配置。 3. 脚本读取服务器最新版本,和本地版本对比。 4. 本地版本不高于服务端时,提示用户重新输入版本名 / 版本码。 -5. 让用户确认发布时间、市场提交目标、是否审核通过后自动发布、是否启用灰度、是否使用 webhook。 -6. 如果选择了定时上架,`autoPublishAfterReview` 会被服务端强制关闭,避免“定时上架”和“审核后自动发布”同时生效造成冲突。 -7. 完成打包后上传到 update-service,再按配置提交市场或执行发布。 +5. 让用户确认发布时间、市场提交目标、是否审核通过后自动发布、是否启用灰度、是否允许免登录检查更新、是否使用 webhook。 +6. 如果启用了免登录检查更新,灰度配置与灰度操作都不再可用。 +7. 如果选择了定时上架,`autoPublishAfterReview` 会被服务端强制关闭,避免“定时上架”和“审核后自动发布”同时生效造成冲突。 +8. 完成打包后上传到 update-service,再按配置提交市场或执行发布。 租户平台里的“发布配置”标签页保存的就是这组脚本默认值。它按平台分别保存,Android / iOS / Harmony 互不覆盖。 diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java index a40e50a..02f2ba1 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/OpsController.java @@ -5,6 +5,7 @@ import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.entity.OperationLogEntity; import com.xuqm.tenant.entity.ServiceActivationRequestEntity; +import com.xuqm.tenant.dto.ServiceRequestView; import com.xuqm.tenant.entity.TenantEntity; import com.xuqm.tenant.service.FeatureServiceManager; import com.xuqm.tenant.entity.RiskConfigEntity; @@ -97,7 +98,7 @@ public class OpsController { @RequestParam(required = false) String status, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { - Page result = opsService.listServiceRequests(status, page, size); + Page result = opsService.listServiceRequests(status, page, size); return ResponseEntity.ok(ApiResponse.success(Map.of( "content", result.getContent(), "total", result.getTotalElements(), diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/AppSummary.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/AppSummary.java new file mode 100644 index 0000000..a1eef6b --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/AppSummary.java @@ -0,0 +1,14 @@ +package com.xuqm.tenant.dto; + +import java.time.LocalDateTime; + +public record AppSummary( + String id, + String appKey, + String name, + String packageName, + String iosBundleId, + String harmonyBundleName, + String tenantId, + LocalDateTime createdAt +) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/ServiceRequestView.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/ServiceRequestView.java new file mode 100644 index 0000000..cbf8671 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/ServiceRequestView.java @@ -0,0 +1,19 @@ +package com.xuqm.tenant.dto; + +import com.xuqm.tenant.entity.FeatureServiceEntity; +import com.xuqm.tenant.entity.ServiceActivationRequestEntity; +import java.time.LocalDateTime; + +public record ServiceRequestView( + String id, + String appKey, + FeatureServiceEntity.Platform platform, + FeatureServiceEntity.ServiceType serviceType, + ServiceActivationRequestEntity.Status status, + String applyReason, + String reviewNote, + LocalDateTime createdAt, + LocalDateTime reviewedAt, + TenantSummary tenant, + AppSummary app +) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/TenantSummary.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/TenantSummary.java new file mode 100644 index 0000000..aeeffe6 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/TenantSummary.java @@ -0,0 +1,13 @@ +package com.xuqm.tenant.dto; + +import com.xuqm.tenant.entity.TenantEntity; + +public record TenantSummary( + String id, + String username, + String nickname, + String email, + String phone, + TenantEntity.Type type, + TenantEntity.Status status +) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/OpsService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/OpsService.java index 63df014..f6c7352 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/OpsService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/OpsService.java @@ -1,6 +1,9 @@ package com.xuqm.tenant.service; import com.xuqm.common.security.JwtUtil; +import com.xuqm.tenant.dto.AppSummary; +import com.xuqm.tenant.dto.ServiceRequestView; +import com.xuqm.tenant.dto.TenantSummary; import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.entity.OpsAdminEntity; @@ -145,14 +148,52 @@ public class OpsService { ); } - public Page listServiceRequests(String statusStr, int page, int size) { + public Page listServiceRequests(String statusStr, int page, int size) { var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page requests; if (statusStr != null && !statusStr.isEmpty()) { ServiceActivationRequestEntity.Status status = ServiceActivationRequestEntity.Status.valueOf(statusStr.toUpperCase()); - return requestRepository.findByStatusOrderByCreatedAtDesc(status, pageable); + requests = requestRepository.findByStatusOrderByCreatedAtDesc(status, pageable); + } else { + requests = requestRepository.findAllByOrderByCreatedAtDesc(pageable); } - return requestRepository.findAllByOrderByCreatedAtDesc(pageable); + return requests.map(this::toView); + } + + private ServiceRequestView toView(ServiceActivationRequestEntity request) { + AppEntity app = appRepository.findByAppKey(request.getAppKey()).orElse(null); + TenantEntity tenant = app != null ? tenantRepository.findById(app.getTenantId()).orElse(null) : null; + return new ServiceRequestView( + request.getId(), + request.getAppKey(), + request.getPlatform(), + request.getServiceType(), + request.getStatus(), + request.getApplyReason(), + request.getReviewNote(), + request.getCreatedAt(), + request.getReviewedAt(), + tenant == null ? null : new TenantSummary( + tenant.getId(), + tenant.getUsername(), + tenant.getNickname(), + tenant.getEmail(), + tenant.getPhone(), + tenant.getType(), + tenant.getStatus() + ), + app == null ? null : new AppSummary( + app.getId(), + app.getAppKey(), + app.getName(), + app.getPackageName(), + app.getIosBundleId(), + app.getHarmonyBundleName(), + app.getTenantId(), + app.getCreatedAt() + ) + ); } public Page listApps(String keyword, int page, int size) { 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 17eb25e..f336f5c 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 @@ -52,6 +52,11 @@ public class AppVersionController { @RequestParam int currentVersionCode, @RequestParam(required = false) String userId) { + boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(appKey); + if (!allowAnonymousCheck && (userId == null || userId.isBlank())) { + return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false))); + } + Optional latest = versionRepository .findTopByAppIdAndPlatformAndPublishStatusAndVersionCodeGreaterThanOrderByVersionCodeDesc( appKey, platform, AppVersionEntity.PublishStatus.PUBLISHED, currentVersionCode); @@ -66,7 +71,7 @@ public class AppVersionController { AppVersionEntity v = latest.get(); // Gray release filtering - if (v.isGrayEnabled() && userId != null && !userId.isBlank()) { + if (!allowAnonymousCheck && v.isGrayEnabled() && userId != null && !userId.isBlank()) { boolean inGray = false; if ("MEMBERS".equals(v.getGrayMode()) && v.getGrayMemberIds() != null) { inGray = v.getGrayMemberIds().contains(userId); @@ -282,9 +287,12 @@ public class AppVersionController { @PostMapping("/app/{id}/gray") public ResponseEntity> gray( - @PathVariable String id, - @RequestBody Map body) throws Exception { + @PathVariable String id, + @RequestBody Map body) throws Exception { AppVersionEntity entity = versionRepository.findById(id).orElseThrow(); + if (publishConfigService.allowAnonymousUpdateCheck(entity.getAppId())) { + throw new com.xuqm.common.exception.BusinessException(400, "允许免登录检查更新的应用不支持灰度发布"); + } boolean enabled = Boolean.TRUE.equals(body.get("enabled")); String grayMode = body.get("grayMode") == null ? "PERCENT" : body.get("grayMode").toString().trim().toUpperCase(); entity.setGrayEnabled(enabled); diff --git a/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java b/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java index a23ba6d..e98c97f 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/PublishConfigController.java @@ -34,9 +34,12 @@ public class PublishConfigController { @GetMapping("/gray/members") public ResponseEntity>> listMembers( - @RequestParam String appKey, - @RequestParam(required = false) String keyword, - @RequestParam(required = false) String groupName) { + @RequestParam String appKey, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String groupName) { + if (publishConfigService.allowAnonymousUpdateCheck(appKey)) { + throw new com.xuqm.common.exception.BusinessException(400, "允许免登录检查更新的应用不支持灰度成员管理"); + } return ResponseEntity.ok(ApiResponse.success( publishConfigService.listGrayMembers(appKey, keyword, groupName))); } @@ -44,6 +47,9 @@ public class PublishConfigController { @PostMapping("/gray/members/sync") public ResponseEntity>> syncMembers( @RequestParam String appKey) { + if (publishConfigService.allowAnonymousUpdateCheck(appKey)) { + throw new com.xuqm.common.exception.BusinessException(400, "允许免登录检查更新的应用不支持灰度成员管理"); + } return ResponseEntity.ok(ApiResponse.success(publishConfigService.syncGrayMembers(appKey))); } @@ -51,6 +57,9 @@ public class PublishConfigController { public ResponseEntity>> importMembers( @RequestParam String appKey, @RequestBody String payload) { + if (publishConfigService.allowAnonymousUpdateCheck(appKey)) { + throw new com.xuqm.common.exception.BusinessException(400, "允许免登录检查更新的应用不支持灰度成员管理"); + } return ResponseEntity.ok(ApiResponse.success(publishConfigService.replaceGrayMembers(appKey, payload))); } 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 e6f5368..07b6a04 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 @@ -46,7 +46,13 @@ public class RnBundleController { @RequestParam String moduleId, @RequestParam String platform, @RequestParam String currentVersion, - @RequestParam(required = false) String packageName) { + @RequestParam(required = false) String packageName, + @RequestParam(required = false) String userId) { + + boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(appKey); + if (!allowAnonymousCheck && (userId == null || userId.isBlank())) { + return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false))); + } RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase()); Optional latest = bundleRepository @@ -59,6 +65,18 @@ public class RnBundleController { RnBundleEntity b = latest.get(); boolean needsUpdate = !b.getVersion().equals(currentVersion); + if (!allowAnonymousCheck && b.isGrayEnabled() && userId != null && !userId.isBlank()) { + boolean inGray = false; + if ("MEMBERS".equals(b.getGrayMode()) && b.getGrayMemberIds() != null) { + inGray = b.getGrayMemberIds().contains(userId); + } else { + int hash = Math.abs(userId.hashCode()) % 100; + inGray = hash < b.getGrayPercent(); + } + if (!inGray) { + needsUpdate = false; + } + } return ResponseEntity.ok(ApiResponse.success(Map.of( "needsUpdate", needsUpdate, "bundleVersion", parseBundleVersion(b.getVersion()), @@ -216,9 +234,12 @@ public class RnBundleController { @PostMapping("/{id}/gray") public ResponseEntity> gray( - @PathVariable String id, - @RequestBody Map body) throws Exception { + @PathVariable String id, + @RequestBody Map body) throws Exception { RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); + if (publishConfigService.allowAnonymousUpdateCheck(entity.getAppId())) { + throw new com.xuqm.common.exception.BusinessException(400, "允许免登录检查更新的应用不支持灰度发布"); + } boolean enabled = Boolean.TRUE.equals(body.get("enabled")); String grayMode = body.get("grayMode") == null ? "PERCENT" : body.get("grayMode").toString().trim().toUpperCase(); entity.setGrayEnabled(enabled); 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 3187aa3..349ab17 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 @@ -315,10 +315,14 @@ public class PublishConfigService { private String defaultConfigJson() { return """ - {"defaultGrayPercent":0,"grayMode":"PERCENT","graySelectCallbackUrl":"","graySelectCallbackSecret":"","grayDirectorySyncCallbackUrl":"","grayDirectorySyncCallbackSecret":"","graySelectionSource":"LOCAL"} + {"allowAnonymousUpdateCheck":false,"defaultGrayPercent":0,"grayMode":"PERCENT","graySelectCallbackUrl":"","graySelectCallbackSecret":"","grayDirectorySyncCallbackUrl":"","grayDirectorySyncCallbackSecret":"","graySelectionSource":"LOCAL"} """.trim(); } + public boolean allowAnonymousUpdateCheck(String appKey) { + return getConfigNode(appKey).path("allowAnonymousUpdateCheck").asBoolean(false); + } + public record GrayMemberGroupView(String groupName, List members) {} public record GrayMemberView(String userId, String name, String groupName, String extraJson, String updatedAt) {} private record GrayMemberGroupPayload(String groupName, List members) {}