From 9f8518888645fe031167fd234c979a592cce3415 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 16:51:51 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Q2=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=9C=B0=E5=9F=BA=20+=20CI/CD=20=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 14 个 Task 覆盖:密钥外部化、启动强制检查、多租户加固、 限流 fail-closed、审计日志补全、Gitea Actions CI/CD、Docker 生产化 --- .../2026-04-17-platform-maturity-q2-plan.md | 765 ++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md diff --git a/docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md b/docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md new file mode 100644 index 0000000..6ef9d93 --- /dev/null +++ b/docs/superpowers/plans/2026-04-17-platform-maturity-q2-plan.md @@ -0,0 +1,765 @@ +# 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 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`): + +```bash +# .env.development — 本地开发用,不提交到仓库 +ERP__DATABASE__URL=postgres://erp:erp_dev_2024@localhost:5432/erp +ERP__JWT__SECRET=dev-local-secret-change-me +ERP__AUTH__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 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 for AuthError { + fn from(err: AppError) -> Self { + match err { + AppError::VersionMismatch => AuthError::VersionMismatch, + other => AuthError::Validation(other.to_string()), + } + } +} +``` + +- [ ] **Step 2: 修复所有依赖此反向映射的调用点** + +搜索 `AuthError` 从 `AppError` 隐式转换的位置: + +```bash +grep -rn "map_err.*AppError" crates/erp-auth/src/ +grep -rn "?.*AuthError" crates/erp-auth/src/ +``` + +将 `on_tenant_deleted` 等函数中的 `AppError` → `AuthError` 转换改为直接返回 `AppError`(函数签名可能需从 `AuthResult` 改为 `AppResult`,或在调用点显式 `.map_err()`)。 + +- [ ] **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 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 中声明的租户 +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` 三个函数) + +- [ ] **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::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` 函数** + +将这两个函数中的 `find_by_id` + 内存 `.filter()` 改为相同的 DB 级过滤模式。 + +- [ ] **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 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 限流保护"); + return RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }.into_response(); +} + +// 第二处:连接失败(约第 135-137 行) +Err(e) => { + tracing::warn!(error = %e, "Redis 连接失败,fail-closed 限流保护"); + avail.mark_failed().await; + return RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }.into_response(); +} + +// 第三处:INCR 失败(约第 143-145 行) +Err(e) => { + tracing::warn!(error = %e, "Redis INCR 失败,fail-closed 限流保护"); + return RateLimitResponse { + error: "Too Many Requests".to_string(), + message: "服务暂时不可用,请稍后重试".to_string(), + }.into_response(); +} +``` + +注意:需将 `RateLimitResponse` 结构体移到函数外部或使其可访问。 + +- [ ] **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 +// 审计日志:登录成功 +audit_service::record( + audit::AuditLog::new("user.login", user_model.tenant_id, user_model.id) + .with_detail("username", &req.username), + db, +).await; +``` + +- [ ] **Step 2: 在 `login` 函数失败路径(密码错误/用户禁用)添加审计** + +在返回 `InvalidCredentials` / `UserDisabled` 之前添加审计日志(不含敏感信息): + +```rust +// 审计日志:登录失败 +audit_service::record( + audit::AuditLog::new("user.login_failed", tenant_id, Uuid::nil()) + .with_detail("username", &req.username) + .with_detail("reason", "invalid_credentials"), + 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` handler 中从 `ConnectInfo` / `X-Forwarded-For` / `X-Real-IP` 提取 IP,从 `User-Agent` header 提取 UA,传给 `AuthService::login`。 + +这可能需要扩展 `login` 函数签名,添加 `ip: Option` 和 `user_agent: Option` 参数。 + +- [ ] **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); + +audit_service::record( + audit::AuditLog::new("user.update", tenant_id, operator_id) + .with_changes(old_json, new_json), + db, +).await; +``` + +- [ ] **Step 2: 同样修改 `role_service::update`** + +- [ ] **Step 3: 确认 `with_changes` 方法存在于 `audit.rs`** + +如果不存在则添加: +```rust +pub fn with_changes(mut self, old: serde_json::Value, new: serde_json::Value) -> Self { + self.old_value = Some(old); + self.new_value = Some(new); + self +} +``` + +- [ ] **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: 在 `create_record`、`update_record`、`delete_record` 中添加审计** + +每个 CUD 操作添加审计日志记录,包含 `plugin_id`、`entity_name`、`record_id`。 + +- [ ] **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: 更新 `.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 流水线