280 lines
9.0 KiB
JavaScript
280 lines
9.0 KiB
JavaScript
const { Router } = require('express')
|
||
const mongoose = require('mongoose')
|
||
|
||
const { getAnswer } = require('../../utils/common')
|
||
const { CigaretteModel } = require('./model/cigarette')
|
||
const { authMiddleware } = require('./middleware/auth')
|
||
|
||
const router = Router()
|
||
|
||
// Все эндпоинты статистики требуют авторизации
|
||
router.use(authMiddleware)
|
||
|
||
// Агрегация по дням: количество сигарет в день для построения графика
|
||
router.get('/daily', async (req, res, next) => {
|
||
try {
|
||
const user = req.user
|
||
const { from, to } = req.query
|
||
|
||
const now = new Date()
|
||
const defaultFrom = new Date(now)
|
||
defaultFrom.setDate(defaultFrom.getDate() - 30)
|
||
|
||
const fromDate = from ? new Date(from) : defaultFrom
|
||
const toDate = to ? new Date(to) : now
|
||
|
||
const match = {
|
||
userId: new mongoose.Types.ObjectId(user.id),
|
||
smokedAt: {
|
||
$gte: fromDate,
|
||
$lte: toDate,
|
||
},
|
||
}
|
||
|
||
// Отладка: проверяем, сколько записей попадает в фильтр
|
||
const totalCount = await CigaretteModel.countDocuments(match)
|
||
console.log('[STATS] Match filter:', JSON.stringify(match, null, 2))
|
||
console.log('[STATS] Total cigarettes in range:', totalCount)
|
||
|
||
const data = await CigaretteModel.aggregate([
|
||
{ $match: match },
|
||
{
|
||
$group: {
|
||
_id: {
|
||
$dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' },
|
||
},
|
||
count: { $sum: 1 },
|
||
},
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
])
|
||
|
||
console.log('[STATS] Aggregation result:', data)
|
||
|
||
const result = data.map((item) => ({
|
||
date: item._id,
|
||
count: item.count,
|
||
}))
|
||
|
||
res.json(getAnswer(null, result))
|
||
} catch (err) {
|
||
next(err)
|
||
}
|
||
})
|
||
|
||
// Сводная статистика: среднее в день, по дням недели, общее по всем пользователям
|
||
router.get('/summary', async (req, res, next) => {
|
||
try {
|
||
const user = req.user
|
||
const { from, to } = req.query
|
||
|
||
const now = new Date()
|
||
const defaultFrom = new Date(now)
|
||
defaultFrom.setDate(defaultFrom.getDate() - 30)
|
||
|
||
const fromDate = from ? new Date(from) : defaultFrom
|
||
const toDate = to ? new Date(to) : now
|
||
|
||
// Фильтр для текущего пользователя
|
||
const userMatch = {
|
||
userId: new mongoose.Types.ObjectId(user.id),
|
||
smokedAt: {
|
||
$gte: fromDate,
|
||
$lte: toDate,
|
||
},
|
||
}
|
||
|
||
// Фильтр для всех пользователей (общая статистика)
|
||
const globalMatch = {
|
||
smokedAt: {
|
||
$gte: fromDate,
|
||
$lte: toDate,
|
||
},
|
||
}
|
||
|
||
// 1. Статистика по дням (для текущего пользователя)
|
||
const dailyStats = await CigaretteModel.aggregate([
|
||
{ $match: userMatch },
|
||
{
|
||
$group: {
|
||
_id: {
|
||
$dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' },
|
||
},
|
||
count: { $sum: 1 },
|
||
},
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
])
|
||
|
||
const dailyData = dailyStats.map((item) => ({
|
||
date: item._id,
|
||
count: item.count,
|
||
}))
|
||
|
||
// 2. Среднее количество в день (для текущего пользователя)
|
||
const totalCigarettes = dailyStats.reduce((sum, item) => sum + item.count, 0)
|
||
const daysWithData = dailyStats.length
|
||
const averagePerDay = daysWithData > 0 ? (totalCigarettes / daysWithData).toFixed(2) : 0
|
||
|
||
// 3. Статистика по дням недели (для текущего пользователя)
|
||
const weekdayStats = await CigaretteModel.aggregate([
|
||
{ $match: userMatch },
|
||
{
|
||
$group: {
|
||
_id: { $dayOfWeek: '$smokedAt' }, // 1 = воскресенье, 2 = понедельник, ..., 7 = суббота
|
||
count: { $sum: 1 },
|
||
},
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
])
|
||
|
||
const weekdayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']
|
||
const weekdayData = weekdayStats.map((item) => {
|
||
const dayIndex = item._id - 1 // MongoDB возвращает 1-7, приводим к 0-6
|
||
return {
|
||
dayOfWeek: item._id,
|
||
dayName: weekdayNames[dayIndex],
|
||
count: item.count,
|
||
}
|
||
})
|
||
|
||
// Вычисляем среднее для каждого дня недели
|
||
const weekdayAverages = weekdayData.map((day) => {
|
||
// Считаем, сколько раз встречается этот день недели в периоде
|
||
const occurrences = Math.floor(
|
||
(toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7)
|
||
) + 1
|
||
return {
|
||
...day,
|
||
average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count,
|
||
}
|
||
})
|
||
|
||
// 4. Определяем активных пользователей (от 2 до 40 сигарет в день в среднем)
|
||
const MIN_CIGARETTES_PER_DAY = 2
|
||
const MAX_CIGARETTES_PER_DAY = 40
|
||
const periodDays = Math.ceil((toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24))
|
||
|
||
// Получаем статистику по каждому пользователю
|
||
const userStats = await CigaretteModel.aggregate([
|
||
{ $match: globalMatch },
|
||
{
|
||
$group: {
|
||
_id: '$userId',
|
||
total: { $sum: 1 },
|
||
},
|
||
},
|
||
])
|
||
|
||
// Фильтруем активных пользователей (исключаем слишком низкие и слишком высокие значения)
|
||
const activeUserIds = userStats
|
||
.filter((stat) => {
|
||
const avgPerDay = stat.total / periodDays
|
||
return avgPerDay > MIN_CIGARETTES_PER_DAY && avgPerDay <= MAX_CIGARETTES_PER_DAY
|
||
})
|
||
.map((stat) => stat._id)
|
||
|
||
const filteredLow = userStats.filter((stat) => stat.total / periodDays <= MIN_CIGARETTES_PER_DAY).length
|
||
const filteredHigh = userStats.filter((stat) => stat.total / periodDays > MAX_CIGARETTES_PER_DAY).length
|
||
|
||
console.log('[STATS] Total users:', userStats.length)
|
||
console.log('[STATS] Active users (2-40 cigs/day):', activeUserIds.length)
|
||
console.log('[STATS] Filtered out (too low):', filteredLow)
|
||
console.log('[STATS] Filtered out (too high):', filteredHigh)
|
||
|
||
// Фильтр только для активных пользователей
|
||
const activeGlobalMatch = {
|
||
...globalMatch,
|
||
userId: { $in: activeUserIds },
|
||
}
|
||
|
||
// Общая статистика по активным пользователям
|
||
const globalDailyStats = await CigaretteModel.aggregate([
|
||
{ $match: activeGlobalMatch },
|
||
{
|
||
$group: {
|
||
_id: {
|
||
$dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' },
|
||
},
|
||
count: { $sum: 1 },
|
||
},
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
])
|
||
|
||
const globalDailyData = globalDailyStats.map((item) => ({
|
||
date: item._id,
|
||
count: item.count,
|
||
}))
|
||
|
||
const globalTotalCigarettes = globalDailyStats.reduce((sum, item) => sum + item.count, 0)
|
||
const globalDaysWithData = globalDailyStats.length
|
||
const globalAveragePerDay =
|
||
globalDaysWithData > 0 ? (globalTotalCigarettes / globalDaysWithData).toFixed(2) : 0
|
||
|
||
// Общая статистика по дням недели (активные пользователи)
|
||
const globalWeekdayStats = await CigaretteModel.aggregate([
|
||
{ $match: activeGlobalMatch },
|
||
{
|
||
$group: {
|
||
_id: { $dayOfWeek: '$smokedAt' },
|
||
count: { $sum: 1 },
|
||
},
|
||
},
|
||
{ $sort: { _id: 1 } },
|
||
])
|
||
|
||
const globalWeekdayData = globalWeekdayStats.map((item) => {
|
||
const dayIndex = item._id - 1
|
||
return {
|
||
dayOfWeek: item._id,
|
||
dayName: weekdayNames[dayIndex],
|
||
count: item.count,
|
||
}
|
||
})
|
||
|
||
const globalWeekdayAverages = globalWeekdayData.map((day) => {
|
||
const occurrences = Math.floor(
|
||
(toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7)
|
||
) + 1
|
||
return {
|
||
...day,
|
||
average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count,
|
||
}
|
||
})
|
||
|
||
// Количество активных пользователей
|
||
const activeUsers = activeUserIds
|
||
|
||
const result = {
|
||
user: {
|
||
daily: dailyData,
|
||
averagePerDay: parseFloat(averagePerDay),
|
||
weekday: weekdayAverages,
|
||
total: totalCigarettes,
|
||
daysWithData,
|
||
},
|
||
global: {
|
||
daily: globalDailyData,
|
||
averagePerDay: parseFloat(globalAveragePerDay),
|
||
weekday: globalWeekdayAverages,
|
||
total: globalTotalCigarettes,
|
||
daysWithData: globalDaysWithData,
|
||
activeUsers: activeUsers.length,
|
||
},
|
||
period: {
|
||
from: fromDate.toISOString(),
|
||
to: toDate.toISOString(),
|
||
},
|
||
}
|
||
|
||
res.json(getAnswer(null, result))
|
||
} catch (err) {
|
||
next(err)
|
||
}
|
||
})
|
||
|
||
module.exports = router
|
||
|
||
|