Кабинет: страницы собирает ядро, на хостинге — ретранслятор (Go и PHP)

- modules/lk: конверт запроса/ответа (PageRequest/PageResponse), ServeEnvelope — тот же
  обработчик страниц внутри ядра; клиент к API внутри процесса (Config.Local/LocalKey);
  режим ретранслятора (Config.Relay, BJ_RENDER=core, выбор в установщике и lk.json)
- ядро: встроенный кабинет (lkcabinet.go), POST /api/lk/render для прямого режима,
  цикл режима ожидания выполняет конверты страниц; API через ретранслятор недостижимо
- PHP-ретранслятор для обычного хостинга (modules/lk/relay-php: index.php, .htaccess,
  README): установщик в браузере, прямой режим через curl, режим ожидания через очередь
  в SQLite (BEGIN IMMEDIATE, busy_timeout), конфиг в bj-lk-data рядом с сайтом или в
  data под .htaccess; архив со страницы настроек (/admin/setup/lk/relay-php.zip)
- настройки: карточка НПР/УДС перенесена из раздела «Лицензия» в «Экспорт QUIK»;
  кнопка «Обновить с сервера лицензий» видна при любом ключе
- инструкция /admin/help/lk: два варианта размещения, шаги для PHP-хостинга
- тесты: TestLKRelayRendersInCore (оба режима), TestLKPHPRelayAgentMode (настоящий PHP
  против цикла ядра, PHP_BIN), проверка безопасности через PHP-ретранслятор 34/34

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 11:15:29 +03:00
parent f51414269d
commit 054222d154
19 changed files with 1403 additions and 97 deletions
@@ -5,10 +5,21 @@
<h2>Как разместить кабинет клиента на хостинге</h2>
<p class="muted">Речь о нашем модуле <code>bj-lk</code> — кабинете, который клиенты открывают в браузере. Он ставится отдельно от ядра: на веб-сервер депозитария или на обычный хостинг с выделенным именем. Своей базы у кабинета нет, данные клиентов он не хранит — берёт у ядра на время запроса.</p>
<h3>Два варианта размещения</h3>
<table>
<tr><th>Где</th><th>Что скачать</th><th>Как работает</th></tr>
<tr><td>Обычный хостинг с PHP («папка сайта»)</td><td>«Скачать для PHP-хостинга» — <code>index.php</code> и <code>.htaccess</code></td><td>Страницы собирает ядро. На хостинге лежит ретранслятор: он передаёт запросы браузера ядру и отдаёт ответы. Ни базы, ни данных клиентов на хостинге нет. Нужны PHP 8.1+, curl, sqlite.</td></tr>
<tr><td>Свой сервер или VPS с Linux</td><td>«Скачать для своего сервера» — программа <code>bj-lk</code></td><td>Установщик спросит, где собирать страницы: в ядре (программа — такой же ретранслятор) или на месте (программа рисует страницы сама из данных ядра).</td></tr>
</table>
<p>В обоих случаях ключа в архиве нет — его вводят в установщике, открыв адрес кабинета в браузере. Способы связи с ядром (кабинет → ядро или ядро → кабинет) одинаковы для всех вариантов.</p>
<h3>Шаг 1. Ключ и архив</h3>
<p>Настройки → «Личный кабинет» → «Кабинет клиента на своём сервере»: создайте ключ доступа (он показан на странице целиком — его нужно ввести в установщике) и скачайте архив. В архиве — исполняемый файл, служба systemd, файл настроек службы, установщик и README. Ключа в архиве нет.</p>
<h3>Шаг 2. Файлы на сервер и запуск</h3>
<h3>Шаг 2а. PHP-хостинг: файлы в каталог сайта</h3>
<p>Создайте в панели хостинга домен (например, <code>lk.депозитарий.ru</code>) с сертификатом, скопируйте <code>index.php</code> и <code>.htaccess</code> в его каталог и откройте адрес в браузере — запустится установщик (шаг 3). Настройки ретранслятор положит в каталог <code>bj-lk-data</code> рядом с сайтом, если хостинг разрешает туда писать, иначе — в <code>data</code> внутри сайта, закрытый правилами <code>.htaccess</code>. Если у хостинга nginx без <code>.htaccess</code> — правила из README архива.</p>
<h3>Шаг 2б. Свой сервер: файлы на сервер и запуск</h3>
<pre>tar xzf bj-lk-ГГГГ-ММ-ДД.tar.gz
cd bj-lk
sudo ./install.sh # на своём сервере: заводит службу bj-lk
@@ -382,6 +382,20 @@
{{/* ============ ЭКСПОРТ БИРЖЕВОЙ ИНФОРМАЦИИ (QUIK) ============ */}}
<section class="settings-section" id="sec-quikexport">
{{.QuikExport.HTML}}
{{/* модуль НПР/УДС работает поверх базы экспорта QUIK — его настройки здесь же */}}
<div class="card" id="sec-npr">
<h2><span class="dot {{if .Settings.NPR.Enabled}}ok{{else}}warn{{end}}"></span>Модуль «НПР / УДС» (риск-монитор по данным QUIK)</h2>
<p class="muted">Показывает НПР1 / НПР2 / УДС клиентов маржинальной торговли по базе экспорта QUIK (схема <code>npr</code>). Отдельно лицензируемый модуль (признак <code>npr</code>); при включении в меню появляется пункт «НПР / УДС». Подключение — логин только для чтения.</p>
{{template "verline" (.Versions.Get "bj-npr")}}
<form method="post" action="/admin/setup/npr" style="margin-top:10px;display:grid;gap:10px;max-width:640px">
<input type="hidden" name="section" value="sec-npr">
<label style="display:flex;gap:8px;align-items:center;font-weight:600"><input type="checkbox" name="enabled" value="1" {{if .Settings.NPR.Enabled}}checked{{end}}> Включить модуль</label>
<label>Строка подключения к базе экспорта QUIK <input type="text" name="dsn" value="{{.Settings.NPR.DSN}}" placeholder="postgres://quik_ro:пароль@localhost:5432/quik_export" style="width:100%;font-family:ui-monospace,monospace;font-size:12px"></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button><button type="submit" formaction="/admin/setup/npr/test" class="btn">Проверить подключение</button></div>
</form>
</div>
<h1>Лицензия</h1>
</section>
{{/* ============ КРИПТОГРАФИЯ ============ */}}
@@ -642,7 +656,8 @@ function hbBind(inp,box,list,onPick){
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="submit" class="btn btn-ok">Сохранить</button>
{{if .Settings.LK.APIKey}}<a class="btn" href="/admin/setup/lk/dist.tar.gz">Скачать дистрибутив кабинета</a>{{end}}
{{if .Settings.LK.APIKey}}<a class="btn" href="/admin/setup/lk/dist.tar.gz">Скачать для своего сервера (Linux)</a>
<a class="btn" href="/admin/setup/lk/relay-php.zip">Скачать для PHP-хостинга</a>{{end}}
<a class="btn btn-secondary" href="/admin/help/lk">Инструкция по размещению</a>
</div>
</form>
@@ -658,7 +673,8 @@ function hbBind(inp,box,list,onPick){
</div>
{{end}}
<p class="muted" style="font-size:12px;margin-top:10px">В архиве: исполняемый файл, служба systemd (отдельный пользователь, ограничения доступа к файловой системе), файл настроек службы, установщик и README. Ключа в архиве нет — его спрашивает веб-установщик. Кабинет входит в модуль «Личный кабинет клиента» и обновляется отдельно от ядра.</p>
<p class="muted" style="font-size:12px;margin-top:10px"><b>Где собираются страницы.</b> Для PHP-хостинга — всегда в ядре: на хостинге лежит только ретранслятор (index.php), который передаёт запросы браузера ядру. Для своего сервера установщик спросит: собирать страницы в ядре (тогда программа на сервере — тоже ретранслятор, новые разделы появляются обновлением ядра) или на месте. Ключа ни в одном архиве нет.</p>
<p class="muted" style="font-size:12px;margin-top:6px">В архиве для своего сервера: исполняемый файл, служба systemd (отдельный пользователь, ограничения доступа к файловой системе), файл настроек службы, установщик и README. Ключа в архиве нет — его спрашивает веб-установщик. Кабинет входит в модуль «Личный кабинет клиента» и обновляется отдельно от ядра.</p>
</div>
<div class="card" id="sec-lk-users">
@@ -1040,18 +1056,6 @@ function hbBind(inp,box,list,onPick){
{{/* ============ ЛИЦЕНЗИЯ ============ */}}
<section class="settings-section" id="sec-license">
<div class="card" id="sec-npr">
<h2><span class="dot {{if .Settings.NPR.Enabled}}ok{{else}}warn{{end}}"></span>Модуль «НПР / УДС» (риск-монитор по данным QUIK)</h2>
<p class="muted">Показывает НПР1 / НПР2 / УДС клиентов маржинальной торговли по базе экспорта QUIK (схема <code>npr</code>). Отдельно лицензируемый модуль (признак <code>npr</code>); при включении в меню появляется пункт «НПР / УДС». Подключение — логин только для чтения.</p>
{{template "verline" (.Versions.Get "bj-npr")}}
<form method="post" action="/admin/setup/npr" style="margin-top:10px;display:grid;gap:10px;max-width:640px">
<input type="hidden" name="section" value="sec-npr">
<label style="display:flex;gap:8px;align-items:center;font-weight:600"><input type="checkbox" name="enabled" value="1" {{if .Settings.NPR.Enabled}}checked{{end}}> Включить модуль</label>
<label>Строка подключения к базе экспорта QUIK <input type="text" name="dsn" value="{{.Settings.NPR.DSN}}" placeholder="postgres://quik_ro:пароль@localhost:5432/quik_export" style="width:100%;font-family:ui-monospace,monospace;font-size:12px"></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Сохранить</button><button type="submit" formaction="/admin/setup/npr/test" class="btn">Проверить подключение</button></div>
</form>
</div>
<h1>Лицензия</h1>
<div class="card">
<h2>
{{if .License.Valid}}<span class="dot ok"></span>Активна
@@ -1081,7 +1085,7 @@ function hbBind(inp,box,list,onPick){
<p class="muted">Введите код активации от поставщика (вида <code>BJ-XXXX-XXXX-XXXX</code>). bj зарегистрирует эту установку на сервере лицензий <code>lic.zetit.ru</code> и дальше сам продлевает ключ раз в сутки. Серверу bj нужен доступ по HTTPS к lic.zetit.ru.</p>
<form method="post" action="/admin/setup/license/activate" style="margin-top:12px;display:grid;gap:10px;max-width:520px">
<label>Код активации <input type="text" name="code" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" spellcheck="false" style="width:100%;font-family:ui-monospace,monospace;letter-spacing:.04em"></label>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Активировать</button>{{if .License.Online}}<button type="submit" formaction="/admin/setup/license/renew" class="btn">Продлить сейчас</button>{{end}}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn btn-ok">Активировать</button>{{if .License.Present}}<button type="submit" formaction="/admin/setup/license/renew" class="btn" title="Запросить у сервера лицензий свежий ключ: срок, модули, режим разработки">Обновить с сервера лицензий</button>{{end}}</div>
</form>
<p class="muted" style="font-size:12px;margin-top:12px">Установка <code>{{if .Settings.License.InstallID}}{{.Settings.License.InstallID}}{{else}}будет создана при первом обращении{{end}}</code>{{if not .Settings.License.LastSync.IsZero}} · последнее продление {{.Settings.License.LastSync.Format "02.01.2006 15:04"}}{{end}}</p>
{{if .Settings.License.LastMsg}}<p style="font-size:13px;margin-top:6px;{{if .Settings.License.LastOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.License.LastOK}}✓{{else}}✗{{end}} {{.Settings.License.LastMsg}} <span class="muted">({{.Settings.License.LastAttempt.Format "02.01.2006 15:04"}})</span></p>{{end}}