feat(product): 商品列表查询支持分页功能

- 添加分页参数pageNum和pageSize到ProductQueryRequestDTO
- 将查询接口返回类型从List改为PageResult分页结果
- 实现MyBatis-Plus分页查询功能
- 添加链接码URL解析功能,支持完整URL或纯链接码输入
- 限制每页最大数量防止性能问题
- 添加分页相关的日志记录和调试信息
- 创建PageResult响应DTO类,包含分页元数据信息
This commit is contained in:
2025-12-25 17:11:30 +08:00
parent 8fb3cdb4b7
commit 10d0bfa9f6
7 changed files with 2832 additions and 26 deletions

View File

@@ -3,6 +3,7 @@ package com.mtkj.mtpay.controller;
import com.mtkj.mtpay.common.Result;
import com.mtkj.mtpay.dto.request.CreateProductRequestDTO;
import com.mtkj.mtpay.dto.request.ProductQueryRequestDTO;
import com.mtkj.mtpay.dto.response.PageResult;
import com.mtkj.mtpay.dto.response.ProductResponseDTO;
import com.mtkj.mtpay.service.ProductService;
import jakarta.validation.Valid;
@@ -79,14 +80,14 @@ public class ProductController {
}
/**
* 查询商品列表(支持多条件查询)
* 查询商品列表(支持多条件查询和分页
* 支持:名称查询、链接查询、商品状态查询、发售地区查询
*/
@PostMapping("/query")
public Result<List<ProductResponseDTO>> queryProducts(@RequestBody ProductQueryRequestDTO query) {
public Result<PageResult<ProductResponseDTO>> queryProducts(@RequestBody ProductQueryRequestDTO query) {
log.info("查询商品列表,查询条件:{}", query);
List<ProductResponseDTO> products = productService.queryProducts(query);
return Result.success(products);
PageResult<ProductResponseDTO> pageResult = productService.queryProducts(query);
return Result.success(pageResult);
}
/**

View File

@@ -27,5 +27,15 @@ public class ProductQueryRequestDTO {
* 发售地区通过SKU的currency查询MYR, PHP, THB, VND, SGD, CNY, USD等
*/
private String salesRegion;
/**
* 当前页码从1开始默认1
*/
private Integer pageNum = 1;
/**
* 每页大小默认10
*/
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,82 @@
package com.mtkj.mtpay.dto.response;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果响应DTO
*/
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码从1开始
*/
private Long current;
/**
* 每页大小
*/
private Long size;
/**
* 总记录数
*/
private Long total;
/**
* 总页数
*/
private Long pages;
/**
* 数据列表
*/
private List<T> records;
/**
* 是否有上一页
*/
private Boolean hasPrevious;
/**
* 是否有下一页
*/
private Boolean hasNext;
/**
* 是否为第一页
*/
private Boolean isFirst;
/**
* 是否为最后一页
*/
private Boolean isLast;
public PageResult() {
}
public PageResult(Long current, Long size, Long total, List<T> records) {
this.current = current;
this.size = size;
this.total = total;
this.records = records;
// 计算总页数
this.pages = (total + size - 1) / size;
// 计算是否有上一页和下一页
this.hasPrevious = current > 1;
this.hasNext = current < pages;
// 计算是否为第一页和最后一页
this.isFirst = current == 1;
this.isLast = current >= pages || pages == 0;
}
}

View File

@@ -2,6 +2,7 @@ package com.mtkj.mtpay.service;
import com.mtkj.mtpay.dto.request.CreateProductRequestDTO;
import com.mtkj.mtpay.dto.request.ProductQueryRequestDTO;
import com.mtkj.mtpay.dto.response.PageResult;
import com.mtkj.mtpay.dto.response.ProductResponseDTO;
import java.util.List;
@@ -53,10 +54,10 @@ public interface ProductService {
void offShelfProduct(Long id);
/**
* 查询商品列表(支持多条件查询)
* @param query 查询条件
* @return 商品列表
* 查询商品列表(支持多条件查询和分页
* @param query 查询条件(包含分页参数)
* @return 分页结果
*/
List<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query);
PageResult<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query);
}

View File

@@ -6,8 +6,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.mtkj.mtpay.common.ResultCode;
import com.mtkj.mtpay.common.enums.ProductStatus;
import com.mtkj.mtpay.common.enums.SkuStatus;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mtkj.mtpay.dto.request.CreateProductRequestDTO;
import com.mtkj.mtpay.dto.request.ProductQueryRequestDTO;
import com.mtkj.mtpay.dto.response.PageResult;
import com.mtkj.mtpay.dto.response.ProductResponseDTO;
import com.mtkj.mtpay.entity.MtProduct;
import com.mtkj.mtpay.entity.MtProductSku;
@@ -658,9 +661,20 @@ public class ProductServiceImpl implements ProductService {
}
@Override
public List<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query) {
public PageResult<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query) {
long startTime = System.currentTimeMillis();
log.info("查询商品列表,查询条件: {}", query);
// 处理分页参数
int pageNum = query.getPageNum() != null && query.getPageNum() > 0 ? query.getPageNum() : 1;
int pageSize = query.getPageSize() != null && query.getPageSize() > 0 ? query.getPageSize() : 10;
// 限制每页最大数量,防止性能问题
if (pageSize > 100) {
pageSize = 100;
log.warn("每页大小超过限制已调整为100原始值: {}", query.getPageSize());
}
log.info("查询商品列表,查询条件: {}, 页码: {}, 每页大小: {}", query, pageNum, pageSize);
// 1. 构建商品查询条件
LambdaQueryWrapper<MtProduct> queryWrapper = new LambdaQueryWrapper<>();
@@ -679,23 +693,29 @@ public class ProductServiceImpl implements ProductService {
}
// 如果指定了链接码先通过链接码获取商品ID
// 支持完整URL或链接码两种输入方式
List<Long> productIdsByLink = null;
if (query.getLinkCode() != null && !query.getLinkCode().trim().isEmpty()) {
try {
Long productId = productLinkService.getProductIdByLinkCode(query.getLinkCode().trim());
String linkCodeInput = query.getLinkCode().trim();
String linkCode = extractLinkCodeFromUrl(linkCodeInput);
log.debug("提取链接码,输入: {}, 提取结果: {}", linkCodeInput, linkCode);
Long productId = productLinkService.getProductIdByLinkCode(linkCode);
if (productId != null) {
productIdsByLink = new ArrayList<>();
productIdsByLink.add(productId);
log.debug("通过链接码查询到商品ID: {}", productId);
} else {
log.debug("链接码无效,未找到商品: {}", query.getLinkCode());
// 如果链接码无效,返回空列表
return new ArrayList<>();
log.debug("链接码无效,未找到商品: {}", linkCode);
// 如果链接码无效,返回空分页结果
return new PageResult<>((long) pageNum, (long) pageSize, 0L, new ArrayList<>());
}
} catch (Exception e) {
log.warn("通过链接码查询商品失败,链接码: {}", query.getLinkCode(), e);
// 链接码无效,返回空列表
return new ArrayList<>();
// 链接码无效,返回空分页结果
return new PageResult<>((long) pageNum, (long) pageSize, 0L, new ArrayList<>());
}
}
@@ -714,8 +734,8 @@ public class ProductServiceImpl implements ProductService {
log.debug("通过发售地区查询到商品ID数量: {}, 货币: {}", productIdsByRegion.size(), currency);
} else {
log.debug("发售地区无匹配商品,货币: {}", currency);
// 如果发售地区无匹配,返回空列表
return new ArrayList<>();
// 如果发售地区无匹配,返回空分页结果
return new PageResult<>((long) pageNum, (long) pageSize, 0L, new ArrayList<>());
}
}
@@ -724,8 +744,8 @@ public class ProductServiceImpl implements ProductService {
// 两个条件都指定,取交集
productIdsByLink.retainAll(productIdsByRegion);
if (productIdsByLink.isEmpty()) {
log.debug("链接码和发售地区条件无交集,返回空列表");
return new ArrayList<>();
log.debug("链接码和发售地区条件无交集,返回空分页结果");
return new PageResult<>((long) pageNum, (long) pageSize, 0L, new ArrayList<>());
}
queryWrapper.in(MtProduct::getId, productIdsByLink);
} else if (productIdsByLink != null) {
@@ -737,13 +757,17 @@ public class ProductServiceImpl implements ProductService {
}
queryWrapper.orderByDesc(MtProduct::getCreateTime);
List<MtProduct> products = productMapper.selectList(queryWrapper);
log.debug("查询到商品数量: {}", products.size());
// 使用MyBatis-Plus分页查询
Page<MtProduct> page = new Page<>(pageNum, pageSize);
IPage<MtProduct> productPage = productMapper.selectPage(page, queryWrapper);
List<MtProduct> products = productPage.getRecords();
log.debug("查询到商品数量: {}/{}, 总记录数: {}", products.size(), pageSize, productPage.getTotal());
if (products.isEmpty()) {
log.info("商品列表为空,耗时: {}ms", System.currentTimeMillis() - startTime);
return new ArrayList<>();
return new PageResult<>((long) pageNum, (long) pageSize, productPage.getTotal(), new ArrayList<>());
}
// 2. 批量查询所有商品的SKU - 1次查询优化N+1问题
@@ -830,10 +854,73 @@ public class ProductServiceImpl implements ProductService {
}
long endTime = System.currentTimeMillis();
log.info("查询商品列表完成,查询条件: {}, 结果数量: {}, 耗时: {}ms",
query, result.size(), endTime - startTime);
log.info("查询商品列表完成,查询条件: {}, 结果数量: {}/{}, 总记录数: {}, 耗时: {}ms",
query, result.size(), pageSize, productPage.getTotal(), endTime - startTime);
return result;
// 构建分页结果
return new PageResult<>(
productPage.getCurrent(),
productPage.getSize(),
productPage.getTotal(),
result
);
}
/**
* 从完整URL中提取链接码
* 支持两种输入:
* 1. 完整URLhttp://localhost:3000/product/2563c74bd94c4a4b95abccf697a6c756
* 2. 链接码2563c74bd94c4a4b95abccf697a6c756
*
* @param input 用户输入的URL或链接码
* @return 提取的链接码
*/
private String extractLinkCodeFromUrl(String input) {
if (input == null || input.trim().isEmpty()) {
return input;
}
String trimmed = input.trim();
// 如果包含 "/product/"说明是完整URL提取链接码
int productIndex = trimmed.indexOf("/product/");
if (productIndex >= 0) {
String linkCode = trimmed.substring(productIndex + "/product/".length());
// 移除可能的查询参数和锚点
int queryIndex = linkCode.indexOf("?");
if (queryIndex >= 0) {
linkCode = linkCode.substring(0, queryIndex);
}
int hashIndex = linkCode.indexOf("#");
if (hashIndex >= 0) {
linkCode = linkCode.substring(0, hashIndex);
}
// 移除尾部斜杠
linkCode = linkCode.replaceAll("/$", "");
return linkCode.trim();
}
// 如果不包含 "/product/",可能是直接输入的链接码,直接返回
// 但也可能是其他格式的URL尝试提取最后一段作为链接码
if (trimmed.contains("/")) {
String[] parts = trimmed.split("/");
if (parts.length > 0) {
String lastPart = parts[parts.length - 1];
// 移除查询参数和锚点
int queryIndex = lastPart.indexOf("?");
if (queryIndex >= 0) {
lastPart = lastPart.substring(0, queryIndex);
}
int hashIndex = lastPart.indexOf("#");
if (hashIndex >= 0) {
lastPart = lastPart.substring(0, hashIndex);
}
return lastPart.trim();
}
}
// 直接返回(可能是纯链接码)
return trimmed;
}
}