| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- import mimetypes
- from typing import List
- from urllib.parse import quote
- from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
- from fastapi.responses import StreamingResponse
- from sqlalchemy.orm import Session
- from app.core.bx_auth import BxUserContext, get_bx_current_user
- from app.core.database import get_db
- from app.core.tenant_context import get_tenant_no
- from app.schemas.bx.attachment import AttachmentOut
- from app.schemas.bx.common import ApiResponse
- from app.services.bx.attachment import AttachmentService
- from app.services.bx.storage import (
- download_file_bytes,
- get_preview_url,
- upload_bx_file,
- )
- router = APIRouter(prefix="/attachments", tags=["附件管理"])
- def _tenant(db: Session, user: BxUserContext) -> str:
- return get_tenant_no(db, user.to_tenant_user_dict())
- def _parse_id_list(ids: str) -> List[str]:
- return [x.strip() for x in (ids or "").split(",") if x.strip()]
- @router.post("/upload", response_model=ApiResponse[AttachmentOut])
- def upload_file(
- file: UploadFile = File(...),
- biz_type: str = "",
- biz_id: str = "0",
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- if not file or not file.filename:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
- file_data = file.file.read()
- content_type = (
- file.content_type
- or mimetypes.guess_type(file.filename or "")[0]
- or "application/octet-stream"
- )
- relative_path, _full_url = upload_bx_file(
- file_data, file.filename or "unknown", biz_type=biz_type or "attachment"
- )
- biz_id_value = str(biz_id).strip() if biz_id else "0"
- data = {
- "biz_type": biz_type or "",
- "biz_id": biz_id_value or "0",
- "file_name": file.filename or "unknown",
- "file_path": relative_path,
- "file_type": content_type,
- "file_size": len(file_data),
- }
- attachment = AttachmentService.create(db, data, user=user)
- db.commit()
- db.refresh(attachment)
- return ApiResponse(data=AttachmentOut.model_validate(attachment))
- @router.get("/download/{attachment_id}")
- def download_file(
- attachment_id: str,
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- tenant_no = _tenant(db, user)
- attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
- if not attachment:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
- try:
- file_data = download_file_bytes(attachment.file_path)
- except FileNotFoundError as exc:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在") from exc
- except Exception as exc:
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="文件读取失败") from exc
- encoded_name = quote(attachment.file_name or "download")
- headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}"}
- media_type = attachment.file_type or "application/octet-stream"
- return StreamingResponse(iter([file_data]), media_type=media_type, headers=headers)
- @router.delete("/{attachment_id}", response_model=ApiResponse[bool])
- def delete_attachment(
- attachment_id: str,
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- tenant_no = _tenant(db, user)
- if not AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user):
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
- success = AttachmentService.soft_delete(db, attachment_id, user=user)
- db.commit()
- return ApiResponse(data=success)
- @router.get("/preview/{attachment_id}", response_model=ApiResponse[dict])
- def preview_attachment(
- attachment_id: str,
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- """返回图片/附件预览 URL(MinIO 预签名或本地公开地址,对齐门户 storage 能力)。"""
- tenant_no = _tenant(db, user)
- attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
- if not attachment:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
- try:
- url = get_preview_url(attachment.file_path, expires_seconds=3600)
- except Exception as exc:
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="获取预览地址失败") from exc
- return ApiResponse(
- data={
- "url": url,
- "file_name": attachment.file_name,
- "file_type": attachment.file_type,
- }
- )
- @router.get("/list", response_model=ApiResponse[List[AttachmentOut]])
- def list_by_biz(
- biz_type: str = "",
- biz_id: str = "0",
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- tenant_no = _tenant(db, user)
- biz_id_value = str(biz_id).strip() if biz_id else "0"
- attachments = AttachmentService.get_by_biz(
- db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no, user=user
- )
- return ApiResponse(data=[AttachmentOut.model_validate(a) for a in attachments])
- @router.put("/bind-biz", response_model=ApiResponse[int])
- def bind_biz(
- ids: str = Query(...),
- biz_type: str = Query(...),
- biz_id: str = Query(...),
- db: Session = Depends(get_db),
- user: BxUserContext = Depends(get_bx_current_user),
- ):
- id_list = _parse_id_list(ids)
- biz_id_value = str(biz_id).strip() or "0"
- count = AttachmentService.bind_biz(db, id_list, biz_type, biz_id_value, user=user)
- db.commit()
- return ApiResponse(data=count)
|