From ca4e7086e91f9868d8601d04edb54e2c8b1e3f40 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 13 Oct 2026 03:45:01 +0800 Subject: [PATCH] perf(selfop): skip unavailable dashboard aggregates --- .../query/AnalyticsInventorySql.java | 13 ++++--- .../analytics/query/DashboardReadSql.java | 7 +++- .../query/DashboardSqlSemanticTest.java | 39 ++++++++++++++++++- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java index 09fb0048b..9bb0def94 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java @@ -17,7 +17,9 @@ public final class AnalyticsInventorySql { private static String defectiveQuantity(){return "CASE WHEN MIN(CASE WHEN product_quality NOT IN (0,1) THEN 0 WHEN product_quality=1 THEN CASE WHEN "+valid("physical_quantity")+" THEN 1 ELSE 0 END ELSE 1 END)=1 " +"THEN SUM(CASE WHEN product_quality=1 THEN physical_quantity ELSE 0 END) END";} static String cte(AnalyticsSqlRequest p){return cte(p,false);} - private static String cte(AnalyticsSqlRequest p,boolean stockFirst){ + static String dashboardCte(AnalyticsSqlRequest p){return cte(p,false,true);} + private static String cte(AnalyticsSqlRequest p,boolean stockFirst){return cte(p,stockFirst,false);} + private static String cte(AnalyticsSqlRequest p,boolean stockFirst,boolean dashboard){ return "WITH candidates AS ("+keys(p)+"), profiles AS (SELECT c.customer_code,c.sku_code,COALESCE(i.product_name,s.product_name) product_name," +"CASE WHEN i.quantity_unit='PCS' THEN 1 ELSE 0 END stock_unit_valid," +"CASE WHEN s.sku_code IS NULL OR s.quantity_unit='PCS' THEN 1 ELSE 0 END sales_unit_valid," @@ -29,9 +31,10 @@ public final class AnalyticsInventorySql { +" stock AS (SELECT customer_code,sku_code,"+quantity("physical_quantity")+" physical_quantity,"+quantity("frozen_quantity")+" frozen_quantity," +quantity("transit_quantity")+" transit_quantity,MAX(CASE WHEN product_quality=0 AND "+valid("available_quantity")+" THEN available_quantity END) available_quantity," +defectiveQuantity()+" defective_physical_quantity,MAX(observed_at_utc) observed_at_utc " - +"FROM ll_selfop_inventory_snapshot WHERE snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code)," + +"FROM ll_selfop_inventory_snapshot WHERE "+(dashboard&&!p.stockFresh()?"1=0 /* dashboard unused stock */ AND ":"") + +"snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code)," +(stockFirst?stockPageKeys():"") - +salesCte(p,stockFirst) + +salesCte(p,stockFirst,dashboard&&!p.complete30()) +" values_by_sku AS (SELECT c.customer_code,c.sku_code,c.product_name,c.unit_valid," +"CASE WHEN c.sales_evidence IS NULL THEN c.stock_evidence WHEN c.stock_evidence IS NULL THEN c.sales_evidence ELSE LEAST(c.sales_evidence,c.stock_evidence) END evidence_at_utc," +stockValue("physical_quantity")+","+stockValue("available_quantity")+","+stockValue("frozen_quantity")+","+stockValue("transit_quantity")+","+stockValue("defective_physical_quantity")+",st.observed_at_utc," @@ -47,12 +50,12 @@ public final class AnalyticsInventorySql { +"WHEN physical_quantity>0 AND available_quantity>0 AND outbound_quantity30d IS NOT NULL THEN CASE WHEN outbound_quantity30d>0 AND available_quantity*30>outbound_quantity30d*#{o.coverageDaysThreshold} THEN 1 ELSE 0 END END above_threshold " +"FROM qualified q), result AS (SELECT c.*,CASE WHEN physical_quantity IS NULL OR available_quantity IS NULL OR outbound_quantity30d IS NULL THEN 1 ELSE 0 END unassessable FROM calculated c) "; } - private static String salesCte(AnalyticsSqlRequest p,boolean stockFirst){String d=stockFirst?"d.":""; + private static String salesCte(AnalyticsSqlRequest p,boolean stockFirst,boolean unused){String d=stockFirst?"d.":""; return " sales AS (SELECT "+(stockFirst?"/*+ JOIN_ORDER(k,d) INDEX(d idx_selfop_daily_sku_cover) */ ":"") +d+"customer_code,"+d+"sku_code,SUM("+d+"outbound_quantity) observed_quantity,SUM("+d+"outbound_order_count) order_count," +"MIN(CASE WHEN "+d+"quantity_unit='PCS' AND "+d+"outbound_quantity>=0 THEN 1 ELSE 0 END) unit_valid FROM ll_selfop_sku_daily_sales " +(stockFirst?"d JOIN stock_paged k ON k.customer_code=d.customer_code AND k.sku_code=d.sku_code ":"") - +"WHERE "+d+"sales_run_id=#{p.salesRun} AND"+scope(p,d)+" AND "+d+"business_date>=#{p.from} AND "+d+"business_date<#{p.until} GROUP BY "+d+"customer_code,"+d+"sku_code),"; + +"WHERE "+(unused?"1=0 /* dashboard unused sales */ AND ":"")+d+"sales_run_id=#{p.salesRun} AND"+scope(p,d)+" AND "+d+"business_date>=#{p.from} AND "+d+"business_date<#{p.until} GROUP BY "+d+"customer_code,"+d+"sku_code),"; } private static String stockAmount(String field){return "CASE WHEN c.stock_unit_valid=1 AND #{p.stockFresh}=1 THEN st."+field+" END";} private static String stockValue(String field){return stockAmount(field)+" "+field;} 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 318d82c1b..69143387c 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 @@ -7,7 +7,12 @@ import java.util.Map; public final class DashboardReadSql { private DashboardReadSql(){} public static String report(Map args){ - var p=(AnalyticsSqlRequest)args.get("p");StringBuilder sql=new StringBuilder(AnalyticsInventorySql.cte(p)); + var p=(AnalyticsSqlRequest)args.get("p"); + // Only dashboard outputs are considered here. Without complete30 all sales-dependent + // rankings are empty; observed sales still come from the independent daily query. + // Stale stock quantities are already masked to NULL. Keep both profile versions so + // candidate identities, observation evidence and unknown reasons remain unchanged. + StringBuilder sql=new StringBuilder(AnalyticsInventorySql.dashboardCte(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)"); 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 517ce442d..054e9e939 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 @@ -45,6 +45,32 @@ class DashboardSqlSemanticTest { 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 unavailableAggregateBranchesPreserveEveryReportValueAndIndependentObservedDailySales()throws Exception{ + fixture("SOLD","90","60","30");fixture("UNSOLD","10","10",null); + fixture("NOAVAILABLE","5","0",null);fixture("ZERO","0","0",null);fixture("UNKNOWN",null,null,null); + profile(11,"A","SAME","PCS");profile(11,"B","SAME","BOX"); + daily("A","SAME",D.minusDays(1),"7","PCS");daily("B","SAME",D.minusDays(1),"3","BOX"); + profile(22,"C","STOCKONLY","PCS");stock("STOCKONLY","4","4"); + daily("C","SOLD",D.minusDays(2),"-1","PCS"); + for(boolean complete:List.of(false,true))for(boolean fresh:List.of(false,true)){ + var p=request(complete,fresh);var args=Map.of("p",p,"o",options()); + String sql=DashboardReadSql.report(args); + assertEquals(!complete,sql.contains("1=0 /* dashboard unused sales */ AND ")); + assertEquals(!fresh,sql.contains("1=0 /* dashboard unused stock */ AND ")); + String ordinary=AnalyticsInventorySql.cte(p); + assertFalse(ordinary.contains("dashboard unused")); + assertEquals(ordinary,AnalyticsInventorySql.dashboardCte(p) + .replace("1=0 /* dashboard unused sales */ AND ","").replace("1=0 /* dashboard unused stock */ AND ","")); + assertTrue(sql.contains("profile_run_id=#{p.salesRun}"));assertTrue(sql.contains("profile_run_id=#{p.inventoryRun}")); + var actual=query(DashboardReadMapper.class,"report",args); + var original=query(DashboardReadMapper.class,"report",args,true); + assertEquals(new HashSet<>(original),new HashSet<>(actual),"complete="+complete+", fresh="+fresh); + assertEquals(original.size(),actual.size()); + var days=query(DashboardReadMapper.class,"daily",Map.of("p",p,"chartFrom",D.minusDays(2),"chartUntil",D)); + var yesterday=days.stream().map(DashboardReadMapper.Day.class::cast).filter(day->day.businessDate().equals(D.minusDays(1))).findFirst().orElseThrow(); + assertEquals(0,new BigDecimal("37").compareTo(yesterday.observedQuantity()));assertEquals(1L,yesterday.invalidRowCount()); + } + } @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()); @@ -99,6 +125,7 @@ class DashboardSqlSemanticTest { private void stock(String sku,String physical,String available)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_inventory_snapshot VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)")){ 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 AnalyticsSqlRequest request(boolean complete,boolean fresh){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),complete,complete,fresh,p.offset(),p.size(),p.kind());} private static AnalyticsInventoryOptions options(){return new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);} @SuppressWarnings("unchecked") private List> report(){ return ((List>)(List)query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()))).stream().map(row->{ @@ -109,12 +136,22 @@ class DashboardSqlSemanticTest { 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){ + return query(mapper,method,args,false); + } + private List query(Class mapper,String method,Map args,boolean originalAggregates){ var original=config.getMappedStatement(mapper.getName()+"."+method);var bound=original.getBoundSql(args);var meta=org.apache.ibatis.reflection.SystemMetaObject.forObject(args); StringBuilder sql=new StringBuilder();int index=0;boolean quoted=false; for(int i=0;i