feat: enhance user profile with detailed tags and asset evaluation
Optimize user detail page for asset assessment and tag info. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -1,27 +1,31 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getDatabases, getDatabaseStructure, getTableStructure } from "@/lib/db-connector"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { getDatabases, getDatabaseStructure } from "@/lib/mongodb-mock-connector" // 更新导入路径
|
||||
|
||||
export async function GET(request: Request) {
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const database = searchParams.get("database")
|
||||
const url = new URL(req.url)
|
||||
const database = url.searchParams.get("database")
|
||||
const table = url.searchParams.get("table")
|
||||
|
||||
if (database) {
|
||||
// 获取指定数据库的结构
|
||||
const structure = await getDatabaseStructure(database)
|
||||
return NextResponse.json({ success: true, data: structure })
|
||||
} else {
|
||||
// 获取所有数据库列表
|
||||
const databases = await getDatabases()
|
||||
return NextResponse.json({ success: true, data: databases })
|
||||
// 无查询参数:返回数据库列表([{ Database: string }])
|
||||
if (!database) {
|
||||
const dbs = await getDatabases()
|
||||
return NextResponse.json({ success: true, data: dbs })
|
||||
}
|
||||
|
||||
// 有 database + table:返回表结构
|
||||
if (database && table) {
|
||||
const fields = await getTableStructure(database, table)
|
||||
return NextResponse.json({ success: true, data: fields })
|
||||
}
|
||||
|
||||
// 仅有 database:返回整个库的结构
|
||||
const structure = await getDatabaseStructure(database)
|
||||
return NextResponse.json({ success: true, data: structure })
|
||||
} catch (error) {
|
||||
console.error("数据库结构查询失败:", error)
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "数据库结构查询失败", error: (error as Error).message },
|
||||
{ status: 500 },
|
||||
)
|
||||
console.error("数据库结构API错误:", error)
|
||||
return NextResponse.json({ success: false, message: "获取数据库结构失败" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
98
app/api/openapi/route.ts
Normal file
98
app/api/openapi/route.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
// 生成最小可用的 OpenAPI 3.1 规范,覆盖当前已实现的关键接口
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
const download = url.searchParams.get("download") === "1"
|
||||
|
||||
const spec = {
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "用户数据资产中台 API",
|
||||
version: "1.0.0",
|
||||
description:
|
||||
"统一用户数据接入与治理接口。包含数据接入、数据库结构浏览等端点。模型结构参考 v1.4 文档中的统一用户画像定义。",
|
||||
},
|
||||
paths: {
|
||||
"/api/ingest": {
|
||||
get: {
|
||||
summary: "获取数据接入状态",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "成功",
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
summary: "提交单条数据进行接入处理",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
source: { type: "string" },
|
||||
sourceUserId: { type: "string" },
|
||||
sourceRecordId: { type: "string" },
|
||||
originalData: { type: "object" },
|
||||
timestamp: { type: "string", format: "date-time" },
|
||||
},
|
||||
required: ["source", "originalData"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: { "200": { description: "成功" }, "400": { description: "参数错误" } },
|
||||
},
|
||||
put: {
|
||||
summary: "批量接入处理",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
requests: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
source: { type: "string" },
|
||||
sourceUserId: { type: "string" },
|
||||
sourceRecordId: { type: "string" },
|
||||
originalData: { type: "object" },
|
||||
timestamp: { type: "string", format: "date-time" },
|
||||
},
|
||||
required: ["source", "originalData"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["requests"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: { "200": { description: "成功" }, "400": { description: "参数错误" } },
|
||||
},
|
||||
},
|
||||
"/api/database-structure": {
|
||||
get: {
|
||||
summary: "获取数据库列表或结构",
|
||||
parameters: [
|
||||
{ name: "database", in: "query", required: false, schema: { type: "string" } },
|
||||
{ name: "table", in: "query", required: false, schema: { type: "string" } },
|
||||
],
|
||||
responses: { "200": { description: "成功" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const res = NextResponse.json(spec)
|
||||
if (download) {
|
||||
res.headers.set("Content-Disposition", 'attachment; filename="openapi.json"')
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
import { NextResponse } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
import { addUser, filterUsers, getDistinctTags, getUserById, queryUsers, type UserStatus, MOCK_USERS } from "@/lib/mock-users"
|
||||
import { addUser, getUsersStore, queryUsers, type User } from "@/lib/mock-users"
|
||||
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
@@ -127,6 +127,9 @@ const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => {
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
city: "",
|
||||
persona: "",
|
||||
rfmScore: Math.floor(Math.random() * 101),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -185,6 +188,9 @@ const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
city: "",
|
||||
persona: "",
|
||||
rfmScore: Math.floor(Math.random() * 101),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -194,33 +200,57 @@ const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
function parseArrayParam(v: string | null) {
|
||||
if (!v) return []
|
||||
return v.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
return v
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const keyword = (searchParams.get("q") || "").trim()
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
const meta = url.searchParams.get("meta")
|
||||
if (meta === "tags") {
|
||||
const tags = Array.from(new Set(getUsersStore().flatMap((u) => u.tags))).sort()
|
||||
const cities = Array.from(new Set(getUsersStore().map((u) => u.city))).sort()
|
||||
const personas = Array.from(new Set(getUsersStore().flatMap((u) => u.persona))).sort()
|
||||
const sources = Array.from(new Set(getUsersStore().map((u) => u.source))).sort()
|
||||
return NextResponse.json({ success: true, data: { tags, cities, personas, sources } })
|
||||
}
|
||||
|
||||
const filtered = keyword
|
||||
? MOCK_USERS.filter(
|
||||
(u) =>
|
||||
u.name.includes(keyword) ||
|
||||
(u.nickname && u.nickname.includes(keyword)) ||
|
||||
u.tags.some((t) => t.includes(keyword)),
|
||||
)
|
||||
: MOCK_USERS
|
||||
const q = url.searchParams.get("q") ?? undefined
|
||||
const tags = url.searchParams.get("tags")?.split(",").filter(Boolean)
|
||||
const status = url.searchParams.get("status")?.split(",").filter(Boolean) as any
|
||||
const city = url.searchParams.get("city")?.split(",").filter(Boolean)
|
||||
const persona = url.searchParams.get("persona")?.split(",").filter(Boolean)
|
||||
const source = url.searchParams.get("source")?.split(",").filter(Boolean)
|
||||
const rfmMin = Number(url.searchParams.get("rfmMin") ?? 0)
|
||||
const rfmMax = Number(url.searchParams.get("rfmMax") ?? 100)
|
||||
const page = Number(url.searchParams.get("page") ?? 1)
|
||||
const pageSize = Number(url.searchParams.get("pageSize") ?? 20)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
total: filtered.length,
|
||||
items: filtered,
|
||||
const { data, pagination } = queryUsers({
|
||||
q,
|
||||
tags,
|
||||
status,
|
||||
city,
|
||||
persona,
|
||||
source,
|
||||
rfmMin,
|
||||
rfmMax,
|
||||
page,
|
||||
pageSize,
|
||||
})
|
||||
|
||||
// 用户估值:简单以 rfmScore * 100 作为估值
|
||||
const totalValue = data.reduce((sum, u) => sum + u.rfmScore * 100, 0)
|
||||
|
||||
return NextResponse.json({ success: true, data: { items: data, pagination, totalValue } })
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const created = addUser(body ?? {})
|
||||
return NextResponse.json({ data: created }, { status: 201 })
|
||||
export async function POST(req: Request) {
|
||||
const body = (await req.json()) as Partial<User>
|
||||
const u = addUser(body)
|
||||
return NextResponse.json({ success: true, data: u })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user