From d2bad4417373b24fbbdb040e932ea320987e5ddd Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 2 Apr 2026 09:05:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20KB=20=E6=9E=B6=E6=A7=8B=E5=AF=A9?= =?UTF-8?q?=E6=9F=A5=E4=BF=AE=E5=BE=A9=20I3-I5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - I3: Service 層加 IKnowledgeRepository Protocol 型別標注 - I4: search 方法加入 tags JSONB 搜尋 (cast→String→ilike) - I5: get_categories 獨立方法,不再繞道 list_entries(limit=0) 首席架構師審查 87/100 → 全部 Important issues 已修復 Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/api/v1/knowledge.py | 4 ++-- .../src/repositories/knowledge_repository.py | 5 +++-- apps/api/src/services/knowledge_service.py | 22 +++++++++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/api/src/api/v1/knowledge.py b/apps/api/src/api/v1/knowledge.py index db72faf1..4aa78198 100644 --- a/apps/api/src/api/v1/knowledge.py +++ b/apps/api/src/api/v1/knowledge.py @@ -71,8 +71,8 @@ async def search_entries( async def get_categories() -> list[dict]: """取得分類樹 (含各類數量)""" service = get_knowledge_service() - result = await service.list_entries(limit=0) - return [cat.model_dump() for cat in result.categories] + cats = await service.get_categories() + return [cat.model_dump() for cat in cats] @router.get("/{entry_id}", response_model=KnowledgeEntry) diff --git a/apps/api/src/repositories/knowledge_repository.py b/apps/api/src/repositories/knowledge_repository.py index 3844ebbf..dbb67179 100644 --- a/apps/api/src/repositories/knowledge_repository.py +++ b/apps/api/src/repositories/knowledge_repository.py @@ -12,7 +12,7 @@ Knowledge Base Phase 1: CRUD + 搜尋 """ import structlog -from sqlalchemy import func, or_, select, update +from sqlalchemy import String, func, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from src.db.models import KnowledgeEntryRecord @@ -154,7 +154,7 @@ class KnowledgeDBRepository: return [(row.category, row.cnt) for row in result.all()] async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]: - """關鍵字搜尋 (title + content)""" + """關鍵字搜尋 (title + content + tags)""" like_q = f"%{query}%" result = await self.db.execute( select(KnowledgeEntryRecord) @@ -163,6 +163,7 @@ class KnowledgeDBRepository: or_( KnowledgeEntryRecord.title.ilike(like_q), KnowledgeEntryRecord.content.ilike(like_q), + KnowledgeEntryRecord.tags.cast(String).ilike(like_q), ), ) .order_by(KnowledgeEntryRecord.view_count.desc()) diff --git a/apps/api/src/services/knowledge_service.py b/apps/api/src/services/knowledge_service.py index 73ca199c..6d834e35 100644 --- a/apps/api/src/services/knowledge_service.py +++ b/apps/api/src/services/knowledge_service.py @@ -24,6 +24,7 @@ from src.models.knowledge import ( KnowledgeEntryUpdate, KnowledgeListResponse, ) +from src.repositories.interfaces import IKnowledgeRepository from src.repositories.knowledge_repository import KnowledgeDBRepository logger = structlog.get_logger(__name__) @@ -49,7 +50,7 @@ class KnowledgeService: async def create_entry(self, data: KnowledgeEntryCreate) -> KnowledgeEntry: """建立知識條目""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) entry = await repo.create(data) logger.info( "knowledge_entry_created", @@ -62,7 +63,7 @@ class KnowledgeService: async def get_entry(self, entry_id: str) -> KnowledgeEntry | None: """取得知識條目 (view_count +1)""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) entry = await repo.get_by_id(entry_id) if entry: await repo.increment_view_count(entry_id) @@ -75,7 +76,7 @@ class KnowledgeService: """更新知識條目""" update_data = data.model_dump(exclude_none=True) async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) if not update_data: return await repo.get_by_id(entry_id) return await repo.update(entry_id, update_data) @@ -83,7 +84,7 @@ class KnowledgeService: async def approve_entry(self, entry_id: str) -> KnowledgeEntry | None: """審核通過 (draft/review → approved)""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) entry = await repo.get_by_id(entry_id) if not entry: return None @@ -94,7 +95,7 @@ class KnowledgeService: async def archive_entry(self, entry_id: str) -> bool: """封存 (軟刪除)""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) return await repo.delete(entry_id) async def list_entries( @@ -109,7 +110,7 @@ class KnowledgeService: ) -> KnowledgeListResponse: """列出知識條目 + 分類統計""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) items, total = await repo.list_entries( category=category, entry_type=entry_type, @@ -127,8 +128,15 @@ class KnowledgeService: items=items, total=total, categories=categories ) + async def get_categories(self) -> list[CategoryCount]: + """取得分類統計(直接呼叫 repo,不走 list_entries)""" + async with get_db_context() as db: + repo: IKnowledgeRepository = KnowledgeDBRepository(db) + categories_raw = await repo.get_categories() + return [CategoryCount(category=cat, count=cnt) for cat, cnt in categories_raw] + async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]: """關鍵字搜尋""" async with get_db_context() as db: - repo = KnowledgeDBRepository(db) + repo: IKnowledgeRepository = KnowledgeDBRepository(db) return await repo.search(query, limit)