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

1341 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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`:
```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<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: 提交**
```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<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: 提交**
```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<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: 提交**
```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<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`:
```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<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`:
```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<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`:
```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<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`:
```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`:
```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`:
```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 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. 项目文档