Files
soul/app/api/user/track/route.ts

222 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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