feat(config): add system configuration module (Phase 3)

Implement the complete erp-config crate with:
- Data dictionaries (CRUD + items management)
- Dynamic menus (tree structure with role filtering)
- System settings (hierarchical: platform > tenant > org > user)
- Numbering rules (concurrency-safe via PostgreSQL advisory_lock)
- Theme and language configuration (via settings store)
- 6 database migrations (dictionaries, menus, settings, numbering_rules)
- Frontend Settings page with 5 tabs (dictionary, menu, numbering, settings, theme)

Refactor: move RBAC functions (require_permission) from erp-auth to erp-core
to avoid cross-module dependencies.

Add 20 new seed permissions for config module operations.
This commit is contained in:
iven
2026-04-11 08:09:19 +08:00
parent 8a012f6c6a
commit 0baaf5f7ee
55 changed files with 5295 additions and 12 deletions

View File

@@ -0,0 +1,329 @@
# Phase 3: 系统配置模块 — 实施计划
## Context
Phase 1基础设施和 Phase 2身份与权限已完成。Phase 3 需要实现系统配置模块erp-config提供数据字典、动态菜单、系统参数、编号规则、i18n 框架和主题自定义能力。当前 `erp-config` 仅为 placeholder。
## 前置重构:将 RBAC 移至 erp-core
**问题:** `require_permission``erp-auth/src/middleware/rbac.rs` 中,但只依赖 `erp-core``TenantContext``AppError`。erp-config 不能依赖 erp-auth架构铁律业务 crate 禁止直接依赖)。
**方案:**
1.`erp-auth/src/middleware/rbac.rs``erp-core/src/rbac.rs`
2. `erp-core/src/lib.rs` 添加 `pub mod rbac;`
3. 更新 `erp-auth` 所有 handler 的 import`crate::middleware::rbac``erp_core::rbac`
4. `erp-auth/src/middleware/mod.rs` 移除 `pub mod rbac;`
---
## Task 1: erp-config 骨架 + ConfigState
**目标:** 创建可编译的最小 crate注册到 erp-server。
**创建/修改文件:**
- 修改: `crates/erp-config/Cargo.toml` — 添加完整依赖
- 修改: `crates/erp-config/src/lib.rs` — 模块声明 + re-export
- 创建: `crates/erp-config/src/config_state.rs` — ConfigState { db, event_bus }
- 创建: `crates/erp-config/src/error.rs` — ConfigError 枚举
- 创建: `crates/erp-config/src/module.rs` — ConfigModule + 空路由
- 创建: `crates/erp-config/src/dto.rs` — 占位
- 创建: `crates/erp-config/src/entity/mod.rs` — 占位
- 创建: `crates/erp-config/src/service/mod.rs` — 占位
- 创建: `crates/erp-config/src/handler/mod.rs` — 占位
- 修改: `crates/erp-server/Cargo.toml` — 添加 erp-config 依赖
- 修改: `crates/erp-server/src/state.rs` — 添加 FromRef<AppState> for ConfigState
- 修改: `crates/erp-server/src/main.rs` — 注册 ConfigModule
**验证:** `cargo check` 通过
---
## Task 2: 数据库迁移6 张表)
**目标:** 创建所有配置模块表。
**创建文件(`crates/erp-server/migration/src/`**
- `m20260412_000012_create_dictionaries.rs` — 字典分类表
- `m20260412_000013_create_dictionary_items.rs` — 字典项表
- `m20260412_000014_create_menus.rs` — 菜单树形表
- `m20260412_000015_create_menu_roles.rs` — 菜单-角色关联表
- `m20260412_000016_create_settings.rs` — 分层键值配置表
- `m20260412_000017_create_numbering_rules.rs` — 编号规则表
- 修改: `lib.rs` — 注册新迁移
**表结构:**
### dictionaries
| 列 | 类型 | 说明 |
|---|---|---|
| id | uuid PK | UUIDv7 |
| tenant_id | uuid NOT NULL | 租户 ID |
| name | string NOT NULL | 显示名称 |
| code | string NOT NULL | 字典键(如 gender |
| description | text NULL | 说明 |
| + 标准审计字段 | | |
| 唯一索引: | `(tenant_id, code) WHERE deleted_at IS NULL` | |
### dictionary_items
| 列 | 类型 | 说明 |
|---|---|---|
| id | uuid PK | UUIDv7 |
| tenant_id | uuid NOT NULL | |
| dictionary_id | uuid NOT NULL | FK → dictionaries |
| label | string NOT NULL | 显示标签 |
| value | string NOT NULL | 存储值 |
| sort_order | int DEFAULT 0 | 排序 |
| color | string NULL | 颜色标签 |
| + 标准审计字段 | | |
| 唯一索引: | `(dictionary_id, value) WHERE deleted_at IS NULL` | |
### menus树形自引用
| 列 | 类型 | 说明 |
|---|---|---|
| id | uuid PK | UUIDv7 |
| tenant_id | uuid NOT NULL | |
| parent_id | uuid NULL | 自引用 |
| title | string NOT NULL | 菜单标题 |
| path | string NULL | 前端路由 |
| icon | string NULL | 图标名 |
| sort_order | int DEFAULT 0 | |
| visible | bool DEFAULT true | |
| menu_type | string DEFAULT 'page' | page/link/button |
| permission | string NULL | 所需权限码 |
| + 标准审计字段 | | |
### menu_roles复合主键
| 列 | 类型 | 说明 |
|---|---|---|
| menu_id | uuid NOT NULL | PK 组成 |
| role_id | uuid NOT NULL | PK 组成 |
| tenant_id | uuid NOT NULL | |
| + 标准审计字段 | | |
| 唯一索引: | `(menu_id, role_id) WHERE deleted_at IS NULL` | |
### settings分层配置
| 列 | 类型 | 说明 |
|---|---|---|
| id | uuid PK | UUIDv7 |
| tenant_id | uuid NOT NULL | |
| scope | string NOT NULL | platform/tenant/org/user |
| scope_id | uuid NULL | 平台=NULL, 租户=tenant_id 等 |
| setting_key | string NOT NULL | |
| setting_value | jsonb DEFAULT '{}' | |
| + 标准审计字段 | | |
| 唯一索引: | `(scope, scope_id, setting_key) WHERE deleted_at IS NULL` | |
### numbering_rules
| 列 | 类型 | 说明 |
|---|---|---|
| id | uuid PK | UUIDv7 |
| tenant_id | uuid NOT NULL | |
| name | string NOT NULL | 规则名称 |
| code | string NOT NULL | 唯一编码(如 INV |
| prefix | string DEFAULT '' | 前缀 |
| date_format | string NULL | 如 %Y%m%d |
| seq_length | int DEFAULT 4 | 序列位数 |
| seq_start | int DEFAULT 1 | 起始值 |
| seq_current | bigint DEFAULT 0 | 当前序列 |
| separator | string DEFAULT '-' | 分隔符 |
| reset_cycle | string DEFAULT 'never' | never/daily/monthly/yearly |
| last_reset_date | date NULL | |
| + 标准审计字段 | | |
| 唯一索引: | `(tenant_id, code) WHERE deleted_at IS NULL` | |
**验证:** `cargo run -p erp-server` 启动后 `\dt` 可见新表
---
## Task 3: SeaORM Entity
**目标:** 为 6 张表创建 Entity 定义。
**创建文件(`crates/erp-config/src/entity/`**
- `mod.rs` — 导出所有实体
- `dictionary.rs` — Dictionary Entity
- `dictionary_item.rs` — DictionaryItem Entity
- `menu.rs` — Menu Entity
- `menu_role.rs` — MenuRole Entity复合主键模式参考 role_permission
- `setting.rs` — Setting Entity
- `numbering_rule.rs` — NumberingRule Entity
**模式:** 参考 `erp-auth/src/entity/role.rs`,包含 Relation 和 Related 实现。
**验证:** `cargo check` 通过
---
## Task 4: DTO 定义
**目标:** 定义所有配置端点的请求/响应类型。
**修改文件:** `crates/erp-config/src/dto.rs`
**包含:**
- 字典 DTODictionaryResp, DictionaryItemResp, CreateDictionaryReq, UpdateDictionaryReq, CreateDictionaryItemReq, UpdateDictionaryItemReq
- 菜单 DTOMenuResp含 children 递归), CreateMenuReq, UpdateMenuReq, BatchSaveMenuReq
- 设置 DTOSettingResp, UpdateSettingReq
- 编号规则 DTONumberingRuleResp, CreateNumberingRuleReq, UpdateNumberingRuleReq, GenerateNumberResp
- 主题 DTOThemeResp, UpdateThemeReq委托 settings 存储)
- 语言 DTOLanguageResp, UpdateLanguageReq委托 settings 存储)
**验证:** `cargo check` 通过
---
## Task 5: Service — 字典 + 系统参数
**创建文件(`crates/erp-config/src/service/`**
- `dictionary_service.rs` — CRUD + 字典项管理 + 按 code 查询
- `setting_service.rs` — 分层读取User>Org>Tenant>Platform+ 写入
**关键逻辑:**
- SettingService::get 实现分层覆盖查找
- SettingService::set 使用 upsert 语义INSERT ON CONFLICT UPDATE
- DictionaryService 遵循 RoleService 的无状态模式
---
## Task 6: Service — 菜单 + 编号规则
**创建文件(`crates/erp-config/src/service/`**
- `menu_service.rs` — 菜单树构建 + 角色过滤 + 批量保存
- `numbering_service.rs` — 编号规则 CRUD + generate_number
**关键逻辑:**
- MenuService::get_menu_tree — 按 role_ids 过滤 menu_roleHashMap 分组构建树(参考 OrgService 的 build_org_tree
- NumberingService::generate_number — PostgreSQL advisory_lock + 事务内序列递增:
```sql
SELECT pg_advisory_xact_lock(hashtext($1), $2::int)
-- $1 = rule_code, $2 = hash(tenant_id)
```
生成格式:`{prefix}{separator}{date}{separator}{seq_padded}`
---
## Task 7: Handler 层
**创建文件(`crates/erp-config/src/handler/`**
- `dictionary_handler.rs` — 5 个端点
- `menu_handler.rs` — 2 个端点
- `setting_handler.rs` — 2 个端点
- `numbering_handler.rs` — 4 个端点(含 generate
- `theme_handler.rs` — 2 个端点(委托 SettingService
- `language_handler.rs` — 2 个端点(委托 SettingService
**端点映射:**
```
GET/POST /config/dictionaries
PUT/DELETE /config/dictionaries/{id}
GET/PUT /config/menus
GET/PUT /config/settings/{key}
GET/POST /config/numbering-rules
PUT /config/numbering-rules/{id}
POST /config/numbering-rules/{id}/generate
GET/PUT /config/themes
GET /config/languages
PUT /config/languages/{code}
```
---
## Task 8: 模块注册 + 种子数据
**修改文件:**
- `crates/erp-config/src/module.rs` — 填充真实路由
- `crates/erp-auth/src/service/seed.rs` — 添加配置模块权限dictionary/menu/setting/numbering/theme/language
- `crates/erp-server/src/main.rs` — 确认路由集成
**新增种子权限17 个):**
- dictionary: create/read/update/delete/list
- menu: read/update
- setting: read/update
- numbering: create/read/update/generate
- theme: read/update
- language: read/update
**验证:** `cargo check` + `cargo test` 通过,服务器启动后 Swagger UI 可见新端点
---
## Task 9: 前端 API 层 + Settings 页面框架
**创建文件(`apps/web/src/`**
- `api/dictionaries.ts` — 字典 CRUD API
- `api/menus.ts` — 菜单 API
- `api/settings.ts` — 设置 API
- `api/numberingRules.ts` — 编号规则 API
- `pages/Settings.tsx` — Tabs 壳页面
**修改文件:**
- `App.tsx` — 替换 settings 占位组件
**Tabs 结构:** 数据字典 | 菜单配置 | 编号规则 | 系统参数 | 主题设置
---
## Task 10: 前端设置子页面
**创建文件(`apps/web/src/pages/settings/`**
- `DictionaryManager.tsx` — Table + 展开行显示字典项,参考 Roles.tsx
- `MenuConfig.tsx` — Tree + 编辑表单,参考 Organizations.tsx
- `NumberingRules.tsx` — Table + Modal CRUD + 生成按钮
- `SystemSettings.tsx` — 键值编辑列表
- `ThemeSettings.tsx` — 颜色选择器 + 表单
**修改文件:**
- `Settings.tsx` — 导入子组件
- `MainLayout.tsx` — 更新设置菜单图标
**验证:** `pnpm dev` 启动,访问 /settings 各 tab 可正常交互
---
## 依赖图
```
前置重构RBAC→erp-core
|
Task 1骨架
|
Task 2迁移→ Task 3Entity→ Task 4DTO
|
+----------------+----------------+
| |
Task 5字典+设置 Service Task 6菜单+编号 Service
| |
+----------------+----------------+
|
Task 7Handler
|
Task 8集成+种子)
|
Task 9前端API+壳)
|
Task 10前端页面
```
## 验证清单
- [ ] `cargo check` 全 workspace 通过
- [ ] `cargo test --workspace` 全部通过
- [ ] Docker 环境正常启动
- [ ] 所有迁移可正/反向执行
- [ ] API 端点可通过 Swagger UI 测试
- [ ] 前端 /settings 页面各 Tab 正常工作
- [ ] 所有代码已提交
## 关键参考文件
| 用途 | 文件路径 |
|------|----------|
| Service 模式 | `crates/erp-auth/src/service/role_service.rs` |
| Handler 模式 | `crates/erp-auth/src/handler/role_handler.rs` |
| 树构建模式 | `crates/erp-auth/src/service/org_service.rs` |
| 迁移模式 | `crates/erp-server/migration/src/m20260411_000005_create_roles.rs` |
| State 桥接 | `crates/erp-server/src/state.rs` |
| 复合主键 Entity | `crates/erp-auth/src/entity/role_permission.rs` |
| 前端 Table CRUD | `apps/web/src/pages/Roles.tsx` |
| 前端树形展示 | `apps/web/src/pages/Organizations.tsx` |
| RBAC 工具函数 | `crates/erp-auth/src/middleware/rbac.rs`(待迁移到 erp-core |