Files
erp/docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md
iven 5e89aef99f docs: 修订 Q2 实施计划 — 修复审查发现的 14 个问题
关键修正:
- AuditLog::new() 签名与代码库一致 (tenant_id, user_id, action, resource_type)
- with_changes() 参数为 Option<Value>,调用时需 Some() 包装
- 限流 fail-closed 使用 (StatusCode, Json) 元组模式
- 添加缺失的 Task 7.5: 登录租户解析 (X-Tenant-ID)
- Task 7 添加 assign_roles 到修复列表
- Task 10 明确所有 auth 函数签名变更需求
- Task 12 添加 import 和参数说明
- Task 14 添加 docker-compose.override.yml 开发端口恢复
- 统一环境变量名为 ERP__SUPER_ADMIN_PASSWORD
2026-04-17 17:02:03 +08:00

24 KiB
Raw Blame History

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 历史

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
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 中的:

url = "postgres://erp:erp_dev_2024@localhost:5432/erp"

改为:

url = "__MUST_SET_VIA_ENV__"

将:

secret = "change-me-in-production"

改为:

secret = "__MUST_SET_VIA_ENV__"

将:

super_admin_password = "Admin@2026"

改为:

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
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 = ... 之后),添加:

// ── 安全检查:拒绝默认密钥 ──────────────────────────
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: 验证默认配置启动被拒绝
ERP__JWT__SECRET="__MUST_SET_VIA_ENV__" cargo run -p erp-server

Expected: 进程退出,输出包含 "JWT 密钥为默认值,拒绝启动"

  • Step 3: 验证环境变量设置后正常启动
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
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 改为显式错误处理

将:

let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
    .unwrap_or_else(|_| "Admin@2026".to_string());

改为:

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: 验证编译通过
cargo check -p erp-auth

Expected: 编译成功

  • Step 3: Commit
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 块:

// 删除以下代码
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_roundtripapp_error_other_maps_to_auth_validation 测试。

  • Step 4: 验证编译和测试
cargo check -p erp-auth && cargo test -p erp-auth

Expected: 编译成功,所有测试通过

  • Step 5: Commit
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 中的用户查询

将:

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)?;

改为:

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: 验证编译
cargo check -p erp-auth
  • Step 3: Commit
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.rsget_by_idupdatedeleteassign_roles 四个函数)

  • Step 1: 修改 get_by_id(约第 129-134 行)

find_by_id + 内存 .filter() 模式改为数据库级查询:

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: 同样修改 updatedeleteassign_roles 函数

将这三个函数中的 find_by_id + 内存 .filter() 改为相同的 DB 级过滤模式。注意:loginlist 函数已正确使用数据库级过滤,无需修改。

  • Step 3: 验证编译和测试
cargo check -p erp-auth && cargo test -p erp-auth
  • Step 4: Commit
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.rslogin 函数签名调整)

  • Step 1: 在 auth_handler.rslogin handler 中提取租户 ID

从请求头 X-Tenant-ID 提取租户 ID若无此头则使用默认租户向后兼容

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: 验证编译
cargo check -p erp-auth
  • Step 4: Commit
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

// 第一处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: 验证编译
cargo check -p erp-server
  • Step 3: Commit
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 之后)添加:

// 审计日志:登录成功
// 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

// 在 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

// 在密码验证失败返回 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: 验证编译

cargo check -p erp-auth
  • Step 6: Commit
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.rswith_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 中创建辅助函数提取请求信息:

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: 验证编译

cargo check -p erp-auth && cargo check -p erp-core
  • Step 5: Commit
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.rsupdate 函数)

  • Modify: crates/erp-auth/src/service/role_service.rsupdate 函数)

  • Step 1: 在 user_service::update 中,先查询旧值再更新

在 update 函数中,获取旧模型后、执行更新前,记录:

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: 验证编译
cargo check -p erp-auth
  • Step 5: Commit
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

use erp_core::{audit, audit_service};

然后在 create_recordupdate_record(含 partial_update)、delete_record 中添加审计日志。审计调用需要 tenant_idoperator_id

  • tenant_id 从函数参数获取
  • operator_id 从函数参数获取(若函数缺少此参数则需补充)

示例:

// create_record 审计
audit_service::record(
    audit::AuditLog::new(tenant_id, Some(operator_id), "plugin.data.create", entity_name),
    db,
).await;
  • Step 2: 验证编译
cargo check -p erp-plugin
  • Step 3: Commit
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: 创建工作流目录

mkdir -p .gitea/workflows
  • Step 2: 创建 CI 流水线文件

创建 .gitea/workflows/ci.yml

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
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}。 为两个服务添加资源限制:

deploy:
  resources:
    limits:
      cpus: "1.0"
      memory: 512M
  • Step 2: 创建开发用 docker-compose.override.yml

由于生产配置移除了端口暴露,本地开发需要 override 文件恢复端口访问:

# docker/docker-compose.override.yml — 本地开发用,不提交到仓库
services:
  postgres:
    ports:
      - "5432:5432"
  redis:
    ports:
      - "6379:6379"

docker-compose.override.yml 添加到 .gitignore。Docker Compose 会自动合并 docker-compose.ymldocker-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 配置有效
cd docker && docker compose config

Expected: 无语法错误

  • Step 5: Commit
git add docker/
git commit -m "fix(docker): 生产化配置 — 端口不暴露、Redis 密码、资源限制"

验证清单

完成所有 Task 后,执行以下验证:

  • V1: 默认配置拒绝启动
cargo run -p erp-server

Expected: 进程退出,日志包含 "JWT 密钥为默认值,拒绝启动"

  • V2: 环境变量设置后正常启动
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: 全量编译和测试
cargo check && cargo test --workspace

Expected: 全部通过

  • V4: 前端构建
cd apps/web && pnpm build

Expected: 构建成功

  • V5: Docker Compose 正常启动
cd docker && docker compose up -d && docker compose ps

Expected: PostgreSQL 和 Redis 状态 healthy

  • V6: Push 到远程仓库
git push origin main

Expected: Gitea Actions 触发 CI 流水线