- [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 打包无错误
159 行
4.1 KiB
TypeScript
159 行
4.1 KiB
TypeScript
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(() => {});
|
|
}
|
|
}
|
|
}
|