222 lines
6.1 KiB
TypeScript
222 lines
6.1 KiB
TypeScript
/**
|
||
* 用户行为轨迹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)}个月前`
|
||
}
|