环境搭建、首页展示与商品分类
环境搭建、首页展示与商品分类
目标
- 使用
Nginx部署前端工程,并完成首页访问 - 从
0到1搭建商城后端基础工程 - 完成首页接口开发
- 完成三级分类树接口开发
- 使用
Stream流优化分类树组装逻辑 - 扩展任务:完成
MinIO或云存储方案调研与图片上传实践
前面几篇笔记更多是在铺底层能力,从这一篇开始,我们正式进入商城前台业务的开发。
因此本篇的内容顺序也会比较贴近真实开发流程:先把前端跑起来,再把后端工程搭起来,最后逐步实现首页与分类接口。
1.前端工程
前言
先把前端页面跑通,后续在联调接口时才更容易看到最终效果,也更方便理解接口到底要返回什么数据。
1.1.效果展示

1.2.功能介绍
商城前台当前涉及的核心功能如下:
首页、商品分类查询、关键字查询、商品详情、注册、登录、购物车、
用户收货地址管理、订单模块、支付...

技术架构如下:
| 模块 | 技术选型 |
|---|---|
| 后端 | SpringBoot3 + Spring Cloud + Spring Cloud Alibaba(Nacos/Sentinel) + MyBatis Plus + Redis |
| 前端 | uni-app |
1.3.Nginx部署
前端工程是静态资源项目,因此最直接的方式就是通过 Nginx 托管。

操作步骤很简单:
- 将前端静态资源放到
html目录下 - 启动
Nginx - 浏览器访问
http://localhost:8088 - 打开浏览器开发者工具,切换到手机模式查看页面效果
注意: Nginx 所在目录尽量不要包含中文路径,否则很容易出现启动失败的问题。
当前阶段前端先跑起来即可,接口还没有完全打通,后面在完成首页与分类接口后再进行前后端联调。
本课程也是通过构建后端接口,串讲微服务所需的各个技术点,因此前端页面只是辅助作用
2.后端工程
前端页面准备好之后,接下来开始搭建后端工程。这里的重点不只是把项目创建出来,更重要的是把模块边界划分清楚,为后续继续拆分业务服务打好基础。
2.1.项目基本信息
前言
1.项目结构

.2.依赖关系
当前模块之间的依赖关系如下:
zx-service依赖zx-commonzx-common依赖zx-model
这套依赖关系的思路非常常见:
zx-model放领域实体、VO、DTO 等基础模型zx-common放公共配置、工具类、公共能力zx-service放具体微服务实现
这样分层之后,后续不管新增 user-service、order-service 还是 product-service,都可以复用底层公共模块。
3.环境说明
本次项目开发使用的软件环境如下:
| 软件名称 | 版本说明 |
|---|---|
| jdk | jdk17 |
| spring boot | 3.0.2 |
| spring cloud | 2022.0.2 |
| spring cloud alibaba | 2022.0.0.0-RC2 |
| redis | 7.0.10 |
| mybaits-spring-boot-starter | 3.0.1 |
| mysql | 8.0.29 |
| idea | 2023以上 |
| nacos server | 2.2.1 |
| sentinel dashboard | 2.0.0-alpha-preview |
2.2 项目搭建
这一节是整篇笔记的基础部分。环境搭好之后,后面的接口开发其实就是在已有脚手架上补业务代码。
前言
2.4.1.数据库初始化
执行如下脚本完成数据库初始化:
甄选资料\Sql\db_zx.sql

一、商品管理模块
- brand:商品品牌表,存储各品牌基础信息
- category:商品三级分类表,树形分类结构
- category_brand:分类-品牌多对多中间表,绑定分类与品牌关联关系
- product_unit:商品计量单位表,如个、台、包等
- product_spec:商品全局规格模板表,定义规格键值模板
- product:商品主表,存储商品基础信息、分类品牌、上下架审核状态
- product_details:商品详情图表,存放商品详情多张图片
- product_attr:商品基础属性表,存储商品静态属性键值
- product_sku:商品SKU库存表,区分不同规格商品,记录价格、库存、销量
二、优惠券模块
- coupon_info:优惠券主表,记录优惠券类型、发放、使用门槛等基础规则
- coupon_range:优惠券使用范围中间表,绑定可用分类/商品
- coupon_user:用户领券记录表,记录用户领取、使用、过期状态
三、订单支付模块
- order_info:订单主表,保存订单整体信息、收货地址、订单状态、金额
- order_item:订单订单项表,一条订单对应多条商品SKU明细
- order_log:订单操作日志,记录订单状态变更操作记录
- order_statistics:订单统计表,按省份、日期汇总订单金额与数量
- payment_info:支付流水表,记录微信/支付宝支付回调、支付状态
四、用户前台模块(C端会员)
- user_info:前台会员用户表,存储手机号、昵称、微信三方信息
- user_address:会员收货地址表,保存用户多个收货地址
- user_browse_history:商品浏览记录表,记录用户浏览过的SKU
- user_collect:商品收藏表,存储用户收藏SKU关联关系
五、地区基础数据
- region:全国省市区三级行政区域数据表
六、后台权限系统(B端管理员)
- sys_user:后台管理员账号表
- sys_role:后台角色表,定义角色名称与权限描述
- sys_menu:后台菜单表,存储左侧菜单栏树形结构
- sys_role_menu:角色-菜单中间表,分配角色可访问菜单
- sys_user_role:管理员-角色中间表,给用户分配角色
- sys_login_log:管理员登录日志,记录登录IP与登录结果
- sys_oper_log:后台操作日志,记录所有后台接口操作、入参返回值
七、其他业务表(测试遗留简易订单)
- tb_user:简易测试用户表(旧测试业务,非商城会员)
- tb_order:简易测试订单表(旧测试业务,正式订单用order_info)
八、分布式事务表
- undo_log:Seata分布式事务回滚日志表,用于事务补偿
2.4.2.项目结构图

2.4.3.依赖配置
先配置父工程 zx-front,统一管理版本与基础依赖。
zx-front 父工程依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.0.2</spring-boot.version>
<spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
<knife4j.version>4.4.0</knife4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.48</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
接着配置 zx-model,它主要负责承载模型相关依赖:

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<pagehelper.version>2.1.0</pagehelper.version>
<mybatis.plus.version>3.5.7</mybatis.plus.version>
<mysql.version>8.0.33</mysql.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
</dependencies>

zx-common 主要放公共能力:

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.zx</groupId>
<artifactId>zx-model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
zx-service 放通用服务层依赖:

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.zx</groupId>
<artifactId>zx-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.39.42.ALL</version>
</dependency>
</dependencies>
product-service 作为商品服务,还需要额外补充 Redisson:

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.1</version>
</dependency>
</dependencies>
创建各种模块后,效果如下:

2.4.4.拷贝实体类和工具类
为了加快项目初始化,可以直接复用资料中已有的模型代码和工具类。
1.实体类拷贝位置:
甄选资料\资料\day05-甄选-环境搭建-商品首页-商品分类\资料\模型代码
放入模块:
zx-model
包名:com.zx.domain

2.工具类拷贝位置:
甄选资料\资料\day05-甄选-环境搭建-商品首页-商品分类\资料\工具类
放入模块:
zx-common
包名:com.zx.common

3.然后在 zx-common 模块下创建自动装配文件:
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容如下:
com.zx.common.FastJsonLongToStringConfiguration
如下图所示:

2.4.5.product-service微服务配置
有了工程结构和依赖之后,就可以开始配置商品服务。后面的首页、分类接口,都是在这个服务里完成。
application.yml
server:
port: 9001 # 服务端口
spring:
data:
redis:
host: localhost
port: 6379 # redis配置端口,暂时配置上,后期再讲
application:
name: product-service # 服务名称
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_zx?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 1234
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos服务地址 sentinel后期再配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.zx.domain.entity.product
global-config:
datacenter-id: 1
workerId: 1
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #
这里特别要注意 mybatis-plus.mapper-locations。
如果 Mapper 接口中存在自定义方法,并且方法对应的 XML 不在默认扫描路径内,就必须显式配置这个属性。
ProductServiceApplication
package com.zx.product;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@MapperScan("com.zx.product.mapper")
@ServletComponentScan("com.zx.common")
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
return new PaginationInnerInterceptor(); // MP分页插件
}
}

关于这几个注解,可以顺手梳理一下:👇 👇
@ServletComponentScan用来扫描Servlet、Filter、Listener等组件,配合@WebServlet、@WebFilter、@WebListener使用@MapperScan("com.zx.product.mapper")用来扫描MyBatis的Mapper接口PaginationInnerInterceptor是MyBatis Plus的分页拦截器,后续分页查询时会直接用到
这里还有一个容易踩坑的点:product-service 的启动类在 com.zx.product 包下,而公共配置类在 com.zx.common 包下。
默认情况下,SpringBoot 只会扫描启动类所在包及其子包,因此 com.zx.common 下的配置不会自动生效。
常见有两种处理方式。
方式1:显式扩大扫描包范围
package com.zx.product;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(scanBasePackages = {"com.zx.product","com.zx.common"})//扩大扫描
@MapperScan("com.zx.product.mapper")
@ServletComponentScan("com.zx.common")
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
return new PaginationInnerInterceptor();
}
}
方式2:通过自动装配加载公共配置,推荐使用 👍
Spring Boot 3.0 之后,spring.factories 不再是自动装配的主入口,应该改为:
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容格式如下:
com.zx.common.FastJsonLongToStringConfiguration
如果是 Spring Boot 3.0 以下版本,也可以使用:
resources/META-INF/spring.factories
格式如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zx.common.FastJsonLongToStringConfiguration
到这里,商品服务的基础环境就算搭好了。接下来就可以真正进入业务接口开发。
3.首页接口开发
前言
首页接口的目标很明确,就是给前端首页一次性返回两类数据:
- 一级分类列表
- 畅销商品列表
3.1.需求分析
前台系统首页需要展示商品一级分类数据以及畅销商品列表数据,如下所示:

对应到数据库查询时,可以拆成两个动作:
- 查询
category表,获取parent_id = 0的一级分类列表 - 查询
product_sku表,按sale_num倒序排序,取前20条数据
这一步其实就是典型的“页面元素反推接口设计”。
前端页面需要展示什么,后端就返回什么,没有必要一开始就把所有商品信息都塞给前端。
3.2.接口文档
开发之前先对照接口文档,明确请求路径和返回结构。
首页接口示例:
get /api/product/index
返回结果:
{
"code": 200,
"message": "成功",
"data": {
"productSkuList": [
{
"id": 1,
"createTime": "2023-05-25 22:21:07",
"skuCode": "1_0",
"skuName": "小米 红米Note10 5G手机 颜色:白色 内存:8G",
"productId": 1,
"thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1 (1).jpg",
"salePrice": 1999.00,
"marketPrice": 2019.00,
"costPrice": 1599.00,
"stockNum": 99,
"saleNum": 1,
"skuSpec": "颜色:白色,内存:8G",
"weight": "1.00",
"volume": "1.00",
"status": null,
"skuSpecList": null
}
],
"categoryList": [
{
"id": 1,
"createTime": "2023-05-22 23:30:28",
"name": "数码办公",
"imageUrl": "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/230f48f024a343c6be9be72597c2dcd0.png",
"parentId": 0,
"status": 1,
"orderNum": 1,
"hasChildren": null,
"children": null
}
]
}
}
说明
接口文档通常是通过网关统一暴露的,所以看到的是 /api/product/index。
如果当前阶段是直接访问 product-service 进行本地调试,请求地址可能会是 /product/index。
3.2.1.IndexVo(已经在zx-model中)
为了让返回结构更清晰,可以单独定义一个 VO 来承载首页所需数据:
@Data
public class IndexVo {
private List<Category> categoryList; // 一级分类数据
private List<ProductSku> productSkuList; // 畅销商品列表
}
3.2.2.准备Mapper,Service,实现类
Mp的Mapper模版:
package com.zx.product.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zx.domain.entity.product.Product;
public interface ProductMapper extends BaseMapper<Product> {
}
Mp的Service模版:
ProductService 接口定义如下:
package com.zx.product.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zx.domain.entity.product.Product;
public interface ProductService extends IService<Product> { //MP标准写法
}
对应的实现类 ProductServiceImpl模版:
package com.zx.product.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zx.domain.entity.product.Product;
import com.zx.product.mapper.ProductMapper;
import com.zx.product.service.ProductService;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
参考这个样式,完成ProductSku,Category 的Mapper,Service,实现类。
如果想省事儿,使用MybatisX插件(用法自行检索),可以自动生成代码



3.2.2.IndexController
操作模块:product-service
表现层代码如下:
package com.zx.product.controller;
import com.zx.domain.vo.common.Result;
import com.zx.domain.vo.common.ResultCodeEnum;
import com.zx.domain.vo.h5.IndexVo;
import com.zx.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
public class IndexController {
@Autowired
private ProductService productService;
@GetMapping("/index")
public Result<IndexVo> findIndexData() {
IndexVo vo = productService.findIndexData();
return Result.build(vo, ResultCodeEnum.SUCCESS);
}
}
控制层本身没有复杂逻辑,核心还是交给 service 去做。
这样控制层只负责接收请求和返回结果,职责会更清晰。
3.2.3.Service实现
ProductServiceImpl
package com.zx.product.service.impl;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product>
implements ProductService{
// 商品分类
@Autowired
private CategoryService categoryService;
// 商品SKU
@Autowired
private ProductSkuService productSkuService;
@Override
public IndexVo findIndexData() {
//查询分类的一级分类
List<Category> categoryList = categoryService.lambdaQuery()
.eq(Category::getParentId, 0)
.list();
//查询热卖的前20个商品
List<ProductSku> productSkuList = productSkuService.lambdaQuery()
.orderByDesc(ProductSku::getSaleNum)
.last("limit 20")
.list();
//封装数据
IndexVo indexVo = new IndexVo();
indexVo.setCategoryList(categoryList);
indexVo.setProductSkuList(productSkuList);
return indexVo;
}
}
这里的实现思路很直接:
- 查一级分类
- 查销量前
20的SKU - 封装到
IndexVo - 返回给前端
其它
service、mapper、实体类可以按照常规方式自行补充。
3.3.测试
接口写完之后,不要急着继续往下做,先把单接口验证通过,再进入前后端联调阶段。
3.3.1.ApiFox测试
本地调试地址:
http://localhost:9001/product/index

3.3.2.前后端联调
启动 Nginx 后,访问:
http://localhost:8088
然后在前端页面中修改接口 base 路径,指向本地商品服务地址:
http://IP:端口


如果联调时出现跨域问题,可以在控制器IndexController上添加跨域注解:
@CrossOrigin(origins = "*", allowedHeaders = "*") // 整类生效
效果如下:

到这里,首页数据已经能够真正展示出来了。接下来继续完成分类导航的数据接口。
:::
4.分类接口开发
前言
首页接口解决的是“首页看什么”,分类接口解决的是“商品怎么分层展示”。
相比首页接口,分类接口的难点不在单表查询,而在于如何把平铺数据组装成三级树形结构。
4.1.需求分析
当用户点击分类导航按钮时,需要把系统中的全部分类数据按照三级结构返回出来。

本质上就是查询 category 表,并将结果组织为三级联动结构。
接口文档如下:
get /api/product/category/findCategoryTree
返回结果:
{
"code": 200,
"message": "成功",
"data": [
{
"id": 1,
"createTime": "2023-05-22 23:30:28",
"name": "数码办公",
"imageUrl": "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/230f48f024a343c6be9be72597c2dcd0.png",
"parentId": 0,
"status": 1,
"orderNum": 1,
"hasChildren": null,
"children": [
{
"id": 2,
"createTime": "2023-05-22 23:30:28",
"name": "手机通讯",
"imageUrl": "",
"parentId": 1,
"status": 1,
"orderNum": 0,
"hasChildren": null,
"children": [
{
"id": 3,
"createTime": "2023-05-22 23:30:28",
"name": "手机",
"imageUrl": "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/1348576427264204943.png",
"parentId": 2,
"status": 1,
"orderNum": 0,
"hasChildren": null,
"children": null
}
]
}
]
}
]
}
4.2.接口开发
操作模块仍然是 product-service。
4.2.1.CategoryController
表现层代码:
package com.zx.product.controller;
@RestController
@RequestMapping("/api/product/category")
public class CategoryController {
// 注入分类服务
@Autowired
private CategoryService categoryService;
@GetMapping("/findCategoryTree")
@CrossOrigin
public Result<List<Category>> findCategoryTree() {
List<Category> list = categoryService.findCategoryTree();
return Result.build(list, ResultCodeEnum.SUCCESS);
}
}
4.2.2.方案一:循环里查询数据库
先定义业务接口:
public interface CategoryService extends IService<Category> {
List<Category> findCategoryTree();
}
第一种实现方式比较容易理解,就是一层一层查数据库。
package com.zx.product.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zx.domain.entity.product.Category;
import com.zx.product.mapper.CategoryMapper;
import com.zx.product.service.CategoryService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
// 方案一:查询多次 SQL
// 优点:思路直观,层次清晰
// 缺点:N 次 IO,效率较低
@Override
public List<Category> findCategoryTree() {
List<Category> oneLevelList = lambdaQuery().eq(Category::getParentId, 0).list();
for (Category one : oneLevelList) {
List<Category> twoLevelList = lambdaQuery().eq(Category::getParentId, one.getId()).list();
for (Category two : twoLevelList) {
List<Category> threeLevelList = lambdaQuery().eq(Category::getParentId, two.getId()).list();
two.setChildren(threeLevelList);
}
one.setChildren(twoLevelList);
}
return oneLevelList;
}
}
这种写法对于初学者很友好,因为每一步都能清楚看到:
- 先查一级分类
- 再查一级下的二级分类
- 最后查二级下的三级分类
但问题也很明显:分类越多,数据库查询次数就越多,性能不够理想。
4.2.3.Stream流
在优化方案之前,先简单回顾一下 Stream 流常用操作:
/*
* Stream流
* 流式操作数据,只对数据加工,并不存储数据
* 如果想要加工后的数据,需要收集
* 常用方法:
* 1. 获取 stream 流 stream()
* 2. 过滤 filter(str -> str.length() == 3)
* 3. 收集 collect(Collectors.toList())
* 4. 类型转换 map()
* 5. 遍历 forEach(str -> System.out.println(str))
*/
4.2.4.方案二:一次查询 + Stream组装树结构
既然分类数据都在同一张表中,那么更高效的思路就是:
- 先一次性把所有分类查出来
- 再在内存中按父子关系进行组装
代码如下:
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category>
implements CategoryService{
@Override
public List<Category> findCategoryTree() {
//查出所有的分类信息
List<Category> categoryList = lambdaQuery().list();
//现在数据有了,接下来基于这些数据进行组装(3级列表)
//1.一级分类
List<Category> oneLevelList = categoryList.stream().filter(category -> category.getParentId().longValue() == 0).collect(Collectors.toList());
//2.遍历一级分类,获取每个一级分类下的二级分类
oneLevelList.forEach(oneLevelCategory -> {
List<Category> twoLevelList = categoryList.stream()
.filter(category -> category.getParentId().longValue() == oneLevelCategory.getId().longValue())
.collect(Collectors.toList());
//3.遍历二级分类,获取每个二级分类下的三级分类
twoLevelList.forEach(twoLevelCategory -> {
List<Category> threeLevelList = categoryList.stream()
.filter(category -> category.getParentId().longValue() == twoLevelCategory.getId().longValue())
.collect(Collectors.toList());
//4.将三级分类设置到二级分类中
twoLevelCategory.setChildren(threeLevelList);
});
//5.将二级分类设置到一级分类中
oneLevelCategory.setChildren(twoLevelList);
});
return oneLevelList;
}
}

4.2.5.两种方案对比
到这里,可以把两种实现思路做一个简单总结:
| 方案 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| 方案一 | 循环中分层查库 | 容易理解 | 查询次数多,性能一般 |
| 方案二 | 一次查全表,内存中组装 | 数据库压力更小,效率更高 | 对 Stream 和数据组装要求更高 |
如果只是练习阶段,先写出方案一也没有问题;但在真实项目里,通常更推荐方案二。
5.小结
这一篇完成的是商城前台开发的第一步,核心产出有三个:
- 前端工程通过
Nginx成功跑通 product-service基础环境搭建完成- 首页接口和分类树接口开发完成
更重要的是,这一篇把后续商城业务开发的基础骨架立住了。
后面不管是商品详情、购物车,还是订单与支付,整体开发方式都会与本篇非常接近:先看页面,再拆需求,最后设计接口并完成联调。