mm_mysql 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,444 +1,1031 @@
1
1
  /**
2
- * @fileOverview Mysql帮助类函数
3
- * @author <a href="http://qww.elins.cn">邱文武</a>
4
- * @version 1.2
2
+ * MySQL数据库操作类
3
+ * @class Mysql
4
+ * @extends BaseService
5
5
  */
6
- const {
7
- createPool
8
- } = require('mysql2/promise');
9
- const {
10
- DB
11
- } = require('./db');
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
+ };
12
34
 
13
- const Link_model = require('./link_model');
35
+ /**
36
+ * 构造函数
37
+ * @param {Object} config - 配置对象
38
+ */
39
+ constructor(config = {}) {
40
+ // 修复配置合并问题 - 手动合并配置以确保用户配置优先级
41
+ const mergedConfig = { ...Mysql.default_config, ...config };
42
+ super(mergedConfig);
14
43
 
15
- var pools = {};
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
+ }
16
57
 
17
58
  /**
18
- * @description 数据库封装
59
+ * 开始事务
60
+ * @returns {Promise<Object>} 事务连接对象
19
61
  */
20
- class Mysql {
21
- /**
22
- * @description 创建Mysql帮助类函数 (构造函数)
23
- * @param {String} scope 作用域
24
- * @param {String} dir 当前路径
25
- * @constructor
26
- */
27
- constructor(scope, dir) {
28
- // 作用域
29
- this.scope;
30
- if (scope) {
31
- this.scope = scope;
32
- } else {
33
- this.scope = $.val.scope + '';
34
- }
35
- // 当前目录
36
- this.dir = __dirname;
37
- if (dir) {
38
- this.dir = dir;
39
- }
40
- // 错误提示
41
- this.error;
42
- /**
43
- * sql语句
44
- */
45
- this.sql = "";
46
- // 连接池
47
- this.pool;
48
- // 连接态 0未连接,1已连接
49
- this.state = 0;
50
-
51
- // 数据库配置参数
52
- this.config = {
53
- // 服务器地址
54
- host: "127.0.0.1",
55
- // 端口号
56
- port: 3306,
57
- // 连接用户名
58
- user: "root",
59
- // 连接密码
60
- password: "asd123",
61
- // 数据库
62
- database: "mm",
63
- // 是否支持多个sql语句同时操作
64
- multipleStatements: false,
65
- // // 打印SQL
66
- // log: true,
67
- // // 排除打印
68
- // log_ignore: [1062],
69
- // debug: true,
70
- // 启用keep-alive,保持连接活跃
71
- enableKeepAlive: true,
72
- waitForConnections: true,
73
- compress: false // 启用压缩
74
- };
75
-
76
- // 唯一标识符
77
- this.identifier = this.config.host + "/" + this.config.database;
78
-
79
- // 定义当前类, 用于数据库实例化访问
80
- var $this = this;
81
-
82
- /**
83
- * @description 查询sql
84
- * @param {String} sql 查询参
85
- * @param {Array} val 替换值
86
- * @return {Promise|Array} 异步构造器, 当await时返回执行结果
87
- */
88
- this.run = async function(sql, val) {
89
- this.sql = sql;
90
- if ($this.config.log) {
91
- $.log.debug("SQL:", sql);
92
- }
93
- var conn;
94
- var list = [];
95
- try {
96
- // 关于连接池初始化,请参阅上文
97
- conn = await $this.getConn();
98
- const [rows] = await conn.query(sql, val);
99
- // 查询解析时,连接会自动释放
100
- list = rows;
101
- } catch (err) {
102
- console.error("mysql查询错误", err);
103
- this.error = {
104
- code: err.errno,
105
- message: err.sqlMessage
106
- };
107
- } finally {
108
- if (conn) conn.release();
109
- }
110
- return list;
111
- };
112
-
113
- /**
114
- * @description 增删改sql
115
- * @param {String} sql 查询参
116
- * @param {Array} val 替换值
117
- * @return {Promise|Array} 异步构造器, 当await时返回执行结果
118
- */
119
- this.exec = async function(sql, val) {
120
- if (this.task) {
121
- this.task_sql += sql + "\n";
122
- if (this.task === 1) {
123
- return this.task_sql;
124
- } else if (this.task === 2) {
125
- this.task = 0;
126
- sql = this.task_sql.trim();
127
- this.task_sql = '';
128
- } else {
129
- this.task = 0;
130
- this.task_sql = '';
131
- return;
132
- }
133
- }
134
- this.sql = sql;
135
- if ($this.config.log) {
136
- $.log.debug("SQL:", sql);
137
- }
138
- var conn;
139
- var bl = -1;
140
- try {
141
- conn = await $this.getConn();
142
- var [rows] = await conn.execute(sql, val);
143
- if (rows.constructor == Array) {
144
- if (rows.length > 0) {
145
- var num = 0;
146
- rows.map(function(item) {
147
- num += item['affectedRows'];
148
- });
149
- if (num === 0) {
150
- bl = rows.length;
151
- } else {
152
- bl = num;
153
- }
154
- } else {
155
- bl = 1;
156
- }
157
- } else {
158
- var num = rows['affectedRows'];
159
- if (num === 0) {
160
- bl = 1;
161
- } else {
162
- bl = num;
163
- }
164
- }
165
- // 查询解析时,连接会自动释放
166
- } catch (err) {
167
- console.error("mysql执行错误", err);
168
- this.error = {
169
- code: err.errno,
170
- message: err.sqlMessage
171
- };
172
- } finally {
173
- if (conn) conn.release();
174
- }
175
-
176
- return bl;
177
- };
178
-
179
- /**
180
- * @description 获取数据库管理器
181
- */
182
- this.db = function() {
183
- return new DB($this);
184
- };
185
- }
186
- }
62
+ Mysql.prototype.beginTransaction = async function () {
63
+ try {
64
+ // 检查连接状态
65
+ if (this._status !== 'connected') {
66
+ throw new Error('数据库连接未建立');
67
+ }
187
68
 
188
- Mysql.prototype.getConn = async function() {
189
- let conn;
190
- try {
191
- if (!this.pool) {
192
- await this.open();
193
- }
194
- conn = await this.pool.getConnection();
195
- } catch (err) {
196
- console.error('Connection failed - retrying: ' + err.message);
197
- // 重试逻辑,例如等待一段时间后再次尝试获取连接
198
- await new Promise(resolve => setTimeout(resolve, 5000)); // 等待5秒后重试
199
- conn = await this.getConn(); // 递归调用直到成功获取连接
200
- }
201
- return conn;
202
- }
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
+ };
203
121
 
204
122
  /**
205
- * 导入数据库
206
- * @param {Object} file sql文件
207
- * @param {Function} func 回调函数
208
- * @return {Promise} 异步构造器, 当await时返回执行结果
123
+ * 在事务中执行多个操作
124
+ * @param {Function} callback - 包含事务操作的回调函数
125
+ * @returns {Promise<*>} 回调函数的返回值
209
126
  */
210
- Mysql.prototype.load = async function(file, func) {
211
- var count = 0;
212
- var progress = 0;
213
- var errors = [];
214
- var index = 0;
215
- try {
216
- var data = file.loadText();
217
- // 将SQL文件内容分割成单独的语句
218
- const arr = data.replace(/\r\n/g, '\n').split(';\n');
219
- count = arr.length;
220
- for (var i = 0; i < arr.length; i++) {
221
- var sql_str = arr[i].trim();
222
- if (sql_str !== '') {
223
- await this.run(sql_str + ";");
224
- if (this.error) {
225
- errors.push({
226
- sql_str: sql_str.slice(0, 512),
227
- error: this.error
228
- });
229
- // 如果数据库链接失败(密码错误或无法连接),则退出循环
230
- if (this.error.code == 1045 || this.error.code == 2003) {
231
- break;
232
- }
233
- }
234
- }
235
- var p = i / arr.length;
236
- progress = Math.ceil(p * 100);
237
- index = i;
238
- if (func) {
239
- func(progress, index, this.error, sql_str);
240
- }
241
- }
242
- } catch (err) {
243
- $.log.error("导入SQL文件失败!", err);
244
- }
245
- return {
246
- index,
247
- count,
248
- progress,
249
- errors
250
- }
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
+ }
251
167
  };
252
168
 
253
169
  /**
254
- * 保存数据库为文件
255
- * @param {String} file 文件对象
256
- * @param {Function} func 回调函数
257
- * @return {Promise} 异步构造器, 当await时返回执行结果
170
+ * 打开数据库连接
171
+ * @param {Number} timeout - 超时时间(毫秒)
172
+ * @returns {Promise<boolean>}
258
173
  */
259
- Mysql.prototype.save = async function(file, func, tables = []) {
260
- const fs = require('fs');
261
- const stream = fs.createWriteStream(file);
262
- let count = 0;
263
- let progress = 0;
264
- let errors = [];
265
- let index = 0;
266
- let tableCount = 0;
267
-
268
- try {
269
- // 开始导出数据库
270
- stream.write('SET FOREIGN_KEY_CHECKS = 0;\n\n');
271
- count++;
272
-
273
- if (!tables.length) {
274
- // 获取所有表
275
- var tbs = await this.run('SHOW TABLES');
276
- tables = tbs.map((item) => {
277
- return item[Object.keys(item)[0]];
278
- });
279
- }
280
-
281
- tableCount = tables.length;
282
- for (var i = 0; i < tables.length; i++) {
283
- var tableName = tables[i];
284
- try {
285
- // 导出表结构
286
- const createTable = await this.run(`SHOW CREATE TABLE ${tableName}`);
287
- stream.write(`-- 表结构: ${tableName}\n`);
288
- stream.write(`DROP TABLE IF EXISTS \`${tableName}\`;\n`);
289
- count++;
290
- stream.write(`${createTable[0]['Create Table']};\n\n`);
291
- count++;
292
- // 导出表数据
293
- const rows = await this.run(`SELECT * FROM ${tableName}`);
294
- if (rows.length > 0) {
295
- stream.write(`-- 表数据: ${tableName}\n`);
296
- for (const row of rows) {
297
- const rowValues = Object.values(row)
298
- .map(value => {
299
- if (value === null) return 'NULL';
300
- if (typeof value === 'boolean') return value ? 1 : 0;
301
- if (typeof value === 'number') return value;
302
- if (value instanceof Date) {
303
- return "'" + value.toStr('yyyy-MM-dd hh:mm:ss') + "'";
304
- }
305
- // 处理字符串,转义特殊字符
306
- return "'" + value.toString()
307
- .replace(/[\\']/g, '\\$&')
308
- .replace(/\n/g, '\\n')
309
- .replace(/\r/g, '\\r')
310
- .replace(/\t/g, '\\t')
311
- .replace(/\u0000/g, '\\0') + "'";
312
- })
313
- .join(',');
314
- stream.write(`INSERT INTO ${tableName} VALUES (${rowValues});\n`);
315
- count++;
316
- }
317
- stream.write('\n');
318
- }
319
- } catch (err) {
320
- errors.push({
321
- table: tableName,
322
- error: err.message
323
- });
324
- }
325
-
326
- index++;
327
- progress = Math.ceil((index / tableCount) * 100);
328
- if (func) {
329
- func(progress, index, this.error, tableName);
330
- }
331
- }
332
-
333
- // 完成导出
334
- stream.write('SET FOREIGN_KEY_CHECKS = 1;\n');
335
- count++;
336
- stream.end();
337
- } catch (err) {
338
- $.log.error("导出SQL文件失败!", err);
339
- stream.end();
340
- }
341
-
342
- return {
343
- tableCount,
344
- index,
345
- count,
346
- progress,
347
- errors
348
- }
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
+ }
349
234
  };
350
235
 
351
236
  /**
352
- * 获取数据库管理器
353
- * @param {String} key 主键
354
- * @param {String|Number} value 对象值
355
- * @param {Boolean} clear_prefix 清除前缀
356
- * @param {Array} arr_table 关联的数据表
357
- * @return {Object} 管理模型
237
+ * 内部连接方法
238
+ * @private
239
+ * @returns {Promise<void>}
358
240
  */
359
- Mysql.prototype.dbs = async function(key, value, clear_prefix, ...arr_table) {
360
- var lm = new Link_model({
361
- key,
362
- value,
363
- clear_prefix
364
- });
365
- lm.sql = this;
366
- for (var i = 0; i < arr_table.length; i++) {
367
- await lm.add(arr_table[i]);
368
- }
369
- return lm;
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
+ });
370
331
  };
371
332
 
372
333
  /**
373
- * 设置配置参数
374
- * @param {Object} cg 配置对象或配置路径
334
+ * 关闭数据库连接
335
+ * @returns {Promise<boolean>}
375
336
  */
376
- Mysql.prototype.setConfig = function(cg) {
377
- var obj;
378
- if (typeof(cg) === "string") {
379
- obj = cg.loadJson(this.dir);
380
- } else {
381
- obj = cg;
382
- }
383
- $.push(this.config, obj);
384
- this.identifier = this.config.host + "/" + this.config.database;
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
+ });
385
379
  };
386
380
 
387
381
  /**
388
- * @description 打开数据库, 如果没有则建立数据库连接再打开
389
- * @param {boolean} 是否重置
382
+ * 获取数据库连接
383
+ * @param {Number} timeout - 超时时间(毫秒)
384
+ * @returns {Promise<Object>}
390
385
  */
391
- Mysql.prototype.open = function(reset) {
392
- if (reset || !this.state || !pools[this.identifier]) {
393
- this.state = 1;
394
- pools[this.identifier] = createPool(this.config);
395
- }
396
- this.pool = pools[this.identifier];
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
+ }
397
439
  };
398
440
 
399
441
  /**
400
- * @description 关闭连接
442
+ * 内部获取连接方法
443
+ * @private
444
+ * @returns {Promise<Object>}
401
445
  */
402
- Mysql.prototype.close = function() {
403
- if (pools[this.identifier]) {
404
- pools[this.identifier].end();
405
- pools[this.identifier] = null;
406
- delete pools[this.identifier];
407
- }
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
+ });
408
471
  };
409
472
 
410
473
  /**
411
- * @description 导出Mysql帮助类
474
+ * 处理连接错误
475
+ * @private
476
+ * @param {Error} err - 错误对象
412
477
  */
413
- exports.Mysql = Mysql;
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;
414
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
+ // 对于增删改操作,返回影响的行数
630
+ return rows.affectedRows || rows.insertId || rows.changedRows || rows;
631
+ } catch (err) {
632
+ $.log.error('[Mysql] [exec]', 'SQL执行失败', {
633
+ error: err.message,
634
+ sql: sql.substring(0, 200),
635
+ params: params
636
+ });
637
+
638
+ // 处理连接错误,触发重连
639
+ if (err.code && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'PROTOCOL_CONNECTION_LOST')) {
640
+ this._handleConnectionError(err);
641
+ }
642
+
643
+ throw err;
644
+ } finally {
645
+ // 释放连接(如果是从连接池获取的)
646
+ if (conn && isPoolConn) {
647
+ try {
648
+ conn.release();
649
+ } catch (releaseErr) {
650
+ $.log.error('释放连接失败', { error: releaseErr.message });
651
+ }
652
+ }
653
+ }
654
+ };
655
+
656
+ /**
657
+ * 读取整张表的数据
658
+ * @param {String} table - 表名
659
+ * @param {Object} condition - 查询条件
660
+ * @param {Object} options - 选项(orderBy, limit, offset等)
661
+ * @returns {Promise<Array>}
662
+ */
663
+ Mysql.prototype.read = async function (table, condition = {}, options = {}) {
664
+ try {
665
+ // 检查连接状态
666
+ if (this._status !== 'connected') {
667
+ throw new Error('数据库连接未建立');
668
+ }
669
+
670
+ // 构建基础SQL查询
671
+ let sql = `SELECT * FROM ${table}`;
672
+ const params = [];
673
+
674
+ // 处理条件
675
+ if (Object.keys(condition).length > 0) {
676
+ const whereClauses = [];
677
+ for (const [field, value] of Object.entries(condition)) {
678
+ whereClauses.push(`${field} = ?`);
679
+ params.push(value);
680
+ }
681
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
682
+ }
683
+
684
+ // 处理排序
685
+ if (options.orderBy) {
686
+ sql += ` ORDER BY ${options.orderBy}`;
687
+ }
688
+
689
+ // 处理分页
690
+ if (options.limit) {
691
+ sql += ` LIMIT ${options.limit}`;
692
+ }
693
+
694
+ if (options.offset) {
695
+ sql += ` OFFSET ${options.offset}`;
696
+ }
697
+
698
+ // 记录查询日志
699
+ if (this.config.debug) {
700
+ $.log.debug(`[${this.constructor.name}] [read] 查询数据`, { sql, params });
701
+ }
702
+
703
+ // 执行查询
704
+ const results = await this.exec(sql, params);
705
+
706
+ if (this.config.debug) {
707
+ $.log.info(`[${this.constructor.name}] [read] 查询成功`, { count: results.length });
708
+ }
709
+
710
+ return results;
711
+ } catch (error) {
712
+ // 记录错误日志
713
+ $.log.error(`[${this.constructor.name}] [read] 查询失败`, {
714
+ error: error.message,
715
+ table: table,
716
+ condition: condition
717
+ });
718
+
719
+ // 抛出错误
720
+ throw error;
721
+ }
722
+ };
723
+
724
+ /**
725
+ * 保存数据库结构到SQL文件
726
+ * @param {String} file - 输出文件路径
727
+ * @param {Array} tables - 要保存的表名数组,空数组表示保存所有表
728
+ * @returns {Promise<boolean>}
729
+ */
730
+ Mysql.prototype.save = async function (file, tables = []) {
731
+ try {
732
+ let sqlContent = '';
733
+
734
+ // 获取所有表名
735
+ const allTablesResult = await this.run('SHOW TABLES');
736
+ let tableNames = [];
737
+
738
+ if (allTablesResult && allTablesResult.length > 0) {
739
+ const tableKey = Object.keys(allTablesResult[0])[0];
740
+ tableNames = allTablesResult.map(row => row[tableKey]);
741
+ }
742
+
743
+ // 过滤要保存的表
744
+ const tablesToSave = tables.length > 0
745
+ ? tableNames.filter(name => tables.includes(name))
746
+ : tableNames;
747
+
748
+ // 导出每个表的结构
749
+ for (const tableName of tablesToSave) {
750
+ const createSqlResult = await this.run(`SHOW CREATE TABLE \`${tableName}\``);
751
+ if (createSqlResult && createSqlResult.length > 0) {
752
+ const createSql = createSqlResult[0]['Create Table'];
753
+ sqlContent += `\n\n-- Table structure for \`${tableName}\`\n`;
754
+ sqlContent += createSql + ';\n';
755
+ }
756
+ }
757
+
758
+ // 确保目录存在
759
+ file.addDir();
760
+ file.saveText(sqlContent);
761
+ $.log.info(`成功导出数据库结构到: ${file}`);
762
+ return true;
763
+ } catch (err) {
764
+ $.log.error(`导出数据库结构失败: ${file}`, { error: err.message });
765
+ throw err;
766
+ }
767
+ };
768
+
769
+ /**
770
+ * 从SQL文件加载数据库结构和数据
771
+ * @param {String} file - SQL文件路径
772
+ * @returns {Promise<boolean>} 是否加载成功
773
+ */
774
+ Mysql.prototype.load = async function (file) {
775
+ try {
776
+ // 记录操作日志
777
+ if (this.config.debug) {
778
+ $.log.debug(`[${this.constructor.name}] [load] 开始从文件加载数据库`, {
779
+ file: file
780
+ });
781
+ }
782
+
783
+ // 检查文件是否存在
784
+ if (!file.hasFile()) {
785
+ throw new Error(`SQL文件不存在: ${file}`);
786
+ }
787
+
788
+ // 读取SQL文件内容
789
+ const sqlContent = file.readText();
790
+
791
+ if (!sqlContent || sqlContent.trim() === '') {
792
+ throw new Error(`SQL文件内容为空: ${file}`);
793
+ }
794
+
795
+ // 分割SQL语句(按分号分割,但需要注意处理字符串中的分号)
796
+ const sqlStatements = this._splitSqlStatements(sqlContent);
797
+
798
+ // 开始事务执行SQL语句
799
+ await this.exec('START TRANSACTION');
800
+
801
+ try {
802
+ // 逐个执行SQL语句
803
+ for (const sql of sqlStatements) {
804
+ const trimmedSql = sql.trim();
805
+ if (trimmedSql) {
806
+ await this.exec(trimmedSql);
807
+ }
808
+ }
809
+
810
+ // 提交事务
811
+ await this.exec('COMMIT');
812
+
813
+ if (this.config.debug) {
814
+ $.log.info(`[${this.constructor.name}] [load] 数据库加载成功`, {
815
+ file: file,
816
+ statementCount: sqlStatements.length
817
+ });
818
+ }
819
+
820
+ return true;
821
+ } catch (err) {
822
+ // 回滚事务
823
+ await this.exec('ROLLBACK').catch(rollbackErr => {
824
+ $.log.error(`[${this.constructor.name}] [load] 事务回滚失败`, {
825
+ error: rollbackErr.message
826
+ });
827
+ });
828
+
829
+ throw err;
830
+ }
831
+ } catch (error) {
832
+ // 记录错误日志
833
+ $.log.error(`[${this.constructor.name}] [load] 数据库加载失败`, {
834
+ error: error.message,
835
+ file: file
836
+ });
837
+
838
+ // 抛出错误
839
+ throw error;
840
+ }
841
+ };
415
842
 
416
843
  /**
417
- * @description mysql连接池
844
+ * 分割SQL语句
845
+ * @private
846
+ * @param {String} sqlContent - SQL内容
847
+ * @returns {Array} SQL语句数组
418
848
  */
849
+ Mysql.prototype._splitSqlStatements = function(sqlContent) {
850
+ const statements = [];
851
+ let inString = false;
852
+ let stringChar = '';
853
+ let inComment = false;
854
+ let currentStatement = '';
855
+
856
+ for (let i = 0; i < sqlContent.length; i++) {
857
+ const char = sqlContent[i];
858
+ const nextChar = i + 1 < sqlContent.length ? sqlContent[i + 1] : '';
859
+
860
+ // 处理注释
861
+ if (!inString && !inComment && char === '-' && nextChar === '-') {
862
+ inComment = true;
863
+ i++; // 跳过第二个'-'
864
+ continue;
865
+ }
866
+
867
+ if (inComment && char === '\n') {
868
+ inComment = false;
869
+ continue;
870
+ }
871
+
872
+ if (inComment) {
873
+ continue;
874
+ }
875
+
876
+ // 处理多行注释
877
+ if (!inString && !inComment && char === '/' && nextChar === '*') {
878
+ inComment = true;
879
+ i++; // 跳过'*'
880
+ continue;
881
+ }
882
+
883
+ if (inComment && char === '*' && nextChar === '/') {
884
+ inComment = false;
885
+ i++; // 跳过'/'
886
+ continue;
887
+ }
888
+
889
+ // 处理字符串
890
+ if (!inComment && (char === "'" || char === '"') && (!inString || stringChar === char)) {
891
+ // 检查是否是转义的引号
892
+ let escaped = false;
893
+ for (let j = i - 1; j >= 0; j--) {
894
+ if (sqlContent[j] === '\\') {
895
+ escaped = !escaped;
896
+ } else {
897
+ break;
898
+ }
899
+ }
900
+
901
+ if (!escaped) {
902
+ if (inString && stringChar === char) {
903
+ inString = false;
904
+ stringChar = '';
905
+ } else if (!inString) {
906
+ inString = true;
907
+ stringChar = char;
908
+ }
909
+ }
910
+ }
911
+
912
+ // 分割语句
913
+ if (!inString && !inComment && char === ';') {
914
+ statements.push(currentStatement.trim());
915
+ currentStatement = '';
916
+ } else {
917
+ currentStatement += char;
918
+ }
919
+ }
920
+
921
+ // 添加最后一个语句(如果有)
922
+ if (currentStatement.trim()) {
923
+ statements.push(currentStatement.trim());
924
+ }
925
+
926
+ return statements;
927
+ };
928
+
929
+ /**
930
+ * 获取DB实例(用于高级操作)
931
+ * @returns {Object} 包含exec、add、set、get、del等方法的数据库操作对象
932
+ */
933
+ Mysql.prototype.db = function () {
934
+ if (this._status !== 'connected') {
935
+ throw new Error('数据库连接未建立');
936
+ }
937
+
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
+ /**
996
+ * 导出模块
997
+ */
998
+ exports.Mysql = Mysql;
999
+
1000
+ /**
1001
+ * 确保连接池对象存在
1002
+ */
1003
+ if (!$.pool) {
1004
+ $.pool = {};
1005
+ }
419
1006
  if (!$.pool.mysql) {
420
- $.pool.mysql = {};
1007
+ $.pool.mysql = {};
421
1008
  }
422
1009
 
423
1010
  /**
424
- * @description 缓存管理器,用于创建缓存
1011
+ * @description Mysql管理器,用于创建缓存
425
1012
  * @param {String} scope 作用域
426
- * @param {String} dir 当前路径
427
- * @return {Object} 返回一个缓存类
1013
+ * @param {Object} config 配置参数
1014
+ * @return {Object} 返回一个Mysql类实例
428
1015
  */
429
- function mysql_admin(scope, dir) {
430
- if (!scope) {
431
- scope = $.val.scope
432
- }
433
- var obj = $.pool.mysql[scope];
434
- if (!obj) {
435
- $.pool.mysql[scope] = new Mysql(scope, dir);
436
- obj = $.pool.mysql[scope];
437
- }
438
- return obj;
1016
+ function mysql_admin(scope, config) {
1017
+ if (!scope) {
1018
+ scope = 'sys';
1019
+ }
1020
+ var obj = $.pool.mysql[scope];
1021
+ if (!obj) {
1022
+ $.pool.mysql[scope] = new Mysql(config);
1023
+ obj = $.pool.mysql[scope];
1024
+ }
1025
+ return obj;
439
1026
  }
440
1027
 
441
1028
  /**
442
- * @description 导出Mysql管理函数
1029
+ * @module 导出Mysql管理器
443
1030
  */
444
1031
  exports.mysql_admin = mysql_admin;