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,209 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService, CoolCommException } from '@cool-midway/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CardEventEntity } from '../entity/event';
|
||||
import { CardEventMemberEntity } from '../entity/event-member';
|
||||
import { CardInfoEntity } from '../entity/info';
|
||||
import {
|
||||
CardContact,
|
||||
CardEvent,
|
||||
CardEventJoinResult,
|
||||
CardEventMember,
|
||||
} from '../../../comm/types/omni';
|
||||
|
||||
export interface JoinEventInput {
|
||||
userId: number;
|
||||
code: string;
|
||||
cardId?: number | null;
|
||||
}
|
||||
|
||||
export interface PageEventFloorQuery {
|
||||
userId: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
keyWord?: string;
|
||||
}
|
||||
|
||||
export function normalizeEventCode(raw: string): string {
|
||||
const code = String(raw || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (!/^[A-Z0-9]{6}$/.test(code)) {
|
||||
throw new CoolCommException('请输入 6 位活动码', 400);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function toEvent(row: CardEventEntity): CardEvent {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
status: row.status,
|
||||
};
|
||||
}
|
||||
|
||||
function toMember(row: CardEventMemberEntity): CardEventMember {
|
||||
return {
|
||||
id: row.id,
|
||||
eventId: row.eventId,
|
||||
userId: row.userId,
|
||||
cardId: row.cardId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 展会现场活动码与同场名片墙
|
||||
*/
|
||||
@Provide()
|
||||
export class CardEventService extends BaseService {
|
||||
@InjectEntityModel(CardEventEntity)
|
||||
cardEventEntity: Repository<CardEventEntity>;
|
||||
|
||||
@InjectEntityModel(CardEventMemberEntity)
|
||||
cardEventMemberEntity: Repository<CardEventMemberEntity>;
|
||||
|
||||
@InjectEntityModel(CardInfoEntity)
|
||||
cardInfoEntity: Repository<CardInfoEntity>;
|
||||
|
||||
async resolveUserCard(
|
||||
userId: number,
|
||||
explicit?: number | null
|
||||
): Promise<CardInfoEntity> {
|
||||
if (explicit != null && Number(explicit) > 0) {
|
||||
const card = await this.cardInfoEntity.findOne({
|
||||
where: { id: Number(explicit) },
|
||||
});
|
||||
if (!card || Number(card.userId) !== Number(userId)) {
|
||||
throw new CoolCommException('名片不属于当前账号', 400);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
const published = await this.cardInfoEntity.findOne({
|
||||
where: { userId: Number(userId), status: 'published' },
|
||||
});
|
||||
if (published) {
|
||||
return published;
|
||||
}
|
||||
const anyCard = await this.cardInfoEntity.findOne({
|
||||
where: { userId: Number(userId) },
|
||||
});
|
||||
if (!anyCard) {
|
||||
throw new CoolCommException('请先创建名片', 400);
|
||||
}
|
||||
return anyCard;
|
||||
}
|
||||
|
||||
async joinEvent(input: JoinEventInput): Promise<CardEventJoinResult> {
|
||||
if (!input?.userId) {
|
||||
throw new CoolCommException('请先登录', 401);
|
||||
}
|
||||
const code = normalizeEventCode(input.code);
|
||||
let event = await this.cardEventEntity.findOne({ where: { code } });
|
||||
if (!event) {
|
||||
event = new CardEventEntity();
|
||||
event.code = code;
|
||||
event.name = code;
|
||||
event.status = 'active';
|
||||
event = await this.cardEventEntity.save(event);
|
||||
}
|
||||
const card = await this.resolveUserCard(input.userId, input.cardId);
|
||||
let member = await this.cardEventMemberEntity.findOne({
|
||||
where: {
|
||||
eventId: Number(event.id),
|
||||
userId: Number(input.userId),
|
||||
},
|
||||
});
|
||||
if (!member) {
|
||||
member = new CardEventMemberEntity();
|
||||
member.eventId = Number(event.id);
|
||||
member.userId = Number(input.userId);
|
||||
}
|
||||
member.cardId = Number(card.id);
|
||||
if (card.tenantId && !member.tenantId) {
|
||||
member.tenantId = Number(card.tenantId);
|
||||
}
|
||||
member = await this.cardEventMemberEntity.save(member);
|
||||
return {
|
||||
event: toEvent(event),
|
||||
member: toMember(member),
|
||||
};
|
||||
}
|
||||
|
||||
async currentEvent(userId: number): Promise<CardEvent | null> {
|
||||
const members = await this.cardEventMemberEntity.find({
|
||||
where: { userId: Number(userId) },
|
||||
});
|
||||
if (!members.length) {
|
||||
return null;
|
||||
}
|
||||
const latest = members[members.length - 1];
|
||||
const event = await this.cardEventEntity.findOne({
|
||||
where: { id: Number(latest.eventId) },
|
||||
});
|
||||
return event ? toEvent(event) : null;
|
||||
}
|
||||
|
||||
async pageEventFloor(
|
||||
query: PageEventFloorQuery
|
||||
): Promise<{
|
||||
list: CardContact[];
|
||||
pagination: { page: number; size: number; total: number };
|
||||
}> {
|
||||
const page = query.page && query.page > 0 ? Math.floor(query.page) : 1;
|
||||
const size = query.size && query.size > 0 ? Math.floor(query.size) : 20;
|
||||
const members = await this.cardEventMemberEntity.find({
|
||||
where: { userId: Number(query.userId) },
|
||||
});
|
||||
if (!members.length) {
|
||||
return {
|
||||
list: [],
|
||||
pagination: { page, size, total: 0 },
|
||||
};
|
||||
}
|
||||
const latest = members[members.length - 1];
|
||||
const peers = await this.cardEventMemberEntity.find({
|
||||
where: { eventId: Number(latest.eventId) },
|
||||
});
|
||||
const keyword = query.keyWord
|
||||
? String(query.keyWord).trim().toLowerCase()
|
||||
: '';
|
||||
const hydrated: CardContact[] = [];
|
||||
for (const peer of peers) {
|
||||
if (Number(peer.userId) === Number(query.userId)) {
|
||||
continue;
|
||||
}
|
||||
const card = await this.cardInfoEntity.findOne({
|
||||
where: { id: Number(peer.cardId) },
|
||||
});
|
||||
if (!card || card.status !== 'published') {
|
||||
continue;
|
||||
}
|
||||
if (keyword) {
|
||||
const hay = `${card.name || ''} ${card.title || ''} ${
|
||||
card.company || ''
|
||||
}`.toLowerCase();
|
||||
if (!hay.includes(keyword)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
hydrated.push({
|
||||
ownerUserId: Number(query.userId),
|
||||
peerCardId: Number(card.id),
|
||||
source: 'event',
|
||||
peerName: card.name,
|
||||
peerTitle: card.title,
|
||||
peerCompany: card.company,
|
||||
peerAvatarUrl: card.avatarUrl,
|
||||
peerSlug: card.slug,
|
||||
});
|
||||
}
|
||||
const total = hydrated.length;
|
||||
const start = (page - 1) * size;
|
||||
return {
|
||||
list: hydrated.slice(start, start + size),
|
||||
pagination: { page, size, total },
|
||||
};
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户