f5d1a1a20d
比价沉淀的价格事实(平台/门店/菜篮/到手价/时间/来源)无条件落库,server 侧存储、 与登录无关,独立于用户视角的 comparison_record。是未来"别人查过同店→秒回价格" 价格大数据的源头(先存数据,用法后续)。 - models/price_observation.py: 新增 price_observation 表。结构化列(平台/门店/价格/ 红包/地理/来源)+ dishes/attrs 两个 JSONB 兜底列;(trace_id,platform,scope) 幂等 唯一约束;observed_at/store_name/geohash/source_device_id/source_user_id 等索引 - alembic/versions/price_observation_table.py: 建表迁移,down_revision=wx_transfer_auth (已核为当前唯一 head,无多 head 风险) - schemas/ + repositories/price_observation.py: 批量收发模型 + 跨方言(PG/SQLite)幂等 批量插入(先查 trace 已有 (platform,scope) 只插新的,并发撞唯一约束则回滚跳过) - api/internal/price.py: 新增 POST /internal/price-observation,X-Internal-Secret 头校验 (常量时间比较,未配密钥→503),server→server 专用,不走用户 JWT - core/config.py + main.py: 新增 INTERNAL_API_SECRET 配置 + 注册内部路由 - .env.example: INTERNAL_API_SECRET(须与 pricebot 侧同值,留空=内部写端点关闭) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: pure <pure@192.168.0.104> Reviewed-on: #21
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""价格观测落库:一次比价的整批观测幂等写入。
|
|
|
|
幂等键 = (trace_id, platform, scope)。pricebot 重试 / replay 会重复上报同一 trace,
|
|
这里先查该 trace 已存在的 (platform, scope),只插新的——跨方言(PG / SQLite dev)都安全,
|
|
不依赖 ON CONFLICT。极端并发下若仍撞唯一约束,IntegrityError 回滚后忽略(本批本就幂等)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.price_observation import PriceObservation
|
|
from app.schemas.price_observation import PriceObservationBatchIn
|
|
|
|
logger = logging.getLogger("shagua.price_obs")
|
|
|
|
|
|
def insert_batch(db: Session, payload: PriceObservationBatchIn) -> tuple[int, int]:
|
|
"""写入一次比价的整批观测,返回 (inserted, skipped)。
|
|
|
|
skipped = 因 (trace_id, platform, scope) 已存在而幂等跳过的条数。
|
|
"""
|
|
if not payload.observations:
|
|
return 0, 0
|
|
|
|
# 该 trace 已有的 (platform, scope),用于幂等过滤
|
|
existing: set[tuple[str, str]] = set(
|
|
db.execute(
|
|
select(PriceObservation.platform, PriceObservation.scope).where(
|
|
PriceObservation.trace_id == payload.trace_id
|
|
)
|
|
).all()
|
|
)
|
|
|
|
inserted = 0
|
|
skipped = 0
|
|
for obs in payload.observations:
|
|
key = (obs.platform, obs.scope)
|
|
if key in existing:
|
|
skipped += 1
|
|
continue
|
|
existing.add(key) # 同批内去重(防 pricebot 同帧给重复平台)
|
|
db.add(
|
|
PriceObservation(
|
|
trace_id=payload.trace_id,
|
|
business_type=payload.business_type,
|
|
platform=obs.platform,
|
|
platform_store_id=obs.platform_store_id,
|
|
store_name=payload.store_name,
|
|
city=payload.city,
|
|
geohash=payload.geohash,
|
|
lng=payload.lng,
|
|
lat=payload.lat,
|
|
is_source=obs.is_source,
|
|
scope=obs.scope,
|
|
price_cents=obs.price_cents,
|
|
coupon_saved_cents=obs.coupon_saved_cents,
|
|
coupon_name=obs.coupon_name,
|
|
store_closed=obs.store_closed,
|
|
rank=obs.rank,
|
|
dishes=payload.dishes,
|
|
attrs=obs.attrs,
|
|
source_device_id=payload.source_device_id,
|
|
source_user_id=payload.source_user_id,
|
|
)
|
|
)
|
|
inserted += 1
|
|
|
|
if inserted == 0:
|
|
return 0, skipped
|
|
|
|
try:
|
|
db.commit()
|
|
except IntegrityError:
|
|
# 并发下另一个请求刚插了同 (trace,platform,scope) → 唯一约束撞了。
|
|
# 本批本就幂等,回滚后当作全跳过(不重试,失败就失败,见 P0 约定)。
|
|
db.rollback()
|
|
logger.warning(
|
|
"price_observation 并发幂等冲突 trace=%s,本批回滚跳过", payload.trace_id
|
|
)
|
|
return 0, skipped + inserted
|
|
|
|
return inserted, skipped
|