⼀、项目介绍

https://www.processon.com/view/link/606bde8b1e08534321fd2103
技术清单

  • 项⽬架构:前后端分离
  • 前端技术:vue、axios、妹⼦UI、layui、bootstrap
  • 后端技术:SpringBoot+MyBatis、RESTful、swagger
  • 服务器搭建:Linux、Nginx

在这里插入图片描述
分布式

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

二、项目搭建

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

2.1 创建Maven聚合⼯程

在这里插入图片描述

2.1.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>

2.1.2 创建common⼯程

选择fmmall,右键—New—Module (Maven⼯程)
修改common的pom.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>

2.1.3 创建beans⼯程

选择fmmall,右键—New—Module (Maven⼯程)
修改beans的pom.xml,设置packing ----- jar

2.1.4 创建mapper⼯程

选择fmmall,右键—New—Module (Maven⼯程)
修改mapper的pom.xml,设置packing ----- jar
在mapper的pom.xml,依赖beans

<dependency>
 <groupId>com.qfedu</groupId>
 <artifactId>beans</artifactId>
 <version>2.0.1</version>
</dependency>

2.1.5 创建service⼯程

选择fmmall,右键—New—Module (Maven⼯程)
修改service的pom.xml,设置packing ----- jar
在service的pom.xml,依赖mapper、commom

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

2.1.6 创建 api ⼯程

选择fmmall,右键—New—Module (SpringBoot⼯程)
修改api的pom.xml,继承fmmall,删除自己的groupId 和 version

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

将spring boot的依赖配置到父工程fmmall的pom.xml
在父工程fmmall的pom.xml的modules添加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>

api的pom.xml继承fmmall

2.2 整合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);
    }
}

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

2.3.1 添加依赖

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

2.3.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);
 }
}

2.4 整合Druid

2.4.1 添加依赖

在mapper⼦⼯程添加druid-starter

<!--druid starter-->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid-spring-boot-starter</artifactId>
 <version>1.1.22</version>
</dependency>

2.4.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

三、业务流程设计-接⼝规范

前后端分离架构:前端和后端分离开发和部署,前端只能通过异步向后端发送请求,后端只负责接收请求及参数、处理请求、返回处理结果,但是后端并不负责流程控制,流程控制是由前端完成

3.1 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

3.2 Swagger-ui 插件

导⼊插件的依赖

<dependency>
 <groupId>com.github.xiaoymin</groupId>
 <artifactId>swagger-bootstrap-ui</artifactId>
 <version>1.9.6</version>
</dependency>

⽂档访问

http://ip:port/doc.html

3.3 RESTful

提供请求时就要对外暴露⼀个 URL ; URL 的设计不能是随意的,需要遵从⼀定的设计规范 ——RESTful

四、设计及实现—⽤户管理

4.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; 
}
  1. 创建 DAO 接⼝、定义操作⽅法
public interface UserDAO {
 //⽤户注册
 public int insert(User user);
 
 //根据⽤户名查询⽤户信息
 public User query(String name);
}
  1. 创建 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);
}
  1. 创建 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();
}

配置映射⽂件

<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实现类

@Service
public class ProductServiceImpl implements ProductService {
 @Autowired
 private ProductMapper productMapper;
 public ResultVO listRecommendProducts() {
 List<ProductVO> productVOS = productMapper.selectRecommendProducts();
 ResultVO resultVO = new ResultVO(ResStatus.OK, "success", productVOS);
 return resultVO;
 }
}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 记录选择的套餐属性
在vue的data中定义 chooseSkuProps
在这里插入图片描述

为sku的属性添加点击事件
在这里插入图片描述

在methods中定义事件函数 changeProp
在这里插入图片描述

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

在这里插入图片描述

16.3.2 套餐属性选中效果
在套餐属性标签上添加name属性
在这里插入图片描述

在属性的点击事件函数实现选中效果
在这里插入图片描述

16.3.3 修改商品数量
在vue的data中定义 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 * 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中声明opts和totalPrice,并且监听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
开通隧道
在这里插入图片描述

获取隧道ID:118c7bfe1ac90369
在这里插入图片描述

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.上传到云服务器

  1. 启动项⽬
​​​​​​​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. 控制台输出⽇志的诸多弊端 催化了 ⽇志框架的诞⽣
    ⽇志框架 ⽤于帮助我们在应⽤开发中完成⽇志记录的帮助类
    ⽇志框架作⽤
  3. 有结构的记录⽇志信息(结构是为了提升⽇志信息的可读性)
  4. 定义⽇志的输出策略(每个⽇志⽂件最⼤ 5m 、每天⼀个⽇志⽂件)
    27.2 ⽇志框架规范
    ⽇志框架规范:⽇志记录实现的规则
    ⽇志框架:实现⽇志记录
    在这里插入图片描述

⽇志接⼝(⽇志框架规范) ​​​​​​​
JCL(Jakatra Commons Logging)
SLF4J(Simple Logging Facade For Java )
JBoss Logging
⽇志实现(⽇志框架) ​​​​​​​
Log4j
Logback
27.3 SLF4J
SLF4J(Simple 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对象,输⼊⽇志

在这里插入图片描述

Logo

前往低代码交流专区

更多推荐