feat: sync Sidebar and BottomNav, standardize user profile API
Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
56
app/api/rfm/analyze/route.ts
Normal file
56
app/api/rfm/analyze/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
import { generateText } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
/**
|
||||
* 可选 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("。")
|
||||
if (!text) return null
|
||||
if (!process.env.OPENAI_API_KEY) return null
|
||||
try {
|
||||
const { text: out } = await generateText({
|
||||
model: openai("gpt-4o"),
|
||||
system:
|
||||
"请根据用户聊天片段输出JSON,字段:emotion(积极|中性|消极), intent(弱意图|中等意图|强意图), behavior(数组,中文标签最多3个)。只输出JSON。",
|
||||
prompt: `聊天片段:${text}`,
|
||||
// @ts-expect-error ai sdk typings are flexible in Next.js
|
||||
maxTokens: 200,
|
||||
})
|
||||
const json = JSON.parse(out.trim())
|
||||
return json
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
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 results = []
|
||||
for (const input of inputs) {
|
||||
const base = analyzeUser(input)
|
||||
if (useAI) {
|
||||
const ai = await aiTagging(input.chat_logs)
|
||||
if (ai) {
|
||||
base.tags.emotion = ai.emotion ?? base.tags.emotion
|
||||
base.tags.intent = ai.intent ?? base.tags.intent
|
||||
if (Array.isArray(ai.behavior)) {
|
||||
base.tags.behavior = Array.from(new Set([...(base.tags.behavior ?? []), ...ai.behavior]))
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(base)
|
||||
}
|
||||
return NextResponse.json({ success: true, data: results })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
13
app/api/rfm/dump_csv/route.ts
Normal file
13
app/api/rfm/dump_csv/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { dumpCsv } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const csv = dumpCsv()
|
||||
return new NextResponse(csv, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="rfm_export_${Date.now()}.csv"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
12
app/api/rfm/get_tags/route.ts
Normal file
12
app/api/rfm/get_tags/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getUserTags } from "@/services/rfm-engine"
|
||||
|
||||
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 })
|
||||
}
|
||||
const data = getUserTags(userId)
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
7
app/api/rfm/group_summary/route.ts
Normal file
7
app/api/rfm/group_summary/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getGroupSummary } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
const data = getGroupSummary()
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
12
app/api/rfm/score/route.ts
Normal file
12
app/api/rfm/score/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { computeRFM, type AnalyzeInput } from "@/services/rfm-engine"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as AnalyzeInput
|
||||
const score = computeRFM(body)
|
||||
return NextResponse.json({ success: true, data: score })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
26
app/api/rfm/weights/route.ts
Normal file
26
app/api/rfm/weights/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getWeights, setWeights } from "@/services/rfm-engine"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ success: true, data: getWeights() })
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
let R = Number(body.R ?? 0.5)
|
||||
let F = Number(body.F ?? 0.3)
|
||||
let M = Number(body.M ?? 0.2)
|
||||
|
||||
// 归一化
|
||||
const sum = Math.max(0.000001, R + F + M)
|
||||
R = R / sum
|
||||
F = F / sum
|
||||
M = M / sum
|
||||
|
||||
setWeights({ R, F, M })
|
||||
return NextResponse.json({ success: true, data: { R, F, M } })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
|
||||
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
|
||||
|
||||
// 初始化MindsDB连接
|
||||
const mindsDBConfig = {
|
||||
host: process.env.MINDSDB_HOST || "localhost",
|
||||
port: Number.parseInt(process.env.MINDSDB_PORT || "47334"),
|
||||
username: process.env.MINDSDB_USERNAME || "mindsdb",
|
||||
password: process.env.MINDSDB_PASSWORD || "",
|
||||
database: process.env.MINDSDB_DATABASE || "mindsdb",
|
||||
}
|
||||
|
||||
// 初始化连接器
|
||||
getMindsDBConnector(mindsDBConfig)
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { getMindsDBConnector } from "@/lib/mindsdb-connector"
|
||||
import { getIntelligentSearchService } from "@/services/intelligent-search-service"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const mindsDB = getMindsDBConnector()
|
||||
const searchService = getIntelligentSearchService()
|
||||
|
||||
// 获取系统状态
|
||||
const systemStatus = await mindsDB.getSystemStatus()
|
||||
const searchStats = searchService.getSearchStats()
|
||||
const status = await mindsDB.getSystemStatus()
|
||||
|
||||
return NextResponse.json({
|
||||
system: systemStatus,
|
||||
search: searchStats,
|
||||
success: true,
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.APP_VERSION || "1.0.0",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("系统状态API错误:", error)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
import { addUser, filterUsers, getDistinctTags, getUserDetail } from "@/lib/mock-users"
|
||||
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
@@ -191,82 +192,51 @@ const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
// 微信好友数据缓存
|
||||
const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = Number.parseInt(searchParams.get("page") || "1")
|
||||
const pageSize = Number.parseInt(searchParams.get("pageSize") || "10")
|
||||
const search = searchParams.get("search") || ""
|
||||
const category = searchParams.get("category") || "all"
|
||||
const source = searchParams.get("source") || "all"
|
||||
const status = searchParams.get("status") || "all"
|
||||
const startDate = searchParams.get("startDate")
|
||||
const endDate = searchParams.get("endDate")
|
||||
const wechatSource = searchParams.get("wechatSource") || ""
|
||||
|
||||
let filteredUsers = [...userPool]
|
||||
|
||||
// 如果有微信来源参数,生成或获取微信好友数据
|
||||
if (wechatSource) {
|
||||
if (!wechatFriendsCache.has(wechatSource)) {
|
||||
// 生成150-300个随机好友
|
||||
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150
|
||||
wechatFriendsCache.set(wechatSource, generateWechatFriends(wechatSource, friendCount))
|
||||
}
|
||||
filteredUsers = wechatFriendsCache.get(wechatSource) || []
|
||||
}
|
||||
|
||||
// 应用过滤条件
|
||||
filteredUsers = filteredUsers.filter((user) => {
|
||||
const matchesSearch = search
|
||||
? user.nickname.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.wechatId.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.phone.includes(search)
|
||||
: true
|
||||
|
||||
const matchesCategory = category === "all" ? true : user.category === category
|
||||
const matchesSource = source === "all" ? true : user.source === source
|
||||
const matchesStatus = status === "all" ? true : user.status === status
|
||||
|
||||
const matchesDate =
|
||||
startDate && endDate
|
||||
? new Date(user.addTime) >= new Date(startDate) && new Date(user.addTime) <= new Date(endDate)
|
||||
: true
|
||||
|
||||
return matchesSearch && matchesCategory && matchesSource && matchesStatus && matchesDate
|
||||
})
|
||||
|
||||
// 按添加时间倒序排序
|
||||
filteredUsers.sort((a, b) => new Date(b.addTime).getTime() - new Date(a.addTime).getTime())
|
||||
|
||||
// 计算分页
|
||||
const total = filteredUsers.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const users = filteredUsers.slice(start, end)
|
||||
|
||||
// 计算分类统计
|
||||
const categoryStats = {
|
||||
potential: userPool.filter((user) => user.category === "potential").length,
|
||||
customer: userPool.filter((user) => user.category === "customer").length,
|
||||
lost: userPool.filter((user) => user.category === "lost").length,
|
||||
}
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
return NextResponse.json({
|
||||
users,
|
||||
pagination: {
|
||||
total,
|
||||
totalPages,
|
||||
currentPage: page,
|
||||
pageSize,
|
||||
},
|
||||
stats: {
|
||||
total: wechatSource ? filteredUsers.length : userPool.length,
|
||||
todayNew: wechatSource ? Math.floor(filteredUsers.length * 0.1) : todayUsers.length,
|
||||
categoryStats,
|
||||
},
|
||||
})
|
||||
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 meta = searchParams.get("meta")
|
||||
const id = searchParams.get("id")
|
||||
|
||||
if (meta === "tags") {
|
||||
const tags = getDistinctTags()
|
||||
return NextResponse.json({ success: true, data: { tags } })
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const user = getUserDetail(id)
|
||||
if (!user) return NextResponse.json({ success: false, error: "NOT_FOUND" }, { status: 404 })
|
||||
return NextResponse.json({ success: true, data: user })
|
||||
}
|
||||
|
||||
const q = searchParams.get("q") || undefined
|
||||
const tagsParam = searchParams.get("tags") || ""
|
||||
const tags = tagsParam ? tagsParam.split(",").filter(Boolean) : []
|
||||
const statusParam = searchParams.get("status") || ""
|
||||
const status = statusParam ? (statusParam.split(",") as any) : []
|
||||
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 res = filterUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
|
||||
return NextResponse.json({ success: true, data: res })
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body || !body.name || !body.phone || !body.email) {
|
||||
return NextResponse.json({ success: false, error: "INVALID_PAYLOAD" }, { status: 400 })
|
||||
}
|
||||
const user = addUser({
|
||||
name: body.name,
|
||||
phone: body.phone,
|
||||
email: body.email,
|
||||
tags: Array.isArray(body.tags) ? body.tags.slice(0, 20) : [],
|
||||
})
|
||||
return NextResponse.json({ success: true, data: user })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user