新增游戏买断制泥点付费领域规则、SpacetimeDB schema、钱包来源与 Rust DTO
Project CI / Backend tests (pull_request) Failing after 19s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 4m10s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 4m18s
Project CI / Repository checks (pull_request) Failing after 26s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 5m5s
Project CI / Frontend tests (pull_request) Successful in 3m48s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m4s
Project CI / Native shell tests (pull_request) Failing after 8m31s

- server-rs/crates/module-game-distribution:新增价格校验 normalize_game_price_mud_points(0..=1000000,0 为免费)、购买资格 resolve_game_purchase_decision、GamePurchaseSnapshot 与 game_purchase_id(前缀 gdpurchase_);GameSnapshot/GameVersionSnapshot 增加 price_mud_points,审核通过时随版本价格生效;新增购买服务方法与纯内存测试。
- server-rs/crates/spacetime-module/src/game_distribution.rs:GameDistributionGame 末尾追加 #[default(0u64)] price_mud_points;新增私有表 game_distribution_purchase(purchase_id 主键 + user_id/game_id btree 索引);新增 purchase_game_distribution_game_and_return(同事务校验可见性/价格 CAS/未购买并原子扣费、幂等收据 action=purchase)与只读 get_game_distribution_purchase_and_return;approve 在同一事务内把冻结资料 priceMudPoints 生效到游戏行;公开游戏投影带出 priceMudPoints。
- server-rs/crates/spacetime-module/src/runtime/active/profile.rs:新增 consume_profile_wallet_points_for_game_purchase 复用通用钱包写入路径(来源类型 game_purchase、流水 ID game_purchase:{userId}:{gameId}、metadata_json 记 kind/gameId);profile_wallet_balance 提升为 pub(crate)。
- server-rs/crates/spacetime-module/src/migration.rs:迁移表白名单登记 game_distribution_purchase。
- server-rs/crates/module-runtime/src/domain.rs:RuntimeProfileWalletLedgerSourceType 追加 GamePurchase 变体与 game_purchase 文案,未改 profile_wallet_ledger 表结构。
- server-rs/crates/shared-contracts:新增 PROFILE_WALLET_LEDGER_SOURCE_TYPE_GAME_PURCHASE;公开游戏摘要/详情增加 priceMudPoints 与 purchased;GameDistributionPrivateVersion 与 GameDistributionCreateVersionRequest 增加 priceMudPoints;新增 GameDistributionPurchaseRequest/GameDistributionPurchase/GameDistributionPurchaseResponse/GameDistributionPlaySessionResponse。
- server-rs/crates/spacetime-client:新增购买与回读 typed facade、GameDistributionPurchaseRecord 与 row mapper,GameDistributionGameRecord 带出 price_mud_points,钱包来源 mapper 支持 GamePurchase。
- docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md:登记 price_mud_points、game_distribution_purchase 与 game_purchase 流水来源。
- 重新生成 SpacetimeDB Rust 绑定(购买表/过程与价格字段)。
This commit is contained in:
2026-10-05 17:02:56 +08:00
parent 7b38ef9623
commit 80fd5a9fea
224 changed files with 3191 additions and 1617 deletions
@@ -485,7 +485,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- 公开素材:游戏行末尾追加可空 `cover_object_key` 与 `screenshots_json`(截图 `{assetId, objectKey}` 数组);创建游戏时 `api-server` 就复核封面/截图素材存在且属于当前作者(不存在 400、他人素材 403),创建版本时按同一口径再次复核并派生对象键。 发布写入受灰度配置键 `game-distribution:publish` 约束:**灰度默认关闭**,未配置或 `enabled=false` 时写入口(创建游戏/版本、确认包、送审、审核通过激活)返回 503 `GAME_DISTRIBUTION_PUBLISH_DISABLED`,`enabled=true` 且白名单/比例/标签命中才放行,读取与安全下架保持可用;同一判据在 `GET /api/runtime/frontend-config` 以 `gameDistributionPublishEnabled` 下发给前端入口,匿名恒为 `false`。只有可见性为 `published` 且存在有效 `active_version_id` 的游戏,其封面/截图素材才在 `/api/assets/read-url` 上获得匿名读授权。
- 复用规则:末尾可空列 `local_project_id` 保存发布方本地项目标识(AGC 的 `manifest.projectId`)。同一 `owner_user_id` 再次以相同 `local_project_id` 创建游戏时复用既有 `game_id` 并只新增版本,避免“更新”被实现成新建游戏;该字段只是复用提示,不构成所有权或路径凭证,也不能用于跨账号匹配。已软删除的游戏不参与复用:删除后重新发布同一本地项目应得到新的游戏身份。
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、发行网关素材授权(`game_distribution_asset_has_public_read_grant`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
- 资料编辑:`update_game_distribution_game_metadata_and_return` 覆盖游戏行上的展示字段(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向)并立即生效,要求 `expected_publication_revision` CAS;版本行与冻结资料不变,下一次审核通过仍会用新版本的冻结资料覆盖游戏行。
- 资料编辑:`update_game_distribution_game_metadata_and_return` 覆盖游戏行上的展示字段(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向)并立即生效,要求 `expected_publication_revision` CAS;版本行与冻结资料不变,下一次审核通过仍会用新版本的冻结资料覆盖游戏行。**资料编辑不得直接改公开价格**:调价必须走新版本审核。
- 买断制定价(2026-10-05):游戏行末尾追加 `price_mud_points: u64` 并设置 `#[default(0u64)]`;`0` 表示免费,上限 `1_000_000`(复用 `module-game-distribution::normalize_game_price_mud_points` 校验)。价格是版本冻结资料的一部分:作者在 `GameDistributionCreateVersionRequest.priceMudPoints` 提交,写入版本冻结 `metadata_json.priceMudPoints`,只有 `approve_game_distribution_version_and_return` 通过审核时才随资料整体生效到本行;未通过审核或资料编辑都不会改变当前公开价格。公开投影(`get_public_game_distribution_game_and_return` 等)在游戏快照上带出 `priceMudPoints`。
- 索引:`by_game_distribution_game_owner_user_id` 用于作者私有游戏列表;`game_id` 为主键。公开目录只返回 `visibility = published`、`deleted_at` 为空且存在有效 `active_version_id` 的投影。
- 游玩计数写入:`play_count` 只由批量 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `GameDistributionPlayCountIncrementInput { increments: Vec<{ gameId, delta }> }`)累加。`api-server` 在内存里按 `identity + gameId` 做 30 分钟去重、按 `IP + gameId` 做固定窗口限流后,按 `GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5 秒)批量落库;事务内只对 `published` 且存在有效 `active_version_id` 的游戏 `saturating_add`,非公开静默跳过,且**不更新** `updated_at`。公开 HTTP 入口为 `POST /api/game-distribution/games/{gameId}/plays`,完整行为见玩法链路的「游玩计数(已实现)」。
@@ -521,7 +522,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
- 用途:不可变发行版本与真实包确认事实。创建后冻结 `package_sha256`、字节数、文件数、根入口和版本号;后续只推进上传、校验、审核、公开、撤回状态,并记录私有对象键、文件清单、入口 URL、审核者和阶段时间。
- 索引:`by_game_distribution_version_game_id`、`by_game_distribution_version_owner_user_id`。真实 ZIP 由 `api-server` 校验并写入私有 OSS 后,才通过 facade 确认 `uploaded`;表不保存 ZIP 正文。
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图)并从素材记录派生对象键后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料,旧版本(无快照)保持原值。
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图/买断制价格 `priceMudPoints`)并从素材记录派生对象键后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料与价格,旧版本(无快照)保持原值。<code>parse_game_distribution_frozen_metadata</code> 会用 <code>normalize_game_price_mud_points</code> 校验价格上限,越界快照在审核时失败关闭。
- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 payload 上追加 `frozenMetadata`(冻结快照原样 JSON,历史版本为 `null`)。只有公开投影会剥掉素材 ID,作者与管理员拿到 `coverAssetId` / `screenshots[].assetId`,因此作者续发时可以直接复用同一批封面与截图素材,不需要为了沿用封面重新上传一次;素材 ID 缺失(旧版本)时前端必须要求作者重新选择封面,不能用对象键反推素材身份。
- 撤回与回读:`cancel_game_distribution_version_and_return` 只允许把未参与当前公开投影的版本推进到 `cancelled`,并要求 `expected_publication_revision` 与游戏公开修订号一致;`get_game_distribution_version_and_return` 供管理员按版本 ID 直读。客户端看到的 `recoveryAction` 由 `api-server` 按 `status` 派生,不落表。
@@ -543,6 +544,16 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- 索引:owner、game、version 和 `by_game_distribution_receipt_scope` 组合索引;默认保留窗口由服务端清理策略控制。
- 重放语义:写操作命中既有收据时,procedure 回传 `replayed = true` 并复用收据里的结果 ID,不重复创建游戏、版本或审核结论;同 key 不同请求摘要必须返回冲突。
### `game_distribution_purchase`
- Rust 结构体:`GameDistributionPurchase`
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
- 用途:游戏买断制购买记录。每账号每游戏最多一条:`purchase_id` 主键由 `game_id` 与 `user_id` 按长度前缀无歧义组合(`module-game-distribution::game_purchase_id`,前缀 `gdpurchase_`),并另建 `by_game_distribution_purchase_user_id` / `by_game_distribution_purchase_game_id` 两个 btree 索引供回读;`(user_id, game_id)` 唯一性由购买事务强制。
- 字段:`purchase_id`(主键)、`game_id`、`user_id`、`price_mud_points`(成交价快照,之后调价不影响既有所有权)、`wallet_ledger_id`(对应 `profile_wallet_ledger` 流水)、`created_at`。表随迁移导出/导入。
- 写入:只由 `purchase_game_distribution_game_and_return`(输入 `GameDistributionPurchaseInput { gameId, userId, expectedPriceMudPoints, idempotencyKey, requestDigest, nowMicros }`)在单一事务内写入:先校验游戏已公开且存在有效公开版本、价格大于 0、`expectedPriceMudPoints` 与当前 `price_mud_points` CAS 一致,再走 runtime profile 钱包扣费路径(`RuntimeProfileWalletLedgerSourceType::GamePurchase`,流水 ID `game_purchase:{userId}:{gameId}`,`metadata_json` 记 `{"kind":"game_purchase","gameId":...}`),最后插入购买行。余额不足、账户冻结或存在退款欠款时失败关闭,不写购买行。
- 幂等:复用 `game_distribution_idempotency_receipt`(`action = purchase`,`owner = 购买者`);同 key 同摘要返回既有记录,同 key 不同摘要冲突;换 key 重复购买命中既有 `(user_id, game_id)` 时只补记收据、不再扣费并回传 `replayed = true`。结果类型 `GameDistributionPurchaseResult { ok, purchase, walletBalance, replayed, errorMessage }`。
- 回读:只读 procedure `get_game_distribution_purchase_and_return`(输入 `GameDistributionPurchaseLookupInput { gameId, userId }`)返回购买快照与当前钱包余额,未购买时 `purchase` 为空。完整行为合同见玩法链路的「游戏买断制泥点付费与播放鉴权合同」。
### `llm_router_account`
- 当前 AGC Router 需求由 `llm_router_account` 表单独承载:API Key 核心字段、加密凭据、Router 账号元数据、生命周期与 provisioning 状态均在该表;不依赖 `external_api_key`。api-server 每次上游调用都读取权威 `llm_router_account` active/revoked 状态并即时解密当前密文,不做 TTL 凭据缓存,避免任意实例轮换或撤销后继续使用旧 Key。
@@ -871,6 +882,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`ProfileWalletLedger`
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
- 说明:账号钱包流水表。`created_at` 表示钱包事务实际结算时间,列表先按当前余额反向校验 `balance_after - amount_delta` 的结算链,再以该时间倒序兜底,避免支付回调或退款重放延迟时出现余额顺序倒置;支付平台确认时间继续保存在充值订单 `paid_at`。`metadata_json` 为可选 JSON 对象字符串,旧行缺失时读取层按 `{}` 归一;外部生成扣费 / 退款写入 `externalGenerationJobId`,使退款记录可以追溯到对应 `external_generation_job`。
- 来源类型(2026-10-05):`RuntimeProfileWalletLedgerSourceType` 追加 `GamePurchase`(`game_purchase`),用于游戏买断制购买扣费;写入走通用入口 `apply_profile_wallet_signed_delta`,流水 ID 固定 `game_purchase:{userId}:{gameId}`(幂等重试不二次扣费),`metadata_json` 记 `{"kind":"game_purchase","gameId":...}`。扣费受人工冻结 / 退款欠款 / 可用余额校验约束,余额不足时失败关闭且不写购买行。**只追加枚举变体,未改本表结构。**
- 实际变动为 0 的条目不进账单:写入侧只在余额真的变化时落账,每日免费 / 会员周期换期先按 `previous_balance - expired_points + granted_points`(饱和运算)算出实际变动,变动为 0 时只更新余额状态、不写本表;通用结算入口 `apply_profile_wallet_signed_delta` 在 `amount_delta == 0` 时同样不落账。读取侧 `list_profile_wallet_ledger_entries` 在结算链排序之后、截断 50 条上限之前剔除 `amount_delta == 0` 的存量行,因此历史遗留的 0 变动流水不会出现在 `GET /api/profile/wallet-ledger`,也不占用列表上限。该规则只影响展示与落账,不改变幂等 ledger、余额结算、消费投影和退款语义。
### `profile_wallet_consumption_total`
@@ -4,10 +4,13 @@ use std::{
};
use crate::{
commands::{CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision},
commands::{
CreateGameInput, CreateVersionInput, IdempotencyRequest, PurchaseGameInput, ReviewDecision,
},
domain::{
GameDistributionAction, GameSnapshot, GameVersionSnapshot, GameVersionStatus,
GameVisibility, normalize_id, resolve_version_number,
GameDistributionAction, GamePurchaseDecision, GamePurchaseSnapshot, GameSnapshot,
GameVersionSnapshot, GameVersionStatus, GameVisibility, normalize_game_price_mud_points,
normalize_id, resolve_game_purchase_decision, resolve_version_number,
},
errors::{GameDistributionError, GameDistributionFieldError},
};
@@ -25,6 +28,13 @@ pub struct PublicationOperationResult {
pub replayed: bool,
}
/// 购买结果:购买快照 + 是否命中幂等/既有所有权重放。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PurchaseOperationResult {
pub purchase: GamePurchaseSnapshot,
pub replayed: bool,
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryGameDistributionStore {
inner: Arc<Mutex<GameDistributionState>>,
@@ -34,6 +44,8 @@ pub struct InMemoryGameDistributionStore {
struct GameDistributionState {
games: HashMap<String, GameSnapshot>,
versions: HashMap<String, GameVersionSnapshot>,
/// 以 `(user_id, game_id)` 为键强制“每账号每游戏最多一条”购买记录。
purchases: HashMap<(String, String), GamePurchaseSnapshot>,
idempotency: HashMap<IdempotencyScope, IdempotencyRecord>,
}
@@ -58,6 +70,7 @@ enum StoredOutcome {
game: GameSnapshot,
version: GameVersionSnapshot,
},
Purchase(GamePurchaseSnapshot),
}
#[derive(Clone, Debug)]
@@ -100,6 +113,7 @@ impl GameDistributionService {
visibility: GameVisibility::Unpublished,
created_at_micros: input.created_at_micros,
updated_at_micros: input.created_at_micros,
price_mud_points: 0,
};
state.games.insert(game_id, snapshot.clone());
remember(
@@ -129,6 +143,7 @@ impl GameDistributionService {
)?;
let package_sha256 = normalize_package_digest(&input.package_sha256)?;
validate_package_bytes(input.package_bytes)?;
let price_mud_points = normalize_game_price_mud_points(input.price_mud_points)?;
let idem = normalize_idempotency(input.idempotency)?;
let scope = scope(&owner_user_id, GameDistributionAction::CreateVersion, &idem);
let mut state = lock(&self.store)?;
@@ -166,6 +181,7 @@ impl GameDistributionService {
review_reason: None,
created_at_micros: input.created_at_micros,
updated_at_micros: input.created_at_micros,
price_mud_points,
};
state.versions.insert(version_id, snapshot.clone());
remember(
@@ -479,6 +495,8 @@ impl GameDistributionService {
next_version.review_reason = None;
next_game.visibility = GameVisibility::Published;
next_game.active_version_id = Some(next_version.version_id.clone());
// 价格随版本冻结:审核通过时与资料一起整体生效到游戏行。
next_game.price_mud_points = next_version.price_mud_points;
}
}
state
@@ -570,6 +588,110 @@ impl GameDistributionService {
.ok_or(GameDistributionError::VersionNotFound)
}
/// 买断制购买:校验可见性/价格/未购买,先写幂等收据再落购买记录。
///
/// 钱包扣费由 runtime profile 域负责;本方法只接收已落账的 `wallet_ledger_id`,
/// 保证购买记录与扣费流水共享同一份成交价快照。
pub fn purchase_game(
&self,
input: PurchaseGameInput,
) -> Result<PurchaseOperationResult, GameDistributionError> {
let game_id = required(input.game_id, GameDistributionFieldError::MissingGameId)?;
let user_id = required(input.user_id, GameDistributionFieldError::MissingActorUserId)?;
let wallet_ledger_id = required(
input.wallet_ledger_id,
GameDistributionFieldError::MissingWalletLedgerId,
)?;
let idem = normalize_idempotency(input.idempotency)?;
let scope = scope(&user_id, GameDistributionAction::Purchase, &idem);
let mut state = lock(&self.store)?;
if let Some(outcome) = replay_outcome(&state, &scope, &idem.request_digest)? {
return purchase_result(outcome, true);
}
let (visibility, has_active_version, price_mud_points) = {
let game = state
.games
.get(&game_id)
.ok_or(GameDistributionError::GameNotFound)?;
(
game.visibility,
game.active_version_id.is_some(),
game.price_mud_points,
)
};
let existing = state
.purchases
.get(&(user_id.clone(), game_id.clone()))
.cloned();
let decision = resolve_game_purchase_decision(
visibility,
has_active_version,
price_mud_points,
existing.is_some(),
)?;
match decision {
GamePurchaseDecision::Free => {
return Err(GameDistributionError::FreeGameNotPurchasable);
}
GamePurchaseDecision::AlreadyOwned => {
let purchase = existing.expect("已拥有的判定必然对应既有购买记录");
remember(
&mut state,
scope,
idem.request_digest,
StoredOutcome::Purchase(purchase.clone()),
);
return Ok(PurchaseOperationResult {
purchase,
replayed: true,
});
}
GamePurchaseDecision::Purchasable => {}
}
if input.expected_price_mud_points != price_mud_points {
return Err(GameDistributionError::PriceChanged);
}
let purchase = GamePurchaseSnapshot {
purchase_id: crate::domain::game_purchase_id(&game_id, &user_id),
game_id: game_id.clone(),
user_id: user_id.clone(),
price_mud_points,
wallet_ledger_id,
created_at_micros: input.created_at_micros,
};
state
.purchases
.insert((user_id, game_id), purchase.clone());
remember(
&mut state,
scope,
idem.request_digest,
StoredOutcome::Purchase(purchase.clone()),
);
Ok(PurchaseOperationResult {
purchase,
replayed: false,
})
}
pub fn get_game_purchase(
&self,
game_id: &str,
user_id: &str,
) -> Result<GamePurchaseSnapshot, GameDistributionError> {
let game_id = required(game_id.to_string(), GameDistributionFieldError::MissingGameId)?;
let user_id = required(
user_id.to_string(),
GameDistributionFieldError::MissingActorUserId,
)?;
let state = lock(&self.store)?;
state
.purchases
.get(&(user_id, game_id))
.cloned()
.ok_or(GameDistributionError::GamePurchaseNotFound)
}
fn transition_owned_version(
&self,
owner_user_id: &str,
@@ -832,6 +954,16 @@ fn publication_result(
}
}
fn purchase_result(
outcome: StoredOutcome,
replayed: bool,
) -> Result<PurchaseOperationResult, GameDistributionError> {
match outcome {
StoredOutcome::Purchase(purchase) => Ok(PurchaseOperationResult { purchase, replayed }),
_ => Err(GameDistributionError::IdempotencyOutcomeMismatch),
}
}
fn ensure_owner(game: &GameSnapshot, owner_user_id: &str) -> Result<(), GameDistributionError> {
if game.owner_user_id == owner_user_id {
Ok(())
@@ -848,8 +980,8 @@ fn invalid_transition(from: GameVersionStatus, to: GameVersionStatus) -> GameDis
mod tests {
use super::*;
use crate::{
CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision,
compute_request_digest,
CreateGameInput, CreateVersionInput, IdempotencyRequest, MAX_GAME_PRICE_MUD_POINTS,
PurchaseGameInput, ReviewDecision, compute_request_digest,
};
fn idem(key: &str, body: &str) -> IdempotencyRequest {
@@ -883,6 +1015,7 @@ mod tests {
owner_user_id: "owner-1".to_string(),
version_id: "version-1".to_string(),
version_number: None,
price_mud_points: 0,
package_sha256: "a".repeat(64),
package_bytes: 42,
idempotency: idem("create-version-1", "version-1"),
@@ -1163,6 +1296,7 @@ mod tests {
owner_user_id: "owner-1".to_string(),
version_id: "version-2".to_string(),
version_number: None,
price_mud_points: 0,
package_sha256: "b".repeat(64),
package_bytes: 7,
idempotency: idem("create-version-2", "version-2"),
@@ -1317,6 +1451,7 @@ mod tests {
owner_user_id: "owner-1".to_string(),
version_id: "version-1-again".to_string(),
version_number: Some(first.version_number),
price_mud_points: 0,
package_sha256: "b".repeat(64),
package_bytes: 9,
idempotency: idem("create-version-again", "version-again"),
@@ -1331,4 +1466,201 @@ mod tests {
first
);
}
/// 创建并发布一个指定价格的版本,返回审核通过后的游戏快照。
fn publish_version_with_price(
service: &GameDistributionService,
price_mud_points: u64,
) -> GameSnapshot {
create_game(service);
service
.create_version(CreateVersionInput {
game_id: "game-1".to_string(),
owner_user_id: "owner-1".to_string(),
version_id: "version-1".to_string(),
version_number: None,
price_mud_points,
package_sha256: "a".repeat(64),
package_bytes: 42,
idempotency: idem("create-version-1", "version-1"),
created_at_micros: 2,
})
.expect("version should be created");
service
.confirm_package(
"owner-1",
"version-1",
&"a".repeat(64),
42,
idem("upload-1", "upload"),
3,
)
.expect("upload should succeed");
service
.submit_version("owner-1", "version-1", idem("submit-1", "submit"), 4)
.expect("submit should start validation");
service
.pass_validation("owner-1", "version-1", idem("pass-1", "pass"), 5)
.expect("validation should pass");
service
.review_version(
"reviewer-1",
"version-1",
ReviewDecision::Approve {
expected_publication_revision: 0,
},
idem("review-1", "approve"),
6,
)
.expect("review should publish")
.game
}
fn purchase_input(
purchase_id_for_ledger: &str,
expected_price_mud_points: u64,
key: &str,
) -> PurchaseGameInput {
PurchaseGameInput {
game_id: "game-1".to_string(),
user_id: "player-1".to_string(),
wallet_ledger_id: format!("game_purchase:{purchase_id_for_ledger}"),
expected_price_mud_points,
idempotency: idem(key, purchase_id_for_ledger),
created_at_micros: 10,
}
}
#[test]
fn price_must_be_within_bounds_and_zero_stays_free() {
assert_eq!(normalize_game_price_mud_points(0).expect("0 是免费"), 0);
assert_eq!(
normalize_game_price_mud_points(MAX_GAME_PRICE_MUD_POINTS).expect("上限合法"),
MAX_GAME_PRICE_MUD_POINTS
);
assert_eq!(
normalize_game_price_mud_points(MAX_GAME_PRICE_MUD_POINTS + 1)
.expect_err("超过上限必须失败"),
GameDistributionFieldError::InvalidGamePrice
);
let err = service()
.create_version(CreateVersionInput {
game_id: "game-1".to_string(),
owner_user_id: "owner-1".to_string(),
version_id: "version-1".to_string(),
version_number: None,
price_mud_points: MAX_GAME_PRICE_MUD_POINTS + 1,
package_sha256: "a".repeat(64),
package_bytes: 42,
idempotency: idem("create-version-1", "version-1"),
created_at_micros: 2,
})
.expect_err("越界价格不能冻结到版本");
assert_eq!(
err,
GameDistributionError::Field(GameDistributionFieldError::InvalidGamePrice)
);
}
#[test]
fn purchase_requires_published_game_with_active_version() {
assert_eq!(
resolve_game_purchase_decision(GameVisibility::Unpublished, true, 100, false)
.expect_err("未公开不能购买"),
GameDistributionError::GameNotPublished
);
assert_eq!(
resolve_game_purchase_decision(GameVisibility::Published, false, 100, false)
.expect_err("没有公开版本不能购买"),
GameDistributionError::VersionNotPublished
);
assert_eq!(
resolve_game_purchase_decision(GameVisibility::Published, true, 0, false)
.expect("免费游戏"),
GamePurchaseDecision::Free
);
assert_eq!(
resolve_game_purchase_decision(GameVisibility::Published, true, 100, true)
.expect("已拥有"),
GamePurchaseDecision::AlreadyOwned
);
assert_eq!(
resolve_game_purchase_decision(GameVisibility::Published, true, 100, false)
.expect("可购买"),
GamePurchaseDecision::Purchasable
);
let service = service();
create_game(&service);
let unpublished = service
.purchase_game(purchase_input("gdpurchase_1", 100, "buy-1"))
.expect_err("未公开游戏不能购买");
assert_eq!(unpublished, GameDistributionError::GameNotPublished);
}
#[test]
fn free_game_rejects_purchase_and_price_change_is_conflict() {
let free_service = service();
publish_version_with_price(&free_service, 0);
let free = free_service
.purchase_game(purchase_input("gdpurchase_1", 0, "buy-1"))
.expect_err("免费游戏不需要购买");
assert_eq!(free, GameDistributionError::FreeGameNotPurchasable);
let paid_service = service();
publish_version_with_price(&paid_service, 100);
let stale = paid_service
.purchase_game(purchase_input("gdpurchase_1", 99, "buy-1"))
.expect_err("陈旧价格必须冲突");
assert_eq!(stale, GameDistributionError::PriceChanged);
}
#[test]
fn purchase_charges_once_and_keeps_single_ownership() {
let service = service();
let game = publish_version_with_price(&service, 100);
assert_eq!(game.price_mud_points, 100);
let first = service
.purchase_game(purchase_input("gdpurchase_1", 100, "buy-1"))
.expect("首次购买");
assert!(!first.replayed);
assert_eq!(first.purchase.price_mud_points, 100);
assert_eq!(
first.purchase.wallet_ledger_id,
"game_purchase:gdpurchase_1".to_string()
);
// 同 key 同请求重放:返回同一条购买记录。
let replay = service
.purchase_game(purchase_input("gdpurchase_1", 100, "buy-1"))
.expect("同请求重放");
assert!(replay.replayed);
assert_eq!(replay.purchase, first.purchase);
let conflict = service
.purchase_game(purchase_input("gdpurchase_other", 100, "buy-1"))
.expect_err("同 key 不同请求必须冲突");
assert_eq!(conflict, GameDistributionError::IdempotencyConflict);
// 换幂等键再买:命中既有所有权,仍只有一条记录、不再扣费。
let repeated = service
.purchase_game(purchase_input("gdpurchase_2", 100, "buy-2"))
.expect("重复购买幂等返回既有记录");
assert!(repeated.replayed);
assert_eq!(repeated.purchase, first.purchase);
assert_eq!(
service
.get_game_purchase("game-1", "player-1")
.expect("购买记录可回读"),
first.purchase
);
assert_eq!(
service
.get_game_purchase("game-1", "player-2")
.expect_err("未购买账号没有记录"),
GameDistributionError::GamePurchaseNotFound
);
}
}
@@ -38,12 +38,28 @@ pub struct CreateVersionInput {
/// 传入时只要求 `>= 1`:同一 gameId 的同一标签可以反复提交,每次都是一条新版本记录。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_number: Option<u64>,
/// 本版本冻结的买断制价格(泥点);`0` 表示免费,审核通过时整体生效到游戏行。
#[serde(default)]
pub price_mud_points: u64,
pub package_sha256: String,
pub package_bytes: u64,
pub idempotency: IdempotencyRequest,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PurchaseGameInput {
pub game_id: String,
/// 购买者;与 `game_id` 一起构成唯一所有权。
pub user_id: String,
/// runtime profile 钱包扣费生成的流水 ID;领域侧只记录,不重复扣费。
pub wallet_ledger_id: String,
/// 与服务端当前价格 CAS:不一致时失败关闭,绝不按陈旧价格扣费。
pub expected_price_mud_points: u64,
pub idempotency: IdempotencyRequest,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReviewDecision {
Approve {
@@ -1,16 +1,34 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::errors::GameDistributionFieldError;
use crate::errors::{GameDistributionError, GameDistributionFieldError};
use shared_kernel::{build_prefixed_seed_id, normalize_required_string};
pub const GAME_ID_PREFIX: &str = "game_";
pub const GAME_VERSION_ID_PREFIX: &str = "gamever_";
pub const GAME_PURCHASE_ID_PREFIX: &str = "gdpurchase_";
/// 买断制价格上限(泥点);`0` 表示免费。上限与 0 一起构成唯一定价约束。
pub const MAX_GAME_PRICE_MUD_POINTS: u64 = 1_000_000;
pub fn generate_game_id(seed_micros: i64) -> String {
build_prefixed_seed_id(GAME_ID_PREFIX, seed_micros)
}
/// 长度前缀使任意游戏和账号组合都只有一个无歧义的主键;每账号每游戏最多一条购买记录。
pub fn game_purchase_id(game_id: &str, user_id: &str) -> String {
format!("{GAME_PURCHASE_ID_PREFIX}{}:{game_id}{user_id}", game_id.len())
}
/// 校验买断制价格。类型本身已保证是整数,这里只拒绝超出 `0..=MAX_GAME_PRICE_MUD_POINTS`
/// 的取值;`0` 是合法价格,代表免费。
pub fn normalize_game_price_mud_points(value: u64) -> Result<u64, GameDistributionFieldError> {
if value > MAX_GAME_PRICE_MUD_POINTS {
return Err(GameDistributionFieldError::InvalidGamePrice);
}
Ok(value)
}
pub fn generate_game_version_id(seed_micros: i64, version_number: u64) -> String {
format!(
"{}{seed_micros:x}_{version_number:x}",
@@ -99,6 +117,8 @@ pub struct GameSnapshot {
pub visibility: GameVisibility,
pub created_at_micros: i64,
pub updated_at_micros: i64,
/// 审核通过后生效的买断制价格(泥点);`0` 表示免费。
pub price_mud_points: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -112,6 +132,54 @@ pub struct GameVersionSnapshot {
pub review_reason: Option<String>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
/// 随版本冻结的价格;审核通过时整体生效到游戏行。
pub price_mud_points: u64,
}
/// 买断制购买记录快照:每账号每游戏最多一条,保存成交价与钱包流水 ID。
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GamePurchaseSnapshot {
pub purchase_id: String,
pub game_id: String,
pub user_id: String,
pub price_mud_points: u64,
pub wallet_ledger_id: String,
pub created_at_micros: i64,
}
/// 一次购买请求落在哪条路径上。
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GamePurchaseDecision {
/// 免费游戏:任何账号都可直接游玩,不产生购买记录。
Free,
/// 已拥有:重复购买幂等返回既有记录,不再扣费。
AlreadyOwned,
/// 可购买:按当前价格扣费并写入购买记录。
Purchasable,
}
/// 判断购买资格:只有「公开 + 存在有效公开版本」的游戏才产生购买语义。
///
/// 免费与已拥有都不是错误,由调用方按决策回传对应结果;未公开或缺少公开版本时失败关闭。
pub fn resolve_game_purchase_decision(
visibility: GameVisibility,
has_active_version: bool,
price_mud_points: u64,
already_owned: bool,
) -> Result<GamePurchaseDecision, GameDistributionError> {
if visibility != GameVisibility::Published {
return Err(GameDistributionError::GameNotPublished);
}
if !has_active_version {
return Err(GameDistributionError::VersionNotPublished);
}
if price_mud_points == 0 {
return Ok(GamePurchaseDecision::Free);
}
if already_owned {
return Ok(GamePurchaseDecision::AlreadyOwned);
}
Ok(GamePurchaseDecision::Purchasable)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -128,6 +196,7 @@ pub enum GameDistributionAction {
RejectVersion,
UnpublishGame,
SuspendGame,
Purchase,
}
pub(crate) fn normalize_id(value: impl AsRef<str>) -> Option<String> {
@@ -15,6 +15,8 @@ pub enum GameDistributionFieldError {
MissingReviewReason,
InvalidVersionNumber,
VersionNumberExhausted,
InvalidGamePrice,
MissingWalletLedgerId,
}
impl fmt::Display for GameDistributionFieldError {
@@ -31,6 +33,8 @@ impl fmt::Display for GameDistributionFieldError {
Self::MissingReviewReason => "拒绝审核必须提供理由",
Self::InvalidVersionNumber => "版本号必须是大于 0 的整数",
Self::VersionNumberExhausted => "版本号已达上限,无法自动递增",
Self::InvalidGamePrice => "priceMudPoints 必须是 0 到 1000000 之间的整数",
Self::MissingWalletLedgerId => "walletLedgerId 不能为空",
};
formatter.write_str(message)
}
@@ -60,6 +64,11 @@ pub enum GameDistributionError {
VersionNotPublished,
VersionAlreadyPublished,
ReviewActionNotAllowed,
/// 免费游戏不需要购买;客户端应直接进入游玩。
FreeGameNotPurchasable,
/// `expectedPriceMudPoints` 与服务端当前价格不一致,必须刷新后重试。
PriceChanged,
GamePurchaseNotFound,
}
impl fmt::Display for GameDistributionError {
@@ -88,6 +97,9 @@ impl fmt::Display for GameDistributionError {
Self::VersionNotPublished => formatter.write_str("游戏版本当前未公开"),
Self::VersionAlreadyPublished => formatter.write_str("游戏版本已经公开"),
Self::ReviewActionNotAllowed => formatter.write_str("当前版本不允许审核操作"),
Self::FreeGameNotPurchasable => formatter.write_str("免费游戏不需要购买"),
Self::PriceChanged => formatter.write_str("价格已变化,请刷新后重试"),
Self::GamePurchaseNotFound => formatter.write_str("购买记录不存在"),
}
}
}
@@ -16,12 +16,17 @@ pub use reviews::{
pub use application::{
GameDistributionService, GameOperationResult, InMemoryGameDistributionStore,
PublicationOperationResult,
PublicationOperationResult, PurchaseOperationResult,
};
pub use commands::{
CreateGameInput, CreateVersionInput, IdempotencyRequest, PurchaseGameInput, ReviewDecision,
};
pub use commands::{CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision};
pub use domain::{
GameDistributionAction, GameSnapshot, GameVersionSnapshot, GameVersionStatus, GameVisibility,
compute_request_digest, generate_game_id, generate_game_version_id, resolve_version_number,
GAME_PURCHASE_ID_PREFIX, MAX_GAME_PRICE_MUD_POINTS, GameDistributionAction,
GamePurchaseDecision, GamePurchaseSnapshot, GameSnapshot, GameVersionSnapshot,
GameVersionStatus, GameVisibility, compute_request_digest, game_purchase_id,
generate_game_id, generate_game_version_id, normalize_game_price_mud_points,
resolve_game_purchase_decision, resolve_version_number,
};
pub use errors::{GameDistributionError, GameDistributionFieldError};
pub use events::GameDistributionEvent;
@@ -1152,6 +1152,8 @@ pub enum RuntimeProfileWalletLedgerSourceType {
DailyFreeReset,
RechargeRefundRecovery,
LlmRouterConsume,
/// 游戏买断制购买扣费(每账号每游戏一次)。
GamePurchase,
}
impl RuntimeProfileWalletLedgerSourceType {
@@ -1173,6 +1175,7 @@ impl RuntimeProfileWalletLedgerSourceType {
Self::DailyFreeReset => "daily_free_reset",
Self::RechargeRefundRecovery => "recharge_refund_recovery",
Self::LlmRouterConsume => "llm_router_consume",
Self::GamePurchase => "game_purchase",
}
}
}
@@ -195,6 +195,12 @@ pub struct GameDistributionGameSummary {
/// 公开列表/详情保证返回;作者侧及旧响应可省略。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rating_summary: Option<GameDistributionRatingSummary>,
/// 买断制价格(泥点);`0` 表示免费。旧响应可省略,按免费处理。
#[serde(default)]
pub price_mud_points: u64,
/// 当前查看者是否已拥有;匿名与未登录恒为 `false`。
#[serde(default)]
pub purchased: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@@ -240,6 +246,10 @@ pub struct GameDistributionCreateVersionRequest {
/// 缺省时序列化省略该字段,保证旧客户端请求的幂等摘要与升级前一致。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_number: Option<u64>,
/// 作者提交的买断制价格(泥点);`0` 表示免费,缺省按免费处理。
/// 价格随版本冻结,审核通过时与资料一起生效到游戏行。
#[serde(default)]
pub price_mud_points: u64,
pub package_sha256: String,
pub package_bytes: u64,
pub package_file_count: u32,
@@ -284,6 +294,9 @@ pub struct GameDistributionPrivateVersion {
pub status: GameDistributionVersionStatus,
/// 这一版所属游戏的公开切换版本号(撤回与审核回传用)。
pub publication_revision: u64,
/// 该版本冻结的买断制价格(泥点);历史无价格版本按 `0` 展示为免费。
#[serde(default)]
pub price_mud_points: u64,
#[serde(default)]
pub review_reason: Option<String>,
#[serde(default)]
@@ -292,6 +305,41 @@ pub struct GameDistributionPrivateVersion {
pub updated_at: String,
}
/// 买断制购买请求。`expectedPriceMudPoints` 与服务端当前价格做 CAS:
/// 不一致说明作者已通过新版本调价,客户端必须刷新详情后重试。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionPurchaseRequest {
pub expected_price_mud_points: u64,
}
/// 购买记录快照;成交价随购买冻结,之后调价不影响既有所有权。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionPurchase {
pub purchase_id: String,
pub game_id: String,
pub price_mud_points: u64,
pub created_at: String,
}
/// 购买响应:购买快照 + 支付后钱包余额 + 是否幂等重放。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionPurchaseResponse {
pub purchase: GameDistributionPurchase,
pub wallet_balance: u64,
pub replayed: bool,
}
/// 付费游戏播放会话:`playUrl` 已绑定 gameId、用户与短时效,过期或下架即失效。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionPlaySessionResponse {
pub play_url: String,
pub expires_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -442,4 +490,80 @@ mod tests {
assert_eq!(serialized["expectedPublicationRevision"], 3);
assert_eq!(serialized["coverAssetId"], "asset-cover");
}
#[test]
fn purchase_and_play_session_dtos_round_trip() {
let request: GameDistributionPurchaseRequest =
serde_json::from_value(serde_json::json!({ "expectedPriceMudPoints": 120 }))
.expect("购买请求应可解析");
assert_eq!(request.expected_price_mud_points, 120);
let response = serde_json::to_value(GameDistributionPurchaseResponse {
purchase: GameDistributionPurchase {
purchase_id: "gdpurchase_1".to_string(),
game_id: "game-1".to_string(),
price_mud_points: 120,
created_at: "2026-10-05T00:00:00Z".to_string(),
},
wallet_balance: 880,
replayed: false,
})
.expect("购买响应应可序列化");
assert_eq!(response["purchase"]["purchaseId"], "gdpurchase_1");
assert_eq!(response["purchase"]["priceMudPoints"], 120);
assert_eq!(response["walletBalance"], 880);
assert_eq!(response["replayed"], false);
let session = serde_json::to_value(GameDistributionPlaySessionResponse {
play_url: "/api/game-distribution/play-sessions/abc/".to_string(),
expires_at: "2026-10-05T00:05:00Z".to_string(),
})
.expect("播放会话应可序列化");
assert_eq!(
session["playUrl"],
"/api/game-distribution/play-sessions/abc/"
);
assert_eq!(session["expiresAt"], "2026-10-05T00:05:00Z");
}
#[test]
fn legacy_game_summary_defaults_to_free_and_not_purchased() {
let payload: GameDistributionGameSummary = serde_json::from_value(serde_json::json!({
"id": "game-1",
"title": "游戏",
"summary": "简介",
"description": "",
"category": "益智",
"tags": [],
"coverColor": "#ffffff",
"icon": "game",
"author": { "id": "user-1", "name": "作者" },
"deviceSupport": { "desktop": true, "mobile": false, "touch": false },
"inputModes": [],
"orientation": "responsive",
"status": "published",
"publicationRevision": 1,
"currentVersion": null,
"playCount": 0,
"createdAt": "2026-10-05T00:00:00Z",
}))
.expect("旧公开响应应继续可解析");
assert_eq!(payload.price_mud_points, 0);
assert!(!payload.purchased);
let version: GameDistributionPrivateVersion = serde_json::from_value(serde_json::json!({
"versionId": "gamever_1",
"gameId": "game-1",
"versionNumber": 1,
"packageSha256": "a".repeat(64),
"packageBytes": 10,
"packageFileCount": 1,
"status": "pending_review",
"publicationRevision": 1,
"createdAt": "2026-10-05T00:00:00Z",
"updatedAt": "2026-10-05T00:00:00Z",
}))
.expect("旧版本 payload 应继续可解析");
assert_eq!(version.price_mud_points, 0);
}
}
@@ -21,6 +21,7 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invit
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
"asset_operation_consume";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_LLM_ROUTER_CONSUME: &str = "llm_router_consume";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_GAME_PURCHASE: &str = "game_purchase";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
@@ -98,7 +98,8 @@ pub use self::game_distribution::{
GameDistributionAdminUserReviewListRecord, GameDistributionAdminUserReviewRecord,
GameDistributionAdminVersionRecord, GameDistributionGameRecord,
GameDistributionOwnerGameRecord, GameDistributionPublicGameRecord,
GameDistributionRatingSummaryRecord, GameDistributionReviewGameListRecord,
GameDistributionPurchaseRecord, GameDistributionRatingSummaryRecord,
GameDistributionReviewGameListRecord,
GameDistributionReviewGameRecord, GameDistributionReviewModerationOperationRecord,
GameDistributionReviewModerationRecord, GameDistributionUserReviewListRecord,
GameDistributionUserReviewRecord, GameDistributionUserReviewSaveRecord,
@@ -169,7 +170,8 @@ pub(crate) use self::game_distribution::{
ensure_admin_user_review_result, ensure_user_review_result, map_admin_user_review,
map_game_distribution_admin_game_list_result, map_game_distribution_game_result,
map_game_distribution_owner_game_list_result, map_game_distribution_public_game_list_result,
map_game_distribution_public_game_result, map_game_distribution_review_list_result,
map_game_distribution_public_game_result, map_game_distribution_purchase_result,
map_game_distribution_review_list_result,
map_game_distribution_version_result, map_review_moderation_operation, map_user_review,
};
pub(crate) use self::payment::{
@@ -181,6 +181,19 @@ pub struct GameDistributionGameRecord {
pub cover_object_key: Option<String>,
pub local_project_id: Option<String>,
pub screenshots_json: Option<String>,
/// 买断制价格(泥点);`0` 表示免费。
pub price_mud_points: u64,
}
/// 买断制购买记录投影;成交价随购买冻结。
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GameDistributionPurchaseRecord {
pub purchase_id: String,
pub game_id: String,
pub user_id: String,
pub price_mud_points: u64,
pub wallet_ledger_id: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -278,6 +291,7 @@ fn map_game(
local_project_id: value.local_project_id,
cover_object_key: value.cover_object_key,
screenshots_json: value.screenshots_json,
price_mud_points: value.price_mud_points,
}
}
@@ -449,3 +463,24 @@ pub(crate) fn map_game_distribution_review_list_result(
}
Ok(result.versions.into_iter().map(map_version).collect())
}
/// 解析购买/回读统一结果:`(购买记录, 钱包余额, replayed)`。
pub(crate) fn map_game_distribution_purchase_result(
result: crate::module_bindings::GameDistributionPurchaseResult,
) -> Result<(Option<GameDistributionPurchaseRecord>, u64, bool), SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok((
result.purchase.map(|purchase| GameDistributionPurchaseRecord {
purchase_id: purchase.purchase_id,
game_id: purchase.game_id,
user_id: purchase.user_id,
price_mud_points: purchase.price_mud_points,
wallet_ledger_id: purchase.wallet_ledger_id,
created_at: shared_kernel::format_timestamp_micros(purchase.created_at_micros),
}),
result.wallet_balance,
result.replayed,
))
}
@@ -70,6 +70,9 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::RechargeRefundRecovery => {
module_runtime::RuntimeProfileWalletLedgerSourceType::RechargeRefundRecovery
}
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::GamePurchase => {
module_runtime::RuntimeProfileWalletLedgerSourceType::GamePurchase
}
}
}
@@ -244,6 +244,17 @@ pub struct GameDistributionDeleteGameRecordInput {
pub now_micros: i64,
}
/// 买断制购买;`expected_price_mud_points` 与服务端当前价格 CAS。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GameDistributionPurchaseRecordInput {
pub game_id: String,
pub user_id: String,
pub expected_price_mud_points: u64,
pub idempotency_key: String,
pub request_digest: String,
pub now_micros: i64,
}
impl SpacetimeClient {
pub async fn list_admin_game_distribution_user_reviews(
&self,
@@ -1260,4 +1271,71 @@ impl SpacetimeClient {
)
.await
}
/// 买断制购买;返回 `(购买记录, 支付后钱包余额, 是否重放)`。
pub async fn purchase_game_distribution_game(
&self,
input: GameDistributionPurchaseRecordInput,
) -> Result<(GameDistributionPurchaseRecord, u64, bool), SpacetimeClientError> {
let procedure_input = crate::module_bindings::GameDistributionPurchaseInput {
game_id: input.game_id,
user_id: input.user_id,
expected_price_mud_points: input.expected_price_mud_points,
idempotency_key: input.idempotency_key,
request_digest: input.request_digest,
now_micros: input.now_micros,
};
self.call_after_connect(
"purchase_game_distribution_game",
move |connection, sender| {
connection
.procedures()
.purchase_game_distribution_game_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_game_distribution_purchase_result)
.and_then(|(purchase, wallet_balance, replayed)| {
purchase
.map(|purchase| (purchase, wallet_balance, replayed))
.ok_or_else(|| {
SpacetimeClientError::missing_snapshot("游戏购买结果")
})
});
send_once(&sender, mapped);
},
);
},
)
.await
}
/// 回读购买记录与当前钱包余额;未购买时购买记录为空。
pub async fn get_game_distribution_purchase(
&self,
game_id: String,
user_id: String,
) -> Result<(Option<GameDistributionPurchaseRecord>, u64), SpacetimeClientError> {
let procedure_input =
crate::module_bindings::GameDistributionPurchaseLookupInput { game_id, user_id };
self.call_after_connect(
"get_game_distribution_purchase",
move |connection, sender| {
connection
.procedures()
.get_game_distribution_purchase_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_game_distribution_purchase_result)
.map(|(purchase, wallet_balance, _)| (purchase, wallet_balance));
send_once(&sender, mapped);
},
);
},
)
.await
}
}
@@ -441,6 +441,12 @@ pub mod game_distribution_procedure_result_type;
pub mod game_distribution_public_game_input_type;
pub mod game_distribution_public_game_list_input_type;
pub mod game_distribution_public_game_snapshot_type;
pub mod game_distribution_purchase_input_type;
pub mod game_distribution_purchase_lookup_input_type;
pub mod game_distribution_purchase_result_type;
pub mod game_distribution_purchase_snapshot_type;
pub mod game_distribution_purchase_table;
pub mod game_distribution_purchase_type;
pub mod game_distribution_reject_input_type;
pub mod game_distribution_restore_input_type;
pub mod game_distribution_review_game_list_input_type;
@@ -486,6 +492,7 @@ pub mod get_external_generation_queue_stats_and_return_procedure;
pub mod get_feature_gate_config_procedure;
pub mod get_game_distribution_game_and_return_procedure;
pub mod get_game_distribution_my_review_and_return_procedure;
pub mod get_game_distribution_purchase_and_return_procedure;
pub mod get_game_distribution_version_and_return_procedure;
pub mod get_llm_router_account_and_return_procedure;
pub mod get_owner_game_distribution_version_and_return_procedure;
@@ -655,6 +662,7 @@ pub mod public_work_like_table;
pub mod public_work_like_type;
pub mod public_work_play_daily_stat_table;
pub mod public_work_play_daily_stat_type;
pub mod purchase_game_distribution_game_and_return_procedure;
pub mod put_database_migration_import_chunk_procedure;
pub mod query_analytics_metric_procedure;
pub mod read_agc_model_catalog_procedure;
@@ -1302,6 +1310,12 @@ pub use game_distribution_procedure_result_type::GameDistributionProcedureResult
pub use game_distribution_public_game_input_type::GameDistributionPublicGameInput;
pub use game_distribution_public_game_list_input_type::GameDistributionPublicGameListInput;
pub use game_distribution_public_game_snapshot_type::GameDistributionPublicGameSnapshot;
pub use game_distribution_purchase_input_type::GameDistributionPurchaseInput;
pub use game_distribution_purchase_lookup_input_type::GameDistributionPurchaseLookupInput;
pub use game_distribution_purchase_result_type::GameDistributionPurchaseResult;
pub use game_distribution_purchase_snapshot_type::GameDistributionPurchaseSnapshot;
pub use game_distribution_purchase_table::*;
pub use game_distribution_purchase_type::GameDistributionPurchase;
pub use game_distribution_reject_input_type::GameDistributionRejectInput;
pub use game_distribution_restore_input_type::GameDistributionRestoreInput;
pub use game_distribution_review_game_list_input_type::GameDistributionReviewGameListInput;
@@ -1347,6 +1361,7 @@ pub use get_external_generation_queue_stats_and_return_procedure::get_external_g
pub use get_feature_gate_config_procedure::get_feature_gate_config;
pub use get_game_distribution_game_and_return_procedure::get_game_distribution_game_and_return;
pub use get_game_distribution_my_review_and_return_procedure::get_game_distribution_my_review_and_return;
pub use get_game_distribution_purchase_and_return_procedure::get_game_distribution_purchase_and_return;
pub use get_game_distribution_version_and_return_procedure::get_game_distribution_version_and_return;
pub use get_llm_router_account_and_return_procedure::get_llm_router_account_and_return;
pub use get_owner_game_distribution_version_and_return_procedure::get_owner_game_distribution_version_and_return;
@@ -1516,6 +1531,7 @@ pub use public_work_like_table::*;
pub use public_work_like_type::PublicWorkLike;
pub use public_work_play_daily_stat_table::*;
pub use public_work_play_daily_stat_type::PublicWorkPlayDailyStat;
pub use purchase_game_distribution_game_and_return_procedure::purchase_game_distribution_game_and_return;
pub use put_database_migration_import_chunk_procedure::put_database_migration_import_chunk;
pub use query_analytics_metric_procedure::query_analytics_metric;
pub use read_agc_model_catalog_procedure::read_agc_model_catalog;
@@ -1882,6 +1898,7 @@ pub struct DbUpdate {
feature_gate_config: __sdk::TableUpdate<FeatureGateConfig>,
game_distribution_game: __sdk::TableUpdate<GameDistributionGame>,
game_distribution_idempotency_receipt: __sdk::TableUpdate<GameDistributionIdempotencyReceipt>,
game_distribution_purchase: __sdk::TableUpdate<GameDistributionPurchase>,
game_distribution_review: __sdk::TableUpdate<GameDistributionReview>,
game_distribution_review_moderation_log:
__sdk::TableUpdate<GameDistributionReviewModerationLog>,
@@ -2104,6 +2121,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
)?,
)
}
"game_distribution_purchase" => db_update.game_distribution_purchase.append(
game_distribution_purchase_table::parse_table_update(table_update)?,
),
"game_distribution_review" => db_update.game_distribution_review.append(
game_distribution_review_table::parse_table_update(table_update)?,
),
@@ -2519,6 +2539,12 @@ impl __sdk::DbUpdate for DbUpdate {
&self.game_distribution_idempotency_receipt,
)
.with_updates_by_pk(|row| &row.receipt_id);
diff.game_distribution_purchase = cache
.apply_diff_to_table::<GameDistributionPurchase>(
"game_distribution_purchase",
&self.game_distribution_purchase,
)
.with_updates_by_pk(|row| &row.purchase_id);
diff.game_distribution_review = cache
.apply_diff_to_table::<GameDistributionReview>(
"game_distribution_review",
@@ -2907,6 +2933,9 @@ impl __sdk::DbUpdate for DbUpdate {
"game_distribution_idempotency_receipt" => db_update
.game_distribution_idempotency_receipt
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"game_distribution_purchase" => db_update
.game_distribution_purchase
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"game_distribution_review" => db_update
.game_distribution_review
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -3193,6 +3222,9 @@ impl __sdk::DbUpdate for DbUpdate {
"game_distribution_idempotency_receipt" => db_update
.game_distribution_idempotency_receipt
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"game_distribution_purchase" => db_update
.game_distribution_purchase
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"game_distribution_review" => db_update
.game_distribution_review
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -3394,6 +3426,7 @@ pub struct AppliedDiff<'r> {
game_distribution_game: __sdk::TableAppliedDiff<'r, GameDistributionGame>,
game_distribution_idempotency_receipt:
__sdk::TableAppliedDiff<'r, GameDistributionIdempotencyReceipt>,
game_distribution_purchase: __sdk::TableAppliedDiff<'r, GameDistributionPurchase>,
game_distribution_review: __sdk::TableAppliedDiff<'r, GameDistributionReview>,
game_distribution_review_moderation_log:
__sdk::TableAppliedDiff<'r, GameDistributionReviewModerationLog>,
@@ -3676,6 +3709,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.game_distribution_idempotency_receipt,
event,
);
callbacks.invoke_table_row_callbacks::<GameDistributionPurchase>(
"game_distribution_purchase",
&self.game_distribution_purchase,
event,
);
callbacks.invoke_table_row_callbacks::<GameDistributionReview>(
"game_distribution_review",
&self.game_distribution_review,
@@ -4157,19 +4195,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
{
}
@@ -4607,6 +4645,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
feature_gate_config_table::register_table(client_cache);
game_distribution_game_table::register_table(client_cache);
game_distribution_idempotency_receipt_table::register_table(client_cache);
game_distribution_purchase_table::register_table(client_cache);
game_distribution_review_table::register_table(client_cache);
game_distribution_review_moderation_log_table::register_table(client_cache);
game_distribution_version_table::register_table(client_cache);
@@ -4700,6 +4739,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
"feature_gate_config",
"game_distribution_game",
"game_distribution_idempotency_receipt",
"game_distribution_purchase",
"game_distribution_review",
"game_distribution_review_moderation_log",
"game_distribution_version",
@@ -34,10 +34,10 @@ pub trait acknowledge_external_generation_job_summaries_and_return {
input: ExternalGenerationJobAcknowledgeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobSummaryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobSummaryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl acknowledge_external_generation_job_summaries_and_return for super::RemoteP
input: ExternalGenerationJobAcknowledgeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<ExternalGenerationJobSummaryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<ExternalGenerationJobSummaryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, ExternalGenerationJobSummaryProcedureResult>(
@@ -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>(
@@ -31,10 +31,10 @@ pub trait activate_editor_canvas_layout_and_return {
input: EditorCanvasLayoutMigrationInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl activate_editor_canvas_layout_and_return for super::RemoteProcedures {
input: EditorCanvasLayoutMigrationInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<EditorCanvasLayoutMigrationProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, EditorCanvasLayoutMigrationProcedureResult>(
@@ -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_and_return {
input: RuntimeProfileAdminWalletGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_get_profile_wallet_and_return for super::RemoteProcedures {
input: RuntimeProfileAdminWalletGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileAdminWalletProcedureResult>(
@@ -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>(
@@ -31,10 +31,10 @@ pub trait admin_get_profile_wallet_detail_and_return {
input: RuntimeProfileAdminWalletGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletDetailProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletDetailProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_get_profile_wallet_detail_and_return for super::RemoteProcedures {
input: RuntimeProfileAdminWalletGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletDetailProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<RuntimeProfileAdminWalletDetailProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileAdminWalletDetailProcedureResult>(
@@ -37,13 +37,13 @@ pub trait admin_initialize_profile_wallet_consumption_projections_and_return {
input: RuntimeProfileWalletConsumptionProjectionInitializeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<
RuntimeProfileWalletConsumptionProjectionInitializeProcedureResult,
__sdk::InternalError,
>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<
RuntimeProfileWalletConsumptionProjectionInitializeProcedureResult,
__sdk::InternalError,
>,
) + Send
+ 'static,
);
}
@@ -55,13 +55,13 @@ impl admin_initialize_profile_wallet_consumption_projections_and_return
input: RuntimeProfileWalletConsumptionProjectionInitializeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<
RuntimeProfileWalletConsumptionProjectionInitializeProcedureResult,
__sdk::InternalError,
>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<
RuntimeProfileWalletConsumptionProjectionInitializeProcedureResult,
__sdk::InternalError,
>,
) + Send
+ 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileWalletConsumptionProjectionInitializeProcedureResult>(
"admin_initialize_profile_wallet_consumption_projections_and_return",
@@ -31,10 +31,10 @@ pub trait admin_list_editor_assets_and_return {
input: AdminEditorAssetListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminEditorAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminEditorAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl admin_list_editor_assets_and_return for super::RemoteProcedures {
input: AdminEditorAssetListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<AdminEditorAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<AdminEditorAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, AdminEditorAssetListProcedureResult>(
@@ -34,10 +34,10 @@ pub trait admin_list_editor_showcase_assets_and_return {
input: EditorShowcaseAssetAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorShowcaseAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<EditorShowcaseAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl admin_list_editor_showcase_assets_and_return for super::RemoteProcedures {
input: EditorShowcaseAssetAdminListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorShowcaseAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<EditorShowcaseAssetListProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, EditorShowcaseAssetListProcedureResult>(

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