3 Архитектура
valalav edited this page 2026-11-09 21:05:22 +03:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Архитектура

Страница для тех, кто дорабатывает код или разбирается в отказе.

┌─────────────────┐     HTTP POST       ┌─────────────────────────┐
│  Яндекс Формы   │ ─────────────────→  │  Google Apps Script     │
│  Интеграция→API │   JSON + secret     │  doPost() → Sheets      │
└─────────────────┘                     │  doGet()  → LiveWidget  │
                                        │  getLiveStats() → JSON  │
                                        └──────────┬──────────────┘
                                                   │
                                        ┌──────────▼──────────────┐
                                        │  Google Sheets          │
                                        │  Ответы 1 | Ответы      │
                                        │  Кодирование | Итоги    │
                                        └──────────┬──────────────┘
                                                   │ google.script.run, 15 с
                                        ┌──────────▼──────────────┐
                                        │  LiveWidget.html        │
                                        │  2 опроса, распределение│
                                        └─────────────────────────┘

Одна точка входа — URL развёртывания веб-приложения.

Метод Назначение
doPost Приём ответа: проверка секрета, дедупликация по ID, запись строки
doGet Отдача экрана LiveWidget.html
getLiveStats Агрегация для экрана, вызывается из HTML
setupTemplate Ручной запуск: привязывает ID копии таблицы (BOUND_SHEET_ID) и создаёт четыре листа с формулами и диаграммой
bindActiveSpreadsheet / onOpen Сохранение ID активной копии таблицы в ScriptProperties (BOUND_SHEET_ID) для доступа из веб-приложения (openById)
clearAllTestResponses Ручной запуск: очищает строки ответов

Настройки в Code.gs

Константа По умолчанию Комментарий
SHEET_ID '' (пусто) При пустом значении openBook_() сохраняет ID активной книги в ScriptProperties (BOUND_SHEET_ID) при запуске setupTemplate/onOpen в редакторе, а в опубликованном web app (/exec), где getActiveSpreadsheet() возвращает null, открывает привязанную копию через openById(BOUND_SHEET_ID). Запись в общий шаблон 1DCAwa... заблокирована
DEFAULT_SECRET ks-live-2026 Ключ приёма POST; переопределяется свойством скрипта WEBHOOK_SECRET
REGION_HEADER Коммуникационная сессия · Наименование региона Подпись над заголовком экрана
PAGE_TITLE КС · Опросы в реальном времени Заголовок вкладки браузера
DEFAULT_CODING_RULES 7 строк Применяются, только если в таблице нет листа «Кодирование»

Контракт POST-запроса

{
  "secret": "ks-live-2026",
  "survey": "1",
  "response_id": "abc123",
  "inflation": "8-10%",
  "submitted_at": "2026-11-19 14:30:00",
  "name": "Иванов И.И.",
  "category": "Бизнес",
  "other_category": "",
  "sector": "Промышленность"
}

Поля читаются и из тела, и из параметров URL (?survey=1&secret=…); тело приоритетнее, пустое поле дополняется параметром. В пользовательских инструкциях принят единый 5-польный шаблон JSON; технически поддерживается также сокращённый 2-польный шаблон тела {"response_id": "", "inflation": ""} при передаче ?survey=1&secret=ks-live-2026 в строке URL.

Обязательные: secret, response_id, inflation (выбранный вариант; имя поля фиксировано, смысл — любой). Необязательные: survey ("1"/"2"), submitted_at, name, category, other_category, sector.

Разбор тела запроса

parseTolerantBody_ пробует JSON.parse, затем разбор с удалением переводов строк, после чего переходит к терпимому разбору: ключи ищутся по списку KNOWN_PAYLOAD_KEYS, допускаются неэкранированные кавычки внутри значения (ООО "Ромашка"), числовой response_id без кавычек, вариант ответа без кавычек и формат key=value построчно.

Ошибки не проглатываются: при невалидных данных doPost завершается с исключением, и код ошибки виден в истории интеграции.

Нормализация ответа

normalizeRange приводит строку к сравнимому виду: тире всех видов → -; «процентов/процента/процент» → %; пробелы удаляются, регистр понижается; «менее» → «ниже»; «более/свыше» → «выше»; до4% → 4%иниже; от20% → 20%ивыше; ниже4% → 4%иниже; выше20% → 20%ивыше. Сравнение идёт со строкой столбца A листа «Кодирование», пропущенной через ту же нормализацию. Прочие формулировки (например от 8 до 10%) не распознаются и попадают в excluded.

Запись в таблицу

Колонка Содержимое
A ID ответа
B Время отправки
C–F Имя/организация, категория, уточнение, вид деятельности
G Исходный текст варианта
H Число из столбца B листа «Кодирование»
I Способ кодирования
J Время получения, Europe/Moscow

Тексты проходят через cellText: длина до 3000 символов, ведущие =, +, @, - экранируются апострофом — защита от формульной инъекции.

Дедупликация: перед записью колонка A ищется точным совпадением; при находке ответ — Already saved, строка не добавляется. Блокировка: критическая секция выполняется под LockService с ожиданием до 20 секунд, блокировка снимается в finally.

Ответ getLiveStats()

{
  "surveys": [
    { "count": 42, "mean": 9.8, "counts": [2, 5, 12, 10, 8, 3, 2], "excluded": 0 },
    { "count": 38, "mean": 8.1, "counts": [4, 8, 14, 7, 3, 1, 1], "excluded": 0 }
  ],
  "labels": ["4% и ниже", "5-7%", "…", "20% и выше"],
  "regionHeader": "Коммуникационная сессия · …",
  "pageTitle": "Опросы в реальном времени",
  "checkedAt": "2026-11-19T11:30:00.000Z"
}

LiveWidget.html использует regionHeader, pageTitle, surveys и labels: при каждом вызове render(data) подписи семи строк распределения (#label0…#label6) обновляются из data.labels (прочитанных из Кодирование!A2:A8) через textContent. Массив const labels в HTML используется только для начального предпросмотра до первого ответа сервера.

Безопасность

  • Проверка secret — единственная защита от посторонних POST-запросов.
  • Экран доступен всем, у кого есть ссылка (публикация «Все»).
  • Реальный рабочий ключ в репозиторий не коммитится: в примерах и на скриншотах учебный ks-live-2026.
  • Персональные данные попадают в таблицу, только если форма их собирает.

Тесты

node tests/test_code_gs.mjs

Проверка поднимает Code.gs в изолированном окружении с фиктивными объектами Sheets и прогоняет: корректный ответ, невалидный JSON, неэкранированные кавычки, числовой response_id, формат key=value, неверный survey, дедупликацию, нормализацию тире и синонимов, экранирование формульных инъекций, идемпотентность setupTemplate, работу clearAllTestResponses.

Полная версия страницы — docs/TECHNICAL.md в репозитории.