refactor: 全面优化后端代码质量与架构
- app.ts 单体路由拆分为 routes/ + helpers + middleware + static 独立模块 - 类型去重:CheckFailure/CheckResult 以 shared/api.ts 为唯一源头,收紧 phase 联合类型 - es-toolkit 替换:isPlainObject/isNil/isEmptyObject/isEqual/isError/Semaphore/groupBy - Bun 内置 API:Object.fromEntries 替代手写 headersToRecord - bun:sqlite 规范:prepare() → query() 利用内置缓存,避免 N+1 查询 - 新增 getLatestChecksMap/allGetTargetStats 批量查询方法 - 新增 backend-code-quality/api-route-separation/batch-data-queries 规范 - 补充 openspec/config.yaml 后端开发规范与 DEVELOPMENT.md 后端开发指引
This commit is contained in:
158
DEVELOPMENT.md
158
DEVELOPMENT.md
@@ -9,10 +9,19 @@
|
||||
```text
|
||||
src/
|
||||
server/
|
||||
app.ts Bun HTTP 路由(API + 静态资源 + SPA fallback)
|
||||
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
|
||||
config.ts CLI 参数解析
|
||||
dev.ts 开发期启动入口
|
||||
server.ts HTTP server 启动
|
||||
helpers.ts 共享响应格式化工具(jsonResponse、createHeaders 等)
|
||||
middleware.ts API 参数校验中间件(guardGetHead、validateTargetId 等)
|
||||
static.ts 静态资源服务 与 SPA fallback
|
||||
routes/ API 路由 handler(按端点拆分)
|
||||
health.ts GET /health
|
||||
summary.ts GET /api/summary
|
||||
targets.ts GET /api/targets
|
||||
history.ts GET /api/targets/:id/history
|
||||
trend.ts GET /api/targets/:id/trend
|
||||
checker/
|
||||
types.ts 类型定义
|
||||
config-loader.ts YAML 配置解析与校验
|
||||
@@ -20,11 +29,11 @@ src/
|
||||
fetcher.ts HTTP 拨测执行
|
||||
command-runner.ts 命令行拨测执行
|
||||
size.ts 大小单位解析
|
||||
engine.ts 调度引擎(按 interval 分组、组内并发)
|
||||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
|
||||
expect/
|
||||
http.ts HTTP 响应断言
|
||||
command.ts 命令行输出断言
|
||||
body.ts HTTP body 断言(JSONPath/XPath/CSS)
|
||||
body.ts HTTP body 断言(JSONPath/XPath/CSS,类型判断使用 es-toolkit)
|
||||
failure.ts 失败信息类型
|
||||
shared/
|
||||
api.ts 前后端共享 TypeScript 类型
|
||||
@@ -82,6 +91,149 @@ bun run verify
|
||||
|
||||
前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
|
||||
|
||||
## 后端开发指引
|
||||
|
||||
### 架构概览
|
||||
|
||||
```
|
||||
启动流程:
|
||||
dev.ts → readRuntimeConfig(cli args) → loadConfig(yaml)
|
||||
→ ProbeStore(db) → ProbeEngine(store, targets) → startServer(store)
|
||||
|
||||
运行时:
|
||||
定时器(tick) → ProbeEngine.probeGroup()
|
||||
→ HTTP: fetcher.ts / Command: command-runner.ts
|
||||
→ expect/*.ts 校验 → store.insertCheckResult()
|
||||
|
||||
HTTP 请求:
|
||||
Request → app.ts(路由分发) → routes/*.ts(handler)
|
||||
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
|
||||
```
|
||||
|
||||
### 库使用优先级
|
||||
|
||||
后端代码开发遵循严格的库选择顺序:
|
||||
|
||||
| 优先级 | 来源 | 典型用途 |
|
||||
| ------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | Bun 内置 API | `Bun.serve`、`bun:sqlite`、`Bun.spawn`、`Bun.file`、`Bun.YAML` |
|
||||
| 2 | es-toolkit | 类型判断(`isPlainObject`/`isNil`/`isEmptyObject`)、深度比较(`isEqual`)、错误判断(`isError`)、并发控制(`Semaphore`)、集合操作(`groupBy`) |
|
||||
| 3 | 标准 Web API | `Object.fromEntries`、`Headers`、`fetch`、`AbortController` |
|
||||
| 4 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
|
||||
| 5 | 自行实现 | 仅在以上都无法满足时(如 `parseDuration`、`parseSize`、`evaluateJsonPath` 等专项逻辑) |
|
||||
|
||||
### API 路由开发
|
||||
|
||||
路由文件位于 `src/server/routes/`,每个端点一个文件。handler 函数签名统一为:
|
||||
|
||||
```typescript
|
||||
export function handleXxx(params, store: ProbeStore, method: string, mode: RuntimeMode): Response;
|
||||
```
|
||||
|
||||
**请求处理流程**:
|
||||
|
||||
1. `app.ts` 的 `createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
|
||||
2. API 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD)
|
||||
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination` 做参数校验
|
||||
4. 校验函数返回 `Response` 表示校验失败(直接返回),返回数据对象表示通过
|
||||
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts` 的 `jsonResponse`、`mapCheckResult`、`formatDuration` 等格式化输出
|
||||
|
||||
**新增路由步骤**:
|
||||
|
||||
1. 在 `src/server/routes/` 下创建 `<name>.ts`
|
||||
2. 实现 handler 函数并 export
|
||||
3. 在 `app.ts` 的 `handleApiRoute` 中注册路径匹配和调用
|
||||
4. 在 `tests/server/app.test.ts` 中添加对应测试
|
||||
|
||||
### 共享工具
|
||||
|
||||
- **`helpers.ts`**:跨路由共用的响应工具函数(`jsonResponse`、`createHeaders`、`createApiError`、`mapCheckResult`、`formatDuration`、`createHealthResponse`)
|
||||
- **`middleware.ts`**:API 参数校验函数(`guardGetHead`、`validateTargetId`、`validateTimeRange`、`validatePagination`)
|
||||
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
|
||||
|
||||
### 类型定义规范
|
||||
|
||||
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
|
||||
- 前端不得 `import src/server/` 下的任何文件
|
||||
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
|
||||
- **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
|
||||
- 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离
|
||||
|
||||
### 数据存储规范
|
||||
|
||||
基于 `bun:sqlite`,WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
|
||||
|
||||
**Statement 使用规范**:
|
||||
|
||||
| 场景 | 方式 | 原因 |
|
||||
| -------------- | -------------------------------------- | ---------------------------------------- |
|
||||
| 单次读/写 | `this.db.query(sql).get()/all()/run()` | bun:sqlite 内置 statement 缓存,自动复用 |
|
||||
| 事务内多次复用 | `this.db.prepare(sql)` 缓存为局部变量 | 事务闭包中需要持有引用 |
|
||||
|
||||
**查询优化**:
|
||||
|
||||
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合(GROUP BY、子查询 JOIN)+ 内存组装
|
||||
- 新增批量查询方法时必须编写对应单元测试
|
||||
- `getSummary()` 和 `GET /api/targets` 的响应组装已通过 `getLatestChecksMap` + `getAllTargetStats` 实现批量查询
|
||||
|
||||
**Schema**:
|
||||
|
||||
- `targets` 表:name(UNIQUE)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grp
|
||||
- `check_results` 表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)
|
||||
- 复合索引:`(target_id, timestamp)`
|
||||
|
||||
### 拨测引擎
|
||||
|
||||
- **调度**:`ProbeEngine` 用 `es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
|
||||
- **并发控制**:`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks`),`acquire()` 阻塞等待
|
||||
- **Runner 选择**:`engine.runCheck()` 按 `target.type` 分发到 `runHttpCheck` 或 `runCommandCheck`
|
||||
- **超时控制**:HTTP 用 `AbortController`,Command 用 `setTimeout` + `proc.kill()`
|
||||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `targetNameToId` 缓存 name→id 映射
|
||||
|
||||
### expect 断言系统
|
||||
|
||||
两层模型:**观测值收集** → **规则校验**。
|
||||
|
||||
**HTTP 校验流程**:
|
||||
|
||||
```
|
||||
runHttpCheck → 收集观测(statusCode/headers/body/durationMs)
|
||||
→ checkHttpExpect → status → duration → headers → body(可选)
|
||||
→ 首个失败即停止,返回 CheckFailure
|
||||
```
|
||||
|
||||
**Command 校验流程**:
|
||||
|
||||
```
|
||||
runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
→ checkCommandExpect → exitCode → duration → stdout → stderr
|
||||
→ 首个失败即停止
|
||||
```
|
||||
|
||||
**Body 规则类型**:
|
||||
|
||||
- `contains`:文本包含匹配
|
||||
- `regex`:正则表达式匹配
|
||||
- `json`:JSONPath 提取 + 操作符比较(使用 `es-toolkit/isPlainObject` 区分纯值和操作符)
|
||||
- `css`:cheerio CSS 选择器 + 操作符比较
|
||||
- `xpath`:XPath 节点提取 + 操作符比较
|
||||
|
||||
**操作符**:`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||
|
||||
### 错误模式
|
||||
|
||||
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404/405/503
|
||||
- **CheckFailure**:`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
|
||||
- **错误处理**:expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`
|
||||
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
|
||||
|
||||
### 测试规范
|
||||
|
||||
- 测试文件与源文件对应:`tests/server/checker/store.test.ts` ↔ `src/server/checker/store.ts`
|
||||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||||
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
||||
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
||||
|
||||
## 前端样式规范
|
||||
|
||||
前端基于 TDesign React 构建UI,样式开发遵循以下优先级(从高到低):
|
||||
|
||||
Reference in New Issue
Block a user