Forráskód Böngészése

优化了分页查询商品列表(H5接口)

jinshihui 4 napja
szülő
commit
477d08bdd1

+ 5 - 1
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/ProductController.java

@@ -89,7 +89,11 @@ public class ProductController extends BaseController {
      * @param page      分页对象
      * @param product   商品实体
      * @param name      商品名称
-     * @param sortField 排序字段(price:价格, sales:销量)
+     * @param sortField 排序字段(price:价格, sales:销量, comprehensive:综合排序)
+     *                   综合排序规则:
+     *                   1. 销量热度(50%):近30天销量/平台总销量,min(1, ratio)
+     *                   2. 新品系数(30%):上架前7天=1.0,之后递减max(0, 1-(days-7)/23)
+     *                   3. 积分性价比(20%):积分值/商品价值
      * @param sortOrder 排序方式(asc:升序, desc:降序)
      * @return R<Page < H5ProductVo>> 所有数据
      */

+ 11 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/H5ProductVo.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import lombok.Data;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
 
 @Data
 public class H5ProductVo {
@@ -65,4 +66,14 @@ public class H5ProductVo {
      * 商品销售量
      */
     private Integer sales;
+
+    /**
+     * 上架开始时间(用于综合排序新品系数计算)
+     */
+    private LocalDate saleStartTime;
+
+    /**
+     * 综合排序得分(仅在综合排序时返回)
+     */
+    private BigDecimal comprehensiveScore;
 }

+ 24 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/PlatformProductStats.java

@@ -0,0 +1,24 @@
+package com.ylx.massage.domain.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 平台商品统计指标(用于综合排序计算)
+ *
+ * @author ylx
+ * @since 2026-04-09
+ */
+@Data
+public class PlatformProductStats {
+    /**
+     * 平台总销量
+     */
+    private Long totalSales;
+
+    /**
+     * 商品平均价值(原价)
+     */
+    private BigDecimal avgProductValue;
+}

+ 22 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/ProductSalesStats.java

@@ -0,0 +1,22 @@
+package com.ylx.massage.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 商品近30天销量统计(用于综合排序计算)
+ *
+ * @author ylx
+ * @since 2026-04-09
+ */
+@Data
+public class ProductSalesStats {
+    /**
+     * 商品ID
+     */
+    private Long productId;
+
+    /**
+     * 近30天销量(订单商品数量之和)
+     */
+    private Long last30DaysSales;
+}

+ 15 - 2
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.massage.domain.Product;
 import com.ylx.massage.domain.vo.H5ProductVo;
+import com.ylx.massage.domain.vo.PlatformProductStats;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -19,10 +20,22 @@ public interface ProductMapper extends BaseMapper<Product> {
      *
      * @param page      分页对象
      * @param product   商品查询参数
-     * @param sortField 排序字段(price:价格, sales:销量)
+     * @param name      商品名称(模糊搜索)
+     * @param sortField 排序字段(price:价格, sales:销量, comprehensive:综合排序)
      * @param sortOrder 排序方式(asc:升序, desc:降序)
      * @return Page<Product> 分页结果
      */
-    Page<H5ProductVo> selectH5Page(Page<H5ProductVo> page, @Param("product") Product product, @Param("name")String name, @Param("sortField") String sortField, @Param("sortOrder") String sortOrder);
+    Page<H5ProductVo> selectH5Page(Page<H5ProductVo> page,
+                                   @Param("product") Product product,
+                                   @Param("name") String name,
+                                   @Param("sortField") String sortField,
+                                   @Param("sortOrder") String sortOrder);
+
+    /**
+     * 查询平台商品总销量(用于综合排序计算)
+     *
+     * @return PlatformProductStats 平台统计指标
+     */
+    PlatformProductStats selectPlatformStats();
 
 }

+ 11 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductOrderItemMapper.java

@@ -3,13 +3,24 @@ package com.ylx.massage.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ylx.massage.domain.ProductOrderItem;
 import com.ylx.massage.domain.vo.OrderItemVo;
+import com.ylx.massage.domain.vo.ProductSalesStats;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 /**
  * 商品订单明细Mapper接口
  */
 @Mapper
 public interface ProductOrderItemMapper extends BaseMapper<ProductOrderItem> {
     OrderItemVo getOrderItemVoByOrderId(@Param("orderId") Long orderId);
+
+    /**
+     * 查询近30天各商品销量统计
+     * 仅统计已支付订单(payStatus=1)的商品销售数量
+     *
+     * @return List<ProductSalesStats> 商品ID与销量映射列表
+     */
+    List<ProductSalesStats> selectLast30DaysSalesStats();
 }

+ 173 - 3
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductServiceImpl.java

@@ -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));
     }
 
     /**

+ 13 - 2
nightFragrance-massage/src/main/resources/mapper/massage/ProductMapper.xml

@@ -4,13 +4,12 @@
 
     <!-- 分页查询商品列表(H5端)-支持排序 -->
     <select id="selectH5Page" resultType="com.ylx.massage.domain.vo.H5ProductVo">
-        SELECT id, category_id, product_no,name,price_point, price_money, stock, sales,product_main_image,payment_type
+        SELECT id, category_id, product_no,name,price_point, price_money, stock, sales,product_main_image,payment_type,sale_start_time
         FROM product
         WHERE deleted = 0 AND status = 1
         <if test="product.categoryId != null">
             AND category_id = #{product.categoryId}
         </if>
-        <!--根据商品名称模糊搜索-->
         <if test="name != null and name!='' ">
             and name like concat('%',#{name},'%')
         </if>
@@ -23,6 +22,10 @@
                 <if test="sortField == 'sales'">
                     sales ${sortOrder == 'asc' ? 'ASC' : 'DESC'}
                 </if>
+                <if test="sortField == 'comprehensive'">
+                    /* 综合排序:按销量降序(实际综合得分在Java层计算) */
+                    sales DESC
+                </if>
             </when>
             <otherwise>
                 ORDER BY sales DESC
@@ -30,4 +33,12 @@
         </choose>
     </select>
 
+    <!-- 查询平台商品总销量(用于综合排序计算) -->
+    <select id="selectPlatformStats" resultType="com.ylx.massage.domain.vo.PlatformProductStats">
+        SELECT
+            COALESCE(SUM(sales), 0) as totalSales
+        FROM product
+        WHERE deleted = 0 AND status = 1
+    </select>
+
 </mapper>

+ 13 - 0
nightFragrance-massage/src/main/resources/mapper/massage/ProductOrderItemMapper.xml

@@ -18,4 +18,17 @@
         WHERE
             poi.order_id = #{orderId}
     </select>
+
+    <!-- 查询近30天各商品销量统计(仅统计已支付订单) -->
+    <select id="selectLast30DaysSalesStats" resultType="com.ylx.massage.domain.vo.ProductSalesStats">
+        SELECT
+            poi.product_id AS productId,
+            SUM(poi.quantity) AS last30DaysSales
+        FROM product_order_item poi
+        INNER JOIN product_order_info poi2 ON poi.order_id = poi2.id
+        WHERE poi2.pay_status = 1
+          AND poi2.is_deleted = 0
+          AND poi2.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
+        GROUP BY poi.product_id
+    </select>
 </mapper>