调整精选后端公开契约
新增精选活动卡图片 objectKey 存储字段和迁移兼容。 新增后台活动卡图片 OSS 直传凭证接口。 移除素材包精选分类约束,允许未设置分类素材进入全部精选。 同步 admin 和前台读取活动卡 objectKey 的类型契约。
This commit is contained in:
@@ -457,6 +457,7 @@ export interface AdminEditorShowcaseCampaignPayload {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
imageObjectKey?: string | null;
|
||||
prompt: string;
|
||||
author: string;
|
||||
costText: string;
|
||||
@@ -471,11 +472,37 @@ export interface AdminUpsertEditorShowcaseCampaignRequest {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
imageObjectKey?: string | null;
|
||||
prompt: string;
|
||||
author: string;
|
||||
costText: string;
|
||||
}
|
||||
|
||||
export interface AdminDirectUploadTicketPayload {
|
||||
bucket: string;
|
||||
host: string;
|
||||
objectKey: string;
|
||||
legacyPublicPath: string;
|
||||
contentType?: string | null;
|
||||
formFields: Record<string, string | null | undefined>;
|
||||
}
|
||||
|
||||
export interface AdminCreateEditorShowcaseCampaignImageUploadTicketRequest {
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
contentLength: number;
|
||||
}
|
||||
|
||||
export interface AdminCreateEditorShowcaseCampaignImageUploadTicketResponse {
|
||||
upload: AdminDirectUploadTicketPayload;
|
||||
}
|
||||
|
||||
export interface AdminUploadedEditorShowcaseCampaignImage {
|
||||
imageSrc: string;
|
||||
imageObjectKey: string;
|
||||
legacyPublicPath: string;
|
||||
}
|
||||
|
||||
export interface AdminUpsertProfileRedeemCodeRequest {
|
||||
code: string;
|
||||
mode: ProfileRedeemCodeMode;
|
||||
|
||||
@@ -18,6 +18,9 @@ use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use shared_contracts::admin::{
|
||||
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
||||
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
|
||||
AdminDirectUploadTicketPayload,
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminDashboardBreakdownRowPayload, AdminDashboardChartBucketPayload,
|
||||
AdminDashboardChartPayload, AdminDashboardMetricsPayload, AdminDashboardOperationMetricPayload,
|
||||
@@ -38,6 +41,9 @@ use shared_contracts::admin::{
|
||||
AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertPublicWorkInteractionConfigRequest,
|
||||
AdminWorkVisibilityListResponse,
|
||||
};
|
||||
use shared_contracts::assets::{
|
||||
CreateDirectUploadTicketRequest, DirectUploadObjectAccess,
|
||||
};
|
||||
use shared_contracts::creation_entry_config::{
|
||||
encode_unified_creation_spec_response, validate_unified_creation_spec_for_play,
|
||||
};
|
||||
@@ -52,6 +58,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
assets::create_direct_upload_ticket_for_owner,
|
||||
editor_generation_config::EditorGenerationPricingConfig,
|
||||
editor_project::current_utc_micros,
|
||||
http_error::AppError,
|
||||
@@ -83,6 +90,7 @@ const ADMIN_DASHBOARD_MAX_PERIOD_DAYS: i64 = 366;
|
||||
const ADMIN_EDITOR_ASSET_DEFAULT_LIMIT: u32 = 80;
|
||||
const ADMIN_EDITOR_ASSET_MAX_LIMIT: u32 = 200;
|
||||
const ADMIN_DASHBOARD_ACTIVE_WINDOW_MICROS: i64 = 5 * 60 * 1_000_000;
|
||||
const ADMIN_EDITOR_SHOWCASE_CAMPAIGN_IMAGE_MAX_BYTES: u64 = 20 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthenticatedAdmin {
|
||||
@@ -578,6 +586,7 @@ pub async fn admin_upsert_editor_showcase_campaign(
|
||||
enabled: payload.enabled,
|
||||
title: payload.title,
|
||||
image_src: payload.image_src,
|
||||
image_object_key: payload.image_object_key,
|
||||
prompt: payload.prompt,
|
||||
author: payload.author,
|
||||
cost_text: payload.cost_text,
|
||||
@@ -592,6 +601,79 @@ pub async fn admin_upsert_editor_showcase_campaign(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_create_editor_showcase_campaign_image_upload_ticket(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminCreateEditorShowcaseCampaignImageUploadTicketRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let file_name = payload.file_name.trim();
|
||||
if file_name.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件名不能为空")
|
||||
);
|
||||
}
|
||||
let content_type = payload.content_type.trim();
|
||||
if !content_type.starts_with("image/") {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("活动卡只能上传图片")
|
||||
);
|
||||
}
|
||||
if payload.content_length == 0 {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件不能为空")
|
||||
);
|
||||
}
|
||||
if payload.content_length > ADMIN_EDITOR_SHOWCASE_CAMPAIGN_IMAGE_MAX_BYTES {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_message("活动卡图片不能超过 20MB")
|
||||
);
|
||||
}
|
||||
|
||||
let ticket = create_direct_upload_ticket_for_owner(
|
||||
&state,
|
||||
&request_context,
|
||||
admin.session().subject.as_str(),
|
||||
CreateDirectUploadTicketRequest {
|
||||
legacy_prefix: "generated-character-drafts".to_string(),
|
||||
path_segments: vec![
|
||||
"editor".to_string(),
|
||||
"showcase-campaign".to_string(),
|
||||
current_utc_micros().to_string(),
|
||||
],
|
||||
file_name: file_name.to_string(),
|
||||
content_type: Some(content_type.to_string()),
|
||||
access: Some(DirectUploadObjectAccess::Private),
|
||||
metadata: BTreeMap::from([
|
||||
(
|
||||
"asset_kind".to_string(),
|
||||
"editor_showcase_campaign_image".to_string(),
|
||||
),
|
||||
("source".to_string(), "admin_editor_showcase_campaign".to_string()),
|
||||
]),
|
||||
max_size_bytes: Some(payload.content_length),
|
||||
expire_seconds: None,
|
||||
success_action_status: Some(200),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let axum::Json(value) = ticket;
|
||||
let payload_value = value.get("data").cloned().unwrap_or(value);
|
||||
let response: shared_contracts::assets::CreateDirectUploadTicketResponse =
|
||||
serde_json::from_value(payload_value).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(serde_json::json!({
|
||||
"message": format!("活动卡上传凭证响应解析失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse {
|
||||
upload: admin_direct_upload_ticket_payload_from_asset_upload(response.upload),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn build_admin_creation_entry_config_response(
|
||||
config: shared_contracts::creation_entry_config::CreationEntryConfigResponse,
|
||||
) -> AdminCreationEntryConfigResponse {
|
||||
@@ -716,6 +798,7 @@ fn admin_editor_showcase_campaign_payload_from_record(
|
||||
enabled: record.enabled,
|
||||
title: record.title,
|
||||
image_src: record.image_src,
|
||||
image_object_key: record.image_object_key,
|
||||
prompt: record.prompt,
|
||||
author: record.author,
|
||||
cost_text: record.cost_text,
|
||||
@@ -723,6 +806,47 @@ fn admin_editor_showcase_campaign_payload_from_record(
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_direct_upload_ticket_payload_from_asset_upload(
|
||||
upload: shared_contracts::assets::DirectUploadTicketPayload,
|
||||
) -> AdminDirectUploadTicketPayload {
|
||||
let mut form_fields = BTreeMap::from([
|
||||
("key".to_string(), upload.form_fields.key),
|
||||
("policy".to_string(), upload.form_fields.policy),
|
||||
(
|
||||
"x-oss-signature-version".to_string(),
|
||||
upload.form_fields.signature_version,
|
||||
),
|
||||
(
|
||||
"x-oss-credential".to_string(),
|
||||
upload.form_fields.credential,
|
||||
),
|
||||
("x-oss-date".to_string(), upload.form_fields.date),
|
||||
(
|
||||
"x-oss-signature".to_string(),
|
||||
upload.form_fields.signature,
|
||||
),
|
||||
(
|
||||
"success_action_status".to_string(),
|
||||
upload.form_fields.success_action_status,
|
||||
),
|
||||
]);
|
||||
if let Some(content_type) = upload.form_fields.content_type {
|
||||
form_fields.insert("Content-Type".to_string(), content_type);
|
||||
}
|
||||
if let Some(cache_control) = upload.form_fields.cache_control {
|
||||
form_fields.insert("Cache-Control".to_string(), cache_control);
|
||||
}
|
||||
form_fields.extend(upload.form_fields.metadata);
|
||||
|
||||
AdminDirectUploadTicketPayload {
|
||||
bucket: upload.bucket,
|
||||
host: upload.host,
|
||||
object_key: upload.object_key,
|
||||
legacy_public_path: upload.legacy_public_path,
|
||||
form_fields,
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_showcase_admin_cursor(record: &EditorShowcaseAssetRecord) -> Option<String> {
|
||||
let submitted_at_micros = parse_timestamp_text_to_micros(record.submitted_at.as_str())?;
|
||||
Some(format!("{submitted_at_micros}:{}", record.showcase_id))
|
||||
|
||||
@@ -617,6 +617,7 @@ pub struct EditorShowcaseCampaignPayload {
|
||||
enabled: bool,
|
||||
title: String,
|
||||
image_src: String,
|
||||
image_object_key: Option<String>,
|
||||
prompt: String,
|
||||
author: String,
|
||||
cost_text: String,
|
||||
@@ -3350,6 +3351,7 @@ fn editor_showcase_campaign_payload_from_record(
|
||||
enabled: record.enabled,
|
||||
title: record.title,
|
||||
image_src: record.image_src,
|
||||
image_object_key: record.image_object_key,
|
||||
prompt: record.prompt,
|
||||
author: record.author,
|
||||
cost_text: record.cost_text,
|
||||
|
||||
@@ -6,6 +6,7 @@ use axum::{
|
||||
use crate::{
|
||||
admin::{
|
||||
admin_dashboard, admin_debug_http, admin_get_creation_entry_config,
|
||||
admin_create_editor_showcase_campaign_image_upload_ticket,
|
||||
admin_get_editor_generation_pricing, admin_get_editor_showcase_campaign,
|
||||
admin_list_database_table_rows, admin_list_database_tables,
|
||||
admin_list_editor_assets, admin_list_editor_showcase_assets, admin_list_tracking_event_keys,
|
||||
@@ -152,6 +153,12 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/editor-showcase/campaign/image-upload-ticket",
|
||||
post(admin_create_editor_showcase_campaign_image_upload_ticket).route_layer(
|
||||
middleware::from_fn_with_state(state.clone(), require_admin_auth),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/works/visibility",
|
||||
get(admin_list_work_visibility)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -254,6 +256,7 @@ pub struct AdminEditorShowcaseCampaignPayload {
|
||||
pub enabled: bool,
|
||||
pub title: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: Option<String>,
|
||||
pub prompt: String,
|
||||
pub author: String,
|
||||
pub cost_text: String,
|
||||
@@ -272,11 +275,36 @@ pub struct AdminUpsertEditorShowcaseCampaignRequest {
|
||||
pub enabled: bool,
|
||||
pub title: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: Option<String>,
|
||||
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 {
|
||||
|
||||
@@ -180,6 +180,7 @@ pub struct EditorShowcaseCampaignConfigRecord {
|
||||
pub enabled: bool,
|
||||
pub title: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: Option<String>,
|
||||
pub prompt: String,
|
||||
pub author: String,
|
||||
pub cost_text: String,
|
||||
@@ -426,6 +427,7 @@ pub struct EditorShowcaseCampaignConfigUpsertRecordInput {
|
||||
pub author: String,
|
||||
pub cost_text: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
impl From<EditorProjectCreateRecordInput> for crate::module_bindings::EditorProjectCreateInput {
|
||||
@@ -783,6 +785,7 @@ impl From<EditorShowcaseCampaignConfigUpsertRecordInput>
|
||||
author: input.author,
|
||||
cost_text: input.cost_text,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
image_object_key: input.image_object_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1214,6 +1217,7 @@ fn map_editor_showcase_campaign_config_snapshot(
|
||||
enabled: snapshot.enabled,
|
||||
title: snapshot.title,
|
||||
image_src: snapshot.image_src,
|
||||
image_object_key: snapshot.image_object_key,
|
||||
prompt: snapshot.prompt,
|
||||
author: snapshot.author,
|
||||
cost_text: snapshot.cost_text,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ pub struct EditorShowcaseCampaignConfigSnapshot {
|
||||
pub cost_text: String,
|
||||
pub updated_by_admin_user_id: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorShowcaseCampaignConfigSnapshot {
|
||||
|
||||
+3
@@ -16,6 +16,7 @@ pub struct EditorShowcaseCampaignConfig {
|
||||
pub cost_text: String,
|
||||
pub updated_by_admin_user_id: Option<String>,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorShowcaseCampaignConfig {
|
||||
@@ -36,6 +37,7 @@ pub struct EditorShowcaseCampaignConfigCols {
|
||||
pub updated_by_admin_user_id:
|
||||
__sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<String>>,
|
||||
pub updated_at: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, __sdk::Timestamp>,
|
||||
pub image_object_key: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig {
|
||||
@@ -54,6 +56,7 @@ impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig {
|
||||
"updated_by_admin_user_id",
|
||||
),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
image_object_key: __sdk::__query_builder::Col::new(table_name, "image_object_key"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ pub struct EditorShowcaseCampaignConfigUpsertInput {
|
||||
pub author: String,
|
||||
pub cost_text: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorShowcaseCampaignConfigUpsertInput {
|
||||
|
||||
@@ -13,7 +13,7 @@ const EDITOR_SHOWCASE_STATUS_PENDING: &str = "pending";
|
||||
const EDITOR_SHOWCASE_STATUS_APPROVED: &str = "approved";
|
||||
const EDITOR_SHOWCASE_STATUS_REJECTED: &str = "rejected";
|
||||
const EDITOR_SHOWCASE_CAMPAIGN_CONFIG_ID: &str = "global";
|
||||
const EDITOR_SHOWCASE_CATEGORIES: [&str; 5] = ["packs", "characters", "ui", "music", "marketing"];
|
||||
const EDITOR_SHOWCASE_CATEGORIES: [&str; 4] = ["characters", "ui", "music", "marketing"];
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = editor_project,
|
||||
@@ -205,6 +205,8 @@ pub struct EditorShowcaseCampaignConfig {
|
||||
cost_text: String,
|
||||
updated_by_admin_user_id: Option<String>,
|
||||
updated_at: Timestamp,
|
||||
#[default(None::<String>)]
|
||||
image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
@@ -463,6 +465,7 @@ pub struct EditorShowcaseCampaignConfigSnapshot {
|
||||
pub cost_text: String,
|
||||
pub updated_by_admin_user_id: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
@@ -617,6 +620,7 @@ pub struct EditorShowcaseCampaignConfigUpsertInput {
|
||||
pub author: String,
|
||||
pub cost_text: String,
|
||||
pub updated_at_micros: i64,
|
||||
pub image_object_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
@@ -1931,10 +1935,6 @@ fn list_public_editor_showcase_assets(
|
||||
&& asset.display_enabled
|
||||
&& !asset.asset_deleted_while_pending
|
||||
&& (asset.refund_mud_points == 0 || asset.refund_completed_at.is_some())
|
||||
&& asset
|
||||
.showcase_category
|
||||
.as_deref()
|
||||
.is_some_and(is_valid_showcase_category)
|
||||
&& !asset.image_src.trim().is_empty()
|
||||
&& is_after_public_showcase_cursor(
|
||||
approved_at_micros,
|
||||
@@ -2077,14 +2077,11 @@ fn update_editor_showcase_asset_display(
|
||||
if asset.asset_deleted_while_pending {
|
||||
return Err("素材已删除,不能公开展示".to_string());
|
||||
}
|
||||
let showcase_category = input
|
||||
.showcase_category
|
||||
.map(|value| normalize_showcase_category(value.as_str()))
|
||||
.transpose()?
|
||||
.or_else(|| asset.showcase_category.clone());
|
||||
if input.display_enabled && showcase_category.is_none() {
|
||||
return Err("公开展示前必须先设置精选分类".to_string());
|
||||
}
|
||||
let showcase_category = match input.showcase_category {
|
||||
Some(value) if value.trim().is_empty() => None,
|
||||
Some(value) => Some(normalize_showcase_category(value.as_str())?),
|
||||
None => asset.showcase_category.clone(),
|
||||
};
|
||||
if input.display_enabled && asset.refund_mud_points > 0 && asset.refund_completed_at.is_none() {
|
||||
return Err("精选返还完成前不能公开展示".to_string());
|
||||
}
|
||||
@@ -2203,7 +2200,9 @@ fn upsert_editor_showcase_campaign_config(
|
||||
"editor_showcase_campaign_config.updated_by_admin_user_id",
|
||||
)?;
|
||||
let title = input.title.trim().to_string();
|
||||
let image_src = input.image_src.trim().to_string();
|
||||
let image_object_key = normalize_optional(input.image_object_key)
|
||||
.map(|value| value.trim_start_matches('/').to_string());
|
||||
let image_src = normalize_campaign_image_src(input.image_src.as_str(), image_object_key.as_deref());
|
||||
if input.enabled {
|
||||
if title.is_empty() {
|
||||
return Err("editor_showcase_campaign_config.title 不能为空".to_string());
|
||||
@@ -2238,6 +2237,7 @@ fn upsert_editor_showcase_campaign_config(
|
||||
cost_text: input.cost_text.trim().to_string(),
|
||||
updated_by_admin_user_id: Some(admin_user_id),
|
||||
updated_at: now,
|
||||
image_object_key,
|
||||
});
|
||||
ctx.db
|
||||
.editor_showcase_campaign_config()
|
||||
@@ -2614,7 +2614,7 @@ fn normalize_showcase_category(value: &str) -> Result<String, String> {
|
||||
if is_valid_showcase_category(normalized) {
|
||||
return Ok(normalized.to_string());
|
||||
}
|
||||
Err("精选分类只支持 packs、characters、ui、music 或 marketing".to_string())
|
||||
Err("精选分类只支持 characters、ui、music 或 marketing".to_string())
|
||||
}
|
||||
|
||||
fn is_valid_showcase_category(value: &str) -> bool {
|
||||
@@ -2712,9 +2712,22 @@ fn showcase_campaign_config_snapshot_from_row(
|
||||
cost_text: row.cost_text,
|
||||
updated_by_admin_user_id: row.updated_by_admin_user_id,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
image_object_key: row.image_object_key,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_campaign_image_src(raw_image_src: &str, image_object_key: Option<&str>) -> String {
|
||||
let image_src = raw_image_src.trim();
|
||||
if !image_src.is_empty() {
|
||||
return image_src.to_string();
|
||||
}
|
||||
image_object_key
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!("/{}", value.trim_start_matches('/')))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn timestamp_option_micros(value: Option<Timestamp>) -> Option<i64> {
|
||||
value.map(|timestamp| timestamp.to_micros_since_unix_epoch())
|
||||
}
|
||||
|
||||
@@ -1265,6 +1265,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "editor_showcase_campaign_config" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:活动卡图片 Object Key 晚于活动卡表加入,旧迁移包按仅有图片地址兼容。
|
||||
object
|
||||
.entry("image_object_key".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "big_fish_creation_session" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。
|
||||
|
||||
@@ -138,6 +138,7 @@ export type EditorShowcaseCampaignSnapshot = {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
imageObjectKey?: string | null;
|
||||
prompt: string;
|
||||
author: string;
|
||||
costText: string;
|
||||
|
||||
Reference in New Issue
Block a user