合并最新master并融合图片生成链路

保留BgFilter complex、cross-check、单次重试、全帧并发排空与任务阶段上报
采用master的生成产物一次上传、真实素材类型及成本和供应商归因
同步后台多账号、Dashboard、SpacetimeDB schema、迁移与生成绑定
融合项目决策记录和相关前后端文档
This commit is contained in:
2026-07-15 05:06:12 +00:00
131 changed files with 8041 additions and 1065 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,283 @@
use axum::{
Json,
extract::{Extension, Path, State},
http::StatusCode,
};
use serde_json::Value;
use shared_contracts::admin::{
ADMIN_TAB_PERMISSIONS, AdminAccountListResponse, AdminAccountPayload, AdminAccountRole,
AdminCreateAccountRequest, AdminCreateAccountResponse, AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
};
use spacetime_client::{
AdminAccountCreateRecordInput, AdminAccountRecord, AdminAccountUpdateRecordInput,
SpacetimeClientError,
};
use uuid::Uuid;
use crate::{
admin::{AuthenticatedAdmin, hash_admin_password},
api_response::json_success_body,
http_error::AppError,
request_context::RequestContext,
state::AppState,
};
const ADMIN_USERNAME_MIN_CHARS: usize = 3;
const ADMIN_USERNAME_MAX_CHARS: usize = 64;
const ADMIN_DISPLAY_NAME_MAX_CHARS: usize = 64;
const ADMIN_PASSWORD_MIN_CHARS: usize = 6;
const ADMIN_PASSWORD_MAX_CHARS: usize = 128;
pub async fn admin_list_accounts(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
) -> Result<Json<Value>, AppError> {
let runtime = state.admin_runtime().ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用")
})?;
let mut accounts = vec![AdminAccountPayload {
account_id: runtime.subject().to_string(),
username: runtime.username().to_string(),
display_name: runtime.display_name().to_string(),
account_role: AdminAccountRole::Owner,
tab_permissions: ADMIN_TAB_PERMISSIONS
.iter()
.map(|permission| (*permission).to_string())
.collect(),
enabled: true,
token_version: 1,
created_by: "environment".to_string(),
updated_by: "environment".to_string(),
created_at: String::new(),
updated_at: String::new(),
}];
accounts.extend(
state
.spacetime_client()
.list_admin_accounts()
.await
.map_err(map_admin_account_error)?
.into_iter()
.map(admin_account_payload),
);
Ok(json_success_body(
Some(&request_context),
AdminAccountListResponse { accounts },
))
}
pub async fn admin_create_account(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminCreateAccountRequest>,
) -> Result<Json<Value>, AppError> {
require_owner(&admin)?;
let username = normalize_admin_account_username(&payload.username)?;
let owner_username = state
.admin_runtime()
.map(|runtime| runtime.username().trim().to_ascii_lowercase())
.unwrap_or_default();
if username == owner_username {
return Err(AppError::from_status(StatusCode::CONFLICT).with_message("后台用户名已存在"));
}
let display_name = normalize_display_name(&payload.display_name)?;
validate_password(&payload.password)?;
let tab_permissions = normalize_tab_permissions(payload.tab_permissions)?;
let password_hash = hash_admin_password(&payload.password).await?;
let account = state
.spacetime_client()
.create_admin_account(AdminAccountCreateRecordInput {
account_id: format!("admin-account-{}", Uuid::new_v4()),
username,
display_name,
password_hash,
tab_permissions,
enabled: payload.enabled,
created_by: admin.session().subject.clone(),
})
.await
.map_err(map_admin_account_error)?;
Ok(json_success_body(
Some(&request_context),
AdminCreateAccountResponse {
account: admin_account_payload(account),
},
))
}
pub async fn admin_update_account(
State(state): State<AppState>,
Path(account_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminUpdateAccountRequest>,
) -> Result<Json<Value>, AppError> {
require_owner(&admin)?;
let account_id = account_id.trim().to_string();
if account_id.is_empty() {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("后台账号 ID 不能为空")
);
}
if state
.admin_runtime()
.is_some_and(|runtime| runtime.subject() == account_id.as_str())
{
return Err(
AppError::from_status(StatusCode::FORBIDDEN).with_message("owner 账号不能通过后台修改")
);
}
let display_name = normalize_display_name(&payload.display_name)?;
let tab_permissions = normalize_tab_permissions(payload.tab_permissions)?;
let password_hash = match payload.password {
Some(password) => {
validate_password(&password)?;
Some(hash_admin_password(&password).await?)
}
None => None,
};
let account = state
.spacetime_client()
.update_admin_account(AdminAccountUpdateRecordInput {
account_id,
display_name,
password_hash,
tab_permissions,
enabled: payload.enabled,
updated_by: admin.session().subject.clone(),
})
.await
.map_err(map_admin_account_error)?;
Ok(json_success_body(
Some(&request_context),
AdminUpdateAccountResponse {
account: admin_account_payload(account),
},
))
}
fn require_owner(admin: &AuthenticatedAdmin) -> Result<(), AppError> {
if admin.session().account_role == AdminAccountRole::Owner {
return Ok(());
}
Err(AppError::from_status(StatusCode::FORBIDDEN).with_message("仅 owner 可管理后台账号"))
}
pub(crate) fn normalize_admin_account_username(raw: &str) -> Result<String, AppError> {
let username = raw.trim().to_ascii_lowercase();
let char_count = username.chars().count();
if !(ADMIN_USERNAME_MIN_CHARS..=ADMIN_USERNAME_MAX_CHARS).contains(&char_count)
|| !username
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("后台用户名格式无效")
);
}
Ok(username)
}
fn normalize_display_name(raw: &str) -> Result<String, AppError> {
let display_name = raw.trim().to_string();
if display_name.is_empty() || display_name.chars().count() > ADMIN_DISPLAY_NAME_MAX_CHARS {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("后台显示名称格式无效")
);
}
Ok(display_name)
}
fn validate_password(password: &str) -> Result<(), AppError> {
let char_count = password.chars().count();
if !(ADMIN_PASSWORD_MIN_CHARS..=ADMIN_PASSWORD_MAX_CHARS).contains(&char_count) {
return Err(AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("后台密码长度必须为 6 至 128 个字符"));
}
Ok(())
}
fn normalize_tab_permissions(permissions: Vec<String>) -> Result<Vec<String>, AppError> {
let requested = permissions
.into_iter()
.map(|permission| permission.trim().to_string())
.collect::<std::collections::HashSet<_>>();
if requested
.iter()
.any(|permission| !ADMIN_TAB_PERMISSIONS.contains(&permission.as_str()))
{
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("后台 Tab 权限包含未知值")
);
}
Ok(ADMIN_TAB_PERMISSIONS
.iter()
.filter(|permission| requested.contains(**permission))
.map(|permission| (*permission).to_string())
.collect())
}
fn admin_account_payload(account: AdminAccountRecord) -> AdminAccountPayload {
AdminAccountPayload {
account_id: account.account_id,
username: account.username,
display_name: account.display_name,
account_role: AdminAccountRole::Member,
tab_permissions: account.tab_permissions,
enabled: account.enabled,
token_version: account.token_version,
created_by: account.created_by,
updated_by: account.updated_by,
created_at: account.created_at,
updated_at: account.updated_at,
}
}
fn map_admin_account_error(error: SpacetimeClientError) -> AppError {
let message = error.to_string();
let status = if message.contains("已存在") {
StatusCode::CONFLICT
} else if message.contains("不存在") {
StatusCode::NOT_FOUND
} else if message.contains("不能为空") || message.contains("无效") {
StatusCode::BAD_REQUEST
} else {
StatusCode::BAD_GATEWAY
};
AppError::from_status(status).with_message(message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_permissions_are_validated_deduplicated_and_ordered() {
assert_eq!(
normalize_tab_permissions(vec![
"tracking".to_string(),
"dashboard".to_string(),
"tracking".to_string(),
])
.expect("permissions should normalize"),
["dashboard", "tracking"]
);
assert!(normalize_tab_permissions(vec!["accounts".to_string()]).is_err());
}
#[test]
fn usernames_are_ascii_and_lowercase() {
assert_eq!(
normalize_admin_account_username(" Operator_1 ").unwrap(),
"operator_1"
);
assert!(normalize_admin_account_username("管理员").is_err());
assert!(normalize_admin_account_username(&"a".repeat(65)).is_err());
}
}
@@ -41,7 +41,7 @@ use shared_contracts::admin::{
use spacetime_client::SpacetimeClientError;
use crate::{
admin::AuthenticatedAdmin,
admin::{AdminDisplayNameDirectory, AuthenticatedAdmin, load_admin_display_name_directory},
api_response::json_success_body,
http_error::AppError,
request_context::RequestContext,
@@ -144,6 +144,9 @@ pub async fn admin_get_user_detail(
.await
.map_err(|error| spacetime_error_response(&request_context, error))?;
let summary = map_user_summary(&user);
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| spacetime_error_response(&request_context, error))?;
Ok(json_success_body(
Some(&request_context),
@@ -157,7 +160,7 @@ pub async fn admin_get_user_detail(
binding_status: user.binding_status.as_str().to_string(),
phone_bound: user.phone_number_masked.is_some(),
wechat_bound: user.wechat_bound,
wallet: map_wallet(wallet),
wallet: map_wallet(wallet, Some(&admin_display_names)),
recharge_orders: orders
.into_iter()
.map(|entry| map_order_entry(entry, Some(summary.clone())))
@@ -526,16 +529,18 @@ pub async fn admin_update_wallet_restriction(
AppError::from_status(StatusCode::BAD_REQUEST).with_message(message),
)
})?;
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| spacetime_error_response(&request_context, error))?;
let wallet = state
.spacetime_client()
.admin_upsert_profile_wallet_manual_restriction(input)
.await
.map_err(|error| spacetime_error_response(&request_context, error))?;
Ok(json_success_body(
Some(&request_context),
AdminWalletRestrictionResponse {
wallet: map_wallet(wallet),
wallet: map_wallet(wallet, Some(&admin_display_names)),
},
))
}
@@ -877,7 +882,7 @@ fn map_order_entry(
.map(|value| value.unrecovered_points)
.unwrap_or(0),
recovery_status: settlement.map(|value| value.recovery_status.as_str().to_string()),
wallet: map_wallet(entry.wallet),
wallet: map_wallet(entry.wallet, None),
refunds: entry.refunds.into_iter().map(map_refund).collect(),
active_hold: entry.active_hold.map(map_hold),
remaining_refundable_cents,
@@ -886,7 +891,10 @@ fn map_order_entry(
}
}
fn map_wallet(wallet: RuntimeProfileAdminWalletSnapshot) -> AdminProfileWalletPayload {
fn map_wallet(
wallet: RuntimeProfileAdminWalletSnapshot,
admin_display_names: Option<&AdminDisplayNameDirectory>,
) -> AdminProfileWalletPayload {
AdminProfileWalletPayload {
user_id: wallet.user_id,
total_balance: wallet.total_balance,
@@ -900,12 +908,20 @@ fn map_wallet(wallet: RuntimeProfileAdminWalletSnapshot) -> AdminProfileWalletPa
refund_debt_frozen: wallet.refund_debt_frozen,
wallet_frozen: wallet.wallet_frozen,
manual_restriction: wallet.manual_restriction.map(|value| {
let created_by_admin_display_name = admin_display_names
.map(|directory| directory.resolve(&value.created_by_admin_user_id))
.unwrap_or_else(|| "已停用管理员".to_string());
let updated_by_admin_display_name = admin_display_names
.map(|directory| directory.resolve(&value.updated_by_admin_user_id))
.unwrap_or_else(|| "已停用管理员".to_string());
AdminWalletManualRestrictionPayload {
frozen: value.frozen,
reason: value.reason,
created_by_admin_user_id: value.created_by_admin_user_id,
created_by_admin_display_name,
created_at_micros: value.created_at_micros,
updated_by_admin_user_id: value.updated_by_admin_user_id,
updated_by_admin_display_name,
updated_at_micros: value.updated_at_micros,
}
}),
@@ -114,8 +114,8 @@ const ARK_VIDEO_TASK_POLL_INTERVAL_MS: u64 = 5_000;
const SOURCE_IMAGE_RESOLVE_TIMEOUT_MS: u64 = 30_000;
const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0-fast";
const EDITOR_CHARACTER_ANIMATION_ASSET_KIND: &str = "editor_character_animation";
const EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND: &str = "editor_green_screen_source";
const EDITOR_GREEN_SCREEN_SOURCE_SLOT: &str = "green_screen_source";
const EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND: &str = "character-animation";
const EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT: &str = "provider_source";
const EDITOR_VIDEO_ASSET_KIND: &str = "editor_video";
const EDITOR_VIDEO_ENTITY_KIND: &str = "editor_canvas";
const EDITOR_VIDEO_SLOT: &str = "video_preview";
@@ -740,7 +740,7 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
source_data_url.as_str(),
)
.await?;
persist_editor_generated_media_asset(
let source_record = persist_editor_generated_media_asset(
&state,
PersistEditorGeneratedAssetRequest {
project_id: project_id.clone(),
@@ -760,16 +760,22 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
provider: "Ark".to_string(),
task_id: task_id.clone(),
source_resource_id: source_resource_id.clone(),
asset_kind: Some(EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND.to_string()),
asset_kind: Some(
EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND.to_string(),
),
generation_inputs: generation_inputs.clone(),
thumbnail_src: None,
generation_cost_mud_points: 0,
generation_cost_mud_points: u64::from(normalized.price_mud_points),
},
)
.await?;
if let Some(reporter) = phase_reporter.as_ref() {
reporter.report_processing(&state).await?;
}
let provider_source_resource_id = source_record
.0
.as_ref()
.map(|resource| resource.resource_id.clone());
let persisted_frames = extract_and_persist_editor_character_animation_frames(
&state,
owner_user_id.as_str(),
@@ -814,11 +820,13 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
model: EDITOR_CHARACTER_ANIMATION_MODEL.to_string(),
provider: "Ark".to_string(),
task_id: task_id.clone(),
source_resource_id: None,
asset_kind: Some("character-animation".to_string()),
source_resource_id: provider_source_resource_id,
asset_kind: Some(
EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND.to_string(),
),
generation_inputs: generation_inputs.clone(),
thumbnail_src: Some(first_frame.image_src.clone()),
generation_cost_mud_points: u64::from(normalized.price_mud_points),
generation_cost_mud_points: 0,
},
)
.await?;
@@ -1924,11 +1932,11 @@ async fn put_generated_editor_character_animation_preview(
preview_payload.mime_type,
preview_payload.bytes,
build_asset_metadata(
EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND,
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
owner_user_id,
"editor_layer",
source_layer_id,
EDITOR_GREEN_SCREEN_SOURCE_SLOT,
EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT,
"editor-character-animation",
),
)
@@ -2376,11 +2384,11 @@ async fn process_and_persist_editor_character_animation_frame(
frame.mime_type.clone(),
frame.bytes.clone(),
build_asset_metadata(
EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND,
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
owner_user_id,
"editor_layer",
source_layer_id,
EDITOR_GREEN_SCREEN_SOURCE_SLOT,
EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT,
"editor-character-animation",
),
)
@@ -2406,8 +2414,8 @@ async fn process_and_persist_editor_character_animation_frame(
"editor_animation_frame_background_removed"
);
let finalized = finalize_animation_frame_payload(
removed.bytes.as_slice(),
removed.mime_type.as_str(),
removed.image.bytes.as_slice(),
removed.image.mime_type.as_str(),
frame_width,
frame_height,
false,
@@ -2818,7 +2826,7 @@ async fn confirm_editor_character_animation_source_asset_object(
task_id,
object_key,
content_type,
EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND,
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
)
.await
}
@@ -6089,8 +6097,8 @@ mod tests {
"async fn process_and_persist_editor_character_animation_frame",
"async fn publish_animation_set",
&[
"EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND",
"EDITOR_GREEN_SCREEN_SOURCE_SLOT",
"EDITOR_CHARACTER_ANIMATION_ASSET_KIND",
"EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT",
"green-screen-frame",
"remove_editor_generated_screen_background_with_bgfilter",
"EDITOR_BGFILTER_DEFAULT_SEG_MODEL",
@@ -6155,14 +6163,14 @@ mod tests {
"attach_editor_character_animation_result_metadata",
"project_id: None",
"label: asset_label.clone()",
"asset_kind: Some(\"character-animation\".to_string())",
"generation_cost_mud_points: u64::from(normalized.price_mud_points)",
"EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND",
"generation_cost_mud_points: 0",
],
);
assert_function_contains(
source,
"async fn extract_and_persist_editor_character_animation_frames",
"async fn persist_editor_character_animation_green_screen_source_frames",
"async fn process_and_persist_editor_character_animation_frame",
&[
"confirm_editor_character_animation_frame_asset_object",
"first_frame_object_key",
@@ -6196,7 +6204,8 @@ mod tests {
"pub async fn generate_editor_video",
&[
"or_else(|| Some(\"project\".to_string()))",
"EDITOR_GREEN_SCREEN_SOURCE_ASSET_KIND",
"EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND",
"generation_cost_mud_points: u64::from(normalized.price_mud_points)",
],
);
assert_function_contains_in_order(
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,6 +1,7 @@
#![recursion_limit = "256"]
mod admin;
mod admin_accounts;
mod admin_recharge;
mod ai_generation_drafts;
mod aliyun_matting;
@@ -18,6 +18,7 @@ use crate::{
admin_upsert_feature_gate_config, admin_upsert_public_work_interaction_config,
require_admin_auth,
},
admin_accounts::{admin_create_account, admin_list_accounts, admin_update_account},
admin_recharge::{
admin_execute_recharge_refund, admin_get_user_detail, admin_list_recharge_orders,
admin_preview_recharge_refund, admin_register_recharge_refund,
@@ -37,6 +38,22 @@ use crate::{
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
.route("/admin/api/login", axum::routing::post(admin_login))
.route(
"/admin/api/accounts",
get(admin_list_accounts)
.post(admin_create_account)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/accounts/{account_id}",
axum::routing::put(admin_update_account).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/me",
get(admin_me).route_layer(middleware::from_fn_with_state(
@@ -86,7 +86,7 @@ use std::{convert::Infallible, time::Duration};
use time::OffsetDateTime;
use crate::{
admin::AuthenticatedAdmin,
admin::{AdminDisplayNameDirectory, AuthenticatedAdmin, load_admin_display_name_directory},
api_response::json_success_body,
auth::AuthenticatedAccessToken,
http_error::AppError,
@@ -856,13 +856,21 @@ pub async fn admin_list_profile_task_configs(
map_runtime_profile_client_error(error),
)
})?;
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
ProfileTaskConfigAdminListResponse {
entries: entries
.into_iter()
.map(build_profile_task_config_admin_response)
.map(|entry| build_profile_task_config_admin_response(entry, &admin_display_names))
.collect(),
},
))
@@ -894,6 +902,14 @@ pub async fn admin_upsert_profile_task_config(
.with_message("个人任务 scopeKind 首版仅支持 user"),
));
}
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
@@ -918,10 +934,9 @@ pub async fn admin_upsert_profile_task_config(
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_task_config_admin_response(record),
build_profile_task_config_admin_response(record, &admin_display_names),
))
}
@@ -931,6 +946,14 @@ pub async fn admin_disable_profile_task_config(
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminDisableProfileTaskConfigRequest>,
) -> Result<Json<Value>, Response> {
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
@@ -946,10 +969,9 @@ pub async fn admin_disable_profile_task_config(
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_task_config_admin_response(record),
build_profile_task_config_admin_response(record, &admin_display_names),
))
}
@@ -968,10 +990,18 @@ pub async fn admin_get_profile_wallet_config(
map_runtime_profile_client_error(error),
)
})?;
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_wallet_config_admin_response(record),
build_profile_wallet_config_admin_response(record, &admin_display_names),
))
}
@@ -981,6 +1011,14 @@ pub async fn admin_upsert_profile_wallet_config(
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminUpsertProfileWalletConfigRequest>,
) -> Result<Json<Value>, Response> {
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
@@ -996,10 +1034,9 @@ pub async fn admin_upsert_profile_wallet_config(
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_wallet_config_admin_response(record),
build_profile_wallet_config_admin_response(record, &admin_display_names),
))
}
@@ -1100,6 +1137,14 @@ pub async fn admin_list_profile_redeem_codes(
map_runtime_profile_client_error(error),
)
})?;
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
@@ -1112,7 +1157,9 @@ pub async fn admin_list_profile_redeem_codes(
operations: record
.operations
.into_iter()
.map(build_profile_code_operation_admin_response)
.map(|operation| {
build_profile_code_operation_admin_response(operation, &admin_display_names)
})
.collect(),
},
))
@@ -1209,6 +1256,14 @@ pub async fn admin_list_profile_invite_codes(
map_runtime_profile_client_error(error),
)
})?;
let admin_display_names = load_admin_display_name_directory(&state)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
@@ -1221,7 +1276,9 @@ pub async fn admin_list_profile_invite_codes(
operations: record
.operations
.into_iter()
.map(build_profile_code_operation_admin_response)
.map(|operation| {
build_profile_code_operation_admin_response(operation, &admin_display_names)
})
.collect(),
},
))
@@ -2068,7 +2125,10 @@ fn build_claim_profile_task_reward_response(
fn build_profile_task_config_admin_response(
record: RuntimeProfileTaskConfigRecord,
admin_display_names: &AdminDisplayNameDirectory,
) -> ProfileTaskConfigAdminResponse {
let created_by_display_name = admin_display_names.resolve(&record.created_by);
let updated_by_display_name = admin_display_names.resolve(&record.updated_by);
ProfileTaskConfigAdminResponse {
task_id: record.task_id,
title: record.title,
@@ -2081,8 +2141,10 @@ fn build_profile_task_config_admin_response(
enabled: record.enabled,
sort_order: record.sort_order,
created_by: record.created_by,
created_by_display_name,
created_at: record.created_at,
updated_by: record.updated_by,
updated_by_display_name,
updated_at: record.updated_at,
}
}
@@ -2116,13 +2178,18 @@ fn build_profile_recharge_product_config_admin_response(
fn build_profile_wallet_config_admin_response(
record: module_runtime::RuntimeProfileWalletConfigRecord,
admin_display_names: &AdminDisplayNameDirectory,
) -> ProfileWalletConfigAdminResponse {
let created_by_display_name = admin_display_names.resolve(&record.created_by);
let updated_by_display_name = admin_display_names.resolve(&record.updated_by);
ProfileWalletConfigAdminResponse {
config_id: record.config_id,
initial_mud_points: record.initial_mud_points,
created_by: record.created_by,
created_by_display_name,
created_at: record.created_at,
updated_by: record.updated_by,
updated_by_display_name,
updated_at: record.updated_at,
}
}
@@ -2318,13 +2385,16 @@ fn build_profile_redeem_code_admin_response(
fn build_profile_code_operation_admin_response(
record: RuntimeProfileCodeOperationRecord,
admin_display_names: &AdminDisplayNameDirectory,
) -> ProfileCodeOperationAdminResponse {
let operator_display_name = admin_display_names.resolve(&record.operator_user_id);
ProfileCodeOperationAdminResponse {
operation_id: record.operation_id,
code_kind: record.code_kind,
code: record.code,
action: record.action,
operator_user_id: record.operator_user_id,
operator_display_name,
created_at: record.created_at,
}
}
+85 -8
View File
@@ -308,6 +308,9 @@ pub struct AdminRuntime {
pub struct AdminClaims {
pub subject: String,
pub username: String,
pub display_name: String,
pub roles: Vec<String>,
pub token_version: u64,
pub issued_at: OffsetDateTime,
pub expires_at: OffsetDateTime,
}
@@ -318,6 +321,8 @@ pub struct AdminSession {
pub username: String,
pub display_name: String,
pub roles: Vec<String>,
pub account_role: String,
pub tab_permissions: Vec<String>,
pub issued_at: OffsetDateTime,
pub expires_at: OffsetDateTime,
}
@@ -1733,7 +1738,34 @@ impl AdminRuntime {
&self.password
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn display_name(&self) -> &str {
&self.display_name
}
pub fn build_claims(&self, now: OffsetDateTime) -> Result<AdminClaims, String> {
self.build_account_claims(
self.subject.to_string(),
self.username.to_string(),
self.display_name.to_string(),
vec![ADMIN_ROLE.to_string(), "owner".to_string()],
1,
now,
)
}
pub fn build_account_claims(
&self,
subject: String,
username: String,
display_name: String,
roles: Vec<String>,
token_version: u64,
now: OffsetDateTime,
) -> Result<AdminClaims, String> {
let expires_at = now
.checked_add(time::Duration::seconds(
i64::try_from(self.token_ttl_seconds)
@@ -1741,8 +1773,11 @@ impl AdminRuntime {
))
.ok_or_else(|| "后台 token 过期时间计算溢出".to_string())?;
Ok(AdminClaims {
subject: self.subject.to_string(),
username: self.username.to_string(),
subject,
username,
display_name,
roles,
token_version,
issued_at: now,
expires_at,
})
@@ -1752,13 +1787,13 @@ impl AdminRuntime {
let jwt_claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: claims.subject.clone(),
session_id: format!("admin-session-{}", claims.username),
session_id: format!("admin-session:{}:{}", claims.subject, claims.token_version),
provider: AuthProvider::Password,
roles: vec![ADMIN_ROLE.to_string()],
token_version: 1,
roles: claims.roles.clone(),
token_version: claims.token_version,
phone_verified: false,
binding_status: BindingStatus::Active,
display_name: Some(self.display_name.to_string()),
display_name: Some(claims.display_name.clone()),
},
&self.jwt_config,
claims.issued_at,
@@ -1787,17 +1822,59 @@ impl AdminRuntime {
username: self.username.to_string(),
display_name: self.display_name.to_string(),
roles: claims.roles.clone(),
account_role: "owner".to_string(),
tab_permissions: Vec::new(),
issued_at,
expires_at,
})
}
pub fn validate_account_claims(
&self,
claims: &AccessTokenClaims,
expected_subject: &str,
expected_token_version: u64,
username: String,
display_name: String,
tab_permissions: Vec<String>,
) -> Result<AdminSession, String> {
if claims.user_id() != expected_subject {
return Err("后台管理员主体不匹配".to_string());
}
if !claims.roles.iter().any(|role| role == ADMIN_ROLE) {
return Err("当前令牌不是管理员令牌".to_string());
}
if claims.token_version() != expected_token_version {
return Err("后台登录状态已失效".to_string());
}
let issued_at = OffsetDateTime::from_unix_timestamp(claims.iat as i64)
.map_err(|_| "后台令牌签发时间无效".to_string())?;
let expires_at = OffsetDateTime::from_unix_timestamp(claims.exp as i64)
.map_err(|_| "后台令牌过期时间无效".to_string())?;
Ok(AdminSession {
subject: claims.user_id().to_string(),
username,
display_name,
roles: claims.roles.clone(),
account_role: "member".to_string(),
tab_permissions,
issued_at,
expires_at,
})
}
pub fn is_owner_subject(&self, subject: &str) -> bool {
subject == self.subject.as_ref()
}
pub fn build_session(&self, claims: &AdminClaims) -> AdminSession {
AdminSession {
subject: claims.subject.clone(),
username: claims.username.clone(),
display_name: self.display_name.to_string(),
roles: vec![ADMIN_ROLE.to_string()],
display_name: claims.display_name.clone(),
roles: claims.roles.clone(),
account_role: "owner".to_string(),
tab_permissions: Vec::new(),
issued_at: claims.issued_at,
expires_at: claims.expires_at,
}
@@ -16,6 +16,7 @@ rand_core = { workspace = true, features = ["getrandom"] }
serde = { workspace = true }
shared-kernel = { workspace = true }
time = { workspace = true, features = ["std"] }
tokio = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
+22 -10
View File
@@ -1755,23 +1755,35 @@ pub fn read_refresh_session_token(
}
pub async fn hash_password(password: &str) -> Result<String, PasswordHashError> {
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|error| PasswordHashError::HashFailed(format!("密码哈希失败:{error}")))
let password = password.to_string();
tokio::task::spawn_blocking(move || {
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|error| PasswordHashError::HashFailed(format!("密码哈希失败:{error}")))
})
.await
.map_err(|error| PasswordHashError::HashFailed(format!("密码哈希任务失败:{error}")))?
}
pub async fn verify_password(
password_hash: &str,
password: &str,
) -> Result<bool, PasswordHashError> {
let parsed_hash = PasswordHash::new(password_hash)
.map_err(|error| PasswordHashError::VerifyFailed(format!("密码哈希格式非法:{error}")))?;
let password_hash = password_hash.to_string();
let password = password.to_string();
tokio::task::spawn_blocking(move || {
let parsed_hash = PasswordHash::new(&password_hash).map_err(|error| {
PasswordHashError::VerifyFailed(format!("密码哈希格式非法:{error}"))
})?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
})
.await
.map_err(|error| PasswordHashError::VerifyFailed(format!("密码校验任务失败:{error}")))?
}
pub fn create_refresh_session_token() -> String {
@@ -15,9 +15,10 @@ use super::{
request::{
build_vector_engine_image_edit_request_log_params,
build_vector_engine_image_request_body_with_model,
build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size,
normalize_vector_engine_image_model, vector_engine_images_edit_url,
vector_engine_images_generation_url, vector_engine_nanobanana_generate_content_url,
build_vector_engine_nanobanana_generate_content_request_body,
normalize_image_size_for_model, normalize_vector_engine_image_model,
vector_engine_images_edit_url, vector_engine_images_generation_url,
vector_engine_nanobanana_generate_content_url,
},
response::handle_vector_engine_response,
types::{GeneratedImages, ReferenceImage, VectorEngineImageSettings},
@@ -79,7 +80,7 @@ pub async fn create_vector_engine_image_generation_with_model(
}
let request_url = vector_engine_images_generation_url(settings);
let normalized_size = normalize_image_size(size);
let normalized_size = normalize_image_size_for_model(model, size);
let request_body = build_vector_engine_image_request_body_with_model(
model,
prompt,
@@ -387,7 +388,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model(
}
let request_url = vector_engine_images_edit_url(settings);
let normalized_size = normalize_image_size(size);
let normalized_size = normalize_image_size_for_model(model, size);
let request_params = build_vector_engine_image_edit_request_log_params(
model,
prompt,
@@ -23,7 +23,7 @@ pub use error::{PlatformImageError, PlatformImageStatusHint};
pub use image_source::download_remote_image;
pub use request::{
build_vector_engine_image_request_body, build_vector_engine_image_request_body_with_model,
build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size,
build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size_for_model,
vector_engine_images_edit_url, vector_engine_images_generation_url,
vector_engine_nanobanana_generate_content_url,
};
@@ -40,7 +40,7 @@ pub fn build_vector_engine_image_request_body_with_model(
("n".to_string(), json!(candidate_count.clamp(1, 4))),
(
"size".to_string(),
Value::String(normalize_image_size(size)),
Value::String(normalize_image_size_for_model(model, size)),
),
]);
@@ -90,9 +90,9 @@ pub fn normalize_vector_engine_image_model(model: &str) -> &str {
}
}
pub fn normalize_image_size(size: &str) -> String {
pub fn normalize_image_size_for_model(model: &str, size: &str) -> String {
let size = size.trim();
match size {
let normalized_size = match size {
"1:1" => "1024x1024".to_string(),
"16:9" | "2k" => "1536x1024".to_string(),
"2k-16:9" => "2048x1152".to_string(),
@@ -100,6 +100,12 @@ pub fn normalize_image_size(size: &str) -> String {
value if is_explicit_pixel_size(value) => normalize_explicit_pixel_size(value),
value if !value.is_empty() => value.to_string(),
_ => "1024x1024".to_string(),
};
if normalize_vector_engine_image_model(model) == GPT_IMAGE_2_MODEL {
clamp_gpt_image_2_pixel_size(normalized_size.as_str())
} else {
normalized_size
}
}
@@ -115,6 +121,94 @@ fn normalize_explicit_pixel_size(value: &str) -> String {
value.replace('*', "x")
}
fn clamp_gpt_image_2_pixel_size(size: &str) -> String {
const MIN_PIXELS: u64 = 655_360;
const MAX_PIXELS: u64 = 8_294_400;
const MAX_EDGE: u32 = 3_840;
const DIMENSION_ALIGNMENT: u32 = 16;
const MAX_ASPECT_RATIO: f64 = 3.0;
// 中文注释:这里是 VectorEngine 的共享发送边界,只处理 gpt-image-2 的显式像素尺寸。
let Some((width, height)) = parse_explicit_pixel_size(size) else {
return size.to_string();
};
// 中文注释:零尺寸无法计算比例或像素数,直接使用已知合法的默认尺寸。
if width == 0 || height == 0 {
return "1024x1024".to_string();
}
let (mut width, mut height) = (f64::from(width), f64::from(height));
// 中文注释:先把短边补至长边的三分之一,确保长短边比不超过 3:1。
if width / height > MAX_ASPECT_RATIO {
height = width / MAX_ASPECT_RATIO;
} else if height / width > MAX_ASPECT_RATIO {
width = height / MAX_ASPECT_RATIO;
}
let pixels = width * height;
// 中文注释:等比缩小,优先保留已收紧后的画面比例,同时满足最大边和最大总像素。
let scale = (MAX_EDGE as f64 / width.max(height))
.min((MAX_PIXELS as f64 / pixels).sqrt())
.min(1.0);
width *= scale;
height *= scale;
let pixels = width * height;
// 中文注释:小于最小总像素时等比放大;此前已处理比例,放大不会重新突破 3:1。
if pixels < MIN_PIXELS as f64 {
let scale = (MIN_PIXELS as f64 / pixels).sqrt();
width *= scale;
height *= scale;
}
// 中文注释:provider 要求两边均为 16px 倍数,向上取整避免对齐后落到最小像素以下。
let width = align_dimension_up(width, DIMENSION_ALIGNMENT);
let height = align_dimension_up(height, DIMENSION_ALIGNMENT);
// TODO unlikely but can improve
// 中文注释:对齐可能触碰最大边或最大像素,因此发送前重新核验全部约束。
if is_valid_gpt_image_2_size(
width,
height,
MIN_PIXELS,
MAX_PIXELS,
MAX_EDGE,
MAX_ASPECT_RATIO,
) {
return format!("{width}x{height}");
}
// 中文注释:无法同时满足全部约束时,回退为已知会被 provider 接受的默认尺寸。
"1024x1024".to_string()
}
fn align_dimension_up(value: f64, alignment: u32) -> u32 {
((value.ceil() as u32).saturating_add(alignment - 1) / alignment) * alignment
}
fn is_valid_gpt_image_2_size(
width: u32,
height: u32,
min_pixels: u64,
max_pixels: u64,
max_edge: u32,
max_aspect_ratio: f64,
) -> bool {
let pixels = u64::from(width) * u64::from(height);
width > 0
&& height > 0
&& width <= max_edge
&& height <= max_edge
&& width.is_multiple_of(16)
&& height.is_multiple_of(16)
&& (min_pixels..=max_pixels).contains(&pixels)
&& f64::from(width.max(height)) / f64::from(width.min(height)) <= max_aspect_ratio
}
fn parse_explicit_pixel_size(value: &str) -> Option<(u32, u32)> {
let (width, height) = value.split_once('x')?;
Some((width.parse().ok()?, height.parse().ok()?))
}
fn normalize_nanobanana_aspect_ratio(aspect_ratio: &str) -> &str {
match aspect_ratio.trim() {
"2:3" => "2:3",
@@ -47,12 +47,12 @@ fn vector_engine_module_exposes_provider_protocol_helpers() {
}
#[test]
fn vector_engine_keeps_explicit_publication_material_pixel_sizes() {
fn vector_engine_clamps_gpt_image_2_explicit_pixel_sizes_to_its_supported_pixel_budget() {
let cover = build_vector_engine_image_request_body("宣发首图", None, "720x540", 1, &[]);
let detail = build_vector_engine_image_request_body("详情单图", None, "720x1280", 1, &[]);
let poster = build_vector_engine_image_request_body("运营海报", None, "1280x720", 1, &[]);
assert_eq!(cover["size"], "720x540");
assert_eq!(cover["size"], "944x704");
assert_eq!(detail["size"], "720x1280");
assert_eq!(poster["size"], "1280x720");
}
@@ -82,6 +82,65 @@ fn vector_engine_request_body_can_use_nanobanana2_model() {
assert_eq!(body["n"], 1);
}
#[test]
fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() {
let gpt_body = build_vector_engine_image_request_body_with_model(
GPT_IMAGE_2_MODEL,
"小尺寸图",
None,
"640x640",
1,
&[],
);
let nanobanana_body = build_vector_engine_image_request_body_with_model(
"gemini-3.1-flash-image-preview",
"小尺寸图",
None,
"640x640",
1,
&[],
);
let oversized_gpt_body = build_vector_engine_image_request_body_with_model(
GPT_IMAGE_2_MODEL,
"大尺寸图",
None,
"4096x4096",
1,
&[],
);
assert_eq!(gpt_body["size"], "816x816");
assert_eq!(nanobanana_body["size"], "640x640");
assert_eq!(oversized_gpt_body["size"], "2880x2880");
}
#[test]
fn vector_engine_gpt_image_2_sizes_always_meet_the_full_provider_envelope() {
for size in [
"1x1",
"720x540",
"3841x1280",
"4096x4096",
"3200x400",
"16x4096",
"3840x3840",
] {
let body = build_vector_engine_image_request_body("约束测试", None, size, 1, &[]);
let normalized = body["size"].as_str().expect("size should be a string");
let (width, height) = normalized
.split_once('x')
.expect("gpt-image-2 size should be explicit pixels");
let width = width.parse::<u32>().expect("width should be numeric");
let height = height.parse::<u32>().expect("height should be numeric");
let pixels = u64::from(width) * u64::from(height);
assert!(width <= 3_840 && height <= 3_840, "{size} -> {normalized}");
assert!(width.is_multiple_of(16) && height.is_multiple_of(16));
assert!((655_360..=8_294_400).contains(&pixels));
assert!(width.max(height) <= width.min(height) * 3);
}
}
#[test]
fn vector_engine_request_body_can_use_nanobanana2_half_k() {
let body = build_vector_engine_image_request_body_with_model(
@@ -8,6 +8,35 @@ use crate::creation_entry_config::{
UnifiedCreationSpecResponse,
};
/// 后台 member 可被授予的一级 Tab 权限;账号管理仅 owner 可见,不进入该集合。
pub const ADMIN_TAB_PERMISSIONS: [&str; 18] = [
"dashboard",
"overview",
"tables",
"debug",
"tracking",
"gray-release",
"redeem",
"invite",
"profile-wallet",
"tasks",
"recharge-products",
"recharge-orders",
"editor-generation-pricing",
"editor-showcase",
"editor-assets",
"creation-announcement",
"creation-entry",
"work-visibility",
];
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AdminAccountRole {
Owner,
Member,
}
// 管理后台协议统一收口在 shared-contracts,避免页面脚本和 Rust handler 各自手拼字段。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
@@ -197,6 +226,10 @@ pub struct AdminEditorAssetPayload {
pub generation_cost_mud_points: u64,
pub created_at: String,
pub updated_at: String,
pub generator: String,
pub task_generator: String,
pub task_cost_mud_points: u64,
pub children: Vec<AdminEditorAssetPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -358,10 +391,67 @@ pub struct AdminSessionPayload {
pub username: String,
pub display_name: String,
pub roles: Vec<String>,
pub account_role: AdminAccountRole,
pub tab_permissions: Vec<String>,
pub issued_at: String,
pub expires_at: String,
}
/// 后台账号列表和写操作响应均复用该安全视图,不返回密码摘要。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminAccountPayload {
pub account_id: String,
pub username: String,
pub display_name: String,
pub account_role: AdminAccountRole,
pub tab_permissions: Vec<String>,
pub enabled: bool,
pub token_version: u64,
pub created_by: String,
pub updated_by: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminAccountListResponse {
pub accounts: Vec<AdminAccountPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreateAccountRequest {
pub username: String,
pub display_name: String,
pub password: String,
pub tab_permissions: Vec<String>,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreateAccountResponse {
pub account: AdminAccountPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpdateAccountRequest {
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
pub tab_permissions: Vec<String>,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpdateAccountResponse {
pub account: AdminAccountPayload,
}
// 页面恢复登录态时读取当前管理员会话。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
@@ -421,6 +511,8 @@ pub struct AdminDashboardMetricsPayload {
pub consumed_mud_points: u64,
pub total_registered_users: u64,
pub new_registered_users: u64,
pub day1_retention: AdminDashboardRetentionPayload,
pub day7_retention: AdminDashboardRetentionPayload,
pub visit_users: u64,
pub total_visit_users: u64,
pub visit_count: u64,
@@ -428,6 +520,15 @@ pub struct AdminDashboardMetricsPayload {
pub current_users: u64,
}
/// dashboard 新增用户留存;rate_basis_points 取值 0..=10000,1 表示 0.01%。
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardRetentionPayload {
pub eligible_users: u64,
pub retained_users: u64,
pub rate_basis_points: u32,
}
/// dashboard 图表定义,前端只渲染后端给出的 bucket。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
@@ -670,8 +771,10 @@ pub struct AdminWalletManualRestrictionPayload {
pub frozen: bool,
pub reason: String,
pub created_by_admin_user_id: String,
pub created_by_admin_display_name: String,
pub created_at_micros: i64,
pub updated_by_admin_user_id: String,
pub updated_by_admin_display_name: String,
pub updated_at_micros: i64,
}
@@ -533,8 +533,10 @@ pub struct ProfileTaskConfigAdminResponse {
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
pub created_by_display_name: String,
pub created_at: String,
pub updated_by: String,
pub updated_by_display_name: String,
pub updated_at: String,
}
@@ -581,8 +583,10 @@ pub struct ProfileWalletConfigAdminResponse {
pub config_id: String,
pub initial_mud_points: u64,
pub created_by: String,
pub created_by_display_name: String,
pub created_at: String,
pub updated_by: String,
pub updated_by_display_name: String,
pub updated_at: String,
}
@@ -731,6 +735,7 @@ pub struct ProfileCodeOperationAdminResponse {
pub code: String,
pub action: String,
pub operator_user_id: String,
pub operator_display_name: String,
pub created_at: String,
}
@@ -39,8 +39,10 @@ fn profile_task_config_admin_response_serializes_scope_kind_as_user() {
enabled: true,
sort_order: 10,
created_by: "admin".to_string(),
created_by_display_name: "管理员".to_string(),
created_at: "2026-05-04T00:00:00Z".to_string(),
updated_by: "admin".to_string(),
updated_by_display_name: "管理员".to_string(),
updated_at: "2026-05-04T00:00:00Z".to_string(),
};
@@ -0,0 +1,106 @@
use super::*;
impl SpacetimeClient {
pub async fn get_admin_account_by_username(
&self,
username: String,
) -> Result<AdminAccountCredentialRecord, SpacetimeClientError> {
let input = AdminAccountGetByUsernameInput { username };
self.call_after_connect(
"get_admin_account_by_username_and_return",
move |connection, sender| {
connection
.procedures()
.get_admin_account_by_username_and_return_then(input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_account_credential_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn get_admin_account_by_id(
&self,
account_id: String,
) -> Result<AdminAccountRecord, SpacetimeClientError> {
let input = AdminAccountGetByIdInput { account_id };
self.call_after_connect(
"get_admin_account_by_id_and_return",
move |connection, sender| {
connection
.procedures()
.get_admin_account_by_id_and_return_then(input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_account_single_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn list_admin_accounts(
&self,
) -> Result<Vec<AdminAccountRecord>, SpacetimeClientError> {
self.call_after_connect(
"list_admin_accounts_and_return",
move |connection, sender| {
connection
.procedures()
.list_admin_accounts_and_return_then(move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_account_list_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn create_admin_account(
&self,
input: AdminAccountCreateRecordInput,
) -> Result<AdminAccountRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"create_admin_account_and_return",
move |connection, sender| {
connection
.procedures()
.create_admin_account_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_account_single_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn update_admin_account(
&self,
input: AdminAccountUpdateRecordInput,
) -> Result<AdminAccountRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_admin_account_and_return",
move |connection, sender| {
connection
.procedures()
.update_admin_account_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_account_single_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
}
@@ -0,0 +1,27 @@
use super::*;
impl SpacetimeClient {
pub async fn get_admin_dashboard_stats(
&self,
input: AdminDashboardStatsRecordInput,
) -> Result<AdminDashboardStatsRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_admin_dashboard_stats_and_return",
move |connection, sender| {
connection
.procedures()
.get_admin_dashboard_stats_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_dashboard_stats_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
}
@@ -6,6 +6,9 @@ mod mapper;
mod telemetry;
use mapper::*;
pub use mapper::{
AdminAccountCreateRecordInput, AdminAccountCredentialRecord, AdminAccountRecord,
AdminAccountUpdateRecordInput, AdminDashboardBreakdownRecord, AdminDashboardDailyCountRecord,
AdminDashboardRetentionRecord, AdminDashboardStatsRecord, AdminDashboardStatsRecordInput,
AdminEditorAssetListRecordInput, AdminEditorAssetRecord, AdminWorkVisibilityRecord,
AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord,
AiTextChunkRecord, BarkBattleDraftConfigRecord, BarkBattleRunRecord,
@@ -131,6 +134,8 @@ pub use mapper::{
WoodenFishWorkSummaryResponse, WoodenFishWorkspaceCreateRequest,
};
pub mod admin_account;
pub mod admin_dashboard;
pub mod ai;
pub mod assets;
pub mod auth;
@@ -1,5 +1,7 @@
use super::*;
mod admin_account;
mod admin_dashboard;
mod ai;
mod assets;
mod auth;
@@ -27,6 +29,14 @@ mod story;
mod visual_novel;
mod wooden_fish;
pub use self::admin_account::{
AdminAccountCreateRecordInput, AdminAccountCredentialRecord, AdminAccountRecord,
AdminAccountUpdateRecordInput,
};
pub use self::admin_dashboard::{
AdminDashboardBreakdownRecord, AdminDashboardDailyCountRecord, AdminDashboardRetentionRecord,
AdminDashboardStatsRecord, AdminDashboardStatsRecordInput,
};
pub use self::ai::{
AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord,
AiTextChunkRecord,
@@ -189,6 +199,11 @@ pub use self::wooden_fish::{
WoodenFishWorkSummaryResponse, WoodenFishWorkspaceCreateRequest,
};
pub(crate) use self::admin_account::{
map_admin_account_credential_procedure_result, map_admin_account_list_procedure_result,
map_admin_account_single_procedure_result,
};
pub(crate) use self::admin_dashboard::map_admin_dashboard_stats_procedure_result;
pub(crate) use self::ai::map_ai_task_procedure_result;
pub(crate) use self::assets::{
map_asset_read_access_procedure_result, map_entity_binding_procedure_result,
@@ -0,0 +1,191 @@
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminAccountRecord {
pub account_id: String,
pub username: String,
pub display_name: String,
pub tab_permissions: Vec<String>,
pub enabled: bool,
pub token_version: u64,
pub created_by: String,
pub updated_by: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminAccountCredentialRecord {
pub account: AdminAccountRecord,
pub password_hash: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminAccountCreateRecordInput {
pub account_id: String,
pub username: String,
pub display_name: String,
pub password_hash: String,
pub tab_permissions: Vec<String>,
pub enabled: bool,
pub created_by: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminAccountUpdateRecordInput {
pub account_id: String,
pub display_name: String,
pub password_hash: Option<String>,
pub tab_permissions: Vec<String>,
pub enabled: bool,
pub updated_by: String,
}
impl From<AdminAccountCreateRecordInput> for crate::module_bindings::AdminAccountCreateInput {
fn from(input: AdminAccountCreateRecordInput) -> Self {
Self {
account_id: input.account_id,
username: input.username,
display_name: input.display_name,
password_hash: input.password_hash,
tab_permissions_json: serde_json::to_string(&input.tab_permissions)
.unwrap_or_else(|_| "[]".to_string()),
enabled: input.enabled,
created_by: input.created_by,
}
}
}
impl From<AdminAccountUpdateRecordInput> for crate::module_bindings::AdminAccountUpdateInput {
fn from(input: AdminAccountUpdateRecordInput) -> Self {
Self {
account_id: input.account_id,
display_name: input.display_name,
password_hash: input.password_hash,
tab_permissions_json: serde_json::to_string(&input.tab_permissions)
.unwrap_or_else(|_| "[]".to_string()),
enabled: input.enabled,
updated_by: input.updated_by,
}
}
}
pub(crate) fn map_admin_account_single_procedure_result(
result: AdminAccountProcedureResult,
) -> Result<AdminAccountRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.account
.map(map_admin_account_snapshot)
.transpose()?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("后台账号快照"))
}
pub(crate) fn map_admin_account_credential_procedure_result(
result: AdminAccountCredentialProcedureResult,
) -> Result<AdminAccountCredentialRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.account
.map(map_admin_account_credential_snapshot)
.transpose()?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("后台账号凭据快照"))
}
pub(crate) fn map_admin_account_list_procedure_result(
result: AdminAccountProcedureResult,
) -> Result<Vec<AdminAccountRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.accounts
.into_iter()
.map(map_admin_account_snapshot)
.collect()
}
fn map_admin_account_snapshot(
snapshot: AdminAccountSnapshot,
) -> Result<AdminAccountRecord, SpacetimeClientError> {
Ok(AdminAccountRecord {
account_id: snapshot.account_id,
username: snapshot.username,
display_name: snapshot.display_name,
tab_permissions: serde_json::from_str::<Vec<String>>(
snapshot.tab_permissions_json.as_str(),
)
.map_err(SpacetimeClientError::validation_failed)?,
enabled: snapshot.enabled,
token_version: snapshot.token_version,
created_by: snapshot.created_by,
updated_by: snapshot.updated_by,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn map_admin_account_credential_snapshot(
snapshot: AdminAccountCredentialSnapshot,
) -> Result<AdminAccountCredentialRecord, SpacetimeClientError> {
Ok(AdminAccountCredentialRecord {
account: map_admin_account_snapshot(snapshot.account)?,
password_hash: snapshot.password_hash,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_public_account_without_password_hash() {
let record = map_admin_account_snapshot(AdminAccountSnapshot {
account_id: "member-1".to_string(),
username: "operator".to_string(),
display_name: "运营".to_string(),
tab_permissions_json: r#"["dashboard","tracking"]"#.to_string(),
enabled: false,
token_version: 3,
created_by: "owner".to_string(),
updated_by: "owner".to_string(),
created_at_micros: 0,
updated_at_micros: 0,
})
.expect("snapshot should map");
assert_eq!(record.tab_permissions, ["dashboard", "tracking"]);
assert!(!record.enabled);
assert_eq!(record.token_version, 3);
}
#[test]
fn maps_login_credential_separately_from_public_account() {
let record = map_admin_account_credential_snapshot(AdminAccountCredentialSnapshot {
account: AdminAccountSnapshot {
account_id: "member-1".to_string(),
username: "operator".to_string(),
display_name: "运营".to_string(),
tab_permissions_json: r#"["dashboard"]"#.to_string(),
enabled: true,
token_version: 3,
created_by: "owner".to_string(),
updated_by: "owner".to_string(),
created_at_micros: 0,
updated_at_micros: 0,
},
password_hash: "$argon2id$hash".to_string(),
})
.expect("credential snapshot should map");
assert_eq!(record.account.account_id, "member-1");
assert_eq!(record.password_hash, "$argon2id$hash");
}
}
@@ -0,0 +1,185 @@
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminDashboardStatsRecordInput {
pub start_day_key: i64,
pub end_day_key: i64,
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminDashboardDailyCountRecord {
pub day_key: i64,
pub count: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdminDashboardBreakdownRecord {
pub key: String,
pub count: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdminDashboardRetentionRecord {
pub eligible_users: u64,
pub retained_users: u64,
pub rate_basis_points: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdminDashboardStatsRecord {
pub total_registered_users: u64,
pub new_registered_users: Vec<AdminDashboardDailyCountRecord>,
pub visit_count: Vec<AdminDashboardDailyCountRecord>,
pub visit_users_by_day: Vec<AdminDashboardDailyCountRecord>,
pub visit_users: u64,
pub total_visit_count: u64,
pub total_visit_users: u64,
pub current_users: u64,
pub day1_retention: AdminDashboardRetentionRecord,
pub day7_retention: AdminDashboardRetentionRecord,
pub module_visit_breakdown: Vec<AdminDashboardBreakdownRecord>,
}
impl From<AdminDashboardStatsRecordInput> for AdminDashboardStatsInput {
fn from(input: AdminDashboardStatsRecordInput) -> Self {
Self {
start_day_key: input.start_day_key,
end_day_key: input.end_day_key,
now_micros: input.now_micros,
}
}
}
pub(crate) fn map_admin_dashboard_stats_procedure_result(
result: AdminDashboardStatsProcedureResult,
) -> Result<AdminDashboardStatsRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.snapshot
.map(map_admin_dashboard_stats_snapshot)
.ok_or_else(|| SpacetimeClientError::missing_snapshot("后台 Dashboard 统计快照"))
}
fn map_admin_dashboard_stats_snapshot(
snapshot: AdminDashboardStatsSnapshot,
) -> AdminDashboardStatsRecord {
AdminDashboardStatsRecord {
total_registered_users: snapshot.total_registered_users,
new_registered_users: snapshot
.new_registered_users
.into_iter()
.map(map_admin_dashboard_daily_count)
.collect(),
visit_count: snapshot
.visit_count
.into_iter()
.map(map_admin_dashboard_daily_count)
.collect(),
visit_users_by_day: snapshot
.visit_users_by_day
.into_iter()
.map(map_admin_dashboard_daily_count)
.collect(),
visit_users: snapshot.visit_users,
total_visit_count: snapshot.total_visit_count,
total_visit_users: snapshot.total_visit_users,
current_users: snapshot.current_users,
day1_retention: map_admin_dashboard_retention(snapshot.day_1_retention),
day7_retention: map_admin_dashboard_retention(snapshot.day_7_retention),
module_visit_breakdown: snapshot
.module_visit_breakdown
.into_iter()
.map(|entry| AdminDashboardBreakdownRecord {
key: entry.key,
count: entry.count,
})
.collect(),
}
}
fn map_admin_dashboard_daily_count(
entry: AdminDashboardDailyCountSnapshot,
) -> AdminDashboardDailyCountRecord {
AdminDashboardDailyCountRecord {
day_key: entry.day_key,
count: entry.count,
}
}
fn map_admin_dashboard_retention(
retention: AdminDashboardRetentionSnapshot,
) -> AdminDashboardRetentionRecord {
AdminDashboardRetentionRecord {
eligible_users: retention.eligible_users,
retained_users: retention.retained_users,
rate_basis_points: retention.rate_basis_points,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_admin_dashboard_stats_procedure_snapshot() {
let record =
map_admin_dashboard_stats_procedure_result(AdminDashboardStatsProcedureResult {
ok: true,
snapshot: Some(AdminDashboardStatsSnapshot {
total_registered_users: 12,
new_registered_users: vec![AdminDashboardDailyCountSnapshot {
day_key: 100,
count: 3,
}],
visit_count: vec![AdminDashboardDailyCountSnapshot {
day_key: 100,
count: 20,
}],
visit_users_by_day: vec![AdminDashboardDailyCountSnapshot {
day_key: 100,
count: 8,
}],
visit_users: 8,
total_visit_count: 99,
total_visit_users: 11,
current_users: 2,
day_1_retention: AdminDashboardRetentionSnapshot {
eligible_users: 4,
retained_users: 2,
rate_basis_points: 5_000,
},
day_7_retention: AdminDashboardRetentionSnapshot {
eligible_users: 3,
retained_users: 1,
rate_basis_points: 3_333,
},
module_visit_breakdown: vec![AdminDashboardBreakdownSnapshot {
key: "profile".to_string(),
count: 7,
}],
}),
error_message: None,
})
.expect("dashboard stats should map");
assert_eq!(record.total_visit_count, 99);
assert_eq!(record.day1_retention.rate_basis_points, 5_000);
assert_eq!(record.module_visit_breakdown[0].key, "profile");
}
#[test]
fn rejects_failed_admin_dashboard_stats_procedure_result() {
let error =
map_admin_dashboard_stats_procedure_result(AdminDashboardStatsProcedureResult {
ok: false,
snapshot: None,
error_message: Some("统计失败".to_string()),
})
.expect_err("failed result should stay failed");
assert!(error.to_string().contains("统计失败"));
}
}
@@ -10,6 +10,22 @@ pub mod accept_quest_reducer;
pub mod acknowledge_external_generation_job_summaries_and_return_procedure;
pub mod acknowledge_external_generation_jobs_and_return_procedure;
pub mod acknowledge_quest_completion_reducer;
pub mod admin_account_create_input_type;
pub mod admin_account_credential_procedure_result_type;
pub mod admin_account_credential_snapshot_type;
pub mod admin_account_get_by_id_input_type;
pub mod admin_account_get_by_username_input_type;
pub mod admin_account_procedure_result_type;
pub mod admin_account_snapshot_type;
pub mod admin_account_table;
pub mod admin_account_type;
pub mod admin_account_update_input_type;
pub mod admin_dashboard_breakdown_snapshot_type;
pub mod admin_dashboard_daily_count_snapshot_type;
pub mod admin_dashboard_retention_snapshot_type;
pub mod admin_dashboard_stats_input_type;
pub mod admin_dashboard_stats_procedure_result_type;
pub mod admin_dashboard_stats_snapshot_type;
pub mod admin_disable_profile_redeem_code_procedure;
pub mod admin_disable_profile_task_config_procedure;
pub mod admin_editor_asset_list_input_type;
@@ -253,6 +269,7 @@ pub mod consume_inventory_item_input_type;
pub mod consume_profile_wallet_points_and_return_procedure;
pub mod continue_story_and_return_procedure;
pub mod continue_story_reducer;
pub mod create_admin_account_and_return_procedure;
pub mod create_ai_task_and_return_procedure;
pub mod create_ai_task_reducer;
pub mod create_bark_battle_draft_procedure;
@@ -523,6 +540,9 @@ pub mod finish_match_3_d_time_up_procedure;
pub mod finish_square_hole_time_up_procedure;
pub mod finish_wooden_fish_run_procedure;
pub mod generate_big_fish_asset_procedure;
pub mod get_admin_account_by_id_and_return_procedure;
pub mod get_admin_account_by_username_and_return_procedure;
pub mod get_admin_dashboard_stats_and_return_procedure;
pub mod get_asset_object_by_id_and_return_procedure;
pub mod get_asset_object_by_location_and_return_procedure;
pub mod get_asset_read_access_by_location_and_return_procedure;
@@ -655,6 +675,7 @@ pub mod jump_hop_work_snapshot_type;
pub mod jump_hop_work_update_input_type;
pub mod jump_hop_works_list_input_type;
pub mod jump_hop_works_procedure_result_type;
pub mod list_admin_accounts_and_return_procedure;
pub mod list_asset_history_and_return_procedure;
pub mod list_big_fish_works_procedure;
pub mod list_custom_world_gallery_entries_procedure;
@@ -1291,6 +1312,7 @@ pub mod turn_in_quest_reducer;
pub mod unequip_inventory_item_input_type;
pub mod unpublish_custom_world_profile_and_return_procedure;
pub mod unpublish_custom_world_profile_reducer;
pub mod update_admin_account_and_return_procedure;
pub mod update_bark_battle_draft_config_procedure;
pub mod update_editor_asset_and_return_procedure;
pub mod update_editor_asset_folder_and_return_procedure;
@@ -1413,6 +1435,22 @@ pub use accept_quest_reducer::accept_quest;
pub use acknowledge_external_generation_job_summaries_and_return_procedure::acknowledge_external_generation_job_summaries_and_return;
pub use acknowledge_external_generation_jobs_and_return_procedure::acknowledge_external_generation_jobs_and_return;
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
pub use admin_account_create_input_type::AdminAccountCreateInput;
pub use admin_account_credential_procedure_result_type::AdminAccountCredentialProcedureResult;
pub use admin_account_credential_snapshot_type::AdminAccountCredentialSnapshot;
pub use admin_account_get_by_id_input_type::AdminAccountGetByIdInput;
pub use admin_account_get_by_username_input_type::AdminAccountGetByUsernameInput;
pub use admin_account_procedure_result_type::AdminAccountProcedureResult;
pub use admin_account_snapshot_type::AdminAccountSnapshot;
pub use admin_account_table::*;
pub use admin_account_type::AdminAccount;
pub use admin_account_update_input_type::AdminAccountUpdateInput;
pub use admin_dashboard_breakdown_snapshot_type::AdminDashboardBreakdownSnapshot;
pub use admin_dashboard_daily_count_snapshot_type::AdminDashboardDailyCountSnapshot;
pub use admin_dashboard_retention_snapshot_type::AdminDashboardRetentionSnapshot;
pub use admin_dashboard_stats_input_type::AdminDashboardStatsInput;
pub use admin_dashboard_stats_procedure_result_type::AdminDashboardStatsProcedureResult;
pub use admin_dashboard_stats_snapshot_type::AdminDashboardStatsSnapshot;
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
pub use admin_disable_profile_task_config_procedure::admin_disable_profile_task_config;
pub use admin_editor_asset_list_input_type::AdminEditorAssetListInput;
@@ -1656,6 +1694,7 @@ pub use consume_inventory_item_input_type::ConsumeInventoryItemInput;
pub use consume_profile_wallet_points_and_return_procedure::consume_profile_wallet_points_and_return;
pub use continue_story_and_return_procedure::continue_story_and_return;
pub use continue_story_reducer::continue_story;
pub use create_admin_account_and_return_procedure::create_admin_account_and_return;
pub use create_ai_task_and_return_procedure::create_ai_task_and_return;
pub use create_ai_task_reducer::create_ai_task;
pub use create_bark_battle_draft_procedure::create_bark_battle_draft;
@@ -1926,6 +1965,9 @@ pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up;
pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up;
pub use finish_wooden_fish_run_procedure::finish_wooden_fish_run;
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
pub use get_admin_account_by_id_and_return_procedure::get_admin_account_by_id_and_return;
pub use get_admin_account_by_username_and_return_procedure::get_admin_account_by_username_and_return;
pub use get_admin_dashboard_stats_and_return_procedure::get_admin_dashboard_stats_and_return;
pub use get_asset_object_by_id_and_return_procedure::get_asset_object_by_id_and_return;
pub use get_asset_object_by_location_and_return_procedure::get_asset_object_by_location_and_return;
pub use get_asset_read_access_by_location_and_return_procedure::get_asset_read_access_by_location_and_return;
@@ -2058,6 +2100,7 @@ pub use jump_hop_work_snapshot_type::JumpHopWorkSnapshot;
pub use jump_hop_work_update_input_type::JumpHopWorkUpdateInput;
pub use jump_hop_works_list_input_type::JumpHopWorksListInput;
pub use jump_hop_works_procedure_result_type::JumpHopWorksProcedureResult;
pub use list_admin_accounts_and_return_procedure::list_admin_accounts_and_return;
pub use list_asset_history_and_return_procedure::list_asset_history_and_return;
pub use list_big_fish_works_procedure::list_big_fish_works;
pub use list_custom_world_gallery_entries_procedure::list_custom_world_gallery_entries;
@@ -2694,6 +2737,7 @@ pub use turn_in_quest_reducer::turn_in_quest;
pub use unequip_inventory_item_input_type::UnequipInventoryItemInput;
pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return;
pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile;
pub use update_admin_account_and_return_procedure::update_admin_account_and_return;
pub use update_bark_battle_draft_config_procedure::update_bark_battle_draft_config;
pub use update_editor_asset_and_return_procedure::update_editor_asset_and_return;
pub use update_editor_asset_folder_and_return_procedure::update_editor_asset_folder_and_return;
@@ -3095,6 +3139,7 @@ impl __sdk::Reducer for Reducer {
#[allow(non_snake_case)]
#[doc(hidden)]
pub struct DbUpdate {
admin_account: __sdk::TableUpdate<AdminAccount>,
ai_result_reference: __sdk::TableUpdate<AiResultReference>,
ai_task: __sdk::TableUpdate<AiTask>,
ai_task_event: __sdk::TableUpdate<AiTaskEvent>,
@@ -3254,6 +3299,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
let mut db_update = DbUpdate::default();
for table_update in __sdk::transaction_update_iter_table_updates(raw) {
match &table_update.table_name[..] {
"admin_account" => db_update
.admin_account
.append(admin_account_table::parse_table_update(table_update)?),
"ai_result_reference" => db_update
.ai_result_reference
.append(ai_result_reference_table::parse_table_update(table_update)?),
@@ -3768,6 +3816,9 @@ impl __sdk::DbUpdate for DbUpdate {
) -> AppliedDiff<'_> {
let mut diff = AppliedDiff::default();
diff.admin_account = cache
.apply_diff_to_table::<AdminAccount>("admin_account", &self.admin_account)
.with_updates_by_pk(|row| &row.account_id);
diff.ai_result_reference = cache
.apply_diff_to_table::<AiResultReference>(
"ai_result_reference",
@@ -4523,6 +4574,9 @@ impl __sdk::DbUpdate for DbUpdate {
let mut db_update = DbUpdate::default();
for table_rows in raw.tables {
match &table_rows.table[..] {
"admin_account" => db_update
.admin_account
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"ai_result_reference" => db_update
.ai_result_reference
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -4974,6 +5028,9 @@ impl __sdk::DbUpdate for DbUpdate {
let mut db_update = DbUpdate::default();
for table_rows in raw.tables {
match &table_rows.table[..] {
"admin_account" => db_update
.admin_account
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"ai_result_reference" => db_update
.ai_result_reference
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -5427,6 +5484,7 @@ impl __sdk::DbUpdate for DbUpdate {
#[allow(non_snake_case)]
#[doc(hidden)]
pub struct AppliedDiff<'r> {
admin_account: __sdk::TableAppliedDiff<'r, AdminAccount>,
ai_result_reference: __sdk::TableAppliedDiff<'r, AiResultReference>,
ai_task: __sdk::TableAppliedDiff<'r, AiTask>,
ai_task_event: __sdk::TableAppliedDiff<'r, AiTaskEvent>,
@@ -5595,6 +5653,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
event: &EventContext,
callbacks: &mut __sdk::DbCallbacks<RemoteModule>,
) {
callbacks.invoke_table_row_callbacks::<AdminAccount>(
"admin_account",
&self.admin_account,
event,
);
callbacks.invoke_table_row_callbacks::<AiResultReference>(
"ai_result_reference",
&self.ai_result_reference,
@@ -6965,6 +7028,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
type QueryBuilder = __sdk::QueryBuilder;
fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {
admin_account_table::register_table(client_cache);
ai_result_reference_table::register_table(client_cache);
ai_task_table::register_table(client_cache);
ai_task_event_table::register_table(client_cache);
@@ -7113,6 +7177,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
wooden_fish_work_profile_table::register_table(client_cache);
}
const ALL_TABLE_NAMES: &'static [&'static str] = &[
"admin_account",
"ai_result_reference",
"ai_task",
"ai_task_event",

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