154 lines
5.6 KiB
Python
154 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])
|
|
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)
|