百积木文档
开发指南后端应用开发

Rust 服务设计约束

用类型化 Rust 模型承载业务语义,并把 JSON、Serde、数据库和外部协议限制在适配器边界。

Hosted Service 负责构建和运行后端应用,但不会替应用决定内部代码质量。Rust 服务必须先定义类型所有权和 边界,再实现 HTTP、数据库、消息或 Artifact 适配器。

核心原则是:对象承载语义,JSON 只承载边界表示。 JSON 进入服务后必须立即转换为明确的 Rust 对象;领域和应用逻辑不得继续处理无类型 JSON。

强制边界

JSON 只允许存在于以下边界:

  1. HTTP 或消息入站:反序列化为请求 Contract DTO,并完成结构与语义校验。
  2. HTTP 或消息出站:把确定的响应 Contract DTO 序列化一次后发送。
  3. 数据库 JSON/TEXT 列:Repository 读取后立即反序列化为明确类型,写入前再从明确类型序列化。
  4. 不可变 Artifact:Artifact Adapter 按声明的契约版本读写类型化内容。

进入应用层后,以下写法禁止作为业务状态、用例参数或转换机制:

serde_json::Value
Map<String, serde_json::Value>
json!({ /* 业务对象 */ })
value.get("field")
serde_json::to_value(source).and_then(serde_json::from_value::<Target>)

禁止递归扫描键名、删除未知字段后重试、连续反序列化猜测结构,或用 Value -> Value 实现“编译”和 转换。serde_json::Value 只可用于协议明确声明的不透明扩展字段;该字段必须有名称、Owner 和严格 SemVer schemaVersion,业务逻辑不得读取内部键。

Serde 不是业务转换器

Serde 只负责“字节与同一个类型”之间的编码和解码。两个业务对象之间的转换必须由显式编译器、应用服务 或 TryFrom / From 完成,不能先转成 JSON 再转成另一个类型。

类型必须有唯一职责

每个结构体或枚举在创建前必须归入一个类别:

类别职责示例命名不得承担的职责
Entity / Aggregate业务身份、不变量和状态变化OrderHTTP、SQL 或任意 JSON 键
Value Object已校验的领域值OrderIdMoney裸字符串兼容和隐式默认
Command / Query应用用例输入CreateOrder直接映射数据库行
Contract DTO版本化跨进程协议CreateOrderRequest仓储状态和内部生命周期
Persistence Record数据库列投影OrderRecord领域行为和跨服务输出
Adapter Payload第三方或框架局部载荷VendorOrderPayload在应用层长期流转

数据库行类型以 RecordRow 结尾,并在 Repository 内转换成领域对象。HTTP 请求和响应只定义在 入站适配器或独立 Contract crate 中。服务不得为了省一次转换,直接把数据库 Record 或可变领域 Entity 作为跨服务响应输出。

同一个协议只能有一个权威 Contract。多个服务共同消费时,应提取独立 Contract crate;无法共享时, 明确一个权威消费者模型,由生产者通过显式转换生成它。禁止复制字段相似但会独立演进的同名结构、枚举或 白名单。

目录和依赖方向

普通服务按职责组织,只有出现真实职责时才创建对应目录:

src/
├── main.rs                 # 组合根:配置、依赖注入、路由、启动
├── config.rs               # 类型化配置读取与校验
├── error.rs                # 顶层错误映射
├── domain/                 # Entity、Value Object、领域错误
├── application/            # Command、Query、用例服务和 Port trait
├── contracts/              # 服务拥有的版本化入站/出站 Contract
└── adapters/
    ├── inbound/http/       # 认证上下文、解码、调用用例、响应映射
    └── outbound/
        ├── persistence/    # Record 与 Repository 实现
        ├── http/           # 外部服务客户端
        ├── messaging/      # 消息协议边界
        └── artifact/       # Artifact 类型与存取
tests/
├── contract/
└── integration/

依赖方向必须指向稳定业务层:

main / composition root
  -> inbound adapters -> application -> domain
  -> outbound adapters ----^          ^
  -> contracts
  • domain 不依赖 Axum、SQLx、Reqwest、消息 SDK 或 serde_json::Value
  • application 只依赖领域类型和自身定义的 Port,不直接创建数据库连接池或 HTTP Client。
  • Adapter 实现 Port,并负责 Contract、Record、第三方 Payload 与领域对象之间的显式转换。
  • 领域 Entity 不为了传输方便直接派生 Serde;只有权威 Contract 类型才定义序列化外形。
  • main.rs 只组装依赖和启动进程,不放业务规则,也不执行数据回填或临时修复。
  • 不使用 utilscommonmodels 汇集跨层职责;文件应只有一个清晰角色和主要变化原因。

入站转换示例

HTTP Adapter 先把 JSON 解码成 Contract DTO,再显式转换成应用 Command:

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct CreateOrderRequest {
    contract_version: SemanticVersion,
    customer_id: String,
    amount_cents: i64,
}

struct CreateOrder {
    customer_id: CustomerId,
    amount: Money,
}

impl TryFrom<CreateOrderRequest> for CreateOrder {
    type Error = ContractError;

    fn try_from(request: CreateOrderRequest) -> Result<Self, Self::Error> {
        ensure_supported_contract(&request.contract_version)?;
        Ok(Self {
            customer_id: CustomerId::parse(request.customer_id)?,
            amount: Money::from_cents(request.amount_cents)?,
        })
    }
}

示例中的 SemanticVersionCustomerIdMoney 都是经校验的值对象,不是类型别名。转换失败返回稳定 错误类型;Adapter 再把它映射成 HTTP 状态和 CModel 错误响应。领域层 不构造 HTTP 状态码。

反序列化错误必须包含字段路径。可使用 serde_path_to_error 或框架等价能力,把缺失字段、未知字段、 错误枚举和错误嵌套映射成稳定错误码;不得把原始用户 Payload、Token 或 Secret 写入错误和日志。

契约、Serde 与版本演进

Serde 属性本身就是协议:renametagaliasdefaultflattenuntagged、可选字段和 deny_unknown_fields 都会改变调用方可接受或收到的数据外形。

  • 新的闭合内部协议默认拒绝未知字段。
  • 不因一次解析失败临时增加 aliasdefault、大小写归一化或平行枚举。
  • 可空与缺省必须分别建模;Option<T> 不能同时模糊表达两种语义。
  • 开放扩展只能使用有 Owner、有 Schema、有严格 SemVer 版本的显式 extension 字段。
  • 所有 Contract、Schema、Artifact 和迁移版本只接受严格 SemVer 2.0.0,不能使用 v11.2、日期或 构建号代替版本。
  • 不兼容变更必须发布新的主版本,并同步盘点和验证生产者、消费者及持久化数据迁移。

历史数据兼容必须通过有 Owner、可审计、可安全重跑且有删除条件的类型化迁移完成。不能把历史格式的 递归修补、双读或无期限回退留在业务运行时。数据库变更必须走 数据库迁移 Artifact,不得在 Handler、Worker、 启动钩子或随服务交付的临时 CLI 中加入一次性修复、回填或生产 SQL。

测试与 CI 门禁

涉及 Contract、Serde、数据库 JSON、Artifact 或模型转换的变更,至少包含:

  1. Contract 正向序列化和反序列化测试,固定字段名、Tag、必填性和版本。
  2. 未知字段、未知变体、缺失字段、非法版本和错误嵌套的负向测试,并断言字段路径。
  3. TryFromFrom 或显式编译器测试,覆盖每个业务变体与不变量。
  4. 生产者和消费者共享 Contract 的契约测试或 Golden Fixture。
  5. Record <-> domainArtifact bytes <-> Contract 的集成测试。
  6. cargo fmt --checkcargo clippy --all-targets --all-features -- -D warnings 和适用范围的 cargo test

CI 应检查领域层和应用层是否出现动态 JSON。以下命令可用于定位,每个命中都必须删除,或证明它只位于 上述允许的不透明扩展边界:

rg -n 'serde_json::(Value|Map)|json!\s*\(' src/domain src/application
rg -n '\.get\("[^\"]+"\)' src/domain src/application
rg -n '#\[serde\((alias|default|flatten|untagged)' src

提交构建前检查

  • 每个类型都已明确归类为 Entity、Value Object、Command/Query、Contract、Record 或 Adapter Payload。
  • JSON 只在 Adapter、Repository 或 Artifact 边界出现,并且只反序列化或序列化一次。
  • 业务转换由显式 TryFromFrom、编译器或应用服务完成。
  • 没有 Record、Entity 和 Contract 同名混用,也没有复制权威模型。
  • Serde Tag、必填字段、未知字段策略和严格 SemVer 版本都有契约测试。
  • domain/application 没有动态 JSON、键扫描、递归修补或解析重试。
  • 配置、数据库连接、凭据、Endpoint 和业务目录数据来自 Environment 或权威 Provider,没有硬编码进源码。
  • 业务二进制、路由、任务、Worker、启动流程和随服务交付的 CLI 中没有一次性维护代码。
  • 格式化、Clippy、单元测试、契约测试和集成测试全部通过后,才从精确 Git commit 创建 BuildJob。

本页内容