api.service.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
  2. import { inject, Injectable } from '@angular/core';
  3. import { catchError, map, Observable, throwError } from 'rxjs';
  4. import { API_CONFIG } from './app.constants';
  5. import { requiresCloudSession, usesCloudGateway } from './cloud-action-migration';
  6. import { ApiEnvelope } from './models';
  7. import { SessionService } from './session.service';
  8. export class ApiError extends Error {
  9. constructor(message: string, readonly status = 0, readonly payload?: unknown) {
  10. super(message);
  11. }
  12. }
  13. @Injectable({ providedIn: 'root' })
  14. export class ApiService {
  15. private readonly http = inject(HttpClient);
  16. private readonly sessions = inject(SessionService);
  17. get<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  18. const token = this.parseSessionToken();
  19. if (usesCloudGateway(action, params, Boolean(token))) return this.callCloud<T>(action, params, requiresCloudSession(action, params) ? token : '');
  20. return this.http
  21. .get<unknown>(this.actionUrl(action), { params: this.toHttpParams(params) })
  22. .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
  23. }
  24. post<T>(action: string, body: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  25. const token = this.parseSessionToken();
  26. if (action === 'user_login_passwd' && usesCloudGateway(action, body, Boolean(token))) {
  27. return this.callCloud<T>(action, body).pipe(
  28. catchError((error: ApiError) => [401, 403].includes(error.status)
  29. ? this.postLegacy<T>(action, body)
  30. : throwError(() => error)),
  31. );
  32. }
  33. if (usesCloudGateway(action, body, Boolean(token))) return this.callCloud<T>(action, body, requiresCloudSession(action, body) ? token : '');
  34. return this.postLegacy<T>(action, body);
  35. }
  36. private postLegacy<T>(action: string, body: Record<string, unknown>): Observable<ApiEnvelope<T>> {
  37. const encoded = this.toHttpParams(body);
  38. return this.http
  39. .post<unknown>(this.actionUrl(action), encoded.toString(), {
  40. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  41. })
  42. .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
  43. }
  44. callCloud<T>(action: string, params: Record<string, unknown> = {}, sessionToken = ''): Observable<ApiEnvelope<T>> {
  45. const body = {
  46. ...(sessionToken ? { token: sessionToken } : {}),
  47. params: { action, ...params },
  48. };
  49. return this.http
  50. .post<unknown>(API_CONFIG.cloudFunctionUrl, body, {
  51. headers: {
  52. 'Content-Type': 'application/json',
  53. 'X-Parse-Application-Id': API_CONFIG.parseAppId,
  54. },
  55. })
  56. .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
  57. }
  58. upload(file: File): Observable<ApiEnvelope<string>> {
  59. const formData = new FormData();
  60. formData.append('file', file, file.name);
  61. return this.http
  62. .post<unknown>(this.actionUrl('upload'), formData)
  63. .pipe(map((response) => this.normalize<string>(response)), catchError((error) => this.handleError(error)));
  64. }
  65. actionUrl(action: string): string {
  66. const query = new URLSearchParams({
  67. apiId: API_CONFIG.apiId,
  68. apiKey: API_CONFIG.apiKey,
  69. action,
  70. });
  71. return `${API_CONFIG.baseUrl}${API_CONFIG.endpoint}?${query.toString()}`;
  72. }
  73. assetUrl(path: unknown): string {
  74. const value = String(path ?? '').trim();
  75. if (!value) return `${API_CONFIG.baseUrl}/UploadFiles/nopic.svg`;
  76. if (/^(data:image|https?:\/\/)/i.test(value)) return value;
  77. return `${API_CONFIG.baseUrl}${value.startsWith('/') ? value : `/${value}`}`;
  78. }
  79. private normalize<T>(input: unknown): ApiEnvelope<T> {
  80. const envelope = (input ?? {}) as ApiEnvelope<unknown>;
  81. let result = envelope.result;
  82. if (typeof result === 'string') {
  83. try {
  84. result = JSON.parse(result) as unknown;
  85. } catch {
  86. // Some endpoints intentionally return plain text.
  87. }
  88. }
  89. const normalized = { ...envelope, result } as ApiEnvelope<T>;
  90. if (normalized.retcode === -1) {
  91. throw new ApiError(normalized.retmsg || '请求失败', 200, normalized);
  92. }
  93. return normalized;
  94. }
  95. private toHttpParams(values: Record<string, unknown>): HttpParams {
  96. let params = new HttpParams();
  97. for (const [key, raw] of Object.entries(values)) {
  98. if (raw === undefined || raw === null || raw === '') continue;
  99. const value = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
  100. params = params.set(key, value);
  101. }
  102. return params;
  103. }
  104. private handleError(error: unknown): Observable<never> {
  105. if (error instanceof ApiError) return throwError(() => error);
  106. if (error instanceof HttpErrorResponse) {
  107. const payload = error.error as { retmsg?: string } | string | null;
  108. const message = typeof payload === 'object' && payload?.retmsg
  109. ? payload.retmsg
  110. : error.message || '网络请求失败';
  111. return throwError(() => new ApiError(message, error.status, payload));
  112. }
  113. return throwError(() => new ApiError(error instanceof Error ? error.message : '未知错误'));
  114. }
  115. private parseSessionToken(): string {
  116. const token = this.sessions.token();
  117. return token.startsWith('r:') ? token : '';
  118. }
  119. }