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, ) -> Result>>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { let tenant_id = params .tenant_id .ok_or_else(|| AppError::Validation("缺少 tenant_id".to_string()))?; let result = banner_service::list_public_banners(&state, tenant_id).await?; Ok(Json(ApiResponse::ok(result))) } /// GET /public/banner-image/{banner_id} — 公开轮播图图片(无需认证,供小程序下载) pub async fn serve_banner_image( State(state): State, Path(banner_id): Path, ) -> Result { use axum::http::{StatusCode, header}; use axum::response::IntoResponse; let path = banner_service::get_banner_image_path(&state, banner_id).await?; let data = tokio::fs::read(&path) .await .map_err(|e| AppError::Internal(format!("读取图片文件失败: {}", e)))?; let mime = if path.ends_with(".png") { "image/png" } else if path.ends_with(".gif") { "image/gif" } else if path.ends_with(".webp") { "image/webp" } else { "image/jpeg" }; Ok(( StatusCode::OK, [ (header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, "public, max-age=3600"), ], data, ) .into_response()) }