06 — 云融(YunRong)· 接口设计文档 (API)
前置文档:03-通信协议详细设计文档、04-数据库设计文档、05-模块详细设计文档
设计思路:00-设计思路文档
版本:v0.1
状态:已发布
最后更新:2025-01
1. 接口体系总览
1.1 双通道架构
┌─────────────────────────────────────────────────────┐
│ 客户端 │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ REST API │ │ WebSocket API │ │
│ │ (HTTPS) │ │ (WSS) │ │
│ │ │ │ │ │
│ │ • 认证/登录 │ │ • 实时消息收发 │ │
│ │ • 文件上传/下载 │ │ • 通知推送 │ │
│ │ • 联系人/组织架构 │ │ • 心跳/状态同步 │ │
│ │ • 任务/审批 │ │ • 增量同步 │ │
│ │ • 数据统计 │ │ │ │
│ └────────┬─────────┘ └───────────┬────────────┘ │
│ │ │ │
└───────────┼──────────────────────────┼───────────────┘
│ │
┌─────┴──────┐ ┌──────┴──────┐
│ API Server │ │ WS Server │
│ (REST) │ │ (WebSocket) │
└─────┬──────┘ └──────┬──────┘
│ │
┌─────┴──────────────────────────┴──────┐
│ PostgreSQL │
└───────────────────────────────────────┘
1.2 使用场景分工
| 场景 | 通道 | 理由 |
|---|
| 登录/注册 | REST | 一次性操作,无需长连接 |
| 获取联系人列表 | REST | 请求-响应模式,适合 HTTP |
| 发送消息 | WebSocket | 实时性要求,双向推送 |
| 接收消息 | WebSocket | 服务端主动推送,HTTP 轮询太浪费 |
| 文件上传 | REST (multipart) | 大块数据传输,HTTP Range 更成熟 |
| 文件下载 | REST (Range) | 断点续传依赖 HTTP Range |
| 搜索消息 | REST | 计算密集型,服务端处理 |
| 心跳 | WebSocket Ping/Pong | 保活长连接 |
| 增量同步 | WebSocket | 和消息推送共用通道 |
1.3 基础约定
| 约定 | 说明 |
|---|
| Base URL | https://{server}/api/v1 |
| 内容类型 | 请求/响应均为 application/json(文件上传除外) |
| 字符编码 | UTF-8 |
| 日期时间 | ISO 8601 格式:2025-01-15T10:30:00.000Z 或 Unix 毫秒时间戳 |
| 分页 | 游标分页(见 §2) |
| 认证 | HTTP Header: Authorization: Bearer <access_token> |
| 版本 | URL 路径前缀 /v1/ |
2. 通用约定
2.1 请求格式
METHOD /api/v1/resource HTTP/1.1
Host: server.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
Accept: application/json
2.2 成功响应格式
{
"code": 0,
"message": "success",
"data": { ... },
"meta": {
"timestamp": 1704067200000,
"request_id": "req_uuid_v4"
}
}
| 字段 | 类型 | 说明 |
|---|
code | int | 0 表示成功 |
message | string | 人类可读的响应消息 |
data | object/array/null | 响应数据 |
meta.timestamp | uint64 | 服务端时间戳 |
meta.request_id | string | 请求追踪 ID(用于日志关联) |
2.3 错误响应格式
{
"code": 401002,
"message": "Token 已过期,请重新登录",
"data": null,
"meta": {
"timestamp": 1704067200000,
"request_id": "req_uuid_v4"
}
}
2.4 游标分页
// 请求
GET /api/v1/messages?conv_id=2001&before=1704067200000&limit=50
// 响应
{
"code": 0,
"message": "success",
"data": {
"items": [ ... ],
"pagination": {
"has_more": true,
"next_cursor": "1704060000000",
"limit": 50
}
}
}
| 参数 | 说明 |
|---|
before | 游标:返回此时间戳之前的记录(首次请求可不传,返回最新) |
after | 游标:返回此时间戳之后的记录(向上翻页用) |
limit | 每页条数(默认 50,最大 200) |
next_cursor | 下一页的游标值(即本页最后一条的时间戳) |
3. REST API 端点清单
3.1 认证模块 /api/v1/auth
3.1.1 登录
POST /api/v1/auth/login
Content-Type: application/json
Request:
{
"username": "zhangsan",
"password": "hashed_password",
"device_id": "uuid-device-001",
"platform": "windows",
"client_version": "0.1.0"
}
Response 200:
{
"code": 0,
"data": {
"user_id": 1001,
"display_name": "张三",
"avatar_url": "https://server/avatars/1001.webp",
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"expires_in": 7200,
"ws_url": "wss://server/ws?token=eyJhbGciOi..."
}
}
Errors:
401001 — 用户名或密码错误
401002 — 账号已被禁用
401003 — 设备未授权
3.1.2 Token 刷新
POST /api/v1/auth/refresh
Authorization: Bearer <expired_access_token>
Request:
{
"refresh_token": "eyJhbGciOi..."
}
Response 200:
{
"code": 0,
"data": {
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"expires_in": 7200
}
}
Errors:
401010 — Refresh Token 已过期
401011 — Refresh Token 无效
3.1.3 登出
POST /api/v1/auth/logout
Authorization: Bearer <access_token>
Request: {}
Response 200:
{
"code": 0,
"data": { "logged_out": true }
}
3.2.1 获取联系人列表
GET /api/v1/contacts?department_id=0&limit=500
Response 200:
{
"code": 0,
"data": {
"items": [
{
"id": 1002,
"name": "李四",
"avatar_url": "https://server/avatars/1002.webp",
"department_id": 10,
"department_name": "研发部",
"title": "高级工程师",
"email": "lisi@example.com",
"phone": "13800138000",
"status": 1,
"pinyin": "lisi"
}
],
"total": 2000
}
}
3.2.2 搜索联系人
GET /api/v1/contacts/search?q=李&limit=20
Response 200:
{
"code": 0,
"data": {
"items": [ ... ]
}
}
3.2.3 获取组织架构树
GET /api/v1/departments?parent_id=0&depth=2
Response 200:
{
"code": 0,
"data": {
"items": [
{
"id": 1,
"name": "总公司",
"parent_id": 0,
"member_count": 1500,
"children": [
{
"id": 10,
"name": "研发部",
"parent_id": 1,
"member_count": 80
}
]
}
]
}
}
3.3 会话模块 /api/v1/conversations
3.3.1 获取会话列表
GET /api/v1/conversations?limit=50
Response 200:
{
"code": 0,
"data": {
"items": [
{
"id": 2001,
"type": 0,
"title": "李四",
"avatar_url": "https://server/avatars/1002.webp",
"last_message": {
"preview": "好的,明天开会讨论",
"timestamp": 1704067200000,
"sender_id": 1002
},
"unread_count": 3,
"is_pinned": false,
"is_muted": false,
"members": [1001, 1002]
}
]
}
}
3.3.2 创建会话
POST /api/v1/conversations
Content-Type: application/json
Request:
{
"type": 1,
"title": "项目讨论组",
"member_ids": [1002, 1003, 1004]
}
Response 201:
{
"code": 0,
"data": {
"id": 2005,
"type": 1,
"title": "项目讨论组",
"created_at": 1704067200000
}
}
3.3.3 获取消息历史
GET /api/v1/conversations/2001/messages?before=1704067200000&limit=50
Response 200:
{
"code": 0,
"data": {
"items": [
{
"msg_id": "msg_abc123",
"sender_id": 1002,
"sender_name": "李四",
"content_type": 0,
"content_body": { "type": "text", "text": "你好" },
"quote_msg_id": null,
"timestamp": 1704067200000
}
],
"pagination": {
"has_more": true,
"next_cursor": "1704060000000"
}
}
}
3.3.4 搜索消息
GET /api/v1/conversations/2001/messages/search?q=报告&limit=20
// 或全局搜索
GET /api/v1/messages/search?q=报告&conv_id=2001&limit=20
3.4 任务模块 /api/v1/tasks
3.4.1 获取任务列表
GET /api/v1/tasks?status=0&priority=2&limit=50
Query 参数:
status — 0=待处理, 1=已读, 2=已处理, 3=已忽略
priority — 0=低, 1=普通, 2=高
type — "approval" | "assignment" | "announcement"
Response 200:
{
"code": 0,
"data": {
"items": [
{
"id": 5001,
"notify_id": "n_12345",
"task_type": "approval",
"title": "请假审批",
"body": "张三申请年假 3 天 (2025-01-20 ~ 2025-01-22)",
"priority": 1,
"status": 0,
"from_user_id": 1001,
"from_user_name": "张三",
"action_url": "/tasks/5001/detail",
"created_at": 1704067200000
}
],
"pagination": { "has_more": false }
}
}
3.4.2 处理任务
POST /api/v1/tasks/5001/handle
Content-Type: application/json
Request:
{
"action": "approve",
"comment": "同意,注意交接工作"
}
// action: "approve" | "reject" | "complete" | "ignore"
Response 200:
{
"code": 0,
"data": {
"id": 5001,
"new_status": 2,
"handled_at": 1704070000000
}
}
3.5 文件模块 /api/v1/files
3.5.1 检查文件是否存在(秒传)
POST /api/v1/files/check
Content-Type: application/json
Request:
{
"hash": "sha256:abc123def456...",
"file_name": "报告.pdf",
"file_size": 1048576
}
Response 200 (已存在 → 可秒传):
{
"code": 0,
"data": {
"exists": true,
"url": "https://server/files/doc_042.pdf"
}
}
Response 200 (不存在 → 需要上传):
{
"code": 0,
"data": {
"exists": false,
"upload_id": "upload_uuid_xxx",
"chunk_size": 1048576,
"total_chunks": 1
}
}
3.5.2 上传文件块
POST /api/v1/files/upload/{upload_id}/chunks/{chunk_index}
Content-Type: application/octet-stream
X-Chunk-Offset: 0
X-Chunk-Size: 1048576
X-Total-Chunks: 1
X-File-Hash: sha256:abc123def456...
Body: <binary data>
Response 200:
{
"code": 0,
"data": {
"chunk_index": 0,
"received": true
}
}
3.5.3 完成上传
POST /api/v1/files/upload/{upload_id}/complete
Content-Type: application/json
Request:
{
"file_hash": "sha256:abc123def456..."
}
Response 200:
{
"code": 0,
"data": {
"url": "https://server/files/doc_042.pdf",
"hash_verified": true,
"file_size": 1048576
}
}
Errors:
9001 — 文件块不完整
9003 — Hash 校验不匹配
3.5.4 下载文件(带 Range)
GET /api/v1/files/download?url=https://server/files/doc_042.pdf
Range: bytes=0-1048575
Response 206 Partial Content:
Content-Range: bytes 0-1048575/1048576
Content-Type: application/octet-stream
Body: <binary data>
3.6 统计报表模块 /api/v1/reports
3.6.1 消息统计
GET /api/v1/reports/message_stats?from=1704067200000&to=1704153600000&granularity=day
Query 参数:
from — 起始时间戳
to — 结束时间戳
granularity — "hour" | "day" | "week" | "month"
Response 200:
{
"code": 0,
"data": {
"total_sent": 1523,
"total_received": 2105,
"by_granularity": [
{ "timestamp": 1704067200000, "sent": 120, "received": 180 },
{ "timestamp": 1704153600000, "sent": 95, "received": 140 }
],
"by_conversation": [
{ "conv_id": 2001, "conv_name": "李四", "count": 300 },
{ "conv_id": 2003, "conv_name": "项目讨论组", "count": 850 }
]
}
}
3.6.2 文件传输统计
GET /api/v1/reports/file_stats?from=1704067200000&to=1704153600000
Response 200:
{
"code": 0,
"data": {
"total_uploads": 45,
"total_downloads": 23,
"total_bytes_up": 524288000,
"total_bytes_down": 104857600
}
}
3.6.3 导出报表
POST /api/v1/reports/export
Content-Type: application/json
Request:
{
"type": "message_stats",
"format": "csv",
"from": 1704067200000,
"to": 1704153600000
}
Response 200:
Content-Type: text/csv
Content-Disposition: attachment; filename="message_report_202501.csv"
Body: <CSV data>
支持的导出格式:csv、json、pdf(服务端生成)。
3.7 用户配置模块 /api/v1/preferences
3.7.1 获取配置
GET /api/v1/preferences
Response 200:
{
"code": 0,
"data": {
"ui.theme": "dark",
"ui.font_size": "14",
"notify.sound": true,
"notify.desktop": true,
"file.download_dir": "/home/user/Downloads",
"file.upload_limit_kbps": 0
}
}
3.7.2 更新配置
PUT /api/v1/preferences
Content-Type: application/json
Request:
{
"ui.theme": "light",
"notify.sound": false
}
Response 200:
{
"code": 0,
"data": {
"updated": ["ui.theme", "notify.sound"]
}
}
4. 错误码完整目录
4.1 错误码结构
AABCCC
││└── 具体错误 (3 位)
│└── 子模块 (1 位)
└── 大模块 (2 位)
01 = 认证
02 = 联系人/组织
03 = 会话/消息
04 = 任务
05 = 文件
09 = 系统
4.2 完整清单
| 错误码 | HTTP 状态码 | 含义 |
|---|
011001 | 401 | 用户名或密码错误 |
011002 | 403 | 账号已被禁用 |
011003 | 403 | 设备未授权 |
011010 | 401 | Refresh Token 已过期 |
011011 | 401 | Refresh Token 无效 |
012001 | 401 | Access Token 过期(需刷新) |
012002 | 401 | Access Token 无效/格式错误 |
021001 | 404 | 联系人不存在 |
022001 | 404 | 部门不存在 |
031001 | 404 | 会话不存在 |
031002 | 403 | 无权限访问该会话 |
032001 | 404 | 消息不存在 |
032002 | 400 | 消息体超过大小限制(10KB) |
041001 | 404 | 任务不存在 |
041002 | 403 | 无权处理该任务 |
041003 | 400 | 任务已被处理,不可重复操作 |
051001 | 404 | 上传任务不存在 |
051002 | 400 | 分块序号越界 |
052001 | 400 | 文件 Hash 校验失败 |
052002 | 400 | 文件大小超出限制(单文件 2GB) |
091001 | 400 | 请求参数缺失或格式错误 |
091002 | 429 | 请求频率超限 |
091003 | 500 | 服务器内部错误 |
091004 | 503 | 服务暂时不可用 |
5. 认证流程
5.1 完整认证时序
Client REST Server WS Server
│ │ │
│ 1. POST /auth/login │ │
│─────────────────────────────►│ │
│ │ 验证凭据 │
│◄── access_token + │ │
│ refresh_token + ws_url ──│ │
│ │ │
│ 2. 存储 token (加密) │ │
│ │ │
│ 3. WSS connect(ws_url) │ │
│──────────────────────────────────────────────────►│
│ │ │
│ 4. WebSocket auth 帧 │ │
│ { type:"auth", token } ────────────────────────►│
│◄── { type:"auth_ok" } ──────────────────────────│
│ │ │
│ ........ 2 小时后 ........ │ │
│ │ │
│ 5. Token 即将过期(提前 5min) │ │
│ POST /auth/refresh │ │
│─────────────────────────────►│ │
│◄── 新的 token pair ─────────│ │
│ │ │
│ 6. 更新本地 token │ │
│ (无缝切换,用户无感知) │ │
5.2 Token 安全策略
| 策略 | 说明 |
|---|
| Access Token | 短期(2h),JWT,Bearer Header 传输 |
| Refresh Token | 长期(7d),不透明字符串,仅在 /auth/refresh 中使用 |
| Token 存储 | 加密存储于系统凭据管理器,不存 SQLite |
| 撤回机制 | 服务端维护黑名单,Refresh Token 被撤回后强制重新登录 |
| 多端支持 | 每个 device_id 拥有独立的 token pair,互不影响 |
6. Mock Server 设计
6.1 架构
┌─────────────────────────────────────────────────────┐
│ Mock Server (Qt Process) │
│ │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ HTTP Server │ │ WebSocket Server │ │
│ │ (QTcpServer) │ │ (QWebSocketServer) │ │
│ │ port: 18002 │ │ port: 18001 │ │
│ └─────────┬──────────┘ └───────────┬────────────┘ │
│ │ │ │
│ ┌─────────┴─────────────────────────┴───────────┐ │
│ │ Route Dispatcher │ │
│ │ /auth/login → AuthHandler │ │
│ │ /contacts → ContactHandler │ │
│ │ /conversations → ConversationHandler │ │
│ │ /files/* → FileHandler │ │
│ │ /ws → WsHandler │ │
│ └──────────────────────┬────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴────────────────────────┐ │
│ │ Scenario Engine │ │
│ │ • 场景配置加载 (JSON) │ │
│ │ • 延迟模拟 (delay_ms) │ │
│ │ • 丢包模拟 (drop_rate) │ │
│ │ • 错误注入 (error_code) │ │
│ │ • 预设数据集 (contacts, messages, tasks) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ In-Memory Store │ │
│ │ • 用户表 / 会话表 / 消息表 / 任务表 / 文件表 │ │
│ │ • 所有数据存内存,重启清空 │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
6.2 场景配置
{
"scenario": "normal_full",
"description": "正常使用完整场景",
"config": {
"network": {
"base_delay_ms": 30,
"jitter_ms": 15,
"drop_rate": 0.0
},
"auth": {
"valid_tokens": ["mock_token_admin", "mock_token_user1", "mock_token_user2"],
"token_expiry_sec": 7200
}
},
"dataset": {
"users": [
{ "id": 1001, "name": "当前用户", "username": "me", "password": "123456" },
{ "id": 1002, "name": "李四", "username": "lisi" },
{ "id": 1003, "name": "王五", "username": "wangwu" }
],
"departments": [
{ "id": 1, "name": "总公司", "parent_id": 0 },
{ "id": 10, "name": "研发部", "parent_id": 1 },
{ "id": 11, "name": "产品部", "parent_id": 1 }
],
"conversations": [
{ "id": 2001, "type": 0, "members": [1001, 1002] },
{ "id": 2003, "type": 1, "title": "项目组", "members": [1001, 1002, 1003] }
],
"messages": [
{ "msg_id": "m001", "conv_id": 2001, "sender_id": 1002, "content_type": 0,
"content_body": { "type": "text", "text": "你好,明天的会议几点?" },
"timestamp": 1704067000000 }
]
}
}
6.3 Mock Server 处理示例
// mock/mock_server.cpp 核心路由
void MockServer::setupRoutes() {
// ---- 认证 ----
httpServer_.route("POST", "/api/v1/auth/login", [this](const Request& req) -> Response {
auto body = json::parse(req.body);
std::string username = body["username"];
std::string password = body["password"];
// 从 dataset 查找用户
auto user = store_.findUser(username, password);
if (!user) {
return errorResponse(401, 11001, "用户名或密码错误");
}
return successResponse({
{"user_id", user->id},
{"display_name", user->name},
{"access_token", "mock_token_" + username},
{"refresh_token", "mock_refresh_" + username},
{"expires_in", 7200},
{"ws_url", "ws://localhost:18001/ws?token=mock_token_" + username}
});
});
// ---- 联系人 ----
httpServer_.route("GET", "/api/v1/contacts", [this](const Request& req) -> Response {
if (!checkAuth(req)) return errorResponse(401, 12001, "Token 过期");
auto contacts = store_.getAllContacts();
return successResponse({
{"items", contacts},
{"total", contacts.size()}
});
});
// ---- 消息历史 (游标分页) ----
httpServer_.route("GET", "/api/v1/conversations/:id/messages",
[this](const Request& req) -> Response {
if (!checkAuth(req)) return errorResponse(401, 12001, "Token 过期");
int64_t convId = std::stoll(req.param("id"));
uint64_t before = req.query("before", UINT64_MAX);
int limit = std::min(req.query("limit", 50), 200);
auto [msgs, hasMore] = store_.getMessages(convId, before, limit);
uint64_t nextCursor = msgs.empty() ? 0 : msgs.back()["timestamp"];
return successResponse({
{"items", msgs},
{"pagination", {
{"has_more", hasMore},
{"next_cursor", std::to_string(nextCursor)},
{"limit", limit}
}}
});
});
}
6.4 错误注入场景
{
"scenario": "error_injection",
"description": "模拟各种错误场景",
"config": {
"network": {
"base_delay_ms": 30
},
"injections": [
{
"path": "/api/v1/auth/login",
"method": "POST",
"condition": "body.password == 'wrong'",
"response": { "code": 11001, "message": "用户名或密码错误" }
},
{
"path": "/api/v1/files/upload/*",
"condition": "chunk_index == 3",
"action": "drop",
"description": "模拟第 3 块上传丢包,测试断点续传"
},
{
"path": "/api/v1/auth/refresh",
"condition": "true",
"response": { "code": 11010, "message": "Refresh Token 已过期" },
"description": "模拟 Token 完全过期,测试重新登录流程"
},
{
"ws": true,
"condition": "after_connected_sec == 15",
"action": "close_connection",
"description": "连接 15 秒后强制断开,测试重连逻辑"
}
]
}
}
6.5 命令行接口
# 启动标准场景
./mock_server --scenario scenarios/normal_full.json
# 启动错误注入场景
./mock_server --scenario scenarios/error_injection.json -v
# 自定义端口
./mock_server --scenario scenarios/normal_full.json --http-port 9002 --ws-port 9001
# 热加载场景(运行时切换)
curl -X POST http://localhost:18002/__admin/scenario/load \
-H "Content-Type: application/json" \
-d '{"scenario": "error_injection"}'
管理接口(/__admin/ 前缀):
POST /__admin/scenario/load — 热加载场景
GET /__admin/scenario/current — 查看当前场景
POST /__admin/network/delay?ms=500 — 动态调整延迟
POST /__admin/network/drop?rate=0.1 — 动态调整丢包率
GET /__admin/stats — 查看请求统计
7. 客户端 HTTP Client 实现要点
// core/net/http_client.h
// HttpClient 的接口设计(已在 05 文档中定义,此处补充实现关键点)
class HttpClient : public IConnection {
public:
// 通用 GET 请求
void get(const std::string& path,
const std::map<std::string, std::string>& query,
std::function<void(int httpCode, const json& body)> callback);
// 通用 POST 请求
void post(const std::string& path,
const json& body,
std::function<void(int httpCode, const json& body)> callback);
// 文件上传(分块)
void uploadChunk(const std::string& uploadId, int chunkIndex,
const std::vector<uint8_t>& data, int totalChunks,
std::function<void(bool)> callback);
// 文件下载(Range)
void downloadRange(const std::string& url, uint64_t from, uint64_t to,
std::function<void(std::vector<uint8_t>)> callback);
private:
QNetworkAccessManager* qnam_; // Qt Network
std::string baseUrl_;
std::string authToken_;
// 自动附加认证 Header
QNetworkRequest buildRequest(const std::string& path);
};
// 认证 Header 注入
QNetworkRequest HttpClient::buildRequest(const std::string& path) {
QUrl url(QString::fromStdString(baseUrl_ + path));
QNetworkRequest req(url);
req.setRawHeader("Authorization",
("Bearer " + authToken_).c_str());
req.setRawHeader("Accept", "application/json");
req.setRawHeader("Content-Type", "application/json");
return req;
}
附录 A — 接口与协议文档的分工
| 文档 | 覆盖范围 |
|---|
| 03-通信协议 | WebSocket 帧格式(JSON 帧 + 二进制帧)、ACK/重传、心跳、TLS |
| 06-API 设计(本文档) | REST API 端点、Schema、错误码、认证流程、Mock Server |
两篇文档共同构成完整的通信契约。
附录 B — HTTP 状态码使用
| 状态码 | 使用场景 |
|---|
| 200 | 成功 |
| 201 | 创建成功(如创建会话) |
| 206 | 部分内容(文件 Range 下载) |
| 400 | 请求参数错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 409 | 冲突(如任务已被处理) |
| 413 | 请求体过大 |
| 429 | 请求频率超限 |
| 500 | 服务端内部错误 |
附录 C — 文档修订记录
| 版本 | 日期 | 作者 | 变更说明 |
|---|
| v0.1 | 2025-01 | — | 初稿:7 个 API 模块,20+ 端点,完整错误码表,Mock Server 设计 |