diff --git a/internal/lkgateway/handbook_embed.go b/internal/lkgateway/handbook_embed.go index 7b1774c..13174d7 100644 --- a/internal/lkgateway/handbook_embed.go +++ b/internal/lkgateway/handbook_embed.go @@ -33,10 +33,15 @@ func handbookAt(b []byte) time.Time { return h.At } -// readHandbook — самый свежий из доступных справочников. +// readHandbook — справочник своего контура: полученный этим bj от НРД, если он есть (боевой и +// тестовый справочники разные — в боевом меньше участников, не все подключились; копия с сервера +// лицензий может быть с другого контура). Нет своего — самый свежий из копии и встроенного снимка. func readHandbook() ([]byte, error) { + if b, err := os.ReadFile(handbookFile); err == nil && len(b) > 0 && !handbookAt(b).IsZero() { + return b, nil + } best := handbookSnapshot - for _, p := range []string{handbookFile, handbookCloudFile} { + for _, p := range []string{handbookCloudFile} { if b, err := os.ReadFile(p); err == nil && len(b) > 0 && handbookAt(b).After(handbookAt(best)) { best = b } @@ -58,6 +63,9 @@ func pushHandbookAsync(rc *RuntimeConfig, raw []byte) { if rc == nil || strings.TrimSpace(rc.Snapshot().License.Key) == "" { return } + if contourOf(currentNSDProfile()) != "prod" { + return // общий справочник — только боевой: тестовый (TEST3) шире и для клиентов неверен + } go func() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/internal/lkgateway/lkportfolio.go b/internal/lkgateway/lkportfolio.go index 405164f..9bdf80f 100644 --- a/internal/lkgateway/lkportfolio.go +++ b/internal/lkgateway/lkportfolio.go @@ -385,11 +385,12 @@ func lkContractByAccount(ps LKPortfolioSyncSettings, acc string) string { return strings.ReplaceAll(ps.contractTemplate(), "{n}", n) } -// lkPortfolioClients — поиск клиентов DEPO по ФИО или номеру портфеля (открытые торговые счета). +// lkPortfolioClients — клиенты DEPO по ФИО, портфелю или счёту (открытые торговые счета); пустой +// запрос — первые по алфавиту, чтобы выбрать из списка без ввода. func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKPortfolioClient, error) { q = strings.TrimSpace(q) - if len([]rune(q)) < 2 { - return nil, nil + if len([]rune(q)) == 1 { + return []LKPortfolioClient{}, nil } fs := rc.Snapshot().Fancy ps := rc.Snapshot().LK.PortfolioSync @@ -400,7 +401,7 @@ func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKP defer db.Close() cctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - rows, err := db.QueryContext(cctx, `SELECT TOP 20 ISNULL(p.PORT_NUM,''), ISNULL(f.NAME,''), c.NUMBER + rows, err := db.QueryContext(cctx, `SELECT TOP 50 ISNULL(p.PORT_NUM,''), ISNULL(f.NAME,''), c.NUMBER FROM OD_CLIENTS c JOIN OD_FACES f ON f.ID=c.FACE LEFT JOIN OD_CLIENTS_PORTS p ON p.CLIENT=c.ID WHERE c.NUMBER LIKE @pre AND c.STATE=0 AND c.D_CLOSE IS NULL AND (f.NAME LIKE @q OR p.PORT_NUM LIKE @q OR c.NUMBER LIKE @q) ORDER BY f.NAME`, sql.Named("q", "%"+likeEscape(q)+"%"), sql.Named("pre", likeEscape(ps.accountPrefix())+"%")) @@ -408,7 +409,7 @@ func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKP return nil, err } defer rows.Close() - var out []LKPortfolioClient + out := []LKPortfolioClient{} for rows.Next() { var c LKPortfolioClient if rows.Scan(&c.Port, &c.Name, &c.Account) == nil && c.Port != "" { diff --git a/internal/lkgateway/nsdpoller.go b/internal/lkgateway/nsdpoller.go index 8afad86..afb19c5 100644 --- a/internal/lkgateway/nsdpoller.go +++ b/internal/lkgateway/nsdpoller.go @@ -268,7 +268,9 @@ func (s *Server) applyIncoming(ctx context.Context, p igw.Package) error { } nsdProbeReply(time.Now()) writeHandbookRawXML(unpacked.DocXML) // подлинный XML НРД — для выгрузки со страницы справочника - if !saveHandbook(&hb) { + changed := saveHandbook(&hb) + participantHandbookCheck(s.rc) // появились в справочнике — анкета принята + if !changed { log.Printf("lk-gateway: поллер справочник M2M id=%d: без изменений (%d участников) — связь с НРД подтверждена", p.ID, len(hb.Participants.Participants)) return nil } diff --git a/internal/lkgateway/participant_form.go b/internal/lkgateway/participant_form.go index ac12b5d..594ff37 100644 --- a/internal/lkgateway/participant_form.go +++ b/internal/lkgateway/participant_form.go @@ -488,21 +488,19 @@ func participantRegStatus(rc *RuntimeConfig) string { if contour == "" { contour = "текущего" } - hb, ok := handbookByCode(orgCode(s)) - if !ok { - if inn := orgINN(s); inn != "" { - hb, ok = handbookByINN(inn) - } + _ = s + in, known, at, n := nsdHandbookHasUs(rc) + switch { + case in: + out.WriteString(`
В справочнике участников M2M, полученном от НРД (` + contour + ` контур, ` + + at.In(listMSK).Format("02.01 15:04") + `), наша организация есть. Регистрация подтверждена.
`) + case known: + out.WriteString(fmt.Sprintf(`
В справочнике участников M2M, полученном от НРД (%s контур, %s, участников %d), нашей организации пока нет. `+ + `После приёма анкеты НРД добавит её в справочник — bj получает его каждые несколько минут и сам отметит регистрацию.
`, + contour, at.In(listMSK).Format("02.01 15:04"), n)) + default: + out.WriteString(`
Справочник участников M2M от НРД ещё не получен — проверить регистрацию по нему пока нельзя.
`) } - if ok { - out.WriteString(`
В справочнике участников M2M ` + contour + ` контура: есть — ` + - html.EscapeString(pickStr(hb.Name, hb.FullName)) + `, код ` + html.EscapeString(hb.Code) + - `, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.
`) - return out.String() - } - out.WriteString(`
В справочнике участников M2M ` + contour + ` контура нашей организации пока нет. ` + - `После приёма анкеты НРД организация появится в справочнике (обновляется каждые ~10 минут) — тогда здесь будет «есть». ` + - `Регистрацию подтверждает также ответный документ от НРД.
`) return out.String() } diff --git a/internal/lkgateway/participant_reg.go b/internal/lkgateway/participant_reg.go index 4260666..eea0b14 100644 --- a/internal/lkgateway/participant_reg.go +++ b/internal/lkgateway/participant_reg.go @@ -149,6 +149,9 @@ func recordParticipantReply(status string, resp *m2m.M2MTransferResponse, rawFil // participantAccepted — последняя отправленная анкета принята НРД. func participantAccepted() bool { + if in, _, _, _ := nsdHandbookHasUs(globalRC); in { + return true // уже в справочнике участников НРД — зарегистрированы (в т.ч. без анкеты из bj) + } r := loadParticipantReg() return !r.RespAt.IsZero() && !strings.EqualFold(r.Status, "ERROR") && !strings.EqualFold(r.Status, "INFO") } @@ -185,3 +188,45 @@ func participantBanner() template.HTML { return template.HTML(`
` + esc(head) + `` + esc(body) + ` Открыть анкету →
`) } + +// nsdHandbookHasUs — наша организация в справочнике участников M2M, который этот bj получил от НРД +// (handbookFile, текущий контур). known=false — справочника от НРД ещё нет. Общий справочник с +// сервера лицензий здесь не годится: он собран с других установок и контуров. +func nsdHandbookHasUs(rc *RuntimeConfig) (in, known bool, at time.Time, n int) { + b, err := os.ReadFile(handbookFile) + if err != nil || rc == nil { + return false, false, time.Time{}, 0 + } + var h struct { + At time.Time `json:"at"` + Participants []HandbookEntry `json:"participants"` + } + if json.Unmarshal(b, &h) != nil { + return false, false, time.Time{}, 0 + } + s := rc.Snapshot() + code, inn := strings.ToUpper(strings.TrimSpace(orgCode(s))), strings.TrimSpace(orgINN(s)) + for _, p := range h.Participants { + if (code != "" && strings.EqualFold(p.Code, code)) || (code == "" && inn != "" && p.INN == inn) { + return true, true, h.At, len(h.Participants) + } + } + return false, true, h.At, len(h.Participants) +} + +// participantHandbookCheck — после каждого справочника от НРД: появились в нём, а анкета ещё не +// отмечена принятой — регистрация подтверждена справочником. +func participantHandbookCheck(rc *RuntimeConfig) { + in, known, at, _ := nsdHandbookHasUs(rc) + if !in || !known { + return + } + r := loadParticipantReg() + if r.SentAt.IsZero() || (!r.RespAt.IsZero() && !strings.EqualFold(r.Status, "INFO") && !strings.EqualFold(r.Status, "ERROR")) { + return + } + saveParticipantReg(func(r *participantReg) { + r.RespAt, r.Status, r.Codes = at, "OK", nil + r.RespText = "организация появилась в справочнике участников M2M НРД" + }) +} diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 364b370..3a8ded6 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -708,7 +708,7 @@
Залить одному клиенту (для теста)
+ @@ -726,20 +726,23 @@ sel.innerHTML=ok?('Выбран: '+esc(c.name)+' · портфель DEPO '+esc(c.port)+' · счёт '+esc(c.account)+' → договор в ЛК '+esc(c.lk)+''):'У клиента нет торгового счёта — договор в ЛК не определить'; list.innerHTML=''; } - q.addEventListener('input',function(){ - clearTimeout(t); var v=q.value.trim(); if(v.length<2){list.innerHTML='';return} + function load(){ + clearTimeout(t); var v=q.value.trim(); if(v.length===1){list.innerHTML='';return} + list.innerHTML='ищу в DEPO…'; t=setTimeout(function(){ - fetch('/admin/setup/lk-portfolio/clients?q='+encodeURIComponent(v),{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){ - if(d.error){list.innerHTML=''+esc(d.error)+'';return} + fetch('/admin/setup/lk-portfolio/clients?q='+encodeURIComponent(v),{cache:'no-store'}).then(function(r){if(!r.ok) throw new Error(r.status); return r.json()}).then(function(d){ + if(d&&d.error){list.innerHTML='DEPO: '+esc(d.error)+'';return} if(!d||!d.length){list.innerHTML='не найдено';return} list.innerHTML=''; d.forEach(function(c){ var a=document.createElement('a'); a.href='#'; a.style.display='block'; a.style.padding='2px 0'; a.innerHTML=esc(c.name)+' · '+esc(c.port)+' · '+esc(c.account)+(c.lk?' → '+esc(c.lk):''); a.onclick=function(e){e.preventDefault(); pick(c)}; list.appendChild(a); }); - }).catch(function(){list.innerHTML='поиск недоступен'}); + }).catch(function(e){list.innerHTML='поиск не ответил ('+esc(e&&e.message||'сеть')+') — повторите'}); },300); - }); + } + q.addEventListener('input',load); + q.addEventListener('focus',function(){ if(!list.innerHTML) load(); }); })(); {{range lkPortfolioStates}}
{{.LK}} ← {{.Depo}}: {{if .Err}}ошибка — {{.Err}}{{else if .PushedAt.IsZero}}сверено {{.CheckedAt.Format "15:04:05"}}, отправки ещё не было{{else}}отправлено {{.PushedAt.Format "02.01 15:04:05"}} ({{.Lines}} бумаг), сверено {{.CheckedAt.Format "15:04:05"}}{{end}}{{range .Skipped}}
пропущено: {{.}}{{end}}
{{end}}