From 47f47608bc4739388440a794b0a443070c649444 Mon Sep 17 00:00:00 2001 From: poignatov Date: Sat, 24 Jan 2026 14:31:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BD=D0=BE=D1=80=D0=BC=D1=8B=20(3.?= =?UTF-8?q?28.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/migrations.mdc | 8 +++ VERSION | 2 +- play-life-backend/main.go | 41 +++++++++++++-- .../migrations/026_weekly_goals_max_score.sql | 10 ++++ ...alized_total_score_to_weekly_report_mv.sql | 51 +++++++++++++++++++ play-life-backend/migrations/README.md | 9 +++- play-life-web/package.json | 2 +- 7 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 .cursor/rules/migrations.mdc create mode 100644 play-life-backend/migrations/026_weekly_goals_max_score.sql create mode 100644 play-life-backend/migrations/027_add_normalized_total_score_to_weekly_report_mv.sql diff --git a/.cursor/rules/migrations.mdc b/.cursor/rules/migrations.mdc new file mode 100644 index 0000000..8e8aaac --- /dev/null +++ b/.cursor/rules/migrations.mdc @@ -0,0 +1,8 @@ +--- +description: "Запрет доработок старых миграций" +alwaysApply: true +--- + +**ВАЖНО:** Если ты меняешь структуру базы данных - напиши НОВУЮ миграцию. +НИ В КОЕМ СЛУЧАЕ не меняй старые миграции, можно добавлять только новые. +Старой миграцией считается та что была уже ранее закомичена diff --git a/VERSION b/VERSION index 8e6b2f2..a72fd67 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.27.2 +3.28.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 8cfd49c..cc28596 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3381,7 +3381,7 @@ func (a *App) initPlayLifeDB() error { goal_week INTEGER NOT NULL, min_goal_score NUMERIC(10,4) NOT NULL DEFAULT 0, max_goal_score NUMERIC(10,4), - actual_score NUMERIC(10,4) DEFAULT 0, + max_score NUMERIC(10,4), priority SMALLINT, CONSTRAINT weekly_goals_project_id_goal_year_goal_week_key UNIQUE (project_id, goal_year, goal_week) ) @@ -3432,6 +3432,11 @@ func (a *App) initPlayLifeDB() error { return fmt.Errorf("failed to create weekly_goals table: %w", err) } + // Авто-миграция weekly_goals: убираем неиспользуемый actual_score и добавляем max_score (snapshot) + // Делаем через ALTER, чтобы работало на уже существующих БД без ручного прогона SQL-миграций. + a.DB.Exec("ALTER TABLE weekly_goals DROP COLUMN IF EXISTS actual_score") + a.DB.Exec("ALTER TABLE weekly_goals ADD COLUMN IF NOT EXISTS max_score NUMERIC(10,4)") + if _, err := a.DB.Exec(createWeeklyGoalsIndex); err != nil { log.Printf("Warning: Failed to create weekly_goals index: %v", err) } @@ -3446,7 +3451,11 @@ func (a *App) initPlayLifeDB() error { p.id AS project_id, agg.report_year, agg.report_week, - COALESCE(agg.total_score, 0.0000) AS total_score + 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 @@ -3464,6 +3473,11 @@ func (a *App) initPlayLifeDB() error { 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 @@ -5164,11 +5178,11 @@ func (a *App) setupWeeklyGoals() error { -- Считаем медиану на основе данных за 3 месяца (12 недель), исключая текущую неделю SELECT project_id, - PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_score) AS median_score + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY normalized_total_score) AS median_score FROM ( SELECT project_id, - total_score, + normalized_total_score, report_year, report_week, -- Нумеруем недели от новых к старым @@ -5190,6 +5204,7 @@ func (a *App) setupWeeklyGoals() error { goal_week, min_goal_score, max_goal_score, + max_score, priority ) SELECT @@ -5205,6 +5220,13 @@ func (a *App) setupWeeklyGoals() error { WHEN p.priority = 2 THEN gm.median_score * 1.3 ELSE gm.median_score * 1.2 END AS max_goal_score, + -- max_score (snapshot) заполняется при INSERT, но НЕ обновляется при конфликте + CASE + WHEN gm.median_score IS NULL THEN NULL + WHEN p.priority = 1 THEN gm.median_score * 1.5 + WHEN p.priority = 2 THEN gm.median_score * 1.3 + ELSE gm.median_score * 1.2 + END AS max_score, p.priority FROM projects p CROSS JOIN current_info ci @@ -5511,7 +5533,11 @@ func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Req p.id AS project_id, agg.report_year, agg.report_week, - COALESCE(agg.total_score, 0.0000) AS total_score + 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 @@ -5529,6 +5555,11 @@ func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Req 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 diff --git a/play-life-backend/migrations/026_weekly_goals_max_score.sql b/play-life-backend/migrations/026_weekly_goals_max_score.sql new file mode 100644 index 0000000..2c3b630 --- /dev/null +++ b/play-life-backend/migrations/026_weekly_goals_max_score.sql @@ -0,0 +1,10 @@ +-- Migration: Add weekly_goals.max_score snapshot column and drop unused actual_score +-- Date: 2026-01-24 + +ALTER TABLE weekly_goals + DROP COLUMN IF EXISTS actual_score; + +-- max_score is a snapshot of max_goal_score for a week, filled only for new weeks by cron +ALTER TABLE weekly_goals + ADD COLUMN IF NOT EXISTS max_score NUMERIC(10,4); + diff --git a/play-life-backend/migrations/027_add_normalized_total_score_to_weekly_report_mv.sql b/play-life-backend/migrations/027_add_normalized_total_score_to_weekly_report_mv.sql new file mode 100644 index 0000000..b374848 --- /dev/null +++ b/play-life-backend/migrations/027_add_normalized_total_score_to_weekly_report_mv.sql @@ -0,0 +1,51 @@ +-- Migration: Add normalized_total_score to weekly_report_mv using weekly_goals.max_score +-- Date: 2026-01-24 +-- +-- normalized_total_score = LEAST(total_score, max_score) if max_score is set, else total_score. +-- Note: max_score is a snapshot field (filled only for new weeks by cron). + +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 IF NOT EXISTS 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.'; + diff --git a/play-life-backend/migrations/README.md b/play-life-backend/migrations/README.md index 4684e3d..ccad621 100644 --- a/play-life-backend/migrations/README.md +++ b/play-life-backend/migrations/README.md @@ -45,7 +45,7 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche - `goal_week` (INTEGER NOT NULL) - `min_goal_score` (NUMERIC(10,4) NOT NULL, DEFAULT 0) - `max_goal_score` (NUMERIC(10,4)) - - `actual_score` (NUMERIC(10,4), DEFAULT 0) + - `max_score` (NUMERIC(10,4), NULL) — snapshot max на неделю (заполняется только для новых недель) - `priority` (SMALLINT) - UNIQUE CONSTRAINT: `(project_id, goal_year, goal_week)` @@ -56,6 +56,7 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche - `report_year` (INTEGER) - `report_week` (INTEGER) - `total_score` (NUMERIC) + - `normalized_total_score` (NUMERIC) ## Миграции @@ -67,6 +68,8 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche 4. **004_add_config_dictionaries.sql** - Добавление связи между конфигурациями и словарями 5. **005_fix_weekly_report_mv.sql** - Исправление использования ISOYEAR вместо YEAR для корректной работы на границе года 6. **006_fix_weekly_report_mv_structure.sql** - Исправление структуры view (добавление LEFT JOIN для включения всех проектов) +7. **026_weekly_goals_max_score.sql** - Добавление snapshot поля weekly_goals.max_score и удаление неиспользуемого actual_score +8. **027_add_normalized_total_score_to_weekly_report_mv.sql** - Добавление normalized_total_score в weekly_report_mv (ограничение total_score по max_score) ### Применение миграций @@ -75,6 +78,8 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche ```bash psql -U playeng -d playeng -f migrations/005_fix_weekly_report_mv.sql psql -U playeng -d playeng -f migrations/006_fix_weekly_report_mv_structure.sql +psql -U playeng -d playeng -f migrations/026_weekly_goals_max_score.sql +psql -U playeng -d playeng -f migrations/027_add_normalized_total_score_to_weekly_report_mv.sql ``` Или через docker-compose: @@ -82,6 +87,8 @@ psql -U playeng -d playeng -f migrations/006_fix_weekly_report_mv_structure.sql ```bash docker-compose exec db psql -U playeng -d playeng -f /migrations/005_fix_weekly_report_mv.sql docker-compose exec db psql -U playeng -d playeng -f /migrations/006_fix_weekly_report_mv_structure.sql +docker-compose exec db psql -U playeng -d playeng -f /migrations/026_weekly_goals_max_score.sql +docker-compose exec db psql -U playeng -d playeng -f /migrations/027_add_normalized_total_score_to_weekly_report_mv.sql ``` ## Обновление Materialized View diff --git a/play-life-web/package.json b/play-life-web/package.json index 32ec4eb..434ec3d 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.27.2", + "version": "3.28.0", "type": "module", "scripts": { "dev": "vite",