【瑞吉外卖项目复写】基本部分复写笔记
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的用法
-
compile(默认值):这是最常用的scope,表示该依赖在编译、测试、运行和打包时都是可见的。这意味着依赖将被包含在生成的JAR或WAR文件中,并且对所有阶段都是可用的。 -
provided:该依赖在编译和测试阶段是可见的,但在运行和打包阶段不会包含在生成的JAR或WAR文件中。它假设运行时环境中已经存在该依赖,比如Java EE容器中的一些API,例如Servlet API、JSP API等。 -
runtime:该依赖在运行和打包阶段是可见的,但在编译和测试阶段不会包含在生成的JAR或WAR文件中。它表示该依赖只在运行时才需要,例如数据库驱动。 -
test:该依赖只在测试阶段可见,不会包含在生成的JAR或WAR文件中,它用于测试时所需的依赖。 -
system:类似于provided,但需要明确指定依赖的路径。这样的依赖将不从Maven仓库获取,而是从本地文件系统中的特定路径加载。一般不推荐使用此scope,除非你确实需要。 -
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);,会产生如下影响:
-
序列化时的影响: 配置了
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);后,在进行序列化时,无论是哪个 ObjectMapper 实例,都不会因为遇到未知属性而抛出异常。如果你的序列化操作中包含了未知属性,那么在序列化过程中,这些未知属性会被忽略,不会导致序列化失败。 -
反序列化时的影响: 配置了
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:一键删除所有空格
现在我们来实现一键删除所有空格的功能。 一、使用原有的代码来实现,测试效果并不理想 在这之前我们已经为String对象编写了一个使用正则表达式来删除所有空格的方法: //功能:删除字符串中的所有空格 //记录: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 ,即结构化查询语言,可以把他理解为一门特殊的编程语言。 那么nosql是什么意思呢?这里的no并不仅是not,而是not only的意思,所以nosql全称应该是Not Only Structure…...
node debian 镜像 new Date 获取时间少 8 小时问题
问题 在 node debian 镜像中,用 (new Date()).getHours() 与系统时间(东 8 区)少了 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恢复,因此这可用于确定周期何时完成(此特性可用于最大限度地提高总线吞吐量)…...
JVM 调优
点击下方关注我,然后右上角点击...“设为星标”,就能第一时间收到更新推送啦~~~ JVM调优是一项重要的任务,可以提高Java应用程序的性能和稳定性。掌握JVM调优需要深入了解JVM的工作原理、参数和配置选项,以及历史JVM参数的调整和优…...
DP-GAN剩余代码
在前面计算完损失后,该进行更新: 1:netEMA是模型的生成器: 遍历生成器的state_dict,将每一个键对应的值乘以EMA_decay。 接着根据当前迭代步数计算num_upd,每1000,2500,10000代倍数就执行一次。 当num…...
在word的文本框内使用Endnote引用文献,如何保证引文编号按照上下文排序
问题 如下图所示,我在word中插入了一个文本框(为了插图),然后文本框内有引用,结果endnote自动将文本框内的引用优先排序,变成文献[1]了,而事实上应该是[31]。请问如何能让文本框内的排序也自动…...
SpringBoot项目上传至服务器
1.服务器安装JDK1.8 通过包管理器安装 2.服务器安装数据库 参考链接: CentOS 7 通过 yum 安装 MariaDB - 知乎 1. 安装之后没有密码,所以需要设置密码,使用下面的语句 set password for rootlocalhost password(111111); 2.在数据库中建…...
C++中实现多线程的三种方式
目录 1 背景2 方法 1 背景 力扣1116题 打印零和奇偶数。 2 方法 方法1:原子操作 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+的目标?
大家好,这里是程序员晚枫,全网同名。 今天给大家分享一个大家都感兴趣的话题:程序员可以做什么副业,年入十万? 01 推荐 程序员可以从事以下副业,以获得一年收入10w: 兼职编程:可…...
excel 计算 分位值
_XLFN.QUARTILE.EXC(Result 1!G:G,2) 和 PERCENTILE 都可以用来计算一组数据的分位数,但是它们的计算方式略有不同。 _XLFN.QUARTILE.EXC(Result 1!G:G,2) 是 Excel 中的一个函数,在计算一个数据集的四分位数时使用。其中,第一个参数 Result…...
一个SpringBoot 项目能处理多少请求?
这篇文章带大家盘一个读者遇到的面试题哈。 根据读者转述,面试官的原问题就是:一个 SpringBoot 项目能同时处理多少请求? 不知道你听到这个问题之后的第一反应是什么。 我大概知道他要问的是哪个方向,但是对于这种只有一句话的…...
Shell编程基础(十)读取多行文本到数组 写入多行文本到文件
读取多行文本到数组 & 写入多行文本到文件 读取多行文本到数组写入多行文本到文件 读取多行文本到数组 创建一个文本文件,内容如下 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接口的包名一致! 2.select 选择查询语句 id:就是对应的namespace中的方法名; resultType:Sql语句执行的返回值! parameterType:参数类型 增删改必须提交事务&…...
spring总结
目录 什么是Spring? Spring的优缺点? 优点: 缺点: Spring IOC的理解 Spring AOP的理解 事务的边界为什么放在service层? Spring Bean的生命周期 什么是单例池?作用是什么? 单例Bean的优势 Bean…...
记录--说一说css的font-size: 0
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白! 问题描述ÿ…...
Matlab实现支持向量机算法(附上多个完整仿真源码)
支持向量机是一种常见的机器学习算法,它可以用于分类和回归问题。在Matlab中使用支持向量机,可以方便地构建和训练模型,并进行预测和评估。本文将介绍Matlab支持向量机的基本原理以及一个简单的分类案例。 文章目录 1. 支持向量机的基本原理2…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...
脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅!
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅! 🌱 前言:一棵树的浪漫,从数组开始说起 程序员的世界里,数组是最常见的基本结构之一,几乎每种语言、每种算法都少不了它。可你有没有想过,一组看似“线性排列”的有序数组,竟然可以**“长”成一棵平衡的二…...
