Merge master into user play stats branch
Some checks failed
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-04-28 15:52:27 +08:00
80 changed files with 3609 additions and 434 deletions

View File

@@ -161,6 +161,48 @@ impl From<module_runtime::RuntimeProfileRechargeOrderCreateInput>
}
}
impl From<module_runtime::RuntimeProfileRewardCodeRedeemInput>
for RuntimeProfileRewardCodeRedeemInput
{
fn from(input: module_runtime::RuntimeProfileRewardCodeRedeemInput) -> Self {
Self {
user_id: input.user_id,
code: input.code,
redeemed_at_micros: input.redeemed_at_micros,
}
}
}
impl From<module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput>
for RuntimeProfileRedeemCodeAdminUpsertInput
{
fn from(input: module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput) -> Self {
Self {
admin_user_id: input.admin_user_id,
code: input.code,
mode: map_runtime_profile_redeem_code_mode(input.mode),
reward_points: input.reward_points,
max_uses: input.max_uses,
enabled: input.enabled,
allowed_user_ids: input.allowed_user_ids,
allowed_public_user_codes: input.allowed_public_user_codes,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeProfileRedeemCodeAdminDisableInput>
for RuntimeProfileRedeemCodeAdminDisableInput
{
fn from(input: module_runtime::RuntimeProfileRedeemCodeAdminDisableInput) -> Self {
Self {
admin_user_id: input.admin_user_id,
code: input.code,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeReferralInviteCenterGetInput>
for RuntimeReferralInviteCenterGetInput
{
@@ -802,6 +844,48 @@ pub(crate) fn map_runtime_referral_redeem_procedure_result(
))
}
pub(crate) fn map_runtime_profile_reward_code_redeem_procedure_result(
result: RuntimeProfileRewardCodeRedeemProcedureResult,
) -> Result<RuntimeProfileRewardCodeRedeemRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 reward redeem 快照".to_string(),
)
})?;
Ok(build_runtime_profile_reward_code_redeem_record(
map_runtime_profile_reward_code_redeem_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
result: RuntimeProfileRedeemCodeAdminProcedureResult,
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 redeem code 快照".to_string())
})?;
Ok(build_runtime_profile_redeem_code_record(
map_runtime_profile_redeem_code_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_profile_play_stats_procedure_result(
result: RuntimeProfilePlayStatsProcedureResult,
) -> Result<RuntimeProfilePlayStatsRecord, SpacetimeClientError> {
@@ -1278,6 +1362,7 @@ pub(crate) fn map_big_fish_session_procedure_result(
pub(crate) fn map_big_fish_works_procedure_result(
result: BigFishWorksProcedureResult,
fallback_owner_user_id: Option<&str>,
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
@@ -1292,9 +1377,15 @@ pub(crate) fn map_big_fish_works_procedure_result(
"SpacetimeDB procedure 未返回 big fish works 快照".to_string(),
)
})?;
serde_json::from_str::<Vec<BigFishWorkSummaryRecord>>(&items_json).map_err(|error| {
SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}"))
})
let items = serde_json::from_str::<Vec<CompatibleBigFishWorkSummaryRecord>>(&items_json)
.map_err(|error| {
SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}"))
})?;
Ok(items
.into_iter()
.map(|item| item.into_record(fallback_owner_user_id))
.collect())
}
pub(crate) fn map_story_session_procedure_result(
@@ -1666,6 +1757,33 @@ pub(crate) fn map_runtime_referral_redeem_snapshot(
}
}
pub(crate) fn map_runtime_profile_reward_code_redeem_snapshot(
snapshot: RuntimeProfileRewardCodeRedeemSnapshot,
) -> module_runtime::RuntimeProfileRewardCodeRedeemSnapshot {
module_runtime::RuntimeProfileRewardCodeRedeemSnapshot {
wallet_balance: snapshot.wallet_balance,
amount_granted: snapshot.amount_granted,
ledger_entry: map_runtime_profile_wallet_ledger_entry_snapshot(snapshot.ledger_entry),
}
}
pub(crate) fn map_runtime_profile_redeem_code_snapshot(
snapshot: RuntimeProfileRedeemCodeSnapshot,
) -> module_runtime::RuntimeProfileRedeemCodeSnapshot {
module_runtime::RuntimeProfileRedeemCodeSnapshot {
code: snapshot.code,
mode: map_runtime_profile_redeem_code_mode_back(snapshot.mode),
reward_points: snapshot.reward_points,
max_uses: snapshot.max_uses,
global_used_count: snapshot.global_used_count,
enabled: snapshot.enabled,
allowed_user_ids: snapshot.allowed_user_ids,
created_by: snapshot.created_by,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_profile_played_world_snapshot(
snapshot: RuntimeProfilePlayedWorldSnapshot,
) -> module_runtime::RuntimeProfilePlayedWorldSnapshot {
@@ -3277,6 +3395,41 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => {
module_runtime::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund
}
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward => {
module_runtime::RuntimeProfileWalletLedgerSourceType::RedeemCodeReward
}
}
}
pub(crate) fn map_runtime_profile_redeem_code_mode(
value: module_runtime::RuntimeProfileRedeemCodeMode,
) -> crate::module_bindings::RuntimeProfileRedeemCodeMode {
match value {
module_runtime::RuntimeProfileRedeemCodeMode::Public => {
crate::module_bindings::RuntimeProfileRedeemCodeMode::Public
}
module_runtime::RuntimeProfileRedeemCodeMode::Unique => {
crate::module_bindings::RuntimeProfileRedeemCodeMode::Unique
}
module_runtime::RuntimeProfileRedeemCodeMode::Private => {
crate::module_bindings::RuntimeProfileRedeemCodeMode::Private
}
}
}
pub(crate) fn map_runtime_profile_redeem_code_mode_back(
value: crate::module_bindings::RuntimeProfileRedeemCodeMode,
) -> module_runtime::RuntimeProfileRedeemCodeMode {
match value {
crate::module_bindings::RuntimeProfileRedeemCodeMode::Public => {
module_runtime::RuntimeProfileRedeemCodeMode::Public
}
crate::module_bindings::RuntimeProfileRedeemCodeMode::Unique => {
module_runtime::RuntimeProfileRedeemCodeMode::Unique
}
crate::module_bindings::RuntimeProfileRedeemCodeMode::Private => {
module_runtime::RuntimeProfileRedeemCodeMode::Private
}
}
}
@@ -4607,6 +4760,124 @@ pub struct BigFishWorkSummaryRecord {
pub level_main_image_ready_count: u32,
pub level_motion_ready_count: u32,
pub background_ready: bool,
pub play_count: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
struct CompatibleBigFishWorkSummaryRecord {
work_id: String,
source_session_id: String,
#[serde(default)]
owner_user_id: Option<String>,
title: String,
subtitle: String,
summary: String,
cover_image_src: Option<String>,
status: String,
updated_at_micros: i64,
publish_ready: bool,
level_count: u32,
level_main_image_ready_count: u32,
level_motion_ready_count: u32,
background_ready: bool,
#[serde(default)]
play_count: u32,
}
impl CompatibleBigFishWorkSummaryRecord {
fn into_record(self, fallback_owner_user_id: Option<&str>) -> BigFishWorkSummaryRecord {
BigFishWorkSummaryRecord {
work_id: self.work_id,
source_session_id: self.source_session_id,
// 中文注释:兼容旧 works JSON 没有 owner_user_id 的历史数据,避免一次字段升级把整个作品列表打崩。
owner_user_id: self.owner_user_id.unwrap_or_else(|| {
fallback_owner_user_id
.map(str::to_string)
.unwrap_or_default()
}),
title: self.title,
subtitle: self.subtitle,
summary: self.summary,
cover_image_src: self.cover_image_src,
status: self.status,
updated_at_micros: self.updated_at_micros,
publish_ready: self.publish_ready,
level_count: self.level_count,
level_main_image_ready_count: self.level_main_image_ready_count,
level_motion_ready_count: self.level_motion_ready_count,
background_ready: self.background_ready,
play_count: self.play_count,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn big_fish_works_mapper_backfills_missing_owner_user_id_for_private_lists() {
let result = BigFishWorksProcedureResult {
ok: true,
items_json: Some(
r#"[{
"work_id":"big-fish-work-session-1",
"source_session_id":"session-1",
"title":"深海草稿",
"subtitle":"副标题",
"summary":"摘要",
"cover_image_src":null,
"status":"draft",
"updated_at_micros":123,
"publish_ready":false,
"level_count":8,
"level_main_image_ready_count":0,
"level_motion_ready_count":0,
"background_ready":false
}]"#
.to_string(),
),
error_message: None,
};
let items = map_big_fish_works_procedure_result(result, Some("user-1"))
.expect("旧 works JSON 应能被兼容解析");
assert_eq!(items.len(), 1);
assert_eq!(items[0].owner_user_id, "user-1");
}
#[test]
fn big_fish_works_mapper_keeps_empty_owner_when_gallery_legacy_json_lacks_field() {
let result = BigFishWorksProcedureResult {
ok: true,
items_json: Some(
r#"[{
"work_id":"big-fish-work-session-2",
"source_session_id":"session-2",
"title":"公开作品",
"subtitle":"副标题",
"summary":"摘要",
"cover_image_src":null,
"status":"published",
"updated_at_micros":456,
"publish_ready":true,
"level_count":8,
"level_main_image_ready_count":8,
"level_motion_ready_count":16,
"background_ready":true
}]"#
.to_string(),
),
error_message: None,
};
let items = map_big_fish_works_procedure_result(result, None)
.expect("公开 works 旧 JSON 也不应因缺字段报错");
assert_eq!(items.len(), 1);
assert!(items[0].owner_user_id.is_empty());
}
}
#[derive(Clone, Debug, PartialEq, Eq)]