Merge branch 'master' into refactor/remove-toolcall.jsonl-legacy
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m39s
Project CI / Backend tests (pull_request) Successful in 4m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m34s
Project CI / Native shell tests (pull_request) Successful in 6m15s
Project CI / Frontend tests (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m23s
Project CI / Repository checks (pull_request) Successful in 2m45s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m19s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m39s
Project CI / Backend tests (pull_request) Successful in 4m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m34s
Project CI / Native shell tests (pull_request) Successful in 6m15s
Project CI / Frontend tests (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m23s
Project CI / Repository checks (pull_request) Successful in 2m45s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m19s
This commit is contained in:
@@ -56,7 +56,9 @@ export default defineConfig(({ command, mode }) => {
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
chunkSizeWarningLimit: 600,
|
||||
// 中文注释:后台单页 chunk 已增长到 600.23 kB;沿用 2026-05-01 把 500 提到 600 的同一处理口径
|
||||
// 提到 700,先让直推 master 的构建门禁恢复可过,真正的按路由拆分另行排期。
|
||||
chunkSizeWarningLimit: 700,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2120,6 +2120,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策:主站与图片画板统一复用公共泥点资产入口,收起态展示总额与充值,展开态只展示不限时泥点、每日免费泥点和使用详情;充值中心 BFF 继续统一下发总额、三桶余额、限时到期时间、每日免费基础重置额及下次重置时间,前端不得自行相减推算,但会员周期限时泥点仅用于存量兼容和后端结算,当前版本不在前台展示。钱包明细每次展开都重新读取充值中心 BFF,打开期间实时总额变化时继续补读;图片画板的生成扣费或退款完成后同时刷新总额与充值中心拆分。充值中心读请求必须使用 revision 门禁,支付创建、到账确认等权威响应写入时使旧读失效,避免旧响应覆盖新的每日免费 / 不限时明细。默认泥点商品收敛为 `60 / ¥6`、`180 + 90 / ¥18`、`300 + 150 / ¥30`、`680 + 340 / ¥68` 四档,`60` 档无赠送,后三档按现有 `user_id + product_id` 独立资格规则首次购买加赠 `50%`。当前版本关闭会员购买页签、会员商品和购买 / 升级入口。
|
||||
- 2026-07-17 追加:主站、图片画板与 AI 游戏创作独立 App 的泥点账单统一复用 `packages/shared/src/components/PlatformProfileWalletLedgerModal`。共享组件只依赖 `ProfileWalletLedgerResponse`,承接来源 label、金额正负号、UTC 日期、余额兜底和 loading / empty / error 展示;`/api/profile/wallet-ledger` 请求、鉴权、打开状态与重试生命周期继续由各宿主持有,不把账户事实或后端副作用下沉到共享 UI。
|
||||
- 2026-09-07 追加:资产扣费在既有钱包流水 metadata 中记录服务端确定的 `assetKind`,`GET /api/profile/wallet-ledger` 只把白名单类型映射为可选用户文案 `reason`,不暴露原始 metadata、资源 ID、任务 ID 或未知内部枚举。共享账单组件优先展示非空 `reason`;历史、未知和空 metadata 继续按 `sourceType` 回退为“资产操作消耗”,不得由客户端猜测业务类型。
|
||||
- 2026-10-01 追加:账单不计入实际变动为 0 的条目,规则落在 SpacetimeDB 侧而不是展示层。写入侧只在余额真的变化时落账:每日免费 / 会员周期换期按 `resolve_expiring_points_wallet_transition` 先算出“换期后余额 + 实际变动”,变动为 0(含到期额度等于发放额度、余额不足以扣回到期额度、额度与发放都为 0)时只更新余额状态、不写 `profile_wallet_ledger`;通用结算入口 `apply_profile_wallet_signed_delta` 在 0 变动时同样直接返回当前余额。读取侧 `list_profile_wallet_ledger_entries` 在按余额结算链排序之后、截断 50 条上限之前剔除 `amount_delta == 0` 的存量行,因此历史遗留的 0 流水也立即从账单消失,且不占用列表上限、不改变其余条目的先后与 `balance_after` 语义。此前 `update_profile_wallet_balance_for_expiring_points` 只判断“到期额度与发放额度都为 0”,判的是入参而不是实际余额变动,所以余额不变的换期流水仍会落账并显示成 0。
|
||||
- 影响范围:`profile_recharge_product_config` 默认商品、充值中心 read model、共享前后端契约、主站与图片画板泥点资产入口、充值弹窗、后台充值商品默认值。
|
||||
- 验证方式:充值与统一入口定向前端测试、`npm run typecheck`、充值商品定向 Rust 测试、`cargo check -p spacetime-module -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
@@ -812,8 +812,9 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
### `profile_wallet_ledger`
|
||||
|
||||
- Rust 结构体:`ProfileWalletLedger`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
|
||||
- 说明:账号钱包流水表。`created_at` 表示钱包事务实际结算时间,列表先按当前余额反向校验 `balance_after - amount_delta` 的结算链,再以该时间倒序兜底,避免支付回调或退款重放延迟时出现余额顺序倒置;支付平台确认时间继续保存在充值订单 `paid_at`。`metadata_json` 为可选 JSON 对象字符串,旧行缺失时读取层按 `{}` 归一;外部生成扣费 / 退款写入 `externalGenerationJobId`,使退款记录可以追溯到对应 `external_generation_job`。
|
||||
- 实际变动为 0 的条目不进账单:写入侧只在余额真的变化时落账,每日免费 / 会员周期换期先按 `previous_balance - expired_points + granted_points`(饱和运算)算出实际变动,变动为 0 时只更新余额状态、不写本表;通用结算入口 `apply_profile_wallet_signed_delta` 在 `amount_delta == 0` 时同样不落账。读取侧 `list_profile_wallet_ledger_entries` 在结算链排序之后、截断 50 条上限之前剔除 `amount_delta == 0` 的存量行,因此历史遗留的 0 变动流水不会出现在 `GET /api/profile/wallet-ledger`,也不占用列表上限。该规则只影响展示与落账,不改变幂等 ledger、余额结算、消费投影和退款语义。
|
||||
|
||||
### `profile_wallet_consumption_total`
|
||||
|
||||
|
||||
@@ -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