Files
wecom_it_smart_desk/docs/system_design.md

24 KiB
Raw Permalink Blame History

系统架构设计 + 任务分解 — 三端认证重构(增量)

文档版本:v1.0(架构师交付稿) 架构师:高见远 (software-architect) 依据:增量 PRD docs/02-产品需求/04-增量PRD-三端认证重构.md + 已锁定决策(决策1–7) 原则:基于现有代码的最小变更增量重构,不引入新框架


Part A:系统设计

1. 实现方案 + 框架选型

1.1 技术栈(沿用,不新增框架)

  • 后端FastAPI + SQLAlchemy(async) + Redis + Pydantic。认证分层沿用 api(路由) / services(逻辑) / models(数据) 结构。
  • 前端:三端独立 Vue3 + Vite SPAfrontend-h5 / frontend-agent / frontend-admin),各自 Axios 实例 + 响应拦截器。
  • OTP 算法pyotp已安装backend/venv,版本 2.10.0)、qrcode 已存在。无需新增任何依赖,直接复用 backend/app/services/mfa_service.pyMFAService 封装(TOTP secret 生成 / 校验 / 二维码 / Redis 标记)。

1.2 本次增量改动点(对照 PRD

决策 改动点 类型
1 三端独立入口 移除 /itportal//api/portal/*;清理 portal_token 引用 删除
2 员工端唯一认证 H5 仅 snsapi_base 静默授权 → 直进;Token 经 ?token= 镜像 h5_token;非 wxwork UA 跳拦截页(仅生产)
3 坐席/管理两方式并列 移除「企微已登录+角色→免密直接进入」分支;保留 ①扫码 ②账密+OTP 删/改
4 OTP 输入框延迟渲染 账密验证通过后才渲染 OTP(前端修正,原型已对齐)
5 OTP 接口统一 新增 /api/auth/otp-*,作废 /api/mfa/*/api/agents/otp-* 新增/删
6 管理端 IP 白名单 新增后端中间件,仅生产启用,非白名单 403 新增
7 响应契约方案A 三端拦截器统一:成功返回内层 data,失败抛 {code,message}
8 env-gating UA 校验 / IP 白名单 / 真实企微 OAuth 仅生产启用;本地走 dev/mock 新增

1.3 关键设计决策(基于代码核实)

  • OTP 逻辑复用:既有 backend/app/api/mfa.pyMFAService(干净);而 backend/app/api/agents.py/agents/otp-*内联 pyotp 重复实现。新端点统一落到 新增 backend/app/api/otp.py(前缀 /auth),复用 MFAServiceagents.mfa_* 字段。Redis 复用标记 key 维持 mfa:verified:{employee_id}TTL 1800s),dependencies.require_high_risk_otp 无需改动
  • 员工 ?token= 镜像已具备frontend-h5/src/router/index.ts 已读取 URL ?token= → 写 localStorage.h5_token(Bug#4 修复)。本次仅把后端 OAuth 回调从「code→前端 POST 取 token」改为「code→后端 302 /itdesk/?token=XXX」,复用既有镜像逻辑,更贴合决策2。
  • 管理端 IP 白名单:以 FastAPI 中间件实现,仅对 /api/admin/*/api/auth/otp-admin-* 生效(生产环境 APP_ENV 判定),非白名单返回 {code:4004, message:"无访问权限"},前端据此展示无权限拦截页。
  • 免密分支移除agents.py agent_loginwecom_verified + role → skip_otp=True 整段删除;前端 Login.vue 移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测自动跳转」分支。

2. 文件列表(标注【新增】/【修改】/【删除】)

2.1 后端

路径 操作 说明
backend/app/config.py 【修改】 新增 app_env(默认 "dev")、admin_allowed_ips(默认 "117.147.35.138,218.75.34.87,10.240.0.0/16");复用 wecom_corp_id
backend/app/utils/env_gating.py 【新增】 is_production()(按 app_env)、ip_in_whitelist(client_ip, allowed)(支持 CIDR);统一 env-gating 判定
backend/app/middleware/admin_ip_whitelist.py 【新增】 AdminIPWhitelistMiddleware:仅 app_env==production 且路径命中 admin 前缀时校验客户端 IP
backend/app/main.py 【修改】 注册 AdminIPWhitelistMiddleware(在 CORS 之后、路由之前)
backend/app/api/otp.py 【新增】 统一 OTP 路由(前缀 /auth):otp-status / otp-bind / otp-verify / otp-unbind / otp-admin-reset/{id} / otp-admin-users
backend/app/api/router.py 【修改】 注册 otp_router;注销 portal_router / mfa_router / agents 内 otp 端点
backend/app/api/mfa.py 【删除】 作废 /api/mfa/*/api/admin/mfa/*
backend/app/api/agents.py 【修改】 删除 /agents/otp-bind / /agents/otp-verify / /agents/otp-unbind 三端点;agent_login 移除 skip_otp 免密分支
backend/app/api/h5.py 【修改】 get_oauth_authorize_urlredirect_uri/itportal//itdesk/(现网bug);新增 GET /h5/oauth/sns-callback 302 带 ?token=_require_wework_ua 改用 env_gating.is_production()
backend/app/api/auth_wecom_sso.py 【修改】 sso_verifysuccess_response 包裹(一致性);备注 env-gating
backend/app/api/dev_auth.py 【修改】 三端点用 success_response 包裹(CTRT-P1-1 一致性,dev 仅本地)
backend/app/api/portal.py 【删除】 作废 /api/portal/*
backend/app/dependencies.py 【修改】 文档更新(mfa:verified: key 不变,仅端点路径变)

2.2 前端 H5frontend-h5

路径 操作 说明
src/api/index.ts 【修改】 拦截器统一方案A(成功返回内层 data);移除非 wxwork→/login 的 dev 兜底,生产改 /wework-only;清理 X-Employee-Id 遗留头
src/router/index.ts 【修改】 生产非 wxwork UA → 跳转 /wework-only(沿用现有 ?token= 镜像分支不动)
src/api/employee.ts 【修改】 调用点适配内层 dataresponse.dataresponse
src/api/conversation.ts 等全部 src/api/*.ts 【修改】 CTRT-P0-2 全量回归(约 8 个文件)
src/views/WeworkOnly.vue 【复用】 非企微拦截页(已存在,无需新建)

2.3 前端坐席(frontend-agent

路径 操作 说明
src/api/index.ts 【修改】 拦截器统一方案A;移除 portal_token 清理遗留
src/views/Login.vue 【修改】 移除「企微免密登录 / JS-SDK 快捷登录 / 智能检测三选项 / onMounted 自动 sso 跳转」;保留 ①扫码 ②账密+OTP(OTP 已 v-if="requireOtp" 延迟渲染)
src/api/mfa.ts 【修改】 路径改 /api/auth/otp-*;调用点适配内层 data
src/api/*.tsqrcode.ts、conversation.ts、message.ts 等) 【修改】 CTRT-P0-2 全量回归

2.4 前端管理(frontend-admin

路径 操作 说明
src/api/index.ts 【修改】 拦截器统一方案A(与坐席同形态;admin 仍无静默刷新,401/1002 清 admin_token/login
src/views/Login.vue 【修改】 移除「企微免密登录」按钮与 JS-SDK 检测分支;保留 ①扫码 ②账密+OTP;非白名单 403 → 无权限页
src/views/NoPermission.vue 【新增】 管理端「无访问权限」拦截页(对应 PRD 原型 note 4)
src/api/mfa.ts 【修改】 /admin/mfa/users/api/auth/otp-admin-users/admin/mfa/reset/{id}/api/auth/otp-admin-reset/{id};适配内层 data
src/api/*.ts 【修改】 CTRT-P0-2 全量回归

2.5 待清理工程

路径 操作 说明
frontend-portal/(整个工程) 【删除】 统一入口 Portal 前端(决策1/C8),含 QrcodeLogin.vue / PortalSelect.vue / stores/portal.ts

3. 数据结构和接口

3.1 类图(Mermaid

classDiagram
    class Employee {
        +str employee_id
        +str corp_id
        +str name
        +str department
        +str position
        +str avatar
    }
    class Agent {
        +str user_id
        +str name
        +str role
        +str status
        +str mfa_secret
        +bool mfa_enabled
        +datetime mfa_bound_at
        +datetime mfa_last_verified_at
        +str password_hash
        +int current_load
        +int max_load
    }
    class OtpSecret {
        <<值对象,内嵌于 Agent>>
        +str secret
        +bool enabled
        +datetime bound_at
        +datetime last_verified_at
    }
    class Token {
        <<Redis 存储>>
        +str token
        +str employee_id
        +list roles
        +str current_role
        +str login_source
        +int ttl_seconds
    }
    class MFAService {
        <<static 封装 pyotp>>
        +generate_secret() str
        +build_provisioning_uri(secret, id) str
        +render_qrcode_base64(uri) str
        +verify_code(secret, code) bool
        +mark_verified(redis, id, ttl)
        +is_verified(redis, id) bool
    }
    class TokenService {
        +create_token(employee_id, name, roles, ...) str
        +get_user_info(token) dict
        +refresh(token) bool
        +switch_role(token, role) bool
    }
    class OtpRouter {
        <<FastAPI 前缀 /api/auth>>
        +GET otp-status
        +POST otp-bind
        +POST otp-verify
        +POST otp-unbind
        +POST otp-admin-reset/{id}
        +GET otp-admin-users
    }
    class LoginRouter {
        <<agents/login + auth_qrcode>>
        +POST agents/login
        +POST auth_qrcode/create
        +GET auth_qrcode/poll/{ticket}
        +POST auth_qrcode/scan
        +POST auth_qrcode/confirm
    }
    class H5OAuthRouter {
        <<h5 OAuth>>
        +GET h5/oauth/authorize
        +GET h5/oauth/sns-callback
        +POST h5/oauth/callback
    }
    class AdminIPWhitelistMiddleware {
        +is_production 门控
        +ip_in_whitelist(ip) bool
    }
    Agent "1" *-- "1" OtpSecret : 内嵌 mfa_*
    OtpRouter ..> MFAService : 复用
    OtpRouter ..> Agent : 读写 mfa_*
    OtpRouter ..> Token : 依赖 Bearer 鉴权
    LoginRouter ..> TokenService : 签发 token
    LoginRouter ..> MFAService : agents/login 内联校验
    H5OAuthRouter ..> TokenService : 签发 employee token
    TokenService ..> Token : 存 Redis(user/employee/agent)
    AdminIPWhitelistMiddleware ..> LoginRouter : 守卫 /api/admin/*

3.2 OTP 接口契约(前缀 /api/auth,全部走统一信封)

方法 & 路径 鉴权 请求体 成功响应 data 说明
GET /otp-status 登录用户 {bound, enabled, last_verified_at} 路由守卫用
POST /otp-bind 登录用户 {secret, otpauth_url, qr_code_base64} 生成 secret 存 mfa_secret(enabled=False);已 enabled 拒绝
POST /otp-verify 登录用户 {otp_code} {verified, bound, expires_in} 未启用→确认绑定(set enabled+bound_at);写 mfa:verified:;用于 bind 闭环 + 高危操作
POST /otp-unbind 登录用户 {otp_code} {success} 校验后清空 mfa_secret/enabled
POST /otp-admin-reset/{employee_id} admin 角色 {success} 丢手机兜底,无 OTP 直接清空
GET /otp-admin-users admin 角色 ?keyword&bound&page&page_size {items,total,page,page_size} 管理页用户 MFA 列表(保留管理页)

3.3 登录接口契约

方法 & 路径 请求体 响应 data
POST /api/agents/login {user_id, name?, password, otp_code?} 成功:{token, user_id, name, role, ...}mfa_enabled 且无 otp_code{require_otp:true, user_id, name, role}不带 token
POST /api/auth_qrcode/create {ticket, qrcode_url, qrcode_png_base64, expires_in, expires_at}
GET /api/auth_qrcode/poll/{ticket} {status, employee_id, name, token}status: waiting/scanned/confirmed/expired
POST /api/auth_qrcode/confirm {ticket, otp_code?} {token, employee_id, name, roles, require_otp}
GET /api/h5/oauth/authorize ?redirect_uri {authorize_url}prod 强制 wxwork UA,否则 4003
GET /api/h5/oauth/sns-callback ?code 302 → /itdesk/?token=XXX
POST /api/h5/oauth/callback {code} {token, employee_id, ...}dev 兜底,保留)
GET /api/dev/login ?userid&role=user {token, user}dev/mockDEV_MODE 启用)
POST /api/auth/refresh ?token {token, expires_in}(坐席静默刷新;H5/admin 不刷新)

管理端登录复用 POST /api/agents/login(同契约,role=admin)。


4. 程序调用流程(Mermaid 时序图)

4.1 员工端 OAuth 静默授权(snsapi_base → ?token= 镜像)

sequenceDiagram
    participant U as 员工(企微WebView)
    participant H5 as H5前端(/itdesk/)
    participant R as 路由守卫
    participant B as 后端(/api/h5)
    participant W as 企微OAuth

    U->>H5: 打开 /itdesk/
    H5->>R: beforeEach 守卫
    R->>R: 读 ?token=(无) / ?code=(无) / 无 token
    R->>B: GET /h5/oauth/authorize (prod 校验 wxwork UA)
    B-->>R: {authorize_url}
    R->>W: 302 跳转企微授权页
    W-->>H5: 回调 redirect_uri?code=CODE
    H5->>B: GET /h5/oauth/sns-callback?code=CODE
    B->>W: code 换 userid + 用户信息
    B->>B: 生成 employee token 存 Redis(employee:token:)
    B-->>H5: 302 /itdesk/?token=XXX
    H5->>R: 守卫读 ?token=XXX
    R->>R: localStorage.h5_token = XXXreplaceState 清除 URL
    R->>B: 携带 Bearer 拉取用户信息
    B-->>H5: 工作台数据(内层 data

4.2 坐席/管理 扫码登录(auth_qrcode

sequenceDiagram
    participant A as 坐席/管理员
    participant FE as 前端登录页
    participant B as 后端(/api/auth_qrcode)
    participant WX as 企微App(扫码确认)
    participant R as Redis

    A->>FE: 点击「企微扫码登录」
    FE->>B: POST /auth_qrcode/create
    B->>R: 写 ticket(120s) + OAuth URL
    B-->>FE: {ticket, qrcode_png_base64}
    FE->>FE: 展示二维码 + 2s 轮询
    loop 轮询
        FE->>B: GET /auth_qrcode/poll/{ticket}
        B-->>FE: {status: waiting/scanned}
    end
    WX->>B: GET /auth_qrcode/scan?code&state=ticket (企微OAuth回调)
    B->>R: 写 scan:{ticket}
    A->>WX: 在企微点「确认登录」
    WX->>B: POST /auth_qrcode/confirm {ticket}
    B->>B: 校验身份→签发 token(agent/admin)
    B->>R: 写 confirm:{ticket}=token
    FE->>B: GET /poll/{ticket} → {status:confirmed, token}
    FE->>FE: localStorage.agent_token/admin_token = token
    FE->>FE: 跳 /workspace 或 /

4.3 坐席/管理 账号密码 + OTP

sequenceDiagram
    participant A as 坐席/管理员
    participant FE as 前端登录页
    participant B as 后端(/api/agents/login)
    participant M as MFAService/Redis

    A->>FE: 输入账号+密码,点登录
    FE->>B: POST /agents/login {user_id, password}
    alt 已绑定 MFA 且无 otp_code
        B-->>FE: {require_otp:true, user_id, name, role}(无 token
        FE->>FE: 渲染 OTP 输入框(v-if requireOtp)
        A->>FE: 输入 6 位 OTP
        FE->>B: POST /agents/login {user_id, password, otp_code}
        B->>M: verify_code(mfa_secret, otp_code)
        M-->>B: True
        B->>B: 签发 token
        B-->>FE: {token, user_id, name, role}
    else 未绑定 MFA
        B-->>FE: {token, ...} 直接登录
    end
    FE->>FE: localStorage.agent_token/admin_token = token;跳主页

4.4 令牌过期 / 401 处理

sequenceDiagram
    participant FE as 三端前端
    participant I as 响应拦截器
    participant B as 后端
    participant R as Redis

    FE->>B: 业务请求(Bearer token)
    B-->>FE: 401 / {code:1002}
    alt H5 员工端
        I->>I: 清 h5_token
        alt 生产(有 CorpId)
            I->>B: 重走 OAuth 重定向(带防循环计数)
        else Mock(dev)
            I->>FE: 跳 /itdesk/login
        end
    else 坐席端
        I->>B: POST /api/auth/refresh?token=(静默)
        B->>R: 延长 user:token TTL
        alt 刷新成功
            I->>FE: 重放原请求
        else 失败
            I->>I: 清 agent_token(不再清 portal_token
            I->>FE: 跳 /login
        end
    else 管理端
        I->>I: 清 admin_token
        I->>FE: 跳 /login
    end

5. 任务列表(有序、含依赖、按 AUTH-/CTRT- 分组)

说明:本重构涉及「后端 + 三前端」,按 PRD 交付要求拆为可独立执行的细粒度任务,按 AUTH(认证)/ CTRT(契约)分组,标注依赖与可并行项。前端契约任务(CTRT)与后端任务可并行启动。

5.1 认证类(AUTH-

ID 任务 涉及文件 依赖 可并行 验收标准
AUTH-01 环境门控与配置 backend/app/config.py【改】、backend/app/utils/env_gating.py【新】 是(与 CTRT-01 并行) is_production()ip_in_whitelist() 单测通过;app_env/admin_allowed_ips 可由环境变量注入
AUTH-02 管理端 IP 白名单中间件 backend/app/middleware/admin_ip_whitelist.py【新】、backend/app/main.py【改】 AUTH-01 仅 prod + /api/admin/*/api/auth/otp-admin-* 命中;非白名单返回 {code:4004}dev 关闭不拦截
AUTH-03 统一 OTP 路由(复用 MFAService backend/app/api/otp.py【新】 AUTH-01 是(与 AUTH-05 并行) 6 端点齐备;复用 mfa:verified: keyotp-bind→otp-verify 闭环、admin-reset 生效
AUTH-04 路由收口与旧端点清理 backend/app/api/router.py【改】、backend/app/api/mfa.py【删】、backend/app/api/agents.py【改】 AUTH-03 router 注册 otp、注销 portal/mfa/agents-otp/agents/otp-*/api/mfa/* 不可达;agent_loginskip_otp 免密分支
AUTH-05 H5 OAuth 修 bug + ?token= 重定向 backend/app/api/h5.py【改】 AUTH-01 是(与 AUTH-03 并行) authorize redirect_uri=/itdesk/sns-callback 302 带 ?token=wxwork UA 校验仅 prod
AUTH-06 移除 Portal 工程与 portal_token backend/app/api/portal.py【删】、frontend-portal/【删】、frontend-agent/src/api/index.ts【改】 AUTH-04 /api/portal/* 不可达;frontend-portal 已删;三端无 portal_token 引用
AUTH-07 后端信封一致性审计 backend/app/api/dev_auth.py【改】、backend/app/api/auth_wecom_sso.py【改】、其余路由抽查 是(并行) 全路由经 success_response/AppException;dev/sso 端点用信封包裹;无裸 dict 直返
AUTH-08 H5 员工端登录页/拦截 frontend-h5/src/router/index.ts【改】、frontend-h5/src/views/WeworkOnly.vue【复用】 CTRT-01 prod 非 wxwork → /wework-only?token= 镜像保留;mock 走 /login
AUTH-09 坐席登录页清理 frontend-agent/src/views/Login.vue【改】 CTRT-01 仅 ①扫码 ②账密+OTP;无免密/JS-SDK 分支;无 onMounted 自动 sso 跳转;OTP 仍延迟渲染
AUTH-10 管理登录页清理 + IP 拦截页 frontend-admin/src/views/Login.vue【改】、frontend-admin/src/views/NoPermission.vue【新】 AUTH-02, CTRT-01 移除免密按钮;非白名单 403 → 无权限页;扫码+账密+OTP 保留
AUTH-11 前端 OTP API 模块迁移 frontend-agent/src/api/mfa.ts【改】、frontend-admin/src/api/mfa.ts【改】 AUTH-03, CTRT-02 路径改 /api/auth/otp-*;调用点适配内层 dataadmin 列表/重置端点对齐

5.2 契约类(CTRT-

ID 任务 涉及文件 依赖 可并行 验收标准
CTRT-01 三端拦截器统一方案A(响应+请求) frontend-h5/src/api/index.ts【改】、frontend-agent/src/api/index.ts【改】、frontend-admin/src/api/index.ts【改】 是(与 AUTH-01/03/05/07 并行) 成功返回内层 data;失败抛 {code,message};请求统一 Authorization:Bearer;清 X-Employee-Id/portal_token 遗留
CTRT-02 三端调用点全量回归 三端 src/api/*.tsemployee/conversation/mfa/qrcode/message 等约 15 文件) CTRT-01 H5 response.dataresponseagent/admin response.data.dataresponse;编译+核心链路无字段错取
CTRT-03 401/1002 上报形态统一 三端 src/api/index.ts(含 CTRT-01 三端 catch 到统一 {code,message};各自重授权逻辑符合 PRD §3「401 处理约定」表

5.3 联调与验证

ID 任务 涉及文件 依赖 可并行 验收标准
VERIFY-01 env-gating 与本地 dev/mock 联调 全端 全部 AUTH/CTRT 本地 VITE_WECOM_CORP_ID 空 → mock 登录链路通;真实 OAuth 仅 staging/prod
VERIFY-02 三端认证回归测试 VERIFY-01 满足 PRD §5.3 五条自动化断言(dev/mock 返回 code:0+合法 token;失效 token 触发各端重授权;拦截器统一契约;OTP 闭环)

6. 依赖包列表

  • 后端无新增pyotp(2.10.0)、qrcoderedisbcryptpasslibslowapi 均已存在。
  • 前端无新增axiosvue-routerpiniaelement-plus(agent/admin)、vant(h5) 均已存在。

7. 共享知识(跨文件约定)

  1. Token 键名
    • localStorageh5_token / agent_token / admin_token
    • Redisemployee:token:{token}→employee_idH5)、user:token:{token}→JSON(坐席/管理统一格式)、agent:token:{token}→user_id(旧格式兼容)。
    • 请求头统一 Authorization: Bearer <token>移除 X-Employee-Id 明文头与 portal_token
  2. 拦截器返回形态(方案A:成功返回内层 data;失败 reject 标准化错误对象 {code, message}。三端一致。
  3. 环境变量
    • 后端:APP_ENV(production/staging/dev/test,默认 dev)、WECOM_CORP_IDADMIN_ALLOWED_IPS("117.147.35.138,218.75.34.87,10.240.0.0/16")、DEV_MODEMOCK_LOGIN_ENABLEDWECOM_SSO_ENABLED
    • 前端:VITE_WECOM_CORP_ID空 = dev/mock)、VITE_APP_ENV(可选,区分 prod/dev)。
  4. env-gating 矩阵(仅生产启用):UA 校验 / IP 白名单 / 真实企微 OAuth;本地/测试跳过,走 dev/mock。
  5. 401 / 1002 处理约定
    • H5:清 h5_token;prod→重走 OAuth(带防循环计数,上限 3);mock→跳 /itdesk/login
    • 坐席:先静默 POST /api/auth/refresh;失败清 agent_token/login不再清 portal_token)。
    • 管理:清 admin_token/login
  6. OTP Redis 标记key mfa:verified:{employee_id}TTL 1800s,由 MFAService 读写;require_high_risk_otp 依赖此 key(端点改名不影响)。
  7. 三端入口/itdesk/(H5) / /itagent/(坐席) / /itadmin/(管理)移除 /itportal/
  8. 错误码1002=未授权;4003=非企微环境/无权限;4004=管理端 IP 无权限(新增);1006=OTP 验证码错误。

8. 待明确事项

  1. 10.240.0.0 网段掩码PRD 写「10.240.0.0(内网/VPN 网段)」,本设计按 /16 CIDR 处理,请确认精确掩码(如 /12 / /16)。
  2. 管理端 MFA 用户列表端点/admin/mfa/users 是否随 PRD 作废?本设计默认迁移GET /api/auth/otp-admin-users 以保留管理页功能;若产品决定下线该管理页,则可一并删除。
  3. agents/login 内联 pyotp 校验:本次保持最小变更(不重构为复用 MFAService);如需消除重复实现,列为可选优化(不影响功能)。
  4. 员工端 OAuth 跳转方式:本设计采用「后端 sns-callback 302 带 ?token=」(贴合决策2,复用现有 ?token= 镜像);旧的「前端 code→POST /h5/oauth/callback 取 token」路径保留为 dev 兜底。如坚持完全走 ?token=,可删除旧 POST 回调。
  5. wecom_jsdk_login 接口:决策4 移除前端「免密」入口,但后端 /api/auth_wecom/jsdk-login 接口本设计保留(仅前端不再调用),避免影响其他潜在调用方;如需彻底删除请确认。

附:类图见 docs/class-diagram.mermaid,时序图见 docs/sequence-diagram.mermaid