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

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

221
app/api/user/track/route.ts Normal file
View 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)}个月前`
}