Files
cmr-mini/miniprogram/utils/backendApi.ts

457 lines
11 KiB
TypeScript

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<string, unknown>
}
}
export interface BackendClientLogInput {
source: string
level: 'debug' | 'info' | 'warn' | 'error'
category: string
message: string
eventId?: string
releaseId?: string
sessionId?: string
manifestUrl?: string
route?: string
occurredAt?: string
details?: Record<string, unknown>
}
type BackendEnvelope<T> = {
data: T
}
type RequestOptions = {
method: 'GET' | 'POST'
baseUrl: string
path: string
authToken?: string
body?: Record<string, unknown>
}
function requestBackend<T>(options: RequestOptions): Promise<T> {
const url = `${normalizeBackendBaseUrl(options.baseUrl)}${options.path}`
const header: Record<string, string> = {}
if (options.body) {
header['Content-Type'] = 'application/json'
}
if (options.authToken) {
header.Authorization = `Bearer ${options.authToken}`
}
return new Promise<T>((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<T> | { error?: { code?: string; message?: string; details?: unknown } }
if (statusCode >= 200 && statusCode < 300 && data && typeof data === 'object' && 'data' in data) {
resolve((data as BackendEnvelope<T>).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<BackendAuthLoginResult> {
return requestBackend<BackendAuthLoginResult>({
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<BackendEventPlayResult> {
return requestBackend<BackendEventPlayResult>({
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<BackendEntryHomeResult> {
const query = `channelCode=${encodeURIComponent(input.channelCode)}&channelType=${encodeURIComponent(input.channelType)}`
return requestBackend<BackendEntryHomeResult>({
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<BackendLaunchResult> {
const body: Record<string, unknown> = {
clientType: input.clientType,
deviceKey: input.deviceKey,
}
if (input.releaseId) {
body.releaseId = input.releaseId
}
if (input.variantId) {
body.variantId = input.variantId
}
return requestBackend<BackendLaunchResult>({
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<BackendSessionResult> {
return requestBackend<BackendSessionResult>({
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<BackendSessionResult> {
return requestBackend<BackendSessionResult>({
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<BackendSessionResultView> {
return requestBackend<BackendSessionResultView>({
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<BackendSessionResultView[]> {
const limit = typeof input.limit === 'number' ? input.limit : 20
return requestBackend<BackendSessionResultView[]>({
method: 'GET',
baseUrl: input.baseUrl,
path: `/me/results?limit=${encodeURIComponent(String(limit))}`,
authToken: input.accessToken,
})
}
export function postClientLog(input: {
baseUrl: string
payload: BackendClientLogInput
}): Promise<void> {
return requestBackend<void>({
method: 'POST',
baseUrl: input.baseUrl,
path: '/dev/client-logs',
body: input.payload as unknown as Record<string, unknown>,
})
}