Files
hms/crates/erp-health/src/handler/media_handler.rs
iven 3a672636c0 feat(health): 实现媒体库 handler (12 端点) + 轮播图 handler (6 端点)
媒体库 handler (media_handler.rs):
- 上传/列表/详情/更新/删除媒体文件 + 文件夹 CRUD + 移动 + 裁剪

轮播图 handler (banner_handler.rs):
- 管理端 5 端点(列表/创建/更新/删除/排序)
- 公开端点 1 个(小程序无需认证获取生效轮播图)
2026-05-10 15:32:09 +08:00

281 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 媒体库 Handler — 12 个 HTTP 端点(文件 CRUD + 上传/裁剪/移动 + 文件夹管理)
use axum::Extension;
use axum::extract::{FromRef, Json, Multipart, Path, Query, State};
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::dto::media_dto::{
BatchDeleteReq, CreateFolderReq, CropReq, FolderResp, MediaItemResp, MediaListParams,
MoveMediaReq, UpdateFolderReq, UpdateMediaItemReq,
};
use crate::service::media_service;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 本地请求结构体
// ---------------------------------------------------------------------------
#[derive(Debug, serde::Deserialize)]
pub struct DeleteVersionReq {
pub version: i32,
}
// ---------------------------------------------------------------------------
// 媒体文件 CRUD
// ---------------------------------------------------------------------------
/// 分页查询媒体文件列表
pub async fn list_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<MediaListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<MediaItemResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.list")?;
let result = media_service::list_media_items(&state, ctx.tenant_id, params).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 上传媒体文件multipart/form-data
pub async fn upload_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
mut multipart: Multipart,
) -> Result<Json<ApiResponse<MediaItemResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
let mut file_data = None;
let mut folder_id: Option<Uuid> = None;
let mut is_public = false;
let mut original_name = String::new();
let mut content_type = String::new();
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| AppError::Validation(format!("读取上传数据失败: {}", e)))?
{
match field.name().unwrap_or("") {
"file" => {
original_name = field.file_name().unwrap_or("file").to_string();
content_type = field
.content_type()
.unwrap_or("application/octet-stream")
.to_string();
file_data = Some(
field
.bytes()
.await
.map_err(|e| AppError::Validation(format!("读取文件数据失败: {}", e)))?,
);
}
"folder_id" => {
let text = field.text().await.unwrap_or_default();
folder_id = text.parse().ok();
}
"is_public" => {
let text = field.text().await.unwrap_or_default();
is_public = text == "true" || text == "1";
}
_ => {}
}
}
let data = file_data.ok_or_else(|| AppError::Validation("未找到上传文件".to_string()))?;
let upload_dir = std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "./uploads".to_string());
let result = media_service::upload_media(
&state,
ctx.tenant_id,
Some(ctx.user_id),
&data,
&original_name,
&content_type,
folder_id,
is_public,
&upload_dir,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 获取单个媒体文件详情
pub async fn get_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<Json<ApiResponse<MediaItemResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.list")?;
let result = media_service::get_media_item(&state, ctx.tenant_id, id).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 更新媒体文件元数据
pub async fn update_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(mut req): Json<UpdateMediaItemReq>,
) -> Result<Json<ApiResponse<MediaItemResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
req.sanitize();
let result =
media_service::update_media_item(&state, ctx.tenant_id, id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 软删除媒体文件
pub async fn delete_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<DeleteVersionReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
media_service::delete_media_item(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
.await?;
Ok(Json(ApiResponse::ok(())))
}
/// 移动媒体文件到指定文件夹
pub async fn move_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<MoveMediaReq>,
) -> Result<Json<ApiResponse<MediaItemResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
let result =
media_service::move_media(&state, ctx.tenant_id, id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 批量软删除媒体文件
pub async fn batch_delete_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<BatchDeleteReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
media_service::batch_delete(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(())))
}
/// 裁剪图片并重新生成缩略图
pub async fn crop_media<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<CropReq>,
) -> Result<Json<ApiResponse<MediaItemResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
let result =
media_service::crop_media(&state, ctx.tenant_id, id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 文件夹管理
// ---------------------------------------------------------------------------
/// 获取文件夹树形结构
pub async fn list_folders<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<Vec<FolderResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.list")?;
let result = media_service::list_folders(&state, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 创建文件夹
pub async fn create_folder<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(mut req): Json<CreateFolderReq>,
) -> Result<Json<ApiResponse<FolderResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
req.sanitize();
let result =
media_service::create_folder(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 更新文件夹信息
pub async fn update_folder<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(mut req): Json<UpdateFolderReq>,
) -> Result<Json<ApiResponse<FolderResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
req.sanitize();
let result =
media_service::update_folder(&state, ctx.tenant_id, id, Some(ctx.user_id), req).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 删除文件夹(仅空文件夹可删除)
pub async fn delete_folder<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<DeleteVersionReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.media.manage")?;
media_service::delete_folder(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
Ok(Json(ApiResponse::ok(())))
}