Merge remote-tracking branch 'origin/test' into test

This commit is contained in:
2026-10-14 20:27:06 +08:00
7 changed files with 106 additions and 10 deletions
@@ -22,7 +22,7 @@ public class InventoryTurnoverQueryService {
var frozen=contexts.resolve(request);var input=InventoryTurnoverSql.input(request,frozen);var context=frozen.response();
var names=ownerDirectory.names(request.sourceAccount(),context.queryAsOf());
long total=0;boolean hasTotal=false;Overview summary=null;Charts charts=null;
List<Row> rows=new ArrayList<>();List<OwnerPhysical> owners=new ArrayList<>();int unknown=0;
List<Row> rows=new ArrayList<>();List<OwnerPhysical> owners=new ArrayList<>();List<OwnerVolume> volumeOwners=new ArrayList<>();int unknown=0,unknownVolume=0;
for(var part:mapper.report(input)) {
var envelope=normalized(part);Map<String,Object> value;
try {
@@ -37,12 +37,14 @@ public class InventoryTurnoverQueryService {
case "OVERVIEW" -> summary=overview(value,context);
case "OWNER" -> owners.add(new OwnerPhysical(text(value,"customer_code"),text(value,"customer_name"),decimal(value,"physical")));
case "OWNER_META" -> unknown=count(value,"unknown_count");
case "OWNER_VOLUME" -> volumeOwners.add(new OwnerVolume(text(value,"customer_code"),text(value,"customer_name"),volume(value,"physical")));
case "OWNER_VOLUME_META" -> unknownVolume=count(value,"unknown_count");
default -> throw new IllegalStateException("Unknown turnover query section");
}
}
if(!hasTotal||request.overview()&&summary==null)throw new IllegalStateException("Missing turnover query section");
if(request.overview()) {
charts=new Charts(List.copyOf(owners),unknown,windows(normalized(mapper.chartWindows(InventoryTurnoverSql.chartInput(request,frozen))),context.endDate().plusDays(1),request.windows()));
charts=new Charts(List.copyOf(owners),unknown,List.copyOf(volumeOwners),unknownVolume,windows(normalized(mapper.chartWindows(InventoryTurnoverSql.chartInput(request,frozen))),context.endDate().plusDays(1),request.windows()));
}
return new Response(context,summary,charts,List.copyOf(rows),total);
}
@@ -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("翻页必须携带首屏上下文");
@@ -31,6 +31,8 @@ public final class InventoryTurnoverResponses {
public record Overview(Total physicalQuantity,Total availableQuantity,Total noOutboundSkuCount,Total unassessableSkuCount,
Volume inventoryVolumeM3,List<VolumeWindow> outboundVolumesM3) {}
public record OwnerPhysical(String customerCode,String customerName,BigDecimal physicalQuantity) {}
public record Charts(List<OwnerPhysical> ownerPhysicalTop,int unknownOwnerCount,List<Window> averageDaily) {}
public record OwnerVolume(String customerCode,String customerName,Volume inventoryVolumeM3) {}
public record Charts(List<OwnerPhysical> ownerPhysicalTop,int unknownOwnerCount,List<OwnerVolume> ownerVolumeTop,
int unknownVolumeOwnerCount,List<Window> averageDaily) {}
public record Response(Context context,Overview summary,Charts charts,List<Row> rows,long total) {}
}
@@ -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";
@@ -270,22 +273,27 @@ public final class InventoryTurnoverSql {
for(String field:List.of("physical","available","frozen","defective","transit"))for(String suffix:List.of("known","unknown"))fields.add(field+"_"+suffix);
for(String risk:List.of("no_outbound","slow"))for(String suffix:List.of("count","physical","unknown"))fields.add(risk+"_"+suffix);
} else fields.addAll(List.of("sku_code","product_name","coverage_days","no_outbound","slow","unit_volume","dimension_source"));
// The physical-stock chart needs owner totals, not owner sales/sample windows.
// Stock charts share owner totals, independently of list filters and pagination.
String sql=cte(p)+(owner?ownerCte(p):overview?", owner_totals AS ("+totals(p,"customer_code",true)+" FROM sku_result GROUP BY customer_code) ":"")
+", page_result AS (SELECT r.*,ROW_NUMBER() OVER ("+order(p,owner)+") ordinal FROM "+source+" r"+filter(p,owner)+order(p,owner)+" LIMIT #{p.query.pageSize} OFFSET #{p.query.offset})";
sql+=pageMissingDates();
if(overview)sql+=", summary_result AS ("+totals(p,null,false)+" FROM sku_result), top_owners AS (SELECT customer_code,physical,ROW_NUMBER() OVER (ORDER BY physical DESC,customer_code) ordinal FROM owner_totals WHERE physical IS NOT NULL ORDER BY physical DESC,customer_code LIMIT 10)";
if(overview)sql+=", summary_result AS ("+totals(p,null,false)+" FROM sku_result), top_owners AS (SELECT customer_code,physical,ROW_NUMBER() OVER (ORDER BY physical DESC,customer_code) ordinal FROM owner_totals WHERE physical IS NOT NULL ORDER BY physical DESC,customer_code LIMIT 10)"
+", top_volume_owners AS (SELECT t.*,ROW_NUMBER() OVER (ORDER BY physical_volume DESC,customer_code) ordinal FROM owner_totals t WHERE physical_volume IS NOT NULL ORDER BY physical_volume DESC,customer_code LIMIT 10)";
// Statement-local only: share intermediate relations across report branches;
// do not change connection/global settings or any result/filter semantics.
sql+=" SELECT /*+ SET_VAR(group_concat_max_len=16384) SET_VAR(optimizer_switch='derived_merge=off') SET_VAR(optimizer_search_depth=4) */ 'ROW' section,ordinal,"+jsonFields(fields)+" payload FROM page_missing"
+" UNION ALL SELECT 'TOTAL' section,0 ordinal,JSON_OBJECT('total',COUNT(*)) payload FROM "+source+filter(p,owner);
if(overview) {
List<String> ownerVolumeFields=new ArrayList<>(List.of("customer_code"));
InventoryVolumeSql.SUFFIXES.forEach(suffix->ownerVolumeFields.add("physical_"+suffix));
List<String> summary=new ArrayList<>(List.of("sku_count","unassessable","no_outbound_count","no_outbound_unknown"));
summary.addAll(InventoryVolumeSql.fields(p));
for(String field:List.of("physical","available")){summary.add(field);summary.add(field+"_known");summary.add(field+"_unknown");}
sql+=" UNION ALL SELECT 'OVERVIEW' section,0 ordinal,"+jsonFields(summary)+" payload FROM summary_result"
+" UNION ALL SELECT 'OWNER' section,ordinal,JSON_OBJECT('customer_code',customer_code,'physical',physical) payload FROM top_owners"
+" UNION ALL SELECT 'OWNER_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical IS NULL";
+" UNION ALL SELECT 'OWNER_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical IS NULL"
+" UNION ALL SELECT 'OWNER_VOLUME' section,ordinal,"+jsonFields(ownerVolumeFields)+" payload FROM top_volume_owners"
+" UNION ALL SELECT 'OWNER_VOLUME_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical_volume IS NULL";
}
return sql+" ORDER BY section,ordinal";
}