| name | genies-usage |
| description | Guide for using the Genies Rust microservice framework with DDD and Dapr. Use when developing with Genies, creating aggregates, domain events, Dapr subscriptions, Casbin field-level permissions, configuration management, or when the user asks about Genies framework usage patterns. |
Genies 框架使用指南
1. 框架简介
Genies 是一个基于 Rust 的 DDD + Dapr 微服务开发框架,通过宏驱动架构提供声明式的聚合根、领域事件、权限控制和配置管理能力。
核心模块
| Crate | 版本 | 说明 |
|---|
genies | 1.4.5 | 主入口,重导出所有子 crate |
genies_derive | 1.4.5 | 过程宏库:Aggregate、DomainEvent、Config、topic、casbin 等 |
genies_core | 1.4.4 | 核心基础:错误处理、JWT、HTTP 响应模型 |
genies_config | 1.4.2 | 配置管理:ApplicationConfig、日志配置 |
genies_context | 1.4.3 | 全局上下文:CONTEXT、REMOTE_TOKEN、SERVICE_STATUS |
genies_cache | 1.4.2 | 缓存抽象:Redis/内存双后端 |
genies_dapr | 1.4.2 | Dapr 集成:CloudEvent、PubSub、Topic 自动收集 |
genies_ddd | 1.4.2 | DDD 核心:聚合根、领域事件、消息发布 |
genies_k8s | 1.4.2 | Kubernetes 探针:存活/就绪检查 |
genies_auth | 1.4.2 | Casbin 权限:API 访问控制、字段级过滤、Admin API |
genies_auth_admin | - | 独立的权限管理 Web 界面(从 genies_auth 迁移) |
2. 快速引用
[dependencies]
genies = "1.4"
genies_derive = "1.4"
genies_auth = "1.4"
rbatis = { version = "4.5", features = ["debug_mode"] }
tokio = { version = "1.22", features = ["full"] }
salvo = { version = "0.89", features = ["rustls", "oapi", "affix-state"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
3. 核心宏速查表
| 宏 | 用途 | 示例 |
|---|
#[derive(Aggregate)] | 定义 DDD 聚合根 | #[derive(Aggregate)] struct Order {...} |
#[derive(DomainEvent)] | 定义领域事件 | #[derive(DomainEvent)] struct OrderCreated {...} |
#[topic(...)] | 订阅 Dapr 事件 | #[topic(name="...", pubsub="messagebus")] |
#[derive(Config)] | 定义配置结构 | #[derive(Config)] struct MyConfig {...} |
#[derive(ConfigCore)] | 框架内部配置(避免循环依赖) | 同 Config |
#[casbin] | 字段级权限控制(Writer 层过滤) | #[casbin] #[derive(Serialize)] struct User {...} |
#[remote] | HTTP 调用包装(自动刷新 Token) | #[remote] #[get("...")] async fn ... |
4. 聚合根定义
使用 #[derive(Aggregate)] 定义 DDD 聚合根:
use genies_derive::Aggregate;
use serde::{Deserialize, Serialize};
#[derive(Aggregate, Debug, Serialize, Deserialize, Clone)]
#[aggregate_type("me.tdcarefor.order.domain.Order")]
#[id_field(id)]
#[initialize_with_defaults]
pub struct Order {
pub id: String,
pub customer_id: String,
pub total_amount: f64,
pub status: String,
}
属性说明:
#[aggregate_type("...")] - 自定义聚合类型名称,默认为结构体名
#[id_field(field_name)] - 必需,指定聚合 ID 字段
#[initialize_with_defaults] - 自动实现 InitializeAggregate trait
生成的 Traits:
AggregateType - 提供 aggregate_type() 和 atype() 方法
WithAggregateId - 提供 aggregate_id() 方法
5. 领域事件
使用 #[derive(DomainEvent)] 标记领域事件:
use genies_derive::DomainEvent;
use serde::{Deserialize, Serialize};
#[derive(DomainEvent, Debug, Serialize, Deserialize, Default, Clone)]
#[event_type_version("V1")]
#[event_source("me.tdcarefor.order.domain.Order")]
#[event_type("me.tdcarefor.order.event.OrderCreated")]
pub struct OrderCreatedEvent {
pub order_id: String,
pub customer_id: String,
pub total_amount: f64,
}
#[derive(DomainEvent, Debug, Serialize, Deserialize, Clone)]
#[event_type_version("V1")]
#[event_source("me.tdcarefor.order.domain.Order")]
pub enum OrderEvent {
#[event_type("OrderCreated")]
Created { order_id: String },
#[event_type("OrderShipped")]
Shipped { tracking_number: String },
}
属性说明:
#[event_type("...")] - 事件类型标识
#[event_type_version("...")] - 事件版本,默认 "V0"
#[event_source("...")] - 事件来源(通常是聚合根全限定名)
6. 事件发布
使用 DomainEventPublisher 发布领域事件:
use genies::ddd::DomainEventPublisher::{publish, publishGenericDomainEvent};
use rbatis::executor::Executor;
async fn create_order(tx: &mut dyn Executor, order: &Order) {
let event = OrderCreatedEvent {
order_id: order.id.clone(),
customer_id: order.customer_id.clone(),
total_amount: order.total_amount,
};
publish(tx, order, Box::new(event)).await;
}
async fn send_notification(tx: &mut dyn Executor) {
let event = NotificationEvent { message: "Hello".to_string() };
publishGenericDomainEvent(tx, Box::new(event)).await;
}
消息表结构(message):
id - VARCHAR(36) 主键
destination - VARCHAR(255) 目标
headers - TEXT 消息头
payload - TEXT 消息体
published - INT 发布状态(0=未发布)
creation_time - BIGINT 创建时间
7. 事件消费
使用 #[topic] 宏订阅 Dapr 事件:
use genies_derive::topic;
use rbatis::executor::Executor;
#[topic(
name = "me.tdcarefor.order.domain.Order", // 订阅的 topic 名称
pubsub = "messagebus" // Dapr pubsub 组件名
)]
pub async fn on_order_created(
tx: &mut dyn Executor,
event: OrderCreatedEvent
) -> anyhow::Result<u64> {
log::info!("收到订单创建事件: {:?}", event);
Ok(0)
}
#[topic] 参数:
name - Topic 名称,默认使用聚合类型名
pubsub - PubSub 组件名,默认 "messagebus"
metadata - 额外元数据,格式 "key1=value1,key2=value2"
宏自动生成:
{fn_name}_hoop - Salvo Handler
{fn_name}_dapr - Dapr 订阅配置
{fn_name}_hoop_router - 路由注册
- 自动幂等性检查(基于 Redis)
- 自动事务管理和重试逻辑
注册消费者路由:
use genies::dapr::dapr_sub::dapr_sub;
pub fn event_router() -> Router {
Router::new().push(
Router::with_path("/daprsub/consumers")
.hoop(on_order_created_hoop)
.post(dapr_sub)
)
}
8. 配置管理
使用 #[derive(Config)] 定义配置结构:
use genies_derive::Config;
use serde::Deserialize;
#[derive(Config, Debug, Deserialize)]
pub struct MyAppConfig {
#[config(default = "my-service")]
pub server_name: String,
#[config(default = "8080")]
pub port: u32,
#[config(default = "")]
pub api_key: Option<String>,
#[config(default = "")]
pub allowed_origins: Vec<String>,
}
let config = MyAppConfig::from_sources("./application.yml").unwrap();
配置加载优先级: 默认值 → YAML 文件 → 环境变量
环境变量格式: 支持 field_name 和 FIELD_NAME 两种格式
生成的方法:
from_file(path) - 从文件加载
from_sources(path) - 从多源加载(推荐)
validate() - 验证配置
merge(other) - 合并配置
load_env() - 加载环境变量
9. 字段级权限(方案 C:Writer 层过滤)
使用 #[casbin] 宏实现 Casbin 字段级访问控制。该宏采用 Writer 层 JSON 树过滤 方案:
9.1 基本用法
use genies_derive::casbin;
use serde::{Deserialize, Serialize};
use salvo::oapi::ToSchema;
#[casbin]
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Employee {
pub id: u64,
pub name: String,
pub id_card_number: String,
pub base_salary: f64,
pub home_address: Address,
pub bank_accounts: Vec<BankAccount>,
}
#[endpoint]
async fn get_employee() -> Employee {
Employee {
id: 1,
name: "张三".into(),
id_card_number: "310101199501011234".into(),
base_salary: 25000.0,
home_address: Address { city: "上海".into(), street: "张江路".into() },
bank_accounts: vec![BankAccount { account_number: "622202xxx".into() }],
}
}
9.2 宏生成内容
#[casbin] 宏自动生成:
casbin_filter() 方法 — 对 JSON Value 树进行递归权限过滤
Writer trait 实现 — 在 HTTP 响应序列化时自动应用权限过滤
- 自动嵌套检测 — 非原始类型字段自动递归过滤(无需
#[casbin(nested)])
casbin_filter 方法签名:
pub fn casbin_filter(value: &mut serde_json::Value, enforcer: &casbin::Enforcer, subject: &str)
原始类型白名单(这些类型不会递归):
i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64,
isize, usize, String, str, bool, char
9.3 ⚠️ RespVO 包装时的字段过滤限制
#[casbin] 生成的 Writer 实现只在 handler 直接返回 Json<T> 或 T 时自动触发字段过滤。当返回 Json<RespVO<T>> 时,RespVO 的 Writer 接管序列化,不会自动调用内层类型 T 的字段过滤。
手动调用 casbin_filter 的代码模板:
use salvo::prelude::*;
use genies_core::RespVO;
#[endpoint]
async fn get_employee(depot: &mut Depot) -> Json<RespVO<Employee>> {
let employee = fetch_employee().await;
let enforcer = depot.obtain::<std::sync::Arc<casbin::Enforcer>>().ok();
let subject = depot.get::<String>("subject").ok();
let mut value = serde_json::to_value(&employee).unwrap();
if let (Some(e), Some(s)) = (&enforcer, &subject) {
Employee::casbin_filter(&mut value, e.as_ref(), s.as_str());
}
let filtered: Employee = serde_json::from_value(value).unwrap();
Json(RespVO::from(&Ok::<_, String>(filtered)))
}
参数类型转换注意事项:
e.as_ref() — Arc<Enforcer> → &Enforcer
s.as_str() — String → &str
类型注册要求: 类型必须出现在 #[endpoint] handler 的返回类型中(如 Json<RespVO<Employee>>),这样 extract_and_sync_schemas 才会将其注册到 auth_api_schemas 表,用户才能在 genies_auth_admin 管理界面中看到并设置字段权限。
9.4 中间件配置
权限过滤依赖 casbin_auth 中间件将 enforcer 和 subject 注入 Depot:
use genies_auth::{EnforcerManager, casbin_auth};
use salvo::prelude::*;
let mgr = Arc::new(EnforcerManager::new().await?);
let router = Router::new()
.hoop(genies::context::auth::salvo_auth)
.hoop(affix_state::inject(mgr.clone()))
.hoop(casbin_auth)
.push(Router::with_path("/api/employees").get(get_employee));
9.5 policy.csv 示例
# 字段级权限(黑名单模式:默认允许,deny 规则生效)
p, guest, Employee.id_card_number, read, deny
p, guest, Employee.base_salary, read, deny
p, guest, BankAccount.account_number, read, deny
# 角色继承
g, alice, hr_admin
g, bob, guest
# 资源分组
g2, Employee.base_salary, sensitive_data
g2, Employee.id_card_number, sensitive_data
10. 缓存服务
使用 CacheService 进行缓存操作:
use genies::context::CONTEXT;
use std::time::Duration;
async fn cache_example() -> Result<()> {
let cache = &CONTEXT.cache_service;
cache.set_string("key1", "value1").await?;
let value = cache.get_string("key1").await?;
cache.del_string("key1").await?;
cache.set_string_ex("session", "token", Some(Duration::from_secs(3600))).await?;
cache.set_json("user:1", &user).await?;
let user: User = cache.get_json("user:1").await?;
let ttl = cache.ttl("session").await?;
Ok(())
}
ICacheService 接口:
set_string(k, v) / get_string(k) / del_string(k)
set_string_ex(k, v, ex) - 带过期时间
ttl(k) - 获取剩余生存时间
切换后端(application.yml):
cache_type: "redis"
redis_url: "redis://:password@localhost:6379"
11. 远程服务调用 (#[remote])
使用 #[remote] 宏实现声明式跨微服务 HTTP 调用(类似 Java @FeignClient),自动管理 Keycloak Token。
11.1 目录结构
remote 模块独立于 DDD 四层架构,按外部服务分文件:
src/remote/
├── mod.rs # 模块导出
├── patient_service.rs # 患者服务远程调用
├── baseinfo_service.rs # 基础信息服务远程调用
└── his_service.rs # HIS 系统远程调用
在 lib.rs 中声明 pub mod remote;。
11.2 定义服务基础 URL
使用 config_gateway! 宏从 application.yml 的 gateway 配置读取服务基础路径:
use once_cell::sync::Lazy;
pub static BaseInfo: Lazy<String> = genies::config_gateway!("/baseinfo");
pub static Patient: Lazy<String> = genies::config_gateway!("/patient");
11.3 声明远程调用函数
use genies_derive::remote;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CustomConfigModel {
pub id: Option<String>,
pub name: Option<String>,
}
#[remote]
#[get(url = BaseInfo, path = "/customconfig/depttypename")]
pub async fn findByDepartmentIdAndTypeName(
#[query] departmentId: &str,
#[query] typeName: &str,
) -> feignhttp::Result<Vec<CustomConfigModel>> { impled!() }
#[remote]
#[get(url = Patient, path = "/api/patient/id/{id}")]
pub async fn get_patient_by_id(
#[path] id: &str,
) -> feignhttp::Result<PatientInfo> { impled!() }
#[remote]
#[post(url = Patient, path = "/api/patient/create")]
pub async fn create_patient(
#[body] patient: PatientCreateDTO,
) -> feignhttp::Result<String> { impled!() }
11.4 参数注解
| 注解 | 说明 | 示例 |
|---|
#[query] | 查询参数(?key=value) | #[query] name: &str |
#[path] | 路径参数(/api/{id}) | #[path] id: &str |
#[body] | 请求体(JSON) | #[body] body: UserDTO |
11.5 关键规则
url + path 分离:url 引用 Lazy<String> 静态变量,path 是具体端点路径
- 函数体:必须写
impled!()(feignhttp 宏要求)
- 参数类型:用
&str 而非 String
- 自动 Token 管理:
#[remote] 自动从 REMOTE_TOKEN 获取 Bearer token,遇 401 自动刷新 Keycloak token 并重试
- 生成两个函数:
{func_name}_feignhttp(带 Authorization 参数的原始版本)和 {func_name}(自动注入 token 的包装版本)
11.6 调用示例
async fn example() {
let patient = get_patient_by_id("123").await.unwrap();
let configs = findByDepartmentIdAndTypeName("dept1", "type1").await.unwrap();
}
12. 便捷宏
let rb = pool!();
User::select_by_id(rb, 1).await?;
let mut tx = tx_defer!();
User::insert(&mut tx, &user).await?;
tx.commit().await;
let user_dto = copy!(&user_entity, UserDTO);
13. K8s 健康检查
use genies::k8s::k8s_health_check;
use salvo::Router;
let router = Router::new()
.push(k8s_health_check());
端点:
GET /actuator/health/liveness - 存活探针
GET /actuator/health/readiness - 就绪探针
修改服务状态:
use genies::context::SERVICE_STATUS;
use std::ops::DerefMut;
let mut status = SERVICE_STATUS.lock().unwrap();
status.deref_mut().insert("readinessProbe".to_string(), false);
14. OpenAPI 集成
Genies 项目使用 Salvo 的 OpenAPI 能力自动生成 API 文档,并与 genies_auth 权限系统联动。
14.1 #[endpoint] vs #[handler]
所有 HTTP handler 必须使用 #[endpoint] 而非 #[handler]:
#[endpoint] — 自动将函数签名注册到 OpenAPI 文档,支持 Schema 同步到权限系统
#[handler] — 不生成 OpenAPI 文档,无法被 extract_and_sync_schemas 识别
两者函数签名完全相同,迁移只需替换宏名。
14.2 OpenAPI 参数提取方式
使用 #[endpoint] 时,推荐使用 OpenAPI 提取器替代手动从 Request 提取参数。提取器会自动在 OpenAPI 文档中生成参数描述:
#[handler] 旧写法 | #[endpoint] 新写法 | 说明 |
|---|
req.param::<T>("name") | name: PathParam<T> | 路径参数 /users/{id} |
req.query::<T>("name") | name: QueryParam<T, REQUIRED> | 查询参数 ?page=0 |
req.parse_json::<T>().await | body: JsonBody<T> | JSON 请求体 |
res.render(Json(data)) | -> Json<T> 返回值 | 响应体(自动生成响应 Schema) |
QueryParam<T, false> 表示可选参数,QueryParam<T, true> 表示必填参数。
完整示例:
use salvo::prelude::*;
use salvo::oapi::extract::*;
#[endpoint]
async fn find_by_id(id: PathParam<String>) -> Json<DeviceVO> {
let result = DeviceAppService::get_device(&id.into_inner()).await.unwrap();
Json(result)
}
#[endpoint]
async fn list(department_id: QueryParam<String, false>) -> Json<Vec<DeviceVO>> {
let dept = department_id.into_inner().unwrap_or_default();
let devices = DeviceAppService::list_by_dept(&dept).await;
Json(devices)
}
#[endpoint]
async fn add(body: JsonBody<DeviceBindRequest>) -> Json<RespVO<String>> {
let dto = body.into_inner();
match DeviceAppService::create(&dto).await {
Ok(id) => Json(RespVO::from(&Ok::<_, String>(id))),
Err(e) => Json(RespVO::<String>::from_error(&e)),
}
}
14.3 DTO 必须 derive ToSchema
所有请求/响应 DTO 必须添加 #[derive(ToSchema)],确保 OpenAPI 文档包含完整的 Schema 定义:
use salvo::oapi::ToSchema;
use serde::{Deserialize, Serialize};
use genies_derive::casbin;
#[derive(Debug, Deserialize, ToSchema)]
pub struct DeviceBindRequest {
pub serial_number: String,
}
#[casbin]
#[derive(Deserialize, ToSchema)]
pub struct DeviceVO {
pub id: String,
pub name: String,
pub serial_number: String,
}
14.4 Schema 同步到权限系统
启动时调用 extract_and_sync_schemas(&doc) 将 OpenAPI Schema 中的所有 DTO 字段信息同步到 auth_api_schemas 表,供 Admin UI 配置字段级权限:
use genies_auth::extract_and_sync_schemas;
use salvo::oapi::OpenApi;
let router = Router::new()
.push(Router::with_path("/api")
.push(business_router()));
let doc = OpenApi::new("my-service", "1.0.0").merge_router(&router);
extract_and_sync_schemas(&doc).await.ok();
14.5 与 #[casbin] 字段级权限的配合
完整链路:
- DTO 添加
#[derive(ToSchema)] → OpenAPI 文档包含字段定义
extract_and_sync_schemas(&doc) → 字段信息写入 auth_api_schemas 表
- 通过 Auth Admin API 或
genies_auth_admin 管理界面基于 Schema 配置字段级 deny 策略
- 响应 DTO 添加
#[casbin] → Writer 层在序列化时根据策略自动过滤字段
如果 DTO 不 derive ToSchema,extract_and_sync_schemas 无法提取其字段,管理界面中将看不到该 DTO 的字段列表。
注意: 当 handler 返回 Json<RespVO<T>> 时,Writer 自动过滤不生效,需手动调用 T::casbin_filter(),详见 9.3 节。
15. 典型开发流程
-
项目初始化
- 创建 Cargo.toml 添加依赖
- 创建 application.yml 配置文件
- 创建 model.conf 和 policy.csv(如需权限控制)
-
定义聚合根
- 使用
#[derive(Aggregate)] 定义领域模型
- 指定
#[id_field] 标识字段
-
定义领域事件
- 使用
#[derive(DomainEvent)] 定义事件
- 设置
event_type、event_source、event_type_version
-
实现业务逻辑
- 使用
tx_defer!() 管理事务
- 使用
DomainEventPublisher::publish() 发布事件
-
订阅事件
- 使用
#[topic] 宏定义事件处理器
- 使用
dapr_event_router() 自动注册路由
-
启动服务
use genies_auth::{EnforcerManager, casbin_auth, auth_router};
#[tokio::main]
async fn main() {
genies::config::log_config::init_log();
CONTEXT.init_mysql().await;
let mgr = Arc::new(EnforcerManager::new().await.unwrap());
let router = Router::new()
.push(genies::k8s::k8s_health_check())
.push(Router::with_path("/api")
.hoop(genies::context::auth::salvo_auth)
.hoop(affix_state::inject(mgr.clone()))
.hoop(casbin_auth)
.push(business_router())
.push(auth_router()))
.push(genies::dapr_event_router());
let acceptor = TcpListener::new(&CONTEXT.config.server_url).bind().await;
Server::new(acceptor).serve(router).await;
}
-
部署
- 配置 Dapr sidecar
- 设置 K8s 健康检查探针
- 通过环境变量覆盖配置
16. Auth 模块
genies_auth 提供完整的 Casbin 权限管理方案:
公开 API
| 导出项 | 说明 |
|---|
EnforcerManager | Casbin Enforcer 管理器,支持热更新 |
casbin_auth | API 接口权限中间件 |
casbin_filter_object | 嵌套字段过滤函数 |
auth_router() | Admin API 路由(需认证) |
auth_public_router() | 公开 API 路由(Token 端点) |
extract_and_sync_schemas() | OpenAPI Schema 同步到数据库 |
快速集成
use genies_auth::{EnforcerManager, casbin_auth, auth_router};
use std::sync::Arc;
let mgr = Arc::new(EnforcerManager::new().await?);
let router = Router::new()
.hoop(affix_state::inject(mgr.clone()))
.hoop(casbin_auth)
.push(auth_router());
注意: 管理界面(Admin UI)已迁移到独立的 genies_auth_admin crate,详见 auth-admin 的文档。
17. ID 生成
Genies 提供了统一的雪花 ID 生成器,用于新项目生成所有业务 ID。
规则:新项目统一使用雪花 ID(genies::next_id())作为实体/聚合根 ID。 雪花 ID 是 Java UUID.randomUUID() 的功能平替,用于生成分布式唯一 ID。使用方式:genies::next_id()(业务代码)或 genies_core::id_gen::next_id()(核心库)。从 Java 迁移时,若已有数据使用 UUID 且雪花 ID 无法兼容,可继续使用 UUID 保持数据兼容性。新功能开发不应引入 uuid crate。
用法
let id = genies::next_id();
ward.id = Some(genies::next_id());
工作原理
- 使用
rs-snowflake(64 位分布式雪花算法)
- Worker ID 在启动时自动解析:Redis 槽位 → K8s HOSTNAME → 配置项 → 兜底值
- ID 以
String 类型返回,避免 JavaScript 精度丢失问题
- 通过
Mutex<SnowflakeIdBucket> 包装在 OnceLock 中,保证线程安全
从 UUID 迁移
替换所有以下写法:
use uuid::Uuid;
let id = Uuid::new_v4().to_string();
let id = genies::next_id();
迁移完成后,若无已有 UUID 数据需要兼容,可从 Cargo.toml 依赖中移除 uuid。