Files
users/services/IdentityService.ts

119 lines
3.9 KiB
TypeScript
Raw Normal View History

// 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)
}
}