use std::collections::BTreeMap; use axum::http::StatusCode; use platform_wechat::WechatSubscribeMessageRequest; use time::{OffsetDateTime, UtcOffset}; use tracing::{info, warn}; use crate::{http_error::AppError, platform_errors::map_wechat_error, state::AppState}; const GENERATION_RESULT_TASK_NAME: &str = "AI创作生成"; const DEFAULT_WORK_NAME: &str = "AI创作作品"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum GenerationResultSubscribeMessageStatus { Succeeded, Failed, } #[derive(Clone, Debug)] pub struct GenerationResultSubscribeMessage { pub owner_user_id: String, pub task_name: Option, pub work_name: Option, pub status: GenerationResultSubscribeMessageStatus, pub consumed_points: u64, pub completed_at_micros: i64, pub page: Option, } pub async fn send_generation_result_subscribe_message_after_completion( state: &AppState, message: GenerationResultSubscribeMessage, ) { if let Err(error) = send_generation_result_subscribe_message(state, message).await { warn!( error = %error, "微信小程序生成结果订阅消息发送失败,已忽略" ); } } async fn send_generation_result_subscribe_message( state: &AppState, message: GenerationResultSubscribeMessage, ) -> Result<(), AppError> { if !state.config.wechat_mini_program_subscribe_message_enabled { return Ok(()); } let template_id = state .config .wechat_mini_program_generation_result_template_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| { AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("微信订阅消息模板 ID 未配置") })?; let user = state .auth_user_service() .get_user_by_id(&message.owner_user_id) .map_err(|error| { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) .with_message(format!("读取微信订阅消息用户失败:{error}")) })? .ok_or_else(|| { AppError::from_status(StatusCode::NOT_FOUND).with_message("微信订阅消息用户不存在") })?; let openid = user .wechat_account .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| { AppError::from_status(StatusCode::BAD_REQUEST) .with_message("用户未绑定微信小程序 openid") })?; state .wechat_client() .send_subscribe_message(WechatSubscribeMessageRequest { touser: openid.to_string(), template_id: template_id.to_string(), page: message .page .clone() .or_else(|| Some("/pages/web-view/index".to_string())), miniprogram_state: Some( normalize_miniprogram_state( &state.config.wechat_mini_program_subscribe_message_state, ) .to_string(), ), lang: Some("zh_CN".to_string()), data: build_generation_result_template_data(&message), }) .await .map_err(map_wechat_error)?; info!( owner_user_id = %message.owner_user_id, template_id, "微信小程序生成结果订阅消息已发送" ); Ok(()) } fn build_generation_result_template_data( message: &GenerationResultSubscribeMessage, ) -> BTreeMap { BTreeMap::from([ ( "thing1".to_string(), truncate_template_value( message .task_name .as_deref() .unwrap_or(GENERATION_RESULT_TASK_NAME), 20, ), ), ( "phrase2".to_string(), truncate_template_value(message.status.template_status_label(), 5), ), ( "time4".to_string(), truncate_template_value( &format_generation_completed_time(message.completed_at_micros), 20, ), ), ( "thing5".to_string(), truncate_template_value( message.work_name.as_deref().unwrap_or(DEFAULT_WORK_NAME), 20, ), ), ( "number6".to_string(), truncate_template_value(&message.consumed_points.to_string(), 32), ), ]) } impl GenerationResultSubscribeMessageStatus { fn template_status_label(self) -> &'static str { match self { Self::Succeeded => "已完成", Self::Failed => "生成失败", } } } fn truncate_template_value(value: &str, max_chars: usize) -> String { let trimmed = value.trim(); let mut result = String::new(); for character in trimmed.chars().take(max_chars) { result.push(character); } if result.is_empty() { DEFAULT_WORK_NAME.to_string() } else { result } } fn format_generation_completed_time(completed_at_micros: i64) -> String { let seconds = completed_at_micros.div_euclid(1_000_000); let Ok(utc_time) = OffsetDateTime::from_unix_timestamp(seconds) else { return "1970-01-01 08:00".to_string(); }; let beijing_offset = UtcOffset::from_hms(8, 0, 0).unwrap_or(UtcOffset::UTC); let local_time = utc_time.to_offset(beijing_offset); format!( "{:04}-{:02}-{:02} {:02}:{:02}", local_time.year(), u8::from(local_time.month()), local_time.day(), local_time.hour(), local_time.minute() ) } fn normalize_miniprogram_state(value: &str) -> &'static str { match value.trim().to_ascii_lowercase().as_str() { "developer" | "develop" | "dev" => "developer", "trial" => "trial", _ => "formal", } } #[cfg(test)] mod tests { use super::*; #[test] fn failed_generation_result_template_uses_failed_status_and_zero_points() { let data = build_generation_result_template_data(&GenerationResultSubscribeMessage { owner_user_id: "user-1".to_string(), task_name: Some("拼图".to_string()), work_name: Some("首关拼图".to_string()), status: GenerationResultSubscribeMessageStatus::Failed, consumed_points: 0, completed_at_micros: 1_762_000_000_000_000, page: None, }); assert_eq!(data.get("phrase2").map(String::as_str), Some("生成失败")); assert_eq!(data.get("number6").map(String::as_str), Some("0")); } #[test] fn generation_result_template_time_uses_wechat_time_format() { let data = build_generation_result_template_data(&GenerationResultSubscribeMessage { owner_user_id: "user-1".to_string(), task_name: Some("拼图".to_string()), work_name: Some("首关拼图".to_string()), status: GenerationResultSubscribeMessageStatus::Succeeded, consumed_points: 15, completed_at_micros: 0, page: None, }); assert_eq!( data.get("time4").map(String::as_str), Some("1970-01-01 08:00") ); } #[test] fn generation_result_template_uses_task_template_name() { let data = build_generation_result_template_data(&GenerationResultSubscribeMessage { owner_user_id: "user-1".to_string(), task_name: Some("敲木鱼".to_string()), work_name: Some("功德木鱼".to_string()), status: GenerationResultSubscribeMessageStatus::Succeeded, consumed_points: 10, completed_at_micros: 0, page: None, }); assert_eq!(data.get("thing1").map(String::as_str), Some("敲木鱼")); } }