From 09722c0cd60785adda33cde16d4fa2f9b1d8f2b3 Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 10:12:31 +0800
Subject: [PATCH 1/7] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91mall=20?=
 =?UTF-8?q?=E7=88=B6=E6=A8=A1=E5=9D=97=20artifactId?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                           | 2 +-
 yudao-module-mall/pom.xml                         | 2 +-
 yudao-module-mall/yudao-module-market-api/pom.xml | 2 +-
 yudao-module-mall/yudao-module-market-biz/pom.xml | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 0ba239d5d..e3c5072ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
         <module>yudao-module-pay</module>
-        <module>yudao-module-market</module>
+        <module>yudao-module-mall</module>
     </modules>
 
     <name>${project.artifactId}</name>
diff --git a/yudao-module-mall/pom.xml b/yudao-module-mall/pom.xml
index 448986c6e..b13dc68f6 100644
--- a/yudao-module-mall/pom.xml
+++ b/yudao-module-mall/pom.xml
@@ -9,7 +9,7 @@
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>yudao-module-market</artifactId>
+    <artifactId>yudao-module-mall</artifactId>
     <packaging>pom</packaging>
 
     <name>${project.artifactId}</name>
diff --git a/yudao-module-mall/yudao-module-market-api/pom.xml b/yudao-module-mall/yudao-module-market-api/pom.xml
index c83179632..fef3428fc 100644
--- a/yudao-module-mall/yudao-module-market-api/pom.xml
+++ b/yudao-module-mall/yudao-module-market-api/pom.xml
@@ -3,8 +3,8 @@
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
-        <artifactId>yudao-module-market</artifactId>
         <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-mall</artifactId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
diff --git a/yudao-module-mall/yudao-module-market-biz/pom.xml b/yudao-module-mall/yudao-module-market-biz/pom.xml
index 0e3a0490e..e7fa33e9d 100644
--- a/yudao-module-mall/yudao-module-market-biz/pom.xml
+++ b/yudao-module-mall/yudao-module-market-biz/pom.xml
@@ -3,8 +3,8 @@
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
-        <artifactId>yudao-module-market</artifactId>
         <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-mall</artifactId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>

From 4e67b6bbcfb353ac8da1df71bb1342deeac337fb Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 14:42:51 +0800
Subject: [PATCH 2/7] =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mall.sql                                  |  55 ++++
 yudao-module-mall/pom.xml                     |   2 +
 .../yudao-module-product-api/pom.xml          |  27 ++
 .../module/product/api/package-info.java      |   4 +
 .../product/enums/ErrorCodeConstants.java     |  14 +
 .../yudao-module-product-biz/pom.xml          |  67 +++++
 .../admin/category/CategoryController.java    | 100 +++++++
 .../admin/category/vo/CategoryBaseVO.java     |  42 +++
 .../category/vo/CategoryCreateReqVO.java      |  14 +
 .../admin/category/vo/CategoryExcelVO.java    |  48 +++
 .../category/vo/CategoryExportReqVO.java      |  30 ++
 .../admin/category/vo/CategoryPageReqVO.java  |  35 +++
 .../admin/category/vo/CategoryRespVO.java     |  19 ++
 .../category/vo/CategoryUpdateReqVO.java      |  18 ++
 .../convert/category/CategoryConvert.java     |  34 +++
 .../dal/dataobject/category/CategoryDO.java   |  58 ++++
 .../dal/mysql/category/CategoryMapper.java    |  37 +++
 .../yudao/module/product/package-info.java    |   7 +
 .../service/category/CategoryService.java     |  70 +++++
 .../service/category/CategoryServiceImpl.java |  82 +++++
 .../category/CategoryServiceImplTest.java     | 194 ++++++++++++
 .../src/test/resources/sql/clean.sql          |   1 +
 .../src/test/resources/sql/create_tables.sql  |  17 ++
 yudao-server/pom.xml                          |  13 +-
 .../src/api/mall/product/category.js          |  54 ++++
 .../src/views/mall/product/category/index.vue | 281 ++++++++++++++++++
 26 files changed, 1319 insertions(+), 4 deletions(-)
 create mode 100644 sql/mall.sql
 create mode 100644 yudao-module-mall/yudao-module-product-api/pom.xml
 create mode 100644 yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/api/package-info.java
 create mode 100644 yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/pom.xml
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryCreateReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExcelVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExportReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryPageReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryRespVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryUpdateReqVO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/category/CategoryConvert.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/category/CategoryDO.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/category/CategoryMapper.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/package-info.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImplTest.java
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/clean.sql
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql
 create mode 100644 yudao-ui-admin/src/api/mall/product/category.js
 create mode 100644 yudao-ui-admin/src/views/mall/product/category/index.vue

diff --git a/sql/mall.sql b/sql/mall.sql
new file mode 100644
index 000000000..390044e06
--- /dev/null
+++ b/sql/mall.sql
@@ -0,0 +1,55 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server         : 127.0.0.1
+ Source Server Type    : MySQL
+ Source Server Version : 80026
+ Source Host           : localhost:3306
+ Source Schema         : ruoyi-vue-pro
+
+ Target Server Type    : MySQL
+ Target Server Version : 80026
+ File Encoding         : 65001
+
+ Date: 05/02/2022 00:50:30
+*/
+SET
+FOREIGN_KEY_CHECKS = 0;
+SET NAMES utf8mb4;
+
+-- ----------------------------
+-- Table structure for product_category
+-- ----------------------------
+DROP TABLE IF EXISTS `product_category`;
+CREATE TABLE `product_category`
+(
+    `id`          bigint        NOT NULL AUTO_INCREMENT COMMENT '分类编号',
+    `pid`         bigint        NOT NULL COMMENT '父分类编号',
+    `name`        varchar(255)  NOT NULL COMMENT '分类名称',
+    `icon`        varchar(100)           DEFAULT '#' COMMENT '分类图标',
+    `banner_url`  varchar(255)  NOT NULL COMMENT '分类图片',
+    `sort`        int           NOT NULL DEFAULT '0' COMMENT '分类排序',
+    `description` varchar(1024) NOT NULL COMMENT '分类描述',
+    `status`      tinyint       NOT NULL COMMENT '开启状态',
+    `creator`     varchar(64)            DEFAULT '' COMMENT '创建者',
+    `create_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `updater`     varchar(64)            DEFAULT '' COMMENT '更新者',
+    `update_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `deleted`     bit(1)        NOT NULL DEFAULT b'0' COMMENT '是否删除',
+    `tenant_id`   bigint        NOT NULL DEFAULT '0' COMMENT '租户编号',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB COMMENT='商品分类';
+
+-- TODO 父级菜单的 id 处理
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES (2000, '商城', '', 1, 1, 0, '/mall', 'merchant', NULL, 0);
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES (2001, '商品管理', '', 1, 1, 2000, 'product', 'dict', NULL, 0);
+-- 菜单 SQL
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类管理', '', 2, 0, 2001, 'category', '', 'mall/product/category/index', 0);
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+-- 按钮 SQL
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类查询', 'product:category:query', 3, 1, @parentId, '', '', '', 0);
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类创建', 'product:category:create', 3, 2, @parentId, '', '', '', 0);
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类更新', 'product:category:update', 3, 3, @parentId, '', '', '', 0);
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类删除', 'product:category:delete', 3, 4, @parentId, '', '', '', 0);
+INSERT INTO `system_menu`(`name`, `permission`, `menu_type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`) VALUES ('商品分类导出', 'product:category:export', 3, 5, @parentId, '', '', '', 0);
diff --git a/yudao-module-mall/pom.xml b/yudao-module-mall/pom.xml
index b13dc68f6..866874852 100644
--- a/yudao-module-mall/pom.xml
+++ b/yudao-module-mall/pom.xml
@@ -21,6 +21,8 @@
     <modules>
         <module>yudao-module-market-api</module>
         <module>yudao-module-market-biz</module>
+        <module>yudao-module-product-api</module>
+        <module>yudao-module-product-biz</module>
     </modules>
 
 </project>
diff --git a/yudao-module-mall/yudao-module-product-api/pom.xml b/yudao-module-mall/yudao-module-product-api/pom.xml
new file mode 100644
index 000000000..7eb38008a
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-api/pom.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-mall</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>yudao-module-product-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        product 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/api/package-info.java b/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/api/package-info.java
new file mode 100644
index 000000000..b19092853
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位
+ */
+package cn.iocoder.yudao.module.product.api;
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java
new file mode 100644
index 000000000..b8c671fc5
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-api/src/main/cn/iocoder/yudao/module/product/enums/ErrorCodeConstants.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.product.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * product 错误码枚举类
+ * <p>
+ * product 系统,使用 1-008-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== 商品分类相关  1008001000============
+    ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1008001000, "商品分类不存在");
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/pom.xml b/yudao-module-mall/yudao-module-product-biz/pom.xml
new file mode 100644
index 000000000..a06f8937c
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/pom.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-mall</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>yudao-module-product-biz</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        product 模块,主要实现商品相关功能
+        例如:品牌、商品分类、spu、sku等功能。
+    </description>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-product-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
new file mode 100644
index 000000000..346b3177a
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
@@ -0,0 +1,100 @@
+package cn.iocoder.yudao.module.product.controller.admin.category;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.annotations.*;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
+import cn.iocoder.yudao.module.product.service.category.CategoryService;
+
+@Api(tags = "管理后台 - 商品分类")
+@RestController
+@RequestMapping("/product/category")
+@Validated
+public class CategoryController {
+
+    @Resource
+    private CategoryService categoryService;
+
+    @PostMapping("/create")
+    @ApiOperation("创建商品分类")
+    @PreAuthorize("@ss.hasPermission('product:category:create')")
+    public CommonResult<Long> createCategory(@Valid @RequestBody CategoryCreateReqVO createReqVO) {
+        return success(categoryService.createCategory(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新商品分类")
+    @PreAuthorize("@ss.hasPermission('product:category:update')")
+    public CommonResult<Boolean> updateCategory(@Valid @RequestBody CategoryUpdateReqVO updateReqVO) {
+        categoryService.updateCategory(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("删除商品分类")
+    @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
+    @PreAuthorize("@ss.hasPermission('product:category:delete')")
+    public CommonResult<Boolean> deleteCategory(@RequestParam("id") Long id) {
+        categoryService.deleteCategory(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @ApiOperation("获得商品分类")
+    @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
+    @PreAuthorize("@ss.hasPermission('product:category:query')")
+    public CommonResult<CategoryRespVO> getCategory(@RequestParam("id") Long id) {
+        CategoryDO category = categoryService.getCategory(id);
+        return success(CategoryConvert.INSTANCE.convert(category));
+    }
+
+    @GetMapping("/list")
+    @ApiOperation("获得商品分类列表")
+    @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
+    @PreAuthorize("@ss.hasPermission('product:category:query')")
+    public CommonResult<List<CategoryRespVO>> getCategoryList(@RequestParam("ids") Collection<Long> ids) {
+        List<CategoryDO> list = categoryService.getCategoryList(ids);
+        return success(CategoryConvert.INSTANCE.convertList(list));
+    }
+
+    @GetMapping("/page")
+    @ApiOperation("获得商品分类分页")
+    @PreAuthorize("@ss.hasPermission('product:category:query')")
+    public CommonResult<PageResult<CategoryRespVO>> getCategoryPage(@Valid CategoryPageReqVO pageVO) {
+        PageResult<CategoryDO> pageResult = categoryService.getCategoryPage(pageVO);
+        return success(CategoryConvert.INSTANCE.convertPage(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @ApiOperation("导出商品分类 Excel")
+    @PreAuthorize("@ss.hasPermission('product:category:export')")
+    @OperateLog(type = EXPORT)
+    public void exportCategoryExcel(@Valid CategoryExportReqVO exportReqVO,
+              HttpServletResponse response) throws IOException {
+        List<CategoryDO> list = categoryService.getCategoryList(exportReqVO);
+        // 导出 Excel
+        List<CategoryExcelVO> datas = CategoryConvert.INSTANCE.convertList02(list);
+        ExcelUtils.write(response, "商品分类.xls", "数据", CategoryExcelVO.class, datas);
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
new file mode 100644
index 000000000..82f2ba09d
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import javax.validation.constraints.*;
+
+/**
+* 商品分类 Base VO,提供给添加、修改、详细的子 VO 使用
+* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+*/
+@Data
+public class CategoryBaseVO {
+
+    @ApiModelProperty(value = "父分类编号", required = true, example = "1")
+    @NotNull(message = "父分类编号不能为空")
+    private Long pid;
+
+    @ApiModelProperty(value = "分类名称", required = true, example = "办公文具")
+    @NotNull(message = "分类名称不能为空")
+    private String name;
+
+    @ApiModelProperty(value = "分类图标")
+    private String icon;
+
+    @ApiModelProperty(value = "分类图片", required = true)
+    @NotNull(message = "分类图片不能为空")
+    private String bannerUrl;
+
+    @ApiModelProperty(value = "分类排序", required = true, example = "1")
+    @NotNull(message = "分类排序不能为空")
+    private Integer sort;
+
+    @ApiModelProperty(value = "分类描述", required = true, example = "描述")
+    @NotNull(message = "分类描述不能为空")
+    private String description;
+
+    @ApiModelProperty(value = "开启状态", required = true, example = "0")
+    @NotNull(message = "开启状态不能为空")
+    private Integer status;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryCreateReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryCreateReqVO.java
new file mode 100644
index 000000000..ce583f08b
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryCreateReqVO.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import javax.validation.constraints.*;
+
+@ApiModel("管理后台 - 商品分类创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CategoryCreateReqVO extends CategoryBaseVO {
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExcelVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExcelVO.java
new file mode 100644
index 000000000..ae754e42d
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExcelVO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+
+/**
+ * 商品分类 Excel VO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class CategoryExcelVO {
+
+    @ExcelProperty("分类编号")
+    private Long id;
+
+    @ExcelProperty("父分类编号")
+    private Long pid;
+
+    @ExcelProperty("分类名称")
+    private String name;
+
+    @ExcelProperty("分类图标")
+    private String icon;
+
+    @ExcelProperty("分类图片")
+    private String bannerUrl;
+
+    @ExcelProperty("分类排序")
+    private Integer sort;
+
+    @ExcelProperty("分类描述")
+    private String description;
+
+    @ExcelProperty(value = "开启状态", converter = DictConvert.class)
+    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
+    private Integer status;
+
+    @ExcelProperty("创建时间")
+    private Date createTime;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExportReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExportReqVO.java
new file mode 100644
index 000000000..c1e23b3ed
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryExportReqVO.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@ApiModel(value = "管理后台 - 商品分类 Excel 导出 Request VO", description = "参数和 CategoryPageReqVO 是一致的")
+@Data
+public class CategoryExportReqVO {
+
+    @ApiModelProperty(value = "分类名称", example = "办公文具")
+    private String name;
+
+    @ApiModelProperty(value = "开启状态", example = "0")
+    private Integer status;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始创建时间")
+    private Date beginCreateTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束创建时间")
+    private Date endCreateTime;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryPageReqVO.java
new file mode 100644
index 000000000..d824b8bd8
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryPageReqVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@ApiModel("管理后台 - 商品分类分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CategoryPageReqVO extends PageParam {
+
+    @ApiModelProperty(value = "分类名称", example = "办公文具")
+    private String name;
+
+    @ApiModelProperty(value = "开启状态", example = "0")
+    private Integer status;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始创建时间")
+    private Date beginCreateTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束创建时间")
+    private Date endCreateTime;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryRespVO.java
new file mode 100644
index 000000000..e7d0b2238
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+
+@ApiModel("管理后台 - 商品分类 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CategoryRespVO extends CategoryBaseVO {
+
+    @ApiModelProperty(value = "分类编号", required = true, example = "2")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时间", required = true)
+    private Date createTime;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryUpdateReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryUpdateReqVO.java
new file mode 100644
index 000000000..13ee83c1e
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.annotations.*;
+import javax.validation.constraints.*;
+
+@ApiModel("管理后台 - 商品分类更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class CategoryUpdateReqVO extends CategoryBaseVO {
+
+    @ApiModelProperty(value = "分类编号", required = true, example = "2")
+    @NotNull(message = "分类编号不能为空")
+    private Long id;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/category/CategoryConvert.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/category/CategoryConvert.java
new file mode 100644
index 000000000..2b2cfe99c
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/convert/category/CategoryConvert.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.product.convert.category;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+
+/**
+ * 商品分类 Convert
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CategoryConvert {
+
+    CategoryConvert INSTANCE = Mappers.getMapper(CategoryConvert.class);
+
+    CategoryDO convert(CategoryCreateReqVO bean);
+
+    CategoryDO convert(CategoryUpdateReqVO bean);
+
+    CategoryRespVO convert(CategoryDO bean);
+
+    List<CategoryRespVO> convertList(List<CategoryDO> list);
+
+    PageResult<CategoryRespVO> convertPage(PageResult<CategoryDO> page);
+
+    List<CategoryExcelVO> convertList02(List<CategoryDO> list);
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/category/CategoryDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/category/CategoryDO.java
new file mode 100644
index 000000000..bbebfc11c
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/category/CategoryDO.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.product.dal.dataobject.category;
+
+import lombok.*;
+import java.util.*;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 商品分类 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("product_category")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CategoryDO extends BaseDO {
+
+    /**
+     * 分类编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 父分类编号
+     */
+    private Long pid;
+    /**
+     * 分类名称
+     */
+    private String name;
+    /**
+     * 分类图标
+     */
+    private String icon;
+    /**
+     * 分类图片
+     */
+    private String bannerUrl;
+    /**
+     * 分类排序
+     */
+    private Integer sort;
+    /**
+     * 分类描述
+     */
+    private String description;
+    /**
+     * 开启状态
+     *
+     * 枚举 {@link TODO common_status 对应的类}
+     */
+    private Integer status;
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/category/CategoryMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/category/CategoryMapper.java
new file mode 100644
index 000000000..109f1ff54
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/category/CategoryMapper.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.product.dal.mysql.category;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 商品分类 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface CategoryMapper extends BaseMapperX<CategoryDO> {
+
+    default PageResult<CategoryDO> selectPage(CategoryPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<CategoryDO>()
+                .likeIfPresent(CategoryDO::getName, reqVO.getName())
+                .eqIfPresent(CategoryDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(CategoryDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
+                .orderByDesc(CategoryDO::getId));
+    }
+
+    default List<CategoryDO> selectList(CategoryExportReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<CategoryDO>()
+                .likeIfPresent(CategoryDO::getName, reqVO.getName())
+                .eqIfPresent(CategoryDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(CategoryDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
+                .orderByDesc(CategoryDO::getId));
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/package-info.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/package-info.java
new file mode 100644
index 000000000..4e80cc2bc
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * TODO
+ *
+ * @author JeromeSoar
+ * @since 2022-04-24
+ */
+package cn.iocoder.yudao.module.product;
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
new file mode 100644
index 000000000..ff5f520fa
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.product.service.category;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 商品分类 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface CategoryService {
+
+    /**
+     * 创建商品分类
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createCategory(@Valid CategoryCreateReqVO createReqVO);
+
+    /**
+     * 更新商品分类
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateCategory(@Valid CategoryUpdateReqVO updateReqVO);
+
+    /**
+     * 删除商品分类
+     *
+     * @param id 编号
+     */
+    void deleteCategory(Long id);
+
+    /**
+     * 获得商品分类
+     *
+     * @param id 编号
+     * @return 商品分类
+     */
+    CategoryDO getCategory(Long id);
+
+    /**
+     * 获得商品分类列表
+     *
+     * @param ids 编号
+     * @return 商品分类列表
+     */
+    List<CategoryDO> getCategoryList(Collection<Long> ids);
+
+    /**
+     * 获得商品分类分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 商品分类分页
+     */
+    PageResult<CategoryDO> getCategoryPage(CategoryPageReqVO pageReqVO);
+
+    /**
+     * 获得商品分类列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 商品分类列表
+     */
+    List<CategoryDO> getCategoryList(CategoryExportReqVO exportReqVO);
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
new file mode 100644
index 000000000..873bad297
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.module.product.service.category;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.*;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
+import cn.iocoder.yudao.module.product.dal.mysql.category.CategoryMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.*;
+
+/**
+ * 商品分类 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class CategoryServiceImpl implements CategoryService {
+
+    @Resource
+    private CategoryMapper categoryMapper;
+
+    @Override
+    public Long createCategory(CategoryCreateReqVO createReqVO) {
+        // 插入
+        CategoryDO category = CategoryConvert.INSTANCE.convert(createReqVO);
+        categoryMapper.insert(category);
+        // 返回
+        return category.getId();
+    }
+
+    @Override
+    public void updateCategory(CategoryUpdateReqVO updateReqVO) {
+        // 校验存在
+        this.validateCategoryExists(updateReqVO.getId());
+        // 更新
+        CategoryDO updateObj = CategoryConvert.INSTANCE.convert(updateReqVO);
+        categoryMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteCategory(Long id) {
+        // 校验存在
+        this.validateCategoryExists(id);
+        // 删除
+        categoryMapper.deleteById(id);
+    }
+
+    private void validateCategoryExists(Long id) {
+        if (categoryMapper.selectById(id) == null) {
+            throw exception(CATEGORY_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public CategoryDO getCategory(Long id) {
+        return categoryMapper.selectById(id);
+    }
+
+    @Override
+    public List<CategoryDO> getCategoryList(Collection<Long> ids) {
+        return categoryMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<CategoryDO> getCategoryPage(CategoryPageReqVO pageReqVO) {
+        return categoryMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<CategoryDO> getCategoryList(CategoryExportReqVO exportReqVO) {
+        return categoryMapper.selectList(exportReqVO);
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImplTest.java b/yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImplTest.java
new file mode 100644
index 000000000..c0ef7e572
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/test/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImplTest.java
@@ -0,0 +1,194 @@
+package cn.iocoder.yudao.module.product.service.category;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryCreateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryUpdateReqVO;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import cn.iocoder.yudao.module.product.dal.mysql.category.CategoryMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+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.randomLongId;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.CATEGORY_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link CategoryServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(CategoryServiceImpl.class)
+public class CategoryServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private CategoryServiceImpl categoryService;
+
+    @Resource
+    private CategoryMapper categoryMapper;
+
+    @Test
+    public void testCreateCategory_success() {
+        // 准备参数
+        CategoryCreateReqVO reqVO = randomPojo(CategoryCreateReqVO.class);
+
+        // 调用
+        Long categoryId = categoryService.createCategory(reqVO);
+        // 断言
+        assertNotNull(categoryId);
+        // 校验记录的属性是否正确
+        CategoryDO category = categoryMapper.selectById(categoryId);
+        assertPojoEquals(reqVO, category);
+    }
+
+    @Test
+    public void testUpdateCategory_success() {
+        // mock 数据
+        CategoryDO dbCategory = randomPojo(CategoryDO.class);
+        categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        CategoryUpdateReqVO reqVO = randomPojo(CategoryUpdateReqVO.class, o -> {
+            o.setId(dbCategory.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        categoryService.updateCategory(reqVO);
+        // 校验是否更新正确
+        CategoryDO category = categoryMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, category);
+    }
+
+    @Test
+    public void testUpdateCategory_notExists() {
+        // 准备参数
+        CategoryUpdateReqVO reqVO = randomPojo(CategoryUpdateReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> categoryService.updateCategory(reqVO), CATEGORY_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteCategory_success() {
+        // mock 数据
+        CategoryDO dbCategory = randomPojo(CategoryDO.class);
+        categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbCategory.getId();
+
+        // 调用
+        categoryService.deleteCategory(id);
+        // 校验数据不存在了
+        assertNull(categoryMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteCategory_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCategoryPage() {
+        // mock 数据
+        CategoryDO dbCategory = randomPojo(CategoryDO.class, o -> { // 等会查询到
+            o.setPid(null);
+            o.setName(null);
+            o.setIcon(null);
+            o.setBannerUrl(null);
+            o.setSort(null);
+            o.setDescription(null);
+            o.setStatus(null);
+            o.setCreateTime(null);
+        });
+        categoryMapper.insert(dbCategory);
+        // 测试 pid 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setPid(null)));
+        // 测试 name 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null)));
+        // 测试 icon 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setIcon(null)));
+        // 测试 bannerUrl 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setBannerUrl(null)));
+        // 测试 sort 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setSort(null)));
+        // 测试 description 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setDescription(null)));
+        // 测试 status 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(null)));
+        // 测试 createTime 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCreateTime(null)));
+        // 准备参数
+        CategoryPageReqVO reqVO = new CategoryPageReqVO();
+        reqVO.setName(null);
+        reqVO.setStatus(null);
+        reqVO.setBeginCreateTime(null);
+        reqVO.setEndCreateTime(null);
+
+        // 调用
+        PageResult<CategoryDO> pageResult = categoryService.getCategoryPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbCategory, pageResult.getList().get(0));
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetCategoryList() {
+        // mock 数据
+        CategoryDO dbCategory = randomPojo(CategoryDO.class, o -> { // 等会查询到
+            o.setPid(null);
+            o.setName(null);
+            o.setIcon(null);
+            o.setBannerUrl(null);
+            o.setSort(null);
+            o.setDescription(null);
+            o.setStatus(null);
+            o.setCreateTime(null);
+        });
+        categoryMapper.insert(dbCategory);
+        // 测试 pid 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setPid(null)));
+        // 测试 name 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null)));
+        // 测试 icon 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setIcon(null)));
+        // 测试 bannerUrl 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setBannerUrl(null)));
+        // 测试 sort 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setSort(null)));
+        // 测试 description 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setDescription(null)));
+        // 测试 status 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(null)));
+        // 测试 createTime 不匹配
+        categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCreateTime(null)));
+        // 准备参数
+        CategoryExportReqVO reqVO = new CategoryExportReqVO();
+        reqVO.setName(null);
+        reqVO.setStatus(null);
+        reqVO.setBeginCreateTime(null);
+        reqVO.setEndCreateTime(null);
+
+        // 调用
+        List<CategoryDO> list = categoryService.getCategoryList(reqVO);
+        // 断言
+        assertEquals(1, list.size());
+        assertPojoEquals(dbCategory, list.get(0));
+    }
+
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/clean.sql b/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/clean.sql
new file mode 100644
index 000000000..7c66b9074
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/clean.sql
@@ -0,0 +1 @@
+DELETE FROM "product_category";
\ No newline at end of file
diff --git a/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql b/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql
new file mode 100644
index 000000000..6400c8f0b
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/test/resources/sql/create_tables.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS "product_category" (
+    "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "pid" bigint(20) NOT NULL,
+    "name" varchar(255) NOT NULL,
+    "icon" varchar(100),
+    "banner_url" varchar(255) NOT NULL,
+    "sort" int(11) NOT NULL,
+    "description" varchar(1024) NOT NULL,
+    "status" tinyint(4) NOT NULL,
+    "creator" varchar(64) DEFAULT '',
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar(64) DEFAULT '',
+    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    "tenant_id" bigint(20) NOT NULL,
+    PRIMARY KEY ("id")
+) COMMENT '商品分类';
\ No newline at end of file
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index 73eba6068..b7eed1e50 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -47,16 +47,21 @@
             <artifactId>yudao-module-market-biz</artifactId>
             <version>${revision}</version>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-product-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
         <!-- 默认引入 yudao-module-bpm-biz-flowable 实现,可以替换为 yudao-module-bpm-biz-activiti 实现-->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-module-bpm-biz-flowable</artifactId>
             <version>${revision}</version>
         </dependency>
-<!--        <dependency>-->
-<!--            <groupId>cn.iocoder.boot</groupId>-->
-<!--            <artifactId>yudao-module-bpm-biz-activiti</artifactId>-->
-<!--            <version>${revision}</version>-->
+        <!--        <dependency>-->
+        <!--            <groupId>cn.iocoder.boot</groupId>-->
+        <!--            <artifactId>yudao-module-bpm-biz-activiti</artifactId>-->
+        <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
         <!-- spring boot 配置所需依赖 -->
diff --git a/yudao-ui-admin/src/api/mall/product/category.js b/yudao-ui-admin/src/api/mall/product/category.js
new file mode 100644
index 000000000..33fde73cd
--- /dev/null
+++ b/yudao-ui-admin/src/api/mall/product/category.js
@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 创建商品分类
+export function createCategory(data) {
+  return request({
+    url: '/product/category/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新商品分类
+export function updateCategory(data) {
+  return request({
+    url: '/product/category/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除商品分类
+export function deleteCategory(id) {
+  return request({
+    url: '/product/category/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得商品分类
+export function getCategory(id) {
+  return request({
+    url: '/product/category/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得商品分类分页
+export function getCategoryPage(query) {
+  return request({
+    url: '/product/category/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出商品分类 Excel
+export function exportCategoryExcel(query) {
+  return request({
+    url: '/product/category/export-excel',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}
diff --git a/yudao-ui-admin/src/views/mall/product/category/index.vue b/yudao-ui-admin/src/views/mall/product/category/index.vue
new file mode 100644
index 000000000..57273c146
--- /dev/null
+++ b/yudao-ui-admin/src/views/mall/product/category/index.vue
@@ -0,0 +1,281 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="分类名称" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入分类名称" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="开启状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)"
+                     :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker v-model="dateRangeCreateTime" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"/>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['product:category:create']">新增
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+                   :loading="exportLoading"
+                   v-hasPermi="['product:category:export']">导出
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="分类编号" align="center" prop="id"/>
+      <el-table-column label="父分类编号" align="center" prop="pid"/>
+      <el-table-column label="分类名称" align="center" prop="name"/>
+      <el-table-column label="分类图标" align="center" prop="icon"/>
+      <el-table-column label="分类图片" align="center" prop="bannerUrl"/>
+      <el-table-column label="分类排序" align="center" prop="sort"/>
+      <el-table-column label="分类描述" align="center" prop="description"/>
+      <el-table-column label="开启状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['product:category:update']">修改
+          </el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['product:category:delete']">删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="父分类编号" prop="pid">
+          <el-input v-model="form.pid" placeholder="请输入父分类编号"/>
+        </el-form-item>
+        <el-form-item label="分类名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入分类名称"/>
+        </el-form-item>
+        <el-form-item label="分类图标" prop="icon">
+          <el-input v-model="form.icon" placeholder="请输入分类图标"/>
+        </el-form-item>
+        <el-form-item label="分类图片" prop="bannerUrl">
+          <el-input v-model="form.bannerUrl" placeholder="请输入分类图片"/>
+        </el-form-item>
+        <el-form-item label="分类排序" prop="sort">
+          <el-input v-model="form.sort" placeholder="请输入分类排序"/>
+        </el-form-item>
+        <el-form-item label="分类描述">
+          <editor v-model="form.description" :min-height="192"/>
+        </el-form-item>
+        <el-form-item label="开启状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)"
+                      :key="dict.value" :label="parseInt(dict.value)">{{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </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 {
+  createCategory,
+  deleteCategory,
+  exportCategoryExcel,
+  getCategory,
+  getCategoryPage,
+  updateCategory
+} from "@/api/mall/product/category";
+import Editor from '@/components/Editor';
+
+export default {
+  name: "Category",
+  components: {
+    Editor,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 商品分类列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        name: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        pid: [{required: true, message: "父分类编号不能为空", trigger: "blur"}],
+        name: [{required: true, message: "分类名称不能为空", trigger: "blur"}],
+        bannerUrl: [{required: true, message: "分类图片不能为空", trigger: "blur"}],
+        sort: [{required: true, message: "分类排序不能为空", trigger: "blur"}],
+        description: [{required: true, message: "分类描述不能为空", trigger: "blur"}],
+        status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getCategoryPage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        pid: undefined,
+        name: undefined,
+        icon: undefined,
+        bannerUrl: undefined,
+        sort: undefined,
+        description: undefined,
+        status: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加商品分类";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id;
+      getCategory(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改商品分类";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (!valid) {
+          return;
+        }
+        // 修改的提交
+        if (this.form.id != null) {
+          updateCategory(this.form).then(response => {
+            this.$modal.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          });
+          return;
+        }
+        // 添加的提交
+        createCategory(this.form).then(response => {
+          this.$modal.msgSuccess("新增成功");
+          this.open = false;
+          this.getList();
+        });
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除商品分类编号为"' + id + '"的数据项?').then(function () {
+        return deleteCategory(id);
+      }).then(() => {
+        this.getList();
+        this.$modal.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      // 处理查询参数
+      let params = {...this.queryParams};
+      params.pageNo = undefined;
+      params.pageSize = undefined;
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行导出
+      this.$modal.confirm('是否确认导出所有商品分类数据项?').then(() => {
+        this.exportLoading = true;
+        return exportCategoryExcel(params);
+      }).then(response => {
+        this.$download.excel(response, '${table.classComment}.xls');
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    }
+  }
+};
+</script>

From d3edaec2b1d7c54bc5e03450431fb68983ec6368 Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 17:01:10 +0800
Subject: [PATCH 3/7] =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB?=
 =?UTF-8?q?=E7=BB=B4=E6=8A=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 sql/mall.sql                                  |  6 +-
 .../admin/category/CategoryController.java    | 48 +++++++------
 .../admin/category/vo/CategoryBaseVO.java     | 16 ++---
 .../service/category/CategoryService.java     | 19 +++--
 .../service/category/CategoryServiceImpl.java | 26 ++++---
 .../src/api/mall/product/category.js          |  9 +++
 .../src/views/mall/product/category/index.vue | 71 +++++++++++++++----
 7 files changed, 137 insertions(+), 58 deletions(-)

diff --git a/sql/mall.sql b/sql/mall.sql
index 390044e06..825d43e36 100644
--- a/sql/mall.sql
+++ b/sql/mall.sql
@@ -26,10 +26,10 @@ CREATE TABLE `product_category`
     `id`          bigint        NOT NULL AUTO_INCREMENT COMMENT '分类编号',
     `pid`         bigint        NOT NULL COMMENT '父分类编号',
     `name`        varchar(255)  NOT NULL COMMENT '分类名称',
-    `icon`        varchar(100)           DEFAULT '#' COMMENT '分类图标',
+    `icon`        varchar(100)  NOT NULL DEFAULT '#' COMMENT '分类图标',
     `banner_url`  varchar(255)  NOT NULL COMMENT '分类图片',
-    `sort`        int           NOT NULL DEFAULT '0' COMMENT '分类排序',
-    `description` varchar(1024) NOT NULL COMMENT '分类描述',
+    `sort`        int                    DEFAULT '0' COMMENT '分类排序',
+    `description` varchar(1024)          DEFAULT NULL COMMENT '分类描述',
     `status`      tinyint       NOT NULL COMMENT '开启状态',
     `creator`     varchar(64)            DEFAULT '' COMMENT '创建者',
     `create_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
index 346b3177a..ed22def1d 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
@@ -1,30 +1,29 @@
 package cn.iocoder.yudao.module.product.controller.admin.category;
 
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.annotations.*;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
 import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
-import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
 import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
 import cn.iocoder.yudao.module.product.service.category.CategoryService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
 @Api(tags = "管理后台 - 商品分类")
 @RestController
@@ -68,15 +67,22 @@ public class CategoryController {
         return success(CategoryConvert.INSTANCE.convert(category));
     }
 
-    @GetMapping("/list")
+    @GetMapping("/listByIds")
     @ApiOperation("获得商品分类列表")
-    @ApiImplicitParam(name = "ids", value = "编号列表", required = true, example = "1024,2048", dataTypeClass = List.class)
     @PreAuthorize("@ss.hasPermission('product:category:query')")
     public CommonResult<List<CategoryRespVO>> getCategoryList(@RequestParam("ids") Collection<Long> ids) {
         List<CategoryDO> list = categoryService.getCategoryList(ids);
         return success(CategoryConvert.INSTANCE.convertList(list));
     }
 
+    @GetMapping("/listByQuery")
+    @ApiOperation("获得商品分类列表")
+    @PreAuthorize("@ss.hasPermission('product:category:query')")
+    public CommonResult<List<CategoryRespVO>> listByQuery() {
+        List<CategoryDO> list = categoryService.listByQuery();
+        return success(CategoryConvert.INSTANCE.convertList(list));
+    }
+
     @GetMapping("/page")
     @ApiOperation("获得商品分类分页")
     @PreAuthorize("@ss.hasPermission('product:category:query')")
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
index 82f2ba09d..d4c2ebb8b 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryBaseVO.java
@@ -1,9 +1,10 @@
 package cn.iocoder.yudao.module.product.controller.admin.category.vo;
 
-import lombok.*;
-import java.util.*;
-import io.swagger.annotations.*;
-import javax.validation.constraints.*;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 
 /**
 * 商品分类 Base VO,提供给添加、修改、详细的子 VO 使用
@@ -17,22 +18,21 @@ public class CategoryBaseVO {
     private Long pid;
 
     @ApiModelProperty(value = "分类名称", required = true, example = "办公文具")
-    @NotNull(message = "分类名称不能为空")
+    @NotBlank(message = "分类名称不能为空")
     private String name;
 
     @ApiModelProperty(value = "分类图标")
+    @NotBlank(message = "分类图标不能为空")
     private String icon;
 
     @ApiModelProperty(value = "分类图片", required = true)
-    @NotNull(message = "分类图片不能为空")
+    @NotBlank(message = "分类图片不能为空")
     private String bannerUrl;
 
     @ApiModelProperty(value = "分类排序", required = true, example = "1")
-    @NotNull(message = "分类排序不能为空")
     private Integer sort;
 
     @ApiModelProperty(value = "分类描述", required = true, example = "描述")
-    @NotNull(message = "分类描述不能为空")
     private String description;
 
     @ApiModelProperty(value = "开启状态", required = true, example = "0")
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
index ff5f520fa..89ac934fb 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
@@ -1,10 +1,15 @@
 package cn.iocoder.yudao.module.product.service.category;
 
-import java.util.*;
-import javax.validation.*;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
-import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryCreateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryUpdateReqVO;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+
+import javax.validation.Valid;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * 商品分类 Service 接口
@@ -67,4 +72,10 @@ public interface CategoryService {
      */
     List<CategoryDO> getCategoryList(CategoryExportReqVO exportReqVO);
 
+    /**
+     * 获得商品分类列表
+     *
+     * @return 商品分类列表
+     */
+    List<CategoryDO> listByQuery();
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
index 873bad297..1465892f6 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
@@ -1,19 +1,22 @@
 package cn.iocoder.yudao.module.product.service.category;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryCreateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryUpdateReqVO;
+import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
+import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
+import cn.iocoder.yudao.module.product.dal.mysql.category.CategoryMapper;
 import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
 
-import java.util.*;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
-import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
-import cn.iocoder.yudao.module.product.dal.mysql.category.CategoryMapper;
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.CATEGORY_NOT_EXISTS;
 
 /**
  * 商品分类 Service 实现类
@@ -79,4 +82,9 @@ public class CategoryServiceImpl implements CategoryService {
         return categoryMapper.selectList(exportReqVO);
     }
 
+    @Override
+    public List<CategoryDO> listByQuery() {
+        return categoryMapper.selectList();
+    }
+
 }
diff --git a/yudao-ui-admin/src/api/mall/product/category.js b/yudao-ui-admin/src/api/mall/product/category.js
index 33fde73cd..bd825034d 100644
--- a/yudao-ui-admin/src/api/mall/product/category.js
+++ b/yudao-ui-admin/src/api/mall/product/category.js
@@ -34,6 +34,15 @@ export function getCategory(id) {
   })
 }
 
+// 获得商品分类
+export function listCategory(query) {
+  return request({
+    url: '/product/category/listByQuery',
+    method: 'get',
+    params: query
+  })
+}
+
 // 获得商品分类分页
 export function getCategoryPage(query) {
   return request({
diff --git a/yudao-ui-admin/src/views/mall/product/category/index.vue b/yudao-ui-admin/src/views/mall/product/category/index.vue
index 57273c146..26cfcdf94 100644
--- a/yudao-ui-admin/src/views/mall/product/category/index.vue
+++ b/yudao-ui-admin/src/views/mall/product/category/index.vue
@@ -40,13 +40,18 @@
 
     <!-- 列表 -->
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="分类编号" align="center" prop="id"/>
-      <el-table-column label="父分类编号" align="center" prop="pid"/>
       <el-table-column label="分类名称" align="center" prop="name"/>
-      <el-table-column label="分类图标" align="center" prop="icon"/>
-      <el-table-column label="分类图片" align="center" prop="bannerUrl"/>
+      <el-table-column label="分类图标" align="center" prop="icon">
+        <template slot-scope="scope">
+          <svg-icon :icon-class="scope.row.icon" />
+        </template>
+      </el-table-column>
+      <el-table-column label="分类图片" align="center" prop="bannerUrl">
+        <template slot-scope="scope">
+          <img v-if="scope.row.bannerUrl" :src="scope.row.bannerUrl" alt="分类图片"/>
+        </template>
+      </el-table-column>
       <el-table-column label="分类排序" align="center" prop="sort"/>
-      <el-table-column label="分类描述" align="center" prop="description"/>
       <el-table-column label="开启状态" align="center" prop="status">
         <template slot-scope="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status"/>
@@ -75,20 +80,28 @@
     <!-- 对话框(添加 / 修改) -->
     <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="父分类编号" prop="pid">
-          <el-input v-model="form.pid" placeholder="请输入父分类编号"/>
+        <el-form-item label="上级分类" prop="pid">
+          <Treeselect v-model="form.pid" :options="parentCategoryOptions" :normalizer="normalizer" :show-count="true"
+                      placeholder="上级分类"/>
         </el-form-item>
         <el-form-item label="分类名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入分类名称"/>
         </el-form-item>
         <el-form-item label="分类图标" prop="icon">
-          <el-input v-model="form.icon" placeholder="请输入分类图标"/>
+          <el-popover placement="bottom-start" width="460" trigger="click" @show="$refs['iconSelect'].reset()">
+            <IconSelect ref="iconSelect" @selected="iconSelected"/>
+            <el-input slot="reference" v-model="form.icon" placeholder="点击选择分类图标" readonly>
+              <svg-icon v-if="form.icon" slot="prefix" :icon-class="form.icon" class="el-input__icon"
+                        style="height: 32px;width: 16px;"/>
+              <i v-else slot="prefix" class="el-icon-search el-input__icon"/>
+            </el-input>
+          </el-popover>
         </el-form-item>
         <el-form-item label="分类图片" prop="bannerUrl">
           <el-input v-model="form.bannerUrl" placeholder="请输入分类图片"/>
         </el-form-item>
         <el-form-item label="分类排序" prop="sort">
-          <el-input v-model="form.sort" placeholder="请输入分类排序"/>
+          <el-input-number v-model="form.sort" controls-position="right" :min="0" />
         </el-form-item>
         <el-form-item label="分类描述">
           <editor v-model="form.description" :min-height="192"/>
@@ -116,14 +129,19 @@ import {
   exportCategoryExcel,
   getCategory,
   getCategoryPage,
+  listCategory,
   updateCategory
 } from "@/api/mall/product/category";
 import Editor from '@/components/Editor';
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import IconSelect from "@/components/IconSelect";
+import ImageUpload from '@/components/ImageUpload';
 
 export default {
   name: "Category",
   components: {
-    Editor,
+    Editor, Treeselect, IconSelect, ImageUpload
   },
   data() {
     return {
@@ -137,6 +155,8 @@ export default {
       total: 0,
       // 商品分类列表
       list: [],
+      // 商品分类树选项
+      parentCategoryOptions: [],
       // 弹出层标题
       title: "",
       // 是否显示弹出层
@@ -153,11 +173,10 @@ export default {
       form: {},
       // 表单校验
       rules: {
-        pid: [{required: true, message: "父分类编号不能为空", trigger: "blur"}],
+        pid: [{required: true, message: "请选择上级分类", trigger: "blur"}],
         name: [{required: true, message: "分类名称不能为空", trigger: "blur"}],
+        icon: [{required: true, message: "分类图标不能为空", trigger: "blur"}],
         bannerUrl: [{required: true, message: "分类图片不能为空", trigger: "blur"}],
-        sort: [{required: true, message: "分类排序不能为空", trigger: "blur"}],
-        description: [{required: true, message: "分类描述不能为空", trigger: "blur"}],
         status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
       }
     };
@@ -179,6 +198,30 @@ export default {
         this.loading = false;
       });
     },
+    // 选择图标
+    iconSelected(name) {
+      this.form.icon = name;
+    },
+    /** 转换菜单数据结构 */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.id,
+        label: node.name,
+        children: node.children
+      };
+    },
+    /** 查询分类下拉树结构 */
+    getTreeselect() {
+      listCategory().then(response => {
+        this.parentCategoryOptions = [];
+        const menu = {id: 0, name: '主分类', children: []};
+        menu.children = this.handleTree(response.data, "id", "pid");
+        this.parentCategoryOptions.push(menu);
+      });
+    },
     /** 取消按钮 */
     cancel() {
       this.open = false;
@@ -212,12 +255,14 @@ export default {
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
+      this.getTreeselect();
       this.open = true;
       this.title = "添加商品分类";
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
       this.reset();
+      this.getTreeselect();
       const id = row.id;
       getCategory(id).then(response => {
         this.form = response.data;

From e4ca2e25f9f518e366b4266cff9553bb49bea1c4 Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 17:37:52 +0800
Subject: [PATCH 4/7] =?UTF-8?q?=E6=A0=91=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/category/CategoryController.java    |  6 ++--
 .../category/vo/CategoryTreeListReqVO.java    | 29 ++++++++++++++++
 .../service/category/CategoryService.java     |  8 ++---
 .../service/category/CategoryServiceImpl.java |  9 ++---
 .../src/views/mall/product/category/index.vue | 34 +++++++++++--------
 5 files changed, 58 insertions(+), 28 deletions(-)
 create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryTreeListReqVO.java

diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
index ed22def1d..9e5614a87 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/CategoryController.java
@@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -78,8 +79,9 @@ public class CategoryController {
     @GetMapping("/listByQuery")
     @ApiOperation("获得商品分类列表")
     @PreAuthorize("@ss.hasPermission('product:category:query')")
-    public CommonResult<List<CategoryRespVO>> listByQuery() {
-        List<CategoryDO> list = categoryService.listByQuery();
+    public CommonResult<List<CategoryRespVO>> listByQuery(@Valid CategoryTreeListReqVO treeListReqVO) {
+        List<CategoryDO> list = categoryService.getCategoryTreeList(treeListReqVO);
+        list.sort(Comparator.comparing(CategoryDO::getSort));
         return success(CategoryConvert.INSTANCE.convertList(list));
     }
 
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryTreeListReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryTreeListReqVO.java
new file mode 100644
index 000000000..12256efd3
--- /dev/null
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/category/vo/CategoryTreeListReqVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.product.controller.admin.category.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Data
+@ApiModel(value = "管理后台 - 商品分类列表查询 Request VO", description = "参数和 CategoryPageReqVO 是一致的")
+public class CategoryTreeListReqVO extends CategoryExportReqVO {
+
+    @ApiModelProperty(value = "分类名称", example = "办公文具")
+    private String name;
+
+    @ApiModelProperty(value = "开启状态", example = "0")
+    private Integer status;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始创建时间")
+    private Date beginCreateTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束创建时间")
+    private Date endCreateTime;
+}
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
index 89ac934fb..b29b3418f 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryService.java
@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.product.service.category;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryCreateReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryUpdateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
 import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
 
 import javax.validation.Valid;
@@ -75,7 +72,8 @@ public interface CategoryService {
     /**
      * 获得商品分类列表
      *
+     * @param treeListReqVO 查询条件
      * @return 商品分类列表
      */
-    List<CategoryDO> listByQuery();
+    List<CategoryDO> getCategoryTreeList(CategoryTreeListReqVO treeListReqVO);
 }
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
index 1465892f6..b84ac5b9a 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
+++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/category/CategoryServiceImpl.java
@@ -1,10 +1,7 @@
 package cn.iocoder.yudao.module.product.service.category;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryCreateReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryExportReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryPageReqVO;
-import cn.iocoder.yudao.module.product.controller.admin.category.vo.CategoryUpdateReqVO;
+import cn.iocoder.yudao.module.product.controller.admin.category.vo.*;
 import cn.iocoder.yudao.module.product.convert.category.CategoryConvert;
 import cn.iocoder.yudao.module.product.dal.dataobject.category.CategoryDO;
 import cn.iocoder.yudao.module.product.dal.mysql.category.CategoryMapper;
@@ -83,8 +80,8 @@ public class CategoryServiceImpl implements CategoryService {
     }
 
     @Override
-    public List<CategoryDO> listByQuery() {
-        return categoryMapper.selectList();
+    public List<CategoryDO> getCategoryTreeList(CategoryTreeListReqVO treeListReqVO) {
+        return categoryMapper.selectList(treeListReqVO);
     }
 
 }
diff --git a/yudao-ui-admin/src/views/mall/product/category/index.vue b/yudao-ui-admin/src/views/mall/product/category/index.vue
index 26cfcdf94..cfef21c0f 100644
--- a/yudao-ui-admin/src/views/mall/product/category/index.vue
+++ b/yudao-ui-admin/src/views/mall/product/category/index.vue
@@ -12,10 +12,6 @@
                      :key="dict.value" :label="dict.label" :value="dict.value"/>
         </el-select>
       </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker v-model="dateRangeCreateTime" style="width: 240px" value-format="yyyy-MM-dd"
-                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"/>
-      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
@@ -29,6 +25,9 @@
                    v-hasPermi="['product:category:create']">新增
         </el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll">展开/折叠</el-button>
+      </el-col>
       <el-col :span="1.5">
         <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
                    :loading="exportLoading"
@@ -39,7 +38,8 @@
     </el-row>
 
     <!-- 列表 -->
-    <el-table v-loading="loading" :data="list">
+    <el-table v-if="refreshTable" v-loading="loading" :data="list"  row-key="id" :default-expand-all="isExpandAll"
+              :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
       <el-table-column label="分类名称" align="center" prop="name"/>
       <el-table-column label="分类图标" align="center" prop="icon">
         <template slot-scope="scope">
@@ -73,9 +73,6 @@
         </template>
       </el-table-column>
     </el-table>
-    <!-- 分页组件 -->
-    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
-                @pagination="getList"/>
 
     <!-- 对话框(添加 / 修改) -->
     <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@@ -161,7 +158,10 @@ export default {
       title: "",
       // 是否显示弹出层
       open: false,
-      dateRangeCreateTime: [],
+      // 是否展开,默认全部折叠
+      isExpandAll: false,
+      // 重新渲染表格状态
+      refreshTable: true,
       // 查询参数
       queryParams: {
         pageNo: 1,
@@ -190,11 +190,9 @@ export default {
       this.loading = true;
       // 处理查询参数
       let params = {...this.queryParams};
-      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
       // 执行查询
-      getCategoryPage(params).then(response => {
-        this.list = response.data.list;
-        this.total = response.data.total;
+      listCategory(params).then(response => {
+        this.list = this.handleTree(response.data, "id", "pid");
         this.loading = false;
       });
     },
@@ -248,10 +246,17 @@ export default {
     },
     /** 重置按钮操作 */
     resetQuery() {
-      this.dateRangeCreateTime = [];
       this.resetForm("queryForm");
       this.handleQuery();
     },
+    /** 展开/折叠操作 */
+    toggleExpandAll() {
+      this.refreshTable = false;
+      this.isExpandAll = !this.isExpandAll;
+      this.$nextTick(() => {
+        this.refreshTable = true;
+      });
+    },
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
@@ -310,7 +315,6 @@ export default {
       let params = {...this.queryParams};
       params.pageNo = undefined;
       params.pageSize = undefined;
-      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
       // 执行导出
       this.$modal.confirm('是否确认导出所有商品分类数据项?').then(() => {
         this.exportLoading = true;

From e3589eae11e00ab530d0ea8eb6bd1fe8d8b367f9 Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 17:55:16 +0800
Subject: [PATCH 5/7] =?UTF-8?q?=E7=AE=80=E5=8D=95=E4=B8=8A=E4=BC=A0?=
 =?UTF-8?q?=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../infra/controller/admin/file/FileController.java  | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
index e3d810dec..3dc49738a 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.infra.controller.admin.file;
 
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -23,6 +24,7 @@ import org.springframework.web.multipart.MultipartFile;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
+import java.util.Date;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
@@ -47,6 +49,16 @@ public class FileController {
         return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
     }
 
+    @PostMapping("/simple-upload")
+    @ApiOperation("简单上传文件")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
+    })
+    public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
+        String path = DateUtil.format(new Date(), "yyyy/MM/dd/") + file.getName();
+        return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
+    }
+
     @DeleteMapping("/delete")
     @ApiOperation("删除文件")
     @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)

From df23750203ab08cb763755448e24b2a506b14ef9 Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 18:02:25 +0800
Subject: [PATCH 6/7] =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/infra/controller/admin/file/FileController.java      | 2 +-
 yudao-ui-admin/src/components/ImageUpload/index.vue             | 2 +-
 yudao-ui-admin/src/views/mall/product/category/index.vue        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
index 3dc49738a..3ec24c9b1 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
@@ -55,7 +55,7 @@ public class FileController {
             @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
     })
     public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
-        String path = DateUtil.format(new Date(), "yyyy/MM/dd/") + file.getName();
+        String path = DateUtil.format(new Date(), "yyyy/MM/dd/") + file.getOriginalFilename();
         return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
     }
 
diff --git a/yudao-ui-admin/src/components/ImageUpload/index.vue b/yudao-ui-admin/src/components/ImageUpload/index.vue
index 170c1bd08..3462f250d 100644
--- a/yudao-ui-admin/src/components/ImageUpload/index.vue
+++ b/yudao-ui-admin/src/components/ImageUpload/index.vue
@@ -77,7 +77,7 @@ export default {
       dialogVisible: false,
       hideUpload: false,
       baseUrl: process.env.VUE_APP_BASE_API,
-      uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
+      uploadImgUrl: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/simple-upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
       },
diff --git a/yudao-ui-admin/src/views/mall/product/category/index.vue b/yudao-ui-admin/src/views/mall/product/category/index.vue
index cfef21c0f..751ad9c52 100644
--- a/yudao-ui-admin/src/views/mall/product/category/index.vue
+++ b/yudao-ui-admin/src/views/mall/product/category/index.vue
@@ -95,7 +95,7 @@
           </el-popover>
         </el-form-item>
         <el-form-item label="分类图片" prop="bannerUrl">
-          <el-input v-model="form.bannerUrl" placeholder="请输入分类图片"/>
+          <ImageUpload v-model="form.bannerUrl" />
         </el-form-item>
         <el-form-item label="分类排序" prop="sort">
           <el-input-number v-model="form.sort" controls-position="right" :min="0" />

From 145238ea967afa381086fd354982fec7defb035e Mon Sep 17 00:00:00 2001
From: JeromeSoar <jeromesoar@mail.com>
Date: Sun, 24 Apr 2022 18:05:54 +0800
Subject: [PATCH 7/7] =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../module/infra/controller/admin/file/FileController.java     | 3 ++-
 yudao-ui-admin/src/components/ImageUpload/index.vue            | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
index 3ec24c9b1..f53b45ac8 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
@@ -56,7 +56,8 @@ public class FileController {
     })
     public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
         String path = DateUtil.format(new Date(), "yyyy/MM/dd/") + file.getOriginalFilename();
-        return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
+        String file1 = fileService.createFile(path, IoUtil.readBytes(file.getInputStream()));
+        return success(file1);
     }
 
     @DeleteMapping("/delete")
diff --git a/yudao-ui-admin/src/components/ImageUpload/index.vue b/yudao-ui-admin/src/components/ImageUpload/index.vue
index 3462f250d..b736b7240 100644
--- a/yudao-ui-admin/src/components/ImageUpload/index.vue
+++ b/yudao-ui-admin/src/components/ImageUpload/index.vue
@@ -127,7 +127,7 @@ export default {
     },
     // 上传成功回调
     handleUploadSuccess(res) {
-      this.uploadList.push({ name: res.fileName, url: res.fileName });
+      this.uploadList.push({ name: res.data, url: res.data });
       if (this.uploadList.length === this.number) {
         this.fileList = this.fileList.concat(this.uploadList);
         this.uploadList = [];