const AUTH_SESSION_KEY = 'teaching-feedback-auth-session-v1' const EXPIRY_SKEW_MS = 60_000 interface RawLoginResponse { access_token: string expires_at: string user_id: string } interface ErrorBody { error?: string } export interface AuthSession { apiBaseUrl: string accessToken: string expiresAt: number userId: string } export class AuthenticationError extends Error { constructor(message: string) { super(message) this.name = 'AuthenticationError' } } let pendingLogin: { apiBaseUrl: string; promise: Promise } | null = null export function getStoredAuthSession(apiBaseUrl: string): AuthSession | null { const value = wx.getStorageSync(AUTH_SESSION_KEY) as Partial | string if (!value || typeof value !== 'object') return null if ( value.apiBaseUrl !== apiBaseUrl || typeof value.accessToken !== 'string' || !value.accessToken || typeof value.expiresAt !== 'number' || typeof value.userId !== 'string' || !value.userId ) { return null } return value as AuthSession } export function clearAuthSession(): void { wx.removeStorageSync(AUTH_SESSION_KEY) } export async function getAccessToken(apiBaseUrl: string, forceRefresh = false): Promise { if (!forceRefresh) { const stored = getStoredAuthSession(apiBaseUrl) if (stored && stored.expiresAt > Date.now() + EXPIRY_SKEW_MS) return stored.accessToken } return (await login(apiBaseUrl)).accessToken } export async function ensureAuthSession(apiBaseUrl: string): Promise { const accessToken = await getAccessToken(apiBaseUrl) const session = getStoredAuthSession(apiBaseUrl) if (!session || session.accessToken !== accessToken) { throw new AuthenticationError('登录状态保存失败,请重试') } return session } async function login(apiBaseUrl: string): Promise { if (pendingLogin?.apiBaseUrl === apiBaseUrl) return pendingLogin.promise const promise = loginWithWechat(apiBaseUrl).finally(() => { if (pendingLogin?.promise === promise) pendingLogin = null }) pendingLogin = { apiBaseUrl, promise } return promise } async function loginWithWechat(apiBaseUrl: string): Promise { const code = await getWechatLoginCode() const response = await exchangeCode(apiBaseUrl, code) const expiresAt = Date.parse(response.expires_at) if ( !response.access_token || !response.user_id || Number.isNaN(expiresAt) || expiresAt <= Date.now() ) { throw new AuthenticationError('登录服务返回了无效会话') } const session: AuthSession = { apiBaseUrl, accessToken: response.access_token, expiresAt, userId: response.user_id } wx.setStorageSync(AUTH_SESSION_KEY, session) return session } function getWechatLoginCode(): Promise { return new Promise((resolve, reject) => { wx.login({ success(result) { if (result.code) { resolve(result.code) return } reject(new AuthenticationError('微信未返回登录凭证')) }, fail(error) { reject(new AuthenticationError(`微信登录失败:${error.errMsg}`)) } }) }) } function exchangeCode(apiBaseUrl: string, code: string): Promise { return new Promise((resolve, reject) => { wx.request({ url: `${apiBaseUrl}/api/v1/auth/wechat/login`, method: 'POST', data: { code }, timeout: 15_000, header: { 'Content-Type': 'application/json' }, success(response) { if (response.statusCode >= 200 && response.statusCode < 300) { resolve(response.data as RawLoginResponse) return } const body = response.data as ErrorBody reject(new AuthenticationError(body?.error || `登录服务返回 HTTP ${response.statusCode}`)) }, fail(error) { reject(new AuthenticationError(`无法连接登录服务:${error.errMsg}`)) } }) }) }