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

news2024/10/7 8:20:36

文章目录

  • 零、MyBatisPlus
  • 一、管理端登录
    • 1.0 统一的返回结果Result类
    • 1.1 admin/login
    • 1.2 admin/logout
    • 1.3 Filter
    • 1.4 自定义消息转换器
  • 二、员工管理
    • 2.1 新增员工-字段填充
    • 2.2 全局异常捕获
    • 2.3 员工信息分页查询
  • 三、分类管理
    • 3.1 分类的删除
  • 四、菜品管理
    • 4.1 文件的上传与下载
        • 1 上传
        • 2 下载
    • 4.2 新增菜品
    • 4.3 修改菜品
    • 4.4 菜品信息分页查询
  • 五、套餐管理
    • 5.1 添加套餐
    • 5.2 批量删除套餐
    • 5.3 套餐信息分页查询
  • 六、用户相关
    • 6.1 发送验证码
    • 6.2 登录
  • 七、购物车
    • 7.1 添加菜品和套餐
  • 文件配置

目标很明确,快速掌握最最基础的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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/668360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CNAPPs投资热度持续攀升 腾讯云被Gartner评为全球案例厂商

近日&#xff0c;Gartner发布《新兴技术&#xff1a;在三重挤压中蓬勃发展—对云安全风险投资的关键洞察》&#xff08;Emerging Tech: Thriving Amid the Triple Squeeze— Critical Insights on VC Funding for Cloud Security&#xff09;&#xff08;以下简称《报告》&…

Flink 学习五 Flink 时间语义

Flink 学习五 Flink 时间语义 1.时间语义 在流式计算中.时间是一个影响计算结果非常重要的因素! (窗口函数,定时器等) Flink 可以根据不同的时间概念处理数据。 处理时间: process time System.currentTimeMillis()是指执行相应操作的机器系统时间&#xff08;也称为纪元时间…

优化|如何减小噪声和误差对梯度下降法的影响

编者按&#xff1a; ​ 许多精确算法在理论上能保证我们的目标函数值一直下降。在随机梯度下降以及无导数优化等情况下&#xff0c;目标移动方向受到噪声干扰&#xff0c;与实际下降方向往往会存在偏差。本文将分析噪声和下降偏差对于梯度下降法等算法的影响&#xff0c;并且介…

SpringMVC08:拦截器+文件下载

目录 一、概述 二、自定义拦截器 1、新建一个Moudule&#xff0c;SpringMVC-07-Interceptor&#xff0c;添加web支持&#xff1b; 2、配置web.xml和springmvc-servlet.xml文件 3、编写一个拦截器 4、在springmvc的配置文件中配置拦截器 5、编写一个Controller&#xff0…

【数据库】Mysq备份与恢复

文章目录 一、数据库备份的分类1. 数据备份的重要性2. 数据库备份的分类3. 常见的备份方法 二、Mysql 完全备份与恢复1. Mysql 完全备份2. 数据库完全备份分类2.1 物理冷备份及恢复2.2 mysqldump 备份数据库完全备份一个或多个完整的库&#xff08;包括其中所有的表&#xff09…

基于YOLOv5实现安全帽检测识别

目录 1、作者介绍2、YOLOv5网络模型2.1 算法简介2.2 数据集介绍2.2.1 VOC数据集准备2.2.2 YOLOv5算法检测流程 3、代码实现3.1 数据集划分部分代码3.2 训练阶段3.3 测试阶段3.4 检测结果 4、问题与分析参考链接 1、作者介绍 陈梦丹&#xff0c;女&#xff0c;西安工程大学电子…

【6.20】sleep()和wait()的区别

sleep()和wait()的区别 1、wait()方法 1.1使用场景 当某个线程获取到锁后&#xff0c;却还是不满足执行的条件&#xff0c;就可以调用对象锁的wait方法&#xff0c;进入等待状态。 直到外在条件满足了&#xff0c;就可以由其它线程调用notify或者notifyAll方法&#xff0c;…

在软件研发排期中要求“倒推时间”,项目结束后悲剧了……

有没有遇到某个项目任务的研发周期已被各路boss定下&#xff0c;研发团队都觉得时间不合理&#xff0c;反馈给上级无果&#xff0c;而要求“倒推时间”进行任务排期的情况&#xff1f; 什么是“倒推时间”&#xff1f; 目标倒推法&#xff0c;从剩下的时间反推算出每天该做的事…

【Java】死锁问题及ThreadLocal

什么是死锁分析过程发生死锁的原因避免死锁ThreadLocal 什么是死锁 多个线程同时被阻塞&#xff0c;它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞&#xff0c;因此程序不可能正常终止。这是一个最严重的BUG之一。 分析过程 1.一个线程一把锁 一个线…

深入理解TDD(测试驱动开发):提升代码质量的利器

在日常的软件开发工作中&#xff0c;我们常常会遇到这样的问题&#xff1a;如何在繁忙的项目进度中&#xff0c;保证我们的代码质量&#xff1f;如何在不断的迭代更新中&#xff0c;避免引入新的错误&#xff1f;对此&#xff0c;有一种有效的开发方式能帮助我们解决这些问题&a…

14.处理大数据集

14.1 随机梯度下降 假设你正在使用梯度下降来训练一个线性回归模型 当m个样本的m很大时&#xff0c;求和计算量太大了。这种梯度下降算法有另外一个名字叫做批量梯度下降&#xff08;batch gradient desent&#xff09;。这种算法每次迭代需要使用全量训练集&#xff0c;直到算…

【代码阅读软件】Source Insight 4 使用教程 | 很详细——适合新手

目录 一、概述二、常用的几个窗口&#x1f449;2.1 符号窗口&#xff08;Symbol Window&#xff09;&#x1f449;2.2 项目文件窗口&#xff08;Project Window&#xff09;&#x1f449;2.3 关系窗口&#xff08;Relation Window&#xff09;&#x1f449;2.4 上下文窗口&…

STM32--基于固件库(Library Faction)的led灯点亮

目录 一、STM32芯片的简单介绍 二、基于固件库&#xff08;Library Faction&#xff09;的led灯点亮 这是一个学习stm32的开端&#xff0c;我们由简入难&#xff0c;之前学过C51/52或是其他型号的一般都是从led开始&#xff0c;也就是简单的输入输出端口的应用。&#xff08;想…

SpringBoot整合模板引擎Thymeleaf(1)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Thymeleaf概述 Thymeleaf是一种用于Web和独立环境的现代服务器端的Java模板引擎&#xff0c;主要目标是将优雅的自然模板带到开发工作流程中&#xff0c;并将HTML在浏览器中…

【kubernetes】Etcd集群部署与验证

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机准备开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

吐血整理,性能测试Jmeter分布式压测遇坑总结+解决

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 为什么要使用分布…

JSON.parse() 全面用法介绍

JSON 通常用于与服务端交换数据。在接收服务器数据时一般是字符串。我们可以使用 JSON.parse() 方法将数据转换为 JavaScript 对象。 语法 JSON.parse(text[, reviver]) text:必需&#xff0c; 一个有效的 JSON 字符串。 reviver: 可选&#xff0c;一个转换结果的函数&#xf…

SPI协议解析

SPI协议介绍 引言介绍SPI简介物理层协议层通讯的起始和停止信号SPI 模式 优缺点优点缺点 使用例程基于STM32的SPI通信准备硬件连接 软件实现 总结 引言 SPI是串行外设接口的缩写&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线。由于SPI高速和同步的特…

vite环境变量与模式

环境变量 Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量&#xff1a; import.meta.env.MODE: {string} 应用运行的模式。 import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。 import.m…

【ESP8266】使用MQTT协议 连接华为云iotDA,实现设备属性上报

相关资料&#xff1a;https://github.com/CQUPTLei/ESP8266 往期文章&#xff1a;【ESP8266】基础AT指令和常用WIF指令 【MQTT 5.0】协议 ——发布订阅模式、Qos、keepalive、连接认证、消息结构 一、华为云iotDA1.1 什么是iotDA1.2 创建 iotDA 产品 二、使用ESP8266上报设备…