| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  | // queryBuilder.js
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | class QueryBuilder { | 
					
						
							|  |  |  |     constructor() { | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         this.conditions = []; // { sql, params }
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         this.limit = null; | 
					
						
							|  |  |  |         this.offset = null; | 
					
						
							|  |  |  |         this.groupBy = null; | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         this.orderBy = null; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     where(condition, ...params) { | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         this.conditions.push({ sql: condition, params }); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setLimit(limit) { | 
					
						
							|  |  |  |         this.limit = limit; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setOffset(offset) { | 
					
						
							|  |  |  |         this.offset = offset; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setGroupBy(groupBy) { | 
					
						
							|  |  |  |         this.groupBy = groupBy; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     orderByField(field, direction = 'ASC') { | 
					
						
							|  |  |  |         this.orderBy = { field, direction: direction.toUpperCase() }; | 
					
						
							|  |  |  |         return this; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     buildConditions() { | 
					
						
							|  |  |  |         if (!this.conditions.length) return { sql: '', params: [] }; | 
					
						
							|  |  |  |         const sql = this.conditions.map(c => `(${c.sql})`).join(' AND '); | 
					
						
							|  |  |  |         const params = this.conditions.flatMap(c => c.params); | 
					
						
							|  |  |  |         return { sql, params }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     buildLimitOffset() { | 
					
						
							|  |  |  |         let sql = ''; | 
					
						
							|  |  |  |         if (this.limit !== null) sql += ` LIMIT ${this.limit}`; | 
					
						
							|  |  |  |         if (this.offset !== null) sql += ` OFFSET ${this.offset}`; | 
					
						
							|  |  |  |         return sql; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async execute(db) { | 
					
						
							|  |  |  |         const { sql, params } = this.build(); | 
					
						
							|  |  |  |         const [rows] = await db.query(sql, params); | 
					
						
							|  |  |  |         return rows; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  | // ------------------- SELECT -------------------
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | class SelectBuilder extends QueryBuilder { | 
					
						
							|  |  |  |     constructor() { | 
					
						
							|  |  |  |         super(); | 
					
						
							|  |  |  |         this.selectFields = []; | 
					
						
							|  |  |  |         this.tables = []; | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         this.joins = []; | 
					
						
							|  |  |  |         this.unions = []; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     select(fields) { | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         if (typeof fields === 'string') { | 
					
						
							|  |  |  |             this.selectFields = fields.split(',').map(f => f.trim()); | 
					
						
							|  |  |  |         } else if (Array.isArray(fields)) { | 
					
						
							|  |  |  |             this.selectFields = fields; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     from(table) { | 
					
						
							|  |  |  |         this.tables.push(table); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     join(type, table, condition) { | 
					
						
							|  |  |  |         this.joins.push(`${type.toUpperCase()} JOIN ${table} ON ${condition}`); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     union(queryBuilder, type = 'UNION') { | 
					
						
							|  |  |  |         this.unions.push({ queryBuilder, type }); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     unionAll(queryBuilder) { | 
					
						
							|  |  |  |         return this.union(queryBuilder, 'UNION ALL'); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     build() { | 
					
						
							|  |  |  |         const selectClause = this.selectFields.length ? this.selectFields.join(', ') : '*'; | 
					
						
							|  |  |  |         let sql = `SELECT ${selectClause} FROM ${this.tables.join(', ')}`; | 
					
						
							|  |  |  |         if (this.joins.length) sql += ' ' + this.joins.join(' '); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const { sql: whereSql, params } = this.buildConditions(); | 
					
						
							|  |  |  |         if (whereSql) sql += ` WHERE ${whereSql}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (this.groupBy) sql += ` GROUP BY ${this.groupBy}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // 处理 UNION
 | 
					
						
							|  |  |  |         for (const u of this.unions) { | 
					
						
							|  |  |  |             const { sql: uSql, params: uParams } = u.queryBuilder.build(); | 
					
						
							|  |  |  |             sql += ` ${u.type} ${uSql}`; | 
					
						
							|  |  |  |             params.push(...uParams); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (this.orderBy) { | 
					
						
							|  |  |  |             sql += ` ORDER BY ${this.orderBy.field} ${this.orderBy.direction}`; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         sql += this.buildLimitOffset(); | 
					
						
							|  |  |  |         return { sql, params }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     async paginateWithCount(db, page = 1, pageSize = 10) { | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         if (page <= 0 || pageSize <= 0) { | 
					
						
							|  |  |  |             throw new Error('分页参数必须大于0'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         // count 查询
 | 
					
						
							|  |  |  |         let countSql = `SELECT COUNT(*) as total FROM ${this.tables.join(', ')}`; | 
					
						
							|  |  |  |         if (this.joins.length) countSql += ' ' + this.joins.join(' '); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         const { sql: whereSql, params } = this.buildConditions(); | 
					
						
							|  |  |  |         if (whereSql) countSql += ` WHERE ${whereSql}`; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         const [countRows] = await db.query(countSql, params); | 
					
						
							|  |  |  |         const total = countRows[0].total; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         // 数据查询
 | 
					
						
							|  |  |  |         this.setLimit(pageSize).setOffset((page - 1) * pageSize); | 
					
						
							|  |  |  |         const { sql, params: dataParams } = this.build(); | 
					
						
							|  |  |  |         const [rows] = await db.query(sql, dataParams); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         return { | 
					
						
							|  |  |  |             data: rows, | 
					
						
							|  |  |  |             total, | 
					
						
							|  |  |  |             page, | 
					
						
							|  |  |  |             pageSize, | 
					
						
							|  |  |  |             totalPages: Math.ceil(total / pageSize) | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  | // ------------------- UPDATE -------------------
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | class UpdateBuilder extends QueryBuilder { | 
					
						
							|  |  |  |     constructor() { | 
					
						
							|  |  |  |         super(); | 
					
						
							|  |  |  |         this.table = ''; | 
					
						
							|  |  |  |         this.updateFields = {}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     update(table) { | 
					
						
							|  |  |  |         this.table = table; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     set(field, value) { | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         if (typeof value === 'object' && value.increment !== undefined) { | 
					
						
							|  |  |  |             this.updateFields[field] = { increment: value.increment }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         } else { | 
					
						
							|  |  |  |             this.updateFields[field] = value; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     setFields(fieldsObj) { | 
					
						
							|  |  |  |         Object.entries(fieldsObj).forEach(([k, v]) => this.set(k, v)); | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     build() { | 
					
						
							|  |  |  |         const updateClauses = Object.keys(this.updateFields).map(field => { | 
					
						
							|  |  |  |             const val = this.updateFields[field]; | 
					
						
							|  |  |  |             if (typeof val === 'object' && val.increment !== undefined) { | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |                 return `${field} = ${field} + ?`; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return `${field} = ?`; | 
					
						
							|  |  |  |         }).join(', '); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         let sql = `UPDATE ${this.table} SET ${updateClauses}`; | 
					
						
							|  |  |  |         const { sql: whereSql, params: whereParams } = this.buildConditions(); | 
					
						
							|  |  |  |         if (whereSql) sql += ` WHERE ${whereSql}`; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const params = [ | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |             ...Object.values(this.updateFields).map(v => | 
					
						
							|  |  |  |                 (typeof v === 'object' && v.increment !== undefined) ? v.increment : v | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |             ...whereParams | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         ]; | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         return { sql, params }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  | // ------------------- INSERT -------------------
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | class InsertBuilder extends QueryBuilder { | 
					
						
							|  |  |  |     constructor() { | 
					
						
							|  |  |  |         super(); | 
					
						
							|  |  |  |         this.table = ''; | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         this.records = []; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         this.updateValues = {}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     into(table) { | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         this.table = table; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     values(records) { | 
					
						
							|  |  |  |         if (!Array.isArray(records)) records = [records]; | 
					
						
							|  |  |  |         this.records = records; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     upsert(records, updateFields) { | 
					
						
							|  |  |  |         this.values(records); | 
					
						
							|  |  |  |         this.updateValues = updateFields; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     build() { | 
					
						
							|  |  |  |         if (!this.records.length) throw new Error('No values to insert'); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         const columns = Object.keys(this.records[0]); | 
					
						
							|  |  |  |         const placeholders = `(${columns.map(() => '?').join(', ')})`; | 
					
						
							|  |  |  |         const valuesClause = this.records.map(() => placeholders).join(', '); | 
					
						
							|  |  |  |         const params = this.records.flatMap(r => columns.map(c => r[c])); | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         let sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES ${valuesClause}`; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         if (Object.keys(this.updateValues).length) { | 
					
						
							|  |  |  |             const updates = Object.keys(this.updateValues) | 
					
						
							|  |  |  |                 .map(f => `${f} = VALUES(${f})`) // MySQL 8.0+ 兼容写法
 | 
					
						
							|  |  |  |                 .join(', '); | 
					
						
							|  |  |  |             sql += ` ON DUPLICATE KEY UPDATE ${updates}`; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |         return { sql, params }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  | // ------------------- DELETE -------------------
 | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | class DeleteBuilder extends QueryBuilder { | 
					
						
							|  |  |  |     constructor() { | 
					
						
							|  |  |  |         super(); | 
					
						
							|  |  |  |         this.table = ''; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     from(table) { | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |         this.table = table; | 
					
						
							|  |  |  |         return this; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     build() { | 
					
						
							|  |  |  |         let sql = `DELETE FROM ${this.table}`; | 
					
						
							|  |  |  |         const { sql: whereSql, params } = this.buildConditions(); | 
					
						
							|  |  |  |         if (!whereSql) throw new Error('DELETE without WHERE is not allowed!'); | 
					
						
							|  |  |  |         sql += ` WHERE ${whereSql}`; | 
					
						
							|  |  |  |         return { sql, params }; | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = { | 
					
						
							|  |  |  |     SelectBuilder, | 
					
						
							|  |  |  |     UpdateBuilder, | 
					
						
							|  |  |  |     InsertBuilder, | 
					
						
							| 
									
										
										
										
											2025-09-26 14:38:44 +08:00
										 |  |  |     DeleteBuilder | 
					
						
							| 
									
										
										
										
											2025-09-24 10:02:03 +08:00
										 |  |  | }; |