8659a7ed2b
新增「平台店铺表」资产层: 淘宝比价拿到 shopId 后, pricebot server→server 把跨平台 店铺身份(各平台 id/名 + 地理 + 来源)落库, 作为未来"我见过这家店→跳过重搜/匹配"的源头。 与 price_observation(价格事实)平行、独立。 - models/store_mapping.py: store_mapping 表 22 列 —— 跨平台身份(id/name_taobao/meituan/jd) + 地理(city/geohash/lng/lat/taobao_address) + 溯源(source_platform/trace_id/device/user) + 淘宝原料(share_url/resolved_url/deeplink) + attrs(JSONB) + created_at。 - schemas/store_mapping.py + repositories/store_mapping.py: append-only, trace_id 幂等 (pricebot 重试/replay 不重复写; 并发 IntegrityError 兜底返已存在行), 跨方言安全。 - api/internal/store.py: POST /internal/store-mapping(复用 price.py 共享密钥 X-Internal-Secret 校验)。 - 注册 model(__init__) + router(main.py); 迁移 store_mapping_table 接现 head coin_txn_task_ref_uq。 验证: alembic 单 head + 零模型/迁移漂移; TestClient 全链(无密钥401/有密钥inserted=1/幂等inserted=0/错密钥401)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Reviewed-on: #48
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""平台店铺映射内部上报端点(pricebot → app-server)。
|
|
|
|
pricebot 在淘宝比价拿到 shopId 后,把这一行跨平台店铺映射 POST 到这里落库。
|
|
**不是给客户端的接口**:不走用户 JWT,靠 server 间共享密钥头 `X-Internal-Secret` 校验
|
|
(复用 price.py 的 _check_secret,与 price-observation 同一密钥)。
|
|
|
|
与 price.py 的 /internal/price-observation 平行:那个落价格事实,这个落店铺身份映射。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Header
|
|
|
|
from app.api.deps import DbSession
|
|
from app.api.internal.price import _check_secret
|
|
from app.repositories import store_mapping as repo
|
|
from app.schemas.store_mapping import StoreMappingIn, StoreMappingOut
|
|
|
|
logger = logging.getLogger("shagua.internal.store")
|
|
|
|
router = APIRouter(prefix="/internal", tags=["internal"])
|
|
|
|
|
|
@router.get(
|
|
"/store-mapping/lookup",
|
|
summary="比价前按源平台店名反查各目标平台已沉淀的店铺 id(命中→pricebot 直接 deeplink)",
|
|
)
|
|
def lookup_store_mapping(
|
|
source_platform: str,
|
|
name: str,
|
|
db: DbSession,
|
|
lat: float | None = None,
|
|
lng: float | None = None,
|
|
x_internal_secret: Annotated[str | None, Header()] = None,
|
|
) -> dict:
|
|
_check_secret(x_internal_secret)
|
|
result = repo.lookup_nearest(db, source_platform, name, lat, lng)
|
|
if result:
|
|
hits = ", ".join(
|
|
f"{t}:row{v['row_id']}"
|
|
f"{'(' + str(v['dist_km']) + 'km)' if 'dist_km' in v else ''}"
|
|
f"→{v.get('deeplink') or '(无deeplink)'}"
|
|
for t, v in result.items()
|
|
)
|
|
logger.info(
|
|
"store_mapping lookup source=%s name=%r geo=(%s,%s) → 命中 %s",
|
|
source_platform, name, lat, lng, hits,
|
|
)
|
|
else:
|
|
logger.info(
|
|
"store_mapping lookup source=%s name=%r geo=(%s,%s) → MISS",
|
|
source_platform, name, lat, lng,
|
|
)
|
|
return result
|
|
|
|
|
|
@router.post(
|
|
"/store-mapping",
|
|
response_model=StoreMappingOut,
|
|
summary="平台店铺映射内部上报(pricebot→app-server,落 store_mapping)",
|
|
)
|
|
def report_store_mapping(
|
|
payload: StoreMappingIn,
|
|
db: DbSession,
|
|
x_internal_secret: Annotated[str | None, Header()] = None,
|
|
) -> StoreMappingOut:
|
|
_check_secret(x_internal_secret)
|
|
created, row_id = repo.upsert(db, payload)
|
|
logger.info(
|
|
"store_mapping trace=%s %s row_id=%s source=%s "
|
|
"taobao=(%s,%s) jd=(%s,%s) device=%s user=%s",
|
|
payload.trace_id, "新建" if created else "合并", row_id, payload.source_platform,
|
|
payload.id_taobao, payload.name_taobao, payload.id_jd, payload.name_jd,
|
|
payload.source_device_id, payload.source_user_id,
|
|
)
|
|
return StoreMappingOut(inserted=created, row_id=row_id)
|