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
+109 -35
View File
@@ -1,79 +1,153 @@
package lkgateway
import (
"strings"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license"
)
// DefaultLicensePublicKey — публичный ключ лицензий, зашитый в релиз.
// Пустой в исходниках; подставляется при официальной сборке. Если задан в
// настройках (LicenseSettings.PublicKey) — приоритет у настроек.
var DefaultLicensePublicKey = ""
// DefaultLicensePublicKey — открытый ключ проверки лицензий: пара к ключу подписи сервера
// лицензий lic.zetit.ru. Зашит в bj и в настройках не меняется, поэтому ключ, подписанный
// чем-то другим, не принимается. var — только для подмены в тестах.
var DefaultLicensePublicKey = "UuvEYFxxzOFk59FjCQTdxKuzkNAJpJyWTuUdlCYfsdI="
// LicenseStatus — сводка состояния лицензии для UI и гейтов.
type LicenseStatus struct {
Present bool // ключ введён
Valid bool // подпись верна и срок не истёк
Present bool // ключ введён
Valid bool // подпись верна, срок не истёк, ключ этой установки
Online bool // выдан сервером лицензий под эту установку и продлевается автоматически
LicenseID string
Tenant string
Plan string
ExpiresAt time.Time
ContractUntil time.Time
DaysLeft int
AllowsUpdates bool
Features []string // признаки (модули), разрешённые ключом
Message string
}
// licensingEnabled — включено ли лицензирование (есть публичный ключ для
// проверки). Если ключа нет вовсе — продукт в открытом режиме, гейты не
// действуют (удобно для разработки и бесплатной редакции).
func licensingEnabled(rc *RuntimeConfig) bool {
return rc.Snapshot().License.PublicKey != "" || DefaultLicensePublicKey != ""
// LicenseFeatureView — признак лицензии для отображения.
type LicenseFeatureView struct {
Key, Title string
On bool
}
// knownFeatures — известные признаки (модули) и их названия.
var knownFeatures = []struct{ Key, Title string }{
{"updates", "Обновления через артефакторию"},
{"web-cabinet", "Личный кабинет клиента (bj-lk)"},
{"quik-mnp", "QUIK: модуль неторговых поручений (цикл M2M)"},
{"quik-export", "QUIK: экспорт биржевой информации"},
{"npr", "Риск-монитор НПР / УДС"},
}
func licenseFeatureViews(rc *RuntimeConfig) []LicenseFeatureView {
out := make([]LicenseFeatureView, 0, len(knownFeatures))
for _, f := range knownFeatures {
out = append(out, LicenseFeatureView{Key: f.Key, Title: f.Title, On: moduleAllowed(rc, f.Key)})
}
st := licenseStatus(rc)
for _, f := range st.Features {
known := false
for _, k := range knownFeatures {
if k.Key == f {
known = true
}
}
if !known {
out = append(out, LicenseFeatureView{Key: f, Title: f, On: st.Valid})
}
}
return out
}
func modulesAllowed(rc *RuntimeConfig) map[string]bool {
m := map[string]bool{}
for _, f := range knownFeatures {
m[f.Key] = moduleAllowed(rc, f.Key)
}
return m
}
// moduleAllowed — разрешён ли модуль (лицензионный признак). План enterprise — все модули.
func moduleAllowed(rc *RuntimeConfig, feature string) bool {
st := licenseStatus(rc)
if !st.Valid {
return false
}
if st.Plan == string(license.PlanEnterprise) {
return true
}
for _, f := range st.Features {
if f == feature {
return true
}
}
return false
}
// licensingEnabled — лицензирование действует всегда: ключ проверки зашит в bj.
func licensingEnabled(_ *RuntimeConfig) bool { return DefaultLicensePublicKey != "" }
// verifyLicenseToken — подпись ключа зашитым открытым ключом (срок не проверяется).
func verifyLicenseToken(key string) (*license.License, error) {
pub, err := license.ParsePublicKey(DefaultLicensePublicKey)
if err != nil {
return nil, err
}
tok, err := license.DecodeToken(key)
if err != nil {
return nil, err
}
return license.Verify(tok, pub)
}
// licenseStatus разбирает и проверяет лицензию из настроек.
func licenseStatus(rc *RuntimeConfig) LicenseStatus {
s := rc.Snapshot().License
st := LicenseStatus{}
if s.Key == "" {
st.Message = "лицензионный ключ не введён"
if strings.TrimSpace(s.Key) == "" {
st.Message = "лицензия не активирована"
return st
}
st.Present = true
pubB64 := s.PublicKey
if pubB64 == "" {
pubB64 = DefaultLicensePublicKey
}
if pubB64 == "" {
st.Message = "нет публичного ключа для проверки лицензии"
return st
}
pub, err := license.ParsePublicKey(pubB64)
lic, err := verifyLicenseToken(s.Key)
if err != nil {
st.Message = "неверный публичный ключ: " + err.Error()
return st
}
tok, err := license.DecodeToken(s.Key)
if err != nil {
st.Message = "неверный формат ключа: " + err.Error()
return st
}
lic, err := license.Verify(tok, pub)
if err != nil {
st.Message = "подпись лицензии недействительна"
st.Message = "ключ недействителен: подпись не сходится или ключ повреждён"
return st
}
now := time.Now().UTC()
st.LicenseID = lic.ID
st.Tenant = lic.Tenant
st.Plan = string(lic.Plan)
st.ExpiresAt = lic.ExpiresAt
st.ContractUntil = lic.ContractUntil
st.DaysLeft = lic.DaysLeft(now)
st.AllowsUpdates = lic.AllowsUpdates()
st.Features = lic.Features
st.Online = lic.InstallID != ""
if lic.Product != "" && lic.Product != "bj-server" {
st.Message = "ключ выпущен для другого продукта (" + lic.Product + ")"
return st
}
if st.Online && !strings.EqualFold(lic.InstallID, s.InstallID) {
st.Message = "ключ выдан другой установке: активируйте эту установку кодом"
return st
}
if err := lic.Valid(now); err != nil {
st.Message = err.Error()
return st
}
st.Valid = true
st.Message = "активна до " + lic.ExpiresAt.Format("02.01.2006")
if st.Online {
st.Message = "активна до " + lic.ExpiresAt.Format("02.01.2006") + ", продлевается автоматически"
if !lic.ContractUntil.IsZero() {
st.Message += "; договор до " + lic.ContractUntil.Format("02.01.2006")
}
} else {
st.Message = "активна до " + lic.ExpiresAt.Format("02.01.2006") + " (офлайн-ключ)"
}
return st
}