chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

315
app/api/ai-chat/route.ts Normal file
View File

@@ -0,0 +1,315 @@
/**
* AI 智能助手 API
* 支持自然语言查询用户数据
*/
import { NextRequest, NextResponse } from "next/server"
import { getMongoClient, intelligentSearch, queryFullProfile, getDatabaseStats } from "@/lib/mongodb"
// 消息类型
interface ChatMessage {
role: "user" | "assistant" | "system"
content: string
timestamp?: string
data?: any
}
// 解析用户意图
function parseIntent(message: string): { type: string; query?: string; params?: any } {
const msg = message.trim().toLowerCase()
// 查询手机号
const phoneMatch = message.match(/(?:查|查询|搜索|找)?[:\s]*(\+?86)?1[3-9]\d{9}/g)
if (phoneMatch) {
const phone = phoneMatch[0].replace(/[查询搜索找::\s]/g, '').replace(/^\+?86/, '')
return { type: "query_phone", query: phone }
}
// 查询 QQ
const qqMatch = message.match(/(?:qq|QQ)[:\s]*(\d{5,11})|(\d{5,11})\s*(?:qq|QQ)/i)
if (qqMatch) {
return { type: "query_qq", query: qqMatch[1] || qqMatch[2] }
}
// 系统状态
if (msg.includes("状态") || msg.includes("统计") || msg.includes("总量")) {
return { type: "system_status" }
}
// RFM 分析
if (msg.includes("rfm") || msg.includes("估值") || msg.includes("价值")) {
return { type: "rfm_analysis" }
}
// 高价值用户
if (msg.includes("高价值") || msg.includes("top") || msg.includes("排行")) {
const limitMatch = msg.match(/(\d+)/)
return { type: "high_value_users", params: { limit: limitMatch ? parseInt(limitMatch[1]) : 10 } }
}
// 帮助
if (msg.includes("帮助") || msg.includes("help") || msg === "?") {
return { type: "help" }
}
// 通用搜索
return { type: "search", query: message }
}
// 格式化用户数据
function formatUserData(user: any): string {
if (!user) return "未找到用户信息"
const lines: string[] = []
if (user.name) lines.push(`👤 姓名: ${user.name}`)
if (user.phone_masked) lines.push(`📱 手机: ${user.phone_masked}`)
if (user.qq) lines.push(`💬 QQ: ${user.qq}`)
if (user.gender) lines.push(`⚧ 性别: ${user.gender}`)
if (user.age_range) lines.push(`📅 年龄段: ${user.age_range}`)
if (user.province || user.city) lines.push(`📍 地区: ${user.province || ''}${user.city || ''}`)
if (user.evaluation_score || user.user_evaluation_score) {
lines.push(`⭐ 估值分: ${user.evaluation_score || user.user_evaluation_score}`)
}
if (user.user_level) lines.push(`🏆 等级: ${user.user_level}`)
if (user.carrier) lines.push(`📶 运营商: ${user.carrier}`)
if (user.tags && user.tags.length > 0) lines.push(`🏷️ 标签: ${user.tags.join(', ')}`)
return lines.join('\n')
}
// 处理 AI 聊天
async function processChat(message: string): Promise<ChatMessage> {
const startTime = Date.now()
const intent = parseIntent(message)
try {
switch (intent.type) {
case "query_phone": {
const result = await queryFullProfile(intent.query!)
if (result.valuation || result.qqPhone) {
const user = {
...result.valuation,
qq: result.qqPhone?.qq,
carrier: result.qqPhone?.运营商
}
return {
role: "assistant",
content: `🎯 手机号 ${intent.query} 查询结果:\n\n${formatUserData(user)}\n\n⏱ 查询耗时: ${Date.now() - startTime}ms`,
data: result
}
} else {
return {
role: "assistant",
content: `❌ 未找到手机号 ${intent.query} 的相关信息\n\n💡 提示: 请检查手机号是否正确11位数字`
}
}
}
case "query_qq": {
const client = await getMongoClient()
const qqDb = client.db("KR_腾讯")
let qqDoc = await qqDb.collection("QQ+手机").findOne({ qq: intent.query })
if (!qqDoc) {
qqDoc = await qqDb.collection("QQ+手机").findOne({ qq: parseInt(intent.query!) })
}
if (qqDoc) {
const phone = qqDoc.phone || qqDoc.
let userInfo = `🎯 QQ ${intent.query} 查询结果:\n\n`
userInfo += `💬 QQ: ${qqDoc.qq}\n`
userInfo += `📱 手机: ${phone ? phone.toString().replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '未知'}\n`
userInfo += `📊 QQ评分: ${qqDoc.QQ号评分 || 'N/A'}\n`
userInfo += `📊 手机评分: ${qqDoc. || 'N/A'}\n`
userInfo += `📍 地区: ${qqDoc. || ''}${qqDoc. || ''}\n`
userInfo += `📶 运营商: ${qqDoc. || 'N/A'}\n`
userInfo += `\n⏱ 查询耗时: ${Date.now() - startTime}ms`
return {
role: "assistant",
content: userInfo,
data: qqDoc
}
} else {
return {
role: "assistant",
content: `❌ 未找到 QQ ${intent.query} 的相关信息`
}
}
}
case "system_status": {
const stats = await getDatabaseStats()
return {
role: "assistant",
content: `📊 神射手系统状态\n\n` +
`🟢 连接状态: ${stats.connected ? '正常' : '异常'}\n` +
`📁 数据库数: ${stats.databases?.length || 0}\n` +
`📄 总记录数: ${(stats.totalDocuments / 1e8).toFixed(2)} 亿条\n` +
`💾 总数据量: ${(stats.totalSize / 1e9).toFixed(2)} GB\n` +
`⏱️ 响应延迟: ${stats.latency}ms`,
data: stats
}
}
case "rfm_analysis": {
const client = await getMongoClient()
const db = client.db("KR")
const pipeline = [
{ $group: { _id: "$user_level", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]
const results = await db.collection("用户估值").aggregate(pipeline).toArray()
let content = `📈 RFM 用户分布统计\n\n`
let total = 0
results.forEach(r => {
const level = r._id || '未分级'
content += `${level}: ${(r.count / 10000).toFixed(1)}\n`
total += r.count
})
content += `\n📊 总用户数: ${(total / 10000).toFixed(1)}`
content += `\n⏱ 统计耗时: ${Date.now() - startTime}ms`
return {
role: "assistant",
content,
data: results
}
}
case "high_value_users": {
const limit = intent.params?.limit || 10
const client = await getMongoClient()
const db = client.db("KR")
const users = await db.collection("用户估值")
.find({ user_evaluation_score: { $exists: true } })
.sort({ user_evaluation_score: -1 })
.limit(limit)
.toArray()
let content = `🏆 高价值用户 TOP${limit}\n\n`
users.forEach((u, i) => {
const phone = u.phone?.replace(/(\+?86)?(\d{3})\d{4}(\d{4})/, '$2****$3') || '***'
content += `${i + 1}. ${u.name || '未知'} (${phone}) - 估值: ${u.user_evaluation_score}\n`
})
content += `\n⏱ 查询耗时: ${Date.now() - startTime}ms`
return {
role: "assistant",
content,
data: users
}
}
case "help": {
return {
role: "assistant",
content: `🎯 神射手 AI 助手使用指南\n\n` +
`📱 查询手机号:\n "查 13800138000" 或 "13800138000"\n\n` +
`💬 查询 QQ:\n "28533368 qq" 或 "qq 28533368"\n\n` +
`📊 系统状态:\n "系统状态" 或 "统计"\n\n` +
`📈 RFM 分析:\n "RFM分析" 或 "用户估值"\n\n` +
`🏆 高价值用户:\n "高价值用户 TOP10"\n\n` +
`💡 数据覆盖: 20亿+用户207GB数据`
}
}
case "search":
default: {
// 尝试智能搜索
const result = await intelligentSearch(intent.query || message, { limit: 5 })
if (result.total > 0) {
let content = `🔍 搜索结果 (共${result.total}条)\n\n`
result.users.slice(0, 5).forEach((u, i) => {
content += `${i + 1}. ${u.name || '未知'} - ${u.city || ''} - 估值: ${u.user_evaluation_score || 'N/A'}\n`
})
content += `\n⏱ 查询耗时: ${Date.now() - startTime}ms`
return {
role: "assistant",
content,
data: result
}
} else {
return {
role: "assistant",
content: `🤔 我不太理解 "${message}"\n\n` +
`💡 你可以尝试:\n` +
`- 查询手机号: "查 13800138000"\n` +
`- 查询QQ: "28533368 qq"\n` +
`- 输入 "帮助" 查看更多功能`
}
}
}
}
} catch (error: any) {
console.error("AI Chat 错误:", error)
return {
role: "assistant",
content: `⚠️ 查询出错: ${error.message}\n\n请稍后重试或检查数据库连接`
}
}
}
/**
* POST /api/ai-chat
* AI 对话接口
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { message, history = [] } = body
if (!message || !message.trim()) {
return NextResponse.json({
success: false,
error: "消息不能为空"
}, { status: 400 })
}
const response = await processChat(message)
response.timestamp = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
return NextResponse.json({
success: true,
response
})
} catch (error: any) {
console.error("AI Chat API 错误:", error)
return NextResponse.json({
success: false,
error: error.message,
response: {
role: "assistant",
content: `⚠️ 服务暂时不可用: ${error.message}`,
timestamp: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
}
}, { status: 500 })
}
}
/**
* GET /api/ai-chat
* 获取 AI 助手状态
*/
export async function GET() {
try {
const stats = await getDatabaseStats()
return NextResponse.json({
status: "online",
model: "神射手 AI v1.0",
capabilities: ["用户查询", "QQ查询", "RFM分析", "智能搜索"],
database: {
connected: stats.connected,
totalUsers: stats.totalDocuments,
latency: stats.latency
}
})
} catch (error: any) {
return NextResponse.json({
status: "degraded",
error: error.message
})
}
}

303
app/api/ai-tagging/route.ts Normal file
View File

@@ -0,0 +1,303 @@
/**
* AI 打标 API
* 提供智能标签生成、批量打标、标签推荐等功能
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient, queryFullProfile, maskPhone } from '@/lib/mongodb'
// AI 标签规则
interface TagRule {
id: string
name: string
description: string
conditions: {
field: string
operator: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'contains' | 'in'
value: any
}[]
resultTag: string
category: string
priority: number
enabled: boolean
}
// 预定义的 AI 标签规则
const AI_TAG_RULES: TagRule[] = [
{
id: 'rule_1',
name: '钻石用户识别',
description: 'RFM评分≥80的用户标记为钻石用户',
conditions: [
{ field: 'rfm_composite_score', operator: 'gte', value: 80 }
],
resultTag: '钻石用户',
category: 'value',
priority: 1,
enabled: true
},
{
id: 'rule_2',
name: '高频活跃用户',
description: '近30天活跃次数>10的用户',
conditions: [
{ field: 'activity_count_30d', operator: 'gt', value: 10 }
],
resultTag: '高频活跃',
category: 'behavior',
priority: 2,
enabled: true
},
{
id: 'rule_3',
name: '流失风险用户',
description: '90天未活跃的用户',
conditions: [
{ field: 'last_active_days', operator: 'gt', value: 90 }
],
resultTag: '流失风险',
category: 'lifecycle',
priority: 3,
enabled: true
},
{
id: 'rule_4',
name: '一线城市用户',
description: '来自北上广深的用户',
conditions: [
{ field: 'city', operator: 'in', value: ['北京', '上海', '广州', '深圳'] }
],
resultTag: '一线城市',
category: 'region',
priority: 4,
enabled: true
},
{
id: 'rule_5',
name: '厦门本地用户',
description: '来自厦门的用户',
conditions: [
{ field: 'city', operator: 'eq', value: '厦门' }
],
resultTag: '厦门本地',
category: 'region',
priority: 5,
enabled: true
}
]
// 根据规则计算用户标签
function computeTagsForUser(userData: any, rules: TagRule[]): string[] {
const tags: string[] = []
for (const rule of rules) {
if (!rule.enabled) continue
let match = true
for (const condition of rule.conditions) {
const fieldValue = userData[condition.field]
switch (condition.operator) {
case 'eq':
match = match && fieldValue === condition.value
break
case 'ne':
match = match && fieldValue !== condition.value
break
case 'gt':
match = match && fieldValue > condition.value
break
case 'lt':
match = match && fieldValue < condition.value
break
case 'gte':
match = match && fieldValue >= condition.value
break
case 'lte':
match = match && fieldValue <= condition.value
break
case 'contains':
match = match && String(fieldValue).includes(condition.value)
break
case 'in':
match = match && Array.isArray(condition.value) && condition.value.includes(fieldValue)
break
}
}
if (match) {
tags.push(rule.resultTag)
}
}
return tags
}
// 为单个用户生成AI标签
async function generateTagsForUser(phone: string): Promise<{
phone: string
phoneMasked: string
originalTags: string[]
newTags: string[]
allTags: string[]
}> {
const profile = await queryFullProfile(phone)
const userData = {
...profile.valuation,
city: profile.valuation?.city || profile.qqPhone?.,
province: profile.valuation?.province || profile.qqPhone?.
}
const originalTags = profile.valuation?.tags || []
const newTags = computeTagsForUser(userData, AI_TAG_RULES)
const allTags = [...new Set([...originalTags, ...newTags])]
return {
phone,
phoneMasked: maskPhone(phone),
originalTags,
newTags,
allTags
}
}
// GET: 获取 AI 标签规则或统计
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const phone = searchParams.get('phone')
try {
// 获取标签规则
if (action === 'rules') {
return NextResponse.json({
success: true,
rules: AI_TAG_RULES,
total: AI_TAG_RULES.length
})
}
// 为单个用户生成标签
if (phone) {
const result = await generateTagsForUser(phone)
return NextResponse.json({
success: true,
result
})
}
// AI 打标统计
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
const stats = await collection.aggregate([
{ $match: { tags: { $exists: true, $ne: [] } } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 }
]).toArray()
return NextResponse.json({
success: true,
rules: AI_TAG_RULES,
tagDistribution: stats.map(s => ({
tag: s._id,
count: s.count
}))
})
} catch (error: any) {
console.error('AI 打标 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, phones, rule, filters } = body
// 批量打标
if (action === 'batch') {
const results = []
const phonesToProcess = phones || []
for (const phone of phonesToProcess.slice(0, 100)) {
try {
const result = await generateTagsForUser(phone)
results.push(result)
} catch (e) {
results.push({
phone,
phoneMasked: maskPhone(phone),
error: '处理失败'
})
}
}
return NextResponse.json({
success: true,
processed: results.length,
results
})
}
// 按条件批量打标
if (action === 'batch_by_filter') {
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
// 构建查询条件
const query: any = {}
if (filters?.province) query.province = filters.province
if (filters?.city) query.city = filters.city
if (filters?.userLevel) query.user_level = filters.userLevel
const count = await collection.countDocuments(query)
// 模拟批量任务创建
const taskId = `task_${Date.now()}`
return NextResponse.json({
success: true,
taskId,
estimatedUsers: count,
message: `已创建批量打标任务,预计处理 ${count} 个用户`
})
}
// 创建标签规则
if (action === 'create_rule') {
const newRule: TagRule = {
id: `rule_${Date.now()}`,
...rule,
enabled: true
}
// TODO: 保存到数据库
return NextResponse.json({
success: true,
rule: newRule,
message: '标签规则创建成功'
})
}
return NextResponse.json({
success: false,
error: '未知操作'
}, { status: 400 })
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}

View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server"
/**
* 本地登录 API - 支持邮箱/手机号 + 密码
* 当未配置 NEXT_PUBLIC_API_BASE_URL 时使用
* 开发账号: zhiqun@qq.com / Zhiqun1984
*/
const MOCK_USERS: Record<string, { password: string }> = {
"zhiqun@qq.com": { password: "Zhiqun1984" },
}
function isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const account = (formData.get("email") || formData.get("phone") || "").toString().trim()
const password = (formData.get("password") || "").toString()
const verificationCode = formData.get("verificationCode")?.toString()
if (!account) {
return NextResponse.json(
{ code: 40001, message: "请输入邮箱或手机号" },
{ status: 200 }
)
}
// 验证码登录开发环境下任意6位验证码通过
if (verificationCode) {
if (verificationCode.length >= 4) {
const token = `mock_token_${Date.now()}_${account}`
return NextResponse.json({
code: 10000,
message: "登录成功",
data: { token },
})
}
return NextResponse.json(
{ code: 40002, message: "验证码错误" },
{ status: 200 }
)
}
// 密码登录
if (!password) {
return NextResponse.json(
{ code: 40003, message: "请输入密码" },
{ status: 200 }
)
}
const key = isEmail(account) ? account : account
const user = MOCK_USERS[key]
if (user && user.password === password) {
const token = `mock_token_${Date.now()}_${account}`
return NextResponse.json({
code: 10000,
message: "登录成功",
data: { token },
})
}
return NextResponse.json(
{ code: 40004, message: "邮箱/手机号或密码错误" },
{ status: 200 }
)
} catch (error) {
console.error("[auth/login]", error)
return NextResponse.json(
{ code: 50000, message: "服务器错误" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server"
/**
* 本地验证码发送 API (mock)
* 开发环境下直接返回成功验证码可为任意4位以上
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const phone = (formData.get("phone") || "").toString().trim()
if (!phone) {
return NextResponse.json(
{ code: 40001, message: "请输入手机号" },
{ status: 200 }
)
}
// Mock: 模拟发送成功,开发时可用 123456 等作为验证码
return NextResponse.json({
code: 10000,
message: "验证码已发送开发模式可使用任意4位以上数字",
})
} catch (error) {
console.error("[auth/send-code]", error)
return NextResponse.json(
{ code: 50000, message: "服务器错误" },
{ status: 500 }
)
}
}

185
app/api/channels/route.ts Normal file
View File

@@ -0,0 +1,185 @@
import { NextRequest, NextResponse } from 'next/server'
// 渠道配置接口
interface ChannelConfig {
id: string
type: 'feishu' | 'wecom' | 'wechat' | 'api'
name: string
status: 'active' | 'pending' | 'error'
config: Record<string, string>
lastChecked?: string
error?: string
}
// 网关地址卡若AI部署的网关
const GATEWAY_URL = process.env.GATEWAY_URL || 'http://localhost:8000'
// GET: 获取渠道状态
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const type = searchParams.get('type')
try {
// 测试飞书连接
if (action === 'test' && type === 'feishu') {
try {
const res = await fetch(`${GATEWAY_URL}/feishu/test`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
const data = await res.json()
return NextResponse.json({
success: data.status === 'success',
message: data.message,
appId: data.app_id
})
} catch (e) {
return NextResponse.json({
success: false,
message: '网关连接失败请确认卡若AI网关已启动',
error: String(e)
})
}
}
// 获取飞书群列表
if (action === 'chats' && type === 'feishu') {
try {
const res = await fetch(`${GATEWAY_URL}/feishu/chats?refresh=true`)
const data = await res.json()
return NextResponse.json({
success: data.success,
chats: data.chats || [],
count: data.count || 0
})
} catch (e) {
return NextResponse.json({
success: false,
message: '获取群列表失败',
chats: []
})
}
}
// 获取所有渠道状态
const channels: ChannelConfig[] = [
{
id: 'feishu-1',
type: 'feishu',
name: '飞书机器人',
status: 'pending',
config: {
webhookUrl: `${GATEWAY_URL}/feishu/webhook`,
testUrl: `${GATEWAY_URL}/feishu/test`
}
},
{
id: 'wecom-1',
type: 'wecom',
name: '企业微信应用',
status: 'pending',
config: {
webhookUrl: `${GATEWAY_URL}/wecom/callback`
}
},
{
id: 'api-1',
type: 'api',
name: 'REST API',
status: 'active',
config: {
endpoint: '/api/ai-chat',
method: 'POST'
}
}
]
return NextResponse.json({
success: true,
channels,
gateway: GATEWAY_URL
})
} catch (error) {
return NextResponse.json({
success: false,
error: String(error)
}, { status: 500 })
}
}
// POST: 发送消息到渠道
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action, type, ...params } = body
// 发送消息到飞书群
if (action === 'send' && type === 'feishu') {
const { chatName, content } = params
if (!chatName || !content) {
return NextResponse.json({
success: false,
error: '缺少参数: chatName, content'
}, { status: 400 })
}
try {
const res = await fetch(`${GATEWAY_URL}/feishu/send_message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_name: chatName,
content: content
})
})
const data = await res.json()
return NextResponse.json(data)
} catch (e) {
return NextResponse.json({
success: false,
message: '发送失败',
error: String(e)
})
}
}
// 发送会议纪要
if (action === 'send_minutes' && type === 'feishu') {
const { minutesUrl, chatName, useLlm = true } = params
try {
const res = await fetch(`${GATEWAY_URL}/feishu/send_minutes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
minutes_url: minutesUrl,
chat_name: chatName,
use_llm: useLlm
})
})
const data = await res.json()
return NextResponse.json(data)
} catch (e) {
return NextResponse.json({
success: false,
message: '发送会议纪要失败',
error: String(e)
})
}
}
return NextResponse.json({
success: false,
error: '未知操作'
}, { status: 400 })
} catch (error) {
return NextResponse.json({
success: false,
error: String(error)
}, { status: 500 })
}
}

View File

@@ -0,0 +1,317 @@
/**
* 清洗规则 API
* 提供数据清洗规则管理、执行清洗任务等功能
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient, maskPhone } from '@/lib/mongodb'
// 清洗规则接口
interface CleaningRule {
id: string
name: string
description: string
type: 'format' | 'dedup' | 'validate' | 'enrich' | 'mask'
targetField: string
config: {
pattern?: string
replacement?: string
validation?: string
enrichSource?: string
maskType?: 'phone' | 'email' | 'idcard' | 'name'
}
priority: number
enabled: boolean
createdAt: string
lastRunAt?: string
processedCount?: number
}
// 预定义清洗规则
const CLEANING_RULES: CleaningRule[] = [
{
id: 'rule_phone_format',
name: '手机号格式标准化',
description: '将各种格式的手机号统一为11位标准格式',
type: 'format',
targetField: 'phone',
config: {
pattern: '^(\\+?86)?([1][3-9]\\d{9})$',
replacement: '$2'
},
priority: 1,
enabled: true,
createdAt: '2025-01-01',
lastRunAt: '2026-01-31',
processedCount: 1500000
},
{
id: 'rule_phone_dedup',
name: '手机号去重',
description: '按手机号去除重复记录,保留最新数据',
type: 'dedup',
targetField: 'phone',
config: {},
priority: 2,
enabled: true,
createdAt: '2025-01-01',
lastRunAt: '2026-01-30',
processedCount: 50000
},
{
id: 'rule_phone_validate',
name: '手机号有效性验证',
description: '验证手机号是否符合中国大陆手机号规则',
type: 'validate',
targetField: 'phone',
config: {
validation: '^1[3-9]\\d{9}$'
},
priority: 3,
enabled: true,
createdAt: '2025-01-01',
lastRunAt: '2026-01-31',
processedCount: 2000000000
},
{
id: 'rule_phone_mask',
name: '手机号脱敏',
description: '对外展示时隐藏手机号中间4位',
type: 'mask',
targetField: 'phone',
config: {
maskType: 'phone'
},
priority: 4,
enabled: true,
createdAt: '2025-01-01',
processedCount: 0
},
{
id: 'rule_province_enrich',
name: '省份信息补全',
description: '根据手机号归属地补全省份信息',
type: 'enrich',
targetField: 'province',
config: {
enrichSource: 'phone_location'
},
priority: 5,
enabled: true,
createdAt: '2025-01-01',
lastRunAt: '2026-01-29',
processedCount: 800000
},
{
id: 'rule_email_format',
name: '邮箱格式标准化',
description: '邮箱地址转小写并去除空格',
type: 'format',
targetField: 'email',
config: {
pattern: '\\s+',
replacement: ''
},
priority: 6,
enabled: true,
createdAt: '2025-01-01',
processedCount: 100000
}
]
// 执行清洗规则
async function executeCleaningRule(ruleId: string, limit: number = 1000): Promise<{
processed: number
cleaned: number
errors: number
samples: any[]
}> {
const rule = CLEANING_RULES.find(r => r.id === ruleId)
if (!rule) throw new Error('规则不存在')
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
// 模拟清洗执行
const samples = await collection.find({})
.limit(10)
.project({ phone: 1, province: 1, city: 1 })
.toArray()
return {
processed: limit,
cleaned: Math.floor(limit * 0.95),
errors: Math.floor(limit * 0.01),
samples: samples.map(s => ({
...s,
phone: maskPhone(s.phone),
cleaningApplied: rule.name
}))
}
}
// GET: 获取清洗规则列表
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const type = searchParams.get('type')
const id = searchParams.get('id')
try {
// 获取规则详情
if (id) {
const rule = CLEANING_RULES.find(r => r.id === id)
if (!rule) {
return NextResponse.json({
success: false,
error: '规则不存在'
}, { status: 404 })
}
return NextResponse.json({
success: true,
rule
})
}
// 获取规则统计
if (action === 'stats') {
const stats = {
totalRules: CLEANING_RULES.length,
enabledRules: CLEANING_RULES.filter(r => r.enabled).length,
totalProcessed: CLEANING_RULES.reduce((sum, r) => sum + (r.processedCount || 0), 0),
rulesByType: {
format: CLEANING_RULES.filter(r => r.type === 'format').length,
dedup: CLEANING_RULES.filter(r => r.type === 'dedup').length,
validate: CLEANING_RULES.filter(r => r.type === 'validate').length,
enrich: CLEANING_RULES.filter(r => r.type === 'enrich').length,
mask: CLEANING_RULES.filter(r => r.type === 'mask').length
}
}
return NextResponse.json({
success: true,
stats
})
}
// 获取规则列表
let rules = [...CLEANING_RULES]
if (type) {
rules = rules.filter(r => r.type === type)
}
rules.sort((a, b) => a.priority - b.priority)
return NextResponse.json({
success: true,
rules,
total: rules.length
})
} 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, ruleId, rule, limit } = body
// 执行清洗
if (action === 'execute') {
if (!ruleId) {
return NextResponse.json({
success: false,
error: '请指定要执行的规则'
}, { status: 400 })
}
const result = await executeCleaningRule(ruleId, limit || 1000)
return NextResponse.json({
success: true,
result
})
}
// 批量执行所有启用的规则
if (action === 'execute_all') {
const enabledRules = CLEANING_RULES.filter(r => r.enabled)
const results = []
for (const r of enabledRules) {
try {
const result = await executeCleaningRule(r.id, limit || 100)
results.push({
ruleId: r.id,
ruleName: r.name,
...result
})
} catch (e: any) {
results.push({
ruleId: r.id,
ruleName: r.name,
error: e.message
})
}
}
return NextResponse.json({
success: true,
executed: results.length,
results
})
}
// 创建规则
if (action === 'create') {
if (!rule?.name || !rule?.type || !rule?.targetField) {
return NextResponse.json({
success: false,
error: '规则名称、类型和目标字段为必填项'
}, { status: 400 })
}
const newRule: CleaningRule = {
id: `rule_${Date.now()}`,
...rule,
priority: CLEANING_RULES.length + 1,
enabled: true,
createdAt: new Date().toISOString().split('T')[0],
processedCount: 0
}
// TODO: 保存到数据库
return NextResponse.json({
success: true,
rule: newRule,
message: '清洗规则创建成功'
})
}
// 更新规则状态
if (action === 'toggle') {
return NextResponse.json({
success: true,
message: `规则 ${ruleId} 状态已更新`
})
}
return NextResponse.json({
success: false,
error: '未知操作'
}, { status: 400 })
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}

View File

@@ -0,0 +1,286 @@
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient } from '@/lib/mongodb'
// 项目定义(基于真实数据库)
const PROJECTS = {
ckb: {
id: 'ckb',
name: '存客宝',
database: 'KR_存客宝',
collection: '用户资产统一视图',
color: 'blue',
icon: 'Users'
},
dlm: {
id: 'dlm',
name: '点了码',
database: 'KR_点了码',
collection: '用户资产统一视图',
color: 'green',
icon: 'QrCode'
},
weibo: {
id: 'weibo',
name: '微博',
database: 'KR_微博',
collection: '微博uid+手机',
color: 'orange',
icon: 'Globe'
},
qq: {
id: 'qq',
name: 'QQ社交',
database: 'KR_腾讯',
collection: 'QQ+手机',
color: 'purple',
icon: 'MessageCircle'
}
}
// 获取项目列表及统计
async function getProjectStats() {
const client = await getMongoClient()
const stats = []
for (const [key, project] of Object.entries(PROJECTS)) {
try {
const db = client.db(project.database)
const count = await db.collection(project.collection).estimatedDocumentCount()
stats.push({
...project,
userCount: count,
status: 'active'
})
} catch {
stats.push({
...project,
userCount: 0,
status: 'error'
})
}
}
return stats
}
// 获取项目下的流量池/标签分类
async function getProjectPools(projectId: string) {
const client = await getMongoClient()
const project = PROJECTS[projectId as keyof typeof PROJECTS]
if (!project) {
return { error: '项目不存在' }
}
const db = client.db(project.database)
const collection = db.collection(project.collection)
// 根据项目类型获取不同的分组字段
let pools: any[] = []
if (projectId === 'ckb') {
// 存客宝按流量池分组
const poolStats = await collection.aggregate([
{ $sample: { size: 50000 } },
{ $group: {
_id: '$traffic_pool.pool_name',
count: { $sum: 1 },
avgScore: { $avg: '$user_evaluation_score' }
}},
{ $sort: { count: -1 } }
], { maxTimeMS: 10000 }).toArray()
// 按标签分组
const tagStats = await collection.aggregate([
{ $sample: { size: 50000 } },
{ $unwind: '$unified_tags' },
{ $group: { _id: '$unified_tags', count: { $sum: 1 } }},
{ $sort: { count: -1 } },
{ $limit: 20 }
], { maxTimeMS: 10000 }).toArray()
const total = await collection.estimatedDocumentCount()
const ratio = total / 50000
pools = [
...poolStats.filter(p => p._id).map(p => ({
id: `pool_${p._id}`,
name: p._id,
type: 'pool',
count: Math.round(p.count * ratio),
avgScore: Math.round(p.avgScore || 0)
})),
...tagStats.filter(t => t._id).map(t => ({
id: `tag_${t._id}`,
name: t._id,
type: 'tag',
count: Math.round(t.count * ratio)
}))
]
} else if (projectId === 'dlm') {
// 点了码按角色标签分组
const roleStats = await collection.aggregate([
{ $unwind: { path: '$角色标签', preserveNullAndEmptyArrays: true } },
{ $group: { _id: '$角色标签', count: { $sum: 1 } }},
{ $sort: { count: -1 } }
], { maxTimeMS: 10000 }).toArray()
// 按用户等级分组
const levelStats = await collection.aggregate([
{ $group: { _id: '$用户等级', count: { $sum: 1 } }},
{ $sort: { count: -1 } }
], { maxTimeMS: 10000 }).toArray()
pools = [
...roleStats.filter(r => r._id).map(r => ({
id: `role_${r._id}`,
name: r._id,
type: 'role',
count: r.count
})),
...levelStats.filter(l => l._id).map(l => ({
id: `level_${l._id}`,
name: `${l._id}级用户`,
type: 'level',
count: l.count
}))
]
} else if (projectId === 'qq') {
// QQ按省份分组
const provinceStats = await collection.aggregate([
{ $sample: { size: 100000 } },
{ $match: { '省份': { $exists: true, $ne: null } } },
{ $group: { _id: '$省份', count: { $sum: 1 } }},
{ $sort: { count: -1 } },
{ $limit: 20 }
], { maxTimeMS: 10000 }).toArray()
const total = await collection.estimatedDocumentCount()
const ratio = total / 100000
pools = provinceStats.map(p => ({
id: `province_${p._id}`,
name: p._id,
type: 'province',
count: Math.round(p.count * ratio)
}))
} else if (projectId === 'weibo') {
// 微博简单统计
const total = await collection.estimatedDocumentCount()
pools = [{
id: 'weibo_all',
name: '微博用户',
type: 'all',
count: total
}]
}
return {
project,
pools,
totalPools: pools.length
}
}
// 获取流量池内的用户列表
async function getPoolUsers(projectId: string, poolId: string, page: number = 1, limit: number = 20) {
const client = await getMongoClient()
const project = PROJECTS[projectId as keyof typeof PROJECTS]
if (!project) {
return { error: '项目不存在' }
}
const db = client.db(project.database)
const collection = db.collection(project.collection)
// 解析poolId构建查询条件
let query: any = {}
const [type, ...nameParts] = poolId.split('_')
const name = nameParts.join('_')
console.log('Pool query:', { type, name, projectId, poolId })
if (type === 'pool') {
query['traffic_pool.pool_name'] = name
} else if (type === 'tag') {
query['unified_tags'] = name
} else if (type === 'role') {
query['角色标签'] = name
} else if (type === 'level') {
query['用户等级'] = name.replace('级用户', '')
} else if (type === 'province') {
query['省份'] = name
}
// 如果没有匹配条件尝试直接用name搜索
if (Object.keys(query).length === 0 || type === 'all') {
query = {}
}
const skip = (page - 1) * limit
const [users, total] = await Promise.all([
collection.find(query)
.project({
phone_masked: 1,
name: 1,
'core_profile.name': 1,
user_evaluation_score: 1,
unified_tags: 1,
'角色标签': 1,
'用户等级': 1,
'rfm_scores.user_level': 1,
created_at: 1
})
.skip(skip)
.limit(limit)
.toArray(),
collection.countDocuments(query)
])
return {
users: users.map(u => ({
id: u._id.toString(),
phone: u.phone_masked || '未知',
name: u.name || u.core_profile?.name || '未知用户',
score: u.user_evaluation_score || 0,
level: u.rfm_scores?.user_level || u['用户等级'] || '-',
tags: u.unified_tags || u['角色标签'] || [],
createdAt: u.created_at
})),
total,
page,
totalPages: Math.ceil(total / limit)
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action') || 'projects'
const projectId = searchParams.get('projectId')
const poolId = searchParams.get('poolId')
const page = parseInt(searchParams.get('page') || '1')
try {
if (action === 'projects') {
const stats = await getProjectStats()
return NextResponse.json({ success: true, projects: stats })
}
if (action === 'pools' && projectId) {
const pools = await getProjectPools(projectId)
return NextResponse.json({ success: true, ...pools })
}
if (action === 'users' && projectId && poolId) {
const users = await getPoolUsers(projectId, poolId, page)
return NextResponse.json({ success: true, ...users })
}
return NextResponse.json({ error: '无效的操作' }, { status: 400 })
} catch (error) {
console.error('流量池API错误:', error)
return NextResponse.json({ error: '查询失败' }, { status: 500 })
}
}

View File

@@ -0,0 +1,297 @@
/**
* 数据源管理 API
* 提供数据源列表、连接状态检测、同步管理等功能
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient, getDatabaseStats } from '@/lib/mongodb'
// 数据源接口定义
interface DataSource {
id: string
name: string
nameCn: string // 中文名称
description: string // 功能描述
type: 'mongodb' | 'mysql' | 'api' | 'webhook'
status: 'connected' | 'disconnected' | 'warning'
host?: string
database?: string
endpoint?: string
lastSync: string
recordCount: number
syncFrequency: string
collections?: number
tables?: number
latency?: number
dataCategory?: string // 数据分类
targetCollection?: string // 目标集合(数据中台分配)
}
// 数据库中文名称和描述映射
const DB_DESCRIPTIONS: Record<string, { nameCn: string; description: string; category: string }> = {
'KR': { nameCn: '核心用户库', description: '用户估值、RFM评分、统一画像', category: '用户画像' },
'KR_KR': { nameCn: '扩展用户库', description: '多源用户数据汇聚', category: '用户数据' },
'KR_Linkedln': { nameCn: '领英数据库', description: '职业社交平台用户数据', category: '社交数据' },
'KR_京东': { nameCn: '京东用户库', description: '京东电商用户消费数据', category: '电商数据' },
'KR_人才库': { nameCn: '人才数据库', description: '招聘平台人才信息', category: '人力资源' },
'KR_企业': { nameCn: '企业信息库', description: '企业工商注册信息', category: '企业数据' },
'KR_企业名录': { nameCn: '企业名录', description: '企业联系方式和经营信息', category: '企业数据' },
'KR_卡若私域': { nameCn: '卡若私域库', description: '私域运营用户数据', category: '私域数据' },
'KR_商城': { nameCn: '商城用户库', description: '电商平台用户交易数据', category: '电商数据' },
'KR_国外': { nameCn: '海外用户库', description: '海外平台用户数据', category: '国际数据' },
'KR_存客宝': { nameCn: '存客宝CRM', description: '私域CRM用户资产数据', category: '私域数据' },
'KR_存客宝_四表重构KR_KR版': { nameCn: '存客宝重构版', description: '存客宝数据统一重构', category: '私域数据' },
'KR_微博': { nameCn: '微博用户库', description: '微博UID与手机号关联', category: '社交数据' },
'KR_快递': { nameCn: '快递信息库', description: '快递收发地址信息', category: '物流数据' },
'KR_户口': { nameCn: '户籍信息库', description: '户籍地址信息', category: '身份数据' },
'KR_手机': { nameCn: '手机号库', description: '手机号归属地和运营商', category: '基础数据' },
'KR_投资': { nameCn: '投资信息库', description: '投资理财用户数据', category: '金融数据' },
'KR_淘宝': { nameCn: '淘宝用户库', description: '淘宝电商用户数据', category: '电商数据' },
'KR_游戏': { nameCn: '游戏用户库', description: '游戏平台用户数据', category: '娱乐数据' },
'KR_点了码': { nameCn: '点了码商户库', description: '点了码商户和用户统一视图', category: '商业数据' },
'KR_腾讯': { nameCn: '腾讯社交库', description: 'QQ号与手机号关联数据', category: '社交数据' },
'KR_酒店': { nameCn: '酒店住宿库', description: '酒店入住记录信息', category: '消费数据' },
'KR_顺丰': { nameCn: '顺丰快递库', description: '顺丰快递收发信息', category: '物流数据' },
'KR_魔兽世界': { nameCn: '魔兽世界库', description: '魔兽世界玩家数据', category: '游戏数据' },
}
// 获取 MongoDB 数据源列表
async function getMongoDBSources(): Promise<DataSource[]> {
try {
const client = await getMongoClient()
const dbList = await client.db().admin().listDatabases()
const sources: DataSource[] = []
for (const dbInfo of dbList.databases) {
if (dbInfo.name.startsWith('KR')) {
const db = client.db(dbInfo.name)
const collections = await db.listCollections().toArray()
let totalDocs = 0
for (const coll of collections) {
const count = await db.collection(coll.name).estimatedDocumentCount()
totalDocs += count
}
// 获取中文名称和描述
const dbMeta = DB_DESCRIPTIONS[dbInfo.name] || {
nameCn: dbInfo.name,
description: `${dbInfo.name} 数据库`,
category: '其他'
}
sources.push({
id: `mongo_${dbInfo.name}`,
name: dbInfo.name,
nameCn: dbMeta.nameCn,
description: dbMeta.description,
type: 'mongodb',
status: 'connected',
host: process.env.MONGODB_URI?.split('@')[1]?.split('/')[0] || 'localhost:27017',
database: dbInfo.name,
lastSync: '实时',
recordCount: totalDocs,
syncFrequency: '实时',
collections: collections.length,
latency: 0,
dataCategory: dbMeta.category,
targetCollection: 'KR.用户估值' // 默认目标集合
})
}
}
return sources
} catch (error) {
console.error('获取 MongoDB 数据源失败:', error)
return []
}
}
// 检测数据源连接状态
async function checkConnectionStatus(): Promise<{
mongodb: { connected: boolean; latency: number }
mysql: { connected: boolean; latency: number }
}> {
const startTime = Date.now()
let mongoConnected = false
let mongoLatency = 0
try {
const client = await getMongoClient()
await client.db().admin().ping()
mongoConnected = true
mongoLatency = Date.now() - startTime
} catch {
mongoLatency = Date.now() - startTime
}
return {
mongodb: { connected: mongoConnected, latency: mongoLatency },
mysql: { connected: false, latency: 0 } // MySQL 暂未实现
}
}
// GET: 获取数据源列表
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
// 检查连接状态
if (action === 'status') {
const status = await checkConnectionStatus()
return NextResponse.json({
success: true,
status
})
}
// 获取数据库统计
if (action === 'stats') {
const stats = await getDatabaseStats()
return NextResponse.json({
success: true,
stats
})
}
// 获取完整数据源列表
const mongoSources = await getMongoDBSources()
// 计算统计信息
const stats = await getDatabaseStats()
// 预定义的外部数据源MySQL、API 等)
const externalSources: DataSource[] = [
{
id: 'mysql_ckb',
name: 'cunkebao_v3',
nameCn: '存客宝MySQL主库',
description: '存客宝CRM系统MySQL数据库包含客户、订单、营销数据',
type: 'mysql',
status: 'warning',
host: 'cdb-xxx.gz.tencentcdb.com:10050',
database: 'cunkebao_v3',
lastSync: '需要配置连接',
recordCount: 0,
syncFrequency: '增量同步',
tables: 45,
dataCategory: '私域数据',
targetCollection: 'KR_存客宝.用户资产统一视图'
},
{
id: 'mysql_dlm',
name: 'dianlema',
nameCn: '点了码MySQL库',
description: '点了码商户系统,包含商户、用户、交易数据',
type: 'mysql',
status: 'warning',
host: 'cdb-xxx.gz.tencentcdb.com:14413',
database: 'dianlema',
lastSync: '需要配置连接',
recordCount: 0,
syncFrequency: '增量同步',
tables: 28,
dataCategory: '商业数据',
targetCollection: 'KR_点了码.用户资产统一视图'
},
{
id: 'api_weibo',
name: 'weibo_api',
nameCn: '微博开放API',
description: '微博开放平台API获取用户公开信息和热点数据',
type: 'api',
status: 'disconnected',
endpoint: 'https://api.weibo.com/2',
lastSync: '未配置',
recordCount: 0,
syncFrequency: '按需调用',
dataCategory: '社交数据',
targetCollection: 'KR_微博.微博uid+手机'
},
{
id: 'webhook_feishu',
name: 'feishu_webhook',
nameCn: '飞书机器人',
description: '飞书机器人Webhook接收对话消息并触发查询',
type: 'webhook',
status: 'connected',
endpoint: '/api/feishu/webhook',
lastSync: '实时',
recordCount: 0,
syncFrequency: '实时',
dataCategory: '消息通道'
}
]
const allSources = [...mongoSources, ...externalSources]
return NextResponse.json({
success: true,
sources: allSources,
summary: {
total: allSources.length,
connected: allSources.filter(s => s.status === 'connected').length,
warning: allSources.filter(s => s.status === 'warning').length,
disconnected: allSources.filter(s => s.status === 'disconnected').length,
totalRecords: stats.totalDocuments,
totalSize: stats.totalSize,
latency: stats.latency
}
})
} catch (error: any) {
console.error('数据源 API 错误:', error)
return NextResponse.json({
success: false,
error: error.message,
sources: [],
summary: {
total: 0,
connected: 0,
warning: 0,
disconnected: 0,
totalRecords: 0,
totalSize: 0,
latency: 0
}
}, { status: 500 })
}
}
// POST: 测试数据源连接
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { type, config } = body
if (type === 'mongodb') {
const { MongoClient } = await import('mongodb')
const uri = config.uri || `mongodb://${config.username}:${config.password}@${config.host}/?authSource=admin`
const client = new MongoClient(uri, {
serverSelectionTimeoutMS: 5000
})
await client.connect()
await client.db().admin().ping()
await client.close()
return NextResponse.json({
success: true,
message: 'MongoDB 连接成功'
})
}
// 其他数据源类型暂未实现
return NextResponse.json({
success: false,
message: `${type} 类型暂未支持`
}, { status: 400 })
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}

View File

@@ -1,25 +1,195 @@
import { NextResponse } from "next/server"
import { getDatabases, getDatabaseStructure } from "@/lib/mongodb-mock-connector" // 更新导入路径
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient } from '@/lib/mongodb'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const database = searchParams.get("database")
if (database) {
// 获取指定数据库的结构
const structure = await getDatabaseStructure(database)
return NextResponse.json({ success: true, data: structure })
} else {
// 获取所有数据库列表
const databases = await getDatabases()
return NextResponse.json({ success: true, data: databases })
// 获取所有数据库结构
async function getDatabaseStructure() {
const client = await getMongoClient()
const admin = client.db().admin()
// 获取数据库列表
const dbList = await admin.listDatabases()
const krDatabases = dbList.databases.filter(db => db.name.startsWith('KR'))
const structure = []
for (const dbInfo of krDatabases.slice(0, 10)) { // 限制前10个
try {
const db = client.db(dbInfo.name)
const collections = await db.listCollections().toArray()
// 获取每个集合的字段示例
const collectionDetails = []
for (const col of collections.slice(0, 5)) { // 每个库最多5个集合
try {
const sample = await db.collection(col.name).findOne()
const fields = sample ? Object.keys(sample).filter(k => k !== '_id').slice(0, 10) : []
const count = await db.collection(col.name).estimatedDocumentCount()
collectionDetails.push({
name: col.name,
fields,
count
})
} catch {
// 忽略错误
}
}
structure.push({
database: dbInfo.name,
sizeGB: (dbInfo.sizeOnDisk / 1024 / 1024 / 1024).toFixed(2),
collections: collectionDetails
})
} catch {
// 忽略错误
}
}
return structure
}
// 生成血缘节点
function generateLineageNodes(structure: any[]) {
const nodes: any[] = []
const connections: any[] = []
let yOffset = 50
// 数据源节点 (左侧)
const colors = [
'from-blue-400 to-blue-600',
'from-orange-400 to-orange-600',
'from-green-400 to-green-600',
'from-red-400 to-red-600',
'from-purple-400 to-purple-600',
]
structure.forEach((db, i) => {
const mainCol = db.collections[0]
if (!mainCol) return
nodes.push({
id: `source_${db.database}`,
type: 'source',
name: db.database.replace('KR_', ''),
database: db.database,
collection: mainCol.name,
fields: mainCol.fields.slice(0, 5),
x: 50,
y: yOffset,
color: colors[i % colors.length],
count: mainCol.count,
sizeGB: db.sizeGB
})
yOffset += 140
})
// AI引擎节点 (中间)
nodes.push({
id: 'transform_ai',
type: 'transform',
name: 'AI标签引擎',
fields: ['phone_norm', 'qq_norm', 'rfm_score', 'user_level', 'tags'],
x: 400,
y: 150,
color: 'from-violet-400 to-violet-600'
})
nodes.push({
id: 'transform_clean',
type: 'transform',
name: '数据清洗',
fields: ['unique_id', 'merged_data', 'quality'],
x: 400,
y: 350,
color: 'from-yellow-400 to-yellow-600'
})
// 目标节点 (右侧)
nodes.push({
id: 'target_valuation',
type: 'target',
name: '用户估值',
database: 'KR',
collection: '用户估值',
fields: ['phone', 'qq', 'rfm_score', 'user_level', 'tags'],
x: 750,
y: 200,
color: 'from-emerald-400 to-emerald-600'
})
nodes.push({
id: 'target_portrait',
type: 'target',
name: '用户画像',
database: 'KR',
collection: '用户画像',
fields: ['user_id', 'portrait', 'behavior'],
x: 750,
y: 400,
color: 'from-cyan-400 to-cyan-600'
})
// 自动生成连接
structure.forEach(db => {
const sourceId = `source_${db.database}`
const mainCol = db.collections[0]
if (!mainCol) return
// 连接到AI引擎
if (mainCol.fields.includes('phone') || mainCol.fields.includes('手机')) {
connections.push({
id: `conn_${sourceId}_phone`,
sourceNode: sourceId,
sourceField: mainCol.fields.includes('phone') ? 'phone' : '手机',
targetNode: 'transform_ai',
targetField: 'phone_norm'
})
}
if (mainCol.fields.includes('qq') || mainCol.fields.includes('QQ')) {
connections.push({
id: `conn_${sourceId}_qq`,
sourceNode: sourceId,
sourceField: mainCol.fields.includes('qq') ? 'qq' : 'QQ',
targetNode: 'transform_ai',
targetField: 'qq_norm'
})
}
})
// AI引擎到目标
connections.push({
id: 'conn_ai_valuation',
sourceNode: 'transform_ai',
sourceField: 'rfm_score',
targetNode: 'target_valuation',
targetField: 'rfm_score'
})
connections.push({
id: 'conn_clean_portrait',
sourceNode: 'transform_clean',
sourceField: 'merged_data',
targetNode: 'target_portrait',
targetField: 'portrait'
})
return { nodes, connections }
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action') || 'structure'
try {
const structure = await getDatabaseStructure()
if (action === 'lineage') {
const lineage = generateLineageNodes(structure)
return NextResponse.json({ success: true, ...lineage })
}
return NextResponse.json({ success: true, databases: structure })
} catch (error) {
console.error("数据库结构查询失败:", error)
return NextResponse.json(
{ success: false, message: "数据库结构查询失败", error: (error as Error).message },
{ status: 500 },
)
console.error('数据库结构查询失败:', error)
return NextResponse.json({ error: '查询失败' }, { status: 500 })
}
}

325
app/api/monitoring/route.ts Normal file
View File

@@ -0,0 +1,325 @@
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient } from '@/lib/mongodb'
// 获取数据库详细状态
async function getDatabaseStats() {
try {
const client = await getMongoClient()
const adminDb = client.db('admin')
// 获取服务器状态
const serverStatus = await adminDb.command({ serverStatus: 1 })
// 获取所有KR_开头的数据库
const dbList = await adminDb.admin().listDatabases()
const krDatabases = dbList.databases.filter((db: any) =>
db.name.startsWith('KR')
)
// 统计各数据库信息
const databaseDetails = await Promise.all(
krDatabases.slice(0, 10).map(async (db: any) => {
try {
const database = client.db(db.name)
const stats = await database.command({ dbStats: 1 })
const collections = await database.listCollections().toArray()
return {
name: db.name,
sizeGB: (db.sizeOnDisk / (1024 * 1024 * 1024)).toFixed(2),
collections: collections.length,
objects: stats.objects || 0,
indexes: stats.indexes || 0,
avgObjSize: stats.avgObjSize || 0
}
} catch (e) {
return {
name: db.name,
sizeGB: (db.sizeOnDisk / (1024 * 1024 * 1024)).toFixed(2),
collections: 0,
objects: 0,
indexes: 0,
error: String(e)
}
}
})
)
return {
success: true,
server: {
version: serverStatus.version,
uptime: serverStatus.uptime,
uptimeHours: Math.floor(serverStatus.uptime / 3600),
host: serverStatus.host,
connections: {
current: serverStatus.connections?.current || 0,
available: serverStatus.connections?.available || 0,
totalCreated: serverStatus.connections?.totalCreated || 0
},
memory: {
resident: serverStatus.mem?.resident || 0,
virtual: serverStatus.mem?.virtual || 0,
mapped: serverStatus.mem?.mapped || 0
},
network: {
bytesIn: serverStatus.network?.bytesIn || 0,
bytesOut: serverStatus.network?.bytesOut || 0,
numRequests: serverStatus.network?.numRequests || 0
},
opcounters: {
insert: serverStatus.opcounters?.insert || 0,
query: serverStatus.opcounters?.query || 0,
update: serverStatus.opcounters?.update || 0,
delete: serverStatus.opcounters?.delete || 0
}
},
databases: {
total: krDatabases.length,
totalSizeGB: krDatabases.reduce((sum: number, db: any) =>
sum + db.sizeOnDisk / (1024 * 1024 * 1024), 0
).toFixed(2),
details: databaseDetails
}
}
} catch (error) {
return {
success: false,
error: String(error),
server: null,
databases: null
}
}
}
// 获取健康检查
async function getHealthStatus() {
const services = []
// 检查MongoDB
try {
const client = await getMongoClient()
const start = Date.now()
await client.db('admin').command({ ping: 1 })
const latency = Date.now() - start
services.push({
name: 'MongoDB',
status: latency < 100 ? 'healthy' : latency < 500 ? 'degraded' : 'unhealthy',
latency: `${latency}ms`,
message: latency < 100 ? '运行正常' : '响应较慢'
})
} catch (e) {
services.push({
name: 'MongoDB',
status: 'unhealthy',
latency: '-',
message: String(e)
})
}
// 检查卡若AI网关
try {
const gatewayUrl = process.env.GATEWAY_URL || 'http://localhost:8000'
const start = Date.now()
const res = await fetch(`${gatewayUrl}/health`, {
signal: AbortSignal.timeout(5000)
})
const latency = Date.now() - start
services.push({
name: '卡若AI网关',
status: res.ok ? 'healthy' : 'degraded',
latency: `${latency}ms`,
message: res.ok ? '运行正常' : '响应异常'
})
} catch (e) {
services.push({
name: '卡若AI网关',
status: 'unhealthy',
latency: '-',
message: '无法连接'
})
}
// 检查飞书服务
try {
const gatewayUrl = process.env.GATEWAY_URL || 'http://localhost:8000'
const res = await fetch(`${gatewayUrl}/feishu/test`, {
signal: AbortSignal.timeout(5000)
})
const data = await res.json()
services.push({
name: '飞书机器人',
status: data.status === 'success' ? 'healthy' : 'degraded',
latency: '-',
message: data.message || '未知状态'
})
} catch (e) {
services.push({
name: '飞书机器人',
status: 'unhealthy',
latency: '-',
message: '未配置或无法连接'
})
}
return {
success: true,
services,
overall: services.every(s => s.status === 'healthy') ? 'healthy' :
services.some(s => s.status === 'unhealthy') ? 'unhealthy' : 'degraded'
}
}
// 获取告警信息
async function getAlerts() {
// 从数据库状态生成告警
const alerts = []
try {
const client = await getMongoClient()
const adminDb = client.db('admin')
const serverStatus = await adminDb.command({ serverStatus: 1 })
// 检查连接数
const connCurrent = serverStatus.connections?.current || 0
const connAvailable = serverStatus.connections?.available || 0
if (connCurrent > connAvailable * 0.8) {
alerts.push({
id: 'conn-high',
type: 'warning',
message: `MongoDB连接数较高 (${connCurrent}/${connAvailable})`,
time: new Date().toISOString(),
status: 'active'
})
}
// 检查内存
const memResident = serverStatus.mem?.resident || 0
if (memResident > 8000) { // 8GB
alerts.push({
id: 'mem-high',
type: 'warning',
message: `MongoDB内存使用较高 (${(memResident/1024).toFixed(1)}GB)`,
time: new Date().toISOString(),
status: 'active'
})
}
} catch (e) {
alerts.push({
id: 'mongo-error',
type: 'error',
message: `MongoDB连接失败: ${String(e)}`,
time: new Date().toISOString(),
status: 'active'
})
}
// 默认告警(如果没有问题)
if (alerts.length === 0) {
alerts.push({
id: 'all-ok',
type: 'info',
message: '系统运行正常,无告警',
time: new Date().toISOString(),
status: 'resolved'
})
}
return {
success: true,
alerts,
activeCount: alerts.filter(a => a.status === 'active').length
}
}
// 获取业务指标
async function getBusinessMetrics() {
try {
const client = await getMongoClient()
// 从KR.用户估值获取统计
const krDb = client.db('KR')
const userCollection = krDb.collection('用户估值')
const totalUsers = await userCollection.estimatedDocumentCount()
// 获取用户等级分布(采样)
const levelStats = await userCollection.aggregate([
{ $sample: { size: 10000 } },
{ $match: { user_level: { $exists: true } } },
{ $group: { _id: '$user_level', count: { $sum: 1 } } }
], { maxTimeMS: 5000 }).toArray()
// 计算各等级占比
const levelDistribution = levelStats.map(l => ({
level: l._id || '未知',
count: l.count,
percentage: ((l.count / 10000) * 100).toFixed(1)
}))
return {
success: true,
metrics: {
totalUsers,
totalUsersFormatted: totalUsers >= 1000000000
? `${(totalUsers/1000000000).toFixed(2)}B`
: `${(totalUsers/1000000).toFixed(1)}M`,
levelDistribution,
lastUpdated: new Date().toISOString()
}
}
} catch (error) {
return {
success: false,
error: String(error)
}
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
switch (action) {
case 'databases':
return NextResponse.json(await getDatabaseStats())
case 'health':
return NextResponse.json(await getHealthStatus())
case 'alerts':
return NextResponse.json(await getAlerts())
case 'metrics':
return NextResponse.json(await getBusinessMetrics())
default:
// 返回综合状态
const [dbStats, health, alerts, metrics] = await Promise.all([
getDatabaseStats(),
getHealthStatus(),
getAlerts(),
getBusinessMetrics()
])
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
database: dbStats,
health,
alerts,
metrics
})
}
} catch (error) {
return NextResponse.json({
success: false,
error: String(error)
}, { status: 500 })
}
}

768
app/api/open-api/route.ts Normal file
View File

@@ -0,0 +1,768 @@
/**
* 神射手开放API接口
*
* 提供给第三方系统(存客宝、点了码等)调用的开放接口
* 支持数据流入、数据查询、标签完善、批量处理等功能
*
* @author 神射手团队
* @version 1.0.0
*/
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
// ==================== 类型定义 ====================
interface Partner {
id: string
name: string
apiKey: string
apiSecret: string
status: 'active' | 'inactive' | 'pending'
permissions: {
dataIngest: boolean
dataQuery: boolean
tagEnrich: boolean
batchProcess: boolean
}
quotaConfig: {
dailyLimit: number
monthlyLimit: number
rateLimit: number
}
}
interface APIResponse<T = any> {
success: boolean
data?: T
error?: string
code?: number
}
// ==================== 模拟数据存储 ====================
// 模拟接入方数据(实际应存储在数据库中)
const PARTNERS: Map<string, Partner> = new Map([
['sk-ckb-xxxxxxxxxx', {
id: 'ckb_001',
name: '存客宝',
apiKey: 'sk-ckb-xxxxxxxxxx',
apiSecret: 'sec-ckb-xxxxxxxxxx',
status: 'active',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: true,
batchProcess: true,
},
quotaConfig: {
dailyLimit: 10000,
monthlyLimit: 300000,
rateLimit: 100,
},
}],
['sk-dlm-xxxxxxxxxx', {
id: 'dlm_001',
name: '点了码',
apiKey: 'sk-dlm-xxxxxxxxxx',
apiSecret: 'sec-dlm-xxxxxxxxxx',
status: 'active',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: false,
batchProcess: false,
},
quotaConfig: {
dailyLimit: 5000,
monthlyLimit: 150000,
rateLimit: 50,
},
}],
])
// 模拟调用计数实际应使用Redis
const CALL_COUNTS: Map<string, { daily: number; monthly: number }> = new Map()
// 模拟批量任务存储
const BATCH_TASKS: Map<string, {
id: string
partnerId: string
type: string
status: 'pending' | 'processing' | 'completed' | 'failed'
progress: number
total: number
processed: number
failed: number
createdAt: Date
completedAt?: Date
}> = new Map()
// ==================== 工具函数 ====================
/**
* 验证API请求
*/
function validateRequest(request: NextRequest): { valid: boolean; partner?: Partner; error?: string } {
const authHeader = request.headers.get('Authorization')
const apiSecret = request.headers.get('X-API-Secret')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return { valid: false, error: '缺少Authorization头' }
}
const apiKey = authHeader.replace('Bearer ', '')
const partner = PARTNERS.get(apiKey)
if (!partner) {
return { valid: false, error: '无效的API Key' }
}
if (partner.status !== 'active') {
return { valid: false, error: '接入方已被禁用' }
}
// 验证API Secret可选增强安全性
if (apiSecret && apiSecret !== partner.apiSecret) {
return { valid: false, error: 'API Secret验证失败' }
}
// 检查配额
const counts = CALL_COUNTS.get(partner.id) || { daily: 0, monthly: 0 }
if (counts.daily >= partner.quotaConfig.dailyLimit) {
return { valid: false, error: '已达到每日调用限额' }
}
if (counts.monthly >= partner.quotaConfig.monthlyLimit) {
return { valid: false, error: '已达到每月调用限额' }
}
// 更新调用计数
CALL_COUNTS.set(partner.id, {
daily: counts.daily + 1,
monthly: counts.monthly + 1,
})
return { valid: true, partner }
}
/**
* 验证手机号格式
*/
function isValidPhone(phone: string): boolean {
return /^1[3-9]\d{9}$/.test(phone)
}
/**
* 脱敏处理
*/
function maskPhone(phone: string): string {
if (!phone || phone.length !== 11) return phone
return phone.slice(0, 3) + '****' + phone.slice(7)
}
function maskName(name: string): string {
if (!name) return name
if (name.length <= 1) return name
return name[0] + '*'.repeat(name.length - 1)
}
/**
* 模拟用户画像数据
*/
function getMockUserProfile(phone: string) {
// 模拟从数据库获取用户画像
const hash = crypto.createHash('md5').update(phone).digest('hex')
const seed = parseInt(hash.slice(0, 8), 16)
const levels = ['S', 'A', 'B', 'C', 'D']
const pools = ['钻石池', '黄金池', '白银池', '青铜池', '潜力池']
const tagOptions = [
'高价值用户', '优质用户', '电商活跃', '社交达人', '一线城市',
'二线城市', '高频用户', '沉默用户', '新用户', '流失风险',
'高消费', '低消费', '金融偏好', '科技爱好者', '时尚达人'
]
const levelIndex = seed % 5
const r = 1 + (seed % 5)
const f = 1 + ((seed >> 4) % 5)
const m = 1 + ((seed >> 8) % 5)
const rfmScore = Math.round(r * 0.3 + f * 0.3 + m * 0.4) * 20
// 随机选择3-5个标签
const numTags = 3 + (seed % 3)
const tags: string[] = []
for (let i = 0; i < numTags; i++) {
const tagIndex = (seed + i * 7) % tagOptions.length
if (!tags.includes(tagOptions[tagIndex])) {
tags.push(tagOptions[tagIndex])
}
}
return {
phone: maskPhone(phone),
name: maskName('张三'),
tags,
rfm: { r, f, m, score: rfmScore },
level: levels[levelIndex],
traffic_pool: pools[levelIndex],
data_sources: ['KR_存客宝', 'KR_腾讯'],
updated_at: new Date().toISOString(),
}
}
/**
* 模拟AI标签完善
*/
function enrichUserTags(phone: string, existingTags: string[] = []) {
const profile = getMockUserProfile(phone)
const allTags = [...new Set([...existingTags, ...profile.tags])]
const newTags = allTags.filter(t => !existingTags.includes(t))
return {
newTags,
allTags,
rfm_score: profile.rfm.score,
level: profile.level,
}
}
// ==================== API处理函数 ====================
/**
* 处理数据流入
*/
async function handleIngest(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.dataIngest) {
return { success: false, error: '无数据流入权限', code: 403 }
}
try {
const body = await request.json()
const { phone, name, source, tags = [], extra = {} } = body
if (!phone) {
return { success: false, error: '缺少phone参数', code: 400 }
}
if (!isValidPhone(phone)) {
return { success: false, error: '手机号格式错误', code: 400 }
}
if (!source) {
return { success: false, error: '缺少source参数', code: 400 }
}
// 模拟数据处理和标签完善
const enrichResult = enrichUserTags(phone, tags)
return {
success: true,
data: {
userId: `usr_${crypto.randomBytes(8).toString('hex')}`,
enriched: true,
originalTags: tags,
newTags: enrichResult.newTags,
allTags: enrichResult.allTags,
rfm_score: enrichResult.rfm_score,
user_level: enrichResult.level,
source,
processedAt: new Date().toISOString(),
},
}
} catch (error) {
return { success: false, error: '请求体解析失败', code: 400 }
}
}
/**
* 处理批量数据流入
*/
async function handleBatchIngest(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.dataIngest) {
return { success: false, error: '无数据流入权限', code: 403 }
}
if (!partner.permissions.batchProcess) {
return { success: false, error: '无批量处理权限', code: 403 }
}
try {
const body = await request.json()
const { users = [], source, async: isAsync = false } = body
if (!Array.isArray(users) || users.length === 0) {
return { success: false, error: '缺少users数组', code: 400 }
}
if (users.length > 1000) {
return { success: false, error: '单次最多支持1000条数据', code: 400 }
}
if (!source) {
return { success: false, error: '缺少source参数', code: 400 }
}
if (isAsync) {
// 创建异步任务
const taskId = `task_${crypto.randomBytes(8).toString('hex')}`
BATCH_TASKS.set(taskId, {
id: taskId,
partnerId: partner.id,
type: 'ingest',
status: 'processing',
progress: 0,
total: users.length,
processed: 0,
failed: 0,
createdAt: new Date(),
})
// 模拟异步处理(实际应使用队列)
setTimeout(() => {
const task = BATCH_TASKS.get(taskId)
if (task) {
task.status = 'completed'
task.progress = 100
task.processed = users.length
task.completedAt = new Date()
}
}, 5000)
return {
success: true,
data: {
taskId,
total: users.length,
processed: 0,
status: 'processing',
estimatedTime: `${Math.ceil(users.length / 100)}`,
},
}
} else {
// 同步处理
const results = users.map((user: { phone: string; name?: string; tags?: string[] }) => {
if (!user.phone || !isValidPhone(user.phone)) {
return { phone: user.phone, success: false, error: '手机号格式错误' }
}
const enrichResult = enrichUserTags(user.phone, user.tags || [])
return {
phone: maskPhone(user.phone),
success: true,
tags: enrichResult.allTags,
level: enrichResult.level,
}
})
const successful = results.filter((r: { success: boolean }) => r.success).length
const failed = results.length - successful
return {
success: true,
data: {
total: users.length,
processed: successful,
failed,
results,
},
}
}
} catch (error) {
return { success: false, error: '请求体解析失败', code: 400 }
}
}
/**
* 处理用户查询
*/
async function handleQuery(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.dataQuery) {
return { success: false, error: '无数据查询权限', code: 403 }
}
const { searchParams } = new URL(request.url)
const phone = searchParams.get('phone')
const qq = searchParams.get('qq')
const fields = searchParams.get('fields')?.split(',')
if (!phone && !qq) {
return { success: false, error: '缺少phone或qq参数', code: 400 }
}
// 优先使用手机号查询
const queryPhone = phone || `138${Math.floor(Math.random() * 100000000).toString().padStart(8, '0')}`
if (phone && !isValidPhone(phone)) {
return { success: false, error: '手机号格式错误', code: 400 }
}
const profile = getMockUserProfile(queryPhone)
// 根据fields参数过滤返回字段
if (fields && fields.length > 0) {
const filteredProfile: Record<string, any> = {}
fields.forEach(field => {
if (field in profile) {
filteredProfile[field] = (profile as Record<string, any>)[field]
}
})
filteredProfile.phone = profile.phone // 始终返回phone
return { success: true, data: filteredProfile }
}
return { success: true, data: profile }
}
/**
* 处理批量用户查询
*/
async function handleBatchQuery(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.dataQuery) {
return { success: false, error: '无数据查询权限', code: 403 }
}
try {
const body = await request.json()
const { phones = [], qqs = [], fields = [] } = body
const identifiers = [...phones, ...qqs]
if (identifiers.length === 0) {
return { success: false, error: '缺少phones或qqs参数', code: 400 }
}
if (identifiers.length > 500) {
return { success: false, error: '单次最多支持500条查询', code: 400 }
}
const users: any[] = []
const notFound: string[] = []
for (const phone of phones) {
if (!isValidPhone(phone)) {
notFound.push(phone)
continue
}
// 模拟30%的用户不存在
if (Math.random() > 0.7) {
notFound.push(phone)
continue
}
const profile = getMockUserProfile(phone)
if (fields.length > 0) {
const filteredProfile: Record<string, any> = { phone: profile.phone }
fields.forEach((field: string) => {
if (field in profile) {
filteredProfile[field] = (profile as Record<string, any>)[field]
}
})
users.push(filteredProfile)
} else {
users.push(profile)
}
}
return {
success: true,
data: {
users,
notFound,
total: phones.length,
found: users.length,
},
}
} catch (error) {
return { success: false, error: '请求体解析失败', code: 400 }
}
}
/**
* 处理标签完善
*/
async function handleTagEnrich(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.tagEnrich) {
return { success: false, error: '无标签完善权限', code: 403 }
}
try {
const body = await request.json()
const { phone, strategy = 'rfm', force = false } = body
if (!phone) {
return { success: false, error: '缺少phone参数', code: 400 }
}
if (!isValidPhone(phone)) {
return { success: false, error: '手机号格式错误', code: 400 }
}
const enrichResult = enrichUserTags(phone, [])
return {
success: true,
data: {
phone: maskPhone(phone),
strategy,
force,
newTags: enrichResult.newTags,
allTags: enrichResult.allTags,
rfm_score: enrichResult.rfm_score,
level: enrichResult.level,
enrichedAt: new Date().toISOString(),
},
}
} catch (error) {
return { success: false, error: '请求体解析失败', code: 400 }
}
}
/**
* 处理标签列表查询
*/
async function handleTagList(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.dataQuery) {
return { success: false, error: '无数据查询权限', code: 403 }
}
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const source = searchParams.get('source')
// 模拟标签列表
const allTags = [
{ id: 'tag_001', name: '高价值用户', category: 'value', count: 1250000 },
{ id: 'tag_002', name: '优质用户', category: 'value', count: 8500000 },
{ id: 'tag_003', name: '普通用户', category: 'value', count: 32000000 },
{ id: 'tag_004', name: '待激活用户', category: 'value', count: 68000000 },
{ id: 'tag_005', name: '高频活跃', category: 'behavior', count: 5600000 },
{ id: 'tag_006', name: '沉默用户', category: 'behavior', count: 12000000 },
{ id: 'tag_007', name: '新用户', category: 'behavior', count: 3200000 },
{ id: 'tag_008', name: '流失风险', category: 'behavior', count: 4500000 },
{ id: 'tag_009', name: '一线城市', category: 'region', count: 15000000 },
{ id: 'tag_010', name: '二线城市', category: 'region', count: 28000000 },
{ id: 'tag_011', name: '电商活跃', category: 'preference', count: 8900000 },
{ id: 'tag_012', name: '社交达人', category: 'preference', count: 4200000 },
{ id: 'tag_013', name: '金融偏好', category: 'preference', count: 2100000 },
{ id: 'tag_014', name: '科技爱好者', category: 'preference', count: 3600000 },
]
let filteredTags = allTags
if (category) {
filteredTags = filteredTags.filter(t => t.category === category)
}
return {
success: true,
data: {
tags: filteredTags,
total: filteredTags.length,
categories: ['value', 'behavior', 'region', 'preference'],
},
}
}
/**
* 处理批量任务创建
*/
async function handleBatchTask(request: NextRequest, partner: Partner): Promise<APIResponse> {
if (!partner.permissions.batchProcess) {
return { success: false, error: '无批量处理权限', code: 403 }
}
try {
const body = await request.json()
const { type, config, callback } = body
if (!type) {
return { success: false, error: '缺少type参数', code: 400 }
}
if (!['enrich', 'export', 'sync'].includes(type)) {
return { success: false, error: '不支持的任务类型', code: 400 }
}
const taskId = `task_${crypto.randomBytes(8).toString('hex')}`
BATCH_TASKS.set(taskId, {
id: taskId,
partnerId: partner.id,
type,
status: 'pending',
progress: 0,
total: 0,
processed: 0,
failed: 0,
createdAt: new Date(),
})
// 模拟异步处理
setTimeout(() => {
const task = BATCH_TASKS.get(taskId)
if (task) {
task.status = 'processing'
task.total = 10000
}
}, 1000)
setTimeout(() => {
const task = BATCH_TASKS.get(taskId)
if (task) {
task.status = 'completed'
task.progress = 100
task.processed = 9995
task.failed = 5
task.completedAt = new Date()
}
}, 10000)
return {
success: true,
data: {
taskId,
type,
status: 'pending',
estimatedTime: '10分钟',
callback: callback || null,
},
}
} catch (error) {
return { success: false, error: '请求体解析失败', code: 400 }
}
}
/**
* 处理批量任务状态查询
*/
async function handleBatchStatus(request: NextRequest, partner: Partner): Promise<APIResponse> {
const { searchParams } = new URL(request.url)
const taskId = searchParams.get('taskId')
if (!taskId) {
return { success: false, error: '缺少taskId参数', code: 400 }
}
const task = BATCH_TASKS.get(taskId)
if (!task) {
return { success: false, error: '任务不存在', code: 404 }
}
if (task.partnerId !== partner.id) {
return { success: false, error: '无权查看此任务', code: 403 }
}
return {
success: true,
data: {
taskId: task.id,
type: task.type,
status: task.status,
progress: task.progress,
total: task.total,
processed: task.processed,
failed: task.failed,
createdAt: task.createdAt.toISOString(),
completedAt: task.completedAt?.toISOString() || null,
},
}
}
/**
* 获取接入方列表
*/
async function handleGetPartners(): Promise<APIResponse> {
const partners = Array.from(PARTNERS.values()).map(p => ({
id: p.id,
name: p.name,
status: p.status,
permissions: p.permissions,
quotaConfig: p.quotaConfig,
}))
return {
success: true,
data: { partners, total: partners.length },
}
}
// ==================== 路由处理 ====================
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
// 获取接入方列表(管理接口,不需要认证)
if (action === 'partners') {
const result = await handleGetPartners()
return NextResponse.json(result)
}
// 其他GET请求需要认证
const validation = validateRequest(request)
if (!validation.valid) {
return NextResponse.json(
{ success: false, error: validation.error, code: 401 },
{ status: 401 }
)
}
const partner = validation.partner!
const endpoint = searchParams.get('endpoint')
let result: APIResponse
switch (endpoint) {
case 'query/user':
result = await handleQuery(request, partner)
break
case 'tag/list':
result = await handleTagList(request, partner)
break
case 'batch/status':
result = await handleBatchStatus(request, partner)
break
default:
result = { success: false, error: '未知的API端点', code: 404 }
}
return NextResponse.json(result, { status: result.code || 200 })
}
export async function POST(request: NextRequest) {
const validation = validateRequest(request)
if (!validation.valid) {
return NextResponse.json(
{ success: false, error: validation.error, code: 401 },
{ status: 401 }
)
}
const partner = validation.partner!
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint')
let result: APIResponse
switch (endpoint) {
case 'ingest/user':
result = await handleIngest(request, partner)
break
case 'ingest/batch':
result = await handleBatchIngest(request, partner)
break
case 'query/batch':
result = await handleBatchQuery(request, partner)
break
case 'tag/enrich':
result = await handleTagEnrich(request, partner)
break
case 'batch/task':
result = await handleBatchTask(request, partner)
break
default:
result = { success: false, error: '未知的API端点', code: 404 }
}
return NextResponse.json(result, { status: result.code || 200 })
}

656
app/api/portrait/route.ts Normal file
View File

@@ -0,0 +1,656 @@
/**
* 用户画像 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<UserPortrait | null> {
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<CrowdDistribution> {
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 })
}
}

411
app/api/tags/route.ts Normal file
View File

@@ -0,0 +1,411 @@
/**
* 标签体系 API
* 提供标签列表、标签统计、标签管理等功能
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient, getDatabase } from '@/lib/mongodb'
// 标签接口定义
interface Tag {
id: string
name: string
category: string
type: 'system' | 'custom' | 'ai'
userCount: number
description?: string
createdAt: string
updatedAt: string
source?: string
}
// 标签分类
const TAG_CATEGORIES = {
value: { name: '价值标签', icon: 'Star', color: 'yellow' },
behavior: { name: '行为标签', icon: 'Activity', color: 'blue' },
channel: { name: '渠道标签', icon: 'Share2', color: 'green' },
region: { name: '地域标签', icon: 'MapPin', color: 'purple' },
lifecycle: { name: '生命周期', icon: 'Clock', color: 'orange' },
preference: { name: '偏好标签', icon: 'Heart', color: 'red' }
}
// 从数据库获取标签统计(优化版:使用采样和限制)
async function getTagsFromDB(): Promise<Tag[]> {
try {
const client = await getMongoClient()
const db = client.db('KR')
const collection = db.collection('用户估值')
// 使用 $sample 采样代替全表扫描,大幅提升性能
const sampleSize = 100000 // 采样10万条估算分布
// 聚合统计用户等级分布(采样)
const levelStats = await collection.aggregate([
{ $sample: { size: sampleSize } },
{ $match: { user_level: { $exists: true, $ne: null } } },
{ $group: { _id: '$user_level', count: { $sum: 1 } } }
], { maxTimeMS: 10000 }).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: 20 }
], { maxTimeMS: 10000 }).toArray()
// 获取总文档数用于估算
const totalDocs = await collection.estimatedDocumentCount()
// 构建标签列表
const tags: Tag[] = []
let tagId = 1
// 价值标签(用户等级)- 按采样比例估算实际数量
const sampleRatio = totalDocs / sampleSize
for (const level of levelStats) {
if (level._id) {
tags.push({
id: `tag_${tagId++}`,
name: level._id,
category: 'value',
type: 'system',
userCount: Math.round(level.count * sampleRatio), // 估算实际数量
description: `${level._id}用户群体`,
createdAt: '2025-01-01',
updatedAt: new Date().toISOString().split('T')[0],
source: 'RFM计算'
})
}
}
// 地域标签
for (const province of provinceStats) {
if (province._id) {
tags.push({
id: `tag_${tagId++}`,
name: province._id,
category: 'region',
type: 'system',
userCount: Math.round(province.count * sampleRatio), // 估算实际数量
description: `来自${province._id}的用户`,
createdAt: '2025-01-01',
updatedAt: new Date().toISOString().split('T')[0],
source: '地域分析'
})
}
}
// 添加预定义的行为标签和生命周期标签
const predefinedTags: Partial<Tag>[] = [
{ name: '高频活跃', category: 'behavior', description: '近30天活跃次数>10', userCount: 0 },
{ name: '流失风险', category: 'lifecycle', description: '90天未活跃', userCount: 0 },
{ name: '新用户', category: 'lifecycle', description: '注册30天内', userCount: 0 },
{ name: '沉睡用户', category: 'lifecycle', description: '60天未活跃', userCount: 0 },
{ name: '微信渠道', category: 'channel', description: '通过微信获客', userCount: 0 },
{ name: '抖音渠道', category: 'channel', description: '通过抖音获客', userCount: 0 },
{ name: '内容偏好', category: 'preference', description: '偏好内容消费', userCount: 0 },
{ name: '社交偏好', category: 'preference', description: '偏好社交互动', userCount: 0 }
]
for (const tag of predefinedTags) {
tags.push({
id: `tag_${tagId++}`,
name: tag.name!,
category: tag.category!,
type: 'ai',
userCount: tag.userCount || Math.floor(Math.random() * 100000),
description: tag.description,
createdAt: '2025-01-01',
updatedAt: new Date().toISOString().split('T')[0],
source: 'AI标签引擎'
})
}
return tags
} catch (error) {
console.error('获取标签失败:', error)
return []
}
}
// GET: 获取标签列表
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const type = searchParams.get('type')
const search = searchParams.get('search')
const action = searchParams.get('action')
try {
// 获取分类统计
if (action === 'categories') {
const tags = await getTagsFromDB()
const categoryStats = Object.entries(TAG_CATEGORIES).map(([key, value]) => {
const categoryTags = tags.filter(t => t.category === key)
return {
id: key,
...value,
tagCount: categoryTags.length,
userCount: categoryTags.reduce((sum, t) => sum + t.userCount, 0)
}
})
return NextResponse.json({
success: true,
categories: categoryStats
})
}
// 获取标签列表
let tags = await getTagsFromDB()
// 筛选
if (category) {
tags = tags.filter(t => t.category === category)
}
if (type) {
tags = tags.filter(t => t.type === type)
}
if (search) {
const searchLower = search.toLowerCase()
tags = tags.filter(t =>
t.name.toLowerCase().includes(searchLower) ||
t.description?.toLowerCase().includes(searchLower)
)
}
// 排序(按用户数降序)
tags.sort((a, b) => b.userCount - a.userCount)
return NextResponse.json({
success: true,
tags,
total: tags.length,
categories: TAG_CATEGORIES
})
} catch (error: any) {
console.error('标签 API 错误:', error)
return NextResponse.json({
success: false,
error: error.message,
tags: [],
total: 0
}, { status: 500 })
}
}
// POST: 创建自定义标签
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { name, category, description, rules } = body
// 验证必填字段
if (!name || !category) {
return NextResponse.json({
success: false,
error: '标签名称和分类为必填项'
}, { status: 400 })
}
// 根据规则计算用户数
let userCount = 0
if (rules) {
try {
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
// 解析简单规则
const query = parseRuleToQuery(rules)
userCount = await collection.countDocuments(query)
} catch (e) {
console.log('规则解析失败,使用默认用户数')
}
}
// 创建标签
const newTag: Tag = {
id: `tag_custom_${Date.now()}`,
name,
category,
type: 'custom',
userCount,
description,
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
source: '手动创建'
}
// 保存到 MongoDB shensheshou.tags 集合
try {
const client = await getMongoClient()
await client.db('shensheshou').collection('tags').insertOne({
...newTag,
rules,
createdAt: new Date(),
updatedAt: new Date()
})
} catch (e) {
console.log('保存标签到数据库失败,使用内存存储')
}
return NextResponse.json({
success: true,
tag: newTag,
message: `标签创建成功,匹配 ${userCount.toLocaleString()} 个用户`
})
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}
// PUT: 更新标签
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { id, name, category, description, rules, enabled } = body
if (!id) {
return NextResponse.json({
success: false,
error: '标签ID为必填项'
}, { status: 400 })
}
// 根据规则重新计算用户数
let userCount = 0
if (rules) {
try {
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
const query = parseRuleToQuery(rules)
userCount = await collection.countDocuments(query)
} catch (e) {
console.log('规则解析失败')
}
}
// 更新数据库
try {
const client = await getMongoClient()
await client.db('shensheshou').collection('tags').updateOne(
{ id },
{
$set: {
...(name && { name }),
...(category && { category }),
...(description !== undefined && { description }),
...(rules !== undefined && { rules }),
...(enabled !== undefined && { enabled }),
...(userCount > 0 && { userCount }),
updatedAt: new Date()
}
},
{ upsert: true }
)
} catch (e) {
console.log('更新标签到数据库失败')
}
return NextResponse.json({
success: true,
message: '标签更新成功',
userCount
})
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}
// DELETE: 删除标签
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json({
success: false,
error: '标签ID为必填项'
}, { status: 400 })
}
// 从数据库删除
try {
const client = await getMongoClient()
const result = await client.db('shensheshou').collection('tags').deleteOne({ id })
return NextResponse.json({
success: true,
message: result.deletedCount > 0 ? '标签删除成功' : '标签不存在或已删除',
deleted: result.deletedCount > 0
})
} catch (e) {
// 如果是系统标签,不允许删除
if (id.startsWith('tag_') && !id.startsWith('tag_custom_')) {
return NextResponse.json({
success: false,
error: '系统标签不允许删除'
}, { status: 403 })
}
return NextResponse.json({
success: true,
message: '标签已标记删除(数据库未配置)'
})
}
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}
// 辅助函数解析规则字符串为MongoDB查询
function parseRuleToQuery(rules: string): any {
const query: any = {}
// 简单规则解析
// 支持格式: field >= value, field = value, field IN (...)
const patterns = [
{ regex: /(\w+)\s*>=\s*(\d+)/g, handler: (field: string, value: string) => ({ [field]: { $gte: parseInt(value) } }) },
{ regex: /(\w+)\s*<=\s*(\d+)/g, handler: (field: string, value: string) => ({ [field]: { $lte: parseInt(value) } }) },
{ regex: /(\w+)\s*>\s*(\d+)/g, handler: (field: string, value: string) => ({ [field]: { $gt: parseInt(value) } }) },
{ regex: /(\w+)\s*<\s*(\d+)/g, handler: (field: string, value: string) => ({ [field]: { $lt: parseInt(value) } }) },
{ regex: /(\w+)\s*=\s*'([^']+)'/g, handler: (field: string, value: string) => ({ [field]: value }) },
{ regex: /(\w+)\s*=\s*"([^"]+)"/g, handler: (field: string, value: string) => ({ [field]: value }) },
]
// 字段映射
const fieldMap: { [key: string]: string } = {
'rfm_score': 'user_evaluation_score',
'province': 'province',
'city': 'city',
'user_level': 'user_level',
}
for (const pattern of patterns) {
let match
while ((match = pattern.regex.exec(rules)) !== null) {
const field = fieldMap[match[1]] || match[1]
const value = match[2]
const condition = pattern.handler(field, value)
Object.assign(query, condition)
}
}
return query
}

View File

@@ -0,0 +1,283 @@
/**
* 流量包管理 API
* 提供流量包列表、创建、导出等功能
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMongoClient, maskPhone } from '@/lib/mongodb'
// 流量包接口
interface TrafficPackage {
id: string
name: string
description: string
userCount: number
conditions: {
userLevel?: string[]
province?: string[]
city?: string[]
rfmScoreRange?: { min: number; max: number }
tags?: string[]
}
status: 'active' | 'expired' | 'pending'
createdAt: string
updatedAt: string
createdBy: string
exportCount: number
lastExportAt?: string
}
// 预定义的流量池配置(基于 user_evaluation_score 字段分数范围0-5000+
const TRAFFIC_POOLS = {
diamond: { name: '钻石池', minScore: 3000, color: 'purple', icon: 'Diamond' },
gold: { name: '黄金池', minScore: 2000, maxScore: 3000, color: 'yellow', icon: 'Award' },
silver: { name: '白银池', minScore: 1000, maxScore: 2000, color: 'gray', icon: 'Medal' },
bronze: { name: '青铜池', minScore: 500, maxScore: 1000, color: 'orange', icon: 'Shield' },
potential: { name: '潜力池', maxScore: 500, color: 'blue', icon: 'TrendingUp' }
}
// 获取流量池统计(使用采样优化性能)
async function getTrafficPoolStats() {
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 poolStats = [
{ pool: 'potential', ...stats.find(s => s._id === 0) || { count: 0 } },
{ pool: 'bronze', ...stats.find(s => s._id === 500) || { count: 0 } },
{ pool: 'silver', ...stats.find(s => s._id === 1000) || { count: 0 } },
{ pool: 'gold', ...stats.find(s => s._id === 2000) || { count: 0 } },
{ pool: 'diamond', ...stats.find(s => s._id === 3000) || { count: 0 } }
].map(s => ({
...TRAFFIC_POOLS[s.pool as keyof typeof TRAFFIC_POOLS],
id: s.pool,
count: Math.round((s.count || 0) * sampleRatio), // 估算实际数量
avgScore: Math.round((s.avgScore || 0) * 100) / 100
}))
return poolStats
} catch (error) {
console.error('获取流量池统计失败:', error)
return []
}
}
// 根据条件查询用户
async function queryUsersByConditions(conditions: TrafficPackage['conditions']) {
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
const query: any = {}
if (conditions.userLevel?.length) {
query.user_level = { $in: conditions.userLevel }
}
if (conditions.province?.length) {
query.province = { $in: conditions.province }
}
if (conditions.city?.length) {
query.city = { $in: conditions.city }
}
if (conditions.rfmScoreRange) {
query.rfm_composite_score = {
$gte: conditions.rfmScoreRange.min,
$lte: conditions.rfmScoreRange.max
}
}
if (conditions.tags?.length) {
query.tags = { $in: conditions.tags }
}
const count = await collection.countDocuments(query)
const samples = await collection.find(query)
.limit(100)
.project({ phone: 1, name: 1, user_level: 1, province: 1, city: 1, rfm_composite_score: 1 })
.toArray()
return { count, samples }
}
// GET: 获取流量包列表或流量池统计
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const id = searchParams.get('id')
try {
// 获取流量池统计
if (action === 'pools') {
const poolStats = await getTrafficPoolStats()
return NextResponse.json({
success: true,
pools: poolStats
})
}
// 导出流量包用户数据
if (action === 'export') {
const packageId = searchParams.get('packageId')
const pool = searchParams.get('pool') || 'gold'
const client = await getMongoClient()
const collection = client.db('KR').collection('用户估值')
// 根据流量池类型获取用户
const scoreRange = {
diamond: { $gte: 3000 },
gold: { $gte: 2000, $lt: 3000 },
silver: { $gte: 1000, $lt: 2000 },
bronze: { $gte: 500, $lt: 1000 },
potential: { $lt: 500 }
}
const query = { user_evaluation_score: scoreRange[pool as keyof typeof scoreRange] || { $gte: 2000 } }
const users = await collection.find(query)
.limit(1000) // 限制导出数量
.project({
phone: '$phone_masked',
name: 1,
level: '$user_level',
score: '$user_evaluation_score',
tags: '$source_channels'
})
.toArray()
return NextResponse.json({
success: true,
users: users.map(u => ({
phone: u.phone || u.phone_masked || '未知',
name: u.name || '未知',
level: u.level || '-',
score: u.score || 0,
tags: u.tags || []
})),
total: users.length
})
}
// 获取流量包详情
if (id) {
// TODO: 从数据库获取流量包详情
return NextResponse.json({
success: true,
package: {
id,
name: '示例流量包',
description: '测试描述',
userCount: 1000,
conditions: {},
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdBy: 'admin',
exportCount: 0
}
})
}
// 获取流量包列表(预定义 + 流量池)
const poolStats = await getTrafficPoolStats()
const packages: TrafficPackage[] = poolStats.map(pool => ({
id: `pool_${pool.id}`,
name: `${pool.name}用户包`,
description: `RFM评分 ${pool.id === 'diamond' ? '≥80' : pool.id === 'potential' ? '<20' : `${TRAFFIC_POOLS[pool.id as keyof typeof TRAFFIC_POOLS].minScore || 0}-${TRAFFIC_POOLS[pool.id as keyof typeof TRAFFIC_POOLS].maxScore || 100}`} 的用户群体`,
userCount: pool.count,
conditions: {
rfmScoreRange: {
min: TRAFFIC_POOLS[pool.id as keyof typeof TRAFFIC_POOLS].minScore || 0,
max: TRAFFIC_POOLS[pool.id as keyof typeof TRAFFIC_POOLS].maxScore || 100
}
},
status: 'active',
createdAt: '2025-01-01',
updatedAt: new Date().toISOString().split('T')[0],
createdBy: 'system',
exportCount: 0
}))
return NextResponse.json({
success: true,
packages,
total: packages.length,
pools: poolStats
})
} 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 { name, description, conditions } = body
if (!name) {
return NextResponse.json({
success: false,
error: '流量包名称为必填项'
}, { status: 400 })
}
// 查询符合条件的用户数
const { count, samples } = await queryUsersByConditions(conditions || {})
const newPackage: TrafficPackage = {
id: `pkg_${Date.now()}`,
name,
description: description || '',
userCount: count,
conditions: conditions || {},
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdBy: 'admin',
exportCount: 0
}
// TODO: 保存到 MongoDB
return NextResponse.json({
success: true,
package: newPackage,
samples: samples.slice(0, 10).map(s => ({
...s,
phone: maskPhone(s.phone)
}))
})
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}