From 2d2fbb67323c05c9df1bb0063d58e17f6514be99 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Tue, 13 Oct 2026 13:15:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B0:=20=D1=83=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB=D0=BB=20=D0=B1?= =?UTF-8?q?=D0=BE=D0=BA=D0=BE=D0=B2=D1=8B=D1=85=20=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8;?= =?UTF-8?q?=20DEPO:=20=C2=AB=D0=A1=D0=BD=D1=8F=D1=82=D1=8C=20=D1=81=20?= =?UTF-8?q?=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D0=B8=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скролл: автообновление карточки (bjReloadKeepScroll) раньше сохраняло только прокрутку окна, а боковой «Путь заявки» (.claim-tl — position:fixed, свой overflow) и другие прокручиваемые блоки (.settings-nav, .doc-toc) отбрасывало наверх. Теперь сохраняется и восстанавливается и их scrollTop (JSON в sessionStorage, восстановление на rAF/timeout/load). DEPO: на «Обмен с DEPO» рядом с «Вернуть в очередь» добавлена «Снять с очереди» — помечает застрявшие записи STATE=SKIPPED (QHl04-06, depoDismissFailures), обработчик их больше не берёт и они уходят из списка ошибок. Для устаревших/ненужных записей (напр. старые тестовые). Co-Authored-By: Claude Opus 4.8 --- internal/lkgateway/depohealth.go | 40 +++++++++++++++++++ internal/lkgateway/depoqueue.go | 27 +++++++++++++ internal/lkgateway/fancydb_handlers.go | 1 + internal/lkgateway/fsql/depohealth_q.go | 8 ++++ .../web/templates/admin_fancy_queue.html | 6 ++- internal/lkgateway/web/templates/layout.html | 31 ++++++++++---- 6 files changed, 104 insertions(+), 9 deletions(-) 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); }