This commit is contained in:
11
server-rs/crates/shared-kernel/Cargo.toml
Normal file
11
server-rs/crates/shared-kernel/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "shared-kernel"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
59
server-rs/crates/shared-kernel/README.md
Normal file
59
server-rs/crates/shared-kernel/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# shared-kernel 共享 crate 阶段性说明
|
||||
|
||||
日期:`2026-04-22`
|
||||
|
||||
## 1. crate 职责
|
||||
|
||||
`shared-kernel` 是跨模块共享领域内核 crate,当前阶段已经开始承接最小共享基础能力,负责:
|
||||
|
||||
1. 共享 ID、值对象、枚举与基础领域类型
|
||||
2. 共享时间、状态、版本、通用校验等基础规则
|
||||
3. 供各模块 package 复用的最小领域内核
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前阶段已落地的共享能力:
|
||||
|
||||
1. 必填/可选字符串归一化
|
||||
2. 字符串列表归一化
|
||||
3. 前缀 UUID / 前缀种子 ID 生成
|
||||
4. RFC3339 格式化与解析
|
||||
5. 微秒时间戳文本格式化
|
||||
|
||||
后续与本 crate 直接相关的任务包括:
|
||||
|
||||
1. 统一用户、会话、世界、角色、资产等核心 ID 类型
|
||||
2. 统一时间戳、版本号、状态枚举等共享结构
|
||||
3. 抽取真正跨模块复用的最小领域规则
|
||||
4. 避免把模块私有规则错误上提到共享内核
|
||||
|
||||
当前已接入的 crate 已覆盖:
|
||||
|
||||
1. `module-assets`
|
||||
2. `module-auth`
|
||||
3. `platform-auth`
|
||||
4. `module-runtime`
|
||||
5. `module-story`
|
||||
6. `spacetime-client`
|
||||
7. `api-server`
|
||||
8. `module-ai`
|
||||
9. `module-inventory`
|
||||
10. `module-runtime-item`
|
||||
11. `module-npc`
|
||||
12. `module-quest`
|
||||
13. `module-combat`
|
||||
14. `module-progression`
|
||||
|
||||
## 3. 边界约束
|
||||
|
||||
1. `shared-kernel` 只放跨模块最小共享内核,不承接具体业务模块的私有规则。
|
||||
2. 任何进入本 crate 的类型都必须证明至少被多个模块稳定复用。
|
||||
3. 不能把主模块实现重新堆进共享内核,避免形成新的“大公共垃圾桶”。
|
||||
|
||||
更详细的阶段性设计见:
|
||||
|
||||
1. `docs/technical/RUST_SHARED_KERNEL_CRATE_STAGE1_DESIGN_2026-04-21.md`
|
||||
2. `docs/technical/RUST_SHARED_KERNEL_CRATE_STAGE2_ADOPTION_2026-04-21.md`
|
||||
3. `docs/technical/RUST_SHARED_KERNEL_CRATE_STAGE3_VALUE_NORMALIZATION_2026-04-22.md`
|
||||
4. `docs/technical/RUST_SHARED_KERNEL_CRATE_STAGE4_REQUIRED_STRING_ADOPTION_2026-04-22.md`
|
||||
5. `docs/technical/RUST_SHARED_KERNEL_CRATE_STAGE5_PURE_DOMAIN_FIELD_ADOPTION_2026-04-22.md`
|
||||
152
server-rs/crates/shared-kernel/src/lib.rs
Normal file
152
server-rs/crates/shared-kernel/src/lib.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use time::OffsetDateTime;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 统一做必填字符串归一化,避免各模块散落重复的 `trim().to_string()`。
|
||||
pub fn normalize_required_string(value: impl AsRef<str>) -> Option<String> {
|
||||
let normalized = value.as_ref().trim();
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(normalized.to_string())
|
||||
}
|
||||
|
||||
/// 统一做可选字符串归一化,空白字符串一律视为 `None`。
|
||||
pub fn normalize_optional_string(value: Option<String>) -> Option<String> {
|
||||
value.and_then(normalize_required_string)
|
||||
}
|
||||
|
||||
/// 统一做字符串列表归一化,逐项裁剪并丢弃空白项。
|
||||
pub fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
values
|
||||
.into_iter()
|
||||
.filter_map(|value| normalize_required_string(value))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 统一生成“前缀 + 十六进制微秒种子”的稳定 ID,适合业务对象主键。
|
||||
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_uuid_id(prefix: &str) -> String {
|
||||
format!("{prefix}{}", Uuid::new_v4().simple())
|
||||
}
|
||||
|
||||
/// SpacetimeDB 的 wasm32 模块不应走浏览器/本地随机 UUID 生成。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn build_prefixed_uuid_id(_prefix: &str) -> String {
|
||||
panic!(
|
||||
"shared-kernel::build_prefixed_uuid_id 不支持 wasm32,请改用显式 ID 或 SpacetimeDB 上下文生成能力"
|
||||
)
|
||||
}
|
||||
|
||||
/// 统一生成 UUID simple 字符串,供 token、随机种子等轻量场景复用。
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new_uuid_simple_string() -> String {
|
||||
Uuid::new_v4().simple().to_string()
|
||||
}
|
||||
|
||||
/// SpacetimeDB 的 wasm32 模块不应走浏览器/本地随机 UUID 生成。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn new_uuid_simple_string() -> String {
|
||||
panic!(
|
||||
"shared-kernel::new_uuid_simple_string 不支持 wasm32,请改用显式 ID 或 SpacetimeDB 上下文生成能力"
|
||||
)
|
||||
}
|
||||
|
||||
/// 统一格式化微秒时间戳,当前阶段固定为 `seconds.microsZ` 文本口径。
|
||||
pub fn format_timestamp_micros(micros: i64) -> String {
|
||||
let seconds = micros.div_euclid(1_000_000);
|
||||
let subsec_micros = micros.rem_euclid(1_000_000);
|
||||
format!("{seconds}.{subsec_micros:06}Z")
|
||||
}
|
||||
|
||||
/// 统一把 `OffsetDateTime` 转成 Unix 微秒时间戳,避免各模块重复手写纳秒除法。
|
||||
pub fn offset_datetime_to_unix_micros(value: OffsetDateTime) -> i64 {
|
||||
(value.unix_timestamp_nanos() / 1_000) as i64
|
||||
}
|
||||
|
||||
/// 统一格式化 RFC3339 字符串,避免每个模块自己拼格式化错误文案。
|
||||
pub fn format_rfc3339(value: OffsetDateTime) -> Result<String, String> {
|
||||
value
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
/// 统一解析 RFC3339 字符串,供模块自行补充更贴近业务的错误上下文。
|
||||
pub fn parse_rfc3339(value: &str) -> Result<OffsetDateTime, String> {
|
||||
OffsetDateTime::parse(value, &time::format_description::well_known::Rfc3339)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalize_required_string_trims_and_filters_blank() {
|
||||
assert_eq!(
|
||||
normalize_required_string(" hero_001 "),
|
||||
Some("hero_001".to_string())
|
||||
);
|
||||
assert_eq!(normalize_required_string(" "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_optional_string_filters_blank() {
|
||||
assert_eq!(
|
||||
normalize_optional_string(Some(" profile_001 ".to_string())),
|
||||
Some("profile_001".to_string())
|
||||
);
|
||||
assert_eq!(normalize_optional_string(Some(" ".to_string())), None);
|
||||
assert_eq!(normalize_optional_string(None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_string_list_trims_and_filters_blank() {
|
||||
assert_eq!(
|
||||
normalize_string_list(vec![
|
||||
" alpha ".to_string(),
|
||||
"".to_string(),
|
||||
" ".to_string(),
|
||||
"beta".to_string()
|
||||
]),
|
||||
vec!["alpha".to_string(), "beta".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_prefixed_seed_id_uses_hex_seed() {
|
||||
assert_eq!(build_prefixed_seed_id("assetobj_", 255), "assetobj_ff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_timestamp_micros_is_stable() {
|
||||
assert_eq!(
|
||||
format_timestamp_micros(1_713_686_401_234_567),
|
||||
"1713686401.234567Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_datetime_to_unix_micros_is_stable() {
|
||||
let value = OffsetDateTime::UNIX_EPOCH
|
||||
+ time::Duration::seconds(1_713_686_401)
|
||||
+ time::Duration::microseconds(234_567);
|
||||
|
||||
assert_eq!(offset_datetime_to_unix_micros(value), 1_713_686_401_234_567);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_and_parse_rfc3339_round_trip() {
|
||||
let now = OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(1_713_686_400);
|
||||
let text = format_rfc3339(now).expect("rfc3339 should format");
|
||||
let parsed = parse_rfc3339(&text).expect("rfc3339 should parse");
|
||||
|
||||
assert_eq!(parsed.unix_timestamp(), now.unix_timestamp());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user