Files
erp/docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md
iven eb856b1d73 feat: 初始化ERP平台底座项目结构
- 添加基础crate结构(erp-core, erp-common)
- 实现核心模块trait和事件总线
- 配置Docker开发环境(PostgreSQL+Redis)
- 添加Tauri桌面端基础框架
- 设置CI/CD工作流
- 编写项目协作规范文档(CLAUDE.md)
2026-04-10 23:40:38 +08:00

33 KiB
Raw Blame History

ERP 平台底座实施计划

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 构建一个模块化的 ERP 平台底座,包含身份权限、工作流引擎、消息中心、系统配置四大核心模块。

Architecture: 模块化单体渐进式Rust workspace 多 crate 结构,模块间通过事件总线和共享 trait 通信。桌面端使用 Tauri 2 + React 18。

Tech Stack: Rust (Axum 0.8 + SeaORM + Tokio) | Vite + React 18 + TypeScript + Ant Design 5 | PostgreSQL 16+ | Redis 7+

Spec: docs/superpowers/specs/2026-04-10-erp-platform-base-design.md


Chunk 1: Phase 1 - 基础设施

Task 1: 初始化 Rust Workspace

Files:

  • Create: Cargo.toml (workspace root)

  • Create: crates/erp-core/Cargo.toml

  • Create: crates/erp-core/src/lib.rs

  • Create: crates/erp-common/Cargo.toml

  • Create: crates/erp-common/src/lib.rs

  • Create: crates/erp-server/Cargo.toml

  • Create: crates/erp-server/src/main.rs

  • Step 1: 创建 workspace root Cargo.toml

[workspace]
resolver = "2"
members = [
    "crates/erp-core",
    "crates/erp-common",
    "crates/erp-server",
    "crates/erp-auth",
    "crates/erp-workflow",
    "crates/erp-message",
    "crates/erp-config",
]

[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"

[workspace.dependencies]
# Async
tokio = { version = "1", features = ["full"] }

# Web
axum = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }

# Database
sea-orm = { version = "1.1", features = [
    "sqlx-postgres", "runtime-tokio-rustls", "macros", "with-uuid", "with-chrono", "with-json"
] }
sea-orm-migration = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-rustls"] }

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# UUID & Time
uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

# Error handling
thiserror = "2"
anyhow = "1"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

# Config
config = "0.14"

# Redis
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }

# JWT
jsonwebtoken = "9"

# Password hashing
argon2 = "0.5"

# API docs
utoipa = { version = "5", features = ["axum_extras", "uuid", "chrono"] }
utoipa-swagger-ui = { version = "8", features = ["axum"] }

# Validation
validator = { version = "0.19", features = ["derive"] }

# Internal crates
erp-core = { path = "crates/erp-core" }
erp-common = { path = "crates/erp-common" }
erp-auth = { path = "crates/erp-auth" }
erp-workflow = { path = "crates/erp-workflow" }
erp-message = { path = "crates/erp-message" }
erp-config = { path = "crates/erp-config" }
  • Step 2: 创建 erp-core crate

crates/erp-core/Cargo.toml:

[package]
name = "erp-core"
version.workspace = true
edition.workspace = true

[dependencies]
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
chrono.workspace = true
thiserror.workspace = true
anyhow.workspace = true
tracing.workspace = true
axum.workspace = true
sea-orm.workspace = true

crates/erp-core/src/lib.rs:

pub mod error;
pub mod events;
pub mod module;
pub mod types;
  • Step 3: 创建 erp-common crate

crates/erp-common/Cargo.toml:

[package]
name = "erp-common"
version.workspace = true
edition.workspace = true

[dependencies]
uuid.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true

crates/erp-common/src/lib.rs:

pub mod utils;
  • Step 4: 创建 erp-server crate (空壳)

crates/erp-server/Cargo.toml:

[package]
name = "erp-server"
version.workspace = true
edition.workspace = true

[[bin]]
name = "erp-server"
path = "src/main.rs"

[dependencies]
erp-core.workspace = true
erp-common.workspace = true
tokio.workspace = true
axum.workspace = true
tower.workspace = true
tower-http.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
config.workspace = true
sea-orm.workspace = true
redis.workspace = true
utoipa.workspace = true
utoipa-swagger-ui.workspace = true
serde_json.workspace = true

crates/erp-server/src/main.rs:

use axum::Router;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize tracing
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env().add_directive("info".parse()?))
        .json()
        .init();

    tracing::info!("ERP Server starting...");

    // TODO: Load config
    // TODO: Connect to database
    // TODO: Connect to Redis
    // TODO: Register modules
    // TODO: Build router

    let app = Router::new()
        .fallback(|| async { axum::Json(serde_json::json!({"error": "Not found"})) });

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    tracing::info!("Server listening on {}", listener.local_addr()?);
    axum::serve(listener, app).await?;

    Ok(())
}
  • Step 5: 创建占位 crate 目录

为后续模块创建空的 crate 目录:

mkdir -p crates/erp-auth/src
mkdir -p crates/erp-workflow/src
mkdir -p crates/erp-message/src
mkdir -p crates/erp-config/src

每个放一个最小 Cargo.tomlsrc/lib.rs(内容类似 erp-core 但依赖 erp-core

  • Step 6: 验证编译

Run: cargo check Expected: 编译通过,无错误

  • Step 7: 初始化 Git 并提交
git init
git add -A
git commit -m "feat: initialize Rust workspace with core, common, server crates"

Task 2: erp-core 错误处理

Files:

  • Create: crates/erp-core/src/error.rs

  • Create: crates/erp-core/src/types.rs

  • Step 1: 编写错误类型测试

Create crates/erp-core/src/error.rs:

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;

/// 统一错误响应格式
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<serde_json::Value>,
}

/// 平台级错误类型
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("资源未找到: {0}")]
    NotFound(String),

    #[error("验证失败: {0}")]
    Validation(String),

    #[error("未授权")]
    Unauthorized,

    #[error("禁止访问: {0}")]
    Forbidden(String),

    #[error("冲突: {0}")]
    Conflict(String),

    #[error("内部错误: {0}")]
    Internal(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "未授权".to_string()),
            AppError::Forbidden(_) => (StatusCode::FORBIDDEN, self.to_string()),
            AppError::Conflict(_) => (StatusCode::CONFLICT, self.to_string()),
            AppError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string()),
        };

        let body = ErrorResponse {
            error: status.canonical_reason().unwrap_or("Error").to_string(),
            message,
            details: None,
        };

        (status, Json(body)).into_response()
    }
}

/// 方便从 anyhow 错误转换
impl From<anyhow::Error> for AppError {
    fn from(err: anyhow::Error) -> Self {
        AppError::Internal(err.to_string())
    }
}

/// 方便从 SeaORM 错误转换
impl From<sea_orm::DbErr> for AppError {
    fn from(err: sea_orm::DbErr) -> Self {
        match err {
            sea_orm::DbErr::RecordNotFound(msg) => AppError::NotFound(msg),
            sea_orm::DbErr::Query(sea_orm::RuntimeErr::SqlxError(e))
                if e.to_string().contains("duplicate key") =>
            {
                AppError::Conflict("记录已存在".to_string())
            }
            _ => AppError::Internal(err.to_string()),
        }
    }
}

pub type AppResult<T> = Result<T, AppError>;
  • Step 2: 编写共享类型

Create crates/erp-core/src/types.rs:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// 所有数据库实体的公共字段
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseFields {
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub created_by: Uuid,
    pub updated_by: Uuid,
    pub deleted_at: Option<DateTime<Utc>>,
    pub version: i32,
}

/// 分页请求
#[derive(Debug, Deserialize)]
pub struct Pagination {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
}

impl Pagination {
    pub fn offset(&self) -> u64 {
        (self.page.unwrap_or(1).saturating_sub(1)) * self.limit()
    }

    pub fn limit(&self) -> u64 {
        self.page_size.unwrap_or(20).min(100)
    }
}

/// 分页响应
#[derive(Debug, Serialize)]
pub struct PaginatedResponse<T> {
    pub data: Vec<T>,
    pub total: u64,
    pub page: u64,
    pub page_size: u64,
    pub total_pages: u64,
}

/// API 统一响应
#[derive(Debug, Serialize)]
pub struct ApiResponse<T: Serialize> {
    pub success: bool,
    pub data: Option<T>,
    pub message: Option<String>,
}

impl<T: Serialize> ApiResponse<T> {
    pub fn ok(data: T) -> Self {
        Self {
            success: true,
            data: Some(data),
            message: None,
        }
    }
}

/// 租户上下文(中间件注入)
#[derive(Debug, Clone)]
pub struct TenantContext {
    pub tenant_id: Uuid,
    pub user_id: Uuid,
    pub roles: Vec<String>,
    pub permissions: Vec<String>,
}
  • Step 3: 验证编译

Run: cargo check -p erp-core Expected: 编译通过

  • Step 4: 提交
git add crates/erp-core/src/
git commit -m "feat(erp-core): add error types, shared types, pagination"

Task 3: 事件总线

Files:

  • Create: crates/erp-core/src/events.rs

  • Step 1: 编写事件总线

Create crates/erp-core/src/events.rs:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::{error, info};
use uuid::Uuid;

/// 领域事件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainEvent {
    pub id: Uuid,
    pub event_type: String,
    pub tenant_id: Uuid,
    pub payload: serde_json::Value,
    pub timestamp: DateTime<Utc>,
    pub correlation_id: Uuid,
}

impl DomainEvent {
    pub fn new(event_type: impl Into<String>, tenant_id: Uuid, payload: serde_json::Value) -> Self {
        Self {
            id: Uuid::now_v7(),
            event_type: event_type.into(),
            tenant_id,
            payload,
            timestamp: Utc::now(),
            correlation_id: Uuid::now_v7(),
        }
    }
}

/// 事件处理器 trait
pub trait EventHandler: Send + Sync {
    fn event_types(&self) -> Vec<String>;
    fn handle(&self, event: &DomainEvent) -> impl std::future::Future<Output = anyhow::Result<()>> + Send;
}

/// 进程内事件总线
#[derive(Clone)]
pub struct EventBus {
    sender: broadcast::Sender<DomainEvent>,
}

impl EventBus {
    pub fn new(capacity: usize) -> Self {
        let (sender, _) = broadcast::channel(capacity);
        Self { sender }
    }

    /// 发布事件
    pub fn publish(&self, event: DomainEvent) {
        info!(event_type = %event.event_type, event_id = %event.id, "Event published");
        if let Err(e) = self.sender.send(event) {
            error!("Failed to publish event: {}", e);
        }
    }

    /// 订阅所有事件,返回接收端
    pub fn subscribe(&self) -> broadcast::Receiver<DomainEvent> {
        self.sender.subscribe()
    }
}
  • Step 2: 验证编译

Run: cargo check -p erp-core Expected: 编译通过

  • Step 3: 提交
git add crates/erp-core/src/events.rs
git commit -m "feat(erp-core): add domain event bus with broadcast channel"

Task 4: ErpModule trait + 模块注册

Files:

  • Create: crates/erp-core/src/module.rs

  • Step 1: 编写 ErpModule trait

Create crates/erp-core/src/module.rs:

use axum::Router;
use uuid::Uuid;
use crate::events::EventBus;
use crate::error::AppResult;

/// 模块注册接口
/// 所有业务模块Auth, Workflow, Message, Config, 行业模块)都实现此 trait
pub trait ErpModule: Send + Sync {
    /// 模块名称(唯一标识)
    fn name(&self) -> &str;

    /// 模块版本
    fn version(&self) -> &str {
        env!("CARGO_PKG_VERSION")
    }

    /// 依赖的其他模块名称
    fn dependencies(&self) -> Vec<&str> {
        vec![]
    }

    /// 注册 Axum 路由
    fn register_routes(&self, router: Router) -> Router;

    /// 注册事件处理器
    fn register_event_handlers(&self, _bus: &EventBus) {}

    /// 租户创建时的初始化钩子
    fn on_tenant_created(&self, _tenant_id: Uuid) -> impl std::future::Future<Output = AppResult<()>> + Send {
        async { Ok(()) }
    }

    /// 租户删除时的清理钩子
    fn on_tenant_deleted(&self, _tenant_id: Uuid) -> impl std::future::Future<Output = AppResult<()>> + Send {
        async { Ok(()) }
    }
}

/// 模块注册器
pub struct ModuleRegistry {
    modules: Vec<Box<dyn ErpModule>>,
}

impl ModuleRegistry {
    pub fn new() -> Self {
        Self { modules: vec![] }
    }

    pub fn register(&mut self, module: Box<dyn ErpModule>) {
        tracing::info!(module = module.name(), version = module.version(), "Module registered");
        self.modules.push(module);
    }

    pub fn build_router(&self, base: Router) -> Router {
        self.modules.iter().fold(base, |router, m| m.register_routes(router))
    }

    pub fn register_handlers(&self, bus: &EventBus) {
        for module in &self.modules {
            module.register_event_handlers(bus);
        }
    }

    pub fn modules(&self) -> &[Box<dyn ErpModule>] {
        &self.modules
    }
}
  • Step 2: 验证编译

Run: cargo check -p erp-core Expected: 编译通过

  • Step 3: 提交
git add crates/erp-core/src/module.rs
git commit -m "feat(erp-core): add ErpModule trait and ModuleRegistry"

Task 5: Docker 开发环境

Files:

  • Create: docker/docker-compose.yml

  • Create: docker/.env.example

  • Create: crates/erp-server/config/default.toml

  • Step 1: 创建 Docker Compose

Create docker/docker-compose.yml:

version: "3.8"

services:
  postgres:
    image: postgres:16-alpine
    container_name: erp-postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-erp}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-erp_dev_2024}
      POSTGRES_DB: ${POSTGRES_DB:-erp}
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-erp}"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: erp-redis
    ports:
      - "${REDIS_PORT:-6379}:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
  redis_data:
  • Step 2: 创建环境变量示例

Create docker/.env.example:

POSTGRES_USER=erp
POSTGRES_PASSWORD=erp_dev_2024
POSTGRES_DB=erp
POSTGRES_PORT=5432
REDIS_PORT=6379
  • Step 3: 创建服务配置文件

Create crates/erp-server/config/default.toml:

[server]
host = "0.0.0.0"
port = 3000

[database]
url = "postgres://erp:erp_dev_2024@localhost:5432/erp"
max_connections = 20
min_connections = 5

[redis]
url = "redis://localhost:6379"

[jwt]
secret = "change-me-in-production"
access_token_ttl = "15m"
refresh_token_ttl = "7d"

[log]
level = "info"
  • Step 4: 启动 Docker 服务验证

Run: cd docker && docker compose up -d Expected: PostgreSQL 和 Redis 容器正常运行

Run: docker compose -f docker/docker-compose.yml ps Expected: 两个服务都是 healthy 状态

  • Step 5: 提交
git add docker/ crates/erp-server/config/
git commit -m "feat: add Docker dev environment and server config"

Task 6: 数据库连接 + SeaORM 配置

Files:

  • Create: crates/erp-server/src/config.rs

  • Create: crates/erp-server/src/db.rs

  • Modify: crates/erp-server/src/main.rs

  • Step 1: 编写配置加载

Create crates/erp-server/src/config.rs:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct AppConfig {
    pub server: ServerConfig,
    pub database: DatabaseConfig,
    pub redis: RedisConfig,
    pub jwt: JwtConfig,
    pub log: LogConfig,
}

#[derive(Debug, Deserialize)]
pub struct ServerConfig {
    pub host: String,
    pub port: u16,
}

#[derive(Debug, Deserialize)]
pub struct DatabaseConfig {
    pub url: String,
    pub max_connections: u32,
    pub min_connections: u32,
}

#[derive(Debug, Deserialize)]
pub struct RedisConfig {
    pub url: String,
}

#[derive(Debug, Deserialize)]
pub struct JwtConfig {
    pub secret: String,
    pub access_token_ttl: String,
    pub refresh_token_ttl: String,
}

#[derive(Debug, Deserialize)]
pub struct LogConfig {
    pub level: String,
}

impl AppConfig {
    pub fn load() -> anyhow::Result<Self> {
        let config = config::Config::builder()
            .add_source(config::File::with_name("config/default"))
            .add_source(config::Environment::with_prefix("ERP").separator("__"))
            .build()?;
        Ok(config.try_deserialize()?)
    }
}
  • Step 2: 编写数据库连接

Create crates/erp-server/src/db.rs:

use sea_orm::{Database, DatabaseConnection};
use sea_orm::ConnectOptions;
use std::time::Duration;
use crate::config::DatabaseConfig;

pub async fn connect(config: &DatabaseConfig) -> anyhow::Result<DatabaseConnection> {
    let mut opt = ConnectOptions::new(&config.url);
    opt.max_connections(config.max_connections)
        .min_connections(config.min_connections)
        .connect_timeout(Duration::from_secs(10))
        .idle_timeout(Duration::from_secs(600));

    let db = Database::connect(opt).await?;
    tracing::info!("Database connected successfully");
    Ok(db)
}
  • Step 3: 更新 main.rs 整合配置和数据库

Update crates/erp-server/src/main.rs:

mod config;
mod db;

use axum::Router;
use config::AppConfig;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Load config
    let config = AppConfig::load()?;

    // Initialize tracing
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new(&config.log.level))
        )
        .json()
        .init();

    tracing::info!("ERP Server starting...");

    // Connect to database
    let db = db::connect(&config.database).await?;

    // Connect to Redis
    let redis_client = redis::Client::open(&config.redis.url[..])?;
    tracing::info!("Redis client created");

    // Build app
    let app = Router::new()
        .fallback(|| async { axum::Json(serde_json::json!({"error": "Not found"})) });

    let addr = format!("{}:{}", config.server.host, config.server.port);
    let listener = tokio::net::TcpListener::bind(&addr).await?;
    tracing::info!("Server listening on {}", addr);
    axum::serve(listener, app).await?;

    Ok(())
}
  • Step 4: 验证编译和启动

Run: cargo check -p erp-server Expected: 编译通过

Run: cd crates/erp-server && cargo run Expected: 输出 "Database connected successfully" 和 "Server listening on 0.0.0.0:3000"

  • Step 5: 提交
git add crates/erp-server/src/
git commit -m "feat(server): add config loading, database and Redis connections"

Task 7: SeaORM 数据库迁移框架

Files:

  • Create: crates/erp-server/migration/Cargo.toml

  • Create: crates/erp-server/migration/src/lib.rs

  • Create: crates/erp-server/migration/src/m20260410_000001_create_tenant.rs

  • Step 1: 创建迁移 crate

crates/erp-server/migration/Cargo.toml:

[package]
name = "erp-server-migration"
version = "0.1.0"
edition = "2024"

[dependencies]
sea-orm-migration.workspace = true
tokio.workspace = true

crates/erp-server/migration/src/lib.rs:

pub use sea_orm_migration::prelude::*;

mod m20260410_000001_create_tenant;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
        vec![
            Box::new(m20260410_000001_create_tenant::Migration),
        ]
    }
}
  • Step 2: 编写首个迁移 - tenants 表

Create crates/erp-server/migration/src/m20260410_000001_create_tenant.rs:

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Tenant::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Tenant::Id)
                            .uuid()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Tenant::Name).string().not_null())
                    .col(ColumnDef::new(Tenant::Code).string().not_null().unique())
                    .col(ColumnDef::new(Tenant::Status).string().not_null().default("active"))
                    .col(ColumnDef::new(Tenant::Settings).json().null())
                    .col(ColumnDef::new(Tenant::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                    .col(ColumnDef::new(Tenant::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
                    .col(ColumnDef::new(Tenant::DeletedAt).timestamp_with_time_zone().null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Tenant::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
enum Tenant {
    Table,
    Id,
    Name,
    Code,
    Status,
    Settings,
    CreatedAt,
    UpdatedAt,
    DeletedAt,
}
  • Step 3: 在 main.rs 中运行迁移

crates/erp-server/src/main.rs 中数据库连接后添加:

// Run migrations
use erp_server_migration::Migrator;
Migrator::up(&db, None).await?;
tracing::info!("Database migrations applied");

同时将 crates/erp-server/migration 加入 workspace members。

  • Step 4: 验证迁移

Run: cd docker && docker compose up -d Run: cd crates/erp-server && cargo run Expected: 迁移成功,日志显示 "Database migrations applied"

Run: docker exec erp-postgres psql -U erp -c "\dt" Expected: 可以看到 tenant

  • Step 5: 提交
git add crates/erp-server/migration/ crates/erp-server/src/main.rs Cargo.toml
git commit -m "feat(server): add SeaORM migration framework with tenants table"

Task 8: 初始化 Web 前端 (Vite + React)

Files:

  • Create: apps/web/ (Vite + React 项目)

  • Step 1: 创建 Vite + React 项目

cd g:/erp
pnpm create vite apps/web --template react-ts
cd apps/web
pnpm install
  • Step 2: 安装 UI 依赖
cd apps/web
pnpm add antd @ant-design/icons zustand react-router-dom axios
pnpm add -D tailwindcss @tailwindcss/vite
  • Step 3: 配置 TailwindCSS + 开发代理

Update apps/web/vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
      "/ws": {
        target: "ws://localhost:3000",
        ws: true,
      },
    },
  },
});
  • Step 4: 验证前端启动

Run: cd apps/web && pnpm dev Expected: 浏览器打开 http://localhost:5173,显示默认 React 页面

  • Step 5: 提交
git add apps/web/
git commit -m "feat(web): initialize Vite + React + TypeScript + Ant Design"

Task 9: Web 前端基础布局

Files:

  • Create: apps/web/src/layouts/MainLayout.tsx

  • Create: apps/web/src/layouts/LoginLayout.tsx

  • Create: apps/web/src/stores/app.ts

  • Create: apps/web/src/App.tsx (update)

  • Create: apps/web/src/index.css (update)

  • Step 1: 编写应用状态 store

Create apps/web/src/stores/app.ts:

import { create } from 'zustand';

interface AppState {
  isLoggedIn: boolean;
  tenantName: string;
  theme: 'light' | 'dark';
  sidebarCollapsed: boolean;
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
  login: () => void;
  logout: () => void;
}

export const useAppStore = create<AppState>((set) => ({
  isLoggedIn: false,
  tenantName: '',
  theme: 'light',
  sidebarCollapsed: false,
  toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
  setTheme: (theme) => set({ theme }),
  login: () => set({ isLoggedIn: true }),
  logout: () => set({ isLoggedIn: false }),
}));
  • Step 2: 编写主布局

Create apps/web/src/layouts/MainLayout.tsx:

import { useState } from 'react';
import { Layout, Menu, theme, Button, Avatar, Badge, Space } from 'antd';
import {
  HomeOutlined,
  UserOutlined,
  SafetyOutlined,
  BellOutlined,
  SettingOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined,
} from '@ant-design/icons';
import { useAppStore } from '../stores/app';

const { Header, Sider, Content, Footer } = Layout;

const menuItems = [
  { key: '/', icon: <HomeOutlined />, label: '首页' },
  { key: '/users', icon: <UserOutlined />, label: '用户管理' },
  { key: '/roles', icon: <SafetyOutlined />, label: '权限管理' },
  { key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
];

export default function MainLayout({ children }: { children: React.ReactNode }) {
  const { sidebarCollapsed, toggleSidebar, tenantName } = useAppStore();
  const { token } = theme.useToken();

  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Sider trigger={null} collapsible collapsed={sidebarCollapsed} width={220}>
        <div style={{ height: 48, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: sidebarCollapsed ? 16 : 18, fontWeight: 'bold' }}>
          {sidebarCollapsed ? 'E' : 'ERP Platform'}
        </div>
        <Menu theme="dark" mode="inline" items={menuItems} defaultSelectedKeys={['/']} />
      </Sider>
      <Layout>
        <Header style={{ padding: '0 16px', background: token.colorBgContainer, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: `1px solid ${token.colorBorderSecondary}` }}>
          <Space>
            <Button type="text" icon={sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={toggleSidebar} />
          </Space>
          <Space size="middle">
            <Badge count={5}>
              <BellOutlined style={{ fontSize: 18 }} />
            </Badge>
            <Avatar icon={<UserOutlined />} />
            <span>Admin</span>
          </Space>
        </Header>
        <Content style={{ margin: 16, padding: 24, background: token.colorBgContainer, borderRadius: token.borderRadiusLG, minHeight: 280 }}>
          {children}
        </Content>
        <Footer style={{ textAlign: 'center', padding: '8px 16px' }}>
          {tenantName || 'ERP Platform'} · v0.1.0
        </Footer>
      </Layout>
    </Layout>
  );
}
  • Step 3: 更新 App.tsx 使用布局和路由

Update apps/web/src/App.tsx:

import { HashRouter, Routes, Route } from 'react-router-dom';
import { ConfigProvider, theme as antdTheme } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import MainLayout from './layouts/MainLayout';
import { useAppStore } from './stores/app';
import './main.css';

function HomePage() {
  return <div>欢迎来到 ERP 平台</div>;
}

export default function App() {
  const { isLoggedIn, theme: appTheme } = useAppStore();

  return (
    <ConfigProvider
      locale={zhCN}
      theme={{
        algorithm: appTheme === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
      }}
    >
      <HashRouter>
        <MainLayout>
          <Routes>
            <Route path="/" element={<HomePage />} />
          </Routes>
        </MainLayout>
      </HashRouter>
    </ConfigProvider>
  );
}
  • Step 4: 更新 CSS

Update apps/web/src/index.css:

@import "tailwindcss";

body {
  margin: 0;
}
  • Step 5: 验证前端布局

Run: cd apps/web && pnpm dev Expected: 浏览器中可以看到带侧边栏、顶栏、内容区域的完整布局

  • Step 6: 提交
git add apps/web/src/
git commit -m "feat(web): add main layout with sidebar, header, and routing"

Task 10: 创建 .gitignore 和项目文档

Files:

  • Create: .gitignore

  • Create: README.md

  • Step 1: 创建 .gitignore

# Rust
/target/
**/*.rs.bk

# Node
node_modules/
dist/

# Tauri
apps/desktop/src-tauri/target/

# IDE
.vscode/
.idea/
*.swp

# Environment
.env
*.env.local

# OS
.DS_Store
Thumbs.db

# Docker data
docker/postgres_data/
docker/redis_data/
  • Step 2: 创建 README.md

简要描述项目结构、技术栈、如何启动开发环境。

  • Step 3: 最终提交
git add .gitignore README.md
git commit -m "chore: add .gitignore and README"

Phase 1 完成标准

  • cargo check 全 workspace 编译通过
  • docker compose up -d 启动 PostgreSQL + Redis
  • cargo run -p erp-server 启动后端服务,连接数据库成功
  • pnpm dev 启动前端,浏览器显示完整布局
  • 数据库迁移运行成功,tenants 表已创建
  • 所有代码已提交到 Git

Chunk 2-6: 后续阶段大纲

后续阶段的详细任务将在每个阶段开始前编写。以下为各阶段的核心任务清单。

Chunk 2: Phase 2 - 身份与权限 (详细计划待编写)

核心任务:

  1. Auth 数据模型迁移users, roles, permissions, organizations, departments, positions, policies, user_credentials, user_tokens
  2. erp-auth crateUser/Role/Organization CRUD service + handler
  3. JWT 令牌签发/刷新/吊销access token 15min + refresh token 7d
  4. RBAC 权限中间件(角色 → 权限列表注入到 TenantContext
  5. 多租户中间件JWT claims 中提取 tenant_id注入请求上下文
  6. 密码哈希Argon2
  7. 登录页面 UI用户名/密码表单 → 调 API → 存 Tauri secure store
  8. 用户管理页面 UI表格 CRUD + 角色分配)
  9. 集成测试:多租户数据隔离、权限拦截、令牌生命周期

Chunk 3: Phase 3 - 系统配置 (详细计划待编写)

核心任务:

  1. Config 数据模型迁移settings, dictionaries, menus, numbering_rules, languages, themes
  2. erp-config crate分层配置Platform > Tenant > Org > User
  3. 数据字典 CRUD + API
  4. 动态菜单(根据角色过滤,树形结构)
  5. 编号规则引擎PostgreSQL advisory_lock + 序列生成)
  6. i18n 框架(前后端共享翻译资源)
  7. 设置页面 UI字典管理、菜单配置、编号规则、系统参数
  8. 主题切换(暗色/亮色)

Chunk 4: Phase 4 - 工作流引擎 (详细计划待编写)

核心任务:

  1. Workflow 数据模型迁移process_definitions, process_instances, tokens, tasks, variables
  2. erp-workflow crate流程定义存储与版本管理
  3. BPMN 子集解析器JSON 格式定义 → 内存图结构)
  4. 执行引擎Token 驱动,支持排他/并行网关)
  5. 用户任务分配(基于角色/部门/指定人)
  6. 条件表达式求值器
  7. 会签/或签逻辑
  8. 任务委派和转办
  9. React 可视化流程设计器(基于 React Flow
  10. 流程图查看器(高亮当前执行节点)
  11. 催办和超时处理(后台定时任务)
  12. 集成测试:完整流程生命周期

Chunk 5: Phase 5 - 消息中心 (详细计划待编写)

核心任务:

  1. Message 数据模型迁移messages, message_templates, message_subscriptions
  2. erp-message crate消息 CRUD + 模板渲染
  3. WebSocket 实时推送Axum WebSocket + JWT 认证 + 自动重连)
  4. 消息聚合逻辑
  5. 已读/未读跟踪
  6. 消息通知面板 UI弹出列表 + 未读计数)
  7. 桌面端系统托盘集成(新消息气泡)
  8. 集成测试消息收发、WebSocket 连接稳定性

Chunk 6: Phase 6 - 整合与打磨 (详细计划待编写)

核心任务:

  1. 审计日志中间件 + 查询 API
  2. 跨模块集成测试
  3. 桌面端打包Windows installer
  4. 自动更新配置Tauri updater
  5. 性能基准测试API p99 < 100ms, WS push < 50ms
  6. 安全审查OWASP top 10
  7. API 文档完善Swagger UI
  8. 项目文档