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")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
"name": "play-life-web",
|
||||||
"version": "5.4.0",
|
"version": "5.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user