feat(selfop): provide frozen turnover batches for backend Excel exports

This commit is contained in:
2026-10-15 15:00:03 +08:00
parent 22d79e08f9
commit dbb93a06c2
18 changed files with 546 additions and 9 deletions
@@ -16,6 +16,7 @@ import com.baiyue.lastleg.fleet.quote.FleetQuoteModels;
import com.baiyue.lastleg.fleet.quote.FleetQuoteService;
import com.baiyue.lastleg.fleet.reporting.TripOperationsModels;
import com.baiyue.lastleg.fleet.reporting.TripOperationsService;
import com.baiyue.lastleg.selfoperated.analytics.query.InventoryTurnoverExport;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
@@ -41,6 +42,7 @@ public class LastlegExportProviderService {
@Resource private VehicleApplicationService vehicles;
@Resource private FleetQuoteService fleetQuotes;
@Resource private TripOperationsService operations;
@Resource private InventoryTurnoverExport turnover;
@Resource private ObjectMapper json;
@Resource private Validator validator;
@Value("${baiyue.security.internal-service-token}") private String signingKey;
@@ -51,15 +53,16 @@ public class LastlegExportProviderService {
}
if (request.taskId() == null || request.requestedBy() == null || request.dispatchVersion() < 1
|| request.limit() < 1 || request.limit() > 200) throw invalid("导出批次参数无效");
List<ExportColumn> columns;
try { columns = LastlegExportColumns.columns(type); }
catch (IllegalArgumentException e) { throw invalid("不支持该导出类型"); }
int offset = cursor(request.cursor());
if (offset % request.limit() != 0) throw invalid("导出游标无效");
String snapshot = signature(type, request);
boolean first = request.snapshotToken() == null;
if ((first && offset != 0) || (!first && !MessageDigest.isEqual(snapshot.getBytes(StandardCharsets.UTF_8),
request.snapshotToken().getBytes(StandardCharsets.UTF_8)))) throw invalid("导出筛选快照不匹配");
if (InventoryTurnoverExport.TYPE.equals(type)) return turnover.batch(request, offset, snapshot);
List<ExportColumn> columns;
try { columns = LastlegExportColumns.columns(type); }
catch (IllegalArgumentException e) { throw invalid("不支持该导出类型"); }
Map<String, Object> parameters = new TreeMap<>(request.parameters() == null ? Map.of() : request.parameters());
String customer = text(parameters.remove("customerId"));
if (customer != null && !customer.matches("[1-9][0-9]*")) throw invalid("客户参数无效");
@@ -0,0 +1,133 @@
package com.baiyue.lastleg.selfoperated.analytics.query;
import com.baiyue.common.core.export.ExportContracts.*;
import com.baiyue.common.core.exception.BizException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import static com.baiyue.lastleg.selfoperated.analytics.query.InventoryTurnoverResponses.*;
/** Download-center projection: the browser supplies column keys, never headings or cell values. */
@Service
public class InventoryTurnoverExport {
public static final String TYPE = "SELFOP_INVENTORY_TURNOVER";
private final InventoryTurnoverQueryService queries;
private final ObjectMapper json;
public InventoryTurnoverExport(InventoryTurnoverQueryService queries, ObjectMapper json) { this.queries=queries; this.json=json; }
private static final Map<String,String> LABELS = Map.ofEntries(
Map.entry("sku","SKU"), Map.entry("productName","商品名称"), Map.entry("customerCode","所属货主"),
Map.entry("physicalQuantity","实物件数"), Map.entry("availableQuantity","正品可售"), Map.entry("transitQuantity","在途"),
Map.entry("frozenQuantity","冻结件数"), Map.entry("defectiveQuantity","次品实物"), Map.entry("physicalSkuCount","有实物SKU数"),
Map.entry("inventoryVolumeM3","库存立方数"), Map.entry("outboundVolumesM3","出库立方数"), Map.entry("unitVolumeM3","单件体积"),
Map.entry("outbound7","7日出库"), Map.entry("outbound15","15日出库"), Map.entry("outbound30","30日出库"),
Map.entry("averageDailyOutboundQuantity","日均"), Map.entry("inventoryCoverageDays","预计可售天数"),
Map.entry("quantityTurnoverRate","期间周转率"), Map.entry("quantityTurnoverCount","期间件数周转"),
Map.entry("averagePhysicalQuantity","期间平均实物"), Map.entry("lastObservedOutboundDate","最近已观测出库日"),
Map.entry("noOutboundInWindow","无出库SKU / 实物"), Map.entry("slowCoverage","慢消化SKU / 待判断"),
Map.entry("period","统计期间"), Map.entry("periodOutbound","期间出库件数"), Map.entry("historySource","历史库存来源"),
Map.entry("historyDays","合格历史库存天数"), Map.entry("historyReason","历史库存说明"));
private static final List<String> REQUIRED = List.of("period","periodOutbound","averagePhysicalQuantity","quantityTurnoverRate","historySource","historyDays","historyReason");
public ExportBatchResponse batch(ExportBatchRequest request, int offset, String signature) {
if(request.limit()>100) throw invalid("库存周转每批最多100条");
Map<String,Object> parameters=new LinkedHashMap<>(request.parameters()==null?Map.of():request.parameters());
Object rawColumns=parameters.remove("columns"), rawTotal=parameters.remove("expectedTotal");
if(!(rawColumns instanceof List<?> selected)||selected.isEmpty()||selected.size()>LABELS.size()) throw invalid("导出列无效");
Set<String> keys=new LinkedHashSet<>();
for(Object key:selected) if(!(key instanceof String s)||!LABELS.containsKey(s)||!keys.add(s)) throw invalid("导出列重复或不支持");
keys.addAll(REQUIRED);
long expected;
try { expected=new BigDecimal(String.valueOf(rawTotal)).longValueExact(); }
catch(RuntimeException e) { throw invalid("导出条数无效"); }
if(expected<0||expected>200000||offset>expected) throw invalid("导出条数超出范围");
if(parameters.containsKey("current")||parameters.containsKey("size")||parameters.containsKey("includeOverview")) throw invalid("导出不接受列表分页参数");
InventoryTurnoverRequest.validateParameters(parameters.keySet());
parameters.put("current",offset/request.limit()+1);parameters.put("size",request.limit());parameters.put("includeOverview",false);
InventoryTurnoverRequest query;
try { query=json.convertValue(parameters,InventoryTurnoverRequest.class); }
catch(IllegalArgumentException e) { throw invalid("导出筛选无效"); }
if(query.snapshotContext()==null||query.snapshotContext().isBlank()||query.endDate()==null||query.baseWindowDays()==null) throw invalid("请先查询后导出");
boolean owner="OWNER".equals(query.group());
if(owner&&(keys.contains("sku")||keys.contains("productName")||keys.contains("unitVolumeM3")||keys.contains("inventoryCoverageDays"))
||!owner&&(keys.contains("physicalSkuCount")||keys.contains("noOutboundInWindow")||keys.contains("slowCoverage"))) throw invalid("导出列不适用于当前分组");
Response result=queries.query(query);
if(result.total()!=expected||!query.snapshotContext().equals(result.context().snapshotContext())
||result.rows().size()!=Math.min(request.limit(),expected-offset)) throw invalid("导出数据范围或版本已变化,请整体刷新后重新导出");
Set<List<String>> seen=new HashSet<>();
for(Row row:result.rows()) if(!seen.add(Arrays.asList(row.customerCode(),row.sku()))) throw invalid("导出数据重复");
List<ExportColumn> columns=keys.stream().map(key->new ExportColumn(key,title(key,query),ExportColumnType.STRING,24)).toList();
List<Map<String,Object>> rows=result.rows().stream().map(row->{
Map<String,Object> values=new LinkedHashMap<>();for(String key:keys)values.put(key,cell(row,key,query));return values;
}).toList();
String hash;
try { hash="sha256:"+HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(json.writeValueAsBytes(columns))); }
catch(Exception e) { throw new IllegalStateException("Cannot hash turnover export columns",e); }
long next=offset+rows.size();
return new ExportBatchResponse(1,hash,signature,request.snapshotToken()==null?columns:List.of(),rows,String.valueOf(next),next<expected,request.snapshotToken()==null?expected:null);
}
private static String title(String key,InventoryTurnoverRequest q) {
if(key.equals("outboundVolumesM3"))return q.days()+"日出库立方数";
if(key.equals("averageDailyOutboundQuantity"))return q.days()+"日日均";
return LABELS.get(key);
}
static String cell(Row r,String key,InventoryTurnoverRequest q) {
return switch(key) {
case "sku" -> text(r.sku()); case "productName" -> text(r.productName());
case "customerCode" -> r.customerName()==null||r.customerName().isBlank()?text(r.customerCode()):r.customerName();
case "physicalQuantity" -> total(r.physicalQuantity()); case "availableQuantity" -> total(r.availableQuantity());
case "transitQuantity" -> total(r.transitQuantity()); case "frozenQuantity" -> total(r.frozenQuantity()); case "defectiveQuantity" -> total(r.defectiveQuantity());
case "physicalSkuCount" -> number(r.physicalSkuCount(),2);
case "inventoryVolumeM3" -> volume(r.inventoryVolumeM3(),false);
case "outboundVolumesM3" -> volume(r.outboundVolumesM3().stream().filter(v->v.days()==q.days()).map(VolumeWindow::volume).findFirst().orElse(null),true);
case "unitVolumeM3" -> number(r.unitVolumeM3(),9)+(r.unitVolumeM3()==null?"":" m³")+";"+switch(text(r.dimensionSource())) { case "WAREHOUSE_MEASURED" -> "仓库实测尺寸";case "PRODUCT_DECLARED" -> "商品尺寸估算";default -> "缺少有效尺寸"; };
case "outbound7","outbound15","outbound30" -> window(r,Integer.parseInt(key.substring(8)));
case "periodOutbound" -> window(r,q.days());
case "averageDailyOutboundQuantity" -> number(r.averageDailyOutboundQuantity(),2);
case "inventoryCoverageDays" -> number(r.inventoryCoverageDays(),1)+";"+reason(r.coverageStatus());
case "averagePhysicalQuantity" -> number(r.averagePhysicalQuantity(),2);
case "quantityTurnoverRate" -> r.quantityTurnoverCount()==null?"—":number(r.quantityTurnoverCount().multiply(BigDecimal.valueOf(100)),2)+"%";
case "quantityTurnoverCount" -> r.quantityTurnoverCount()==null?"—":number(r.quantityTurnoverCount(),2)+" 次/"+q.days()+"日";
case "period" -> "截至 "+q.endDate()+",最近 "+q.days()+" 天";
case "historySource" -> "库存流水还原(日末实物库存)";
case "historyDays" -> r.qualifiedSampleDays()+"/"+r.requiredSampleDays();
case "historyReason" -> reasons(r.sampleReasonCodes());
case "lastObservedOutboundDate" -> r.lastObservedOutboundDate()==null?"已采集历史内未见出库":r.lastObservedOutboundDate().toString();
case "noOutboundInWindow" -> number(r.noOutboundInWindow().matchedSkuCount(),2)+" SKU / "+number(r.noOutboundInWindow().knownPhysicalQuantity(),2)+" 件;待判断 "+r.noOutboundInWindow().unknownSkuCount()+" SKU";
case "slowCoverage" -> q.coverageDaysThreshold()==null?"未设查询阈值":number(r.slowCoverage().matchedSkuCount(),2)+" SKU / "+r.slowCoverage().unknownSkuCount()+" SKU";
default -> throw invalid("不支持的导出列");
};
}
static String number(BigDecimal value,int decimals) {
if(value==null)return "—";
BigDecimal threshold=BigDecimal.ONE.movePointLeft(decimals);
if(value.signum()>0&&value.compareTo(threshold)<0)return "<"+threshold.setScale(decimals).toPlainString();
var f=new java.text.DecimalFormat("#,##0"+(decimals>0?"."+"#".repeat(decimals):""),java.text.DecimalFormatSymbols.getInstance(Locale.ROOT));
f.setRoundingMode(RoundingMode.HALF_UP);return f.format(value);
}
private static String total(Total t) {
if(t.value()!=null)return number(t.value(),2);
if(t.knownSubtotal()==null&&t.unknownSkuCount()==0)return reasons(t.reasonCodes());
return "已知 "+number(t.knownSubtotal(),2)+";"+t.unknownSkuCount()+" SKU待核验";
}
private static String window(Row r,int days) {
return r.windows().stream().filter(w->w.days()==days).findFirst().map(w->"COMPLETE".equals(w.dataStatus())?number(w.outboundQuantity(),2):"已采集 "+number(w.observedQuantity(),2)+",覆盖不足").orElse("—");
}
private static String volume(Volume v,boolean outbound) {
if(v==null)return "—;数量或尺寸待确认";
List<String> notes=new ArrayList<>();
if(v.value()==null)notes.add(v.knownSubtotal()==null?"暂无已知小计":"已知小计 "+number(v.knownSubtotal(),6)+" m³");
if(v.missingDimensionSkuCount()>0)notes.add("缺尺寸 "+v.missingDimensionSkuCount()+" SKU / "+number(v.missingDimensionQuantity(),2)+" 件");
if(v.unknownQuantitySkuCount()>0)notes.add("数量待确认 "+v.unknownQuantitySkuCount()+" SKU");
if(v.declaredDimensionQuantity()!=null&&v.declaredDimensionQuantity().signum()>0)notes.add("其中 "+number(v.declaredDimensionQuantity(),2)+" 件按商品尺寸估算");
if(outbound)notes.add("按当前商品尺寸估算");
else if(v.value()!=null&&v.value().signum()==0)notes.add("已知体积为0");
else if(v.value()!=null&&(v.declaredDimensionQuantity()==null||v.declaredDimensionQuantity().signum()==0))notes.add("按仓库实测尺寸计算");
return number(v.value(),6)+(v.value()==null?"":" m³")+";"+(notes.isEmpty()?"数量或尺寸待确认":String.join(";",notes));
}
private static String reasons(List<String> reasons){return reasons==null||reasons.isEmpty()?"—":String.join("、",reasons.stream().map(InventoryTurnoverExport::reason).toList());}
private static String reason(String reason){return TurnoverExportReasons.LABELS.getOrDefault(reason==null?"":reason,"数据待核验");}
private static String text(String value){return value==null||value.isBlank()?"—":value;}
private static BizException invalid(String message){return new BizException(message);}
}
@@ -0,0 +1,154 @@
package com.baiyue.lastleg.selfoperated.analytics.query;
import java.util.Map;
/** Business explanations retained in server-generated spreadsheets. */
final class TurnoverExportReasons {
private TurnoverExportReasons() {}
static final Map<String,String> LABELS = Map.ofEntries(
Map.entry("COMPLETE","完整"),
Map.entry("PARTIAL","未完整"),
Map.entry("UNKNOWN","未知"),
Map.entry("QUEUED","排队中"),
Map.entry("RUNNING","执行中"),
Map.entry("WAITING","等待中"),
Map.entry("WAITING_UPSTREAM","等待上游"),
Map.entry("WAITING_SOURCE","等待来源数据"),
Map.entry("RETRY_WAIT","待重试"),
Map.entry("YIELDED","预算等待"),
Map.entry("FAILED","失败"),
Map.entry("DEAD","待处理"),
Map.entry("SUCCEEDED","执行成功"),
Map.entry("ROUTED","账号共享采集完成"),
Map.entry("SAMPLE_DONE","测试样本结束"),
Map.entry("PUBLISHED","已发布"),
Map.entry("REUSED","沿用已有结果"),
Map.entry("NOT_PUBLISHED","未发布"),
Map.entry("PENDING","待处理"),
Map.entry("BLOCKED","修订阻断"),
Map.entry("OFF","关闭"),
Map.entry("VALIDATION","验证采样"),
Map.entry("BUSINESS","正式采集"),
Map.entry("FRESH","有效"),
Map.entry("STALE","过期"),
Map.entry("INVALID","异常"),
Map.entry("OBSERVING","观察中"),
Map.entry("HEALTHY","正常"),
Map.entry("STALE_HEARTBEAT","心跳过期,执行状态待确认"),
Map.entry("UNCONFIRMED","执行状态待确认"),
Map.entry("TERMINATED","尝试已结束"),
Map.entry("ABORTED","已停止"),
Map.entry("NOT_STARTED","尚未开始"),
Map.entry("FETCHING","采集中"),
Map.entry("FETCHED","采集完成"),
Map.entry("VALIDATING","校验中"),
Map.entry("BUILDING","计算中"),
Map.entry("READY","待发布"),
Map.entry("INITIALIZING","首采中"),
Map.entry("COVERED","覆盖完整"),
Map.entry("ACCOUNT_DISCOVERY","账号共享出库目录"),
Map.entry("WAREHOUSE_SYNC","本仓采集"),
Map.entry("VALID","有效"),
Map.entry("CURRENT","已核对"),
Map.entry("LAGGING","核对滞后"),
Map.entry("MISSING","尚无可用数据"),
Map.entry("UNCOVERED","覆盖不足"),
Map.entry("INCOMPLETE","未完整"),
Map.entry("REBUILD_REQUIRED","销量待重新统计发布"),
Map.entry("NO_AVAILABLE_INVENTORY","无可售库存"),
Map.entry("UNIT_UNKNOWN_OR_CONFLICT","数量单位未知或不一致"),
Map.entry("SALES_UNTRUSTED","销量待核对"),
Map.entry("COVERAGE_INSUFFICIENT","覆盖不足"),
Map.entry("OBSERVATION_INSUFFICIENT","观察期不足"),
Map.entry("SALES_UNKNOWN","销量未知"),
Map.entry("NO_OUTBOUND_IN_WINDOW","所选窗口无出库"),
Map.entry("NO_OUTBOUND_30_DAYS","近30天无出库"),
Map.entry("INVENTORY_INVALID","库存异常"),
Map.entry("INVENTORY_STALE","库存快照过期"),
Map.entry("INVENTORY_UNKNOWN","库存未知"),
Map.entry("UNASSESSABLE_SKUS","存在待评估SKU"),
Map.entry("INVENTORY_UNAVAILABLE","库存资料不可用"),
Map.entry("SALES_UNAVAILABLE","销量资料不可用"),
Map.entry("PHYSICAL_INVENTORY_UNKNOWN","实物库存待核验"),
Map.entry("AVAILABLE_INVENTORY_UNKNOWN","可售库存待核验"),
Map.entry("SALES_QUANTITY_OR_UNIT_INVALID","出库数量或单位异常"),
Map.entry("NO_ELIGIBLE_SKUS","暂无符合条件的SKU"),
Map.entry("NO_OUTBOUND_30D","近30天无出库"),
Map.entry("COVERAGE_GT_THRESHOLD","可售天数超过本次阈值"),
Map.entry("FROZEN_STOCK","存在冻结库存"),
Map.entry("DEFECTIVE_STOCK","存在次品实物库存"),
Map.entry("UNASSESSABLE","数据不足/待观察"),
Map.entry("THRESHOLD_NOT_SET","未设置查询阈值"),
Map.entry("NO_PHYSICAL_INVENTORY","无实物库存"),
Map.entry("METRIC_DEPENDENCY_UNKNOWN","该指标依赖资料未知"),
Map.entry("SALES_WINDOW_UNAVAILABLE","同期完整销量不足"),
Map.entry("SALES_WINDOW_INCOMPLETE","所选时段销量尚未采齐"),
Map.entry("RISK_DEPENDENCY_UNKNOWN","部分SKU关注判断未知"),
Map.entry("NO_OUTBOUND_IN_COLLECTED_HISTORY","已采集历史内未见出库"),
Map.entry("SALES_NOT_COVERED","销量未覆盖"),
Map.entry("SKU_HISTORY_UNKNOWN","SKU历史未知"),
Map.entry("QUANTITY_UNIT_UNKNOWN","数量单位未知"),
Map.entry("INVENTORY_NOT_PUBLISHED","库存尚未发布"),
Map.entry("SALES_NOT_PUBLISHED","销量尚未发布"),
Map.entry("OBSERVATION_INCOMPLETE","观察期不足"),
Map.entry("SCOPE_CHANGED","配置范围已变化"),
Map.entry("UPSTREAM_NOT_READY","账号共享上游未就绪"),
Map.entry("TASK_NOT_RETRYABLE","任务当前不可恢复"),
Map.entry("COLLECTION_MODE_NOT_BUSINESS","当前不是正式采集模式"),
Map.entry("VALIDATION_NOT_RETRYABLE","验证会话不可恢复"),
Map.entry("ACCOUNT_SHARED_TASK","账号共享任务由协调器恢复"),
Map.entry("SOURCE_TIMEZONE_UNKNOWN","来源时区未确认"),
Map.entry("SALES_DAY_INCOMPLETE","销量日期未完整"),
Map.entry("SALES_NOT_COVERED_OR_SKU_HISTORY_UNKNOWN","销量未覆盖或SKU历史未知"),
Map.entry("SCOPE_LEASE_BUSY","本仓任务正在执行"),
Map.entry("COLLECTION_OFF","采集模式关闭"),
Map.entry("COLLECTION_MODE_INVALID","采集模式配置无效"),
Map.entry("BUSINESS_ENVIRONMENT_REJECTED","正式采集只允许生产环境"),
Map.entry("EXECUTION_DISABLED","采集执行未启用"),
Map.entry("WAREHOUSE_NOT_CONFIGURED","仓库未配置"),
Map.entry("TIMEZONE_OR_UNIT_NOT_CONFIRMED","时间或件数口径配置不符合确认值"),
Map.entry("ACCOUNT_PAUSED","账号采集已暂停"),
Map.entry("ACCOUNT_COOLDOWN","账号处于冷却期"),
Map.entry("EXCLUSIVE_GRANT_NOT_VALID","账号独占窗口授权无效"),
Map.entry("DAILY_BUDGET_NOT_CONFIGURED","账号日额度未配置"),
Map.entry("DAILY_BUDGET_EXHAUSTED","账号日额度已用完"),
Map.entry("SALES_MISSING","尚无销量发布版本"),
Map.entry("INVENTORY_MISSING","尚无库存发布版本"),
Map.entry("SALES_BLOCKED","销量修订阻断"),
Map.entry("SALES_STALE","销量数据过期"),
Map.entry("REVISION_REVIEW_UNKNOWN","修订核对状态未知"),
Map.entry("REVISION_REVIEW_LAGGING","修订核对滞后"),
Map.entry("SALES_REBUILD_REQUIRED","销量有变动,待重新统计发布"),
Map.entry("TODAY_INCOMPLETE","今日尚未结束,不能作为完整自然日"),
Map.entry("SALES_SOURCE_TIMEZONE_UNKNOWN","销量来源时区未确认"),
Map.entry("INDEPENDENT_ANCHOR_MISSING","缺少独立库存对账记录"),
Map.entry("INDEPENDENT_ANCHOR_QUALITY_MISSING","独立库存品质记录不完整"),
Map.entry("INDEPENDENT_ANCHOR_INVALID","独立库存记录无效"),
Map.entry("INDEPENDENT_ANCHOR_COVERAGE_MISSING","库存对账区间流水未齐"),
Map.entry("INDEPENDENT_ANCHOR_MISMATCH","流水与独立库存记录不符"),
Map.entry("INDEPENDENT_ANCHOR_FLOW_UNKNOWN","库存对账流水待核验"),
Map.entry("ANCHOR_TIME_AMBIGUOUS","采集库存时发生变动,待稳定库存记录核验"),
Map.entry("ANCHOR_MISSING","缺少库存锚点"),
Map.entry("FLOW_COVERAGE_GAP","库存流水覆盖不完整"),
Map.entry("FLOW_TYPE_UNVERIFIED","流水类型尚未核验"),
Map.entry("CHAIN_DISCONTINUITY","批次结存不连续"),
Map.entry("CHAIN_ORDER_AMBIGUOUS","同秒流水顺序待核验"),
Map.entry("FLOW_QUANTITY_OR_TIME_INVALID","流水数量或时间待核验"),
Map.entry("FLOW_QUANTITY_CONFLICT","流水变动数量不一致"),
Map.entry("NEGATIVE_CHAIN_STOCK","批次结存出现负数"),
Map.entry("NEGATIVE_RECONSTRUCTED_STOCK","还原库存出现负数"),
Map.entry("UNIT_UNKNOWN","库存数量单位待确认"),
Map.entry("ANCHOR_DUPLICATE","库存锚点重复"),
Map.entry("ANCHOR_QUANTITY_INVALID","库存锚点数量无效"),
Map.entry("ANCHOR_INTERVAL_INVALID","库存观测时间无效"),
Map.entry("ANCHOR_BEFORE_DAY_END","库存锚点早于日末"),
Map.entry("FLOW_ID_INVALID","流水标识无效"),
Map.entry("FLOW_ID_CONFLICT","流水标识存在冲突"),
Map.entry("UNKNOWN_QUALITY","库存品质待确认"),
Map.entry("CHAIN_IDENTITY_MISSING","流水库位或批次缺失"),
Map.entry("INSUFFICIENT","合格日末库存记录不足"),
Map.entry("ZERO_AVERAGE_STOCK","期间平均日末库存为0,无法计算周转"),
Map.entry("INVENTORY_UNKNOWN_OR_STALE","当前库存未知、异常或已过期"),
Map.entry("NOT_APPLICABLE","货主不计算整体可售天数"),
Map.entry("SALES_OR_STOCK_UNASSESSABLE","部分商品的销量或库存不足以判断"));
}