1
0

refactor: 后端架构加固 — 泛型化、批量查询、bootstrap 统一、路径修复与 pageSize 上限

- CheckerDefinition 泛型化,HTTP/Command checker 移除 resolved target 断言
- 新增 ProbeStore.getAllRecentSamples 消除 targets 路由 N+1 查询
- 统一 getAllTargetStats 与 getTargetStats 的 availability 精度
- Engine rejected 结果写入 internal error 记录,提升可观测性
- 新增 bootstrap.ts 统一 dev/production 启动序列
- dataDir 相对路径改为基于配置文件目录解析
- validatePagination 增加 pageSize 上限 200 校验
- 修复 ErrorBoundary override 标记
- 更新 README/DEVELOPMENT 文档,新增完整测试覆盖
This commit is contained in:
2026-05-13 18:15:46 +08:00
parent 6ea185315f
commit 147a2559ae
30 changed files with 930 additions and 129 deletions

View File

@@ -21,6 +21,7 @@
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义)
bootstrap.ts 后端统一启动引导loadConfig → store → engine → startServer → shutdown
config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 开发模式启动入口
server.ts HTTP server 启动工厂(接收 StartServerOptions
@@ -95,11 +96,13 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
```
启动流程:
dev.ts → readRuntimeConfig(cli args, 仅提取 configPath)
dev.ts / build entry → readRuntimeConfig(cli args, 仅提取 configPath)
→ bootstrap({ configPath, mode, staticAssets? })
→ loadConfig(yaml) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets }
→ ProbeStore(db) → store.syncTargets(targets)
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs)
→ startServer({ config, mode: "development", store })
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → engine.start()
→ startServer({ config, mode, store, staticAssets? })
→ 注册 SIGINT/SIGTERM shutdownengine.stop + store.close
运行时:
定时器(tick) → ProbeEngine.probeGroup()
@@ -148,7 +151,7 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob
1. `app.ts``createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts``allowsGetHead` 自行校验方法)
3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD返回 `null` 表示通过
4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验
4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验`pageSize` 最大值为 `200`
5. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
6. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
@@ -170,7 +173,7 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob
- `jsonResponse(body, options)` — JSON 响应构造(自动处理 HEAD 空体)
- `mapCheckResult(row)` — 数据库行转 API CheckResult
- `methodNotAllowedResponse(allow, mode)` — 构造 405 响应
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`,其中 `pageSize` 上限为 `200`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
### 1.5 类型定义规范
@@ -184,7 +187,9 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob
- `checker/types.ts` 定义 base interface`ResolvedTargetBase``RawTargetConfig``DefaultsConfig`),使用 index signature 支持扩展
- 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget``ResolvedCommandTarget`),满足 base interface 约束
- 中间层engine、store、config-loader只依赖 base interface不感知具体 checker 类型
- Checker 内部通过 `as` 类型断言将 base 窄化为具体类型
- `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>` 使用泛型约束 `resolve` 返回值以及 `execute``serialize` 的 target 参数
- checker 实现指定具体 `ResolvedXxxTarget` 类型中间层registry、engine、config-loader、store使用默认泛型参数完成类型擦除
- Checker 内部 `execute``serialize` 直接接收具体类型;`resolve` 输入仍是 `RawTargetConfig`,可在读取 checker 专属原始配置时做必要窄化
### 1.6 配置契约与校验
@@ -194,15 +199,15 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob
`ResolvedConfig` 包含以下字段:
| 字段 | 来源 | 默认值 |
| --------------------- | ----------------------------- | ----------- |
| `configDir` | 配置文件所在目录 | — |
| `dataDir` | `server.dataDir` | `./data` |
| `host` | `server.host` | `127.0.0.1` |
| `port` | `server.port` | `3000` |
| `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` |
| `retentionMs` | `runtime.retention` | `7d` |
| `targets` | `targets[]` 经 resolve 后 | — |
| 字段 | 来源 | 默认值 |
| --------------------- | -------------------------------------------------- | ---------------- |
| `configDir` | 配置文件所在目录 | — |
| `dataDir` | `server.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/data` |
| `host` | `server.host` | `127.0.0.1` |
| `port` | `server.port` | `3000` |
| `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` |
| `retentionMs` | `runtime.retention` | `7d` |
| `targets` | `targets[]` 经 resolve 后 | — |
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
@@ -412,18 +417,19 @@ TcpChecker implements Checker
**核心方法**
| 方法 | 用途 |
| ---------------------- | ---------------------------------------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets基于 name 做 upsert + delete 事务) |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targetsdefault 分组优先排序) |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
| `getAllTargetStats()` | 批量获取每个 target 的可用率统计GROUP BY 聚合) |
| `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total |
| `getTrend()` | 获取按小时聚合的趋势数据 |
| `getHistory()` | 分页查询历史记录 |
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
| 方法 | 用途 |
| ----------------------- | ---------------------------------------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets基于 name 做 upsert + delete 事务) |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targetsdefault 分组优先排序) |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
| `getAllTargetStats()` | 批量获取每个 target 的可用率统计GROUP BY 聚合) |
| `getAllRecentSamples()` | 批量获取每个 target 的最近 N 条采样window function |
| `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total |
| `getTrend()` | 获取按小时聚合的趋势数据 |
| `getHistory()` | 分页查询历史记录 |
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
**Statement 使用规范**
@@ -436,7 +442,7 @@ TcpChecker implements Checker
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合GROUP BY、子查询 JOIN+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
- `getSummary()``GET /api/targets` 的响应组装已通过 `getLatestChecksMap` + `getAllTargetStats` 实现批量查询
- `getSummary()``GET /api/targets` 的响应组装已通过 `getLatestChecksMap` + `getAllTargetStats` + `getAllRecentSamples` 实现批量查询
**Schema**
@@ -451,6 +457,7 @@ TcpChecker implements Checker
- **Runner 选择**`engine.runCheck()` 通过 `checkerRegistry.get(target.type)` 获取 checker并调用 `checker.execute(target, { signal })`
- **超时控制**`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abortchecker 必须使用 `CheckerContext.signal` 感知超时HTTP 将 signal 传给 `fetch()`Command 在 signal abort 时 `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **异常可观测**`probeGroup()``Promise.allSettled` 的 rejected 结果通过索引关联 target并写入 `phase:"internal"` 的失败记录
- **数据清理**:当 `retentionMs > 0`engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
- **生命周期**`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval`
@@ -815,7 +822,7 @@ bun run build
└── 导出 staticAssets: StaticAssets 对象
3. 生成 .build/server-entry.ts临时文件
└── import 后端入口模块 + staticAssets作为 Bun.build 入口
└── import bootstrap + staticAssets调用 production bootstrap作为 Bun.build 入口
4. Bun.build({ compile, minify, sourcemap: "linked" })
└── 输出dist/dial-server单文件可执行 binary