Merge branch 'master' into codex/game-rating-review
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m30s
Project CI / Backend tests (pull_request) Successful in 5m11s
Project CI / Native shell tests (pull_request) Successful in 6m6s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m23s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m30s
Project CI / Backend tests (pull_request) Successful in 5m11s
Project CI / Native shell tests (pull_request) Successful in 6m6s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m23s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 58s
This commit is contained in:
@@ -2879,6 +2879,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_ledger_list_drops_zero_change_entries_without_reordering_the_rest() {
|
||||
let mut entries = vec![
|
||||
wallet_ledger_snapshot("daily-free-reset", 0, 97, 4),
|
||||
wallet_ledger_snapshot("recharge-60", 60, 97, 3),
|
||||
wallet_ledger_snapshot("membership-period-reset", 0, 37, 2),
|
||||
wallet_ledger_snapshot("recharge-180-delayed", 180, 37, 1),
|
||||
];
|
||||
|
||||
retain_profile_wallet_ledger_entries_with_change(&mut entries);
|
||||
|
||||
assert_eq!(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| entry.wallet_ledger_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["recharge-60", "recharge-180-delayed"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expiring_points_transition_reports_zero_change_when_balance_is_unchanged() {
|
||||
assert_eq!(
|
||||
resolve_expiring_points_wallet_transition(0, 0, 0),
|
||||
(0, 0),
|
||||
"没有额度和发放时不产生账单"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_expiring_points_wallet_transition(5, 5, 5),
|
||||
(5, 0),
|
||||
"每日免费 / 会员周期到期额度等于发放额度时余额不变"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_expiring_points_wallet_transition(0, 5, 0),
|
||||
(0, 0),
|
||||
"余额不足以扣回到期额度时不产生负向账单"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expiring_points_transition_reports_real_balance_change() {
|
||||
assert_eq!(resolve_expiring_points_wallet_transition(0, 0, 5), (5, 5));
|
||||
assert_eq!(resolve_expiring_points_wallet_transition(5, 5, 0), (0, -5));
|
||||
assert_eq!(resolve_expiring_points_wallet_transition(3, 5, 2), (2, -1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_wallet_spend_counts_asset_and_llm_consumption() {
|
||||
assert_eq!(
|
||||
@@ -4453,11 +4499,20 @@ fn list_profile_wallet_ledger_entries(
|
||||
|
||||
let current_balance = profile_wallet_balance(ctx, &validated_input.user_id);
|
||||
sort_profile_wallet_ledger_entries(&mut entries, current_balance);
|
||||
retain_profile_wallet_ledger_entries_with_change(&mut entries);
|
||||
entries.truncate(PROFILE_WALLET_LEDGER_LIST_LIMIT);
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
// 账单只呈现真实余额变化:历史遗留的 0 变动流水在这里统一剔除,排序链已经按余额反推完成,
|
||||
// 去掉 0 变动行不改变其余条目的先后与 `balance_after` 语义,也不占用列表上限。
|
||||
fn retain_profile_wallet_ledger_entries_with_change(
|
||||
entries: &mut Vec<RuntimeProfileWalletLedgerEntrySnapshot>,
|
||||
) {
|
||||
entries.retain(|entry| entry.amount_delta != 0);
|
||||
}
|
||||
|
||||
fn sort_profile_wallet_ledger_entries(
|
||||
entries: &mut Vec<RuntimeProfileWalletLedgerEntrySnapshot>,
|
||||
current_balance: u64,
|
||||
@@ -8843,6 +8898,18 @@ fn membership_cycle_metadata(
|
||||
.unwrap_or_else(|_| PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON.to_string())
|
||||
}
|
||||
|
||||
// 每日免费 / 会员周期额度换期的余额迁移:返回换期后的余额与实际变动。
|
||||
// 到期额度和发放额度相同时余额不变,调用方不得再落一条 0 变动账单。
|
||||
fn resolve_expiring_points_wallet_transition(
|
||||
previous_balance: u64,
|
||||
expired_points: u64,
|
||||
granted_points: u64,
|
||||
) -> (u64, i64) {
|
||||
let after_expiry = previous_balance.saturating_sub(expired_points);
|
||||
let next_balance = after_expiry.saturating_add(granted_points);
|
||||
(next_balance, next_balance as i64 - previous_balance as i64)
|
||||
}
|
||||
|
||||
fn update_profile_wallet_balance_for_expiring_points(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -8853,9 +8920,6 @@ fn update_profile_wallet_balance_for_expiring_points(
|
||||
created_at: Timestamp,
|
||||
metadata_json: String,
|
||||
) {
|
||||
if expired_points == 0 && granted_points == 0 {
|
||||
return;
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.profile_wallet_ledger()
|
||||
@@ -8872,8 +8936,11 @@ fn update_profile_wallet_balance_for_expiring_points(
|
||||
.user_id()
|
||||
.find(&user_id.to_string());
|
||||
let previous_balance = current.as_ref().map(|row| row.wallet_balance).unwrap_or(0);
|
||||
let after_expiry = previous_balance.saturating_sub(expired_points);
|
||||
let next_balance = after_expiry.saturating_add(granted_points);
|
||||
let (next_balance, amount_delta) =
|
||||
resolve_expiring_points_wallet_transition(previous_balance, expired_points, granted_points);
|
||||
if amount_delta == 0 {
|
||||
return;
|
||||
}
|
||||
let created_state_at = current
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
@@ -8908,7 +8975,7 @@ fn update_profile_wallet_balance_for_expiring_points(
|
||||
ctx.db.profile_wallet_ledger().insert(ProfileWalletLedger {
|
||||
wallet_ledger_id: ledger_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
amount_delta: next_balance as i64 - previous_balance as i64,
|
||||
amount_delta,
|
||||
balance_after: next_balance,
|
||||
source_type,
|
||||
created_at,
|
||||
@@ -10422,6 +10489,10 @@ fn apply_profile_wallet_signed_delta(
|
||||
let settled_at = ctx.timestamp;
|
||||
let ledger_recorded_at = profile_wallet_ledger_recorded_at(created_at, settled_at);
|
||||
refresh_profile_wallet_expiring_points(ctx, user_id, settled_at);
|
||||
// 实际变动为 0 的结算不产生账单流水:既不改变余额,也不该在账单里占一条 0。
|
||||
if amount_delta == 0 {
|
||||
return Ok(profile_wallet_balance(ctx, user_id));
|
||||
}
|
||||
if idempotent {
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
|
||||
Reference in New Issue
Block a user