sz-rust-core 1.1.0

SZ-Rust 核心库:HTTP 服务器、路由、控制器、中间件,对标 ThinkPHP 8
Documentation
//! 共享 Schema 模型定义 — sys_users / sys_permissions / sys_events。
//!
//! 所有模型含 `tenant_id` 字段实现多租户隔离。
//! 敏感字段标注 `#[serde(skip_serializing)]`(铁律 7)。

use serde::{Deserialize, Serialize};

/// 系统用户(共享 Schema)。
///
/// `extra` 为 JSON 类型,供插件存储扩展字段。
/// `password_hash` 为敏感字段,序列化时跳过(铁律 7)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SysUser {
    pub id: i64,
    pub tenant_id: i64,
    pub username: String,
    pub display_name: String,
    #[serde(skip_serializing)]
    pub password_hash: String,
    pub email: Option<String>,
    pub phone: Option<String>,
    pub status: String,
    pub extra: serde_json::Value,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

/// 系统权限(共享 Schema)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SysPermission {
    pub id: i64,
    pub tenant_id: i64,
    pub name: String,
    pub description: String,
    pub resource: String,
    pub action: String,
    pub conditions: Option<serde_json::Value>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

/// 系统事件(共享 Schema)。
///
/// 事件总线持久化事件记录,支持至少一次投递。
/// `delivered`/`delivered_at`/`retry_count` 跟踪投递状态。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SysEvent {
    pub id: i64,
    pub tenant_id: i64,
    pub event_type: String,
    pub source_plugin: String,
    pub payload: serde_json::Value,
    pub delivered: bool,
    pub delivered_at: Option<chrono::DateTime<chrono::Utc>>,
    pub retry_count: i32,
    pub max_retries: i32,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

impl SysUser {
    pub fn table_name() -> &'static str {
        "sys_users"
    }
}

impl SysPermission {
    pub fn table_name() -> &'static str {
        "sys_permissions"
    }
}

impl SysEvent {
    pub fn table_name() -> &'static str {
        "sys_events"
    }

    /// 创建新事件
    pub fn new(
        tenant_id: i64,
        event_type: impl Into<String>,
        source_plugin: impl Into<String>,
        payload: serde_json::Value,
    ) -> Self {
        let now = chrono::Utc::now();
        Self {
            id: 0,
            tenant_id,
            event_type: event_type.into(),
            source_plugin: source_plugin.into(),
            payload,
            delivered: false,
            delivered_at: None,
            retry_count: 0,
            max_retries: 3,
            created_at: now,
            updated_at: now,
        }
    }

    /// 标记已投递
    pub fn mark_delivered(&mut self) {
        self.delivered = true;
        self.delivered_at = Some(chrono::Utc::now());
        self.updated_at = chrono::Utc::now();
    }

    /// 增加重试计数
    pub fn increment_retry(&mut self) {
        self.retry_count += 1;
        self.updated_at = chrono::Utc::now();
    }

    /// 是否已耗尽重试次数
    pub fn is_exhausted(&self) -> bool {
        self.retry_count >= self.max_retries
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sys_event_new() {
        let event = SysEvent::new(1, "order.created", "shop", serde_json::json!({"id": 1}));
        assert_eq!(event.tenant_id, 1);
        assert_eq!(event.event_type, "order.created");
        assert!(!event.delivered);
        assert_eq!(event.retry_count, 0);
        assert_eq!(event.max_retries, 3);
    }

    #[test]
    fn test_mark_delivered() {
        let mut event = SysEvent::new(1, "test", "test", serde_json::json!({}));
        event.mark_delivered();
        assert!(event.delivered);
        assert!(event.delivered_at.is_some());
    }

    #[test]
    fn test_retry_exhaustion() {
        let mut event = SysEvent::new(1, "test", "test", serde_json::json!({}));
        assert!(!event.is_exhausted());
        event.increment_retry();
        event.increment_retry();
        event.increment_retry();
        assert!(event.is_exhausted());
    }

    #[test]
    fn test_password_hash_skip_serializing() {
        let user = SysUser {
            id: 1,
            tenant_id: 1,
            username: "admin".to_string(),
            display_name: "Admin".to_string(),
            password_hash: "secret_hash".to_string(),
            email: None,
            phone: None,
            status: "active".to_string(),
            extra: serde_json::json!({}),
            created_at: chrono::Utc::now(),
            updated_at: chrono::Utc::now(),
        };
        let json = serde_json::to_string(&user).expect("序列化失败");
        assert!(!json.contains("password_hash"), "敏感字段不应出现在 JSON 中");
        assert!(!json.contains("secret_hash"), "敏感值不应出现在 JSON 中");
    }
}