|
|
@@ -1,9 +1,13 @@
|
|
|
package com.dtm.order.shop.service;
|
|
|
|
|
|
+import com.dtm.order.shop.mapper.ShopValueMapper;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.HashSet;
|
|
|
@@ -15,10 +19,12 @@ import java.util.Set;
|
|
|
public class ShopAnalysisService {
|
|
|
|
|
|
private final ShopSalesDataStore dataStore;
|
|
|
+ private final ShopValueMapper shopValueMapper;
|
|
|
|
|
|
@Autowired
|
|
|
- public ShopAnalysisService(ShopSalesDataStore dataStore) {
|
|
|
+ public ShopAnalysisService(ShopSalesDataStore dataStore, ShopValueMapper shopValueMapper) {
|
|
|
this.dataStore = dataStore;
|
|
|
+ this.shopValueMapper = shopValueMapper;
|
|
|
}
|
|
|
|
|
|
public int importAllShopSalesData() {
|
|
|
@@ -33,56 +39,111 @@ public class ShopAnalysisService {
|
|
|
return dataStore.getLastUploadDebug();
|
|
|
}
|
|
|
|
|
|
- public Map<String, Double> getChannelSalesContribution() {
|
|
|
+ public String getMaxSalesDate() {
|
|
|
+ String databaseMaxDate = getDatabaseMaxSalesDate();
|
|
|
+ if (databaseMaxDate != null && !databaseMaxDate.isEmpty()) {
|
|
|
+ return databaseMaxDate;
|
|
|
+ }
|
|
|
+ LocalDate maxDate = dataStore.getMaxSalesDate();
|
|
|
+ return maxDate == null ? LocalDate.now().toString() : maxDate.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Double> getChannelSalesContribution(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectPlatformOrderCount(startDate, endDate));
|
|
|
+ if (!databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> contributionData = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
contributionData.merge(record.getPlatformName(), 1.0, Double::sum);
|
|
|
}
|
|
|
return contributionData.isEmpty() ? null : contributionData;
|
|
|
}
|
|
|
|
|
|
- public Map<String, Double> getChannelRoiValue() {
|
|
|
+ public Map<String, Double> getChannelRoiValue(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectPlatformSalesAmount(startDate, endDate));
|
|
|
+ if (!databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> resultData = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
resultData.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
|
|
|
}
|
|
|
return resultData.isEmpty() ? null : resultData;
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getUnitContribution() {
|
|
|
+ public List<Map<String, Object>> getUnitContribution(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ List<Map<String, Object>> databaseData = shopValueMapper.selectUnitContribution(startDate, endDate);
|
|
|
+ if (databaseData != null && !databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> volumeByUnit = new HashMap<>();
|
|
|
Map<String, Double> amountByUnit = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
volumeByUnit.merge(record.getBusinessUnitName(), record.getQuantity(), Double::sum);
|
|
|
amountByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
|
|
|
}
|
|
|
return toContributionList(volumeByUnit, amountByUnit);
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getChannelTotalContribution() {
|
|
|
+ public List<Map<String, Object>> getChannelTotalContribution(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ List<Map<String, Object>> databaseData = shopValueMapper.selectChannelContribution(startDate, endDate);
|
|
|
+ if (databaseData != null && !databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> volumeByChannel = new HashMap<>();
|
|
|
Map<String, Double> amountByChannel = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
volumeByChannel.merge(record.getChannelName(), record.getQuantity(), Double::sum);
|
|
|
amountByChannel.merge(record.getChannelName(), record.getSalesAmount(), Double::sum);
|
|
|
}
|
|
|
return toContributionList(volumeByChannel, amountByChannel);
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getPlatformTotalContribution() {
|
|
|
+ public List<Map<String, Object>> getPlatformTotalContribution(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ List<Map<String, Object>> databaseData = shopValueMapper.selectPlatformContribution(startDate, endDate);
|
|
|
+ if (databaseData != null && !databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> volumeByPlatform = new HashMap<>();
|
|
|
Map<String, Double> amountByPlatform = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
volumeByPlatform.merge(record.getPlatformName(), record.getQuantity(), Double::sum);
|
|
|
amountByPlatform.merge(record.getPlatformName(), record.getSalesAmount(), Double::sum);
|
|
|
}
|
|
|
return toContributionList(volumeByPlatform, amountByPlatform);
|
|
|
}
|
|
|
|
|
|
- public Map<String, Object> getTopProductContribution() {
|
|
|
+ public Map<String, Object> getTopProductContribution(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ List<Map<String, Object>> topProducts = shopValueMapper.selectTopProducts(startDate, endDate);
|
|
|
+ Double totalSales = shopValueMapper.selectTotalSales(startDate, endDate);
|
|
|
+ if (topProducts != null && !topProducts.isEmpty()) {
|
|
|
+ double top5SalesSum = topProducts.stream()
|
|
|
+ .mapToDouble(row -> toDouble(getMapValue(row, "salesAmount")))
|
|
|
+ .sum();
|
|
|
+ double total = totalSales == null ? 0.0 : totalSales;
|
|
|
+ Map<String, Object> finalResult = new HashMap<>();
|
|
|
+ finalResult.put("top5Products", topProducts);
|
|
|
+ finalResult.put("top5TotalSales", top5SalesSum);
|
|
|
+ finalResult.put("totalSales", total);
|
|
|
+ finalResult.put("contributionRatio", total > 0 ? top5SalesSum / total : 0.0);
|
|
|
+ return finalResult;
|
|
|
+ }
|
|
|
+ }
|
|
|
double totalSales = 0.0;
|
|
|
Map<String, Double> productSales = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
totalSales += record.getSalesAmount();
|
|
|
productSales.merge(record.getProductCode(), record.getSalesAmount(), Double::sum);
|
|
|
}
|
|
|
@@ -102,7 +163,7 @@ public class ShopAnalysisService {
|
|
|
topProducts.add(productMap);
|
|
|
}
|
|
|
|
|
|
- double contributionRatio = (totalSales > 0) ? (top5SalesSum / totalSales) : 0.0;
|
|
|
+ double contributionRatio = totalSales > 0 ? (top5SalesSum / totalSales) : 0.0;
|
|
|
Map<String, Object> finalResult = new HashMap<>();
|
|
|
finalResult.put("top5Products", topProducts);
|
|
|
finalResult.put("top5TotalSales", top5SalesSum);
|
|
|
@@ -111,9 +172,15 @@ public class ShopAnalysisService {
|
|
|
return finalResult;
|
|
|
}
|
|
|
|
|
|
- public List<Map<String, Object>> getCrossSellingProducts() {
|
|
|
+ public List<Map<String, Object>> getCrossSellingProducts(String startDate, String endDate, String skuKeyword) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ List<Map<String, Object>> databaseData = shopValueMapper.selectCrossSellingProducts(startDate, endDate, normalize(skuKeyword));
|
|
|
+ if (databaseData != null && !databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Set<String>> platformByProduct = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, skuKeyword)) {
|
|
|
platformByProduct
|
|
|
.computeIfAbsent(record.getProductCode(), k -> new HashSet<>())
|
|
|
.add(record.getPlatformName());
|
|
|
@@ -129,13 +196,20 @@ public class ShopAnalysisService {
|
|
|
crossSellingData.add(productMap);
|
|
|
}
|
|
|
}
|
|
|
+ crossSellingData.sort((a, b) -> Long.compare((Long) b.get("platformCount"), (Long) a.get("platformCount")));
|
|
|
return crossSellingData.isEmpty() ? null : crossSellingData;
|
|
|
}
|
|
|
|
|
|
- public Map<String, Double> getDepartmentOperationalEfficiency() {
|
|
|
+ public Map<String, Double> getDepartmentOperationalEfficiency(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ Map<String, Double> databaseData = toDoubleMap(shopValueMapper.selectDepartmentEfficiency(startDate, endDate));
|
|
|
+ if (!databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Double> sumByUnit = new HashMap<>();
|
|
|
Map<String, Long> countByUnit = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
sumByUnit.merge(record.getBusinessUnitName(), record.getSalesAmount(), Double::sum);
|
|
|
countByUnit.merge(record.getBusinessUnitName(), 1L, Long::sum);
|
|
|
}
|
|
|
@@ -149,9 +223,15 @@ public class ShopAnalysisService {
|
|
|
return efficiencyData.isEmpty() ? null : efficiencyData;
|
|
|
}
|
|
|
|
|
|
- public Map<String, Long> getChannelProductDiversity() {
|
|
|
+ public Map<String, Long> getChannelProductDiversity(String startDate, String endDate) {
|
|
|
+ if (hasDatabaseRows()) {
|
|
|
+ Map<String, Long> databaseData = toLongMap(shopValueMapper.selectChannelDiversity(startDate, endDate));
|
|
|
+ if (!databaseData.isEmpty()) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ }
|
|
|
Map<String, Set<String>> productsByChannel = new HashMap<>();
|
|
|
- for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ for (ShopSalesRecord record : filterRecords(startDate, endDate, null)) {
|
|
|
productsByChannel
|
|
|
.computeIfAbsent(record.getChannelName(), k -> new HashSet<>())
|
|
|
.add(record.getProductCode());
|
|
|
@@ -163,10 +243,25 @@ public class ShopAnalysisService {
|
|
|
return diversityData.isEmpty() ? null : diversityData;
|
|
|
}
|
|
|
|
|
|
- private List<Map<String, Object>> toContributionList(
|
|
|
- Map<String, Double> volume,
|
|
|
- Map<String, Double> amount
|
|
|
- ) {
|
|
|
+ private List<ShopSalesRecord> filterRecords(String startDate, String endDate, String skuKeyword) {
|
|
|
+ LocalDateRange range = parseRange(startDate, endDate);
|
|
|
+ String normalizedKeyword = normalize(skuKeyword).toLowerCase();
|
|
|
+ boolean hasKeyword = !normalizedKeyword.isEmpty();
|
|
|
+
|
|
|
+ List<ShopSalesRecord> filtered = new ArrayList<>();
|
|
|
+ for (ShopSalesRecord record : dataStore.getSalesRecords()) {
|
|
|
+ if (!range.contains(record.getSalesDate())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (hasKeyword && !normalize(record.getProductCode()).toLowerCase().contains(normalizedKeyword)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ filtered.add(record);
|
|
|
+ }
|
|
|
+ return filtered;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> toContributionList(Map<String, Double> volume, Map<String, Double> amount) {
|
|
|
List<Map<String, Object>> result = new ArrayList<>();
|
|
|
for (String name : volume.keySet()) {
|
|
|
Map<String, Object> map = new HashMap<>();
|
|
|
@@ -175,9 +270,153 @@ public class ShopAnalysisService {
|
|
|
map.put("totalAmount", amount.getOrDefault(name, 0.0));
|
|
|
result.add(map);
|
|
|
}
|
|
|
+ result.sort((a, b) -> Double.compare((Double) b.get("totalAmount"), (Double) a.get("totalAmount")));
|
|
|
return result.isEmpty() ? null : result;
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
+ private LocalDateRange parseRange(String startDate, String endDate) {
|
|
|
+ LocalDate start = parseDate(startDate);
|
|
|
+ LocalDate end = parseDate(endDate);
|
|
|
+ if (start == null && end == null) {
|
|
|
+ return new LocalDateRange(null, null);
|
|
|
+ }
|
|
|
+ if (start == null) {
|
|
|
+ start = end;
|
|
|
+ }
|
|
|
+ if (end == null) {
|
|
|
+ end = start;
|
|
|
+ }
|
|
|
+ return new LocalDateRange(start, end);
|
|
|
+ }
|
|
|
|
|
|
+ private LocalDate parseDate(String raw) {
|
|
|
+ if (raw == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String value = raw.trim();
|
|
|
+ if (value.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value.length() > 10) {
|
|
|
+ value = value.substring(0, 10);
|
|
|
+ }
|
|
|
+ value = value.replace('/', '-');
|
|
|
+ try {
|
|
|
+ return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-M-d"));
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String normalize(String value) {
|
|
|
+ return value == null ? "" : value.trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasDatabaseRows() {
|
|
|
+ try {
|
|
|
+ Long count = shopValueMapper.countRows();
|
|
|
+ return count != null && count > 0;
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getDatabaseMaxSalesDate() {
|
|
|
+ if (!hasDatabaseRows()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return shopValueMapper.selectMaxStatDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Double> toDoubleMap(List<Map<String, Object>> rows) {
|
|
|
+ Map<String, Double> result = new HashMap<>();
|
|
|
+ if (rows == null) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ String name = toStringValue(getMapValue(row, "name"));
|
|
|
+ if (!name.isEmpty()) {
|
|
|
+ result.put(name, toDouble(getMapValue(row, "value")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Long> toLongMap(List<Map<String, Object>> rows) {
|
|
|
+ Map<String, Long> result = new HashMap<>();
|
|
|
+ if (rows == null) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ String name = toStringValue(getMapValue(row, "name"));
|
|
|
+ if (!name.isEmpty()) {
|
|
|
+ result.put(name, Math.round(toDouble(getMapValue(row, "value"))));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toStringValue(Object value) {
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
+ }
|
|
|
|
|
|
+ private Object getMapValue(Map<String, Object> row, String key) {
|
|
|
+ if (row == null || key == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (row.containsKey(key)) {
|
|
|
+ return row.get(key);
|
|
|
+ }
|
|
|
+ String lowerKey = key.toLowerCase();
|
|
|
+ String upperKey = key.toUpperCase();
|
|
|
+ if (row.containsKey(lowerKey)) {
|
|
|
+ return row.get(lowerKey);
|
|
|
+ }
|
|
|
+ if (row.containsKey(upperKey)) {
|
|
|
+ return row.get(upperKey);
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, Object> entry : row.entrySet()) {
|
|
|
+ if (key.equalsIgnoreCase(entry.getKey())) {
|
|
|
+ return entry.getValue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double toDouble(Object value) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).doubleValue();
|
|
|
+ }
|
|
|
+ if (value == null) {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Double.parseDouble(String.valueOf(value));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class LocalDateRange {
|
|
|
+ private final LocalDate start;
|
|
|
+ private final LocalDate end;
|
|
|
+
|
|
|
+ private LocalDateRange(LocalDate start, LocalDate end) {
|
|
|
+ this.start = start;
|
|
|
+ this.end = end;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean contains(LocalDate date) {
|
|
|
+ if (date == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (start != null && date.isBefore(start)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (end != null && date.isAfter(end)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|