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


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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8pQhqq9-1687270622520)(【瑞吉外卖】项目总结/image-20230620214441699.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v1Cqrnv4-1687270622521)(【瑞吉外卖】项目总结/image-20230620164910232.png)]

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

零、MyBatisPlus

极大简化CRUD代码。

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

img

一、管理端登录

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表删除该分类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxJJjRUJ-1687270622522)(【瑞吉外卖】项目总结/image-20230620204156323.png)]

四、菜品管理

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfpAA5U2-1687270622522)(【瑞吉外卖】项目总结/image-20230620205552163.png)]

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表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wsvmoaQO-1687270622522)(【瑞吉外卖】项目总结/image-20230620210516483-1687266318622-1.png)]

4.3 修改菜品

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsGyX2bm-1687270622523)(【瑞吉外卖】项目总结/image-20230620211508771.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPAotosV-1687270622523)(【瑞吉外卖】项目总结/image-20230620211111624.png)]

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,存入表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLt9ah0o-1687270622523)(【瑞吉外卖】项目总结/image-20230620212915291.png)]

5.2 批量删除套餐

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LqALzDS-1687270622524)(【瑞吉外卖】项目总结/image-20230620213128638.png)]

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 添加菜品和套餐

购物车表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TjHCZ0PT-1687270622524)(【瑞吉外卖】项目总结/image-20230620214054308.png)]

  • 判断是菜品还是套餐
  • 每个用户对应一个购物车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

<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>
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值