fix(daily-volume): 历史快照的 partial 自动修正 + 三个回归测试

=== 现象 ===
仓库里 6 条快照 partial 全是 true(10-27 到 10-30),前端把昨天的柱子
也画成虚线空心,看着像"每天都在采样中"。

=== 根因 ===
早期实现把两件事都塞进 partial:「今天还没过完」和「起点超出 24h」。
10-27/10-28 的数据现在已经被 retention 删掉、永远不会再被抓一次,
所以这两条错误标记会永久留在库里。

=== 修法 ===
prune 里加幂等修正:过去的天,只有快照是**在那天结束之后**才抓的
(说明抓到的是完整一天)才改成 partial=false;当天中途抓的继续保持 partial
(那时确实只有半天,不能伪装成完整数据)。

=== 测试(3/3 PASS)===
TestPruneDailyVolumeNormalizesPartialAndDropsExpired
  · 昨天结束之后抓的 → partial=false
  · 前天中途抓的     → 保持 partial=true
  · 今天             → 保持 partial=true
  · 超留存期         → 被清理
  · 条数不被清理流程清空
TestUpsertVolumeSnapshotKeepsEntries  刷新体积不清空已抓到的条数
TestGetDailyVolumeHistoryReturnsAllDaysWithMissing  始终返回 days 个点,缺的标 Missing 不伪造 0

go vet ./services/ ./models/ 干净;go test ./services/ 全绿。
This commit is contained in:
opsx committed 2026-10-30 16:56:39 +00:00
1 parent b7de337c9e
commit 63dd491c5b
2 files changed
+164 -3

No files matched your search

+28 -3
View File
@@ -301,24 +301,49 @@ func CaptureRecentDailyVolumes() {
pruneDailyVolumeSnapshots()
}
// pruneDailyVolumeSnapshots 丢弃超过保留期的历史。
// pruneDailyVolumeSnapshots 丢弃超过保留期的历史,并修正历史遗留的 partial 标记。
func pruneDailyVolumeSnapshots() {
db := repository.GetDB()
if db == nil {
return
}
cutoff := dateKey(time.Now().AddDate(0, 0, -dailyVolumeRetentionDays))
today := dateKey(time.Now())
db.Lock()
defer db.Unlock()
changed := false
kept := make([]models.DailyVolumeSnapshot, 0, len(db.DailyVolumeSnapshots))
for _, snap := range db.DailyVolumeSnapshots {
if snap.Date >= cutoff {
kept = append(kept, snap)
if snap.Date < cutoff {
continue
}
kept = append(kept, snap)
}
if len(kept) != len(db.DailyVolumeSnapshots) {
log.Printf("[daily-volume] 清理过期快照 %d 条", len(db.DailyVolumeSnapshots)-len(kept))
db.DailyVolumeSnapshots = kept
changed = true
}
// 历史遗留修正:**过去的天不可能"还在进行中"**。
// 早期版本把「起点超出 24h」也当成 partial,于是昨天/前天的柱子一直是虚线空心,
// 看起来像"每天都在采样中"。只有当快照是在那天**结束之后**才抓的
// (说明抓的是完整一天)才改成完整;当天中途抓到的继续保持 partial —— 那时确实只有半天。
for i := range db.DailyVolumeSnapshots {
snap := &db.DailyVolumeSnapshots[i]
if !snap.Partial || snap.Date >= today {
continue
}
day, err := time.ParseInLocation("2006-01-02", snap.Date, time.Local)
if err != nil {
continue
}
if _, dayEnd := dayRange(day); snap.CapturedAt.UnixNano() >= dayEnd {
snap.Partial = false
changed = true
}
}
if changed {
db.RequestSave()
}
}