环境搭建、首页展示与商品分类

YangeIT大约 18 分钟高级服务框架环境搭建Nginx首页接口商品分类Stream流

环境搭建、首页展示与商品分类

目标

  • 使用 Nginx 部署前端工程,并完成首页访问
  • 01 搭建商城后端基础工程
  • 完成首页接口开发
  • 完成三级分类树接口开发
  • 使用 Stream 流优化分类树组装逻辑
  • 扩展任务:完成 MinIO 或云存储方案调研与图片上传实践

前面几篇笔记更多是在铺底层能力,从这一篇开始,我们正式进入商城前台业务的开发。
因此本篇的内容顺序也会比较贴近真实开发流程:先把前端跑起来,再把后端工程搭起来,最后逐步实现首页与分类接口。

1.前端工程

前言

先把前端页面跑通,后续在联调接口时才更容易看到最终效果,也更方便理解接口到底要返回什么数据。

1.1.效果展示

首页效果图
首页效果图

1.2.功能介绍

商城前台当前涉及的核心功能如下:

首页、商品分类查询、关键字查询、商品详情、注册、登录、购物车、
用户收货地址管理、订单模块、支付...
功能截图
功能截图

技术架构如下:

模块技术选型
后端SpringBoot3 + Spring Cloud + Spring Cloud Alibaba(Nacos/Sentinel) + MyBatis Plus + Redis
前端uni-app

1.3.Nginx部署

前端工程是静态资源项目,因此最直接的方式就是通过 Nginx 托管。

Nginx部署
Nginx部署

操作步骤很简单:

  1. 将前端静态资源放到 html 目录下
  2. 启动 Nginx
  3. 浏览器访问 http://localhost:8088
  4. 打开浏览器开发者工具,切换到手机模式查看页面效果

注意: Nginx 所在目录尽量不要包含中文路径,否则很容易出现启动失败的问题。

当前阶段前端先跑起来即可,接口还没有完全打通,后面在完成首页与分类接口后再进行前后端联调。

本课程也是通过构建后端接口,串讲微服务所需的各个技术点,因此前端页面只是辅助作用

2.后端工程

前端页面准备好之后,接下来开始搭建后端工程。这里的重点不只是把项目创建出来,更重要的是把模块边界划分清楚,为后续继续拆分业务服务打好基础。

2.1.项目基本信息

前言

1.项目结构

项目结构
项目结构

.2.依赖关系

当前模块之间的依赖关系如下:

  • zx-service 依赖 zx-common
  • zx-common 依赖 zx-model

这套依赖关系的思路非常常见:

  • zx-model 放领域实体、VO、DTO 等基础模型
  • zx-common 放公共配置、工具类、公共能力
  • zx-service 放具体微服务实现

这样分层之后,后续不管新增 user-serviceorder-service 还是 product-service,都可以复用底层公共模块。

3.环境说明

本次项目开发使用的软件环境如下:

软件名称版本说明
jdkjdk17
spring boot3.0.2
spring cloud2022.0.2
spring cloud alibaba2022.0.0.0-RC2
redis7.0.10
mybaits-spring-boot-starter3.0.1
mysql8.0.29
idea2023以上
nacos server2.2.1
sentinel dashboard2.0.0-alpha-preview

2.2 项目搭建

这一节是整篇笔记的基础部分。环境搭好之后,后面的接口开发其实就是在已有脚手架上补业务代码。

前言

2.4.1.数据库初始化

执行如下脚本完成数据库初始化:

甄选资料\Sql\db_zx.sql
image
image

一、商品管理模块

  1. brand:商品品牌表,存储各品牌基础信息
  2. category:商品三级分类表,树形分类结构
  3. category_brand:分类-品牌多对多中间表,绑定分类与品牌关联关系
  4. product_unit:商品计量单位表,如个、台、包等
  5. product_spec:商品全局规格模板表,定义规格键值模板
  6. product:商品主表,存储商品基础信息、分类品牌、上下架审核状态
  7. product_details:商品详情图表,存放商品详情多张图片
  8. product_attr:商品基础属性表,存储商品静态属性键值
  9. product_sku:商品SKU库存表,区分不同规格商品,记录价格、库存、销量

二、优惠券模块

  1. coupon_info:优惠券主表,记录优惠券类型、发放、使用门槛等基础规则
  2. coupon_range:优惠券使用范围中间表,绑定可用分类/商品
  3. coupon_user:用户领券记录表,记录用户领取、使用、过期状态

三、订单支付模块

  1. order_info:订单主表,保存订单整体信息、收货地址、订单状态、金额
  2. order_item:订单订单项表,一条订单对应多条商品SKU明细
  3. order_log:订单操作日志,记录订单状态变更操作记录
  4. order_statistics:订单统计表,按省份、日期汇总订单金额与数量
  5. payment_info:支付流水表,记录微信/支付宝支付回调、支付状态

四、用户前台模块(C端会员)

  1. user_info:前台会员用户表,存储手机号、昵称、微信三方信息
  2. user_address:会员收货地址表,保存用户多个收货地址
  3. user_browse_history:商品浏览记录表,记录用户浏览过的SKU
  4. user_collect:商品收藏表,存储用户收藏SKU关联关系

五、地区基础数据

  1. region:全国省市区三级行政区域数据表

六、后台权限系统(B端管理员)

  1. sys_user:后台管理员账号表
  2. sys_role:后台角色表,定义角色名称与权限描述
  3. sys_menu:后台菜单表,存储左侧菜单栏树形结构
  4. sys_role_menu:角色-菜单中间表,分配角色可访问菜单
  5. sys_user_role:管理员-角色中间表,给用户分配角色
  6. sys_login_log:管理员登录日志,记录登录IP与登录结果
  7. sys_oper_log:后台操作日志,记录所有后台接口操作、入参返回值

七、其他业务表(测试遗留简易订单)

  1. tb_user:简易测试用户表(旧测试业务,非商城会员)
  2. tb_order:简易测试订单表(旧测试业务,正式订单用order_info)

八、分布式事务表

  1. 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,它主要负责承载模型相关依赖:

image
image
<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>
image
image

zx-common 主要放公共能力:

image
image
<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 放通用服务层依赖:

image
image
<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

image
image
<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>

创建各种模块后,效果如下:

image
image

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
image
image

文件内容如下:

com.zx.common.FastJsonLongToStringConfiguration

如下图所示:

image
image

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分页插件
    }
}
image
image

关于这几个注解,可以顺手梳理一下:👇 👇

  • @ServletComponentScan 用来扫描 ServletFilterListener 等组件,配合 @WebServlet@WebFilter@WebListener 使用
  • @MapperScan("com.zx.product.mapper") 用来扫描 MyBatisMapper 接口
  • PaginationInnerInterceptorMyBatis 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.需求分析

前台系统首页需要展示商品一级分类数据以及畅销商品列表数据,如下所示:

首页需求分析
首页需求分析

对应到数据库查询时,可以拆成两个动作:

  1. 查询 category 表,获取 parent_id = 0 的一级分类列表
  2. 查询 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 {
}

参考这个样式,完成ProductSkuCategory 的Mapper,Service,实现类。

如果想省事儿,使用MybatisX插件(用法自行检索),可以自动生成代码

image
image
image
image
image
image

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;
    }
}

这里的实现思路很直接:

  1. 查一级分类
  2. 查销量前 20SKU
  3. 封装到 IndexVo
  4. 返回给前端

其它 servicemapper、实体类可以按照常规方式自行补充。

3.3.测试

接口写完之后,不要急着继续往下做,先把单接口验证通过,再进入前后端联调阶段。

3.3.1.ApiFox测试

本地调试地址:

http://localhost:9001/product/index
ApiFox测试
ApiFox测试

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;
    }
}

这种写法对于初学者很友好,因为每一步都能清楚看到:

  1. 先查一级分类
  2. 再查一级下的二级分类
  3. 最后查二级下的三级分类

但问题也很明显:分类越多,数据库查询次数就越多,性能不够理想。

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组装树结构

既然分类数据都在同一张表中,那么更高效的思路就是:

  1. 先一次性把所有分类查出来
  2. 再在内存中按父子关系进行组装

代码如下:


@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;
    }
}

image
image

4.2.5.两种方案对比

到这里,可以把两种实现思路做一个简单总结:

方案思路优点缺点
方案一循环中分层查库容易理解查询次数多,性能一般
方案二一次查全表,内存中组装数据库压力更小,效率更高Stream 和数据组装要求更高

如果只是练习阶段,先写出方案一也没有问题;但在真实项目里,通常更推荐方案二。

5.小结

这一篇完成的是商城前台开发的第一步,核心产出有三个:

  1. 前端工程通过 Nginx 成功跑通
  2. product-service 基础环境搭建完成
  3. 首页接口和分类树接口开发完成

更重要的是,这一篇把后续商城业务开发的基础骨架立住了。
后面不管是商品详情、购物车,还是订单与支付,整体开发方式都会与本篇非常接近:先看页面,再拆需求,最后设计接口并完成联调。