chore: 以本地为准,强制同步全部内容

This commit is contained in:
卡若
2026-01-31 12:07:46 +08:00
parent ce0a716d02
commit b4aa620c93
44 changed files with 7140 additions and 314 deletions

View File

@@ -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 })
}
}