chore: 以本地为准,强制同步全部内容
This commit is contained in:
@@ -1,230 +1,186 @@
|
||||
/**
|
||||
* 用户 API 路由
|
||||
* 对接神射手 MongoDB 数据库 - KR.用户估值
|
||||
*/
|
||||
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
import { addUser, filterUsers, getDistinctTags, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
|
||||
import {
|
||||
queryUserList,
|
||||
queryUserByPhone,
|
||||
queryFullProfile,
|
||||
UserValuationDoc
|
||||
} from "@/lib/mongodb"
|
||||
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
"张",
|
||||
"王",
|
||||
"李",
|
||||
"赵",
|
||||
"陈",
|
||||
"刘",
|
||||
"杨",
|
||||
"黄",
|
||||
"周",
|
||||
"吴",
|
||||
"朱",
|
||||
"孙",
|
||||
"马",
|
||||
"胡",
|
||||
"郭",
|
||||
"林",
|
||||
"何",
|
||||
"高",
|
||||
"梁",
|
||||
"郑",
|
||||
"罗",
|
||||
"宋",
|
||||
"谢",
|
||||
"唐",
|
||||
"韩",
|
||||
"曹",
|
||||
"许",
|
||||
"邓",
|
||||
"萧",
|
||||
"冯",
|
||||
]
|
||||
const givenNames1 = [
|
||||
"志",
|
||||
"建",
|
||||
"文",
|
||||
"明",
|
||||
"永",
|
||||
"春",
|
||||
"秀",
|
||||
"金",
|
||||
"水",
|
||||
"玉",
|
||||
"国",
|
||||
"立",
|
||||
"德",
|
||||
"海",
|
||||
"和",
|
||||
"荣",
|
||||
"伟",
|
||||
"新",
|
||||
"英",
|
||||
"佳",
|
||||
]
|
||||
const givenNames2 = [
|
||||
"华",
|
||||
"平",
|
||||
"军",
|
||||
"强",
|
||||
"辉",
|
||||
"敏",
|
||||
"峰",
|
||||
"磊",
|
||||
"超",
|
||||
"艳",
|
||||
"娜",
|
||||
"霞",
|
||||
"燕",
|
||||
"娟",
|
||||
"静",
|
||||
"丽",
|
||||
"涛",
|
||||
"洋",
|
||||
"勇",
|
||||
"龙",
|
||||
]
|
||||
|
||||
// 生成固定的用户数据池
|
||||
const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去7天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 7))
|
||||
/**
|
||||
* 脱敏手机号
|
||||
*/
|
||||
function maskPhone(phone: string | undefined): string {
|
||||
if (!phone) return ''
|
||||
if (phone.length !== 11) return phone
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换用户数据格式(适配前端)
|
||||
*/
|
||||
function transformUser(doc: UserValuationDoc, index: number = 0): any {
|
||||
const name = doc.name || '未知用户'
|
||||
return {
|
||||
id: `${Date.now()}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页"][Math.floor(Math.random() * 6)],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
id: doc._id?.toString() || `user-${index}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${name[0] || 'U'}`,
|
||||
nickname: name,
|
||||
wechatId: doc.phone ? `wxid_${doc.phone.slice(-8)}` : '',
|
||||
phone: doc.phone || '',
|
||||
phone_masked: doc.phone_masked || maskPhone(doc.phone),
|
||||
region: doc.province && doc.city ? `${doc.province}${doc.city}` : (doc.province || '未知'),
|
||||
note: '',
|
||||
status: 'added' as const,
|
||||
addTime: doc.created_at?.toISOString() || new Date().toISOString(),
|
||||
source: (doc.source_channels && doc.source_channels[0]) || '神射手',
|
||||
assignedTo: '',
|
||||
category: 'customer' as const,
|
||||
tags: doc.tags || [],
|
||||
// RFM 数据
|
||||
userLevel: doc.user_level || 'D',
|
||||
rfmScore: doc.rfm_composite_score || 0,
|
||||
rfmR: doc.rfm_r_score,
|
||||
rfmF: doc.rfm_f_score,
|
||||
rfmM: doc.rfm_m_score,
|
||||
totalAmount: doc.total_amount || 0,
|
||||
orderCount: doc.order_count || 0,
|
||||
// 额外信息
|
||||
email: doc.email,
|
||||
address: doc.address,
|
||||
province: doc.province,
|
||||
city: doc.city,
|
||||
}
|
||||
})
|
||||
|
||||
// 计算今日新增数量
|
||||
const todayStart = new Date()
|
||||
todayStart.setHours(0, 0, 0, 0)
|
||||
const todayUsers = userPool.filter((user) => new Date(user.addTime) >= todayStart)
|
||||
|
||||
// 生成微信好友数据池
|
||||
const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去30天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 30))
|
||||
|
||||
return {
|
||||
id: `wechat-${wechatId}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页", "微信好友"][
|
||||
Math.floor(Math.random() * 7)
|
||||
],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 微信好友数据缓存
|
||||
const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
function parseArrayParam(v: string | null) {
|
||||
if (!v) return []
|
||||
return v.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/users
|
||||
* 查询用户列表或单个用户详情
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
// 详情优先
|
||||
const id = searchParams.get('id')
|
||||
if (id) {
|
||||
const detail = getUserById(id)
|
||||
return NextResponse.json({ data: detail }, { headers: { 'Cache-Control': 'no-store' } })
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
// 单用户详情查询(按ID或手机号)
|
||||
const id = searchParams.get('id')
|
||||
const phone = searchParams.get('phone')
|
||||
|
||||
if (id || phone) {
|
||||
// 如果是手机号格式,按手机号查询
|
||||
const queryPhone = phone || (id && /^1[3-9]\d{9}$/.test(id) ? id : null)
|
||||
|
||||
if (queryPhone) {
|
||||
// 完整画像查询(跨库)
|
||||
const profile = await queryFullProfile(queryPhone)
|
||||
|
||||
if (profile.valuation) {
|
||||
const user = transformUser(profile.valuation)
|
||||
|
||||
// 补充 QQ 信息
|
||||
if (profile.qq) {
|
||||
user.qq = profile.qq.qq
|
||||
user.qqScore = profile.qq.QQ号评分
|
||||
user.phoneScore = profile.qq.手机号评分
|
||||
}
|
||||
|
||||
// 补充存客宝信息
|
||||
if (profile.ckb) {
|
||||
user.wechat = profile.ckb.social_accounts?.wechat
|
||||
user.trafficPool = profile.ckb.traffic_pool?.pool_name
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: user,
|
||||
sources: {
|
||||
valuation: !!profile.valuation,
|
||||
qq: !!profile.qq,
|
||||
ckb: !!profile.ckb
|
||||
}
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: null,
|
||||
error: '未找到该用户'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data: null,
|
||||
error: '无效的查询参数'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 列表查询
|
||||
const q = searchParams.get('q') || undefined
|
||||
const tagsStr = searchParams.get('tags') || ''
|
||||
const userLevel = searchParams.get('userLevel') || searchParams.get('status') || undefined
|
||||
const rfmMin = searchParams.get('rfmMin') ? Number(searchParams.get('rfmMin')) : undefined
|
||||
const rfmMax = searchParams.get('rfmMax') ? Number(searchParams.get('rfmMax')) : undefined
|
||||
const page = Number(searchParams.get('page') ?? 1)
|
||||
const pageSize = Number(searchParams.get('pageSize') ?? 20)
|
||||
|
||||
const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
|
||||
|
||||
const result = await queryUserList({
|
||||
page,
|
||||
pageSize,
|
||||
userLevel,
|
||||
minRfm: rfmMin,
|
||||
maxRfm: rfmMax,
|
||||
search: q,
|
||||
tags
|
||||
})
|
||||
|
||||
const transformedData = result.data.map((doc, i) => transformUser(doc, i))
|
||||
|
||||
return NextResponse.json({
|
||||
data: transformedData,
|
||||
total: result.total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(result.total / pageSize)
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Users API error:', error)
|
||||
|
||||
// 数据库连接失败时返回模拟数据
|
||||
return NextResponse.json({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
totalPages: 0,
|
||||
error: error instanceof Error ? error.message : '查询失败',
|
||||
fallback: true
|
||||
}, {
|
||||
status: 500,
|
||||
headers: { 'Cache-Control': 'no-store' }
|
||||
})
|
||||
}
|
||||
|
||||
// 列表
|
||||
const q = searchParams.get('q') ?? undefined
|
||||
const tagsStr = searchParams.get('tags') ?? ''
|
||||
const statusStr = searchParams.get('status') ?? ''
|
||||
const rfmMin = Number(searchParams.get('rfmMin') ?? 0)
|
||||
const rfmMax = Number(searchParams.get('rfmMax') ?? 100)
|
||||
const page = Number(searchParams.get('page') ?? 1)
|
||||
const pageSize = Number(searchParams.get('pageSize') ?? 20)
|
||||
|
||||
const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
|
||||
const status = statusStr ? (statusStr.split(',').filter(Boolean) as any) : undefined
|
||||
|
||||
const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
|
||||
return NextResponse.json(result, { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users
|
||||
* 创建用户(预留接口)
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const created = addUser(body ?? {})
|
||||
return NextResponse.json({ data: created }, { status: 201 })
|
||||
try {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
|
||||
// TODO: 实现用户创建逻辑
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户创建功能暂未开放'
|
||||
}, { status: 501 })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
error: error instanceof Error ? error.message : '创建失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user