SpringBoot学习+秒杀项目

目录一 ~ 六来源:GitHub

一、项目简介

1.商品列表页获取秒杀商品列表
2.进入商品详情页获取秒杀商品详情
3.秒杀开始后进入下单确认页下单并支付成功

二、应用springboot完成基础项目搭建

2.1 使用IDEA创建maven项目

1.new->project->maven项目->选择maven-archetype-quickstart
这种方式创建的maven项目是以jar包方式对外输出
2.新建一个resources目录,作为资源文件目录,指定为Resource root

2.2 引入SpringBoot依赖包实现简单的Web项目

进入官方文档
Building a RESTful Web Service

1.引入父pom

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

2.引入依赖

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

这个依赖里面就集成了一个小项目,通过引入这个依赖,可以快速搭建一个springboot项目

3.maven Reimport

刷新一下,会自动下载相应jar包(注:可以把idea设定为自动导入maven依赖),下载之后的jar包在External.Libraries里

4.SpringBoot的Web项目

@EnableAutoConfiguration
@RestController
public class App 
{

    @RequestMapping("/")
    public String home() {
        return "hello World!";
    }
    public static void main( String[] args )
    {
        System.out.println("Hello World!");
        SpringApplication.run(App.class,args);
    }
}

启动App,访问localhost:8080(springboot会自动启动一个内嵌的tomcat,端口号默认8080)

2.3 Mybatis接入SpringBoot项目

1.SpringBoot的默认配置

在resources目录下新建SpringBoot的默认配置文件application.properties

通过一行简单的属性就能更改tomcat的端口

server.port=8090

2.配置pom文件

<!--数据库-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<!--数据库连接池-->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.3</version>
</dependency>
<!--Mybatis依赖-->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.1</version>
</dependency>

3.配置文件application.properties

设置
mybatis.mapper-locations=classpath:mapping/*.xml

这是dao层mapper.xml的映射路径
然后在resources目录下新建mapping目录

4.自动生成工具,生成数据库文件的映射

引入插件

<!--自动生成工具,生成数据库文件的映射-->
<plugin>
  <groupId>org.mybatis.generator</groupId>
  <artifactId>mybatis-generator-maven-plugin</artifactId>
  <version>1.3.5</version>
  <dependencies>
    <dependency>
      <groupId>org.mybatis.generator</groupId>
      <artifactId>mybatis-generator-core</artifactId>
      <version>1.3.5</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.41</version>
    </dependency>
  </dependencies>
  <executions>
    <execution>
      <id>mybatis generator</id>
      <phase>package</phase>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <!--允许移动生成的文件-->
    <verbose>true</verbose>
    <!--允许自动覆盖文件(生产环境中千万不要这样做)-->
    <overwrite>true</overwrite>
    <configurationFile>
      src/main/resources/mybatis-generator.xml
    </configurationFile>
  </configuration>
</plugin>

2.4 Mybatis自动生成器的使用方式

1.新建文件src/main/resources/mybatis-generator.xml

从官网下载xml配置文件下载地址

2.新建数据库

新建一个miaosha的数据库,并建立两张表,分别是user_info和user_password

3.修改配置文件

<?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="DB2Tables" targetRuntime="MyBatis3">
        <!--数据库链接地址账号密码-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/miaosha"
                        userId="root"
                        password="123456">
        </jdbcConnection>

        <!--生成DataObject类存放位置-->
        <javaModelGenerator targetPackage="com.miaoshaproject.dataobject" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!--生成映射文件存放位置-->
        <sqlMapGenerator targetPackage="mapping"  targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <!--生成Dao类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.miaoshaproject.dao"  targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <!--生成对应表及类名-->
        <!--  enableCountByExample="false"
               enableUpdateByExample="false"
               enableDeleteByExample="false"
               enableSelectByExample="false"
               selectByExampleQueryId="false"
               这些属性是为了使得只生成简单查询的对应文件,去掉复杂查询的生成文件,因为一般开发中不太用的到-->
        <table tableName="user_info" domainObjectName="UserDO"
               enableCountByExample="false"
               enableUpdateByExample="false"
               enableDeleteByExample="false"
               enableSelectByExample="false"
               selectByExampleQueryId="false"></table>
        <table tableName="user_password" domainObjectName="userPasswordDO"
               enableCountByExample="false"
               enableUpdateByExample="false"
               enableDeleteByExample="false"
               enableSelectByExample="false"
               selectByExampleQueryId="false" ></table>

    </context>
</generatorConfiguration>

4.生成文件

在终端运行 mvn mybatis-generator:generate命令
生成的:
dataobject
在这里插入图片描述
从数据库的表中的字段映射过来的,包装成类
dao
在这里插入图片描述
操作数据库中数据的一些方法,查询,插入,删除之类的,都是接口
mapping
在这里插入图片描述
是接口中方法的具体实现

5.接入mysql数据源

在配置文件application.properties中写入:

spring.datasource.name=miaosha
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha
spring.datasource.username=root
spring.datasource.password=123456

#使用druid数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

6.测试数据库

修改App类

@SpringBootApplication(scanBasePackages = {"com.miaoshaproject"})
@RestController
@MapperScan("com.miaoshaproject.dao")
public class App {

    @Autowired
    private UserDOMapper userDOMapper;

    @RequestMapping("/")
    public String home() {
        UserDO userDO = userDOMapper.selectByPrimaryKey(1);
        if (userDO == null) {
            return "用户对象不存在";
        } else {
            return userDO.getName();
        }
    }
}

第三章 用户模块开发

3.1 使用SpringMVC方式开发用户信息

1.增加controller层、service层

创建UserController

@Controller("user")
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/get")
    @ResponseBody
    public UserModel getUser(@RequestParam(name = "id") Integer id) {
        //调用service服务获取对应id的用户对象并返回给前端
        UserModel userModel = userService.getUserById(id);
        return userModel;
    }
}

2.在service层增加UserModel

UserDO不能直接给前端,service层必须有一个model的概念。
因为UserModel里不仅仅只包含UserDO这一个表里的信息,还可能有其他表信息,比如UserPasswordDO里的信息

package com.miaoshaproject.service.model;

/**
 * @author KiroScarlet
 * @date 2019-05-15  -16:50
 */
public class UserModel {
    private Integer id;
    private String name;
    private Byte gender;
    private Integer age;
    private String telphone;
    private String regisitMode;
    private Integer thirdPartyId;
    private String encrptPassword;//另一个表里
}

UserModel需要增加 用户的密码,其通过userPasswordDOMapper从userPasswordDO得到

3.修改userPasswordDOMapper.xml和.java文件

增加方法

<select id="selectByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
  select
  <include refid="Base_Column_List" />
  from user_password
  where user_id = #{userId,jdbcType=INTEGER}
</select>
userPasswordDO selectByUserId(Integer UserId);

4.编写UserService

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDOMapper userDOMapper;

    @Autowired
    private userPasswordDOMapper userPasswordDOMapper;

    @Override
    public UserModel getUserById(Integer id) {
        //调用UserDOMapper获取到对应的用户dataobject
        UserDO userDO = userDOMapper.selectByPrimaryKey(id);
        if (userDO == null) {
            return null;
        }

        //通过用户id获取对应的用户加密密码信息
        userPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId());

        return convertFromDataObject(userDO, userPasswordDO);
    }

    private UserModel convertFromDataObject(UserDO userDO,userPasswordDO userPasswordDO) {
        if (userDO == null) {
            return null;
        }

        UserModel userModel = new UserModel();
        BeanUtils.copyProperties(userDO, userModel);

        if (userPasswordDO != null) {
            userModel.setEncrptPassword(userPasswordDO.getEncrptPassword());
        }
        return userModel;

    }
}

5.这种方式存在的问题

直接给前端用户返回了UserModel,使得攻击者可以直接看到密码
需要在controller层增加一个viewobject模型对象
只需要这些信息:

public class UserVO {
    private Integer id;
    private String name;
    private Byte gender;
    private Integer age;
    private String telphone;
}

6.改造controller

public UserVO getUser(@RequestParam(name = "id") Integer id) {
    //调用service服务获取对应id的用户对象并返回给前端
    UserModel userModel = userService.getUserById(id);

    //将核心领域模型用户对象转化为可供UI使用的viewobject
    return convertFromModel(userModel);
}

private UserVO convertFromModel(UserModel userModel) {
    if (userModel == null) {
        return null;
    }
    UserVO userVO = new UserVO();
    BeanUtils.copyProperties(userModel, userVO);
    return userVO;

}

3.2 定义通用的返回对象——返回正确信息

之前的程序一旦出错,只会返回一个白页,并没有错误信息,需要返回一个有意义的错误信息。

1.增加一个response包。创建CommonReturnType类

public class CommonReturnType {

    //表明对应请求的返回处理结果“success”或“fail”
    private String status;

    //若status=success,则data内返回前端需要的json数据
    //若status=fail,则data内使用通用的错误码格式
    private Object data;

    //定义一个通用的创建方法
    public static CommonReturnType create(Object result) {
        return CommonReturnType.create(result, "success");
    }

    public static CommonReturnType create(Object result,String status) {
        CommonReturnType type = new CommonReturnType();
        type.setStatus(status);
        type.setData(result);
        return type;
    }
}

2.改造返回值

public CommonReturnType getUser(@RequestParam(name = "id") Integer id) {
    //调用service服务获取对应id的用户对象并返回给前端
    UserModel userModel = userService.getUserById(id);

    //将核心领域模型用户对象转化为可供UI使用的viewobject
    UserVO userVO = convertFromModel(userModel);
    
    //返回通用对象
    return CommonReturnType.create(userVO);
}

3.3 定义通用的返回对象——返回错误信息

1.创建error包

2.创建commonError接口

public interface CommonError {
    public int getErrCode();

    public String getErrMsg();

    public CommonError setErrMsg(String errMs);
}

3.创建实现类

public enum EmBusinessError implements CommonError {
    //通用错误类型00001
    PARAMETER_VALIDATION_ERROR(00001, "参数不合法"),


    //10000开头为用户信息相关错误定义
    USER_NOT_EXIST(10001, "用户不存在")
    ;

    private EmBusinessError(int errCode, String errMsg) {
        this.errCode = errCode;
        this.errMsg = errMsg;
    }

    private int errCode;
    private String errMsg;

    @Override
    public int getErrCode() {
        return this.errCode;
    }

    @Override
    public String getErrMsg() {
        return this.errMsg;
    }

    @Override
    public CommonError setErrMsg(String errMsg) {
        this.errMsg = errMsg;
        return this;
    }
}

4.包装器模式实现BusinessException类

/包装器业务异常实现
public class BusinessException extends Exception implements CommonError {

    private CommonError commonError;

    //直接接受EmBusinessError的传参用于构造业务异常
    public BusinessException(CommonError commonError) {
        super();
        this.commonError = commonError;
    }

    //接收自定义errMsg的方式构造业务异常
    public BusinessException(CommonError commonError, String errMsg) {
        super();
        this.commonError = commonError;
        this.commonError.setErrMsg(errMsg);
    }

    @Override
    public int getErrCode() {
        return this.commonError.getErrCode();
    }

    @Override
    public String getErrMsg() {
        return this.commonError.getErrMsg();
    }

    @Override
    public CommonError setErrMsg(String errMsg) {
        this.commonError.setErrMsg(errMsg);
        return this;
    }
}

3.4 定义通用的返回对象——异常处理

public class BaseController {

    //定义exceptionHandler解决未被controller层吸收的exception
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Object handlerException(HttpServletRequest request, Exception ex) {
        Map<String, Object> responseData = new HashMap<>();
        if (ex instanceof BusinessException) {
            BusinessException businessException = (BusinessException) ex;
            responseData.put("errCode", businessException.getErrCode());
            responseData.put("errMsg", businessException.getErrMsg());
        } else {
            responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode());
            responseData.put("errMsg", EmBusinessError.UNKNOWN_ERROR.getErrMsg());
        }
        return CommonReturnType.create(responseData, "fail");
    }

}

定义一个BaseController是因为所有controller都需要这种异常处理方法

3.5 用户模型管理——otp验证码获取

public class UserController extends BaseController{

    @Autowired
    private UserService userService;

    @Autowired
    private HttpServletRequest httpServletRequest;

    //用户获取otp短信接口
    @RequestMapping("/getotp")
    @ResponseBody
    public CommonReturnType getOtp(@RequestParam(name = "telphone") String telphone) {
        //需要按照一定的规则生成OTP验证码
        Random random = new Random();
        int randomInt = random.nextInt(99999);
        randomInt += 10000;
        String otpCode = String.valueOf(randomInt);

        //将OTP验证码同对应用户的手机号关联,使用httpsession的方式绑定手机号与OTPCDOE
        httpServletRequest.getSession().setAttribute(telphone, otpCode);

        //将OTP验证码通过短信通道发送给用户,省略
        System.out.println("telphone=" + telphone + "&otpCode=" + otpCode);

        return CommonReturnType.create(null);
    }

测试,在控制台打印数据

3.6 用户模型管理——Metronic模板简介

采用前后端分离的思想,建立一个html文件夹,引入static文件夹
前端文件保存在本地的哪个盘下都可以,因为是通过ajax来异步获取接口

Metronic:基于bootstrap做的一个模板

3.7 用户模型管理——getotp页面实现

1.getotp.html:

<html>
<head>
    <meta charset="UTF-8">
    <script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script>
    <title>Title</title>
</head>
<body>
    <div>
        <h3>获取otp信息</h3>
        <div>
            <label>手机号</label>
            <div>
                <input type="text" placeholder="手机号" name="telphone" id="telphone"/>
            </div>
        </div>
        <div>
            <button id="getotp" type="submit">
                获取otp短信
            </button>
        </div>
    </div>

</body>

<script>
    jQuery(document).ready(function () {

        //绑定otp的click事件用于向后端发送获取手机验证码的请求
        $("#getotp").on("click",function () {

            var telphone=$("#telphone").val();
            if (telphone==null || telphone=="") {
                alert("手机号不能为空");
                return false;
            }


            //映射到后端@RequestMapping(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
            $.ajax({
                type:"POST",
                contentType:"application/x-www-form-urlencoded",
                url:"http://localhost:8080/user/getotp",
                data:{
                    "telphone":$("#telphone").val(),
                },
                success:function (data) {
                    if (data.status=="success") {
                        alert("otp已经发送到了您的手机,请注意查收");
                    }else {
                        alert("otp发送失败,原因为" + data.data.errMsg);
                    }
                },
                error:function (data) {
                    alert("otp发送失败,原因为"+data.responseText);
                }
            });
        });
    });
</script>
</html>

2.指定controller的method

@RequestMapping(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})

3.提示发送失败,使用chrome调试,发现报错为

getotp.html?_ijt=cqdae6hmhq9069c9s4muooakju:1 Access to XMLHttpRequest at 'http://localhost:8080/user/getotp' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

跨域请求错误,只需要在UserController类上加一个注解@CrossOrigin即可

3.8 用户模型管理——getotp页面美化

1.引入样式表

<link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/global/plugins/css/component.css" rel="stylesheet" type="text/css"/>
<link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/>

2.使用样式

<body class="login">
    <div class="content">
        <h3 class="form-title">获取otp信息</h3>
        <div class="form-group">
            <label class="control-label">手机号</label>
            <div>
                <input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
            </div>
        </div>
        <div class="form-actions">
            <button class="btn blue" id="getotp" type="submit">
                获取otp短信
            </button>
        </div>
    </div>

</body>

3.9 用户模型管理——用户注册功能实现

1.实现方法:用户注册接口

     //用户注册接口
    @RequestMapping(value = "/register", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType register(@RequestParam(name = "telphone") String telphone,
                                     @RequestParam(name = "otpCode") String otpCode,
                                     @RequestParam(name = "name") String name,
                                     @RequestParam(name = "gender") String gender,
                                     @RequestParam(name = "age") String age,
                                     @RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {

        //验证手机号和对应的otpCode相符合
        String inSessionOtpCode = (String) this.httpServletRequest.getSession().getAttribute(telphone);
        if (!com.alibaba.druid.util.StringUtils.equals(otpCode, inSessionOtpCode)) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "短信验证码不符合");
        }
        //用户的注册流程
        UserModel userModel = new UserModel();
        userModel.setName(name);
        userModel.setAge(Integer.valueOf(age));
        userModel.setGender(Byte.valueOf(gender));
        userModel.setTelphone(telphone);
        userModel.setRegisitMode("byphone");

        //密码加密,这样存到数据库才安全,不对外泄露
        userModel.setEncrptPassword(this.EncodeByMd5(password));

        userService.register(userModel);
        return CommonReturnType.create(null);

    }

    //密码加密
    public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        //确定计算方法
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        BASE64Encoder base64en = new BASE64Encoder();
        //加密字符串
        String newstr = base64en.encode(md5.digest(str.getBytes("utf-8")));
        return newstr;
    }

2.引入做输入校验的依赖

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.7</version>
</dependency>

3.UserServiceImpl的register方法

insertSelective相对于insert方法,不会覆盖掉数据库的默认值
数据库设计过程中,尽量避免使用null字段,避免使用null字段的好处:(1)java代码处理null非常脆弱(2)null字段对于前端的展示没有任何意义。所以数据库中的字段应该设计成not null

    @Override
    @Transactional//声明事务
    public void register(UserModel userModel) throws BusinessException {
        //校验
        if (userModel == null) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
        }
        if (StringUtils.isEmpty(userModel.getName())
                || userModel.getGender() == null
                || userModel.getAge() == null
                || StringUtils.isEmpty(userModel.getTelphone())) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
        }

        //实现model->dataobject方法
        UserDO userDO = convertFromModel(userModel);
        //insertSelective相对于insert方法,不会覆盖掉数据库的默认值
        userDOMapper.insertSelective(userDO);

        userModel.setId(userDO.getId());

        userPasswordDO userPasswordDO = convertPasswordFromModel(userModel);
        userPasswordDOMapper.insertSelective(userPasswordDO);

        return;
    }

    private userPasswordDO convertPasswordFromModel(UserModel userModel) {
        if (userModel == null) {
            return null;
        }
        userPasswordDO userPasswordDO = new userPasswordDO();
        userPasswordDO.setEncrptPassword(userModel.getEncrptPassword());
        userPasswordDO.setUserId(userModel.getId());

        return userPasswordDO;
    }

    private UserDO convertFromModel(UserModel userModel) {
        if (userModel == null) {
            return null;
        }
        UserDO userDO = new UserDO();
        BeanUtils.copyProperties(userModel, userDO);
        return userDO;
    }

4.前端界面

首先在getotp界面添加注册成功的跳转界面

success:function (data) {
    if (data.status=="success") {
        alert("otp已经发送到了您的手机,请注意查收");
        window.location.href="register.html";
    }else {
        alert("otp发送失败,原因为" + data.data.errMsg);
    }
},

模仿之前写的界面,新建一个register.html

<body class="login">
    <div class="content">
        <h3 class="form-title">用户注册</h3>
        <div class="form-group">
            <label class="control-label">手机号</label>
            <div>
                <input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">验证码</label>
            <div>
                <input class="form-control" type="text" placeholder="验证码" name="otpCode" id="otpCode"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">用户昵称</label>
            <div>
                <input class="form-control" type="text" placeholder="用户昵称" name="name" id="name"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">性别</label>
            <div>
                <input class="form-control" type="text" placeholder="性别" name="gender" id="gender"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">年龄</label>
            <div>
                <input class="form-control" type="text" placeholder="年龄" name="age" id="age"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">密码</label>
            <div>
                <input class="form-control" type="password" placeholder="密码" name="password" id="password"/>
            </div>
        </div>
        <div class="form-actions">
            <button class="btn blue" id="register" type="submit">
                提交注册
            </button>
        </div>
    </div>

</body>

<script>
    jQuery(document).ready(function () {

        //绑定otp的click事件用于向后端发送获取手机验证码的请求
        $("#register").on("click",function () {

            var telphone=$("#telphone").val();
            var otpCode=$("#otpCode").val();
            var password=$("#password").val();
            var age=$("#age").val();
            var gender=$("#gender").val();
            var name=$("#name").val();
            if (telphone==null || telphone=="") {
                alert("手机号不能为空");
                return false;
            }
            if (otpCode==null || otpCode=="") {
                alert("验证码不能为空");
                return false;
            }
            if (name==null || name=="") {
                alert("用户名不能为空");
                return false;
            }
            if (gender==null || gender=="") {
                alert("性别不能为空");
                return false;
            }
            if (age==null || age=="") {
                alert("年龄不能为空");
                return false;
            }
            if (password==null || password=="") {
                alert("密码不能为空");
                return false;
            }

            //映射到后端@RequestMapping(value = "/register", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
            $.ajax({
                type:"POST",
                contentType:"application/x-www-form-urlencoded",
                url:"http://localhost:8080/user/register",
                data:{
                    "telphone":telphone,
                    "otpCode":otpCode,
                    "password":password,
                    "age":age,
                    "gender":gender,
                    "name":name
                },
                //允许跨域请求
                xhrFields:{withCredentials:true},
                success:function (data) {
                    if (data.status=="success") {
                        alert("注册成功");
                    }else {
                        alert("注册失败,原因为" + data.data.errMsg);
                    }
                },
                error:function (data) {
                    alert("注册失败,原因为"+data.responseText);
                }
            });
            return false;
        });
    });
</script>

5.调试

发现报错,获取不到验证码

跨域请求问题

(1)在UserController上添加如下注解:

//跨域请求中,不能做到session共享
@CrossOrigin(allowCredentials = "true",allowedHeaders = "*")

(2)
在getotp.xml和register.html上添加如下代码:允许跨域授信请求,使其session变成跨域可授信

//允许跨域请求
xhrFields:{withCredentials:true},

在这里插入图片描述

6.注册成功,但是查看数据库,发现password表中并没有user_id

在UserDOMapper的insertSelective方法中添加如下代码:

 <insert id="insertSelective" parameterType="com.miaoshaproject.dataobject.UserDO" keyProperty="id" useGeneratedKeys="true">

通过这样的方式将自增id取出之后复制给对应的UserDO

7.修改UserServiceImpl

UserDO userDO = convertFromModel(userModel);
//insertSelective相对于insert方法,不会覆盖掉数据库的默认值
userDOMapper.insertSelective(userDO);

userModel.setId(userDO.getId());

userPasswordDO userPasswordDO = convertPasswordFromModel(userModel);
userPasswordDOMapper.insertSelective(userPasswordDO);

return;

重新测试成功

8.上面并没有做手机号的唯一性验证

首先,在数据库中添加索引:

索引名称为:telphone_unique_index,索引字段选择telphone,索引类型为UNIQUE,索引方法为BTREE

然后修改以下代码:

try {
    userDOMapper.insertSelective(userDO);
} catch (DuplicateKeyException ex) {
    throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "手机号已注册");
}

3.10 用户模型管理——用户登录功能实现

1.UserController中的用户登录接口


    //用户登录接口
    @RequestMapping(value = "/login", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType login(@RequestParam(name = "telphone") String telphone,
                                  @RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
        //入参校验
        if (StringUtils.isEmpty(telphone) || StringUtils.isEmpty(password)) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
        }

        //用户登录服务,用来校验用户登录是否合法
        //用户加密后的密码
        UserModel userModel = userService.validateLogin(telphone, this.EncodeByMd5(password));

        //将登陆凭证加入到用户登录成功的session内
        this.httpServletRequest.getSession().setAttribute("IS_LOGIN", true);
        this.httpServletRequest.getSession().setAttribute("LOGIN_USER", userModel);

        return CommonReturnType.create(null);

    }

2.UserService中的校验登录方法

    /*
    telphone:用户注册手机
    encrptPassowrd:用户加密后的密码
     */
    UserModel validateLogin(String telphone, String encrptPassword) throws BusinessException;

3.UserServiceImpl的登录方法实现

    @Override
    public UserModel validateLogin(String telphone, String encrptPassword) throws BusinessException {
        //通过用户手机获取用户信息
        UserDO userDO = userDOMapper.selectByTelphone(telphone);
        if (userDO == null) {
            throw new BusinessException(EmBusinessError.USER_LOOGIN_FAIL);
        }
        userPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId(userDO.getId());
        UserModel userModel = convertFromDataObject(userDO, userPasswordDO);

        //比对用户信息内加密的密码是否和传输进来的密码相匹配
        if (StringUtils.equals(encrptPassword, userModel.getEncrptP	assword())) {
            throw new BusinessException(EmBusinessError.USER_LOOGIN_FAIL);
        }

        return userModel;
    }

4.UserDOMapper.xml中的新建方法

<select id="selectByTelphone" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from user_info
    where telphone = #{telphone,jdbcType=VARCHAR}
</select>

5.UserDOMapper中建立映射

//根据电话号码取得用户对象
UserDO selectByTelphone(String telphone);

6.新建前端界面:login.html

<body class="login">
    <div class="content">
        <h3 class="form-title">用户登录</h3>
        <div class="form-group">
            <label class="control-label">手机号</label>
            <div>
                <input class="form-control" type="text" placeholder="手机号" name="telphone" id="telphone"/>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label">密码</label>
            <div>
                <input class="form-control" type="password" placeholder="密码" name="password" id="password"/>
            </div>
        </div>
        <div class="form-actions">
            <button class="btn blue" id="login" type="submit">
                登录
            </button>
            <button class="btn green" id="register" type="submit">
                注册
            </button>
        </div>
    </div>

</body>

<script>
    jQuery(document).ready(function () {

        //绑定注册按钮的click事件用于跳转到注册页面
        $("#register").on("click",function () {
            window.location.href = "getotp.html";
        });

        //绑定登录按钮的click事件用于登录
        $("#login").on("click",function () {

            var telphone=$("#telphone").val();
            var password=$("#password").val();
            if (telphone==null || telphone=="") {
                alert("手机号不能为空");
                return false;
            }
            if (password==null || password=="") {
                alert("密码不能为空");
                return false;
            }

            //映射到后端@RequestMapping(value = "/login", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
            $.ajax({
                type:"POST",
                contentType:"application/x-www-form-urlencoded",
                url:"http://localhost:8080/user/login",
                data:{
                    "telphone":telphone,
                    "password":password
                },
                //允许跨域请求
                xhrFields:{withCredentials:true},
                success:function (data) {
                    if (data.status=="success") {
                        alert("登录成功");
                    }else {
                        alert("登录失败,原因为" + data.data.errMsg);
                    }
                },
                error:function (data) {
                    alert("登录失败,原因为"+data.responseText);
                }
            });
            return false;
        });
    });

3.11 优化校验规则

1.查询maven仓库中是否由可用类库

<!--校验-->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>5.2.4.Final</version>
</dependency>

2.对validator进行一个简单的封装

新建validator的目录

新建一个ValidationResult的类

public class ValidationResult {
    //校验结果是否有错
    private boolean hasErrors = false;

    //存放错误信息的map
    private Map<String, String> errorMsgMap = new HashMap<>();

    public boolean isHasErrors() {
        return hasErrors;
    }

    public void setHasErrors(boolean hasErrors) {
        this.hasErrors = hasErrors;
    }

    public Map<String, String> getErrorMsgMap() {
        return errorMsgMap;
    }

    public void setErrorMsgMap(Map<String, String> errorMsgMap) {
        this.errorMsgMap = errorMsgMap;
    }

    //实现通用的通过格式化字符串信息获取错误结果的msg方法
    public String getErrMsg() {
        return StringUtils.join(errorMsgMap.values().toArray(), ",");
    }
}

新建一个ValidatiorImpl的类

@Component
public class ValidatorImpl implements InitializingBean {

    private Validator validator;

    //实现校验方法并返回校验结果
    public ValidationResult validate(Object bean) {
        final ValidationResult result = new ValidationResult();
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean);
        if (constraintViolationSet.size() > 0) {
            //有错误
            result.setHasErrors(true);
            constraintViolationSet.forEach(constraintViolation ->{
                String errMsg = constraintViolation.getMessage();
                String propertyName = constraintViolation.getPropertyPath().toString();
                result.getErrorMsgMap().put(propertyName, errMsg);
            });
        }
        return result;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //将hibernate validator通过工厂的初始化方式使其实例化
        this.validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
}

3.修改UserModel,基于注解的校验方式

@NotBlank(message = "用户名不能为空")
private String name;

@NotNull(message = "性别不能填写")
private Byte gender;

@NotNull(message = "年龄不能不填写")
@Min(value = 0, message = "年龄必须大于0岁")
@Max(value = 150, message = "年龄必须小于150岁")
private Integer age;

@NotBlank(message = "手机号不能为空")
private String telphone;
private String regisitMode;
private Integer thirdPartyId;

@NotBlank(message = "密码不能为空")
private String encrptPassword;

4.在UserServiceImpl中使用validator

引入bean

@Autowired
private ValidatorImpl validator;
        //校验
//        if (userModel == null) {
//            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
//        }
//        if (StringUtils.isEmpty(userModel.getName())
//                || userModel.getGender() == null
//                || userModel.getAge() == null
//                || StringUtils.isEmpty(userModel.getTelphone())) {
//            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
//        }

        ValidationResult result = validator.validate(userModel);
        if (result.isHasErrors()) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, result.getErrMsg());
        }

以后做校验时只需要在model的属性上做注解即可

第四章 商品模块开发

4.1 商品模型管理——商品创建

1.首先设计商品领域模型

price为什么用decimal而不是double:是因为double传到前端有精度问题

public class ItemModel {
    private Integer id;

    //商品名称
    private String title;

    //商品价格
    private BigDecimal price;

    //商品的库存
    private Integer stock;

    //商品的描述
    private String description;

    //商品的销量
    private Integer sales;

    //商品描述图片的url
    private String imgUrl;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Integer getSales() {
        return sales;
    }

    public void setSales(Integer sales) {
        this.sales = sales;
    }

    public String getImgUrl() {
        return imgUrl;
    }

    public void setImgUrl(String imgUrl) {
        this.imgUrl = imgUrl;
    }
}

2.设计数据库

两张表:商品表和库存表

tips:
stock(库存),和商品是一对一的关系,但是后面考虑到库存是和交易流水相关的,即每次对商品表的操作就是对库存表的操作,以后会涉及到一些分库分表,所以这个stock字段,类似password字段,最好拆到另外一张表里,不放在item表里,便于后期性能优化和水平拆分,这就是领域模型,为了数据库的性能。
**sales(销量)**暂时采取放在item表里,在用户发生交易行为之后,我们通过异步的方式给对应的item销量值加1,不会影响主链路

3.修改pom文件

<!--允许移动生成的文件-->
<verbose>true</verbose>
<!--允许自动覆盖文件(生产环境中千万不要这样做)-->
<overwrite>false</overwrite>

4.修改mybatis-generator配置文件

添加两张表

运行mvn mybatis-generator:generate

5.修改mapper的xml文件

把insert和insertSelective方法后添加属性== keyProperty=“id” useGeneratedKeys=“true”==使其保持自增

6.创建ItemService接口

public interface ItemService {

    //创建商品
    ItemModel createItem(ItemModel itemModel);

    //商品列表浏览
    List<ItemModel> listItem();


    //商品详情浏览
    ItemModel getItemById(Integer id);
}

7.ItemServiceImpl实现类

入参校验

//商品名称
@NotBlank(message = "商品名称不能为空")
private String title;

//商品价格
@NotNull(message = "商品价格不能为空")
@Min(value = 0,message = "商品价格必须大于0")
private BigDecimal price;

//商品的库存
@NotNull(message = "库存不能不填")
private Integer stock;

//商品的描述
@NotBlank(message = "商品描述信息不能为空")
private String description;

//商品的销量
@NotBlank(message = "商品图片信息不能为空")
private Integer sales;

//商品描述图片的url
private String imgUrl;

实现方法

@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ValidatorImpl validator;

    @Autowired
    private ItemDOMapper itemDOMapper;

    @Autowired
    private ItemStockDOMapper itemStockDOMapper;

    @Override
    @Transactional
    public ItemModel createItem(ItemModel itemModel) throws BusinessException {

        //校验入参
        ValidationResult result = validator.validate(itemModel);
        if (result.isHasErrors()) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, result.getErrMsg());
        }
        //转化itemmodel->dataobject
        ItemDO itemDO = this.convertItemDOFromItemModel(itemModel);

        //写入数据库
        itemDOMapper.insertSelective(itemDO);
        itemModel.setId(itemDO.getId());

        ItemStockDO itemStockDO = this.convertItemStockDOFromItemModel(itemModel);
        itemStockDOMapper.insertSelective(itemStockDO);

        //返回创建完成的对象
        return this.getItemById(itemModel.getId());

    }

    private ItemDO convertItemDOFromItemModel(ItemModel itemModel) {
        if (itemModel == null) {
            return null;
        }
        ItemDO itemDO = new ItemDO();
        BeanUtils.copyProperties(itemModel, itemDO);
        return itemDO;
    }

    private ItemStockDO convertItemStockDOFromItemModel(ItemModel itemModel) {
        if (itemModel == null) {
            return null;
        }
        ItemStockDO itemStockDO = new ItemStockDO();
        itemStockDO.setItemId(itemModel.getId());
        itemStockDO.setStock(itemModel.getStock());

        return itemStockDO;
    }

    @Override
    public List<ItemModel> listItem() {
        return null;
    }

    @Override
    public ItemModel getItemById(Integer id) {
        ItemDO itemDO = itemDOMapper.selectByPrimaryKey(id);
        if (itemDO == null) {
            return null;
        }
        //操作获得库存数量
        ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId());

        //将dataobject-> Model
        ItemModel itemModel = convertModelFromDataObject(itemDO, itemStockDO);
        return itemModel;
    }

    private ItemModel convertModelFromDataObject(ItemDO itemDO, ItemStockDO itemStockDO) {
        ItemModel itemModel = new ItemModel();
        BeanUtils.copyProperties(itemDO, itemModel);
        itemModel.setStock(itemStockDO.getStock());
        return itemModel;
    }
}

8.ItemController

@Controller("/item")
@RequestMapping("/item")
//跨域请求中,不能做到session共享
@CrossOrigin(origins = {"*"}, allowCredentials = "true")
public class ItemController extends BaseController {

    @Autowired
    private ItemService itemService;

    //创建商品的controller
    @RequestMapping(value = "/create", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
    @ResponseBody
    public CommonReturnType createItem(@RequestParam(name = "title") String title,
                                       @RequestParam(name = "description") String description,
                                       @RequestParam(name = "price") BigDecimal price,
                                       @RequestParam(name = "stock") Integer stock,
                                       @RequestParam(name = "imgUrl") String imgUrl) throws BusinessException {
        //封装service请求用来创建商品
        ItemModel itemModel = new ItemModel();
        itemModel.setTitle(title);
        itemModel.setDescription(description);
        itemModel.setPrice(price);
        itemModel.setStock(stock);
        itemModel.setImgUrl(imgUrl);

        ItemModel itemModelForReturn = itemService.createItem(itemModel);
        ItemVO itemVO = convertVOFromModel(itemModelForReturn);
        return CommonReturnType.create(itemVO);

    }

    private ItemVO convertVOFromModel(ItemModel itemModel) {
        if (itemModel == null) {
            return null;
        }
        ItemVO itemVO = new ItemVO();
        BeanUtils.copyProperties(itemModel, itemVO);
        return itemVO;
    }

}

9.商品详情页浏览

@RequestMapping(value = "/get", method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id") Integer id) {
    ItemModel itemModel = itemService.getItemById(id);

    ItemVO itemVO = convertVOFromModel(itemModel);

    return CommonReturnType.create(itemVO);
}

删掉了consumes = {CONTENT_TYPE_FORMED},因为:默认浏览器访问时候不会在CommonReturnType上加任何信息,如果在@RequestMapping设置consumes = {CONTENT_TYPE_FORMED},就不行

4.2 商品模型管理——商品列表

假设我们的需求是按照销量从高到低显示所有商品

1.创建sql语句

在ItemDOMapper.xml中新建方法

<select id="listItem"  resultMap="BaseResultMap">

  select
  <include refid="Base_Column_List" />
  /*通过销量倒序排序*/
  from item ORDER BY sales DESC;
</select>

2.在ItemDOMapper中创建方法

List<ItemDO> listItem();

3.在ItemServiceImpl中实现方法

@Override
public List<ItemModel> listItem() {
    List<ItemDO> itemDOList = itemDOMapper.listItem();

    //使用Java8的stream API
    List<ItemModel> itemModelList = itemDOList.stream().map(itemDO -> {
        ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId());
        ItemModel itemModel = this.convertModelFromDataObject(itemDO, itemStockDO);
        return itemModel;
    }).collect(Collectors.toList());

    return itemModelList;
}

4.controller层

//商品列表页面浏览
@RequestMapping(value = "/list", method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType listItem() {
    List<ItemModel> itemModelList = itemService.listItem();
    List<ItemVO> itemVOList = itemModelList.stream().map(itemModel -> {
        ItemVO itemVO = this.convertVOFromModel(itemModel);
        return itemVO;
    }).collect(Collectors.toList());

    return CommonReturnType.create(itemVOList);
}

4.3 商品模型管理——商品列表页面

<html>
<head>
    <meta charset="UTF-8">
    <link href="D:\files\SpringBoot\static\assets\global\plugins\bootstrap\css\bootstrap.min.css" rel="stylesheet" type="text/css"\>
    <link href="D:\files\SpringBoot\static\assets\global\css\components.css" rel="stylesheet" type="text/css"\>
    <link href="D:\files\SpringBoot\static\assets\admin\pages\css\login.css" rel="stylesheet" type="text/css"\>
    <script src="D:\files\SpringBoot\static\assets\global\plugins\jquery-1.11.0.min.js" type="text/javascript"></script>
    <title>Title</title>
</head>
<body>
<div class="content">
    <h3 class="form-title">商品列表浏览</h3>
    <div class="table-responsive">
        <table class="table">
            <thead>
            <tr>
                <th>商品名</th>
                <th>商品图片</th>
                <th>商品描述</th>
                <th>商品价格</th>
                <th>商品库存</th>
                <th>商品销量</th>
            </tr>
            </thead>
            <tbody id="container">

            </tbody>
        </table>
    </div>
</div>
</body>

<script>
    //定义全局商品数组信息
    var g_itemList=[];//空字符串
    jQuery(document).ready(function () {
            $.ajax({
                type:"GET",
                url:"http://localhost:8090/item/list",
                xhrFields:{withCredentials:true},
                success:function (data) {
                    if (data.status=="success"){
                        g_itemList=data.data;
                        reloadDom();
                    }else{
                        alert("获取商品信息失败,原因为"+data.data.errMsg);
                    }
                },
                error:function (data) {
                    alert("获取商品信息失败,原因为"+data.resonseText);
                }
            });
    });

function reloadDom() {
    for(var i=0;i<g_itemList.length;i++){
        var itemVO =g_itemList[i];
        var dom="<tr data-id='"+itemVO.id+"' id='itemDatail"+itemVO.id+"'><td>"+itemVO.title+"</td><td><img style='width:100px;height: auto;' src='"+itemVO.imgUrl+"'/></td><td>"+itemVO.description+"</td><td>"+itemVO.price+"</td><td>"+itemVO.stock+"</td><td>"+itemVO.sales+"</td></tr>";
        //使用jQuery的方法往id=container的元素中append
        $("#container").append($(dom));
        $("#itemDatail"+itemVO.id).on("click",function (e) {
            window.location.href="getitem.html?id="+$(this).data("id");
        })
    }

}

</script>

</html>

4.4 商品模型管理——商品详情页面

<html>
<head>
    <meta charset="UTF-8">
    <link href="D:\files\SpringBoot\static\assets\global\plugins\bootstrap\css\bootstrap.min.css" rel="stylesheet" type="text/css"\>
    <link href="D:\files\SpringBoot\static\assets\global\css\components.css" rel="stylesheet" type="text/css"\>
    <link href="D:\files\SpringBoot\static\assets\admin\pages\css\login.css" rel="stylesheet" type="text/css"\>
    <script src="D:\files\SpringBoot\static\assets\global\plugins\jquery-1.11.0.min.js" type="text/javascript"></script>
    <title>Title</title>
</head>
<body class="login">
<div class="content">
    <h3 class="form-title">商品详情</h3>
    <div class="form-group">
        <div>
            <label class="control-label" id="title"/>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label">商品描述</label>
        <div>
            <label class="control-label" id="description"/>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label">价格</label>
        <div>
            <label class="control-label" id="price"/>
        </div>
    </div>
    <div class="form-group">
        <div>
            <img style="width:200px;height: auto" id="imgUrl"/>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label">库存</label>
        <div>
            <label class="control-label" id="stock"/>
        </div>
    </div>
    <div class="form-group">
        <label class="control-label">销量</label>
        <div>
            <label class="control-label" id="sales"/>
        </div>
    </div>
</div>
</body>

<script>
//url解析代码getParam
function getParam(paramName) {
    paramValue = "", isFound = !1;
    if (this.location.search.indexOf("?") == 0 && this.location.search.indexOf("=") > 1) {
        arrSource = unescape(this.location.search).substring(1, this.location.search.length).split("&"), i = 0;
        while (i < arrSource.length && !isFound) arrSource[i].indexOf("=") > 0 && arrSource[i].split("=")[0].toLowerCase() == paramName.toLowerCase() && (paramValue = arrSource[i].split("=")[1], isFound = !0), i++
    }
    return paramValue == "" && (paramValue = null), paramValue
}

var g_itemVO={};
    jQuery(document).ready(function () {
            //获取商品详情
            $.ajax({
                type:"GET",
                url:"http://localhost:8090/item/get",
                data:{
                    "id":getParam("id")
                },
                xhrFields:{withCredentials:true},
                success:function (data) {
                    if (data.status=="success"){
                        g_itemVO=data.data;
                        reloadDom();
                    }else{
                        alert("获取信息失败,原因为"+data.data.errMsg);
                    }
                },
                error:function (data) {
                    alert("获取信息失败,原因为"+data.resonseText);
                }
            });
            return false;
    });

    function reloadDom() {
        $("#title").text(g_itemVO.title);
        $("#description").text(g_itemVO.description);
        $("#stock").text(g_itemVO.stock);
        $("#price").text(g_itemVO.price);
        $("#imgUrl").attr("src",g_itemVO.imgUrl);
        $("#sales").text(g_itemVO.sales);
    }
</script>

</html>

第五章 交易模块开发

5.1 交易模型管理——交易模型创建

1.先设计用户下单的交易模型

//用户下单的交易模型
public class OrderModel {
    //交易单号,例如2019052100001212,使用string类型
    private String id;

    //购买的用户id
    private Integer userId;

    //购买的商品id
    private Integer itemId;

    //购买时商品的单价
    private BigDecimal itemPrice;

    //购买数量
    private Integer amount;

    //购买金额
    private BigDecimal orderPrice;
    
    ...
}

2.设计数据库

CREATE TABLE `order_info`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `user_id` int(11) NOT NULL DEFAULT 0,
  `item_id` int(11) NOT NULL DEFAULT 0,
  `item_price` decimal(10, 2) NOT NULL DEFAULT 0.00,
  `amount` int(11) NOT NULL DEFAULT 0,
  `order_price` decimal(40, 2) NOT NULL DEFAULT 0.00,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

3.修改配置

<table tableName="order_info" domainObjectName="OrderDO"
       enableCountByExample="false"
       enableUpdateByExample="false"
       enableDeleteByExample="false"
       enableSelectByExample="false"
       selectByExampleQueryId="false" ></table>

4.生成文件

在终端运行mvn mybatis-generator:generate命令

5.2 交易模型管理——交易下单

1.OrderService
public interface OrderService {

    OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException;
}
2.OrderServiceImpl
@Override
@Transactional
public OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException {
    //1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
    ItemModel itemModel = itemService.getItemById(itemId);
    if (itemModel == null) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
    }

    UserModel userModel = userService.getUserById(userId);
    if (userModel == null) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在");
    }

    if (amount <= 0 || amount > 99) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不存在");
    }

    //2.落单减库存
    boolean result = itemService.decreaseStock(itemId, amount);
    if (!result) {
        throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
    }

    //3.订单入库

    //4.返回前端
}
3.落单减库存
  • ItemService
 //库存扣减
  boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException;
  • ItemServiceImpl
     @Override
      @Transactional
      public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
          int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount);
          if (affectedRow > 0) {
              //更新库存成功
              return true;
          } else {
              //更新库存失败
              return false;
          }
      }
  
  • ItemStockMapper
      int decreaseStock(@Param("itemId") Integer itemId, @Param("amount") Integer amount);
  • ItemStockMapper.xml
    <update id="decreaseStock">
  
      update item_stock
      set stock = stock-#{amount}
      where item_id = #{itemId} and stock>=#{amount}
    </update>
4.生成交易流水号

新建一个数据库

CREATE TABLE `sequence_info`  (
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `current_value` int(11) NOT NULL DEFAULT 0,
  `step` int(11) NOT NULL DEFAULT 0,
  PRIMARY KEY (`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

插入一条语句,用来生成当前流水号

INSERT INTO `sequence_info` VALUES ('order_info', 0, 1);

修改mybatis-generator

<table tableName="sequence_info" domainObjectName="SequenceDO"
       enableCountByExample="false"
       enableUpdateByExample="false"
       enableDeleteByExample="false"
       enableSelectByExample="false"
       selectByExampleQueryId="false" ></table>

在终端运行mvn mybatis-generator:generate命令

修改SequenceDOMapper.xml

<select id="getSequenceByName" parameterType="java.lang.String" resultMap="BaseResultMap">
  select
  <include refid="Base_Column_List" />
  from sequence_info
  where name = #{name,jdbcType=VARCHAR} for update
</select>

添加方法

SequenceDO getSequenceByName(String name);
private String generateOrderNo() {
    //订单有16位
    StringBuilder stringBuilder = new StringBuilder();
    //前8位为时间信息,年月日
    LocalDateTime now = LocalDateTime.now();
    String nowDate = now.format(DateTimeFormatter.ISO_DATE).replace("-", "");
    stringBuilder.append(nowDate);

    //中间6位为自增序列
    //获取当前sequence
    int sequence = 0;
    SequenceDO sequenceDO = sequenceDOMapper.getSequenceByName("order_info");

    sequence = sequenceDO.getCurrentValue();
    sequenceDO.setCurrentValue(sequenceDO.getCurrentValue() + sequenceDO.getStep());
    sequenceDOMapper.updateByPrimaryKeySelective(sequenceDO);
    //拼接
    String sequenceStr = String.valueOf(sequence);
    for (int i = 0; i < 6 - sequenceStr.length(); i++) {
        stringBuilder.append(0);
    }
    stringBuilder.append(sequenceStr);

    //最后两位为分库分表位,暂时不考虑
    stringBuilder.append("00");

    return stringBuilder.toString();
}
5.销量增加

itemDOMapper.xml

<update id="increaseSales">
  update item
  set sales = sales+ #{amount}
  where id = #{id,jdbcType=INTEGER}
</update>

itemDOMapper

int increaseSales(@Param("id") Integer id, @Param("amount") Integer amount);

ItemServiceImpl

@Override
@Transactional
public void increaseSales(Integer itemId, Integer amount) throws BusinessException {
    itemDOMapper.increaseSales(itemId,amount);
}
6.最终的OrderServiceImpl
@Override
@Transactional
public OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException {
    //1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
    ItemModel itemModel = itemService.getItemById(itemId);
    if (itemModel == null) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
    }

    UserModel userModel = userService.getUserById(userId);
    if (userModel == null) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在");
    }

    if (amount <= 0 || amount > 99) {
        throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不存在");
    }

    //2.落单减库存
    boolean result = itemService.decreaseStock(itemId, amount);
    if (!result) {
        throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
    }

    //3.订单入库
    OrderModel orderModel = new OrderModel();
    orderModel.setUserId(userId);
    orderModel.setItemId(itemId);
    orderModel.setAmount(amount);
    orderModel.setItemPrice(itemModel.getPrice());
    orderModel.setOrderPrice(itemModel.getPrice().multiply(BigDecimal.valueOf(amount)));

    //生成交易流水号
    orderModel.setId(generateOrderNo());
    OrderDO orderDO = this.convertFromOrderModel(orderModel);
    orderDOMapper.insertSelective(orderDO);
    //加上商品的销量
    itemService.increaseSales(itemId, amount);

    //4.返回前端
    return orderModel;
}
7.controller层
//封装下单请求
@RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
                                    @RequestParam(name = "amount") Integer amount) throws BusinessException {

    //获取用户登录信息
    Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
    if (isLogin == null || !isLogin.booleanValue()) {
        throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
    }
    UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");


    OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, amount);

    return CommonReturnType.create(null);
}

第六章 秒杀模块开发

6.1 活动模型创建

1.引入joda-time依赖

使用joda-time,不使用java自带的time

<dependency>
  <groupId>joda-time</groupId>
  <artifactId>joda-time</artifactId>
  <version>2.9.1</version>
</dependency>

2.创建活动模型

public class PromoModel {
    private Integer id;

    //秒杀活动状态:1表示还未开始,2表示正在进行,3表示已结束
    private Integer status;

    
    //秒杀活动名称
    private String promoName;

    //秒杀活动的开始时间
    private DateTime startDate;

    //秒杀活动的结束时间
    private DateTime endDate;

    //秒杀活动的适用商品
    private Integer itemId;

    //秒杀活动的商品价格
    private BigDecimal promoItemPrice;

3.设计数据库

CREATE TABLE `promo`  (
  `id` int(100) NOT NULL AUTO_INCREMENT,
  `promo_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '',
  `start_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `end_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `item_id` int(11) NOT NULL DEFAULT 0,
  `promo_item_price` decimal(10, 2) NOT NULL DEFAULT 0.00,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

4.mybatis自动生成工具

生成dataobject和dao(mapper.java和xml)

<table tableName="promo" domainObjectName="PromoDO"
       enableCountByExample="false"
       enableUpdateByExample="false"
       enableDeleteByExample="false"
       enableSelectByExample="false"
       selectByExampleQueryId="false" ></table>

6.2 活动模型与商品模型结合

1.service

秒杀服务根据商品id,查询得到当前的活动以及其价格
PromoService

PromoModel getPromoByItemId(Integer itemId);

PromoServiceImpl

@Service
public class PromoServiceImpl implements PromoService {

    @Autowired
    private PromoDOMapper promoDOMapper;



    //根据itemId获取即将开始的或者正在进行的活动
    @Override
    public PromoModel getPromoByItemId(Integer itemId) {

        //获取商品对应的秒杀信息
        PromoDO promoDO = promoDOMapper.selectByItemId(itemId);

        //dataobject->model
        PromoModel promoModel = convertFromDataObject(promoDO);
        if (promoModel == null) {
            return null;
        }

        //判断当前时间是否秒杀活动即将开始或正在进行
        DateTime now = new DateTime();
        if (promoModel.getStartDate().isAfterNow()) {
            promoModel.setStatus(1);
        } else if (promoModel.getEndDate().isBeforeNow()) {
            promoModel.setStatus(3);
        } else {
            promoModel.setStatus(2);
        }

        return promoModel;
    }

    private PromoModel convertFromDataObject(PromoDO promoDO) {
        if (promoDO == null) {
            return null;
        }
        PromoModel promoModel = new PromoModel();
        BeanUtils.copyProperties(promoDO, promoModel);
        promoModel.setStartDate(new DateTime(promoDO.getStartDate()));
        promoModel.setEndDate(new DateTime(promoDO.getEndDate()));

        return promoModel;
    }
}

2.使用聚合模型,在ItemModel上添加属性

//使用聚合模型,如果promoModel不为空,则表示其拥有还未结束的秒杀活动
private PromoModel promoModel;

更改ItemServiceImpl

@Override
public ItemModel getItemById(Integer id) {
    ItemDO itemDO = itemDOMapper.selectByPrimaryKey(id);
    if (itemDO == null) {
        return null;
    }
    //操作获得库存数量
    ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId());

    //将dataobject-> Model
    ItemModel itemModel = convertModelFromDataObject(itemDO, itemStockDO);

    //获取活动商品信息
    PromoModel promoModel = promoService.getPromoByItemId(itemModel.getId());
    if (promoModel != null && promoModel.getStatus().intValue() != 3) {
        itemModel.setPromoModel(promoModel);
    }
    return itemModel;
}

同时修改ItemVO

//商品是否在秒杀活动中,以及对应的状态:0表示没有秒杀活动,1表示秒杀活动等待开始,2表示进行中
private Integer promoStatus;

//秒杀活动价格
private BigDecimal promoPrice;

//秒杀活动id
private Integer promoId;

//秒杀活动开始时间
private String startDate;

修改ItemController

private ItemVO convertVOFromModel(ItemModel itemModel) {
    if (itemModel == null) {
        return null;
    }
    ItemVO itemVO = new ItemVO();
    BeanUtils.copyProperties(itemModel, itemVO);
    if (itemModel.getPromoModel() != null) {
        itemVO.setPromoStatus(itemModel.getPromoModel().getStatus());
        itemVO.setPromoId(itemModel.getPromoModel().getId());
                   itemVO.setStartDate(itemModel.getPromoModel().getStartDate().
                    toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")));
        itemVO.setPromoPrice(itemModel.getPromoModel().getPromoItemPrice());
    } else {
        itemVO.setPromoStatus(0);
    }
    return itemVO;
}

3.修改前端界面

4.修改OrderModel

增加秒杀价格字段

//若非空,则表示是以秒杀商品方式下单
private Integer promoId;

//购买时商品的单价,若promoId非空,则表示是以秒杀商品方式下单
private BigDecimal itemPrice;

然后在数据库中,DO中,DOMapper中增加此字段

5.改造下单接口

//1.通过url上传过来秒杀活动id,然后下单接口内校验对应id是否属于对应商品且活动已开始
//2.直接在下单接口内判断对应的商品是否存在秒杀活动,若存在进行中的则以秒杀价格下单
//倾向于使用第一种形式,因为对同一个商品可能存在不同的秒杀活动,而且第二种方案普通销售的商品也需要校验秒杀
OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException;

实现

//校验活动信息
        if (promoId != null) {
            //(1)校验对应活动是否存在这个适用商品
            if (promoId.intValue() != itemModel.getPromoModel().getId()) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确");
                //(2)校验活动是否正在进行中
            } else if (itemModel.getPromoModel().getStatus() != 2) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确");
            }
        }

        //2.落单减库存
        boolean result = itemService.decreaseStock(itemId, amount);
        if (!result) {
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }

        //3.订单入库
        OrderModel orderModel = new OrderModel();
        orderModel.setUserId(userId);
        orderModel.setItemId(itemId);
        orderModel.setPromoId(promoId);
        orderModel.setAmount(amount);

        if (promoId != null) {
            orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
        } else {
            orderModel.setItemPrice(itemModel.getPrice());
        }

        orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount)));

在controller层添加参数

@RequestParam(name = "promoId",required = false) Integer promoId,

进行测试

基础知识

1.DAO层、Service层和Controller层

分层的目的:高内聚,低耦合。
来源:地址

(1)DAO层:

DAO层叫数据访问层,全称为data access object,属于一种比较底层,比较基础的操作,具体到对于某个表的增删改查,也就是说某个DAO一定是和数据库的某一张表一一对应的,其中封装了增删改查基本操作,建议DAO只做原子操作,增删改查。
只负责对数据进行访问,而不管其他的什么业务逻辑,其实就是只干活,而不管为什么干。在dao层里面要完成的是数据访问逻辑以及对数据的访问。数据访问,大部分情况下就是对数据进行操作。dao层为上层的service层提供接口。dao层在操作完成后,如果是查询,则返回对象,如果是增删改,则仅仅需要返回一个boolean值表示成功失败即可。

(2)Service层:

Service层叫服务层,被称为服务,粗略的理解就是对一个或多个DAO进行的再次封装,封装成一个服务,所以这里也就不会是一个原子操作了,需要事物控制。

(3)Controler层:

Controler负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面

(4)Conroller层和Service层的区别

Controlle层负责具体的业务模块流程的控制;Service层负责业务模块的逻辑应用设计;
Controller,从字面上理解是控制器,所以它是负责业务调度的,所以在这一层应写一些业务的调度代码,而具体的业务处理应放在service中去写,而且service不单纯是对于dao的增删改查的调用,service是业务层,所以应该更切近于具体业务功能要求,所以在这一层,一个方法所体现的是一个可以对外提供的功能,比如购物商城中的生成订单方法,这里面就不简单是增加个订单记录那么简单,我们需要查询库存,核对商品等一系列实际业务逻辑的处理。

2.MVC模式

来源:地址
MVC:Model、View、Controller
在这里插入图片描述
代码的调用顺序:
View>Controller>Service>Dao,如果上层代码对下层代码的依赖程度过高,就需要对每层的代码定义一个(标准)接口。
Dao层设计思想:
为数据库中的表设计数据操作的Dao表,在实际开发过程中,Dao层需要先定义出自己的标准(接口),降低耦合度

3.一些注解+方法:

**

(1)@EnableAutoConfiguration:

在这里插入图片描述
上图通过一句话即可描述:Spring boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中的注册的各种AutoConfiguration,当某个AutoConfiguration类满足@Conditional指定的生效条件(starters提供的依赖、配置或Spring容器中是否存在某Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。
使用这个注解,springboot会自动启动一个内嵌的tomcat,并加载默认的配置
此注释自动载入应用程序所需的所有Bean

(2)SpringApplication.run

SpringApplication.run一共做了两件事
A.创建SpringApplication对象;在对象初始化时保存事件监听器,容器初始化类以及判断是否为web应用,保存包含main方法的主配置类。
B.调用run方法;准备spring的上下文,完成容器的初始化,创建,加载等。会在不同的时机触发监听器的不同事件。

(3)@RestController

@RestController = @Controller + @ResponseBody组成,等号右边两位同志简单介绍两句,就明白我们@RestController的意义了:
A. @Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。当然也有语义化的作用,即代表该类是充当Controller的作用
B. @ResponseBody 它的作用简短说就是指该类中所有的API接口返回的数据,甭管你对应的方法返回Map或是其他Object,它会以Json字符串的形式返回给客户端,本人尝试了一下,如果返回的是String类型,则仍然是String。

(4)@RequestMapping("/")

在Spring MVC 中使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求。
参数consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;

(5)@ExceptionHandler(Exception.class)

用来统一处理方法抛出的异常,需要定义一个异常的处理方法。
注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常。

(6)@RequestParam

用于将请求参数区数据映射到功能处理方法的参数上

public String queryUserName(@RequestParam String userName)

在url中输入:localhost:8080/**/?userName=zhangsan

请求中包含username参数(如/requestparam1?userName=zhang),则自动传入。

接下来我们看一下@RequestParam注解主要有哪些参数:

value:参数名字,即入参的请求参数名字,如username表示请求的参数区中的名字为username的参数的值将传入;

required:是否必须,默认是true,表示请求中一定要有相应的参数,否则将报404错误码;

defaultValue:默认值,表示如果请求中没有同名参数时的默认值,默认值可以是SpEL表达式,如“#{systemProperties['java.vm.version']}”。

(7) @ResponseStatus

在这里插入图片描述
声明一个异常类在类上面加上ResponseStatus注解,就表明,在系统运行期间,抛出AuthException的时候,就会使用这里声明的 error code 和 error reasoon 返回给客户端,提高可读性。

(8)@CrossOrigin

出于安全原因,浏览器禁止Ajax调用驻留在当前原点之外的资源。例如,当你在一个标签中检查你的银行账户时,你可以在另一个选项卡上拥有EVILL网站。来自EVILL的脚本不能够对你的银行API做出Ajax请求(从你的帐户中取出钱!)使用您的凭据。

跨源资源共享(CORS)是由大多数浏览器实现的W3C规范,允许您灵活地指定什么样的跨域请求被授权,而不是使用一些不太安全和不太强大的策略,如IFRAME或JSONP。

(9)@Transactional

Spring 事务

(10)EncodeByMd5(password)

//密码加密,这样存到数据库才安全,不对外泄露
userModel.setEncrptPassword(this.EncodeByMd5(password));

(11)@controller:

@Controller本身是基于@Component注解的扩展,被@Controller注解的类表示Web层实现,从而见到该注解就想到Web层实现,使用方式和@Component相同;

(12)@service :

@Service本身是基于@Component注解的扩展,被@Service注解的POJO类表示Service层实现,从而见到该注解就想到Service层实现,使用方式和@Component相同;

(13)@repository :

@Repository本身是基于@Component注解的扩展,被@Repository注解的POJO类表示DAO层实现,从而见到该注解就想到DAO层实现,使用方式和@Component相同;

(14)@component:

标注一个类为Spring容器的Bean,(把普通pojo实例化到spring容器中,相当于配置文件中的)

(15)BeanUtils.copyProperties(source,target)

复制属性值,从source类实例中复制属性值到target类实例中,属性名要一致才可以复制

3.父pom

Maven中可以通过继承父模块pom,来实现pom.xml配置的继承和传递,便于各种Maven插件以及程序依赖的统一管理。**通过将子类模块的公共配置,抽象聚合生成父类模块,能够避免pom.xml的重复配置。**由于父类模块本身并不包含除了POM之外的项目文件,也就不需要src/main/java之类的文件夹了。每当需要对多个子模块进行相同的配置时,只需要在父类模块的pom中进行配置,而子类中声明使用此配置即可,当然子类pom中也可以自定义配置,并覆盖父类中的各项配置,和Java中类的继承类似。

4.数据库连接池

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。

传统的连接机制与数据库连接池的运行机制区别:
不使用连接池流程:
在这里插入图片描述
不使用数据库连接池的步骤:
TCP建立连接的三次握手
MySQL认证的三次握手
真正的SQL执行
MySQL的关闭
TCP的四次握手关闭
可以看到,为了执行一条SQL,却多了非常多我们不关心的网络交互。

优点:
实现简单
缺点:
网络IO较多
数据库的负载较高
响应时间较长及QPS较低
应用频繁的创建连接和关闭连接,导致临时对象较多,GC频繁
在关闭连接后,会出现大量TIME_WAIT 的TCP状态(在2个MSL之后关闭)

使用连接池流程:
在这里插入图片描述
使用数据库连接池的步骤:
第一次访问的时候,需要建立连接。 但是之后的访问,均会复用之前创建的连接,直接执行SQL语句。
优点:
较少了网络开销
系统的性能会有一个实质的提升
没了麻烦的TIME_WAIT状态

5.HttpServletRequest

客户端浏览器发出的请求,会被封装成为一个HttpServletRequest对象。
请求的所有的信息,包括请求的地址、请求的参数、提交的数据、上传的文件、客户端的ip,甚至客户端操作系统都包含在其内。
可以使用HttpServletRequest获取客户端的请求参数

HttpServletRequest 提供了两个重载的getSession方法,分别是getSession()和getSession(boolean create)
getSession()和getSession(true)是相同的,都是获取当前客户端的Session,如果获取不到则创建一个新的Session返回。
getSession(false),也是获取当前客户端的Session,不同的是,如果获取不到则返回null。

6.jQuery

是一个 JavaScript 函数库。

7.AJAX

是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。

8.设计

企业级应用,应该尽可能让controller简单,而service复杂,把逻辑尽可能聚合在service内部,用于实现内部的流转处理

遇到的问题:

1.UserController无法获取到前端session里的验证码

问题描述:
已经在后端controller类上加了@CrossOrigin(allowCredentials = “true”,allowedHeaders = “*”)
前端的getotp.xml和register.xml加了xhrFields:{withCredentials:true},
但是还是获取不到前端session里的验证码
在这里插入图片描述

问题解决:
跨域问题解决:google配置后仍然无法正确获取otpcode。由于谷歌浏览器的SameSite安全机制的问题,浏览器在跨域的时候不允许request请求携带cookie,导致每次sessionId都是新的,这里有个出问题前提:跨域,刚好和调试时的环境情况一致。浏览器版本chrome84.0.4147.135(谷歌游览器好像从80版本之后就加入了SameSite安全机制),直接在地址栏里输入chrome://flags/,然后在搜索框里搜索关键字SameSite,找到与之匹配的项SameSite by default cookies,将其设置为Disabled,然后关闭浏览器再打开,请求。

Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,主要用于防止CSRF攻击和用户追踪。由于SameSite默认值的改变,导致大部分浏览器在跳转跨站的网站时没有携带Cookie,造成登录态失效等一系列问题。
最直观的就是,比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

跨域对应的是同域,同域即是浏览器的同源策略,这里不单单是指相同域名,域名、协议、端口号必须完全一致,才属于同源。只要其中一个不相同,都是属于跨域了。

2.用户登录成功,跳转到商品下单界面,下单失败

找了好久好久,定位到是减少库存和增加销量这两个函数的错误
最后发现是:
(1)ItemStockDOMapper.java和ItemStockDOMapper.xml的参数不对应
在这里插入图片描述
在这里插入图片描述
这两个参数要保持一致,否则无法正常解析
(2)ItemDOMapper.java和ItemDOMapper.xml函数名字写的不一致
这种拼写错误真想打我自己一顿
在这里插入图片描述

在这里插入图片描述

总结:

1.所用技术:

Java
前端:HTML, CSS, JavaScript, jQuery,Metronic模板(html,css样式)
后端:Springboot
数据库:MySQL
开发工具:Idea,Maven

2.代码结构:

在这里插入图片描述

3.数据库结构:

在这里插入图片描述

4.各个模块:

(1)用户模块

A.数据库中的表:

user_info:自增id,姓名,性别,年龄,电话,注册方式,第三方id
在这里插入图片描述
user_password:自增id,密码,用户id
密码进行了加密处理
在这里插入图片描述

B.实现功能:
1)注册:

(1)获取验证码
在这里插入图片描述
在这里插入图片描述

输入手机号之后,点击蓝色按钮,idea后台会收到一个验证码

(2)输入注册信息
在这里插入图片描述

输入信息之后,点击提交注册,就会在数据库的user_info和user_password两个表中插入一条新的数据

2)登录:

在这里插入图片描述
登录成功后,会自动跳转到商品列表界面

(2)商品模块

A.数据库中的表

商品表item:自增id,商品名,价格,描述,销量,图片url
在这里插入图片描述
库存表item_stock:自增id,库存,商品id
在这里插入图片描述

B.实现功能
1)显示商品列表

在这里插入图片描述

2)显示商品详情,通过单击列表中的商品即可跳转

在这里插入图片描述

(3)交易模块

A.数据库中的表

交易order_info:自增id,用户id,商品id,商品价格,购买数量,订单总价格,是否是秒杀商品(如果是,则为1)
在这里插入图片描述

B.实现功能
下单

单击蓝色按钮,如果用户未登录,则会跳转到用户登录界面
下单成功后,会进行数据库中各个表的更新,向order_info插入新订单,增加item_info中的销量,减小item_stock中的库存等操作

在这里插入图片描述

(4)秒杀模块

A.数据库中的表

秒杀promo:自增id,秒杀活动名字,秒杀开始时间,商品id,秒杀价格,秒杀结束时间
在这里插入图片描述

B.实现功能
秒杀

和商品模型相结合,向其中添加秒杀信息,并展示给前端
是通过前端将当前详情页商品的id传给后端,判断是否是一个秒杀活动的商品,
如果是,会通过判断当前时间是否大于秒杀开始时间来对前端页面进行改动
在这里插入图片描述

(5)各个模块图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.有关项目的一些问题:

(1)为什么把用户信息和密码拆成两个表进行存放?

原因有两个
A.在一些大企业中,密码表的管理通常都是单独使用密码机进行管理,单独创建一个密码表更有利于管理
B.一般情况下,当用户已经登录之后,就不再需要密码信息,所以在向内存加载用户信息时候,就可以不用加载密码,这样也节省内存

(2)为什么把商品信息和商品库存拆成两个表?

因为商品中的信息一般都是不变的,只是用来读取,但是库存却是经常改变的,如果放在一个表里,当修改库存时候,会给表加上行锁,这样会降低数据库的读写效率.
库存表一般会存放到其他数据库中

6.用到AOP:

我们在 Controller 里提供接口,通常需要捕捉异常,并进行友好提示,否则一旦出错,界面上就会显示报错信息,给用户一种不好的体验。
定义的通用异常处理器BaseController.class里面使用了**@ExceptionHandler**(Exception.class)注解,遇到异常会进行捕捉并报错
接口发生异常会自动调用该方法。

存在的一些缺陷:

1.只能一个用户登录,登录之后下单购买,不能实现高并发的秒杀

优化:

由于商品中有的是参与秒杀活动的商品,有的是不参与的,所以可以模拟对参与秒杀活动的商品进行秒杀,不参与秒杀商品的活动可以正常下单。

所做的工作包括:
(1)使用redis对秒杀商品信息进行缓存,下一次用户访问商品信息直接从缓存里取,避免频繁访问mysql数据库,给数据库带来很大压力。
(2)使用redis对秒杀过程中生成的订单信息进行缓存,当所有商品秒杀结束之后再将所有的订单信息一并写入数据库,这样也是避免频繁访问mysql数据库,给数据库带来很大压力。
(3)使用压测工具jmeter对秒杀过程进行模拟,设置多线程进行秒杀

具体做法:
(1)redis在java中有很多工具包,这里使用RedisTemplate操作redis
(2)写一个配置类RedisConfig
(3)使用redisTemplate.opsForValue()存放商品信息,使用redisTemplate.opsForList()存放订单信息。
(4)应用redisTemplate中的watch,multi,exec实现乐观锁
(5)使用jmeter设置多线程模拟秒杀

存在的缺点:
(1)没有实现用户登录,秒杀过程只是使用多线程模拟,没有真实的用户登录进行秒杀

过程中遇到的困难:
(1)joda.datetime的序列化与反序列化:Jackson内置的没有转换joda.DateTime的方式,需要通过第三个Jar包或者自定义序列化/反序列化来解决这个问题。
自定义序列化/反序列化解决:csdn
(2)运行APP之后,RedisTemplate的bean找不到
解决:不能仅仅加注解@Resource成为一个bean,还需要弄一个配置类RedisConfig,配置类里有redis的连接以及序列化反序列化等等(代码还需要再看看,不太懂),然后声明的这个bean名字要和配置类里redisTemplate的函数名字保持一致(不知道为啥。。。)

秒杀中存在的三个问题:(1)超卖(2)库存遗留(3)超时

1.超卖

可以使用redis中的watch+事务进行解决,相当于一个乐观锁,且解决了“ABA”问题,比悲观锁效率高
首先使用watch对一个key进行监控,然后使用multi进行操作的组队,最后使用exec执行。如果在执行的时候发现key已经被更改,则所有操作执行失败。这样就可以防止超卖。
下面是代码实现。

redisTemplate.setEnableTransactionSupport(true);//开启事务支持
redisTemplate.watch(successOrder);//监视successOrder

if(redisTemplate.opsForList().size(successOrder)<stock){
    redisTemplate.multi();//开启事务

    //定义一个新订单对象
    OrderModel orderModel = new OrderModel();
    orderModel.setItemId(itemId);
    orderModel.setAmount(amount);
    orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
    System.out.println("商品价格:"+orderModel.getItemPrice());
    orderModel.setPromoId(promoId);
    System.out.println("promoId"+promoId);
    orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount)));
    System.out.println("订单价格:"+orderModel.getOrderPrice());
    orderModel.setId(generateOrderNo());//生成交易流水号

    redisTemplate.opsForList().rightPush(successOrder,orderModel);//将新订单从右边插入缓存列表

    redisTemplate.exec();//执行事务
    System.out.println("您已秒杀成功!");
    System.out.println();
    orderModelRes= orderModel;
}else{//订单数量>=库存数量时候,将缓存里的所有订单入库,并进行减库存增加商品销量等操作
    writeToSql(redisTemplate.opsForList().range(successOrder, 0, -1));//订单入库
    itemService.decreaseStock(itemId, redisTemplate.opsForList().size(successOrder).intValue());//减库存
    itemService.increaseSales(itemId, redisTemplate.opsForList().size(successOrder).intValue());//增加销量
    throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH, "库存不足,很遗憾,秒杀失败");
}

2.库存遗留

使用乐观锁可以解决超卖问题,但是会造成库存遗留,因为大量的线程并发下单,会造成大量下单失败,导致最后线程都运行完了,库存里的商品却有剩余。
这种问题的解决办法应该是:让下单请求排成一个队列,一个一个进行下单,就能解决库存遗留问题。
乐观锁加上lua脚本可以解决,因为lua脚本是一个原子性操作,可以满足上述要求。但是我不会写lua脚本,目前也没时间学,so…

使用分布式锁Redisson解决。
这个锁和普通的锁区别就是具有分布式特性。

基本思路是:在创建订单之前要先获取锁,然后才能创建订单并将订单添加到redis的订单缓存列表,这期间向mysql数据库写入的函数也在一直尝试获取锁,当订单数量和库存数相等时,将创建订单添加缓存的入口关闭,然后将订单迭代写入数据库。
流程图:
在这里插入图片描述
在这里插入图片描述

具体代码:
(1)将订单缓存到redis的函数:

/**
     * 将秒杀商品的信息,库存数量,生成的订单列表放入redis缓存中
     */
    //使用redisson实现分布式锁
    @Override
    //由于使用压测工具进行秒杀时候不会进入前段,所以无法通过前端界面的访问为promoId赋值,所以这个参数相当于白费
    public OrderModel createOrder(Integer itemId,Integer promoId, Integer amount) throws BusinessException {
        promoItemId=itemId;

        //1.购买数量是否正确,有时候会限制用户购买的数量,如果输入的amount超出购买限额,则抛出异常
        if (amount <= 0 || amount > 99) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不存在");
        }


        //2.查询缓存中的库存,如果值为-1,则从数据库中加载库存信息,否则不加载
        if(redisTemplate.opsForValue().get(StorageNum)==null){
            //从数据库中查询得到当前商品的库存信息,如果库存小于amount,直接抛出异常,否则继续下面操作
            Integer stock = itemService.getStock(itemId);
            if(stock<amount){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "当前商品库存小于购买数量,不能购买");
            }
            redisTemplate.opsForValue().set(StorageNum,stock);
        }

        //定义一个最后要返回前段的订单模型变量,为了后面的赋值
        OrderModel orderModelRes=new OrderModel();


        //3.判断缓存里是否有商品信息,无论是不是秒杀商品都加载到缓存中(由于promoId的限制)
        ItemModel itemModel;
        if(redisTemplate.opsForValue().get(itemModelInfo)==null){//缓存里不存在商品信息,访问数据库得到商品信息并添加到缓存
            //校验下单状态,下单的商品是否存在
            itemModel = itemService.getItemById(itemId);
            if (itemModel == null) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
            }
            redisTemplate.opsForValue().set(itemModelInfo,itemModel);
        }else{//缓存里存在商品信息,直接从缓存中得到商品信息
            itemModel=(ItemModel) redisTemplate.opsForValue().get(itemModelInfo);
        }


        //4.校验活动信息,访问数据库判断当前商品是否参与秒杀活动的商品
        //(1)当前商品为秒杀商品
        if(itemService.getPromoModel(itemId)!=null){
            //校验活动是否正在进行中
            if(itemModel.getPromoModel().getStatus().intValue()!=2){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "秒杀活动还未开始");
            }
            if(Flag){
                String lockKey = "Redis_Order_Lock";
                RLock lock = redisson.getLock(lockKey);
                lock.lock(30, TimeUnit.SECONDS);
                System.out.println("写redis获取锁成功");
                try{
                    //定义一个新订单对象
                    OrderModel orderModel = new OrderModel();
                    orderModel.setItemId(itemId);
                    orderModel.setAmount(amount);
                    orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
                    orderModel.setPromoId(promoId);
                    orderModel.setOrderPrice(orderModel.getItemPrice().multiply(BigDecimal.valueOf(amount)));
//                    orderModel.setId(generateOrderNo());//生成交易流水号
                    Date date=new Date();
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd :hh:mm:ss");
                    orderModel.setId("00"+simpleDateFormat.format(date)+"ORDER_"+ORDER+"");//生成交易流水号
                    ORDER++;


                    Integer stockNum=(Integer)redisTemplate.opsForValue().get(StorageNum);
                    if(stockNum>0){
                        redisTemplate.opsForList().rightPush(successOrder,orderModel);//将新订单从右边插入缓存列表
                        System.out.println("订单列表长度:"+redisTemplate.opsForList().size(successOrder));
                        redisTemplate.opsForValue().set(StorageNum,stockNum-1);
                        System.out.println("您已秒杀成功!");
                        System.out.println();
                        orderModelRes= orderModel;
                    }else{//订单数量>=库存数量时候,将缓存里的所有订单入库,并进行减库存增加商品销量等操作
                        System.out.println("很遗憾,库存不足,秒杀失败");

                    }
                }finally {
                    lock.unlock();
                }
            }else{
                System.out.println("秒杀活动已结束");;
            }
            mysqlOpe();


        }else{//(1)当前商品不是秒杀商品
            OrderModel orderModel = writeToSql(itemId, promoId, amount);
            //返回前端
            orderModelRes= orderModel;
        }

        return orderModelRes;
    }

(2)将redis缓存中订单写入数据库并增加销量减少库存的函数:

/**
     * 用于从缓存向数据库写入订单信息,并增加商品销量以及减少库存
     */
    @Transactional
    @Scheduled(cron = "5/1 1-3 * * * ?") //从第5秒启动定时器,每一秒调用一次该函数,总共的时间为3分钟
    public void mysqlOpe() throws BusinessException {
        String lockKey="DAO_Lock";
        RLock lock = redisson.getLock(lockKey);
        lock.lock(30,TimeUnit.SECONDS);
        System.out.println("写数据库获取锁成功");


        try {
            List<Object> list=redisTemplate.opsForList().range(successOrder, 0, redisTemplate.opsForList().size(successOrder)-1);

            if(0==(Integer) redisTemplate.opsForValue().get(StorageNum)){
                Flag=false;
                System.out.println("list长度"+list.size());
                if(list.size()!=0){
                    for(Object orderModel:list){
                        System.out.println(orderModel);
                        i++;
                        System.out.println("第"+i+"个订单写入数据库");
                        OrderDO orderDO = this.convertFromOrderModel((OrderModel) orderModel);
                        orderDOMapper.insertSelective(orderDO);
                    }
                    System.out.println("数据库中共写入"+i+"条数据");
                    itemService.decreaseStock(promoItemId,i);//减库存
                    System.out.println("减库存成功");
                    itemService.increaseSales(promoItemId, i);//增加销量
                    System.out.println("增加销量成功");
                    redisTemplate.delete(successOrder);
                    redisTemplate.delete(StorageNum);
                    redisTemplate.delete(itemModelInfo);

                }
            }
        }finally {
            lock.unlock();
        }
    }

面试被问到的

1.如何判断当前用户登录状态

也就是怎么确定当前用户是登录的还是没登录
是通过session
在用户登录成功之后会将登录状态及登录的用户信息存储在session内

this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true);
//如果登录成功,就把用户的model放到对应的用户的session里
this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);

具体在UserController.java中

public CommonReturnType login(@RequestParam(name = "telphone")String telphone,
                                  @RequestParam(name = "password")String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
        //入参校验
        if(org.apache.commons.lang3.StringUtils.isEmpty(telphone) || org.apache.commons.lang3.StringUtils.isEmpty(password)){
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
        }
        //用户登录服务,用来校验用户登录是否合法
        UserModel userModel = userService.validateLogin(telphone, this.EncodeByMd5(password));
        //将登录凭证加入到用户登录成功的session内
        this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true);
        //如果登录成功,就把用户的model放到对应的用户的session里
        this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);
        return CommonReturnType.create(null);
    }

当在商品详情页点击下单时,会检测用户是否登录,是从session里获取是否登录的信息

Boolean is_login = (Boolean)this.httpServletRequest.getSession().getAttribute("IS_LOGIN");

如果没有用户登录则会抛出异常,前端代码收到这个异常的errCode,会跳转到用户登录界面

具体在OrderController.java

@RequestMapping(value = "/createorder",method = {RequestMethod.POST},consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name ="itemId")Integer itemId,
                                    @RequestParam(name ="promoId",required = false)Integer promoId,
                                    @RequestParam(name ="amount")Integer amount) throws BusinessException {
    Boolean is_login = (Boolean)this.httpServletRequest.getSession().getAttribute("IS_LOGIN");
    if(is_login==null || !is_login.booleanValue()){
        throw new BusinessException(EmBusinessError.USER_NOT_LOING,"用户还未登录,不能下单");
    }
    UserModel login_user = (UserModel)this.httpServletRequest.getSession().getAttribute("LOGIN_USER");
    OrderModel orderModel = orderService.createOrder(login_user.getId(), itemId,promoId, amount);
    return CommonReturnType.create(null);
    }

2.根据要求写sql

要求:
#秒杀时间在2021-06-01 13:00:00-2021-05-31 13:00:00,秒杀价格小于5000元,且有库存的商品总数

需要从两个表查询:promo和item_stock

select count(*) from (select promo.item_id as p_item_id,item_stock.item_id as s_item_id from promo,item_stock where promo.start_date<='2021-06-01 13:00:00' and promo.end_date>='2021-05-31 13:00:00' and promo.promo_item_price<5000 and item_stock.stock>0) as total where p_item_id=s_item_id;
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值