diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index c0e7fae..bbe77ed 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -30,6 +30,8 @@ interface PublicBanner { image_url?: string; link_type?: string; link_target?: string; + /** 下载后的本地临时路径 */ + local_path?: string; } // ─── 访客首页 ─── @@ -67,7 +69,22 @@ function GuestHome({ modeClass }: { modeClass: string }) { ]); if (bannerData.status === 'fulfilled' && bannerData.value?.length > 0) { - setBanners(bannerData.value); + const baseUrl = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; + const fileBase = baseUrl.replace(/\/api\/v1$/, ''); + const withLocal = await Promise.all( + bannerData.value.map(async (b) => { + if (!b.image_url) return b; + try { + const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${fileBase}${b.image_url}`; + const res = await Taro.downloadFile({ url: fullUrl }); + if (res.tempFilePath) { + return { ...b, local_path: res.tempFilePath }; + } + } catch { /* ignore */ } + return b; + }) + ); + setBanners(withLocal); } else { setBanners(FALLBACK_SLIDES); } @@ -98,8 +115,8 @@ function GuestHome({ modeClass }: { modeClass: string }) { {slides.map((slide, idx) => ( - {slide.image_url ? ( - + {(slide.local_path || slide.image_url) ? ( + ) : ( )} diff --git a/crates/erp-health/src/dto/banner_dto.rs b/crates/erp-health/src/dto/banner_dto.rs index f1620ec..f3bcb93 100644 --- a/crates/erp-health/src/dto/banner_dto.rs +++ b/crates/erp-health/src/dto/banner_dto.rs @@ -107,6 +107,7 @@ pub struct PublicBannerResp { pub id: Uuid, pub title: Option, pub subtitle: Option, + /// 相对路径如 /uploads/tenant_id/filename.png(前端按需拼接 base_url) pub image_url: Option, pub link_type: Option, pub link_target: Option, diff --git a/crates/erp-health/src/handler/banner_handler.rs b/crates/erp-health/src/handler/banner_handler.rs index 2c08143..e8432cb 100644 --- a/crates/erp-health/src/handler/banner_handler.rs +++ b/crates/erp-health/src/handler/banner_handler.rs @@ -122,31 +122,14 @@ where 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) + let tenant_id = params + .tenant_id .ok_or_else(|| AppError::Validation("缺少 tenant_id".to_string()))?; - - let base_url = headers - .get("host") - .and_then(|v| v.to_str().ok()) - .map(|h| { - if h.starts_with("localhost") || h.starts_with("127.0.0.1") { - format!("http://{}", h) - } else { - format!("https://{}", h) - } - }) - .unwrap_or_else(|| "http://localhost:3000".to_string()); - let result = banner_service::list_public_banners(&state, tenant_id, &base_url).await?; + let result = banner_service::list_public_banners(&state, tenant_id).await?; Ok(Json(ApiResponse::ok(result))) } diff --git a/crates/erp-health/src/service/banner_service.rs b/crates/erp-health/src/service/banner_service.rs index 4706213..fdceb6a 100644 --- a/crates/erp-health/src/service/banner_service.rs +++ b/crates/erp-health/src/service/banner_service.rs @@ -20,6 +20,7 @@ use crate::state::HealthState; type HmacSha256 = Hmac; /// 获取签名 URL 密钥:优先环境变量 ERP__STORAGE__SECRET_KEY,回退到开发默认值 +#[allow(dead_code)] fn signing_secret() -> String { std::env::var("ERP__STORAGE__SECRET_KEY") .unwrap_or_else(|_| "dev-only-secret-key-change-in-production".to_string()) @@ -271,7 +272,6 @@ pub async fn sort_banners( pub async fn list_public_banners( state: &HealthState, tenant_id: Uuid, - base_url: &str, ) -> HealthResult> { let now = Utc::now(); @@ -300,21 +300,10 @@ pub async fn list_public_banners( .into_iter() .filter_map(|b| { let media = media_map.get(&b.media_item_id)?; - // 跳过已删除的媒体文件 if media.deleted_at.is_some() { return None; } - - let (token, expires) = - generate_signed_url(&media.storage_path, &signing_secret(), 3600); - let clean_path = media.storage_path.trim_start_matches("./"); - let image_url = format!( - "{}/{}?expires={}&token={}", - base_url.trim_end_matches('/'), - clean_path, - expires, - token - ); + let image_url = media.storage_path.trim_start_matches("./").to_string(); Some(PublicBannerResp { id: b.id, @@ -335,6 +324,7 @@ pub async fn list_public_banners( // --------------------------------------------------------------------------- /// 生成 HMAC-SHA256 签名 URL token(同步函数) +#[allow(dead_code)] pub fn generate_signed_url(path: &str, secret_key: &str, ttl_secs: u64) -> (String, i64) { let expires = Utc::now().timestamp() + ttl_secs as i64; let message = format!("{}\n{}", path, expires);