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 打包无错误
这个提交包含在:
@@ -18,6 +18,18 @@ export class DeepSeekService extends BaseService {
|
||||
private config: DeepSeekConfig;
|
||||
private httpClient: AxiosInstance;
|
||||
|
||||
/** 多租户私有 BYOK 配置映射表 */
|
||||
private tenantConfigs: Map<string | number, DeepSeekConfig> = new Map();
|
||||
|
||||
/** 租户 Token 累计消耗账本 */
|
||||
private tenantUsage: Map<
|
||||
string | number,
|
||||
{ totalTokens: number; promptTokens: number; completionTokens: number; callCount: number }
|
||||
> = new Map();
|
||||
|
||||
/** 租户 Token 配额上限表 */
|
||||
private tenantQuotas: Map<string | number, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const envKey = process.env.DEEPSEEK_API_KEY?.trim() || undefined;
|
||||
@@ -46,6 +58,113 @@ export class DeepSeekService extends BaseService {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定租户是否已配置专属私有 Key
|
||||
*/
|
||||
hasTenantConfig(tenantId: string | number): boolean {
|
||||
return this.tenantConfigs.has(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定租户有效配置(未单独配置则优雅回退至平台公共默认配置)
|
||||
*/
|
||||
getTenantConfig(tenantId?: string | number): DeepSeekConfig {
|
||||
if (tenantId !== undefined && tenantId !== null && this.tenantConfigs.has(tenantId)) {
|
||||
return { ...this.tenantConfigs.get(tenantId)! };
|
||||
}
|
||||
return this.getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定企业租户更新专属定制配置 (BYOK)
|
||||
*/
|
||||
updateTenantConfig(tenantId: string | number, partial: Partial<DeepSeekConfig>): void {
|
||||
const current = this.getTenantConfig(tenantId);
|
||||
const updated: DeepSeekConfig = {
|
||||
...current,
|
||||
...partial,
|
||||
enabled: partial.apiKey ? Boolean(partial.apiKey.trim()) : current.enabled,
|
||||
};
|
||||
this.tenantConfigs.set(tenantId, updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录指定租户的 Token 消耗
|
||||
*/
|
||||
recordTokenUsage(
|
||||
tenantId: string | number,
|
||||
tokens:
|
||||
| number
|
||||
| { promptTokens?: number; completionTokens?: number; totalTokens?: number }
|
||||
): void {
|
||||
const current = this.tenantUsage.get(tenantId) || {
|
||||
totalTokens: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
callCount: 0,
|
||||
};
|
||||
|
||||
let prompt = 0;
|
||||
let completion = 0;
|
||||
let total = 0;
|
||||
|
||||
if (typeof tokens === 'number') {
|
||||
total = Number(tokens) || 0;
|
||||
} else if (tokens && typeof tokens === 'object') {
|
||||
prompt = Number(tokens.promptTokens) || 0;
|
||||
completion = Number(tokens.completionTokens) || 0;
|
||||
total = Number(tokens.totalTokens) || prompt + completion;
|
||||
}
|
||||
|
||||
this.tenantUsage.set(tenantId, {
|
||||
totalTokens: current.totalTokens + total,
|
||||
promptTokens: current.promptTokens + prompt,
|
||||
completionTokens: current.completionTokens + completion,
|
||||
callCount: current.callCount + 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定租户的 Token 用量统计
|
||||
*/
|
||||
getTenantTokenUsage(tenantId: string | number): {
|
||||
totalTokens: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
callCount: number;
|
||||
} {
|
||||
return this.tenantUsage.get(tenantId) || {
|
||||
totalTokens: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
callCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定租户的 Token 最大额度上限
|
||||
*/
|
||||
setTenantTokenQuota(tenantId: string | number, maxTotalTokens: number): void {
|
||||
this.tenantQuotas.set(tenantId, Math.max(0, Number(maxTotalTokens) || 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言当前租户 Token 配额充足,超限则抛出 429 阻断
|
||||
*/
|
||||
assertTenantTokenQuota(tenantId: string | number, estimatedTokens = 1): void {
|
||||
const quota = this.tenantQuotas.get(tenantId);
|
||||
if (quota === undefined) {
|
||||
return;
|
||||
}
|
||||
const current = this.getTenantTokenUsage(tenantId).totalTokens;
|
||||
if (current + estimatedTokens > quota) {
|
||||
throw new CoolCommException(
|
||||
`当前租户 AI Token 配额已耗尽 (${current}/${quota}),请升级套餐或补充额度`,
|
||||
429
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态更新配置
|
||||
*/
|
||||
@@ -61,10 +180,11 @@ export class DeepSeekService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已配置有效 API Key
|
||||
* 检查是否已配置有效 API Key(支持按租户检查或默认平台池检查)
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return Boolean(this.config.apiKey && this.config.apiKey.trim().length > 0);
|
||||
isConfigured(tenantId?: number | string): boolean {
|
||||
const cfg = this.getTenantConfig(tenantId);
|
||||
return Boolean(cfg.apiKey && cfg.apiKey.trim().length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,13 +268,15 @@ export class DeepSeekService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话补全调用 (支持优雅降级)
|
||||
* 对话补全调用 (支持多租户 BYOK、Token 计量与优雅降级)
|
||||
*/
|
||||
async chatCompletion(
|
||||
req: DeepSeekChatCompletionRequest,
|
||||
fallbackReply?: string
|
||||
fallbackReply?: string,
|
||||
tenantId?: number | string
|
||||
): Promise<string> {
|
||||
const apiKey = this.config.apiKey?.trim();
|
||||
const cfg = this.getTenantConfig(tenantId);
|
||||
const apiKey = cfg.apiKey?.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
if (fallbackReply !== undefined) {
|
||||
@@ -163,12 +285,18 @@ export class DeepSeekService extends BaseService {
|
||||
throw new CoolCommException('DeepSeek API Key 未配置');
|
||||
}
|
||||
|
||||
const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
|
||||
// 租户 Token 配额校验 (预估输入字符数 / 2)
|
||||
const estimatedTokens = Math.ceil(
|
||||
(req.messages || []).reduce((acc, m) => acc + (m.content || '').length, 0) / 2
|
||||
);
|
||||
this.assertTenantTokenQuota(tenantId, estimatedTokens);
|
||||
|
||||
const endpoint = `${cfg.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
|
||||
const payload = {
|
||||
model: req.model || this.config.model || 'deepseek-chat',
|
||||
model: req.model || cfg.model || 'deepseek-chat',
|
||||
messages: req.messages,
|
||||
temperature: req.temperature ?? this.config.temperature ?? 0.7,
|
||||
max_tokens: req.maxTokens ?? this.config.maxTokens ?? 2048,
|
||||
temperature: req.temperature ?? cfg.temperature ?? 0.7,
|
||||
max_tokens: req.maxTokens ?? cfg.maxTokens ?? 2048,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
@@ -184,6 +312,11 @@ export class DeepSeekService extends BaseService {
|
||||
}
|
||||
);
|
||||
|
||||
// 记录实际 Token 消耗到租户账本
|
||||
if (response.data?.usage?.total_tokens) {
|
||||
this.recordTokenUsage(tenantId, response.data.usage.total_tokens);
|
||||
}
|
||||
|
||||
const content = response.data?.choices?.[0]?.message?.content;
|
||||
if (content) {
|
||||
return content.trim();
|
||||
|
||||
在新工单中引用
屏蔽一个用户