- 添加基础crate结构(erp-core, erp-common) - 实现核心模块trait和事件总线 - 配置Docker开发环境(PostgreSQL+Redis) - 添加Tauri桌面端基础框架 - 设置CI/CD工作流 - 编写项目协作规范文档(CLAUDE.md)
1341 lines
33 KiB
Markdown
1341 lines
33 KiB
Markdown
# 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 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. 项目文档
|