酒店静态数据的采集与聚合。从两个项目里拆出来:
| 拆自 | 拆什么 | 为什么拆 |
|---|---|---|
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 家全部落库失败 | 能 |
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 里。