Свод 2: серверная постраничная загрузка (поиск, три фильтра, total, варианты фильтров) и компактный ответ; фронт догружает строки окном страниц; замеры памяти, времени до первых строк, байтов и шага скролла. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 KiB
Purpose
Контракт API свода 2 (/api/v1/svod/2/...): постраничная выдача строк листа GO или RF с общим числом строк, серверные поиск и фильтры, компактное представление строки для экрана, варианты колоночных фильтров и совместимость прежнего ответа для выгрузки, тестов и стенда замеров.
ADDED Requirements
Requirement: Прежний ответ без новых параметров
Запрос GET /api/v1/svod/2/{sheet}/{year}, в котором нет ни одного из параметров view, search, smeta_type, smeta_direction, SHALL возвращать ответ, байт-в-байт совпадающий с ответом до изменения на тех же данных и с теми же org_ids, offset, limit, sort_by, sort_direction. Совпадать должны набор и порядок строк, sort_order, содержимое data со всеми ключами, включая null, и верхний уровень success/message/result/count. Поля total в таком ответе SHALL NOT быть. Excel-выгрузка свода 2 SHALL выдавать тот же файл, что до изменения.
Scenario: Полный ответ «все организации» не изменился
- WHEN на одних и тех же данных стенда выполняется
GET /api/v1/svod/2/GO/{year}иGET /api/v1/svod/2/RF/{year}без параметров до и после изменения - THEN тела ответов после распаковки совпадают побайтно (одинаковый sha256)
Scenario: Прежняя пагинация без total
- WHEN выполняется
GET /api/v1/svod/2/GO/{year}?offset=0&limit=2 - THEN ответ содержит
countне больше 2,resultне длиннее 2 строк и не содержит ключаtotal
Scenario: Невалидные прежние параметры
- WHEN передан
offset=-1,limit=0,sort_by=badилиsort_direction=bad - THEN ответ — 422, как до изменения
Requirement: Постраничная выдача с общим числом строк
Если в запросе есть хотя бы один из параметров view, search, smeta_type, smeta_direction (это «новый режим»), ответ SHALL содержать поле total. total — число строк, удовлетворяющих всем условиям запроса (год, организации, поиск, фильтры), без учёта offset и limit. count SHALL по-прежнему равняться длине result. Строки SHALL идти в том же порядке, что в полном ответе с теми же условиями: sort_order — номер строки в этом порядке, начиная с 1. Страницы с последовательными offset при limit = L SHALL в объединении давать ровно полный список без пропусков и повторов, если данные между запросами не менялись. Если offset не меньше total, result SHALL быть пустым, а total — верным.
Scenario: Склейка страниц равна полному списку
- WHEN при неизменных данных запрашиваются страницы
offset = 0, L, 2L, …сlimit = Lиview=full, пока страница не станет корочеL - THEN последовательность
data.line_idпо всем страницам совпадает с последовательностью в ответе безoffset/limitс теми же условиями, аtotalна каждой странице равен длине этого полного списка
Scenario: Страница за концом списка
- WHEN запрашивается
offset = totalв новом режиме - THEN
resultпустой,count= 0,totalравен числу строк по условиям запроса
Scenario: Явный view=full включает total
- WHEN выполняется
GET /api/v1/svod/2/RF/{year}?view=full&offset=0&limit=50 - THEN строки в
dataсовпадают с прежним полным представлением тех же строк, а в ответе естьtotal
Requirement: Серверный поиск
Параметр search SHALL отбирать строки, в которых строка поиска (без пробелов по краям) входит как подстрока, без учёта регистра (и для кириллицы), хотя бы в одно значение полей строки:
- все поля
data.header; - все поля
data.contract_summary; - все поля
data.contract_detail.
Числа сравниваются по их текстовой записи, отсутствующие значения не совпадают ни с чем. Пустой после обрезки search SHALL NOT сужать выборку. search длиннее 200 символов SHALL отклоняться с 422. Поиск SHALL сочетаться с остальными условиями по «И».
Scenario: Поиск без учёта регистра по кириллице
- WHEN в данных есть строка с
header.name = «Аренда помещения»и выполняется запрос сsearch=аРЕНДА - THEN строка есть в выдаче, а
totalравен числу строк, у которых хотя бы одно поле из перечисленных групп содержит «аренда» без учёта регистра
Scenario: Поиск по договору
- WHEN у строки
contract_detail.counterparty = «ООО Ромашка»иsearch=ромаш - THEN строка есть в выдаче
Scenario: Пустой поиск не сужает
- WHEN передан
search=%20%20(два пробела) в новом режиме - THEN
totalравенtotalтого же запроса безsearch
Scenario: Слишком длинный поиск
- WHEN длина
searchбольше 200 символов - THEN ответ — 422
Requirement: Фильтры по виду сметы, направлению и организации
smeta_type SHALL принимать одно из значений АХР, КВ, Операц и оставлять строки с точно таким data.header.smeta_type. smeta_direction SHALL принимать Поддержка или Развитие и оставлять строки с точно таким data.header.smeta_direction. Другие значения SHALL отклоняться с 422. Фильтр по организации задаётся параметром org_ids (список id) и SHALL пересекаться с организациями, разрешёнными пользователю, как и раньше. Все условия сочетаются по «И».
Scenario: Фильтр по виду сметы
- WHEN запрос с
smeta_type=КВ - THEN у всех строк выдачи
data.header.smeta_type = «КВ», аtotalравен числу таких строк в полном ответе с теми же организациями
Scenario: Комбинация условий
- WHEN запрос с
search=X,smeta_direction=Развитиеиorg_ids=<id> - THEN выдача — пересечение трёх условий,
totalравен числу строк полного ответа дляorg_ids=<id>, которые удовлетворяют поиску и направлению
Scenario: Недопустимое значение фильтра
- WHEN передан
smeta_type=AHRилиsmeta_direction=Support - THEN ответ — 422
Requirement: Компактное представление строки
При view=compact поле data каждой строки SHALL содержать только поля ниже, со значениями, равными значениям тех же путей в полном представлении:
| группа | поля |
|---|---|
| верхний уровень | line_id |
header |
source_form, source_id, smeta_type, smeta_direction, org_name, name, item_id, num_group, justification |
plan |
plan_q1..4 |
allocation |
internal_order, property_object |
sequestration.DFIP |
adj_q1..4 |
reserve |
amount_q1..4 |
approved |
approved_q1..4 |
contract_detail |
counterparty, reference, subject, currency, ceiling_amount, expenses_q1..4, vat_rate, deadline |
q1–q4 |
adj_current, adj_ssp, adj_rf, adj_reserve, payment_date, payment_amount, payment_amount_ho, payment_amount_rf, payment_act, booking_amount, actual_m1..3 |
только q4 |
actual_spod |
q1–q3 |
transfer_to_q<k> для всех k больше номера квартала, transfer_to_economy |
q2–q4 |
plan_revision_eco_change, plan_revision_item_adj, plan_revision_increase, plan_revision_sequester |
Ключи со значением null SHALL отсутствовать. Нулевые числа SHALL сохраняться. Обёртка строки (row_type, depth, sort_order) SHALL совпадать с полным представлением.
Scenario: Компактная строка — проекция полной
- WHEN одна и та же страница запрошена с
view=fullи сview=compact - THEN строки идут в одном порядке, и для каждой строки компактное
dataравно проекции полногоdataна перечисленные пути без ключей со значениемnull
Scenario: Ноль не теряется
- WHEN в полном представлении
plan.plan_q1 = 0 - THEN в компактном
plan.plan_q1 = 0
Requirement: Варианты колоночных фильтров
GET /api/v1/svod/2/{sheet}/{year}/facets с необязательным org_ids SHALL возвращать для строк листа за год по этим организациям (без учёта поиска и колоночных фильтров):
total— число строк;smeta_typeиsmeta_direction— отсортированные списки различных непустых значений;org— список различных организаций строк как пар{id, name}, отсортированный поname.
Результат SHALL совпадать с тем, что можно вычислить по полному ответу того же листа с теми же org_ids. Неизвестный sheet SHALL давать 404.
Scenario: Варианты совпадают с полным ответом
- WHEN запрошены
/facetsи полный ответ GO для одного года иorg_ids - THEN
totalравен длине полного ответа, множестваsmeta_type,smeta_directionи{source_org_unit_id, org_name}совпадают с вычисленными по полному ответу
Scenario: Неизвестный лист
- WHEN запрошен
/api/v1/svod/2/XX/{year}/facets - THEN ответ — 404
Requirement: Права доступа на страницы и варианты
Для не-администратора выдача страниц, total и варианты фильтров SHALL строиться только по организациям, привязанным к пользователю. Запрошенные org_ids вне этого множества SHALL отбрасываться, как в прежнем режиме. Если пересечение пусто, ответ SHALL быть пустым: result пустой, total = 0, списки вариантов пусты.
Scenario: Чужая организация в фильтре
- WHEN исполнитель, привязанный к организации A, запрашивает страницу и
/facetsсorg_ids=<B> - THEN
resultпустой,total= 0,orgв вариантах пуст
Requirement: Размер и стоимость ответа
На данных стенда масштаба 1:
- распакованный размер страницы
view=compactSHALL быть не больше 40 % размера той же страницыview=full; - распакованный размер первой страницы (
limit= размер страницы экрана), который экран получает до показа первых строк, SHALL быть не больше 5 % прежнего полного ответа того же листа.
Время ответа запроса в прежнем режиме (без новых параметров) SHALL ухудшиться не больше чем на 10 % по медиане.
Scenario: Замер размеров на стенде
- WHEN
perf/analysis/api_split.py --scale 1выполняет для GO и RF полный прежний запрос, страницуoffset=0&limit=<размер страницы>сview=fullи сview=compactи запрос/facets - THEN отношение
bytes(compact) / bytes(full)той же страницы не больше 0,40,bytes(compact первой страницы) + bytes(facets)не больше 5 %bytesполного ответа, медиана полного времени прежнего запроса не больше 1,10 от замера «до»