|
|
@@ -1,6 +1,6 @@
|
|
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
|
import { inject, Injectable } from '@angular/core';
|
|
|
-import { firstValueFrom, Subject, timeout, throwError } from 'rxjs';
|
|
|
+import { firstValueFrom, Subject, timeout, throwError, retry, timer, mergeMap, of } from 'rxjs';
|
|
|
import { CLOUD_FUNCTION_BASE_URL } from './admin.constants';
|
|
|
import { AdminSessionService } from './admin-session.service';
|
|
|
import { CloudEnvelope, LegacyCloudEnvelope } from './admin.models';
|
|
|
@@ -22,10 +22,8 @@ export class CloudFunctionsService {
|
|
|
const token = this.sessions.session()?.sessionToken;
|
|
|
if (!token) throw new CloudFunctionError('管理员会话已失效', 401);
|
|
|
try {
|
|
|
- const envelope = await firstValueFrom(this.http.post<CloudEnvelope<T>>(
|
|
|
- `${CLOUD_FUNCTION_BASE_URL}/xiaoshu/admin/gateway`,
|
|
|
- { token, params: { operation, ...params } },
|
|
|
- ).pipe(timeout({first:20000,with:()=>throwError(()=>new CloudFunctionError('请求暂未完成,结果待核对;请刷新后重试,已填写内容请保留。'))})));
|
|
|
+ const envelope = await this.request<CloudEnvelope<T>>('xiaoshu/admin/gateway', token,
|
|
|
+ { ...params, operation }, false);
|
|
|
if (!envelope.success) { await this.handleAuthenticationFailure(token, 0, envelope); throw new CloudFunctionError(envelope.message || envelope.error || '云函数执行失败'); }
|
|
|
if (this.isMutation(operation)) this.announceDataChange();
|
|
|
return envelope.data;
|
|
|
@@ -40,6 +38,36 @@ export class CloudFunctionsService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Explicitly allow only reads. Unknown operations and all submissions must never replay.
|
|
|
+ private isReadOperation(operation: string): boolean {
|
|
|
+ return /^ops\/(?:users\/(?:list|detail|notes|audit)|schedule\/calendar|live\/versions|dashboard\/(?:summary|schedule-trend)|coaches\/(?:list|detail)|stores\/(?:list|detail)|coach-availability\/list|learning-reports\/(?:list|detail)|vocabulary\/(?:tree|list)|teaching-rules\/list|pay-rates\/list|appointments\/preflight)$/.test(operation);
|
|
|
+ }
|
|
|
+
|
|
|
+ private transportMessage(readOnly: boolean): string {
|
|
|
+ return readOnly
|
|
|
+ ? '数据加载暂未完成,请稍后重试;已填写内容请保留。'
|
|
|
+ : '连接暂未完成,提交结果待核对;请先刷新核对,避免重复提交,已填写内容请保留。';
|
|
|
+ }
|
|
|
+
|
|
|
+ private async request<T>(path: string, token: string, params: Record<string, unknown>, readOnly: boolean, timeoutMs = 20000): Promise<T> {
|
|
|
+ try {
|
|
|
+ return await firstValueFrom(this.http.post<T>(`${CLOUD_FUNCTION_BASE_URL}/${path}`, { token, params }).pipe(
|
|
|
+ timeout({ first: timeoutMs, with: () => throwError(() => new CloudFunctionError(this.transportMessage(readOnly))) }),
|
|
|
+ retry({ count: 1, delay: (error) => {
|
|
|
+ const transient = error instanceof HttpErrorResponse && [0, 502, 503, 504].includes(error.status);
|
|
|
+ // A timeout may leave work running on the server; do not add another slow request.
|
|
|
+ if (!readOnly || !transient || this.sessions.session()?.sessionToken !== token) return throwError(() => error);
|
|
|
+ return timer(500).pipe(mergeMap(() => this.sessions.session()?.sessionToken === token ? of(0) : throwError(() => error)));
|
|
|
+ } }),
|
|
|
+ ));
|
|
|
+ } catch (error) {
|
|
|
+ if (error instanceof HttpErrorResponse && [0, 502, 503, 504].includes(error.status)) {
|
|
|
+ throw new CloudFunctionError(this.transportMessage(readOnly), error.status);
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private async handleAuthenticationFailure(token: string, status: number, payload: unknown): Promise<void> {
|
|
|
const body = payload as { code?: number; error?: string; message?: string; retmsg?: string } | null;
|
|
|
const message = String(body?.message || body?.retmsg || body?.error || '');
|
|
|
@@ -69,10 +97,9 @@ export class CloudFunctionsService {
|
|
|
const token = this.sessions.session()?.sessionToken;
|
|
|
if (!token) throw new CloudFunctionError('管理员会话已失效', 401);
|
|
|
try {
|
|
|
- const envelope = await firstValueFrom(this.http.post<CloudEnvelope<T>>(
|
|
|
- `${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`,
|
|
|
- { token, params: { operation, ...params } },
|
|
|
- ).pipe(timeout({first:/^ops\/vocabulary\/import-(preview|commit)$/.test(operation)?60000:20000,with:()=>throwError(()=>new CloudFunctionError('请求暂未完成,结果待核对;请刷新后重试,已填写内容请保留。'))})));
|
|
|
+ const envelope = await this.request<CloudEnvelope<T>>('xiaoshu/ops/gateway-v3', token,
|
|
|
+ { ...params, operation }, this.isReadOperation(operation),
|
|
|
+ /^ops\/vocabulary\/import-(preview|commit)$/.test(operation) ? 60000 : 20000);
|
|
|
if (!envelope.success) { await this.handleAuthenticationFailure(token, 0, envelope); throw new CloudFunctionError(envelope.message || envelope.error || '运营云函数执行失败'); }
|
|
|
if (this.isMutation(operation)) this.announceDataChange();
|
|
|
return envelope.data;
|
|
|
@@ -91,10 +118,8 @@ export class CloudFunctionsService {
|
|
|
const token = this.sessions.session()?.sessionToken;
|
|
|
if (!token) throw new CloudFunctionError('管理员会话已失效', 401);
|
|
|
try {
|
|
|
- const envelope = await firstValueFrom(this.http.post<LegacyCloudEnvelope<T>>(
|
|
|
- `${CLOUD_FUNCTION_BASE_URL}/xiaoshu/app/gateway`,
|
|
|
- { token, params: { action, ...params } },
|
|
|
- ).pipe(timeout({first:20000,with:()=>throwError(()=>new CloudFunctionError('请求暂未完成,结果待核对;请刷新后重试,已填写内容请保留。'))})));
|
|
|
+ const envelope = await this.request<LegacyCloudEnvelope<T>>('xiaoshu/app/gateway', token,
|
|
|
+ { ...params, action }, false);
|
|
|
if (envelope.retcode === -1) { await this.handleAuthenticationFailure(token, 0, envelope); throw new CloudFunctionError(envelope.retmsg || '云函数执行失败'); }
|
|
|
return envelope.result;
|
|
|
} catch (error) {
|