|
|
@@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import com.ylx.massage.domain.*;
|
|
|
import com.ylx.massage.domain.dto.*;
|
|
|
import com.ylx.massage.domain.vo.H5ProductVo;
|
|
|
+import com.ylx.massage.domain.vo.PlatformProductStats;
|
|
|
+import com.ylx.massage.domain.vo.ProductSalesStats;
|
|
|
import com.ylx.massage.domain.vo.SpecComboVO;
|
|
|
import com.ylx.massage.mapper.*;
|
|
|
import com.ylx.massage.service.ProductService;
|
|
|
@@ -18,7 +20,10 @@ import org.springframework.util.CollectionUtils;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
@@ -49,6 +54,9 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
|
|
|
@Resource
|
|
|
private ProductMapper productMapper;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private ProductOrderItemMapper productOrderItemMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 新增商品
|
|
|
*
|
|
|
@@ -296,13 +304,175 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
|
|
|
*
|
|
|
* @param page 分页参数
|
|
|
* @param product 商品查询参数
|
|
|
- * @param sortField 排序字段(price:价格, sales:销量)
|
|
|
+ * @param sortField 排序字段(price:价格, sales:销量, comprehensive:综合排序)
|
|
|
* @param sortOrder 排序方式(asc:升序, desc:降序)
|
|
|
* @return Page<Product> 分页结果
|
|
|
*/
|
|
|
@Override
|
|
|
- public Page<H5ProductVo> selectH5Page(Page<H5ProductVo> page, Product product, String name,String sortField, String sortOrder) {
|
|
|
- return productMapper.selectH5Page(page, product, name,sortField, sortOrder);
|
|
|
+ public Page<H5ProductVo> selectH5Page(Page<H5ProductVo> page, Product product, String name, String sortField, String sortOrder) {
|
|
|
+ // 综合排序,需要在Java层计算得分
|
|
|
+ if ("comprehensive".equals(sortField)) {
|
|
|
+ return selectH5PageWithComprehensiveSort(page, product, name, sortOrder);
|
|
|
+ }
|
|
|
+ return productMapper.selectH5Page(page, product, name, sortField, sortOrder);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 综合排序查询(H5端)
|
|
|
+ * 综合得分 = 0.5 * 销量热度 + 0.3 * 新品系数 + 0.2 * 积分性价比
|
|
|
+ *
|
|
|
+ * @param page 分页参数
|
|
|
+ * @param product 商品查询参数
|
|
|
+ * @param name 商品名称
|
|
|
+ * @param sortOrder 排序方式(综合排序固定降序)
|
|
|
+ * @return Page<H5ProductVo> 分页结果
|
|
|
+ */
|
|
|
+ private Page<H5ProductVo> selectH5PageWithComprehensiveSort(Page<H5ProductVo> page, Product product, String name, String sortOrder) {
|
|
|
+ // 1. 查询近30天各商品销量统计(从订单表)
|
|
|
+ List<ProductSalesStats> salesStatsList = productOrderItemMapper.selectLast30DaysSalesStats();
|
|
|
+ // 转换为Map:productId -> last30DaysSales
|
|
|
+ Map<Long, Long> productSalesMap = salesStatsList.stream()
|
|
|
+ .collect(Collectors.toMap(ProductSalesStats::getProductId, ProductSalesStats::getLast30DaysSales, (v1, v2) -> v1));
|
|
|
+
|
|
|
+ // 2. 计算平台近30天总销量
|
|
|
+ long totalSales = productSalesMap.values().stream().mapToLong(Long::longValue).sum();
|
|
|
+
|
|
|
+ // 3. 为了支持分页+排序,先查询所有匹配商品(不分页,按综合得分排序后取分页数据)
|
|
|
+ Page<H5ProductVo> allDataPage = new Page<>(1, Integer.MAX_VALUE);
|
|
|
+ Page<H5ProductVo> allProductsPage = productMapper.selectH5Page(allDataPage, product, null, null, null);
|
|
|
+
|
|
|
+ List<H5ProductVo> allProducts = allProductsPage.getRecords();
|
|
|
+ if (allProducts.isEmpty()) {
|
|
|
+ return new Page<>(page.getCurrent(), page.getSize());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 计算每个商品的综合得分并排序
|
|
|
+ List<H5ProductVo> sortedProducts = allProducts.stream()
|
|
|
+ .peek(vo -> {
|
|
|
+ // 使用近30天订单销量计算综合得分
|
|
|
+ long last30DaysSales = productSalesMap.getOrDefault(vo.getId(), 0L);
|
|
|
+ BigDecimal comprehensiveScore = calculateComprehensiveScore(last30DaysSales, vo, totalSales);
|
|
|
+ vo.setComprehensiveScore(comprehensiveScore);
|
|
|
+ })
|
|
|
+ .sorted((p1, p2) -> {
|
|
|
+ // 综合排序:得分高的在前
|
|
|
+ int cmp = p2.getComprehensiveScore().compareTo(p1.getComprehensiveScore());
|
|
|
+ if (cmp != 0) {
|
|
|
+ return cmp;
|
|
|
+ }
|
|
|
+ // 得分相同时,按近30天销量降序作为二级排序
|
|
|
+ Long sales1 = productSalesMap.getOrDefault(p1.getId(), 0L);
|
|
|
+ Long sales2 = productSalesMap.getOrDefault(p2.getId(), 0L);
|
|
|
+ return sales2.compareTo(sales1);
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 5. 计算分页位置并返回
|
|
|
+ long current = page.getCurrent();
|
|
|
+ long size = page.getSize();
|
|
|
+ long fromIndex = (current - 1) * size;
|
|
|
+ long toIndex = Math.min(fromIndex + size, sortedProducts.size());
|
|
|
+
|
|
|
+ List<H5ProductVo> pageData = fromIndex < sortedProducts.size()
|
|
|
+ ? sortedProducts.subList((int) fromIndex, (int) toIndex)
|
|
|
+ : Collections.emptyList();
|
|
|
+
|
|
|
+ Page<H5ProductVo> resultPage = new Page<>(current, size);
|
|
|
+ resultPage.setRecords(pageData);
|
|
|
+ resultPage.setTotal(sortedProducts.size());
|
|
|
+ return resultPage;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算商品综合排序得分
|
|
|
+ * 综合得分 = 0.5 * 销量热度得分 + 0.3 * 新品系数得分 + 0.2 * 积分性价比得分
|
|
|
+ *
|
|
|
+ * @param last30DaysSales 商品近30天销量
|
|
|
+ * @param vo 商品VO
|
|
|
+ * @param totalSales 平台近30天总销量
|
|
|
+ * @return BigDecimal 综合得分
|
|
|
+ */
|
|
|
+ private BigDecimal calculateComprehensiveScore(long last30DaysSales, H5ProductVo vo, long totalSales) {
|
|
|
+ // 1. 销量热度得分 (50%权重)
|
|
|
+ // 公式:min(1, 当前商品近30天销量 / 平台近30天总销量)
|
|
|
+ BigDecimal salesScore = BigDecimal.ZERO;
|
|
|
+ if (totalSales > 0) {
|
|
|
+ double salesRatio = (double) last30DaysSales / totalSales;
|
|
|
+ salesScore = BigDecimal.valueOf(Math.min(1.0, salesRatio));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 新品系数得分 (30%权重)
|
|
|
+ // 公式:上架前7天=1.0,后续递减 max(0, 1 - (上架天数-7)/23)
|
|
|
+ BigDecimal newProductScore = calculateNewProductScore(vo.getSaleStartTime());
|
|
|
+
|
|
|
+ // 3. 积分性价比得分 (20%权重)
|
|
|
+ // 公式:积分值 / 商品价值
|
|
|
+ BigDecimal pointValueScore = calculatePointValueScore(vo);
|
|
|
+
|
|
|
+ // 综合得分 = 0.5 * 销量热度 + 0.3 * 新品系数 + 0.2 * 积分性价比
|
|
|
+ BigDecimal comprehensiveScore = salesScore.multiply(BigDecimal.valueOf(0.5))
|
|
|
+ .add(newProductScore.multiply(BigDecimal.valueOf(0.3)))
|
|
|
+ .add(pointValueScore.multiply(BigDecimal.valueOf(0.2)));
|
|
|
+
|
|
|
+ // 保留2位小数
|
|
|
+ return comprehensiveScore.setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算新品系数得分
|
|
|
+ * 上架前7天系数 = 1.0,后续30天递减,系数 = max(0, 1 - (上架天数-7)/23)
|
|
|
+ *
|
|
|
+ * @param saleStartTime 上架开始时间
|
|
|
+ * @return BigDecimal 新品系数得分
|
|
|
+ */
|
|
|
+ private BigDecimal calculateNewProductScore(LocalDate saleStartTime) {
|
|
|
+ if (saleStartTime == null) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ long daysOnSale = ChronoUnit.DAYS.between(saleStartTime, today);
|
|
|
+
|
|
|
+ if (daysOnSale < 0) {
|
|
|
+ // 上架时间是未来时间,视为未上架,得分为0
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ } else if (daysOnSale <= 7) {
|
|
|
+ // 上架前7天,系数为1.0
|
|
|
+ return BigDecimal.ONE;
|
|
|
+ } else {
|
|
|
+ // 第7天起开始递减
|
|
|
+ double coefficient = Math.max(0, 1 - (daysOnSale - 7) / 23.0);
|
|
|
+ return BigDecimal.valueOf(coefficient);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算积分性价比得分
|
|
|
+ * 积分值/商品价值,积分值越高、商品价值越低,得分越高
|
|
|
+ *
|
|
|
+ * @param vo 商品VO
|
|
|
+ * @return BigDecimal 积分性价比得分
|
|
|
+ */
|
|
|
+ private BigDecimal calculatePointValueScore(H5ProductVo vo) {
|
|
|
+ Integer pricePoint = vo.getPricePoint();
|
|
|
+ BigDecimal originalPrice = vo.getOriginalPrice();
|
|
|
+
|
|
|
+ if (pricePoint == null || pricePoint <= 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果商品价值(原价值)为空或为0,使用积分价格作为参考(得分较低)
|
|
|
+ if (originalPrice == null || originalPrice.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ // 积分价格越高,得分越高(但最高不超过0.5)
|
|
|
+ // 这里使用一个经验公式:积分价格的对数归一化
|
|
|
+ double normalizedScore = Math.min(0.5, Math.log10(pricePoint + 1) / 4.0);
|
|
|
+ return BigDecimal.valueOf(normalizedScore);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 积分性价比 = 积分值 / 商品价值
|
|
|
+ // 为了避免得分过高,使用 min(积分性价比, 1.0) 限制最高为1
|
|
|
+ double ratio = (double) pricePoint / originalPrice.doubleValue();
|
|
|
+ return BigDecimal.valueOf(Math.min(1.0, ratio));
|
|
|
}
|
|
|
|
|
|
/**
|