Performance improvements: - Vite build: manual chunks, terser minification, optimizeDeps - API response caching with 5s TTL via axios interceptors - React.memo for SidebarMenuItem, useCallback for handlers - CSS classes replacing inline styles to reduce reflows UI/UX enhancements (inspired by SAP Fiori, Linear, Feishu): - Dashboard: trend indicators, sparkline charts, CountUp animation on stat cards - Dashboard: pending tasks section with priority labels - Dashboard: recent activity timeline - Design system tokens: trend colors, line-height, dark mode refinements - Enhanced quick actions with hover animations Accessibility (Lighthouse 100/100): - Skip-to-content link, ARIA landmarks, heading hierarchy - prefers-reduced-motion support, focus-visible states - Color contrast fixes: all text meets 4.5:1 ratio - Keyboard navigation for stat cards and task items SEO: meta theme-color, format-detection, robots.txt
33 KiB
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.toml 和 src/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 + Rediscargo run -p erp-server启动后端服务,连接数据库成功pnpm dev启动前端,浏览器显示完整布局- 数据库迁移运行成功,
tenants表已创建 - 所有代码已提交到 Git
Chunk 2-6: 后续阶段大纲
后续阶段的详细任务将在每个阶段开始前编写。以下为各阶段的核心任务清单。
Chunk 2: Phase 2 - 身份与权限 (详细计划待编写)
核心任务:
- Auth 数据模型迁移(users, roles, permissions, organizations, departments, positions, policies, user_credentials, user_tokens)
- erp-auth crate:User/Role/Organization CRUD service + handler
- JWT 令牌签发/刷新/吊销(access token 15min + refresh token 7d)
- RBAC 权限中间件(角色 → 权限列表注入到 TenantContext)
- 多租户中间件(JWT claims 中提取 tenant_id,注入请求上下文)
- 密码哈希(Argon2)
- 登录页面 UI(用户名/密码表单 → 调 API → 存 Tauri secure store)
- 用户管理页面 UI(表格 CRUD + 角色分配)
- 集成测试:多租户数据隔离、权限拦截、令牌生命周期
Chunk 3: Phase 3 - 系统配置 (详细计划待编写)
核心任务:
- Config 数据模型迁移(settings, dictionaries, menus, numbering_rules, languages, themes)
- erp-config crate:分层配置(Platform > Tenant > Org > User)
- 数据字典 CRUD + API
- 动态菜单(根据角色过滤,树形结构)
- 编号规则引擎(PostgreSQL advisory_lock + 序列生成)
- i18n 框架(前后端共享翻译资源)
- 设置页面 UI(字典管理、菜单配置、编号规则、系统参数)
- 主题切换(暗色/亮色)
Chunk 4: Phase 4 - 工作流引擎 (详细计划待编写)
核心任务:
- Workflow 数据模型迁移(process_definitions, process_instances, tokens, tasks, variables)
- erp-workflow crate:流程定义存储与版本管理
- BPMN 子集解析器(JSON 格式定义 → 内存图结构)
- 执行引擎(Token 驱动,支持排他/并行网关)
- 用户任务分配(基于角色/部门/指定人)
- 条件表达式求值器
- 会签/或签逻辑
- 任务委派和转办
- React 可视化流程设计器(基于 React Flow)
- 流程图查看器(高亮当前执行节点)
- 催办和超时处理(后台定时任务)
- 集成测试:完整流程生命周期
Chunk 5: Phase 5 - 消息中心 (详细计划待编写)
核心任务:
- Message 数据模型迁移(messages, message_templates, message_subscriptions)
- erp-message crate:消息 CRUD + 模板渲染
- WebSocket 实时推送(Axum WebSocket + JWT 认证 + 自动重连)
- 消息聚合逻辑
- 已读/未读跟踪
- 消息通知面板 UI(弹出列表 + 未读计数)
- 桌面端系统托盘集成(新消息气泡)
- 集成测试:消息收发、WebSocket 连接稳定性
Chunk 6: Phase 6 - 整合与打磨 (详细计划待编写)
核心任务:
- 审计日志中间件 + 查询 API
- 跨模块集成测试
- 桌面端打包(Windows installer)
- 自动更新配置(Tauri updater)
- 性能基准测试(API p99 < 100ms, WS push < 50ms)
- 安全审查(OWASP top 10)
- API 文档完善(Swagger UI)
- 项目文档 vc aq