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

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -4,10 +4,12 @@ import type React from "react"
import "./globals.css"
import { Inter } from "next/font/google"
import { useState, useEffect } from "react"
import { usePathname } from "next/navigation"
import Sidebar from "./components/Sidebar"
import MobileHeader from "./components/MobileHeader"
import MobileSidebar from "./components/MobileSidebar"
import BottomNav from "./components/BottomNav"
import { Toaster } from "@/components/ui/toaster"
const inter = Inter({ subsets: ["latin"] })
@@ -16,8 +18,10 @@ export default function ClientLayout({
}: {
children: React.ReactNode
}) {
const pathname = usePathname()
const [isMobile, setIsMobile] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(false)
const isLoginPage = pathname === "/login"
useEffect(() => {
const checkMobile = () => {
@@ -30,6 +34,21 @@ export default function ClientLayout({
return () => window.removeEventListener("resize", checkMobile)
}, [])
if (isLoginPage) {
return (
<html lang="zh-CN">
<head>
<title> - </title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
</head>
<body className={inter.className}>
{children}
<Toaster />
</body>
</html>
)
}
return (
<html lang="zh-CN">
<head>
@@ -65,6 +84,8 @@ export default function ClientLayout({
{/* 移动端底部导航 */}
{isMobile && <BottomNav />}
<Toaster />
</body>
</html>
)

View File

@@ -0,0 +1,77 @@
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

@@ -0,0 +1,31 @@
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,12 +1,16 @@
/**
* 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 SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
* - 无 OPENAI_API_KEY 时自动跳过,保持稳定
* 可选 AI 标签增强
*/
async function aiTagging(chat_logs?: string[]) {
const text = (chat_logs ?? []).slice(0, 8).join("。")
@@ -33,13 +37,38 @@ 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) {
const base = analyzeUser(input)
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 标签增强
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)) {
@@ -47,10 +76,55 @@ export async function POST(req: Request) {
}
}
}
results.push(base)
results.push({ ...base, source })
}
return NextResponse.json({ success: true, data: results })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
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 })
}
}

View File

@@ -1,12 +1,51 @@
/**
* 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) {
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 })
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 data = getUserTags(userId)
return NextResponse.json({ success: true, data })
}

View File

@@ -1,7 +1,54 @@
/**
* 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() {
const data = getGroupSummary()
return NextResponse.json({ success: true, data })
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'
})
}
}

View File

@@ -1,52 +1,177 @@
import { type NextRequest, NextResponse } from "next/server"
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
/**
* 智能搜索 API 路由
* 对接神射手 MongoDB - 跨库查询
*/
import { type NextRequest, NextResponse } from "next/server"
import {
intelligentSearch,
queryFullProfile,
queryPhoneByQQ,
UserValuationDoc
} from "@/lib/mongodb"
/**
* 脱敏手机号
*/
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 searchService = getIntelligentSearchService()
// 执行智能搜索
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 results = await searchService.search(query, type, {
limit,
offset,
useAI,
includeInsights,
filters: {},
return NextResponse.json({
query,
queryType: result.queryType,
total: result.total,
items,
pagination: {
limit,
offset,
hasMore: offset + items.length < result.total
}
})
return NextResponse.json(results)
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
return NextResponse.json({
error: "搜索失败,请稍后重试",
details: error instanceof Error ? error.message : 'Unknown 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 searchService = getIntelligentSearchService()
const results = await searchService.search(query, type, options)
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
}
}
})
return NextResponse.json(results)
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
})
} catch (error) {
console.error("搜索API错误:", error)
return NextResponse.json({ error: "搜索失败,请稍后重试" }, { status: 500 })
return NextResponse.json({
error: "搜索失败,请稍后重试",
results: [],
stats: { totalResults: 0, queryTime: 0, suggestions: [], filters: {} },
hasMore: false
}, { status: 500 })
}
}

View File

@@ -1,18 +1,78 @@
import { type NextRequest, NextResponse } from "next/server"
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
/**
* 系统状态 API 路由
* 返回 MongoDB 数据库真实状态
*/
export async function GET(request: NextRequest) {
import { NextResponse } from "next/server"
import { getDatabaseStats, healthCheck } from "@/lib/mongodb"
/**
* GET /api/system-status
* 获取系统状态
*/
export async function GET() {
try {
const mindsDB = getMindsDBConnector()
const status = await mindsDB.getSystemStatus()
// 健康检查
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({
success: true,
status,
timestamp: new Date().toISOString(),
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("系统状态API错误:", error)
return NextResponse.json({ error: "获取系统状态失败" }, { status: 500 })
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 })
}
}
/**
* 格式化数字显示
*/
function formatNumber(num: number): string {
if (num >= 1000000000) {
return `${(num / 1000000000).toFixed(2)}B`
}
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`
}
return num.toString()
}

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

View File

@@ -2,7 +2,7 @@
import type React from "react"
import { useState, useEffect } from "react"
import { Eye, EyeOff, Phone } from "lucide-react"
import { Eye, EyeOff, Mail, Phone } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -12,8 +12,8 @@ import { WeChatIcon } from "@/components/icons/wechat-icon"
import { AppleIcon } from "@/components/icons/apple-icon"
import { useToast } from "@/components/ui/use-toast"
// 使用环境变量获取API域名
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"
// 使用环境变量:不配置则用本地 API支持 zhiqun@qq.com / Zhiqun1984
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""
// 定义登录响应类型
interface LoginResponse {
@@ -25,7 +25,7 @@ interface LoginResponse {
}
interface LoginForm {
phone: string
account: string // 邮箱或手机号
password: string
verificationCode: string
agreeToTerms: boolean
@@ -36,7 +36,7 @@ export default function LoginPage() {
const [activeTab, setActiveTab] = useState<"password" | "verification">("password")
const [isLoading, setIsLoading] = useState(false)
const [form, setForm] = useState<LoginForm>({
phone: "",
account: "",
password: "",
verificationCode: "",
agreeToTerms: false,
@@ -55,11 +55,11 @@ export default function LoginPage() {
}
const validateForm = () => {
if (!form.phone) {
if (!form.account.trim()) {
toast({
variant: "destructive",
title: "请输入手机号",
description: "手机号不能为空",
title: "请输入号",
description: "请输入邮箱或手机号",
})
return false
}
@@ -101,9 +101,9 @@ export default function LoginPage() {
setIsLoading(true)
try {
// 创建FormData对象
const formData = new FormData()
formData.append("phone", form.phone)
const isEmail = form.account.includes("@")
formData.append(isEmail ? "email" : "phone", form.account)
if (activeTab === "password") {
formData.append("password", form.password)
@@ -111,8 +111,8 @@ export default function LoginPage() {
formData.append("verificationCode", form.verificationCode)
}
// 发送登录请求
const response = await fetch(`${API_BASE_URL}/auth/login`, {
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/api/auth/login"
const response = await fetch(apiUrl, {
method: "POST",
body: formData,
// 不需要设置Content-Type浏览器会自动设置为multipart/form-data并添加boundary
@@ -125,7 +125,7 @@ export default function LoginPage() {
localStorage.setItem("token", result.data.token)
// 成功后跳转
router.push("/profile")
router.push("/")
toast({
title: "登录成功",
@@ -146,7 +146,7 @@ export default function LoginPage() {
}
const handleSendVerificationCode = async () => {
if (!form.phone) {
if (!form.account.trim()) {
toast({
variant: "destructive",
title: "请输入手机号",
@@ -157,12 +157,11 @@ export default function LoginPage() {
setIsLoading(true)
try {
// 创建FormData对象
const formData = new FormData()
formData.append("phone", form.phone)
formData.append("phone", form.account)
// 发送验证码请求
const response = await fetch(`${API_BASE_URL}/auth/send-code`, {
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/send-code` : "/api/auth/send-code"
const response = await fetch(apiUrl, {
method: "POST",
body: formData,
})
@@ -220,22 +219,25 @@ export default function LoginPage() {
</TabsList>
<div className="mt-8">
<p className="text-gray-600 mb-6"> / / Apple </p>
<p className="text-gray-600 mb-6"> / / / Apple </p>
<form onSubmit={handleLogin} className="space-y-6">
<div className="relative">
<Input
type="tel"
name="phone"
value={form.phone}
type="text"
name="account"
value={form.account}
onChange={handleInputChange}
placeholder="手机号"
className="pl-16 border-gray-300 text-gray-900 h-12"
placeholder="邮箱或手机号"
className="pl-12 border-gray-300 text-gray-900 h-12"
disabled={isLoading}
/>
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 flex items-center gap-1">
<Phone className="h-4 w-4" />
+86
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">
{form.account.includes("@") ? (
<Mail className="h-4 w-4" />
) : (
<Phone className="h-4 w-4" />
)}
</span>
</div>