Optimize user detail page for asset assessment and tag info. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
// services/IdentityService.ts
|
||
// 统一身份识别服务(内存索引),用于 /api/ingest 流程中匹配或创建用户。
|
||
// 与 v1.4 文档的统一用户结构保持一致(source_profiles / ai_insights / crm_info)[^5]
|
||
|
||
export interface IdentityMatch {
|
||
userId: string
|
||
confidence: number
|
||
reasons: string[]
|
||
}
|
||
|
||
type IndexMaps = {
|
||
emailToUser: Map<string, string>
|
||
phoneToUser: Map<string, string>
|
||
usernameToUser: Map<string, string>
|
||
userStore: Map<string, Record<string, any>>
|
||
}
|
||
|
||
export class IdentityService {
|
||
private static instance: IdentityService
|
||
private index: IndexMaps
|
||
|
||
private constructor() {
|
||
this.index = {
|
||
emailToUser: new Map(),
|
||
phoneToUser: new Map(),
|
||
usernameToUser: new Map(),
|
||
userStore: new Map(),
|
||
}
|
||
}
|
||
|
||
public static getInstance(): IdentityService {
|
||
if (!IdentityService.instance) {
|
||
IdentityService.instance = new IdentityService()
|
||
}
|
||
return IdentityService.instance
|
||
}
|
||
|
||
// 基于映射数据寻找可能的用户匹配
|
||
public async findMatchingIdentity(mappedData: Record<string, any>): Promise<IdentityMatch[]> {
|
||
const matches: IdentityMatch[] = []
|
||
const seen = new Set<string>()
|
||
|
||
const tryAdd = (userId: string | undefined, confidence: number, reason: string) => {
|
||
if (!userId) return
|
||
if (seen.has(userId)) return
|
||
seen.add(userId)
|
||
matches.push({ userId, confidence, reasons: [reason] })
|
||
}
|
||
|
||
if (mappedData.email) {
|
||
tryAdd(this.index.emailToUser.get(String(mappedData.email).toLowerCase()), 0.95, "email")
|
||
}
|
||
if (mappedData.phone) {
|
||
tryAdd(this.index.phoneToUser.get(String(mappedData.phone)), 0.92, "phone")
|
||
}
|
||
if (mappedData.username) {
|
||
tryAdd(this.index.usernameToUser.get(String(mappedData.username).toLowerCase()), 0.7, "username")
|
||
}
|
||
|
||
// 简单加权:若同一userId命中多个关键字段,提升置信度
|
||
const aggregated = new Map<string, IdentityMatch>()
|
||
for (const m of matches) {
|
||
const exists = aggregated.get(m.userId)
|
||
if (!exists) {
|
||
aggregated.set(m.userId, { ...m })
|
||
} else {
|
||
exists.confidence = Math.min(0.99, exists.confidence + 0.05)
|
||
exists.reasons = Array.from(new Set([...exists.reasons, ...m.reasons]))
|
||
}
|
||
}
|
||
|
||
return Array.from(aggregated.values()).sort((a, b) => b.confidence - a.confidence)
|
||
}
|
||
|
||
// 创建新身份
|
||
public async createNewIdentity(mappedData: Record<string, any>): Promise<string> {
|
||
const base =
|
||
(mappedData.phone && `phone_${mappedData.phone}`) ||
|
||
(mappedData.email && `email_${String(mappedData.email).toLowerCase()}`) ||
|
||
(mappedData.username && `uname_${String(mappedData.username).toLowerCase()}`) ||
|
||
`anon_${Date.now()}`
|
||
|
||
const userId = `user_${this.shortHash(base)}`
|
||
this.index.userStore.set(userId, { userId, ...mappedData })
|
||
this.bindIndexes(userId, mappedData)
|
||
return userId
|
||
}
|
||
|
||
// 更新身份(同步索引)
|
||
public async updateIdentity(userId: string, mappedData: Record<string, any>): Promise<void> {
|
||
const cur = this.index.userStore.get(userId) || { userId }
|
||
const updated = { ...cur, ...mappedData }
|
||
this.index.userStore.set(userId, updated)
|
||
this.bindIndexes(userId, mappedData)
|
||
}
|
||
|
||
private bindIndexes(userId: string, mappedData: Record<string, any>) {
|
||
if (mappedData.email) {
|
||
this.index.emailToUser.set(String(mappedData.email).toLowerCase(), userId)
|
||
}
|
||
if (mappedData.phone) {
|
||
this.index.phoneToUser.set(String(mappedData.phone), userId)
|
||
}
|
||
if (mappedData.username) {
|
||
this.index.usernameToUser.set(String(mappedData.username).toLowerCase(), userId)
|
||
}
|
||
}
|
||
|
||
private shortHash(str: string): string {
|
||
// 简单、稳定的字符串哈希(djb2 变体),避免依赖
|
||
let h = 5381
|
||
for (let i = 0; i < str.length; i++) {
|
||
h = (h * 33) ^ str.charCodeAt(i)
|
||
}
|
||
// 转为正数并截断
|
||
return (h >>> 0).toString(36)
|
||
}
|
||
}
|