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

103 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/