parent
27d65ea08f
commit
b43fa24165
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,162 @@
|
||||
// 使用统计和管理员路由
|
||||
import express from 'express'
|
||||
import { db } from '../db/database.js'
|
||||
import { requireAdmin } from './auth.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 记录事件(前端调用)
|
||||
router.post('/event', (req, res) => {
|
||||
try {
|
||||
const { userId, guestId, eventType, eventData } = req.body
|
||||
|
||||
if (!eventType) {
|
||||
return res.status(400).json({ error: '事件类型不能为空' })
|
||||
}
|
||||
|
||||
db.prepare('INSERT INTO stats (user_id, guest_id, event_type, event_data) VALUES (?, ?, ?, ?)').run(
|
||||
userId || null,
|
||||
guestId || null,
|
||||
eventType,
|
||||
eventData ? JSON.stringify(eventData) : null
|
||||
)
|
||||
|
||||
res.json({ message: '事件记录成功' })
|
||||
} catch (err) {
|
||||
console.error('记录事件失败:', err)
|
||||
res.status(500).json({ error: '记录事件失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 管理员:获取总体统计数据
|
||||
router.get('/admin/overview', requireAdmin, (req, res) => {
|
||||
try {
|
||||
// 用户总数
|
||||
const totalUsers = db.prepare('SELECT COUNT(*) as count FROM users').get()
|
||||
|
||||
// 管理员数量
|
||||
const adminCount = db.prepare('SELECT COUNT(*) as count FROM users WHERE role = ?', ['admin']).get()
|
||||
|
||||
// 总计划数
|
||||
const totalPlans = db.prepare('SELECT COUNT(*) as count FROM plans').get()
|
||||
|
||||
// 游客计划数(未过期)
|
||||
const guestPlans = db.prepare('SELECT COUNT(*) as count FROM guest_plans WHERE expires_at > ?', [new Date().toISOString()]).get()
|
||||
|
||||
// 总事件数
|
||||
const totalEvents = db.prepare('SELECT COUNT(*) as count FROM stats').get()
|
||||
|
||||
// 今日事件数
|
||||
const todayEvents = db.prepare(`SELECT COUNT(*) as count FROM stats WHERE date(created_at) = date('now')`).get()
|
||||
|
||||
// 今日活跃用户
|
||||
const todayActiveUsers = db.prepare(`SELECT COUNT(DISTINCT user_id) as count FROM stats WHERE date(created_at) = date('now') AND user_id IS NOT NULL`).get()
|
||||
|
||||
// 今日活跃游客
|
||||
const todayActiveGuests = db.prepare(`SELECT COUNT(DISTINCT guest_id) as count FROM stats WHERE date(created_at) = date('now') AND guest_id IS NOT NULL`).get()
|
||||
|
||||
res.json({
|
||||
overview: {
|
||||
totalUsers: totalUsers.count,
|
||||
adminCount: adminCount.count,
|
||||
totalPlans: totalPlans.count,
|
||||
activeGuestPlans: guestPlans.count,
|
||||
totalEvents: totalEvents.count,
|
||||
todayEvents: todayEvents.count,
|
||||
todayActiveUsers: todayActiveUsers.count,
|
||||
todayActiveGuests: todayActiveGuests.count
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('获取统计数据失败:', err)
|
||||
res.status(500).json({ error: '获取统计数据失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 管理员:按类型统计事件
|
||||
router.get('/admin/events-by-type', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const { days = 30 } = req.query
|
||||
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
const events = db.prepare(`
|
||||
SELECT event_type, COUNT(*) as count
|
||||
FROM stats
|
||||
WHERE created_at > ?
|
||||
GROUP BY event_type
|
||||
ORDER BY count DESC
|
||||
`).all(since)
|
||||
|
||||
res.json({ events, days })
|
||||
} catch (err) {
|
||||
console.error('获取事件统计失败:', err)
|
||||
res.status(500).json({ error: '获取事件统计失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 管理员:每日活跃趋势
|
||||
router.get('/admin/daily-active', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const { days = 30 } = req.query
|
||||
|
||||
const dailyUsers = db.prepare(`
|
||||
SELECT date(created_at) as date, COUNT(DISTINCT user_id) as users, COUNT(DISTINCT guest_id) as guests
|
||||
FROM stats
|
||||
WHERE created_at > datetime('now', ?)
|
||||
GROUP BY date(created_at)
|
||||
ORDER BY date ASC
|
||||
`).all(`-${days} days`)
|
||||
|
||||
res.json({ daily: dailyUsers, days })
|
||||
} catch (err) {
|
||||
console.error('获取活跃趋势失败:', err)
|
||||
res.status(500).json({ error: '获取活跃趋势失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 管理员:用户列表
|
||||
router.get('/admin/users', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const users = db.prepare(`
|
||||
SELECT u.id, u.username, u.email, u.role, u.created_at,
|
||||
COUNT(DISTINCT p.id) as plan_count,
|
||||
COUNT(DISTINCT s.id) as event_count,
|
||||
MAX(s.created_at) as last_active
|
||||
FROM users u
|
||||
LEFT JOIN plans p ON u.id = p.user_id
|
||||
LEFT JOIN stats s ON u.id = s.user_id
|
||||
GROUP BY u.id
|
||||
ORDER BY u.created_at DESC
|
||||
`).all()
|
||||
|
||||
res.json({ users })
|
||||
} catch (err) {
|
||||
console.error('获取用户列表失败:', err)
|
||||
res.status(500).json({ error: '获取用户列表失败' })
|
||||
}
|
||||
})
|
||||
|
||||
// 管理员:热门行程
|
||||
router.get('/admin/popular-plans', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const { limit = 10 } = req.query
|
||||
|
||||
const plans = db.prepare(`
|
||||
SELECT p.id, p.name, p.description, p.created_at, u.username,
|
||||
COUNT(s.id) as view_count
|
||||
FROM plans p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
LEFT JOIN stats s ON s.event_data LIKE '%' || p.id || '%' AND s.event_type = 'plan_load'
|
||||
GROUP BY p.id
|
||||
ORDER BY view_count DESC
|
||||
LIMIT ?
|
||||
`).all(limit)
|
||||
|
||||
res.json({ plans })
|
||||
} catch (err) {
|
||||
console.error('获取热门行程失败:', err)
|
||||
res.status(500).json({ error: '获取热门行程失败' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,52 @@
|
||||
// 使用统计服务
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const API_BASE = 'http://localhost:3001/api'
|
||||
|
||||
export function trackEvent(eventType, eventData = {}) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const payload = {
|
||||
userId: authStore.user?.id || null,
|
||||
guestId: authStore.guestId || null,
|
||||
eventType,
|
||||
eventData
|
||||
}
|
||||
|
||||
// 异步发送,不阻塞主流程
|
||||
fetch(`${API_BASE}/stats/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}).catch(err => console.error('统计记录失败:', err))
|
||||
}
|
||||
|
||||
// 页面访问统计
|
||||
export function trackPageView(pageName, extra = {}) {
|
||||
trackEvent('page_view', { page: pageName, ...extra })
|
||||
}
|
||||
|
||||
// 规划创建统计
|
||||
export function trackPlanCreate(planName, mode) {
|
||||
trackEvent('plan_create', { planName, mode })
|
||||
}
|
||||
|
||||
// 规划加载统计
|
||||
export function trackPlanLoad(planId) {
|
||||
trackEvent('plan_load', { planId })
|
||||
}
|
||||
|
||||
// 规划导出统计
|
||||
export function trackPlanExport(planId, format) {
|
||||
trackEvent('plan_export', { planId, format })
|
||||
}
|
||||
|
||||
// 聊天开始统计
|
||||
export function trackChatStart(mode) {
|
||||
trackEvent('chat_start', { mode })
|
||||
}
|
||||
|
||||
// 方案选择统计
|
||||
export function trackSchemeSelect(schemeName, schemeIndex) {
|
||||
trackEvent('scheme_select', { schemeName, schemeIndex })
|
||||
}
|
||||
@ -0,0 +1,218 @@
|
||||
// 用户认证状态管理
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const API_BASE = 'http://localhost:3001/api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('auth_token') || null)
|
||||
const guestId = ref(localStorage.getItem('guest_id') || null)
|
||||
const guestExpiresAt = ref(localStorage.getItem('guest_expires_at') || null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!user.value && !!token.value)
|
||||
const isGuest = computed(() => !!guestId.value && !user.value)
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
const isGuestDataExpiring = computed(() => {
|
||||
if (!guestExpiresAt.value) return false
|
||||
const expires = new Date(guestExpiresAt.value)
|
||||
const now = new Date()
|
||||
const hoursLeft = (expires - now) / (1000 * 60 * 60)
|
||||
return hoursLeft < 2 // 少于2小时提醒
|
||||
})
|
||||
const isGuestDataExpired = computed(() => {
|
||||
if (!guestExpiresAt.value) return false
|
||||
return new Date(guestExpiresAt.value) < new Date()
|
||||
})
|
||||
|
||||
// 方法
|
||||
function setAuthHeader() {
|
||||
if (token.value) {
|
||||
return { 'Authorization': `Bearer ${token.value}` }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || '登录失败')
|
||||
|
||||
user.value = data.user
|
||||
token.value = data.token
|
||||
localStorage.setItem('auth_token', data.token)
|
||||
|
||||
// 如果有游客数据,提示迁移
|
||||
return { success: true, user: data.user }
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function register(username, email, password) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || '注册失败')
|
||||
|
||||
user.value = data.user
|
||||
token.value = data.token
|
||||
localStorage.setItem('auth_token', data.token)
|
||||
|
||||
return { success: true, user: data.user }
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
user.value = null
|
||||
token.value = null
|
||||
localStorage.removeItem('auth_token')
|
||||
}
|
||||
|
||||
async function initGuestSession() {
|
||||
// 检查现有游客会话是否过期
|
||||
if (guestId.value && guestExpiresAt.value) {
|
||||
const expires = new Date(guestExpiresAt.value)
|
||||
if (expires > new Date()) {
|
||||
return guestId.value // 会话仍有效
|
||||
}
|
||||
// 过期了,清除
|
||||
clearGuestSession()
|
||||
}
|
||||
|
||||
// 创建新游客会话
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/guest`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || '创建游客会话失败')
|
||||
|
||||
guestId.value = data.guestId
|
||||
guestExpiresAt.value = data.expiresAt
|
||||
localStorage.setItem('guest_id', data.guestId)
|
||||
localStorage.setItem('guest_expires_at', data.expiresAt)
|
||||
|
||||
return data.guestId
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearGuestSession() {
|
||||
guestId.value = null
|
||||
guestExpiresAt.value = null
|
||||
localStorage.removeItem('guest_id')
|
||||
localStorage.removeItem('guest_expires_at')
|
||||
}
|
||||
|
||||
async function migrateGuestData() {
|
||||
if (!token.value || !guestId.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/migrate-guest-data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...setAuthHeader()
|
||||
},
|
||||
body: JSON.stringify({ guestId: guestId.value })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || '数据迁移失败')
|
||||
|
||||
clearGuestSession()
|
||||
return data
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
if (!token.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/me`, {
|
||||
headers: setAuthHeader()
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
user.value = data.user
|
||||
} else {
|
||||
// Token 无效,清除
|
||||
logout()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载用户信息失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载用户信息
|
||||
async function init() {
|
||||
if (token.value) {
|
||||
await loadCurrentUser()
|
||||
} else {
|
||||
await initGuestSession()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
guestId,
|
||||
guestExpiresAt,
|
||||
loading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
isAdmin,
|
||||
isGuestDataExpiring,
|
||||
isGuestDataExpired,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
initGuestSession,
|
||||
clearGuestSession,
|
||||
migrateGuestData,
|
||||
loadCurrentUser,
|
||||
init,
|
||||
setAuthHeader
|
||||
}
|
||||
})
|
||||
Loading…
Reference in new issue