/* * 统一数据访问层 * * 默认走本地数据层,方便网页端离线演示。 * 页面带 ?demo=0 时切换到真实后端,供之后继续联调。 */ (() => { 'use strict'; const API_BASE = `${location.origin}/api`; const SOURCE_KEY = 'youthome:web:dataSource'; const SOURCE_VERSION_KEY = 'youthome:web:sourceDefault:v2'; const TOKEN_KEY = 'youthome:web:accessToken'; const USER_KEY = 'youthome:web:authUser'; const demoMode = new URLSearchParams(location.search).get('demo'); if (demoMode === '0') { localStorage.setItem(SOURCE_KEY, 'api'); localStorage.setItem(SOURCE_VERSION_KEY, 'true'); if ((sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY) || '').startsWith('local-')) { [localStorage, sessionStorage].forEach((storage) => { storage.removeItem(TOKEN_KEY); storage.removeItem(USER_KEY); }); } } else { if (demoMode === '1' || localStorage.getItem(SOURCE_VERSION_KEY) !== 'true') { localStorage.setItem(SOURCE_KEY, 'local'); localStorage.setItem(SOURCE_VERSION_KEY, 'true'); } if (localStorage.getItem(SOURCE_KEY) === 'local') { const existingToken = sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY) || ''; if (existingToken && !existingToken.startsWith('local-')) { [localStorage, sessionStorage].forEach((storage) => { storage.removeItem(TOKEN_KEY); storage.removeItem(USER_KEY); }); } window.YouthomeLocal?.useDemoAccount(); } } const source = () => (localStorage.getItem(SOURCE_KEY) === 'local' ? 'local' : 'api'); const isLocal = () => source() === 'local'; const token = () => sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); const currentUser = () => { try { return JSON.parse(sessionStorage.getItem(USER_KEY) || localStorage.getItem(USER_KEY) || 'null'); } catch (error) { return null; } }; const applicationKey = () => `youthome:web:activityApplications:${currentUser()?.id || 'guest'}`; const applicationRecords = () => { try { return JSON.parse(localStorage.getItem(applicationKey()) || '[]'); } catch (error) { return []; } }; const clearSession = () => { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(USER_KEY); }; const request = async (path, options = {}) => { if (isLocal()) { if (!window.YouthomeLocal) throw new Error('本地数据层未加载,请刷新页面重试'); return window.YouthomeLocal.request(path, options); } const headers = { ...(options.headers || {}) }; if (token()) headers.access_token = token(); const isForm = options.body instanceof FormData; if (options.body && !isForm) headers['Content-Type'] = 'application/json'; let response; try { response = await fetch(`${API_BASE}${path}`, { ...options, headers, signal: AbortSignal.timeout(isForm ? 600000 : 20000) }); } catch (error) { throw new Error(error.name === 'TimeoutError' ? '请求超时,请稍后重试' : '无法连接后端,请稍后重试'); } let result; try { result = await response.json(); } catch (error) { throw new Error(`请求失败(HTTP ${response.status})`); } if (response.status === 401) { clearSession(); throw new Error('登录已失效,请重新登录'); } if (!response.ok || (typeof result.code === 'number' && result.code !== 200)) { throw new Error(result.message || `请求失败(HTTP ${response.status})`); } return Object.prototype.hasOwnProperty.call(result, 'data') ? result.data : result; }; // 后端头像字段是资源 key(如 man1),不是可直接访问的 URL const avatarUrl = (value) => { if (!value || typeof value !== 'string') return ''; if (/^(https?:|data:image\/|\.\.\/|<)/.test(value)) return value; return ''; }; const me = () => currentUser(); window.YouthomeApi = { source, isLocal, setSource(value) { localStorage.setItem(SOURCE_KEY, value === 'local' ? 'local' : 'api'); localStorage.setItem(SOURCE_VERSION_KEY, 'true'); }, token, currentUser, applicationRecords, saveApplication(record) { const records = applicationRecords().filter((item) => item.activityId !== record.activityId); records.push(record); localStorage.setItem(applicationKey(), JSON.stringify(records)); }, removeApplication(activityId) { localStorage.setItem(applicationKey(), JSON.stringify(applicationRecords().filter((item) => item.activityId !== activityId))); }, clearSession, request, get: (path) => request(path), post: (path, body) => request(path, { method: 'POST', ...(body === undefined ? {} : { body: JSON.stringify(body) }) }), delete: (path) => request(path, { method: 'DELETE' }), avatarUrl, updateSessionUser(patch) { const storage = sessionStorage.getItem(USER_KEY) ? sessionStorage : localStorage; const merged = { ...(currentUser() || {}), ...patch }; storage.setItem(USER_KEY, JSON.stringify(merged)); return merged; }, async profile() { if (isLocal()) return window.YouthomeLocal?.account?.() || null; const user = me(); if (!user) return null; return request(`/users/profile?userId=${encodeURIComponent(user.id)}`); }, async points() { if (isLocal()) return Number(window.YouthomeLocal?.account?.()?.points || 0); const data = await request('/profile/points'); return Number(data?.points ?? 0); }, async checkIn() { if (isLocal()) return null; return request('/campus/points/check-in', { method: 'POST' }); }, async followStats() { if (isLocal()) { const account = window.YouthomeLocal?.account?.(); return { followingCount: window.YouthomeLocal?.following?.().length || 0, followersCount: account?.fans || 0 }; } const user = me(); if (!user) return { followingCount: 0, followersCount: 0 }; return request(`/users/follow-status?userId=${encodeURIComponent(user.id)}`); }, async favorites() { if (isLocal()) return window.YouthomeLocal.bucketMoments('favorites'); const data = await request('/campus/moments/favorites?page=1&size=50'); return data?.items || []; }, async following() { if (isLocal()) return window.YouthomeLocal.following().map((nickname) => ({ nickname })); const user = me(); if (!user) return []; const data = await request(`/users/following?userId=${encodeURIComponent(user.id)}&page=1&size=50`); return data?.items || []; }, async toggleFollow(userId) { if (isLocal() || !userId) return null; return request(`/users/follow?userId=${encodeURIComponent(userId)}`, { method: 'POST' }); }, async joinedActivities() { if (isLocal()) return window.YouthomeLocal.joinedActivities(); const data = await request('/campus/activities/joined?page=1&size=50'); return data?.items || []; }, async registrations() { if (isLocal()) return []; const data = await request('/campus/activities/registrations/mine'); return Array.isArray(data) ? data : (data?.items || []); }, async cancelRegistration(id) { return request(`/campus/activities/registrations/${encodeURIComponent(id)}`, { method: 'DELETE' }); }, async joinActivity(id, remark = '') { if (isLocal()) return null; const query = remark ? `?remark=${encodeURIComponent(remark)}` : ''; return request(`/campus/activities/${encodeURIComponent(id)}/applications${query}`, { method: 'POST' }); }, async quitActivity(id) { if (isLocal()) return window.YouthomeLocal.leaveActivity(id); return request(`/campus/activities/quit?id=${encodeURIComponent(id)}`, { method: 'DELETE' }); }, async mallProducts() { if (isLocal()) return []; const data = await request('/campus/mall/products'); return data?.items || []; }, async purchase(productId) { return request('/campus/mall/purchase', { method: 'POST', body: JSON.stringify({ productId }) }); } }; })();