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:
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user