一、《锋迷商城》项目介绍
1.1 项目背景
锋迷商城——电商平台
- B2C商家对客户
- C2B2C 客户对商家对客户
1.1.1 B2C(一家超市)
平台运营方即商品的卖家,例如:小米商城
- 商品
- 用户
1.1.2 C2B2C(一条街)
平台运营方不卖商品(也可以卖)
卖家是平台的用户
买家也是平台的用户
- 用户(店铺)
- 用户(买家)
- 服务
- 商品
1.1.3 Java
Java语言的应用领域很广,但主要应用于web领域的项目开发,web项目类型分为两类:
- 企业级开发(供企业内部使用的系统:企业内部的管理系统CRM\ERP,学校的教务管理系统)
- 互联网开发(提供给所有互联网用户使用的系统——用户量)——电商,扩展知识(BAT:百度、阿里、腾讯;TMD:头条、美团、滴滴)
1.2 项目功能
https://www.processon.com/mindmap/60fbfb63e0b34d49622edc3a
1.3 技术选型
SSM 企业开发框架 基础的开发技术
1.3.1 单体项目
项目的页面和代码都在同一个项目,项目开发完成之后直接部署在一台服务器
单体项目遇到的问题:用户对页面静态资源以及对Java代码的请求压力都会落在Tomcat服务器上。
1.3.2 技术清单
- 项目架构:前后端分离
- 前端技术:vue、axios 、Amaze UI、layui、bootstrap(jQuery已成过去式(页面动态刷新+异步通信 ==> 笨重))
- 后端技术:SpringBoot(包含Spring、SpringMVC)+MyBatis、RESTful、swagger
- 服务器搭建:Linux、Nginx
二、项目架构的演进
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 构建父工程
-
创建一个Maven工程、packaging设置为 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.5.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lzp</groupId><artifactId>fmmall</artifactId><version>2.0.1</version><packaging>pom</packaging></project>
3.2.2 创建common工程
-
选择fmmall,右键——New——Module(Maven工程)
-
修改common的pom.xml,设置packaging=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><groupId>com.lzp</groupId><artifactId>fmmall</artifactId><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工程)
- 修改beans的pom.xml,设置packaging=jar
3.2.4 创建mapper工程
-
选择fmmall,右键——New——Module(Maven工程)
-
修改mapper的pom.xml,设置packaging=jar
-
在mapper的pom.xml,依赖beans
<dependency><groupId>com.lzp</groupId><artifactId>beans</artifactId><version>2.0.1</version> </dependency>
3.2.5 创建service工程
-
选择fmmall,右键——New——Module(Maven工程)
-
修改service的pom.xml,设置packaging=jar
-
在service的pom.xml,依赖mapper,common
<dependency><groupId>com.lzp</groupId><artifactId>mapper</artifactId><version>2.0.1</version> </dependency> <dependency><groupId>com.lzp</groupId><artifactId>common</artifactId><version>2.0.1</version> </dependency>
3.2.6 创建api工程
-
选择fmmall,右键——New——Module(SpringBoot工程)
-
修改api的pom.xml,继承fmmall,删除自己的groupId 和 version
<parent><groupId>com.lzp</groupId><artifactId>fmmall</artifactId><version>2.0.1</version> </parent>
-
将springboot的依赖配置到父工程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.5.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lzp</groupId><artifactId>fmmall</artifactId><version>2.0.1</version><modules><module>common</module><module>beans</module><module>mapper</module><module>service</module><module>api</module></modules><packaging>pom</packaging><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><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
-
在api的pom.xml中,依赖service
<!-- 依赖了service,等于依赖了所有 --> <dependency><groupId>com.lzp</groupId><artifactId>service</artifactId><version>2.0.1</version> </dependency>
3.3 Maven聚合工程依赖分析
如果将依赖添加到父工程的pom中,根据依赖的继承关系,所有的子工程都会继承父工程的依赖:
- 好处:当有多个子工程都需要相同的依赖时,无需在子工程中重复添加依赖
- 缺点:如果某些子工程不需要这个依赖,还是会被强行继承
如果在父工程中没有添加统一依赖,则每个子工程所需的依赖需要在子工程的pom中自行添加
如果存在多个子工程需要添加相同的依赖,则需在父工程pom进行依赖版本的管理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6gEP39o-1646129708196)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210727145559634.png)]
说明
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.Driverurl: jdbc:mysql://localhost:3306/db_2010_mybatis?characterEncoding=utf-8username: rootpassword: 123 mybatis:mapper-locations: classpath:mappers/*Mapper.xmltype-aliases-package: com.lzp.fmmall.entity
-
在**api子工程*的启动类通过 @MapperScan 声明dao包的路径
@SpringBootApplication @MapperScan("com.lzp.fmmall.dao") public class ApiApplication {public static void main(String[] args) {SpringApplication.run(ApiApplication.class, args);}}
3.5 基于SpringBoot的单元测试
3.5.1 添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId>
</dependency>
3.5.2 测试类
package com.lzp.fmmall.dao;import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
public class UserDAOTest {@Testpublic void queryUserByName() {System.out.println("测试成功");}
}
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.Driverurl: jdbc:mysql://localhost:3306/db_2010_mybatis?characterEncoding=utf-8username: rootpassword: 123
mybatis:mapper-locations: classpath:mappers/*Mapper.xmltype-aliases-package: com.lzp.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 2999 1
102 4G / 64G 20 1999 1
103 8G / 128G 12 2999 1
104 12G / 256G 11 3999 1
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.5.2 接口规范
作为一个后端开发者,我们不仅要完成接口程序的开发,还要编写接口的说明文档——接口规范
接口规范示例 :
参考:《锋迷商城》后端接口说明
5.3 Swagger
前后端分离开发,后端需要编写接口说明文档,会耗费比较多的时间
swagger是一个用于生成服务器接口的规范性文档,并且能够对接口进行测试的工具
5.3.1 作用
- 生成接口说明文档
- 对接口进行测试
5.3.2 整合
-
在api子工程添加依赖(Swagger2 \ Swagger UI)
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version> </dependency>
-
在zapi子工程创建swagger的配置(基于Java的配置方式)
package com.lzp.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;/*** @Author LZP* @Date 2021/7/29 9:57* @Version 1.0*/ @Configuration @EnableSwagger2 public class SwaggerConfig {/*** 返回一个封装接口文档信息的Docket对象*/@Beanpublic Docket getDocket() {// 创建Docket实例Docket docket = new Docket(DocumentationType.SWAGGER_2);// 创建封面信息对象,通过ApiInfoBuilder来创建ApiInfoBuilder builder = new ApiInfoBuilder();// 进行相应的配置builder.title("《锋迷商城》后端接口说明文档").description("此文档详细说明了锋迷商城项目后端接口规范...").version("v 2.0.1").contact(new Contact("LZP", "https://www.cnblogs.com/pengsay", "1471591945@qq.com"));ApiInfo apiInfo = builder.build();docket.apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.lzp.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(value = "username", required = true) String name,@RequestParam(value = "password", required = false, defaultValue = "111111") String pwd) {return userService.checkLogin(name, pwd);
}
@ApiModel
和 @ApiModelProperty
当接口参数和返回值为对象类型时,在实体类中添加注解说明
package com.lzp.fmmall.entity;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author LZP* @Date 2021/7/27 15:52* @Version 1.0*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "User对象", description = "用户/买家信息")
public class User {@ApiModelProperty(dataType = "int", required = false, value = "用户id")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-bootstrap-ui插件
-
导入插件的依赖
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/swagger-bootstrap-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 商品1http://localhost:8080/goods/delete?goodsId=2 商品2RESTful设计:http://localhost:8080/goods/delete/1 商品1http://localhost:8080/goods/delete/2 商品2
@RequestMapping("/delete/{gid}") public ResultVO deleteGoods(@PathVariable("gid") int goodsId) {System.out.println("--goodsId-->" + goodsId);return new ResultVO(10000, "SUCCESS", null); }
-
使用不同的请求方式表示不同的操作
SpringMVC对RESTful风格提供了很好的支持,在我么定义一个接口的URL时,可以通过
@RequestMapping(value="/{id}", method=RequestMapping.GET)
,形式指定请求方式,也可使用特定请求方式的注解设定URL@PostMapping("/add")
@DeleteMapping("/{id}")
@PutMapping("/{id}")
@GetMapping("/{id}")
- post 添加
- get 查询
- put 修改
- delete 删除
- option(预检)
根据ID删除一个商品 // localhost:8080/goods/1 [delete] 商品1 @RequestMapping(value = "/{gid}", method = RequestMethod.DELETE) public ResultVO deleteGoods(@PathVariable("gid") int goodsId) {System.out.println("--goodsId-->" + goodsId);return new ResultVO(10000, "SUCCESS", null); }根据ID查询一个商品 // localhost:8080/goods/1 [get] 商品1 @RequestMapping(value = "/{gid}", method = RequestMethod.GET) public ResultVO getGoods(@PathVariable("gid") int goodsId) {System.out.println("--goodsId-->" + goodsId);return new ResultVO(10000, "SUCCESS", null); }
-
接口响应的资源的表现形式采用JSON(或者XML)
- 在控制器类或者每个接口方法添加
@ResponseBody
注解将返回的对象格式为JSON - 或者直接在控制器类使用
@RestController
注解声明控制器
- 在控制器类或者每个接口方法添加
-
前端(Android\ios\pc)
通过无状态的HTTP协议与后端接口进行交互
六、《锋迷商城》设计及实现—用户管理
6.1 实现流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sADil8TS-1646129708206)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210729152133861.png)]
6.2 后端接口开发
6.2.1 完成DAO操作
-
创建实体类
@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;}
-
创建DAO接口,定义操作方法
package com.lzp.fmmall.dao;import com.lzp.fmmall.entity.User;public interface UserDAO {// 用户注册public int insertUser(User user);// 根据用户名查询用户信息public User queryUserByName(String name); }
-
创建DAO接口的mapper文件并完成配置
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.lzp.fmmall.dao.UserDAO"><insert id="insertUser">insert intousers (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" resultType="userMap">selectuser_id,username,password,nickname,realname,user_img,user_mobile,user_email,user_sex,user_birth,user_regtime.user_modtimefrom userswhere username = #{name}</select> </mapper>
6.2.2 完成Service业务
-
创建service接口
public interface UserService {// 用户注册public ResultVO userRegister(String name, String pwd);// 用户登录public ResultVO checkLogin(String name, String pwd); }
-
创建service接口实现类,完成业务实现
@Service public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Transactionalpublic ResultVO userRegister(String name, String pwd) {// 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);}}@Overridepublic 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提供接口
-
创建controller,调用service
-
添加接口注解
package com.lzp.fmmall.controller;import com.lzp.fmmall.service.UserService; import com.lzp.fmmall.vo.ResultVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*;/*** @Author LZP* @Date 2021/7/27 19:06* @Version 1.0*/ @RestController @RequestMapping("/user") @Api(value = "提供用户的登录与注册接口", tags = "用户管理") public class UserController {@Autowiredprivate 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("password") String pwd) {return userService.checkLogin(name, pwd);}@ApiOperation("用户注册接口")@ApiImplicitParams({@ApiImplicitParam(dataType = "string", name = "username", value = "用户注册账号", required = true),@ApiImplicitParam(dataType = "string", name = "password", value = "用户注册密码", required = true)})@PostMapping("/register")public ResultVO register(String username, String password) {return userService.userRegister(username, password);} }
6.2.4 接口测试
- 基于swagger2进行测试
- 测试地址:http://localhost:8080/doc.html
6.3 前端功能实现
6.3.1 跨域访问概念
-
什么是跨域访问?
AJAX 跨域访问是用户访问A网站时所产生的对B网站的跨域访问请求均提交到A网站的指定页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6utdMcWq-1646129708209)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210730132631230.png)]
6.3.2 如何解决跨域访问?
- 前端使用JSONP设置
- 后端使用
@CrossOrigin
——就是设置响应头允许跨域
6.4 前端页面之间的传值
6.4.1 cookie
-
工具方法封装:
// 定义键值对之间的分隔符 var operator = "=";// 根据键获取cookie的值 function getCookieValue(key) {// 获取cookie字符串对象var cookieStr = document.cookie;// 按'; '切割cookieStr字符串var array = cookieStr.split("; ");for (var i = 0; i < array.length; i++) {// 按'='切割键值对var kvStr = array[i].split(operator);var k = kvStr[0];var v = kvStr[1];if (k === key) {return v;}}return null; }// 将键值对保存到cookie中 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页面
// JSON.stringify()函数:将对象转成json格式字符串 localStorage.setItem("user", JSON.stringify(userInfo));
-
B页面
var jsonStr = localStorage.getItem("user"); // eval()函数:将json格式字符串转为对象 var userInfo = eval("(" + jsonStr + ")"); // 取到值之后,立马将其移除 localStorage.removeItem("user");
七、前后端分离开发-用户认证
7.1 基于session实现单体项目用户认证
在单体项目中如何保证受限资源在用户未登录的情况下不允许访问?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vICS2ht-1646129708211)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210805090319162.png)]
在单体项目中,视图资源(页面)和接口(控制器)都在同一台服务器,用户的多次请求都是基于同一个会话(session),因此可以借助session来进行用户认证判断:
- 当用户登录成功之后,将用户信息存放到session
- 当用户再次访问受限资源时,验证session中是否存在用户信息,可以根据session有无用户信息来判断用户是否登录
7.2 基于token实现前后端分离用户认证
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WcX7QXGF-1646129708212)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210805104951376.png)]
7.3 基于token的用户认证实现
7.3.1 登录认证接口生成token
// UserController
@GetMapping("/login")
public ResultVO login(@RequestParam("username") String name,@RequestParam("password") String pwd) {return userService.checkLogin(name, pwd);
}
// UserServiceImpl
@Override
public ResultVO checkLogin(String name, String pwd) {Example userExample = new Example(Users.class);Example.Criteria userCriteria = userExample.createCriteria();userCriteria.andEqualTo("username", name);List<Users> users = usersMapper.selectByExample(userExample);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 + "LZP123456");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 () {// 特判:防止用户在浏览器保存了登录信息,即此时一打开登录页面账号和密码就已经存在this.checkLoginInfo();if (!vm.isRight) {// 校验不通过,给出提示信息vm.tip = "校验失败:" + vm.tip;} else {var url = baseUrl + "user/login";// 验证通过,提交信息到后端// 通过axios发送异步请求axios({method: "GET",url: url,params: {username: vm.username,password: vm.password}}).then(res => {// 校验后台是否登录成功var vo = res.data;if (vo.code === 10000) {// 如果登录成功,就把token存储到cookiesetCookieValue("token", vo.msg);// 跳转到index页面window.location.href = "index.html";} else {// 后台登录失败,给出提示信息vm.tip = vo.msg;}});}
}
7.3.3 购物车页面加载时访问购物车列表接口
-
获取token
-
携带token访问接口
<script type="text/javascript">var baseUrl = "http://localhost:8080/";var vm = new Vue({el: "#container",data: {token: ""},created: function () {// 从cookie中获取tokenthis.token = getCookieValue("token");console.log("token:" + this.token);// 发送请求axios({method: "GET",url: baseUrl + "shopcar/list",params: {token: this.token}}).then(res => {console.log(res);});}}); </script>
7.3.4 在购物车列表接口校验token
@GetMapping("/list")
@ApiImplicitParam(dataType = "string", name = "token", value = "授权令牌", required = true)
public ResultVO listCars(String token) {// 1、获取token// 2、校验tokenif (token == null) {return new ResultVO(ResStatus.NO, "请先登录!", null);}String decode = Base64Utils.decode(token);if (decode.endsWith("LZP123456")) {// token校验成功return new ResultVO(ResStatus.OK, "success", null);}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的结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CR0vBIzx-1646129708213)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210805142631751.png)]
7.4.2 生成JWT
-
添加依赖
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version> </dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <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)) // 设置token过期时间.signWith(SignatureAlgorithm.HS256, "LZP123456") // 设置加密方式和加密密码.compact();
7.4.3 JWT校验
-
如果token正确则正常解析,如果token不正确或者过期,则通过抛出的异常进行识别
try {// 验证tokenJwtParser parser = Jwts.parser();parser.setSigningKey("LZP123456"); // 解析token的signingKey必须和生成token时设置密码一致 "LZP123456"// 如果token 正确(密码正确,有效期内)则正常执行、否则抛出异常Jws<Claims> claimsJws = parser.parseClaimsJws(token);Claims body = claimsJws.getBody(); // 获取token中用户数据String subject = body.getSubject(); // 获取生效token设置的subjectString v1 = body.get("key1", String.class); // 获取生成token时存储的Claims的map中的值System.out.println("subject:" + subject);System.out.println("v1:" + v1);// token校验成功return new ResultVO(ResStatus.OK, "success", null); } catch (ExpiredJwtException e) {return new ResultVO(ResStatus.NO, "登录过期,请重新登录!", null); } catch (UnsupportedJwtException e) {return new ResultVO(ResStatus.NO, "Token不合法,请自重!", null); } catch (Exception e) {return new ResultVO(ResStatus.NO, "请重新登录!", null); }
7.4.3 拦截器校验Token
-
创建拦截器
package com.lzp.fmmall.intercept;import com.fasterxml.jackson.databind.ObjectMapper; import com.lzp.fmmall.vo.ResStatus; import com.lzp.fmmall.vo.ResultVO; import io.jsonwebtoken.*; import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;/*** @Author LZP* @Date 2021/8/5 16:24* @Version 1.0*/ public class CheckTokenInterceptor implements HandlerInterceptor {@Overridepublic 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);return false;} else {try {//验证tokenJwtParser parser = Jwts.parser();parser.setSigningKey("LZP123456");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 {@Autowiredprivate CheckTokenInterceptor checkTokenInterceptor;@Overridepublic 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 + "shopcar/list",headers: {token: this.token}
}).then(res => {console.log(res);
});
7.5.2 在拦截器重放行options请求
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 处理预检// 放行options请求String method = request.getMethod();if ("OPTIONS".equalsIgnoreCase(method)) {return true;}// 通过请求头参数获取tokenString token = request.getHeader("token");System.out.println("---------------" + token);if (token == null) {ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录", null);// 提示请求登录doResponse(response, resultVO);} else {try {//验证tokenJwtParser parser = Jwts.parser();parser.setSigningKey("LZP123456");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 实现流程分析
- 流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IojJfYHV-1646129708214)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210806154325715.png)]
8.2 完成后台接口开发
8.2.1 数据库操作实现
-
分析数据表结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3I5trhj2-1646129708215)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210806162717891.png)]
-
添加测试数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aN9Z4Xw7-1646129708216)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210806164116830.png)]
-
编写sql语句
selectimg_id,img_url,img_bg_color,prod_id,category_id,index_type,seq,status,create_time,update_timefrom 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">selectimg_id,img_url,img_bg_color,prod_id,category_id,index_type,seq,status,create_time,update_timefrom index_imgwhere `status` = 1ORDER BY seq;</select>
8.2.2 业务层实现
-
IndexImgService接口
public interface IndexImgService {public ResultVO listIndexImgs();}
-
IndexImgServiceImpl实现类
@Service public class IndexImgServiceImpl implements IndexImgService {@Autowiredprivate IndexImgMapper indexImgMapper;@Overridepublic 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类
package com.lzp.fmmall.controller;import com.lzp.fmmall.service.IndexImgService; import com.lzp.fmmall.vo.ResultVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** @Author LZP* @Date 2021/8/6 16:53* @Version 1.0*/ @RestController @CrossOrigin @RequestMapping("/index") @Api(value = "提供首页数据显示所需的接口", tags = "首页管理") public class IndexController {@Autowiredprivate IndexImgService indexImgService;@GetMapping("/indeximg")@ApiOperation("首页轮播图接口")public ResultVO listIndexImgs() {return indexImgService.listIndexImgs();}}
8.3 完成前端功能
当进入到index.html,在进行页面初始化之后,就需要请求轮播图数据进行轮播图的显示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZzzhB8S-1646129708217)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210806183041664.png)]
九、首页—分类列表
9.1 实现流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I648WZdL-1646129708218)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210807131307420.png)]
- 方案一:一次性查询三级分类
- 优点:只需要一次查询,根据一级分类显示二级分类时响应速度较快
- 缺点:数据库查询效率较低,页面首次加载的速度也相对较慢
- 方案二:先只查询一级分类,用户点击/鼠标移动到一级分类,动态加载二级分类
- 优点:数据库查询效率提高,页面首次加载速度提高
- 缺点:需要多次连接数据库
9.2 接口开发
9.2.1 数据库操作实现
- 数据表结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dOsmZiqA-1646129708219)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210807131907731.png)]
-
添加测试数据
-
编写接口实现所需的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' fromcategory c1 INNER JOINcategory c2 on c2.parent_id = c1.category_id LEFT JOINcategory c3 on c3.parent_id = c2.category_id where c1.category_level = 1
-
子查询
-- 根据父级分类的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> {// 连接查询public List<CategoryVO> selectAllCategories();// 子查询,根据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.lzp.fmmall.dao.CategoryMapper"><resultMap id="BaseResultMap" type="com.lzp.fmmall.entity.Category"><!--WARNING - @mbg.generated--><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.lzp.fmmall.entity.CategoryVO"><!--WARNING - @mbg.generated--><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.lzp.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.lzp.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">selectc1.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'fromcategory c1INNER JOINcategory c2onc2.parent_id = c1.category_idLEFT JOINcategory c3onc3.parent_id = c2.category_idwherec1.category_level = 1</select><!-------------------------------------------><!-- 子查询优势:无论有多少层级,都能查询的到 --><resultMap id="categoryVOMap2" type="com.lzp.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.lzp.fmmall.dao.CategoryMapper.selectAllCategories2"/></resultMap><!-- 根据父级分类的id查询子级分类 --><select id="selectAllCategories2" resultMap="categoryVOMap2">selectcategory_id,category_name,category_level,parent_id,category_icon,category_slogan,category_pic,category_bg_colorfromcategorywhereparent_id = #{parentId}</select> </mapper>
9.2.2 业务层实现
-
CategoryService接口
package com.lzp.fmmall.service;import com.lzp.fmmall.vo.ResultVO;public interface CategoryService {public ResultVO listCategories();}
-
CategoryServiceImpl
@Service public class CategoryServiceImpl implements CategoryService {@Autowiredprivate CategoryMapper categoryMapper;@Overridepublic 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 listCategory() {return categoryService.listCategories(); }
9.3 前端功能实现
见源码即可
十、首页—商品推荐
10.1 流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sz4M3ea3-1646129708220)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210807185246390.png)]
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 = 1;
-
在beans子工程entity包创建ProductVO,并在其中添加一个字段imgs,用来存储商品的图片集合
public class ProductVO [private List<ProductImg> imgs;public List<ProductImg> getImgs() { return imgs;}public void setImgs(List<ProductImg> imgs) {this.imgs = imgs;} ]
10.2.2 DAO
-
ProductMapper
@Repository public interface ProductMapper extends GeneralDAO<Product> {// 查询推荐商品/*推荐算法:1、根据用户最近搜索商品进行查询2、根据商品销量进行查询3、根据后台管理员指定4、根据最新上架商品进行查询*/public List<ProductVO> selectRecommendProducts();}
-
ProductMapper映射文件
<resultMap id="ProductVOMap" type="com.lzp.fmmall.entity.ProductVO"><!--WARNING - @mbg.generated--><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" column="product_id" select="com.lzp.fmmall.dao.ProductImgMapper.selectProductImgsByProductId"/></resultMap><select id="selectRecommendProducts" resultMap="ProductVOMap">SELECTproduct_id,product_name,category_id,root_category_id,sold_num,product_status,content,create_time,update_timefromproductORDER BYcreate_time DESCLIMIT 0, 3</select>
-
ProductImgMapper
public interface ProductImgMapper extends GeneralDAO<ProductImg> {// 根据商品id查询商品对应的图片集合public List<ProductImg> selectProductImgsByProductId();}
-
ProductImgMapper映射文件
<!-- resultMap用逆向工程自动生成的 --> <select id="selectProductImgsByProductId" resultMap="BaseResultMap">selectid,item_id,url,sort,is_main,created_time,updated_timeFROMproduct_imgWHEREitem_id = #{producId};</select>
10.2.3 业务层
-
ProductService
public interface ProductService {public ResultVO listRecommendProducts();}
-
ProductServiceImpl
@Service public class ProductServiceImpl implements ProductService {@Autowiredprivate ProductMapper productMapper;@Overridepublic ResultVO listRecommendProducts() {List<ProductVO> productVOS = productMapper.selectRecommendProducts();ResultVO resultVO = new ResultVO(ResStatus.OK, "success", productVOS);return resultVO;} }
10.2.4 控制层
直接在IndexController中编写接口
@Autowired
private ProductService productService;@GetMapping("/recommend-products")
@ApiOperation("商品推荐查询接口")
public ResultVO listRecommendProducts() {return productService.listRecommendProducts();
}
10.3 前端实现
见源码即可
十一、首页—分类商品推荐
按照商品的分类推荐销量最高的6个商品
11.1 流程分析
加载分类商品推荐有两种实现方案:
方案一:当加载首页面时不加载分类的推荐商品,监听进度条滚动事件,当进度条触底(滚动指定的距离)就触发分类推荐商品的加载,每次只加载一个分类的商品。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cO0Am8Xh-1646129708222)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808101008256.png)]
方案二:一次性加载所有分类的推荐商品,整体进行初始化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NUTT0o7K-1646129708223)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808101145780.png)]
11.2 接口开发
11.2.1 数据库实现
-
数据准备
-- 添加商品
-
查询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 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 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-75vcPIzf-1646129708224)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808154021063.png)] ProductMapper [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obOy3V5k-1646129708225)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808154622563.png)] -
映射文件
ProductMapper.xml [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9slphuB-1646129708226)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808155521505.png)] CategoryMapper.xml [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFNWizyD-1646129708227)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210808161114825.png)]
11.2.2 业务层实现
-
CategoryService接口
public interface CategoryService {public ResultVO listFirstLevelCategories();}
-
CategoryServiceImpl实现类
/*** 查询所有一级分类,同时查询当前分类下销量最高的6个商品* @return*/ @Override public ResultVO listFirstLevelCategories() {List<CategoryVO> categoryVOS = categoryMapper.selectFirstLevelCategories();ResultVO resultVO = new ResultVO(ResStatus.OK, "success", categoryVOS);return resultVO; }
11.2.3 控制层实现
11.3 前端实现
十二、商品详情展示—显示商品基本信息
点击首页推荐的商品、轮播图商品广告、商品列表页面点击商品,就会进入到商品的详情页面
12.1 流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-avWUWB8R-1646129708228)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210809085810869.png)]
12.2 接口实现
12.2.1 商品详情接口
商品基本信息、商品套餐、商品图片
-
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完成
-
业务层实现
@Transactional(propagation = Propagation.SUPPORTS) @Override public ResultVO getProductBasicInfo(String productId) {// 1、商品基本信息Example example = new Example(Product.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("productId", productId);criteria.andEqualTo("productStatus", 1); // 状态为1表示上架商品List<Product> products = productMapper.selectByExample(example);if (products.size() > 0) {// 2、商品图片Example example1 = new Example(ProductImg.class);Example.Criteria criteria1 = example1.createCriteria();criteria1.andEqualTo("itemId", productId);List<ProductImg> productImgs = productImgMapper.selectByExample(example1);// 3、商品套餐Example example2 = new Example(ProductSku.class);Example.Criteria criteria2 = example2.createCriteria();criteria2.andEqualTo("productId", productId);criteria2.andEqualTo("status", 1);List<ProductSku> productSkus = productSkuMapper.selectByExample(example2);Map<String, Object> basicInfo = new HashMap<>(3);basicInfo.put("product", products.get(0));basicInfo.put("productImgs", productImgs);basicInfo.put("productSkus", productSkus);return new ResultVO(ResStatus.OK, "success", basicInfo);} else {return new ResultVO(ResStatus.NO, "查询的商品不存在!", null);} }
-
控制层实现
@RestController @CrossOrigin @RequestMapping("/product") @Api(value = "提供商品信息相关的接口", tags = "商品管理") public class ProductController {@Autowiredprivate ProductService productService;@ApiOperation("商品基本信息查询接口")@GetMapping("/detail-info/{pid}")public ResultVO getProductBasicInfo(@PathVariable("pid") String pid) {return productService.getProductBasicInfo(pid);}}
12.3 前端实现
十三、商品详情展示—显示商品参数信息
-
业务层实现
package com.lzp.fmmall.service.impl;import com.lzp.fmmall.dao.ProductParamsMapper; import com.lzp.fmmall.entity.ProductParams; import com.lzp.fmmall.service.ProductParamsService; import com.lzp.fmmall.vo.ResStatus; import com.lzp.fmmall.vo.ResultVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tk.mybatis.mapper.entity.Example;import java.util.List;/*** @Author LZP* @Date 2021/8/10 9:43* @Version 1.0*/ @Service public class ProductParamsServiceImpl implements ProductParamsService {@Autowiredprivate ProductParamsMapper productParamsMapper;@Overridepublic ResultVO getProductParamsInfo(String productId) {Example example = new Example(ProductParams.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("productId", productId);List<ProductParams> productParams = productParamsMapper.selectByExample(example);if (productParams.size() > 0) {return new ResultVO(ResStatus.OK, "success", productParams.get(0));} else {return new ResultVO(ResStatus.NO, "该商品可能是三无产品!", null);}} }
-
控制层实现
@Autowired private ProductParamsService productParamsService;@ApiOperation("商品参数信息查询接口") @GetMapping("/detail-params/{pid}") public ResultVO getProductParamsInfo(@PathVariable("pid") String pid) {return productParamsService.getProductParamsInfo(pid); }
-
前端实现
十四、商品详情显示—显示商品评论信息
-
数据库ti添加数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M24UdK8z-1646129708229)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810142534399.png)]
-
sql
# 联表查询 评论表和用户表 SELECTu.user_id,u.username,u.nickname,u.user_img,c.comm_id,c.product_id,c.product_name,c.order_item_id,c.is_anonymous,c.comm_type,c.comm_level,c.comm_content,c.comm_imgs,c.sepc_name,c.reply_status,c.reply_content,c.reply_time,c.is_show FROM users u INNER JOINproduct_comments c ON u.user_id = c.user_id where c.product_id = 3 and c.is_show = 1
-
业务层实现
- 接口
package com.lzp.fmmall.service;import com.lzp.fmmall.vo.ResultVO;public interface ProductCommentsService {public ResultVO getProductComments(String productId);}
- 实现类
package com.lzp.fmmall.service.impl;import com.lzp.fmmall.dao.ProductCommentsMapper; import com.lzp.fmmall.entity.ProductCommentsVO; import com.lzp.fmmall.service.ProductCommentsService; import com.lzp.fmmall.vo.ResStatus; import com.lzp.fmmall.vo.ResultVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tk.mybatis.mapper.entity.Example;import java.util.List;/*** @Author LZP* @Date 2021/8/10 14:34* @Version 1.0*/ @Service public class ProductCommentsServiceImpl implements ProductCommentsService {@Autowiredprivate ProductCommentsMapper productCommentsMapper;@Overridepublic ResultVO getProductComments(String productId) {List<ProductCommentsVO> productCommentsVOS = productCommentsMapper.selectProductComments(productId);return new ResultVO(ResStatus.OK, "success", productCommentsVOS);} }
-
控制层实现
@Autowired private ProductCommentsService productCommentsService;@ApiOperation("商品评论信息查询接口") @GetMapping("/detail-comments/{pid}") public ResultVO getProductCommentsInfo(@PathVariable("pid") String pid) {return productCommentsService.getProductComments(pid); }
-
前端实现
十五、商品详情展示—商品评论分页及统计信息
15.1 流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EVUKXUyO-1646129708231)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810165054971.png)]
15.2 接口开发
15.2.1 改成商品评论列表接口
分页查询
-
定义PageHelper
package com.lzp.fmmall.utils;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.util.List;/*** @Author LZP* @Date 2021/8/10 16:52* @Version 1.0*/ @Data @NoArgsConstructor @AllArgsConstructor public class PageHelper<T> {// 总记录数private int count;// 总页数private int pageCount;// 分页数据private List<T> list;}
-
改造数据库操作
ProductCommentsMapper [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dX7QsmYS-1646129708232)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810171945565.png)] ProductCommentsMapper.xml映射配置文件 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cpbAYBA-1646129708233)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810172044122.png)] -
改造业务逻辑层
ProductCommentsService [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeX79mNp-1646129708233)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810172734418.png)] ProductCommentsServiceImpl [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Ei95w3e-1646129708235)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210810172828944.png)]
15.2.2 评价统计接口实现
-
数据库实现
- 统计当前商品的总评论数
- 统计当前商品的好评/中评/差评
-
业务层
-
接口
/*** 根据商品id查询当前商品的总评论数* @param productId* @return*/ public ResultVO getCommentsCountByProductId(String productId);
-
实现类
@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、查询好评数Example example1 = new Example(ProductComments.class);Example.Criteria criteria1 = example1.createCriteria();criteria1.andEqualTo("productId", productId);criteria1.andEqualTo("commType", 1);int goodTotal = productCommentsMapper.selectCountByExample(example1);// 3、查询中评数Example example2 = new Example(ProductComments.class);Example.Criteria criteria2 = example2.createCriteria();criteria2.andEqualTo("productId", productId);criteria2.andEqualTo("commType", 0);int midTotal = productCommentsMapper.selectCountByExample(example2);// 4、查询差评数Example example3 = new Example(ProductComments.class);Example.Criteria criteria3 = example3.createCriteria();criteria3.andEqualTo("productId", productId);criteria3.andEqualTo("commType", -1);int badTotal = productCommentsMapper.selectCountByExample(example3);// 5、计算好评率String p = String.valueOf(Double.parseDouble(String.valueOf(goodTotal)) / Double.parseDouble(String.valueOf(total)) * 100);// 小数点后保留两位小数String percent = p.substring(0, p.lastIndexOf(".") + 3);Map<String, Object> map = new HashMap<>(5);map.put("total", total);map.put("goodTotal", goodTotal);map.put("midTotal", midTotal);map.put("badTotal", badTotal);map.put("percent", percent);return new ResultVO(ResStatus.OK, "success", map); }
-
-
控制层
@Autowired private ProductCommentsService productCommentsService;@ApiOperation("商品评论统计信息查询接口") @GetMapping("/detail-comments-count/{pid}") public ResultVO getCommentsCountByProductId(@PathVariable("pid") String pid) {return productCommentsService.getCommentsCountByProductId(pid); }
15.3 前端实现
十六、购物车—添加购物车(已登录状态)
略
十七、购物车—添加购物车(未登录状态)
17.1 流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7iKj9Fpq-1646129708236)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210811141800270.png)]
17.2 接口实现
17.2.1 修改购物车数据表结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYOv0lV6-1646129708237)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210811141909373.png)]
- 数据表修改完成之后,对此表重新进行逆向工程,这里要将逆向工程的配置文件里面生成表的策略里的’%'改成具体的表名,否则会对其他表造成影响
17.2.2 业务层
-
定义一个购物车业务类ShoppingCarService接口
public interface ShoppingCarService {public ResultVO addShoppingCar(ShoppingCar car);}
-
实现类ShoppingCarServiceImpl
@Service public class ShoppingCarServiceImpl implements ShoppingCarService {@Autowiredprivate ShoppingCarMapper shoppingCarMapper;@Overridepublic ResultVO addShoppingCar(ShoppingCar car) {int i = shoppingCarMapper.insert(car);if (i > 0) {return new ResultVO(ResStatus.OK, "success", null);} else {return new ResultVO(ResStatus.NO, "fail", null);}} }
17.3 前端展示
introduction.html
addShoppingCar() {/*注意:这里受限资源需要判断当前用户是否登录,而判断是否登录不是简简单单的判断以下token是否为null或者空字符串""就行的因为我们此时用的是jwt,是后台自动生成的token,只有后台知道当前的token是否有效,(token怎样才算有效:具有正确性、时效性)正因如此所以要通过后台去完成校验*/var url5 = baseUrl + "shopcar/add";// 获取从cookie中获取token和userIdvar userId = getCookieValue("userId");// 拼接选中的套餐属性var chooseSkuPropStr = "";for (var key in this.chooseSkuProps) {chooseSkuPropStr += key + ":" + this.chooseSkuProps[key] + ";";}// 发送异步请求axios({url: url5,method: "POST",headers: {token: this.token},data: {"carNum": this.num,"carTime": "","productId": this.productId,"productPrice": this.productSkus[this.currentSkuIndex].sellPrice,"skuId": this.productSkus[this.currentSkuIndex].skuId,"skuProps": chooseSkuPropStr,"userId": userId}}).then(res => {var vo = res.data;if (vo.code == 10000) {// 添加成功layer.msg("添加购物车成功!");} else if (vo.code == 10001) {// 添加失败layer.msg("添加购物车失败!");} else {var tipStr = "";if (vo.code == 20001) {// 没有登录tipStr = "请登录";} else if (vo.code == 20002) {// 登录过期tipStr = "登录过期,请重新登录";}// 跳转到登录页,而且要带参数跳/*参数如下:1、tip(提示信息)2、returnUrl(回跳url)3、pid(商品id)4、currentSkuIndex(当前选中的套餐下标)5、num(当前商品下当前套餐的数量)*/var loginUrl = "login.html?tip=" + tipStr+ "&returnUrl=introduction.html"+ "&pid=" + this.productId+ "&sid=" + this.currentSkuIndex+ "&num=" + this.num;// 对url进行编码window.location.href = encodeURI(loginUrl);}});
}
这里我们使用了一个类似京东的效果,就是用户在没有登录之前是可以查看商品详情的,但是如果想要添加购物车等访问这些一系列受限资源时,需要去判断当前用户是否登录,若已登录,则可以直接添加,反之,则需要跳到登录页面;而这里我们跳到登录页面时传递了一个returnUrl参数,目的就是方便用户登录成功后再回跳到上一次访问的页面,这样一来给用户的体验感会比较好。跳转所要传递的参数如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fl5k2Rrp-1646129708238)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210822092649654.png)]
login.html
doSubmit: function () {// 特判:防止用户在浏览器保存了登录信息,即此时一打开登录页面账号和密码就已经存在this.checkLoginInfo();if (!vm.isRight) {// 校验不通过,给出提示信息vm.tip = "校验失败:" + vm.tip;} else {var url = baseUrl + "user/login";// 验证通过,提交信息到后端// 通过axios发送异步请求axios({method: "GET",url: url,params: {username: vm.username,password: vm.password}}).then(res => {// 校验后台是否登录成功console.log(res);var vo = res.data;if (vo.code == 20000) {// 如果登录成功,就把token存储到cookiesetCookieValue("token", vo.msg);// 将用户id、用户名和用户头像存入cookie中setCookieValue("userId", vo.data.userId);setCookieValue("username", vo.data.username);setCookieValue("userImg", vo.data.userImg);// 先判断,returnUrl是否为nullif (this.returnUrl == null) {// 跳转到index页面window.location.href = "index.html";} else {// 回到returnUrl指定的页面var url = this.returnUrl + "?pid=" + this.pid+ "&sid=" + this.sid+ "&num=" + this.num;window.location.href = url;}} else {// 后台登录失败,给出提示信息vm.tip = vo.msg;}});}
}
登录成功之后的逻辑我们也进行了相应的修改,如果当前页面获取到的returnUrl不为null,即表示用户此时是从某个受限资源页面跳转过来的,等登录成功后必须回跳到之前的页面,反之,就跳到index.html(主页)即可。
修改后的跳转逻辑代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YDlAlvoL-1646129708239)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210822093140884.png)]
十八、购物车—购物车列表
18.1 流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-akMLyoDA-1646129708240)(C:\Users\14715\AppData\Roaming\Typora\typora-user-images\image-20210822093808979.png)]