import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { catchError, map, Observable, throwError } from 'rxjs'; import { API_CONFIG } from './app.constants'; import { requiresCloudSession, usesCloudGateway } from './cloud-action-migration'; import { ApiEnvelope } from './models'; import { SessionService } from './session.service'; export class ApiError extends Error { constructor(message: string, readonly status = 0, readonly payload?: unknown) { super(message); } } @Injectable({ providedIn: 'root' }) export class ApiService { private readonly http = inject(HttpClient); private readonly sessions = inject(SessionService); get(action: string, params: Record = {}): Observable> { const token = this.parseSessionToken(); if (usesCloudGateway(action, params, Boolean(token))) return this.callCloud(action, params, requiresCloudSession(action, params) ? token : ''); return this.http .get(this.actionUrl(action), { params: this.toHttpParams(params) }) .pipe(map((response) => this.normalize(response)), catchError((error) => this.handleError(error))); } post(action: string, body: Record = {}): Observable> { const token = this.parseSessionToken(); if (action === 'user_login_passwd' && usesCloudGateway(action, body, Boolean(token))) { return this.callCloud(action, body).pipe( catchError((error: ApiError) => [401, 403].includes(error.status) ? this.postLegacy(action, body) : throwError(() => error)), ); } if (usesCloudGateway(action, body, Boolean(token))) return this.callCloud(action, body, requiresCloudSession(action, body) ? token : ''); return this.postLegacy(action, body); } private postLegacy(action: string, body: Record): Observable> { const encoded = this.toHttpParams(body); return this.http .post(this.actionUrl(action), encoded.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }) .pipe(map((response) => this.normalize(response)), catchError((error) => this.handleError(error))); } callCloud(action: string, params: Record = {}, sessionToken = ''): Observable> { const body = { ...(sessionToken ? { token: sessionToken } : {}), params: { action, ...params }, }; return this.http .post(API_CONFIG.cloudFunctionUrl, body, { headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': API_CONFIG.parseAppId, }, }) .pipe(map((response) => this.normalize(response)), catchError((error) => this.handleError(error))); } upload(file: File): Observable> { const formData = new FormData(); formData.append('file', file, file.name); return this.http .post(this.actionUrl('upload'), formData) .pipe(map((response) => this.normalize(response)), catchError((error) => this.handleError(error))); } actionUrl(action: string): string { const query = new URLSearchParams({ apiId: API_CONFIG.apiId, apiKey: API_CONFIG.apiKey, action, }); return `${API_CONFIG.baseUrl}${API_CONFIG.endpoint}?${query.toString()}`; } assetUrl(path: unknown): string { const value = String(path ?? '').trim(); if (!value) return `${API_CONFIG.baseUrl}/UploadFiles/nopic.svg`; if (/^(data:image|https?:\/\/)/i.test(value)) return value; return `${API_CONFIG.baseUrl}${value.startsWith('/') ? value : `/${value}`}`; } private normalize(input: unknown): ApiEnvelope { const envelope = (input ?? {}) as ApiEnvelope; let result = envelope.result; if (typeof result === 'string') { try { result = JSON.parse(result) as unknown; } catch { // Some endpoints intentionally return plain text. } } const normalized = { ...envelope, result } as ApiEnvelope; if (normalized.retcode === -1) { throw new ApiError(normalized.retmsg || '请求失败', 200, normalized); } return normalized; } private toHttpParams(values: Record): HttpParams { let params = new HttpParams(); for (const [key, raw] of Object.entries(values)) { if (raw === undefined || raw === null || raw === '') continue; const value = typeof raw === 'object' ? JSON.stringify(raw) : String(raw); params = params.set(key, value); } return params; } private handleError(error: unknown): Observable { if (error instanceof ApiError) return throwError(() => error); if (error instanceof HttpErrorResponse) { const payload = error.error as { retmsg?: string } | string | null; const message = typeof payload === 'object' && payload?.retmsg ? payload.retmsg : error.message || '网络请求失败'; return throwError(() => new ApiError(message, error.status, payload)); } return throwError(() => new ApiError(error instanceof Error ? error.message : '未知错误')); } private parseSessionToken(): string { const token = this.sessions.token(); return token.startsWith('r:') ? token : ''; } }