feat: implement backend knowledge management
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user