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);}}
复写部分基本完成~