769 lines
21 KiB
TypeScript
769 lines
21 KiB
TypeScript
|
|
/**
|
|||
|
|
* 神射手开放API接口
|
|||
|
|
*
|
|||
|
|
* 提供给第三方系统(存客宝、点了码等)调用的开放接口
|
|||
|
|
* 支持数据流入、数据查询、标签完善、批量处理等功能
|
|||
|
|
*
|
|||
|
|
* @author 神射手团队
|
|||
|
|
* @version 1.0.0
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|||
|
|
import crypto from 'crypto'
|
|||
|
|
|
|||
|
|
// ==================== 类型定义 ====================
|
|||
|
|
|
|||
|
|
interface Partner {
|
|||
|
|
id: string
|
|||
|
|
name: string
|
|||
|
|
apiKey: string
|
|||
|
|
apiSecret: string
|
|||
|
|
status: 'active' | 'inactive' | 'pending'
|
|||
|
|
permissions: {
|
|||
|
|
dataIngest: boolean
|
|||
|
|
dataQuery: boolean
|
|||
|
|
tagEnrich: boolean
|
|||
|
|
batchProcess: boolean
|
|||
|
|
}
|
|||
|
|
quotaConfig: {
|
|||
|
|
dailyLimit: number
|
|||
|
|
monthlyLimit: number
|
|||
|
|
rateLimit: number
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface APIResponse<T = any> {
|
|||
|
|
success: boolean
|
|||
|
|
data?: T
|
|||
|
|
error?: string
|
|||
|
|
code?: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 模拟数据存储 ====================
|
|||
|
|
|
|||
|
|
// 模拟接入方数据(实际应存储在数据库中)
|
|||
|
|
const PARTNERS: Map<string, Partner> = new Map([
|
|||
|
|
['sk-ckb-xxxxxxxxxx', {
|
|||
|
|
id: 'ckb_001',
|
|||
|
|
name: '存客宝',
|
|||
|
|
apiKey: 'sk-ckb-xxxxxxxxxx',
|
|||
|
|
apiSecret: 'sec-ckb-xxxxxxxxxx',
|
|||
|
|
status: 'active',
|
|||
|
|
permissions: {
|
|||
|
|
dataIngest: true,
|
|||
|
|
dataQuery: true,
|
|||
|
|
tagEnrich: true,
|
|||
|
|
batchProcess: true,
|
|||
|
|
},
|
|||
|
|
quotaConfig: {
|
|||
|
|
dailyLimit: 10000,
|
|||
|
|
monthlyLimit: 300000,
|
|||
|
|
rateLimit: 100,
|
|||
|
|
},
|
|||
|
|
}],
|
|||
|
|
['sk-dlm-xxxxxxxxxx', {
|
|||
|
|
id: 'dlm_001',
|
|||
|
|
name: '点了码',
|
|||
|
|
apiKey: 'sk-dlm-xxxxxxxxxx',
|
|||
|
|
apiSecret: 'sec-dlm-xxxxxxxxxx',
|
|||
|
|
status: 'active',
|
|||
|
|
permissions: {
|
|||
|
|
dataIngest: true,
|
|||
|
|
dataQuery: true,
|
|||
|
|
tagEnrich: false,
|
|||
|
|
batchProcess: false,
|
|||
|
|
},
|
|||
|
|
quotaConfig: {
|
|||
|
|
dailyLimit: 5000,
|
|||
|
|
monthlyLimit: 150000,
|
|||
|
|
rateLimit: 50,
|
|||
|
|
},
|
|||
|
|
}],
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
// 模拟调用计数(实际应使用Redis)
|
|||
|
|
const CALL_COUNTS: Map<string, { daily: number; monthly: number }> = new Map()
|
|||
|
|
|
|||
|
|
// 模拟批量任务存储
|
|||
|
|
const BATCH_TASKS: Map<string, {
|
|||
|
|
id: string
|
|||
|
|
partnerId: string
|
|||
|
|
type: string
|
|||
|
|
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|||
|
|
progress: number
|
|||
|
|
total: number
|
|||
|
|
processed: number
|
|||
|
|
failed: number
|
|||
|
|
createdAt: Date
|
|||
|
|
completedAt?: Date
|
|||
|
|
}> = new Map()
|
|||
|
|
|
|||
|
|
// ==================== 工具函数 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证API请求
|
|||
|
|
*/
|
|||
|
|
function validateRequest(request: NextRequest): { valid: boolean; partner?: Partner; error?: string } {
|
|||
|
|
const authHeader = request.headers.get('Authorization')
|
|||
|
|
const apiSecret = request.headers.get('X-API-Secret')
|
|||
|
|
|
|||
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|||
|
|
return { valid: false, error: '缺少Authorization头' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const apiKey = authHeader.replace('Bearer ', '')
|
|||
|
|
const partner = PARTNERS.get(apiKey)
|
|||
|
|
|
|||
|
|
if (!partner) {
|
|||
|
|
return { valid: false, error: '无效的API Key' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (partner.status !== 'active') {
|
|||
|
|
return { valid: false, error: '接入方已被禁用' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证API Secret(可选,增强安全性)
|
|||
|
|
if (apiSecret && apiSecret !== partner.apiSecret) {
|
|||
|
|
return { valid: false, error: 'API Secret验证失败' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查配额
|
|||
|
|
const counts = CALL_COUNTS.get(partner.id) || { daily: 0, monthly: 0 }
|
|||
|
|
if (counts.daily >= partner.quotaConfig.dailyLimit) {
|
|||
|
|
return { valid: false, error: '已达到每日调用限额' }
|
|||
|
|
}
|
|||
|
|
if (counts.monthly >= partner.quotaConfig.monthlyLimit) {
|
|||
|
|
return { valid: false, error: '已达到每月调用限额' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新调用计数
|
|||
|
|
CALL_COUNTS.set(partner.id, {
|
|||
|
|
daily: counts.daily + 1,
|
|||
|
|
monthly: counts.monthly + 1,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return { valid: true, partner }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证手机号格式
|
|||
|
|
*/
|
|||
|
|
function isValidPhone(phone: string): boolean {
|
|||
|
|
return /^1[3-9]\d{9}$/.test(phone)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 脱敏处理
|
|||
|
|
*/
|
|||
|
|
function maskPhone(phone: string): string {
|
|||
|
|
if (!phone || phone.length !== 11) return phone
|
|||
|
|
return phone.slice(0, 3) + '****' + phone.slice(7)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function maskName(name: string): string {
|
|||
|
|
if (!name) return name
|
|||
|
|
if (name.length <= 1) return name
|
|||
|
|
return name[0] + '*'.repeat(name.length - 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 模拟用户画像数据
|
|||
|
|
*/
|
|||
|
|
function getMockUserProfile(phone: string) {
|
|||
|
|
// 模拟从数据库获取用户画像
|
|||
|
|
const hash = crypto.createHash('md5').update(phone).digest('hex')
|
|||
|
|
const seed = parseInt(hash.slice(0, 8), 16)
|
|||
|
|
|
|||
|
|
const levels = ['S', 'A', 'B', 'C', 'D']
|
|||
|
|
const pools = ['钻石池', '黄金池', '白银池', '青铜池', '潜力池']
|
|||
|
|
const tagOptions = [
|
|||
|
|
'高价值用户', '优质用户', '电商活跃', '社交达人', '一线城市',
|
|||
|
|
'二线城市', '高频用户', '沉默用户', '新用户', '流失风险',
|
|||
|
|
'高消费', '低消费', '金融偏好', '科技爱好者', '时尚达人'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const levelIndex = seed % 5
|
|||
|
|
const r = 1 + (seed % 5)
|
|||
|
|
const f = 1 + ((seed >> 4) % 5)
|
|||
|
|
const m = 1 + ((seed >> 8) % 5)
|
|||
|
|
const rfmScore = Math.round(r * 0.3 + f * 0.3 + m * 0.4) * 20
|
|||
|
|
|
|||
|
|
// 随机选择3-5个标签
|
|||
|
|
const numTags = 3 + (seed % 3)
|
|||
|
|
const tags: string[] = []
|
|||
|
|
for (let i = 0; i < numTags; i++) {
|
|||
|
|
const tagIndex = (seed + i * 7) % tagOptions.length
|
|||
|
|
if (!tags.includes(tagOptions[tagIndex])) {
|
|||
|
|
tags.push(tagOptions[tagIndex])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
phone: maskPhone(phone),
|
|||
|
|
name: maskName('张三'),
|
|||
|
|
tags,
|
|||
|
|
rfm: { r, f, m, score: rfmScore },
|
|||
|
|
level: levels[levelIndex],
|
|||
|
|
traffic_pool: pools[levelIndex],
|
|||
|
|
data_sources: ['KR_存客宝', 'KR_腾讯'],
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 模拟AI标签完善
|
|||
|
|
*/
|
|||
|
|
function enrichUserTags(phone: string, existingTags: string[] = []) {
|
|||
|
|
const profile = getMockUserProfile(phone)
|
|||
|
|
const allTags = [...new Set([...existingTags, ...profile.tags])]
|
|||
|
|
const newTags = allTags.filter(t => !existingTags.includes(t))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
newTags,
|
|||
|
|
allTags,
|
|||
|
|
rfm_score: profile.rfm.score,
|
|||
|
|
level: profile.level,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== API处理函数 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理数据流入
|
|||
|
|
*/
|
|||
|
|
async function handleIngest(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.dataIngest) {
|
|||
|
|
return { success: false, error: '无数据流入权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const body = await request.json()
|
|||
|
|
const { phone, name, source, tags = [], extra = {} } = body
|
|||
|
|
|
|||
|
|
if (!phone) {
|
|||
|
|
return { success: false, error: '缺少phone参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isValidPhone(phone)) {
|
|||
|
|
return { success: false, error: '手机号格式错误', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!source) {
|
|||
|
|
return { success: false, error: '缺少source参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟数据处理和标签完善
|
|||
|
|
const enrichResult = enrichUserTags(phone, tags)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
userId: `usr_${crypto.randomBytes(8).toString('hex')}`,
|
|||
|
|
enriched: true,
|
|||
|
|
originalTags: tags,
|
|||
|
|
newTags: enrichResult.newTags,
|
|||
|
|
allTags: enrichResult.allTags,
|
|||
|
|
rfm_score: enrichResult.rfm_score,
|
|||
|
|
user_level: enrichResult.level,
|
|||
|
|
source,
|
|||
|
|
processedAt: new Date().toISOString(),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return { success: false, error: '请求体解析失败', code: 400 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理批量数据流入
|
|||
|
|
*/
|
|||
|
|
async function handleBatchIngest(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.dataIngest) {
|
|||
|
|
return { success: false, error: '无数据流入权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!partner.permissions.batchProcess) {
|
|||
|
|
return { success: false, error: '无批量处理权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const body = await request.json()
|
|||
|
|
const { users = [], source, async: isAsync = false } = body
|
|||
|
|
|
|||
|
|
if (!Array.isArray(users) || users.length === 0) {
|
|||
|
|
return { success: false, error: '缺少users数组', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (users.length > 1000) {
|
|||
|
|
return { success: false, error: '单次最多支持1000条数据', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!source) {
|
|||
|
|
return { success: false, error: '缺少source参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isAsync) {
|
|||
|
|
// 创建异步任务
|
|||
|
|
const taskId = `task_${crypto.randomBytes(8).toString('hex')}`
|
|||
|
|
BATCH_TASKS.set(taskId, {
|
|||
|
|
id: taskId,
|
|||
|
|
partnerId: partner.id,
|
|||
|
|
type: 'ingest',
|
|||
|
|
status: 'processing',
|
|||
|
|
progress: 0,
|
|||
|
|
total: users.length,
|
|||
|
|
processed: 0,
|
|||
|
|
failed: 0,
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 模拟异步处理(实际应使用队列)
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const task = BATCH_TASKS.get(taskId)
|
|||
|
|
if (task) {
|
|||
|
|
task.status = 'completed'
|
|||
|
|
task.progress = 100
|
|||
|
|
task.processed = users.length
|
|||
|
|
task.completedAt = new Date()
|
|||
|
|
}
|
|||
|
|
}, 5000)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
taskId,
|
|||
|
|
total: users.length,
|
|||
|
|
processed: 0,
|
|||
|
|
status: 'processing',
|
|||
|
|
estimatedTime: `${Math.ceil(users.length / 100)}秒`,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 同步处理
|
|||
|
|
const results = users.map((user: { phone: string; name?: string; tags?: string[] }) => {
|
|||
|
|
if (!user.phone || !isValidPhone(user.phone)) {
|
|||
|
|
return { phone: user.phone, success: false, error: '手机号格式错误' }
|
|||
|
|
}
|
|||
|
|
const enrichResult = enrichUserTags(user.phone, user.tags || [])
|
|||
|
|
return {
|
|||
|
|
phone: maskPhone(user.phone),
|
|||
|
|
success: true,
|
|||
|
|
tags: enrichResult.allTags,
|
|||
|
|
level: enrichResult.level,
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const successful = results.filter((r: { success: boolean }) => r.success).length
|
|||
|
|
const failed = results.length - successful
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
total: users.length,
|
|||
|
|
processed: successful,
|
|||
|
|
failed,
|
|||
|
|
results,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return { success: false, error: '请求体解析失败', code: 400 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理用户查询
|
|||
|
|
*/
|
|||
|
|
async function handleQuery(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.dataQuery) {
|
|||
|
|
return { success: false, error: '无数据查询权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { searchParams } = new URL(request.url)
|
|||
|
|
const phone = searchParams.get('phone')
|
|||
|
|
const qq = searchParams.get('qq')
|
|||
|
|
const fields = searchParams.get('fields')?.split(',')
|
|||
|
|
|
|||
|
|
if (!phone && !qq) {
|
|||
|
|
return { success: false, error: '缺少phone或qq参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 优先使用手机号查询
|
|||
|
|
const queryPhone = phone || `138${Math.floor(Math.random() * 100000000).toString().padStart(8, '0')}`
|
|||
|
|
|
|||
|
|
if (phone && !isValidPhone(phone)) {
|
|||
|
|
return { success: false, error: '手机号格式错误', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const profile = getMockUserProfile(queryPhone)
|
|||
|
|
|
|||
|
|
// 根据fields参数过滤返回字段
|
|||
|
|
if (fields && fields.length > 0) {
|
|||
|
|
const filteredProfile: Record<string, any> = {}
|
|||
|
|
fields.forEach(field => {
|
|||
|
|
if (field in profile) {
|
|||
|
|
filteredProfile[field] = (profile as Record<string, any>)[field]
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
filteredProfile.phone = profile.phone // 始终返回phone
|
|||
|
|
return { success: true, data: filteredProfile }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { success: true, data: profile }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理批量用户查询
|
|||
|
|
*/
|
|||
|
|
async function handleBatchQuery(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.dataQuery) {
|
|||
|
|
return { success: false, error: '无数据查询权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const body = await request.json()
|
|||
|
|
const { phones = [], qqs = [], fields = [] } = body
|
|||
|
|
|
|||
|
|
const identifiers = [...phones, ...qqs]
|
|||
|
|
if (identifiers.length === 0) {
|
|||
|
|
return { success: false, error: '缺少phones或qqs参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (identifiers.length > 500) {
|
|||
|
|
return { success: false, error: '单次最多支持500条查询', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const users: any[] = []
|
|||
|
|
const notFound: string[] = []
|
|||
|
|
|
|||
|
|
for (const phone of phones) {
|
|||
|
|
if (!isValidPhone(phone)) {
|
|||
|
|
notFound.push(phone)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟30%的用户不存在
|
|||
|
|
if (Math.random() > 0.7) {
|
|||
|
|
notFound.push(phone)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const profile = getMockUserProfile(phone)
|
|||
|
|
|
|||
|
|
if (fields.length > 0) {
|
|||
|
|
const filteredProfile: Record<string, any> = { phone: profile.phone }
|
|||
|
|
fields.forEach((field: string) => {
|
|||
|
|
if (field in profile) {
|
|||
|
|
filteredProfile[field] = (profile as Record<string, any>)[field]
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
users.push(filteredProfile)
|
|||
|
|
} else {
|
|||
|
|
users.push(profile)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
users,
|
|||
|
|
notFound,
|
|||
|
|
total: phones.length,
|
|||
|
|
found: users.length,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return { success: false, error: '请求体解析失败', code: 400 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理标签完善
|
|||
|
|
*/
|
|||
|
|
async function handleTagEnrich(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.tagEnrich) {
|
|||
|
|
return { success: false, error: '无标签完善权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const body = await request.json()
|
|||
|
|
const { phone, strategy = 'rfm', force = false } = body
|
|||
|
|
|
|||
|
|
if (!phone) {
|
|||
|
|
return { success: false, error: '缺少phone参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isValidPhone(phone)) {
|
|||
|
|
return { success: false, error: '手机号格式错误', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const enrichResult = enrichUserTags(phone, [])
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
phone: maskPhone(phone),
|
|||
|
|
strategy,
|
|||
|
|
force,
|
|||
|
|
newTags: enrichResult.newTags,
|
|||
|
|
allTags: enrichResult.allTags,
|
|||
|
|
rfm_score: enrichResult.rfm_score,
|
|||
|
|
level: enrichResult.level,
|
|||
|
|
enrichedAt: new Date().toISOString(),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return { success: false, error: '请求体解析失败', code: 400 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理标签列表查询
|
|||
|
|
*/
|
|||
|
|
async function handleTagList(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.dataQuery) {
|
|||
|
|
return { success: false, error: '无数据查询权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { searchParams } = new URL(request.url)
|
|||
|
|
const category = searchParams.get('category')
|
|||
|
|
const source = searchParams.get('source')
|
|||
|
|
|
|||
|
|
// 模拟标签列表
|
|||
|
|
const allTags = [
|
|||
|
|
{ id: 'tag_001', name: '高价值用户', category: 'value', count: 1250000 },
|
|||
|
|
{ id: 'tag_002', name: '优质用户', category: 'value', count: 8500000 },
|
|||
|
|
{ id: 'tag_003', name: '普通用户', category: 'value', count: 32000000 },
|
|||
|
|
{ id: 'tag_004', name: '待激活用户', category: 'value', count: 68000000 },
|
|||
|
|
{ id: 'tag_005', name: '高频活跃', category: 'behavior', count: 5600000 },
|
|||
|
|
{ id: 'tag_006', name: '沉默用户', category: 'behavior', count: 12000000 },
|
|||
|
|
{ id: 'tag_007', name: '新用户', category: 'behavior', count: 3200000 },
|
|||
|
|
{ id: 'tag_008', name: '流失风险', category: 'behavior', count: 4500000 },
|
|||
|
|
{ id: 'tag_009', name: '一线城市', category: 'region', count: 15000000 },
|
|||
|
|
{ id: 'tag_010', name: '二线城市', category: 'region', count: 28000000 },
|
|||
|
|
{ id: 'tag_011', name: '电商活跃', category: 'preference', count: 8900000 },
|
|||
|
|
{ id: 'tag_012', name: '社交达人', category: 'preference', count: 4200000 },
|
|||
|
|
{ id: 'tag_013', name: '金融偏好', category: 'preference', count: 2100000 },
|
|||
|
|
{ id: 'tag_014', name: '科技爱好者', category: 'preference', count: 3600000 },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
let filteredTags = allTags
|
|||
|
|
if (category) {
|
|||
|
|
filteredTags = filteredTags.filter(t => t.category === category)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
tags: filteredTags,
|
|||
|
|
total: filteredTags.length,
|
|||
|
|
categories: ['value', 'behavior', 'region', 'preference'],
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理批量任务创建
|
|||
|
|
*/
|
|||
|
|
async function handleBatchTask(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
if (!partner.permissions.batchProcess) {
|
|||
|
|
return { success: false, error: '无批量处理权限', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const body = await request.json()
|
|||
|
|
const { type, config, callback } = body
|
|||
|
|
|
|||
|
|
if (!type) {
|
|||
|
|
return { success: false, error: '缺少type参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!['enrich', 'export', 'sync'].includes(type)) {
|
|||
|
|
return { success: false, error: '不支持的任务类型', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const taskId = `task_${crypto.randomBytes(8).toString('hex')}`
|
|||
|
|
|
|||
|
|
BATCH_TASKS.set(taskId, {
|
|||
|
|
id: taskId,
|
|||
|
|
partnerId: partner.id,
|
|||
|
|
type,
|
|||
|
|
status: 'pending',
|
|||
|
|
progress: 0,
|
|||
|
|
total: 0,
|
|||
|
|
processed: 0,
|
|||
|
|
failed: 0,
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 模拟异步处理
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const task = BATCH_TASKS.get(taskId)
|
|||
|
|
if (task) {
|
|||
|
|
task.status = 'processing'
|
|||
|
|
task.total = 10000
|
|||
|
|
}
|
|||
|
|
}, 1000)
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const task = BATCH_TASKS.get(taskId)
|
|||
|
|
if (task) {
|
|||
|
|
task.status = 'completed'
|
|||
|
|
task.progress = 100
|
|||
|
|
task.processed = 9995
|
|||
|
|
task.failed = 5
|
|||
|
|
task.completedAt = new Date()
|
|||
|
|
}
|
|||
|
|
}, 10000)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
taskId,
|
|||
|
|
type,
|
|||
|
|
status: 'pending',
|
|||
|
|
estimatedTime: '10分钟',
|
|||
|
|
callback: callback || null,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return { success: false, error: '请求体解析失败', code: 400 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理批量任务状态查询
|
|||
|
|
*/
|
|||
|
|
async function handleBatchStatus(request: NextRequest, partner: Partner): Promise<APIResponse> {
|
|||
|
|
const { searchParams } = new URL(request.url)
|
|||
|
|
const taskId = searchParams.get('taskId')
|
|||
|
|
|
|||
|
|
if (!taskId) {
|
|||
|
|
return { success: false, error: '缺少taskId参数', code: 400 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const task = BATCH_TASKS.get(taskId)
|
|||
|
|
|
|||
|
|
if (!task) {
|
|||
|
|
return { success: false, error: '任务不存在', code: 404 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (task.partnerId !== partner.id) {
|
|||
|
|
return { success: false, error: '无权查看此任务', code: 403 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
taskId: task.id,
|
|||
|
|
type: task.type,
|
|||
|
|
status: task.status,
|
|||
|
|
progress: task.progress,
|
|||
|
|
total: task.total,
|
|||
|
|
processed: task.processed,
|
|||
|
|
failed: task.failed,
|
|||
|
|
createdAt: task.createdAt.toISOString(),
|
|||
|
|
completedAt: task.completedAt?.toISOString() || null,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取接入方列表
|
|||
|
|
*/
|
|||
|
|
async function handleGetPartners(): Promise<APIResponse> {
|
|||
|
|
const partners = Array.from(PARTNERS.values()).map(p => ({
|
|||
|
|
id: p.id,
|
|||
|
|
name: p.name,
|
|||
|
|
status: p.status,
|
|||
|
|
permissions: p.permissions,
|
|||
|
|
quotaConfig: p.quotaConfig,
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
data: { partners, total: partners.length },
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 路由处理 ====================
|
|||
|
|
|
|||
|
|
export async function GET(request: NextRequest) {
|
|||
|
|
const { searchParams } = new URL(request.url)
|
|||
|
|
const action = searchParams.get('action')
|
|||
|
|
|
|||
|
|
// 获取接入方列表(管理接口,不需要认证)
|
|||
|
|
if (action === 'partners') {
|
|||
|
|
const result = await handleGetPartners()
|
|||
|
|
return NextResponse.json(result)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他GET请求需要认证
|
|||
|
|
const validation = validateRequest(request)
|
|||
|
|
if (!validation.valid) {
|
|||
|
|
return NextResponse.json(
|
|||
|
|
{ success: false, error: validation.error, code: 401 },
|
|||
|
|
{ status: 401 }
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const partner = validation.partner!
|
|||
|
|
const endpoint = searchParams.get('endpoint')
|
|||
|
|
|
|||
|
|
let result: APIResponse
|
|||
|
|
|
|||
|
|
switch (endpoint) {
|
|||
|
|
case 'query/user':
|
|||
|
|
result = await handleQuery(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'tag/list':
|
|||
|
|
result = await handleTagList(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'batch/status':
|
|||
|
|
result = await handleBatchStatus(request, partner)
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
result = { success: false, error: '未知的API端点', code: 404 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return NextResponse.json(result, { status: result.code || 200 })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function POST(request: NextRequest) {
|
|||
|
|
const validation = validateRequest(request)
|
|||
|
|
if (!validation.valid) {
|
|||
|
|
return NextResponse.json(
|
|||
|
|
{ success: false, error: validation.error, code: 401 },
|
|||
|
|
{ status: 401 }
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const partner = validation.partner!
|
|||
|
|
const { searchParams } = new URL(request.url)
|
|||
|
|
const endpoint = searchParams.get('endpoint')
|
|||
|
|
|
|||
|
|
let result: APIResponse
|
|||
|
|
|
|||
|
|
switch (endpoint) {
|
|||
|
|
case 'ingest/user':
|
|||
|
|
result = await handleIngest(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'ingest/batch':
|
|||
|
|
result = await handleBatchIngest(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'query/batch':
|
|||
|
|
result = await handleBatchQuery(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'tag/enrich':
|
|||
|
|
result = await handleTagEnrich(request, partner)
|
|||
|
|
break
|
|||
|
|
case 'batch/task':
|
|||
|
|
result = await handleBatchTask(request, partner)
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
result = { success: false, error: '未知的API端点', code: 404 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return NextResponse.json(result, { status: result.code || 200 })
|
|||
|
|
}
|