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 @@
@@ -72,7 +116,7 @@ const retry = () => router.back()
- 重试
+ 返回
返回首页