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:
v0
2025-08-21 05:32:37 +00:00
parent 9bb0ee2758
commit afc77439bb
25 changed files with 2421 additions and 1645 deletions

View File

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