|
|
@@ -0,0 +1,391 @@
|
|
|
+package com.ylx.massage.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import com.ylx.massage.domain.*;
|
|
|
+import com.ylx.massage.domain.dto.ProductCreateDTO;
|
|
|
+import com.ylx.massage.domain.dto.ProductSkuDTO;
|
|
|
+import com.ylx.massage.domain.dto.ProductSpecDTO;
|
|
|
+import com.ylx.massage.domain.dto.ProductSpecSetupDTO;
|
|
|
+import com.ylx.massage.domain.dto.ProductSpecValueDTO;
|
|
|
+import com.ylx.massage.domain.vo.SpecComboVO;
|
|
|
+import com.ylx.massage.mapper.*;
|
|
|
+import com.ylx.massage.service.ProductService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 商品表(Product)表服务实现类
|
|
|
+ *
|
|
|
+ * @author ylx
|
|
|
+ * @since 2026-03-26
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service("productService")
|
|
|
+public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductCategoryMapper productCategoryMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductImageMapper productImageMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductSpecMapper productSpecMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductSpecValueMapper productSpecValueMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ProductSkuMapper productSkuMapper;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 新增商品(包含规格、SKU、图片)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public Long createProduct(ProductCreateDTO dto) {
|
|
|
+ // 1. 校验分类是否存在
|
|
|
+ ProductCategory category = productCategoryMapper.selectById(dto.getCategoryId());
|
|
|
+ if (category == null) {
|
|
|
+ throw new RuntimeException("商品分类不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 生成商品编号
|
|
|
+ String productNo = generateProductNo(dto.getCategoryId());
|
|
|
+
|
|
|
+ // 3. 保存商品基本信息
|
|
|
+ Product product = new Product();
|
|
|
+ product.setProductNo(productNo);
|
|
|
+ product.setCategoryId(dto.getCategoryId());
|
|
|
+ product.setName(dto.getName());
|
|
|
+ product.setProductMainImage(dto.getProductMainImage());
|
|
|
+ product.setProductImage(dto.getProductImage());
|
|
|
+ product.setSummary(dto.getSummary());
|
|
|
+ product.setDetail(dto.getDetail());
|
|
|
+ product.setPricePoint(dto.getPricePoint());
|
|
|
+ product.setPriceMoney(dto.getPriceMoney());
|
|
|
+ product.setStock(dto.getStock());
|
|
|
+ product.setFreight(dto.getFreight());
|
|
|
+ product.setDeliveryTime(dto.getDeliveryTime());
|
|
|
+ product.setServicePromise(dto.getServicePromise());
|
|
|
+ product.setSaleStartTime(dto.getSaleStartTime());
|
|
|
+ product.setSaleEndTime(dto.getSaleEndTime());
|
|
|
+ //product.setHasSpec(dto.getHasSpec());
|
|
|
+ product.setStatus(0); // 默认下架
|
|
|
+ product.setSales(0); // 初始销量为0
|
|
|
+ product.setDeleted(0); // 未删除
|
|
|
+
|
|
|
+ // 计算总库存
|
|
|
+ if (!CollectionUtils.isEmpty(dto.getSkuList())) {
|
|
|
+ int totalStock = dto.getSkuList().stream()
|
|
|
+ .mapToInt(ProductSkuDTO::getStock)
|
|
|
+ .sum();
|
|
|
+ product.setStock(totalStock);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.save(product);
|
|
|
+ Long productId = product.getId();
|
|
|
+
|
|
|
+ // 4. 保存商品副图
|
|
|
+ saveProductImages(productId, dto.getProductImage());
|
|
|
+
|
|
|
+ // 5. 如果有规格,保存规格和SKU
|
|
|
+ if (!CollectionUtils.isEmpty(dto.getSpecList())) {
|
|
|
+ saveProductSpecsAndSkus(productId, dto);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("商品创建成功,商品ID:{},商品编号:{}", productId, productNo);
|
|
|
+ return productId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成商品编号(公开方法,供Controller调用)
|
|
|
+ * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
|
|
|
+ * 示例:AD20260327001
|
|
|
+ *
|
|
|
+ * @param categoryId 分类ID
|
|
|
+ * @return 生成的商品编号
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public String generateProductNo(Long categoryId) {
|
|
|
+ // 1. 根据分类ID获取分类信息
|
|
|
+ ProductCategory category = productCategoryMapper.selectById(categoryId);
|
|
|
+ if (category == null) {
|
|
|
+ throw new RuntimeException("商品分类不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取分类编码
|
|
|
+ String categoryCode = category.getCode();
|
|
|
+ if (categoryCode == null || categoryCode.trim().isEmpty()) {
|
|
|
+ throw new RuntimeException("商品分类编码不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 生成商品编号
|
|
|
+ return generateProductNoByCategoryCode(categoryCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据分类编码生成商品编号(私有方法)
|
|
|
+ * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
|
|
|
+ *
|
|
|
+ * @param categoryCode 分类编码
|
|
|
+ * @return String 生成的商品编号
|
|
|
+ */
|
|
|
+ private String generateProductNoByCategoryCode(String categoryCode) {
|
|
|
+ String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
|
|
+ String prefix = categoryCode.toUpperCase();
|
|
|
+
|
|
|
+ // 查询当天该分类下最大编号
|
|
|
+ LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.likeRight(Product::getProductNo, prefix + dateStr).orderByDesc(Product::getProductNo).last("LIMIT 1");
|
|
|
+ Product lastProduct = this.getOne(wrapper);
|
|
|
+
|
|
|
+ int seq = 1;
|
|
|
+ if (lastProduct != null && lastProduct.getProductNo() != null) {
|
|
|
+ String lastNo = lastProduct.getProductNo();
|
|
|
+ // 从编号中提取序列号(最后3位)
|
|
|
+ String seqStr = lastNo.substring(lastNo.length() - 3);
|
|
|
+ try {
|
|
|
+ seq = Integer.parseInt(seqStr) + 1;
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析商品编号序列号失败:{}", lastNo);
|
|
|
+ seq = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return prefix + dateStr + String.format("%03d", seq);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存商品图片
|
|
|
+ */
|
|
|
+ private void saveProductImages(Long productId, List<String> imageUrls) {
|
|
|
+ if (CollectionUtils.isEmpty(imageUrls)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<ProductImage> images = new ArrayList<>();
|
|
|
+ for (int i = 0; i < imageUrls.size(); i++) {
|
|
|
+ ProductImage image = new ProductImage();
|
|
|
+ image.setProductId(productId);
|
|
|
+ image.setUrl(imageUrls.get(i));
|
|
|
+ image.setType(2); // 副图
|
|
|
+ image.setSort(i + 1);
|
|
|
+ image.setCreatedAt(LocalDateTime.now());
|
|
|
+ images.add(image);
|
|
|
+ }
|
|
|
+ images.forEach(productImageMapper::insert);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存商品规格和SKU
|
|
|
+ */
|
|
|
+ private void saveProductSpecsAndSkus(Long productId, ProductCreateDTO dto) {
|
|
|
+ List<ProductSpecDTO> specList = dto.getSpecList();
|
|
|
+ List<ProductSkuDTO> skuList = dto.getSkuList();
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(specList) || CollectionUtils.isEmpty(skuList)) {
|
|
|
+ throw new RuntimeException("规格商品必须包含规格和SKU信息");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存规格名和规格值,并记录ID映射
|
|
|
+ AtomicInteger specIndex = new AtomicInteger(0);
|
|
|
+
|
|
|
+ for (ProductSpecDTO specDTO : specList) {
|
|
|
+ // 保存规格名
|
|
|
+ ProductSpec spec = new ProductSpec();
|
|
|
+ spec.setProductId(productId);
|
|
|
+ spec.setSpecName(specDTO.getSpecName());
|
|
|
+ spec.setSort(specDTO.getSort() != null ? specDTO.getSort() : specIndex.get() + 1);
|
|
|
+ spec.setCreateTime(LocalDateTime.now());
|
|
|
+ productSpecMapper.insert(spec);
|
|
|
+ Long specId = spec.getId();
|
|
|
+
|
|
|
+ // 保存规格值
|
|
|
+ if (!CollectionUtils.isEmpty(specDTO.getSpecValues())) {
|
|
|
+ AtomicInteger valueIndex = new AtomicInteger(0);
|
|
|
+ for (ProductSpecValueDTO valueDTO : specDTO.getSpecValues()) {
|
|
|
+ ProductSpecValue specValue = new ProductSpecValue();
|
|
|
+ specValue.setSpecId(specId);
|
|
|
+ specValue.setProductId(productId);
|
|
|
+ specValue.setSpecValue(valueDTO.getSpecValue());
|
|
|
+ specValue.setSort(valueDTO.getSort() != null ? valueDTO.getSort() : valueIndex.get() + 1);
|
|
|
+ specValue.setCreateTime(LocalDateTime.now());
|
|
|
+ productSpecValueMapper.insert(specValue);
|
|
|
+ valueIndex.getAndIncrement();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ specIndex.getAndIncrement();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存SKU
|
|
|
+ for (ProductSkuDTO skuDTO : skuList) {
|
|
|
+ ProductSku sku = new ProductSku();
|
|
|
+ sku.setProductId(productId);
|
|
|
+ sku.setSkuNo(generateSkuNo(productId));
|
|
|
+ sku.setSpecCombo(skuDTO.getSpecCombo());
|
|
|
+ sku.setSpecValueIds(skuDTO.getSpecValueIndexCombo());
|
|
|
+ sku.setImage(skuDTO.getImage());
|
|
|
+ sku.setPriceMoney(skuDTO.getPriceMoney());
|
|
|
+ sku.setPricePoint(skuDTO.getPricePoint());
|
|
|
+ sku.setOriginPrice(skuDTO.getOriginPrice());
|
|
|
+ sku.setStock(skuDTO.getStock());
|
|
|
+ sku.setSales(0);
|
|
|
+ sku.setStatus(1);
|
|
|
+ sku.setCreatedAt(LocalDateTime.now());
|
|
|
+ sku.setUpdatedAt(LocalDateTime.now());
|
|
|
+ productSkuMapper.insert(sku);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成SKU编号
|
|
|
+ */
|
|
|
+ private String generateSkuNo(Long productId) {
|
|
|
+ return "SKU" + productId + System.currentTimeMillis();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置商品规格
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void setupSpec(ProductSpecSetupDTO dto) {
|
|
|
+ // 保存新的规格
|
|
|
+ if (!CollectionUtils.isEmpty(dto.getSpecList())) {
|
|
|
+ AtomicInteger specIndex = new AtomicInteger(0);
|
|
|
+ for (ProductSpecSetupDTO.SpecItemDTO specItem : dto.getSpecList()) {
|
|
|
+ // 保存规格名
|
|
|
+ ProductSpec spec = new ProductSpec();
|
|
|
+ //spec.setProductId(productId);
|
|
|
+ // 检查规格名是否存在,如果存在,则直接通过,不插入
|
|
|
+ LambdaQueryWrapper<ProductSpec> specWrapper = new LambdaQueryWrapper<>();
|
|
|
+ specWrapper.eq(ProductSpec::getSpecName, specItem.getSpecName());
|
|
|
+ ProductSpec existingSpec = productSpecMapper.selectOne(specWrapper);
|
|
|
+ if (existingSpec != null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ spec.setSpecName(specItem.getSpecName());
|
|
|
+ spec.setSort(specItem.getSort() != null ? specItem.getSort() : specIndex.get() + 1);
|
|
|
+ spec.setCreateTime(LocalDateTime.now());
|
|
|
+ productSpecMapper.insert(spec);
|
|
|
+ Long specId = spec.getId();
|
|
|
+
|
|
|
+ // 保存规格值
|
|
|
+ if (!CollectionUtils.isEmpty(specItem.getSpecValues())) {
|
|
|
+ AtomicInteger valueIndex = new AtomicInteger(0);
|
|
|
+ for (ProductSpecSetupDTO.SpecValueItemDTO valueItem : specItem.getSpecValues()) {
|
|
|
+ ProductSpecValue specValue = new ProductSpecValue();
|
|
|
+ specValue.setSpecId(specId);
|
|
|
+ //specValue.setProductId(productId);
|
|
|
+ specValue.setSpecValue(valueItem.getSpecValue());
|
|
|
+ specValue.setSort(valueItem.getSort() != null ? valueItem.getSort() : valueIndex.get() + 1);
|
|
|
+ specValue.setCreateTime(LocalDateTime.now());
|
|
|
+ productSpecValueMapper.insert(specValue);
|
|
|
+ valueIndex.getAndIncrement();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ specIndex.getAndIncrement();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取商品规格组合(笛卡尔积)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<SpecComboVO> getSpecCombinations() {
|
|
|
+ // 查询商品的所有规格名
|
|
|
+ LambdaQueryWrapper<ProductSpec> specWrapper = new LambdaQueryWrapper<>();
|
|
|
+ //specWrapper.eq(ProductSpec::getProductId, productId).orderByAsc(ProductSpec::getSort);
|
|
|
+ specWrapper.orderByAsc(ProductSpec::getSort);
|
|
|
+ List<ProductSpec> specList = productSpecMapper.selectList(specWrapper);
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(specList)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询所有规格值,按规格名分组
|
|
|
+ LambdaQueryWrapper<ProductSpecValue> valueWrapper = new LambdaQueryWrapper<>();
|
|
|
+ //valueWrapper.eq(ProductSpecValue::getProductId, productId).orderByAsc(ProductSpecValue::getSort);
|
|
|
+ valueWrapper.orderByAsc(ProductSpecValue::getSort);
|
|
|
+ List<ProductSpecValue> allValues = productSpecValueMapper.selectList(valueWrapper);
|
|
|
+
|
|
|
+ // 按规格名ID分组
|
|
|
+ Map<Long, List<ProductSpecValue>> valueMap = allValues.stream().collect(Collectors.groupingBy(ProductSpecValue::getSpecId));
|
|
|
+
|
|
|
+ // 构建每个规格的规格值列表
|
|
|
+ List<List<SpecComboVO.SpecNameValue>> specValueGroups = new ArrayList<>();
|
|
|
+ for (ProductSpec spec : specList) {
|
|
|
+ List<ProductSpecValue> values = valueMap.getOrDefault(spec.getId(), Collections.emptyList());
|
|
|
+ if (values.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<SpecComboVO.SpecNameValue> nameValueList = new ArrayList<>();
|
|
|
+ for (ProductSpecValue value : values) {
|
|
|
+ SpecComboVO.SpecNameValue nameValue = new SpecComboVO.SpecNameValue();
|
|
|
+ nameValue.setSpecId(spec.getId());
|
|
|
+ nameValue.setSpecName(spec.getSpecName());
|
|
|
+ nameValue.setSpecValue(value.getSpecValue());
|
|
|
+ nameValue.setSort(value.getSort());
|
|
|
+ nameValueList.add(nameValue);
|
|
|
+ }
|
|
|
+ specValueGroups.add(nameValueList);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算笛卡尔积
|
|
|
+ List<List<SpecComboVO.SpecNameValue>> cartesianProduct = cartesianProduct(specValueGroups);
|
|
|
+ // 构建返回结果
|
|
|
+ List<SpecComboVO> result = new ArrayList<>();
|
|
|
+ int index = 1;
|
|
|
+ for (List<SpecComboVO.SpecNameValue> combo : cartesianProduct) {
|
|
|
+ SpecComboVO vo = new SpecComboVO();
|
|
|
+ vo.setIndex(index++);
|
|
|
+
|
|
|
+ List<String> specValues = combo.stream().map(SpecComboVO.SpecNameValue::getSpecValue).collect(Collectors.toList());
|
|
|
+ vo.setSpecValues(specValues);
|
|
|
+
|
|
|
+ /*List<Long> specValueIds = combo.stream().map(SpecComboVO.SpecNameValue::getSpecValueId).collect(Collectors.toList());
|
|
|
+ vo.setSpecValueIds(specValueIds);*/
|
|
|
+
|
|
|
+ vo.setSpecValueText(String.join(",", specValues));
|
|
|
+ vo.setSpecNameValueList(combo);
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算笛卡尔积
|
|
|
+ */
|
|
|
+ private <T> List<List<T>> cartesianProduct(List<List<T>> lists) {
|
|
|
+ if (lists.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<List<T>> result = new ArrayList<>();
|
|
|
+ result.add(new ArrayList<>());
|
|
|
+
|
|
|
+ for (List<T> list : lists) {
|
|
|
+ List<List<T>> temp = new ArrayList<>();
|
|
|
+ for (List<T> partial : result) {
|
|
|
+ for (T item : list) {
|
|
|
+ List<T> newPartial = new ArrayList<>(partial);
|
|
|
+ newPartial.add(item);
|
|
|
+ temp.add(newPartial);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ result = temp;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|