Java电商《锋迷商城》笔记

目录

⼀、《锋迷商城》项⽬介绍

1.1 项⽬背景

1.1.1 B2C

1.1.2 C2B2C

1.1.3 Java

1.2 项⽬功能

1.3 技术选型

1.3.1 单体项⽬

1.3.2 技术清单

⼆、项⽬架构的演进

2.1 单体架构

2.2 前后端分离

2.3 集群与负载均衡

2.5 微服务架构

三、《锋迷商城》项⽬搭建

3.1 技术储备

3.2 创建Maven聚合⼯程

3.2.1 构建⽗⼯程fmmall

3.2.2 创建common⼯程

3.2.3 创建beans⼯程

3.2.4 创建mapper⼯程

3.2.5 创建service⼯程

3.3 Maven聚合⼯程依赖分析

3.4 整合MyBatis

3.4.1 common⼦⼯程

3.4.2 beans⼦⼯程

3.4.3 MyBatis整合

3.5 基于SpringBoot的单元测试(API模块)

3.5.1 添加依赖

3.5.2 测试类

3.6 整合Druid

3.6.1 添加依赖

3.6.2 修改数据源配置

四、《锋迷商城》数据库设计

4.1 软件开发步骤

4.2 数据库设计流程

4.3 数据库设计分析

4.3.1 PDMan建模⼯具使⽤

4.3.2 分析《锋迷商城》的数据库模型

4.4 SPU 和 SKU

4.4.1 SPU

4.4.2 SKU

4.5 建库建表

4.5.1 创建数据表

4.5.2 准备测试数据

五、《锋迷商城》业务流程设计-接⼝规范

5.1 前后端分离与单体架构流程实现的区别

5.1.1 单体架构

5.1.2 前后端分离架构

5.2 接⼝介绍

5.2.1 接⼝概念

5.2.2 接⼝规范

5.3 Swagger

5.3.1 作⽤

5.3.2 Swagger整合

5.3.3 Swagger注解说明

5.3.4 Swagger-ui 插件

5.4 RESTful

六、《锋迷商城》设计及实现—⽤户管理

6.1 实现流程

6.2 后端接⼝开发

6.2.1 完成DAO操作

6.2.2 完成Service业务

6.2.4 接⼝测试

6.3 前端跨域访问

6.3.1 跨域访问概念

 6.3.2 如何解决跨域访问?

6.4 前端⻚⾯之间的传值

6.4.1 cookie

6.4.2 localStorage

七、前后端分离⽤户认证-JWT

7.1 基于session实现单体项⽬⽤户认证

7.2 基于token实现前后端分离⽤户认证

7.3 基于token的⽤户认证的实现

7.3.1 登录认证接⼝⽣成token

7.3.2 登录⻚⾯接收到token存储到cookie

7.3.3 购物⻋⻚⾯加载时访问购物⻋列表接⼝

7.3.4 在购物⻋列表接⼝校验token

7.4 JWT

7.4.1 JWT简介

7.4.2 ⽣成JWT

7.4.3 JWT校验

7.4.4 拦截器校验Token

7.5 请求头传递token

7.5.1 axios通过请求头传值

7.5.2 在拦截器中放⾏options请求

⼋、⾸⻚—轮播图

8.1 实现流程分析

8.2 完成后台接⼝开发

8.2.1 数据库操作实现

8.2.2 业务层实现

8.2.3 控制层实现

8.3 完成前端功能

九、⾸⻚-分类列表

9.1 实现流程分析

9.2 接⼝开发

9.2.1 数据库操作实现

​​​​​​​9.2.2 业务层实现

​​​​​​​9.2.3 控制层实现

9.3 前端功能实现

⼗、⾸⻚-商品推荐

10.1 流程分析

10.2 接⼝开发

10.2.1 数据库实现

10.2.2 业务层实现

10.2.3 控制层实现

10.3 前端实现

⼗⼀、⾸⻚-分类商品推荐

11.1 流程分析

11.2 接⼝实现

11.2.1 数据库实现

11.2.2 业务层实现

11.2.3 控制层实现

11.3 前端实现

⼗⼆、商品详情展示—显示商品基本信息

12.1 流程分析

12.2 商品基础信息-接⼝实现

12.3 商品基础信息-前端显示

⼗三、商品详情展示—显示商品参数信息

13.2 前端显示商品参数

13.3 前端显示商品细节

⼗四、商品详情展示—显示商品评论信息

14.1 接⼝实现

14.1.1 数据库实现

14.1.2 业务层实现

14.1.3 控制层实现

14.2 前端评论内容显示

⼗五、商品详情展示—商品评论分⻚及统计信息

15.1 流程分析

15.2 接⼝开发

15.2.1 改造商品评论列表接⼝

15.2.2 评价统计接⼝实现

15.3 前端实现

15.3.1 商品评论的分⻚

15.3.2 商品评价统计

⼗六、购物⻋—添加购物⻋(登陆状态)

16.1 流程分析  

16.2 接⼝实现

16.2.1 修改购物⻋数据表结构

16.2.2 数据库实现

16.2.3 业务层实现

16.3 前端实现

16.3.1 记录选择的套餐属性

16.3.2 套餐属性选中效果

16.3.3 修改商品数量

16.3.4 提交购物⻋

⼗七、购物⻋—添加购物⻋(未登录状态)

17.1 流程分析

17.2 功能实现

17.2.1 定义新的状态码

17.2.2 在详情⻚⾯判断如果⽤户未登录,则跳转到登录⻚⾯

17.2.3 登录⻚⾯接收回跳信息

17.2.4 回到详情⻚时接收参数

17.2.5 使⽤layui添加购物⻋成功/失败进⾏提示

⼗⼋、购物⻋—购物⻋列表

18.1 流程分析

18.2 接⼝实现

18.2.1 数据库实现

18.2.2 业务层实现

18.2.3 控制层实现

18.3 前端实现

18.3.1 显示购物⻋列表

18.3.2 显示购物⻋中商品价格

⼗九、购物⻋-修改购物⻋数量

19.1 流程分析

19.2 接⼝实现

19.3 前端实现

⼆⼗、购物⻋—结算、提交订单

20.1 流程图

20.2 接⼝实现

20.2.1 收货地址列表接⼝

20.2.2 购物⻋记录列表接⼝

20.2.3 保存订单

20.3 前端实现

20.3.1 选择购物⻋记录价格联动

20.3.2 点击“结算”跳转到订单添加⻚⾯

20.3.3 显示收货地址及订单商品

20.3.4 订单确认⻚⾯选择地址

⼆⼗⼀、订单提交及⽀付

21.1 流程分析

21.2 订单添加接⼝实现

21.2.1 数据库操作

21.2.2 业务层实现

21.2.3 订单添加接⼝实现

21.3 前端提交订单信息

21.4 ⽀付回调

2.4.1 创建⼀个控制器定义回调接⼝

2.4.2 设置回调URL

21.5 Ngrok实现内⽹穿透

21.5.1 注册帐号,申请隧道ID

21.5.2 下载ngrok客户端  

21.5.3 启动客户端

21.6 前端通过轮询访问获取订单⽀付状态

21.7 webSocket消息推送

21.7.1 实现流程

21.7.2 创建webSocket服务器

21.7.3 在⽀付回调接⼝,通过订单id获取session返回结果

21.7.4 前端进⼊到⽀付⻚⾯时,就建⽴websocket连接

⼆⼗⼆、订单超时取消

22.1 实现流程

​​​​​​​22.2 quartz定时任务框架使⽤

22.2.1 添加依赖

22.2.2 创建定时任务

22.2.3 在启动类开启定时任务

22.3 实现订单超时取消

22.3.1 在service⼦⼯程添加spring-boot-starter-quartz依赖

22.3.2 在api⾃动启动类添加@EnableScheduling注解

⼆⼗三、按类别查询商品

23.1 流程分析

23.2 接⼝开发

23.2.1 根据类别查询商品接⼝

23.2.2 根据类别ID查询当前类别下所有商品的品牌

23.3 前端实现

⼆⼗四、商品搜索

24.1 流程分析

24.2 接⼝实现

24.2.1 模糊查询商品信息

24.2.2 根据关键字查询对应商品的品牌

24.3 前端实现

⼆⼗五、⽤户中⼼

25.1 ⽤户中⼼登录校验

25.1.1 校验token接⼝实现

25.1.2 前端⽤户中⼼⾸⻚校验token

25.2 ⽤户中⼼的订单管理

25.2.1 流程分析

25.2.2 接⼝实现

25.2.3 前端实现

⼆⼗六、项⽬云部署

26.1 项⽬部署介绍

26.2 后端项⽬部署

26.3 前端项⽬部署(tomcat)

26.4 前端项⽬部署(Nginx)

⼆⼗七、项⽬⽇志管理

27.1 ⽇志框架的概念

27.2 ⽇志框架规范

27.3 SLF4J

27.4 slf4j-simple

27.5 log4j使⽤介绍

27.6 基于SpringBoot应⽤的logback⽇志配置

27.7 在锋迷商城项⽬进⾏⽇志配置

27.7.1 在api⼦⼯程的resources⽬录添加⽇志配置⽂件

27.7.2 在sercie实现类创建Logger对象,输⼊⽇志


⼀、《锋迷商城》项⽬介绍

1.1 项⽬背景

锋迷商城——电商平台

  • B2C 商家对客户
  • C2B2C 客户对商家对客户

1.1.1 B2C

平台运营⽅即商品的卖家 ⼩⽶商城
  • 商品
  • ⽤户

1.1.2 C2B2C

平台运营⽅不卖商品(也可以卖)
卖家是平台的⽤户
买家也是平台⽤户
  • ⽤户(店铺)
  • ⽤户(买家)
  • 服务
  • 商品

1.1.3 Java

Java 语⾔的应⽤领域很⼴,但主要应⽤于 web 领域的项⽬开发, web 项⽬类型分为两
类:
  • 企业级开发 (供企业内部使⽤的系统:企业内部的管理系统CRM\ERP、学校的教务
管理系统)
  • 互联⽹开发(提供给所有互联⽹⽤户使⽤的系统——⽤户量)—— 电商

1.2 项⽬功能

https://www.processon.com/view/link/606bde8b1e08534321fd2103

1.3 技术选型

SSM 企业开发框架 基础的开发技术

1.3.1 单体项⽬

项⽬的⻚⾯和代码都在同⼀个项⽬,项⽬开发完成之后直接部署在⼀台服务器

单体项⽬遇到的问题:⽤户对⻚⾯静态资源以及对 Java 代码的访问压⼒都会落在 Tomcat 服务
器上。

1.3.2 技术清单

  • 项⽬架构:前后端分离
  • 前端技术:vueaxios、妹⼦UIlayuibootstrap
  • 后端技术:SpringBoot+MyBatisRESTfulswagger
  • 服务器搭建:LinuxNginx

⼆、项⽬架构的演进

2.1 单体架构

  • 前后端都部署在同⼀台服务器上(前后端代码都在同⼀个应⽤中)
  • 缺点:对静态资源的访问压⼒也会落在Tomcat

2.2 前后端分离

 
  • 前后端分离:前端和后端分离开发和部署(前后端部署在不同的服务器)
  • 优点:将对静态资源的访问和对接⼝的访问进⾏分离,Tomcat服务器只负责数据服务的访问

2.3 集群与负载均衡

  • 优点:提供并发能⼒、可⽤性
2.4 分布式

  • 基于redis实现 分布式锁
  • 分布式数据库mycat
  • redis集群
  • 数据库中间件
  • 消息中间件

2.5 微服务架构

  • 微服务架构:将原来在⼀个应⽤中开发的多个模块进⾏拆分,单ᇿ开发和部署
  • 保证可⽤性、性能

三、《锋迷商城》项⽬搭建

基于 Maven 的聚合⼯程完成项⽬搭建,前端采⽤ vue+axios ,后端使⽤ SpringBoot 整合
SSM

3.1 技术储备

  • (√)SpringBoot: 实现⽆配置的SSM整合
  • (√)Maven聚合⼯程:实现模块的复⽤

3.2 创建Maven聚合⼯程

3.2.1 构建⽗⼯程fmmall

  • 创建⼀个maven⼯程、packing设置为 pom
  • ⽗⼯程继承继承 spring-boot-starter-parent
<?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>

    <!-- spring-boot-starter-parent -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.qfedu</groupId>
    <artifactId>fmmall</artifactId>
    <version>2.0.1</version>

    <packaging>pom</packaging>
</project>

3.2.2 创建common⼯程

  • 选择fmmall,右键---New---Module Maven⼯程)
  • 修改commonpom.xml,设置packing=jar
  • <?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">
     <parent>
         <artifactId>fmmall</artifactId>
         <groupId>com.qfedu</groupId>
         <version>2.0.1</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <artifactId>common</artifactId>
     <packaging>jar</packaging>
    </project>

3.2.3 创建beans⼯程

  • 选择fmmall,右键---New---Module Maven⼯程)
  • 修改beanspom.xml,设置packing ----- jar

3.2.4 创建mapper⼯程

  • 选择fmmall,右键---New---Module Maven⼯程)
  • 修改mapperpom.xml,设置packing ----- jar
  • mapperpom.xml,依赖beans
<dependency>
 <groupId>com.qfedu</groupId>
 <artifactId>beans</artifactId>
 <version>2.0.1</version>
</dependency>

3.2.5 创建service⼯程

  • 选择fmmall,右键---New---Module Maven⼯程)
  • 修改servicepom.xml,设置packing ----- jar
  • servicepom.xml,依赖mappercommom
<dependency>
 <groupId>com.qfedu</groupId>
 <artifactId>mapper</artifactId>
 <version>2.0.1</version>
</dependency> <dependency>
 <groupId>com.qfedu</groupId>
 <artifactId>common</artifactId>
 <version>2.0.1</version>
</dependency>

3.2.6 创建 api ⼯程
  • 选择fmmall,右键---New---Module SpringBoot⼯程)
  • 修改apipom.xml,继承fmmall,删除⾃⼰的groupId version
<parent>
 <groupId>com.qfedu</groupId>
 <artifactId>fmmall</artifactId>
 <version>2.0.1</version>
</parent>
  • spring boot的依赖配置到⽗⼯程fmmallpom.xml
  • 在⽗⼯程fmmallpom.xmlmodules添加api
<?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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.qfedu</groupId>
    <artifactId>fmmall</artifactId>
    <version>2.0.1</version>

    <packaging>pom</packaging>
    <modules>
        <module>beans</module>
        <module>common</module>
        <module>mapper</module>
        <module>service</module>
        <module>api</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.4</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
  • api中,依赖service
<dependency>
 <groupId>com.qfedu</groupId>
 <artifactId>service</artifactId>
 <version>2.0.1</version>
</dependency>
  • apipom.xml继承fmmall

3.3 Maven聚合⼯程依赖分析

如果将依赖添加到⽗⼯程的 pom 中,根据依赖的继承关系,所有的⼦⼯程中都会继承⽗
⼯程的依赖:
  • 好处:当有多个⼦⼯程都需要相同的依赖时,⽆需在⼦⼯程中重复添加依赖
  • 缺点:如果某些⼦⼯程不需要这个依赖,还是会被强⾏继承
如果在⽗⼯程中没有添加统⼀依赖,则每个⼦⼯程所需的依赖需要在⼦⼯程的 pom 中⾃
⾏添加
如果存在多个⼦⼯程需要添加相同的依赖,则需在⽗⼯程 pom 进⾏依赖版本的管理
依赖配置说明
1. 在⽗⼯程的 pom ⽂件中⼀次性添加各个⼦⼯程所需的所有依赖
2. 在各个⼦⼯程中单ᇿ添加当前⼦⼯程的依赖

3.4 整合MyBatis

3.4.1 common⼦⼯程

  • lombok

3.4.2 beans⼦⼯程

  • lombok

3.4.3 MyBatis整合

  • mapper⼦⼯程pom⽂件,新增mybatis所需的依赖
<!--mysql-->
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.47</version>
</dependency>
<!--spring-boot-starter-->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter</artifactId>
 <version>2.4.4</version>
</dependency>
<!--mybatis starter-->
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>2.1.4</version>
</dependency>
  • mapper⼦⼯程resources ⽬录创建 application.yml
spring:
 datasource:
 driver-class-name: com.mysql.jdbc.Driver
 url: jdbc:mysql://localhost:3306/db_2010_mybatis?characterEncoding=utf-8
 username: root
 password: admin123
mybatis:
 mapper-locations: classpath:mappers/*Mapper.xml
 type-aliases-package: com.qfedu.fmmall.entity
  • api⼦⼯程的启动类通过 @MpperScan 声明dao包的路径
@SpringBootApplication
@MapperScan("com.qfedu.fmmall.dao")
public class ApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }
}

3.5 基于SpringBoot的单元测试(API模块)

3.5.1 添加依赖

<!--test starter-->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
</dependency> 
<dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <scope>test</scope>
</dependency>

3.5.2 测试类

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApiApplication.class)
public class UserDAOTest {
 @Resource
 private UserDAO userDAO;
 @Test
 public void queryUserByName() {
     User user = userDAO.queryUserByName("Lucy");
     System.out.println(user);
 }
}

3.6 整合Druid

3.6.1 添加依赖

  • mapper⼦⼯程添加druid-starter
<!--druid starter-->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid-spring-boot-starter</artifactId>
 <version>1.1.22</version>
</dependency>

3.6.2 修改数据源配置

  • 修改mapper⼦⼯程application.yml⽂件
spring:
 datasource:
 druid:
 driver-class-name: com.mysql.jdbc.Driver
 url: jdbc:mysql://localhost:3306/db_2010_mybatis?
characterEncoding=utf-8
 username: root
 password: admin123
mybatis:
 mapper-locations: classpath:mappers/*Mapper.xml
 type-aliases-package: com.qfedu.fmmall.entity

四、《锋迷商城》数据库设计

4.1 软件开发步骤

  • 问题定义/提出问题
  • 可⾏性分析(技术、成本、法律法规)
  • 需求分析(需求采集、需求分析)---->甲⽅
  • 概要设计
    • 架构设计(技术选型、架构模式、项⽬搭建)
    • 数据库设计
    • UI设计
    • 业务流程设计
  • 详细设计
    • 实现步骤(业务流程的实现细节)
  • 编码
    • 根据设计好的实现步骤进⾏代码实现
    • 开发过程中开发者要进⾏单元测试
  • 测试
    • 集成测试
    • 功能测试(⿊盒)
    • 性能测试(⽩盒)
  • 交付/部署实施

4.2 数据库设计流程

  • 根据项⽬功能分析数据实体(数据实体,就是应⽤系统中要存储的数据对象)
    • 商品、订单、购物⻋、⽤户、评价、地址...
  • 提取数据实体的数据项(数据对象的属性)
    • 商品(商品id、商品名称、商品描述,特征)
    • 地址(姓名、地址、电话...)
  • 使⽤数据库设计三范式检查数据项是否合理
  • 分析实体关系:E-R
  • 数据库建模(三线图)、建模⼯具
  • 建库建表-SQL

4.3 数据库设计分析

4.3.1 PDMan建模⼯具使⽤

  • 可视化创建数据表(数据表)
  • 视图显示数据表之间的关系(关系图)
  • 导出SQL指令(模型--导出DDL脚本)
  • 记录数据设计的版本-数据库模型版本的管理(模型版本)
  • 同步数据模型到数据库(开始-数据库连接)

4.3.2 分析《锋迷商城》的数据库模型

  • ⽤户
  • ⾸⻚
  • 商品
  • 购物⻋
  • 订单 和 订单项
  • 评论

4.4 SPU SKU

4.4.1 SPU

SPU(Standard Product Unit) :标准化产品单元。是商品信息聚合的最⼩单位,是⼀组
可复⽤、易检索的标准化信息的集合,该集合描述了⼀个产品的特性。通俗点讲,属性
值、特性相同的商品就可以称为⼀个 SPU
1 荣耀 8
2 ⼩⽶ 10

4.4.2 SKU

SKU (中⽂译为 最⼩存货单位 ,英⽂全称为 Stock Keeping Unit, 简称 SKU ,定义为保存
库存控制的最⼩可⽤单位 )
101 8G / 128G 10 1800 1
102 4G / 128G 20 1500 1
103 8G / 128G 12 2999 2
104 12G / 256G 11 3999 2

4.5 建库建表

4.5.1 创建数据表

  • PDMan导出sql,导⼊到mysql

4.5.2 准备测试数据

  • ⾸⻚轮播图 index_img

  • ⾸⻚类别信息 category

  • 商品信息
  • sku

五、《锋迷商城》业务流程设计-接⼝规范

在企业项⽬开发中,当完成项⽬的需求分析、功能分析、数据库分析与设计之后,项⽬
组就会按照项⽬中的功能进⾏开发任务的分配

5.1 前后端分离与单体架构流程实现的区别

单体架构:⻚⾯和控制之间可以进⾏跳转,同步请求控制器,流程控制由的控制来完成
前后端分离架构:前端和后端分离开发和部署,前端只能通过异步向后端发送请求,后
端只负责接收请求及参数、处理请求、返回处理结果,但是后端并不负责流程控制,流
程控制是由前端完成

5.1.1 单体架构

5.1.2 前后端分离架构

5.2 接⼝介绍

5.2.1 接⼝概念

狭义的理解:就是控制器中可以接受⽤户请求的某个⽅法

应⽤程序编程接⼝,简称 API Application Programming Interface ),就是 软件 系统
不同 组成 部分衔接的约定

5.2.2 接⼝规范

作为⼀个后端开发者,我们不仅要完成接⼝程序的开发,还要编写接⼝的说明⽂档 ——
接⼝规范
接⼝规范示例
参考: 《锋迷商城》后端接⼝说明

5.3 Swagger

前后端分离开发,后端需要编写接⼝说明⽂档,会耗费⽐较多的时间
swagger 是⼀个⽤于⽣成服务器接⼝的规范性⽂档、并且能够对接⼝进⾏测试的⼯具

5.3.1 作⽤

  • ⽣成接⼝说明⽂档
  • 对接⼝进⾏测试

5.3.2 Swagger整合

  • api⼦⼯程添加依赖(Swagger2 \ Swagger UI
<dependency>
 <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger2</artifactId>
 <version>2.9.2</version>
</dependency> <dependency>
 <groupId>io.springfox</groupId>
 <artifactId>springfox-swagger-ui</artifactId>
 <version>2.9.2</version>
</dependency>
  • api⼦⼯程创建swagger的配置(Java配置⽅式)
package com.qfedu.fmmall.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    /*swagger会帮助我们⽣成接⼝⽂档
     * 1:配置⽣成的⽂档信息
     * 2: 配置⽣成规则*/
    /*Docket封装接⼝⽂档信息*/
    @Bean
    public Docket getDocket(){
        //创建封面信息对象
        ApiInfoBuilder apiInfoBuilder = new ApiInfoBuilder();
        apiInfoBuilder.title("《峰迷商城》后端接口说明")
                .description("此⽂档详细说明了锋迷商城项⽬后端接⼝规\n" +
                        "范....")
                .version("V 2.0.1")
                .contact(new Contact("龙哥","www.longge.com","longge@liu.com"));
        ApiInfo apiInfo = apiInfoBuilder.build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)//指定生成的文档中的封面信息:文档标题、版本、作者
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.qfedu.fmmall.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}
测试:
启动 SpringBoot 应⽤,访问: http://localhost:8080/swagger-ui.html

5.3.3 Swagger注解说明

swagger 提供了⼀套注解,可以对每个接⼝进⾏详细说明
@Api 类注解,在控制器类添加此注解,可以对控制器类进⾏功能说明
@Api(value = "提供商品添加、修改、删除及查询的相关接⼝",tags = "商品管理")
@ApiOperation ⽅法注解:说明接⼝⽅法的作⽤
@ApiImplicitParams @ApiImplicitParam ⽅法注解,说名接⼝⽅法的参数
@ApiOperation("⽤户登录接⼝")
@ApiImplicitParams({
 @ApiImplicitParam(dataType = "string",name = "username", value = "⽤户登录账号",required = true),
 @ApiImplicitParam(dataType = "string",name = "password", value = "⽤户登录密码",required = false,defaultValue = "111111")
})
@RequestMapping(value = "/login",method = RequestMethod.GET)
public ResultVO login(@RequestParam("username") String name,
 @RequestParam(value = "password",defaultValue =
"111111") String pwd){
 return userService.checkLogin(name,pwd);
}
@ApiModel @ApiModelProperty 当接⼝参数和返回值为对象类型时,在实体类中添加注解
说明
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "User对象",description = "⽤户/买家信息")
public class User {
 @ApiModelProperty(dataType = "int",required = false)
 private int userId;
 @ApiModelProperty(dataType = "String",required = true, value = "⽤
户注册账号")
 private String userName;
 @ApiModelProperty(dataType = "String",required = true, value = "⽤
户注册密码")
 private String userPwd;
 @ApiModelProperty(dataType = "String",required = true, value = "⽤
户真实姓名")
 private String userRealname;
 @ApiModelProperty(dataType = "String",required = true, value = "⽤
户头像url")
 private String userImg; }
@ApiIgnore 接⼝⽅法注解,添加此注解的⽅法将不会⽣成到接⼝⽂档中

5.3.4 Swagger-ui 插件

  • 导⼊插件的依赖
<dependency>
 <groupId>com.github.xiaoymin</groupId>
 <artifactId>swagger-bootstrap-ui</artifactId>
 <version>1.9.6</version>
</dependency>
  • ⽂档访问
        http://ip:port/doc.html

5.4 RESTful

前后端分离开发的项⽬中,前后端之间是接⼝进⾏请求和响应,后端向前端提供请求时
就要对外暴露⼀个 URL URL 的设计不能是随意的,需要遵从⼀定的设计规范 ——
RESTful
RESTful 是⼀种 Web api 的标准,也就是⼀种 url 设计⻛格 / 规范
  • 每个URL请求路径代表服务器上的唯⼀资源
传统的URL设计:
 http://localhost:8080/goods/delete?goodsId=1 商品1
 http://localhost:8080/goods/delete?goodsId=2 商品2
RESTful设计:
 http://localhost:8080/goods/delete/1 商品1
 http://localhost:8080/goods/delete/2 商品2
@RequestMapping("/delete/{gid}")
public ResultVO deleteGoods(@PathVariable("gid") int goodsId){
 System.out.println("-----"+goodsId);
 return new ResultVO(10000,"delete success",null);
}
  • 使⽤不同的请求⽅式表示不同的操作
SpringMVC RESTful ⻛格提供了很好的⽀持,在我们定义⼀个接⼝的 URL 时,可以
通过 @RequestMapping(value="/{id}",method=RequestMethod.GET) 形式指定请
求⽅式,也可使⽤特定请求⽅式的注解设定 URL
@PostMapping("/add")
@DeleteMapping("/{id}")
@PutMapping("/{id}")
@GetMapping("/{id}")
  • post 添加
  • get 查询
  • put 修改
  • delete 删除
  • option (预检)
根据ID删除⼀个商品:
//http://localhost:8080/goods/1 [delete]
@RequestMapping(value = "/{id}",method = RequestMethod.DELETE)
public ResultVO deleteGoods(@PathVariable("id") int goodsId){
 System.out.println("-----"+goodsId);
 return new ResultVO(10000,"delete success",null);
}
根据ID查询⼀个商品:
//http://localhost:8080/goods/1 [get]
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public ResultVO getGoods(@PathVariable("id") int goodsId){
 return null; 
}
  • 接⼝响应的资源的表现形式采⽤JSON(或者XML
  • 在控制类或者每个接⼝⽅法添加 @ResponseBody 注解将返回的对象格式为json
  • 或者直接在控制器类使⽤ @RestController 注解声明控制器
  • 前端(Android\ios\pc)通过⽆状态的HTTP协议与后端接⼝进⾏交互

六、《锋迷商城》设计及实现⽤户管理

6.1 实现流程

6.2 后端接⼝开发

6.2.1 完成DAO操作

1. 创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "User对象",description = "⽤户/买家信息")
public class User {
 
 private int userId;
 private String username;
 private String password;
 private String nickname;
 private String realname;
 private String userImg;
 private String userMobile;
 private String userEmail;
 private String userSex;
 private Date userBirth;
 private Date userRegtime;
 private Date userModtime; 
}
2. 创建 DAO 接⼝、定义操作⽅法
public interface UserDAO {
 //⽤户注册
 public int insert(User user);
 
 //根据⽤户名查询⽤户信息
 public User query(String name);
}
3. 创建 DAO 接⼝的 mapper ⽂件并完成配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.fmmall.dao.UserDAO">
 <insert id="insertUser">
 insert into
users(username,password,user_regtime,user_modtime)
 values(#{username},#{password},#{userRegtime},#
{userModtime})
 </insert>
 <resultMap id="userMap" type="User">
 <id column="user_id" property="userId"/>
 <result column="username" property="username"/>
 <result column="password" property="password"/>
 <result column="nickname" property="nickname"/>
 <result column="realname" property="realname"/>
 <result column="user_img" property="userImg"/>
 <result column="user_mobile" property="userMobile"/>
 <result column="user_email" property="userEmail"/>
 <result column="user_sex" property="userSex"/>
 <result column="user_birth" property="userBirth"/>
 <result column="user_regtime" property="userRegtime"/>
 <result column="user_modtime" property="userModtime"/>
 </resultMap>
 <select id="queryUserByName" resultMap="userMap">
 select
 user_id,
 username,
 password,
 nickname,
 realname,
 user_img,
 user_mobile,
 user_email,
 user_sex,
 user_birth,
 user_regtime,
 user_modtime
 from users
 where username=#{name}
 </select>
</mapper>

6.2.2 完成Service业务

1. 创建 service 接⼝
public interface UserService {
 //⽤户注册
 public ResultVO userResgit(String name, String pwd);
 //⽤户登录
 public ResultVO checkLogin(String name, String pwd);
}
2. 创建 service 接⼝实现类,完成业务实现
@Service
public class UserServiceImpl implements UserService {
 @Autowired
 private UserDAO userDAO;
 @Transactional
 public ResultVO userResgit(String name, String pwd) {
 synchronized (this) {
 //1.根据⽤户查询,这个⽤户是否已经被注册
 User user = userDAO.queryUserByName(name);
 //2.如果没有被注册则进⾏保存操作
 if (user == null) {
 String md5Pwd = MD5Utils.md5(pwd);
 user = new User();
 user.setUsername(name);
 user.setPassword(md5Pwd);
 user.setUserRegtime(new Date());
 user.setUserModtime(new Date());
 int i = userDAO.insertUser(user);
 if (i > 0) {
 return new ResultVO(10000, "注册成功!", null);
 } else {
 return new ResultVO(10002, "注册失败!", null);
 }
 } else {
 return new ResultVO(10001, "⽤户名已经被注册!",
null);
 }
 }
 }
 @Override
 public ResultVO checkLogin(String name, String pwd) {
 User user = userDAO.queryUserByName(name);
 if(user == null){
 return new ResultVO(10001,"登录失败,⽤户名不存在!",null);
 }else{
 String md5Pwd = MD5Utils.md5(pwd);
 if(md5Pwd.equals(user.getPassword())){
 return new ResultVO(10000,"登录成功!",user);
 }else{
 return new ResultVO(10001,"登录失败,密码错误!",null);
 }
 }
 }
}

6.2.3 完成Controller提供接⼝

1. 创建 controller ,调⽤ service
2. 添加接⼝注解
@RestController
@RequestMapping("/user")
@Api(value = "提供⽤户的登录和注册接⼝",tags = "⽤户管理")
public class UserController {
 @Resource
 private UserService userService;
 @ApiOperation("⽤户登录接⼝")
 @ApiImplicitParams({
 @ApiImplicitParam(dataType = "string",name =
    "username", value = "⽤户登录账号",required = true),
 @ApiImplicitParam(dataType = "string",name =
    "password", value = "⽤户登录密码",required = true)
 })
 @GetMapping("/login")
 public ResultVO login(@RequestParam("username") String name,
 @RequestParam(value = "password") Stringpwd){
     ResultVO resultVO = userService.checkLogin(name, pwd);
     return resultVO;
 }
 @ApiOperation("⽤户注册接⼝")
 @ApiImplicitParams({
 @ApiImplicitParam(dataType = "string",name =
    "username", value = "⽤户注册账号",required = true),
 @ApiImplicitParam(dataType = "string",name =
    "password", value = "⽤户注册密码",required = true)
 })
 @PostMapping("/regist")
 public ResultVO regist(String username,String password){
     ResultVO resultVO = userService.userResgit(username,password);
     return resultVO;
 }
}

6.2.4 接⼝测试

基于 swagger 进⾏测试

6.3 前端跨域访问

6.3.1 跨域访问概念

  • 什么时跨域访问?
AJAX 跨域访问是⽤户访问 A ⽹站时所产⽣的对 B ⽹站的跨域访问请求均 提交 A ⽹站
的指定⻚⾯

 6.3.2 如何解决跨域访问?

  • 前端使⽤JSONP设置
  • 后端使⽤ @CrossOrigin --- 就是设置响应头允许跨域

6.4 前端⻚⾯之间的传值

6.4.1 cookie

  • ⼯具⽅法封装:
var operator = "=";
function getCookieValue(keyStr){
    var value = null;
     var s = window.document.cookie;
     var arr = s.split("; ");
     for(var i=0; i<arr.length; i++){
         var str = arr[i];
         var k = str.split(operator)[0];
         var v = str.split(operator)[1];
         if(k == keyStr){
             value = v;
             break;
         }
     }
     return value; 
}
function setCookieValue(key,value){
 document.cookie = key+operator+value; 
}
  • A⻚⾯
setCookieValue("username",userInfo.username);
setCookieValue("userimg",userInfo.userImg);
  • B⻚⾯
var name = getCookieValue("username");
var img = getCookieValue("userimg");

6.4.2 localStorage

  • A⻚⾯
localStorage.setItem("user",JSON.stringify(userInfo));
  • B⻚⾯
var jsonStr = localStorage.getItem("user");
var userInfo = eval("("+jsonStr+")");
//移出localStorage键值对
localStorage.removeItem("user");

七、前后端分离⽤户认证-JWT

7.1 基于session实现单体项⽬⽤户认证

在单体项⽬中如何保证受限资源在⽤户未登录的情况下不允许访问?
在单体项⽬中,视图资源(⻚⾯)和接⼝(控制器)都在同⼀台服务器,⽤户的多次请
求都是基于同⼀个会话( session ),因此可以借助 session 来进⾏⽤户认证判断:
1. 当⽤户登录成功之后,将⽤户信息存放到 session
2. 当⽤户再次访问受限资源时,验证 session 中是否存在⽤户信息,可以根据 session
⽆⽤户信息来判断⽤户是否登录

7.2 基于token实现前后端分离⽤户认证

由于在前后端分离项⽬开发中,前后端之间是通过异步交互完成数据访问的,请求是⽆
状态的,因此不能基于 session 实现⽤户的认证。

7.3 基于token的⽤户认证的实现

7.3.1 登录认证接⼝⽣成token

// UserController
@GetMapping("/login")
public ResultVO login(@RequestParam("username") String name,
 @RequestParam(value = "password") String pwd){
 ResultVO resultVO = userService.checkLogin(name, pwd);
 return resultVO;
 }
// UserServiceImpl
public ResultVO checkLogin(String name, String pwd) {
 Example example = new Example(Users.class);
 Example.Criteria criteria = example.createCriteria();
 criteria.andEqualTo("username", name);
 List<Users> users = usersMapper.selectByExample(example);
 if(users.size() == 0){
 return new ResultVO(ResStatus.NO,"登录失败,⽤户名不存在!",null);
 }else{
 String md5Pwd = MD5Utils.md5(pwd);
 if(md5Pwd.equals(users.get(0).getPassword())){
 //如果登录验证成功,则需要⽣成令牌token(token就是按照特定规则⽣成的
字符串)
 String token = Base64Utils.encode(name+"QIANfeng6666");
 return new ResultVO(ResStatus.OK,token,users.get(0));
 }else{
 return new ResultVO(ResStatus.NO,"登录失败,密码错误!",null);
 }
 }
}

7.3.2 登录⻚⾯接收到token存储到cookie

// login.html
doSubmit:function(){
 if(vm.isRight){
 var url = baseUrl+"user/login";
 axios.get(url,{
 params:{
 username:vm.username,
 password:vm.password
 }
 }).then((res)=>{
 var vo = res.data;
 if(vo.code == 10000){
 //如果登录成功,就把token存储到cookie
 setCookieValue("token",vo.msg);
 window.location.href = "index.html";
 }else{
 vm.tips = "登录失败,账号或密码错误!";
 }
 });
 }else{
 vm.tips = "请正确输⼊帐号和密码!";
 }
}

7.3.3 购物⻋⻚⾯加载时访问购物⻋列表接⼝

  • 获取token
  • 携带token访问接⼝
// login.html
doSubmit:function(){
 if(vm.isRight){
 var url = baseUrl+"user/login";
 axios.get(url,{
 params:{
 username:vm.username,
 password:vm.password
 }
 }).then((res)=>{
 var vo = res.data;
 if(vo.code == 10000){
 //如果登录成功,就把token存储到cookie
 setCookieValue("token",vo.msg);
 window.location.href = "index.html";
 }else{
 vm.tips = "登录失败,账号或密码错误!";
 }
 });
 }else{
 vm.tips = "请正确输⼊帐号和密码!";
 }
}

7.3.4 在购物⻋列表接⼝校验token

@GetMapping("/list")
@ApiImplicitParam(dataType = "string",name = "token", value = "授权令
牌",required = true)
public ResultVO listCarts(String token){
 //1.获取token
 //2.校验token
 if(token == null){
 return new ResultVO(ResStatus.NO,"请先登录",null);
 }else{
 String decode = Base64Utils.decode(token);
 if(decode.endsWith("QIANfeng6666")){
 //token校验成功
 return new ResultVO(ResStatus.OK,"success",null);
 }else{
 return new ResultVO(ResStatus.NO,"登录过期,请重新登
录!",null);
 }
 }
} 

7.4 JWT

如果按照上述规则⽣成 token
1. 简易的 token ⽣成规则安全性较差,如果要⽣成安全性很⾼的 token 对加密算法要求较
⾼;
2. ⽆法完成时效性的校验(登录过期)

7.4.1 JWT简介

  • JWT Json Web Token
  • 官⽹:https://jwt.io
  • jwt的结构

 

7.4.2 ⽣成JWT

  • 添加依赖
<dependency>
<groupId> com.auth0 </groupId>
<artifactId> java-jwt </artifactId>
<version> 3.10.3 </version>
</dependency>
<dependency>
<groupId> io.jsonwebtoken </groupId>
<artifactId> jjwt </artifactId>
<version> 0.9.1 </version>
</dependency>
  • ⽣成token
String token = builder.setSubject(name) //主题,就是
token中携带的数据
 .setIssuedAt(new Date()) //设置token的
⽣成时间
 .setId(users.get(0).getUserId() + "") //设置⽤户id为
token id
 .setClaims(map) //map中可以存
放⽤户的⻆⾊权限信息
 .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 *
60 * 1000)) //设置过期时间
 .signWith(SignatureAlgorithm.HS256, "QIANfeng6666") //设置加密
⽅式和加密密码
 .compact();

7.4.3 JWT校验

如果 token 正确则正常解析,如果 token 不正确或者过期,则通过抛出的异常进⾏识别
try {
 //验证token
 JwtParser parser = Jwts.parser();
 parser.setSigningKey("QIANfeng6666"); //解析token的SigningKey必须
和⽣成token时设置密码⼀致
 //如果token正确(密码正确,有效期内)则正常执⾏,否则抛出异常
 Jws<Claims> claimsJws = parser.parseClaimsJws(token);
 Claims body = claimsJws.getBody(); //获取token中⽤户数据
 String subject = body.getSubject(); //获取⽣成token设置的subject
 String v1 = body.get("key1", String.class); //获取⽣成token时存储
的Claims的map中的值
 return new ResultVO(ResStatus.OK,"success",null);
}catch (ExpiredJwtException e){
 return new ResultVO(ResStatus.NO,"登录过期,请重新登录!",null);
}catch (UnsupportedJwtException e){
 return new ResultVO(ResStatus.NO,"Tonken不合法,请⾃重!",null);
}catch (Exception e){
 return new ResultVO(ResStatus.NO,"请重新登录!",null);
}

7.4.4 拦截器校验Token

  • 创建拦截器
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
 String token = request.getParameter("token");
 if(token == null){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登
录!", null);
 //提示请先登录
 doResponse(response,resultVO);
 }else{
 try {
 //验证token
 JwtParser parser = Jwts.parser();
 //解析token的SigningKey必须和⽣成token时设置密码⼀致
 parser.setSigningKey("QIANfeng6666");
 //如果token正确(密码正确,有效期内)则正常执⾏,否则抛出异
常
 Jws<Claims> claimsJws =
parser.parseClaimsJws(token);
 return true;
 }catch (ExpiredJwtException e){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "登录
过期,请重新登录!", null);
 doResponse(response,resultVO);
 }catch (UnsupportedJwtException e){
 ResultVO resultVO = new ResultVO(ResStatus.NO,
"Token不合法,请⾃重!", null);
 doResponse(response,resultVO);
 }catch (Exception e){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "请先
登录!", null);
 doResponse(response,resultVO);
 }
 }
 return false;
 }
 private void doResponse(HttpServletResponse response,ResultVO
resultVO) throws IOException {
 response.setContentType("application/json");
 response.setCharacterEncoding("utf-8");
 PrintWriter out = response.getWriter();
 String s = new ObjectMapper().writeValueAsString(resultVO);
 out.print(s);
 out.flush();
 out.close();
 }
}
  • 配置拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 @Autowired
 private CheckTokenInterceptor checkTokenInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 registry.addInterceptor(checkTokenInterceptor)
 .addPathPatterns("/**")
 .excludePathPatterns("/user/**");
 }
}

7.5 请求头传递token

前端但凡访问受限资源,都必须携带 token 发送请求; token 可以通过请求⾏ (params)
请求头( header )以及请求体( data )传递,但是习惯性使⽤ header 传递

7.5.1 axios通过请求头传值

axios({
 method:"get",
 url:baseUrl+"shopcart/list",
 headers:{
 token:this.token
 }
}).then(function(res){
 console.log(res);
});

7.5.2 在拦截器中放⾏options请求

@Component
public class CheckTokenInterceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
 //放⾏options请求
 String method = request.getMethod();
 if("OPTIONS".equalsIgnoreCase(method)){
 return true;
 }
 String token = request.getHeader("token");
 System.out.println("-------------"+token);
 if(token == null){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录!",
null);
 //提示请先登录
 doResponse(response,resultVO);
 }else{
 try {
 //验证token
 JwtParser parser = Jwts.parser();
 //解析token的SigningKey必须和⽣成token时设置密码⼀致
 parser.setSigningKey("QIANfeng6666");
 //如果token正确(密码正确,有效期内)则正常执⾏,否则抛出异常
 Jws<Claims> claimsJws = parser.parseClaimsJws(token);
 return true;
 }catch (ExpiredJwtException e){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "登录过
期,请重新登录!", null);
 doResponse(response,resultVO);
 }catch (UnsupportedJwtException e){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "Token不
合法,请⾃重!", null);
 doResponse(response,resultVO);
 }catch (Exception e){
 ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登
录!", null);
 doResponse(response,resultVO);
 }
 }
 return false;
 }
 private void doResponse(HttpServletResponse response,ResultVO
resultVO) throws IOException {
 response.setContentType("application/json");
 response.setCharacterEncoding("utf-8");
 PrintWriter out = response.getWriter();
 String s = new ObjectMapper().writeValueAsString(resultVO);
 out.print(s);
 out.flush();
 out.close();
 }
}

⼋、⾸⻚轮播图

8.1 实现流程分析

  • 流程图

 

  • 接⼝
    • ​​​​​​​查询轮播图信息返回

8.2 完成后台接⼝开发

8.2.1 数据库操作实现

  • 分析数据表结构

  • 添加测试数据

  • 编写sql语句
select img_id,
 img_url,
 img_bg_color,
 prod_id,
 category_id,
 index_type,
 seq,
 status,
 create_time,
 update_time
 from index_img
 where status=1
 order by seq
  • Mapper接⼝(DAO)中定义操作⽅法
public interface IndexImgMapper extends GeneralDAO<IndexImg> {
 //1.查询轮播图信息: 查询status=1 且 按照seq进⾏排序
 public List<IndexImg> listIndexImgs();
}
  • 配置映射⽂件
<!--BaseResultMap是由逆向⼯程⽣成的-->
<select id="listIndexImgs" resultMap="BaseResultMap">
 select img_id,
 img_url,
 img_bg_color,
 prod_id,
 category_id,
 index_type,
 seq,
 status,
 create_time,
 update_time
 from index_img
 where status=1
 order by seq
 </select>

8.2.2 业务层实现

  • IndexImgService接⼝
public interface IndexImgService {
 public ResultVO listIndexImgs();
}
  • IndexImgServiceImpl实现类
@Service
public class IndexImgServiceImpl implements IndexImgService {
 @Autowired
 private IndexImgMapper indexImgMapper;
 public ResultVO listIndexImgs() {
 List<IndexImg> indexImgs = indexImgMapper.listIndexImgs();
 if(indexImgs.size()==0){
 return new ResultVO(ResStatus.NO,"fail",null);
 }else{
 return new ResultVO(ResStatus.OK,"success",indexImgs);
 }
 }
}

8.2.3 控制层实现

  • IndexController
@RestController
@CrossOrigin
@RequestMapping("/index")
@Api(value = "提供⾸⻚数据显示所需的接⼝",tags = "⾸⻚管理")
public class IndexController {
 @Autowired
 private IndexImgService indexImgService;
 @GetMapping("/indeximg")
 @ApiOperation("⾸⻚轮播图接⼝")
 public ResultVO listIndexImgs(){
 return indexImgService.listIndexImgs();
 }
}

8.3 完成前端功能

当进⼊到 index.html ,在进⾏⻚⾯初始化之后,就需要请求轮播图数据进⾏轮播图的显
index.html

 

 

九、⾸⻚-分类列表

9.1 实现流程分析

  • ⽅案⼀:⼀次性查询三级分类
    • 优点:只需要⼀次查询,根据⼀级分类显示⼆级分类时响应速度较快
    • 缺点:数据库查询效率较低,⻚⾯⾸次加载的速度也相对较慢
  • ⽅案⼆:先只查询⼀级分类,⽤户点击/⿏标移动到⼀级分类,动态加载⼆级分类 ​​​​​​​
    • 优点:数据库查询效率提⾼,⻚⾯⾸次加载速度提⾼
    • 缺点:需要多次连接数据库

9.2 接⼝开发

9.2.1 数据库操作实现

  • 数据表结构

 

  • 添加测试数据
  • 编写接⼝实现所需的SQL
    • 连接查询
select 
c1.category_id 'category_id1',
c1.category_name 'category_name1',
c1.category_level 'category_level1',
c1.parent_id 'parent_id1',
c1.category_icon 'category_icon1',
c1.category_slogan 'category_slogan1',
c1.category_pic 'category_pic1',
c1.category_bg_color 'category_bg_color1',
c2.category_id 'category_id2',
c2.category_name 'category_name2',
c2.category_level 'category_level2',
c2.parent_id 'parent_id2',
c3.category_id 'category_id3',
c3.category_name 'category_name3',
c3.category_level 'category_level3',
c3.parent_id 'parent_id3'
from category c1
inner join category c2
on c2.parent_id=c1.category_id
left join category c3
on c3.parent_id=c2.category_id
where c1.category_level=1 23456789
  • ⼦查询
-- 根据⽗级分类的id查询类别信息
select * from category where parent_id=3;
  • 创建⽤于封装查询的类别信息的CategoryVO
        在beans ⼦⼯程的 entity 包新建⼀个 CategoryVO ⽤于封装查询到类别信息,相对于 Category
        说,新增了如下属性:
public class CategoryVO {
 //⽤于存放当前分类的⼦分类
 private List<CategoryVO> categories;
 public List<CategoryVO> getCategories() {
 return categories;
 }
}
  • CategoryMapper定义操作⽅法
@Repository
public interface CategoryMapper extends GeneralDAO<Category> {
 //1.连接查询
 public List<CategoryVO> selectAllCategories();
 //2.⼦查询:根据parentId查询⼦分类
 public List<CategoryVO> selectAllCategories2(int parentId);
}
  • 映射配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.fmmall.dao.CategoryMapper">
 <resultMap id="BaseResultMap"
type="com.qfedu.fmmall.entity.Category">
 <id column="category_id" jdbcType="INTEGER"
property="categoryId" />
 <result column="category_name" jdbcType="VARCHAR"
property="categoryName" />
 <result column="category_level" jdbcType="INTEGER"
property="categoryLevel" />
 <result column="parent_id" jdbcType="INTEGER"
property="parentId" />
 <result column="category_icon" jdbcType="VARCHAR"
property="categoryIcon" />
 <result column="category_slogan" jdbcType="VARCHAR"
property="categorySlogan" />
 <result column="category_pic" jdbcType="VARCHAR"
property="categoryPic" />
 <result column="category_bg_color" jdbcType="VARCHAR"
property="categoryBgColor" />
 </resultMap>
 <resultMap id="categoryVOMap"
type="com.qfedu.fmmall.entity.CategoryVO"> 123456789
 <id column="category_id1" jdbcType="INTEGER"
property="categoryId" />
 <result column="category_name1" jdbcType="VARCHAR"
property="categoryName" />
 <result column="category_level1" jdbcType="INTEGER"
property="categoryLevel" />
 <result column="parent_id1" jdbcType="INTEGER"
property="parentId" />
 <result column="category_icon1" jdbcType="VARCHAR"
property="categoryIcon" />
 <result column="category_slogan1" jdbcType="VARCHAR"
property="categorySlogan" />
 <result column="category_pic1" jdbcType="VARCHAR"
property="categoryPic" />
 <result column="category_bg_color1" jdbcType="VARCHAR"
property="categoryBgColor" />
 <collection property="categories"
ofType="com.qfedu.fmmall.entity.CategoryVO">
 <id column="category_id2" jdbcType="INTEGER"
property="categoryId" />
 <result column="category_name2" jdbcType="VARCHAR"
property="categoryName" />
 <result column="category_level2" jdbcType="INTEGER"
property="categoryLevel" />
 <result column="parent_id2" jdbcType="INTEGER"
property="parentId" />
 <collection property="categories"
ofType="com.qfedu.fmmall.entity.CategoryVO">
 <id column="category_id3" jdbcType="INTEGER"
property="categoryId" />
 <result column="category_name3" jdbcType="VARCHAR"
property="categoryName" />
 <result column="category_level3" jdbcType="INTEGER"
property="categoryLevel" />
 <result column="parent_id3" jdbcType="INTEGER"
property="parentId" />
 </collection>
 </collection>
 
 </resultMap>
 <select id="selectAllCategories" resultMap="categoryVOMap">
 select
 c1.category_id 'category_id1',
 c1.category_name 'category_name1',
 c1.category_level 'category_level1',
 c1.parent_id 'parent_id1',
 c1.category_icon 'category_icon1',
 c1.category_slogan 'category_slogan1',
 c1.category_pic 'category_pic1',
 c1.category_bg_color 'category_bg_color1',
 c2.category_id 'category_id2',
 c2.category_name 'category_name2',
 c2.category_level 'category_level2',
 c2.parent_id 'parent_id2',
 c3.category_id 'category_id3',
 c3.category_name 'category_name3',
 c3.category_level 'category_level3',
 c3.parent_id 'parent_id3'
 from category c1
 inner join category c2
 on c2.parent_id=c1.category_id
 left join category c3
 on c3.parent_id=c2.category_id
 where c1.category_level=1
 </select>
 
 <!-------------------------------------------------------------
--------------->
 <resultMap id="categoryVOMap2"
type="com.qfedu.fmmall.entity.CategoryVO">
 <id column="category_id" jdbcType="INTEGER"
property="categoryId" />
 <result column="category_name" jdbcType="VARCHAR"
property="categoryName" />
 <result column="category_level" jdbcType="INTEGER"
property="categoryLevel" />
 <result column="parent_id" jdbcType="INTEGER"
property="parentId" />
 <result column="category_icon" jdbcType="VARCHAR"
property="categoryIcon" />
 <result column="category_slogan" jdbcType="VARCHAR"
property="categorySlogan" />
 <result column="category_pic" jdbcType="VARCHAR"
property="categoryPic" />
 <result column="category_bg_color" jdbcType="VARCHAR"
property="categoryBgColor" />
 <collection property="categories" column="category_id"
select="com.qfedu.fmmall.dao.CategoryMapper.selectAllCategories2"/>
 </resultMap>
 <!-- 根据⽗级分类的id查询⼦级分类 -->
 <select id="selectAllCategories2" resultMap="categoryVOMap2">
 select
 category_id,
 category_name,
 category_level,
 parent_id,
 category_icon,
 category_slogan,
 category_pic,
 category_bg_color
 from category
 where parent_id=#{parentId}
 </select>
</mapper>

​​​​​​​9.2.2 业务层实现

  • CategoryService接⼝
public interface CategoryService {
 public ResultVO listCategories();
}
  • CategoryServiceImpl
@Service
public class CategoryServiceImpl implements CategoryService {
 @Autowired
 private CategoryMapper categoryMapper;
 public ResultVO listCategories() {
 List<CategoryVO> categoryVOS =
categoryMapper.selectAllCategories();
 ResultVO resultVO = new ResultVO(ResStatus.OK, "success",
categoryVOS);
 return resultVO;
 }
}

​​​​​​​9.2.3 控制层实现

IndexController
@Autowired
private CategoryService categoryService;
@GetMapping("/category-list")
@ApiOperation("商品分类查询接⼝")
public ResultVO listCatetory(){
 return categoryService.listCategories();
}

9.3 前端功能实现

⼗、⾸⻚-商品推荐

10.1 流程分析

10.2 接⼝开发

10.2.1 数据库实现

商品推荐算法:推荐最新上架的商品
说明:商品推荐算法是根据多个维度进⾏权重计算,计算出⼀个匹配值
  • 数据表分析及数据准备
  • sql
-- 商品推荐:查询最新上架的商品
select * from product order by create_time desc limit 0,3;
-- ⼦查询:根据商品id查询商品图⽚
select * from product_img where item_id=2; 
  • beans⼦⼯程entity包创建ProductVO,相⽐较Product新增了List imgs⽤于存储商品的图⽚
public class ProductVO{
 private List<ProductImg> imgs;
 public List<ProductImg> getImgs() {
 return imgs;
 }
 public void setImgs(List<ProductImg> imgs) {
 this.imgs = imgs;
 }
}
  • Mapper接⼝定义操作⽅法:
        ProductMapper
public interface ProductMapper extends GeneralDAO<Product> {
 public List<ProductVO> selectRecommendProducts();
}

ProductImgMapper

public interface ProductImgMapper extends GeneralDAO<ProductImg> {
 //根据商品id查询当前商品的图⽚信息
 public List<ProductImg> selectProductImgByProductId(int
productId);
 
} 
  • 配置映射⽂件

ProductMapper.xml

<resultMap id="ProductVOMap"
type="com.qfedu.fmmall.entity.ProductVO">
 <id column="product_id" jdbcType="VARCHAR" property="productId"
/>
 <result column="product_name" jdbcType="VARCHAR"
property="productName" />
 <result column="category_id" jdbcType="INTEGER"
property="categoryId" />
 <result column="root_category_id" jdbcType="INTEGER"
property="rootCategoryId" />
 <result column="sold_num" jdbcType="INTEGER" property="soldNum"
/>
 <result column="product_status" jdbcType="INTEGER"
property="productStatus" />
 <result column="create_time" jdbcType="TIMESTAMP"
property="createTime" />
 <result column="update_time" jdbcType="TIMESTAMP"
property="updateTime" />
 <result column="content" jdbcType="LONGVARCHAR"
property="content" />
 <collection property="imgs"
select="com.qfedu.fmmall.dao.ProductImgMapper.selectProductImgByPro
ductId" column="product_id"/>
</resultMap> <select id="selectRecommendProducts" resultMap="ProductVOMap">
 select
 product_id,
 product_name,
 category_id,
 root_category_id,
 sold_num,
 product_status,
 content,
 create_time,
 update_time
 from product
 order by create_time desc
 limit 0,3
</select>
ProductImgMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.fmmall.dao.ProductImgMapper">
 <resultMap id="BaseResultMap"
type="com.qfedu.fmmall.entity.ProductImg">
 <id column="id" jdbcType="VARCHAR" property="id" />
 <result column="item_id" jdbcType="VARCHAR" property="itemId"
/>
 <result column="url" jdbcType="VARCHAR" property="url" />
 <result column="sort" jdbcType="INTEGER" property="sort" />
 <result column="is_main" jdbcType="INTEGER" property="isMain"
/>
 <result column="created_time" jdbcType="TIMESTAMP"
property="createdTime" />
 <result column="updated_time" jdbcType="TIMESTAMP"
property="updatedTime" />
 </resultMap>
 <select id="selectProductImgByProductId"
resultMap="BaseResultMap">
 select
 id,
 item_id,
 url,
 sort,
 is_main,
 created_time,
 updated_time
 from product_img
 where item_id=#{productId}
 </select>
</mapper>

10.2.2 业务层实现

  • ProductService接⼝
public interface ProductService {
 public ResultVO listRecommendProducts();
}
  • ProductServiceImpl实现类
public interface ProductService {
 public ResultVO listRecommendProducts();
}

10.2.3 控制层实现

IndexController
@Autowired
private ProductService productService;
@GetMapping("/list-recommends")
@ApiOperation("查询推荐商品接⼝")
public ResultVO listRecommendProducts() {
 return productService.listRecommendProducts();
}

10.3 前端实现

⼗⼀、⾸⻚-分类商品推荐

按照商品的分类(⼀级分类)推荐销量最⾼的 6 个商品

11.1 流程分析

加载分类商品推荐有两种实现⽅案:
⽅案⼀:当加载⾸⻚⾯时不加载分类的推荐商品,监听进度条滚动事件,当进度条触底
(滚动指定的距离)就触发分类推荐商品的加载,每次只加载⼀个分类的商品。
 
⽅案⼆:⼀次性加载所有分类的推荐商品,整体进⾏初始化。
 

11.2 接⼝实现

11.2.1 数据库实现

  • 数据准备
-- 添加商品
-- 添加⼗个分类下的商品:
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('5','商品5',10,1,122,1,'商品说明','2021-04-26 11:11:11','2021-
04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('6','商品6',10,1,123,1,'商品说明','2021-04-26 11:11:11','2021-
04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('7','商品7',10,1,124,1,'商品说明','2021-04-26 11:11:11','2021-
04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('8','商品8',10,1,125,1,'商品说明','2021-04-26 11:11:11','2021-
04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('9','商品9',10,1,126,1,'商品说明','2021-04-26 11:11:11','2021-
04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('10','商品10',10,1,127,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('11','商品11',10,1,128,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('12','商品12',46,2,122,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('13','商品13',46,2,123,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('14','商品14',46,2,124,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('15','商品15',46,2,125,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('16','商品16',46,2,126,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('17','商品17',46,2,127,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
insert into
product(product_id,product_name,category_id,root_category_id,sold_n
um,product_status,content,create_time,updated_time)
values('18','商品18',46,2,128,1,'商品说明','2021-04-26
11:11:11','2021-04-26 11:11:11');
-- 添加商品图⽚
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('9','5','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('10','6','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('11','7','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('12','8','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('13','9','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('14','10','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('15','11','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('16','12','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('17','13','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('18','14','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('19','15','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('20','16','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('21','17','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
insert into
product_img(id,item_id,url,sort,is_main,created_time,updated_time)
values('22','18','wwxb_1.png',1,1,'2021-04-26 11:11:11','2021-04-26
11:11:11');
  • 查询SQL
-- 查询所有的⼀级分类
select * from category where category_level=1;
-- 查询每个分类下销量前6的商品
select * from product where root_category_id=2 order by sold_num
desc limit 0,6;
-- 查询每个商品的图⽚
select * from product_img where item_id = 1;
  • 实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CategoryVO {
 private Integer categoryId;
 private String categoryName;
 private Integer categoryLevel;
 private Integer parentId; 
 private String categoryIcon;
 private String categorySlogan;
 private String categoryPic;
 private String categoryBgColor;
 //实现⾸⻚的类别显示
 private List<CategoryVO> categories;
 //实现⾸⻚分类商品推荐
 private List<ProductVO> products; }
  • Mapper接⼝中定义查询⽅法
CategoryMapper
 
ProductMapper
  • 映射配置

ProductMapper.xml

CategoryMapper.xml

 

11.2.2 业务层实现

11.2.3 控制层实现

11.3 前端实现

⼗⼆、商品详情展示显示商品基本信息

点击⾸⻚推荐的商品、轮播图商品⼴告、商品列表⻚⾯点击商品,就会进⼊到商品的详
情⻚⾯

12.1 流程分析

12.2 商品基础信息-接⼝实现

商品基本信息、商品套餐、商品图⽚
  • SQL
-- 根据id查询商品基本信息
select * from product where product_id=3;
-- 根据商品id查询当前商品的图⽚(√)
select * from product_img where item_id=3;
-- 根据商品id查询当前商品的套餐
select * from product_sku where product_id=3;
  • 因为上述的三个查询都是单表查询,可以通过tkmapper完成,⽆需在Mapper接⼝定义 新的⽅法
  • 业务层实现
ProductService 接⼝
ProductServiceImpl 类实现

 

控制层实现
ProductController

 

12.3 商品基础信息-前端显示

⼗三、商品详情展示显示商品参数信息

13.1 接⼝实现
根据商品 id 查询商品参数信息
  • 数据库操作直接只⽤tkMapper的默认⽅法实现
  • 业务层实现

  • 控制层实现

13.2 前端显示商品参数

13.3 前端显示商品细节

前端⻚⾯间 URL 传值
  • utils.js
function getUrlParam(key){
 var url = decodeURI( window.location.toString() );
 var arr = url.split("?");
 if(arr.length>1){
 var params = arr[1].split("&");
 for(var i=0; i<params.length; i++){
 var param = params[i]; //"pid=101"
 if(param.split("=")[0] == key ){
 return param.split("=")[1];
 }
 }
 }
 return null; }
  • a.html
<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title></title>
 </head>
 <body>
 
 <a href="b.html?pid=101&pname=咪咪虾条">跳转到B⻚⾯</a>
 
 </body>
</html>
  • b.html
<!DOCTYPE html>
<html> 
 <head>
 <meta charset="UTF-8">
 <title></title>
 </head>
 <body>
 This is Page B...
 <hr/>
 
 <script type="text/javascript" src="js/utils.js" ></script>
 <script type="text/javascript">
 var pid = getUrlParam("pid");
 
 </script>
 
 </body>
</html> 

⼗四、商品详情展示显示商品评论信息

14.1 接⼝实现

14.1.1 数据库实现

  • 数据表分析及数据准备
  • SQL
-- 根据ID查询商品的评价信息,关联查询评价⽤户的信息
select u.username,u.nickname,u.user_img,c.*
from product_comments c
INNER JOIN users u
ON u.user_id = c.user_id
WHERE c.product_id =3;
  • 实体类封装 ProductCommentsVO
@Data 
@AllArgsConstructor
@NoArgsConstructor
public class ProductCommentsVO {
 private String commId;
 private String productId;
 private String productName;
 private String orderItemId;
 private Integer isAnonymous;
 private Integer commType;
 private Integer commLevel;
 private String commContent;
 private String commImgs;
 private Date sepcName;
 private Integer replyStatus;
 private String replyContent;
 private Date replyTime;
 private Integer isShow;
 //封装评论对应的⽤户数据
 private String userId;
 private String username;
 private String nickname;
 private String userImg; }
  • Mapper接⼝定义查询⽅法
@Repository
public interface ProductCommentsMapper extends
GeneralDAO<ProductComments> {
 
 public List<ProductCommentsVO> selectCommontsByProductId(String
productId);
 
}
  • 映射配置:

14.1.2 业务层实现

创建 ProductCommontsService 接⼝定义⽅法
public interface ProductCommontsService {
 public ResultVO listCommontsByProductId(String productId);
}
创建实现类 ProductCommontsServiceImpl 实现查询操作
@Service
public class ProductCommontsServiceImpl implements
ProductCommontsService {
 
 @Autowired
 private ProductCommentsMapper productCommentsMapper;
 
 @Override
 public ResultVO listCommontsByProductId(String productId) {
 List<ProductCommentsVO> productCommentsVOS =
productCommentsMapper.selectCommontsByProductId(productId);
 ResultVO resultVO = new ResultVO(ResStatus.OK, "success",
productCommentsVOS);
 return resultVO;
 }
}

14.1.3 控制层实现

  • ProductController
@ApiOperation("商品评论信息查询接⼝")
@GetMapping("/detail-commonts/{pid}")
public ResultVO getProductCommonts(@PathVariable("pid") String pid){
 return productCommontsService.listCommontsByProductId(pid);
}

14.2 前端评论内容显示

⼗五、商品详情展示商品评论分⻚及统计信息

15.1 流程分析

​​​​​​​

 

15.2 接⼝开发

15.2.1 改造商品评论列表接⼝

分⻚查询
  • 定义PageHelper
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageHelper<T> {
 //总记录数
 private int count;
 //总⻚数
 private int pageCount;
 //分⻚数据
 private List<T> list; 

}
  • 改造数据库操作
ProductCommentsMapper 接⼝

 

ProductCommentsMapper.xml 映射配置
  •  改造业务逻辑层
ProductCommontsService 接⼝

ProductCommontsServiceImpl

 改造控制层

ProductController

 

15.2.2 评价统计接⼝实现

  • 数据库实现 ​​​​​​​
    • 统计当前商品的总记录数
    • 统计当前商品的好评/中评/差评
  • 业务层实现: ProductCommontsServiceImpl
@Override
public ResultVO getCommentsCountByProductId(String productId) {
 //1.查询当前商品评价的总数
 Example example = new Example(ProductComments.class);
 Example.Criteria criteria = example.createCriteria();
 criteria.andEqualTo("productId",productId);
 int total =
productCommentsMapper.selectCountByExample(example);
 //2.查询好评评价数
 criteria.andEqualTo("commType",1);
 int goodTotal =
productCommentsMapper.selectCountByExample(example);
 //3.查询好评评价数
 Example example1 = new Example(ProductComments.class);
 Example.Criteria criteria1 = example1.createCriteria();
 criteria1.andEqualTo("productId",productId);
 criteria1.andEqualTo("commType",0);
 int midTotal =
productCommentsMapper.selectCountByExample(example1);
 //4.查询好评评价数
 Example example2 = new Example(ProductComments.class);
 Example.Criteria criteria2 = example2.createCriteria();
 criteria2.andEqualTo("productId",productId);
 criteria2.andEqualTo("commType",-1);
 int badTotal =
productCommentsMapper.selectCountByExample(example2);
 //5.计算好评率
 double percent = (Double.parseDouble(goodTotal+"") /
Double.parseDouble(total+"") )*100;
 String percentValue = (percent+"").substring(0, (percent+"").lastIndexOf(".")+3);
 HashMap<String,Object> map = new HashMap<>();
 map.put("total",total);
 map.put("goodTotal",goodTotal);
 map.put("midTotal",midTotal);
 map.put("badTotal",badTotal);
 map.put("percent",percentValue);
 ResultVO success = new ResultVO(ResStatus.OK, "success", map);
 return success; } 

15.3 前端实现

15.3.1 商品评论的分⻚

  • 引⽤elementUI分⻚组件
<!-- 引⼊样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/themechalk/index.css">
<!-- vue的引⼊必须在elementUI组件库引⼊之前 -->
<script type="text/javascript" src="js/vue.js"></script>
<!-- 引⼊组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
  • 引⽤分⻚组件
<!--分⻚ -->
<el-pagination background layout="prev, pager, next"
 :current-page="pageNum"
 :page-size="limit"
 :total="count"
 @current-change="pager"> </el-pagination>
  • 监听分⻚组件的 ⻚码改变 事件(点击上⼀⻚、下⼀⻚、⻚码都会导致 ⻚码改变
分⻚组件的事件函数默认传递当前⻚码参数
pager:function(currentPage){
 this.pageNum = currentPage;
 //请求下⼀⻚数据
 var url3 = baseUrl+"product/detail-commonts/"+this.productId;
 axios.get(url3,{
 params:{
 pageNum:this.pageNum,
 limit:this.limit
 }
 }).then((res)=>{
 //获取到评论分⻚数据
 var pageHelper = res.data.data;
 //当前⻚的评论列表
 this.productCommonts = pageHelper.list;
 //总⻚数
 this.pageCount = pageHelper.pageCount; 

 //总记录数
 this.count = pageHelper.count;
 });
}

15.3.2 商品评价统计

⼗六、购物⻋添加购物⻋(登陆状态)

16.1 流程分析  

16.2 接⼝实现

16.2.1 修改购物⻋数据表结构

shopping_cart

数据表修改完成之后,对此表重新进⾏逆向⼯程

16.2.2 数据库实现

单表添加操作,可以直接使⽤ tkMapper 完成

16.2.3 业务层实现

ShoppingCartService 接⼝
public interface ShoppingCartService {
 public ResultVO addShoppingCart(ShoppingCart cart);
}
实现类

16.3 前端实现

16.3.1 记录选择的套餐属性

  • vuedata中定义 chooseSkuProps

 

  • sku的属性添加点击事件

 

  • methods中定义事件函数 changeProp

 

  • 添加套餐切换的监听事件:

 

16.3.2 套餐属性选中效果

  • 在套餐属性标签上添加name属性

 

  • 在属性的点击事件函数实现选中效果

 

16.3.3 修改商品数量

  • vuedata中定义 num 存储商品数量(默认值为1
  • +-添加点击事件监听

 

  • 定义点击事件函数

 

16.3.4 提交购物⻋

 

⼗七、购物⻋添加购物⻋(未登录状态)

17.1 流程分析

17.2 功能实现

17.2.1 定义新的状态码

ResStatus

 登录认证拦截器

 

17.2.2 在详情⻚⾯判断如果⽤户未登录,则跳转到登录⻚⾯

introduction.html

 

17.2.3 登录⻚⾯接收回跳信息

login.html

 

17.2.4 回到详情⻚时接收参数

introduction.html

 

17.2.5 使⽤layui添加购物⻋成功/失败进⾏提示

  • 引⼊layui layui.com
<!-- 引⼊ layui.css -->
<link rel="stylesheet"
href="//unpkg.com/layui@2.6.5/dist/css/layui.css">
<!-- 引⼊ layui.js -->
<script src="//unpkg.com/layui@2.6.5/dist/layui.js">
  • 声明弹窗组件

 

  • 当添加购物⻋成功或者失败的时候,进⾏提示:

 

⼗⼋、购物⻋购物⻋列表

18.1 流程分析

 

18.2 接⼝实现

18.2.1 数据库实现

  • SQL
-- 根据⽤户ID查询当前⽤户的购物⻋信息
select c.*, p.product_name,i.url
from shopping_cart c 
INNER JOIN product p
INNER JOIN product_img i
ON c.product_id = p.product_id and i.item_id=p.product_id
where user_id=6 and i.is_main=1;
  • 实体类

 

  • Mapper接⼝定义查询⽅法
@Repository
public interface ShoppingCartMapper extends GeneralDAO<ShoppingCart> {
 public List<ShoppingCartVO> selectShopcartByUserId(int userId);
}
  • 映射配置

 

18.2.2 业务层实现

  • Service接⼝

 

  • Service实现类

 

18.2.3 控制层实现

18.3 前端实现

18.3.1 显示购物⻋列表

18.3.2 显示购物⻋中商品价格

⼗九、购物⻋-修改购物⻋数量

19.1 流程分析

19.2 接⼝实现

  • Mapper接⼝定义修改⽅法
@Repository
public interface ShoppingCartMapper extends GeneralDAO<ShoppingCart> {
 public List<ShoppingCartVO> selectShopcartByUserId(int userId);
 public int updateCartnumByCartid(@Param("cartId") int cartId,
 @Param("cartNum") int cartNum);
}
  • 映射配置
<update id="updateCartnumByCartid">
 update shopping_cart set cart_num=#{cartNum} where cart_id=#
{cartId}
</update>
  • Service接⼝
public interface ShoppingCartService {
 public ResultVO addShoppingCart(ShoppingCart cart);
 public ResultVO listShoppingCartsByUserId(int userId);
 public ResultVO updateCartNum(int cartId,int cartNum);
}
  • Service实现类
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService
{
 @Autowired
 private ShoppingCartMapper shoppingCartMapper; 
 private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd
hh:mm:ss");
 @Override
 public ResultVO updateCartNum(int cartId, int cartNum) {
 int i = shoppingCartMapper.updateCartnumByCartid(cartId,
cartNum);
 if(i>0){
 return new ResultVO(ResStatus.OK,"update
success",null);
 }else{
 return new ResultVO(ResStatus.NO,"update fail",null);
 }
 }
} 
  • 控制层实现
@PutMapping("/update/{cid}/{cnum}")
public ResultVO updateNum(@PathVariable("cid") Integer cartId,
 @PathVariable("cnum") Integer cartNum,
 @RequestHeader("token") String token){
 ResultVO resultVO = shoppingCartService.updateCartNum(cartId,
cartNum);
 return resultVO; }

19.3 前端实现

  • 为按钮添加点击事件

 

  • 定义changeNum事件函数

 

⼆⼗、购物⻋结算、提交订单

在购物⻋列表中选择对应的的商品之后,点击提交⽣成订单的过程

20.1 流程图

 

20.2 接⼝实现

20.2.1 收货地址列表接⼝

此操作的数据库实现可以通过 tkmapper 通⽤⽅法完成
  • service接⼝ UserAddrService
public interface UserAddrService {
 
 public ResultVO listAddrsByUid(int userId);
 
}
  • Service实现类 UserAddrServiceImpl
@Service
public class UserAddrServiceImpl implements UserAddrService {
 @Autowired
 private UserAddrMapper userAddrMapper;
 @Transactional(propagation = Propagation.SUPPORTS)
 public ResultVO listAddrsByUid(int userId) {
 Example example = new Example(UserAddr.class);
 Example.Criteria criteria = example.createCriteria();
 criteria.andEqualTo("userId",userId);
 criteria.andEqualTo("status",1);
 List<UserAddr> userAddrs =
userAddrMapper.selectByExample(example);
 ResultVO resultVO = new ResultVO(ResStatus.OK, "success",
userAddrs);
 return resultVO;
 }
}
  • 控制器实现
@RestController
@CrossOrigin
@Api(value = "提供收货地址相关接⼝",tags = "收货地址管理")
@RequestMapping("/useraddr") 
criteria.andIn("cartId",ids);
Mapper接⼝定义查询⽅法
public class UserAddrController {
 @Autowired
 private UserAddrService userAddrService;
 @GetMapping("/list")
 @ApiImplicitParam(dataType = "int",name = "userId", value = "⽤ 户ID",required = true)
 public ResultVO listAddr(Integer userId,
@RequestHeader("token") String token){
 ResultVO resultVO = userAddrService.listAddrsByUid(userId);
 return resultVO;
 }
}

20.2.2 购物⻋记录列表接⼝

根据⼀个 ID 的集合,查询购物⻋记录,实现⽅式有两种:
  • 动态sql
<select id="searchShoppingCartById" resultMap="shopCartMap">
 select * from shopping_cart where cart_id in
 <foreach collection="list" item="cid" separator="," open="("
close=")">
 #{cid}
 </foreach>
</select>
  • tkMapper条件查询
        criteria.andIn("cartId",ids);
  • Mapper接⼝定义查询⽅法

  • 映射配置(动态sql foreach)

 

 

  • Service接⼝

  • Service实现类

 

  • 控制器实现
@GetMapping("/listbycids")
@ApiImplicitParam(dataType = "String",name = "cids", value = "选择的购
物⻋记录id",required = true)
public ResultVO listByCids(String cids,
@RequestHeader("token")String token){
 ResultVO resultVO =
shoppingCartService.listShoppingCartsByCids(cids);
 return resultVO; }

20.2.3 保存订单

20.3 前端实现

20.3.1 选择购物⻋记录价格联动

  • 列表前的复选框标签

 

  • 渲染商品数量以及总价格

 

  • vue示例的data中声明optstotalPrice,并且监听opts选项的改变选项⼀旦改变就 计算总价格

​​​​​​​

 

20.3.2 点击结算跳转到订单添加⻚⾯

在购物⻋列表⻚⾯,选择购物⻋记录,点击 结算之后 将选择的购物⻋记录 ID传递到order-add.html
  • shopcart.html

 

 

order-add.html

20.3.3 显示收货地址及订单商品

20.3.4 订单确认⻚⾯选择地址

⼆⼗⼀、订单提交及⽀付

21.1 流程分析

 

21.2 订单添加接⼝实现

21.2.1 数据库操作

  • 根据收货地址ID,获取收货地址信息(tkMapper):收货⼈姓名、电话、地址也可以从 前端传递过来
  • 根据购物⻋ID,查询购物⻋详情(需要关联查询商品名称、sku名称、 库存 、商品图⽚、 商品价格)
改造: ShoppingCartMapper 中的 selectShopcartByCids

 

 

 

  • 保存订单(tkMapper
  • 修改库存(tkMapper
  • 保存商品快照(tkMapper

21.2.2 业务层实现

@Service
public class OrderServiceImpl implements OrderService {
 @Autowired
 private ShoppingCartMapper shoppingCartMapper;
 @Autowired
 private OrdersMapper ordersMapper;
 @Autowired
 private OrderItemMapper orderItemMapper;
 @Autowired
 private ProductSkuMapper productSkuMapper;
 /**
 * 保存订单业务
 */
 @Transactional
 public Map<String,String> addOrder(String cids,Orders order) throws
SQLException {
 Map<String,String> map = new HashMap<>();
 //1.校验库存:根据cids查询当前订单中关联的购物⻋记录详情(包括库存)
 String[] arr = cids.split(",");
 List<Integer> cidsList = new ArrayList<>();
 for (int i = 0; i <arr.length ; i++) {
 cidsList.add(Integer.parseInt(arr[i]));
 }
 List<ShoppingCartVO> list =
shoppingCartMapper.selectShopcartByCids(cidsList);
 boolean f = true;
 String untitled = "";
 for (ShoppingCartVO sc: list) {
 if(Integer.parseInt(sc.getCartNum()) > sc.getSkuStock()){
 f = false;
 }
 //获取所有商品名称,以,分割拼接成字符串
 untitled = untitled+sc.getProductName()+","; 
 }
 if(f){
 //2.保存订单
 order.setUntitled(untitled);
 order.setCreateTime(new Date());
 order.setStatus("1");
 //⽣成订单编号
 String orderId = UUID.randomUUID().toString().replace("-",
"");
 order.setOrderId(orderId);
 int i = ordersMapper.insert(order);
 //3.⽣成商品快照
 for (ShoppingCartVO sc: list) {
 int cnum = Integer.parseInt(sc.getCartNum());
 String itemId = System.currentTimeMillis()+""+ (new
Random().nextInt(89999)+10000);
 OrderItem orderItem = new OrderItem(itemId, orderId,
sc.getProductId(), sc.getProductName(), sc.getProductImg(),
sc.getSkuId(), sc.getSkuName(), new BigDecimal(sc.getSellPrice()),
cnum, new BigDecimal(sc.getSellPrice() * cnum), new Date(), new Date(),
0);
 orderItemMapper.insert(orderItem);
 }
 //4.扣减库存:根据套餐ID修改套餐库存量
 for (ShoppingCartVO sc: list) {
 String skuId = sc.getSkuId();
 int newStock = sc.getSkuStock()-
Integer.parseInt(sc.getCartNum());
 ProductSku productSku = new ProductSku();
 productSku.setSkuId(skuId);
 productSku.setStock(newStock);
 
productSkuMapper.updateByPrimaryKeySelective(productSku);
 }
 //5.删除购物⻋:当购物⻋中的记录购买成功之后,购物⻋中对应做删除操作
 for (int cid: cidsList) {
 shoppingCartMapper.deleteByPrimaryKey(cid);

 }
 map.put("orderId",orderId);
 map.put("productNames",untitled);
 return map;
 }else{
 //表示库存不⾜
 return null;
 }
 }
}

21.2.3 订单添加接⼝实现

  • 订单保存
  • 申请⽀付链接
@PostMapping("/add")
public ResultVO add(String cids, @RequestBody Orders order){
 ResultVO resultVO = null;
 try {
 Map<String, String> orderInfo = orderService.addOrder(cids,
order);
 String orderId = orderInfo.get("orderId");
 if(orderId!=null){
 //设置当前订单信息
 HashMap<String,String> data = new HashMap<>();
 data.put("body",orderInfo.get("productNames")); //商
品描述
 data.put("out_trade_no",orderId); //使⽤当前⽤户订单的编号作
为当前⽀付交易的交易号
 data.put("fee_type","CNY"); //⽀付币种
 data.put("total_fee",order.getActualAmount()*100+""); 
 //⽀付⾦额
 data.put("trade_type","NATIVE"); //交易类型
 data.put("notify_url","/pay/success"); //设置⽀付
完成时的回调⽅法接⼝
 //发送请求,获取响应
 //微信⽀付:申请⽀付连接
 WXPay wxPay = new WXPay(new MyPayConfig());
 Map<String, String> resp = wxPay.unifiedOrder(data);
 orderInfo.put("payUrl",resp.get("code_url"));
 resultVO = new ResultVO(ResStatus.OK,"提交订单成
功!",orderInfo);
 }else{
 resultVO = new ResultVO(ResStatus.NO,"提交订单失败!",null);
 }
 } catch (SQLException e) {
 resultVO = new ResultVO(ResStatus.NO,"提交订单失败!",null);
 } catch (Exception e) {
 e.printStackTrace();
 }
 return resultVO; }

21.3 前端提交订单信息

  • 点击提交订单按钮,触发 doSubmit

order-add.html

21.4 ⽀付回调

⽀付回调:当⽤户⽀付成功之后,⽀付平台会向我们指定的服务器接⼝发送请求传递订
单⽀付状态数据

2.4.1 创建⼀个控制器定义回调接⼝

@RestController
@RequestMapping("/pay")
public class PayController {
 @Autowired
 private OrderService orderService;
 /**
 * 回调接⼝:当⽤户⽀付成功之后,微信⽀付平台就会请求这个接⼝,将⽀付状态的数据
传递过来
 *
 */
 @RequestMapping("/callback")
 public String paySuccess(HttpServletRequest request) throws
Exception {
 System.out.println("--------------------callback");
 // 1.接收微信⽀付平台传递的数据(使⽤request的输⼊流接收)
 ServletInputStream is = request.getInputStream();
 byte[] bs = new byte[1024];
 int len = -1;
 StringBuilder builder = new StringBuilder();
 while((len = is.read(bs))!=-1){
 builder.append(new String(bs,0,len));
 }
 String s = builder.toString();
 //使⽤帮助类将xml接⼝的字符串装换成map
 Map<String, String> map = WXPayUtil.xmlToMap(s);
 if(map!=null &&
"success".equalsIgnoreCase(map.get("result_code"))){
 //⽀付成功
 //2.修改订单状态为“待发货/已⽀付”
 String orderId = map.get("out_trade_no");
 int i = orderService.updateOrderStatus(orderId, "2");
 System.out.println("--orderId:"+orderId);
 //3.响应微信⽀付平台
 if(i>0){
 HashMap<String,String> resp = new HashMap<>();
 resp.put("return_code","success");
 resp.put("return_msg","OK");
 resp.put("appid",map.get("appid"));
 resp.put("result_code","success");
 return WXPayUtil.mapToXml(resp);
 }
 }
 return null;
 }
}

2.4.2 设置回调URL

在订单接⼝中申请⽀付连接的时候将回调接⼝的路径设置给微信⽀付平台
​​​​​​​ OrderController

 

思考:如果按照上图所示的配置,当⽤户⽀付成功之后,微信⽀付平台会向
http://192.168.55.3:8080/pay/callback )发送请求,因为我们的服务端项⽬是运⾏在
本地计算机的( IP 为内⽹ IP ),微信平台没办法访问 —— 内⽹穿透

21.5 Ngrok实现内⽹穿透

21.5.1 注册帐号,申请隧道ID

  • 注册 www.ngrok.cc
  • 开通隧道

 

  • 获取隧道ID118c7bfe1ac90369

21.5.2 下载ngrok客户端  

https://ngrok.cc/download.html

21.5.3 启动客户端

 

访问 http://ytao.free.idcfengye.com 就相当于访问本地的 8080

21.6 前端通过轮询访问获取订单⽀付状态

  • 流程图

 

  • 接⼝实现
  • 前端轮询访问接⼝

21.7 webSocket消息推送

21.7.1 实现流程

 

21.7.2 创建webSocket服务器

  • 添加依赖
<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-starter-websocket </artifactId>
</dependency>
  • 添加websocket服务节点配置(Java配置⽅式)
@Configuration
public class WebSocketConfig {
 
 @Bean
 public ServerEndpointExporter getServerEndpointExporter(){
 return new ServerEndpointExporter();
 }
 
}
  • 创建websocket服务器
@Component
@ServerEndpoint("/webSocket/{oid}")
public class WebSocketServer {

 private static ConcurrentHashMap<String,Session> sessionMap =
new ConcurrentHashMap<>();
 /**前端发送请求建⽴websocket连接,就会执⾏@OnOpen⽅法**/
 @OnOpen
 public void open(@PathParam("oid") String orderId, Session
session){
 sessionMap.put(orderId,session);
 }
 /**前端关闭⻚⾯或者主动关闭websocket连接,都会执⾏close**/
 @OnClose
 public void close(@PathParam("oid") String orderId){
 sessionMap.remove(orderId);
 }
 
 public static void sendMsg(String orderId,String msg){
 try {
 Session session = sessionMap.get(orderId);
 session.getBasicRemote().sendText(msg);
 }catch (Exception e){
 e.printStackTrace();
 }
 }
} 

21.7.3 在⽀付回调接⼝,通过订单id获取session返回结果

@RestController
@RequestMapping("/pay")
public class PayController {
 @Autowired
 private OrderService orderService;
 /**
 * 回调接⼝:当⽤户⽀付成功之后,微信⽀付平台就会请求这个接⼝,将⽀付状态的数据
传递过来
 */
 @RequestMapping("/callback") 

 public String paySuccess(HttpServletRequest request) throws
Exception {
 System.out.println("--------------------callback");
 // 1.接收微信⽀付平台传递的数据(使⽤request的输⼊流接收)
 ServletInputStream is = request.getInputStream();
 byte[] bs = new byte[1024];
 int len = -1;
 StringBuilder builder = new StringBuilder();
 while((len = is.read(bs))!=-1){
 builder.append(new String(bs,0,len));
 }
 String s = builder.toString();
 //使⽤帮助类将xml接⼝的字符串装换成map
 Map<String, String> map = WXPayUtil.xmlToMap(s);
 if(map!=null &&
"success".equalsIgnoreCase(map.get("result_code"))){
 //⽀付成功
 //2.修改订单状态为“待发货/已⽀付”
 String orderId = map.get("out_trade_no");
 int i = orderService.updateOrderStatus(orderId, "2");
 System.out.println("--orderId:"+orderId);
 //3.通过websocket连接,向前端推送消息
 WebSocketServer.sendMsg(orderId,"1");
 //4.响应微信⽀付平台
 if(i>0){
 HashMap<String,String> resp = new HashMap<>();
 resp.put("return_code","success");
 resp.put("return_msg","OK");
 resp.put("appid",map.get("appid"));
 resp.put("result_code","success");
 return WXPayUtil.mapToXml(resp);
 }
 }
 return null;
 }
}

21.7.4 前端进⼊到⽀付⻚⾯时,就建⽴websocket连接

//前端发送websocket连接请求
var webSocketUrl = webSocketBaseUrl + "webSocket/"+
this.orderInfo.orderId;
var websocket = new WebSocket( webSocketUrl );
//只要后端通过websocket向此连接发消息就会触发onmessage事件
websocket.onmessage = function(event){
 var msg = event.data;
 if(msg=="1"){
 $("#div1").html("<label style='font-size:20px; color:green'>订
单⽀付完成!</label>");
 }
}

⼆⼗⼆、订单超时取消

订单超时取消,指的是当⽤户成功提交订单之后在规定时间内没有完成⽀付,则将订单
关闭还原库存。
实现订单的超时取消业务通常有两种解决⽅案:
  • 定时任务(quartz
  • 延时队列(MQ

22.1 实现流程

// 前端发送 websocket 连接请求

 

​​​​​​​22.2 quartz定时任务框架使⽤

22.2.1 添加依赖

<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-starter-quartz </artifactId>
</dependency>

22.2.2 创建定时任务

定时任务,每隔指定的时间就执⾏⼀次任务
案例:每隔 3s 就打印⼀次 Helloworld
@Component
public class PrintHelloWorldJob {
 //https://cron.qqe2.com
 @Scheduled(cron = "0/3 * * * * ?")
 public void printHelloWorld(){
 System.out.println("----hello world.");
 }
}
@SpringBootApplication
@EnableScheduling
public class QuartzDemoApplication {
 public static void main(String[] args) {
 SpringApplication.run(QuartzDemoApplication.class, args);
 }
}

22.2.3 在启动类开启定时任务

22.3 实现订单超时取消

22.3.1 service⼦⼯程添加spring-boot-starter-quartz依赖

22.3.2 api⾃动启动类添加@EnableScheduling注解

⼆⼗三、按类别查询商品

23.1 流程分析

 

23.2 接⼝开发

23.2.1 根据类别查询商品接⼝

  • 数据库分析及SQL
  • 数据库实现

​​​​​​​实体类​​​​​​​

ProductMapper接⼝

 

 

ProductSkuMapper

 

 

  • 业务层实现
  • 控制层实现

23.2.2 根据类别ID查询当前类别下所有商品的品牌

  • SQL
-- 查询统计某个类别下所有商品的品牌:SQL
select DISTINCT brand from product_params where product_id in
(select product_id from product where category_id=49)
  • 数据库实现
ProductMapper 接⼝

 

映射配置

 

  • Service实现
  • Controller实现

23.3 前端实现

⼆⼗四、商品搜索

24.1 流程分析

 

24.2 接⼝实现

24.2.1 模糊查询商品信息

  • 数据库实现
ProductMapper

 

映射配置

 

  • Service实现

 ProductServiceImpl

  • Controller实现

24.2.2 根据关键字查询对应商品的品牌

  • SQL
-- 根据关键字查询对应商品的品牌名称
select DISTINCT brand from product_params where product_id in
(select product_id from product where product_name like '%⼩%')
  • 数据库实现
ProductMapper

 ​​​​​​​

映射配置

 

  • Service实现
ProductServiceImpl

 

  • Controller实现

24.3 前端实现

⼆⼗五、⽤户中⼼

25.1 ⽤户中⼼登录校验

25.1.1 校验token接⼝实现

  • UserController
@ApiOperation("校验token是否过期接⼝")
@GetMapping("/check")
public ResultVO userTokencheck(@RequestHeader("token") String token){
 return new ResultVO(ResStatus.OK,"success",null);
}
  • token校验拦截器配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 @Autowired
 private CheckTokenInterceptor checkTokenInterceptor;
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
 registry.addInterceptor(checkTokenInterceptor)
 .addPathPatterns("/shopcart/**")
 .addPathPatterns("/orders/**")
 .addPathPatterns("/useraddr/**")
 .addPathPatterns("/user/check"); //将接⼝配置到token校验
拦截器
 }
}

25.1.2 前端⽤户中⼼⾸⻚校验token

<script type="text/javascript">
 var baseUrl = "http://localhost:8080/"
 var vm = new Vue({
 el:"#div1", 
 data:{
 token:"",
 username:"",
 userImg:""
 },
 created:function(){
 //从cookie中获取⽤户信息(token、⽤户id、⽤户名、头像)
 this.token = getCookieValue("token");
 if(this.token == null){
 window.location.href = "login.html?returnUrl=usercenter.html";
 }else{
 //如果登录过期需要重新登录,创建⼀个校验⽤户登录的接⼝,通过请求
这个接⼝进⾏token的检验
 //请求user/check接⼝
 var url1 = baseUrl+"user/check";
 axios({
 url:url1,
 method:"get",
 headers:{
 token:this.token
 }
 }).then((res)=>{
 if(res.data.code == 10000){
 //token校验通过
 this.username = getCookieValue("username");
 this.userImg = getCookieValue("userImg");
 }else{
 window.location.href = "login.html?
returnUrl=user-center.html";
 }
 });
 }
 }
 });
</script>

25.2 ⽤户中⼼的订单管理

25.2.1 流程分析

 

25.2.2 接⼝实现

  • 数据库实现
    • 根据⽤户的ID分⻚查询当前⽤户的订单信息、关联查询订单中的商品快照
    • 根据⽤户的ID和订单状态分⻚查询当前⽤户的订单信息、关联查询订单中的商品快照
 封装OrdersVO

 

OrderMapper

 

映射配置

 

 

  • Service实现
OrderServiceImpl

 

  • 控制器实现

25.2.3 前端实现

https://www.processon.com/view/link/606bde8b1e08534321fd2103
​​​​​​​

⼆⼗六、项⽬云部署

26.1 项⽬部署介绍

项⽬部署就是将开发 测试完成的项⽬运⾏在⽣产环境中
多种的部署环境是为了实现数据的隔离、对数据进⾏保护
开发环境: windows (有⾮常便利的可视化操作系统)
⽣产环境: Linux (开源免费、系统开销⼩、安全性⾼)
  • 开发环境(windows
    • 应⽤服务器 Tomcat-windows
    • 数据库服务器 MySQL-Linux/windows
  • 测试环境(模拟的⽣产环境)
    • 应⽤服务器 Tomcat-Linux
    • 数据库服务器 Tomcat-Linux
  • ⽣产环境(需要进⾏保护的、不能被破坏的) ​​​​​​​
    • 应⽤服务器 Tomcat-Linux
    • 数据库服务器 Tomcat-Linux

26.2 后端项⽬部署

1. 搭建云服务器环境
  • JDK
  • MySQL
2.maven 聚合⼯程打包

 

3.上传到云服务器

4. 启动项⽬
​​​​​​​ java -jar api-2.0.1.jar &

26.3 前端项⽬部署(tomcat

前端项⽬也需要部署在服务器上,才能够提供多⽤户访问
tomcat 可以作为前端项⽬部署的服务器使⽤
  • 安装Tomcat,配置port 9999
  • 将前端项⽬上传到tomcat/webapps
  • 启动Tomcat
  • 访问:http://47.118.45.73:9999/fmall-static/index.html

 

使⽤ Tomcat 部署前端项⽬存在的问题:
1. 前端项⽬的⼀个⻚⾯会包含⼤量的 css\js\ 图⽚,会有⼤量的并发请求, Tomcat 难以满
⾜并发的需求
2.Tomcat 的核⼼价值在于能够便于执⾏ Java 程序,⽽不是处理并发,同时前端项⽬中没
Java 程序,从功能上讲前端项⽬的部署也⽤不着 Tomcat

 

结论 :使⽤ Tomcat 作为部署前端项⽬的服务器是不合适的。

26.4 前端项⽬部署(Nginx

Linux 安装 Nginx
修改前端项⽬的 baseUrl
将前端项⽬上传到 nginx ⽬录
配置 nginx 的访问路径

⼆⼗七、项⽬⽇志管理

27.1 ⽇志框架的概念

在项⽬开发、运维过程中,为了能够清晰的知道项⽬在服务器中的运⾏过程、便于查找
服务器运⾏过程的异常原因,我们需要对系统运⾏过程进⾏记录 --- 运⾏⽇志
1. 我们可以使⽤ 控制台输出 的形式进⾏运⾏过程记录(不便于⽇志信息跟踪和维护、
不能够持久化存储)
2. 控制台输出⽇志的诸多弊端 催化了 ⽇志框架的诞⽣
⽇志框架 ⽤于帮助我们在应⽤开发中完成⽇志记录的帮助类
⽇志框架作⽤
1. 有结构的记录⽇志信息(结构是为了提升⽇志信息的可读性)
2. 定义⽇志的输出策略(每个⽇志⽂件最⼤ 5m 、每天⼀个⽇志⽂件)

27.2 ⽇志框架规范

⽇志框架规范:⽇志记录实现的规则
⽇志框架:实现⽇志记录

 

  • ⽇志接⼝(⽇志框架规范) ​​​​​​​
    • JCLJakatra Commons Logging
    • SLF4JSimple Logging Facade For Java
    • JBoss Logging
  • ⽇志实现(⽇志框架) ​​​​​​​
    • Log4j
    • Logback

27.3 SLF4J

  • SLF4JSimple Logging Facade For Java )简单⽇志⻔⾯,定义了⼀套⽇志规范,并不 是⽇志框架解决⽅法。
  • SLF4J的实现框架

 

27.4 slf4j-simple

  • 创建springBoot应⽤
  • 移出springboot应⽤默认logback-classic的⽇志依赖
<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-starter-web </artifactId>
<exclusions>
<exclusion>
<groupId> ch.qos.logback </groupId>
<artifactId> logback-classic </artifactId>
</exclusion>
</exclusions>
</dependency>
  • 添加依赖

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
<groupId> org.slf4j </groupId>
<artifactId> slf4j-simple </artifactId>
<version> 1.7.25 </version>
</dependency>
  • service层打印⽇志
@Service
public class TestServiceImpl implements TestService {
 private Logger logger =
LoggerFactory.getLogger(TestServiceImpl.class);
 @Override
 public void testLog() {
 //⽇志记录
 //System.out.println("⽇志信息");
 logger.info("订单添加完成");
 }
}

27.5 log4j使⽤介绍

log4j 没有实现 slf4j, 如果基于 slf4j 规范使⽤ log4j ,则需要添加 slf4j-log4j12 依赖
  • 添加依赖
<dependency>
<groupId> org.slf4j </groupId>
<artifactId> slf4j-log4j12 </artifactId>
<version> 1.7.25 </version>
<scope> test </scope>
</dependency>

 

  • resources⽬录下创建log4j.properties⽂件
log4j.rootLogger = DEBUG,stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper = TRACE
# Console output...
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%t] %5p - %n%m

27.6 基于SpringBoot应⽤的logback⽇志配置

SpringBoot 默认整合了 logback-classic ⽇志框架,我们需要对 logback ⽇志框架进⾏配置
以⾃定义⽇志输出格式、⽇志⽂件配置、⽇志⽂件保存策略等信息
  • logback配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="stdout"
class="ch.qos.logback.core.ConsoleAppender">
 <encoder>
 <springProfile name="dev">
 <pattern>%d{yyyy-MM-dd-HH:mm:ss E} %level [%thread]-
%class[%line]: %msg%n</pattern>
 </springProfile>
 <springProfile name="!dev">
 <pattern>%d{yyyy-MM-dd-HH:mm:ss E} %level [%thread]-
%class[%line]: %msg%n</pattern>
 </springProfile>
 <!--⽇志的编码格式-->
 <charset>UTF-8</charset>
 </encoder>
 </appender>
 <!--这个就表示的是要把 ⽇志输出到⽂件(FileAppender)-->
 <appender name="file" class="ch.qos.logback.core.FileAppender">
 <file>D:/log/output.log</file>
 <!--设置⽇志是否追加-->
 <append>true</append>
 <encoder>
 <pattern>%d{yyyy-MM-dd-HH:mm:ss.SSS} %level [%thread]-
%class:%line>>%msg%n</pattern>
 <charset>UTF-8</charset>
 </encoder>
 <!--设置⽇志写⼊是否线程安全-->
 <prudent>false</prudent>
 </appender>
 <appender name="timeFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
 <!--TimeBasedRollingPolicy 基于时间的滚动策略-->
 <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 <fileNamePattern>d:/log/log-%d{yyyy-MM-ddHH}.log</fileNamePattern>
 </rollingPolicy>
 <encoder>
 <pattern>%d{yyyy-MM-dd-HH:mm:ss.SSS} %level [%thread]-
%class:%line>>%msg%n</pattern>
 <charset>UTF-8</charset>
 </encoder>
 </appender>
 <appender name="fixedFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
 <file>d:/log/fixedFile.log</file>
 <rollingPolicy
class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
 <fileNamePattern>log/fixedFile%i.log</fileNamePattern>
 <minIndex>1</minIndex>
 <maxIndex>10</maxIndex>
 </rollingPolicy>
 <!--SizeBasedTriggeringPolicy-->
 <triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
 <maxFileSize>5MB</maxFileSize>
 </triggeringPolicy>
 <encoder>
 <pattern>%d{yyyy-MM-dd-HH:mm:ss.SSS} %level [%thread]-
%class:%line>>%msg%n
 </pattern>
 <charset>UTF-8</charset>
 </encoder>
 </appender>
 <root level="info">
 <appender-ref ref="stdout" />
 <appender-ref ref="timeFile"/>
 </root>
</configuration>

27.7 在锋迷商城项⽬进⾏⽇志配置

27.7.1 api⼦⼯程的resources⽬录添加⽇志配置⽂件

  • logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="stdout"
class="ch.qos.logback.core.ConsoleAppender">
 <encoder>
 <springProfile name="dev">
 <pattern>%d{yyyy-MM-dd-HH:mm:ss E} %level
[%thread]-%class[%line]: %msg%n</pattern>
 </springProfile>
 <springProfile name="!dev">
 <pattern>%d{yyyy-MM-dd-HH:mm:ss E} %level
[%thread]-%class[%line]: %msg%n</pattern>
 </springProfile>
 <!--⽇志的编码格式-->
 <charset>UTF-8</charset> 123456789
 </encoder>
 </appender>
 <appender name="timeFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
 <!--TimeBasedRollingPolicy 基于时间的滚动策略-->
 <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 <!--⽇志⽂件的存储路径-->
 <fileNamePattern>log/log-%d{yyyy-MM-ddHH}.log</fileNamePattern>
 </rollingPolicy>
 <encoder>
 <pattern>%d{yyyy-MM-dd-HH:mm:ss.SSS} %level [%thread]-
%class:%line>>%msg%n</pattern>
 <charset>UTF-8</charset>
 </encoder>
 </appender>
 <root level="info">
 <appender-ref ref="stdout"/>
 <appender-ref ref="timeFile"/>
 </root>
</configuration>

27.7.2 sercie实现类创建Logger对象,输⼊⽇志

​​​​​​​

  • 8
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值