mm_mysql 2.0.2 → 2.0.4
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/README.md +266 -234
- package/db.js +516 -286
- package/index.js +1022 -394
- package/package.json +13 -12
- package/sql.js +993 -396
- package/config.json +0 -8
- package/link_model.js +0 -132
- package/sql.json +0 -56
- package/test.js +0 -846
- package/upgrade.sql +0 -34
package/index.js
CHANGED
|
@@ -1,444 +1,1072 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @
|
|
4
|
-
* @
|
|
2
|
+
* MySQL数据库操作类
|
|
3
|
+
* @class Mysql
|
|
4
|
+
* @extends BaseService
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} = require('
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
35
|
+
/**
|
|
36
|
+
* 构造函数
|
|
37
|
+
* @param {Object} config - 配置对象
|
|
38
|
+
*/
|
|
39
|
+
constructor(config = {}) {
|
|
40
|
+
// 修复配置合并问题 - 手动合并配置以确保用户配置优先级
|
|
41
|
+
const mergedConfig = { ...Mysql.default_config, ...config };
|
|
42
|
+
super(mergedConfig);
|
|
14
43
|
|
|
15
|
-
|
|
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
|
-
*
|
|
59
|
+
* 开始事务
|
|
60
|
+
* @returns {Promise<Object>} 事务连接对象
|
|
19
61
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 {
|
|
207
|
-
* @
|
|
208
|
-
* @return {Promise} 异步构造器, 当await时返回执行结果
|
|
123
|
+
* 在事务中执行多个操作
|
|
124
|
+
* @param {Function} callback - 包含事务操作的回调函数
|
|
125
|
+
* @returns {Promise<*>} 回调函数的返回值
|
|
209
126
|
*/
|
|
210
|
-
Mysql.prototype.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 {
|
|
256
|
-
* @
|
|
257
|
-
* @return {Promise} 异步构造器, 当await时返回执行结果
|
|
170
|
+
* 打开数据库连接
|
|
171
|
+
* @param {Number} timeout - 超时时间(毫秒)
|
|
172
|
+
* @returns {Promise<boolean>}
|
|
258
173
|
*/
|
|
259
|
-
Mysql.prototype.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
* @
|
|
354
|
-
* @
|
|
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.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
* @
|
|
334
|
+
* 关闭数据库连接
|
|
335
|
+
* @returns {Promise<boolean>}
|
|
375
336
|
*/
|
|
376
|
-
Mysql.prototype.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
*
|
|
389
|
-
* @param {
|
|
382
|
+
* 获取数据库连接
|
|
383
|
+
* @param {Number} timeout - 超时时间(毫秒)
|
|
384
|
+
* @returns {Promise<Object>}
|
|
390
385
|
*/
|
|
391
|
-
Mysql.prototype.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
*
|
|
442
|
+
* 内部获取连接方法
|
|
443
|
+
* @private
|
|
444
|
+
* @returns {Promise<Object>}
|
|
401
445
|
*/
|
|
402
|
-
Mysql.prototype.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
*
|
|
474
|
+
* 处理连接错误
|
|
475
|
+
* @private
|
|
476
|
+
* @param {Error} err - 错误对象
|
|
412
477
|
*/
|
|
413
|
-
|
|
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;
|
|
414
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
|
+
};
|
|
415
601
|
|
|
416
602
|
/**
|
|
417
|
-
*
|
|
603
|
+
* 执行SQL语句(用于执行非查询语句如INSERT/UPDATE/DELETE)
|
|
604
|
+
* @param {String} sql - SQL语句
|
|
605
|
+
* @param {Array} params - 参数数组
|
|
606
|
+
* @param {Number} timeout - 超时时间(毫秒)
|
|
607
|
+
* @returns {Promise<Object>}
|
|
418
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
|
+
}
|
|
419
1047
|
if (!$.pool.mysql) {
|
|
420
|
-
|
|
1048
|
+
$.pool.mysql = {};
|
|
421
1049
|
}
|
|
422
1050
|
|
|
423
1051
|
/**
|
|
424
|
-
* @description
|
|
1052
|
+
* @description Mysql管理器,用于创建缓存
|
|
425
1053
|
* @param {String} scope 作用域
|
|
426
|
-
* @param {
|
|
427
|
-
* @return {Object}
|
|
1054
|
+
* @param {Object} config 配置参数
|
|
1055
|
+
* @return {Object} 返回一个Mysql类实例
|
|
428
1056
|
*/
|
|
429
|
-
function mysql_admin(scope,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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;
|
|
439
1067
|
}
|
|
440
1068
|
|
|
441
1069
|
/**
|
|
442
|
-
* @
|
|
1070
|
+
* @module 导出Mysql管理器
|
|
443
1071
|
*/
|
|
444
1072
|
exports.mysql_admin = mysql_admin;
|