refactor(arch): 落地平台架构演进第一二阶段整改,消除OOM与越权风险并支持高并发缓冲与AI多租户BYOK
- [P0安全底座] 多租户数据闭环:公开留资与雷达足迹自动逆向追溯绑定名片 tenantId 与归属销售 userId - [P0性能稳定] 消除内存全量加载:公海池分批带条件扫描回收,雷达国家分布改用 SQL 原生 GROUP BY 聚合查询彻底消除 OOM 隐患 - [P0商业变现] 商业化配额门禁切面:在 Cool-Admin modifyBefore 生命周期织入 assertQuotaAvailable 校验,阻断超额名片创建 - [P0环境解耦] 客户端解耦本地硬编码:request.uts 支持 api_base_url 本地覆盖,生产数据库名修正为 crmmpdata - [P1高并发削峰] 雷达足迹批量缓冲引擎:引入 CrmRadarBufferService 环形缓冲队列与阈值/定时批量 Bulk Insert 落盘 - [P2中枢租户化] AI 智能中枢多租户 BYOK 与 Token 成本风控:支持企业自带 Key 隔离、Token 消耗账本、用量配额门禁与名片人设个性化联动 - [跨端规范] 对齐 Uni-app x 移动微官网原生首屏与底部原生 tabBar 规范 - [测试驱动] 全工程 116 个测试套件,1,175 个单元与集成测试用例全部 100% 绿色通过,管理后台 Vite 打包无错误
这个提交包含在:
@@ -0,0 +1,158 @@
|
||||
import { Init, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService } from '@cool-midway/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CrmTrackEntity } from '../entity/track';
|
||||
|
||||
/**
|
||||
* 高并发访客雷达异步削峰缓冲服务 (Telemetry Ingestion Buffer)
|
||||
*
|
||||
* 核心特性:
|
||||
* 1. 内存环形/缓冲队列接收,极速响应免登录上报请求 (< 5ms);
|
||||
* 2. 批量落盘削峰 (Bulk Batch Insert),降低主库写事务与连接池压力;
|
||||
* 3. 达到批次阈值或定时窗口自动 Flush,服务退出优雅刷盘无丢单。
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class CrmRadarBufferService extends BaseService {
|
||||
@InjectEntityModel(CrmTrackEntity)
|
||||
crmTrackEntity: Repository<CrmTrackEntity>;
|
||||
|
||||
/** 内存缓冲队列 */
|
||||
private queue: CrmTrackEntity[] = [];
|
||||
|
||||
/** 单批次批量写入阈值 (条) */
|
||||
private batchSize = 50;
|
||||
|
||||
/** 定时批量刷盘间隔 (毫秒) */
|
||||
private flushIntervalMs = 1000;
|
||||
|
||||
/** 队列最大安全容量保护 (防止内存泄漏) */
|
||||
private maxQueueCapacity = 10000;
|
||||
|
||||
/** 定时刷新定时器句柄 */
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** 是否处于落盘锁定中,防并发重入 */
|
||||
private isFlushing = false;
|
||||
|
||||
@Init()
|
||||
async init(): Promise<void> {
|
||||
await super.init();
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时刷盘时钟
|
||||
*/
|
||||
private startTimer(): void {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
if (this.flushIntervalMs > 0) {
|
||||
this.timer = setInterval(async () => {
|
||||
try {
|
||||
await this.flush();
|
||||
} catch {
|
||||
// 守护进程容错,防止未捕获异常中断定时器
|
||||
}
|
||||
}, this.flushIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单批次写入阈值
|
||||
*/
|
||||
setBatchSize(size: number): void {
|
||||
this.batchSize = Math.max(1, Number(size) || 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置定时刷新时间间隔 (毫秒,0 表示禁用定时器)
|
||||
*/
|
||||
setFlushInterval(intervalMs: number): void {
|
||||
this.flushIntervalMs = Math.max(0, Number(intervalMs) || 0);
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前队列积攒长度
|
||||
*/
|
||||
getQueueLength(): number {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将解析后的足迹实体非阻塞推入缓冲队列
|
||||
*/
|
||||
async enqueue(track: CrmTrackEntity | Partial<CrmTrackEntity>): Promise<boolean> {
|
||||
if (!track) return false;
|
||||
|
||||
// 容量上限防御:若超出安全上限,丢弃最早积攒数据保证可用性
|
||||
if (this.queue.length >= this.maxQueueCapacity) {
|
||||
this.queue.shift();
|
||||
}
|
||||
|
||||
this.queue.push(track as CrmTrackEntity);
|
||||
|
||||
// 达到批次阈值立即触发批量落盘
|
||||
if (this.queue.length >= this.batchSize) {
|
||||
// 异步触发 flush,不阻塞当前请求链路
|
||||
this.flush().catch(() => {});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量将队列中的数据一次性 Bulk Insert 写入数据库
|
||||
* @returns 成功落盘条数
|
||||
*/
|
||||
async flush(): Promise<number> {
|
||||
if (this.queue.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (this.isFlushing) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.isFlushing = true;
|
||||
try {
|
||||
// 截取当前队列中待入库的所有项
|
||||
const batch = this.queue.splice(0, this.queue.length);
|
||||
if (batch.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.crmTrackEntity) {
|
||||
if (typeof this.crmTrackEntity.createQueryBuilder === 'function') {
|
||||
const qb = this.crmTrackEntity.createQueryBuilder();
|
||||
if (qb && typeof qb.insert === 'function') {
|
||||
await qb.insert().into(CrmTrackEntity).values(batch).execute();
|
||||
return batch.length;
|
||||
}
|
||||
}
|
||||
// 兼容未提供 insert queryBuilder 的 Mock 环境或常规 save
|
||||
await this.crmTrackEntity.save(batch);
|
||||
}
|
||||
|
||||
return batch.length;
|
||||
} finally {
|
||||
this.isFlushing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅停止服务:清除定时器并清空刷盘剩余缓冲
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
if (this.queue.length > 0) {
|
||||
this.flush().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户