Fix: Исправление определения недели на границе года (ISOYEAR)

- Заменен EXTRACT(YEAR) на EXTRACT(ISOYEAR) в materialized view для корректной работы на границе года
- Обновлена миграция 001_create_schema.sql для использования ISOYEAR
- Создана миграция 006_fix_weekly_report_mv_structure.sql для исправления структуры view (LEFT JOIN)
- Добавлен endpoint /admin/recreate-mv для пересоздания materialized view
- Обновлена документация миграций в README.md
- Обновлены зависимости Go (go.mod, go.sum)
This commit is contained in:
poignatov
2025-12-29 20:58:34 +03:00
parent 4f8a793377
commit 8d79a8f692
6 changed files with 151 additions and 8 deletions

View File

@@ -3,12 +3,9 @@ module play-eng-backend
go 1.21
require (
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/gorilla/mux v1.8.1
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/robfig/cron/v3 v3.0.1
)
require (
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
github.com/joho/godotenv v1.5.1 // indirect
)

View File

@@ -6,3 +6,5 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=

View File

@@ -1871,7 +1871,7 @@ func (a *App) initPlayLifeDB() error {
(
SELECT
n.project_id,
EXTRACT(YEAR FROM e.created_date)::INTEGER AS report_year,
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
@@ -2342,6 +2342,7 @@ func main() {
r.HandleFunc("/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b", app.getFullStatisticsHandler).Methods("GET", "OPTIONS")
r.HandleFunc("/admin", app.adminHandler).Methods("GET")
r.HandleFunc("/admin.html", app.adminHandler).Methods("GET")
r.HandleFunc("/admin/recreate-mv", app.recreateMaterializedViewHandler).Methods("POST", "OPTIONS")
port := getEnv("PORT", "8080")
log.Printf("Server starting on port %s", port)
@@ -3147,6 +3148,75 @@ func (a *App) adminHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, adminPath)
}
// recreateMaterializedViewHandler пересоздает materialized view с исправленной логикой ISOYEAR
func (a *App) recreateMaterializedViewHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
setCORSHeaders(w)
w.WriteHeader(http.StatusOK)
return
}
setCORSHeaders(w)
log.Printf("Recreating materialized view weekly_report_mv with ISOYEAR fix")
// Удаляем старый view
dropMaterializedView := `DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv`
if _, err := a.DB.Exec(dropMaterializedView); err != nil {
log.Printf("Error dropping materialized view: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error dropping materialized view: %v", err), http.StatusInternalServerError)
return
}
// Создаем новый view с ISOYEAR
createMaterializedView := `
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
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
ORDER BY
p.id, agg.report_year, agg.report_week
`
if _, err := a.DB.Exec(createMaterializedView); err != nil {
log.Printf("Error creating materialized view: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error creating materialized view: %v", err), http.StatusInternalServerError)
return
}
// Создаем индекс
createMVIndex := `
CREATE INDEX IF NOT EXISTS idx_weekly_report_mv_project_year_week
ON weekly_report_mv(project_id, report_year, report_week)
`
if _, err := a.DB.Exec(createMVIndex); err != nil {
log.Printf("Warning: Failed to create materialized view index: %v", err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Materialized view recreated successfully with ISOYEAR fix",
})
}
func (a *App) getProjectsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
setCORSHeaders(w)

View File

@@ -73,9 +73,10 @@ FROM
LEFT JOIN
(
-- 1. Предварительная агрегация: суммируем score по неделям
-- Используем ISOYEAR для корректной работы на границе года
SELECT
n.project_id,
EXTRACT(YEAR FROM e.created_date)::INTEGER AS report_year,
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
@@ -101,5 +102,5 @@ COMMENT ON TABLE projects IS 'Projects table storing project information with pr
COMMENT ON TABLE entries IS 'Entries table storing entry creation timestamps';
COMMENT ON TABLE nodes IS 'Nodes table linking projects, entries and storing scores';
COMMENT ON TABLE weekly_goals IS 'Weekly goals for projects';
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project';
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries';

View File

@@ -0,0 +1,46 @@
-- Migration: Fix weekly_report_mv structure to include all projects via LEFT JOIN
-- This ensures the view structure matches the code in main.go
-- Date: 2024-12-29
--
-- Issue: Migration 005 created the view without LEFT JOIN to projects table,
-- which means projects without data were not included in the view.
-- This migration fixes the structure to match main.go implementation.
-- Drop existing materialized view
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
-- Recreate materialized view with correct structure (LEFT JOIN with projects)
-- This ensures all projects are included, even if they have no data for a given week
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
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
ORDER BY
p.id, agg.report_year, agg.report_week
WITH DATA;
-- Recreate index
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.';

View File

@@ -57,6 +57,33 @@ docker-compose exec db psql -U playeng -d playeng -f /migrations/001_create_sche
- `report_week` (INTEGER)
- `total_score` (NUMERIC)
## Миграции
### Порядок применения миграций
1. **001_create_schema.sql** - Создание базовой структуры (таблицы, индексы, materialized view)
2. **002_add_dictionaries.sql** - Добавление таблиц для словарей
3. **003_remove_words_unique_constraint.sql** - Удаление уникального ограничения на words.name
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 для включения всех проектов)
### Применение миграций
Для существующей базы данных применяйте миграции последовательно:
```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
```
Или через docker-compose:
```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
```
## Обновление Materialized View
После изменения данных в таблицах `nodes` или `entries`, необходимо обновить materialized view: