feat(update): 添加应用更新检查功能支持用户ID参数

- 在UpdateApi接口中新增可选的userId查询参数
- 新增UpdateSDK对象用于统一管理应用更新逻辑
- 实现应用版本检查、下载安装和APK文件处理功能
- 添加下载URL规范化处理逻辑
- 在Flutter SDK中新增update模块实现跨平台更新功能
- 在iOS SDK中新增UpdateSDK类提供应用更新检查接口
- 支持Android和iOS平台的应用商店跳转功能
- 添加React Native SDK的更新检查和插件注册功能
- 实现RN Bundle的检查、下载和缓存机制
这个提交包含在:
XuqmGroup 2026-05-08 12:00:34 +08:00
父节点 832d180ff3
当前提交 55826db8c4
共有 4 个文件被更改,包括 76 次插入10 次删除

查看文件

@ -49,6 +49,25 @@ export interface ServiceRequest {
reviewNote?: string reviewNote?: string
createdAt: string createdAt: string
reviewedAt?: string reviewedAt?: string
tenant?: {
id: string
username: string
nickname: string
email: string
phone?: string
type: 'MAIN' | 'SUB'
status: 'ACTIVE' | 'DISABLED' | 'PENDING_EMAIL'
} | null
app?: {
id: string
appKey: string
name: string
packageName: string
iosBundleId?: string
harmonyBundleName?: string
tenantId: string
createdAt: string
} | null
} }
export interface ServiceRequestPage { export interface ServiceRequestPage {

查看文件

@ -11,6 +11,22 @@
<el-table :data="requests" v-loading="loading"> <el-table :data="requests" v-loading="loading">
<el-table-column prop="appKey" label="AppKey" width="180" /> <el-table-column prop="appKey" label="AppKey" width="180" />
<el-table-column label="租户信息" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<div>
<div>{{ row.tenant?.nickname || '-' }}</div>
<div class="el-text--info" style="font-size:12px">{{ row.tenant?.username || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="应用信息" min-width="260" show-overflow-tooltip>
<template #default="{ row }">
<div>
<div>{{ row.app?.name || '-' }}</div>
<div class="el-text--info" style="font-size:12px">{{ row.app?.packageName || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="platform" label="平台" width="100"> <el-table-column prop="platform" label="平台" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag size="small">{{ row.platform }}</el-tag> <el-tag size="small">{{ row.platform }}</el-tag>

查看文件

@ -90,6 +90,7 @@ export interface PublishConfig {
appKey: string appKey: string
configJson?: string configJson?: string
updatedAt: string updatedAt: string
allowAnonymousUpdateCheck?: boolean
} }
export interface OperationLog { export interface OperationLog {

查看文件

@ -87,7 +87,7 @@
link type="warning" size="small" link type="warning" size="small"
@click="openPublishDialog(row, 'app')">重新上架</el-button> @click="openPublishDialog(row, 'app')">重新上架</el-button>
<el-button <el-button
v-if="row.publishStatus === 'PUBLISHED'" v-if="row.publishStatus === 'PUBLISHED' && !publishConfigForm.allowAnonymousUpdateCheck"
link type="warning" size="small" link type="warning" size="small"
@click="openGrayDialog(row, 'app')">灰度</el-button> @click="openGrayDialog(row, 'app')">灰度</el-button>
<el-button <el-button
@ -152,7 +152,7 @@
<template #default="{row}"> <template #default="{row}">
<el-button v-if="row.publishStatus === 'DRAFT'" link type="success" size="small" @click="openPublishDialog(row, 'rn')">发布</el-button> <el-button v-if="row.publishStatus === 'DRAFT'" link type="success" size="small" @click="openPublishDialog(row, 'rn')">发布</el-button>
<el-button v-if="row.publishStatus === 'DEPRECATED'" link type="warning" size="small" @click="openPublishDialog(row, 'rn')">重新上架</el-button> <el-button v-if="row.publishStatus === 'DEPRECATED'" link type="warning" size="small" @click="openPublishDialog(row, 'rn')">重新上架</el-button>
<el-button v-if="row.publishStatus === 'PUBLISHED'" link type="warning" size="small" @click="openGrayDialog(row, 'rn')">灰度</el-button> <el-button v-if="row.publishStatus === 'PUBLISHED' && !publishConfigForm.allowAnonymousUpdateCheck" link type="warning" size="small" @click="openGrayDialog(row, 'rn')">灰度</el-button>
<el-button v-if="row.publishStatus === 'PUBLISHED'" link type="danger" size="small" @click="promptUnpublishRn(row.id)">下架</el-button> <el-button v-if="row.publishStatus === 'PUBLISHED'" link type="danger" size="small" @click="promptUnpublishRn(row.id)">下架</el-button>
</template> </template>
</el-table-column> </el-table-column>
@ -260,36 +260,47 @@
</div> </div>
<el-form :model="publishConfigForm" label-width="170px" class="release-config-form"> <el-form :model="publishConfigForm" label-width="170px" class="release-config-form">
<el-form-item label="更新免登录">
<el-switch v-model="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item>
<el-alert
v-if="publishConfigForm.allowAnonymousUpdateCheck"
type="warning"
:closable="false"
show-icon
title="开启后,未登录设备也可以检查更新,但灰度发布相关功能将被禁用。"
style="margin-bottom:16px"
/>
<el-form-item label="默认灰度模式"> <el-form-item label="默认灰度模式">
<el-radio-group v-model="publishConfigForm.grayMode"> <el-radio-group v-model="publishConfigForm.grayMode" :disabled="publishConfigForm.allowAnonymousUpdateCheck">
<el-radio-button value="PERCENT">比例</el-radio-button> <el-radio-button value="PERCENT">比例</el-radio-button>
<el-radio-button value="MEMBERS">成员</el-radio-button> <el-radio-button value="MEMBERS">成员</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="publishConfigForm.grayMode === 'PERCENT'" label="默认灰度比例"> <el-form-item v-if="publishConfigForm.grayMode === 'PERCENT'" label="默认灰度比例">
<el-slider v-model="publishConfigForm.defaultGrayPercent" :min="1" :max="100" show-input /> <el-slider v-model="publishConfigForm.defaultGrayPercent" :min="1" :max="100" show-input :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item> </el-form-item>
<template v-if="publishConfigForm.grayMode === 'MEMBERS'"> <template v-if="publishConfigForm.grayMode === 'MEMBERS'">
<el-form-item label="成员选择回调"> <el-form-item label="成员选择回调">
<el-input v-model="publishConfigForm.graySelectCallbackUrl" placeholder="选择成员时调用的回调地址" /> <el-input v-model="publishConfigForm.graySelectCallbackUrl" placeholder="选择成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item> </el-form-item>
<el-form-item label="成员选择密钥"> <el-form-item label="成员选择密钥">
<el-input v-model="publishConfigForm.graySelectCallbackSecret" type="password" show-password placeholder="可选,用于成员选择回调验签" /> <el-input v-model="publishConfigForm.graySelectCallbackSecret" type="password" show-password placeholder="可选,用于成员选择回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item> </el-form-item>
<el-form-item label="成员目录同步回调"> <el-form-item label="成员目录同步回调">
<el-input v-model="publishConfigForm.grayDirectorySyncCallbackUrl" placeholder="同步所有成员时调用的回调地址" /> <el-input v-model="publishConfigForm.grayDirectorySyncCallbackUrl" placeholder="同步所有成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item> </el-form-item>
<el-form-item label="成员目录密钥"> <el-form-item label="成员目录密钥">
<el-input v-model="publishConfigForm.grayDirectorySyncCallbackSecret" type="password" show-password placeholder="可选,用于成员同步回调验签" /> <el-input v-model="publishConfigForm.grayDirectorySyncCallbackSecret" type="password" show-password placeholder="可选,用于成员同步回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
</el-form-item> </el-form-item>
<el-form-item label="成员选择方式"> <el-form-item label="成员选择方式">
<el-radio-group v-model="publishConfigForm.graySelectionSource"> <el-radio-group v-model="publishConfigForm.graySelectionSource" :disabled="publishConfigForm.allowAnonymousUpdateCheck">
<el-radio-button value="LOCAL" :disabled="!hasGrayDirectorySyncCallback">同步后本地选择</el-radio-button> <el-radio-button value="LOCAL" :disabled="!hasGrayDirectorySyncCallback">同步后本地选择</el-radio-button>
<el-radio-button value="CALLBACK" :disabled="!hasGraySelectCallback">回调直接返回成员</el-radio-button> <el-radio-button value="CALLBACK" :disabled="!hasGraySelectCallback">回调直接返回成员</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="syncGrayMembers" :loading="loadingGrayMembers" :disabled="!hasGrayDirectorySyncCallback">同步成员</el-button> <el-button @click="syncGrayMembers" :loading="loadingGrayMembers" :disabled="!hasGrayDirectorySyncCallback || publishConfigForm.allowAnonymousUpdateCheck">同步成员</el-button>
</el-form-item> </el-form-item>
</template> </template>
</el-form> </el-form>
@ -700,6 +711,7 @@ const storeConfigs = ref<StoreConfig[]>([])
const loadingPublishConfig = ref(false) const loadingPublishConfig = ref(false)
const savingPublishConfig = ref(false) const savingPublishConfig = ref(false)
const publishConfigForm = ref({ const publishConfigForm = ref({
allowAnonymousUpdateCheck: false,
defaultGrayPercent: 0, defaultGrayPercent: 0,
grayMode: 'PERCENT' as GrayMode, grayMode: 'PERCENT' as GrayMode,
graySelectionSource: 'LOCAL' as GraySelectionSource, graySelectionSource: 'LOCAL' as GraySelectionSource,
@ -734,6 +746,7 @@ const loadingOperationLogs = ref(false)
const hasGraySelectCallback = computed(() => Boolean(publishConfigForm.value.graySelectCallbackUrl.trim())) const hasGraySelectCallback = computed(() => Boolean(publishConfigForm.value.graySelectCallbackUrl.trim()))
const hasGrayDirectorySyncCallback = computed(() => Boolean(publishConfigForm.value.grayDirectorySyncCallbackUrl.trim())) const hasGrayDirectorySyncCallback = computed(() => Boolean(publishConfigForm.value.grayDirectorySyncCallbackUrl.trim()))
const hasAnyGrayCallback = computed(() => hasGraySelectCallback.value || hasGrayDirectorySyncCallback.value) const hasAnyGrayCallback = computed(() => hasGraySelectCallback.value || hasGrayDirectorySyncCallback.value)
const allowAnonymousUpdateCheck = computed(() => Boolean(publishConfigForm.value.allowAnonymousUpdateCheck))
type FieldDef = { key: string; label: string; type?: 'password' | 'textarea'; placeholder?: string } type FieldDef = { key: string; label: string; type?: 'password' | 'textarea'; placeholder?: string }
type GuideStep = { title: string; description: string } type GuideStep = { title: string; description: string }
@ -1041,6 +1054,7 @@ function normalizePublishConfig(raw: Record<string, unknown> | null | undefined)
const grayMode = String(raw?.grayMode ?? 'PERCENT') as GrayMode const grayMode = String(raw?.grayMode ?? 'PERCENT') as GrayMode
const graySelectionSource = String(raw?.graySelectionSource ?? 'LOCAL') as GraySelectionSource const graySelectionSource = String(raw?.graySelectionSource ?? 'LOCAL') as GraySelectionSource
return { return {
allowAnonymousUpdateCheck: Boolean(raw?.allowAnonymousUpdateCheck ?? raw?.defaultAllowAnonymousUpdateCheck ?? false),
defaultGrayPercent: Number((raw as Record<string, unknown>)?.defaultGrayPercent ?? 0), defaultGrayPercent: Number((raw as Record<string, unknown>)?.defaultGrayPercent ?? 0),
grayMode, grayMode,
graySelectionSource, graySelectionSource,
@ -1067,6 +1081,10 @@ async function loadPublishConfig() {
try { try {
const res = await updateAdminApi.getPublishConfig(appKey) const res = await updateAdminApi.getPublishConfig(appKey)
publishConfigForm.value = parsePublishConfig(res.data.data.configJson) publishConfigForm.value = parsePublishConfig(res.data.data.configJson)
if (allowAnonymousUpdateCheck.value) {
publishConfigForm.value.grayMode = 'PERCENT'
publishConfigForm.value.graySelectionSource = 'LOCAL'
}
if (publishConfigForm.value.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) { if (publishConfigForm.value.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
publishConfigForm.value.grayMode = 'PERCENT' publishConfigForm.value.grayMode = 'PERCENT'
} }
@ -1090,6 +1108,10 @@ async function savePublishConfig() {
graySelectCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.graySelectCallbackUrl), graySelectCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.graySelectCallbackUrl),
grayDirectorySyncCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.grayDirectorySyncCallbackUrl), grayDirectorySyncCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.grayDirectorySyncCallbackUrl),
} }
if (payload.allowAnonymousUpdateCheck) {
payload.grayMode = 'PERCENT'
payload.graySelectionSource = 'LOCAL'
}
if (payload.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) { if (payload.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
ElMessage.warning('成员模式至少需要配置一个回调地址') ElMessage.warning('成员模式至少需要配置一个回调地址')
return return
@ -1161,6 +1183,10 @@ const grayForm = ref({
}) })
function openGrayDialog(row: { id: string }, type: 'app' | 'rn') { function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
if (allowAnonymousUpdateCheck.value) {
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
return
}
grayTarget.value = { id: row.id, type } grayTarget.value = { id: row.id, type }
grayForm.value = { grayForm.value = {
enabled: true, enabled: true,
@ -1212,6 +1238,10 @@ async function syncGrayMembers() {
async function submitGray() { async function submitGray() {
if (!grayTarget.value) return if (!grayTarget.value) return
if (allowAnonymousUpdateCheck.value) {
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
return
}
if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !hasGrayDirectorySyncCallback.value) { if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !hasGrayDirectorySyncCallback.value) {
ElMessage.warning('未配置成员目录同步回调,无法选择本地成员') ElMessage.warning('未配置成员目录同步回调,无法选择本地成员')
return return