From b77ccc663a533e8cd322862df50cff740774818d Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Mon, 18 May 2026 18:37:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(private-deploy):=20=E6=94=AF=E6=8C=81=20My?= =?UTF-8?q?SQL/Redis=20=E5=A4=96=E9=83=A8=E8=BF=9E=E6=8E=A5=E5=92=8C?= =?UTF-8?q?=E6=89=98=E7=AE=A1=E6=A8=A1=E5=BC=8F=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 external 和 managed 两种数据库/缓存模式支持 - 实现 MySQL/Redis 托管安装脚本和配置向导 - 支持客户自备连接或部署脚本新建基础设施 - 更新部署文档说明不同模式的配置和验证要求 - 添加应用版本防重复上传和删除功能 - 实现应用商店预提交检查和发布计划功能 --- docs-site/docs/guide/flow.md | 17 +- tenant-platform/src/api/update.ts | 31 +++ .../views/update/VersionManagementView.vue | 178 +++++++++++++++++- 3 files changed, 209 insertions(+), 17 deletions(-) diff --git a/docs-site/docs/guide/flow.md b/docs-site/docs/guide/flow.md index 9ef0dab..f2cf573 100644 --- a/docs-site/docs/guide/flow.md +++ b/docs-site/docs/guide/flow.md @@ -15,13 +15,14 @@ ## 私有化流程 1. 运维在私有化部署仓库执行一键部署。 -2. 部署脚本使用用户提供的 MySQL、Redis、域名、证书、SMTP 和厂商凭证完成配置。 -3. 系统初始化内置主租户、运营管理员和默认应用。 -4. 文档站生成私有化 SDK 接入示例。 -5. 客户端集成私有化 SDK。 -6. 客户端使用 `xuqm-private-sdk.json` 初始化。 -7. 业务服务端签发 `UserSig`。 -8. 客户端登录 SDK 并使用 IM、Push、Update、File、License 能力。 +2. 部署脚本先选择 MySQL/Redis 模式:客户自备连接,或由脚本新建并完成数据库、账号、密码和服务配置。 +3. 部署脚本使用 MySQL、Redis、域名、证书、SMTP 和厂商凭证完成配置。 +4. 系统初始化内置主租户、运营管理员和默认应用。 +5. 文档站生成私有化 SDK 接入示例。 +6. 客户端集成私有化 SDK。 +7. 客户端使用 `xuqm-private-sdk.json` 初始化。 +8. 业务服务端签发 `UserSig`。 +9. 客户端登录 SDK 并使用 IM、Push、Update、File、License 能力。 ## 服务端签发 UserSig @@ -50,6 +51,6 @@ 1. 私有化环境不开放主租户注册。 2. 私有化 SDK 不使用 `dev.xuqinmin.com` 作为默认地址。 3. 厂商推送和应用市场自动发布需要客户网络放通厂商公网 API。 -4. MySQL、Redis 由客户提供,部署脚本只做连接校验。 +4. MySQL、Redis 可由客户提供连接,也可选择由部署脚本新建;客户自备模式只做连接校验,脚本新建模式由部署脚本负责安装、初始化和健康检查。 [快速开始](./quickstart) diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts index 10f470f..434cd05 100644 --- a/tenant-platform/src/api/update.ts +++ b/tenant-platform/src/api/update.ts @@ -154,6 +154,26 @@ export interface AppVersion { createdAt: string } +export interface StoreRemoteStateDto { + storeType: StoreType + reviewState: 'ONLINE' | 'UNDER_REVIEW' | 'UNDER_REVIEW_XIAOMI' | 'REJECTED' | 'NOT_FOUND' | 'UNKNOWN' | 'QUERY_FAILED' + onlineVersionName?: string + onlineVersionCode?: string + reviewVersionName?: string + reviewVersionCode?: string + currentSubmissionLive: boolean + nonCurrentRelease: boolean + canSubmit: boolean + blockReason?: string +} + +export interface PreflightSubmitResultDto { + versionId: string + versionName: string + versionCode: number + stores: StoreRemoteStateDto[] +} + export interface AppPackageInspectResult { platform: 'ANDROID' | 'IOS' | 'HARMONY' packageName?: string @@ -341,6 +361,17 @@ export const updateAdminApi = { ) }, + preflightStoreSubmission(versionId: string) { + return updateClient.post<{ data: PreflightSubmitResultDto }>( + `/api/v1/updates/store/app/${versionId}/preflight-submit`, + {}, + ) + }, + + deleteAppVersion(versionId: string) { + return updateClient.delete<{ data: null }>(`/api/v1/updates/app/${versionId}`) + }, + updatePublishSchedule( versionId: string, publishType: 'IMMEDIATE' | 'SCHEDULED', diff --git a/tenant-platform/src/views/update/VersionManagementView.vue b/tenant-platform/src/views/update/VersionManagementView.vue index 65474da..c550d76 100644 --- a/tenant-platform/src/views/update/VersionManagementView.vue +++ b/tenant-platform/src/views/update/VersionManagementView.vue @@ -78,14 +78,14 @@ :type="reviewTagType(item.state)" size="small" style="margin:2px" - >{{ storeLabel(item.store) }} · {{ reviewLabel(item.state) }} + >{{ storeLabel(item.store) }} · {{ reviewDisplayLabel(item) }} {{ storeLabel(item.store) }} · {{ reviewLabel(item.state) }} + >{{ storeLabel(item.store) }} · {{ reviewDisplayLabel(item) }} 提交市场 + 删除 @@ -545,6 +549,56 @@ + + +
+
+

版本 {{ preflightResult.versionName }} 的前置检查结果:

+ + + + + + + + + + + + + + + + + + + 跳过厂商状态检查后继续提交(管理员) + +
+ + +
@@ -574,7 +628,7 @@
{{ storeLabel(item.store) }} - {{ reviewLabel(item.state) }} + {{ reviewDisplayLabel(item) }}
@@ -609,10 +663,18 @@ 批次 {{ item.batchId.slice(0, 8) }}…
+
+ 发布归属 + 非本次发布 +
+
+ 线上版本 + {{ [item.liveVersionName, item.liveVersionCode].filter(Boolean).join(' · ') }} +
-
+
{{ item.reason }}
@@ -915,6 +977,7 @@ import { type GrayMemberGroup, type GrayMode, type GraySelectionSource, + type PreflightSubmitResultDto, type PublishMode, type RnBundle, type RnBundleInspectResult, @@ -1485,9 +1548,30 @@ const selectedStores = ref([]) const submitStoreMode = ref('MANUAL') const submitStoreScheduledAt = ref('') +// Preflight dialog state +const showPreflight = ref(false) +const preflightLoading = ref(false) +const preflightResult = ref(null) +const preflightSkipCheck = ref(false) + const showStoreReviewDetail = ref(false) const storeReviewDetailVersion = ref(null) -const storeReviewDetailItems = ref<{ store: string; state: string; reason?: string; stage?: string; submittedAt?: string; updatedAt?: string; batchId?: string; liveOnStore?: boolean; preExisting?: boolean }[]>([]) +type StoreReviewItem = { + store: string + state: string + reason?: string + stage?: string + submittedAt?: string + updatedAt?: string + batchId?: string + liveOnStore?: boolean + preExisting?: boolean + currentSubmissionLive?: boolean + nonCurrentRelease?: boolean + liveVersionName?: string + liveVersionCode?: string +} +const storeReviewDetailItems = ref([]) const storeReviewDetailLive = ref(false) const cancellingReview = ref(false) const retryingStores = ref>(new Set()) @@ -1584,15 +1668,32 @@ async function handleUpdatePublishSchedule() { } } -function openSubmitStoreDialog(row: AppVersion) { +async function openSubmitStoreDialog(row: AppVersion) { submitStoreVersion.value = row - // Exclude stores already in active review from the pre-selection + preflightResult.value = null + preflightSkipCheck.value = false + showPreflight.value = true + preflightLoading.value = true + try { + const res = await updateAdminApi.preflightStoreSubmission(row.id) + preflightResult.value = res.data.data + // Auto-open submit dialog if all allowed + const allAllowed = preflightResult.value?.stores.every(s => s.canSubmit) ?? false + if (allAllowed) { + showPreflight.value = false + showSubmitStore.value = true + } + } catch (e: any) { + ElMessage.error(e?.response?.data?.message || '前置检查失败') + } finally { + preflightLoading.value = false + } + // Fallback: pre-select stores selectedStores.value = enabledStores.value .map(s => s.type) .filter(t => !isStoreActiveReview(row, t)) submitStoreMode.value = row.storeSubmitMode ?? 'MANUAL' submitStoreScheduledAt.value = row.storeSubmitScheduledAt ?? '' - showSubmitStore.value = true } function getSubmitDialogStoreState(version: AppVersion | null, storeType: string): string { @@ -2143,6 +2244,54 @@ async function promptUnpublishApp(id: string) { } } +async function promptDeleteAppVersion(row: AppVersion) { + try { + await ElMessageBox.confirm( + `确定删除版本 ${row.versionName}(${row.versionCode})吗?删除后不可恢复。`, + '删除确认', + { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }, + ) + await updateAdminApi.deleteAppVersion(row.id) + ElMessage.success('已删除') + await loadAppVersions() + await loadOperationLogs() + } catch (e: any) { + if (e !== 'cancel') { + ElMessage.error(e?.response?.data?.message || '删除失败') + } + } +} + +function hasActiveStoreReview(row: AppVersion): boolean { + if (!row.storeReviewStatus) return false + const items = parseStoreReview(row.storeReviewStatus) + return items.some(i => i.state === 'SUBMITTING' || i.state === 'UNDER_REVIEW') +} + +function preflightTagType(state: string): string { + return { + ONLINE: 'success', + UNDER_REVIEW: 'warning', + UNDER_REVIEW_XIAOMI: 'warning', + REJECTED: 'danger', + NOT_FOUND: 'info', + UNKNOWN: 'info', + QUERY_FAILED: 'danger', + }[state] ?? '' +} + +function preflightStateLabel(state: string): string { + return { + ONLINE: '已上线', + UNDER_REVIEW: '审核中', + UNDER_REVIEW_XIAOMI: '审核中(版本号可能不准确)', + REJECTED: '已拒绝', + NOT_FOUND: '未找到', + UNKNOWN: '未知', + QUERY_FAILED: '查询失败', + }[state] ?? state +} + async function promptUnpublishRn(id: string) { const reason = await promptUnpublishReason('下架确认') if (!reason) return @@ -2179,6 +2328,13 @@ function reviewLabel(state: string): string { }[state] ?? state } +function reviewDisplayLabel(item: StoreReviewItem): string { + if (item.state === 'APPROVED' && (item.nonCurrentRelease || item.currentSubmissionLive === false)) { + return '已上线(非本次发布)' + } + return reviewLabel(item.state) +} + function reviewTagType(state: string): string { return { PENDING: 'info', SUBMITTING: 'primary', UNDER_REVIEW: 'warning', @@ -2308,7 +2464,7 @@ function scheduleStoreReviewReload() { }, 200) } -function parseStoreReview(json?: string): { store: string; state: string; reason?: string; stage?: string; submittedAt?: string; updatedAt?: string; batchId?: string; liveOnStore?: boolean; preExisting?: boolean }[] { +function parseStoreReview(json?: string): StoreReviewItem[] { if (!json) return [] try { const m = JSON.parse(json) as Record @@ -2328,6 +2484,10 @@ function parseStoreReview(json?: string): { store: string; state: string; reason batchId: String(item.batchId ?? ''), liveOnStore: item.liveOnStore === true, preExisting: item.preExisting === true, + currentSubmissionLive: typeof item.currentSubmissionLive === 'boolean' ? item.currentSubmissionLive : undefined, + nonCurrentRelease: item.nonCurrentRelease === true, + liveVersionName: String(item.liveVersionName ?? ''), + liveVersionCode: String(item.liveVersionCode ?? ''), } } return { store, state: String(value ?? ''), reason: '' }