feat: 初始化ERP平台底座项目结构

- 添加基础crate结构(erp-core, erp-common)
- 实现核心模块trait和事件总线
- 配置Docker开发环境(PostgreSQL+Redis)
- 添加Tauri桌面端基础框架
- 设置CI/CD工作流
- 编写项目协作规范文档(CLAUDE.md)
This commit is contained in:
iven
2026-04-10 23:40:38 +08:00
commit eb856b1d73
32 changed files with 8049 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
[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

View File

@@ -0,0 +1,78 @@
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()
}
}
impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
AppError::Internal(err.to_string())
}
}
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>;

View File

@@ -0,0 +1,61 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
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()
}
}

View File

@@ -0,0 +1,4 @@
pub mod error;
pub mod events;
pub mod module;
pub mod types;

View File

@@ -0,0 +1,76 @@
use axum::Router;
use uuid::Uuid;
use crate::error::AppResult;
use crate::events::EventBus;
/// 模块注册接口
/// 所有业务模块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
}
}

View File

@@ -0,0 +1,70 @@
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>,
}