需求分析:产品原型、需求规格说明书
设计:产品文档、UI界面设计、概要设计、详细设计、数据库设计
编码:项目代码、单元测试
测试:测试用例、测试报告
上线运维:软件环境安装、配置
项目经理:对整个项目负责,任务分配、把控进度
产品经理:进行需求调研,输出需求调研文档、产品原型等
UI设计师:根据产品原型输出界面效果图
架构师:项目整体架构设计、技术选型等
开发工程师:代码实现
测试工程师:编写测试用例,输出测试报告
运维工程师:软件环境搭建、项目上线
序号 | 表名 | 说明 |
---|---|---|
1 | employee | 员工表 |
2 | category | 菜品和套餐分类表 |
3 | dish | 菜品表 |
4 | setmeal | 套餐表 |
5 | setmeal_dish | 套餐菜品关系表 |
6 | dish_flavor | 菜品口味关系表 |
7 | user | 用户表(C端) |
8 | address_book | 地址簿表 |
9 | shopping_cart | 购物车表 |
10 | orders | 订单表 |
11 | order_detail | 订单明细表 |
package com.jihua.reggie.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
*
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射 ...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 员工实体类
*/
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;//身份证号码
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
package com.jihua.reggie.controller;
import com.jihua.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Employee;
public interface EmployeeService extends IService<Employee> {
}
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.Employee;
import com.jihua.reggie.mapper.EmployeeMapper;
import com.jihua.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.Employee;
public interface EmployeeMapper extends BaseMapper<Employee> {
}
package com.jihua.reggie.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
*
* @param
*/
@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;
}
}
package com.jihua.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jihua.reggie.common.R;
import com.jihua.reggie.entity.Employee;
import com.jihua.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工登录
*
* @param request
* @param employee
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
//1. 将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2. 根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
//3. 如果没有查询到则返回登录失败结果
if (emp == null) {
return R.error("用户不存在");
}
//4. 密码比对,如果不一致则返回登录失败结果
if (!emp.getPassword().equals(password)) {
return R.error("密码错误");
}
//5. 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if (emp.getStatus() == 0) {
return R.error("账号已禁用");
}
//6. 登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}
}
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:
清理Session中的用户id
返回结果
/**
* 员工退出
* @param request
* @return
*/
@RequestMapping("/logout")
public R<String> logout(HttpServletRequest request) {
//1. 清理Session中的当前登录员工id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
实现步骤:
package com.jihua.reggie.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检查用户是否已经登录
*/
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到请求:{}", request.getRequestURI());
filterChain.doFilter(request, response);
}
}
@Slf4j//配置log
@SpringBootApplication
@ServletComponentScan//开启filter注解扫描
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
log.info("项目启动成功...");
}
}
/**
* 检查用户是否已经登录
*/
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher ANT_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();
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2. 判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3. 如果不需要处理,则直接放行
if (check) {
filterChain.doFilter(request, response);
return;
}
//4. 判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
filterChain.doFilter(request, response);
return;
}
//5. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
log.info("拦截到请求:{}", request.getRequestURI());
}
/**
* 路径匹配,检查本次请求是否需要放行
*
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = ANT_PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。
在开发代码之前,需要梳理一下整个程序的执行过程:
/**
* 新增员工
*
* @param request
* @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(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
*
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
//用户名重复异常
if (ex.getMessage().contains("Duplicate")) {
String[] s = ex.getMessage().split(" ");
String msg = "用户" + s[2] + "已存在";
return R.error(msg);
}
log.error(ex.getMessage());
return R.error("未知错误");
}
}
在开发代码之前,需要梳理一下整个程序的执行过程:
/**
* 分页查询(包括搜索)
*
* @param page 当前页面
* @param pageSize 每页大小
* @param name 搜索参数
* @return 附带Page的成功消息
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}", page, pageSize, name);
//制作分页查询构造器
Page<Employee> pageInfo = new Page<Employee>(page, pageSize);
//制作条件查询构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<Employee>();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
在开发代码之前,需要梳理一下整个程序的执行过程:
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作
在Controller中创建update方法,此方法是一个通用的修改员工信息的方法
/**
* 根据id修改员工信息
* @param request
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
log.info(employee.toString());
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser((Long) request.getSession().getAttribute("employee"));
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
具体实现步骤:
package com.jihua.reggie.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于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)
.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);
}
}
/**
* 扩展mvc的消息转换器
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建对象转换器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中(并设置优先级)
converters.add(0, messageConverter);
}
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
/**
* 根据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("没有查询到对应员工信息");
}
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser((Long) request.getSession().getAttribute("employee"));
能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答案就是使用Mybatis Plus提供的公共字段自动填充功能。
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private Long updateUser;
package com.jihua.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
*
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert] ...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", 1L);
metaObject.setValue("updateUser", 1L);
log.info(metaObject.toString());
}
/**
* 更新操作,自动填充
*
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update] ...");
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", 1L);
log.info(metaObject.toString());
}
}
注意,我们在MyMetaObjectHander类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
在使用ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
可以在上面的三个方法中分别加入下面代码(获取当前线程id)
long id = Thread.currentThread().getId() ;
log.info("线程id: {0}" ,id);
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
实现步骤:
编写BaseContext工具类,基于ThreadLocal封装的工具类
package com.jihua.reggie.common;
/**
* 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
/**
* 设置id
*
* @param id 存放当前登录用户的id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取id
*
* @return 返回当前登录用户的id
*/
public static Long getCurrentId() {
return threadLocal.get();
}
}
在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
//获取当前登录用户的id
Long userId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(userId);
在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id
metaObject.setValue("updateUser", BaseContext.getCurrentId());
后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:
需要注意,category表中对name字段加入了唯一约束,保证分类的名称是唯一的
在开发业务功能前,先将需要用到的类和接口基本结构创建好
实体类Category
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
private Integer sort;
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
Mapper接口CategoryMapper
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
业务层接口CategoryService
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Category;
public interface CategoryService extends IService<Category> {
}
业务层实现类CategoryServicelmpl
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.Category;
import com.jihua.reggie.mapper.CategoryMapper;
import com.jihua.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
控制层CategoryController
package com.jihua.reggie.controller;
import com.jihua.reggie.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 分类管理
*/
@RestController
@RequestMapping("/Category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
}
在开发代码之前,需要梳理一下整个程序的执行过程:
新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,使用type字段区分,所以服务端只需要提供一个方法统—处理即可
在开发代码之前,需要梳理一下整个程序的执行过程:
/**
* 分页查询
*
* @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(ids);
return R.success("分类信息删除成功");
}
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
实体类Dish和Setmeal
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 菜品
*/
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//商品码
private String code;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//顺序
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//分类id
private Long categoryId;
//套餐名称
private String name;
//套餐价格
private BigDecimal price;
//状态 0:停用 1:启用
private Integer status;
//编码
private String code;
//描述信息
private String description;
//图片
private String image;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
Mapper接口DishMapper和SetmealMapper
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}
Service接口DishService和SetmealService
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Dish;
public interface DishService extends IService<Dish> {
}
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Setmeal;
public interface SetmealService extends IService<Setmeal> {
}
Service实现类DishServicelmpl和SetmealServicelmpl
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.Dish;
import com.jihua.reggie.mapper.DishMapper;
import com.jihua.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.Setmeal;
import com.jihua.reggie.mapper.SetmealMapper;
import com.jihua.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
}
package com.jihua.reggie.common;
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Category;
public interface CategoryService extends IService<Category> {
/**
* 根据id删除分类,并进行删除之前的判断
*
* @param id
*/
public void remove(Long id);
}
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.common.CustomException;
import com.jihua.reggie.entity.Category;
import com.jihua.reggie.entity.Dish;
import com.jihua.reggie.entity.Setmeal;
import com.jihua.reggie.mapper.CategoryMapper;
import com.jihua.reggie.service.CategoryService;
import com.jihua.reggie.service.DishService;
import com.jihua.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@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);
}
}
/**
* 异常处理方法
*
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category) {
log.info("修改分类信息:{}", category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的form表单有如下要求:
method=“post” 采用post方式提交数据
enctype=“multipart/form-data” 采用multipart格式上传文件
type=“file” 使用input的file控件上传
举例:
<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file" />
<input type="submit" value="提交"/>
form>
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。例如ElementUI中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
/**
* 文件上传
* @param file
* @return
*/
@PostMapping(value = "upload")
public R<String> upload(MultipartFile file) {
System.out.println(file);
return R.success("文件上传成功");
}
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
文件上传,页面端可以使用ElementUI提供的上传组件。
<el-upload class="avatar-uploader"
action="/common/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar">img>
<i v-else class="el-icon-plus avatar-uploader-icon">i>
el-upload>
后端代码实现
/**
* 文件上传
*
* @param file
* @return
*/
@PostMapping(value = "/upload")
public R<String> upload(MultipartFile file) {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删陈
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
//获得文件后缀名称
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//xxxxxxxx.jpg
//创建一个目录对象
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);
}
文件下载,页面端可以使用标签展示下载的图片
<img v-if="imageUrl" : src="imageUrl" class="avatar">img>
handleAvatarSuccess (response,file,fileList){
this.imageUrl ='/common/download?name=${response.data}'
}
/**
* 文件下载
*
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
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];
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并日需要卜传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类DishFlavor(Dish实体前面已经导入过了)
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 菜品口味
*/
@Data
public class DishFlavor implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品id
private Long dishId;
//口味名称
private String name;
//口味数据list
private String value;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
Mapper接口DishFlavorMapper
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
业务层接口DishFlavorService
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.DishFlavor;
public interface DishFlavorService extends IService<DishFlavor> {
}
业务层实现类DishFlavorServicelmpl
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.DishFlavor;
import com.jihua.reggie.mapper.DishFlavorMapper;
import com.jihua.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}
控制层DishController
package com.jihua.reggie.controller;
import com.jihua.reggie.service.DishFlavorService;
import com.jihua.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
}
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
//在CategoryController中书写
/**
* 根据条件查询分类数据
*
* @param type 区分菜品分类(1)和套餐(2)分类
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(String type) {
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(type != null, Category::getType, type);
//添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
页面发送请求进行图片上传,请求服务端将图片保存到服务器
页面发送请求进行图片下载,将上传的图片进行回显
点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
DTO,全称为Data Transfer object,即数据传输对象,一般用于展示层与服务层之间的数据传输.
前面的内容前端传过来的数据可以直接用实体类来接收,如:
@PutMapping
public R<String> update(@RequestBody Category category)
当前端传送的数据不能直接接受时,就要编写DTO,例如此处还需要接收flavors数组:
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
}
/**
* 新增菜品
*
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {
log.info("新增菜品:{}", dishDto);
dishService.saveWithFlavor(dishDto);
return R.success("菜品新增成功");
}
此处的saveWithFlavor是自己编写的同时保存菜品,同时保存对应的口味数据选择两个数据的功能
public interface DishService extends IService<Dish> {
/**
* 新增菜品,同时保存对应的口味数据
*
* @param dishDto
*/
public void saveWithFlavor(DishDto dishDto);
}
/**
* 新增菜品,同时保存对应的口味数据
*
* @param dishDto
*/
@Override
@Transactional//多表操作,需要开启事务
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
flavors.forEach(flavor -> flavor.setDishId(dishId));
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
因为同时操作了多张表,需要进行事务处理,在启动类上开启事务注解支持注解
@Slf4j//配置log
@SpringBootApplication
@ServletComponentScan//开启filter注解扫描
@EnableTransactionManagement//开启事务注解支持
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
log.info("项目启动成功...");
}
}
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
Page
类型的数据没有菜品分类的具体名称,需要在后端重新封装一个Page
,将/**
* 菜品信息分页查询
*
* @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);
Page<DishDto> pageDto = new Page<>(page, pageSize);
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null, Dish::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo, queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo, pageDto, "records");//忽略records,因为records需要进行处理
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(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());
pageDto.setRecords(list);
return R.success(pageDto);
}
在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
/**
* 根据id查询菜品信息和对应的口味信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id) {
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
此处的getByIdWithFlavor是自己编写的方法:
/**
* 根据id查询菜品信息和对应的口味信息
*
* @param id
*/
public DishDto getByIdWithFlavor(Long id);
/**
* 根据id查询菜品信息和对应的口味信息
*
* @param id
*/
@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;
}
/**
* 根据id修改菜品信息
*
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
dishService.updateWithFlavor(dishDto);
log.info("修改菜品信息:{}", dishDto);
return R.success("菜品信息修改成功");
}
此处的updateWithFlavor是自己编写的方法:
/**
* 更新菜品信息,同时更新对应的口味信息
*
* @param dishDto
*/
public void updateWithFlavor(DishDto dishDto);
/**
* 更新菜品信息,同时更新对应的口味信息
*
* @param dishDto
*/
@Override
@Transactional//事务
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.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.forEach(flavor -> flavor.setDishId(dishDto.getId()));
dishFlavorService.saveBatch(flavors);
}
删除菜品时需要同时删除菜品信息、口味信息以及菜品图片
/**
* 根据id删除菜品
*
* @param arrayIDs
* @return
*/
@DeleteMapping()
public R<String> delete(@RequestParam("ids") Long[] arrayIDs) {
List<Long> ids = Arrays.asList(arrayIDs);
log.info("删除菜品id:{}", ids);
dishService.deleteWithFlavor(ids);
return R.success("菜品信息删除成功");
}
其中的deleteWithFlavor时自己写的方法
/**
* 删除菜品信息,同时删除对应的口味信息和图片
*
* @param ids
*/
public void deleteWithFlavor(List<Long> ids);
@Value("${reggie.path}")
private String basePath;
/**
* 删除菜品信息,同时删除对应的口味信息和图片
*
* @param ids
*/
@Override
@Transactional//事务
public void deleteWithFlavor(List<Long> ids) {
//先查询图片名称,否则菜品信息删除后无法查询到图片名称
LambdaQueryWrapper<Dish> fileQueryWrapper = new LambdaQueryWrapper<>();
fileQueryWrapper.eq(Dish::getId, ids);
List<Dish> dishList = this.listByIds(ids);
//根据id删除菜品信息
this.removeByIds(ids);
//清理当前菜品对应口味数据——dish_flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId, ids);
dishFlavorService.remove(queryWrapper);
//删除菜品图片
for (Dish dish : dishList) {
File pictureFile = new File(basePath + dish.getImage());
boolean delete = pictureFile.delete();
if (delete) {
log.info("图片{}删除成功", dish.getImage());
} else {
log.info("图片{}删除失败", dish.getImage());
}
}
}
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
在开发业务功能前,先将需要用到的类和接口基本结构创建好︰
实体类SetmealDish(Setmeal实体前面已经导入过了)
package com.jihua.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐id
private Long setmealId;
//菜品id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
DTO SetmealDto
package com.jihua.reggie.dto;
import com.jihua.reggie.entity.Setmeal;
import com.jihua.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
Mapper接口SetmealDishMapper
package com.jihua.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
业务层接口SetmealDishService
package com.jihua.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.SetmealDish;
public interface SetmealDishService extends IService<SetmealDish> {
}
业务层实现类SetmealDishServicelmpl
package com.jihua.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.SetmealDish;
import com.jihua.reggie.mapper.SetmealDishMapper;
import com.jihua.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}
控制层SetmealController
package com.jihua.reggie.controller;
import com.jihua.reggie.service.SetmealDishService;
import com.jihua.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
}
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(前面已完成,和菜品分类共用)
页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(菜品分类前面已完成)
页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中(其实就是根据分类条件和搜索条件进行菜品的条件查询,所以写在DishController中)
/**
* 根据条件查询对应的菜品数据
*
* @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() != null, Dish::getStatus, dish.getStatus());
//添加name搜索条件
queryWrapper.like(dish.getName() != null, Dish::getName, dish.getName());
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
页面发送请求进行图片上传,请求服务端将图片保存到服务器
页面发送请求进行图片下载,将上传的图片进行回显
点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐
*
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto) {
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
此处的saveWithDish是自己写的方法
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
*
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
*
* @param setmealDto
*/
@Override
@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);
}
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
/**
* 分页查询套餐信息
* @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.orderByAsc(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);
}
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
/**
* 删除套餐
*
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
setmealService.deleteWithDish(ids);
return R.success("套餐数据删除成功");
}
此处的deleteWithDish是自己写的方法
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据,以及套餐图片
*
* @param ids
*/
public void deleteWithDish(List<Long> ids);
@Value("${reggie.path}")
private String basePath;
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据,以及套餐图片
*
* @param ids
*/
@Override
@Transactional//事务
public void deleteWithDish(List<Long> ids) {
//查询套餐状态,确定是否可用删陈
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids != null, Setmeal::getId, ids);
queryWrapper.eq(Setmeal::getStatus, 1);
int count = this.count(queryWrapper);
//如果不能删除,抛出一个业务异常
if (count > 0) {
throw new CustomException("套餐正在售卖中,不能删除");
}
//先查询图片名称,否则套餐信息删除后无法查询到图片名称
LambdaQueryWrapper<Setmeal> fileQueryWrapper = new LambdaQueryWrapper<>();
fileQueryWrapper.eq(Setmeal::getId, ids);
List<Setmeal> setmealList = this.listByIds(ids);
//如果可以删除,先删除套餐表中的数据——setmeal
this.removeByIds(ids);
//删除关系表中的数据——setmeal_dish
LambdaQueryWrapper<SetmealDish> dishQueryWrapper = new LambdaQueryWrapper<>();
dishQueryWrapper.in(SetmealDish::getSetmealId, ids);
setmealDishService.remove(dishQueryWrapper);
//删除套餐图片
for (Setmeal setmea : setmealList) {
File pictureFile = new File(basePath + setmea.getImage());
boolean delete = pictureFile.delete();
if (delete) {
log.info("图片{}删除成功", setmea.getImage());
} else {
log.info("图片{}删除失败", setmea.getImage());
}
}
}