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

Binary file not shown.

View File

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

View File

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

View File

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