"""平台店铺表(store_mapping)—— 跨平台"同一家店"的 id/名 映射资产层。 每完成一次淘宝比价(在目标淘宝店通过 更多操作→分享→复制链接 拿到分享短链、 HTTP 解析出 shopId 后),pricebot server→server 内部上报落这里一行。**与登录无关、 不依赖客户端鉴权**(比价透传链路当前不鉴权,user_id 客户端带上时一并记)。 与 price_observation 的区别: - price_observation:平台/门店视角的**价格事实**(某店这单多少钱)。 - store_mapping:平台/门店视角的**身份映射**(同一家物理店在 淘宝/美团/京东 各自的 店铺 id 与店名)。是未来"我见过这家店→跳过重新搜索/匹配"的源头。两表独立。 「先存下来、用法后说」:列尽量铺全(各平台 id/名 + 地理 + 溯源 + 淘宝原料 URL), attrs(JSONB)兜底存灵活明细,免得每多记一个字段就迁移 schema。 ⚠️ 数据质量:跨平台"同一家店"的连接来自 agent 的 LLM 店铺匹配,匹配错则一行里连错店。 本表是 append-only 原始记录(每比价一行、trace_id 幂等防重试重复),清洗/归一二期再做。 当前只接通**淘宝**(id_taobao 才有真值;美团/京东多为 name-only 或空)。 """ from __future__ import annotations from datetime import datetime from sqlalchemy import ( JSON, DateTime, Float, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base # PG 上用 JSONB,SQLite(本地/测试)退化为通用 JSON(同 price_observation / comparison_record)。 _JSON = JSON().with_variant(JSONB(), "postgresql") class StoreMapping(Base): __tablename__ = "store_mapping" __table_args__ = ( # 一次比价(trace)只记一条:pricebot 重试 / 客户端 replay 重复上报时幂等去重。 # 一次淘宝比价 = 一个目标淘宝店 → 一行映射(源 + 各平台身份压在同一行)。 UniqueConstraint("trace_id", name="uq_store_mapping_trace"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # ===== 跨平台店铺身份(同一家物理店在各平台的 id/名;按比价角色稀疏填充)===== # id_taobao = 分享短链解析出的 shopId(淘宝当目标、走完取 id 流程才有); # name_taobao = 店铺页 a11y content_desc "店铺标题:xxx" 剥前缀。 id_taobao: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True) name_taobao: Mapped[str | None] = mapped_column(String(128), nullable=True) # 美团/京东:当前无同款 share→id 机制,多为 name-only(源平台店名来自 intent / agent 匹配名)。 id_meituan: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True) name_meituan: Mapped[str | None] = mapped_column(String(128), nullable=True) id_jd: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True) name_jd: Mapped[str | None] = mapped_column(String(128), nullable=True) # ===== 地理(同名店异地区分 / 地理分桶匹配的主要燃料)===== city: Mapped[str | None] = mapped_column(String(64), nullable=True) geohash: Mapped[str | None] = mapped_column(String(16), index=True, nullable=True) lng: Mapped[float | None] = mapped_column(Float, nullable=True) lat: Mapped[float | None] = mapped_column(Float, nullable=True) # 淘宝门店地址(店铺页 a11y 抓到才有;比经纬度更利于人工/LLM 匹配) taobao_address: Mapped[str | None] = mapped_column(String(256), nullable=True) # ===== 溯源 / 用户画像 ===== # 源平台(发起比价那家:meituan / taobao_flash / jd_waimai ...) source_platform: Mapped[str | None] = mapped_column(String(32), index=True, nullable=True) business_type: Mapped[str] = mapped_column(String(16), nullable=False, default="food") # pricebot 侧 trace_id:回指原始 trace(溯源)+ 幂等去重键(uq_store_mapping_trace) trace_id: Mapped[str] = mapped_column(String(64), nullable=False) source_device_id: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True) # user_id 当前比价链路不鉴权拿不到,客户端带上时才有;先可空。 source_user_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True) # ===== 淘宝原料(可复跳 / 可重解析 / 调试;URL 可能很长 → Text)===== taobao_share_url: Mapped[str | None] = mapped_column(String(256), nullable=True) # m.tb.cn 短链 taobao_resolved_url: Mapped[str | None] = mapped_column(Text, nullable=True) # 解析出的目标 URL(含 shopId) taobao_deeplink: Mapped[str | None] = mapped_column(Text, nullable=True) # 拼好的 et-store/search deeplink # 灵活字段兜底(免得加字段就迁移) attrs: Mapped[dict | None] = mapped_column(_JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), index=True, nullable=False ) def __repr__(self) -> str: # pragma: no cover return ( f"" )