Files
Genarrative/server-rs/crates/shared-contracts/src/admin.rs
T
kdletters 06aa99e3eb 修复后台表查询筛选与分页
使用单次哨兵查询统一计算扫描范围、匹配总数和截断状态
增加完整候选集服务端排序与越界页码钳制
增加固定分页栏、扫描上限提示和后端排序交互
补充大表响应边界文档与前后端回归测试
2026-07-13 13:09:53 +08:00

695 lines
23 KiB
Rust

use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::creation_entry_config::{
CreationEntryEventBannerResponse, PublicWorkInteractionConfigResponse,
UnifiedCreationSpecResponse,
};
// 管理后台协议统一收口在 shared-contracts,避免页面脚本和 Rust handler 各自手拼字段。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminLoginRequest {
pub username: String,
pub password: String,
}
// 登录成功后返回管理员访问令牌与基础会话信息。
/// 后台创作入口开关列表响应。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreationEntryConfigResponse {
pub entries: Vec<AdminCreationEntryTypeConfigPayload>,
/// 底部加号创作入口页的后台公告列表。
pub event_banners: Vec<CreationEntryEventBannerResponse>,
/// 公开作品详情页点赞 / 改造能力配置。
pub public_work_interactions: Vec<PublicWorkInteractionConfigResponse>,
}
/// 后台单个创作入口开关配置。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreationEntryTypeConfigPayload {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub category_id: String,
pub category_label: String,
pub category_sort_order: i32,
pub updated_at_micros: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub unified_creation_spec: Option<UnifiedCreationSpecResponse>,
}
/// 后台保存创作入口开关配置请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertCreationEntryTypeConfigRequest {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub category_id: String,
pub category_label: String,
pub category_sort_order: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub unified_creation_spec: Option<UnifiedCreationSpecResponse>,
}
/// 后台保存创作入口公告表单序列化结果请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertCreationEntryEventBannersRequest {
/// 传输字段沿用既有契约,内容由后台标题 / HTML 表单生成。
pub event_banners_json: String,
}
/// 后台保存公开作品点赞 / 改造能力配置请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertPublicWorkInteractionConfigRequest {
pub public_work_interactions: Vec<PublicWorkInteractionConfigResponse>,
}
/// 后台灰度配置列表响应。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminFeatureGateConfigResponse {
pub gates: Vec<AdminFeatureGateConfigPayload>,
}
/// 后台单个灰度 gate 配置。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminFeatureGateConfigPayload {
pub gate_key: String,
pub enabled: bool,
pub rollout_percent: u32,
pub allow_user_ids: Vec<String>,
pub allow_user_tags: Vec<String>,
pub deny_user_ids: Vec<String>,
pub description: String,
pub updated_at: String,
}
/// 后台保存灰度 gate 配置请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertFeatureGateConfigRequest {
pub gate_key: String,
pub enabled: bool,
pub rollout_percent: u32,
pub allow_user_ids: Vec<String>,
pub allow_user_tags: Vec<String>,
pub deny_user_ids: Vec<String>,
pub description: String,
}
/// 后台作品可见性列表项。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminWorkVisibilityEntryPayload {
pub source_type: String,
pub work_id: String,
pub profile_id: String,
pub source_session_id: Option<String>,
pub public_work_code: String,
pub owner_user_id: String,
pub author_display_name: String,
pub title: String,
pub subtitle: String,
pub cover_image_src: Option<String>,
pub visible: bool,
pub published_at_micros: Option<i64>,
pub updated_at_micros: i64,
}
/// 后台作品可见性列表响应。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminWorkVisibilityListResponse {
pub entries: Vec<AdminWorkVisibilityEntryPayload>,
}
/// 后台修改作品可见性请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpdateWorkVisibilityRequest {
pub source_type: String,
pub profile_id: String,
pub visible: bool,
}
/// 后台修改作品可见性响应。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpdateWorkVisibilityResponse {
pub entry: AdminWorkVisibilityEntryPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetListQuery {
pub cursor: Option<String>,
pub owner_user_id: Option<String>,
pub keyword: Option<String>,
pub created_after: Option<String>,
pub created_before: Option<String>,
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetPayload {
pub asset_id: String,
pub owner_user_id: String,
pub author_display_name: Option<String>,
pub author_public_user_code: Option<String>,
pub folder_id: String,
pub label: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub source_resource_id: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetListResponse {
pub entries: Vec<AdminEditorAssetPayload>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseListQuery {
pub cursor: Option<String>,
pub owner_user_id: Option<String>,
pub review_status: Option<String>,
pub submitted_after: Option<String>,
pub submitted_before: Option<String>,
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseAssetPayload {
pub showcase_id: String,
pub asset_id: String,
pub owner_user_id: String,
pub author_display_name: Option<String>,
pub author_public_user_code: Option<String>,
pub label: String,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub generation_cost_mud_points: u64,
pub refund_mud_points: u64,
pub review_status: String,
pub display_enabled: bool,
pub like_count: u64,
pub asset_deleted_while_pending: bool,
pub reviewed_by_admin_user_id: Option<String>,
pub review_note: Option<String>,
pub refund_ledger_id: Option<String>,
pub refund_completed_at: Option<String>,
pub submitted_at: String,
pub reviewed_at: Option<String>,
pub approved_at: Option<String>,
pub rejected_at: Option<String>,
pub updated_at: String,
pub showcase_category: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseListResponse {
pub entries: Vec<AdminEditorShowcaseAssetPayload>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseReviewRequest {
pub showcase_id: String,
pub review_status: String,
pub review_note: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseDisplayRequest {
pub showcase_id: String,
pub display_enabled: bool,
pub showcase_category: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseAssetResponse {
pub entry: AdminEditorShowcaseAssetPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseCampaignPayload {
pub enabled: bool,
pub title: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub prompt: String,
pub author: String,
pub cost_text: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseCampaignResponse {
pub campaign: Option<AdminEditorShowcaseCampaignPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertEditorShowcaseCampaignRequest {
pub enabled: bool,
pub title: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub prompt: String,
pub author: String,
pub cost_text: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreateEditorShowcaseCampaignImageUploadTicketRequest {
pub file_name: String,
pub content_type: String,
pub content_length: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreateEditorShowcaseCampaignImageUploadTicketResponse {
pub upload: AdminDirectUploadTicketPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDirectUploadTicketPayload {
pub bucket: String,
pub host: String,
pub object_key: String,
pub legacy_public_path: String,
pub form_fields: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminLoginResponse {
pub token: String,
pub admin: AdminSessionPayload,
}
// 管理员会话只暴露页面展示和鉴权调试所需的最小字段。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminSessionPayload {
pub subject: String,
pub username: String,
pub display_name: String,
pub roles: Vec<String>,
pub issued_at: String,
pub expires_at: String,
}
// 页面恢复登录态时读取当前管理员会话。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminMeResponse {
pub admin: AdminSessionPayload,
}
// 后台概览统一返回服务信息与数据库信息两块,前端不再额外拼装。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminOverviewResponse {
pub service: AdminServiceOverviewPayload,
pub database: AdminDatabaseOverviewPayload,
}
/// 后台 dashboard 查询参数;日期均使用北京时间日历日期 YYYY-MM-DD。
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardQuery {
pub granularity: Option<String>,
/// day / week / month 兼容旧查询口径,表示被选中的日期。
pub anchor: Option<String>,
/// period 自定义时段起始日期,闭区间。
pub start_date: Option<String>,
/// period 自定义时段终止日期,闭区间。
pub end_date: Option<String>,
}
/// 后台 dashboard 返回运营指标、趋势图和读取告警。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardResponse {
pub range: AdminDashboardRangePayload,
pub metrics: AdminDashboardMetricsPayload,
pub charts: Vec<AdminDashboardChartPayload>,
pub operations: AdminDashboardOperationsPayload,
pub warnings: Vec<String>,
pub generated_at: String,
}
/// dashboard 当前筛选范围,所有日/周/月图表共用同一时间窗。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardRangePayload {
pub granularity: String,
pub anchor_date: String,
pub period_start_date: String,
pub period_end_date: String,
pub period_label: String,
}
/// dashboard 首屏核心指标。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardMetricsPayload {
pub generated_assets: u64,
pub consumed_mud_points: u64,
pub total_registered_users: u64,
pub new_registered_users: u64,
pub visit_users: u64,
pub total_visit_users: u64,
pub visit_count: u64,
pub total_visit_count: u64,
pub current_users: u64,
}
/// dashboard 图表定义,前端只渲染后端给出的 bucket。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardChartPayload {
pub id: String,
pub title: String,
pub unit: String,
pub total: u64,
pub buckets: Vec<AdminDashboardChartBucketPayload>,
}
/// dashboard 单个趋势桶。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardChartBucketPayload {
pub key: String,
pub label: String,
pub value: u64,
}
/// dashboard 运营页签的常用运营看板。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardOperationsPayload {
pub cards: Vec<AdminDashboardOperationMetricPayload>,
pub asset_kind_breakdown: Vec<AdminDashboardBreakdownRowPayload>,
pub module_visit_breakdown: Vec<AdminDashboardBreakdownRowPayload>,
}
/// dashboard 运营指标卡。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardOperationMetricPayload {
pub id: String,
pub label: String,
pub value: u64,
pub unit: String,
}
/// dashboard 常用排行 / 分布行。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDashboardBreakdownRowPayload {
pub key: String,
pub label: String,
pub value: u64,
}
// 服务概览描述当前 api-server 与 SpacetimeDB 连接配置。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminServiceOverviewPayload {
pub bind_host: String,
pub bind_port: u16,
pub jwt_issuer: String,
pub admin_enabled: bool,
pub spacetime_server_url: String,
pub spacetime_database: String,
}
// 数据库概览返回真实数据库元信息、表清单与统计错误。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseOverviewPayload {
pub database_identity: Option<String>,
pub owner_identity: Option<String>,
pub host_type: Option<String>,
pub schema_table_names: Vec<String>,
pub table_stats: Vec<AdminDatabaseTableStatPayload>,
pub fetch_errors: Vec<String>,
}
// 单表统计允许成功和失败并存,避免某张表失败导致整页概览不可用。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseTableStatPayload {
pub table_name: String,
pub row_count: Option<u64>,
pub error_message: Option<String>,
}
// 后台表清单独立用于“表查询”页,避免页面必须先拉完整总览。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseTableListResponse {
pub tables: Vec<String>,
pub fetch_errors: Vec<String>,
}
// 后台通用表查询参数,用户输入不进入 SQL,只在 API Server 内存中过滤。
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseTableRowsQuery {
pub limit: Option<u32>,
pub page: Option<u32>,
pub search: Option<String>,
pub filters: Option<String>,
pub sort_column: Option<String>,
pub sort_direction: Option<String>,
}
// 后台通用表查询响应,cells 使用列名映射,raw 保留原始行便于详情排障。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseTableRowsResponse {
pub table_name: String,
pub columns: Vec<String>,
pub rows: Vec<AdminDatabaseTableRowPayload>,
pub total_returned: usize,
pub total_matched: usize,
pub limit: u32,
pub page: u32,
pub scanned_count: usize,
pub scan_limit: u32,
pub scan_limit_reached: bool,
}
// 单行查询结果,值统一用 JSON 承载以兼容不同表字段类型。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDatabaseTableRowPayload {
pub cells: Value,
pub raw: Value,
}
// 调试请求只允许同源路径、受控请求头和有限请求体。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDebugHttpRequest {
pub method: String,
pub path: String,
pub headers: Option<Vec<AdminDebugHeaderInput>>,
pub body: Option<String>,
}
// 调试请求头使用显式结构,避免页面直接塞任意对象。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDebugHeaderInput {
pub name: String,
pub value: String,
}
// 调试响应回显状态、响应头与文本/JSON 预览,便于后台排查接口问题。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDebugHttpResponse {
pub status: u16,
pub status_text: String,
pub headers: Vec<AdminDebugHeaderInput>,
pub body_text: String,
pub body_json: Option<Value>,
}
// 后台埋点明细查询参数只保留运营筛选需要的只读字段。
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminTrackingEventListQuery {
pub event_key: Option<String>,
pub user_id: Option<String>,
pub scope_kind: Option<String>,
pub scope_id: Option<String>,
/// 北京时间日历日期 YYYY-MM-DD,按 tracking_event.day_key 闭区间过滤。
pub start_date: Option<String>,
/// 北京时间日历日期 YYYY-MM-DD,按 tracking_event.day_key 闭区间过滤。
pub end_date: Option<String>,
pub limit: Option<u32>,
/// 后台导出专用开关;列表查询仍使用较小上限,避免日常误刷拖慢数据库。
pub export_all: Option<bool>,
}
// 单条埋点原始事件明细,字段与 tracking_event 表一一对应并补充事件名称。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminTrackingEventEntryPayload {
pub event_id: String,
pub event_key: String,
pub event_title: String,
pub scope_kind: String,
pub scope_id: String,
pub day_key: i64,
pub user_id: Option<String>,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub module_key: Option<String>,
pub metadata_json: String,
pub occurred_at: String,
}
// 后台埋点明细列表响应,前端导出 Excel 时直接使用 entries。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminTrackingEventListResponse {
pub entries: Vec<AdminTrackingEventEntryPayload>,
}
// 后台埋点 key 候选来自真实库中已经落库的 tracking_event,静态标题仅用于展示补充。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminTrackingEventKeyPayload {
pub event_key: String,
pub event_title: String,
pub scope_kinds: Vec<String>,
}
// 后台埋点 key 列表响应,任务配置页和埋点明细页共用。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminTrackingEventKeyListResponse {
pub event_keys: Vec<AdminTrackingEventKeyPayload>,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::AdminEditorShowcaseAssetPayload;
#[test]
fn editor_showcase_asset_payload_serializes_author_fields_as_camel_case() {
let payload = AdminEditorShowcaseAssetPayload {
showcase_id: "showcase-1".to_string(),
asset_id: "asset-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: Some("作者昵称".to_string()),
author_public_user_code: Some("SY-00000042".to_string()),
label: "角色形象 1".to_string(),
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: Some("generated-character-drafts/editor/spec.png".to_string()),
width: 1024,
height: 1024,
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: None,
asset_kind: Some("character".to_string()),
generation_inputs: None,
generation_cost_mud_points: 12,
refund_mud_points: 6,
review_status: "pending".to_string(),
display_enabled: false,
like_count: 0,
asset_deleted_while_pending: false,
reviewed_by_admin_user_id: None,
review_note: None,
refund_ledger_id: None,
refund_completed_at: None,
submitted_at: "2026-07-04T10:00:00.000Z".to_string(),
reviewed_at: None,
approved_at: None,
rejected_at: None,
updated_at: "2026-07-04T10:00:00.000Z".to_string(),
showcase_category: None,
};
let value = serde_json::to_value(payload).expect("payload should serialize");
assert_eq!(value["authorDisplayName"], json!("作者昵称"));
assert_eq!(value["authorPublicUserCode"], json!("SY-00000042"));
assert!(value.get("author_display_name").is_none());
assert!(value.get("author_public_user_code").is_none());
}
}