feat: send puzzle result subscribe messages
This commit is contained in:
@@ -100,6 +100,10 @@ pub struct AppConfig {
|
||||
pub wechat_mini_program_virtual_payment_sandbox_app_key: Option<String>,
|
||||
pub wechat_mini_program_message_token: Option<String>,
|
||||
pub wechat_mini_program_message_encoding_aes_key: Option<String>,
|
||||
pub wechat_mini_program_subscribe_message_enabled: bool,
|
||||
pub wechat_mini_program_generation_result_template_id: Option<String>,
|
||||
pub wechat_mini_program_subscribe_message_endpoint: String,
|
||||
pub wechat_mini_program_subscribe_message_state: String,
|
||||
pub wechat_mini_program_virtual_payment_env: u8,
|
||||
pub oss_bucket: Option<String>,
|
||||
pub oss_endpoint: Option<String>,
|
||||
@@ -250,6 +254,13 @@ impl Default for AppConfig {
|
||||
wechat_mini_program_virtual_payment_sandbox_app_key: None,
|
||||
wechat_mini_program_message_token: None,
|
||||
wechat_mini_program_message_encoding_aes_key: None,
|
||||
wechat_mini_program_subscribe_message_enabled: true,
|
||||
wechat_mini_program_generation_result_template_id: Some(
|
||||
"m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU".to_string(),
|
||||
),
|
||||
wechat_mini_program_subscribe_message_endpoint:
|
||||
"https://api.weixin.qq.com/cgi-bin/message/subscribe/send".to_string(),
|
||||
wechat_mini_program_subscribe_message_state: "formal".to_string(),
|
||||
wechat_mini_program_virtual_payment_env: 0,
|
||||
oss_bucket: None,
|
||||
oss_endpoint: None,
|
||||
@@ -613,6 +624,26 @@ impl AppConfig {
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_TOKEN"]);
|
||||
config.wechat_mini_program_message_encoding_aes_key =
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY"]);
|
||||
if let Some(enabled) =
|
||||
read_first_bool_env(&["WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED"])
|
||||
{
|
||||
config.wechat_mini_program_subscribe_message_enabled = enabled;
|
||||
}
|
||||
if let Some(template_id) =
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID"])
|
||||
{
|
||||
config.wechat_mini_program_generation_result_template_id = Some(template_id);
|
||||
}
|
||||
if let Some(endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENDPOINT"])
|
||||
{
|
||||
config.wechat_mini_program_subscribe_message_endpoint = endpoint;
|
||||
}
|
||||
if let Some(state) =
|
||||
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE"])
|
||||
{
|
||||
config.wechat_mini_program_subscribe_message_state = state;
|
||||
}
|
||||
if let Some(env) = read_first_u8_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV"])
|
||||
&& env <= 1
|
||||
{
|
||||
@@ -1419,6 +1450,9 @@ mod tests {
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
|
||||
std::env::set_var("WECHAT_PAY_ENABLED", "true");
|
||||
std::env::set_var("WECHAT_PAY_PROVIDER", "real");
|
||||
@@ -1446,6 +1480,12 @@ mod tests {
|
||||
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
|
||||
);
|
||||
std::env::set_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED", "true");
|
||||
std::env::set_var(
|
||||
"WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID",
|
||||
"tmpl-generation-result",
|
||||
);
|
||||
std::env::set_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE", "trial");
|
||||
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV", "1");
|
||||
}
|
||||
|
||||
@@ -1497,6 +1537,14 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some("sandbox-app-key-001")
|
||||
);
|
||||
assert!(config.wechat_mini_program_subscribe_message_enabled);
|
||||
assert_eq!(
|
||||
config
|
||||
.wechat_mini_program_generation_result_template_id
|
||||
.as_deref(),
|
||||
Some("tmpl-generation-result")
|
||||
);
|
||||
assert_eq!(config.wechat_mini_program_subscribe_message_state, "trial");
|
||||
assert_eq!(config.wechat_mini_program_virtual_payment_env, 1);
|
||||
|
||||
unsafe {
|
||||
@@ -1514,6 +1562,9 @@ mod tests {
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID");
|
||||
std::env::remove_var("WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE");
|
||||
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ mod volcengine_speech;
|
||||
mod wechat_auth;
|
||||
mod wechat_pay;
|
||||
mod wechat_provider;
|
||||
mod wechat_subscribe_message;
|
||||
mod wooden_fish;
|
||||
mod work_author;
|
||||
mod work_play_tracking;
|
||||
|
||||
@@ -2,6 +2,7 @@ use axum::http::{HeaderValue, StatusCode};
|
||||
use platform_auth::{AuthPlatformErrorKind, WechatProviderError};
|
||||
use platform_llm::{LlmError, LlmErrorKind};
|
||||
use platform_oss::{OssError, OssErrorKind};
|
||||
use platform_wechat::{WechatError, WechatErrorKind};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::http_error::AppError;
|
||||
@@ -68,6 +69,17 @@ pub fn map_wechat_provider_error(error: WechatProviderError) -> AppError {
|
||||
AppError::from_status(status).with_message(error.to_string())
|
||||
}
|
||||
|
||||
pub fn map_wechat_error(error: WechatError) -> AppError {
|
||||
let status = match error.kind() {
|
||||
WechatErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
|
||||
WechatErrorKind::RequestFailed
|
||||
| WechatErrorKind::DeserializeFailed
|
||||
| WechatErrorKind::Upstream => StatusCode::BAD_GATEWAY,
|
||||
};
|
||||
|
||||
AppError::from_status(status).with_message(error.to_string())
|
||||
}
|
||||
|
||||
pub fn attach_retry_after(error: AppError, retry_after_seconds: u64) -> AppError {
|
||||
match HeaderValue::from_str(&retry_after_seconds.to_string()) {
|
||||
Ok(value) => error.with_header("retry-after", value),
|
||||
|
||||
@@ -58,16 +58,15 @@ use spacetime_client::{
|
||||
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
|
||||
PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftCompileFailureRecordInput,
|
||||
PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput,
|
||||
PuzzleGeneratedImageCandidateRecord,
|
||||
PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord,
|
||||
PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord,
|
||||
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
|
||||
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput,
|
||||
PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput,
|
||||
PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput,
|
||||
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
|
||||
SpacetimeClientError,
|
||||
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
|
||||
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput,
|
||||
PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord,
|
||||
PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleRunDragRecordInput,
|
||||
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
|
||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput,
|
||||
PuzzleUiBackgroundSaveRecordInput, PuzzleWorkLikeReportRecordInput,
|
||||
PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
|
||||
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
|
||||
@@ -106,6 +105,10 @@ use crate::{
|
||||
puzzle_gallery_cache::{build_puzzle_gallery_window_response, puzzle_gallery_cached_json},
|
||||
request_context::RequestContext,
|
||||
state::{AppState, PuzzleApiState},
|
||||
wechat_subscribe_message::{
|
||||
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
|
||||
send_generation_result_subscribe_message_after_completion,
|
||||
},
|
||||
work_author::resolve_puzzle_work_author_by_user_id,
|
||||
work_play_tracking::{WorkPlayTrackingDraft, record_puzzle_work_play_start_after_success},
|
||||
};
|
||||
|
||||
@@ -617,13 +617,14 @@ pub async fn execute_puzzle_agent_action(
|
||||
let log_session_id = session_id.clone();
|
||||
let log_owner_user_id = owner_user_id.clone();
|
||||
async move {
|
||||
let failed_at_micros = current_utc_micros();
|
||||
let result = state
|
||||
.spacetime_client()
|
||||
.mark_puzzle_draft_generation_failed(PuzzleDraftCompileFailureRecordInput {
|
||||
session_id,
|
||||
owner_user_id,
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
error_message,
|
||||
failed_at_micros: current_utc_micros(),
|
||||
failed_at_micros,
|
||||
})
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
@@ -634,6 +635,19 @@ pub async fn execute_puzzle_agent_action(
|
||||
message = %error,
|
||||
"拼图草稿失败态回写失败,继续返回原始错误"
|
||||
);
|
||||
} else {
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
state.root_state(),
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id,
|
||||
work_name: None,
|
||||
status: GenerationResultSubscribeMessageStatus::Failed,
|
||||
consumed_points: 0,
|
||||
completed_at_micros: failed_at_micros,
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -677,10 +691,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
);
|
||||
state
|
||||
.spacetime_client()
|
||||
.get_puzzle_agent_session(
|
||||
compile_session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
)
|
||||
.get_puzzle_agent_session(compile_session_id.clone(), owner_user_id.clone())
|
||||
.await
|
||||
.map(mark_puzzle_initial_generation_started_snapshot)
|
||||
.map_err(map_puzzle_client_error)
|
||||
@@ -696,10 +707,9 @@ pub async fn execute_puzzle_agent_action(
|
||||
.map_err(map_puzzle_compile_error);
|
||||
match compiled_session {
|
||||
Ok(compiled_session) => {
|
||||
let response_session =
|
||||
mark_puzzle_initial_generation_started_snapshot(
|
||||
compiled_session.clone(),
|
||||
);
|
||||
let response_session = mark_puzzle_initial_generation_started_snapshot(
|
||||
compiled_session.clone(),
|
||||
);
|
||||
let background_state = state.clone();
|
||||
let background_request_context = request_context.clone();
|
||||
let background_session_id = compile_session_id.clone();
|
||||
@@ -708,13 +718,15 @@ pub async fn execute_puzzle_agent_action(
|
||||
let background_reference_image_src =
|
||||
primary_reference_image_src.map(str::to_string);
|
||||
let background_image_model = payload.image_model.clone();
|
||||
let background_work_name = compiled_session
|
||||
.draft
|
||||
.as_ref()
|
||||
.map(|draft| draft.work_title.clone());
|
||||
let background_billing_asset_id =
|
||||
format!("{background_session_id}:compile_puzzle_draft");
|
||||
tokio::spawn(async move {
|
||||
let operation_owner_user_id =
|
||||
background_owner_user_id.clone();
|
||||
let background_root_state =
|
||||
background_state.root_state().clone();
|
||||
let operation_owner_user_id = background_owner_user_id.clone();
|
||||
let background_root_state = background_state.root_state().clone();
|
||||
let operation_state = background_state.clone();
|
||||
let result = execute_billable_asset_operation_with_cost(
|
||||
&background_root_state,
|
||||
@@ -739,6 +751,23 @@ pub async fn execute_puzzle_agent_action(
|
||||
.await;
|
||||
match result {
|
||||
Ok(session) => {
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
&background_root_state,
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id: background_owner_user_id.clone(),
|
||||
work_name: session
|
||||
.draft
|
||||
.as_ref()
|
||||
.map(|draft| draft.work_title.clone()),
|
||||
status:
|
||||
GenerationResultSubscribeMessageStatus::Succeeded,
|
||||
consumed_points:
|
||||
PUZZLE_IMAGE_GENERATION_POINTS_COST,
|
||||
completed_at_micros: current_utc_micros(),
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
tracing::info!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
session_id = %session.session_id,
|
||||
@@ -748,15 +777,15 @@ pub async fn execute_puzzle_agent_action(
|
||||
}
|
||||
Err(error) => {
|
||||
let error_message = error.body_text();
|
||||
let failed_at_micros = current_utc_micros();
|
||||
let failure_result = background_state
|
||||
.spacetime_client()
|
||||
.mark_puzzle_draft_generation_failed(
|
||||
PuzzleDraftCompileFailureRecordInput {
|
||||
session_id: background_session_id.clone(),
|
||||
owner_user_id: background_owner_user_id
|
||||
.clone(),
|
||||
owner_user_id: background_owner_user_id.clone(),
|
||||
error_message: error_message.clone(),
|
||||
failed_at_micros: current_utc_micros(),
|
||||
failed_at_micros,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -768,6 +797,20 @@ pub async fn execute_puzzle_agent_action(
|
||||
message = %mark_error,
|
||||
"拼图首图后台生成失败态回写失败"
|
||||
);
|
||||
} else {
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
&background_root_state,
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id: background_owner_user_id.clone(),
|
||||
work_name: background_work_name.clone(),
|
||||
status:
|
||||
GenerationResultSubscribeMessageStatus::Failed,
|
||||
consumed_points: 0,
|
||||
completed_at_micros: failed_at_micros,
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
tracing::warn!(
|
||||
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
@@ -778,9 +821,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
);
|
||||
}
|
||||
}
|
||||
unregister_puzzle_background_compile_task(
|
||||
&background_session_id,
|
||||
);
|
||||
unregister_puzzle_background_compile_task(&background_session_id);
|
||||
});
|
||||
Ok(response_session)
|
||||
}
|
||||
@@ -1428,6 +1469,29 @@ pub async fn execute_puzzle_agent_action(
|
||||
};
|
||||
|
||||
let session = session?;
|
||||
if operation_type == "compile_puzzle_draft"
|
||||
&& session
|
||||
.draft
|
||||
.as_ref()
|
||||
.is_some_and(|draft| draft.generation_status == "ready")
|
||||
{
|
||||
send_generation_result_subscribe_message_after_completion(
|
||||
state.root_state(),
|
||||
GenerationResultSubscribeMessage {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
work_name: session.draft.as_ref().map(|draft| draft.work_title.clone()),
|
||||
status: GenerationResultSubscribeMessageStatus::Succeeded,
|
||||
consumed_points: if payload.ai_redraw.unwrap_or(true) {
|
||||
PUZZLE_IMAGE_GENERATION_POINTS_COST
|
||||
} else {
|
||||
0
|
||||
},
|
||||
completed_at_micros: current_utc_micros(),
|
||||
page: Some("/pages/web-view/index".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
|
||||
@@ -10,12 +10,12 @@ use std::{
|
||||
|
||||
use axum::extract::FromRef;
|
||||
use module_ai::{AiTaskService, InMemoryAiTaskStore};
|
||||
#[cfg(not(test))]
|
||||
use module_auth::RefreshAuthStoreSnapshotResult;
|
||||
use module_auth::{
|
||||
AuthUserService, InMemoryAuthStore, PasswordEntryService, PhoneAuthService,
|
||||
RefreshSessionService, WechatAuthService, WechatAuthStateService,
|
||||
};
|
||||
#[cfg(not(test))]
|
||||
use module_auth::RefreshAuthStoreSnapshotResult;
|
||||
use module_runtime::RuntimeSnapshotRecord;
|
||||
#[cfg(test)]
|
||||
use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros};
|
||||
@@ -27,6 +27,7 @@ use platform_auth::{
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider};
|
||||
use platform_oss::{OssClient, OssConfig, OssError};
|
||||
use platform_wechat::{WechatClient, WechatConfig};
|
||||
use serde_json::Value;
|
||||
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||
use shared_contracts::creative_agent::CreativeAgentSessionSnapshot;
|
||||
@@ -251,6 +252,7 @@ pub struct AppStateInner {
|
||||
wechat_auth_state_service: WechatAuthStateService,
|
||||
wechat_auth_service: WechatAuthService,
|
||||
wechat_provider: WechatProvider,
|
||||
wechat_client: WechatClient,
|
||||
wechat_pay_client: WechatPayClient,
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
ai_task_service: AiTaskService,
|
||||
@@ -385,6 +387,7 @@ impl AppState {
|
||||
WechatAuthStateService::new(auth_store.clone(), config.wechat_state_ttl_minutes);
|
||||
let wechat_auth_service = WechatAuthService::new(auth_store.clone());
|
||||
let wechat_provider = build_wechat_provider(&config);
|
||||
let wechat_client = build_wechat_client(&config);
|
||||
let wechat_pay_client =
|
||||
WechatPayClient::from_config(&config).map_err(map_wechat_pay_init_error)?;
|
||||
let refresh_session_service =
|
||||
@@ -424,6 +427,7 @@ impl AppState {
|
||||
wechat_auth_state_service,
|
||||
wechat_auth_service,
|
||||
wechat_provider,
|
||||
wechat_client,
|
||||
wechat_pay_client,
|
||||
ai_task_service,
|
||||
spacetime_client,
|
||||
@@ -776,6 +780,10 @@ impl AppState {
|
||||
&self.wechat_provider
|
||||
}
|
||||
|
||||
pub fn wechat_client(&self) -> &WechatClient {
|
||||
&self.wechat_client
|
||||
}
|
||||
|
||||
pub fn wechat_pay_client(&self) -> &WechatPayClient {
|
||||
&self.wechat_pay_client
|
||||
}
|
||||
@@ -1333,6 +1341,17 @@ fn build_oss_client(config: &AppConfig) -> Result<Option<OssClient>, AppStateIni
|
||||
Ok(Some(OssClient::new(oss_config)))
|
||||
}
|
||||
|
||||
fn build_wechat_client(config: &AppConfig) -> WechatClient {
|
||||
WechatClient::new(WechatConfig {
|
||||
app_id: config.wechat_mini_program_app_id.clone(),
|
||||
app_secret: config.wechat_mini_program_app_secret.clone(),
|
||||
stable_access_token_endpoint: config.wechat_stable_access_token_endpoint.clone(),
|
||||
subscribe_message_endpoint: config
|
||||
.wechat_mini_program_subscribe_message_endpoint
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_llm_client(config: &AppConfig) -> Result<Option<LlmClient>, AppStateInitError> {
|
||||
let Some(api_key) = config
|
||||
.llm_api_key
|
||||
|
||||
197
server-rs/crates/api-server/src/wechat_subscribe_message.rs
Normal file
197
server-rs/crates/api-server/src/wechat_subscribe_message.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use platform_wechat::WechatSubscribeMessageRequest;
|
||||
use shared_kernel::format_timestamp_micros;
|
||||
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 work_name: Option<String>,
|
||||
pub status: GenerationResultSubscribeMessageStatus,
|
||||
pub consumed_points: u64,
|
||||
pub completed_at_micros: i64,
|
||||
pub page: Option<String>,
|
||||
}
|
||||
|
||||
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<String, String> {
|
||||
BTreeMap::from([
|
||||
(
|
||||
"thing1".to_string(),
|
||||
truncate_template_value(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 {
|
||||
format_timestamp_micros(completed_at_micros)
|
||||
.replace('T', " ")
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
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(),
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user