【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

这篇具有很好参考价值的文章主要介绍了【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


目标很明确,快速掌握最最基础的SpringBoot + MyBatis-Plus怎么用,两天赶着把项目做了一大半,但过程里缺乏一些思考和总结,现在来复盘一下。仅列出觉得有价值的部分。

还是很适合作为上手项目,业务逻辑确实比较简单,主要是要掌握一整套流程,以及涉及到多个表的连接查询操作、一个表的分页查询应该如何处理,以及文件的上传下载、手机短信发送验证码知识。

但这样的项目,如果不主动思考,能得到的东西就很少了,因为它开发的流程已经给了一个答案,虽然未必是标准答案,但是直接照着抄、不考虑应该怎么实现,可能除了查表更熟练以外能收获的技能不多。不过查表更熟练也算小提升吧。

以及觉得如果有个ER图 / 接口说明的话,会清晰很多,不用这样对着前端分析传过来什么,应该传回去什么。

MyBatis Plus确实方便了很多,这个项目从头到尾没写过<if> <foreach> <set> <where>,方便得让人不安,牛的。

自己一个人git还是缺少锻炼,体会不到那种pull下来发现有冲突,需要merge的绝望。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

下一步速成redis和微服务,主要还是学学各种中间件怎么使。然后找个能拿得出手的项目。

零、MyBatisPlus

极大简化CRUD代码。

  • 基本上是傻瓜式操作,因为几乎不用记对应的SQL查询要怎么写,戳一个.就能得到一波hint和提示补全。
  • 提供分页插件。
  • 提供全局拦截规则,设置@TableField及对应的MetaObjectHandler就可以对字段进行填充。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

一、管理端登录

1.0 统一的返回结果Result类

还是有必要的,之前写前端的时候很需要这个code和msg让我知道这个接口我是调成功了还是失败了,调失败了的话问题在哪。

@Data
public class Result<T> {
    /**
     *  code - 编码:1成功,0和其它数字为失败
     *  msg - 错误信息
     *  data - 数据
     *  map - 动态数据
     */
    private Integer code;
    private String msg;
    private T data;
    private Map map = new HashMap();

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

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

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

}

1.1 admin/login

说明:这一部分是好久好久好久以前写的,改了改前端和接口,但逻辑是一样的。

客户端请求(TODO: 前端裸传密码还是有一点怪怪……有时间了解一下现实世界的实现):

POST
/admin/login
参数:
{
  "name": "扣扣",
  "password": "koukou123456"
}

管理员实体:

@Data
public class Admin {
    private Long adminId;
    private String password;
    private String phoneNumber;
    private String name;
}

逻辑:

  1. 将参数password进行MD5加密
import org.springframework.util.DigestUtils;
password = DigestUtils.md5DigestAsHex(password.getBytes());
  1. 判断数据库中是否存在该对象,与数据库中取到的密码是否一致

  2. 登录成功时,将管理员id存入当前session,作为本次会话的一个属性。

request.getSession().setAttribute("admin", adm.getAdminId());

AdminController代码:

	/**
     * 密码md5加密 + 根据name查询数据库 + 比对密码
     * @param request 该参数为了将该admin对象的id存入当前session中
     * @param admin 封装好的Admin Bean参数
     * @return
     */
    @PostMapping("/login")
    public Result<Admin> login(HttpServletRequest request, @RequestBody Admin admin) {

        // 1 将页面提交的密码进行md5加密处理
        String password = admin.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        // 2 根据页面提交的用户名username查询数据库
        LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Admin::getName, admin.getName());
        Admin adm = adminService.getOne(queryWrapper);

        // 3、无结果返回登陆失败
        if (adm == null) {
            return Result.error("用户名错误,登录失败");
        }

        // 4、比对密码
        if (!adm.getPassword().equals(password)) {
            return Result.error("密码错误,登录失败");
        }

        // 5、登录成功,将管理员id存入Session并返回登录成功结果
        request.getSession().setAttribute("admin", adm.getAdminId());
        return Result.success(adm);
    }

1.2 admin/logout

把当前管理员的id移出session

	@PostMapping("/logout")
    public Result<String> login(HttpServletRequest request) {
        request.getSession().removeAttribute("admin");
        return Result.success("退出成功");
    }

1.3 Filter

Servelet中的Filter接口。需要加入@WebFilter注解声明拦截路径,并在启动类加入@ServletComponentScan注解,使得这个Filter可以被Scan到。

一些页面 / 接口需要在访问前判断当前是否为登录状态,所以设置这个Filter。

核心逻辑为判断当前访问的Url以及从Session中取出id。

/**
 * 检查是否登录
 */
@WebFilter(urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    /**
     * 路径匹配器,用于检查该路径是否需要拦截
     */
    private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       
    }


    /**
    *   判断requestUrl是否在urls中
    */
    public boolean canPass(String[] urls, String requestURI) {
        for (String url: urls) {
            if (PATH_MATCHER.match(url, requestURI)) {
                return true;
            }
        }
        return false;
    }
}

核心为doFilter方法,逻辑如下:

  • 定义可放行请求路径集合,判断request的Url是否在集合中,如果在集合中,可以直接放行;

  • 尝试从session中得到login时存入的属性(可能是管理员login,也可能是用户login)

    req.getSession().getAttribute("admin");
    
  • 如果返回值不为空,说明已经登录,可以放行

  • 否则需要response拒绝请求:

    Result<String> error = Result.error("对不起,您尚未登录,请先进行登录操作!");
    resp.getWriter().write(JSONObject.toJSONString(error));
    return;
    

完整代码如下:

 	@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
      
		HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;

        // 可放行集合
        String[] canPassUrls = {
                "/admin/login",
                "/admin/logout",
                // 静态资源路径就不处理了
                "/backend/**",
                "/front/**",
                // 一些其他请求,发送短信、移动端登录
                "/common/**",
                "/user/sendMsg",
                "/user/login"
        };

        // 1、得到URI
        String requestURI = req.getRequestURI();
        log.info("拦截到请求: {}", requestURI);

        // 2、得到登录状态
        Object adminLoginId = req.getSession().getAttribute("admin");
        Object userLoginId = req.getSession().getAttribute("user");

        // 3、如果未登录且是不可访问页面,拒绝请求
        if (!canPass(canPassUrls, requestURI) && adminLoginId == null && userLoginId == null) {
            Result<String> error = Result.error("对不起,您尚未登录,请先进行登录操作!");
            resp.getWriter().write(JSONObject.toJSONString(error));
            return;
        }

        if (adminLoginId != null) {
            BaseContext.setCurrentId((Long)adminLoginId);
        }

        if (userLoginId != null) {
            BaseContext.setCurrentId((Long)userLoginId);
        }

        filterChain.doFilter(servletRequest, servletResponse);
   }

1.4 自定义消息转换器

这部分只是意会了,让我自己写可能还是不会。

long转为js会精度丢失,那么我们就对数据进行转型,响应json时进行处理,将long转为字符串。

并且转换时间格式。

还是有点AOP的。

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);
    }
}

WebMVCConfig中需要进行相依ing的设置。

import com.beautysalon.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 java.util.List;

@Slf4j
@Configuration
public class WebMVCConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器");
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        converters.add(0, messageConverter);
    }
}

二、员工管理

2.1 新增员工-字段填充

可以统一处理的变量可以使用注解@TableField,然后再定义一个Handler实现填充方法。

@Slf4j
@Data
public class Employee {
    private Long id;
    private String name;
    private String username;
    private String password;
    private String phone;
    private String sex;
    private String idNumberReal;
    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long createByAdmin;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateByAdmin;
}

实现MetaObjectHandler接口和insertFillupDateFill方法。

可以使用hasSetter判断是否具有某个属性。

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        if (metaObject.hasSetter("createByAdmin")) {
            metaObject.setValue("createByAdmin", BaseContext.getCurrentId());
            metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
        }
        if (metaObject.hasSetter("createUser")) {
            metaObject.setValue("createUser", BaseContext.getCurrentId());
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }

    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]");
        metaObject.setValue("updateTime", LocalDateTime.now());
        if (metaObject.hasSetter("updateByAdmin")) {
            metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
        }
        if (metaObject.hasSetter("updateUser")) {
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    }
}

BaseContext如下,在login时设置了BaseContext相关属性,需要填充时再get,因为是静态方法,所以不需要注入:

/**
 * 基于ThreadLocal封装工具类
 */
@Component
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();
    }
}

2.2 全局异常捕获

使用@ControllerAdvice@ExceptionHandler注解,@ExceptionHandler指明了捕获什么样的异常。

/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());

        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return Result.error(msg);
        }

        return Result.error("未知错误");
    }

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public Result<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());
        return Result.error(ex.getMessage());
    }

}

2.3 员工信息分页查询

需要配置MyBatis提供的分页插件拦截器:

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


@Configuration
public class MyBatisPlusConfig {

    /**
     * 分页插件
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

使用MyBatis-PlusPage进行分页:

	@GetMapping("/page")
    public Result<Page<Employee>> page(@RequestParam Integer page,
                                       @RequestParam Integer pageSize,
                                       @RequestParam(required = false) String name) {
        log.info("员工分页信息查询:{}, {}", page, pageSize);

        // 配置分页构造器
        Page<Employee> pageInfo = new Page<>(page, pageSize);

        // 条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        // 添加过滤条件,如果name不为空,加入name=#{name}条件
        queryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);
        // 添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        // 执行查询
        employeeService.page(pageInfo, queryWrapper);
        return Result.success(pageInfo);
    }

三、分类管理

3.1 分类的删除

删除前需要先去dish表和setmeal表查看有无菜品。操作涉及到3个表:

  • dish表是否有元素categoryId为当前分类
  • setmeal表是否有元素categoryId为当前分类
  • category表删除该分类

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

四、菜品管理

4.1 文件的上传与下载

上传:保存到本地指定位置

下载:作为Response吐给浏览器显示

1 上传

在属性的yml文件中定义相关路径位置:

koukou:
  path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\

使用${}指定图片保存路径

	@Value("${koukou.path}")
    private String basePath;

    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) {
        
        // 提取文件相关信息
        String filename = file.getOriginalFilename();
        int index = filename.lastIndexOf('.');
        String ext = filename.substring(index);

        // UUID赋予新名称
        String newName = UUID.randomUUID().toString();
        String path = basePath + newName + ext;
        log.info(path);

        // 保存文件
        try {
            file.transferTo(new File(path));
        }
        catch (IOException e) {
            e.printStackTrace();
        }

        return Result.success(newName + ext);
    }
2 下载
	/**
     * 让本地的图片在浏览器上显示,写入Response的输出流
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response) {
        try {
            // 输入流
            FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
            // 输出流
            ServletOutputStream outputStream = response.getOutputStream();
            // 设置response的content类型
            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();
            }
            outputStream.close();
            fileInputStream.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

4.2 新增菜品

@Transactional(rollbackFor = Exception.class)开启事务,并在启动类上加上@EnableTransactionManagement.

设计到三个表:

  1. 菜品的分类:因为前端在新增菜品时,需要选择菜品分类,因此需要返回菜品的所有可能分类取值。
  2. dish表,表示菜品
  3. dish_flavor表,表示菜品的口味,由于是一对多关系,该表存储了dish的主码id

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

1、查询所有可能的菜品分类,使用一个category来接收参数,解释是这样以后需求增加时(比如按其它属性search)不必重构这个方法

	@GetMapping("/list")
    public Result<List<Category>> list(Category category) {
        log.info("根据条件查询分类数据");

        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(category.getType() != null, Category::getType, category.getType());
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

        List<Category> list = categoryService.list(queryWrapper);

        return Result.success(list);
    }

2、这个add请求由于携带了额外的信息,用一个DishDTO接住:

	@PostMapping
    public Result<String> save(@RequestBody DishDto dishDto) {
        dishService.saveWithFlavor(dishDto);
        return Result.success("成功保存菜品");
    }

DishDto继承了Dish类,包含Dish的所有属性,但增加了flavors的扩展。

categoryName我觉得是想说明怎么实现两个表的连接,把categoryId转为categoryName

/**
 * DTO:Data Transfer Object,用于传输数据, 对dish的扩展
 */
@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();
    private String categoryName;
    private Integer copies;
}

  • 先将dishDto存入Dish表
  • 然后设置每个Flavor的dishId,并存进Flavor表。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

4.3 修改菜品

修改菜品的逻辑比较类似,但首先需要先把这个菜品的信息查询出来,放进DishDto里传给前端,前端显示这个菜品。

使用到了BeanUtils.copyProperties进行两个对象间的复制。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

然后前端进行修改,然后再传回后端,后端进行修改。类似地,先update这个dish,然后再update这个菜品对应的口味。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

4.4 菜品信息分页查询

类似地,需要查找菜品及其对应的口味,并将categoryId转为name,同样用到了BeanUtils进行Page之间的复制。

  • 查找满足条件的分页数据 Page<Dish>,赋值给 Page<DishDto>
  • 查找所有dish的口味和种类,赋值给DishDto,加入列表。
	@GetMapping("/page")
    public Result<Page> page(int page, int pageSize, String name) {

        // 先把分页数据查出来
        Page<Dish> pageInfo = new Page<>(page, pageSize);

        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(name != null, Dish::getName, name);
        queryWrapper.orderByDesc(Dish::getUpdateTime);

        dishService.page(pageInfo, queryWrapper);

        // 和另一个flavor表综合
        Page<DishDto> dishDtoPage = new Page<>();
        // 把查询出来的数据拷贝到新对象
        BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
        
        // 处理records
        List<Dish> dishes = pageInfo.getRecords();
        List<DishDto> dishDtos = new ArrayList<>();
        
        for (Dish dish: dishes) {
            DishDto dishDto = new DishDto();
            // 把dish拷贝到新对象
            BeanUtils.copyProperties(dish, dishDto);
            Long categoryId = dish.getCategoryId();
            String categoryName = categoryService.getById(categoryId).getName();
            dishDto.setCategoryName(categoryName);
            dishDtos.add(dishDto);
        }

        // 赋值
        dishDtoPage.setRecords(dishDtos);
        return Result.success(dishDtoPage);
    }

五、套餐管理

5.1 添加套餐

和新增菜品的逻辑很类似,涉及到setmeal和setmealdish两张表,setmeal保存套餐信息,setmealdish记录菜品与套餐间的关系。

  • 先保存套餐信息
  • 然后设置 套餐菜品关系 的套餐id,存入表

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

5.2 批量删除套餐

需要先批量删除setmeal套餐表,然后用.in判断菜品套餐关系表,删除SetmealDish表中含该套餐id的项。

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

5.3 套餐信息分页查询

与菜品信息分页查询类似:

  • Setmeal和SetmealDto之间的BeanUtils.copyProperties
  • 以及两个Page之间的BeanUtils.copyProperties
    @GetMapping("/page")
    public Result<Page> page(int page, int pageSize, String name) {

        Page<Setmeal> pageInfo = new Page<>(page, pageSize);
        // 需要返回的数据类型
        Page<SetmealDto> dtoPage = new Page<>();

        // 先把这一页的信息查出来
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name != null, Setmeal::getName, name);
        setmealService.page(pageInfo, queryWrapper);

        List<Setmeal> setmeals = pageInfo.getRecords();
        List<SetmealDto> setmealDtos = new ArrayList<>();

        BeanUtils.copyProperties(pageInfo, dtoPage, "records");
        // 将id转换为name
        for (Setmeal setmeal: setmeals) {
            String categoryName = categoryService.getById(setmeal.getCategoryId()).getName();
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(setmeal, setmealDto);
            setmealDto.setCategoryName(categoryName);
            setmealDtos.add(setmealDto);
        }

        dtoPage.setRecords(setmealDtos);
        return Result.success(dtoPage);
    }

六、用户相关

6.1 发送验证码

生成4位验证码:

public class ValidateCodeUtils {

    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

发送短信,即调用API发请求的过程:

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 {
	private static final String SIGN_NAME = "小扣外卖";
	private static final String TEMPLATE_CODE = "SM1";

	/**
	 * 发送短信
	 * @param phoneNumbers 手机号
	 * @param param 参数
	 */
	public static void sendMessage(String phoneNumbers, String param){
		DefaultProfile profile = DefaultProfile.getProfile(
				"cn-hangzhou",
				"key",
				"private key"
		);
		IAcsClient client = new DefaultAcsClient(profile);

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

}

controller需要调用工具类发送短信,并将验证码存入Session:

	@PostMapping("/sendMsg")
    public Result<String> sendMsg(@RequestBody User user, HttpSession session) {
        String code = ValidateCodeUtils.generateValidateCode4String(4);
        SMSUtils.sendMessage(user.getPhone(), code);
        log.info("发送验证码:{}", code);
        
        // 将验证码保存到Session
        session.setAttribute(user.getPhone(), code);
        return Result.success("短信发送成功,验证码为" + code);
    }

6.2 登录

  • 将用户发来的验证码,与session中存起来的验证码进行比较
    • 不同,登录失败
    • 相同,用户表中是否有该user,如果是新用户,加入user表里
      • 将id存入session,以便CheckLoginFilter能够取到
      • 如果仔细观察你会发现userService.save(user)以后用户自动拥有了一个id。
@PostMapping("/login")
    public Result<User> login(@RequestBody Map<String, String> map, HttpSession session) {
        // 获取手机号、验证码进行比对
        String phone = map.get("phone");
        String code = map.get("code");
        String sessionCode = (String) session.getAttribute(phone);

        // 比对成功,登录成功
        if (code != null && code.equals(sessionCode)) {
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone, phone);
            User user = userService.getOne(queryWrapper);
            // 如果当前用户是新用户,加入user表中
            if (user == null) {
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
            session.setAttribute("user", user.getId());
            log.info("用户登录成功,{}", user.getId());
            return Result.success(user);
        }

        // 比对失败
        return Result.error("登录失败");
    }

七、购物车

7.1 添加菜品和套餐

购物车表:

【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目

  • 判断是菜品还是套餐
  • 每个用户对应一个购物车id,查看该用户的购物车中是否存在该item
  • 存在,count + 1,更新;不存在,count=1,写入。
@PostMapping("/add")
public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车数据:{}",shoppingCart);

    // 先设置相应属性,然后看看这道菜购物车里有没有,如果没有,加入表;如果有,number+1
    shoppingCart.setUserId(BaseContext.getCurrentId());

    // 查看当前菜品 或 套餐是否在购物车中
    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.eq(ShoppingCart::getUserId, shoppingCart.getUserId());
    
    if (shoppingCart.getDishId() != null) {
        queryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
    }
    if (shoppingCart.getSetmealId() != null) {
        queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
    }
    ShoppingCart target = shoppingCartService.getOne(queryWrapper);

    if (target != null) {
        // 在购物车里,数量加一
        target.setNumber(target.getNumber() + 1);
        shoppingCartService.updateById(target);
    }
    else {
        shoppingCart.setNumber(1);
        shoppingCartService.save(shoppingCart);
        target = shoppingCart;
    }

    return Result.success(target);
}

文件配置

通过配置这里设置了端口,发送response的编码,mybatis plus的名字映射方式,全局id的生成方式,文件上传路径。

application.yml

server:
  port: 629
  servlet:
    encoding:
      force: true
      charset: UTF-8

spring:
  application:
    #应用的名称,
    name: 
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/beautysalon?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 

mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
    # 全局id的生成方式
      id-type: ASSIGN_ID

koukou:
  path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\

pom.xml文章来源地址https://www.toymoban.com/news/detail-501057.html

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

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

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

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

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

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

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

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

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

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.16</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.1.0</version>
        </dependency>

    </dependencies>

到了这里,关于【瑞吉外卖】适合速成SpringBoot和MyBatis的作业项目的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【SpringBoot项目实战+思维导图】瑞吉外卖①(项目介绍、开发环境搭建、后台登陆/退出功能开发)

    全文主体框架来源于黑马瑞吉外卖的项目资料,我在文中会嵌入如下五个方面的个人内容: 项目中易发生错误的地方 项目中涉及的一些难理解知识点 一些遗忘知识点的回顾 业务的多种实现方法 我在做项目时的思考和一些踩坑 作为一名软件开发工程师,我们需要了解在软件开

    2024年02月05日
    浏览(41)
  • 瑞吉外卖项目——瑞吉外卖

    需求分析:产品原型、需求规格说明书 设计:产品文档、UI界面设计、概要设计、详细设计、数据库设计 编码:项目代码、单元测试 测试:测试用例、测试报告 上线运维:软件环境安装、配置 项目经理:对整个项目负责,任务分配、把控进度 产品经理:进行需求调研,输

    2023年04月26日
    浏览(80)
  • 最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(二)项目概述

    黑马程序员最新Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战。 《苍穹外卖》项目的定位是一款为餐饮企业(餐厅、饭店)定制的软件产品。该项目是一个在线外卖订购系统,顾客可以通过网站或者手机 App 订购餐点。该项目可以提供以下的功能:

    2024年02月12日
    浏览(33)
  • 最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(一)项目概述

    黑马程序员最新Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战。 《苍穹外卖》项目的定位是一款为餐饮企业(餐厅、饭店)定制的软件产品。该项目是一个在线外卖订购系统,顾客可以通过网站或者手机 App 订购餐点。该项目可以提供以下的功能:

    2024年02月15日
    浏览(32)
  • 最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(五)员工管理

    黑马程序员最新Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战。 设计 DTO 类 我们需要根据新增员工接口设计对应的 DTO 类去接收前端传递的参数,前端传递参数列表如下: 注意: 当前端提交的数据和实体类中对应的属性差别比较大时,建议使用

    2024年02月15日
    浏览(31)
  • 最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(二)开发环境搭建

    黑马程序员最新Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战。 前端工程基于 nginx 运行,因为《苍穹外卖》项目侧重于后端开发,所以黑马程序员给我们直接提供了前端的代码部分,我们只需要在本地搭建好前端环境,并运行起来,专注于后端开

    2024年02月10日
    浏览(33)
  • 瑞吉外卖项目——前后端分离

    前后端分离开发,就是在项目开发过程中,对于前端代码的开发由专门的 前端开发人员 负责,后端代码则由 后端开发人员 负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。 目前,前后端分离开发方式已经被越来越多

    2023年04月20日
    浏览(37)
  • 项目笔记-瑞吉外卖(全)

    1.对后端返回请求值的分析 2.对不同种请求参数的分析 3.事务管理 1.软件开发整体介绍 2.项目整体介绍⭐️ 后端:管理菜品和员工信息 前台:通过手机端,可以浏览菜品和添加客户端 开发项目流程: 实现基本需求,用户能在手机浏览器访问 对移动端应用改进,使用微信小程

    2024年02月07日
    浏览(27)
  • 瑞吉外卖项目记录

    本文为个人学习黑马《瑞吉外卖》项目后进行的项目总结,更偏向于对自己编写文本能力的锻炼以及对项目知识点的简短记录。因为个人能力问题,其中可行性分析和测试部分只进行了小标题的陈列,并没有进行编辑。对《瑞吉外卖》项目感兴趣的朋友也可以浏览本文后再去

    2024年02月05日
    浏览(25)
  • 瑞吉外卖项目----(2)缓存优化

    将项目推送到远程仓库里,教程在git 提交远程仓库前建议取消代码检查 创建新的分支v1.0(用于实现缓存优化)并推送到远程仓库 1.1.1 maven坐标 导入spring-data-redis的maven坐标: 1.1.2 配置文件 在application.yml中加入redis相关配置: 1.1.3 配置类 在项目中加入RedisConfig 1.2.1 实现思路

    2024年02月14日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包