From c04422ed690716866e99390925f854760687f7ac Mon Sep 17 00:00:00 2001 From: poignatov Date: Tue, 24 Feb 2026 17:29:02 +0300 Subject: [PATCH] =?UTF-8?q?5.5.0:=20=D0=A1=D1=80=D0=BE=D0=BA=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=D0=B7=20min=5Fgoal=5Fscore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- play-life-backend/main.go | 42 +++++++++---------- .../000024_drop_projects_median_mv.down.sql | 30 +++++++++++++ .../000024_drop_projects_median_mv.up.sql | 4 ++ play-life-web/package.json | 2 +- 5 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 play-life-backend/migrations/000024_drop_projects_median_mv.down.sql create mode 100644 play-life-backend/migrations/000024_drop_projects_median_mv.up.sql diff --git a/VERSION b/VERSION index 8a30e8f..d50359d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.4.0 +5.5.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 185bfa0..bb6cfbf 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3140,14 +3140,6 @@ func (a *App) startWeeklyGoalsScheduler() { log.Printf("Materialized view refreshed successfully") } - // Обновляем projects_median_mv после обновления weekly_report_mv - _, err = a.DB.Exec("REFRESH MATERIALIZED VIEW projects_median_mv") - if err != nil { - log.Printf("Error refreshing projects_median_mv: %v", err) - } else { - log.Printf("Projects median materialized view refreshed successfully") - } - // Обновляем project_score_sample_mv _, err = a.DB.Exec("REFRESH MATERIALIZED VIEW project_score_sample_mv") if err != nil { @@ -11465,22 +11457,25 @@ func (a *App) calculateProjectPointsFromDate( return totalScore, nil } -// getProjectMedian получает медиану проекта из materialized view projects_median_mv -// Если медиана отсутствует, возвращает ошибку -func (a *App) getProjectMedian(projectID int) (float64, error) { - var median float64 +// getProjectMinGoalScoreCurrentWeek получает min_goal_score проекта для текущей недели из weekly_goals. +// Используется как «недельная норма» баллов при расчёте срока разблокировки желаний. +// Если запись отсутствует или значение 0, возвращает ошибку. +func (a *App) getProjectMinGoalScoreCurrentWeek(projectID int) (float64, error) { + var minGoal float64 err := a.DB.QueryRow(` - SELECT median_score - FROM projects_median_mv + SELECT min_goal_score + FROM weekly_goals WHERE project_id = $1 - `, projectID).Scan(&median) + AND goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER + AND goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER + `, projectID).Scan(&minGoal) if err != nil { if err == sql.ErrNoRows { - return 0, fmt.Errorf("median not found for project %d", projectID) + return 0, fmt.Errorf("min_goal_score not found for project %d (current week)", projectID) } return 0, err } - return median, nil + return minGoal, nil } // calculateProjectUnlockWeeks рассчитывает срок разблокировки проекта в неделях @@ -11488,10 +11483,11 @@ func (a *App) getProjectMedian(projectID int) (float64, error) { // requiredPoints - необходимое количество баллов // startDate - дата начала подсчета (может быть nil - за всё время) // userID - ID пользователя (владельца условия) +// «Недельная норма» берётся из weekly_goals.min_goal_score для текущей недели. // Возвращает количество недель (float64): // - > 0: условие не выполнено, возвращает количество недель // - 0: условие уже выполнено (remaining <= 0) -// - 99999: медиана отсутствует или равна 0 (нельзя рассчитать) или ошибка расчета +// - 99999: min_goal_score отсутствует или равен 0 (нельзя рассчитать) или ошибка расчета func (a *App) calculateProjectUnlockWeeks(projectID int, requiredPoints float64, startDate sql.NullTime, userID int) float64 { // 1. Получаем текущие баллы от startDate currentPoints, err := a.calculateProjectPointsFromDate(projectID, startDate, userID) @@ -11507,16 +11503,16 @@ func (a *App) calculateProjectUnlockWeeks(projectID int, requiredPoints float64, return 0 } - // 3. Получаем медиану проекта - median, err := a.getProjectMedian(projectID) - if err != nil || median <= 0 { - // Если медиана отсутствует или равна 0, возвращаем 99999 (нельзя рассчитать) + // 3. Получаем недельную норму (min_goal_score текущей недели) + minGoal, err := a.getProjectMinGoalScoreCurrentWeek(projectID) + if err != nil || minGoal <= 0 { + // Если min_goal_score отсутствует или равен 0, возвращаем 99999 (нельзя рассчитать) // Это нормальная ситуация, не логируем return 99999 } // 4. Рассчитываем недели - weeks := remaining / median + weeks := remaining / minGoal return weeks } diff --git a/play-life-backend/migrations/000024_drop_projects_median_mv.down.sql b/play-life-backend/migrations/000024_drop_projects_median_mv.down.sql new file mode 100644 index 0000000..7b86525 --- /dev/null +++ b/play-life-backend/migrations/000024_drop_projects_median_mv.down.sql @@ -0,0 +1,30 @@ +-- Migration: Recreate projects_median_mv (rollback of 000024) +-- Definition: last 4 weeks per 000008/000023 + +CREATE MATERIALIZED VIEW projects_median_mv AS +SELECT + p.id AS project_id, + p.user_id, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY normalized_total_score) AS median_score +FROM ( + SELECT + project_id, + normalized_total_score, + report_year, + report_week, + ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY report_year DESC, report_week DESC) as rn + FROM weekly_report_mv + WHERE + (report_year < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER) + OR (report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER + AND report_week < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER) +) sub +JOIN projects p ON p.id = sub.project_id +WHERE rn <= 4 AND p.deleted = FALSE +GROUP BY p.id, p.user_id +WITH DATA; + +CREATE INDEX idx_projects_median_mv_project_id ON projects_median_mv(project_id); +CREATE INDEX idx_projects_median_mv_user_id ON projects_median_mv(user_id); + +COMMENT ON MATERIALIZED VIEW projects_median_mv IS 'Materialized view calculating median score for each project based on last 4 weeks of historical data. Includes user_id for multi-tenant support.'; diff --git a/play-life-backend/migrations/000024_drop_projects_median_mv.up.sql b/play-life-backend/migrations/000024_drop_projects_median_mv.up.sql new file mode 100644 index 0000000..dfc6b61 --- /dev/null +++ b/play-life-backend/migrations/000024_drop_projects_median_mv.up.sql @@ -0,0 +1,4 @@ +-- Migration: Drop projects_median_mv (unlock weeks now use weekly_goals.min_goal_score) +-- Date: 2026-02-24 + +DROP MATERIALIZED VIEW IF EXISTS projects_median_mv; diff --git a/play-life-web/package.json b/play-life-web/package.json index 1d0c85c..553be4f 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "5.4.0", + "version": "5.5.0", "type": "module", "scripts": { "dev": "vite",