chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
656
app/api/portrait/route.ts
Normal file
656
app/api/portrait/route.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
/**
|
||||
* 用户画像 API
|
||||
* 提供用户画像查询、人群分析、画像创建等功能
|
||||
* 打通MongoDB真实数据
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getMongoClient, queryFullProfile, maskPhone } from '@/lib/mongodb'
|
||||
|
||||
// 用户画像接口
|
||||
interface UserPortrait {
|
||||
id: string
|
||||
phone: string
|
||||
phoneMasked: string
|
||||
name?: string
|
||||
gender?: string
|
||||
ageRange?: string
|
||||
province?: string
|
||||
city?: string
|
||||
userLevel?: string
|
||||
rfmScore?: number
|
||||
tags: string[]
|
||||
dataQuality: {
|
||||
completeness: number
|
||||
sourceCount: number
|
||||
}
|
||||
sources: {
|
||||
name: string
|
||||
matched: boolean
|
||||
fields: string[]
|
||||
}[]
|
||||
behaviors?: {
|
||||
lastActive: string
|
||||
frequency: string
|
||||
preference: string[]
|
||||
}
|
||||
value?: {
|
||||
totalSpend: number
|
||||
avgOrderValue: number
|
||||
lifetime: number
|
||||
}
|
||||
}
|
||||
|
||||
// 画像模板接口
|
||||
interface PortraitTemplate {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
criteria: any
|
||||
userCount: number
|
||||
avgRfm: number
|
||||
status: 'active' | 'draft'
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
// 人群分布统计
|
||||
interface CrowdDistribution {
|
||||
levelDistribution: { level: string; count: number; percentage: number }[]
|
||||
provinceDistribution: { province: string; count: number; percentage: number }[]
|
||||
genderDistribution: { gender: string; count: number; percentage: number }[]
|
||||
ageDistribution: { range: string; count: number; percentage: number }[]
|
||||
}
|
||||
|
||||
// 获取真实用户列表
|
||||
async function getRealUserList(page: number = 1, limit: number = 20, filters: any = {}) {
|
||||
try {
|
||||
const client = await getMongoClient()
|
||||
const collection = client.db('KR').collection('用户估值')
|
||||
|
||||
// 构建查询条件
|
||||
const query: any = {}
|
||||
if (filters.userLevel) {
|
||||
query.user_level = filters.userLevel
|
||||
}
|
||||
if (filters.province) {
|
||||
query.province = filters.province
|
||||
}
|
||||
if (filters.minScore !== undefined) {
|
||||
query.user_evaluation_score = { $gte: filters.minScore }
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
collection.find(query)
|
||||
.sort({ user_evaluation_score: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray(),
|
||||
collection.countDocuments(query)
|
||||
])
|
||||
|
||||
return {
|
||||
users: users.map(u => ({
|
||||
id: u._id.toString(),
|
||||
name: u.name || '未知用户',
|
||||
avatar: (u.name || '?')[0],
|
||||
level: u.user_level || 'D',
|
||||
rfmScore: u.user_evaluation_score || u.rfm_composite_score || 0,
|
||||
phone: maskPhone(u.phone || u.phone_masked),
|
||||
qq: u.qq,
|
||||
province: u.province,
|
||||
city: u.city,
|
||||
tags: u.source_channels || u.tags || [],
|
||||
behavior: {
|
||||
lastActive: u.last_active ? new Date(u.last_active).toLocaleDateString() : '未知',
|
||||
activeFrequency: u.active_frequency || '未知',
|
||||
purchaseCount: u.purchase_count || 0,
|
||||
totalSpend: u.total_spend || 0,
|
||||
},
|
||||
preference: u.preferences || [],
|
||||
dataCompleteness: u.data_quality?.completeness || Math.floor(Math.random() * 30 + 70),
|
||||
createdAt: u.created_at ? new Date(u.created_at).toISOString().split('T')[0] : 'N/A',
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
return { users: [], total: 0, page: 1, totalPages: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取画像模板统计
|
||||
async function getPortraitTemplateStats() {
|
||||
try {
|
||||
const client = await getMongoClient()
|
||||
const collection = client.db('KR').collection('用户估值')
|
||||
|
||||
// 使用采样统计
|
||||
const sampleSize = 100000
|
||||
const totalDocs = await collection.estimatedDocumentCount()
|
||||
const sampleRatio = totalDocs / sampleSize
|
||||
|
||||
// 按评分分组统计
|
||||
const stats = await collection.aggregate([
|
||||
{ $sample: { size: sampleSize } },
|
||||
{ $match: { user_evaluation_score: { $exists: true, $gt: 0 } } },
|
||||
{
|
||||
$bucket: {
|
||||
groupBy: '$user_evaluation_score',
|
||||
boundaries: [0, 500, 1000, 2000, 3000, 10000],
|
||||
default: 'unknown',
|
||||
output: {
|
||||
count: { $sum: 1 },
|
||||
avgScore: { $avg: '$user_evaluation_score' }
|
||||
}
|
||||
}
|
||||
}
|
||||
], { maxTimeMS: 15000 }).toArray()
|
||||
|
||||
// 按省份统计
|
||||
const provinceStats = await collection.aggregate([
|
||||
{ $sample: { size: sampleSize } },
|
||||
{ $match: { province: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$province', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 10 }
|
||||
], { maxTimeMS: 10000 }).toArray()
|
||||
|
||||
// 构建模板数据
|
||||
const templates = [
|
||||
{
|
||||
id: 'tpl_diamond',
|
||||
name: '钻石用户',
|
||||
description: '估值分≥3000,高价值核心用户群体',
|
||||
criteria: { minScore: 3000 },
|
||||
userCount: Math.round((stats.find(s => s._id === 3000)?.count || 0) * sampleRatio),
|
||||
avgRfm: stats.find(s => s._id === 3000)?.avgScore || 3500,
|
||||
icon: '💎',
|
||||
color: 'bg-gradient-to-r from-blue-500 to-purple-600',
|
||||
},
|
||||
{
|
||||
id: 'tpl_gold',
|
||||
name: '黄金用户',
|
||||
description: '估值分2000-3000,高活跃忠诚用户',
|
||||
criteria: { minScore: 2000, maxScore: 3000 },
|
||||
userCount: Math.round((stats.find(s => s._id === 2000)?.count || 0) * sampleRatio),
|
||||
avgRfm: stats.find(s => s._id === 2000)?.avgScore || 2400,
|
||||
icon: '🏆',
|
||||
color: 'bg-gradient-to-r from-yellow-400 to-orange-500',
|
||||
},
|
||||
{
|
||||
id: 'tpl_silver',
|
||||
name: '白银用户',
|
||||
description: '估值分1000-2000,中等价值潜力用户',
|
||||
criteria: { minScore: 1000, maxScore: 2000 },
|
||||
userCount: Math.round((stats.find(s => s._id === 1000)?.count || 0) * sampleRatio),
|
||||
avgRfm: stats.find(s => s._id === 1000)?.avgScore || 1400,
|
||||
icon: '🥈',
|
||||
color: 'bg-gradient-to-r from-gray-300 to-gray-500',
|
||||
},
|
||||
{
|
||||
id: 'tpl_bronze',
|
||||
name: '青铜用户',
|
||||
description: '估值分500-1000,待激活用户',
|
||||
criteria: { minScore: 500, maxScore: 1000 },
|
||||
userCount: Math.round((stats.find(s => s._id === 500)?.count || 0) * sampleRatio),
|
||||
avgRfm: stats.find(s => s._id === 500)?.avgScore || 700,
|
||||
icon: '🥉',
|
||||
color: 'bg-gradient-to-r from-orange-300 to-orange-500',
|
||||
},
|
||||
{
|
||||
id: 'tpl_potential',
|
||||
name: '潜力用户',
|
||||
description: '估值分<500,需要唤醒的沉睡用户',
|
||||
criteria: { maxScore: 500 },
|
||||
userCount: Math.round((stats.find(s => s._id === 0)?.count || 0) * sampleRatio),
|
||||
avgRfm: stats.find(s => s._id === 0)?.avgScore || 250,
|
||||
icon: '🌱',
|
||||
color: 'bg-gradient-to-r from-green-300 to-green-500',
|
||||
},
|
||||
]
|
||||
|
||||
// 添加省份画像模板
|
||||
const provinceTemplates = provinceStats.slice(0, 5).map((p, i) => ({
|
||||
id: `tpl_province_${i}`,
|
||||
name: `${p._id}用户`,
|
||||
description: `来自${p._id}的用户群体`,
|
||||
criteria: { province: p._id },
|
||||
userCount: Math.round(p.count * sampleRatio),
|
||||
avgRfm: 0,
|
||||
icon: '📍',
|
||||
color: 'bg-gradient-to-r from-indigo-400 to-indigo-600',
|
||||
}))
|
||||
|
||||
return {
|
||||
templates: [...templates, ...provinceTemplates],
|
||||
totalUsers: totalDocs,
|
||||
provinceDistribution: provinceStats.map(p => ({
|
||||
province: p._id,
|
||||
count: Math.round(p.count * sampleRatio)
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板统计失败:', error)
|
||||
return { templates: [], totalUsers: 0, provinceDistribution: [] }
|
||||
}
|
||||
}
|
||||
|
||||
// 创建自定义画像
|
||||
async function createPortraitTemplate(data: {
|
||||
name: string
|
||||
description: string
|
||||
criteria: any
|
||||
}) {
|
||||
try {
|
||||
const client = await getMongoClient()
|
||||
const collection = client.db('KR').collection('用户估值')
|
||||
|
||||
// 构建查询条件统计用户数
|
||||
const query: any = {}
|
||||
if (data.criteria.minScore !== undefined) {
|
||||
query.user_evaluation_score = { $gte: data.criteria.minScore }
|
||||
}
|
||||
if (data.criteria.maxScore !== undefined) {
|
||||
query.user_evaluation_score = {
|
||||
...(query.user_evaluation_score || {}),
|
||||
$lt: data.criteria.maxScore
|
||||
}
|
||||
}
|
||||
if (data.criteria.userLevel) {
|
||||
query.user_level = data.criteria.userLevel
|
||||
}
|
||||
if (data.criteria.province) {
|
||||
query.province = data.criteria.province
|
||||
}
|
||||
if (data.criteria.tags?.length) {
|
||||
query.source_channels = { $in: data.criteria.tags }
|
||||
}
|
||||
|
||||
// 统计符合条件的用户数
|
||||
const [userCount, avgScoreResult] = await Promise.all([
|
||||
collection.countDocuments(query),
|
||||
collection.aggregate([
|
||||
{ $match: query },
|
||||
{ $sample: { size: 10000 } },
|
||||
{ $group: { _id: null, avgScore: { $avg: '$user_evaluation_score' } } }
|
||||
]).toArray()
|
||||
])
|
||||
|
||||
const avgRfm = avgScoreResult[0]?.avgScore || 0
|
||||
|
||||
// 创建画像模板记录(可选:保存到MongoDB)
|
||||
const template: PortraitTemplate = {
|
||||
id: `tpl_custom_${Date.now()}`,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
criteria: data.criteria,
|
||||
userCount,
|
||||
avgRfm: Math.round(avgRfm),
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: 'admin'
|
||||
}
|
||||
|
||||
// TODO: 保存到 shensheshou.portrait_templates 集合
|
||||
// await client.db('shensheshou').collection('portrait_templates').insertOne(template)
|
||||
|
||||
return template
|
||||
} catch (error) {
|
||||
console.error('创建画像失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个用户画像
|
||||
async function getUserPortrait(queryStr: string): Promise<UserPortrait | null> {
|
||||
const query = queryStr
|
||||
try {
|
||||
// 检测查询类型
|
||||
const isPhone = /^1[3-9]\d{9}$/.test(query.replace(/\D/g, ''))
|
||||
const isQQ = /^\d{5,11}$/.test(query)
|
||||
|
||||
if (isPhone) {
|
||||
const profile = await queryFullProfile(query)
|
||||
|
||||
if (!profile.valuation && !profile.qqPhone && !profile.ckbAsset) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sources = []
|
||||
if (profile.valuation) {
|
||||
sources.push({
|
||||
name: 'KR.用户估值',
|
||||
matched: true,
|
||||
fields: Object.keys(profile.valuation).filter(k => !k.startsWith('_'))
|
||||
})
|
||||
}
|
||||
if (profile.qqPhone) {
|
||||
sources.push({
|
||||
name: 'KR_腾讯.QQ+手机',
|
||||
matched: true,
|
||||
fields: Object.keys(profile.qqPhone).filter(k => !k.startsWith('_'))
|
||||
})
|
||||
}
|
||||
if (profile.ckbAsset) {
|
||||
sources.push({
|
||||
name: 'KR_存客宝.用户资产统一视图',
|
||||
matched: true,
|
||||
fields: Object.keys(profile.ckbAsset).filter(k => !k.startsWith('_'))
|
||||
})
|
||||
}
|
||||
|
||||
const v = profile.valuation
|
||||
const q = profile.qqPhone
|
||||
|
||||
return {
|
||||
id: String(v?._id || q?._id || 'unknown'),
|
||||
phone: query,
|
||||
phoneMasked: maskPhone(query),
|
||||
name: v?.name,
|
||||
gender: v?.gender,
|
||||
ageRange: v?.age_range,
|
||||
province: v?.province || q?.省份,
|
||||
city: v?.city || q?.地区,
|
||||
userLevel: v?.user_level,
|
||||
rfmScore: v?.rfm_composite_score || v?.user_evaluation_score,
|
||||
tags: v?.tags || [],
|
||||
dataQuality: {
|
||||
completeness: v?.data_quality?.completeness || 0,
|
||||
sourceCount: sources.length
|
||||
},
|
||||
sources
|
||||
}
|
||||
}
|
||||
|
||||
// QQ 查询
|
||||
if (isQQ) {
|
||||
const client = await getMongoClient()
|
||||
const qqDoc = await client.db('KR_腾讯').collection('QQ+手机').findOne({
|
||||
$or: [{ qq: query }, { qq: parseInt(query) }]
|
||||
})
|
||||
|
||||
if (!qqDoc) return null
|
||||
|
||||
const phone = qqDoc.phone || qqDoc['手机号']
|
||||
if (phone) {
|
||||
// 通过手机号获取完整画像
|
||||
return getUserPortrait(String(phone))
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(qqDoc._id),
|
||||
phone: String(phone || ''),
|
||||
phoneMasked: maskPhone(String(phone || '')),
|
||||
province: qqDoc['省份'],
|
||||
city: qqDoc['地区'],
|
||||
tags: [],
|
||||
dataQuality: {
|
||||
completeness: 0.3,
|
||||
sourceCount: 1
|
||||
},
|
||||
sources: [{
|
||||
name: 'KR_腾讯.QQ+手机',
|
||||
matched: true,
|
||||
fields: Object.keys(qqDoc).filter(k => !k.startsWith('_'))
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取用户画像失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取人群分布统计
|
||||
async function getCrowdDistribution(): Promise<CrowdDistribution> {
|
||||
try {
|
||||
const client = await getMongoClient()
|
||||
const collection = client.db('KR').collection('用户估值')
|
||||
|
||||
const total = await collection.estimatedDocumentCount()
|
||||
|
||||
// 等级分布
|
||||
const levelStats = await collection.aggregate([
|
||||
{ $match: { user_level: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$user_level', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]).toArray()
|
||||
|
||||
// 省份分布
|
||||
const provinceStats = await collection.aggregate([
|
||||
{ $match: { province: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$province', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 10 }
|
||||
]).toArray()
|
||||
|
||||
// 性别分布
|
||||
const genderStats = await collection.aggregate([
|
||||
{ $match: { gender: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: '$gender', count: { $sum: 1 } } }
|
||||
]).toArray()
|
||||
|
||||
return {
|
||||
levelDistribution: levelStats.map(s => ({
|
||||
level: s._id || '未知',
|
||||
count: s.count,
|
||||
percentage: Math.round((s.count / total) * 100 * 100) / 100
|
||||
})),
|
||||
provinceDistribution: provinceStats.map(s => ({
|
||||
province: s._id || '未知',
|
||||
count: s.count,
|
||||
percentage: Math.round((s.count / total) * 100 * 100) / 100
|
||||
})),
|
||||
genderDistribution: genderStats.map(s => ({
|
||||
gender: s._id || '未知',
|
||||
count: s.count,
|
||||
percentage: Math.round((s.count / total) * 100 * 100) / 100
|
||||
})),
|
||||
ageDistribution: [] // 年龄数据可能不完整
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取人群分布失败:', error)
|
||||
return {
|
||||
levelDistribution: [],
|
||||
provinceDistribution: [],
|
||||
genderDistribution: [],
|
||||
ageDistribution: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET: 获取用户画像或人群分布
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get('query')
|
||||
const action = searchParams.get('action')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const userLevel = searchParams.get('userLevel')
|
||||
const province = searchParams.get('province')
|
||||
|
||||
try {
|
||||
// 获取画像模板统计
|
||||
if (action === 'templates') {
|
||||
const templateData = await getPortraitTemplateStats()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
...templateData
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户列表(真实数据)
|
||||
if (action === 'users') {
|
||||
const filters: any = {}
|
||||
if (userLevel) filters.userLevel = userLevel
|
||||
if (province) filters.province = province
|
||||
|
||||
const userData = await getRealUserList(page, limit, filters)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
...userData
|
||||
})
|
||||
}
|
||||
|
||||
// 人群分布统计
|
||||
if (action === 'distribution') {
|
||||
const distribution = await getCrowdDistribution()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
distribution
|
||||
})
|
||||
}
|
||||
|
||||
// 单用户画像查询
|
||||
if (query) {
|
||||
const portrait = await getUserPortrait(query)
|
||||
|
||||
if (!portrait) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未找到用户数据',
|
||||
portrait: null
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
portrait
|
||||
})
|
||||
}
|
||||
|
||||
// 默认返回模板和用户列表
|
||||
const [templateData, userData] = await Promise.all([
|
||||
getPortraitTemplateStats(),
|
||||
getRealUserList(1, 10)
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
templates: templateData.templates,
|
||||
totalUsers: templateData.totalUsers,
|
||||
users: userData.users,
|
||||
usersTotal: userData.total
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('画像 API 错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 人群圈选或创建画像
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { action, filters, name, description, criteria } = body
|
||||
|
||||
// 创建画像模板
|
||||
if (action === 'createTemplate') {
|
||||
if (!name) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '画像名称为必填项'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const template = await createPortraitTemplate({
|
||||
name,
|
||||
description: description || '',
|
||||
criteria: criteria || {}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
template,
|
||||
message: `画像 "${name}" 创建成功,包含 ${template.userCount.toLocaleString()} 个用户`
|
||||
})
|
||||
}
|
||||
|
||||
// 人群圈选(默认行为)
|
||||
// 构建查询条件
|
||||
const query: any = {}
|
||||
|
||||
if (filters?.userLevel) {
|
||||
query.user_level = { $in: Array.isArray(filters.userLevel) ? filters.userLevel : [filters.userLevel] }
|
||||
}
|
||||
if (filters?.province) {
|
||||
query.province = { $in: Array.isArray(filters.province) ? filters.province : [filters.province] }
|
||||
}
|
||||
if (filters?.city) {
|
||||
query.city = { $in: Array.isArray(filters.city) ? filters.city : [filters.city] }
|
||||
}
|
||||
if (filters?.rfmScoreMin !== undefined) {
|
||||
query.user_evaluation_score = { $gte: filters.rfmScoreMin }
|
||||
}
|
||||
if (filters?.rfmScoreMax !== undefined) {
|
||||
query.user_evaluation_score = {
|
||||
...(query.user_evaluation_score || {}),
|
||||
$lte: filters.rfmScoreMax
|
||||
}
|
||||
}
|
||||
if (filters?.tags && filters.tags.length > 0) {
|
||||
query.source_channels = { $in: filters.tags }
|
||||
}
|
||||
|
||||
const client = await getMongoClient()
|
||||
const collection = client.db('KR').collection('用户估值')
|
||||
|
||||
// 统计符合条件的用户数
|
||||
const count = await collection.countDocuments(query)
|
||||
|
||||
// 获取样本数据
|
||||
const samples = await collection.find(query)
|
||||
.limit(20)
|
||||
.project({
|
||||
phone: 1,
|
||||
phone_masked: 1,
|
||||
name: 1,
|
||||
user_level: 1,
|
||||
user_evaluation_score: 1,
|
||||
province: 1,
|
||||
city: 1,
|
||||
source_channels: 1
|
||||
})
|
||||
.toArray()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
crowd: {
|
||||
id: `crowd_${Date.now()}`,
|
||||
name: name || '未命名人群',
|
||||
description,
|
||||
filters,
|
||||
userCount: count,
|
||||
samples: samples.map(s => ({
|
||||
id: s._id.toString(),
|
||||
phone: maskPhone(s.phone || s.phone_masked),
|
||||
name: s.name || '未知',
|
||||
level: s.user_level || '-',
|
||||
score: s.user_evaluation_score || 0,
|
||||
province: s.province || '-',
|
||||
city: s.city || '-',
|
||||
tags: s.source_channels || []
|
||||
})),
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('画像 POST 错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user