机制详解:Redis 滑动 TTL、管理类 API 过滤与 Celery 巡检的完整实现)
BISHENG 多租户管理视图切换Admin Scope机制详解Redis 滑动 TTL、管理类 API 过滤与 Celery 巡检的完整实现【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng导读本文围绕 BISHENG v2.5.1 的 F019-admin-tenant-scope管理视图切换 / Admin Scope功能完整讲解全局超管临时切换到某个 Child Tenant 管理视角的机制设计、API 契约、Service 实现、中间件注入、Celery 巡检与全量验收AC映射。读完本文你将掌握该机制为何选 Redis 而非 JWT、4 小时滑动 TTL 如何在每次管理类 API 命中时刷新、scope 为何只作用于管理类 API 白名单、以及登出/主部门变更/角色撤销/租户禁用四类生命周期事件如何清理 scope。所有结论均可在当前仓库源码中逐一验证。1. 功能背景为什么需要管理视图切换1.1 问题跨 Child 管理缺少上下文在 v2.5.1 的 Tenant 树形架构下集团 IT 的全局超管需要跨 Child Tenant 管理资源尤其是各 Child 专属的 LLM 模型、角色、配额和审计日志。原方案POST /api/v1/user/switch-tenant在 2026-04-20 收窄中已废弃返回 410 Gone理由是用户归属由主部门派生不应由 JWT 随意切换见 spec.md §1 与 §3。F019 提供的是一套语义完全不同的机制——管理视图切换Admin ScopeRedis 驻留非 JWT仅限全局超管使用仅对管理类 API 生效与用户归属完全解耦。1.2 核心用户故事作为集团 IT 的全局超管我希望临时切换到某个 Child Tenant 的管理视角查看/配置该 Child 专属的 LLM 模型、角色、配额、审计日志以便跨 Child 管理时有明确的上下文而不需要登出/重登或改动自己的用户归属。1.3 仓库落地位置需求与验收标准features/v2.5.1/019-admin-tenant-scope/spec.mdAC 对照与回归验证features/v2.5.1/019-admin-tenant-scope/ac-verification.md角色撤销钩子调研features/v2.5.1/019-admin-tenant-scope/role-revoke-hook.md2. 架构决策7 项 AD 及其理由spec §4 给出了 7 项关键架构决策ac-verification.md 的架构决策§4 AD-01~07落地证据表逐一核对了它们在当前仓库中的实现ID决策结论落地证据仓库路径AD-01存储介质Redis非 JWT、非 MySQLtenant_scope.py 只读写admin_scope:{user_id}keyJWT 签名逻辑未改动AD-02TTL 策略滑动刷新非固定admin_scope.py 每次管理类 API 命中调aexpire_keyAD-03TTL 长度4h对比 1h/8h 折衷multi_tenant.pyadmin_scope_ttl_seconds14400AD-04scope 作用域仅管理类 API 白名单admin_scope.pyMANAGEMENT_API_PREFIXESAD-05适用角色仅全局超管Endpoint 层_check_is_global_super守卫 Middleware 层 fail-closed 再校验AD-06非超管调用处理403 拒绝非静默忽略AdminScopeForbiddenError(Code19701)→ HTTP 403AD-07Child 禁用后清理Celery 巡检非实时钩子tasks.py beat*/10 * * * *其中 AD-07 的权衡值得注意若在 Tenant 禁用/归档的热路径上同步扫描 Redis 清理 scope会拖慢禁用操作本身选择 Celery 每 10 分钟巡检换来的是最大不一致窗口 ≤ 10 分钟的折衷spec §3 边界情况、AC-13。3. 16 条验收标准AC全景spec §2 定义了 16 条验收标准ac-verification.md 将全部 16 条映射到了自动化测试与手工 QA 清单AC描述要点预期结果AC-01超管 POST{tenant_id: 5}200 scope_tenant_id5 Redis TTL≈14400AC-02超管 GET200 {scope_tenant_id, expires_at}ISO 格式 剩余 TTLAC-03POST{tenant_id: null}200 Redis DEL 后续 GET 返 nullAC-04 / AC-05Child Admin / 普通用户 POSTHTTP 403 错误码 19701AC-06已设 scope5 调管理类 API如/llm结果按tenant_id IN (5, 1)过滤AC-07已设 scope5 调业务 API如/chat不受影响按 JWT leaf 执行AC-084h 内持续调用管理类 APIRedis TTL 滑动刷新scope 不过期AC-09超过 TTL 未调用Redis 自然过期回退看全树AC-10logoutRedis DELadmin_scope:{user_id}AC-11user.token_version 1主部门变更钩子 DEL Redis keyAC-12超管角色被撤销钩子 DEL当前无公开撤销 API由中间件 fail-closed 兜底AC-13Child 被禁用/归档/删除Celery 每 10 分钟清理指向非 active Tenant 的 keyAC-14每次 POST写audit_logactionadmin.scope_switch含 from/to_scope、ip、user_agent、operator_idAC-15tenant_id 指向不存在 TenantHTTP 400 错误码 19702AC-16tenant_id1Root允许等价锁定看 Root 视图IN 列表 {1}3.1 AC-16 的一个易混淆点AC-16 与不设 scope有行为差异spec §2 特别说明显式设 scope1管理类 API 的可见集合 IN 列表 {1}只看到 Root 自身不设 scope可见集合 None看到全树全部 Child。4. API 契约与错误码4.1 端点定义模块编码197admin_scope共两个端点定义在 tenant_scope.pyPOST /api/v1/admin/tenant-scope 设置或清除当前调用者的 scope GET /api/v1/admin/tenant-scope 读取当前 scope 与剩余 TTL请求体为{tenant_id: int | null}pydantic 模型SetScopeRequesttenant_id可空。4.2 错误码spec §5错误码定义在 admin_scope.py错误码错误类HTTP 状态触发位置19701AdminScopeForbiddenError403Endpoint_check_is_global_super失败19702AdminScopeTenantNotFoundError400Serviceset_scope校验 tenant_id 不存在时抛出Endpoint 层维护_ERRCODE_HTTP_STATUS映射通过_errcode_to_response将结构化错误码翻译为真实 HTTP 403/400 响应体遵循 F018 的先例。4.3 超管识别细节spec 声明依赖 F013 的LoginUser.is_global_super()但该公开方法在落地时未随 F013 交付。F019 的 Endpoint 与中间件统一复用bisheng.utils.http_middleware._check_is_global_super(user_id)——同一套 FGA 查询 Redis 缓存逻辑保证全栈判定一致见 tenant_scope.py 模块 docstring 与 ac-verification.md开发实际偏差。这是 F013 合约缺口的临时补丁F020 落地时可一并抽公共 helper。5. Service 层Redis 读写与审计日志核心实现位于 tenant_scope.pyRedis key 模板为admin_scope:{user_id}。5.1 set_scope设置 / 覆盖 / 清除key _redis_key(user_id) # admin_scope:{user_id} old_raw await redis.aget(key) # 读旧值用于 audit from_scope old_scope int(old_raw) if old_raw else None if tenant_id is None: await redis.adelete(key) # AC-03清除 expires_at None else: if not await TenantDao.aexists(tenant_id): # AC-15先校验存在 raise AdminScopeTenantNotFoundError() # 19702 ttl settings.multi_tenant.admin_scope_ttl_seconds await redis.aset(key, str(tenant_id), expirationttl) # 值存字符串 expires_at _iso_expiry(ttl)要点校验顺序先查TenantDao.aexists(tenant_id)再写 Redis不存在直接抛 19702AC-15值类型Redis 里存的是str(tenant_id)读取时再int()还原过期时间expires_at用_iso_expiry生成 ISO-8601 UTC 时间戳返回给前端展示。5.2 audit_log每次切换留痕AC-14每次 POST 都会写一条audit_logaction 使用TenantAuditAction.ADMIN_SCOPE_SWITCH定义于bisheng/tenant/domain/constants.pymetadata 含{ from_scope: 3, // 旧 scope首次为 null to_scope: 5, // 新 scope ip: 192.168.x.x, user_agent: curl/8.x }operator_tenant_id硬编码为ROOT_TENANT_ID1因为调用者必然是全局超管Endpoint 层已强制且 INV-T11 保证 Root 是唯一恒存在的 Tenantspec §5.2 注释说明硬编码 1 避免查库。5.3 get_scope精确剩余 TTLraw await redis.aget(key) if raw is None: return {scope_tenant_id: None, expires_at: None} ttl await redis.attl(key) # 新增基础设施方法 if ttl is None or ttl 0: expires_at None # key 无 TTL(-1) 或不存在(-2) else: expires_at _iso_expiry(ttl) return {scope_tenant_id: int(raw), expires_at: expires_at}注意RedisClient.attl是 F019 为精确读取剩余 TTL 而新增的一行封装async_connection.ttl(key)属于通用基础设施扩展非 F019 专有见 ac-verification.md开发实际偏差。5.4 生命周期清理钩子Service 暴露三个幂等 DEL 钩子钩子调用方对应 ACclear_on_logoutAuthService.logoutbisheng/user/api/user.pyAC-10clear_on_token_version_bumpUserTenantSyncService.sync_user 主部门变更成功后AC-11clear_on_role_revoke未来 RoleService 撤销超管角色时AC-12当前未挂接见 §8三个钩子统一委托_clear(user_id)执行redis.adelete(key)天然幂等。6. 中间件ContextVar 注入与滑动 TTL中间件 admin_scope.py 是 scope 生效的核心消费点。6.1 白名单AD-04MANAGEMENT_API_PREFIXES: tuple[str, ...] ( /api/v1/llm, /api/v1/workstation, /api/v1/linsight, /api/v1/tool, /api/v1/knowledge, /api/v1/chat/online, /api/v1/admin, )与 spec 初稿相比有一处值得注意的收窄v2.5.1 原计划包含/api/v1/roles和/api/v1/audit_log但产品评审结论是——角色使用全局视图 字段过滤模式审计日志对超管天然全局、对 Child Admin 天然按visible_tenant_ids子树收敛均无需 scope 切换且/api/v1/audit_log字面量本就是死代码真实路由前缀是/api/v1/audit。最终 scope 只影响 LLM 管理面及设置它的 admin 端点本身中间件注释明确记载了这一决策过程。代码注释强调白名单刻意保守给整个 API 面套上 scope 会改变 chat、workflow 执行、知识库摄取等业务流。6.2 注入与滑动刷新逻辑dispatch流程对应 AC-06/07/08/09判断is_mgmt并写入_is_management_apiContextVar非管理类 API 直接放行——不解 JWT、不读 Redis、不做 FGA 检查热路径零额外开销AC-07spec §3 边界情况管理类 API解 JWT 取user_id→_check_is_global_super校验非超管 fail-closed即使 Redis 里残留 scope key 也不读取防止非超管被代理进 scope 视图spec §3 边界情况非超管误设 scopeRedis key 虽写入但中间件仅为超管读取读admin_scope:{user_id}非空则set_admin_scope_tenant_id(int(raw))注入 ContextVar由 F012 §5.4 在core/context/tenant.py定义本 Feature 消费而非定义滑动刷新调redis.aexpire_key(key, settings.multi_tenant.admin_scope_ttl_seconds)刷新 TTLAC-08TTL 更新失败不阻塞请求fail-open 仅记 debug 日志。6.3 注册顺序关键细节中间件在 main.py 中于CustomMiddleware之前添加使其入站顺序在 Custom 之后成为运行时内层中间件——即 CustomMiddleware 先解码 JWT 并填充visible_tenant_idsContextVarAdminScopeMiddleware 的dispatch在其后执行。这一顺序是 6.2 中解 JWT → 校验超管 → 注入 scope能工作的前提。6.4 Redis 竞态与失败语义spec §3 明示了两类边界行为TTL 临界点竞态两个并发请求可能一个读到 scope、一个读到 None不做跨请求事务UI 下次刷新即恢复Redis 故障 fail-open中间件对 Redis 读失败、超管校验异常均放行并记 debug 日志避免中间件故障拖垮整条请求链。7. Celery 巡检任务清理失效 scopeAC-13任务定义在 tasks.pybeat schedule 在 settings.py 注册为*/10 * * * *每 10 分钟。7.1 执行流程bisheng_celery.task(acks_lateTrue, time_limit600, soft_time_limit540, namebisheng.worker.admin_scope.tasks.admin_scope_cleanup) def admin_scope_cleanup(): run_async_task(_cleanup_async) async def _cleanup_async(): redis await get_redis_client() keys await redis.akeys(admin_scope:*) if not keys: return # 快速路径无 key 直接跳过 DB 查询 with bypass_tenant_filter(): # 关键跨 Tenant 查询必须 bypass non_active set(await TenantDao.aget_non_active_ids()) if not non_active: return for key in keys: # 读取值 → int() 解析解析失败视为损坏值直接 DEL # scope_id in non_active → DEL7.2 两个必须知道的实现细节bypass_tenant_filter()包裹Celery worker 进程没有 HTTP 请求上下文current_tenant_idContextVar 为 None。若不做 bypassSQLAlchemy 自动注入的 tenant_filter 事件在current_tenant_idNone时行为未定义可能过滤为空结果或抛TenantContextMissing。任务 docstring 与 spec §5.4 均特别强调了这一点。快速路径优化admin_scope:*无 key 时连 DB 查询都跳过non_active为空时也提前返回避免空转。7.3 与 AC-12 的配合AC-13 覆盖的是租户状态变化禁用/归档/孤儿/删除status IN (disabled, archived, orphaned)AC-12 覆盖的是角色撤销。二者共同保证 stale key 的最终收敛中间件 fail-closed 则在两个事件窗口内兜底。8. 角色撤销钩子AC-12 的现实处理ac-verification.md开发实际偏差与 tenant_scope.py docstring 都记载了同一结论TenantScopeService.clear_on_role_revoke方法已就位但未挂接原因调研发现system:global#super_admin当前无公开撤销 APIuser_addrole/user_delete都显式拒绝操作超管AC-12 的可观察行为由中间件_check_is_global_super每请求再校验兜底——即使 Redis 残留了 scope key已撤销超管角色的用户也无法获得 scope 注入未来 RoleService 落地撤销 API 时只需 3 行接入详见 role-revoke-hook.md。这是方法就位 中间件兜底 文档化 TODO的典型渐进落地模式代码中以TODO(#F019-role-revoke)标注。9. 配置项# config.yaml → multi_tenant 段 multi_tenant: admin_scope_ttl_seconds: 14400 # 默认 4h 滑动 TTL配置模型定义在 multi_tenant.pypydanticField(default14400)注释明确Sliding refresh on each management API hit. Default 4h.。该值同时被 Serviceset_scope写 key 时设置过期与中间件滑动刷新消费。10. 手工 QA16 个场景的完整 curl 演练ac-verification.md 提供了基于 114 环境的完整手工验证清单以下为可直接执行的验证脚本前置要求全局超管账号 ≥1 个 Child Tenant。10.1 登录与设置AC-01# 以 admin 登录取 Cookie curl -b jar -c jar -X POST http://host:7860/api/v1/user/login \ -H Content-Type: application/json \ -d {user_name: admin, password: Bishengtop1} # POST scope5 curl -b jar -X POST http://host:7860/api/v1/admin/tenant-scope \ -H Content-Type: application/json -d {tenant_id: 5} # 期望200body.data.scope_tenant_id5body.data.expires_at≠null10.2 读取AC-02与清除AC-03curl -b jar http://host:7860/api/v1/admin/tenant-scope # 期望200body.data.scope_tenant_id5 # 清除 curl -b jar -X POST http://host:7860/api/v1/admin/tenant-scope \ -H Content-Type: application/json -d {tenant_id: null} curl -b jar http://host:7860/api/v1/admin/tenant-scope # 期望后续 GET 返 {scope_tenant_id: null, expires_at: null}10.3 权限拒绝AC-04/AC-05# 以 Child Admin 登录 → POST /admin/tenant-scope → HTTP 403 status_code19701 # 以普通用户登录 → POST /admin/tenant-scope → HTTP 403 status_code1970110.4 滑动 TTLAC-08与自然过期AC-09# scope5 → 等 3 分钟 → GET /api/v1/llm命中中间件 # redis-cli TTL admin_scope:1 → 预期刷新回 14000 # 快速模拟过期等价语义 redis-cli DEL admin_scope:1 # 再 GET → 回退ContextVarNone10.5 登出清理AC-10curl -b jar -X POST http://host:7860/api/v1/admin/tenant-scope \ -H Content-Type: application/json -d {tenant_id: 5} curl -b jar -X POST http://host:7860/api/v1/user/logout # redis-cli EXISTS admin_scope:1 → 期望 010.6 主部门变更清理AC-11# 手工调 POST /api/v1/departments/... 变更超管主部门跨 Tenant # → UserTenantSyncService.sync_user 执行 → redis-cli EXISTS admin_scope:1 → 010.7 中间件兜底AC-12 可观察行为# 模拟非超管的 stale key redis-cli SET admin_scope:999 5 # 以 user_id999 普通用户登录调 GET /api/v1/llm # 中间件 _check_is_global_super 返 False → 不注入 scope10.8 Celery 巡检AC-13# 1. scope5 后禁用 Child 5PUT /api/v1/tenants/5/status {status: disabled} # 2. 等 10 分钟或手动触发 celery beat admin_scope_cleanup # 3. redis-cli EXISTS admin_scope:1 → 期望 010.9 审计日志AC-14SELECT id, tenant_id, operator_id, operator_tenant_id, action, audit_metadata FROM audit_log WHERE actionadmin.scope_switch ORDER BY id DESC LIMIT 5; -- 最新一行 metadata 含 from_scope / to_scope / ip / user_agent10.10 不存在的 TenantAC-15与 RootAC-16curl -b jar -X POST http://host:7860/api/v1/admin/tenant-scope \ -H Content-Type: application/json -d {tenant_id: 99999} # 期望HTTP 400 body.status_code19702 curl -b jar -X POST http://host:7860/api/v1/admin/tenant-scope \ -H Content-Type: application/json -d {tenant_id: 1} # 期望200 scope_tenant_id1ContextVar 注入后管理类 API 见集合 {1}11. 自动化测试37/37 全通过ac-verification.md 记录了 F019 新增的 6 个测试文件、37 个用例全部通过与 16 条 AC 一一对应测试文件用例数覆盖 ACtest_admin_tenant_scope_service.py10Service 单测含 AC-01/02/03/14/15 等test_admin_tenant_scope_api.py9HTTP 集成403/400 映射test_admin_scope_middleware.py8中间件注入与滑动刷新test_logout_scope_clear.py3AC-10AST 源码检查 Service 行为test_sync_user_scope_clear.py3AC-11 钩子test_admin_scope_cleanup_task.py4AC-13 Celery 巡检回归结果F012 71/71、F011/F013 53/53 全部通过其余失败均为预存 flakinesspermission_enrichment / role_service / f017_chat_message_service 在 2.5.0-PM baseline 相同失败minimax_provider 1 条预存内容 mismatch非 F019 引入。11.1 回归命令# 单独 F019 专项 cd src/backend uv run pytest \ test/test_admin_tenant_scope_service.py \ test/test_admin_tenant_scope_api.py \ test/test_admin_scope_middleware.py \ test/test_logout_scope_clear.py \ test/test_sync_user_scope_clear.py \ test/test_admin_scope_cleanup_task.py -v # 全量回归 cd src/backend uv run pytest -q11.2 开发过程中的三个测试偏差值得借鉴ac-verification.md开发实际偏差记录了三个务实的测试工程决策_FakeRedis替代真实 pickle 往返Service 层与真实实现的契约是存str(tenant_id)读出来同值测试以 mock 的 aget/aset 为准pickle 行为由 F012 的 Redis 同异步测试矩阵覆盖logout 测试改 AST 源码检查 Service 行为测试原计划 importlib 动态导入bisheng.user.api.user但该模块在测试环境下有 import chain 脆性conftest pre-mock 后子模块 import 失败 FastAPI response-model validation error改为 AST 读源码断言钩子存在 重新验证 Service DEL 行为语义等价且更抗未来 import chain 变化Celery 测试 importlib 路径派生从__file__.resolve().parent.parent派生而非 hard-code 路径支持任意 checkout root。12. 遗留事项与后续依赖ac-verification.md 与 spec §9/§10 共同交代了本功能的边界与后续AC-12 挂接点clear_on_role_revoke方法已就位等未来 RoleService 落地撤销 API 时 3 行接入F019 → F020 依赖前端useAdminScopehook AdminScopeSelector组件归 F020 拥有本 Feature 仅提供src/frontend/platform/src/controllers/API/admin.ts中的setTenantScope/getTenantScopeaxios 封装AC-06 真正的GET /api/v1/llm 结果按 IN(5,1) 过滤需 F020 的LLMDaotenant 感知改造到位后联调Out of ScopeRedis key 可观测性仪表盘、scope 历史查询 APIaudit_log 已含MVP 不单独建表、跨实例 scope 同步bisheng 私有化部署单实例即可、自动按上次操作的 Child预置 scope避免魔法行为。13. 关键文件速查类别文件说明API 端点tenant_scope.pyPOST/GET 两个端点 errcode→HTTP 映射Servicetenant_scope.pyset_scope/get_scope/clear_on_logout/clear_on_token_version_bump/clear_on_role_revoke中间件admin_scope.py白名单 超管 fail-closed 滑动刷新Celery 任务tasks.pybypass_tenant_filter包裹 快速路径错误码admin_scope.py19701 / 19702配置multi_tenant.pyadmin_scope_ttl_seconds14400beat 注册settings.py*/10 * * * *巡检调度Redis 基础设施redis_conn.pyattl/aexpire_keyhelper前端 API 封装admin.tssetTenantScope/getTenantScope设计文档spec.md需求、AC、架构决策、实现骨架AC 对照ac-verification.md16 条 AC 映射、手工 QA、回归结果调研笔记role-revoke-hook.mdT10 方案 2 调研总结F019 Admin Scope 以Redis 驻留、非 JWT、仅超管、仅管理类 API四个约束在不动用户归属的前提下为全局超管提供了跨 Child 的管理视图切换能力。其设计精髓在于Redis 滑动 TTL 平衡安全与体验4h、白名单限定作用域避免污染业务热路径、三层清理登出/主部门变更/角色撤销钩子 Celery 巡检 中间件 fail-closed 兜底保证 stale key 最终收敛。从 spec 到 AC 对照表再到源码16 条验收标准、37 个自动化用例与 7 项架构决策在仓库中全部有据可查是一套可直接复用的多租户管理上下文设计范本。【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考