chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
303
app/api/ai-tagging/route.ts
Normal file
303
app/api/ai-tagging/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user