feat: establish production SaaS foundation

This commit is contained in:
Codex
2026-07-12 19:26:57 +08:00
parent 1c2ce38cea
commit 39f7332f33
219 changed files with 20647 additions and 2628 deletions

View File

@@ -1,5 +1,12 @@
# SaaS 重构工作区
当前接管先读:
- `production-foundation-baseline-20260712.md`:后端/API 冻结结论、前端启动边界、生产硬阻断和正式上线顺序。
- `clean-room-migration-audit-20260712.md`:官方 Supabase PG15 空库 78 个迁移、扩展/ACL、运行角色、RLS、约束和幂等审计证据。
- `taro-h5-browser-qa-20260712.md`:学生端、租户后台、平台后台桌面/移动浏览器和主交互验收记录。
- `taro-supply-chain-baseline-20260712.md`Taro 4.2.0 安全 override、H5 runtime patch、干净安装与构建工具链风险边界。
这个目录记录从 PocketBase 单体项目迁移到 Supabase/PostgreSQL + 新 API + Taro 学生端的重构过程。
当前阶段目标:
@@ -18,7 +25,7 @@
- `scripts/import-pocketbase`PocketBase schema/数据导入工具。
- `docker-compose.api.yml``docker-compose.api.benchmark.yml``apps/api/Dockerfile`:本地 Docker API 和受限资源压测入口。
- `scripts/deploy/README.md`:云服务器部署 runbook覆盖 `tjszsb.com` 六域名规划、服务器目录、Gitea 安全部署、Nginx、systemd 和更新脚本。
- `scripts/deploy/bin/deploy.sh`:服务器端发布脚本模板,负责拉取 Gitea、构建 API/worker/Taro H5、发布静态文件和重启服务真实密钥只从 `/etc/tiku-saas/*.env` 读取。
- 根目录 `deploy.sh`:推荐的 release/symlink 原子发布入口,包含严格 runtime config、readiness、安全、H5 smoke、manifest 和真实 production launch gate`scripts/deploy/bin/deploy.sh` 仅保留为旧服务器兼容入口并同步执行同类门禁。真实密钥只从服务器受控 env 读取。
- `docs/refactor/architecture.md`:新重构目录边界和工程规范。
- `docs/refactor/ai-development-guardrails.md`:后续 AI/开发者必须遵守的 Supabase-first 架构和安全守则。
- `docs/refactor/content-import-contract.md`:题目、单词、知识手册导入契约,明确后端校验、旧格式转换和前端职责。
@@ -32,10 +39,11 @@
- `docs/refactor/taro-h5-deployment.md`Taro H5 三域名部署、运行时配置、Nginx、CSP、缓存和 CORS 边界。
- `docs/refactor/postgresql-4c16g-tuning.md`4 核 16G 自托管 PostgreSQL 起步调参、观察 SQL 和回滚方式。
- `docs/refactor/performance-benchmark-runbook.md`:本地/云端 API 压测、4 核 16G 阶梯并发矩阵、Docker 受限资源预演和报告归档方式。
- `docs/refactor/tenant-student-capacity-runbook.md`:单租户最多 10 万学生的安全合成夹具、cursor/搜索 SQL 基准、EXPLAIN 证据和专用命名空间清理流程。
- `docs/refactor/performance-benchmark-summary-20260630.md`:真实迁移数据压测脱敏摘要。
- `docs/refactor/backend-open-items-and-capacity-20260701.md`:后端剩余功能、已定稿产品口径和最新在线容量估算。
- `docs/refactor/multitenant-auth-security-contract.md`:多租户隔离、鉴权、权限和资源安全红线。
- `docs/refactor/production-launch-evidence.template.json`:生产上线证据模板;真实证据填入本地 `production-launch-evidence.json` 后运行 `npm run launch:gate`
- `docs/refactor/production-launch-evidence.template.json`:生产上线证据模板;真实 evidence 与 `launch-artifacts/` 作为完整 bundle 放在服务器受控目录,不进入 Git再运行 `npm run launch:gate -- --evidence <absolute-path>`
下一步优先级:

View File

@@ -48,7 +48,7 @@ docs/refactor/
```bash
npm run supabase:start
npm run supabase:reset
npm run db:smoke-seed
npm run db:smoke-seed -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
npm run dev:api
```
@@ -60,7 +60,7 @@ npm run docker:api:build
npm run docker:api:up
```
如果 Docker 拉取 `node:20-alpine` 超时,先配置 Docker Desktop 镜像源代理,再重试 `npm run docker:api:build`
API Dockerfile 锁定 `node:20.20.2-alpine3.23` 多架构 manifest最终镜像以 `node` 用户运行,只复制生产依赖和编译产物。若 Docker Hub 超时,先配置受信镜像源/代理并确认拉取到相同 digest,再重试 `npm run docker:api:build`,不要移除 digest 锁定
本地容量预演可以使用专门的 benchmark override
@@ -89,10 +89,10 @@ npm run pb:import:validate
- Docker Desktop 可用。
- Supabase 本地容器可启动。
- API Dockerfile 已验证可构建benchmark override 可启动受限 API 容器并通过短压测 smoke。
- API Dockerfile 已验证可构建;最终镜像约 `53 MB`、生产 `node_modules``24.3 MB`,不含 TypeScript/tsxUID 为 `1000(node)`,连接隔离测试库通过 `/health`benchmark override 可启动受限 API 容器并通过短压测 smoke。
- `supabase db reset` 可完整执行三份 migration 和 seed。
- `supabase db reset` 可完整执行全部 migration 和 seed。
- `npm run db:smoke-seed`恢复最小业务烟测数据。
- `npm run db:smoke-seed -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY` 只能在已标记为 `local/test/ci` 的隔离库恢复最小业务烟测数据。
- `platform-admin` 可完成平台概览、租户创建、订阅、账单生成、人工收款确认、使用量记录。
- API `/health` 可连 PostgreSQL 并返回 `db: ok`
- API `/api/tenant/resolve?host=localhost` 可解析主租户。

View File

@@ -1,6 +1,8 @@
# 后端进度同步与前端接入路线图
更新时间2026-06-30
更新时间2026-07-12
> 权威决策已迁移到 `production-foundation-baseline-20260712.md`。本文继续保留详细能力清单,凡与 2026-07-12 基线冲突的历史表述,以该基线为准。
这份文档用于在进入 Taro 前端开发前,快速确认新 Supabase/PostgreSQL 后端已经做到哪里、还缺什么、前端应如何接入,以及后续继续开发的优先级。
@@ -14,7 +16,7 @@
- 销售/代理/CRM 已经有邀请码、扫码/分享事件、首绑客资保护、团队关系、统计、销售/代理转化报表、CRM 配置、入队、worker 推送、失败死信运营、手动重试/忽略和分佣结算基础闭环。
- 旧题库 JSON、单词模板、知识手册嵌套模板、分数线 JSON 和视频绑定 JSON 已经进入后端 preview/import 管线,由后端负责规范化、校验、幂等、审计和租户隔离。
因此,后端现在已经具备进入 Taro 前端第一阶段联调的基础。需要注意的是,它还不是完整生产交付状态,真实云端鉴权、对象存储生产安全、支付/短信/OAuth 生产账号、真实数据 dry-run 迁移仍需要继续补齐或联调;导入后复检、模板下载、字段映射 API 和导入任务详情已可联调Taro 租户内容页已接入上传/粘贴预览、字段别名覆盖、同步/异步执行、异步轮询和复检详情第一版,租户营销中心已接入 CRM 配置/队列、分佣结算、积分任务/兑换和积分风控只读摘要第一版。
因此,后端现在已经具备在现有 `apps/taro` 中开始正式前端重构的基础。需要注意的是,它还不是完整生产交付状态,真实云端鉴权、对象存储生产安全、支付/短信/OAuth 生产账号、真实数据 dry-run 迁移仍需要继续补齐或联调;导入后复检、模板下载、字段映射 API 和导入任务详情已可联调Taro 租户内容页已接入上传/粘贴预览、字段别名覆盖、同步/异步执行、异步轮询和复检详情第一版,租户营销中心已接入 CRM 配置/队列、分佣结算、积分任务/兑换和积分风控只读摘要第一版。
## 后端模块进度
@@ -39,7 +41,7 @@
## 前端接入建议
建议新建 `apps/taro`,不要在旧 React Web 上继续堆大量兼容。旧项目继续作为样式、页面和交互参照,真正的新业务调用以 `apps/api` 为准
继续在已建立的 `apps/taro` 中开发,不新建第二套前端,也不在旧 React Web 上继续堆大量兼容。旧项目作为样式、页面状态和微信能力参照,新业务调用以 `apps/api` 和受控兼容基线为准。学生端共享 H5/小程序/后续 App租户后台和平台后台首发仅做响应式 H5
前端第一阶段应该先做能跑完整学生链路的页面:

View File

@@ -0,0 +1,127 @@
# Clean-room migration audit - 2026-07-12
## Audit scope
- Isolated project ID: `tiku-clean-final-20260712-v2`
- Database image: official Supabase PostgreSQL `15.8`
- Isolated database endpoint used during verification: `127.0.0.1:55522`
- Migration role: standard non-superuser `postgres`
- Privileged bootstrap/test role: `supabase_admin`, used for the runtime-role bootstrap and controlled role-boundary probes
- The existing databases on ports `5432`, `54322`, and `55432` were explicitly excluded and were not modified.
The clean-room startup log reported that no `supabase/seed.sql` matched. The seed was therefore not executed. At the end of migration verification, `public.tenants`, `app_private.environment_safety`, and `auth.users` all contained zero rows.
## Migration result
All 78 migrations were applied successfully in filename order to the empty database. `supabase migration list --local` subsequently showed every local version paired with the applied version through `202607120019`.
Migration history verification returned:
- Rows: `78`
- Distinct versions: `78`
- Duplicate versions: `0`
- Maximum version: `202607120019`
The privileged `scripts/deploy/sql/bootstrap-backend-runtime-roles.sql` bootstrap ran before the normal migrations. Migrations `202607120013_backend_runtime_roles.sql`, `202607120018_auth_user_reference_boundary.sql`, and `202607120019_production_migration_history_boundary.sql` were later replayed directly by the non-superuser `postgres` role. All three replays succeeded. A normalized fingerprint covering role attributes, memberships, schema/table/sequence/function ACLs, default ACLs, and security-definer attributes remained `1691|7aee08a0b05a30fc49fd278748d4c4e3` before and after replay.
## Extensions and function ACLs
The required extensions were installed in the `extensions` schema:
- `citext`
- `ltree`
- `pg_trgm`
- `pgcrypto`
No client-facing role can execute a function exposed through `public`. The final extension-function matrix was:
| Role | `citext` | `ltree` | `pg_trgm` | `pgcrypto` |
| --- | ---: | ---: | ---: | ---: |
| `anon` | 0/45 | 0/78 | 0/31 | 0/36 |
| `authenticated` | 0/45 | 0/78 | 0/31 | 0/36 |
| `tiku_api` | 45/45 | 78/78 | 31/31 | 0/36 |
| `tiku_worker` | 45/45 | 78/78 | 31/31 | 0/36 |
`citext` also owns two aggregates; when all `pg_proc` extension members are counted, API and worker have all 47 required `citext` members while client roles still have zero.
## Runtime roles and Auth boundary
Both backend roles were verified as:
- `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS`
- No parent-role memberships
- `search_path=pg_catalog, public, extensions`
Runtime identity checks confirmed that both roles can execute `citext` comparisons, `ltree` operators, and `extensions.similarity()`.
The API role could execute exactly five reviewed `app` functions:
- `app.auth_user_exists(uuid)`
- `app.production_migration_history(text)`
- `app.uuid_array_from_jsonb(jsonb)`
- `app.public_question_bank_grant_allows(uuid[], uuid[], uuid, uuid[])`
- `app.public_question_bank_subscription_allows(jsonb, jsonb, uuid, uuid, uuid[])`
Direct `tiku_api` access to `supabase_migrations.schema_migrations` was denied. The API-only `app.production_migration_history('202607120019')` boundary returned `latest_version=202607120019`, `applied_count=78`, `distinct_version_count=78`, and `expected_version_applied=true`. Worker and anonymous execution were denied.
The worker has no `app` schema usage and can execute no `app` functions. Both API-only boundary functions are stable `SECURITY DEFINER` functions with an empty search path. `app.auth_user_exists(uuid)` returned `false` for an absent UUID and `true` for a temporary clean-room Auth user; that user was deleted immediately after the probe. Worker and anonymous execution were denied. Direct reads of `auth.users` were denied for both API and worker.
Supabase internal compatibility checks also passed:
- `supabase_auth_admin` could query `auth.users`.
- `supabase_storage_admin` could query `storage.objects`.
- `authenticator` could still `SET ROLE anon` and `SET ROLE authenticated`.
## RLS, constraints, and indexes
All 140 `public` tables had RLS enabled. Across `public` and `app_private`, all 134 tables containing a `tenant_id` column had RLS enabled.
The only checked internal table without RLS was `app_private.environment_safety`. It is not tenant data, is intentionally fail-closed, and explicitly revokes access from `public`, `anon`, and `authenticated`.
The project tenant-foreign-key audit matched all `189/189` expected relations and its expected SHA-256 fingerprint, found all three reviewed exceptions, found no unvalidated relations, and reported `0` data violations.
All 22 explicitly checked critical foreign keys, unique constraints, and check constraints introduced by migrations `202607120010` through `202607120017` existed with `convalidated=true`. All 16 explicitly checked critical indexes existed with `indisvalid=true` and `indisready=true`, including tenant-safe question/version relations, answer semantics, tenant student keyset/search indexes, SMS reservation limits, audit-log capacity indexes, and import-job lease indexes.
Supabase lint completed successfully:
```text
Linting schema: public
Linting schema: app
Linting schema: app_private
No schema errors found
```
The command used was:
```bash
npx --no-install supabase db lint \
--local \
--workdir /tmp/tiku-clean-final-20260712-v2 \
--schema public,app,app_private \
--level error \
--fail-on error
```
## Residual foreign-key index risk
The application schemas (`public` and `app_private`) contain 465 foreign keys. The structural audit found:
- 146 with an unconditional complete left-prefix index
- 6 covered only by a partial left-prefix index
- 313 without a complete left-prefix index
The newly hardened `202607120015` and `202607120017` hot paths are substantially covered. The remaining count is a capacity and operations backlog, not a migration correctness or tenant-isolation failure.
Adding 313 indexes blindly is not recommended. Every index increases storage, write amplification, vacuum work, cache pressure, migration time, and lock risk. Some foreign keys are low-volume, rarely joined, never cascaded in normal operations, or already served by a more useful query-specific index. Production indexing should therefore be prioritized from representative capacity tests, cascade/delete behavior, slow-query evidence, and `pg_stat_statements`, then introduced in controlled batches.
## Cleanup
After all checks passed, the isolated Supabase project was stopped with `--no-backup`. The following were verified absent:
- `/tmp/tiku-clean-final-20260712-v2`
- Clean-room containers
- Clean-room Docker volumes
- Clean-room Docker networks
The pre-existing databases on ports `5432`, `54322`, and `55432` remained running after cleanup.

View File

@@ -1,6 +1,6 @@
# 内容导入契约
更新时间2026-06-29
更新时间2026-07-12
## 结论
@@ -98,6 +98,24 @@ npm --workspace @tiku-saas/worker run imports:once
前端提交异步导入后不要重复同步执行同一 job只需要轮询 `GET /api/tenant-content/imports` 并用 `GET /api/tenant-content/imports/issues` 展示问题行。worker 会按 `attempt_count/max_attempts` 记录重试,失败时写入 `errorMessage` 和审计日志。
异步 worker 使用数据库持久 lease不能只依赖进程内状态
- claim 是单条 `UPDATE ... FROM (SELECT ... FOR UPDATE SKIP LOCKED)`,多实例不会领取同一个 job。
- 每次 claim 都生成新的 `lease_token` fencing token并写入 `locked_by``locked_at``lease_expires_at``last_heartbeat_at`
- 长任务按 `WORKER_IMPORT_HEARTBEAT_INTERVAL_MS` 续租;该值必须小于 `WORKER_IMPORT_LEASE_SECONDS` 的一半。
- worker 崩溃后,其他实例可在 lease 过期后重新领取,并原子增加 `attempt_count`
- 完成、失败和重试提交都必须同时匹配 job、`status=importing`、未过期 lease 和 `lease_token`。旧实例丢失 lease 后,其导入事务整体回滚,不能覆盖接管者的结果或审计。
- attempt 已耗尽的过期 job 会直接转为 `failed`,不会额外执行一次。
生产建议先保持默认配置:
```env
WORKER_IMPORT_LEASE_SECONDS=120
WORKER_IMPORT_HEARTBEAT_INTERVAL_MS=30000
```
lease 应覆盖数据库短暂抖动,但不应长到显著拖慢崩溃恢复;调整时必须同时运行 `test:worker:imports` 和 production readiness。
## 模板、字段映射和导入后复检
租户后台前端不要把导入字段写死在页面里。导入页初始化时先读取字段映射,下载模板时调用模板接口:

View File

@@ -1,15 +1,18 @@
# 前端交接索引
更新时间2026-07-02
更新时间2026-07-12
这份文件是给 Taro/H5/小程序前端同事的入口。当前仓库的前端重构建议从 `apps/taro` 新建工程开始,不再把旧 React/Vite 前端搬回根目录继续开发
这份文件是给 Taro/H5/小程序前端同事的入口。`apps/taro` 已经是唯一的新前端工程,后续应在该工程内重构,不要重新新建第二套 Taro 工程,也不要把旧 React/Vite 前端搬回根目录。
## 2026-07-02 接管重点
开始设计或改页面前先读 `production-foundation-baseline-20260712.md`。该文件冻结了当前 API、身份和状态机边界并区分了“可以开始前端”与“已经可切生产流量”。浏览器现状证据见 `taro-h5-browser-qa-20260712.md`
当前 `main` 已包含旧题库视觉对齐版本,最新提交是 `f54421f test: align Taro visual guardrails with legacy UI`。另一台工作机接管后,先确认本地代码至少包含该提交:
## 2026-07-12 接管重点
接管后先确认当前候选分支和工作区,不要依赖历史 commit 文案判断是否最新:
```bash
git log -2 --oneline
git status --short --branch
git log -3 --oneline
```
本轮前端变化的边界:
@@ -87,6 +90,7 @@ node scripts/taro-h5-release-guardrails-test.js
- H5 构建完成后必须运行 `npm run smoke:taro:h5:interaction` 做真实浏览器点击验证。它会覆盖学生首页到题库练习、答题、收藏、会员收银台下单/支付参数/订单状态,租户后台工作台到题库内容/财务运营,以及平台后台工作台到租户管理/账务中心;如果 Chrome/Edge 缺失,可设置 `TARO_H5_SMOKE_BROWSER` 指向 Chromium 浏览器。
- H5 可以优先验证 `@supabase/supabase-js` 管理 Auth session微信小程序端先验证运行时兼容性业务数据默认仍走 `apps/api`
- H5 生产部署优先用每个静态目录自己的 `runtime-config.json` 配置 `apiBaseUrl``supabaseUrl``supabasePublishableKey``tenantCode`;不要为了换域名重打包,也不要把任何 service role、数据库、支付、短信、对象存储密钥放进该文件。
- 学生端当前 production 入口约 `500 KiB`,前端重构必须先建立路由拆包、延迟加载和资源预算;视觉组件不得无约束进入首包。
- 上线前需要把三套 H5 构建、`npm run smoke:taro:h5` 静态启动烟测、`npm run smoke:taro:h5:interaction` 真实浏览器交互烟测、严格 `taro-h5-release-guardrails-test --require-runtime-config``runtime-config.json` 人工复核、真实 Auth/RLS、迁移 dry-run、对象存储、支付对账、`security:repo` 和真实 `@codex-security` 结果写入 `production-launch-evidence.json`,并通过 `npm run launch:gate`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。
- 可以接入租户品牌、已发布主题、公开素材、功能开关和域名/小程序参数解析;学生端只读 `/api/tenant/resolve``branding.theme/publicAssets`,租户后台草稿走 `/api/tenant-admin/theme`
- 租户后台可以接入角色模板和成员 API`/api/tenant-admin/role-templates``/api/tenant-admin/members`,用于运营、教师、销售、代理等自定义菜单/模块/字段可见性和成员模板绑定。

View File

@@ -1,6 +1,8 @@
# Supabase 重构功能进度矩阵
更新时间2026-06-30
更新时间2026-07-12
> 2026-07-12 权威基线:多租户 RLS、运行角色/Data API 权限、Auth 用户最小边界、动态 CORS、短信限流、Worker lease/fencing、生产 readiness 和十万学生容量证据已经完成本地及独立 clean-room 验证,后端/API 可以进入受控冻结并开始正式前端重构。正式上线仍需目标云服务器的真实 provider、数据迁移、备份恢复、首个平台超管、systemd、真实压测、三套 H5 runtime config 和 launch evidence。完整结论见 `production-foundation-baseline-20260712.md`;下方长表保留历史功能明细,若与该基线冲突,以 7 月 12 日基线为准。
## 当前结论
@@ -23,7 +25,7 @@
| 模块 | 数据模型 | PocketBase 导入 | API | 自动化测试 | 当前状态 |
| --- | --- | --- | --- | --- | --- |
| 多租户隔离 | 已建 `tenants``tenant_domains``tenant_branding``tenant_settings`RLS 基础 | 部分支持 | 租户解析、品牌、域名、支付账户、登录 provider、平台建租户已实现 | 核心 API 集成测试含租户隔离断言 | 基础可用,正式 JWT/RLS 权限闭环完成 |
| 多租户隔离 | 已建 `tenants``tenant_domains``tenant_branding``tenant_settings`完整 RLS/ACL/运行角色边界 | 部分支持 | 租户解析、品牌、域名、支付账户、登录 provider、平台建租户已实现 | 核心 API、动态 RLS、Data API ACL、clean-room migration 和 readiness 均有自动化证据 | 本地与 clean-room 闭环完成;目标生产环境仍需真实 Auth/JWKS、运行角色 bootstrap 和远程隔离验收 |
| 刷题题库 | 已建题库、题目、题目版本、内容入口、任意深度分类树、考试意向标记、题目集合、练习蓝图、导入任务台账、导出任务台账、公共题库授权/采纳表、租户内容通知表 | 已支持核心映射JSON/CSV/Excel 导入可落到新入口/节点/集合,阅读理解/案例分析子题沿用 `subQuestions/sub_questions` | 题目列表、内容入口、分类树、集合题目、顺序/随机/全真模拟 session、答题提交、复合题 `subAnswers` 判分和报告明细、租户后台题目录入/更新、JSON/CSV/Excel 预览/导入、JSON/试卷 payload 导出、PDF/Word 异步导出 worker、每日一练九宫格 metadata、PDF/Word 运营版式、ZIP 图片素材包、异步导入 worker、平台公共题库授权、租户采纳快照、手动同步、自动同步 worker、同步通知、冲突查询和单条/批量冲突处理已实现 | 核心 API 集成测试含导航、组卷、复合题后台录入/练习/判分/报告、导入、导出权限/脱敏、每日一练导出 metadata、异步 PDF/Word/每日一练 ZIP job 创建、exports worker、公共题库授权、采纳后组卷、同步新增题、通知隔离/已读/自动 resolved、租户自改冲突保护、单条/批量冲突处理和 worker 自动同步断言Taro 类型检查覆盖 RichContent 接入 | 新题库导航和组卷基础闭环可跑,阅读理解/案例分析多小题、题干/选项/解析 RichContent 安全渲染和逐题复盘第一版可联调,公共题库采纳/手动/自动同步、同步通知、冲突查询/处理、导入后复检、模板下载、字段映射 API、JSON/PDF/Word/每日一练 ZIP 基础导出可联调;真正 KaTeX/小程序公式方案、私有题图签名映射、公共题库生产调度/失败告警、更精细导出模板和更完整运营消息仍需补齐 |
| 错题本 | 已建 `wrong_questions` | 已支持旧错题归一化 | 错题列表、答题自动入错题、移出错题已实现 | 仅烟测 | 基础功能已实现,复习计划和统计未完成 |
| 收藏夹 | 已建 `favorite_questions` | 已支持旧收藏归一化 | 收藏/取消收藏、收藏列表已实现 | 仅烟测 | 基础功能已实现 |

View File

@@ -0,0 +1,42 @@
# Import worker 持久 lease 验证报告
更新时间2026-07-12
## 结论
`content_import_jobs` 已具备多实例和进程重启所需的持久 lease 与 fencing 语义。验证只在专用测试库 `127.0.0.1:55432` 执行,没有连接生产环境。
## 实现边界
- migration `202607120016_content_import_job_leases.sql` 增加 `lease_token``lease_expires_at``last_heartbeat_at`、一致性约束和 pending/expired 部分索引。
- claim 使用原子 `SKIP LOCKED`,可同时领取 ready pending job 和 lease 已过期的 importing job。
- 每次 claim 只增加一次 `attempt_count` 并生成新 token失败调度只写 `next_attempt_at`,不会重复增加 attempt。
- worker 在执行期间续租;续租、完成、失败和重试都要求 token 匹配且 lease 未过期。
- API executor 的业务写入和终态更新处于同一事务。fencing 校验失败会回滚题目、版本、集合绑定、item 和 audit 写入。
- 最后一次 attempt 的 lease 过期后由 claim/reaper 路径直接标记 failed避免第 `max_attempts + 1` 次执行。
## 动态覆盖
`scripts/import-worker-integration-test.js` 在 destructive-test database guard 后验证:
1. 两个并发 worker 对三个 job 原子 claim没有重复领取。
2. 心跳推进 `last_heartbeat_at` 并延长 `lease_expires_at`
3. 模拟崩溃后,过期 job 被新 worker 接管token 旋转且 attempt 从 1 变为 2。
4. 旧 token 的执行在业务写入前被拒绝,旧 token 的失败提交也不能覆盖新 lease。
5. 新 lease 可完成 job终态清空 lease 字段并保持准确 attempt。
6. retry 保持 pending、持久 `next_attempt_at`,再次 claim 才增加 attempt。
7. 最后 attempt 过期后进入 failed不再重新执行。
## 验证命令
```bash
npm run check:worker
npm run check:api
npm run build:worker
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:55432/postgres \
node scripts/import-worker-integration-test.js \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
node scripts/production-readiness-check-test.js
```
完整 worker integration 会先跑 smoke seed执行时必须明确指向允许 destructive tests 的本地/CI 数据库。

View File

@@ -37,9 +37,13 @@ Inbucket: http://127.0.0.1:54324
```bash
npm run supabase:reset
npm run db:smoke-seed
npm run db:smoke-seed -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
```
`supabase:reset` 执行本地 `supabase/seed.sql`,会在 `app_private.environment_safety` 写入唯一的 `local/true` 标记。`smoke-seed` 还要求精确确认短语,两者缺一即在事务和任何持久化写入前拒绝。`npm run test:api``npm run test:rls``test:worker:*` 会通过受控 npm script 传入确认值,但仍必须通过数据库标记。
CI 或生产快照的隔离克隆库需由初始化流程显式写入 `environment='ci'``test``allow_destructive_tests=true`,并只授予测试数据库角色读取该标记的权限。生产和预发不得设置放行标记,也不得执行 `supabase/seed.sql`
## API 服务
```bash

View File

@@ -1,6 +1,6 @@
# 多租户与鉴权安全契约
更新时间2026-06-30
更新时间2026-07-11
这个系统后续要卖给同行作为题库 SaaS因此租户隔离、鉴权、资源权限和审计是商用红线。前端可以先按迁移期接口联调也可以按 Supabase 官方推荐使用 publishable key + RLS 的客户端能力管理 Auth/session但正式上云验收前必须完成本文件的 P0 项。
@@ -20,7 +20,8 @@
当前后端已经进入“session 优先、迁移头受控兼容”的状态:
- `Authorization: Bearer <tk_session>` 会优先解析 `app_private.auth_sessions`,并作为用户身份来源。
- `Authorization: Bearer <supabase_access_token>` 已支持服务端验签,后端通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射到业务用户和租户成员
- `Authorization: Bearer <supabase_access_token>` 已支持服务端验签,后端通过 `auth.users.id -> platform_users.auth_user_id` 映射平台身份;普通租户用户再通过 `tenant_memberships` 映射当前租户角色
- Supabase JWT 顶层 `role=authenticated` 不会覆盖数据库中的平台管理员身份;平台权限只认 active 的 `platform_users.primary_role='platform_admin'``platform_permissions`,也不要求平台管理员先加入某个租户。
- Supabase JWT 支持 `AUTH_JWT_SECRET``AUTH_JWT_JWKS_URL`;生产推荐优先配置 Supabase Auth JWKS 和 `AUTH_JWT_ISSUER`,或在自托管兼容模式下配置强随机 JWT secret。配置 JWKS 但缺少 issuer 会被生产 fail-fast 阻断。
- JWT 可以在 `app_metadata.tenant_id` 或请求租户上下文中确定当前租户;如果两者冲突,后端拒绝,不允许前端覆盖 token 中的租户声明。
- 登录后如果请求中的 `x-user-id`、query/body `userId` 与 session 用户不一致,后端返回 `AUTH_USER_MISMATCH`
@@ -31,12 +32,15 @@
- 本地短信 provider 可使用 `mock`
- 真实短信 provider 已支持阿里云和腾讯云,密钥只能从 `app_private.tenant_secrets` 读取。
- 微信小程序登录已由后端调用 `code2Session`,前端不得接触 AppSecret 或 session_key。
- `NODE_ENV=production` 下禁止 `ALLOW_LEGACY_AUTH_HEADERS=true``ALLOW_PLATFORM_ADMIN_KEY=true``AUTH_SMS_PROVIDER=mock`、默认/弱密钥`CORS_ORIGIN=*`
- `NODE_ENV=production` 下禁止 `ALLOW_LEGACY_AUTH_HEADERS=true``ALLOW_PLATFORM_ADMIN_KEY=true``AUTH_SMS_PROVIDER=mock`、默认/弱密钥`CORS_ORIGIN=*``CORS_TENANT_DOMAINS_ENABLED=false`
- `CORS_ORIGIN` 只列少量中央平台/运维 Origin租户 H5 Origin 必须命中 `active tenant_domains + active tenants`。动态查询使用有界正负 TTL 缓存与同 host 并发去重,查询故障、未知/禁用域名和非标准 HTTPS Origin 一律 fail closed。CORS 不得信任请求 `Host`/`X-Forwarded-Host` 或租户头进行准入。
这些只允许用于本地开发和内网联调,不允许作为正式云端验收方案。
Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小 grant 和 JWT 权限模型都正确。本项目的核心业务表默认不开放给 Taro 直写;任何新增直连表都必须先通过 RLS、跨租户、权限和性能评审。
`202607110001_data_api_acl_rls_hardening.sql` 把这个约定落到数据库:`anon/authenticated``public` 表、视图、序列和 RPC 默认无权限,后续新建对象也不会自动获得 Data API 权限。当前 Taro 只用 Supabase Auth业务数据统一走 `apps/api`。如未来需要前端直连,必须在独立 migration 中逐对象写明 policy、`TO`、命令类型和最小 grant并补同租户垂直越权测试。
## P0正式云端测试前必须完成
1. 正式用户鉴权
@@ -64,6 +68,19 @@ Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小
- 已支持平台管理员 Supabase JWT且以后端 `platform_users.primary_role='platform_admin'` 为准,不只信 JWT claim。
- 平台管理员已支持 `platform_users.platform_permissions` 细粒度权限,`{"*":true}` 为超级管理员;接口按 `platform:staff:*`、`platform:tenant:*`、`platform:billing:*`、`platform:audit:*`、`platform:question_bank:*` 等权限点强制校验。
- 平台员工管理已落到 `GET/PUT/PATCH /api/platform-admin/staff` 和 Taro 平台员工页;员工必须绑定 Supabase Auth 用户 ID`platform_users.status='disabled'` 后不能再通过 Supabase JWT 映射为平台管理员,禁用时也会默认撤销迁移期 session。
- 首个超级管理员只能通过服务器侧 CLI 绑定一个已经存在的 Supabase Auth UUID不能提供公开“创建首个超管”接口。先在 Auth 控制台或受控后台创建/确认账号,再在生产运维终端执行 dry-run
```bash
DATABASE_URL='<production-database-url>' \
BOOTSTRAP_PLATFORM_ADMIN_AUTH_USER_ID='<auth.users UUID>' \
BOOTSTRAP_PLATFORM_ADMIN_USERNAME='<operator username>' \
BOOTSTRAP_PLATFORM_ADMIN_NAME='<display name>' \
npm run bootstrap:platform-admin
```
- 审核 dry-run 的脱敏结果后,才允许在同一受控终端执行:
```bash
npm run bootstrap:platform-admin -- --apply --confirm BOOTSTRAP_FIRST_PLATFORM_ADMIN
```
- CLI 使用事务级 advisory lock已有 active 且已绑定 Auth 的平台管理员后永久拒绝再次引导。迁移库只允许绑定唯一一条未绑定的历史 `platform_admin`,多个候选会拒绝并要求人工消歧。成功后固定写入 `status='active'`、`platform_permissions={"*":true}` 和脱敏审计事件。
- 生产前继续补平台后台关键操作审计报表。
4. 生产配置 fail-fast
@@ -76,7 +93,8 @@ Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小
- 禁止 `AUTH_SMS_PROVIDER=mock`。
- 禁止 `ALLOW_LEGACY_AUTH_HEADERS=true`。
- 禁止 `ALLOW_PLATFORM_ADMIN_KEY=true`。
- 上云前必须运行 `npm run readiness:production`;连接生产数据库后再运行 `npm run readiness:production:db`。
- 上云前必须运行 `npm run readiness:production`;连接生产数据库后再运行 `npm run readiness:production:db`,并确认 `db.environment.destructive_tests_disabled` 通过
- 数据库门禁会阻断 active 租户 `public_config` 中非空但不是生产 HTTPS 的 `*Url/*Uri` 字段,并对尚未发布租户主题的 active 租户给出 warning允许继续使用平台默认主题但必须在上线审批中确认品牌表现。
5. 请求体大小限制
- 普通 JSON API 必须有默认上限。
@@ -95,8 +113,11 @@ Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小
- API SQL 必须显式带 `tenant_id`。
- 测试必须覆盖跨租户读取、写入、下载、后台权限越权。
- `npm run readiness:production:db` 会阻断带 `tenant_id` 但未启用 RLS、没有 policy、或 public policy 未包含 `app.current_tenant_id()` 的表。
- `npm run test:rls` 会在本地 smoke seed 后模拟 Supabase `authenticated/anon/platform_admin` JWT claims动态验证主租户和合作商租户代表性表不会跨租户读写泄露并验证无 `tenant_id` claim 不能读取租户数据
- `test:rls` 为了模拟 PostgREST 角色会在事务内临时授予 `authenticated/anon` 查询探针权限,所有 grant、写入探针和跨租户插入都会回滚它验证的是 RLS policy 行为,不代表生产要开放核心业务表直连
- 同一门禁还会阻断 `anon/authenticated` 对 `public` 表/视图/序列/RPC 的直接权限、危险的默认 ACL、`platform_users` 客户端写 policy以及仅信任陈旧 JWT `platform_admin` claim 的 RLS 旁路
- `npm run test:data-api:security` 在不启动数据库时检查 deny-by-default migration、readiness 门禁和 Taro 不直连业务表的源码合同
- `npm run test:rls` 会先提交破坏性 smoke seed再模拟 Supabase `authenticated/anon/platform_admin` JWT claims只允许连接 `app_private.environment_safety` 标记为 `local/test/ci` 且显式放行的隔离库。
- `test:rls` 为了模拟 PostgREST 角色会在事务内临时授予 `authenticated/anon` 查询探针权限,该事务内的 grant、写入探针和跨租户插入会回滚但前置 seed 不会回滚,所以禁止连接生产或预发。生产上线的动态 RLS 证据必须来自生产 schema/脱敏快照克隆库。
- 禁止在 `source /etc/tiku-saas/api.env` 后运行 `test:rls`、`test:api` 或 `test:worker:*`;真实生产库只运行只读 readiness 或专门设计的无 seed 远程探针。
- 新增租户表时必须同时提交 migration、RLS policy、API 权限测试或明确说明只允许平台级访问的原因。
## 前端必须遵守
@@ -109,6 +130,7 @@ Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小
- 不允许“切换销售归属”这类破坏首绑保护的入口,除非后端提供带权限的管理接口。
- 不在前端直接判断“这个用户能不能看某题/某视频/某资料”的最终结果;必须请求后端。
- 切换租户、退出登录、登录新账号时,清理旧租户缓存和用户缓存。
- Taro 会话按 portal、域名或 tenantCode、tenantId 隔离,业务缓存再增加 userId显式退出、确认失效、租户切换和账号切换会删除旧用户数据前缀。H5 通过跨标签事件同步身份变化,页面存储句柄固定 tenantId/userId避免另一个标签切号后写入新账号空间。Supabase 与短信 app session 具有明确当前来源,不允许某一来源失效后静默回退到上一账号。
## 后端接口约定
@@ -224,10 +246,12 @@ GET /api/platform-admin/permissions
- `npm run audit:runtime` 为 0 high/critical 漏洞Taro 构建工具链 audit 单独跟踪,不能用破坏性降级绕过。
- `npm run check:refactor` 通过。
- `npm run test:auth:foundation` 通过,覆盖标准 Supabase JWT 的平台身份映射、恶意 JWT 角色不提权和首个超管 CLI 的 dry-run/拒绝/审计契约。
- `npm run smoke:auth:remote` 在预生产/生产 API 上通过,并使用真实 Supabase Auth access token 覆盖学生、租户管理员、平台管理员、坏 token 和错租户上下文。
- `npm run test:rls` 通过;必须确认主租户、合作商租户、无租户 claim、平台管理员旁路和跨租户写入拒绝都有运行时证据。
- 在生产 schema/脱敏快照的隔离克隆库上 `npm run test:rls` 通过;必须确认主租户、合作商租户、无租户 claim、平台管理员旁路和跨租户写入拒绝都有运行时证据。
- `npm run readiness:production` 没有 blocker。
- `npm run readiness:production:db` 没有 blocker尤其是 `db.rls.tenant_tables_enabled`、`db.rls.tenant_tables_policy`、`db.rls.public_tenant_context` 必须通过。
- `db.platform_admin_active`、`db.platform_admin_auth_binding`、`db.platform_admin_permissions` 必须通过;`db.tenant_public_urls` 必须通过,`db.tenant_theme_published` warning 必须有上线审批结论。
- 生产环境启动时默认密钥 fail-fast 生效。
- 跨租户学生读取题目/订单/资料返回拒绝。
- 销售只能查看自己权限范围内客资。

View File

@@ -1,6 +1,17 @@
# 后续开发 TODO
更新时间2026-07-01
更新时间2026-07-12
## 当前优先级2026-07-12
后端地基不再以“继续补齐所有可能功能”为主线。当前优先级调整为:
1.`production-foundation-baseline-20260712.md` 冻结现有 API/状态机兼容边界。
2. 启动 Taro 正式前端重构,先做设计 token、跨端基础组件、信息架构、权限驱动导航、拆包和性能预算再按学生端、租户后台、平台后台推进垂直切片。
3. 同步准备目标生产环境的真实上线证据备份恢复、Auth/provider/storage、首个平台超管、systemd、数据迁移、目标规格压测、runtime config 和 launch gate。
4. 只有前端发现确定的契约缺口、生产 readiness 暴露阻断或监控数据证明需要时,才继续修改后端;所有破坏性变更必须走兼容迁移和 contract test。
本文件后续条目是历史能力清单和增强 backlog不代表前端启动前都必须完成。
## 当前后端基线
@@ -267,7 +278,7 @@
2. 继续补 Taro 学生端旧体验:地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、视频播放、反馈、模考报告、逐题复盘、错题/收藏专题、个人中心学习报告、男女预设头像选择、收银台、订单详情、售后入口、独立消息中心、积分任务/兑换/积分明细、题干/解析/知识手册 RichContent 安全渲染、知识手册章节内搜索/安全摘要高亮/目录定位、H5 KaTeX 公式渲染、私有资源 ID 题图短签名、背单词学习概览/卡片学习/发音/收藏练习、资料短签名水印预览/下载确认已接第一版;继续补小程序公式真机验收、题图资源字段化、小程序支付容器、分享场景和状态管理。前端不得实现头像上传、头像裁剪或第三方头像同步。
3. 补平台后台增强:租户基础资料编辑增强、平台审计告警升级策略、平台催缴通知配置操作台细节和平台在线收款。
4. 云服务器部署 Supabase/PostgreSQL 和 API配置对象存储生产环境变量`check:refactor` 的远程等价测试。
5. 三套 H5 上云前必须在目标目录补真实公开 `runtime-config.json`,先执行 `npm run smoke:taro:h5``npm run smoke:taro:h5:interaction``node scripts/taro-h5-release-guardrails-test.js --require-dist` 做本地发布目录烟测、真实浏览器交互烟测和发布守卫;写入生产上线证据时必须执行 `npm --silent run smoke:taro:h5 -- --json``npm --silent run smoke:taro:h5:interaction -- --json``node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json`,确保 warning 为 0。当前 H5 交互烟测为 32/32 通过,已经覆盖租户后台和平台后台真实写操作;随后仍要人工打开学生端/租户后台/平台后台域名,确认入口、租户解析、登录态和 `Authorization + x-tenant-id` 请求正常。当前构建仍有 webpack 体积 warning后续做首屏拆包、按入口拆页面和 Supabase client 引入优化。
5. 三套 H5 上云前必须在目标目录补真实公开 `runtime-config.json`,先执行 `npm run smoke:taro:h5``npm run smoke:taro:h5:interaction``node scripts/taro-h5-release-guardrails-test.js --require-dist` 做本地发布目录烟测、真实浏览器交互烟测和发布守卫;写入生产上线证据时必须执行 `npm --silent run smoke:taro:h5 -- --json``npm --silent run smoke:taro:h5:interaction -- --json``node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json`,确保 warning 为 0。当前 H5 交互烟测为 33/33 通过,已经覆盖租户后台和平台后台真实写操作;随后仍要人工打开学生端/租户后台/平台后台域名,确认入口、租户解析、登录态和 `Authorization + x-tenant-id` 请求正常。当前学生端入口约 500 KiB正式视觉重构先做首屏拆包、按入口延迟加载和 Supabase client 引入优化。
6. 使用 `F:\project\参考\旧题库数据库文件` 中的真实 PocketBase 数据,按 `docs/refactor/pocketbase-real-data-migration-runbook.md` 继续做人工复核和抽样验收;当前 dry-run/导入/校验链路已跑通,下一步重点是 7 个已支付缺用户订单、22 个待复核手册章节、2533 道旧分类缺失题目和 5298 条引用已删除题目的答题记录的运营处理结论。导入过程中发现的字段污染、跨集合引用断裂、敏感字段和旧权限问题都要沉淀到 importer mapper 或修复脚本,不手工临时修库。
7. 并行补真实登录、真实生产账单格式验收、异常订单运营台、对象存储真实 AV/内容安全服务联调、转码/CDN 级水印/生命周期、题库导出模板精排/操作台、公共题库生产定时调度和失败告警。
8. 前后端联调通过后,再做支付、权限、数据导入、资料下载、视频播放的商用验收。

View File

@@ -73,7 +73,7 @@ npm run perf:api:local
```powershell
$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
npm run db:smoke-seed
npm run db:smoke-seed -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
npm run perf:api:local
```

View File

@@ -0,0 +1,84 @@
# SaaS 题库生产地基基线2026-07-12
## 决策
当前仓库的后端代码、数据库迁移、租户隔离和三类角色业务契约可以进入受控冻结,前端可以在现有 `apps/taro` 上开始正式重构。
这不等于已经可以直接切生产流量。正式上线仍以目标云服务器上的真实配置、真实 provider、真实数据迁移和 `production-launch-evidence.json` 全量通过为准。任何本地 mock、clean-room 或预览构建都不能替代生产证据。
## 已验证的地基
- 官方 Supabase PostgreSQL 15.8 空库中,特权 bootstrap 后由非 superuser migration role 成功应用全部 78 个 migration未执行 seed后期安全迁移重放幂等官方 schema lint 通过。详见 `clean-room-migration-audit-20260712.md`
- 140 张 `public` 表全部启用 RLS134 张包含 `tenant_id` 的业务表全部启用 RLS。`anon/authenticated``public` RPC 或扩展函数执行权。
- `tiku_api/tiku_worker` 是独立最小权限运行角色;均不能直接读取 `auth.users`,只有 API 可通过 `app.auth_user_exists(uuid)` 获得布尔存在性。
- Data API、动态 CORS、短信限流、审计容量索引、导入 Worker lease/fencing、租户外键与关键查询索引已经进入 readiness 门禁。
- 单租户 100,000 学生容量证据通过:首屏查询 P95 `11.245ms`,深游标 P95 `3.114ms`,关键 keyset/trigram 索引被使用并完成清理。
- `npm run test:readiness`、API TypeScript 检查、后端生产依赖审计和仓库自带安全扫描通过;仓库扫描结果为 0 findings。
- API Docker 运行镜像已锁定 Node `20.20.2`/Alpine `3.23` 多架构摘要,以 `node` 用户运行,只含生产依赖和编译产物;本机验证镜像约 `53 MB``node_modules``24.3 MB`,不含 `typescript/tsx`,并连接 `55432` 隔离库通过 `/health`
- Taro 固定稳定版 `4.2.0`;两项 H5 运行时补丁由 workspace postinstall 原子应用并按源码 hash fail closed。仓库外干净 `npm ci`、供应链门禁和补丁契约均通过。
- 学生端、租户后台、平台后台三套 H5 production 构建、静态烟测 `25/25` 和真实 Chrome 业务交互烟测 `33/33` 通过Input 挂载前后值同步和 Button loading 200 次稳定节点探针通过。
- 三端桌面 `1440x900` 与移动 `390x844` 浏览器验收通过,无控制台 error/warn、无页面级横向溢出并验证每个门户至少一个主入口。详见 `taro-h5-browser-qa-20260712.md`
## API 冻结边界
前端重构默认只能消费现有契约,以下内容从本基线起视为兼容性边界:
- 路由、HTTP method、状态码、错误 `code` 和分页 cursor 语义。
- `GET /api/tenant/resolve` 的 Origin/tenantCode 解析规则和公开配置字段。
- Supabase access token、迁移期 `tk_` session、`Authorization``x-tenant-id` 的统一 client 行为。
- 学生、租户成员、平台员工三类身份和权限目录;页面可隐藏入口,但后端仍是最终授权源。
- 练习 session、导入 job、导出 job、支付/退款、CRM、账单和 Worker 状态机。
- 私有资料、图片、PDF、视频的短签名、水印和访问审计结果。
需要修改上述契约时必须同时更新后端注册表、Taro service、类型、route/API/persona contract test、交接文档和版本说明。破坏性字段变更应采用新增字段、双读/双写或版本化接口,不允许直接让已有三端同时失效。
当前冻结属于受控兼容基线,不是覆盖全部请求/响应字段的完整 OpenAPI v1 冻结。仓库已机器锁定 method/path、统一 `meta.requestId`、错误 `code`、租户解析、认证头、十万学生 keyset 分页和订单/退款/导入/CRM/佣金等核心状态值开发新垂直切片时还必须为本切片的请求字段、响应字段、HTTP 状态和业务错误码补 contract snapshot直到这些 snapshot 汇总为完整 schema。
页面不得直接写 Supabase 业务表,不得自行拼接对象存储 URL也不得在页面层手写身份头或长期保存另一套租户/会话状态。
## 多端产品边界
- 学生端:继续使用 Taro共享 H5、微信小程序和后续 App 的页面、services、capabilities 与领域类型。
- 租户后台、平台后台:首发只做响应式 H5。批量导入、复杂表格、财务和运营工作流不强行迁入小程序。
- 微信小程序:只发布学生端。数百租户共用包时使用 launch tenant mode由受控 scene/query 解析租户;缺租户码必须 fail closed。
- 后续 App优先复用学生端业务组件和 API client支付、文件、音视频、推送、分享等能力通过 capabilities 层逐项替换并真机验收。
## 前端启动门禁
前端可以开始,但第一阶段必须先完成以下工程约束,再批量做页面视觉:
1. 冻结三类 persona 的信息架构、导航和权限矩阵,不删现有业务入口。
2. 建立跨端设计 token、基础组件和状态规范租户品牌只能覆盖已批准 token不允许注入任意 CSS。
3. 保持 `api.ts`、全局 App Provider、租户解析和缓存隔离为唯一基础设施。
4. 每个垂直切片同时完成桌面 H5、移动 H5和学生小程序兼容检查后台无需小程序化。
5. 把包体和首屏性能设为硬门禁。当前 production 入口约为学生端 `501 KiB`、租户后台 `477 KiB`、平台后台 `450 KiB`;重构前应先规划路由拆包、延迟加载和资源预算,避免继续扩大首包。
6. 每次关键页面改动运行 Taro 类型检查、route/API/persona/compatibility contract、Taro 供应链审计、视觉守卫、三套构建、学生小程序生产构建、静态烟测和交互烟测。
## 生产硬阻断
以下项目只能在目标生产环境完成,未完成前 `launch:gate` 应继续阻断:
- 立即撤销曾在对话中暴露的临时 Git 令牌,并重新生成最小权限凭据。
- 由数据库 superuser 执行 runtime role/extension bootstrap再由标准 migration role 应用迁移API/Worker 使用独立强密码运行角色。
- 配置真实 Supabase Auth/JWKS、阿里云 PNVS、微信/QQ OAuth、微信/支付宝支付和回调域名。
- 配置真实对象存储、外部 AV/内容安全扫描、CDN 边界、水印和生命周期策略。
- 在 Supabase Auth 创建或确认首个管理员身份后,通过 `npm run bootstrap:platform-admin` dry-run 和精确确认短语创建唯一首个平台超管,再运行数据库 readiness。
- 在目标 Linux 主机验证 API、Worker target、job service、timer、日志、告警和重启恢复。
- 生成生产备份/快照并完成至少一次隔离恢复演练;保留旧 PocketBase 只读快照和回滚步骤。
- 用真实完整数据执行 PocketBase production dry-run、导入校验和用户/题目/订单/资源抽样。
- 在目标 4C16G 配置收集 PostgreSQL tuning evidence、真实数据 API 读/混合压测和 100k 学生证据。
- 为三套 H5 放置仅含公开值的真实 `runtime-config.json`,生成 release manifest并校验线上 index/app hash。
- 保持 `swiper@12.1.2``lodash-es@4.18.1` 和两个 Taro H5 runtime patch 的精确版本/hash运行 `npm run audit:taro:supply-chain`;剩余 Taro CLI/构建工具链漏洞必须与已审查 allowlist 一致,不得扩展到 H5/小程序 bundle 运行路径。
- 完成真实 Auth、短信、CORS、三类 persona、支付/退款对账和对象存储抽样。
- 完成真实 Codex Security 扫描。仓库自带扫描不能替代该项,工具不可用时只能保持待补。
- 填写所有 artifact hash 和人工 attestations最后运行 `npm run launch:gate -- --evidence ... --verify-live-h5`
## 上线顺序
1. 撤销暴露凭据并冻结候选 commit。
2. 备份、bootstrap、migration、运行角色和数据库 readiness。
3. 配置 Auth/provider/storage创建首个超管验证 systemd/Worker。
4. 执行真实数据迁移演练、抽样、目标规格压测和远程安全烟测。
5. 生成三套 production H5、注入真实公开 runtime config、发布候选目录并生成 hash manifest。
6. 填写生产证据和人工签字,通过离线 gate 后灰度发布。
7. 对线上 H5 执行 hash/runtime config 校验再逐步放量并观察错误率、P95、数据库连接、锁等待和 Worker backlog。

View File

@@ -1,12 +1,20 @@
{
"schemaVersion": 1,
"environment": "production",
"releaseTargets": [
"h5"
],
"artifactIntegrityNote": "Every check must record artifactSha256. Generate it with sha256sum or shasum -a 256; launch gate rejects missing, empty, or hash-mismatched artifacts. The H5 release manifest also records full-directory treeSha256 values, which deployment verifies against the candidate release.",
"commit": "replace-with-deployed-git-sha",
"target": {
"apiBaseUrl": "https://api.example.com",
"studentH5Url": "https://www.example.com",
"tenantAdminH5Url": "https://admin.example.com",
"platformAdminH5Url": "https://console.example.com"
"apiBaseUrl": "replace-with-real-production-api-https-url",
"studentH5Url": "replace-with-real-production-student-h5-https-url",
"tenantAdminH5Url": "replace-with-real-production-tenant-admin-h5-https-url",
"platformAdminH5Url": "replace-with-real-production-platform-admin-h5-https-url"
},
"liveH5": {
"releaseManifestArtifact": "launch-artifacts/taro-h5-release-manifest.json",
"releaseManifestSha256": "replace-with-64-char-sha256"
},
"checks": [
{
@@ -15,6 +23,7 @@
"command": "npm run readiness:production -- --json > docs/refactor/launch-artifacts/readiness-production.json",
"completedAt": "2026-06-30T10:00:00+08:00",
"artifact": "launch-artifacts/readiness-production.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"blocker": 0
}
@@ -25,16 +34,35 @@
"command": "npm run readiness:production:db -- --json > docs/refactor/launch-artifacts/readiness-production-db.json",
"completedAt": "2026-06-30T10:05:00+08:00",
"artifact": "launch-artifacts/readiness-production-db.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"blocker": 0
}
},
{
"id": "db.migration-history",
"status": "pass",
"command": "node -e \"const fs=require('fs'),crypto=require('crypto');const source='docs/refactor/launch-artifacts/readiness-production-db.json';const bytes=fs.readFileSync(source);const readiness=JSON.parse(bytes);const check=readiness.checks.find(item=>item.id==='db.migrations.current'&&item.status==='pass');if(!check)throw new Error('readiness db.migrations.current pass evidence is missing');const d=check.details||{};const payload={schemaVersion:1,status:'pass',failed:0,latestRepositoryMigration:String(d.expectedVersion||''),latestAppliedMigration:String(d.latestAppliedVersion||''),missingMigrations:d.expectedVersionApplied===true?[]:[String(d.expectedVersion||'')],readinessArtifactSha256:crypto.createHash('sha256').update(bytes).digest('hex')};fs.writeFileSync('docs/refactor/launch-artifacts/db-migration-history.json',JSON.stringify(payload,null,2)+'\\n')\"",
"completedAt": "2026-06-30T10:06:00+08:00",
"artifact": "launch-artifacts/db-migration-history.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"schemaVersion": 1,
"status": "pass",
"failed": 0,
"latestRepositoryMigration": "replace-with-repository-latest-migration-version",
"latestAppliedMigration": "replace-with-production-latest-applied-migration-version",
"missingMigrations": [],
"readinessArtifactSha256": "replace-with-64-char-sha256"
}
},
{
"id": "postgres.tuning-evidence",
"status": "pass",
"command": "PG_TUNING_PROFILE=shared-host npm run perf:postgres:evidence -- --strict --json > docs/refactor/launch-artifacts/postgres-tuning-evidence.json",
"completedAt": "2026-06-30T10:08:00+08:00",
"artifact": "launch-artifacts/postgres-tuning-evidence.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"status": "pass",
"profile": "shared-host",
@@ -46,20 +74,46 @@
{
"id": "auth.remote-smoke",
"status": "pass",
"command": "AUTH_SMOKE_REQUIRE_ADMIN_TOKENS=true npm run smoke:auth:remote > docs/refactor/launch-artifacts/auth-remote-smoke.log",
"command": "AUTH_SMOKE_REQUIRE_ADMIN_TOKENS=true npm run smoke:auth:remote > docs/refactor/launch-artifacts/auth-remote-smoke.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:auth.remote-smoke' >> docs/refactor/launch-artifacts/auth-remote-smoke.log",
"completedAt": "2026-06-30T10:10:00+08:00",
"artifact": "launch-artifacts/auth-remote-smoke.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0,
"requireAdminTokens": true
}
},
{
"id": "auth.platform-admin-bootstrap",
"status": "pass",
"command": "Run npm run bootstrap:platform-admin dry-run, then npm run bootstrap:platform-admin -- --apply --confirm BOOTSTRAP_FIRST_PLATFORM_ADMIN, verify the platform.admin.bootstrapped audit event, and run npm run smoke:auth:remote with AUTH_SMOKE_EXPECTED_PLATFORM_ADMIN_USER_ID set to the same Auth identity; save the three raw artifacts and generate this redacted summary with SHA-256 identity bindings",
"completedAt": "2026-06-30T10:10:30+08:00",
"artifact": "launch-artifacts/platform-admin-bootstrap.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"schemaVersion": 1,
"status": "pass",
"failed": 0,
"dryRunVerified": true,
"applied": true,
"adminUserIdSha256": "replace-with-sha256-of-bootstrap-auth-user-uuid",
"authSmokeExpectedUserIdSha256": "replace-with-same-sha256",
"identityMatches": true,
"auditEvent": "platform.admin.bootstrapped",
"auditVerified": true,
"dryRunArtifactSha256": "replace-with-64-char-sha256",
"applyArtifactSha256": "replace-with-64-char-sha256",
"authSmokeArtifactSha256": "replace-with-64-char-sha256",
"auditArtifactSha256": "replace-with-64-char-sha256"
}
},
{
"id": "auth.sms-pnvs-diagnostics",
"status": "pass",
"command": "PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run diagnose:aliyun-pnvs > docs/refactor/launch-artifacts/sms-pnvs-diagnostics.json",
"completedAt": "2026-06-30T10:11:00+08:00",
"artifact": "launch-artifacts/sms-pnvs-diagnostics.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"ok": true,
"env": {
@@ -86,6 +140,7 @@
"command": "SMS_SMOKE_API_BASE_URL=https://api.example.com SMS_SMOKE_TENANT_ID=00000000-0000-0000-0000-000000000001 SMS_SMOKE_PHONE=replace-with-real-phone SMS_SMOKE_ORIGIN=https://admin.example.com npm run smoke:sms-login:remote -- --write docs/refactor/launch-artifacts/sms-pnvs-remote-smoke.json",
"completedAt": "2026-06-30T10:12:00+08:00",
"artifact": "launch-artifacts/sms-pnvs-remote-smoke.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0,
"provider": "aliyun-pnvs",
@@ -96,11 +151,38 @@
{
"id": "rls.tenant-isolation",
"status": "pass",
"command": "npm run test:rls > docs/refactor/launch-artifacts/rls-tenant-isolation.log",
"command": "DATABASE_URL=<isolated-production-schema-clone-url> npm run test:rls > docs/refactor/launch-artifacts/rls-tenant-isolation.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:rls.tenant-isolation' >> docs/refactor/launch-artifacts/rls-tenant-isolation.log",
"completedAt": "2026-06-30T10:20:00+08:00",
"artifact": "launch-artifacts/rls-tenant-isolation.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
"failed": 0,
"databaseEnvironment": "ci",
"databaseSource": "isolated-production-schema-or-sanitized-snapshot-clone"
}
},
{
"id": "db.tenant-foreign-key-audit",
"status": "pass",
"command": "DATABASE_URL=<isolated-production-schema-clone-url> npm run audit:tenant-foreign-keys -- --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY --write=docs/refactor/launch-artifacts/tenant-foreign-key-audit.json",
"completedAt": "2026-06-30T10:25:00+08:00",
"artifact": "launch-artifacts/tenant-foreign-key-audit.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"status": "pass",
"kind": "tenant-foreign-key-audit",
"safety": {
"databaseEnvironment": "ci"
},
"schema": {
"schemaMatches": true,
"relationCount": 189,
"exceptionCount": 3
},
"data": {
"auditedRelations": 189,
"invalidRelations": 0
}
}
},
{
@@ -109,6 +191,7 @@
"command": "npm run pb:import:dry-run -- --profile=production --json --fail-on-warnings > docs/refactor/launch-artifacts/pb-production-dry-run.json",
"completedAt": "2026-06-30T10:30:00+08:00",
"artifact": "launch-artifacts/pb-production-dry-run.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"blocker": 0,
"warning": 0,
@@ -120,9 +203,10 @@
{
"id": "migration.pb-import-validate",
"status": "pass",
"command": "FAIL_ON_WARNINGS=true npm run pb:import:validate > docs/refactor/launch-artifacts/pb-import-validate.log",
"command": "FAIL_ON_WARNINGS=true npm run pb:import:validate > docs/refactor/launch-artifacts/pb-import-validate.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:migration.pb-import-validate' >> docs/refactor/launch-artifacts/pb-import-validate.log",
"completedAt": "2026-06-30T10:40:00+08:00",
"artifact": "launch-artifacts/pb-import-validate.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0
}
@@ -130,9 +214,10 @@
{
"id": "migration.pb-import-sample",
"status": "pass",
"command": "PB_SAMPLE_WRITE_REPORT=true npm run pb:import:sample > docs/refactor/launch-artifacts/pb-import-sample.log",
"command": "PB_SAMPLE_WRITE_REPORT=true npm run pb:import:sample > docs/refactor/launch-artifacts/pb-import-sample.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:migration.pb-import-sample' >> docs/refactor/launch-artifacts/pb-import-sample.log",
"completedAt": "2026-06-30T10:45:00+08:00",
"artifact": "launch-artifacts/pb-import-sample.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0,
"warn": 0,
@@ -143,9 +228,10 @@
{
"id": "performance.api-real-data-read",
"status": "pass",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=300 PERF_CONCURRENCY=30 PERF_RAMP_SECONDS=30 PERF_INCLUDE_WRITES=false npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-read-benchmark.log",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=300 PERF_CONCURRENCY=30 PERF_RAMP_SECONDS=30 PERF_INCLUDE_WRITES=false npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-read-benchmark.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:performance.api-real-data-read' >> docs/refactor/launch-artifacts/api-real-data-read-benchmark.log",
"completedAt": "2026-06-30T10:48:00+08:00",
"artifact": "launch-artifacts/api-real-data-read-benchmark.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"errors": 0,
"errorRate": 0,
@@ -159,9 +245,10 @@
{
"id": "performance.api-real-data-mixed",
"status": "pass",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=120 PERF_CONCURRENCY=50 PERF_RAMP_SECONDS=15 PERF_INCLUDE_WRITES=true PERF_PRACTICE_FLOW_RATIO=0.1 PERF_AUTH_MODE=app_session npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-mixed-benchmark.log",
"command": "PERF_START_SERVER=false PERF_API_BASE=https://api.example.com PERF_DURATION_SECONDS=120 PERF_CONCURRENCY=50 PERF_RAMP_SECONDS=15 PERF_INCLUDE_WRITES=true PERF_PRACTICE_FLOW_RATIO=0.1 PERF_AUTH_MODE=app_session npm run perf:api:local > docs/refactor/launch-artifacts/api-real-data-mixed-benchmark.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:performance.api-real-data-mixed' >> docs/refactor/launch-artifacts/api-real-data-mixed-benchmark.log",
"completedAt": "2026-06-30T10:49:00+08:00",
"artifact": "launch-artifacts/api-real-data-mixed-benchmark.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"errors": 0,
"errorRate": 0,
@@ -172,12 +259,88 @@
"includeWrites": true
}
},
{
"id": "performance.tenant-students-100k",
"status": "pass",
"command": "output_dir=$(mktemp -d) && DATABASE_URL=<target-spec-isolated-production-schema-clone-url> npm run perf:tenant-students:evidence -- --count=100000 --batch-size=10000 --iterations=20 --warmup-iterations=3 --output-dir=\"$output_dir\" --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY && cp \"$output_dir\"/tenant-student-capacity-*.json docs/refactor/launch-artifacts/tenant-student-capacity-100k.json",
"completedAt": "2026-06-30T10:49:10+08:00",
"artifact": "launch-artifacts/tenant-student-capacity-100k.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"databaseEnvironment": "test",
"fixture": {
"platformUsers": 100000,
"tenantMemberships": 100000,
"studentProfiles": 100000
},
"caseCount": 5,
"deepCursorApproximateOffset": 90000,
"firstPageP95Ms": 0,
"deepCursorP95Ms": 0,
"searchP95MaxMs": 0,
"keysetIndexUsed": true,
"trigramIndexUsed": true,
"cleanupVerified": true,
"remainingManagedUsers": 0
}
},
{
"id": "api.dynamic-tenant-cors-smoke",
"status": "pass",
"command": "TENANT_CORS_API_BASE_URL=https://api.example.com TENANT_CORS_ACTIVE_ORIGIN=https://active-tenant.example.com TENANT_CORS_DISABLED_ORIGIN=https://disabled-tenant.example.com TENANT_CORS_UNKNOWN_ORIGIN=https://unknown-tenant.example.com npm run smoke:tenant-cors:remote -- --write docs/refactor/launch-artifacts/tenant-cors-remote-smoke.json",
"completedAt": "2026-06-30T10:49:15+08:00",
"artifact": "launch-artifacts/tenant-cors-remote-smoke.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0,
"activeTenantOriginAllowed": true,
"unknownOriginDenied": true,
"disabledOriginDenied": true,
"noOriginHealthAllowed": true
}
},
{
"id": "deploy.linux-systemd-verify",
"status": "pass",
"command": "(systemd-analyze verify /etc/systemd/system/tiku-api.service /etc/systemd/system/tiku-worker@.service /etc/systemd/system/tiku-worker-job@.service /etc/systemd/system/tiku-worker-*.service /etc/systemd/system/tiku-worker-*.timer && systemctl is-active tiku-api.service tiku-workers.target && systemctl list-timers 'tiku-worker-*' --no-legend && echo 'TIKU_LAUNCH_GATE_SUCCESS:deploy.linux-systemd-verify') > docs/refactor/launch-artifacts/linux-systemd-verify.log",
"completedAt": "2026-06-30T10:49:20+08:00",
"artifact": "launch-artifacts/linux-systemd-verify.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0,
"apiServiceActive": true,
"workerTargetActive": true,
"workerJobServices": 9,
"enabledTimers": 6
}
},
{
"id": "backup.restore-drill",
"status": "pass",
"command": "RESTORE_DRILL_VERIFY_COMMAND=<provider-specific-isolated-restore-and-integrity-command> && eval \"$RESTORE_DRILL_VERIFY_COMMAND\" > docs/refactor/launch-artifacts/backup-restore-verification.log && node -e \"const fs=require('fs'),crypto=require('crypto');const required=name=>{const value=String(process.env[name]||'').trim();if(!value)throw new Error(name+' is required');return value};const number=name=>{const value=Number(required(name));if(!Number.isFinite(value)||value<0)throw new Error(name+' must be non-negative');return value};const verification=fs.readFileSync('docs/refactor/launch-artifacts/backup-restore-verification.log');const payload={schemaVersion:1,status:'pass',failed:0,snapshotId:required('RESTORE_DRILL_SNAPSHOT_ID'),restoreTarget:required('RESTORE_DRILL_TARGET'),isolated:true,integrityVerified:true,rtoMinutes:number('RESTORE_DRILL_RTO_MINUTES'),rpoMinutes:number('RESTORE_DRILL_RPO_MINUTES'),verificationArtifactSha256:crypto.createHash('sha256').update(verification).digest('hex')};fs.writeFileSync('docs/refactor/launch-artifacts/backup-restore-drill.json',JSON.stringify(payload,null,2)+'\\n')\"",
"completedAt": "2026-06-30T10:49:25+08:00",
"artifact": "launch-artifacts/backup-restore-drill.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"schemaVersion": 1,
"status": "pass",
"failed": 0,
"snapshotId": "replace-with-provider-snapshot-or-backup-id",
"restoreTarget": "replace-with-isolated-restore-target",
"isolated": true,
"integrityVerified": true,
"rtoMinutes": 0,
"rpoMinutes": 0,
"verificationArtifactSha256": "replace-with-64-char-sha256"
}
},
{
"id": "api.launch-persona-smoke",
"status": "pass",
"command": "LAUNCH_SMOKE_AUTH_MODE=app_session npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md > docs/refactor/launch-artifacts/launch-persona-smoke.log",
"completedAt": "2026-06-30T10:49:30+08:00",
"artifact": "launch-artifacts/launch-persona-smoke.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"status": "pass",
"authMode": "app_session",
@@ -221,9 +384,10 @@
{
"id": "api.integration",
"status": "pass",
"command": "npm run test:api > docs/refactor/launch-artifacts/api-integration.log",
"command": "npm run test:api > docs/refactor/launch-artifacts/api-integration.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:api.integration' >> docs/refactor/launch-artifacts/api-integration.log",
"completedAt": "2026-06-30T10:50:00+08:00",
"artifact": "launch-artifacts/api-integration.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -231,9 +395,10 @@
{
"id": "worker.assets",
"status": "pass",
"command": "npm run test:worker:assets > docs/refactor/launch-artifacts/worker-assets.log",
"command": "npm run test:worker:assets > docs/refactor/launch-artifacts/worker-assets.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.assets' >> docs/refactor/launch-artifacts/worker-assets.log",
"completedAt": "2026-06-30T11:00:00+08:00",
"artifact": "launch-artifacts/worker-assets.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -241,9 +406,10 @@
{
"id": "worker.commerce",
"status": "pass",
"command": "npm run test:worker:commerce > docs/refactor/launch-artifacts/worker-commerce.log",
"command": "npm run test:worker:commerce > docs/refactor/launch-artifacts/worker-commerce.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.commerce' >> docs/refactor/launch-artifacts/worker-commerce.log",
"completedAt": "2026-06-30T11:10:00+08:00",
"artifact": "launch-artifacts/worker-commerce.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -251,9 +417,10 @@
{
"id": "worker.platform-billing",
"status": "pass",
"command": "npm run test:worker:platform-billing > docs/refactor/launch-artifacts/worker-platform-billing.log",
"command": "npm run test:worker:platform-billing > docs/refactor/launch-artifacts/worker-platform-billing.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.platform-billing' >> docs/refactor/launch-artifacts/worker-platform-billing.log",
"completedAt": "2026-06-30T11:15:00+08:00",
"artifact": "launch-artifacts/worker-platform-billing.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -261,9 +428,10 @@
{
"id": "worker.platform-dunning",
"status": "pass",
"command": "npm run test:worker:platform-dunning > docs/refactor/launch-artifacts/worker-platform-dunning.log",
"command": "npm run test:worker:platform-dunning > docs/refactor/launch-artifacts/worker-platform-dunning.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.platform-dunning' >> docs/refactor/launch-artifacts/worker-platform-dunning.log",
"completedAt": "2026-06-30T11:18:00+08:00",
"artifact": "launch-artifacts/worker-platform-dunning.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -271,9 +439,10 @@
{
"id": "worker.imports",
"status": "pass",
"command": "npm run test:worker:imports > docs/refactor/launch-artifacts/worker-imports.log",
"command": "npm run test:worker:imports > docs/refactor/launch-artifacts/worker-imports.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.imports' >> docs/refactor/launch-artifacts/worker-imports.log",
"completedAt": "2026-06-30T11:20:00+08:00",
"artifact": "launch-artifacts/worker-imports.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -281,9 +450,10 @@
{
"id": "worker.public-banks",
"status": "pass",
"command": "npm run test:worker:public-banks > docs/refactor/launch-artifacts/worker-public-banks.log",
"command": "npm run test:worker:public-banks > docs/refactor/launch-artifacts/worker-public-banks.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:worker.public-banks' >> docs/refactor/launch-artifacts/worker-public-banks.log",
"completedAt": "2026-06-30T11:30:00+08:00",
"artifact": "launch-artifacts/worker-public-banks.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -291,19 +461,46 @@
{
"id": "taro.check",
"status": "pass",
"command": "npm run check:taro > docs/refactor/launch-artifacts/taro-check.log",
"command": "npm run check:taro > docs/refactor/launch-artifacts/taro-check.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:taro.check' >> docs/refactor/launch-artifacts/taro-check.log",
"completedAt": "2026-06-30T11:40:00+08:00",
"artifact": "launch-artifacts/taro-check.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
},
{
"id": "taro.supply-chain",
"status": "pass",
"command": "npm --silent run audit:taro:supply-chain -- --json > docs/refactor/launch-artifacts/taro-supply-chain.json",
"completedAt": "2026-06-30T11:45:00+08:00",
"artifact": "launch-artifacts/taro-supply-chain.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"schemaVersion": 1,
"status": "pass-with-reviewed-toolchain-risk",
"securedBundleDependencies": {
"swiper": "12.1.2",
"lodash-es": "4.18.1"
},
"audit": {
"counts": {
"critical": 3,
"high": 10
}
},
"reviewedInvalidEdgeCount": 4,
"riskBoundaryPresent": true,
"riskControlCount": 4
}
},
{
"id": "taro.build.student",
"status": "pass",
"command": "npm run build:taro:h5:student > docs/refactor/launch-artifacts/taro-build-student.log",
"command": "npm run build:taro:h5:student > docs/refactor/launch-artifacts/taro-build-student.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:taro.build.student' >> docs/refactor/launch-artifacts/taro-build-student.log",
"completedAt": "2026-06-30T11:50:00+08:00",
"artifact": "launch-artifacts/taro-build-student.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -311,9 +508,10 @@
{
"id": "taro.build.tenant",
"status": "pass",
"command": "npm run build:taro:h5:tenant > docs/refactor/launch-artifacts/taro-build-tenant.log",
"command": "npm run build:taro:h5:tenant > docs/refactor/launch-artifacts/taro-build-tenant.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:taro.build.tenant' >> docs/refactor/launch-artifacts/taro-build-tenant.log",
"completedAt": "2026-06-30T12:00:00+08:00",
"artifact": "launch-artifacts/taro-build-tenant.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -321,9 +519,10 @@
{
"id": "taro.build.platform",
"status": "pass",
"command": "npm run build:taro:h5:platform > docs/refactor/launch-artifacts/taro-build-platform.log",
"command": "npm run build:taro:h5:platform > docs/refactor/launch-artifacts/taro-build-platform.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:taro.build.platform' >> docs/refactor/launch-artifacts/taro-build-platform.log",
"completedAt": "2026-06-30T12:10:00+08:00",
"artifact": "launch-artifacts/taro-build-platform.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"failed": 0
}
@@ -334,6 +533,7 @@
"command": "npm --silent run smoke:taro:h5 -- --json > docs/refactor/launch-artifacts/taro-h5-static-smoke.json",
"completedAt": "2026-06-30T12:12:00+08:00",
"artifact": "launch-artifacts/taro-h5-static-smoke.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0,
"portals": 3,
@@ -346,6 +546,7 @@
"command": "npm --silent run smoke:taro:h5:interaction -- --json > docs/refactor/launch-artifacts/taro-h5-interaction-smoke.json",
"completedAt": "2026-06-30T12:13:00+08:00",
"artifact": "launch-artifacts/taro-h5-interaction-smoke.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0,
"pass": 32,
@@ -372,6 +573,7 @@
"command": "node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json > docs/refactor/launch-artifacts/taro-h5-release-guardrails.json",
"completedAt": "2026-06-30T12:15:00+08:00",
"artifact": "launch-artifacts/taro-h5-release-guardrails.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0,
"warn": 0
@@ -383,20 +585,23 @@
"command": "npm --silent run manifest:taro:h5 -- --require-dist --require-runtime-config --json --write docs/refactor/launch-artifacts/taro-h5-release-manifest.json > docs/refactor/launch-artifacts/taro-h5-release-manifest.stdout.json",
"completedAt": "2026-06-30T12:16:00+08:00",
"artifact": "launch-artifacts/taro-h5-release-manifest.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"fail": 0,
"warn": 0,
"portals": 3,
"distReady": 3,
"runtimeConfigs": 3
"runtimeConfigs": 3,
"treeHashes": 3
}
},
{
"id": "audit.runtime",
"status": "pass",
"command": "npm run audit:runtime > docs/refactor/launch-artifacts/audit-runtime.log",
"command": "npm run audit:runtime > docs/refactor/launch-artifacts/audit-runtime.log && echo 'TIKU_LAUNCH_GATE_SUCCESS:audit.runtime' >> docs/refactor/launch-artifacts/audit-runtime.log",
"completedAt": "2026-06-30T12:20:00+08:00",
"artifact": "launch-artifacts/audit-runtime.log",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"critical": 0,
"high": 0
@@ -408,6 +613,7 @@
"command": "npm run security:repo -- --json > docs/refactor/launch-artifacts/repo-security-scan.json",
"completedAt": "2026-06-30T12:22:00+08:00",
"artifact": "launch-artifacts/repo-security-scan.json",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"critical": 0,
"high": 0
@@ -416,9 +622,10 @@
{
"id": "security.codex-scan",
"status": "replace-with-pass-after-real-scan",
"command": "Run the real @codex-security scan only when the tool is exposed; save findings to docs/refactor/launch-artifacts/codex-security.md",
"command": "Run the real @codex-security scan only when the tool is exposed; save findings to docs/refactor/launch-artifacts/codex-security.md and append a final line TIKU_LAUNCH_GATE_SUCCESS:security.codex-scan only after the scan exits successfully",
"completedAt": "replace-with-real-scan-time",
"artifact": "launch-artifacts/codex-security.md",
"artifactSha256": "replace-with-64-char-sha256",
"summary": {
"critical": "replace-with-number",
"high": "replace-with-number"

View File

@@ -0,0 +1,54 @@
# Taro H5 浏览器验收记录2026-07-12
## 验收范围
- 产物:学生端、租户后台、平台后台 H5
- 浏览器Codex 应用内 Chromium 浏览器
- 视口:桌面 `1440x900`、移动 `390x844`
- 数据:本地 mock API不连接生产数据库或第三方 provider
- 流程:首屏渲染、控制台、页面溢出、每个门户至少一个主入口交互
为了让应用内浏览器直接访问本地 API视觉验收使用相同代码的 preview 构建。验收结束后必须重新生成 production 构建并重跑静态及交互烟测preview 产物不能用于发布。
## 结果
| 门户 | 桌面首屏 | 移动首屏 | 控制台 error/warn | 页面级横向溢出 | 主入口交互 |
| --- | --- | --- | --- | --- | --- |
| 学生端 | 通过 | 通过 | 0 | 0 | `开始刷题` 进入题库页 |
| 租户后台 | 通过 | 通过 | 0 | 0 | `学生运营` 进入学生运营页 |
| 平台后台 | 通过 | 通过 | 0 | 0 | `租户管理` 进入租户管理页 |
独立 Chrome 业务交互烟测另行覆盖三端完整关键链路,最终结果为 `33/33`。Taro H5 runtime 加固后Input 探针验证挂载前 `before-mount` 和挂载后 `after-mount` 均正确同步Button 探针连续切换 loading 200 次loading DOM 节点保持同一引用,子节点数始终为 `1`,显示状态为 `none -> inline-block -> none`
## 发现与前端重构约束
1. 当前界面可用且响应式基础成立,但仍是功能原型,不代表视觉设计已达到正式商用标准。
2. 移动端无页面级横向滚动;顶部导航采用横向可滚动模式,属于既有设计行为。
3. 学生端构建入口约 `500 KiB`,且存在多个超过 `244 KiB` 的异步资源。前端重构必须建立路由拆包、资源预算和低端移动网络首屏指标,不能在当前入口继续无约束叠加组件。
4. 租户后台工作台 DOM 中存在位于横向快捷操作容器可视区之外的按钮,但容器自身裁切且页面宽度未溢出;正式重构时应改为明确的滚动、折叠或响应式操作布局。
## 复现
```bash
# 应用内浏览器视觉 QA 必须先生成三套 preview 产物。
npm run build:taro:h5:preview
# 该命令保持运行并打印三个门户 URL若检测到 production 产物会直接拒绝启动。
npm run serve:taro:h5:qa
# 视觉 QA 结束后必须重新生成 production 产物preview 产物不得发布。
npm run build:taro:h5:student
npm run build:taro:h5:tenant
npm run build:taro:h5:platform
npm run smoke:taro:h5
npm run smoke:taro:h5:interaction
```
截图证据保存在本机临时目录 `/tmp`,不进入发布仓库:
- `tiku-student-desktop-1440x900.png`
- `tiku-student-mobile-390x844.png`
- `tiku-tenant-desktop-1440x900.png`
- `tiku-tenant-mobile-390x844.png`
- `tiku-platform-desktop-1440x900.png`
- `tiku-platform-mobile-390x844.png`

View File

@@ -1,6 +1,6 @@
# Taro H5 三入口部署说明
# Taro H5 三入口与学生微信小程序部署说明
更新时间2026-07-01
更新时间2026-07-11
当前 `apps/taro` 采用一个 Taro 4 React 工程、三套 H5 产物的方式交付:
@@ -8,7 +8,7 @@
- 租户后台品牌、主题、域名、题库、导入、学生、订单、营销、销售、CRM、财务和数据看板。
- 平台后台租户、SaaS 套餐、订阅账单、公共题库授权和平台审计。
后续微信小程序复用同一套业务 services 和页面逻辑,但 H5 是当前优先上线形态
学生微信小程序复用同一套业务 services、权限、主题和页面逻辑;复杂租户后台与平台后台仍优先发布桌面 H5
## 构建命令
@@ -16,6 +16,7 @@
npm run build:taro:h5:student
npm run build:taro:h5:tenant
npm run build:taro:h5:platform
npm run build:taro:weapp:student
```
输出目录:
@@ -24,8 +25,11 @@ npm run build:taro:h5:platform
apps/taro/dist/h5-student
apps/taro/dist/h5-tenant-admin
apps/taro/dist/h5-platform-admin
apps/taro/dist/weapp-student
```
三套 H5 会按 portal 裁剪实际注册页面;微信小程序主包只保留 `pages/bootstrap/index`,学生页面放入 `pages/student` 分包并启用组件按需注入。每种 target/portal 都有独立输出目录,连续构建不会互相覆盖。
注意Taro H5 入口依赖 `apps/taro/src/index.html` 模板生成 `index.html`。如果构建产物目录里只有 `js/css/assets` 而没有 `index.html`,不要发布;重新构建并运行发布守卫脚本。
推荐部署:
@@ -75,7 +79,7 @@ portal student | tenant-admin | platform-admin
apiBaseUrl apps/api 公开 HTTPS 地址
supabaseUrl Supabase Auth/API 公开 HTTPS 地址
supabasePublishableKey Supabase publishable/anon key
tenantCode 小程序、预览环境或指定租户部署可用
tenantCode H5 生产必须为空;仅小程序或本地预览可用
```
这些字段是前端公开配置,不是密钥。`apps/taro/src/env.ts` 会拒绝 `runtime-config.json` 中出现服务端密钥类字段,例如:
@@ -129,18 +133,28 @@ server {
## CORS 和 Cookie
API 的生产 `CORS_ORIGIN` 必须只包含实际前端域名
API 的生产 `CORS_ORIGIN` 只维护数量很少、由平台自己管理的中央平台/运维 Origin
```text
CORS_ORIGIN=https://www.example.com,https://admin.example.com,https://console.example.com
CORS_ORIGIN=https://platform-admin.example.com,https://ops.example.com
CORS_TENANT_DOMAINS_ENABLED=true
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
```
学生端和租户后台的业务 Origin 不展开写入 `CORS_ORIGIN`。API 使用 Origin 的规范化 hostname 查询 `active tenant_domains + active tenants`,并使用有界的 LRU TTL 正/负缓存。未知域名、`pending/failed/disabled` 域名、非 active 租户、非 HTTPS 或使用非默认端口的动态租户 Origin 都返回 `403 CORS_ORIGIN_DENIED`。本地开发端口只能作为完整 Origin 显式写入静态列表。
正缓存 TTL 是域名禁用后最长的准入传播时间;负缓存 TTL 是新域名启用后最长的生效等待时间。数据库查询异常会 fail closed 并短暂负缓存,不会因数据库故障放开未验证 Origin。紧急禁用域名时可在修改数据库状态后重启 API 进程立即清空进程内缓存。
禁止生产环境使用:
```text
CORS_ORIGIN=*
```
H5 租户解析和 CORS 都以浏览器自动发送的 `Origin` hostname 为权威域名:只查询已启用的 `tenant_domains` 和已启用租户未绑定域名不回退主租户。CORS 不读取 `Host`/`X-Forwarded-Host`/`x-tenant-code` 来决定准入,伪造这些头无法绕过未知 Origin 拒绝。Nginx/CDN 不得覆盖或伪造 `Origin`,并应在 API 反代层清空客户端传入的 `X-Forwarded-Host``host` query 只用于 localhost 开发合同;无浏览器 Origin 的健康检查、服务端客户端和微信小程序不会被 CORS 拦截,小程序使用 `tenantCode` 解析租户。
当前前端以 `Authorization: Bearer <supabase_access_token 或 tk_session>` 调用 API`x-tenant-id` 只作为租户上下文,不作为身份来源。生产建议:
```text
@@ -163,13 +177,15 @@ H5 正式回归时建议把前端登录态切到 Supabase Auth并观察业务
## 发布步骤
1. 在新服务器或 CI 环境构建三套 H5
1. 在新服务器或 CI 环境构建三套 H5 和学生微信小程序
```bash
npm ci
npm ci --workspaces --include-workspace-root --include=dev
npm run audit:taro:supply-chain
npm run build:taro:h5:student
npm run build:taro:h5:tenant
npm run build:taro:h5:platform
npm run build:taro:weapp:student
```
2. 拷贝静态产物到对应 Web 根目录。
@@ -197,7 +213,7 @@ H5 正式回归时建议把前端登录态切到 Supabase Auth并观察业务
node scripts/taro-h5-release-guardrails-test.js --require-dist
```
`smoke:taro:h5` 会用临时静态服务器检查三套 H5 产物可托管、资源可加载、history fallback 可用,并用 mock API 验证租户解析契约。`smoke:taro:h5:interaction` 会用真实 Chrome/Edge 打开三套发布产物并点击 32 项关键路径:学生刷题、收藏、错题/收藏复习、背单词、知识手册、资料、视频、分数线、AI 择校、消息、会员下单和订单状态,租户后台内容导入、公共题库采纳/同步/冲突处理、学生运营、营销/CRM/分佣、主题/角色/成员写操作,以及平台后台租户、账务、公共题库授权和员工写操作若服务器没有默认浏览器,可设置 `TARO_H5_SMOKE_BROWSER=/path/to/chrome`。`manifest:taro:h5` 会生成三套 H5 的部署清单,包含构建命令、发布目录、入口路由、`index.html` hash、资源数量、runtime-config 是否存在、租户解析模式和公开配置状态。发布守卫会检查三套 H5 产物是否存在 `index.html`,源码和产物是否混入 `x-user-id`、`x-platform-admin-key`、PocketBase 引用、数据库连接串、服务端密钥形态,并检查运行时配置示例只包含公开字段。若还没有把真实 `runtime-config.json` 放入静态目录,会显示 warning正式发布前必须在每个 H5 目录根部补齐该文件。
`smoke:taro:h5` 会用临时静态服务器检查三套 H5 产物可托管、资源可加载、history fallback 可用,并用 mock API 验证租户解析契约。`smoke:taro:h5:interaction` 会用真实 Chrome/Edge 打开三套发布产物并点击 33 项关键路径:学生登录 401、刷题、收藏、错题/收藏复习、背单词、知识手册、资料、视频、分数线、AI 择校、消息、会员下单和订单状态,租户后台内容导入、公共题库采纳/同步/冲突处理、学生运营、营销/CRM/分佣、主题/角色/成员写操作,以及平台后台租户、账务、公共题库授权和员工写操作。它还会跨三个门户切换桌面/移动视口,验证 Input 挂载前后同步,并将 Button loading 连续切换 200 次,要求 loading 节点和子节点数量保持稳定任何浏览器异常、console error、非允许 HTTP 错误都会失败并记录时间戳、行列号和 stack。若服务器没有默认浏览器,可设置 `TARO_H5_SMOKE_BROWSER=/path/to/chrome`。`manifest:taro:h5` 会生成三套 H5 的部署清单,包含构建命令、发布目录、入口路由、`index.html` hash、资源数量、runtime-config 是否存在、租户解析模式和公开配置状态。发布守卫会检查三套 H5 产物是否存在 `index.html`,源码和产物是否混入 `x-user-id`、`x-platform-admin-key`、PocketBase 引用、数据库连接串、服务端密钥形态,并检查运行时配置示例只包含公开字段。若还没有把真实 `runtime-config.json` 放入静态目录,会显示 warning正式发布前必须在每个 H5 目录根部补齐该文件。
写入 `production-launch-evidence.json` 的正式证据必须使用严格模式,确保三套发布目录都已放置真实公开 `runtime-config.json` 且没有 warning
@@ -219,6 +235,8 @@ H5 正式回归时建议把前端登录态切到 Supabase Auth并观察业务
8. 打开三个域名,确认 `index.html` 正常加载,启动页能解析租户,登录后接口请求使用 `Authorization` 和正确的 `x-tenant-id`。
9. 用微信开发者工具打开 `apps/taro/dist/weapp-student`,再用真机验证租户解析、登录、刷题、支付、文件、音视频和分享。`apps/taro/project.config.json` 的 `miniprogramRoot` 已指向该目录;正式发布前必须替换测试 AppID并配置 API、下载、上传、媒体和业务回调合法域名。
## 安全审计边界
H5 线上只发布 `apps/taro/dist/**` 静态文件和每个目录自己的 `runtime-config.json`,不要把 `apps/taro/node_modules`、源码目录、`.env`、部署脚本缓存放进 Web 根目录。
@@ -229,8 +247,10 @@ H5 线上只发布 `apps/taro/dist/**` 静态文件和每个目录自己的 `run
npm run audit:runtime
```
Taro 4.2.0 当前构建工具链仍可能触发 `npm run audit:taro:toolchain` 的上游 high/critical 告警,主要来自构建期 CLI、webpack、swiper、lodash-es 等传递依赖。不要使用 `npm audit fix --force` 将 Taro 降级到 3.x;应等 Taro 官方升级后再处理,或后续评估 Vite runner 替代方案。上线时以静态产物前端密钥检查、CORS 域名白名单、CSP 和 API runtime audit 作为阻断项
Taro 4.2.0 当前构建工具链仍触发 `npm run audit:taro:toolchain` 的上游 high/critical 告警`npm audit --omit=dev` 已验证生产运行时为 0 漏洞。不要使用 `npm audit fix --force` 将 Taro 降级到 3.x。`swiper@12.1.2`、`lodash-es@4.18.1` 和两项 H5 runtime patch 已完成可重复干净安装、三端构建、静态/交互 smoke 和小程序构建验证,正式基线以 `npm run audit:taro:supply-chain` 的精确 allowlist/hash 为准。部署必须允许 workspace postinstall不得使用 `--ignore-scripts`。构建机必须隔离、不得对公网暴露 dev server、不得处理不可信模板/压缩包或执行不可信 CLI 参数;上线阻断项仍包括 production runtime audit、静态产物前端密钥检查、CORS/CSP、三端烟测和 release manifest。工具链风险需持续跟踪不能把运行时 0 漏洞表述成工具链 0 漏洞
## 小程序后续兼容
## 微信小程序发布边界
当前 `runtime-config.json` 只用于 H5。微信小程序版本应通过编译变量、小程序启动参数或后台小程序配置传入 `tenantCode`,再调用 `GET /api/tenant/resolve?tenantCode=...`。小程序端如 `supabase-js` 兼容性不稳定,保留 `apps/api/auth/*` 登录适配层H5 继续使用 Supabase client 管理 Auth
`runtime-config.json` 只用于 H5。微信小程序通过 `TARO_APP_TENANT_CODE`、小程序启动参数或受控后台配置传入 `tenantCode`,再调用 `GET /api/tenant/resolve?tenantCode=...`。小程序登录优先走 `apps/api/auth/*` 的短信或 `code2Session` 适配层,不把 AppSecret、session_key、service role key 或数据库连接信息放进小程序包
当前只提供学生微信小程序构建。`npm run build:taro:weapp:student` 仅用于本地预览;正式上传必须设置 `TARO_APP_API_BASE_URL=https://...`、`WECHAT_MINIAPP_APP_ID` 并运行 `npm run build:taro:weapp:student:production`。单租户品牌包使用默认 `fixed` 模式并设置 `TARO_APP_TENANT_CODE`;面向数百租户的共享小程序使用 `TARO_APP_WEAPP_TENANT_MODE=launch`,从小程序码 query/scene 或 `referrerInfo.extraData.tenantCode` 解析租户码。launch 模式启动时缺少租户码会直接阻断,不会回退默认租户。严格构建会拒绝 localhost、测试 AppID、关闭合法域名检查或超出 4 MiB 总包/2 MiB 主包预算的产物。租户后台和平台后台包含大表格、批量导入、财务与运营工作流,不应为了“多端一致”强行塞进小程序;后续 App 同样优先复用学生端页面和共享 services再按原生能力逐项验收。

View File

@@ -1,6 +1,6 @@
# Taro 生产接入检查清单
更新时间2026-07-01
更新时间2026-07-12
这份清单给前端同事和后续 AI 使用。目标是让 `apps/taro` 的 H5 学生端、租户后台、平台后台按当前 Supabase/PostgreSQL 新后端上线,后续再扩展微信小程序。旧小程序前端文件在 `F:\project\参考\旧题库小程序前端文件`,只作为视觉、交互状态和微信平台能力参考,不继承旧 PocketBase 直连、旧 token、旧安全假设。
@@ -31,10 +31,12 @@
"apiBaseUrl": "https://api.example.com",
"supabaseUrl": "https://supabase.example.com",
"supabasePublishableKey": "sb_publishable_xxx",
"tenantCode": "optional-tenant-slug"
"tenantCode": ""
}
```
生产 H5 的 `tenantCode` 必须为空,租户以当前浏览器 Origin/绑定域名为权威来源;非本地 H5 不允许用固定 `tenantCode` 覆盖域名。`tenantCode` 只用于无 host 的学生小程序/后端受信客户端,或本地 H5 开发。
禁止出现在 `runtime-config.json`、Taro 环境变量、源码和构建产物中的内容:
- Supabase service role / secret key。
@@ -72,11 +74,13 @@ node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime
npm run readiness:production
npm run readiness:production:db
npm run smoke:auth:remote
npm run test:rls
npm run audit:runtime
npm run audit:taro:supply-chain
npm run security:repo
```
`npm run test:rls` 另行在生产 schema/脱敏快照的隔离克隆库执行并留存证据;它会提交 smoke seed不得在真实生产 `DATABASE_URL` 上运行。不要在 `source /etc/tiku-saas/api.env` 后执行它。
正式上线前,三套 H5 严格发布证据、真实 Auth/RLS、真实 provider 抽样、对象存储控制、支付对账、PostgreSQL 严格调参证据、`security:repo` 和真实数据压测都要写入本地 `docs/refactor/production-launch-evidence.json`,再运行:
```bash

View File

@@ -0,0 +1,54 @@
# Taro supply-chain baseline - 2026-07-12
## Decision
Taro remains pinned to `4.2.0`. The root package now overrides the two vulnerable dependencies that are compiled into the H5 output:
- `swiper@12.1.2`
- `lodash-es@4.18.1`
Taro 4.2.0 declares exact older versions for these packages. npm therefore installs the secured overrides but reports `ELSPROBLEMS`. This is an upstream dependency-contract mismatch, not permission to ignore arbitrary dependency-tree errors.
Taro also remains on stable `4.2.0` because the available `4.2.1-beta.2` still contains the reproduced H5 Input watcher defect and introduces a wider beta regression surface. The Taro workspace postinstall applies two reviewed, fail-closed H5 runtime patches to the exact `@tarojs/components@4.2.0` package:
- Input watcher: guards `inputRef` before synchronizing `value`.
- Button loading: keeps the loading `<i>` node stable and changes only its `display`, avoiding Stencil child insertion/removal while React changes adjacent buttons.
The patcher validates the package version, lock integrity, pristine or patched source hashes, and all targets before writing either file. H5 build/dev commands run `--check` first. Student WeApp uses native mini-program components and does not depend on these H5-only patches.
`npm run audit:taro:supply-chain` fails unless all of the following remain true:
- `package.json`, `package-lock.json`, and the installed tree use the exact secured versions and expected integrity hashes.
- The only `npm ls` invalid packages are `swiper@12.1.2` and `lodash-es@4.18.1`.
- The only invalid edges are the four reviewed Taro 4.2.0 exact declarations.
- A full `apps/taro` workspace audit, including development dependencies, no longer reports `swiper` or `lodash-es`.
- Every remaining high or critical package is already present in the explicit reviewed build-toolchain allowlist. A new high or critical package fails closed.
- The installed Input target SHA-256 is `260bb8a07d66eaf3398904acb94a7c2cacabe4411b70a01fb0d03931fe95c499`.
- The installed Button target SHA-256 is `428db74e51382c68bc10211ff7815d494b086de465fdef97ca09f5b7ab8368ea`.
Do not replace this check with `npm audit --omit=dev`. Taro declares much of the frontend stack as development dependencies even though `swiper` and `lodash-es` are compiled into the shipped H5 JavaScript.
## Reproducibility evidence
Using npm `11.12.1` and Node.js `24.15.0`, a repository-external clean snapshot completed `npm ci --workspaces --include-workspace-root --include=dev` in `9.77s`. The Taro workspace postinstall automatically produced both reviewed patched hashes; the patch contract and independent supply-chain audit then passed. Deployment must not use `--ignore-scripts`, and must include development dependencies because Taro is a static build workspace.
Generate a machine-readable launch artifact with `npm --silent run audit:taro:supply-chain -- --json`. The `--silent` flag is required when redirecting stdout because the normal npm script banner is not JSON.
The secured versions and runtime patches have passed Taro TypeScript, three production H5 builds, static smoke `25/25`, full Chrome interaction smoke `33/33`, and the student WeApp preview build/guard. The browser runtime probe sets the Input value before and after mount, and toggles Button loading 200 times while asserting that the same loading node and child count remain stable. Release checks must continue to run after any Taro upgrade, override, patch hash, or lock-file change.
## Remaining risk boundary
The full workspace audit currently reports 38 findings: 3 critical, 10 high, and 25 moderate. The removed bundle findings account for the reduction from 40 findings and eliminate the known `swiper` prototype-pollution and `lodash-es` advisories from the resolved frontend dependency tree.
The remaining high and critical findings are in the Taro CLI and build chain, including repository/template download helpers, archive extraction, glob processing, minification, serialization, and webpack runner paths. They are not declared resolved and the raw `npm run audit:taro:toolchain` command intentionally remains non-zero.
Until Taro publishes a compatible upgrade, apply these controls:
- Build only on an isolated trusted runner with least-privilege credentials and no production database access.
- Do not expose the Taro development server to public networks.
- Do not feed untrusted templates, repositories, archives, configuration, or CLI arguments to the build process.
- Publish only reviewed static files under `apps/taro/dist`; never publish `node_modules`, source files, or build caches.
- Do not run `npm audit fix --force`; its current proposal crosses the Taro major-version contract and can regress the multi-end build.
- Re-run the supply-chain gate, TypeScript checks, H5/WeApp builds, and smoke tests whenever the lock file or any Taro package changes.
This is a controlled risk acceptance for the isolated build toolchain. It is not a claim that the Taro toolchain has zero vulnerabilities.

View File

@@ -0,0 +1,37 @@
# 租户外键完整性审计
## 目的
RLS 负责控制一行是否可见,但不能保证一条租户内记录引用的父记录也属于同一租户。历史 schema 仍有一批只引用父表 `id` 的外键,因此上线门禁同时执行:
- 快速 schema 指纹:生产 readiness 查询 `pg_catalog`,发现新增、删除、改名、未验证的租户间外键即阻断。
- 完整数据审计:只在 `local``test``ci` 隔离克隆库执行,逐条检查现有数据是否满足租户不变量并输出 JSON 证据。
默认规则是子表与父表 `tenant_id` 必须一致。当前只有三个显式例外:
- 平台审计规则允许 `tenant_id is null`,代表全局规则;租户专属规则必须与告警租户一致。
- 租户采用公共题库时,源题库可以属于平台租户,但必须是 `source_scope='platform'`
- 公共题库通知可以指向平台源题库,同样必须是 `source_scope='platform'`
新增例外必须同时补业务理由、查询不变量、合同测试和 schema 指纹。不得只更新指纹跳过审计。
## 执行
隔离库必须已应用全部迁移,并在 `app_private.environment_safety` 标记为 `local/test/ci``allow_destructive_tests=true`。该命令只读,但会扫描相关业务表,因此沿用破坏性测试确认口令,防止误连生产主库:
```bash
DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:55432/postgres' \
node scripts/tenant-foreign-key-audit.js \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY \
--write=docs/refactor/launch-artifacts/tenant-foreign-key-audit.json
```
通过条件:
- `schema.schemaMatches=true`
- `schema.unvalidatedRelations=[]`
- `data.auditedRelations` 等于指纹关系数
- `data.invalidRelations=0`
- `status=pass`
生成的 JSON 及 SHA-256 需要写入 `production-launch-evidence.json``db.tenant-foreign-key-audit` 检查项。

View File

@@ -0,0 +1,106 @@
# 单租户 10 万学生容量验证手册
本工具用合成数据验证租户后台学生列表在单租户最多 100000 名学生时的 PostgreSQL 查询形状、cursor 分页和子串搜索。它不会连接 API直接使用与 `GET /api/tenant-admin/students` 相同的 SQL 核心和下列索引:
- `idx_memberships_student_keyset_page`
- `idx_platform_users_identity_search_trgm`
## 安全边界
- 只能用于 `local` / `test` / `ci` 数据库,且 `app_private.environment_safety.allow_destructive_tests` 必须为 `true`
- 任何写入、基准或清理模式都必须显式传入 `SMOKE_SEED_LOCAL_OR_CI_ONLY`
- 租户 slug 必须以 `capacity-test-` 开头。如果同名租户没有匹配的 `metadata.capacityHarness` 标记,工具会拒绝使用它。
- 合成用户同时使用 `legacy_id``raw_profile.capacityHarness` 标记。清理前如果发现真实 `auth_user_id`、其他租户 membership、租户 owner 或专用租户中的非 harness membership工具会拒绝操作。
- 禁止使用 `tikupro-pg` 或任何生产数据库。当前本机 `127.0.0.1:5432` 映射到 `tikupro-pg`,因此工具也会对 localhost/loopback 的 5432 端口在连接前硬拒绝。本工具的本地结果 is not a production SLA。
不要为了跑工具而在现有服务器数据库上修改环境标记。应该新建专用的非生产 PostgreSQL/Supabase 实例,运行全部迁移,再设置测试环境标记。
## 1. 先看计划
默认模式不连接数据库、不写数据:
```bash
npm run perf:tenant-students:plan
```
如果希望在计划中显示脱敏后的 host / port / database / user可以同时提供 `DATABASE_URL`;工具不会输出密码。
## 2. 小规模 smoke
以专用隔离库为例smoke 使用独立的 `capacity-test-students-smoke` 租户生成 250 名学生,执行 5 组查询、写出 JSON/Markdown 证据,然后在成功或失败路径清理专用租户和合成用户。它不会清理 `capacity-test-students-100k` 中人为保留的基准夹具。
```bash
DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:55432/postgres' \
npm run test:tenant-students:capacity:smoke
```
报告默认保存到已忽略的 `docs/refactor/performance-reports/`。smoke 返回后应确认 `cleanup.deletedTenant=1``cleanup.deletedUsers=250`
## 3. 生成 10 万行并跑基准
`run` 会保留夹具,便于重复采样或查看查询计划:
```bash
DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:55432/postgres' \
npm run perf:tenant-students:run -- \
--count=100000 \
--batch-size=10000 \
--iterations=20 \
--warmup-iterations=3 \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
```
写入是 set-based 的 `generate_series` + UPSERT每批在独立事务中完成。同一个 count 可重复执行;如果已有夹具行数高于新 count工具会要求先清理避免隐式删数据。
正式上线证据应使用一次性 `evidence` 模式,它会在同一个受保护流程中完成 seed、benchmark、cleanup并把清理结果与四类残留计数写入同一份 JSON
```bash
DATABASE_URL='<target-spec-isolated-clone-url>' \
npm run perf:tenant-students:evidence -- \
--count=100000 \
--batch-size=10000 \
--iterations=20 \
--warmup-iterations=3 \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
```
`evidence` 模式成功时必须包含 `cleanup.cleanupVerified=true`,且 `remaining.tenants/platformUsers/memberships/profiles` 全部为 `0`
每组证据包含:
- 实际 `platform_users` / `tenant_memberships` / `student_profiles` 行数。
- 工具内部记录的 seed / benchmark / cleanup / total 阶段耗时。
- 首页和约 90% 深度 cursor 页。
- 姓名、手机号、email substring 搜索。
- 应用端观测 P50/P95以及每组 SQL 的 `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)`
- 实际使用的 plan node、index name、shared/local/temp buffer 摘要与完整 JSON plan。
## 4. 重复基准
```bash
DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:55432/postgres' \
npm run perf:tenant-students:benchmark -- \
--iterations=30 \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
```
深 cursor 使用约 90% 位置的真实 `(created_at, membership_id)` 锚点,不使用 OFFSET 制造查询。
## 5. 清理
```bash
DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:55432/postgres' \
npm run perf:tenant-students:cleanup -- \
--confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY
```
清理只会命中:
- `slug=capacity-test-students-100k` 且带匹配 `metadata.capacityHarness` 的专用租户。
- `raw_profile.capacityHarness.namespace=tiku.student-capacity.v1` 且 tenant slug 相同的合成用户。
自定义租户名时,六个命令都必须传入同一个 `--tenant-slug=capacity-test-...`
## 验收口径
这个工具首先是数据量和查询计划验证,不应单独用它承诺线上 SLA。正式验收还需要在与生产同规格的非生产环境复跑并与 API 并发压测、PostgreSQL 调优证据、连接池和云盘 I/O 指标一起归档。

View File

@@ -1,6 +1,6 @@
# Web 版上线前验收清单
更新时间2026-07-01
更新时间2026-07-11
这份清单用于先上线 H5 Web 题库。Taro 仍然是前端工程,后端以 Supabase Auth/JWT、PostgreSQL/RLS、`apps/api`、worker 为主。前端视觉和交互可以参考 `F:\project\参考\旧题库小程序前端文件` 和旧 Web 版,但不能继承旧 PocketBase 直连、旧鉴权或旧字段模型。
@@ -15,6 +15,24 @@
- 学生头像只做男女预设,不做上传、裁剪或第三方头像同步。
- 排行榜默认不请求、不展示;只有租户购买/开启活动且完成专项压测后再接独立页面。
- H5 构建目录必须包含 `index.html``js/``css/`,并且每个上线目录根部必须由部署方放置对应 `runtime-config.json`
- 三套 H5 只注册本 portal 页面;学生微信小程序使用独立 `dist/weapp-student` 目录和学生分包,不能覆盖 `dist/h5-student`
- 租户、会话、权限和主题由全局 App Provider 管理;业务缓存键按 portal、域名或 tenantCode、tenantId、userId 隔离,跨标签账号变化会触发重验,页面不得自行维护另一套长期身份状态。
## 首个平台超级管理员
生产库没有可登录平台管理员时,先在 Supabase Auth 创建或确认你的账号并取得 `auth.users.id`。只在生产运维终端运行服务器 CLI先 dry-run再使用精确确认短语写入
```bash
DATABASE_URL='<production-database-url>' \
BOOTSTRAP_PLATFORM_ADMIN_AUTH_USER_ID='<auth.users UUID>' \
BOOTSTRAP_PLATFORM_ADMIN_USERNAME='<operator username>' \
BOOTSTRAP_PLATFORM_ADMIN_NAME='<display name>' \
npm run bootstrap:platform-admin
npm run bootstrap:platform-admin -- --apply --confirm BOOTSTRAP_FIRST_PLATFORM_ADMIN
```
该命令不是公开 API不接收密码、验证码或 service role key不输出完整 Auth UUID、邮箱或手机号。写入后立即运行 `npm run readiness:production:db`,确认 active 超管、Auth 绑定和 `{"*":true}` 权限门禁通过;已有可登录超管时命令必须拒绝。
## 学生端验收旅程
@@ -76,12 +94,16 @@ npm run smoke:launch-persona
```bash
npm run test:readiness
npm run test:rls
npm run test:auth:foundation
npm run audit:runtime
npm run security:repo
npm run check:api
npm run check:worker
npm run check:taro
npm run build:taro:h5:student
npm run build:taro:h5:tenant
npm run build:taro:h5:platform
npm run build:taro:weapp:student
npm run readiness:production
npm run readiness:production:db
npm run smoke:taro:h5
@@ -90,9 +112,18 @@ node scripts/taro-h5-release-guardrails-test.js --require-dist
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md
```
`test:rls` 和完整 API/worker 集成测试是会写入数据的测试套件只允许连接本地、CI 或生产 schema/脱敏快照的隔离克隆库。克隆库必须在 `app_private.environment_safety` 显式标记 `environment='test'|'ci'``allow_destructive_tests=true`,然后单独留存动态 RLS 证据:
```bash
DATABASE_URL='<isolated-rls-clone-url>' \
npm run test:rls > docs/refactor/launch-artifacts/rls-tenant-isolation.log
```
禁止在 `source /etc/tiku-saas/api.env` 后直接运行 `test:rls``test:api``test:worker:*`。真实生产数据库只执行 `readiness:production:db` 等只读门禁;真实 API 验收使用无 seed 的 `smoke:auth:remote`、受控灰度租户 `smoke:launch-persona` 等专用脚本。
`smoke:launch-persona` 是真实 API 角色旅程烟测,必须进入生产上线证据;正式证据必须使用 `LAUNCH_SMOKE_AUTH_MODE=app_session`,通过 Bearer `tk_` session 验证身份,不使用旧 `x-user-id` 或平台本地 key。普通学生要能在 SVIP 权益下创建练习、答题、收藏题目、进入收藏复习和错题复习入口;租户管理员要能读取看板/主题/学生/销售转化并拒绝学生或跨租户访问;平台管理员要能读取租户/套餐/审计入口并拒绝学生访问平台后台。生产证据命令必须带 `--write docs/refactor/launch-artifacts/launch-persona-smoke.json`,确保 `production-launch-evidence.json` 引用的 artifact 是稳定路径,不是只存在带时间戳的本地报告。
`smoke:taro:h5` 会启动临时静态服务器和 mock API验证三套 H5 的 `index.html`、JS/CSS 资源、history fallback、公开 runtime config 和 `/api/tenant/resolve` 契约。`smoke:taro:h5:interaction` 会在真实 Chrome/Edge 中点击学生、租户后台、平台后台关键路径,覆盖静态烟测发现不了的 JS 运行时、直接 history 路由刷新和 Taro 点击事件问题;当前脚本覆盖 32 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台内容导入、公共题库采纳/同步/冲突处理、学生运营、营销/CRM/分佣、主题/角色/成员写操作,以及平台后台租户、账务、公共题库授权和员工写操作。`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id``x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning正式部署目录必须补齐。
`smoke:taro:h5` 会启动临时静态服务器和 mock API验证三套 H5 的 `index.html`、JS/CSS 资源、history fallback、公开 runtime config 和 `/api/tenant/resolve` 契约。`smoke:taro:h5:interaction` 会在真实 Chrome/Edge 中点击学生、租户后台、平台后台关键路径,覆盖静态烟测发现不了的 JS 运行时、直接 history 路由刷新和 Taro 点击事件问题;当前脚本覆盖 33 项检查,包括学生未登录 401、首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台内容导入、公共题库采纳/同步/冲突处理、学生运营、营销/CRM/分佣、主题/角色/成员写操作,以及平台后台租户、账务、公共题库授权和员工写操作另有跨门户移动视口、Input 挂载竞态和 Button loading 200 次稳定节点探针`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id``x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning正式部署目录必须补齐。
写入生产上线证据时,三套正式发布目录必须先放入真实公开 `runtime-config.json`,再运行严格模式:
@@ -103,6 +134,15 @@ node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime
npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-persona-smoke.json --write-md docs/refactor/launch-artifacts/launch-persona-smoke.md > docs/refactor/launch-artifacts/launch-persona-smoke.log
```
每个 `checks[].artifact` 都必须同时填写真实 `artifactSha256`。三端静态目录已经切换到正式域名后,部署脚本应显式再执行严格线上校验:
```bash
LAUNCH_GATE_VERIFY_LIVE_H5=true \
npm run launch:gate -- --evidence /etc/tiku-saas/production-launch-evidence.json --verify-live-h5
```
证据中的 `liveH5.releaseManifestArtifact` 指向本次候选 `taro-h5-release-manifest.json``liveH5.releaseManifestSha256` 记录该 manifest 文件的 SHA-256。严格模式会请求学生端、租户后台和平台后台各自的 `index.html``runtime-config.json` 和主 app bundle要求 HTTP 成功、portal 正确、`apiBaseUrl` 与证据中的生产 HTTPS API 一致,并验证线上 index/app 哈希与候选发布目录一致。默认 launch gate 保持离线,不带显式开关时不会访问公网。
PNVS 短信登录上线前要用真实手机号跑一次远程 smoke。脚本不会读取或输出密钥它只调用公网 API发送验证码后在终端输入收到的短信验证码再确认 `/api/auth/me` 可用:
```bash
@@ -125,7 +165,19 @@ PNVS 只验收手机号登录/换绑验证码链路。催缴、营销、CRM 等
`security:repo` 是仓库自带的静态安全扫描,会拦截密钥形态、前端旧鉴权头、真实 runtime-config 和生产证据误入 Git。它不能替代真实 `@codex-security`;如插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。
`readiness:production``readiness:production:db` 是生产阻断门禁:会拒绝 mock/未知短信 provider、弱密钥、`CORS=*`、旧身份头、local_dev 存储、非 HTTPS 对象存储公开 URL、阿里云 OSS 内网直签、未接外部资源扫描、localhost webhook以及租户短信/OAuth/支付公开配置缺字段、OAuth redirectUri/支付 notifyUrl 非 HTTPS、公开配置混入密钥、active provider 缺私密 `tenant_secrets` 等问题
`readiness:production``readiness:production:db` 是生产阻断门禁:会拒绝 mock/未知短信 provider、弱密钥、`CORS=*`、旧身份头、local_dev 存储、非 HTTPS 对象存储公开 URL、阿里云 OSS 内网直签、未接外部资源扫描、localhost webhook以及生产库残留 `local/test/ci``allow_destructive_tests=true` 标记、运行角色未通过 superuser bootstrap、租户公开配置缺字段/混入密钥、OAuth redirectUri/支付 notifyUrl 非 HTTPS、active provider 缺私密 `tenant_secrets`、active 租户公开 URL 指向 localhost/HTTP、缺少可登录平台管理员等问题。尚未发布主题只 warning 并回退平台默认主题,正式上线前仍要逐租户确认
首次应用数据库迁移前,由 self-hosted PostgreSQL/Supabase 的真正 superuser 执行一次:
```bash
DATABASE_ADMIN_URL='<secret-managed-superuser-url>' \
npm run bootstrap:db-runtime-roles -- \
--apply --confirm=BOOTSTRAP_BACKEND_RUNTIME_ROLES
```
该管理员连接只用于集群角色和官方 Supabase 基础镜像 `public` 扩展函数 ACL bootstrap不得进入 API/Worker 配置或发布证据。普通 migration 会验证 `tiku_api/tiku_worker` 已是 `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS` 且无父角色,并在 `anon/authenticated` 仍可执行任何 `public` 函数时直接阻断。每次 Supabase 镜像或扩展升级后都要重跑该幂等 bootstrap 与数据库 readiness。
微信小程序产物还必须在微信开发者工具和至少一台真机验证:打开 `apps/taro/dist/weapp-student`确认主包只保留启动页、学生页面位于分包、tenantCode 能解析正确租户、短信/微信登录能建立当前租户会话,并完成刷题、支付调起、文件预览、音视频和分享能力。租户后台与平台后台本阶段只发布桌面 H5。
生产 API 推荐:
@@ -133,9 +185,15 @@ PNVS 只验收手机号登录/换绑验证码链路。催缴、营销、CRM 等
ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false
CORS_ORIGIN=https://student.example.com,https://tenant-admin.example.com,https://platform-admin.example.com
CORS_TENANT_DOMAINS_ENABLED=true
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
AUTH_SMS_PROVIDER=aliyun-pnvs
```
`CORS_ORIGIN` 中的学生/租户后台域名只适用于平台自营的少量固定 Origin合作租户自定义域名必须由 `active tenant_domains + active tenants` 动态准入。上线验收必须同时验证 active Origin 通过inactive/unknown Origin 的 OPTIONS 和普通请求返回 `403 CORS_ORIGIN_DENIED`,伪造 Host 无法绕过,且无 Origin 的 `/health` 仍可用。
生产 worker 推荐:
```text