diff --git a/internal/lkgateway/depohealth.go b/internal/lkgateway/depohealth.go index 5280cfb..51d482d 100644 --- a/internal/lkgateway/depohealth.go +++ b/internal/lkgateway/depohealth.go @@ -120,6 +120,46 @@ func depoRequeueFailures(ctx context.Context, db *sql.DB, days int) (orders, eve return orders, events, nil } +// depoDismissFailures — снять записи с ошибкой (за days дней) с очереди: STATE='SKIPPED'. +// Обработчик их больше не берёт, и из списка ошибок они пропадают. Для устаревших/ненужных +// записей, которые не нужно переобрабатывать. +func depoDismissFailures(ctx context.Context, db *sql.DB, days int) (orders, events int64, err error) { + fails, err := depoJobFailures(ctx, db, days, 500) + if err != nil { + return 0, 0, err + } + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return 0, 0, err + } + const comment = "снято с очереди администратором (bj)" + for _, f := range fails { + q := fsql.QHl04 + if f.Table == "FTV_M2M_EVENTS" { + q = fsql.QHl05 + } + res, e := tx.ExecContext(ctx, q, sql.Named("c", comment), sql.Named("id", f.ID)) + if e != nil { + tx.Rollback() + return 0, 0, e + } + n, _ := res.RowsAffected() + if f.Table == "FTV_M2M" { + orders += n + if _, e := tx.ExecContext(ctx, fsql.QHl06, sql.Named("g", f.GUID)); e != nil { + tx.Rollback() + return 0, 0, e + } + } else { + events += n + } + } + if err := tx.Commit(); err != nil { + return 0, 0, err + } + return orders, events, nil +} + var depoFailAlertedID int64 // depoAlertEventFailures — оповещение о новых ошибках обработки событий BJ_* (поручения с diff --git a/internal/lkgateway/depoqueue.go b/internal/lkgateway/depoqueue.go index 4600f9d..90e0a70 100644 --- a/internal/lkgateway/depoqueue.go +++ b/internal/lkgateway/depoqueue.go @@ -302,6 +302,33 @@ func (h *setupHandlers) fancyQueueRequeue(w http.ResponseWriter, r *http.Request http.Redirect(w, r, "/admin/fancy-queue?flash="+url.QueryEscape(msg), http.StatusSeeOther) } +// fancyQueueDismiss — POST /admin/fancy-queue/dismiss: снять записи с ошибкой с очереди +// (STATE='SKIPPED'). Для устаревших/ненужных записей, которые не нужно переобрабатывать: +// обработчик их больше не берёт и из списка ошибок они пропадают. +func (h *setupHandlers) fancyQueueDismiss(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + msg := "" + db, err := depoEventsDB(h.rc) + if err == nil { + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) + var o, e int64 + o, e, err = depoDismissFailures(ctx, db, 7) + cancel() + db.Close() + if err == nil { + msg = fmt.Sprintf("Снято с очереди: поручений %d, событий %d. Обработчик их больше не возьмёт, из списка ошибок они убраны.", o, e) + log.Printf("depo-events: записи с ошибкой сняты с очереди: поручений %d, событий %d", o, e) + } + } + if err != nil { + msg = "Не удалось снять записи с очереди: " + quikErrorDetail(err) + } + http.Redirect(w, r, "/admin/fancy-queue?flash="+url.QueryEscape(msg), http.StatusSeeOther) +} + func depoJobModeRu(m string) string { switch m { case "bj": diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go index 3266332..e341b17 100644 --- a/internal/lkgateway/fancydb_handlers.go +++ b/internal/lkgateway/fancydb_handlers.go @@ -28,6 +28,7 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) { mux.HandleFunc("/admin/setup/fancy/depo-job/script.sql", h.fancyDepoJobScript) mux.HandleFunc("/admin/fancy-queue", h.fancyQueue) mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue) + mux.HandleFunc("/admin/fancy-queue/dismiss", h.fancyQueueDismiss) } // fancyFromForm собирает настройки из формы (пароль пустой = оставить прежний). diff --git a/internal/lkgateway/fsql/depohealth_q.go b/internal/lkgateway/fsql/depohealth_q.go index 8998b31..d3bb38a 100644 --- a/internal/lkgateway/fsql/depohealth_q.go +++ b/internal/lkgateway/fsql/depohealth_q.go @@ -5,3 +5,11 @@ var QHl01 = "UPDATE dbo.FTV_M2M SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL, CO var QHl02 = "UPDATE dbo.FTV_M2M_EVENTS SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL, COMMENT=@c WHERE ID=@id AND STATE='ERROR'" var QHl03 = "UPDATE dbo.FTV_M2M_LINES SET STATE='TODO', TimeBeg=NULL, TimeEnd=NULL WHERE GUID=@g AND STATE='ERROR'" + +// Снять с очереди (устаревшие/ненужные записи): STATE='SKIPPED' — обработчик их больше не +// берёт (берёт только TODO) и из списка ошибок (ERROR) они пропадают. +var QHl04 = "UPDATE dbo.FTV_M2M SET STATE='SKIPPED', TimeEnd=GETDATE(), COMMENT=@c WHERE ID=@id AND STATE='ERROR'" + +var QHl05 = "UPDATE dbo.FTV_M2M_EVENTS SET STATE='SKIPPED', TimeEnd=GETDATE(), COMMENT=@c WHERE ID=@id AND STATE='ERROR'" + +var QHl06 = "UPDATE dbo.FTV_M2M_LINES SET STATE='SKIPPED', TimeEnd=GETDATE() WHERE GUID=@g AND STATE='ERROR'" diff --git a/internal/lkgateway/web/templates/admin_fancy_queue.html b/internal/lkgateway/web/templates/admin_fancy_queue.html index 6cb0db4..7d3a0af 100644 --- a/internal/lkgateway/web/templates/admin_fancy_queue.html +++ b/internal/lkgateway/web/templates/admin_fancy_queue.html @@ -38,7 +38,11 @@ {{if not .At.IsZero}}{{wall .At}}{{end}}{{.Comment}} {{end}} -
+
+
+
+
+

«Вернуть в очередь» — повторить обработку после устранения причины. «Снять с очереди» — пометить записи как снятые (SKIPPED) и убрать из ошибок, если переобрабатывать их не нужно (например, старые тестовые).

{{end}} {{end}} diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index 2f0670a..0da9395 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -841,18 +841,33 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd document.addEventListener('change', function(e){ if (e.target.closest('main form')) dirty = true; }, true); try { var u = new URL(location.href); if (u.searchParams.has('flash')) { u.searchParams.delete('flash'); history.replaceState(null, '', u.pathname + u.search + u.hash); } } catch(e){} // Автообновление карточки перерисовывает страницу через перезагрузку. Сохраняем позицию - // прокрутки, чтобы при чтении заявку не отбрасывало наверх (автообновление остаётся). + // прокрутки окна И внутренних блоков со своим скроллом (боковой «Путь заявки» .claim-tl, + // навигация настроек и т.п.), чтобы при чтении ничего не отбрасывало наверх. var scrollKey = 'bj-scroll:' + location.pathname; - function bjReloadKeepScroll(){ try { sessionStorage.setItem(scrollKey, String(window.scrollY||window.pageYOffset||0)); } catch(e){} location.reload(); } + var keepScrollSel = ['.claim-tl', '.settings-nav', '.doc-toc']; // блоки с собственным скроллом + function bjReloadKeepScroll(){ + try { + var st = { w: window.scrollY || window.pageYOffset || 0, el: {} }; + keepScrollSel.forEach(function(sel){ var e = document.querySelector(sel); if (e && e.scrollTop) st.el[sel] = e.scrollTop; }); + sessionStorage.setItem(scrollKey, JSON.stringify(st)); + } catch(e){} + location.reload(); + } try { - var sy = sessionStorage.getItem(scrollKey); - if (sy !== null) { + var raw = sessionStorage.getItem(scrollKey); + if (raw !== null) { sessionStorage.removeItem(scrollKey); - var syY = parseInt(sy, 10) || 0; + var st = JSON.parse(raw); if (!st || typeof st !== 'object') st = {}; + var wy = st.w || 0, els = st.el || {}; if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e){} } - window.scrollTo(0, syY); - setTimeout(function(){ window.scrollTo(0, syY); }, 0); - window.addEventListener('load', function(){ window.scrollTo(0, syY); }); + var restore = function(){ + window.scrollTo(0, wy); + Object.keys(els).forEach(function(sel){ var e = document.querySelector(sel); if (e) e.scrollTop = els[sel]; }); + }; + restore(); + if (window.requestAnimationFrame) requestAnimationFrame(restore); + setTimeout(restore, 0); + window.addEventListener('load', restore); } } catch(e){} function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); }