打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
从0到1搭建精品电商项目(用于毕设、简历等)—— 项目介绍与初步搭建(1)

文章目录

1 设计与准备工作

1.1 概述

  不少的同学在正式找工作前肯定都接触过了Java著名的SSM框架,当然SpringBoot和SpringCloud相信很多人也都了解过,前一段时间我抽空完完整整的完成了一个基于这些主流技术的“项目”,并且体验了一把将项目部署到云服务器上,从此调试不再是localhost了而是我自己的域名,对于现阶段来讲确实是一件很酷的事情,幸运的是在完成的过程中我完整的保留了每一步开发的笔记,现做整理如下,希望可以帮助到需要的同学:

  1. 基于Springboot2.x实现单体架构设计与准备工作;
  2. 实现电商项目核心功能与个人中心功能;
  3. 部署到腾讯云的详细步骤。

高并发系统的演进应该是循序渐进,以解决系统中存在的问题为目的和驱动力的,我们需要先从最基础的单体项目开始搭建。

部分效果展示:



友情提示:一入此坑深似海,没有几个星期的功夫是肯定没办法完成的,我的文章也会分成好几篇来写,前方高能,请想好了再上车!!

老规矩,源码请联系公众号:

  本文只提供后端代码的详细编写流程,前端源码不是我们要关注的重点,文内只会给出片段,想要页面展示效果的也请联系公众号,仅仅使用PostMan测接口的话不需要前端源码。

  接口文档api:http://wjwqxy.cn:8088/foodie-dev-api/doc.html

  同样的,数据库设计的也比较简单,可以自己动手或者找我要~

话不多说,下面正式开始。


1.2 技术栈

SpringMVC 对比 SpringBoot:

  • SpringMVC是框架,而SpringBoot是工具,其中整合了很多工具,使用pom来实现依赖,主要实现了自动配置。
  • 前者配置繁琐(xml),后者是零配置(yml)。
  • SpringBoot集成了多样化中间件[XXX-stater]。
  • 从外置tomcat变为内置tomcat。

前端技术选型:

  • MVVM开发模式(vue和小程序等),通过数据双向绑定来渲染页面而不是使用DOM来操作节点
  • Jquery、vue渐进式框架,可以逐步将Jquery用vue替代
  • html
  • css

采用前后端分离的开发模式:
  浏览器请求的是反向代理服务器nginx中的静态页面,nginx可以通过Restful访问另外一台服务器

1.3 项目拆分与聚合

  使用Maven项目管理工具,项目名称为foodie-dev,拆分出通用模块common,类模块pojo,映射器模块mapper,服务模块service和控制器模块controller等。模块之间使用pom文件关联起来。



1.4 构建聚合工程

  创建foodie-dev-common模块、foodie-dev-pojo模块。

foodie-dev-pojo依赖foodie-dev-common

<dependency>
    <groupId>com.wjw</groupId>
    <artifactId>foodie-dev-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

创建foodie-dev-mapper模块,依赖pojo

<!-- mapper -> pojo -> commmon -->
<dependency>
    <groupId>com.wjw</groupId>
    <artifactId>foodie-dev-pojo</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

创建foodie-dev-service模块,依赖mapper

<!-- service -> mapper -> pojo -> commmon -->
<dependency>
    <groupId>com.wjw</groupId>
    <artifactId>foodie-dev-mapper</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

创建foodie-dev-api模块(控制层),依赖service

<!-- api -> service -> mapper -> pojo -> commmon -->
<dependency>
    <groupId>com.wjw</groupId>
    <artifactId>foodie-dev-service</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

但实际上api不应该调用mapper

  所有的创建完毕之后要执行foodie-dev的Maven的安装命令。

1.5 数据库相关


这里没有使用外键,原因如下:

  1. 会有一定的性能影响。
  2. 如果要进行热更新(不停机维护),如果有外键可能会导致新更新的代码无法运行,因为要去匹配到现有的外键,所以可能要重启服务器。
  3. 删除物理外键可以降低耦合度。
  4. 数据库的分库分表有外键的话很难实现。

1.6 项目整合SpringBoot

在父工程foodie-dev的pom文件中修改

1. 引入依赖 parent

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath />
</parent>

之后子模块就可以不用单独制定版本号了。

2. 设置资源属性

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

3. 引入依赖 dependency

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!--解析除了yml以外的配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

创建foodie-dev-api的配置文件application.yml
和启动类:

package com.wjw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/20 15:11
 * 4
 */
@SpringBootApplication
@MapperScan(basePackages = "com.wjw.mapper")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

创建controller包下的类HelloController:
  使用@RestController注解使得返回的对象为json格式

package com.wjw.controller;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/20 16:29
 * 4
 */
@RestController
@ApiIgnore
public class HelloController {

    @GetMapping("/hello")
    @Transactional
    public Object hello() {
        return "Hello World";
    }

}

1.7 SpringBoot自动装配简述

为什么SpringBoot可以近乎0配置?

  • 主要就是基于其自动装配特性。

默认的设置来自于@SpringBootApplication注解,点进该注解中:

我的启动类是在com.wjw包下,当主函数运行后,会自动扫描包下的所有类

@SpringBootConfiguration注解是一个接口被@Configuration修饰,表示它是一个容器:


@EnableAutoConfiguration是开启自动装配的,@Import是用于做导入的,并且导入的是一个个的Configuration(即容器)

AutoConfigurationImportSelector是一个选择器,类比jquery,可以选择自动装配的类

getAutoConfigurationEntry用于自动装配


configurations被存在于一个List,来自getCandidateConfigurations方法

spring.factories文件中存了一些自动装配的类



中可以看到内置的tomcat


中内置的是SpringMVC等等等等

1.8 HikariCP数据源与Mybatis整合

导入数据库文件…(略)

HikariCP文档

1. 父工程foodie-dev的pom中引入数据源驱动与mybatis依赖

<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.41</version>
</dependency>
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

2. 在doofie-dev-api项目的yml中配置数据源和mybatis

最大连接数经验上是核数的1.5倍

############################################################
#
# 配置数据源信息
#
############################################################
spring:
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource # 数据源类型:HikariCP
    driver-class-name: com.mysql.jdbc.Driver # mysql驱动
    url: jdbc:mysql://localhost:3306/foodie-shop-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect
    username: root
    password: root
    hikari:
      connection-timeout: 30000 # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQ
      minimum-idle: 5 # 最小连接数
      maximum-pool-size: 20 # 最大连接数
      auto-commit: true # 自动提交
      idle-timeout: 600000 # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP # 连接池名字
      max-lifetime: 1800000 # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟
      connection-test-query: SELECT 1

############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.wjw.pojo # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml # mapper映射文件

  同时,在foodie-dev-mapper中创建com.wjw.mapper包,resources中创建mapper包
  在foodie-dev-pojo中创建com.wjw.pojo包

3. 内置tomcat

############################################################
#
# web访问端口号 约定:8088
#
############################################################
server:
  port: 8088
  tomcat:
    uri-encoding: UTF-8
  max-http-header-size: 80KB

1.9 MyBatis 数据库逆向生成工具

1.9.1 代码自动生成


打开我提供的另外一个项目,代码自取

运行utils包下的GeneratorDisplay文件,按照注释做填空题,会根据配置文件来生成mapper和pojo:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.wjw.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/foodie-shop-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.wjw.pojo" targetProject="src/main/java"/>

      <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>

      <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.wjw.mapper" targetProject="src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
      <table tableName="carousel"></table>
        <table tableName="category"></table>
        <table tableName="items"></table>
        <table tableName="items_comments"></table>
        <table tableName="items_img"></table>
        <table tableName="items_param"></table>
        <table tableName="items_spec"></table>
        <table tableName="order_items"></table>
        <table tableName="order_status"></table>
        <table tableName="orders"></table>
        <table tableName="user_address"></table>
        <table tableName="users"></table>

    </context>
</generatorConfiguration>

把自动生成的mapper、pojo、resources/mapper文件夹拷贝到我们的主项目中。


第一个导入foodie-dev-mapper的com.wjw.mapper包下
第三个导入foodie-dev-mapper的resources/mapper下
第二个导入foodie-dev-pojo的com.wjw.pojo包下

1.9.2 完善项目

  1. 在父工程pom中引入通用mapper工具
<!-- 通用mapper逆向工具 -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>
  1. 在foodie-dev-api的yml中引入通用mapper配置
############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.wjw.my.mapper.MyMapper
  not-empty: false  # 在进行数据库操作时,判断表达式 username !=null,是否追加 username != ''
  identity: MYSQL
  1. 在foodie-dev-mapper项目中引入MyMapper接口类
package com.wjw.my.mapper;

import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;

/**
 * 继承自己的MyMapper
 */
public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> {
}

运行Application启动成功。

1.10 通用Mapper接口所封装的常用方法

  1. 首先先来看一下MyMapper 所继承的父类,如:
interface MyMapper<T> extends Mapper<T>, MySqlMapper<T>

这里有两个父类, Mapper 与MySqlMapper ,我们可以打开MySqlMapper 看一下:

public interface MySqlMapper<T> extends InsertListMapper<T>, InsertUseGeneratedKeysMapper<T> {
}

这里面又继承了了两个mapper,从类名上可以看得出来,是用于操作数据库的,这两个类里又分别包含了如下方法,简单归类一下:


很明显,在传统JavaWeb开发,这两个方法使用是没有问题的,但是我们的数据库表主键设计肯定是全局唯一的,所以不可能使用自增长id,所以这两个方法在我们开发过程中是不会使用的。

  1. 随后再来看一下Mapper 中所继承的父类,如下:
public interface Mapper<T> extends BaseMapper<T>, ExampleMapper<T>, RowBoundsMapper<T>, Marker {
}
  • BaseMapper<T>
  • ExampleMapper<T>
    Example类是用于提供给用户实现自定义条件的,也就是where 条件

1.11 关于Restful WebService

  Restful是一种通信方式,在系统与系统之间可以传递相应的消息,客户端与服务器端通信载体也是可以使用Restful WebService的。

  Restful WebService的一个特点是无状态,服务器接收客户端请求时,服务器不需要了解这个request之前做过什么以及将来可能做什么。

  使用Restful之后系统有独立性,可以做拆分。

Rest设计规范:

  • GET,多用于查询 -> /order/{id} -> /getOrder?id=1001
  • POST,多用于更新资源保存信息 -> /order -> /saveOrder
  • PUT,多用于更新资源 -> /order/{id} -> /modifyOrder
  • DELETE -> /order/{id} -> /deleteOrder?id=1001

1.12 详解事务的传播

使用@Transactional注解来实现事务,查看源码:


默认值是Propagation.REQUIRED,即如果当前没有事务则新建一个,修饰的方法必须运行在一个事务中,当前有事务的话会加入到现有的事务中,事务一共有7类:

  • REQUIRED:如果当前有事务,则使用该事务,没有事务自己创建一个。举例:领导没饭吃,我会自己买了吃;领导有的吃,会分给我一起吃。
  • SUPPORT:主要用于查询,修饰的方法如果当前有事务,则使用事务,否则不使用。举例:领导有饭吃,我也有饭吃;领导没饭吃,我也没饭吃。
  • MANDATORY:强制必须存在一个事务,如果不存在,抛出异常。举例:领导必须管饭,否则就出异常。
  • REQUIRES_NEW:如果当前有事务,则挂起该事务,并且自己创建一个新的事务给自己用;如果当前没有事务则同REQUIRED。举例:领导有饭吃,我偏自己买了自己吃。
  • NOT_SUPPORTED:如果当前有事务,则把事务挂起,自己不使用事务去操作数据库。举例:领导有饭吃,我太忙了,放一边不吃。
  • NEVER:如果当前有事务存在,则抛出异常。举例:领导有饭给我吃,抛出异常。
  • NESTED:如果当前有事务,则开启子事务(嵌套事务),嵌套事务是独立提交或回滚;如果当前没有事务,则同REQUIRED;但如果主事务提交,会携带子事务一起提交;如果主事务回滚,则会带着子事务一起回滚。相反,子事务异常,则父事务可以回滚或不回滚(借助try/catch)。举例:领导决策不对,老板怪罪,领导带着小弟一同受罪。小弟出了差错,领导可以推卸责任。

2 用户登录注册模块开发

2.1 注册登录流程

用户名注册登录流程:

邮箱注册流程:


手机号注册登录流程:

2.2 用户注册 - 判断用户名存在

  分布式系统表的主键一般不设为自增,并且多设计成varchar类型的,原因是要保证全局唯一性。

先从service层写起,编辑foodie-dec-service项目:
创建UserService接口:

package com.wjw.service;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/21 17:38
 * 4
 */
public interface UserService {

    /**
     * 判断用户名是否存在
     */
    public boolean queryUsernameIsExist(String username);
    
}

UserService接口的实现类:

package com.wjw.service.impl;

import com.wjw.mapper.UsersMapper;
import com.wjw.pojo.Users;
import com.wjw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/21 17:39
 * 4
 */
public class UserServiceImpl implements UserService {

    @Autowired
    public UsersMapper usersMapper;

    /**
     * 判断用户名是否存在
     *
     * @param username
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    @Override
    public boolean queryUsernameIsExist(String username) {
        Example userExample = new Example(Users.class);
        Example.Criteria userCriteria = userExample.createCriteria();
        userCriteria.andEqualTo("username", username);

        Users result = usersMapper.selectOneByExample(userExample);

        return result != null;
    }
}

在主项目的pom中添加依赖:

<!--apache工具类-->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>1.3.2</version>
</dependency>

在foodie-dev-api项目中定义controller:

package com.wjw.controller;

import com.wjw.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/20 16:29
 * 4
 */
@RestController
@RequestMapping("passport")
public class PassportController {

    @Autowired
    private UserService userService;

    @GetMapping("/usernameIsExist")
    public int usernameIsExist(@RequestParam String username) {
        // 判断用户名不能为空
        if (StringUtils.isBlank(username)){
            return 500;
        }
        // 查找注册的用户名是否存在
        boolean isExist = userService.queryUsernameIsExist(username);
        if (isExist) {
            return 500;
        }
        // 用户名没有重复
        return 200;
    }

}

2.3 自定义响应数据结构

在foodie-dev-common模块中创建com.wjw.utils包,并定义响应数据结构。

package com.wjw.utils;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @Description: 自定义响应数据结构
 *              本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *              前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 * 
 *              200:表示成功
 *              500:表示错误,错误信息在msg字段中
 *              501:bean验证错误,不管多少个错误都以map形式返回
 *              502:拦截器拦截到用户token出错
 *              555:异常抛出信息
 *              556: 用户qq校验异常
 */
public class WJWJSONResult {

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    // 响应业务状态
    private Integer status;

    // 响应消息
    private String msg;

    // 响应中的数据
    private Object data;
    
    @JsonIgnore
    private String ok; // 不使用

    public static WJWJSONResult build(Integer status, String msg, Object data) {
        return new WJWJSONResult(status, msg, data);
    }

    public static WJWJSONResult build(Integer status, String msg, Object data, String ok) {
        return new WJWJSONResult(status, msg, data, ok);
    }
    
    public static WJWJSONResult ok(Object data) {
        return new WJWJSONResult(data);
    }

    public static WJWJSONResult ok() {
        return new WJWJSONResult(null);
    }
    
    public static WJWJSONResult errorMsg(String msg) {
        return new WJWJSONResult(500, msg, null);
    }
    
    public static WJWJSONResult errorMap(Object data) {
        return new WJWJSONResult(501, "error", data);
    }
    
    public static WJWJSONResult errorTokenMsg(String msg) {
        return new WJWJSONResult(502, msg, null);
    }
    
    public static WJWJSONResult errorException(String msg) {
        return new WJWJSONResult(555, msg, null);
    }
    
    public static WJWJSONResult errorUserQQ(String msg) {
        return new WJWJSONResult(556, msg, null);
    }

    public WJWJSONResult() {

    }

    public WJWJSONResult(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
    
    public WJWJSONResult(Integer status, String msg, Object data, String ok) {
        this.status = status;
        this.msg = msg;
        this.data = data;
        this.ok = ok;
    }

    public WJWJSONResult(Object data) {
        this.status = 200;
        this.msg = "OK";
        this.data = data;
    }

    public Boolean isOK() {
        return this.status == 200;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

   public String getOk() {
      return ok;
   }

   public void setOk(String ok) {
      this.ok = ok;
   }

}

之后重新修改刚刚的PassportController

package com.wjw.controller;

import com.wjw.service.UserService;
import com.wjw.utils.WJWJSONResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/20 16:29
 * 4
 */
@RestController
@RequestMapping("passport")
public class PassportController {

    @Autowired
    private UserService userService;

    @GetMapping("/usernameIsExist")
    public WJWJSONResult usernameIsExist(@RequestParam String username) {
        // 判断用户名不能为空
        if (StringUtils.isBlank(username)){
            return WJWJSONResult.errorMsg("用户名不能为空");
        }
        // 查找注册的用户名是否存在
        boolean isExist = userService.queryUsernameIsExist(username);
        if (isExist) {
            return WJWJSONResult.errorMsg("用户名已经存在");
        }
        // 用户名没有重复
        return WJWJSONResult.ok();
    }

}

再次用postman测试刚刚的那个接口:

2.4 用户注册

2.4.1 创建用户service

  只要是前端传向后端接收的数据体,都可以统一的定义为XxxBO

在foodie-dev-pojo项目下创建bo类:

package com.wjw.pojo.bo;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/21 20:41
 * 4
 */
public class UserBO {

    private String username;
    private String password;
    private String confirmPassword;
    
    ......
}

对应注册时填写的数据

在common模块中引入一些工具:
引入MD5加密工具:

package com.wjw.utils;

import org.apache.commons.codec.binary.Base64;

import java.security.MessageDigest;

public class MD5Utils {

   /**
    * 
    * @Title: MD5Utils.java
    * @Package com.wjw.utils
    * @Description: 对字符串进行md5加密
    */
   public static String getMD5Str(String strValue) throws Exception {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      String newstr = Base64.encodeBase64String(md5.digest(strValue.getBytes()));
      return newstr;
   }

   public static void main(String[] args) {
      try {
         String md5 = getMD5Str("wjw");
         System.out.println(md5);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

引入操作日期的工具:

  点击获取

引入表示性别的枚举类:

package com.wjw.enums;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/21 21:04
 * 4
 */
public enum Sex {

    woman(0, "女"),
    man(1, "女"),
    secret(2, "女");

    public final Integer type;
    public final String value;

    Sex(Integer type, String value) {
        this.type = type;
        this.value = value;
    }
}

引入生成全局唯一ID的三个包(以后会详细讨论,现在先用):

  点击获取

不要忘了在启动类上添加注解来扫描这个包。

package com.wjw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/20 15:11
 * 4
 */
@SpringBootApplication
@MapperScan(basePackages = "com.wjw.mapper")
@ComponentScan(basePackages = {"com.wjw", "org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}


创建用户对应的service:

@Autowired
private Sid sid;
/**
 * 创建用户
 *
 * @param userBO
 * @return
 */
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Users createUser(UserBO userBO) {

    String userId = sid.nextShort();

    Users user = new Users();
    user.setId(userId);
    user.setUsername(userBO.getUsername());
    try {
        user.setPassword(MD5Utils.getMD5Str(userBO.getPassword()));
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 默认用户昵称同用户名
    user.setNickname(userBO.getUsername());
    // 默认头像
    user.setFace(USER_FACE);
    // 默认生日
    user.setBirthday(DateUtil.stringToDate("1900-01-01"));
    // 默认性别 保密
    user.setSex(Sex.secret.type);

    user.setCreatedTime(new Date());
    user.setUpdatedTime(new Date());

    usersMapper.insert(user);
    // 返回用于在页面里显示用户基本信息
    return user;
}

2.4.2 创建用户对应的controller与测试

PassportController中创建注册方法

@PostMapping("/regist")
public WJWJSONResult regist(@RequestBody UserBO userBO) {

    String username = userBO.getUsername();
    String password = userBO.getPassword();
    String confirmPwd = userBO.getConfirmPassword();

    // 判断用户名和密码必须不为空
    if (StringUtils.isBlank(username) || StringUtils.isBlank(password) || StringUtils.isBlank(confirmPwd)){
        return WJWJSONResult.errorMsg("用户名或密码不能为空");
    }

    // 查询用户名是否存在
    boolean isExist = userService.queryUsernameIsExist(username);
    if (isExist) {
        return WJWJSONResult.errorMsg("用户名已经存在");
    }

    // 密码长度不能少于6位
    if (password.length() < 6) {
        return WJWJSONResult.errorMsg("密码长度不能少于6");
    }

    // 判断两次密码是否一致
    if (!password.equals(confirmPwd)) {
        return WJWJSONResult.errorMsg("两次密码输入不一致");
    }

    // 实现注册
    userService.createUser(userBO);

    // TODO 生成用户token,存入redis会话
    // TODO 同步购物车数据

    return WJWJSONResult.ok();
}

2.5 整合Swagger2文档api

  为了减少程序员撰写文档时间,提高生产力, Swagger2 应运而生,使用Swagger2 可以减少编写过多的文档,只需要通过代码就能生成文档API,提供给前端人员常方便。
  在父工程pom中添加依赖:

<!-- swagger2 配置 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.4.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.4.0</version>
</dependency>
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.6</version>
</dependency>

在foodie-dev-api项目中创建Swagger配置类:

package com.wjw.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;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/21 22:22
 * 4
 */
@Configuration
@EnableSwagger2
public class Swagger2 {

    // 配置swagger2核心配置
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                     // 用于定义api文档汇总信息
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wjw.controller"))    // 指定controller包
                .paths(PathSelectors.any())             // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("天天吃货电商平台接口api")
                .contact(new Contact("wjw", "https://www.baidu.com", "wangjiawei@smail.nju.edu.cn"))    // 联系人信息
                .description("专为天天吃货提供的api文档")
                .version("1.0.1")
                .termsOfServiceUrl("http://shop.wjwqxy.cn/")
                .build();
    }

}

原路径:http://localhost:8088/swagger-ui.html


swagger-bootstrap-ui提供的界面:http://localhost:8088/doc.html

2.6 优化Swagger2显示

@ApiIgnore可以忽略掉某个controller不在文档中显示

@Api(value = "注册登录", tags = {"用于注册登录的相关接口"})


在方法上面添加

@ApiOperation(value = "用户名是否存在", notes = "用户名是否存在", httpMethod = "GET")
@ApiOperation(value = "用户注册", notes = "用户注册", httpMethod = "POST")



可以在UserBo类上添加注释

@ApiModel(value = "用户对象BO", description = "从客户端,由用户传入的数据封装在此entity中")

在该类的属性上添加注释

@ApiModelProperty(value = "用户名", name = "username", example = "wjw", required = true)
private String username;
@ApiModelProperty(value = "密码", name = "password", example = "123456", required = true)
private String password;
@ApiModelProperty(value = "确认密码", name = "confirmPassword", example = "123456", required = true)
private String confirmPassword;

2.7 使用tomcat运行前端源码

把前端源码拷贝到tomcat文件夹下的webapps里,启动bin文件夹下的startup.bat

2.8 设置跨域配置实现前后端联调

以注册服务为例,先修改接口服务接口地址


在foodie-dev-api中配置跨域:

package com.wjw.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/23 9:43
 * 4
 */
@Configuration
public class CorsConfig {

    public CorsConfig() {
    }

    @Bean
    public CorsFilter corsFilter() {
        // 1. 添加cors配置信息
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:8080");

        // 设置是否发送cookie信息
        config.setAllowCredentials(true);

        // 设置允许请求的方式
        config.addAllowedMethod("*");

        // 设置允许的header
        config.addAllowedHeader("*");

        // 2. 为url添加映射路径
        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/**",config);

        return new CorsFilter(corsSource);
    }
}

测试注册一个名为test的用户发现可以注册成功

2.9 实现用户登录

从service层开始写起,在UserService中添加接口:

/**
 * 检索用户名密码是否匹配,用于登录
 * @param username
 * @param password
 * @return
 */
public Users queryUserForLogin(String username, String password);

在UserServiceImpl中实现接口:

/**
 * 检索用户名密码是否匹配,用于登录
 *
 * @param username
 * @param password
 * @return
 */
@Override
public Users queryUserForLogin(String username, String password) {
    Example userExample = new Example(Users.class);
    Example.Criteria userCriteria = userExample.createCriteria();
    userCriteria.andEqualTo("username", username);
    userCriteria.andEqualTo("password", password);

    Users result = usersMapper.selectOneByExample(userExample);

    return result;
}

  由于登录时没有确认密码,所以UserBo类的定义中confirmPassword字段的required属性设为false


实现相应的controller方法(PassportController中):

@ApiOperation(value = "用户登录", notes = "用户登录", httpMethod = "POST")
@PostMapping("/login")
public WJWJSONResult login(@RequestBody UserBO userBO) throws Exception {

    String username = userBO.getUsername();
    String password = userBO.getPassword();

    // 判断用户名和密码必须不为空
    if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
        return WJWJSONResult.errorMsg("用户名或密码不能为空");
    }

    // 实现登录
    Users userResult = userService.queryUserForLogin(username, MD5Utils.getMD5Str(password));

    if (userResult == null) {
        return WJWJSONResult.errorMsg("用户名或密码不正确");
    }

    return WJWJSONResult.ok(userResult);
}

2.10 cookie与session

2.10.1 cookie

  • 以键值对的形式存储信息在浏览器,一个cookie的大小不能超过4KB
  • cookie不能跨域,当前及其父级域名可以取到该值
  • 可以设置有效期
  • 可以设置path,path就是路由,表明那些路径可以访问到相应的cookie,一般设置为 /

2.10.2 session

  • 基于服务器内存的缓存(非持久化),可保存请求会话
  • 每个session通过sessionid来区分不同请求
  • 可以设置过期时间
  • 以键值对形式存在
 @GetMapping("/setSession")
    public Object setSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.setAttribute("userInfo", "new user");
        session.setMaxInactiveInterval(3600);
        session.getAttribute("userInfo");
//        session.removeAttribute("userInfo");
        return "ok";
    }


2.11 实现用户信息在页面显示

一些私密的信息不需要返回给前端:

/**
 * 保护隐私
 * @param userResult
 * @return
 */
private Users setNullProperty(Users userResult) {
    userResult.setPassword(null);
    userResult.setMobile(null);
    userResult.setEmail(null);
    userResult.setCreatedTime(null);
    userResult.setUpdatedTime(null);
    userResult.setBirthday(null);
    return userResult;
}

导入操作cookie的工具类以及将对象转为Json字符串的工具类:

  CookieUtils.java和JsonUtils

controller中添加登录方法:

@ApiOperation(value = "用户登录", notes = "用户登录", httpMethod = "POST")
@PostMapping("/login")
public WJWJSONResult login(@RequestBody UserBO userBO,
                           HttpServletRequest request,
                           HttpServletResponse response) throws Exception {

    String username = userBO.getUsername();
    String password = userBO.getPassword();

    // 判断用户名和密码必须不为空
    if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
        return WJWJSONResult.errorMsg("用户名或密码不能为空");
    }

    // 实现登录
    Users userResult = userService.queryUserForLogin(username, MD5Utils.getMD5Str(password));
    // 保护隐私
    userResult = setNullProperty(userResult);
    // 设置cookie,最后参数是开启加密
    CookieUtils.setCookie(request, response, "user",
            JsonUtils.objectToJson(userResult), true);

    if (userResult == null) {
        return WJWJSONResult.errorMsg("用户名或密码不正确");
    }
    return WJWJSONResult.ok(userResult);
}

测试:


同样在注册controller中也添加cookie

运行测试会发现注册完毕后自动登录。

2.12 整合log4j打印日志

1. 移除默认日志
要整合必须先把starter-logging剔除


2. 添加日志框架依赖

<!--引入日志依赖 抽象层 与 实现层-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
</dependency>

3. 创建 log4j.properties 并且放到api模块资源文件目录src/main/resources

log4j.rootLogger=DEBUG,stdout,file
log4j.additivity.org.apache=true

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.threshold=INFO
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-5p %c{1}:%L - %m%n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.DatePattern='.'yyyy-MM-dd-HH-mm
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
log4j.appender.file.Threshold=INFO
log4j.appender.file.append=true
log4j.appender.file.File=/workspaces/logs/foodie-api/mylog.log

2.13 通过日志监控service运行时间

利用apo技术来实现。
父项目的pom中引入依赖:

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

在api项目中创建辅助切面类:
AOP通知:

  1. 前置通知:在方法调用之前执行
  2. 后置通知:在方法正常调用之后执行
  3. 环绕通知:在方法调用前后,都分别可以执行的通知
  4. 异常通知:在方法调用过程中发生异常,则通知
  5. 最终通知:在方法调用之后执行

使用@Around注解,切面表达式:
  execution 代表所要执行的表达式主体

  • 第一处 * 代表方法返回类型 * 代表所有类型
  • 第二处 包名 代表aop监控的类所在的包
  • 第三处 … 代表该包及其子包下的所有类方法
  • 第四处 * 代表类名, * 代表所有类
  • 第五处 *(…) *代表类中的方法名,(…)表示方法中的任何参数
package com.wjw.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 2 * @Author: 小王同学
 * 3 * @Date: 2020/12/23 19:02
 * 4
 */
@Aspect
@Component
public class ServiceLogAspect {

    public static final Logger log = LoggerFactory.getLogger(ServiceLogAspect.class);

    /**
     * 切面表达式:
     *  execution 代表所要执行的表达式主体
     *  第一处 * 代表方法返回类型 *代表所有类型
     *  第二处 包名 代表aop监控的类所在的包
     *  第三处 .. 代表该包及其子包下的所有类方法
     *  第四处 * 代表类名, *代表所有类
     *  第五处 *(..) *代表类中的方法名,(..)表示方法中的任何参数
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.wjw.service.impl..*.*(..))")
    public Object recordTimeLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("—————— 开始执行 {}.{} ——————", joinPoint.getTarget().getClass(), joinPoint.getSignature().getName());

        // 记录开始时间
        long begin = System.currentTimeMillis();

        // 执行目标service
        Object result = joinPoint.proceed();

        // 记录结束时间
        long end = System.currentTimeMillis();
        long takeTime = end - begin;

        if (takeTime > 3000) {
            log.error("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
        }else if (takeTime > 2000){
            log.warn("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
        }else {
            log.info("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
        }

        return result;
    }
}

2.14 用户退出登录清空cookie

PassportController中添加相应的方法:

@ApiOperation(value = "用户退出登录", notes = "用户退出登录", httpMethod = "POST")
@PostMapping("/logout")
public WJWJSONResult logout(@RequestParam String userId,
                            HttpServletRequest request,
                            HttpServletResponse response) {
    // 清除用户的相关信息的cookie
    CookieUtils.deleteCookie(request, response, "user");

    // TODO 用户退出登录,需清除购物车
    // TODO 分布式会话中需要清除用户数据

    return WJWJSONResult.ok();
}

2.15 开启Mybatis日志sql打印

修改api项目的配置文件:


登录时的输出:


注册时的输出:

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
关于整合基于注解的SSM框架小结
若依前后端分离版本集成Mybatis-plus
MyBatis+Spring轻量级整合(Maven)
Springboot+Mybatis+通用Mapper多数据源实现数据同步
手把手教你SpringBoot整合MybatisPlus 代码生成器
Mybatis自动生成实体代码的 generator 插件
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服