121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
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)
|