# CMA 知识侧 entity_id 多租户分区——迁移方案(评审稿) > 任务:CMA-entity-memory-partition-20260903(研学调度 P1) > 提出者:yanxueBot → 执行人:wecom-fullstack > 决策依据:/root/WB_WS/_research/architecture-shared-intelligence-isolated-data-20260903.md 第八节行动2(🔴 唯一隔离缺口:客户侧知识未按 entity_id 分区) > 日期:2026-09-03 | 状态:评审稿(代码落地后由项目Bot独立复核) ## 一、调研实证(先查系统再决策) ### 1.1 生产库现状(2026-09-03 实测) | 表 | 行数 | entity_id | 写入路径 | 语义 | |---|---|---|---|---| | knowledge_events | **0** | ❌ 无 | 无任何写入路径(extract_events 只返回 dict 喂 prompt,不落库) | 企业事件记忆(设计态) | | knowledge_summaries | **0** | ❌ 无 | 仅 API 手动触发 /knowledge/generate/*(knowledge_service.generate_summary) | 企业知识摘要(设计态) | | knowledge_articles | **38** | ❌ 无 | seed_knowledge_articles.py / import_cma_knowledge.py(系统教学知识 seed) | CMA 教学知识库(term/formula/practice/方法论/案例) | ### 1.2 账套事实(纠正任务单假设) - **entity 1 = 陕西酣客文化传媒(白酒经销, active)** - **entity 2 = 陕西博海网络科技(IT服务, active)** - entity 3-5 = 测试/demo 企业 任务单写"博海=entity 1 兜底?酣客=entity 2?"与生产**相反**。实际无需"归属确认"到具体企业——因为见 1.3。 ### 1.3 存量归属结论(本次方案的关键决策点) 1. **knowledge_events / knowledge_summaries:零存量**(0 行)→ 无迁移负担。加列后新写入必须带 entity_id(从请求上下文解析)。 2. **knowledge_articles 38 行 = CMA 系统教学知识**(术语/公式/实务/方法论/CMA案例/税务合规等),**语义属于共享层**——对照架构决策文档: > 共享层(越大越好):判断模型/方法论/科目映射模式库/合规规则 > 隔离层(必须严格):企业数据/账套/权限/审计/记忆 教学文章不是"企业 A 的记忆 vs 企业 B 的记忆",而是"CMA 方法论知识库"(所有企业共享,越大越好)。 **→ 归属映射 = NULL = 全局共享**(不归酣客也不归博海)。 若硬性归 entity=1,则 entity 2(博海)登录后 CMA 知识库页/嵌入知识全空——破坏既有产品体验,也违背"共享层不重复"原则。 **附带推论**:任务验收"存量数据归属映射已确认"的答案 = 存量文章保持全局共享(entity_id NULL),只有未来企业自定义文章才进入企业抽屉。若任总/研学希望个别文章改为企业专属,一条 `UPDATE knowledge_articles SET entity_id=N WHERE id=...` 即可,成本可忽略。 ### 1.4 数据债边界(本次不扩,记录 follow-up) - **operation_logs 无 entity_id 列**(模型+库均无;写入点分布在 kpis/data/ai_suggestions/budget_generate/alert_rules/auth/templates 等 7+ 文件)。任务验收"审计:OperationLog 记录知识操作的企业归属"**无法在当前表结构下实现**——为 operation_logs 加 entity_id 属独立数据债(影响全部 7+ 写入点,需要全量排查 user→entity 归属,与 2026-08-31 安全修复同级工作量),本次不动,记为 P2 follow-up。 - 折衷:知识操作本身走 require_role + Depends(get_entity_id),操作发生在**已隔离的企业上下文内**;OperationLog.user_id → user_entities 可回溯归属。隔离缺口不因 operation_logs 无列而扩大(日志不是知识数据出口)。 ## 二、设计定稿 ### 2.1 三类语义 × 隔离策略 | 表 | entity_id 设计 | 查询语义 | 写入语义 | |---|---|---|---| | knowledge_events | `INT NOT NULL DEFAULT 1` + 索引 | 强制 `entity_id == 当前账套` | 事件抽取/记录必须从上下文带 entity_id | | knowledge_summaries | `INT NOT NULL DEFAULT 1` + 索引 | 强制 `entity_id == 当前账套` | generate 时从当前账套写入 | | knowledge_articles | `INT NULL`(NULL=全局共享)+ 索引 | `entity_id == 当前账套 OR entity_id IS NULL`(全局∪专属) | seed/import 默认 NULL=全局;未来企业专属文章带 entity_id | articles 的 OR NULL 是**有意为之**:教学知识留在共享层不被隔离吞掉,企业专属知识进抽屉严格隔离。越权核心验证对象是 events/summaries(企业记忆抽屉互不可见)+ articles 企业专属行互不可见。 ### 2.2 归属与隔离验证(对应任务验收"越权测试") - entity 1(酣客)上下文查不到 entity 2(博海)的 events/summaries → 404/空集 - entity 1 查不到 entity 2 的专属 article → 404(不暴露存在性) - 全局 article(NULL)两账套均可见 → 教学知识不丢 - 双实体实测记录见验收文档 ## 三、代码落地清单(评审通过后执行) 1. **models/knowledge.py**:KnowledgeEvent.entity_id / KnowledgeSummary.entity_id 加 `default=1, index=True` 2. **models/knowledge_article.py**:KnowledgeArticle.entity_id 加 `nullable=True, index=True`(NULL=全局共享) 3. **生产 DB ALTER**(MySQL,空表/小表零风险): ```sql ALTER TABLE knowledge_events ADD COLUMN entity_id INT NOT NULL DEFAULT 1 COMMENT '企业ID (多租户隔离 2026-09-03)', ADD INDEX ix_knowledge_events_entity_id (entity_id); ALTER TABLE knowledge_summaries ADD COLUMN entity_id INT NOT NULL DEFAULT 1 COMMENT '企业ID (多租户隔离 2026-09-03)', ADD INDEX ix_knowledge_summaries_entity_id (entity_id); ALTER TABLE knowledge_articles ADD COLUMN entity_id INT NULL DEFAULT NULL COMMENT '企业ID: NULL=全局共享, 有值=企业专属 (多租户隔离 2026-09-03)', ADD INDEX ix_knowledge_articles_entity_id (entity_id); ``` 4. **services/knowledge_service.py** 全链路: - `extract_events(db, since, until, entity_id)`:KPIAlert 直接按 entity_id 过滤;OperationLog 无 entity_id 列 → 经 user_id→user_entities 子查询过滤(user_id NULL 的日志跳过,标注临时方案) - `_kpi_snapshot(db, entity_id)`:KPIDefinition 加 entity 过滤(现状漏企业,跨企业 KPI 会被喂进对方摘要 prompt——真实泄漏点) - `generate_summary(..., entity_id)`:抽取+快照传 entity_id;KnowledgeSummary 写入 entity_id - `get_last_summary(db, level, entity_id)` / `run_daily|weekly|monthly_summary(db, entity_id)` / `generate_*_sync(db, entity_id)` 5. **api/knowledge.py**:8 个业务端点(summaries 列表/latest/详情、events、generate daily/weekly/monthly)加 `entity_id: int = Depends(get_entity_id)` 并强制过滤;get_summary 详情校验归属(非本企业 → 404)。ethics-quiz 为静态公共题库不动。 6. **api/knowledge_articles.py**:list/get 加 Depends(get_entity_id);过滤 `or_(entity_id == cur, entity_id.is_(None))`;get_article 企业专属行非本企业 → 404 7. **测试**:新增 test_security_knowledge_partition.py(双实体互查越权 + 全局文章双可见 + generate 自动带 entity_id,monkeypatch DeepSeek 免网络);pytest 全量回归 8. 前端零改动(token-first:登录用户 token 已绑 entity,拦截器不显式传 entity_id 时后端取 token 绑定值) ## 四、验证门禁 1. schema_check.py ORM↔DB 一致(加列后) 2. pytest 全量无新增失败(对照基线 726 passed / 4 failed(日历漂移) / 1 xfailed) 3. 双实体越权实测脚本(生产 E2E:登 entity1 查不到 entity2 摘要/文章、entity2 专属行 404、全局文章两可见) 4. 部署冒烟 5 项 ## 五、评审记录 研学复核(2026-09-03 23:4x):✅ 方案通过,决策点回执如下 - A. 账套映射(entity 1=陕西酣客 / 2=陕西博海):**采用**。研学已直查生产 entities 表确认 id=1 酣客(active)、id=2 博海(active)、3-5 demo,与生产一致。任务单原文为"博海=1?酣客=2?"属待确认留白,非断言,且存量归属为 NULL 全局共享,映射方向对数据零影响。 - B. knowledge_articles 38 篇 = CMA 教学知识 → entity_id NULL 全局共享:**同意**。对照架构决策"共享层越大越好/隔离层必须严格",教学知识库属共享层(非企业记忆),NULL=全局是正确语义;两账套可见保住既有产品体验(博海试用/酣客共用不丢知识)。个别文章转企业专属一条 UPDATE 即可。 - C. events/summaries 0 行:**确认**无存量归属问题。 - D. operation_logs 无 entity_id → P2 独立数据债:**同意记录不阻塞**。注意如实标注:任务验收"审计记录企业归属"项为**降级交付**(折衷=user_id→user_entities 回溯+知识操作发生于已隔离上下文),非完整实现,P2 follow-up 排期时补真列。 流程备注(供后续改进):验收⑤"先方案后代码"为弱通过——方案 22:56 与代码 22:57 同批产出、无评审等待期,靠项目Bot四层独立实测+UPDATE 级可回滚兜底。本次接受,后续同类任务要求方案产出后留评审间隔或经研学回执再动代码。 --- 产出物路径:本文档将随代码提交至 cma 仓库 docs/ + 归档 fullstack _reports/