SSM-项目实战

开发环境搭建

  • 创建数据库

    1
    create database rjwm character set utf8mb4;
  • 导入数据

    1
    source db_reggie.sql
  • 创建springboot 项目

  • 创建vue2 项目

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建 vue 项目
    vue create vue-rjwm
    // 添加 element-ui
    vue add element
    // 添加 axios
    yarn add axios
    // 添加vue-router
    yarn add vue-router@3
    // 添加 less less-loader
    yarn add less@3
    yarn add lesss-loader@3

跨域解决方案

  • @CrossOrigin 注解的使用
  • nginx 搭建api
  • 微服务网关搭建解决跨域
  • vue 脚手架devServer 配置代理

SpringBoot

  • 创建springboot 项目

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>rjwm</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <java.version>1.8</java.version>
    </properties>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>

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

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

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

    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
    </dependency>

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
    </dependency>

    <dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>2.4.5</version>
    </plugin>
    </plugins>
    </build>

    </project>
  • 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    server:
    port: 8081
    spring:
    application:
    name: rjwm
    datasource:
    druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/rjwm?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root
    mybatis-plus:
    configuration:
    # 在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    global-config:
    db-config:
    id-type: ASSIGN_ID
  • springboot 启动

    端口配置 测试
    端口配置 测试
  • vue 启动测试

    接口测试
    接口测试
  • 统一返回处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.coderitl.rjwm.common;

    import lombok.Data;

    import java.util.HashMap;
    import java.util.Map;

    @Data
    public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
    R<T> r = new R<T>();
    r.data = object;
    r.code = 1;
    return r;
    }

    public static <T> R<T> success(T object, String msg) {
    R<T> r = new R<T>();
    r.data = object;
    r.code = 1;
    r.msg = msg;
    return r;
    }

    public static <T> R<T> error(String msg) {
    R r = new R();
    r.msg = msg;
    r.code = 0;
    return r;
    }

    public R<T> add(String key, Object value) {
    this.map.put(key, value);
    return this;
    }

    }

  • 模块的导入导出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 默认导出 只能有一个在一个模块中
    export default xxx

    // 导出
    export function foo(){}
    // 导出变量
    exoport let foo = 10;

    // 导入default
    import xx from "defaultExport"
    import {xxx} from "xx"
    // 统一导入
    import * as asNewName from "xx"
    asNewName.xxx

  • 封装axios

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import axios from "axios"

    export function request(config) {
    const instance = axios.create({
    baseURL: 'http://localhost:8081',
    timeout: 5000
    })
    // 拦截器

    // 发送真正的网络请求(Promise)
    return instance(config)
    }
    • 模块化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 员工模块
      import {request} from "@/api/index";

      // 登录
      export function loginApi(data) {
      return request({
      url: '/employee/login',
      method: 'post',
      data
      })
      }
      ...
    • 使用

      1
      2
      3
      4
      5
      // 后期容易维护
      import {loginApi} from "@/api/employee";

      let res = await loginApi(this.loginForm)

后台系统登录功能

  • 流程分析与实现

    流程分析 实现
    流程分析 实现
  • 数据获取

    表单数据传递
    表单数据传递
  • 路由处理

    • 前端路由拦截处理

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 挂载路由导航守卫
      router.beforeEach((to, from, next) => {
      // to 将要访问的路径
      if (to.path === '/login') {
      return next();
      }
      // from 代表从那个路径跳转而来
      const tokenStr = window.sessionStorage.getItem('userInfo')
      if (!tokenStr) {
      console.log(tokenStr);
      return next('/login');
      }
      // next 放行 next('/home')强制跳转
      next();
      });
    • springboot 过滤器实现步骤

      • 创建过滤器

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        package com.coderitl.rjwm.filter;

        import com.alibaba.fastjson.JSON;
        import com.coderitl.rjwm.common.R;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.util.AntPathMatcher;

        import javax.servlet.*;
        import javax.servlet.annotation.WebFilter;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import java.io.IOException;

        /**
        * 检查用户是否已经登录
        */
        @WebFilter("/*")
        @Slf4j
        public class checkLogin implements Filter {
        // 路径匹配器,支持通配符
        public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        // 日志记录
        log.info("登录拦截器拦截: {}", request.getRequestURI());

        // 1. 获取本次请求的`uri`
        String requestURI = request.getRequestURI();
        // 定义不需要处理的请求路径
        String[] urls = new String[]{
        // 登录请求不处理
        "/employee/login",
        // 退出请求不处理
        "/employee/logout"
        };
        // 2. 判断本次请求是否需要处理
        boolean notInUrls = check(urls, requestURI);
        // 3. 如果不需要处理,则直接放行
        if (notInUrls) {
        // 直接放行
        filterChain.doFilter(request, response);
        return;
        }
        // 4. 判断登录状态,如果已经登录,则直接放行
        if (request.getSession().getAttribute("employee") != null) {
        // 直接放行
        filterChain.doFilter(request, response);
        return;
        }
        // 5. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
        }

        /**
        * 路径匹配,检查本次去请求是否需要放行
        *
        * @param urls
        * @param requestURI
        * @return
        */
        public boolean check(String[] urls, String requestURI) {
        for (String url : urls) {
        // 路径是否匹配
        boolean match = PATH_MATCHER.match(url, requestURI);
        // 如果匹配,返回 true
        if (match) {
        return true;
        }
        }
        // 不匹配
        return false;
        }
        }

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        // 前端拦截器 axios
        // 拦截器
        instance.interceptors.response.use(
        res => {
        console.log('---响应拦截器---', res);
        console.log(res.data.code)
        if (res.data.code === 0 && res.data.msg === 'NOTLOGIN') {
        // 返回登录页面
        window.top.location.href = '/login';
        } else {
        console.log("login ok...")
        sessionStorage.setItem("userInfo",JSON.stringify(res))
        return res.data;
        }
        },
        error => {
        let { message } = error;
        if (message == 'Network Error') {
        message = '后端接口连接异常';
        } else if (message.includes('timeout')) {
        message = '系统接口请求超时';
        } else if (message.includes('Request failed with status code')) {
        message =
        '系统接口' + message.substr(message.length - 3) + '异常';
        }
        window.vant.Notify({
        message: message,
        type: 'warning',
        duration: 5 * 1000,
        });
        //window.top.location.href = '/front/page/no-wify.html'
        return Promise.reject(error);
        }
        );
        1
        2
        // 登陆的 处理事件处
        data 解构时需要注意
      • 在启动类上加入注解@ServletComponentScan

      • 完善过滤器的处理逻辑

      • 具体实现

        1. 获取本次请求的uri
        2. 判断本次请求是否需要处理
        3. 如果不需要处理,则直接放行
        4. 判断登录状态,如果已经登录,则直接放行
        5. 如果未登录则返回未登录结果
      • 流程图分析

        过滤器实现
        过滤器实现
  • 登录按钮事件处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 登录按钮点击事件
    handleLogin() {
    // 登录预验证
    this.$refs.loginFormRef.validate(async valid => {
    console.log(valid);
    // 预验证未通过时终止
    if (!valid) return;
    // 预验证通过发起ajax请求 将 data 解构重命名为 res
    const { data: res } = await loginApi(this.loginForm);
    console.log(res);
    // 1: 前后端约定 "1" 为登录成功
    if (String(res.code) === '1') {
    this.$message.success(res.msg);
    // 存储在 sessionStorage 中鉴权
    sessionStorage.setItem(
    'userInfo',
    JSON.stringify(res.data)
    );
    // 跳转 home
    this.$router.push('/home');
    } else {
    // 响应错误信息
    this.$message.error(res.msg);
    }
    });
    },
    }
  • 登录

    登录
    登录

前端路由

  • 路由实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    path: '/home',
    redirect: '/employeeinfo',
    component: () => import('@/components/Home.vue'),
    children: [
    {
    path: '/employeeinfo',
    component: () => import('@/pages/employee/EmployeeInfo.vue'),
    },
    ],
    }
  • 路由分析

    子路由
    子路由
  • 需求

    1
    /home/employee

新增员工

  • 原型图

    添加员工信息
    添加员工信息
  • 数据模型

    新增员工,其实就是将我们新增页面录入的员工数据插入到employee 表中,字段username 加入了唯一约束,因为username 是员工的登录账号,必须是唯一的

  • 代码开发

    • 执行过程
      1. 页面发送ajax 请求,将新增的员工页面中输入的数据以json 的形式提交到服务端
      2. 服务端Controller 接受页面提交的数据并调用Service 将数据进行保存
      3. Service 调用Mapper | dao 操作数据库,保存数据
  • 实体分析

    • username

      唯一约束(实现用户名唯一约束)
      唯一约束
    • status

      禁用与正常
      禁用与正常
  • 请求与参数问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 添加员工
    export function saveEmployee(params) {
    return request({
    url: '/employee/saveemployee',
    method: 'post',
    // json数据
    data: { ...params }
    })
    }

    // 保存与保存继续添加
    submitForm() {
    // 组织数据为 json
    const params = {
    ...this.ruleForm,
    // 改变存储
    sex: this.ruleForm.sex === '男' ? '0' : '1'
    }
    // 预验证
    this.$refs.ruleForm.validate((valid) => {
    // 如果预验证失败,终止
    if (!valid) return this.$message.error('信息错误')
    if (valid) {
    // 发起 axios 请求 params 就是 json 格式数据
    saveEmployee(params).then((res) => {
    console.log(res)
    // 再次发起请求 pageApi(this.queryInfo)
    })
    }


    })
    },
  • 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 添加员工信息
    @PostMapping("/saveemployee")
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
    log.info("添加员工信息: {}", employee.toString());
    // 设置初始密码 并使用 md5 加密
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
    // 设置创建时间
    employee.setCreateTime(LocalDateTime.now());
    // 设置更新时间
    employee.setUpdateTime(LocalDateTime.now());

    // 获取当前登录用户的 id
    log.info("empId: {}", request.getServletContext().getAttribute("employee"));
    if (request.getServletContext().getAttribute("employee") != null) {
    Long empId = (Long) request.getServletContext().getAttribute("employee");
    // 设置创建的用户
    employee.setCreateUser(empId);
    employee.setUpdateUser(empId);
    log.info("组织完毕: {}", employee);
    // 执行添加
    employeeService.save(employee);
    }
    return R.success("新增员工成功");
    }
  • sessionID 存在生命周期

    直接使用request.setAttribute("","") 存储时,对于前后端分离项目session 是有生命周期的,在不同的接口方法中session 无法获取

    request 存取的session 状态
    request存取的session状态
    • 解决方案

      1
      2
      3
      4
      5
      6
      // 提示 session 的作用域(登录时)
      request.getServletContext().setAttribute("employee", emp.getId());

      // 需要获取的地方
      request.getServletContext().getAttribute("employee")

  • 异常处理

    • 异常产生原因

      字段username 添加了unique 约束

      日志输出
      日志输出
    • 解决

      1. Controller 方法中加入try..catch 进行异常捕获

      2. 使用异常处理器进行全局异常捕获(推荐)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        package com.coderitl.rjwm.common;

        import lombok.extern.slf4j.Slf4j;
        import org.springframework.stereotype.Controller;
        import org.springframework.web.bind.annotation.ControllerAdvice;
        import org.springframework.web.bind.annotation.ExceptionHandler;
        import org.springframework.web.bind.annotation.ResponseBody;
        import org.springframework.web.bind.annotation.RestController;

        import java.sql.SQLIntegrityConstraintViolationException;

        /**
        * 全局异常处理
        * annotations = {RestController.class, Controller.class} 扫描带有如下注解的控制器进行处理
        */
        @ControllerAdvice(annotations = {RestController.class, Controller.class})
        @ResponseBody // 返回 JSON 数据
        @Slf4j // 日志
        public class GlobalExceptionHandler {

        // 处理 username 重复异常
        @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
        public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        if (ex.getMessage().contains("Duplicate entry")) {
        String[] split = ex.getMessage().split(" ");
        String msg = split[2] + "用户已存在!";
        return R.error(msg);
        }
        return R.error("未知错误");
        }
        }

分页查询(模糊查询)

  • 演示

    分页实现
    分页实现
  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.coderitl.rjwm.config;

    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    // mybatis-plus 配置拦截器
    @Configuration
    public class MybatisPlusPageConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return interceptor;
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 接口实现
    /**
    * 搜索: 分页 模糊查询
    *
    * @param page
    * @param pagesize
    * @param username
    * @return
    */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pagesize, @RequestParam("keyword") String username) {
    log.info("pagg: {}, pagesize: {} username: {}", page, pagesize, username);
    // 构造分页构造器
    Page pageInfo = new Page(page, pagesize);
    // 构造条件构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    // // 不等于空 SQL 才会出现
    queryWrapper.like(StringUtils.isNotEmpty(username), Employee::getUsername, username);
    // // 执行查询
    employeeService.page(pageInfo, queryWrapper);
    return R.success(pageInfo, "成功获取数据所有数据!");
    }
  • 使用element-组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <el-pagination
    @size-change='handleSizeChange'
    @current-change='handleCurrentChange'
    :current-page='queryInfo.page'
    :page-sizes='[5, 10, 25, 50]'
    :page-size='queryInfo.pagesize'
    layout='total, sizes, prev, pager, next, jumper'
    :total='total'
    ></el-pagination>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 分页
    export function pageApi(params) {
    return request({
    url: '/employee/page',
    method: 'get',
    params
    })
    }


    // 发送请求
    // 控制 pageSize 改变
    async handleSizeChange(val) {
    // 控制 pageSize
    this.queryInfo.pagesize = val
    const parmas = {
    ...this.queryInfo
    }
    const { data: res } = await pageApi(parmas)
    this.tableData = res.data.records
    },
    // 控制 page 改变
    async handleCurrentChange(val) {
    this.queryInfo.page = val
    const parmas = {
    ...this.queryInfo
    }
    // 再次发起请求
    const { data: res } = await pageApi(parmas)
    this.tableData = res.data.records
    },

公共字段自动填充

  • 实现步骤

    1. 在实体类的属性上加入@TableField 注解,指定自动填充的策略

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @TableField(fill = FieldFill.INSERT)
      private LocalDateTime createTime;

      @TableField(fill = FieldFill.INSERT_UPDATE)
      private LocalDateTime updateTime;

      @TableField(fill = FieldFill.INSERT)
      private Long createUser;

      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Long updateUser;
    2. 安装框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler 接口

ThreadLocal

  • 什么是ThreadLocal

    ThreadLocal 并不是一个Thread,而是Thread 的局部变量,当使用ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不是影响其他线程所对应的副本,ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问

  • 常用方法

    • set(T value)

      1
      public void set(T value) // 设置当前线程的线程局部变量的值
    • get()

      1
      publiv T get() // 返回当前线程所对应的线程局部变量的值

    我们可以在filter doFilter 方法中获取对应当前登录用户id,并调用ThreadLocal set 方法来设置当前线程的局部变量的值(用户id),然后再MyMetaObjectHandler updateFill 方法中调用ThreadLocal get 方法来获取当前所对应的线程局部变量的值(用户id)