schema-dsl 2.0.0 → 2.0.1

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 (145) hide show
  1. package/CHANGELOG.md +130 -113
  2. package/LICENSE +21 -21
  3. package/README.md +628 -628
  4. package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
  5. package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
  6. package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
  7. package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
  8. package/dist/index.cjs +75 -29
  9. package/dist/index.d.cts +10 -4
  10. package/dist/index.d.ts +10 -4
  11. package/dist/index.js +75 -29
  12. package/dist/plugins/custom-format.cjs +33 -17
  13. package/dist/plugins/custom-format.d.cts +1 -1
  14. package/dist/plugins/custom-format.d.ts +1 -1
  15. package/dist/plugins/custom-format.js +33 -17
  16. package/dist/plugins/custom-type-example.cjs +33 -17
  17. package/dist/plugins/custom-type-example.d.cts +1 -1
  18. package/dist/plugins/custom-type-example.d.ts +1 -1
  19. package/dist/plugins/custom-type-example.js +33 -17
  20. package/dist/plugins/custom-validator.cjs +0 -2
  21. package/dist/plugins/custom-validator.d.cts +1 -1
  22. package/dist/plugins/custom-validator.d.ts +1 -1
  23. package/dist/plugins/custom-validator.js +0 -2
  24. package/docs/FEATURE-INDEX.md +553 -553
  25. package/docs/add-custom-locale.md +496 -496
  26. package/docs/add-keyword.md +24 -24
  27. package/docs/api-reference.md +1047 -1047
  28. package/docs/api.md +13 -13
  29. package/docs/best-practices-project-structure.md +417 -417
  30. package/docs/best-practices.md +712 -712
  31. package/docs/cache-manager.md +344 -344
  32. package/docs/compile.md +45 -45
  33. package/docs/conditional-api.md +1307 -1307
  34. package/docs/custom-extensions-guide.md +339 -339
  35. package/docs/design-philosophy.md +606 -606
  36. package/docs/doc-index.md +324 -324
  37. package/docs/dsl-syntax.md +714 -714
  38. package/docs/dynamic-locale.md +608 -608
  39. package/docs/enum.md +482 -482
  40. package/docs/error-handling.md +1975 -1975
  41. package/docs/export-guide.md +501 -501
  42. package/docs/export-limitations.md +567 -567
  43. package/docs/faq.md +596 -596
  44. package/docs/frontend-i18n-guide.md +307 -307
  45. package/docs/i18n-user-guide.md +487 -487
  46. package/docs/i18n.md +476 -476
  47. package/docs/index.md +48 -48
  48. package/docs/json-schema-basics.md +40 -40
  49. package/docs/label-vs-description.md +271 -271
  50. package/docs/markdown-exporter.md +406 -406
  51. package/docs/mongodb-exporter.md +302 -302
  52. package/docs/multi-language.md +26 -26
  53. package/docs/multi-type-support.md +322 -322
  54. package/docs/mysql-exporter.md +280 -280
  55. package/docs/number-operators.md +449 -449
  56. package/docs/optional-marker-guide.md +326 -326
  57. package/docs/performance-guide.md +49 -49
  58. package/docs/plugin-system.md +381 -381
  59. package/docs/plugin-type-registration.md +34 -34
  60. package/docs/postgresql-exporter.md +311 -311
  61. package/docs/public/favicon.svg +4 -4
  62. package/docs/quick-start.md +435 -435
  63. package/docs/runtime-locale-support.md +532 -532
  64. package/docs/schema-helper.md +345 -345
  65. package/docs/schema-utils-advanced-issues.md +23 -23
  66. package/docs/schema-utils-best-practices.md +20 -20
  67. package/docs/schema-utils-chaining.md +150 -150
  68. package/docs/schema-utils.md +524 -524
  69. package/docs/security-checklist.md +20 -20
  70. package/docs/string-extensions.md +488 -488
  71. package/docs/troubleshooting.md +486 -486
  72. package/docs/type-converter.md +310 -310
  73. package/docs/type-reference.md +242 -242
  74. package/docs/typescript-guide.md +584 -584
  75. package/docs/union-type-guide.md +157 -157
  76. package/docs/union-types.md +284 -284
  77. package/docs/validate-async.md +491 -491
  78. package/docs/validate-batch.md +49 -49
  79. package/docs/validate-dsl-object-support.md +578 -578
  80. package/docs/validate.md +506 -506
  81. package/docs/validation-guide.md +502 -502
  82. package/docs/validator.md +39 -39
  83. package/package.json +131 -131
  84. package/plugins/custom-format.cjs +8 -8
  85. package/plugins/custom-type-example.cjs +8 -8
  86. package/plugins/custom-validator.cjs +8 -8
  87. package/src/adapters/DslAdapter.ts +111 -111
  88. package/src/adapters/index.ts +1 -1
  89. package/src/config/constants.ts +83 -83
  90. package/src/config/index.ts +2 -2
  91. package/src/config/patterns.ts +77 -77
  92. package/src/core/CacheManager.ts +169 -159
  93. package/src/core/ConditionalBuilder.ts +382 -382
  94. package/src/core/ConditionalRuntime.ts +27 -27
  95. package/src/core/ConditionalValidator.ts +254 -254
  96. package/src/core/DslBuilder.ts +687 -677
  97. package/src/core/ErrorCodes.ts +38 -38
  98. package/src/core/ErrorFormatter.ts +271 -271
  99. package/src/core/JSONSchemaCore.ts +65 -65
  100. package/src/core/Locale.ts +187 -187
  101. package/src/core/MessageTemplate.ts +42 -42
  102. package/src/core/ObjectDslBuilder.ts +64 -64
  103. package/src/core/PluginManager.ts +326 -326
  104. package/src/core/StringExtensions.ts +140 -140
  105. package/src/core/TemplateEngine.ts +44 -44
  106. package/src/core/Validator.ts +448 -448
  107. package/src/errors/I18nError.ts +159 -159
  108. package/src/errors/ValidationError.ts +105 -105
  109. package/src/exporters/BaseExporter.ts +60 -60
  110. package/src/exporters/MarkdownExporter.ts +305 -305
  111. package/src/exporters/MongoDBExporter.ts +126 -126
  112. package/src/exporters/MySQLExporter.ts +156 -155
  113. package/src/exporters/PostgreSQLExporter.ts +222 -222
  114. package/src/exporters/index.ts +18 -18
  115. package/src/index.ts +651 -633
  116. package/src/locales/en-US.ts +160 -160
  117. package/src/locales/es-ES.ts +160 -160
  118. package/src/locales/fr-FR.ts +160 -160
  119. package/src/locales/index.ts +103 -103
  120. package/src/locales/ja-JP.ts +160 -160
  121. package/src/locales/types.ts +156 -156
  122. package/src/locales/zh-CN.ts +160 -160
  123. package/src/parser/ConstraintParser.ts +101 -101
  124. package/src/parser/DslParser.ts +470 -470
  125. package/src/parser/SchemaCompiler.ts +66 -66
  126. package/src/parser/TypeRegistry.ts +250 -250
  127. package/src/parser/index.ts +6 -6
  128. package/src/plugins/custom-format.ts +124 -126
  129. package/src/plugins/custom-type-example.ts +106 -108
  130. package/src/plugins/custom-validator.ts +138 -140
  131. package/src/types/conditional.ts +28 -28
  132. package/src/types/config.ts +59 -59
  133. package/src/types/dsl.ts +131 -131
  134. package/src/types/error.ts +60 -60
  135. package/src/types/index.ts +17 -17
  136. package/src/types/infer.ts +127 -127
  137. package/src/types/plugin.ts +58 -58
  138. package/src/types/safe-regex.d.ts +9 -9
  139. package/src/types/schema.ts +66 -66
  140. package/src/types/validate.ts +71 -71
  141. package/src/utils/SchemaHelper.ts +196 -196
  142. package/src/utils/SchemaUtils.ts +365 -346
  143. package/src/utils/TypeConverter.ts +215 -215
  144. package/src/utils/index.ts +10 -10
  145. package/src/validators/CustomKeywords.ts +477 -477
@@ -1,1975 +1,1975 @@
1
- # schema-dsl 错误处理完整指南
2
-
3
- > **更新**: 2026-01-30
4
- > **版本**: v1.1.8+
5
- > **适用**: 企业级应用开发
6
-
7
- ---
8
-
9
- ## 📋 目录
10
-
11
- 1. [错误对象结构](#错误对象结构)
12
- 2. [I18nError - 多语言错误抛出](#i18nerror---多语言错误抛出) 🆕
13
- - [📖 概述](#-概述)
14
- - [🚀 快速开始](#-快速开始)
15
- - [📚 核心 API](#-核心-api)
16
- - [🔧 配置语言包](#-配置语言包)
17
- - [🌐 默认语言机制](#-默认语言机制)
18
- - [智能参数识别(v1.1.8)](#智能参数识别v118)
19
- - [🌐 实际场景](#-实际场景)
20
- - [📦 错误对象结构](#-错误对象结构)
21
- - [❓ 常见问题](#-常见问题)
22
- 3. [错误消息定制](#错误消息定制)
23
- 4. [错误码系统](#错误码系统)
24
- 5. [多层级错误处理](#多层级错误处理)
25
- 6. [API响应设计](#api响应设计)
26
- 7. [前端错误展示](#前端错误展示)
27
- 8. [错误日志记录](#错误日志记录)
28
- 9. [最佳实践](#最佳实践)
29
-
30
- ---
31
-
32
- ## I18nError - 多语言错误抛出
33
-
34
- ### 📖 概述
35
-
36
- `I18nError` 是 schema-dsl 提供的**统一多语言错误抛出机制**,专为企业级应用设计。
37
-
38
- **核心价值**:
39
- - ✅ **多语言支持**: 一套代码,自动适配中文/英文/日文等
40
- - ✅ **统一错误码**: 跨语言使用相同数字 code,前端处理不受语言影响
41
- - ✅ **参数插值**: 支持 `{{#balance}}` 等动态参数
42
- - ✅ **框架集成**: 与 Express/Koa 无缝集成
43
- - ✅ **TypeScript 支持**: 完整的类型定义
44
-
45
- **适用场景**:
46
- - API 业务逻辑错误(账户不存在、余额不足、权限不足等)
47
- - 多语言用户场景(国际化应用)
48
- - 需要统一错误码的系统
49
-
50
- **与 ValidationError 的区别**:
51
- - `ValidationError`: 表单验证错误(字段级错误)
52
- - `I18nError`: 业务逻辑错误(应用级错误)
53
-
54
- ---
55
-
56
- ### 🚀 快速开始
57
-
58
- #### 5分钟上手
59
-
60
- ```javascript
61
- const { I18nError, Locale } = require('schema-dsl');
62
-
63
- // 步骤1:配置语言包
64
- Locale.addLocale('zh-CN', {
65
- 'account.notFound': {
66
- code: 40001,
67
- message: '账户不存在'
68
- }
69
- });
70
-
71
- Locale.addLocale('en-US', {
72
- 'account.notFound': {
73
- code: 40001,
74
- message: 'Account not found'
75
- }
76
- });
77
-
78
- // 步骤2:设置默认语言
79
- Locale.setLocale('zh-CN');
80
-
81
- // 步骤3:使用 I18nError
82
- try {
83
- I18nError.throw('account.notFound');
84
- } catch (error) {
85
- console.log(error.message); // "账户不存在"
86
- console.log(error.code); // 40001
87
- }
88
- ```
89
-
90
- ---
91
-
92
- ### 📚 核心 API
93
-
94
- #### I18nError.create()
95
-
96
- **创建错误对象(不抛出)**
97
-
98
- ```javascript
99
- /**
100
- * @param {string} code - 错误代码(多语言 key)
101
- * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码(智能识别)
102
- * @param {number} statusCode - HTTP 状态码(默认 400)
103
- * @param {string} locale - 语言环境(可选)
104
- * @returns {I18nError} 错误实例
105
- */
106
- I18nError.create(code, paramsOrLocale?, statusCode?, locale?)
107
- ```
108
-
109
- **使用示例**:
110
- ```javascript
111
- // 基础用法
112
- const error = I18nError.create('account.notFound');
113
-
114
- // 带参数
115
- const error = I18nError.create('account.insufficientBalance', {
116
- balance: 50,
117
- required: 100
118
- });
119
-
120
- // 指定状态码
121
- const error = I18nError.create('user.notFound', {}, 404);
122
-
123
- // 运行时指定语言(v1.1.8+)
124
- const error = I18nError.create('account.notFound', 'en-US', 404);
125
- ```
126
-
127
- ---
128
-
129
- #### I18nError.throw()
130
-
131
- **直接抛出错误**
132
-
133
- ```javascript
134
- /**
135
- * @param {string} code - 错误代码
136
- * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码
137
- * @param {number} statusCode - HTTP 状态码
138
- * @param {string} locale - 语言环境
139
- * @throws {I18nError}
140
- */
141
- I18nError.throw(code, paramsOrLocale?, statusCode?, locale?)
142
- ```
143
-
144
- **使用示例**:
145
- ```javascript
146
- // 直接抛错
147
- I18nError.throw('user.noPermission');
148
-
149
- // 带参数和状态码
150
- I18nError.throw('account.insufficientBalance', { balance: 50, required: 100 }, 400);
151
-
152
- // 简化语法(v1.1.8+)
153
- I18nError.throw('account.notFound', 'zh-CN', 404);
154
- ```
155
-
156
- ---
157
-
158
- #### I18nError.assert()
159
-
160
- **断言风格 - 条件不满足时抛错**
161
-
162
- ```javascript
163
- /**
164
- * @param {any} condition - 条件表达式(falsy 时抛错)
165
- * @param {string} code - 错误代码
166
- * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码
167
- * @param {number} statusCode - HTTP 状态码
168
- * @param {string} locale - 语言环境
169
- * @throws {I18nError} 条件为 false 时抛出
170
- */
171
- I18nError.assert(condition, code, paramsOrLocale?, statusCode?, locale?)
172
- ```
173
-
174
- **使用示例**:
175
- ```javascript
176
- function getAccount(id) {
177
- const account = db.findAccount(id);
178
-
179
- // 断言:账户必须存在
180
- I18nError.assert(account, 'account.notFound', { id });
181
-
182
- // 断言:余额必须充足
183
- I18nError.assert(
184
- account.balance >= 100,
185
- 'account.insufficientBalance',
186
- { balance: account.balance, required: 100 }
187
- );
188
-
189
- return account;
190
- }
191
- ```
192
-
193
- ---
194
-
195
- #### dsl.error 快捷方法
196
-
197
- `dsl.error` 是 `I18nError` 的快捷访问方式,提供相同的三个方法:
198
-
199
- ```javascript
200
- const { dsl } = require('schema-dsl');
201
-
202
- // 等价于 I18nError.create()
203
- dsl.error.create('account.notFound');
204
-
205
- // 等价于 I18nError.throw()
206
- dsl.error.throw('order.notPaid');
207
-
208
- // 等价于 I18nError.assert()
209
- dsl.error.assert(order, 'order.notFound');
210
- ```
211
-
212
- **推荐使用场景**:
213
- - ✅ 与 `dsl()` 函数一起使用时(风格统一)
214
- - ✅ 导入较少依赖时(只需 `dsl`)
215
-
216
- ---
217
-
218
- ### 🔧 配置语言包
219
-
220
- #### 方式1:使用 Locale.addLocale()(推荐)
221
-
222
- ```javascript
223
- const { Locale } = require('schema-dsl');
224
-
225
- Locale.addLocale('zh-CN', {
226
- // 字符串格式(简单场景)
227
- 'user.notFound': '用户不存在',
228
-
229
- // 对象格式(推荐,v1.1.5+)
230
- 'account.notFound': {
231
- code: 40001, // 数字错误码
232
- message: '账户不存在'
233
- },
234
- 'account.insufficientBalance': {
235
- code: 40002,
236
- message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
237
- }
238
- });
239
-
240
- Locale.addLocale('en-US', {
241
- 'user.notFound': 'User not found',
242
- 'account.notFound': {
243
- code: 40001, // 相同的错误码
244
- message: 'Account not found'
245
- },
246
- 'account.insufficientBalance': {
247
- code: 40002,
248
- message: 'Insufficient balance: {{#balance}}, required {{#required}}'
249
- }
250
- });
251
- ```
252
-
253
- ---
254
-
255
- #### 方式2:使用 dsl.config()(批量配置)
256
-
257
- ```javascript
258
- const { dsl } = require('schema-dsl');
259
-
260
- dsl.config({
261
- i18n: {
262
- 'zh-CN': {
263
- 'payment.failed': {
264
- code: 50001,
265
- message: '支付失败:{{#reason}}'
266
- }
267
- },
268
- 'en-US': {
269
- 'payment.failed': {
270
- code: 50001,
271
- message: 'Payment failed: {{#reason}}'
272
- }
273
- }
274
- }
275
- });
276
- ```
277
-
278
- ---
279
-
280
- #### 方式3:从目录加载(大型项目)
281
-
282
- **目录结构**:
283
- ```text
284
- project/
285
- ├── i18n/
286
- │ └── errors/
287
- │ ├── zh-CN.cjs
288
- │ ├── en-US.jsonc
289
- │ └── ja-JP.json5
290
- └── app.js
291
- ```
292
-
293
- **配置**:
294
- ```javascript
295
- const path = require('path');
296
-
297
- dsl.config({
298
- i18n: path.join(__dirname, 'i18n/errors')
299
- });
300
- ```
301
-
302
- **语言包文件**(例如 `i18n/errors/zh-CN.cjs`):
303
- ```javascript
304
- module.exports = {
305
- 'account.notFound': {
306
- code: 40001,
307
- message: '账户不存在'
308
- },
309
- 'account.insufficientBalance': {
310
- code: 40002,
311
- message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
312
- },
313
- 'user.noPermission': {
314
- code: 40003,
315
- message: '您没有权限执行此操作'
316
- }
317
- };
318
- ```
319
-
320
- ---
321
-
322
- ### 🌐 默认语言机制
323
-
324
- #### 默认语言设置
325
-
326
- **默认值**: `'en-US'`(英文)
327
-
328
- **全局设置**:
329
- ```javascript
330
- const { Locale } = require('schema-dsl');
331
-
332
- // 按应用需要切换默认语言
333
- Locale.setLocale('zh-CN');
334
-
335
- // 获取当前语言
336
- console.log(Locale.getLocale()); // 'zh-CN'
337
- ```
338
-
339
- ---
340
-
341
- #### 语言优先级规则
342
-
343
- ```javascript
344
- 运行时 locale 参数 > 全局 Locale.currentLocale > 默认 'en-US'
345
- ```
346
-
347
- **示例**:
348
- ```javascript
349
- // 场景1:使用全局语言
350
- Locale.setLocale('zh-CN');
351
- I18nError.throw('account.notFound'); // 使用中文 'zh-CN'
352
-
353
- // 场景2:运行时覆盖
354
- Locale.setLocale('zh-CN');
355
- I18nError.throw('account.notFound', 'en-US'); // 覆盖为英文 'en-US'
356
-
357
- // 场景3:参数对象 + 运行时语言
358
- I18nError.throw('account.insufficientBalance',
359
- { balance: 50, required: 100 }, // 参数对象
360
- 400,
361
- 'ja-JP' // 运行时指定日文
362
- );
363
- ```
364
-
365
- ---
366
-
367
- #### 实际应用 - API 多语言响应
368
-
369
- ```javascript
370
- const express = require('express');
371
- const { I18nError } = require('schema-dsl');
372
-
373
- const app = express();
374
-
375
- // 中间件:提取客户端语言
376
- app.use((req, res, next) => {
377
- req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
378
- next();
379
- });
380
-
381
- // API 路由
382
- app.get('/api/account/:id', async (req, res) => {
383
- try {
384
- const account = await findAccount(req.params.id);
385
-
386
- // 🎯 运行时指定语言(根据客户端请求)
387
- I18nError.assert(account, 'account.notFound', req.locale, 404);
388
-
389
- res.json({ success: true, data: account });
390
- } catch (error) {
391
- if (error instanceof I18nError) {
392
- res.status(error.statusCode).json(error.toJSON());
393
- } else {
394
- res.status(500).json({ error: 'Internal Server Error' });
395
- }
396
- }
397
- });
398
- ```
399
-
400
- **效果**:
401
- - 客户端请求头 `Accept-Language: zh-CN` → 返回中文错误
402
- - 客户端请求头 `Accept-Language: en-US` → 返回英文错误
403
- - 无需修改业务代码,自动适配
404
-
405
- ---
406
-
407
- ### 智能参数识别(v1.1.8)
408
-
409
- ### 智能参数识别(v1.1.8)
410
-
411
- **v1.1.8 新增**:支持简化语法,智能识别第2个参数类型
412
-
413
- #### 简化语法
414
-
415
- ```javascript
416
- const { dsl, Locale } = require('schema-dsl');
417
-
418
- // 配置语言包
419
- Locale.addLocale('zh-CN', {
420
- 'account.notFound': {
421
- code: 40001,
422
- message: '账户不存在'
423
- }
424
- });
425
-
426
- Locale.addLocale('en-US', {
427
- 'account.notFound': {
428
- code: 40001,
429
- message: 'Account not found'
430
- }
431
- });
432
-
433
- // ✅ 新增:简化语法(推荐)
434
- dsl.error.throw('account.notFound', 'zh-CN');
435
- dsl.error.throw('account.notFound', 'zh-CN', 404);
436
-
437
- // ✅ 标准语法(完全兼容)
438
- dsl.error.throw('account.notFound', {}, 404, 'zh-CN');
439
- dsl.error.throw('account.notFound', { id: '123' }, 404, 'zh-CN');
440
- ```
441
-
442
- #### 智能识别规则
443
-
444
- ```javascript
445
- // 规则:自动判断第2个参数类型
446
- typeof params === 'string' → 识别为语言参数
447
- typeof params === 'object' → 识别为参数对象
448
- params === null/undefined → 使用默认值
449
- ```
450
-
451
- #### 所有调用方式
452
-
453
- ```javascript
454
- // 1. 简化语法 - 只传语言
455
- dsl.error.throw('account.notFound', 'zh-CN');
456
- dsl.error.create('account.notFound', 'en-US');
457
- dsl.error.assert(account, 'account.notFound', 'zh-CN');
458
-
459
- // 2. 简化语法 - 语言 + 状态码
460
- dsl.error.throw('account.notFound', 'zh-CN', 404);
461
- dsl.error.assert(account, 'account.notFound', 'zh-CN', 404);
462
-
463
- // 3. 标准语法 - 带参数对象
464
- dsl.error.throw('account.insufficientBalance',
465
- { balance: 50, required: 100 },
466
- 400,
467
- 'zh-CN'
468
- );
469
-
470
- // 4. 省略所有参数 - 使用全局语言
471
- dsl.error.throw('account.notFound');
472
- ```
473
-
474
- #### 实际应用
475
-
476
- ```javascript
477
- // Express API
478
- app.get('/api/account/:id', async (req, res) => {
479
- try {
480
- const account = await findAccount(req.params.id);
481
- const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
482
-
483
- // 🎯 简化语法:只需2个参数
484
- dsl.error.assert(account, 'account.notFound', locale);
485
-
486
- res.json(account);
487
- } catch (error) {
488
- res.status(error.statusCode).json(error.toJSON());
489
- }
490
- });
491
- ```
492
-
493
- ---
494
-
495
- ### 🌐 实际场景
496
-
497
- #### Express 完整集成
498
-
499
- ```javascript
500
- const express = require('express');
501
- const { I18nError, Locale } = require('schema-dsl');
502
-
503
- const app = express();
504
- app.use(express.json());
505
-
506
- // ========== 配置语言包 ==========
507
- Locale.addLocale('zh-CN', {
508
- 'account.notFound': {
509
- code: 40001,
510
- message: '账户不存在'
511
- },
512
- 'account.insufficientBalance': {
513
- code: 40002,
514
- message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
515
- }
516
- });
517
-
518
- Locale.addLocale('en-US', {
519
- 'account.notFound': {
520
- code: 40001,
521
- message: 'Account not found'
522
- },
523
- 'account.insufficientBalance': {
524
- code: 40002,
525
- message: 'Insufficient balance: {{#balance}}, required {{#required}}'
526
- }
527
- });
528
-
529
- // ========== 中间件:提取语言 ==========
530
- app.use((req, res, next) => {
531
- req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
532
- next();
533
- });
534
-
535
- // ========== 错误处理中间件 ==========
536
- app.use((error, req, res, next) => {
537
- if (error instanceof I18nError) {
538
- return res.status(error.statusCode).json({
539
- success: false,
540
- error: error.toJSON()
541
- });
542
- }
543
-
544
- // 其他错误
545
- res.status(500).json({
546
- success: false,
547
- message: 'Internal Server Error'
548
- });
549
- });
550
-
551
- // ========== 业务路由 ==========
552
- app.get('/api/account/:id', async (req, res, next) => {
553
- try {
554
- const account = await findAccount(req.params.id);
555
-
556
- // 使用运行时语言
557
- I18nError.assert(account, 'account.notFound', req.locale, 404);
558
-
559
- res.json({ success: true, data: account });
560
- } catch (error) {
561
- next(error);
562
- }
563
- });
564
-
565
- app.post('/api/account/transfer', async (req, res, next) => {
566
- try {
567
- const { fromId, toId, amount } = req.body;
568
- const account = await findAccount(fromId);
569
-
570
- I18nError.assert(account, 'account.notFound', req.locale, 404);
571
- I18nError.assert(
572
- account.balance >= amount,
573
- 'account.insufficientBalance',
574
- { balance: account.balance, required: amount },
575
- 400,
576
- req.locale
577
- );
578
-
579
- await transferMoney(fromId, toId, amount);
580
- res.json({ success: true });
581
- } catch (error) {
582
- next(error);
583
- }
584
- });
585
- ```
586
-
587
- ---
588
-
589
- #### Koa 完整集成
590
-
591
- ```javascript
592
- const Koa = require('koa');
593
- const { I18nError, Locale } = require('schema-dsl');
594
-
595
- const app = new Koa();
596
-
597
- // ========== 配置语言包 ==========
598
- Locale.addLocale('zh-CN', {
599
- 'user.noPermission': {
600
- code: 40003,
601
- message: '您没有权限执行此操作'
602
- }
603
- });
604
-
605
- // ========== 中间件:提取语言 ==========
606
- app.use(async (ctx, next) => {
607
- ctx.locale = ctx.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
608
- await next();
609
- });
610
-
611
- // ========== 错误处理中间件 ==========
612
- app.use(async (ctx, next) => {
613
- try {
614
- await next();
615
- } catch (error) {
616
- if (error instanceof I18nError) {
617
- ctx.status = error.statusCode;
618
- ctx.body = {
619
- success: false,
620
- error: error.toJSON()
621
- };
622
- } else {
623
- ctx.status = 500;
624
- ctx.body = { success: false, message: 'Internal Server Error' };
625
- }
626
- }
627
- });
628
-
629
- // ========== 业务路由 ==========
630
- app.use(async (ctx) => {
631
- if (ctx.path === '/api/admin/users' && ctx.method === 'GET') {
632
- const user = await getCurrentUser(ctx);
633
-
634
- I18nError.assert(user.role === 'admin', 'user.noPermission', ctx.locale, 403);
635
-
636
- ctx.body = { success: true, data: await getUsers() };
637
- }
638
- });
639
- ```
640
-
641
- ---
642
-
643
- #### 原生 Node.js HTTP Server
644
-
645
- ```javascript
646
- const http = require('http');
647
- const { I18nError, Locale } = require('schema-dsl');
648
-
649
- // 配置语言包
650
- Locale.addLocale('zh-CN', {
651
- 'order.notPaid': {
652
- code: 50001,
653
- message: '订单未支付'
654
- }
655
- });
656
-
657
- const server = http.createServer((req, res) => {
658
- try {
659
- // 提取语言
660
- const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
661
-
662
- // 业务逻辑
663
- const order = getOrder(req.url);
664
- I18nError.assert(order && order.paid, 'order.notPaid', locale, 400);
665
-
666
- res.writeHead(200, { 'Content-Type': 'application/json' });
667
- res.end(JSON.stringify({ success: true, data: order }));
668
- } catch (error) {
669
- if (error instanceof I18nError) {
670
- res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
671
- res.end(JSON.stringify({
672
- success: false,
673
- error: error.toJSON()
674
- }));
675
- } else {
676
- res.writeHead(500);
677
- res.end('Internal Server Error');
678
- }
679
- }
680
- });
681
-
682
- server.listen(3000);
683
- ```
684
-
685
- ---
686
-
687
- #### TypeScript 支持
688
-
689
- ```typescript
690
- import { I18nError, Locale } from 'schema-dsl';
691
-
692
- // 类型安全的语言包配置
693
- interface ErrorMessages {
694
- [key: string]: {
695
- code: number;
696
- message: string;
697
- };
698
- }
699
-
700
- const zhCN: ErrorMessages = {
701
- 'account.notFound': {
702
- code: 40001,
703
- message: '账户不存在'
704
- }
705
- };
706
-
707
- Locale.addLocale('zh-CN', zhCN);
708
-
709
- // 使用类型守卫
710
- function handleError(error: unknown): void {
711
- if (error instanceof I18nError) {
712
- console.log(`错误码: ${error.code}`);
713
- console.log(`错误消息: ${error.message}`);
714
- console.log(`HTTP状态: ${error.statusCode}`);
715
- console.log(`语言: ${error.locale}`);
716
- }
717
- }
718
-
719
- // 业务函数
720
- async function getAccount(id: string): Promise<Account> {
721
- const account = await findAccount(id);
722
-
723
- I18nError.assert(account, 'account.notFound', { id }, 404);
724
-
725
- return account;
726
- }
727
- ```
728
-
729
- ---
730
-
731
- ### 📦 错误对象结构
732
-
733
- #### toJSON() 输出格式
734
-
735
- ```javascript
736
- try {
737
- I18nError.throw('account.notFound', {}, 404);
738
- } catch (error) {
739
- console.log(error.toJSON());
740
- }
741
- ```
742
-
743
- **输出**:
744
- ```json
745
- {
746
- "error": "I18nError",
747
- "originalKey": "account.notFound",
748
- "code": 40001,
749
- "message": "账户不存在",
750
- "params": {},
751
- "statusCode": 404,
752
- "locale": "zh-CN"
753
- }
754
- ```
755
-
756
- **字段说明**:
757
- - `error`: 固定为 `"I18nError"`
758
- - `originalKey`: 原始错误 key(v1.1.5 新增,用于日志追踪)
759
- - `code`: 错误代码(数字或字符串)
760
- - `message`: 已翻译的错误消息
761
- - `params`: 参数对象
762
- - `statusCode`: HTTP 状态码
763
- - `locale`: 使用的语言
764
-
765
- ---
766
-
767
- #### 错误对象属性
768
-
769
- ```javascript
770
- try {
771
- I18nError.throw('account.insufficientBalance',
772
- { balance: 50, required: 100 },
773
- 400,
774
- 'zh-CN'
775
- );
776
- } catch (error) {
777
- console.log(error.name); // 'I18nError'
778
- console.log(error.message); // '余额不足,当前50元,需要100元'
779
- console.log(error.originalKey); // 'account.insufficientBalance'
780
- console.log(error.code); // 40002
781
- console.log(error.params); // { balance: 50, required: 100 }
782
- console.log(error.statusCode); // 400
783
- console.log(error.locale); // 'zh-CN'
784
- console.log(error.stack); // 堆栈跟踪
785
- }
786
- ```
787
-
788
- ---
789
-
790
- #### is() 方法 - 错误类型判断
791
-
792
- ```javascript
793
- try {
794
- I18nError.throw('account.notFound');
795
- } catch (error) {
796
- if (error instanceof I18nError) {
797
- // 使用 originalKey 判断
798
- if (error.is('account.notFound')) {
799
- console.log('账户不存在错误');
800
- }
801
-
802
- // 使用数字 code 判断(v1.1.5+)
803
- if (error.is(40001)) {
804
- console.log('账户不存在错误(通过数字码判断)');
805
- }
806
- }
807
- }
808
- ```
809
-
810
- ---
811
-
812
- ### ❓ 常见问题
813
-
814
- #### Q1: 如何动态切换语言?
815
-
816
- **A**: 有两种方式:
817
-
818
- ```javascript
819
- // 方式1:全局切换(影响所有后续调用)
820
- Locale.setLocale('en-US');
821
- I18nError.throw('account.notFound'); // 使用英文
822
-
823
- // 方式2:运行时指定(只影响当次调用)
824
- I18nError.throw('account.notFound', 'en-US'); // 使用英文
825
- I18nError.throw('account.notFound', 'zh-CN'); // 使用中文
826
- ```
827
-
828
- **推荐**: 在 API 中根据客户端请求头动态指定(见上面的 Express 示例)
829
-
830
- ---
831
-
832
- #### Q2: 字符串格式和对象格式有什么区别?
833
-
834
- **A**:
835
-
836
- | 格式 | 优势 | 适用场景 |
837
- |------|------|---------|
838
- | 字符串 | 简单快捷 | 内部错误、不需要统一码 |
839
- | 对象 | 统一错误码、跨语言一致 | 暴露给前端的错误、国际化 |
840
-
841
- ```javascript
842
- // 字符串格式
843
- 'user.notFound': '用户不存在'
844
-
845
- // 对象格式(推荐)
846
- 'user.notFound': {
847
- code: 40001, // 统一的数字码
848
- message: '用户不存在'
849
- }
850
- ```
851
-
852
- **建议**: 优先使用对象格式,便于前端统一处理。
853
-
854
- ---
855
-
856
- #### Q3: 参数插值如何使用?
857
-
858
- **A**: 使用 `{{#参数名}}` 语法:
859
-
860
- ```javascript
861
- // 语言包配置
862
- Locale.addLocale('zh-CN', {
863
- 'account.insufficientBalance': {
864
- code: 40002,
865
- message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
866
- }
867
- });
868
-
869
- // 使用
870
- I18nError.throw('account.insufficientBalance', {
871
- balance: 50,
872
- required: 100
873
- });
874
- // 输出: "余额不足,当前50元,需要100元"
875
- ```
876
-
877
- **注意**: 参数名必须用 `{{#参数名}}` 格式(井号必须有)。
878
-
879
- ---
880
-
881
- #### Q4: 与 dsl.if 的 message() 有什么区别?
882
-
883
- **A**:
884
-
885
- - `dsl.if().message()`: 用于**数据验证错误**(Schema 验证)
886
- - `I18nError`: 用于**业务逻辑错误**(API 业务逻辑)
887
-
888
- ```javascript
889
- // dsl.if - 数据验证
890
- dsl.if(d => !d).message('user.notFound').assert(user);
891
-
892
- // I18nError - 业务逻辑
893
- I18nError.assert(user.role === 'admin', 'user.noPermission');
894
- ```
895
-
896
- **可以混合使用**:
897
- ```javascript
898
- function validateAndProcess(user) {
899
- // 步骤1:数据验证(使用 dsl.if)
900
- dsl.if(d => !d).message('user.notFound').assert(user);
901
-
902
- // 步骤2:业务逻辑验证(使用 I18nError)
903
- I18nError.assert(user.role === 'admin', 'user.noPermission');
904
- }
905
- ```
906
-
907
- ---
908
-
909
- #### Q5: 如何获取所有可用语言?
910
-
911
- **A**:
912
-
913
- ```javascript
914
- const { Locale } = require('schema-dsl');
915
-
916
- const locales = Locale.getAvailableLocales();
917
- console.log(locales); // ['en-US', 'zh-CN', 'ja-JP', ...]
918
- ```
919
-
920
- ---
921
-
922
- #### Q6: 如何在前端统一处理错误码?
923
-
924
- **A**: 使用数字 `code` 字段:
925
-
926
- ```javascript
927
- // 前端错误处理
928
- async function apiCall() {
929
- try {
930
- const response = await fetch('/api/account');
931
- const data = await response.json();
932
- } catch (error) {
933
- // 根据数字 code 统一处理(不受语言影响)
934
- switch (error.code) {
935
- case 40001:
936
- router.push('/login'); // 账户不存在 → 跳转登录
937
- break;
938
- case 40002:
939
- showTopUpDialog(); // 余额不足 → 显示充值弹窗
940
- break;
941
- case 40003:
942
- showError('权限不足'); // 权限不足
943
- break;
944
- default:
945
- showError(error.message);
946
- }
947
- }
948
- }
949
- ```
950
-
951
- **优势**: 前端逻辑不受后端语言切换影响。
952
-
953
- ---
954
-
955
- #### Q7: 默认语言是什么?如何修改?
956
-
957
- **A**:
958
-
959
- - **默认语言**: `'en-US'`(英文)
960
- - **修改方式**:
961
-
962
- ```javascript
963
- const { Locale } = require('schema-dsl');
964
-
965
- // 启动时按应用需要设置默认语言
966
- Locale.setLocale('zh-CN');
967
-
968
- // 获取当前默认语言
969
- console.log(Locale.getLocale()); // 'zh-CN'
970
- ```
971
-
972
- **建议**: 在应用启动时(app.js 入口)设置默认语言。
973
-
974
- ---
975
-
976
- #### Q8: 如何处理未配置的错误 key?
977
-
978
- **A**: 如果错误 key 未在语言包中配置,会直接返回原始 key:
979
-
980
- ```javascript
981
- // 未配置 'custom.error'
982
- I18nError.throw('custom.error');
983
- // message: 'custom.error'(原样返回)
984
- ```
985
-
986
- **建议**:
987
- 1. 使用 TypeScript 定义错误 key 类型,避免拼写错误
988
- 2. 在开发环境检查是否所有错误 key 都已配置
989
-
990
- ---
991
-
992
- #### Q9: 支持哪些内置语言?
993
-
994
- **A**:
995
-
996
- | 语言代码 | 语言名称 | 支持状态 |
997
- |---------|---------|---------|
998
- | `en-US` | 英语(美国) | ✅ 内置 |
999
- | `zh-CN` | 简体中文 | ✅ 内置 |
1000
- | `ja-JP` | 日语 | ✅ 可扩展 |
1001
- | `fr-FR` | 法语 | ✅ 可扩展 |
1002
- | `es-ES` | 西班牙语 | ✅ 可扩展 |
1003
-
1004
- **自定义语言**: 使用 `Locale.addLocale()` 添加任意语言。
1005
-
1006
- ---
1007
-
1008
- #### Q10: 如何在日志中记录错误详情?
1009
-
1010
- **A**:
1011
-
1012
- ```javascript
1013
- const winston = require('winston');
1014
-
1015
- app.use((error, req, res, next) => {
1016
- if (error instanceof I18nError) {
1017
- // 记录详细日志
1018
- winston.error('业务错误', {
1019
- originalKey: error.originalKey, // 原始 key(便于追踪)
1020
- code: error.code, // 错误码
1021
- message: error.message, // 已翻译的消息
1022
- params: error.params, // 参数
1023
- statusCode: error.statusCode,
1024
- locale: error.locale,
1025
- url: req.url,
1026
- method: req.method,
1027
- ip: req.ip
1028
- });
1029
-
1030
- return res.status(error.statusCode).json(error.toJSON());
1031
- }
1032
- next(error);
1033
- });
1034
- ```
1035
-
1036
- **推荐**: 使用 `originalKey` 而非 `message`,因为 `message` 会随语言变化。
1037
-
1038
- ---
1039
-
1040
- ## 错误对象结构
1041
-
1042
- ### 基础结构
1043
-
1044
- schema-dsl 验证返回的错误对象结构:
1045
-
1046
- ```javascript
1047
- const { dsl, validate } = require('schema-dsl');
1048
-
1049
- const schema = dsl({
1050
- username: 'string:3-32!'.label('用户名')
1051
- });
1052
-
1053
- const result = validate(schema, { username: 'ab' });
1054
-
1055
- // 返回结构
1056
- {
1057
- valid: false,
1058
- errors: [
1059
- {
1060
- path: 'username',
1061
- field: 'username',
1062
- keyword: 'minLength',
1063
- params: { limit: 3 },
1064
- message: '用户名长度不能少于3个字符'
1065
- }
1066
- ]
1067
- }
1068
- ```
1069
-
1070
- ### 嵌套对象错误
1071
-
1072
- ```javascript
1073
- const { dsl, validate } = require('schema-dsl');
1074
-
1075
- const schema = dsl({
1076
- user: {
1077
- profile: {
1078
- email: 'email!'
1079
- }
1080
- }
1081
- });
1082
-
1083
- const result = validate(schema, {
1084
- user: {
1085
- profile: {
1086
- email: 'invalid'
1087
- }
1088
- }
1089
- });
1090
-
1091
- // 错误路径
1092
- console.log(result.errors[0].path); // 'user/profile/email'
1093
- console.log(result.errors[0].message); // '邮箱必须是有效的邮箱地址'
1094
- ```
1095
-
1096
- ### 数组项错误
1097
-
1098
- ```javascript
1099
- const { dsl, validate } = require('schema-dsl');
1100
-
1101
- const schema = dsl({
1102
- items: 'array<string:3->!'
1103
- });
1104
-
1105
- const result = validate(schema, {
1106
- items: ['ab', 'valid']
1107
- });
1108
-
1109
- // 错误路径
1110
- console.log(result.errors[0].path); // 'items/0'
1111
- ```
1112
-
1113
- ---
1114
-
1115
- ## 错误消息定制
1116
-
1117
- ### 单字段定制
1118
-
1119
- ```javascript
1120
- const { dsl } = require('schema-dsl');
1121
-
1122
- // 使用 String 扩展定制消息
1123
- const schema = dsl({
1124
- username: 'string:3-32!'
1125
- .label('用户名')
1126
- .messages({
1127
- 'min': '太短了!至少要3个字符'
1128
- })
1129
- });
1130
- ```
1131
-
1132
- ### 多规则定制
1133
-
1134
- ```javascript
1135
- const { dsl } = require('schema-dsl');
1136
-
1137
- const schema = dsl({
1138
- email: 'email!'
1139
- .label('邮箱地址')
1140
- .messages({
1141
- 'format': '邮箱格式不对哦',
1142
- 'required': '邮箱不能为空'
1143
- })
1144
- });
1145
- ```
1146
-
1147
- ### 对象级定制
1148
-
1149
- ```javascript
1150
- const { dsl } = require('schema-dsl');
1151
-
1152
- const schema = dsl({
1153
- username: 'string:3-32!'
1154
- .label('用户名')
1155
- .messages({
1156
- 'min': '{{#label}}至少{{#limit}}个字符',
1157
- 'max': '{{#label}}最多{{#limit}}个字符'
1158
- }),
1159
-
1160
- email: 'email!'
1161
- .label('邮箱')
1162
- .messages({
1163
- 'format': '{{#label}}格式无效'
1164
- })
1165
- });
1166
- ```
1167
-
1168
- ### 全局定制
1169
-
1170
- ```javascript
1171
- const { Locale } = require('schema-dsl');
1172
-
1173
- // 设置全局消息
1174
- Locale.setMessages({
1175
- 'min': '输入太短,要{{#limit}}个字符',
1176
- 'format': '格式不正确'
1177
- });
1178
- ```
1179
-
1180
- ---
1181
-
1182
- ## 错误码系统
1183
-
1184
- ### 内置错误码(简化版)
1185
-
1186
- schema-dsl 对 Ajv 的错误关键字进行了统一格式化,使其更易用:
1187
-
1188
- #### 字符串错误码
1189
-
1190
- | 关键字 | 原始关键字 | 说明 | params |
1191
- |--------|-----------|------|--------|
1192
- | `min` | `minLength` | 长度小于最小值 | { limit: number } |
1193
- | `max` | `maxLength` | 长度大于最大值 | { limit: number } |
1194
- | `format` | `format` | 格式验证失败 | { format: 'email'/'uri'/etc } |
1195
- | `pattern` | `pattern` | 正则不匹配 | { pattern: string } |
1196
- | `enum` | `enum` | 不在枚举值中 | { allowedValues: array } |
1197
-
1198
- #### 数字错误码
1199
-
1200
- | 关键字 | 原始关键字 | 说明 | params |
1201
- |--------|-----------|------|--------|
1202
- | `min` | `minimum` | 小于最小值 | { limit: number } |
1203
- | `max` | `maximum` | 大于最大值 | { limit: number } |
1204
-
1205
- #### 通用错误码
1206
-
1207
- | 关键字 | 说明 | params |
1208
- |--------|------|--------|
1209
- | `required` | 必填字段缺失 | { missingProperty: string } |
1210
- | `type` | 类型不匹配 | { type: string } |
1211
-
1212
- **💡 提示**: 您可以使用简化关键字(如 `min`)或原始关键字(如 `minLength`)来定制错误消息,系统会自动处理映射。
1213
-
1214
- ### 自动 Label 翻译
1215
-
1216
- 如果您在语言包中定义了 `label.{fieldName}`,系统会自动将其作为 Label 使用,无需显式调用 `.label()`。
1217
-
1218
- ```javascript
1219
- // 语言包
1220
- Locale.addLocale('zh-CN', {
1221
- 'label.username': '用户名',
1222
- 'required': '{{#label}}不能为空'
1223
- });
1224
-
1225
- // Schema
1226
- const schema = dsl({
1227
- username: 'string!' // 自动查找 label.username
1228
- });
1229
-
1230
- // 错误消息: "用户名不能为空"
1231
- ```
1232
-
1233
- ### 自定义验证错误
1234
-
1235
- ```javascript
1236
- const { dsl } = require('schema-dsl');
1237
-
1238
- const schema = dsl({
1239
- username: 'string:3-32!'
1240
- .custom((value) => {
1241
- if (value.includes('forbidden')) {
1242
- return '内容包含禁止的词语';
1243
- }
1244
- // 验证通过时无需返回
1245
- })
1246
- .label('用户名')
1247
- });
1248
- ```
1249
-
1250
- ---
1251
-
1252
- ## 多层级错误处理
1253
-
1254
- ### 嵌套对象验证
1255
-
1256
- ```javascript
1257
- const { dsl, validate } = require('schema-dsl');
1258
-
1259
- const schema = dsl({
1260
- user: {
1261
- name: 'string:1-100!',
1262
- address: {
1263
- country: 'string!'.label('国家'),
1264
- city: 'string!'.label('城市'),
1265
- street: 'string!'.label('街道')
1266
- }
1267
- }
1268
- });
1269
-
1270
- const result = validate(schema, {
1271
- user: {
1272
- name: 'John',
1273
- address: {
1274
- country: 'CN'
1275
- // 缺少city和street
1276
- }
1277
- }
1278
- });
1279
-
1280
- // 错误示例
1281
- // result.errors[0].path: 'user/address/city'
1282
- // result.errors[1].path: 'user/address/street'
1283
- ```
1284
-
1285
- ### 数组验证
1286
-
1287
- ```javascript
1288
- const { dsl, validate } = require('schema-dsl');
1289
-
1290
- const schema = dsl({
1291
- items: 'array:1-<string:3->!'
1292
- .label('商品列表')
1293
- });
1294
-
1295
- const result = validate(schema, {
1296
- items: ['ab', 'valid'] // 第一项太短
1297
- });
1298
-
1299
- // 错误路径
1300
- console.log(result.errors[0].path); // 'items/0'
1301
- ```
1302
-
1303
- ---
1304
-
1305
- ## API响应设计
1306
-
1307
- ### 标准响应格式
1308
-
1309
- ```javascript
1310
- // 成功响应
1311
- {
1312
- success: true,
1313
- code: 'SUCCESS',
1314
- data: { ... }
1315
- }
1316
-
1317
- // 验证错误响应
1318
- {
1319
- success: false,
1320
- code: 'VALIDATION_ERROR',
1321
- message: '数据验证失败',
1322
- errors: [
1323
- {
1324
- field: 'username',
1325
- message: 'must NOT have fewer than 3 characters',
1326
- keyword: 'minLength',
1327
- params: { limit: 3 }
1328
- }
1329
- ]
1330
- }
1331
-
1332
- // 服务器错误响应
1333
- {
1334
- success: false,
1335
- code: 'SERVER_ERROR',
1336
- message: '服务器内部错误'
1337
- }
1338
- ```
1339
-
1340
- ### Express中间件
1341
-
1342
- ```javascript
1343
- const { dsl, Validator } = require('schema-dsl');
1344
-
1345
- // 验证中间件
1346
- function validateBody(schema) {
1347
- const validator = new Validator();
1348
-
1349
- return (req, res, next) => {
1350
- const result = validator.validate(schema, req.body);
1351
-
1352
- if (!result.valid) {
1353
- return res.status(400).json({
1354
- success: false,
1355
- code: 'VALIDATION_ERROR',
1356
- message: '请检查输入信息',
1357
- errors: result.errors.map(err => ({
1358
- field: err.path.replace(/\//g, '.'),
1359
- message: err.message,
1360
- keyword: err.keyword,
1361
- params: err.params
1362
- }))
1363
- });
1364
- }
1365
-
1366
- // 验证通过,继续处理
1367
- next();
1368
- };
1369
- }
1370
-
1371
- // 使用示例
1372
- const userSchema = dsl({
1373
- username: 'string:3-32!',
1374
- email: 'email!',
1375
- password: 'string:8-64!'
1376
- });
1377
-
1378
- app.post('/api/users',
1379
- validateBody(userSchema),
1380
- async (req, res) => {
1381
- const user = await createUser(req.body);
1382
- res.json({ success: true, data: user });
1383
- }
1384
- );
1385
- ```
1386
-
1387
- ### Koa中间件
1388
-
1389
- ```javascript
1390
- const { dsl, Validator } = require('schema-dsl');
1391
-
1392
- function validateBody(schema) {
1393
- const validator = new Validator();
1394
-
1395
- return async (ctx, next) => {
1396
- const result = validator.validate(schema, ctx.request.body);
1397
-
1398
- if (!result.valid) {
1399
- ctx.status = 400;
1400
- ctx.body = {
1401
- success: false,
1402
- code: 'VALIDATION_ERROR',
1403
- message: '数据验证失败',
1404
- errors: result.errors.map(err => ({
1405
- field: err.path.replace(/\//g, '.'),
1406
- message: err.message,
1407
- keyword: err.keyword
1408
- }))
1409
- };
1410
- return;
1411
- }
1412
-
1413
- await next();
1414
- };
1415
- }
1416
-
1417
- // 使用示例
1418
- const registerSchema = dsl({
1419
- username: 'string:3-32!'.username(),
1420
- email: 'email!',
1421
- password: 'string!'.password('strong')
1422
- });
1423
-
1424
- router.post('/register', validateBody(registerSchema), async (ctx) => {
1425
- ctx.body = { success: true, data: await register(ctx.request.body) };
1426
- });
1427
- ```
1428
-
1429
- ---
1430
-
1431
- ## 前端错误展示
1432
-
1433
- ### React示例
1434
-
1435
- ```javascript
1436
- import React, { useState } from 'react';
1437
-
1438
- function RegisterForm() {
1439
- const [errors, setErrors] = useState({});
1440
-
1441
- const handleSubmit = async (e) => {
1442
- e.preventDefault();
1443
-
1444
- try {
1445
- const response = await fetch('/api/register', {
1446
- method: 'POST',
1447
- headers: { 'Content-Type': 'application/json' },
1448
- body: JSON.stringify(formData)
1449
- });
1450
-
1451
- const data = await response.json();
1452
-
1453
- if (!data.success && data.code === 'VALIDATION_ERROR') {
1454
- // 将错误数组转为对象
1455
- const errorMap = {};
1456
- data.errors.forEach(err => {
1457
- errorMap[err.field] = err.message;
1458
- });
1459
- setErrors(errorMap);
1460
- }
1461
-
1462
- } catch (error) {
1463
- console.error(error);
1464
- }
1465
- };
1466
-
1467
- return (
1468
- <form onSubmit={handleSubmit}>
1469
- <div>
1470
- <input name="username" />
1471
- {errors.username && (
1472
- <span className="error">{errors.username}</span>
1473
- )}
1474
- </div>
1475
-
1476
- <div>
1477
- <input name="email" type="email" />
1478
- {errors.email && (
1479
- <span className="error">{errors.email}</span>
1480
- )}
1481
- </div>
1482
-
1483
- <button type="submit">注册</button>
1484
- </form>
1485
- );
1486
- }
1487
- ```
1488
-
1489
- ### Vue示例
1490
-
1491
- ```vue
1492
- <template>
1493
- <form @submit.prevent="handleSubmit">
1494
- <div>
1495
- <input v-model="form.username" />
1496
- <span v-if="errors.username" class="error">
1497
- {{ errors.username }}
1498
- </span>
1499
- </div>
1500
-
1501
- <div>
1502
- <input v-model="form.email" type="email" />
1503
- <span v-if="errors.email" class="error">
1504
- {{ errors.email }}
1505
- </span>
1506
- </div>
1507
-
1508
- <button type="submit">注册</button>
1509
- </form>
1510
- </template>
1511
-
1512
- <script>
1513
- export default {
1514
- data() {
1515
- return {
1516
- form: {
1517
- username: '',
1518
- email: ''
1519
- },
1520
- errors: {}
1521
- };
1522
- },
1523
- methods: {
1524
- async handleSubmit() {
1525
- try {
1526
- const response = await fetch('/api/register', {
1527
- method: 'POST',
1528
- headers: { 'Content-Type': 'application/json' },
1529
- body: JSON.stringify(this.form)
1530
- });
1531
-
1532
- const data = await response.json();
1533
-
1534
- if (!data.success && data.code === 'VALIDATION_ERROR') {
1535
- this.errors = data.errors.reduce((acc, err) => {
1536
- acc[err.field] = err.message;
1537
- return acc;
1538
- }, {});
1539
- }
1540
-
1541
- } catch (error) {
1542
- console.error(error);
1543
- }
1544
- }
1545
- }
1546
- };
1547
- </script>
1548
- ```
1549
-
1550
- ---
1551
-
1552
- ## 错误日志记录
1553
-
1554
- ### 基础日志
1555
-
1556
- ```javascript
1557
- app.post('/api/register', async (req, res) => {
1558
- const result = await registerSchema.validate(req.body, {
1559
- abortEarly: false
1560
- });
1561
-
1562
- if (!result.isValid) {
1563
- // 记录验证错误
1564
- logger.warn('用户注册验证失败', {
1565
- ip: req.ip,
1566
- errors: result.errors,
1567
- data: req.body
1568
- });
1569
-
1570
- return res.status(400).json({
1571
- success: false,
1572
- errors: result.errors
1573
- });
1574
- }
1575
-
1576
- // 继续处理
1577
- });
1578
- ```
1579
-
1580
- ### 结构化日志
1581
-
1582
- ```javascript
1583
- const logger = require('winston');
1584
-
1585
- function logValidationError(req, result) {
1586
- logger.warn({
1587
- message: '验证失败',
1588
- type: 'VALIDATION_ERROR',
1589
- timestamp: new Date().toISOString(),
1590
- ip: req.ip,
1591
- url: req.url,
1592
- method: req.method,
1593
- errors: result.errors.map(err => ({
1594
- path: err.path.replace(/\//g, '.'),
1595
- type: err.type,
1596
- message: err.message
1597
- })),
1598
- // 敏感数据脱敏
1599
- data: maskSensitiveData(req.body)
1600
- });
1601
- }
1602
- ```
1603
-
1604
- ---
1605
-
1606
- ## 最佳实践
1607
-
1608
- ### 1. 使用 label 让错误消息更清晰
1609
-
1610
- ```javascript
1611
- const { dsl } = require('schema-dsl');
1612
-
1613
- // ✅ 推荐:使用 label
1614
- const schema = dsl({
1615
- username: 'string:3-32!'.label('用户名')
1616
- });
1617
- // 错误消息会包含"用户名"标签
1618
-
1619
- // ❌ 不推荐:不使用 label
1620
- const schema = dsl({
1621
- username: 'string:3-32!'
1622
- });
1623
- // 错误消息只显示字段名 "username"
1624
- ```
1625
-
1626
- ### 2. 提供友好的中文错误消息
1627
-
1628
- ```javascript
1629
- const { dsl } = require('schema-dsl');
1630
-
1631
- // ✅ 推荐:自定义中文消息
1632
- const schema = dsl({
1633
- username: 'string:3-32!'
1634
- .label('用户名')
1635
- .messages({
1636
- 'minLength': '{{#label}}至少需要{{#limit}}个字符',
1637
- 'maxLength': '{{#label}}最多{{#limit}}个字符'
1638
- })
1639
- });
1640
-
1641
- // ❌ 不推荐:使用默认英文消息
1642
- const schema = dsl({
1643
- username: 'string:3-32!'
1644
- });
1645
- ```
1646
-
1647
- ### 3. 使用自定义验证实现业务逻辑
1648
-
1649
- ```javascript
1650
- const { dsl } = require('schema-dsl');
1651
-
1652
- // ✅ 推荐:返回错误消息字符串
1653
- const schema = dsl({
1654
- username: 'string:3-32!'
1655
- .custom((value) => {
1656
- if (value === 'admin') {
1657
- return '用户名已被占用';
1658
- }
1659
- // 验证通过时无需返回
1660
- })
1661
- .label('用户名')
1662
- });
1663
- ```
1664
-
1665
- ### 4. 敏感数据不要出现在错误日志中
1666
-
1667
- ```javascript
1668
- function maskSensitiveData(data) {
1669
- return {
1670
- ...data,
1671
- password: '***',
1672
- confirmPassword: '***',
1673
- creditCard: data.creditCard ? '****' + data.creditCard.slice(-4) : undefined
1674
- };
1675
- }
1676
-
1677
- // 使用
1678
- logger.warn('验证失败', {
1679
- errors: result.errors,
1680
- data: maskSensitiveData(req.body)
1681
- });
1682
- ```
1683
-
1684
- ### 5. 统一错误格式便于前端处理
1685
-
1686
- ```javascript
1687
- // 统一的错误格式化函数
1688
- function formatValidationErrors(errors) {
1689
- return errors.map(err => ({
1690
- field: err.path.replace(/\//g, '.'),
1691
- message: err.message,
1692
- keyword: err.keyword,
1693
- params: err.params
1694
- }));
1695
- }
1696
-
1697
- // 使用
1698
- if (!result.valid) {
1699
- return res.status(400).json({
1700
- success: false,
1701
- code: 'VALIDATION_ERROR',
1702
- errors: formatValidationErrors(result.errors)
1703
- });
1704
- }
1705
- ```
1706
-
1707
- ---
1708
-
1709
- ## v1.1.5 新功能:对象格式错误配置
1710
-
1711
- ### 概述
1712
-
1713
- 从 v1.1.5 开始,语言包支持对象格式 `{ code, message }`,实现统一的错误代码管理。
1714
-
1715
- ### 基础用法
1716
-
1717
- **语言包配置**:
1718
- ```javascript
1719
- // i18n/errors/zh-CN.cjs(或任意 .json/.jsonc/.json5 自定义语言包文件)
1720
- module.exports = {
1721
- // 字符串格式(向后兼容)
1722
- 'user.notFound': '用户不存在',
1723
-
1724
- // 对象格式(v1.1.5 新增)✨ - 使用数字错误码
1725
- 'account.notFound': {
1726
- code: 40001,
1727
- message: '账户不存在'
1728
- },
1729
- 'account.insufficientBalance': {
1730
- code: 40002,
1731
- message: '余额不足,当前余额{{#balance}},需要{{#required}}'
1732
- },
1733
- 'order.notPaid': {
1734
- code: 50001,
1735
- message: '订单未支付'
1736
- }
1737
- };
1738
- ```
1739
-
1740
- **使用示例**:
1741
- ```javascript
1742
- const { dsl } = require('schema-dsl');
1743
-
1744
- try {
1745
- dsl.error.throw('account.notFound');
1746
- } catch (error) {
1747
- console.log(error.originalKey); // 'account.notFound'
1748
- console.log(error.code); // 40001 ✨ 数字错误码
1749
- console.log(error.message); // '账户不存在'
1750
- }
1751
- ```
1752
-
1753
- ### 核心特性
1754
-
1755
- #### 1. originalKey 字段(新增)
1756
-
1757
- 保留原始的 key,便于调试和日志追踪:
1758
-
1759
- ```javascript
1760
- try {
1761
- dsl.error.throw('account.notFound');
1762
- } catch (error) {
1763
- error.originalKey // 'account.notFound' (原始 key)
1764
- error.code // 40001 (数字错误码)
1765
- }
1766
- ```
1767
-
1768
- #### 2. 多语言共享 code
1769
-
1770
- 不同语言使用相同的数字 `code`,便于前端统一处理:
1771
-
1772
- ```javascript
1773
- // zh-CN.cjs
1774
- 'account.notFound': {
1775
- code: 40001, // ← 数字 code 一致
1776
- message: '账户不存在'
1777
- }
1778
-
1779
- // en-US.cjs
1780
- 'account.notFound': {
1781
- code: 40001, // ← 数字 code 一致
1782
- message: 'Account not found'
1783
- }
1784
-
1785
- // 前端处理 - 不受语言影响
1786
- switch (error.code) {
1787
- case 40001:
1788
- redirectToLogin();
1789
- break;
1790
- case 40002:
1791
- showTopUpDialog();
1792
- break;
1793
- case 50001:
1794
- showPaymentDialog();
1795
- break;
1796
- }
1797
- ```
1798
-
1799
- #### 3. 增强的 error.is() 方法
1800
-
1801
- 同时支持 `originalKey` 和数字 `code` 判断:
1802
-
1803
- ```javascript
1804
- try {
1805
- dsl.error.throw('account.notFound');
1806
- } catch (error) {
1807
- // 两种方式都可以
1808
- if (error.is('account.notFound')) { } // ✅ 使用 originalKey
1809
- if (error.is(40001)) { } // ✅ 使用数字 code
1810
- }
1811
- ```
1812
-
1813
- #### 4. toJSON 包含 originalKey
1814
-
1815
- ```javascript
1816
- const json = error.toJSON();
1817
- // {
1818
- // error: 'I18nError',
1819
- // originalKey: 'account.notFound', // ✨ v1.1.5 新增
1820
- // code: 'ACCOUNT_NOT_FOUND',
1821
- // message: '账户不存在',
1822
- // params: {},
1823
- // statusCode: 400,
1824
- // locale: 'zh-CN'
1825
- // }
1826
- ```
1827
-
1828
- ### 向后兼容
1829
-
1830
- **完全向后兼容** ✅ - 字符串格式自动转换:
1831
-
1832
- ```javascript
1833
- // 字符串格式(原有)
1834
- 'user.notFound': '用户不存在'
1835
-
1836
- // 自动转换为对象
1837
- dsl.error.throw('user.notFound');
1838
- // error.code = 'user.notFound' (使用 key 作为 code)
1839
- // error.originalKey = 'user.notFound'
1840
- // error.message = '用户不存在'
1841
- ```
1842
-
1843
- ### 最佳实践
1844
-
1845
- #### 1. 何时使用对象格式
1846
-
1847
- **推荐使用对象格式**:
1848
- - ✅ 需要在多语言中统一处理的错误
1849
- - ✅ 需要前端统一判断的错误
1850
- - ✅ 核心业务错误(账户、订单、支付等)
1851
-
1852
- **可以使用字符串格式**:
1853
- - ✅ 简单的验证错误
1854
- - ✅ 内部错误(不暴露给前端)
1855
- - ✅ 不需要统一处理的错误
1856
-
1857
- #### 2. 错误代码命名规范
1858
-
1859
- 推荐使用**数字错误码**,按模块分段:
1860
-
1861
- ```javascript
1862
- // 错误码规范(5位数字)
1863
- // 4xxxx - 客户端错误
1864
- // 5xxxx - 业务逻辑错误
1865
- // 6xxxx - 系统错误
1866
-
1867
- 'account.notFound': {
1868
- code: 40001, // ✅ 推荐:账户模块,序号001
1869
- message: '账户不存在'
1870
- }
1871
-
1872
- 'account.insufficientBalance': {
1873
- code: 40002, // 账户模块,序号002
1874
- message: '余额不足'
1875
- }
1876
-
1877
- 'order.notPaid': {
1878
- code: 50001, // ✅ 订单模块,序号001
1879
- message: '订单未支付'
1880
- }
1881
-
1882
- 'order.cancelled': {
1883
- code: 50002, // 订单模块,序号002
1884
- message: '订单已取消'
1885
- }
1886
-
1887
- 'database.connectionError': {
1888
- code: 60001, // ✅ 系统错误
1889
- message: '数据库连接失败'
1890
- }
1891
- ```
1892
-
1893
- **错误码分段建议**:
1894
- - `40001-49999` - 客户端错误(账户、权限、参数验证等)
1895
- - `50001-59999` - 业务逻辑错误(订单、支付、库存等)
1896
- - `60001-69999` - 系统错误(数据库、服务不可用等)
1897
-
1898
- #### 3. 前端统一错误处理
1899
-
1900
- ```javascript
1901
- // API 调用
1902
- try {
1903
- const response = await fetch('/api/account');
1904
- const data = await response.json();
1905
- } catch (error) {
1906
- // 使用数字 code 统一处理,不受语言影响
1907
- switch (error.code) {
1908
- case 40001: // ACCOUNT_NOT_FOUND
1909
- showNotFoundPage();
1910
- break;
1911
- case 40002: // INSUFFICIENT_BALANCE
1912
- showTopUpDialog(error.params);
1913
- break;
1914
- case 50001: // ORDER_NOT_PAID
1915
- showPaymentDialog();
1916
- break;
1917
- case 60001: // SYSTEM_ERROR
1918
- showSystemErrorPage();
1919
- break;
1920
- default:
1921
- showGenericError(error.message);
1922
- }
1923
- }
1924
- ```
1925
-
1926
- **更优雅的方式 - 错误码映射**:
1927
- ```javascript
1928
- // errorCodeMap.js
1929
- const ERROR_HANDLERS = {
1930
- 40001: () => router.push('/account-not-found'),
1931
- 40002: (error) => showDialog('topup', error.params),
1932
- 50001: (error) => showDialog('payment', error.params),
1933
- 60001: () => showSystemErrorPage(),
1934
- };
1935
-
1936
- // 统一错误处理
1937
- function handleError(error) {
1938
- const handler = ERROR_HANDLERS[error.code];
1939
- if (handler) {
1940
- handler(error);
1941
- } else {
1942
- showGenericError(error.message);
1943
- }
1944
- }
1945
- ```
1946
-
1947
- ### 更多信息
1948
-
1949
- - [v1.1.5 完整变更日志](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md)
1950
- - [升级指南](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md#升级指南)
1951
- - [最佳实践](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md#最佳实践)
1952
-
1953
- ---
1954
-
1955
- ## 相关文档
1956
-
1957
- - [API 参考文档](./api-reference.md)
1958
- - [DSL 语法指南](./dsl-syntax.md)
1959
- - [String 扩展文档](./string-extensions.md)
1960
- - [多语言配置](./dynamic-locale.md)
1961
- - [v1.1.5 变更日志](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md)
1962
-
1963
- ---
1964
-
1965
- ## 对应示例文件
1966
-
1967
- **示例入口**: [error-handling.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/error-handling.ts)
1968
- **说明**: 覆盖 `validate()` 产生的字段错误、`I18nError` 业务错误对象、`toJSON()` 输出与错误码判断。
1969
-
1970
- ---
1971
-
1972
- **最后更新**: 2026-05-08
1973
- **版本**: v1.1.5
1974
-
1975
-
1
+ # schema-dsl 错误处理完整指南
2
+
3
+ > **更新**: 2026-01-30
4
+ > **版本**: v1.1.8+
5
+ > **适用**: 企业级应用开发
6
+
7
+ ---
8
+
9
+ ## 📋 目录
10
+
11
+ 1. [错误对象结构](#错误对象结构)
12
+ 2. [I18nError - 多语言错误抛出](#i18nerror---多语言错误抛出) 🆕
13
+ - [📖 概述](#-概述)
14
+ - [🚀 快速开始](#-快速开始)
15
+ - [📚 核心 API](#-核心-api)
16
+ - [🔧 配置语言包](#-配置语言包)
17
+ - [🌐 默认语言机制](#-默认语言机制)
18
+ - [智能参数识别(v1.1.8)](#智能参数识别v118)
19
+ - [🌐 实际场景](#-实际场景)
20
+ - [📦 错误对象结构](#-错误对象结构)
21
+ - [❓ 常见问题](#-常见问题)
22
+ 3. [错误消息定制](#错误消息定制)
23
+ 4. [错误码系统](#错误码系统)
24
+ 5. [多层级错误处理](#多层级错误处理)
25
+ 6. [API响应设计](#api响应设计)
26
+ 7. [前端错误展示](#前端错误展示)
27
+ 8. [错误日志记录](#错误日志记录)
28
+ 9. [最佳实践](#最佳实践)
29
+
30
+ ---
31
+
32
+ ## I18nError - 多语言错误抛出
33
+
34
+ ### 📖 概述
35
+
36
+ `I18nError` 是 schema-dsl 提供的**统一多语言错误抛出机制**,专为企业级应用设计。
37
+
38
+ **核心价值**:
39
+ - ✅ **多语言支持**: 一套代码,自动适配中文/英文/日文等
40
+ - ✅ **统一错误码**: 跨语言使用相同数字 code,前端处理不受语言影响
41
+ - ✅ **参数插值**: 支持 `{{#balance}}` 等动态参数
42
+ - ✅ **框架集成**: 与 Express/Koa 无缝集成
43
+ - ✅ **TypeScript 支持**: 完整的类型定义
44
+
45
+ **适用场景**:
46
+ - API 业务逻辑错误(账户不存在、余额不足、权限不足等)
47
+ - 多语言用户场景(国际化应用)
48
+ - 需要统一错误码的系统
49
+
50
+ **与 ValidationError 的区别**:
51
+ - `ValidationError`: 表单验证错误(字段级错误)
52
+ - `I18nError`: 业务逻辑错误(应用级错误)
53
+
54
+ ---
55
+
56
+ ### 🚀 快速开始
57
+
58
+ #### 5分钟上手
59
+
60
+ ```javascript
61
+ const { I18nError, Locale } = require('schema-dsl');
62
+
63
+ // 步骤1:配置语言包
64
+ Locale.addLocale('zh-CN', {
65
+ 'account.notFound': {
66
+ code: 40001,
67
+ message: '账户不存在'
68
+ }
69
+ });
70
+
71
+ Locale.addLocale('en-US', {
72
+ 'account.notFound': {
73
+ code: 40001,
74
+ message: 'Account not found'
75
+ }
76
+ });
77
+
78
+ // 步骤2:设置默认语言
79
+ Locale.setLocale('zh-CN');
80
+
81
+ // 步骤3:使用 I18nError
82
+ try {
83
+ I18nError.throw('account.notFound');
84
+ } catch (error) {
85
+ console.log(error.message); // "账户不存在"
86
+ console.log(error.code); // 40001
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ### 📚 核心 API
93
+
94
+ #### I18nError.create()
95
+
96
+ **创建错误对象(不抛出)**
97
+
98
+ ```javascript
99
+ /**
100
+ * @param {string} code - 错误代码(多语言 key)
101
+ * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码(智能识别)
102
+ * @param {number} statusCode - HTTP 状态码(默认 400)
103
+ * @param {string} locale - 语言环境(可选)
104
+ * @returns {I18nError} 错误实例
105
+ */
106
+ I18nError.create(code, paramsOrLocale?, statusCode?, locale?)
107
+ ```
108
+
109
+ **使用示例**:
110
+ ```javascript
111
+ // 基础用法
112
+ const error = I18nError.create('account.notFound');
113
+
114
+ // 带参数
115
+ const error = I18nError.create('account.insufficientBalance', {
116
+ balance: 50,
117
+ required: 100
118
+ });
119
+
120
+ // 指定状态码
121
+ const error = I18nError.create('user.notFound', {}, 404);
122
+
123
+ // 运行时指定语言(v1.1.8+)
124
+ const error = I18nError.create('account.notFound', 'en-US', 404);
125
+ ```
126
+
127
+ ---
128
+
129
+ #### I18nError.throw()
130
+
131
+ **直接抛出错误**
132
+
133
+ ```javascript
134
+ /**
135
+ * @param {string} code - 错误代码
136
+ * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码
137
+ * @param {number} statusCode - HTTP 状态码
138
+ * @param {string} locale - 语言环境
139
+ * @throws {I18nError}
140
+ */
141
+ I18nError.throw(code, paramsOrLocale?, statusCode?, locale?)
142
+ ```
143
+
144
+ **使用示例**:
145
+ ```javascript
146
+ // 直接抛错
147
+ I18nError.throw('user.noPermission');
148
+
149
+ // 带参数和状态码
150
+ I18nError.throw('account.insufficientBalance', { balance: 50, required: 100 }, 400);
151
+
152
+ // 简化语法(v1.1.8+)
153
+ I18nError.throw('account.notFound', 'zh-CN', 404);
154
+ ```
155
+
156
+ ---
157
+
158
+ #### I18nError.assert()
159
+
160
+ **断言风格 - 条件不满足时抛错**
161
+
162
+ ```javascript
163
+ /**
164
+ * @param {any} condition - 条件表达式(falsy 时抛错)
165
+ * @param {string} code - 错误代码
166
+ * @param {Object|string} paramsOrLocale - 参数对象 或 语言代码
167
+ * @param {number} statusCode - HTTP 状态码
168
+ * @param {string} locale - 语言环境
169
+ * @throws {I18nError} 条件为 false 时抛出
170
+ */
171
+ I18nError.assert(condition, code, paramsOrLocale?, statusCode?, locale?)
172
+ ```
173
+
174
+ **使用示例**:
175
+ ```javascript
176
+ function getAccount(id) {
177
+ const account = db.findAccount(id);
178
+
179
+ // 断言:账户必须存在
180
+ I18nError.assert(account, 'account.notFound', { id });
181
+
182
+ // 断言:余额必须充足
183
+ I18nError.assert(
184
+ account.balance >= 100,
185
+ 'account.insufficientBalance',
186
+ { balance: account.balance, required: 100 }
187
+ );
188
+
189
+ return account;
190
+ }
191
+ ```
192
+
193
+ ---
194
+
195
+ #### dsl.error 快捷方法
196
+
197
+ `dsl.error` 是 `I18nError` 的快捷访问方式,提供相同的三个方法:
198
+
199
+ ```javascript
200
+ const { dsl } = require('schema-dsl');
201
+
202
+ // 等价于 I18nError.create()
203
+ dsl.error.create('account.notFound');
204
+
205
+ // 等价于 I18nError.throw()
206
+ dsl.error.throw('order.notPaid');
207
+
208
+ // 等价于 I18nError.assert()
209
+ dsl.error.assert(order, 'order.notFound');
210
+ ```
211
+
212
+ **推荐使用场景**:
213
+ - ✅ 与 `dsl()` 函数一起使用时(风格统一)
214
+ - ✅ 导入较少依赖时(只需 `dsl`)
215
+
216
+ ---
217
+
218
+ ### 🔧 配置语言包
219
+
220
+ #### 方式1:使用 Locale.addLocale()(推荐)
221
+
222
+ ```javascript
223
+ const { Locale } = require('schema-dsl');
224
+
225
+ Locale.addLocale('zh-CN', {
226
+ // 字符串格式(简单场景)
227
+ 'user.notFound': '用户不存在',
228
+
229
+ // 对象格式(推荐,v1.1.5+)
230
+ 'account.notFound': {
231
+ code: 40001, // 数字错误码
232
+ message: '账户不存在'
233
+ },
234
+ 'account.insufficientBalance': {
235
+ code: 40002,
236
+ message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
237
+ }
238
+ });
239
+
240
+ Locale.addLocale('en-US', {
241
+ 'user.notFound': 'User not found',
242
+ 'account.notFound': {
243
+ code: 40001, // 相同的错误码
244
+ message: 'Account not found'
245
+ },
246
+ 'account.insufficientBalance': {
247
+ code: 40002,
248
+ message: 'Insufficient balance: {{#balance}}, required {{#required}}'
249
+ }
250
+ });
251
+ ```
252
+
253
+ ---
254
+
255
+ #### 方式2:使用 dsl.config()(批量配置)
256
+
257
+ ```javascript
258
+ const { dsl } = require('schema-dsl');
259
+
260
+ dsl.config({
261
+ i18n: {
262
+ 'zh-CN': {
263
+ 'payment.failed': {
264
+ code: 50001,
265
+ message: '支付失败:{{#reason}}'
266
+ }
267
+ },
268
+ 'en-US': {
269
+ 'payment.failed': {
270
+ code: 50001,
271
+ message: 'Payment failed: {{#reason}}'
272
+ }
273
+ }
274
+ }
275
+ });
276
+ ```
277
+
278
+ ---
279
+
280
+ #### 方式3:从目录加载(大型项目)
281
+
282
+ **目录结构**:
283
+ ```text
284
+ project/
285
+ ├── i18n/
286
+ │ └── errors/
287
+ │ ├── zh-CN.cjs
288
+ │ ├── en-US.jsonc
289
+ │ └── ja-JP.json5
290
+ └── app.js
291
+ ```
292
+
293
+ **配置**:
294
+ ```javascript
295
+ const path = require('path');
296
+
297
+ dsl.config({
298
+ i18n: path.join(__dirname, 'i18n/errors')
299
+ });
300
+ ```
301
+
302
+ **语言包文件**(例如 `i18n/errors/zh-CN.cjs`):
303
+ ```javascript
304
+ module.exports = {
305
+ 'account.notFound': {
306
+ code: 40001,
307
+ message: '账户不存在'
308
+ },
309
+ 'account.insufficientBalance': {
310
+ code: 40002,
311
+ message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
312
+ },
313
+ 'user.noPermission': {
314
+ code: 40003,
315
+ message: '您没有权限执行此操作'
316
+ }
317
+ };
318
+ ```
319
+
320
+ ---
321
+
322
+ ### 🌐 默认语言机制
323
+
324
+ #### 默认语言设置
325
+
326
+ **默认值**: `'en-US'`(英文)
327
+
328
+ **全局设置**:
329
+ ```javascript
330
+ const { Locale } = require('schema-dsl');
331
+
332
+ // 按应用需要切换默认语言
333
+ Locale.setLocale('zh-CN');
334
+
335
+ // 获取当前语言
336
+ console.log(Locale.getLocale()); // 'zh-CN'
337
+ ```
338
+
339
+ ---
340
+
341
+ #### 语言优先级规则
342
+
343
+ ```javascript
344
+ 运行时 locale 参数 > 全局 Locale.currentLocale > 默认 'en-US'
345
+ ```
346
+
347
+ **示例**:
348
+ ```javascript
349
+ // 场景1:使用全局语言
350
+ Locale.setLocale('zh-CN');
351
+ I18nError.throw('account.notFound'); // 使用中文 'zh-CN'
352
+
353
+ // 场景2:运行时覆盖
354
+ Locale.setLocale('zh-CN');
355
+ I18nError.throw('account.notFound', 'en-US'); // 覆盖为英文 'en-US'
356
+
357
+ // 场景3:参数对象 + 运行时语言
358
+ I18nError.throw('account.insufficientBalance',
359
+ { balance: 50, required: 100 }, // 参数对象
360
+ 400,
361
+ 'ja-JP' // 运行时指定日文
362
+ );
363
+ ```
364
+
365
+ ---
366
+
367
+ #### 实际应用 - API 多语言响应
368
+
369
+ ```javascript
370
+ const express = require('express');
371
+ const { I18nError } = require('schema-dsl');
372
+
373
+ const app = express();
374
+
375
+ // 中间件:提取客户端语言
376
+ app.use((req, res, next) => {
377
+ req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
378
+ next();
379
+ });
380
+
381
+ // API 路由
382
+ app.get('/api/account/:id', async (req, res) => {
383
+ try {
384
+ const account = await findAccount(req.params.id);
385
+
386
+ // 🎯 运行时指定语言(根据客户端请求)
387
+ I18nError.assert(account, 'account.notFound', req.locale, 404);
388
+
389
+ res.json({ success: true, data: account });
390
+ } catch (error) {
391
+ if (error instanceof I18nError) {
392
+ res.status(error.statusCode).json(error.toJSON());
393
+ } else {
394
+ res.status(500).json({ error: 'Internal Server Error' });
395
+ }
396
+ }
397
+ });
398
+ ```
399
+
400
+ **效果**:
401
+ - 客户端请求头 `Accept-Language: zh-CN` → 返回中文错误
402
+ - 客户端请求头 `Accept-Language: en-US` → 返回英文错误
403
+ - 无需修改业务代码,自动适配
404
+
405
+ ---
406
+
407
+ ### 智能参数识别(v1.1.8)
408
+
409
+ ### 智能参数识别(v1.1.8)
410
+
411
+ **v1.1.8 新增**:支持简化语法,智能识别第2个参数类型
412
+
413
+ #### 简化语法
414
+
415
+ ```javascript
416
+ const { dsl, Locale } = require('schema-dsl');
417
+
418
+ // 配置语言包
419
+ Locale.addLocale('zh-CN', {
420
+ 'account.notFound': {
421
+ code: 40001,
422
+ message: '账户不存在'
423
+ }
424
+ });
425
+
426
+ Locale.addLocale('en-US', {
427
+ 'account.notFound': {
428
+ code: 40001,
429
+ message: 'Account not found'
430
+ }
431
+ });
432
+
433
+ // ✅ 新增:简化语法(推荐)
434
+ dsl.error.throw('account.notFound', 'zh-CN');
435
+ dsl.error.throw('account.notFound', 'zh-CN', 404);
436
+
437
+ // ✅ 标准语法(完全兼容)
438
+ dsl.error.throw('account.notFound', {}, 404, 'zh-CN');
439
+ dsl.error.throw('account.notFound', { id: '123' }, 404, 'zh-CN');
440
+ ```
441
+
442
+ #### 智能识别规则
443
+
444
+ ```javascript
445
+ // 规则:自动判断第2个参数类型
446
+ typeof params === 'string' → 识别为语言参数
447
+ typeof params === 'object' → 识别为参数对象
448
+ params === null/undefined → 使用默认值
449
+ ```
450
+
451
+ #### 所有调用方式
452
+
453
+ ```javascript
454
+ // 1. 简化语法 - 只传语言
455
+ dsl.error.throw('account.notFound', 'zh-CN');
456
+ dsl.error.create('account.notFound', 'en-US');
457
+ dsl.error.assert(account, 'account.notFound', 'zh-CN');
458
+
459
+ // 2. 简化语法 - 语言 + 状态码
460
+ dsl.error.throw('account.notFound', 'zh-CN', 404);
461
+ dsl.error.assert(account, 'account.notFound', 'zh-CN', 404);
462
+
463
+ // 3. 标准语法 - 带参数对象
464
+ dsl.error.throw('account.insufficientBalance',
465
+ { balance: 50, required: 100 },
466
+ 400,
467
+ 'zh-CN'
468
+ );
469
+
470
+ // 4. 省略所有参数 - 使用全局语言
471
+ dsl.error.throw('account.notFound');
472
+ ```
473
+
474
+ #### 实际应用
475
+
476
+ ```javascript
477
+ // Express API
478
+ app.get('/api/account/:id', async (req, res) => {
479
+ try {
480
+ const account = await findAccount(req.params.id);
481
+ const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
482
+
483
+ // 🎯 简化语法:只需2个参数
484
+ dsl.error.assert(account, 'account.notFound', locale);
485
+
486
+ res.json(account);
487
+ } catch (error) {
488
+ res.status(error.statusCode).json(error.toJSON());
489
+ }
490
+ });
491
+ ```
492
+
493
+ ---
494
+
495
+ ### 🌐 实际场景
496
+
497
+ #### Express 完整集成
498
+
499
+ ```javascript
500
+ const express = require('express');
501
+ const { I18nError, Locale } = require('schema-dsl');
502
+
503
+ const app = express();
504
+ app.use(express.json());
505
+
506
+ // ========== 配置语言包 ==========
507
+ Locale.addLocale('zh-CN', {
508
+ 'account.notFound': {
509
+ code: 40001,
510
+ message: '账户不存在'
511
+ },
512
+ 'account.insufficientBalance': {
513
+ code: 40002,
514
+ message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
515
+ }
516
+ });
517
+
518
+ Locale.addLocale('en-US', {
519
+ 'account.notFound': {
520
+ code: 40001,
521
+ message: 'Account not found'
522
+ },
523
+ 'account.insufficientBalance': {
524
+ code: 40002,
525
+ message: 'Insufficient balance: {{#balance}}, required {{#required}}'
526
+ }
527
+ });
528
+
529
+ // ========== 中间件:提取语言 ==========
530
+ app.use((req, res, next) => {
531
+ req.locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
532
+ next();
533
+ });
534
+
535
+ // ========== 错误处理中间件 ==========
536
+ app.use((error, req, res, next) => {
537
+ if (error instanceof I18nError) {
538
+ return res.status(error.statusCode).json({
539
+ success: false,
540
+ error: error.toJSON()
541
+ });
542
+ }
543
+
544
+ // 其他错误
545
+ res.status(500).json({
546
+ success: false,
547
+ message: 'Internal Server Error'
548
+ });
549
+ });
550
+
551
+ // ========== 业务路由 ==========
552
+ app.get('/api/account/:id', async (req, res, next) => {
553
+ try {
554
+ const account = await findAccount(req.params.id);
555
+
556
+ // 使用运行时语言
557
+ I18nError.assert(account, 'account.notFound', req.locale, 404);
558
+
559
+ res.json({ success: true, data: account });
560
+ } catch (error) {
561
+ next(error);
562
+ }
563
+ });
564
+
565
+ app.post('/api/account/transfer', async (req, res, next) => {
566
+ try {
567
+ const { fromId, toId, amount } = req.body;
568
+ const account = await findAccount(fromId);
569
+
570
+ I18nError.assert(account, 'account.notFound', req.locale, 404);
571
+ I18nError.assert(
572
+ account.balance >= amount,
573
+ 'account.insufficientBalance',
574
+ { balance: account.balance, required: amount },
575
+ 400,
576
+ req.locale
577
+ );
578
+
579
+ await transferMoney(fromId, toId, amount);
580
+ res.json({ success: true });
581
+ } catch (error) {
582
+ next(error);
583
+ }
584
+ });
585
+ ```
586
+
587
+ ---
588
+
589
+ #### Koa 完整集成
590
+
591
+ ```javascript
592
+ const Koa = require('koa');
593
+ const { I18nError, Locale } = require('schema-dsl');
594
+
595
+ const app = new Koa();
596
+
597
+ // ========== 配置语言包 ==========
598
+ Locale.addLocale('zh-CN', {
599
+ 'user.noPermission': {
600
+ code: 40003,
601
+ message: '您没有权限执行此操作'
602
+ }
603
+ });
604
+
605
+ // ========== 中间件:提取语言 ==========
606
+ app.use(async (ctx, next) => {
607
+ ctx.locale = ctx.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
608
+ await next();
609
+ });
610
+
611
+ // ========== 错误处理中间件 ==========
612
+ app.use(async (ctx, next) => {
613
+ try {
614
+ await next();
615
+ } catch (error) {
616
+ if (error instanceof I18nError) {
617
+ ctx.status = error.statusCode;
618
+ ctx.body = {
619
+ success: false,
620
+ error: error.toJSON()
621
+ };
622
+ } else {
623
+ ctx.status = 500;
624
+ ctx.body = { success: false, message: 'Internal Server Error' };
625
+ }
626
+ }
627
+ });
628
+
629
+ // ========== 业务路由 ==========
630
+ app.use(async (ctx) => {
631
+ if (ctx.path === '/api/admin/users' && ctx.method === 'GET') {
632
+ const user = await getCurrentUser(ctx);
633
+
634
+ I18nError.assert(user.role === 'admin', 'user.noPermission', ctx.locale, 403);
635
+
636
+ ctx.body = { success: true, data: await getUsers() };
637
+ }
638
+ });
639
+ ```
640
+
641
+ ---
642
+
643
+ #### 原生 Node.js HTTP Server
644
+
645
+ ```javascript
646
+ const http = require('http');
647
+ const { I18nError, Locale } = require('schema-dsl');
648
+
649
+ // 配置语言包
650
+ Locale.addLocale('zh-CN', {
651
+ 'order.notPaid': {
652
+ code: 50001,
653
+ message: '订单未支付'
654
+ }
655
+ });
656
+
657
+ const server = http.createServer((req, res) => {
658
+ try {
659
+ // 提取语言
660
+ const locale = req.headers['accept-language']?.split(',')[0]?.trim() || 'zh-CN';
661
+
662
+ // 业务逻辑
663
+ const order = getOrder(req.url);
664
+ I18nError.assert(order && order.paid, 'order.notPaid', locale, 400);
665
+
666
+ res.writeHead(200, { 'Content-Type': 'application/json' });
667
+ res.end(JSON.stringify({ success: true, data: order }));
668
+ } catch (error) {
669
+ if (error instanceof I18nError) {
670
+ res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
671
+ res.end(JSON.stringify({
672
+ success: false,
673
+ error: error.toJSON()
674
+ }));
675
+ } else {
676
+ res.writeHead(500);
677
+ res.end('Internal Server Error');
678
+ }
679
+ }
680
+ });
681
+
682
+ server.listen(3000);
683
+ ```
684
+
685
+ ---
686
+
687
+ #### TypeScript 支持
688
+
689
+ ```typescript
690
+ import { I18nError, Locale } from 'schema-dsl';
691
+
692
+ // 类型安全的语言包配置
693
+ interface ErrorMessages {
694
+ [key: string]: {
695
+ code: number;
696
+ message: string;
697
+ };
698
+ }
699
+
700
+ const zhCN: ErrorMessages = {
701
+ 'account.notFound': {
702
+ code: 40001,
703
+ message: '账户不存在'
704
+ }
705
+ };
706
+
707
+ Locale.addLocale('zh-CN', zhCN);
708
+
709
+ // 使用类型守卫
710
+ function handleError(error: unknown): void {
711
+ if (error instanceof I18nError) {
712
+ console.log(`错误码: ${error.code}`);
713
+ console.log(`错误消息: ${error.message}`);
714
+ console.log(`HTTP状态: ${error.statusCode}`);
715
+ console.log(`语言: ${error.locale}`);
716
+ }
717
+ }
718
+
719
+ // 业务函数
720
+ async function getAccount(id: string): Promise<Account> {
721
+ const account = await findAccount(id);
722
+
723
+ I18nError.assert(account, 'account.notFound', { id }, 404);
724
+
725
+ return account;
726
+ }
727
+ ```
728
+
729
+ ---
730
+
731
+ ### 📦 错误对象结构
732
+
733
+ #### toJSON() 输出格式
734
+
735
+ ```javascript
736
+ try {
737
+ I18nError.throw('account.notFound', {}, 404);
738
+ } catch (error) {
739
+ console.log(error.toJSON());
740
+ }
741
+ ```
742
+
743
+ **输出**:
744
+ ```json
745
+ {
746
+ "error": "I18nError",
747
+ "originalKey": "account.notFound",
748
+ "code": 40001,
749
+ "message": "账户不存在",
750
+ "params": {},
751
+ "statusCode": 404,
752
+ "locale": "zh-CN"
753
+ }
754
+ ```
755
+
756
+ **字段说明**:
757
+ - `error`: 固定为 `"I18nError"`
758
+ - `originalKey`: 原始错误 key(v1.1.5 新增,用于日志追踪)
759
+ - `code`: 错误代码(数字或字符串)
760
+ - `message`: 已翻译的错误消息
761
+ - `params`: 参数对象
762
+ - `statusCode`: HTTP 状态码
763
+ - `locale`: 使用的语言
764
+
765
+ ---
766
+
767
+ #### 错误对象属性
768
+
769
+ ```javascript
770
+ try {
771
+ I18nError.throw('account.insufficientBalance',
772
+ { balance: 50, required: 100 },
773
+ 400,
774
+ 'zh-CN'
775
+ );
776
+ } catch (error) {
777
+ console.log(error.name); // 'I18nError'
778
+ console.log(error.message); // '余额不足,当前50元,需要100元'
779
+ console.log(error.originalKey); // 'account.insufficientBalance'
780
+ console.log(error.code); // 40002
781
+ console.log(error.params); // { balance: 50, required: 100 }
782
+ console.log(error.statusCode); // 400
783
+ console.log(error.locale); // 'zh-CN'
784
+ console.log(error.stack); // 堆栈跟踪
785
+ }
786
+ ```
787
+
788
+ ---
789
+
790
+ #### is() 方法 - 错误类型判断
791
+
792
+ ```javascript
793
+ try {
794
+ I18nError.throw('account.notFound');
795
+ } catch (error) {
796
+ if (error instanceof I18nError) {
797
+ // 使用 originalKey 判断
798
+ if (error.is('account.notFound')) {
799
+ console.log('账户不存在错误');
800
+ }
801
+
802
+ // 使用数字 code 判断(v1.1.5+)
803
+ if (error.is(40001)) {
804
+ console.log('账户不存在错误(通过数字码判断)');
805
+ }
806
+ }
807
+ }
808
+ ```
809
+
810
+ ---
811
+
812
+ ### ❓ 常见问题
813
+
814
+ #### Q1: 如何动态切换语言?
815
+
816
+ **A**: 有两种方式:
817
+
818
+ ```javascript
819
+ // 方式1:全局切换(影响所有后续调用)
820
+ Locale.setLocale('en-US');
821
+ I18nError.throw('account.notFound'); // 使用英文
822
+
823
+ // 方式2:运行时指定(只影响当次调用)
824
+ I18nError.throw('account.notFound', 'en-US'); // 使用英文
825
+ I18nError.throw('account.notFound', 'zh-CN'); // 使用中文
826
+ ```
827
+
828
+ **推荐**: 在 API 中根据客户端请求头动态指定(见上面的 Express 示例)
829
+
830
+ ---
831
+
832
+ #### Q2: 字符串格式和对象格式有什么区别?
833
+
834
+ **A**:
835
+
836
+ | 格式 | 优势 | 适用场景 |
837
+ |------|------|---------|
838
+ | 字符串 | 简单快捷 | 内部错误、不需要统一码 |
839
+ | 对象 | 统一错误码、跨语言一致 | 暴露给前端的错误、国际化 |
840
+
841
+ ```javascript
842
+ // 字符串格式
843
+ 'user.notFound': '用户不存在'
844
+
845
+ // 对象格式(推荐)
846
+ 'user.notFound': {
847
+ code: 40001, // 统一的数字码
848
+ message: '用户不存在'
849
+ }
850
+ ```
851
+
852
+ **建议**: 优先使用对象格式,便于前端统一处理。
853
+
854
+ ---
855
+
856
+ #### Q3: 参数插值如何使用?
857
+
858
+ **A**: 使用 `{{#参数名}}` 语法:
859
+
860
+ ```javascript
861
+ // 语言包配置
862
+ Locale.addLocale('zh-CN', {
863
+ 'account.insufficientBalance': {
864
+ code: 40002,
865
+ message: '余额不足,当前{{#balance}}元,需要{{#required}}元'
866
+ }
867
+ });
868
+
869
+ // 使用
870
+ I18nError.throw('account.insufficientBalance', {
871
+ balance: 50,
872
+ required: 100
873
+ });
874
+ // 输出: "余额不足,当前50元,需要100元"
875
+ ```
876
+
877
+ **注意**: 参数名必须用 `{{#参数名}}` 格式(井号必须有)。
878
+
879
+ ---
880
+
881
+ #### Q4: 与 dsl.if 的 message() 有什么区别?
882
+
883
+ **A**:
884
+
885
+ - `dsl.if().message()`: 用于**数据验证错误**(Schema 验证)
886
+ - `I18nError`: 用于**业务逻辑错误**(API 业务逻辑)
887
+
888
+ ```javascript
889
+ // dsl.if - 数据验证
890
+ dsl.if(d => !d).message('user.notFound').assert(user);
891
+
892
+ // I18nError - 业务逻辑
893
+ I18nError.assert(user.role === 'admin', 'user.noPermission');
894
+ ```
895
+
896
+ **可以混合使用**:
897
+ ```javascript
898
+ function validateAndProcess(user) {
899
+ // 步骤1:数据验证(使用 dsl.if)
900
+ dsl.if(d => !d).message('user.notFound').assert(user);
901
+
902
+ // 步骤2:业务逻辑验证(使用 I18nError)
903
+ I18nError.assert(user.role === 'admin', 'user.noPermission');
904
+ }
905
+ ```
906
+
907
+ ---
908
+
909
+ #### Q5: 如何获取所有可用语言?
910
+
911
+ **A**:
912
+
913
+ ```javascript
914
+ const { Locale } = require('schema-dsl');
915
+
916
+ const locales = Locale.getAvailableLocales();
917
+ console.log(locales); // ['en-US', 'zh-CN', 'ja-JP', ...]
918
+ ```
919
+
920
+ ---
921
+
922
+ #### Q6: 如何在前端统一处理错误码?
923
+
924
+ **A**: 使用数字 `code` 字段:
925
+
926
+ ```javascript
927
+ // 前端错误处理
928
+ async function apiCall() {
929
+ try {
930
+ const response = await fetch('/api/account');
931
+ const data = await response.json();
932
+ } catch (error) {
933
+ // 根据数字 code 统一处理(不受语言影响)
934
+ switch (error.code) {
935
+ case 40001:
936
+ router.push('/login'); // 账户不存在 → 跳转登录
937
+ break;
938
+ case 40002:
939
+ showTopUpDialog(); // 余额不足 → 显示充值弹窗
940
+ break;
941
+ case 40003:
942
+ showError('权限不足'); // 权限不足
943
+ break;
944
+ default:
945
+ showError(error.message);
946
+ }
947
+ }
948
+ }
949
+ ```
950
+
951
+ **优势**: 前端逻辑不受后端语言切换影响。
952
+
953
+ ---
954
+
955
+ #### Q7: 默认语言是什么?如何修改?
956
+
957
+ **A**:
958
+
959
+ - **默认语言**: `'en-US'`(英文)
960
+ - **修改方式**:
961
+
962
+ ```javascript
963
+ const { Locale } = require('schema-dsl');
964
+
965
+ // 启动时按应用需要设置默认语言
966
+ Locale.setLocale('zh-CN');
967
+
968
+ // 获取当前默认语言
969
+ console.log(Locale.getLocale()); // 'zh-CN'
970
+ ```
971
+
972
+ **建议**: 在应用启动时(app.js 入口)设置默认语言。
973
+
974
+ ---
975
+
976
+ #### Q8: 如何处理未配置的错误 key?
977
+
978
+ **A**: 如果错误 key 未在语言包中配置,会直接返回原始 key:
979
+
980
+ ```javascript
981
+ // 未配置 'custom.error'
982
+ I18nError.throw('custom.error');
983
+ // message: 'custom.error'(原样返回)
984
+ ```
985
+
986
+ **建议**:
987
+ 1. 使用 TypeScript 定义错误 key 类型,避免拼写错误
988
+ 2. 在开发环境检查是否所有错误 key 都已配置
989
+
990
+ ---
991
+
992
+ #### Q9: 支持哪些内置语言?
993
+
994
+ **A**:
995
+
996
+ | 语言代码 | 语言名称 | 支持状态 |
997
+ |---------|---------|---------|
998
+ | `en-US` | 英语(美国) | ✅ 内置 |
999
+ | `zh-CN` | 简体中文 | ✅ 内置 |
1000
+ | `ja-JP` | 日语 | ✅ 可扩展 |
1001
+ | `fr-FR` | 法语 | ✅ 可扩展 |
1002
+ | `es-ES` | 西班牙语 | ✅ 可扩展 |
1003
+
1004
+ **自定义语言**: 使用 `Locale.addLocale()` 添加任意语言。
1005
+
1006
+ ---
1007
+
1008
+ #### Q10: 如何在日志中记录错误详情?
1009
+
1010
+ **A**:
1011
+
1012
+ ```javascript
1013
+ const winston = require('winston');
1014
+
1015
+ app.use((error, req, res, next) => {
1016
+ if (error instanceof I18nError) {
1017
+ // 记录详细日志
1018
+ winston.error('业务错误', {
1019
+ originalKey: error.originalKey, // 原始 key(便于追踪)
1020
+ code: error.code, // 错误码
1021
+ message: error.message, // 已翻译的消息
1022
+ params: error.params, // 参数
1023
+ statusCode: error.statusCode,
1024
+ locale: error.locale,
1025
+ url: req.url,
1026
+ method: req.method,
1027
+ ip: req.ip
1028
+ });
1029
+
1030
+ return res.status(error.statusCode).json(error.toJSON());
1031
+ }
1032
+ next(error);
1033
+ });
1034
+ ```
1035
+
1036
+ **推荐**: 使用 `originalKey` 而非 `message`,因为 `message` 会随语言变化。
1037
+
1038
+ ---
1039
+
1040
+ ## 错误对象结构
1041
+
1042
+ ### 基础结构
1043
+
1044
+ schema-dsl 验证返回的错误对象结构:
1045
+
1046
+ ```javascript
1047
+ const { dsl, validate } = require('schema-dsl');
1048
+
1049
+ const schema = dsl({
1050
+ username: 'string:3-32!'.label('用户名')
1051
+ });
1052
+
1053
+ const result = validate(schema, { username: 'ab' });
1054
+
1055
+ // 返回结构
1056
+ {
1057
+ valid: false,
1058
+ errors: [
1059
+ {
1060
+ path: 'username',
1061
+ field: 'username',
1062
+ keyword: 'minLength',
1063
+ params: { limit: 3 },
1064
+ message: '用户名长度不能少于3个字符'
1065
+ }
1066
+ ]
1067
+ }
1068
+ ```
1069
+
1070
+ ### 嵌套对象错误
1071
+
1072
+ ```javascript
1073
+ const { dsl, validate } = require('schema-dsl');
1074
+
1075
+ const schema = dsl({
1076
+ user: {
1077
+ profile: {
1078
+ email: 'email!'
1079
+ }
1080
+ }
1081
+ });
1082
+
1083
+ const result = validate(schema, {
1084
+ user: {
1085
+ profile: {
1086
+ email: 'invalid'
1087
+ }
1088
+ }
1089
+ });
1090
+
1091
+ // 错误路径
1092
+ console.log(result.errors[0].path); // 'user/profile/email'
1093
+ console.log(result.errors[0].message); // '邮箱必须是有效的邮箱地址'
1094
+ ```
1095
+
1096
+ ### 数组项错误
1097
+
1098
+ ```javascript
1099
+ const { dsl, validate } = require('schema-dsl');
1100
+
1101
+ const schema = dsl({
1102
+ items: 'array<string:3->!'
1103
+ });
1104
+
1105
+ const result = validate(schema, {
1106
+ items: ['ab', 'valid']
1107
+ });
1108
+
1109
+ // 错误路径
1110
+ console.log(result.errors[0].path); // 'items/0'
1111
+ ```
1112
+
1113
+ ---
1114
+
1115
+ ## 错误消息定制
1116
+
1117
+ ### 单字段定制
1118
+
1119
+ ```javascript
1120
+ const { dsl } = require('schema-dsl');
1121
+
1122
+ // 使用 String 扩展定制消息
1123
+ const schema = dsl({
1124
+ username: 'string:3-32!'
1125
+ .label('用户名')
1126
+ .messages({
1127
+ 'min': '太短了!至少要3个字符'
1128
+ })
1129
+ });
1130
+ ```
1131
+
1132
+ ### 多规则定制
1133
+
1134
+ ```javascript
1135
+ const { dsl } = require('schema-dsl');
1136
+
1137
+ const schema = dsl({
1138
+ email: 'email!'
1139
+ .label('邮箱地址')
1140
+ .messages({
1141
+ 'format': '邮箱格式不对哦',
1142
+ 'required': '邮箱不能为空'
1143
+ })
1144
+ });
1145
+ ```
1146
+
1147
+ ### 对象级定制
1148
+
1149
+ ```javascript
1150
+ const { dsl } = require('schema-dsl');
1151
+
1152
+ const schema = dsl({
1153
+ username: 'string:3-32!'
1154
+ .label('用户名')
1155
+ .messages({
1156
+ 'min': '{{#label}}至少{{#limit}}个字符',
1157
+ 'max': '{{#label}}最多{{#limit}}个字符'
1158
+ }),
1159
+
1160
+ email: 'email!'
1161
+ .label('邮箱')
1162
+ .messages({
1163
+ 'format': '{{#label}}格式无效'
1164
+ })
1165
+ });
1166
+ ```
1167
+
1168
+ ### 全局定制
1169
+
1170
+ ```javascript
1171
+ const { Locale } = require('schema-dsl');
1172
+
1173
+ // 设置全局消息
1174
+ Locale.setMessages({
1175
+ 'min': '输入太短,要{{#limit}}个字符',
1176
+ 'format': '格式不正确'
1177
+ });
1178
+ ```
1179
+
1180
+ ---
1181
+
1182
+ ## 错误码系统
1183
+
1184
+ ### 内置错误码(简化版)
1185
+
1186
+ schema-dsl 对 Ajv 的错误关键字进行了统一格式化,使其更易用:
1187
+
1188
+ #### 字符串错误码
1189
+
1190
+ | 关键字 | 原始关键字 | 说明 | params |
1191
+ |--------|-----------|------|--------|
1192
+ | `min` | `minLength` | 长度小于最小值 | { limit: number } |
1193
+ | `max` | `maxLength` | 长度大于最大值 | { limit: number } |
1194
+ | `format` | `format` | 格式验证失败 | { format: 'email'/'uri'/etc } |
1195
+ | `pattern` | `pattern` | 正则不匹配 | { pattern: string } |
1196
+ | `enum` | `enum` | 不在枚举值中 | { allowedValues: array } |
1197
+
1198
+ #### 数字错误码
1199
+
1200
+ | 关键字 | 原始关键字 | 说明 | params |
1201
+ |--------|-----------|------|--------|
1202
+ | `min` | `minimum` | 小于最小值 | { limit: number } |
1203
+ | `max` | `maximum` | 大于最大值 | { limit: number } |
1204
+
1205
+ #### 通用错误码
1206
+
1207
+ | 关键字 | 说明 | params |
1208
+ |--------|------|--------|
1209
+ | `required` | 必填字段缺失 | { missingProperty: string } |
1210
+ | `type` | 类型不匹配 | { type: string } |
1211
+
1212
+ **💡 提示**: 您可以使用简化关键字(如 `min`)或原始关键字(如 `minLength`)来定制错误消息,系统会自动处理映射。
1213
+
1214
+ ### 自动 Label 翻译
1215
+
1216
+ 如果您在语言包中定义了 `label.{fieldName}`,系统会自动将其作为 Label 使用,无需显式调用 `.label()`。
1217
+
1218
+ ```javascript
1219
+ // 语言包
1220
+ Locale.addLocale('zh-CN', {
1221
+ 'label.username': '用户名',
1222
+ 'required': '{{#label}}不能为空'
1223
+ });
1224
+
1225
+ // Schema
1226
+ const schema = dsl({
1227
+ username: 'string!' // 自动查找 label.username
1228
+ });
1229
+
1230
+ // 错误消息: "用户名不能为空"
1231
+ ```
1232
+
1233
+ ### 自定义验证错误
1234
+
1235
+ ```javascript
1236
+ const { dsl } = require('schema-dsl');
1237
+
1238
+ const schema = dsl({
1239
+ username: 'string:3-32!'
1240
+ .custom((value) => {
1241
+ if (value.includes('forbidden')) {
1242
+ return '内容包含禁止的词语';
1243
+ }
1244
+ // 验证通过时无需返回
1245
+ })
1246
+ .label('用户名')
1247
+ });
1248
+ ```
1249
+
1250
+ ---
1251
+
1252
+ ## 多层级错误处理
1253
+
1254
+ ### 嵌套对象验证
1255
+
1256
+ ```javascript
1257
+ const { dsl, validate } = require('schema-dsl');
1258
+
1259
+ const schema = dsl({
1260
+ user: {
1261
+ name: 'string:1-100!',
1262
+ address: {
1263
+ country: 'string!'.label('国家'),
1264
+ city: 'string!'.label('城市'),
1265
+ street: 'string!'.label('街道')
1266
+ }
1267
+ }
1268
+ });
1269
+
1270
+ const result = validate(schema, {
1271
+ user: {
1272
+ name: 'John',
1273
+ address: {
1274
+ country: 'CN'
1275
+ // 缺少city和street
1276
+ }
1277
+ }
1278
+ });
1279
+
1280
+ // 错误示例
1281
+ // result.errors[0].path: 'user/address/city'
1282
+ // result.errors[1].path: 'user/address/street'
1283
+ ```
1284
+
1285
+ ### 数组验证
1286
+
1287
+ ```javascript
1288
+ const { dsl, validate } = require('schema-dsl');
1289
+
1290
+ const schema = dsl({
1291
+ items: 'array:1-<string:3->!'
1292
+ .label('商品列表')
1293
+ });
1294
+
1295
+ const result = validate(schema, {
1296
+ items: ['ab', 'valid'] // 第一项太短
1297
+ });
1298
+
1299
+ // 错误路径
1300
+ console.log(result.errors[0].path); // 'items/0'
1301
+ ```
1302
+
1303
+ ---
1304
+
1305
+ ## API响应设计
1306
+
1307
+ ### 标准响应格式
1308
+
1309
+ ```javascript
1310
+ // 成功响应
1311
+ {
1312
+ success: true,
1313
+ code: 'SUCCESS',
1314
+ data: { ... }
1315
+ }
1316
+
1317
+ // 验证错误响应
1318
+ {
1319
+ success: false,
1320
+ code: 'VALIDATION_ERROR',
1321
+ message: '数据验证失败',
1322
+ errors: [
1323
+ {
1324
+ field: 'username',
1325
+ message: 'must NOT have fewer than 3 characters',
1326
+ keyword: 'minLength',
1327
+ params: { limit: 3 }
1328
+ }
1329
+ ]
1330
+ }
1331
+
1332
+ // 服务器错误响应
1333
+ {
1334
+ success: false,
1335
+ code: 'SERVER_ERROR',
1336
+ message: '服务器内部错误'
1337
+ }
1338
+ ```
1339
+
1340
+ ### Express中间件
1341
+
1342
+ ```javascript
1343
+ const { dsl, Validator } = require('schema-dsl');
1344
+
1345
+ // 验证中间件
1346
+ function validateBody(schema) {
1347
+ const validator = new Validator();
1348
+
1349
+ return (req, res, next) => {
1350
+ const result = validator.validate(schema, req.body);
1351
+
1352
+ if (!result.valid) {
1353
+ return res.status(400).json({
1354
+ success: false,
1355
+ code: 'VALIDATION_ERROR',
1356
+ message: '请检查输入信息',
1357
+ errors: result.errors.map(err => ({
1358
+ field: err.path.replace(/\//g, '.'),
1359
+ message: err.message,
1360
+ keyword: err.keyword,
1361
+ params: err.params
1362
+ }))
1363
+ });
1364
+ }
1365
+
1366
+ // 验证通过,继续处理
1367
+ next();
1368
+ };
1369
+ }
1370
+
1371
+ // 使用示例
1372
+ const userSchema = dsl({
1373
+ username: 'string:3-32!',
1374
+ email: 'email!',
1375
+ password: 'string:8-64!'
1376
+ });
1377
+
1378
+ app.post('/api/users',
1379
+ validateBody(userSchema),
1380
+ async (req, res) => {
1381
+ const user = await createUser(req.body);
1382
+ res.json({ success: true, data: user });
1383
+ }
1384
+ );
1385
+ ```
1386
+
1387
+ ### Koa中间件
1388
+
1389
+ ```javascript
1390
+ const { dsl, Validator } = require('schema-dsl');
1391
+
1392
+ function validateBody(schema) {
1393
+ const validator = new Validator();
1394
+
1395
+ return async (ctx, next) => {
1396
+ const result = validator.validate(schema, ctx.request.body);
1397
+
1398
+ if (!result.valid) {
1399
+ ctx.status = 400;
1400
+ ctx.body = {
1401
+ success: false,
1402
+ code: 'VALIDATION_ERROR',
1403
+ message: '数据验证失败',
1404
+ errors: result.errors.map(err => ({
1405
+ field: err.path.replace(/\//g, '.'),
1406
+ message: err.message,
1407
+ keyword: err.keyword
1408
+ }))
1409
+ };
1410
+ return;
1411
+ }
1412
+
1413
+ await next();
1414
+ };
1415
+ }
1416
+
1417
+ // 使用示例
1418
+ const registerSchema = dsl({
1419
+ username: 'string:3-32!'.username(),
1420
+ email: 'email!',
1421
+ password: 'string!'.password('strong')
1422
+ });
1423
+
1424
+ router.post('/register', validateBody(registerSchema), async (ctx) => {
1425
+ ctx.body = { success: true, data: await register(ctx.request.body) };
1426
+ });
1427
+ ```
1428
+
1429
+ ---
1430
+
1431
+ ## 前端错误展示
1432
+
1433
+ ### React示例
1434
+
1435
+ ```javascript
1436
+ import React, { useState } from 'react';
1437
+
1438
+ function RegisterForm() {
1439
+ const [errors, setErrors] = useState({});
1440
+
1441
+ const handleSubmit = async (e) => {
1442
+ e.preventDefault();
1443
+
1444
+ try {
1445
+ const response = await fetch('/api/register', {
1446
+ method: 'POST',
1447
+ headers: { 'Content-Type': 'application/json' },
1448
+ body: JSON.stringify(formData)
1449
+ });
1450
+
1451
+ const data = await response.json();
1452
+
1453
+ if (!data.success && data.code === 'VALIDATION_ERROR') {
1454
+ // 将错误数组转为对象
1455
+ const errorMap = {};
1456
+ data.errors.forEach(err => {
1457
+ errorMap[err.field] = err.message;
1458
+ });
1459
+ setErrors(errorMap);
1460
+ }
1461
+
1462
+ } catch (error) {
1463
+ console.error(error);
1464
+ }
1465
+ };
1466
+
1467
+ return (
1468
+ <form onSubmit={handleSubmit}>
1469
+ <div>
1470
+ <input name="username" />
1471
+ {errors.username && (
1472
+ <span className="error">{errors.username}</span>
1473
+ )}
1474
+ </div>
1475
+
1476
+ <div>
1477
+ <input name="email" type="email" />
1478
+ {errors.email && (
1479
+ <span className="error">{errors.email}</span>
1480
+ )}
1481
+ </div>
1482
+
1483
+ <button type="submit">注册</button>
1484
+ </form>
1485
+ );
1486
+ }
1487
+ ```
1488
+
1489
+ ### Vue示例
1490
+
1491
+ ```vue
1492
+ <template>
1493
+ <form @submit.prevent="handleSubmit">
1494
+ <div>
1495
+ <input v-model="form.username" />
1496
+ <span v-if="errors.username" class="error">
1497
+ {{ errors.username }}
1498
+ </span>
1499
+ </div>
1500
+
1501
+ <div>
1502
+ <input v-model="form.email" type="email" />
1503
+ <span v-if="errors.email" class="error">
1504
+ {{ errors.email }}
1505
+ </span>
1506
+ </div>
1507
+
1508
+ <button type="submit">注册</button>
1509
+ </form>
1510
+ </template>
1511
+
1512
+ <script>
1513
+ export default {
1514
+ data() {
1515
+ return {
1516
+ form: {
1517
+ username: '',
1518
+ email: ''
1519
+ },
1520
+ errors: {}
1521
+ };
1522
+ },
1523
+ methods: {
1524
+ async handleSubmit() {
1525
+ try {
1526
+ const response = await fetch('/api/register', {
1527
+ method: 'POST',
1528
+ headers: { 'Content-Type': 'application/json' },
1529
+ body: JSON.stringify(this.form)
1530
+ });
1531
+
1532
+ const data = await response.json();
1533
+
1534
+ if (!data.success && data.code === 'VALIDATION_ERROR') {
1535
+ this.errors = data.errors.reduce((acc, err) => {
1536
+ acc[err.field] = err.message;
1537
+ return acc;
1538
+ }, {});
1539
+ }
1540
+
1541
+ } catch (error) {
1542
+ console.error(error);
1543
+ }
1544
+ }
1545
+ }
1546
+ };
1547
+ </script>
1548
+ ```
1549
+
1550
+ ---
1551
+
1552
+ ## 错误日志记录
1553
+
1554
+ ### 基础日志
1555
+
1556
+ ```javascript
1557
+ app.post('/api/register', async (req, res) => {
1558
+ const result = await registerSchema.validate(req.body, {
1559
+ abortEarly: false
1560
+ });
1561
+
1562
+ if (!result.isValid) {
1563
+ // 记录验证错误
1564
+ logger.warn('用户注册验证失败', {
1565
+ ip: req.ip,
1566
+ errors: result.errors,
1567
+ data: req.body
1568
+ });
1569
+
1570
+ return res.status(400).json({
1571
+ success: false,
1572
+ errors: result.errors
1573
+ });
1574
+ }
1575
+
1576
+ // 继续处理
1577
+ });
1578
+ ```
1579
+
1580
+ ### 结构化日志
1581
+
1582
+ ```javascript
1583
+ const logger = require('winston');
1584
+
1585
+ function logValidationError(req, result) {
1586
+ logger.warn({
1587
+ message: '验证失败',
1588
+ type: 'VALIDATION_ERROR',
1589
+ timestamp: new Date().toISOString(),
1590
+ ip: req.ip,
1591
+ url: req.url,
1592
+ method: req.method,
1593
+ errors: result.errors.map(err => ({
1594
+ path: err.path.replace(/\//g, '.'),
1595
+ type: err.type,
1596
+ message: err.message
1597
+ })),
1598
+ // 敏感数据脱敏
1599
+ data: maskSensitiveData(req.body)
1600
+ });
1601
+ }
1602
+ ```
1603
+
1604
+ ---
1605
+
1606
+ ## 最佳实践
1607
+
1608
+ ### 1. 使用 label 让错误消息更清晰
1609
+
1610
+ ```javascript
1611
+ const { dsl } = require('schema-dsl');
1612
+
1613
+ // ✅ 推荐:使用 label
1614
+ const schema = dsl({
1615
+ username: 'string:3-32!'.label('用户名')
1616
+ });
1617
+ // 错误消息会包含"用户名"标签
1618
+
1619
+ // ❌ 不推荐:不使用 label
1620
+ const schema = dsl({
1621
+ username: 'string:3-32!'
1622
+ });
1623
+ // 错误消息只显示字段名 "username"
1624
+ ```
1625
+
1626
+ ### 2. 提供友好的中文错误消息
1627
+
1628
+ ```javascript
1629
+ const { dsl } = require('schema-dsl');
1630
+
1631
+ // ✅ 推荐:自定义中文消息
1632
+ const schema = dsl({
1633
+ username: 'string:3-32!'
1634
+ .label('用户名')
1635
+ .messages({
1636
+ 'minLength': '{{#label}}至少需要{{#limit}}个字符',
1637
+ 'maxLength': '{{#label}}最多{{#limit}}个字符'
1638
+ })
1639
+ });
1640
+
1641
+ // ❌ 不推荐:使用默认英文消息
1642
+ const schema = dsl({
1643
+ username: 'string:3-32!'
1644
+ });
1645
+ ```
1646
+
1647
+ ### 3. 使用自定义验证实现业务逻辑
1648
+
1649
+ ```javascript
1650
+ const { dsl } = require('schema-dsl');
1651
+
1652
+ // ✅ 推荐:返回错误消息字符串
1653
+ const schema = dsl({
1654
+ username: 'string:3-32!'
1655
+ .custom((value) => {
1656
+ if (value === 'admin') {
1657
+ return '用户名已被占用';
1658
+ }
1659
+ // 验证通过时无需返回
1660
+ })
1661
+ .label('用户名')
1662
+ });
1663
+ ```
1664
+
1665
+ ### 4. 敏感数据不要出现在错误日志中
1666
+
1667
+ ```javascript
1668
+ function maskSensitiveData(data) {
1669
+ return {
1670
+ ...data,
1671
+ password: '***',
1672
+ confirmPassword: '***',
1673
+ creditCard: data.creditCard ? '****' + data.creditCard.slice(-4) : undefined
1674
+ };
1675
+ }
1676
+
1677
+ // 使用
1678
+ logger.warn('验证失败', {
1679
+ errors: result.errors,
1680
+ data: maskSensitiveData(req.body)
1681
+ });
1682
+ ```
1683
+
1684
+ ### 5. 统一错误格式便于前端处理
1685
+
1686
+ ```javascript
1687
+ // 统一的错误格式化函数
1688
+ function formatValidationErrors(errors) {
1689
+ return errors.map(err => ({
1690
+ field: err.path.replace(/\//g, '.'),
1691
+ message: err.message,
1692
+ keyword: err.keyword,
1693
+ params: err.params
1694
+ }));
1695
+ }
1696
+
1697
+ // 使用
1698
+ if (!result.valid) {
1699
+ return res.status(400).json({
1700
+ success: false,
1701
+ code: 'VALIDATION_ERROR',
1702
+ errors: formatValidationErrors(result.errors)
1703
+ });
1704
+ }
1705
+ ```
1706
+
1707
+ ---
1708
+
1709
+ ## v1.1.5 新功能:对象格式错误配置
1710
+
1711
+ ### 概述
1712
+
1713
+ 从 v1.1.5 开始,语言包支持对象格式 `{ code, message }`,实现统一的错误代码管理。
1714
+
1715
+ ### 基础用法
1716
+
1717
+ **语言包配置**:
1718
+ ```javascript
1719
+ // i18n/errors/zh-CN.cjs(或任意 .json/.jsonc/.json5 自定义语言包文件)
1720
+ module.exports = {
1721
+ // 字符串格式(向后兼容)
1722
+ 'user.notFound': '用户不存在',
1723
+
1724
+ // 对象格式(v1.1.5 新增)✨ - 使用数字错误码
1725
+ 'account.notFound': {
1726
+ code: 40001,
1727
+ message: '账户不存在'
1728
+ },
1729
+ 'account.insufficientBalance': {
1730
+ code: 40002,
1731
+ message: '余额不足,当前余额{{#balance}},需要{{#required}}'
1732
+ },
1733
+ 'order.notPaid': {
1734
+ code: 50001,
1735
+ message: '订单未支付'
1736
+ }
1737
+ };
1738
+ ```
1739
+
1740
+ **使用示例**:
1741
+ ```javascript
1742
+ const { dsl } = require('schema-dsl');
1743
+
1744
+ try {
1745
+ dsl.error.throw('account.notFound');
1746
+ } catch (error) {
1747
+ console.log(error.originalKey); // 'account.notFound'
1748
+ console.log(error.code); // 40001 ✨ 数字错误码
1749
+ console.log(error.message); // '账户不存在'
1750
+ }
1751
+ ```
1752
+
1753
+ ### 核心特性
1754
+
1755
+ #### 1. originalKey 字段(新增)
1756
+
1757
+ 保留原始的 key,便于调试和日志追踪:
1758
+
1759
+ ```javascript
1760
+ try {
1761
+ dsl.error.throw('account.notFound');
1762
+ } catch (error) {
1763
+ error.originalKey // 'account.notFound' (原始 key)
1764
+ error.code // 40001 (数字错误码)
1765
+ }
1766
+ ```
1767
+
1768
+ #### 2. 多语言共享 code
1769
+
1770
+ 不同语言使用相同的数字 `code`,便于前端统一处理:
1771
+
1772
+ ```javascript
1773
+ // zh-CN.cjs
1774
+ 'account.notFound': {
1775
+ code: 40001, // ← 数字 code 一致
1776
+ message: '账户不存在'
1777
+ }
1778
+
1779
+ // en-US.cjs
1780
+ 'account.notFound': {
1781
+ code: 40001, // ← 数字 code 一致
1782
+ message: 'Account not found'
1783
+ }
1784
+
1785
+ // 前端处理 - 不受语言影响
1786
+ switch (error.code) {
1787
+ case 40001:
1788
+ redirectToLogin();
1789
+ break;
1790
+ case 40002:
1791
+ showTopUpDialog();
1792
+ break;
1793
+ case 50001:
1794
+ showPaymentDialog();
1795
+ break;
1796
+ }
1797
+ ```
1798
+
1799
+ #### 3. 增强的 error.is() 方法
1800
+
1801
+ 同时支持 `originalKey` 和数字 `code` 判断:
1802
+
1803
+ ```javascript
1804
+ try {
1805
+ dsl.error.throw('account.notFound');
1806
+ } catch (error) {
1807
+ // 两种方式都可以
1808
+ if (error.is('account.notFound')) { } // ✅ 使用 originalKey
1809
+ if (error.is(40001)) { } // ✅ 使用数字 code
1810
+ }
1811
+ ```
1812
+
1813
+ #### 4. toJSON 包含 originalKey
1814
+
1815
+ ```javascript
1816
+ const json = error.toJSON();
1817
+ // {
1818
+ // error: 'I18nError',
1819
+ // originalKey: 'account.notFound', // ✨ v1.1.5 新增
1820
+ // code: 'ACCOUNT_NOT_FOUND',
1821
+ // message: '账户不存在',
1822
+ // params: {},
1823
+ // statusCode: 400,
1824
+ // locale: 'zh-CN'
1825
+ // }
1826
+ ```
1827
+
1828
+ ### 向后兼容
1829
+
1830
+ **完全向后兼容** ✅ - 字符串格式自动转换:
1831
+
1832
+ ```javascript
1833
+ // 字符串格式(原有)
1834
+ 'user.notFound': '用户不存在'
1835
+
1836
+ // 自动转换为对象
1837
+ dsl.error.throw('user.notFound');
1838
+ // error.code = 'user.notFound' (使用 key 作为 code)
1839
+ // error.originalKey = 'user.notFound'
1840
+ // error.message = '用户不存在'
1841
+ ```
1842
+
1843
+ ### 最佳实践
1844
+
1845
+ #### 1. 何时使用对象格式
1846
+
1847
+ **推荐使用对象格式**:
1848
+ - ✅ 需要在多语言中统一处理的错误
1849
+ - ✅ 需要前端统一判断的错误
1850
+ - ✅ 核心业务错误(账户、订单、支付等)
1851
+
1852
+ **可以使用字符串格式**:
1853
+ - ✅ 简单的验证错误
1854
+ - ✅ 内部错误(不暴露给前端)
1855
+ - ✅ 不需要统一处理的错误
1856
+
1857
+ #### 2. 错误代码命名规范
1858
+
1859
+ 推荐使用**数字错误码**,按模块分段:
1860
+
1861
+ ```javascript
1862
+ // 错误码规范(5位数字)
1863
+ // 4xxxx - 客户端错误
1864
+ // 5xxxx - 业务逻辑错误
1865
+ // 6xxxx - 系统错误
1866
+
1867
+ 'account.notFound': {
1868
+ code: 40001, // ✅ 推荐:账户模块,序号001
1869
+ message: '账户不存在'
1870
+ }
1871
+
1872
+ 'account.insufficientBalance': {
1873
+ code: 40002, // 账户模块,序号002
1874
+ message: '余额不足'
1875
+ }
1876
+
1877
+ 'order.notPaid': {
1878
+ code: 50001, // ✅ 订单模块,序号001
1879
+ message: '订单未支付'
1880
+ }
1881
+
1882
+ 'order.cancelled': {
1883
+ code: 50002, // 订单模块,序号002
1884
+ message: '订单已取消'
1885
+ }
1886
+
1887
+ 'database.connectionError': {
1888
+ code: 60001, // ✅ 系统错误
1889
+ message: '数据库连接失败'
1890
+ }
1891
+ ```
1892
+
1893
+ **错误码分段建议**:
1894
+ - `40001-49999` - 客户端错误(账户、权限、参数验证等)
1895
+ - `50001-59999` - 业务逻辑错误(订单、支付、库存等)
1896
+ - `60001-69999` - 系统错误(数据库、服务不可用等)
1897
+
1898
+ #### 3. 前端统一错误处理
1899
+
1900
+ ```javascript
1901
+ // API 调用
1902
+ try {
1903
+ const response = await fetch('/api/account');
1904
+ const data = await response.json();
1905
+ } catch (error) {
1906
+ // 使用数字 code 统一处理,不受语言影响
1907
+ switch (error.code) {
1908
+ case 40001: // ACCOUNT_NOT_FOUND
1909
+ showNotFoundPage();
1910
+ break;
1911
+ case 40002: // INSUFFICIENT_BALANCE
1912
+ showTopUpDialog(error.params);
1913
+ break;
1914
+ case 50001: // ORDER_NOT_PAID
1915
+ showPaymentDialog();
1916
+ break;
1917
+ case 60001: // SYSTEM_ERROR
1918
+ showSystemErrorPage();
1919
+ break;
1920
+ default:
1921
+ showGenericError(error.message);
1922
+ }
1923
+ }
1924
+ ```
1925
+
1926
+ **更优雅的方式 - 错误码映射**:
1927
+ ```javascript
1928
+ // errorCodeMap.js
1929
+ const ERROR_HANDLERS = {
1930
+ 40001: () => router.push('/account-not-found'),
1931
+ 40002: (error) => showDialog('topup', error.params),
1932
+ 50001: (error) => showDialog('payment', error.params),
1933
+ 60001: () => showSystemErrorPage(),
1934
+ };
1935
+
1936
+ // 统一错误处理
1937
+ function handleError(error) {
1938
+ const handler = ERROR_HANDLERS[error.code];
1939
+ if (handler) {
1940
+ handler(error);
1941
+ } else {
1942
+ showGenericError(error.message);
1943
+ }
1944
+ }
1945
+ ```
1946
+
1947
+ ### 更多信息
1948
+
1949
+ - [v1.1.5 完整变更日志](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md)
1950
+ - [升级指南](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md#升级指南)
1951
+ - [最佳实践](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md#最佳实践)
1952
+
1953
+ ---
1954
+
1955
+ ## 相关文档
1956
+
1957
+ - [API 参考文档](./api-reference.md)
1958
+ - [DSL 语法指南](./dsl-syntax.md)
1959
+ - [String 扩展文档](./string-extensions.md)
1960
+ - [多语言配置](./dynamic-locale.md)
1961
+ - [v1.1.5 变更日志](https://github.com/vextjs/schema-dsl/blob/main/changelogs/v1.1.5.md)
1962
+
1963
+ ---
1964
+
1965
+ ## 对应示例文件
1966
+
1967
+ **示例入口**: [error-handling.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/error-handling.ts)
1968
+ **说明**: 覆盖 `validate()` 产生的字段错误、`I18nError` 业务错误对象、`toJSON()` 输出与错误码判断。
1969
+
1970
+ ---
1971
+
1972
+ **最后更新**: 2026-05-08
1973
+ **版本**: v1.1.5
1974
+
1975
+