166 lines
7.6 KiB
Python
166 lines
7.6 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.app.core.config import get_settings
|
|
from backend.app.core.error_codes import ErrorCode
|
|
from backend.app.core.exceptions import AppException
|
|
from backend.app.repositories.file_repository import FileRepository
|
|
from backend.app.services.audit_service import audit_service
|
|
from backend.app.services.storage_service import storage_service
|
|
|
|
|
|
class FileService:
|
|
def __init__(self) -> None:
|
|
self.settings = get_settings()
|
|
self.repository = FileRepository()
|
|
self.attachments: list[dict] = []
|
|
self.next_attachment_id = 8001
|
|
|
|
def create_upload_token(self, payload: dict, session: Session | None = None) -> dict:
|
|
if not payload["file_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="文件名不能为空", status_code=400)
|
|
if not payload["biz_type"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="业务类型不能为空", status_code=400)
|
|
|
|
file_category = self._detect_file_category(payload["file_type"], payload["file_name"])
|
|
self._validate_file_size(file_category, payload["file_size"])
|
|
token_payload = storage_service.create_upload_token(
|
|
payload["biz_type"],
|
|
payload["biz_id"],
|
|
payload["file_name"],
|
|
payload["file_size"],
|
|
)
|
|
token_payload["file_category"] = file_category
|
|
return token_payload
|
|
|
|
def save_attachment(self, payload: dict, session: Session | None = None) -> dict:
|
|
if not payload["biz_type"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="业务类型不能为空", status_code=400)
|
|
if not payload["file_name"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="文件名不能为空", status_code=400)
|
|
if not payload["file_url"].strip():
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="文件地址不能为空", status_code=400)
|
|
|
|
file_category = self._detect_file_category(payload["file_type"], payload["file_name"])
|
|
self._validate_file_size(file_category, payload["file_size"])
|
|
object_key = storage_service.extract_object_key(payload["file_url"])
|
|
file_ext = self._extract_file_ext(payload["file_name"])
|
|
self._validate_attachment_file(payload["file_name"], payload["file_url"], object_key)
|
|
|
|
if session is not None:
|
|
try:
|
|
attachment = self.repository.create_attachment(
|
|
session,
|
|
{
|
|
"biz_type": payload["biz_type"],
|
|
"biz_id": payload["biz_id"],
|
|
"file_name": payload["file_name"],
|
|
"file_url": payload["file_url"],
|
|
"file_type": payload["file_type"],
|
|
"file_size": payload["file_size"],
|
|
"created_by": None,
|
|
"storage_provider": "aliyun_oss",
|
|
"bucket_name": self.settings.aliyun_oss_bucket,
|
|
"object_key": object_key,
|
|
"content_type": payload["file_type"],
|
|
"file_ext": file_ext,
|
|
"file_category": file_category,
|
|
},
|
|
)
|
|
audit_service.write_log(
|
|
session,
|
|
{
|
|
"operate_type": "attachment_create",
|
|
"biz_type": payload["biz_type"],
|
|
"biz_id": payload["biz_id"],
|
|
"before_value": None,
|
|
"after_value": self._map_attachment(attachment),
|
|
"remark": f"保存附件 {payload['file_name']}",
|
|
},
|
|
)
|
|
session.commit()
|
|
return self._map_attachment(attachment)
|
|
except AppException:
|
|
session.rollback()
|
|
raise
|
|
except SQLAlchemyError:
|
|
session.rollback()
|
|
|
|
attachment = {
|
|
"attachment_id": self.next_attachment_id,
|
|
"biz_type": payload["biz_type"],
|
|
"biz_id": payload["biz_id"],
|
|
"file_name": payload["file_name"],
|
|
"file_url": payload["file_url"],
|
|
"file_type": payload["file_type"],
|
|
"file_size": payload["file_size"],
|
|
"storage_provider": "aliyun_oss",
|
|
"bucket_name": self.settings.aliyun_oss_bucket,
|
|
"object_key": object_key,
|
|
"content_type": payload["file_type"],
|
|
"file_ext": file_ext,
|
|
"file_category": file_category,
|
|
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
self.next_attachment_id += 1
|
|
self.attachments.append(attachment)
|
|
return attachment
|
|
|
|
def _validate_attachment_file(self, file_name: str, file_url: str, object_key: str) -> None:
|
|
expected_name = file_name.strip()
|
|
object_name = object_key.rsplit("/", 1)[-1]
|
|
# 保存附件时再次核对对象 key 与文件名,避免前端绕过上传凭证直接塞入不匹配文件。
|
|
if expected_name and expected_name not in object_name:
|
|
raise AppException(code=ErrorCode.FILE_UPLOAD_FAILED, message="附件文件名与对象地址不匹配", status_code=400)
|
|
if not file_url.startswith(("http://", "https://")):
|
|
raise AppException(code=ErrorCode.FILE_UPLOAD_FAILED, message="附件地址格式不正确", status_code=400)
|
|
|
|
def _detect_file_category(self, file_type: str, file_name: str) -> str:
|
|
content_type = (file_type or "").lower()
|
|
suffix = self._extract_file_ext(file_name).lower()
|
|
if content_type.startswith("image/") or suffix in {"jpg", "jpeg", "png", "gif", "webp"}:
|
|
return "image"
|
|
if content_type.startswith("video/") or suffix in {"mp4", "mov", "avi"}:
|
|
return "video"
|
|
if suffix in {"xlsx", "xls", "csv"}:
|
|
return "import"
|
|
return "document"
|
|
|
|
def _validate_file_size(self, file_category: str, file_size: int) -> None:
|
|
max_mb = (
|
|
self.settings.oss_upload_max_image_mb
|
|
if file_category == "image"
|
|
else self.settings.oss_upload_max_video_mb
|
|
if file_category == "video"
|
|
else max(self.settings.oss_upload_max_image_mb, self.settings.oss_upload_max_video_mb)
|
|
)
|
|
if file_size > max_mb * 1024 * 1024:
|
|
raise AppException(code=ErrorCode.FILE_UPLOAD_FAILED, message="文件大小超出限制", status_code=400)
|
|
|
|
def _extract_file_ext(self, file_name: str) -> str:
|
|
parts = file_name.rsplit(".", 1)
|
|
return parts[1] if len(parts) == 2 else ""
|
|
|
|
def _map_attachment(self, attachment) -> dict:
|
|
return {
|
|
"attachment_id": attachment.id,
|
|
"biz_type": attachment.biz_type,
|
|
"biz_id": attachment.biz_id,
|
|
"file_name": attachment.file_name,
|
|
"file_url": attachment.file_url,
|
|
"file_type": attachment.file_type,
|
|
"file_size": attachment.file_size,
|
|
"storage_provider": attachment.storage_provider,
|
|
"bucket_name": attachment.bucket_name,
|
|
"object_key": attachment.object_key,
|
|
"content_type": attachment.content_type,
|
|
"file_ext": attachment.file_ext,
|
|
"file_category": attachment.file_category,
|
|
"created_at": attachment.created_at.strftime("%Y-%m-%d %H:%M:%S") if attachment.created_at else "",
|
|
}
|
|
|
|
|
|
file_service = FileService()
|