Files
Bridge-and-Join-s/internal/lkgateway/handbook_embed.go
T
fontvielle f296ac5ad8 Справочник участников — своего контура; регистрация — по справочнику НРД; поиск клиентов портфеля
readHandbook брал самый свежий из своего, общего (с сервера лицензий) и
встроенного — общий был тестовым (TEST3, 166 участников, прислан с 28), и на
боевом 9 bj считал «регистрация подтверждена», хотя в боевом справочнике
НРД (89) нас нет. Теперь при своём справочнике от НРД — только он; на сервер
лицензий справочник уходит только с боевого контура.

Регистрация участника проверяется по справочнику, полученному от НРД;
появились в нём — анкета отмечается принятой автоматически, плашка уходит.

Портфель ЛК: клиенты DEPO списком по клику (первые 50 по алфавиту), «не
найдено» вместо «поиск недоступен» (пустой ответ был null).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-22 11:28:13 +03:00

155 lines
5.7 KiB
Go

package lkgateway
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
// Справочник участников M2M (открытые данные НРД). Источники, из которых берётся самый свежий:
// - полученный от НРД этим bj (M2MTH через шлюз НРД) — handbookFile;
// - копия с сервера лицензий (её присылает установка со шлюзом НРД, помеченная источником) —
// handbookCloudFile, забирается раз в сутки;
// - снимок, встроенный в bj (чистая установка без шлюза и без связи).
//
//go:embed handbook/m2m-handbook.json
var handbookSnapshot []byte
const handbookCloudFile = "/var/lib/bj/.bj/m2m-handbook-cloud.json"
func handbookAt(b []byte) time.Time {
var h struct {
At time.Time `json:"at"`
}
_ = json.Unmarshal(b, &h)
return h.At
}
// 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{handbookCloudFile} {
if b, err := os.ReadFile(p); err == nil && len(b) > 0 && handbookAt(b).After(handbookAt(best)) {
best = b
}
}
return best, nil
}
func handbookURL(rc *RuntimeConfig) string {
base := strings.TrimSpace(rc.Snapshot().Update.BaseURL)
if base == "" {
base = DefaultUpdateURL
}
return strings.TrimRight(base, "/") + "/v1/handbook"
}
// pushHandbookAsync — отправить полученный от НРД справочник на сервер лицензий. Сервер примет его,
// только если лицензия отмечена как источник справочника; у остальных — тихий отказ.
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()
req, err := http.NewRequestWithContext(ctx, http.MethodPut, handbookURL(rc), bytes.NewReader(raw))
if err != nil {
return
}
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(rc.Snapshot().License.Key))
req.Header.Set("Content-Type", "application/json")
resp, err := licenseHTTP.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
log.Printf("lk-gateway: справочник участников отправлен на сервер лицензий")
}
}()
}
// fetchHandbook — забрать справочник с сервера лицензий, если он свежее уже имеющихся.
func fetchHandbook(ctx context.Context, rc *RuntimeConfig) error {
key := strings.TrimSpace(rc.Snapshot().License.Key)
if key == "" {
return nil
}
if fi, err := os.Stat(handbookFile); err == nil && fi.Size() > 0 {
return nil // справочник уже приходит от НРД через шлюз — к серверу лицензий не обращаемся
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, handbookURL(rc), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := licenseHTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return err
}
var doc struct {
At time.Time `json:"at"`
Participants []json.RawMessage `json:"participants"`
}
if json.Unmarshal(b, &doc) != nil || len(doc.Participants) == 0 || doc.At.IsZero() {
return fmt.Errorf("не справочник")
}
cur, _ := readHandbook()
if !doc.At.After(handbookAt(cur)) {
return nil
}
if err := os.WriteFile(handbookCloudFile, b, 0o600); err != nil {
return err
}
log.Printf("lk-gateway: справочник участников обновлён с сервера лицензий (%d участников, от %s)", len(doc.Participants), doc.At.Format("02.01.2006 15:04"))
return nil
}
// handbookLoop — пока своего справочника от НРД нет (новая установка без шлюза), раз в сутки
// забирает копию с сервера лицензий; как только справочник пришёл от НРД — больше не обращается.
func handbookLoop(ctx context.Context, rc *RuntimeConfig) {
t := time.NewTimer(2 * time.Minute)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
cctx, cancel := context.WithTimeout(ctx, time.Minute)
if err := fetchHandbook(cctx, rc); err != nil {
log.Printf("lk-gateway: справочник с сервера лицензий: %v", err)
}
cancel()
t.Reset(24 * time.Hour)
}
}