Преглед на файлове

Merge remote-tracking branch 'origin/main' into main

# Conflicts:
#	edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/ShopProductService.java
#	edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/impl/ShopProductParametersServiceImpl.java
#	edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/web/ShopProductController.java
zhangwei преди 1 седмица
родител
ревизия
06efe69482

+ 2 - 2
edu-travel-remote/edu-travel-remote-commodity/src/main/java/edu/travel/remote/commodity/ShopCategoryRemoteController.java

@@ -32,8 +32,8 @@ public interface ShopCategoryRemoteController extends RemoteBaseController<ShopC
     @PostMapping("/updateTargetFormId")
     public RPCBaseResponse<ShopCategoryVo> updateTargetFormId(ShopCategoryDto entity);
 
-    @PostMapping("/saveForm")
-    public RPCBaseResponse<ShopCategoryVo> saveFormTarget(@RequestBody ShopCategoryDto entity);
+//    @PostMapping("/saveFormTarget")
+//    public RPCBaseResponse<ShopCategoryVo> saveFormTarget(@RequestBody ShopCategoryDto entity);
 
     /**
      * 树形结构

+ 19 - 1
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/ShopProductService.java

@@ -3,13 +3,16 @@ package edu.travel.commodity.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import edu.travel.commodity.entity.ShopProduct;
+import edu.travel.remote.dto.*;
 import edu.travel.remote.dto.GetProductByTypeDto;
 import edu.travel.remote.dto.InsertProductDto;
 import edu.travel.remote.dto.SearchProductDto;
 import edu.travel.remote.vo.ShopProductVo;
+import edu.travel.rpc.RPCBaseResponse;
 
 import java.io.IOException;
-
+import java.util.List;
+import java.util.Map;
 
 public interface ShopProductService extends IService<ShopProduct> {
 
@@ -19,4 +22,19 @@ public interface ShopProductService extends IService<ShopProduct> {
 
     void insertProduct(InsertProductDto params);
 
+    RPCBaseResponse<Void> saveProduct(InsertProductDto params);
+
+    /**
+     * 批量规格输入,生成组合
+     * @param specList 规格列表,最多输入3组规格
+     * @return 组合后的规格名与规格值列表,每个组合包含以索引(0,1,2...)为键的规格对象(含specName和specValueName)和默认库存"inventory"为"0"
+     */
+    RPCBaseResponse<List<Map<String, Object>>> batchSpecAndValueGenerate(List<InsertProductSpecDto> specList);
+
+    /**
+     * 生成更结构化的规格组合
+     * @param specList 规格列表
+     * @return 结构化的规格组合结果,每个组合包含以索引(0,1,2...)为键的规格对象(含specName和specValueName)和默认库存"inventory"为"0"
+     */
+    RPCBaseResponse<List<Map<String, Object>>> generateStructuredSpecCombinations(List<InsertProductSpecDto> specList);
 }

+ 0 - 10
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/impl/ShopProductParametersServiceImpl.java

@@ -1,7 +1,5 @@
 package edu.travel.commodity.service.impl;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import edu.travel.commodity.entity.ShopProduct;
 import edu.travel.commodity.entity.ShopProductParameters;
@@ -30,12 +28,4 @@ public class ShopProductParametersServiceImpl extends ServiceImpl<ShopProductPar
         }
         return this.saveBatch(list);
     }
-
-//    @Override
-    public boolean deleteByProductId(Long productId) {
-        LambdaUpdateWrapper<ShopProductParameters> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.eq(ShopProductParameters::getProductId, productId)
-                     .set(ShopProductParameters::getDeleteFlag, 1);
-        return this.update(updateWrapper);
-    }
 }

+ 324 - 3
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/impl/ShopProductServiceImpl.java

@@ -1,9 +1,10 @@
 package edu.travel.commodity.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import edu.travel.commodity.constant.BaseConstant;
@@ -14,14 +15,23 @@ import edu.travel.commodity.utils.PageUtil;
 import edu.travel.exception.BaseException;
 import edu.travel.remote.dto.*;
 import edu.travel.remote.vo.ShopProductVo;
+import edu.travel.rpc.RPCBaseResponse;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.Assert;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
+@Slf4j
 @Service
 public class ShopProductServiceImpl extends ServiceImpl<ShopProductMapper, ShopProduct> implements ShopProductService {
     @Autowired
@@ -38,10 +48,16 @@ public class ShopProductServiceImpl extends ServiceImpl<ShopProductMapper, ShopP
 
     @Autowired
     private ShopSpecService shopSpecService;
+    
+    @Autowired
+    private ShopSpecValueService shopSpecValueService;
+    
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
 
     @Override
     public Page<ShopProductVo> getHotProduct(GetProductByTypeDto type) {
-        LambdaQueryWrapper<ShopProduct> query = Wrappers.<ShopProduct>lambdaQuery()
+        LambdaQueryWrapper<ShopProduct> query = new QueryWrapper<ShopProduct>().lambda()
                 .eq(ShopProduct::getStatus, BaseConstant.BASIC_STATUS_NO_NUM)
                 .eq(ShopProduct::getCountryId, type.getCountryId());
 
@@ -86,4 +102,309 @@ public class ShopProductServiceImpl extends ServiceImpl<ShopProductMapper, ShopP
         boolean b4 = shopProductParametersService.insertProductParameters(params.getParameters(), bean);
         //TODO 同步到es
     }
+
+    @Override
+    public RPCBaseResponse<Void> saveProduct(InsertProductDto params) {
+        ShopProduct bean = BeanUtil.toBean(params, ShopProduct.class);
+        if (ObjectUtil.isEmpty(params.getImages())) {
+            throw new BaseException("至少需要一张图片");
+        }
+        List<FileDto> images = params.getImages();
+        bean.setMainImageUrl(images.get(0).getFilePath());
+        //保存商品
+        this.save(bean);
+        //保存商品规格和规格值
+        return null;
+    }
+    @Override
+    public RPCBaseResponse<List<Map<String, Object>>> batchSpecAndValueGenerate(List<InsertProductSpecDto> specList) {
+        if (specList == null || specList.isEmpty()) {
+            return RPCBaseResponse.error("规格参数为空");
+        }
+        
+        // 限制最多输入3组规格
+        if (specList.size() > 3) {
+            return RPCBaseResponse.error("最多只能输入3组规格");
+        }
+        
+        try {
+            // 自动清除之前的缓存
+            clearSpecCache();
+            
+            // 验证每组规格
+            for (InsertProductSpecDto spec : specList) {
+                // 验证规格名称
+                if (ObjectUtil.isEmpty(spec.getSpecName())) {
+                    return RPCBaseResponse.error("规格名称不能为空");
+                }
+                
+                // 验证规格值集合
+                if (spec.getSpecValues() == null || spec.getSpecValues().isEmpty()) {
+                    return RPCBaseResponse.error("规格[" + spec.getSpecName() + "]的规格值不能为空");
+                }
+                
+                // 限制最多只能有3个规格值
+                if (spec.getSpecValues().size() > 3) {
+                    return RPCBaseResponse.error("规格[" + spec.getSpecName() + "]最多只能有3个规格值");
+                }
+                
+                // 验证规格值名称
+                for (SpecValueDto valueDto : spec.getSpecValues()) {
+                    if (ObjectUtil.isEmpty(valueDto.getSpecValueName())) {
+                        return RPCBaseResponse.error("规格[" + spec.getSpecName() + "]的规格值名称不能为空");
+                    }
+                }
+            }
+            
+            // 创建一个二维数组,用于存储每个规格的规格值对
+            List<List<SpecValueWithNameDto>> allSpecValues = new ArrayList<>();
+            
+            for (InsertProductSpecDto spec : specList) {
+                List<SpecValueWithNameDto> specValues = new ArrayList<>();
+                
+                for (SpecValueDto valueDto : spec.getSpecValues()) {
+                    SpecValueWithNameDto valueWithName = new SpecValueWithNameDto();
+                    valueWithName.setSpecName(spec.getSpecName());
+                    valueWithName.setSpecValueName(valueDto.getSpecValueName());
+                    specValues.add(valueWithName);
+                }
+                
+                allSpecValues.add(specValues);
+            }
+            
+            // 计算组合数量
+            int totalCombinations = 1;
+            StringBuilder logMessage = new StringBuilder("规格值数量: ");
+            
+            for (int i = 0; i < specList.size(); i++) {
+                InsertProductSpecDto spec = specList.get(i);
+                int valueCount = spec.getSpecValues().size();
+                totalCombinations *= valueCount;
+                logMessage.append(spec.getSpecName()).append("(").append(valueCount).append(")");
+                
+                if (i < specList.size() - 1) {
+                    logMessage.append(" × ");
+                }
+            }
+            
+            logMessage.append(" = ").append(totalCombinations);
+            log.info(logMessage.toString());
+            
+            // 生成笛卡尔积组合
+            List<List<SpecValueWithNameDto>> cartesianResult = new ArrayList<>();
+            generateCartesianCombinations(allSpecValues, 0, new ArrayList<>(), cartesianResult);
+            
+            log.info("生成的组合总数: {}", cartesianResult.size());
+            
+            // 转换为新格式的结果,每个组合为一个Map,包含规格对象数组和库存
+            List<Map<String, Object>> resultList = cartesianResult.stream()
+                    .map(combination -> {
+                        Map<String, Object> resultMap = new HashMap<>();
+                        List<Map<String, String>> specItems = new ArrayList<>();
+                        
+                        // 将规格值转换为对象数组
+                        for (SpecValueWithNameDto item : combination) {
+                            Map<String, String> specItem = new HashMap<>();
+                            specItem.put("specName", item.getSpecName());
+                            specItem.put("specValueName", item.getSpecValueName());
+                            specItems.add(specItem);
+                        }
+                        
+                        // 添加规格数组到结果中
+                        for (int i = 0; i < specItems.size(); i++) {
+                            resultMap.put(String.valueOf(i), specItems.get(i));
+                        }
+                        
+                        // 添加默认库存为0
+                        resultMap.put("inventory", "0");
+                        
+                        return resultMap;
+                    })
+                    .collect(Collectors.toList());
+            
+            return RPCBaseResponse.success("规格组合生成成功", resultList);
+        } catch (Exception e) {
+            log.error("规格组合生成失败:", e);
+            return RPCBaseResponse.error("规格组合生成失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 清除规格缓存
+     */
+    private void clearSpecCache() {
+        String specListKey = "product:spec:list";
+        Set<Object> keys = redisTemplate.opsForSet().members(specListKey);
+        if (keys != null) {
+            for (Object key : keys) {
+                redisTemplate.delete("product:spec:" + key);
+            }
+        }
+        redisTemplate.delete(specListKey);
+        log.info("已清空规格缓存");
+    }
+    
+    /**
+     * 递归生成规格值的笛卡尔积组合
+     */
+    private void generateCartesianCombinations(
+            List<List<SpecValueWithNameDto>> allSpecValues,
+            int currentIndex,
+            List<SpecValueWithNameDto> currentCombination,
+            List<List<SpecValueWithNameDto>> result) {
+        
+        // 基本情况:已经处理完所有规格
+        if (currentIndex == allSpecValues.size()) {
+            result.add(new ArrayList<>(currentCombination));
+            return;
+        }
+        
+        // 获取当前层级的规格值
+        List<SpecValueWithNameDto> currentValues = allSpecValues.get(currentIndex);
+        
+        // 遍历当前规格的所有值
+        for (SpecValueWithNameDto value : currentValues) {
+            // 添加当前规格值到组合中
+            currentCombination.add(value);
+            
+            // 递归处理下一个规格
+            generateCartesianCombinations(allSpecValues, currentIndex + 1, currentCombination, result);
+            
+            // 回溯:移除最后添加的规格值
+            currentCombination.remove(currentCombination.size() - 1);
+        }
+    }
+
+    /**
+     * 生成更结构化的规格组合
+     *
+     * @param specList 规格列表
+     * @return 结构化的规格组合结果
+     */
+    @Override
+    public RPCBaseResponse<List<Map<String, Object>>> generateStructuredSpecCombinations(List<InsertProductSpecDto> specList) {
+        if (specList == null || specList.isEmpty()) {
+            return RPCBaseResponse.error("规格参数为空");
+        }
+        
+        // 限制最多输入3组规格
+        if (specList.size() > 3) {
+            return RPCBaseResponse.error("最多只能输入3组规格");
+        }
+        
+        try {
+            // 自动清除之前的缓存
+            clearSpecCache();
+            
+            // 验证每组规格
+            for (InsertProductSpecDto spec : specList) {
+                // 验证规格名称
+                if (ObjectUtil.isEmpty(spec.getSpecName())) {
+                    return RPCBaseResponse.error("规格名称不能为空");
+                }
+                
+                // 验证规格值集合
+                if (CollectionUtil.isEmpty(spec.getSpecValues())) {
+                    return RPCBaseResponse.error("规格[" + spec.getSpecName() + "]的规格值不能为空");
+                }
+                
+                // 限制最多只能有10个规格值
+                if (spec.getSpecValues().size() > 8) {
+                    return RPCBaseResponse.error("规格[" + spec.getSpecName() + "]最多只能有8个规格值");
+                }
+            }
+            
+            // 处理所有规格
+            List<List<SpecValueWithNameDto>> specValueList = new ArrayList<>();
+            for (InsertProductSpecDto insertProductSpecDto : specList) {
+                String specName = insertProductSpecDto.getSpecName();
+                List<SpecValueDto> specValues = insertProductSpecDto.getSpecValues();
+                
+                // 为每个规格值创建DTO
+                List<SpecValueWithNameDto> specValueWithNames = specValues.stream()
+                        .map(specValue -> new SpecValueWithNameDto(specName, specValue.getSpecValueName()))
+                        .collect(Collectors.toList());
+                specValueList.add(specValueWithNames);
+            }
+            
+            // 生成规格组合
+            List<List<SpecValueWithNameDto>> combinations = buildCombinations(specValueList);
+            
+            // 转换为新格式的结果,每个组合为一个Map,包含规格对象数组和库存
+            List<Map<String, Object>> resultList = combinations.stream()
+                    .map(combination -> {
+                        Map<String, Object> resultMap = new HashMap<>();
+                        List<Map<String, String>> specItems = new ArrayList<>();
+                        
+                        // 将规格值转换为对象数组
+                        for (SpecValueWithNameDto item : combination) {
+                            Map<String, String> specItem = new HashMap<>();
+                            specItem.put("specName", item.getSpecName());
+                            specItem.put("specValueName", item.getSpecValueName());
+                            specItems.add(specItem);
+                        }
+                        
+                        // 添加规格数组到结果中
+                        for (int i = 0; i < specItems.size(); i++) {
+                            resultMap.put(String.valueOf(i), specItems.get(i));
+                        }
+                        
+                        // 添加默认库存为0
+                        resultMap.put("inventory", "0");
+                        
+                        // 日志记录
+                        StringBuilder debug = new StringBuilder("组合: ");
+                        for (Map<String, String> spec : specItems) {
+                            debug.append(spec.get("specName")).append("=").append(spec.get("specValueName")).append(", ");
+                        }
+                        log.debug(debug.toString());
+                        
+                        return resultMap;
+                    })
+                    .collect(Collectors.toList());
+            
+            log.info("生成规格组合总数: {}", resultList.size());
+            return RPCBaseResponse.success("规格组合生成成功", resultList);
+        } catch (Exception e) {
+            log.error("生成规格组合出错", e);
+            return RPCBaseResponse.error("生成规格组合失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 递归构建规格值的组合
+     *
+     * @param specValuesList 规格值列表
+     * @return 所有组合的列表
+     */
+    private List<List<SpecValueWithNameDto>> buildCombinations(List<List<SpecValueWithNameDto>> specValuesList) {
+        // 基本情况:如果只有一组规格值,则直接返回包装后的列表
+        if (specValuesList.size() == 1) {
+            return specValuesList.get(0).stream()
+                    .map(value -> {
+                        List<SpecValueWithNameDto> combination = new ArrayList<>();
+                        combination.add(value);
+                        return combination;
+                    })
+                    .collect(Collectors.toList());
+        }
+
+        // 取出第一组规格值
+        List<SpecValueWithNameDto> first = specValuesList.get(0);
+        
+        // 递归处理剩余的规格值
+        List<List<SpecValueWithNameDto>> restCombinations = buildCombinations(specValuesList.subList(1, specValuesList.size()));
+        
+        // 合并第一组与剩余组合
+        List<List<SpecValueWithNameDto>> result = new ArrayList<>();
+        for (SpecValueWithNameDto value : first) {
+            for (List<SpecValueWithNameDto> restCombination : restCombinations) {
+                List<SpecValueWithNameDto> newCombination = new ArrayList<>();
+                newCombination.add(value);
+                newCombination.addAll(restCombination);
+                result.add(newCombination);
+            }
+        }
+        return result;
+    }
 }

+ 1 - 0
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/service/impl/ShopProductSpecServiceImpl.java

@@ -149,6 +149,7 @@ public class ShopProductSpecServiceImpl extends SysServiceImpl<ShopProductSpecMa
         this.saveBatch(list);
         return shopSpecValueService.saveBatch(shopSpecValues);
     }
+    
     //通过spec_id获取规格值
     @Override
     public RPCBaseResponse<List<ProductSpecVo>> getShopSpecSortVoList(String specId) {

+ 1 - 1
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/web/ShopCategoryController.java

@@ -111,7 +111,7 @@ public class ShopCategoryController extends BaseController<ShopCategory> impleme
      * 新增商品类型
      */
     @Override
-    @PostMapping("/saveForm")
+    @PostMapping("/saveFormTarget")
     public RPCBaseResponse<ShopCategoryVo> saveFormTarget(@RequestBody ShopCategoryDto entity) {
         ShopCategory shopCurrency = new ShopCategory();
         BeanUtils.copyProperties(entity, shopCurrency);

+ 64 - 4
edu-travel-service/edu-travel-service-commodity/src/main/java/edu/travel/commodity/web/ShopProductController.java

@@ -15,7 +15,11 @@ import edu.travel.commodity.utils.IdUtils;
 import edu.travel.remote.commodity.ShopProductRemoteController;
 import edu.travel.remote.dto.GetProductByTypeDto;
 import edu.travel.remote.dto.InsertProductDto;
+import edu.travel.remote.dto.InsertProductSpecDto;
 import edu.travel.remote.dto.SearchProductDto;
+import edu.travel.remote.dto.SpecCombinationDto;
+import edu.travel.remote.dto.SpecCombinationResultDto;
+import edu.travel.remote.dto.SpecValueWithNameDto;
 import edu.travel.remote.upload.dto.EduFileDTO;
 import edu.travel.remote.upload.vo.FileVo;
 import edu.travel.remote.vo.FileWebVo;
@@ -31,6 +35,7 @@ import org.springframework.web.multipart.MultipartFile;
 import java.io.IOException;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import static edu.travel.rpc.RPCBaseResponse.error;
 import static edu.travel.rpc.RPCBaseResponse.success;
@@ -132,6 +137,24 @@ public class ShopProductController extends BaseController<ShopProduct> implement
         shopProductService.insertProduct(params);
         return success();
     }
+    /**
+     * 新增商品(new)
+     */
+    @PostMapping("/saveProduct")
+    public RPCBaseResponse<Void> saveProductResponse(@RequestBody InsertProductDto params) {
+        return shopProductService.saveProduct(params);
+    }
+
+    /**
+     * 批量规格/规格值(生成列表)
+     * 一次输入多组规格(最多三组),返回所有可能的组合
+     * 每个组合包含规格对象和默认库存,例如:{"0": {"specName": "颜色", "specValueName": "红色"}, "1": {"specName": "尺寸", "specValueName": "S"}, "2": {"specName": "材质", "specValueName": "棉"}, "inventory": "0"}
+     * 与generateStructuredSpecCombinations方法相同,保留此方法是为了兼容旧代码
+     */
+    @PostMapping("/batchSpecAndValueGenerate")
+    public RPCBaseResponse<List<Map<String, Object>>> batchSpecAndValueGenerate(@RequestBody List<InsertProductSpecDto> specList) {
+        return shopProductService.batchSpecAndValueGenerate(specList);
+    }
 
     /**
      * 获取SkuId
@@ -165,7 +188,7 @@ public class ShopProductController extends BaseController<ShopProduct> implement
      * @return
      */
     @Override
-    @PostMapping("/updateFormId")
+    @PostMapping("/updateTargetFormId")
     public RPCBaseResponse<ShopProductVo> updateTargetFormId(@RequestBody ShopProductVo entity) {
         ShopProduct shopProduct = new ShopProduct();
         BeanUtils.copyProperties(entity, shopProduct);
@@ -181,7 +204,7 @@ public class ShopProductController extends BaseController<ShopProduct> implement
      * @return
      */
     @Override
-    @PostMapping("/saveForm")
+    @PostMapping("/saveFormTarget")
     public RPCBaseResponse<ShopProductVo> saveFormTarget(@RequestBody ShopProductVo entity) {
         ShopProduct shopProduct = new ShopProduct();
         BeanUtils.copyProperties(entity, shopProduct);
@@ -197,7 +220,7 @@ public class ShopProductController extends BaseController<ShopProduct> implement
      * @return
      */
     @Override
-    @PostMapping("/deleteFormId")
+    @PostMapping("/deleteTargetFormId")
     public RPCBaseResponse<ShopProductVo> deleteTargetFormId(List<String> ids) {
         RPCBaseResponse<ShopProduct> shopCurrencyRPCBaseResponse = super.deleteTargetById(ids);
         RPCBaseResponse<ShopProductVo> shopCurrencyVoRPCBaseResponse = new RPCBaseResponse<>();
@@ -210,11 +233,48 @@ public class ShopProductController extends BaseController<ShopProduct> implement
      * @return
      */
     @Override
-    @GetMapping("/listForm")
+    @GetMapping("/getAllForm")
     public RPCBaseResponse<List<ShopProductVo>> getAllForm() {
         RPCBaseResponse<List<ShopProduct>> shopCurrencyRPCBaseResponse = super.listAll();
         RPCBaseResponse<List<ShopProductVo>> shopCurrencyVoRPCBaseResponse = new RPCBaseResponse<>();
         BeanUtils.copyProperties(shopCurrencyRPCBaseResponse, shopCurrencyVoRPCBaseResponse);
         return shopCurrencyVoRPCBaseResponse;
     }
+
+    /**
+     * 生成结构化的规格组合列表
+     * 接收规格和规格值列表,返回所有可能的组合
+     * 每个组合包含规格对象和默认库存,例如:{"0": {"specName": "颜色", "specValueName": "红色"}, "1": {"specName": "尺寸", "specValueName": "S"}, "2": {"specName": "材质", "specValueName": "棉"}, "inventory": "0"}
+     *
+     * 示例输入:
+     * [
+     *   {
+     *     "specName": "颜色",
+     *     "specValues": [{"specValueName": "红色"}, {"specValueName": "蓝色"}, {"specValueName": "黑色"}]
+     *   },
+     *   {
+     *     "specName": "尺寸",
+     *     "specValues": [{"specValueName": "S"}, {"specValueName": "M"}, {"specValueName": "L"}]
+     *   },
+     *   {
+     *     "specName": "材质",
+     *     "specValues": [{"specValueName": "棉"}, {"specValueName": "麻"}]
+     *   }
+     * ]
+     *
+     * 示例输出:
+     * [
+     *   {"0": {"specName": "颜色", "specValueName": "红色"}, "1": {"specName": "尺寸", "specValueName": "S"}, "2": {"specName": "材质", "specValueName": "棉"}, "inventory": "0"},
+     *   {"0": {"specName": "颜色", "specValueName": "红色"}, "1": {"specName": "尺寸", "specValueName": "S"}, "2": {"specName": "材质", "specValueName": "麻"}, "inventory": "0"},
+     *   {"0": {"specName": "颜色", "specValueName": "红色"}, "1": {"specName": "尺寸", "specValueName": "M"}, "2": {"specName": "材质", "specValueName": "棉"}, "inventory": "0"},
+     *   ...
+     * ]
+     *
+     * @param specList 规格列表
+     * @return 结构化的规格组合
+     */
+    @PostMapping("/generateStructuredSpecCombinations")
+    public RPCBaseResponse<List<Map<String, Object>>> generateStructuredSpecCombinations(@RequestBody List<InsertProductSpecDto> specList) {
+        return shopProductService.generateStructuredSpecCombinations(specList);
+    }
 }