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)