/** * 存客宝双向同步API * * 功能: * 1. 从存客宝拉取用户数据(按手机号) * 2. 将本系统用户数据同步到存客宝 * 3. 合并标签体系 * 4. 同步行为轨迹 * * 手机号为唯一主键 */ import { NextRequest, NextResponse } from 'next/server' import { query } from '@/lib/db' // 存客宝API配置(需要替换为实际配置) const CKB_API_BASE = process.env.CKB_API_BASE || 'https://api.cunkebao.com' const CKB_API_KEY = process.env.CKB_API_KEY || '' /** * POST - 执行同步操作 */ export async function POST(request: NextRequest) { try { const body = await request.json() const { action, phone, userId, userData, trackData } = body switch (action) { case 'pull': // 从存客宝拉取用户数据 return await pullFromCKB(phone) case 'push': // 推送用户数据到存客宝 return await pushToCKB(phone, userData) case 'sync_tags': // 同步标签 return await syncTags(phone, userId) case 'sync_track': // 同步行为轨迹 return await syncTrack(phone, trackData) case 'full_sync': // 完整双向同步 return await fullSync(phone, userId) case 'batch_sync': // 批量同步所有用户 return await batchSync() default: return NextResponse.json({ success: false, error: '未知操作类型' }, { status: 400 }) } } catch (error) { console.error('[CKB Sync] Error:', error) return NextResponse.json({ success: false, error: '同步失败: ' + (error as Error).message }, { status: 500 }) } } /** * GET - 获取同步状态 */ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const phone = searchParams.get('phone') try { if (phone) { // 获取单个用户的同步状态 const users = await query(` SELECT id, phone, nickname, ckb_synced_at, ckb_user_id, tags, ckb_tags, source_tags FROM users WHERE phone = ? `, [phone]) as any[] if (users.length === 0) { return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) } return NextResponse.json({ success: true, syncStatus: { user: users[0], isSynced: !!users[0].ckb_synced_at, lastSyncTime: users[0].ckb_synced_at } }) } // 获取整体同步统计 const stats = await query(` SELECT COUNT(*) as total, SUM(CASE WHEN ckb_synced_at IS NOT NULL THEN 1 ELSE 0 END) as synced, SUM(CASE WHEN phone IS NOT NULL THEN 1 ELSE 0 END) as has_phone FROM users `) as any[] return NextResponse.json({ success: true, stats: stats[0] }) } catch (error) { console.error('[CKB Sync] GET Error:', error) return NextResponse.json({ success: false, error: '获取同步状态失败' }, { status: 500 }) } } /** * 从存客宝拉取用户数据 */ async function pullFromCKB(phone: string) { if (!phone) { return NextResponse.json({ success: false, error: '手机号不能为空' }, { status: 400 }) } try { // 调用存客宝API获取用户数据 // 注意:需要根据实际存客宝API文档调整 const ckbResponse = await fetch(`${CKB_API_BASE}/api/user/get`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CKB_API_KEY}` }, body: JSON.stringify({ phone }) }).catch(() => null) let ckbData = null if (ckbResponse && ckbResponse.ok) { ckbData = await ckbResponse.json() } // 查找本地用户 const localUsers = await query('SELECT * FROM users WHERE phone = ?', [phone]) as any[] if (localUsers.length === 0 && !ckbData) { return NextResponse.json({ success: false, error: '用户不存在于本系统和存客宝' }, { status: 404 }) } // 如果存客宝有数据,更新本地 if (ckbData && ckbData.success && ckbData.user) { const ckbUser = ckbData.user if (localUsers.length > 0) { // 更新已有用户 await query(` UPDATE users SET ckb_user_id = ?, ckb_tags = ?, ckb_synced_at = NOW(), updated_at = NOW() WHERE phone = ? `, [ ckbUser.id || null, JSON.stringify(ckbUser.tags || []), phone ]) } else { // 创建新用户 const userId = 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9) const referralCode = 'SOUL' + phone.slice(-4).toUpperCase() await query(` INSERT INTO users ( id, phone, nickname, referral_code, ckb_user_id, ckb_tags, ckb_synced_at, has_full_book, is_admin, earnings, pending_earnings, referral_count ) VALUES (?, ?, ?, ?, ?, ?, NOW(), FALSE, FALSE, 0, 0, 0) `, [ userId, phone, ckbUser.nickname || '用户' + phone.slice(-4), referralCode, ckbUser.id || null, JSON.stringify(ckbUser.tags || []) ]) } } // 返回合并后的用户数据 const updatedUsers = await query('SELECT * FROM users WHERE phone = ?', [phone]) as any[] return NextResponse.json({ success: true, user: updatedUsers[0], ckbData: ckbData?.user || null, message: '数据拉取成功' }) } catch (error) { console.error('[CKB Pull] Error:', error) return NextResponse.json({ success: false, error: '拉取存客宝数据失败: ' + (error as Error).message }, { status: 500 }) } } /** * 推送用户数据到存客宝 */ async function pushToCKB(phone: string, userData: any) { if (!phone) { return NextResponse.json({ success: false, error: '手机号不能为空' }, { status: 400 }) } try { // 获取本地用户数据 const localUsers = await query(` SELECT u.*, (SELECT JSON_ARRAYAGG(JSON_OBJECT( 'chapter_id', ut.chapter_id, 'action', ut.action, 'created_at', ut.created_at )) FROM user_tracks ut WHERE ut.user_id = u.id ORDER BY ut.created_at DESC LIMIT 50) as tracks FROM users u WHERE u.phone = ? `, [phone]) as any[] if (localUsers.length === 0) { return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) } const localUser = localUsers[0] // 构建推送数据 const pushData = { phone, nickname: localUser.nickname, source: 'soul_miniprogram', tags: [ ...(localUser.tags ? JSON.parse(localUser.tags) : []), localUser.has_full_book ? '已购全书' : '未购买', localUser.referral_count > 0 ? `推荐${localUser.referral_count}人` : null ].filter(Boolean), tracks: localUser.tracks ? JSON.parse(localUser.tracks) : [], customData: userData || {} } // 调用存客宝API const ckbResponse = await fetch(`${CKB_API_BASE}/api/user/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CKB_API_KEY}` }, body: JSON.stringify(pushData) }).catch(() => null) let ckbResult = null if (ckbResponse && ckbResponse.ok) { ckbResult = await ckbResponse.json() } // 更新本地同步时间 await query(` UPDATE users SET ckb_synced_at = NOW(), updated_at = NOW() WHERE phone = ? `, [phone]) return NextResponse.json({ success: true, pushed: pushData, ckbResult, message: '数据推送成功' }) } catch (error) { console.error('[CKB Push] Error:', error) return NextResponse.json({ success: false, error: '推送数据到存客宝失败: ' + (error as Error).message }, { status: 500 }) } } /** * 同步标签 */ async function syncTags(phone: string, userId: string) { try { const id = phone || userId const field = phone ? 'phone' : 'id' // 获取本地用户 const users = await query(`SELECT * FROM users WHERE ${field} = ?`, [id]) as any[] if (users.length === 0) { return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) } const user = users[0] // 合并标签 const localTags = user.tags ? JSON.parse(user.tags) : [] const ckbTags = user.ckb_tags ? JSON.parse(user.ckb_tags) : [] const sourceTags = user.source_tags ? JSON.parse(user.source_tags) : [] // 去重合并 const mergedTags = [...new Set([...localTags, ...ckbTags, ...sourceTags])] // 更新合并后的标签 await query(` UPDATE users SET merged_tags = ?, updated_at = NOW() WHERE ${field} = ? `, [JSON.stringify(mergedTags), id]) return NextResponse.json({ success: true, tags: { local: localTags, ckb: ckbTags, source: sourceTags, merged: mergedTags }, message: '标签同步成功' }) } catch (error) { console.error('[Sync Tags] Error:', error) return NextResponse.json({ success: false, error: '同步标签失败' }, { status: 500 }) } } /** * 同步行为轨迹 */ async function syncTrack(phone: string, trackData: any) { if (!phone) { return NextResponse.json({ success: false, error: '手机号不能为空' }, { status: 400 }) } try { // 获取用户ID const users = await query('SELECT id FROM users WHERE phone = ?', [phone]) as any[] if (users.length === 0) { return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) } const userId = users[0].id // 获取本地行为轨迹 const tracks = await query(` SELECT * FROM user_tracks WHERE user_id = ? ORDER BY created_at DESC LIMIT 100 `, [userId]) as any[] // 推送到存客宝 const pushData = { phone, tracks: tracks.map(t => ({ action: t.action, target: t.chapter_id || t.target, timestamp: t.created_at, data: t.extra_data ? JSON.parse(t.extra_data) : {} })) } const ckbResponse = await fetch(`${CKB_API_BASE}/api/track/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CKB_API_KEY}` }, body: JSON.stringify(pushData) }).catch(() => null) return NextResponse.json({ success: true, tracksCount: tracks.length, synced: ckbResponse?.ok || false, message: '行为轨迹同步成功' }) } catch (error) { console.error('[Sync Track] Error:', error) return NextResponse.json({ success: false, error: '同步行为轨迹失败' }, { status: 500 }) } } /** * 完整双向同步 */ async function fullSync(phone: string, userId: string) { try { const id = phone || userId if (!id) { return NextResponse.json({ success: false, error: '需要手机号或用户ID' }, { status: 400 }) } // 如果只有userId,先获取手机号 let targetPhone = phone if (!phone && userId) { const users = await query('SELECT phone FROM users WHERE id = ?', [userId]) as any[] if (users.length > 0 && users[0].phone) { targetPhone = users[0].phone } } if (!targetPhone) { return NextResponse.json({ success: false, error: '用户未绑定手机号,无法同步存客宝' }, { status: 400 }) } // 1. 拉取存客宝数据 const pullResult = await pullFromCKB(targetPhone) const pullData = await pullResult.json() // 2. 同步标签 const tagsResult = await syncTags(targetPhone, '') const tagsData = await tagsResult.json() // 3. 推送本地数据 const pushResult = await pushToCKB(targetPhone, {}) const pushData = await pushResult.json() // 4. 同步行为轨迹 const trackResult = await syncTrack(targetPhone, {}) const trackData = await trackResult.json() return NextResponse.json({ success: true, phone: targetPhone, results: { pull: pullData, tags: tagsData, push: pushData, track: trackData }, message: '完整双向同步成功' }) } catch (error) { console.error('[Full Sync] Error:', error) return NextResponse.json({ success: false, error: '完整同步失败: ' + (error as Error).message }, { status: 500 }) } } /** * 批量同步所有有手机号的用户 */ async function batchSync() { try { // 获取所有有手机号的用户 const users = await query(` SELECT id, phone, nickname FROM users WHERE phone IS NOT NULL ORDER BY updated_at DESC LIMIT 100 `) as any[] const results = { total: users.length, success: 0, failed: 0, details: [] as any[] } // 逐个同步(避免并发过高) for (const user of users) { try { // 推送到存客宝 await pushToCKB(user.phone, {}) results.success++ results.details.push({ phone: user.phone, status: 'success' }) } catch (e) { results.failed++ results.details.push({ phone: user.phone, status: 'failed', error: (e as Error).message }) } // 添加延迟避免请求过快 await new Promise(resolve => setTimeout(resolve, 100)) } return NextResponse.json({ success: true, results, message: `批量同步完成: ${results.success}/${results.total} 成功` }) } catch (error) { console.error('[Batch Sync] Error:', error) return NextResponse.json({ success: false, error: '批量同步失败' }, { status: 500 }) } }