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:
@@ -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 shutdown(engine.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()` | 查询全部 targets(default 分组优先排序) |
|
||||
| `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()` | 查询全部 targets(default 分组优先排序) |
|
||||
| `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` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Command 在 signal abort 时 `proc.kill()`
|
||||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `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)
|
||||
|
||||
Reference in New Issue
Block a user