fastify-wrap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +980 -0
  2. package/dist/src/client/TestClient.d.ts +33 -0
  3. package/dist/src/client/TestClient.d.ts.map +1 -0
  4. package/dist/src/client/TestClient.js +359 -0
  5. package/dist/src/client/TestClient.js.map +1 -0
  6. package/dist/src/client/index.d.ts +4 -0
  7. package/dist/src/client/index.d.ts.map +1 -0
  8. package/dist/src/client/index.js +3 -0
  9. package/dist/src/client/index.js.map +1 -0
  10. package/dist/src/client/sign.d.ts +12 -0
  11. package/dist/src/client/sign.d.ts.map +1 -0
  12. package/dist/src/client/sign.js +44 -0
  13. package/dist/src/client/sign.js.map +1 -0
  14. package/dist/src/client/types.d.ts +41 -0
  15. package/dist/src/client/types.d.ts.map +1 -0
  16. package/dist/src/client/types.js +20 -0
  17. package/dist/src/client/types.js.map +1 -0
  18. package/dist/src/common/auth/AuthStrategy.d.ts +6 -0
  19. package/dist/src/common/auth/AuthStrategy.d.ts.map +1 -0
  20. package/dist/src/common/auth/AuthStrategy.js +2 -0
  21. package/dist/src/common/auth/AuthStrategy.js.map +1 -0
  22. package/dist/src/common/auth/JwtStrategy.d.ts +9 -0
  23. package/dist/src/common/auth/JwtStrategy.d.ts.map +1 -0
  24. package/dist/src/common/auth/JwtStrategy.js +34 -0
  25. package/dist/src/common/auth/JwtStrategy.js.map +1 -0
  26. package/dist/src/common/auth/NonceStore.d.ts +22 -0
  27. package/dist/src/common/auth/NonceStore.d.ts.map +1 -0
  28. package/dist/src/common/auth/NonceStore.js +27 -0
  29. package/dist/src/common/auth/NonceStore.js.map +1 -0
  30. package/dist/src/common/auth/SecretResolvers.d.ts +18 -0
  31. package/dist/src/common/auth/SecretResolvers.d.ts.map +1 -0
  32. package/dist/src/common/auth/SecretResolvers.js +30 -0
  33. package/dist/src/common/auth/SecretResolvers.js.map +1 -0
  34. package/dist/src/common/auth/SignatureStrategy.d.ts +23 -0
  35. package/dist/src/common/auth/SignatureStrategy.d.ts.map +1 -0
  36. package/dist/src/common/auth/SignatureStrategy.js +115 -0
  37. package/dist/src/common/auth/SignatureStrategy.js.map +1 -0
  38. package/dist/src/common/auth/index.d.ts +4 -0
  39. package/dist/src/common/auth/index.d.ts.map +1 -0
  40. package/dist/src/common/auth/index.js +3 -0
  41. package/dist/src/common/auth/index.js.map +1 -0
  42. package/dist/src/common/auth/signToken.d.ts +4 -0
  43. package/dist/src/common/auth/signToken.d.ts.map +1 -0
  44. package/dist/src/common/auth/signToken.js +11 -0
  45. package/dist/src/common/auth/signToken.js.map +1 -0
  46. package/dist/src/common/config/loader.d.ts +10 -0
  47. package/dist/src/common/config/loader.d.ts.map +1 -0
  48. package/dist/src/common/config/loader.js +20 -0
  49. package/dist/src/common/config/loader.js.map +1 -0
  50. package/dist/src/common/container/index.d.ts +7 -0
  51. package/dist/src/common/container/index.d.ts.map +1 -0
  52. package/dist/src/common/container/index.js +15 -0
  53. package/dist/src/common/container/index.js.map +1 -0
  54. package/dist/src/common/db/index.d.ts +19 -0
  55. package/dist/src/common/db/index.d.ts.map +1 -0
  56. package/dist/src/common/db/index.js +64 -0
  57. package/dist/src/common/db/index.js.map +1 -0
  58. package/dist/src/common/dto/PageResult.d.ts +12 -0
  59. package/dist/src/common/dto/PageResult.d.ts.map +1 -0
  60. package/dist/src/common/dto/PageResult.js +2 -0
  61. package/dist/src/common/dto/PageResult.js.map +1 -0
  62. package/dist/src/common/errorHandler.d.ts +3 -0
  63. package/dist/src/common/errorHandler.d.ts.map +1 -0
  64. package/dist/src/common/errorHandler.js +47 -0
  65. package/dist/src/common/errorHandler.js.map +1 -0
  66. package/dist/src/common/exceptions/AuthException.d.ts +5 -0
  67. package/dist/src/common/exceptions/AuthException.d.ts.map +1 -0
  68. package/dist/src/common/exceptions/AuthException.js +8 -0
  69. package/dist/src/common/exceptions/AuthException.js.map +1 -0
  70. package/dist/src/common/exceptions/BaseException.d.ts +4 -0
  71. package/dist/src/common/exceptions/BaseException.d.ts.map +1 -0
  72. package/dist/src/common/exceptions/BaseException.js +7 -0
  73. package/dist/src/common/exceptions/BaseException.js.map +1 -0
  74. package/dist/src/common/exceptions/BusinessException.d.ts +5 -0
  75. package/dist/src/common/exceptions/BusinessException.d.ts.map +1 -0
  76. package/dist/src/common/exceptions/BusinessException.js +8 -0
  77. package/dist/src/common/exceptions/BusinessException.js.map +1 -0
  78. package/dist/src/common/exceptions/NotFoundException.d.ts +5 -0
  79. package/dist/src/common/exceptions/NotFoundException.d.ts.map +1 -0
  80. package/dist/src/common/exceptions/NotFoundException.js +8 -0
  81. package/dist/src/common/exceptions/NotFoundException.js.map +1 -0
  82. package/dist/src/common/exceptions/ServiceException.d.ts +5 -0
  83. package/dist/src/common/exceptions/ServiceException.d.ts.map +1 -0
  84. package/dist/src/common/exceptions/ServiceException.js +8 -0
  85. package/dist/src/common/exceptions/ServiceException.js.map +1 -0
  86. package/dist/src/common/exceptions/index.d.ts +6 -0
  87. package/dist/src/common/exceptions/index.d.ts.map +1 -0
  88. package/dist/src/common/exceptions/index.js +6 -0
  89. package/dist/src/common/exceptions/index.js.map +1 -0
  90. package/dist/src/common/migrate/index.d.ts +3 -0
  91. package/dist/src/common/migrate/index.d.ts.map +1 -0
  92. package/dist/src/common/migrate/index.js +49 -0
  93. package/dist/src/common/migrate/index.js.map +1 -0
  94. package/dist/src/common/redis/index.d.ts +14 -0
  95. package/dist/src/common/redis/index.d.ts.map +1 -0
  96. package/dist/src/common/redis/index.js +41 -0
  97. package/dist/src/common/redis/index.js.map +1 -0
  98. package/dist/src/common/scanner/auth-factory.d.ts +42 -0
  99. package/dist/src/common/scanner/auth-factory.d.ts.map +1 -0
  100. package/dist/src/common/scanner/auth-factory.js +86 -0
  101. package/dist/src/common/scanner/auth-factory.js.map +1 -0
  102. package/dist/src/common/scanner/decorators.d.ts +123 -0
  103. package/dist/src/common/scanner/decorators.d.ts.map +1 -0
  104. package/dist/src/common/scanner/decorators.js +158 -0
  105. package/dist/src/common/scanner/decorators.js.map +1 -0
  106. package/dist/src/common/scanner/handler-factory.d.ts +38 -0
  107. package/dist/src/common/scanner/handler-factory.d.ts.map +1 -0
  108. package/dist/src/common/scanner/handler-factory.js +76 -0
  109. package/dist/src/common/scanner/handler-factory.js.map +1 -0
  110. package/dist/src/common/scanner/index.d.ts +16 -0
  111. package/dist/src/common/scanner/index.d.ts.map +1 -0
  112. package/dist/src/common/scanner/index.js +157 -0
  113. package/dist/src/common/scanner/index.js.map +1 -0
  114. package/dist/src/common/validation/decorators.d.ts +28 -0
  115. package/dist/src/common/validation/decorators.d.ts.map +1 -0
  116. package/dist/src/common/validation/decorators.js +70 -0
  117. package/dist/src/common/validation/decorators.js.map +1 -0
  118. package/dist/src/common/validation/errorRules.d.ts +5 -0
  119. package/dist/src/common/validation/errorRules.d.ts.map +1 -0
  120. package/dist/src/common/validation/errorRules.js +56 -0
  121. package/dist/src/common/validation/errorRules.js.map +1 -0
  122. package/dist/src/common/validation/index.d.ts +4 -0
  123. package/dist/src/common/validation/index.d.ts.map +1 -0
  124. package/dist/src/common/validation/index.js +3 -0
  125. package/dist/src/common/validation/index.js.map +1 -0
  126. package/dist/src/common/validation/schema.d.ts +35 -0
  127. package/dist/src/common/validation/schema.d.ts.map +1 -0
  128. package/dist/src/common/validation/schema.js +67 -0
  129. package/dist/src/common/validation/schema.js.map +1 -0
  130. package/dist/src/index.d.ts +26 -0
  131. package/dist/src/index.d.ts.map +1 -0
  132. package/dist/src/index.js +293 -0
  133. package/dist/src/index.js.map +1 -0
  134. package/dist/src/main.d.ts +2 -0
  135. package/dist/src/main.d.ts.map +1 -0
  136. package/dist/src/main.js +27 -0
  137. package/dist/src/main.js.map +1 -0
  138. package/dist/src/modules/health/controller/HealthController.d.ts +11 -0
  139. package/dist/src/modules/health/controller/HealthController.d.ts.map +1 -0
  140. package/dist/src/modules/health/controller/HealthController.js +31 -0
  141. package/dist/src/modules/health/controller/HealthController.js.map +1 -0
  142. package/dist/src/modules/health/service/HealthService.d.ts +4 -0
  143. package/dist/src/modules/health/service/HealthService.d.ts.map +1 -0
  144. package/dist/src/modules/health/service/HealthService.js +6 -0
  145. package/dist/src/modules/health/service/HealthService.js.map +1 -0
  146. package/dist/src/types.d.ts +75 -0
  147. package/dist/src/types.d.ts.map +1 -0
  148. package/dist/src/types.js +2 -0
  149. package/dist/src/types.js.map +1 -0
  150. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,980 @@
1
+ # fastify-api
2
+
3
+ Fastify 之上的一层薄封装。开发者只需写 controller / service / dao,路由注册、JWT 鉴权、JSON Schema 校验、统一异常处理、DB 连接池全部自动处理。
4
+
5
+ **核心理念:极简。不包装 Happy Path,不做 `{ code, data, message }` 统一响应格式。Controller 返回什么就是什么,异常由框架统一捕获。**
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install fastify-wrap
11
+ ```
12
+
13
+ ## 快速开始
14
+
15
+ ### 1. 入口文件
16
+
17
+ ```typescript
18
+ // src/main.ts
19
+ import { createApp } from 'fastify-wrap';
20
+ import config from './config';
21
+
22
+ const app = await createApp(config);
23
+ await app.listen({ port: 3000 });
24
+ ```
25
+
26
+ ```typescript
27
+ // src/config.ts
28
+ import type { AppConfig } from 'fastify-wrap';
29
+ import { createDbSecretResolver } from 'fastify-wrap';
30
+
31
+ const config: AppConfig = {
32
+ db: {
33
+ host: '127.0.0.1',
34
+ port: 3306,
35
+ user: 'root',
36
+ password: 'root',
37
+ database: 'mydb',
38
+ },
39
+ auth: {
40
+ jwtSecret: 'your-secret',
41
+ },
42
+ signature: {
43
+ resolveSecret: createDbSecretResolver(),
44
+ },
45
+ };
46
+
47
+ export default config;
48
+ ```
49
+
50
+ 多环境部署详见[多环境配置](#多环境配置)。
51
+
52
+ ### 2. 写 Controller
53
+
54
+ ```typescript
55
+ // src/modules/user/controller/UserController.ts
56
+ import { Controller, Get, Post, Public, Login, Valid } from 'fastify-wrap';
57
+ import type { FastifyRequest } from 'fastify';
58
+
59
+ @Controller('/api/users')
60
+ export class UserController {
61
+
62
+ @Post('/')
63
+ @Public()
64
+ @Valid({
65
+ body: {
66
+ type: 'object',
67
+ required: ['name'],
68
+ properties: { name: { type: 'string', minLength: 1 } },
69
+ },
70
+ })
71
+ async create(request: FastifyRequest) {
72
+ // request.body 已通过 JSON Schema 校验
73
+ return { id: '1', name: request.body.name };
74
+ }
75
+
76
+ @Get('/me')
77
+ @Login()
78
+ async profile(request: FastifyRequest) {
79
+ // request.user 已注入(JWT 鉴权通过后)
80
+ return request.user;
81
+ }
82
+
83
+ @Get('/admin')
84
+ @Login('admin')
85
+ async adminOnly() {
86
+ return { secret: 42 };
87
+ }
88
+ }
89
+
90
+ // Controller 只需 request,无需 reply:
91
+ // - 返回值自动序列化为 JSON
92
+ // - JWT 签名用 signToken() 全局函数
93
+ // - 文件下载用 @Stream 装饰器
94
+ ```
95
+
96
+ 文件放到约定目录即自动发现,无需手动注册路由。
97
+
98
+ ### 3. RPC 风格 Controller
99
+
100
+ 加 `@Rpc(name)` 将类切换为 RPC 风格。无需 `@Controller`,无需在每个方法上加 `@Post`——框架自动扫描所有 public 方法,URL 由模块目录 + 控制器名 + 方法名自动推导。
101
+
102
+ ```typescript
103
+ import { Rpc, Login, Body, signToken } from 'fastify-wrap';
104
+ import type { User } from 'fastify-wrap';
105
+ import { Type } from '@sinclair/typebox';
106
+
107
+ @Rpc('users') // 控制器名,URL: /{module}/users/*
108
+ @Login() // 类级别:所有方法默认需要 JWT(可被方法级 @Login/@Public 覆盖)
109
+ export class UsersController {
110
+
111
+ // POST /{module}/users/create
112
+ @Body(Type.Object({ name: Type.String() }))
113
+ @Response('创建用户', Type.Object({ id: Type.String() }))
114
+ create(body: { name: string }, user: User): { id: string } {
115
+ return { id: 'u_001' };
116
+ }
117
+
118
+ // POST /{module}/users/login — public 端点,覆盖类级 @Login
119
+ @Public()
120
+ @Body(Type.Object({ code: Type.String() }))
121
+ @Response('登录', Type.Object({ token: Type.String() }))
122
+ async login(body: { code: string }): Promise<{ token: string }> {
123
+ const token = await signToken({ id: 'openid_xxx' });
124
+ return { token };
125
+ }
126
+
127
+ // POST /{module}/users/profile — 继承类级 @Login
128
+ @Response('获取用户信息', Type.Object({ id: Type.String() }))
129
+ profile(_body: Record<string, never>, user: User): { id: string } {
130
+ return { id: user.id };
131
+ }
132
+ }
133
+ ```
134
+
135
+ RPC 方法签名 `(body, user)`:第一个参数是已校验的请求体,第二个参数是 `request.user`。
136
+
137
+ 路由推导规则:
138
+
139
+ | 来源 | 推导逻辑 |
140
+ |------|----------|
141
+ | module | 模块文件夹名,可通过 `route.config.ts` 覆盖 |
142
+ | controller | `@Rpc(name)` 参数,默认为类名 |
143
+ | method | 方法名,保持不变 |
144
+
145
+ ```typescript
146
+ // src/modules/user/route.config.ts
147
+ export const module = 'u';
148
+ // → URL: /u/{controller}/{method}
149
+ ```
150
+
151
+ **`urlPrefix` 是完整的路径前缀段,支持多级路径。** 可以包含 `/` 构造嵌套路由:
152
+
153
+ ```typescript
154
+ // src/modules/user-rpc/route.config.ts
155
+ export const urlPrefix = 'api/urpc';
156
+ // → URL: /api/urpc/{controller}/{method}
157
+ ```
158
+
159
+ 不配置时 URL 前缀默认使用模块目录名。仅在 `@Rpc` 控制器中生效,`@Controller` 不受影响。
160
+
161
+ **模块级 token 隔离**:`route.config.ts` 可配置 `module`,校验 token 的 `user.type`,防止跨模块 token 滥用:
162
+
163
+ ```typescript
164
+ // src/modules/admin/route.config.ts
165
+ export const urlPrefix = 'api/admin'; // URL 路径(可选,默认使用模块目录名)
166
+ export const module = 'admin'; // 模块归属,所有 @Rpc 路由要求 user.type === 'admin'
167
+ ```
168
+
169
+ `@Login()` 的参数与模块配置的 `module` 是同一概念——两者都声明"此路由需要什么系统的 token",区别仅在于作用范围。优先级:**方法级 > 类级 > 模块级**:
170
+
171
+ | 配置 | 校验 | 作用范围 |
172
+ |------|------|----------|
173
+ | `@Login('admin')`(方法上) | HMAC + JWT + `user.type === 'admin'` | 单个方法 |
174
+ | `@Login('admin')`(类上) | 同上 | 整个 Controller,可被方法级覆盖 |
175
+ | 模块 `module:'admin'` | 同上 | 整个模块,可被类/方法级覆盖 |
176
+ | `@Login()`(无参数) | HMAC + JWT(不校验 type) | 方法/类 |
177
+ | `@Public()` | 跳过所有 | 单个方法 |
178
+
179
+ | 限制 | 说明 |
180
+ |------|------|
181
+ | HTTP 方法 | 全部使用 `@Post`,不可更改 |
182
+ | 参数 | 只用 `@Body`,不支持 `@Query`/`@Params` |
183
+ | URL | 不支持路径参数 `:id` |
184
+
185
+ ### 4. contextPath
186
+
187
+ 部署在反向代理子路径下时,配置 `contextPath`,Controller 代码无需变动:
188
+
189
+ ```typescript
190
+ const app = await createApp({
191
+ db: { ... },
192
+ contextPath: '/myapp',
193
+ });
194
+ // @Controller('/api/users') → 响应地址:/myapp/api/users
195
+ ```
196
+
197
+ 搭配 nginx:
198
+
199
+ ```nginx
200
+ location /myapp/ {
201
+ proxy_pass http://127.0.0.1:3000;
202
+ }
203
+ ```
204
+
205
+ ## 配置项
206
+
207
+ | 字段 | 类型 | 必填 | 说明 |
208
+ |------|------|------|------|
209
+ | `db` | `object` | 是 | MySQL 连接(host / port / user / password / database / connectionLimit) |
210
+ | `auth.jwtSecret` | `string` | 否 | JWT 密钥,不设则跳过鉴权 |
211
+ | `signature` | `object` | 否 | HMAC 签名鉴权配置(resolveSecret / timestampWindowMs / nonceStore) |
212
+ | `redis` | `object` | 否 | Redis 连接(host / port / password / db),配置后签名 nonce 自动切换 Redis 存储 |
213
+ | `contextPath` | `string` | 否 | 路径前缀,如 `/myapp`,Controller 代码无需变动 |
214
+ | `swagger` | `object` | 否 | Swagger / OpenAPI 文档配置(title / description / version / uiPrefix) |
215
+ | `migrate` | `boolean` | 否 | 启动时执行 `src/migrations/*.sql`,默认 false |
216
+
217
+ ## 多环境配置
218
+
219
+ 源码保留 3 份环境配置文件作为参考,构建时只复制目标那份为 `config.ts` 参与编译。
220
+
221
+ ### 目录结构
222
+
223
+ ```
224
+ src/
225
+ ├── main.ts
226
+ ├── config.ts ← 构建时由脚本写入(.gitignore)
227
+ └── config/
228
+ ├── development.ts ← 源码参考:本地开发
229
+ ├── test.ts ← 源码参考:测试环境
230
+ └── production.ts ← 源码参考:生产环境
231
+ ```
232
+
233
+ ### 构建脚本
234
+
235
+ ```json
236
+ {
237
+ "scripts": {
238
+ "build:dev": "copy src\\config\\development.ts src\\config.ts >nul 2>&1 & npx tsc",
239
+ "build:test": "copy src\\config\\test.ts src\\config.ts >nul 2>&1 & npx tsc",
240
+ "build:prod": "copy src\\config\\production.ts src\\config.ts >nul 2>&1 & npx tsc"
241
+ }
242
+ }
243
+ ```
244
+
245
+ ### 配置文件写法
246
+
247
+ ```typescript
248
+ // src/config/production.ts
249
+ import type { AppConfig } from 'fastify-wrap';
250
+
251
+ const config: AppConfig = {
252
+ db: {
253
+ host: process.env.DB_HOST!, // 敏感信息从环境变量注入
254
+ port: Number(process.env.DB_PORT),
255
+ user: process.env.DB_USER!,
256
+ password: process.env.DB_PASSWORD!,
257
+ database: process.env.DB_NAME!,
258
+ connectionLimit: 20, // 生产环境提高连接数
259
+ },
260
+ auth: {
261
+ jwtSecret: process.env.JWT_SECRET!,
262
+ },
263
+ swagger: false, // 生产环境关闭文档
264
+ migrate: false, // 生产环境不在应用进程内执行迁移
265
+ };
266
+
267
+ export default config;
268
+ ```
269
+
270
+ ### 入口文件
271
+
272
+ ```typescript
273
+ // src/main.ts
274
+ import { createApp } from 'fastify-wrap';
275
+ import config from './config';
276
+
277
+ const app = await createApp(config);
278
+ await app.listen({ port: 3000 });
279
+ ```
280
+
281
+ ### Docker 部署
282
+
283
+ ```dockerfile
284
+ FROM node:24-alpine
285
+ WORKDIR /app
286
+ COPY . .
287
+ RUN npm install && npm run build:prod
288
+ CMD ["node", "dist/main.js"]
289
+ ```
290
+
291
+ 敏感信息通过环境变量注入,不写在配置文件里:
292
+
293
+ ```bash
294
+ # 测试环境(生产用 build:prod,方法同上)
295
+ docker run -d \
296
+ -e DB_HOST=192.168.1.10 \
297
+ -e DB_PORT=3306 \
298
+ -e DB_USER=app \
299
+ -e DB_PASSWORD=test123 \
300
+ -e DB_NAME=mydb_test \
301
+ -e JWT_SECRET=test-secret \
302
+ my-app:latest
303
+ ```
304
+
305
+ ## 项目结构
306
+
307
+ ```
308
+ src/
309
+ ├── main.ts
310
+ └── modules/
311
+ └── user/
312
+ ├── controller/UserController.ts ← @Controller/@Get/@Post 声明路由
313
+ ├── service/UserService.ts ← 纯业务流程编排
314
+ ├── dao/
315
+ │ ├── UserDao.ts ← db.query/db.queryOne/db.execute
316
+ │ └── UserDao.sql.ts ← SQL 常量(禁止内联 SQL)
317
+ ├── dto/
318
+ │ ├── CreateUserRequest.ts
319
+ │ └── UserResponse.ts
320
+ ├── entity/UserEntity.ts
321
+ └── enums/UserStatus.ts
322
+ ```
323
+
324
+ ## 装饰器
325
+
326
+ | 装饰器 | 作用 |
327
+ |--------|------|
328
+ | `@Controller('/prefix')` | 声明路由前缀,类必须 `export` |
329
+ | `@Get('/path')` | GET 路由 |
330
+ | `@Post('/path')` | POST 路由 |
331
+ | `@Put('/path')` | PUT 路由 |
332
+ | `@Delete('/path')` | DELETE 路由 |
333
+ | `@Login()` | 需要 JWT + 签名验证。支持类级别(作用于所有方法)和方法级别(覆盖类级设置) |
334
+ | `@Login('admin')` | 需要 JWT + 校验 `user.type === 'admin'` + 签名验证。支持类级别和方法级别 |
335
+ | `@Public()` | 跳过签名验证(公开接口) |
336
+ | `@Valid(schema)` | 绑定 Fastify JSON Schema 校验 |
337
+ | `@Body(def)` | 声明式请求体校验 |
338
+ | `@Query(def)` | 声明式查询参数校验 |
339
+ | `@Params(def)` | 声明式路径参数校验 |
340
+ | `@Response(description, def, statusCode?)` | 声明响应 Schema,用于 OpenAPI 文档生成,默认 200 |
341
+ | `@Rpc(name?)` | RPC 风格控制器。无需 `@Controller`,方法自动扫描,签名 `(body, user)`。`name` 覆盖控制器名,默认类名。 |
342
+ | `@Stream(description?)` | 流式下载端点,方法返回 `StreamResponse { name, content }`。`description` 注入 OpenAPI 响应描述。 |
343
+ | `@File(description?)` | 文件下载端点,方法返回 `FileResponse { name, path }`。`description` 注入 OpenAPI 响应描述。 |
344
+ | `@Multipart()` | 标记 multipart/form-data 文件上传端点。替代 `@Body`(输入层),仍需 `@Response`(输出层)。 |
345
+
346
+ ### 声明式校验
347
+
348
+ 比手写 JSON Schema 更简洁的类型工具:
349
+
350
+ ```typescript
351
+ import {
352
+ Body, Query, Params,
353
+ String, Number, Boolean, Email, Required, ArrayOf, Enum, Min, Max,
354
+ } from 'fastify-wrap';
355
+
356
+ @Controller('/api/users')
357
+ class UserController {
358
+
359
+ @Post('/')
360
+ @Body({
361
+ name: Required(String),
362
+ email: Required(Email),
363
+ age: Number,
364
+ tags: ArrayOf(String),
365
+ status: Enum(['active', 'inactive']),
366
+ })
367
+ create(request: FastifyRequest) {
368
+ // request.body 已校验
369
+ }
370
+
371
+ @Get('/')
372
+ @Query({ page: Number, keyword: String })
373
+ list(request: FastifyRequest) { ... }
374
+
375
+ @Get('/:id')
376
+ @Params({ id: Required(String) })
377
+ show(request: FastifyRequest) { ... }
378
+ }
379
+ ```
380
+
381
+ `@Body`/`@Query`/`@Params` 自动生成标准 JSON Schema,与 `@Valid` 等价但可读性更高。
382
+
383
+ ## Swagger / OpenAPI
384
+
385
+ 配置 `swagger` 即可自动生成 OpenAPI 文档,路由、参数、Schema 全部自动提取:
386
+
387
+ ```typescript
388
+ const app = await createApp({
389
+ db: { ... },
390
+ swagger: {
391
+ title: 'My API',
392
+ description: 'API 文档',
393
+ version: '1.0.0', // 可选,默认读取 package.json version
394
+ uiPrefix: '/docs', // 可选,默认 /docs
395
+ },
396
+ });
397
+ ```
398
+
399
+ 启动后访问 `/docs` 即可浏览 Swagger UI,`/docs/json` 获取 OpenAPI JSON。
400
+
401
+ 配置 `contextPath` 时自动联动,例如 `contextPath: '/myapp'` → Swagger UI 在 `/myapp/docs`。
402
+
403
+ 依赖自动补充 `@fastify/swagger` + `@fastify/swagger-ui`。
404
+
405
+ ## 统一响应
406
+
407
+ **Happy path**:Controller `return data`,原样输出 JSON,不做包装。
408
+
409
+ ```json
410
+ { "id": "1", "name": "张三" }
411
+ ```
412
+
413
+ **Error path**:自动捕获异常,统一格式:
414
+
415
+ ```
416
+ HTTP 401 ← JWT 鉴权失败 / HMAC 签名失败
417
+ HTTP 403 ← 角色权限不足
418
+ HTTP 404 ← 路由不存在
419
+ HTTP 422 ← BusinessException、NotFoundException、Schema 校验失败
420
+ HTTP 500 ← ServiceException、未预期异常(message 通用化,不暴露堆栈)
421
+ ```
422
+
423
+ 业务/校验异常:
424
+ ```json
425
+ { "error": "手机号已存在" }
426
+ ```
427
+
428
+ 鉴权异常:
429
+ ```json
430
+ { "code": "login", "error": "Authorization token is invalid" }
431
+ { "code": "error", "error": "签名验证失败" }
432
+ { "code": "retry", "error": "凭证无效或已过期" }
433
+ ```
434
+
435
+ ## 异常
436
+
437
+ ```typescript
438
+ import { BusinessException, NotFoundException, ServiceException } from 'fastify-wrap';
439
+
440
+ throw new BusinessException('库存不足'); // → 422 { error: '库存不足' }
441
+ throw new NotFoundException('用户不存在'); // → 422 { error: '用户不存在' }
442
+ throw new ServiceException('推送失败'); // → 500 { error: '服务器内部错误' }
443
+ ```
444
+
445
+ ## API 签名鉴权
446
+
447
+ 所有接口默认需要 HMAC 签名验证。`@Public()` 标记的接口跳过签名,`@Login()` 标记的接口在签名基础上额外要求 JWT 登录。
448
+
449
+ 签名验证按 `module_name` 隔离——admin 的 clientId 无法调用其他模块的接口。
450
+
451
+ ### 鉴权模型
452
+
453
+ ```
454
+ 1. 线下派发 clientId + secretKey
455
+ 2. 客户端每次请求计算 HMAC-SHA256 签名
456
+ 3. 服务端验证时间戳、nonce、签名,按 module_name 过滤
457
+ ```
458
+
459
+ ### 客户端签名算法
460
+
461
+ ```
462
+ stringToSign = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + BODY
463
+ signature = base64(HMAC-SHA256(secretKey, stringToSign))
464
+ ```
465
+
466
+ 请求头:
467
+
468
+ | Header | 说明 |
469
+ |--------|------|
470
+ | `X-App-Key` | 客户端 ID |
471
+ | `X-Timestamp` | 毫秒时间戳 |
472
+ | `X-Nonce` | 随机 UUID |
473
+ | `X-Signature` | 签名结果 |
474
+
475
+ ### 服务端配置
476
+
477
+ 框架内置两种 `SecretResolver` 驱动:
478
+
479
+ ```typescript
480
+ import { createDbSecretResolver, createRedisSecretResolver } from 'fastify-wrap';
481
+
482
+ // DB 驱动 — 从 MySQL 的 api_keys 表查签名密钥
483
+ createDbSecretResolver();
484
+
485
+ // Redis 驱动 — 从 Redis 查 clientId,值存 JSON
486
+ // Redis key = `${keyPrefix}${moduleName}:${clientId}`(不同模块隔离)
487
+ createRedisSecretResolver({
488
+ keyPrefix: 'client:secret:', // 默认 'client:secret:'
489
+ });
490
+ ```
491
+
492
+ 配置示例:
493
+
494
+ ```typescript
495
+ const app = await createApp({
496
+ db: { ... },
497
+ signature: {
498
+ resolveSecret: createDbSecretResolver(),
499
+ timestampWindowMs: 5 * 60 * 1000, // 可选,默认 5 分钟
500
+ },
501
+ });
502
+ ```
503
+
504
+ 也可自定义 `resolveSecret` 函数对接任意存储。
505
+
506
+ ### 路由使用
507
+
508
+ 默认所有接口需签名,`request.user.id` 为 clientId:
509
+
510
+ ```typescript
511
+ @Controller('/api/app')
512
+ export class AppController {
513
+
514
+ @Get('/data')
515
+ async data(request: FastifyRequest) {
516
+ return { clientId: request.user.id };
517
+ }
518
+
519
+ @Get('/profile')
520
+ @Login()
521
+ async profile(request: FastifyRequest) {
522
+ // 签名通过 + JWT 登录通过 → request.user 为 JWT 用户
523
+ return request.user;
524
+ }
525
+ }
526
+ ```
527
+
528
+ 签名失败统一返回 `401 { code: 'error' | 'retry', error: string }`:
529
+
530
+ - `code: 'error'` — 不可重试(签名不匹配、缺少参数、timestamp/nonce 异常)
531
+ - `code: 'retry'` — 凭证无效或过期,客户端刷新凭证后重试
532
+
533
+ 服务端日志会详细记录失败原因和客户端数据。支持 nonce 防重放(内存或 Redis,自动切换)、timestamp 时间窗口校验、constant-time 签名比对。
534
+
535
+ ### 文件上传
536
+
537
+ 上传时签名逻辑不变:将 JSON 元数据签名后以 `_sign_body` 字段嵌入 `multipart/form-data`,文件以 `file` 字段附带:
538
+
539
+ ```typescript
540
+ @Controller('/api/app')
541
+ class AppController {
542
+
543
+ @Post('/upload')
544
+ async upload(request: FastifyRequest) {
545
+ const body = request.body as Record<string, unknown>;
546
+ // body.file → { type: 'file', fieldname, filename, mimetype, file, _buf }
547
+ // body._sign_body → { type: 'field', value: '{"fileName":"a.png",...}' }
548
+ }
549
+ }
550
+ ```
551
+
552
+ ### 文件下载
553
+
554
+ ```typescript
555
+ import { Controller, Get, Stream } from 'fastify-wrap';
556
+ import type { StreamResponse } from 'fastify-wrap';
557
+
558
+ @Controller('/api/report')
559
+ class ReportController {
560
+
561
+ @Get('/export')
562
+ @Stream('导出CSV报表')
563
+ async exportCSV(): Promise<StreamResponse> {
564
+ const csv = ['name,phone', '张三,138xxxx'].join('\n');
565
+ return { name: 'report.csv', content: csv };
566
+ }
567
+ }
568
+ ```
569
+
570
+ Controller 只需返回 `{ name, content }`,框架自动设置 `Content-Disposition` 并流式返回。无需 `reply` 参数。
571
+
572
+ `content` 支持 `string` 和 `Buffer`,适用 CSV / Excel / 图片等任意二进制数据。
573
+
574
+ ## JWT 工具
575
+
576
+ ### signToken — 全局 JWT 签名
577
+
578
+ ```typescript
579
+ import { signToken } from 'fastify-wrap';
580
+
581
+ const token = await signToken({ id: 1, role: 'admin', type: 'admin' });
582
+ ```
583
+
584
+ 全局可用,无需传递 `reply` 对象。在 `createApp()` 初始化 JWT 插件后自动生效。
585
+
586
+ ## DB 工具
587
+
588
+ ```typescript
589
+ import { db } from 'fastify-wrap';
590
+
591
+ // 查多行
592
+ const users = await db.query<UserRow>('SELECT * FROM users WHERE status = ?', [0]);
593
+
594
+ // 查单行
595
+ const user = await db.queryOne<UserRow>('SELECT * FROM users WHERE id = ?', [id]);
596
+
597
+ // 增删改
598
+ await db.execute('UPDATE users SET name = ? WHERE id = ?', ['李四', id]);
599
+ ```
600
+
601
+ 无需手动 `getConnection` / `release`,自动管理连接池。
602
+
603
+ ### 事务
604
+
605
+ ```typescript
606
+ import { db, Transaction } from 'fastify-wrap';
607
+
608
+ await db.transaction(async (tx: Transaction) => {
609
+ await tx.execute('UPDATE account SET balance = balance - 100 WHERE id = ?', [1]);
610
+ await tx.execute('UPDATE account SET balance = balance + 100 WHERE id = ?', [2]);
611
+ });
612
+ // 成功自动 commit,异常自动 rollback + release
613
+ ```
614
+
615
+ 事务内使用 `tx.query` / `tx.queryOne` / `tx.execute`,接口与 `db` 一致。
616
+
617
+ ## Redis 工具
618
+
619
+ ```typescript
620
+ import { redis } from 'fastify-wrap';
621
+
622
+ await redis.set('key', 'value', 60); // 带 TTL(秒)
623
+ const val = await redis.get('key'); // → 'value' | null
624
+ await redis.del('key');
625
+ const exists = await redis.exists('key'); // → boolean
626
+
627
+ // 原子 SET NX EX(用于分布式锁/防重放)
628
+ const ok = await redis.setnx('lock:order:123', '1', 30);
629
+ ```
630
+
631
+ 配置:
632
+
633
+ ```typescript
634
+ createApp({
635
+ db: { ... },
636
+ redis: {
637
+ host: '127.0.0.1',
638
+ port: 6379,
639
+ password: 'optional',
640
+ db: 0,
641
+ },
642
+ });
643
+ ```
644
+
645
+ 配置 Redis 后,签名验证的 nonce 防重放自动切换为 `RedisNonceStore`(跨实例安全)。不配 Redis 则用内存 `MemoryNonceStore`。
646
+
647
+ 也可显式传入自定义 NonceStore:
648
+
649
+ ```typescript
650
+ import { SignatureStrategy, RedisNonceStore } from 'fastify-wrap';
651
+
652
+ const strategy = new SignatureStrategy({
653
+ resolveSecret,
654
+ nonceStore: new RedisNonceStore(),
655
+ });
656
+ ```
657
+
658
+ ## 分页
659
+
660
+ ```typescript
661
+ import type { PageResult } from 'fastify-wrap';
662
+
663
+ @Get('/')
664
+ async list(): Promise<PageResult<UserResponse>> {
665
+ return {
666
+ items: [...],
667
+ total: 100,
668
+ page: 1,
669
+ pageSize: 20,
670
+ };
671
+ }
672
+ ```
673
+
674
+ ## 枚举规范
675
+
676
+ ```typescript
677
+ // DB number → 数值枚举
678
+ export enum UserStatus {
679
+ NORMAL, // 0
680
+ LOCKED, // 1
681
+ }
682
+
683
+ // DB string → 字符串枚举
684
+ export enum OrderType {
685
+ NORMAL = 'NORMAL',
686
+ RUSH = 'RUSH',
687
+ }
688
+ ```
689
+
690
+ 所见即所得,无额外 code/description 字段。
691
+
692
+ ## 二次开发
693
+
694
+ 框架设计为可扩展,以下是在其之上做二次开发的常见模式。
695
+
696
+ ### 定制校验错误消息
697
+
698
+ 框架内置 `errorRules`,覆盖 24 个 JSON Schema 关键字的默认中文提示。可通过 `addRule` 覆盖:
699
+
700
+ ```typescript
701
+ import { addRule } from 'fastify-wrap';
702
+
703
+ // 全局生效,框架启动前调用一次即可
704
+ addRule('required', (field) => `${field} 是必填的`);
705
+ addRule('pattern', (field) => `${field} 格式不对`);
706
+ addRule('type', (field) => `${field} 类型不匹配`);
707
+ ```
708
+
709
+ 不调用则使用内置默认(`password 必填`、`phone 格式错误`、`age 类型错误` 等)。未覆盖的关键字回退到 AJV 原始英文。
710
+
711
+ 完整的关键字列表见 `src/common/validation/errorRules.ts`。
712
+
713
+ ### 自定义 AJV 校验关键字
714
+
715
+ 标准 JSON Schema 关键字不够用时,通过 `addKeyword` 注册自己的规则:
716
+
717
+ ```typescript
718
+ // src/main.ts — createApp 之前调用
719
+ import Ajv from 'ajv';
720
+
721
+ const ajv = new Ajv();
722
+ ajv.addKeyword({
723
+ keyword: 'isPhone',
724
+ validate: (_schema, data) => /^1[0-9]{10}$/.test(data),
725
+ errors: true,
726
+ error: {
727
+ message: (cxt) => `${cxt.instancePath.replace(/^\//, '')} 手机号格式不正确`,
728
+ },
729
+ });
730
+ ```
731
+
732
+ 然后在 Controller 中使用:
733
+
734
+ ```typescript
735
+ // Typebox 风格
736
+ const Request = Type.Object({
737
+ phone: Type.String({ isPhone: true }),
738
+ });
739
+
740
+ // SchemaProp 风格
741
+ @Body({ phone: { type: 'string', isPhone: true } })
742
+ ```
743
+
744
+ 关键字注册时自带的 `error.message` 会自动被 `errorRules.formatError` 使用,无需额外配置。
745
+
746
+ ### 自定义 SchemaProp 辅助
747
+
748
+ 在 `schema.ts` 中新增类型辅助,在项目中任何 Controller 直接使用:
749
+
750
+ ```typescript
751
+ // src/common/validation/schema.ts
752
+ export const Phone: SchemaProp = { type: 'string', pattern: '^1[0-9]{10}$' };
753
+
754
+ export const IdCard: SchemaProp = {
755
+ type: 'string',
756
+ pattern: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$',
757
+ };
758
+ ```
759
+
760
+ ```typescript
761
+ // Controller
762
+ @Body({
763
+ phone: Required(Phone),
764
+ idCard: Required(IdCard),
765
+ })
766
+ ```
767
+
768
+ ### 自定义签名 SecretResolver
769
+
770
+ 对接任何存储系统:
771
+
772
+ ```typescript
773
+ createApp({
774
+ signature: {
775
+ resolveSecret: async (clientId: string, moduleName: string) => {
776
+ const row = await db.queryOne(
777
+ 'SELECT secret_key FROM api_keys WHERE client_id = ? AND module_name = ?',
778
+ [clientId, moduleName],
779
+ );
780
+ return row
781
+ ? { appSecret: row.secret_key as string }
782
+ : null;
783
+ },
784
+ },
785
+ });
786
+ ```
787
+
788
+ ### 自定义 NonceStore
789
+
790
+ 签名防重放实现可替换:
791
+
792
+ ```typescript
793
+ import { SignatureStrategy } from 'fastify-wrap';
794
+
795
+ class MyNonceStore {
796
+ async exists(nonce: string): Promise<boolean> { /* ... */ }
797
+ async mark(nonce: string, ttlMs: number): Promise<void> { /* ... */ }
798
+ async clear(/* ... */): Promise<void> { /* ... */ }
799
+ }
800
+
801
+ const strategy = new SignatureStrategy({
802
+ resolveSecret,
803
+ nonceStore: new MyNonceStore(),
804
+ });
805
+ ```
806
+
807
+ ### 自定义 LoginStrategy
808
+
809
+ 对接非 JWT 的登录鉴权(Session、OAuth、自定义 Token):
810
+
811
+ ```typescript
812
+ import type { AuthStrategy, User } from 'fastify-wrap';
813
+
814
+ class SessionStrategy implements AuthStrategy {
815
+ async authenticate(request: FastifyRequest, _reply: FastifyReply): Promise<User> {
816
+ const sessionId = request.cookies.session;
817
+ const user = await db.queryOne('SELECT * FROM sessions WHERE id = ?', [sessionId]);
818
+ if (!user) throw new AuthException('会话已过期');
819
+ return { id: user.id, role: user.role, type: 'session' };
820
+ }
821
+ }
822
+ ```
823
+
824
+ ### 自定义 Controller 元数据扫描
825
+
826
+ 框架通过 `Reflect.defineMetadata` / `getMetadata` 管理路由元数据。装饰器和扫描逻辑在 `src/common/scanner/` 中,侵入式修改可参考:
827
+
828
+ - `decorators.ts` — 新增装饰器(如 `@RateLimit`、`@Cache`)
829
+ - `index.ts` — 扫描和路由注册(`scanControllers`)
830
+
831
+ 框架内置的 `@Response`、`@Stream` 均按此模式实现,是添加新装饰器的范本。
832
+
833
+ 新增装饰器示例:
834
+
835
+ ```typescript
836
+ const RATE_LIMIT_META = Symbol('route:rateLimit');
837
+
838
+ export function RateLimit(max: number, windowMs: number): MethodDecorator {
839
+ return (target, propertyKey) => {
840
+ Reflect.defineMetadata(RATE_LIMIT_META, { max, windowMs }, target, propertyKey);
841
+ };
842
+ }
843
+ ```
844
+
845
+ ```typescript
846
+ // scanner/index.ts 中读取
847
+ const rateLimitMeta = Reflect.getMetadata(RATE_LIMIT_META, exported, route.handlerName);
848
+ if (rateLimitMeta) {
849
+ routeDef.preHandler = [/* rate limiter hook */];
850
+ }
851
+ ```
852
+
853
+ ## 自测客户端
854
+
855
+ 框架内置 Node.js 端 HMAC 签名客户端,无需浏览器或额外依赖,可直接用于集成测试:
856
+
857
+ ```typescript
858
+ import { TestClient, ApiError, IOException } from 'fastify-wrap/client';
859
+
860
+ const client = TestClient.withCredential(
861
+ 'http://127.0.0.1:3001',
862
+ 'app-demo-001',
863
+ 'sk-your-secret',
864
+ );
865
+
866
+ // JSON 请求 — 成功返回 body,失败抛 ApiError
867
+ const profile = await client.get<{ appKey: string }>('/api/app/profile');
868
+ const echo = await client.post<{ appKey: string }>('/api/app/echo', { msg: 'hello' });
869
+
870
+ // 文件上传
871
+ const uploadRes = await client.upload<{ file: { name: string } }>(
872
+ '/api/app/upload',
873
+ { fileName: 'test.png', type: 'image/png', size: 11 },
874
+ { data: Buffer.from('...'), filename: 'test.png', mimetype: 'image/png' },
875
+ );
876
+
877
+ // 文件下载
878
+ const blobRes = await client.blob('/api/app/download');
879
+ const text: string = await blobRes.body.text();
880
+ // blobRes.filename — 从 Content-Disposition 解析
881
+
882
+ // POST 下载(带请求体的文件下载)
883
+ const blobRes2 = await client.postBlob('/api/app/export', { format: 'csv' });
884
+
885
+ // 错误处理
886
+ try {
887
+ await client.get('/api/secret');
888
+ } catch (e) {
889
+ if (e instanceof ApiError) {
890
+ console.log(e.statusCode, e.code, e.message);
891
+ // statusCode: 422, code: undefined, message: "..."
892
+ }
893
+ if (e instanceof IOException) {
894
+ console.log('Network error');
895
+ }
896
+ }
897
+ ```
898
+
899
+ 复杂配置模式:
900
+
901
+ ```typescript
902
+ const client = new TestClient({
903
+ baseUrl: 'http://127.0.0.1:3001',
904
+ getCredential: () => ({ appKey: 'k', appSecret: 's' }),
905
+ refreshCredential: async (appKey) => {
906
+ // 401 code=retry 时自动调用,传入当前 appKey
907
+ return { appKey: 'k2', appSecret: 's2' };
908
+ },
909
+ getToken: () => process.env.JWT_TOKEN ?? null, // JWT Bearer token
910
+ onLoginRequired: () => { /* 401 code=login 时跳转登录 */ },
911
+ skipAuthPaths: ['/api/auth/login'], // 跳过签名的路径
912
+ toast: (msg) => console.error(msg), // error='toast' 时触发
913
+ alert: (msg) => window.alert(msg), // error='alert' 时触发
914
+ logger: true, // 打印请求/响应日志
915
+ });
916
+
917
+ // 每个请求可单独控制错误处理
918
+ await client.get('/api/resource'); // 默认 toast
919
+ await client.get('/api/resource', 'alert'); // alert 弹窗
920
+ await client.get('/api/resource', (msg) => console.log(msg));// 自定义处理
921
+ ```
922
+
923
+ ## 数据库迁移
924
+
925
+ 配置 `migrate: true` 即可在启动时自动执行 `src/migrations/` 下的 `.sql` 文件:
926
+
927
+ ```typescript
928
+ const app = await createApp({
929
+ db: { ... },
930
+ migrate: true,
931
+ });
932
+ ```
933
+
934
+ 约定:
935
+
936
+ - SQL 文件放在 `src/migrations/`,按文件名字母序执行
937
+ - 框架自动创建 `migrations` 追踪表,已执行的不会重复跑
938
+ - 无需手动登库执行 DDL
939
+
940
+ ```sql
941
+ -- src/migrations/001_create_users.sql
942
+ CREATE TABLE IF NOT EXISTS users (
943
+ id INT AUTO_INCREMENT PRIMARY KEY,
944
+ name VARCHAR(50) NOT NULL
945
+ );
946
+ ```
947
+
948
+ ## 依赖
949
+
950
+ - Fastify 5
951
+ - @fastify/jwt(JWT 鉴权)
952
+ - @fastify/multipart(文件上传)
953
+ - @fastify/mysql + mysql2(MySQL 连接池)
954
+ - @fastify/redis + ioredis(Redis,可选)
955
+ - @fastify/swagger + @fastify/swagger-ui(API 文档,可选)
956
+ - reflect-metadata(装饰器元数据)
957
+ - fast-glob(Controller 自动扫描)
958
+
959
+ TypeScript `experimentalDecorators` 和 `emitDecoratorMetadata` 必须启用。
960
+
961
+ ## 参考
962
+
963
+ - [多环境配置设计](./docs/env-config-design.md)
964
+ - [JWT 登录设计思路](./docs/jwt-login-design.md)
965
+ - [HMAC 签名鉴权方案设计](./docs/signature-design.md)
966
+ - [DB 事务设计](./docs/db-transaction-design.md)
967
+ - [数据库迁移设计](./docs/migration-design.md)
968
+ - [声明式校验设计](./docs/validation-design.md)
969
+ - [Swagger / OpenAPI 自动文档设计](./docs/swagger-design.md)
970
+ - [优雅关闭设计](./docs/shutdown-design.md)
971
+
972
+ ## 待实现
973
+
974
+ | 特性 | 说明 |
975
+ |------|------|
976
+ | CORS | `@fastify/cors`,跨域支持 |
977
+ | 安全头 | `@fastify/helmet`,CSP / HSTS / X-Frame-Options 等 |
978
+ | 请求 ID / 链路追踪 | 为每个请求注入 trace-id,日志可串联 |
979
+ | 限流 | 基于令牌桶或滑动窗口的速率限制 |
980
+ | 缓存抽象 | 防穿透(空值缓存)、防击穿(单飞锁)、防雪崩(随机 TTL) |