# 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** ```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`: ```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`: ```rust pub mod error; pub mod events; pub mod module; pub mod types; ``` - [ ] **Step 3: 创建 erp-common crate** `crates/erp-common/Cargo.toml`: ```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`: ```rust pub mod utils; ``` - [ ] **Step 4: 创建 erp-server crate (空壳)** `crates/erp-server/Cargo.toml`: ```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`: ```rust 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 目录: ```bash 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 并提交** ```bash 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`: ```rust 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, } /// 平台级错误类型 #[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 for AppError { fn from(err: anyhow::Error) -> Self { AppError::Internal(err.to_string()) } } /// 方便从 SeaORM 错误转换 impl From 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 = Result; ``` - [ ] **Step 2: 编写共享类型** Create `crates/erp-core/src/types.rs`: ```rust 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, pub updated_at: DateTime, pub created_by: Uuid, pub updated_by: Uuid, pub deleted_at: Option>, pub version: i32, } /// 分页请求 #[derive(Debug, Deserialize)] pub struct Pagination { pub page: Option, pub page_size: Option, } 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 { pub data: Vec, pub total: u64, pub page: u64, pub page_size: u64, pub total_pages: u64, } /// API 统一响应 #[derive(Debug, Serialize)] pub struct ApiResponse { pub success: bool, pub data: Option, pub message: Option, } impl ApiResponse { 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, pub permissions: Vec, } ``` - [ ] **Step 3: 验证编译** Run: `cargo check -p erp-core` Expected: 编译通过 - [ ] **Step 4: 提交** ```bash 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`: ```rust 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, pub correlation_id: Uuid, } impl DomainEvent { pub fn new(event_type: impl Into, 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; fn handle(&self, event: &DomainEvent) -> impl std::future::Future> + Send; } /// 进程内事件总线 #[derive(Clone)] pub struct EventBus { sender: broadcast::Sender, } 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 { self.sender.subscribe() } } ``` - [ ] **Step 2: 验证编译** Run: `cargo check -p erp-core` Expected: 编译通过 - [ ] **Step 3: 提交** ```bash 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`: ```rust 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> + Send { async { Ok(()) } } /// 租户删除时的清理钩子 fn on_tenant_deleted(&self, _tenant_id: Uuid) -> impl std::future::Future> + Send { async { Ok(()) } } } /// 模块注册器 pub struct ModuleRegistry { modules: Vec>, } impl ModuleRegistry { pub fn new() -> Self { Self { modules: vec![] } } pub fn register(&mut self, module: Box) { 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] { &self.modules } } ``` - [ ] **Step 2: 验证编译** Run: `cargo check -p erp-core` Expected: 编译通过 - [ ] **Step 3: 提交** ```bash 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`: ```yaml 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`: ```env 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`: ```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: 提交** ```bash 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`: ```rust 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 { 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`: ```rust 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 { 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`: ```rust 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: 提交** ```bash 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`: ```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`: ```rust 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> { vec![ Box::new(m20260410_000001_create_tenant::Migration), ] } } ``` - [ ] **Step 2: 编写首个迁移 - tenants 表** Create `crates/erp-server/migration/src/m20260410_000001_create_tenant.rs`: ```rust 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` 中数据库连接后添加: ```rust // 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: 提交** ```bash 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 项目** ```bash cd g:/erp pnpm create vite apps/web --template react-ts cd apps/web pnpm install ``` - [ ] **Step 2: 安装 UI 依赖** ```bash 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`: ```typescript 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: 提交** ```bash 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`: ```typescript 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((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`: ```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: , label: '首页' }, { key: '/users', icon: , label: '用户管理' }, { key: '/roles', icon: , label: '权限管理' }, { key: '/settings', icon: , label: '系统设置' }, ]; export default function MainLayout({ children }: { children: React.ReactNode }) { const { sidebarCollapsed, toggleSidebar, tenantName } = useAppStore(); const { token } = theme.useToken(); return (
{sidebarCollapsed ? 'E' : 'ERP Platform'}
{children}
{tenantName || 'ERP Platform'} · v0.1.0
); } ``` - [ ] **Step 3: 更新 App.tsx 使用布局和路由** Update `apps/web/src/App.tsx`: ```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
欢迎来到 ERP 平台
; } export default function App() { const { isLoggedIn, theme: appTheme } = useAppStore(); return ( } /> ); } ``` - [ ] **Step 4: 更新 CSS** Update `apps/web/src/index.css`: ```css @import "tailwindcss"; body { margin: 0; } ``` - [ ] **Step 5: 验证前端布局** Run: `cd apps/web && pnpm dev` Expected: 浏览器中可以看到带侧边栏、顶栏、内容区域的完整布局 - [ ] **Step 6: 提交** ```bash 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** ```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: 最终提交** ```bash 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 crate:User/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. 项目文档