From 3a672636c0e7702c37066626d7c3ecc23cd64600 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 10 May 2026 15:29:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20=E5=AE=9E=E7=8E=B0=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E5=BA=93=20handler=20(12=20=E7=AB=AF=E7=82=B9)=20+=20?= =?UTF-8?q?=E8=BD=AE=E6=92=AD=E5=9B=BE=20handler=20(6=20=E7=AB=AF=E7=82=B9?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 媒体库 handler (media_handler.rs): - 上传/列表/详情/更新/删除媒体文件 + 文件夹 CRUD + 移动 + 裁剪 轮播图 handler (banner_handler.rs): - 管理端 5 端点(列表/创建/更新/删除/排序) - 公开端点 1 个(小程序无需认证获取生效轮播图) --- .../erp-health/src/handler/banner_handler.rs | 142 +++++++++ .../erp-health/src/handler/media_handler.rs | 280 ++++++++++++++++++ crates/erp-health/src/handler/mod.rs | 2 + 3 files changed, 424 insertions(+) create mode 100644 crates/erp-health/src/handler/banner_handler.rs create mode 100644 crates/erp-health/src/handler/media_handler.rs diff --git a/crates/erp-health/src/handler/banner_handler.rs b/crates/erp-health/src/handler/banner_handler.rs new file mode 100644 index 0000000..dd0556f --- /dev/null +++ b/crates/erp-health/src/handler/banner_handler.rs @@ -0,0 +1,142 @@ +use axum::Extension; +use axum::extract::{FromRef, Json, Path, Query, State}; + +use erp_core::error::AppError; +use erp_core::rbac::require_permission; +use erp_core::types::{ApiResponse, TenantContext}; + +use crate::dto::banner_dto::{ + BannerResp, CreateBannerReq, PublicBannerResp, SortBannerReq, UpdateBannerReq, +}; +use crate::service::banner_service; +use crate::state::HealthState; + +// --------------------------------------------------------------------------- +// 本地请求结构体 +// --------------------------------------------------------------------------- + +#[derive(Debug, serde::Deserialize)] +pub struct DeleteVersionReq { + pub version: i32, +} + +#[derive(Debug, serde::Deserialize)] +pub struct BannerListQuery { + pub status: Option, +} + +#[derive(Debug, serde::Deserialize)] +pub struct PublicBannerQuery { + pub tenant_id: Option, +} + +// --------------------------------------------------------------------------- +// 管理端端点 +// --------------------------------------------------------------------------- + +/// GET /health/banners — 列出租户所有轮播图 +pub async fn list_banners( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, +) -> Result>>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.banners.list")?; + let result = banner_service::list_banners(&state, ctx.tenant_id, params.status).await?; + Ok(Json(ApiResponse::ok(result))) +} + +/// POST /health/banners — 创建轮播图 +pub async fn create_banner( + State(state): State, + Extension(ctx): Extension, + mut req: Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.banners.manage")?; + req.sanitize(); + let result = banner_service::create_banner(&state, ctx.tenant_id, ctx.user_id, req.0).await?; + Ok(Json(ApiResponse::ok(result))) +} + +/// PUT /health/banners/{id} — 更新轮播图 +pub async fn update_banner( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + mut req: Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.banners.manage")?; + req.sanitize(); + let result = + banner_service::update_banner(&state, ctx.tenant_id, id, ctx.user_id, req.0).await?; + Ok(Json(ApiResponse::ok(result))) +} + +/// DELETE /health/banners/{id} — 软删除轮播图 +pub async fn delete_banner( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.banners.manage")?; + banner_service::delete_banner(&state, ctx.tenant_id, id, ctx.user_id, req.version).await?; + Ok(Json(ApiResponse::ok(()))) +} + +/// PUT /health/banners/sort — 批量更新排序 +pub async fn sort_banners( + State(state): State, + Extension(ctx): Extension, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.banners.manage")?; + banner_service::sort_banners(&state, ctx.tenant_id, req.items).await?; + Ok(Json(ApiResponse::ok(()))) +} + +// --------------------------------------------------------------------------- +// 公开端点(小程序 / 无需认证) +// --------------------------------------------------------------------------- + +/// GET /public/banners — 公开轮播图列表 +pub async fn list_public_banners( + State(state): State, + Query(params): Query, + headers: axum::http::HeaderMap, +) -> Result>>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + // 从 X-Tenant-Id 请求头或查询参数中解析租户 ID + let tenant_id = headers + .get("X-Tenant-Id") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()) + .or(params.tenant_id) + .ok_or_else(|| AppError::Validation("缺少 tenant_id".to_string()))?; + + let base_url = "http://localhost:3000".to_string(); + let result = banner_service::list_public_banners(&state, tenant_id, &base_url).await?; + Ok(Json(ApiResponse::ok(result))) +} diff --git a/crates/erp-health/src/handler/media_handler.rs b/crates/erp-health/src/handler/media_handler.rs new file mode 100644 index 0000000..a989c1d --- /dev/null +++ b/crates/erp-health/src/handler/media_handler.rs @@ -0,0 +1,280 @@ +//! 媒体库 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( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, +) -> Result>>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + mut multipart: Multipart, +) -> Result>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.media.manage")?; + + let mut file_data = None; + let mut folder_id: Option = 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(mut req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, +) -> Result>>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Json(mut req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(mut req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + HealthState: FromRef, + 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(()))) +} diff --git a/crates/erp-health/src/handler/mod.rs b/crates/erp-health/src/handler/mod.rs index 93db4fe..45eb4f0 100644 --- a/crates/erp-health/src/handler/mod.rs +++ b/crates/erp-health/src/handler/mod.rs @@ -5,6 +5,7 @@ pub mod appointment_handler; pub mod article_category_handler; pub mod article_handler; pub mod article_tag_handler; +pub mod banner_handler; pub mod ble_gateway_handler; pub mod care_plan_handler; pub mod consent_handler; @@ -20,6 +21,7 @@ pub mod family_proxy_handler; pub mod follow_up_handler; pub mod follow_up_template_handler; pub mod health_data_handler; +pub mod media_handler; pub mod medication_record_handler; pub mod medication_reminder_handler; pub mod patient_handler;