b8ad65f7de
v2.5.0 的扁平多租户模型没有"用户归属到哪个 Tenant"的源头——登录时 tenant_id 仍走 DEFAULT_TENANT_ID=1 兜底,主部门调岗后旧 JWT 继续有效。F011 已把 Tenant 树结构(Root + 0~N Child)落地但缺用户侧映射;F012 补齐这一层,同时为 F013/F016/F019/F020 铺好 ContextVar 契约。
核心变更:
- 新增 TenantResolver:沿主部门 path 反向找最近 is_tenant_root=1 的挂载点,返回其 Child Tenant;disabled/archived/orphaned 跳过;最终回落 Root(1);带循环保护
- 新增 UserTenantSyncService.sync_user:resolve → 比对当前 leaf → 计数旧 Tenant 下资源 → 切归属 + 递增 token_version + 重写 FGA tenant#member 元组(crash_safe=True)+ DEL Redis 缓存 + 写 user.tenant_relocated audit
- 新增 UserDepartmentService.change_primary_department:事务内 demote/promote + 触发 sync(trigger=dept_change)
- user 表加 token_version INT NOT NULL DEFAULT 0(Alembic v2_5_1_f012_user_token_version,down=f011_tenant_tree)
- UserDao.aget_token_version (Redis 缓存 300s) + aincrement_token_version (原子 UPDATE) + alist_users_paginated
- LoginUser.token_version 字段 + create_access_token/init_login_user/get_login_user/get_login_user_from_ws 全链路透传
- 登录流程 (user_login) 集成 sync_user(LOGIN) + aget_token_version 刷新版本
- core/context/tenant.py 扩展 4 ContextVar:visible_tenant_ids / _strict_tenant_filter / _admin_scope_tenant_id / _is_management_api;新 strict_tenant_filter() CM;get_current_tenant_id() 优先级 admin_scope>current_tenant_id(v2.5.0 签名保留不变)
- CustomMiddleware 原地增强:_validate_token_version(401 + 19103)+ _check_is_global_super(FGA + Redis 缓存)+ _compute_visible_tenant_ids 计算 {leaf,1} / {1} / None 三档
- GET /api/v1/user/current-tenant:返 {leaf_tenant_id, is_child, mounted_department_id, root_tenant_id}(handler 抽到 current_tenant.py 便于单测)
- Celery 6h reconcile:worker.tenant_reconcile.tasks.reconcile_user_tenant_assignments,分页 batch=500 扫全量 user 表,beat crontab 0 */6 * * *
- UserTenantSyncConf 新配置类(enforce_transfer_before_relocate,默认 False)注册到 Settings
- TenantAuditAction 枚举加 USER_TENANT_RELOCATED / USER_TENANT_RELOCATE_BLOCKED;新 UserTenantSyncTrigger 枚举(LOGIN/DEPT_CHANGE/CELERY_RECONCILE/MANUAL)
- 错误码模块 191 tenant_resolver (19101-19104): TenantRelocateBlockedError / TenantResolveFailedError / TokenVersionMismatchError / TenantCycleDetectedError
F011/F012 共用重构:
- 抽取 tenant/domain/services/inbox_helper.py:集中 send_inbox_notice + list_global_super_admin_ids,替代 F011 department_deletion_handler 内的 _send_inbox_notice/_list_global_super_admin_ids;F011 handler 改为从 helper 导入(保留 __all__ re-export 给 F011 旧测试兼容)
- F012 UserTenantSyncService._notify_resource_owner_relocation 复用 send_inbox_notice
关键不变量:
- 资源不随归属迁移(PRD Review P0-C,INV-T4)——主部门跨 Tenant 变更后 sync_user 仅更新 user_tenant + JWT,资源 tenant_id 保持不动
- enforce_transfer_before_relocate=true 时名下有资源即阻断(409 + 19101 + audit user.tenant_relocate_blocked),降低误操作成本
- FGA 元组重写走 crash_safe failed_tuples 补偿;主事务不因 FGA 失败回滚(归属切换优先成功)
- visible_tenant_ids 为 F013 IN-list 过滤的契约;F019 AdminScopeMiddleware 之后通过 _admin_scope_tenant_id 覆盖 get_current_tenant_id()
测试(87 passed / 10 files):
- test_user_token_version_dao.py (6): ORM 默认 / NOT NULL / UPDATE 原子 / 重复 +1
- test_tenant_context_vars.py (16): 4 ContextVar 默认 + set/reset + 优先级 + strict CM 嵌套/异常安全 + v2.5.0 签名守护
- test_tenant_resolver.py (12): 路径派生 / disabled 跳过 / 多层取最近 / 无主部门 / cycle 检测 / Tenant 缺失回落
- test_user_tenant_sync_service.py (10): 无变化直返 / 阻断 / 告警不阻断 / FGA 重写 delete+write / token_version 递增 / Redis DEL / 首次同步无 delete / FGA 故障容错 / reason=no_primary_department
- test_auth_jwt_token_version.py (7): payload 含字段 / classmethod 读 User.token_version / 兼容 v2.5.0 无字段 User / 显式参数覆盖
- test_middleware_token_version.py (13): visible 计算 5 档 / token_version 匹配/不匹配/fail-open / apply 401 / super None / child {leaf,1}
- test_user_department_service.py (5): 同主部门 no-op / 跨 Tenant 触发 sync(DEPT_CHANGE) / 首次 primary / Blocked 透传 / 枚举值
- test_current_tenant_api.py (3): Root/Child 两档 + switch-tenant 410 回归
- test_tenant_reconcile_task.py (5): 单批/多批分页 / Blocked 吞掉 / 泛型异常继续 / 空表 no-op
- test_department_deletion_handler.py (10) [F011 回归]: handler 使用 inbox_helper 后原测试全绿
回归:
- F011 85 tests passed(含 department_deletion_handler 10 条,重构后无损)
- v2.5.0 基线维持(553 passed + 16 pre-existing failures 皆与 F012 无关)
SDD 产物:
- features/v2.5.1/012-tenant-resolver/tasks.md(12 任务全部完成 + 实际偏差记录)
- features/v2.5.1/012-tenant-resolver/ac-verification.md(AC-01~AC-11 → 测试映射 + spec §8 手工 QA 清单)
下游解锁:
- F013-tenant-fga-tree:读 get_visible_tenant_ids() 做 IN-list FGA check
- F014-sso-org-realtime-sync:直接调 UserTenantSyncService.sync_user(trigger=DEPT_CHANGE)
- F016-tenant-quota-hierarchy:strict_tenant_filter() 精确配额计数
- F019-admin-tenant-scope:set_admin_scope_tenant_id() + set_is_management_api() 已就位
- F020-llm-tenant-isolation:LLMDao 查询读 visible_tenant_ids
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDD (Spec-Driven Development) — BiSheng 适配版
完整方法论指南:
docs/SDD-Guide.md本目录存放 SDD 产物——版本契约、Feature 规格和任务清单。
工作流(9 步)
0. release-contract.md 版本开始时,一次性
↓
1. Spec Discovery 架构师提问,识别 PRD 不确定性
↓ ★ 手动暂停点:用户确认
2. 编写 spec.md 合并需求规范 + 技术设计
↓
3. /sdd-review <dir> spec 审查 spec(11 项检查)
↓ ★ 手动暂停点:用户确认
4. 编写 tasks.md 拆解为原子任务
↓
5. /sdd-review <dir> tasks 审查 tasks(17 项,自动推进)
↓
6. 创建 Feature 分支 feat/v2.5.0/{NNN}-{name},基于 2.5.0-PM
↓
7. 逐任务执行 实现 → 测试 → /task-review → 打勾
↓
7.5. /e2e-test <dir> E2E 测试(强制)
↓
8. /code-review --base 2.5.0-PM 多维度代码审查(自动)
↓
9. 合并回 2.5.0-PM
核心约束:
- 每步只产出该步骤的文件,不提前执行后续步骤
- 两个 ★ 手动暂停点必须等待用户确认
- 实现偏差必须记录在 tasks.md §实际偏差记录
目录结构
features/
├── README.md # 本文件
├── _templates/ # 可复用模板
│ ├── release-contract.md # 版本契约模板
│ ├── spec.md # 规格文档模板(BiSheng 适配版)
│ └── tasks.md # 任务清单模板(BiSheng 适配版)
└── v2.5.0/ # v2.5.0 版本产物
├── release-contract.md # 版本契约(预填)
├── README.md # Feature 索引
├── 001-feature-name/
│ ├── spec.md
│ └── tasks.md
└── ...
命名规范
Feature 目录
{NNN}-{kebab-case-name}
NNN— 零补齐三位数字(000, 001, 002, ...)- Name — 小写、连字符分隔、描述性名称
- 示例:
000-test-infrastructure、001-multi-tenant、004-rebac-core
Feature 分支
feat/v2.5.0/{NNN}-{short-name}
- 基于
2.5.0-PM拉出 - 合并回
2.5.0-PM(git merge --no-ff) - 示例:
feat/v2.5.0/004-rebac-core
审查命令
| 命令 | 时机 | 说明 |
|---|---|---|
/sdd-review <dir> spec |
spec.md 编写后 | 11 项需求+架构检查 |
/sdd-review <dir> tasks |
tasks.md 编写后 | 17 项拆解质量检查(自动) |
/task-review <dir> <task_id> |
每个任务完成后 | L1 约定合规(6 项) |
/code-review --base 2.5.0-PM |
Feature 全部完成后 | L2 多维度深度审查 |
/e2e-test <dir> |
全部任务完成后 | 生成并运行 E2E 测试 |
快速开始
新建 Feature
# 1. 复制模板
cp features/_templates/spec.md features/v2.5.0/NNN-feature-name/spec.md
cp features/_templates/tasks.md features/v2.5.0/NNN-feature-name/tasks.md
# 2. 按工作流执行:Discovery → spec → review → tasks → review → 实现
新建版本
# 1. 创建版本目录
mkdir features/vX.Y.Z
# 2. 复制版本契约模板
cp features/_templates/release-contract.md features/vX.Y.Z/release-contract.md
# 3. 填写领域对象归属、不变量、依赖图