backendApi.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import { normalizeBackendBaseUrl } from './backendAuth'
  2. export interface BackendApiError {
  3. statusCode: number
  4. code: string
  5. message: string
  6. details?: unknown
  7. }
  8. export interface BackendAuthLoginResult {
  9. user?: {
  10. id?: string
  11. nickname?: string
  12. avatarUrl?: string
  13. }
  14. tokens: {
  15. accessToken: string
  16. refreshToken: string
  17. }
  18. }
  19. export interface BackendResolvedRelease {
  20. launchMode: string
  21. source: string
  22. eventId: string
  23. releaseId: string
  24. configLabel: string
  25. manifestUrl: string
  26. manifestChecksumSha256?: string | null
  27. routeCode?: string | null
  28. }
  29. export interface BackendCourseVariantSummary {
  30. id: string
  31. name: string
  32. description?: string | null
  33. routeCode?: string | null
  34. selectable?: boolean
  35. }
  36. export interface BackendLaunchVariantSummary {
  37. id: string
  38. name: string
  39. routeCode?: string | null
  40. assignmentMode?: string | null
  41. }
  42. export interface BackendRuntimeSummary {
  43. runtimeBindingId?: string | null
  44. placeId?: string | null
  45. placeName?: string | null
  46. mapId?: string | null
  47. mapName?: string | null
  48. tileReleaseId?: string | null
  49. courseSetId?: string | null
  50. courseVariantId?: string | null
  51. routeCode?: string | null
  52. }
  53. export interface BackendPresentationSummary {
  54. presentationId?: string | null
  55. templateKey?: string | null
  56. version?: string | null
  57. }
  58. export interface BackendPreviewControlSummary {
  59. id?: string | null
  60. label?: string | null
  61. kind?: string | null
  62. lon?: number | null
  63. lat?: number | null
  64. }
  65. export interface BackendPreviewLegSummary {
  66. fromLon?: number | null
  67. fromLat?: number | null
  68. toLon?: number | null
  69. toLat?: number | null
  70. }
  71. export interface BackendPreviewVariantSummary {
  72. variantId?: string | null
  73. id?: string | null
  74. name?: string | null
  75. routeCode?: string | null
  76. controls?: BackendPreviewControlSummary[] | null
  77. legs?: BackendPreviewLegSummary[] | null
  78. }
  79. export interface BackendPreviewSummary {
  80. mode?: string | null
  81. baseTiles?: {
  82. tileBaseUrl?: string | null
  83. zoom?: number | null
  84. tileSize?: number | null
  85. } | null
  86. viewport?: {
  87. width?: number | null
  88. height?: number | null
  89. minLon?: number | null
  90. minLat?: number | null
  91. maxLon?: number | null
  92. maxLat?: number | null
  93. } | null
  94. variants?: BackendPreviewVariantSummary[] | null
  95. selectedVariantId?: string | null
  96. }
  97. export interface BackendContentBundleSummary {
  98. bundleId?: string | null
  99. bundleType?: string | null
  100. version?: string | null
  101. }
  102. export interface BackendExperienceMapSummary {
  103. placeId?: string | null
  104. placeName?: string | null
  105. mapId?: string | null
  106. mapName?: string | null
  107. coverUrl?: string | null
  108. summary?: string | null
  109. defaultExperienceCount?: number | null
  110. defaultExperienceEventIds?: string[] | null
  111. }
  112. export interface BackendDefaultExperienceSummary {
  113. eventId?: string | null
  114. title?: string | null
  115. subtitle?: string | null
  116. eventType?: string | null
  117. status?: string | null
  118. statusCode?: string | null
  119. ctaText?: string | null
  120. isDefaultExperience?: boolean
  121. showInEventList?: boolean
  122. currentPresentation?: BackendPresentationSummary | null
  123. currentContentBundle?: BackendContentBundleSummary | null
  124. }
  125. export interface BackendExperienceMapDetail {
  126. placeId?: string | null
  127. placeName?: string | null
  128. mapId?: string | null
  129. mapName?: string | null
  130. coverUrl?: string | null
  131. summary?: string | null
  132. tileBaseUrl?: string | null
  133. tileMetaUrl?: string | null
  134. defaultExperiences?: BackendDefaultExperienceSummary[] | null
  135. }
  136. export interface BackendEntrySessionSummary {
  137. id: string
  138. status: string
  139. eventId?: string
  140. eventName?: string
  141. releaseId?: string | null
  142. configLabel?: string | null
  143. routeCode?: string | null
  144. variantId?: string | null
  145. variantName?: string | null
  146. runtime?: BackendRuntimeSummary | null
  147. launchedAt?: string | null
  148. startedAt?: string | null
  149. endedAt?: string | null
  150. // 兼容前端旧字段名,避免联调过渡期多处判断
  151. sessionId?: string
  152. sessionStatus?: string
  153. eventDisplayName?: string
  154. }
  155. export interface BackendCardResult {
  156. id: string
  157. type: string
  158. title: string
  159. subtitle?: string | null
  160. summary?: string | null
  161. status?: string | null
  162. statusCode?: string | null
  163. timeWindow?: string | null
  164. ctaText?: string | null
  165. isDefaultExperience?: boolean
  166. eventType?: string | null
  167. currentPresentation?: BackendPresentationSummary | null
  168. currentContentBundle?: BackendContentBundleSummary | null
  169. coverUrl?: string | null
  170. displaySlot: string
  171. displayPriority: number
  172. event?: {
  173. id: string
  174. displayName: string
  175. summary?: string | null
  176. } | null
  177. htmlUrl?: string | null
  178. }
  179. export interface BackendEntryHomeResult {
  180. user: {
  181. id: string
  182. publicId: string
  183. status: string
  184. nickname?: string | null
  185. avatarUrl?: string | null
  186. }
  187. tenant: {
  188. id: string
  189. code: string
  190. name: string
  191. }
  192. channel: {
  193. id: string
  194. code: string
  195. type: string
  196. platformAppId?: string | null
  197. displayName: string
  198. status: string
  199. isDefault: boolean
  200. }
  201. cards: BackendCardResult[]
  202. ongoingSession?: BackendEntrySessionSummary | null
  203. recentSession?: BackendEntrySessionSummary | null
  204. }
  205. export interface BackendEventPlayResult {
  206. event: {
  207. id: string
  208. slug: string
  209. displayName: string
  210. summary?: string | null
  211. status: string
  212. }
  213. currentPresentation?: BackendPresentationSummary | null
  214. currentContentBundle?: BackendContentBundleSummary | null
  215. preview?: BackendPreviewSummary | null
  216. release?: {
  217. id: string
  218. configLabel: string
  219. manifestUrl: string
  220. manifestChecksumSha256?: string | null
  221. routeCode?: string | null
  222. } | null
  223. resolvedRelease?: BackendResolvedRelease | null
  224. play: {
  225. canLaunch: boolean
  226. primaryAction: string
  227. reason: string
  228. launchSource?: string
  229. assignmentMode?: string | null
  230. courseVariants?: BackendCourseVariantSummary[] | null
  231. ongoingSession?: BackendEntrySessionSummary | null
  232. recentSession?: BackendEntrySessionSummary | null
  233. }
  234. }
  235. export interface BackendLaunchResult {
  236. event: {
  237. id: string
  238. displayName: string
  239. }
  240. launch: {
  241. source: string
  242. resolvedRelease?: BackendResolvedRelease | null
  243. config: {
  244. configUrl: string
  245. configLabel: string
  246. configChecksumSha256?: string | null
  247. releaseId: string
  248. routeCode?: string | null
  249. }
  250. business: {
  251. source: string
  252. isGuest?: boolean
  253. eventId: string
  254. sessionId: string
  255. sessionToken: string
  256. sessionTokenExpiresAt: string
  257. routeCode?: string | null
  258. }
  259. variant?: BackendLaunchVariantSummary | null
  260. runtime?: BackendRuntimeSummary | null
  261. presentation?: BackendPresentationSummary | null
  262. contentBundle?: BackendContentBundleSummary | null
  263. }
  264. }
  265. export interface BackendSessionFinishSummaryPayload {
  266. finalDurationSec?: number
  267. finalScore?: number
  268. completedControls?: number
  269. totalControls?: number
  270. distanceMeters?: number
  271. averageSpeedKmh?: number
  272. maxHeartRateBpm?: number
  273. }
  274. export interface BackendSessionResult {
  275. session: {
  276. id: string
  277. status: string
  278. clientType: string
  279. deviceKey: string
  280. routeCode?: string | null
  281. runtime?: BackendRuntimeSummary | null
  282. sessionTokenExpiresAt: string
  283. launchedAt: string
  284. startedAt?: string | null
  285. endedAt?: string | null
  286. }
  287. event: {
  288. id: string
  289. displayName: string
  290. }
  291. resolvedRelease?: BackendResolvedRelease | null
  292. }
  293. export interface BackendSessionResultView {
  294. session: BackendEntrySessionSummary
  295. result: {
  296. status: string
  297. finalDurationSec?: number
  298. finalScore?: number
  299. completedControls?: number
  300. totalControls?: number
  301. distanceMeters?: number
  302. averageSpeedKmh?: number
  303. maxHeartRateBpm?: number
  304. summary?: Record<string, unknown>
  305. }
  306. }
  307. export interface BackendClientLogInput {
  308. source: string
  309. level: 'debug' | 'info' | 'warn' | 'error'
  310. category: string
  311. message: string
  312. eventId?: string
  313. releaseId?: string
  314. sessionId?: string
  315. manifestUrl?: string
  316. route?: string
  317. occurredAt?: string
  318. details?: Record<string, unknown>
  319. }
  320. type BackendEnvelope<T> = {
  321. data: T
  322. }
  323. type RequestOptions = {
  324. method: 'GET' | 'POST'
  325. baseUrl: string
  326. path: string
  327. authToken?: string
  328. body?: Record<string, unknown>
  329. }
  330. function requestBackend<T>(options: RequestOptions): Promise<T> {
  331. const url = `${normalizeBackendBaseUrl(options.baseUrl)}${options.path}`
  332. const header: Record<string, string> = {}
  333. if (options.body) {
  334. header['Content-Type'] = 'application/json'
  335. }
  336. if (options.authToken) {
  337. header.Authorization = `Bearer ${options.authToken}`
  338. }
  339. return new Promise<T>((resolve, reject) => {
  340. wx.request({
  341. url,
  342. method: options.method,
  343. header,
  344. data: options.body,
  345. success: (response) => {
  346. const statusCode = typeof response.statusCode === 'number' ? response.statusCode : 0
  347. const data = response.data as BackendEnvelope<T> | { error?: { code?: string; message?: string; details?: unknown } }
  348. if (statusCode >= 200 && statusCode < 300 && data && typeof data === 'object' && 'data' in data) {
  349. resolve((data as BackendEnvelope<T>).data)
  350. return
  351. }
  352. const errorPayload = data && typeof data === 'object' && 'error' in data
  353. ? (data as { error?: { code?: string; message?: string; details?: unknown } }).error
  354. : undefined
  355. reject({
  356. statusCode,
  357. code: errorPayload && errorPayload.code ? errorPayload.code : 'backend_error',
  358. message: errorPayload && errorPayload.message ? errorPayload.message : `request failed: ${statusCode}`,
  359. details: errorPayload && errorPayload.details ? errorPayload.details : response.data,
  360. } as BackendApiError)
  361. },
  362. fail: (error) => {
  363. reject({
  364. statusCode: 0,
  365. code: 'network_error',
  366. message: error && error.errMsg ? error.errMsg : 'network request failed',
  367. } as BackendApiError)
  368. },
  369. })
  370. })
  371. }
  372. export function loginWechatMini(input: {
  373. baseUrl: string
  374. code: string
  375. deviceKey: string
  376. clientType?: string
  377. }): Promise<BackendAuthLoginResult> {
  378. return requestBackend<BackendAuthLoginResult>({
  379. method: 'POST',
  380. baseUrl: input.baseUrl,
  381. path: '/auth/login/wechat-mini',
  382. body: {
  383. code: input.code,
  384. clientType: input.clientType || 'wechat',
  385. deviceKey: input.deviceKey,
  386. },
  387. })
  388. }
  389. export function getEventPlay(input: {
  390. baseUrl: string
  391. eventId: string
  392. accessToken: string
  393. }): Promise<BackendEventPlayResult> {
  394. return requestBackend<BackendEventPlayResult>({
  395. method: 'GET',
  396. baseUrl: input.baseUrl,
  397. path: `/events/${encodeURIComponent(input.eventId)}/play`,
  398. authToken: input.accessToken,
  399. })
  400. }
  401. export function getPublicEventPlay(input: {
  402. baseUrl: string
  403. eventId: string
  404. }): Promise<BackendEventPlayResult> {
  405. return requestBackend<BackendEventPlayResult>({
  406. method: 'GET',
  407. baseUrl: input.baseUrl,
  408. path: `/public/events/${encodeURIComponent(input.eventId)}/play`,
  409. })
  410. }
  411. export function getEntryHome(input: {
  412. baseUrl: string
  413. accessToken: string
  414. channelCode: string
  415. channelType: string
  416. }): Promise<BackendEntryHomeResult> {
  417. const query = `channelCode=${encodeURIComponent(input.channelCode)}&channelType=${encodeURIComponent(input.channelType)}`
  418. return requestBackend<BackendEntryHomeResult>({
  419. method: 'GET',
  420. baseUrl: input.baseUrl,
  421. path: `/me/entry-home?${query}`,
  422. authToken: input.accessToken,
  423. })
  424. }
  425. export function launchEvent(input: {
  426. baseUrl: string
  427. eventId: string
  428. accessToken: string
  429. releaseId?: string
  430. variantId?: string
  431. clientType: string
  432. deviceKey: string
  433. }): Promise<BackendLaunchResult> {
  434. const body: Record<string, unknown> = {
  435. clientType: input.clientType,
  436. deviceKey: input.deviceKey,
  437. }
  438. if (input.releaseId) {
  439. body.releaseId = input.releaseId
  440. }
  441. if (input.variantId) {
  442. body.variantId = input.variantId
  443. }
  444. return requestBackend<BackendLaunchResult>({
  445. method: 'POST',
  446. baseUrl: input.baseUrl,
  447. path: `/events/${encodeURIComponent(input.eventId)}/launch`,
  448. authToken: input.accessToken,
  449. body,
  450. })
  451. }
  452. export function launchPublicEvent(input: {
  453. baseUrl: string
  454. eventId: string
  455. releaseId?: string
  456. variantId?: string
  457. clientType: string
  458. deviceKey: string
  459. }): Promise<BackendLaunchResult> {
  460. const body: Record<string, unknown> = {
  461. clientType: input.clientType,
  462. deviceKey: input.deviceKey,
  463. }
  464. if (input.releaseId) {
  465. body.releaseId = input.releaseId
  466. }
  467. if (input.variantId) {
  468. body.variantId = input.variantId
  469. }
  470. return requestBackend<BackendLaunchResult>({
  471. method: 'POST',
  472. baseUrl: input.baseUrl,
  473. path: `/public/events/${encodeURIComponent(input.eventId)}/launch`,
  474. body,
  475. })
  476. }
  477. export function startSession(input: {
  478. baseUrl: string
  479. sessionId: string
  480. sessionToken: string
  481. }): Promise<BackendSessionResult> {
  482. return requestBackend<BackendSessionResult>({
  483. method: 'POST',
  484. baseUrl: input.baseUrl,
  485. path: `/sessions/${encodeURIComponent(input.sessionId)}/start`,
  486. body: {
  487. sessionToken: input.sessionToken,
  488. },
  489. })
  490. }
  491. export function finishSession(input: {
  492. baseUrl: string
  493. sessionId: string
  494. sessionToken: string
  495. status: 'finished' | 'failed' | 'cancelled'
  496. summary: BackendSessionFinishSummaryPayload
  497. }): Promise<BackendSessionResult> {
  498. return requestBackend<BackendSessionResult>({
  499. method: 'POST',
  500. baseUrl: input.baseUrl,
  501. path: `/sessions/${encodeURIComponent(input.sessionId)}/finish`,
  502. body: {
  503. sessionToken: input.sessionToken,
  504. status: input.status,
  505. summary: input.summary,
  506. },
  507. })
  508. }
  509. export function getSessionResult(input: {
  510. baseUrl: string
  511. accessToken: string
  512. sessionId: string
  513. }): Promise<BackendSessionResultView> {
  514. return requestBackend<BackendSessionResultView>({
  515. method: 'GET',
  516. baseUrl: input.baseUrl,
  517. path: `/sessions/${encodeURIComponent(input.sessionId)}/result`,
  518. authToken: input.accessToken,
  519. })
  520. }
  521. export function getMyResults(input: {
  522. baseUrl: string
  523. accessToken: string
  524. limit?: number
  525. }): Promise<BackendSessionResultView[]> {
  526. const limit = typeof input.limit === 'number' ? input.limit : 20
  527. return requestBackend<BackendSessionResultView[]>({
  528. method: 'GET',
  529. baseUrl: input.baseUrl,
  530. path: `/me/results?limit=${encodeURIComponent(String(limit))}`,
  531. authToken: input.accessToken,
  532. })
  533. }
  534. export function postClientLog(input: {
  535. baseUrl: string
  536. payload: BackendClientLogInput
  537. }): Promise<void> {
  538. return requestBackend<void>({
  539. method: 'POST',
  540. baseUrl: input.baseUrl,
  541. path: '/dev/client-logs',
  542. body: input.payload as unknown as Record<string, unknown>,
  543. })
  544. }
  545. export function getExperienceMaps(input: {
  546. baseUrl: string
  547. accessToken: string
  548. }): Promise<BackendExperienceMapSummary[]> {
  549. return requestBackend<BackendExperienceMapSummary[]>({
  550. method: 'GET',
  551. baseUrl: input.baseUrl,
  552. path: '/experience-maps',
  553. authToken: input.accessToken,
  554. })
  555. }
  556. export function getPublicExperienceMaps(input: {
  557. baseUrl: string
  558. }): Promise<BackendExperienceMapSummary[]> {
  559. return requestBackend<BackendExperienceMapSummary[]>({
  560. method: 'GET',
  561. baseUrl: input.baseUrl,
  562. path: '/public/experience-maps',
  563. })
  564. }
  565. export function getExperienceMapDetail(input: {
  566. baseUrl: string
  567. accessToken: string
  568. mapAssetId: string
  569. }): Promise<BackendExperienceMapDetail> {
  570. return requestBackend<BackendExperienceMapDetail>({
  571. method: 'GET',
  572. baseUrl: input.baseUrl,
  573. path: `/experience-maps/${encodeURIComponent(input.mapAssetId)}`,
  574. authToken: input.accessToken,
  575. })
  576. }
  577. export function getPublicExperienceMapDetail(input: {
  578. baseUrl: string
  579. mapAssetId: string
  580. }): Promise<BackendExperienceMapDetail> {
  581. return requestBackend<BackendExperienceMapDetail>({
  582. method: 'GET',
  583. baseUrl: input.baseUrl,
  584. path: `/public/experience-maps/${encodeURIComponent(input.mapAssetId)}`,
  585. })
  586. }