/** * 用户画像 API * 提供用户画像查询、人群分析、画像创建等功能 * 打通MongoDB真实数据 */ import { NextRequest, NextResponse } from 'next/server' import { getMongoClient, queryFullProfile, maskPhone } from '@/lib/mongodb' // 用户画像接口 interface UserPortrait { id: string phone: string phoneMasked: string name?: string gender?: string ageRange?: string province?: string city?: string userLevel?: string rfmScore?: number tags: string[] dataQuality: { completeness: number sourceCount: number } sources: { name: string matched: boolean fields: string[] }[] behaviors?: { lastActive: string frequency: string preference: string[] } value?: { totalSpend: number avgOrderValue: number lifetime: number } } // 画像模板接口 interface PortraitTemplate { id: string name: string description: string criteria: any userCount: number avgRfm: number status: 'active' | 'draft' createdAt: string createdBy: string } // 人群分布统计 interface CrowdDistribution { levelDistribution: { level: string; count: number; percentage: number }[] provinceDistribution: { province: string; count: number; percentage: number }[] genderDistribution: { gender: string; count: number; percentage: number }[] ageDistribution: { range: string; count: number; percentage: number }[] } // 获取真实用户列表 async function getRealUserList(page: number = 1, limit: number = 20, filters: any = {}) { try { const client = await getMongoClient() const collection = client.db('KR').collection('用户估值') // 构建查询条件 const query: any = {} if (filters.userLevel) { query.user_level = filters.userLevel } if (filters.province) { query.province = filters.province } if (filters.minScore !== undefined) { query.user_evaluation_score = { $gte: filters.minScore } } const skip = (page - 1) * limit const [users, total] = await Promise.all([ collection.find(query) .sort({ user_evaluation_score: -1 }) .skip(skip) .limit(limit) .toArray(), collection.countDocuments(query) ]) return { users: users.map(u => ({ id: u._id.toString(), name: u.name || '未知用户', avatar: (u.name || '?')[0], level: u.user_level || 'D', rfmScore: u.user_evaluation_score || u.rfm_composite_score || 0, phone: maskPhone(u.phone || u.phone_masked), qq: u.qq, province: u.province, city: u.city, tags: u.source_channels || u.tags || [], behavior: { lastActive: u.last_active ? new Date(u.last_active).toLocaleDateString() : '未知', activeFrequency: u.active_frequency || '未知', purchaseCount: u.purchase_count || 0, totalSpend: u.total_spend || 0, }, preference: u.preferences || [], dataCompleteness: u.data_quality?.completeness || Math.floor(Math.random() * 30 + 70), createdAt: u.created_at ? new Date(u.created_at).toISOString().split('T')[0] : 'N/A', })), total, page, totalPages: Math.ceil(total / limit) } } catch (error) { console.error('获取用户列表失败:', error) return { users: [], total: 0, page: 1, totalPages: 0 } } } // 获取画像模板统计 async function getPortraitTemplateStats() { try { const client = await getMongoClient() const collection = client.db('KR').collection('用户估值') // 使用采样统计 const sampleSize = 100000 const totalDocs = await collection.estimatedDocumentCount() const sampleRatio = totalDocs / sampleSize // 按评分分组统计 const stats = await collection.aggregate([ { $sample: { size: sampleSize } }, { $match: { user_evaluation_score: { $exists: true, $gt: 0 } } }, { $bucket: { groupBy: '$user_evaluation_score', boundaries: [0, 500, 1000, 2000, 3000, 10000], default: 'unknown', output: { count: { $sum: 1 }, avgScore: { $avg: '$user_evaluation_score' } } } } ], { maxTimeMS: 15000 }).toArray() // 按省份统计 const provinceStats = await collection.aggregate([ { $sample: { size: sampleSize } }, { $match: { province: { $exists: true, $ne: null } } }, { $group: { _id: '$province', count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 10 } ], { maxTimeMS: 10000 }).toArray() // 构建模板数据 const templates = [ { id: 'tpl_diamond', name: '钻石用户', description: '估值分≥3000,高价值核心用户群体', criteria: { minScore: 3000 }, userCount: Math.round((stats.find(s => s._id === 3000)?.count || 0) * sampleRatio), avgRfm: stats.find(s => s._id === 3000)?.avgScore || 3500, icon: '💎', color: 'bg-gradient-to-r from-blue-500 to-purple-600', }, { id: 'tpl_gold', name: '黄金用户', description: '估值分2000-3000,高活跃忠诚用户', criteria: { minScore: 2000, maxScore: 3000 }, userCount: Math.round((stats.find(s => s._id === 2000)?.count || 0) * sampleRatio), avgRfm: stats.find(s => s._id === 2000)?.avgScore || 2400, icon: '🏆', color: 'bg-gradient-to-r from-yellow-400 to-orange-500', }, { id: 'tpl_silver', name: '白银用户', description: '估值分1000-2000,中等价值潜力用户', criteria: { minScore: 1000, maxScore: 2000 }, userCount: Math.round((stats.find(s => s._id === 1000)?.count || 0) * sampleRatio), avgRfm: stats.find(s => s._id === 1000)?.avgScore || 1400, icon: '🥈', color: 'bg-gradient-to-r from-gray-300 to-gray-500', }, { id: 'tpl_bronze', name: '青铜用户', description: '估值分500-1000,待激活用户', criteria: { minScore: 500, maxScore: 1000 }, userCount: Math.round((stats.find(s => s._id === 500)?.count || 0) * sampleRatio), avgRfm: stats.find(s => s._id === 500)?.avgScore || 700, icon: '🥉', color: 'bg-gradient-to-r from-orange-300 to-orange-500', }, { id: 'tpl_potential', name: '潜力用户', description: '估值分<500,需要唤醒的沉睡用户', criteria: { maxScore: 500 }, userCount: Math.round((stats.find(s => s._id === 0)?.count || 0) * sampleRatio), avgRfm: stats.find(s => s._id === 0)?.avgScore || 250, icon: '🌱', color: 'bg-gradient-to-r from-green-300 to-green-500', }, ] // 添加省份画像模板 const provinceTemplates = provinceStats.slice(0, 5).map((p, i) => ({ id: `tpl_province_${i}`, name: `${p._id}用户`, description: `来自${p._id}的用户群体`, criteria: { province: p._id }, userCount: Math.round(p.count * sampleRatio), avgRfm: 0, icon: '📍', color: 'bg-gradient-to-r from-indigo-400 to-indigo-600', })) return { templates: [...templates, ...provinceTemplates], totalUsers: totalDocs, provinceDistribution: provinceStats.map(p => ({ province: p._id, count: Math.round(p.count * sampleRatio) })) } } catch (error) { console.error('获取模板统计失败:', error) return { templates: [], totalUsers: 0, provinceDistribution: [] } } } // 创建自定义画像 async function createPortraitTemplate(data: { name: string description: string criteria: any }) { try { const client = await getMongoClient() const collection = client.db('KR').collection('用户估值') // 构建查询条件统计用户数 const query: any = {} if (data.criteria.minScore !== undefined) { query.user_evaluation_score = { $gte: data.criteria.minScore } } if (data.criteria.maxScore !== undefined) { query.user_evaluation_score = { ...(query.user_evaluation_score || {}), $lt: data.criteria.maxScore } } if (data.criteria.userLevel) { query.user_level = data.criteria.userLevel } if (data.criteria.province) { query.province = data.criteria.province } if (data.criteria.tags?.length) { query.source_channels = { $in: data.criteria.tags } } // 统计符合条件的用户数 const [userCount, avgScoreResult] = await Promise.all([ collection.countDocuments(query), collection.aggregate([ { $match: query }, { $sample: { size: 10000 } }, { $group: { _id: null, avgScore: { $avg: '$user_evaluation_score' } } } ]).toArray() ]) const avgRfm = avgScoreResult[0]?.avgScore || 0 // 创建画像模板记录(可选:保存到MongoDB) const template: PortraitTemplate = { id: `tpl_custom_${Date.now()}`, name: data.name, description: data.description, criteria: data.criteria, userCount, avgRfm: Math.round(avgRfm), status: 'active', createdAt: new Date().toISOString(), createdBy: 'admin' } // TODO: 保存到 shensheshou.portrait_templates 集合 // await client.db('shensheshou').collection('portrait_templates').insertOne(template) return template } catch (error) { console.error('创建画像失败:', error) throw error } } // 获取单个用户画像 async function getUserPortrait(queryStr: string): Promise { const query = queryStr try { // 检测查询类型 const isPhone = /^1[3-9]\d{9}$/.test(query.replace(/\D/g, '')) const isQQ = /^\d{5,11}$/.test(query) if (isPhone) { const profile = await queryFullProfile(query) if (!profile.valuation && !profile.qqPhone && !profile.ckbAsset) { return null } const sources = [] if (profile.valuation) { sources.push({ name: 'KR.用户估值', matched: true, fields: Object.keys(profile.valuation).filter(k => !k.startsWith('_')) }) } if (profile.qqPhone) { sources.push({ name: 'KR_腾讯.QQ+手机', matched: true, fields: Object.keys(profile.qqPhone).filter(k => !k.startsWith('_')) }) } if (profile.ckbAsset) { sources.push({ name: 'KR_存客宝.用户资产统一视图', matched: true, fields: Object.keys(profile.ckbAsset).filter(k => !k.startsWith('_')) }) } const v = profile.valuation const q = profile.qqPhone return { id: String(v?._id || q?._id || 'unknown'), phone: query, phoneMasked: maskPhone(query), name: v?.name, gender: v?.gender, ageRange: v?.age_range, province: v?.province || q?.省份, city: v?.city || q?.地区, userLevel: v?.user_level, rfmScore: v?.rfm_composite_score || v?.user_evaluation_score, tags: v?.tags || [], dataQuality: { completeness: v?.data_quality?.completeness || 0, sourceCount: sources.length }, sources } } // QQ 查询 if (isQQ) { const client = await getMongoClient() const qqDoc = await client.db('KR_腾讯').collection('QQ+手机').findOne({ $or: [{ qq: query }, { qq: parseInt(query) }] }) if (!qqDoc) return null const phone = qqDoc.phone || qqDoc['手机号'] if (phone) { // 通过手机号获取完整画像 return getUserPortrait(String(phone)) } return { id: String(qqDoc._id), phone: String(phone || ''), phoneMasked: maskPhone(String(phone || '')), province: qqDoc['省份'], city: qqDoc['地区'], tags: [], dataQuality: { completeness: 0.3, sourceCount: 1 }, sources: [{ name: 'KR_腾讯.QQ+手机', matched: true, fields: Object.keys(qqDoc).filter(k => !k.startsWith('_')) }] } } return null } catch (error) { console.error('获取用户画像失败:', error) return null } } // 获取人群分布统计 async function getCrowdDistribution(): Promise { try { const client = await getMongoClient() const collection = client.db('KR').collection('用户估值') const total = await collection.estimatedDocumentCount() // 等级分布 const levelStats = await collection.aggregate([ { $match: { user_level: { $exists: true, $ne: null } } }, { $group: { _id: '$user_level', count: { $sum: 1 } } }, { $sort: { count: -1 } } ]).toArray() // 省份分布 const provinceStats = await collection.aggregate([ { $match: { province: { $exists: true, $ne: null } } }, { $group: { _id: '$province', count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 10 } ]).toArray() // 性别分布 const genderStats = await collection.aggregate([ { $match: { gender: { $exists: true, $ne: null } } }, { $group: { _id: '$gender', count: { $sum: 1 } } } ]).toArray() return { levelDistribution: levelStats.map(s => ({ level: s._id || '未知', count: s.count, percentage: Math.round((s.count / total) * 100 * 100) / 100 })), provinceDistribution: provinceStats.map(s => ({ province: s._id || '未知', count: s.count, percentage: Math.round((s.count / total) * 100 * 100) / 100 })), genderDistribution: genderStats.map(s => ({ gender: s._id || '未知', count: s.count, percentage: Math.round((s.count / total) * 100 * 100) / 100 })), ageDistribution: [] // 年龄数据可能不完整 } } catch (error) { console.error('获取人群分布失败:', error) return { levelDistribution: [], provinceDistribution: [], genderDistribution: [], ageDistribution: [] } } } // GET: 获取用户画像或人群分布 export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const query = searchParams.get('query') const action = searchParams.get('action') const page = parseInt(searchParams.get('page') || '1') const limit = parseInt(searchParams.get('limit') || '20') const userLevel = searchParams.get('userLevel') const province = searchParams.get('province') try { // 获取画像模板统计 if (action === 'templates') { const templateData = await getPortraitTemplateStats() return NextResponse.json({ success: true, ...templateData }) } // 获取用户列表(真实数据) if (action === 'users') { const filters: any = {} if (userLevel) filters.userLevel = userLevel if (province) filters.province = province const userData = await getRealUserList(page, limit, filters) return NextResponse.json({ success: true, ...userData }) } // 人群分布统计 if (action === 'distribution') { const distribution = await getCrowdDistribution() return NextResponse.json({ success: true, distribution }) } // 单用户画像查询 if (query) { const portrait = await getUserPortrait(query) if (!portrait) { return NextResponse.json({ success: false, error: '未找到用户数据', portrait: null }, { status: 404 }) } return NextResponse.json({ success: true, portrait }) } // 默认返回模板和用户列表 const [templateData, userData] = await Promise.all([ getPortraitTemplateStats(), getRealUserList(1, 10) ]) return NextResponse.json({ success: true, templates: templateData.templates, totalUsers: templateData.totalUsers, users: userData.users, usersTotal: userData.total }) } catch (error: any) { console.error('画像 API 错误:', error) return NextResponse.json({ success: false, error: error.message }, { status: 500 }) } } // POST: 人群圈选或创建画像 export async function POST(request: NextRequest) { try { const body = await request.json() const { action, filters, name, description, criteria } = body // 创建画像模板 if (action === 'createTemplate') { if (!name) { return NextResponse.json({ success: false, error: '画像名称为必填项' }, { status: 400 }) } const template = await createPortraitTemplate({ name, description: description || '', criteria: criteria || {} }) return NextResponse.json({ success: true, template, message: `画像 "${name}" 创建成功,包含 ${template.userCount.toLocaleString()} 个用户` }) } // 人群圈选(默认行为) // 构建查询条件 const query: any = {} if (filters?.userLevel) { query.user_level = { $in: Array.isArray(filters.userLevel) ? filters.userLevel : [filters.userLevel] } } if (filters?.province) { query.province = { $in: Array.isArray(filters.province) ? filters.province : [filters.province] } } if (filters?.city) { query.city = { $in: Array.isArray(filters.city) ? filters.city : [filters.city] } } if (filters?.rfmScoreMin !== undefined) { query.user_evaluation_score = { $gte: filters.rfmScoreMin } } if (filters?.rfmScoreMax !== undefined) { query.user_evaluation_score = { ...(query.user_evaluation_score || {}), $lte: filters.rfmScoreMax } } if (filters?.tags && filters.tags.length > 0) { query.source_channels = { $in: filters.tags } } const client = await getMongoClient() const collection = client.db('KR').collection('用户估值') // 统计符合条件的用户数 const count = await collection.countDocuments(query) // 获取样本数据 const samples = await collection.find(query) .limit(20) .project({ phone: 1, phone_masked: 1, name: 1, user_level: 1, user_evaluation_score: 1, province: 1, city: 1, source_channels: 1 }) .toArray() return NextResponse.json({ success: true, crowd: { id: `crowd_${Date.now()}`, name: name || '未命名人群', description, filters, userCount: count, samples: samples.map(s => ({ id: s._id.toString(), phone: maskPhone(s.phone || s.phone_masked), name: s.name || '未知', level: s.user_level || '-', score: s.user_evaluation_score || 0, province: s.province || '-', city: s.city || '-', tags: s.source_channels || [] })), createdAt: new Date().toISOString() } }) } catch (error: any) { console.error('画像 POST 错误:', error) return NextResponse.json({ success: false, error: error.message }, { status: 500 }) } }