feat(#254): user_custom_pois — backend schema + CRUD + scoring integration #257
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#257
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/254-custom-pois-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
101_user_custom_pois.sql: GEOGRAPHY(POINT) + GIST, weight CHECK [-5,5]custom_pois.py: CRUD +get_overlaps_for_scoring(psycopg v3 CAST)/api/v1/custom-pois: X-Session-Id header authanalyze_parcel: custom POI after OSM loop, contribution=weight*decay,custom_poi_score_itemsin responseFrontend contract (X-Session-Id)
Frontend saves uuid from
X-Session-Idresponse header to localStorage, sends it asX-Session-Idrequest header in all custom-pois and analyze calls.Test plan
101_user_custom_pois.sqlDependencies
Deep Code Review — verdict: APPROVE
Summary
485f489b, base9dd72ec, mergeable=trueCritical / High / Medium
Нет блокирующих находок. Все P0/P1 правила соблюдены.
Конвенции (rules check)
data/sql/101_user_custom_pois.sql— корректное sequential naming (после100_user_weight_profiles_default_seed.sql),BEGIN; ... COMMIT;,CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS— идемпотентно.:name::typeв diff (grep -E ':[a-z_]+::[a-z]'→ пусто). ТолькоCAST(:x AS type)+ PostgreSQL-нативные касты::geography(это inline cast результатаST_MakePoint(...), к bind-name не относится — OK).SET-список из whitelisted полей с bind-параметрами — safe.logger.*вместоprint(): соблюдено (logger.info/debug/warning).Schema (data/sql/101_user_custom_pois.sql)
geom GEOGRAPHY(POINT, 4326) GENERATED ALWAYS AS (...) STORED— отличный выбор, PostGIS сам пересчитывает на UPDATElon/lat(видно вupdate_custom_poi, не пишет вgeomявно).weight REAL NOT NULL CHECK (weight BETWEEN -5 AND 5)— соответствует Pydantic-валидации (ge=-5, le=5).(user_id, parcel_cad)— хорошо покрывает scoring-запрос (WHERE user_id = ... AND ST_DWithin(geom, ...)).created_at/updated_atсDEFAULT NOW(), trigger не нужен —updated_at = NOW()ставится в UPDATE-сервисе явно.Pydantic schemas (custom_poi.py)
CustomPoiCreate:min_length=1, max_length=200на name; lon/lat в WGS-84; weightge=-5, le=5. Все границы покрыты.CustomPoiUpdate: все поля Optional, проверка диапазонов сохранена.CustomPoiOut: типы соответствуют DDL.Service (services/site_finder/custom_pois.py)
_INSERT_SQL,_SELECT_BY_USER_ALL, ...) — DRY, инъекции невозможны.update_custom_poi: динамическое построениеSET— корректно, имена колонок жёстко заданы (whitelist), bind-параметры для значений.get_overlaps_for_scoring:ST_Centroid(ST_GeomFromText(CAST(:wkt AS text), 4326))::geography+ST_DWithin(..., 1000)— radius hardcoded 1000m matched сanalyze_parceldecay1 - distance / 1000. Согласовано.try/except Exceptionвget_overlaps_for_scoring→logger.warning+ return[]— graceful, scoring продолжает работать если custom POI таблица недоступна.API (api/v1/custom_pois.py)
X-Session-Idresolved через_resolve_session_idhelper: при отсутствии генерирует UUID и кладёт в response header — frontend-friendly.user_idscope (WHERE user_id = :user_id) — изоляция данных пользователей.user_idfilter в SELECT/DELETE WHERE) — privacy-safe (404 не раскрывает existence чужой записи).Scoring integration (parcels.py)
score_final = score + center_bonus— корректное место, contribution учитывается вscore_top_3_positives/negatives,factors_detailed,score_by_group._decay = max(0.0, 1.0 - _distance_m / 1000.0)— линейный decay, согласован с radius 1000m в_OVERLAPS_SQL.ST_DWithin.try/except Exceptionвокруг custom POI блока →logger.warning— failure не ломает analyze.custom_poi_score_itemsдобавлен в response — frontend (PR #258) сможет отрисовать.if not _session_id: skip— gracefully скипает если header отсутствует.Tests (13 тестов)
Cross-file impact
backend/app/main.py: router подключён под/api/v1/custom-pois— OK.analyze_parcelresponse shape: добавлено новое optional-by-default полеcustom_poi_score_items: list— backward-compatible (старый FE проигнорирует).Vault check
code/schemas/Schema_User_Custom_Pois.md— присутствует, корректный frontmatter (status: ready_to_apply,github_issue: 254).domains/api/endpoints/Endpoint_Parcel_Analyze.md— содержит ссылку наSchema_User_Custom_Pois, описаниеcustom_poi_score_items.Security
user_idscope — корректная фильтрация во всех CRUD-операциях.session_idможет читать/редактировать его POI. Это явный trade-off в PR ("workaround до #67 NextAuth"). Acceptable для текущего этапа./custom-poisотсутствует — может быть abused (10k POI за минуту). Создать отдельный issue, не блокирует merge.Performance
(user_id, parcel_cad)покрываетWHERE user_id = ... AND (parcel_cad IS NULL OR parcel_cad = ...).ST_DWithinв scoring query._OVERLAPS_SQLотсутствует — теоретический риск, если у пользователя 10000 POI и все в радиусе 1км. Realistic risk низкий, но suggest: добавитьLIMIT 1000на будущее (не блокирующее).Risk score
DROP TABLE user_custom_pois CASCADE+ revert PR.Post-merge checklist
101_user_custom_pois.sqlприменится автоматически через deploy.yml.curl -X POST -H 'X-Session-Id: test-uuid' -H 'Content-Type: application/json' -d '{"name":"test","weight":1,"lon":60.6,"lat":56.84}' https://gendsgn.ru/api/v1/custom-pois→ 201.Merging via deep-code-reviewer.