Files
erp/plans/stateless-swimming-perlis.md
iven 0baaf5f7ee 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.
2026-04-11 08:09:19 +08:00

330 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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 |