/** * 神射手开放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 { success: boolean data?: T error?: string code?: number } // ==================== 模拟数据存储 ==================== // 模拟接入方数据(实际应存储在数据库中) const PARTNERS: Map = 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 = new Map() // 模拟批量任务存储 const BATCH_TASKS: Map = 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 { 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 { 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 { 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 = {} fields.forEach(field => { if (field in profile) { filteredProfile[field] = (profile as Record)[field] } }) filteredProfile.phone = profile.phone // 始终返回phone return { success: true, data: filteredProfile } } return { success: true, data: profile } } /** * 处理批量用户查询 */ async function handleBatchQuery(request: NextRequest, partner: Partner): Promise { 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 = { phone: profile.phone } fields.forEach((field: string) => { if (field in profile) { filteredProfile[field] = (profile as Record)[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 { 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 { 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 { 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 { 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 { 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 }) }