当前位置: 首页 > news >正文

【瑞吉外卖项目复写】基本部分复写笔记

Day1 瑞吉外卖项目概述

mysql的数据源配置

spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/regie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: root

注意要配置mysql其实是配置druid数据源。

注意url的后缀。

mybatisplus配置

mybatis-plus:configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: assign_id

①log-impl:在MybatisPlus中,log-impl是用于配置mybatis的日志实现方式的属性。log-impl属性允许您指定mybatis再执行sql语句时使用哪种日志实现。

其中“org.apache.ibatis.logging.stdout.StdOutImpl”是mybatis提供的一种日志实现,它将日志信息输出到标准输出(控制台)。

②assign-id:在mybatisplus中,global-config是全局配置的一部分,用于配置一些全局的属性和策略。在global-config中,db-config是数据库配置的子属性,用于配置数据库相关的一些选项。

具体来说,id-type是db-config的子属性,用于指定主键id的生成策略。

1.auto:自增逐渐,使用与数据库自增长类型的字段(如mysql的auto_increment)

2.input:用户输入主键值,用户手动输入主键的值

3.assign-id:分配id主键,通过代码手动分配主键的值

4.assign-uuid:分配uuid主键,通过代码手动分配uuid类型的主键值

5.none:无主键生成策略,需要手动设置主键的值,不推荐使用

修改静态资源映射路径

如果前端资源不在static或template目录下,则需要修改静态资源映射路径

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {@Overrideprotected void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");}
}

第一步:创建config类型的类继承WebMvcConfigurationSupport

第二步:重写addResourceHandlers方法

将前端资源通过addResourceHandler方法、addResourceLocations方法映射到静态资源路径

后台登录功能开发

Java实体类实现序列化

在Java中,实现Serializable接口是为了表明该类的对象可以被序列化。序列化是将对象转换为字节流的过程,以便对象存储在磁盘上或通过网络进行传输。

在实现Serializable接口时,并没有需要实现的抽象方法,它只是一个标记接口(Marker Interface),标志着该类的对象是可以序列化的。

private static final long serialVersionUID=1L:

是在实现Serializable接口的类中顶一个序列化版本号(Serialization Version UID)。这个版本号是为了确保序列化和反序列化过程中的兼容性。

比如对于如下的MyClass类实现了Serializable接口,并显示的设置了serialVersionUID的值为123456789L。这样,当MyClass类发生变化时,版本号将保持一致,从而确保序列化和反序列化的兼容性。

import java.io.Serializable;public class MyClass implements Serializable{private static final long serialVersionUID=123456789L;//类的其他成员和方法private String name;private int age;}

封装通用响应类

在这个类中,泛型<T>被用作数据的类型参数,允许在运行中指定具体的数据类型。这使得R类在返回数据时可以根据实际需要返回不同类型的数据,而不限于特定类型。

其中map是一个HashMap对象,用于在响应中存储其他键值对的附加信息。

其中add(String key,Object value)实例方法,用于向响应中的map添加附加信息。它接收一个字符串key和一个对象value,将键值对添加到map中,并返回当前R<T>对象本身。这使得可以链式调用该方法来添加多个键值对。

public class R<T> {private int code;private String errMsg;private T data;private Map map=new HashMap();public static <T> R<T> success(T object){R<T> tr = new R<>();tr.data=object;tr.code=1;return tr;}public static <T> R<T> error(String msg){R<T> tr = new R<>();tr.errMsg=msg;tr.code=0;return tr;}public R<T> add(String key,Object value){this.map.put(key,value);return this;}
}

编写Controller报错

居然是因为依赖有问题:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.10</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.itheima</groupId><artifactId>reggie_take_out</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><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>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.31</version><scope>runtime</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.23</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.17</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.4.5</version></plugin></plugins></build></project>

备注:scope的用法

  1. compile(默认值):这是最常用的scope,表示该依赖在编译、测试、运行和打包时都是可见的。这意味着依赖将被包含在生成的JAR或WAR文件中,并且对所有阶段都是可用的。

  2. provided:该依赖在编译和测试阶段是可见的,但在运行和打包阶段不会包含在生成的JAR或WAR文件中。它假设运行时环境中已经存在该依赖,比如Java EE容器中的一些API,例如Servlet API、JSP API等。

  3. runtime:该依赖在运行和打包阶段是可见的,但在编译和测试阶段不会包含在生成的JAR或WAR文件中。它表示该依赖只在运行时才需要,例如数据库驱动。

  4. test:该依赖只在测试阶段可见,不会包含在生成的JAR或WAR文件中,它用于测试时所需的依赖。

  5. system:类似于provided,但需要明确指定依赖的路径。这样的依赖将不从Maven仓库获取,而是从本地文件系统中的特定路径加载。一般不推荐使用此scope,除非你确实需要。

  6. import:该scope用于定义一个依赖POM的依赖。它表示该依赖将被传递到项目中,并且不会用于构建项目本身。

通过合理使用scope属性,可以帮助优化项目的依赖管理,减少不必要的依赖传递和构建时的冗余。例如,对于只在编译时使用的依赖,可以设置为provided,从而在运行时不包含这些依赖,减小了最终生成的包的大小。

Day2 员工业务管理开发

完善登录功能

现存问题:即使没有登陆也可以直接访问index页面

改进思路:添加Filter

改进步骤:①实现Filter接口

                  ②重写doFilter方法

                  注意:1.匹配路径需要用到路径匹配器AntPatchMatcher。

                                匹配规则:?匹配一个字符

                                                  * 匹配任意字符序列,但不包括路径分隔符

                                                  ** 匹配任意字符序列,包含路径分隔符

                                在使用antPatchMatcher的时候,可以用match()方法进行匹配

                             2.获取请求路径用httpServletRequest.getRequestURI()方法

                             3.如果用户没有登陆,因为doFilter方法的返回值为void,所以应该用response的输出流返回响应数据。

                                       response.getWriter().write(JSON.toJSONString(R.error("NOT LOGIN")));

                        ③完成注解标注

                                i.要在Filter上方标注@WebFilter注解。其中filterName唯一,urlPatterns="/*"代表Filter将过滤所有HTTP请求,即对所有的请求进行拦截和处理。

                                ii.要在启动类上标注@ServletComponentScan,才能扫描到Filter

代码实现:

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {private AntPathMatcher PATCH_MATCHER=new AntPathMatcher();@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;//放行不需要被检查的资源String requestURI = request.getRequestURI();boolean check = checkURI(requestURI);if(check){filterChain.doFilter(request,response);return;}//判断用户是否登录,登录则直接放行if(request.getSession().getAttribute("employee")!=null){filterChain.doFilter(request,response);return;}if(request.getSession().getAttribute("user")!=null){filterChain.doFilter(request,response);return;}//如果未登录,则通过输出流方式向客户端响应数据response.getWriter().write(JSON.toJSONString(R.error("NOT LOGIN")));}private boolean checkURI(String requestURI){String[] uris=new String[]{"/employee/login","/employee/logout","/user/sendMsg","/user/login","/backend/**","/front/**"};for(String uri:uris){boolean match = PATCH_MATCHER.match(uri, requestURI);if(match){return true;}}return false;}
}

新增员工

对于新增员工,由于账号应该唯一不重复,所以如果账号重复会抛出异常:

可以编写全局异常处理器来解决这个问题:

 编写GlobalExceptionHandler

        1.@ControllerAdvice注解用于声明一个全局异常处理器类

                annotations属性指定了该全局异常处理器只处理带有@RestController或@Controller注解的控制器类(Controller)抛出的异常

        2.@ResponseBody注解,用于表示方法的返回值将直接作为响应体(Response Body)返回给客户端,而不会被视图解析器处理

          在全局异常处理器中,通过添加@ResponseBody注解,确保异常处理方法的返回值会被转换为JSON格式并返回给客户端

@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
public class GlobalExceptionHandler {@ExceptionHandler(SQLIntegrityConstraintViolationException.class)public R<String> handleCustomException(SQLIntegrityConstraintViolationException ex){if(ex.getMessage().contains("Duplicate entry")){String[] split = ex.getMessage().split(" ");String msg = split[2] + "已存在";return R.error(msg);}return R.error("未知错误");}
}

员工信息分页查询

第一步:添加mybatisplus分页器

@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor());return interceptor;}}

第二步:编写Controller

    @GetMapping("/page")public R<Page<Employee>> getByPage(@RequestParam int page, @RequestParam int pageSize,@RequestParam String name){Page<Employee> employeePage = new Page<>(page,pageSize);LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(StrUtil.isNotEmpty(name),Employee::getName,name);queryWrapper.orderByDesc(Employee::getUpdateTime);employeeService.page(employeePage);return R.success(employeePage);}

备注:加与不加@RequestParam的区别

①不加@RequestParam前端的参数名需要和后端控制器的变量名保持一致才能生效

②不加@RequestParam参数为非必传,加@RequestParam则参数为必传。但是@RequestParam可以通过@RequestParam(required=false)设置为非必传

③@RequestParam可以通过@RequestParam("userId")或者@RequestParam(value="userId")指定传入的参数名(最主要的作用)

④@RequestParam可以通过@RequestParam(defaultValue="0")指定参数默认值

⑤如果接口除了前端调用还有后端RPC调用,则不能省略@RequestParam,否则RPC会找不到参数报错

⑥Get方式请求,参数放在url中时:

        不加@RequestParam注解:url可带参数也可不带参数,输入localhost:8080/list1以及localhost:8080/list1?userId=xxx方法都能执行

        加@RequestParam注解:url必须带有参数。也就是说你直接输入localhost:8080/list2会报错,不会执行方法。只能输入localhost:8080/list2?userId=xxx才能执行相应的方法

员工启用和禁用

在员工启用和禁用功能中,虽然后台已经修改了员工的状态,但是前台却不会显示出来。这是因为前台将整型以数值型类型读出,出现了精度丢失,导致员工id与后台id不一致。

此外,前台对时间的读取不方便阅读,也可以通过自定义的JacksonObjectMapper进行自定义的序列化和反序列化。

第一步:编写JacksonObjectMapper

①在默认情况下,Jackson对象映射器(ObjectMapper)在进行反序列化时,会尝试根据需要自动将字符串类型转换为其他数据类型,包括Long类型。这个转换是基于目标属性的数据类型和字符串内容进行判断的。

例如,如果目标属性是Long类型,而JSON中的对应值是一个合法的表示长整型的字符串,那么Jackson会自动将该字符串转换为Long类型。

②this.configure(FAIL_ON_UNKNOWN_PROPERTIES,false);这个配置是针对整个ObjectMapper对象的,它会将整个ObjectMapper实例的FAIL_ON_UNKNOWN_PROPERTIES设置为false,意味着该ObjectMapper在进行序列化和反序列化时,都不会报告未知属性的异常。

this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);这个配置是在进行反序列化时,针对当前ObjectMapper实例的DeserializationConfig对象,将其中的 "FAIL_ON_UNKNOWN_PROPERTIES" 设置为 false。这样,仅针对当前的 ObjectMapper,反序列化操作在遇到未知属性时才不会抛出异常。

③区别:

如果你只配置 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 而不配置 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);,会产生如下影响:

  1. 序列化时的影响: 配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 后,在进行序列化时,无论是哪个 ObjectMapper 实例,都不会因为遇到未知属性而抛出异常。如果你的序列化操作中包含了未知属性,那么在序列化过程中,这些未知属性会被忽略,不会导致序列化失败。

  2. 反序列化时的影响: 配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 对反序列化的影响是不明显的。因为这个配置是针对整个 ObjectMapper 对象的,而在反序列化过程中,通常会使用局部的 DeserializationConfig 对象,例如 this.getDeserializationConfig(),而并不直接使用全局配置。所以,在反序列化时,未知属性是否会导致异常取决于局部的 DeserializationConfig 配置,而不是全局的配置。如果局部的 DeserializationConfig 也禁用了未知属性异常(即 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);),那么在反序列化时也会忽略未知属性,否则仍然可能抛出异常。

因此,如果你只配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);,并没有配置 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);,那么在序列化时未知属性会被忽略,但在反序列化时未知属性可能仍然会导致异常,具体取决于反序列化时局部的 DeserializationConfig 配置。如果你希望在序列化和反序列化时都忽略未知属性,建议两个配置都使用。

ToStringSerializer.instance 是 Jackson 库中的一个特殊的序列化器对象,用于将对象的值以字符串形式进行序列化。

在默认情况下,Jackson 库会根据对象的实际类型进行序列化,并输出相应的 JSON 格式。例如,对于 Java 对象的整数属性,Jackson 会将其序列化为 JSON 中的数值类型(例如整数),而对于字符串属性,Jackson 会将其序列化为 JSON 中的字符串类型。

然而,有时候我们希望将某些属性以字符串形式进行序列化,而不是根据实际类型进行序列化。这时,可以使用 ToStringSerializer.instance 来达到这个目的。

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(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类的extendMessageConverters方法

记得将自定义的ObjectMapper对应的消息转换器放在第一个优先使用。

    @Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();messageConverter.setObjectMapper(new JacksonObjectMapper());converters.add(0,messageConverter);}

Day3 分类管理业务开发

公共字段填充

在后台系统的员工管理功能开发中,新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时,也需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段。

我们可以用mybatisplus提供的公共字段自动填充功能统一处理。

第一步:编写通用工具类封装ThreadLocal,用于存储登录用户的id

public class BaseContext {private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id){threadLocal.set(id);}public static Long getCurrentId(){return threadLocal.get();}
}

第二步:在LoginCheckFilter中为已登录的用户添加id到ThreadLocal

        //判断用户是否登录,登录则直接放行if(request.getSession().getAttribute("employee")!=null){Long empId =(Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(empId);filterChain.doFilter(request,response);return;}if(request.getSession().getAttribute("user")!=null){Long userId =(Long) request.getSession().getAttribute("user");BaseContext.setCurrentId(userId);filterChain.doFilter(request,response);return;}

第三步:自定义类实现接口MetaObjectHandler,实现公共字段自动填充

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {metaObject.setValue("createTime", LocalDateTime.now());metaObject.setValue("updateTime", LocalDateTime.now());Long currentId = BaseContext.getCurrentId();metaObject.setValue("createUser", currentId);metaObject.setValue("updateUser", currentId);}@Overridepublic void updateFill(MetaObject metaObject) {metaObject.setValue("updateTime", LocalDateTime.now());Long currentId = BaseContext.getCurrentId();metaObject.setValue("updateUser", currentId);}
}

第四步:删除EmployeeController中创建时间、创建人、修改时间、修改人相关的冗余代码

删除分类

删除分类的时候需要检查该分类是否关联了菜品或者套餐,若关联应该抛出异常

第一步:自定义删除异常

public class CustomDeleteException extends RuntimeException{public CustomDeleteException(String message){super(message);}}

第二步:注册自定义删除异常

    @ExceptionHandler(CustomDeleteException.class)public R<String> handleCustomDeleteException(CustomDeleteException ex){return R.error(ex.getMessage());}

第三步:自定义删除方法

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {@Autowiredprivate DishService dishService;@Autowiredprivate SetmealService setmealService;@Overridepublic void deleteCategory(Long ids) {LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper<>();dishQueryWrapper.eq(Dish::getCategoryId,ids);int countDish = dishService.count(dishQueryWrapper);if(countDish>0){throw new CustomDeleteException("该分类含菜品,无法删除");}LambdaQueryWrapper<Setmeal> setmealQueryWrapper = new LambdaQueryWrapper<>();setmealQueryWrapper.eq(Setmeal::getCategoryId,ids);int countSetmeal = setmealService.count(setmealQueryWrapper);if(countSetmeal>0){throw new CustomDeleteException("该分类含套餐,无法删除");}this.removeById(ids);}
}

第四步:Controller调用自定义删除方法

    @DeleteMappingpublic R<String> delete(Long ids){categoryService.deleteCategory(ids);return R.success("删除分类成功");}

Day4 菜品管理业务开发

文件上传下载

文件上传:

前端要求:①表单提交,method="post" ②enctype="multipart/form-data" ③type="file"

后端要求:使用MultipartFile作为形参类型接收上传的文件

file.transferTo()方法,将文件上传到服务器指定位置

文件下载:

图片以流的形式读出并写回网页

@RestController
@RequestMapping("/common")
public class CommonsController {@Value("${reggie.path}")private String basePath;@PostMapping("/upload")public R<String> upload(MultipartFile file){String originalFilename = file.getOriginalFilename();String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));String prefix = IdUtil.simpleUUID();String filename = prefix+suffix;File dir = new File(basePath);if(!dir.exists()){dir.mkdirs();}try {file.transferTo(new File(basePath+filename));} catch (IOException e) {e.printStackTrace();}return R.success(filename);}@GetMapping("/download")public void download(String name, HttpServletResponse response){try {FileInputStream fileInputStream = new FileInputStream(basePath + name);ServletOutputStream outputStream = response.getOutputStream();response.setContentType("image/jepg");int len = 0;byte[] bytes = new byte[1024];while ((len = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, len);outputStream.flush();}fileInputStream.close();outputStream.close();}catch (Exception e){e.printStackTrace();}}
}

新增菜品

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据

注意:因为要同时操作两张表,所以需要在方法上加上注解@Transactional,同时在启动类上加注解@EnableTransactionManagement

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {@Autowiredprivate DishFlavorService dishFlavorService;@Override@Transactionalpublic void saveWithFlavor(DishDTO dishDTO) {System.out.println(dishDTO.getId());this.save(dishDTO);System.out.println(dishDTO.getId());Long dishId = dishDTO.getId();List<DishFlavor> dishFlavors = dishDTO.getDishFlavors();dishFlavors = dishFlavors.stream().map(item -> {item.setDishId(dishId);return item;}).collect(Collectors.toList());dishFlavorService.saveBatch(dishFlavors);}
}

我添加了两条打印菜品ID的语句:

 由此可见,尽管传递过来的数据菜品ID为空,但是在保存菜品到数据库以后,会将菜品ID返回至dishDTO实体类中,并可以通过dishDTO.getId()得到菜品的ID

菜品信息分页查询

注意不能在DishServiceImpl注入CategoryService,因为之前已经在CategoryService中注入过DishServiceImpl了。

解决方法:直接在DishController中写分页信息查询:

        因为页面需要的是CategoryName而非CategoryId,所以需要用categoryService查询

        返回的DishDTO里包含categoryName属性

        注意DishDTO作为一种传输手段,只需要满足需要的属性不为空即可,这里用不到DishFlavor,可以为空

    @GetMapping("/page")public R<Page<DishDTO>> getByPage(int page,int pageSize,String name) {Page<Dish> dishPage = new Page<>(page, pageSize);LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper<>();dishQueryWrapper.like(name != null, Dish::getName, name);dishQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);dishService.page(dishPage, dishQueryWrapper);Page<DishDTO> dishDTOPage = new Page<>();BeanUtils.copyProperties(dishPage, dishDTOPage, "records");List<Dish> dishRecords = dishPage.getRecords();List<DishDTO> dishDTOList = dishRecords.stream().map(item -> {DishDTO dishDTO = new DishDTO();BeanUtils.copyProperties(item, dishDTO);Long categoryId = item.getCategoryId();String categoryName = categoryService.getById(categoryId).getName();dishDTO.setCategoryName(categoryName);return dishDTO;}).collect(Collectors.toList());dishDTOPage.setRecords(dishDTOList);return R.success(dishDTOPage);}

修改菜品

第一步:菜品内容回显

    @Overridepublic DishDTO editWithFlavor(Long id) {DishDTO dishDTO = new DishDTO();Dish dish = this.getById(id);BeanUtils.copyProperties(dish,dishDTO);LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(DishFlavor::getDishId,id);List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper);dishDTO.setDishFlavors(dishFlavors);return dishDTO;}

        注意前后端内容传递与接收,前台需要用res.data.dishFlavors接收后台传递的dishFlavors,如果接收不到的话回显是会失败的

第二步:修改菜品信息

    @Overridepublic void updateWithFlavor(DishDTO dishDTO) {this.updateById(dishDTO);LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(DishFlavor::getDishId,dishDTO.getId());dishFlavorService.remove(queryWrapper);List<DishFlavor> dishFlavors = dishDTO.getDishFlavors();dishFlavors=dishFlavors.stream().map(item->{item.setDishId(dishDTO.getId());return item;}).collect(Collectors.toList());dishFlavorService.saveBatch(dishFlavors);}

Day5 套餐业务管理开发

删除套餐

在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。 注意,对于状态在售卖中的套餐不能删除,需要先停售,然后才能删除。

    @Override@Transactionalpublic void deleteWithDish(List<Long> ids) {LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();setmealLambdaQueryWrapper.in(Setmeal::getId,ids).eq(Setmeal::getStatus,1);int count = this.count(setmealLambdaQueryWrapper);if(count>0){throw new CustomDeleteException("套餐正在售卖中,不能删除");}this.removeByIds(ids);LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(SetmealDish::getSetmealId,ids);setmealDishService.remove(queryWrapper);}

注意,当接收的参数不是基本类型也不是实体类的时候,应该使用@RequestParam注解

    @DeleteMappingpublic R<String> delete(@RequestParam List<Long> ids){setmealService.deleteWithDish(ids);return R.success("删除套餐成功");}

手机验证码登录

第一步:发送验证码

第二步:登录

优化:存储“code”的时候,拼接了phone-code,这样就能避免传递过来code正确,而phone悄悄改了的问题

@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/sendMsg")public R<String> getCode(@RequestBody User user, HttpSession session){String phone = user.getPhone();String code = RandomUtil.randomNumbers(6);session.setAttribute("code", phone+"-"+code);return R.success(code);}@PostMapping("/login")public R<User> login(@RequestBody UserDTO userDTO, HttpSession session){String phone = userDTO.getPhone();String code = userDTO.getCode();String testCode =(String) session.getAttribute("code");if(testCode==null){return R.error("验证码已失效");}code = phone+"-"+code;if(!testCode.equals(code)){return R.error("验证码或手机号有误");}LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getPhone, phone);User one = userService.getOne(queryWrapper);if(one == null){one=new User();one.setPhone(phone);userService.save(one);}session.setAttribute("user", one.getId());return R.success(one);}}

Day6 菜品展示、购物车、下单

设置默认地址

第一步:将收件人的所有地址改为非默认

第二步:通过updateById()方法将指定收件地址改为默认

    @PutMapping("/default")public R<AddressBook> setDefault(@RequestBody AddressBook addressBook){Long userId = BaseContext.getCurrentId();LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>();updateWrapper.set(AddressBook::getIsDefault,0).in(AddressBook::getUserId,userId);addressBookService.update(updateWrapper);addressBook.setIsDefault(1);addressBookService.updateById(addressBook);return R.success(addressBook);}

菜品展示

前端会根据返回的结果是否含有flavors做判断,从而对没有口味选择的菜品展示【+】,对有口味选择的菜品展示【选规格】。所以只需要改造listDishes,将返回值改为R<List<DishDTO>>,并对每一个DishDTO填充flavors(如果有)即可。

菜品:

    @GetMapping("/list")public R<List<DishDTO>> listDishes(Long categoryId){LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Dish::getCategoryId,categoryId);queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List<Dish> dishList = dishService.list(queryWrapper);List<DishDTO> dishDTOList = dishList.stream().map(item -> {DishDTO dishDTO = new DishDTO();BeanUtils.copyProperties(item, dishDTO);Category category = categoryService.getById(categoryId);if (category != null) {dishDTO.setCategoryName(category.getName());}LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();wrapper.eq(DishFlavor::getDishId, item.getId());List<DishFlavor> flavors = dishFlavorService.list(wrapper);dishDTO.setFlavors(flavors);return dishDTO;}).collect(Collectors.toList());return R.success(dishDTOList);}

套餐:

    @GetMapping("/list")public R<List<Setmeal>> list(Setmeal setmeal){LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());queryWrapper.orderByDesc(Setmeal::getUpdateTime);List<Setmeal> list = setmealService.list(queryWrapper);return R.success(list);}

将菜品/套餐添加至购物车

将菜品/购物车添加至购物车的时候需要判断是否为第一次添加,如果不是则只修改数量

要区分是哪个用户添加的

    @PostMapping("/add")public R<ShoppingCart> save(@RequestBody ShoppingCart shoppingCart){Long userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ShoppingCart::getUserId,userId);Long dishId = shoppingCart.getDishId();if(dishId!=null){queryWrapper.eq(ShoppingCart::getDishId,dishId);}else{queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());}ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);if(cartServiceOne!=null){Integer number = cartServiceOne.getNumber();cartServiceOne.setNumber(number+1);shoppingCartService.updateById(cartServiceOne);}else{shoppingCart.setNumber(1);shoppingCartService.save(shoppingCart);cartServiceOne=shoppingCart;}return R.success(cartServiceOne);}

用户下单

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {@Autowiredprivate ShoppingCartService shoppingCartService;@Autowiredprivate UserService userService;@Autowiredprivate AddressBookService addressBookService;@Autowiredprivate OrderDetailService orderDetailService;public OrdersServiceImpl() {}@Override@Transactionalpublic void submit(Orders orders) {//获得当前用户idLong currentId = BaseContext.getCurrentId();//查询当前用户的购物车数据LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,currentId);List<ShoppingCart> shoppingCarts = shoppingCartService.list(shoppingCartLambdaQueryWrapper);if(shoppingCarts==null || shoppingCarts.size()==0){throw new CustomDeleteException("购物车为空,不能下单!");}//查询用户数据User user = userService.getById(currentId);//查询地址数据Long addressBookId = orders.getAddressBookId();AddressBook addressBook = addressBookService.getById(addressBookId);if(addressBook==null){throw new CustomDeleteException("用户地址信息有误,不能下单!");}//向订单表插入数据,一条数据long orderId = IdWorker.getId();AtomicInteger amount=new AtomicInteger(0);List<OrderDetail> orderDetails=shoppingCarts.stream().map(item->{OrderDetail orderDetail=new OrderDetail();orderDetail.setOrderId(orderId);orderDetail.setNumber(item.getNumber());orderDetail.setDishFlavor(item.getDishFlavor());orderDetail.setDishId(item.getDishId());orderDetail.setSetmealId(item.getSetmealId());orderDetail.setName(item.getName());orderDetail.setImage(item.getImage());orderDetail.setAmount(item.getAmount());amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());return orderDetail;}).collect(Collectors.toList());orders.setId(orderId);orders.setOrderTime(LocalDateTime.now());orders.setCheckoutTime(LocalDateTime.now());orders.setStatus(2);orders.setAmount(new BigDecimal(amount.get()));orders.setUserId(currentId);orders.setNumber(String.valueOf(orderId));orders.setUserName(user.getName());orders.setConsignee(addressBook.getConsignee());orders.setPhone(addressBook.getPhone());orders.setAddress((addressBook.getProvinceName()==null?"":addressBook.getProvinceName())+(addressBook.getCityName()==null?"":addressBook.getCityName())+(addressBook.getDistrictName()==null?"":addressBook.getDistrictName())+(addressBook.getDetail()==null?"":addressBook.getDetail()));this.save(orders);//向订单明细表插入数据,多条数据orderDetailService.saveBatch(orderDetails);//清空购物车数据shoppingCartService.remove(shoppingCartLambdaQueryWrapper);}}

 


复写部分基本完成~

相关文章:

【瑞吉外卖项目复写】基本部分复写笔记

Day1 瑞吉外卖项目概述 mysql的数据源配置 spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/regie?serverTimezoneAsia/Shanghai&useUnicodetrue&characterEncodingutf-8&zeroDateTimeBehaviorconvertTo…...

用html+javascript打造公文一键排版系统15:一键删除所有空格

现在我们来实现一键删除所有空格的功能。 一、使用原有的代码来实现&#xff0c;测试效果并不理想 在这之前我们已经为String对象编写了一个使用正则表达式来删除所有空格的方法&#xff1a; //功能&#xff1a;删除字符串中的所有空格 //记录&#xff1a;20230726创建 Stri…...

苍穹外卖day12(完结撒花)——工作台+Spring_Apche_POI+导出运营数据Excel报表

工作台——需求分析与设计 产品原型 接口设计 工作台——代码导入 将提供的代码导入对应的位置。 工作台——功能测试 Apache POI_介绍 应用场景 Apache POI_入门案例 导入坐标 <!-- poi --><dependency><groupId>org.apache.poi</groupId><ar…...

SQL与NoSQL概念(详细介绍!!)

先搞清楚全称 SQL全称为Structured query language &#xff0c;即结构化查询语言&#xff0c;可以把他理解为一门特殊的编程语言。 那么nosql是什么意思呢&#xff1f;这里的no并不仅是not&#xff0c;而是not only的意思&#xff0c;所以nosql全称应该是Not Only Structure…...

node debian 镜像 new Date 获取时间少 8 小时问题

问题 在 node debian 镜像中&#xff0c;用 (new Date()).getHours() 与系统时间&#xff08;东 8 区&#xff09;少了 8 小时 系统时间 $ node > (new Date()).getHours() 11容器中的时间 $ node > (new Date()).getHours() 3原 Dockerfile FROM node:20.5-bullsey…...

【N32L40X】学习笔记13-软件IIC读写EEPROM AT24C02

AT24C02 8个字节每页,累计32个页 通讯频率MAX 400K AT24C02大小 2K 芯片地址 对于at24c02 A2A1A0 这三个引脚没有使用 写时序 由于设备在写周期中不会产生ACK恢复&#xff0c;因此这可用于确定周期何时完成&#xff08;此特性可用于最大限度地提高总线吞吐量&#xff09;…...

JVM 调优

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ JVM调优是一项重要的任务&#xff0c;可以提高Java应用程序的性能和稳定性。掌握JVM调优需要深入了解JVM的工作原理、参数和配置选项&#xff0c;以及历史JVM参数的调整和优…...

DP-GAN剩余代码

在前面计算完损失后&#xff0c;该进行更新&#xff1a; 1&#xff1a;netEMA是模型的生成器&#xff1a; 遍历生成器的state_dict&#xff0c;将每一个键对应的值乘以EMA_decay。 接着根据当前迭代步数计算num_upd&#xff0c;每1000,2500,10000代倍数就执行一次。 当num…...

在word的文本框内使用Endnote引用文献,如何保证引文编号按照上下文排序

问题 如下图所示&#xff0c;我在word中插入了一个文本框&#xff08;为了插图&#xff09;&#xff0c;然后文本框内有引用&#xff0c;结果endnote自动将文本框内的引用优先排序&#xff0c;变成文献[1]了&#xff0c;而事实上应该是[31]。请问如何能让文本框内的排序也自动…...

SpringBoot项目上传至服务器

1.服务器安装JDK1.8 通过包管理器安装 2.服务器安装数据库 参考链接&#xff1a; CentOS 7 通过 yum 安装 MariaDB - 知乎 1. 安装之后没有密码&#xff0c;所以需要设置密码&#xff0c;使用下面的语句 set password for rootlocalhost password(111111); 2.在数据库中建…...

C++中实现多线程的三种方式

目录 1 背景2 方法 1 背景 力扣1116题 打印零和奇偶数。 2 方法 方法1&#xff1a;原子操作 class ZeroEvenOdd { private:int n;atomic<int> flag 0; public:ZeroEvenOdd(int n) {this->n n;}// printNumber(x) outputs "x", where x is an integer.…...

程序员副业指南:怎样实现年入10w+的目标?

大家好&#xff0c;这里是程序员晚枫&#xff0c;全网同名。 今天给大家分享一个大家都感兴趣的话题&#xff1a;程序员可以做什么副业&#xff0c;年入十万&#xff1f; 01 推荐 程序员可以从事以下副业&#xff0c;以获得一年收入10w&#xff1a; 兼职编程&#xff1a;可…...

excel 计算 分位值

_XLFN.QUARTILE.EXC(Result 1!G:G,2) 和 PERCENTILE 都可以用来计算一组数据的分位数&#xff0c;但是它们的计算方式略有不同。 _XLFN.QUARTILE.EXC(Result 1!G:G,2) 是 Excel 中的一个函数&#xff0c;在计算一个数据集的四分位数时使用。其中&#xff0c;第一个参数 Result…...

mongodb-windows-x86_64-4.4.23-signed.msi

...

一个SpringBoot 项目能处理多少请求?

这篇文章带大家盘一个读者遇到的面试题哈。 根据读者转述&#xff0c;面试官的原问题就是&#xff1a;一个 SpringBoot 项目能同时处理多少请求&#xff1f; 不知道你听到这个问题之后的第一反应是什么。 我大概知道他要问的是哪个方向&#xff0c;但是对于这种只有一句话的…...

Shell编程基础(十)读取多行文本到数组 写入多行文本到文件

读取多行文本到数组 & 写入多行文本到文件 读取多行文本到数组写入多行文本到文件 读取多行文本到数组 创建一个文本文件&#xff0c;内容如下 1 zhangsan 男 10 2 liis 女 12 3 wangwu 男 17读取这个文件中所有人的信息 #!/bin/bash while read u do echo $u done <…...

MyBatis学习笔记2

CRUD 1.namespace namespace中的包名要和mapper接口的包名一致&#xff01; 2.select 选择查询语句 id&#xff1a;就是对应的namespace中的方法名&#xff1b; resultType:Sql语句执行的返回值&#xff01; parameterType&#xff1a;参数类型 增删改必须提交事务&…...

spring总结

目录 什么是Spring? Spring的优缺点&#xff1f; 优点&#xff1a; 缺点&#xff1a; Spring IOC的理解 Spring AOP的理解 事务的边界为什么放在service层&#xff1f; Spring Bean的生命周期 什么是单例池&#xff1f;作用是什么&#xff1f; 单例Bean的优势 Bean…...

记录--说一说css的font-size: 0

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 平常我们说的font-size&#xff1a;0&#xff1b;就是设置字体大小为0对吧&#xff0c;但是它的用处不仅仅如此哦&#xff0c;它还可以消除子行内元素间额外多余的空白&#xff01; 问题描述&#xff…...

Matlab实现支持向量机算法(附上多个完整仿真源码)

支持向量机是一种常见的机器学习算法&#xff0c;它可以用于分类和回归问题。在Matlab中使用支持向量机&#xff0c;可以方便地构建和训练模型&#xff0c;并进行预测和评估。本文将介绍Matlab支持向量机的基本原理以及一个简单的分类案例。 文章目录 1. 支持向量机的基本原理2…...

19c补丁后oracle属主变化,导致不能识别磁盘组

补丁后服务器重启&#xff0c;数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后&#xff0c;存在与用户组权限相关的问题。具体表现为&#xff0c;Oracle 实例的运行用户&#xff08;oracle&#xff09;和集…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩

目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

IT供电系统绝缘监测及故障定位解决方案

随着新能源的快速发展&#xff0c;光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域&#xff0c;IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选&#xff0c;但在长期运行中&#xff0c;例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式&#xff0c;以r为参数&#xff1a; p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]&#xff1b; 此多项式的根为&#xff1a; 尽管看起来这个多项式是特殊的&#xff0c;其实一般的三次多项式都是可以通过线性变换化为这个形式…...

【Linux】Linux 系统默认的目录及作用说明

博主介绍&#xff1a;✌全网粉丝23W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...