fix(knowledge): articles共享层语义对齐评审方案——entity_id=0系统共享(替代NULL) (entity-memory-partition 复核对齐)
This commit is contained in:
@@ -37,17 +37,17 @@ def list_articles(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
entity_id: int = Depends(get_entity_id),
|
entity_id: int = Depends(get_entity_id),
|
||||||
):
|
):
|
||||||
"""查询知识文章(本企业专属 ∪ 全局共享),可按关联页面或分类筛选
|
"""查询知识文章(本企业专属 ∪ 系统共享),可按关联页面或分类筛选
|
||||||
|
|
||||||
隔离语义(2026-09-03 多租户分区):
|
隔离语义(2026-09-03 多租户分区):
|
||||||
- entity_id IS NULL → 全局共享(CMA 系统教学知识,所有企业可见)
|
- entity_id = 0 → 系统共享(CMA 教学知识,所有企业可见)
|
||||||
- entity_id == 当前账套 → 企业专属(只本企业可见)
|
- entity_id == 当前账套 → 企业专属(只本企业可见)
|
||||||
- 其他企业的专属文章不可见
|
- 其他企业的专属文章不可见
|
||||||
"""
|
"""
|
||||||
q = db.query(KnowledgeArticle).filter(
|
q = db.query(KnowledgeArticle).filter(
|
||||||
or_(
|
or_(
|
||||||
KnowledgeArticle.entity_id == entity_id,
|
KnowledgeArticle.entity_id == entity_id,
|
||||||
KnowledgeArticle.entity_id.is_(None),
|
KnowledgeArticle.entity_id == 0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if related_page:
|
if related_page:
|
||||||
@@ -72,6 +72,6 @@ def get_article(
|
|||||||
)
|
)
|
||||||
if not a:
|
if not a:
|
||||||
raise HTTPException(404, "文章不存在")
|
raise HTTPException(404, "文章不存在")
|
||||||
if a.entity_id is not None and a.entity_id != entity_id:
|
if a.entity_id != 0 and a.entity_id != entity_id:
|
||||||
raise HTTPException(404, "文章不存在")
|
raise HTTPException(404, "文章不存在")
|
||||||
return article_to_dict(a)
|
return article_to_dict(a)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class KnowledgeArticle(Base):
|
|||||||
__tablename__ = "knowledge_articles"
|
__tablename__ = "knowledge_articles"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
entity_id = Column(Integer, nullable=True, index=True, comment="企业ID: NULL=全局共享(系统教学), 有值=企业专属 (多租户隔离 2026-09-03)")
|
entity_id = Column(Integer, default=0, nullable=False, index=True, comment="企业ID: 0=系统共享(CMA教学知识), >0=企业专属 (多租户隔离 2026-09-03)")
|
||||||
title = Column(String(200), nullable=False, comment="文章标题")
|
title = Column(String(200), nullable=False, comment="文章标题")
|
||||||
summary = Column(String(500), nullable=True, comment="一句话摘要")
|
summary = Column(String(500), nullable=True, comment="一句话摘要")
|
||||||
content = Column(Text, nullable=False, comment="文章正文(支持Markdown)")
|
content = Column(Text, nullable=False, comment="文章正文(支持Markdown)")
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ for t in ['knowledge_events', 'knowledge_summaries', 'knowledge_articles']:
|
|||||||
cur.execute(f"SHOW INDEX FROM {t} WHERE Column_name='entity_id'")
|
cur.execute(f"SHOW INDEX FROM {t} WHERE Column_name='entity_id'")
|
||||||
has_idx = cur.fetchone() is not None
|
has_idx = cur.fetchone() is not None
|
||||||
check(f"{t}.entity_id 列+索引", has_col and has_idx)
|
check(f"{t}.entity_id 列+索引", has_col and has_idx)
|
||||||
# 存量全局文章(NULL)在加列后保持 38 篇
|
# 存量全局文章(entity_id=0 系统共享)在加列后保持 38 篇
|
||||||
cur.execute("SELECT COUNT(*) FROM knowledge_articles WHERE entity_id IS NULL")
|
cur.execute("SELECT COUNT(*) FROM knowledge_articles WHERE entity_id = 0")
|
||||||
g = cur.fetchone()[0]
|
g = cur.fetchone()[0]
|
||||||
print(f" 存量全局文章(NULL): {g} 篇")
|
print(f" 系统共享文章(entity_id=0): {g} 篇")
|
||||||
check("存量全局教学文章保持全局共享", g == 38, f"(期望38)")
|
check("存量系统共享教学文章保持", g == 38, f"(期望38)")
|
||||||
|
|
||||||
# 1. 插临时验证文章(标题带标记,source 专属 entity)
|
# 1. 插临时验证文章(标题带标记,source 专属 entity)
|
||||||
cur.execute("DELETE FROM knowledge_articles WHERE title LIKE %s", (f"%{MARK}%",))
|
cur.execute("DELETE FROM knowledge_articles WHERE title LIKE %s", (f"%{MARK}%",))
|
||||||
@@ -56,7 +56,7 @@ cur.execute(
|
|||||||
(2, f"{MARK}E2专属越权测试", "s", "c", "term"))
|
(2, f"{MARK}E2专属越权测试", "s", "c", "term"))
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO knowledge_articles (entity_id, title, summary, content, category) VALUES (%s,%s,%s,%s,%s)",
|
"INSERT INTO knowledge_articles (entity_id, title, summary, content, category) VALUES (%s,%s,%s,%s,%s)",
|
||||||
(None, f"{MARK}全局共享测试", "s", "c", "term"))
|
(0, f"{MARK}系统共享测试", "s", "c", "term"))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.execute("SELECT id, entity_id, title FROM knowledge_articles WHERE title LIKE %s", (f"%{MARK}%",))
|
cur.execute("SELECT id, entity_id, title FROM knowledge_articles WHERE title LIKE %s", (f"%{MARK}%",))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
@@ -98,13 +98,13 @@ if not t1 or not t2:
|
|||||||
st, body = api("GET", "/api/cma/knowledge-articles", token=t1)
|
st, body = api("GET", "/api/cma/knowledge-articles", token=t1)
|
||||||
d1 = _json.loads(body)
|
d1 = _json.loads(body)
|
||||||
titles1 = {i["title"] for i in d1.get("data", [])}
|
titles1 = {i["title"] for i in d1.get("data", [])}
|
||||||
check("E1 见全局共享文章", f"{MARK}全局共享测试" in titles1)
|
check("E1 见系统共享文章", f"{MARK}系统共享测试" in titles1)
|
||||||
check("E1 见自己专属文章", f"{MARK}E1专属越权测试" in titles1)
|
check("E1 见自己专属文章", f"{MARK}E1专属越权测试" in titles1)
|
||||||
check("E1 不见 E2 专属文章", f"{MARK}E2专属越权测试" not in titles1, "(核心越权1)")
|
check("E1 不见 E2 专属文章", f"{MARK}E2专属越权测试" not in titles1, "(核心越权1)")
|
||||||
# E2 视角
|
# E2 视角
|
||||||
st, body = api("GET", "/api/cma/knowledge-articles", token=t2)
|
st, body = api("GET", "/api/cma/knowledge-articles", token=t2)
|
||||||
titles2 = {i["title"] for i in _json.loads(body).get("data", [])}
|
titles2 = {i["title"] for i in _json.loads(body).get("data", [])}
|
||||||
check("E2 见全局共享文章", f"{MARK}全局共享测试" in titles2)
|
check("E2 见系统共享文章", f"{MARK}系统共享测试" in titles2)
|
||||||
check("E2 见自己专属文章", f"{MARK}E2专属越权测试" in titles2)
|
check("E2 见自己专属文章", f"{MARK}E2专属越权测试" in titles2)
|
||||||
check("E2 不见 E1 专属文章", f"{MARK}E1专属越权测试" not in titles2, "(核心越权2)")
|
check("E2 不见 E1 专属文章", f"{MARK}E1专属越权测试" not in titles2, "(核心越权2)")
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class TestKnowledgeArticlesGlobalUnion:
|
|||||||
def test_articles_global_union_scope(self, client: TestClient, db: Session):
|
def test_articles_global_union_scope(self, client: TestClient, db: Session):
|
||||||
"""文章查询 = 本企业专属 ∪ 全局共享;他企专属不可见"""
|
"""文章查询 = 本企业专属 ∪ 全局共享;他企专属不可见"""
|
||||||
_ensure_entity(db, 2)
|
_ensure_entity(db, 2)
|
||||||
g = KnowledgeArticle(title="全局教学文章", content="c", category="term", entity_id=None)
|
g = KnowledgeArticle(title="全局教学文章", content="c", category="term", entity_id=0)
|
||||||
e1 = KnowledgeArticle(title="E1专属文章", content="c", category="term", entity_id=1)
|
e1 = KnowledgeArticle(title="E1专属文章", content="c", category="term", entity_id=1)
|
||||||
e2 = KnowledgeArticle(title="E2专属文章", content="c", category="term", entity_id=2)
|
e2 = KnowledgeArticle(title="E2专属文章", content="c", category="term", entity_id=2)
|
||||||
db.add_all([g, e1, e2])
|
db.add_all([g, e1, e2])
|
||||||
@@ -166,7 +166,7 @@ class TestKnowledgeArticlesGlobalUnion:
|
|||||||
def test_get_article_cross_entity_404(self, client: TestClient, db: Session):
|
def test_get_article_cross_entity_404(self, client: TestClient, db: Session):
|
||||||
"""越权:entity1 直取 entity2 专属文章 → 404;全局文章两账套可取"""
|
"""越权:entity1 直取 entity2 专属文章 → 404;全局文章两账套可取"""
|
||||||
_ensure_entity(db, 2)
|
_ensure_entity(db, 2)
|
||||||
g = KnowledgeArticle(title="全局文章", content="c", entity_id=None)
|
g = KnowledgeArticle(title="全局文章", content="c", entity_id=0)
|
||||||
e2 = KnowledgeArticle(title="E2秘密文章", content="c", entity_id=2)
|
e2 = KnowledgeArticle(title="E2秘密文章", content="c", entity_id=2)
|
||||||
db.add_all([g, e2])
|
db.add_all([g, e2])
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user