284 lines
8.4 KiB
TypeScript
284 lines
8.4 KiB
TypeScript
/**
|
||
* 流量包管理 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 })
|
||
}
|
||
}
|