补齐创作生成订阅消息通知

订阅消息任务名称改为玩法模板名。

拼图、敲木鱼、抓大鹅、跳一跳、方洞、视觉小说在草稿生成成功或失败终态发送通知。

订阅消息泥点字段按本次生成结算后的实际扣除展示,失败退款后显示0。

更新微信订阅消息运维和支付方案文档口径。
This commit is contained in:
kdletters
2026-06-08 19:21:05 +08:00
parent a4ee6ff698
commit 11c5e3edf4
10 changed files with 349 additions and 56 deletions

View File

@@ -45,6 +45,10 @@ use crate::{
},
request_context::RequestContext,
state::AppState,
wechat_subscribe_message::{
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
send_generation_result_subscribe_message_after_completion,
},
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
@@ -150,27 +154,86 @@ pub async fn execute_jump_hop_action(
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_CREATION_PROVIDER)?;
let owner_user_id = authenticated.claims().user_id().to_string();
let mut payload = payload;
maybe_generate_jump_hop_assets(
&state,
&request_context,
session_id.as_str(),
owner_user_id.as_str(),
&mut payload,
)
.await?;
let response = state
.spacetime_client()
.execute_jump_hop_action(session_id, owner_user_id, payload)
.await
.map_err(|error| {
jump_hop_error_response(
&request_context,
JUMP_HOP_CREATION_PROVIDER,
map_jump_hop_client_error(error),
)
})?;
let is_compile_draft = matches!(payload.action_type, JumpHopActionType::CompileDraft);
let generation_points_cost = if is_compile_draft {
resolve_jump_hop_generation_points_cost(&state).await
} else {
0
};
let result = async {
maybe_generate_jump_hop_assets(
&state,
&request_context,
session_id.as_str(),
owner_user_id.as_str(),
&mut payload,
)
.await?;
state
.spacetime_client()
.execute_jump_hop_action(session_id, owner_user_id.clone(), payload)
.await
.map_err(|error| {
jump_hop_error_response(
&request_context,
JUMP_HOP_CREATION_PROVIDER,
map_jump_hop_client_error(error),
)
})
}
.await;
Ok(json_success_body(Some(&request_context), response))
match result {
Ok(response) => {
if is_compile_draft && response.session.status == JumpHopGenerationStatus::Ready {
send_generation_result_subscribe_message_after_completion(
&state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some(JUMP_HOP_TEMPLATE_NAME.to_string()),
work_name: response
.session
.draft
.as_ref()
.map(|draft| draft.work_title.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: generation_points_cost,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
Ok(json_success_body(Some(&request_context), response))
}
Err(response) => {
if is_compile_draft && response.status().is_server_error() {
send_generation_result_subscribe_message_after_completion(
&state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some(JUMP_HOP_TEMPLATE_NAME.to_string()),
work_name: None,
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
Err(response)
}
}
}
async fn resolve_jump_hop_generation_points_cost(state: &AppState) -> u64 {
crate::creation_entry_config::resolve_creation_entry_mud_point_cost(
state,
JUMP_HOP_TEMPLATE_ID,
u64::from(shared_contracts::creation_entry_config::DEFAULT_UNIFIED_CREATION_MUD_POINT_COST),
)
.await
}
pub async fn publish_jump_hop_work(

View File

@@ -84,6 +84,10 @@ use crate::{
vector_engine_audio_generation::{
GeneratedCreationAudioTarget, generate_sound_effect_asset_for_creation,
},
wechat_subscribe_message::{
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
send_generation_result_subscribe_message_after_completion,
},
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
const MATCH3D_AGENT_PROVIDER: &str = "match3d-agent";

View File

@@ -323,27 +323,56 @@ pub(super) async fn compile_match3d_draft_for_session(
)
.await;
if let Err(response) = result.as_ref()
&& response.status().is_server_error()
{
let failure_message = match3d_response_failure_message(response);
persist_failed_match3d_draft_generation(
state,
request_context,
authenticated,
compile_session_id,
compile_owner_user_id,
compile_profile_id,
compile_initial_game_name,
compile_requested_summary,
compile_initial_tags,
compile_requested_cover_image_src,
failure_message,
)
.await;
match result {
Ok((session, generated_item_assets)) => {
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id: compile_owner_user_id.clone(),
task_name: Some("抓大鹅".to_string()),
work_name: session.draft.as_ref().map(|draft| draft.game_name.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: points_cost,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
Ok((session, generated_item_assets))
}
Err(response) if response.status().is_server_error() => {
let failure_message = match3d_response_failure_message(&response);
persist_failed_match3d_draft_generation(
state,
request_context,
authenticated,
compile_session_id,
compile_owner_user_id.clone(),
compile_profile_id,
compile_initial_game_name.clone(),
compile_requested_summary,
compile_initial_tags,
compile_requested_cover_image_src,
failure_message,
)
.await;
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id: compile_owner_user_id,
task_name: Some("抓大鹅".to_string()),
work_name: Some(compile_initial_game_name),
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
Err(response)
}
Err(response) => Err(response),
}
result
}
#[allow(clippy::too_many_arguments)]

View File

@@ -641,6 +641,7 @@ pub async fn execute_puzzle_agent_action(
state.root_state(),
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some("拼图".to_string()),
work_name: None,
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
@@ -768,6 +769,7 @@ pub async fn execute_puzzle_agent_action(
&background_root_state,
GenerationResultSubscribeMessage {
owner_user_id: background_owner_user_id.clone(),
task_name: Some("拼图".to_string()),
work_name: session
.draft
.as_ref()
@@ -814,6 +816,7 @@ pub async fn execute_puzzle_agent_action(
&background_root_state,
GenerationResultSubscribeMessage {
owner_user_id: background_owner_user_id.clone(),
task_name: Some("拼图".to_string()),
work_name: background_work_name.clone(),
status:
GenerationResultSubscribeMessageStatus::Failed,
@@ -1491,6 +1494,7 @@ pub async fn execute_puzzle_agent_action(
state.root_state(),
GenerationResultSubscribeMessage {
owner_user_id: owner_user_id.clone(),
task_name: Some("拼图".to_string()),
work_name: session.draft.as_ref().map(|draft| draft.work_title.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: operation_consumed_points,

View File

@@ -81,12 +81,18 @@ use crate::{
SquareHoleAgentTurnRequest, build_finalize_record_input, run_square_hole_agent_turn,
},
state::AppState,
wechat_subscribe_message::{
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
send_generation_result_subscribe_message_after_completion,
},
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
const SQUARE_HOLE_AGENT_PROVIDER: &str = "square-hole-agent";
const SQUARE_HOLE_WORKS_PROVIDER: &str = "square-hole-works";
const SQUARE_HOLE_RUNTIME_PROVIDER: &str = "square-hole-runtime";
const SQUARE_HOLE_TEMPLATE_ID: &str = "square-hole";
const SQUARE_HOLE_TEMPLATE_NAME: &str = "方洞";
const SQUARE_HOLE_DEFAULT_THEME: &str = "纸箱";
const SQUARE_HOLE_DEFAULT_TWIST_RULE: &str = "方洞万能";
const SQUARE_HOLE_DEFAULT_SHAPE_COUNT: u32 = 12;
@@ -1112,14 +1118,21 @@ async fn compile_square_hole_draft_for_session(
.as_ref()
.map(|tags| serde_json::to_string(&normalize_tags(tags.clone())).unwrap_or_default());
state
let resolved_game_name = game_name.or_else(|| Some(format!("{}方洞挑战", config.theme_text)));
let generation_points_cost = crate::creation_entry_config::resolve_creation_entry_mud_point_cost(
state,
SQUARE_HOLE_TEMPLATE_ID,
u64::from(shared_contracts::creation_entry_config::DEFAULT_UNIFIED_CREATION_MUD_POINT_COST),
)
.await;
let result = state
.spacetime_client()
.compile_square_hole_draft(SquareHoleCompileDraftRecordInput {
session_id,
owner_user_id,
owner_user_id: owner_user_id.clone(),
profile_id: build_prefixed_uuid_id(SQUARE_HOLE_PROFILE_ID_PREFIX),
author_display_name: resolve_author_display_name(state, authenticated),
game_name: game_name.or_else(|| Some(format!("{}方洞挑战", config.theme_text))),
game_name: resolved_game_name.clone(),
summary_text: summary,
tags_json,
cover_image_src,
@@ -1132,7 +1145,43 @@ async fn compile_square_hole_draft_for_session(
SQUARE_HOLE_AGENT_PROVIDER,
map_square_hole_client_error(error),
)
})
});
match result {
Ok(session) => {
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some(SQUARE_HOLE_TEMPLATE_NAME.to_string()),
work_name: session.draft.as_ref().map(|draft| draft.game_name.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: generation_points_cost,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
Ok(session)
}
Err(response) => {
if response.status().is_server_error() {
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some(SQUARE_HOLE_TEMPLATE_NAME.to_string()),
work_name: resolved_game_name,
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
Err(response)
}
}
}
mod visual_assets;

View File

@@ -35,6 +35,10 @@ use crate::{
prompt::visual_novel as vn_prompt,
request_context::RequestContext,
state::AppState,
wechat_subscribe_message::{
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
send_generation_result_subscribe_message_after_completion,
},
work_author::resolve_work_author_by_user_id,
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
@@ -1743,8 +1747,15 @@ async fn compile_visual_novel_session_inner(
current_utc_iso().as_str(),
);
let projection = project_draft_for_work(&draft, &profile_id)?;
let notification_work_name = projection.work_title.clone();
let generation_points_cost = crate::creation_entry_config::resolve_creation_entry_mud_point_cost(
state,
VISUAL_NOVEL_RUNTIME_KIND,
u64::from(shared_contracts::creation_entry_config::DEFAULT_UNIFIED_CREATION_MUD_POINT_COST),
)
.await;
let author = resolve_work_author_by_user_id(state, &owner_user_id, None, None);
let compiled_session = state
let compile_result = state
.spacetime_client()
.compile_visual_novel_work_profile(VisualNovelWorkCompileRecordInput {
session_id: session_id.clone(),
@@ -1761,7 +1772,43 @@ async fn compile_visual_novel_session_inner(
.await
.map_err(|error| {
visual_novel_error_response(request_context, map_spacetime_error(error))
})?;
});
let compiled_session = match compile_result {
Ok(session) => {
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id: owner_user_id.clone(),
task_name: Some("视觉小说".to_string()),
work_name: Some(notification_work_name.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: generation_points_cost,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
session
}
Err(response) => {
if response.status().is_server_error() {
send_generation_result_subscribe_message_after_completion(
state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some("视觉小说".to_string()),
work_name: Some(notification_work_name),
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
return Err(response);
}
};
let work = state
.spacetime_client()
.get_visual_novel_work_detail(profile_id, owner_user_id)

View File

@@ -19,6 +19,7 @@ pub enum GenerationResultSubscribeMessageStatus {
#[derive(Clone, Debug)]
pub struct GenerationResultSubscribeMessage {
pub owner_user_id: String,
pub task_name: Option<String>,
pub work_name: Option<String>,
pub status: GenerationResultSubscribeMessageStatus,
pub consumed_points: u64,
@@ -110,7 +111,13 @@ fn build_generation_result_template_data(
BTreeMap::from([
(
"thing1".to_string(),
truncate_template_value(GENERATION_RESULT_TASK_NAME, 20),
truncate_template_value(
message
.task_name
.as_deref()
.unwrap_or(GENERATION_RESULT_TASK_NAME),
20,
),
),
(
"phrase2".to_string(),
@@ -192,6 +199,7 @@ mod tests {
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,
@@ -207,6 +215,7 @@ mod tests {
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,
@@ -219,4 +228,19 @@ mod tests {
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("敲木鱼"));
}
}

View File

@@ -43,6 +43,10 @@ use crate::{
platform_errors::map_oss_error,
request_context::RequestContext,
state::AppState,
wechat_subscribe_message::{
GenerationResultSubscribeMessage, GenerationResultSubscribeMessageStatus,
send_generation_result_subscribe_message_after_completion,
},
};
const WOODEN_FISH_PROVIDER: &str = "wooden-fish";
@@ -147,6 +151,15 @@ pub async fn execute_wooden_fish_action(
wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?;
let owner_user_id = authenticated.claims().user_id().to_string();
let author_display_name = resolve_author_display_name(&state, &authenticated);
let is_compile_draft = matches!(
payload.action_type,
shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft
);
let generation_points_cost = if is_compile_draft {
resolve_wooden_fish_generation_points_cost(&state).await
} else {
0
};
let result = execute_wooden_fish_action_with_generated_assets(
&state,
&request_context,
@@ -160,21 +173,55 @@ pub async fn execute_wooden_fish_action(
.as_ref()
.err()
.is_some_and(|response| response.status().is_server_error())
&& matches!(
payload.action_type,
shared_contracts::wooden_fish::WoodenFishActionType::CompileDraft
)
&& is_compile_draft
{
mark_wooden_fish_generation_failed(
let failed_at_micros = current_utc_micros();
let work_name =
resolve_wooden_fish_notification_work_name(&state, &session_id, &owner_user_id).await;
if mark_wooden_fish_generation_failed(
&state,
&request_context,
&session_id,
owner_user_id.as_str(),
author_display_name.as_str(),
)
.await;
.await
{
send_generation_result_subscribe_message_after_completion(
&state,
GenerationResultSubscribeMessage {
owner_user_id: owner_user_id.clone(),
task_name: Some(WOODEN_FISH_TEMPLATE_NAME.to_string()),
work_name,
status: GenerationResultSubscribeMessageStatus::Failed,
consumed_points: 0,
completed_at_micros: failed_at_micros,
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
}
let response = result?;
if is_compile_draft && response.session.status == WoodenFishGenerationStatus::Ready {
send_generation_result_subscribe_message_after_completion(
&state,
GenerationResultSubscribeMessage {
owner_user_id,
task_name: Some(WOODEN_FISH_TEMPLATE_NAME.to_string()),
work_name: response
.session
.draft
.as_ref()
.map(|draft| draft.work_title.clone()),
status: GenerationResultSubscribeMessageStatus::Succeeded,
consumed_points: generation_points_cost,
completed_at_micros: current_utc_micros(),
page: Some("/pages/web-view/index".to_string()),
},
)
.await;
}
Ok(json_success_body(Some(&request_context), response))
}
@@ -588,13 +635,37 @@ async fn execute_wooden_fish_action_with_generated_assets(
})
}
async fn resolve_wooden_fish_generation_points_cost(state: &AppState) -> u64 {
crate::creation_entry_config::resolve_creation_entry_mud_point_cost(
state,
WOODEN_FISH_TEMPLATE_ID,
u64::from(shared_contracts::creation_entry_config::DEFAULT_UNIFIED_CREATION_MUD_POINT_COST),
)
.await
}
async fn resolve_wooden_fish_notification_work_name(
state: &AppState,
session_id: &str,
owner_user_id: &str,
) -> Option<String> {
state
.spacetime_client()
.get_wooden_fish_session(session_id.to_string(), owner_user_id.to_string())
.await
.ok()
.and_then(|session| session.draft)
.map(|draft| draft.work_title)
.filter(|value| !value.trim().is_empty())
}
async fn mark_wooden_fish_generation_failed(
state: &AppState,
request_context: &RequestContext,
session_id: &str,
owner_user_id: &str,
author_display_name: &str,
) {
) -> bool {
if let Err(error) = state
.spacetime_client()
.mark_wooden_fish_generation_failed(
@@ -612,7 +683,9 @@ async fn mark_wooden_fish_generation_failed(
error = %error,
"敲木鱼草稿生成失败后的状态回写失败"
);
return false;
}
true
}
fn default_wooden_fish_hit_object_asset() -> WoodenFishImageAsset {