@@ -0,0 +1,64 @@
|
|||||||
|
# 资产生成叙世币消耗接入方案
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前叙世币钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成由 Axum API 调用外部模型并写入 OSS,SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此扣费需要拆成两层:
|
||||||
|
|
||||||
|
- SpacetimeDB 负责钱包余额和流水的原子变更。
|
||||||
|
- Axum 负责在发起外部生成前扣费,并在生成或持久化失败时补偿退款。
|
||||||
|
|
||||||
|
## 首期范围
|
||||||
|
|
||||||
|
首期接入带 Bearer 身份、能明确归属真实用户的资产生成与发布入口:
|
||||||
|
|
||||||
|
- `POST /api/custom-world/scene-image`
|
||||||
|
- `POST /api/custom-world/cover-image`
|
||||||
|
- `POST /api/runtime/custom-world/cover-image`
|
||||||
|
- `POST /api/runtime/custom-world-library/{profile_id}/publish`
|
||||||
|
- 自定义世界 Agent 动作 `publish_world`
|
||||||
|
- Big Fish Agent 正式图片生成动作 `big_fish_generate_level_main_image`、`big_fish_generate_level_motion`、`big_fish_generate_stage_background`
|
||||||
|
- Big Fish Agent 动作 `big_fish_publish_game`
|
||||||
|
- Puzzle Agent 图片生成动作 `compile_puzzle_draft`、`generate_puzzle_images`
|
||||||
|
- Puzzle Agent 动作 `publish_puzzle_work`
|
||||||
|
|
||||||
|
暂不接入以下入口:
|
||||||
|
|
||||||
|
- 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。
|
||||||
|
- 手动上传封面:不调用外部生成模型,不消耗叙世币。
|
||||||
|
- 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。
|
||||||
|
- 文本实体、NPC 生成:本次需求聚焦资产生成,首期只覆盖图片资产。
|
||||||
|
|
||||||
|
## 计费规则
|
||||||
|
|
||||||
|
- 每次图片资产生成请求消耗 `1` 枚叙世币。
|
||||||
|
- 每次作品发布请求消耗 `1` 枚叙世币;余额不足时禁止发布。
|
||||||
|
- 在调用外部图片生成前预扣,余额不足时直接返回业务错误,不调用外部模型。
|
||||||
|
- 发布请求在写入发布状态前预扣,余额不足时直接返回业务错误,不调用发布 mutation。
|
||||||
|
- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,Axum 自动发起同额退款。
|
||||||
|
- 如果退款失败,原始错误仍返回给调用方,同时服务端日志记录退款失败,便于后续人工核对。
|
||||||
|
|
||||||
|
## 钱包流水
|
||||||
|
|
||||||
|
新增两个流水来源类型,首期同时覆盖“资产生成”和“资产发布”这两类资产操作:
|
||||||
|
|
||||||
|
- `asset_generation_consume`:资产生成预扣,`amount_delta = -1`。
|
||||||
|
- `asset_generation_refund`:资产生成失败退款,`amount_delta = +1`。
|
||||||
|
|
||||||
|
`wallet_ledger_id` 由 Axum 传入,格式:
|
||||||
|
|
||||||
|
- 扣费:`asset_generation_consume:{user_id}:{asset_kind}:{asset_id}`
|
||||||
|
- 退款:`asset_generation_refund:{user_id}:{asset_kind}:{asset_id}`
|
||||||
|
|
||||||
|
SpacetimeDB procedure 对 `ledger_id` 做幂等保护:如果同一个流水 ID 已存在,则直接返回当前钱包快照,不重复变更余额。
|
||||||
|
|
||||||
|
## 工程落点
|
||||||
|
|
||||||
|
- `module-runtime`:新增钱包调整输入、钱包调整结果、流水来源枚举。
|
||||||
|
- `spacetime-module`:新增 `consume_profile_wallet_points_and_return` 与 `refund_profile_wallet_points_and_return` procedure,并扩展钱包变更 helper 支持负数。
|
||||||
|
- `spacetime-client`:新增对应调用方法和绑定类型。
|
||||||
|
- `api-server`:在自定义世界图片生成与发布入口前扣费,错误分支退款。
|
||||||
|
- `shared-contracts`:新增 API 流水来源常量,保证“我的-钱包流水”输出使用稳定契约字符串。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
本次不做分档价格、不做会员免扣、不做前端计费展示改造,也不迁移旧 `server-node` 逻辑。旧资产工坊角色主形象/动作生成与发布接口仍需要先补齐 Bearer 身份归属后再纳入扣费范围。
|
||||||
81
server-rs/crates/api-server/src/asset_billing.rs
Normal file
81
server-rs/crates/api-server/src/asset_billing.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use serde_json::json;
|
||||||
|
use spacetime_client::SpacetimeClientError;
|
||||||
|
|
||||||
|
use crate::{http_error::AppError, state::AppState};
|
||||||
|
|
||||||
|
pub(crate) const ASSET_OPERATION_POINTS_COST: u64 = 1;
|
||||||
|
|
||||||
|
/// 资产操作统一预扣叙世币;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。
|
||||||
|
pub(crate) async fn consume_asset_operation_points(
|
||||||
|
state: &AppState,
|
||||||
|
owner_user_id: &str,
|
||||||
|
asset_kind: &str,
|
||||||
|
asset_id: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let ledger_id = format!(
|
||||||
|
"asset_generation_consume:{}:{}:{}",
|
||||||
|
owner_user_id, asset_kind, asset_id
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.spacetime_client()
|
||||||
|
.consume_profile_wallet_points(
|
||||||
|
owner_user_id.to_string(),
|
||||||
|
ASSET_OPERATION_POINTS_COST,
|
||||||
|
ledger_id,
|
||||||
|
current_utc_micros(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(map_asset_operation_wallet_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 外部生成或发布 mutation 失败后补偿退款;退款失败只记日志,避免覆盖原始业务错误。
|
||||||
|
pub(crate) async fn refund_asset_operation_points(
|
||||||
|
state: &AppState,
|
||||||
|
owner_user_id: &str,
|
||||||
|
asset_kind: &str,
|
||||||
|
asset_id: &str,
|
||||||
|
) {
|
||||||
|
let ledger_id = format!(
|
||||||
|
"asset_generation_refund:{}:{}:{}",
|
||||||
|
owner_user_id, asset_kind, asset_id
|
||||||
|
);
|
||||||
|
if let Err(error) = state
|
||||||
|
.spacetime_client()
|
||||||
|
.refund_profile_wallet_points(
|
||||||
|
owner_user_id.to_string(),
|
||||||
|
ASSET_OPERATION_POINTS_COST,
|
||||||
|
ledger_id,
|
||||||
|
current_utc_micros(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
asset_id,
|
||||||
|
error = %error,
|
||||||
|
"资产操作失败后的叙世币退款失败"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> AppError {
|
||||||
|
let status = match &error {
|
||||||
|
SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST,
|
||||||
|
SpacetimeClientError::Procedure(message) if message.contains("叙世币余额不足") => {
|
||||||
|
StatusCode::CONFLICT
|
||||||
|
}
|
||||||
|
_ => StatusCode::BAD_GATEWAY,
|
||||||
|
};
|
||||||
|
|
||||||
|
AppError::from_status(status).with_details(json!({
|
||||||
|
"provider": "profile-wallet",
|
||||||
|
"message": error.to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_utc_micros() -> i64 {
|
||||||
|
time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64 / 1_000
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ use crate::{
|
|||||||
AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter,
|
AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter,
|
||||||
},
|
},
|
||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
|
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
|
||||||
auth::AuthenticatedAccessToken,
|
auth::AuthenticatedAccessToken,
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
request_context::RequestContext,
|
request_context::RequestContext,
|
||||||
@@ -471,9 +472,30 @@ pub async fn execute_big_fish_action(
|
|||||||
|
|
||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
let now = current_utc_micros();
|
let now = current_utc_micros();
|
||||||
let session = match payload.action.trim() {
|
let action = payload.action.trim().to_string();
|
||||||
|
let billed_asset_kind = match action.as_str() {
|
||||||
|
"big_fish_generate_level_main_image" => Some("big_fish_level_main_image"),
|
||||||
|
"big_fish_generate_level_motion" => Some("big_fish_level_motion"),
|
||||||
|
"big_fish_generate_stage_background" => Some("big_fish_stage_background"),
|
||||||
|
"big_fish_publish_game" => Some("big_fish_publish_game"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let billing_asset_id = format!("{session_id}:{now}");
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| big_fish_error_response(&request_context, error))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_result = match action.as_str() {
|
||||||
"big_fish_compile_draft" => {
|
"big_fish_compile_draft" => {
|
||||||
compile_big_fish_draft_with_all_assets(&state, session_id, owner_user_id, now).await
|
compile_big_fish_draft_with_all_assets(
|
||||||
|
&state,
|
||||||
|
session_id.clone(),
|
||||||
|
owner_user_id.clone(),
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
"big_fish_generate_level_main_image" => {
|
"big_fish_generate_level_main_image" => {
|
||||||
let asset_url = generate_big_fish_formal_asset(
|
let asset_url = generate_big_fish_formal_asset(
|
||||||
@@ -486,12 +508,30 @@ pub async fn execute_big_fish_action(
|
|||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| big_fish_error_response(&request_context, error))?;
|
.map_err(|error| {
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
tokio::spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
let owner_user_id = owner_user_id.clone();
|
||||||
|
let billing_asset_id = billing_asset_id.clone();
|
||||||
|
async move {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
&billing_asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
big_fish_error_response(&request_context, error)
|
||||||
|
})?;
|
||||||
state
|
state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||||
session_id,
|
owner_user_id: owner_user_id.clone(),
|
||||||
owner_user_id,
|
session_id: session_id.clone(),
|
||||||
asset_kind: "level_main_image".to_string(),
|
asset_kind: "level_main_image".to_string(),
|
||||||
level: payload.level,
|
level: payload.level,
|
||||||
motion_key: None,
|
motion_key: None,
|
||||||
@@ -511,12 +551,30 @@ pub async fn execute_big_fish_action(
|
|||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| big_fish_error_response(&request_context, error))?;
|
.map_err(|error| {
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
tokio::spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
let owner_user_id = owner_user_id.clone();
|
||||||
|
let billing_asset_id = billing_asset_id.clone();
|
||||||
|
async move {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
&billing_asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
big_fish_error_response(&request_context, error)
|
||||||
|
})?;
|
||||||
state
|
state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||||
session_id,
|
owner_user_id: owner_user_id.clone(),
|
||||||
owner_user_id,
|
session_id: session_id.clone(),
|
||||||
asset_kind: "level_motion".to_string(),
|
asset_kind: "level_motion".to_string(),
|
||||||
level: payload.level,
|
level: payload.level,
|
||||||
motion_key: payload.motion_key,
|
motion_key: payload.motion_key,
|
||||||
@@ -536,12 +594,30 @@ pub async fn execute_big_fish_action(
|
|||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| big_fish_error_response(&request_context, error))?;
|
.map_err(|error| {
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
tokio::spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
let owner_user_id = owner_user_id.clone();
|
||||||
|
let billing_asset_id = billing_asset_id.clone();
|
||||||
|
async move {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
&billing_asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
big_fish_error_response(&request_context, error)
|
||||||
|
})?;
|
||||||
state
|
state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||||
session_id,
|
owner_user_id: owner_user_id.clone(),
|
||||||
owner_user_id,
|
session_id: session_id.clone(),
|
||||||
asset_kind: "stage_background".to_string(),
|
asset_kind: "stage_background".to_string(),
|
||||||
level: None,
|
level: None,
|
||||||
motion_key: None,
|
motion_key: None,
|
||||||
@@ -553,7 +629,7 @@ pub async fn execute_big_fish_action(
|
|||||||
"big_fish_publish_game" => {
|
"big_fish_publish_game" => {
|
||||||
state
|
state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.publish_big_fish_game(session_id, owner_user_id, now)
|
.publish_big_fish_game(session_id, owner_user_id.clone(), now)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
@@ -562,8 +638,25 @@ pub async fn execute_big_fish_action(
|
|||||||
format!("action `{other}` is not supported").as_str(),
|
format!("action `{other}` is not supported").as_str(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
.map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?;
|
let session = match session_result {
|
||||||
|
Ok(session) => session,
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
&billing_asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
return Err(big_fish_error_response(
|
||||||
|
&request_context,
|
||||||
|
map_big_fish_client_error(error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(json_success_body(
|
Ok(json_success_body(
|
||||||
Some(&request_context),
|
Some(&request_context),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ use crate::{
|
|||||||
AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter,
|
AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter,
|
||||||
},
|
},
|
||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
|
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
|
||||||
auth::AuthenticatedAccessToken,
|
auth::AuthenticatedAccessToken,
|
||||||
character_visual_assets::generate_character_primary_visual_for_profile,
|
character_visual_assets::generate_character_primary_visual_for_profile,
|
||||||
custom_world_agent_entities::generate_custom_world_agent_entities,
|
custom_world_agent_entities::generate_custom_world_agent_entities,
|
||||||
@@ -350,20 +351,37 @@ pub async fn publish_custom_world_library_profile(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mutation = state
|
consume_asset_operation_points(&state, &owner_user_id, "custom_world_publish", &profile_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||||
|
|
||||||
|
let mutation_result = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.publish_custom_world_profile(
|
.publish_custom_world_profile(
|
||||||
profile_id,
|
profile_id.clone(),
|
||||||
owner_user_id,
|
owner_user_id.clone(),
|
||||||
None,
|
None,
|
||||||
resolve_author_public_user_code(&state, &authenticated, &request_context)?,
|
resolve_author_public_user_code(&state, &authenticated, &request_context)?,
|
||||||
resolve_author_display_name(&state, &authenticated),
|
resolve_author_display_name(&state, &authenticated),
|
||||||
current_utc_micros(),
|
current_utc_micros(),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.map_err(|error| {
|
let mutation = match mutation_result {
|
||||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
Ok(mutation) => mutation,
|
||||||
})?;
|
Err(error) => {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"custom_world_publish",
|
||||||
|
&profile_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Err(custom_world_error_response(
|
||||||
|
&request_context,
|
||||||
|
map_custom_world_client_error(error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(json_success_body(
|
Ok(json_success_body(
|
||||||
Some(&request_context),
|
Some(&request_context),
|
||||||
@@ -1227,7 +1245,19 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = state
|
let should_bill_publish = action == "publish_world";
|
||||||
|
if should_bill_publish {
|
||||||
|
consume_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"custom_world_agent_publish",
|
||||||
|
&session_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
@@ -1238,9 +1268,24 @@ pub async fn execute_custom_world_agent_action(
|
|||||||
submitted_at_micros,
|
submitted_at_micros,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
{
|
||||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
Ok(result) => result,
|
||||||
})?;
|
Err(error) => {
|
||||||
|
if should_bill_publish {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"custom_world_agent_publish",
|
||||||
|
&session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
return Err(custom_world_error_response(
|
||||||
|
&request_context,
|
||||||
|
map_custom_world_client_error(error),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
action.as_str(),
|
action.as_str(),
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ use crate::{
|
|||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ASSET_GENERATION_POINTS_COST: u64 = 1;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct CustomWorldEntityRequest {
|
pub(crate) struct CustomWorldEntityRequest {
|
||||||
@@ -443,14 +441,14 @@ pub async fn generate_custom_world_scene_image(
|
|||||||
let normalized = normalize_scene_image_request(payload)
|
let normalized = normalize_scene_image_request(payload)
|
||||||
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
let asset_id = format!("custom-scene-{}", current_utc_millis());
|
let asset_id = format!("custom-scene-{}", current_utc_millis());
|
||||||
consume_asset_generation_points(
|
crate::asset_billing::consume_asset_operation_points(
|
||||||
&state,
|
&state,
|
||||||
&owner_user_id,
|
&owner_user_id,
|
||||||
"scene_image",
|
"scene_image",
|
||||||
asset_id.as_str(),
|
asset_id.as_str(),
|
||||||
&request_context,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
let asset_result = async {
|
let asset_result = async {
|
||||||
let settings = require_dashscope_settings(&state)?;
|
let settings = require_dashscope_settings(&state)?;
|
||||||
let http_client = build_dashscope_http_client(&settings)?;
|
let http_client = build_dashscope_http_client(&settings)?;
|
||||||
@@ -553,7 +551,13 @@ pub async fn generate_custom_world_scene_image(
|
|||||||
let asset = match asset_result {
|
let asset = match asset_result {
|
||||||
Ok(asset) => asset,
|
Ok(asset) => asset,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
refund_asset_generation_points(&state, &owner_user_id, "scene_image", &asset_id).await;
|
crate::asset_billing::refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"scene_image",
|
||||||
|
&asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
return Err(custom_world_ai_error_response(&request_context, error));
|
return Err(custom_world_ai_error_response(&request_context, error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -713,14 +717,14 @@ pub async fn generate_custom_world_cover_image(
|
|||||||
let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone());
|
let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone());
|
||||||
let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string());
|
let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string());
|
||||||
let asset_id = format!("custom-cover-{}", current_utc_millis());
|
let asset_id = format!("custom-cover-{}", current_utc_millis());
|
||||||
consume_asset_generation_points(
|
crate::asset_billing::consume_asset_operation_points(
|
||||||
&state,
|
&state,
|
||||||
&owner_user_id,
|
&owner_user_id,
|
||||||
"custom_world_cover",
|
"custom_world_cover",
|
||||||
asset_id.as_str(),
|
asset_id.as_str(),
|
||||||
&request_context,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||||
let asset_result = async {
|
let asset_result = async {
|
||||||
let settings = require_dashscope_settings(&state)?;
|
let settings = require_dashscope_settings(&state)?;
|
||||||
let http_client = build_dashscope_http_client(&settings)?;
|
let http_client = build_dashscope_http_client(&settings)?;
|
||||||
@@ -824,8 +828,13 @@ pub async fn generate_custom_world_cover_image(
|
|||||||
let asset = match asset_result {
|
let asset = match asset_result {
|
||||||
Ok(asset) => asset,
|
Ok(asset) => asset,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
refund_asset_generation_points(&state, &owner_user_id, "custom_world_cover", &asset_id)
|
crate::asset_billing::refund_asset_operation_points(
|
||||||
.await;
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"custom_world_cover",
|
||||||
|
&asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
return Err(custom_world_ai_error_response(&request_context, error));
|
return Err(custom_world_ai_error_response(&request_context, error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -903,81 +912,6 @@ pub async fn upload_custom_world_cover_image(
|
|||||||
Ok(json_success_body(Some(&request_context), asset))
|
Ok(json_success_body(Some(&request_context), asset))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn consume_asset_generation_points(
|
|
||||||
state: &AppState,
|
|
||||||
owner_user_id: &str,
|
|
||||||
asset_kind: &str,
|
|
||||||
asset_id: &str,
|
|
||||||
request_context: &RequestContext,
|
|
||||||
) -> Result<(), Response> {
|
|
||||||
let ledger_id = format!(
|
|
||||||
"asset_generation_consume:{}:{}:{}",
|
|
||||||
owner_user_id, asset_kind, asset_id
|
|
||||||
);
|
|
||||||
let created_at_micros = current_utc_micros();
|
|
||||||
state
|
|
||||||
.spacetime_client()
|
|
||||||
.consume_profile_wallet_points(
|
|
||||||
owner_user_id.to_string(),
|
|
||||||
ASSET_GENERATION_POINTS_COST,
|
|
||||||
ledger_id,
|
|
||||||
created_at_micros,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|error| {
|
|
||||||
custom_world_ai_error_response(
|
|
||||||
request_context,
|
|
||||||
map_asset_generation_wallet_error(error),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn refund_asset_generation_points(
|
|
||||||
state: &AppState,
|
|
||||||
owner_user_id: &str,
|
|
||||||
asset_kind: &str,
|
|
||||||
asset_id: &str,
|
|
||||||
) {
|
|
||||||
let ledger_id = format!(
|
|
||||||
"asset_generation_refund:{}:{}:{}",
|
|
||||||
owner_user_id, asset_kind, asset_id
|
|
||||||
);
|
|
||||||
if let Err(error) = state
|
|
||||||
.spacetime_client()
|
|
||||||
.refund_profile_wallet_points(
|
|
||||||
owner_user_id.to_string(),
|
|
||||||
ASSET_GENERATION_POINTS_COST,
|
|
||||||
ledger_id,
|
|
||||||
current_utc_micros(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
owner_user_id,
|
|
||||||
asset_kind,
|
|
||||||
asset_id,
|
|
||||||
error = %error,
|
|
||||||
"资产生成失败后的叙世币退款失败"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_asset_generation_wallet_error(error: SpacetimeClientError) -> AppError {
|
|
||||||
let status = match &error {
|
|
||||||
SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST,
|
|
||||||
SpacetimeClientError::Procedure(message) if message.contains("叙世币余额不足") => {
|
|
||||||
StatusCode::CONFLICT
|
|
||||||
}
|
|
||||||
_ => StatusCode::BAD_GATEWAY,
|
|
||||||
};
|
|
||||||
|
|
||||||
AppError::from_status(status).with_details(json!({
|
|
||||||
"provider": "profile-wallet",
|
|
||||||
"message": error.to_string(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn persist_custom_world_asset(
|
async fn persist_custom_world_asset(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
owner_user_id: &str,
|
owner_user_id: &str,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod ai_generation_drafts;
|
|||||||
mod ai_tasks;
|
mod ai_tasks;
|
||||||
mod api_response;
|
mod api_response;
|
||||||
mod app;
|
mod app;
|
||||||
|
mod asset_billing;
|
||||||
mod assets;
|
mod assets;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod auth_me;
|
mod auth_me;
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ use shared_contracts::{
|
|||||||
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
|
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
|
||||||
puzzle_runtime::{
|
puzzle_runtime::{
|
||||||
AdvanceLocalPuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
|
AdvanceLocalPuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
|
||||||
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse,
|
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
|
||||||
PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse, PuzzleRunResponse,
|
PuzzlePieceStateResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
|
||||||
PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest,
|
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
|
||||||
SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest,
|
SwapPuzzlePiecesRequest,
|
||||||
},
|
},
|
||||||
puzzle_works::{
|
puzzle_works::{
|
||||||
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
||||||
@@ -55,12 +55,11 @@ use spacetime_client::{
|
|||||||
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
|
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
|
||||||
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
|
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
|
||||||
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
|
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
|
||||||
PuzzlePieceStateRecord, PuzzlePublishRecordInput,
|
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
|
||||||
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
|
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||||
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunRecord,
|
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
|
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
|
||||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
|
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||||
SpacetimeClientError,
|
|
||||||
};
|
};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@@ -68,6 +67,7 @@ use tokio::time::sleep;
|
|||||||
use crate::{
|
use crate::{
|
||||||
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
|
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
|
||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
|
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
|
||||||
auth::AuthenticatedAccessToken,
|
auth::AuthenticatedAccessToken,
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
prompt::puzzle_image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt},
|
prompt::puzzle_image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt},
|
||||||
@@ -441,8 +441,22 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
|
|
||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
let now = current_utc_micros();
|
let now = current_utc_micros();
|
||||||
|
let action = payload.action.trim().to_string();
|
||||||
|
let billed_asset_kind = match action.as_str() {
|
||||||
|
"compile_puzzle_draft" => Some("puzzle_initial_image"),
|
||||||
|
"generate_puzzle_images" => Some("puzzle_generated_image"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let billing_asset_id = format!("{session_id}:{now}");
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
let (operation_type, phase_label, phase_detail, session) = match payload.action.trim() {
|
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
|
||||||
"compile_puzzle_draft" => {
|
"compile_puzzle_draft" => {
|
||||||
let session = compile_puzzle_draft_with_initial_cover(
|
let session = compile_puzzle_draft_with_initial_cover(
|
||||||
&state,
|
&state,
|
||||||
@@ -510,7 +524,7 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
.save_puzzle_generated_images(
|
.save_puzzle_generated_images(
|
||||||
PuzzleGeneratedImagesSaveRecordInput {
|
PuzzleGeneratedImagesSaveRecordInput {
|
||||||
session_id: session.session_id,
|
session_id: session.session_id,
|
||||||
owner_user_id,
|
owner_user_id: owner_user_id.clone(),
|
||||||
candidates_json,
|
candidates_json,
|
||||||
saved_at_micros: now,
|
saved_at_micros: now,
|
||||||
},
|
},
|
||||||
@@ -565,13 +579,18 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
}
|
}
|
||||||
"publish_puzzle_work" => {
|
"publish_puzzle_work" => {
|
||||||
let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id);
|
let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id);
|
||||||
let profile = state
|
consume_asset_operation_points(&state, &owner_user_id, "puzzle_publish_work", &work_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||||
|
})?;
|
||||||
|
let profile_result = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.publish_puzzle_work(PuzzlePublishRecordInput {
|
.publish_puzzle_work(PuzzlePublishRecordInput {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
owner_user_id: owner_user_id.clone(),
|
owner_user_id: owner_user_id.clone(),
|
||||||
// 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。
|
// 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。
|
||||||
work_id,
|
work_id: work_id.clone(),
|
||||||
profile_id,
|
profile_id,
|
||||||
author_display_name: resolve_author_display_name(&state, &authenticated),
|
author_display_name: resolve_author_display_name(&state, &authenticated),
|
||||||
level_name: payload.level_name.clone(),
|
level_name: payload.level_name.clone(),
|
||||||
@@ -579,14 +598,24 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
theme_tags: payload.theme_tags.clone(),
|
theme_tags: payload.theme_tags.clone(),
|
||||||
published_at_micros: now,
|
published_at_micros: now,
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.map_err(|error| {
|
let profile = match profile_result {
|
||||||
puzzle_error_response(
|
Ok(profile) => profile,
|
||||||
|
Err(error) => {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
"puzzle_publish_work",
|
||||||
|
&work_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Err(puzzle_error_response(
|
||||||
&request_context,
|
&request_context,
|
||||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
map_puzzle_client_error(error),
|
map_puzzle_client_error(error),
|
||||||
)
|
));
|
||||||
})?;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let session = state
|
let session = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
@@ -626,6 +655,22 @@ pub async fn execute_puzzle_agent_action(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let session = session.map_err(|error| {
|
let session = session.map_err(|error| {
|
||||||
|
if let Some(asset_kind) = billed_asset_kind {
|
||||||
|
tokio::spawn({
|
||||||
|
let state = state.clone();
|
||||||
|
let owner_user_id = owner_user_id.clone();
|
||||||
|
let billing_asset_id = billing_asset_id.clone();
|
||||||
|
async move {
|
||||||
|
refund_asset_operation_points(
|
||||||
|
&state,
|
||||||
|
&owner_user_id,
|
||||||
|
asset_kind,
|
||||||
|
&billing_asset_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
puzzle_error_response(
|
puzzle_error_response(
|
||||||
&request_context,
|
&request_context,
|
||||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_wallet_adjustment_input_type::RuntimeProfileWalletAdjustmentInput;
|
||||||
|
use super::runtime_profile_wallet_adjustment_procedure_result_type::RuntimeProfileWalletAdjustmentProcedureResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct ConsumeProfileWalletPointsAndReturnArgs {
|
||||||
|
pub input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ConsumeProfileWalletPointsAndReturnArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `consume_profile_wallet_points_and_return`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait consume_profile_wallet_points_and_return {
|
||||||
|
fn consume_profile_wallet_points_and_return(&self, input: RuntimeProfileWalletAdjustmentInput) {
|
||||||
|
self.consume_profile_wallet_points_and_return_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_profile_wallet_points_and_return_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl consume_profile_wallet_points_and_return for super::RemoteProcedures {
|
||||||
|
fn consume_profile_wallet_points_and_return_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp
|
||||||
|
.invoke_procedure_with_callback::<_, RuntimeProfileWalletAdjustmentProcedureResult>(
|
||||||
|
"consume_profile_wallet_points_and_return",
|
||||||
|
ConsumeProfileWalletPointsAndReturnArgs { input },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_wallet_adjustment_input_type::RuntimeProfileWalletAdjustmentInput;
|
||||||
|
use super::runtime_profile_wallet_adjustment_procedure_result_type::RuntimeProfileWalletAdjustmentProcedureResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct RefundProfileWalletPointsAndReturnArgs {
|
||||||
|
pub input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RefundProfileWalletPointsAndReturnArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `refund_profile_wallet_points_and_return`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait refund_profile_wallet_points_and_return {
|
||||||
|
fn refund_profile_wallet_points_and_return(&self, input: RuntimeProfileWalletAdjustmentInput) {
|
||||||
|
self.refund_profile_wallet_points_and_return_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refund_profile_wallet_points_and_return_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl refund_profile_wallet_points_and_return for super::RemoteProcedures {
|
||||||
|
fn refund_profile_wallet_points_and_return_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileWalletAdjustmentInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileWalletAdjustmentProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp
|
||||||
|
.invoke_procedure_with_callback::<_, RuntimeProfileWalletAdjustmentProcedureResult>(
|
||||||
|
"refund_profile_wallet_points_and_return",
|
||||||
|
RefundProfileWalletPointsAndReturnArgs { input },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileWalletAdjustmentInput {
|
||||||
|
pub user_id: String,
|
||||||
|
pub amount: u64,
|
||||||
|
pub ledger_id: String,
|
||||||
|
pub created_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileWalletAdjustmentInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_dashboard_snapshot_type::RuntimeProfileDashboardSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileWalletAdjustmentProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<RuntimeProfileDashboardSnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileWalletAdjustmentProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user