refactor: restructure project into 5 core modules

Organize project by 5 core modules based on requirement docs.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2026-01-31 04:18:24 +00:00
parent b4aa620c93
commit 22e725887a
74 changed files with 4219 additions and 9234 deletions

View File

@@ -1,77 +0,0 @@
import { NextRequest, NextResponse } from "next/server"
/**
* 本地登录 API - 支持邮箱/手机号 + 密码
* 当未配置 NEXT_PUBLIC_API_BASE_URL 时使用
* 开发账号: zhiqun@qq.com / Zhiqun1984
*/
const MOCK_USERS: Record<string, { password: string }> = {
"zhiqun@qq.com": { password: "Zhiqun1984" },
}
function isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const account = (formData.get("email") || formData.get("phone") || "").toString().trim()
const password = (formData.get("password") || "").toString()
const verificationCode = formData.get("verificationCode")?.toString()
if (!account) {
return NextResponse.json(
{ code: 40001, message: "请输入邮箱或手机号" },
{ status: 200 }
)
}
// 验证码登录开发环境下任意6位验证码通过
if (verificationCode) {
if (verificationCode.length >= 4) {
const token = `mock_token_${Date.now()}_${account}`
return NextResponse.json({
code: 10000,
message: "登录成功",
data: { token },
})
}
return NextResponse.json(
{ code: 40002, message: "验证码错误" },
{ status: 200 }
)
}
// 密码登录
if (!password) {
return NextResponse.json(
{ code: 40003, message: "请输入密码" },
{ status: 200 }
)
}
const key = isEmail(account) ? account : account
const user = MOCK_USERS[key]
if (user && user.password === password) {
const token = `mock_token_${Date.now()}_${account}`
return NextResponse.json({
code: 10000,
message: "登录成功",
data: { token },
})
}
return NextResponse.json(
{ code: 40004, message: "邮箱/手机号或密码错误" },
{ status: 200 }
)
} catch (error) {
console.error("[auth/login]", error)
return NextResponse.json(
{ code: 50000, message: "服务器错误" },
{ status: 500 }
)
}
}

View File

@@ -1,31 +0,0 @@
import { NextRequest, NextResponse } from "next/server"
/**
* 本地验证码发送 API (mock)
* 开发环境下直接返回成功验证码可为任意4位以上
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const phone = (formData.get("phone") || "").toString().trim()
if (!phone) {
return NextResponse.json(
{ code: 40001, message: "请输入手机号" },
{ status: 200 }
)
}
// Mock: 模拟发送成功,开发时可用 123456 等作为验证码
return NextResponse.json({
code: 10000,
message: "验证码已发送开发模式可使用任意4位以上数字",
})
} catch (error) {
console.error("[auth/send-code]", error)
return NextResponse.json(
{ code: 50000, message: "服务器错误" },
{ status: 500 }
)
}
}

View File

@@ -1,16 +1,12 @@
/**
* RFM 分析 API
* 支持 MongoDB 真实数据 + 内存计算
*/
import { NextResponse } from "next/server"
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
import { analyzeUserRFM } from "@/services/rfm-mongodb-service"
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
/**
* 可选 AI 标签增强
* 可选 AI 标签增强
* - 使用 AI SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
* - 无 OPENAI_API_KEY 时自动跳过,保持稳定
*/
async function aiTagging(chat_logs?: string[]) {
const text = (chat_logs ?? []).slice(0, 8).join("。")
@@ -37,38 +33,13 @@ export async function POST(req: Request) {
const body = await req.json()
const inputs: AnalyzeInput[] = Array.isArray(body) ? body : [body]
const useAI = (Array.isArray(body) ? (body as any).useAI : (body as any)?.useAI) ?? false
const useMongoData = (body as any)?.useMongoData ?? true
const results = []
for (const input of inputs) {
let base: any
let source = 'memory'
// 尝试从 MongoDB 获取真实数据
if (useMongoData && input.user_id) {
// 如果 user_id 是手机号格式,尝试从 MongoDB 查询
const phone = input.user_id.replace(/\D/g, '')
if (/^1[3-9]\d{9}$/.test(phone)) {
const mongoResult = await analyzeUserRFM(phone)
if (mongoResult.found) {
base = mongoResult.data
source = 'mongodb'
}
}
}
// 如果 MongoDB 没有数据,使用内存计算
if (!base) {
base = analyzeUser(input)
source = 'memory'
}
// AI 标签增强
const base = analyzeUser(input)
if (useAI) {
const ai = await aiTagging(input.chat_logs)
if (ai) {
base.tags = base.tags || {}
base.tags.emotion = ai.emotion ?? base.tags.emotion
base.tags.intent = ai.intent ?? base.tags.intent
if (Array.isArray(ai.behavior)) {
@@ -76,55 +47,10 @@ export async function POST(req: Request) {
}
}
}
results.push({ ...base, source })
results.push(base)
}
return NextResponse.json({ success: true, data: results })
} catch (e: any) {
console.error('RFM analyze error:', e)
return NextResponse.json({
success: false,
error: e?.message || "Invalid input"
}, { status: 400 })
}
}
/**
* GET 方法:按手机号查询用户 RFM
*/
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const phone = searchParams.get('phone')
if (!phone) {
return NextResponse.json({
success: false,
error: '请提供手机号参数'
}, { status: 400 })
}
const result = await analyzeUserRFM(phone)
if (!result.found) {
return NextResponse.json({
success: false,
error: '未找到该用户'
}, { status: 404 })
}
return NextResponse.json({
success: true,
data: result.data,
source: 'mongodb'
})
} catch (e: any) {
return NextResponse.json({
success: false,
error: e?.message || "查询失败"
}, { status: 500 })
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -1,51 +1,12 @@
/**
* RFM 标签获取 API
* 对接 MongoDB 真实数据
*/
import { NextResponse } from "next/server"
import { getRFMTagsDistribution } from "@/services/rfm-mongodb-service"
import { getUserTags } from "@/services/rfm-engine"
/**
* GET /api/rfm/get_tags
* 获取标签分布或指定用户的标签
*/
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url)
const userId = searchParams.get('user_id')
// 如果指定用户,返回用户标签
if (userId) {
const userTags = getUserTags(userId)
if (userTags) {
return NextResponse.json({
success: true,
data: userTags.tags,
source: 'memory'
})
}
return NextResponse.json({
success: false,
error: '未找到该用户标签'
}, { status: 404 })
}
// 否则返回标签分布
const distribution = await getRFMTagsDistribution()
return NextResponse.json({
success: true,
data: distribution,
source: 'mongodb'
})
} catch (error) {
console.error('Get tags error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 })
const url = new URL(req.url)
const userId = url.searchParams.get("user_id")
if (!userId) {
return NextResponse.json({ success: false, error: "missing user_id" }, { status: 400 })
}
const data = getUserTags(userId)
return NextResponse.json({ success: true, data })
}

View File

@@ -1,54 +1,7 @@
/**
* RFM 分组统计 API
* 对接 MongoDB 真实数据
*/
import { NextResponse } from "next/server"
import { getMongoRFMGroupSummary } from "@/services/rfm-mongodb-service"
import { getGroupSummary } from "@/services/rfm-engine"
export async function GET() {
try {
// 尝试从 MongoDB 获取真实数据
const mongoData = await getMongoRFMGroupSummary()
if (mongoData.totalUsers > 0) {
return NextResponse.json({
success: true,
data: {
gradeCount: mongoData.gradeCount,
valueCount: mongoData.valueCount,
lifecycleCount: {}, // MongoDB 暂无此字段
totalUsers: mongoData.totalUsers,
avgScore: mongoData.avgScore
},
source: 'mongodb'
})
}
// 回退到内存数据
const memData = getGroupSummary()
return NextResponse.json({
success: true,
data: memData,
source: 'memory'
})
} catch (error) {
console.error('RFM group summary error:', error)
// 返回默认数据
return NextResponse.json({
success: true,
data: {
gradeCount: { S: 0, A: 0, B: 0, C: 0, D: 0 },
valueCount: { '高': 0, '中': 0, '低': 0 },
lifecycleCount: {},
totalUsers: 0,
avgScore: 0
},
source: 'fallback',
error: error instanceof Error ? error.message : 'Unknown error'
})
}
const data = getGroupSummary()
return NextResponse.json({ success: true, data })
}

View File

@@ -1,177 +1,52 @@
/**
* 智能搜索 API 路由
* 对接神射手 MongoDB - 跨库查询
*/
import { type NextRequest, NextResponse } from "next/server"
import {
intelligentSearch,
queryFullProfile,
queryPhoneByQQ,
UserValuationDoc
} from "@/lib/mongodb"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
/**
* 脱敏手机号
*/
function maskPhone(phone: string | undefined): string {
if (!phone) return ''
if (phone.length !== 11) return phone
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
}
/**
* 转换搜索结果
*/
function transformSearchResult(doc: UserValuationDoc, queryType: string): any {
const name = doc.name || '未知用户'
return {
id: doc._id?.toString(),
type: 'user',
title: name,
subtitle: doc.phone_masked || maskPhone(doc.phone),
description: `${doc.province || ''}${doc.city || ''} | ${doc.user_level || '未分级'} | RFM: ${doc.rfm_composite_score?.toFixed(2) || 'N/A'}`,
data: {
phone: doc.phone,
phone_masked: doc.phone_masked || maskPhone(doc.phone),
name: doc.name,
province: doc.province,
city: doc.city,
userLevel: doc.user_level,
rfmScore: doc.rfm_composite_score,
tags: doc.tags || [],
email: doc.email
},
matchedBy: queryType,
relevanceScore: doc.rfm_composite_score || 0
}
}
/**
* GET /api/search
* 智能搜索
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const query = searchParams.get("q") || ""
const type = (searchParams.get("type") as "user" | "traffic" | "all") || "all"
const limit = Number.parseInt(searchParams.get("limit") || "50")
const offset = Number.parseInt(searchParams.get("offset") || "0")
const useAI = searchParams.get("ai") === "true"
const includeInsights = searchParams.get("insights") === "true"
if (!query.trim()) {
return NextResponse.json({
error: "搜索查询不能为空"
}, { status: 400 })
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
// 执行智能搜索
const result = await intelligentSearch(query, { limit, offset })
// 转换结果
const items = result.users.map(doc => transformSearchResult(doc, result.queryType))
// 如果是 QQ 查询,补充 QQ 信息
if (result.queryType === 'qq' && result.users.length > 0) {
const qqInfo = await queryPhoneByQQ(query.trim())
if (qqInfo) {
items[0].data.qq = qqInfo.qq
items[0].data.qqScore = qqInfo.QQ号评分
items[0].data.carrier = qqInfo.
}
}
const searchService = getIntelligentSearchService()
return NextResponse.json({
query,
queryType: result.queryType,
total: result.total,
items,
pagination: {
limit,
offset,
hasMore: offset + items.length < result.total
}
const results = await searchService.search(query, type, {
limit,
offset,
useAI,
includeInsights,
filters: {},
})
return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({
error: "搜索失败,请稍后重试",
details: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 })
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}
/**
* POST /api/search
* 高级搜索(支持更多参数)
* 返回格式适配前端 SearchResponse 接口
*/
export async function POST(request: NextRequest) {
try {
const startTime = Date.now()
const body = await request.json()
const { query, type = "all", options = {} } = body
if (!query || !query.trim()) {
return NextResponse.json({
error: "搜索查询不能为空"
}, { status: 400 })
return NextResponse.json({ error: "搜索查询不能为空" }, { status: 400 })
}
const limit = options.limit || 50
const offset = options.offset || 0
const result = await intelligentSearch(query, { limit, offset })
// 转换为前端期望的格式
const results = result.users.map(doc => {
const name = doc.name || '未知用户'
return {
id: doc._id?.toString() || '',
type: 'user' as const,
title: name,
description: `${doc.province || ''}${doc.city || ''} | 估值: ${doc.user_evaluation_score || 'N/A'}`,
tags: doc.tags || [],
relevanceScore: doc.user_evaluation_score || 0,
updatedAt: doc.computed_at?.toISOString() || new Date().toISOString(),
metadata: {
phone: doc.phone,
phone_masked: doc.phone_masked || maskPhone(doc.phone),
province: doc.province,
city: doc.city,
gender: doc.gender,
age_range: doc.age_range,
userLevel: doc.user_level,
rfmScore: doc.rfm_composite_score,
evaluationScore: doc.user_evaluation_score,
dataQuality: doc.data_quality
}
}
})
const searchService = getIntelligentSearchService()
const results = await searchService.search(query, type, options)
const queryTime = Date.now() - startTime
// 返回前端期望的 SearchResponse 格式
return NextResponse.json({
results,
stats: {
totalResults: result.total,
queryTime,
suggestions: [],
filters: {
queryType: result.queryType
}
},
hasMore: offset + results.length < result.total
})
return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({
error: "搜索失败,请稍后重试",
results: [],
stats: { totalResults: 0, queryTime: 0, suggestions: [], filters: {} },
hasMore: false
}, { status: 500 })
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
}
}

View File

@@ -1,78 +1,18 @@
/**
* 系统状态 API 路由
* 返回 MongoDB 数据库真实状态
*/
import { type NextRequest, NextResponse } from "next/server"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
import { NextResponse } from "next/server"
import { getDatabaseStats, healthCheck } from "@/lib/mongodb"
/**
* GET /api/system-status
* 获取系统状态
*/
export async function GET() {
export async function GET(request: NextRequest) {
try {
// 健康检查
const health = await healthCheck()
if (!health.mongodb) {
return NextResponse.json({
status: 'error',
connected: false,
latencyMs: health.latencyMs,
error: health.error || 'MongoDB 连接失败',
databases: [],
totalDocuments: 0,
totalSizeGB: 0,
lastCheck: new Date().toISOString()
}, { status: 503 })
}
// 获取数据库统计
const stats = await getDatabaseStats()
return NextResponse.json({
status: 'healthy',
connected: stats.connected,
latencyMs: health.latencyMs,
databases: stats.databases,
totalDocuments: stats.totalDocuments,
totalSizeGB: stats.totalSizeGB,
lastCheck: new Date().toISOString(),
// 格式化显示
summary: {
userCount: formatNumber(stats.totalDocuments),
dataSize: `${stats.totalSizeGB} GB`,
dbCount: stats.databases.length,
responseTime: `${health.latencyMs}ms`
}
}, {
headers: { 'Cache-Control': 'no-store, max-age=0' }
})
} catch (error) {
console.error('System status error:', error)
return NextResponse.json({
status: 'error',
connected: false,
error: error instanceof Error ? error.message : 'Unknown error',
lastCheck: new Date().toISOString()
}, { status: 500 })
}
}
const mindsDB = getMindsDBConnector()
const status = await mindsDB.getSystemStatus()
/**
* 格式化数字显示
*/
function formatNumber(num: number): string {
if (num >= 1000000000) {
return `${(num / 1000000000).toFixed(2)}B`
return NextResponse.json({
success: true,
status,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error("系统状态API错误:", error)
return NextResponse.json({ error: "获取系统状态失败" }, { status: 500 })
}
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`
}
return num.toString()
}

View File

@@ -1,186 +1,233 @@
/**
* 用户 API 路由
* 对接神射手 MongoDB 数据库 - KR.用户估值
*/
import { NextResponse, type NextRequest } from "next/server"
import type { TrafficUser } from "@/types/traffic"
import { addUser, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
import { NextResponse, NextRequest } from "next/server"
import {
queryUserList,
queryUserByPhone,
queryFullProfile,
UserValuationDoc
} from "@/lib/mongodb"
// 中文名字生成器数据
const familyNames = [
"张",
"王",
"李",
"赵",
"陈",
"刘",
"杨",
"黄",
"周",
"吴",
"朱",
"孙",
"马",
"胡",
"郭",
"林",
"何",
"高",
"梁",
"郑",
"罗",
"宋",
"谢",
"唐",
"韩",
"曹",
"许",
"邓",
"萧",
"冯",
]
const givenNames1 = [
"志",
"建",
"文",
"明",
"永",
"春",
"秀",
"金",
"水",
"玉",
"国",
"立",
"德",
"海",
"和",
"荣",
"伟",
"新",
"英",
"佳",
]
const givenNames2 = [
"华",
"平",
"军",
"强",
"辉",
"敏",
"峰",
"磊",
"超",
"艳",
"娜",
"霞",
"燕",
"娟",
"静",
"丽",
"涛",
"洋",
"勇",
"龙",
]
/**
* 脱敏手机号
*/
function maskPhone(phone: string | undefined): string {
if (!phone) return ''
if (phone.length !== 11) return phone
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
}
// 生成固定的用户数据池
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 transformUser(doc: UserValuationDoc, index: number = 0): any {
const name = doc.name || '未知用户'
return {
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,
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: [],
}
}
})
/**
* GET /api/users
* 查询用户列表或单个用户详情
*/
export async function GET(req: NextRequest) {
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 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 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' }
})
}
})
}
/**
* POST /api/users
* 创建用户(预留接口)
*/
export async function POST(req: NextRequest) {
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 })
}
// 微信好友数据缓存
const wechatFriendsCache = new Map<string, TrafficUser[]>()
function parseArrayParam(v: string | null) {
if (!v) return []
return v
.split(",")
.map((s) => s.trim())
.filter(Boolean)
}
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" } })
}
// 列表
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 UserStatus[]) : undefined
const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
return NextResponse.json(result, { headers: { "Cache-Control": "no-store" } })
}
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => ({}))
const created = addUser(body ?? {})
return NextResponse.json({ data: created }, { status: 201 })
}