fix(health,miniprogram): 轮播图图片改用相对路径 + wx.downloadFile 解决 HTTP 限制
问题:微信小程序 <image> 不支持 HTTP URL,签名 URL 与 upload 中间件不兼容。 修复: 1. 公开轮播图 API 返回相对路径(/uploads/...)而非签名 URL 2. 小程序用 wx.downloadFile 下载图片后使用本地临时路径 3. 移除 banner_handler 中不再需要的 base_url/Host header 逻辑
This commit is contained in:
@@ -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) => (
|
||||
<SwiperItem key={slide.id || idx}>
|
||||
<View className='guest-slide'>
|
||||
{slide.image_url ? (
|
||||
<Image className='guest-slide-image' src={slide.image_url} mode='aspectFill' />
|
||||
{(slide.local_path || slide.image_url) ? (
|
||||
<Image className='guest-slide-image' src={slide.local_path || slide.image_url} mode='aspectFill' />
|
||||
) : (
|
||||
<View className={`guest-slide-bg guest-slide-bg--${(idx % 3) + 1}`} />
|
||||
)}
|
||||
|
||||
@@ -107,6 +107,7 @@ pub struct PublicBannerResp {
|
||||
pub id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub subtitle: Option<String>,
|
||||
/// 相对路径如 /uploads/tenant_id/filename.png(前端按需拼接 base_url)
|
||||
pub image_url: Option<String>,
|
||||
pub link_type: Option<String>,
|
||||
pub link_target: Option<String>,
|
||||
|
||||
@@ -122,31 +122,14 @@ where
|
||||
pub async fn list_public_banners<S>(
|
||||
State(state): State<HealthState>,
|
||||
Query(params): Query<PublicBannerQuery>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Result<Json<ApiResponse<Vec<PublicBannerResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
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::<uuid::Uuid>().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)))
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::state::HealthState;
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
/// 获取签名 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<Vec<PublicBannerResp>> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user