@maiyunnet/kebab 9.14.0 → 9.14.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.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.14.0";
8
+ export declare const VER = "9.14.2";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.14.0';
9
+ export const VER = '9.14.2';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/core.d.ts CHANGED
@@ -133,6 +133,23 @@ export declare const REAL_IP_CF = "cf-connecting-ip";
133
133
  * @param name 输入安全的 header
134
134
  */
135
135
  export declare function realIP(ctr: sCtr.Ctr, name?: string): string;
136
+ /**
137
+ * --- 截取 IP 的限速段 ---
138
+ * --- IPv4 原样返回;IPv6 截取前 mask 位(默认 /64),防止同一人使用大量 IPv6 地址绕过限速 ---
139
+ * @param ip IP 地址
140
+ * @param mask IPv6 前缀长度,0-128 的整数,默认 64
141
+ * @returns 限速段,如 192.168.1.1、2001:db8:1234:5678::
142
+ */
143
+ export declare function ipLimit(ip: string, mask?: number): string;
144
+ /**
145
+ * --- 获取安全 IP 的限速段(用于 ratelimit 库的 key) ---
146
+ * --- IPv4 原样返回;IPv6 截取前 mask 位(默认 /64) ---
147
+ * @param ctr Ctr 实例
148
+ * @param name 输入安全的 header
149
+ * @param mask IPv6 前缀长度,0-128 的整数,默认 64
150
+ * @returns 限速段,如 192.168.1.1、2001:db8:1234:5678::
151
+ */
152
+ export declare function realIPLimit(ctr: sCtr.Ctr, name?: string, mask?: number): string;
136
153
  /**
137
154
  * --- 间隔一段时间 ---
138
155
  * @param ms 间隔毫秒
package/lib/core.js CHANGED
@@ -307,10 +307,11 @@ export function muid(ctr, opt = {}) {
307
307
  export function ip(ctr, req) {
308
308
  const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
309
309
  if (typeof headers['cf-connecting-ip'] === 'string') {
310
- return headers['cf-connecting-ip'];
310
+ return normalizeIP(headers['cf-connecting-ip']);
311
311
  }
312
312
  else if (typeof headers['x-forwarded-for'] === 'string') {
313
- return headers['x-forwarded-for'];
313
+ // --- X-Forwarded-For 可能为逗号分隔的多个 IP,取第一个即客户端 IP ---
314
+ return normalizeIP(headers['x-forwarded-for'].split(',')[0].trim());
314
315
  }
315
316
  else {
316
317
  if (!req) {
@@ -321,21 +322,37 @@ export function ip(ctr, req) {
321
322
  return '';
322
323
  }
323
324
  }
324
- return req.socket.remoteAddress ?? '';
325
+ return normalizeIP(req.socket.remoteAddress ?? '');
325
326
  }
326
327
  }
327
328
  /** --- 获取 CF 和 X 的 IP --- */
328
329
  export function ips(ctr) {
329
330
  const headers = ctr instanceof sCtr.Ctr ? ctr.getPrototype('_headers') : ctr;
330
331
  return {
331
- 'cf': typeof headers['cf-connecting-ip'] === 'string' ? headers['cf-connecting-ip'] : '',
332
- 'x': typeof headers['x-forwarded-for'] === 'string' ? headers['x-forwarded-for'] : ''
332
+ 'cf': typeof headers['cf-connecting-ip'] === 'string' ? normalizeIP(headers['cf-connecting-ip']) : '',
333
+ 'x': typeof headers['x-forwarded-for'] === 'string' ? normalizeIP(headers['x-forwarded-for'].split(',')[0].trim()) : ''
333
334
  };
334
335
  }
335
336
  /** --- 使用 X-Forwarded-For 的 CDN 厂商 --- */
336
337
  export const REAL_IP_X = 'x-forwarded-for';
337
338
  /** --- 使用的是 Cloudflare --- */
338
339
  export const REAL_IP_CF = 'cf-connecting-ip';
340
+ /**
341
+ * --- 规范化 IP 地址 ---
342
+ * --- 将 IPv4-mapped IPv6(如 ::ffff:127.0.0.1)转换为纯 IPv4,原生 IPv6 保持不变 ---
343
+ * @param ip 原始 IP 地址
344
+ */
345
+ function normalizeIP(ip) {
346
+ if (!ip) {
347
+ return '';
348
+ }
349
+ const value = ip.trim();
350
+ const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(value);
351
+ if (mapped) {
352
+ return mapped[1];
353
+ }
354
+ return value;
355
+ }
339
356
  /**
340
357
  * --- 获取直连 IP(安全 IP) ---
341
358
  * @param ctr
@@ -346,11 +363,150 @@ export function realIP(ctr, name = '') {
346
363
  if (name !== '') {
347
364
  const value = headers[name];
348
365
  if (typeof value === 'string') {
349
- return value;
366
+ return normalizeIP(value);
350
367
  }
351
368
  }
352
369
  const req = ctr.getPrototype('_req');
353
- return req.socket.remoteAddress ?? '';
370
+ return normalizeIP(req.socket.remoteAddress ?? '');
371
+ }
372
+ /**
373
+ * --- 截取 IP 的限速段 ---
374
+ * --- IPv4 原样返回;IPv6 截取前 mask 位(默认 /64),防止同一人使用大量 IPv6 地址绕过限速 ---
375
+ * @param ip IP 地址
376
+ * @param mask IPv6 前缀长度,0-128 的整数,默认 64
377
+ * @returns 限速段,如 192.168.1.1、2001:db8:1234:5678::
378
+ */
379
+ export function ipLimit(ip, mask = 64) {
380
+ if (!ip) {
381
+ return '';
382
+ }
383
+ const value = ip.trim();
384
+ if (net.isIP(value) === 6) {
385
+ return ipv6Segment(value, mask);
386
+ }
387
+ // --- IPv4 或无法识别的字符串原样返回 ---
388
+ return value;
389
+ }
390
+ /**
391
+ * --- 获取安全 IP 的限速段(用于 ratelimit 库的 key) ---
392
+ * --- IPv4 原样返回;IPv6 截取前 mask 位(默认 /64) ---
393
+ * @param ctr Ctr 实例
394
+ * @param name 输入安全的 header
395
+ * @param mask IPv6 前缀长度,0-128 的整数,默认 64
396
+ * @returns 限速段,如 192.168.1.1、2001:db8:1234:5678::
397
+ */
398
+ export function realIPLimit(ctr, name = '', mask = 64) {
399
+ return ipLimit(realIP(ctr, name), mask);
400
+ }
401
+ /**
402
+ * --- 截取 IPv6 地址的前缀段 ---
403
+ * @param ip IPv6 地址
404
+ * @param mask 前缀长度,0-128
405
+ */
406
+ function ipv6Segment(ip, mask) {
407
+ if (mask >= 128) {
408
+ return ip;
409
+ }
410
+ if (mask <= 0) {
411
+ return '::';
412
+ }
413
+ // --- 展开为 8 组 16 进制字符串 ---
414
+ const groups = expandIPv6(ip);
415
+ if (!groups) {
416
+ return ip;
417
+ }
418
+ /** --- 完整保留的组数 --- */
419
+ const full = Math.floor(mask / 16);
420
+ /** --- 下一组需保留的位数 --- */
421
+ const bits = mask % 16;
422
+ const parts = groups.slice(0, full);
423
+ if (bits > 0) {
424
+ // --- 保留下一组的高 bits 位,其余位清零 ---
425
+ const next = parseInt(groups[full], 16);
426
+ parts.push((next & (0xFFFF << (16 - bits))).toString(16));
427
+ }
428
+ // --- 补零到 8 组后按标准压缩 ---
429
+ while (parts.length < 8) {
430
+ parts.push('0');
431
+ }
432
+ return compressIPv6(parts);
433
+ }
434
+ /**
435
+ * --- 将 8 组 IPv6 分组压缩为标准 RFC 5952 表示(最长连续零段压缩为 ::) ---
436
+ * @param groups 8 组 16 进制字符串
437
+ */
438
+ function compressIPv6(groups) {
439
+ /** --- 最长连续零组的起始位置与长度 --- */
440
+ let bestStart = -1;
441
+ let bestLen = 1;
442
+ let curStart = -1;
443
+ let curLen = 0;
444
+ for (let i = 0; i < groups.length; ++i) {
445
+ if (parseInt(groups[i], 16) === 0) {
446
+ if (curStart === -1) {
447
+ curStart = i;
448
+ curLen = 0;
449
+ }
450
+ ++curLen;
451
+ if (curLen > bestLen) {
452
+ bestLen = curLen;
453
+ bestStart = curStart;
454
+ }
455
+ }
456
+ else {
457
+ curStart = -1;
458
+ curLen = 0;
459
+ }
460
+ }
461
+ /** --- 统一为小写无前导零 --- */
462
+ const norm = (g) => parseInt(g, 16).toString(16);
463
+ if (bestLen < 2) {
464
+ // --- 无连续零段,直接拼接 ---
465
+ return groups.map(norm).join(':');
466
+ }
467
+ const left = groups.slice(0, bestStart).map(norm);
468
+ const right = groups.slice(bestStart + bestLen).map(norm);
469
+ return (left.length ? left.join(':') : '') + '::' + (right.length ? right.join(':') : '');
470
+ }
471
+ /**
472
+ * --- 将 IPv6 地址展开为 8 组 16 进制字符串数组 ---
473
+ * @param ip IPv6 地址
474
+ * @returns 8 组字符串数组,解析失败返回 null
475
+ */
476
+ function expandIPv6(ip) {
477
+ // --- 处理内嵌 IPv4(如 ::ffff:1.2.3.4、1:2:3:4:5:6:1.2.3.4) ---
478
+ let value = ip;
479
+ const ipv4Match = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(value);
480
+ if (ipv4Match) {
481
+ const v4 = ipv4Match[2].split('.').map((n) => parseInt(n, 10).toString(16).padStart(2, '0'));
482
+ value = ipv4Match[1] + v4[0] + v4[1] + ':' + v4[2] + v4[3];
483
+ }
484
+ // --- 处理 :: 压缩 ---
485
+ const dc = value.indexOf('::');
486
+ let groups;
487
+ if (dc !== -1) {
488
+ const left = value.slice(0, dc);
489
+ const right = value.slice(dc + 2);
490
+ const leftGroups = left ? left.split(':') : [];
491
+ const rightGroups = right ? right.split(':') : [];
492
+ const missing = 8 - leftGroups.length - rightGroups.length;
493
+ if (missing < 1) {
494
+ return null;
495
+ }
496
+ groups = leftGroups.concat(new Array(missing).fill('0'), rightGroups);
497
+ }
498
+ else {
499
+ groups = value.split(':');
500
+ }
501
+ if (groups.length !== 8) {
502
+ return null;
503
+ }
504
+ for (const group of groups) {
505
+ if (!/^[\da-f]{1,4}$/i.test(group)) {
506
+ return null;
507
+ }
508
+ }
509
+ return groups;
354
510
  }
355
511
  /**
356
512
  * --- 间隔一段时间 ---
package/lib/db/pool.js CHANGED
@@ -82,11 +82,12 @@ async function checkConnection() {
82
82
  }
83
83
  setTimeout(function () {
84
84
  checkConnection().catch(e => { lCore.display('[DB][checkConnection]', e); });
85
- }, 10_000);
85
+ }, 10_000).unref();
86
86
  }
87
+ // --- 模块级巡检定时器,unref 防止在一次性 CLI 进程中阻止退出(服务进程内有其他句柄保持存活,行为不变) ---
87
88
  setTimeout(function () {
88
89
  checkConnection().catch(e => { lCore.display('[DB][checkConnection]', e); });
89
- }, 10_000);
90
+ }, 10_000).unref();
90
91
  /** --- 数据库连接池对象 --- */
91
92
  export class Pool {
92
93
  /** --- SQL 执行次数 --- */
package/lib/sql.d.ts CHANGED
@@ -42,6 +42,8 @@ export declare class Sql {
42
42
  private _placeholderCounter;
43
43
  /** --- 是否忽略错误 --- */
44
44
  private _ignore;
45
+ /** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
46
+ private _hint;
45
47
  constructor(opt: {
46
48
  'service': ESERVICE;
47
49
  'ctr'?: ctr.Ctr;
@@ -51,6 +53,30 @@ export declare class Sql {
51
53
  'sql'?: string[];
52
54
  'alias'?: string[];
53
55
  });
56
+ /**
57
+ * --- 设置 MySQL 优化器 Hint ---
58
+ * --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
59
+ * --- 传入 hint 内容即可,无需包裹注释符号 ---
60
+ * @param h Hint 内容
61
+ * @example
62
+ * // --- 强制走指定索引(最常用) ---
63
+ * INDEX(`supply_date_0_0` `idx_supply_date_query`)
64
+ *
65
+ * // --- 指定某表不用某索引 ---
66
+ * NO_INDEX(`supply_date_0_0` `idx_old`)
67
+ *
68
+ * // --- 指定 JOIN 顺序和索引 ---
69
+ * JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
70
+ *
71
+ * // --- 指定 JOIN 中某表使用的索引 ---
72
+ * JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
73
+ *
74
+ * // --- 多表多索引组合 ---
75
+ * INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
76
+ *
77
+ * // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
78
+ */
79
+ hint(h: string): this;
54
80
  /**
55
81
  * --- 插入数据前导 ---
56
82
  * @param table 表名
package/lib/sql.js CHANGED
@@ -45,6 +45,8 @@ export class Sql {
45
45
  _placeholderCounter = 1;
46
46
  /** --- 是否忽略错误 --- */
47
47
  _ignore = false;
48
+ /** --- MySQL 优化器 Hint,如 INDEX(table idx) --- */
49
+ _hint = '';
48
50
  // --- 实例化 ---
49
51
  constructor(opt) {
50
52
  this._ctr = opt.ctr;
@@ -60,6 +62,33 @@ export class Sql {
60
62
  this._alias = opt.alias;
61
63
  }
62
64
  }
65
+ /**
66
+ * --- 设置 MySQL 优化器 Hint ---
67
+ * --- 会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
68
+ * --- 传入 hint 内容即可,无需包裹注释符号 ---
69
+ * @param h Hint 内容
70
+ * @example
71
+ * // --- 强制走指定索引(最常用) ---
72
+ * INDEX(`supply_date_0_0` `idx_supply_date_query`)
73
+ *
74
+ * // --- 指定某表不用某索引 ---
75
+ * NO_INDEX(`supply_date_0_0` `idx_old`)
76
+ *
77
+ * // --- 指定 JOIN 顺序和索引 ---
78
+ * JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)
79
+ *
80
+ * // --- 指定 JOIN 中某表使用的索引 ---
81
+ * JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)
82
+ *
83
+ * // --- 多表多索引组合 ---
84
+ * INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)
85
+ *
86
+ * // --- 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html ---
87
+ */
88
+ hint(h) {
89
+ this._hint = h;
90
+ return this;
91
+ }
63
92
  // --- 前导 ---
64
93
  /**
65
94
  * --- 插入数据前导 ---
@@ -880,7 +909,7 @@ export class Sql {
880
909
  'data': data,
881
910
  'sql': sql,
882
911
  'alias': lCore.clone(this._alias),
883
- });
912
+ }).hint(this._hint);
884
913
  }
885
914
  // --- 操作 ---
886
915
  /**
@@ -888,6 +917,10 @@ export class Sql {
888
917
  */
889
918
  getSql() {
890
919
  let sql = this._sql.join('');
920
+ // --- 注入优化器 Hint 到 SELECT 关键字后 ---
921
+ if (this._hint && !sql.includes('/*+')) {
922
+ sql = sql.replace(/^SELECT /i, `SELECT /*+ ${this._hint} */ `);
923
+ }
891
924
  if (this._pre) {
892
925
  return this._alias.reduce((result, item) => {
893
926
  if (this._service === ESERVICE.MYSQL) {
package/lib/text.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Project: Kebab, User: JianSuoQiYue
3
- * Date: 2019-5-15 16:49:39
4
- * Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49
5
- */
6
1
  import * as kebab from '#kebab/index.js';
7
2
  /**
8
3
  * --- 将文件大小格式化为带单位的字符串 ---
@@ -42,15 +37,13 @@ export declare const REGEXP_EMAIL: RegExp;
42
37
  * @param email
43
38
  */
44
39
  export declare function isEMail(email: string): boolean;
45
- export declare const REGEXP_IPV4: RegExp;
46
40
  /**
47
- * --- 是否是 IPv4 ---
41
+ * --- 是否是 IPv4(基于 Node 原生 net.isIP,严格校验) ---
48
42
  * @param ip
49
43
  */
50
44
  export declare function isIPv4(ip: string): boolean;
51
- export declare const REGEXP_IPV6: RegExp;
52
45
  /**
53
- * --- 是否是 IPv6 ---
46
+ * --- 是否是 IPv6(基于 Node 原生 net.isIP,严格校验) ---
54
47
  * @param ip
55
48
  */
56
49
  export declare function isIPv6(ip: string): boolean;
package/lib/text.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Date: 2019-5-15 16:49:39
4
4
  * Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49
5
5
  */
6
+ import * as net from 'net';
6
7
  import * as kebab from '#kebab/index.js';
7
8
  import * as lFs from './fs.js';
8
9
  import * as lCore from './core.js';
@@ -219,21 +220,19 @@ export const REGEXP_EMAIL = /^[-_\w.]+@[-_\w.]+\.([a-zA-Z]+)$/i;
219
220
  export function isEMail(email) {
220
221
  return REGEXP_EMAIL.test(email);
221
222
  }
222
- export const REGEXP_IPV4 = /^[0-9]{1,3}(\.[0-9]{1,3}){3}$/i;
223
223
  /**
224
- * --- 是否是 IPv4 ---
224
+ * --- 是否是 IPv4(基于 Node 原生 net.isIP,严格校验) ---
225
225
  * @param ip
226
226
  */
227
227
  export function isIPv4(ip) {
228
- return REGEXP_IPV4.test(ip);
228
+ return net.isIP(ip) === 4;
229
229
  }
230
- export const REGEXP_IPV6 = /^(\w*?:){2,7}[\w.]*$/i;
231
230
  /**
232
- * --- 是否是 IPv6 ---
231
+ * --- 是否是 IPv6(基于 Node 原生 net.isIP,严格校验) ---
233
232
  * @param ip
234
233
  */
235
234
  export function isIPv6(ip) {
236
- return REGEXP_IPV6.test(ip);
235
+ return net.isIP(ip) === 6;
237
236
  }
238
237
  export const REGEXP_DOMAIN = /^.+?\.((?![0-9]).)+$/i;
239
238
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.14.0",
3
+ "version": "9.14.2",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
package/sys/ctr.d.ts CHANGED
@@ -342,5 +342,9 @@ export declare class Ctr {
342
342
  'maxFileSize'?: number;
343
343
  /** --- 允许的文件扩展名(含点号),如 ['.jpg', '.png', '.pdf'] --- */
344
344
  'allowedExts'?: string[];
345
+ /** --- 单个字段(非文件)最大字节数,默认 1 MB --- */
346
+ 'maxFieldSize'?: number;
347
+ /** --- 整体请求超时时间(毫秒),默认 5 分钟,设为 0 禁用超时 --- */
348
+ 'timeout'?: number;
345
349
  }): Promise<boolean>;
346
350
  }
package/sys/mod.d.ts CHANGED
@@ -80,6 +80,8 @@ export default class Mod {
80
80
  'key': string;
81
81
  'list': string[];
82
82
  };
83
+ /** --- MySQL 优化器 Hint,如 `INDEX(\`t\` \`idx_xx\`)`,注入到 SELECT 后 --- */
84
+ 'hint'?: string;
83
85
  /** --- MySQL 表前缀或 PostgreSQL Schema 名,优先级:选项 > 类属性 > 配置 --- */
84
86
  'pre'?: string;
85
87
  });
@@ -208,6 +210,7 @@ export default class Mod {
208
210
  'key': string;
209
211
  'list': string[];
210
212
  };
213
+ 'hint'?: string;
211
214
  }): T & Record<string, any>;
212
215
  /**
213
216
  * --- 通过 where 条件获取模型 ---
@@ -224,6 +227,7 @@ export default class Mod {
224
227
  'key': string;
225
228
  'list': string[];
226
229
  };
230
+ 'hint'?: string;
227
231
  }): T & Record<string, any>;
228
232
  /**
229
233
  * --- 获取创建对象,通常用于新建数据库条目 ---
@@ -248,6 +252,7 @@ export default class Mod {
248
252
  'index'?: string | string[];
249
253
  /** --- 通过 key 字段获取,默认为 false,即从主键获取 --- */
250
254
  'key'?: boolean;
255
+ 'hint'?: string;
251
256
  }): Promise<false | null | (T & Record<string, any>)>;
252
257
  static one(db: lDb.Pool | lDb.Transaction, s: string | kebab.Json, opt: {
253
258
  'ctr'?: sCtr.Ctr;
@@ -256,6 +261,7 @@ export default class Mod {
256
261
  'index'?: string | string[];
257
262
  'select'?: string | string[];
258
263
  'by'?: [string | string[], 'DESC' | 'ASC'];
264
+ 'hint'?: string;
259
265
  'array': true;
260
266
  }): Promise<false | null | Record<string, any>>;
261
267
  static one<T extends Mod>(db: lDb.Pool | lDb.Transaction, s: string | kebab.Json, opt?: {
@@ -265,6 +271,7 @@ export default class Mod {
265
271
  'index'?: string | string[];
266
272
  'select'?: string | string[];
267
273
  'by'?: [string | string[], 'DESC' | 'ASC'];
274
+ 'hint'?: string;
268
275
  'array'?: false;
269
276
  }): Promise<false | null | (T & Record<string, any>)>;
270
277
  /**
@@ -279,6 +286,7 @@ export default class Mod {
279
286
  'pre'?: string;
280
287
  'index'?: string | string[];
281
288
  'select'?: string | string[];
289
+ 'hint'?: string;
282
290
  }): Promise<false | null | Record<string, any>>;
283
291
  /**
284
292
  * --- 根据 where 条件获取主键值列表 ---
@@ -414,6 +422,30 @@ export default class Mod {
414
422
  * @param pre 前缀,仅与主表的 pre 不同时传入
415
423
  */
416
424
  crossJoin(f: string, s: kebab.Json, index?: string, pre?: string): this;
425
+ /**
426
+ * --- 设置 MySQL 优化器 Hint ---
427
+ * --- 内容会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
428
+ * --- 必须在查询入口调用之后、执行方法之前链式调用 ---
429
+ * @param h Hint 内容
430
+ * @example
431
+ * // 强制走指定索引(最常用)
432
+ * .hint("INDEX(`supply_date_0_0` `idx_supply_date_query`)")
433
+ *
434
+ * // 指定某表不用某索引
435
+ * .hint("NO_INDEX(`supply_date_0_0` `idx_old`)")
436
+ *
437
+ * // 指定 JOIN 顺序和索引
438
+ * .hint("JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)")
439
+ *
440
+ * // 指定 JOIN 中某表使用的索引
441
+ * .hint("JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)")
442
+ *
443
+ * // 多表多索引组合
444
+ * .hint("INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)")
445
+ *
446
+ * // 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
447
+ */
448
+ hint(h: string): this;
417
449
  /**
418
450
  * --- 筛选器 ---
419
451
  * @param s 筛选条件数组或字符串
package/sys/mod.js CHANGED
@@ -89,6 +89,10 @@ export default class Mod {
89
89
  'ctr': opt.ctr,
90
90
  'pre': opt.pre ?? this.constructor._$pre,
91
91
  });
92
+ /** --- 设置优化器 Hint --- */
93
+ if (opt.hint) {
94
+ this._sql.hint(opt.hint);
95
+ }
92
96
  if (opt.index) {
93
97
  this._index = typeof opt.index === 'string' ? [opt.index] : [...new Set(opt.index)];
94
98
  }
@@ -375,7 +379,8 @@ export default class Mod {
375
379
  'select': c,
376
380
  'index': opt.index,
377
381
  'alias': opt.alias,
378
- 'contain': opt.contain
382
+ 'contain': opt.contain,
383
+ 'hint': opt.hint,
379
384
  });
380
385
  }
381
386
  /**
@@ -393,6 +398,7 @@ export default class Mod {
393
398
  'index': opt.index,
394
399
  'contain': opt.contain,
395
400
  'alias': opt.alias,
401
+ 'hint': opt.hint,
396
402
  });
397
403
  }
398
404
  /**
@@ -427,6 +433,7 @@ export default class Mod {
427
433
  [opt.key ? this._$key : this._$primary]: val,
428
434
  }],
429
435
  'index': opt.index,
436
+ 'hint': opt.hint,
430
437
  }).first(opt.lock);
431
438
  }
432
439
  /**
@@ -444,6 +451,7 @@ export default class Mod {
444
451
  'ctr': opt.ctr,
445
452
  'pre': opt.pre,
446
453
  'where': s,
454
+ 'hint': opt.hint,
447
455
  });
448
456
  if (opt.by) {
449
457
  o.by(opt.by[0], opt.by[1]);
@@ -459,6 +467,7 @@ export default class Mod {
459
467
  'pre': opt.pre,
460
468
  'where': s,
461
469
  'index': item,
470
+ 'hint': opt.hint,
462
471
  });
463
472
  if (opt.by) {
464
473
  row.by(opt.by[0], opt.by[1]);
@@ -1307,6 +1316,33 @@ export default class Mod {
1307
1316
  this._sql.crossJoin(f, s, index ? '_' + index : '', pre);
1308
1317
  return this;
1309
1318
  }
1319
+ /**
1320
+ * --- 设置 MySQL 优化器 Hint ---
1321
+ * --- 内容会以 Optimizer Hint 注释语法注入到 SELECT 关键字后 ---
1322
+ * --- 必须在查询入口调用之后、执行方法之前链式调用 ---
1323
+ * @param h Hint 内容
1324
+ * @example
1325
+ * // 强制走指定索引(最常用)
1326
+ * .hint("INDEX(`supply_date_0_0` `idx_supply_date_query`)")
1327
+ *
1328
+ * // 指定某表不用某索引
1329
+ * .hint("NO_INDEX(`supply_date_0_0` `idx_old`)")
1330
+ *
1331
+ * // 指定 JOIN 顺序和索引
1332
+ * .hint("JOIN_ORDER(`a` `b`) INDEX(`a` `idx_a`) INDEX(`b` `idx_b`)")
1333
+ *
1334
+ * // 指定 JOIN 中某表使用的索引
1335
+ * .hint("JOIN_INDEX(`supply_date_0_0` `idx_supply_date_query`)")
1336
+ *
1337
+ * // 多表多索引组合
1338
+ * .hint("INDEX(`t1` `idx_a`) JOIN_INDEX(`t2` `idx_b`)")
1339
+ *
1340
+ * // 官方文档: https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
1341
+ */
1342
+ hint(h) {
1343
+ this._sql.hint(h);
1344
+ return this;
1345
+ }
1310
1346
  /**
1311
1347
  * --- 筛选器 ---
1312
1348
  * @param s 筛选条件数组或字符串
package/sys/route.d.ts CHANGED
@@ -77,6 +77,10 @@ export declare function getFormData(req: http2.Http2ServerRequest | http.Incomin
77
77
  'maxFileSize'?: number;
78
78
  /** --- 允许的文件扩展名(含点号),如 ['.jpg', '.png', '.pdf'] --- */
79
79
  'allowedExts'?: string[];
80
+ /** --- 单个字段(非文件)最大字节数,默认 1 MB --- */
81
+ 'maxFieldSize'?: number;
82
+ /** --- 整体请求超时时间(毫秒),默认 5 分钟,设为 0 禁用超时 --- */
83
+ 'timeout'?: number;
80
84
  }): Promise<{
81
85
  'post': Record<string, kebab.Json>;
82
86
  'files': Record<string, kebab.IPostFile | kebab.IPostFile[]>;