liuq 3 месяцев назад
Родитель
Сommit
bf1d64d05e

+ 82 - 0
frontend/public/ai-dev-guide.md

@@ -0,0 +1,82 @@
+# Unified Authentication Platform - AI 开发指南
+
+## 1. 项目概述
+Unified Authentication Platform (UAP) 是一个统一认证平台,提供简易认证 (Simple Auth)、OIDC、SSO、账号同步等功能。
+
+### 技术栈
+- **Frontend**: Vue 3, TypeScript, Vite, Element Plus, Pinia, Axios.
+- **Backend**: FastAPI (Python), SQLModel (SQLAlchemy + Pydantic), PostgreSQL.
+- **Infrastructure**: Docker, Docker Compose.
+
+### 项目结构
+```
+/backend
+  /app
+    /api/v1          # API 路由
+    /core            # 核心配置 (Config, Security, DB)
+    /models          # SQLModel 数据库模型
+    /schemas         # Pydantic 数据验证模型
+    /services        # 业务逻辑层
+/frontend
+  /src
+    /api             # Axios API 封装
+    /components      # Vue 组件
+    /views           # 页面视图
+    /store           # Pinia 状态管理
+```
+
+## 2. 接口开发规范 (API Convention)
+
+### Base URL
+- Base URL: `{{API_BASE_URL}}` (Backend)
+- 生产环境: 根据部署域名配置
+
+### 认证方式
+1. **Bearer Token**: 用于管理后台 API。Header: `Authorization: Bearer <token>`
+2. **Signature (签名)**: 用于 `Simple Auth` 业务 API。需使用 App ID 和 App Secret 计算签名。
+
+---
+
+## 3. 核心业务流程:简易认证 (Simple Auth)
+
+### 3.1 签名算法
+所有 `Simple Auth` 接口需校验签名。
+
+**步骤**:
+1. 收集所有参数(排除 `sign`)。
+2. 按 Key 字母顺序排序。
+3. 拼接成 `key1=val1&key2=val2...` 字符串。
+4. 使用 `App Secret` 进行 HMAC-SHA256 计算。
+5. 转换为 Hex 字符串。
+
+### 3.2 关键接口
+
+#### 登录 (Login)
+- **POST** `/api/v1/simple/login`
+- **Body**: `{ "app_id": "...", "identifier": "...", "password": "...", "timestamp": 123, "sign": "..." }`
+- **Response**: `{ "ticket": "..." }`
+
+#### 验证票据 (Validate)
+- **POST** `/api/v1/simple/validate`
+- **Body**: `{ "app_id": "...", "ticket": "...", "timestamp": 123, "sign": "..." }`
+- **Response**: `{ "valid": true, "user_id": 1, ... }`
+
+#### 免登跳转 (Exchange)
+- **POST** `/api/v1/simple/exchange`
+- **Body**: `{ "app_id": "...", "target_app_id": "...", "user_mobile": "...", ... }`
+- **Response**: `{ "redirect_url": "http://target/callback?ticket=..." }`
+
+---
+
+## 4. 数据库模型概览
+
+- **User**: 用户表 (`backend/app/models/user.py`) - `id`, `mobile`, `hashed_password`, `is_active`.
+- **Application**: 应用表 (`backend/app/models/application.py`) - `id`, `app_id`, `app_secret`, `redirect_uris`.
+- **UserMapping**: 用户-应用映射表 (`backend/app/models/mapping.py`) - `user_id`, `app_id`, `mapped_key`.
+- **LoginLog**: 登录日志 (`backend/app/models/login_log.py`).
+
+## 5. 开发建议
+- 后端修改 Model 时,记得运行 `alembic` (如果配置了) 或检查数据库同步。
+- 前端添加新 API 时,请在 `src/api` 下创建对应模块文件。
+- 使用 `read_lints` 检查代码质量。
+

+ 57 - 0
frontend/public/docs/account_sync.md

@@ -0,0 +1,57 @@
+# 统一认证平台 - 账号同步 (M2M)
+
+## 1. 概述
+用于将外部业务系统(如 OA、CRM)的用户账号关系同步到本平台。支持批量调用。
+
+**Base URL**: `{{API_BASE_URL}}`
+
+## 2. 认证方式
+此接口不使用签名算法,而是使用 **App Access Token**。
+- **Header**: `X-App-Access-Token: <YOUR_TOKEN>`
+- Token 可在平台管理界面的应用详情页查看。
+
+## 3. 同步接口 (Sync)
+创建或更新用户,并建立应用映射关系。
+
+- **URL**: `POST /apps/mapping/sync`
+- **Content-Type**: `application/json`
+
+### 逻辑
+1. 根据 `mobile` 查找用户。若不存在则**自动创建**(随机密码)。
+2. 更新该用户与当前应用的映射 (`mapped_key`, `mapped_email`)。
+
+### Request Body
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `mobile` | string | Yes | 用户手机号 (平台唯一标识) |
+| `mapped_key` | string | No | 外部系统用户ID |
+| `mapped_email` | string | No | 外部系统邮箱 |
+| `is_active` | boolean | No | 映射状态 (默认 true) |
+
+### 示例
+```bash
+curl -X POST "{{API_BASE_URL}}/apps/mapping/sync" \
+     -H "Content-Type: application/json" \
+     -H "X-App-Access-Token: eyJhbGci..." \
+     -d '{
+           "mobile": "13800138000",
+           "mapped_key": "user_1001",
+           "mapped_email": "zhangsan@example.com"
+         }'
+```
+
+### Response (200)
+```json
+{
+  "id": 123,           // 映射记录 ID
+  "user_id": 456,      // 平台用户 ID
+  "user_mobile": "13800138000",
+  "mapped_key": "user_1001",
+  "is_active": true
+}
+```
+
+## 4. 错误码
+- `400 Bad Request`: 参数错误或映射关系冲突(如 mapped_key 已被占用)。
+- `403 Forbidden`: Token 无效。
+

+ 48 - 0
frontend/public/docs/api_comparison.md

@@ -0,0 +1,48 @@
+# 统一认证平台 - 接口对比与选择
+
+## 1. 接口对比: `/login` vs `/sso-login`
+
+### 1.1 `/login` (通用登录)
+- **定位**: 后端服务对接、高安全性场景。
+- **认证方式**: 仅支持 `identifier` + `password`。
+- **安全性**: 推荐使用 **签名 (Sign)** 验证。
+- **返回**: 仅返回 `ticket` (应用模式) 或 `access_token` (平台模式)。
+- **适用**: 后端代理登录、OIDC 应用后端交互。
+
+### 1.2 `/sso-login` (简易 SSO)
+- **定位**: 前端浏览器直接对接、快速实现 SSO。
+- **认证方式**: **优先使用 Session (Cookie)**,如果未登录则使用 `username` + `password`。
+- **安全性**: 无签名校验(依赖 HTTPS 和 Cookie 安全)。
+- **返回**: `redirect_url` (包含 ticket)。
+- **适用**: SPA 前端、需要利用平台已登录状态实现免登的场景。**仅支持 SIMPLE_API 类型应用**。
+
+## 2. 详细对比表
+
+| 特性 | `/login` | `/sso-login` |
+|---|---|---|
+| **调用端** | 后端服务器 (推荐) | 前端浏览器 |
+| **支持 Session 免登** | ❌ 否 | ✅ 是 (自动检测) |
+| **签名验证** | ✅ 支持 | ❌ 不支持 |
+| **应用类型** | 全部 | 仅 SIMPLE_API |
+| **返回值** | JSON 数据 (ticket/token) | JSON (redirect_url) |
+
+## 3. 使用示例
+
+### 3.1 /sso-login (前端调用)
+```javascript
+// 场景:用户点击“登录”,前端调用此接口
+const response = await fetch('{{API_BASE_URL}}/simple/sso-login', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    app_id: 'my_app_123'
+    // 如果确定用户未在平台登录,可附加 username/password
+    // username: '...', password: '...'
+  })
+});
+
+const data = await response.json();
+// 直接跳转,后端已生成好带 ticket 的 URL
+window.location.href = data.redirect_url;
+```
+

+ 267 - 0
frontend/public/docs/simple_auth.md

@@ -0,0 +1,267 @@
+# 统一认证平台 - 简易认证 (Simple Auth) 集成指南
+
+## 1. 概述
+本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。
+
+**Base URL**: `{{API_BASE_URL}}/simple` (请根据实际部署环境替换)
+
+## 2. 核心流程
+1. **用户输入**: 用户在客户端输入账号密码。
+2. **签名**: 客户端/后端生成签名 (Sign)。
+3. **登录**: POST `/login` (账号+密码+签名) -> 获取 `Ticket`。
+4. **验证**: POST `/validate` (Ticket+签名) -> 获取用户信息。
+
+## 3. 安全警告
+- **App Secret** 严禁泄露给前端浏览器。
+- 建议所有涉及 Secret 的签名计算都在**后端**完成。
+
+## 4. 签名算法 (Signature)
+所有接口(除部分公开接口外)都需要校验签名。
+
+**步骤**:
+1. **准备参数**: 收集所有请求参数(**排除 `sign` 本身**)。
+2. **排序**: 按照参数名(key)的 ASCII 码从小到大排序。
+3. **拼接**: 将排序后的参数拼接成 `key1=value1&key2=value2...` 格式。
+4. **计算 HMAC**: 使用 `App Secret` 作为密钥,对拼接字符串进行 **HMAC-SHA256** 计算。
+5. **Hex 编码**: 将结果转换为十六进制字符串。
+
+### Python 示例
+```python
+import hmac
+import hashlib
+
+def generate_signature(secret: str, params: dict) -> str:
+    data = {k: v for k, v in params.items() if k != "sign" and v is not None}
+    sorted_keys = sorted(data.keys())
+    query_string = "&".join([f"{k}={data[k]}" for k in sorted_keys])
+    signature = hmac.new(
+        secret.encode('utf-8'),
+        query_string.encode('utf-8'),
+        hashlib.sha256
+    ).hexdigest()
+    return signature
+```
+
+## 5. 接口定义
+
+### 5.1 密码登录 (Login)
+获取临时票据 (Ticket)。
+
+- **URL**: `POST /login`
+- **Content-Type**: `application/json`
+
+**Request Body**:
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `app_id` | string | Yes | 应用 ID |
+| `identifier` | string | Yes | 用户标识(手机号、用户名或邮箱) |
+| `password` | string | Yes | 明文密码 |
+| `timestamp` | int | Yes | 当前时间戳 (秒) |
+| `sign` | string | Yes | 签名 |
+
+**Response (200)**:
+```json
+{
+  "ticket": "TICKET-7f8e9d0a-..." 
+}
+```
+
+### 5.2 验证票据 (Validate)
+解析票据获取用户信息。
+
+- **URL**: `POST /validate`
+- **Content-Type**: `application/json`
+
+**Request Body**:
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `app_id` | string | Yes | 应用 ID |
+| `ticket` | string | Yes | 上一步获取的票据 |
+| `timestamp` | int | Yes | 当前时间戳 |
+| `sign` | string | Yes | 签名 (参数变化需重新计算) |
+
+**Response (200)**:
+```json
+{
+  "valid": true,
+  "user_id": 1001,
+  "mobile": "13800138000",
+  "mapped_key": "user_zhangsan",  // 第三方映射ID
+  "mapped_email": "zhangsan@example.com"
+}
+```
+
+**Response (Invalid)**:
+```json
+{ "valid": false }
+```
+
+## 6. 多语言调用示例
+
+### Python
+```python
+import requests
+import time
+import hmac
+import hashlib
+import json
+
+# 配置信息
+API_BASE = "{{API_BASE_URL}}/simple"
+APP_ID = "test_app_001"
+APP_SECRET = "secret_key_abc123" # 务必保密
+
+def get_sign(params):
+    # 排除 sign 字段
+    data = {k: v for k, v in params.items() if k != "sign"}
+    # 排序并拼接
+    query_string = "&".join([f"{k}={data[k]}" for k in sorted(data.keys())])
+    # HMAC-SHA256
+    return hmac.new(APP_SECRET.encode(), query_string.encode(), hashlib.sha256).hexdigest()
+
+def main():
+    # === 步骤 1: 登录获取 Ticket ===
+    login_ts = int(time.time())
+    login_payload = {
+        "app_id": APP_ID,
+        "identifier": "13800000001",
+        "password": "password123",
+        "timestamp": login_ts
+    }
+    # 计算签名
+    login_payload["sign"] = get_sign(login_payload)
+
+    print(f"1. 正在尝试登录: {login_payload['identifier']} ...")
+    resp = requests.post(f"{API_BASE}/login", json=login_payload)
+    
+    if resp.status_code != 200:
+        print(f"登录失败: {resp.text}")
+        return
+
+    ticket = resp.json().get("ticket")
+    print(f"登录成功! 获取到 Ticket: {ticket}")
+
+    # === 步骤 2: 使用 Ticket 换取用户信息 ===
+    validate_ts = int(time.time())
+    validate_payload = {
+        "app_id": APP_ID,
+        "ticket": ticket,
+        "timestamp": validate_ts
+    }
+    # 重新计算签名(参数变了,签名必须重算)
+    validate_payload["sign"] = get_sign(validate_payload)
+
+    print(f"\n2. 正在验证 Ticket...")
+    v_resp = requests.post(f"{API_BASE}/validate", json=validate_payload)
+    
+    user_info = v_resp.json()
+    if user_info.get("valid"):
+        print("验证成功! 用户信息如下:")
+        print(json.dumps(user_info, indent=2, ensure_ascii=False))
+    else:
+        print("Ticket 无效或已过期")
+
+if __name__ == "__main__":
+    main()
+```
+
+### Java
+```java
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.*;
+import java.nio.charset.StandardCharsets;
+
+public class AuthExample {
+    private static final String SECRET = "secret_key_abc123";
+
+    public static String generateSign(Map<String, String> params) {
+        try {
+            // 1. 排序
+            List<String> sortedKeys = new ArrayList<>(params.keySet());
+            Collections.sort(sortedKeys);
+            
+            // 2. 拼接
+            StringBuilder sb = new StringBuilder();
+            for (String key : sortedKeys) {
+                if (!key.equals("sign") && params.get(key) != null) {
+                    if (sb.length() > 0) sb.append("&");
+                    sb.append(key).append("=").append(params.get(key));
+                }
+            }
+            
+            // 3. HMAC-SHA256
+            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
+            SecretKeySpec secret_key = new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+            sha256_HMAC.init(secret_key);
+            
+            byte[] bytes = sha256_HMAC.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
+            
+            // 4. Hex
+            StringBuilder hex = new StringBuilder();
+            for (byte b : bytes) {
+                hex.append(String.format("%02x", b));
+            }
+            return hex.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+    
+    public static void main(String[] args) {
+        Map<String, String> params = new HashMap<>();
+        params.put("app_id", "test_app_001");
+        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
+        
+        System.out.println("Sign: " + generateSign(params));
+    }
+}
+```
+
+### JavaScript (Node.js)
+```javascript
+const crypto = require('crypto');
+const axios = require('axios'); // npm install axios
+
+const APP_ID = 'test_app_001';
+const APP_SECRET = 'secret_key_abc123';
+const BASE_URL = '{{API_BASE_URL}}/simple';
+
+function getSign(params) {
+  // 1. 过滤 & 排序
+  const keys = Object.keys(params)
+    .filter(k => k !== 'sign' && params[k] !== undefined)
+    .sort();
+  
+  // 2. 拼接 Query String
+  const queryString = keys.map(k => `${k}=${params[k]}`).join('&');
+  
+  // 3. HMAC-SHA256
+  return crypto.createHmac('sha256', APP_SECRET)
+    .update(queryString)
+    .digest('hex');
+}
+
+async function login() {
+  const timestamp = Math.floor(Date.now() / 1000);
+  const payload = {
+    app_id: APP_ID,
+    identifier: '13800000001',
+    password: 'password123',
+    timestamp: timestamp
+  };
+  
+  payload.sign = getSign(payload);
+  
+  try {
+    console.log('Sending login request...');
+    const res = await axios.post(`${BASE_URL}/login`, payload);
+    console.log('Ticket:', res.data.ticket);
+  } catch (error) {
+    console.error('Login Failed:', error.response?.data || error.message);
+  }
+}
+
+login();
+```

+ 47 - 0
frontend/public/docs/ticket_exchange.md

@@ -0,0 +1,47 @@
+# 统一认证平台 - 票据交互 (Ticket Exchange)
+
+## 1. 概述
+当用户已在 **源应用 (Source App)** 登录,需要无缝跳转到 **目标应用 (Target App)** 且实现免登录时,使用此接口。
+
+**Base URL**: `{{API_BASE_URL}}/simple` (请根据实际部署环境替换)
+
+## 2. 交互流程
+1. **源应用**后端调用 `POST /exchange` 获取跳转 URL (包含目标应用的 Ticket)。
+2. **源应用**前端重定向用户浏览器到该 URL。
+3. **目标应用**接收请求,提取 `ticket`。
+4. **目标应用**后端调用 `POST /validate` 验证票据并登录用户。
+
+## 3. 接口定义
+
+### 3.1 获取跳转 URL (Exchange)
+- **URL**: `POST /exchange`
+- **Content-Type**: `application/json`
+
+**Request Body**:
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `app_id` | string | Yes | **源应用** ID |
+| `target_app_id` | string | Yes | **目标应用** ID |
+| `user_mobile` | string | Yes | 用户手机号 (身份标识) |
+| `timestamp` | int | Yes | 当前时间戳 |
+| `sign` | string | Yes | 签名 (**使用源应用 Secret**) |
+
+**Response (200)**:
+```json
+{
+  "ticket": "TICKET-for-target-app-...",
+  "redirect_url": "http://target-app-b.com/sso/callback?ticket=TICKET-..."
+}
+```
+*注*: `redirect_url` 自动取自目标应用配置的 `redirect_uris` 的第一个地址。
+
+### 3.2 验证票据 (Validate)
+目标应用接收到 ticket 后进行验证。
+
+- **URL**: `POST /validate`
+- **注意**: 此时 `app_id` 为**目标应用** ID,签名需使用**目标应用 Secret**。
+
+## 4. 常见错误
+- `404 Not Found`: 应用 ID 错误或用户不存在。
+- `400 Bad Request`: 签名验证失败。
+

+ 121 - 8
frontend/src/views/Help.vue

@@ -1,11 +1,23 @@
 <template>
   <div class="help-container">
-    <h1>使用帮助</h1>
+    <div class="help-header">
+      <h1>使用帮助</h1>
+      <el-button type="info" plain @click="openSwagger">
+        <el-icon style="margin-right: 5px"><Link /></el-icon>
+        API 文档 (Swagger)
+      </el-button>
+    </div>
     
     <el-tabs v-model="activeTab" class="help-tabs">
       <el-tab-pane label="自定义登录页面" name="custom-login">
         <div class="help-content">
-          <h2>统一认证平台 - 简易认证 (Simple Auth) 集成指南</h2>
+          <div class="content-header">
+            <h2>统一认证平台 - 简易认证 (Simple Auth) 集成指南</h2>
+            <el-button type="primary" size="small" plain @click="downloadDoc('/docs/simple_auth.md', 'Simple_Auth_Guide.md')">
+              <el-icon style="margin-right: 5px"><Download /></el-icon>
+              下载 AI 开发文档
+            </el-button>
+          </div>
           <p class="intro">本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。</p>
 
           <div class="section">
@@ -530,7 +542,13 @@ print(sign)
 
       <el-tab-pane label="票据交互" name="ticket-exchange">
         <div class="help-content">
-          <h2>票据交互 (Ticket Exchange) 使用说明</h2>
+          <div class="content-header">
+            <h2>票据交互 (Ticket Exchange) 使用说明</h2>
+            <el-button type="primary" size="small" plain @click="downloadDoc('/docs/ticket_exchange.md', 'Ticket_Exchange_Guide.md')">
+              <el-icon style="margin-right: 5px"><Download /></el-icon>
+              下载 AI 开发文档
+            </el-button>
+          </div>
           <p class="intro">当用户已在 <strong>源应用 (Source App)</strong> 登录,需要无缝跳转到 <strong>目标应用 (Target App)</strong> 且实现免登录时,使用此接口。</p>
 
           <div class="section">
@@ -635,7 +653,13 @@ print(sign)
 
       <el-tab-pane label="接口对比" name="api-comparison">
         <div class="help-content">
-          <h2>接口对比:/login vs /sso-login</h2>
+          <div class="content-header">
+            <h2>接口对比:/login vs /sso-login</h2>
+            <el-button type="primary" size="small" plain @click="downloadDoc('/docs/api_comparison.md', 'API_Comparison_Guide.md')">
+              <el-icon style="margin-right: 5px"><Download /></el-icon>
+              下载 AI 开发文档
+            </el-button>
+          </div>
           <p class="intro">平台提供了两个登录接口,它们在功能上有一定重叠,但<strong>设计目的和使用场景不同</strong>。本章节详细对比两个接口的区别,帮助您选择最合适的接口。</p>
 
           <div class="section">
@@ -955,7 +979,13 @@ window.location.href = redirect_url;  // 直接跳转
 
       <el-tab-pane label="账号同步 (M2M)" name="account-sync">
         <div class="help-content">
-          <h2>账号同步 (M2M) 使用说明</h2>
+          <div class="content-header">
+            <h2>账号同步 (M2M) 使用说明</h2>
+            <el-button type="primary" size="small" plain @click="downloadDoc('/docs/account_sync.md', 'Account_Sync_Guide.md')">
+              <el-icon style="margin-right: 5px"><Download /></el-icon>
+              下载 AI 开发文档
+            </el-button>
+          </div>
           <p class="intro">此接口用于将外部业务系统(如 OA、CRM)的用户账号关系同步到本平台。支持批量调用,实现“本平台用户(手机号)”与“外部应用账号(ID/邮箱)”的绑定。</p>
 
           <div class="section">
@@ -1043,8 +1073,68 @@ curl -X POST "http://your-uap-domain/api/v1/apps/mapping/sync" \
 
 <script setup lang="ts">
 import { ref } from 'vue'
+import { Download } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
 
 const activeTab = ref('custom-login')
+
+const downloadDoc = async (filePath: string, fileName: string) => {
+  try {
+    const response = await fetch(filePath)
+    if (!response.ok) throw new Error('Network response was not ok')
+    
+    let text = await response.text()
+
+    // 获取动态 Base URL
+    // 1. 优先尝试使用环境变量
+    let baseUrl = import.meta.env.VITE_API_BASE_URL
+    
+    // 2. 如果没有环境变量或为相对路径,基于当前 window.location 构建
+    if (!baseUrl) {
+      // 假设后端 API 挂载在 /api/v1 下 (常见 Nginx 代理或 Docker 配置)
+      baseUrl = window.location.origin + '/api/v1'
+    } else if (baseUrl.startsWith('/')) {
+      baseUrl = window.location.origin + baseUrl
+    }
+
+    // 3. 特殊处理: 如果是 localhost 但当前通过 IP 访问,尝试自动替换 hostname
+    // 这样开发者在局域网测试时,下载的文档会显示局域网 IP
+    if (baseUrl.includes('localhost') && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
+       baseUrl = baseUrl.replace('localhost', window.location.hostname)
+    }
+
+    // 去除末尾斜杠,保证拼接一致性
+    if (baseUrl.endsWith('/')) {
+      baseUrl = baseUrl.slice(0, -1)
+    }
+
+    // 执行替换
+    // 替换文档中的 {{API_BASE_URL}} 占位符
+    text = text.replace(/{{API_BASE_URL}}/g, baseUrl)
+    
+    // 兼容处理:替换可能遗漏的 http://localhost:8000/api/v1
+    text = text.replace(/http:\/\/localhost:8000\/api\/v1/g, baseUrl)
+
+    // 创建并下载 Blob
+    const blob = new Blob([text], { type: 'text/markdown' })
+    const url = window.URL.createObjectURL(blob)
+    
+    const link = document.createElement('a')
+    link.href = url
+    link.download = fileName
+    document.body.appendChild(link)
+    link.click()
+    
+    // 清理
+    document.body.removeChild(link)
+    window.URL.revokeObjectURL(url)
+    
+    ElMessage.success('文档下载成功,已更新 API 地址')
+  } catch (error) {
+    console.error('Download failed:', error)
+    ElMessage.error('文档下载失败,请检查网络或文件路径')
+  }
+}
 </script>
 
 <style scoped>
@@ -1059,13 +1149,36 @@ const activeTab = ref('custom-login')
   font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
 }
 
+.help-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #eaecef;
+  margin-bottom: 20px;
+  padding-bottom: 15px;
+}
+
+.content-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  margin-bottom: 20px;
+}
+
+.content-header h2 {
+  margin: 0;
+  border: none;
+  padding: 0;
+}
+
 h1 {
   font-size: 28px;
-  margin-bottom: 20px;
   color: #1f2f3d;
   font-weight: 600;
-  border-bottom: 1px solid #eaecef;
-  padding-bottom: 15px;
+  margin: 0;
+  padding: 0;
+  border-bottom: none;
 }
 
 h2 {