首页 > 其他分享 >瑞吉外卖实战项目全攻略——总结篇

瑞吉外卖实战项目全攻略——总结篇

时间:2022-11-01 07:55:24浏览次数:87  
标签:org springframework 全攻略 瑞吉 外卖 import reggie qiuluo com

瑞吉外卖实战项目全攻略——总结篇

该系列将记录一份完整的实战项目的完成过程,该篇属于总结篇,主要负责总结整个项目技术点和注意点

案例来自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

标签:org,springframework,全攻略,瑞吉,外卖,import,reggie,qiuluo,com
From: https://www.cnblogs.com/qiuluoyuweiliang/p/16846500.html

相关文章

  • 瑞吉外卖实战项目全攻略——优化篇第三天
    瑞吉外卖实战项目全攻略——优化篇第三天该系列将记录一份完整的实战项目的完成过程,该篇属于优化篇第三天,主要负责完成前后端分离问题案例来自B站黑马程序员Java项目实战......
  • 瑞吉外卖实战项目全攻略——优化篇第二天
    瑞吉外卖实战项目全攻略——优化篇第二天该系列将记录一份完整的实战项目的完成过程,该篇属于优化篇第二天,主要负责完成读写分离问题案例来自B站黑马程序员Java项目实战《......
  • 42-《百万后端架构师千亿级真实项目案例实战营1期》36-外卖平台真实项目实战(上)_ev
                               ......
  • 瑞吉外卖实战项目全攻略——优化篇第一天
    瑞吉外卖实战项目全攻略——优化篇第一天该系列将记录一份完整的实战项目的完成过程,该篇属于优化篇第一天,主要负责完成缓存优化问题案例来自B站黑马程序员Java项目实战《......
  • 瑞吉外卖实战项目全攻略——功能补充篇
    瑞吉外卖实战项目全攻略——功能补充篇该系列将记录一份完整的实战项目的完成过程,该篇属于功能补充篇案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读......
  • 瑞吉外卖实战项目全攻略——第六天
    瑞吉外卖实战项目全攻略——第六天该系列将记录一份完整的实战项目的完成过程,该篇属于第六天案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容......
  • 瑞吉外卖实战项目全攻略——第六天
    瑞吉外卖实战项目全攻略——第六天该系列将记录一份完整的实战项目的完成过程,该篇属于第六天案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容......
  • [外卖电商]小程序的类目选择的注意事项
    最近接触了一个电商的项目,外卖点餐的平台开发,零售端使用小程序,门店端发布成App版本,在开发小程序的时候,遇到了一些类目权限上的问题,下面将问题及解决方案,一一做下解答。一......
  • 瑞吉外卖实战项目全攻略——第五天
    瑞吉外卖实战项目全攻略——第五天该系列将记录一份完整的实战项目的完成过程,该篇属于第五天案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容......
  • 瑞吉外卖实战项目全攻略
    该系列将记录一份完整的实战项目的完成过程,该篇属于第四天案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容该篇我们将完成以下内容:文件上传下载......