UNPKG

mysql78

Version:
452 lines (391 loc) 17.5 kB
import { promises as fs } from 'node:fs'; import dayjs from 'dayjs'; import { createHash } from 'node:crypto'; import * as mysql from 'mysql2/promise'; import UpInfo from 'koa78-upinfo'; import TsLog78 from 'tslog78'; import md5 from 'md5'; /** * 如果不行就回退到2.4.0 */ export default class Mysql78 { private _statementCache: Map<string, mysql.PreparedStatementInfo> = new Map(); private _pool: mysql.Pool | null = null; private _host: string = ''; public isLog: boolean = false; public isCount: boolean = false; private log: TsLog78 = TsLog78.Instance; private warnHandler: ((info: string, kind: string, up: UpInfo) => Promise<any>) | null = null; // 设置重试次数和重试延迟 private readonly maxRetryAttempts: number = 3; private readonly retryDelayMs: number = 1000; // 1秒延迟 constructor(config: { host?: string; port?: number; max?: number; user?: string; password: string; database: string; isLog?: boolean; isCount?: boolean; }) { if (!config) return; this._host = config.host ?? '127.0.0.1'; const port = config.port ?? 3306; // 端口 const max = config.max ?? 10; // 最大线程数 const user = config.user ?? 'root'; // mysql用户名 this.isLog = config.isLog ?? false; // 是否打印日志(影响性能) this.isCount = config.isCount ?? false; // 是否统计效率(影响性能) this._pool = mysql.createPool({ connectionLimit: max, host: this._host, port, user, password: config.password, database: config.database, dateStrings: true, connectTimeout: 30 * 1000, waitForConnections: true, // 等待连接池中的连接可用 }); } // 延迟函数 private async delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } // 获取连接,并在发生错误时重试 private async getConnectionWithRetry(): Promise<mysql.PoolConnection | null> { let attempts = 0; while (attempts < this.maxRetryAttempts) { try { const connection = await this._pool?.getConnection(); if (connection === undefined) { return null; // Explicitly return null if connection is undefined } return connection; } catch (err) { attempts++; this.log.error(`Connection attempt ${attempts} failed:`, err); if (attempts >= this.maxRetryAttempts) { throw err; } await this.delay(this.retryDelayMs); } } return null; } // 重试函数的包装:用于 `doGet`、`doM`、`doT` 等方法 private async retryOperation<T>(operation: () => Promise<T>): Promise<T> { let attempts = 0; while (attempts < this.maxRetryAttempts) { try { return await operation(); } catch (err) { attempts++; this.log.error(`Operation attempt ${attempts} failed:`, err); if (attempts >= this.maxRetryAttempts) { throw err; } await this.delay(this.retryDelayMs); } } throw new Error("Max retry attempts reached."); } /** * 创建系统常用表 * Create system common table * * */ async creatTb(up: UpInfo): Promise<string> { if (!this._pool) { return 'pool null'; } const cmdtext1 = "CREATE TABLE IF NOT EXISTS `sys_warn` ( `uid` varchar(36) NOT NULL DEFAULT '', `kind` varchar(100) NOT NULL DEFAULT '', `apisys` varchar(100) NOT NULL DEFAULT '', `apiobj` varchar(100) NOT NULL DEFAULT '', `content` text NOT NULL, `upid` varchar(36) NOT NULL DEFAULT '', `upby` varchar(50) DEFAULT '', `uptime` datetime NOT NULL, `idpk` int(11) NOT NULL AUTO_INCREMENT, `id` varchar(36) NOT NULL, `remark` varchar(200) NOT NULL DEFAULT '', `remark2` varchar(200) NOT NULL DEFAULT '', `remark3` varchar(200) NOT NULL DEFAULT '', `remark4` varchar(200) NOT NULL DEFAULT '', `remark5` varchar(200) NOT NULL DEFAULT '', `remark6` varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (`idpk`)) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;"; const cmdtext2 = "CREATE TABLE IF NOT EXISTS `sys_sql` ( `cid` varchar(36) NOT NULL DEFAULT '', `apiv` varchar(50) NOT NULL DEFAULT '', `apisys` varchar(50) NOT NULL DEFAULT '', `apiobj` varchar(50) NOT NULL DEFAULT '', `cmdtext` varchar(200) NOT NULL, `uname` varchar(50) NOT NULL DEFAULT '', `num` int(11) NOT NULL DEFAULT '0', `dlong` int(32) NOT NULL DEFAULT '0', `downlen` int(32) NOT NULL DEFAULT '0', `upby` varchar(50) NOT NULL DEFAULT '', `cmdtextmd5` varchar(50) NOT NULL DEFAULT '', `uptime` datetime NOT NULL, `idpk` int(11) NOT NULL AUTO_INCREMENT, `id` varchar(36) NOT NULL, `remark` varchar(200) NOT NULL DEFAULT '', `remark2` varchar(200) NOT NULL DEFAULT '', `remark3` varchar(200) NOT NULL DEFAULT '', `remark4` varchar(200) NOT NULL DEFAULT '', `remark5` varchar(200) NOT NULL DEFAULT '', `remark6` varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (`idpk`), UNIQUE KEY `u_v_sys_obj_cmdtext` (`apiv`,`apisys`,`apiobj`,`cmdtext`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;"; try { await this._pool.execute(cmdtext1); await this._pool.execute(cmdtext2); return 'ok'; } catch (err) { this.log.error(err as Error); return 'error'; } } async getStatement(connection: mysql.PoolConnection, cmdtext: string): Promise<mysql.PreparedStatementInfo> { const cacheKey = `${connection.threadId}:${cmdtext}`; if (this._statementCache.has(cacheKey)) { return this._statementCache.get(cacheKey)!; } const statement = await connection.prepare(cmdtext); this._statementCache.set(cacheKey, statement); return statement; } /** * sql get * @param cmdtext sql * @param values * @param up user upload */ async doGet(cmdtext: string, values: any[], up: UpInfo): Promise<any[]> { if (!this._pool) { return []; } const debug = up.debug ?? false; const dstart = new Date(); let connection: mysql.PoolConnection | null = null; let statement: mysql.PreparedStatementInfo | null = null; try { connection = await this.retryOperation(() => this.getConnectionWithRetry()); if (!connection) { throw new Error("Failed to get a valid connection."); } statement = await this.getStatement(connection, cmdtext); const [rows] = await statement.execute(values); const back = rows as any[]; if (debug) { this._addWarn(JSON.stringify(back) + " c:" + cmdtext + " v" + values.join(","), "debug_" + up.apisys, up); } const lendown = JSON.stringify(back).length; this._saveLog(cmdtext, values, new Date().getTime() - dstart.getTime(), lendown, up); return back; } catch (err) { this._addWarn(JSON.stringify(err) + " c:" + cmdtext + " v" + values.join(","), "err_" + up.apisys, up); this.log.error(err as Error, 'mysql_doGet'); throw err; } finally { // if (statement) { // await statement.close(); // 确保预处理语句被关闭 // } if (connection) { connection.release(); // 确保连接被释放 } } } async doT(cmds: string[], values: string[][], errtexts: string[], logtext: string, logvalue: string[], up: UpInfo): Promise<any> { if (!this._pool) { return 'pool null'; } const debug = up.debug ?? false; const dstart = new Date(); let connection: mysql.PoolConnection | null = null; try { connection = await this.retryOperation(() => this.getConnectionWithRetry()); if (!connection) { throw new Error("Failed to get a valid connection."); } await connection.beginTransaction(); const promises: Promise<any>[] = []; for (let i = 0; i < cmds.length; i++) { promises.push(this.doTran(cmds[i], values[i], connection, up)); } const results = await Promise.all(promises); let errmsg = "err!"; let haveAff0 = false; for (let i = 0; i < results.length; i++) { if (results[i].affectedRows === 0) { errmsg += errtexts[i]; haveAff0 = true; break; } } if (haveAff0 || results.length < cmds.length) { await connection.rollback(); connection.release(); return errmsg; } await connection.commit(); connection.release(); this._saveLog(logtext, logvalue, new Date().getTime() - dstart.getTime(), 1, up); return "ok"; } catch (err) { if (connection) { await connection.rollback(); connection.release(); } this.log.error(err as Error, 'mysql_doT'); return 'error'; } } /** * sql update Method returns the number of affected rows * @param cmdtext sql * @param values * @param up user upload */ async doM(cmdtext: string, values: any[], up: UpInfo): Promise<number> { if (!this._pool) { return 0; } const debug = up.debug ?? false; const dstart = new Date(); let connection: mysql.PoolConnection | null = null; let statement: mysql.PreparedStatementInfo | null = null; try { connection = await this.retryOperation(() => this.getConnectionWithRetry()); if (!connection) { throw new Error("Failed to get a valid connection."); } statement = await this.getStatement(connection, cmdtext); const [result] = await statement.execute(values); const affectedRows = (result as mysql.ResultSetHeader).affectedRows; if (debug) { this._addWarn(JSON.stringify(result) + " c:" + cmdtext + " v" + values.join(","), "debug_" + up.apisys, up); } const lendown = JSON.stringify(result).length; this._saveLog(cmdtext, values, new Date().getTime() - dstart.getTime(), lendown, up); return affectedRows; } catch (err) { this._addWarn(JSON.stringify(err) + " c:" + cmdtext + " v" + values.join(","), "err" + up.apisys, up); this.log.error(err as Error, 'mysql_doM'); return -1; } finally { // if (statement) { // await statement.close(); // 确保预处理语句被关闭 // } if (connection) { connection.release(); // 确保连接被释放 } } } /** * Inserting a row returns the inserted row number * @param cmdtext * @param values * @param up */ async doMAdd(cmdtext: string, values: any[], up: UpInfo): Promise<number> { if (!this._pool) { return 0; } const debug = up.debug ?? false; const dstart = new Date(); try { const [result] = await this._pool.execute(cmdtext, values); const insertId = (result as mysql.ResultSetHeader).insertId; if (debug) { this._addWarn(JSON.stringify(result) + " c:" + cmdtext + " v" + values.join(","), "debug_" + up.apisys, up); } const lendown = JSON.stringify(result).length; this._saveLog(cmdtext, values, new Date().getTime() - dstart.getTime(), lendown, up); return insertId; } catch (err) { this._addWarn(JSON.stringify(err) + " c:" + cmdtext + " v" + values.join(","), "err" + up.apisys, up); this.log.error(err as Error, 'mysql_doMAdd'); return 0; } } /** * Transactions are executed piecemeal (it is usually better not to use doT) * You need to release the connection yourself * There may be complicated scenarios where the first sentence is successful but what condition has changed and you still need to roll back the transaction * @param cmdtext * @param values * @param con * @param up */ async doTran(cmdtext: string, values: any[], con: mysql.PoolConnection, up: UpInfo): Promise<any> { const debug = up.debug ?? false; try { const [result] = await con.execute(cmdtext, values); if (debug) { this.log.info(`${cmdtext} v:${values.join(",")} r:${JSON.stringify(result)} 0, 'mysql', 'doTran', ${up.uname}`, 0); } return result; } catch (err) { this.log.error(err as Error, 'mysql_doTran'); throw err; } } /** * doget doM does not need to be released manually * getConnection * */ async releaseConnection(client: mysql.PoolConnection): Promise<void> { if (this._pool) { this._pool.releaseConnection(client); } } /** * Get the connection (remember to release it) * */ async getConnection(): Promise<mysql.PoolConnection | null> { if (!this._pool) { return null; } try { const connection = await this._pool.getConnection(); return connection; } catch (err) { this.log.error(err as Error, 'mysql_getConnection'); return null; } } /** * 设置警告处理器 * @param handler 处理警告的函数 */ setWarnHandler(handler: (info: string, kind: string, up: UpInfo) => Promise<any>): void { this.warnHandler = handler; } /** * 调试函数,用于跟踪SQL调用的在线调试问题 * 可以设置跟踪用户、表、目录或函数等 * 开启会影响性能,建议主要用于跟踪开发者和正在开发的目录 * 如果设置了自定义的warnHandler,将优先使用它处理警告 * 否则,将警告信息插入sys_warn表 * @param info 日志信息 * @param kind 日志类型 * @param up 用户上传信息 */ private async _addWarn(info: string, kind: string, up: UpInfo): Promise<string | number> { if (this.warnHandler) { try { return await this.warnHandler(info, kind, up); } catch (err) { this.log.error(err as Error, 'mysql__addWarn_handler'); } } if (!this._pool || !this.isLog) { return this.isLog ? 'pool null' : 'isLog is false'; } const cmdtext = 'INSERT INTO sys_warn (`kind`,apisys,apiobj,`content`,`upby`,`uptime`,`id`,upid)VALUES(?,?,?,?,?,?,?,?)'; const values = [kind, up.apisys, up.apiobj, info, up.uname, up.uptime, UpInfo.getNewid(), up.upid]; try { const [results] = await this._pool.execute(cmdtext, values); return (results as mysql.ResultSetHeader).affectedRows; } catch (err) { this.log.error(err as Error, 'mysql__addWarn'); return 0; } } /** * If the table name SYS_SQL is opened after the function, it will affect performance * @param cmdtext SQL * @param values * @param dlong Function Timing * @param lendown down bytes * @param up user upload */ private async _saveLog(cmdtext: string, values: any[], dlong: number, lendown: number, up: UpInfo): Promise<string> { if (!this.isCount || !this._pool) { return this.isCount ? 'pool null' : 'isCount is false'; } const cmdtextmd5 = md5(cmdtext); const sb = 'INSERT INTO sys_sql(apiv,apisys,apiobj,cmdtext,num,dlong,downlen,id,uptime,cmdtextmd5)VALUES(?,?,?,?,?,?,?,?,?,?) ' + 'ON DUPLICATE KEY UPDATE num=num+1,dlong=dlong+?,downlen=downlen+?'; try { await this._pool.execute(sb, [ up.v, up.apisys, up.apiobj, cmdtext, 1, dlong, lendown, UpInfo.getNewid(), new Date(), cmdtextmd5, dlong, lendown ]); return 'ok'; } catch (err) { this.log.error(err as Error); return 'error'; } } async close() { if (this._pool) { await this._pool.end(); } } }