perf: 全栈性能排查与优化落地 (消灭运行时DDL/消除全表扫描/内存泄漏治理/高频静态缓存)
这个提交包含在:
@@ -539,6 +539,21 @@ function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
|
||||
const geocodeCache = new Map<string, { geo: { lat: number; lng: number } | null; at: number }>();
|
||||
|
||||
// 周期性淘汰防爆破记录与地理编码缓存,杜绝常驻进程内存泄漏
|
||||
const securitySweepTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [k, v] of loginFailureMap) {
|
||||
if (now - v.firstAt > LOGIN_WINDOW_MS) loginFailureMap.delete(k);
|
||||
}
|
||||
if (loginFailureMap.size > 20000) loginFailureMap.clear();
|
||||
|
||||
for (const [k, v] of geocodeCache) {
|
||||
if (now - v.at > 10 * 60_000) geocodeCache.delete(k);
|
||||
}
|
||||
if (geocodeCache.size > 5000) geocodeCache.clear();
|
||||
}, 5 * 60 * 1000);
|
||||
if (typeof (securitySweepTimer as any).unref === 'function') (securitySweepTimer as any).unref();
|
||||
|
||||
/** 地址 -> 坐标(腾讯 WebService 地理编码),带进程内缓存 */
|
||||
async function geocodeAddress(address: string): Promise<{ lat: number; lng: number } | null> {
|
||||
const text = String(address || '').trim();
|
||||
@@ -601,6 +616,7 @@ app.post('/api/admin/session/revoke-all', requireAdmin, async (req: Request, res
|
||||
status: 1,
|
||||
});
|
||||
adminTokenEpoch = next;
|
||||
invalidatePlatformConfigsCache();
|
||||
console.warn(`[Peiwan Admin][AUDIT] 后台登录态已全部吊销 version->${next} ip=${clientIpOf(req)}`);
|
||||
res.json({ code: 20000, message: '已吊销所有后台登录态,请重新登录' });
|
||||
} catch (err: any) {
|
||||
@@ -1728,10 +1744,37 @@ app.get('/api/expert/stats/:id/trends', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
|
||||
// 静态高频接口的进程内 TTL 缓存(降低 MySQL 重复读取开销)
|
||||
interface FastCacheEntry<T> {
|
||||
data: T;
|
||||
expireAt: number;
|
||||
}
|
||||
const STATIC_CACHE_TTL_MS = 60 * 1000;
|
||||
|
||||
let cachedBanners: FastCacheEntry<any[]> | null = null;
|
||||
export function invalidateBannersCache(): void {
|
||||
cachedBanners = null;
|
||||
}
|
||||
|
||||
let cachedNavCategories: FastCacheEntry<any[]> | null = null;
|
||||
export function invalidateNavCategoriesCache(): void {
|
||||
cachedNavCategories = null;
|
||||
}
|
||||
|
||||
let cachedPlatformConfigs: FastCacheEntry<{ [key: string]: any }> | null = null;
|
||||
export function invalidatePlatformConfigsCache(): void {
|
||||
cachedPlatformConfigs = null;
|
||||
}
|
||||
|
||||
// 首页运营 Banner 列表 (小程序端读取启用状态)
|
||||
app.get('/api/banners', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (cachedBanners && now < cachedBanners.expireAt) {
|
||||
return res.json({ code: 20000, data: cachedBanners.data });
|
||||
}
|
||||
const banners = await repo.getBanners();
|
||||
cachedBanners = { data: banners, expireAt: now + STATIC_CACHE_TTL_MS };
|
||||
res.json({ code: 20000, data: banners });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
@@ -1775,6 +1818,7 @@ app.post('/api/admin/banners', async (req: Request, res: Response) => {
|
||||
status: status !== undefined ? Number(status) : 1
|
||||
});
|
||||
|
||||
invalidateBannersCache();
|
||||
res.json({ code: 20000, data: banner, message: '轮播图创建成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1811,6 +1855,7 @@ app.put('/api/admin/banners/:id', async (req: Request, res: Response) => {
|
||||
return res.status(404).json({ code: 40400, message: '轮播图不存在或已被删除' });
|
||||
}
|
||||
|
||||
invalidateBannersCache();
|
||||
res.json({ code: 20000, data: updated, message: '轮播图修改成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1831,6 +1876,7 @@ app.post('/api/admin/banners/:id/status', async (req: Request, res: Response) =>
|
||||
return res.status(404).json({ code: 40400, message: '轮播图不存在' });
|
||||
}
|
||||
|
||||
invalidateBannersCache();
|
||||
res.json({ code: 20000, data: updated, message: status === 1 ? '轮播图已上架生效' : '轮播图已下线隐藏' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1846,6 +1892,7 @@ app.delete('/api/admin/banners/:id', async (req: Request, res: Response) => {
|
||||
return res.status(404).json({ code: 40400, message: '轮播图不存在或已被删除' });
|
||||
}
|
||||
|
||||
invalidateBannersCache();
|
||||
res.json({ code: 20000, data: null, message: '轮播图已删除' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1855,7 +1902,12 @@ app.delete('/api/admin/banners/:id', async (req: Request, res: Response) => {
|
||||
// 首页金刚区活动品类导航列表 (小程序端读取上架状态)
|
||||
app.get('/api/nav-categories', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (cachedNavCategories && now < cachedNavCategories.expireAt) {
|
||||
return res.json({ code: 20000, data: cachedNavCategories.data });
|
||||
}
|
||||
const list = await repo.getNavCategories();
|
||||
cachedNavCategories = { data: list, expireAt: now + STATIC_CACHE_TTL_MS };
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
@@ -1901,6 +1953,7 @@ app.post('/api/admin/nav-categories', async (req: Request, res: Response) => {
|
||||
status: status !== undefined ? Number(status) : 1
|
||||
});
|
||||
|
||||
invalidateNavCategoriesCache();
|
||||
res.json({ code: 20000, data: item, message: '品类导航创建成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1935,6 +1988,7 @@ app.put('/api/admin/nav-categories/:id', async (req: Request, res: Response) =>
|
||||
return res.status(404).json({ code: 40400, message: '品类导航不存在或已被删除' });
|
||||
}
|
||||
|
||||
invalidateNavCategoriesCache();
|
||||
res.json({ code: 20000, data: updated, message: '品类导航修改成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1955,6 +2009,7 @@ app.post('/api/admin/nav-categories/:id/status', async (req: Request, res: Respo
|
||||
return res.status(404).json({ code: 40400, message: '品类导航不存在' });
|
||||
}
|
||||
|
||||
invalidateNavCategoriesCache();
|
||||
res.json({ code: 20000, data: updated, message: status === 1 ? '品类导航已上架' : '品类导航已下线隐藏' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -1970,6 +2025,7 @@ app.delete('/api/admin/nav-categories/:id', async (req: Request, res: Response)
|
||||
return res.status(404).json({ code: 40400, message: '品类导航不存在或已被删除' });
|
||||
}
|
||||
|
||||
invalidateNavCategoriesCache();
|
||||
res.json({ code: 20000, data: null, message: '品类导航已删除' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -2206,7 +2262,12 @@ app.delete('/api/admin/coupons/:id', async (req: Request, res: Response) => {
|
||||
// 移动端:获取全部有效系统通用配置
|
||||
app.get('/api/system/configs', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (cachedPlatformConfigs && now < cachedPlatformConfigs.expireAt) {
|
||||
return res.json({ code: 20000, data: cachedPlatformConfigs.data });
|
||||
}
|
||||
const configs = await repo.getPlatformConfigs();
|
||||
cachedPlatformConfigs = { data: configs, expireAt: now + STATIC_CACHE_TTL_MS };
|
||||
res.json({ code: 20000, data: configs });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
@@ -2285,6 +2346,7 @@ app.put('/api/admin/system/configs/:key', async (req: Request, res: Response) =>
|
||||
contentSecurityService.setCustomKeywords(valueJson);
|
||||
}
|
||||
|
||||
invalidatePlatformConfigsCache();
|
||||
res.json({ code: 20000, data: updated, message: '系统配置更新成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
@@ -2620,6 +2682,7 @@ app.post('/api/admin/pay/wechat/config', async (req: Request, res: Response) =>
|
||||
valueJson: payload
|
||||
});
|
||||
|
||||
invalidatePlatformConfigsCache();
|
||||
const latest = await resolveWechatPayRuntimeConfig();
|
||||
res.json({
|
||||
code: 20000,
|
||||
|
||||
在新工单中引用
屏蔽一个用户