wechat.ts 5.6 KB
import { service as http } from "@/utils/request";

// ==================== Types ====================

export type WechatLoginStatus =
  | "PENDING"
  | "SCANNED"
  | "CONFIRMED"
  | "EXPIRED"
  | "CANCELLED";

export interface LoginResponse {
  token: string;
  refreshToken?: string | null;
  userId: number;
  email: string;
  role: string;
}

export interface WechatQrCodeResponse {
  stateToken: string;
  qrCodeUrl: string;
  expiresIn: number; // seconds
  timestamp: number; // ms
}

export interface WechatLoginCheckResponse {
  status: WechatLoginStatus;
  message: string;
  loginResult?: LoginResponse | null;
  remainingTime?: number | null; // seconds
  timestamp: number; // ms
}

export interface ErrorResponse {
  code: string;
  message: string;
}

export interface GenericMessageResponse {
  message: string;
}

export interface WechatBindRequestPayload {
  code?: string;
  state: string;
}

export interface WechatBindResponse {
  message: string; // "绑定成功"
}

export interface WechatBindStatusResponse {
  bound: boolean;
  wechatNickname: string;
  message: string;
}

export interface MergeEmailRequestPayload {
  email: string;
  password: string;
}

export interface MergeEmailResponse {
  message: string;
  userId: number | null;
}

// ==================== APIs ====================

/** 获取微信登录二维码(可选传入 mode=bind 用于账号绑定) */
export const getWechatQrCode = (mode?: "bind" | "login", redirectUri?: string) =>
  http.get<WechatQrCodeResponse>("/auth/wechat/qrcode", {
    params: {
      ...(mode === "bind" ? { mode: "bind" } : {}),
      ...(redirectUri ? { redirectUri } : {}),
    },
  });

/** 检查扫码登录状态(重定向成功页一次性获取复用此方法) */
export const checkWechatLoginStatus = (stateToken: string) =>
  http.get<WechatLoginCheckResponse>(`/auth/wechat/status/${stateToken}`);

/** 绑定微信到当前已登录账号(邮箱账号场景) */
export const bindWechat = (payload: WechatBindRequestPayload) =>
  http.post<WechatBindResponse>("/auth/wechat/bind", payload);

/** 解绑当前账号的微信 */
export const unbindWechat = () =>
  http.post<GenericMessageResponse>("/auth/wechat/unbind");

/** 获取当前账号微信绑定状态 */
export const getWechatBindStatus = () =>
  http.get<WechatBindStatusResponse>("/auth/wechat/bind-status");

/** 微信登录后,和已有邮箱账号进行合并(保留较小ID) */
export const mergeEmailToWechatUser = (payload: MergeEmailRequestPayload) =>
  http.post<MergeEmailResponse>("/auth/wechat/merge-email", payload);

/** 清理过期微信登录状态(管理员) */
export const cleanupWechatStates = () =>
  http.post<GenericMessageResponse>("/auth/wechat/cleanup");

/**
 * 获取小程序不限制的小程序码(用于推荐海报等场景)
 * @param scene - 场景值,如 ref=邀请码
 * @param page - 小程序页面路径,可选,如 components/auth/login/login
 * @returns Promise<string> 返回 Base64 data URL,可直接用于 img src
 */
export async function getMiniappUnlimitedQrcode(
  scene: string,
  page?: string,
): Promise<string> {
  const params: { scene: string; page?: string } = { scene };
  if (page) params.page = page;
  const res = await http.get<unknown>("/auth/wechat/miniapp/unlimited-qrcode", {
    params,
  });
  const responseData = res.data;
  let base64Data: string | null = null;
  if (typeof responseData === "string") {
    try {
      const parsed = JSON.parse(responseData) as Record<string, unknown>;
      base64Data =
        (parsed.buffer as string) ||
        (parsed.data as string) ||
        (responseData as string);
    } catch {
      base64Data = responseData;
    }
  } else if (responseData && typeof responseData === "object") {
    const obj = responseData as Record<string, unknown>;
    base64Data = (obj.buffer as string) || (obj.data as string) || null;
  }
  if (!base64Data || typeof base64Data !== "string") {
    throw new Error("接口返回数据格式错误");
  }
  return base64Data.startsWith("data:image")
    ? base64Data
    : `data:image/jpeg;base64,${base64Data.replace(/^data:image\/\w+;base64,/, "")}`;
}

// ==================== 使用示例 ====================
/**
 * 一、扫码登录(后端回调重定向模式)
 * 1) 获取二维码
 *    const { data: qr } = await getWechatQrCode();
 *    // 渲染 qr.qrCodeUrl 为二维码;保留 qr.stateToken
 *
 * 2) 用户扫码并确认后,后端回调并重定向前端成功页:/wechat-login?wechat_login=success&state=xxxx
 *    成功页加载时一次性获取登录结果:
 *    const state = new URLSearchParams(location.search).get('state')!;
 *    const { data: s } = await checkWechatLoginStatus(state);
 *    if (s.status === 'CONFIRMED' && s.loginResult) {
 *      // 持久化 token/refreshToken,并跳转业务首页
 *    } else {
 *      // 兜底:处理 CANCELLED/EXPIRED
 *    }
 * 
 *   若登录失败,后端回调并重定向前端失败页:/wechat-login?wechat_login=error&message=
 *
 * 二、绑定微信(已登录邮箱账号)
 *    // 微信端返回 code 与同一次生成二维码的 state
 *    await bindWechat({ code, state });
 *    // 如后端返回业务错误(如 WECHAT_ALREADY_BOUND),捕获并提示合并流程
 *
 * 三、微信账号登录后合并邮箱账号
 *    await mergeEmailToWechatUser({ email, password });
 *    // 成功后返回合并后的 userId,前端应刷新用户信息/状态
 *
 * 四、解绑微信
 *    await unbindWechat();
 *
 * 五、获取绑定状态
 *    const { data: s } = await getWechatBindStatus();
 *    if (s.bound) { 
 *      // 已绑定
 *    } else { 
 *      // 未绑定
 *    }
 */