mm_mysql 2.0.4 → 2.0.5

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 (3) hide show
  1. package/index.js +360 -1071
  2. package/package.json +1 -1
  3. package/test.js +518 -0
package/index.js CHANGED
@@ -1,1072 +1,361 @@
1
- /**
2
- * MySQL数据库操作类
3
- * @class Mysql
4
- * @extends BaseService
5
- */
6
- // 添加模块导入检查
7
- let mysql = require('mysql2/promise');
8
- const { BaseService } = require('mm_base_service');
9
- const { DB } = require('./db');
10
- class Mysql extends BaseService {
11
- /**
12
- * 默认配置
13
- */
14
- static default_config = {
15
- host: '127.0.0.1',
16
- port: 3306,
17
- user: 'root',
18
- password: '',
19
- database: '',
20
- charset: 'utf8mb4',
21
- timezone: '+08:00',
22
- connectTimeout: 20000, // mysql2原生支持的连接超时
23
- acquireTimeout: 20000, // 自定义获取连接超时
24
- queryTimeout: 20000, // 自定义查询超时
25
- connectionLimit: 10,
26
- queueLimit: 0,
27
- enableKeepAlive: true,
28
- keepAliveInitialDelay: 10000,
29
- enableReconnect: true,
30
- reconnectInterval: 1000,
31
- maxReconnectAttempts: 5, // 最大重连次数
32
- waitForConnections: true // 确保连接池等待连接可用
33
- };
34
-
35
- /**
36
- * 构造函数
37
- * @param {Object} config - 配置对象
38
- */
39
- constructor(config = {}) {
40
- // 修复配置合并问题 - 手动合并配置以确保用户配置优先级
41
- const mergedConfig = { ...Mysql.default_config, ...config };
42
- super(mergedConfig);
43
-
44
- // 确保this.config包含合并后的配置
45
- this.config = { ...Mysql.default_config, ...config };
46
-
47
- // 初始化状态
48
- this._connection = null; // 单个连接
49
- this._pool = null; // 连接池
50
- this._status = 'closed'; // closed, connecting, connected
51
- this._lastConnectTime = 0;
52
- this._reconnecting = false;
53
- this._isInited = false;
54
- this._isDestroyed = false;
55
- }
56
- }
57
-
58
- /**
59
- * 开始事务
60
- * @returns {Promise<Object>} 事务连接对象
61
- */
62
- Mysql.prototype.beginTransaction = async function () {
63
- try {
64
- // 检查连接状态
65
- if (this._status !== 'connected') {
66
- throw new Error('数据库连接未建立');
67
- }
68
-
69
- // 获取连接
70
- const connection = await this.getConn();
71
-
72
- // 开始事务
73
- await connection.beginTransaction();
74
-
75
- if (this.config.debug) {
76
- $.log.debug(`[${this.constructor.name}] [beginTransaction] 事务开始`);
77
- }
78
-
79
- // 返回事务连接对象,包含提交和回滚方法
80
- return {
81
- connection,
82
- commit: async () => {
83
- await connection.commit();
84
- if (this.config.debug) {
85
- $.log.debug(`[${this.constructor.name}] [beginTransaction] 事务提交`);
86
- }
87
- // 如果使用连接池,释放连接
88
- if (this.config.usePool) {
89
- connection.release();
90
- }
91
- },
92
- rollback: async () => {
93
- await connection.rollback();
94
- if (this.config.debug) {
95
- $.log.debug(`[${this.constructor.name}] [beginTransaction] 事务回滚`);
96
- }
97
- // 如果使用连接池,释放连接
98
- if (this.config.usePool) {
99
- connection.release();
100
- }
101
- },
102
- exec: async (sql, params = []) => {
103
- // 在事务中执行SQL,直接实现超时控制
104
- const timeout = this.config.queryTimeout || 20000;
105
- const queryPromise = connection.query(sql, params);
106
- const timeoutPromise = new Promise((_, reject) => {
107
- setTimeout(() => {
108
- reject(new Error(`事务查询超时: ${timeout}ms`));
109
- }, timeout);
110
- });
111
-
112
- const result = await Promise.race([queryPromise, timeoutPromise]);
113
- return result[0];
114
- }
115
- };
116
- } catch (error) {
117
- $.log.error(`[${this.constructor.name}] [beginTransaction] 事务开始失败`, { error: error.message });
118
- throw error;
119
- }
120
- };
121
-
122
- /**
123
- * 在事务中执行多个操作
124
- * @param {Function} callback - 包含事务操作的回调函数
125
- * @returns {Promise<*>} 回调函数的返回值
126
- */
127
- Mysql.prototype.transaction = async function (callback) {
128
- let transaction = null;
129
-
130
- try {
131
- // 开始事务
132
- transaction = await this.beginTransaction();
133
-
134
- // 执行回调函数,传入事务对象
135
- const result = await callback(transaction);
136
-
137
- // 提交事务
138
- await transaction.commit();
139
-
140
- return result;
141
- } catch (error) {
142
- // 如果有事务,回滚
143
- if (transaction) {
144
- await transaction.rollback().catch(err => {
145
- $.log.error(`[${this.constructor.name}] [transaction] 事务回滚失败`, { error: err.message });
146
- });
147
- }
148
-
149
- $.log.error(`[${this.constructor.name}] [transaction] 事务执行失败`, { error: error.message });
150
- throw error;
151
- }
152
- };
153
- Mysql.prototype._initService = async function () {
154
- if (this._isInited) {
155
- $.log.warn('MySQL服务已初始化');
156
- return;
157
- }
158
-
159
- try {
160
- $.log.debug('[Mysql] [_initService]', '初始化MySQL服务', { config: this.config });
161
- this._isInited = true;
162
- $.log.debug('[Mysql] [_initService]', 'MySQL服务初始化完成');
163
- } catch (error) {
164
- $.log.error('[Mysql] [_initService]', 'MySQL服务初始化失败', { error: error.message });
165
- throw error;
166
- }
167
- };
168
-
169
- /**
170
- * 打开数据库连接
171
- * @param {Number} timeout - 超时时间(毫秒)
172
- * @returns {Promise<boolean>}
173
- */
174
- Mysql.prototype.open = async function (timeout = null) {
175
- if (this._status === 'connected' || this._status === 'connecting') {
176
- $.log.warn('数据库连接已存在或正在连接中');
177
- return true;
178
- }
179
-
180
- timeout = timeout || this.config.connectTimeout || 10000;
181
- this._status = 'connecting';
182
-
183
- try {
184
- $.log.debug(`[${this.constructor.name}] [open]`, '开始创建数据库连接', {
185
- host: this.config.host,
186
- port: this.config.port,
187
- database: this.config.database,
188
- user: this.config.user,
189
- timeout: timeout
190
- });
191
-
192
- // 创建连接或连接池
193
- const connectionPromise = this._openInternal();
194
- // 直接在方法内部实现超时控制
195
- const timeoutPromise = new Promise((_, reject) => {
196
- setTimeout(() => {
197
- reject(new Error(`数据库连接超时(${timeout}ms)`));
198
- }, timeout);
199
- });
200
-
201
- try {
202
- await Promise.race([connectionPromise, timeoutPromise]);
203
-
204
- this._lastConnectTime = Date.now();
205
- this._status = 'connected';
206
- $.log.info(`[${this.constructor.name}] [open]`, '数据库连接成功', {
207
- host: this.config.host,
208
- port: this.config.port,
209
- database: this.config.database
210
- });
211
- return true;
212
- } catch (connectionErr) {
213
- // 捕获连接过程中的具体错误
214
- $.log.error(`[${this.constructor.name}] [open]`, '连接过程错误详情', {
215
- error: connectionErr.message,
216
- code: connectionErr.code,
217
- syscall: connectionErr.syscall,
218
- address: connectionErr.address,
219
- port: connectionErr.port,
220
- stack: connectionErr.stack
221
- });
222
- throw connectionErr;
223
- }
224
- } catch (err) {
225
- this._status = 'closed';
226
- $.log.error(`[${this.constructor.name}] [open]`, '数据库连接失败', {
227
- error: err.message,
228
- host: this.config.host,
229
- port: this.config.port,
230
- database: this.config.database
231
- });
232
- throw err;
233
- }
234
- };
235
-
236
- /**
237
- * 内部连接方法
238
- * @private
239
- * @returns {Promise<void>}
240
- */
241
- Mysql.prototype._openInternal = function () {
242
- return new Promise((resolve, reject) => {
243
- // 检查mysql模块是否可用
244
- if (!mysql) {
245
- const error = new Error('mysql2模块不可用,请先安装依赖: npm install');
246
- $.log.error(`[${this.constructor.name}] [_openInternal] 错误:`, error.message);
247
- return reject(error);
248
- }
249
-
250
- // 配置对象 - 尽量使用mysql2原生支持的配置项
251
- const mysqlConfig = {
252
- host: this.config.host,
253
- port: this.config.port,
254
- user: this.config.user,
255
- password: this.config.password,
256
- database: this.config.database,
257
- charset: this.config.charset || 'utf8mb4',
258
- timezone: this.config.timezone || '+08:00',
259
- connectTimeout: this.config.connectTimeout || 10000,
260
- enableKeepAlive: this.config.enableKeepAlive !== false,
261
- keepAliveInitialDelay: this.config.keepAliveInitialDelay || 10000,
262
- waitForConnections: true
263
- };
264
-
265
- // 输出连接配置(隐藏密码)
266
- const logConfig = { ...mysqlConfig };
267
- logConfig.password = '***';
268
- $.log.debug(`[${this.constructor.name}] [_openInternal] 连接配置:`, JSON.stringify(logConfig, null, 2));
269
-
270
- if (this.config.connectionLimit > 1) {
271
- // 使用连接池 - 直接使用mysql2原生连接池功能
272
- $.log.debug(`[${this.constructor.name}] [_openInternal] 创建连接池`);
273
- try {
274
- this._pool = mysql.createPool({
275
- ...mysqlConfig,
276
- connectionLimit: this.config.connectionLimit || 10,
277
- queueLimit: this.config.queueLimit || 0,
278
- waitForConnections: true
279
- });
280
-
281
- // 验证连接池
282
- $.log.debug(`[${this.constructor.name}] [_openInternal] 验证连接池连接...`);
283
- this._pool.getConnection()
284
- .then(conn => {
285
- $.log.debug(`[${this.constructor.name}] [_openInternal] 连接池连接验证成功`);
286
- conn.release();
287
- resolve();
288
- })
289
- .catch(err => {
290
- $.log.error(`[${this.constructor.name}] [_openInternal] 连接池验证失败详情:`, {
291
- message: err.message,
292
- code: err.code,
293
- errno: err.errno,
294
- syscall: err.syscall,
295
- address: err.address,
296
- port: err.port
297
- });
298
- reject(err);
299
- });
300
- } catch (poolError) {
301
- $.log.error(`[${this.constructor.name}] [_openInternal] 创建连接池失败:`, poolError.message);
302
- reject(poolError);
303
- }
304
- } else {
305
- // 使用单个连接
306
- $.log.debug(`[${this.constructor.name}] [_openInternal] 创建单个连接`);
307
- try {
308
- mysql.createConnection(mysqlConfig)
309
- .then(conn => {
310
- $.log.debug(`[${this.constructor.name}] [_openInternal] 单个连接创建成功`);
311
- this._connection = conn;
312
- resolve();
313
- })
314
- .catch(err => {
315
- $.log.error(`[${this.constructor.name}] [_openInternal] 单个连接创建失败详情:`, {
316
- message: err.message,
317
- code: err.code,
318
- errno: err.errno,
319
- syscall: err.syscall,
320
- address: err.address,
321
- port: err.port
322
- });
323
- reject(err);
324
- });
325
- } catch (connError) {
326
- $.log.error(`[${this.constructor.name}] [_openInternal] 创建连接失败:`, connError.message);
327
- reject(connError);
328
- }
329
- }
330
- });
331
- };
332
-
333
- /**
334
- * 关闭数据库连接
335
- * @returns {Promise<boolean>}
336
- */
337
- Mysql.prototype.close = function () {
338
- if (this._status !== 'connected') {
339
- $.log.warn('数据库连接未建立');
340
- return Promise.resolve(false);
341
- }
342
-
343
- return new Promise((resolve) => {
344
- try {
345
- if (this._pool) {
346
- $.log.debug('关闭连接池');
347
- this._pool.end(err => {
348
- if (err) {
349
- $.log.error('关闭连接池失败', { error: err.message });
350
- } else {
351
- $.log.info('连接池已关闭');
352
- }
353
- this._pool = null;
354
- this._status = 'closed';
355
- resolve(!err);
356
- });
357
- } else if (this._connection) {
358
- $.log.debug('关闭单个连接');
359
- this._connection.end(err => {
360
- if (err) {
361
- $.log.error('关闭连接失败', { error: err.message });
362
- } else {
363
- $.log.info('连接已关闭');
364
- }
365
- this._connection = null;
366
- this._status = 'closed';
367
- resolve(!err);
368
- });
369
- } else {
370
- this._status = 'closed';
371
- resolve(true);
372
- }
373
- } catch (err) {
374
- $.log.error('关闭数据库连接时发生异常', { error: err.message });
375
- this._status = 'closed';
376
- resolve(false);
377
- }
378
- });
379
- };
380
-
381
- /**
382
- * 获取数据库连接
383
- * @param {Number} timeout - 超时时间(毫秒)
384
- * @returns {Promise<Object>}
385
- */
386
- Mysql.prototype.getConn = async function (timeout = null) {
387
- // 确定最终超时时间,优先使用传入参数,其次是配置,最后是默认值
388
- timeout = timeout || this.config.acquireTimeout || this.config.connectTimeout || 20000;
389
-
390
- // 记录实际使用的超时时间
391
- $.log.debug(`[${this.constructor.name}] [getConn] 使用超时时间: ${timeout}ms`);
392
-
393
- if (this._status !== 'connected') {
394
- throw new Error('数据库连接未建立');
395
- }
396
-
397
- try {
398
- // 先检查连接池状态(如果存在)
399
- if (this._pool && typeof this._pool._closed !== 'undefined' && this._pool._closed) {
400
- $.log.warn('连接池已关闭,尝试重新连接');
401
- await this.close();
402
- await this.open();
403
- }
404
-
405
- // 直接在方法内部实现超时控制
406
- const connectionPromise = this._getConnectionInternal();
407
- const timeoutPromise = new Promise((_, reject) => {
408
- setTimeout(() => {
409
- reject(new Error(`获取数据库连接超时(${timeout}ms)`));
410
- }, timeout);
411
- });
412
-
413
- const conn = await Promise.race([connectionPromise, timeoutPromise]);
414
-
415
- $.log.debug(`[${this.constructor.name}] [getConn] 成功获取数据库连接`);
416
-
417
- // 处理连接错误
418
- conn.on('error', (err) => {
419
- if (err.code && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'PROTOCOL_CONNECTION_LOST' ||
420
- err.code === 'POOL_CLOSED' || err.code === 'CONNECTION_TIMED_OUT')) {
421
- this._handleConnectionError(err);
422
- }
423
- });
424
-
425
- return conn;
426
- } catch (error) {
427
- $.log.error(`[${this.constructor.name}] [getConn] 获取连接失败`, {
428
- error: error.message
429
- });
430
-
431
- // 处理连接错误
432
- if (error.code && (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'PROTOCOL_CONNECTION_LOST' ||
433
- error.code === 'POOL_CLOSED' || error.code === 'CONNECTION_TIMED_OUT')) {
434
- this._handleConnectionError(error);
435
- }
436
-
437
- throw error;
438
- }
439
- };
440
-
441
- /**
442
- * 内部获取连接方法
443
- * @private
444
- * @returns {Promise<Object>}
445
- */
446
- Mysql.prototype._getConnectionInternal = function () {
447
- return new Promise((resolve, reject) => {
448
- if (this._pool) {
449
- // 从连接池获取连接
450
- this._pool.getConnection()
451
- .then(conn => {
452
- // 设置连接的超时处理
453
- conn.on('error', (err) => {
454
- $.log.error('数据库连接发生错误', { error: err.message });
455
- if (this.config.enableReconnect) {
456
- this._handleConnectionError(err);
457
- }
458
- });
459
- resolve(conn);
460
- })
461
- .catch(err => {
462
- reject(err);
463
- });
464
- } else if (this._connection) {
465
- // 返回单个连接
466
- resolve(this._connection);
467
- } else {
468
- reject(new Error('无可用连接'));
469
- }
470
- });
471
- };
472
-
473
- /**
474
- * 处理连接错误
475
- * @private
476
- * @param {Error} err - 错误对象
477
- */
478
- Mysql.prototype._handleConnectionError = function (err) {
479
- if (this._reconnecting) {
480
- $.log.debug('重连已在进行中');
481
- return;
482
- }
483
-
484
- if (this.config.enableReconnect) {
485
- $.log.info('开始自动重连...');
486
- this._reconnect(this.config.maxReconnectAttempts || 3)
487
- .catch(reconnectErr => {
488
- $.log.error('重连失败', { error: reconnectErr.message });
489
- this._status = 'closed';
490
- });
491
- } else {
492
- $.log.warn('自动重连已禁用');
493
- this._status = 'closed';
494
- }
495
- };
496
-
497
- /**
498
- * 重新连接数据库
499
- * @private
500
- * @param {Number} maxRetries - 最大重试次数
501
- * @returns {Promise<boolean>}
502
- */
503
- Mysql.prototype._reconnect = async function (maxRetries = 3) {
504
- if (this._reconnecting) {
505
- $.log.warn('重连已在进行中');
506
- return false;
507
- }
508
-
509
- this._reconnecting = true;
510
- let retryCount = 0;
511
-
512
- try {
513
- // 先关闭现有连接
514
- if (this._status === 'connected') {
515
- await this.close();
516
- }
517
-
518
- while (retryCount < maxRetries) {
519
- retryCount++;
520
- $.log.info(`第${retryCount}/${maxRetries}次尝试重连`);
521
-
522
- try {
523
- // 使用配置中的超时时间,优先使用connectTimeout,其次是acquireTimeout,最后是默认值20000ms
524
- const reconnectTimeout = this.config.connectTimeout || this.config.acquireTimeout || 20000;
525
- $.log.debug(`重连超时设置为: ${reconnectTimeout}ms`);
526
- await this.open(reconnectTimeout);
527
- $.log.info('重连成功');
528
- return true;
529
- } catch (err) {
530
- $.log.error(`重连失败(${retryCount}/${maxRetries})`, { error: err.message });
531
-
532
- if (retryCount < maxRetries) {
533
- // 等待一段时间后重试
534
- const waitTime = this.config.reconnectInterval || 1000;
535
- $.log.debug(`等待${waitTime}ms后重试`);
536
- await this._sleep(waitTime);
537
- }
538
- }
539
- }
540
-
541
- $.log.error(`达到最大重试次数(${maxRetries}),重连失败`);
542
- throw new Error(`重连失败:达到最大重试次数(${maxRetries})`);
543
- } finally {
544
- this._reconnecting = false;
545
- }
546
- };
547
-
548
- /**
549
- * 执行SQL查询
550
- * @param {String} sql - SQL语句
551
- * @param {Array} params - 参数数组
552
- * @param {Number} timeout - 超时时间(毫秒)
553
- * @returns {Promise<Object>}
554
- */
555
- Mysql.prototype.run = async function (sql, params = [], timeout = null) {
556
- let conn = null;
557
- let isPoolConn = false;
558
- timeout = timeout || this.config.queryTimeout || 30000;
559
-
560
- try {
561
- // 获取连接
562
- conn = await this.getConn(timeout);
563
- isPoolConn = !!this._pool;
564
-
565
- // 直接在方法内部实现超时控制
566
- const queryPromise = conn.query(sql, params);
567
- const timeoutPromise = new Promise((_, reject) => {
568
- setTimeout(() => {
569
- reject(new Error(`SQL查询超时: ${timeout}ms`));
570
- }, timeout);
571
- });
572
-
573
- const [rows] = await Promise.race([queryPromise, timeoutPromise]);
574
-
575
- // 返回查询结果的第一行或整个结果集,与之前版本兼容
576
- return rows;
577
- } catch (err) {
578
- $.log.error('[Mysql] [run]', 'SQL执行失败', {
579
- error: err.message,
580
- sql: typeof sql === 'string' ? sql.substring(0, 200) : sql,
581
- params: params
582
- });
583
-
584
- // 处理连接错误,触发重连
585
- if (err.code && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'PROTOCOL_CONNECTION_LOST')) {
586
- this._handleConnectionError(err);
587
- }
588
-
589
- throw err;
590
- } finally {
591
- // 释放连接
592
- if (conn && isPoolConn) {
593
- try {
594
- conn.release();
595
- } catch (releaseErr) {
596
- $.log.error('释放连接失败', { error: releaseErr.message });
597
- }
598
- }
599
- }
600
- };
601
-
602
- /**
603
- * 执行SQL语句(用于执行非查询语句如INSERT/UPDATE/DELETE)
604
- * @param {String} sql - SQL语句
605
- * @param {Array} params - 参数数组
606
- * @param {Number} timeout - 超时时间(毫秒)
607
- * @returns {Promise<Object>}
608
- */
609
- Mysql.prototype.exec = async function (sql, params = [], timeout = null) {
610
- let conn = null;
611
- let isPoolConn = false;
612
- timeout = timeout || this.config.queryTimeout || 30000;
613
-
614
- try {
615
- // 获取连接
616
- conn = await this.getConn(timeout);
617
- isPoolConn = !!this._pool;
618
-
619
- // 直接在方法内部实现超时控制
620
- const queryPromise = conn.query(sql, params);
621
- const timeoutPromise = new Promise((_, reject) => {
622
- setTimeout(() => {
623
- reject(new Error(`SQL执行超时: ${timeout}ms`));
624
- }, timeout);
625
- });
626
-
627
- const [rows] = await Promise.race([queryPromise, timeoutPromise]);
628
-
629
- // 返回与mm_sqlite兼容的格式
630
- return {
631
- affectedRows: rows.affectedRows || 0,
632
- insertId: rows.insertId || 0,
633
- changedRows: rows.changedRows || 0
634
- };
635
- } catch (err) {
636
- $.log.error('[Mysql] [exec]', 'SQL执行失败', {
637
- error: err.message,
638
- sql: sql.substring(0, 200),
639
- params: params
640
- });
641
-
642
- // 处理连接错误,触发重连
643
- if (err.code && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'PROTOCOL_CONNECTION_LOST')) {
644
- this._handleConnectionError(err);
645
- }
646
-
647
- throw err;
648
- } finally {
649
- // 释放连接(如果是从连接池获取的)
650
- if (conn && isPoolConn) {
651
- try {
652
- conn.release();
653
- } catch (releaseErr) {
654
- $.log.error('释放连接失败', { error: releaseErr.message });
655
- }
656
- }
657
- }
658
- };
659
-
660
- /**
661
- * 读取整张表的数据
662
- * @param {String} table - 表名
663
- * @param {Object} condition - 查询条件
664
- * @param {Object} options - 选项(orderBy, limit, offset等)
665
- * @returns {Promise<Array>}
666
- */
667
- Mysql.prototype.read = async function (table, condition = {}, options = {}) {
668
- try {
669
- // 检查连接状态
670
- if (this._status !== 'connected') {
671
- throw new Error('数据库连接未建立');
672
- }
673
-
674
- // 构建基础SQL查询
675
- let sql = `SELECT * FROM ${table}`;
676
- const params = [];
677
-
678
- // 处理条件
679
- if (Object.keys(condition).length > 0) {
680
- const whereClauses = [];
681
- for (const [field, value] of Object.entries(condition)) {
682
- whereClauses.push(`${field} = ?`);
683
- params.push(value);
684
- }
685
- sql += ` WHERE ${whereClauses.join(' AND ')}`;
686
- }
687
-
688
- // 处理排序
689
- if (options.orderBy) {
690
- sql += ` ORDER BY ${options.orderBy}`;
691
- }
692
-
693
- // 处理分页
694
- if (options.limit) {
695
- sql += ` LIMIT ${options.limit}`;
696
- }
697
-
698
- if (options.offset) {
699
- sql += ` OFFSET ${options.offset}`;
700
- }
701
-
702
- // 记录查询日志
703
- if (this.config.debug) {
704
- $.log.debug(`[${this.constructor.name}] [read] 查询数据`, { sql, params });
705
- }
706
-
707
- // 执行查询,使用run方法获取数组格式的结果
708
- const results = await this.run(sql, params);
709
-
710
- if (this.config.debug) {
711
- $.log.info(`[${this.constructor.name}] [read] 查询成功`, { count: results.length });
712
- }
713
-
714
- return results;
715
- } catch (error) {
716
- // 记录错误日志
717
- $.log.error(`[${this.constructor.name}] [read] 查询失败`, {
718
- error: error.message,
719
- table: table,
720
- condition: condition
721
- });
722
-
723
- // 抛出错误
724
- throw error;
725
- }
726
- };
727
-
728
- /**
729
- * 保存数据库结构到SQL文件
730
- * @param {String} file - 输出文件路径
731
- * @param {Array} tables - 要保存的表名数组,空数组表示保存所有表
732
- * @returns {Promise<boolean>}
733
- */
734
- Mysql.prototype.save = async function (file, tables = []) {
735
- try {
736
- let sqlContent = '';
737
-
738
- // 获取所有表名
739
- const allTablesResult = await this.run('SHOW TABLES');
740
- let tableNames = [];
741
-
742
- if (allTablesResult && allTablesResult.length > 0) {
743
- const tableKey = Object.keys(allTablesResult[0])[0];
744
- tableNames = allTablesResult.map(row => row[tableKey]);
745
- }
746
-
747
- // 过滤要保存的表
748
- const tablesToSave = tables.length > 0
749
- ? tableNames.filter(name => tables.includes(name))
750
- : tableNames;
751
-
752
- // 导出每个表的结构
753
- for (const tableName of tablesToSave) {
754
- const createSqlResult = await this.run(`SHOW CREATE TABLE \`${tableName}\``);
755
- if (createSqlResult && createSqlResult.length > 0) {
756
- const createSql = createSqlResult[0]['Create Table'];
757
- sqlContent += `\n\n-- Table structure for \`${tableName}\`\n`;
758
- sqlContent += createSql + ';\n';
759
- }
760
- }
761
-
762
- // 确保目录存在
763
- file.addDir();
764
- file.saveText(sqlContent);
765
- $.log.info(`成功导出数据库结构到: ${file}`);
766
- return true;
767
- } catch (err) {
768
- $.log.error(`导出数据库结构失败: ${file}`, { error: err.message });
769
- throw err;
770
- }
771
- };
772
-
773
- /**
774
- * 从SQL文件加载数据库结构和数据
775
- * @param {String} file - SQL文件路径
776
- * @returns {Promise<boolean>} 是否加载成功
777
- */
778
- Mysql.prototype.load = async function (file) {
779
- try {
780
- // 记录操作日志
781
- if (this.config.debug) {
782
- $.log.debug(`[${this.constructor.name}] [load] 开始从文件加载数据库`, {
783
- file: file
784
- });
785
- }
786
-
787
- // 检查文件是否存在
788
- if (!file.hasFile()) {
789
- throw new Error(`SQL文件不存在: ${file}`);
790
- }
791
-
792
- // 读取SQL文件内容
793
- const sqlContent = file.readText();
794
-
795
- if (!sqlContent || sqlContent.trim() === '') {
796
- throw new Error(`SQL文件内容为空: ${file}`);
797
- }
798
-
799
- // 分割SQL语句(按分号分割,但需要注意处理字符串中的分号)
800
- const sqlStatements = this._splitSqlStatements(sqlContent);
801
-
802
- // 开始事务执行SQL语句
803
- await this.exec('START TRANSACTION');
804
-
805
- try {
806
- // 逐个执行SQL语句
807
- for (const sql of sqlStatements) {
808
- const trimmedSql = sql.trim();
809
- if (trimmedSql) {
810
- await this.exec(trimmedSql);
811
- }
812
- }
813
-
814
- // 提交事务
815
- await this.exec('COMMIT');
816
-
817
- if (this.config.debug) {
818
- $.log.info(`[${this.constructor.name}] [load] 数据库加载成功`, {
819
- file: file,
820
- statementCount: sqlStatements.length
821
- });
822
- }
823
-
824
- return true;
825
- } catch (err) {
826
- // 回滚事务
827
- await this.exec('ROLLBACK').catch(rollbackErr => {
828
- $.log.error(`[${this.constructor.name}] [load] 事务回滚失败`, {
829
- error: rollbackErr.message
830
- });
831
- });
832
-
833
- throw err;
834
- }
835
- } catch (error) {
836
- // 记录错误日志
837
- $.log.error(`[${this.constructor.name}] [load] 数据库加载失败`, {
838
- error: error.message,
839
- file: file
840
- });
841
-
842
- // 抛出错误
843
- throw error;
844
- }
845
- };
846
-
847
- /**
848
- * 分割SQL语句
849
- * @private
850
- * @param {String} sqlContent - SQL内容
851
- * @returns {Array} SQL语句数组
852
- */
853
- Mysql.prototype._splitSqlStatements = function(sqlContent) {
854
- const statements = [];
855
- let inString = false;
856
- let stringChar = '';
857
- let inComment = false;
858
- let currentStatement = '';
859
-
860
- for (let i = 0; i < sqlContent.length; i++) {
861
- const char = sqlContent[i];
862
- const nextChar = i + 1 < sqlContent.length ? sqlContent[i + 1] : '';
863
-
864
- // 处理注释
865
- if (!inString && !inComment && char === '-' && nextChar === '-') {
866
- inComment = true;
867
- i++; // 跳过第二个'-'
868
- continue;
869
- }
870
-
871
- if (inComment && char === '\n') {
872
- inComment = false;
873
- continue;
874
- }
875
-
876
- if (inComment) {
877
- continue;
878
- }
879
-
880
- // 处理多行注释
881
- if (!inString && !inComment && char === '/' && nextChar === '*') {
882
- inComment = true;
883
- i++; // 跳过'*'
884
- continue;
885
- }
886
-
887
- if (inComment && char === '*' && nextChar === '/') {
888
- inComment = false;
889
- i++; // 跳过'/'
890
- continue;
891
- }
892
-
893
- // 处理字符串
894
- if (!inComment && (char === "'" || char === '"') && (!inString || stringChar === char)) {
895
- // 检查是否是转义的引号
896
- let escaped = false;
897
- for (let j = i - 1; j >= 0; j--) {
898
- if (sqlContent[j] === '\\') {
899
- escaped = !escaped;
900
- } else {
901
- break;
902
- }
903
- }
904
-
905
- if (!escaped) {
906
- if (inString && stringChar === char) {
907
- inString = false;
908
- stringChar = '';
909
- } else if (!inString) {
910
- inString = true;
911
- stringChar = char;
912
- }
913
- }
914
- }
915
-
916
- // 分割语句
917
- if (!inString && !inComment && char === ';') {
918
- statements.push(currentStatement.trim());
919
- currentStatement = '';
920
- } else {
921
- currentStatement += char;
922
- }
923
- }
924
-
925
- // 添加最后一个语句(如果有)
926
- if (currentStatement.trim()) {
927
- statements.push(currentStatement.trim());
928
- }
929
-
930
- return statements;
931
- };
932
-
933
- /**
934
- * 获取DB实例(用于高级操作)
935
- * @returns {Object} 包含exec、add、set、get、del等方法的数据库操作对象
936
- */
937
- Mysql.prototype.db = function () {
938
- return new DB(this);
939
- };
940
-
941
- /**
942
- * 睡眠指定时间(工具方法)
943
- * @private
944
- * @param {Number} ms - 毫秒数
945
- * @returns {Promise<void>}
946
- */
947
- Mysql.prototype._sleep = function (ms) {
948
- return new Promise(resolve => setTimeout(resolve, ms));
949
- };
950
-
951
- /**
952
- * @description 销毁MySQL服务,关闭连接池
953
- * @async
954
- * @returns {Promise<void>}
955
- */
956
- Mysql.prototype.destroy = async function () {
957
- if (this._isDestroyed) {
958
- $.log.warn('MySQL服务已销毁');
959
- return;
960
- }
961
-
962
- try {
963
- $.log.debug('[Mysql] [destroy]', '开始销毁MySQL服务');
964
-
965
- // 关闭数据库连接
966
- if (this._status === 'connected') {
967
- await this.close();
968
- }
969
-
970
- // 重置状态
971
- this._isDestroyed = true;
972
- this._isInited = false;
973
- this._connection = null;
974
- this._pool = null;
975
- this._status = 'closed';
976
-
977
- $.log.debug('[Mysql] [destroy]', 'MySQL服务销毁完成');
978
- } catch (error) {
979
- $.log.error('[Mysql] [destroy]', 'MySQL服务销毁失败', { error: error.message });
980
- throw error;
981
- }
982
- };
983
-
984
- /**
985
- * 睡眠指定时间(工具方法)
986
- * @private
987
- * @param {Number} ms - 毫秒数
988
- * @returns {Promise<void>}
989
- */
990
- Mysql.prototype._sleep = function (ms) {
991
- return new Promise(resolve => setTimeout(resolve, ms));
992
- };
993
-
994
- /**
995
- * SQL语法转换方法(与mm_sqlite保持兼容)
996
- * @param {String} sql - 原始SQL语句
997
- * @returns {String} 转换后的SQL语句
998
- */
999
- Mysql.prototype._convertSqlSyntax = function (sql) {
1000
- // MySQL不需要特殊的语法转换,直接返回原SQL
1001
- // 此方法主要用于保持与mm_sqlite的接口兼容性
1002
- return sql;
1003
- };
1004
-
1005
- /**
1006
- * 获取数据库路径(与mm_sqlite保持兼容)
1007
- * @returns {String} 数据库连接信息
1008
- */
1009
- Mysql.prototype._getDatabasePath = function () {
1010
- // MySQL不需要文件路径,返回连接信息用于兼容性
1011
- return `${this.config.host}:${this.config.port}/${this.config.database}`;
1012
- };
1013
-
1014
- /**
1015
- * 初始化MySQL服务
1016
- * @async
1017
- * @returns {Promise<void>}
1018
- */
1019
- Mysql.prototype.init = async function () {
1020
- if (this._isInited) {
1021
- $.log.warn('MySQL服务已初始化');
1022
- return;
1023
- }
1024
-
1025
- try {
1026
- $.log.debug('[Mysql] [init]', '初始化MySQL服务');
1027
- await this._initService();
1028
- await this.open();
1029
- $.log.debug('[Mysql] [init]', 'MySQL服务初始化完成');
1030
- } catch (error) {
1031
- $.log.error('[Mysql] [init]', 'MySQL服务初始化失败', { error: error.message });
1032
- throw error;
1033
- }
1034
- };
1035
-
1036
- /**
1037
- * 导出模块
1038
- */
1039
- exports.Mysql = Mysql;
1040
-
1041
- /**
1042
- * 确保连接池对象存在
1043
- */
1044
- if (!$.pool) {
1045
- $.pool = {};
1046
- }
1047
- if (!$.pool.mysql) {
1048
- $.pool.mysql = {};
1049
- }
1050
-
1051
- /**
1052
- * @description Mysql管理器,用于创建缓存
1053
- * @param {String} scope 作用域
1054
- * @param {Object} config 配置参数
1055
- * @return {Object} 返回一个Mysql类实例
1056
- */
1057
- function mysql_admin(scope, config) {
1058
- if (!scope) {
1059
- scope = 'sys';
1060
- }
1061
- var obj = $.pool.mysql[scope];
1062
- if (!obj) {
1063
- $.pool.mysql[scope] = new Mysql(config);
1064
- obj = $.pool.mysql[scope];
1065
- }
1066
- return obj;
1067
- }
1068
-
1069
- /**
1070
- * @module 导出Mysql管理器
1071
- */
1
+ const mysql = require('mysql2/promise');
2
+ const { BaseService } = require('mm_base_service');
3
+ const { DB } = require('./db');
4
+ /**
5
+ * 优化版MySQL数据库操作类
6
+ * 保持必要功能,简化过度封装,直接使用mysql2模块
7
+ * @class Mysql
8
+ * @extends BaseService
9
+ */
10
+ class Mysql extends BaseService {
11
+ /**
12
+ * 默认配置
13
+ */
14
+ static default_config = {
15
+ host: '127.0.0.1',
16
+ port: 3306,
17
+ user: 'root',
18
+ password: '',
19
+ database: '',
20
+ charset: 'utf8mb4',
21
+ timezone: '+08:00',
22
+ connectTimeout: 20000,
23
+ acquireTimeout: 20000,
24
+ queryTimeout: 20000,
25
+ connectionLimit: 10,
26
+ queueLimit: 0
27
+ };
28
+
29
+ /**
30
+ * 构造函数
31
+ * @param {Object} config - 配置对象
32
+ */
33
+ constructor(config = {}) {
34
+ const mergedConfig = Object.assign({}, Mysql.default_config, config);
35
+ super(mergedConfig);
36
+
37
+ this.config = mergedConfig;
38
+ this._pool = null;
39
+ this._connection = null;
40
+ this._usePool = this.config.connectionLimit > 1;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 打开数据库连接
46
+ * @returns {Promise<boolean>}
47
+ */
48
+ Mysql.prototype.open = async function() {
49
+ try {
50
+ if (this._usePool) {
51
+ this._pool = mysql.createPool(this.config);
52
+ // 测试连接池连接
53
+ const conn = await this._pool.getConnection();
54
+ conn.release();
55
+ } else {
56
+ this._connection = await mysql.createConnection(this.config);
57
+ }
58
+
59
+ $.log.info(`[${this.constructor.name}] [open] 数据库连接成功`);
60
+ return true;
61
+ } catch (error) {
62
+ $.log.error(`[${this.constructor.name}] [open] 数据库连接失败`, {
63
+ error: error.message
64
+ });
65
+ throw error;
66
+ }
67
+ };
68
+
69
+ /**
70
+ * 关闭数据库连接
71
+ * @returns {Promise<boolean>}
72
+ */
73
+ Mysql.prototype.close = async function() {
74
+ try {
75
+ if (this._pool) {
76
+ await this._pool.end();
77
+ this._pool = null;
78
+ }
79
+ if (this._connection) {
80
+ await this._connection.end();
81
+ this._connection = null;
82
+ }
83
+
84
+ $.log.info(`[${this.constructor.name}] [close] 数据库连接已关闭`);
85
+ return true;
86
+ } catch (error) {
87
+ $.log.error(`[${this.constructor.name}] [close] 关闭连接失败`, {
88
+ error: error.message
89
+ });
90
+ throw error;
91
+ }
92
+ };
93
+
94
+ /**
95
+ * 获取数据库连接(保持兼容性)
96
+ * @param {Number} timeout - 超时时间(毫秒)
97
+ * @returns {Promise<Object>}
98
+ */
99
+ Mysql.prototype.getConn = async function(timeout = null) {
100
+ try {
101
+ if (this._usePool) {
102
+ return await this._pool.getConnection();
103
+ } else {
104
+ return this._connection;
105
+ }
106
+ } catch (error) {
107
+ $.log.error(`[${this.constructor.name}] [getConn] 获取连接失败`, {
108
+ error: error.message
109
+ });
110
+ throw error;
111
+ }
112
+ };
113
+
114
+ /**
115
+ * 执行SQL查询(保持兼容性)
116
+ * @param {String} sql - SQL语句
117
+ * @param {Array} params - 参数数组
118
+ * @param {Number} timeout - 超时时间(毫秒)
119
+ * @returns {Promise<Object>}
120
+ */
121
+ Mysql.prototype.run = async function(sql, params = [], timeout = null) {
122
+ let conn = null;
123
+ let isPoolConn = false;
124
+
125
+ try {
126
+ // 获取连接
127
+ conn = await this.getConn();
128
+ isPoolConn = this._usePool;
129
+
130
+ // 直接使用mysql2的query方法
131
+ const [rows] = await conn.query(sql, params);
132
+ return rows;
133
+ } catch (error) {
134
+ $.log.error(`[${this.constructor.name}] [run] SQL执行失败`, {
135
+ error: error.message,
136
+ sql: typeof sql === 'string' ? sql.substring(0, 200) : sql,
137
+ params: params
138
+ });
139
+ throw error;
140
+ } finally {
141
+ // 释放连接
142
+ if (conn && isPoolConn) {
143
+ try {
144
+ conn.release();
145
+ } catch (releaseErr) {
146
+ $.log.error('释放连接失败', {
147
+ error: releaseErr.message
148
+ });
149
+ }
150
+ }
151
+ }
152
+ };
153
+
154
+ /**
155
+ * 执行SQL语句(保持兼容性)
156
+ * @param {String} sql - SQL语句
157
+ * @param {Array} params - 参数数组
158
+ * @param {Number} timeout - 超时时间(毫秒)
159
+ * @returns {Promise<Object>}
160
+ */
161
+ Mysql.prototype.exec = async function(sql, params = [], timeout = null) {
162
+ let conn = null;
163
+ let isPoolConn = false;
164
+
165
+ try {
166
+ // 获取连接
167
+ conn = await this.getConn();
168
+ isPoolConn = this._usePool;
169
+
170
+ // 直接使用mysql2的execute方法
171
+ const [result] = await conn.execute(sql, params);
172
+
173
+ // 返回与mm_sqlite兼容的格式
174
+ return {
175
+ affectedRows: result.affectedRows || 0,
176
+ insertId: result.insertId || 0,
177
+ changedRows: result.changedRows || 0
178
+ };
179
+ } catch (error) {
180
+ $.log.error(`[${this.constructor.name}] [exec] SQL执行失败`, {
181
+ error: error.message,
182
+ sql: sql.substring(0, 200),
183
+ params: params
184
+ });
185
+ throw error;
186
+ } finally {
187
+ // 释放连接
188
+ if (conn && isPoolConn) {
189
+ try {
190
+ conn.release();
191
+ } catch (releaseErr) {
192
+ $.log.error('释放连接失败', {
193
+ error: releaseErr.message
194
+ });
195
+ }
196
+ }
197
+ }
198
+ };
199
+
200
+ /**
201
+ * 开始事务(保持兼容性)
202
+ * @returns {Promise<Object>} 事务连接对象
203
+ */
204
+ Mysql.prototype.beginTransaction = async function() {
205
+ try {
206
+ const conn = await this.getConn();
207
+ await conn.beginTransaction();
208
+
209
+ return {
210
+ connection: conn,
211
+ commit: async () => {
212
+ await conn.commit();
213
+ if (this._usePool) {
214
+ conn.release();
215
+ }
216
+ },
217
+ rollback: async () => {
218
+ await conn.rollback();
219
+ if (this._usePool) {
220
+ conn.release();
221
+ }
222
+ }
223
+ };
224
+ } catch (error) {
225
+ $.log.error(`[${this.constructor.name}] [beginTransaction] 事务开始失败`, {
226
+ error: error.message
227
+ });
228
+ throw error;
229
+ }
230
+ };
231
+
232
+ /**
233
+ * 在事务中执行多个操作(保持兼容性)
234
+ * @param {Function} callback - 包含事务操作的回调函数
235
+ * @returns {Promise<*>} 回调函数的返回值
236
+ */
237
+ Mysql.prototype.transaction = async function(callback) {
238
+ let transaction = null;
239
+
240
+ try {
241
+ transaction = await this.beginTransaction();
242
+ const result = await callback(transaction);
243
+ await transaction.commit();
244
+ return result;
245
+ } catch (error) {
246
+ if (transaction) {
247
+ await transaction.rollback().catch(err => {
248
+ $.log.error(`[${this.constructor.name}] [transaction] 事务回滚失败`, {
249
+ error: err.message
250
+ });
251
+ });
252
+ }
253
+
254
+ $.log.error(`[${this.constructor.name}] [transaction] 事务执行失败`, {
255
+ error: error.message
256
+ });
257
+ throw error;
258
+ }
259
+ };
260
+
261
+ /**
262
+ * 读取整张表的数据(保持兼容性)
263
+ * @param {String} table - 表名
264
+ * @param {Object} condition - 查询条件
265
+ * @param {Object} options - 选项(orderBy, limit, offset等)
266
+ * @returns {Promise<Array>}
267
+ */
268
+ Mysql.prototype.read = async function(table, condition = {}, options = {}) {
269
+ try {
270
+ let sql = `SELECT * FROM ${table}`;
271
+ const params = [];
272
+
273
+ // 处理条件
274
+ if (Object.keys(condition).length > 0) {
275
+ const whereClauses = [];
276
+ for (const [field, value] of Object.entries(condition)) {
277
+ whereClauses.push(`${field} = ?`);
278
+ params.push(value);
279
+ }
280
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
281
+ }
282
+
283
+ // 处理排序
284
+ if (options.orderBy) {
285
+ sql += ` ORDER BY ${options.orderBy}`;
286
+ }
287
+
288
+ // 处理分页
289
+ if (options.limit) {
290
+ sql += ` LIMIT ${options.limit}`;
291
+ }
292
+
293
+ if (options.offset) {
294
+ sql += ` OFFSET ${options.offset}`;
295
+ }
296
+
297
+ // 执行查询
298
+ return await this.run(sql, params);
299
+ } catch (error) {
300
+ $.log.error(`[${this.constructor.name}] [read] 查询失败`, {
301
+ error: error.message,
302
+ table: table,
303
+ condition: condition
304
+ });
305
+ throw error;
306
+ }
307
+ };
308
+
309
+ /**
310
+ * 获取数据库管理器(保持兼容性)
311
+ * @returns {Object} DB实例
312
+ */
313
+ Mysql.prototype.db = function() {
314
+ return new DB(this);
315
+ };
316
+
317
+ /**
318
+ * 初始化MySQL服务(保持兼容性)
319
+ * @returns {Promise<void>}
320
+ */
321
+ Mysql.prototype.init = async function() {
322
+ await this.open();
323
+ };
324
+
325
+ /**
326
+ * 导出模块
327
+ */
328
+ exports.Mysql = Mysql;
329
+
330
+ /**
331
+ * 确保连接池对象存在
332
+ */
333
+ if (!$.pool) {
334
+ $.pool = {};
335
+ }
336
+ if (!$.pool.mysql) {
337
+ $.pool.mysql = {};
338
+ }
339
+
340
+ /**
341
+ * @description Mysql管理器,用于创建缓存
342
+ * @param {String} scope 作用域
343
+ * @param {Object} config 配置参数
344
+ * @return {Object} 返回一个Mysql类实例
345
+ */
346
+ function mysql_admin(scope, config) {
347
+ if (!scope) {
348
+ scope = 'sys';
349
+ }
350
+ var obj = $.pool.mysql[scope];
351
+ if (!obj) {
352
+ $.pool.mysql[scope] = new Mysql(config);
353
+ obj = $.pool.mysql[scope];
354
+ }
355
+ return obj;
356
+ }
357
+
358
+ /**
359
+ * @module 导出Mysql管理器
360
+ */
1072
361
  exports.mysql_admin = mysql_admin;