schema-dsl 1.2.0 → 1.2.2

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.
@@ -10,9 +10,15 @@
10
10
 
11
11
  1. [错误对象结构](#错误对象结构)
12
12
  2. [I18nError - 多语言错误抛出](#i18nerror---多语言错误抛出) 🆕
13
+ - [📖 概述](#-概述)
14
+ - [🚀 快速开始](#-快速开始)
15
+ - [📚 核心 API](#-核心-api)
16
+ - [🔧 配置语言包](#-配置语言包)
17
+ - [🌐 默认语言机制](#-默认语言机制)
13
18
  - [智能参数识别(v1.1.8)](#智能参数识别v118)
14
- - [简化语法](#简化语法)
15
- - [所有调用方式](#所有调用方式)
19
+ - [🌐 实际场景](#-实际场景)
20
+ - [📦 错误对象结构](#-错误对象结构)
21
+ - [❓ 常见问题](#-常见问题)
16
22
  3. [错误消息定制](#错误消息定制)
17
23
  4. [错误码系统](#错误码系统)
18
24
  5. [多层级错误处理](#多层级错误处理)
@@ -25,6 +31,381 @@
25
31
 
26
32
  ## I18nError - 多语言错误抛出
27
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
+ ```
284
+ project/
285
+ ├── lib/
286
+ │ └── locales/
287
+ │ ├── zh-CN.js
288
+ │ ├── en-US.js
289
+ │ └── ja-JP.js
290
+ └── app.js
291
+ ```
292
+
293
+ **配置**:
294
+ ```javascript
295
+ const path = require('path');
296
+
297
+ dsl.config({
298
+ i18n: path.join(__dirname, 'lib/locales')
299
+ });
300
+ ```
301
+
302
+ **语言包文件** (`lib/locales/zh-CN.js`):
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] || '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
+
28
409
  ### 智能参数识别(v1.1.8)
29
410
 
30
411
  **v1.1.8 新增**:支持简化语法,智能识别第2个参数类型
@@ -111,6 +492,551 @@ app.get('/api/account/:id', async (req, res) => {
111
492
 
112
493
  ---
113
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] || '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] || '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] || '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
+
114
1040
  ## 错误对象结构
115
1041
 
116
1042
  ### 基础结构