改造充值订单过期补偿流程

新增 SpacetimeDB 原生充值订单过期 timer 与 expired 状态。

移除 api-server 轮询 worker,改为 HTTP 角色订阅 Pending 到 Expired 事件并查单补偿。

统一微信下单 5 分钟 time_expire,并恢复前端支付通道选择与 expired 展示。

补齐生成绑定、前后端契约、测试和项目文档。
This commit is contained in:
kdletters
2026-07-10 01:37:49 +08:00
parent 6ddde830ef
commit 00ff425e03
383 changed files with 4811 additions and 2938 deletions
@@ -16,6 +16,14 @@
---
## 2026-07-09 充值订单过期改为 SpacetimeDB scheduled 表触发
- 背景:旧充值过期处理使用 api-server 后台轮询 worker claim 普通 schedule 表,非 HTTP 的 external-generation-worker / controller 进程也可能启动同一过期任务;扩外部生成 worker 会意外放大微信查单 / 关单流量,并且本地过期后若微信仍可支付,容易出现“微信扣款但本地拒绝入账”的风险。
- 决策:新建原生 scheduled 表 `profile_recharge_order_expiration_timer`,创建真实微信 pending 充值订单时写入 5 分钟 timerscheduled reducer 到点只把仍为 `pending` 的订单改为 `expired` 并写 `expired_at`。HTTP `api-server` 订阅 `profile_recharge_order``Pending -> Expired` 更新并执行微信查单补偿;`SUCCESS` 允许 `Expired -> Paid` 入账,未支付或远端已终态只记录检查结果,本地保持 `expired``external-generation-worker` 和 controller 不处理充值过期。
- 影响范围:`profile_recharge_order``profile_recharge_order_expiration_timer`、充值订单状态契约、`spacetime-client` bindings/facade、`api-server` 充值过期监听器、微信支付查单 / 关单、个人中心充值前端、后台表查询和运维文档。
- 验证方式:`npm run spacetime:generate``npm run check:spacetime-schema``cargo check -p api-server --manifest-path server-rs/Cargo.toml`、充值过期 listener / 微信支付 / shared contracts / 前端充值定向测试、`npm run check:encoding``git diff --check`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-07-09 会员有效期与周期泥点重置分离
- 背景:账户会员制度新增 Starter / Basic / Pro / Ultimate 四档后,会员有效期、周期限时泥点和普通永久泥点容易被混成同一条时间线;升级场景尤其容易误把“补差额”实现成延长会员或重算 reset time。
@@ -2058,10 +2058,18 @@
- 现象:后台“表查询”查看 `profile_recharge_order` 时,`kind``status` 显示为空数组 `[]`,例如充值订单原始行里 `points_60` 的类型和状态都不可读。
- 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态 `[variant_index, []]`;后台通用 normalizer 曾把任何 `[0, value]` 都当作 `Option::Some(value)` 展开,导致 `[0, []]` 最终只剩 `[]`
- 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如 `profile_recharge_order.kind` 映射为 `points` / `membership``profile_recharge_order.status` 映射为 `pending` / `paid` / `failed` / `closed` / `refunded`
- 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如 `profile_recharge_order.kind` 映射为 `points` / `membership``profile_recharge_order.status` 映射为 `pending` / `paid` / `failed` / `closed` / `refunded` / `expired`
- 验证:执行 `cargo test -p api-server admin_database -- --nocapture`,并确认后台详情弹层的 `raw` 与表格 `cells` 都显示业务字符串。
- 关联:`server-rs/crates/api-server/src/admin.rs``docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md`
## 充值订单过期补偿不要放进外部生成 worker
- 现象:外部生成 worker/controller 扩容后,微信充值过期查单和关单流量也被同步放大;排查时还会误去外部生成 worker 日志里找支付过期任务。
- 原因:支付过期是账户资金链路,不是外部内容生成队列;旧实现把充值过期轮询 worker 挂在通用后台任务启动函数里,非 HTTP 角色也会启动。
- 处理:充值订单过期由 SpacetimeDB 原生 `profile_recharge_order_expiration_timer` 到点把 `pending` 改为 `expired`,只有 HTTP `api-server` 订阅 `profile_recharge_order``Pending -> Expired` 更新并查微信补偿。未支付终态本地保持 `expired`,不要再改写成 `closed`;微信成功支付通知或补偿查单仍可把 `Expired -> Paid` 入账。
- 验证:确认 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` / `external-generation-controller` 不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生 `expired`HTTP api-server listener 记录 `expiration_checked_at` 或补入账。
- 关联:`server-rs/crates/api-server/src/profile_recharge_expiration_listener.rs``server-rs/crates/spacetime-module/src/runtime/profile.rs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
@@ -197,10 +197,10 @@ npm run check:server-rs-ddd
7. 会员有效期和周期重置时间是两条独立时间线。`expires_at` 只决定会员是否生效;`cycle_resets_at` 只决定当前周期限时泥点何时重置。会员升级只更新档位并补齐当前周期限时泥点差额,不延长 `expires_at`,不移动 `cycle_resets_at` 和周期天数。同级会员购买只从当前 `expires_at` 延长有效期,不发放额外当前周期泥点,也不移动重置时间。
8. 会员周期刷新发生在个人中心、充值中心、任务中心、账单读取和钱包扣费入口:到达 `cycle_resets_at` 时先清除上周期剩余限时泥点,再发放当前会员档位周期额度;会员过期时清除剩余限时泥点并把状态降为普通。周期发放和重置流水分别使用 `membership_period_grant``membership_period_reset`
9. `paymentChannel` 缺失、未知或冒用小程序支付设备时必须拒绝;真实微信渠道只允许 `wechat_mp``wechat_mp_virtual``wechat_jsapi``wechat_h5``wechat_native`,生产配置不得把真实支付静默降级为 `mock`
10. access JWT 只携带最小设备快照 `device.client_type``device.client_runtime``device.client_platform`。充值下单按该快照拦截小程序渠道:小程序只允许 `wechat_mp` / `wechat_mp_virtual`微信内浏览器使用 `wechat_jsapi`;普通 Web 使用 `wechat_native`历史普通 Web 登录态若缺少设备快照也允许继续进入 JSAPI / H5 / Native 渠道的后续支付配置校验,但不放宽小程序虚拟支付。
10. access JWT 只携带最小设备快照 `device.client_type``device.client_runtime``device.client_platform`。充值下单按该快照拦截小程序渠道:小程序只允许 `wechat_mp` / `wechat_mp_virtual`移动网页和微信内 H5 走 `wechat_h5`;桌面网页和桌面微信走 `wechat_native``wechat_jsapi` 仅保留后端能力,未接微信开放平台前不由前端自动选择。历史普通 Web 登录态若缺少设备快照也允许继续进入 JSAPI / H5 / Native 渠道的后续支付配置校验,但不放宽小程序虚拟支付。
11. 所有微信真实渠道都以微信支付通知或服务端查单确认 `SUCCESS` 为到账事实;小程序、H5 跳转和 Native 二维码返回都不能直接发放泥点或会员。
12. 微信 Native 下单显式传 `time_expire`当前有效期为 5 分钟,并通过 `wechatNativePayment.expiresAt` 下发给前端二维码弹窗展示。
13. 普通微信支付渠道的新建 pending 充值订单会写入 `profile_recharge_order_expiration_schedule`。到期处理由 `api-server` 后台 worker claim 调度行后调用微信查单;只有微信返回 `SUCCESS` 才补确认入账,返回 `NOTPAY` / `CLOSED` / `REVOKED` / `PAYERROR` 才关闭本地订单,查询失败或 `USERPAYING` 保留租约等待重试。SpacetimeDB module 不直接发起微信 HTTP 请求
12. 微信 JSAPI / H5 / 小程序 / Native 下单统一显式传 5 分钟 `time_expire`格式为 RFC3339 秒级时间;Native 额外通过 `wechatNativePayment.expiresAt` 下发给前端二维码弹窗展示。
13. 真实微信渠道的新建 pending 充值订单会写入 SpacetimeDB 原生 scheduled 表 `profile_recharge_order_expiration_timer`。到期 reducer 只做数据库内状态转换:订单仍为 `pending` 时更新为 `expired` 并写 `expired_at`,同时删除 timer。HTTP `api-server` 订阅 `profile_recharge_order``Pending -> Expired` 更新并执行微信查单补偿:`SUCCESS` 可把 `expired` 补确认成 `paid` 入账;`NOTPAY` 会调用微信关单并把本地订单保持为 `expired``CLOSED` / `REVOKED` / `PAYERROR` / `ORDER_NOT_EXIST` 只记录检查结果。`external-generation-worker` / controller处理充值过期;`wechat_mp_virtual` 到期只记录虚拟渠道不可查,后续真实支付通知仍允许 `Expired -> Paid`
## 创作入口泥点扣费契约
@@ -704,12 +704,19 @@ npm run check:server-rs-ddd
- Rust 结构体:`ProfileRechargeOrder`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 作用:账户充值订单事实源。`status` 包含 `pending``paid``failed``closed``refunded``expired`;过期补偿字段 `expired_at``expiration_checked_at``expiration_provider_state``expiration_last_error` 用于记录本地过期和微信查单结果。
### `profile_recharge_order_expiration_schedule`
- Rust 结构体:`ProfileRechargeOrderExpirationSchedule`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 作用:普通微信充值订单到期查单调度表。表内只保存待检查订单、计划检查时间和 worker 短租约;支付成功或本地关闭后删除对应行
- 作用:普通微信充值订单到期查单调度表,保留 schema 兼容但当前不再写入;当前过期路径改由原生 scheduled 表 `profile_recharge_order_expiration_timer` 触发
### `profile_recharge_order_expiration_timer`
- Rust 结构体:`ProfileRechargeOrderExpirationTimer`
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
- 作用:充值订单原生 scheduled 表,`scheduled_at` 到点触发 `expire_profile_recharge_order_timer``order_id` 唯一,支付成功、本地关闭或 scheduled reducer 执行后删除对应行。
### `profile_redeem_code`
@@ -71,6 +71,8 @@ Windows 本地如果已在 `%LOCALAPPDATA%\Genarrative\ffmpeg\bin` 安装 FFmpeg
微信小程序虚拟支付使用 `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`
普通微信充值订单本地有效期为 5 分钟。SpacetimeDB 原生 `profile_recharge_order_expiration_timer` 到点后只把仍为 `pending` 的订单改为 `expired`HTTP `api-server` 通过订阅 `profile_recharge_order``Pending -> Expired` 更新执行微信查单补偿。`external-generation-worker``external-generation-controller` 不运行充值过期逻辑,也不应因为扩容外部生成 worker 放大微信查单或关单流量。查账时本地未支付终态保持 `expired`,不再改写为 `closed``expiration_checked_at``expiration_provider_state``expiration_last_error` 用于判断 HTTP 监听器是否已经完成补偿。
微信小程序订阅消息生成结果通知使用 `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` 指向可发布的本地库。
@@ -587,6 +589,9 @@ SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created
-- 充值订单
SELECT * FROM profile_recharge_order ORDER BY created_at DESC LIMIT 50;
-- 未完成过期补偿的充值订单
SELECT * FROM profile_recharge_order WHERE status = 'expired' AND expiration_checked_at IS NULL ORDER BY expired_at ASC;
-- 充值商品配置
SELECT * FROM profile_recharge_product_config ORDER BY sort_order ASC;
```
+6 -1
View File
@@ -117,7 +117,8 @@ export type ProfileRechargeOrderStatus =
| 'paid'
| 'failed'
| 'closed'
| 'refunded';
| 'refunded'
| 'expired';
export type ProfileRechargeProduct = {
productId: string;
@@ -174,6 +175,10 @@ export type ProfileRechargeOrder = {
createdAt: string;
pointsDelta: number;
membershipExpiresAt: string | null;
expiredAt?: string | null;
expirationCheckedAt?: string | null;
expirationProviderState?: string | null;
expirationLastError?: string | null;
};
export type ProfileRechargeCenterResponse = {
+1
View File
@@ -2447,6 +2447,7 @@ fn normalize_admin_database_known_enum(
2 => "failed",
3 => "closed",
4 => "refunded",
5 => "expired",
_ => return None,
},
_ => return None,
+30 -7
View File
@@ -75,7 +75,7 @@ mod phone_auth;
mod platform_errors;
mod process_metrics;
mod profile_identity;
mod profile_recharge_expiration_worker;
mod profile_recharge_expiration_listener;
mod prompt;
mod public_work;
mod puzzle;
@@ -132,7 +132,7 @@ use crate::{
config::{AppConfig, ProcessRole},
external_generation_worker::run_external_generation_worker,
external_generation_worker_controller::run_external_generation_worker_controller,
profile_recharge_expiration_worker::spawn_profile_recharge_expiration_worker,
profile_recharge_expiration_listener::spawn_profile_recharge_expiration_listener,
state::{AppState, AppStateInitError},
tracking_outbox::TrackingOutbox,
wallet_refund_outbox::WalletRefundOutbox,
@@ -200,7 +200,7 @@ async fn run_worker_only(config: AppConfig) -> Result<(), io::Error> {
"初始化 external generation worker 状态失败:{error}"
))
})?;
spawn_app_state_background_workers(&state);
spawn_common_app_state_background_workers(&state);
info!(
process_role = process_role.as_str(),
"api-server 以非 HTTP 角色启动"
@@ -245,7 +245,7 @@ async fn run_http_role(config: AppConfig) -> Result<(), io::Error> {
let (router, shutdown_context, worker_state) = match restore_app_state_for_startup(config).await
{
Ok(state) => {
spawn_app_state_background_workers(&state);
spawn_http_app_state_background_workers(&state, process_role);
let tracking_outbox = state.tracking_outbox();
let wallet_refund_outbox = state.wallet_refund_outbox();
let worker_state = process_role
@@ -408,9 +408,8 @@ async fn finalize_shutdown(context: ShutdownContext) {
}
}
fn spawn_app_state_background_workers(state: &AppState) {
fn spawn_common_app_state_background_workers(state: &AppState) {
state.puzzle_gallery_cache().spawn_cleanup_task();
spawn_profile_recharge_expiration_worker(state.clone());
if let Some(outbox) = state.tracking_outbox() {
outbox.spawn_worker();
}
@@ -419,6 +418,17 @@ fn spawn_app_state_background_workers(state: &AppState) {
}
}
fn spawn_http_app_state_background_workers(state: &AppState, process_role: ProcessRole) {
spawn_common_app_state_background_workers(state);
if should_start_profile_recharge_expiration_listener(process_role) {
spawn_profile_recharge_expiration_listener(state.clone());
}
}
fn should_start_profile_recharge_expiration_listener(process_role: ProcessRole) -> bool {
process_role.runs_http()
}
fn build_tcp_listener(
bind_address: SocketAddr,
listen_backlog: i32,
@@ -549,7 +559,8 @@ fn is_valid_env_key(key: &str) -> bool {
mod tests {
use super::{
AUTH_STORE_STARTUP_RETRY_INTERVAL, is_valid_env_key, protected_env_keys_from,
should_restore_auth_store_for_startup, strip_env_value,
should_restore_auth_store_for_startup, should_start_profile_recharge_expiration_listener,
strip_env_value,
};
use crate::config::ProcessRole;
@@ -609,4 +620,16 @@ mod tests {
ProcessRole::ExternalGenerationController
));
}
#[test]
fn profile_recharge_expiration_listener_is_limited_to_http_roles() {
assert!(should_start_profile_recharge_expiration_listener(ProcessRole::Api));
assert!(should_start_profile_recharge_expiration_listener(ProcessRole::All));
assert!(!should_start_profile_recharge_expiration_listener(
ProcessRole::ExternalGenerationWorker
));
assert!(!should_start_profile_recharge_expiration_listener(
ProcessRole::ExternalGenerationController
));
}
}
@@ -0,0 +1,345 @@
use std::{collections::HashSet, time::Duration};
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
};
use platform_wechat::pay::{WechatPayError, WechatPayNotifyOrder};
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
use tokio::time::sleep;
use tracing::{debug, info, warn};
use crate::{state::AppState, wechat::pay::current_unix_micros};
const PROFILE_RECHARGE_EXPIRATION_LISTENER_RECONNECT_DELAY: Duration = Duration::from_secs(5);
const PROFILE_RECHARGE_EXPIRATION_CATCH_UP_LIMIT: u32 = 100;
const PROFILE_RECHARGE_EXPIRATION_RETRY_DELAYS: [Duration; 3] = [
Duration::from_secs(5),
Duration::from_secs(15),
Duration::from_secs(30),
];
pub fn spawn_profile_recharge_expiration_listener(state: AppState) {
tokio::spawn(async move {
run_profile_recharge_expiration_listener(state).await;
});
}
async fn run_profile_recharge_expiration_listener(state: AppState) {
loop {
match state
.spacetime_client()
.subscribe_profile_recharge_order_expiration_events()
.await
{
Ok(mut subscription) => {
info!("profile recharge expiration listener connected");
run_profile_recharge_expiration_catch_up(&state).await;
loop {
match subscription.recv().await {
Ok(order) => {
let state = state.clone();
tokio::spawn(async move {
process_expired_profile_recharge_order_with_retries(state, order)
.await;
});
}
Err(error) => {
warn!(
error = %error,
"profile recharge expiration listener disconnected"
);
break;
}
}
}
}
Err(error) => {
warn!(
error = %error,
"profile recharge expiration listener failed to subscribe"
);
}
}
sleep(PROFILE_RECHARGE_EXPIRATION_LISTENER_RECONNECT_DELAY).await;
}
}
async fn run_profile_recharge_expiration_catch_up(state: &AppState) {
let mut processed_count = 0usize;
let mut seen_order_ids = HashSet::new();
loop {
let mut orders = match state
.spacetime_client()
.list_unchecked_expired_profile_recharge_orders(
PROFILE_RECHARGE_EXPIRATION_CATCH_UP_LIMIT,
)
.await
{
Ok(orders) => orders,
Err(error) => {
warn!(
error = %error,
"profile recharge expiration catch-up failed to list unchecked orders"
);
return;
}
};
if orders.is_empty() {
if processed_count == 0 {
debug!("profile recharge expiration catch-up found no unchecked orders");
}
return;
}
let listed_len = orders.len();
orders.retain(|order| seen_order_ids.insert(order.order_id.clone()));
if orders.is_empty() {
return;
}
let batch_len = orders.len();
processed_count += batch_len;
info!(
order_count = batch_len,
total_count = processed_count,
"profile recharge expiration catch-up processing unchecked orders"
);
let mut handles = Vec::with_capacity(batch_len);
for order in orders {
let state = state.clone();
handles.push(tokio::spawn(async move {
process_expired_profile_recharge_order_with_retries(state, order).await;
}));
}
for handle in handles {
if let Err(error) = handle.await {
warn!(
error = %error,
"profile recharge expiration catch-up task failed to join"
);
}
}
if listed_len < PROFILE_RECHARGE_EXPIRATION_CATCH_UP_LIMIT as usize {
return;
}
tokio::task::yield_now().await;
}
}
async fn process_expired_profile_recharge_order_with_retries(
state: AppState,
order: RuntimeProfileRechargeOrderRecord,
) {
let order_id = order.order_id.clone();
for (attempt_index, delay) in std::iter::once(Duration::ZERO)
.chain(PROFILE_RECHARGE_EXPIRATION_RETRY_DELAYS)
.enumerate()
{
if !delay.is_zero() {
sleep(delay).await;
}
match process_expired_profile_recharge_order_once(&state, &order).await {
Ok(()) => return,
Err(error) => {
warn!(
order_id = order_id.as_str(),
attempt = attempt_index + 1,
error = %error,
"profile recharge expiration compensation attempt failed"
);
record_profile_recharge_expiration_error(&state, &order_id, error.to_string())
.await;
}
}
}
}
async fn process_expired_profile_recharge_order_once(
state: &AppState,
order: &RuntimeProfileRechargeOrderRecord,
) -> Result<(), ExpirationCompensationError> {
if order.status != RuntimeProfileRechargeOrderStatus::Expired {
return Ok(());
}
if order.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL {
mark_profile_recharge_expiration_checked(
state,
&order.order_id,
Some(current_unix_micros()),
Some("VIRTUAL_UNQUERYABLE".to_string()),
Some("wechat_mp_virtual has no v3 transaction query".to_string()),
)
.await?;
state.publish_profile_recharge_order_update(order.order_id.clone());
return Ok(());
}
let wechat_order = match state
.wechat_pay_client()
.query_order_by_out_trade_no(&order.order_id)
.await
{
Ok(order) => order,
Err(WechatPayError::OrderNotExist(message)) => {
mark_profile_recharge_expiration_checked(
state,
&order.order_id,
Some(current_unix_micros()),
Some("ORDER_NOT_EXIST".to_string()),
Some(message),
)
.await?;
state.publish_profile_recharge_order_update(order.order_id.clone());
return Ok(());
}
Err(error) => return Err(error.into()),
};
if wechat_order.out_trade_no != order.order_id {
return Err(ExpirationCompensationError::Runtime(format!(
"wechat query returned mismatched out_trade_no: {}",
wechat_order.out_trade_no
)));
}
match wechat_order.trade_state.as_str() {
"SUCCESS" => mark_expired_profile_recharge_order_paid(state, order, wechat_order).await,
"NOTPAY" => {
state
.wechat_pay_client()
.close_order_by_out_trade_no(&order.order_id)
.await?;
mark_profile_recharge_expiration_checked(
state,
&order.order_id,
Some(current_unix_micros()),
Some("NOTPAY".to_string()),
None,
)
.await?;
state.publish_profile_recharge_order_update(order.order_id.clone());
Ok(())
}
"CLOSED" | "REVOKED" | "PAYERROR" => {
mark_profile_recharge_expiration_checked(
state,
&order.order_id,
Some(current_unix_micros()),
Some(wechat_order.trade_state),
None,
)
.await?;
state.publish_profile_recharge_order_update(order.order_id.clone());
Ok(())
}
trade_state => Err(ExpirationCompensationError::Runtime(format!(
"wechat trade_state is not final for expiration compensation: {trade_state}"
))),
}
}
async fn mark_expired_profile_recharge_order_paid(
state: &AppState,
order: &RuntimeProfileRechargeOrderRecord,
wechat_order: WechatPayNotifyOrder,
) -> Result<(), ExpirationCompensationError> {
let paid_at_micros = wechat_order
.success_time
.as_deref()
.and_then(|value| parse_rfc3339(value).ok())
.map(offset_datetime_to_unix_micros)
.unwrap_or_else(current_unix_micros);
state
.spacetime_client()
.mark_profile_recharge_order_paid(
order.order_id.clone(),
paid_at_micros,
wechat_order.transaction_id,
)
.await?;
state.publish_profile_recharge_order_update(order.order_id.clone());
info!(
order_id = order.order_id.as_str(),
"expired profile recharge order compensated as paid"
);
Ok(())
}
async fn mark_profile_recharge_expiration_checked(
state: &AppState,
order_id: &str,
checked_at_micros: Option<i64>,
provider_state: Option<String>,
last_error: Option<String>,
) -> Result<(), ExpirationCompensationError> {
state
.spacetime_client()
.mark_profile_recharge_order_expiration_checked(
order_id.to_string(),
checked_at_micros,
provider_state,
last_error,
)
.await?;
Ok(())
}
async fn record_profile_recharge_expiration_error(
state: &AppState,
order_id: &str,
error: String,
) {
if let Err(mark_error) = mark_profile_recharge_expiration_checked(
state,
order_id,
None,
None,
Some(error),
)
.await
{
warn!(
order_id,
error = %mark_error,
"failed to record profile recharge expiration compensation error"
);
}
}
#[derive(Debug)]
enum ExpirationCompensationError {
WechatPay(WechatPayError),
Spacetime(spacetime_client::SpacetimeClientError),
Runtime(String),
}
impl From<WechatPayError> for ExpirationCompensationError {
fn from(error: WechatPayError) -> Self {
Self::WechatPay(error)
}
}
impl From<spacetime_client::SpacetimeClientError> for ExpirationCompensationError {
fn from(error: spacetime_client::SpacetimeClientError) -> Self {
Self::Spacetime(error)
}
}
impl std::fmt::Display for ExpirationCompensationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WechatPay(error) => write!(formatter, "wechat pay error: {error}"),
Self::Spacetime(error) => write!(formatter, "spacetime error: {error}"),
Self::Runtime(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for ExpirationCompensationError {}
@@ -1,207 +0,0 @@
use std::{process, time::Duration};
use module_runtime::RuntimeProfileRechargeOrderExpirationScheduleSnapshot;
use platform_wechat::pay::WechatPayError;
use shared_kernel::offset_datetime_to_unix_micros;
use time::OffsetDateTime;
use tokio::time::MissedTickBehavior;
use tracing::{debug, info, warn};
use crate::state::AppState;
const PROFILE_RECHARGE_EXPIRATION_WORKER_INTERVAL: Duration = Duration::from_secs(15);
const PROFILE_RECHARGE_EXPIRATION_WORKER_LEASE: Duration = Duration::from_secs(90);
const PROFILE_RECHARGE_EXPIRATION_WORKER_CLAIM_LIMIT: u32 = 20;
pub fn spawn_profile_recharge_expiration_worker(state: AppState) {
tokio::spawn(async move {
run_profile_recharge_expiration_worker(state).await;
});
}
async fn run_profile_recharge_expiration_worker(state: AppState) {
let worker_id = format!("api:{}:profile-recharge-expiration", process::id());
let mut interval = tokio::time::interval(PROFILE_RECHARGE_EXPIRATION_WORKER_INTERVAL);
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
interval.tick().await;
let now_micros = current_unix_micros();
let lease_expires_at_micros = now_micros
+ i64::try_from(PROFILE_RECHARGE_EXPIRATION_WORKER_LEASE.as_micros())
.unwrap_or(90_000_000);
let schedules = match state
.spacetime_client()
.claim_profile_recharge_order_expiration_schedules(
worker_id.clone(),
now_micros,
lease_expires_at_micros,
PROFILE_RECHARGE_EXPIRATION_WORKER_CLAIM_LIMIT,
)
.await
{
Ok(schedules) => schedules,
Err(error) => {
warn!("充值订单过期检查 claim 失败:{error}");
continue;
}
};
if schedules.is_empty() {
debug!("充值订单过期检查暂无到期任务");
continue;
}
for schedule in schedules {
process_profile_recharge_expiration_schedule(&state, schedule).await;
}
}
}
async fn process_profile_recharge_expiration_schedule(
state: &AppState,
schedule: RuntimeProfileRechargeOrderExpirationScheduleSnapshot,
) {
let order_id = schedule.order_id.clone();
let wechat_order = match state
.wechat_pay_client()
.query_order_by_out_trade_no(&order_id)
.await
{
Ok(order) => order,
Err(WechatPayError::OrderNotExist(message)) => {
warn!(
order_id = order_id.as_str(),
error = message.as_str(),
"过期前微信查单确认订单不存在,关闭本地 pending 订单"
);
close_order_not_found_profile_recharge_order(state, &order_id).await;
return;
}
Err(error) => {
warn!(
order_id = order_id.as_str(),
"过期前微信查单失败,将等待租约过期后重试:{error}"
);
return;
}
};
if wechat_order.out_trade_no != order_id {
warn!(
order_id = order_id.as_str(),
provider_order_id = wechat_order.out_trade_no.as_str(),
"过期前微信查单返回的商户订单号不匹配,将等待租约过期后重试"
);
return;
}
match wechat_order.trade_state.as_str() {
"SUCCESS" => {
let paid_at_micros = wechat_order
.success_time
.as_deref()
.and_then(|value| shared_kernel::parse_rfc3339(value).ok())
.map(offset_datetime_to_unix_micros)
.unwrap_or_else(current_unix_micros);
match state
.spacetime_client()
.mark_profile_recharge_order_paid(
order_id.clone(),
paid_at_micros,
wechat_order.transaction_id,
)
.await
{
Ok(_) => {
complete_profile_recharge_expiration_schedule(state, &order_id).await;
state.publish_profile_recharge_order_update(order_id.clone());
info!(
order_id = order_id.as_str(),
"过期检查发现微信已支付,已补确认入账"
);
}
Err(error) => {
warn!(
order_id = order_id.as_str(),
"过期检查确认已支付订单失败:{error}"
);
}
}
}
"NOTPAY" | "CLOSED" | "REVOKED" | "PAYERROR" => {
match state
.spacetime_client()
.close_profile_recharge_order(order_id.clone(), current_unix_micros())
.await
{
Ok(_) => {
complete_profile_recharge_expiration_schedule(state, &order_id).await;
state.publish_profile_recharge_order_update(order_id.clone());
info!(
order_id = order_id.as_str(),
trade_state = wechat_order.trade_state.as_str(),
"充值订单到期且微信确认未支付,已关闭本地订单"
);
}
Err(error) => {
warn!(
order_id = order_id.as_str(),
"关闭过期充值订单失败:{error}"
);
}
}
}
"USERPAYING" => {
info!(
order_id = order_id.as_str(),
"微信订单仍在支付中,将等待租约过期后重试"
);
}
trade_state => {
warn!(
order_id = order_id.as_str(),
trade_state, "微信订单状态暂不适合本地关闭,将等待租约过期后重试"
);
}
}
}
async fn close_order_not_found_profile_recharge_order(state: &AppState, order_id: &str) {
match state
.spacetime_client()
.close_profile_recharge_order(order_id.to_string(), current_unix_micros())
.await
{
Ok(_) => {
complete_profile_recharge_expiration_schedule(state, order_id).await;
state.publish_profile_recharge_order_update(order_id.to_string());
info!(
order_id,
reason = "ORDER_NOT_EXIST",
"过期检查发现微信订单不存在,已关闭本地充值订单"
);
}
Err(error) => {
warn!(
order_id,
reason = "ORDER_NOT_EXIST",
"关闭微信不存在的过期充值订单失败:{error}"
);
}
}
}
async fn complete_profile_recharge_expiration_schedule(state: &AppState, order_id: &str) {
if let Err(error) = state
.spacetime_client()
.complete_profile_recharge_order_expiration_schedule(order_id.to_string())
.await
{
warn!(order_id, "删除充值订单过期检查任务失败:{error}");
}
}
fn current_unix_micros() -> i64 {
offset_datetime_to_unix_micros(OffsetDateTime::now_utc())
}
@@ -26,7 +26,7 @@ use module_runtime::{
RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType,
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
};
use platform_wechat::pay::WechatPayNotifyOrder;
use platform_wechat::pay::{WechatPayError, WechatPayNotifyOrder};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::Sha256;
@@ -368,7 +368,10 @@ pub async fn confirm_wechat_profile_recharge_order(
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
if order.status != RuntimeProfileRechargeOrderStatus::Pending {
if !matches!(
order.status,
RuntimeProfileRechargeOrderStatus::Pending | RuntimeProfileRechargeOrderStatus::Expired
) {
return Ok(json_success_body(
Some(&request_context),
build_wechat_profile_recharge_order_confirmation(center, order),
@@ -381,13 +384,26 @@ pub async fn confirm_wechat_profile_recharge_order(
));
}
let wechat_order = state
let order_was_expired = order.status == RuntimeProfileRechargeOrderStatus::Expired;
let wechat_order = match state
.wechat_pay_client()
.query_order_by_out_trade_no(&order.order_id)
.await
.map_err(|error| {
runtime_profile_error_response(&request_context, map_wechat_pay_error(error))
})?;
{
Ok(wechat_order) => wechat_order,
Err(WechatPayError::OrderNotExist(_)) if order_was_expired => {
return Ok(json_success_body(
Some(&request_context),
build_wechat_profile_recharge_order_confirmation(center, order),
));
}
Err(error) => {
return Err(runtime_profile_error_response(
&request_context,
map_wechat_pay_error(error),
));
}
};
if wechat_order.out_trade_no != order.order_id {
return Err(runtime_profile_error_response(
&request_context,
@@ -1252,6 +1268,7 @@ fn build_profile_recharge_order_status(status: RuntimeProfileRechargeOrderStatus
RuntimeProfileRechargeOrderStatus::Failed => "failed",
RuntimeProfileRechargeOrderStatus::Closed => "closed",
RuntimeProfileRechargeOrderStatus::Refunded => "refunded",
RuntimeProfileRechargeOrderStatus::Expired => "expired",
}
.to_string()
}
@@ -1691,6 +1708,10 @@ fn build_profile_recharge_order_response(
created_at: record.created_at,
points_delta: record.points_delta,
membership_expires_at: record.membership_expires_at,
expired_at: record.expired_at,
expiration_checked_at: record.expiration_checked_at,
expiration_provider_state: record.expiration_provider_state,
expiration_last_error: record.expiration_last_error,
}
}
@@ -2895,6 +2916,12 @@ mod tests {
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
expired_at: None,
expired_at_micros: None,
expiration_checked_at: None,
expiration_checked_at_micros: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = RuntimeProfileRechargeCenterRecord {
@@ -3004,6 +3031,12 @@ mod tests {
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
expired_at: None,
expired_at_micros: None,
expiration_checked_at: None,
expiration_checked_at_micros: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = RuntimeProfileRechargeCenterRecord {
@@ -3111,6 +3144,12 @@ mod tests {
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
expired_at: None,
expired_at_micros: None,
expiration_checked_at: None,
expiration_checked_at_micros: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = RuntimeProfileRechargeCenterRecord {
@@ -3231,6 +3270,12 @@ mod tests {
points_delta: 0,
membership_expires_at: None,
membership_expires_at_micros: None,
expired_at: None,
expired_at_micros: None,
expiration_checked_at: None,
expiration_checked_at_micros: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = RuntimeProfileRechargeCenterRecord {
@@ -1159,6 +1159,12 @@ pub fn build_runtime_profile_recharge_order_record(
points_delta: snapshot.points_delta,
membership_expires_at: snapshot.membership_expires_at_micros.map(format_utc_micros),
membership_expires_at_micros: snapshot.membership_expires_at_micros,
expired_at: snapshot.expired_at_micros.map(format_utc_micros),
expired_at_micros: snapshot.expired_at_micros,
expiration_checked_at: snapshot.expiration_checked_at_micros.map(format_utc_micros),
expiration_checked_at_micros: snapshot.expiration_checked_at_micros,
expiration_provider_state: snapshot.expiration_provider_state,
expiration_last_error: snapshot.expiration_last_error,
}
}
@@ -449,6 +449,28 @@ pub fn build_runtime_profile_recharge_order_expiration_complete_input(
Ok(RuntimeProfileRechargeOrderExpirationCompleteInput { order_id })
}
pub fn build_runtime_profile_recharge_order_expiration_check_list_input(
limit: u32,
) -> Result<RuntimeProfileRechargeOrderExpirationCheckListInput, RuntimeProfileFieldError> {
Ok(RuntimeProfileRechargeOrderExpirationCheckListInput { limit })
}
pub fn build_runtime_profile_recharge_order_expiration_check_input(
order_id: String,
checked_at_micros: Option<i64>,
provider_state: Option<String>,
last_error: Option<String>,
) -> Result<RuntimeProfileRechargeOrderExpirationCheckInput, RuntimeProfileFieldError> {
let order_id =
normalize_required_string(order_id).ok_or(RuntimeProfileFieldError::MissingOrderId)?;
Ok(RuntimeProfileRechargeOrderExpirationCheckInput {
order_id,
checked_at_micros,
provider_state: provider_state.and_then(normalize_required_string),
last_error: last_error.and_then(normalize_required_string),
})
}
pub fn build_runtime_profile_feedback_submission_input(
user_id: String,
description: String,
@@ -1184,6 +1184,7 @@ pub enum RuntimeProfileRechargeOrderStatus {
Failed,
Closed,
Refunded,
Expired,
}
impl RuntimeProfileRechargeOrderStatus {
@@ -1194,6 +1195,7 @@ impl RuntimeProfileRechargeOrderStatus {
Self::Failed => "failed",
Self::Closed => "closed",
Self::Refunded => "refunded",
Self::Expired => "expired",
}
}
}
@@ -1288,6 +1290,10 @@ pub struct RuntimeProfileRechargeOrderSnapshot {
pub created_at_micros: i64,
pub points_delta: i64,
pub membership_expires_at_micros: Option<i64>,
pub expired_at_micros: Option<i64>,
pub expiration_checked_at_micros: Option<i64>,
pub expiration_provider_state: Option<String>,
pub expiration_last_error: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -1415,6 +1421,21 @@ pub struct RuntimeProfileRechargeOrderExpirationCompleteInput {
pub order_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderExpirationCheckListInput {
pub limit: u32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderExpirationCheckInput {
pub order_id: String,
pub checked_at_micros: Option<i64>,
pub provider_state: Option<String>,
pub last_error: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderExpirationScheduleSnapshot {
@@ -1442,6 +1463,22 @@ pub struct RuntimeProfileRechargeOrderExpirationCompleteProcedureResult {
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderExpirationCheckListProcedureResult {
pub ok: bool,
pub entries: Vec<RuntimeProfileRechargeOrderSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderExpirationCheckProcedureResult {
pub ok: bool,
pub record: Option<RuntimeProfileRechargeOrderSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileWalletLedgerEntrySnapshot {
@@ -1931,6 +1968,12 @@ pub struct RuntimeProfileRechargeOrderRecord {
pub points_delta: i64,
pub membership_expires_at: Option<String>,
pub membership_expires_at_micros: Option<i64>,
pub expired_at: Option<String>,
pub expired_at_micros: Option<i64>,
pub expiration_checked_at: Option<String>,
pub expiration_checked_at_micros: Option<i64>,
pub expiration_provider_state: Option<String>,
pub expiration_last_error: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
+58 -6
View File
@@ -47,7 +47,7 @@ const WECHAT_PAY_CLIENT_IP_MAX_CHARS: usize = 45;
const WECHAT_PAY_JSAPI_PATH: &str = "/v3/pay/transactions/jsapi";
const WECHAT_PAY_H5_PATH: &str = "/v3/pay/transactions/h5";
const WECHAT_PAY_NATIVE_PATH: &str = "/v3/pay/transactions/native";
const WECHAT_NATIVE_PAY_EXPIRE_SECONDS: i64 = 5 * 60;
const WECHAT_PAY_ORDER_EXPIRE_SECONDS: i64 = 5 * 60;
const WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY_BYTES: usize = 43;
const WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BYTES: usize = 32;
const WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES: usize = 16;
@@ -149,6 +149,7 @@ struct WechatJsapiOrderRequest<'a> {
mchid: &'a str,
description: &'a str,
out_trade_no: &'a str,
time_expire: &'a str,
notify_url: &'a str,
amount: WechatJsapiAmount,
payer: WechatJsapiPayer<'a>,
@@ -171,6 +172,7 @@ struct WechatH5OrderRequest<'a> {
mchid: &'a str,
description: &'a str,
out_trade_no: &'a str,
time_expire: &'a str,
notify_url: &'a str,
amount: WechatJsapiAmount,
scene_info: WechatH5SceneInfo<'a>,
@@ -205,6 +207,11 @@ struct WechatNativeSceneInfo<'a> {
payer_client_ip: &'a str,
}
#[derive(Serialize)]
struct WechatCloseOrderRequest<'a> {
mchid: &'a str,
}
#[derive(Deserialize)]
struct WechatJsapiOrderResponse {
prepay_id: Option<String>,
@@ -452,6 +459,17 @@ impl WechatPayClient {
Self::Real(client) => client.query_order_by_out_trade_no(order_id).await,
}
}
pub async fn close_order_by_out_trade_no(&self, order_id: &str) -> Result<(), WechatPayError> {
match self {
Self::Disabled => Err(WechatPayError::Disabled),
Self::Mock => {
normalize_out_trade_no(order_id)?;
Ok(())
}
Self::Real(client) => client.close_order_by_out_trade_no(order_id).await,
}
}
}
impl RealWechatPayClient {
@@ -462,11 +480,13 @@ impl RealWechatPayClient {
validate_jsapi_order_request(self, &request)?;
let amount_total = i64::try_from(request.amount_cents)
.map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?;
let (_, expires_at_text) = build_wechat_pay_expire_time("wechat pay JSAPI time_expire")?;
let body = serde_json::to_string(&WechatJsapiOrderRequest {
appid: &self.app_id,
mchid: &self.mch_id,
description: &request.description,
out_trade_no: &request.order_id,
time_expire: &expires_at_text,
notify_url: &self.notify_url,
amount: WechatJsapiAmount {
total: amount_total,
@@ -527,11 +547,13 @@ impl RealWechatPayClient {
validate_web_order_request(self, &request)?;
let amount_total = i64::try_from(request.amount_cents)
.map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?;
let (_, expires_at_text) = build_wechat_pay_expire_time("wechat pay H5 time_expire")?;
let body = serde_json::to_string(&WechatH5OrderRequest {
appid: &self.app_id,
mchid: &self.mch_id,
description: &request.description,
out_trade_no: &request.order_id,
time_expire: &expires_at_text,
notify_url: &self.notify_url,
amount: WechatJsapiAmount {
total: amount_total,
@@ -580,10 +602,7 @@ impl RealWechatPayClient {
validate_web_order_request(self, &request)?;
let amount_total = i64::try_from(request.amount_cents)
.map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?;
let expires_at =
OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_NATIVE_PAY_EXPIRE_SECONDS);
let expires_at_text =
format_wechat_pay_rfc3339_seconds(expires_at, "微信支付 Native 过期时间")?;
let (_, expires_at_text) = build_wechat_pay_expire_time("wechat pay Native time_expire")?;
let body = serde_json::to_string(&WechatNativeOrderRequest {
appid: &self.app_id,
mchid: &self.mch_id,
@@ -805,6 +824,26 @@ impl RealWechatPayClient {
})
}
async fn close_order_by_out_trade_no(&self, order_id: &str) -> Result<(), WechatPayError> {
let order_id = normalize_out_trade_no(order_id)?;
let encoded_order_id = urlencoding::encode(&order_id);
let path = format!("/v3/pay/transactions/out-trade-no/{encoded_order_id}/close");
let request_url = format!(
"{}/{encoded_order_id}/close",
self.query_order_endpoint_base.trim_end_matches('/')
);
let body = serde_json::to_string(&WechatCloseOrderRequest {
mchid: &self.mch_id,
})
.map_err(|error| {
WechatPayError::Deserialize(format!("wechat pay close request serialize failed: {error}"))
})?;
self.post_wechat_json(&request_url, &path, body, "wechat pay close request failed")
.await
.map(|_| ())
}
fn verify_notify_signature(
&self,
headers: &HeaderMap,
@@ -905,6 +944,15 @@ fn build_mock_h5_payment(order_id: &str) -> WechatH5PaymentResponse {
}
}
fn build_wechat_pay_expire_time(
context: &str,
) -> Result<(OffsetDateTime, String), WechatPayError> {
let expires_at =
OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_PAY_ORDER_EXPIRE_SECONDS);
let expires_at_text = format_wechat_pay_rfc3339_seconds(expires_at, context)?;
Ok((expires_at, expires_at_text))
}
fn format_wechat_pay_rfc3339_seconds(
value: OffsetDateTime,
context: &str,
@@ -919,7 +967,7 @@ fn format_wechat_pay_rfc3339_seconds(
fn build_mock_native_payment(order_id: &str) -> WechatNativePaymentResponse {
let expires_at =
OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_NATIVE_PAY_EXPIRE_SECONDS);
OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_PAY_ORDER_EXPIRE_SECONDS);
WechatNativePaymentResponse {
code_url: format!(
"weixin://pay.weixin.qq.com/bizpayurl/up?pr=mock-{}",
@@ -1635,6 +1683,7 @@ mod tests {
mchid: "1900000001",
description: "陶泥儿 - 60泥点",
out_trade_no: "rcgtest001",
time_expire: "2026-05-15T10:05:00Z",
notify_url: "https://api.example.com/api/profile/recharge/wechat/notify",
amount: WechatJsapiAmount {
total: 600,
@@ -1647,6 +1696,7 @@ mod tests {
.expect("JSAPI order request should serialize");
assert_eq!(body["out_trade_no"], "rcgtest001");
assert_eq!(body["time_expire"], "2026-05-15T10:05:00Z");
assert_eq!(
body["notify_url"],
"https://api.example.com/api/profile/recharge/wechat/notify"
@@ -1662,6 +1712,7 @@ mod tests {
mchid: "1900000001",
description: "陶泥儿 - 60泥点",
out_trade_no: "rcgtest001",
time_expire: "2026-05-15T10:05:00Z",
notify_url: "https://api.example.com/api/profile/recharge/wechat/notify",
amount: WechatJsapiAmount {
total: 600,
@@ -1676,6 +1727,7 @@ mod tests {
assert_eq!(body["scene_info"]["payer_client_ip"], "203.0.113.10");
assert_eq!(body["scene_info"]["h5_info"]["type"], "Wap");
assert_eq!(body["time_expire"], "2026-05-15T10:05:00Z");
assert_eq!(body["amount"]["currency"], "CNY");
assert!(body.get("sceneInfo").is_none());
assert!(body["scene_info"].get("payerClientIp").is_none());
@@ -254,6 +254,10 @@ pub struct ProfileRechargeOrderResponse {
pub created_at: String,
pub points_delta: i64,
pub membership_expires_at: Option<String>,
pub expired_at: Option<String>,
pub expiration_checked_at: Option<String>,
pub expiration_provider_state: Option<String>,
pub expiration_last_error: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -1517,6 +1521,10 @@ mod tests {
created_at: "2026-05-15T10:00:00Z".to_string(),
points_delta: 0,
membership_expires_at: None,
expired_at: None,
expiration_checked_at: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = ProfileRechargeCenterResponse {
wallet_balance: 0,
@@ -1581,6 +1589,10 @@ mod tests {
created_at: "2026-05-15T10:00:00Z".to_string(),
points_delta: 0,
membership_expires_at: Some("2026-06-15T10:00:00Z".to_string()),
expired_at: None,
expiration_checked_at: None,
expiration_provider_state: None,
expiration_last_error: None,
};
let center = ProfileRechargeCenterResponse {
wallet_balance: 0,
@@ -144,6 +144,8 @@ pub mod editor_project;
pub mod external_api_key;
pub mod external_generation;
pub use external_generation::ExternalGenerationQueueWakeSubscription;
pub mod profile_recharge_expiration;
pub use profile_recharge_expiration::ProfileRechargeExpirationSubscription;
pub mod inventory;
pub mod jump_hop;
@@ -291,9 +291,12 @@ pub(crate) use self::runtime_profile::{
map_runtime_profile_invite_code_admin_procedure_result,
map_runtime_profile_play_stats_procedure_result,
map_runtime_profile_recharge_center_procedure_result,
map_runtime_profile_recharge_order_expiration_check_list_procedure_result,
map_runtime_profile_recharge_order_expiration_check_procedure_result,
map_runtime_profile_recharge_order_expiration_claim_procedure_result,
map_runtime_profile_recharge_order_expiration_complete_procedure_result,
map_runtime_profile_recharge_order_procedure_result,
map_runtime_profile_recharge_order_table_row,
map_runtime_profile_recharge_product_admin_list_procedure_result,
map_runtime_profile_recharge_product_admin_procedure_result,
map_runtime_profile_redeem_code_admin_list_procedure_result,
@@ -244,6 +244,27 @@ impl From<module_runtime::RuntimeProfileRechargeOrderExpirationCompleteInput>
}
}
impl From<module_runtime::RuntimeProfileRechargeOrderExpirationCheckListInput>
for RuntimeProfileRechargeOrderExpirationCheckListInput
{
fn from(input: module_runtime::RuntimeProfileRechargeOrderExpirationCheckListInput) -> Self {
Self { limit: input.limit }
}
}
impl From<module_runtime::RuntimeProfileRechargeOrderExpirationCheckInput>
for RuntimeProfileRechargeOrderExpirationCheckInput
{
fn from(input: module_runtime::RuntimeProfileRechargeOrderExpirationCheckInput) -> Self {
Self {
order_id: input.order_id,
checked_at_micros: input.checked_at_micros,
provider_state: input.provider_state,
last_error: input.last_error,
}
}
}
impl From<module_runtime::RuntimeProfileRechargeProductAdminListInput>
for RuntimeProfileRechargeProductAdminListInput
{
@@ -518,6 +539,36 @@ pub(crate) fn map_runtime_profile_recharge_order_expiration_complete_procedure_r
Ok(())
}
pub(crate) fn map_runtime_profile_recharge_order_expiration_check_list_procedure_result(
result: RuntimeProfileRechargeOrderExpirationCheckListProcedureResult,
) -> Result<Vec<RuntimeProfileRechargeOrderRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(map_runtime_profile_recharge_order_snapshot)
.map(module_runtime::build_runtime_profile_recharge_order_record)
.collect())
}
pub(crate) fn map_runtime_profile_recharge_order_expiration_check_procedure_result(
result: RuntimeProfileRechargeOrderExpirationCheckProcedureResult,
) -> Result<RuntimeProfileRechargeOrderRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let order = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("profile recharge order snapshot"))?;
Ok(module_runtime::build_runtime_profile_recharge_order_record(
map_runtime_profile_recharge_order_snapshot(order),
))
}
pub(crate) fn map_runtime_profile_feedback_submission_procedure_result(
result: RuntimeProfileFeedbackSubmissionProcedureResult,
) -> Result<RuntimeProfileFeedbackSubmissionRecord, SpacetimeClientError> {
@@ -1046,9 +1097,47 @@ pub(crate) fn map_runtime_profile_recharge_order_snapshot(
created_at_micros: snapshot.created_at_micros,
points_delta: snapshot.points_delta,
membership_expires_at_micros: snapshot.membership_expires_at_micros,
expired_at_micros: snapshot.expired_at_micros,
expiration_checked_at_micros: snapshot.expiration_checked_at_micros,
expiration_provider_state: snapshot.expiration_provider_state,
expiration_last_error: snapshot.expiration_last_error,
}
}
pub(crate) fn map_runtime_profile_recharge_order_table_row(
row: ProfileRechargeOrder,
) -> RuntimeProfileRechargeOrderRecord {
module_runtime::build_runtime_profile_recharge_order_record(
module_runtime::RuntimeProfileRechargeOrderSnapshot {
order_id: row.order_id,
user_id: row.user_id,
product_id: row.product_id,
product_title: row.product_title,
kind: map_runtime_profile_recharge_product_kind_back(row.kind),
amount_cents: row.amount_cents,
status: map_runtime_profile_recharge_order_status_back(row.status),
payment_channel: row.payment_channel,
paid_at_micros: row
.paid_at
.map(|value| value.to_micros_since_unix_epoch()),
provider_transaction_id: row.provider_transaction_id,
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
points_delta: row.points_delta,
membership_expires_at_micros: row
.membership_expires_at
.map(|value| value.to_micros_since_unix_epoch()),
expired_at_micros: row
.expired_at
.map(|value| value.to_micros_since_unix_epoch()),
expiration_checked_at_micros: row
.expiration_checked_at
.map(|value| value.to_micros_since_unix_epoch()),
expiration_provider_state: row.expiration_provider_state,
expiration_last_error: row.expiration_last_error,
},
)
}
pub(crate) fn map_runtime_profile_recharge_order_expiration_schedule_snapshot(
snapshot: RuntimeProfileRechargeOrderExpirationScheduleSnapshot,
) -> module_runtime::RuntimeProfileRechargeOrderExpirationScheduleSnapshot {
@@ -1479,6 +1568,9 @@ pub(crate) fn map_runtime_profile_recharge_order_status_back(
crate::module_bindings::RuntimeProfileRechargeOrderStatus::Refunded => {
module_runtime::RuntimeProfileRechargeOrderStatus::Refunded
}
crate::module_bindings::RuntimeProfileRechargeOrderStatus::Expired => {
module_runtime::RuntimeProfileRechargeOrderStatus::Expired
}
}
}
@@ -448,6 +448,7 @@ pub mod enqueue_external_generation_job_and_return_procedure;
pub mod ensure_analytics_date_dimension_for_date_reducer;
pub mod equip_inventory_item_input_type;
pub mod execute_custom_world_agent_action_procedure;
pub mod expire_profile_recharge_order_timer_reducer;
pub mod export_auth_store_projection_from_tables_procedure;
pub mod export_database_migration_to_file_procedure;
pub mod external_api_key_authenticate_input_type;
@@ -636,10 +637,12 @@ pub mod list_puzzle_clear_works_procedure;
pub mod list_puzzle_gallery_procedure;
pub mod list_puzzle_works_procedure;
pub mod list_square_hole_works_procedure;
pub mod list_unchecked_expired_profile_recharge_orders_procedure;
pub mod list_visual_novel_runtime_history_procedure;
pub mod list_visual_novel_works_procedure;
pub mod list_wooden_fish_works_procedure;
pub mod mark_editor_showcase_asset_refunded_and_return_procedure;
pub mod mark_profile_recharge_order_expiration_checked_procedure;
pub mod mark_profile_recharge_order_paid_and_return_procedure;
pub mod mark_puzzle_clear_level_time_up_procedure;
pub mod mark_puzzle_draft_generation_failed_procedure;
@@ -719,6 +722,8 @@ pub mod profile_played_world_table;
pub mod profile_played_world_type;
pub mod profile_recharge_order_expiration_schedule_table;
pub mod profile_recharge_order_expiration_schedule_type;
pub mod profile_recharge_order_expiration_timer_table;
pub mod profile_recharge_order_expiration_timer_type;
pub mod profile_recharge_order_table;
pub mod profile_recharge_order_type;
pub mod profile_recharge_product_config_table;
@@ -1004,6 +1009,10 @@ pub mod runtime_profile_recharge_center_procedure_result_type;
pub mod runtime_profile_recharge_center_snapshot_type;
pub mod runtime_profile_recharge_order_close_input_type;
pub mod runtime_profile_recharge_order_create_input_type;
pub mod runtime_profile_recharge_order_expiration_check_input_type;
pub mod runtime_profile_recharge_order_expiration_check_list_input_type;
pub mod runtime_profile_recharge_order_expiration_check_list_procedure_result_type;
pub mod runtime_profile_recharge_order_expiration_check_procedure_result_type;
pub mod runtime_profile_recharge_order_expiration_claim_input_type;
pub mod runtime_profile_recharge_order_expiration_claim_procedure_result_type;
pub mod runtime_profile_recharge_order_expiration_complete_input_type;
@@ -1745,6 +1754,7 @@ pub use enqueue_external_generation_job_and_return_procedure::enqueue_external_g
pub use ensure_analytics_date_dimension_for_date_reducer::ensure_analytics_date_dimension_for_date;
pub use equip_inventory_item_input_type::EquipInventoryItemInput;
pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action;
pub use expire_profile_recharge_order_timer_reducer::expire_profile_recharge_order_timer;
pub use export_auth_store_projection_from_tables_procedure::export_auth_store_projection_from_tables;
pub use export_database_migration_to_file_procedure::export_database_migration_to_file;
pub use external_api_key_authenticate_input_type::ExternalApiKeyAuthenticateInput;
@@ -1933,10 +1943,12 @@ pub use list_puzzle_clear_works_procedure::list_puzzle_clear_works;
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
pub use list_puzzle_works_procedure::list_puzzle_works;
pub use list_square_hole_works_procedure::list_square_hole_works;
pub use list_unchecked_expired_profile_recharge_orders_procedure::list_unchecked_expired_profile_recharge_orders;
pub use list_visual_novel_runtime_history_procedure::list_visual_novel_runtime_history;
pub use list_visual_novel_works_procedure::list_visual_novel_works;
pub use list_wooden_fish_works_procedure::list_wooden_fish_works;
pub use mark_editor_showcase_asset_refunded_and_return_procedure::mark_editor_showcase_asset_refunded_and_return;
pub use mark_profile_recharge_order_expiration_checked_procedure::mark_profile_recharge_order_expiration_checked;
pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return;
pub use mark_puzzle_clear_level_time_up_procedure::mark_puzzle_clear_level_time_up;
pub use mark_puzzle_draft_generation_failed_procedure::mark_puzzle_draft_generation_failed;
@@ -2016,6 +2028,8 @@ pub use profile_played_world_table::*;
pub use profile_played_world_type::ProfilePlayedWorld;
pub use profile_recharge_order_expiration_schedule_table::*;
pub use profile_recharge_order_expiration_schedule_type::ProfileRechargeOrderExpirationSchedule;
pub use profile_recharge_order_expiration_timer_table::*;
pub use profile_recharge_order_expiration_timer_type::ProfileRechargeOrderExpirationTimer;
pub use profile_recharge_order_table::*;
pub use profile_recharge_order_type::ProfileRechargeOrder;
pub use profile_recharge_product_config_table::*;
@@ -2301,6 +2315,10 @@ pub use runtime_profile_recharge_center_procedure_result_type::RuntimeProfileRec
pub use runtime_profile_recharge_center_snapshot_type::RuntimeProfileRechargeCenterSnapshot;
pub use runtime_profile_recharge_order_close_input_type::RuntimeProfileRechargeOrderCloseInput;
pub use runtime_profile_recharge_order_create_input_type::RuntimeProfileRechargeOrderCreateInput;
pub use runtime_profile_recharge_order_expiration_check_input_type::RuntimeProfileRechargeOrderExpirationCheckInput;
pub use runtime_profile_recharge_order_expiration_check_list_input_type::RuntimeProfileRechargeOrderExpirationCheckListInput;
pub use runtime_profile_recharge_order_expiration_check_list_procedure_result_type::RuntimeProfileRechargeOrderExpirationCheckListProcedureResult;
pub use runtime_profile_recharge_order_expiration_check_procedure_result_type::RuntimeProfileRechargeOrderExpirationCheckProcedureResult;
pub use runtime_profile_recharge_order_expiration_claim_input_type::RuntimeProfileRechargeOrderExpirationClaimInput;
pub use runtime_profile_recharge_order_expiration_claim_procedure_result_type::RuntimeProfileRechargeOrderExpirationClaimProcedureResult;
pub use runtime_profile_recharge_order_expiration_complete_input_type::RuntimeProfileRechargeOrderExpirationCompleteInput;
@@ -2644,6 +2662,9 @@ pub enum Reducer {
EnsureAnalyticsDateDimensionForDate {
input: AnalyticsDateDimensionEnsureInput,
},
ExpireProfileRechargeOrderTimer {
timer: ProfileRechargeOrderExpirationTimer,
},
GrantPlayerProgressionExperience {
input: PlayerProgressionGrantInput,
},
@@ -2711,6 +2732,9 @@ impl __sdk::Reducer for Reducer {
Reducer::EnsureAnalyticsDateDimensionForDate { .. } => {
"ensure_analytics_date_dimension_for_date"
}
Reducer::ExpireProfileRechargeOrderTimer { .. } => {
"expire_profile_recharge_order_timer"
}
Reducer::GrantPlayerProgressionExperience { .. } => {
"grant_player_progression_experience"
}
@@ -2792,6 +2816,11 @@ impl __sdk::Reducer for Reducer {
input,
} => __sats::bsatn::to_vec(&ensure_analytics_date_dimension_for_date_reducer::EnsureAnalyticsDateDimensionForDateArgs {
input: input.clone(),
}),
Reducer::ExpireProfileRechargeOrderTimer{
timer,
} => __sats::bsatn::to_vec(&expire_profile_recharge_order_timer_reducer::ExpireProfileRechargeOrderTimerArgs {
timer: timer.clone(),
}),
Reducer::GrantPlayerProgressionExperience{
input,
@@ -2947,6 +2976,8 @@ pub struct DbUpdate {
profile_recharge_order: __sdk::TableUpdate<ProfileRechargeOrder>,
profile_recharge_order_expiration_schedule:
__sdk::TableUpdate<ProfileRechargeOrderExpirationSchedule>,
profile_recharge_order_expiration_timer:
__sdk::TableUpdate<ProfileRechargeOrderExpirationTimer>,
profile_recharge_product_config: __sdk::TableUpdate<ProfileRechargeProductConfig>,
profile_redeem_code: __sdk::TableUpdate<ProfileRedeemCode>,
profile_redeem_code_usage: __sdk::TableUpdate<ProfileRedeemCodeUsage>,
@@ -3250,6 +3281,13 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
)?,
)
}
"profile_recharge_order_expiration_timer" => {
db_update.profile_recharge_order_expiration_timer.append(
profile_recharge_order_expiration_timer_table::parse_table_update(
table_update,
)?,
)
}
"profile_recharge_product_config" => {
db_update.profile_recharge_product_config.append(
profile_recharge_product_config_table::parse_table_update(table_update)?,
@@ -3821,6 +3859,12 @@ impl __sdk::DbUpdate for DbUpdate {
&self.profile_recharge_order_expiration_schedule,
)
.with_updates_by_pk(|row| &row.order_id);
diff.profile_recharge_order_expiration_timer = cache
.apply_diff_to_table::<ProfileRechargeOrderExpirationTimer>(
"profile_recharge_order_expiration_timer",
&self.profile_recharge_order_expiration_timer,
)
.with_updates_by_pk(|row| &row.scheduled_id);
diff.profile_recharge_product_config = cache
.apply_diff_to_table::<ProfileRechargeProductConfig>(
"profile_recharge_product_config",
@@ -4360,6 +4404,9 @@ impl __sdk::DbUpdate for DbUpdate {
"profile_recharge_order_expiration_schedule" => db_update
.profile_recharge_order_expiration_schedule
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_recharge_order_expiration_timer" => db_update
.profile_recharge_order_expiration_timer
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_recharge_product_config" => db_update
.profile_recharge_product_config
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -4772,6 +4819,9 @@ impl __sdk::DbUpdate for DbUpdate {
"profile_recharge_order_expiration_schedule" => db_update
.profile_recharge_order_expiration_schedule
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_recharge_order_expiration_timer" => db_update
.profile_recharge_order_expiration_timer
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_recharge_product_config" => db_update
.profile_recharge_product_config
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -5041,6 +5091,8 @@ pub struct AppliedDiff<'r> {
profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>,
profile_recharge_order_expiration_schedule:
__sdk::TableAppliedDiff<'r, ProfileRechargeOrderExpirationSchedule>,
profile_recharge_order_expiration_timer:
__sdk::TableAppliedDiff<'r, ProfileRechargeOrderExpirationTimer>,
profile_recharge_product_config: __sdk::TableAppliedDiff<'r, ProfileRechargeProductConfig>,
profile_redeem_code: __sdk::TableAppliedDiff<'r, ProfileRedeemCode>,
profile_redeem_code_usage: __sdk::TableAppliedDiff<'r, ProfileRedeemCodeUsage>,
@@ -5472,6 +5524,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.profile_recharge_order_expiration_schedule,
event,
);
callbacks.invoke_table_row_callbacks::<ProfileRechargeOrderExpirationTimer>(
"profile_recharge_order_expiration_timer",
&self.profile_recharge_order_expiration_timer,
event,
);
callbacks.invoke_table_row_callbacks::<ProfileRechargeProductConfig>(
"profile_recharge_product_config",
&self.profile_recharge_product_config,
@@ -6014,19 +6071,19 @@ impl __sdk::SubscriptionHandle for SubscriptionHandle {
/// either a [`DbConnection`] or an [`EventContext`] and operate on either.
pub trait RemoteDbContext:
__sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>
{
}
impl<
Ctx: __sdk::DbContext<
Ctx: __sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>,
> RemoteDbContext for Ctx
> RemoteDbContext for Ctx
{
}
@@ -6493,6 +6550,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
profile_played_world_table::register_table(client_cache);
profile_recharge_order_table::register_table(client_cache);
profile_recharge_order_expiration_schedule_table::register_table(client_cache);
profile_recharge_order_expiration_timer_table::register_table(client_cache);
profile_recharge_product_config_table::register_table(client_cache);
profile_redeem_code_table::register_table(client_cache);
profile_redeem_code_usage_table::register_table(client_cache);
@@ -6628,6 +6686,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
"profile_played_world",
"profile_recharge_order",
"profile_recharge_order_expiration_schedule",
"profile_recharge_order_expiration_timer",
"profile_recharge_product_config",
"profile_redeem_code",
"profile_redeem_code_usage",
@@ -47,9 +47,11 @@ pub trait accept_quest {
&self,
input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl accept_quest for super::RemoteReducers {
&self,
input: QuestRecordInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcceptQuestArgs { input }, callback)
@@ -34,10 +34,10 @@ pub trait acknowledge_external_generation_jobs_and_return {
input: ExternalGenerationJobAcknowledgeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl acknowledge_external_generation_jobs_and_return for super::RemoteProcedures
input: ExternalGenerationJobAcknowledgeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ExternalGenerationJobProcedureResult>(
@@ -47,9 +47,11 @@ pub trait acknowledge_quest_completion {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()>;
}
@@ -58,9 +60,11 @@ impl acknowledge_quest_completion for super::RemoteReducers {
&self,
input: QuestCompletionAckInput,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
callback: impl FnOnce(
&super::ReducerEventContext,
Result<Result<(), String>, __sdk::InternalError>,
) + Send
+ 'static,
) -> __sdk::Result<()> {
self.imp
.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback)
@@ -31,10 +31,10 @@ pub trait admin_disable_profile_redeem_code {
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures {
input: RuntimeProfileRedeemCodeAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
@@ -31,10 +31,10 @@ pub trait admin_disable_profile_task_config {
input: RuntimeProfileTaskConfigAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_disable_profile_task_config for super::RemoteProcedures {
input: RuntimeProfileTaskConfigAdminDisableInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileTaskConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminProcedureResult>(
@@ -31,10 +31,10 @@ pub trait admin_get_profile_wallet_config {
input: RuntimeProfileWalletConfigAdminGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileWalletConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileWalletConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_get_profile_wallet_config for super::RemoteProcedures {
input: RuntimeProfileWalletConfigAdminGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileWalletConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileWalletConfigAdminProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileWalletConfigAdminProcedureResult>(

Some files were not shown because too many files have changed in this diff Show More