Files
soul/app/api/ckb/sync/route.ts

526 lines
14 KiB
TypeScript
Raw 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
*
* 功能:
* 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 })
}
}