Compare commits
6 Commits
9fb73788f7
...
2bd274b39a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd274b39a | ||
|
|
d6dc47ab6a | ||
|
|
5e89aef99f | ||
|
|
9f85188886 | ||
|
|
b6c4e14b58 | ||
|
|
432eb2f9f5 |
871
docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md
Normal file
871
docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md
Normal file
@@ -0,0 +1,871 @@
|
||||
# Q2 安全地基 + CI/CD 实施计划
|
||||
|
||||
> **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:** 消除所有 CRITICAL/HIGH 安全风险,建立 CI/CD 自动化质量门,完成审计日志补全和 Docker 生产化。
|
||||
|
||||
**Architecture:** 密钥外部化通过环境变量强制注入 + 启动检查拒绝默认值;CI/CD 使用 Gitea Actions 四 job 并行;限流改为 fail-closed;审计日志补全 IP/UA 和变更值。
|
||||
|
||||
**Tech Stack:** Rust (Axum, SeaORM), Gitea Actions, Docker Compose, PostgreSQL 16, Redis 7
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md` §2
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Modify | `crates/erp-server/config/default.toml` | 敏感值改为占位符 |
|
||||
| Modify | `crates/erp-server/src/main.rs` | 启动时拒绝默认密钥 |
|
||||
| Modify | `crates/erp-auth/src/module.rs:149-150` | 移除密码 fallback |
|
||||
| Modify | `crates/erp-auth/src/error.rs:46-53` | 移除 `From<AppError> for AuthError` 反向映射 |
|
||||
| Modify | `crates/erp-auth/src/service/auth_service.rs:177-181` | refresh 添加 tenant_id 过滤 |
|
||||
| Modify | `crates/erp-auth/src/service/user_service.rs` | get_by_id/update/delete 改为 DB 级 tenant 过滤 |
|
||||
| Modify | `crates/erp-server/src/middleware/rate_limit.rs:122-124,135-137` | fail-closed |
|
||||
| Modify | `crates/erp-core/src/audit.rs` | `with_request_info` 类型扩展 |
|
||||
| Modify | `crates/erp-auth/src/service/auth_service.rs` | login/logout/change_password 添加审计 |
|
||||
| Modify | `crates/erp-plugin/src/data_service.rs` | CRUD 操作添加审计 |
|
||||
| Modify | `docker/docker-compose.yml` | 端口不暴露、Redis 密码、资源限制 |
|
||||
| Modify | `.gitignore` | 添加 `.test_token` |
|
||||
| Create | `.gitea/workflows/ci.yml` | CI/CD 流水线 |
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: 密钥外部化与启动强制检查
|
||||
|
||||
### Task 1: 清理 `.test_token` 和 `.gitignore`
|
||||
|
||||
**Files:**
|
||||
- Modify: `.gitignore`
|
||||
- Delete: `.test_token`(仅本地文件)
|
||||
|
||||
- [ ] **Step 1: 验证 `.test_token` 是否曾提交到 git 历史**
|
||||
|
||||
```bash
|
||||
git log --all --oneline -- .test_token
|
||||
```
|
||||
|
||||
Expected: 无输出(从未提交)。如果有输出,需额外执行 BFG 清理。
|
||||
|
||||
- [ ] **Step 2: 添加 `.test_token` 到 `.gitignore`**
|
||||
|
||||
在 `.gitignore` 末尾添加:
|
||||
|
||||
```
|
||||
# Test artifacts
|
||||
.test_token
|
||||
*.heapsnapshot
|
||||
perf-trace-*.json
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .gitignore
|
||||
git commit -m "chore: 添加 .test_token 和测试产物到 .gitignore"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `default.toml` 敏感值改为占位符
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-server/config/default.toml`
|
||||
|
||||
- [ ] **Step 1: 替换敏感值**
|
||||
|
||||
将 `crates/erp-server/config/default.toml` 中的:
|
||||
|
||||
```toml
|
||||
url = "postgres://erp:erp_dev_2024@localhost:5432/erp"
|
||||
```
|
||||
改为:
|
||||
```toml
|
||||
url = "__MUST_SET_VIA_ENV__"
|
||||
```
|
||||
|
||||
将:
|
||||
```toml
|
||||
secret = "change-me-in-production"
|
||||
```
|
||||
改为:
|
||||
```toml
|
||||
secret = "__MUST_SET_VIA_ENV__"
|
||||
```
|
||||
|
||||
将:
|
||||
```toml
|
||||
super_admin_password = "Admin@2026"
|
||||
```
|
||||
改为:
|
||||
```toml
|
||||
super_admin_password = "__MUST_SET_VIA_ENV__"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 `.env.development` 供本地开发使用**
|
||||
|
||||
在项目根目录创建 `.env.development`(已被 `.gitignore` 中 `*.env.local` 覆盖,但需显式添加 `.env.development`):
|
||||
|
||||
```
|
||||
# .env.development — 本地开发用,不提交到仓库
|
||||
# 注意:此文件需要手动 source 或通过 dotenv 工具加载,config crate 不会自动读取
|
||||
ERP__DATABASE__URL=postgres://erp:erp_dev_2024@localhost:5432/erp
|
||||
ERP__JWT__SECRET=dev-local-secret-change-me
|
||||
ERP__SUPER_ADMIN_PASSWORD=Admin@2026
|
||||
```
|
||||
|
||||
更新 `.gitignore`,添加 `.env.development`。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/config/default.toml .gitignore
|
||||
git commit -m "fix(security): default.toml 敏感值改为占位符,强制通过环境变量注入"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 启动检查 — 拒绝默认密钥
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-server/src/main.rs`(在服务启动前添加检查)
|
||||
|
||||
- [ ] **Step 1: 在 `main.rs` 的配置加载后、服务启动前添加安全检查**
|
||||
|
||||
在配置加载完成后(`let config = ...` 之后),添加:
|
||||
|
||||
```rust
|
||||
// ── 安全检查:拒绝默认密钥 ──────────────────────────
|
||||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
|
||||
tracing::error!(
|
||||
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.database.url == "__MUST_SET_VIA_ENV__" {
|
||||
tracing::error!(
|
||||
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证默认配置启动被拒绝**
|
||||
|
||||
```bash
|
||||
ERP__JWT__SECRET="__MUST_SET_VIA_ENV__" cargo run -p erp-server
|
||||
```
|
||||
|
||||
Expected: 进程退出,输出包含 "JWT 密钥为默认值,拒绝启动"
|
||||
|
||||
- [ ] **Step 3: 验证环境变量设置后正常启动**
|
||||
|
||||
```bash
|
||||
ERP__JWT__SECRET="my-real-secret" ERP__DATABASE__URL="postgres://erp:erp_dev_2024@localhost:5432/erp" ERP__AUTH__SUPER_ADMIN_PASSWORD="TestPass123" cargo run -p erp-server
|
||||
```
|
||||
|
||||
Expected: 服务正常启动(或因数据库未运行而失败,但不应因安全检查退出)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/src/main.rs
|
||||
git commit -m "fix(security): 启动时拒绝默认 JWT 密钥和数据库 URL"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 移除密码 fallback 硬编码
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/module.rs:149-150`
|
||||
|
||||
- [ ] **Step 1: 将 `unwrap_or_else` 改为显式错误处理**
|
||||
|
||||
将:
|
||||
```rust
|
||||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||||
.unwrap_or_else(|_| "Admin@2026".to_string());
|
||||
```
|
||||
|
||||
改为:
|
||||
```rust
|
||||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||||
.map_err(|_| {
|
||||
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
||||
erp_core::error::AppError::Internal(
|
||||
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
|
||||
)
|
||||
})?;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证编译通过**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth
|
||||
```
|
||||
|
||||
Expected: 编译成功
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/module.rs
|
||||
git commit -m "fix(security): 移除 super_admin_password 硬编码 fallback"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 移除 `From<AppError> for AuthError` 反向映射
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/error.rs:46-53`
|
||||
|
||||
- [ ] **Step 1: 删除反向映射 impl**
|
||||
|
||||
删除 `crates/erp-auth/src/error.rs` 中的整个 impl 块:
|
||||
|
||||
```rust
|
||||
// 删除以下代码
|
||||
impl From<AppError> for AuthError {
|
||||
fn from(err: AppError) -> Self {
|
||||
match err {
|
||||
AppError::VersionMismatch => AuthError::VersionMismatch,
|
||||
other => AuthError::Validation(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修复所有依赖此反向映射的调用点**
|
||||
|
||||
反向映射主要用于 `on_tenant_created` / `on_tenant_deleted` 中。检查这两个函数 — 它们已经返回 `AppResult<()>`(不是 `AuthResult`),所以不会直接受影响。
|
||||
|
||||
真正受影响的是 `auth_service.rs` 中可能从其他 crate 传入 `AppError` 并隐式转为 `AuthError` 的路径。逐一检查:
|
||||
- `auth_service.rs` — 所有 `.map_err()` 调用是否仍能编译
|
||||
- `user_service.rs` — 同上
|
||||
- 如果有编译错误,在调用点使用显式 `.map_err(|e| AuthError::Validation(e.to_string()))` 而非依赖隐式转换
|
||||
|
||||
- [ ] **Step 3: 删除反向映射的测试**
|
||||
|
||||
删除 `crates/erp-auth/src/error.rs` 测试中的 `app_error_version_mismatch_roundtrip` 和 `app_error_other_maps_to_auth_validation` 测试。
|
||||
|
||||
- [ ] **Step 4: 验证编译和测试**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth && cargo test -p erp-auth
|
||||
```
|
||||
|
||||
Expected: 编译成功,所有测试通过
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/
|
||||
git commit -m "refactor(auth): 移除 From<AppError> for AuthError 反向映射"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: 多租户安全加固 + 限流 fail-closed
|
||||
|
||||
### Task 6: `auth_service::refresh()` 添加 tenant_id 过滤
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/service/auth_service.rs:177-181`
|
||||
|
||||
- [ ] **Step 1: 修改 refresh 中的用户查询**
|
||||
|
||||
将:
|
||||
```rust
|
||||
let user_model = user::Entity::find_by_id(claims.sub)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||||
.ok_or(AuthError::TokenRevoked)?;
|
||||
```
|
||||
|
||||
改为:
|
||||
```rust
|
||||
let user_model = user::Entity::find_by_id(claims.sub)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||||
.ok_or(AuthError::TokenRevoked)?;
|
||||
|
||||
// 验证用户属于 JWT 中声明的租户
|
||||
// 注意:JWT claims 中租户 ID 字段名为 `tid`(与 TokenService 签发时一致)
|
||||
if user_model.tenant_id != claims.tid {
|
||||
tracing::warn!(
|
||||
user_id = %claims.sub,
|
||||
jwt_tenant = %claims.tid,
|
||||
actual_tenant = %user_model.tenant_id,
|
||||
"Token tenant_id 与用户实际租户不匹配"
|
||||
);
|
||||
return Err(AuthError::TokenRevoked);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/service/auth_service.rs
|
||||
git commit -m "fix(auth): refresh token 流程添加 tenant_id 校验"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: `user_service` 改为 DB 级 tenant_id 过滤
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/service/user_service.rs`(`get_by_id`、`update`、`delete`、`assign_roles` 四个函数)
|
||||
|
||||
- [ ] **Step 1: 修改 `get_by_id`(约第 129-134 行)**
|
||||
|
||||
将 `find_by_id` + 内存 `.filter()` 模式改为数据库级查询:
|
||||
|
||||
```rust
|
||||
pub async fn get_by_id(id: Uuid, tenant_id: Uuid, db: &DatabaseConnection) -> AuthResult<user::Model> {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::Id.eq(id))
|
||||
.filter(user::Column::TenantId.eq(tenant_id))
|
||||
.filter(user::Column::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||||
.ok_or(AuthError::Validation("用户不存在".to_string()))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 同样修改 `update`、`delete` 和 `assign_roles` 函数**
|
||||
|
||||
将这三个函数中的 `find_by_id` + 内存 `.filter()` 改为相同的 DB 级过滤模式。注意:`login`、`list` 函数已正确使用数据库级过滤,无需修改。
|
||||
|
||||
- [ ] **Step 3: 验证编译和测试**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth && cargo test -p erp-auth
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/service/user_service.rs
|
||||
git commit -m "fix(auth): get_by_id/update/delete 改为数据库级 tenant_id 过滤"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7.5: 登录租户解析
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/handler/auth_handler.rs`(登录 handler 提取租户信息)
|
||||
- Modify: `crates/erp-auth/src/service/auth_service.rs`(login 函数签名调整)
|
||||
|
||||
- [ ] **Step 1: 在 `auth_handler.rs` 的 `login` handler 中提取租户 ID**
|
||||
|
||||
从请求头 `X-Tenant-ID` 提取租户 ID,若无此头则使用默认租户(向后兼容):
|
||||
|
||||
```rust
|
||||
let tenant_id = headers
|
||||
.get("X-Tenant-ID")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| Uuid::parse_str(v).ok())
|
||||
.unwrap_or(state.default_tenant_id);
|
||||
```
|
||||
|
||||
将 `tenant_id` 传入 `AuthService::login`。
|
||||
|
||||
- [ ] **Step 2: 更新 `AuthService::login` 签名**
|
||||
|
||||
如果当前签名不含 `tenant_id` 参数,添加 `tenant_id: Uuid` 参数,替换函数内部对 `state.default_tenant_id` 的使用。
|
||||
|
||||
- [ ] **Step 3: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/handler/auth_handler.rs crates/erp-auth/src/service/auth_service.rs
|
||||
git commit -m "feat(auth): 登录接口支持 X-Tenant-ID 请求头租户解析"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 限流 fail-closed
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-server/src/middleware/rate_limit.rs:122-137`
|
||||
|
||||
- [ ] **Step 1: 将 Redis 不可达时的放行改为拒绝**
|
||||
|
||||
在 `apply_rate_limit` 函数中,将三处 `return next.run(req).await;` 改为返回 429:
|
||||
|
||||
```rust
|
||||
// 第一处:Redis 不可达快速检查(约第 122-124 行)
|
||||
if !avail.should_try().await {
|
||||
tracing::warn!("Redis 不可达,启用 fail-closed 限流保护");
|
||||
let body = RateLimitResponse {
|
||||
error: "Too Many Requests".to_string(),
|
||||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||||
};
|
||||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||||
}
|
||||
|
||||
// 第二处:连接失败(约第 135-137 行)
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Redis 连接失败,fail-closed 限流保护");
|
||||
avail.mark_failed().await;
|
||||
let body = RateLimitResponse {
|
||||
error: "Too Many Requests".to_string(),
|
||||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||||
};
|
||||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||||
}
|
||||
|
||||
// 第三处:INCR 失败(约第 143-145 行)
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Redis INCR 失败,fail-closed 限流保护");
|
||||
let body = RateLimitResponse {
|
||||
error: "Too Many Requests".to_string(),
|
||||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||||
};
|
||||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||||
}
|
||||
```
|
||||
|
||||
注意:`RateLimitResponse` 已在模块级别定义(第 17-20 行),无需移动。使用 `(StatusCode, Json)` 元组模式与现有代码一致。
|
||||
|
||||
- [ ] **Step 2: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-server
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/src/middleware/rate_limit.rs
|
||||
git commit -m "fix(server): 限流改为 fail-closed — Redis 不可达时拒绝请求"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: 审计日志补全
|
||||
|
||||
### Task 9: 登录/登出/密码修改添加审计日志
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/service/auth_service.rs`
|
||||
- Reference: `crates/erp-core/src/audit.rs`
|
||||
|
||||
- [ ] **Step 1: 在 `login` 函数成功路径添加审计**
|
||||
|
||||
在登录成功后(签发 token 之后)添加:
|
||||
|
||||
```rust
|
||||
// 审计日志:登录成功
|
||||
// AuditLog::new 签名:(tenant_id: Uuid, user_id: Option<Uuid>, action: &str, resource_type: &str)
|
||||
audit_service::record(
|
||||
audit::AuditLog::new(user_model.tenant_id, Some(user_model.id), "user.login", "user"),
|
||||
db,
|
||||
).await;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 `login` 函数失败路径添加审计**
|
||||
|
||||
失败审计需区分两种情况:
|
||||
|
||||
a) **用户不存在**(`find_by_username` 返回 None)— 此时无 `user_model`,使用 `Uuid::nil()` 作为 user_id:
|
||||
```rust
|
||||
// 在 Ok(None) => return Err(AuthError::InvalidCredentials) 之前添加
|
||||
audit_service::record(
|
||||
audit::AuditLog::new(tenant_id, None, "user.login_failed", "user")
|
||||
.with_resource_id("username", &req.username),
|
||||
db,
|
||||
).await;
|
||||
```
|
||||
|
||||
b) **密码错误** — 此时已有 `user_model`:
|
||||
```rust
|
||||
// 在密码验证失败返回 InvalidCredentials 之前添加
|
||||
audit_service::record(
|
||||
audit::AuditLog::new(tenant_id, Some(user_model.id), "user.login_failed", "user"),
|
||||
db,
|
||||
).await;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 在 `logout` 函数添加审计**
|
||||
|
||||
- [ ] **Step 4: 在 `change_password` 函数添加审计**
|
||||
|
||||
- [ ] **Step 5: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/service/auth_service.rs
|
||||
git commit -m "feat(auth): 登录/登出/密码修改添加审计日志"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 审计日志添加 IP 和 User-Agent
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-core/src/audit.rs`(确保 `with_request_info` 接受 IP + UA)
|
||||
- Modify: `crates/erp-auth/src/handler/auth_handler.rs`(从请求提取信息传入 service)
|
||||
|
||||
- [ ] **Step 1: 确认 `audit.rs` 中 `with_request_info` 的签名**
|
||||
|
||||
确认 `AuditLogBuilder::with_request_info(ip: String, user_agent: String)` 存在且类型正确。如果不存在则添加。
|
||||
|
||||
- [ ] **Step 2: 在 auth handler 中提取 IP 和 UA 并传给 service**
|
||||
|
||||
**必须修改**以下函数签名(不仅仅是 `login`):
|
||||
|
||||
- `AuthService::login` — 添加 `client_info: Option<ClientInfo>` 参数
|
||||
- `AuthService::logout` — 同上
|
||||
- `AuthService::change_password` — 同上
|
||||
|
||||
在 `auth_handler.rs` 中创建辅助函数提取请求信息:
|
||||
|
||||
```rust
|
||||
struct ClientInfo {
|
||||
ip: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
}
|
||||
|
||||
fn extract_client_info(req: &Request) -> ClientInfo {
|
||||
let ip = req.headers()
|
||||
.get("X-Forwarded-For")
|
||||
.or_else(|| req.headers().get("X-Real-IP"))
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string());
|
||||
let user_agent = req.headers()
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
ClientInfo { ip, user_agent }
|
||||
}
|
||||
```
|
||||
|
||||
在每个 auth handler 函数中调用 `extract_client_info` 并传给 service。
|
||||
|
||||
- [ ] **Step 3: 在审计日志记录时调用 `.with_request_info(ip, user_agent)`**
|
||||
|
||||
- [ ] **Step 4: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth && cargo check -p erp-core
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/ crates/erp-core/src/audit.rs
|
||||
git commit -m "feat(audit): 审计日志添加 IP 地址和 User-Agent"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 关键实体 update 添加变更前后值
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/service/user_service.rs`(`update` 函数)
|
||||
- Modify: `crates/erp-auth/src/service/role_service.rs`(`update` 函数)
|
||||
|
||||
- [ ] **Step 1: 在 `user_service::update` 中,先查询旧值再更新**
|
||||
|
||||
在 update 函数中,获取旧模型后、执行更新前,记录:
|
||||
|
||||
```rust
|
||||
let old_json = serde_json::to_value(&old_user)
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
// ... 执行更新 ...
|
||||
let new_json = serde_json::to_value(&updated_user)
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
// AuditLog::new 签名:(tenant_id, user_id, action, resource_type)
|
||||
// with_changes 签名:(Option<Value>, Option<Value>)
|
||||
audit_service::record(
|
||||
audit::AuditLog::new(tenant_id, Some(operator_id), "user.update", "user")
|
||||
.with_resource_id("user_id", &old_user.id.to_string())
|
||||
.with_changes(Some(old_json), Some(new_json)),
|
||||
db,
|
||||
).await;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 同样修改 `role_service::update`**
|
||||
|
||||
- [ ] **Step 3: 确认 `with_changes` 方法签名**
|
||||
|
||||
实际签名为 `with_changes(mut self, old: Option<Value>, new: Option<Value>) -> Self`,已在 `audit.rs` 第 51-59 行定义。调用时用 `Some()` 包装值。
|
||||
|
||||
- [ ] **Step 4: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-auth
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/ crates/erp-core/src/audit.rs
|
||||
git commit -m "feat(audit): 用户/角色更新记录变更前后值"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 插件 CRUD 添加审计日志
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-plugin/src/data_service.rs`
|
||||
|
||||
- [ ] **Step 1: 添加审计日志 import 和调用**
|
||||
|
||||
首先在 `data_service.rs` 顶部添加 import:
|
||||
```rust
|
||||
use erp_core::{audit, audit_service};
|
||||
```
|
||||
|
||||
然后在 `create_record`、`update_record`(含 `partial_update`)、`delete_record` 中添加审计日志。审计调用需要 `tenant_id` 和 `operator_id`:
|
||||
- `tenant_id` 从函数参数获取
|
||||
- `operator_id` 从函数参数获取(若函数缺少此参数则需补充)
|
||||
|
||||
示例:
|
||||
```rust
|
||||
// create_record 审计
|
||||
audit_service::record(
|
||||
audit::AuditLog::new(tenant_id, Some(operator_id), "plugin.data.create", entity_name),
|
||||
db,
|
||||
).await;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-plugin
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-plugin/src/data_service.rs
|
||||
git commit -m "feat(plugin): 数据 CRUD 操作添加审计日志"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: CI/CD + Docker 生产化
|
||||
|
||||
### Task 13: 创建 Gitea Actions CI/CD 流水线
|
||||
|
||||
**Files:**
|
||||
- Create: `.gitea/workflows/ci.yml`
|
||||
|
||||
- [ ] **Step 1: 创建工作流目录**
|
||||
|
||||
```bash
|
||||
mkdir -p .gitea/workflows
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 CI 流水线文件**
|
||||
|
||||
创建 `.gitea/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
rust-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: ". -> target"
|
||||
- run: cargo fmt --check --all
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
rust-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: erp_test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: ". -> target"
|
||||
- run: cargo test --workspace
|
||||
env:
|
||||
ERP__DATABASE__URL: postgres://test:test@localhost:5432/erp_test
|
||||
ERP__JWT__SECRET: ci-test-secret
|
||||
ERP__AUTH__SUPER_ADMIN_PASSWORD: CI_Test_Pass_2026
|
||||
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- run: cd apps/web && corepack enable && pnpm install --frozen-lockfile
|
||||
- run: cd apps/web && pnpm build
|
||||
|
||||
security-audit:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo install cargo-audit && cargo audit
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- run: cd apps/web && corepack enable && pnpm install --frozen-lockfile && pnpm audit
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .gitea/
|
||||
git commit -m "ci: 添加 Gitea Actions CI/CD 流水线"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 14: Docker 生产化
|
||||
|
||||
**Files:**
|
||||
- Modify: `docker/docker-compose.yml`
|
||||
|
||||
- [ ] **Step 1: 移除端口暴露,添加 Redis 密码和资源限制**
|
||||
|
||||
将 PostgreSQL 的 `ports: "5432:5432"` 改为 `expose: ["5432"]`(仅容器网络内部可访问)。
|
||||
将 Redis 的 `ports: "6379:6379"` 改为 `expose: ["6379"]`,并添加命令 `--requirepass ${REDIS_PASSWORD:-erp_redis_dev}`。
|
||||
为两个服务添加资源限制:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建开发用 `docker-compose.override.yml`**
|
||||
|
||||
由于生产配置移除了端口暴露,本地开发需要 override 文件恢复端口访问:
|
||||
|
||||
```yaml
|
||||
# docker/docker-compose.override.yml — 本地开发用,不提交到仓库
|
||||
services:
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
```
|
||||
|
||||
将 `docker-compose.override.yml` 添加到 `.gitignore`。Docker Compose 会自动合并 `docker-compose.yml` 和 `docker-compose.override.yml`。
|
||||
|
||||
- [ ] **Step 3: 更新 `.env.example`**
|
||||
|
||||
添加 `REDIS_PASSWORD` 变量说明。
|
||||
|
||||
- [ ] **Step 3: 更新 `default.toml` 的 Redis URL 格式**
|
||||
|
||||
如果 Redis 需要密码,URL 格式改为 `redis://:password@localhost:6379`。
|
||||
|
||||
- [ ] **Step 4: 验证 Docker Compose 配置有效**
|
||||
|
||||
```bash
|
||||
cd docker && docker compose config
|
||||
```
|
||||
|
||||
Expected: 无语法错误
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docker/
|
||||
git commit -m "fix(docker): 生产化配置 — 端口不暴露、Redis 密码、资源限制"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
完成所有 Task 后,执行以下验证:
|
||||
|
||||
- [ ] **V1: 默认配置拒绝启动**
|
||||
```bash
|
||||
cargo run -p erp-server
|
||||
```
|
||||
Expected: 进程退出,日志包含 "JWT 密钥为默认值,拒绝启动"
|
||||
|
||||
- [ ] **V2: 环境变量设置后正常启动**
|
||||
```bash
|
||||
ERP__JWT__SECRET="test-secret" ERP__DATABASE__URL="postgres://erp:erp_dev_2024@localhost:5432/erp" ERP__AUTH__SUPER_ADMIN_PASSWORD="TestPass123" cargo run -p erp-server
|
||||
```
|
||||
Expected: 服务正常启动
|
||||
|
||||
- [ ] **V3: 全量编译和测试**
|
||||
```bash
|
||||
cargo check && cargo test --workspace
|
||||
```
|
||||
Expected: 全部通过
|
||||
|
||||
- [ ] **V4: 前端构建**
|
||||
```bash
|
||||
cd apps/web && pnpm build
|
||||
```
|
||||
Expected: 构建成功
|
||||
|
||||
- [ ] **V5: Docker Compose 正常启动**
|
||||
```bash
|
||||
cd docker && docker compose up -d && docker compose ps
|
||||
```
|
||||
Expected: PostgreSQL 和 Redis 状态 healthy
|
||||
|
||||
- [ ] **V6: Push 到远程仓库**
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
Expected: Gitea Actions 触发 CI 流水线
|
||||
1112
docs/superpowers/plans/2026-04-17-platform-maturity-q3-plan.md
Normal file
1112
docs/superpowers/plans/2026-04-17-platform-maturity-q3-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
706
docs/superpowers/plans/2026-04-17-platform-maturity-q4-plan.md
Normal file
706
docs/superpowers/plans/2026-04-17-platform-maturity-q4-plan.md
Normal file
@@ -0,0 +1,706 @@
|
||||
# Q4 测试覆盖 + 插件生态 实施计划
|
||||
|
||||
> **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:** 建立 Testcontainers 集成测试框架覆盖核心模块;Playwright E2E 覆盖关键用户旅程;开发进销存插件验证插件系统扩展性;实现插件热更新能力。
|
||||
|
||||
**Architecture:** Testcontainers 启动真实 PostgreSQL 容器运行迁移后执行集成测试;Playwright 驱动浏览器完成端到端验证;进销存插件复用 CRM 插件的 manifest + dynamic_table 模式;热更新通过版本对比 + 增量 DDL + 两阶段提交实现。
|
||||
|
||||
**Tech Stack:** Rust (testcontainers, testcontainers-modules), Playwright, WASM (wit-bindgen), SeaORM Migration
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md` §4
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Create | `crates/erp-server/tests/integration/mod.rs` | 集成测试入口 |
|
||||
| Create | `crates/erp-server/tests/integration/test_db.rs` | Testcontainers 测试基座 |
|
||||
| Create | `crates/erp-server/tests/integration/auth_tests.rs` | Auth 模块集成测试 |
|
||||
| Create | `crates/erp-server/tests/integration/plugin_tests.rs` | Plugin 模块集成测试 |
|
||||
| Create | `crates/erp-server/tests/integration/workflow_tests.rs` | Workflow 模块集成测试 |
|
||||
| Create | `crates/erp-server/tests/integration/event_tests.rs` | EventBus 端到端测试 |
|
||||
| Create | `apps/web/e2e/login.spec.ts` | 登录流程 E2E |
|
||||
| Create | `apps/web/e2e/users.spec.ts` | 用户管理 E2E |
|
||||
| Create | `apps/web/e2e/plugins.spec.ts` | 插件安装 E2E |
|
||||
| Create | `apps/web/e2e/tenant-isolation.spec.ts` | 多租户隔离 E2E |
|
||||
| Create | `apps/web/playwright.config.ts` | Playwright 配置 |
|
||||
| Create | `crates/erp-plugin-inventory/` | 进销存插件 crate |
|
||||
| Modify | `Cargo.toml` | workspace 添加新 crate |
|
||||
| Modify | `crates/erp-plugin/src/engine.rs` | 热更新 upgrade 端点支持 |
|
||||
| Modify | `crates/erp-plugin/src/service.rs` | upgrade 生命周期 |
|
||||
| Modify | `crates/erp-plugin/src/handler/plugin_handler.rs` | upgrade 路由 |
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: 集成测试框架
|
||||
|
||||
### Task 1: 添加 Testcontainers 依赖
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-server/Cargo.toml`(dev-dependencies)
|
||||
|
||||
- [ ] **Step 1: 在 `erp-server` 的 `[dev-dependencies]` 中添加**
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
testcontainers = "0.23"
|
||||
testcontainers-modules = { version = "0.11", features = ["postgres"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
注意:版本号需与 workspace 已有依赖兼容。如果 workspace 已有 `testcontainers`,使用 workspace 引用。
|
||||
|
||||
- [ ] **Step 2: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-server
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/Cargo.toml Cargo.lock
|
||||
git commit -m "chore(server): 添加 testcontainers 开发依赖"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 创建测试基座
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/erp-server/tests/integration/mod.rs`
|
||||
- Create: `crates/erp-server/tests/integration/test_db.rs`
|
||||
|
||||
- [ ] **Step 1: 创建测试模块入口**
|
||||
|
||||
```rust
|
||||
// crates/erp-server/tests/integration/mod.rs
|
||||
mod test_db;
|
||||
mod auth_tests;
|
||||
mod plugin_tests;
|
||||
```
|
||||
|
||||
注意:需要确保 `erp-server` 的 `Cargo.toml` 中有 `[[test]]` 配置或集成测试自动发现。
|
||||
|
||||
- [ ] **Step 2: 创建 Testcontainers 测试基座**
|
||||
|
||||
```rust
|
||||
// crates/erp-server/tests/integration/test_db.rs
|
||||
use testcontainers_modules::postgres::Postgres;
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use sea_orm::{Database, DatabaseConnection};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 测试数据库容器 — 使用 once_cell 确保每进程一个容器
|
||||
pub struct TestDb {
|
||||
pub db: DatabaseConnection,
|
||||
pub container: testcontainers::ContainerAsync<Postgres>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub async fn new() -> Self {
|
||||
let postgres = Postgres::default()
|
||||
.with_db_name("erp_test")
|
||||
.with_user("test")
|
||||
.with_password("test");
|
||||
|
||||
let container = postgres.start().await
|
||||
.expect("Failed to start PostgreSQL container");
|
||||
|
||||
let host_port = container.get_host_port_ipv4(5432).await
|
||||
.expect("Failed to get port");
|
||||
|
||||
let url = format!("postgres://test:test@localhost:{}/erp_test", host_port);
|
||||
let db = Database::connect(&url).await
|
||||
.expect("Failed to connect to test database");
|
||||
|
||||
// 运行所有迁移
|
||||
run_migrations(&db).await;
|
||||
|
||||
Self { db, container }
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_migrations(db: &DatabaseConnection) {
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
Migrator::up(db, None).await.expect("Failed to run migrations");
|
||||
}
|
||||
```
|
||||
|
||||
注意:需要确保 `migration` crate 可被测试引用。可能需要调整 `Cargo.toml` 的依赖。
|
||||
|
||||
- [ ] **Step 3: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-server
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/tests/
|
||||
git commit -m "test(server): 创建 Testcontainers 集成测试基座"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Auth 模块集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/erp-server/tests/integration/auth_tests.rs`
|
||||
|
||||
- [ ] **Step 1: 编写用户 CRUD 测试**
|
||||
|
||||
```rust
|
||||
// crates/erp-server/tests/integration/auth_tests.rs
|
||||
use super::test_db::TestDb;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_crud() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = &test_db.db;
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
|
||||
// 创建用户
|
||||
let user = erp_auth::service::UserService::create(
|
||||
tenant_id,
|
||||
uuid::Uuid::new_v4(),
|
||||
erp_auth::dto::CreateUserReq {
|
||||
username: "testuser".to_string(),
|
||||
password: "TestPass123".to_string(),
|
||||
email: Some("test@example.com".to_string()),
|
||||
phone: None,
|
||||
display_name: Some("测试用户".to_string()),
|
||||
},
|
||||
db,
|
||||
&erp_core::events::EventBus::new(100),
|
||||
).await.expect("Failed to create user");
|
||||
|
||||
assert_eq!(user.username, "testuser");
|
||||
|
||||
// 查询用户
|
||||
let found = erp_auth::service::UserService::get_by_id(user.id, tenant_id, db)
|
||||
.await.expect("Failed to get user");
|
||||
assert_eq!(found.username, "testuser");
|
||||
|
||||
// 列表查询
|
||||
let (users, total) = erp_auth::service::UserService::list(
|
||||
tenant_id, erp_core::types::Pagination { page: 1, page_size: 10 }, None, db,
|
||||
).await.expect("Failed to list users");
|
||||
assert_eq!(total, 1);
|
||||
assert_eq!(users[0].username, "testuser");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写多租户隔离测试**
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_tenant_isolation() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = &test_db.db;
|
||||
let tenant_a = uuid::Uuid::new_v4();
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
|
||||
// 租户 A 创建用户
|
||||
let user_a = erp_auth::service::UserService::create(
|
||||
tenant_a,
|
||||
uuid::Uuid::new_v4(),
|
||||
erp_auth::dto::CreateUserReq {
|
||||
username: "user_a".to_string(),
|
||||
password: "Pass123!".to_string(),
|
||||
email: None, phone: None, display_name: None,
|
||||
},
|
||||
db,
|
||||
&erp_core::events::EventBus::new(100),
|
||||
).await.unwrap();
|
||||
|
||||
// 租户 B 查询不应看到租户 A 的用户
|
||||
let (users_b, total_b) = erp_auth::service::UserService::list(
|
||||
tenant_b, erp_core::types::Pagination { page: 1, page_size: 10 }, None, db,
|
||||
).await.unwrap();
|
||||
|
||||
assert_eq!(total_b, 0);
|
||||
assert!(users_b.is_empty());
|
||||
|
||||
// 租户 B 通过 ID 查询租户 A 的用户应返回 NotFound
|
||||
let result = erp_auth::service::UserService::get_by_id(user_a.id, tenant_b, db).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行测试验证**
|
||||
|
||||
```bash
|
||||
cargo test -p erp-server --test integration auth_tests
|
||||
```
|
||||
|
||||
注意:需要 Docker 运行。Windows 上可能需要 WSL2。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/tests/integration/auth_tests.rs
|
||||
git commit -m "test(auth): 添加用户 CRUD 和多租户隔离集成测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Plugin 模块集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/erp-server/tests/integration/plugin_tests.rs`
|
||||
|
||||
- [ ] **Step 1: 编写插件生命周期测试**
|
||||
|
||||
测试 install → enable → data CRUD → disable → uninstall 完整流程。
|
||||
|
||||
- [ ] **Step 2: 编写 JSONB 查询测试**
|
||||
|
||||
验证 dynamic_table 的 generated column、pg_trgm 索引是否正确创建。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/tests/integration/plugin_tests.rs
|
||||
git commit -m "test(plugin): 添加插件生命周期和 JSONB 集成测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Workflow + EventBus 集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/erp-server/tests/integration/workflow_tests.rs`
|
||||
- Create: `crates/erp-server/tests/integration/event_tests.rs`
|
||||
|
||||
- [ ] **Step 1: Workflow 测试 — 流程实例启动和任务完成**
|
||||
|
||||
- [ ] **Step 2: EventBus 测试 — 发布/订阅端到端 + outbox relay**
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-server/tests/integration/
|
||||
git commit -m "test: 添加 workflow 和 EventBus 集成测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: E2E 测试
|
||||
|
||||
### Task 6: Playwright 环境搭建
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/playwright.config.ts`
|
||||
- Modify: `apps/web/package.json`
|
||||
|
||||
- [ ] **Step 1: 安装 Playwright**
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm add -D @playwright/test && pnpm exec playwright install chromium
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 Playwright 配置**
|
||||
|
||||
```ts
|
||||
// apps/web/playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30000,
|
||||
retries: 1,
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
headless: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
webServer: {
|
||||
command: 'pnpm dev',
|
||||
port: 5173,
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web/playwright.config.ts apps/web/package.json
|
||||
git commit -m "test(web): 搭建 Playwright E2E 测试环境"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 登录流程 E2E 测试
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/e2e/login.spec.ts`
|
||||
|
||||
- [ ] **Step 1: 编写登录 E2E 测试**
|
||||
|
||||
```ts
|
||||
// apps/web/e2e/login.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('完整登录流程', async ({ page }) => {
|
||||
await page.goto('/#/login');
|
||||
await expect(page.locator('h2, .ant-card-head-title')).toContainText('登录');
|
||||
|
||||
// 输入凭据
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[placeholder*="密码"]', 'Admin@2026');
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
// 验证跳转到首页
|
||||
await page.waitForURL('**/'),
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
});
|
||||
```
|
||||
|
||||
注意:此测试需要后端服务运行。可在 CI 中使用 service container 或手动启动。
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web/e2e/login.spec.ts
|
||||
git commit -m "test(web): 添加登录流程 E2E 测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 用户管理 E2E 测试
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/e2e/users.spec.ts`
|
||||
|
||||
- [ ] **Step 1: 编写用户管理闭环测试**
|
||||
|
||||
创建 → 搜索 → 编辑 → 软删除 → 验证列表不显示。
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web/e2e/users.spec.ts
|
||||
git commit -m "test(web): 添加用户管理 E2E 测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 插件安装 + 多租户 E2E 测试
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/e2e/plugins.spec.ts`
|
||||
- Create: `apps/web/e2e/tenant-isolation.spec.ts`
|
||||
|
||||
- [ ] **Step 1: 插件安装 E2E 测试**
|
||||
|
||||
上传 → 安装 → 验证菜单 → 数据 CRUD → 卸载。
|
||||
|
||||
- [ ] **Step 2: 多租户隔离 E2E 测试**
|
||||
|
||||
租户 A 创建数据 → 切换租户 B → 验证不可见。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web/e2e/
|
||||
git commit -m "test(web): 添加插件安装和多租户 E2E 测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: 进销存插件
|
||||
|
||||
### Task 10: 创建插件 crate 骨架
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/erp-plugin-inventory/Cargo.toml`
|
||||
- Create: `crates/erp-plugin-inventory/src/lib.rs`
|
||||
- Create: `crates/erp-plugin-inventory/manifest.toml`
|
||||
- Modify: `Cargo.toml`(workspace members)
|
||||
|
||||
- [ ] **Step 1: 创建 Cargo.toml**
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "erp-plugin-inventory"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.38"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 manifest.toml**
|
||||
|
||||
定义 6 个实体(product, warehouse, stock, supplier, purchase_order, sales_order)的完整 schema,包括字段、关系、页面、权限声明。参考 CRM 插件的 `crates/erp-plugin-crm/manifest.toml`。
|
||||
|
||||
- [ ] **Step 3: 创建 lib.rs(Guest trait 实现)**
|
||||
|
||||
```rust
|
||||
// crates/erp-plugin-inventory/src/lib.rs
|
||||
wit_bindgen::generate!({
|
||||
path: "../erp-plugin-prototype/wit",
|
||||
world: "plugin-world",
|
||||
});
|
||||
|
||||
struct InventoryPlugin;
|
||||
|
||||
impl Guest for InventoryPlugin {
|
||||
fn init() -> Result<(), String> { Ok(()) }
|
||||
fn on_tenant_created(_tenant_id: String) -> Result<(), String> { Ok(()) }
|
||||
fn handle_event(_event_type: String, _event_data: String) -> Result<(), String> { Ok(()) }
|
||||
}
|
||||
|
||||
export_plugin!(InventoryPlugin);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 添加到 workspace**
|
||||
|
||||
在根 `Cargo.toml` 的 `members` 中添加 `"crates/erp-plugin-inventory"`。
|
||||
|
||||
- [ ] **Step 5: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-plugin-inventory
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-plugin-inventory/ Cargo.toml
|
||||
git commit -m "feat(inventory): 创建进销存插件 crate 骨架"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 定义实体 Schema
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-plugin-inventory/manifest.toml`
|
||||
|
||||
- [ ] **Step 1: 定义 6 个实体**
|
||||
|
||||
参考 CRM 插件 manifest 格式,定义:
|
||||
|
||||
| 实体 | 关键字段 | 关联 | 页面类型 |
|
||||
|------|---------|------|---------|
|
||||
| product | code, name, spec, unit, category, price, cost | — | CRUD |
|
||||
| warehouse | code, name, address, manager, status | — | CRUD |
|
||||
| stock | product_id, warehouse_id, qty, cost, alert_line | → product, warehouse | CRUD |
|
||||
| supplier | code, name, contact, phone, address | — | CRUD |
|
||||
| purchase_order | supplier_id, total_amount, status, date | → supplier, stock | CRUD + Dashboard |
|
||||
| sales_order | customer_id, total_amount, status, date | → customer(CRM), stock | CRUD + Kanban |
|
||||
|
||||
- [ ] **Step 2: 定义 6 个页面**(4 CRUD + 1 Dashboard 库存汇总 + 1 Kanban 销售看板)
|
||||
|
||||
- [ ] **Step 3: 定义 9 个权限**(每个实体 list/create/update/delete + 全局 manage)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-plugin-inventory/manifest.toml
|
||||
git commit -m "feat(inventory): 定义 6 实体/6 页面/9 权限 manifest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 编译 WASM 并测试安装
|
||||
|
||||
**Files:**
|
||||
- Build output: `apps/web/public/inventory.wasm`
|
||||
|
||||
- [ ] **Step 1: 编译为 WASM Component**
|
||||
|
||||
```bash
|
||||
cargo build -p erp-plugin-inventory --target wasm32-unknown-unknown --release
|
||||
wasm-tools component new target/wasm32-unknown-unknown/release/erp_plugin_inventory.wasm -o target/erp_plugin_inventory.component.wasm
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 复制到前端 public 目录**
|
||||
|
||||
```bash
|
||||
cp target/erp_plugin_inventory.component.wasm apps/web/public/inventory.wasm
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 通过 API 安装插件并验证**
|
||||
|
||||
使用 curl 或前端插件管理页面上传 `inventory.wasm`,验证动态表创建成功,CRUD 页面正常工作。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/web/public/inventory.wasm
|
||||
git commit -m "feat(inventory): 编译并部署进销存插件 WASM"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: 插件热更新
|
||||
|
||||
### Task 13: 添加 upgrade 端点
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-plugin/src/handler/plugin_handler.rs`
|
||||
- Modify: `crates/erp-plugin/src/service.rs`
|
||||
|
||||
- [ ] **Step 1: 在 plugin_handler 中添加 upgrade 路由**
|
||||
|
||||
```rust
|
||||
pub fn protected_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
// ... 现有路由 ...
|
||||
.route("/admin/plugins/:plugin_id/upgrade", post(upgrade_plugin))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 upgrade handler**
|
||||
|
||||
接收新 WASM 文件,调用 service 层执行升级。
|
||||
|
||||
- [ ] **Step 3: 在 service 中实现升级逻辑**
|
||||
|
||||
```rust
|
||||
pub async fn upgrade_plugin(
|
||||
plugin_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
new_wasm_bytes: Vec<u8>,
|
||||
db: &DatabaseConnection,
|
||||
) -> PluginResult<()> {
|
||||
// 1. 解析新 manifest
|
||||
let new_manifest = parse_manifest_from_wasm(&new_wasm_bytes)?;
|
||||
|
||||
// 2. 获取当前插件信息
|
||||
let current = find_by_id(plugin_id, tenant_id, db).await?;
|
||||
|
||||
// 3. 对比 schema 变更,生成增量 DDL
|
||||
let schema_diff = compare_schemas(¤t.manifest, &new_manifest)?;
|
||||
|
||||
// 4. 暂存新 WASM,尝试验证初始化
|
||||
// 5. 初始化成功后,在事务中执行 DDL + 状态更新
|
||||
// 6. 失败时保持旧 WASM 继续运行
|
||||
// 详见 spec §4.4 回滚策略
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 验证编译**
|
||||
|
||||
```bash
|
||||
cargo check -p erp-plugin
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-plugin/src/
|
||||
git commit -m "feat(plugin): 添加插件热更新 upgrade 端点"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 5: 文档更新与清理
|
||||
|
||||
### Task 14: 更新 Wiki 文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `wiki/frontend.md`
|
||||
- Modify: `wiki/database.md`
|
||||
- Modify: `wiki/testing.md`
|
||||
- Modify: `wiki/index.md`
|
||||
|
||||
- [ ] **Step 1: 更新 `wiki/frontend.md`**
|
||||
|
||||
更新为反映当前 16 条路由、6 种插件页面类型、Zustand stores 等实际状态。
|
||||
|
||||
- [ ] **Step 2: 更新 `wiki/testing.md`**
|
||||
|
||||
更新测试数量、添加 Testcontainers 集成测试和 Playwright E2E 描述。
|
||||
|
||||
- [ ] **Step 3: 更新 `wiki/index.md`**
|
||||
|
||||
添加进销存插件到模块导航树,更新开发进度表。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add wiki/
|
||||
git commit -m "docs: 更新 Wiki 文档到当前状态"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 15: CLAUDE.md 版本号修正 + 根目录清理
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md`
|
||||
- Cleanup: 根目录未跟踪文件
|
||||
|
||||
- [ ] **Step 1: 修正 CLAUDE.md 版本号**
|
||||
|
||||
将 `React 18 + Ant Design 5` 改为 `React 19 + Ant Design 6`。
|
||||
|
||||
- [ ] **Step 2: 清理根目录未跟踪文件**
|
||||
|
||||
删除开发临时文件:截图、heap dump、perf trace、agent plan 文件。
|
||||
|
||||
```bash
|
||||
rm -f current-page.png home-full.png home-improved.png docs/debug-*.png
|
||||
rm -f docs/memory-snapshot-*.heapsnapshot docs/perf-trace-*.json
|
||||
rm -f test_api_auth.py test_users.py
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 处理 integration-tests/ 目录**
|
||||
|
||||
验证 `integration-tests/` 中的测试是否能编译。若已失效则删除(新的集成测试在 `crates/erp-server/tests/integration/`)。若仍有效则添加到 workspace。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs: 修正 CLAUDE.md 版本号 (React 19 / AD 6) 并清理临时文件"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] **V1: 全 workspace 编译和测试**
|
||||
```bash
|
||||
cargo check && cargo test --workspace
|
||||
```
|
||||
|
||||
- [ ] **V2: 集成测试通过**
|
||||
```bash
|
||||
cargo test -p erp-server --test integration
|
||||
```
|
||||
注意:需要 Docker 运行
|
||||
|
||||
- [ ] **V3: 前端构建**
|
||||
```bash
|
||||
cd apps/web && pnpm build
|
||||
```
|
||||
|
||||
- [ ] **V4: E2E 测试**
|
||||
```bash
|
||||
cd apps/web && pnpm exec playwright test
|
||||
```
|
||||
|
||||
- [ ] **V5: 进销存插件安装验证**
|
||||
|
||||
通过 API 安装 inventory.wasm,验证动态表和 CRUD 页面正常。
|
||||
|
||||
- [ ] **V6: Wiki 文档同步**
|
||||
|
||||
确认 Wiki 描述与代码实际状态一致。
|
||||
@@ -0,0 +1,456 @@
|
||||
# ERP 平台底座 — 全面成熟度提升路线图
|
||||
|
||||
> 创建日期:2026-04-17
|
||||
> 状态:审查修订完成
|
||||
> 范围:安全、架构、测试、前端体验、插件生态 — 3 季度分层推进
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 项目现状
|
||||
|
||||
ERP 平台底座已完成 Phase 1-6 基础设施建设 + WASM 插件系统集成 + CRM 客户管理插件。当前具备:
|
||||
|
||||
- 6 个业务模块(auth, config, workflow, message, plugin, server)
|
||||
- 36 个数据库迁移
|
||||
- 完整的 WASM 插件运行时
|
||||
- Schema 驱动的动态前端(6 种页面类型)
|
||||
- React 19 + Ant Design 6 + Zustand 5 前端 SPA
|
||||
|
||||
### 1.2 分析发现摘要
|
||||
|
||||
| 维度 | 评分 | 关键问题 |
|
||||
|------|------|---------|
|
||||
| 架构健壮性 | 8/10 | ErpModule trait 死代码、路由注册未自动化 |
|
||||
| 代码质量 | 7/10 | N+1 查询、错误映射过宽、 oversized 组件 |
|
||||
| 安全性 | 5/10 | 3 个 CRITICAL(硬编码密钥/密码)、4 个 HIGH |
|
||||
| 测试覆盖 | 4/10 | 零数据库集成测试、关键流程未覆盖 |
|
||||
| 前端体验 | 7/10 | 无 i18n、无 Error Boundary、无虚拟滚动 |
|
||||
| 基础设施 | 4/10 | 无 CI/CD、Wiki 过时、大量未跟踪文件 |
|
||||
|
||||
### 1.3 目标
|
||||
|
||||
通过 3 个季度的分层改进,将平台从"功能完整"推进到"生产就绪":
|
||||
|
||||
- **Q2(4-5月)**:消除安全风险,建立自动化质量门
|
||||
- **Q3(6-8月)**:强化架构,提升前端工程化水平
|
||||
- **Q4(9-11月)**:补齐测试覆盖,扩展插件生态
|
||||
|
||||
### 1.4 约束
|
||||
|
||||
- **独立开发者** + Claude 辅助 — 每季度聚焦单一维度
|
||||
- **SaaS 优先**部署 — 多租户安全是硬性要求
|
||||
- **不破坏现有功能** — 所有改进必须向后兼容
|
||||
|
||||
---
|
||||
|
||||
## 2. Q2:安全地基 + CI/CD(4-5月)
|
||||
|
||||
### 2.1 密钥外部化与启动强制检查
|
||||
|
||||
**问题:**
|
||||
- JWT 密钥 `"change-me-in-production"` 硬编码在 `crates/erp-server/config/default.toml`
|
||||
- 管理员密码 `"Admin@2026"` 硬编码 + fallback
|
||||
- 数据库凭据 `postgres://erp:erp_dev_2024@...` 硬编码
|
||||
- `.test_token` 含有效 admin JWT 提交到仓库
|
||||
|
||||
**方案:**
|
||||
|
||||
1. **配置强制化**:`default.toml` 只保留开发环境默认值。生产敏感值通过环境变量 `ERP__` 前缀注入(已有机制)
|
||||
2. **启动检查**:服务启动时检测 JWT 密钥是否为默认值,若是则 **拒绝启动**(返回错误退出码,不只是警告)
|
||||
3. **密码初始化**:`seed_tenant_auth` 从环境变量 `ERP__SUPER_ADMIN_PASSWORD` 读取初始密码(与现有 `module.rs:149` 中的变量名一致),未设置则拒绝初始化(移除 fallback 到硬编码值的逻辑)
|
||||
4. **清理 `.test_token`**:立即加入 `.gitignore`。验证该文件是否曾被提交到 git 历史 — 如果曾提交,需使用 BFG Repo-Cleaner 清理历史(因包含用硬编码密钥签名的 admin JWT,等同于密钥泄露)
|
||||
5. **`default.toml` 占位符**:敏感字段改为 `"__MUST_SET_VIA_ENV__"` 之类的明显占位值
|
||||
|
||||
**验证标准:**
|
||||
- 默认配置启动时服务拒绝运行
|
||||
- 环境变量设置后正常启动
|
||||
- `.test_token` 不再出现在仓库中
|
||||
|
||||
### 2.2 Gitea Actions CI/CD
|
||||
|
||||
**流水线设计:**
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
rust-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo fmt --check --all
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
rust-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env: { POSTGRES_DB: erp_test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo test --workspace
|
||||
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: cd apps/web && pnpm install && pnpm build
|
||||
|
||||
security-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo audit
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: cd apps/web && pnpm audit
|
||||
```
|
||||
|
||||
**关键决策:**
|
||||
- 使用 Gitea Actions(与 GitHub Actions 语法兼容)
|
||||
- 每个 job 包含 `actions/checkout@v4` + 对应语言 toolchain setup
|
||||
- Rust 使用 `Swatinem/rust-cache@v2` 缓存编译产物,避免每次全量编译
|
||||
- PostgreSQL 通过 service 容器提供
|
||||
- 四个 job 并行运行,互不依赖
|
||||
- 后续可扩展:Redis service、Playwright E2E、Docker 镜像构建推送
|
||||
|
||||
### 2.3 审计日志补全
|
||||
|
||||
**当前缺口与改进:**
|
||||
|
||||
| 缺口 | 改进方案 |
|
||||
|------|---------|
|
||||
| 登录/登出只发 DomainEvent,不写审计日志 | 在 `auth_service` 的 login/logout/change_password 中调用 `audit_service::record()` |
|
||||
| 审计日志缺少 `old_value`/`new_value` | 关键实体(user/role/permission/org)的 update 操作添加 `.with_changes(old, new)`。序列化完整的旧模型和新模型为 JSON,由审计日志消费者计算 diff — 比应用层计算细粒度 diff 更简单健壮 |
|
||||
| 缺少 IP 地址和 User-Agent | `AuditLogBuilder::with_request_info()` 在 handler 层传入请求上下文 |
|
||||
| 插件 CRUD 无审计 | `data_service` 的 create/update/delete 操作添加审计日志记录 |
|
||||
| 登录失败无记录 | 添加失败登录审计(含尝试的用户名/IP),用于入侵检测 |
|
||||
|
||||
**验证标准:**
|
||||
- 登录成功/失败均写入审计日志
|
||||
- 用户更新操作记录变更前后值
|
||||
- 审计日志包含 IP 和 User-Agent
|
||||
|
||||
### 2.4 Docker 生产化
|
||||
|
||||
| 改进项 | 当前 | 目标 |
|
||||
|--------|------|------|
|
||||
| PostgreSQL 端口 | `ports: "5432:5432"` 暴露到宿主机 | 移除 `ports:`,使用 Docker 网络内部通信 |
|
||||
| Redis 端口 | `ports: "6379:6379"` 无认证 | 移除 `ports:`,添加 `--requirepass` |
|
||||
| 容器资源限制 | 无 | CPU 1核 / 内存 512MB |
|
||||
| 应用镜像 | 无 Dockerfile | 多阶段构建:Rust build → 精简 runtime 镜像 |
|
||||
| Redis 宕机时限流 | fail-open(无限流) | fail-closed(拒绝请求) |
|
||||
|
||||
**限流 fail-closed 改动:**
|
||||
`crates/erp-server/src/middleware/rate_limit.rs` 中 Redis 不可用时,返回 `429 Too Many Requests` 而非放行。
|
||||
|
||||
### 2.5 多租户安全加固
|
||||
|
||||
| 问题 | 改进方案 |
|
||||
|------|---------|
|
||||
| 登录使用硬编码 `default_tenant_id` | 登录接口增加租户解析(从子域名/请求头 `X-Tenant-ID`) |
|
||||
| `auth_service::refresh()` 用户查询缺少 tenant_id(`auth_service.rs:177`) | `find_by_id` 添加 `.filter(user::Column::TenantId.eq(claims.tenant_id))` |
|
||||
| 内存级 tenant_id 过滤(`user_service.rs` 的 `get_by_id`/`update`/`delete`) | 改为数据库级 `.filter(Column::TenantId.eq(tenant_id))` 查询。注意:`login`/`list`/`assign_roles` 已正确使用数据库级过滤,无需修改 |
|
||||
|
||||
**涉及文件:**
|
||||
- `crates/erp-auth/src/handler/auth_handler.rs`
|
||||
- `crates/erp-auth/src/service/auth_service.rs`
|
||||
- `crates/erp-auth/src/service/user_service.rs`
|
||||
- `crates/erp-auth/src/middleware/jwt_auth.rs`
|
||||
|
||||
---
|
||||
|
||||
## 3. Q3:架构强化 + 前端体验(6-8月)
|
||||
|
||||
### 3.1 ErpModule Trait 重构
|
||||
|
||||
**当前问题:**
|
||||
- `register_event_handlers` 是死代码 — 所有模块实现为空操作
|
||||
- 路由注册需在 `main.rs` 手动编辑两处
|
||||
- 事件订阅在 `main.rs` 中手动调用,绕过 trait
|
||||
|
||||
**改进方案:**
|
||||
|
||||
基于当前 trait 签名(`erp-core/src/module.rs`),新增双路由注册和权限声明。保持与现有 `ModuleContext` 参数一致,不引入 `AppState` 依赖(避免 `erp-core` → `erp-server` 反向依赖):
|
||||
|
||||
```rust
|
||||
pub trait ErpModule: Send + Sync + 'static {
|
||||
// 保留已有方法
|
||||
fn name(&self) -> &str;
|
||||
fn version(&self) -> &str;
|
||||
fn module_type(&self) -> &str { "business" }
|
||||
fn dependencies(&self) -> Vec<&str> { vec![] }
|
||||
fn id(&self) -> Uuid { /* 默认实现 */ }
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
// 新增:双路由注册(匹配现有 public/protected 分离模式)
|
||||
fn register_public_routes(&self, router: Router) -> Router { router }
|
||||
fn register_protected_routes(&self, router: Router) -> Router { router }
|
||||
|
||||
// 重构:事件订阅真正生效(当前所有模块实现为空操作)
|
||||
fn register_event_handlers(&self, bus: &EventBus) {}
|
||||
|
||||
// 新增:模块权限声明
|
||||
fn permissions(&self) -> Vec<PermissionDef> { vec![] }
|
||||
|
||||
// 保留已有生命周期钩子(保持 ModuleContext 参数签名)
|
||||
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
async fn on_shutdown(&self) -> AppResult<()> { Ok(()) }
|
||||
async fn on_tenant_created(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
|
||||
async fn on_tenant_deleted(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
|
||||
async fn health_check(&self) -> AppResult<serde_json::Value> { Ok(serde_json::json!({})) }
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleRegistry::build()` 自动收集路由、事件处理器和权限,`main.rs` 简化为:
|
||||
|
||||
```rust
|
||||
let (registry, public_routes, protected_routes) = ModuleRegistry::new()
|
||||
.register(auth_module)
|
||||
.register(config_module)
|
||||
.register(workflow_module)
|
||||
.register(message_module)
|
||||
.register(plugin_module)
|
||||
.build();
|
||||
|
||||
// 自动组合:public_routes 直接挂载,protected_routes 包裹 JWT 中间件
|
||||
let app = Router::new()
|
||||
.merge(public_routes)
|
||||
.merge(protected_routes.layer(jwt_middleware));
|
||||
```
|
||||
|
||||
**迁移策略:** 逐模块迁移 — 每个模块从静态 `public_routes()`/`protected_routes()` 函数改为 trait 方法实现,`main.rs` 逐步简化。
|
||||
|
||||
**已知例外:** PluginModule 的两阶段初始化(先注册再启动事件监听器)在初期保持独立处理,不强行纳入自动化。`MessageModule::start_event_listener`、`WorkflowModule::start_timeout_checker`、`outbox::start_outbox_relay` 等独立生命周期钩子作为范围排除项,后续迭代再统一。
|
||||
|
||||
**迁移策略:** 逐模块迁移 — 每个模块从静态函数改为 trait 方法实现,`main.rs` 逐步简化。
|
||||
|
||||
### 3.2 错误映射修正 + N+1 查询优化
|
||||
|
||||
**错误映射修正:**
|
||||
|
||||
当前 `erp-auth` 服务中直接 `.map_err(|e| AuthError::Validation(e.to_string()))` 将所有 `DbErr` 映射为 `Validation`,绕过了 `erp-core` 中已有的 `From<DbErr> for AppError` 语义映射(该映射已正确处理 `RecordNotFound` → `NotFound`、重复键 → `Conflict`)。
|
||||
|
||||
**修复策略:** `erp-auth` 服务层停止手动包装 `DbErr`,改为通过 `?` 操作符依赖 `DbErr → AppError` 的核心映射,通过现有的 `From<AuthError> for AppError` 转换传播。这样数据库连接错误会正确显示为 `Internal`,唯一约束冲突会正确显示为 `Conflict`。
|
||||
|
||||
移除 `From<AppError> for AuthError` 的反向映射(当前是 lossy wrapping — `AppError::NotFound` 变为 `AuthError::Validation`,丢失语义信息)。
|
||||
|
||||
**N+1 查询优化:**
|
||||
|
||||
`user_service.rs` 的 `list()` 方法改为批量查询:
|
||||
1. 先查询当前页用户列表
|
||||
2. 收集所有 `user_id`
|
||||
3. 一次 `WHERE user_id IN (...)` 查询 `user_role` + `role`
|
||||
4. 内存中按 `user_id` 分组组装
|
||||
|
||||
从 N+1 查询降为 3 次固定查询(用户列表 + 角色关联 + 角色详情)。
|
||||
|
||||
### 3.3 前端 Error Boundary + hooks 提取
|
||||
|
||||
**Error Boundary:**
|
||||
- `App.tsx` 根组件包裹全局 Error Boundary(捕获未预期崩溃)
|
||||
- 每个懒加载页面外包裹页面级 Error Boundary(隔离单页面崩溃)
|
||||
- 失败时展示友好错误页面 + 重试按钮
|
||||
|
||||
**hooks 提取:**
|
||||
|
||||
| Hook | 提取来源 | 用途 |
|
||||
|------|---------|------|
|
||||
| `usePaginatedData<T>` | 6+ 页面的分页加载逻辑 | 统一分页/搜索/加载状态 |
|
||||
| `useDarkMode` | 8+ 文件的 `token.colorBgContainer` 字符串比较 | 提供可靠的 boolean 暗色模式判断 |
|
||||
| `useCountUp` | Home.tsx + DashboardWidgets 重复实现 | 计数动画复用 |
|
||||
| `useDebouncedValue` | Users.tsx 等搜索输入 | 防抖搜索,避免每次按键触发 API |
|
||||
| `useApiRequest` | 所有页面的 try/catch + message.error | 统一 API 错误处理和消息提示 |
|
||||
|
||||
### 3.4 i18n 基础设施搭建
|
||||
|
||||
**方案:react-i18next**
|
||||
|
||||
- 安装 `react-i18next` + `i18next`
|
||||
- 创建 `locales/zh-CN.json`,提取所有硬编码中文为 key
|
||||
- 配置 i18next 初始化,默认 `zh-CN`
|
||||
- 用 `useTranslation()` hook 替换硬编码字符串
|
||||
|
||||
**实施策略:** 增量式 — 新页面强制使用 i18n,旧页面按模块逐步迁移。不强求一次性替换。
|
||||
|
||||
**命名规范:**
|
||||
- 页面文案:`{module}.{page}.{element}` 如 `auth.login.username`
|
||||
- 通用文案:`common.{action}` 如 `common.save`, `common.cancel`
|
||||
- 错误消息:`error.{type}` 如 `error.network`, `error.unauthorized`
|
||||
|
||||
### 3.5 行级数据权限接线
|
||||
|
||||
**当前状态:** 数据库列、SQL 条件构建器、manifest 声明已就绪,handler 层有 TODO 未实现。
|
||||
|
||||
**完成步骤:**
|
||||
1. JWT 中间件注入 `department_ids`(完成 `jwt_auth.rs:50` 的 TODO)
|
||||
2. `data_handler` 查询接口注入 data scope 条件
|
||||
3. 前端角色权限编辑页添加 `data_scope` 选择控件
|
||||
4. 端到端验证:创建测试角色 → 设置数据范围 → 验证查询过滤
|
||||
|
||||
### 3.6 前端共享类型统一
|
||||
|
||||
- `PaginatedResponse<T>` 从 `users.ts` 提取到 `api/types.ts`
|
||||
- 错误提取工具函数 `extractErrorMessage(err: unknown): string` → `api/errors.ts`
|
||||
- 插件 Schema 类型定义集中到 `types/plugin.ts`
|
||||
- 移除 `api/client.ts` 中已废弃的 `CancelToken`,改用 `AbortController`
|
||||
|
||||
---
|
||||
|
||||
## 4. Q4:测试覆盖 + 插件生态(9-11月)
|
||||
|
||||
### 4.1 Q4 范围调整说明
|
||||
|
||||
Q4 原始范围较大(Testcontainers + Playwright + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
|
||||
|
||||
- **Q4a(9-10月)**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
|
||||
- **Q4b(11月+)**:插件生态 — 进销存插件 + 热更新
|
||||
|
||||
热更新功能可视 Q4a 进度推迟到 Q1 2027,避免在单季度内承载过多工作。
|
||||
|
||||
### 4.2 数据库集成测试框架
|
||||
|
||||
**方案:Testcontainers + PostgreSQL**
|
||||
|
||||
创建 `crates/erp-server/tests/integration/` 目录,使用 `testcontainers` crate 启动真实 PostgreSQL 容器。
|
||||
|
||||
**测试基座:**
|
||||
- 每个测试套件共享一个 PostgreSQL 容器
|
||||
- 自动运行所有迁移
|
||||
- 提供 `setup_test_db()` 辅助函数返回连接池
|
||||
- 测试结束自动清理
|
||||
|
||||
**覆盖优先级:**
|
||||
|
||||
| 优先级 | 模块 | 测试场景 |
|
||||
|--------|------|---------|
|
||||
| P0 | erp-auth | 用户 CRUD、角色权限分配、登录/JWT 完整流程 |
|
||||
| P0 | erp-auth | 多租户隔离 — 租户 A 数据对租户 B 不可见 |
|
||||
| P0 | erp-plugin | 插件生命周期(install→enable→disable→uninstall) |
|
||||
| P1 | erp-auth | 乐观锁并发冲突、软删除恢复 |
|
||||
| P1 | erp-plugin | 行级数据权限过滤、JSONB 查询/索引 |
|
||||
| P1 | erp-plugin | 动态表 DDL 正确性(generated column、pg_trgm 索引) |
|
||||
| P1 | erp-workflow | 流程实例启动、任务完成、网关分支 |
|
||||
| P1 | erp-core | 事件总线发布/订阅端到端、outbox relay 补偿 |
|
||||
|
||||
### 4.2 核心流程 E2E 测试
|
||||
|
||||
**方案:Playwright**
|
||||
|
||||
放在 `apps/web/e2e/` 目录,CI 中作为独立 job 运行。
|
||||
|
||||
**覆盖场景(4 个关键旅程):**
|
||||
|
||||
| 场景 | 步骤 |
|
||||
|------|------|
|
||||
| 完整登录流程 | 打开登录页 → 输入密码 → 验证 token → 刷新 token → 登出 → 验证跳转 |
|
||||
| 用户管理闭环 | 创建用户 → 分配角色 → 搜索用户 → 编辑 → 软删除 → 验证列表不显示 |
|
||||
| 插件安装流程 | 上传 WASM → 安装 → 验证菜单出现 → 数据 CRUD → 卸载 → 验证菜单消失 |
|
||||
| 多租户隔离 | 租户 A 创建用户 → 切换租户 B → 验证查询结果为空 |
|
||||
|
||||
### 4.3 第二个行业插件 — 进销存(Inventory)
|
||||
|
||||
**选择理由:**
|
||||
- 与 CRM 有天然关联(客户 → 订单 → 出库)
|
||||
- 实体数量适中(5-8 个),复杂度可控
|
||||
- 能验证插件系统的复用性和跨实体关联能力
|
||||
- 为后续财务模块铺垫
|
||||
|
||||
**实体设计:**
|
||||
|
||||
| 实体 | 字段 | 关联 |
|
||||
|------|------|------|
|
||||
| product 商品 | 名称/编码/规格/单位/分类/售价/成本价 | — |
|
||||
| warehouse 仓库 | 名称/地址/负责人/状态 | — |
|
||||
| stock 库存 | 商品/仓库/数量/成本/预警线 | → product, warehouse |
|
||||
| purchase_order 采购单 | 供应商/总金额/状态/日期 | → supplier(CRM), stock |
|
||||
| sales_order 销售单 | 客户/总金额/状态/日期 | → customer(CRM), stock |
|
||||
| supplier 供应商 | 名称/编码/联系方式/地址 | — |
|
||||
|
||||
**需要验证的插件能力:**
|
||||
- 跨实体关联(订单 → 商品 → 库存联动)
|
||||
- 事务性事件(库存扣减在订单确认时原子执行)
|
||||
- 页面间导航(从订单跳转客户详情)
|
||||
- 报表/统计页面(库存汇总、进销存明细)
|
||||
|
||||
### 4.4 插件热更新能力
|
||||
|
||||
**当前限制:** 更新插件需要完整 uninstall/reinstall。
|
||||
|
||||
**改进方案:**
|
||||
- 新增 `POST /api/v1/admin/plugins/{id}/upgrade` 端点
|
||||
- 升级流程:上传新 WASM → 对比 manifest schema → 增量 DDL(ADD COLUMN 等) → 热替换 WASM 模块
|
||||
- 数据安全:`tenant_id` 数据不丢失
|
||||
- 版本兼容性检查:新版本必须向后兼容或提供迁移脚本
|
||||
|
||||
**回滚策略:** 升级前创建 schema 备份点。升级流程分两步执行:
|
||||
1. 先暂存新 WASM 并尝试验证初始化(不应用 DDL)
|
||||
2. 初始化成功后,在单事务中执行 DDL 变更 + 状态转换
|
||||
3. 如果新 WASM 初始化失败,保持旧 WASM 继续运行,回滚暂存状态
|
||||
4. DDL 已应用但 WASM 运行异常时,保留旧 WASM 可加载作为 fallback
|
||||
|
||||
### 4.5 文档更新与清理
|
||||
|
||||
| 项目 | 改进 |
|
||||
|------|------|
|
||||
| Wiki 文档 | 全面更新到当前状态(前端路由、测试数量、模块能力、插件系统) |
|
||||
| CLAUDE.md | 版本号修正(React 19 / Ant Design 6) |
|
||||
| 根目录清理 | 删除未跟踪的开发临时文件(截图、heap dump、perf trace、agent plan 文件) |
|
||||
| integration-tests/ | 验证现有测试是否能编译。若已失效则删除,用新的 Testcontainers 框架替代;若仍有效则纳入 Cargo workspace |
|
||||
| N+1 查询(plugin) | `plugin_service.rs` 的列表查询也存在 N+1 问题(每条插件单独查询 entities),需一并优化 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| 安全修复引入新 bug | 中 | 高 | 每个修复配有对应的测试用例 |
|
||||
| ErpModule trait 重构影响所有模块 | 高 | 中 | 逐模块迁移,每步验证 `cargo test` |
|
||||
| i18n 迁移工作量大 | 中 | 低 | 增量式,不追求一次性完成 |
|
||||
| Testcontainers 在 CI 环境不稳定 | 低 | 中 | 本地开发可跳过集成测试,CI 用 service container 兜底 |
|
||||
| Testcontainers 在 Windows (WSL2) 上兼容性 | 中 | 中 | 主开发环境为 Windows 11,Testcontainers 对 Windows 支持有限。本地开发可依赖 CI service container 运行集成测试,或使用 WSL2 环境 |
|
||||
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 成功标准
|
||||
|
||||
**Q2 完成标准:**
|
||||
- [ ] 3 个 CRITICAL 安全问题全部修复
|
||||
- [ ] Gitea Actions CI/CD 流水线运行通过
|
||||
- [ ] 默认配置启动被拒绝
|
||||
- [ ] 登录/登出写入审计日志
|
||||
- [ ] Docker 生产化配置就绪
|
||||
|
||||
**Q3 完成标准:**
|
||||
- [ ] ErpModule trait 路由注册自动化
|
||||
- [ ] N+1 查询优化,用户列表查询次数固定为 3
|
||||
- [ ] 前端 Error Boundary 覆盖全局 + 页面级
|
||||
- [ ] 5 个自定义 hooks 提取完成
|
||||
- [ ] i18n 基础设施可用,至少 1 个页面完成迁移
|
||||
- [ ] 行级数据权限端到端验证通过
|
||||
|
||||
**Q4 完成标准:**
|
||||
- [ ] 集成测试覆盖 auth + plugin 核心流程
|
||||
- [ ] 4 个 E2E 测试场景通过
|
||||
- [ ] 进销存插件 6 个实体可用
|
||||
- [ ] 插件热更新功能可用
|
||||
- [ ] Wiki 文档与代码同步
|
||||
Reference in New Issue
Block a user