finai/backend/app/api/documents.py

335 lines
11 KiB
Python

from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, UploadFile
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db import get_db
from app.deps import get_current_user, require_admin
from app.integrations.adp_client import AdpClient
from app.integrations.storage_client import StorageClient
from app.models import Document, User
from app.schemas.document import DocumentCreate, DocumentResponse, DocumentUpdate
from app.services.audit_service import write_audit
from app.services.document_service import get_or_create_tags, serialize_document
router = APIRouter()
@router.get("", response_model=list[DocumentResponse])
def list_documents(db: Session = Depends(get_db), _: User = Depends(require_admin)) -> list[dict]:
documents = db.scalars(select(Document).order_by(Document.id.desc())).all()
return [serialize_document(document) for document in documents]
@router.get("/adp")
def list_adp_documents(
page: int = 1,
page_size: int = 50,
query: str = "",
cate_biz_id: str = "",
user: User = Depends(get_current_user),
) -> dict:
try:
result = AdpClient().list_documents(
page=page,
page_size=page_size,
query=query,
cate_biz_id=cate_biz_id,
)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP ListDoc 失败: {exc}")
is_admin = user.is_superuser or any(
role.code in {"admin", "system_admin"} for role in user.roles
)
if is_admin:
return result
# 从当前单位起向上收集单位名,用作匹配对象
unit_chain: list[str] = []
seen_ids: set[int] = set()
current_unit = user.unit
while current_unit is not None and current_unit.id not in seen_ids:
seen_ids.add(current_unit.id)
name = (current_unit.name or "").strip()
if name:
unit_chain.append(name)
current_unit = current_unit.parent
filtered: list[dict] = []
for doc in result.get("list") or []:
# 标签值先去掉"可见"再判断
bases: set[str] = set()
for attr in (doc.get("attr_labels") or []):
for label in (attr.get("labels") or []):
name = (label.get("label_name") or "").strip()
if not name:
continue
bases.add(name.replace("可见", ""))
if "所有人" in bases:
filtered.append(doc)
continue
matched = False
for unit_name in unit_chain:
if any(unit_name in base for base in bases):
matched = True
break
if matched:
filtered.append(doc)
return {"total": len(filtered), "list": filtered}
@router.get("/adp/categories")
def list_adp_categories(_: User = Depends(get_current_user)) -> dict:
try:
return {"list": AdpClient().list_categories()}
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP ListDocCate 失败: {exc}")
@router.post("/adp/categories")
def create_adp_category(
payload: dict = Body(...),
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
name = (payload.get("name") or "").strip()
parent_biz_id = (payload.get("parent_biz_id") or "").strip()
if not name:
raise HTTPException(status_code=400, detail="分类名称不能为空")
try:
result = AdpClient().create_category(name=name, parent_biz_id=parent_biz_id)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP CreateDocCate 失败: {exc}")
write_audit(
db,
"document.adp_cate_create",
actor_id=admin.id,
target_type="adp_category",
target_id=result.get("cate_biz_id") or "",
detail={"name": name, "parent_biz_id": parent_biz_id},
)
db.commit()
return result
@router.patch("/adp/categories/{cate_biz_id}")
def modify_adp_category(
cate_biz_id: str,
payload: dict = Body(...),
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
name = (payload.get("name") or "").strip()
if not name:
raise HTTPException(status_code=400, detail="分类名称不能为空")
try:
AdpClient().modify_category(cate_biz_id, name)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP ModifyDocCate 失败: {exc}")
write_audit(
db,
"document.adp_cate_modify",
actor_id=admin.id,
target_type="adp_category",
target_id=cate_biz_id,
detail={"name": name},
)
db.commit()
return {"cate_biz_id": cate_biz_id, "name": name}
@router.delete("/adp/categories/{cate_biz_id}")
def delete_adp_category(
cate_biz_id: str,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
try:
AdpClient().delete_category(cate_biz_id)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP DeleteDocCate 失败: {exc}")
write_audit(
db,
"document.adp_cate_delete",
actor_id=admin.id,
target_type="adp_category",
target_id=cate_biz_id,
)
db.commit()
return {"cate_biz_id": cate_biz_id, "deleted": True}
@router.delete("/adp/{doc_biz_id}")
def delete_from_adp(
doc_biz_id: str,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
if not doc_biz_id:
raise HTTPException(status_code=400, detail="缺少 DocBizId")
try:
AdpClient().delete_documents([doc_biz_id])
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP DeleteDoc 失败: {exc}")
write_audit(
db,
"document.adp_delete",
actor_id=admin.id,
target_type="adp_document",
target_id=doc_biz_id,
)
db.commit()
return {"doc_biz_id": doc_biz_id, "deleted": True}
@router.post("/adp/upload")
def upload_to_adp(
file: UploadFile = File(...),
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
if not file.filename:
raise HTTPException(status_code=400, detail="请选择文件")
file_bytes = file.file.read()
try:
result = AdpClient().upload_document(file.filename, file_bytes)
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=502, detail=f"调用 ADP SaveDoc 失败: {exc}")
if result.get("error_msg"):
raise HTTPException(status_code=400, detail=result["error_msg"])
write_audit(
db,
"document.adp_upload",
actor_id=admin.id,
target_type="adp_document",
target_id=str(result.get("doc_biz_id") or ""),
detail={"filename": file.filename, "size": len(file_bytes)},
)
db.commit()
return result
@router.post("", response_model=DocumentResponse)
def create_document(
payload: DocumentCreate,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
document = Document(
title=payload.title,
version=payload.version,
secrecy_level=payload.secrecy_level,
owner_dept=payload.owner_dept,
description=payload.description,
status="pending",
created_by_id=admin.id,
)
document.tags = get_or_create_tags(db, payload.tags)
db.add(document)
db.flush()
document.adp_doc_id = f"mock-adp-doc-{document.id}"
document.status = "parsed"
write_audit(db, "document.create", actor_id=admin.id, target_type="document", target_id=str(document.id))
db.commit()
db.refresh(document)
return serialize_document(document)
@router.post("/upload", response_model=DocumentResponse)
def upload_document(
file: UploadFile = File(...),
tags: str = Form(default=""),
secrecy_level: str = Form(default="internal"),
owner_dept: str = Form(default="财务部"),
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
if not file.filename:
raise HTTPException(status_code=400, detail="请选择文件")
storage_key = StorageClient().save_upload(file)
document = Document(
title=file.filename,
storage_key=storage_key,
secrecy_level=secrecy_level,
owner_dept=owner_dept,
status="pending",
created_by_id=admin.id,
)
tag_names = [tag.strip() for tag in tags.split(",") if tag.strip()]
document.tags = get_or_create_tags(db, tag_names)
db.add(document)
db.flush()
document.adp_doc_id = f"mock-adp-doc-{document.id}"
document.status = "parsed"
write_audit(
db,
"document.upload",
actor_id=admin.id,
target_type="document",
target_id=str(document.id),
detail={"filename": file.filename, "storage_key": storage_key},
)
db.commit()
db.refresh(document)
return serialize_document(document)
@router.patch("/{document_id}", response_model=DocumentResponse)
def update_document(
document_id: int,
payload: DocumentUpdate,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
) -> dict:
document = db.get(Document, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
for field in ("title", "version", "secrecy_level", "status", "owner_dept", "description"):
value = getattr(payload, field)
if value is not None:
setattr(document, field, value)
if payload.tags is not None:
document.tags = get_or_create_tags(db, payload.tags)
write_audit(db, "document.update", actor_id=admin.id, target_type="document", target_id=str(document.id))
db.commit()
db.refresh(document)
return serialize_document(document)
@router.delete("/{document_id}")
def delete_document(document_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin)) -> dict:
document = db.get(Document, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
document.status = "deleted"
write_audit(db, "document.delete", actor_id=admin.id, target_type="document", target_id=str(document.id))
db.commit()
return {"message": "文档已删除"}
@router.post("/{document_id}/retry-parse", response_model=DocumentResponse)
def retry_parse(document_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin)) -> dict:
document = db.get(Document, document_id)
if not document:
raise HTTPException(status_code=404, detail="文档不存在")
document.status = "parsed"
write_audit(db, "document.retry_parse", actor_id=admin.id, target_type="document", target_id=str(document.id))
db.commit()
db.refresh(document)
return serialize_document(document)