# 智能IT支持服务台 — 统一入口（Portal）技术设计文档

**版本**: v1.1  
**日期**: 2026-06-13  
**作者**: 宋献  
**状态**: 实施中

---

## 〇、测试环境说明

**⚠️ 重要约束**：
- 本地开发环境无法完成企微 OAuth2 认证
- 所有前端都通过企微认证，不支持独立登录页面
- **所有登录相关验证必须在生产服务器 `10.90.5.110` 上进行**

**测试流程**：
1. 将代码部署到生产服务器
2. 通过企微工作台访问应用
3. 完成 OAuth2 认证后验证功能

---

## 一、概述

### 1.1 背景

当前 智能IT支持服务台 存在三个独立入口：
- **用户端** `/itdesk/` — 员工提交工单、查看进度
- **坐席端** `/itagent/` — IT坐席处理会话、AI辅助
- **管理端** `/itadmin/` — 系统配置、数据分析

**问题**：
1. 各端认证方式不统一（用户端OAuth2、坐席端用户名+通讯录验证）
2. 公网可直接访问登录页面，存在暴力破解风险
3. 用户需要记住多个URL，体验不佳

### 1.2 方案目标

**统一入口架构**：
- 所有用户必须通过 **企微工作台 → 智能IT支持服务台应用** 进入
- 进入时自动检测账户关联的角色
- 提供卡片选择页面，让用户选择进入哪个端
- 无坐席/管理角色的用户直接进入用户端

**安全目标**：
- 消除公网可直接访问的登录页面
- 统一使用企微 OAuth2 认证
- 管理端仅限内网/VPN访问
- API端点保留独立认证通道（API Key + IP白名单）

---

## 二、系统架构

### 2.1 整体架构图

```
┌─────────────────────────────────────────────────────────────┐
│                    用户访问流程                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  企微工作台 → 智能IT支持服务台应用                                │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────┐                                        │
│  │  OAuth2 授权     │  ← 统一入口，唯一认证点                │
│  └────────┬────────┘                                        │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────┐                                        │
│  │  角色检测 + 路由  │  ← 查询数据库中的角色                 │
│  └────────┬────────┘                                        │
│           │                                                 │
│     ┌─────┴─────┬──────────┐                                │
│     ▼           ▼          ▼                                │
│  ┌──────┐  ┌──────┐  ┌──────┐                              │
│  │用户端 │  │坐席端│  │管理端│                              │
│  └──────┘  └──────┘  └──────┘                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 角色路由逻辑

```
OAuth2 授权完成
    │
    ▼
查询角色列表: GET /api/portal/roles
    │
    ├── 仅 user 角色 → 直接跳转 /itdesk/（不显示选择页）
    │
    ├── user + agent → 显示选择页（2张卡片）
    │
    ├── user + admin → 显示选择页（2张卡片）
    │
    └── user + agent + admin → 显示选择页（3张卡片）
```

### 2.3 URL 路径规划

| 端 | 路径 | 说明 |
|---|------|------|
| 统一入口 | `/itportal/` | 路由选择页 |
| 用户端 | `/itdesk/` | 员工提交工单、查看进度 |
| 坐席端 | `/itagent/` | IT坐席处理会话 |
| 管理端 | `/itadmin/` | 系统配置、数据分析 |
| API | `/api/` | 后端接口 |

---

## 三、数据库设计

### 3.1 角色表 (roles)

```sql
CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE,           -- 角色标识: user/agent/admin
    display_name VARCHAR(100) NOT NULL,          -- 显示名称: 用户/坐席/管理员
    description TEXT,                            -- 角色描述
    permissions JSONB DEFAULT '[]',              -- 权限列表（JSON数组）
    is_default BOOLEAN DEFAULT FALSE,            -- 是否默认角色（user=true）
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 预置角色
INSERT INTO roles (name, display_name, description, is_default) VALUES
('user', '用户', '所有在职员工默认角色，可提交工单、查看进度、浏览知识库', TRUE),
('agent', '坐席', 'IT支持人员，可处理会话、使用AI辅助、管理工单', FALSE),
('admin', '管理员', '系统管理员，可配置系统、管理权限、查看数据分析', FALSE);
```

### 3.2 用户角色关联表 (user_roles)

```sql
CREATE TABLE user_roles (
    id SERIAL PRIMARY KEY,
    employee_id VARCHAR(100) NOT NULL,           -- 企微 UserID
    role_id INTEGER NOT NULL REFERENCES roles(id),
    source VARCHAR(50) NOT NULL,                 -- 来源: auto/tag/ehr/manual
    assigned_by VARCHAR(100),                    -- 分配者（手动分配时记录）
    assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,                        -- 过期时间（可选）
    UNIQUE(employee_id, role_id)
);

-- 索引
CREATE INDEX idx_user_roles_employee ON user_roles(employee_id);
CREATE INDEX idx_user_roles_role ON user_roles(role_id);
```

### 3.3 角色映射规则表 (role_mapping_rules)

```sql
CREATE TABLE role_mapping_rules (
    id SERIAL PRIMARY KEY,
    role_id INTEGER NOT NULL REFERENCES roles(id),
    source_type VARCHAR(50) NOT NULL,            -- 来源类型: wecom_tag/ehr_position/manual
    source_value VARCHAR(200) NOT NULL,          -- 来源值: 标签名/岗位关键词
    priority INTEGER DEFAULT 0,                  -- 优先级（数值越大优先级越高）
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 预置映射规则
INSERT INTO role_mapping_rules (role_id, source_type, source_value, priority) VALUES
((SELECT id FROM roles WHERE name='agent'), 'wecom_tag', 'IT坐席', 10),
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', 'IT支持', 10),
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', 'IT运维', 10),
((SELECT id FROM roles WHERE name='agent'), 'ehr_position', '技术支持', 10);
```

### 3.4 Token 存储（Redis）

**统一 Token 格式**：

```
Key:   user:token:{token}
Value: {
    "employee_id": "zhangsan",
    "name": "张三",
    "department": "IT支持组",
    "avatar": "https://...",
    "roles": ["user", "agent"],
    "current_role": "agent",
    "login_source": "portal",
    "created_at": "2026-06-12T22:00:00",
    "last_active": "2026-06-12T22:30:00"
}
TTL: 8 小时（28800秒）
```

---

## 四、API 设计

### 4.1 Portal API

#### 4.1.1 获取当前用户角色

```
GET /api/portal/roles
Authorization: Bearer {token}

Response:
{
    "code": 0,
    "data": {
        "employee_id": "zhangsan",
        "name": "张三",
        "department": "IT支持组",
        "avatar": "https://...",
        "roles": [
            {
                "name": "user",
                "display_name": "用户",
                "description": "所有在职员工默认角色"
            },
            {
                "name": "agent",
                "display_name": "坐席",
                "description": "IT支持人员"
            }
        ],
        "current_role": "user"
    }
}
```

#### 4.1.2 切换当前角色

```
POST /api/portal/switch-role
Authorization: Bearer {token}
Content-Type: application/json

{
    "new_role": "agent"
}

Response:
{
    "code": 0,
    "data": {
        "current_role": "agent",
        "redirect_url": "/itagent/"
    }
}
```

#### 4.1.3 获取角色对应的入口 URL

```
GET /api/portal/entry/{role_name}
Authorization: Bearer {token}

Response:
{
    "code": 0,
    "data": {
        "role": "agent",
        "url": "/itagent/",
        "display_name": "坐席端"
    }
}
```

### 4.2 角色管理 API（管理端）

#### 4.2.1 获取所有角色

```
GET /api/admin/roles
Authorization: Bearer {token}
X-Forwarded-For: {内网IP}

Response:
{
    "code": 0,
    "data": [
        {
            "id": 1,
            "name": "user",
            "display_name": "用户",
            "is_default": true,
            "user_count": 1500
        },
        {
            "id": 2,
            "name": "agent",
            "display_name": "坐席",
            "is_default": false,
            "user_count": 15
        },
        {
            "id": 3,
            "name": "admin",
            "display_name": "管理员",
            "is_default": false,
            "user_count": 3
        }
    ]
}
```

#### 4.2.2 手动分配角色

```
POST /api/admin/roles/assign
Authorization: Bearer {token}
Content-Type: application/json

{
    "employee_id": "zhangsan",
    "role_name": "admin",
    "reason": "新任IT组长"
}

Response:
{
    "code": 0,
    "message": "角色分配成功"
}
```

#### 4.2.3 撤销角色

```
POST /api/admin/roles/revoke
Authorization: Bearer {token}
Content-Type: application/json

{
    "employee_id": "zhangsan",
    "role_name": "admin",
    "reason": "岗位调整"
}

Response:
{
    "code": 0,
    "message": "角色撤销成功"
}
```

#### 4.2.4 获取角色映射规则

```
GET /api/admin/roles/mapping-rules
Authorization: Bearer {token}

Response:
{
    "code": 0,
    "data": [
        {
            "id": 1,
            "role_name": "agent",
            "source_type": "wecom_tag",
            "source_value": "IT坐席",
            "priority": 10,
            "is_active": true
        }
    ]
}
```

#### 4.2.5 创建/更新映射规则

```
POST /api/admin/roles/mapping-rules
Authorization: Bearer {token}
Content-Type: application/json

{
    "role_name": "agent",
    "source_type": "wecom_tag",
    "source_value": "IT运维",
    "priority": 10,
    "is_active": true
}

Response:
{
    "code": 0,
    "message": "映射规则创建成功",
    "data": {
        "id": 4
    }
}
```

### 4.3 认证中间件改造

#### 4.3.1 统一认证依赖

```python
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    redis_client: Redis = Depends(dep_redis)
) -> UserInfo:
    """统一认证中间件：从 Redis 获取用户信息和角色"""
    data = await redis_client.get(f"user:token:{token}")
    if not data:
        raise AppException(1002, "Token 已过期或无效")
    
    user_data = json.loads(data)
    
    # 更新最后活跃时间
    user_data["last_active"] = datetime.now().isoformat()
    await redis_client.setex(
        f"user:token:{token}",
        TOKEN_TTL,
        json.dumps(user_data)
    )
    
    return UserInfo(
        employee_id=user_data["employee_id"],
        name=user_data["name"],
        department=user_data["department"],
        roles=user_data["roles"],
        current_role=user_data["current_role"]
    )
```

#### 4.3.2 角色验证装饰器

```python
def require_role(*required_roles: str):
    """角色验证装饰器：检查用户是否拥有指定角色之一"""
    def decorator(func):
        @wraps(func)
        async def wrapper(
            *args,
            current_user: UserInfo = Depends(get_current_user),
            **kwargs
        ):
            # 检查用户是否有任一所需角色
            user_roles = set(current_user.roles)
            required = set(required_roles)
            
            if not user_roles.intersection(required):
                raise AppException(
                    4003,
                    f"需要以下角色之一: {', '.join(required_roles)}"
                )
            
            return await func(*args, current_user=current_user, **kwargs)
        return wrapper
    return decorator

# 使用示例
@router.get("/api/agent/conversations")
@require_role("agent")
async def get_conversations(current_user: UserInfo = Depends(get_current_user)):
    # 只有 agent 角色才能访问
    pass

@router.get("/api/admin/dashboard")
@require_role("admin")
async def get_dashboard(current_user: UserInfo = Depends(get_current_user)):
    # 只有 admin 角色才能访问
    pass

@router.get("/api/h5/tickets")
@require_role("user", "agent", "admin")
async def get_tickets(current_user: UserInfo = Depends(get_current_user)):
    # 所有角色都可以访问
    pass
```

---

## 五、前端设计

### 5.1 Portal 前端应用

**项目结构**：

```
frontend-portal/
├── src/
│   ├── App.vue
│   ├── main.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   └── portal.ts          # Portal 状态管理
│   ├── api/
│   │   └── portal.ts          # Portal API 调用
│   ├── views/
│   │   ├── PortalSelect.vue   # 角色选择页
│   │   └── PortalLoading.vue  # 加载页
│   └── components/
│       └── RoleCard.vue       # 角色卡片组件
├── index.html
├── vite.config.ts
└── package.json
```

### 5.2 角色选择页 UI

**页面布局**：

```
┌─────────────────────────────────────────────────────────┐
│                                                         │
│                    智能IT支持服务台                          │
│                                                         │
│              选择您要进入的工作台                         │
│                                                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │             │  │             │  │             │     │
│  │   👤 用户   │  │   🎧 坐席   │  │   ⚙️ 管理   │     │
│  │             │  │             │  │             │     │
│  │  提交工单   │  │  处理会话   │  │  系统配置   │     │
│  │  查看进度   │  │  AI 辅助    │  │  数据分析   │     │
│  │  知识库     │  │  知识库     │  │  权限管理   │     │
│  │             │  │             │  │             │     │
│  │   [进入]    │  │   [进入]    │  │   [进入]    │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
│                                                         │
│  当前账号：张三 (IT支持组)                               │
│                                                         │
└─────────────────────────────────────────────────────────┘
```

**RoleCard 组件**：

```vue
<template>
  <div 
    class="role-card"
    :class="{ 'role-card--disabled': !available }"
    @click="handleClick"
  >
    <div class="role-card__icon">
      <el-icon :size="48">
        <component :is="icon" />
      </el-icon>
    </div>
    <div class="role-card__title">{{ role.display_name }}</div>
    <div class="role-card__desc">{{ role.description }}</div>
    <el-button 
      v-if="available"
      type="primary"
      class="role-card__btn"
    >
      进入
    </el-button>
    <el-tag v-else type="info">无权限</el-tag>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Role {
  name: string
  display_name: string
  description: string
}

const props = defineProps<{
  role: Role
  available: boolean
}>()

const emit = defineEmits<{
  (e: 'select', role: Role): void
}>()

const icon = computed(() => {
  switch (props.role.name) {
    case 'user': return 'User'
    case 'agent': return 'Headset'
    case 'admin': return 'Setting'
    default: return 'User'
  }
})

const handleClick = () => {
  if (props.available) {
    emit('select', props.role)
  }
}
</script>
```

### 5.3 坐席端改造

**认证流程改造**：

```typescript
// 坐席端路由守卫
router.beforeEach(async (to, from, next) => {
  const agentStore = useAgentStore()
  
  // 检查是否已认证
  if (!agentStore.token) {
    // 尝试从 URL 参数获取 token（从 Portal 跳转时传递）
    const token = to.query.token as string
    if (token) {
      agentStore.setToken(token)
      // 清除 URL 中的 token 参数
      next({ ...to, query: {} })
      return
    }
    
    // 未认证，跳转到 Portal
    window.location.href = '/itportal/'
    return
  }
  
  // 验证角色权限
  try {
    const userInfo = await agentStore.validateRole('agent')
    if (!userInfo.roles.includes('agent')) {
      // 无坐席角色，跳转到 Portal
      window.location.href = '/itportal/'
      return
    }
    next()
  } catch (error) {
    // Token 无效，跳转到 Portal
    window.location.href = '/itportal/'
  }
})
```

**角色切换入口**：

```vue
<!-- 坐席端右上角菜单 -->
<template>
  <el-dropdown @command="handleCommand">
    <div class="user-info">
      <el-avatar :src="userStore.avatar" :size="32" />
      <span class="user-name">{{ userStore.name }}</span>
      <el-icon><ArrowDown /></el-icon>
    </div>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="switch-user">
          <el-icon><User /></el-icon>
          切换到用户端
        </el-dropdown-item>
        <el-dropdown-item v-if="hasAdminRole" command="switch-admin">
          <el-icon><Setting /></el-icon>
          切换到管理端
        </el-dropdown-item>
        <el-dropdown-item divided command="logout">
          <el-icon><SwitchButton /></el-icon>
          退出登录
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = useRouter()
const userStore = useUserStore()

const hasAdminRole = computed(() => {
  return userStore.roles.includes('admin')
})

const handleCommand = async (command: string) => {
  switch (command) {
    case 'switch-user':
      await switchRole('user')
      break
    case 'switch-admin':
      await switchRole('admin')
      break
    case 'logout':
      await logout()
      break
  }
}

const switchRole = async (role: string) => {
  try {
    const { redirect_url } = await api.switchRole(role)
    window.location.href = redirect_url
  } catch (error) {
    ElMessage.error('切换失败')
  }
}

const logout = async () => {
  userStore.clearAuth()
  window.location.href = '/itportal/'
}
</script>
```

---

## 六、安全设计

### 6.1 认证安全

| 安全措施 | 说明 | 状态 |
|----------|------|------|
| OAuth2 静默授权 | scope=snsapi_base，用户无感知 | ✅ 已实现 |
| state 参数防 CSRF | 随机 state，回调时验证 | ✅ 已实现 |
| Token 密码学安全 | secrets.token_urlsafe(32) | ✅ 已实现 |
| Token TTL 8小时 | Redis 自动过期 | ✅ 已实现 |
| redirect_uri 白名单 | 生产环境仅允许正式域名 | ⚠️ 需收紧 |
| 企微 UA 检测 | 非企微 WebView 拒绝访问 | ✅ 已实现 |

### 6.2 角色安全

| 安全措施 | 说明 | 状态 |
|----------|------|------|
| 角色最小权限 | 默认仅 user 角色 | ✅ 设计完成 |
| 角色来源追溯 | user_roles 表记录 source 和 assigned_by | ✅ 设计完成 |
| 管理端 IP 白名单 | 仅内网/VPN 可访问 | ⚠️ 待配置 |
| 管理端紧急通道 | 管理员密码 + 二次验证 | ⚠️ 待设计 |

### 6.3 API 安全

| 安全措施 | 说明 | 状态 |
|----------|------|------|
| API Key 认证 | 外部系统独立认证通道 | ⚠️ 待实现 |
| API IP 白名单 | 外部系统限制来源 IP | ⚠️ 待实现 |
| 速率限制 | slowapi 中间件 | ✅ 已实现 |

### 6.4 管理端访问控制

**Nginx 配置**：

```nginx
# 管理端：仅限内网/VPN 访问
location /itadmin/ {
    # 允许内网网段
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    allow 192.168.0.0/16;
    # 允许 VPN 网段
    allow 10.212.0.0/16;
    # 拒绝其他来源
    deny all;
    
    # 反向指向前端应用
    try_files $uri $uri/ /itadmin/index.html;
}

# 管理端 API：同样限制访问来源
location /api/admin/ {
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    allow 192.168.0.0/16;
    allow 10.212.0.0/16;
    deny all;
    
    proxy_pass http://backend;
}
```

---

## 七、角色映射机制

### 7.1 映射流程

```
用户 OAuth2 登录
    │
    ▼
获取企微 access_token
    │
    ▼
查询企微通讯录 → 获取标签、部门、岗位
    │
    ▼
查询 role_mapping_rules 表
    │
    ├─ 标签匹配 → agent
    ├─ 岗位匹配 → agent
    ├─ 数据库 user_roles 表有 admin 记录 → admin
    └─ 默认 → user
    │
    ▼
合并角色列表 → 存入 Redis Token
```

### 7.2 企微标签配置

**标签组**：`IT服务台角色`

**标签值**：
- `IT坐席` — 映射为 agent 角色
- `IT管理员` — 映射为 admin 角色（备用，实际以手动分配为准）

**配置步骤**：
1. 登录企微管理后台
2. 通讯录 → 标签 → 新建标签组
3. 添加标签值
4. 为坐席人员打上 `IT坐席` 标签

### 7.3 eHR 字段映射（后续阶段）

**映射规则**：
- 岗位包含 `IT支持`、`IT运维`、`技术支持` → agent

**实现方式**：
- 对接北森 eHR OAuth2 API
- 定时同步员工岗位信息
- 根据岗位关键词自动映射角色

---

## 八、实施计划

### 阶段一：角色系统 + 统一 Token（1.5 周）

| 序号 | 任务 | 说明 | 工时 |
|------|------|------|------|
| 1 | 创建角色表 | roles + user_roles + role_mapping_rules | 2h |
| 2 | 实现角色 CRUD API | 管理后台管理角色分配 | 4h |
| 3 | 实现角色映射服务 | 企微标签 → 角色 | 4h |
| 4 | 改造 Token 存储 | 统一为 user:token:{token} | 4h |
| 5 | 改造认证中间件 | 统一 get_current_user，支持角色验证 | 4h |
| 6 | 坐席端认证改造 | 使用统一的 get_current_user | 4h |
| 7 | 单元测试 | 角色系统测试 | 4h |

**小计**：约 26 小时

### 阶段二：路由选择页（1 周）

| 序号 | 任务 | 说明 | 工时 |
|------|------|------|------|
| 8 | 创建 Portal Vue 应用 | 前端项目初始化 | 2h |
| 9 | 实现角色选择 UI | 卡片选择组件 | 4h |
| 10 | 改造 OAuth2 回调 | 授权后跳转到 Portal | 4h |
| 11 | 实现角色切换 API | POST /api/portal/switch-role | 2h |
| 12 | 坐席端切换入口 | 右上角菜单切换角色 | 2h |
| 13 | 前端测试 | 端到端测试 | 4h |

**小计**：约 18 小时

### 阶段三：管理端访问控制（0.5 周）

| 序号 | 任务 | 说明 | 工时 |
|------|------|------|------|
| 14 | Nginx IP 白名单配置 | 限制 /itadmin/ 访问来源 | 2h |
| 15 | 管理端 OAuth2 接入 | 主通道：企微授权 | 4h |
| 16 | 管理员角色分配 | 初始管理员配置 | 1h |
| 17 | 测试验证 | 内网/外网访问测试 | 2h |

**小计**：约 9 小时

### 阶段四：安全加固 + 测试（1 周）

| 序号 | 任务 | 说明 | 工时 |
|------|------|------|------|
| 18 | 收紧 redirect_uri 白名单 | 生产环境仅允许正式域名 | 1h |
| 19 | 移除所有降级逻辑 | 生产环境禁用 Mock 登录等 | 2h |
| 20 | 端到端测试 | 完整流程测试 | 4h |
| 21 | 安全审计 | 第三方安全测试 | 4h |
| 22 | 文档更新 | 更新部署文档、用户手册 | 2h |

**小计**：约 13 小时

---

**总工时**：约 66 小时（约 2.5 周全职工作）

---

## 九、风险与缓解措施

| 风险 | 等级 | 缓解措施 |
|------|------|----------|
| 企微 OAuth2 故障 | 高 | 管理端保留紧急通道 |
| 企微标签配置错误 | 中 | 角色映射有日志，可追溯 |
| Token 切换时的竞态条件 | 低 | Redis 事务保证原子性 |
| 内网 IP 白名单误配 | 中 | 配置前在测试环境验证 |
| 现有坐席登录方式变更 | 中 | 变更前手动通知坐席 |

---

## 十、附录

### 10.1 术语表

| 术语 | 说明 |
|------|------|
| Portal | 统一入口，路由选择页 |
| OAuth2 | 开放授权协议，用于企微身份验证 |
| Bearer Token | 用于 API 认证的令牌 |
| RBAC | 基于角色的访问控制 |
| eHR | 人力资源管理系统（北森） |

### 10.2 参考文档

- [企业微信 OAuth2 开发文档](https://developer.work.weixin.qq.com/document/path/91022)
- [企业微信 通讯录管理](https://developer.work.weixin.qq.com/document/path/90193)
- [FastAPI 安全最佳实践](https://fastapi.tiangolo.com/tutorial/security/)
