diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java index 2115afd66..af69a9088 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java @@ -43,8 +43,9 @@ public record InventoryTurnoverRequest(String sourceAccount, String warehouseCod "OUTBOUND_7_ASC","OUTBOUND_7_DESC","OUTBOUND_15_ASC","OUTBOUND_15_DESC","OUTBOUND_30_ASC","OUTBOUND_30_DESC", "AVERAGE_DAILY_ASC","AVERAGE_DAILY_DESC","COVERAGE_ASC","COVERAGE_DESC", "TRANSIT_ASC","TRANSIT_DESC","FROZEN_ASC","FROZEN_DESC","DEFECTIVE_ASC","DEFECTIVE_DESC","AVERAGE_PHYSICAL_ASC","AVERAGE_PHYSICAL_DESC", + "INVENTORY_VOLUME_ASC","INVENTORY_VOLUME_DESC","OUTBOUND_VOLUME_ASC","OUTBOUND_VOLUME_DESC","UNIT_VOLUME_ASC","UNIT_VOLUME_DESC", "TURNOVER_ASC","TURNOVER_DESC","NO_OUTBOUND_PHYSICAL_DESC").contains(sort()) - || "OWNER".equals(group())&&sort().startsWith("COVERAGE_") + || "OWNER".equals(group())&&(sort().startsWith("COVERAGE_")||sort().startsWith("UNIT_VOLUME_")) || "SKU".equals(group())&&"NO_OUTBOUND_PHYSICAL_DESC".equals(sort())) throw new BizException("排序不适用于当前分组"); if(page()<1||pageSize()<1||pageSize()>100) throw new BizException("分页范围为current≥1,size为1至100"); if(snapshotContext==null&&page()!=1) throw new BizException("翻页必须携带首屏上下文"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java index 1340d3767..7b127f5f5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java @@ -215,6 +215,9 @@ public final class InventoryTurnoverSql { } private static String order(Input p,boolean owner) { String field=switch(p.query.sort()) { + case "INVENTORY_VOLUME_ASC","INVENTORY_VOLUME_DESC" -> "physical_volume"; + case "OUTBOUND_VOLUME_ASC","OUTBOUND_VOLUME_DESC" -> "w"+p.query.days()+"_outbound_volume"; + case "UNIT_VOLUME_ASC","UNIT_VOLUME_DESC" -> "unit_volume"; case "PHYSICAL_ASC","PHYSICAL_DESC" -> "physical"; case "AVAILABLE_ASC","AVAILABLE_DESC" -> "available"; case "OUTBOUND_7_ASC","OUTBOUND_7_DESC" -> "w7_outbound"; diff --git a/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml b/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml index ed5465c5b..4c64264db 100644 --- a/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml @@ -41,8 +41,8 @@ paths: - {name: turnoverStatus, in: query, schema: {type: string, enum: [INSUFFICIENT]}} - name: sortBy in: query - description: 'OWNER禁止COVERAGE_ASC/DESC,SKU禁止NO_OUTBOUND_PHYSICAL_DESC。全量筛选后按正式未舍入值排序再分页,未知值始终末尾,同值按SKU及货主升序。AVERAGE_DAILY使用baseWindowDays指定的日均窗口。' - schema: {type: string, default: PHYSICAL_DESC, enum: [PHYSICAL_ASC, PHYSICAL_DESC, AVAILABLE_ASC, AVAILABLE_DESC, OUTBOUND_7_ASC, OUTBOUND_7_DESC, OUTBOUND_15_ASC, OUTBOUND_15_DESC, OUTBOUND_30_ASC, OUTBOUND_30_DESC, AVERAGE_DAILY_ASC, AVERAGE_DAILY_DESC, COVERAGE_ASC, COVERAGE_DESC, TRANSIT_ASC, TRANSIT_DESC, FROZEN_ASC, FROZEN_DESC, DEFECTIVE_ASC, DEFECTIVE_DESC, AVERAGE_PHYSICAL_ASC, AVERAGE_PHYSICAL_DESC, TURNOVER_ASC, TURNOVER_DESC, NO_OUTBOUND_PHYSICAL_DESC]} + description: 'OWNER禁止COVERAGE_ASC/DESC及UNIT_VOLUME_ASC/DESC,SKU禁止NO_OUTBOUND_PHYSICAL_DESC。全量筛选后按正式未舍入值排序再分页,未知值始终末尾,同值按SKU及货主升序。AVERAGE_DAILY及OUTBOUND_VOLUME使用baseWindowDays指定的窗口;体积按正式值排序,不使用已知小计。' + schema: {type: string, default: PHYSICAL_DESC, enum: [PHYSICAL_ASC, PHYSICAL_DESC, AVAILABLE_ASC, AVAILABLE_DESC, OUTBOUND_7_ASC, OUTBOUND_7_DESC, OUTBOUND_15_ASC, OUTBOUND_15_DESC, OUTBOUND_30_ASC, OUTBOUND_30_DESC, AVERAGE_DAILY_ASC, AVERAGE_DAILY_DESC, COVERAGE_ASC, COVERAGE_DESC, TRANSIT_ASC, TRANSIT_DESC, FROZEN_ASC, FROZEN_DESC, DEFECTIVE_ASC, DEFECTIVE_DESC, AVERAGE_PHYSICAL_ASC, AVERAGE_PHYSICAL_DESC, TURNOVER_ASC, TURNOVER_DESC, INVENTORY_VOLUME_ASC, INVENTORY_VOLUME_DESC, OUTBOUND_VOLUME_ASC, OUTBOUND_VOLUME_DESC, UNIT_VOLUME_ASC, UNIT_VOLUME_DESC, NO_OUTBOUND_PHYSICAL_DESC]} - {name: current, in: query, schema: {type: integer, minimum: 1, default: 1}} - {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}} - {name: includeOverview, in: query, schema: {type: boolean, default: true}} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java index f95cf7fed..fd02d7d8c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java @@ -33,6 +33,31 @@ class InventoryTurnoverSqlTest { private void product(String owner,String sku,String declared,String measured) throws Exception { execute("INSERT INTO ll_selfop_product VALUES(901,'demo','"+owner+"','"+sku+"',"+declared+","+measured+")"); } + @Test void volumeSortsUseSelectedWindowFormalValuesBeforePagingWithStableTiesAndUnknownLast() throws Exception { + master(); + for(String owner:List.of("A","B","C","D"))seed(owner,"SAME",0); + for(String owner:List.of("A","B","D"))product(owner,"SAME",("B".equals(owner)?"2":"1")+",1,1,'m'","NULL,NULL,NULL,NULL"); + execute("UPDATE ll_selfop_sku_daily_sales SET outbound_quantity=CASE WHEN customer_code='A' THEN CASE WHEN business_date>=DATE '"+END.minusDays(2)+"' THEN 100 ELSE 0 END WHEN customer_code='B' THEN CASE WHEN business_date>=DATE '"+END.minusDays(2)+"' THEN 1 ELSE 100 END ELSE 10 END"); + for(String group:List.of("SKU","OWNER"))for(String metric:List.of("INVENTORY_VOLUME","OUTBOUND_VOLUME","UNIT_VOLUME")) { + if(group.equals("OWNER")&&metric.equals("UNIT_VOLUME"))continue; + for(int days:List.of(3,30))for(String direction:List.of("ASC","DESC")) { + var expected=metric.equals("OUTBOUND_VOLUME")&&days==3 + ? direction.equals("ASC")?List.of("B","D","A","C"):List.of("A","D","B","C") + : direction.equals("ASC")?List.of("A","D","B","C"):List.of("B","A","D","C"); + var actual=new ArrayList(); + for(int page=1;page<=2;page++) { + var q=new InventoryTurnoverRequest("demo","W",END,days,group,null,null,"ALL",null,null,metric+"_"+direction,page,2,true,"fixed"); + q.validate(END.plusDays(1)); + actual.addAll(reportRows(input(q,30,0,true)).stream().map(r->r.get("customer_code")).toList()); + } + assertEquals(expected,actual,group+" "+metric+" "+days+" "+direction); + } + } + var incomplete=reportRows(input(request("SKU","ALL","OUTBOUND_VOLUME_DESC",null,null,1,20),7,0,true)); + assertEquals(List.of("A","B","C","D"),incomplete.stream().map(r->r.get("customer_code")).toList()); + assertTrue(incomplete.stream().allMatch(r->r.get("w30_outbound_volume")==null)); + for(String direction:List.of("ASC","DESC"))assertThrows(RuntimeException.class,()->request("OWNER","ALL","UNIT_VOLUME_"+direction,null,null,1,20).validate(END.plusDays(1))); + } @Test void emptyUncoveredOutboundVolumeIsUnknownAndCompleteEmptyVolumesAreZero() throws Exception { var q=request("SKU","ALL","PHYSICAL_DESC",null,null,1,20); var unknown=query("overview",input(q,0,0,true)).get(0);