停止使用原始 UUID:在 Rust 中实现类型安全、带前缀的 ID(Stripe 风格)

发布: (2025年12月24日 GMT+8 13:46)
3 min read
原文: Dev.to

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,因此仍然可以调用 uuid crate 中的 .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 或者前缀混淆,反序列化会因验证错误而失败。

  • Crates.io:
  • Docs:
  • GitHub:

欢迎分享你对 API 设计的想法! 🦀

Back to Blog

相关文章

阅读更多 »

Rust 中的数据 vs 行为

Rust 中的结构体:建模数据 在 Rust 中,结构体纯粹用于表示数据。 ```rust struct Rect { width: f32, height: f32, } ``` 结构体不定义行为…