feat: implement backend knowledge management

This commit is contained in:
2026-10-15 19:24:21 +08:00
parent b6adddee42
commit 6eb1d12580
33 changed files with 1604 additions and 55 deletions
+120
View File
@@ -0,0 +1,120 @@
from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy import func, inspect, select
from sqlalchemy.orm import Session
from app.models import AuditEvent, Document, KnowledgeItem, Source
from app.models.catalog import utc_now
def get_item(db: Session, model, item_id: int, *, include_deleted=False, lock=False):
if lock:
item = db.scalar(
select(model)
.where(model.id == item_id)
.with_for_update()
.execution_options(populate_existing=True)
)
else:
item = db.get(model, item_id)
if item is None or (item.is_deleted and not include_deleted):
raise HTTPException(404, "记录不存在")
return item
def ensure_version(item, expected_version: int):
if item.version != expected_version:
raise HTTPException(409, "记录已更新,请刷新后重试")
def values(item) -> dict:
return jsonable_encoder(
{column.key: getattr(item, column.key) for column in inspect(type(item)).columns}
)
def record_event(db: Session, item, action: str, *, actor=None, notes=""):
db.flush()
snapshot = {"item": values(item)}
if isinstance(item, KnowledgeItem):
source = db.get(Source, item.source_id) if item.source_id else None
document = db.get(Document, item.document_id) if item.document_id else None
snapshot["source"] = values(source) if source else None
snapshot["document"] = values(document) if document else None
db.add(
AuditEvent(
entity_type=item.__tablename__,
entity_id=item.id,
action=action,
actor=actor,
notes=notes,
snapshot=snapshot,
)
)
def update_values(item, payload):
data = payload.model_dump(mode="json", exclude_unset=True, exclude={"expected_version"})
data.update(
{
key: value
for key, value in payload.model_dump(exclude_unset=True).items()
if key.endswith("_at")
}
)
for name, value in data.items():
setattr(item, name, value)
item.updated_at = utc_now()
def reset_review(item: KnowledgeItem):
item.verification_status = "pending"
item.reviewer = None
item.reviewed_at = None
item.verification_notes = None
item.updated_at = utc_now()
def invalidate_knowledge(db: Session, condition, *, source_url=None, update_url=False):
for item in db.scalars(
select(KnowledgeItem).where(condition, KnowledgeItem.is_deleted.is_(False))
):
reset_review(item)
if update_url:
item.source_url = source_url
record_event(db, item, "invalidate", notes="来源或文档已变更,需要重新核验")
def resolve_knowledge_links(db: Session, item: KnowledgeItem):
# 先锁依据记录再保存条目,避免引用与父记录修改/删除同时发生。
with db.no_autoflush:
if item.document_id is not None:
document = get_item(db, Document, item.document_id, lock=True)
if item.source_id is not None and item.source_id != document.source_id:
raise HTTPException(422, "文档与来源不一致")
item.source_id = document.source_id
if item.source_id is not None:
source = get_item(db, Source, item.source_id, lock=True)
if item.source_url and item.source_url != source.source_url:
raise HTTPException(422, "来源网址与登记来源不一致")
item.source_url = source.source_url
def page_result(db: Session, statement, page: int, page_size: int):
total = db.scalar(select(func.count()).select_from(statement.order_by(None).subquery()))
return {
"items": list(db.scalars(statement.offset((page - 1) * page_size).limit(page_size))),
"total": total,
"page": page,
"page_size": page_size,
}
def history(db: Session, model, item_id: int, page: int, page_size: int):
get_item(db, model, item_id, include_deleted=True)
statement = (
select(AuditEvent)
.where(AuditEvent.entity_type == model.__tablename__, AuditEvent.entity_id == item_id)
.order_by(AuditEvent.id.desc())
)
return page_result(db, statement, page, page_size)