test: strengthen launch security and capacity gates

This commit is contained in:
Codex
2026-07-01 04:54:07 +08:00
parent 69b4d3b62d
commit 1eb5a13df7
13 changed files with 452 additions and 31 deletions

View File

@@ -53,7 +53,7 @@
| 对象存储/资料安全 | √ 可联调,待生产 AV/CDN | OSS/COS/Supabase Storage 签名、上传确认、短签名预览下载、水印 traceId、复检和安全扫描地基已完成 |
| PocketBase 真实数据迁移 | √ 本地跑通,待人工复核 blocker | SQLite 导出、标准化导入、校验和抽样脚本已跑通;正式切换前处理缺用户订单和缺归属手册章节 |
| Taro H5 三端前端 | √ 第一版可构建 | 学生端、租户后台、平台后台均有真实 API 页面;已补 H5 `index.html` 模板和发布产物守卫;后续继续补小程序兼容、视觉精修、状态管理、包体优化和端到端测试 |
| 生产安全/压测交付 | △ 本地真实数据压测已跑,云端待复测 | 本地 Docker/Supabase 已完成真实迁移数据 30/50/100/150 并发只读和混合读写压测;受限 API 容器复核中抓到并修复了自动勋章并发发放唯一键冲突,并补并发回归测试。最近一次只读上线门禁和 50/100/150 混合读写均 0 错误50/100 混合读写约 418-420 req/s150 并发 P95 超 600ms 进入压力区;上云后仍需执行生产 readiness、远程 Auth/RLS、4c16g 压测、PostgreSQL 调优和上线证据门禁。`@codex-security` 只有在 Codex 环境暴露扫描工具时才能标记完成 |
| 生产安全/压测交付 | △ 本地真实数据压测已跑,云端待复测 | 本地 Docker/Supabase 已完成真实迁移数据 30/50/100/150 并发只读和混合读写压测;受限 API 容器复核中抓到并修复了自动勋章并发发放唯一键冲突,并补并发回归测试。最近一次只读上线门禁和 50/100/150 混合读写均 0 错误50/100 混合读写约 428-444 req/s150 并发 P95 约 574ms 进入压力区;上云后仍需执行生产 readiness、远程 Auth/RLS、4c16g 压测、PostgreSQL 调优`security:repo`、真实 `@codex-security` 扫描和上线证据门禁。 |
更完整的进度看这些文档:
@@ -153,7 +153,7 @@ npm run smoke:taro:h5:interaction
`taro-route-contract-test` 会校验 `apps/taro/src/app.config.ts`、真实 `pages/**/index.tsx`、启动页三端跳转、H5 静态烟测入口和前端交接文档中的页面引用保持一致。新增或删除页面时必须同步路由和文档,避免 H5/小程序构建后才发现入口漂移。
`taro-api-contract-test` 会比对 `apps/taro/src` 中所有 `apiRequest('/api/...')` 调用与 `apps/api/src/features/*/index.ts` 注册路由,阻断前端调用不存在 API、method 写错或绕过统一 `/api` 命名空间的漂移;动态导入和少量 server alias 需要在脚本 allowlist 中显式声明。
`taro-persona-contract-test` 会从学生、租户管理员、平台管理员三类前端视角检查关键页面、路由和服务调用,阻断刷题、会员订单、错题收藏、学生运营、内容导入、营销财务、租户设置、平台租户账务和公共题库授权入口被误删或漂移。
`smoke:taro:h5:interaction` 会启动三套 H5 发布产物、本地 mock API 和本机 Chrome/Edge在真实浏览器里点击学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台,租户后台六个主模块,以及平台后台四个主模块,用来补足静态烟测无法发现的 H5 运行时空白页、history 路由和点击事件问题。
`smoke:taro:h5:interaction` 会启动三套 H5 发布产物、本地 mock API 和本机 Chrome/Edge在真实浏览器里点击学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块,用来补足静态烟测无法发现的 H5 运行时空白页、history 路由和点击事件问题。
H5 线上推荐每个静态目录放独立 `runtime-config.json` 覆盖公开配置,避免 API/Auth 域名变化时重打包:
@@ -393,6 +393,7 @@ npm run test:auth:remote-smoke
npm run test:launch-gate
npm run smoke:auth:remote
npm run audit:runtime
npm run security:repo
npm run pb:import:dry-run
npm run pb:import:validate
npm run test:pb:dry-run
@@ -468,10 +469,10 @@ npm run launch:gate -- --evidence docs/refactor/production-launch-evidence.json
- PocketBase production dry-run、`pb:import:validate``pb:import:sample`
- 真实数据 API 读路径压测、API/worker/Taro 构建。
- `smoke:taro:h5``smoke:taro:h5:interaction`、严格 `taro-h5-release-guardrails-test --require-runtime-config`
- `audit:runtime`、真实 `@codex-security` 扫描。
- `audit:runtime``security:repo`真实 `@codex-security` 扫描。
- 备份、回滚、真实数据抽样、生产 provider、对象存储控制、支付对账和三套 H5 `runtime-config.json` 人工确认。
补充说明:当前 Codex 环境如果没有暴露 `@codex-security` 可调用工具,不能把插件扫描写成已完成;只能先用 `npm run audit:runtime``npm run test:readiness``npm run test:rls` 和代码审查作为临时安全证据,并在上线证据里保留插件扫描待补项。
补充说明:当前 Codex 环境如果没有暴露 `@codex-security` 可调用工具,不能把插件扫描写成已完成;只能先用 `npm run audit:runtime``npm run security:repo``npm run test:readiness``npm run test:rls` 和代码审查作为临时安全证据,并在上线证据里保留插件扫描待补项。
模板文件:
@@ -616,7 +617,7 @@ npm run smoke:taro:h5:interaction
node scripts\taro-h5-release-guardrails-test.js --require-dist
```
`smoke:taro:h5` 会启动临时静态服务器和 mock API验证三套 H5 的 `index.html`、静态资源、history fallback、公开 runtime config 和租户解析契约。`smoke:taro:h5:interaction` 会再拉起真实 Chrome/Edge打开三套 H5 产物并点击 25 项关键入口,覆盖学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台,租户后台六个主模块,以及平台后台四个主模块,确认页面 JS 执行、路由跳转、关键 API 和后台入口点击没有空白页或运行时异常。`taro-h5-release-guardrails-test` 会确认三套 H5 目录存在 `index.html`,并扫描源码/产物是否混入旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串或服务端密钥形态。正式部署时还必须在每个 H5 目录根部放置对应的 `runtime-config.json`
`smoke:taro:h5` 会启动临时静态服务器和 mock API验证三套 H5 的 `index.html`、静态资源、history fallback、公开 runtime config 和租户解析契约。`smoke:taro:h5:interaction` 会再拉起真实 Chrome/Edge打开三套 H5 产物并点击 26 项关键入口,覆盖学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块,确认页面 JS 执行、路由跳转、关键 API 和后台入口点击没有空白页或运行时异常。`taro-h5-release-guardrails-test` 会确认三套 H5 目录存在 `index.html`,并扫描源码/产物是否混入旧 PocketBase、`x-user-id`、平台本地 key、数据库连接串或服务端密钥形态。正式部署时还必须在每个 H5 目录根部放置对应的 `runtime-config.json`
写入生产上线证据时使用严格模式,确保三套正式发布目录已经放好真实公开 `runtime-config.json`,且 warning 为 0
@@ -653,10 +654,10 @@ node scripts\taro-h5-release-guardrails-test.js --require-dist --require-runtime
| 50 | 60s | 10% | 35,608 | 0.00% | 584.18 req/s | 174.26 ms | 224.04 ms |
| 100 | 60s | 8% | 33,843 | 0.00% | 554.42 req/s | 326.21 ms | 422.33 ms |
| 150 | 60s | 6% | 32,364 | 0.00% | 528.81 req/s | 490.38 ms | 662.42 ms |
| Docker API 2c4g / 30 | 120s | 0% | 44,038 | 0.00% | 366.27 req/s | 185.80 ms | 254.08 ms |
| Docker API 2c4g / 50 | 60s | 10% | 25,327 | 0.00% | 419.62 req/s | 213.17 ms | 270.94 ms |
| Docker API 2c4g / 100 | 60s | 8% | 25,378 | 0.00% | 417.77 req/s | 383.72 ms | 461.22 ms |
| Docker API 2c4g / 150 | 60s | 6% | 23,444 | 0.00% | 384.39 req/s | 605.89 ms | 743.19 ms |
| Docker API 2c4g / 30 | 120s | 0% | 44,692 | 0.00% | 371.62 req/s | 188.91 ms | 275.79 ms |
| Docker API 2c4g / 50 | 60s | 10% | 26,815 | 0.00% | 444.49 req/s | 204.08 ms | 268.88 ms |
| Docker API 2c4g / 100 | 60s | 8% | 25,950 | 0.00% | 428.30 req/s | 375.38 ms | 447.47 ms |
| Docker API 2c4g / 150 | 60s | 6% | 23,970 | 0.00% | 394.80 req/s | 574.37 ms | 696.35 ms |
只读上线门禁继续要求 `includeWrites=false`;混合读写报告需要显式使用 `--allow-writes` 做人工容量观察,例如:
@@ -666,7 +667,7 @@ npm run perf:summary -- --input docs/refactor/performance-reports/api-benchmark-
使用 `--allow-writes` 时会输出 `capacityObservation`,不输出 `launchGateCheck`,不能把写入场景误填成生产上线门禁的只读证据。
本地结论:当前 Docker Desktop 分配 20 CPU、约 62.7GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 03:58 受限 API 容器复核中30 worker/120 秒只读上线门禁为 44,038 请求、0 错误、366.27 req/s、P95 185.80ms50 worker/60 秒/10% 写入为 25,327 请求、0 错误、419.62 req/s、P95 213.17ms100 worker/60 秒/8% 写入为 25,378 请求、0 错误、417.77 req/s、P95 383.72ms150 worker/60 秒/6% 写入为 23,444 请求、0 错误、384.39 req/s、P95 605.89ms属于压力区。该压测曾抓到自动勋章并发发放唯一键冲突,已修复并新增并发回归测试。按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,当前受限 API 容器舒适观察区间约对应 2,000 到 8,400 名活跃在线学生的请求吞吐。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见:
本地结论:当前 Docker Desktop 分配 20 CPU、约 62.7GB 内存,高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。2026-07-01 04:44 受限 API 容器复核中30 worker/120 秒只读上线门禁为 44,692 请求、0 错误、371.62 req/s、P95 188.91ms50 worker/60 秒/10% 写入为 26,815 请求、0 错误、444.49 req/s、P95 204.08ms100 worker/60 秒/8% 写入为 25,950 请求、0 错误、428.30 req/s、P95 375.38ms150 worker/60 秒/6% 写入为 23,970 请求、0 错误、394.80 req/s、P95 574.37ms属于压力区。该受限容器系列压测曾抓到自动勋章并发发放唯一键冲突,已修复并新增并发回归测试。按单学生 0.05 到 0.2 req/s 的页面节奏粗略折算,当前受限 API 容器舒适观察区间约对应 2,100 到 8,900 名活跃在线学生的请求吞吐。正式对外容量承诺必须在目标 4 核 16G 云服务器、生产 PostgreSQL 参数、生产对象存储/CDN 和真实前端请求节奏下复跑。脱敏摘要和剩余功能清单见:
```text
docs/refactor/performance-benchmark-summary-20260630.md

View File

@@ -43,7 +43,7 @@
- 环境Windows + Docker Desktop + 本地 Supabase/PostgreSQL + 本地 API 进程。
- Docker Desktop 当前资源20 CPU、约 62.7GB 内存。这个本机结果会高于常见 4 核 16G 云服务器,不能直接作为生产 SLA。
- 数据库PocketBase 真实导入数据,并已包含前期写入压测产生的练习/答题记录。
- 压测基线规模:约 7.4 万道题、3,700 个用户、1,600 个题目合集、3,100 个练习蓝图、3,500 个单词、2,600 条知识手册、19.6 万条答题记录、3.8 万条错题、466 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按压测基线描述,不把每次测试后的瞬时行数当作 SLA 口径。
- 当前本地迁移库规模:约 7.4 万道题、3,700 个用户、1,600 个题目合集、3,100 个练习蓝图、3,500 个单词、2,600 条知识手册、25.2 万条答题记录、3.8 万条错题、468 条权益。后续 `db:smoke-seed`、集成测试和压测会继续写入练习/答题/测试租户数据,因此这里按当前本地观察描述,不把每次测试后的瞬时行数当作生产 SLA 口径。
- 排行榜未纳入默认负载,因为产品默认关闭。
### 2026-07-01 真实迁移库 Docker API 受限资源复核
@@ -54,10 +54,10 @@
| 场景 | 并发 worker | 时长 | 刷题写入比例 | 请求数 | 错误率 | 吞吐 | P95 | P99 | 结论 |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| 只读上线门禁 | 30 | 120s | 0% | 44,038 | 0.00% | 366.27 req/s | 185.80 ms | 254.08 ms | 通过 |
| 混合读写 | 50 | 60s | 10% | 25,327 | 0.00% | 419.62 req/s | 213.17 ms | 270.94 ms | 舒适 |
| 混合读写 | 100 | 60s | 8% | 25,378 | 0.00% | 417.77 req/s | 383.72 ms | 461.22 ms | 接近舒适区上沿 |
| 混合读写 | 150 | 60s | 6% | 23,444 | 0.00% | 384.39 req/s | 605.89 ms | 743.19 ms | 压力区 |
| 只读上线门禁 | 30 | 120s | 0% | 44,692 | 0.00% | 371.62 req/s | 188.91 ms | 275.79 ms | 通过 |
| 混合读写 | 50 | 60s | 10% | 26,815 | 0.00% | 444.49 req/s | 204.08 ms | 268.88 ms | 舒适 |
| 混合读写 | 100 | 60s | 8% | 25,950 | 0.00% | 428.30 req/s | 375.38 ms | 447.47 ms | 接近舒适区上沿 |
| 混合读写 | 150 | 60s | 6% | 23,970 | 0.00% | 394.80 req/s | 574.37 ms | 696.35 ms | 压力区 |
混合读写包含真实刷题闭环:创建练习 session、拉取 session detail、提交答案、交卷、读取报告。
@@ -69,10 +69,10 @@
按目前常见页面节奏先用 `0.05 到 0.2 req/s/人` 粗略折算:
- 50 worker 舒适场景 420 req/s约等于 2,100 到 8,400 名活跃在线学生的请求吞吐。
- 100 worker 上沿场景 418 req/s约等于 2,090 到 8,360 名活跃在线学生的请求吞吐。
- 150 worker 压力场景 384 req/s约等于 1,920 到 7,680 名活跃在线学生的请求吞吐。
- 50 worker 舒适场景 444 req/s约等于 2,220 到 8,880 名活跃在线学生的请求吞吐。
- 100 worker 上沿场景 428 req/s约等于 2,140 到 8,560 名活跃在线学生的请求吞吐。
- 150 worker 压力场景 395 req/s约等于 1,975 到 7,900 名活跃在线学生的请求吞吐。
当前本地结论:在受限 API 容器下,后端真实刷题读写链路的舒适观察区间约为 418 到 420 req/s折算约 2,000 到 8,400 名活跃在线学生150 worker 仍 0 错误但 P95 已超过 600ms视为压力区不建议作为生产承诺。此前未限制 API 容器时本机可观察到约 550 到 585 req/s说明 API CPU/连接池约束会明显影响上沿
当前本地结论:在受限 API 容器下,后端真实刷题读写链路的舒适观察区间约为 428 到 444 req/s折算约 2,100 到 8,900 名活跃在线学生150 worker 仍 0 错误但 P95 已接近 600ms视为压力区不建议作为生产承诺。同日本地 API 进程 30 worker/30 秒/10% 写入观察到 15,864 请求、0 错误、513.56 req/s、P95 113.97ms;受限 API 容器会明显压低上沿,因此上线承诺仍以云端受限资源复测为准
正式对外容量必须等 4 核 16G 云服务器部署后复跑。保守规划时,生产首版建议先按本地折算值的 30% 到 50% 做容量承诺等云端压测、CDN、对象存储和真实前端埋点完成后再上调。

View File

@@ -46,10 +46,10 @@
- 新增、删除或重命名 Taro 页面时必须同步 `apps/taro/src/app.config.ts`、启动页跳转、H5 静态烟测入口和本文页面清单,并运行 `node scripts/taro-route-contract-test.js`。该脚本会阻断“页面文件存在但未注册”“路由注册但文件缺失”“启动页或烟测跳到不存在页面”的漂移。
- 新增或修改 Taro API service 时必须运行 `node scripts/taro-api-contract-test.js`。该脚本会比对前端 `apiRequest('/api/...')` 与后端 `RouteDefinition[]` 注册表阻断调用不存在的接口、method 写错或绕过统一 API client动态路由只能通过脚本 allowlist 明确声明。
- 修改学生端、租户后台或平台后台关键页面时必须运行 `node scripts/taro-persona-contract-test.js`。该脚本按学生刷题/会员订单/错题收藏、租户学生运营/内容导入/营销财务/品牌权限、平台租户/账务/公共题库/员工权限三类角色旅程检查路由和服务调用,避免前端样式重做时误删核心业务入口。
- H5 构建完成后必须运行 `npm run smoke:taro:h5:interaction` 做真实浏览器点击验证。它会覆盖学生首页到题库练习、答题、收藏、个人中心开通会员,租户后台工作台到题库内容/财务运营,以及平台后台工作台到租户管理/账务中心;如果 Chrome/Edge 缺失,可设置 `TARO_H5_SMOKE_BROWSER` 指向 Chromium 浏览器。
- 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、数据库、支付、短信、对象存储密钥放进该文件。
- 上线前需要把三套 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、对象存储、支付对账和真实 `@codex-security` 结果写入 `production-launch-evidence.json`,并通过 `npm run launch:gate`。当前环境没有暴露安全扫描工具时只能标记待补,不能把模板占位当完成。
- 上线前需要把三套 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`,用于运营、教师、销售、代理等自定义菜单/模块/字段可见性和成员模板绑定。
- 租户后台可以接入勋章管理、手动发放、积分任务、每日签到奖励、积分兑换、积分风控只读报表和反馈运营报表:`GET/PUT /api/tenant-admin/badges``GET/POST /api/tenant-admin/badge-grants``GET/PUT /api/tenant-admin/point-activity-tasks``GET /api/tenant-admin/point-activity-claims``GET/PUT /api/tenant-admin/point-exchange-items``GET /api/tenant-admin/point-exchange-orders``GET /api/tenant-admin/points-risk-report``GET /api/tenant-admin/feedbacks/report`;学生端用 `GET /api/profile/badges` 展示成就,通过 `POST /api/profile/check-in` 自动领取 `daily_check_in` 任务奖励,并通过 `GET /api/profile/activity-tasks``POST /api/profile/activity-tasks/claim``GET /api/profile/exchange-items``POST /api/profile/exchange-items/redeem` 接其它积分活动和兑换。

View File

@@ -91,10 +91,10 @@
- 生产 `.env` 模板和 `npm run readiness:production` / `npm run readiness:production:db` 已补,后续上云必须作为验收 gate。
- Auth/JWKS 上云后必须临时设置 `AUTH_SMOKE_*` 环境变量并运行 `npm run smoke:auth:remote`,真实 access token 不得写入仓库、前端配置或日志。
- 本地/预生产必须同时跑 `npm run test:rls`,它验证运行时 JWT claim 下的租户隔离,和 `readiness:production:db` 的静态 policy 检查互补。
- 已补 `npm run launch:gate` 生产上线证据门禁和 `docs/refactor/production-launch-evidence.template.json` 模板;最终切换前必须把 readiness、远程 Auth、RLS、PostgreSQL 4c16g 严格调参证据、生产 dry-run、导入校验、`pb:import:sample` 业务抽样、真实数据 API 读路径压测、API/worker/Taro、运行时审计、`@codex-security`、备份/回滚/真实抽样/生产 provider 等证据填入本地 `production-launch-evidence.json` 并通过门禁。当前 Codex 环境未暴露可调用的 `@codex-security` 扫描工具时,该项只能标为待补,不能伪造完成。
- 已补 `npm run launch:gate` 生产上线证据门禁和 `docs/refactor/production-launch-evidence.template.json` 模板;最终切换前必须把 readiness、远程 Auth、RLS、PostgreSQL 4c16g 严格调参证据、生产 dry-run、导入校验、`pb:import:sample` 业务抽样、真实数据 API 只读压测、真实数据混合读写压测、API/worker/Taro、运行时审计、`security:repo`、真实 `@codex-security`、备份/回滚/真实抽样/生产 provider 等证据填入本地 `production-launch-evidence.json` 并通过门禁。当前 Codex 环境未暴露可调用的 `@codex-security` 扫描工具时,该项只能标为待补,不能伪造完成。
- 确认数据库迁移流程、备份恢复、日志、告警。
- 准备 API 容器部署和 Supabase 云端/自托管连接方案。
- 已补 `npm run perf:api:local``npm run perf:summary``npm run perf:postgres:evidence``npm run perf:postgres:sql``npm run smoke:launch-persona``npm run smoke:taro:h5``npm run smoke:taro:h5:interaction``node scripts/taro-api-contract-test.js``node scripts/taro-persona-contract-test.js``docs/refactor/performance-benchmark-runbook.md`可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 4c16g profile 调参证据、三类后端角色旅程烟测、三类 Taro 前端角色旅程契约、H5 发布目录启动烟测、真实浏览器关键点击烟测和前端 API 契约检查。2026-07-01 03:58 受限 API 容器真实迁移库复核中30 worker/120s 只读为 44,038 请求、0 错误、366.27 req/s、P95 185.80ms、P99 254.08ms50 worker/60s/10% 写入为 25,327 请求、0 错误、419.62 req/s、P95 213.17ms、P99 270.94ms100 worker/60s/8% 写入为 25,378 请求、0 错误、417.77 req/s、P95 383.72ms、P99 461.22ms150 worker/60s/6% 写入为 23,444 请求、0 错误、384.39 req/s、P95 605.89ms、P99 743.19ms。压测抓到自动勋章并发发放唯一键冲突,已修复并新增 `scripts/auto-badge-concurrency-test.js`。当前 Docker Desktop 给了 20 CPU/约 62.7GB 内存,但 API 容器限制为 2 CPU/4G舒适观察区暂按 50 到 100 个无停顿 worker 估算150 worker 已是压力区;按单学生 0.05 到 0.2 req/s 粗略折算约为 2,000 到 8,400 名活跃在线学生的本机吞吐观察区间,正式容量仍以上云 4 核 16G 复测为准。
- 已补 `npm run perf:api:local``npm run perf:summary``npm run perf:postgres:evidence``npm run perf:postgres:sql``npm run smoke:launch-persona``npm run smoke:taro:h5``npm run smoke:taro:h5:interaction``node scripts/taro-api-contract-test.js``node scripts/taro-persona-contract-test.js``docs/refactor/performance-benchmark-runbook.md`可在本地或云端对真实迁移数据做只读门禁、混合读写容量观察、PostgreSQL 4c16g profile 调参证据、三类后端角色旅程烟测、三类 Taro 前端角色旅程契约、H5 发布目录启动烟测、真实浏览器关键点击烟测和前端 API 契约检查。2026-07-01 04:44 受限 API 容器真实迁移库复核中30 worker/120s 只读为 44,692 请求、0 错误、371.62 req/s、P95 188.91ms、P99 275.79ms50 worker/60s/10% 写入为 26,815 请求、0 错误、444.49 req/s、P95 204.08ms、P99 268.88ms100 worker/60s/8% 写入为 25,950 请求、0 错误、428.30 req/s、P95 375.38ms、P99 447.47ms150 worker/60s/6% 写入为 23,970 请求、0 错误、394.80 req/s、P95 574.37ms、P99 696.35ms。受限容器系列压测抓到自动勋章并发发放唯一键冲突,已修复并新增 `scripts/auto-badge-concurrency-test.js`。当前 Docker Desktop 给了 20 CPU/约 62.7GB 内存,但 API 容器限制为 2 CPU/4G舒适观察区暂按 50 到 100 个无停顿 worker 估算150 worker 已是压力区;按单学生 0.05 到 0.2 req/s 粗略折算约为 2,100 到 8,900 名活跃在线学生的本机吞吐观察区间,正式容量仍以上云 4 核 16G 复测为准。
- 当前本地 PostgreSQL evidence 仍提示 `jit=on``statement_timeout=0``idle_in_transaction_session_timeout=0``lock_timeout=0`;上云后必须按 `docs/refactor/postgresql-4c16g-tuning.md` 调整参数,执行 `PG_TUNING_PROFILE=shared-host npm run perf:postgres:evidence -- --strict --json` 并通过后,再按 6/30/50/100 阶梯并发复跑容量报告并归档到本地上线证据。
- 本轮剩余功能和容量复核已经整理到 `docs/refactor/backend-open-items-and-capacity-20260701.md`。后续不要再把学生头像上传或默认排行榜当作待办;头像只保留男女预设,排行榜仅作为租户显式开启后的活动能力。

View File

@@ -117,6 +117,22 @@
"includeWrites": false
}
},
{
"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",
"completedAt": "2026-06-30T10:49:00+08:00",
"artifact": "launch-artifacts/api-real-data-mixed-benchmark.log",
"summary": {
"errors": 0,
"errorRate": 0,
"p95Ms": 0,
"p99Ms": 0,
"concurrency": 50,
"durationSeconds": 120,
"includeWrites": true
}
},
{
"id": "api.integration",
"status": "pass",
@@ -247,12 +263,14 @@
"artifact": "launch-artifacts/taro-h5-interaction-smoke.json",
"summary": {
"fail": 0,
"pass": 11,
"pass": 26,
"mockApi": {
"keyRequests": {
"answers": 1,
"favorites": 2,
"tenantResolve": 3
"ordersCreated": 1,
"paymentsCreated": 1,
"orderStatus": 1
}
}
}
@@ -279,6 +297,17 @@
"high": 0
}
},
{
"id": "security.repo-scan",
"status": "pass",
"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",
"summary": {
"critical": 0,
"high": 0
}
},
{
"id": "security.codex-scan",
"status": "replace-with-pass-after-real-scan",

View File

@@ -73,9 +73,10 @@ npm run readiness:production:db
npm run smoke:auth:remote
npm run test:rls
npm run audit:runtime
npm run security:repo
```
正式上线前,三套 H5 严格发布证据、真实 Auth/RLS、真实 provider 抽样、对象存储控制、支付对账、PostgreSQL 严格调参证据和真实数据压测都要写入本地 `docs/refactor/production-launch-evidence.json`,再运行:
正式上线前,三套 H5 严格发布证据、真实 Auth/RLS、真实 provider 抽样、对象存储控制、支付对账、PostgreSQL 严格调参证据`security:repo` 和真实数据压测都要写入本地 `docs/refactor/production-launch-evidence.json`,再运行:
```bash
npm run launch:gate -- --evidence docs/refactor/production-launch-evidence.json

View File

@@ -78,6 +78,7 @@ npm run smoke:launch-persona
npm run test:readiness
npm run test:rls
npm run audit:runtime
npm run security:repo
npm run check:api
npm run check:worker
npm run check:taro
@@ -87,7 +88,7 @@ node scripts/taro-h5-release-guardrails-test.js --require-dist
npm run smoke:launch-persona
```
`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 点击事件问题;当前脚本覆盖 25 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心,租户后台六个主模块,以及平台后台四个主模块。`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 点击事件问题;当前脚本覆盖 26 项检查,包括学生首页、题库、答题、收藏、错题/收藏复习、背单词、知识手册、资料短签名和水印、视频播放授权、分数线、AI 择校、消息中心、会员收银台下单/支付参数/订单状态,租户后台六个主模块,以及平台后台四个主模块。`taro-h5-release-guardrails-test` 会扫描源码、三套 H5 产物和 runtime-config 边界,防止旧 PocketBase、`x-user-id``x-platform-admin-key`、数据库连接串和服务端密钥形态进入前端发布目录。若刚构建完但未放入真实 `runtime-config.json`,脚本允许 warning正式部署目录必须补齐。
写入生产上线证据时,三套正式发布目录必须先放入真实公开 `runtime-config.json`,再运行严格模式:
@@ -97,7 +98,7 @@ npm --silent run smoke:taro:h5:interaction -- --json > docs/refactor/launch-arti
node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config --json > docs/refactor/launch-artifacts/taro-h5-release-guardrails.json
```
`@codex-security` 插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。
`security:repo` 是仓库自带的静态安全扫描,会拦截密钥形态、前端旧鉴权头、真实 runtime-config 和生产证据误入 Git。它不能替代真实 `@codex-security`;如插件在当前 Codex 环境暴露扫描工具,再补插件扫描结果。若工具不可用,不能把该项标记为已完成,只能在上线证据里标记为待补。
生产 API 推荐:

View File

@@ -27,6 +27,7 @@
"check:refactor": "npm run check:api && npm run check:worker && npm run check:importer && npm run pb:import:validate && npm run test:readiness && npm run test:pb:dry-run && npm run test:api",
"audit:runtime": "npm audit --omit=dev --audit-level=high",
"audit:taro:toolchain": "npm audit --workspace @tiku-saas/taro --audit-level=high",
"security:repo": "node scripts/repo-security-scan.js",
"docker:api:build": "docker compose -f docker-compose.api.yml build",
"docker:api:up": "docker compose -f docker-compose.api.yml up api",
"docker:api:down": "docker compose -f docker-compose.api.yml down",
@@ -57,7 +58,7 @@
"test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js",
"test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js",
"test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js",
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js",
"test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/production-launch-gate-test.js",
"test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js",
"test:launch-gate": "node scripts/production-launch-gate-test.js",
"smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js",

View File

@@ -153,6 +153,29 @@ assert.ok(
'slow API benchmark should be reported as a blocker',
);
const missingMixedBenchmark = runGate(tempDir => {
const evidence = createEvidence(tempDir);
evidence.checks = evidence.checks.filter(item => item.id !== 'performance.api-real-data-mixed');
return evidence;
});
assert.notEqual(missingMixedBenchmark.status, 0, 'missing mixed read/write benchmark should fail launch gate');
assert.ok(
missingMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed' && item.status === 'blocker'),
'missing mixed benchmark should be reported as a blocker',
);
const slowMixedBenchmark = runGate(tempDir => {
const evidence = createEvidence(tempDir);
const item = evidence.checks.find(check => check.id === 'performance.api-real-data-mixed');
item.summary.p95Ms = 501;
return evidence;
});
assert.notEqual(slowMixedBenchmark.status, 0, 'slow mixed API benchmark should fail launch gate');
assert.ok(
slowMixedBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-mixed.summary' && item.status === 'blocker'),
'slow mixed benchmark should be reported as a blocker',
);
const missingBusinessSampling = runGate(tempDir => {
const evidence = createEvidence(tempDir);
evidence.checks = evidence.checks.filter(item => item.id !== 'migration.pb-import-sample');
@@ -186,6 +209,17 @@ assert.ok(
'missing H5 interaction smoke should be reported as a blocker',
);
const missingRepoSecurityScan = runGate(tempDir => {
const evidence = createEvidence(tempDir);
evidence.checks = evidence.checks.filter(item => item.id !== 'security.repo-scan');
return evidence;
});
assert.notEqual(missingRepoSecurityScan.status, 0, 'missing repository security scan should fail launch gate');
assert.ok(
missingRepoSecurityScan.payload.checks?.some(item => item.id === 'check.security.repo-scan' && item.status === 'blocker'),
'missing repository security scan should be reported as a blocker',
);
const missingAttestation = runGate(tempDir => {
const evidence = createEvidence(tempDir);
evidence.attestations = evidence.attestations.filter(item => item.id !== 'backup.snapshot');

View File

@@ -82,6 +82,20 @@ const gateChecks = [
includeWrites: false,
},
},
{
id: 'performance.api-real-data-mixed',
label: 'Real-data API mixed read/write benchmark',
commandIncludes: 'perf:api:local',
summary: {
errors: 0,
errorRate: { lte: 0.001 },
p95Ms: { lte: 500 },
p99Ms: { lte: 1200 },
concurrency: { gte: 50 },
durationSeconds: { gte: 60 },
includeWrites: true,
},
},
{
id: 'api.integration',
label: 'API integration regression',
@@ -152,10 +166,12 @@ const gateChecks = [
commandIncludes: 'smoke:taro:h5:interaction',
summary: {
fail: 0,
pass: { gte: 10 },
pass: { gte: 26 },
'mockApi.keyRequests.answers': { gte: 1 },
'mockApi.keyRequests.favorites': { gte: 1 },
'mockApi.keyRequests.tenantResolve': { gte: 3 },
'mockApi.keyRequests.ordersCreated': { gte: 1 },
'mockApi.keyRequests.paymentsCreated': { gte: 1 },
'mockApi.keyRequests.orderStatus': { gte: 1 },
},
},
{
@@ -173,6 +189,12 @@ const gateChecks = [
commandIncludes: 'audit:runtime',
summary: { critical: 0, high: 0 },
},
{
id: 'security.repo-scan',
label: 'Repository static security scan',
commandIncludes: 'security:repo',
summary: { critical: 0, high: 0 },
},
{
id: 'security.codex-scan',
label: 'Codex Security scan',

View File

@@ -0,0 +1,59 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const repoRoot = process.cwd();
const scriptPath = path.join(repoRoot, 'scripts', 'repo-security-scan.js');
function run(cwd) {
return spawnSync(process.execPath, [scriptPath, '--json'], {
cwd,
encoding: 'utf8',
env: {
PATH: process.env.PATH || '',
Path: process.env.Path || '',
SystemRoot: process.env.SystemRoot || '',
ComSpec: process.env.ComSpec || '',
TEMP: process.env.TEMP || os.tmpdir(),
TMP: process.env.TMP || os.tmpdir(),
},
});
}
const clean = run(repoRoot);
assert.equal(clean.status, 0, `current repository should pass security scan: ${clean.stdout} ${clean.stderr}`);
const cleanPayload = JSON.parse(clean.stdout);
assert.equal(cleanPayload.summary.critical, 0);
assert.equal(cleanPayload.summary.high, 0);
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-security-scan-'));
try {
fs.mkdirSync(path.join(tempDir, 'apps', 'taro', 'src'), { recursive: true });
fs.writeFileSync(
path.join(tempDir, 'apps', 'taro', 'src', 'unsafe.ts'),
"export const headers = { 'x-user-id': '123' };\n",
'utf8',
);
fs.writeFileSync(
path.join(tempDir, 'secret.env'),
[
'DATABASE_URL=postgresql://postgres:real-password@db.example.com:5432/postgres',
'SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake-service-role-token-that-should-not-ship',
'',
].join('\n'),
'utf8',
);
const unsafe = run(tempDir);
assert.notEqual(unsafe.status, 0, 'unsafe fixture should fail security scan');
const payload = JSON.parse(unsafe.stdout);
assert.ok(payload.findings.some(item => item.id === 'frontend-legacy-user-header'), 'x-user-id should be detected');
assert.ok(payload.findings.some(item => item.id === 'postgres-url'), 'database URL should be detected');
assert.ok(payload.findings.some(item => item.id === 'supabase-service-role'), 'service role key should be detected');
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
console.log('[PASS] repository security scan');

View File

@@ -0,0 +1,261 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const repoRoot = process.cwd();
const textExtensions = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.json',
'.md',
'.sql',
'.toml',
'.yml',
'.yaml',
'.html',
'.css',
'.scss',
'.env',
'.example',
]);
const ignoredSegments = new Set([
'.git',
'node_modules',
'dist',
'.swc',
'.temp',
'.branches',
'launch-artifacts',
'performance-reports',
'migration-reports',
'pb_export',
'pb_data',
'pb_public',
'参考',
'新UI参考',
'sao_live_0630',
'whisper_models',
]);
const allowlistedFiles = new Set([
'.env.example',
'README.md',
'docs/refactor/ai-development-guardrails.md',
'docs/refactor/api-structure.md',
'docs/refactor/auth-payment-provider-plan.md',
'docs/refactor/frontend-handoff-index.md',
'docs/refactor/multitenant-auth-security-contract.md',
'docs/refactor/object-storage-production-runbook.md',
'docs/refactor/object-storage.md',
'docs/refactor/production-launch-evidence.template.json',
'docs/refactor/supabase-frontend-access-strategy.md',
'docs/refactor/taro-frontend-integration.md',
'docs/refactor/taro-h5-deployment.md',
'docs/refactor/taro-production-integration-checklist.md',
'docs/refactor/web-launch-acceptance-checklist.md',
'scripts/production-readiness-check-test.js',
'scripts/repo-security-scan.js',
'scripts/taro-h5-release-guardrails-test.js',
]);
const ruleAllowlistedFiles = {
'postgres-url': new Set([
'scripts/production-config-failfast-test.js',
'scripts/repo-security-scan-test.js',
]),
'supabase-service-role': new Set([
'scripts/repo-security-scan-test.js',
'scripts/taro-runtime-config-test.js',
]),
'frontend-legacy-user-header': new Set([
'scripts/repo-security-scan-test.js',
]),
};
const rules = [
{
id: 'postgres-url',
severity: 'high',
pattern: /postgres(?:ql)?:\/\/[^\s"'`<>]+/i,
message: 'Database connection strings must not be committed outside approved examples/docs.',
validate: (match) => {
try {
const url = new URL(match[0]);
return !['127.0.0.1', 'localhost', 'host.docker.internal'].includes(url.hostname);
} catch {
return true;
}
},
},
{
id: 'private-key',
severity: 'critical',
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
message: 'Private keys must not be committed.',
},
{
id: 'supabase-service-role',
severity: 'critical',
pattern: /\b(?:SUPABASE_SERVICE_ROLE_KEY|SUPABASE_SECRET_KEY)\s*[:=]\s*["']?([A-Za-z0-9._-]{20,})/i,
message: 'Supabase service role or secret keys must never enter the repository.',
validate: (match) => {
const value = String(match[1] || '');
return /^eyJ/.test(value) || /^sb_secret_/.test(value) || value.length >= 80;
},
},
{
id: 'provider-secret-token',
severity: 'critical',
pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/,
message: 'Provider secret keys must not be committed.',
},
{
id: 'cloud-access-key',
severity: 'critical',
pattern: /\bAKIA[0-9A-Z]{16}\b/,
message: 'Cloud access keys must not be committed.',
},
{
id: 'wechat-pay-private-key',
severity: 'critical',
pattern: /\b(?:WECHAT_PAY_PRIVATE_KEY|ALIPAY_APP_PRIVATE_KEY)\s*[:=]\s*["']?(?!replace-|<|xxx|your-).{20,}/i,
message: 'Payment private keys must stay in backend secrets/KMS.',
},
{
id: 'frontend-legacy-user-header',
severity: 'high',
pathPattern: /^apps\/taro\/src\//,
pattern: /\bx-user-id\b/i,
message: 'Taro source must not use x-user-id; identity comes from Supabase JWT/session.',
},
{
id: 'frontend-platform-key',
severity: 'high',
pathPattern: /^apps\/taro\/src\//,
pattern: /\bx-platform-admin-key\b/i,
message: 'Taro source must not use platform admin key headers.',
},
{
id: 'frontend-pocketbase',
severity: 'high',
pathPattern: /^apps\/taro\/src\//,
pattern: /\bpocketbase\b/i,
message: 'Taro source must not depend on PocketBase.',
},
{
id: 'runtime-config-committed',
severity: 'high',
pathPattern: /(^|\/)runtime-config\.json$/,
pattern: /./,
message: 'Real H5 runtime-config.json files are deployment artifacts and must not be committed.',
},
{
id: 'production-launch-evidence-committed',
severity: 'high',
pathPattern: /^docs\/refactor\/production-launch-evidence\.json$/,
pattern: /./,
message: 'Real production launch evidence may contain internal evidence paths and must not be committed.',
},
];
function normalizeSlashes(value) {
return value.replace(/\\/g, '/');
}
function relative(filePath) {
return normalizeSlashes(path.relative(repoRoot, filePath));
}
function isIgnoredPath(filePath) {
const rel = relative(filePath);
return rel.split('/').some(segment => ignoredSegments.has(segment));
}
function shouldRead(filePath) {
if (isIgnoredPath(filePath)) return false;
const ext = path.extname(filePath).toLowerCase();
if (textExtensions.has(ext)) return true;
return ['Dockerfile', '.gitignore', '.dockerignore'].includes(path.basename(filePath));
}
function walk(dir) {
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (isIgnoredPath(fullPath)) continue;
if (entry.isDirectory()) result.push(...walk(fullPath));
else if (shouldRead(fullPath)) result.push(fullPath);
}
return result;
}
function getTrackedFiles() {
const result = spawnSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' });
if (result.status !== 0) return null;
return new Set(result.stdout.split(/\r?\n/).filter(Boolean).map(normalizeSlashes));
}
function scanFile(filePath, trackedFiles) {
const rel = relative(filePath);
const text = fs.readFileSync(filePath, 'utf8');
const findings = [];
const isTracked = !trackedFiles || trackedFiles.has(rel);
for (const rule of rules) {
if (rule.pathPattern && !rule.pathPattern.test(rel)) continue;
if (!rule.pathPattern && allowlistedFiles.has(rel)) continue;
if (ruleAllowlistedFiles[rule.id]?.has(rel)) continue;
const match = text.match(rule.pattern);
if (!match) continue;
if (rule.validate && !rule.validate(match, { file: rel, text })) continue;
if ((rule.id === 'runtime-config-committed' || rule.id === 'production-launch-evidence-committed') && !isTracked) continue;
findings.push({
id: rule.id,
severity: rule.severity,
file: rel,
message: rule.message,
});
}
return findings;
}
function summarize(findings) {
return findings.reduce(
(summary, item) => {
summary[item.severity] = (summary[item.severity] || 0) + 1;
return summary;
},
{ critical: 0, high: 0, medium: 0, low: 0 },
);
}
function main() {
const json = process.argv.includes('--json');
const trackedFiles = getTrackedFiles();
const files = walk(repoRoot);
const findings = files.flatMap(filePath => scanFile(filePath, trackedFiles));
const summary = {
...summarize(findings),
scannedFiles: files.length,
findings: findings.length,
};
const payload = { summary, findings };
if (json) {
console.log(JSON.stringify(payload, null, 2));
} else {
console.log(`Repository security scan: ${summary.findings} finding(s), ${summary.scannedFiles} file(s) scanned`);
for (const item of findings) {
console.log(`[${item.severity.toUpperCase()}] ${item.id} ${item.file}: ${item.message}`);
}
}
if (summary.critical > 0 || summary.high > 0) process.exitCode = 1;
}
main();

View File

@@ -1015,6 +1015,15 @@ async function runStudentJourney(browser, portal, api) {
await waitForText(page, '会员收银台');
checks.push({ id: 'student.profile.to_checkout', status: 'pass', detail: await currentPath(page) });
await clickText(page, '提交订单');
await waitForApiRequest(api, '/api/commerce/orders', 'POST');
await waitForApiRequest(api, '/api/commerce/payments/create', 'POST');
await waitForText(page, '支付参数已生成');
await waitForText(page, 'SMOKE202607010001');
await clickText(page, '刷新状态');
await waitForApiRequest(api, '/api/commerce/orders/status', 'GET');
checks.push({ id: 'student.checkout.order_payment', status: 'pass', detail: '收银台下单、支付参数生成和状态刷新 API 已触发' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/review/index?type=wrong', '错题本');
await waitForText(page, '开始复习');
await waitForText(page, '这是 H5 交互烟测题目');
@@ -1213,6 +1222,9 @@ async function main() {
keyRequests: {
answers: api.requests.filter(item => item.path === '/api/learning/answers').length,
favorites: api.requests.filter(item => item.path === '/api/learning/favorites/questions').length,
ordersCreated: api.requests.filter(item => item.path === '/api/commerce/orders' && item.method === 'POST').length,
paymentsCreated: api.requests.filter(item => item.path === '/api/commerce/payments/create' && item.method === 'POST').length,
orderStatus: api.requests.filter(item => item.path === '/api/commerce/orders/status' && item.method === 'GET').length,
tenantResolve: api.requests.filter(item => item.path === '/api/tenant/resolve').length,
},
},