Files
users/app/api/open-api/route.ts

769 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 神射手开放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 })
}