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)}个月前`
|
|||
|
|
}
|