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 用例全绿)
这个提交包含在:
xcbyy
2026-10-19 09:28:02 +08:00
父节点 d665cd6976
当前提交 cbf77a7252
共有 3 个文件被更改,包括 99 次插入 和 3 次删除

查看文件

@@ -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');
});
});