Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

244 Commits

Folders and files

Repository files navigation

trip-booking-agg

酒店静态数据的采集与聚合。从两个项目里拆出来:

拆自 拆什么 为什么拆
trip-booking-spa Expedia 静态采集 spa 是供应商网关(查价/验价/下单/取消),静态采集不属于它。现在那套挂在 BackDoorController 的 /hotel/expedia/* 后门上,本来就是历史遗留
tg-trip-cursor elong 静态采集 + 聚合 那套聚合是多源混着长出来的,治不动,只能另起

本服务只写自己的库 tg_trip_agg,对老库和 spa 库零写操作。


分层

web/            运维入口(必须带鉴权,不做 BackDoor)
  ↓
ingest/         采集:把各家原话取回来,落进来源层
aggregate/      聚合:来源层 → 骨架 → 对照表
  ↓
source/         来源层与骨架层的读写
platform/       公共设施:http / ratelimit / util

采集不许调聚合(由 AggregationArchRulesTest 强制)。老库的 ElongHotelStaticWrapper.processSingleHotel 里顺手触发了 aggregateFromDb, 结果「补个静态数据」会连带改动 canonical,两件事再也拆不开—— 想补数据就得接受聚合被重跑,想重跑聚合就得重新采集。

关键接口

接口 位置 作用
SupplierStaticSource ingest/spi 每家供应商一个实现,按 IngestScope 取数并推给 sink
SupplierStaticNormalizer ingest/spi 把原话解析成统一形状,各自消化各家的坑
BackboneSource aggregate/spi 骨架源角色声明。让「骨架选谁」变成配置,不是散落的 if
HotelMatcher / RoomMatcher aggregate/spi 判同。三种结局:能证明是 / 能证明不是 / 证不出来
LegacyIdResolver aggregate/spi 新库 canonical ↔ 老库 hotel_id 的翻译

为什么「证不出来」要独立成一种结局

老库只有「匹配上 / 没匹配上」两分,没匹配上就硬猜, 抽样实测 46% 是危险的模糊命中(Single Room-Smoking 配到了 SINGLE ROOM(NON-SMOKING))。

新库证不出来就进 room_unclassified 桶:照常能卖能下单,只是不参与比价。 归不了类不等于丢货。

为什么房型匹配不许用名字当主判据

实跑 The Briza Beach Resort 时,词袋判据把 Beachfront Premier Room 和骨架的 Premier Beach Front 当成两间房,凭空造了一间重复。而这两间的床型、人数、面积必然一致。

主判据是结构化属性:床型签名 + 可住人数 + 面积(±15%)。实测覆盖率:

属性 Expedia elong
床型签名 100% 91.3%
可住人数 100% 100%
面积 64.5% 82.3%

骨架源

当前:Expedia 打底,elong 扩充。

依据是 80 家泰国重合酒店逐房比对——自带重复率 Expedia 1.9% vs elong 15.0%, 且 Expedia 从没有比 elong 多(0/80)。骨架的职责是定义「这家店有哪几间房」, 拿一个自带 15% 重复的源当地基,等于把病种进去。

但房型必须允许 elong 扩充:去重后 elong 仍有 44.6% 是 Expedia 真没有的房型, 死守打底源就是重演「十万间暗房」。

优先级写在 application.yml 的 agg.aggregate.backbone,换骨架源改配置不改代码—— 因为这个结论是可能被推翻的(elong 覆盖 254,323 家是 Expedia 97,265 家的 2.6 倍)。 可切换的前提是骨架尚未对外发布:canonical 酒店 id 沿用老库(渠道在用,本来就锁死), 但 canonical 房型 id 是新库自己铸的,一旦对渠道放量就锁死。

本地怎么跑通(别再拿生产当测试环境)

docker start agg-mysql          # 本机已有:映射 13306,库 tg_trip_agg
mvn test -Dtest=IngestPipelineLocalIT -Dspring.profiles.active=localit

对着真库真表跑整条采集链路,只把「打供应商接口」换成假的。8 秒出结果。

为什么非有不可:2026-08-28 有三个 bug 是部署到生产才发现的,每轮 4~8 分钟 (还常遇北京 runner 拉 GitHub 抽风重试),而三个全是「字段装不下」或「参数放错位置」, 本地对着真表跑一次就当场炸:

bug 现象 本地能否提前发现
442 个酒店号拼进 query 撞 Tomcat 8KB 请求行上限,HTTP 400 且返回一页 HTML 能
同一批名单塞进 ingest_batch.scope_key(varchar(200)) Data too long,整批 500 能
电话是字符串套 JSON,整串写进 phone 列 Data too long,5 家全部落库失败 能

⚠️ DDL 一律先落 deploy/schema/ 再上生产。 上面那次 mapping_version 就是 直接在生产 ALTER、没进仓库,结果本地库缺列、集成测试一跑就 Unknown column。

技术选型

  • Spring Boot 3.2 + Java 21,不跟随 spa 的 2.7(2023-11 已 EOL)。 代价是不能直接复制 spa 代码(javax → jakarta),但要搬的是 HTTP 客户端和解析逻辑, 基本不碰 Servlet API。
  • 虚拟线程默认开。采集是 IO 密集:拉一家酒店 = 1~2 次 HTTP + 一次 upsert, 线程绝大部分时间在等网络。
  • 采集与聚合开关默认全关。新服务不该一启动就拉全量。

库

tg_trip_agg(腾讯 cdb-ip29xqww),16 张表,DDL 见 tg-trip-cursor/backend/specs/2026-08-26-aggregation-schema.sql。

  • hotel_base / room_base 的 id 都从 3,000,000,000 起。 老库现 20.09 亿,留 10 亿缓冲;2 字头 = 老库生、3 字头 = 新库生,一眼认出身。
  • 只软删不删行。当年「删爹不删儿子」+ 自增回拨造出嵌合体 (12,081 条孤儿映射指向不存在的酒店),根治办法就是永不 DELETE。

设计依据

tg-trip-cursor/backend/specs/2026-08-26-aggregation-split-expedia-backbone.md ——本文所有数字的出处、6 个被否方案及否决理由、7 条风险,都在那份 spec 里。

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages