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,608 +1,608 @@
1
- # 动态多语言配置指南
2
-
3
- > **更新时间**: 2025-12-25
4
- > **场景**: 从请求头动态获取语言配置
5
-
6
- ---
7
-
8
- ## 📑 目录
9
-
10
- - [基本原理](#基本原理)
11
- - [方案1: 验证时指定语言(推荐)](#方案1-验证时指定语言推荐)
12
- - [方案2: 临时切换语言](#方案2-临时切换语言)
13
- - [方案3: Express/Koa 中间件](#方案3-expresskoa-中间件)
14
- - [完整示例](#完整示例)
15
- - [最佳实践](#最佳实践)
16
-
17
- ---
18
-
19
- ## 基本原理
20
-
21
- schema-dsl 的 `Validator` 支持在验证时动态指定语言,无需全局切换。
22
-
23
- ### 核心方法
24
-
25
- ```javascript
26
- validator.validate(schema, data, {
27
- locale: 'zh-CN' // 动态指定语言
28
- });
29
- ```
30
-
31
- ---
32
-
33
- ## 方案1: 验证时指定语言(推荐)✅
34
-
35
- 这是**最推荐**的方案,无需修改全局状态,支持并发请求。
36
-
37
- ### 1.1 应用启动时配置(一次性加载所有语言)
38
-
39
- 使用 `dsl.config` 在应用启动时一次性加载所有自定义语言包。
40
-
41
- ```javascript
42
- const { dsl, validate } = require('schema-dsl');
43
- const path = require('path');
44
-
45
- // ========== 应用启动时配置(只执行一次)==========
46
-
47
- // 方式一:传入目录路径(推荐)⭐
48
- // Node >=18:自动扫描目录下的 .js(CommonJS)、.cjs、.json、.jsonc、.json5 文件
49
- dsl.config({
50
- i18n: path.join(__dirname, 'locales')
51
- });
52
-
53
- // 方式二:直接传入对象
54
- dsl.config({
55
- i18n: {
56
- 'fr-FR': {
57
- 'required': '{{#label}} est requis',
58
- 'string.minLength': '{{#label}} doit contenir au moins {{#limit}} caractères'
59
- },
60
- 'de-DE': {
61
- 'required': '{{#label}} ist erforderlich',
62
- 'string.minLength': '{{#label}} muss mindestens {{#limit}} Zeichen lang sein'
63
- }
64
- }
65
- });
66
-
67
- // 说明:
68
- // - 只在应用启动时执行一次
69
- // - 自动与系统内置语言包合并(用户自定义的优先)
70
- // - 运行时无需重新加载,直接切换
71
- ```
72
-
73
- ### 1.2 运行时直接切换语言(无需重新加载)
74
-
75
- ```javascript
76
- const { dsl, validate } = require('schema-dsl');
77
-
78
- // 定义 Schema
79
- const schema = dsl({
80
- username: 'string:3-32!',
81
- email: 'email!'
82
- });
83
-
84
- // 测试数据
85
- const data = { username: 'ab', email: 'invalid' };
86
-
87
- // ========== 运行时直接切换语言 ==========
88
-
89
- // 使用中文
90
- const result1 = validate(schema, data, { locale: 'zh-CN' });
91
- // 错误: "username长度不能少于3个字符"
92
-
93
- // 使用法语
94
- const result2 = validate(schema, data, { locale: 'fr-FR' });
95
- // 错误: "username doit contenir au moins 3 caractères"
96
-
97
- // 使用德语
98
- const result3 = validate(schema, data, { locale: 'de-DE' });
99
- // 错误: "username muss mindestens 3 Zeichen lang sein"
100
-
101
- // 说明:
102
- // - 无需重新加载语言包
103
- // - 每次验证可以使用不同语言
104
- // - 支持高并发(无全局状态修改)
105
- ```
106
-
107
- ### 1.3 从请求头获取语言(实际应用场景)
108
-
109
- ```javascript
110
- const express = require('express');
111
- const { dsl, validate } = require('schema-dsl');
112
- const path = require('path');
113
-
114
- const app = express();
115
-
116
- // ========== 应用启动时配置(只执行一次)==========
117
- dsl.config({
118
- i18n: path.join(__dirname, 'locales')
119
- });
120
-
121
- // 定义 Schema
122
- const userSchema = dsl({
123
- username: 'string:3-32!',
124
- email: 'email!',
125
- password: 'string:8-32!'
126
- });
127
-
128
- // ========== Express 路由 ==========
129
- app.post('/api/user/register', (req, res) => {
130
- // 从请求头获取语言偏好
131
- const locale = parseAcceptLanguage(req.headers['accept-language']);
132
-
133
- // 验证数据(直接切换语言,无需重新加载)
134
- const result = validate(userSchema, req.body, { locale });
135
-
136
- if (!result.valid) {
137
- return res.status(400).json({
138
- errors: result.errors // 自动使用用户偏好的语言
139
- });
140
- }
141
-
142
- // 处理成功...
143
- res.json({ message: 'User registered successfully' });
144
- });
145
- ```
146
-
147
- ### 1.3 解析 Accept-Language 头
148
-
149
- ```javascript
150
- /**
151
- * 解析 Accept-Language 头
152
- * @param {string} acceptLanguage - Accept-Language 头的值
153
- * @returns {string} 语言代码
154
- */
155
- function parseAcceptLanguage(acceptLanguage) {
156
- if (!acceptLanguage) return 'en-US';
157
-
158
- // Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
159
- const languages = acceptLanguage.split(',').map(lang => {
160
- const [code, qValue] = lang.trim().split(';');
161
- const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
162
- return { code: code.trim(), q };
163
- });
164
-
165
- // 按权重排序
166
- languages.sort((a, b) => b.q - a.q);
167
-
168
- // 映射到支持的语言
169
- const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
170
- for (const lang of languages) {
171
- const matched = supportedLocales.find(locale =>
172
- locale.toLowerCase() === lang.code.toLowerCase() ||
173
- locale.split('-')[0] === lang.code.split('-')[0]
174
- );
175
- if (matched) return matched;
176
- }
177
-
178
- return 'en-US'; // 默认语言
179
- }
180
-
181
- // 使用
182
- app.post('/api/user/register', (req, res) => {
183
- const locale = parseAcceptLanguage(req.headers['accept-language']);
184
-
185
- const result = validator.validate(schema, req.body, { locale });
186
-
187
- // ...
188
- });
189
- ```
190
-
191
- ---
192
-
193
- ## 方案2: 临时切换语言
194
-
195
- 适用于少数场景。
196
-
197
- ### 2.1 使用闭包保存原语言
198
-
199
- ```javascript
200
- function validateWithLocale(validator, schema, data, locale) {
201
- const originalLocale = Locale.getLocale();
202
-
203
- try {
204
- Locale.setLocale(locale);
205
- return validator.validate(schema, data);
206
- } finally {
207
- Locale.setLocale(originalLocale); // 恢复原语言
208
- }
209
- }
210
-
211
- // 使用
212
- app.post('/api/user/register', (req, res) => {
213
- const locale = parseAcceptLanguage(req.headers['accept-language']);
214
-
215
- const result = validateWithLocale(validator, schema, req.body, locale);
216
-
217
- // ...
218
- });
219
- ```
220
-
221
- ---
222
-
223
- ## 方案3: Express/Koa 中间件
224
-
225
- 封装为中间件,自动处理语言切换。
226
-
227
- ### 3.1 Express 中间件 (推荐)
228
-
229
- 通过中间件一次性配置,后续业务代码无需关心语言参数。
230
-
231
- ```javascript
232
- const { Validator } = require('schema-dsl');
233
- const validator = new Validator();
234
-
235
- const schemaIoMiddleware = (req, res, next) => {
236
- // 1. 自动获取语言
237
- const lang = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
238
- // 简单匹配逻辑 (实际可使用 accept-language-parser)
239
- const locale = lang.includes('zh') ? 'zh-CN' :
240
- lang.includes('ja') ? 'ja-JP' :
241
- lang.includes('es') ? 'es-ES' :
242
- lang.includes('fr') ? 'fr-FR' : 'en-US';
243
-
244
- // 2. 挂载绑定了语言的验证方法
245
- req.validate = (schema, data) => {
246
- return validator.validate(schema, data, { locale });
247
- };
248
-
249
- next();
250
- };
251
-
252
- app.use(schemaIoMiddleware);
253
-
254
- // 业务中使用
255
- app.post('/users', (req, res) => {
256
- // 直接调用,自动使用中间件解析的语言
257
- const result = req.validate(userSchema, req.body);
258
-
259
- if (!result.valid) {
260
- return res.status(400).json({ errors: result.errors });
261
- }
262
-
263
- // ...
264
- });
265
- ```
266
-
267
- 完整示例请参考 [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)。
268
-
269
- ### 3.2 Koa 中间件
270
-
271
- ```javascript
272
- const { Locale, Validator } = require('schema-dsl');
273
-
274
- const validator = new Validator();
275
-
276
- /**
277
- * Koa 语言中间件
278
- */
279
- function localeMiddleware() {
280
- return async (ctx, next) => {
281
- // 解析语言
282
- const locale = parseAcceptLanguage(ctx.headers['accept-language']);
283
-
284
- // 保存到上下文
285
- ctx.locale = locale;
286
-
287
- // 复用共享 Validator,避免每个请求都重新建立实例和缓存
288
- ctx.validate = function(schema, data) {
289
- return validator.validate(schema, data, { locale: ctx.locale });
290
- };
291
-
292
- await next();
293
- };
294
- }
295
-
296
- // 应用中间件
297
- app.use(localeMiddleware());
298
-
299
- // 使用
300
- router.post('/api/user/register', async (ctx) => {
301
- // 自动使用请求的语言
302
- const result = ctx.validate(userSchema, ctx.request.body);
303
-
304
- if (!result.valid) {
305
- ctx.status = 400;
306
- ctx.body = { errors: result.errors };
307
- return;
308
- }
309
-
310
- // ...
311
- });
312
- ```
313
-
314
- ---
315
-
316
- ## 完整示例
317
-
318
- ### Express 完整示例
319
-
320
- ```javascript
321
- const express = require('express');
322
- const { dsl, Validator, Locale } = require('schema-dsl');
323
-
324
- const app = express();
325
- app.use(express.json());
326
-
327
- // ========== 1. 初始化语言包 ==========
328
-
329
- Locale.addLocale('zh-CN', {
330
- 'required': '{{#label}}不能为空',
331
- 'min': '{{#label}}至少{{#limit}}个字符',
332
- 'max': '{{#label}}最多{{#limit}}个字符',
333
- 'pattern': '{{#label}}格式不正确',
334
- 'format': '请输入有效的{{#label}}'
335
- });
336
-
337
- Locale.addLocale('en-US', {
338
- 'required': '{{#label}} is required',
339
- 'min': '{{#label}} must be at least {{#limit}} characters',
340
- 'max': '{{#label}} must be at most {{#limit}} characters',
341
- 'pattern': '{{#label}} format is invalid',
342
- 'format': 'Please enter a valid {{#label}}'
343
- });
344
-
345
- // ========== 2. 工具函数 ==========
346
-
347
- function parseAcceptLanguage(acceptLanguage) {
348
- if (!acceptLanguage) return 'en-US';
349
-
350
- const languages = acceptLanguage.split(',').map(lang => {
351
- const [code, qValue] = lang.trim().split(';');
352
- const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
353
- return { code: code.trim(), q };
354
- });
355
-
356
- languages.sort((a, b) => b.q - a.q);
357
-
358
- const supportedLocales = ['zh-CN', 'en-US'];
359
- for (const lang of languages) {
360
- const matched = supportedLocales.find(locale =>
361
- locale.toLowerCase() === lang.code.toLowerCase()
362
- );
363
- if (matched) return matched;
364
- }
365
-
366
- return 'en-US';
367
- }
368
-
369
- // ========== 3. 中间件 ==========
370
-
371
- const validator = new Validator();
372
-
373
- function localeMiddleware(req, res, next) {
374
- req.locale = parseAcceptLanguage(req.headers['accept-language']);
375
-
376
- req.validate = function(schema, data) {
377
- return validator.validate(schema, data, { locale: req.locale });
378
- };
379
-
380
- next();
381
- }
382
-
383
- app.use(localeMiddleware);
384
-
385
- // ========== 4. 定义Schema ==========
386
-
387
- const userSchema = dsl({
388
- username: 'string:3-32!'.label('用户名'),
389
- email: 'email!'.label('邮箱地址'),
390
- password: 'string:8-64!'
391
- .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
392
- .label('密码')
393
- .messages({
394
- 'pattern': '密码必须包含大小写字母和数字'
395
- }),
396
- age: 'number:18-120'.label('年龄')
397
- });
398
-
399
- // ========== 5. API 路由 ==========
400
-
401
- app.post('/api/user/register', (req, res) => {
402
- // 验证数据(自动使用请求语言)
403
- const result = req.validate(userSchema, req.body);
404
-
405
- if (!result.valid) {
406
- return res.status(400).json({
407
- success: false,
408
- errors: result.errors,
409
- locale: req.locale // 返回使用的语言
410
- });
411
- }
412
-
413
- // 处理注册逻辑
414
- res.json({
415
- success: true,
416
- message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
417
- });
418
- });
419
-
420
- // ========== 6. 测试 ==========
421
-
422
- app.listen(3000, () => {
423
- console.log('Server running on http://localhost:3000');
424
- console.log('\n测试命令:');
425
- console.log('# 中文错误消息');
426
- console.log('curl -X POST http://localhost:3000/api/user/register \\');
427
- console.log(' -H "Content-Type: application/json" \\');
428
- console.log(' -H "Accept-Language: zh-CN" \\');
429
- console.log(' -d \'{"username":"ab"}\'');
430
- console.log('\n# 英文错误消息');
431
- console.log('curl -X POST http://localhost:3000/api/user/register \\');
432
- console.log(' -H "Content-Type: application/json" \\');
433
- console.log(' -H "Accept-Language: en-US" \\');
434
- console.log(' -d \'{"username":"ab"}\'');
435
- });
436
- ```
437
-
438
- ---
439
-
440
- ## 最佳实践
441
-
442
- ### 1. 语言包集中管理
443
-
444
- ```javascript
445
- // locales/index.js
446
- module.exports = {
447
- 'zh-CN': require('./zh-CN.json'),
448
- 'en-US': require('./en-US.json'),
449
- 'ja-JP': require('./ja-JP.json')
450
- };
451
-
452
- // locales/zh-CN.json
453
- {
454
- "required": "{{#label}}不能为空",
455
- "min": "{{#label}}至少{{#limit}}个字符",
456
- "max": "{{#label}}最多{{#limit}}个字符",
457
- "pattern": "{{#label}}格式不正确",
458
- "format": "请输入有效的{{#label}}"
459
- }
460
-
461
- // 初始化
462
- const locales = require('./locales');
463
- Object.entries(locales).forEach(([locale, messages]) => {
464
- Locale.addLocale(locale, messages);
465
- });
466
- ```
467
-
468
- ### 2. 支持的语言列表
469
-
470
- ```javascript
471
- const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
472
-
473
- function getSupportedLocale(requestLocale) {
474
- return SUPPORTED_LOCALES.includes(requestLocale)
475
- ? requestLocale
476
- : 'en-US';
477
- }
478
- ```
479
-
480
- ### 3. 缓存验证器
481
-
482
- ```javascript
483
- // 为每个语言缓存验证器
484
- const validators = {
485
- 'zh-CN': new Validator(),
486
- 'en-US': new Validator(),
487
- 'ja-JP': new Validator()
488
- };
489
-
490
- function getValidator(locale) {
491
- return validators[locale] || validators['en-US'];
492
- }
493
-
494
- // 使用
495
- const result = getValidator(req.locale).validate(
496
- schema,
497
- data,
498
- { locale: req.locale }
499
- );
500
- ```
501
-
502
- ### 4. 错误响应标准化
503
-
504
- ```javascript
505
- function sendValidationError(res, result, locale) {
506
- res.status(400).json({
507
- success: false,
508
- code: 'VALIDATION_ERROR',
509
- message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
510
- errors: result.errors,
511
- locale: locale
512
- });
513
- }
514
-
515
- // 使用
516
- if (!result.valid) {
517
- return sendValidationError(res, result, req.locale);
518
- }
519
- ```
520
-
521
- ---
522
-
523
- ## 方案对比
524
-
525
- | 方案 | 优点 | 缺点 | 推荐度 |
526
- |------|------|------|--------|
527
- | **方案1: 验证时指定** | ✅ 无竞态问题<br>✅ 支持并发<br>✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
528
- | 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
529
- | 方案3: 中间件 | ✅ 自动化<br>✅ 统一管理<br>✅ 可复用共享 Validator 缓存 | - | ⭐⭐⭐⭐⭐ |
530
-
531
- **推荐**: 方案1 + 方案3(中间件封装)
532
-
533
- ---
534
-
535
- ## 常见问题
536
-
537
- ### Q1: 如何处理不支持的语言?
538
-
539
- **A**: 回退到默认语言
540
-
541
- 不要直接把原始 `Accept-Language` 头透传给 `locale`;浏览器常见值会带 `q=` 权重,应该先解析再回退。
542
-
543
- ```javascript
544
- function parseAcceptLanguage(acceptLanguage) {
545
- // ...解析逻辑
546
- return supportedLocale || 'en-US'; // 默认英文
547
- }
548
- ```
549
-
550
- ### Q2: 是否支持动态加载语言包?
551
-
552
- **A**: 支持
553
-
554
- ```javascript
555
- async function loadLocale(locale) {
556
- if (!Locale.getAvailableLocales().includes(locale)) {
557
- const messages = await import(`./locales/${locale}.json`);
558
- Locale.addLocale(locale, messages);
559
- }
560
- }
561
-
562
- // 使用
563
- app.use(async (req, res, next) => {
564
- await loadLocale(req.locale);
565
- next();
566
- });
567
- ```
568
-
569
- ### Q3: 如何自定义某些字段的错误消息?
570
-
571
- **A**: 使用 `.messages()` 方法
572
-
573
- ```javascript
574
- const schema = dsl({
575
- password: 'string:8-64!'
576
- .label('密码')
577
- .messages({
578
- 'required': req.locale === 'zh-CN'
579
- ? '请输入密码'
580
- : 'Please enter password',
581
- 'min': req.locale === 'zh-CN'
582
- ? '密码太短了,至少8个字符'
583
- : 'Password is too short, at least 8 characters'
584
- })
585
- });
586
- ```
587
-
588
- ---
589
-
590
- ## 相关文档
591
-
592
- - [String 扩展](./string-extensions.md#多语言支持)
593
- - [Locale API](./api-reference.md#locale-类)
594
- - [Validator API](./api-reference.md#validator-类)
595
-
596
- ---
597
-
598
- ## 对应示例文件
599
-
600
- **示例入口**: [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)
601
- **说明**: 覆盖 `Accept-Language` 解析、运行时 locale 选择,以及同一 schema 在不同请求语言下的验证入口。
602
-
603
- ---
604
-
605
- **最后更新**: 2026-05-08
606
- **作者**: schema-dsl Team
607
-
608
-
1
+ # 动态多语言配置指南
2
+
3
+ > **更新时间**: 2025-12-25
4
+ > **场景**: 从请求头动态获取语言配置
5
+
6
+ ---
7
+
8
+ ## 📑 目录
9
+
10
+ - [基本原理](#基本原理)
11
+ - [方案1: 验证时指定语言(推荐)](#方案1-验证时指定语言推荐)
12
+ - [方案2: 临时切换语言](#方案2-临时切换语言)
13
+ - [方案3: Express/Koa 中间件](#方案3-expresskoa-中间件)
14
+ - [完整示例](#完整示例)
15
+ - [最佳实践](#最佳实践)
16
+
17
+ ---
18
+
19
+ ## 基本原理
20
+
21
+ schema-dsl 的 `Validator` 支持在验证时动态指定语言,无需全局切换。
22
+
23
+ ### 核心方法
24
+
25
+ ```javascript
26
+ validator.validate(schema, data, {
27
+ locale: 'zh-CN' // 动态指定语言
28
+ });
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 方案1: 验证时指定语言(推荐)✅
34
+
35
+ 这是**最推荐**的方案,无需修改全局状态,支持并发请求。
36
+
37
+ ### 1.1 应用启动时配置(一次性加载所有语言)
38
+
39
+ 使用 `dsl.config` 在应用启动时一次性加载所有自定义语言包。
40
+
41
+ ```javascript
42
+ const { dsl, validate } = require('schema-dsl');
43
+ const path = require('path');
44
+
45
+ // ========== 应用启动时配置(只执行一次)==========
46
+
47
+ // 方式一:传入目录路径(推荐)⭐
48
+ // Node >=18:自动扫描目录下的 .js(CommonJS)、.cjs、.json、.jsonc、.json5 文件
49
+ dsl.config({
50
+ i18n: path.join(__dirname, 'locales')
51
+ });
52
+
53
+ // 方式二:直接传入对象
54
+ dsl.config({
55
+ i18n: {
56
+ 'fr-FR': {
57
+ 'required': '{{#label}} est requis',
58
+ 'string.minLength': '{{#label}} doit contenir au moins {{#limit}} caractères'
59
+ },
60
+ 'de-DE': {
61
+ 'required': '{{#label}} ist erforderlich',
62
+ 'string.minLength': '{{#label}} muss mindestens {{#limit}} Zeichen lang sein'
63
+ }
64
+ }
65
+ });
66
+
67
+ // 说明:
68
+ // - 只在应用启动时执行一次
69
+ // - 自动与系统内置语言包合并(用户自定义的优先)
70
+ // - 运行时无需重新加载,直接切换
71
+ ```
72
+
73
+ ### 1.2 运行时直接切换语言(无需重新加载)
74
+
75
+ ```javascript
76
+ const { dsl, validate } = require('schema-dsl');
77
+
78
+ // 定义 Schema
79
+ const schema = dsl({
80
+ username: 'string:3-32!',
81
+ email: 'email!'
82
+ });
83
+
84
+ // 测试数据
85
+ const data = { username: 'ab', email: 'invalid' };
86
+
87
+ // ========== 运行时直接切换语言 ==========
88
+
89
+ // 使用中文
90
+ const result1 = validate(schema, data, { locale: 'zh-CN' });
91
+ // 错误: "username长度不能少于3个字符"
92
+
93
+ // 使用法语
94
+ const result2 = validate(schema, data, { locale: 'fr-FR' });
95
+ // 错误: "username doit contenir au moins 3 caractères"
96
+
97
+ // 使用德语
98
+ const result3 = validate(schema, data, { locale: 'de-DE' });
99
+ // 错误: "username muss mindestens 3 Zeichen lang sein"
100
+
101
+ // 说明:
102
+ // - 无需重新加载语言包
103
+ // - 每次验证可以使用不同语言
104
+ // - 支持高并发(无全局状态修改)
105
+ ```
106
+
107
+ ### 1.3 从请求头获取语言(实际应用场景)
108
+
109
+ ```javascript
110
+ const express = require('express');
111
+ const { dsl, validate } = require('schema-dsl');
112
+ const path = require('path');
113
+
114
+ const app = express();
115
+
116
+ // ========== 应用启动时配置(只执行一次)==========
117
+ dsl.config({
118
+ i18n: path.join(__dirname, 'locales')
119
+ });
120
+
121
+ // 定义 Schema
122
+ const userSchema = dsl({
123
+ username: 'string:3-32!',
124
+ email: 'email!',
125
+ password: 'string:8-32!'
126
+ });
127
+
128
+ // ========== Express 路由 ==========
129
+ app.post('/api/user/register', (req, res) => {
130
+ // 从请求头获取语言偏好
131
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
132
+
133
+ // 验证数据(直接切换语言,无需重新加载)
134
+ const result = validate(userSchema, req.body, { locale });
135
+
136
+ if (!result.valid) {
137
+ return res.status(400).json({
138
+ errors: result.errors // 自动使用用户偏好的语言
139
+ });
140
+ }
141
+
142
+ // 处理成功...
143
+ res.json({ message: 'User registered successfully' });
144
+ });
145
+ ```
146
+
147
+ ### 1.3 解析 Accept-Language 头
148
+
149
+ ```javascript
150
+ /**
151
+ * 解析 Accept-Language 头
152
+ * @param {string} acceptLanguage - Accept-Language 头的值
153
+ * @returns {string} 语言代码
154
+ */
155
+ function parseAcceptLanguage(acceptLanguage) {
156
+ if (!acceptLanguage) return 'en-US';
157
+
158
+ // Accept-Language 格式: zh-CN,zh;q=0.9,en;q=0.8
159
+ const languages = acceptLanguage.split(',').map(lang => {
160
+ const [code, qValue] = lang.trim().split(';');
161
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
162
+ return { code: code.trim(), q };
163
+ });
164
+
165
+ // 按权重排序
166
+ languages.sort((a, b) => b.q - a.q);
167
+
168
+ // 映射到支持的语言
169
+ const supportedLocales = ['zh-CN', 'en-US', 'ja-JP'];
170
+ for (const lang of languages) {
171
+ const matched = supportedLocales.find(locale =>
172
+ locale.toLowerCase() === lang.code.toLowerCase() ||
173
+ locale.split('-')[0] === lang.code.split('-')[0]
174
+ );
175
+ if (matched) return matched;
176
+ }
177
+
178
+ return 'en-US'; // 默认语言
179
+ }
180
+
181
+ // 使用
182
+ app.post('/api/user/register', (req, res) => {
183
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
184
+
185
+ const result = validator.validate(schema, req.body, { locale });
186
+
187
+ // ...
188
+ });
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 方案2: 临时切换语言
194
+
195
+ 适用于少数场景。
196
+
197
+ ### 2.1 使用闭包保存原语言
198
+
199
+ ```javascript
200
+ function validateWithLocale(validator, schema, data, locale) {
201
+ const originalLocale = Locale.getLocale();
202
+
203
+ try {
204
+ Locale.setLocale(locale);
205
+ return validator.validate(schema, data);
206
+ } finally {
207
+ Locale.setLocale(originalLocale); // 恢复原语言
208
+ }
209
+ }
210
+
211
+ // 使用
212
+ app.post('/api/user/register', (req, res) => {
213
+ const locale = parseAcceptLanguage(req.headers['accept-language']);
214
+
215
+ const result = validateWithLocale(validator, schema, req.body, locale);
216
+
217
+ // ...
218
+ });
219
+ ```
220
+
221
+ ---
222
+
223
+ ## 方案3: Express/Koa 中间件
224
+
225
+ 封装为中间件,自动处理语言切换。
226
+
227
+ ### 3.1 Express 中间件 (推荐)
228
+
229
+ 通过中间件一次性配置,后续业务代码无需关心语言参数。
230
+
231
+ ```javascript
232
+ const { Validator } = require('schema-dsl');
233
+ const validator = new Validator();
234
+
235
+ const schemaIoMiddleware = (req, res, next) => {
236
+ // 1. 自动获取语言
237
+ const lang = req.headers['accept-language']?.split(',')[0]?.trim() || 'en-US';
238
+ // 简单匹配逻辑 (实际可使用 accept-language-parser)
239
+ const locale = lang.includes('zh') ? 'zh-CN' :
240
+ lang.includes('ja') ? 'ja-JP' :
241
+ lang.includes('es') ? 'es-ES' :
242
+ lang.includes('fr') ? 'fr-FR' : 'en-US';
243
+
244
+ // 2. 挂载绑定了语言的验证方法
245
+ req.validate = (schema, data) => {
246
+ return validator.validate(schema, data, { locale });
247
+ };
248
+
249
+ next();
250
+ };
251
+
252
+ app.use(schemaIoMiddleware);
253
+
254
+ // 业务中使用
255
+ app.post('/users', (req, res) => {
256
+ // 直接调用,自动使用中间件解析的语言
257
+ const result = req.validate(userSchema, req.body);
258
+
259
+ if (!result.valid) {
260
+ return res.status(400).json({ errors: result.errors });
261
+ }
262
+
263
+ // ...
264
+ });
265
+ ```
266
+
267
+ 完整示例请参考 [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)。
268
+
269
+ ### 3.2 Koa 中间件
270
+
271
+ ```javascript
272
+ const { Locale, Validator } = require('schema-dsl');
273
+
274
+ const validator = new Validator();
275
+
276
+ /**
277
+ * Koa 语言中间件
278
+ */
279
+ function localeMiddleware() {
280
+ return async (ctx, next) => {
281
+ // 解析语言
282
+ const locale = parseAcceptLanguage(ctx.headers['accept-language']);
283
+
284
+ // 保存到上下文
285
+ ctx.locale = locale;
286
+
287
+ // 复用共享 Validator,避免每个请求都重新建立实例和缓存
288
+ ctx.validate = function(schema, data) {
289
+ return validator.validate(schema, data, { locale: ctx.locale });
290
+ };
291
+
292
+ await next();
293
+ };
294
+ }
295
+
296
+ // 应用中间件
297
+ app.use(localeMiddleware());
298
+
299
+ // 使用
300
+ router.post('/api/user/register', async (ctx) => {
301
+ // 自动使用请求的语言
302
+ const result = ctx.validate(userSchema, ctx.request.body);
303
+
304
+ if (!result.valid) {
305
+ ctx.status = 400;
306
+ ctx.body = { errors: result.errors };
307
+ return;
308
+ }
309
+
310
+ // ...
311
+ });
312
+ ```
313
+
314
+ ---
315
+
316
+ ## 完整示例
317
+
318
+ ### Express 完整示例
319
+
320
+ ```javascript
321
+ const express = require('express');
322
+ const { dsl, Validator, Locale } = require('schema-dsl');
323
+
324
+ const app = express();
325
+ app.use(express.json());
326
+
327
+ // ========== 1. 初始化语言包 ==========
328
+
329
+ Locale.addLocale('zh-CN', {
330
+ 'required': '{{#label}}不能为空',
331
+ 'min': '{{#label}}至少{{#limit}}个字符',
332
+ 'max': '{{#label}}最多{{#limit}}个字符',
333
+ 'pattern': '{{#label}}格式不正确',
334
+ 'format': '请输入有效的{{#label}}'
335
+ });
336
+
337
+ Locale.addLocale('en-US', {
338
+ 'required': '{{#label}} is required',
339
+ 'min': '{{#label}} must be at least {{#limit}} characters',
340
+ 'max': '{{#label}} must be at most {{#limit}} characters',
341
+ 'pattern': '{{#label}} format is invalid',
342
+ 'format': 'Please enter a valid {{#label}}'
343
+ });
344
+
345
+ // ========== 2. 工具函数 ==========
346
+
347
+ function parseAcceptLanguage(acceptLanguage) {
348
+ if (!acceptLanguage) return 'en-US';
349
+
350
+ const languages = acceptLanguage.split(',').map(lang => {
351
+ const [code, qValue] = lang.trim().split(';');
352
+ const q = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
353
+ return { code: code.trim(), q };
354
+ });
355
+
356
+ languages.sort((a, b) => b.q - a.q);
357
+
358
+ const supportedLocales = ['zh-CN', 'en-US'];
359
+ for (const lang of languages) {
360
+ const matched = supportedLocales.find(locale =>
361
+ locale.toLowerCase() === lang.code.toLowerCase()
362
+ );
363
+ if (matched) return matched;
364
+ }
365
+
366
+ return 'en-US';
367
+ }
368
+
369
+ // ========== 3. 中间件 ==========
370
+
371
+ const validator = new Validator();
372
+
373
+ function localeMiddleware(req, res, next) {
374
+ req.locale = parseAcceptLanguage(req.headers['accept-language']);
375
+
376
+ req.validate = function(schema, data) {
377
+ return validator.validate(schema, data, { locale: req.locale });
378
+ };
379
+
380
+ next();
381
+ }
382
+
383
+ app.use(localeMiddleware);
384
+
385
+ // ========== 4. 定义Schema ==========
386
+
387
+ const userSchema = dsl({
388
+ username: 'string:3-32!'.label('用户名'),
389
+ email: 'email!'.label('邮箱地址'),
390
+ password: 'string:8-64!'
391
+ .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/)
392
+ .label('密码')
393
+ .messages({
394
+ 'pattern': '密码必须包含大小写字母和数字'
395
+ }),
396
+ age: 'number:18-120'.label('年龄')
397
+ });
398
+
399
+ // ========== 5. API 路由 ==========
400
+
401
+ app.post('/api/user/register', (req, res) => {
402
+ // 验证数据(自动使用请求语言)
403
+ const result = req.validate(userSchema, req.body);
404
+
405
+ if (!result.valid) {
406
+ return res.status(400).json({
407
+ success: false,
408
+ errors: result.errors,
409
+ locale: req.locale // 返回使用的语言
410
+ });
411
+ }
412
+
413
+ // 处理注册逻辑
414
+ res.json({
415
+ success: true,
416
+ message: req.locale === 'zh-CN' ? '注册成功' : 'Registration successful'
417
+ });
418
+ });
419
+
420
+ // ========== 6. 测试 ==========
421
+
422
+ app.listen(3000, () => {
423
+ console.log('Server running on http://localhost:3000');
424
+ console.log('\n测试命令:');
425
+ console.log('# 中文错误消息');
426
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
427
+ console.log(' -H "Content-Type: application/json" \\');
428
+ console.log(' -H "Accept-Language: zh-CN" \\');
429
+ console.log(' -d \'{"username":"ab"}\'');
430
+ console.log('\n# 英文错误消息');
431
+ console.log('curl -X POST http://localhost:3000/api/user/register \\');
432
+ console.log(' -H "Content-Type: application/json" \\');
433
+ console.log(' -H "Accept-Language: en-US" \\');
434
+ console.log(' -d \'{"username":"ab"}\'');
435
+ });
436
+ ```
437
+
438
+ ---
439
+
440
+ ## 最佳实践
441
+
442
+ ### 1. 语言包集中管理
443
+
444
+ ```javascript
445
+ // locales/index.js
446
+ module.exports = {
447
+ 'zh-CN': require('./zh-CN.json'),
448
+ 'en-US': require('./en-US.json'),
449
+ 'ja-JP': require('./ja-JP.json')
450
+ };
451
+
452
+ // locales/zh-CN.json
453
+ {
454
+ "required": "{{#label}}不能为空",
455
+ "min": "{{#label}}至少{{#limit}}个字符",
456
+ "max": "{{#label}}最多{{#limit}}个字符",
457
+ "pattern": "{{#label}}格式不正确",
458
+ "format": "请输入有效的{{#label}}"
459
+ }
460
+
461
+ // 初始化
462
+ const locales = require('./locales');
463
+ Object.entries(locales).forEach(([locale, messages]) => {
464
+ Locale.addLocale(locale, messages);
465
+ });
466
+ ```
467
+
468
+ ### 2. 支持的语言列表
469
+
470
+ ```javascript
471
+ const SUPPORTED_LOCALES = ['zh-CN', 'en-US', 'ja-JP'];
472
+
473
+ function getSupportedLocale(requestLocale) {
474
+ return SUPPORTED_LOCALES.includes(requestLocale)
475
+ ? requestLocale
476
+ : 'en-US';
477
+ }
478
+ ```
479
+
480
+ ### 3. 缓存验证器
481
+
482
+ ```javascript
483
+ // 为每个语言缓存验证器
484
+ const validators = {
485
+ 'zh-CN': new Validator(),
486
+ 'en-US': new Validator(),
487
+ 'ja-JP': new Validator()
488
+ };
489
+
490
+ function getValidator(locale) {
491
+ return validators[locale] || validators['en-US'];
492
+ }
493
+
494
+ // 使用
495
+ const result = getValidator(req.locale).validate(
496
+ schema,
497
+ data,
498
+ { locale: req.locale }
499
+ );
500
+ ```
501
+
502
+ ### 4. 错误响应标准化
503
+
504
+ ```javascript
505
+ function sendValidationError(res, result, locale) {
506
+ res.status(400).json({
507
+ success: false,
508
+ code: 'VALIDATION_ERROR',
509
+ message: locale === 'zh-CN' ? '验证失败' : 'Validation failed',
510
+ errors: result.errors,
511
+ locale: locale
512
+ });
513
+ }
514
+
515
+ // 使用
516
+ if (!result.valid) {
517
+ return sendValidationError(res, result, req.locale);
518
+ }
519
+ ```
520
+
521
+ ---
522
+
523
+ ## 方案对比
524
+
525
+ | 方案 | 优点 | 缺点 | 推荐度 |
526
+ |------|------|------|--------|
527
+ | **方案1: 验证时指定** | ✅ 无竞态问题<br>✅ 支持并发<br>✅ 代码简洁 | - | ⭐⭐⭐⭐⭐ |
528
+ | 方案2: 临时切换 | ✅ 实现简单 | ⚠️ 并发竞态问题 | ⭐⭐⭐ |
529
+ | 方案3: 中间件 | ✅ 自动化<br>✅ 统一管理<br>✅ 可复用共享 Validator 缓存 | - | ⭐⭐⭐⭐⭐ |
530
+
531
+ **推荐**: 方案1 + 方案3(中间件封装)
532
+
533
+ ---
534
+
535
+ ## 常见问题
536
+
537
+ ### Q1: 如何处理不支持的语言?
538
+
539
+ **A**: 回退到默认语言
540
+
541
+ 不要直接把原始 `Accept-Language` 头透传给 `locale`;浏览器常见值会带 `q=` 权重,应该先解析再回退。
542
+
543
+ ```javascript
544
+ function parseAcceptLanguage(acceptLanguage) {
545
+ // ...解析逻辑
546
+ return supportedLocale || 'en-US'; // 默认英文
547
+ }
548
+ ```
549
+
550
+ ### Q2: 是否支持动态加载语言包?
551
+
552
+ **A**: 支持
553
+
554
+ ```javascript
555
+ async function loadLocale(locale) {
556
+ if (!Locale.getAvailableLocales().includes(locale)) {
557
+ const messages = await import(`./locales/${locale}.json`);
558
+ Locale.addLocale(locale, messages);
559
+ }
560
+ }
561
+
562
+ // 使用
563
+ app.use(async (req, res, next) => {
564
+ await loadLocale(req.locale);
565
+ next();
566
+ });
567
+ ```
568
+
569
+ ### Q3: 如何自定义某些字段的错误消息?
570
+
571
+ **A**: 使用 `.messages()` 方法
572
+
573
+ ```javascript
574
+ const schema = dsl({
575
+ password: 'string:8-64!'
576
+ .label('密码')
577
+ .messages({
578
+ 'required': req.locale === 'zh-CN'
579
+ ? '请输入密码'
580
+ : 'Please enter password',
581
+ 'min': req.locale === 'zh-CN'
582
+ ? '密码太短了,至少8个字符'
583
+ : 'Password is too short, at least 8 characters'
584
+ })
585
+ });
586
+ ```
587
+
588
+ ---
589
+
590
+ ## 相关文档
591
+
592
+ - [String 扩展](./string-extensions.md#多语言支持)
593
+ - [Locale API](./api-reference.md#locale-类)
594
+ - [Validator API](./api-reference.md#validator-类)
595
+
596
+ ---
597
+
598
+ ## 对应示例文件
599
+
600
+ **示例入口**: [dynamic-locale.ts](https://github.com/vextjs/schema-dsl/blob/main/examples/docs/dynamic-locale.ts)
601
+ **说明**: 覆盖 `Accept-Language` 解析、运行时 locale 选择,以及同一 schema 在不同请求语言下的验证入口。
602
+
603
+ ---
604
+
605
+ **最后更新**: 2026-05-08
606
+ **作者**: schema-dsl Team
607
+
608
+