Files
Bridge-and-Join-s/internal/lkgateway/depoqueue.go
T
zuevav 73b246831c Пароль домена не попадает в страницу; домен в мастере — открыт, крупно, группы; справочник DEPO без переводов; 1С — бэк-офис
- Безопасность: в мастере подпись к паролю выводила сам сохранённый пароль служебной записи домена — теперь только «задан/введён» (тест: пароль не попадает в HTML). Список сессий в настройках больше не содержит токены (по ним можно было войти под чужим именем) — короткий ID.
- Домен: после подключения и «Доверять» блок остаётся открытым, страница возвращается к нему; карточка «Домен подключён» и «Найден домен» — крупно; поиск сотрудников и групп; группе — доступ с ролью (ADGroups), список групп с «убрать».
- «Обмен с DEPO»: справочник FTV_M2M_HANDBOOK считается и когда переводов ещё нет (на свежей установке писало «не создана»).
- Модуль 1С: onec-back «1С: база бэк-офиса (тест)» вместо мидл-офиса (в лицензиях не выдавался).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 01:19:30 +03:00

316 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Страница «Обмен с DEPO (Fancy)» — /admin/fancy-queue. Показывает реальный
// обмен через трансферные таблицы FTV_M2M / FTV_M2M_LINES / FTV_M2M_EVENTS:
// какие переводы bj положил в DEPO, что ответил job bj2depo (DEPO_ORDER_CREATED /
// DEPO_ERROR), в каком состоянии документ в депозитарии и какие события ещё
// не обработаны. Заменила прежнюю «очередь на проверку крана».
import (
"context"
"database/sql"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"log"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
type depoQueueLine struct {
ISIN, SecCode string
Amount string
State string
Comment string
}
type depoQueueEvent struct {
Ref, State, Type, Codes, Comment string
At time.Time
}
type depoQueueRow struct {
GUID string
State string
Direction string
Deponent string
DeponentDoc string
CP string
CreatedAt time.Time
Doc string
DocState string
DocAt time.Time
Comment string
Lines []depoQueueLine
Events []depoQueueEvent
Pending int // события со STATE=TODO (job ещё не обработал)
Errors []string
LastEvent string
LastAt time.Time
Link *DepoLink
}
func (r depoQueueRow) DirectionRu() string {
if strings.EqualFold(r.Direction, "OUT") {
return "↗ списание (мы отдаём)"
}
return "↘ зачисление (мы принимаем)"
}
// StageRu — человеческое состояние перевода по совокупности признаков.
func (r depoQueueRow) StageRu() (cls, text string) {
up := strings.ToUpper(r.State)
switch {
case len(r.Errors) > 0 || up == "ERROR":
return "rejected", "ошибка DEPO"
case strings.Contains(strings.ToLower(r.DocState), "отмен"):
return "rejected", "документ отменён в DEPO"
case strings.Contains(strings.ToLower(r.DocState), "конец"):
return "confirmed", "документ проведён"
case r.Doc != "":
return "sent", "документ создан: " + r.DocState
case r.Pending > 0:
return "awaiting_decision", "ждёт job bj2depo"
case up == "SUCCESS" || up == "DONE":
return "confirmed", "обработан"
}
if up == "" {
return "", "—"
}
return "", up
}
type depoQueueSummary struct {
Total, Pending, WithDoc, Errors int
Handbook int
HandbookAt time.Time
HasHandbook bool
LastEventAt time.Time
}
func depoQueueLoad(ctx context.Context, rc *RuntimeConfig, limit int) ([]depoQueueRow, depoQueueSummary, error) {
var sum depoQueueSummary
db, err := depoEventsDB(rc)
if err != nil {
return nil, sum, err
}
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
if !depoTableExists(ctx, db, "FTV_M2M") {
return nil, sum, errNoDepoTables
}
docCol := "NULL"
if depoHasColumn(ctx, db, "FTV_M2M", "DOC") {
docCol = "CAST(m.DOC AS varchar(32))"
}
timeCol := "NULL"
for _, c := range []string{"TimeNew", "TIME_NEW", "CREATED_AT"} {
if depoHasColumn(ctx, db, "FTV_M2M", c) {
timeCol = "m." + c
break
}
}
rows, err := db.QueryContext(ctx, fsql.Que01+timeCol+`, ISNULL(`+docCol+fsql.Que02+docCol+fsql.Que03+docCol+`)
FROM dbo.FTV_M2M m ORDER BY `+strings.Replace(timeCol, "NULL", "m.GUID", 1)+` DESC`, sql.Named("n", limit))
if err != nil {
return nil, sum, err
}
var out []depoQueueRow
idx := map[string]int{}
for rows.Next() {
var r depoQueueRow
var created, docAt sql.NullTime
if err := rows.Scan(&r.GUID, &r.State, &r.Direction, &r.Deponent, &r.DeponentDoc, &r.CP, &created, &r.Doc, &r.Comment, &r.DocState, &docAt); err != nil {
rows.Close()
return nil, sum, err
}
if created.Valid {
r.CreatedAt = created.Time
}
if docAt.Valid {
r.DocAt = docAt.Time
}
r.GUID = strings.ToUpper(r.GUID)
r.Link = globalDepoLinks.Get(r.GUID)
idx[r.GUID] = len(out)
out = append(out, r)
}
rows.Close()
// справочник участников — и когда переводов ещё нет (свежая установка): иначе страница писала «не создана»
if depoTableExists(ctx, db, "FTV_M2M_HANDBOOK") {
sum.HasHandbook = true
_ = db.QueryRowContext(ctx, fsql.QQu01).Scan(&sum.Handbook)
if depoHasColumn(ctx, db, "FTV_M2M_HANDBOOK", "HANDBOOK_AT") {
var t sql.NullTime
if db.QueryRowContext(ctx, fsql.QQu02).Scan(&t) == nil && t.Valid {
sum.HandbookAt = t.Time
}
}
}
if len(out) == 0 {
return out, sum, nil
}
// строки
if depoTableExists(ctx, db, "FTV_M2M_LINES") {
lr, err := db.QueryContext(ctx, fsql.Que04+strings.Replace(strings.TrimPrefix(timeCol, "m."), "NULL", "GUID", 1)+` DESC)`, sql.Named("n", limit))
if err == nil {
for lr.Next() {
var g string
var l depoQueueLine
if lr.Scan(&g, &l.ISIN, &l.SecCode, &l.Amount, &l.State, &l.Comment) == nil {
if i, ok := idx[strings.ToUpper(g)]; ok {
l.Amount = strings.TrimRight(strings.TrimRight(l.Amount, "0"), ".")
out[i].Lines = append(out[i].Lines, l)
if strings.EqualFold(l.State, "ERROR") && l.Comment != "" {
out[i].Errors = append(out[i].Errors, l.Comment)
}
}
}
}
lr.Close()
}
}
// события
if depoTableExists(ctx, db, "FTV_M2M_EVENTS") {
er, err := db.QueryContext(ctx, fsql.Que05+strings.Replace(strings.TrimPrefix(timeCol, "m."), "NULL", "GUID", 1)+` DESC) ORDER BY e.EVENT_DATE`, sql.Named("n", limit))
if err == nil {
for er.Next() {
var g string
var e depoQueueEvent
var at sql.NullTime
if er.Scan(&g, &e.Ref, &e.State, &e.Type, &at, &e.Codes, &e.Comment) == nil {
if at.Valid {
e.At = at.Time
}
if i, ok := idx[strings.ToUpper(g)]; ok {
out[i].Events = append(out[i].Events, e)
if strings.EqualFold(e.State, "TODO") {
out[i].Pending++
}
if e.Type == "DEPO_ERROR" && e.Comment != "" {
out[i].Errors = append(out[i].Errors, e.Comment)
}
if e.At.After(out[i].LastAt) {
out[i].LastAt, out[i].LastEvent = e.At, e.Type
}
if e.At.After(sum.LastEventAt) {
sum.LastEventAt = e.At
}
}
}
}
er.Close()
}
}
for i := range out {
sum.Total++
if out[i].Pending > 0 {
sum.Pending++
}
if out[i].Doc != "" {
sum.WithDoc++
}
if len(out[i].Errors) > 0 {
sum.Errors++
}
}
sort.SliceStable(out, func(i, j int) bool {
ai, aj := out[i].CreatedAt, out[j].CreatedAt
if ai.IsZero() {
ai = out[i].LastAt
}
if aj.IsZero() {
aj = out[j].LastAt
}
return ai.After(aj)
})
return out, sum, nil
}
var errNoDepoTables = errNoDepo("в базе DEPO нет таблицы FTV_M2M — обмен ещё не настроен программистом Fancy")
type errNoDepo string
func (e errNoDepo) Error() string { return string(e) }
// fancyQueue — GET /admin/fancy-queue: обмен с DEPO через FTV_M2M*.
func (h *setupHandlers) fancyQueue(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Fancy
var rows []depoQueueRow
var sum depoQueueSummary
var loadErr string
if s.Server == "" {
loadErr = "Подключение к базе DEPO (Fancy) не настроено — Настройка → Обмен Fancy"
} else if rs, sm, err := depoQueueLoad(r.Context(), h.rc, 100); err != nil {
loadErr = "не удалось прочитать трансферные таблицы: " + err.Error()
} else {
rows, sum = rs, sm
}
// ошибки обработчика и пользователь для EXECUTE AS — видны сразу на странице
var fails []depoJobFailure
runAsOK, runAsMsg := true, ""
if s.Server != "" && loadErr == "" {
if db, err := depoEventsDB(h.rc); err == nil {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
fails, _ = depoJobFailures(ctx, db, 7, 50)
runAsOK, runAsMsg = depoRunAsCheck(ctx, db, s)
cancel()
db.Close()
}
}
data := struct {
page
Rows []depoQueueRow
Sum depoQueueSummary
Error string
Flash string
DB string
Fancy FancyMSSQLSettings
JobMode string
Failures []depoJobFailure
RunAs string
RunAsOK bool
RunAsMsg string
}{nowPage("Обмен с DEPO", "fancy"), rows, sum, loadErr, r.URL.Query().Get("flash"), s.DepoDB(), s, depoJobModeRu(s.DepoJobMode), fails, depoJobRunAs(s), runAsOK, runAsMsg}
render(w, h.tpl.a.fancyQueue, data)
}
// fancyQueueRequeue — POST /admin/fancy-queue/requeue: вернуть записи с ошибкой обработчика в очередь.
func (h *setupHandlers) fancyQueueRequeue(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 = depoRequeueFailures(ctx, db, 7)
cancel()
db.Close()
if err == nil {
msg = fmt.Sprintf("Возвращено в очередь обработчика: поручений %d, событий %d. Обновите страницу через 10–20 секунд.", 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":
return "процедуру вызывает bj"
case "sql-auto":
return "SQL Agent (создан из bj)"
case "sql-manual":
return "SQL Agent (настроен вручную)"
}
return "не выбран"
}