feat(product): 添加商品下架和多条件查询功能

- 实现商品下架功能,下架后商品所有SKU库存改为0,链接失效无法访问
- 添加商品多条件查询接口,支持名称、链接码、商品状态、发售地区查询
- 新增ProductQueryRequestDTO用于商品查询条件传递
- 优化商品详情访问逻辑,下架商品无法访问
- 优化库存验证逻辑,库存为0时不能创建订单
- 优化订单创建流程,添加商品状态验证,下架商品不能创建订单
This commit is contained in:
2025-12-25 16:25:09 +08:00
parent 7a97ddc860
commit 8fb3cdb4b7
7 changed files with 2663 additions and 3 deletions

View File

@@ -2,6 +2,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.ProductResponseDTO;
import com.mtkj.mtpay.service.ProductService;
import jakarta.validation.Valid;
@@ -68,7 +69,7 @@ public class ProductController {
}
/**
* 获取商品列表
* 获取商品列表(无查询条件)
*/
@GetMapping("/list")
public Result<List<ProductResponseDTO>> listProducts() {
@@ -77,6 +78,17 @@ public class ProductController {
return Result.success(products);
}
/**
* 查询商品列表(支持多条件查询)
* 支持:名称查询、链接查询、商品状态查询、发售地区查询
*/
@PostMapping("/query")
public Result<List<ProductResponseDTO>> queryProducts(@RequestBody ProductQueryRequestDTO query) {
log.info("查询商品列表,查询条件:{}", query);
List<ProductResponseDTO> products = productService.queryProducts(query);
return Result.success(products);
}
/**
* 获取商品详情页URL
*/
@@ -218,6 +230,17 @@ public class ProductController {
}
}
/**
* 下架商品
* 下架后商品所有SKU库存改为0链接失效无法再被访问
*/
@PutMapping("/{id}/off-shelf")
public Result<String> offShelfProduct(@PathVariable Long id) {
log.info("下架商品请求商品ID{}", id);
productService.offShelfProduct(id);
return Result.success("商品下架成功");
}
/**
* 判断是否为图片文件
*/

View File

@@ -0,0 +1,31 @@
package com.mtkj.mtpay.dto.request;
import lombok.Data;
/**
* 商品查询请求DTO
*/
@Data
public class ProductQueryRequestDTO {
/**
* 商品名称(模糊查询)
*/
private String name;
/**
* 商品链接码(精确查询)
*/
private String linkCode;
/**
* 商品状态ACTIVE-上架INACTIVE-下架)
*/
private String status;
/**
* 发售地区通过SKU的currency查询MYR, PHP, THB, VND, SGD, CNY, USD等
*/
private String salesRegion;
}

View File

@@ -1,6 +1,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.ProductResponseDTO;
import java.util.List;
@@ -43,5 +44,19 @@ public interface ProductService {
* @return 商品列表
*/
List<ProductResponseDTO> listProducts();
/**
* 下架商品
* 下架后商品所有SKU库存改为0链接失效无法再被访问
* @param id 商品ID
*/
void offShelfProduct(Long id);
/**
* 查询商品列表(支持多条件查询)
* @param query 查询条件
* @return 商品列表
*/
List<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query);
}

View File

@@ -2,6 +2,7 @@ package com.mtkj.mtpay.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mtkj.mtpay.common.ResultCode;
import com.mtkj.mtpay.common.enums.ProductStatus;
import com.mtkj.mtpay.dto.request.CreateCustomerOrderRequestDTO;
import com.mtkj.mtpay.dto.response.CustomerOrderResponseDTO;
import com.mtkj.mtpay.entity.CustomerOrder;
@@ -52,6 +53,12 @@ public class CustomerOrderServiceImpl implements CustomerOrderService {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "商品不存在");
}
// 验证商品状态:下架商品不能创建订单
if (ProductStatus.INACTIVE.getCode().equals(product.getStatus())) {
log.warn("商品已下架无法创建订单商品ID: {}", request.getProductId());
throw new BusinessException(ResultCode.BUSINESS_ERROR, "商品已下架,无法创建订单");
}
// 验证SKU是否存在
MtProductSku sku = productSkuMapper.selectById(request.getSkuId());
if (sku == null || !sku.getProductId().equals(request.getProductId())) {
@@ -60,8 +67,13 @@ public class CustomerOrderServiceImpl implements CustomerOrderService {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "SKU不存在");
}
// 验证库存
if (sku.getStock() == null || sku.getStock() < request.getQuantity()) {
// 验证库存库存为0或不足时不能创建订单
if (sku.getStock() == null || sku.getStock() <= 0) {
log.warn("库存为0无法创建订单SKU ID: {}, 库存: {}",
request.getSkuId(), sku.getStock());
throw new BusinessException(ResultCode.BUSINESS_ERROR, "商品库存为0无法创建订单");
}
if (sku.getStock() < request.getQuantity()) {
log.warn("库存不足SKU ID: {}, 库存: {}, 需要: {}",
request.getSkuId(), sku.getStock(), request.getQuantity());
throw new BusinessException(ResultCode.BUSINESS_ERROR, "库存不足");

View File

@@ -7,6 +7,7 @@ import com.mtkj.mtpay.common.ResultCode;
import com.mtkj.mtpay.common.enums.ProductStatus;
import com.mtkj.mtpay.common.enums.SkuStatus;
import com.mtkj.mtpay.dto.request.CreateProductRequestDTO;
import com.mtkj.mtpay.dto.request.ProductQueryRequestDTO;
import com.mtkj.mtpay.dto.response.ProductResponseDTO;
import com.mtkj.mtpay.entity.MtProduct;
import com.mtkj.mtpay.entity.MtProductSku;
@@ -24,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -252,6 +254,12 @@ public class ProductServiceImpl implements ProductService {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "商品不存在");
}
// 检查商品状态:下架商品不能访问
if (ProductStatus.INACTIVE.getCode().equals(product.getStatus())) {
log.warn("商品已下架无法访问商品ID: {}", id);
throw new BusinessException(ResultCode.BUSINESS_ERROR, "商品已下架,无法访问");
}
// 查询SKU列表
LambdaQueryWrapper<MtProductSku> skuWrapper = new LambdaQueryWrapper<>();
skuWrapper.eq(MtProductSku::getProductId, id);
@@ -317,9 +325,62 @@ public class ProductServiceImpl implements ProductService {
}
log.debug("根据链接码获取商品ID成功链接码: {}, 商品ID: {}", linkCode, productId);
// 检查商品状态:下架商品不能通过链接访问
MtProduct product = productMapper.selectById(productId);
if (product != null && ProductStatus.INACTIVE.getCode().equals(product.getStatus())) {
log.warn("商品已下架,链接失效,链接码: {}, 商品ID: {}", linkCode, productId);
throw new BusinessException(ResultCode.BUSINESS_ERROR, "商品已下架,链接失效");
}
return productId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void offShelfProduct(Long id) {
log.info("下架商品商品ID: {}", id);
// 检查商品是否存在
MtProduct product = productMapper.selectById(id);
if (product == null) {
log.warn("商品不存在无法下架商品ID: {}", id);
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "商品不存在");
}
// 检查商品是否已经下架
if (ProductStatus.INACTIVE.getCode().equals(product.getStatus())) {
log.warn("商品已经下架商品ID: {}", id);
throw new BusinessException(ResultCode.BUSINESS_ERROR, "商品已经下架");
}
// 更新商品状态为下架
product.setStatus(ProductStatus.INACTIVE.getCode());
int updateCount = productMapper.updateById(product);
if (updateCount <= 0) {
log.error("更新商品状态失败商品ID: {}", id);
throw new BusinessException(ResultCode.SYSTEM_ERROR, "下架商品失败");
}
log.info("商品状态已更新为下架商品ID: {}", id);
// 将该商品所有SKU的库存改为0
LambdaQueryWrapper<MtProductSku> skuWrapper = new LambdaQueryWrapper<>();
skuWrapper.eq(MtProductSku::getProductId, id);
List<MtProductSku> skus = productSkuMapper.selectList(skuWrapper);
if (!skus.isEmpty()) {
for (MtProductSku sku : skus) {
sku.setStock(0);
productSkuMapper.updateById(sku);
}
log.info("商品所有SKU库存已设置为0商品ID: {}, SKU数量: {}", id, skus.size());
} else {
log.info("商品没有SKU商品ID: {}", id);
}
log.info("商品下架成功商品ID: {}, SKU数量: {}", id, skus.size());
}
@Override
public List<ProductResponseDTO> listProducts() {
long startTime = System.currentTimeMillis();
@@ -596,5 +657,184 @@ public class ProductServiceImpl implements ProductService {
}
}
@Override
public List<ProductResponseDTO> queryProducts(ProductQueryRequestDTO query) {
long startTime = System.currentTimeMillis();
log.info("查询商品列表,查询条件: {}", query);
// 1. 构建商品查询条件
LambdaQueryWrapper<MtProduct> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ne(MtProduct::getStatus, "DELETED"); // 排除已删除的商品
// 商品名称模糊查询
if (query.getName() != null && !query.getName().trim().isEmpty()) {
queryWrapper.like(MtProduct::getName, query.getName().trim());
log.debug("添加商品名称查询条件: {}", query.getName());
}
// 商品状态查询
if (query.getStatus() != null && !query.getStatus().trim().isEmpty()) {
queryWrapper.eq(MtProduct::getStatus, query.getStatus().trim());
log.debug("添加商品状态查询条件: {}", query.getStatus());
}
// 如果指定了链接码先通过链接码获取商品ID
List<Long> productIdsByLink = null;
if (query.getLinkCode() != null && !query.getLinkCode().trim().isEmpty()) {
try {
Long productId = productLinkService.getProductIdByLinkCode(query.getLinkCode().trim());
if (productId != null) {
productIdsByLink = new ArrayList<>();
productIdsByLink.add(productId);
log.debug("通过链接码查询到商品ID: {}", productId);
} else {
log.debug("链接码无效,未找到商品: {}", query.getLinkCode());
// 如果链接码无效,返回空列表
return new ArrayList<>();
}
} catch (Exception e) {
log.warn("通过链接码查询商品失败,链接码: {}", query.getLinkCode(), e);
// 链接码无效,返回空列表
return new ArrayList<>();
}
}
// 如果指定了发售地区通过SKU的currency查询先查询符合条件的商品ID
List<Long> productIdsByRegion = null;
if (query.getSalesRegion() != null && !query.getSalesRegion().trim().isEmpty()) {
String currency = query.getSalesRegion().trim().toUpperCase();
LambdaQueryWrapper<MtProductSku> skuQueryWrapper = new LambdaQueryWrapper<>();
skuQueryWrapper.eq(MtProductSku::getCurrency, currency);
List<MtProductSku> skus = productSkuMapper.selectList(skuQueryWrapper);
if (!skus.isEmpty()) {
productIdsByRegion = skus.stream()
.map(MtProductSku::getProductId)
.distinct()
.collect(Collectors.toList());
log.debug("通过发售地区查询到商品ID数量: {}, 货币: {}", productIdsByRegion.size(), currency);
} else {
log.debug("发售地区无匹配商品,货币: {}", currency);
// 如果发售地区无匹配,返回空列表
return new ArrayList<>();
}
}
// 合并链接码和发售地区的商品ID条件
if (productIdsByLink != null && productIdsByRegion != null) {
// 两个条件都指定,取交集
productIdsByLink.retainAll(productIdsByRegion);
if (productIdsByLink.isEmpty()) {
log.debug("链接码和发售地区条件无交集,返回空列表");
return new ArrayList<>();
}
queryWrapper.in(MtProduct::getId, productIdsByLink);
} else if (productIdsByLink != null) {
// 只指定了链接码
queryWrapper.in(MtProduct::getId, productIdsByLink);
} else if (productIdsByRegion != null) {
// 只指定了发售地区
queryWrapper.in(MtProduct::getId, productIdsByRegion);
}
queryWrapper.orderByDesc(MtProduct::getCreateTime);
List<MtProduct> products = productMapper.selectList(queryWrapper);
log.debug("查询到商品数量: {}", products.size());
if (products.isEmpty()) {
log.info("商品列表为空,耗时: {}ms", System.currentTimeMillis() - startTime);
return new ArrayList<>();
}
// 2. 批量查询所有商品的SKU - 1次查询优化N+1问题
List<Long> productIds = products.stream()
.map(MtProduct::getId)
.collect(Collectors.toList());
LambdaQueryWrapper<MtProductSku> skuWrapper = new LambdaQueryWrapper<>();
skuWrapper.in(MtProductSku::getProductId, productIds);
List<MtProductSku> allSkus = productSkuMapper.selectList(skuWrapper);
// 3. 按productId分组SKU在内存中分组避免N+1查询
Map<Long, List<MtProductSku>> skuMap = allSkus.stream()
.collect(Collectors.groupingBy(MtProductSku::getProductId));
log.debug("批量查询到SKU数量: {}, 涉及商品数: {}", allSkus.size(), skuMap.size());
// 4. 批量查询所有商品的链接优化N+1查询
Map<Long, String> productUrlMap = new HashMap<>();
try {
if (!productIds.isEmpty()) {
LambdaQueryWrapper<com.mtkj.mtpay.entity.MtProductLink> linkWrapper = new LambdaQueryWrapper<>();
linkWrapper.in(com.mtkj.mtpay.entity.MtProductLink::getProductId, productIds)
.eq(com.mtkj.mtpay.entity.MtProductLink::getStatus, "ACTIVE")
.gt(com.mtkj.mtpay.entity.MtProductLink::getExpireTime, java.time.LocalDateTime.now())
.orderByDesc(com.mtkj.mtpay.entity.MtProductLink::getCreateTime);
List<com.mtkj.mtpay.entity.MtProductLink> allLinks = productLinkMapper.selectList(linkWrapper);
// 按productId分组每个商品取最新的有效链接已按createTime降序排序
Map<Long, List<com.mtkj.mtpay.entity.MtProductLink>> linkGroupMap = allLinks.stream()
.collect(Collectors.groupingBy(
com.mtkj.mtpay.entity.MtProductLink::getProductId,
LinkedHashMap::new,
Collectors.toList()
));
for (Map.Entry<Long, List<com.mtkj.mtpay.entity.MtProductLink>> entry : linkGroupMap.entrySet()) {
Long pid = entry.getKey();
List<com.mtkj.mtpay.entity.MtProductLink> links = entry.getValue();
if (!links.isEmpty()) {
productUrlMap.put(pid, links.get(0).getFullUrl());
}
}
log.debug("批量查询到商品链接数量: {}", productUrlMap.size());
}
} catch (Exception e) {
log.warn("批量查询商品链接失败", e);
}
// 5. 组装响应数据
List<ProductResponseDTO> result = new ArrayList<>();
for (MtProduct product : products) {
ProductResponseDTO dto = new ProductResponseDTO();
BeanUtils.copyProperties(product, dto);
// 处理主图
processMainImage(product, dto);
// 设置SKU列表
List<MtProductSku> productSkus = skuMap.getOrDefault(product.getId(), new ArrayList<>());
List<ProductResponseDTO.ProductSkuResponseDTO> skuDTOs = productSkus.stream().map(sku -> {
ProductResponseDTO.ProductSkuResponseDTO skuDTO = new ProductResponseDTO.ProductSkuResponseDTO();
BeanUtils.copyProperties(sku, skuDTO);
return skuDTO;
}).collect(Collectors.toList());
dto.setSkus(skuDTOs);
// 设置商品链接
String productUrl = productUrlMap.get(product.getId());
if (productUrl == null) {
// 如果没有找到有效链接,尝试创建新链接
try {
com.mtkj.mtpay.entity.MtProductLink link = productLinkService.createOrGetProductLink(product.getId(), 90);
productUrl = link.getFullUrl();
} catch (Exception e) {
log.warn("创建商品链接失败商品ID: {}", product.getId(), e);
}
}
dto.setProductUrl(productUrl);
result.add(dto);
}
long endTime = System.currentTimeMillis();
log.info("查询商品列表完成,查询条件: {}, 结果数量: {}, 耗时: {}ms",
query, result.size(), endTime - startTime);
return result;
}
}