fix: 补齐知识库元数据管理与生命周期校验

This commit is contained in:
2026-07-12 15:30:23 +08:00
parent c3639fe30d
commit 8266d845c0
13 changed files with 640 additions and 59 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.database import get_db
@@ -24,6 +25,7 @@ from app.models.knowledge import (
from app.schemas.admin import KnowledgeSaveRequest
from app.services.admin_service import OperationLogService
from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_catalog_cache_service import KnowledgeCatalogCacheService
router = APIRouter()
@@ -40,6 +42,7 @@ def import_knowledge_from_space(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
node_token = payload.get("node_token", "").strip()
if not node_token:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="NodeToken 不能为空")
@@ -61,7 +64,7 @@ def import_knowledge_from_space(
name=info.get("title", node_token),
feishu_space_id=space_id,
feishu_node_id=node_token,
status=1,
status=0,
knowledge_type="general",
review_mode="manual",
remark=f"obj_type={info.get('obj_type', '')}",
@@ -71,8 +74,13 @@ def import_knowledge_from_space(
OperationLogService.write(
db, admin_id=current_admin.id, module="knowledge", action="import", target_id=knowledge.id
)
db.commit()
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该飞书文章已创建知识库") from exc
db.refresh(knowledge)
KnowledgeCatalogCacheService.invalidate()
return api_success({"imported": 1, "skipped": 0, "id": knowledge.id, "name": knowledge.name})
@@ -82,11 +90,20 @@ def create_knowledge(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
existing = db.scalar(
select(Knowledge).where(
Knowledge.feishu_space_id == payload.feishuSpaceId,
Knowledge.feishu_node_id == payload.feishuNodeId,
)
)
if existing is not None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该飞书文章已创建知识库")
knowledge = Knowledge(
name=payload.name,
feishu_space_id=payload.feishuSpaceId,
feishu_node_id=payload.feishuNodeId,
status=payload.status,
status=0,
remark=payload.remark,
knowledge_type=payload.knowledgeType,
review_mode=payload.reviewMode,
@@ -94,8 +111,13 @@ def create_knowledge(
db.add(knowledge)
db.flush()
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="create", target_id=knowledge.id)
db.commit()
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该飞书文章已创建知识库") from exc
db.refresh(knowledge)
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_dict(knowledge))
@@ -106,11 +128,9 @@ def update_knowledge(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _get_knowledge(db, knowledge_id)
knowledge.name = payload.name
knowledge.feishu_space_id = payload.feishuSpaceId
knowledge.feishu_node_id = payload.feishuNodeId
knowledge.status = payload.status
knowledge.remark = payload.remark
knowledge.knowledge_type = payload.knowledgeType
knowledge.review_mode = payload.reviewMode
@@ -118,6 +138,7 @@ def update_knowledge(
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="update", target_id=knowledge.id)
db.commit()
db.refresh(knowledge)
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_dict(knowledge))
@@ -127,7 +148,13 @@ def delete_knowledge(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _get_knowledge(db, knowledge_id)
if knowledge.lifecycle_status != "archived" or knowledge.status != 0:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="物理删除前必须先关闭并归档知识库",
)
# 清理早期按用户授权口径留下的历史关联,避免外键阻止知识库删除。
permissions = db.scalars(
select(UserKnowledgePermission).where(UserKnowledgePermission.knowledge_id == knowledge.id)
@@ -146,6 +173,7 @@ def delete_knowledge(
db.delete(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success()
@@ -156,6 +184,11 @@ def _get_knowledge(db: Session, knowledge_id: int) -> Knowledge:
return knowledge
def _require_super_admin(admin: Admin) -> None:
if admin.role is not None and admin.role.code != "super_admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅超级管理员可以管理知识库")
def _knowledge_dict(knowledge: Knowledge) -> dict:
return {
"id": knowledge.id,

View File

@@ -21,6 +21,8 @@ from app.models.knowledge import (
)
from app.schemas.knowledge import (
KnowledgeLifecycleRequest,
KnowledgeBatchMetadataUpdateRequest,
KnowledgeMetadataUpdateRequest,
KnowledgePublishRequest,
KnowledgeReviewRequest,
KnowledgeStatusRequest,
@@ -28,10 +30,74 @@ from app.schemas.knowledge import (
)
from app.services.admin_service import OperationLogService
from app.services.knowledge_pipeline_service import KnowledgePipelineService
from app.services.knowledge_catalog_cache_service import KnowledgeCatalogCacheService
router = APIRouter()
@router.patch("/knowledge/batch/metadata")
def batch_update_metadata(
payload: KnowledgeBatchMetadataUpdateRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
if payload.knowledgeType is None and payload.reviewMode is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请至少选择一个待修改字段")
knowledge_ids = list(dict.fromkeys(payload.knowledgeIds))
rows = {
item.id: item
for item in db.scalars(select(Knowledge).where(Knowledge.id.in_(knowledge_ids))).all()
}
results: list[dict] = []
for knowledge_id in knowledge_ids:
item = rows.get(knowledge_id)
if item is None:
results.append({"knowledgeId": knowledge_id, "ok": False, "error": "知识库不存在"})
continue
if payload.knowledgeType is not None:
item.knowledge_type = payload.knowledgeType
if payload.reviewMode is not None:
item.review_mode = payload.reviewMode
db.add(item)
OperationLogService.write(
db,
admin_id=current_admin.id,
module="knowledge",
action="batch_update_metadata",
target_id=item.id,
)
results.append({"knowledgeId": knowledge_id, "ok": True})
db.commit()
KnowledgeCatalogCacheService.invalidate()
success = sum(1 for item in results if item["ok"])
return api_success(
{"total": len(results), "success": success, "failed": len(results) - success, "results": results}
)
@router.patch("/knowledge/{knowledge_id}/metadata")
def update_metadata(
knowledge_id: int,
payload: KnowledgeMetadataUpdateRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
knowledge.name = payload.name.strip()
knowledge.knowledge_type = payload.knowledgeType
knowledge.review_mode = payload.reviewMode
db.add(knowledge)
OperationLogService.write(
db, admin_id=current_admin.id, module="knowledge", action="update_metadata", target_id=knowledge.id
)
db.commit()
db.refresh(knowledge)
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_detail(knowledge))
@router.get("/knowledge/{knowledge_id}/detail")
def detail(
knowledge_id: int,
@@ -159,6 +225,7 @@ async def synchronize(
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"知识同步失败:{exc}") from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="sync", target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success(_job_dict(job))
@@ -171,6 +238,8 @@ def confirm_manifest(
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
if knowledge.pending_version_id != version.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="只能确认当前待发布版本的说明卡")
try:
KnowledgePipelineService.confirm_manifest(db, knowledge, version, current_admin.id)
except ValueError as exc:
@@ -190,6 +259,8 @@ def review(
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
if knowledge.pending_version_id != version.id or version.status != "pending_review":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="只能审核当前待审核版本")
if version.quality_status == "blocked" and payload.action == "approve":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="自动质检存在阻断项,不能审核通过")
KnowledgePipelineService.review_version(db, knowledge, version, current_admin.id, payload.action, payload.reason)
@@ -208,12 +279,15 @@ def publish(
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
if knowledge.pending_version_id != version.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="只能发布当前待发布版本")
try:
KnowledgePipelineService.publish_version(db, knowledge, version, current_admin.id, payload.keepOpen)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="publish", target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_detail(knowledge))
@@ -227,12 +301,17 @@ def rollback(
) -> dict:
_require_super_admin(current_admin)
knowledge, version = _knowledge_version(db, knowledge_id, version_id)
if knowledge.current_version_id == version.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="目标版本已是当前正式版本")
if payload.keepOpen and knowledge.lifecycle_status != "active":
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="归档知识库不能在回滚后开放")
try:
KnowledgePipelineService.rollback(db, knowledge, version, current_admin.id, payload.keepOpen)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="rollback", target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_detail(knowledge))
@@ -258,6 +337,7 @@ def open_status(
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="open" if payload.open else "close", target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_detail(knowledge))
@@ -270,12 +350,15 @@ def lifecycle(
) -> dict:
_require_super_admin(current_admin)
knowledge = _knowledge(db, knowledge_id)
if knowledge.lifecycle_status == payload.status:
return api_success(_knowledge_detail(knowledge))
knowledge.lifecycle_status = payload.status
if payload.status == "archived":
knowledge.status = 0
db.add(knowledge)
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action=payload.status, target_id=knowledge.id)
db.commit()
KnowledgeCatalogCacheService.invalidate()
return api_success(_knowledge_detail(knowledge))
@@ -347,6 +430,8 @@ def _knowledge_detail(item: Knowledge) -> dict:
return {
"id": item.id,
"name": item.name,
"status": item.status,
"remark": item.remark,
"sourceTitle": item.source_title,
"feishuSpaceId": item.feishu_space_id,
"feishuNodeId": item.feishu_node_id,
@@ -362,6 +447,8 @@ def _knowledge_detail(item: Knowledge) -> dict:
"lastSyncedAt": item.last_synced_at,
"lastPublishedAt": item.last_published_at,
"migrationStatus": item.migration_status,
"createdAt": item.created_at,
"updatedAt": item.updated_at,
}