|
|
@@ -0,0 +1,522 @@
|
|
|
+package com.dtm.lifecycle.service.impl;
|
|
|
+
|
|
|
+import com.dtm.lifecycle.domain.LifecycleSkuDailySales;
|
|
|
+import com.dtm.lifecycle.mapper.LifecycleDatabaseOverviewMapper;
|
|
|
+import com.dtm.lifecycle.service.ILifecycleDatabaseAnalysisService;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class LifecycleDatabaseAnalysisServiceImpl implements ILifecycleDatabaseAnalysisService
|
|
|
+{
|
|
|
+ private static final List<String> STAGES = Arrays.asList("引入期", "成长期", "成熟期", "衰退期");
|
|
|
+
|
|
|
+ private static final int COMPLETE_DAYS = 120;
|
|
|
+
|
|
|
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private LifecycleDatabaseOverviewMapper lifecycleDatabaseOverviewMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getSkuResults(Map<String, Object> params)
|
|
|
+ {
|
|
|
+ return buildResults(lifecycleDatabaseOverviewMapper.selectSkuDailySales(getParam(params, "startDate"), getParam(params, "endDate")));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getSpuResults(Map<String, Object> params)
|
|
|
+ {
|
|
|
+ return buildResults(lifecycleDatabaseOverviewMapper.selectSpuDailySales(getParam(params, "startDate"), getParam(params, "endDate")));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildResults(List<LifecycleSkuDailySales> rows)
|
|
|
+ {
|
|
|
+ Map<String, EntitySeries> grouped = groupRows(rows);
|
|
|
+ Map<String, Object> results = new LinkedHashMap<>();
|
|
|
+ int completeCount = 0;
|
|
|
+ int insufficientCount = 0;
|
|
|
+
|
|
|
+ for (Map.Entry<String, EntitySeries> entry : grouped.entrySet())
|
|
|
+ {
|
|
|
+ Map<String, Object> detail = entry.getValue().toDetail();
|
|
|
+ results.put(entry.getKey(), detail);
|
|
|
+ if (Boolean.TRUE.equals(detail.get("is_complete")))
|
|
|
+ {
|
|
|
+ completeCount++;
|
|
|
+ }
|
|
|
+ if (Boolean.TRUE.equals(detail.get("insufficient")))
|
|
|
+ {
|
|
|
+ insufficientCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> summary = new HashMap<>();
|
|
|
+ summary.put("total_count", grouped.size());
|
|
|
+ summary.put("complete_count", completeCount);
|
|
|
+ summary.put("insufficient_count", insufficientCount);
|
|
|
+ summary.put("generated_at", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
|
|
+ results.put("_analysis_summary_", summary);
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, EntitySeries> groupRows(List<LifecycleSkuDailySales> rows)
|
|
|
+ {
|
|
|
+ Map<String, EntitySeries> grouped = new LinkedHashMap<>();
|
|
|
+ if (rows == null)
|
|
|
+ {
|
|
|
+ return grouped;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (LifecycleSkuDailySales row : rows)
|
|
|
+ {
|
|
|
+ if (row == null || isBlank(row.getSku()) || row.getSaleDate() == null)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ EntitySeries series = grouped.get(row.getSku());
|
|
|
+ if (series == null)
|
|
|
+ {
|
|
|
+ series = new EntitySeries(row.getSku(), row.getProductName());
|
|
|
+ grouped.put(row.getSku(), series);
|
|
|
+ }
|
|
|
+ series.add(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ return grouped;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getParam(Map<String, Object> params, String key)
|
|
|
+ {
|
|
|
+ if (params == null || params.get(key) == null)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return String.valueOf(params.get(key));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBlank(String value)
|
|
|
+ {
|
|
|
+ return value == null || value.trim().isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ private double round(double value, int scale)
|
|
|
+ {
|
|
|
+ return BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private class EntitySeries
|
|
|
+ {
|
|
|
+ private final String entityId;
|
|
|
+
|
|
|
+ private final String name;
|
|
|
+
|
|
|
+ private final Map<Long, DailyValue> dailyValues = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ private EntitySeries(String entityId, String name)
|
|
|
+ {
|
|
|
+ this.entityId = entityId;
|
|
|
+ this.name = isBlank(name) ? entityId : name;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void add(LifecycleSkuDailySales row)
|
|
|
+ {
|
|
|
+ long day = clearTime(row.getSaleDate()).getTime();
|
|
|
+ DailyValue value = dailyValues.get(day);
|
|
|
+ if (value == null)
|
|
|
+ {
|
|
|
+ value = new DailyValue(day);
|
|
|
+ dailyValues.put(day, value);
|
|
|
+ }
|
|
|
+ value.quantity += row.getTotalQuantity() == null ? 0L : row.getTotalQuantity();
|
|
|
+ value.revenue = value.revenue.add(row.getTotalRevenue() == null ? BigDecimal.ZERO : row.getTotalRevenue());
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> toDetail()
|
|
|
+ {
|
|
|
+ List<DailyValue> values = new ArrayList<>(dailyValues.values());
|
|
|
+ Map<String, Object> detail = new HashMap<>();
|
|
|
+ List<String> dates = new ArrayList<>();
|
|
|
+ List<BigDecimal> revenueSeries = new ArrayList<>();
|
|
|
+ List<Long> quantitySeries = new ArrayList<>();
|
|
|
+ List<String> stagesMap = new ArrayList<>();
|
|
|
+ BigDecimal totalRevenue = BigDecimal.ZERO;
|
|
|
+ long totalQuantity = 0L;
|
|
|
+ BigDecimal peakRevenue = BigDecimal.ZERO;
|
|
|
+ long peakQuantity = 0L;
|
|
|
+ int peakRevenueIdx = 0;
|
|
|
+ int peakQuantityIdx = 0;
|
|
|
+
|
|
|
+ for (int i = 0; i < values.size(); i++)
|
|
|
+ {
|
|
|
+ DailyValue value = values.get(i);
|
|
|
+ dates.add(dateFormat.format(new Date(value.day)));
|
|
|
+ revenueSeries.add(value.revenue.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ quantitySeries.add(value.quantity);
|
|
|
+ totalRevenue = totalRevenue.add(value.revenue);
|
|
|
+ totalQuantity += value.quantity;
|
|
|
+ if (value.revenue.compareTo(peakRevenue) > 0)
|
|
|
+ {
|
|
|
+ peakRevenue = value.revenue;
|
|
|
+ peakRevenueIdx = i;
|
|
|
+ }
|
|
|
+ if (value.quantity > peakQuantity)
|
|
|
+ {
|
|
|
+ peakQuantity = value.quantity;
|
|
|
+ peakQuantityIdx = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ long lifecycleDays = values.isEmpty() ? 0L : TimeUnit.MILLISECONDS.toDays(values.get(values.size() - 1).day - values.get(0).day) + 1L;
|
|
|
+ Map<String, StageStats> stageStats = buildStageStats(values, lifecycleDays);
|
|
|
+ Map<String, Object> stageStatistics = toStageStatistics(stageStats, totalRevenue, totalQuantity);
|
|
|
+ stagesMap.addAll(buildStagesMap(values, lifecycleDays));
|
|
|
+
|
|
|
+ Map<String, CompletionHit> completion = buildCompletion(values, stageStats, lifecycleDays, peakRevenueIdx, revenueSeries, quantitySeries);
|
|
|
+ int score = 0;
|
|
|
+ for (CompletionHit hit : completion.values())
|
|
|
+ {
|
|
|
+ if (hit.hit)
|
|
|
+ {
|
|
|
+ score += hit.weight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ boolean complete = score >= 80;
|
|
|
+ String currentStage = complete ? latestStageWithSales(stageStats) : "数据不足";
|
|
|
+
|
|
|
+ detail.put("details", name);
|
|
|
+ detail.put("current_stage", currentStage);
|
|
|
+ detail.put("is_complete", complete);
|
|
|
+ detail.put("insufficient", lifecycleDays < COMPLETE_DAYS || values.size() < 3);
|
|
|
+ detail.put("completeness_score", score);
|
|
|
+ detail.put("completion_details", completionToMap(completion));
|
|
|
+ detail.put("date_series", dates);
|
|
|
+ detail.put("revenue_series", revenueSeries);
|
|
|
+ detail.put("quantity_series", quantitySeries);
|
|
|
+ detail.put("smoothed_revenue", revenueSeries);
|
|
|
+ detail.put("smoothed_quantity", quantitySeries);
|
|
|
+ detail.put("stage_statistics", stageStatistics);
|
|
|
+ detail.put("stage_boundaries", buildStageBoundaries(values, lifecycleDays));
|
|
|
+ detail.put("stages_map", stagesMap);
|
|
|
+ detail.put("total_revenue", totalRevenue.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ detail.put("total_quantity", totalQuantity);
|
|
|
+ detail.put("peak_revenue", peakRevenue.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ detail.put("peak_revenue_date", dates.isEmpty() ? null : dates.get(peakRevenueIdx));
|
|
|
+ detail.put("peak_quantity", peakQuantity);
|
|
|
+ detail.put("peak_quantity_date", dates.isEmpty() ? null : dates.get(peakQuantityIdx));
|
|
|
+ detail.put("revenue_peak_idx", peakRevenueIdx);
|
|
|
+ detail.put("quantity_peak_idx", peakQuantityIdx);
|
|
|
+ detail.put("next_stage_prediction", complete ? null : predictNextStage(currentStage));
|
|
|
+ detail.put("entity_id", entityId);
|
|
|
+ return detail;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, StageStats> buildStageStats(List<DailyValue> values, long lifecycleDays)
|
|
|
+ {
|
|
|
+ Map<String, StageStats> map = new LinkedHashMap<>();
|
|
|
+ for (String stage : STAGES)
|
|
|
+ {
|
|
|
+ map.put(stage, new StageStats(stage));
|
|
|
+ }
|
|
|
+ if (values.isEmpty())
|
|
|
+ {
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ long first = values.get(0).day;
|
|
|
+ long totalDays = Math.max(1L, lifecycleDays);
|
|
|
+ long base = totalDays / 4;
|
|
|
+ long remainder = totalDays % 4;
|
|
|
+ long startOffset = 0L;
|
|
|
+
|
|
|
+ for (int i = 0; i < STAGES.size(); i++)
|
|
|
+ {
|
|
|
+ String stage = STAGES.get(i);
|
|
|
+ StageStats stats = map.get(stage);
|
|
|
+ long duration = base + (i < remainder ? 1 : 0);
|
|
|
+ long endOffset = startOffset + Math.max(1L, duration) - 1L;
|
|
|
+ stats.durationDays = (int) Math.max(1L, duration);
|
|
|
+ stats.startDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(startOffset)));
|
|
|
+ stats.endDate = dateFormat.format(new Date(first + TimeUnit.DAYS.toMillis(Math.min(totalDays - 1L, endOffset))));
|
|
|
+ startOffset = endOffset + 1L;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (DailyValue value : values)
|
|
|
+ {
|
|
|
+ long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
|
|
|
+ int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
|
|
|
+ map.get(STAGES.get(index)).add(value);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> buildStagesMap(List<DailyValue> values, long lifecycleDays)
|
|
|
+ {
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
+ if (values.isEmpty())
|
|
|
+ {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ long first = values.get(0).day;
|
|
|
+ long totalDays = Math.max(1L, lifecycleDays);
|
|
|
+ for (DailyValue value : values)
|
|
|
+ {
|
|
|
+ long offset = TimeUnit.MILLISECONDS.toDays(value.day - first);
|
|
|
+ int index = (int) Math.min(3L, Math.max(0L, offset * 4 / totalDays));
|
|
|
+ result.add(STAGES.get(index));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> toStageStatistics(Map<String, StageStats> stageStats, BigDecimal totalRevenue, long totalQuantity)
|
|
|
+ {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ for (String stage : STAGES)
|
|
|
+ {
|
|
|
+ StageStats stats = stageStats.get(stage);
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put("totalRevenue", stats.totalRevenue.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ map.put("revenuePercentage", totalRevenue.compareTo(BigDecimal.ZERO) == 0 ? 0 : round(stats.totalRevenue.multiply(BigDecimal.valueOf(100)).divide(totalRevenue, 6, RoundingMode.HALF_UP).doubleValue(), 1));
|
|
|
+ map.put("totalQuantity", stats.totalQuantity);
|
|
|
+ map.put("quantityPercentage", totalQuantity == 0 ? 0 : round(stats.totalQuantity * 100.0 / totalQuantity, 1));
|
|
|
+ map.put("avgDailyRevenue", stats.durationDays == 0 ? 0 : stats.totalRevenue.divide(BigDecimal.valueOf(stats.durationDays), 2, RoundingMode.HALF_UP));
|
|
|
+ map.put("avgDailyQuantity", stats.durationDays == 0 ? 0 : round(stats.totalQuantity * 1.0 / stats.durationDays, 1));
|
|
|
+ map.put("durationDays", stats.durationDays);
|
|
|
+ map.put("startDate", stats.startDate);
|
|
|
+ map.put("endDate", stats.endDate);
|
|
|
+ result.put(stage, map);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> buildStageBoundaries(List<DailyValue> values, long lifecycleDays)
|
|
|
+ {
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ if (values.size() < 4)
|
|
|
+ {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ long first = values.get(0).day;
|
|
|
+ long totalDays = Math.max(1L, lifecycleDays);
|
|
|
+ for (int i = 1; i < STAGES.size(); i++)
|
|
|
+ {
|
|
|
+ long boundaryOffset = totalDays * i / 4;
|
|
|
+ int index = findNearestIndex(values, first + TimeUnit.DAYS.toMillis(boundaryOffset));
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ item.put("type", STAGES.get(i) + "开始");
|
|
|
+ item.put("date", dateFormat.format(new Date(values.get(index).day)));
|
|
|
+ item.put("index", index);
|
|
|
+ result.add(item);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findNearestIndex(List<DailyValue> values, long targetDay)
|
|
|
+ {
|
|
|
+ int index = 0;
|
|
|
+ long diff = Long.MAX_VALUE;
|
|
|
+ for (int i = 0; i < values.size(); i++)
|
|
|
+ {
|
|
|
+ long currentDiff = Math.abs(values.get(i).day - targetDay);
|
|
|
+ if (currentDiff < diff)
|
|
|
+ {
|
|
|
+ diff = currentDiff;
|
|
|
+ index = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, CompletionHit> buildCompletion(List<DailyValue> values, Map<String, StageStats> stats, long lifecycleDays, int peakRevenueIdx, List<BigDecimal> revenue, List<Long> qty)
|
|
|
+ {
|
|
|
+ Map<String, CompletionHit> map = new LinkedHashMap<>();
|
|
|
+ int length = values.size();
|
|
|
+ boolean hasGrowth = stats.get("成长期").totalQuantity > stats.get("引入期").totalQuantity || stats.get("成长期").totalRevenue.compareTo(stats.get("引入期").totalRevenue) > 0;
|
|
|
+ boolean hasDecline = stats.get("衰退期").totalQuantity < stats.get("成熟期").totalQuantity || stats.get("衰退期").totalRevenue.compareTo(stats.get("成熟期").totalRevenue) < 0;
|
|
|
+ double revenueCv = coefficientOfVariation(revenue);
|
|
|
+ double qtyCv = coefficientOfVariation(qty);
|
|
|
+ double avgRevenue = averageBigDecimal(revenue);
|
|
|
+ boolean peakSignificant = avgRevenue <= 0 ? false : revenue.get(peakRevenueIdx).doubleValue() >= avgRevenue * 1.8;
|
|
|
+
|
|
|
+ map.put("sufficient_time", new CompletionHit(lifecycleDays >= COMPLETE_DAYS, 15));
|
|
|
+ map.put("reasonable_peak_position", new CompletionHit(length > 0 && peakRevenueIdx >= length * 0.25 && peakRevenueIdx <= length * 0.75, 10));
|
|
|
+ map.put("significant_growth", new CompletionHit(hasGrowth, 15));
|
|
|
+ map.put("noticeable_decline", new CompletionHit(hasDecline, 15));
|
|
|
+ map.put("has_lifecycle_shape", new CompletionHit(revenueCv >= 0.3 && qtyCv >= 0.25, 10));
|
|
|
+ map.put("peak_significance", new CompletionHit(peakSignificant, 10));
|
|
|
+ map.put("data_quality_check", new CompletionHit(length >= 3 && lifecycleDays >= COMPLETE_DAYS && !revenue.isEmpty(), 10));
|
|
|
+ map.put("cycle_completeness", new CompletionHit(hasStageSales(stats, "引入期") && hasStageSales(stats, "成长期") && hasStageSales(stats, "成熟期") && hasStageSales(stats, "衰退期"), 10));
|
|
|
+ map.put("trend_consistency", new CompletionHit(hasGrowth || hasDecline, 5));
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasStageSales(Map<String, StageStats> stats, String stage)
|
|
|
+ {
|
|
|
+ StageStats value = stats.get(stage);
|
|
|
+ return value != null && (value.totalQuantity > 0 || value.totalRevenue.compareTo(BigDecimal.ZERO) > 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> completionToMap(Map<String, CompletionHit> completion)
|
|
|
+ {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, CompletionHit> entry : completion.entrySet())
|
|
|
+ {
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ item.put("hit", entry.getValue().hit);
|
|
|
+ item.put("weight", entry.getValue().weight);
|
|
|
+ result.put(entry.getKey(), item);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String latestStageWithSales(Map<String, StageStats> stats)
|
|
|
+ {
|
|
|
+ for (int i = STAGES.size() - 1; i >= 0; i--)
|
|
|
+ {
|
|
|
+ String stage = STAGES.get(i);
|
|
|
+ if (hasStageSales(stats, stage))
|
|
|
+ {
|
|
|
+ return stage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return "数据不足";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String predictNextStage(String currentStage)
|
|
|
+ {
|
|
|
+ if ("数据不足".equals(currentStage))
|
|
|
+ {
|
|
|
+ return "继续积累销售数据";
|
|
|
+ }
|
|
|
+ int index = STAGES.indexOf(currentStage);
|
|
|
+ if (index >= 0 && index < STAGES.size() - 1)
|
|
|
+ {
|
|
|
+ return STAGES.get(index + 1);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double coefficientOfVariation(List<?> values)
|
|
|
+ {
|
|
|
+ if (values == null || values.isEmpty())
|
|
|
+ {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ double avg = 0.0;
|
|
|
+ for (Object value : values)
|
|
|
+ {
|
|
|
+ avg += numberValue(value);
|
|
|
+ }
|
|
|
+ avg = avg / values.size();
|
|
|
+ if (avg == 0.0)
|
|
|
+ {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ double variance = 0.0;
|
|
|
+ for (Object value : values)
|
|
|
+ {
|
|
|
+ double diff = numberValue(value) - avg;
|
|
|
+ variance += diff * diff;
|
|
|
+ }
|
|
|
+ return Math.sqrt(variance / values.size()) / avg;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double averageBigDecimal(List<BigDecimal> values)
|
|
|
+ {
|
|
|
+ if (values == null || values.isEmpty())
|
|
|
+ {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ BigDecimal total = BigDecimal.ZERO;
|
|
|
+ for (BigDecimal value : values)
|
|
|
+ {
|
|
|
+ total = total.add(value == null ? BigDecimal.ZERO : value);
|
|
|
+ }
|
|
|
+ return total.divide(BigDecimal.valueOf(values.size()), 6, RoundingMode.HALF_UP).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private double numberValue(Object value)
|
|
|
+ {
|
|
|
+ if (value instanceof BigDecimal)
|
|
|
+ {
|
|
|
+ return ((BigDecimal) value).doubleValue();
|
|
|
+ }
|
|
|
+ if (value instanceof Number)
|
|
|
+ {
|
|
|
+ return ((Number) value).doubleValue();
|
|
|
+ }
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Date clearTime(Date date)
|
|
|
+ {
|
|
|
+ return new Date(date.getTime() / 86400000L * 86400000L);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class DailyValue
|
|
|
+ {
|
|
|
+ private final long day;
|
|
|
+
|
|
|
+ private long quantity = 0L;
|
|
|
+
|
|
|
+ private BigDecimal revenue = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ private DailyValue(long day)
|
|
|
+ {
|
|
|
+ this.day = day;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class StageStats
|
|
|
+ {
|
|
|
+ private final String stage;
|
|
|
+
|
|
|
+ private BigDecimal totalRevenue = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ private long totalQuantity = 0L;
|
|
|
+
|
|
|
+ private int durationDays = 0;
|
|
|
+
|
|
|
+ private String startDate;
|
|
|
+
|
|
|
+ private String endDate;
|
|
|
+
|
|
|
+ private StageStats(String stage)
|
|
|
+ {
|
|
|
+ this.stage = stage;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void add(DailyValue value)
|
|
|
+ {
|
|
|
+ totalQuantity += value.quantity;
|
|
|
+ totalRevenue = totalRevenue.add(value.revenue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class CompletionHit
|
|
|
+ {
|
|
|
+ private final boolean hit;
|
|
|
+
|
|
|
+ private final int weight;
|
|
|
+
|
|
|
+ private CompletionHit(boolean hit, int weight)
|
|
|
+ {
|
|
|
+ this.hit = hit;
|
|
|
+ this.weight = weight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|