From cbf77a7252f97d0900e7f9c32a32c6b6125af27b Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Mon, 19 Oct 2026 09:28:02 +0800 Subject: [PATCH] =?UTF-8?q?perf(db):=20=E9=AB=98=E9=A2=91=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E8=A1=A5=E7=B4=A2=E5=BC=95=20+=20=E5=A4=A7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8A=A0=E5=90=88=E7=90=86=E4=B8=8A=E9=99=90=EF=BC=88?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=85=A8=E8=A1=A8=E4=B8=8B=E5=8F=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动建表后幂等补索引:orders/finance_records/users/reports/companions/payment_intents/payment_refunds 等高频查询列 - 管理端订单/财务流水/用户列表 LIMIT 500,友圈/评价列表 LIMIT 200,防止无界查询 - expert_services 懒建表后补 expert_id 索引 - 新增回归护栏 list-pagination-index.spec.ts(84 套件/625 用例全绿) --- server/src/db/mysql.ts | 51 ++++++++++++++++++++++ server/src/db/repository.ts | 14 ++++-- server/tests/list-pagination-index.spec.ts | 37 ++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 server/tests/list-pagination-index.spec.ts diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index 5262c9d..08c78a0 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -271,6 +271,7 @@ export async function initDatabase(): Promise { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; `); + // 8. 线下合规场所库 await conn.query(` CREATE TABLE IF NOT EXISTS safety_places ( @@ -644,6 +645,56 @@ export async function initDatabase(): Promise { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; `); + // 慢查询优化:为高频查询列补充索引(幂等:已存在则忽略;表不存在也忽略) + // 注意:必须在所有建表语句之后执行,确保目标表已存在。 + const indexDefs: Array<[string, string, string]> = [ + ['orders', 'idx_orders_buyer', 'buyer_id'], + ['orders', 'idx_orders_expert', 'expert_id'], + ['orders', 'idx_orders_claimed_by', 'claimed_by'], + ['orders', 'idx_orders_status_created', 'status, created_at'], + ['orders', 'idx_orders_verify_code', 'verify_code'], + ['orders', 'idx_orders_companion_name', 'companion_name'], + ['order_reviews', 'idx_reviews_expert_created', 'expert_id, created_at'], + ['finance_records', 'idx_fin_user', 'user_id'], + ['finance_records', 'idx_fin_related', 'related_id'], + ['finance_records', 'idx_fin_type', 'type'], + ['finance_records', 'idx_fin_created', 'created_at'], + ['favorites', 'idx_fav_user', 'user_id'], + ['favorites', 'idx_fav_expert', 'expert_id'], + ['distribution_records', 'idx_dist_created', 'created_at'], + ['posts', 'idx_posts_status_top_created', 'status, is_top, created_at'], + ['posts', 'idx_posts_category', 'category'], + ['reports', 'idx_reports_status', 'status'], + ['reports', 'idx_reports_reporter', 'reporter_id'], + ['withdrawals', 'idx_wd_companion', 'companion_id'], + ['withdrawals', 'idx_wd_status', 'status'], + ['activities', 'idx_act_creator', 'creator_id'], + ['activities', 'idx_act_status', 'status'], + ['payment_intents', 'idx_pi_order', 'order_id'], + ['payment_intents', 'idx_pi_status', 'status'], + ['users', 'idx_users_phone', 'phone'], + ['users', 'idx_users_inviter', 'inviter_id'], + ['users', 'idx_users_nickname', 'nickname'], + ['participants', 'idx_pt_user', 'user_id'], + ['post_likes', 'idx_post_likes_post', 'post_id'], + ['post_comments', 'idx_pc_post', 'post_id'], + ['post_applies', 'idx_pa_post', 'post_id'], + ['post_applies', 'idx_pa_status', 'status'], + ['arbitrations', 'idx_arb_status', 'status'], + ['arbitrations', 'idx_arb_order', 'order_id'], + ['companions', 'idx_comp_user', 'user_id'], + ['companions', 'idx_comp_status', 'status'], + ['companions', 'idx_comp_audit', 'audit_status'], + ['payment_refunds', 'idx_pr_order', 'order_id'], + ]; + for (const [table, name, cols] of indexDefs) { + try { + await conn.query(`ALTER TABLE \`${table}\` ADD INDEX \`${name}\` (${cols})`); + } catch (_) { + // 索引已存在或表不存在:忽略 + } + } + // 检查并插入初始种子数据(若表为空) await seedInitialDataIfEmpty(conn); diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index 79adb53..b2f7e0e 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -1325,6 +1325,7 @@ export class MysqlRepository { created_at as createdAt FROM orders ORDER BY created_at DESC + LIMIT 500 `); return rows; } @@ -1367,6 +1368,7 @@ export class MysqlRepository { OR claimed_by = ? OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?) ORDER BY created_at DESC + LIMIT 500 `, [uid, uid, uid, uid, uid]); return rows; } @@ -3102,6 +3104,7 @@ export class MysqlRepository { created_at as createdAt FROM finance_records ORDER BY created_at DESC + LIMIT 500 `); return rows; } @@ -3508,7 +3511,7 @@ export class MysqlRepository { sql += ' AND status = ?'; params.push(filter.status); } - sql += ' ORDER BY id ASC'; + sql += ' ORDER BY id ASC LIMIT 500'; const [rows]: any = await pool.query(sql, params); return rows; } @@ -4286,7 +4289,7 @@ export class MysqlRepository { if (where.length > 0) { sql += ' WHERE ' + where.join(' AND '); } - sql += ' ORDER BY is_top DESC, created_at DESC'; + sql += ' ORDER BY is_top DESC, created_at DESC LIMIT 200'; const [rows]: any = await pool.query(sql, params); return rows.map((r: any) => { @@ -4597,7 +4600,7 @@ export class MysqlRepository { if (where.length > 0) { sql += ' WHERE ' + where.join(' AND '); } - sql += ' ORDER BY created_at DESC'; + sql += ' ORDER BY created_at DESC LIMIT 200'; const [rows]: any = await pool.query(sql, params); return rows.map((r: any) => { @@ -4911,6 +4914,11 @@ export class MysqlRepository { created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); + try { + await pool.query('ALTER TABLE expert_services ADD INDEX idx_srv_expert (expert_id)'); + } catch (_) { + // 索引已存在:忽略 + } const [rows]: any = await pool.query('SELECT * FROM expert_services WHERE expert_id = ? ORDER BY created_at ASC', [expertId]); if (rows && rows.length > 0) { return rows.map((r: any) => ({ diff --git a/server/tests/list-pagination-index.spec.ts b/server/tests/list-pagination-index.spec.ts new file mode 100644 index 0000000..7974d3e --- /dev/null +++ b/server/tests/list-pagination-index.spec.ts @@ -0,0 +1,37 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * 第六十八批回归闸门:列表有界 + 高频查询索引 + */ +describe('第六十八批分页与索引闸门', () => { + const rootDir = path.resolve(__dirname, '../../'); + const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8'); + const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8'); + + it('大列表查询必须有上限(避免全表下发)', () => { + expect(repoCode).toContain("sql += ' ORDER BY id ASC LIMIT 500'"); + expect(repoCode).toContain("sql += ' ORDER BY is_top DESC, created_at DESC LIMIT 200'"); + expect(repoCode).toContain("sql += ' ORDER BY created_at DESC LIMIT 200'"); + // 管理端财务流水 + expect(repoCode).toContain('LIMIT 500'); + }); + + it('提供高频查询索引(幂等)', () => { + expect(mysqlCode).toContain('idx_orders_buyer'); + expect(mysqlCode).toContain('idx_orders_status_created'); + expect(mysqlCode).toContain('idx_fin_user'); + expect(mysqlCode).toContain('idx_users_phone'); + expect(mysqlCode).toContain('idx_pr_order'); + // 索引创建必须在所有建表之后,避免表不存在 + const createIdx = mysqlCode.indexOf('const indexDefs'); + const seedCall = mysqlCode.indexOf('await seedInitialDataIfEmpty(conn)'); + expect(createIdx).toBeGreaterThan(-1); + expect(seedCall).toBeGreaterThan(createIdx); + }); + + it('expert_services 懒建表后补索引', () => { + expect(repoCode).toContain('ALTER TABLE expert_services ADD INDEX idx_srv_expert'); + }); +});