chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

768
app/api/open-api/route.ts Normal file
View File

@@ -0,0 +1,768 @@
/**
* 神射手开放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 })
}