Next.js + NextAuth用户会话追踪方案咨询(含DAU统计需求)
解决方案:用户会话追踪与DAU计算(Next.js + Prisma + NextAuth)
1. 数据库模型设计(Prisma Schema)
首先扩展Prisma模型,新增会话追踪表关联用户与NextAuth会话,记录核心信息:
model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? sessions Session[] // NextAuth默认会话关联 sessionTracks SessionTrack[] // 新增会话追踪记录关联 } model SessionTrack { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) sessionId String // 关联NextAuth的Session.id startTime DateTime @default(now()) endTime DateTime? lastActiveTime DateTime @default(now()) // 可选:记录用户最后活跃时间 ipAddress String? // 可选:记录访问IP userAgent String? // 可选:记录客户端信息 @@unique([userId, sessionId]) // 确保一个用户的单个NextAuth会话对应一条追踪记录 }
2. 会话追踪逻辑实现
2.1 登录时初始化会话记录
在NextAuth的signIn回调中,用户登录成功后自动创建会话追踪记录:
import NextAuth from "next-auth"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import prisma from "@/lib/prisma"; export const authOptions = { adapter: PrismaAdapter(prisma), callbacks: { async signIn({ user, session, req }) { await prisma.sessionTrack.create({ data: { userId: user.id, sessionId: session.id, ipAddress: req.ip ?? null, userAgent: req.headers["user-agent"] ?? null, }, }); return true; }, }, // 其他NextAuth配置(Providers、session策略等) }; export default NextAuth(authOptions);
2.2 处理会话结束
主动登出时标记结束
在signOut回调中更新会话结束时间:
callbacks: { // ...其他回调 async signOut({ session }) { await prisma.sessionTrack.updateMany({ where: { sessionId: session.id, endTime: null, }, data: { endTime: new Date() }, }); return true; }, },
自动过期会话处理
通过定时任务清理过期会话并标记结束(可使用node-cron实现):
import cron from "node-cron"; import prisma from "@/lib/prisma"; // 每天凌晨1点执行 cron.schedule("0 1 * * *", async () => { // 找出所有已过期的NextAuth会话 const expiredSessions = await prisma.session.findMany({ where: { expires: { lt: new Date() } }, select: { id: true }, }); // 标记对应会话追踪记录为结束 await prisma.sessionTrack.updateMany({ where: { sessionId: { in: expiredSessions.map(s => s.id) }, endTime: null, }, data: { endTime: new Date() }, }); });
前端关闭页面时标记结束(可选)
监听前端beforeunload事件,发送请求标记会话结束:
// 全局脚本或布局组件中 window.addEventListener('beforeunload', async () => { try { await fetch('/api/end-session', { method: 'POST', credentials: 'include', }); } catch (e) { // 忽略请求失败,beforeunload可能中断请求 } });
对应API路由pages/api/end-session.ts:
import { getServerSession } from "next-auth/next"; import { authOptions } from "../auth/[...nextauth]"; import prisma from "@/lib/prisma"; export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end(); const session = await getServerSession(req, res, authOptions); if (!session) return res.status(401).end(); await prisma.sessionTrack.updateMany({ where: { sessionId: session.id, endTime: null }, data: { endTime: new Date() }, }); res.status(200).json({ success: true }); }
2.3 更新用户活跃时间(可选)
在受保护路由或Middleware中,定期更新用户最后活跃时间(避免频繁更新,可设置5分钟间隔):
// Middleware示例 import { getServerSession } from "next-auth/next"; import { authOptions } from "./auth/[...nextauth]"; import prisma from "@/lib/prisma"; export async function middleware(req, res) { const session = await getServerSession(req, res, authOptions); if (session) { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); await prisma.sessionTrack.updateMany({ where: { sessionId: session.id, endTime: null, lastActiveTime: { lt: fiveMinutesAgo }, }, data: { lastActiveTime: new Date() }, }); } return NextResponse.next(); }
3. DAU计算逻辑
DAU定义为当日有活跃行为的用户数,可根据业务需求选择查询方式:
3.1 基础DAU(当日启动新会话的用户)
async function getDAU() { const today = new Date(); const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const endOfDay = new Date(startOfDay); endOfDay.setDate(endOfDay.getDate() + 1); const activeUsers = await prisma.sessionTrack.groupBy({ by: ['userId'], where: { startTime: { gte: startOfDay, lt: endOfDay } }, }); return activeUsers.length; }
3.2 精确DAU(当日有任何活跃的用户)
包含当日启动新会话或已有会话在当日活跃的用户:
async function getDAU() { const today = new Date(); const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const endOfDay = new Date(startOfDay); endOfDay.setDate(endOfDay.getDate() + 1); const activeUsers = await prisma.sessionTrack.groupBy({ by: ['userId'], where: { OR: [ { startTime: { gte: startOfDay, lt: endOfDay } }, { lastActiveTime: { gte: startOfDay, lt: endOfDay } }, ], }, }); return activeUsers.length; }
4. 注意事项
- 性能优化:更新活跃时间时设置时间间隔,避免高频数据库操作;对会话追踪表的
userId、startTime、lastActiveTime字段建立索引。 - 隐私合规:若记录IP、UserAgent等信息,需符合GDPR等隐私法规,提前告知用户并获取同意。
- 定时任务部署:Vercel等无服务器环境需用Serverless Functions或第三方定时服务(如Cron-Job.org)执行定时任务。
内容的提问来源于stack exchange,提问作者E11even
相关产品推荐
相关产品推荐

