feat: enhance user profile with detailed tags and asset evaluation
Optimize user detail page for asset assessment and tag info. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,27 +1,31 @@
|
||||
// 身份识别与合并服务
|
||||
// 实现基于数据字典标识的关键字段的匹配逻辑
|
||||
|
||||
import { getIdentityKeyFields } from "@/lib/data-dictionary"
|
||||
// services/IdentityService.ts
|
||||
// 统一身份识别服务(内存索引),用于 /api/ingest 流程中匹配或创建用户。
|
||||
// 与 v1.4 文档的统一用户结构保持一致(source_profiles / ai_insights / crm_info)[^5]
|
||||
|
||||
export interface IdentityMatch {
|
||||
userId: string
|
||||
confidence: number
|
||||
matchedFields: string[]
|
||||
matchType: "exact" | "fuzzy" | "partial"
|
||||
reasons: string[]
|
||||
}
|
||||
|
||||
export interface UserIdentity {
|
||||
userId: string
|
||||
identityFields: Record<string, any>
|
||||
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 userIdentities: Map<string, UserIdentity> = new Map()
|
||||
private index: IndexMaps
|
||||
|
||||
private constructor() {
|
||||
// 初始化一些模拟数据
|
||||
this.initializeMockData()
|
||||
this.index = {
|
||||
emailToUser: new Map(),
|
||||
phoneToUser: new Map(),
|
||||
usernameToUser: new Map(),
|
||||
userStore: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): IdentityService {
|
||||
@@ -31,202 +35,84 @@ export class IdentityService {
|
||||
return IdentityService.instance
|
||||
}
|
||||
|
||||
private initializeMockData() {
|
||||
// 模拟已存在的用户身份数据
|
||||
const mockUsers = [
|
||||
{
|
||||
userId: "user_global_id_1",
|
||||
identityFields: {
|
||||
phone: "13800138000",
|
||||
email: "zhangsan@example.com",
|
||||
username: "zhangsan",
|
||||
fullName: "张三",
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: "user_global_id_2",
|
||||
identityFields: {
|
||||
phone: "13912345678",
|
||||
email: "lisi@example.com",
|
||||
username: "lisi",
|
||||
fullName: "李四",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
mockUsers.forEach((user) => {
|
||||
this.userIdentities.set(user.userId, user)
|
||||
})
|
||||
}
|
||||
|
||||
// 根据输入数据查找匹配的用户身份
|
||||
public async findMatchingIdentity(inputData: Record<string, any>): Promise<IdentityMatch[]> {
|
||||
const identityKeyFields = getIdentityKeyFields()
|
||||
// 基于映射数据寻找可能的用户匹配
|
||||
public async findMatchingIdentity(mappedData: Record<string, any>): Promise<IdentityMatch[]> {
|
||||
const matches: IdentityMatch[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (const [userId, userIdentity] of this.userIdentities) {
|
||||
const matchResult = this.calculateMatch(inputData, userIdentity.identityFields, identityKeyFields)
|
||||
if (matchResult.confidence > 0) {
|
||||
matches.push({
|
||||
userId,
|
||||
...matchResult,
|
||||
})
|
||||
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 matches.sort((a, b) => b.confidence - a.confidence)
|
||||
return Array.from(aggregated.values()).sort((a, b) => b.confidence - a.confidence)
|
||||
}
|
||||
|
||||
// 计算匹配度
|
||||
private calculateMatch(
|
||||
inputData: Record<string, any>,
|
||||
existingData: Record<string, any>,
|
||||
keyFields: string[],
|
||||
): { confidence: number; matchedFields: string[]; matchType: "exact" | "fuzzy" | "partial" } {
|
||||
const matchedFields: string[] = []
|
||||
let exactMatches = 0
|
||||
let fuzzyMatches = 0
|
||||
let totalFields = 0
|
||||
|
||||
for (const field of keyFields) {
|
||||
if (inputData[field] && existingData[field]) {
|
||||
totalFields++
|
||||
|
||||
if (this.isExactMatch(inputData[field], existingData[field])) {
|
||||
exactMatches++
|
||||
matchedFields.push(field)
|
||||
} else if (this.isFuzzyMatch(inputData[field], existingData[field])) {
|
||||
fuzzyMatches++
|
||||
matchedFields.push(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFields === 0) {
|
||||
return { confidence: 0, matchedFields: [], matchType: "partial" }
|
||||
}
|
||||
|
||||
const confidence = (exactMatches * 1.0 + fuzzyMatches * 0.7) / totalFields
|
||||
|
||||
let matchType: "exact" | "fuzzy" | "partial" = "partial"
|
||||
if (exactMatches > 0 && fuzzyMatches === 0) {
|
||||
matchType = "exact"
|
||||
} else if (exactMatches > 0 || fuzzyMatches > 0) {
|
||||
matchType = "fuzzy"
|
||||
}
|
||||
|
||||
return { confidence, matchedFields, matchType }
|
||||
}
|
||||
|
||||
// 精确匹配
|
||||
private isExactMatch(value1: any, value2: any): boolean {
|
||||
if (typeof value1 === "string" && typeof value2 === "string") {
|
||||
return value1.toLowerCase().trim() === value2.toLowerCase().trim()
|
||||
}
|
||||
return value1 === value2
|
||||
}
|
||||
|
||||
// 模糊匹配
|
||||
private isFuzzyMatch(value1: any, value2: any): boolean {
|
||||
if (typeof value1 === "string" && typeof value2 === "string") {
|
||||
const str1 = value1.toLowerCase().trim()
|
||||
const str2 = value2.toLowerCase().trim()
|
||||
|
||||
// 简单的相似度计算
|
||||
const similarity = this.calculateStringSimilarity(str1, str2)
|
||||
return similarity > 0.8
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 计算字符串相似度
|
||||
private calculateStringSimilarity(str1: string, str2: string): number {
|
||||
const longer = str1.length > str2.length ? str1 : str2
|
||||
const shorter = str1.length > str2.length ? str2 : str1
|
||||
|
||||
if (longer.length === 0) {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
const editDistance = this.levenshteinDistance(longer, shorter)
|
||||
return (longer.length - editDistance) / longer.length
|
||||
}
|
||||
|
||||
// 计算编辑距离
|
||||
private levenshteinDistance(str1: string, str2: string): number {
|
||||
const matrix = []
|
||||
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
matrix[i] = [i]
|
||||
}
|
||||
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
matrix[0][j] = j
|
||||
}
|
||||
|
||||
for (let i = 1; i <= str2.length; i++) {
|
||||
for (let j = 1; j <= str1.length; j++) {
|
||||
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
||||
matrix[i][j] = matrix[i - 1][j - 1]
|
||||
} else {
|
||||
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[str2.length][str1.length]
|
||||
}
|
||||
|
||||
// 创建新的用户身份
|
||||
public async createNewIdentity(inputData: Record<string, any>): Promise<string> {
|
||||
const userId = `user_global_id_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const identityKeyFields = getIdentityKeyFields()
|
||||
|
||||
const identityFields: Record<string, any> = {}
|
||||
identityKeyFields.forEach((field) => {
|
||||
if (inputData[field]) {
|
||||
identityFields[field] = inputData[field]
|
||||
}
|
||||
})
|
||||
|
||||
this.userIdentities.set(userId, {
|
||||
userId,
|
||||
identityFields,
|
||||
})
|
||||
// 创建新身份
|
||||
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, updateData: Record<string, any>): Promise<void> {
|
||||
const existingIdentity = this.userIdentities.get(userId)
|
||||
if (existingIdentity) {
|
||||
const identityKeyFields = getIdentityKeyFields()
|
||||
identityKeyFields.forEach((field) => {
|
||||
if (updateData[field]) {
|
||||
existingIdentity.identityFields[field] = updateData[field]
|
||||
}
|
||||
})
|
||||
this.userIdentities.set(userId, existingIdentity)
|
||||
// 更新身份(同步索引)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并用户身份
|
||||
public async mergeIdentities(primaryUserId: string, secondaryUserId: string): Promise<void> {
|
||||
const primaryIdentity = this.userIdentities.get(primaryUserId)
|
||||
const secondaryIdentity = this.userIdentities.get(secondaryUserId)
|
||||
|
||||
if (primaryIdentity && secondaryIdentity) {
|
||||
// 合并身份字段,优先保留主身份的数据
|
||||
Object.keys(secondaryIdentity.identityFields).forEach((field) => {
|
||||
if (!primaryIdentity.identityFields[field] && secondaryIdentity.identityFields[field]) {
|
||||
primaryIdentity.identityFields[field] = secondaryIdentity.identityFields[field]
|
||||
}
|
||||
})
|
||||
|
||||
this.userIdentities.set(primaryUserId, primaryIdentity)
|
||||
this.userIdentities.delete(secondaryUserId)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user