refactor(cmd): объединить Go-сервисы в один бинарник bj-server
Под выбранный объём 100-1000 сделок/день микросервисная архитектура с 5 отдельными процессами избыточна. Объединяем lk-gateway, m2m-core, nsd-adapter, notify в один Go-бинарник bj-server. lk-emulator остаётся отдельным бинарником как QA-инструмент (имитация ЛК ESIA Finance). cmd/bj-server/main.go: - Поднимает lkgateway.NewServer (HTTP :8080, REST API + admin UI) - Фоновый poller NSD ИШ (если задан BJ_NSD_PROFILE) - Заглушка notify worker (M3-M4: e-mail / Yandex Messenger / WS-push) - Graceful shutdown через signal.NotifyContext cmd/lk-gateway/, cmd/m2m-core/, cmd/nsd-adapter/, cmd/notify/ — удалены. deploy/systemd/: - bj-server.service — systemd unit для основного сервиса (один файл, простой деплой) - bj-emulator.service — systemd unit для эмулятора - README.md с инструкцией по установке (useradd bj, /opt/bj, daemon-reload) Makefile: - build теперь собирает только bj-server и lk-emulator - бывшие пять бинарей удалены Размер: - bj-server: 19.5 МБ (включает lk-gateway + m2m-core + nsd-adapter + notify) - lk-emulator: 12.7 МБ - общий размер артефактов уменьшился c ~50 МБ до 32 МБ Внутренние пакеты internal/<...> не изменились — разделение сохраняется на уровне Go-пакетов, что облегчает возврат к микросервисам если объём вырастет до 1000+ сделок/день. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,8 @@ help:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
$(GO) build -o bin/lk-gateway ./cmd/lk-gateway
|
$(GO) build -o bin/bj-server ./cmd/bj-server
|
||||||
$(GO) build -o bin/m2m-core ./cmd/m2m-core
|
|
||||||
$(GO) build -o bin/nsd-adapter ./cmd/nsd-adapter
|
|
||||||
$(GO) build -o bin/lk-emulator ./cmd/lk-emulator
|
$(GO) build -o bin/lk-emulator ./cmd/lk-emulator
|
||||||
$(GO) build -o bin/notify ./cmd/notify
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test ./... -race -count=1
|
$(GO) test ./... -race -count=1
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// 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, profile.Channel, since, 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 входящий пакет %s типа %s (канал %s, получен %s)",
|
||||||
|
serviceName, p.PackageID, p.PackageType, p.Channel, p.ReceivedAt.Format(time.RFC3339))
|
||||||
|
// TODO(M3): парсить тело пакета, передавать в 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
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Package main — сервис lk-gateway. BFF слой ЛК клиента:
|
|
||||||
// принимает REST-заявки по контракту ESIA Finance, валидирует,
|
|
||||||
// собирает M2MTransferRequest, отправляет в НРД через nsd-adapter,
|
|
||||||
// эмитит callback статуса обратно в ЛК.
|
|
||||||
//
|
|
||||||
// На M2 — in-memory репозиторий + mock NSDSender (имитация принимающей
|
|
||||||
// стороны через 3 секунды). На M3 переключим на pgx + реальный
|
|
||||||
// nsd-adapter без изменения контракта.
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
addr := getenv("BJ_HTTP_ADDR", ":8080")
|
|
||||||
defaultSender := m2m.DeponentCode(getenv("BJ_M2M_SENDER", "MC0079200000"))
|
|
||||||
defaultReceiver := m2m.DeponentCode(getenv("BJ_M2M_RECEIVER", "MC0010300000"))
|
|
||||||
|
|
||||||
cfg := lkgateway.ServerConfig{
|
|
||||||
Addr: addr,
|
|
||||||
DefaultSender: defaultSender,
|
|
||||||
DefaultReceiver: defaultReceiver,
|
|
||||||
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("lk-gateway: NewServer: %v", err)
|
|
||||||
}
|
|
||||||
if cb := os.Getenv("BJ_LK_CALLBACK_URL"); cb != "" {
|
|
||||||
srv.SetCallbackURL(cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
||||||
runErr := srv.Run(ctx)
|
|
||||||
stop()
|
|
||||||
if runErr != nil {
|
|
||||||
log.Printf("lk-gateway: %v", runErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getenv(k, def string) string {
|
|
||||||
if v, ok := os.LookupEnv(k); ok && v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Package main — сервис m2m-core. Бизнес-логика и FSM сделки M2M-перевода:
|
|
||||||
// идемпотентность по GUID, валидация по XSD, метрики SLA, ветка ручного
|
|
||||||
// согласования и таймаут-отказа MOST.
|
|
||||||
//
|
|
||||||
// На этапе M1 — заглушка.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const serviceName = "m2m-core"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Fprintf(os.Stdout, "%s: запуск (заглушка M1)\n", serviceName)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// Package main — сервис notify. Отправка уведомлений по нескольким каналам:
|
|
||||||
// e-mail (SMTP), Yandex Messenger (Yandex 360), WebSocket-push в admin-ui,
|
|
||||||
// плюс расширяемая модель провайдеров-плагинов (smtp, yandex360, telegram,
|
|
||||||
// mattermost, webhook) под единый интерфейс Notifier — для тиражирования
|
|
||||||
// продукта другим компаниям.
|
|
||||||
//
|
|
||||||
// На этапе M1 — заглушка.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const serviceName = "notify"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Fprintf(os.Stdout, "%s: запуск (заглушка M1)\n", serviceName)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Package main — сервис nsd-adapter. Транспорт к НРД:
|
|
||||||
// - Интеграционный шлюз через REST API (основной канал, ИШ сам подписывает);
|
|
||||||
// - Web-сервис ONYX напрямую (резерв);
|
|
||||||
// - Файловый шлюз / обменные папки ИШ (fallback).
|
|
||||||
//
|
|
||||||
// Сериализация и парсинг XML по схемам M2MSchemas в windows-1251,
|
|
||||||
// маршрутизация по типам пакетов (#M2MTR / #M2MTD / #M2MER / SUBBR / SUBER /
|
|
||||||
// SUB16 / Справки / квитанции ЭДО).
|
|
||||||
//
|
|
||||||
// На этапе M1 — заглушка.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const serviceName = "nsd-adapter"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Fprintf(os.Stdout, "%s: запуск (заглушка M1)\n", serviceName)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# deploy/systemd — юниты для деплоя
|
||||||
|
|
||||||
|
Минимальный production-деплой Bridge-and-Join-s — два бинарника + два
|
||||||
|
systemd-юнита.
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
- `bj-server.service` — основной сервис: lk-gateway BFF + admin UI +
|
||||||
|
m2m-core FSM + nsd-adapter поллер + notify. HTTP `:8080`.
|
||||||
|
- `bj-emulator.service` — имитация ЛК (QA-инструмент). HTTP `:8083`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd --system --no-create-home --shell /usr/sbin/nologin bj
|
||||||
|
sudo mkdir -p /opt/bj /var/lib/bj /var/log/bj /run/bj
|
||||||
|
sudo chown bj:bj /var/lib/bj /var/log/bj /run/bj
|
||||||
|
|
||||||
|
# собрать бинарники на dev-ВМ и положить в /opt/bj/
|
||||||
|
sudo cp bin/bj-server bin/lk-emulator /opt/bj/
|
||||||
|
|
||||||
|
# юниты
|
||||||
|
sudo cp deploy/systemd/bj-server.service deploy/systemd/bj-emulator.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now bj-server bj-emulator
|
||||||
|
|
||||||
|
# проверка
|
||||||
|
systemctl status bj-server bj-emulator
|
||||||
|
journalctl -u bj-server -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Веб-интерфейс: `http://<host>:8080/admin/setup` — настройка PostgreSQL,
|
||||||
|
КриптоПро CSP, ИШ НРД, callback ЛК.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Bridge-and-Join-s — эмулятор ЛК ESIA Finance (QA)
|
||||||
|
Documentation=https://git.zetit.ru/zuevav/Bridge-and-Join-s
|
||||||
|
After=network-online.target bj-server.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=bj
|
||||||
|
Group=bj
|
||||||
|
WorkingDirectory=/opt/bj
|
||||||
|
ExecStart=/opt/bj/lk-emulator
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
Environment=BJ_HTTP_ADDR=:8083
|
||||||
|
Environment=BJ_GATEWAY_URL=http://127.0.0.1:8080
|
||||||
|
Environment=BJ_EMULATOR_PUBLIC_URL=http://127.0.0.1:8083
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
|
||||||
|
LimitNOFILE=65536
|
||||||
|
TasksMax=128
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Bridge-and-Join-s — единый сервис M2M-переводов
|
||||||
|
Documentation=https://git.zetit.ru/zuevav/Bridge-and-Join-s
|
||||||
|
After=network-online.target postgresql.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=bj
|
||||||
|
Group=bj
|
||||||
|
WorkingDirectory=/opt/bj
|
||||||
|
ExecStart=/opt/bj/bj-server
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Конфигурация — через ENV или ~/.bj/setup.json (UI /admin/setup).
|
||||||
|
Environment=BJ_HTTP_ADDR=:8080
|
||||||
|
Environment=BJ_SETUP_PATH=/var/lib/bj/setup.json
|
||||||
|
Environment=BJ_M2M_SENDER=MC0079200000
|
||||||
|
Environment=BJ_M2M_RECEIVER=MC0010300000
|
||||||
|
|
||||||
|
# Безопасность.
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/var/lib/bj /var/log/bj /run/bj
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
LockPersonality=true
|
||||||
|
|
||||||
|
# Лимиты.
|
||||||
|
LimitNOFILE=65536
|
||||||
|
TasksMax=512
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user