功能迭代:用户管理与存客宝同步、管理后台与小程序优化、开发文档更新
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
525
app/api/ckb/sync/route.ts
Normal file
525
app/api/ckb/sync/route.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* 存客宝双向同步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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user