4.1.0: Оптимизация получения данных текущей недели
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
This commit is contained in:
@@ -26,20 +26,21 @@ import (
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"image/jpeg"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/lib/pq"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"image/jpeg"
|
||||
)
|
||||
|
||||
type Word struct {
|
||||
@@ -2437,18 +2438,17 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Printf("getWeeklyStatsHandler called from %s, path: %s, user: %d", r.RemoteAddr, r.URL.Path, userID)
|
||||
|
||||
// Опционально обновляем materialized view перед запросом
|
||||
// Это можно сделать через query parameter ?refresh=true
|
||||
if r.URL.Query().Get("refresh") == "true" {
|
||||
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
// Получаем данные текущей недели напрямую из nodes
|
||||
currentWeekScores, err := a.getCurrentWeekScores(userID)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
// Продолжаем выполнение даже если обновление не удалось
|
||||
}
|
||||
log.Printf("Error getting current week scores: %v", err)
|
||||
sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
p.name AS project_name,
|
||||
-- Используем COALESCE для установки total_score в 0.0000, если нет данных в weekly_report_mv
|
||||
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||
@@ -2486,11 +2486,13 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
for rows.Next() {
|
||||
var project WeeklyProjectStats
|
||||
var projectID int
|
||||
var minGoalScore sql.NullFloat64
|
||||
var maxGoalScore sql.NullFloat64
|
||||
var priority sql.NullInt64
|
||||
|
||||
err := rows.Scan(
|
||||
&projectID,
|
||||
&project.ProjectName,
|
||||
&project.TotalScore,
|
||||
&minGoalScore,
|
||||
@@ -2503,6 +2505,11 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Объединяем данные: если есть данные текущей недели, используем их вместо MV
|
||||
if currentWeekScore, exists := currentWeekScores[projectID]; exists {
|
||||
project.TotalScore = currentWeekScore
|
||||
}
|
||||
|
||||
if minGoalScore.Valid {
|
||||
project.MinGoalScore = minGoalScore.Float64
|
||||
} else {
|
||||
@@ -2770,7 +2777,17 @@ func (a *App) startWeeklyGoalsScheduler() {
|
||||
// Cron выражение: "0 6 * * 1" означает: минута=0, час=6, любой день месяца, любой месяц, понедельник (1)
|
||||
_, err = c.AddFunc("0 6 * * 1", func() {
|
||||
now := time.Now().In(loc)
|
||||
log.Printf("Scheduled task: Setting up weekly goals (timezone: %s, local time: %s)", timezoneStr, now.Format("2006-01-02 15:04:05 MST"))
|
||||
log.Printf("Scheduled task: Refreshing weekly report MV and setting up weekly goals (timezone: %s, local time: %s)", timezoneStr, now.Format("2006-01-02 15:04:05 MST"))
|
||||
|
||||
// Сначала обновляем MV (чтобы в ней были данные прошлой недели)
|
||||
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
if err != nil {
|
||||
log.Printf("Error refreshing materialized view: %v", err)
|
||||
} else {
|
||||
log.Printf("Materialized view refreshed successfully")
|
||||
}
|
||||
|
||||
// Затем настраиваем цели на новую неделю
|
||||
if err := a.setupWeeklyGoals(); err != nil {
|
||||
log.Printf("Error in scheduled weekly goals setup: %v", err)
|
||||
}
|
||||
@@ -2785,17 +2802,94 @@ func (a *App) startWeeklyGoalsScheduler() {
|
||||
log.Println("Weekly goals scheduler started")
|
||||
}
|
||||
|
||||
// getCurrentWeekScores получает данные текущей недели напрямую из таблицы nodes для конкретного пользователя
|
||||
// Возвращает map[project_id]total_score для текущей недели
|
||||
func (a *App) getCurrentWeekScores(userID int) (map[int]float64, error) {
|
||||
query := `
|
||||
SELECT
|
||||
n.project_id,
|
||||
COALESCE(SUM(n.score), 0) AS total_score
|
||||
FROM nodes n
|
||||
JOIN projects p ON n.project_id = p.id
|
||||
WHERE
|
||||
p.deleted = FALSE
|
||||
AND p.user_id = $1
|
||||
AND n.user_id = $1
|
||||
AND EXTRACT(ISOYEAR FROM n.created_date)::INTEGER = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND EXTRACT(WEEK FROM n.created_date)::INTEGER = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
|
||||
GROUP BY n.project_id
|
||||
`
|
||||
|
||||
rows, err := a.DB.Query(query, userID)
|
||||
if err != nil {
|
||||
log.Printf("Error querying current week scores: %v", err)
|
||||
return nil, fmt.Errorf("error querying current week scores: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
scores := make(map[int]float64)
|
||||
for rows.Next() {
|
||||
var projectID int
|
||||
var totalScore float64
|
||||
if err := rows.Scan(&projectID, &totalScore); err != nil {
|
||||
log.Printf("Error scanning current week scores row: %v", err)
|
||||
return nil, fmt.Errorf("error scanning current week scores row: %w", err)
|
||||
}
|
||||
scores[projectID] = totalScore
|
||||
}
|
||||
|
||||
return scores, nil
|
||||
}
|
||||
|
||||
// getCurrentWeekScoresAllUsers получает данные текущей недели для всех пользователей
|
||||
// Возвращает map[project_id]total_score для текущей недели
|
||||
func (a *App) getCurrentWeekScoresAllUsers() (map[int]float64, error) {
|
||||
query := `
|
||||
SELECT
|
||||
n.project_id,
|
||||
COALESCE(SUM(n.score), 0) AS total_score
|
||||
FROM nodes n
|
||||
JOIN projects p ON n.project_id = p.id
|
||||
WHERE
|
||||
p.deleted = FALSE
|
||||
AND EXTRACT(ISOYEAR FROM n.created_date)::INTEGER = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND EXTRACT(WEEK FROM n.created_date)::INTEGER = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
|
||||
GROUP BY n.project_id
|
||||
`
|
||||
|
||||
rows, err := a.DB.Query(query)
|
||||
if err != nil {
|
||||
log.Printf("Error querying current week scores for all users: %v", err)
|
||||
return nil, fmt.Errorf("error querying current week scores for all users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
scores := make(map[int]float64)
|
||||
for rows.Next() {
|
||||
var projectID int
|
||||
var totalScore float64
|
||||
if err := rows.Scan(&projectID, &totalScore); err != nil {
|
||||
log.Printf("Error scanning current week scores row: %v", err)
|
||||
return nil, fmt.Errorf("error scanning current week scores row: %w", err)
|
||||
}
|
||||
scores[projectID] = totalScore
|
||||
}
|
||||
|
||||
return scores, nil
|
||||
}
|
||||
|
||||
// getWeeklyStatsData получает данные о проектах и их целях (без HTTP обработки)
|
||||
func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
|
||||
// Обновляем materialized view перед запросом
|
||||
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
// Получаем данные текущей недели для всех пользователей
|
||||
currentWeekScores, err := a.getCurrentWeekScoresAllUsers()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
// Продолжаем выполнение даже если обновление не удалось
|
||||
log.Printf("Error getting current week scores: %v", err)
|
||||
return nil, fmt.Errorf("error getting current week scores: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
p.name AS project_name,
|
||||
-- Используем COALESCE для установки total_score в 0.0000, если нет данных в weekly_report_mv
|
||||
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||
@@ -2832,11 +2926,13 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
|
||||
|
||||
for rows.Next() {
|
||||
var project WeeklyProjectStats
|
||||
var projectID int
|
||||
var minGoalScore sql.NullFloat64
|
||||
var maxGoalScore sql.NullFloat64
|
||||
var priority sql.NullInt64
|
||||
|
||||
err := rows.Scan(
|
||||
&projectID,
|
||||
&project.ProjectName,
|
||||
&project.TotalScore,
|
||||
&minGoalScore,
|
||||
@@ -2848,6 +2944,11 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
|
||||
return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
|
||||
}
|
||||
|
||||
// Объединяем данные: если есть данные текущей недели, используем их вместо MV
|
||||
if currentWeekScore, exists := currentWeekScores[projectID]; exists {
|
||||
project.TotalScore = currentWeekScore
|
||||
}
|
||||
|
||||
if minGoalScore.Valid {
|
||||
project.MinGoalScore = minGoalScore.Float64
|
||||
} else {
|
||||
@@ -2929,14 +3030,16 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
|
||||
|
||||
// getWeeklyStatsDataForUser получает данные о проектах для конкретного пользователя
|
||||
func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error) {
|
||||
// Обновляем materialized view перед запросом
|
||||
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
// Получаем данные текущей недели напрямую из nodes
|
||||
currentWeekScores, err := a.getCurrentWeekScores(userID)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
log.Printf("Error getting current week scores: %v", err)
|
||||
return nil, fmt.Errorf("error getting current week scores: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
p.name AS project_name,
|
||||
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||
wg.min_goal_score,
|
||||
@@ -2970,11 +3073,13 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
||||
|
||||
for rows.Next() {
|
||||
var project WeeklyProjectStats
|
||||
var projectID int
|
||||
var minGoalScore sql.NullFloat64
|
||||
var maxGoalScore sql.NullFloat64
|
||||
var priority sql.NullInt64
|
||||
|
||||
err := rows.Scan(
|
||||
&projectID,
|
||||
&project.ProjectName,
|
||||
&project.TotalScore,
|
||||
&minGoalScore,
|
||||
@@ -2985,6 +3090,11 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
||||
return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
|
||||
}
|
||||
|
||||
// Объединяем данные: если есть данные текущей недели, используем их вместо MV
|
||||
if currentWeekScore, exists := currentWeekScores[projectID]; exists {
|
||||
project.TotalScore = currentWeekScore
|
||||
}
|
||||
|
||||
if minGoalScore.Valid {
|
||||
project.MinGoalScore = minGoalScore.Float64
|
||||
} else {
|
||||
@@ -3759,7 +3869,6 @@ func (a *App) getTelegramIntegrationForUser(userID int) (*TelegramIntegration, e
|
||||
return &integration, nil
|
||||
}
|
||||
|
||||
|
||||
// sendTelegramMessageToChat - отправляет сообщение в конкретный чат по chat_id
|
||||
func (a *App) sendTelegramMessageToChat(chatID int64, text string) error {
|
||||
if a.telegramBot == nil {
|
||||
@@ -4272,29 +4381,25 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr
|
||||
return fmt.Errorf("failed to find project %s: %w", node.Project, err)
|
||||
}
|
||||
|
||||
// Вставляем node с user_id
|
||||
// Вставляем node с user_id и created_date (денормализация)
|
||||
if userID != nil {
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO nodes (project_id, entry_id, score, user_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, projectID, entryID, node.Score, *userID)
|
||||
INSERT INTO nodes (project_id, entry_id, score, user_id, created_date)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, projectID, entryID, node.Score, *userID, createdDate)
|
||||
} else {
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO nodes (project_id, entry_id, score)
|
||||
VALUES ($1, $2, $3)
|
||||
`, projectID, entryID, node.Score)
|
||||
INSERT INTO nodes (project_id, entry_id, score, created_date)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, projectID, entryID, node.Score, createdDate)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert node for project %s: %w", node.Project, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем materialized view после вставки данных
|
||||
_, err = tx.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
// Не возвращаем ошибку, так как это не критично
|
||||
}
|
||||
// MV обновляется только по крону в понедельник в 6:00 утра
|
||||
// Данные текущей недели берутся напрямую из nodes
|
||||
|
||||
// Коммитим транзакцию
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -5126,11 +5231,8 @@ func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем materialized view
|
||||
_, err = a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
}
|
||||
// MV обновляется только по крону в понедельник в 6:00 утра
|
||||
// Данные текущей недели берутся напрямую из nodes
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
@@ -5207,11 +5309,8 @@ func (a *App) deleteProjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем materialized view
|
||||
_, err = a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to refresh materialized view: %v", err)
|
||||
}
|
||||
// MV обновляется только по крону в понедельник в 6:00 утра
|
||||
// Данные текущей недели берутся напрямую из nodes
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
@@ -5703,6 +5802,19 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем данные текущей недели
|
||||
currentWeekScores, err := a.getCurrentWeekScores(userID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting current week scores: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error getting current week scores: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ISO год и неделю для текущей даты
|
||||
now := time.Now()
|
||||
_, currentWeekInt := now.ISOWeek()
|
||||
currentYearInt := now.Year()
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
p.name AS project_name,
|
||||
@@ -5717,7 +5829,8 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
COALESCE(wg.min_goal_score, 0.0000) AS min_goal_score,
|
||||
|
||||
-- Максимальная цель: COALESCE(NULL, 0.0000)
|
||||
COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score
|
||||
COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score,
|
||||
p.id AS project_id
|
||||
FROM
|
||||
weekly_report_mv wr
|
||||
FULL OUTER JOIN
|
||||
@@ -5748,8 +5861,10 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer rows.Close()
|
||||
|
||||
statistics := make([]FullStatisticsItem, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var item FullStatisticsItem
|
||||
var projectID int
|
||||
|
||||
err := rows.Scan(
|
||||
&item.ProjectName,
|
||||
@@ -5758,6 +5873,7 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
&item.TotalScore,
|
||||
&item.MinGoalScore,
|
||||
&item.MaxGoalScore,
|
||||
&projectID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error scanning full statistics row: %v", err)
|
||||
@@ -5765,9 +5881,78 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Если это текущая неделя, заменяем данные из MV на данные из nodes
|
||||
if item.ReportYear == currentYearInt && item.ReportWeek == currentWeekInt {
|
||||
if score, exists := currentWeekScores[projectID]; exists {
|
||||
item.TotalScore = score
|
||||
}
|
||||
}
|
||||
|
||||
statistics = append(statistics, item)
|
||||
}
|
||||
|
||||
// Добавляем проекты текущей недели, которых нет в MV (новые проекты без исторических данных)
|
||||
// Получаем goals для текущей недели
|
||||
currentWeekGoalsQuery := `
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
p.name AS project_name,
|
||||
COALESCE(wg.min_goal_score, 0.0000) AS min_goal_score,
|
||||
COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score
|
||||
FROM projects p
|
||||
LEFT JOIN weekly_goals wg ON wg.project_id = p.id
|
||||
AND wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND wg.goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
|
||||
WHERE p.deleted = FALSE AND p.user_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM weekly_report_mv wr
|
||||
WHERE wr.project_id = p.id
|
||||
AND wr.report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND wr.report_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
|
||||
)
|
||||
`
|
||||
|
||||
goalsRows, err := a.DB.Query(currentWeekGoalsQuery, userID)
|
||||
if err == nil {
|
||||
defer goalsRows.Close()
|
||||
existingProjects := make(map[int]bool)
|
||||
for _, stat := range statistics {
|
||||
if stat.ReportYear == currentYearInt && stat.ReportWeek == currentWeekInt {
|
||||
// Найдем project_id по имени проекта (не идеально, но работает)
|
||||
var pid int
|
||||
if err := a.DB.QueryRow("SELECT id FROM projects WHERE name = $1 AND user_id = $2", stat.ProjectName, userID).Scan(&pid); err == nil {
|
||||
existingProjects[pid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for goalsRows.Next() {
|
||||
var projectID int
|
||||
var projectName string
|
||||
var minGoalScore, maxGoalScore float64
|
||||
if err := goalsRows.Scan(&projectID, &projectName, &minGoalScore, &maxGoalScore); err == nil {
|
||||
// Добавляем только если проекта еще нет в статистике
|
||||
if !existingProjects[projectID] {
|
||||
totalScore := 0.0
|
||||
if score, exists := currentWeekScores[projectID]; exists {
|
||||
totalScore = score
|
||||
}
|
||||
|
||||
_, weekISO := time.Now().ISOWeek()
|
||||
item := FullStatisticsItem{
|
||||
ProjectName: projectName,
|
||||
ReportYear: time.Now().Year(),
|
||||
ReportWeek: weekISO,
|
||||
TotalScore: totalScore,
|
||||
MinGoalScore: minGoalScore,
|
||||
MaxGoalScore: maxGoalScore,
|
||||
}
|
||||
statistics = append(statistics, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(statistics)
|
||||
}
|
||||
@@ -8581,7 +8766,7 @@ func (a *App) todoistDisconnectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// ============================================
|
||||
|
||||
// calculateProjectPointsFromDate считает баллы проекта с указанной даты до текущего момента
|
||||
// Считает напрямую из таблицы nodes, фильтруя по дате entries
|
||||
// Считает напрямую из таблицы nodes, используя денормализованное поле created_date
|
||||
func (a *App) calculateProjectPointsFromDate(
|
||||
projectID int,
|
||||
startDate sql.NullTime,
|
||||
@@ -8600,17 +8785,17 @@ func (a *App) calculateProjectPointsFromDate(
|
||||
`, projectID, userID).Scan(&totalScore)
|
||||
} else {
|
||||
// С указанной даты до текущего момента
|
||||
// Считаем все nodes этого пользователя, где дата entry >= startDate
|
||||
// Считаем все nodes этого пользователя, где дата created_date >= startDate
|
||||
// Используем DATE() для сравнения только по дате (без времени)
|
||||
// Теперь используем nodes.created_date напрямую (без JOIN с entries)
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(n.score), 0)
|
||||
FROM nodes n
|
||||
JOIN entries e ON n.entry_id = e.id
|
||||
JOIN projects p ON n.project_id = p.id
|
||||
WHERE n.project_id = $1
|
||||
AND n.user_id = $2
|
||||
AND p.user_id = $2
|
||||
AND DATE(e.created_date) >= DATE($3)
|
||||
AND DATE(n.created_date) >= DATE($3)
|
||||
`, projectID, userID, startDate.Time).Scan(&totalScore)
|
||||
}
|
||||
|
||||
@@ -12632,4 +12817,3 @@ func decodeHTMLEntities(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
-- Migration: Revert optimization of weekly_report_mv
|
||||
-- Date: 2026-01-26
|
||||
--
|
||||
-- This migration reverts:
|
||||
-- 1. Removes created_date column from nodes table
|
||||
-- 2. Drops indexes
|
||||
-- 3. Restores MV to original structure (include current week, use entries.created_date)
|
||||
|
||||
-- ============================================
|
||||
-- Step 1: Recreate MV with original structure
|
||||
-- ============================================
|
||||
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
|
||||
|
||||
CREATE MATERIALIZED VIEW weekly_report_mv AS
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
agg.report_year,
|
||||
agg.report_week,
|
||||
COALESCE(agg.total_score, 0.0000) AS total_score,
|
||||
CASE
|
||||
WHEN wg.max_score IS NULL THEN COALESCE(agg.total_score, 0.0000)
|
||||
ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_score)
|
||||
END AS normalized_total_score
|
||||
FROM
|
||||
projects p
|
||||
LEFT JOIN
|
||||
(
|
||||
SELECT
|
||||
n.project_id,
|
||||
EXTRACT(ISOYEAR FROM e.created_date)::INTEGER AS report_year,
|
||||
EXTRACT(WEEK FROM e.created_date)::INTEGER AS report_week,
|
||||
SUM(n.score) AS total_score
|
||||
FROM
|
||||
nodes n
|
||||
JOIN
|
||||
entries e ON n.entry_id = e.id
|
||||
GROUP BY
|
||||
1, 2, 3
|
||||
) agg
|
||||
ON p.id = agg.project_id
|
||||
LEFT JOIN
|
||||
weekly_goals wg
|
||||
ON wg.project_id = p.id
|
||||
AND wg.goal_year = agg.report_year
|
||||
AND wg.goal_week = agg.report_week
|
||||
WHERE
|
||||
p.deleted = FALSE
|
||||
ORDER BY
|
||||
p.id, agg.report_year, agg.report_week
|
||||
WITH DATA;
|
||||
|
||||
CREATE INDEX idx_weekly_report_mv_project_year_week
|
||||
ON weekly_report_mv(project_id, report_year, report_week);
|
||||
|
||||
-- ============================================
|
||||
-- Step 2: Drop indexes
|
||||
-- ============================================
|
||||
DROP INDEX IF EXISTS idx_nodes_project_user_created_date;
|
||||
DROP INDEX IF EXISTS idx_nodes_created_date_user;
|
||||
|
||||
-- ============================================
|
||||
-- Step 3: Remove created_date column from nodes
|
||||
-- ============================================
|
||||
ALTER TABLE nodes
|
||||
DROP COLUMN IF EXISTS created_date;
|
||||
|
||||
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries. Includes all projects via LEFT JOIN. Adds normalized_total_score using weekly_goals.max_score snapshot.';
|
||||
@@ -0,0 +1,94 @@
|
||||
-- Migration: Optimize weekly_report_mv by denormalizing created_date into nodes and excluding current week from MV
|
||||
-- Date: 2026-01-26
|
||||
--
|
||||
-- This migration:
|
||||
-- 1. Adds created_date column to nodes table (denormalization to avoid JOIN with entries)
|
||||
-- 2. Populates existing data from entries
|
||||
-- 3. Creates indexes for optimized queries
|
||||
-- 4. Updates MV to exclude current week and use nodes.created_date instead of entries.created_date
|
||||
|
||||
-- ============================================
|
||||
-- Step 1: Add created_date column to nodes
|
||||
-- ============================================
|
||||
ALTER TABLE nodes
|
||||
ADD COLUMN created_date TIMESTAMP WITH TIME ZONE;
|
||||
|
||||
-- ============================================
|
||||
-- Step 2: Populate existing data from entries
|
||||
-- ============================================
|
||||
UPDATE nodes n
|
||||
SET created_date = e.created_date
|
||||
FROM entries e
|
||||
WHERE n.entry_id = e.id;
|
||||
|
||||
-- ============================================
|
||||
-- Step 3: Set NOT NULL constraint
|
||||
-- ============================================
|
||||
ALTER TABLE nodes
|
||||
ALTER COLUMN created_date SET NOT NULL;
|
||||
|
||||
-- ============================================
|
||||
-- Step 4: Create indexes for optimized queries
|
||||
-- ============================================
|
||||
-- Index for filtering by date and user (for current week queries)
|
||||
CREATE INDEX IF NOT EXISTS idx_nodes_created_date_user
|
||||
ON nodes(created_date, user_id);
|
||||
|
||||
-- Index for queries with grouping by project (for current week queries)
|
||||
CREATE INDEX IF NOT EXISTS idx_nodes_project_user_created_date
|
||||
ON nodes(project_id, user_id, created_date);
|
||||
|
||||
COMMENT ON INDEX idx_nodes_created_date_user IS 'Index for filtering nodes by created_date and user_id - optimized for current week queries';
|
||||
COMMENT ON INDEX idx_nodes_project_user_created_date IS 'Index for grouping nodes by project, user and created_date - optimized for current week aggregation queries';
|
||||
|
||||
-- ============================================
|
||||
-- Step 5: Recreate MV to exclude current week and use nodes.created_date
|
||||
-- ============================================
|
||||
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
|
||||
|
||||
CREATE MATERIALIZED VIEW weekly_report_mv AS
|
||||
SELECT
|
||||
p.id AS project_id,
|
||||
agg.report_year,
|
||||
agg.report_week,
|
||||
COALESCE(agg.total_score, 0.0000) AS total_score,
|
||||
CASE
|
||||
WHEN wg.max_score IS NULL THEN COALESCE(agg.total_score, 0.0000)
|
||||
ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_score)
|
||||
END AS normalized_total_score
|
||||
FROM
|
||||
projects p
|
||||
LEFT JOIN
|
||||
(
|
||||
SELECT
|
||||
n.project_id,
|
||||
EXTRACT(ISOYEAR FROM n.created_date)::INTEGER AS report_year,
|
||||
EXTRACT(WEEK FROM n.created_date)::INTEGER AS report_week,
|
||||
SUM(n.score) AS total_score
|
||||
FROM
|
||||
nodes n
|
||||
WHERE
|
||||
-- Exclude current week: only include data from previous weeks
|
||||
(EXTRACT(ISOYEAR FROM n.created_date)::INTEGER < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
|
||||
OR (EXTRACT(ISOYEAR FROM n.created_date)::INTEGER = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
||||
AND EXTRACT(WEEK FROM n.created_date)::INTEGER < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
|
||||
GROUP BY
|
||||
1, 2, 3
|
||||
) agg
|
||||
ON p.id = agg.project_id
|
||||
LEFT JOIN
|
||||
weekly_goals wg
|
||||
ON wg.project_id = p.id
|
||||
AND wg.goal_year = agg.report_year
|
||||
AND wg.goal_week = agg.report_week
|
||||
WHERE
|
||||
p.deleted = FALSE
|
||||
ORDER BY
|
||||
p.id, agg.report_year, agg.report_week
|
||||
WITH DATA;
|
||||
|
||||
-- Recreate index on MV
|
||||
CREATE INDEX idx_weekly_report_mv_project_year_week
|
||||
ON weekly_report_mv(project_id, report_year, report_week);
|
||||
|
||||
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries. Includes all projects via LEFT JOIN. Adds normalized_total_score using weekly_goals.max_score snapshot. Contains only historical data (excludes current week). Uses nodes.created_date (denormalized) instead of entries.created_date.';
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "4.0.6",
|
||||
"version": "4.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Reference in New Issue
Block a user