# Daylight 合并与 `dl_reservation_id` 去重说明 本文档说明 `BookingService` 中为减少「同一 Daylight 预订在 `bookings` 表出现多行」所做的修改及调用关系。 --- ## 1. 涉及文件与入口 | 位置 | 说明 | |------|------| | `BookingService.java` | `mergeDaylightRecordToDatabaseInfo`、`insertDaylightBooking`、`consolidateDuplicateDlReservations` 及相关私有辅助方法 | | `BookingController`(recordSearch 等) | 在拉取 Daylight 列表后调用 `mergeDaylightRecordToDatabaseInfo` | --- ## 2. `mergeDaylightRecordToDatabaseInfo` 的改动要点 ### 2.1 预查询:按 `dlReservationId` 收集(不再依赖确认号) **原问题**:仅当 `confirmationNo` 非空时才把 `dlReservationId` 放进 `findByDlReservationIdIn` 的入参。若接口有 `dl_reservation_id` 但尚未返回确认号,则查不到库里已有行,会误判为「不存在」而走插入,产生重复。 **现逻辑**:对本次 `result`(Daylight 返回列表)中每条记录,只要 `dlReservationId` 非空白即加入 `LinkedHashSet`,再转为 `List` 做 `findByDlReservationIdIn`。 ### 2.2 合并后物理去重:`consolidateDuplicateDlReservations` 在 `updateBookingList` 执行 `bookingDao.saveAll` **之后**,对本次涉及到的每个 `dl_reservation_id` 再查库: - 若同一 `dl_reservation_id` 仍对应 **≥2 行**(例如一条前台 `PN`、一条同步误插入的 `N`),则: - 按评分规则选出 **keeper(主行)**,其余为 **duplicate(次行)**; - 将次行上可对齐的字段并入主行后 **`delete` 次行**。 **主行评分** `keeperScoreForDlDedup`(分数高者优先): - 有 `player_id`:+1000 - `approve_status == "PN"`:+100 - 有 `confirmation_no`:+10 - 有 `member`:+5 同分按 **较小 `id`** 靠前(作为 keeper)。 **不合并、不删除** `shouldSkipConsolidatingDuplicateRow`: - 次行 `approve_status` 以 `W` 开头(候补等) - 次行 `revision_of` 指向 keeper(待审批修订子单) - keeper `revision_of` 指向次行 **字段合并** `mergeDuplicateBookingIntoKeeper`: - 确认号、`resv_id`、Opera 状态、酒店/客源/房价码、`player_id`、`member` 与姓名字段、房/房型、`booking_type_id`、`chain_code`、`block_id` 等在主行为空或可从次行安全补齐时写入主行;**`last_opera_status`** 若次行有值则写入主行(与 Opera 同步一致)。 日志前缀:`[consolidateDuplicateDlReservations]`。 --- ## 3. `insertDaylightBooking` 的改动要点 ### 3.1 按 `dl_reservation_id` 兜底防插 在组好 `Bookings book` 并准备 `save` **之前**,若 `operaBooking.getDlReservationId()` 非空,则先 `findByDlReservationIdIn`: - **若已存在任一行**:**不再 `save` 新单**,打日志 `[insertDaylightBooking] skip insert, existing dl_reservation_id=...`,并设置返回视图上的 `id` 等为已有记录,直接 `return operaBooking`。 避免「合并阶段漏匹配」时再次插入第二条同 `dl_reservation_id`。 ### 3.2 按确认号防重(空列表才插入) - 仅当 `confirmationNo` 非空时执行 `findByConfirmationNo`; - **仅当查询结果为空** 时才 `bookingDao.save(book)`; - 若已有同确认号记录,则复用其 `id`,**不重复插入**。 --- ## 4. 根因归纳(与旧版对比) | 原因 | 说明 | |------|------| | 预查漏掉无确认号的 `dl_reservation_id` | 已用 `LinkedHashSet` + 非空 `dlReservationId` 修复 | | 仅合并不删重复行 | 已用 `consolidateDuplicateDlReservations` 在当次同步末尾收敛 | | 插入路径无 `dl_reservation_id` 防重 / 确认号逻辑不当 | 已在 `insertDaylightBooking` 中补齐 | | 数据库无唯一约束 | **仍建议在库上对非空 `dl_reservation_id` 做过滤唯一索引**(上线前清理历史重复) | | 并发双写 | 当前以逻辑合并 + 完成后去重为主;多实例强一致可再加分布式锁或 DB 唯一约束 | --- ## 5. 运维与验证 1. **验证**:部署后对曾出现双行的会员再触发一次会走 `mergeDaylightRecordToDatabaseInfo` 的查询(如 recordSearch);观察日志中 `consolidateDuplicateDlReservations` 是否合并并删除次行。 2. **历史数据**:长期未再走合并流程的重复行,需一次性脚本或手工按业务规则保留主行、删除/归档次行。 3. **外键**:若 `delete` 次行失败(子表引用),需先迁移或删除子表记录后再删。 --- ## 6. 可选后续增强(未列入当前分支必选) - 合并匹配改为 `Strings.CI.equals`(忽略 `dl_reservation_id` 大小写),与接口/库实际存储一致。 - 对 `mergeDaylightRecordToDatabaseInfo` 增加 `@Transactional(rollbackFor = Exception.class)`,使 `saveAll` 与 `consolidateDuplicateDlReservations` 同事务回滚。 - 高并发下对同一 `dl_reservation_id` 的插入加应用层锁或依赖唯一索引 + 捕获冲突后改走更新。 --- *文档与 `daylight_1.0.0` 分支中 `BookingService` 当前实现对应;若行号随重构变化请以方法名为准。*