QUIK МНП: bj ведёт справочник депозитариев-получателей M2M (M2MDepoRecipients) по справочнику НРД — раз в 30 мин до 20:00 и при изменении справочника; состояние на «Поручения QUIK»
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
+19
-18
@@ -47,24 +47,25 @@ var templateFuncs = template.FuncMap{
|
|||||||
return template.URL(defaultLogoDataURI(uri))
|
return template.URL(defaultLogoDataURI(uri))
|
||||||
},
|
},
|
||||||
// m2mCode — человекочитаемое название кода M2M: «Текст (M2Mxx)».
|
// m2mCode — человекочитаемое название кода M2M: «Текст (M2Mxx)».
|
||||||
"m2mCode": m2mCodeLabel,
|
"m2mCode": m2mCodeLabel,
|
||||||
"quikCodeMap": quikClientCodeMapText,
|
"quikCodeMap": quikClientCodeMapText,
|
||||||
"curUser": authTemplateFuncs["curUser"],
|
"curUser": authTemplateFuncs["curUser"],
|
||||||
"can": authTemplateFuncs["can"],
|
"can": authTemplateFuncs["can"],
|
||||||
"sendFailText": sendFailText,
|
"sendFailText": sendFailText,
|
||||||
"realTest": realTestGet,
|
"realTest": realTestGet,
|
||||||
"realNSDAs": realNSDAsText,
|
"realNSDAs": realNSDAsText,
|
||||||
"nsdSegTip": nsdSegTip,
|
"quikRecipientsInfo": quikRecipientsInfo,
|
||||||
"devOn": authTemplateFuncs["devOn"],
|
"nsdSegTip": nsdSegTip,
|
||||||
"devLicensed": authTemplateFuncs["devLicensed"],
|
"devOn": authTemplateFuncs["devOn"],
|
||||||
"mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии
|
"devLicensed": authTemplateFuncs["devLicensed"],
|
||||||
"isIIS": incomingIsIIS, // входящий перевод на ИИС
|
"mod": func(f string) bool { return globalRC == nil || moduleAllowed(globalRC, f) }, // модуль в лицензии
|
||||||
"depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера
|
"isIIS": incomingIsIIS, // входящий перевод на ИИС
|
||||||
"authOpen": authOpen,
|
"depoSubs": func() any { return depoWizardSubs }, // экраны шага 3 мастера
|
||||||
"internalDocs": func() bool { return internalDocsBuilt },
|
"authOpen": authOpen,
|
||||||
"roleTitle": roleTitle,
|
"internalDocs": func() bool { return internalDocsBuilt },
|
||||||
"stageCls": func(r depoQueueRow) string { c, _ := r.StageRu(); return c },
|
"roleTitle": roleTitle,
|
||||||
"stageText": func(r depoQueueRow) string { _, t := r.StageRu(); return t },
|
"stageCls": func(r depoQueueRow) string { c, _ := r.StageRu(); return c },
|
||||||
|
"stageText": func(r depoQueueRow) string { _, t := r.StageRu(); return t },
|
||||||
"deref": func(v *float64) float64 {
|
"deref": func(v *float64) float64 {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//go:build live
|
||||||
|
|
||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Только чтение: остатки SBER по счёту клиента в DEPO во всех разрезах. ACC=ВТНКЦ00002000
|
||||||
|
func TestDepoRestsLive(t *testing.T) {
|
||||||
|
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fs := rc.Snapshot().Fancy
|
||||||
|
db, err := fancyOpen(fs, fs.DepoDB())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
rows, err := db.QueryContext(context.Background(), `SELECT c.NUMBER, ISNULL(p.PORT_NUM,''), v.ISIN, CAST(rst.K AS float), CAST(ISNULL(rst.A,0) AS float), ISNULL(rst.BLOCK_REASON,''), ISNULL(rst.BAL_ACC,0)
|
||||||
|
FROM OD_RESTS rst JOIN OD_CLIENTS c ON c.ID=rst.REG_1 LEFT JOIN OD_CLIENTS_PORTS p ON p.CLIENT=c.ID
|
||||||
|
JOIN OD_VALUES v ON v.ID=rst.VALUE_ID
|
||||||
|
WHERE c.NUMBER IN ('ВТНКЦ00002000','ВТНКЦ00000816') AND v.ISIN='RU0009029540'`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var num, port, isin, br string
|
||||||
|
var k, a float64
|
||||||
|
var bal int
|
||||||
|
_ = rows.Scan(&num, &port, &isin, &k, &a, &br, &bal)
|
||||||
|
fmt.Printf("%s %s %s K=%v A=%v блок=%q бал.счёт=%d\n", num, port, isin, k, a, br, bal)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -263,6 +263,7 @@ func (s *Server) applyIncoming(ctx context.Context, p igw.Package) error {
|
|||||||
log.Printf("depo-events: справочник после получения: %v", err)
|
log.Printf("depo-events: справочник после получения: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
quikRecipientsSyncAsync(s.rc) // депозитарии-получатели в МНП (quikrecipients.go)
|
||||||
log.Printf("lk-gateway: поллер СПРАВОЧНИК M2M получен id=%d: участников %d, мест расчётов %d (состав изменился)",
|
log.Printf("lk-gateway: поллер СПРАВОЧНИК M2M получен id=%d: участников %d, мест расчётов %d (состав изменился)",
|
||||||
p.ID, len(hb.Participants.Participants), len(hb.SettlementPlaces.Places))
|
p.ID, len(hb.Participants.Participants), len(hb.SettlementPlaces.Places))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
// Справочник «Депозитарии получателей для переводов M2M» в БД МНП (M2MDepoRecipients) ведёт bj — по
|
||||||
|
// справочнику участников M2M от НРД (решение 29.09.2026). QUIK этот справочник сам не получает, а в
|
||||||
|
// поручении «Вывод ЦБ (M2M)» депозитарий получателя обязателен и сверяется с ним: без записи МНП
|
||||||
|
// отвергает регистрацию («Неизвестный депозитарий получателя»), и бумаги клиента не блокируются.
|
||||||
|
// Модуль МНП перечитывает справочник при запуске (ночной перезапуск), поэтому справочник обновляется
|
||||||
|
// в течение дня до 20:00 — к ночи в базе МНП полный актуальный список: раз в 30 минут и сразу при
|
||||||
|
// изменении справочника НРД. Добавляются недостающие депозитарии и исправляются наименования;
|
||||||
|
// записи, которых нет в справочнике НРД, не удаляются.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
quikRecipientsEvery = 30 * time.Minute
|
||||||
|
quikRecipientsLastHour = 20 // после 20:00 не обновляем: модуль уже не перечитает до ночного перезапуска
|
||||||
|
)
|
||||||
|
|
||||||
|
var quikRecipientsState struct {
|
||||||
|
sync.Mutex
|
||||||
|
At time.Time
|
||||||
|
Total int
|
||||||
|
Added int
|
||||||
|
Renamed int
|
||||||
|
Err string
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikRecipientsSync — привести M2MDepoRecipients в соответствие справочнику НРД.
|
||||||
|
func quikRecipientsSync(ctx context.Context, rc *RuntimeConfig) (total, added, renamed int, err error) {
|
||||||
|
s := rc.Snapshot().Quik
|
||||||
|
if strings.TrimSpace(s.Server) == "" {
|
||||||
|
return 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
raw, _ := readHandbook()
|
||||||
|
var hb struct {
|
||||||
|
Participants []HandbookEntry `json:"participants"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &hb); err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("справочник НРД: %w", err)
|
||||||
|
}
|
||||||
|
want := map[string]string{}
|
||||||
|
for _, p := range hb.Participants {
|
||||||
|
inn := strings.TrimSpace(p.INN)
|
||||||
|
if inn == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(p.Name)
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(p.DisplayName)
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(p.FullName)
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = "Депозитарий ИНН " + inn
|
||||||
|
}
|
||||||
|
if r := []rune(name); len(r) > 128 {
|
||||||
|
name = string(r[:128])
|
||||||
|
}
|
||||||
|
if _, ok := want[inn]; !ok {
|
||||||
|
want[inn] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(want) == 0 {
|
||||||
|
return 0, 0, 0, fmt.Errorf("справочник НРД пуст")
|
||||||
|
}
|
||||||
|
conn, err := quikOpen(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
have := map[string]string{}
|
||||||
|
rows, err := conn.QueryContext(cctx, "SELECT INN, ISNULL(Name,'') FROM M2MDepoRecipients")
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("чтение M2MDepoRecipients: %s", quikErrorDetail(err))
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var inn, name string
|
||||||
|
if rows.Scan(&inn, &name) == nil {
|
||||||
|
have[strings.TrimSpace(inn)] = strings.TrimSpace(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
for inn, name := range want {
|
||||||
|
cur, ok := have[inn]
|
||||||
|
if ok && cur == name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := conn.ExecContext(cctx, "exec dbo.imp_M2MDepoRecipients @Operation=1, @INN=@inn, @Name=@nm", sql.Named("inn", inn), sql.Named("nm", name)); err != nil {
|
||||||
|
return len(want), added, renamed, fmt.Errorf("imp_M2MDepoRecipients %s: %s", inn, quikErrorDetail(err))
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
renamed++
|
||||||
|
} else {
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(want), added, renamed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikRecipientsRun — один проход с учётом окна (до 20:00) и записью состояния.
|
||||||
|
func quikRecipientsRun(ctx context.Context, rc *RuntimeConfig, why string) {
|
||||||
|
if rc == nil || !moduleAllowed(rc, "quik-mnp") || time.Now().Hour() >= quikRecipientsLastHour {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
quikRecipientsState.Lock()
|
||||||
|
if quikRecipientsState.running {
|
||||||
|
quikRecipientsState.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
quikRecipientsState.running = true
|
||||||
|
quikRecipientsState.Unlock()
|
||||||
|
total, added, renamed, err := quikRecipientsSync(ctx, rc)
|
||||||
|
quikRecipientsState.Lock()
|
||||||
|
quikRecipientsState.running = false
|
||||||
|
quikRecipientsState.At, quikRecipientsState.Total, quikRecipientsState.Added, quikRecipientsState.Renamed = time.Now(), total, added, renamed
|
||||||
|
prevErr := quikRecipientsState.Err
|
||||||
|
quikRecipientsState.Err = ""
|
||||||
|
if err != nil {
|
||||||
|
quikRecipientsState.Err = err.Error()
|
||||||
|
}
|
||||||
|
quikRecipientsState.Unlock()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
log.Printf("quik: справочник получателей M2M (%s): %v", why, err)
|
||||||
|
if prevErr == "" {
|
||||||
|
raiseSystemAlert(rc, "QUIK МНП: справочник депозитариев-получателей не обновлён",
|
||||||
|
"bj не смог обновить справочник «Депозитарии получателей для переводов M2M» в базе МНП: "+err.Error()+". Без записи о депозитарии получателя МНП отвергает поручения на вывод, и перевод не уйдёт в НРД.")
|
||||||
|
}
|
||||||
|
case added > 0 || renamed > 0:
|
||||||
|
log.Printf("quik: справочник получателей M2M обновлён (%s): добавлено %d, наименований исправлено %d, всего по справочнику НРД %d — модуль МНП применит после перезапуска", why, added, renamed, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikRecipientsSyncAsync — справочник НРД изменился: обновить и в МНП.
|
||||||
|
func quikRecipientsSyncAsync(rc *RuntimeConfig) {
|
||||||
|
go quikRecipientsRun(context.Background(), rc, "изменился справочник НРД")
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikRecipientsLoop — раз в 30 минут до 20:00.
|
||||||
|
func (s *Server) quikRecipientsLoop(ctx context.Context) {
|
||||||
|
t := time.NewTimer(time.Minute)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
quikRecipientsRun(ctx, s.rc, "плановое")
|
||||||
|
t.Reset(quikRecipientsEvery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quikRecipientsInfo — строка о справочнике получателей для страницы «Поручения QUIK».
|
||||||
|
func quikRecipientsInfo() string {
|
||||||
|
quikRecipientsState.Lock()
|
||||||
|
defer quikRecipientsState.Unlock()
|
||||||
|
st := struct {
|
||||||
|
At time.Time
|
||||||
|
Total, Added, Renamed int
|
||||||
|
Err string
|
||||||
|
}{quikRecipientsState.At, quikRecipientsState.Total, quikRecipientsState.Added, quikRecipientsState.Renamed, quikRecipientsState.Err}
|
||||||
|
switch {
|
||||||
|
case st.At.IsZero():
|
||||||
|
return "Справочник депозитариев-получателей M2M в МНП: ещё не сверялся после запуска bj (сверка раз в 30 мин до 20:00)."
|
||||||
|
case st.Err != "":
|
||||||
|
return "Справочник депозитариев-получателей M2M в МНП: ошибка обновления " + st.At.Format("02.01 15:04") + " — " + st.Err
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("Справочник депозитариев-получателей M2M в МНП сверен со справочником НРД %s: %d депозитариев", st.At.Format("02.01 15:04"), st.Total)
|
||||||
|
if st.Added > 0 || st.Renamed > 0 {
|
||||||
|
s += fmt.Sprintf(" (добавлено %d, исправлено наименований %d — модуль МНП применит после ночного перезапуска)", st.Added, st.Renamed)
|
||||||
|
}
|
||||||
|
return s + ". Обновляется раз в 30 мин до 20:00."
|
||||||
|
}
|
||||||
@@ -331,6 +331,7 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
healthRegister("poll-quik", 30*time.Second)
|
healthRegister("poll-quik", 30*time.Second)
|
||||||
go superviseLoop(ctx, "poll-quik", s.rc, s.quikCycleLoop)
|
go superviseLoop(ctx, "poll-quik", s.rc, s.quikCycleLoop)
|
||||||
|
go superviseLoop(ctx, "quik-recipients", s.rc, s.quikRecipientsLoop) // справочник получателей M2M в МНП до 20:00
|
||||||
|
|
||||||
// Контроль сроков депо-поручений 16/16-1: MS010 за 5 мин, исполнение до 30 дней.
|
// Контроль сроков депо-поручений 16/16-1: MS010 за 5 мин, исполнение до 30 дней.
|
||||||
healthRegister("order-watch", time.Minute)
|
healthRegister("order-watch", time.Minute)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
{{if .Flash}}<div style="padding:12px 16px;background:var(--ok-weak);border-radius:8px;margin-bottom:16px">{{.Flash}}</div>{{end}}
|
{{if .Flash}}<div style="padding:12px 16px;background:var(--ok-weak);border-radius:8px;margin-bottom:16px">{{.Flash}}</div>{{end}}
|
||||||
|
<p class="muted" style="font-size:12.5px;margin:0 0 10px">{{quikRecipientsInfo}}</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<style>
|
<style>
|
||||||
.qk-steps{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
|
.qk-steps{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user