feat: complete Phase 1 infrastructure
- erp-core: error types, shared types, event bus, ErpModule trait - erp-server: config loading, database/Redis connections, migrations - erp-server/migration: tenants table with SeaORM - apps/web: Vite + React 18 + TypeScript + Ant Design 5 + TailwindCSS - Web frontend: main layout with sidebar, header, routing - Docker: PostgreSQL 16 + Redis 7 development environment - All workspace crates compile successfully (cargo check passes)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
@wiki/index.md
|
||||
|
||||
# ERP 平台底座 — 协作与实现规则
|
||||
|
||||
> **ERP Platform Base** 是一个模块化的商业 SaaS ERP 底座,目标是提供核心基础设施(身份权限、工作流、消息、配置),使行业业务模块(进销存、生产、财务等)可以快速插接。
|
||||
|
||||
475
Cargo.lock
generated
475
Cargo.lock
generated
@@ -61,21 +61,62 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.1"
|
||||
@@ -163,42 +204,13 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core 0.4.5",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"matchit 0.7.3",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sync_wrapper",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
dependencies = [
|
||||
"axum-core 0.5.6",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
@@ -208,7 +220,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit 0.8.4",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
@@ -225,26 +237,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
@@ -435,6 +427,52 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -599,6 +637,40 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -620,17 +692,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -720,7 +781,7 @@ name = "erp-auth"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.8",
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"sea-orm",
|
||||
@@ -747,7 +808,7 @@ name = "erp-config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.8",
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"sea-orm",
|
||||
@@ -763,7 +824,8 @@ name = "erp-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.8",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"chrono",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
@@ -779,7 +841,7 @@ name = "erp-message"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.8",
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"sea-orm",
|
||||
@@ -794,10 +856,12 @@ dependencies = [
|
||||
name = "erp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum 0.8.8",
|
||||
"anyhow",
|
||||
"axum",
|
||||
"config",
|
||||
"erp-common",
|
||||
"erp-core",
|
||||
"erp-server-migration",
|
||||
"redis",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
@@ -808,7 +872,14 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erp-server-migration"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"sea-orm-migration",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -816,7 +887,7 @@ name = "erp-workflow"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.8",
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"sea-orm",
|
||||
@@ -892,6 +963,12 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -1046,6 +1123,12 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1337,6 +1420,12 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -1381,6 +1470,12 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -1507,12 +1602,6 @@ dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
@@ -1550,16 +1639,6 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -1687,6 +1766,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
@@ -2186,40 +2271,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.117",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.20.0"
|
||||
@@ -2302,15 +2353,6 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -2361,6 +2403,25 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-cli"
|
||||
version = "1.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da80ebcdb44571e86f03a2bdcb5532136a87397f366f38bbce64673fc5e6a450"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
"glob",
|
||||
"regex",
|
||||
"sea-schema",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-macros"
|
||||
version = "1.1.20"
|
||||
@@ -2375,6 +2436,22 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-migration"
|
||||
version = "1.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c577f2959277e936c1d08109acd1e08fc36a95ef29ec028190ba82cad8f96e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
"sea-orm",
|
||||
"sea-orm-cli",
|
||||
"sea-schema",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-query"
|
||||
version = "0.32.7"
|
||||
@@ -2386,6 +2463,7 @@ dependencies = [
|
||||
"inherent",
|
||||
"ordered-float",
|
||||
"rust_decimal",
|
||||
"sea-query-derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"uuid",
|
||||
@@ -2407,6 +2485,45 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-query-derive"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-schema"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"sea-query",
|
||||
"sea-query-binder",
|
||||
"sea-schema-derive",
|
||||
"sqlx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sea-schema-derive"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@@ -2855,6 +2972,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
@@ -3264,12 +3387,6 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
@@ -3333,6 +3450,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.4.0"
|
||||
@@ -3358,24 +3481,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "8.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4b5ac679cc6dfc5ea3f2823b0291c777750ffd5e13b21137e0f7ac0e8f9617"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.0"
|
||||
@@ -3406,16 +3511,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -3570,15 +3665,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@@ -4033,37 +4119,8 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"crates/erp-workflow",
|
||||
"crates/erp-message",
|
||||
"crates/erp-config",
|
||||
"crates/erp-server/migration",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -60,7 +61,8 @@ argon2 = "0.5"
|
||||
|
||||
# API docs
|
||||
utoipa = { version = "5", features = ["axum_extras", "uuid", "chrono"] }
|
||||
utoipa-swagger-ui = { version = "8", features = ["axum"] }
|
||||
# utoipa-swagger-ui 需要下载 GitHub 资源,网络受限时暂不使用
|
||||
# utoipa-swagger-ui = { version = "8", features = ["axum"] }
|
||||
|
||||
# Validation
|
||||
validator = { version = "0.19", features = ["derive"] }
|
||||
|
||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# ERP Platform Base
|
||||
|
||||
模块化商业 SaaS ERP 平台底座。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Rust (Axum 0.8 + SeaORM + Tokio) |
|
||||
| 数据库 | PostgreSQL 16+ |
|
||||
| 缓存 | Redis 7+ |
|
||||
| 前端 | Vite + React 18 + TypeScript + Ant Design 5 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
erp/
|
||||
├── crates/
|
||||
│ ├── erp-core/ # 基础类型、错误、事件总线、模块 trait
|
||||
│ ├── erp-common/ # 共享工具
|
||||
│ ├── erp-auth/ # 身份与权限 (Phase 2)
|
||||
│ ├── erp-workflow/ # 工作流引擎 (Phase 4)
|
||||
│ ├── erp-message/ # 消息中心 (Phase 5)
|
||||
│ ├── erp-config/ # 系统配置 (Phase 3)
|
||||
│ └── erp-server/ # Axum 服务入口
|
||||
│ └── migration/ # SeaORM 数据库迁移
|
||||
├── apps/web/ # React SPA 前端
|
||||
├── docker/ # Docker 开发环境
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 启动基础设施
|
||||
|
||||
```bash
|
||||
cd docker && docker compose up -d
|
||||
```
|
||||
|
||||
### 2. 启动后端
|
||||
|
||||
```bash
|
||||
cargo run -p erp-server
|
||||
```
|
||||
|
||||
### 3. 启动前端
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm install && pnpm dev
|
||||
```
|
||||
|
||||
### 4. 访问
|
||||
|
||||
- 前端: http://localhost:5173
|
||||
- 后端 API: http://localhost:3000
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
cargo check # 编译检查
|
||||
cargo test --workspace # 运行测试
|
||||
cargo run -p erp-server # 启动后端
|
||||
cd apps/web && pnpm dev # 启动前端
|
||||
```
|
||||
24
apps/web/.gitignore
vendored
Normal file
24
apps/web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
apps/web/README.md
Normal file
73
apps/web/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
apps/web/eslint.config.js
Normal file
23
apps/web/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
apps/web/index.html
Normal file
13
apps/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
apps/web/package.json
Normal file
37
apps/web/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"antd": "^6.3.5",
|
||||
"axios": "^1.15.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.14.0",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.4"
|
||||
}
|
||||
}
|
||||
3177
apps/web/pnpm-lock.yaml
generated
Normal file
3177
apps/web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
apps/web/public/favicon.svg
Normal file
1
apps/web/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
apps/web/public/icons.svg
Normal file
24
apps/web/public/icons.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
30
apps/web/src/App.tsx
Normal file
30
apps/web/src/App.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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';
|
||||
|
||||
function HomePage() {
|
||||
return <div>欢迎来到 ERP 平台</div>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { 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>
|
||||
);
|
||||
}
|
||||
5
apps/web/src/index.css
Normal file
5
apps/web/src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
87
apps/web/src/layouts/MainLayout.tsx
Normal file
87
apps/web/src/layouts/MainLayout.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
10
apps/web/src/main.tsx
Normal file
10
apps/web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
23
apps/web/src/stores/app.ts
Normal file
23
apps/web/src/stores/app.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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 }),
|
||||
}));
|
||||
25
apps/web/tsconfig.app.json
Normal file
25
apps/web/tsconfig.app.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
apps/web/tsconfig.json
Normal file
7
apps/web/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
apps/web/tsconfig.node.json
Normal file
24
apps/web/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
20
apps/web/vite.config.ts
Normal file
20
apps/web/vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1 +1,3 @@
|
||||
/// Shared utility functions for the ERP platform.
|
||||
#[allow(dead_code)]
|
||||
pub fn noop() {}
|
||||
|
||||
@@ -14,3 +14,4 @@ anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
axum.workspace = true
|
||||
sea-orm.workspace = true
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::events::EventBus;
|
||||
|
||||
/// 模块注册接口
|
||||
/// 所有业务模块(Auth, Workflow, Message, Config, 行业模块)都实现此 trait
|
||||
#[async_trait::async_trait]
|
||||
pub trait ErpModule: Send + Sync {
|
||||
/// 模块名称(唯一标识)
|
||||
fn name(&self) -> &str;
|
||||
@@ -27,19 +28,13 @@ pub trait ErpModule: Send + Sync {
|
||||
fn register_event_handlers(&self, _bus: &EventBus) {}
|
||||
|
||||
/// 租户创建时的初始化钩子
|
||||
fn on_tenant_created(
|
||||
&self,
|
||||
_tenant_id: Uuid,
|
||||
) -> impl std::future::Future<Output = AppResult<()>> + Send {
|
||||
async { Ok(()) }
|
||||
async fn on_tenant_created(&self, _tenant_id: Uuid) -> AppResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 租户删除时的清理钩子
|
||||
fn on_tenant_deleted(
|
||||
&self,
|
||||
_tenant_id: Uuid,
|
||||
) -> impl std::future::Future<Output = AppResult<()>> + Send {
|
||||
async { Ok(()) }
|
||||
async fn on_tenant_deleted(&self, _tenant_id: Uuid) -> AppResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ config.workspace = true
|
||||
sea-orm.workspace = true
|
||||
redis.workspace = true
|
||||
utoipa.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
erp-server-migration = { path = "migration" }
|
||||
anyhow.workspace = true
|
||||
|
||||
8
crates/erp-server/migration/Cargo.toml
Normal file
8
crates/erp-server/migration/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "erp-server-migration"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
sea-orm-migration.workspace = true
|
||||
tokio.workspace = true
|
||||
12
crates/erp-server/migration/src/lib.rs
Normal file
12
crates/erp-server/migration/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
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)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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_key(),
|
||||
)
|
||||
.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,
|
||||
}
|
||||
@@ -3,6 +3,8 @@ mod db;
|
||||
|
||||
use axum::Router;
|
||||
use config::AppConfig;
|
||||
use erp_server_migration::Migrator;
|
||||
use erp_server_migration::MigratorTrait;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -24,6 +26,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Connect to database
|
||||
let db = db::connect(&config.database).await?;
|
||||
|
||||
// Run migrations
|
||||
Migrator::up(&db, None).await?;
|
||||
tracing::info!("Database migrations applied");
|
||||
|
||||
// Connect to Redis
|
||||
let _redis_client = redis::Client::open(&config.redis.url[..])?;
|
||||
tracing::info!("Redis client created");
|
||||
|
||||
84
wiki/architecture.md
Normal file
84
wiki/architecture.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# architecture (架构决策记录)
|
||||
|
||||
## 设计思想
|
||||
|
||||
ERP 平台采用 **模块化单体 + 渐进式拆分** 架构。核心原则:模块间零直接依赖,所有跨模块通信通过事件总线和 trait 接口。
|
||||
|
||||
## 关键架构决策
|
||||
|
||||
### Q: 为什么用模块化单体而非微服务?
|
||||
|
||||
**A:** ERP 系统的模块间数据一致性要求高,分布式事务成本大。单体起步,模块边界清晰,未来需要时可按模块拆分为微服务。`ErpModule` trait 天然支持这种渐进式迁移。
|
||||
|
||||
### Q: 为什么用 UUIDv7 而不是自增 ID?
|
||||
|
||||
**A:** UUIDv7 是时间排序的,既有 UUID 的分布式唯一性,又有接近自增 ID 的索引性能。对多租户 SaaS 尤其重要——不同租户的数据不会因为 ID 冲突而互相影响。
|
||||
|
||||
### Q: 为什么用 broadcast channel 做事件总线?
|
||||
|
||||
**A:** `tokio::sync::broadcast` 提供多消费者发布/订阅,语义匹配模块间事件通知。设计规格要求 outbox 模式(持久化到 domain_events 表),但当前 Phase 1 先用内存 broadcast,后续再补持久化。
|
||||
|
||||
### Q: 为什么错误类型跨 crate 边界必须用 thiserror?
|
||||
|
||||
**A:** `anyhow` 的错误没有类型信息,无法在 API 层做精确的 HTTP 状态码映射。`thiserror` 定义明确的错误变体,`AppError` 可以精确映射到 400/401/403/404/409/500。crate 内部可以用 `anyhow` 简化,但对外必须转 `AppError`。
|
||||
|
||||
### Q: 为什么 tenant_id 不在 API 路径中?
|
||||
|
||||
**A:** 从 JWT token 中提取 tenant_id,通过中间件注入 `TenantContext`。这防止了:
|
||||
- 用户手动修改 URL 访问其他租户数据
|
||||
- API 路径暴露租户信息
|
||||
- 开发者忘记检查租户权限
|
||||
|
||||
管理员接口例外,可以通过路径指定 tenant_id。
|
||||
|
||||
### Q: 为什么前端用 HashRouter 而非 BrowserRouter?
|
||||
|
||||
**A:** 部署时可能不在根路径下,HashRouter 不需要服务端配置 fallback 路由。对 SPA 来说更稳健。
|
||||
|
||||
## 模块依赖铁律
|
||||
|
||||
```
|
||||
erp-core (L1)
|
||||
erp-common (L1)
|
||||
|
|
||||
+--------------+--------------+--------------+
|
||||
| | | |
|
||||
erp-auth erp-config erp-workflow erp-message (L2)
|
||||
| | | |
|
||||
+--------------+--------------+--------------+
|
||||
|
|
||||
erp-server (L3: 唯一组装点)
|
||||
```
|
||||
|
||||
**禁止:**
|
||||
- L2 模块之间直接依赖
|
||||
- L1 模块依赖任何业务模块
|
||||
- 绕过事件总线直接调用其他模块
|
||||
|
||||
## 多租户隔离策略
|
||||
|
||||
**当前策略:共享数据库 + tenant_id 列过滤**
|
||||
|
||||
所有业务表包含 `tenant_id` 列,查询时通过中间件自动注入过滤条件。这是最简单的 SaaS 多租户方案,未来可扩展为:
|
||||
- Schema 隔离 — 每个租户独立 schema
|
||||
- 数据库隔离 — 每个租户独立数据库(私有化部署)
|
||||
|
||||
`ErpModule::on_tenant_created()` 和 `on_tenant_deleted()` 钩子确保模块能在租户创建/删除时初始化/清理数据。
|
||||
|
||||
## 技术选型理由
|
||||
|
||||
| 选择 | 理由 |
|
||||
|------|------|
|
||||
| Axum 0.8 | Tokio 团队维护,与 tower 生态无缝集成,类型安全路由 |
|
||||
| SeaORM 1.1 | 异步、类型安全、Rust 原生 ORM,迁移工具完善 |
|
||||
| PostgreSQL 16 | 企业级关系型数据库,JSON 支持好,扩展丰富 |
|
||||
| Redis 7 | 高性能缓存,会话存储,限流 token bucket |
|
||||
| React 19 + Ant Design 6 | 成熟的组件库,企业后台 UI 标配 |
|
||||
| Zustand | 极简状态管理,无 boilerplate |
|
||||
| utoipa | Rust 代码生成 OpenAPI 文档,零额外维护 |
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-core]]** — 架构契约的定义者
|
||||
- **[[erp-server]]** — 架构的组装执行者
|
||||
- **[[database]]** — 多租户隔离的物理实现
|
||||
73
wiki/database.md
Normal file
73
wiki/database.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# database (数据库迁移与模式)
|
||||
|
||||
## 设计思想
|
||||
|
||||
数据库迁移使用 SeaORM Migration 框架,遵循以下原则:
|
||||
|
||||
- **所有表必须包含标准字段** — `id`(UUIDv7), `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version`
|
||||
- **软删除** — 不执行硬删除,设置 `deleted_at` 时间戳
|
||||
- **乐观锁** — 更新时检查 `version` 字段
|
||||
- **多租户隔离** — 所有业务表必须含 `tenant_id`,查询时自动过滤
|
||||
- **幂等迁移** — 使用 `if_not_exists` 确保可重复执行
|
||||
- **可回滚** — 每个迁移必须实现 `down()` 方法
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 迁移文件命名规则
|
||||
```
|
||||
m{YYYYMMDD}_{6位序号}_{描述}.rs
|
||||
例: m20260410_000001_create_tenant.rs
|
||||
```
|
||||
|
||||
### 当前表结构
|
||||
|
||||
**tenant 表** (唯一已实现的表):
|
||||
| 列名 | 类型 | 约束 |
|
||||
|------|------|------|
|
||||
| id | UUID | PK, NOT NULL |
|
||||
| name | STRING | NOT NULL |
|
||||
| code | STRING | NOT NULL, UNIQUE |
|
||||
| status | STRING | NOT NULL, DEFAULT 'active' |
|
||||
| settings | JSON | NULLABLE |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
|
||||
| deleted_at | TIMESTAMPTZ | NULLABLE |
|
||||
|
||||
### 已知缺失字段
|
||||
tenant 表缺少 `BaseFields` 要求的:
|
||||
- `created_by` — 创建人
|
||||
- `updated_by` — 最后修改人
|
||||
- `version` — 乐观锁版本号
|
||||
|
||||
### 迁移执行
|
||||
```
|
||||
erp-server 启动 → Migrator::up(&db_conn) → 自动运行所有 pending 迁移
|
||||
```
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-core]]** — `BaseFields` 定义了标准字段规范,迁移表结构必须对齐
|
||||
- **[[erp-server]]** — 启动时自动运行迁移
|
||||
- **[[erp-auth]]** — Phase 2 将创建 users, roles, permissions 表
|
||||
- **[[erp-config]]** — Phase 3 将创建 system_configs 表
|
||||
- **[[erp-workflow]]** — Phase 4 将创建 workflow_definitions, workflow_instances 表
|
||||
- **[[erp-message]]** — Phase 5 将创建 messages, notification_settings 表
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-server/migration/src/lib.rs` | Migrator 注册所有迁移 |
|
||||
| `crates/erp-server/migration/src/m20260410_000001_create_tenant.rs` | tenant 表迁移 |
|
||||
| `crates/erp-core/src/types.rs` | BaseFields 标准字段定义 |
|
||||
| `docker/docker-compose.yml` | PostgreSQL 16 服务定义 |
|
||||
|
||||
## 未来迁移计划 (按 Phase)
|
||||
|
||||
| Phase | 表 | 说明 |
|
||||
|-------|-----|------|
|
||||
| Phase 2 | users, roles, permissions, user_roles, role_permissions | RBAC + ABAC |
|
||||
| Phase 3 | system_configs, config_histories | 层级配置 |
|
||||
| Phase 4 | workflow_definitions, workflow_instances, workflow_tasks | BPMN 工作流 |
|
||||
| Phase 5 | messages, notification_settings, message_templates | 多渠道消息 |
|
||||
| 持续 | domain_events | 事件 outbox 表 |
|
||||
68
wiki/erp-core.md
Normal file
68
wiki/erp-core.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# erp-core
|
||||
|
||||
## 设计思想
|
||||
|
||||
`erp-core` 是整个 ERP 平台的 L1 基础层,所有业务模块的唯一共同依赖。它的职责是定义**跨模块共享的契约**,而非实现业务逻辑。
|
||||
|
||||
核心设计决策:
|
||||
- **AppError 统一错误体系** — 6 种错误变体映射到 HTTP 状态码,业务 crate 只需 `?` 传播错误,由 Axum `IntoResponse` 自动转换
|
||||
- **EventBus 进程内广播** — 用 `tokio::sync::broadcast` 实现发布/订阅,模块间零耦合通信
|
||||
- **ErpModule 插件 trait** — 每个业务模块实现此 trait,由 `ModuleRegistry` 统一注册路由和事件处理器
|
||||
- **BaseFields 强制多租户** — 所有实体的基础字段模板,确保 `tenant_id` 从第一天就存在
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 错误处理链
|
||||
```
|
||||
业务 crate (thiserror) → AppError → IntoResponse → HTTP JSON 响应
|
||||
数据库 (sea_orm::DbErr) → From 转换 → AppError (自动识别 duplicate key → Conflict)
|
||||
```
|
||||
|
||||
错误响应统一格式:`{ "error": "not_found", "message": "资源不存在", "details": null }`
|
||||
|
||||
### 事件总线
|
||||
```
|
||||
发布者: EventBus::publish(DomainEvent) → broadcast channel
|
||||
订阅者: EventBus::subscribe() → Receiver<DomainEvent>
|
||||
事件字段: id(UUIDv7), event_type("user.created"), tenant_id, payload(JSON), timestamp, correlation_id
|
||||
```
|
||||
|
||||
事件类型命名规则:`{模块}.{动作}` 如 `user.created`, `workflow.task.completed`
|
||||
|
||||
### 模块注册
|
||||
```
|
||||
业务模块实现 ErpModule trait → ModuleRegistry::register() →
|
||||
build_router(): 折叠所有模块的 register_routes() → Axum Router
|
||||
register_handlers(): 注册所有模块的事件处理器 → EventBus
|
||||
```
|
||||
|
||||
### 共享类型
|
||||
- `TenantContext` — 中间件注入的租户上下文(tenant_id, user_id, roles, permissions)
|
||||
- `Pagination` / `PaginatedResponse<T>` — 分页查询标准化(每页上限 100)
|
||||
- `ApiResponse<T>` — API 统一信封 `{ success, data, message }`
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — 消费所有 erp-core 类型和 trait,是唯一组装点
|
||||
- **[[erp-auth]]** — 实现 `ErpModule` trait,发布认证事件
|
||||
- **[[erp-workflow]]** — 实现 `ErpModule` trait,订阅业务事件
|
||||
- **[[erp-message]]** — 实现 `ErpModule` trait,订阅通知事件
|
||||
- **[[erp-config]]** — 实现 `ErpModule` trait
|
||||
- **[[database]]** — 迁移表结构必须与 `BaseFields` 对齐
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-core/src/error.rs` | AppError 定义、HTTP 映射、From 转换 |
|
||||
| `crates/erp-core/src/events.rs` | DomainEvent、EventBus、EventHandler trait |
|
||||
| `crates/erp-core/src/module.rs` | ErpModule trait、ModuleRegistry |
|
||||
| `crates/erp-core/src/types.rs` | BaseFields、Pagination、ApiResponse、TenantContext |
|
||||
| `crates/erp-core/src/lib.rs` | 模块导出入口 |
|
||||
|
||||
## 当前状态
|
||||
|
||||
**已实现,Phase 1 可用。** 但以下部分尚未被 erp-server 集成:
|
||||
- `ModuleRegistry` 未在 `main.rs` 中使用
|
||||
- `EventBus` 未创建实例
|
||||
- `TenantContext` 未通过中间件注入
|
||||
66
wiki/erp-server.md
Normal file
66
wiki/erp-server.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# erp-server
|
||||
|
||||
## 设计思想
|
||||
|
||||
`erp-server` 是 L3 层——**唯一的组装点**。它不包含业务逻辑,只负责把所有业务模块组装成可运行的服务。
|
||||
|
||||
核心决策:
|
||||
- **配置优先** — 使用 `config` crate 从 TOML 文件 + 环境变量加载,`ERP__` 前缀覆盖(如 `ERP__DATABASE__URL`)
|
||||
- **启动序列严格有序** — 配置 → 日志 → 数据库 → 迁移 → Redis → 路由 → 监听,每步失败即终止
|
||||
- **单一入口** — 所有模块通过 `ModuleRegistry` 注册,server 本身不直接 import 业务模块的类型
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 启动流程 (`main.rs`)
|
||||
```
|
||||
1. AppConfig::load() ← config/default.toml + 环境变量
|
||||
2. init_tracing(level) ← JSON 格式日志
|
||||
3. db::connect(&db_config) ← SeaORM 连接池 (max=20, min=5)
|
||||
4. Migrator::up(&db_conn) ← 运行所有待执行迁移
|
||||
5. redis::Client::open(url) ← Redis 客户端(当前未使用)
|
||||
6. Router::new() ← 当前仅有 404 fallback
|
||||
7. bind(host, port).serve() ← 启动 HTTP 服务
|
||||
```
|
||||
|
||||
### 配置结构
|
||||
```
|
||||
AppConfig
|
||||
├── server: ServerConfig { host: "0.0.0.0", port: 3000 }
|
||||
├── database: DatabaseConfig { url, max_connections: 20, min_connections: 5 }
|
||||
├── redis: RedisConfig { url: "redis://localhost:6379" }
|
||||
├── jwt: JwtConfig { secret, access_token_ttl, refresh_token_ttl }
|
||||
└── log: LogConfig { level: "info" }
|
||||
```
|
||||
|
||||
### 当前状态
|
||||
- 数据库连接池正常工作
|
||||
- 迁移自动执行
|
||||
- **没有注册任何路由** — 仅返回 404
|
||||
- **没有使用 ModuleRegistry** — 未集成业务模块
|
||||
- Redis 客户端已创建但未执行任何命令
|
||||
- 缺少 CORS、压缩、请求追踪中间件
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-core]]** — 提供 AppError、ErpModule trait、ModuleRegistry
|
||||
- **[[database]]** — 迁移文件通过 `erp-server-migration` crate 引用
|
||||
- **[[infrastructure]]** — Docker 提供 PostgreSQL 和 Redis 服务
|
||||
- **[[frontend]]** — Vite 代理 `/api` 请求到 server 的 3000 端口
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `crates/erp-server/src/main.rs` | 服务启动入口 |
|
||||
| `crates/erp-server/src/config.rs` | 5 个配置 struct + 加载逻辑 |
|
||||
| `crates/erp-server/src/db.rs` | SeaORM 连接池配置 |
|
||||
| `crates/erp-server/config/default.toml` | 默认配置值 |
|
||||
| `crates/erp-server/Cargo.toml` | 依赖声明 |
|
||||
|
||||
## 待完成 (Phase 1 剩余)
|
||||
|
||||
1. 实例化 `ModuleRegistry` 并注册模块
|
||||
2. 添加 CORS 中间件(tower-http)
|
||||
3. 添加请求追踪中间件
|
||||
4. 将 Redis 连接注入 AppState
|
||||
5. 健康检查端点 (`/api/v1/health`)
|
||||
81
wiki/frontend.md
Normal file
81
wiki/frontend.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# frontend (Web 前端)
|
||||
|
||||
## 设计思想
|
||||
|
||||
前端是一个 Vite + React SPA,遵循 **UI 层只做展示** 的原则:
|
||||
|
||||
- **组件库优先** — 使用 Ant Design,不自造轮子
|
||||
- **状态集中** — Zustand 管理全局状态(主题、侧边栏、认证)
|
||||
- **API 层分离** — HTTP 调用封装到 service 层,组件不直接 fetch
|
||||
- **代理开发** — Vite 开发服务器代理 `/api` 到后端 3000 端口
|
||||
|
||||
版本实际使用情况(与设计规格有差异):
|
||||
| 技术 | 规格 | 实际 |
|
||||
|------|------|------|
|
||||
| React | 18 | 19.2.4 |
|
||||
| Ant Design | 5 | 6.3.5 |
|
||||
| React Router | 7 | 7.14.0 |
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 应用结构
|
||||
```
|
||||
main.tsx → App.tsx (ConfigProvider + HashRouter) → MainLayout → 各页面组件
|
||||
```
|
||||
|
||||
### MainLayout 布局
|
||||
经典 SaaS 后台管理布局:
|
||||
- **左侧 Sidebar** — 可折叠暗色菜单,分组:首页/用户/权限/设置
|
||||
- **顶部 Header** — 侧边栏切换 + 通知徽标(硬编码5) + 头像("Admin")
|
||||
- **中间 Content** — React Router Outlet,多标签页切换
|
||||
- **底部 Footer** — 租户名 + 版本号
|
||||
|
||||
### 状态管理 (Zustand)
|
||||
```typescript
|
||||
appStore {
|
||||
isLoggedIn: boolean // 未使用,无登录页
|
||||
tenantName: string // 默认 "ERP Platform"
|
||||
theme: 'light' | 'dark' // 切换 Ant Design 主题
|
||||
sidebarCollapsed: boolean
|
||||
toggleSidebar(), setTheme(), login(), logout()
|
||||
}
|
||||
```
|
||||
|
||||
### 开发服务器代理
|
||||
```
|
||||
http://localhost:5173/api/* → http://localhost:3000/* (API)
|
||||
ws://localhost:5173/ws/* → ws://localhost:3000/* (WebSocket)
|
||||
```
|
||||
|
||||
### 当前状态
|
||||
- 布局壳体完整,暗色/亮色主题切换可用
|
||||
- 只有一个路由 `/` → 占位 HomePage ("Welcome to ERP Platform")
|
||||
- 无 API 调用、无认证流程、无真实数据
|
||||
- 通知计数硬编码为 5,用户名硬编码为 "Admin"
|
||||
- 未实现 i18n(代码中有 zh_CN locale 但文案硬编码)
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — API 后端,通过 Vite proxy 连接
|
||||
- **[[infrastructure]]** — Docker 提供 PostgreSQL + Redis
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `apps/web/src/main.tsx` | React 入口 |
|
||||
| `apps/web/src/App.tsx` | 根组件:ConfigProvider + 路由 |
|
||||
| `apps/web/src/layouts/MainLayout.tsx` | 完整后台管理布局 |
|
||||
| `apps/web/src/stores/app.ts` | Zustand 全局状态 |
|
||||
| `apps/web/src/index.css` | TailwindCSS 导入 |
|
||||
| `apps/web/vite.config.ts` | Vite 配置 + API 代理 |
|
||||
| `apps/web/package.json` | 依赖声明 |
|
||||
|
||||
## 待实现 (按 Phase)
|
||||
|
||||
| Phase | 内容 |
|
||||
|-------|------|
|
||||
| Phase 2 | 登录页、用户管理页、角色权限页 |
|
||||
| Phase 3 | 系统配置管理页 |
|
||||
| Phase 4 | 工作流设计器、审批列表 |
|
||||
| Phase 5 | 消息中心、通知设置 |
|
||||
65
wiki/index.md
Normal file
65
wiki/index.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# ERP 平台底座 — 知识库
|
||||
|
||||
## 项目画像
|
||||
|
||||
**模块化 SaaS ERP 底座**,Rust + React 技术栈,提供身份权限/工作流/消息/配置四大基础模块,支持行业业务模块快速插接。
|
||||
|
||||
关键数字:
|
||||
- 8 个 Rust crate(4 个 placeholder),1 个前端 SPA
|
||||
- 1 个数据库迁移(tenant 表)
|
||||
- Phase 1 基础设施完成约 85%
|
||||
|
||||
## 模块导航树
|
||||
|
||||
### L1 基础层
|
||||
- [[erp-core]] — 错误体系 · 事件总线 · 模块 trait · 共享类型
|
||||
- [[erp-common]] — 共享工具(当前为 stub)
|
||||
|
||||
### L2 业务层(均为 placeholder)
|
||||
- erp-auth — 身份与权限(Phase 2)
|
||||
- erp-config — 系统配置(Phase 3)
|
||||
- erp-workflow — 工作流引擎(Phase 4)
|
||||
- erp-message — 消息中心(Phase 5)
|
||||
|
||||
### L3 组装层
|
||||
- [[erp-server]] — Axum 服务入口 · 配置加载 · 数据库连接 · 迁移执行
|
||||
|
||||
### 基础设施
|
||||
- [[database]] — SeaORM 迁移 · 多租户表结构 · 软删除模式
|
||||
- [[infrastructure]] — Docker Compose · PostgreSQL 16 · Redis 7
|
||||
- [[frontend]] — React SPA · Ant Design 布局 · Zustand 状态
|
||||
|
||||
### 横切关注点
|
||||
- [[architecture]] — 架构决策记录 · 设计原则 · 技术选型理由
|
||||
|
||||
## 核心架构决策
|
||||
|
||||
**模块间如何通信?** 通过 [[erp-core]] 的 EventBus 发布/订阅 DomainEvent,不直接依赖。
|
||||
|
||||
**多租户怎么隔离?** 共享数据库 + tenant_id 列过滤,中间件从 JWT 注入 TenantContext。详见 [[database]] 和 [[architecture]]。
|
||||
|
||||
**错误怎么传播?** 业务 crate 用 thiserror → AppError → Axum IntoResponse 自动转 HTTP。详见 [[erp-core]] 错误处理链。
|
||||
|
||||
**为什么没有路由?** Phase 1 只搭建基础设施。ModuleRegistry 已定义但未集成到 [[erp-server]],Phase 2 开始注册路由。
|
||||
|
||||
**版本差异怎么办?** package.json 使用 React 19 + Ant Design 6(比规格文档更新),以实际代码为准。
|
||||
|
||||
## 开发进度
|
||||
|
||||
| Phase | 内容 | 状态 |
|
||||
|-------|------|------|
|
||||
| 1 | 基础设施 | 85% — 缺 README、ModuleRegistry 集成、中间件 |
|
||||
| 2 | 身份与权限 | 待开始 |
|
||||
| 3 | 系统配置 | 待开始 |
|
||||
| 4 | 工作流引擎 | 待开始 |
|
||||
| 5 | 消息中心 | 待开始 |
|
||||
| 6 | 整合与打磨 | 待开始 |
|
||||
|
||||
## 关键文档索引
|
||||
|
||||
| 文档 | 位置 |
|
||||
|------|------|
|
||||
| 设计规格 | `docs/superpowers/specs/2026-04-10-erp-platform-base-design.md` |
|
||||
| 实施计划 | `docs/superpowers/plans/2026-04-10-erp-platform-base-plan.md` |
|
||||
| 协作规则 | `CLAUDE.md` |
|
||||
| 设计评审 | `plans/squishy-pondering-aho-agent-a23c7497aadc6da41.md` |
|
||||
57
wiki/infrastructure.md
Normal file
57
wiki/infrastructure.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# infrastructure (Docker 与开发环境)
|
||||
|
||||
## 设计思想
|
||||
|
||||
开发环境使用 Docker Compose 提供基础设施服务,应用服务在宿主机运行。这种设计允许:
|
||||
- 后端 Rust 服务快速重启(无需容器化构建)
|
||||
- 前端 Vite 热更新直接在宿主机
|
||||
- 数据库和缓存服务标准化,团队成员环境一致
|
||||
|
||||
## 代码逻辑
|
||||
|
||||
### 服务配置
|
||||
|
||||
| 服务 | 镜像 | 端口 | 用途 |
|
||||
|------|------|------|------|
|
||||
| erp-postgres | postgres:16-alpine | 5432 | 主数据库 |
|
||||
| erp-redis | redis:7-alpine | 6379 | 缓存 + 会话 |
|
||||
|
||||
### 连接信息
|
||||
```
|
||||
PostgreSQL: postgres://erp:erp_dev_2024@localhost:5432/erp
|
||||
Redis: redis://localhost:6379
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
- PostgreSQL: `pg_isready` 每 5 秒,5 次重试
|
||||
- Redis: `redis-cli ping` 每 5 秒,5 次重试
|
||||
|
||||
### 数据持久化
|
||||
- `postgres_data` — 命名卷,PostgreSQL 数据
|
||||
- `redis_data` — 命名卷,Redis 数据
|
||||
|
||||
### 环境变量
|
||||
通过 `docker/.env.example` 文档化,使用默认值即可启动开发环境。
|
||||
|
||||
## 关联模块
|
||||
|
||||
- **[[erp-server]]** — 连接 PostgreSQL 和 Redis
|
||||
- **[[database]]** — 迁移在 PostgreSQL 中执行
|
||||
- **[[frontend]]** — Vite 代理 API 到后端
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docker/docker-compose.yml` | 服务定义 |
|
||||
| `docker/.env.example` | 环境变量模板 |
|
||||
| `crates/erp-server/config/default.toml` | 默认连接配置 |
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
cd docker && docker compose up -d # 启动服务
|
||||
docker compose -f docker/docker-compose.yml ps # 查看状态
|
||||
docker compose -f docker/docker-compose.yml down # 停止
|
||||
docker exec -it erp-postgres psql -U erp # 连接数据库
|
||||
```
|
||||
Reference in New Issue
Block a user