Files
cma-management/docs/knowledge-entity-partition-plan-20260903.md
T

9.0 KiB
Raw Blame History

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,空表/小表零风险):
    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/