氚云访客历史同步漏数据?分页方式、日期语义、头像容错三处全改

氚云访客同步跑了几个月,某天业务反馈”刘树明这条访客记录根本没入库”。查代码、查日志、查公网接口——数据其实是有的,但被同步逻辑漏掉了。

排查下来发现这不是一个 bug,而是三个 bug 叠加:分页方式有缺陷、日期语义理解错了、头像上传失败会阻断主数据入库。任何一个 bug 单拿出来都不会让数据丢失,三个叠在一起就稳定漏数据。

本文把三个根因、四个修复点、服务器验证步骤完整拆开,让你下次遇到第三方同步漏数据时能直接套用这套排查思路。

🎧 文章导读

🎵 背景音乐

氚云访客同步的三层漏数据问题

图1:分页方式、日期语义、头像容错——三层叠在一起才让访客漏入库

一、问题现象:业务反馈某访客”凭空消失”

业务方反馈:氚云上有”刘树明”这条访客预约记录,但在 through 库 visitor_appointment 表里查不到。

排查的过程是这样:

  1. 先查同步日志,发现刘树明的同步日志只在”已存在,跳过”那一种结果中出现——这说明同步任务跑过这条数据,但当时判定为已存在。
  2. 反查 visitor_appointment 表,确实没有 peo_id = '79a8ec3d-0fa2-4652-9faa-51a0fce6dfef' 的记录。
  3. 那”已存在”判定的依据是什么?查代码发现是按 peo_id 查的,可新数据根本没这个 peo_id,于是出现”查到已存在但实际不存在”的诡异状态。

继续往上游查,发现了三个独立但叠加的 bug。

二、根因 1:历史同步分页方式有缺陷

2.1 旧逻辑的问题

旧实现是”先取 objectId,再按 objectId 处理”的两阶段流程:

1
2
阶段一:分页拉取所有 objectId
阶段二:再按 objectId 拉详情或处理

这个流程有两个隐患:

  1. 阶段一和阶段二之间存在数据漂移:如果分页中途有新数据进入,objectId 集合可能漏或重复。
  2. 阶段二失败时没有断点保护:一旦失败,已经处理过的 objectId 和未处理的混在一起,重试可能跳过或重复。

2.2 修复方案

改为单阶段分页,直接按氚云分页响应逐页同步:

1
每页响应 → 同步该页 → 推进断点 → 拉下一页

每页内的 objectId 重复只同步一次;某页失败立即停止,且不推进 Redis 断点——避免失败数据被静默跳过。

单阶段分页的好处是”所见即所得”,分页中途的数据漂移窗口被关掉了一半。

三、根因 2:历史补跑日期语义错误

3.1 旧代码理解错了什么

氚云请求中的 startTime/endTime 字段,更接近表单创建或提交日期,并不等于返回记录中的”访客来访开始时间”。

这个差异在大部分场景下看不出来——如果表单创建日期和来访日期一致,一切正常。但刘树明这条记录刚好横跨月底:

字段
氚云查询日期归属 2026-06-30
返回 startTime 2026-07-01 00:00:00
返回 endTime 2026-09-30 00:00:00

如果按”查询日期 2026-06-30“补跑,旧代码会用 2026-06-30 作为 apply_date 入库——查询日期是创建表单的日期,不是访客实际来访日期。

3.2 修复方案

  • 历史任务按”查询日期”翻页——保持和定时任务一致;
  • 入库 apply_date/begin_date/end_date 按响应中的来访起止时间计算——这才是业务的真实日期。

修复后刘树明这条记录的入库结果是:

1
2
3
4
5
6
visitor_name: 刘树明
source: 3
peo_id: 79a8ec3d-0fa2-4652-9faa-51a0fce6dfef
apply_date: 2026-07-01 00:00:00
begin_date: 2026-07-01 00:00:00
end_date: 2026-09-30 00:00:00

source = 3 是 SOURCE_3(氚云),apply_date 用了响应的 startTime,符合业务预期。

同步任务的日期语义错位是定时任务最容易踩的坑。第三方接口的”日期字段”通常是请求参数语义,不一定是响应数据语义。

四、根因 3:头像上传失败阻断主数据入库

4.1 旧逻辑的问题

公网氚云接口响应里包含较大的头像 Base64,size=100 在本地公网链路容易触发 Feign 读取超时。同时,本地 aided-service(头像上传服务)未启动时,旧逻辑会因头像上传失败而丢弃整条访客数据

也就是说:头像上传失败 → 整条访客主数据不入库。这违反了”附属信息失败不应该阻断主数据”的原则。

4.2 修复方案

头像上传失败时只记录 WARN,主数据继续保存:

1
2
3
4
5
6
7
try {
uploadAvatar(avatarBase64);
} catch (Exception e) {
log.warn("头像上传失败,继续保存访客主数据: {}", e.getMessage());
// 继续执行下面的主数据保存逻辑
}
saveVisitorMainData(...);

附属信息和主数据的关系:头像、缩略图、附件都属于附属信息,不应阻断主数据的保存。这是容错设计里最基本的原则——核心业务数据优先于附属数据

五、本次修复的四个代码点

修复涉及的代码模块与调用关系

图2:SyncController + VisitingAircraftSyncServiceImpl + VisitorAppointmentServiceImpl 三处协同修复

5.1 SyncController

  • 历史同步改为单阶段分页,默认每页 100 条。
  • /sync/triggerHistorySync 增加可选参数:startDateendDatepageSize
  • pageSize 限制为 1~100;不传时仍为 100,不影响原定时任务。
  • 同一页出现重复 objectId 时只同步一次。
  • 当某日出现失败时立即停止,并且不推进 Redis 断点,避免失败数据被静默跳过。
  • 修正本地 through 端口注释为 18134

5.2 VisitingAircraftSyncServiceImpl

  • applyDate 优先使用氚云响应中的 startTime
  • 时间解析失败时,回退日期使用本次补跑日期,不再错误使用服务器当天日期。
  • 保留原有起止时间相等时拉开为 08:00-18:00 的逻辑。
  • 去掉重复查库去重。
  • 头像上传失败时继续保存访客主数据

5.3 VisitorAppointmentServiceImpl

  • SOURCE_3(氚云)保留上游传入的历史 applyDate
  • 其他来源继续保持旧行为:applyDate 使用当前入库时间,避免影响访客机等现有业务。

为什么只对 SOURCE_3 保留历史 applyDate?因为只有氚云这条链路是”补跑历史数据”的场景,其他来源都是实时同步,用当前入库时间更合适。容错规则要按业务场景分别设计,不能一刀切

六、本地验证结果

6.1 自动化测试

  • 共执行 11 个测试。
  • 结果:Failures: 0, Errors: 0
  • 覆盖:历史日期语义、默认日期、单阶段分页、同页去重、失败断点、成功断点、分页大小校验、头像失败继续入库。

6.2 构建

1
2
mvn -pl service-provider/through-service -am package -DskipTests
# BUILD SUCCESS

产物:

1
E:\nanwang\smart-park-cloud-0617\service-provider\through-service\target\through-service-1.0.0-SNAPSHOT.jar

6.3 真实接口和数据库验证

公网氚云接口已查到刘树明:

1
2
3
4
objectId: 79a8ec3d-0fa2-4652-9faa-51a0fce6dfef
name: 刘树明
startTime: 2026-07-01 00:00:00
endTime: 2026-09-30 00:00:00

本地补跑 2026-06-30 后数据库结果:

1
2
3
4
5
6
visitor_name: 刘树明
source: 3
peo_id: 79a8ec3d-0fa2-4652-9faa-51a0fce6dfef
apply_date: 2026-07-01 00:00:00
begin_date: 2026-07-01 00:00:00
end_date: 2026-09-30 00:00:00

重复检查未发现重复的 SOURCE_3 + peo_id 数据。

七、服务器部署验证

7.1 部署范围

只需要部署新打包的 through-service。Nacos 中氚云 host 使用服务器实际可访问的地址;如果内网代理稳定,生产环境无需因为本地公网测试而改成公网地址。

[!warning] 凭证安全
EngineSecret 是接口密钥,不要写进日志、截图或知识库。此前已经在聊天和截图中暴露,条件允许时建议在氚云后台轮换。

7.2 先做单日小范围补跑

刘树明归属于氚云查询日期 2026-06-30

1
curl -X POST 'http://127.0.0.1:18134/sync/triggerHistorySync?startDate=2026-06-30&endDate=2026-06-30&pageSize=100'

如果生产内网接口仍超时,把 pageSize 临时调小:

1
curl -X POST 'http://127.0.0.1:18134/sync/triggerHistorySync?startDate=2026-06-30&endDate=2026-06-30&pageSize=10'

公网链路本地验证时使用 pageSize=1 最稳定,但全历史补跑会很慢。

7.3 观察日志

1
2
tail -Fn0 /data/work/zhyq/logs/through/through-all.log \
| grep --line-buffered -aE '同步氚云访客|刘树明|头像上传失败|ERROR|Exception'

预期:

  • 当日同步完成且失败数为 0。
  • aided 不可用时可能出现”头像上传失败,继续保存访客主数据”WARN,但访客仍应入库。
  • 不应因单条失败继续推进该日断点。

7.4 查库

1
2
3
SELECT id, visitor_name, source, peo_id, apply_date, begin_date, end_date, create_time
FROM visitor_appointment
WHERE peo_id = '79a8ec3d-0fa2-4652-9faa-51a0fce6dfef';

预期:source=3,且三个业务时间与本地验证结果一致。

7.5 再补跑历史数据

建议按月或按年分段执行,避免单个 HTTP 请求长时间占用 Tomcat 线程。每段完成后检查同步失败数和 Redis 断点,再执行下一段。

[!warning] 断点注意事项
显式补跑较早日期会把共享 Redis 断点更新到该日期。之后无参数执行会从断点下一天继续,并通过数据库去重;部署验证期间不要并发触发多段历史补跑。

八、经验总结

8.1 同步漏数据的 5 步排查法

  1. 数据到底有没有被拉过? 看同步任务的处理日志。
  2. 如果拉过,”已存在”判定依据是什么? 查去重 SQL 的查询条件。
  3. 如果没拉过,分页是否漏了? 看分页边界、断点推进、重复 objectId 处理。
  4. 日期语义对不对? 查请求参数日期 vs 响应数据日期。
  5. 附属信息失败有没有阻断主数据? 查 try-catch 范围。

按这个顺序排查,大部分同步漏数据问题都能在 1 小时内定位。

8.2 同步任务的 4 个不可妥协点

不可妥协点 原因
失败不推进断点 避免失败数据被静默跳过
主数据与附属信息分离保存 头像失败不应阻断访客主数据
日期语义必须验证 请求日期 ≠ 响应日期时不能想当然
同页去重只一次 objectId 重复时按”一次”处理,避免重复插入

8.3 补跑历史数据的 3 个最佳实践

  1. 单日先验:先补跑 1 天,确认日志和数据库都符合预期,再补跑更大范围。
  2. 分段执行:按月或按年分段,每段完成后检查失败数和断点,再执行下一段。
  3. 不并发补跑:多个补跑请求会互相覆盖 Redis 断点,导致数据丢失或重复。

8.4 同步链路的可观测性建议

  • 每条同步记录的处理路径(拉取 → 去重 → 入库 → 标记状态)应该有明确日志。
  • 失败原因应该分门别类:分页失败 vs 去重失败 vs 入库失败 vs 头像失败。
  • Redis 断点应该有专门的 metric 或日志,方便运维观察”当前同步到哪里了”。

九、结语

第三方同步漏数据是看起来”很神秘的 bug”——因为日志里有这条数据的处理记录,但数据库里就是没有。这次排查下来,其实就是分页、日期、容错三个老问题叠加在一起。

同步链路的设计原则

  • 单阶段优于两阶段;
  • 日期语义要按响应数据来;
  • 主数据优先于附属数据;
  • 失败要显式,不要静默跳过。

下次再遇到第三方同步漏数据,按这套排查思路走一遍,大概率能在 1 小时内定位。

同步链路没有银弹,只有把每一层的失败模式都想到、补上对应的处理逻辑。