XuqmGroup-Web/tenant-platform/src/api/update.ts

358 行
10 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' | '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
appId: string
configJson?: string
updatedAt: string
}
export interface OperationLog {
id: string
appId: 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
appId: string
storeType: StoreType
configJson?: string
enabled: boolean
updatedAt: string
}
export interface AppVersion {
id: string
appId: 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 AppPackageInspectResult {
platform: 'ANDROID' | 'IOS' | 'HARMONY'
packageName?: string
versionName?: string
versionCode?: number
fileName?: string
detected: boolean
}
export interface RnBundle {
id: string
appId: 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(appId: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') {
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
params: { appId, 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 })
},
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(appId: string, moduleId?: string, platform?: string) {
return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', {
params: { appId, ...(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(appId: string) {
return updateClient.get<{ data: StoreConfig[] }>('/api/v1/updates/store/configs', { params: { appId } })
},
saveStoreConfig(appId: string, storeType: StoreType, configJson: string, enabled: boolean) {
return updateClient.put<{ data: StoreConfig }>(
`/api/v1/updates/store/configs/${storeType}`,
{ configJson, enabled },
{ params: { appId } },
)
},
deleteStoreConfig(appId: string, storeType: StoreType) {
return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appId } })
},
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) {
return updateClient.post<{ data: AppVersion }>(
`/api/v1/updates/store/app/${versionId}/review`,
{ storeType, state },
)
},
getPublishConfig(appId: string) {
return updateClient.get<{ data: PublishConfig }>('/api/v1/updates/publish/config', { params: { appId } })
},
savePublishConfig(appId: string, config: Record<string, unknown>) {
return updateClient.put<{ data: PublishConfig }>('/api/v1/updates/publish/config', config, { params: { appId } })
},
listOperationLogs(appId: string, limit = 100) {
return updateClient.get<{ data: OperationLog[] }>('/api/v1/updates/ops/logs', {
params: { appId, limit },
})
},
listGrayMembers(appId: string, keyword?: string, groupName?: string) {
return updateClient.get<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members', {
params: { appId, ...(keyword && { keyword }), ...(groupName && { groupName }) },
})
},
syncGrayMembers(appId: string) {
return updateClient.post<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members/sync', null, {
params: { appId },
})
},
}