schema-dsl 1.1.0 → 1.1.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.
package/index.js CHANGED
@@ -22,6 +22,10 @@ const ErrorCodes = require('./lib/core/ErrorCodes');
22
22
  const MessageTemplate = require('./lib/core/MessageTemplate');
23
23
  const Locale = require('./lib/core/Locale');
24
24
 
25
+ // ========== 错误类 ==========
26
+ const ValidationError = require('./lib/errors/ValidationError');
27
+ const I18nError = require('./lib/errors/I18nError');
28
+
25
29
  // ========== String 扩展 ==========
26
30
  const { installStringExtensions, uninstallStringExtensions } = require('./lib/core/StringExtensions');
27
31
 
@@ -48,6 +52,36 @@ dsl.if = function(...args) {
48
52
  return ConditionalBuilder.start(args[0]);
49
53
  };
50
54
 
55
+ // ✅ dsl.error:统一的多语言错误抛出(v1.1.1+)
56
+ dsl.error = {
57
+ /**
58
+ * 创建多语言错误(不抛出)
59
+ * @param {string} code - 错误代码(多语言 key)
60
+ * @param {Object} params - 错误参数
61
+ * @param {number} statusCode - HTTP 状态码
62
+ * @returns {I18nError} 错误实例
63
+ */
64
+ create: (code, params, statusCode) => I18nError.create(code, params, statusCode),
65
+
66
+ /**
67
+ * 抛出多语言错误
68
+ * @param {string} code - 错误代码(多语言 key)
69
+ * @param {Object} params - 错误参数
70
+ * @param {number} statusCode - HTTP 状态码
71
+ * @throws {I18nError} 直接抛出错误
72
+ */
73
+ throw: (code, params, statusCode) => I18nError.throw(code, params, statusCode),
74
+
75
+ /**
76
+ * 断言方法 - 条件不满足时抛错
77
+ * @param {boolean} condition - 条件表达式
78
+ * @param {string} code - 错误代码(多语言 key)
79
+ * @param {Object} params - 错误参数
80
+ * @param {number} statusCode - HTTP 状态码
81
+ */
82
+ assert: (condition, code, params, statusCode) => I18nError.assert(condition, code, params, statusCode)
83
+ };
84
+
51
85
  /**
52
86
  * 全局配置
53
87
  * @param {Object} options - 配置选项
@@ -182,8 +216,6 @@ installStringExtensions(dsl);
182
216
 
183
217
  // ========== 导出 ==========
184
218
 
185
- // 导入 ValidationError
186
- const ValidationError = require('./lib/errors/ValidationError');
187
219
 
188
220
  // 导入 validateAsync
189
221
  const { validateAsync } = require('./lib/adapters/DslAdapter');
@@ -214,6 +246,7 @@ module.exports = {
214
246
 
215
247
  // 错误类 (v2.1.0 新增)
216
248
  ValidationError,
249
+ I18nError, // v1.1.1 新增:多语言错误类
217
250
 
218
251
  // 错误消息系统
219
252
  ErrorCodes,
@@ -268,12 +268,59 @@ class DslAdapter {
268
268
  * @param {string} baseType - 基础类型
269
269
  * @param {string} constraint - 约束字符串
270
270
  * @returns {Object} 约束对象
271
+ *
272
+ * @example
273
+ * // 比较运算符 (v1.2.0+)
274
+ * _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
275
+ * _parseConstraint('number', '>=18') // { minimum: 18 }
276
+ * _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
277
+ * _parseConstraint('number', '<=100') // { maximum: 100 }
278
+ * _parseConstraint('number', '=100') // { enum: [100] }
279
+ * _parseConstraint('number', '>0.5') // { exclusiveMinimum: 0.5 } 支持小数
271
280
  */
272
281
  static _parseConstraint(baseType, constraint) {
273
282
  if (!constraint) return {};
274
283
 
275
284
  const result = {};
276
285
 
286
+ // ========== 比较运算符(v1.1.2新增,最高优先级)==========
287
+ if (baseType === 'number' || baseType === 'integer') {
288
+ // 1. 大于等于: >=18, >=-10 (支持负数)
289
+ const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
290
+ if (gteMatch) {
291
+ result.minimum = parseFloat(gteMatch[1]);
292
+ return result;
293
+ }
294
+
295
+ // 2. 小于等于: <=100, <=-10 (支持负数)
296
+ const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
297
+ if (lteMatch) {
298
+ result.maximum = parseFloat(lteMatch[1]);
299
+ return result;
300
+ }
301
+
302
+ // 3. 大于: >0, >-10 (不包括边界值,支持负数)
303
+ const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
304
+ if (gtMatch) {
305
+ result.exclusiveMinimum = parseFloat(gtMatch[1]);
306
+ return result;
307
+ }
308
+
309
+ // 4. 小于: <100, <-10 (不包括边界值,支持负数)
310
+ const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
311
+ if (ltMatch) {
312
+ result.exclusiveMaximum = parseFloat(ltMatch[1]);
313
+ return result;
314
+ }
315
+
316
+ // 5. 等于: =100, =-10 (支持负数)
317
+ const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
318
+ if (eqMatch) {
319
+ result.enum = [parseFloat(eqMatch[1])];
320
+ return result;
321
+ }
322
+ }
323
+
277
324
  // 枚举: value1|value2|value3 (优先检查,避免被数字范围误判)
278
325
  if (constraint.includes('|') && !/^\d+-\d+$/.test(constraint)) {
279
326
  result.enum = constraint.split('|').map(v => v.trim());
@@ -281,7 +328,8 @@ class DslAdapter {
281
328
  }
282
329
 
283
330
  // 范围约束: min-max 或 min- 或 -max
284
- const rangeMatch = constraint.match(/^(\d*)-(\d*)$/);
331
+ // 支持小数: 0.5-99.9
332
+ const rangeMatch = constraint.match(/^(\d*\.?\d*)-(\d*\.?\d*)$/);
285
333
  if (rangeMatch) {
286
334
  const [, min, max] = rangeMatch;
287
335
  if (baseType === 'string') {
@@ -291,20 +339,21 @@ class DslAdapter {
291
339
  if (min) result.minItems = parseInt(min, 10);
292
340
  if (max) result.maxItems = parseInt(max, 10);
293
341
  } else {
294
- if (min) result.minimum = parseInt(min, 10);
295
- if (max) result.maximum = parseInt(max, 10);
342
+ if (min) result.minimum = parseFloat(min);
343
+ if (max) result.maximum = parseFloat(max);
296
344
  }
297
345
  return result;
298
346
  }
299
347
 
300
348
  // 单个约束: min, max
301
- const singleMatch = constraint.match(/^(\d+)$/);
349
+ // 支持小数: 99.99
350
+ const singleMatch = constraint.match(/^(\d+(?:\.\d+)?)$/);
302
351
  if (singleMatch) {
303
- const value = parseInt(singleMatch[1], 10);
352
+ const value = parseFloat(singleMatch[1]);
304
353
  if (baseType === 'string') {
305
- result.maxLength = value;
354
+ result.maxLength = Math.floor(value);
306
355
  } else if (baseType === 'array') {
307
- result.maxItems = value;
356
+ result.maxItems = Math.floor(value);
308
357
  } else {
309
358
  result.maximum = value;
310
359
  }
@@ -59,7 +59,7 @@ class ConditionalBuilder {
59
59
  this._conditions.push({
60
60
  type: 'if',
61
61
  condition: conditionFn,
62
- combinedConditions: [{ op: 'root', fn: conditionFn }]
62
+ combinedConditions: [{ op: 'root', fn: conditionFn, message: null }]
63
63
  });
64
64
 
65
65
  return this;
@@ -73,6 +73,7 @@ class ConditionalBuilder {
73
73
  * @example
74
74
  * dsl.if((data) => data.age >= 18)
75
75
  * .and((data) => data.userType === 'admin')
76
+ * .message('必须是管理员')
76
77
  * .then('email!')
77
78
  */
78
79
  and(conditionFn) {
@@ -85,7 +86,7 @@ class ConditionalBuilder {
85
86
  throw new Error('.and() must follow .if() or .elseIf()');
86
87
  }
87
88
 
88
- last.combinedConditions.push({ op: 'and', fn: conditionFn });
89
+ last.combinedConditions.push({ op: 'and', fn: conditionFn, message: null });
89
90
  return this;
90
91
  }
91
92
 
@@ -109,7 +110,7 @@ class ConditionalBuilder {
109
110
  throw new Error('.or() must follow .if() or .elseIf()');
110
111
  }
111
112
 
112
- last.combinedConditions.push({ op: 'or', fn: conditionFn });
113
+ last.combinedConditions.push({ op: 'or', fn: conditionFn, message: null });
113
114
  return this;
114
115
  }
115
116
 
@@ -136,7 +137,7 @@ class ConditionalBuilder {
136
137
  this._conditions.push({
137
138
  type: 'elseIf',
138
139
  condition: conditionFn,
139
- combinedConditions: [{ op: 'root', fn: conditionFn }]
140
+ combinedConditions: [{ op: 'root', fn: conditionFn, message: null }]
140
141
  });
141
142
 
142
143
  return this;
@@ -144,7 +145,7 @@ class ConditionalBuilder {
144
145
 
145
146
  /**
146
147
  * 设置错误消息(支持多语言 key)
147
- t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
148
+ * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
148
149
  *
149
150
  * @param {string} msg - 错误消息或多语言 key
150
151
  * @returns {ConditionalBuilder} 当前实例(支持链式调用)
@@ -153,6 +154,13 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
153
154
  * // 如果是未成年人,抛出错误
154
155
  * dsl.if((data) => data.age < 18)
155
156
  * .message('未成年用户不能注册')
157
+ *
158
+ * @example
159
+ * // 为 and 条件设置独立消息
160
+ * dsl.if((data) => !data)
161
+ * .message('账户不存在')
162
+ * .and((data) => data.balance < 100)
163
+ * .message('余额不足')
156
164
  */
157
165
  message(msg) {
158
166
  if (typeof msg !== 'string') {
@@ -164,6 +172,14 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
164
172
  throw new Error('.message() must follow .if() or .elseIf()');
165
173
  }
166
174
 
175
+ // 找到最后一个添加的条件(可能是 root、and 或 or)
176
+ const lastCombined = last.combinedConditions[last.combinedConditions.length - 1];
177
+ if (lastCombined) {
178
+ // 为最后一个组合条件设置消息
179
+ lastCombined.message = msg;
180
+ }
181
+
182
+ // 同时设置整体消息(作为后备)
167
183
  last.message = msg;
168
184
  last.action = 'throw'; // 有 message 就自动 throw
169
185
  return this;
@@ -216,31 +232,117 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
216
232
  * @private
217
233
  * @param {Object} conditionObj - 条件对象
218
234
  * @param {*} data - 待验证数据对象
219
- * @returns {boolean} 条件结果
235
+ * @returns {Object} { result: boolean, failedMessage: string|null } 条件结果和失败消息
236
+ *
237
+ * 语义说明:
238
+ * - 传统 AND 模式:所有条件都为 true 才失败
239
+ * - 链式检查模式:root 有 message,且任何 .and() 也有独立 message 时
240
+ * → 依次检查,第一个为 true 的失败
220
241
  */
221
242
  _evaluateCondition(conditionObj, data) {
222
243
  try {
244
+ // 检查是否是链式检查模式:
245
+ // 1. 必须是 message 模式(action='throw')
246
+ // 2. root 条件有 message
247
+ // 3. 有 .and() 条件(不管是否有独立 message)
248
+ // 4. 没有 .or() 条件(有 OR 就使用传统逻辑)
249
+ const isMessageMode = conditionObj.action === 'throw';
250
+ const rootHasMessage = conditionObj.combinedConditions[0]?.message != null;
251
+ const hasAndConditions = conditionObj.combinedConditions.some(c => c.op === 'and');
252
+ const hasOrConditions = conditionObj.combinedConditions.some(c => c.op === 'or');
253
+ const isChainCheckMode = isMessageMode && rootHasMessage && hasAndConditions && !hasOrConditions;
254
+
223
255
  let result = false;
256
+ let failedMessage = null;
224
257
 
225
258
  for (let i = 0; i < conditionObj.combinedConditions.length; i++) {
226
259
  const combined = conditionObj.combinedConditions[i];
260
+ let conditionResult = false;
227
261
 
228
262
  if (combined.op === 'root') {
229
263
  // 第一个条件
230
- result = combined.fn(data);
264
+ conditionResult = combined.fn(data);
265
+ result = conditionResult;
266
+
267
+ if (isChainCheckMode) {
268
+ // 链式检查模式:第一个条件为 true 就失败
269
+ if (result) {
270
+ failedMessage = combined.message || conditionObj.message;
271
+ return { result: true, failedMessage };
272
+ }
273
+ } else {
274
+ // 传统模式
275
+ failedMessage = combined.message || conditionObj.message;
276
+
277
+ // 如果第一个条件为 false,检查是否有 OR 条件
278
+ if (!result) {
279
+ const hasOrConditions = conditionObj.combinedConditions.some(c => c.op === 'or');
280
+ if (!hasOrConditions) {
281
+ // 没有 OR 条件,直接通过
282
+ return { result: false, failedMessage: null };
283
+ }
284
+ // 有 OR 条件,继续检查
285
+ }
286
+ }
231
287
  } else if (combined.op === 'and') {
232
- // AND 组合
233
- result = result && combined.fn(data);
288
+ conditionResult = combined.fn(data);
289
+
290
+ if (isChainCheckMode) {
291
+ // 链式检查模式:任一条件为 true 就失败
292
+ if (conditionResult) {
293
+ // 使用独立消息,如果没有则使用整体消息
294
+ failedMessage = combined.message || conditionObj.message;
295
+ return { result: true, failedMessage };
296
+ }
297
+ // 条件为 false,继续检查下一个条件
298
+ } else {
299
+ // 传统 AND 模式:所有条件都必须为 true 才失败
300
+ if (!conditionResult) {
301
+ // AND 条件为 false
302
+ // 检查是否有 OR 条件
303
+ if (hasOrConditions) {
304
+ // 有 OR 条件,将 result 设为 false,继续检查 OR
305
+ result = false;
306
+ } else {
307
+ // 没有 OR 条件,任一 AND 条件为 false 就验证通过
308
+ return { result: false, failedMessage: null };
309
+ }
310
+ } else {
311
+ // AND 条件为 true,继续累积
312
+ result = true;
313
+ }
314
+ }
234
315
  } else if (combined.op === 'or') {
235
- // OR 组合
236
- result = result || combined.fn(data);
316
+ // OR 逻辑:任一条件为 true 就失败
317
+ if (!result) {
318
+ conditionResult = combined.fn(data);
319
+
320
+ if (conditionResult) {
321
+ // OR 条件为 true
322
+ result = true; // 更新 result
323
+
324
+ if (isMessageMode) {
325
+ // message 模式:立即返回失败
326
+ failedMessage = combined.message || conditionObj.message;
327
+ return { result: true, failedMessage };
328
+ }
329
+ // then/else 模式:继续累积 result(已经设置为true)
330
+ }
331
+ }
237
332
  }
238
333
  }
239
334
 
240
- return result;
335
+ // 返回最终结果
336
+ if (isChainCheckMode) {
337
+ // 链式检查模式:所有条件都为 false,验证通过
338
+ return { result: false, failedMessage: null };
339
+ } else {
340
+ // 传统模式:返回累积结果
341
+ return { result, failedMessage };
342
+ }
241
343
  } catch (error) {
242
344
  // 条件函数执行出错,视为不满足
243
- return false;
345
+ return { result: false, failedMessage: null };
244
346
  }
245
347
  }
246
348
 
@@ -383,12 +383,58 @@ class DslBuilder {
383
383
  /**
384
384
  * 解析约束
385
385
  * @private
386
+ *
387
+ * @example
388
+ * // 比较运算符 (v1.2.0+)
389
+ * _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
390
+ * _parseConstraint('number', '>=18') // { minimum: 18 }
391
+ * _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
392
+ * _parseConstraint('number', '<=100') // { maximum: 100 }
393
+ * _parseConstraint('number', '=100') // { enum: [100] }
386
394
  */
387
395
  _parseConstraint(type, constraint) {
388
396
  const result = {};
389
397
 
390
398
  if (type === 'string' || type === 'number' || type === 'integer') {
391
- // 范围约束: min-max
399
+ // ========== 比较运算符(v1.1.2新增,仅number/integer,最高优先级)==========
400
+ if (type === 'number' || type === 'integer') {
401
+ // 1. 大于等于: >=18, >=-10 (支持负数)
402
+ const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
403
+ if (gteMatch) {
404
+ result.minimum = parseFloat(gteMatch[1]);
405
+ return result;
406
+ }
407
+
408
+ // 2. 小于等于: <=100, <=-10 (支持负数)
409
+ const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
410
+ if (lteMatch) {
411
+ result.maximum = parseFloat(lteMatch[1]);
412
+ return result;
413
+ }
414
+
415
+ // 3. 大于: >0, >-10 (不包括边界值,支持负数)
416
+ const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
417
+ if (gtMatch) {
418
+ result.exclusiveMinimum = parseFloat(gtMatch[1]);
419
+ return result;
420
+ }
421
+
422
+ // 4. 小于: <100, <-10 (不包括边界值,支持负数)
423
+ const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
424
+ if (ltMatch) {
425
+ result.exclusiveMaximum = parseFloat(ltMatch[1]);
426
+ return result;
427
+ }
428
+
429
+ // 5. 等于: =100, =-10 (支持负数)
430
+ const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
431
+ if (eqMatch) {
432
+ result.enum = [parseFloat(eqMatch[1])];
433
+ return result;
434
+ }
435
+ }
436
+
437
+ // ========== 范围约束: min-max ==========
392
438
  if (constraint.includes('-')) {
393
439
  const [min, max] = constraint.split('-').map(v => v.trim());
394
440
 
@@ -464,16 +464,20 @@ class Validator {
464
464
  for (let i = 0; i < conditionalSchema.conditions.length; i++) {
465
465
  const cond = conditionalSchema.conditions[i];
466
466
 
467
- // 执行组合条件(支持 and/or
468
- const matched = conditionalSchema._evaluateCondition(cond, data);
467
+ // 执行组合条件(支持 and/or),获取结果和失败消息
468
+ const evaluation = conditionalSchema._evaluateCondition(cond, data);
469
+ const matched = evaluation.result;
470
+ const failedMessage = evaluation.failedMessage;
469
471
 
470
472
  if (cond.action === 'throw') {
471
473
  // ✅ message 模式:条件为 true 时抛错,条件为 false 时通过
472
474
  if (matched) {
473
475
  // ✅ 条件满足(true),抛出错误
476
+ // 优先使用具体的失败消息,如果没有则使用整体消息
477
+ const errorMsg = failedMessage || cond.message;
474
478
  // 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
475
479
  // 传递 locale 参数以支持动态语言切换
476
- const errorMessage = Locale.getMessage(cond.message, options.messages || {}, locale);
480
+ const errorMessage = Locale.getMessage(errorMsg, options.messages || {}, locale);
477
481
 
478
482
  return {
479
483
  valid: false,
@@ -0,0 +1,222 @@
1
+ /**
2
+ * I18nError - 多语言错误工具类
3
+ *
4
+ * 提供统一的多语言错误抛出机制,支持:
5
+ * - 多语言 key 自动翻译
6
+ * - 参数插值(如 {{field}}, {{limit}})
7
+ * - 自定义错误代码
8
+ * - Express/Koa 集成
9
+ *
10
+ * @module lib/errors/I18nError
11
+ * @version 1.1.1
12
+ *
13
+ * @example 基础用法
14
+ * const { I18nError } = require('schema-dsl');
15
+ *
16
+ * // 抛出多语言错误
17
+ * throw I18nError.create('error.notFound', { resource: '账户' });
18
+ * // 中文: "找不到账户"
19
+ * // 英文: "Account not found"
20
+ *
21
+ * @example 业务代码中使用
22
+ * function getAccount(id) {
23
+ * const account = db.findAccount(id);
24
+ * if (!account) {
25
+ * throw I18nError.create('account.notFound', { accountId: id });
26
+ * }
27
+ * if (account.balance < 100) {
28
+ * throw I18nError.create('account.insufficientBalance', {
29
+ * balance: account.balance,
30
+ * required: 100
31
+ * });
32
+ * }
33
+ * return account;
34
+ * }
35
+ *
36
+ * @example Express 中间件
37
+ * app.use((error, req, res, next) => {
38
+ * if (error instanceof I18nError) {
39
+ * return res.status(error.statusCode).json(error.toJSON());
40
+ * }
41
+ * next(error);
42
+ * });
43
+ */
44
+
45
+ const Locale = require('../core/Locale');
46
+ const MessageTemplate = require('../core/MessageTemplate');
47
+
48
+ /**
49
+ * 多语言错误类
50
+ *
51
+ * @class I18nError
52
+ * @extends Error
53
+ *
54
+ * @property {string} name - 错误名称(固定为 'I18nError')
55
+ * @property {string} message - 错误消息(已翻译)
56
+ * @property {string} code - 错误代码(多语言 key)
57
+ * @property {Object} params - 错误参数(用于插值)
58
+ * @property {number} statusCode - HTTP 状态码(默认 400)
59
+ * @property {string} locale - 使用的语言环境
60
+ */
61
+ class I18nError extends Error {
62
+ /**
63
+ * 构造函数
64
+ * @param {string} code - 错误代码(多语言 key)
65
+ * @param {Object} params - 错误参数(用于插值)
66
+ * @param {number} statusCode - HTTP 状态码(默认 400)
67
+ * @param {string} locale - 语言环境(默认使用当前语言)
68
+ */
69
+ constructor(code, params = {}, statusCode = 400, locale = null) {
70
+ // 获取翻译后的消息模板
71
+ const actualLocale = locale || Locale.getLocale();
72
+ const template = Locale.getMessage(code, {}, actualLocale);
73
+
74
+ // 使用 MessageTemplate 进行参数插值
75
+ const messageTemplate = new MessageTemplate(template);
76
+ const message = messageTemplate.render(params || {});
77
+
78
+ super(message);
79
+
80
+ this.name = 'I18nError';
81
+ this.code = code;
82
+ this.params = params || {};
83
+ this.statusCode = statusCode;
84
+ this.locale = actualLocale;
85
+
86
+ // 保持堆栈跟踪
87
+ if (Error.captureStackTrace) {
88
+ Error.captureStackTrace(this, I18nError);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 静态工厂方法 - 创建并抛出错误
94
+ *
95
+ * @param {string} code - 错误代码(多语言 key)
96
+ * @param {Object} params - 错误参数
97
+ * @param {number} statusCode - HTTP 状态码
98
+ * @returns {I18nError} 错误实例
99
+ *
100
+ * @example
101
+ * // 创建错误(不抛出)
102
+ * const error = I18nError.create('error.notFound', { resource: '用户' });
103
+ *
104
+ * @example
105
+ * // 直接抛出
106
+ * throw I18nError.create('error.notFound', { resource: '用户' });
107
+ */
108
+ static create(code, params = {}, statusCode = 400) {
109
+ return new I18nError(code, params, statusCode);
110
+ }
111
+
112
+ /**
113
+ * 静态工厂方法 - 快速抛出错误
114
+ *
115
+ * @param {string} code - 错误代码(多语言 key)
116
+ * @param {Object} params - 错误参数
117
+ * @param {number} statusCode - HTTP 状态码
118
+ * @throws {I18nError} 直接抛出错误
119
+ *
120
+ * @example
121
+ * I18nError.throw('error.notFound', { resource: '用户' });
122
+ * // 等同于:throw I18nError.create('error.notFound', { resource: '用户' });
123
+ */
124
+ static throw(code, params = {}, statusCode = 400) {
125
+ throw new I18nError(code, params, statusCode);
126
+ }
127
+
128
+ /**
129
+ * 断言方法 - 条件不满足时抛错
130
+ *
131
+ * @param {boolean} condition - 条件表达式
132
+ * @param {string} code - 错误代码(多语言 key)
133
+ * @param {Object} params - 错误参数
134
+ * @param {number} statusCode - HTTP 状态码
135
+ * @throws {I18nError} 条件为 false 时抛出错误
136
+ *
137
+ * @example
138
+ * I18nError.assert(account, 'account.notFound', { accountId: id });
139
+ * // 等同于:if (!account) throw I18nError.create('account.notFound', { accountId: id });
140
+ *
141
+ * @example
142
+ * I18nError.assert(
143
+ * account.balance >= 100,
144
+ * 'account.insufficientBalance',
145
+ * { balance: account.balance, required: 100 }
146
+ * );
147
+ */
148
+ static assert(condition, code, params = {}, statusCode = 400) {
149
+ if (!condition) {
150
+ throw new I18nError(code, params, statusCode);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 检查错误是否为指定代码
156
+ *
157
+ * @param {string} code - 错误代码
158
+ * @returns {boolean} 是否匹配
159
+ *
160
+ * @example
161
+ * try {
162
+ * // ...
163
+ * } catch (error) {
164
+ * if (error instanceof I18nError && error.is('account.notFound')) {
165
+ * // 处理账户不存在的情况
166
+ * }
167
+ * }
168
+ */
169
+ is(code) {
170
+ return this.code === code;
171
+ }
172
+
173
+ /**
174
+ * 转换为 JSON 格式(用于 API 响应)
175
+ *
176
+ * @returns {Object} JSON 对象
177
+ * @returns {string} return.error - 错误名称
178
+ * @returns {string} return.code - 错误代码
179
+ * @returns {string} return.message - 错误消息(已翻译)
180
+ * @returns {Object} return.params - 错误参数
181
+ * @returns {number} return.statusCode - 状态码
182
+ * @returns {string} return.locale - 语言环境
183
+ *
184
+ * @example
185
+ * const json = error.toJSON();
186
+ * res.status(error.statusCode).json(json);
187
+ * // {
188
+ * // error: 'I18nError',
189
+ * // code: 'account.notFound',
190
+ * // message: '找不到账户',
191
+ * // params: { accountId: '123' },
192
+ * // statusCode: 404,
193
+ * // locale: 'zh-CN'
194
+ * // }
195
+ */
196
+ toJSON() {
197
+ return {
198
+ error: this.name,
199
+ code: this.code,
200
+ message: this.message,
201
+ params: this.params,
202
+ statusCode: this.statusCode,
203
+ locale: this.locale
204
+ };
205
+ }
206
+
207
+ /**
208
+ * 转换为字符串
209
+ *
210
+ * @returns {string} 格式化的错误信息
211
+ *
212
+ * @example
213
+ * console.log(error.toString());
214
+ * // "I18nError [account.notFound]: 找不到账户"
215
+ */
216
+ toString() {
217
+ return `${this.name} [${this.code}]: ${this.message}`;
218
+ }
219
+ }
220
+
221
+ module.exports = I18nError;
222
+