停止使用原始 UUID:在 Rust 中实现类型安全、带前缀的 ID(Stripe 风格)
Source: Dev.to
我们都遇到过这种情况。凌晨 2 点,你盯着服务器日志调试请求,看到下面这行:
Processing request for ID: 550e8400-e29b-41d4-a716-446655440000
这是 用户 ID 吗?订单 ID 吗?API 密钥 吗?谁知道——只是一串十六进制字符。
更糟的是,你可能会写出这样的函数:
fn process_payment(user_id: Uuid, order_id: Uuid) { /* … */ }
如果不小心写成 process_payment(order_id, user_id),编译器也拦不住。两个参数都是 Uuid,所以你要等到数据库抛出 “记录未找到” 错误——甚至更糟,数据被破坏时才会发现问题。
Stripe 风格的解决方案
Stripe 的 ID 是自描述的(cus_018…、ch_018…),既可读又 类型安全。
我想在 Rust 中获得同样的体验,并且在编译期就能保证安全。于是诞生了 puuid。
puuid(Prefixed UUID)是一个轻量级 crate,包装了标准的 uuid 库,并提供:
- 可读性 – ID 会打印成
"user_018c…"而不是纯数字。 - 类型安全 – 你不能把
UserId当作OrderId使用。
定义前缀
use puuid::{Puuid, prefix};
// 1. 定义你的前缀
prefix!(User, "user");
prefix!(Order, "ord");
// 2. 创建强类型
pub type UserId = Puuid;
pub type OrderId = Puuid;
编译期保护
fn delete_order(id: OrderId) {
println!("Deleting order: {}", id);
}
fn main() {
let user_id = UserId::new_v7();
// ❌ COMPILE ERROR: expected `OrderId`, found `UserId`
delete_order(user_id);
}
现在 Rust 编译器会阻止意外的混用。
实现细节
Puuid是一个#[repr(transparent)]包装器,内部是uuid::Uuid。- 内存 – 与标准 UUID 大小完全相同(16 字节)。
- 数据库 – 可以直接作为 UUID 插入(
.into_inner()),也可以存为文本以保留前缀可见。 - 方法 – 实现了
Deref,因此仍然可以调用uuidcrate 中的.as_bytes()、.get_version()等方法。
默认使用 UUID v7
如果你还没从 v4(随机)迁移到 v7,应该考虑迁移。v7 UUID 是 时间可排序 的,这意味着在数据库中会自然按创建时间建立索引,减少索引碎片并加速大表插入。
let id = UserId::new_v7();
// Output: user_018c6427-4f30-7f89-a1b2-c3d4e5f67890
JSON API 集成
在构建 JSON API(Axum、Actix 等)时,puuid 会自动处理(反)序列化:
#[derive(Serialize, Deserialize)]
struct Checkout {
id: OrderId,
customer: UserId,
}
客户端 JSON
{
"id": "ord_018...",
"customer": "user_018..."
}
如果客户端发送的是原始 UUID 或者前缀混淆,反序列化会因验证错误而失败。
Links
- Crates.io:
- Docs:
- GitHub:
欢迎分享你对 API 设计的想法! 🦀