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

本文阅读 22 分钟
首页 代码,Java 正文

1.1 概述

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

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

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

部分效果展示: img imgimg

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

老规矩,源码请联系公众号: img   本文只提供后端代码的详细编写流程,前端源码不是我们要关注的重点,文内只会给出片段,想要页面展示效果的也请联系公众号,仅仅使用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访问另外一台服务器 img

1.3 项目拆分与聚合

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

1.4 构建聚合工程

img

  创建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 数据库相关

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

  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注解,点进该注解中:img

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

@SpringBootConfiguration注解是一个接口被@Configuration修饰,表示它是一个容器: img @EnableAutoConfiguration是开启自动装配的,@Import是用于做导入的,并且导入的是一个个的Configuration(即容器) img AutoConfigurationImportSelector是一个选择器,类比jquery,可以选择自动装配的类 img getAutoConfigurationEntry用于自动装配

img configurations被存在于一个List,来自getCandidateConfigurations方法 img 在spring.factories文件中存了一些自动装配的类

img img 中可以看到内置的tomcat img img 中内置的是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

img

1.9 MyBatis 数据库逆向生成工具

1.9.1 代码自动生成

img 打开我提供的另外一个项目, 代码自取 img 运行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文件夹拷贝到我们的主项目中。 img 第一个导入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,从类名上可以看得出来,是用于操作数据库的,这两个类里又分别包含了如下方法,简单归类一下:

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

  1. 随后再来看一下Mapper 中所继承的父类,如下:
public interface Mapper<T> extends BaseMapper<T>, ExampleMapper<T>, RowBoundsMapper<T>, Marker { 
}
  • BaseMapper<T> img
  • 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注解来实现事务,查看源码:

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

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

2.1 注册登录流程

用户名注册登录流程: img

邮箱注册流程: img 手机号注册登录流程: img

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

}

img

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测试刚刚的那个接口: img

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

对应注册时填写的数据

img

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

}

img 创建用户对应的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();
}

img

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 img swagger-bootstrap-ui提供的界面:http://localhost:8088/doc.html img

2.6 优化Swagger2显示

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

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

img 在方法上面添加

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

img img 可以在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;

img

2.7 使用tomcat运行前端源码

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

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

以注册服务为例,先修改接口服务接口地址 img 在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的用户发现可以注册成功 img

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 img 实现相应的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);
}

img

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

img img

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

测试: img 同样在注册controller中也添加cookie img 运行测试会发现注册完毕后自动登录。

2.12 整合log4j打印日志

1. 移除默认日志 要整合必须先把starter-logging剔除 img 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项目的配置文件:

img 登录时的输出:

img 注册时的输出: img

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://wangjiawei.blog.csdn.net/article/details/112918024
-- 展开阅读全文 --
安全面试之XSS(跨站脚本攻击)
« 上一篇 07-24

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复