import { apiEnabled } from "@/config/api"; import type { ApiEnvelope } from "@/utils/api/http"; import { getMpToken, MP_LOGIN_CHANNEL_KEY, post, setMpAuth } from "@/utils/api/http"; import { loadUserProfile, saveUserProfile, type UserProfile } from "@/utils/userProfileStorage"; import { resolveMediaUrl } from "@/utils/mediaUrl"; let bootstrapPromise: Promise | null = null; export type MpAuthUser = { id: number; nickname: string; avatar: string; bio: string; gender: number; phone?: string; }; type MpAuthPayload = { token: string; client_id: string; user: MpAuthUser; }; function isGenericNickname(name: string): boolean { const n = String(name || "").trim(); return n === "" || n === "微信用户" || n === "游客"; } function syncLocalProfile(u: MpAuthUser) { const gender: UserProfile["gender"] = u.gender === 1 ? "male" : u.gender === 2 ? "female" : "secret"; const nickname = String(u.nickname || "").trim(); const avatar = String(u.avatar || "").trim(); const bio = typeof u.bio === "string" ? u.bio : ""; const phone = typeof u.phone === "string" ? u.phone.trim() : ""; saveUserProfile({ nickname, avatarUrl: avatar ? resolveMediaUrl(avatar) : "", bio, gender, /* 与后端一致;空字符串覆盖本地,避免换账号后仍残留旧手机号 */ phone, }); } function finishAuth(data: MpAuthPayload) { setMpAuth(data.client_id, data.token); syncLocalProfile(data.user); } type MpLoginChannel = "guest" | "weixin" | "phone"; function setMpLoginChannel(ch: MpLoginChannel): void { try { uni.setStorageSync(MP_LOGIN_CHANNEL_KEY, ch); } catch { /* ignore */ } } /** 当前登录渠道(小程序端上传/「我的」仅允许 weixin) */ export function getMpLoginChannel(): MpLoginChannel | "" { try { const v = String(uni.getStorageSync(MP_LOGIN_CHANNEL_KEY) || "").trim(); if (v === "guest" || v === "weixin" || v === "phone") { return v; } return ""; } catch { return ""; } } function readClientId(): string { try { return (uni.getStorageSync("mp_client_id") as string) || ""; } catch { return ""; } } export function resetMpBootstrap() { bootstrapPromise = null; } /** 调用登录接口并写入 token / 本地资料 */ export async function authMpRequest(body: Record): Promise> { return post("/api/v1/auth/mp", body); } /** * 无 token 时用游客流换取 token(不含 wx_code,避免绕过登录页静默绑微信)。 * 需微信授权登录请使用 {@link loginWithWeixin}。 */ export function ensureMpSession(): Promise { if (!apiEnabled()) { return Promise.resolve(); } if (getMpToken()) { return Promise.resolve(); } if (bootstrapPromise) { return bootstrapPromise; } bootstrapPromise = (async () => { const res = await authMpRequest({ client_id: readClientId() }); finishAuth(res.data); setMpLoginChannel("guest"); })().finally(() => { bootstrapPromise = null; }); return bootstrapPromise; } /** 登录页:与 ensureMpSession 相同,显式调用 */ export async function loginAsGuest(): Promise { resetMpBootstrap(); const res = await authMpRequest({ client_id: readClientId() }); finishAuth(res.data); setMpLoginChannel("guest"); } /** 登录页:微信小程序 code 换票并绑定 openid */ export async function loginWithWeixin(): Promise { resetMpBootstrap(); let wxCode = ""; // #ifdef MP-WEIXIN wxCode = await new Promise((resolve, reject) => { uni.login({ provider: "weixin", success: (r) => { if (r.code) { resolve(r.code); } else { reject(new Error("未获取到登录凭证")); } }, fail: (e) => reject(e), }); }); // #endif if (!wxCode) { throw new Error("请在小程序内完成快捷登录"); } const res = await authMpRequest({ client_id: readClientId(), wx_code: wxCode, }); finishAuth(res.data); setMpLoginChannel("weixin"); return { ...res.data.user, nickname: isGenericNickname(res.data.user.nickname) ? "" : String(res.data.user.nickname || "").trim(), avatar: String(res.data.user.avatar || "").trim(), }; } /** 兼容旧后端:登录时附带昵称头像 */ export async function loginWithWeixinProfile(payload: { nickname: string; avatar: string; gender?: number; }): Promise { resetMpBootstrap(); let wxCode = ""; // #ifdef MP-WEIXIN wxCode = await new Promise((resolve, reject) => { uni.login({ provider: "weixin", success: (r) => { if (r.code) { resolve(r.code); } else { reject(new Error("未获取到登录凭证")); } }, fail: (e) => reject(e), }); }); // #endif if (!wxCode) { throw new Error("请在小程序内完成快捷登录"); } const res = await authMpRequest({ client_id: readClientId(), wx_code: wxCode, wx_nickname: String(payload.nickname || "").trim(), wx_avatar: String(payload.avatar || "").trim(), wx_gender: Number(payload.gender || 0), }); finishAuth(res.data); setMpLoginChannel("weixin"); return { ...res.data.user, nickname: isGenericNickname(res.data.user.nickname) ? String(payload.nickname || "").trim() : String(res.data.user.nickname || "").trim(), avatar: String(res.data.user.avatar || "").trim() || String(payload.avatar || "").trim(), }; } /** 登录后绑定手机号(getPhoneNumber 回调 code) */ export async function bindWechatPhone(phoneCode: string): Promise { const res = await post<{ phone: string }>("/api/v1/auth/bind-phone", { phone_code: phoneCode, }); const phone = String(res.data.phone || "").trim(); if (!phone) { throw new Error("手机号绑定失败"); } const cur = loadUserProfile(); saveUserProfile({ ...cur, phone }); return phone; } /** 手机号 + 密码登录 */ export async function loginWithPhone(phone: string, pwd: string): Promise { resetMpBootstrap(); const res = await post("/api/v1/auth/login-phone", { phone, password: pwd, }); finishAuth(res.data); setMpLoginChannel("phone"); } /** 手机号注册(成功后直接登录) */ export async function registerWithPhone( phone: string, password: string, password_confirm: string ): Promise { resetMpBootstrap(); const res = await post("/api/v1/auth/register", { phone, password, password_confirm, }); finishAuth(res.data); setMpLoginChannel("phone"); }