【Spring项目实践】百万级《抽奖系统》设计:Spring Boot全链路技术栈实战五万字详解

本篇会加入个人的所谓鱼式疯言

❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言

而是理解过并总结出来通俗易懂的大白话,

小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.

🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!


欢迎小伙伴游玩本项目:

在这里插入图片描述

💞 💞 💞 抽奖系统项目URL 💞 💞 💞

在这里插入图片描述

引言

想体验极致刺激的抽奖乐趣吗?我们的个人抽奖系统,凭借 Spring Boot 快速搭建,依托 MySQL 稳定存储数据,借助 MyBatis 高效操作数据库。Redis 极速缓存,让抽奖响应如闪电般迅速;RabbitMQ 消息队列,实现异步处理,保障系统稳定运行。公平公正,惊喜不断,快来开启你的幸运之旅!

目录

  1. 项目展示与需求分析

  2. 需求分析

  3. 环境配置

  4. 前端代码模块

  5. 公共模块

  6. 注册登录模块

  7. 人员模块

  8. 奖品模块

  9. 活动模块

  10. 抽奖模块

  11. Mapper模块XML实现

  12. 项目部署与项目代码gitee

一. 项目展示与需求分析

1. 项目产品展示

随着电商的兴起,例如拼多多,淘宝,京东等…都利用在线活动吸引用户,促进产品的营销。

常见的就是抽奖活动的出现,利用抽奖活动可以抽代金券,优惠券,生活用品礼包等… 既吸引顾客又达到促进产品营销的目的~~

  1. 登录页面
  • 手机号登录
    在这里插入图片描述

  • 验证码登录

在这里插入图片描述

  1. 注册

在这里插入图片描述

  1. 活动列表

在这里插入图片描述

  1. 新建抽奖活动

在这里插入图片描述

  1. 奖品列表

在这里插入图片描述

  1. 创建奖品

在这里插入图片描述

  1. 人员列表

在这里插入图片描述

  1. 注册用户

在这里插入图片描述

  1. 抽奖

在这里插入图片描述
请添加图片描述
10. 分享中奖结果

在这里插入图片描述

2. 抽奖流程介绍

  • 首先用户首先 创建账号和密码 ,创建的用户权限为 管理员 权限! 跳转到 登录页面 表示注册成功 !

  • 管理员可进行 密码登录 或者 验证码登录验证码登录 通过 邮件 的形式发送给用户,用户收到短信后,验证通过后, 管理员进入 活动中心页面!

  • 在活动中心,管理员用户可以 创建用户,创建的用户权限为 普通用户 权限, 创建成功后会跳转到并出现在 用户列表 上,表示创建用户成功!

  • 然后, 管理员用户可以 创建奖品,记录对应的奖品信息之后,当跳转到奖品 列表页面 并出现在 奖品列表 ,表示创建奖品成功!

  • 继而,管理员用户可以 创建活动 ,创建活动过程中,关联的参与抽奖人员为 普通用户 ,管理员用户不能参与抽奖, 关联的奖品为 奖品列表中的奖品 ,并可以设置 奖品等级…等一系列信息。

  • 当提示活动 创建成功 后, 会跳转到 活动列表页 , 当点击活动列表中活动状态为:“活动进行中,去抽奖” 时, 进入 抽奖页面

  • 进入 抽奖页面 ,显示相应的 活动信息奖品信息,以 奖品 为维度进行抽奖,点击 “开始抽奖”,就会进入人员闪动,当点击 “开始抽奖”闪动停止,确定中奖人员 ,并发送 邮件 通知中奖人员尽快领取奖品。

  • 抽奖过程中, 可以点击 “查看上一奖项” ,观察 上一奖项的信息以及中奖人员

  • 全部奖品抽完 之后,可以点击 “分享结果” , 分享给 用户普通用户或管理员用户 都可查看分享结果

  • 当管理员返回活动列表,已经抽完的活动,就会从 “活动进行中,去抽奖” 状态切换成 “活动已完成,查看中奖名单” , 点击也可查看 所有人的中奖名单

  • 如果 活动列表奖品列表人员列表 中,不需要的相关信息,可点击 × 进行 删除操作

  • 普通用户可以查看 所有人的中奖名单 , 可以 参与抽奖 ,但是没有进入 活动中心创建活动奖品普通用户 等…支配抽奖流程的权限

二. 项目分析

1. 架构设计

在线项目——抽奖系统

前端:使用 JS 与用户进行交互,并且AJAX发送请求给后端,并从后端中获取数据

后端: SpringBoot 构建整个后端体系,实现整个业务逻辑

数据层:以 MySQL 为主数据库,存储用户信息,和活动信息等…

缓存:搭配 Redis 作为缓存,处理热点高访问的数据,减少 MySQL 的访问次数

消息队列:以 RabbitMQ 实现异步处理,主要用于抽奖行为。

日志和安全: 使用 logbackSLF4J 进行日志提示,搭配 JWT 令牌验证用户登录认证。

涉及的前端技术:HTML,CSS,JS,JQuery,AJAX

涉及的后端技术:JavaSE,HashMap,Maven,SpringBoot,SpringMVC,SpringIOC,SpringAOP,MySQL,MyBatis,logback,JWT + 加密,Redis,RabbitMQ。

涉及的设计模式:单例模式,工厂模式,生产者消费者模式,责任链模式,策略模式

在这里插入图片描述

2. 模块设计

首先用户进入登录页面, 由于还没有账号,去注册账号

  1. 注册页面:
  • 姓名: 用户输入正确的姓名

  • 邮箱:输入合法的QQ邮箱

  • 手机号: 输入11位正确的手机号

  • 密码: 输入6-12位的密码

  • 身份: 管理员

注册失败会提示失败原因,

注册成功后会提示弹窗并跳转到登录页面

  1. 登录页面

    密码登录:

    • 手机号:输入注册成功的手机号/邮箱

    • 密码:输入注册成功的密码

    • 点击:登录按钮

    验证码登录:

    • 邮箱:输入注册成功的邮箱

    • 验证码:输入该邮箱接收到的验证码

    • 点击:登录按钮(验证码一分钟之内有效)

      登录失败会提示失败原因

      登录成功后会跳转到活动中心页面

  2. 活动中心页面:

  3. 活动管理:

    • 活动列表选项

      1) 分页显示:最新活动从上往下展示,每页十条活动

      2) 每条活动内容:活动名,活动描述,活动状态:“活动已完成,查看中奖名单” / “活动进行中,去抽奖”
      3). 删除选项:点击是否删除该活动

    • 新建抽奖活动选项
      1)输入活动名称
      2)输入活动描述
      3)圈选奖品:勾选需要用于抽奖的奖品列表下的奖品,选择对应奖品的奖品数量,设置奖品等级:一等奖/二等奖/三等级
      4)圈选人员:勾选需要参与抽奖的人员列表下的普通用户人员(数量必须大于所有奖品总数量)
      5)点击创建奖品:创建失败,提示失败原因,创建成功,弹窗提示并跳转到活动列表并展示最新抽奖活动

    1. 奖品管理:
    • 奖品列表选项
      1) 分页显示:最新奖品从上往下展示,每页五种奖品
      2)奖品内容:奖品id,奖品图,奖品名,奖品描述,奖品价值
      3)删除选项:选择是否删除该奖品

    • 创建奖品选项
      1)输入奖品名称
      2)从文件选择图片进行上传
      3)输入奖品价格
      4)奖品描述
      5)创建奖品选项:创建成功,弹窗提示并跳转到奖品列表,创建失败,提示失败原因。

    1. 人员管理:
    • 人员列表选项
      1)显示所有身份的人员信息:人员id,姓名,身份
      2) 删除按钮:选择是否删除该人员

    • 注册用户选项
      1)输入姓名
      2)输入正确的QQ邮箱
      3)输入11位正确的手机号
      4)普通用户

    当点击活动列表中某个正在进行的活动,去抽奖就会跳转到抽奖页面

  4. 抽奖页面

  • 活动信息展示:活动名,需要抽取的奖品名,奖品等级,奖品名,奖品数量,奖品图片

  • 查看上一奖项选项:可查看上一个奖品信息,和对应的中奖人员

  • 开始抽奖选项:点击后出现人员闪动

  • 点我确定选项:确定中奖人员

  • 已抽完,下一步选项:抽取下一个奖品

  • 已全部抽完选项:展示中奖名单

  • 中奖名单信息:中奖时间,中奖人员,中奖奖品,奖品等级

  • 分享结果选项:链接复制到剪切板可分享他人

    返回活动列表选项:活动信息由 “活动进行中,去抽奖” -> “活动已完成,查看中奖记录”, 点击该文字,可以进入分享页面(或通过URL也可访问分享页面)

  1. 分享页面

    活动信息:活动名
    中奖名单信息:中奖时间,中奖人员,中奖奖品,奖品等级
    分享结果选项:链接复制到剪切板可分享他人

3. 数据库设计

在这里插入图片描述

连接好MySQL之后,建立 lottery_system ,建立六个表:

user,activity,prize,activity_user,activity_prize,winning_record 。

用户表(user)主要存储用户的基本信息,如用户名,密码,手机号,邮箱等… 一般用于注册,登录,展示用户模块等…

奖品(prize): 主要存储奖品的基本信息,存储奖品的基本信息, 如奖品名,奖品描述,奖品价格… 用于创建奖品,展示奖品模块等…

活动(activity)主要存储活动的基本信息,如活动名,活动状态等… 用于创建活动,展示活动,抽奖模块等…

活动用户表(activity_user)建立用户信息与活动信息的关联,搭配活动需求使用, 包括活动id,用户id,用户状态等… 用于创建活动,抽奖模块等…

活动奖品表(activity_prize):建立奖品信息与活动信息的关联,搭配活动需求使用,包括活动id,奖品id,奖品状态等… 一般用于创建活动,抽奖模块等…

中奖记录表(winning_record)保存最终活动的中奖人员的信息,包括中奖信息,活动信息,奖品信息等… 一般用于抽奖,分享中奖结果模块等…

三. 环境配置

1. Maven依赖导入

依赖类型:

  • SpringBoot: 提供整个Spring框架 ,版本: 3.4.2

  • Rabbit MQ实现消息的异步处理

  • MySQL驱动驱动应用程序连接MySQL

  • Mybatis操作MySQL的框架, 版本:3.0.4

  • Spring 参数校验校验前面传入的Java属性是否为空

  • Spring test测试代码开发是否有异常BUG

  • 配置JSON 数据转化用于前后端传输对象的JSON序列化格式,版本:2.13.3

  • JWT 令牌登录凭证, 版本:0.11.5

  • Redis缓存热点数据

  • 短信服务提供发送短信的服务,版本:3.1.1

  • 加密工具包加密手机号,密码的工具包, 版本:5.7.6

  • 邮箱服务提供发送邮箱服务

  • lombok提供Java类的get方法,set方法等… 工具包

2. application 的配置文件

项目的配置文件主要分为两种:

开发环境下的配置文件:

开发配置文件URL

测试/线上环境下的配置文件:

测试配置文件URL

关于配置文件这些,小编就简单带过一下,不做重点讲解哦~

虽然两个文件的内容有点差异,但是配置的属性基本上都是一样:

  • 数据库的相关配置

  • logback 的相关配置

  • Mybatis 的相关配置

  • Redis的相关配置

  • 短信服务的相关配置

  • 奖品图片路径的相关配置

  • MQ的相关配置

  • 邮箱服务的相关配置

  • 线程池的相关配置

3.数据库创库表

SQL代码文件URL

关于数据库的库表结构,小编在上面的内容中已经讲解过了,这里就不在赘述,更详细的说明,在下面的内容中会多次提及哦~~

4. Mybatis 层的XML 文件操作MySQL

Mybatis操作数据库表的XML文件URL

由于操作数据库需要写的代码量比较多,这边小编就同一使用XML 的方式来管理Mapper层 的代码, 个人感觉更高效~

四. 前端代码模块

前端代码的URL

关于前端代码而已,并不是咱们Java开发程序猿的重点,所以小编至少单独拎出一个章节讲解,主要核心也是讲解JS相关的内容(下面内容细讲),小伙伴们如果对前端代码感兴趣的话,可以进入下面这个网站学习哦~

前端代码学习网站

五. 公共模块

在本项目中采用常见的Spring MVC 三层架构的方式来进行管理业务代码。
`

  • controller层负责 前后端请求和响应的交互
  • service层 实现 具体的业务逻辑
  • mapper层 建立 对数据库的各种访问
  • 除了上述主要的核心功能之外,还需要一个 common层 来辅助这三层的实现,也就是公共模块,公共模块是不属于三层架构但是对三层架构的服务进行保驾护航,提供便利的独立的一层~~

下面小编介绍本次项目的思路是从模块的角度来剖析,并不是从三层来剖析,个人认为如果只是从三层来解析,会显得太笼统,不够具体,希望小伙伴们能不能理解小编的初衷~

1. Jackson序列化

相信小伙伴们都知道,前后端传输对象数据一般都是JSON格式进行序列化传输。

所以我们就需要用到Spring 给我们提供的 ObjectMapper 的序列化和反序列化

需要同时也需要处理一些异常相关和类型强化相关的就比较麻烦。

所以这里就需要在公共模块下,创建一个 Jackson 单元封装序列化格式的方法转化。

package com.example.lottery_system.common.utils;


import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.json.JsonParseException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.Callable;

/**
 * 单例进行序列化和反序列化
 */
@Component
public class JacksonUtil {
    private JacksonUtil() {

    }
    private static final  ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static ObjectMapper getObjectMapper() {
        OBJECT_MAPPER.registerModule(new JavaTimeModule());
        OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return OBJECT_MAPPER;
    }


    // 尝试转化
    private static<T> T  tryParse(Callable<T> parser, Class<? extends Exception> check)  {
        try {
            return parser.call();
        } catch (Exception e) {
//            如果是这个类的异常
            if (check.isAssignableFrom(e.getClass())) {
                throw new JsonParseException(e);
            }

            // 否则就抛出其他异常
            throw new IllegalStateException(e);
        }
    }

    private static<T> T tryParse(Callable<T> parser) {

        return  tryParse(parser, JacksonException.class);
    }

    /**
     * 序列化
     * @param object
     * @return
     */

    public static String writeValueAsString(Object object) {
       String ret =   tryParse(()->{
            return getObjectMapper().writeValueAsString(object);
        });

       return ret;
    }



    /**
     * 反序列化
     * @param content   数据的内容
     * @param valueType 数据的类型
     * @return          返回该数据的类型
     * @param <T>
     */

    public static<T> T readValue(String content, Class<T> valueType) {
         T ret = tryParse(()->{
            return getObjectMapper().readValue(content,valueType);
        });

        return ret;
    }

    /**
     * list 的反序列化
     * 1. 从ObjectMapper 获取类型工厂
     * 2.  类型工厂中的 constructType方法(java的类对象, 参数对象)
     * 3. 最终得到 JavaType 来转化
     * @param content
     * @param paramClasses
     * @return
     * @param <T>
     */


    public  static<T> T readListValue(String content,Class<?> paramClasses) {

        JavaType javaType = getObjectMapper().getTypeFactory().constructParametricType(List.class,paramClasses);

        return tryParse(()->{
            return getObjectMapper().readValue(content,javaType);
        });
    }
}
  1. 首先单例一个 ObjectMapper 类型,只能使用getObjectMapper()这个方法调用,不允许再实例化新的对象
  1. 然后 tryParse() 方法来对实例化过程进行尝试转化, 并且捕获异常
  1. 如果是序列化,传入对象参数, 直接调用 tryParse 进行在里面 writeValueAsString序列化 即可
  1. 需要反序列化时, 传入序列化参数,类型参数,如果是普通类型的话,直接调用 tryParse 进行在里面 readValue反序列化 即可 。
  1. 如果是反序列化成 List 的话, 就需要单独添加一个方法 readListValue() 操作, 额外加上 getTypeFactory().constructParametricType(List.class,paramClasses) 这个配置转化为Java属性才可转换!

鱼式疯言

用途

序列化:用于 后端Java对象转化为JSON字符串的格式 传给前端的响应。

反序列化:用于 解析前端传入的JSON数据解析成想要的Java对象

List反序列化: 用于 解析前端或Redis传入的JSON数据解析成List中的Java对象

这样的作用,就是 统一,统一,统一,统一传输过程都是以JSON字符串的格式来传输数据,以免发生错误!

2. 统一异常处理

package com.example.lottery_system.common.advice;

import com.example.lottery_system.common.errcode.CommonErrorCodeConstant;
import com.example.lottery_system.common.exception.ControllerException;
import com.example.lottery_system.common.exception.ServiceException;
import com.example.lottery_system.common.pojo.CommonResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;



@ControllerAdvice
@ResponseBody
public class CommonExceptionReturn  {
    @ExceptionHandler(ControllerException.class)
    public CommonResult<?> exception(ControllerException exception) {
        /**
         * 设置控制层的错误响应
         */
        Integer code = exception.getCode();
        String message = exception.getMsg();
        CommonResult<Object> error = CommonResult.error(code, message);
        return error;
    }

    @ExceptionHandler(ServiceException.class)
    public CommonResult<?> exception(ServiceException exception) {
        /**
         * 设置控制层的错误响应
         */
        Integer code = exception.getCode();
        String message = exception.getMsg();
        CommonResult<Object> error = CommonResult.error(code, message);
        return error;
    }

    @ExceptionHandler(Exception.class)
    public CommonResult<?> exception(Exception exception) {
        Integer code = CommonErrorCodeConstant.INNER_SERVICE_ERROR.getCode();
        String message = exception.getMessage();
        CommonResult<Object> error = CommonResult.error(code, message);
        return error;
    }

}

1) 首先定义异常基本属性类,codemsg 属性为基本的自定义的异常常量的使用

2)定义异常接口,方便异常说明:ServiceErrorCodeConstant 等…

3)然后自定义两个异常类:ControllerExceptionServiceException, 当抛出这两个异常,参数就使用上面的接口定义的异常常量,提示用户错误信息。

4)最后创建 CommonExceptionReturn 类,并由 @ExceptionHandler 注解标记捕获哪个异常,当程序中的某个功能发生了异常,就能捕获是 ControllerException 发生异常,还是 ServiceException ,并且 以异常自定义常量的信息告知用户错误原因

3. 统一结果返回

package com.example.lottery_system.common.advice;

import com.example.lottery_system.common.pojo.CommonResult;
import com.example.lottery_system.common.utils.JacksonUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;




@ControllerAdvice
@ResponseBody
public class CommonResultReturn implements ResponseBodyAdvice {


    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        /**
         * 讲这个设置为 true 表示需要统一结果返回
         */
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        /**
         1. 首先判断是否是CommonResult 类型
         */

        if (body instanceof CommonResult<?>) {
            return JacksonUtil.writeValueAsString(body);
        }

        /**
         2. 否则就转化成统一数据并返回
         */
        CommonResult<Object> result = CommonResult.success(body);

        /**
         3. 转化为JSON 字符串返回
         */
        return JacksonUtil.writeValueAsString(result);
    }
}
  1. 首先,定义一个结果返回的类型 CommonResult,定义三个属性: data, code, msg . 如果效应成功就返回传入类型的 data, 如果失败就返回 codemsg

  2. 最后定义CommonResultReturn 类, 实现 ResponseBodyAdvice 接口,先打开中supports方法这个开关(返回true),然后在 beforeBodyWrite 这个方法中,最终搭建需要统一返回的类型结果即可~~

鱼式疯言

  1. 以上两种方式就是 Spring AOP 的原理,统一结果返回统一异常处理
  • 如果 程序成功效应 , 就一定会被Spring 的 @ControllerAdvice 这个注解捕获走CommonResultReturn 这个类,统一调用beforeBodyWrite 这个方法,统一返回 CommonResult 类型数据。
  • 如果 程序效应失败 ,就一定会被 Spring的 @ControllerAdvice 这个注解捕获走 CommonExceptionReturn 这个类,统一调用对应抛出异常的 exception 方法, 统一返回 CommonResult 类型数据。
  1. 小结

以上

  • 一方面 JSON 统一传输的数据格式,

  • 另一方面 统一结果返回 和 统一异常处理 统一传输的类型。

从而更好的用于 前后端交换数据,规范数据的传输,利于前端数据的接收,更高效的效应给用户

4. JWT令牌

package com.example.lottery_system.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
    /**
     * 密钥:Base64编码的密钥
     */
    private static final String SECRET = "SDKltwTl3SiWX62dQiSHblEB6O03FG9/vEaivFu6c6g=";
    /**
     * 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
     */
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
            Decoders.BASE64.decode(SECRET));
    /**
     * 过期时间(单位: 毫秒)
     */
    private static final long EXPIRATION = 60*60*1000;

    /**
     * 生成密钥
     *
     * @param claim  {"id": 12, "name":"张山"}
     * @return
     */
    public static String genJwt(Map<String, Object> claim){
        //签名算法
        String jwt = Jwts.builder()
                .setClaims(claim)             // 自定义内容(载荷)
                .setIssuedAt(new Date())      // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
                .signWith(SECRET_KEY)         // 签名算法
                .compact();
        return jwt;
    }

    /**
     * 验证密钥
     */
    public static Claims parseJWT(String jwt){
        if (!StringUtils.hasLength(jwt)){
            return null;
        }
        // 创建解析器, 设置签名密钥
        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
        Claims claims = null;
        try {
            //解析token
            claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
        }catch (Exception e){
            // 签名验证失败
            logger.error("解析令牌错误,jwt:{}", jwt, e);
        }
        return claims;

    }

    /**
     * 从token中获取用户ID
     */
    public static Integer  getUserIdFromToken(String jwtToken) {
        Claims claims = JWTUtil.parseJWT(jwtToken);
        if (claims != null) {
            Map<String, Object> userInfo = new HashMap<>(claims);
            if (userInfo.get("userId") == null) {
                System.out.println("map 钟不含有userId这个 key!!!");
                return null;
            }
            return (Integer) userInfo.get("userId");
        }
        return null;
    }
}
  1. 生成密钥:利用传入的Map参数设置JWT的载荷,签发时间,过期时间,签名算法

  2. 验证密钥:通过传入的 JWTToken 参数, 解析JWT中的载荷,如果解析成功,说明 Token 存在 ,返回载荷为 登录状态 , 如果解析失败,就返回 null,为 未登录状态

  3. Token 中获取载荷,进一步从 载荷中获取用户相关的信息

鱼式疯言

有小伙伴们就有疑问了 ? ? ?

我们不是可以用服务端存 Session 和 客户端存 Cookie的方式吗? 为什么还要那么麻烦单独写一个类来生成 JWT 令牌的方式来存储并验证 ?

其实小伙伴们想的很正确, CookieSession 来存储确定很正确。但是那是对于一个主机而言的!

如果是多台主机, 可能用 CookieSession 的方式来存储并不是一件很好的事情

在这里插入图片描述

如上图,如果是多台主机的服务, 就不适用Cookie -Session 的方式来存储。

由于客户端先对主机1访问,进行登录,登录成功后。

第一台服务器存入一个 Session ,并效应给客户端带上 SessionId ,存入到客户端的 Cookie 中。

但是第二次访问服务器, 就不一定对主机1访问,就有可能对主机2访问,这时客户端带着 SessionId 去寻找 Session , 结果 主机2并没有存在对应的Session这台主机就会认为该用户没有进行登录。 就会造成用户重复登录的过程 ! ! !

显然,这样的问题对用户来说,是很影响体验的。 所以我们就不得不引入新的方案来解决 多主机的问题

在这里插入图片描述

  • 于是我们就使用 JWT的Token 令牌 的方式, 只要 用户登录成功 。就会生成一个Token令牌,只要用户携带这个令牌,
  • 即使 访问多台主机 ,主机就会 根据这个令牌进行解析出载荷,如果解析成功, 就会返回该载荷,表示用户处于 登录状态 ,如果解析失败, 就直接返回 null ,表示用户处于 未登录状态

5. 登录验证拦截器

登录拦截器模块代码URL

package com.example.lottery_system.common.interceptor;

import com.example.lottery_system.common.utils.JWTUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;


/**
 * 拦截器的实现
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("user_token");
        Claims claims = JWTUtil.parseJWT(token);
        logger.info("从拦截器中获取到的token 为 : " + token);

        if (claims == null) {
//            设置状态码
            logger.error("未登录状态!");
            response.setStatus(401);
            return false;
        }

        return true;
    }
}
  1. 首先实现 HandlerInterceptor 接口,重写 preHandle 方法。

  2. 然后从 request 中获取对应 请求头的token信息

  3. 调用前面已经写好的解析Token 的方法, 判断返回是否存在载荷 , 如果 载荷不为空 ,验证通过,为登录状态 。 如果载荷为空 , 验证不通过,返回 401,让前端来 处理相关的页面跳转,用户重新登录

但是这里还是有个问题, Spring怎么知道什么时候拦截, 拦截哪些接口 ? ? ?

难道登录请求一要一并拦截吗? 还有我们之后的分享抽奖结果的请求也要拦截吗?

那么用户还能登录成功吗?

对于这些问题, 所以我们就需要再写一个 拦截器的配置类对拦截器进行配置

拦截器的相关配置URL

梳理逻辑:

  1. 实现 WebMvcConfigurer 这个接口 addInterceptors 这个方式,配置对应需要添加的自身实现的拦截器 LoginInterceptor
  1. 配置需要拦截的接口 , 以及 不需要拦截的接口, 页面,文件 等…

鱼式疯言

话说就有小伙伴有疑问了? 拦截器是怎么拦截器这些请求的?

其实是这样滴 ~

  • 在前端发送请求给后端, 拦截器就会先于后端接口,拦截对于的请求进行验证,我们知道, 请求一般都是带有载荷和请求头的。
  • 当用户登录成功之后,就会返回一个 Token , 这时前端就会在每次发送请求的时候, 前端就会设置请求头带上对应的 Token(就好像我们出门坐火车一样,都要带上身份证一样)
  • 所以这个拦截器就从这个请求头中去获取 , 但是能获取到还不够,万一不是有效的Token (就好像有身份证还不够,万一是假的怎么办?) 这时就需要后端通过解密的方式去解析这个Token (进站闸机去验证这个身份证是否真实),如果Token解析出载荷,验证用户是登录状态,就会放行交给后端来接收这个请求 (身份证验证通过,正常放行通过闸机,上车~),处理相对于的逻辑。
  • 如果解析这个Token 失败,就会直接不经过后端接口入口(身份证是假的,直接拦截,不允许放行过闸机),直接返回 401,表示请求失败,直接让前端处理跳转到登录页面。

6. 正则表达式校验格式

搭配正则表达式的使用目的就为了校验手机号,邮箱和密码的格式是否正确

package com.example.lottery_system.common.utils;

import org.springframework.util.StringUtils;

import java.util.regex.Pattern;


/**
 * 正则表达式 判断格式
 */
public class RegexUtil {

    /**
     * 邮箱:xxx@xx.xxx(形如:abc@qq.com)
     *
     * @param content
     * @return
     */
    public static boolean checkMail(String content) {
        if (!StringUtils.hasText(content)) {
            return false;
        }
        /**
         * ^ 表示匹配字符串的开始。
         * [a-z0-9]+ 表示匹配一个或多个小写字母或数字。
         * ([._\\-]*[a-z0-9])* 表示匹配零次或多次下述模式:一个点、下划线、反斜杠或短横线,后面跟着一个或多个小写字母或数字。这部分是可选的,并且可以重复出现。
         * @ 字符字面量,表示电子邮件地址中必须包含的"@"符号。
         * ([a-z0-9]+[-a-z0-9]*[a-z0-9]+.) 表示匹配一个或多个小写字母或数字,后面可以跟着零个或多个短横线或小写字母和数字,然后是一个小写字母或数字,最后是一个点。这是匹配域名的一部分。
         * {1,63} 表示前面的模式重复1到63次,这是对顶级域名长度的限制。
         * [a-z0-9]+ 表示匹配一个或多个小写字母或数字,这是顶级域名的开始部分。
         * $ 表示匹配字符串的结束。
         */
        String regex = "^[a-z0-9]+([._\\\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$";
        return Pattern.matches(regex, content);
    }

    /**
     * 手机号码以1开头的11位数字
     *
     * @param content
     * @return
     */
    public static boolean checkMobile(String content) {
        if (!StringUtils.hasText(content)) {
            return false;
        }
        /**
         * ^ 表示匹配字符串的开始。
         * 1 表示手机号码以数字1开头。
         * [3|4|5|6|7|8|9] 表示接下来的数字是3到9之间的任意一个数字。这是中国大陆手机号码的第二位数字,通常用来区分不同的运营商。
         * [0-9]{9} 表示后面跟着9个0到9之间的任意数字,这代表手机号码的剩余部分。
         * $ 表示匹配字符串的结束。
         */
        String regex = "^1[3|4|5|6|7|8|9][0-9]{9}$";
        return Pattern.matches(regex, content);
    }

    /**
     * 密码强度正则,6到12位
     *
     * @param content
     * @return
     */
    public static boolean checkPassword(String content){
        if (!StringUtils.hasText(content)) {
            return false;
        }
        /**
         * ^ 表示匹配字符串的开始。
         * [0-9A-Za-z] 表示匹配的字符可以是:
         * 0-9:任意一个数字(0到9)。
         * A-Z:任意一个大写字母(从A到Z)。
         * a-z:任意一个小写字母(从a到z)。
         * {6,12} 表示前面的字符集合(数字、大写字母和小写字母)可以重复出现6到12次。
         * $ 表示匹配字符串的结束。
         */
        String regex= "^[0-9A-Za-z]{6,12}$";
        return Pattern.matches(regex, content);
    }
}

正则表示式:

  1. 邮箱必须带 @ 的格式的字符串

  2. 手机号必须是1 开头,第二位不能为 2 的11 为的数字

  3. 密码必须是 6 ~ 12 位的任意数字或字符

鱼式疯言

在这里,小编的认为正则表达式没有必要去重点掌握。

这里我们只需要了解校验的结果即可, 还有一点就是正则表达式 只能校验数据的格式是否合法,但是 不能校验输入的内容是否有效,正确,安全 ! ! !

  1. 短信服务与邮箱服务

短信服务和邮箱服务在本项目中的场景是:

  1. 发生验证码给用户,以此来验证用户身份登录

  2. 发送中奖通知给用户,通知中奖者尽快领取奖品

    短信服务相关URL

关于短信服务,主要是学习如何调用其中的sendMessage()这个方法,

  • templateCode: 表示模版号,用来区分我们是使用发送验证码模版,还是发送中奖通知的模版

  • phoneNumbers:发送给哪个用户的电话号码

  • templateParam:需要发送传入的参数,比如 code : 验证码等…

生成验证码相关URL

getCaptcha()只需要调用,即可生成一段4位的0~9的数字验证码(下面详解)

邮箱服务相关URL

sendSampleMail(),注意参数的传递

  • to:向哪个邮件发送的邮件地址

  • subject:邮件标题

  • context:邮件正文

7. Redis单元

package com.example.lottery_system.common.utils;

import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@Configuration
public class RedisUtil {

     private  static Logger logger = LoggerFactory.getLogger(RedisUtil.class);
    /**
     * stringRedisTemplate 字符串redis 模版
     * RedisTemplate byte数组redis模版
     */
    @Resource(name = "stringRedisTemplate")
    private  StringRedisTemplate stringRedisTemplate ;



    /***
     * 添加键值对
     * @param key <String>
     * @param value <String>
     * @return
     */
    public  boolean put(String key, String value) {
//        特判一下
        if (!StringUtils.hasLength(key) || !StringUtils.hasLength(value)) {
            return false;
        }
        try {
            stringRedisTemplate.opsForValue().set(key,value);
            return true;
        } catch (Exception e) {
            logger.info("RedisUtil->key:{},value:{} " ,key,value );
            logger.error(",得到的异常为:" + e.getMessage());
            return false;
        }
    }


    /**
     *
     * 设置超时时间
     *
     * @param key
     * @param value
     * @param outTime 单位(分钟)
     * @return
     */
    public  boolean put(String key, String value, long outTime) {
//        特判一下
        if (!StringUtils.hasLength(key) || !StringUtils.hasLength(value)) {
            return false;
        }
        try {
            stringRedisTemplate.opsForValue().set(key,value,outTime, TimeUnit.MINUTES);
            return true;
        } catch (Exception e) {
            logger.info("RedisUtil->key:{},value:{} " ,key,value );
            logger.error(",得到的异常为:" + e.getMessage());
            return false;
        }
    }


    /**
     * 从key中获取值
     * @param key
     * @return
     */

    public  String get(String key) {
        // 特判一下
        if (!StringUtils.hasLength(key)) {
            return new String();
        }

        try {
           return stringRedisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            logger.info("RedisUtil->key:{} " ,key );
            logger.error(",得到的异常为:" + e.getMessage());

            return new String();
        }
    }

    /**
     * 查找这个key 是否存储
     * @param key
     * @return
     */
    public  boolean contains(String key) {
        // 特判一下
        if (!StringUtils.hasLength(key)) {
            return false;
        }

        try {
            Boolean aBoolean = stringRedisTemplate.hasKey(key);
            return aBoolean;
        } catch (Exception e) {
            logger.info("RedisUtil->key:{} " ,key );
            logger.error(",得到的异常为:" + e.getMessage());

            return false;
        }
    }

    /**
     * 删除 key-value
     * @param key
     * @return
     */
    public   boolean delete(String key) {
        // 特判一下
        if (!StringUtils.hasLength(key)) {
            return false;
        }

        try {
            Boolean aBoolean = stringRedisTemplate.delete(key);
            return aBoolean;
        } catch (Exception e) {
            logger.error("RedisUtil->key:{}, 得到的异常为:" ,key,e.getMessage());

            return false;
        }
    }


}

Redis 作为我们的缓存,用于查询一点热点数据,减轻MySQL数据库的访问压力。

所以MySQL有的 增删查 操作,对于Redis 来说也必须也有。

  • 首先,我们定义RedisUtil 为操作Redis 的一个组件单元,并从 Spring 中获取 StringRedisTemplate 这个类

  • 增加数据 : 设置put 用于添加数据到Redis 中, 我们知道对于 Redis 来说, 是以键值对的形式 来存储的。 所以我们就需要设置传入的参数为 key, value , 如果存放成功就返回true, 存放失败就返回捕获异常,返回 false 。同时由于业务需求,像验证码这种就需要设置一个超时时间,如果一旦超过了这个超时时间,Redis 就会自动删除这个key 。

  • 获取数据: 由于 Redis 是以键值对的形式存储, 所以我们就可以通过 key 的方式来获取value

  • 判断数据是否存在: 通过key 来搜寻 ,在Redis 中判断是否存在,存在返回 true, 不存在返回 false

  • 删除数据: StringRedisTemplate 这个类中就实现了 删除对应的key

鱼式疯言

相比小伙伴们一定有疑问了,我们都有数据库存储了, 为啥还需要Redis 来存储。

用一个MySQL 存储数据不就可以了吗? 还需要那么麻烦干啥?

其实是有原因的 ! ! !

我们可以设想一个场景, 现在是双十一拼多多需要用这个抽奖系统来进行抽奖。

  • 如果我们只用数据库MySQL存储,由于双十一,访问的人数几千万的数据量, 并且由于MySQL 是在硬盘上操作数据, 这时就由于访问量过大, 数据在数据中难得及时的响应, 就会出现卡顿,甚至出现完全不响应 ,由于 MySQL 是一个客户端服务器结构的程序 , MySQL就极有可能出现 服务器崩溃 的情况。
  • 所以我们怎么解决这个问题, 这时我们就需要 缓解MySQL 服务器的压力 , 并且替换一个效应更快 的中间件来存储和访问。
  • 那么 Redis 无疑就是比较好的选择, 由于 Redis 的存储是存放在内存中, 相比小伙伴都知晓,从内存中访问的数据就会比硬盘上访问的数据快很多。
  • 所以从一方面考虑, Redis 可以比 MySQL 更快的查找并效应给客户端, 从另外一方面考虑, Redis 可以帮MySQL 服务器减少由于数据量访问过大的情况,替MySQL负重前行

但是这时就有小伙伴问了? 那么我们为何不都让Redis 存所有的数据,而只是存储一部分热点数据给MySQL 呢?

  • 那么势必要考虑到 Redis存储在内存中的不足 , 虽然在内存中访问数据要比MySQL 快很多,但是内存的空间是比硬盘的空间小很多,所以不足以存放大量的数据于Redis, 并且当 主机掉电后,内存上的数据是容易丢失的!
  • 所以处于Redis 的虽然快但是 空间不足容易丢失数据 的情况,我们一般采用 二八原则 , 就是20%可以效应80%的请求。 所以比较好的方案就是 让数据库存储所有的数据,然后Redis 同步备份存储MySQL中20%的热点数据

这样的话,当有大量的数据请求时, 提前访问 Redis, 就可以 大幅度的减缓MySQL的访问压力,降低MySQL服务器崩溃的概率!

8. RabbitMQ的配置

RabbitMQ相关配置文件URL

在这个配置文件中,主要绑定两种消息队列:

  • 普通消息队列:用于存储客户端发来的请求数据,等到合适的时机 来处理该数据

  • 死信队列:当程序处理普通消息队列的请求数据时,发生了异常,就会存放到该队列中,重新想办法处理这些请求。

RabbitMQ的相关配置文件主要的核心:

  1. 死信队列的配置
  • 连接死信队列
  • 建立死信队列交换机
  • 建立死信队列和死信交换机的连接
  1. 普通队列的配置
  • 建立普通消息队列并绑定死信队列
  • 建立交换机
  • 建立交换机和消息队列的连接

鱼式疯言

有小伙伴可能问了?

这个RabbitMQ 作为消息队列的作用是什么? 为什么要进行异步处理? 平常同步处理请求也是可以的呀~ 还需要再加一个这个的消息队列岂不是浪费空间。

其实小伙伴们想的也不错,确实同步正常处理请求,处理结束后当即返回给客户端确定不错~

但是试问,如果是现在是一场世界杯的直播,阿根廷队对战法国队的晚间比赛

现在晚间,几亿用户同时打开网页,发起观看世界杯直播的请求。

这时服务器接收到那么多请求数据,如果是同步处理请求,一下子接收那么多请求,就很有可能会出现诸多问题?

  • 服务器一下接收那么海量请求,就有可能会导致服务器无法及时的效应给用户
  • 服务器一下子要处理那么海量数据就可能会导致内存泄漏,甚至服务器崩溃的情况
  • 一旦如果程序请求这边出现了严重的问题,比如(同一进程下的不同线程,进程崩溃了,连带着线程也会跟着崩溃),也会导致其他服务也同时也附带那么大量问题产生的连带效果。

所以,如果是面对这样高请求量的场景,我们就必须借助对应的工具采取更可行性的方案来解决这样的场景。

RabbitMQ 作为消息队列的使用,无疑是比较好的选择。

消息队列的策略,本质上就是一种设计模式的实现——生产者消费者模型

生产者消费者模型:让生产者把数据放入缓存区,让消费者从缓存区拿数据。

而这个缓存区就是可以是我们使用的 RabbitMQ的消息队列

使用生产者消费者模型好处:

  1. 解耦合: 生产者只需要往消息队列中存数据,消费者只需要从消息队列中取数据,即使生产者这边出现了问题,或消费者这边出现了问题,也不会影响对方的服务。使生产者和消费者两边能够比较独立。
  1. 削峰填谷: 如果是在高请求量的情况,消息队列就可以作为一个缓存,当请求数据太大的时候,生成者生产的速度大于消费者消费,消费者这时还来不及处理, 就可以暂时存放到消息队列中缓存起来, 以免消费者这边处于阻塞状态。 同时如果请求数据没有那么大的情况, 当消费者消费的速度大于生成者生产的速度,就可以从消息队列中去取数据,以免生产者这边出现空闲状态
  1. 缓冲请求:请求量太多的情况下,像上面高请求量的场景,就可以暂时存放到消息队列中, 以免服务器一下子接收那么多数据请求,又来不及处理,导致服务器内存泄漏等一系列诸多问题。 以此降低服务器崩溃的风险。

9. 线程池与日志配置

线性池配置文件URL

在本项目中,线程池的作用主要用于同时发短信服务和邮箱服务

也就是说,当某个用户抽中奖品之后,就立即同时发送短信和邮箱,通知中奖者已经中奖了(下面详讲)

日志配置文件URL

日志配置xml配置文件URL

日志配置文件,目的就在于:

infoerror 两种日志都管理到两个不同的文件中, 当 有异常或程序出现问题 时,便于发生问题并及时处理~


相关的组件和配置说明结束,小伙伴们一定要注意区分哦~ 上面的内容的掌握优先级不是特别高。

接下来小编要细讲的这些模块,才是我们业务逻辑的具体实现,需要掌握的优先级程度:五颗星

六. 注册登录模块

1. 注册模块

<1>. 约定前后端交互接口

用户注册参数URL

在这里插入图片描述

用户注册响应参数URL
在这里插入图片描述

如上图:前端发送 POST 请求到后端接口 /register 这个路径,发送相关的注册的人员数据和身份信息

后端注册成功后,就效应给前端该注册用户的 id

如果后端注册失败,就 响应错误码和错误信息

注册接口的实现文件URL

<2>. 梳理实现逻辑

在这里插入图片描述

如上方的时序图:

  1. 首先用户通过注册页面发送请求后端,由后端 controller层 来接收

    service层实现注册模块URL

  1. 然后通过 service层 进行继而接收 注册信息 ,先对 用户信息进行校验
    • 校验参数是否为空

    • 校验手机号的格式是否合法

    • 校验手机号是否已经注册过

    • 校验邮箱的格式是否合法

    • 校验邮箱是否被注册过

    • 校验身份信息是否是管理员

    • 校验密码是否在六位到十二位之间

  1. service层接着对一些 隐私数据进行加密 ,比如 密码,手机号加密(下面详解)
  1. 最终到 mapper层 调用 数据库插入用户信息到用户表
  1. 如果落库成功就效应给当前用户的 id

需要注意的一点是:

  • 密码的加密,我们一般是采取 Hash加密 的方案(sha256Hex)
  • 而对于手机号而言, 由于手机号需要频繁使用,可能要进行 登录功能短信服务 等…所以手机号一般采取的 对称加密 的方案(aes加密), 并且为了让 方便获取手机号加密手机号 ,小编这里的措施是 MySQL字段自动转化Java对象 的方式。

手机号的对称加密以及自动转化类URL

鱼式疯言

小伙伴们应该不了解对称加密和Hash加密吧~

  • 对称加密: 明文(原文) + 密钥 -> 密文(加密后的内容)

    对称加密含义就是: 可以通过密钥 和 明文 可以获取密文
    同时也可以利用密钥对密文进行解析成明文
    属于(可逆性加密)

  • Hash加密:明文(原文)-> 哈希 -> 密文
    Hash加密: 可以对明文对象 Hash 加密获取密文,
    但是不能对密文解析还原成明文
    属于不可逆加密

有小伙伴可能要问了,对于密码这种隐私数据,虽然要加密,但是进行密码登录的时候,也不是需要用到吗?

那么使用Hash加密确定好吗?

因为对于密码这种隐私数据,一般我们很少采取 对称加密 的方案, 由于对称加密需要密钥,如果有黑客等别有用心之人,一旦被他们获取密钥,就有可能出现下面的问题

  • 数据库的密文就会被一一破解成明文 ! ! !
  • 可以完全像管理员一样进行密码登录获取管理员的所有信息并篡改! ! !

所以这样对密码这样进行对称加密是比较危险的, 所以这里就采用Hash加密的方式更安全。

在这里插入图片描述

但是问题来了,Hash加密是不可逆加密,如果我们要验证密码怎么办?

答案很简单,其实我们只需要当 用户在登录时输入的密码 , 再利用 sha256Hex 对**传入的原文(密码)**进行加密

由于是统一用 sha256Hex 加密后的密文和注册的加密后的密文进行比对

如果 相等就代表是同一组密码 ,验证成功,如果 不相等就验证失败!!!

2. 登录模块

登录基本参数URL

登录分为两种登录方式:

  • 密码登录

  • 验证码登录

<1>. 约定前后端交互接口

首先我们看看 密码登录

  • 对于 密码登录请求 来说主要传入参数为 用户名和密码以及对应身份信息

密码登录参数URL

在这里插入图片描述

  • 对于 验证码登录请求 来说主要传入参数的为 手机号/邮箱和验证码以及身份信息

验证码登录参数
在这里插入图片描述

在这里插入图片描述

校验验证码 的过程就必然有 发送验证码 的过程~

发送验证码只需要传入对应的手机号/邮箱即可发送,这里就不需要 JSON 格式来穿参了!

<2>. 梳理实现逻辑

这样吧~ 小编先接收短信发送的接口,然后一起介绍登录的相关逻辑

短信/邮箱服务的service层实现URL

在这里插入图片描述

观看上方时序图, 可以分为一下几个逻辑:

  1. 首先用户传入手机号/邮箱,通过前端发送获取验证码请求给后端

  2. 后端接收到这个手机号/邮箱, 进行校验是否合法

    • 这里不仅需要校验两种是否合法,同时还需要区别用户是通过手机号还是邮箱来发送验证码, 这里的逻辑就可以通过前面我们实现的正则表达式来判断。
    • 如果正则表达式判断是 手机号合法 ,就走 手机号的短信发送逻辑
    • 如果正在表达式判断是 邮箱合法 ,就走 邮箱的的邮件发送逻辑
    • 还可能都不合法, 那么久直接抛出异常,提示给用户输入不合法 。
  3. 校验成功后,生成随机验证码并发送给用户对应的手机号/邮箱

  4. 发送的同时进行 Redis缓存处理,缓存验证码并设定超时时间 60s

  5. 等到后期需要登录来比对这个验证码的时候,就可以随时到 Redis 这边获取进行~

登录service密码和验证码的实现URL

在这里插入图片描述

如果是密码登录逻辑:

  1. 首先用户选择输入电话/邮箱和密码进行登录,由前端传输给后端
  1. 后端接收到请求之后,由服务层进行校验登录 信息的完整性
  1. 校验成功之后, service层 就调用 mapper层 来获取 数据库中的用户相关信息
  1. 获取用户信息之后,还需要 检查用户信息是否是管理员用户
-  ==管理员用户才允许登录==, 如果只是 **普通用户就无权限登录**
  1. 接下来就通过对传入的密码进行 sha256 加密,和 数据库的密文进行比对
  1. 如果比对成功, 就进行下一步借助 JWT生成Token令牌 返回给用户,作为用户的身份凭借。

如果是 短信/邮件的登录逻辑

  1. 首先在 service层 校验用户传入的手机号/邮箱和验证码

  2. 然后调用 mapper层 的获取用户信息, 并且检查用户身份是否是管理员

    • 如果是 管理员用户才允许登录, 如果只是 普通用户就无权限登录
  3. 之后使用Redis单元从获取验证码, 比对Redis中的验证码和传入的验证码是否一致

  4. 如果一致我们就借助 JWT 生成 Token令牌,返回给用户,作为 用户的身份凭证 。如果比对失败,就提示用户错误信息

鱼式疯言

不知道小伙伴们有没有发现:

一般而言:

  • Java对象与数据库相关使用 DO 后缀的类名 (体现在Mapper层)

DO类型URL

  • 前端发送过来的数据一般以 param 为后缀的类名 (体现在controller层)

param类型URL

  • service层 完成所有的业务逻辑之后一般返回 DTO 后缀的类名 (体现到service层)

DTO类型URL

  • 后端接收到 DTO 类型数据需要以 Result 为后缀的类名(返回给前端)

result类型URL

有小伙伴可能会问了, 这样的话类型转化来转化去岂不是很麻烦吗?

我们为何要这样设计?

其实都是有原因的~~

对于 paramresult 这样的参数,想必小伙伴们一定能够理解。

这里的小编主要是介绍 DO 和 DTO 的区别

DO : 全称—— 数据库持久化对象 , 主要作为数据库映射对象,一般对应数据库的全部的字段, 用于与数据库的数据交互 ,用于传输数据库的数据

DTO :全称——数据传输对象,主要作为系统传输对象,一般根据 前端的数据的字段需求进行定义, 传输部分数据库的字段数据

那么有小伙伴就更疑惑了,我们直接使用 DO 不是更好, 直接返回 result 对应的结果不是更方便吗? 还需要中间建立一层来转化成 DTO , 再转化成 result 对象 , 岂不是更麻烦吗?

答案是肯定的, 但是试想一下, 如果别有用心之人获取到 对应的接口, 从接口中来获取数据, 这样就把数据库表中的全部信息等… 都给暴露在外, 岂不是很危险!

所以小编认为使用DTO / DO 的优势有以下几点来参考

DO 优势

  1. DO 可以紧密的联系数据库的 属性 信息, 高度耦合,这样有便于与数据库进行交互
  1. 通过 数据库可以直接映射DO 上, 可以有效的 简化数据库的操作
  1. DO 通常与 数据库的字段保持一致 ,便于后期维护~

DTO优势

  1. DTO 可以根据不同需求来定义属性, 减少冗余字段的传输, 不仅提高传输的性能还能够 更加灵活 ~
  1. DTO 由于是根据需求来定义属性 信息 ,一般不包含数据库字段的全部信息, 从而 防止数据库表结构的暴露 ,同时能 降低耦合程度 ~
  1. DTO 并一定是和数据库的类型 一 一 对应的, 对于一些敏感的信息(如手机号, 密码)等一些需要加密的信息, 就会在 DTO 层进行隐藏 ~ 更加 安全可靠

总而言之,言而总之 ~~

DO : 只对数据库专一 ,与 数据库的结构耦合紧密 ~
DTO: 只对数据传输专一, 减少数据库, 提高性能安全性~

在实际的开发过程中, 合理使用 DODTO 能够 提高代码的可维护性性能安全性 ~

七. 人员模块

1. 获取用户列表模块

<1>. 约定前后端交互接口

在这里插入图片描述

小编这里设计的 获取用户列表的维度是以成员身份来设计的

如果是获取全部的用户, 那么就直接传入一个 空值的身份信息(用户展示列表)

如果是只获取需要参与抽奖的普通用户, 那么我们就可以传入一个 NORMAL 的身份信息(创建抽奖活动)

响应结果类URL

在这里插入图片描述

而响应就在前面的内容中小编已经交代了,这里就不赘述了~

<2>. 梳理实现逻辑

获取用户信息service层URL
在这里插入图片描述

  1. 首先我们用户发起请求 传入人员身份信息 ,查询人员列表
  1. server层 接收到对应的参数, 就进入mapper 层 进行查询
  1. mapper根据查询的身份信息到 user 表中进行查询,返回人员列表, 最终再进一步的效应给用户

2. 删除用户模块

<1>. 约定前后端交互接口

在这里插入图片描述

在这里插入图片描述

删除接口controller 层URL

  • 接口映射的路径为 /deleteUser
  • 传入的参数为 userId
  • 传出结果为 布尔类型, 如果成功删除就返回 true , 否则直接抛出 错误信息

<2>. 梳理实现逻辑

在这里插入图片描述

  1. 首先, 像上面的流程一样, 用户发起 删除请求 , 传入对应 userId
  1. service层 接收到对应的用户id, 就进入到 mapper层进行删除
  1. mapper层 根据对应的 userId 到user 表中进行删除 , 如果 删除成功就返回 true , 并且告知 用户删除成功~

八. 奖品模块

1. 奖品图片上传

在创建奖品之前, 由于我们的奖品中含有上次奖品图片这一功能,作为前置知识先给小伙伴讲解一番:

<1>. 约定前后端交互接口

奖品图片上传接口controller层URL

在这里插入图片描述

这里的效应就返回一个 文件名 即可(奖品创建的实现逻辑中讲解)

<2>. 梳理实现逻辑

奖品图片创建service层URL
在这里插入图片描述

  1. 判断 目录是否存在如果不存在就创建一个新的目录

  2. 修改原有的文件名为 UUID随机文件名

  3. 保存到 本地默认路径 + 新文件名,保存成功后返回给前端新的文件名, 方便下次调用

2. 奖品创建

<1>. 约定前后端交互接口

奖品参数URL

还有一个 prizePic 会自动转化为 文件路径 ~

在这里插入图片描述

在这里插入图片描述

效应 的话, 只需要 返回保存成功后的奖品id

<2>. 梳理实现逻辑

创建奖品service层URL

在这里插入图片描述

如时序图:

  1. 首先用户发送单个奖品的创建请求, 传入一个奖品信息和图片信息
  1. 传入到 service层 之后 , 一方面在讲奖品图片保存在本地, 另一个方面将 奖品信息保存到数据库
  1. 保存成功效应奖品id,如果 保存异常抛出对应的错误信息

鱼式疯言

对于 图片文件上传 的系列, 就有可能出现 二进制流数据转化被序列化的 的问题

文件数据转化配置URL

3. 奖品列表展示

<1>. 约定前后端交互接口

请求参数URL

在这里插入图片描述

效应结果URL
在这里插入图片描述

注: 本次查询为翻页查询, 需要传入的参数为: currentage当前页数pageSize当前页展示条数

所以响应的结果, 就需要对应的 总条数, 以及 响应数据(奖品id , 奖品名, 奖品描述, 奖品价格, 奖品图片Url)

<2>. 梳理实现逻辑

在这里插入图片描述

  1. 首先前端传入 当前页数和页面条数
  1. service层 接收到当前数据进入到 Mapper层 进行 分页查询
  1. 查询成功 返回总条数和数据结果 ~

4. 奖品删除

<1>. 约定前后端交互接口

删除奖品controller层Url

在这里插入图片描述

在这里插入图片描述

如果 删除成功 ,就响应 true , 否则就 抛出异常信息~

<2>. 梳理实现逻辑

在这里插入图片描述

如时序图:

  1. 首先用户发送 删除奖品信息请求, 带入对应的 奖品id
  1. ``service 层获取到奖品id` , 就到 Mapper 层进行删除操作~
  1. 删除成功就返回 true , 否则抛出异常信息~

九. 活动模块

1. 创建活动

<1>. 约定前后端交互接口

活动参数URL

活动奖品参数URL

活动用户参数URL

在这里插入图片描述

如上图, 其中 请求参数就包含活动信息 ,以及该活动下 关联的需要抽奖的奖品信息列表和最后需要 关联的参与抽奖的人员信息列表

在这里插入图片描述

活动创建成功之后,后端就会返回一个 活动id 给前端, 表示 活动创建并且保存成功~

<2>. 梳理实现逻辑

创建活动service层URL
在这里插入图片描述

如时序图:

  1. 后端从前端接收到 活动创建的对应的请求和数据
  1. 进入到后端的 service 层 之后, 对传入 数据进行校验

    • 校验 活动数据是否为空
    • 校验 活动奖品是否存在
    • 校验 活动用户是否存在
    • 校验该 活动下的奖品是否 <= 该活动下抽奖人员的人数
  1. 创建 活动DO 对象,然后调用Mapper层保存到数据库中的活动表中~
  1. 创建 关联活动人员DO 对象, 然后调用Mapper 层保存到数据库的活动人员表中~
  1. 创建关联活动奖品DO 对象, 然后调用 Mapper层保存到数据库的活动人员表中~
  1. 整合活动 + 人员 + 奖品的完整信息 , 并保存到 Redis 中, 作为缓存, 以便下次使用~

2. 活动列表展示

<1>. 约定前后端交互接口

翻页属性参数URL

在这里插入图片描述

活动列表属性URL

翻页结果属性URL
在这里插入图片描述

请求参数,主要是在 URL的查询字符串中包含两个参数: 当前页面 currentPage大小和当前页面展示的条数 pageSize 的多少~

以此后端效应就根据这两个信息来确定需要查询的哪部分数据, 并把 查询的结果统一封装成列表 , 并且再进一步的都以 PageResult 统一的翻页的结果数据返回效应~

<2>. 梳理实现逻辑

活动列表查询service层URL

在这里插入图片描述

如时序图:

  1. 首先后端接口从 前端获取到页面请求和数据

  2. 之后服务层调用 Mapper 层, 在Mapper层先进行所有总数数量的查询, 并保存到上述的 total

  3. 接着根据 当前页数和条数进行活动信息查询, 返回 活动列表响应给前端~

3. 活动详情展示

<1>. 约定前后端交互接口

在这里插入图片描述

活动详情结果URL

在这里插入图片描述

有活动创建的保存就必然有对 活动详情的请求:

请求是以 活动id 为线索进行发送

响应的话, 我们就以根据该活动, 查询出活动信息, 关联活动奖品信息列表, 以及关联的活动用户信息列表响应给前端~

<2>. 梳理实现逻辑

在这里插入图片描述

如时序图:

  1. 首先前端传入一个 活动id 给后端, 后端传入控制层接口,接口交给服务层来处理
  1. 服务层先到 Redis 中 进行查询, 如果存在就直接从Redis中返回活动详情数据~
  1. 但是 有可能Redis不存在, 那么我们就通过MySQL 查询
  1. MySQL中就需要查询对应的 活动信息, 关联奖品, 以及关联人员信息~
  1. 查询成功后,别着急返回, 这时先 整合数据保存到Redis中一份以便下一次更高效的查询~ 最终效应活动详情~

鱼式疯言

通过上面的实现逻辑的梳理, 小伙伴们想必也理解,为何要加入Redis?

  1. Redis 可以明显配合MySQL进行读写分离
    • 如果要读(查询)数据, 就先去Redis找, 既快又能缓解MySQL的访问压力
    • 如果要插入数据, 就需要先插入 MySQL 之后, 并且还需要 在Redis中同时也存一份~
    • 从而替 MySQL负重前行防止MySQL服务器崩溃~
  1. 但是一定要注意数据同步:
    • 如果 MySQL有的数据, 而 Redis 没有, 就需要同步添加到Redis中
    • 如果 MySQL改了数据(删除数据, 修改数据等...) , 也需要 同步到Redis中进行修改~ 反正出现未及时更新的数据~

4. 活动删除

<1>. 约定前后端交互接口

controller层URL

在这里插入图片描述

在这里插入图片描述

传入一个 活动id , 对该 活动 进行删除~

删除成功返回 true ,否则提示 错误信息 ~

<2>. 梳理实现逻辑

活动删除service层URL

在这里插入图片描述

时序图

  1. 首先后端从 前端获取到活动id , 进入到 controller层 , 并通过 service层 来实现逻辑
  1. 其次 service层 调用 Mapper层 ,对 数据库中活动表, 活动关联人员表,活动关联奖品表 进行 删除
  1. 最后对 Redis缓存 中的数据进行删除,保证缓存与数据库中数据的一致性 ~
  1. 如果 该活动完成抽奖活动, 就需要 删除数据库的中奖表的数据 ,以及 删除缓存Redis中的中奖记录~ (下面详解)

十. 抽奖模块

1. 抽奖请求

<1>. 约定前后端交互接口

抽奖请求参数URL

在这里插入图片描述

在这里插入图片描述

  1. 请求参数: 发送一个以奖品为维度的抽奖请求, 也就是说, 这个奖品可能数量不止一个, 所有不仅要包含奖品的基本信息,还需要包含中奖人员列表的id和姓名信息传给后端,让后端处理该中奖请求~

  2. 一定处理成功,一定响应true(下面细讲~)

<2>. 梳理实现逻辑

推送抽奖请求信息service层URL

在这里插入图片描述

  1. 首先 服务层 通过 控制层接口接收到前端发送过来的数据
  1. 服务层讲请求消息推送给 消息队列RabbitMQ, 让 消息队列来处理相关逻辑

3.当服务层推送完 请求消息,就可以立即返回给前端,效应true

鱼式疯言

在前面的RabbitMQ 的配置中, 我们讲解过消息队列RabbitMQ 这一系列的使用,主要处理高请求量高峰期的场景~

当数据量很高的情况, 然后业务逻辑很复杂的情况下, 使用RabbitMQ就可以实现异步处理, 事务回滚等…

后端可以先保证抽奖请求一定成功: 前端接收到抽奖成功!

后端于是就把 请求消息生产给MQ , 让 MQ处理相关的逻辑

这时需要保证两点:

  1. MQ那边消息一定要 消费成功

  2. 保证事务的一致性, 如果出现异常,就需要及时处理,并成功消费掉信息,否则会出现堆积

在这里插入图片描述

MQ 的业务逻辑处理过程中

设想一下

有一天小编和女神出去逛公园,

走着走着就出现想法不和,惹女神不开心了!, 就好比MQ消息消费异常!

如果 MQ消息消费异常(吵架了), 就需要一定次数的重发,也就是多次去哄女神, 假如设定哄的次数为 5 次,如果哄的次数超过5次

那么小编就需要重新存放到死信队列 (重新思考女神怎么样才能开心起来的解决策略) , 于是再次去 消息重发, 进行处理异常, 重新再次去哄

如此循环往复,直到 女神满意 ,保证 消息消费成功 ,不会出现异常 ~

但是出现消息处理异常的之后,还需要保证 事务的一致性

比如如果一个抽奖活动,当奖品已经从正在抽奖中,逆转为已被抽取, 但是当逆转人员的抽奖状态的时候发现异常, 这时需要保证 事务的一致性 (把已经修改的数据库表的内容恢复到处理消息之前的内容) , 就需要把奖品状态逆转回正在抽奖中的状态~

怎么理解呢?

就好比

有一天,女神邀请我来向她学习化妆 , 这样以后好给她化妆。

但是,学着学着, 小编不小心把女神的香水瓶给打破了,已经惹女神不高兴了~

那么我首先做的 第一件时除了哄女神之外,还需要恢复之前的状态 (配一瓶香水给女神,恢复原样!)

2. MQ 业务逻辑处理

<1>. MQ普通队列的业务逻辑实现

MQ业务逻辑处理

在这里插入图片描述

如时序图:

  1. 首先 MQ将请求消息参数传给service层进行校验:
  • 查询 活动是否存在
  • 查询 活动奖品是否存在
  • 查询 活动是否完成
  • 查询 奖品是否被抽取完成
  • 查询 奖品人员是否抽取完成
  • 查询 奖品人数是否小于等于中奖人员人数
  1. 进行状态扭转(采用 责任链模式策略模式 两种设计模式)依次对中奖人员状态,活动关联奖品状态,活动状态依次进行逆转~
- 首先定义需要逆转的目标状态传入状态管理器修改
  1. 在状态管理器下统一进行事务处理
  • 判空

  • 修改数据库中的表状态

  • 同步 缓存中的活动状态

保存中奖记录service层URL

  1. 逆转成功之后, 保存中奖记录,同时发生短信,邮件通知中奖者

    在这里插入图片描述

保存中奖记录的主要流程

  • MySQL中查询中奖者,活动,奖品消息

  • 整合以上数据,并保存到 MySQL 的中奖记录表中

  • 缓存进 Redis奖品维度 的记录信息~

  • 缓存进 Redis活动维度 的记录信息!

  1. 若逆转失败,为 保证事务一致性,回滚事务初始状态

有始必有终, 竟然 状态扭转的过程是在状态管理器中进行的, ==那么状态的回滚也必然要通过它来操作!

  1. 状态管理器回滚事务
    • 首先由于是以奖品为维度的状态扭转, 判断奖品状态是否被逆转, 如果逆转了才需要被回滚!
    • 然后 全部回滚到处理前的原始状态
    • 最后 修改缓存Redis中的状态也为原始状态!

鱼式疯言

在上面的环节中, 小伙伴是否留意状态扭转这个代码的实现是否有点复杂~

是这样的! 小编在这里使用了常用的设计模式——责任链模式, 策略模式

如果不用这两个模式, 逆转状态的话, 可能小伙伴们可以会

  1. 判断是否需要 逆转奖品人员
  2. 先逆转 奖品和人员
  3. 然后根据 奖品的状态和人员的状态, 还得判断活动状态是否需要逆转!
  4. 最后 逆转活动

这样写必然是没有问题的,但是小伙伴是否思考过,如果可能出现多个关联的状态需要扭转, 是否会出现在原来的代码上大幅度的修改,耦合度过大的情况

如果需要 调整状态逆转的顺序,如果要复用一些功能,也达不到方便复用的便捷可能还需要扩展一部分功能,也是难以实现的~

所以我们就需要引入责任链,策略模式来解决上述的问题!

首先, 什么是责任链模式,策略模式?

简单来说:责任链模式就是每一个对象都有机会去尝试是否执行请求的要求,如果能符合能执行的条件, 就承担单一责任处理单独的业务逻辑

好比工作中, 老板考核某个员工,如果某个员工能达到考核要求, 就会分配单独的任务给他, 如果不能通过考核, 就只能寻找下一位员工进行考核,直到知道符合要求才分配任务并完成。

小编认为: 也好比一个链表,从头到尾去找,如果找到某个结果符合要求, 就交给这个节点拥有单一责任去完成请求, 所以才称之为责任链模式

策略模式,就是 将多种的算法逻辑进行各种封装,让算法逻辑变化独立于它的实现者,可以进行替换,复用等优势。

小编认为: 封装着多种不同的算法逻辑,当调用者需要用的话,就可以不断的更新替换策略 , 所以称之为策略模式~

说完概念, 不结合咱们项目的具体实现都是 “耍流氓”

AbstractActivityOperator 活动控制器

活动状态扭转控制器实现

奖品状态扭转控制器实现

中奖人员状态扭转控制器实现

在这里插入图片描述

如上图:

  • 在上层: AbstractActivityOperator 统一管理每个单一的责任节点
  • 到具体每个实现的类中, 可自行调整顺序灵活支配责任链更好的符合当下业务场景, 先处理奖品, 中奖人员的状态为 1 , 最后处理活动相关状态为 2
  • 无论是 中奖人员,奖品人员,还是活动人员的扭转状态的思路大体都是一样的首先判断是否需要扭转,最后进行逆转逻辑
  • 但是在具体的代码的实现中,是有很大差别的,这时就需要借助 策略模式 的思路,封装每一步,各自实现不同状态逆转的代码逻辑

小伙伴认识到了 责任链和策略模式 的作用!

小编就浅浅的总结下, 这俩大设计模式在等下逆转关联状态下的优势:

责任链模式的小优点:

  • 解耦合: 能有效的将请求和效应两端独立开去实现,有效的降低耦合程度
  • 扩展性: 首先责任链是可以动态调整顺序的, 不仅是使用多个场景; 并且如果需要修改和添加某个业务逻辑 , 不仅只需要实现AbstractActivityOperator 这个抽象类, 对于整个系统来说, 每个业务逻辑又是独立的,几乎不影响原有代码的实现,增加可扩展性。
  • 可维护性:如果某个实现逻辑出现异常, 可快速对责任链上的异常节点快速查找错误并解决!

策略模式的小优点:

解耦合: 对多种不同的算法逻辑进行实现, 每一种算法逻辑都进行了封装, 不会相互影响各字的实现逻辑。 降低了耦合程度

可扩展性:对上面的多种实现思路进行了 封装, 不仅方便调用,而且 能在原有的代码的基础上再创建方法实现新的算法逻辑。

复用性: 封装了多种算法逻辑,每个方法相当于一个个 小盒子不仅方便随时调用, 还和抽离出独立的一个个模块多次复用。

<2>. 死信队列重发

死信队列处理逻辑

在这里插入图片描述

如上图, 当MQ进行多次消息重发之后, 仍然失败!

下一步就会将异常的信息请求加入到死信队列。

死信队列就会实时消息, 如果有异常就需要重新检查逻辑代码的情况, 发现问题并解决!

如果异常解决了,进行MQ就会重新进行消息的重发~保证请求消息一定发送并且处理成功!

在这里插入图片描述

  1. 在本项目中, 当出现了异常,MQ会进行五次消息重发, 都重发失败后存入死信队列
  1. 当存入死信队列后, 由于死信队列会实现消费请求数据,请求消息会重新加入到普通队列中。
  1. 最后普通队列又会进行不断的消费, 如果异常没有及时处理之后,又会 重新进行消息重发~
  1. 如果 消息没有及时的处理成功, 请求消息就会不断的堆积到死信队列中

3. 中奖记录展示

<1>. 约定前后端交互接口

在这里插入图片描述

中奖记录结果类型URL

在这里插入图片描述

如上图:
前端可传入一个 活动id , 后端 处理完相关业务逻辑后,返回该活动下所以的中奖者以及中奖的相关信息 返回给前端展示~

<2>. 梳理实现逻辑

在这里插入图片描述

如时序图:

  1. 首先用户传入活动id/奖品id, 查看中奖名单
  1. 判断 Redis 是否存在中奖记录,如果存在, 就去Redis中获取直接返回
  1. 如果 Redis 不存在, 就去Mapper层调用MySQL的 中奖记录表 进行活动/奖品维度中奖记录的查询
  1. 正确从MySQL查询到的结果,不要先返回, 还需要同步Redis中的MySQL的数据, 然后才返回!

鱼式疯言

需要注意的是:

怎么辨别是 活动维度的中奖记录还是奖品维度的中奖记录

其实很简单, 小编这里教小伙伴们一招:

如果前端只是传入一个 活动id, 说明 用户只需要该活动下的所有中奖记录, 必然是活动维度的中奖记录!

如果前端既传入了 活动id ,同时也传入了 奖品id, 说明 用户是既要在当下活动的,并且也要具体到某个奖品的中奖记录, 必然是奖品维度的中奖记录!

十一. Mapper模块XML实现

Java的Mapper层URL

Mybatis的XML文件URL

Mapper层定义接口, Mybatis 的 XML 用于 实现接口的功能 , 书写SQL语句以此来操作 数据库

有一些 重要且常用的动态SQL语句,小编在这里需要重点介绍一下~

1. 常用易错语句详解

主要查询用户的数量, 电话号码,邮箱,身份信息相关的功能,这边基本的小编就不赘述了, 其中数量的查询是可以用 count(1) 这样的调用方式 ~

需要注意的有——> 条件查询, 匹配查询, 分批传入

下面小编拿两个表举例细讲哦~

<1>. 条件查询

用户表Mapper接口URL

用户表XML实现URL

<select id="selectUserListByIdentity" resultType="com.example.lottery_system.dao.DataObject.UserDO">
        select * from user
        <if test =" identity!=null">
        where `identity` = #{identity}
        </if>
        order by id desc
    </select>

上面的条件查询, 使用 <if></if> 标签其中标签上使用 test 填入需要判定的条件, 并且需要用双引号包裹如果双引号里面标签满足, 就执行 <if></if> 标签里面的 where identity = #{identity} 内容。

<2>. 匹配查询

<select id="selectDOByUserIds" resultType="com.example.lottery_system.dao.DataObject.UserDO">
        select * from user where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
</select>

匹配查询的作用, 就是把 多个单一的元素放在一起去查

就像上面这段语句,
where id in 后面加上一个 <foreach></foreach> 表示 循环遍历元素

  • 其中在标签上 collection 表示 传入的列表名
  • item 表示需要操作的 每一个元素的定义
  • open 表示 in开始
  • separator 表示 每一个元素用什么符号分隔
  • close 表示 in结束

等效于SQL语句

select * from user where id in (id1, id2 , id3 ...);

<3>. 分批插入

中奖记录表Mapper接口URL

中奖记录表XML实现URL

多条数据一次性插入库表

 <insert id="insertDoList">
        insert into winning_record (activity_id, activity_name,
                                    prize_id,prize_name , prize_tier,
                                    winner_id,winner_name , winner_email, winner_phone_number, winning_time)
            values <foreach collection="items" item="item" separator="," >
                (#{item.activityId} , #{item.activityName},
                #{item.prizeId} , #{item.prizeName} , #{item.prizeTier},
                #{item.winnerId} , #{item.winnerName},#{item.winnerEmail},#{item.winnerPhoneNumber}, #{item.winningTime})
    </foreach>
    </insert>

如上面的语句

  • values 后加入 <foreach></foreach> 标签 表示需要循环插入数据
  • 其中在标签上, collection 表示 设置的列表参数名
  • item 表示需要操作的每一个对象名
  • separator 表示每一个括号之间使用逗号分隔

等效于SQL语句

insert into winning_record (activity_id, activity_name,
 prize_id,prize_name , prize_tier,
                                
   winner_id,winner_name , winner_email,
    winner_phone_number, winning_time)   values (#{item1.activityId} , #{item1.activityName},
                #{item1.prizeId} , #{item1.prizeName} , #{item1.prizeTier},
                #{item1.winnerId} , #{item1.winnerName},#{item1.winnerEmail},#{item1.winnerPhoneNumber}, #{item1.winningTime}) , 
                (#{item2.activityId} , #{item2.activityName},
                #{item2.prizeId} , #{item2.prizeName} , #{item2.prizeTier},
                #{item2.winnerId} , #{item2.winnerName},#{item2.winnerEmail},#{item2.winnerPhoneNumber}, #{item2.winningTime});
                

其他SQL都是比较简单的, 小伙伴们可以细看小编的文章来学习哦~

Mybatis基础操作详解文章URL

12. 项目部署与项目代码gitee

1. 项目部署

当我们写完项目代码并成功运行项目, 并没有出现 BUG 的后, 我们就可以 部署到自己的服务器上让所有人访问我们这个项目了~

<1>. 添加配置

  1. 开发环境配置转化为 测试环境配置

当需要部署项目的时候, 我们就需要 把平常在本地的配置修改成服务器兼容的配置才可正常使用

当需要使用 部署环境, 直接在主配置文件的等号右边写上test

spring.profiles.active=test

如果是 自身的开发环境, 直接在主配置文件的等号右边写上dev

spring.profiles.active=dev

鱼式疯言

在这里插入图片描述

什么是开发环境, 什么是测试环境, 什么又是线上/生产环境, 想必小伙伴们一定听过很多吧~

这里小编就大致的说明一下 企业级的开发环境

开发环境: 就是我们 程序猿们自身编写代码,自己测试, 自己玩转的环境~

测试环境开发环境完成之后就会进入测试环境,由 测试人员测试咱们开发人员的写的代码,保存咱们软件的产品质量。

线上/生产环境测试环境完成之后就进入线上/生产环境真正到达用户的手上, 线上用户使用软件的环境~

由于咱们是个人项目, 所以就不分什么 测试环境, 和 线上/生产环境 , 都统一称为 test 测试环境

  1. 日志相关配置

Java日志等级的声明URL

日志打印的文件路径URL

小伙伴们复制即可,这里不做了解哦~

<2>. 打包jar

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

<3>. 部署项目

  • 拖拽上传文件

在这里插入图片描述

  • 运行项目

使用 java -jar 【jar包名】 运行程序

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

运行成功,看看效果~

在这里插入图片描述

在这里插入图片描述

收工,over~

如果对这边部署有问题的小伙伴们, 可以参考小编写的具体的部署项目的文章详解哦~

项目部署细节详解URL

2. 项目全代码gitee

相比打开过前面URL的小伙伴应该都明白, 前面的URL 也出自这里小编的个人 gitee

项目全代码展示giteeURL

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邂逅岁月

感谢干爹的超能力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值