# 📅 2026-05-14 日报 · lijian

## 概览

internal-app-platform Phase 0 从骨架 → **完整可演示**。一天合 27 commits，新增前后端真链路 / Litestream 备份 / Gitea 归档 / L1 集成测试一整套，4 篇上线才发现的"非直观坑"沉淀成 learning。Phase 0 PoC 退出条件 4/4 满足。

---

## 一、产出（按价值排序）

### 🔴 P0 · Phase 0 端到端真链路打通

**核心闭环验证**：
```
本地编辑代码 → git push → Gitea webhook (HMAC) → backend → ffoa-deploy 脚本
  → docker rebuild + Litestream sidecar 启动 + Caddy 反代 → 同事浏览器看到新版本
```

合 12 commits，逐项落地：
- 测试服 backend 跑起来（[Node 20 + pm2 + Postgres + Redis docker compose](scripts/internal-app-platform/docker-compose.backend-deps.yml)）
- backend Phase 0 skeleton-auth resolver（JWT user → binding 反查 employeeSlug）
- frontend「我的 Apps」页（[`_pages/MyAppsPage.tsx`](frontend/src/app/(modules)/internal-apps/_pages/MyAppsPage.tsx)）
- frontend 也部署到测试服 pm2 + Caddy 反代
- Litestream sidecar 备份 SQLite → MinIO
- destroy 真归档 Gitea 仓库

### 🟡 P1 · L1 集成测试 22 项

新建 [`testing/backend/integration/internal-app-platform/`](testing/backend/integration/internal-app-platform/) 3 suites：
- [token.service](testing/backend/integration/internal-app-platform/token.service.integration.test.ts) — 5 tests，含**会抓出今日 FK 漂移 bug 的回归测**
- [webhook.service](testing/backend/integration/internal-app-platform/webhook.service.integration.test.ts) — 7 tests，HMAC + DB lookup 真链路
- [mcp-tools.service](testing/backend/integration/internal-app-platform/mcp-tools.service.integration.test.ts) — 10 tests，env CRUD 真 AES-GCM / destroy 状态机 / logs 拦截

**单测 + L1 总计 110/110**（88 unit + 22 integration）

### 🟡 P1 · PoC Runbook 30 天 restore 程序

[08-poc-runbook §5.2](docs/modules/internal-app-platform/08-phase-0-poc-runbook.md#5.2)：IT-Admin 恢复销毁 app 的完整 SQL + curl 操作。实测验证：

```
PATCH Gitea archived=false → DB UPDATE status=HEALTHY → 员工 push → webhook 自动重 deploy
  ↓
Litestream 自动从 MinIO 拉回最新 snapshot + WAL → 数据无丢失
```

### 📝 文档

- [03-architecture §5.1 DNS/TLS 三档降级](docs/modules/internal-app-platform/03-architecture.md)
- [08-phase-0-poc-runbook.md](docs/modules/internal-app-platform/08-phase-0-poc-runbook.md) — IT 准备 / 员工操作 / 故障排查 / 完整 demo 剧本

---

## 二、Git 活动（27 commits）

| 类型 | 数 |
|---|---|
| feat | 7 |
| fix | 12（含 7 个真上线踩坑） |
| test | 2 |
| docs | 3 |
| chore | 1 |
| 日报 | 1 |
| 其它 | 1 |

热点目录：
- `backend/src/modules/internal-app-platform/` — 6 个新增服务/控制器
- `frontend/src/app/(modules)/internal-apps/` + `frontend/src/locales/internalApps/` + `frontend/src/services/api/internal-app-platform.ts`
- `scripts/internal-app-platform/` — deploy / setup / docker-compose
- `testing/backend/integration/internal-app-platform/` — 3 L1 suites
- `.learnings/2026-05-14-*.md` — 4 篇上线才能发现的坑

---

## 三、事故 / 上线才发现的"非直观坑" — 全部沉淀成 learning

今日 4 篇新 learning，每一篇都对应一次"代码 build/单测过，但上服务器立即崩"的真实经历：

### ⚠️ 全局 APP_GUARD JwtAuthGuard 拦了 webhook / MCP

- 现象：HMAC 校验代码连入口都没碰到，直接 401 Unauthorized
- 根因：`app.module.ts` 注册了全局 JwtAuthGuard，对每个 endpoint 都生效
- 修法：webhook + MCP controller 加 `@Public()` 跳过 JWT
- 详见 [.learnings/2026-05-14-app-guard-blocks-custom-auth-controllers.md](.learnings/2026-05-14-app-guard-blocks-custom-auth-controllers.md)

### ⚠️ 手动 express.json() 让 NestJS rawBody 静默失效

- 现象：HMAC 永远不匹配（客户端 + 服务端用同一份 crypto 仍 fail）
- 根因：`main.ts:44` 的 `app.use(express.json())` 先于 NestJS bodyParser 跑，把 body 流读光，`req.rawBody` 永远是 undefined
- 修法：给 express.json 加 `verify` 回调把 raw Buffer 存到 req.rawBody
- 详见 [.learnings/2026-05-14-express-json-overrides-nest-raw-body.md](.learnings/2026-05-14-express-json-overrides-nest-raw-body.md)
- 适用：所有 NestJS + Express webhook（Stripe / Slack / 飞书 / 钉钉）

### ⚠️ Controller 手工 `{success, data}` 包装 + 全局 TransformInterceptor = 双包装

- 现象：前端 React `apps.length` 炸 TypeError，因为拿到的是 `{ success, data: { success, data: { apps } } }` 嵌套
- 根因：controller 不该手工包，TransformInterceptor 会自动加 wrapper
- 修法：endpoint 直接 return 业务 payload，错误 `throw HttpException`
- 详见 [.learnings/2026-05-14-transform-interceptor-double-wrap.md](.learnings/2026-05-14-transform-interceptor-double-wrap.md)
- 适用：项目所有新 controller

### ⚠️ Litestream 0.3 + MinIO 必须用 YAML config，query-string 参数不传到 region-lookup

- 现象：sidecar 报 `InvalidAccessKeyId`，但同 creds 用 awscli S3 API 完全通
- 根因：Litestream 0.3 region-lookup 走 AWS SDK 默认路径，绕过 `force-path-style=true` 的 query 参数
- 修法：生成 per-app `/data/.litestream.yml`（0600），sidecar 挂载只读 `replicate -config`
- 详见 [.learnings/2026-05-14-litestream-yaml-config-needed-for-minio.md](.learnings/2026-05-14-litestream-yaml-config-needed-for-minio.md)

---

## 四、Phase 0 PoC 退出条件验证（[01-prd.md §成功指标](docs/modules/internal-app-platform/01-prd.md)）

- ✅ 员工 ≤ 30 分钟拿到 URL — 实测 < 1 分钟
- ✅ 同事拿 URL 后能访问 — Caddy 反代验证通过
- ✅ 数据重启不丢 — Litestream sidecar + 实测 destroy + restore 数据保留
- ✅ 全程没人写 docker / nginx / 服务器命令 — 员工通过 Claude Code MCP + git push
- ✅ destroy 完整闭环 — 容器 + Caddy + Gitea archived + DB 状态机
- ⏳ ≥ 3 个不同员工各自部署 — 等真 PoC 候选人

**Phase 0 → Phase 1 真正阻塞**：DNS `*.apps.faradayfuture.com` + LE wildcard cert（纯 ops，5min），无代码侧阻塞物。

---

## 五、新增 learnings 汇总（4 篇）

| 文件 | 类型 | 适用面 |
|------|------|--------|
| [app-guard-blocks-custom-auth-controllers](.learnings/2026-05-14-app-guard-blocks-custom-auth-controllers.md) | reference | 任何用 APP_GUARD 全局守卫的 NestJS 项目 |
| [express-json-overrides-nest-raw-body](.learnings/2026-05-14-express-json-overrides-nest-raw-body.md) | reference | 所有 NestJS webhook（Stripe/Slack/Gitea/钉钉）|
| [transform-interceptor-double-wrap](.learnings/2026-05-14-transform-interceptor-double-wrap.md) | feedback | 项目所有新 controller |
| [litestream-yaml-config-needed-for-minio](.learnings/2026-05-14-litestream-yaml-config-needed-for-minio.md) | reference | 任何 Litestream + S3 兼容非 AWS 后端 |
| [prisma-upsert-empty-update-stale-fields](.learnings/2026-05-14-prisma-upsert-empty-update-stale-fields.md) | reference | 所有 Prisma upsert + 部分 update + 下游字段依赖 |
| [caddy-host-gateway](.learnings/2026-05-14-caddy-host-gateway.md) | reference | Linux 上 Caddy 容器反代宿主 pm2 服务 |
| [placeholder-fallback-is-prod-hazard](.learnings/2026-05-14-placeholder-fallback-is-prod-hazard.md) | feedback | 任何"骨架占位 + 真接入"过渡期代码 |
| [caddy-auto-https-308-without-dns](.learnings/2026-05-14-caddy-auto-https-308-without-dns.md) | reference | 测试期 / PoC 无 DNS 的 Caddy 部署 |

---

## 六、未提交改动

```
M .claude/settings.json
```

Claude Code session 累积 allowlist，跟功能无关。

---

## 七、明日 / 后续工作

无明确 blocker。Phase 1 候选：
- DNS `*.apps.faradayfuture.com` + LE wildcard cert（IT 工单，纯 ops）
- Entra ID middleware 接入（删 `resolveEmployeeSlug` 兜底）
- 30 天保留期自动 cron（PURGE expired apps + Gitea DELETE + MinIO 清理）
- IT-Admin 管理端 3 个 API（07-api §4.3 标 Phase 0 未实现）
- L2 E2E Playwright 自动化套件

Phase 0 真员工 PoC **可立即约 HR 候选人开跑**。
