功能迭代:用户管理与存客宝同步、管理后台与小程序优化、开发文档更新
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
221
app/api/user/track/route.ts
Normal file
221
app/api/user/track/route.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 用户行为轨迹API
|
||||
*
|
||||
* 记录用户的所有行为:
|
||||
* - 查看章节
|
||||
* - 购买行为
|
||||
* - 匹配操作
|
||||
* - 登录/注册
|
||||
* - 分享行为
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
// 行为类型枚举
|
||||
const ActionTypes = {
|
||||
VIEW_CHAPTER: 'view_chapter', // 查看章节
|
||||
PURCHASE: 'purchase', // 购买
|
||||
MATCH: 'match', // 匹配
|
||||
LOGIN: 'login', // 登录
|
||||
REGISTER: 'register', // 注册
|
||||
SHARE: 'share', // 分享
|
||||
BIND_PHONE: 'bind_phone', // 绑定手机
|
||||
BIND_WECHAT: 'bind_wechat', // 绑定微信
|
||||
WITHDRAW: 'withdraw', // 提现
|
||||
REFERRAL_CLICK: 'referral_click', // 推荐链接点击
|
||||
REFERRAL_BIND: 'referral_bind', // 推荐绑定
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 记录用户行为
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { userId, phone, action, target, extraData } = body
|
||||
|
||||
if (!userId && !phone) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '需要用户ID或手机号'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '行为类型不能为空'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 如果只有手机号,查找用户ID
|
||||
let targetUserId = userId
|
||||
if (!userId && phone) {
|
||||
const users = await query('SELECT id FROM users WHERE phone = ?', [phone]) as any[]
|
||||
if (users.length > 0) {
|
||||
targetUserId = users[0].id
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// 记录行为轨迹
|
||||
const trackId = 'track_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6)
|
||||
|
||||
await query(`
|
||||
INSERT INTO user_tracks (
|
||||
id, user_id, action, chapter_id, target, extra_data
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
trackId,
|
||||
targetUserId,
|
||||
action,
|
||||
action === 'view_chapter' ? target : null,
|
||||
target || null,
|
||||
extraData ? JSON.stringify(extraData) : null
|
||||
])
|
||||
|
||||
// 更新用户最后活跃时间
|
||||
await query('UPDATE users SET updated_at = NOW() WHERE id = ?', [targetUserId])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
trackId,
|
||||
message: '行为记录成功'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[User Track] POST 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 userId = searchParams.get('userId')
|
||||
const phone = searchParams.get('phone')
|
||||
const action = searchParams.get('action')
|
||||
const limit = parseInt(searchParams.get('limit') || '50')
|
||||
|
||||
try {
|
||||
// 确定查询条件
|
||||
let targetUserId = userId
|
||||
if (!userId && phone) {
|
||||
const users = await query('SELECT id FROM users WHERE phone = ?', [phone]) as any[]
|
||||
if (users.length > 0) {
|
||||
targetUserId = users[0].id
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetUserId) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '需要用户ID或手机号'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
let sql = `
|
||||
SELECT ut.*
|
||||
FROM user_tracks ut
|
||||
WHERE ut.user_id = ?
|
||||
`
|
||||
const params: any[] = [targetUserId]
|
||||
|
||||
if (action) {
|
||||
sql += ' AND ut.action = ?'
|
||||
params.push(action)
|
||||
}
|
||||
|
||||
sql += ' ORDER BY ut.created_at DESC LIMIT ?'
|
||||
params.push(limit)
|
||||
|
||||
const tracks = await query(sql, params) as any[]
|
||||
|
||||
// 格式化轨迹数据
|
||||
const formattedTracks = tracks.map(t => ({
|
||||
id: t.id,
|
||||
action: t.action,
|
||||
actionLabel: getActionLabel(t.action),
|
||||
target: t.target || t.chapter_id,
|
||||
chapterTitle: t.chapter_id || null, // 直接使用chapter_id作为标题
|
||||
extraData: t.extra_data ? JSON.parse(t.extra_data) : null,
|
||||
createdAt: t.created_at,
|
||||
timeAgo: getTimeAgo(t.created_at)
|
||||
}))
|
||||
|
||||
// 统计信息
|
||||
const stats = await query(`
|
||||
SELECT
|
||||
action,
|
||||
COUNT(*) as count
|
||||
FROM user_tracks
|
||||
WHERE user_id = ?
|
||||
GROUP BY action
|
||||
`, [targetUserId]) as any[]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tracks: formattedTracks,
|
||||
stats: stats.reduce((acc, s) => ({ ...acc, [s.action]: s.count }), {}),
|
||||
total: formattedTracks.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[User Track] GET Error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取行为轨迹失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取行为标签
|
||||
function getActionLabel(action: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'view_chapter': '查看章节',
|
||||
'purchase': '购买',
|
||||
'match': '匹配伙伴',
|
||||
'login': '登录',
|
||||
'register': '注册',
|
||||
'share': '分享',
|
||||
'bind_phone': '绑定手机',
|
||||
'bind_wechat': '绑定微信',
|
||||
'withdraw': '提现',
|
||||
'referral_click': '点击推荐链接',
|
||||
'referral_bind': '推荐绑定',
|
||||
}
|
||||
return labels[action] || action
|
||||
}
|
||||
|
||||
// 获取相对时间
|
||||
function getTimeAgo(date: string | Date): string {
|
||||
const now = new Date()
|
||||
const then = new Date(date)
|
||||
const diffMs = now.getTime() - then.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMins < 1) return '刚刚'
|
||||
if (diffMins < 60) return `${diffMins}分钟前`
|
||||
if (diffHours < 24) return `${diffHours}小时前`
|
||||
if (diffDays < 7) return `${diffDays}天前`
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)}周前`
|
||||
return `${Math.floor(diffDays / 30)}个月前`
|
||||
}
|
||||
Reference in New Issue
Block a user