- [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 打包无错误
210 行
5.8 KiB
TypeScript
210 行
5.8 KiB
TypeScript
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 },
|
|
};
|
|
}
|
|
}
|