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