perf(db): 高频查询补索引 + 大列表加合理上限(避免全表下发)
- 启动建表后幂等补索引: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 用例全绿)
这个提交包含在:
@@ -271,6 +271,7 @@ export async function initDatabase(): Promise<void> {
|
||||
) 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<void> {
|
||||
) 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);
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户