o2o-site-AEO/solution/backend/crud/search_console_crud.py
Mina Choi 3f47d5ecd2 [feat] solution/backend: 서치콘솔 자동 제출·색인 상태 추적 추가
사이트 발행 성공과 Google 색인 관측은 별도 상태다. 외부 API 장애로 발행이 실패하거나 재시작 때 추적 정보가 사라지지 않도록 분리.

- Google 클라이언트·배치·DB·Teams 알림 모듈 분리
- 기존 스케줄러 연결, 재시도·중복 실행 방지와 선택 설정 추가
- ORM·초기 DDL·마이그레이션·운영 설정 문서 동시 갱신

검증: 관련 59건 통과, compose 설정·diff 검사 통과. 추가 회귀 23건 통과, 기존 발행 검수 실패 1건은 변경 전 코드에서도 재현. 운영 배포·Google/Teams 실호출 미실행.
2026-09-15 14:50:30 +09:00

58 lines
2.5 KiB
Python

"""발행 DB를 작업 원장으로 사용해 알림 적재 실패/재시작에도 대상을 다시 찾는다."""
from sqlalchemy import select, or_, text
from sqlalchemy.dialects.postgresql import insert
from common.database.model.models import sites, places, site_versions, site_search_status as Status
from common.enums import SiteStatus
def published_conditions():
return (
sites.deleted.is_(False), places.deleted.is_(False),
sites.status == SiteStatus.PUBLISHED.value,
sites.published_at.is_not(None), sites.current_version_id.is_not(None),
)
async def lock_batch(session) -> bool:
# 여러 API 프로세스가 같은 크론을 등록해도 외부 호출은 한 곳만 수행한다.
return bool(await session.scalar(text("SELECT pg_try_advisory_xact_lock(734920151)")))
async def new_publications(session, property_url: str):
result = await session.execute(
select(sites, places, site_versions)
.join(places, places.place_id == sites.place_id)
.join(site_versions, site_versions.site_version_id == sites.current_version_id)
.outerjoin(Status, Status.site_id == sites.site_id)
.where(*published_conditions(), site_versions.deleted.is_(False), or_(
Status.site_id.is_(None), Status.site_version_id != sites.current_version_id,
Status.property_url != property_url, Status.published_at != sites.published_at,
))
.order_by(sites.published_at).limit(100)
)
return result.all()
async def register(session, values: dict):
reset = dict(values, sitemap_submitted_at=None, inspected_at=None, first_indexed_at=None,
inspection=None, error_code=None, failures=0, alerted_at=None,
next_check_at=text("now()"), updated_at=text("now()"), deleted=False)
await session.execute(insert(Status).values(**values).on_conflict_do_update(
index_elements=[Status.site_id], set_=reset,
))
async def due_sites(session, property_url: str):
result = await session.execute(
select(Status).join(sites, sites.site_id == Status.site_id)
.join(places, places.place_id == sites.place_id)
.where(*published_conditions(), Status.deleted.is_(False),
Status.site_version_id == sites.current_version_id,
Status.published_at == sites.published_at, Status.property_url == property_url,
Status.next_check_at <= text("now()"))
.order_by(Status.next_check_at).limit(5)
.with_for_update(of=Status)
)
return result.scalars().all()