Day1 瑞吉外卖项目概述
mysql的数据源配置
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/regie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
注意要配置mysql其实是配置druid数据源。
注意url的后缀。
mybatisplus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-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 {
@Override
protected 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();
@Override
public 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 {
@Bean
public 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对应的消息转换器放在第一个优先使用。
@Override
protected 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 {
@Override
public 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);
}
@Override
public 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 {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
@Override
public 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调用自定义删除方法
@DeleteMapping
public 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 {
@Autowired
private DishFlavorService dishFlavorService;
@Override
@Transactional
public 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);
}
修改菜品
第一步:菜品内容回显
@Override
public 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,如果接收不到的话回显是会失败的
第二步:修改菜品信息
@Override
public 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
@Transactional
public 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注解
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
setmealService.deleteWithDish(ids);
return R.success("删除套餐成功");
}
手机验证码登录
第一步:发送验证码
第二步:登录
优化:存储“code”的时候,拼接了phone-code,这样就能避免传递过来code正确,而phone悄悄改了的问题
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private 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 {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
public OrdersServiceImpl() {
}
@Override
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long 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);
}
}
复写部分基本完成~