(工作区既有改动, 非本次会话产出)
11 KiB
store_mapping — 跨平台「同一家店」的 id/名/deeplink 身份映射(门店资产层)
模型
app/models/store_mapping.py· 仓库app/repositories/store_mapping.py· 上报/反查端点app/api/internal/store.py(POST /internal/store-mapping、GET /internal/store-mapping/lookup、POST /internal/store-mapping/invalidate) · ← 索引 · 总览
每完成一次比价,pricebot 把同一家物理店在 淘宝/美团/京东 各自的店铺 id、店名、可复跳 deeplink server→server 内部上报落这里一行(一次比价 = 一个 trace = 一行,各平台「腿」解析出 id 后填空合并进同一行)。与登录无关、不依赖客户端鉴权(同 price_observation,source_user_id 客户端带上时才有,匿名也记)。
与 price_observation 的区别(两表平行、独立):
price_observation= 平台/门店视角的价格事实(某店这单多少钱)。store_mapping= 平台/门店视角的身份映射(同一家店在各平台的店铺 id/名/deeplink)。是未来「我见过这家店 → 跳过重新搜索/匹配,直接 deeplink 进店内搜索」的源头(#48)。
与 comparison_record(用户视角、带 JWT、按 user_id 存)也无关:本表是 server 无条件沉淀的客观映射、匿名也记。
⚠️ 数据质量:跨平台「同一家店」的连接来自 agent 的 LLM 店铺匹配,匹配错则一行里
id_taobao与id_jd/name_meituan连到不同店。本表是 append-only 原始记录(每比价一行、trace_id幂等防重试重复),「一行里多平台 id 共存」只表示「两条腿搜同一个源店名各自匹配到了某家店」= name-match 置信度、非已核实同一实体;清洗/归一/精确匹配二期由下游做。
用在哪 / 增删改查
- C / U(upsert,trace_id 维度幂等 + 填空合并):
POST /internal/store-mapping(report_store_mapping→repo.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 走现场反查老路。 - U(deeplink 失效标记):
POST /internal/store-mapping/invalidate({platform, shop_id})。pricebot 撞错误页回退时上报,按shop_id把所有匹配行的对应*_deeplink_invalid_at置now()(幂等:已标记的跳过)。taobao→mark_taobao_deeplink_invalid、jd→mark_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 shopId;3.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 |
JSON(PG 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 重复上报幂等去重;后到的平台腿填空合并进同一行。 - index:
id_taobao、id_meituan、id_jd、meituan_poi_id_str、jd_vender_id、geohash、source_platform、source_device_id、source_user_id、created_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 / 未启用时该表不会有新数据。