6.16.5: Оптимизация выполнения задачи
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m39s
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:
@@ -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)
|
|
||||||
for subtaskRewardRows.Next() {
|
|
||||||
var reward Reward
|
var reward Reward
|
||||||
err := subtaskRewardRows.Scan(&reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression)
|
err := rewardRows.Scan(&stTaskID, &reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scanning subtask reward: %v", err)
|
log.Printf("Error scanning subtask reward: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
subtaskRewards = append(subtaskRewards, reward)
|
subtaskRewardsMap[stTaskID] = append(subtaskRewardsMap[stTaskID], 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()
|
||||||
|
go func() {
|
||||||
|
_, err := a.processMessage(finalMessageStr, userIDPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Логируем ошибку, но не откатываем транзакцию
|
|
||||||
log.Printf("Error sending message to Telegram: %v", err)
|
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,11 +11197,24 @@ 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")
|
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",
|
"message": "Task completed successfully",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"tasks": tasks,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// completeAndDeleteTaskHandler выполняет задачу и затем удаляет её
|
// completeAndDeleteTaskHandler выполняет задачу и затем удаляет её
|
||||||
@@ -11232,11 +11271,23 @@ 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")
|
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",
|
"message": "Task completed and deleted successfully",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"tasks": tasks,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// postponeTaskHandler переносит задачу на указанную дату
|
// postponeTaskHandler переносит задачу на указанную дату
|
||||||
|
|||||||
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user