feat(product): 添加商品下架和多条件查询功能
- 实现商品下架功能,下架后商品所有SKU库存改为0,链接失效无法访问 - 添加商品多条件查询接口,支持名称、链接码、商品状态、发售地区查询 - 新增ProductQueryRequestDTO用于商品查询条件传递 - 优化商品详情访问逻辑,下架商品无法访问 - 优化库存验证逻辑,库存为0时不能创建订单 - 优化订单创建流程,添加商品状态验证,下架商品不能创建订单
This commit is contained in:
@@ -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("商品下架成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片文件
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "库存不足");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user