Мастер, учёт DEPO: скрипт таблиц не выходит за карточку; на «Подключении» — как создать логин bj (права, скрипт MS SQL/PostgreSQL с базой и логином из полей)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Только чтение: что сейчас в BJ_DEPO_DATA — таблицы обмена и тестовые остатки.
|
||||
func TestDepoNowLive(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()
|
||||
ctx := context.Background()
|
||||
for _, tb := range []string{"FTV_M2M", "FTV_M2M_LINES", "FTV_M2M_EVENTS", "FTV_M2M_HANDBOOK"} {
|
||||
var n int
|
||||
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dbo."+tb).Scan(&n)
|
||||
fmt.Printf("%-18s строк %d %v\n", tb, n, err)
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, `SELECT c.NUMBER, v.ISIN, CAST(rst.K AS float) FROM OD_RESTS rst JOIN OD_CLIENTS c ON c.ID=rst.REG_1 JOIN OD_VALUES v ON v.ID=rst.VALUE_ID
|
||||
WHERE c.NUMBER IN ('ВТНКЦ00002000','ВТНКЦ00000816') AND v.ISIN='RU0009029540'`)
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var a, i string
|
||||
var k float64
|
||||
_ = rows.Scan(&a, &i, &k)
|
||||
fmt.Println("остаток SBER:", a, k)
|
||||
}
|
||||
}
|
||||
var mx string
|
||||
_ = db.QueryRowContext(ctx, "SELECT ISNULL(CONVERT(varchar(19),MAX(D_DATE),120),'') FROM OD_DOCS").Scan(&mx)
|
||||
fmt.Println("последний документ OD_DOCS:", mx)
|
||||
var jobs int
|
||||
_ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM sys.objects WHERE name LIKE '%M2M%' AND type IN ('P','TR','U','V')").Scan(&jobs)
|
||||
fmt.Println("объектов с M2M в имени (таблицы/процедуры/триггеры):", jobs)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Только чтение: базы BJ_DEPO*, режим восстановления, резервные копии и восстановления (msdb).
|
||||
func TestDBHistoryLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := rc.Snapshot().Fancy
|
||||
fmt.Println("база DEPO в настройках bj:", fs.DepoDB())
|
||||
db, err := fancyOpen(fs, "master")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
ctx := context.Background()
|
||||
dump := func(title, q string) {
|
||||
fmt.Println("==", title)
|
||||
rows, err := db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
fmt.Println(" ошибка:", err)
|
||||
return
|
||||
}
|
||||
cols, _ := rows.Columns()
|
||||
n := 0
|
||||
for rows.Next() {
|
||||
vals := make([]sql.NullString, len(cols))
|
||||
ptrs := make([]any, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
_ = rows.Scan(ptrs...)
|
||||
line := ""
|
||||
for i, c := range cols {
|
||||
line += c + "=" + vals[i].String + " | "
|
||||
}
|
||||
fmt.Println(" ", line)
|
||||
n++
|
||||
}
|
||||
if n == 0 {
|
||||
fmt.Println(" (пусто)")
|
||||
}
|
||||
}
|
||||
dump("базы", `SELECT name, recovery_model_desc, state_desc, CONVERT(varchar(19),create_date,120) created FROM sys.databases WHERE name LIKE 'BJ%' OR name LIKE '%DEPO%' ORDER BY name`)
|
||||
dump("резервные копии (последние 30)", `SELECT TOP 30 b.database_name, b.type, CONVERT(varchar(19),b.backup_start_date,120) start, CONVERT(varchar(19),b.backup_finish_date,120) finish, m.physical_device_name
|
||||
FROM msdb.dbo.backupset b JOIN msdb.dbo.backupmediafamily m ON m.media_set_id=b.media_set_id
|
||||
WHERE b.database_name LIKE 'BJ%' ORDER BY b.backup_finish_date DESC`)
|
||||
dump("восстановления (последние 20)", `SELECT TOP 20 r.destination_database_name, CONVERT(varchar(19),r.restore_date,120) restored, r.restore_type, r.user_name, b.database_name src_db, CONVERT(varchar(19),b.backup_finish_date,120) src_backup, m.physical_device_name
|
||||
FROM msdb.dbo.restorehistory r JOIN msdb.dbo.backupset b ON b.backup_set_id=r.backup_set_id JOIN msdb.dbo.backupmediafamily m ON m.media_set_id=b.media_set_id
|
||||
WHERE r.destination_database_name LIKE 'BJ%' ORDER BY r.restore_date DESC`)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package lkgateway
|
||||
|
||||
// Логин bj в базе DEPO (05.10.2026): на шаге «Подключение» мастера — что нужно и скрипт для администратора
|
||||
// СУБД. Скрипт создаёт логин (SQL Server) / роль (PostgreSQL) и даёт чтение учётных таблиц DEPO; права на
|
||||
// таблицы обмена FTV_M2M* выдаёт скрипт таблиц на следующем шаге (или bj, если таблицы создаёт сам).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// depoLoginScript — скрипт создания логина bj. pg — PostgreSQL; db — база DEPO; login — имя логина.
|
||||
func depoLoginScript(pg bool, db, login string) string {
|
||||
if !validDBName(db) {
|
||||
db = depoDefaultDatabase
|
||||
}
|
||||
if !depoLoginRe.MatchString(login) {
|
||||
login = "bj_user"
|
||||
}
|
||||
var b strings.Builder
|
||||
if pg {
|
||||
fmt.Fprintf(&b, `-- Bridge&Join: роль (логин) для bj в базе DEPO. PostgreSQL 10 и новее.
|
||||
-- Выполнить суперпользователем или владельцем базы, например:
|
||||
-- psql -h <сервер> -U postgres -f bj-depo-login-postgres.sql
|
||||
-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»).
|
||||
-- Права: только подключение и чтение учётных таблиц DEPO. Запись — только в таблицы обмена FTV_M2M*,
|
||||
-- её выдаёт скрипт таблиц на следующем шаге мастера.
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[2]s') THEN
|
||||
CREATE ROLE "%[2]s" LOGIN PASSWORD 'ЗАМЕНИТЕ_НА_ПАРОЛЬ';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s";
|
||||
|
||||
\c "%[1]s"
|
||||
GRANT USAGE ON SCHEMA public TO "%[2]s";
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "%[2]s";
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "%[2]s";
|
||||
`, db, login)
|
||||
return b.String()
|
||||
}
|
||||
fmt.Fprintf(&b, `-- Bridge&Join: логин для bj в базе DEPO. Microsoft SQL Server.
|
||||
-- Выполнить в SSMS или sqlcmd под логином с правами sysadmin (или securityadmin + db_owner в базе %[1]s).
|
||||
-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»).
|
||||
-- Права: только чтение учётных таблиц DEPO (db_datareader). Запись — только в таблицы обмена FTV_M2M*,
|
||||
-- её выдаёт скрипт таблиц на следующем шаге мастера. Скрипт можно запускать повторно.
|
||||
|
||||
USE [master];
|
||||
GO
|
||||
|
||||
IF SUSER_ID(N'%[2]s') IS NULL
|
||||
CREATE LOGIN [%[2]s] WITH PASSWORD = N'ЗАМЕНИТЕ_НА_ПАРОЛЬ', CHECK_POLICY = ON, DEFAULT_DATABASE = [%[1]s];
|
||||
GO
|
||||
|
||||
USE [%[1]s];
|
||||
GO
|
||||
|
||||
IF USER_ID(N'%[2]s') IS NULL CREATE USER [%[2]s] FOR LOGIN [%[2]s];
|
||||
GO
|
||||
|
||||
IF IS_ROLEMEMBER(N'db_datareader', N'%[2]s') = 0 ALTER ROLE db_datareader ADD MEMBER [%[2]s];
|
||||
GO
|
||||
`, db, login)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// fancyDepoLoginScript — GET /admin/setup/fancy/depo-login/script.sql?type=mssql|postgres&db=…&login=…
|
||||
// Без параметров — по сохранённым настройкам; download=0 — показать текстом (предпросмотр в мастере).
|
||||
func (h *setupHandlers) fancyDepoLoginScript(w http.ResponseWriter, r *http.Request) {
|
||||
s := h.rc.Snapshot().Fancy
|
||||
q := r.URL.Query()
|
||||
pg := s.IsPostgres()
|
||||
if t := q.Get("type"); t != "" {
|
||||
pg = t == "postgres"
|
||||
}
|
||||
db := strings.TrimSpace(q.Get("db"))
|
||||
if db == "" {
|
||||
db = s.DepoDB()
|
||||
}
|
||||
login := strings.TrimSpace(q.Get("login"))
|
||||
if login == "" {
|
||||
login = pickStr(strings.TrimSpace(s.User), "bj_user")
|
||||
}
|
||||
body := depoLoginScript(pg, db, login)
|
||||
name := "bj-depo-login-mssql.sql"
|
||||
if pg {
|
||||
name = "bj-depo-login-postgres.sql"
|
||||
} else {
|
||||
body = "\uFEFF" + body // SSMS без BOM читает кириллицу в комментариях как ANSI
|
||||
}
|
||||
if q.Get("download") == "0" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte(strings.TrimPrefix(body, "\uFEFF")))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDepoLoginScript(t *testing.T) {
|
||||
ms := depoLoginScript(false, "CLIENT_DEPO", "bj_svc")
|
||||
for _, w := range []string{"CREATE LOGIN [bj_svc]", "USE [CLIENT_DEPO]", "CREATE USER [bj_svc] FOR LOGIN [bj_svc]", "db_datareader"} {
|
||||
if !strings.Contains(ms, w) {
|
||||
t.Fatalf("MS SQL: нет %q", w)
|
||||
}
|
||||
}
|
||||
pg := depoLoginScript(true, "depo", "bj_svc")
|
||||
for _, w := range []string{`CREATE ROLE "bj_svc" LOGIN`, `GRANT CONNECT ON DATABASE "depo"`, `GRANT SELECT ON ALL TABLES`} {
|
||||
if !strings.Contains(pg, w) {
|
||||
t.Fatalf("PostgreSQL: нет %q", w)
|
||||
}
|
||||
}
|
||||
if s := depoLoginScript(false, "bad;db", "x'; DROP"); !strings.Contains(s, "[BJ_DEPO_DATA]") || !strings.Contains(s, "[bj_user]") {
|
||||
t.Fatal("недопустимые имена — подставляются значения по умолчанию")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/admin/setup/fancy/depo-tables/create", h.createFancyDepoTables)
|
||||
mux.HandleFunc("/admin/setup/fancy/depo-tables/check", h.checkFancyDepoTables)
|
||||
mux.HandleFunc("/admin/setup/fancy/depo-tables/script.sql", h.fancyDepoTablesScript)
|
||||
mux.HandleFunc("/admin/setup/fancy/depo-login/script.sql", h.fancyDepoLoginScript)
|
||||
mux.HandleFunc("/admin/fancy-queue", h.fancyQueue)
|
||||
mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
.where { font-size:12px; color:var(--accent); margin-left:8px; }
|
||||
.help-block { background:var(--accent-weak); background:var(--accent-weak); padding:10px 14px; margin:10px 0; font-size:13px; border-radius:0 8px 8px 0; }
|
||||
.help-block strong { color:var(--accent); }
|
||||
.wiz-card { max-width:680px; margin:0 auto; }
|
||||
.wiz-card { max-width:680px; margin:0 auto; min-width:0; }
|
||||
.wiz-card * { min-width:0; }
|
||||
</style>
|
||||
|
||||
<div class="wiz-head">
|
||||
@@ -110,6 +111,37 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<div><label>База DEPO</label><input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="{{.Settings.Fancy.DepoDB}}" pattern="[A-Za-z][A-Za-z0-9_]*" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
|
||||
<div><label>Логин</label><input type="text" name="user" value="{{.Settings.Fancy.User}}" autocomplete="off" required style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
|
||||
<div><label>Пароль {{if .Settings.Fancy.Server}}<span class="muted">(пусто — оставить прежний)</span>{{end}}</label><input type="password" name="password" autocomplete="new-password" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"></div>
|
||||
<details id="wiz-login-help" style="border:1px solid var(--border);border-radius:8px;padding:8px 12px;background:var(--card-2)">
|
||||
<summary style="cursor:pointer;font-weight:600">Логина для bj ещё нет? Как создать</summary>
|
||||
<div style="font-size:13px;line-height:1.5;margin-top:6px">
|
||||
<p style="margin:0 0 6px">bj нужен <b>отдельный логин</b> в базе DEPO. Его создаёт администратор СУБД. Права минимальные:</p>
|
||||
<ul style="margin:0 0 6px;padding-left:18px">
|
||||
<li><b>чтение</b> учётных таблиц DEPO (лица, счета, остатки, бумаги) — для сверки переводов;</li>
|
||||
<li><b>запись только в таблицы обмена</b> <code>FTV_M2M*</code> — их права выдаются на следующем экране, вместе с созданием таблиц.</li>
|
||||
</ul>
|
||||
<p style="margin:0 0 6px">Вариант 1 — администратор создаёт логин сам с этими правами. Вариант 2 — передайте ему скрипт: в нём уже подставлены тип СУБД, база и имя логина из полей выше, нужно только заменить пароль.</p>
|
||||
<a id="wiz-login-dl" href="/admin/setup/fancy/depo-login/script.sql">Скачать скрипт создания логина</a>
|
||||
<pre class="copyable" id="wiz-login-pre" style="max-height:260px;margin-top:6px;font-size:12px">…</pre>
|
||||
<p class="muted" style="margin:4px 0 0">После выполнения скрипта впишите логин и пароль в поля выше и нажмите «Проверить и далее».</p>
|
||||
</div>
|
||||
</details>
|
||||
<script>
|
||||
(function(){
|
||||
var f=document.currentScript.closest('form'), d=document.getElementById('wiz-login-help'), pre=document.getElementById('wiz-login-pre'), a=document.getElementById('wiz-login-dl');
|
||||
function q(){
|
||||
var t=f.querySelector('[name=db_type]'), db=f.querySelector('[name=depo_database]'), u=f.querySelector('[name=user]');
|
||||
return 'type='+encodeURIComponent(t?t.value:'mssql')+'&db='+encodeURIComponent(db&&db.value.trim()?db.value.trim():(db?db.placeholder:''))+'&login='+encodeURIComponent(u&&u.value.trim()?u.value.trim():'bj_user');
|
||||
}
|
||||
function upd(){
|
||||
a.href='/admin/setup/fancy/depo-login/script.sql?'+q();
|
||||
if(!d.open) return;
|
||||
fetch('/admin/setup/fancy/depo-login/script.sql?download=0&'+q(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(t){pre.textContent=t}).catch(function(){});
|
||||
}
|
||||
d.addEventListener('toggle',upd);
|
||||
['db_type','depo_database','user'].forEach(function(n){var e=f.querySelector('[name='+n+']'); if(e) e.addEventListener('change',upd);});
|
||||
upd();
|
||||
})();
|
||||
</script>
|
||||
<input type="hidden" name="encrypt" value="{{if .Settings.Fancy.Encrypt}}{{.Settings.Fancy.Encrypt}}{{else}}disable{{end}}">
|
||||
<div style="display:flex;justify-content:space-between;margin-top:6px"><a href="/admin/wizard?step=3&sub=intro" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← Назад</a><button type="submit" class="btn">Проверить и далее →</button></div>
|
||||
</form>
|
||||
|
||||
@@ -302,6 +302,9 @@ label { font-size:13px; font-weight:500; }
|
||||
.callout strong:first-child { color:var(--text); }
|
||||
/* Код-блоки справки — тёмная тема учтена */
|
||||
.doc-body pre, pre.copyable, pre.code { background:var(--card-2) !important; border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); }
|
||||
/* скрипты и код в карточках: не шире блока — длинные строки переносятся, а не уезжают за край */
|
||||
pre.copyable { max-width:100%; box-sizing:border-box; white-space:pre-wrap; overflow-wrap:anywhere; overflow:auto; }
|
||||
details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
||||
@media (max-width:900px){ .doc { grid-template-columns:1fr; gap:0; } .doc-toc { position:static; max-height:none; margin-bottom:18px; border-bottom:1px solid var(--border); padding-bottom:12px; } }
|
||||
|
||||
/* ---- Toast ---- */
|
||||
|
||||
Reference in New Issue
Block a user