作者: gh-xiaohe
gh-xiaohe的博客
觉得博主文章写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!
项目视频链接:黑马程序员,瑞吉外卖项目
数据表
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
*/
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// 登录功能
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
// 初始化密码
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 通过username进行对比
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
// 没有数据
if (emp == null) {
return R.error("用户名不正确");
}
// 密码不正确
if (!emp.getPassword().equals(password)) {
return R.error("密码不正确");
}
// 判断用户是否是禁用状态
if (emp.getStatus() == 0) {
return R.error("用户是禁用状态");
}
// 登录成功
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}
}
http://localhost:8080/backend/page/login/login.html
/**
* @author gh Email:@2495140780qq.com
* @Description
* @date 2022-07-05-下午 9:53
*/
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射 resources 下 不写static 和 template 无法访问页面 处理映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射");
// 使用 returnValueHandlers 来设置要映射哪些访问路径 访问路径映射到哪些资源文件
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// 退出功能
@RequestMapping("logout")
public R<String> logout(HttpServletRequest request) {
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
}
实现Filter
AntPathMatcher 路径匹配器,支持通配符
启动类上加入注解 @ServletComponentScan
/**
* 检查用户是否已经完成登录过滤器
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*") // 顾虑器的名称 和 拦截的路径
@Slf4j
public class LoginCheckFilter implements Filter{
// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 向下转型
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1、获取本次请求的URI
String requestURI = request.getRequestURI();//
log.info("拦截到请求:{}",requestURI);
// 定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
// 静态资源 /backend/index.html 需要使用 通配符的方式进行匹配上 需要使用AntPathMatcher对象
"/backend/**",
// 移动端页面 都是静态的 (页面也可以查看,数据不可以展示)
"/front/**"
};
// 2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
// 放行
filterChain.doFilter(request,response);
return;
}
// 4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
// 放行
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
// 5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
// backend/js/request.js 文件中响应拦截器 45行
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
// match 匹配
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;// 匹配成功
}
}
return false;
}
}
@Slf4j
@SpringBootApplication()
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
log.info("项目启动成功...");
}
}
初始化密码 DigestUtils 工具类
账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
使用异常处理器进行全局异常捕获 GlobalExceptionHandler
执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
/**
* 新增员工
*
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
// 设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
// 获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
// 创建人
employee.setCreateUser(empId);
// 更新人
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功");
}
/**
* 全局异常处理
*/
// ControllerAdvice 通知 拦截哪些Controller 拦截类上面加了这个两个注解的Controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
log.info(ex.getMessage()); // 输出异常信息
// 异常信息 java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan' for key 'employee.idx_username'
if (ex.getMessage().contains("Duplicate entry")) { // 异常信息是否包含 Duplicate entry (重复录入)
// 动态截取 重复信息
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上
页面中使用的是ElementUI提供的分页组件进行分页条的展示
页面中创建VUE对象完成后会调用init方法,在init方法中发送ajax请求并提交分页参数(page、pageSize、name),请求服务端Controller进行分页查询
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // MybatisPlus拦截器
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // addInnerInterceptor 添加内部拦截器 PaginationInnerInterceptor 页码内部拦截器
return mybatisPlusInterceptor;
}
}
/**
* 员工信息分页查询
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
// 构造分页构造器
Page pageInfo = new Page(page,pageSize);
// 构造条件构造器 name 可不能传递
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
// 添加过滤条件
// like 自己封装了一个 条件判断 如果不传递: 就不进行添加 name 值
// ==> Preparing: SELECT COUNT(*) FROM employee WHERE (name LIKE ?)
// ==> Parameters: %李四%(String)
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name); // 当name 不等于空时才会添加
// 添加排序条件 使用更新时间进行排序
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?
执行过程:
1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
页面中的ajax请求是如果发送的呢?
/**
* 修改员工信息
*
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
// 更新时间
employee.setUpdateTime(LocalDateTime.now());
// 更新人 --> 登录用户
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
// 分页 发送时 id 正确 但是 点击禁用时 id 传递错误 页面js的问题
// long的数据 19位 js对数字处理的时候 丢失的精度 js(只能保证前16位)
// 如何解决这个问题?我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
}
具体实现步骤:
1)提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2)在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到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 添加序列化器
.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)
// Long型数据处理时 使用 此 ToStringSerializer.instance 序列化器
// 最终结果 Long 转换成 String
.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);
}
}
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展mvc框架的消息转换器 项目启动时就会调用
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建消息转换器对象 作用: 将controller 的返回结果 转成 相应的 JSON 在通过输出流的方式 响应给页面
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到mvc框架的转换器集合中
// 索引 0 让自己转换器放在最前面 会优先使用自己的转换器
converters.add(0,messageConverter);
}
}
执行过程:
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
/**
* 根据id查询员工信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
使用Mybatis Plus提供的公共字段自动填充功能
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
1、在实体类的属性上加入@TableField注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
/**
* 员工信息
* @TableName employee
*/
@TableName(value ="employee")
@Data
public class Employee implements Serializable {
/**
* 创建时间
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建人
*/
@TableField(value = "create_user",fill = FieldFill.INSERT)
private Long createUser;
/**
* 修改人
*/
@TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
注意:当前我们设置createUser和updateUser为固定值,后面我们需要进行改造,改为动态获得当前登录用户的id
/**
* 自定义 元数据对象处理器
*
* 注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
*
* 可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now()); // 属性名 设置时间
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser", new Long(1)); // 当前登录用户的id
metaObject.setValue("updateUser",new Long(1));
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",new Long(1));
}
}
前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。
将用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
在使用ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
- LoginCheckFilter的doFilter方法
- EmployeeController的update方法
- MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码(获取当前线程id):
Long id = Thread.currentThread().getId(); log.info("线程id:{}",id)
通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
编写BaseContext工具类
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
// id 是Long类型
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
调用BaseContext来设置当前登录用户的id
/**
* 检查用户是否已经完成登录过滤器
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*") // 顾虑器的名称 和 拦截的路径
@Slf4j
public class LoginCheckFilter implements Filter{
// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
// 放行
filterChain.doFilter(request,response);
return;
}
}
调用BaseContext获取登录用户的id
/**
* 自定义 元数据对象处理器
*
* 注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
*
* 可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now()); // 属性名 设置时间
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId()); // 当前登录用户的id
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
/**
* 新增分类
*
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category){ // 传入的数据是JSON格式需要加入 @RequestBody注解
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
// 分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);
// 条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);
categoryService.removeById(id);
return R.success("分类信息删除成功");
}
实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
Dish表 菜品
Setmeal表 套餐
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
// 传递提示信息
public CustomException(String message){
super(message);
}
}
/**
* 全局异常处理
*/
// ControllerAdvice 通知 拦截哪些Controller 拦截类上面加了这个两个注解的Controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常自己定义的异常
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService{
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前需要进行判断
*
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if(count1 > 0){
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 > 0){
//已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);
//categoryService.removeById(id);
categoryService.remove(ids);
return R.success("分类信息删除成功");
}
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
method=“post” 采用post方式提交数据
enctype=“multipart/form-data” 采用multipart格式上传文件
type=“file” 使用input的file控件上传
举例:
commons-fileupload
commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath; // 获取配置文件application.yml 中的 reggie.path信息
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){ // 必须声明这个类 MultipartFile file 要和页面中的name="" 保持一致 Spring提供的
// file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
// 原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
// 把原始文件名称后面的 后缀截取过来
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
// 使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg
// 创建一个目录对象
File dir = new File(basePath);
// 判断当前目录是否存在 (传递路径可能是 D:/E/ed/abc) 目录不存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}
try {
// 将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath; // 获取配置文件application.yml 中的 reggie.path信息
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
// 输出流需要通过 HttpServletResponse 来响应
try {
// 输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
// 输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
// 设置响应回去的是什么类型的文件
response.setContentType("image/jpeg"); // 图片文件
int len = 0;
byte[] bytes = new byte[1024];
// 输入流来读 放到数组中去 =-1 表示读取完毕
while ((len = fileInputStream.read(bytes)) != -1){
// 通过输出流 向浏览器 写
outputStream.write(bytes,0,len);
// flush 刷新
outputStream.flush();
}
// 关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
dish 菜品表
dish_flavor 菜品口味表
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
// 条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
// 添加排序条件 优先使用 getSort 其次使用getUpdateTime
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
页面发送请求进行图片上传,请求服务端将图片保存到服务器,使用前面开发的CommonController 的upload 方法来处理即可。
页面发送请求进行图片下载,将上传的图片进行回显,使用前面开发的CommonController 的download 方法来处理即可。
多表操作,为了保证数据一致性,需要启用事务管理
@Transactional 事务控制
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper,Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
*
* @param dishDto
*/
@Transactional // 多张表操作 加入事务控制 主启动类 开始是事务支持
public void saveWithFlavor(DishDto dishDto) {
// 保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
// 菜品口味
List<DishFlavor> flavors = dishDto.getFlavors(); // dishDto.getFlavors() 没有装转好 dishId 需要单独处理
// stream 流的方式处理 集合
flavors = flavors.stream().map((item) -> { // 遍历出来的 item 就是 每个DishFlavor实体
item.setDishId(dishId);
return item;
}).collect(Collectors.toList()); // collect(Collectors.toList()); 又把数据重新转换成集合
// 保存菜品口味数据到菜品口味表dish_flavor
// dishFlavorService.saveBatch(dishDto.getFlavors()); 只封装 name 和 value 没有封装上 dishId(对应菜品的id),dishId需要单独处理
dishFlavorService.saveBatch(flavors);// saveBatch 批量保存
}
}
@Autowired
private DishService dishService;
/**
* 新增菜品
*
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){ // 参数用来接收前端的发送的数据 flavors特殊 (所以Dish 不可以)
// flavors 没有办法接收 方法: 封装类外一个类(DishDto) 在这个类中 把所有的参数都接收到
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
@Autowired
private CategoryService categoryService;
/**
* 菜品信息分页查询
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
// 构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize); // dish 只有 categoryId(菜品分类id) 想展示 分类名称(无法做到)
Page<DishDto> dishDtoPage = new Page<>();// 菜品分类的名称
// 条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
// 添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
// 执行分页查询
dishService.page(pageInfo,queryWrapper);// 经过查询之后数据就已经存在值 (需要赋值的只是categoryId )
// 对象拷贝 copyProperties 拷贝属性 pageInfo 从 拷贝到 dishDtoPage 不需要拷贝全部(除了 dishDtoPage 之外的属性)
// records 对应的是list结合就是页面上展示出来的列表数据 所承载的集合 这个地方需要进行处理
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords(); // 把 Dish 处理成 DishDto
List<DishDto> list = records.stream().map((item) -> { // item 表示 Dish 遍历出来的每一个 菜品对象
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);// 把 item 中的属性 拷贝到 dishDto中
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();// 分类的名称
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
/**
* 根据id查询菜品信息和对应的口味信息
*
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
// 查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
// 查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
/**
* 更新菜品信息,同时更新对应的口味信息
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
// 更新dish表基本信息
this.updateById(dishDto);
// 清理当前菜品对应口味数据---dish_flavor表的delete操作
// delete from dish_flavor where dish_id = ??? 清理口味数据
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());// 根据 DishFlavor中的 DishId 传入 DishId
dishFlavorService.remove(queryWrapper);
// 添加当前提交过来的口味数据---dish_flavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> { // dishId 没有封装 同 saveWithFlavor 新增 一样的处理方式
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
/**
* 根据id查询菜品信息和对应的口味信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
/**
* 修改菜品
*
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
/**
* 起售和停售
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable Integer status,Long ids){
log.info("status:{}",status);
log.info("ids:{}",ids);
Dish dish = dishService.getById(ids);
if (dish != null) {
dish.setStatus(status);
dishService.updateById(dish);
return R.success("开始起售");
}
return R.error("停售售卖");
}
对单个菜品的售卖状态的方法进行修改;
/**
* 批量起售和批量停售
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable Integer status, @RequestParam List<Long> ids) {
// @RequestParam 形参的参数名,请求参数的参数名不一致
// @RequestParam 是将请求参数和控制器方法的形参创建映射关系
log.info("status:{}", status);
log.info("ids:{}", ids);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids != null, Dish::getId, ids);
// 根据数据进行批量查询
List<Dish> list = dishService.list(queryWrapper);
for (Dish dish : list) {
if (dish != null) {
dish.setStatus(status);
dishService.updateById(dish);
}
}
return R.success("批量状态修改成功");
}
/**
* 是否删除
*/
@TableField(value = "is_deleted")
@TableLogic
private Integer isDeleted;
/**
* 套餐批量删除和单个删除
*
* @param ids
*/
@Override
@Transactional
public void removeWitchDishFlavor(List<Long> ids) {
//构造条件查询器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Dish::getCategoryId, ids);
queryWrapper.eq(Dish::getStatus, 1);
int count = this.count(queryWrapper);
if (count > 0 ) {
//如果不能删除,抛出一个业务异常
throw new CustomException("菜品正在售卖,不能删除");
}
// 删除菜品表中数据
this.removeByIds(ids);
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(DishFlavor::getDishId, ids);
dishFlavorService.remove(lambdaQueryWrapper);
}
/**
* 套餐批量删除和单个删除
*
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids) {
log.info("ids:{}", ids);
dishService.removeWitchDishFlavor(ids);
return R.success("菜品删除成功");
}
setmeal 套餐表
setmeal_dish 套餐菜品关系表
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
在CategoryController中提供方法,查询分类数据。注意:此方法在前面实现新增菜品时已经创建过了,此处直接调用即可。
/**
* 根据条件查询对应的菜品数据
*
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
直接使用我们前面开发的CommonController的upload方法来处理即可。
直接使用我们前面开发的CommonController的download方法来处理即可。
点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端。请求信息如下:
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
控制层
/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
业务层实现
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
}
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where id in (1,2,3) and status = 1
//查询套餐状态,确定是否可用删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if(count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据---setmeal
this.removeByIds(ids);
//delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
//删除关系表中的数据----setmeal_dish
setmealDishService.remove(lambdaQueryWrapper);
}
/**
* 套餐批量删除和单个删除
*
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids) {
log.info("ids:{}", ids);
dishService.removeWitchDishFlavor(ids);
return R.success("菜品删除成功");
}
SetmealServiceImpl
/**
* 根据条件查询对应的套餐数据
*
* @param id
*/
@Override
public SetmealDto getDate(Long id) {
// 根据id获取 setmeal 数据
Setmeal setmeal = this.getById(id);
SetmealDto setmealDto = new SetmealDto();
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(id != null, SetmealDish::getSetmealId,id);
if (setmeal!= null) {
// 拷贝数据
BeanUtils.copyProperties(setmeal, setmealDto);
// 根据套餐id查询数据
List<SetmealDish> list = setmealDishService.list(queryWrapper);
// 赋值
setmealDto.setSetmealDishes(list);
return setmealDto;
}
return null;
}
SetmealController
/**
* 根据条件查询对应的套餐数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> list(@PathVariable Long id){
log.info("id:{}",id);
SetmealDto setmealDto = setmealService.getDate(id);
return R.success(setmealDto);
}
SetmealServiceImpl
/**
* 更新套餐信息,同时更新对应的套餐的菜品
*
* @param setmealDto
*/
@Override
@Transactional
public void updateWithSetmeal(SetmealDto setmealDto) {
// 修改setmeal表中信息
this.updateById(setmealDto);
// 删除对应的套餐菜品
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
setmealDishService.remove(queryWrapper);
// 提交当前的套餐菜品信息
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map((item) ->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
SetmealController
/**
* 修改套餐数据
*
* @param setmealDto
* @return
*/
@PutMapping
public R<SetmealDto> update(@RequestBody SetmealDto setmealDto) {
log.info(setmealDto.toString());
setmealService.updateWithSetmeal(setmealDto);
return null;
}
/**
* 套餐批量起售和批量停售
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable Integer status, @RequestParam List<Long> ids) {
log.info("status:{}", status);
log.info("ids:{}", ids);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids != null, Setmeal::getId, ids);
// 根据数据进行查询
List<Setmeal> list = setmealService.list(queryWrapper);
for (Setmeal setmeal : list) {
if (setmeal != null) {
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
}
return R.success("批量修改状态成功");
}