功能迭代:用户管理与存客宝同步、管理后台与小程序优化、开发文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-01-29 17:15:00 +08:00
parent 8f01de4f9a
commit d87fa5c175
18 changed files with 2693 additions and 149 deletions

525
app/api/ckb/sync/route.ts Normal file
View 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 })
}
}