5.5.0: Срок разблокировки из min_goal_score
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m24s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m24s
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.';
|
||||
@@ -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;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "5.4.0",
|
||||
"version": "5.5.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Reference in New Issue
Block a user