de41aea00c
Полный клиент Интеграционного шлюза НРД в internal/nsdadapter/igw/:
client.go — REST endpoint'ы по свежей спецификации НРД:
- POST /api/package/{channel}/file — отправка ZIP (Type=archive, File=base64)
возвращает id пакета (поддерживаются варианты id|package_id|ID)
- GET /api/package/status/{id} — статус NEW|SENT|ERROR (с error-полем)
- GET /api/package?channel=&type=M2MTD|M2MER&date=&id=&count=&excludeErrors=
— список входящих от НРД, с files[] и signs[] (ИШ сам проверяет ЭП и
выдаёт VALID|INVALID)
- GET /api/package/{id} — скачать ZIP (raw или base64-в-JSON, авто-детект
по сигнатуре PK\x03\x04)
- Ретраи только на 5xx/сетевые ошибки (4xx — сразу ошибка)
- HTTP-клиент через options, кастомный таймаут, ретраи
pack.go — упаковщик/распаковщик ZIP по разделу 2.3 инструкции:
- PackRequest(req, docName) — M2MTransferRequest→ZIP с config.xml
- PackXML(xml, docName, packageType) — для эталонных сообщений
- UnpackPackage(zip) → {DocXML, WinfXML, Signature, Filenames}
- ParseDecision / ParseResponse через nsdxml.Unmarshal
Покрыто тестами (10/10 PASS):
- send happy path с проверкой формата JSON-body
- retry на 5xx, без ретраев на 4xx
- GetStatus с числовым id
- ListIncoming как массив (новый формат) и как {items:[]} (старый)
- GetPackage raw ZIP + GetPackage с base64-в-JSON
- упаковка/распаковка: 2 файла в ZIP, имена, содержимое config.xml
- распаковка с .sgn и winf.xml
cmd/bj-server/main.go — NSD-poller адаптирован под новый API
(client.ListIncoming(ctx, ListFilter{}) вместо позиционных параметров;
поля Package.ID/Name/Type/State вместо PackageID/PackageType).
Скачана и положена в DOC/ свежая спецификация (798 KB, 15 стр):
DOC/instr-ish-rest-api.pdf — это исходный документ для нашей реализации.
REPORT.md обновлён:
- общая готовность 65% → 70%
- готовность к роботу 80% → 85%
- добавлен раздел про REST-клиент ИШ
- блокер #6 — отсутствие «Руководства по установке ИШ»
155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
// Package main — единый сервис bj-server.
|
|
//
|
|
// Объединяет в одном процессе: lk-gateway (REST API ЛК + admin web UI),
|
|
// m2m-core (FSM сделки, репозиторий, эмиссия и потребление Decision),
|
|
// nsd-adapter (REST к ИШ НРД и опрос входящих, когда профиль настроен),
|
|
// notify (заглушка отправки уведомлений). lk-emulator живёт отдельным
|
|
// бинарником как QA-инструмент.
|
|
//
|
|
// Архитектура подсказана объёмом 100-1000 сделок/день: для такого
|
|
// потока избыточно держать 5 отдельных процессов и микросервисную
|
|
// шину. Один Go-бинарник проще деплоить, проще наблюдать и
|
|
// масштабировать вертикально, а компоненты внутри по-прежнему
|
|
// разделены пакетами internal/<...>.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
|
|
)
|
|
|
|
const serviceName = "bj-server"
|
|
|
|
func main() {
|
|
addr := getenv("BJ_HTTP_ADDR", ":8080")
|
|
defaultSender := m2m.DeponentCode(getenv("BJ_M2M_SENDER", "MC0079200000"))
|
|
defaultReceiver := m2m.DeponentCode(getenv("BJ_M2M_RECEIVER", "MC0010300000"))
|
|
setupPath := os.Getenv("BJ_SETUP_PATH")
|
|
|
|
cfg := lkgateway.ServerConfig{
|
|
Addr: addr,
|
|
DefaultSender: defaultSender,
|
|
DefaultReceiver: defaultReceiver,
|
|
SetupPath: setupPath,
|
|
CheckOptions: func() lkgateway.CheckOptions {
|
|
return lkgateway.CheckOptions{
|
|
PostgresDSN: os.Getenv("BJ_DSN"),
|
|
CryptoSocket: getenv("BJ_CRYPTO_SOCKET", "/run/bj/crypto.sock"),
|
|
NSDAdapterURL: os.Getenv("BJ_NSD_ADAPTER_URL"),
|
|
LKCallbackURL: os.Getenv("BJ_LK_CALLBACK_URL"),
|
|
Profile: getenv("BJ_NSD_PROFILE", "demo (mock NSD)"),
|
|
CryptoProvider: getenv("BJ_CRYPTO_PROVIDER", "stub"),
|
|
Timeout: 2 * time.Second,
|
|
}
|
|
},
|
|
}
|
|
|
|
srv, err := lkgateway.NewServer(cfg)
|
|
if err != nil {
|
|
log.Fatalf("%s: NewServer: %v", serviceName, err)
|
|
}
|
|
if cb := os.Getenv("BJ_LK_CALLBACK_URL"); cb != "" {
|
|
srv.SetCallbackURL(cb)
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
|
|
// Опционально — поллер входящих пакетов ИШ НРД. Запускается если
|
|
// BJ_NSD_PROFILE задан (после установки реального ИШ через UI этот
|
|
// блок будет тянуть Decisions из настоящего НРД и применять их через
|
|
// lkgateway.Service.ApplyDecision).
|
|
if profileName := os.Getenv("BJ_NSD_PROFILE"); profileName != "" {
|
|
go runNSDPoller(ctx, profileName)
|
|
}
|
|
|
|
// notify-демон: пока заглушка, в M3-M4 будет рассылать события
|
|
// (e-mail, Yandex Messenger, Telegram, WS-push в admin-ui).
|
|
go runNotifyWorker(ctx)
|
|
|
|
log.Printf("%s: запуск, HTTP %s", serviceName, addr)
|
|
runErr := srv.Run(ctx)
|
|
stop()
|
|
if runErr != nil {
|
|
log.Printf("%s: %v", serviceName, runErr)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// runNSDPoller — фоновый поллер входящих пакетов ИШ НРД.
|
|
func runNSDPoller(ctx context.Context, profileName string) {
|
|
profile, err := nsdadapter.LookupProfile(profileName)
|
|
if err != nil {
|
|
log.Printf("%s: NSD poller: %v (доступные профили: %v)", serviceName, err, nsdadapter.AvailableProfiles())
|
|
return
|
|
}
|
|
interval := 30 * time.Second
|
|
if v := os.Getenv("BJ_NSD_POLL_INTERVAL"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
interval = d
|
|
}
|
|
}
|
|
client := igw.NewClient(profile.IGWBaseURL, igw.WithRetry(profile.RetryMax, profile.RetryBackoff))
|
|
log.Printf("%s: NSD poller: профиль %s, канал %s, ИШ %s, интервал %s",
|
|
serviceName, profile.Name, profile.Channel, profile.IGWBaseURL, interval)
|
|
|
|
t := time.NewTicker(interval)
|
|
defer t.Stop()
|
|
since := time.Now().UTC().Add(-time.Hour)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
for _, kind := range nsdadapter.IncomingPackageKinds() {
|
|
pkgs, err := client.ListIncoming(ctx, igw.ListFilter{
|
|
Channel: profile.Channel,
|
|
Date: since,
|
|
Type: string(kind),
|
|
})
|
|
if err != nil {
|
|
log.Printf("%s: NSD poller ListIncoming(%s, %s): %v", serviceName, profile.Channel, kind, err)
|
|
continue
|
|
}
|
|
for _, p := range pkgs {
|
|
log.Printf("%s: NSD входящий пакет id=%d (%s) типа %s, канал %s, state %s",
|
|
serviceName, p.ID, p.Name, p.Type, p.Channel, p.State)
|
|
// TODO(M3): GetPackage(p.ID) → unpack ZIP → парсить XML →
|
|
// передавать в lkgateway.Service.ApplyDecision
|
|
}
|
|
}
|
|
since = time.Now().UTC()
|
|
}
|
|
}
|
|
}
|
|
|
|
// runNotifyWorker — заглушка демона уведомлений.
|
|
func runNotifyWorker(ctx context.Context) {
|
|
t := time.NewTicker(time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
// На M3-M4 здесь будет: вытащить очередь событий из БД,
|
|
// разослать по настроенным каналам (e-mail, мессенджер).
|
|
}
|
|
}
|
|
}
|
|
|
|
func getenv(k, def string) string {
|
|
if v, ok := os.LookupEnv(k); ok && v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|