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

659 行
19 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/updates/public/') ||
url.startsWith('/api/v1/rn/release-set/check') ||
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 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
source: 'SYNC' | 'MANUAL'
active: boolean
tags: string[]
}
export interface GrayTag {
tagName: string
memberCount: number
}
export interface StoreConfig {
id: string
appKey: string
storeType: StoreType
configJson?: string
enabled: boolean
updatedAt: string
}
export type StoreMonitoringType = 'APP_STORE' | 'HARMONY_APP'
export type StoreMonitoringPlatform = 'IOS' | 'HARMONY'
export type StoreVersionState =
| 'DISCOVERED'
| 'PREPARING'
| 'SUBMITTED'
| 'WAITING_FOR_REVIEW'
| 'IN_REVIEW'
| 'REJECTED'
| 'APPROVED_PENDING_RELEASE'
| 'RELEASE_SCHEDULED'
| 'RELEASE_REQUESTED'
| 'PROCESSING_DISTRIBUTION'
| 'PARTIALLY_AVAILABLE'
| 'AVAILABLE'
| 'WITHDRAWN'
| 'REMOVED'
| 'UNKNOWN'
| 'SYNC_FAILED'
export interface StoreMonitoringBinding {
id: string
appKey: string
platform: StoreMonitoringPlatform
storeType: StoreMonitoringType
packageIdentifier: string
officialAppId?: string
marketUrl?: string
targetRegion: 'CHN'
enabled: boolean
monitorCredentialConfigured: boolean
lastSyncStatus?: string
lastSyncError?: string
lastSyncedAt?: string
}
export interface StoreMonitoringBindingInput {
packageIdentifier: string
marketUrl?: string
enabled: boolean
credentialDisplayName?: string
credential?: Record<string, string>
}
export interface StoreMonitoringVersion {
id: string
officialVersionId: string
versionName: string
buildVersion: string
source: 'XUQM_PLATFORM' | 'EXTERNAL_STORE'
state: StoreVersionState
rawState?: string
chinaAvailable: boolean
releaseSequence?: number
stateOccurredAt?: string
lastSyncedAt?: string
}
export interface StoreMonitoringEvent {
id: string
eventType: string
source: 'WEBHOOK' | 'POLL' | 'MANUAL' | 'IMPORT'
previousState?: StoreVersionState
currentState?: StoreVersionState
rawState?: string
occurredAt?: string
receivedAt?: string
}
export interface StoreUpdateDraft {
storeVersionId: string
appVersionId: string
platform: StoreMonitoringPlatform
versionName: string
buildVersion: string
releaseSequence: number
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
storeUrl?: string
}
export interface StoreNotificationPolicy {
emailEnabled: boolean
emailRecipients: string[]
webhookEnabled: boolean
webhookUrl?: string
webhookSecretConfigured: boolean
eventTypes: string[]
updatedAt?: string
}
export interface StoreNotificationPolicyInput {
emailEnabled: boolean
emailRecipients: string[]
webhookEnabled: boolean
webhookUrl?: string
webhookSecret?: string
eventTypes: 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
packageName: string
moduleId: string
type: 'common' | 'app' | 'buz'
platform: 'ANDROID' | 'IOS'
version: string
appVersionRange: string
builtAgainstNativeBaselineId: string
buildId: string
bundleFormat: string
commonVersionRange?: string
minNativeApiLevel: number
bundleSha256: string
archiveSha256: string
keyId: string
signature: string
note?: string
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
publishMode?: PublishMode
scheduledPublishAt?: string
grayEnabled: boolean
grayPercent: number
grayMode?: GrayMode
grayMemberIds?: string
createdAt: string
}
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 UnifiedReleaseManifest {
appVersions: UnifiedAppUploadItem[]
}
export interface DownloadPageHistoryItem {
versionName: string
versionCode: number
downloadUrl?: string
available: boolean
changeLog?: string
publishedAt: string
current: boolean
}
export interface DownloadPageAppInfo {
versionName: string
versionCode: number
downloadUrl?: string
available?: boolean
changeLog?: string
storeUrl?: string
storeLinks?: Record<string, string>
publishedAt: string
history?: DownloadPageHistoryItem[]
}
export interface DownloadPageInfo {
android: DownloadPageAppInfo | null
ios: DownloadPageAppInfo | null
harmony: DownloadPageAppInfo | null
}
export function fetchDownloadPageInfo(appKey: string) {
return updateClient.get<{ data: DownloadPageInfo }>('/api/v1/updates/public/download-info', {
params: { appKey },
})
}
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
groupNames?: string[]
extraMemberIds?: string[]
}) {
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
groupNames?: string[]
extraMemberIds?: string[]
}) {
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))
},
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 } })
},
// ── iOS / Harmony official store read-only monitoring ──────────────────
listStoreMonitoringBindings(appKey: string) {
return updateClient.get<{ data: StoreMonitoringBinding[] }>(
'/api/v1/updates/store-monitoring/bindings',
{ params: { appKey } },
)
},
saveStoreMonitoringBinding(
appKey: string,
storeType: StoreMonitoringType,
body: StoreMonitoringBindingInput,
) {
return updateClient.put<{ data: StoreMonitoringBinding }>(
`/api/v1/updates/store-monitoring/bindings/${storeType}`,
body,
{ params: { appKey } },
)
},
synchronizeStoreMonitoring(appKey: string, storeType: StoreMonitoringType) {
return updateClient.post<{ data: StoreMonitoringVersion[] }>(
`/api/v1/updates/store-monitoring/bindings/${storeType}/sync`,
null,
{ params: { appKey } },
)
},
listStoreMonitoringVersions(appKey: string, bindingId: string) {
return updateClient.get<{ data: StoreMonitoringVersion[] }>(
`/api/v1/updates/store-monitoring/bindings/${bindingId}/versions`,
{ params: { appKey } },
)
},
listStoreMonitoringEvents(appKey: string, bindingId: string) {
return updateClient.get<{ data: StoreMonitoringEvent[] }>(
`/api/v1/updates/store-monitoring/bindings/${bindingId}/events`,
{ params: { appKey } },
)
},
createStoreUpdateDraft(appKey: string, bindingId: string, storeVersionId: string) {
return updateClient.post<{ data: StoreUpdateDraft }>(
`/api/v1/updates/store-monitoring/bindings/${bindingId}/versions/${storeVersionId}/update-draft`,
null,
{ params: { appKey } },
)
},
getStoreNotificationPolicy(appKey: string) {
return updateClient.get<{ data: StoreNotificationPolicy }>(
'/api/v1/updates/store-monitoring/notification-policy',
{ params: { appKey } },
)
},
saveStoreNotificationPolicy(appKey: string, body: StoreNotificationPolicyInput) {
return updateClient.put<{ data: StoreNotificationPolicy }>(
'/api/v1/updates/store-monitoring/notification-policy',
body,
{ 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) {
return updateClient.get<{ data: GrayMember[] }>('/api/v1/updates/gray/members', {
params: { appKey },
})
},
addGrayMembers(appKey: string, userIds: string[], name?: string) {
return updateClient.post('/api/v1/updates/gray/members', { appKey, userIds, name })
},
deleteGrayMember(appKey: string, userId: string) {
return updateClient.delete(`/api/v1/updates/gray/members/${userId}`, { params: { appKey } })
},
syncGrayMembers(appKey: string, members: { userId: string; name?: string }[]) {
return updateClient.post<{ data: { added: number; updated: number; removed: number } }>(
'/api/v1/updates/gray/members/sync', { appKey, members })
},
importGrayMembersFromIm(appKey: string) {
return updateClient.post<{ data: { added: number; updated: number; removed: number } }>(
'/api/v1/updates/gray/members/import-im', null, { params: { appKey } })
},
listGrayTags(appKey: string) {
return updateClient.get<{ data: GrayTag[] }>('/api/v1/updates/gray/tags', { params: { appKey } })
},
createGrayTag(appKey: string, tagName: string, userIds: string[]) {
return updateClient.post('/api/v1/updates/gray/tags', { appKey, tagName, userIds })
},
deleteGrayTag(appKey: string, tagName: string) {
return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}`, { params: { appKey } })
},
addMembersToTag(appKey: string, tagName: string, userIds: string[]) {
return updateClient.post(`/api/v1/updates/gray/tags/${tagName}/members`, { appKey, userIds })
},
removeMembersFromTag(appKey: string, tagName: string, userIds: string[]) {
return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}/members`, { data: { appKey, userIds } })
},
// ── Statistics ─────────────────────────────────────────────────────────
getVersionDistribution(appKey: string, platform: string, days?: number) {
return updateClient.get<{ data: { versionCode: number; versionName: string; deviceCount: number }[] }>(
'/api/v1/updates/stats/version-distribution',
{ params: { appKey, platform, days: days ?? 30 } },
)
},
getDeviceDistribution(appKey: string, platform: string, days?: number) {
return updateClient.get<{
data: {
models: { model: string; deviceCount: number }[]
osVersions: { osVersion: string; deviceCount: number }[]
vendors: { vendor: string; deviceCount: number }[]
}
}>('/api/v1/updates/stats/device-distribution', { params: { appKey, platform, days: days ?? 30 } })
},
getDailyTrend(appKey: string, platform: string, days?: number) {
return updateClient.get<{ data: { day: string; deviceCount: number }[] }>(
'/api/v1/updates/stats/daily-trend',
{ params: { appKey, platform, days: days ?? 7 } },
)
},
}