attachment.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. """附件管理 API"""
  2. from typing import Optional, List
  3. from fastapi import APIRouter, Depends, Query, UploadFile, File, Form
  4. from sqlalchemy.orm import Session
  5. from app.core.database import get_db
  6. from app.core.auth import get_current_user
  7. from app.core.response import success_response, error_response
  8. from app.crud.attachment import attachment_crud
  9. from app.core.storage import storage
  10. router = APIRouter(prefix="/api/web/admin/attachment", tags=["附件管理"])
  11. # 业务类型常量
  12. BUSINESS_TYPES = {
  13. "delivery_pickup": "配送取货凭证",
  14. "delivery_deliver": "配送送达凭证",
  15. "delivery_prepare": "备货完成凭证",
  16. "settlement": "结算文件",
  17. "order": "订单附件",
  18. "inquiry": "询价附件",
  19. "quotation": "报价附件",
  20. "other": "其他"
  21. }
  22. @router.get("/list", response_model=dict)
  23. def list_attachments(
  24. business_type: Optional[str] = Query(None, description="业务类型"),
  25. business_id: Optional[str] = Query(None, description="业务ID"),
  26. page: int = Query(1, ge=1, description="页码"),
  27. pageSize: int = Query(20, ge=1, le=100, description="每页条数"),
  28. db: Session = Depends(get_db),
  29. current_user: dict = Depends(get_current_user)
  30. ):
  31. """获取附件列表"""
  32. from app.models.models import BizAttachment
  33. from sqlalchemy import func
  34. query = db.query(BizAttachment).filter(BizAttachment.is_del == 0)
  35. if business_type:
  36. query = query.filter(BizAttachment.business_type == business_type)
  37. if business_id:
  38. query = query.filter(BizAttachment.business_id == business_id)
  39. total = query.count()
  40. skip = (page - 1) * pageSize
  41. attachments = query.order_by(BizAttachment.create_at.desc()).offset(skip).limit(pageSize).all()
  42. return success_response({
  43. "list": [{
  44. "id": att.id,
  45. "businessType": att.business_type,
  46. "businessTypeText": BUSINESS_TYPES.get(att.business_type, att.business_type),
  47. "businessId": att.business_id,
  48. "fileName": att.file_name,
  49. "fileUrl": att.file_url,
  50. "fileSize": att.file_size,
  51. "fileType": att.file_type,
  52. "uploadBy": att.upload_by,
  53. "uploadByName": att.upload_by_name,
  54. "createAt": str(att.create_at) if att.create_at else None
  55. } for att in attachments],
  56. "total": total,
  57. "page": page,
  58. "pageSize": pageSize
  59. })
  60. @router.get("/business/{business_type}/{business_id}", response_model=dict)
  61. def get_business_attachments(
  62. business_type: str,
  63. business_id: str,
  64. db: Session = Depends(get_db),
  65. current_user: dict = Depends(get_current_user)
  66. ):
  67. """获取指定业务的附件列表"""
  68. attachments = attachment_crud.get_by_business(db, business_type, business_id)
  69. return success_response([{
  70. "id": att.id,
  71. "fileName": att.file_name,
  72. "fileUrl": att.file_url,
  73. "fileSize": att.file_size,
  74. "fileType": att.file_type,
  75. "uploadByName": att.upload_by_name,
  76. "createAt": str(att.create_at) if att.create_at else None
  77. } for att in attachments])
  78. @router.post("/upload", response_model=dict)
  79. async def upload_attachment(
  80. business_type: str = Form(..., description="业务类型"),
  81. business_id: Optional[str] = Form(None, description="业务ID"),
  82. file: UploadFile = File(..., description="上传文件"),
  83. db: Session = Depends(get_db),
  84. current_user: dict = Depends(get_current_user)
  85. ):
  86. """上传附件"""
  87. try:
  88. allowed_image = frozenset({
  89. "image/jpeg", "image/png", "image/gif", "image/webp",
  90. "image/heic", "image/heif",
  91. })
  92. allowed_video = frozenset({
  93. "video/mp4", "video/quicktime", "video/webm", "video/x-msvideo",
  94. })
  95. allowed_doc = frozenset({
  96. "application/pdf", "application/msword",
  97. "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  98. })
  99. allowed_types = allowed_image | allowed_video | allowed_doc
  100. ct = (file.content_type or "").strip().lower() or "application/octet-stream"
  101. if ct not in allowed_types:
  102. return error_response(400, f"不支持的文件类型: {file.content_type}")
  103. # 读取文件内容
  104. content = await file.read()
  105. def _max_bytes_for(content_type: str) -> int:
  106. if content_type.startswith("video/"):
  107. return 100 * 1024 * 1024
  108. if content_type.startswith("image/"):
  109. return 10 * 1024 * 1024
  110. return 20 * 1024 * 1024
  111. max_bytes = _max_bytes_for(ct)
  112. if len(content) > max_bytes:
  113. label = "视频" if ct.startswith("video/") else ("图片" if ct.startswith("image/") else "文件")
  114. mb = max_bytes // (1024 * 1024)
  115. return error_response(400, f"{label}大小不能超过 {mb}MB")
  116. # 上传到存储
  117. folder = f"attachments/{business_type}"
  118. file_url = storage.upload_file(content, file.filename or "file", folder)
  119. # 创建附件记录
  120. from app.core.utils import get_file_size_display
  121. attachment = attachment_crud.create_attachment(
  122. db=db,
  123. business_type=business_type,
  124. business_id=business_id,
  125. file_name=file.filename or "file",
  126. file_url=file_url,
  127. file_size=len(content),
  128. file_type=file.content_type,
  129. upload_by=current_user.get("user_id"),
  130. upload_by_name=current_user.get("real_name", current_user.get("username", ""))
  131. )
  132. return success_response({
  133. "id": attachment.id,
  134. "fileName": attachment.file_name,
  135. "fileUrl": attachment.file_url,
  136. "fileSize": attachment.file_size,
  137. "fileSizeDisplay": get_file_size_display(attachment.file_size),
  138. "fileType": attachment.file_type
  139. })
  140. except Exception as e:
  141. return error_response(500, f"上传失败: {str(e)}")
  142. @router.delete("/{attachment_id}", response_model=dict)
  143. def delete_attachment(
  144. attachment_id: str,
  145. db: Session = Depends(get_db),
  146. current_user: dict = Depends(get_current_user)
  147. ):
  148. """删除附件"""
  149. attachment = attachment_crud.get(db, attachment_id)
  150. if not attachment:
  151. return error_response(404, "附件不存在")
  152. if attachment.is_del == 1:
  153. return error_response(404, "附件已删除")
  154. # 软删除
  155. attachment_crud.soft_delete(db, attachment_id)
  156. return success_response({"message": "删除成功"})
  157. @router.get("/business-types", response_model=dict)
  158. def get_business_types(
  159. current_user: dict = Depends(get_current_user)
  160. ):
  161. """获取业务类型列表"""
  162. return success_response([{
  163. "value": key,
  164. "label": value
  165. } for key, value in BUSINESS_TYPES.items()])