import { Inject, Provide } from '@midwayjs/core'; import { BaseService, CoolCommException } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { CardInfoEntity } from '../entity/info'; import { CardBentoEntity } from '../entity/bento'; import { BentoBlockPublic, BentoBlockType, BentoColSpan, BentoRowSpan, CardPortalDetail, } from '../../../comm/types/omni'; /** * 本地内存缓存项 */ interface CacheItem { data: CardPortalDetail; expiresAt: number; } /** * 访客端公开微官网门户服务 * * 核心特性: * 1. 免登录无鉴权查询已发布名片 * 2. 自动聚合全部有效 Bento 组件块 (按 sortOrder 升序排列) * 3. 严格数据安全脱敏 (剔除员工 userId、后台 remark、内部配置 internalConfig 等) * 4. 高并发多级缓存加速与首屏秒开优化 (<500ms) */ @Provide() export class CardPortalService extends BaseService { @InjectEntityModel(CardInfoEntity) cardInfoEntity: Repository; @InjectEntityModel(CardBentoEntity) cardBentoEntity: Repository; @Inject() ctx: unknown; /** * 本地高速 L1 缓存 (支持秒开与高并发) */ private static localCache = new Map(); /** * 默认缓存过期时长 (毫秒,默认 60 秒) */ private readonly defaultTtlMs = 60 * 1000; /** * 获取微官网公开详情 (通过名片 ID 或自定义 Slug 短链) * * @param slugOrId 名片主键 ID 或 自定义短链标识 * @returns 脱敏后的微官网聚合资产 */ async getPortalDetail(slugOrId: string | number): Promise { if (slugOrId === undefined || slugOrId === null || slugOrId === '') { throw new CoolCommException('参数错误:缺少名片标识', 400); } const queryKey = String(slugOrId).trim(); if (!queryKey) { throw new CoolCommException('参数错误:缺少名片标识', 400); } // 1. 尝试从高速缓存读取 const cached = this.getFromCache(queryKey); if (cached) { return cached; } // 2. 判定查询维度 (ID 优先或 Slug 优先) const isNumeric = /^\d+$/.test(queryKey); let card: CardInfoEntity | null = null; if (isNumeric) { card = await this.cardInfoEntity.findOne({ where: [{ id: Number(queryKey) }, { slug: queryKey }], }); } else { card = await this.cardInfoEntity.findOne({ where: { slug: queryKey }, }); } // 3. 实体存在性与发布状态检查 if (!card) { throw new CoolCommException('名片不存在或已下线', 404); } if (card.status !== 'published') { throw new CoolCommException('名片未发布或已下线', 404); } // 4. 查询关联的有效 Bento 组件块列表 (status = 1) const rawBlocks = await this.cardBentoEntity.find({ where: { cardId: Number(card.id), status: 1, }, order: { sortOrder: 'ASC', id: 'ASC', } as any, }); // 5. 数据脱敏与公开资产结构清洗 (剔除 internalNotes 与内部 status) const bentoBlocks: BentoBlockPublic[] = (rawBlocks || []) .sort( (a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || Number(a.id) - Number(b.id) ) .map(b => ({ id: b.id, cardId: b.cardId, type: b.type as BentoBlockType, colSpan: b.colSpan as BentoColSpan, rowSpan: b.rowSpan as BentoRowSpan, sortOrder: b.sortOrder, title: b.title, content: b.content, mediaUrl: b.mediaUrl, linkUrl: b.linkUrl, metaJson: b.metaJson, })); // 6. 构造对外聚合模型 (敏感信息 userId、remark、internalConfig 严禁输出) const portalDetail: CardPortalDetail = { id: card.id, slug: card.slug, name: card.name, title: card.title, company: card.company, slogan: card.slogan, avatarUrl: card.avatarUrl, phone: card.phone, email: card.email, whatsapp: card.whatsapp, linkedin: card.linkedin, timezone: card.timezone, audioPronunciationUrl: card.audioPronunciationUrl, themeCode: card.themeCode, status: card.status, bentoBlocks, cachedAt: Date.now(), }; // 7. 写入多键关联高速缓存 this.setToCache(portalDetail); return portalDetail; } /** * 清除名片相关的微官网缓存 (供名片/Bento 编辑发布时调用) * * @param idOrSlug 名片 ID 或 Slug */ async clearPortalCache(idOrSlug: string | number): Promise { const key = String(idOrSlug).trim(); CardPortalService.localCache.delete(`id:${key}`); CardPortalService.localCache.delete(`slug:${key}`); // 同时遍历清除关联包含的缓存 for (const [cacheKey, item] of CardPortalService.localCache.entries()) { if ( String(item.data.id) === key || (item.data.slug && item.data.slug === key) ) { CardPortalService.localCache.delete(cacheKey); } } } /** * 清除所有微官网本地缓存 (供全量重置或单测环境隔离使用) */ static clearAllCache(): void { CardPortalService.localCache.clear(); } /** * 从缓存中读取数据 (包含过期检查) */ private getFromCache(key: string): CardPortalDetail | null { const now = Date.now(); const tryKeys = [`id:${key}`, `slug:${key}`]; for (const k of tryKeys) { const item = CardPortalService.localCache.get(k); if (item) { if (item.expiresAt > now) { return item.data; } // 过期自动清理 CardPortalService.localCache.delete(k); } } return null; } /** * 写入缓存,支持 ID 与 Slug 双重检索命中 */ private setToCache(detail: CardPortalDetail): void { const expiresAt = Date.now() + this.defaultTtlMs; const item: CacheItem = { data: detail, expiresAt, }; if (detail.id !== undefined && detail.id !== null) { CardPortalService.localCache.set(`id:${detail.id}`, item); } if (detail.slug) { CardPortalService.localCache.set(`slug:${detail.slug}`, item); } } }