From b0f7d1ba9ef8c42904eb21113d7424eb18675aa3 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 28 Jan 2026 18:18:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9D=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 SDK 文档 API 路由顺序问题 - 将静态路由 /sdk-docs, /test-script, /secrets 移到动态路由 /{task_id} 之前 - 解决 "请求参数验证失败" 错误 2. 优化错误页面体验 - 使用 sessionStorage 传递错误信息,URL 保持干净 - 使用 router.replace 替代 push,浏览器返回不会停留在错误页 - 记录来源页面,支持正确返回 3. 增强网络错误处理 - 区分超时、网络错误、服务不可用 - 后端未启动时显示友好的 "服务暂时不可用" 提示 4. 添加定时任务模块文档 --- backend/app/routers/tasks.py | 374 ++++++++++++++--------------- docs/scheduled-tasks.md | 358 +++++++++++++++++++++++++++ frontend/src/api/index.js | 54 +++-- frontend/src/views/error/index.vue | 86 +++++-- 4 files changed, 649 insertions(+), 223 deletions(-) create mode 100644 docs/scheduled-tasks.md diff --git a/backend/app/routers/tasks.py b/backend/app/routers/tasks.py index 431025f..3fee208 100644 --- a/backend/app/routers/tasks.py +++ b/backend/app/routers/tasks.py @@ -75,6 +75,191 @@ class TestScriptRequest(BaseModel): params: Optional[dict] = None +# ==================== Static Routes (must be before dynamic routes) ==================== + +@router.get("/sdk-docs") +async def get_sdk_docs(): + """获取SDK文档""" + return { + "functions": [ + { + "name": "log", + "signature": "log(message: str, level: str = 'INFO')", + "description": "记录日志", + "example": "log('处理完成', 'INFO')" + }, + { + "name": "print", + "signature": "print(*args)", + "description": "打印输出", + "example": "print('Hello', 'World')" + }, + { + "name": "ai", + "signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)", + "description": "调用AI模型", + "example": "result = ai('生成一段问候语', system='你是友善的助手')" + }, + { + "name": "dingtalk", + "signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)", + "description": "发送钉钉消息", + "example": "dingtalk(webhook_url, '# 标题\\n内容')" + }, + { + "name": "wecom", + "signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')", + "description": "发送企微消息", + "example": "wecom(webhook_url, '消息内容')" + }, + { + "name": "http_get", + "signature": "http_get(url: str, headers: dict = None, params: dict = None)", + "description": "发起GET请求", + "example": "resp = http_get('https://api.example.com/data')" + }, + { + "name": "http_post", + "signature": "http_post(url: str, data: any = None, headers: dict = None)", + "description": "发起POST请求", + "example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})" + }, + { + "name": "db_query", + "signature": "db_query(sql: str, params: dict = None)", + "description": "执行只读SQL查询", + "example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})" + }, + { + "name": "get_var", + "signature": "get_var(key: str, default: any = None)", + "description": "获取持久化变量", + "example": "counter = get_var('counter', 0)" + }, + { + "name": "set_var", + "signature": "set_var(key: str, value: any)", + "description": "设置持久化变量", + "example": "set_var('counter', counter + 1)" + }, + { + "name": "del_var", + "signature": "del_var(key: str)", + "description": "删除持久化变量", + "example": "del_var('temp_data')" + }, + { + "name": "get_param", + "signature": "get_param(key: str, default: any = None)", + "description": "获取任务参数", + "example": "prompt = get_param('prompt', '默认提示词')" + }, + { + "name": "get_params", + "signature": "get_params()", + "description": "获取所有任务参数", + "example": "params = get_params()" + }, + { + "name": "get_tenants", + "signature": "get_tenants(app_code: str = None)", + "description": "获取租户列表", + "example": "tenants = get_tenants('notification-service')" + }, + { + "name": "get_tenant_config", + "signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)", + "description": "获取租户的应用配置", + "example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')" + }, + { + "name": "get_all_tenant_configs", + "signature": "get_all_tenant_configs(app_code: str)", + "description": "获取所有租户的应用配置", + "example": "configs = get_all_tenant_configs('notification-service')" + }, + { + "name": "get_secret", + "signature": "get_secret(key: str)", + "description": "获取密钥(优先租户级)", + "example": "api_key = get_secret('api_key')" + } + ], + "variables": [ + {"name": "task_id", "description": "当前任务ID"}, + {"name": "tenant_id", "description": "当前租户ID"}, + {"name": "trace_id", "description": "当前执行追踪ID"} + ], + "libraries": [ + {"name": "json", "description": "JSON处理"}, + {"name": "re", "description": "正则表达式"}, + {"name": "math", "description": "数学函数"}, + {"name": "random", "description": "随机数"}, + {"name": "hashlib", "description": "哈希函数"}, + {"name": "base64", "description": "Base64编解码"}, + {"name": "datetime", "description": "日期时间处理"}, + {"name": "timedelta", "description": "时间差"}, + {"name": "urlencode/quote/unquote", "description": "URL编码"} + ] + } + + +@router.post("/test-script") +async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)): + """测试脚本执行""" + executor = ScriptExecutor(db) + result = executor.test_script( + script_content=data.script_content, + task_id=0, + tenant_id=data.tenant_id, + params=data.params + ) + return result + + +@router.get("/secrets") +async def list_secrets( + tenant_id: Optional[str] = None, + db: Session = Depends(get_db) +): + """获取密钥列表""" + query = db.query(Secret) + if tenant_id: + query = query.filter(Secret.tenant_id == tenant_id) + + items = query.order_by(desc(Secret.created_at)).all() + + return { + "items": [ + { + "id": s.id, + "tenant_id": s.tenant_id, + "secret_key": s.secret_key, + "description": s.description, + "created_at": s.created_at, + "updated_at": s.updated_at + } + for s in items + ] + } + + +@router.post("/secrets") +async def create_secret(data: SecretCreate, db: Session = Depends(get_db)): + """创建密钥""" + secret = Secret( + tenant_id=data.tenant_id, + secret_key=data.secret_key, + secret_value=data.secret_value, + description=data.description + ) + db.add(secret) + db.commit() + db.refresh(secret) + + return {"success": True, "id": secret.id} + + # ==================== Task CRUD ==================== @router.get("") @@ -272,194 +457,7 @@ async def get_task_logs( } -# ==================== Script Testing ==================== - -@router.post("/test-script") -async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)): - """测试脚本执行""" - executor = ScriptExecutor(db) - result = executor.test_script( - script_content=data.script_content, - task_id=0, - tenant_id=data.tenant_id, - params=data.params - ) - return result - - -# ==================== SDK Documentation ==================== - -@router.get("/sdk-docs") -async def get_sdk_docs(): - """获取SDK文档""" - return { - "functions": [ - { - "name": "log", - "signature": "log(message: str, level: str = 'INFO')", - "description": "记录日志", - "example": "log('处理完成', 'INFO')" - }, - { - "name": "print", - "signature": "print(*args)", - "description": "打印输出", - "example": "print('Hello', 'World')" - }, - { - "name": "ai", - "signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)", - "description": "调用AI模型", - "example": "result = ai('生成一段问候语', system='你是友善的助手')" - }, - { - "name": "dingtalk", - "signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)", - "description": "发送钉钉消息", - "example": "dingtalk(webhook_url, '# 标题\\n内容')" - }, - { - "name": "wecom", - "signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')", - "description": "发送企微消息", - "example": "wecom(webhook_url, '消息内容')" - }, - { - "name": "http_get", - "signature": "http_get(url: str, headers: dict = None, params: dict = None)", - "description": "发起GET请求", - "example": "resp = http_get('https://api.example.com/data')" - }, - { - "name": "http_post", - "signature": "http_post(url: str, data: any = None, headers: dict = None)", - "description": "发起POST请求", - "example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})" - }, - { - "name": "db_query", - "signature": "db_query(sql: str, params: dict = None)", - "description": "执行只读SQL查询", - "example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})" - }, - { - "name": "get_var", - "signature": "get_var(key: str, default: any = None)", - "description": "获取持久化变量", - "example": "counter = get_var('counter', 0)" - }, - { - "name": "set_var", - "signature": "set_var(key: str, value: any)", - "description": "设置持久化变量", - "example": "set_var('counter', counter + 1)" - }, - { - "name": "del_var", - "signature": "del_var(key: str)", - "description": "删除持久化变量", - "example": "del_var('temp_data')" - }, - { - "name": "get_param", - "signature": "get_param(key: str, default: any = None)", - "description": "获取任务参数", - "example": "prompt = get_param('prompt', '默认提示词')" - }, - { - "name": "get_params", - "signature": "get_params()", - "description": "获取所有任务参数", - "example": "params = get_params()" - }, - { - "name": "get_tenants", - "signature": "get_tenants(app_code: str = None)", - "description": "获取租户列表", - "example": "tenants = get_tenants('notification-service')" - }, - { - "name": "get_tenant_config", - "signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)", - "description": "获取租户的应用配置", - "example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')" - }, - { - "name": "get_all_tenant_configs", - "signature": "get_all_tenant_configs(app_code: str)", - "description": "获取所有租户的应用配置", - "example": "configs = get_all_tenant_configs('notification-service')" - }, - { - "name": "get_secret", - "signature": "get_secret(key: str)", - "description": "获取密钥(优先租户级)", - "example": "api_key = get_secret('api_key')" - } - ], - "variables": [ - {"name": "task_id", "description": "当前任务ID"}, - {"name": "tenant_id", "description": "当前租户ID"}, - {"name": "trace_id", "description": "当前执行追踪ID"} - ], - "libraries": [ - {"name": "json", "description": "JSON处理"}, - {"name": "re", "description": "正则表达式"}, - {"name": "math", "description": "数学函数"}, - {"name": "random", "description": "随机数"}, - {"name": "hashlib", "description": "哈希函数"}, - {"name": "base64", "description": "Base64编解码"}, - {"name": "datetime", "description": "日期时间处理"}, - {"name": "timedelta", "description": "时间差"}, - {"name": "urlencode/quote/unquote", "description": "URL编码"} - ] - } - - -# ==================== Secrets ==================== - -@router.get("/secrets") -async def list_secrets( - tenant_id: Optional[str] = None, - db: Session = Depends(get_db) -): - """获取密钥列表""" - query = db.query(Secret) - if tenant_id: - query = query.filter(Secret.tenant_id == tenant_id) - - items = query.order_by(desc(Secret.created_at)).all() - - return { - "items": [ - { - "id": s.id, - "tenant_id": s.tenant_id, - "secret_key": s.secret_key, - "description": s.description, - "created_at": s.created_at, - "updated_at": s.updated_at - } - for s in items - ] - } - - -@router.post("/secrets") -async def create_secret(data: SecretCreate, db: Session = Depends(get_db)): - """创建密钥""" - secret = Secret( - tenant_id=data.tenant_id, - secret_key=data.secret_key, - secret_value=data.secret_value, - description=data.description - ) - db.add(secret) - db.commit() - db.refresh(secret) - - return {"success": True, "id": secret.id} - +# ==================== Secrets (dynamic routes) ==================== @router.put("/secrets/{secret_id}") async def update_secret(secret_id: int, data: SecretUpdate, db: Session = Depends(get_db)): diff --git a/docs/scheduled-tasks.md b/docs/scheduled-tasks.md new file mode 100644 index 0000000..3d5bcda --- /dev/null +++ b/docs/scheduled-tasks.md @@ -0,0 +1,358 @@ +# 定时任务系统文档 + +## 功能概述 + +平台定时任务系统,支持 Python 脚本或 Webhook 定时执行,执行结果可自动推送到钉钉/企微机器人。 + +**核心能力**: +- 脚本执行:安全沙箱运行 Python 脚本,内置 AI、HTTP、数据库等 SDK +- 调度方式:指定时间点(多选)或 CRON 表达式 +- 消息推送:支持钉钉/企微机器人所有消息格式(markdown、actionCard、feedCard 等) +- 失败处理:支持重试和告警通知 + +--- + +## 数据库表 + +### platform_scheduled_tasks(定时任务表) + +```sql +CREATE TABLE platform_scheduled_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(50) COMMENT '租户ID,空为全局任务', + task_name VARCHAR(100) NOT NULL COMMENT '任务名称', + task_desc VARCHAR(500) COMMENT '任务描述', + schedule_type ENUM('simple', 'cron') NOT NULL DEFAULT 'simple', + time_points JSON COMMENT '时间点列表 ["08:00", "12:00"]', + cron_expression VARCHAR(100) COMMENT 'CRON表达式', + timezone VARCHAR(50) DEFAULT 'Asia/Shanghai', + execution_type ENUM('webhook', 'script') NOT NULL DEFAULT 'script', + webhook_url VARCHAR(500), + script_content TEXT COMMENT 'Python脚本内容', + script_deps TEXT COMMENT '脚本依赖', + input_params JSON COMMENT '输入参数', + retry_count INT DEFAULT 0, + retry_interval INT DEFAULT 60, + alert_on_failure TINYINT(1) DEFAULT 0, + alert_webhook VARCHAR(500), + notify_channels JSON COMMENT '通知渠道ID列表', + notify_wecom_app_id INT COMMENT '企微应用ID', + is_enabled TINYINT(1) DEFAULT 1, + last_run_at DATETIME, + last_run_status ENUM('success', 'failed', 'running'), + last_run_message TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +### platform_task_notify_channels(通知渠道表) + +```sql +CREATE TABLE platform_task_notify_channels ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id VARCHAR(50) NOT NULL COMMENT '租户ID', + channel_name VARCHAR(100) NOT NULL COMMENT '渠道名称', + channel_type ENUM('dingtalk_bot', 'wecom_bot') NOT NULL, + webhook_url VARCHAR(500) NOT NULL, + sign_secret VARCHAR(200) COMMENT '钉钉加签密钥', + description VARCHAR(255), + is_enabled TINYINT(1) DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +### platform_task_logs(执行日志表) + +```sql +CREATE TABLE platform_task_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + task_id INT NOT NULL, + tenant_id VARCHAR(50), + trace_id VARCHAR(100), + status ENUM('running', 'success', 'failed'), + started_at DATETIME, + finished_at DATETIME, + duration_ms INT, + output TEXT, + error TEXT, + retry_count INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 后端文件结构 + +``` +backend/app/ +├── models/ +│ ├── scheduled_task.py # ScheduledTask, TaskLog, ScriptVar, Secret 模型 +│ └── notification_channel.py # TaskNotifyChannel 模型 +├── routers/ +│ ├── tasks.py # 定时任务 API (/api/scheduled-tasks) +│ └── notification_channels.py # 通知渠道 API (/api/notification-channels) +└── services/ + ├── scheduler.py # APScheduler 调度服务 + ├── script_executor.py # 脚本执行器(安全沙箱) + └── script_sdk.py # 脚本内置 SDK +``` + +--- + +## 脚本 SDK 文档 + +### 内置函数 + +```python +# 日志 +log(message) # 记录日志 +print(message) # 打印输出 + +# AI 调用 +ai(prompt, system=None, model='deepseek-chat') # 调用 AI + +# 通知发送(直接发送,不走 result) +dingtalk(webhook_url, content, title='通知') +wecom(webhook_url, content) + +# HTTP 请求 +http_get(url, headers=None, params=None) +http_post(url, data=None, json=None, headers=None) + +# 数据库查询(只读) +db_query(sql, params=None) + +# 变量存储(跨执行持久化) +get_var(key, default=None) +set_var(key, value) +del_var(key) + +# 任务参数 +get_param(key, default=None) # 获取单个参数 +get_params() # 获取所有参数 + +# 租户相关 +get_tenants() # 获取所有租户 +get_tenant_config(tenant_id, app_code, key) # 获取租户配置 +get_all_tenant_configs(app_code, key) # 获取所有租户的配置 + +# 密钥 +get_secret(key) # 获取密钥 +``` + +### 内置变量 + +```python +task_id # 当前任务ID +tenant_id # 当前租户ID(可能为空) +trace_id # 追踪ID +``` + +### 内置模块(无需 import) + +```python +datetime # datetime.now(), datetime.strptime() +date # date.today() +timedelta # timedelta(days=1) +time # time.sleep(), time.time() +json # json.dumps(), json.loads() +re # re.search(), re.match() +math # math.ceil(), math.floor() +random # random.randint(), random.choice() +hashlib # hashlib.md5() +base64 # base64.b64encode() +``` + +--- + +## 消息格式(result 变量) + +### 基础格式(默认 markdown) + +```python +result = { + 'content': 'Markdown 内容', + 'title': '消息标题' +} +``` + +### 钉钉 ActionCard(交互卡片) + +```python +result = { + 'msg_type': 'actionCard', + 'title': '卡片标题', + 'content': '''### 正文内容 +| 列1 | 列2 | +|:---:|:---:| +| A | B | +''', + 'btn_orientation': '1', # 0-竖向 1-横向 + 'buttons': [ + {'title': '按钮1', 'url': 'https://...'}, + {'title': '按钮2', 'url': 'https://...'} + ] +} +``` + +### 钉钉 FeedCard(信息流) + +```python +result = { + 'msg_type': 'feedCard', + 'links': [ + {'title': '标题1', 'url': 'https://...', 'pic_url': 'https://...'}, + {'title': '标题2', 'url': 'https://...', 'pic_url': 'https://...'} + ] +} +``` + +### 钉钉 Link(链接消息) + +```python +result = { + 'msg_type': 'link', + 'title': '链接标题', + 'content': '链接描述', + 'url': 'https://...', + 'pic_url': 'https://...' +} +``` + +### 企微 News(图文消息) + +```python +result = { + 'msg_type': 'news', + 'articles': [ + { + 'title': '文章标题', + 'description': '文章描述', + 'url': 'https://...', + 'picurl': 'https://...' + } + ] +} +``` + +### 企微 Template Card(模板卡片) + +```python +result = { + 'msg_type': 'template_card', + 'card_type': 'text_notice', # text_notice / news_notice / button_interaction + 'title': '卡片标题', + 'content': '卡片内容', + 'horizontal_list': [ + {'keyname': '申请人', 'value': '张三'}, + {'keyname': '金额', 'value': '¥5,000'} + ], + 'jump_list': [ + {'type': 1, 'title': '查看详情', 'url': 'https://...'} + ] +} +``` + +--- + +## API 端点 + +### 定时任务 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/scheduled-tasks | 任务列表 | +| GET | /api/scheduled-tasks/{id} | 任务详情 | +| POST | /api/scheduled-tasks | 创建任务 | +| PUT | /api/scheduled-tasks/{id} | 更新任务 | +| DELETE | /api/scheduled-tasks/{id} | 删除任务 | +| POST | /api/scheduled-tasks/{id}/toggle | 启用/禁用 | +| POST | /api/scheduled-tasks/{id}/run | 立即执行 | +| GET | /api/scheduled-tasks/{id}/logs | 执行日志 | +| POST | /api/scheduled-tasks/test-script | 测试脚本 | +| GET | /api/scheduled-tasks/sdk-docs | SDK 文档 | + +### 通知渠道 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/notification-channels | 渠道列表 | +| POST | /api/notification-channels | 创建渠道 | +| PUT | /api/notification-channels/{id} | 更新渠道 | +| DELETE | /api/notification-channels/{id} | 删除渠道 | +| POST | /api/notification-channels/{id}/test | 测试渠道 | + +--- + +## 前端文件 + +``` +frontend/src/views/ +├── scheduled-tasks/ +│ └── index.vue # 定时任务管理页面 +└── notification-channels/ + └── index.vue # 通知渠道管理页面 +``` + +--- + +## 示例脚本 + +### 基础示例 + +```python +# 无需 import,模块已内置 +log('任务开始执行') + +now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') +prompt = get_param('prompt', '默认提示词') + +content = ai(prompt, system='你是一个助手') + +result = { + 'title': '每日推送', + 'content': f'**生成时间**: {now}\n\n{content}' +} + +log('任务执行完成') +``` + +### 复杂卡片示例 + +```python +log('生成销售日报') + +now = datetime.now() +today = now.strftime('%Y年%m月%d日') + +# 模拟数据 +revenue = random.randint(50000, 150000) + +result = { + 'msg_type': 'actionCard', + 'title': f'销售日报 | {today}', + 'content': f'''### 今日业绩 + +| 指标 | 数值 | +|:---:|:---:| +| 销售额 | **¥{revenue:,}** | +| 订单数 | **{random.randint(40, 80)}** | + +> 点击查看详情 +''', + 'buttons': [ + {'title': '查看详情', 'url': 'https://example.com/report'} + ] +} +``` + +--- + +## 部署信息 + +- **测试环境**: https://platform.test.ai.ireborn.com.cn +- **数据库**: new_qiqi (测试) / new_platform_prod (生产) +- **Docker 容器**: platform-backend-test / platform-frontend-test diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 075fbef..7e0ab49 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -18,9 +18,21 @@ function parseApiError(error) { status: 500 } + // 网络错误(后端未启动、网络断开等) if (!error.response) { - result.code = 'NETWORK_ERROR' - result.message = '网络连接失败,请检查网络后重试' + if (error.code === 'ECONNABORTED') { + result.code = 'TIMEOUT_ERROR' + result.message = '请求超时,请稍后重试' + result.status = 0 + } else if (error.message?.includes('Network Error')) { + result.code = 'SERVICE_UNAVAILABLE' + result.message = '服务暂时不可用,请稍后重试' + result.status = 503 + } else { + result.code = 'NETWORK_ERROR' + result.message = '网络连接失败,请检查网络后重试' + result.status = 0 + } return result } @@ -40,18 +52,23 @@ function parseApiError(error) { } /** - * 跳转到错误页面 + * 跳转到错误页面(使用 sessionStorage + replace,不影响浏览器历史) */ function navigateToErrorPage(errorInfo) { - router.push({ - name: 'Error', - query: { - code: errorInfo.code, - message: errorInfo.message, - trace_id: errorInfo.traceId, - status: String(errorInfo.status) - } - }) + // 记录当前页面路径(用于返回) + sessionStorage.setItem('errorFromPath', router.currentRoute.value.fullPath) + + // 保存错误信息到 sessionStorage(不会显示在 URL 中) + sessionStorage.setItem('errorInfo', JSON.stringify({ + code: errorInfo.code, + message: errorInfo.message, + traceId: errorInfo.traceId, + status: errorInfo.status, + timestamp: Date.now() + })) + + // 使用 replace 而不是 push,这样浏览器返回时不会停留在错误页 + router.replace({ name: 'Error' }) } // 请求拦截器 @@ -75,6 +92,15 @@ api.interceptors.response.use( console.error(`[API Error] ${errorInfo.code}: ${errorInfo.message}${traceLog}`) + // 严重错误列表(跳转错误页) + const criticalErrors = [ + 'INTERNAL_ERROR', + 'SERVICE_UNAVAILABLE', + 'GATEWAY_ERROR', + 'NETWORK_ERROR', + 'TIMEOUT_ERROR' + ] + if (error.response?.status === 401) { localStorage.removeItem('token') localStorage.removeItem('user') @@ -82,8 +108,8 @@ api.interceptors.response.use( ElMessage.error('登录已过期,请重新登录') } else if (error.response?.status === 403) { ElMessage.error('没有权限执行此操作') - } else if (['INTERNAL_ERROR', 'SERVICE_UNAVAILABLE', 'GATEWAY_ERROR'].includes(errorInfo.code)) { - // 严重错误跳转到错误页面 + } else if (criticalErrors.includes(errorInfo.code)) { + // 严重错误(包括网络错误、服务不可用)跳转到错误页面 navigateToErrorPage(errorInfo) } else { // 普通错误显示消息 diff --git a/frontend/src/views/error/index.vue b/frontend/src/views/error/index.vue index 9db86d8..aca8a85 100644 --- a/frontend/src/views/error/index.vue +++ b/frontend/src/views/error/index.vue @@ -1,33 +1,69 @@