diff --git a/VERSION b/VERSION index ab0fa33..c20c645 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.0.5 +5.0.6 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 98a9095..1ce11f1 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -5202,7 +5202,6 @@ func (a *App) setupWeeklyGoals() error { goal_week, min_goal_score, max_goal_score, - max_score, priority, user_id ) @@ -5212,20 +5211,13 @@ func (a *App) setupWeeklyGoals() error { ci.c_week, -- Если нет данных (gm.median_score IS NULL), используем 0 (значение по умолчанию) COALESCE(gm.median_score, 0) AS min_goal_score, - -- Логика max_score в зависимости от приоритета (только если есть данные) + -- Логика max_goal_score в зависимости от приоритета (только если есть данные) CASE WHEN gm.median_score IS NULL THEN NULL WHEN p.priority = 1 THEN gm.median_score * 2.0 WHEN p.priority = 2 THEN gm.median_score * 1.7 ELSE gm.median_score * 1.4 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 * 2.0 - WHEN p.priority = 2 THEN gm.median_score * 1.7 - ELSE gm.median_score * 1.4 - END AS max_score, p.priority, p.user_id FROM projects p @@ -5597,8 +5589,8 @@ func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Req 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) + WHEN wg.max_goal_score IS NULL THEN COALESCE(agg.total_score, 0.0000) + ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_goal_score) END AS normalized_total_score FROM projects p diff --git a/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.down.sql b/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.down.sql new file mode 100644 index 0000000..2c083ca --- /dev/null +++ b/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.down.sql @@ -0,0 +1,51 @@ +-- Migration: Restore max_score column and MV using max_score for normalized_total_score + +ALTER TABLE weekly_goals ADD COLUMN max_score NUMERIC(10,4); +UPDATE weekly_goals SET max_score = max_goal_score WHERE max_score IS NULL; + +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 + (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; + +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.'; diff --git a/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.up.sql b/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.up.sql new file mode 100644 index 0000000..1ef1524 --- /dev/null +++ b/play-life-backend/migrations/000020_remove_max_score_use_max_goal_score.up.sql @@ -0,0 +1,51 @@ +-- Migration: Remove max_score from weekly_goals, use max_goal_score for normalized_total_score +-- normalized_total_score is now computed from max_goal_score (current goal) instead of max_score (snapshot). + +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_goal_score IS NULL THEN COALESCE(agg.total_score, 0.0000) + ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_goal_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 + (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; + +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_goal_score. Contains only historical data (excludes current week). Uses nodes.created_date (denormalized) instead of entries.created_date.'; + +ALTER TABLE weekly_goals DROP COLUMN max_score; diff --git a/play-life-backend/migrations/README.md b/play-life-backend/migrations/README.md index ccad621..f8c4a2a 100644 --- a/play-life-backend/migrations/README.md +++ b/play-life-backend/migrations/README.md @@ -45,7 +45,6 @@ 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)) - - `max_score` (NUMERIC(10,4), NULL) — snapshot max на неделю (заполняется только для новых недель) - `priority` (SMALLINT) - UNIQUE CONSTRAINT: `(project_id, goal_year, goal_week)` @@ -56,7 +55,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) + - `normalized_total_score` (NUMERIC) — ограничение total_score по `max_goal_score` (миграция 000020 удалила колонку `max_score`, normalized считается по `max_goal_score`) ## Миграции diff --git a/play-life-web/package.json b/play-life-web/package.json index 2b23493..f172dec 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "5.0.5", + "version": "5.0.6", "type": "module", "scripts": { "dev": "vite",