import { normalizeBackendBaseUrl } from './backendAuth' export interface BackendApiError { statusCode: number code: string message: string details?: unknown } export interface BackendAuthLoginResult { user?: { id?: string nickname?: string avatarUrl?: string } tokens: { accessToken: string refreshToken: string } } export interface BackendResolvedRelease { launchMode: string source: string eventId: string releaseId: string configLabel: string manifestUrl: string manifestChecksumSha256?: string | null routeCode?: string | null } export interface BackendCourseVariantSummary { id: string name: string description?: string | null routeCode?: string | null selectable?: boolean } export interface BackendLaunchVariantSummary { id: string name: string routeCode?: string | null assignmentMode?: string | null } export interface BackendRuntimeSummary { runtimeBindingId?: string | null placeId?: string | null placeName?: string | null mapId?: string | null mapName?: string | null tileReleaseId?: string | null courseSetId?: string | null courseVariantId?: string | null routeCode?: string | null } export interface BackendPresentationSummary { presentationId?: string | null templateKey?: string | null version?: string | null } export interface BackendContentBundleSummary { bundleId?: string | null bundleType?: string | null version?: string | null } export interface BackendEntrySessionSummary { id: string status: string eventId?: string eventName?: string releaseId?: string | null configLabel?: string | null routeCode?: string | null variantId?: string | null variantName?: string | null runtime?: BackendRuntimeSummary | null launchedAt?: string | null startedAt?: string | null endedAt?: string | null // 兼容前端旧字段名,避免联调过渡期多处判断 sessionId?: string sessionStatus?: string eventDisplayName?: string } export interface BackendCardResult { id: string type: string title: string subtitle?: string | null coverUrl?: string | null displaySlot: string displayPriority: number event?: { id: string displayName: string summary?: string | null } | null htmlUrl?: string | null } export interface BackendEntryHomeResult { user: { id: string publicId: string status: string nickname?: string | null avatarUrl?: string | null } tenant: { id: string code: string name: string } channel: { id: string code: string type: string platformAppId?: string | null displayName: string status: string isDefault: boolean } cards: BackendCardResult[] ongoingSession?: BackendEntrySessionSummary | null recentSession?: BackendEntrySessionSummary | null } export interface BackendEventPlayResult { event: { id: string slug: string displayName: string summary?: string | null status: string } currentPresentation?: BackendPresentationSummary | null currentContentBundle?: BackendContentBundleSummary | null release?: { id: string configLabel: string manifestUrl: string manifestChecksumSha256?: string | null routeCode?: string | null } | null resolvedRelease?: BackendResolvedRelease | null play: { canLaunch: boolean primaryAction: string reason: string launchSource?: string assignmentMode?: string | null courseVariants?: BackendCourseVariantSummary[] | null ongoingSession?: BackendEntrySessionSummary | null recentSession?: BackendEntrySessionSummary | null } } export interface BackendLaunchResult { event: { id: string displayName: string } launch: { source: string resolvedRelease?: BackendResolvedRelease | null config: { configUrl: string configLabel: string configChecksumSha256?: string | null releaseId: string routeCode?: string | null } business: { source: string eventId: string sessionId: string sessionToken: string sessionTokenExpiresAt: string routeCode?: string | null } variant?: BackendLaunchVariantSummary | null runtime?: BackendRuntimeSummary | null presentation?: BackendPresentationSummary | null contentBundle?: BackendContentBundleSummary | null } } export interface BackendSessionFinishSummaryPayload { finalDurationSec?: number finalScore?: number completedControls?: number totalControls?: number distanceMeters?: number averageSpeedKmh?: number maxHeartRateBpm?: number } export interface BackendSessionResult { session: { id: string status: string clientType: string deviceKey: string routeCode?: string | null runtime?: BackendRuntimeSummary | null sessionTokenExpiresAt: string launchedAt: string startedAt?: string | null endedAt?: string | null } event: { id: string displayName: string } resolvedRelease?: BackendResolvedRelease | null } export interface BackendSessionResultView { session: BackendEntrySessionSummary result: { status: string finalDurationSec?: number finalScore?: number completedControls?: number totalControls?: number distanceMeters?: number averageSpeedKmh?: number maxHeartRateBpm?: number summary?: Record } } type BackendEnvelope = { data: T } type RequestOptions = { method: 'GET' | 'POST' baseUrl: string path: string authToken?: string body?: Record } function requestBackend(options: RequestOptions): Promise { const url = `${normalizeBackendBaseUrl(options.baseUrl)}${options.path}` const header: Record = {} if (options.body) { header['Content-Type'] = 'application/json' } if (options.authToken) { header.Authorization = `Bearer ${options.authToken}` } return new Promise((resolve, reject) => { wx.request({ url, method: options.method, header, data: options.body, success: (response) => { const statusCode = typeof response.statusCode === 'number' ? response.statusCode : 0 const data = response.data as BackendEnvelope | { error?: { code?: string; message?: string; details?: unknown } } if (statusCode >= 200 && statusCode < 300 && data && typeof data === 'object' && 'data' in data) { resolve((data as BackendEnvelope).data) return } const errorPayload = data && typeof data === 'object' && 'error' in data ? (data as { error?: { code?: string; message?: string; details?: unknown } }).error : undefined reject({ statusCode, code: errorPayload && errorPayload.code ? errorPayload.code : 'backend_error', message: errorPayload && errorPayload.message ? errorPayload.message : `request failed: ${statusCode}`, details: errorPayload && errorPayload.details ? errorPayload.details : response.data, } as BackendApiError) }, fail: (error) => { reject({ statusCode: 0, code: 'network_error', message: error && error.errMsg ? error.errMsg : 'network request failed', } as BackendApiError) }, }) }) } export function loginWechatMini(input: { baseUrl: string code: string deviceKey: string clientType?: string }): Promise { return requestBackend({ method: 'POST', baseUrl: input.baseUrl, path: '/auth/login/wechat-mini', body: { code: input.code, clientType: input.clientType || 'wechat', deviceKey: input.deviceKey, }, }) } export function getEventPlay(input: { baseUrl: string eventId: string accessToken: string }): Promise { return requestBackend({ method: 'GET', baseUrl: input.baseUrl, path: `/events/${encodeURIComponent(input.eventId)}/play`, authToken: input.accessToken, }) } export function getEntryHome(input: { baseUrl: string accessToken: string channelCode: string channelType: string }): Promise { const query = `channelCode=${encodeURIComponent(input.channelCode)}&channelType=${encodeURIComponent(input.channelType)}` return requestBackend({ method: 'GET', baseUrl: input.baseUrl, path: `/me/entry-home?${query}`, authToken: input.accessToken, }) } export function launchEvent(input: { baseUrl: string eventId: string accessToken: string releaseId?: string variantId?: string clientType: string deviceKey: string }): Promise { const body: Record = { clientType: input.clientType, deviceKey: input.deviceKey, } if (input.releaseId) { body.releaseId = input.releaseId } if (input.variantId) { body.variantId = input.variantId } return requestBackend({ method: 'POST', baseUrl: input.baseUrl, path: `/events/${encodeURIComponent(input.eventId)}/launch`, authToken: input.accessToken, body, }) } export function startSession(input: { baseUrl: string sessionId: string sessionToken: string }): Promise { return requestBackend({ method: 'POST', baseUrl: input.baseUrl, path: `/sessions/${encodeURIComponent(input.sessionId)}/start`, body: { sessionToken: input.sessionToken, }, }) } export function finishSession(input: { baseUrl: string sessionId: string sessionToken: string status: 'finished' | 'failed' | 'cancelled' summary: BackendSessionFinishSummaryPayload }): Promise { return requestBackend({ method: 'POST', baseUrl: input.baseUrl, path: `/sessions/${encodeURIComponent(input.sessionId)}/finish`, body: { sessionToken: input.sessionToken, status: input.status, summary: input.summary, }, }) } export function getSessionResult(input: { baseUrl: string accessToken: string sessionId: string }): Promise { return requestBackend({ method: 'GET', baseUrl: input.baseUrl, path: `/sessions/${encodeURIComponent(input.sessionId)}/result`, authToken: input.accessToken, }) } export function getMyResults(input: { baseUrl: string accessToken: string limit?: number }): Promise { const limit = typeof input.limit === 'number' ? input.limit : 20 return requestBackend({ method: 'GET', baseUrl: input.baseUrl, path: `/me/results?limit=${encodeURIComponent(String(limit))}`, authToken: input.accessToken, }) }