perf(selfop): share dashboard aggregates and cover daily trend reads

This commit is contained in:
2026-10-13 01:29:14 +08:00
parent 99d206eb00
commit d689112656
5 changed files with 64 additions and 23 deletions
@@ -23,14 +23,15 @@ class DashboardQueryServiceTest {
var p=new AnalyticsSqlRequest("demo","W",null,null,11,0,day.minusDays(14),day.plusDays(1),day,day.minusDays(30),day.minusDays(30),day,day.minusDays(30).atStartOfDay().minusHours(8),day.atStartOfDay(),true,true,false,true,false,0,20,"DASHBOARD");
when(contexts.request(snapshot,"DASHBOARD")).thenReturn(p);
when(mapper.daily(any(),any(),any())).thenReturn(List.of(new DashboardReadMapper.Day(day.minusDays(1),new BigDecimal("30"),0L)));
when(mapper.statistics(any(),any())).thenReturn(Map.of("total_skus",200L,"hot_eligible",200L,"hot_unknown",0L));
when(mapper.rankings(any(),any())).thenReturn(List.of(new DashboardReadMapper.Ranked("HOT","A","X","Product",new BigDecimal("30"),new BigDecimal("30"),1L,null,null,null,null,null,null)));
var reportRow=new HashMap<String,Object>(Map.of("total_skus",200L,"hot_eligible",200L,"hot_unknown",0L));
reportRow.putAll(Map.of("rank_code","HOT","customer_code","A","sku_code","X","product_name","Product","main_value",new BigDecimal("30"),"owner_outbound",new BigDecimal("30"),"order_count",1L));
when(mapper.report(any(),any())).thenReturn(List.of(reportRow));
var result=service.dashboard(q);
assertEquals(0,BigDecimal.ONE.compareTo(result.kpis().averageDailyOutboundQuantity30d().value()));
assertEquals(200L,result.hotSkus().eligibleCount());assertEquals(1,result.hotSkus().items().size());assertEquals("A",result.hotSkus().items().get(0).customerCode());
assertNull(result.coverageDistribution().candidateSkuCount());assertTrue(result.coverageDistribution().buckets().stream().allMatch(b->b.skuCount()==null));
assertNull(result.context().inventoryDrilldownExpiresAt());assertEquals(day.minusDays(30),result.context().fixedWindowStartDate());
verify(mapper).daily(any(),any(),any());verify(mapper).statistics(any(),any());verify(mapper).rankings(any(),any());verifyNoMoreInteractions(mapper);
verify(mapper).daily(any(),any(),any());verify(mapper).report(any(),any());verifyNoMoreInteractions(mapper);
var json=new ObjectMapper().registerModule(new JavaTimeModule()).valueToTree(result);assertTrue(json.path("context").has("salesRunId"));assertFalse(json.path("context").has("versionContext"));
assertTrue(DashboardQueryService.class.getAnnotation(Transactional.class).readOnly());
}
@@ -27,6 +27,33 @@ class DashboardSqlSemanticTest {
session=new SqlSessionFactoryBuilder().build(config).openSession();
}
@AfterEach void close()throws Exception{if(session!=null)session.close();if(connection!=null)connection.close();}
@Test void dailyBoundSqlPinsDateCoverWithoutChangingChartOrFixedWindowBindings(){
var statement=config.getMappedStatement(DashboardReadMapper.class.getName()+".daily");
for(int days:List.of(1,30,366)){
var bound=statement.getBoundSql(Map.of("p",request(),"chartFrom",D.minusDays(days-1),"chartUntil",D.plusDays(1)));
assertTrue(bound.getSql().startsWith("SELECT /*+ INDEX(ll_selfop_sku_daily_sales idx_selfop_daily_window_cover) */"));
assertEquals(List.of("p.salesRun","p.account","p.warehouse","chartFrom","chartUntil","p.from","p.until"),
bound.getParameterMappings().stream().map(ParameterMapping::getProperty).toList());
assertTrue(bound.getSql().endsWith("GROUP BY business_date ORDER BY business_date"));
}
}
@Test void combinedReportKeepsStatisticsWhenAllThreeRankingsAreEmpty()throws Exception{
var empty=report();assertEquals(1,empty.size());assertEquals(0,count(empty.get(0),"total_skus"));
assertNull(empty.get(0).get("rank_code"));assertNull(empty.get(0).get("physical_quantity_known"));
fixture("UNKNOWN",null,null,null);
var unknown=report();assertEquals(1,unknown.size());assertNull(unknown.get(0).get("rank_code"));
assertEquals(1,count(unknown.get(0),"candidate_skus"));assertEquals(1,count(unknown.get(0),"physical_quantity_unknown"));
assertNull(unknown.get(0).get("physical_quantity_known"));assertEquals(1,count(unknown.get(0),"bucket_unknown"));
}
@Test void combinedReportReturnsAtMostThirtyRowsWithoutMultiplyingStatistics()throws Exception{
for(int i=0;i<15;i++){fixture("SOLD-"+i,"30.04","30.04","30");fixture("UNSOLD-"+i,"5","5",null);}
var rows=report();assertEquals(30,rows.size());
for(var row:rows){assertEquals(30,count(row,"total_skus"));assertEquals(15,count(row,"hot_eligible"));
assertEquals(15,count(row,"long_eligible"));assertEquals(15,count(row,"no_outbound_eligible"));
assertEquals(0,new BigDecimal("525.60").compareTo((BigDecimal)row.get("physical_quantity_known")));}
for(String kind:List.of("HOT","LONG","NO_OUTBOUND"))assertEquals(10,rows.stream().filter(row->kind.equals(row.get("rank_code"))).count());
assertTrue(rows.stream().filter(row->"LONG".equals(row.get("rank_code"))).allMatch(row->new BigDecimal(row.get("main_value").toString()).compareTo(new BigDecimal("30.04"))==0));
}
@Test void sevenExclusiveBucketsConserveCandidatesAndEveryInventoryDrilldownMatches()throws Exception{
fixture("NOOUT","100","100",null);fixture("FROZEN","50","0",null);fixture("UNKNOWN",null,null,null);
fixture("THIRTY","30","30","30");fixture("THIRTY04","30.04","30.04","30");fixture("ZERO","0","0",null);
@@ -73,8 +100,13 @@ class DashboardSqlSemanticTest {
p.setLong(1,22);p.setString(2,"demo");p.setString(3,"W");p.setString(4,"C");p.setString(5,sku);p.setInt(6,0);p.setBigDecimal(7,physical==null?null:new BigDecimal(physical));p.setBigDecimal(8,available==null?null:new BigDecimal(available));p.setBigDecimal(9,BigDecimal.ZERO);p.setBigDecimal(10,BigDecimal.ZERO);p.setObject(11,D.atStartOfDay());p.setString(12,physical==null?"UNKNOWN":"VALID");p.setString(13,physical==null?"FIELD_MISSING":null);p.executeUpdate();}}
private AnalyticsSqlRequest request(){return new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,D.minusDays(30).atStartOfDay().minusHours(8),D.atStartOfDay(),true,true,true,true,true,0,20,"DASHBOARD");}
private static AnalyticsInventoryOptions options(){return new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);}
@SuppressWarnings("unchecked") private Map<String,Object> stats(){Map<String,Object> result=new HashMap<>();((Map<String,Object>)query(DashboardReadMapper.class,"statistics",Map.of("p",request(),"o",options())).get(0)).forEach((k,v)->result.put(k.toLowerCase(Locale.ROOT),v));return result;}
@SuppressWarnings("unchecked") private List<DashboardReadMapper.Ranked> ranks(){return (List<DashboardReadMapper.Ranked>)(List<?>)query(DashboardReadMapper.class,"rankings",Map.of("p",request(),"o",options()));}
@SuppressWarnings("unchecked") private List<Map<String,Object>> report(){
return ((List<Map<String,Object>>)(List<?>)query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()))).stream().map(row->{
Map<String,Object> normalized=new HashMap<>();row.forEach((key,value)->normalized.put(key.toLowerCase(Locale.ROOT),value));return normalized;
}).toList();
}
private Map<String,Object> stats(){return report().get(0);}
private List<DashboardReadMapper.Ranked> ranks(){return report().stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();}
private static long count(Map<String,Object> stats,String key){return ((Number)stats.get(key)).longValue();}
private List<?> query(Class<?> mapper,String method,Map<String,Object> args){
var original=config.getMappedStatement(mapper.getName()+"."+method);var bound=original.getBoundSql(args);var meta=org.apache.ibatis.reflection.SystemMetaObject.forObject(args);