补齐创作生成订阅消息通知
订阅消息任务名称改为玩法模板名。 拼图、敲木鱼、抓大鹅、跳一跳、方洞、视觉小说在草稿生成成功或失败终态发送通知。 订阅消息泥点字段按本次生成结算后的实际扣除展示,失败退款后显示0。 更新微信订阅消息运维和支付方案文档口径。
This commit is contained in:
@@ -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` 指向可发布的本地库。
|
||||
|
||||
|
||||
@@ -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 等待期间展示不可关闭遮罩“正在确认支付”,阻止用户离开或继续操作;只有确认到最终订单状态后才展示一次最终结果弹窗,不能先弹“正在支付/支付已提交”再二次弹成功。
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("敲木鱼"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user