reggie(瑞吉外卖)项目过程总结
记,第一次跟黑马视频学做项目的过程笔记。
仅记录
,不做任何责任声明。我坚信
进步的过程
就是一次一次试错
的结果。
随时会修改,因为我们都随时在进步。
说不定以后你就是那个大神!说的不是我,是你!
同时也希望有前辈能给些意见~
一、项目创建
1、创建SpringBoot项目,(maven)
-
SprinBoot
版本选择3.0以下
的,3.0以上需要JDK17 -
JDK版本统一
1.8
-
勾选组件,
web、SQL驱动、lombok
-
Pom.xml中导入自定义坐标:
-
<!--MybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!--JSON格式转换工具--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.21</version> </dependency> <!--跟java.lang这个包的作用类似,Commons Lang这一组API也是提供一些基础的、通用的操作和处理--> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <!--druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.23</version> </dependency>
-
-
修改
application.properties
文件的格式为yml
,并添加相关配置-
server: port: 8080 spring: application: name: reggie_take_out datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: jasonzhang1511 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 #定义生成ID的类型 雪花算法
-
-
创建要用的的包在
com.jason
下- 实体类:
domain或entity
- 数据层:
dao或mapper
- 服务层:
service
及其实现类层impl
- 控制层:
controller
- 后续可能会出现的包:
通用层:common/异常处理/结果封装
;过滤层:filter/过滤器
;配置层:config/静态资源映射等...
- …
- 实体类:
二、登陆功能-前后端协调-细节处理
是否需要静态资源映射?
spring扫描静态资源,默认是在resources/statics,
如果开发时用的是别的静态资源包,则需要进行配置
新建一个
config
文件夹,创建
WebMvcConfig
类,继承
WebMvcConfigurationSupport
,重写
addResourceHandlers(ResourceHandlerRegistry registry)
方法protected void addResourceHandlers (ResourceHandlerRegistry registry) { //访问/backend包下的任何资源时,重定向到/backend去,可以重设静态资源包 registry.addResourceHandler("/backend/**") .addResourceLocations("classpath:/backend/"); }
1、编写domain、dao、service
-
domain/entity: 使用
@Data
注解,让lombok自动生成一些列方法。实体类实现Serializable接口
,id为Long型
,因为MP生成的id是雪花算法生成,若要跟随数据库自增,可以设置id的属性为AUTO
。 -
dao/mapper: 接口要继承MP的
BaseMapper<实体类>
,并且添加类注解@Mapper
。 -
service&impl:
-
service: 是一个接口,继承自MP提供的
IService<实体类>
。 -
ServiceImpl: 继承自MP提供的
ServiceImp<实体类Mapper,实体类>
,同时也要实现Service接口
加上类注解@Service
public interface EmployeeService extends IService<Employee> {} @Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService {}
-
2、编写Controller
-
根据前端页面反馈或规定,确定编写controller的信息:
- 请求的路径
- 请求的形式:Get/Post/Put/Delete…
- 返回形式以及参数:一般返回的都是前后端协调后的协议,
common
包下的R
类【R类中规定了后端处理完成之后需要返回给前端的信息( 属性: code,msg,data,map; 方法: success,error,add),前端接到返回的R,再按照格式去取其中需要的信息即可】
-
Controller中注意的事项
-
@RestController
使用Rest风格。 -
@RequestMapping("/employee")
针对请求路径进行映射 -
内部调用
@AutoWired ...employeeService
进行方法调用。 -
接收到
/login
请求,携带来JSON数据,用@RequstBody Employee employee
来接收,同时也需要一个请求参数HttpServletRequest request
用来存储session。-
对密码进行MD5加密处理
//1 对密码进行MD5加密处理 String password = employee.getPassword(); password = DigestUtils.md5DigestAsHex(password.getBytes());
-
根据请求对象的
username
进行查询数据库,并返回一个对象 -
判断入返回的对象为空,则用户不存在,
return R.error
。 -
对象不为空,判断对象的密码与输入的密码是否相同,若不同,
return R.error
。 -
用户名和密码都匹配成功需要判断用户的
状态
是否为1,即启用。否则return R.error
。 -
经过上述三级判断,用户登录成功,将用户的
唯一标识id
存储为到session
中, -
return R.success(查询出的对象)
;如下
@PostMapping("/login") public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){ //1 对密码进行MD5加密处理 String password = employee.getPassword(); password = DigestUtils.md5DigestAsHex(password.getBytes()); //2 根据提交的用户名进行查询 LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Employee::getUsername,employee.getUsername()); Employee one = employeeService.getOne(queryWrapper); //3 若没有查询到 则return失败的结果 if (one == null){ return R.error("登陆失败,用户不存在。"); } //4 密码比对,如果不一致则返回登陆失败的结果 if (!one.getPassword().equals(password)){ return R.error("登录失败,密码错误。"); } //5 查看员工状态status是否是禁用 if (one.getStatus()!=1){ return R.error("登录失败,用户已被禁用。"); } //6 登陆成功,将员工id存入session并返回登陆成功结果 request.getSession().setAttribute("employee",one.getId()); //返回成功信息 放入查出来的对象 return R.success(one); }
-
-
3、Servlet过滤器Filter实现登陆拦截
-
在包
filter
下创建拦截器类LoginFilter
,实现Filter接口,重写doFilter()
方法,内有三个参数:ServletRequest servletRequest
:一般使用HttpServletRequest,进行强转再使用。ServletResponse servletResponse
:一般使用HttpServletResponse,进行强转再使用。FilterChain filterChain
:控制是否放行。
-
类上注解
@webFilter(filterName = "loginCheckFilter" ,urlPatterns = "/*")
,指定拦截的路径为所有。同时在SpringBoot启动类上也要注解@ServletComponentScan
进行扫描有@WebFilter注解的类
-
具体拦截策略:
-
通过 request.getRequestURI() 获取请求的URI;
-
定义一个
String[] 数组
用来存放静态资源路径和登陆注册相关
的请求路径; -
通过
AntPathMatcher
的PATH_MATCHER
属性的方法match
来匹配请求的URI是否是需要放行的路径。public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); //路径匹配封装为一个方法check public boolean check(String[] urls, String requestURI){ for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if (match){ return true; } } return false; }
-
若匹配到的是放行的资源,进行放行:
// 若匹配到的是放行的资源 放行 if (check){ filterChain.doFilter(request,response); return; //若放行 后面代码无需执行 直接return }
-
若时已登陆的状态,放行
// 若是已登陆的状态 放行 if (request.getSession().getAttribute("employee")!=null){ filterChain.doFilter(request,response); return; //若放行 后面代码无需执行 直接return }
-
经上方判定,不是放行的资源,且为未登陆,返回数据:
// 返回未登陆的结果 通过输出流的方式 向客户端页面响应JSON数据,客户端通过ajax处理返回的数据,进行重定向。 response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
-
4、退出
清除session即可。
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
三、员工管理业务开发
1、分页查询
-
客户端发送的请求时分页查询
/employee/page
,并携带了两个参数,page和pageSize
-
查阅前端页面可以发现要求返回的数据是
this.data.records 和 total
这些参数都是MP的Page<T>
类下的属性 直接使用即可。 -
当然使用分页查询,我们需要配置MP的分页插件,添加分页内部拦截器,并交给spring进行管理。
/** *配置MP的分页插件 */ @Configuration //配置类统一注解 public class MybatisPlusConfig { @Bean //交给spring管理 public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); //添加分页内部拦截器 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }
-
上面配置好之后,就可以直接使用MP提供的page方法。
@GetMapping("/page") //参数直接携带过来,不是JSON,不用注解。但是参数和变量名一定要写一样。
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器,两个参数为前端页面传过来的当前页和每页展示条数
Page<Employee> pageInfo = new Page(page,pageSize);
//构造条件构造器,通过条件构造器,可以为sql语句添加很多条件
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件 name 若name为空则无需执行该语句
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件 通过创建时间排序
queryWrapper.orderByDesc(Employee::getCreateTime);
//执行查询,并封装给pageInfo对象,所以直接返回经过操作的 pageInfo对象即可。
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
2、启用禁用
该功能只有管理员才可以编辑,其他用户登录后不会显示此操作按钮
前端代码已经实现该功能,我们只需要实现后端相关的操作即可
- 确定前端请求的路径
/employee
;以及请求方式Put
- 确定前端请求携带的参数
Employee
- 确定需要返回的类型
R.success("修改成功")
–> 在EmployeeController中编写update方法,还额外需要一个HttpServletRequest request
参数,用来获取操作者的信息,并存储操作记录(在Employee中有更新操作用户和更新时间需要每次都设定)
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee ){
Long employee1 = (Long) request.getSession().getAttribute("employee");
employee.setUpdateUser(employee1);//设置操作修改的人
employee.setUpdateTime(LocalDateTime.now());//设置修改信息的时间
employeeService.updateById(employee);
return R.success("修改成功。");
}
-
但是在经过测试后,提示修改成功,但是数据库中的操作对象的status属性并没有改变
发现前端发送的id丢失了精度
(Long型19位,js中只保证16位,导致后三位精度丢失)
-
需要在
common包
添加一个对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
, -
即将Long型的ID序列化为JSON形式,在传输数据时两端进行序列化与反序列化。
-
同时也可以对日期和时间类型的属性进行统一格式处理。
/** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] */ public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(BigInteger.class, ToStringSerializer.instance) .addSerializer(Long.class, ToStringSerializer.instance) //将Long型转为字符串,方式数据精度丢失。 .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); } }
-
在配置包
config
下的WebMvcConfig配置类
中扩展mvc框架的消息转换器,重写方法extendMessageConverters
,将上述编写的随想映射器加载进去排在第一位即可。/** * 扩展mvc框架的消息转换器 * @param converters */ @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("启动消息转换器..."); //创建消息转换器对象 MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); //设置对象转换器,底层使用jackson将java转为json messageConverter.setObjectMapper(new JacksonObjectMapper()); //将上面的创建的消息转换器追加到mvc框架的转换器集合中,指定追加的未知位第一位,index=0 converters.add(0,messageConverter); }
3、编辑信息
-
点击
编辑
按钮时,需要对员工信息进行回显,调用根据id查询数据的方法。 -
分析请求路径
/employee/{id}
;请求方式Get
。 -
在EmployeeController中编写
getById()
的方法。/** * 根据id查询员工信息 进行回显 * @param id * @return */ @GetMapping("/{id}")//占位,与参数id进行映射 public R<Employee> getById(@PathVariable Long id){ //参数从路径传来,注意注解@PathVariable Employee emp = employeeService.getById(id); if (emp == null){ return R.error("操作失败,未查询到..."); } return R.success(emp); }
-
测试通过,另外编写信息后,点击确定时的方法不用再写了,上方
update
已经完成了。
针对公共字段的代码优化
1、公共字段自动填充
在项目中我们发现,几乎每一个数据表中都有相同的字段:
创建时间,创建人,修改时间,修改人
他们被称为公共字段,MybaitsPlus提供了对公共字段自动填充的处理,分为以下两步:
-
在实体类的属性上添加
@TableField
注解,指定自动填充策略//创建时间 @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; //更新时间 @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;
-
根据MP框架的要求,编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现
MetaObjectHandler
接口,用来管理自动填充公共字段的类。并注解`@Component,交给spring去管理。 -
重写两个方法:
插入:insertFill()
和更新:updateFill()
,使用参数metaObject
的setValue
方法,第一个参数为“属性名”第二个参数为要设置的值,如下:@Component public class MyMetaObjectHandler implements MetaObjectHandler { /** * 执行插入/新增时 自动填充 * 调用工具类 获取在过滤器中判定为登陆的用户的id * @param metaObject */ @Override public void insertFill(MetaObject metaObject) { log.info("插入时执行的自动填充...{}",metaObject); metaObject.setValue("createTime", LocalDateTime.now()); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("createUser", BaseContext.getCurrentId()); metaObject.setValue("updateUser", BaseContext.getCurrentId()); } /** * 执行更新/修改时 自动填充 * 调用工具类 获取在过滤器中判定为登陆的用户的id * @param metaObject */ @Override public void updateFill(MetaObject metaObject) { log.info("更新时执行的自动填充...{}",metaObject); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("updateUser", BaseContext.getCurrentId()); } }
-
关于第3步中获取操作用户id的问题详解:
-
在公共字段设置操作人和创建人时,我们需要动态获取当前登陆用户的 id。但是我们写的类和继承的类中并没有
HttpSession
可以调取,这时候需要用到ThreadLocal类,它是JDK中提供的一个类。在客户端每次发送http请求时,对应的在服务端都会重新创建一个线程来处理。
在一个线程的处理过程中,涉及到的下面的方法都属于一个线程中:
LoginCheckFilter
中的doFilter
方法EmployeeController
中的update
方法MyMetaObjectHandler
中的updateFill
方法在三个方法中都打印
Thread.currentThread().getId()
,会发现是一样的。这样就证明了在一次请求中的线程id是相同的。
所以:我们可以在登陆请求时就将操作人的id存入Thread中。
这样在线程中随时可以获得操作人的id。
-
我们将id存储到Thread中的操作,放在
LoginCheckFilter
中的doFilter
方法中判断登陆成功时执行,注意存储的id是Long型的。// 4 若是已登陆的状态 放行 if (request.getSession().getAttribute("employee")!=null){ log.info("监测到已登陆,id为:{}",request.getSession().getAttribute("employee")); //调用我们基于ThreadLocal的工具类,将用户id存储到线程中,方便在自动填充时取出用户id Long employeeId = (Long) request.getSession().getAttribute("employee"); BaseContext.setCurrentId(employeeId); filterChain.doFilter(request,response); //放行 return; //若放行 后面代码无需执行 直接return }
-
在上一步之前,我们要创建一个
基于ThreadLocal封装的工具类BaseContext
,一次请求会有一个Thread,不会混淆。在其中创建get和set方法来保存和去除操作者的id:(在不同的地方调用id,可以直接通过此工具类来调用,无需再重新创建ThreadLocal)/** * 基于ThreadLocal封装的工具类,用户保存和获取当前用户的id * 一次请求一个thread 不会混淆 */ public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * 设置id值,用于在登陆成功后保存操作者的id * @param id */ public static void setCurrentId(Long id){ threadLocal.set(id); } /** * 获取id,用于在元数据对象拦截填充时,获取操作者的id * @return */ public static Long getCurrentId(){ return threadLocal.get(); } }
-
四、分类管理业务开发
1、编写分类Category
相关的domain、dao、service、serviceImpl
包和类。
其中注意domain中的公共字段要设置@TableFiled(fill= FiledFil.INSERT)
dao中要加类注解@Mapper;serviceImpl中类注解@Service
2、编写controller及方法:新增(菜品分类、套餐分类)、分页、删除、编辑等操作。
过于简单,省略过程。
需要注意的是:若客户端发送请求的参数,不需要转换,则方法不需要加
@RequestBody
。在分页请求中客户端携带的参数是
page:1 pageSize:10
,删除功能中参数id=112311213...
。若参数是JSON格式,则要加注解,将传来的参数转换为对应的对象即可。
//客户端传来的参数就是id,直接接收即可,分页查询中也是不需要加参数注解,请求携带的参数就是 @DeleteMapping public R<String> deleteById(Long id){ categoryService.remove(id); return R.success("删除成功"); }
编辑中的回显功能,在前端html中已经添加了,我们只需要编写一个修改的方法即可。
3、删除操作需要注意的相关问题:
因为本页面是分类管理,每个分类下面会关联很多菜品或者套餐,删除时要判断
是否关联?不删除,提示失败 ; 删除,提示成功
。为此,我们需要
在service中编写remove方法
,虽然MP中也有,但是并不满足我们的要求,我们需要在之前加入判断。在新写的remove方法中,先进行条件判断,最后再调用MP的removeById方法。
要进行判断,我们就需要创建判断的两个类,
菜品Dish和套餐Setmeal
与数据库对应;并编写对应的mapper(dao)、service、serviceImpl,
之后在分类的服务层
CategoryService和CategoryServiceImpl
中注入上面的两个类,编写remove方法,最后调用MP的removeById即可。
4、CategoryServiceImpl
中的方法编写
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
public void remove(Long id){
LambdaQueryWrapper<Dish> dishWrapper = new LambdaQueryWrapper<>();
dishWrapper.eq(Dish::getCategoryId,id);//用id与Dish中的categoryId进行匹配
long count1 = dishService.count(dishWrapper);
if (count1>0){
//删除的菜品分类或套餐分类中关联了菜品,删除失败,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,删除失败!");
}
LambdaQueryWrapper<Setmeal> setmealWrapper = new LambdaQueryWrapper<>();
setmealWrapper.eq(Setmeal::getCategoryId,id);
long count2 = setmealService.count(setmealWrapper);
if (count2>0){
//删除的菜品分类或套餐分类中关联了套餐,删除失败,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,删除失败!");
}
//上面两个条件都不成立时 就执行MP提供的ServiceImpl的removeById方法,执行删除。
super.removeById(id);
}
五、菜品管理业务开发
0、上传和下载
经查看前端代码得知,上传和下载的请求路径为
/commom
下的/update
和/download
。**注意:**参数的传递,上传的是文件,使用的参数只能用
MultipartFile file
,这里的file名字必须与页面传的参数名字一致。
-
参数接收到的file是一个临时文件,会随着程序的接收而自动删除。
-
这里需要将file转存到自己的文件夹下。等到浏览器下载进行回显的时候,从同样的文件夹去下载。
-
转存时需要给文件重新设置一个文件名,但是文件类型后缀不能更改。防止以后上传的数据多的时候,存在重名的情况。
-
增强代码灵活性,我们使用动态命名的方式。在yml配置文件中,配置一个路径:
reggie: path:/Volumes/酷炫的u盘/学习相关/上传图片转存位置
,在类中我们可以自动装配在一个属性pathName,${reggie.path}
。//pathName在yml配置文件中进行配置 用@Value取值即可。 @Value("${reggie.path}") private String pathName; @PostMapping("/upload") public R<String> upload(MultipartFile file) { log.info("上传的文件:" + file); //注意:此时的file是一个临时文件,随着程序的结束而自动删除。 // 所以我们需要将其存储在一个文件夹下,方便后续下载到浏览器进行展示。 //获取原始文件名 String originalFilename = file.getOriginalFilename(); //截取原始文件名的后缀.jpg(含'.') String substring = originalFilename.substring(originalFilename.lastIndexOf(".")); //使用uuid随机生成新的文件名,防止以后上传文件出现重复名字。 String filename = UUID.randomUUID().toString() + substring; //防止目标目录不存在,可以添加一次判断 // File file1 = new File("/Volumes/酷炫的u盘/学习相关/上传图片转存位置"); // if (!file1.exists()) { // file1.mkdirs(); // } try { //将临时文件转存到指定位置 使用动态命名。 file.transferTo(new File(pathName + filename)); } catch (Exception e) { e.printStackTrace(); } //返回的是存储的文件名!返回给表单之后,保存时会将文件名保存在数据库。 return R.success(filename); }
下载时,页面传来的是文件名,我们可以根据pathName+文件名的形式,通过输入流,读取到文件,再通过response的输出流将文件返回给页面。
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流来读取文件内容
FileInputStream inputStream = new FileInputStream(pathName + name);
//输出流,通过输出流将文件写回到浏览器,展示图片
//注意,这里的输出流是要向浏览器写回数据,所以要通过response来获取
ServletOutputStream outputStream = response.getOutputStream();
//因为返回的是图片,所以要在写回之前设置格式,
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes))!= -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭并释放资源
outputStream.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
1、新增菜品及DTO开发
DTO,data transform object,数据传输对象。
在新增菜品中,还会有分类以及口味信息,所以,一项功能中,将会操作多张表。
但是单单Dish的实体类对象的属性无法满足数据的接收。
这时再创建一个DishDto,包含所有的属性,继承于Dish,再进行操作。
- 请求路径
/dish
; - 请求携带的参数JSON格式的
除了dish的属性,还有category和flavor属性
,所以这里要使用dto接收。 - 在
dishService和impl中重写一个saveWithFlavor方法。
如下:
@Transactional //涉及多张表的操作,需要开启事务
public void saveWithFlavor(DishDto dishDto) {
//先存储dish的基本信息到菜品表dish,因为dishDto继承自dish,所以可以直接添加。
this.save(dishDto);
//获得菜品id
Long id = dishDto.getId();
//处理id信息,保存到集合中的每个dishFlavor的dishId属性
//使用流的lambda表达式,对flavors集合中的每个元素进行操作,操作完后又赋给它自己。
//也可以使用forEach方法。
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(id);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到dish_flavor表
dishFlavorService.saveBatch(flavors);
}
- 注意:涉及多张表的时候,记得加上事务
@Transactional
,在springboot启动类上添加事务管理的支持@EnableTransactionManagement
,最后在DishController中进行调用即可。
2、分页查询
- MP相关的分页插件,在做员工管理时已经做完了,整体方法于员工管理和分类管理差不多,但是,要注意这里要操作的表多,需要使用Dto对象。
- 创建两个Page对象:
Page<Dish> dishPage
和Page<DishDto> dishDtoPage
; - 进行条件查询后,将dishPage对象copy给dishDtoPage中,但是不用copy
records
属性,因为records属性中还缺少一个字段categoryName
,我们需要操作之后再加上。
注意:前端携带的参数要使用Dto接收,否则获得的数据不全,页面的分类名称无法获得
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
Page<Dish> pageInfo = new Page<>(page, pageSize);
//前端需要的信息中还有一个分类名称 dish中没有 dishDto中刚好有
Page<DishDto> dishDtoPage = new Page<>();
//添加过滤条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//添加模糊查询条件 并判断
queryWrapper.like(StringUtils.isNotEmpty(name), Dish::getName, name);
//查询
dishService.page(pageInfo, queryWrapper);
//对象拷贝,添加新的属性categoryName,忽略records属性,需要向其中添加categoryName
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);//将每一项copy给dishDto
Long categoryId = item.getCategoryId();//分类id
Category category = categoryService.getById(categoryId);//查询分类
if (category != null){//若不加判断,遇到没有分类的菜品,将会报空指针异常
String categoryName = category.getName();//分类名称
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());//将经过操作的dto对象收集并返回list
dishDtoPage.setRecords(list); //将最后得到的list设置给分页即可
return R.success(dishDtoPage);
}
3、启售、停售状态修改
-
确定请求路径为
/dish/status/status?ids=123123...,1231231...,1231...,
-
可将请求数据封装为ids数组,这样无论是单个修改还是批量都可以在一个方法中完成。
-
参数status,是路径参数,且其携带的值即是目标值,在方法中直接使用即可。
/** * 批量修改启售和停售状态。 * * @param ids 需要操作的id数组 * @param status 要修改的目标状态值 * @return */ @PostMapping("/status/{status}") public R<String> status(Long[] ids, @PathVariable int status) { for (Long id : ids) { Dish dish = dishService.getById(id); dish.setStatus(status); dishService.updateById(dish); } return R.success("修改状态成功"); }
4、删除和批量删除
-
与上面的思路相同。
/** * 删除和批量删除 * * @param ids * @return */ @DeleteMapping public R<String> delete(Long[] ids) { boolean bool = dishService.removeByIds(Arrays.asList(ids)); if (bool) { //进行一次判断是否删除成功。 return R.success("删除成功..."); } return R.error("删除失败..."); }
5、编辑菜品
此操作中分为两步:
第一步:需要根据id查询,将数据返回并回显。
第二步:点击确定修改之后,需要将封装的数据分别存储到不同的表中。
-
根据id查询数据并回显。
-
在后端接收的数据中,我们发现有些属性在Dish实体类中并没有,所有这里我们还是需要使用
DishDto
对象,它可以满足对这些数据的封装。 -
请求路径携带来的id为路径参数,要使用注解
@PathVariable
。 -
返回值的类型为
R<DishDto>
,先从dish表中直接查询基本信息,使用条件构造器从flavor表中查询数据取出,最后将两个数据都给dishDto。/** * 根据id查询 * 要将dish信息保存到dishDto中 才能满足所需要的信息 * @param id * @return */ @GetMapping("/{id}") public R<DishDto> getById(@PathVariable Long id) { //查询菜品的基本信息,从dish表查 Dish dish = dishService.getById(id); DishDto dishDto = new DishDto(); //条件构造器,从dish_flavor表查询口味信息 LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId,id); List<DishFlavor> dishFlavor = dishFlavorService.list(queryWrapper); //将基本信息copy给dishDto,再将口味信息给它 BeanUtils.copyProperties(dish,dishDto); dishDto.setFlavors(dishFlavor); return R.success(dishDto); }
-
-
修改/更新菜品信息
-
和上面的操作类似,也是要操作多张表。传回的数据封装为DishDto对象。
-
先更新dish表的基本信息,再将flover相关的信息更新到flavor表中。
-
先清除当前菜品对应的口味数据,再添加当前提交过来的口味。
/** * 修改菜品信息 * @param dishDto * @return */ @Transactional @PutMapping public R<String> update(@RequestBody DishDto dishDto){ //先更新dish表 dishService.updateById(dishDto); //清除当前菜品对应的口味数据 dish_flavor表 delete LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId,dishDto.getId()); dishFlavorService.remove(queryWrapper); //添加当前提交过来的口味 dish_flavor表 insert List<DishFlavor> flavors = dishDto.getFlavors(); flavors = flavors.stream().map(item->{ item.setDishId(dishDto.getId()); return item; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); return R.success("修改成功..."); }
-
六、套餐管理业务开发
0、准备工作
编写相关的domian、dao、service及其impl;
dao中要注解@Mapper
,ServiceImpl中要注解@Service和事务@Transactional
还需要dto->SetmealDto,注意公共字段的注解FIeldFill
编写SetmealController,加上正确路径以及注解。
@RestController @RequestMapping("/setmeal") public class SetmealController{...}
1、新增套餐
-
请求的路径为
"/setmeal"
,请求方式为Post
-
新增套餐界面,除了套餐的信息以外,还保存了套餐与菜品的关联信息,所以该操作其实是操作了两张表,需要添加事务。
-
页面带回的参数的数据要比Setmeal实体类多,所以需要SetmealDto类来接收数据,它继承于Setmeal。
-
我们在
SetmealServiceImpl
中重写一个方法saveWithDish
,并开启事务。@Autowired private SetmealDishService setmealDishService; /** * 新增套餐,同时需要保存套餐和菜品的关联关系 * @param setmealDto */ @Override public void saveWithDish(SetmealDto setmealDto) { //保存套餐的基本信息 操作setmeal表 insert操作 this.save(setmealDto); //将Dto中与套餐关联的菜品集合取出 并重新给它的套餐id赋值 List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes(); setmealDishes = setmealDishes.stream().map(item->{ item.setSetmealId(setmealDto.getId()); return item; }).collect(Collectors.toList()); //保存套餐与菜品的关联信息 操作setmeal_dish表 insert操作 setmealDishService.saveBatch(setmealDishes); }
-
在Controller中编写save请求方法,调用上面的方法即可:
@Autowired private SetmealService setmealService; @PostMapping public R<String> save(@RequestBody SetmealDto setmealDto){ //在service中重写的方法。 setmealService.saveWithDish(setmealDto); return R.success("新增套餐成功..."); }
2、分页查询
-
参考
DishController
中的分页查询。 -
注意:!!!
//将pageInfo的信息copy给dto的分页构造器,但是要排除records,因为其中还差一项categoryName BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
在此处,我想过视频上的内容可能会比较麻烦,难道不能全copy过去之后再添加字段吗?
所以我试着将records中的信息也copy进去,再将records中的每一条数据加上cetegoryName,最后失败了,这种逻辑会报一个错误,具体错误忘记了(好像是ClassCastException什么的)。应该是在一开始records中每一条数据都是Setmeal,强行copy进SetmealDto中,就会发生不兼容的异常。
总结:copy的时候将records忽略,单独取出,操作的单位缩小到Setmeal和SetmealDto之间,再进行copy因为是父子关系,所以不会报不兼容异常。上面尝试的方法是将封装了他们的records进行copy,就会报异常。如下:
//取出刚才排除的pageInfo的records List<Setmeal> records = pageInfo.getRecords(); //使用stream流遍历每一个item List<SetmealDto> list =records.stream().map(item->{ //新建Dto SetmealDto setmealDto = new SetmealDto(); //将item所有的信息copy进dto,还剩下categoryName需要设置进去 BeanUtils.copyProperties(item,setmealDto); //从item中获得categoryId,查询得到categoryName Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); //判断一下,避免报空指针异常 if (category!=null){ //将得到的categoryName赋给dto String categoryName = category.getName(); setmealDto.setCategoryName(categoryName); } //直接将dto返回,并收集为集合,用List<SetmealDto>接收 return setmealDto; }).collect(Collectors.toList()); //将经过处理的Dto集合重新设置给Dto的分页构造器 setmealDtoPage.setRecords(list); //返回给页面的是dto的分页构造器,这样菜能有categoryName。 return R.success(setmealDtoPage);
3、更改售卖状态及批量更改
-
较为简单,直接遍历页面传来的参数,设置status即可。
请求的路径为:/setmeal/status/0?ids=123123…,234234…;请求方式Post
经尝试,ids的接收,如果使用
Long[] ids
数组进行接收,不需要加注解,保证ids名字相同即可。若是使用List<Long>集合进行封装,需要加上注解@RequestParam
问号前面的0,即为想要修改的status值,直接进行操作,不用判断。
/** * 批量更改售卖状态 * 注意 @RequestParam List<Long> ids 可以更换为 Long[] ids * 若要封装为list集合 需要加注解,使用数组接收的话不需要加注解 * @param status * @param ids * @return */ @PostMapping("/status/{status}") public R<String> status(@PathVariable int status,@RequestParam List<Long> ids){ for (Long id : ids) { Setmeal setmeal = setmealService.getById(id); setmeal.setStatus(status); setmealService.updateById(setmeal); } return R.success("状态修改成功..."); }
4、删除和批量删除
- 该操作也是调用多张表的操作,我们移到service去重写方法,并开启事务。
- **注意:**删除套餐时,还需要删除其关联的菜品信息;
- 同时,只有停售状态的套餐才可以删除,是比较合理的。
- 先根据ids查询非停售状态的套餐有几个,若是大于0,则删除失败;
- 我们之前写了一个自定义的
CustomException
异常类,抛出一个异常即可。 - 若是非停售状态的个数为0,再执行删除操作,该操作分两个表:
- 先删除套餐表中的数据 setmeal表
- 再移除套餐与菜品之间的关系数据 seatmeal_dish表
- 再service中封装的方法如下:(事务已经加在了类注解上,不知道会不会有其他影响,以后会继续研究。)
/**
* 删除套餐,并相应的删除关联的菜品信息
* @param ids
*/
@Override
public void removeWithDish(List<Long> ids) {
//注意:删除的套餐的售卖状态要是停售才可以删除
//具体的sql应该是下面这样,判断删除id的状态是1的是不是0,大于0,则抛出一个自定的也无异常
//select count(*) from setmeal where id in (ids) and status = 1;
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
long count = this.count(queryWrapper);
//若不能删除,抛出一个业务异常
if (count>0){
throw new CustomException("该套餐为启售状态,无法删除。");
}
//若可以删除,先删除套餐表中的数据
this.removeBatchByIds(ids);
//再移除套餐与菜品之间的关系数据
LambdaQueryWrapper<SetmealDish> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(queryWrapper1);
}
-
在controller中我们直接调用即可:
/** * 删除和批量删除 套餐信息 * 但是注意要操作两张表 * 所以在service中重新写了一个方法 并要添加事务 * @param ids * @return */ @DeleteMapping public R<String> delete(@RequestParam List<Long> ids){ //调用重写的方法,内部需要移除套餐数据以及关联表中的关联数据 setmealService.removeWithDish(ids); return R.success("删除成功..."); }
**
未完待续… 奋力学习中…
**