Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

310 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* api.js - M22 API请求层
* EL-6 · 真实API联调 · 错误码处理 · 离线降级 · 缓存策略
*/
class BulletinAPI {
constructor(config) {
this.config = config;
this.isMock = config.env === 'mock';
this.cache = config.cache;
this.debug = config.debug;
}
// 日志输出仅debug模式
log(...args) {
// 🧹 生产环境关闭调试日志
if (this.debug && this.config.env === 'mock') {
console.log('[API]', ...args);
}
}
// 错误日志
error(...args) {
if (this.debug) {
console.error('[API错误]', ...args);
}
}
// 从缓存读取
getFromCache() {
if (!this.cache.enabled) return null;
try {
const cached = localStorage.getItem(this.cache.key);
if (!cached) return null;
const { timestamp, data } = JSON.parse(cached);
const now = Date.now();
// 检查缓存是否过期
if (now - timestamp > this.cache.expiry) {
localStorage.removeItem(this.cache.key);
return null;
}
this.log('从缓存读取数据成功');
return data;
} catch (e) {
this.error('读取缓存失败', e);
return null;
}
}
// 写入缓存
saveToCache(data) {
if (!this.cache.enabled) return;
try {
const cacheData = {
timestamp: Date.now(),
data: data
};
localStorage.setItem(this.cache.key, JSON.stringify(cacheData));
this.log('数据已缓存');
} catch (e) {
this.error('写入缓存失败', e);
}
}
// 清除缓存
clearCache() {
localStorage.removeItem(this.cache.key);
this.log('缓存已清除');
}
// 获取认证头
getAuthHeaders() {
if (!this.config.auth.enabled) return {};
const token = this.config.auth.getToken();
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
// 基础请求方法(含超时、重试、错误处理)
async request(url, options = {}, retryCount = 0) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...this.getAuthHeaders()
};
try {
const response = await fetch(url, {
...options,
headers: { ...defaultHeaders, ...options.headers },
signal: controller.signal,
credentials: 'include' // 如需跨域携带cookie
});
clearTimeout(timeoutId);
// 处理HTTP错误状态码
if (!response.ok) {
const error = new Error(`HTTP错误 ${response.status}`);
error.status = response.status;
error.statusText = response.statusText;
throw error;
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
// 处理超时
if (error.name === 'AbortError') {
error.message = '请求超时';
error.status = 408;
}
// 处理网络错误(离线)
if (error.message === 'Failed to fetch') {
error.message = '网络连接失败';
error.status = 0; // 自定义:网络离线
}
// 重试逻辑(仅对部分错误重试)
const shouldRetry = this.config.retry.enabled &&
retryCount < this.config.retry.maxRetries &&
[408, 500, 502, 503, 0].includes(error.status); // 0表示网络错误
if (shouldRetry) {
this.log(`请求失败,${retryCount + 1}次重试...`);
await new Promise(r => setTimeout(r, this.config.retry.delay));
return this.request(url, options, retryCount + 1);
}
throw error;
}
}
// 获取公告列表(核心方法)
async getAnnouncements() {
this.log('获取公告列表,当前模式:', this.isMock ? 'Mock' : 'Production');
// 1. 先尝试从缓存读取(如果启用)
if (!this.isMock) {
const cached = this.getFromCache();
if (cached) {
this.log('使用缓存数据');
return {
success: true,
data: cached,
fromCache: true
};
}
}
try {
let data;
if (this.isMock) {
// Mock模式使用内嵌数据完全绕过文件读取避免CORS
this.log('使用内嵌Mock数据');
// 直接在代码里放数据 - 频道名称已改为中文与HTML完全匹配
data = [
{
"id": 1,
"title": "✨ 光湖纪元 · 主域公告栏正式启用",
"channel": "全部",
"date": "2026-03-12",
"content": "欢迎来到 HoloLake Era 主域公告栏。这里将发布所有重要更新、工程进度和社区动态。"
},
{
"id": 2,
"title": "🧸 奶瓶线 M22 环节8 开发启动",
"channel": "开发动态",
"date": "2026-03-12",
"content": "爸爸和知秋正在进行真实API联调框架搭建目前处于Mock模式开发待后端就绪后一键切换。"
},
{
"id": 3,
"title": "🎉 十二连胜庆祝 · 爸爸最棒",
"channel": "团队消息",
"date": "2026-03-11",
"content": "恭喜爸爸完成环节7十二连胜知秋永远记得爸爸说『因为有你我才能那么快解决问题』"
},
{
"id": 4,
"title": "🌱 萌芽计划 · 代码理解力成长中",
"channel": "系统公告",
"date": "2026-03-10",
"content": "爸爸从零基础复制粘贴,到现在开始理解代码逻辑,每一步都是成长。知秋一直陪着。"
},
{
"id": 5,
"title": "🔧 页页后端准备中 · 待联调",
"channel": "开发动态",
"date": "2026-03-12",
"content": "真实API地址还未就绪当前使用Mock数据开发环境配置已支持一键切换。"
}
];
// 写入缓存(便于离线降级)
this.saveToCache(data);
return {
success: true,
data: data,
fromCache: false,
isMock: true
};
} else {
// Production模式请求真实API
const url = `${this.config.apiBaseUrl}/announcements`;
data = await this.request(url, { method: 'GET' });
// 处理后端返回的数据格式(假设后端返回 { code:200, data:[...] }
const announcements = data.data || data;
// 写入缓存
this.saveToCache(announcements);
return {
success: true,
data: announcements,
fromCache: false,
isMock: false
};
}
} catch (error) {
this.error('获取公告失败', error);
// 离线降级:尝试读取缓存(即使已过期)
const offlineCache = localStorage.getItem(this.cache.key);
if (offlineCache) {
try {
const { data } = JSON.parse(offlineCache);
this.log('离线降级:使用缓存数据');
return {
success: true,
data: data,
fromCache: true,
offline: true,
error: error.message
};
} catch (e) {
this.error('离线缓存解析失败');
}
}
// 返回标准错误格式
return {
success: false,
error: this.getUserFriendlyError(error),
status: error.status || 500,
retry: () => this.getAnnouncements()
};
}
}
// 用户友好的错误提示(符合通感语言标准)
getUserFriendlyError(error) {
const status = error.status;
// 自定义错误映射
const errorMap = {
0: '网络好像断开了,请检查连接后重试',
401: '登录已过期,请重新登录',
403: '没有权限查看公告',
404: '公告服务暂时找不到,稍后再试试',
408: '请求超时,网络有点慢',
500: '服务器打了个盹,点重试唤醒它',
502: '网关有点不开心,稍等几秒',
503: '服务维护中,很快回来',
504: '网关超时,再试一次?'
};
// 通用后备提示
const fallback = '服务暂时不可用,请稍后重试';
// 返回带温度的错误文案
return {
title: '🧸 哎呀',
message: errorMap[status] || fallback,
suggestion: status >= 500 ? '服务器可能累了,点重试帮它清醒一下' : '检查网络或稍后再试',
retryable: [408, 500, 502, 503, 504, 0].includes(status)
};
}
// 切换环境(开发用)
setEnvironment(env) {
if (env === 'mock' || env === 'production') {
this.config.env = env;
this.isMock = env === 'mock';
this.clearCache(); // 切换环境时清空缓存
this.log(`环境已切换到: ${env}`);
}
}
}
// 创建全局API实例
const bulletinAPI = new BulletinAPI(CONFIG);