package lkgateway // Предупреждения лицензии. // // 1. Заранее об окончании: за 30, 14, 7 и 1 день до окончания договора (или ключа, если договора // нет или сервер лицензий перестал продлевать ключ). Каждый порог — одно уведомление; если // договор продлили (дата сменилась), отсчёт начинается заново. // 2. При закрытых обновлениях bj — о смене версии внешней системы (МНП QUIK, ИШ, DEPO, …). // // Решение 16.09.2026: после окончания лицензии фоновые процессы (цикл QUIK МНП, личный кабинет, // обмен с НРД и DEPO) не останавливаются. Последствие одно — bj перестаёт обновляться, и новые // версии внешних систем со временем могут перестать с ним корректно работать; об этом и уведомляем. import ( "encoding/json" "fmt" "log" "math" "os" "path/filepath" "strings" "sync" "time" ) // licenseWatchFile — что уже отправлено и последние известные версии внешних систем. var licenseWatchFile = "/var/lib/bj/.bj/license-watch.json" type licenseWatch struct { EndDate string `json:"end_date,omitempty"` // дата окончания, по которой шли предупреждения WarnedDays int `json:"warned_days,omitempty"` // последний отправленный порог (дней) Versions map[string]string `json:"versions,omitempty"` // ключ системы → последняя известная версия } var ( licenseWatchMu sync.Mutex licenseWarnSteps = []int{30, 14, 7, 1} ) // externalSystems — внешние системы, с которыми работает bj (свои компоненты bj обновляются вместе с ним). var externalSystems = map[string]bool{ "quik-mnp": true, "ish": true, "depo": true, "quikexport": true, "validata": true, "java": true, "postgres": true, "os": true, } func loadLicenseWatch() licenseWatch { var w licenseWatch if b, err := os.ReadFile(licenseWatchFile); err == nil { _ = json.Unmarshal(b, &w) } if w.Versions == nil { w.Versions = map[string]string{} } return w } func saveLicenseWatch(w licenseWatch) { b, _ := json.MarshalIndent(w, "", " ") tmp := licenseWatchFile + ".tmp" if err := os.MkdirAll(filepath.Dir(licenseWatchFile), 0o700); err == nil { if err = os.WriteFile(tmp, b, 0o600); err == nil { err = os.Rename(tmp, licenseWatchFile) } if err != nil { log.Printf("lk-gateway: лицензия: запись %s: %v", licenseWatchFile, err) } } } // ---- 1. заранее об окончании ---- // licenseEnd — когда лицензия перестанет действовать и что именно заканчивается. func licenseEnd(st LicenseStatus, lastSync, now time.Time) (end time.Time, what string) { if !st.Valid { return time.Time{}, "" // уже не действует — об этом говорит статус и отказ в продлении } if !st.ContractUntil.IsZero() { end, what = st.ContractUntil, "договор" } renewStuck := st.Online && now.Sub(lastSync) > 48*time.Hour if (!st.Online || renewStuck) && !st.ExpiresAt.IsZero() && (end.IsZero() || st.ExpiresAt.Before(end)) { end, what = st.ExpiresAt, "ключ" if renewStuck { what = "ключ (сервер лицензий не продлевает его)" } } return end, what } // expiryStep — какой порог отправить (0 — ничего): наименьший порог, в который уже попали, если он // ещё не отправлен для этой даты окончания. func expiryStep(daysLeft int, w licenseWatch, endDate string) int { warned := w.WarnedDays if w.EndDate != endDate { warned = 0 } step := 0 for _, s := range licenseWarnSteps { if daysLeft <= s { step = s } } if step == 0 || (warned != 0 && step >= warned) { return 0 } return step } func ruDays(n int) string { switch { case n%10 == 1 && n%100 != 11: return fmt.Sprintf("%d день", n) case n%10 >= 2 && n%10 <= 4 && (n%100 < 12 || n%100 > 14): return fmt.Sprintf("%d дня", n) } return fmt.Sprintf("%d дней", n) } // licenseExpiryWarn — вызывается раз в час из licenseLoop. func licenseExpiryWarn(rc *RuntimeConfig, now time.Time) { st := licenseStatus(rc) end, what := licenseEnd(st, rc.Snapshot().License.LastSync, now) if end.IsZero() { return } endDate := end.Local().Format("2006-01-02") daysLeft := int(math.Ceil(end.Sub(now).Hours() / 24)) if daysLeft < 0 { daysLeft = 0 } licenseWatchMu.Lock() defer licenseWatchMu.Unlock() w := loadLicenseWatch() step := expiryStep(daysLeft, w, endDate) dateChanged := w.EndDate != endDate if dateChanged { w.EndDate, w.WarnedDays = endDate, 0 // договор продлили или сменился ключ — отсчёт заново } if step != 0 { w.WarnedDays = step } if dateChanged || step != 0 { saveLicenseWatch(w) } if step == 0 { return } left := "осталось " + ruDays(daysLeft) if daysLeft == 0 { left = "сегодня последний день" } raiseSystemAlert(rc, "Лицензия: "+strings.SplitN(what, " ", 2)[0]+" заканчивается через "+ruDays(daysLeft), fmt.Sprintf("Лицензия bj (%s) действует до %s — %s. После этого переводы M2M, обмен с НРД и DEPO, цикл QUIK МНП и личный кабинет продолжат работать, "+ "но отключатся модули по лицензии и обновления bj: новые версии МНП, ИШ и других систем со временем могут перестать корректно работать с bj. "+ "Для продления обратитесь к поставщику bj.", what, end.Local().Format("02.01.2006"), left)) } // ---- 2. смена версии внешней системы при закрытых обновлениях ---- // versionChanges — сверка с последними известными версиями (запоминает новые); список изменений // возвращается только когда обновления bj недоступны. func versionChanges(w *licenseWatch, items []SystemVersion, updatesOK bool) []string { var changed []string for _, it := range items { if !externalSystems[it.Key] || it.Version == "" { continue } if prev := w.Versions[it.Key]; prev != "" && prev != it.Version && !updatesOK { changed = append(changed, fmt.Sprintf("%s: %s → %s", it.Title, prev, it.Version)) } w.Versions[it.Key] = it.Version } return changed } // licenseVersionWatch — вызывается после каждого опроса версий систем. func licenseVersionWatch(rc *RuntimeConfig, v SystemVersions) { st := licenseStatus(rc) updatesOK := !licensingEnabled(rc) || (st.Valid && st.AllowsUpdates) licenseWatchMu.Lock() w := loadLicenseWatch() before, _ := json.Marshal(w.Versions) changed := versionChanges(&w, v.Items, updatesOK) if after, _ := json.Marshal(w.Versions); string(after) != string(before) { saveLicenseWatch(w) } licenseWatchMu.Unlock() if len(changed) == 0 { return } reason := "лицензия: " + st.Message if st.Valid { reason = "обновления не входят в лицензию" } raiseSystemAlert(rc, "Версия внешней системы изменилась, а обновления bj недоступны", fmt.Sprintf("Изменилось: %s. Обновления bj сейчас недоступны (%s). bj продолжает работать, но совместимость с новой версией не проверена и не гарантируется — "+ "при сбоях обмена причина может быть в этом. Совместимое обновление bj придёт после продления лицензии.", strings.Join(changed, "; "), reason)) }