Procházet zdrojové kódy

product:优化管理后台相关的接口
area:增加地区列表界面

YunaiV před 2 roky
rodič
revize
7a4a6a3046
31 změnil soubory, kde provedl 401 přidání a 339 odebrání
  1. 2 0
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java
  2. 0 3
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv
  3. 4 0
      yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java
  4. 0 39
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/ProductPropertyViewRespVO.java
  5. 23 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueDetailRespVO.java
  6. 4 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http
  7. 26 23
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.java
  8. 3 49
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java
  9. 4 4
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
  10. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java
  11. 1 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageItemRespVO.java
  12. 38 9
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/spu/ProductSpuConvert.java
  13. 1 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/property/ProductPropertyMapper.java
  14. 1 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java
  15. 4 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java
  16. 1 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyValueService.java
  17. 5 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyValueServiceImpl.java
  18. 1 9
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
  19. 11 63
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
  20. 25 2
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceTest.java
  21. 0 56
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/SkuServiceImplTest.java
  22. 7 53
      yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImplTest.java
  23. 4 0
      yudao-module-system/yudao-module-system-biz/pom.xml
  24. 5 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http
  25. 50 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.java
  26. 24 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/vo/AreaNodeRespVO.java
  27. 17 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/ip/AreaConvert.java
  28. 1 9
      yudao-ui-admin/src/api/mall/product/spu.js
  29. 17 0
      yudao-ui-admin/src/api/system/area.js
  30. 7 10
      yudao-ui-admin/src/views/mall/product/spu/save.vue
  31. 114 0
      yudao-ui-admin/src/views/system/area/index.vue

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.ip.core.utils;
 
 import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.text.csv.CsvRow;
 import cn.hutool.core.text.csv.CsvUtil;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -50,6 +51,7 @@ public class AreaUtils {
         for (CsvRow row : rows) {
             Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
             Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
+            Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
             area.setParent(parent);
             parent.getChildren().add(area);
         }

+ 0 - 3
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv

@@ -2567,8 +2567,6 @@ id,name,type,parentId
 441826,连南瑶族自治县,4,441800
 441881,英德市,4,441800
 441882,连州市,4,441800
-441900,东莞市,4,441900
-442000,中山市,4,442000
 445102,湘桥区,4,445100
 445103,潮安区,4,445100
 445122,饶平县,4,445100
@@ -2704,7 +2702,6 @@ id,name,type,parentId
 460321,西沙群岛,4,460300
 460322,南沙群岛,4,460300
 460323,中沙群岛的岛礁及其海域,4,460300
-460400,儋州市,4,460400
 469001,五指山市,4,469000
 469002,琼海市,4,469000
 469005,文昌市,4,469000

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java

@@ -33,6 +33,10 @@ public class AssertUtils {
     public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) {
         Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
         Arrays.stream(expectedFields).forEach(expectedField -> {
+            // 忽略 jacoco 自动生成的 $jacocoData 属性的情况
+            if (expectedField.isSynthetic()) {
+                return;
+            }
             // 如果是忽略的属性,则不进行比对
             if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
                 return;

+ 0 - 39
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/ProductPropertyViewRespVO.java

@@ -1,39 +0,0 @@
-package cn.iocoder.yudao.module.product.controller.admin.property.vo;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-import lombok.ToString;
-
-import java.util.List;
-
-// TODO 芋艿:看看怎么删除
-@ApiModel("管理后台 - 商品属性详情展示 Request VO")
-@Data
-@ToString(callSuper = true)
-public class ProductPropertyViewRespVO {
-
-    @ApiModelProperty(value = "属性项 id", example = "1")
-    public Long propertyId;
-
-    @ApiModelProperty(value = "属性项的名字", example = "内存")
-    public String name;
-
-    @ApiModelProperty(value = "属性值数组")
-    public List<Tuple2> propertyValues;
-
-    @Data
-    @ApiModel(value = "属性值元组")
-    public static class Tuple2 {
-        private final long id;
-        private final String name;
-
-        public Tuple2(Long id, String name) {
-            this.id = id;
-            this.name = name;
-        }
-
-    }
-
-
-}

+ 23 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/property/vo/value/ProductPropertyValueDetailRespVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.product.controller.admin.property.vo.value;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@ApiModel("管理后台 - 商品属性值的明细 Response VO")
+@Data
+public class ProductPropertyValueDetailRespVO {
+
+    @ApiModelProperty(value = "属性的编号", required = true, example = "1")
+    private Long propertyId;
+
+    @ApiModelProperty(value = "属性的名称", required = true, example = "颜色")
+    private String propertyName;
+
+    @ApiModelProperty(value = "属性值的编号", required = true, example = "1024")
+    private Long valueId;
+
+    @ApiModelProperty(value = "属性值的名称", required = true, example = "红色")
+    private String valueName;
+
+}

+ 4 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http

@@ -0,0 +1,4 @@
+### 获得商品 SPU 明细
+GET {{baseUrl}}/product/spu/get-detail?id=4
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 26 - 23
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.java

@@ -3,8 +3,13 @@ package cn.iocoder.yudao.module.product.controller.admin.spu;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
+import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
 import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
+import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
+import cn.iocoder.yudao.module.product.service.property.bo.ProductPropertyValueDetailRespBO;
+import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
 import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
@@ -15,10 +20,11 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
-import java.util.Collection;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS;
 
 @Api(tags = "管理后台 - 商品 SPU")
 @RestController
@@ -28,6 +34,10 @@ public class ProductSpuController {
 
     @Resource
     private ProductSpuService productSpuService;
+    @Resource
+    private ProductSkuService productSkuService;
+    @Resource
+    private ProductPropertyValueService productPropertyValueService;
 
     @PostMapping("/create")
     @ApiOperation("创建商品 SPU")
@@ -53,31 +63,24 @@ public class ProductSpuController {
         return success(true);
     }
 
-    // TODO 芋艿:修改接口
-    @GetMapping("/get/detail")
-    @ApiOperation("获得商品 SPU")
+    @GetMapping("/get-detail")
+    @ApiOperation("获得商品 SPU 明细")
     @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
     @PreAuthorize("@ss.hasPermission('product:spu:query')")
     public CommonResult<ProductSpuDetailRespVO> getSpuDetail(@RequestParam("id") Long id) {
-        return success(productSpuService.getSpuDetail(id));
-    }
+        // 获得商品 SPU
+        ProductSpuDO spu = productSpuService.getSpu(id);
+        if (spu == null) {
+            throw exception(SPU_NOT_EXISTS);
+        }
 
-    @GetMapping("/get")
-    @ApiOperation("获得商品 SPU")
-    @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
-    @PreAuthorize("@ss.hasPermission('product:spu:query')")
-    public CommonResult<ProductSpuDO> getSpu(@RequestParam("id") Long id) {
-        return success(productSpuService.getSpu(id));
-    }
-
-
-    @GetMapping("/list")
-    @ApiOperation("获得商品 SPU 列表")
-    @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
-    @PreAuthorize("@ss.hasPermission('product:spu:query')")
-    public CommonResult<List<ProductSpuDO>> getSpuList(@RequestParam("ids") Collection<Long> ids) {
-        List<ProductSpuDO> list = productSpuService.getSpuList(ids);
-        return success(ProductSpuConvert.INSTANCE.convertList(list));
+        // 查询商品 SKU
+        List<ProductSkuDO> skus = productSkuService.getSkuListBySpuIdAndStatus(spu.getId(), null);
+        // 查询商品属性
+        List<ProductPropertyValueDetailRespBO> propertyValues = productPropertyValueService
+                .getPropertyValueDetailList(ProductSkuConvert.INSTANCE.convertPropertyValueIds(skus));
+        // 拼接
+        return success(ProductSpuConvert.INSTANCE.convert03(spu, skus, propertyValues));
     }
 
     @GetMapping("/get-simple-list")
@@ -92,7 +95,7 @@ public class ProductSpuController {
     @ApiOperation("获得商品 SPU 分页")
     @PreAuthorize("@ss.hasPermission('product:spu:query')")
     public CommonResult<PageResult<ProductSpuRespVO>> getSpuPage(@Valid ProductSpuPageReqVO pageVO) {
-        return success(productSpuService.getSpuPage(pageVO));
+        return success(ProductSpuConvert.INSTANCE.convertPage(productSpuService.getSpuPage(pageVO)));
     }
 
 }

+ 3 - 49
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java

@@ -1,52 +1,27 @@
 package cn.iocoder.yudao.module.product.controller.admin.spu.vo;
 
-import cn.iocoder.yudao.module.product.controller.admin.property.vo.ProductPropertyViewRespVO;
+import cn.iocoder.yudao.module.product.controller.admin.property.vo.value.ProductPropertyValueDetailRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuBaseVO;
 import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-import java.time.LocalDateTime;
 import java.util.List;
 
 @ApiModel(value = "管理后台 - 商品 SPU 详细 Response VO", description = "包括关联的 SKU 等信息")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
-
-    @ApiModelProperty(value = "主键", required = true, example = "1")
-    private Long id;
-
-    @ApiModelProperty(value = "创建时间")
-    private LocalDateTime createTime;
+public class ProductSpuDetailRespVO extends ProductSpuRespVO {
 
     // ========== SKU 相关字段 =========
 
-    @ApiModelProperty(value = "库存", required = true, example = "true")
-    private Integer totalStock;
-
-    @ApiModelProperty(value = " 最小价格,单位使用:分", required = true, example = "1024")
-    private Integer minPrice;
-
-    @ApiModelProperty(value = "最大价格,单位使用:分", required = true, example = "1024")
-    private Integer maxPrice;
-
-    @ApiModelProperty(value = "商品销量", example = "1024")
-    private Integer salesCount;
-
     /**
      * SKU 数组
      */
     private List<Sku> skus;
 
-    // ========== 统计相关字段 =========
-
-    @ApiModelProperty(value = "点击量", example = "1024")
-    private Integer clickCount;
-
     @ApiModel(value = "管理后台 - 商品 SKU 详细 Response VO")
     @Data
     @EqualsAndHashCode(callSuper = true)
@@ -56,29 +31,8 @@ public class ProductSpuDetailRespVO extends ProductSpuBaseVO {
         /**
          * 属性数组
          */
-        private List<ProductSpuDetailRespVO.Property> properties;
-
-    }
-
-    @ApiModel(value = "管理后台 - 商品属性的详细 Response VO")
-    @Data
-    @EqualsAndHashCode(callSuper = true)
-    @ToString(callSuper = true)
-    public static class Property extends ProductSkuBaseVO.Property {
-
-        @ApiModelProperty(value = "属性项的名字", required = true, example = "颜色")
-        private String propertyName;
-
-        @ApiModelProperty(value = "属性值的名称", required = true, example = "蓝色")
-        private String valueName;
+        private List<ProductPropertyValueDetailRespVO> properties;
 
     }
 
-    @ApiModelProperty(value = "分类 id 数组,一直递归到一级父节点", example = "4")
-    private Long categoryId;
-
-    // TODO @芋艿:在瞅瞅~
-    @ApiModelProperty(value = "属性修改和详情展示组合", example = "[{\"propertyId\":2,\"name\":\"内存\",\"propertyValues\":[{\"v1\":11,\"v2\":\"64G\"},{\"v1\":10,\"v2\":\"32G\"}]},{\"propertyId\":3,\"name\":\"尺寸\",\"propertyValues\":[{\"v1\":16,\"v2\":\"6.1\"},{\"v1\":15,\"v2\":\"5.7\"}]}]")
-    private List<ProductPropertyViewRespVO> productPropertyViews;
-
 }

+ 4 - 4
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java

@@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
-import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuDetailRespVO;
-import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageItemRespVO;
+import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
+import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageItemRespVO;
 import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
 import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
@@ -48,7 +48,7 @@ public class AppProductSpuController {
 
     @GetMapping("/page")
     @ApiOperation("获得商品 SPU 分页")
-    public CommonResult<PageResult<AppSpuPageItemRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
+    public CommonResult<PageResult<AppProductSpuPageItemRespVO>> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) {
         PageResult<ProductSpuDO> pageResult = productSpuService.getSpuPage(pageVO, ProductSpuStatusEnum.ENABLE.getStatus());
         return success(ProductSpuConvert.INSTANCE.convertPage02(pageResult));
     }
@@ -56,7 +56,7 @@ public class AppProductSpuController {
     @GetMapping("/get-detail")
     @ApiOperation("获得商品 SPU 明细")
     @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
-    public CommonResult<AppSpuDetailRespVO> getSpuDetail(@RequestParam("id") Long id) {
+    public CommonResult<AppProductSpuDetailRespVO> getSpuDetail(@RequestParam("id") Long id) {
         // 获得商品 SPU
         ProductSpuDO spu = productSpuService.getSpu(id);
         if (spu == null) {

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppSpuDetailRespVO.java → yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java

@@ -9,7 +9,7 @@ import java.util.List;
 
 @ApiModel("用户 App - 商品 SPU 明细 Response VO")
 @Data
-public class AppSpuDetailRespVO {
+public class AppProductSpuDetailRespVO {
 
     @ApiModelProperty(value = "商品 SPU 编号", required = true, example = "1")
     private Long id;

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppSpuPageItemRespVO.java → yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/vo/AppProductSpuPageItemRespVO.java

@@ -10,7 +10,7 @@ import java.util.List;
 
 @ApiModel("用户 App - 商品 SPU 分页项 Response VO")
 @Data
-public class AppSpuPageItemRespVO {
+public class AppProductSpuPageItemRespVO {
 
     @ApiModelProperty(value = "商品 SPU 编号", required = true, example = "1")
     private Long id;

+ 38 - 9
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/spu/ProductSpuConvert.java

@@ -3,11 +3,12 @@ package cn.iocoder.yudao.module.product.convert.spu;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
+import cn.iocoder.yudao.module.product.controller.admin.property.vo.value.ProductPropertyValueDetailRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
 import cn.iocoder.yudao.module.product.controller.app.property.vo.value.AppProductPropertyValueDetailRespVO;
+import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
-import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuDetailRespVO;
-import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppSpuPageItemRespVO;
+import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageItemRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import cn.iocoder.yudao.module.product.service.property.bo.ProductPropertyValueDetailRespBO;
@@ -45,9 +46,9 @@ public interface ProductSpuConvert {
 
     List<ProductSpuSimpleRespVO> convertList02(List<ProductSpuDO> list);
 
-    default AppSpuDetailRespVO convert(ProductSpuDO spu, List<ProductSkuDO> skus,
-                                       List<ProductPropertyValueDetailRespBO> propertyValues) {
-        AppSpuDetailRespVO spuVO = convert02(spu)
+    default AppProductSpuDetailRespVO convert(ProductSpuDO spu, List<ProductSkuDO> skus,
+                                              List<ProductPropertyValueDetailRespBO> propertyValues) {
+        AppProductSpuDetailRespVO spuVO = convert02(spu)
                 .setSalesCount(spu.getSalesCount() + defaultIfNull(spu.getVirtualSalesCount(), 0));
         spuVO.setSkus(convertList03(skus));
         // 处理商品属性
@@ -57,7 +58,7 @@ public interface ProductSpuConvert {
             if (CollUtil.isEmpty(properties)) {
                 continue;
             }
-            AppSpuDetailRespVO.Sku sku = spuVO.getSkus().get(i);
+            AppProductSpuDetailRespVO.Sku sku = spuVO.getSkus().get(i);
             sku.setProperties(new ArrayList<>(properties.size()));
             // 遍历每个 properties,设置到 AppSpuDetailRespVO.Sku 中
             properties.forEach(property -> {
@@ -70,10 +71,38 @@ public interface ProductSpuConvert {
         }
         return spuVO;
     }
-    AppSpuDetailRespVO convert02(ProductSpuDO spu);
-    List<AppSpuDetailRespVO.Sku> convertList03(List<ProductSkuDO> skus);
+    AppProductSpuDetailRespVO convert02(ProductSpuDO spu);
+    List<AppProductSpuDetailRespVO.Sku> convertList03(List<ProductSkuDO> skus);
     AppProductPropertyValueDetailRespVO convert03(ProductPropertyValueDetailRespBO propertyValue);
 
-    PageResult<AppSpuPageItemRespVO> convertPage02(PageResult<ProductSpuDO> page);
+    PageResult<AppProductSpuPageItemRespVO> convertPage02(PageResult<ProductSpuDO> page);
+
+    default ProductSpuDetailRespVO convert03(ProductSpuDO spu, List<ProductSkuDO> skus,
+                                             List<ProductPropertyValueDetailRespBO> propertyValues) {
+        ProductSpuDetailRespVO spuVO = convert03(spu);
+        spuVO.setSkus(convertList04(skus));
+        // 处理商品属性
+        Map<Long, ProductPropertyValueDetailRespBO> propertyValueMap = convertMap(propertyValues, ProductPropertyValueDetailRespBO::getValueId);
+        for (int i = 0; i < skus.size(); i++) {
+            List<ProductSkuDO.Property> properties = skus.get(i).getProperties();
+            if (CollUtil.isEmpty(properties)) {
+                continue;
+            }
+            ProductSpuDetailRespVO.Sku sku = spuVO.getSkus().get(i);
+            sku.setProperties(new ArrayList<>(properties.size()));
+            // 遍历每个 properties,设置到 AppSpuDetailRespVO.Sku 中
+            properties.forEach(property -> {
+                ProductPropertyValueDetailRespBO propertyValue = propertyValueMap.get(property.getValueId());
+                if (propertyValue == null) {
+                    return;
+                }
+                sku.getProperties().add(convert04(propertyValue));
+            });
+        }
+        return spuVO;
+    }
+    ProductSpuDetailRespVO convert03(ProductSpuDO spu);
+    List<ProductSpuDetailRespVO.Sku> convertList04(List<ProductSkuDO> skus);
+    ProductPropertyValueDetailRespVO convert04(ProductPropertyValueDetailRespBO propertyValue);
 
 }

+ 1 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/property/ProductPropertyMapper.java

@@ -21,8 +21,7 @@ public interface ProductPropertyMapper extends BaseMapperX<ProductPropertyDO> {
     }
 
     default ProductPropertyDO selectByName(String name) {
-        return selectOne(new LambdaQueryWrapperX<ProductPropertyDO>()
-                .eqIfPresent(ProductPropertyDO::getName, name));
+        return selectOne(ProductPropertyDO::getName, name);
     }
 
     default List<ProductPropertyDO> selectList(ProductPropertyListReqVO listReqVO) {

+ 1 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyService.java

@@ -17,6 +17,7 @@ public interface ProductPropertyService {
 
     /**
      * 创建属性项
+     * 注意,如果已经存在该属性项,直接返回它的编号即可
      *
      * @param createReqVO 创建信息
      * @return 编号

+ 4 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyServiceImpl.java

@@ -40,9 +40,10 @@ public class ProductPropertyServiceImpl implements ProductPropertyService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createProperty(ProductPropertyCreateReqVO createReqVO) {
-        // 校验名字重复
-        if (productPropertyMapper.selectByName(createReqVO.getName()) != null) {
-            throw exception(PROPERTY_EXISTS);
+        // 如果已经添加过该属性项,直接返回
+        ProductPropertyDO dbProperty = productPropertyMapper.selectByName(createReqVO.getName());
+        if (dbProperty != null) {
+            return dbProperty.getId();
         }
 
         // 插入

+ 1 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyValueService.java

@@ -19,6 +19,7 @@ public interface ProductPropertyValueService {
 
     /**
      * 创建属性值
+     * 注意,如果已经存在该属性值,直接返回它的编号即可
      *
      * @param createReqVO 创建信息
      * @return 编号

+ 5 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/property/ProductPropertyValueServiceImpl.java

@@ -42,9 +42,11 @@ public class ProductPropertyValueServiceImpl implements ProductPropertyValueServ
 
     @Override
     public Long createPropertyValue(ProductPropertyValueCreateReqVO createReqVO) {
-        // 校验名字唯一
-        if (productPropertyValueMapper.selectByName(createReqVO.getPropertyId(), createReqVO.getName()) != null) {
-            throw exception(PROPERTY_VALUE_EXISTS);
+        // 如果已经添加过该属性值,直接返回
+        ProductPropertyValueDO dbValue = productPropertyValueMapper.selectByName(
+                createReqVO.getPropertyId(), createReqVO.getName());
+        if (dbValue != null) {
+            return dbValue.getId();
         }
 
         // 新增

+ 1 - 9
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java

@@ -41,14 +41,6 @@ public interface ProductSpuService {
      */
     void deleteSpu(Long id);
 
-    /**
-     * 获得商品 SPU 详情
-     *
-     * @param id 编号
-     * @return 商品 SPU
-     */
-    ProductSpuDetailRespVO getSpuDetail(Long id);
-
     /**
      * 获得商品 SPU
      *
@@ -88,7 +80,7 @@ public interface ProductSpuService {
      * @param pageReqVO 分页查询
      * @return 商品spu分页
      */
-    PageResult<ProductSpuRespVO> getSpuPage(ProductSpuPageReqVO pageReqVO);
+    PageResult<ProductSpuDO> getSpuPage(ProductSpuPageReqVO pageReqVO);
 
     /**
      * 获得商品 SPU 分页

+ 11 - 63
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java

@@ -1,27 +1,19 @@
 package cn.iocoder.yudao.module.product.service.spu;
 
-import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.module.product.controller.admin.property.vo.ProductPropertyViewRespVO;
-import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuBaseVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuRespVO;
-import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuCreateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateReqVO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
-import cn.iocoder.yudao.module.product.convert.sku.ProductSkuConvert;
 import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
-import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyDO;
-import cn.iocoder.yudao.module.product.dal.dataobject.property.ProductPropertyValueDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
 import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import cn.iocoder.yudao.module.product.dal.mysql.spu.ProductSpuMapper;
-import cn.iocoder.yudao.module.product.enums.spu.ProductSpuSpecTypeEnum;
 import cn.iocoder.yudao.module.product.service.brand.ProductBrandService;
 import cn.iocoder.yudao.module.product.service.category.ProductCategoryService;
-import cn.iocoder.yudao.module.product.service.property.ProductPropertyService;
-import cn.iocoder.yudao.module.product.service.property.ProductPropertyValueService;
 import cn.iocoder.yudao.module.product.service.sku.ProductSkuService;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
@@ -29,8 +21,10 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -49,18 +43,13 @@ public class ProductSpuServiceImpl implements ProductSpuService {
     @Resource
     private ProductSpuMapper productSpuMapper;
 
-    @Resource
-    private ProductCategoryService categoryService;
-
     @Resource
     @Lazy // 循环依赖,避免报错
     private ProductSkuService productSkuService;
     @Resource
-    private ProductPropertyService productPropertyService;
-    @Resource
-    private ProductPropertyValueService productPropertyValueService;
-    @Resource
     private ProductBrandService brandService;
+    @Resource
+    private ProductCategoryService categoryService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -148,46 +137,6 @@ public class ProductSpuServiceImpl implements ProductSpuService {
         }
     }
 
-    @Override
-    // TODO @芋艿:需要再 review 下
-    public ProductSpuDetailRespVO getSpuDetail(Long id) {
-        ProductSpuDO spu = productSpuMapper.selectById(id);
-        ProductSpuDetailRespVO respVO = BeanUtil.copyProperties(spu, ProductSpuDetailRespVO.class);
-        if (null != spu) {
-            List<ProductSpuDetailRespVO.Sku> skuReqs = ProductSkuConvert.INSTANCE.convertList03(productSkuService.getSkuListBySpuId(id));
-            respVO.setSkus(skuReqs);
-            // 组合 sku 属性
-            if (spu.getSpecType().equals(ProductSpuSpecTypeEnum.DISABLE.getType())) {
-                List<ProductSkuRespVO.Property> properties = new ArrayList<>();
-                for (ProductSpuDetailRespVO.Sku productSkuRespVO : skuReqs) {
-                    properties.addAll(productSkuRespVO.getProperties());
-                }
-                Map<Long, List<ProductSkuBaseVO.Property>> propertyMaps = properties.stream().collect(Collectors.groupingBy(ProductSkuBaseVO.Property::getPropertyId));
-
-                List<ProductPropertyValueDO> propertyValueList = productPropertyValueService.getPropertyValueListByPropertyId(propertyMaps.keySet());
-                List<ProductPropertyDO> propertyList = productPropertyService.getPropertyList(propertyMaps.keySet());
-                // 装载组装过后的属性
-                List<ProductPropertyViewRespVO> productPropertyViews = new ArrayList<>();
-                propertyList.forEach(p -> {
-                    ProductPropertyViewRespVO productPropertyViewRespVO = new ProductPropertyViewRespVO();
-                    productPropertyViewRespVO.setPropertyId(p.getId());
-                    productPropertyViewRespVO.setName(p.getName());
-                    List<ProductPropertyViewRespVO.Tuple2> propertyValues = new ArrayList<>();
-                    // 转换成map是为了能快速获取
-                    Map<Long, ProductPropertyValueDO> propertyValueMaps = convertMap(propertyValueList, ProductPropertyValueDO::getId);
-                    propertyMaps.get(p.getId()).forEach(pv -> {
-                        ProductPropertyViewRespVO.Tuple2 tuple2 = new ProductPropertyViewRespVO.Tuple2(pv.getValueId(), propertyValueMaps.get(pv.getValueId()).getName());
-                        propertyValues.add(tuple2);
-                    });
-                    productPropertyViewRespVO.setPropertyValues(propertyValues.stream().distinct().collect(Collectors.toList()));
-                    productPropertyViews.add(productPropertyViewRespVO);
-                });
-                respVO.setProductPropertyViews(productPropertyViews);
-            }
-        }
-        return respVO;
-    }
-
     @Override
     public ProductSpuDO getSpu(Long id) {
         return productSpuMapper.selectById(id);
@@ -203,9 +152,8 @@ public class ProductSpuServiceImpl implements ProductSpuService {
         return productSpuMapper.selectList();
     }
 
-    // TODO 芋艿:改成 DO 返回
     @Override
-    public PageResult<ProductSpuRespVO> getSpuPage(ProductSpuPageReqVO pageReqVO) {
+    public PageResult<ProductSpuDO> getSpuPage(ProductSpuPageReqVO pageReqVO) {
         // 库存告警的 SPU 编号的集合
         Set<Long> alarmStockSpuIds = null;
         if (Boolean.TRUE.equals(pageReqVO.getAlarmStock())) {
@@ -215,7 +163,7 @@ public class ProductSpuServiceImpl implements ProductSpuService {
             }
         }
         // 分页查询
-        return ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(pageReqVO, alarmStockSpuIds));
+        return productSpuMapper.selectPage(pageReqVO, alarmStockSpuIds);
     }
 
     @Override

+ 25 - 2
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/ProductSkuServiceTest.java

@@ -19,11 +19,12 @@ import java.util.Arrays;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
 import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
 import static java.util.Collections.singletonList;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.verify;
 
@@ -145,4 +146,26 @@ public class ProductSkuServiceTest extends BaseDbUnitTest {
                 SKU_STOCK_NOT_ENOUGH);
     }
 
+    @Test
+    public void testDeleteSku_success() {
+        // mock 数据
+        ProductSkuDO dbSku = randomPojo(ProductSkuDO.class);
+        productSkuMapper.insert(dbSku);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbSku.getId();
+
+        // 调用
+        productSkuService.deleteSku(id);
+        // 校验数据不存在了
+        assertNull(productSkuMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteSku_notExists() {
+        // 准备参数
+        Long id = 1L;
+
+        // 调用, 并断言异常
+        assertServiceException(() -> productSkuService.deleteSku(id), SKU_NOT_EXISTS);
+    }
 }

+ 0 - 56
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/sku/SkuServiceImplTest.java

@@ -1,56 +0,0 @@
-package cn.iocoder.yudao.module.product.service.sku;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
-import cn.iocoder.yudao.module.product.dal.mysql.sku.ProductSkuMapper;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.springframework.context.annotation.Import;
-
-import javax.annotation.Resource;
-
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
-import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-// TODO 芋艿:整合到 {@link ProductSkuServiceTest} 中
-/**
-* {@link ProductSkuServiceImpl} 的单元测试类
-*
-* @author 芋道源码
-*/
-@Import(ProductSkuServiceImpl.class)
-@Disabled // TODO 芋艿:临时去掉
-public class SkuServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private ProductSkuServiceImpl ProductSkuService;
-
-    @Resource
-    private ProductSkuMapper ProductSkuMapper;
-
-    @Test
-    public void testDeleteSku_success() {
-        // mock 数据
-        ProductSkuDO dbSku = randomPojo(ProductSkuDO.class);
-        ProductSkuMapper.insert(dbSku);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        Long id = dbSku.getId();
-
-        // 调用
-        ProductSkuService.deleteSku(id);
-       // 校验数据不存在了
-       assertNull(ProductSkuMapper.selectById(id));
-    }
-
-    @Test
-    public void testDeleteSku_notExists() {
-        // 准备参数
-        Long id = 1L;
-
-        // 调用, 并断言异常
-        assertServiceException(() -> ProductSkuService.deleteSku(id), SKU_NOT_EXISTS);
-    }
-
-}

+ 7 - 53
yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImplTest.java

@@ -8,10 +8,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-import cn.iocoder.yudao.module.product.controller.admin.property.vo.property.ProductPropertyRespVO;
-import cn.iocoder.yudao.module.product.controller.admin.property.vo.value.ProductPropertyValueRespVO;
 import cn.iocoder.yudao.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.spu.vo.*;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuCreateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuRespVO;
+import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateReqVO;
 import cn.iocoder.yudao.module.product.controller.app.spu.vo.AppProductSpuPageReqVO;
 import cn.iocoder.yudao.module.product.convert.spu.ProductSpuConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO;
@@ -148,53 +149,6 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         Assertions.assertNull(productSpuMapper.selectById(createReqVO.getId()));
     }
 
-    @Test
-    void getSpuDetail() {
-        // 准备spu参数
-        ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class, o -> {
-            o.setSpecType(ProductSpuSpecTypeEnum.DISABLE.getType());
-        });
-        productSpuMapper.insert(createReqVO);
-
-        // 创建两个属性
-        ArrayList<ProductPropertyRespVO> productPropertyRespVOS = Lists.newArrayList(
-                randomPojo(ProductPropertyRespVO.class),
-                randomPojo(ProductPropertyRespVO.class));
-
-        // 所有属性值
-        ArrayList<ProductPropertyValueRespVO> productPropertyValueRespVO = new ArrayList<>();
-
-        // 每个属性创建属性值
-        productPropertyRespVOS.forEach(v -> {
-            ProductPropertyValueRespVO productPropertyValueRespVO1 = randomPojo(ProductPropertyValueRespVO.class, o -> o.setPropertyId(v.getId()));
-            productPropertyValueRespVO.add(productPropertyValueRespVO1);
-        });
-
-        // 属性值建立笛卡尔积
-        Map<Long, List<ProductPropertyValueRespVO>> collect = productPropertyValueRespVO.stream().collect(Collectors.groupingBy(ProductPropertyValueRespVO::getPropertyId));
-        List<List<ProductPropertyValueRespVO>> lists = cartesianProduct(Lists.newArrayList(collect.values()));
-
-        // 准备sku参数
-        ArrayList<ProductSkuDO> productSkuDOS = Lists.newArrayList();
-        lists.forEach(pp -> {
-            List<ProductSkuDO.Property> property = pp.stream().map(ppv -> new ProductSkuDO.Property(ppv.getPropertyId(), ppv.getId())).collect(Collectors.toList());
-            ProductSkuDO productSkuDO = randomPojo(ProductSkuDO.class, o -> {
-                o.setProperties(property);
-            });
-            productSkuDOS.add(productSkuDO);
-
-        });
-
-        Mockito.when(productSkuService.getSkuListBySpuId(createReqVO.getId())).thenReturn(productSkuDOS);
-//        Mockito.when(productPropertyValueService.getPropertyValueListByPropertyId(new ArrayList<>(collect.keySet()))).thenReturn(productPropertyValueRespVO);
-//        Mockito.when(productPropertyService.getPropertyVOList(new ArrayList<>(collect.keySet()))).thenReturn(productPropertyRespVOS);
-//
-        // 调用
-        ProductSpuDetailRespVO spuDetail = productSpuService.getSpuDetail(createReqVO.getId());
-
-        assertPojoEquals(createReqVO, spuDetail);
-    }
-
     @Test
     void getSpu() {
         // 准备参数
@@ -222,7 +176,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO();
         productSpuPageReqVO.setAlarmStock(true);
 
-        PageResult<ProductSpuRespVO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
+        PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
 
         PageResult<Object> result = PageResult.empty();
         Assertions.assertIterableEquals(result.getList(), spuPage.getList());
@@ -271,7 +225,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         // 调用
         ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO();
         productSpuPageReqVO.setAlarmStock(true);
-        PageResult<ProductSpuRespVO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
+        PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
 
         PageResult<ProductSpuRespVO> result = ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(productSpuPageReqVO, alarmStockSpuIds));
         Assertions.assertIterableEquals(result.getList(), spuPage.getList());
@@ -323,7 +277,7 @@ public class ProductSpuServiceImplTest extends BaseDbUnitTest {
         productSpuPageReqVO.setStatus(ProductSpuStatusEnum.ENABLE.getStatus());
         productSpuPageReqVO.setCategoryId(categoryId);
 
-        PageResult<ProductSpuRespVO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
+        PageResult<ProductSpuDO> spuPage = productSpuService.getSpuPage(productSpuPageReqVO);
 
         PageResult<ProductSpuRespVO> result = ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(productSpuPageReqVO, (Set<Long>) null));
         assertEquals(result, spuPage);

+ 4 - 0
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -54,6 +54,10 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
+        </dependency>
 
         <!-- Web 相关 -->
         <dependency>

+ 5 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http

@@ -0,0 +1,5 @@
+### 获得地区树
+GET {{baseUrl}}/system/area/tree
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+

+ 50 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.system.controller.admin.ip;
+
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
+import cn.iocoder.yudao.framework.ip.core.utils.IPUtils;
+import cn.iocoder.yudao.module.system.controller.admin.ip.vo.AreaNodeRespVO;
+import cn.iocoder.yudao.module.system.convert.ip.AreaConvert;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Api(tags = "管理后台 - 地区")
+@RestController
+@RequestMapping("/system/area")
+@Validated
+public class AreaController {
+
+    @GetMapping("/tree")
+    @ApiOperation("获得地区树")
+    public CommonResult<List<AreaNodeRespVO>> getAreaTree() {
+        Area area = AreaUtils.getArea(Area.ID_CHINA);
+        Assert.notNull(area, "获取不到中国");
+        return success(AreaConvert.INSTANCE.convertList(area.getChildren()));
+    }
+
+    @GetMapping("/get-by-ip")
+    @ApiOperation("获得 IP 对应的地区名")
+    @ApiImplicitParam(name = "ip", value = "IP", required = true, dataTypeClass = String.class)
+    public CommonResult<String> getAreaByIp(@RequestParam("ip") String ip) {
+        // 获得城市
+        Area area = IPUtils.getArea(ip);
+        if (area == null) {
+            return success("未知");
+        }
+        // 格式化返回
+        return success(AreaUtils.format(area.getId()));
+    }
+
+}

+ 24 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/vo/AreaNodeRespVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.system.controller.admin.ip.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@ApiModel("管理后台 - 地区节点 Response VO")
+@Data
+public class AreaNodeRespVO {
+
+    @ApiModelProperty(value = "编号", required = true, example = "110000")
+    private Integer id;
+
+    @ApiModelProperty(value = "名字", required = true, example = "北京")
+    private String name;
+
+    /**
+     * 子节点
+     */
+    private List<AreaNodeRespVO> children;
+
+}

+ 17 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/ip/AreaConvert.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.system.convert.ip;
+
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.module.system.controller.admin.ip.vo.AreaNodeRespVO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface AreaConvert {
+
+    AreaConvert INSTANCE = Mappers.getMapper(AreaConvert.class);
+
+    List<AreaNodeRespVO> convertList(List<Area> list);
+
+}

+ 1 - 9
yudao-ui-admin/src/api/mall/product/spu.js

@@ -26,18 +26,10 @@ export function deleteSpu(id) {
   })
 }
 
-// 获得商品spu
-export function getSpu(id) {
-  return request({
-    url: '/product/spu/get?id=' + id,
-    method: 'get'
-  })
-}
-
 // 获得商品 SPU 详情
 export function getSpuDetail(id) {
   return request({
-    url: '/product/spu/get/detail?id=' + id,
+    url: '/product/spu/get-detail?id=' + id,
     method: 'get'
   })
 }

+ 17 - 0
yudao-ui-admin/src/api/system/area.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// 获得地区树
+export function getAreaTree() {
+  return request({
+    url: '/system/area/tree',
+    method: 'get'
+  })
+}
+
+// 获得 IP 对应的地区名
+export function getAreaByIp(ip) {
+  return request({
+    url: '/system/area/get-by-ip?ip=' + ip,
+    method: 'get'
+  })
+}

+ 7 - 10
yudao-ui-admin/src/views/mall/product/spu/save.vue

@@ -4,9 +4,8 @@
     <el-tabs v-model="activeName" class="tabs">
       <!-- 基础设置 -->
       <!-- TODO @luowenfeng:基础设置,分成基础信息、配送信息 -->
-      <!-- TODO @luowenfeng:base=》basic 会更好哈 -->
-      <el-tab-pane label="基础设置" name="base">
-        <el-form ref="base" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
+      <el-tab-pane label="基础设置" name="basic">
+        <el-form ref="basic" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
           <el-form-item label="商品名称" prop="name">
             <el-input v-model="baseForm.name" placeholder="请输入商品名称" />
           </el-form-item>
@@ -155,9 +154,8 @@
       </el-tab-pane>
 
       <!-- 商品详情 -->
-      <!-- TODO @luowenfeng:third=》detail 会更好哈 -->
-      <el-tab-pane label="商品详情" name="third">
-        <el-form ref="third" :model="baseForm" :rules="rules">
+      <el-tab-pane label="商品详情" name="detail">
+        <el-form ref="detail" :model="baseForm" :rules="rules">
           <el-form-item prop="description">
             <editor v-model="baseForm.description" :min-height="380"/>
           </el-form-item>
@@ -165,9 +163,8 @@
       </el-tab-pane>
 
       <!-- 销售设置 -->
-      <!-- TODO @luowenfeng:fourth=》senior 会更好哈 -->
-      <el-tab-pane label="高级设置" name="fourth">
-        <el-form ref="fourth" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
+      <el-tab-pane label="高级设置" name="senior">
+        <el-form ref="senior" :model="baseForm" :rules="rules" label-width="100px" style="width: 95%">
           <el-form-item label="排序字段">
             <el-input v-model="baseForm.sort" placeholder="请输入排序字段" oninput="value=value.replace(/^(0+)|[^\d]+/g,'')"/>
           </el-form-item>
@@ -208,7 +205,7 @@ export default {
   data() {
     return {
       specSwitch: false,
-      activeName: "base",
+      activeName: "basic",
       propName: {
         checkStrictly: true,
         label: "name",

+ 114 - 0
yudao-ui-admin/src/views/system/area/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="app-container">
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">IP 查询
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-if="refreshTable" v-loading="loading" :data="list"  row-key="id"
+              :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
+      <el-table-column label="编号" prop="id"/>
+      <el-table-column label="名字" prop="name"/>
+    </el-table>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog title="IP 查询" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="IP" prop="ip">
+          <el-input v-model="form.ip" placeholder="请输入 IP 地址"/>
+        </el-form-item>
+        <el-form-item label="地址" prop="result">
+          <el-input v-model="form.result" readonly placeholder="展示查询 IP 结果" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">查 询</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {getAreaByIp, getAreaTree} from "@/api/system/area";
+
+export default {
+  name: "Area",
+  components: {
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 地区列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 表单参数
+      form: {
+        ip: undefined,
+        result: undefined,
+      },
+      // 表单校验
+      rules: {
+        ip: [{required: true, message: "IP 地址不能为控", trigger: "blur"}],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      getAreaTree().then(response => {
+        this.list = response.data;
+        this.loading = false;
+      })
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        ip: undefined,
+        result: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        getAreaByIp(this.form.ip).then(response => {
+          this.$modal.msgSuccess("查询成功");
+          this.form.result = response.data
+        });
+      });
+    }
+  }
+};
+</script>