526 lines
14 KiB
TypeScript
526 lines
14 KiB
TypeScript
/**
|
||
* 存客宝双向同步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 })
|
||
}
|
||
}
|