attachment.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import mimetypes
  2. from typing import List
  3. from urllib.parse import quote
  4. from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
  5. from fastapi.responses import StreamingResponse
  6. from sqlalchemy.orm import Session
  7. from app.core.bx_auth import BxUserContext, get_bx_current_user
  8. from app.core.database import get_db
  9. from app.core.tenant_context import get_tenant_no
  10. from app.schemas.bx.attachment import AttachmentOut
  11. from app.schemas.bx.common import ApiResponse
  12. from app.services.bx.attachment import AttachmentService
  13. from app.services.bx.storage import (
  14. download_file_bytes,
  15. get_preview_url,
  16. upload_bx_file,
  17. )
  18. router = APIRouter(prefix="/attachments", tags=["附件管理"])
  19. def _tenant(db: Session, user: BxUserContext) -> str:
  20. return get_tenant_no(db, user.to_tenant_user_dict())
  21. def _parse_id_list(ids: str) -> List[str]:
  22. return [x.strip() for x in (ids or "").split(",") if x.strip()]
  23. @router.post("/upload", response_model=ApiResponse[AttachmentOut])
  24. def upload_file(
  25. file: UploadFile = File(...),
  26. biz_type: str = "",
  27. biz_id: str = "0",
  28. db: Session = Depends(get_db),
  29. user: BxUserContext = Depends(get_bx_current_user),
  30. ):
  31. if not file or not file.filename:
  32. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
  33. file_data = file.file.read()
  34. content_type = (
  35. file.content_type
  36. or mimetypes.guess_type(file.filename or "")[0]
  37. or "application/octet-stream"
  38. )
  39. relative_path, _full_url = upload_bx_file(
  40. file_data, file.filename or "unknown", biz_type=biz_type or "attachment"
  41. )
  42. biz_id_value = str(biz_id).strip() if biz_id else "0"
  43. data = {
  44. "biz_type": biz_type or "",
  45. "biz_id": biz_id_value or "0",
  46. "file_name": file.filename or "unknown",
  47. "file_path": relative_path,
  48. "file_type": content_type,
  49. "file_size": len(file_data),
  50. }
  51. attachment = AttachmentService.create(db, data, user=user)
  52. db.commit()
  53. db.refresh(attachment)
  54. return ApiResponse(data=AttachmentOut.model_validate(attachment))
  55. @router.get("/download/{attachment_id}")
  56. def download_file(
  57. attachment_id: str,
  58. db: Session = Depends(get_db),
  59. user: BxUserContext = Depends(get_bx_current_user),
  60. ):
  61. tenant_no = _tenant(db, user)
  62. attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
  63. if not attachment:
  64. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
  65. try:
  66. file_data = download_file_bytes(attachment.file_path)
  67. except FileNotFoundError as exc:
  68. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在") from exc
  69. except Exception as exc:
  70. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="文件读取失败") from exc
  71. encoded_name = quote(attachment.file_name or "download")
  72. headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}"}
  73. media_type = attachment.file_type or "application/octet-stream"
  74. return StreamingResponse(iter([file_data]), media_type=media_type, headers=headers)
  75. @router.delete("/{attachment_id}", response_model=ApiResponse[bool])
  76. def delete_attachment(
  77. attachment_id: str,
  78. db: Session = Depends(get_db),
  79. user: BxUserContext = Depends(get_bx_current_user),
  80. ):
  81. tenant_no = _tenant(db, user)
  82. if not AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user):
  83. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
  84. success = AttachmentService.soft_delete(db, attachment_id, user=user)
  85. db.commit()
  86. return ApiResponse(data=success)
  87. @router.get("/preview/{attachment_id}", response_model=ApiResponse[dict])
  88. def preview_attachment(
  89. attachment_id: str,
  90. db: Session = Depends(get_db),
  91. user: BxUserContext = Depends(get_bx_current_user),
  92. ):
  93. """返回图片/附件预览 URL(MinIO 预签名或本地公开地址,对齐门户 storage 能力)。"""
  94. tenant_no = _tenant(db, user)
  95. attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
  96. if not attachment:
  97. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
  98. try:
  99. url = get_preview_url(attachment.file_path, expires_seconds=3600)
  100. except Exception as exc:
  101. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="获取预览地址失败") from exc
  102. return ApiResponse(
  103. data={
  104. "url": url,
  105. "file_name": attachment.file_name,
  106. "file_type": attachment.file_type,
  107. }
  108. )
  109. @router.get("/list", response_model=ApiResponse[List[AttachmentOut]])
  110. def list_by_biz(
  111. biz_type: str = "",
  112. biz_id: str = "0",
  113. db: Session = Depends(get_db),
  114. user: BxUserContext = Depends(get_bx_current_user),
  115. ):
  116. tenant_no = _tenant(db, user)
  117. biz_id_value = str(biz_id).strip() if biz_id else "0"
  118. attachments = AttachmentService.get_by_biz(
  119. db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no, user=user
  120. )
  121. return ApiResponse(data=[AttachmentOut.model_validate(a) for a in attachments])
  122. @router.put("/bind-biz", response_model=ApiResponse[int])
  123. def bind_biz(
  124. ids: str = Query(...),
  125. biz_type: str = Query(...),
  126. biz_id: str = Query(...),
  127. db: Session = Depends(get_db),
  128. user: BxUserContext = Depends(get_bx_current_user),
  129. ):
  130. id_list = _parse_id_list(ids)
  131. biz_id_value = str(biz_id).strip() or "0"
  132. count = AttachmentService.bind_biz(db, id_list, biz_type, biz_id_value, user=user)
  133. db.commit()
  134. return ApiResponse(data=count)