From d6891126560efddd668276e5fe14776b5d951ac6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 13 Oct 2026 01:29:14 +0800 Subject: [PATCH] perf(selfop): share dashboard aggregates and cover daily trend reads --- .../query/DashboardQueryService.java | 9 ++++- .../analytics/query/DashboardReadMapper.java | 10 +++--- .../analytics/query/DashboardReadSql.java | 25 ++++++------- .../query/DashboardQueryServiceTest.java | 7 ++-- .../query/DashboardSqlSemanticTest.java | 36 +++++++++++++++++-- 5 files changed, 64 insertions(+), 23 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java index c21fd504b..0d885c455 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java @@ -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 stats=normalize(mapper.statistics(p,o));var ranked=mapper.rankings(p,o); + var report=mapper.report(p,o).stream().map(DashboardQueryService::normalize).toList(); + Map 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 normalize(Map source){Map result=new HashMap<>();if(source!=null)source.forEach((key,value)->result.put(key.toLowerCase(Locale.ROOT),value));return result;} + static DashboardReadMapper.Ranked ranked(Map 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 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 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);} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java index 30e1ec4af..fc2924807 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java @@ -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 daily(@Param("p") AnalyticsSqlRequest request,@Param("chartFrom") LocalDate chartFrom,@Param("chartUntil") LocalDate chartUntil); - @SelectProvider(type=DashboardReadSql.class,method="statistics") - Map statistics(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options); - @SelectProvider(type=DashboardReadSql.class,method="rankings") - List rankings(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options); + @SelectProvider(type=DashboardReadSql.class,method="report") + List> report(@Param("p") AnalyticsSqlRequest request,@Param("o") AnalyticsInventoryOptions options); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java index ecd198ae4..318d82c1b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java @@ -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 args){ + public static String report(Map 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 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;} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java index b0ddd192a..3baa60bfc 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java @@ -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(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()); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java index 187e8d624..517ce442d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java @@ -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 stats(){Map result=new HashMap<>();((Map)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 ranks(){return (List)(List)query(DashboardReadMapper.class,"rankings",Map.of("p",request(),"o",options()));} + @SuppressWarnings("unchecked") private List> report(){ + return ((List>)(List)query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()))).stream().map(row->{ + Map normalized=new HashMap<>();row.forEach((key,value)->normalized.put(key.toLowerCase(Locale.ROOT),value));return normalized; + }).toList(); + } + private Map stats(){return report().get(0);} + private List ranks(){return report().stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();} private static long count(Map stats,String key){return ((Number)stats.get(key)).longValue();} private List query(Class mapper,String method,Map args){ var original=config.getMappedStatement(mapper.getName()+"."+method);var bound=original.getBoundSql(args);var meta=org.apache.ibatis.reflection.SystemMetaObject.forObject(args);