5.5.0: Срок разблокировки из min_goal_score
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m24s

This commit is contained in:
poignatov
2026-02-24 17:29:02 +03:00
parent 7bbd732d72
commit c04422ed69
5 changed files with 55 additions and 25 deletions

View File

@@ -1 +1 @@
5.4.0 5.5.0

View File

@@ -3140,14 +3140,6 @@ func (a *App) startWeeklyGoalsScheduler() {
log.Printf("Materialized view refreshed successfully") 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 // Обновляем project_score_sample_mv
_, err = a.DB.Exec("REFRESH MATERIALIZED VIEW project_score_sample_mv") _, err = a.DB.Exec("REFRESH MATERIALIZED VIEW project_score_sample_mv")
if err != nil { if err != nil {
@@ -11465,22 +11457,25 @@ func (a *App) calculateProjectPointsFromDate(
return totalScore, nil return totalScore, nil
} }
// getProjectMedian получает медиану проекта из materialized view projects_median_mv // getProjectMinGoalScoreCurrentWeek получает min_goal_score проекта для текущей недели из weekly_goals.
// Если медиана отсутствует, возвращает ошибку // Используется как «недельная норма» баллов при расчёте срока разблокировки желаний.
func (a *App) getProjectMedian(projectID int) (float64, error) { // Если запись отсутствует или значение 0, возвращает ошибку.
var median float64 func (a *App) getProjectMinGoalScoreCurrentWeek(projectID int) (float64, error) {
var minGoal float64
err := a.DB.QueryRow(` err := a.DB.QueryRow(`
SELECT median_score SELECT min_goal_score
FROM projects_median_mv FROM weekly_goals
WHERE project_id = $1 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 != nil {
if err == sql.ErrNoRows { 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 0, err
} }
return median, nil return minGoal, nil
} }
// calculateProjectUnlockWeeks рассчитывает срок разблокировки проекта в неделях // calculateProjectUnlockWeeks рассчитывает срок разблокировки проекта в неделях
@@ -11488,10 +11483,11 @@ func (a *App) getProjectMedian(projectID int) (float64, error) {
// requiredPoints - необходимое количество баллов // requiredPoints - необходимое количество баллов
// startDate - дата начала подсчета (может быть nil - за всё время) // startDate - дата начала подсчета (может быть nil - за всё время)
// userID - ID пользователя (владельца условия) // userID - ID пользователя (владельца условия)
// «Недельная норма» берётся из weekly_goals.min_goal_score для текущей недели.
// Возвращает количество недель (float64): // Возвращает количество недель (float64):
// - > 0: условие не выполнено, возвращает количество недель // - > 0: условие не выполнено, возвращает количество недель
// - 0: условие уже выполнено (remaining <= 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 { func (a *App) calculateProjectUnlockWeeks(projectID int, requiredPoints float64, startDate sql.NullTime, userID int) float64 {
// 1. Получаем текущие баллы от startDate // 1. Получаем текущие баллы от startDate
currentPoints, err := a.calculateProjectPointsFromDate(projectID, startDate, userID) currentPoints, err := a.calculateProjectPointsFromDate(projectID, startDate, userID)
@@ -11507,16 +11503,16 @@ func (a *App) calculateProjectUnlockWeeks(projectID int, requiredPoints float64,
return 0 return 0
} }
// 3. Получаем медиану проекта // 3. Получаем недельную норму (min_goal_score текущей недели)
median, err := a.getProjectMedian(projectID) minGoal, err := a.getProjectMinGoalScoreCurrentWeek(projectID)
if err != nil || median <= 0 { if err != nil || minGoal <= 0 {
// Если медиана отсутствует или равна 0, возвращаем 99999 (нельзя рассчитать) // Если min_goal_score отсутствует или равен 0, возвращаем 99999 (нельзя рассчитать)
// Это нормальная ситуация, не логируем // Это нормальная ситуация, не логируем
return 99999 return 99999
} }
// 4. Рассчитываем недели // 4. Рассчитываем недели
weeks := remaining / median weeks := remaining / minGoal
return weeks return weeks
} }

View File

@@ -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.';

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "5.4.0", "version": "5.5.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",