feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР

- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:12 +03:00
parent 59fc774f08
commit da12a9ec1c
132 changed files with 29969 additions and 1089 deletions
+218
View File
@@ -0,0 +1,218 @@
package lkgateway
// ish_version.go — определение установленной версии Интеграционного шлюза НРД
// (ИШ) и сравнение с версией, доступной на сайте НРД.
//
// Установленная версия берётся из igate.deps.json (world-readable), доступная —
// со страницы дистрибутивов НРД. ВАЖНО: на странице соседствуют сборки под разные
// ОС с РАЗНЫМИ номерами (на момент внедрения Windows x86/x64 = 107.0.0.933, а
// Astra-Linux/amd64 = 107.0.0.930). Мы работаем на Linux (.deb, amd64), поэтому
// сравнивать нужно именно с Linux-сборкой, иначе система ложно покажет «доступно
// обновление» на версию, которой под нашу ОС ещё нет.
import (
"context"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
)
// ishDepsPath — файл манифеста .NET-сборки ИШ; содержит "igate/<версия>".
const ishDepsPath = "/opt/nsd/igate/igate/igate.deps.json"
// ishSiteURL — страница дистрибутивов ПО НРД (там же ИШ).
const ishSiteURL = "https://www.nsd.ru/workflow/system/programs/web-service/"
var (
ishVerRe = regexp.MustCompile(`"igate/(\d+(?:\.\d+){2,3})"`)
ishAstraRe = regexp.MustCompile(`Astra-Linux[^"]*?(\d{2,3}(?:\.\d+){3})`)
ishAnyRe = regexp.MustCompile(`\d{2,3}(?:\.\d+){3}`)
)
// IshVersionInfo — сводка по версиям ИШ для отображения в интерфейсе.
type IshVersionInfo struct {
Installed string `json:"installed"` // напр. 107.0.930 (из deps.json)
SiteLinux string `json:"site_linux,omitempty"` // Astra-Linux сборка на сайте НРД
SiteLatest string `json:"site_latest,omitempty"` // самая новая любая ОС (инфо)
UpToDate bool `json:"up_to_date"` // installed соответствует Linux-сборке
Note string `json:"note,omitempty"` // человекочитаемый комментарий
CheckedAt time.Time `json:"checked_at"`
}
// ishInstalledVersion читает версию ИШ из deps.json. Пусто, если ИШ не найден.
func ishInstalledVersion() string {
b, err := os.ReadFile(ishDepsPath)
if err != nil {
return ""
}
m := ishVerRe.FindSubmatch(b)
if m == nil {
return ""
}
return string(m[1])
}
// canonVer приводит версию к форме без внутренних нулей: 107.0.0.930 и 107.0.930
// дают одинаковый результат (сборочная версия .deb опускает нулевой компонент).
func canonVer(v string) string {
p := strings.Split(strings.TrimSpace(v), ".")
if len(p) < 2 {
return v
}
core := []string{p[0]}
for _, x := range p[1 : len(p)-1] {
if x != "0" {
core = append(core, x)
}
}
core = append(core, p[len(p)-1])
return strings.Join(core, ".")
}
var (
ishVerMu sync.Mutex
ishVerCache *IshVersionInfo
ishVerTTL = 6 * time.Hour
)
// ishVersionInfo возвращает сводку по версиям ИШ (с кэшем на 6ч, чтобы не дёргать
// сайт НРД на каждый показ статуса). Сетевая часть best-effort: при недоступности
// сайта возвращается только установленная версия.
func ishVersionInfo(ctx context.Context) IshVersionInfo {
ishVerMu.Lock()
if ishVerCache != nil && time.Since(ishVerCache.CheckedAt) < ishVerTTL {
c := *ishVerCache
ishVerMu.Unlock()
return c
}
ishVerMu.Unlock()
info := IshVersionInfo{
Installed: ishInstalledVersion(),
CheckedAt: time.Now().UTC(),
}
linux, latest := ishSiteVersions(ctx)
info.SiteLinux, info.SiteLatest = linux, latest
switch {
case info.Installed == "":
info.Note = "ИШ не обнаружен (deps.json недоступен)"
case linux == "":
info.UpToDate = true // не с чем сравнивать — не тревожим
info.Note = "версия на сайте НРД недоступна (проверьте вручную)"
case canonVer(info.Installed) == canonVer(linux):
info.UpToDate = true
info.Note = "актуальная версия для Linux (Astra/amd64)"
if latest != "" && canonVer(latest) != canonVer(linux) {
info.Note += "; для Windows доступна " + latest
}
default:
info.UpToDate = false
info.Note = "доступно обновление до " + linux + " (Astra-Linux)"
}
ishVerMu.Lock()
ishVerCache = &info
ishVerMu.Unlock()
return info
}
// ishSiteVersions скачивает страницу НРД и возвращает (Linux-сборка, самая новая
// по всем ОС). Обе строки пусты при ошибке сети/парсинга.
func ishSiteVersions(ctx context.Context) (linux, latest string) {
c := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ishSiteURL, nil)
if err != nil {
return "", ""
}
// WAF сайта НРД отвечает 403 на «Mozilla»-UA без заголовка Accept (эвристика
// анти-бот). Отправляем полноценный браузерный набор заголовков.
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en;q=0.8")
resp, err := c.Do(req)
if err != nil {
return "", ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", ""
}
body := make([]byte, 0, 256*1024)
buf := make([]byte, 32*1024)
for len(body) < 4*1024*1024 { // не читаем больше 4 МБ
n, rerr := resp.Body.Read(buf)
body = append(body, buf[:n]...)
if rerr != nil {
break
}
}
if m := ishAstraRe.FindSubmatch(body); m != nil {
linux = string(m[1])
}
// Самая новая версия по всем сборкам (для информационной строки).
var best string
for _, v := range ishAnyRe.FindAllString(string(body), -1) {
if best == "" || verLess(best, v) {
best = v
}
}
latest = best
return linux, latest
}
// verLess сравнивает версии покомпонентно как числа: verLess(a,b)=true если a<b.
func verLess(a, b string) bool {
pa, pb := strings.Split(a, "."), strings.Split(b, ".")
for i := 0; i < len(pa) || i < len(pb); i++ {
var x, y int
if i < len(pa) {
x = atoiSafe(pa[i])
}
if i < len(pb) {
y = atoiSafe(pb[i])
}
if x != y {
return x < y
}
}
return false
}
func atoiSafe(s string) int {
n := 0
for _, r := range s {
if r < '0' || r > '9' {
return n
}
n = n*10 + int(r-'0')
}
return n
}
// ishVersionStatus — версия ИШ как отдельная проверка для панели статуса.
func ishVersionStatus(ctx context.Context) Status {
info := ishVersionInfo(ctx)
s := Status{Name: "Версия Интеграционного шлюза", Optional: true}
if info.Installed == "" {
s.OK = true
s.Message = "ИШ не обнаружен на этом сервере"
return s
}
s.OK = info.UpToDate
if info.UpToDate {
s.Message = "установлена " + info.Installed + " — " + info.Note
} else {
s.Message = "установлена " + info.Installed + "; " + info.Note
}
if info.SiteLinux != "" {
s.Detail = "на сайте НРД (Linux): " + info.SiteLinux
if info.SiteLatest != "" && info.SiteLatest != info.SiteLinux {
s.Detail += ", последняя (Windows): " + info.SiteLatest
}
}
return s
}