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,7 +23,8 @@ public class DashboardQueryService {
base.completeFrom(),base.completeUntil(),base.coveredFrom(),base.coveredUntil(),base.salesKnown(),base.formalSales(),base.complete30(),base.complete30(),base.stockFresh(),0,10,"DASHBOARD");
var o=new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);
var days=mapper.daily(p,c.rangeStartDate(),c.rangeEndDateExclusive());
Map<String,Object> stats=normalize(mapper.statistics(p,o));var ranked=mapper.rankings(p,o);
var report=mapper.report(p,o).stream().map(DashboardQueryService::normalize).toList();
Map<String,Object> stats=report.get(0);var ranked=report.stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();
var trendPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),c.rangeStartDate(),c.rangeEndDateExclusive(),days);
var trendSummary=SalesBucketPolicy.aggregate(trendPoints);
var fixedPoints=DashboardSalesPolicy.points(c,snapshot.sourceKnown(),fixedFrom,fixedUntil,days);var fixed=SalesBucketPolicy.aggregate(fixedPoints);
@@ -84,6 +85,12 @@ public class DashboardQueryService {
return new DashboardResponses.Ranking(from,until,30,available?count(s,prefix+"_eligible"):null,available?unknown:null,items,status,reasons);
}
private static Map<String,Object> normalize(Map<String,Object> source){Map<String,Object> result=new HashMap<>();if(source!=null)source.forEach((key,value)->result.put(key.toLowerCase(Locale.ROOT),value));return result;}
static DashboardReadMapper.Ranked ranked(Map<String,Object> row){
return new DashboardReadMapper.Ranked((String)row.get("rank_code"),(String)row.get("customer_code"),(String)row.get("sku_code"),(String)row.get("product_name"),
decimal(row,"main_value"),decimal(row,"owner_outbound"),row.get("order_count")==null?null:decimal(row,"order_count").longValueExact(),
decimal(row,"physical_quantity"),decimal(row,"available_quantity"),decimal(row,"frozen_quantity"),decimal(row,"defective_physical_quantity"),
decimal(row,"outbound_quantity30d"),decimal(row,"coverage_days"));
}
private static BigDecimal decimal(Map<String,Object> source,String key){Object value=source.get(key);return value==null?null:value instanceof BigDecimal decimal?decimal:new BigDecimal(value.toString());}
private static long count(Map<String,Object> source,String key){BigDecimal value=decimal(source,key);return value==null?0:value.longValueExact();}
private static BigDecimal divide30(BigDecimal value){return value==null?null:value.divide(BigDecimal.valueOf(30),12,RoundingMode.HALF_UP);}
@@ -12,13 +12,13 @@ public interface DashboardReadMapper {
record Ranked(String rankCode,String customerCode,String skuCode,String productName,BigDecimal mainValue,
BigDecimal ownerOutbound,Long orderCount,BigDecimal physicalQuantity,BigDecimal availableQuantity,
BigDecimal frozenQuantity,BigDecimal defectivePhysicalQuantity,BigDecimal outboundQuantity30d,BigDecimal coverageDays){}
@Select("SELECT business_date,SUM(CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN outbound_quantity END) observed_quantity,"
// Dense 366-day EXPLAIN: the natural unique-index plan performs non-covering reads;
// the date-ordered covering index also matches warehouse daily GROUP BY/ORDER BY.
@Select("SELECT /*+ INDEX(ll_selfop_sku_daily_sales idx_selfop_daily_window_cover) */ business_date,SUM(CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN outbound_quantity END) observed_quantity,"
+"COUNT(CASE WHEN quantity_unit IS NULL OR quantity_unit<>'PCS' OR outbound_quantity IS NULL OR outbound_quantity<0 THEN 1 END) invalid_row_count "
+"FROM ll_selfop_sku_daily_sales WHERE sales_run_id=#{p.salesRun} AND source_account=#{p.account} AND warehouse_code=#{p.warehouse} "
+"AND ((business_date>=#{chartFrom} AND business_date<#{chartUntil}) OR (business_date>=#{p.from} AND business_date<#{p.until})) GROUP BY business_date ORDER BY business_date")
List<Day> daily(@Param("p") AnalyticsSqlRequest request,@Param("chartFrom") LocalDate chartFrom,@Param("chartUntil") LocalDate chartUntil);
@SelectProvider(type=DashboardReadSql.class,method="statistics")
Map<String,Object> statistics(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options);
@SelectProvider(type=DashboardReadSql.class,method="rankings")
List<Ranked> rankings(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options);
@SelectProvider(type=DashboardReadSql.class,method="report")
List<Map<String,Object>> report(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options);
}
@@ -3,13 +3,16 @@ package com.baiyue.lastleg.selfoperated.analytics.query;
import java.util.List;
import java.util.Map;
/** Three bounded aggregate queries: warehouse daily points, one statistics row, at most 30 ranked rows. */
/** One shared inventory/sales aggregate returns statistics and at most 30 ranked rows. */
public final class DashboardReadSql {
private DashboardReadSql(){}
public static String statistics(Map<String,Object> args){
public static String report(Map<String,Object> args){
var p=(AnalyticsSqlRequest)args.get("p");StringBuilder sql=new StringBuilder(AnalyticsInventorySql.cte(p));
// SKU identity is unique already; DISTINCT is a MySQL merge barrier so the four consumers
// share one calculation rather than expanding profile/stock/sales joins for each ranking.
sql.append(", dashboard_result AS (SELECT DISTINCT * FROM result)");
sql.append(", classified AS (SELECT r.*,CASE WHEN ").append(CoverageDistributionSql.candidate()).append(" THEN ")
.append(CoverageDistributionSql.bucket()).append(" END bucket_code,").append(CoverageDistributionSql.unknownReason()).append(" unknown_reason FROM result r) SELECT COUNT(*) total_skus");
.append(CoverageDistributionSql.bucket()).append(" END bucket_code,").append(CoverageDistributionSql.unknownReason()).append(" unknown_reason FROM dashboard_result r), statistics AS (SELECT COUNT(*) total_skus");
for(String field:List.of("physical_quantity","available_quantity"))sql.append(",SUM(").append(field).append(") ").append(field).append("_known,COUNT(*)-COUNT(").append(field).append(") ").append(field).append("_unknown");
sql.append(",COUNT(CASE WHEN physical_quantity>0 THEN 1 END) physical_skus,COUNT(CASE WHEN physical_quantity=0 THEN 1 END) excluded_zero_skus")
.append(",COUNT(CASE WHEN bucket_code IS NOT NULL THEN 1 END) candidate_skus")
@@ -24,15 +27,13 @@ public final class DashboardReadSql {
}
for(String reason:List.of("PHYSICAL_INVENTORY_UNKNOWN","AVAILABLE_INVENTORY_UNKNOWN","UNIT_UNKNOWN_OR_CONFLICT","SALES_WINDOW_UNAVAILABLE","OBSERVATION_INSUFFICIENT","METRIC_DEPENDENCY_UNKNOWN"))
sql.append(",COUNT(CASE WHEN bucket_code='UNKNOWN' AND unknown_reason='").append(reason).append("' THEN 1 END) reason_").append(reason.toLowerCase(java.util.Locale.ROOT));
return AnalyticsInventorySql.typed(sql+" FROM classified");
}
public static String rankings(Map<String,Object> args){var p=(AnalyticsSqlRequest)args.get("p");
String sql=AnalyticsInventorySql.cte(p)
+", hot_rank AS (SELECT * FROM result WHERE owner_outbound>0 ORDER BY owner_outbound DESC,order_count DESC,sku_code ASC,customer_code ASC LIMIT 10)"
+", long_rank AS (SELECT * FROM result WHERE "+CoverageDistributionSql.finitePositive()+" ORDER BY coverage_days DESC,sku_code ASC,customer_code ASC LIMIT 10)"
+", no_rank AS (SELECT * FROM result WHERE no_outbound=1 ORDER BY physical_quantity DESC,sku_code ASC,customer_code ASC LIMIT 10) ";
sql+=select("HOT","owner_outbound","hot_rank")+" UNION ALL "+select("LONG","coverage_days","long_rank")+" UNION ALL "+select("NO_OUTBOUND","physical_quantity","no_rank");
return AnalyticsInventorySql.typed(sql);
sql.append(" FROM classified)")
.append(", hot_rank AS (SELECT * FROM dashboard_result WHERE owner_outbound>0 ORDER BY owner_outbound DESC,order_count DESC,sku_code ASC,customer_code ASC LIMIT 10)")
.append(", long_rank AS (SELECT * FROM dashboard_result WHERE ").append(CoverageDistributionSql.finitePositive()).append(" ORDER BY coverage_days DESC,sku_code ASC,customer_code ASC LIMIT 10)")
.append(", no_rank AS (SELECT * FROM dashboard_result WHERE no_outbound=1 ORDER BY physical_quantity DESC,sku_code ASC,customer_code ASC LIMIT 10)")
.append(", rankings AS (").append(select("HOT","owner_outbound","hot_rank")).append(" UNION ALL ").append(select("LONG","coverage_days","long_rank"))
.append(" UNION ALL ").append(select("NO_OUTBOUND","physical_quantity","no_rank")).append(") SELECT s.*,r.* FROM statistics s LEFT JOIN rankings r ON 1=1");
return AnalyticsInventorySql.typed(sql.toString());
}
private static String select(String kind,String value,String table){return "SELECT '"+kind+"' rank_code,customer_code,sku_code,product_name,"+value+" main_value,owner_outbound,order_count,physical_quantity,available_quantity,frozen_quantity,defective_physical_quantity,outbound_quantity30d,coverage_days FROM "+table;}
}
@@ -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);