Files
users/app/api/traffic-packages/route.ts

284 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 流量包管理 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 })
}
}