Справочник участников M2M: установка-источник отправляет его на сервер лицензий; установки без своего справочника от НРД забирают копию раз в сутки, после подключения шлюза — не обращаются
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -1,22 +1,146 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Снимок справочника участников M2M (получен от НРД, открытые данные): на чистой установке
|
||||
// шлюза НРД ещё нет, и справочник не получить — тогда поиск организации и остальные места
|
||||
// работают по снимку, поставляемому с bj. Как только bj получит справочник от НРД (M2MTH),
|
||||
// используется он. Снимок обновляется с выпусками bj.
|
||||
// Справочник участников M2M (открытые данные НРД). Источники, из которых берётся самый свежий:
|
||||
// - полученный от НРД этим bj (M2MTH через шлюз НРД) — handbookFile;
|
||||
// - копия с сервера лицензий (её присылает установка со шлюзом НРД, помеченная источником) —
|
||||
// handbookCloudFile, забирается раз в сутки;
|
||||
// - снимок, встроенный в bj (чистая установка без шлюза и без связи).
|
||||
//
|
||||
//go:embed handbook/m2m-handbook.json
|
||||
var handbookSnapshot []byte
|
||||
|
||||
// readHandbook — справочник от НРД, если он уже получен, иначе встроенный снимок.
|
||||
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 — самый свежий из доступных справочников.
|
||||
func readHandbook() ([]byte, error) {
|
||||
if b, err := os.ReadFile(handbookFile); err == nil && len(b) > 0 {
|
||||
return b, nil
|
||||
best := handbookSnapshot
|
||||
for _, p := range []string{handbookFile, 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
return handbookSnapshot, nil
|
||||
}
|
||||
|
||||
@@ -331,5 +331,6 @@ func saveHandbook(hb *m2m.M2MTransferHandbook) {
|
||||
}
|
||||
if raw, err := json.Marshal(out); err == nil {
|
||||
_ = os.WriteFile(handbookFile, raw, 0o600)
|
||||
pushHandbookAsync(globalRC, raw) // свежий справочник — на сервер лицензий для всех клиентов
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +293,7 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
// в лог и в ленту, горутина перезапускается. Один сбой не роняет сервис.
|
||||
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
|
||||
go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск
|
||||
go handbookLoop(ctx, s.rc) // справочник участников M2M с сервера лицензий (для установок без шлюза НРД)
|
||||
|
||||
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
|
||||
// ответы робота/контрагента и применяет их через ApplyDecision.
|
||||
|
||||
Reference in New Issue
Block a user