合并最新master并融合图片生成链路
保留BgFilter complex、cross-check、单次重试、全帧并发排空与任务阶段上报 采用master的生成产物一次上传、真实素材类型及成本和供应商归因 同步后台多账号、Dashboard、SpacetimeDB schema、迁移与生成绑定 融合项目决策记录和相关前后端文档
This commit is contained in:
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,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user