XuqmGroup-Web/tenant-platform/src/api/update.ts
XuqmGroup a1e4d5741b docs(private): 完善私有化部署开发计划和设计规范
- 增加实时进度和交接规则,定义任务状态枚举和更新格式
- 创建任务进度台账,涵盖P0-P5阶段全部开发任务
- 补充部署仓库交付边界确认和进度审计规范
- 完善MySQL/Redis双模式支持,增加external/managed选项
- 增加离线部署、安全治理、可观测性等完整交付能力
- 更新仓库结构设计,增加secrets.env、observability、data目录
- 补充健康检查、诊断脚本、升级回滚、备份恢复详细要求
- 优化应用商店审核状态查询逻辑,增加手动刷新接口
- 修复小米和VIVO商店状态查询中的版本匹配逻辑错误
- 增加缓存键版本隔离,防止不同版本状态混淆
- 优化厂商API连通性检查和审核状态轮询机制
2026-05-18 19:00:38 +08:00

419 行
12 KiB
TypeScript

import axios, { type AxiosProgressEvent } from 'axios'
import { isJwtExpired } from '@/utils/jwt'
const updateClient = axios.create({
baseURL: import.meta.env.VITE_UPDATE_API_BASE_URL ?? '',
timeout: 30000,
})
type UploadProgressHandler = (percent: number) => void
function uploadProgressConfig(onProgress?: UploadProgressHandler) {
if (!onProgress) return {}
return {
onUploadProgress: (event: AxiosProgressEvent) => {
const total = event.total ?? 0
if (!total) return
onProgress(Math.min(100, Math.round((event.loaded * 100) / total)))
},
}
}
if (import.meta.env.DEV) {
updateClient.interceptors.request.use((config) => {
console.debug('[tenant-platform][UPDATE] request', {
method: config.method?.toUpperCase(),
url: config.baseURL ? `${config.baseURL}${config.url ?? ''}` : config.url,
params: config.params,
})
return config
})
updateClient.interceptors.response.use((res) => {
console.debug('[tenant-platform][UPDATE] response', {
status: res.status,
url: res.config.url,
})
return res
})
}
updateClient.interceptors.request.use((config) => {
const url = config.url ?? ''
const skipAuth = (
url.startsWith('/api/v1/updates/app/check') ||
url.startsWith('/api/v1/updates/app/inspect') ||
url.startsWith('/api/v1/rn/update/check') ||
url.startsWith('/api/v1/rn/inspect') ||
url.startsWith('/api/v1/rn/files/')
)
if (!skipAuth) {
const token = localStorage.getItem('token')
if (token && !isJwtExpired(token)) {
config.headers.Authorization = `Bearer ${token}`
} else if (token && isJwtExpired(token)) {
localStorage.removeItem('token')
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
window.location.href = `/login?reason=${encodeURIComponent('登录已失效,请重新登录')}`
}
return Promise.reject(new Error('登录已失效,请重新登录'))
}
}
return config
})
updateClient.interceptors.response.use(
(response) => response,
(error) => {
const status = error?.response?.status
if (status === 401) {
localStorage.removeItem('token')
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
window.location.href = `/login?reason=${encodeURIComponent('登录已失效,请重新登录')}`
}
return Promise.reject(error)
}
if (status === 403) {
return Promise.reject(error)
}
return Promise.reject(error)
},
)
export type StoreType = 'HUAWEI' | 'MI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APP_STORE' | 'GOOGLE_PLAY' | 'HARMONY_APP' | 'REVIEW_WEBHOOK'
export type StoreReviewState = 'PENDING' | 'SUBMITTING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED'
export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW'
export type GrayMode = 'PERCENT' | 'MEMBERS'
export type GraySelectionSource = 'LOCAL' | 'CALLBACK'
export interface PublishConfig {
id: string
appKey: string
configJson?: string
updatedAt: string
allowAnonymousUpdateCheck?: boolean
}
export interface OperationLog {
id: string
appKey: string
resourceType: string
resourceId: string
action: string
operator?: string
reason?: string
detailJson?: string
createdAt: string
}
export interface GrayMember {
userId: string
name?: string
groupName?: string
extraJson?: string
updatedAt?: string
}
export interface GrayMemberGroup {
groupName: string
members: GrayMember[]
}
export interface StoreConfig {
id: string
appKey: string
storeType: StoreType
configJson?: string
enabled: boolean
updatedAt: string
}
export interface AppVersion {
id: string
appKey: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
versionName: string
versionCode: number
packageName?: string
downloadUrl?: string
changeLog?: string
forceUpdate: boolean
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
grayEnabled: boolean
grayPercent: number
appStoreUrl?: string
marketUrl?: string
scheduledPublishAt?: string
autoPublishAfterReview: boolean
webhookUrl?: string
storeSubmitTargets?: string
storeReviewStatus?: string
storeSubmitMode?: PublishMode
storeSubmitScheduledAt?: string
grayMode?: GrayMode
grayMemberIds?: string
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
versionName?: string
versionCode?: number
fileName?: string
detected: boolean
}
export interface RnBundle {
id: string
appKey: string
moduleId: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
version: string
md5: string
minCommonVersion?: string
packageName?: string
note?: string
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
publishMode?: PublishMode
scheduledPublishAt?: string
grayEnabled: boolean
grayPercent: number
grayMode?: GrayMode
grayMemberIds?: string
createdAt: string
}
export interface RnBundleInspectResult {
moduleId?: string
platform?: 'ANDROID' | 'IOS' | 'HARMONY'
version?: string
minCommonVersion?: string
packageName?: string
fileName?: string
detected: boolean
}
export interface UnifiedAppUploadItem {
fileKey: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
versionName: string
versionCode: number
changeLog?: string
forceUpdate: boolean
packageName?: string
appStoreUrl?: string
marketUrl?: string
publishImmediately: boolean
}
export interface UnifiedRnUploadItem {
fileKey: string
moduleId: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
version: string
minCommonVersion?: string
packageName?: string
note?: string
}
export interface UnifiedReleaseManifest {
appVersions: UnifiedAppUploadItem[]
rnBundles: UnifiedRnUploadItem[]
}
export const updateAdminApi = {
listAppVersions(appKey: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') {
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
params: { appKey, platform },
})
},
publishAppVersion(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string; forceUpdate?: boolean }) {
return updateClient.post(`/api/v1/updates/app/${id}/publish`, body ?? {})
},
unpublishAppVersion(id: string, reason: string) {
return updateClient.post(`/api/v1/updates/app/${id}/unpublish`, { reason })
},
patchChangeLog(id: string, changeLog: string) {
return updateClient.patch<{ data: AppVersion }>(`/api/v1/updates/app/${id}/changelog`, { changeLog })
},
grayAppVersion(id: string, body: {
enabled: boolean
grayMode: GrayMode
percent?: number
memberIds?: string[]
selectionSource?: GraySelectionSource
}) {
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
},
uploadAppVersion(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, uploadProgressConfig(onProgress))
},
inspectAppPackage(apkUrl: string) {
return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', {
params: { apkUrl },
})
},
listRnBundles(appKey: string, moduleId?: string, platform?: string) {
return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', {
params: { appKey, ...(moduleId && { moduleId }), ...(platform && { platform }) },
})
},
publishRnBundle(id: string, body?: { publishImmediately?: boolean; scheduledPublishAt?: string }) {
return updateClient.post(`/api/v1/rn/${id}/publish`, body ?? {})
},
unpublishRnBundle(id: string, reason: string) {
return updateClient.post(`/api/v1/rn/${id}/unpublish`, { reason })
},
grayRnBundle(id: string, body: {
enabled: boolean
grayMode: GrayMode
percent?: number
memberIds?: string[]
selectionSource?: GraySelectionSource
}) {
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
},
uploadRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress))
},
inspectRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, uploadProgressConfig(onProgress))
},
uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) {
return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress))
},
// ── Store config ────────────────────────────────────────────────────────
getStoreConfigs(appKey: string) {
return updateClient.get<{ data: StoreConfig[] }>('/api/v1/updates/store/configs', { params: { appKey } })
},
saveStoreConfig(appKey: string, storeType: StoreType, configJson: string, enabled: boolean) {
return updateClient.put<{ data: StoreConfig }>(
`/api/v1/updates/store/configs/${storeType}`,
{ configJson, enabled },
{ params: { appKey } },
)
},
deleteStoreConfig(appKey: string, storeType: StoreType) {
return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appKey } })
},
executeSubmitToStores(
versionId: string,
storeTypes: StoreType[],
submitMode: PublishMode = 'MANUAL',
scheduledPublishAt?: string,
autoPublishAfterReview = false,
) {
return updateClient.post<{ data: AppVersion }>(
`/api/v1/updates/store/app/${versionId}/execute-submit`,
{ storeTypes, submitMode, scheduledPublishAt, autoPublishAfterReview },
)
},
updateStoreReview(versionId: string, storeType: StoreType, state: StoreReviewState, reason?: string) {
return updateClient.post<{ data: AppVersion }>(
`/api/v1/updates/store/app/${versionId}/review`,
{ storeType, state, ...(reason ? { reason } : {}) },
)
},
cancelStoreReview(versionId: string, storeTypes?: StoreType[]) {
return updateClient.post<{ data: null }>(
`/api/v1/updates/store/app/${versionId}/cancel-review`,
storeTypes ? { storeTypes } : {},
)
},
preflightStoreSubmission(versionId: string) {
return updateClient.post<{ data: PreflightSubmitResultDto }>(
`/api/v1/updates/store/app/${versionId}/preflight-submit`,
{},
)
},
refreshStoreReviewStatus(versionId: string) {
return updateClient.post<{ data: PreflightSubmitResultDto }>(
`/api/v1/updates/store/app/${versionId}/refresh-review-status`,
{},
)
},
deleteAppVersion(versionId: string) {
return updateClient.delete<{ data: null }>(`/api/v1/updates/app/${versionId}`)
},
updatePublishSchedule(
versionId: string,
publishType: 'IMMEDIATE' | 'SCHEDULED',
scheduledAt?: string,
) {
return updateClient.put<{ data: AppVersion }>(
`/api/v1/updates/store/app/${versionId}/publish-schedule`,
{ publishType, scheduledAt },
)
},
getPublishConfig(appKey: string) {
return updateClient.get<{ data: PublishConfig }>('/api/v1/updates/publish/config', { params: { appKey } })
},
savePublishConfig(appKey: string, config: Record<string, unknown>) {
return updateClient.put<{ data: PublishConfig }>('/api/v1/updates/publish/config', config, { params: { appKey } })
},
listOperationLogs(appKey: string, limit = 100) {
return updateClient.get<{ data: OperationLog[] }>('/api/v1/updates/ops/logs', {
params: { appKey, limit },
})
},
listGrayMembers(appKey: string, keyword?: string, groupName?: string) {
return updateClient.get<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members', {
params: { appKey, ...(keyword && { keyword }), ...(groupName && { groupName }) },
})
},
syncGrayMembers(appKey: string) {
return updateClient.post<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members/sync', null, {
params: { appKey },
})
},
}