Files
shanxia2.0/backend/app/api/bx/attachment.py
T
2026-06-07 00:40:01 +08:00

152 lines
5.6 KiB
Python

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])
async 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 = await 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)
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):
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)
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)
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)