@maiyunnet/kebab 9.4.1 → 9.6.0

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.4.1";
8
+ export declare const VER = "9.6.0";
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.4.1';
9
+ export const VER = '9.6.0';
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
@@ -167,12 +167,18 @@ export declare function exec(command: string, options?: {
167
167
  * --- 向主进程(或局域网同代码机子)发送广播将进行 reload 操作,等待回传 ---
168
168
  * --- 主要作用除代码热更新以外的其他情况 ---
169
169
  */
170
- export declare function sendReload(hosts?: string[] | 'config'): Promise<string[]>;
170
+ export declare function sendReload(hosts?: string[] | 'config'): Promise<Record<string, {
171
+ 'result': boolean;
172
+ 'return': string;
173
+ }>>;
171
174
  /**
172
175
  * --- 向主进程(或局域网同代码机子)发送广播将进行 restart 操作,停止监听并启动新进程,老进程在连接全部断开后自行销毁 ---
173
176
  * --- 主要用作不间断的代码热更新 ---
174
177
  */
175
- export declare function sendRestart(hosts?: string[] | 'config'): Promise<string[]>;
178
+ export declare function sendRestart(hosts?: string[] | 'config'): Promise<Record<string, {
179
+ 'result': boolean;
180
+ 'return': string;
181
+ }>>;
176
182
  /** --- PM2 操作类型 --- */
177
183
  export type TPm2Action = 'start' | 'stop' | 'restart';
178
184
  /**
@@ -181,13 +187,19 @@ export type TPm2Action = 'start' | 'stop' | 'restart';
181
187
  * @param action PM2 操作类型
182
188
  * @param hosts 局域网列表
183
189
  */
184
- export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<string[]>;
190
+ export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<Record<string, {
191
+ 'result': boolean;
192
+ 'return': string;
193
+ }>>;
185
194
  /**
186
195
  * --- 向本机或局域网 RPC 发送 npm install 操作 ---
187
196
  * @param path 路径,如 /home/kebab/
188
- * @param hosts 局域网列表
197
+ * @param hosts 局域网列表,不填则代表本机
189
198
  */
190
- export declare function sendNpm(path: string, hosts?: string[] | 'config'): Promise<string[]>;
199
+ export declare function sendNpm(path: string, hosts?: string[] | 'config'): Promise<Record<string, {
200
+ 'result': boolean;
201
+ 'return': string;
202
+ }>>;
191
203
  /** --- 跨进程全局变量 --- */
192
204
  export declare const global: Record<string, any>;
193
205
  /**
@@ -196,13 +208,19 @@ export declare const global: Record<string, any>;
196
208
  * @param data 变量值
197
209
  * @param hosts 局域网列表
198
210
  */
199
- export declare function setGlobal(key: string, data: any, hosts?: string[] | 'config'): Promise<string[]>;
211
+ export declare function setGlobal(key: string, data: any, hosts?: string[] | 'config'): Promise<Record<string, {
212
+ 'result': boolean;
213
+ 'return': string;
214
+ }>>;
200
215
  /**
201
216
  * --- 移除某个跨线程/跨内网服务器全局变量 ---
202
217
  * @param key 变量名
203
218
  * @param hosts 局域网列表
204
219
  */
205
- export declare function removeGlobal(key: string, hosts?: string[]): Promise<string[]>;
220
+ export declare function removeGlobal(key: string, hosts?: string[] | 'config'): Promise<Record<string, {
221
+ 'result': boolean;
222
+ 'return': string;
223
+ }>>;
206
224
  /**
207
225
  * --- 上传并覆盖代码文件,config.json、kebab.json、.js.map、.ts, .gitignore 不会被覆盖和新创建 ---
208
226
  * @param sourcePath zip 文件
@@ -222,13 +240,19 @@ export declare function updateCode(sourcePath: string, path: string, hosts?: str
222
240
  * @param value 要更新的值
223
241
  * @param hosts 局域网列表
224
242
  */
225
- export declare function sendProject(path: string, key: string, value: string, hosts?: string[] | 'config'): Promise<string[]>;
243
+ export declare function sendProject(path: string, key: string, value: string, hosts?: string[] | 'config'): Promise<Record<string, {
244
+ 'result': boolean;
245
+ 'return': string;
246
+ }>>;
226
247
  /**
227
248
  * --- 向本机或局域网 RPC 发送 package.json 更新操作 ---
228
249
  * @param content package.json 文件内容
229
250
  * @param hosts 局域网列表
230
251
  */
231
- export declare function sendPackage(content: string, hosts?: string[] | 'config'): Promise<string[]>;
252
+ export declare function sendPackage(content: string, hosts?: string[] | 'config'): Promise<Record<string, {
253
+ 'result': boolean;
254
+ 'return': string;
255
+ }>>;
232
256
  /** --- log 设置的选项 --- */
233
257
  export interface ILogOptions {
234
258
  'path'?: string;
package/lib/core.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-5-3 23:54
4
- * Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53
4
+ * Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00
5
5
  */
6
6
  import * as cp from 'child_process';
7
7
  import * as http2 from 'http2';
@@ -464,7 +464,9 @@ export async function sendReload(hosts) {
464
464
  process.send({
465
465
  'action': 'reload'
466
466
  });
467
- return [];
467
+ return {
468
+ '127.0.0.1': { 'result': true, 'return': 'Done' }
469
+ };
468
470
  }
469
471
  if (hosts === 'config') {
470
472
  hosts = globalConfig.hosts;
@@ -472,7 +474,7 @@ export async function sendReload(hosts) {
472
474
  // --- 局域网模式 ---
473
475
  const time = lTime.stamp();
474
476
  /** --- 返回成功的 host --- */
475
- const rtn = [];
477
+ const rtn = {};
476
478
  for (const host of hosts) {
477
479
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
478
480
  'action': 'reload',
@@ -486,7 +488,10 @@ export async function sendReload(hosts) {
486
488
  }
487
489
  const str = content.toString();
488
490
  if (str === 'Done') {
489
- rtn.push(host);
491
+ rtn[host] = { 'result': true, 'return': 'Done' };
492
+ }
493
+ else {
494
+ rtn[host] = { 'result': false, 'return': str };
490
495
  }
491
496
  }
492
497
  return rtn;
@@ -503,7 +508,9 @@ export async function sendRestart(hosts) {
503
508
  process.send({
504
509
  'action': 'restart'
505
510
  });
506
- return [];
511
+ return {
512
+ '127.0.0.1': { 'result': true, 'return': 'Done' }
513
+ };
507
514
  }
508
515
  if (hosts === 'config') {
509
516
  hosts = globalConfig.hosts;
@@ -511,7 +518,7 @@ export async function sendRestart(hosts) {
511
518
  // --- 局域网模式 ---
512
519
  const time = lTime.stamp();
513
520
  /** --- 返回成功的 host --- */
514
- const rtn = [];
521
+ const rtn = {};
515
522
  for (const host of hosts) {
516
523
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
517
524
  'action': 'restart',
@@ -525,7 +532,10 @@ export async function sendRestart(hosts) {
525
532
  }
526
533
  const str = content.toString();
527
534
  if (str === 'Done') {
528
- rtn.push(host);
535
+ rtn[host] = { 'result': true, 'return': 'Done' };
536
+ }
537
+ else {
538
+ rtn[host] = { 'result': false, 'return': str };
529
539
  }
530
540
  }
531
541
  return rtn;
@@ -547,7 +557,7 @@ export async function sendPm2(name, action = 'restart', hosts) {
547
557
  // --- 局域网模式 ---
548
558
  const time = lTime.stamp();
549
559
  /** --- 返回成功的 host --- */
550
- const rtn = [];
560
+ const rtn = {};
551
561
  for (const host of hosts) {
552
562
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
553
563
  'action': 'pm2',
@@ -563,9 +573,10 @@ export async function sendPm2(name, action = 'restart', hosts) {
563
573
  }
564
574
  const str = content.toString();
565
575
  if (str === 'Done') {
566
- rtn.push(host);
576
+ rtn[host] = { 'result': true, 'return': 'Done' };
567
577
  }
568
578
  else {
579
+ rtn[host] = { 'result': false, 'return': str };
569
580
  debug('[CORE][sendPm2] rpc server content error:', str);
570
581
  }
571
582
  }
@@ -574,7 +585,7 @@ export async function sendPm2(name, action = 'restart', hosts) {
574
585
  /**
575
586
  * --- 向本机或局域网 RPC 发送 npm install 操作 ---
576
587
  * @param path 路径,如 /home/kebab/
577
- * @param hosts 局域网列表
588
+ * @param hosts 局域网列表,不填则代表本机
578
589
  */
579
590
  export async function sendNpm(path, hosts) {
580
591
  if (hosts === 'config') {
@@ -587,7 +598,7 @@ export async function sendNpm(path, hosts) {
587
598
  // --- 局域网模式 ---
588
599
  const time = lTime.stamp();
589
600
  /** --- 返回成功的 host --- */
590
- const rtn = [];
601
+ const rtn = {};
591
602
  for (const host of hosts) {
592
603
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
593
604
  'action': 'npm',
@@ -602,9 +613,10 @@ export async function sendNpm(path, hosts) {
602
613
  }
603
614
  const str = content.toString();
604
615
  if (str === 'Done') {
605
- rtn.push(host);
616
+ rtn[host] = { 'result': true, 'return': 'Done' };
606
617
  }
607
618
  else {
619
+ rtn[host] = { 'result': false, 'return': str };
608
620
  debug('[CORE][sendNpmInstall] rpc server content error:', str);
609
621
  }
610
622
  }
@@ -626,7 +638,9 @@ export async function setGlobal(key, data, hosts) {
626
638
  'key': key,
627
639
  'data': data
628
640
  });
629
- return [];
641
+ return {
642
+ '127.0.0.1': { 'result': true, 'return': 'Done' }
643
+ };
630
644
  }
631
645
  if (hosts === 'config') {
632
646
  hosts = globalConfig.hosts;
@@ -634,7 +648,7 @@ export async function setGlobal(key, data, hosts) {
634
648
  // --- 局域网模式 ---
635
649
  const time = lTime.stamp();
636
650
  /** --- 返回成功的 host --- */
637
- const rtn = [];
651
+ const rtn = {};
638
652
  for (const host of hosts) {
639
653
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
640
654
  'action': 'global',
@@ -648,7 +662,10 @@ export async function setGlobal(key, data, hosts) {
648
662
  }
649
663
  const str = content.toString();
650
664
  if (str === 'Done') {
651
- rtn.push(host);
665
+ rtn[host] = { 'result': true, 'return': 'Done' };
666
+ }
667
+ else {
668
+ rtn[host] = { 'result': false, 'return': str };
652
669
  }
653
670
  }
654
671
  return rtn;
@@ -730,7 +747,7 @@ export async function sendProject(path, key, value, hosts) {
730
747
  // --- 局域网模式 ---
731
748
  const time = lTime.stamp();
732
749
  /** --- 返回成功的 host --- */
733
- const rtn = [];
750
+ const rtn = {};
734
751
  for (const host of hosts) {
735
752
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
736
753
  'action': 'project',
@@ -746,9 +763,10 @@ export async function sendProject(path, key, value, hosts) {
746
763
  }
747
764
  const str = content.toString();
748
765
  if (str === 'Done') {
749
- rtn.push(host);
766
+ rtn[host] = { 'result': true, 'return': 'Done' };
750
767
  }
751
768
  else {
769
+ rtn[host] = { 'result': false, 'return': str };
752
770
  debug('[CORE][sendProject] rpc server content error:', str);
753
771
  }
754
772
  }
@@ -770,7 +788,7 @@ export async function sendPackage(content, hosts) {
770
788
  // --- 局域网模式 ---
771
789
  const time = lTime.stamp();
772
790
  /** --- 返回成功的 host --- */
773
- const rtn = [];
791
+ const rtn = {};
774
792
  for (const host of hosts) {
775
793
  const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
776
794
  'action': 'package',
@@ -785,9 +803,10 @@ export async function sendPackage(content, hosts) {
785
803
  }
786
804
  const str = resContent.toString();
787
805
  if (str === 'Done') {
788
- rtn.push(host);
806
+ rtn[host] = { 'result': true, 'return': 'Done' };
789
807
  }
790
808
  else {
809
+ rtn[host] = { 'result': false, 'return': str };
791
810
  debug('[CORE][sendPackage] rpc server content error:', str);
792
811
  }
793
812
  }
package/lib/sql.d.ts CHANGED
@@ -69,6 +69,16 @@ export declare class Sql {
69
69
  * @param conflict 冲突字段,PostgreSQL 用于指定 ON CONFLICT 字段;MySQL 时忽略,因为会对所有唯一键冲突执行更新
70
70
  */
71
71
  upsert(data: kebab.Json, conflict?: string | string[]): this;
72
+ /**
73
+ * --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
74
+ * --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
75
+ * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
76
+ * @param table 表名
77
+ * @param key 用于定位的主键/唯一键字段名
78
+ * @param cols 要更新的列名数组(不含 key)
79
+ * @param rows 数据行数组,每行顺序为 [keyVal, col1Val, col2Val, ...](与 [key, ...cols] 对应)
80
+ */
81
+ updateByValues(table: string, key: string, cols: string[], rows: any[][]): this;
72
82
  /**
73
83
  * --- '*', 'xx' ---
74
84
  * @param c 字段字符串或字段数组
@@ -237,6 +247,11 @@ export declare class Sql {
237
247
  private _isValue;
238
248
  /** --- 获取占位符 --- */
239
249
  private _placeholder;
250
+ /**
251
+ * --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
252
+ * @param v 要处理的值
253
+ */
254
+ private _pgCastSuffix;
240
255
  /**
241
256
  * --- 处理单个值,检测数据类型并返回 SQL 和 data ---
242
257
  * @param v 要处理的值
package/lib/sql.js CHANGED
@@ -166,6 +166,59 @@ export class Sql {
166
166
  }
167
167
  return this;
168
168
  }
169
+ /**
170
+ * --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
171
+ * --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
172
+ * --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
173
+ * @param table 表名
174
+ * @param key 用于定位的主键/唯一键字段名
175
+ * @param cols 要更新的列名数组(不含 key)
176
+ * @param rows 数据行数组,每行顺序为 [keyVal, col1Val, col2Val, ...](与 [key, ...cols] 对应)
177
+ */
178
+ updateByValues(table, key, cols, rows) {
179
+ this._data = [];
180
+ this._placeholderCounter = 1;
181
+ const allCols = [key, ...cols];
182
+ const quotedTable = this.field(table, this._pre);
183
+ const quotedKey = this.field(key);
184
+ if (this._service === ESERVICE.MYSQL) {
185
+ // --- MySQL 8.0.19+ VALUES ROW() 派生表语法 ---
186
+ const valueParts = [];
187
+ for (const row of rows) {
188
+ const parts = row.map(v => {
189
+ const result = this._processValue(v);
190
+ if (result.data.length > 0) {
191
+ this._data.push(...result.data);
192
+ }
193
+ return result.sql;
194
+ });
195
+ valueParts.push(`ROW(${parts.join(', ')})`);
196
+ }
197
+ const tmpCols = allCols.map(c => this.field(c)).join(', ');
198
+ const setClauses = cols.map(c => `t.${this.field(c)} = tmp.${this.field(c)}`).join(', ');
199
+ this._sql = [`UPDATE ${quotedTable} t INNER JOIN (VALUES ${valueParts.join(', ')}) AS tmp(${tmpCols}) ON t.${quotedKey} = tmp.${quotedKey} SET ${setClauses}`];
200
+ }
201
+ else {
202
+ // --- PostgreSQL 使用 UPDATE FROM (VALUES ...) ---
203
+ const valueParts = [];
204
+ for (let ri = 0; ri < rows.length; ri++) {
205
+ const row = rows[ri];
206
+ const parts = row.map(v => {
207
+ const result = this._processValue(v);
208
+ if (result.data.length > 0) {
209
+ this._data.push(...result.data);
210
+ }
211
+ // --- 第一行加显式类型转换,帮助 PostgreSQL 推断 VALUES 派生表的列类型 ---
212
+ return ri === 0 ? result.sql + this._pgCastSuffix(v) : result.sql;
213
+ });
214
+ valueParts.push(`(${parts.join(', ')})`);
215
+ }
216
+ const tmpCols = allCols.map(c => this.field(c)).join(', ');
217
+ const setClauses = cols.map(c => `${this.field(c)} = tmp.${this.field(c)}`).join(', ');
218
+ this._sql = [`UPDATE ${quotedTable} t SET ${setClauses} FROM (VALUES ${valueParts.join(', ')}) AS tmp(${tmpCols}) WHERE t.${quotedKey} = tmp.${quotedKey}`];
219
+ }
220
+ return this;
221
+ }
169
222
  /**
170
223
  * --- '*', 'xx' ---
171
224
  * @param c 字段字符串或字段数组
@@ -984,6 +1037,48 @@ export class Sql {
984
1037
  _placeholder() {
985
1038
  return this._service === ESERVICE.MYSQL ? '?' : `$${this._placeholderCounter++}`;
986
1039
  }
1040
+ /**
1041
+ * --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
1042
+ * @param v 要处理的值
1043
+ */
1044
+ _pgCastSuffix(v) {
1045
+ if (v === null || v === undefined) {
1046
+ return '';
1047
+ }
1048
+ if (typeof v === 'number') {
1049
+ return Number.isInteger(v) ? '::bigint' : '::float8';
1050
+ }
1051
+ if (typeof v === 'boolean') {
1052
+ return '::boolean';
1053
+ }
1054
+ if (v instanceof Buffer) {
1055
+ return '::bytea';
1056
+ }
1057
+ if (Array.isArray(v)) {
1058
+ // --- 函数式语法 ['FUNC(?)', [...]],不加转换 ---
1059
+ if (typeof v[0] === 'string' && v[0].includes('(')) {
1060
+ return '';
1061
+ }
1062
+ // --- POLYGON ---
1063
+ if (v[0]?.y !== undefined) {
1064
+ return '::polygon';
1065
+ }
1066
+ // --- JSON 数组或 PG 原生数组(text[]、int[] 等),
1067
+ // 不加转换,由 pg 驱动与目标列类型决定 ---
1068
+ return '';
1069
+ }
1070
+ if (typeof v === 'object') {
1071
+ // --- POINT ---
1072
+ if (v.y !== undefined) {
1073
+ return '::point';
1074
+ }
1075
+ // --- JSON 对象(用户应通过 sql.json() 包裹为字符串后传入),不加转换 ---
1076
+ return '';
1077
+ }
1078
+ // --- string:保持 unknown 类型,兼容 text/varchar/jsonb 等目标列类型;
1079
+ // 使用 sql.json() 包裹的 jsonb 数据经此路径,unknown 可隐式 cast 到 jsonb ---
1080
+ return '';
1081
+ }
987
1082
  /**
988
1083
  * --- 处理单个值,检测数据类型并返回 SQL 和 data ---
989
1084
  * @param v 要处理的值
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.4.1",
3
+ "version": "9.6.0",
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/master.js CHANGED
@@ -404,7 +404,7 @@ function createRpcListener() {
404
404
  // --- 特殊文件不能覆盖 ---
405
405
  continue;
406
406
  }
407
- if (fname.endsWith('.js.map') || fname.endsWith('.ts') || fname.endsWith('.scss') || fname.endsWith('.gitignore') || fname.endsWith('.DS_Store')) {
407
+ if (fname.endsWith('.js.map') || fname.endsWith('.ts') || fname.endsWith('.tsx') || fname.endsWith('.scss') || fname.endsWith('.gitignore') || fname.endsWith('.DS_Store')) {
408
408
  // --- 测试或开发文件不覆盖 ---
409
409
  continue;
410
410
  }
package/sys/mod.d.ts CHANGED
@@ -180,8 +180,10 @@ export default class Mod {
180
180
  /**
181
181
  * --- 批量更新数据 ---
182
182
  * @param db 数据库对象
183
- * @param data 数据列表
184
- * @param key 用于定位的主键或唯一键字段名
183
+ * @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
184
+ * 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行
185
+ * @param key 用于定位记录的字段名(主键或唯一键),该字段仅用于 WHERE 条件匹配,不会被更新;
186
+ * data 中每个元素都必须包含此字段,否则该元素会被跳过
185
187
  * @param opt 选项(opt.pre: MySQL 表前缀/PostgreSQL Schema 名)
186
188
  */
187
189
  static updateList(db: lDb.Pool | lDb.Transaction, data: Array<Record<string, any>>, key: string, opt?: {
package/sys/mod.js CHANGED
@@ -305,74 +305,53 @@ export default class Mod {
305
305
  /**
306
306
  * --- 批量更新数据 ---
307
307
  * @param db 数据库对象
308
- * @param data 数据列表
309
- * @param key 用于定位的主键或唯一键字段名
308
+ * @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
309
+ * 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行
310
+ * @param key 用于定位记录的字段名(主键或唯一键),该字段仅用于 WHERE 条件匹配,不会被更新;
311
+ * data 中每个元素都必须包含此字段,否则该元素会被跳过
310
312
  * @param opt 选项(opt.pre: MySQL 表前缀/PostgreSQL Schema 名)
311
313
  */
312
314
  static async updateList(db, data, key, opt = {}) {
313
315
  if (!data.length) {
314
316
  return true;
315
317
  }
316
- // --- 获取所有涉及的字段(除了 key) ---
317
- const columns = new Set();
318
+ // --- 按列集合分组(处理稀疏数据,保证每组内所有行的列完全一致)---
319
+ const groups = new Map();
318
320
  for (const item of data) {
319
- for (const k in item) {
320
- if (k !== key) {
321
- columns.add(k);
322
- }
321
+ const itemCols = Object.keys(item).filter(k => k !== key).sort();
322
+ if (itemCols.length === 0) {
323
+ continue;
324
+ }
325
+ const groupKey = itemCols.join('\0');
326
+ if (!groups.has(groupKey)) {
327
+ groups.set(groupKey, []);
323
328
  }
329
+ groups.get(groupKey).push(item);
324
330
  }
325
- if (columns.size === 0) {
331
+ if (groups.size === 0) {
326
332
  return true;
327
333
  }
328
- const cols = Array.from(columns);
329
- // --- 计算分批大小 ---
330
- // --- 每个字段需要 2 个占位符 (WHEN ? THEN ?),加上 WHERE IN (?) 的 1 个 ---
331
- // --- Total params per row = cols.length * 2 + 1 ---
332
- const paramCountPerRow = cols.length * 2 + 1;
333
- const batchSize = Math.floor(60000 / paramCountPerRow);
334
- const batches = [];
335
- for (let i = 0; i < data.length; i += batchSize) {
336
- batches.push(data.slice(i, i + batchSize));
337
- }
338
- for (const batch of batches) {
339
- const sq = lSql.get({
340
- 'service': db.getService() ?? lDb.ESERVICE.PGSQL,
341
- 'ctr': opt.ctr,
342
- 'pre': opt.pre ?? this._$pre,
343
- });
344
- const updates = {};
345
- const keys = [];
346
- for (const col of cols) {
347
- let caseSql = `(CASE ${sq.field(key)}`;
348
- const params = [];
349
- let hasUpdate = false;
350
- for (const item of batch) {
351
- if (item[col] !== undefined) {
352
- caseSql += ` WHEN ? THEN ?`;
353
- params.push(item[key], item[col]);
354
- hasUpdate = true;
355
- }
356
- }
357
- if (hasUpdate) {
358
- caseSql += ` ELSE ${sq.field(col)} END)`;
359
- updates[col] = [caseSql, params];
334
+ const tableName = this._$table + (opt.index ? ('_' + opt.index) : '');
335
+ for (const [, groupItems] of groups) {
336
+ const cols = Object.keys(groupItems[0]).filter(k => k !== key).sort();
337
+ const allCols = [key, ...cols];
338
+ // --- 每行占位符数量 = key + 所有列,分批避免超出数据库参数上限 ---
339
+ const batchSize = Math.floor(60000 / allCols.length);
340
+ for (let i = 0; i < groupItems.length; i += batchSize) {
341
+ const batch = groupItems.slice(i, i + batchSize);
342
+ const sq = lSql.get({
343
+ 'service': db.getService() ?? lDb.ESERVICE.PGSQL,
344
+ 'ctr': opt.ctr,
345
+ 'pre': opt.pre ?? this._$pre,
346
+ });
347
+ const rows = batch.map(item => allCols.map(c => item[c] ?? null));
348
+ sq.updateByValues(tableName, key, cols, rows);
349
+ const r = await db.execute(sq.getSql(), sq.getData());
350
+ if (r.packet === null) {
351
+ lCore.log(opt.ctr ?? {}, '[MOD][updateList] ' + (lText.stringifyJson(r.error?.message ?? '').slice(1, -1) + ' - ' + sq.format()).replaceAll('"', '""'), '-error');
352
+ return false;
360
353
  }
361
354
  }
362
- // --- 收集 keys ---
363
- for (const item of batch) {
364
- keys.push(item[key]);
365
- }
366
- if (Object.keys(updates).length === 0) {
367
- continue;
368
- }
369
- sq.update(this._$table + (opt.index ? ('_' + opt.index) : ''), updates)
370
- .where({ [key]: keys });
371
- const r = await db.execute(sq.getSql(), sq.getData());
372
- if (r.packet === null) {
373
- lCore.log(opt.ctr ?? {}, '[MOD][updateList] ' + (lText.stringifyJson(r.error?.message ?? '').slice(1, -1) + ' - ' + sq.format()).replaceAll('"', '""'), '-error');
374
- return false;
375
- }
376
355
  }
377
356
  return true;
378
357
  }
@@ -1407,7 +1407,10 @@ for (let i = 0; i < 30000; ++i) {
1407
1407
  else {
1408
1408
  // --- jsonl 格式:object ---
1409
1409
  const r = row;
1410
- for (const val of [r.time, r.unix, r.url, r.cookie, r.session, r.userAgent, r.realIp, r.cfIp, r.xIp, r.osMem, r.procMem, r.message]) {
1410
+ for (const val of [
1411
+ r.time, r.unix, r.url,
1412
+ r.cookie, r.session, r.userAgent, r.realIp, r.cfIp, r.xIp, r.osMem, r.procMem, r.message
1413
+ ]) {
1411
1414
  echo.push('<td>' + lText.htmlescape(typeof val === 'string' ? val : lText.stringifyJson(val) ?? '') + '</td>');
1412
1415
  }
1413
1416
  }
@@ -1466,12 +1469,12 @@ to: ${to}`
1466
1469
  return echo.join('') + '<br><br>' + this._getEnd();
1467
1470
  }
1468
1471
  async coreReload() {
1469
- await lCore.sendReload();
1470
- return 'The reload request has been sent, please review the console.<br><br>' + this._getEnd();
1472
+ const list = await lCore.sendReload();
1473
+ return `The reload request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1471
1474
  }
1472
1475
  async coreRestart() {
1473
- await lCore.sendRestart();
1474
- return 'The restart request has been sent, please review the console.<br><br>' + this._getEnd();
1476
+ const list = await lCore.sendRestart();
1477
+ return `The restart request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1475
1478
  }
1476
1479
  async corePm2() {
1477
1480
  const name = this._get['name'] ?? '';
@@ -1483,7 +1486,7 @@ to: ${to}`
1483
1486
  return 'Invalid action. Must be: start, stop, restart<br><br>' + this._getEnd();
1484
1487
  }
1485
1488
  const list = await lCore.sendPm2(name, action);
1486
- return `PM2 ${action} request has been sent for "${name}".<br>Success hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1489
+ return `PM2 ${action} request has been sent for "${name}".<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1487
1490
  }
1488
1491
  async coreNpm() {
1489
1492
  // --- 创建一个临时目录 ---
@@ -1498,7 +1501,7 @@ to: ${to}`
1498
1501
  }
1499
1502
  }));
1500
1503
  const list = await lCore.sendNpm(path);
1501
- return `NPM request has been sent.<br>Success hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1504
+ return `NPM request has been sent.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
1502
1505
  }
1503
1506
  async coreGlobal() {
1504
1507
  const ts = lTime.stamp().toString();