子系统与自定义功能开发
[!TIP] 完整系统开发教程:如果你希望从宏观架构、控制器路由、数据持久化到实战案例全面了解如何构建完整系统,请参阅全新的 系统开发核心教程总览。
Foundry 提倡高内聚、低耦合的模块化设计。业务开发者可以将独立业务组织为 子系统 (Subsystem) 与 生命周期钩子 (Mutation Hook)。
1. 业务子系统目录结构
Section titled “1. 业务子系统目录结构”在用户业务工程中,子系统通常放置在 src/systems/<slug>/:
src/systems/blog/├── mod.rs # SubsystemModule 特征实现入口├── controllers/ # Axum HTTP 控制器 (挂载至 /api/v1/s/blog/ext/*)│ └── mod.rs├── logic/ # 纯业务领域服务 (包含数据库读写)│ └── mod.rs├── dto/ # 请求与响应 DTO 结构体及 validator 校验注解│ └── mod.rs└── custom_pages/ # 自定义 Admin 运营看板 (HTML, React, Vue) └── article_editor.html2. 编写子系统标准步骤 (三层架构模式)
Section titled “2. 编写子系统标准步骤 (三层架构模式)”步骤 1: 定义 DTO 与参数校验 (dto/mod.rs)
Section titled “步骤 1: 定义 DTO 与参数校验 (dto/mod.rs)”use serde::{Deserialize, Serialize};use validator::Validate;
#[derive(Debug, Clone, Deserialize, Validate)]pub struct CreateArticleRequest { #[validate(length(min = 3, max = 120, message = "标题长度须在 3-120 字符之间"))] pub title: String, #[validate(length(min = 10, message = "正文至少需要 10 个字符"))] pub content: String, pub author: String,}
#[derive(Debug, Clone, Serialize)]pub struct ArticleResponse { pub id: i64, pub title: String, pub content: String, pub author: String,}步骤 2: 编写领域服务与数据库交互 (logic/mod.rs)
Section titled “步骤 2: 编写领域服务与数据库交互 (logic/mod.rs)”use crate::systems::blog::dto::{CreateArticleRequest, ArticleResponse};use foundry::prelude::*;
pub struct ArticleService;
impl ArticleService { pub async fn create_article( ctx: &SystemContext, db: &Database, req: CreateArticleRequest, ) -> AppResult<ArticleResponse> { // 方式 A: 使用多数据库透明的动态模型持久化 let record = db.records().create( &ctx.system_slug, "articles", serde_json::json!({ "title": req.title, "content": req.content, "author": req.author }) ).await?;
Ok(ArticleResponse { id: record.id, title: req.title, content: req.content, author: req.author, }) }}步骤 3: 编写 HTTP 控制器并提取 Database 门面 (controllers/mod.rs)
Section titled “步骤 3: 编写 HTTP 控制器并提取 Database 门面 (controllers/mod.rs)”use axum::{extract::Extension, routing::post, Json, Router};use foundry::prelude::*;use validator::Validate;use crate::systems::blog::dto::{CreateArticleRequest, ArticleResponse};use crate::systems::blog::logic::ArticleService;
pub fn build_routes() -> Router { Router::new().route("/articles", post(handle_create_article))}
pub async fn handle_create_article( Extension(ctx): Extension<SystemContext>, Extension(db): Extension<Database>, Json(payload): Json<CreateArticleRequest>,) -> AppResult<Json<ApiResponse<ArticleResponse>>> { payload.validate()?; let article = ArticleService::create_article(&ctx, &db, payload).await?; Ok(Json(ApiResponse::created(article)))}步骤 4: 实现 SubsystemModule 特征 (mod.rs)
Section titled “步骤 4: 实现 SubsystemModule 特征 (mod.rs)”pub mod controllers;pub mod dto;pub mod logic;
use foundry::prelude::*;use std::path::PathBuf;
pub struct BlogSubsystem;
impl SubsystemModule for BlogSubsystem { fn slug(&self) -> &'static str { "blog" }
fn display_name(&self) -> &'static str { "博客与内容管理系统" }
fn description(&self) -> &'static str { "文章内容创作、标签分类与发布工作流" }
fn register_routes(&self, router: Router) -> Router { let mut r = router.merge(controllers::build_routes());
// 挂载自定义 Admin 静态页面 let custom_dir = PathBuf::from("src/systems/blog/custom_pages"); if custom_dir.exists() { r = r.nest_service("/custom-pages", ServeDir::new(custom_dir)); } r }
fn custom_admin_pages(&self) -> Vec<CustomAdminPageSpec> { vec![CustomAdminPageSpec { key: "article_editor".to_string(), title: "文章创作工作室".to_string(), icon: "FileEdit".to_string(), page_type: "iframe".to_string(), entry: "/api/v1/s/blog/ext/custom-pages/article_editor.html".to_string(), required_role: None, // 如需限制超管可见:Some("super_admin".to_string()) }] }}3. 在应用启动器中注册
Section titled “3. 在应用启动器中注册”在用户应用 main.rs 中:
use foundry::prelude::*;use systems::BlogSubsystem;
#[foundry::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let app = FoundryApp::builder() .register_subsystem(BlogSubsystem) .build() .await?;
app.run().await?; Ok(())}4. 自定义 Admin 运营看板与 SDK Bridge 通信
Section titled “4. 自定义 Admin 运营看板与 SDK Bridge 通信”嵌入在 Foundry Admin 外壳中的页面会自动通过 postMessage 接收管理员登录凭据和主题:
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <title>文章创作工作室</title> <script src="https://cdn.tailwindcss.com"></script></head><body class="bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 min-h-screen p-6"> <div class="max-w-4xl mx-auto space-y-4"> <h1 class="text-xl font-bold text-indigo-600 dark:text-indigo-400">📝 文章创作工作室</h1> <p id="admin-badge" class="text-xs text-slate-500">正在连接 Foundry Admin 外壳...</p> </div> <script> let authContext = null;
// 1. 监听来自 Admin Shell 的初始化广播 window.addEventListener('message', function(event) { if (event.data?.type === 'FOUNDRY_INIT') { authContext = event.data.payload; if (authContext.theme === 'dark') document.documentElement.classList.add('dark'); document.getElementById('admin-badge').innerText = `当前登录: ${authContext.admin.username} (${authContext.admin.role}) | 子系统: ${authContext.subsystemSlug}`; } });
// 2. 向外壳发送 Toast 通知 function showToast(message, level = 'success') { window.parent.postMessage({ type: 'FOUNDRY_TOAST', payload: { message, level } }, '*'); } </script></body></html>5. 生命周期变更钩子 (Mutation Hooks)
Section titled “5. 生命周期变更钩子 (Mutation Hooks)”通过实现 MutationHook 特征拦截动态数据模型的写操作:
use foundry::prelude::*;use serde_json::Value;
pub struct AuditHook;
#[async_trait]impl MutationHook for AuditHook { async fn before_create( &self, ctx: &SystemContext, model_slug: &str, data: &mut Value, ) -> AppResult<()> { tracing::info!("在子系统 '{}' 创建模型 '{}' 记录", ctx.system_slug, model_slug); Ok(()) }
async fn after_create( &self, ctx: &SystemContext, model_slug: &str, record_id: i64, _data: &Value, ) -> AppResult<()> { tracing::info!("记录 {} 创建完成", record_id); Ok(()) }}在启动构造器中注入:
let app = FoundryApp::builder() .register_hook(AuditHook) .build() .await?;