2025-08-21 05:32:37 +00:00
|
|
|
|
// services/IdentityService.ts
|
|
|
|
|
|
// 统一身份识别服务(内存索引),用于 /api/ingest 流程中匹配或创建用户。
|
|
|
|
|
|
// 与 v1.4 文档的统一用户结构保持一致(source_profiles / ai_insights / crm_info)[^5]
|
2025-07-25 06:42:34 +00:00
|
|
|
|
|
|
|
|
|
|
export interface IdentityMatch {
|
|
|
|
|
|
userId: string
|
|
|
|
|
|
confidence: number
|
2025-08-21 05:32:37 +00:00
|
|
|
|
reasons: string[]
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
type IndexMaps = {
|
|
|
|
|
|
emailToUser: Map<string, string>
|
|
|
|
|
|
phoneToUser: Map<string, string>
|
|
|
|
|
|
usernameToUser: Map<string, string>
|
|
|
|
|
|
userStore: Map<string, Record<string, any>>
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class IdentityService {
|
|
|
|
|
|
private static instance: IdentityService
|
2025-08-21 05:32:37 +00:00
|
|
|
|
private index: IndexMaps
|
2025-07-25 06:42:34 +00:00
|
|
|
|
|
|
|
|
|
|
private constructor() {
|
2025-08-21 05:32:37 +00:00
|
|
|
|
this.index = {
|
|
|
|
|
|
emailToUser: new Map(),
|
|
|
|
|
|
phoneToUser: new Map(),
|
|
|
|
|
|
usernameToUser: new Map(),
|
|
|
|
|
|
userStore: new Map(),
|
|
|
|
|
|
}
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static getInstance(): IdentityService {
|
|
|
|
|
|
if (!IdentityService.instance) {
|
|
|
|
|
|
IdentityService.instance = new IdentityService()
|
|
|
|
|
|
}
|
|
|
|
|
|
return IdentityService.instance
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
// 基于映射数据寻找可能的用户匹配
|
|
|
|
|
|
public async findMatchingIdentity(mappedData: Record<string, any>): Promise<IdentityMatch[]> {
|
2025-07-25 06:42:34 +00:00
|
|
|
|
const matches: IdentityMatch[] = []
|
2025-08-21 05:32:37 +00:00
|
|
|
|
const seen = new Set<string>()
|
2025-07-25 06:42:34 +00:00
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
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] })
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
if (mappedData.email) {
|
|
|
|
|
|
tryAdd(this.index.emailToUser.get(String(mappedData.email).toLowerCase()), 0.95, "email")
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
2025-08-21 05:32:37 +00:00
|
|
|
|
if (mappedData.phone) {
|
|
|
|
|
|
tryAdd(this.index.phoneToUser.get(String(mappedData.phone)), 0.92, "phone")
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
2025-08-21 05:32:37 +00:00
|
|
|
|
if (mappedData.username) {
|
|
|
|
|
|
tryAdd(this.index.usernameToUser.get(String(mappedData.username).toLowerCase()), 0.7, "username")
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
// 简单加权:若同一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]))
|
|
|
|
|
|
}
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
return Array.from(aggregated.values()).sort((a, b) => b.confidence - a.confidence)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
// 创建新身份
|
|
|
|
|
|
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
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
// 更新身份(同步索引)
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-07-25 06:42:34 +00:00
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
private bindIndexes(userId: string, mappedData: Record<string, any>) {
|
|
|
|
|
|
if (mappedData.email) {
|
|
|
|
|
|
this.index.emailToUser.set(String(mappedData.email).toLowerCase(), userId)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
2025-08-21 05:32:37 +00:00
|
|
|
|
if (mappedData.phone) {
|
|
|
|
|
|
this.index.phoneToUser.set(String(mappedData.phone), userId)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
2025-08-21 05:32:37 +00:00
|
|
|
|
if (mappedData.username) {
|
|
|
|
|
|
this.index.usernameToUser.set(String(mappedData.username).toLowerCase(), userId)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 05:32:37 +00:00
|
|
|
|
private shortHash(str: string): string {
|
|
|
|
|
|
// 简单、稳定的字符串哈希(djb2 变体),避免依赖
|
|
|
|
|
|
let h = 5381
|
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
|
h = (h * 33) ^ str.charCodeAt(i)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
2025-08-21 05:32:37 +00:00
|
|
|
|
// 转为正数并截断
|
|
|
|
|
|
return (h >>> 0).toString(36)
|
2025-07-25 06:42:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|