Нормализация недельной нормы (3.28.0)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m29s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m29s
This commit is contained in:
8
.cursor/rules/migrations.mdc
Normal file
8
.cursor/rules/migrations.mdc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
description: "Запрет доработок старых миграций"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
**ВАЖНО:** Если ты меняешь структуру базы данных - напиши НОВУЮ миграцию.
|
||||||
|
НИ В КОЕМ СЛУЧАЕ не меняй старые миграции, можно добавлять только новые.
|
||||||
|
Старой миграцией считается та что была уже ранее закомичена
|
||||||
@@ -3381,7 +3381,7 @@ func (a *App) initPlayLifeDB() error {
|
|||||||
goal_week INTEGER NOT NULL,
|
goal_week INTEGER NOT NULL,
|
||||||
min_goal_score NUMERIC(10,4) NOT NULL DEFAULT 0,
|
min_goal_score NUMERIC(10,4) NOT NULL DEFAULT 0,
|
||||||
max_goal_score NUMERIC(10,4),
|
max_goal_score NUMERIC(10,4),
|
||||||
actual_score NUMERIC(10,4) DEFAULT 0,
|
max_score NUMERIC(10,4),
|
||||||
priority SMALLINT,
|
priority SMALLINT,
|
||||||
CONSTRAINT weekly_goals_project_id_goal_year_goal_week_key UNIQUE (project_id, goal_year, goal_week)
|
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)
|
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 {
|
if _, err := a.DB.Exec(createWeeklyGoalsIndex); err != nil {
|
||||||
log.Printf("Warning: Failed to create weekly_goals index: %v", err)
|
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,
|
p.id AS project_id,
|
||||||
agg.report_year,
|
agg.report_year,
|
||||||
agg.report_week,
|
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
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -3464,6 +3473,11 @@ func (a *App) initPlayLifeDB() error {
|
|||||||
1, 2, 3
|
1, 2, 3
|
||||||
) agg
|
) agg
|
||||||
ON p.id = agg.project_id
|
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
|
WHERE
|
||||||
p.deleted = FALSE
|
p.deleted = FALSE
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -5164,11 +5178,11 @@ func (a *App) setupWeeklyGoals() error {
|
|||||||
-- Считаем медиану на основе данных за 3 месяца (12 недель), исключая текущую неделю
|
-- Считаем медиану на основе данных за 3 месяца (12 недель), исключая текущую неделю
|
||||||
SELECT
|
SELECT
|
||||||
project_id,
|
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 (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
project_id,
|
project_id,
|
||||||
total_score,
|
normalized_total_score,
|
||||||
report_year,
|
report_year,
|
||||||
report_week,
|
report_week,
|
||||||
-- Нумеруем недели от новых к старым
|
-- Нумеруем недели от новых к старым
|
||||||
@@ -5190,6 +5204,7 @@ func (a *App) setupWeeklyGoals() error {
|
|||||||
goal_week,
|
goal_week,
|
||||||
min_goal_score,
|
min_goal_score,
|
||||||
max_goal_score,
|
max_goal_score,
|
||||||
|
max_score,
|
||||||
priority
|
priority
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -5205,6 +5220,13 @@ func (a *App) setupWeeklyGoals() error {
|
|||||||
WHEN p.priority = 2 THEN gm.median_score * 1.3
|
WHEN p.priority = 2 THEN gm.median_score * 1.3
|
||||||
ELSE gm.median_score * 1.2
|
ELSE gm.median_score * 1.2
|
||||||
END AS max_goal_score,
|
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
|
p.priority
|
||||||
FROM projects p
|
FROM projects p
|
||||||
CROSS JOIN current_info ci
|
CROSS JOIN current_info ci
|
||||||
@@ -5511,7 +5533,11 @@ func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Req
|
|||||||
p.id AS project_id,
|
p.id AS project_id,
|
||||||
agg.report_year,
|
agg.report_year,
|
||||||
agg.report_week,
|
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
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -5529,6 +5555,11 @@ func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Req
|
|||||||
1, 2, 3
|
1, 2, 3
|
||||||
) agg
|
) agg
|
||||||
ON p.id = agg.project_id
|
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
|
WHERE
|
||||||
p.deleted = FALSE
|
p.deleted = FALSE
|
||||||
ORDER BY
|
ORDER BY
|
||||||
|
|||||||
10
play-life-backend/migrations/026_weekly_goals_max_score.sql
Normal file
10
play-life-backend/migrations/026_weekly_goals_max_score.sql
Normal file
@@ -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);
|
||||||
|
|
||||||
@@ -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.';
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche
|
|||||||
- `goal_week` (INTEGER NOT NULL)
|
- `goal_week` (INTEGER NOT NULL)
|
||||||
- `min_goal_score` (NUMERIC(10,4) NOT NULL, DEFAULT 0)
|
- `min_goal_score` (NUMERIC(10,4) NOT NULL, DEFAULT 0)
|
||||||
- `max_goal_score` (NUMERIC(10,4))
|
- `max_goal_score` (NUMERIC(10,4))
|
||||||
- `actual_score` (NUMERIC(10,4), DEFAULT 0)
|
- `max_score` (NUMERIC(10,4), NULL) — snapshot max на неделю (заполняется только для новых недель)
|
||||||
- `priority` (SMALLINT)
|
- `priority` (SMALLINT)
|
||||||
- UNIQUE CONSTRAINT: `(project_id, goal_year, goal_week)`
|
- 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_year` (INTEGER)
|
||||||
- `report_week` (INTEGER)
|
- `report_week` (INTEGER)
|
||||||
- `total_score` (NUMERIC)
|
- `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** - Добавление связи между конфигурациями и словарями
|
4. **004_add_config_dictionaries.sql** - Добавление связи между конфигурациями и словарями
|
||||||
5. **005_fix_weekly_report_mv.sql** - Исправление использования ISOYEAR вместо YEAR для корректной работы на границе года
|
5. **005_fix_weekly_report_mv.sql** - Исправление использования ISOYEAR вместо YEAR для корректной работы на границе года
|
||||||
6. **006_fix_weekly_report_mv_structure.sql** - Исправление структуры view (добавление LEFT JOIN для включения всех проектов)
|
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
|
```bash
|
||||||
psql -U playeng -d playeng -f migrations/005_fix_weekly_report_mv.sql
|
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/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:
|
Или через docker-compose:
|
||||||
@@ -82,6 +87,8 @@ psql -U playeng -d playeng -f migrations/006_fix_weekly_report_mv_structure.sql
|
|||||||
```bash
|
```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/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/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
|
## Обновление Materialized View
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.27.2",
|
"version": "3.28.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user