From cebcdfb66ed4eb1e8662d2bed9e3ef5834f7a61f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 13 Oct 2026 13:43:13 +0800 Subject: [PATCH] fix: preserve complete historical dashboard days --- .../analytics/query/DashboardSalesPolicy.java | 4 ++- .../analytics/query/DashboardPolicyTest.java | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java index 2eb1fbff9..721dbff5c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java @@ -17,7 +17,9 @@ public final class DashboardSalesPolicy { Instant upper=c.salesCoveredUntil()==null?null:(c.salesCoveredUntil().isBefore(end)?c.salesCoveredUntil():end); boolean intersects=lower!=null&&upper!=null&&lower.isBefore(upper); boolean invalid=actual!=null&&actual.invalidRowCount()!=null&&actual.invalidRowCount()>0; - boolean trusted="VALID".equals(c.salesStatus())&&!c.rebuildRequired(); + // Global recency describes how far collection has progressed, not whether an + // already published historical day has complete coverage. Keep STALE on context. + boolean trusted=("VALID".equals(c.salesStatus())||"STALE".equals(c.salesStatus()))&&!c.rebuildRequired(); boolean complete=sourceKnown&&trusted&&!invalid&&day.isBefore(c.anchorDate())&&c.completeDayFrom()!=null&&c.completeDayUntil()!=null &&!day.isBefore(c.completeDayFrom())&&day.isBefore(c.completeDayUntil())&&intersects&&lower.equals(start)&&upper.equals(end); BigDecimal known=sourceKnown&&actual!=null?actual.observedQuantity():null; diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java index e19d6b5ba..9a15b4252 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java @@ -24,6 +24,36 @@ class DashboardPolicyTest { assertEquals("PARTIAL",points.get(1).dataStatus());assertEquals(BigDecimal.ZERO,points.get(1).observedQuantity());assertNull(points.get(1).outboundQuantity()); var unknown=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(context(null,null,false),false,DAY.minusDays(1),DAY.plusDays(1),List.of()));assertNull(unknown.observedQuantity()); } + @Test void staleCollectionDoesNotDowngradePublishedHistoricalDayOrInventOtherFourteenDays(){ + LocalDate published=DAY.minusDays(3);var c=publishedDayContext("STALE",false); + for(BigDecimal amount:List.of(BigDecimal.ZERO,new BigDecimal("2251"))) { + var rows=amount.signum()==0?List.of():List.of(new DashboardReadMapper.Day(published,amount,0L)); + var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(14),DAY.plusDays(1),rows); + var point=points.stream().filter(p->p.date().equals(published)).findFirst().orElseThrow(); + assertEquals("COMPLETE",point.dataStatus());assertEquals(amount,point.outboundQuantity()); + assertFalse(point.reasonCodes().contains("SALES_DAY_INCOMPLETE"));assertFalse(point.reasonCodes().contains("SALES_STALE")); + var summary=SalesBucketPolicy.aggregate(points); + assertEquals(1,summary.completeDays());assertEquals(14,summary.unknownDays());assertEquals(0,summary.partialDays()); + assertEquals("PARTIAL",summary.dataStatus());assertNull(summary.outboundQuantity());assertEquals(amount,summary.observedQuantity()); + var single=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(c,true,published,published.plusDays(1),rows)); + assertEquals("COMPLETE",single.dataStatus());assertEquals(amount,single.outboundQuantity()); + assertEquals("STALE",c.salesStatus());assertTrue(c.reasonCodes().contains("SALES_STALE")); + } + } + @Test void historicalRecencyFixDoesNotRelaxInvalidQuantitySourceOrRebuildGuards(){ + LocalDate day=DAY.minusDays(3);var rows=List.of(new DashboardReadMapper.Day(day,new BigDecimal("2251"),0L)); + assertEquals("UNKNOWN",DashboardSalesPolicy.points(publishedDayContext("STALE",false),false,day,day.plusDays(1),rows).get(0).dataStatus()); + var invalid=DashboardSalesPolicy.points(publishedDayContext("STALE",false),true,day,day.plusDays(1),List.of(new DashboardReadMapper.Day(day,new BigDecimal("2"),1L))).get(0); + assertEquals("PARTIAL",invalid.dataStatus());assertNull(invalid.outboundQuantity());assertTrue(invalid.reasonCodes().contains("SALES_QUANTITY_OR_UNIT_INVALID")); + var rebuilding=DashboardSalesPolicy.points(publishedDayContext("REBUILD_REQUIRED",true),true,day,day.plusDays(1),rows).get(0); + assertEquals("PARTIAL",rebuilding.dataStatus());assertNull(rebuilding.outboundQuantity()); + } + private static AnalyticsResponses.Context publishedDayContext(String status,boolean rebuild){ + LocalDate day=DAY.minusDays(3);var zone=ZoneId.of("Asia/Shanghai"); + return new AnalyticsResponses.Context(11L,22L,"a".repeat(64),"b".repeat(64),NOW,NOW.plusSeconds(1800),"Asia/Shanghai",DAY, + DAY.minusDays(14),DAY.plusDays(1),null,day,day.plusDays(1),day.atStartOfDay(zone).toInstant(),day.plusDays(1).atStartOfDay(zone).toInstant(), + NOW.minusSeconds(60),NOW.minusSeconds(60),NOW.minusSeconds(30),status,"VALID",rebuild,"CURRENT",List.of("SALES_"+status)); + } @Test void strictInventoryDrilldownExpiresBeforeContextWhileOrdinaryInventoryRemainsAllowed(){ var c=context(NOW.minusSeconds(86400),NOW,true);var q=query(true);var strict=new AnalyticsInventoryOptions(null,null,null,null,"UNKNOWN",CoverageDistributionPolicy.RULE_VERSION,null,"DASHBOARD",null,null); Instant fresh=NOW.plusSeconds(60);assertEquals(fresh,CoverageDistributionPolicy.expiresAt(c,fresh));