backendApi.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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 BackendEntrySessionSummary {
  43. id: string
  44. status: string
  45. eventId?: string
  46. eventName?: string
  47. releaseId?: string | null
  48. configLabel?: string | null
  49. routeCode?: string | null
  50. variantId?: string | null
  51. variantName?: string | null
  52. launchedAt?: string | null
  53. startedAt?: string | null
  54. endedAt?: string | null
  55. // 兼容前端旧字段名,避免联调过渡期多处判断
  56. sessionId?: string
  57. sessionStatus?: string
  58. eventDisplayName?: string
  59. }
  60. export interface BackendCardResult {
  61. id: string
  62. type: string
  63. title: string
  64. subtitle?: string | null
  65. coverUrl?: string | null
  66. displaySlot: string
  67. displayPriority: number
  68. event?: {
  69. id: string
  70. displayName: string
  71. summary?: string | null
  72. } | null
  73. htmlUrl?: string | null
  74. }
  75. export interface BackendEntryHomeResult {
  76. user: {
  77. id: string
  78. publicId: string
  79. status: string
  80. nickname?: string | null
  81. avatarUrl?: string | null
  82. }
  83. tenant: {
  84. id: string
  85. code: string
  86. name: string
  87. }
  88. channel: {
  89. id: string
  90. code: string
  91. type: string
  92. platformAppId?: string | null
  93. displayName: string
  94. status: string
  95. isDefault: boolean
  96. }
  97. cards: BackendCardResult[]
  98. ongoingSession?: BackendEntrySessionSummary | null
  99. recentSession?: BackendEntrySessionSummary | null
  100. }
  101. export interface BackendEventPlayResult {
  102. event: {
  103. id: string
  104. slug: string
  105. displayName: string
  106. summary?: string | null
  107. status: string
  108. }
  109. release?: {
  110. id: string
  111. configLabel: string
  112. manifestUrl: string
  113. manifestChecksumSha256?: string | null
  114. routeCode?: string | null
  115. } | null
  116. resolvedRelease?: BackendResolvedRelease | null
  117. play: {
  118. canLaunch: boolean
  119. primaryAction: string
  120. reason: string
  121. launchSource?: string
  122. assignmentMode?: string | null
  123. courseVariants?: BackendCourseVariantSummary[] | null
  124. ongoingSession?: BackendEntrySessionSummary | null
  125. recentSession?: BackendEntrySessionSummary | null
  126. }
  127. }
  128. export interface BackendLaunchResult {
  129. event: {
  130. id: string
  131. displayName: string
  132. }
  133. launch: {
  134. source: string
  135. resolvedRelease?: BackendResolvedRelease | null
  136. config: {
  137. configUrl: string
  138. configLabel: string
  139. configChecksumSha256?: string | null
  140. releaseId: string
  141. routeCode?: string | null
  142. }
  143. business: {
  144. source: string
  145. eventId: string
  146. sessionId: string
  147. sessionToken: string
  148. sessionTokenExpiresAt: string
  149. routeCode?: string | null
  150. }
  151. variant?: BackendLaunchVariantSummary | null
  152. }
  153. }
  154. export interface BackendSessionFinishSummaryPayload {
  155. finalDurationSec?: number
  156. finalScore?: number
  157. completedControls?: number
  158. totalControls?: number
  159. distanceMeters?: number
  160. averageSpeedKmh?: number
  161. maxHeartRateBpm?: number
  162. }
  163. export interface BackendSessionResult {
  164. session: {
  165. id: string
  166. status: string
  167. clientType: string
  168. deviceKey: string
  169. routeCode?: string | null
  170. sessionTokenExpiresAt: string
  171. launchedAt: string
  172. startedAt?: string | null
  173. endedAt?: string | null
  174. }
  175. event: {
  176. id: string
  177. displayName: string
  178. }
  179. resolvedRelease?: BackendResolvedRelease | null
  180. }
  181. export interface BackendSessionResultView {
  182. session: BackendEntrySessionSummary
  183. result: {
  184. status: string
  185. finalDurationSec?: number
  186. finalScore?: number
  187. completedControls?: number
  188. totalControls?: number
  189. distanceMeters?: number
  190. averageSpeedKmh?: number
  191. maxHeartRateBpm?: number
  192. summary?: Record<string, unknown>
  193. }
  194. }
  195. type BackendEnvelope<T> = {
  196. data: T
  197. }
  198. type RequestOptions = {
  199. method: 'GET' | 'POST'
  200. baseUrl: string
  201. path: string
  202. authToken?: string
  203. body?: Record<string, unknown>
  204. }
  205. function requestBackend<T>(options: RequestOptions): Promise<T> {
  206. const url = `${normalizeBackendBaseUrl(options.baseUrl)}${options.path}`
  207. const header: Record<string, string> = {}
  208. if (options.body) {
  209. header['Content-Type'] = 'application/json'
  210. }
  211. if (options.authToken) {
  212. header.Authorization = `Bearer ${options.authToken}`
  213. }
  214. return new Promise<T>((resolve, reject) => {
  215. wx.request({
  216. url,
  217. method: options.method,
  218. header,
  219. data: options.body,
  220. success: (response) => {
  221. const statusCode = typeof response.statusCode === 'number' ? response.statusCode : 0
  222. const data = response.data as BackendEnvelope<T> | { error?: { code?: string; message?: string; details?: unknown } }
  223. if (statusCode >= 200 && statusCode < 300 && data && typeof data === 'object' && 'data' in data) {
  224. resolve((data as BackendEnvelope<T>).data)
  225. return
  226. }
  227. const errorPayload = data && typeof data === 'object' && 'error' in data
  228. ? (data as { error?: { code?: string; message?: string; details?: unknown } }).error
  229. : undefined
  230. reject({
  231. statusCode,
  232. code: errorPayload && errorPayload.code ? errorPayload.code : 'backend_error',
  233. message: errorPayload && errorPayload.message ? errorPayload.message : `request failed: ${statusCode}`,
  234. details: errorPayload && errorPayload.details ? errorPayload.details : response.data,
  235. } as BackendApiError)
  236. },
  237. fail: (error) => {
  238. reject({
  239. statusCode: 0,
  240. code: 'network_error',
  241. message: error && error.errMsg ? error.errMsg : 'network request failed',
  242. } as BackendApiError)
  243. },
  244. })
  245. })
  246. }
  247. export function loginWechatMini(input: {
  248. baseUrl: string
  249. code: string
  250. deviceKey: string
  251. clientType?: string
  252. }): Promise<BackendAuthLoginResult> {
  253. return requestBackend<BackendAuthLoginResult>({
  254. method: 'POST',
  255. baseUrl: input.baseUrl,
  256. path: '/auth/login/wechat-mini',
  257. body: {
  258. code: input.code,
  259. clientType: input.clientType || 'wechat',
  260. deviceKey: input.deviceKey,
  261. },
  262. })
  263. }
  264. export function getEventPlay(input: {
  265. baseUrl: string
  266. eventId: string
  267. accessToken: string
  268. }): Promise<BackendEventPlayResult> {
  269. return requestBackend<BackendEventPlayResult>({
  270. method: 'GET',
  271. baseUrl: input.baseUrl,
  272. path: `/events/${encodeURIComponent(input.eventId)}/play`,
  273. authToken: input.accessToken,
  274. })
  275. }
  276. export function getEntryHome(input: {
  277. baseUrl: string
  278. accessToken: string
  279. channelCode: string
  280. channelType: string
  281. }): Promise<BackendEntryHomeResult> {
  282. const query = `channelCode=${encodeURIComponent(input.channelCode)}&channelType=${encodeURIComponent(input.channelType)}`
  283. return requestBackend<BackendEntryHomeResult>({
  284. method: 'GET',
  285. baseUrl: input.baseUrl,
  286. path: `/me/entry-home?${query}`,
  287. authToken: input.accessToken,
  288. })
  289. }
  290. export function launchEvent(input: {
  291. baseUrl: string
  292. eventId: string
  293. accessToken: string
  294. releaseId?: string
  295. variantId?: string
  296. clientType: string
  297. deviceKey: string
  298. }): Promise<BackendLaunchResult> {
  299. const body: Record<string, unknown> = {
  300. clientType: input.clientType,
  301. deviceKey: input.deviceKey,
  302. }
  303. if (input.releaseId) {
  304. body.releaseId = input.releaseId
  305. }
  306. if (input.variantId) {
  307. body.variantId = input.variantId
  308. }
  309. return requestBackend<BackendLaunchResult>({
  310. method: 'POST',
  311. baseUrl: input.baseUrl,
  312. path: `/events/${encodeURIComponent(input.eventId)}/launch`,
  313. authToken: input.accessToken,
  314. body,
  315. })
  316. }
  317. export function startSession(input: {
  318. baseUrl: string
  319. sessionId: string
  320. sessionToken: string
  321. }): Promise<BackendSessionResult> {
  322. return requestBackend<BackendSessionResult>({
  323. method: 'POST',
  324. baseUrl: input.baseUrl,
  325. path: `/sessions/${encodeURIComponent(input.sessionId)}/start`,
  326. body: {
  327. sessionToken: input.sessionToken,
  328. },
  329. })
  330. }
  331. export function finishSession(input: {
  332. baseUrl: string
  333. sessionId: string
  334. sessionToken: string
  335. status: 'finished' | 'failed' | 'cancelled'
  336. summary: BackendSessionFinishSummaryPayload
  337. }): Promise<BackendSessionResult> {
  338. return requestBackend<BackendSessionResult>({
  339. method: 'POST',
  340. baseUrl: input.baseUrl,
  341. path: `/sessions/${encodeURIComponent(input.sessionId)}/finish`,
  342. body: {
  343. sessionToken: input.sessionToken,
  344. status: input.status,
  345. summary: input.summary,
  346. },
  347. })
  348. }
  349. export function getSessionResult(input: {
  350. baseUrl: string
  351. accessToken: string
  352. sessionId: string
  353. }): Promise<BackendSessionResultView> {
  354. return requestBackend<BackendSessionResultView>({
  355. method: 'GET',
  356. baseUrl: input.baseUrl,
  357. path: `/sessions/${encodeURIComponent(input.sessionId)}/result`,
  358. authToken: input.accessToken,
  359. })
  360. }
  361. export function getMyResults(input: {
  362. baseUrl: string
  363. accessToken: string
  364. limit?: number
  365. }): Promise<BackendSessionResultView[]> {
  366. const limit = typeof input.limit === 'number' ? input.limit : 20
  367. return requestBackend<BackendSessionResultView[]>({
  368. method: 'GET',
  369. baseUrl: input.baseUrl,
  370. path: `/me/results?limit=${encodeURIComponent(String(limit))}`,
  371. authToken: input.accessToken,
  372. })
  373. }