th-app-auth.ts 5.13 KB
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';

import { unwrapAbpResponse } from '#/api/th/abp-unwrap';

/** 绑定门店(与 ThAppLoginOutputDto.locations 一致) */
export interface ThAppBoundLocationDto {
  id: string;
  locationCode: string;
  locationName: string;
  fullAddress: string;
  state: boolean;
}

export interface ThAppLoginInput {
  tenantId: string;
  email: string;
  password: string;
  uuid?: string;
  code?: string;
}

export interface ThAppLoginOutputDto {
  token: string;
  refreshToken: string;
  tenantId: string;
  tenantName: string;
  locations: ThAppBoundLocationDto[];
}

const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);

/** 防止重复提交导致前一个请求被 Abort、后一个已在 Network 成功但 UI 仍报错 */
let loginInFlight: Promise<ThAppLoginOutputDto> | null = null;

function getApiBase(): string {
  const raw = (import.meta.env.VITE_GLOB_API_URL as string | undefined) ?? '/dev-api';
  return raw.replace(/^"|"$/g, '').replace(/\/$/, '');
}

function normalizeLocation(raw: Record<string, unknown>): ThAppBoundLocationDto {
  return {
    id: String(raw.id ?? raw.Id ?? ''),
    locationCode: String(raw.locationCode ?? raw.LocationCode ?? ''),
    locationName: String(raw.locationName ?? raw.LocationName ?? ''),
    fullAddress: String(raw.fullAddress ?? raw.FullAddress ?? ''),
    state: raw.state !== false && raw.State !== false,
  };
}

function normalizeLoginOutput(raw: unknown): ThAppLoginOutputDto {
  const o = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
  const locs = o.locations ?? o.Locations;
  const arr = Array.isArray(locs) ? locs : [];
  return {
    token: String(o.token ?? o.Token ?? ''),
    refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''),
    tenantId: String(o.tenantId ?? o.TenantId ?? ''),
    tenantName: String(o.tenantName ?? o.TenantName ?? ''),
    locations: arr.map((x) =>
      normalizeLocation(x as Record<string, unknown>),
    ),
  };
}

function normalizeLocationList(raw: unknown): ThAppBoundLocationDto[] {
  const arr = Array.isArray(raw) ? raw : [];
  return arr.map((x) => normalizeLocation(x as Record<string, unknown>));
}

/**
 * 使用 XHR 登录:dev 代理下 fetch/axios 常出现「Network 已有 body 但 JS 一直等连接结束」。
 * XHR onload 在收齐 responseText 后即触发,不依赖 fetch 的 body 流结束。
 */
function thAppLoginXhr(input: ThAppLoginInput): Promise<ThAppLoginOutputDto> {
  const url = `${getApiBase()}/th-app-auth/login`;
  const language = preferences.app.locale.replace('-', '_');
  const body = JSON.stringify({
    tenantId: input.tenantId,
    email: input.email.trim(),
    password: input.password,
    ...(input.uuid ? { uuid: input.uuid } : {}),
    ...(input.code != null && input.code !== '' ? { code: input.code } : {}),
  });

  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.timeout = 60_000;
    xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8');
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.setRequestHeader('Accept-Language', language);
    xhr.setRequestHeader('Content-Language', language);
    if (clientId) {
      xhr.setRequestHeader('ClientID', clientId);
    }

    xhr.onload = () => {
      try {
        const text = xhr.responseText ?? '';
        const json = text ? JSON.parse(text) : null;
        if (xhr.status < 200 || xhr.status >= 300) {
          try {
            unwrapAbpResponse(json);
          } catch (e) {
            reject(e instanceof Error ? e : new Error(`登录失败 HTTP ${xhr.status}`));
            return;
          }
          reject(new Error(`登录失败 HTTP ${xhr.status}`));
          return;
        }
        const data = unwrapAbpResponse<unknown>(json);
        resolve(normalizeLoginOutput(data));
      } catch (e) {
        reject(
          e instanceof Error
            ? e
            : new Error('登录响应解析失败,请查看 Network 中 login 的 Response'),
        );
      }
    };

    xhr.onerror = () => {
      reject(
        new Error(
          '登录网络异常:请确认 dev 服务已启动且代理地址正确(/dev-api → saas-test)',
        ),
      );
    };

    xhr.ontimeout = () => {
      reject(
        new Error(
          '登录请求超时:若 Network 中已有 token 响应,请刷新页面后只点一次登录',
        ),
      );
    };

    xhr.send(body);
  });
}

/** POST /api/app/th-app-auth/login(匿名) */
export async function thAppLogin(
  input: ThAppLoginInput,
): Promise<ThAppLoginOutputDto> {
  if (loginInFlight) {
    return loginInFlight;
  }
  loginInFlight = thAppLoginXhr(input).finally(() => {
    loginInFlight = null;
  });
  return loginInFlight;
}

/** GET /api/app/th-app-auth/my-locations(Bearer + 租户上下文) */
export async function thAppMyLocations(): Promise<ThAppBoundLocationDto[]> {
  const { requestClient } = await import('#/api/request');
  const raw = await requestClient.get<unknown>('th-app-auth/my-locations', {
    timeout: 30_000,
  });
  return normalizeLocationList(raw);
}