瑞吉外賣實戰專案全攻略——總結篇

2022-11-01 09:01:01

瑞吉外賣實戰專案全攻略——總結篇

該系列將記錄一份完整的實戰專案的完成過程,該篇屬於總結篇,主要負責總結整個專案技術點和注意點

案例來自B站黑馬程式設計師Java專案實戰《瑞吉外賣》,請結合課程資料閱讀以下內容

我們將從下面幾個方面進行總結:

  • 專案整體介紹
  • 專案准備階段
  • 後臺程式碼開發
  • 前臺程式碼開發
  • 專案部署階段

專案整體介紹

本小節我們將介紹專案的整體構架

前言

該專案是我完成的第一份整體專案,主要採用SpringBoot框架來進行完成,該專案中我們只負責後端程式碼開發以及單元測試

專案本身介紹

專案整體介紹:

  • 本專案(瑞吉外賣)是專門為餐飲企業(餐廳、飯店)客製化的一款軟體產 品,包括系統管理後臺和行動端應用兩部分。
  • 其中系統管理後臺主要提供給餐飲企業內部員工使用,可以對餐廳的菜品、套餐、訂單等進行管理維護。
  • 行動端應用主要提供給消費者使用,可以線上瀏覽菜品新增購物車、下單等。

專案技術展示

我們的專案主要採用以下技術完成:

專案功能展示

我們的專案主要需要完成以下功能:

專案准備階段

本小節我們將介紹專案的準備工作

資料庫

我們的資料庫主要採用MYSQL來儲存資料,下面介紹資料整體框架:

此外,我們採用Redis資料庫來進行快取優化階段,用於儲存頁面資訊以及菜品資訊:

開發工具

我們的開發工具主要包括有:

  • IDEA:程式碼書寫軟體

  • Tomcat:常用伺服器

  • Junit:業務層功能測試工具

  • Postman:服務層功能測試工具

  • Maven:依賴匯入下載jar包必備

  • Git:程式碼備份工具,儲存各版本程式碼以便於回顧檢查

檔案工具

我們的檔案工具主要採用:

  • Swagger:採用knife4j的Swagger拓展工具,設定Docket和ApiInfo設定方法,查詢doc.html即可存取介面檔案

主要使用步驟:

  1. 匯入依賴座標
        <!--knife4j(Swagger)座標-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.2</version>
        </dependency>
  1. 書寫設定方法
package com.qiuluo.reggie.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.qiuluo.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.List;

// 新匯入兩個註解@EnableSwagger2,@EnableKnife4j
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {

        // 系統自動幫忙生成這doc.html頁面用於展示我們的介面資訊,我們需要將他們放行
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        
    }

    @Bean
    public Docket createRestApi() {
        // 檔案型別
        // (返回一個檔案型別Docket,下面是返回檔案的型別,基本為固定形式,除了basePackage,書寫你的Controller包的位置)
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.qiuluo.reggie.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        // 描述檔案內容
        return new ApiInfoBuilder()
                .title("瑞吉外賣")
                .version("1.0")
                .description("瑞吉外賣介面檔案")
                .build();
    }
}
  1. 書寫檔案註釋資訊
// 這裡僅給出檔案註釋(不給出範例)

// 用於請求的類上,表示對類的說明(Controller)
@Api(tags = "")				

// 用於類上,通常是實體類,表示一個返回資料的資訊(domain,Result)
@ApiModel(value = "")			

// 用於屬性上,描述相應類的屬性(name)
@ApiModelProperty(value = "")	

// 用於請求的方法上,說明方法的用途,作用
@ApiOperation(value = "")		

// 用於請求的方法上,表示一組引數說明
@ApiImplicitParams({@ApiImplicitParam,@ApiImplicitParam})	

// 用於請求的方法上,表示單個引數說明
@ApiImplicitParam(name = "",value = "",required = true/false)	
  1. 開啟doc.html頁面即可

後臺程式碼開發

本小節我們將介紹專案的後臺開發階段

設定對映地址

在專案開始前,我們都需要設定對映地址,讓前端頁面展示直接進行頁面展示而不是對映在服務層方法上:

  • 建立一個設定類WebMvcConfig
  • 設定為設定類@Configuration
  • 繼承WebMvcConfigurationSupport
  • 實現方法addResourceHandlers並設定對映地址

我們給出專案中的範例展示:

package com.qiuluo.reggie.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.qiuluo.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.List;

@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("開始靜態對映");

        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

        //掃描static下的所有html頁面
        registry.addResourceHandler("classpath:/static/*.html");
        //掃描static下的所有資源
        registry.addResourceHandler("/static/**");

        // 系統自動幫忙生成這doc.html頁面用於展示我們的介面資訊,我們需要將他們放行
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

登入退出功能

我們一個專案的首先實現就是來完成登入與退出,本身的邏輯很簡單,但我們需要在登入退出時對頁面進行部分操作:

  • 在Session中記錄或刪除使用者的網頁資訊,因為我們後期需要根據Session內部的資訊來判斷使用者是否登入

我們給出專案中的範例展示:

package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Employee;
import com.qiuluo.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**  登入功能處理邏輯如下:
     1、將頁面提交的密碼password進行 MD5 加密處理
     2、根據頁面提交的使用者名稱username查詢資料庫
     3、如果沒有查詢到資料,則返回登入失敗的結果
     4、進行密碼比對,如果不一致,則返回登入失敗的結果
     5、檢視員工狀態,如果為 已禁用狀態,則返回被禁用的結果資訊
     6、登入成功,將員工id 存入Session並返回登入成功的結果
     * */

    @PostMapping("/login")
    public Result<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());

        Employee emp = employeeService.getOne(queryWrapper);

        if (emp == null){
            return Result.error("使用者名稱不存在!");
        }

        if (!emp.getPassword().equals(password)){
            return Result.error("使用者名稱或密碼錯誤!");
        }

        if (emp.getStatus() == 0){  // 賬號被禁用,status == 1,賬號可以正常登入
            return Result.error("賬號被禁用,請聯絡管理員或客服!");
        }

        // 將使用者ID存入Session
        request.getSession().setAttribute("employee",emp.getId());

        return Result.success(emp);

    }

    //  退出功能實現
    //  1、LocalStorage 清理Session中的使用者id
    //  2、返回結果

    @PostMapping("/logout")
    public Result<String> logout(HttpServletRequest request){
        
        // 將使用者ID移出Session
        request.getSession().removeAttribute("employee");
        
        return Result.success("安全退出成功!");
    }
    
}

實現登入過濾

我們的前臺和後臺頁面不能隨意直接存取,所以我們需要判定使用者是否登入,若登入後才可以進入頁面進行操縱

我們採用過濾器來完成這部分功能實現,過濾器實現步驟:

  • 建立一個過濾器類
  • 繼承Filter
  • 設定為過濾器類@WebFilter(fileName = "loginCheckFilter",urlPatterns = "/*")
  • 實現doFilter方法

在其中我們還採用了一個路徑匹配器:

//路徑匹配器,支援萬用字元 PATH_MATCHER.match(A,B);
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

我們判斷是否登入成功的依據來自設定登入時的Session內容:

// 前臺
request.getSession().getAttribute("user") != null

// 後臺
request.getSession().getAttribute("employee") != null

我們給出專案中的範例展示:

package com.qiuluo.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.qiuluo.reggie.common.BaseContext;
import com.qiuluo.reggie.common.Result;
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(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter 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;

        //1、獲取本次請求的URI
        String requestURI = request.getRequestURI();// /backend/index.html

        log.info("攔截到請求:{}",requestURI);

        //定義不需要處理的請求路徑
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/login",
                "/user/sendMsg",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs"
        };


        //2、判斷本次請求是否需要處理
        boolean check = check(urls, requestURI);

        //3、如果不需要處理,則直接放行
        if(check){
            log.info("本次請求{}不需要處理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        //4-1、判斷後臺登入狀態,如果已登入,則直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("使用者已登入,使用者id為:{}",request.getSession().getAttribute("employee"));

            log.info("登入中...");
            log.info("執行緒id" + Thread.currentThread().getId());

            // 我們在這裡獲得empID用於公共部分自動填充
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }

        //4-2、判斷行動端登入狀態,如果已登入,則直接放行
        if(request.getSession().getAttribute("user") != null){
            log.info("使用者已登入,使用者id為:{}",request.getSession().getAttribute("user"));

            log.info("登入中...");
            log.info("執行緒id" + Thread.currentThread().getId());

            // 我們在這裡獲得empID用於公共部分自動填充
            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);

            filterChain.doFilter(request,response);
            return;
        }

        log.info("使用者未登入");
        //5、如果未登入則返回未登入結果,通過輸出流方式向用戶端頁面響應資料
        response.getWriter().write(JSON.toJSONString(Result.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);
            if(match){
                return true;
            }
        }
        return false;
    }
}

工具類的使用

工具類是為了提供一些通用的、某一非業務領域內的公共方法,不需要配套的成員變數,僅僅是作為工具方法被使用。

專案中的工具類是藉助LocalThread的當前執行緒儲存功能來設定工具類,我們只需要定義LocalThread並給出其方法的新方法定義即可

我們給出專案中的範例展示:

package com.qiuluo.reggie.common;

/**
 * 基於ThreadLocal的工具類,用於儲存使用者id
 * 工具類方法大多數是靜態方法,因為重新定義一個類需要一定記憶體,直接設定為靜態方法可以節省記憶體
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

設定公共欄位

我們在資料庫中會注意到我們的各種菜品,套餐等都具有統一的引數,我們將他們稱為公共欄位

同時這些欄位基本需要初始化設定或者在修改更新時進行資料的更新設定,所以我們希望統一進行設定來簡化操作

我們採用MyBatisPlus提供的公共欄位自動填充的功能:

我們先來簡單介紹一下流程:

  1. 首先在我們需要修改的欄位屬性上新增註解:
// 屬性包括有INSERT,UPDATE,INSERT_UPDATE

@TableField(fill = FieldFill.屬性)
  1. 按照框架書寫後設資料物件處理器,需要實現MetaObjectHandler介面
package com.qiuluo.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

// 記得設定為設定類
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 新增時自動設定
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {

    }

    /**
     * 修改時自動設定
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {

    }
}
  1. 在後設資料物件處理器中對方法進行書寫,在此類中統一為公共欄位設定值,藉助了LocalThread來獲得當前使用者ID
package com.qiuluo.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 新增時自動設定
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共欄位自動填充[insert]...");
        log.info(metaObject.toString());
        
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }

    /**
     * 修改時自動設定
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共欄位自動填充[update]...");
        log.info(metaObject.toString());

        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
}

實現型別轉換

我們在專案遇到的一個簡單的小問題:

  • 我們的empId設計為Long型,其中資料庫為19位,但網頁的JS為16位元,這就會導致empId傳遞時會出現損失

我們通過採用訊息轉換器來實現傳送型別發生改變:

  • 使網頁的Long型傳遞過來時變為String型別,在傳遞到後端之後,再變為Long型賦值給後端程式碼

我們的要實現訊息轉化器主要需要兩步:

  1. 設定一個訊息轉換器
package com.qiuluo.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 物件對映器:基於jackson將Java物件轉為json,或者將json轉為Java物件
 * 將JSON解析為Java物件的過程稱為 [從JSON反序列化Java物件]
 * 從Java物件生成JSON的過程稱為 [序列化Java物件到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知屬性時不報異常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化時,屬性不存在的相容處理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //註冊功能模組 例如,可以新增自定義序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}
  1. 將該訊息轉換器設定到設定類中
package com.qiuluo.reggie.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.qiuluo.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.List;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 擴充套件mvc框架的訊息轉換器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("擴充套件訊息轉換器...");
        //建立訊息轉換器物件
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //設定物件轉換器,底層使用Jackson將Java物件轉為json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //將上面的訊息轉換器物件追加到mvc框架的轉換器集合中
        converters.add(0,messageConverter);
    }

}

實現例外處理

我們專案中的例外處理通常分為兩部分:

  • 系統意外異常
  • 自定義業務異常

我們在後臺不可避免地發生錯誤,這些錯誤通常被稱為系統意外異常

處理系統意外異常我們只需要設定例外處理器即可:

package com.qiuluo.reggie.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全域性例外處理
 * @ControllerAdvice 來書寫需要修改異常的註解類(該類中包含以下註解)
 * @RESTControllerAdvice 表示annotations為RestController.class的@ControllerAdvice
 * @ResponseBody 因為返回資料為JSON資料,需要進行格式轉換
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 處理異常
     * @ExceptionHandler 來書寫需要修改的異常
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){

        // 我們可以通過log.error輸出錯誤提醒(我們可以得到以下提示資訊:Duplicate entry '123' for key 'employee.idx_username')
        log.error(ex.getMessage());
        // 我們希望將id:123提取出來做一個簡單的反饋資訊
        if (ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return Result.error(msg);
        }
        return Result.error("未知錯誤");
    }
}

程式設計師在後臺自我設定的異常被稱為自定義業務異常,通常用於業務層的功能實現無法實現時丟擲異常給使用者檢視

設定自定義異常主要分為兩步:

  1. 設定自定義異常類
package com.qiuluo.reggie.common;

/**
 * 自定義業務異常類
 */
public class CustomException extends RuntimeException{

    // 實現一個構造方法即可
    public CustomException(String message){
        super(message);
    }
}
  1. 將該自定義異常加入例外處理器即可
package com.qiuluo.reggie.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全域性例外處理
 * @ControllerAdvice 來書寫需要修改異常的註解類(該類中包含以下註解)
 * @ResponseBody 因為返回資料為JSON資料,需要進行格式轉換
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 處理異常
     * @ExceptionHandler 來書寫需要修改的異常
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){

        // 我們可以通過log.error輸出錯誤提醒(我們可以得到以下提示資訊:Duplicate entry '123' for key 'employee.idx_username')
        log.error(ex.getMessage());
        // 我們希望將id:123提取出來做一個簡單的反饋資訊
        if (ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return Result.error(msg);
        }
        return Result.error("未知錯誤");
    }

    /**
     * 處理自定義異常
     * @ExceptionHandler 來書寫需要修改的異常
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public Result<String> CustomExceptionHandler(CustomException ex){

        log.error(ex.getMessage());

        return Result.error(ex.getMessage());
    }
}

檔案上傳下載

我們的檔案上傳下載操作之前主要依靠Apache的兩個元件:commons-fileupload 和 commons-io

現在我們的檔案上傳下載經過簡化可以採用簡單的方法來實現

首先我們給出檔案上傳程式碼:

package com.qiuluo.reggie.controller;

import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;

/*
我們通過MultipartFile獲得檔案,但這個檔案是暫時性的,我們需要把他儲存在伺服器中
*/

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {

    // 定義主路徑(在yaml中設定一個自定義路徑即可)
    @Value("${reggie.path}")
    private String BasePath;

    /**
     * 上傳操作
     * @param file 注意需要與前端傳來的資料名一致
     * @return
     */
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file){
        // 注意:file只是一個臨時檔案,當我們的request請求結束時,file也會消失,所以我們需要將它儲存起來

        // 這個方法可以獲得檔案的原名稱,但不推薦設定為檔名儲存(因為可能出現重複名稱導致檔案覆蓋)
        String originalFilename = file.getOriginalFilename();

        // 將原始檔案的字尾擷取下來
        String substring = originalFilename.substring(originalFilename.lastIndexOf("."));

        // UUID生成隨機名稱,檔名設定為 UUID隨機值+原始檔字尾
        String fileName = UUID.randomUUID().toString() + substring;

        // 判斷資料夾是否存在,若不存在需建立一個
        File dir = new File(BasePath);

        if (!dir.exists()){
            dir.mkdirs();
        }

        // 這個方法可以轉載檔案到指定目錄
        try {
            file.transferTo(new File(BasePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Result.success(fileName);
    }
}

我們再給出檔案下載程式碼:

package com.qiuluo.reggie.controller;

import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {

    @Value("${reggie.path}")
    private String BasePath;

    /**
     * 檔案下載
     * @param name
     * @param response
     * @return
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){

        try {
            // 輸入流獲得資料
            FileInputStream fileInputStream = new FileInputStream(new File(BasePath + name));

            // 輸出流寫出資料
            ServletOutputStream outputStream = response.getOutputStream();

            // 設定檔案型別(可設可不設)
            response.setContentType("image/jpeg");

            // 轉載資料
            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }

            // 關閉資料
            fileInputStream.close();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

簡單功能開發

我們的專案中大多都是簡單功能,可以直接根據MyBatisPlus提供的基本方法完成,我們在這裡介紹簡單模板:

  1. 在專案中檢視該方法的請求資訊

  1. 在專案中檢視該方法的請求資料

  1. 實現實體類
package com.qiuluo.reggie.domain;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 分類
 */
@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //型別 1 菜品分類 2 套餐分類
    private Integer type;


    //分類名稱
    private String name;


    //順序
    private Integer sort;


    //建立時間
    @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;

}
  1. 實現業務層介面
package com.qiuluo.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;

public interface CategoryService extends IService<Category> {
}
  1. 實現業務層
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.mapper.CategoryMapper;
import com.qiuluo.reggie.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
  1. 實現服務層
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {

    @Autowired
    private CategoryServiceImpl categoryService;

    @PostMapping
    public Result<String> save(@RequestBody Category category){
        categoryService.save(category);
        return Result.success("新增成功");
    }

}

複雜功能開發

有時候我們的MyBatisPlus提供的簡單方法不足以滿足我們的需求,這時我們就需要採用MyBatis的原始方法來定義方法完成功能開發

例如我們的需求中需要進行部分判斷或操作兩個資料表,我們需要建立新方法來完成新功能的開發:

  1. 在專案中檢視該方法的請求資訊

  1. 在專案中檢視該方法的請求資料

  1. 在業務層介面定義方法
package com.qiuluo.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;

public interface CategoryService extends IService<Category> {

    // 根據id刪除
    public Result<String> remove(Long id);
}
  1. 在業務層實現方法
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.mapper.CategoryMapper;
import com.qiuluo.reggie.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private SetmealServiceImpl setmealService;

    // 實現方法
    public Result<String> remove(Long id){

        // 判斷是否有菜品相連
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);

        int count1 = dishService.count(dishLambdaQueryWrapper);

        if (count1 > 0){
            // 丟擲業務異常
            throw new CustomException("已有菜品關聯,無法刪除!");
        }

        // 判斷是否有套餐相連
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);

        int count2 = setmealService.count(setmealLambdaQueryWrapper);

        if (count2 > 0){
            // 丟擲業務異常
            throw new CustomException("已有套餐關聯,無法刪除!");
        }

        super.removeById(id);

        return Result.success("成功刪除");
    }

}
  1. 在服務層使用方法
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {

    @Autowired
    private CategoryServiceImpl categoryService;

    @DeleteMapping
    public Result<String> delete(Long ids){

        categoryService.remove(ids);

        return Result.success("刪除成功");
    }

}

DTO具體使用

我們在實際開發中,其操作可能會同時兼顧兩張資料表,這時我們就需要採用DTO並且採用複雜功能開發來重新定義方法

首先我們先來講解DTO的具體使用:

  1. 首先我們需要一張資料表的實體類
package com.qiuluo.reggie.domain;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 菜品
 */
@Data
public class Dish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //菜品名稱
    private String name;


    //菜品分類id
    private Long categoryId;


    //菜品價格
    private BigDecimal price;


    //商品碼
    private String code;


    //圖片
    private String image;


    //描述資訊
    private String description;


    //0 停售 1 起售
    private Integer status;


    //順序
    private Integer sort;


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

}
  1. 我們根據實際需求,在實體類的基礎上,新增一些其他屬性
package com.qiuluo.reggie.dto;

import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

// 在Dish的基礎上新增了DishFlavor資料表,以及categoryName所屬分類名

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}
  1. 然後我們在業務層使用時,就可以引入DTO類作為引數,對內部資料進行操作
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.mapper.DishMapper;
import com.qiuluo.reggie.service.DishService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

    // 調入dishFlavor的業務層實現類
    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    public void saveWithFlavor(DishDto dishDto){

        // 將菜品資料匯入
        this.save(dishDto);

        // 將Flavor匯入(注意:Flavor傳入時沒有傳入dishID,需要我們手動設定)
        List<DishFlavor> flavors = dishDto.getFlavors();
        for (DishFlavor flavor:flavors) {
            flavor.setDishId(dishDto.getId());
        }

        dishFlavorService.saveBatch(flavors);

    }


    /**
     * 查詢菜品,並查詢對應口味
     * @param id
     * @return
     */
    public DishDto getByIdWithFlavors(Long id){

        // 創造返回物件
        DishDto dishDto = new DishDto();

        // 首先根據id獲得菜品資訊
        Dish dish = this.getById(id);

        // 根據id獲得調料資訊
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,id);

        List<DishFlavor> list = dishFlavorService.list(queryWrapper);


        // 將資料傳入
        BeanUtils.copyProperties(dish,dishDto);
        dishDto.setFlavors(list);

        return dishDto;
    }

    // 修改菜品
    public void updateWithFlavor(DishDto dishDto){

        // Dish修改
        this.updateById(dishDto);

        // Flavor修改(我們先全部刪除,再全部重新新增)

        // 刪除操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

        dishFlavorService.remove(queryWrapper);


        // 新增操作
        List<DishFlavor> flavors = dishDto.getFlavors();

        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());


    }

}

前臺程式碼開發

本小節我們將介紹專案的前臺開發階段

簡訊傳送技術

我們的簡訊傳送技術的原理其實很簡單:

  • 自定義生成驗證碼並暫時儲存
  • 將驗證碼通過簡訊服務發給使用者手機
  • 使用者收到後填寫進行比對判斷是否登陸成功

簡訊服務實現

我們目前的難點是簡訊服務,我們在專案中採用了阿里雲簡訊服務,下面做簡單介紹:

  1. 進入阿里雲登陸註冊,做簡單身份驗證等

  1. 進入簡訊服務頁面,註冊簡訊簽名,簡訊模板,AcessKey並授予SMS許可權

  1. 根據阿里雲給出的簡訊傳送API設定工具類,實現簡訊傳送
package com.qiuluo.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
 * 簡訊傳送工具類
 */
public class SMSUtils {

	/**
	 * 傳送簡訊
	 * @param signName 簽名
	 * @param templateCode 模板
	 * @param phoneNumbers 手機號
	 * @param param 引數
	 */
	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
		// 下面兩個空分別填寫
		// AccessKey ID 
		// AccessKey Secret 
		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
		IAcsClient client = new DefaultAcsClient(profile);

		SendSmsRequest request = new SendSmsRequest();
		request.setSysRegionId("cn-hangzhou");
		request.setPhoneNumbers(phoneNumbers);
		request.setSignName(signName);
		request.setTemplateCode(templateCode);
		request.setTemplateParam("{\"code\":\""+param+"\"}");
		try {
			SendSmsResponse response = client.getAcsResponse(request);
			System.out.println("簡訊傳送成功");
		}catch (ClientException e) {
			e.printStackTrace();
		}
	}

}

簡訊傳送實現

最後我們再來介紹整個簡訊傳送流程:

  1. 製作工具類生成四位亂數
package com.qiuluo.reggie.utils;

import java.util.Random;

/**
 * 隨機生成驗證碼工具類
 */
public class ValidateCodeUtils {
    /**
     * 隨機生成驗證碼
     * @param length 長度為4位元或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成亂數,最大為9999
            if(code < 1000){
                code = code + 1000;//保證亂數為4位元數位
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成亂數,最大為999999
            if(code < 100000){
                code = code + 100000;//保證亂數為6位數位
            }
        }else{
            throw new RuntimeException("只能生成4位元或6位數位驗證碼");
        }
        return code;
    }

    /**
     * 隨機生成指定長度字串驗證碼
     * @param length 長度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}
  1. 實現使用者傳送簡訊功能
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/sendMsg")
    public Result<String> sendMsg(@RequestBody User user, HttpSession session){

        // 儲存手機號
        String phone = user.getPhone();

        // 判斷手機號是否存在並設定內部邏輯
        if (phone != null){

            // 隨機生成四位密碼
            String code = ValidateCodeUtils.generateValidateCode(4).toString();

            // 因為無法申請signName簽名,我們直接在後臺檢視密碼
            // log.info(code);

            // 我們採用阿里雲傳送驗證碼
            SMSUtils.sendMessage("簽名","模板",phone,code);

            // 將驗證碼放在session中待比對
            session.setAttribute(phone,code);

            return Result.success("驗證碼傳送成功");

        }

        return Result.success("驗證碼傳送失敗");
    }

}
  1. 完成比對驗證碼使用者登入功能
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/login")
    public Result<User> login(@RequestBody Map map, HttpSession session){

        // 獲得手機號
        String phone = map.get("phone").toString();

        // 獲得驗證碼
        String code = map.get("code").toString();

        // 獲得Session中的驗證碼
        String codeInSession = session.getAttribute(phone).toString();

        // 進行驗證碼比對
        if (codeInSession != null && codeInSession.equals(code) ){
            // 登陸成功
            log.info("使用者登陸成功");

            // 判斷是否為新使用者,如果是自動註冊
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);

            User user = userService.getOne(queryWrapper);

            if (user == null){

                user = new User();

                 user.setPhone(phone);
                 user.setStatus(1);
                 userService.save(user);
            }

            // 登陸成功就刪除驗證碼
            redisTemplate.delete(phone);

            session.setAttribute("user",user.getId());

            return Result.success(user);
        }


        // 比對失敗登陸失敗
        return Result.error("登陸失敗");
    }

}

Redis快取技術

我們在菜品選擇介面會發現有很多套餐分類菜品資料,如果存取人數過多,資料庫存取次數過多會導致系統崩毀

所以我們希望將相關重要的資料進行快取,同時為了保證前臺後臺資料一致的前提下,我們採用Redis來實現快取技術

Redis環境搭建

首先我們來回顧Redis基礎環境搭建:

  1. 匯入Redis相關依賴座標
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 設定Redis相關資訊
server:
  port: 8080
  
# redis設定在spring下
spring:
  application:
    name: qiuluo
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie
      username: root
      password: 123456
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0

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
reggie:
  path: E:\程式設計內容\實戰專案\瑞吉外賣\Code\reggie\imgs\
  1. 設定序列化設定類
// 我們希望在Redis資料庫中可以直接檢視到key的原始名稱,所以我們需要修改其序列化方法

package com.qiuluo.reggie.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        //預設的Key序列化器為:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

Redis基本操作

在完成上述環境搭建操作之後,我們就可以來實現RedisTemplate的自動裝配,然後我們就可以採用RedisTemplate來實現Redis操作

@Autowired
private RedisTemplate redisTemplate;

我們專案中以Dish為例來完成了Redis的基本菜品快取操作:

package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishFlavorServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    @Autowired
    private CategoryServiceImpl categoryService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根據id查詢菜品
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public Result<List<DishDto>> list(Dish dish){

        // 構造返回型別
        List<DishDto> dishDtoList = null;

        // 動態構造構造Redis的key
        String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();

        // 1. 先從Redis中查詢是否有菜品快取
        dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

        // 2.如果存在,則直接返回即可
        if (dishDtoList != null){
            return Result.success(dishDtoList);
        }

        // 3.如果不存在,採用mysql語法呼叫獲得值

        // 提取CategoryID
        Long id = dish.getCategoryId();

        // 判斷條件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(id != null,Dish::getCategoryId,id);
        queryWrapper.eq(Dish::getStatus,1);
        queryWrapper.orderByAsc(Dish::getSort);

        List<Dish> list = dishService.list(queryWrapper);

        // 建立返回型別
        dishDtoList = list.stream().map((item) -> {

            // 建立新的返回型別內部
            DishDto dishDto = new DishDto();

            // 將元素複製過去
            BeanUtils.copyProperties(item,dishDto);

            // 設定CategoryName
            Long categoryId = item.getCategoryId();

            LambdaQueryWrapper<Category> categoryLambdaQueryWrapper = new LambdaQueryWrapper<>();
            categoryLambdaQueryWrapper.eq(Category::getId,categoryId);

            Category category = categoryService.getOne(categoryLambdaQueryWrapper);

            String categoryName = category.getName();

            dishDto.setCategoryName(categoryName);

            // 設定flavor
            Long dishId = item.getId();

            LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper();
            lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);

            List<DishFlavor> dishFlavors = dishFlavorService.list(lambdaQueryWrapper);

            dishDto.setFlavors(dishFlavors);

            return dishDto;
        }).collect(Collectors.toList());

        // 4.最後獲得成功後,將資料存入redis快取中
        redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);

        return Result.success(dishDtoList);

    }
}

同時為了保證前後臺資料一致,我們在後臺進行資料修改時,需要將快取消除,使前臺再次從MYSQL中讀取資料:

package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishFlavorServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    @Autowired
    private CategoryServiceImpl categoryService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    public Result<String> save(@RequestBody DishDto dishDto){

        dishService.saveWithFlavor(dishDto);

        // 全域性快取清理
        // Set keys = redisTemplate.keys("dish_*");
        // redisTemplate.delete(keys);

        // 單個清理
        String key = "dish_" + dishDto.getCategoryId() + "_1";
        redisTemplate.delete(key);

        return Result.success("新創成功");
    }

    /**
     * 修改資料
     * @param
     * @return
     */
    @PutMapping
    public Result<String> update(@RequestBody DishDto dishDto){

        dishService.updateWithFlavor(dishDto);

        log.info("修改完成");

        // 全域性快取清理
        // Set keys = redisTemplate.keys("dish_*");
        // redisTemplate.delete(keys);

        // 單個清理
        String key = "dish_" + dishDto.getCategoryId() + "_1";
        redisTemplate.delete(key);

        return Result.success("修改完成");


    }

    /**
     * 多個刪除
     * @param ids
     * @return
     */
    @DeleteMapping
    public Result<String> deleteByIds(Long[] ids){

        for (Long id:ids
             ) {
            dishService.removeById(id);
        }

        // 全域性快取清理
        // Set keys = redisTemplate.keys("dish_*");
        // redisTemplate.delete(keys);

        // 單個清理
        String key = "dish_" + ids + "_1";
        redisTemplate.delete(key);

        return Result.success("刪除成功");
    }

}

Redis高階操作

Redis為我們提供了一種註解快取的方法來簡化操作,主要依賴於框架Spring Cache

Spring Cache提供了一層抽象,底層可以切換不同的Cache實現,我們主要使用RedisCacheManager這個介面來完成操作

我們來介紹Spring Cache用於快取的常用的四個註解:

註解 說明
@EnableCaching 開啟快取註解功能
@Cacheable 在方法執行前先檢視快取中是否存有資料,如果有資料直接返回資料;如果沒有,呼叫方法並將返回值存入快取
@CachePut 將方法的返回值放到快取
@CacheEvict 將一條或多條從快取中刪除

下面我們來介紹Spring Cache的具體實現步驟:

  1. 匯入相關依賴座標
        <!--Cache座標-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
  1. 在組態檔中統一設定過期時間
server:
  port: 8080
spring:
  application:
    name: qiuluo
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie
      username: root
      password: 123456
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0
  cache:
    redis:
      time-to-live: 180000 # 注意單位是毫秒

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
reggie:
  path: E:\程式設計內容\實戰專案\瑞吉外賣\Code\reggie\imgs\
  1. 在啟動類上新增@EnableCaching註解,開啟快取註解功能
package com.qiuluo.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableCaching
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("專案成功執行");
    }
}
  1. 在SetmealController的list方法上加上@Cacheable註解
package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    /**
     * 根據條件查詢套餐資料
     * @param setmeal
     * @return
     */
    @Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
    @GetMapping("/list")
    public Result<List<Setmeal>> list(Setmeal setmeal){
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        List<Setmeal> list = setmealService.list(queryWrapper);

        return Result.success(list);
    }
}
  1. 在SetmealController的save,update,delete方法上加上@CacheEvict註解
package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    /**
     * 新增
     * @CacheEvict:刪除快取功能,allEntries = true表示刪除該value型別的所有快取
     * @param setmealDto
     * @return
     */
    @CacheEvict(value = "setmealCache",allEntries = true)
    @PostMapping
    public Result<String> save(@RequestBody SetmealDto setmealDto){

        setmealService.saveWithDish(setmealDto);

        log.info("套餐新增成功");

        return Result.success("新創套餐成功");
    }

     /**
     * 修改
     * @CacheEvict:刪除快取功能,allEntries = true表示刪除該value型別的所有快取
     * @param setmealDto
     * @return
     */
    @PutMapping
    @CacheEvict(value = "setmealCache",allEntries = true)
    public Result<String> update(@RequestBody SetmealDto setmealDto){

        setmealService.updateById(setmealDto);

        return Result.success("修改成功");
    }
    
    /**
     * 刪除
     * @CacheEvict:刪除快取功能,allEntries = true表示刪除該value型別的所有快取
     * @param ids
     * @return
     */
    @CacheEvict(value = "setmealCache",allEntries = true)
    @DeleteMapping
    public Result<String> delete(@RequestParam List<Long> ids){

        setmealService.removeWithDish(ids);

        return Result.success("刪除成功");
    }

}

專案部署階段

本小節我們將介紹專案的專案部署階段

資料庫讀寫分離

資料庫的讀寫分離操作相對而言比較簡單,但前置的mysql主從複製相對比較繁瑣

主從複製

我們先來介紹主從複製的具體流程:

  1. 主庫從庫設定固定ID,並且給主庫設定紀錄檔開啟
# 進入組態檔
vim /etc/my.cnf

# 主庫設定
[mysqld]
log-bin=mysql-bin # 啟動二進位制紀錄檔
server-id=128 # 設定伺服器唯一ID

# 從庫設定
server-id=129 # 設定伺服器唯一ID

# 記得重新整理資料庫服務
systemctl restart mysqld
  1. 主庫建立使用者並記錄紀錄檔當前狀況
# 登入資料庫
mysql -uroot -p123456

# 執行下列語句(生成一個使用者,使其具有查詢紀錄檔的權力)
GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';

# 執行語句,你將會看到File和Position資訊,該頁面不要改變
# (你將會看到紀錄檔相關資訊,接下來不要對資料庫操作,因為操作會導致紀錄檔資訊改變)
show master status;
  1. 從庫使用使用者連線主庫並記錄紀錄檔資訊,實現slave同步
# 執行下列語句(使用該使用者查詢紀錄檔,注意內容是需要修改的)
# master_host主庫IP,master_user主庫使用者,master_password主庫使用者密碼,master_log_file,master_log_pos為紀錄檔資訊
change master to
master_host='192.168.44.128',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=439;

# 輸入後執行以下語句開啟slave
start slave;

# 如果顯示slave衝突(如果你之前執行過slave),使用下列方法結束之前slave
stop slave;
  1. 從庫檢視主從複製是否成功
# 檢視語句
show slave starts\G;

# 我們只需要關注三個點:(為下述即為成功)
Slave_IO_State: Waiting for master to send event
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

讀寫分離

我們再來介紹讀寫分離的具體流程:

  1. 匯入Sharding-JDBC的maven座標
        <!--Sharding-jdbc座標-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>
  1. 在組態檔中書寫讀寫分離原則和Bean定義覆蓋原則
server:
  port: 8080
spring:
  application:
    name: qiuluo
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主資料來源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.44.128:3306/reggie?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: 123456
      # 從資料來源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.44.129:3306/reggie?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: 123456
    masterslave:
      # 讀寫分離設定
      load-balance-algorithm-type: round_robin #輪詢
      # 最終的資料來源名稱
      name: dataSource
      # 主庫資料來源名稱
      master-data-source-name: master
      # 從庫資料來源名稱列表,多個逗號分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #開啟SQL顯示,預設false
  main:
    allow-bean-definition-overriding: true # 允許bean定義覆蓋
  redis:
    host: localhost
    port: 6379
    # password: 123456
    database: 0
  cache:
    redis:
      time-to-live: 180000 # 注意單位是毫秒

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
reggie:
  path: E:\程式設計內容\實戰專案\瑞吉外賣\Code\reggie\imgs\

前後端專案部署

我們的實際部署通常分為兩臺伺服器,來完成前後端分開部署

前端專案部署

我們首先來完成前端專案的部署:

  1. 在伺服器中安裝Nginx,並將課程中的dist目錄(已打包的前端資料)上傳至Nginx下的html頁面

  1. 修改Nginx組態檔nginx.conf

後端專案部署

我們再來完成後端專案的部署:

  1. 使用git clone命令將git遠端倉庫的程式碼克隆下來:

  1. 將資料中的reggieStart.sh檔案上傳到伺服器B中,通過chmod命令設定許可權

  1. 然後我們直接執行sh檔案即可,後端專案開啟

結束語

到這裡我們的第一個專案就徹底完成了,以上就是《瑞吉外賣》所有技術點的總結內容,希望能為你帶來幫助!

附錄

該文章屬於總結內容,具體參考B站黑馬程式設計師的Java專案實戰《瑞吉外賣》

這裡附上視訊連結:黑馬程式設計師Java專案實戰《瑞吉外賣》嗶哩嗶哩_bilibili