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

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

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

订阅消息泥点字段按本次生成结算后的实际扣除展示,失败退款后显示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

@@ -55,7 +55,7 @@ Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模
微信小程序虚拟支付使用 `WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID``WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY``WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY``WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV` 配置。小程序充值统一走 `wechat_mp_virtual` / `wx.requestVirtualPayment`:泥点属于代币(`coin``buyQuantity` 按当前充值商品快照里的 `points_amount` 传;会员和后台新增道具类商品走 `short_series_goods``productId` 对应微信后台道具 ID。旧登录快照若缺 `session_key`,需要用户在小程序内重新登录后再支付;客户端成功回调不是最终到账,仍以后端通知或查询确认订单为准。详细口径见 `docs/【技术方案】微信虚拟支付接入-2026-05-26.md`
微信小程序订阅消息生成结果通知使用 `WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED``WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID``WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE` 配置。当前模板为 `AI创作生成结果通知`H5 在拼图 `compile_puzzle_draft` 生成动作发起前先进入生成进度态并立即继续生成动作,同时非阻塞跳转到小程序原生订阅授权页尝试请求授权,用户接受、拒绝或返回都不能阻塞生成,且原生页不改写上一页 `webViewUrl`,避免返回后丢失 H5 当前进度页状态。后端只在拼图资产生成成功或失败终态后用微信登录保存的 openid 调用 `subscribeMessage.send`,发送失败只打 warning不影响生成主链路。模板 `time4` 字段固定发送北京时间 `YYYY-MM-DD HH:mm`,不要使用内部微秒时间戳、秒级时间戳或带时区后缀的 RFC3339 字符串,否则微信会返回 `argument invalid! data.time4.value invalid`
微信小程序订阅消息生成结果通知使用 `WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED``WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID``WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE` 配置。当前模板为 `AI创作生成结果通知`H5 在生成动作发起前先进入生成进度态并立即继续生成动作,同时非阻塞跳转到小程序原生订阅授权页尝试请求授权,用户接受、拒绝或返回都不能阻塞生成,且原生页不改写上一页 `webViewUrl`,避免返回后丢失 H5 当前进度页状态。后端只在玩法草稿生成成功或失败终态后用微信登录保存的 openid 调用 `subscribeMessage.send`,发送失败只打 warning不影响生成主链路。模板 `thing1` 字段发送玩法模板名,例如 `拼图``敲木鱼``抓大鹅``number6` 字段发送本次生成结算后的实际泥点扣除,失败退款后固定为 `0`。模板 `time4` 字段固定发送北京时间 `YYYY-MM-DD HH:mm`,不要使用内部微秒时间戳、秒级时间戳或带时区后缀的 RFC3339 字符串,否则微信会返回 `argument invalid! data.time4.value invalid`当前已接入拼图、敲木鱼、抓大鹅、跳一跳、方洞、视觉小说的草稿生成终态;分槽素材生成或发布动作不得直接复用生成结果通知,避免一次作品生成产生多条订阅消息。
如果本地 `GET /api/creation-entry/config` 返回 `No such procedure`,或 `api-server` 日志出现 `no such table: puzzle_gallery_card_view` / `no such table: wooden_fish_gallery_card_view` 这类公开 view 缺失,通常是 `.env.local` 指向的 SpacetimeDB 库还没有发布当前 `spacetime-module`,或当前 CLI 身份无权发布该库。debug 构建的 `api-server` 会临时使用后端默认入口配置兜底,避免创作作品架整块消失;正式修复仍应切换到拥有目标库权限的 SpacetimeDB 身份后重新运行 `npm run dev` 完成发布,或用 gitignored 的 `spacetime.local.json` 指向可发布的本地库。

View File

@@ -72,5 +72,5 @@ npm run check:encoding
- 沙箱或基础库失败会把微信返回的 `errCode` / `errMsg` 透传到前端失败弹窗,便于区分微信后台道具、沙箱 AppKey、签名和基础库能力问题。
- Web 侧在拉起虚拟支付后会短时轮询 `wx_pay_result`,即使小程序 `web-view` 回写 hash 没触发浏览器 `hashchange`,也必须展示回写的微信错误内容。
- WebView 返回但没有拿到 `wx_pay_result` 时,前端必须主动调用订单确认接口,并接入 `/api/profile/recharge/orders/{orderId}/wechat/events` 的 SSE 事件流作为服务端推送兜底后端收到虚拟支付消息推送并入账后会发布订单更新SSE 先推当前订单快照,再在订单结束时推 `done`
- 小程序订阅消息用于拼图 AI 创作生成结果通知H5 在拼图 `compile_puzzle_draft` 生成动作发起前先把页面切到生成进度态并立即调用生成 action同时非阻塞跳转到小程序原生订阅授权页尝试请求授权授权接受、拒绝或页面返回都不得阻塞或取消生成。原生页不得改写上一页 `webViewUrl`,避免返回后丢失 H5 当前进度页状态。通知发送只允许发生在拼图后台首图 / UI 资产生成成功或失败终态之后api-server 使用当前用户微信登录保存的 openid 调用微信 `subscribeMessage.send`。发送失败只记录 warning不阻断作品生成。模板 `time4` 字段必须是北京时间 `YYYY-MM-DD HH:mm``WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE` 支持 `formal` / `trial` / `developer`,应与当前发布环境一致。
- 小程序订阅消息用于 AI 创作生成结果通知H5 在生成动作发起前先把页面切到生成进度态并立即调用生成 action同时非阻塞跳转到小程序原生订阅授权页尝试请求授权授权接受、拒绝或页面返回都不得阻塞或取消生成。原生页不得改写上一页 `webViewUrl`,避免返回后丢失 H5 当前进度页状态。通知发送只允许发生在玩法草稿生成成功或失败终态之后api-server 使用当前用户微信登录保存的 openid 调用微信 `subscribeMessage.send`。发送失败只记录 warning不阻断作品生成。模板 `thing1` 发送玩法模板名,`number6` 发送本次生成结算后的实际泥点扣除,失败退款后固定为 `0`;模板 `time4` 字段必须是北京时间 `YYYY-MM-DD HH:mm``WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE` 支持 `formal` / `trial` / `developer`,应与当前发布环境一致。
- WebView 返回后,在订单状态拉取或 SSE 等待期间展示不可关闭遮罩“正在确认支付”,阻止用户离开或继续操作;只有确认到最终订单状态后才展示一次最终结果弹窗,不能先弹“正在支付/支付已提交”再二次弹成功。

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 {