Files
2026-06-17 23:32:36 +08:00

11 KiB
Raw Permalink Blame History

store_mapping — 跨平台「同一家店」的 id/名/deeplink 身份映射(门店资产层)

模型 app/models/store_mapping.py · 仓库 app/repositories/store_mapping.py · 上报/反查端点 app/api/internal/store.pyPOST /internal/store-mappingGET /internal/store-mapping/lookupPOST /internal/store-mapping/invalidate · ← 索引 · 总览

每完成一次比价,pricebot 把同一家物理店在 淘宝/美团/京东 各自的店铺 id、店名、可复跳 deeplink server→server 内部上报落这里一行(一次比价 = 一个 trace = 一行,各平台「腿」解析出 id 后填空合并进同一行)。与登录无关、不依赖客户端鉴权(同 price_observationsource_user_id 客户端带上时才有,匿名也记)。

price_observation 的区别(两表平行、独立):

  • price_observation = 平台/门店视角的价格事实(某店这单多少钱)。
  • store_mapping = 平台/门店视角的身份映射(同一家店在各平台的店铺 id/名/deeplink)。是未来「我见过这家店 → 跳过重新搜索/匹配,直接 deeplink 进店内搜索」的源头(#48)。

comparison_record(用户视角、带 JWT、按 user_id 存)也无关:本表是 server 无条件沉淀的客观映射、匿名也记。

⚠️ 数据质量:跨平台「同一家店」的连接来自 agent 的 LLM 店铺匹配,匹配错则一行里 id_taobaoid_jd/name_meituan 连到不同店。本表是 append-only 原始记录(每比价一行、trace_id 幂等防重试重复),「一行里多平台 id 共存」只表示「两条腿搜同一个源店名各自匹配到了某家店」= name-match 置信度、非已核实同一实体;清洗/归一/精确匹配二期由下游做。

用在哪 / 增删改查

  • C / Uupserttrace_id 维度幂等 + 填空合并)POST /internal/store-mappingreport_store_mappingrepo.upsert)。合并键 = trace_id(唯一约束)。一次跨平台比价 = 一个 trace = 一个真实店铺:某条腿解析出 id 先建行(填自己那几列),后到的腿(将来)再 upsert 进同一行合并策略 = 填空(fill-the-blanks_merge_fill_blanks 只写该行当前为 NULL 的列、绝不覆盖已有非空值;共享列(geo/source/溯源)先到先得。首写用逐列 setattr(而非硬编码构造器,否则首写是美团/京东腿时其列会漏)。不依赖 ON CONFLICT,跨方言安全;并发撞唯一约束则 rollback() 后转填空合并路径。返回 {inserted, row_id}inserted=1 新建 / 0 合并)。
  • R(lookup 反查,命中即省一整段现场反查)GET /internal/store-mapping/lookup?source_platform=&name=&lat=&lng=lookup_nearest)。比价前按「源平台店名」反查各目标平台已沉淀的店铺 id,命中就让 pricebot 直接 deeplink 跳店内搜索,省掉「开平台→进店→分享→反查」整段。机制:匹配键 = 源平台对应的 name 列精确等于 name;每个目标分别取「含该目标 id 的同名候选里最近一条」(有入参 geo 且候选带 geo → haversine 取最近,否则取 created_at 最新);排除源平台自己(不复用源平台 id);过滤掉 deeplink 已失效的候选(见下)。返回 {target_key: {id..., deeplink, row_id, dist_km?}},空 = 没缓存、pricebot 走现场反查老路。
  • Udeeplink 失效标记)POST /internal/store-mapping/invalidate{platform, shop_id})。pricebot 撞错误页回退时上报,按 shop_id所有匹配行的对应 *_deeplink_invalid_atnow()(幂等:已标记的跳过)。taobaomark_taobao_deeplink_invalidjdmark_jd_deeplink_invalid其它平台(含 meituan)当前 no-op 返回 affected=0、不报错(向后兼容 pricebot 将来扩展)。返回 {ok, affected}
  • D:无业务删除。

上报/反查/失效端点全是 server→server 专用,复用 price.py_check_secret(同一 X-Internal-Secret 密钥,未配置→503,不匹配→401)。

字段

各平台列按比价角色稀疏填充(淘宝当目标才有 *_taobao,依此类推)。

类型 约束 / 默认 说明(取值 / join
id Integer PK, autoincrement
跨平台身份 同一物理店在各平台的 id/名
id_taobao String(64) index, 可空 淘宝 = 分享短链解析出的 shopId(淘宝当目标、走完取 id 流程才有)。门店稳定数字主键
name_taobao String(128) 可空 店铺页 a11y content_desc「店铺标题:xxx」剥前缀
id_meituan String(64) index, 可空 留给将来 CPS API 的稳定数字 poi_id;当前无 share→稳定 id 机制 → 多为空(可复跳票据见 meituan_poi_id_str
name_meituan String(128) 可空 源平台店名(来自 intent/agent 匹配名)
id_jd String(64) index, 可空 京东 = storeId(门店稳定数字主键,同 taobao shopId3.cn 短链反查)。venderId 另存 jd_vender_id
name_jd String(128) 可空 京东店铺页店名
地理 同名店异地区分 / 地理分桶匹配主要燃料
city String(64) 可空
geohash String(16) index, 可空
lng / lat Float 可空 lookup 时按入参 geo 取最近候选用
taobao_address String(256) 可空 淘宝门店地址(店铺页 a11y 抓到才有;比经纬度更利于人工/LLM 匹配)
溯源 / 画像
source_platform String(32) index, 可空 发起比价那家:meituan / taobao_flash / jd_waimai … lookup 据此选 name 列、排除源平台自己
business_type String(16) NOT NULL, default food
trace_id String(64) NOT NULL 幂等合并键(唯一约束)+ 回指原始 trace(溯源)。软指 pricebot work_logs,无硬 FK
source_device_id String(64) index, 可空
source_user_id Integer index, 可空 比价链路不鉴权拿不到,客户端带上时才有,软指 user.id
淘宝原料(可复跳/重解析/调试)
taobao_share_url String(256) 可空 m.tb.cn 短链
taobao_resolved_url Text 可空 解析出的目标 URL(含 shopId)。Text 因 URL 可能很长
taobao_deeplink Text 可空 拼好的 et-store/search deeplink
taobao_deeplink_invalid_at DateTime(tz) 可空 淘宝 deeplink 失效标记(#53)。撞「页面出错了」降级页时被置;NULL=有效。lookup 反查过滤掉非 NULL 的淘宝候选
美团原料dpurl.cn 短链 → 302 反查 → imeituan://
meituan_poi_id_str String(64) index, 可空 ⚠️ 每次分享重新加密、非稳定主键(调研 §八)→ 单列存「可复跳的一次性票据」,不进 id_meituan
meituan_share_url String(256) 可空 dpurl.cn 短链
meituan_resolved_url Text 可空 302 落地 menu URL(含 poi_id_str
meituan_deeplink Text 可空 拼好的 imeituan:// 店内搜索 deeplink
京东原料(3.cn 短链 → 重定向反查 → openapp.jdmobile://
jd_vender_id String(64) index, 可空 venderId 单列存(deeplink 模板 venderId+storeId 都要;venderId 是商家维度、可跨门店,与门店 storeId 分开记)
jd_share_url String(256) 可空 3.cn 短链
jd_resolved_url Text 可空 反查出的目标 openapp.jdmobile:// URL
jd_deeplink Text 可空 拼好的 pages/search 店内搜索 deeplink
jd_deeplink_invalid_at DateTime(tz) 可空 京东 deeplink 失效标记(#56,对标淘宝)。撞「当前门店超出配送范围」页时被置;NULL=有效。lookup 过滤非 NULL 的京东候选
attrs JSONPG JSONB 可空 灵活字段兜底,免得加字段就迁移
created_at DateTime(tz) server_default now(), index, NOT NULL lookup 同名候选无 geo 时按它取最新

关系 / Join Key

  • 无硬外键
  • trace_id 软指 pricebot work_logs(溯源/排查),并作 upsert 幂等合并键。
  • source_user_id 软指 user.id(可空,不进唯一键、不阻塞落库)。
  • lookup 的匹配靠店名字符串精确相等_SOURCE_NAME_COLUMN 把源平台映到对应 name 列)+ 可选 geo 取最近,非 id 级 join

索引与约束

  • PK id
  • UNIQUE(trace_id) = uq_store_mapping_trace:一次比价(trace)只一行 → pricebot 重试 / 客户端 replay 重复上报幂等去重;后到的平台腿填空合并进同一行。
  • indexid_taobaoid_meituanid_jdmeituan_poi_id_strjd_vender_idgeohashsource_platformsource_device_idsource_user_idcreated_at(lookup 反查 / 失效标记按 id 列查需要)。

注意

  • deeplink 失效→标记→回退闭环(#53 淘宝 / #56 京东,本表最关键机制):缓存的 deeplink 会过期(淘宝撞「页面出错了」、京东撞「当前门店超出配送范围」)。pricebot 撞到错误降级页 → 调 /internal/store-mapping/invalidate 按 shopId/storeId 把所有相关行打上 *_deeplink_invalid_at → 后续 lookup_nearest 整条排除失效候选(当没缓存,pricebot 走正常搜店)→ 重搜成功会写新行自然覆盖。按 shopId 标记所有行(不是单行)是因为同一坏 id 可能散在多次比价的多行里,全标掉才能让 lookup 不再返回它。
  • 美团没有失效标记meituan_poi_id_str 是「每次分享重新加密」的一次性票据、本就不稳定不复用为稳定 id,故无 invalidate 路径(invalidate 端点对 meituan no-op)。三平台 share→id 反查机制不对称:淘宝 m.tb.cn→shopId(稳定)、京东 3.cn→venderId+storeId(稳定)、美团 dpurl.cn→poi_id_str非稳定)。
  • 资产层「先存后用」+ append-only:lookup 是已接通的读用途(省现场反查),但表本身是原始记录、不做清洗归一。「一行多平台 id 共存」不代表已核实同一实体——下游做精确匹配前别当真值用。
  • 数据质量依赖 LLM 匹配:跨平台连接来自 agent 店铺匹配,可能连错店;排查某行可疑映射时回 trace_id 指的 pricebot trace 看当时的匹配上下文。
  • attrs 是 JSONB 兜底,别塞大树(同 price_observation);_JSON = JSON().with_variant(JSONB(), "postgresql")PG JSONB / SQLite dev 通用 JSON。
  • 填空合并不覆盖:共享列(geo/source/device)先到先得,后到的腿只填自己那几列的空位;想「纠正」已写错的列当前没有路径append-only,靠新 trace 写新行)。
  • 上报端点未配 INTERNAL_API_SECRET 时返 503(默认关闭态),本地 dev / 未启用时该表不会有新数据。