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)