From 70935bdc785a9a09ac4b640edd01086231ec779f Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 15 Jul 2026 09:46:10 +0000 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=AF=B9=E8=B1=A1/=E7=BB=91?= =?UTF-8?q?=E5=AE=9AID=E6=94=B9=E7=94=A8=E5=BE=AE=E7=A7=92=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3+UUID=E9=98=B2=E5=B9=B6=E5=8F=91=E4=B8=BB?= =?UTF-8?q?=E9=94=AE=E7=A2=B0=E6=92=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 纯微秒种子ID在同一微秒并发确认资产时会产生相同主键(如角色 动作帧48路并发确认),导致SpacetimeDB唯一约束冲突、整项动作 退款并遗留已上传OSS对象。shared-kernel新增带熵的 build_prefixed_seed_entropy_id(微秒hex+UUID后缀,wasm32为 panic变体),generate_asset_object_id与generate_asset_binding_id 切换过去,签名不变,全部调用点一次修复。 Co-Authored-By: Claude Fable 5 --- .../module-assets/src/asset_object_core.rs | 23 ++++++++++++++--- server-rs/crates/shared-kernel/src/lib.rs | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server-rs/crates/module-assets/src/asset_object_core.rs b/server-rs/crates/module-assets/src/asset_object_core.rs index 56f66f705..6e7d567c3 100644 --- a/server-rs/crates/module-assets/src/asset_object_core.rs +++ b/server-rs/crates/module-assets/src/asset_object_core.rs @@ -1,5 +1,5 @@ use shared_kernel::{ - build_prefixed_seed_id, format_timestamp_micros, normalize_optional_string, + build_prefixed_seed_entropy_id, format_timestamp_micros, normalize_optional_string, normalize_required_string, }; @@ -203,12 +203,14 @@ pub fn build_asset_entity_binding_record( } } +// 资产确认可能在同一微秒内并发发生(例如角色动作帧 48 路并发确认), +// 主键必须带随机熵;时间前缀仅保留调试可读性,不参与唯一性保证。 pub fn generate_asset_object_id(seed_micros: i64) -> String { - build_prefixed_seed_id(ASSET_OBJECT_ID_PREFIX, seed_micros) + build_prefixed_seed_entropy_id(ASSET_OBJECT_ID_PREFIX, seed_micros) } pub fn generate_asset_binding_id(seed_micros: i64) -> String { - build_prefixed_seed_id(ASSET_BINDING_ID_PREFIX, seed_micros) + build_prefixed_seed_entropy_id(ASSET_BINDING_ID_PREFIX, seed_micros) } pub fn normalize_optional_value(value: Option) -> Option { @@ -219,6 +221,21 @@ pub fn normalize_optional_value(value: Option) -> Option { mod tests { use super::*; + #[test] + fn generated_ids_with_same_seed_micros_never_collide() { + let seed_micros = 1_713_686_400_000_000; + + let first_object_id = generate_asset_object_id(seed_micros); + let second_object_id = generate_asset_object_id(seed_micros); + assert!(first_object_id.starts_with(ASSET_OBJECT_ID_PREFIX)); + assert_ne!(first_object_id, second_object_id); + + let first_binding_id = generate_asset_binding_id(seed_micros); + let second_binding_id = generate_asset_binding_id(seed_micros); + assert!(first_binding_id.starts_with(ASSET_BINDING_ID_PREFIX)); + assert_ne!(first_binding_id, second_binding_id); + } + #[test] fn validate_asset_object_fields_accepts_minimal_private_object_contract() { let result = validate_asset_object_fields( diff --git a/server-rs/crates/shared-kernel/src/lib.rs b/server-rs/crates/shared-kernel/src/lib.rs index ba9d67250..613ef14c3 100644 --- a/server-rs/crates/shared-kernel/src/lib.rs +++ b/server-rs/crates/shared-kernel/src/lib.rs @@ -30,6 +30,21 @@ pub fn build_prefixed_seed_id(prefix: &str, seed_micros: i64) -> String { format!("{prefix}{seed_micros:x}") } +/// 统一生成“前缀 + 十六进制微秒种子 + UUID simple 随机后缀”的唯一 ID。 +/// 纯微秒种子在并发写入时会撞主键,带熵版本保留时间前缀的可读排序,同时保证全局唯一。 +#[cfg(not(target_arch = "wasm32"))] +pub fn build_prefixed_seed_entropy_id(prefix: &str, seed_micros: i64) -> String { + format!("{prefix}{seed_micros:x}_{}", Uuid::new_v4().simple()) +} + +/// SpacetimeDB 的 wasm32 模块不应走浏览器/本地随机 UUID 生成。 +#[cfg(target_arch = "wasm32")] +pub fn build_prefixed_seed_entropy_id(_prefix: &str, _seed_micros: i64) -> String { + panic!( + "shared-kernel::build_prefixed_seed_entropy_id 不支持 wasm32,请改用显式 ID 或 SpacetimeDB 上下文生成能力" + ) +} + /// 统一生成“前缀 + UUID simple”随机 ID,适合会话态或一次性票据主键。 #[cfg(not(target_arch = "wasm32"))] pub fn build_prefixed_uuid_id(prefix: &str) -> String { @@ -124,6 +139,16 @@ mod tests { assert_eq!(build_prefixed_seed_id("assetobj_", 255), "assetobj_ff"); } + #[test] + fn build_prefixed_seed_entropy_id_keeps_seed_prefix_and_never_collides_on_same_seed() { + let first = build_prefixed_seed_entropy_id("assetobj_", 255); + let second = build_prefixed_seed_entropy_id("assetobj_", 255); + + assert!(first.starts_with("assetobj_ff_")); + assert!(second.starts_with("assetobj_ff_")); + assert_ne!(first, second); + } + #[test] fn format_timestamp_micros_is_stable() { assert_eq!(