6.16.5: Оптимизация выполнения задачи
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m39s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
poignatov
2026-03-15 13:14:25 +03:00
parent 7889922d9b
commit 95985f97f2
6 changed files with 135 additions and 70 deletions

View File

@@ -1 +1 @@
6.16.4 6.16.5

View File

@@ -8454,22 +8454,8 @@ func (a *App) getTodoistStatusHandler(w http.ResponseWriter, r *http.Request) {
// Tasks handlers // Tasks handlers
// ============================================ // ============================================
// getTasksHandler возвращает список задач пользователя // fetchTasksForUser возвращает список задач пользователя из БД
func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { func (a *App) fetchTasksForUser(userID int) ([]Task, error) {
if r.Method == "OPTIONS" {
setCORSHeaders(w)
w.WriteHeader(http.StatusOK)
return
}
setCORSHeaders(w)
userID, ok := getUserIDFromContext(r)
if !ok {
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Запрос с получением всех необходимых данных для группировки и отображения
query := ` query := `
SELECT SELECT
t.id, t.id,
@@ -8526,8 +8512,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
rows, err := a.DB.Query(query, userID) rows, err := a.DB.Query(query, userID)
if err != nil { if err != nil {
log.Printf("Error querying tasks: %v", err) log.Printf("Error querying tasks: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error querying tasks: %v", err), http.StatusInternalServerError) return nil, err
return
} }
defer rows.Close() defer rows.Close()
@@ -8637,6 +8622,30 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
tasks = append(tasks, task) tasks = append(tasks, task)
} }
return tasks, nil
}
// getTasksHandler возвращает список задач пользователя
func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
setCORSHeaders(w)
w.WriteHeader(http.StatusOK)
return
}
setCORSHeaders(w)
userID, ok := getUserIDFromContext(r)
if !ok {
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized)
return
}
tasks, err := a.fetchTasksForUser(userID)
if err != nil {
sendErrorWithCORS(w, fmt.Sprintf("Error querying tasks: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks) json.NewEncoder(w).Encode(tasks)
} }
@@ -10709,61 +10718,79 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error
if err != nil { if err != nil {
log.Printf("Error querying subtasks: %v", err) log.Printf("Error querying subtasks: %v", err)
} else { } else {
defer subtaskRows.Close() // Собираем подзадачи с reward_message
for subtaskRows.Next() { type subtaskInfo struct {
var subtaskID int id int
var subtaskName string name string
var subtaskRewardMessage sql.NullString rewardMessage string
var subtaskProgressionBase sql.NullFloat64 progressionBase sql.NullFloat64
}
var subtasks []subtaskInfo
subtaskIDs := make([]int, 0)
err := subtaskRows.Scan(&subtaskID, &subtaskName, &subtaskRewardMessage, &subtaskProgressionBase) for subtaskRows.Next() {
var st subtaskInfo
var subtaskRewardMessage sql.NullString
err := subtaskRows.Scan(&st.id, &st.name, &subtaskRewardMessage, &st.progressionBase)
if err != nil { if err != nil {
log.Printf("Error scanning subtask: %v", err) log.Printf("Error scanning subtask: %v", err)
continue continue
} }
// Пропускаем подзадачи с пустым reward_message
if !subtaskRewardMessage.Valid || subtaskRewardMessage.String == "" { if !subtaskRewardMessage.Valid || subtaskRewardMessage.String == "" {
continue continue
} }
st.rewardMessage = subtaskRewardMessage.String
subtasks = append(subtasks, st)
subtaskIDs = append(subtaskIDs, st.id)
}
subtaskRows.Close()
// Получаем награды подзадачи // Батчевый запрос наград для всех подзадач за один раз
subtaskRewardRows, err := a.DB.Query(` subtaskRewardsMap := make(map[int][]Reward)
SELECT rc.position, p.name AS project_name, rc.value, rc.use_progression if len(subtaskIDs) > 0 {
idArgs := make([]interface{}, len(subtaskIDs))
idPlaceholders := make([]string, len(subtaskIDs))
for i, id := range subtaskIDs {
idArgs[i] = id
idPlaceholders[i] = fmt.Sprintf("$%d", i+1)
}
batchQuery := fmt.Sprintf(`
SELECT rc.task_id, rc.position, p.name AS project_name, rc.value, rc.use_progression
FROM reward_configs rc FROM reward_configs rc
JOIN projects p ON rc.project_id = p.id JOIN projects p ON rc.project_id = p.id
WHERE rc.task_id = $1 WHERE rc.task_id IN (%s)
ORDER BY rc.position ORDER BY rc.task_id, rc.position
`, subtaskID) `, strings.Join(idPlaceholders, ","))
rewardRows, err := a.DB.Query(batchQuery, idArgs...)
if err != nil { if err != nil {
log.Printf("Error querying subtask rewards: %v", err) log.Printf("Error querying subtask rewards batch: %v", err)
continue } else {
} for rewardRows.Next() {
var stTaskID int
subtaskRewards := make([]Reward, 0) var reward Reward
for subtaskRewardRows.Next() { err := rewardRows.Scan(&stTaskID, &reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression)
var reward Reward if err != nil {
err := subtaskRewardRows.Scan(&reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression) log.Printf("Error scanning subtask reward: %v", err)
if err != nil { continue
log.Printf("Error scanning subtask reward: %v", err) }
continue subtaskRewardsMap[stTaskID] = append(subtaskRewardsMap[stTaskID], reward)
} }
subtaskRewards = append(subtaskRewards, reward) rewardRows.Close()
} }
subtaskRewardRows.Close() }
// Вычисляем score для наград подзадачи // Формируем сообщения для каждой подзадачи
for _, st := range subtasks {
subtaskRewardStrings := make(map[int]string) subtaskRewardStrings := make(map[int]string)
var subtaskProgressionBasePtr *float64 var subtaskProgressionBasePtr *float64
if subtaskProgressionBase.Valid { if st.progressionBase.Valid {
subtaskProgressionBasePtr = &subtaskProgressionBase.Float64 subtaskProgressionBasePtr = &st.progressionBase.Float64
} else if progressionBase.Valid { } else if progressionBase.Valid {
subtaskProgressionBasePtr = &progressionBase.Float64 subtaskProgressionBasePtr = &progressionBase.Float64
} }
for _, reward := range subtaskRewards { for _, reward := range subtaskRewardsMap[st.id] {
score := calculateRewardScore(reward, req.Value, subtaskProgressionBasePtr) score := calculateRewardScore(reward, req.Value, subtaskProgressionBasePtr)
var rewardStr string var rewardStr string
if score >= 0 { if score >= 0 {
rewardStr = fmt.Sprintf("**%s+%.4g**", reward.ProjectName, score) rewardStr = fmt.Sprintf("**%s+%.4g**", reward.ProjectName, score)
@@ -10772,10 +10799,7 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error
} }
subtaskRewardStrings[reward.Position] = rewardStr subtaskRewardStrings[reward.Position] = rewardStr
} }
subtaskMessage := replaceRewardPlaceholders(st.rewardMessage, subtaskRewardStrings, task.Name, st.name)
// Подставляем в reward_message подзадачи
subtaskMessage := replaceRewardPlaceholders(subtaskRewardMessage.String, subtaskRewardStrings, task.Name, subtaskName)
subtaskMessages = append(subtaskMessages, subtaskMessage) subtaskMessages = append(subtaskMessages, subtaskMessage)
} }
} }
@@ -10789,13 +10813,15 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error
finalMessage.WriteString(subtaskMsg) finalMessage.WriteString(subtaskMsg)
} }
// Отправляем сообщение через processMessage // Отправляем сообщение через processMessage асинхронно, чтобы не блокировать ответ
userIDPtr := &userID userIDPtr := &userID
_, err = a.processMessage(finalMessage.String(), userIDPtr) finalMessageStr := finalMessage.String()
if err != nil { go func() {
// Логируем ошибку, но не откатываем транзакцию _, err := a.processMessage(finalMessageStr, userIDPtr)
log.Printf("Error sending message to Telegram: %v", err) if err != nil {
} log.Printf("Error sending message to Telegram: %v", err)
}
}()
// Обновляем completed и last_completed_at для основной задачи // Обновляем completed и last_completed_at для основной задачи
// Если repetition_date установлен, вычисляем next_show_at // Если repetition_date установлен, вычисляем next_show_at
@@ -11171,10 +11197,23 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Возвращаем обновлённый список задач чтобы фронтенд не делал повторный GET
tasks, err := a.fetchTasksForUser(userID)
if err != nil {
log.Printf("Error fetching tasks after completion: %v", err)
// Фолбэк: возвращаем минимальный ответ, фронтенд сам обновится
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Task completed successfully",
})
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "success": true,
"message": "Task completed successfully", "tasks": tasks,
}) })
} }
@@ -11232,10 +11271,22 @@ func (a *App) completeAndDeleteTaskHandler(w http.ResponseWriter, r *http.Reques
return return
} }
// Возвращаем обновлённый список задач чтобы фронтенд не делал повторный GET
tasks, err := a.fetchTasksForUser(userID)
if err != nil {
log.Printf("Error fetching tasks after completion: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Task completed and deleted successfully",
})
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "success": true,
"message": "Task completed and deleted successfully", "tasks": tasks,
}) })
} }

Binary file not shown.

View File

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

View File

@@ -1325,7 +1325,13 @@ function AppContent() {
backgroundLoading={tasksBackgroundLoading} backgroundLoading={tasksBackgroundLoading}
error={tasksError} error={tasksError}
onRetry={() => fetchTasksData(false)} onRetry={() => fetchTasksData(false)}
onRefresh={(isBackground = false) => fetchTasksData(isBackground)} onRefresh={(tasksOrBackground) => {
if (Array.isArray(tasksOrBackground)) {
setTasksData(tasksOrBackground)
} else {
fetchTasksData(tasksOrBackground === true)
}
}}
/> />
</div> </div>
</div> </div>

View File

@@ -593,13 +593,17 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
throw new Error(errorData.message || 'Ошибка при выполнении задачи') throw new Error(errorData.message || 'Ошибка при выполнении задачи')
} }
const data = await response.json().catch(() => ({}))
// Показываем уведомление о выполнении // Показываем уведомление о выполнении
if (onTaskCompleted) { if (onTaskCompleted) {
onTaskCompleted() onTaskCompleted()
} }
// Обновляем список и закрываем модальное окно // Если бэкенд вернул обновлённый список — передаём его, иначе делаем повторный GET
if (onRefresh) { if (data.tasks && onRefresh) {
onRefresh(data.tasks)
} else if (onRefresh) {
onRefresh() onRefresh()
} }
if (onClose) { if (onClose) {
@@ -656,13 +660,17 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
throw new Error(errorData.message || 'Ошибка при выполнении задачи') throw new Error(errorData.message || 'Ошибка при выполнении задачи')
} }
const data = await response.json().catch(() => ({}))
// Показываем уведомление о выполнении // Показываем уведомление о выполнении
if (onTaskCompleted) { if (onTaskCompleted) {
onTaskCompleted() onTaskCompleted()
} }
// Обновляем список и закрываем модальное окно // Если бэкенд вернул обновлённый список — передаём его, иначе делаем повторный GET
if (onRefresh) { if (data.tasks && onRefresh) {
onRefresh(data.tasks)
} else if (onRefresh) {
onRefresh() onRefresh()
} }
if (onClose) { if (onClose) {