基于springboot+mybatis plus开发核心技术的Java项目,包括系统管理后台和移动端应用两个部分,其中管理后台 部分提供给内部人员使用,可以对菜品、套餐、订单等进行管理以及维护;移动端主要提供给消费者使用,实现了 在线浏览商品、添加购物车、下单等业务。用户层面采用的技术栈为H5、Vue等,网关层采用Nginx,应用层采用 SpringBoot、SpringMVC等技术栈,数据层使用MySQL以及Redis。

前端要求:后端需要返回code、data、msg三个参数;

前端发起的请求:

前端发起请求后携带的参数:

登录功能对应的数据库表中的员工表,所以需要针对员工表进行一系列架构,例如实体类,mapper,service,controller:
Mapper:
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
Service:
public interface IEmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
}
Controller:
@RestController
@RequestMapping("/emploee")
public class EmployeeController {
@Autowired
public IEmployeeService employeeService;
}
并且在经过上面步骤之后,我们需要一个通过结果类,因为我们会编写非常多的Controller,我们应该将返回的结果进行统一!也就是将服务端响应后返回到页面的数据都应该被封装为一个统一的对象!代码如下:
@Data
public class ResultBean<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> ResultBean<T> success(T object) {
ResultBean<T> result = new ResultBean<>();
result.data = object;
result.code = 1;
return result;
}
public static <T> ResultBean<T> error(String msg) {
ResultBean result = new ResultBean();
result.msg = msg;
result.code = 0;
return result;
}
public ResultBean<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
具体业务实现代码:
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
public IEmployeeService employeeService;
/**
* 员工登录
* @param request 用于获取用户的session
* @param employee
* @return
*/
@PostMapping("/login")
public ResultBean login(HttpServletRequest request, @RequestBody Employee employee) {
// 将页面传来的数据,也就是账号与密码,将密码进行md5加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 根据用户名查询用户
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername, employee.getUsername()); // eq:等值查询
Employee emp = employeeService.getOne(lqw);
// 判断是否查询到用户
if (emp == null) {
return ResultBean.error("用户名错误");
}
// 判断密码是否匹配
if ( ! password.equals(emp.getPassword())) {
return ResultBean.error("密码错误");
}
// 查询账号是否处于封禁状态
// 在数据库中,员工表具有一个status字段,代表着员工的封禁状态,如果status=1,则代表可登录状态,如果为0,则代表该用户不可登录
if (emp.getStatus().equals(0)) {
return ResultBean.error("账号已禁用");
}
// 登录成功,将用户id放入session中
request.getSession().setAttribute("employee", emp.getId());
return ResultBean.success(emp);
}
}
当前情况下,用户是可以直接通过访问主页面来跳过登录页面的,所以我们需要对该问题进行一些优化,让未登录的用户必须登录之后,才能访问主页面!
实现方案:过滤器或拦截器,这里使用过滤器
实现步骤:
1、创建一个自定义的过滤器
2、为过滤器增加@WebFilter注解,并在注解中配置过滤器的名称以及需要拦截的路径(这里选择拦截所有路径/*)
3、过滤器继承Filter类
4、在启动类上增加注解@ServletComponentScan
5、完善过滤器逻辑

@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
private 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;
// 获得本次请求的uri
String uri = request.getRequestURI();
// 定义并不需要拦截的路径(登录、退出、静态资源)
String[] urls = new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 判断本次路径是否需要进行处理(需要用到AntPathMatcher对象)
boolean checkUrl = checkUrl(urls, uri);
if (checkUrl) {
log.info("本次请求{}不需要处理" + uri);
filterChain.doFilter(request, response);
return;
}
// 如果已经是登录状态,则放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户{}已登录" + request.getSession().getAttribute("employee"));
return;
}
// 如果是未登录状态,则返回未登录的结果,并通过输出流的方式向客户端响应数据
// 在前端界面中,响应数据中含msg=NOTLOGIN会进行页面的跳转
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(ResultBean.error("NOTLOGIN")));
return;
}
private boolean checkUrl(String[] urls, String requestUri) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestUri);
if (match) {
return true;
}
}
return false;
}
}
前端跳转的拦截器reques.js:当响应返回一个“NOTLOGIN”字符串时,进行页面跳转

点击退出按钮,将会发起一个退出登录的请求logout:

/**
* 员工退出登录
* @param request
* @return
*/
@PostMapping("logout")
public ResultBean logout(HttpServletRequest request) {
request.getSession().removeAttribute("employee");
return ResultBean.success("退出成功");
}
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public ResultBean<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 ResultBean.success("新增员工成功");
}
由于数据库中,员工的账号是具有唯一约束的,所以当新增的员工账号与数据库中已有的数据冲突时,会报异常(SQLIntegrityConstraintViolationException),异常信息为:
:::success
Duplicate entry ‘xxx’ for key ‘idx_username’
:::
意思为username该字段具有唯一约束,不可以存在有重复的值!
我们需要对异常进行处理。
创建一个全局异常类,用于捕获异常:
@ControllerAdvice:参数为需要处理异常的类,例如此时参数为RestController.class,那么加了@RestController注解的类抛出异常时会被捕捉。
@ExceptionHandler:捕获指定的异常
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalException {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public ResultBean exceptionHandle(SQLIntegrityConstraintViolationException e) {
log.info("捕获到该异常" + e.getMessage());
// 账号名重复:Duplicate entry 'test' for key 'employee.idx_username'
if (e.getMessage().contains("Duplicate entry")) {
// 空格分割异常信息,并放入字符串数组中
String[] split = e.getMessage().split(" ");
String msg = split[2] + "已存在!";
return ResultBean.error(msg);
}
return ResultBean.error("未知错误!");
}
}

当进入该页面,也就是员工管理页面时,会自动发起一个请求,而我们则需要对该请求进行处理,编写对应的Controller,不过在此之前,我们需要引用MybatisPlus的分页插件,该分页插件可以很好地帮我们对数据进行分页,这个请求在前端中是一个getMemberList方法发起的,可以看到在该方法中,后端提交给前端的数据应该要有records、total这些属性,**正好在MP中,就有一个具有这些属性的分页对象Page!**所以我们的Controller可以将Page对象作为返回的数据!

@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
对于Controller的编写,也颇有讲究,我们需要接受前端发过来的参数,那么前端发过来哪些参数呢?
当我们在页面的搜索框内输入内容,例如“123”,页面会发起一个请求,这个请求一共携带了三个参数,分别是page(当前页数),pageSize(一页所展示的数据量),name(搜索的关键字),所以我们在Controller也需要接收这三个参数!

/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public ResultBean<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}" + page, pageSize, name);
// 构造分页选择器
Page pageInfo = new Page(page, pageSize);
// 构造条件选择器
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper();
// 模糊查询
lqw.like(StringUtils.isNotEmpty(name), Employee::getName, name);
// 排序
lqw.orderByDesc(Employee::getUpdateTime);
// 执行查询
employeeService.page(pageInfo, lqw);
return ResultBean.success(pageInfo);
}
管理员admin登录后台系统之后,可以对员工账号的状态进行更改,也就是启用账号或者是禁用账号!

普通员工登录后台系统之后,并不能对账号的状态进行更改:

首先,分析一下为什么管理员admin会有员工状态更改选项:
1、在前段页面中,有这样一段代码,这是一个生命周期函数,在页面启动时就会执行,这个init方法会拿到本地存储中的userInfo,也就是当前登录用户,并且拿到user的username属性!

2、在下面所示的代码中,这里是用于展示操作选项的,可以看到v-if对user进行了值的判断,如果user为admin,才会显示状态更改的操作栏,而且在这里也对状态码进行了动态判断,如果用户已经被封禁了,那么在状态更改的操作选项中显示的应该是“启用”!

分析完毕,接下来分析页面请求,当我们点击操作,也就是启用/禁用时,页面会发起一个请求,需要注意的是,这个请求方式是PUT:

这个时候你可能已经自信满满写好了Controller,但是运行之后你会发现,程序可以正常运行,但是数据库并没有更新,前后分析一通,发现我们在page方法中传给页面的数据是正确的,但是我们点击更改用户状态,也就是“禁用”时,页面回传给我们的参数(用户id)却发生了差错!这是因为js对long类型的数据进行处理时会丢失精度,导致提交的用户id和数据库中的id并不一致!所以我们需要进行优化,也就是给页面响应json数据时进行处理,将long类型的数据转为String字符串!
具体实现步骤:
1、提供对象转换器JacksonObjectMapper,基于Jackson进行java对象到json数据的转换
2、在WebMvcConfig中配置类中扩展SpringMVC的转换器,在此消息转换器中使用提供的对象转换器来进行java对象到json数据的转换
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON
* 这个对象转换器不仅提供了Long类型到String类型的转换,也提供了一些日期类型的转换
*/
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);
}
}
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展Mvc的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建一个新的消息转化器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置消息转换器
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将消息转换器追加到Mvc的转换器集合中,index表示优先级
converters.add(0, messageConverter);
}
}
/**
* 更改用户状态
* @param request
* @param employee 网页已经传入的参数中已经包含用户id和状态码了
* @return
*/
@PutMapping
public ResultBean<String> update(HttpServletRequest request, @RequestBody Employee employee) {
log.info("id=" + employee.getId());
// 获取当前登录用户
Long userID = (Long) request.getSession().getAttribute("employee");
// 更改参数
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(userID);
// 更新用户
employeeService.updateById(employee);
return ResultBean.success("修改成功");
}
前端请求的路径如下所示,可以看到这里是路径携带的参数,想要获得该情况下的参数,Controller也会有所不同!不过这里的请求仅仅是前端点击修改选项时,可以展现修改用户的当前信息,并不是直接的修改操作,需要注意!

@GetMapping("/{id}")
public ResultBean<Employee> update(@PathVariable Long id) {
Employee employee = employeeService.getById(id);
return ResultBean.success(employee);
}
在前端中,修改员工信息的页面请求和我们在启用/禁用员工功能的路径是一样的,所以直接的修改操作会直接通过上述功能的Controller去实现!
@Data
public class Employee implements Serializable {
......
@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;
}
@Component
public class MyMetaObjectHandle implements MetaObjectHandler {
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("meta" + metaObject);
}
/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("meta" + metaObject);
}
}
按照以上的代码步骤去编写公共字段自动填充(MP),但这样会存在一个问题,我们虽然已经可以为这个公共字段填充数据了,但如果我们此时需要填充的数据,是在网页session中存放的数据,该怎么去拿取到这个数据呢?
你可能会想到,我们是数据存放到HttpSession中,那么我们只需要再从HttpSession中获取数据就可以了,但事实上,我们并不能在自定义数据对象处理器的类中获取到HttpSession对象!所以也不能拿到session中存放的数据!
所以我们需要使用到一个ThreadLocal来解决这个问题,它是JDK提供的一个类!我们可以通过类名就可以判断,这是一个有关于线程的类!
我们可以分析到目前为止,页面发起请求所需要经过的类,例如此时页面发起update请求:
1、经过过滤器LoginCheckFilter;
2、经过Controller;
3、经过MyMetaObjectHandle自定义元数据处理器(公共字段自动填充处理类)
而以上三个步骤,他们是同一个线程的!你完全可以在这三个类中通过加入获取当前线程id的方法来测试这三个类所处的线程是否一致!答案肯定是true!
这个时候,你可能就会想起一个概念,当客户端每次发起一个http请求,在对应的服务端都会分配一个新的线程来处理!
什么是ThreadLocal?
ThreadLocal并不是一个Thread,面是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocat为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立改变自己的副本,而不会影响到其他线程所对应的副本!
ThreadLoc单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到的值,线程外则了
能访问。
ThreadLocal常用方法:
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id) 然后在MVMetaoObiectHandlerenupdateFil[方法中调用ThreadLocal法来获得当前线程所对应的线程局部变量的值(用户id)。
1、编写基于ThreadLocal的工具类;
/**
* 基于ThreadLocal的工具类
*/
public class BaseContext {
// 获取公用字段员工id的值
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentID(Long id) {
threadLocal.set(id);
}
public static Long getCurrentID() {
return threadLocal.get();
}
}
2、在LoginCheckFilter的doFilter方法中调用工具类获取当前登录用户id;
// 如果已经是登录状态,则放行
if (request.getSession().getAttribute("employee") != null) {
Long employeeID = (Long) request.getSession().getAttribute("employee");
log.info("用户{}已登录" + employeeID);
// 放行
BaseContext.setCurrentID(employeeID);
filterChain.doFilter(request,response);
return;
}
3、在MyMetaObjectHandle方法中调用工具类获取当前登录用户id;
@Component
public class MyMetaObjectHandle implements MetaObjectHandler {
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentID());
metaObject.setValue("updateUser", BaseContext.getCurrentID());
}
/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentID());
}
}
涉及的表结构

@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
ICategoryService categoryService;
/**
* 添加分类(添加菜品或者套餐)
* @param category
* @return
*/
@PostMapping
public ResultBean<String> add(@RequestBody Category category) {
log.info( "msg" + category);
categoryService.save(category);
return ResultBean.success("新增成功!");
}
}
/**
* 分类页面分页
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public ResultBean<Page> page(int page, int pageSize, String name) {
// 构造分页选择器
Page pageInfo = new Page(page, pageSize);
// 构造条件选择器
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper();
lqw.orderByAsc(Category::getSort);
// 执行查询
categoryService.page(pageInfo, lqw);;
return ResultBean.success(pageInfo);
}
删除分类的Controller很简单,但是这里提出一个问题,如果删除数据的表中存在外键,该怎么办?
例如此时我们页面展示的分类有菜品分类和套餐类型两种,其中,菜品分类有湘菜、粤菜等,而湘菜(id)下面关联了一些菜品(category_id),例如辣子鸡、麻婆豆腐等,如果我们直接删除湘菜这个分类,但是这个湘菜却关联着菜品,我们并不能让这些菜品突然就变成野菜了,这并不符合逻辑。于是我们希望,当删除某个分类时,如果分类下存在有菜品关联,那么提示员工该分类“无法删除”!
具体步骤:
1、首先将菜品分类和套餐类型的Mapper、Service、pojo补充完整;
2、在Category的Service中,重写remove方法,在remove方法中,我们会对菜品分类和套餐类型下面是否关联有菜品进行判断,如果存在菜品,那么就会抛出一个异常;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {
@Autowired
IDishService dishService;
@Autowired
ISetmealService setmealService;
/**
* 根据id删除分类(菜品或套餐)
* 因为删除某种套餐时,可能下面关联着某些菜品,所以需要对删除操作增加一个判断
* @param id
*/
@Override
public void remove(Long id) {
// 查询当前分类(菜品分类)是否关联了菜品,如果关联了,抛出一个业务异常
LambdaQueryWrapper<Dish> dishLqw = new LambdaQueryWrapper();
dishLqw.eq(Dish::getCategoryId, id);
int dishCount = dishService.count(dishLqw);
if(dishCount > 0) {
// 抛出业务异常
throw new DeleteException("当前菜品分类下存在关联菜品,无法删除!");
}
// 查询当前分类(套餐类型)是否关联了菜品,如果关联了,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLqw = new LambdaQueryWrapper<>();
setmealLqw.eq(Setmeal::getCategoryId, id);
int setmealCount = setmealService.count(setmealLqw);
if(setmealCount > 0) {
// 抛出业务异常
throw new DeleteException("当前套餐类型下存在关联菜品,无法删除!");
}
// 正常删除分类
super.removeById(id);
}
}
3、去定义一个自定义异常,作为步骤2抛出的异常,自定义异常需要继承RuntimeException,将错误信息(String)返回;
/**
* 自定义异常:分类删除异常(分类存在关联,表中存在外键关联)
*/
public class DeleteException extends RuntimeException {
public DeleteException(String message) {
super(message);
}
}
4、在全局异常处理类中,处理我们的自定义异常,并且将错误信息(RestBean)返回;
/**
* 异常处理方法(删除分类时,存在关联外键)
* @return
*/
@ExceptionHandler(DeleteException.class)
public ResultBean<String> exceptionHandle(DeleteException e) {
log.info("捕获到该异常" + e.getMessage());
return ResultBean.error(e.getMessage());
}
5、在Category的Controller中,编写页面发起删除请求的处理方法,并在其中调用重写过的remove方法;
/**
* 删除分类(删除菜品分类或套餐类型)
* @param ids
* @return
*/
@DeleteMapping()
public ResultBean<String> delete(Long ids) {
log.info("id=" + ids);
categoryService.remove(ids);
return ResultBean.success("删除成功!");
}
/**
* 修改分类信息
* @param category
* @return
*/
@PutMapping()
public ResultBean<String> update(@RequestBody Category category) {
log.info( "category=" + category);
categoryService.updateById(category);
return ResultBean.success("修改成功!");
}
文件上传
文件上传时,对页面的form表单有如下要求:
举例:
<form method="post"action="/common/upload"enctype="multipart/form-data">
<input type:="submit"value="提交"/>
form>
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。
例如ElementUl中提供的upload上传组件:

服务端要接收客户端页面上传的文件,通常都会使用Apachel的两个组件:
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:

我们在配置文件中定义了文件保存的路径,然后通过@Value(“${reggie.path}”)来获取,需要注意的是,Controller的参数名称需要和请求负载中的name属性名称一致!

@Value("${reggie.path}")
private String path;
/**
* 文件上传方法
* 注意,参数名称需要与前端传来的参数(也就是name属性的名称一致)
* @param file
* @return
*/
@PostMapping("/upload")
public ResultBean<String> upload(MultipartFile file) {
// 这里的file是一个临时文件,需要转存到指定位置,否则等到这次请求完成之后,就会删除
log.info(file.toString());
// 防止path路径不存在
File dir = new File(path);
if ( ! dir.exists()) {
dir.mkdir();
}
String originalFilename = file.getOriginalFilename();
// 使用UUID重新生成文件名,防止文件名重复造成覆盖
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID() + suffix;
try {
file.transferTo(new File(path + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return ResultBean.success(fileName);
}
文件下载
而通过浏览器进行文件下载,通常有两种表现形式:
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
在这里,上传文件的时候会紧接着发起一个下载文件的请求,这样就可以将用户上传的文件展现在页面中!
/**
* 文件下载(将文件返回给页面显示)
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
// 输入流
FileInputStream inputStream = new FileInputStream(new File(path + name));
// 输出流,负责将文件数据返回给页面
ServletOutputStream outputStream = response.getOutputStream();
// 设置响应类型
response.setContentType("image/jepg");
int len = 0;
byte[] bytes = new byte[1024];
while((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
1、进入页面会发起一个ajax请求,将分类信息展示到下拉框中;
2、页面发送请求上传图片;
3、页面发送请求下载图片;
4、保存菜品信息,发送请求后将信息以json形式提交到服务端。
新增菜品的分类下拉框
/**
* 在增加菜品页面中,回显分类下拉框
* 页面请求携带的参数是String type=1,但是我们可以将其直接封装到Category中
* @param category
* @return
*/
@GetMapping("/list")
public ResultBean<List<Category>> list(Category category) {
// 条件构造器
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper();
// 查询条件
if (category.getType() != null) {
lqw.eq(Category::getType, category.getType());
}
// 排序条件(按照)
lqw.orderByAsc(Category::getSort).orderByDesc(Category::getCreateTime);
List<Category> list = categoryService.list(lqw);
return ResultBean.success(list);
}
文件的上传与下载
上文有提及!
保存菜品信息
假如此时存在一个情况,前端页面提交的数据并没有对应的实体类用于接收,例如:

数据需要两个实习类配合才能接收参数,所以这个时候,我们都会设置一个dto,用于传输数据(一般用于展示层和服务层)。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements IDishService {
@Autowired
private IDishFlavorService dishFlavorService;
/**
* 新增菜品,同时更新菜品对应的口味数据
* @param dishDto
*/
@Override
@Transactional // 事务控制
public void saveWithFlavors(DishDto dishDto) {
// 将菜品的基本信息保存到菜品中
this.save(dishDto);
// 但需要注意的是,这里只是将菜品的信息保存到了菜品表中
// 我们是需要对菜品表和菜品口味表这两张表进行操作
// 通过流的方式遍历集合,并对集合中的每个元素进行属性赋值
Long id = dishDto.getId(); // 菜品ID,因为前端提交的口味信息中国,只有name和value字段
// 而对应的口味表还却有一个dish_id字段,所以我们还需要将这个字段封装进去
List<DishFlavor> flavors = dishDto.getFlavors();
flavors.stream().map((item) -> {
item.setDishId(id);
return item;
}).collect(Collectors.toList());
// 将菜品口味细细保存到菜品中
dishFlavorService.saveBatch(flavors);
}
}
@PostMapping
public ResultBean<String> dish(@RequestBody DishDto dishDto) {
// 这里肯定不能直接去调用dishService的save方法,因为我们需要对两张表进行操作
log.info(dishDto.toString());
dishService.saveWithFlavors(dishDto);
return ResultBean.success("新增成功!");
}
注意,因为在DishService中,我们对两张表进行了操作,所以添加了事物支持的注释,于是我们需要在启动类上开启事务支持!
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
public class ReggieTakeOutApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieTakeOutApplication.class, args);
}
}
/**
* 菜品分页,使用dishDto对象
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public ResultBean<Page> page(int page, int pageSize, String name) {
// 分页构造器
Page<Dish> pageInfo = new Page<>(page, pageSize);
Page<DishDto> dishDtoPage = new Page<>(page, pageSize);
// 条件选择器
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
lqw.like(name != null, Dish::getName, name);
lqw.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo, lqw);
// 这里存在一个问题,就是页面中需要展示菜品分类名称,而Dish表中并不存在该属性
// 于是我们需要用到DishDto
// 对象拷贝(使用BeanUtils),并且忽略records属性(因为我们需要对records属性,也就是dish对象集合,进行单独处理)
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
// 获得pageInfo的records属性(dish对象集合)
List<Dish> records = pageInfo.getRecords();
// 通过流的方式去赋值
List<DishDto> list = records.stream().map((item)->{
// 返回的集合对象DishDto
DishDto dishDto = new DishDto();
// 对象拷贝(因为此时的dishDto是空的,于是我们需要将item赋值给dishDto)
BeanUtils.copyProperties(item, dishDto);
// 获得categoryId
Long categoryId = item.getCategoryId();
// 通过categoryService.getById获得Category对象
Category category = categoryService.getById(categoryId);
// 通过Category对象获得categoryName
String categoryName = category.getName();
// 将categoryName赋值给dishDto
dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
// 返回dishDtoPage,也就是新增categoryName字段的dish对象
return ResultBean.success(dishDtoPage);
}
/**
* 根据id查询对应的菜品以及口味
* @param id
* @return
*/
@GetMapping("/{id}")
public ResultBean<DishDto> getByIdWithFlavors(@PathVariable Long id) {
DishDto dishDto = dishService.getByIdWithFlavors(id);
return ResultBean.success(dishDto);
}
/**
* 根据菜品id来查询对应的菜品以及口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavors(Long id) {
// 查询菜品基本信息
Dish dish = this.getById(id);
// 将菜品对象拷贝到dishDto对象
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish, dishDto);
// 查询菜品对应的口味信息
LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper();
lqw.eq(DishFlavor::getDishId, id);
List<DishFlavor> dishFlavors = dishFlavorService.list(lqw);
dishDto.setFlavors(dishFlavors);
return dishDto;
}
@PutMapping
public ResultBean<String> update(@RequestBody DishDto dishDto) {
// 这里肯定不能直接去调用dishService的save方法,因为我们需要对两张表进行操作
log.info(dishDto.toString());
dishService.updateWithFlavors(dishDto);
return ResultBean.success("修改成功!");
}
在新增套餐中,我们添加了一个特殊的功能,这个功能可以快速添加套餐所包含的菜品(一种或者多种),于是我们需要展示出各种菜品分类中所包含的菜品!

/**
* 前端传来的路径为list?categoryId=xxx,所以参数可以直接写Long categoryId
* 但是为了代码的包容性,所以选择Dish对象来存储参数
* @param dish
* @return
*/
@GetMapping("/list")
public ResultBean<List<Dish>> list(Dish dish) {
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lqw);
return ResultBean.success(list);
}
新增套餐功能的代码:
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements ISetmealService {
@Autowired
ISetmealDishService iSetmealDishService;
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
iSetmealDishService.saveBatch(setmealDishes);
}
}
/**
* 新增套餐
* @return
*/
@PostMapping
public ResultBean<String> save(@RequestBody SetmealDto setmealDto) {
setmealService.saveWithDish(setmealDto);
return ResultBean.success("新增成功!");
}
@GetMapping("/page")
public ResultBean<Page> page(int page, int pageSize, String name) {
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>(page, pageSize);
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper();
lqw.like(name != null, Setmeal::getName, name);
lqw.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo, lqw);
// 这里一样存在一个问题,页面中需要展示菜品分类名称,而Setmeal表中并不存在该属性
// 该问题在菜品(page)的分页功能中也存在,这里不过多赘述
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");
// 获得pageInfo的records属性(dish对象集合)
List<Setmeal> records = pageInfo.getRecords();
// 通过流的方式去赋值
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
// 对象拷贝
BeanUtils.copyProperties(item, setmealDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
// 返回新增categoryName字段的对象
return ResultBean.success(setmealDtoPage);
}
在该页面中,有批量删除和单条数据删除两种功能,而且需要注意,这里的删除功能需要在商品处于停售状态下,才可以进行删除,不然是无法进行删除的,于是在此之前,我们需要对套餐的起售/停售功能进行实现:


@Override
@Transactional
public void removeWithDish(List<Long> ids) {
// 查询套餐的状态,如果是处于停售状态,则可以进行删除
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper();
// select count(*) from setmeal where id in (ids...) and status = 1
lqw.in(Setmeal::getId, ids);
lqw.eq(Setmeal::getStatus, 1);
int count = this.count(lqw);
// 如果不可删除,抛出一个业务异常
if (count > 0) {
throw new DeleteException("部分套餐正在售卖中,无法删除!");
}
// 删除套餐表中的数据
this.removeByIds(ids);
// iSetmealDishService.removeByIds(ids);
// 由于在setmeal_dish这张表中,setmeal_id并不是主键,所以不可以使用该方法删除
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// delete * from setmeal_dish where setmeal_id in (ids...)
lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);
// 删除关系表(setmeal_dish)中的数据
iSetmealDishService.remove(lambdaQueryWrapper);
}
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping()
public ResultBean<String> delete(@RequestParam List<Long> ids) {
setmealService.removeWithDish(ids);
return ResultBean.success("删除成功!");
}
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。

当然,在使用之前,我们需要一个阿里云网站的账号,以及开通短信通知服务!
1、订阅短信通知服务!
2、申请签名以及模版,签名可以理解为发送短信方的署名,而模版可以理解为发送的短信文字模版;需要注意的是,你需要记住签名,也就是用户所展示的Accesskey ID和Accesskey Secret,相当于账号和密码;


3、准备好AccessKey ID,以及为该用户进行授权(SMS短信服务权限);

可以通过以下两种方式安装Java SDK。
方式一:导入Maven依赖
通过在pom.xml文件中添加Maven依赖安装阿里云Java SDK。您可以在OpenAPI开发者门户,选择原版 SDK,查看各云产品的Maven依赖信息。
添加以下依赖安装阿里云Java SDK。
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.16version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-dysmsapiartifactId>
<version>1.1.0version>
dependency>
在集成开发环境中导入JAR文件
通过导入aliyu-java-sdk-core JAR文件的方式安装阿里云Java SDK。
代码实例
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;
import java.util.*;
import com.aliyuncs.dysmsapi.model.v20170525.*;
public class SendSms {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void main(String[] args) {
DefaultProfile profile = DefaultProfile.getProfile("cn-beijing", "" , "" );
/** use STS Token
DefaultProfile profile = DefaultProfile.getProfile(
"", // The region ID
"", // The AccessKey ID of the RAM account
"", // The AccessKey Secret of the RAM account
""); // STS Token
**/
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers("1368846****");//接收短信的手机号码
request.setSignName("阿里云");//短信签名名称
request.setTemplateCode("SMS_20933****");//短信模板CODE
request.setTemplateParam("张三");//短信模板变量对应的实际值
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println(new Gson().toJson(response));
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
System.out.println("ErrCode:" + e.getErrCode());
System.out.println("ErrMsg:" + e.getErrMsg());
System.out.println("RequestId:" + e.getRequestId());
}
}
}
1、获取依赖;

2、将控制台中的业务代码粘贴进去即可!我们甚至可以在阿里云提供的网页控制台中,将参数进行补充,然后直接复制粘贴!
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
登录流程:输入手机号 > 获取验证码 > 输入验证码 > 点击登录 > 登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识。
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
注意,因为我们这里将视角转向了手机端登录,所以我们也需要对手机端登录页面的请求,在过滤器中进行防行,这倒是简单,只需要在放行路径集合中添加手机登录路径即可!
// 定义并不需要拦截的路径(登录、退出、静态资源)
String[] urls = new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login"
};
如果这里遇到了页面无法加载,但是代码并没有出现错误的情况下,可以使用Ctrl+F5强制刷新网页,来清理缓存!
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
IUserService userService;
/**
* 发送验证码,并将手机信息以及验证码存放在session中
* @param session
* @param user
* @return
*/
@PostMapping("/sendMsg")
public ResultBean<String> sendMsg(HttpSession session, @RequestBody User user) {
// 获取手机号
String phone = user.getPhone();
// 随机生成验证码(这里使用的是工具类生成,但事实上,我们应该使用阿里云的短信服务来生成验证码)
if (phone != null) {
Integer code = ValidateCodeUtils.generateValidateCode(4);
log.info("code=" + code);
session.setAttribute(phone, code);
return ResultBean.success("短信发送成功");
}
return ResultBean.error("短信发送失败");
}
/**
* 登录:
* @param session
* @param map 因为页面传来的参数仅靠User是无法接收的,所以定义了一个Map来接受键值对(或者新建一个UserDto)
* @return
*/
@PostMapping("/login")
@Transactional
public ResultBean<User> login(HttpSession session, @RequestBody Map map) {
log.info("map=" + map);
// 获取手机号
String phone = map.get("phone").toString();
// 获取验证码
String code = map.get("code").toString();
Object attribute = session.getAttribute(phone);
String codeInSession = attribute.toString();
// 如果满足session不为空,但是session中存放的code和参数传来的code一致,则说明验证码通过
// 但是该用户为第一次登录,应该为其默认注册
if (codeInSession != null && codeInSession.equals(code)) {
// 对表进行操作
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper();
lqw.eq(User::getPhone, phone);
User user = userService.getOne(lqw);
// 为第一次登录的用户执行注册服务
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user", user.getId());
return ResultBean.success(user);
}
return ResultBean.error("登录失败");
}
}
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址!
我们可以从页面上看到,用户可以新增收货地址,并且在地址管理页面中国,可以将某个地址设置为默认地址,点击地址旁边的铅笔图标,还可以修改当前地址的信息!
也就是对应着增删改查这四大功能模块。

@RestController
@RequestMapping("/addressBook")
@Slf4j
public class AddressBookController {
@Autowired
IAddressBookService addressBookService;
/**
* 新增地址
* @param addressBook
* @return
*/
@PostMapping
public ResultBean<AddressBook> add(@RequestBody AddressBook addressBook) {
log.info("address" + addressBook);
addressBook.setUserId(BaseContext.getCurrentID());
// 判断当前用户的地址簿是否存在有地址(是否存在有默认地址),如果没有,则将这次新增的地址设置为默认地址
LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper();
lqw.eq(AddressBook::getUserId, BaseContext.getCurrentID())
.eq(AddressBook::getIsDefault, 1);
AddressBook defaultAddress = addressBookService.getOne(lqw);
if (defaultAddress == null) {
addressBook.setIsDefault(1);
}
addressBookService.save(addressBook);
log.info("addressBook = " + addressBook);
return ResultBean.success(addressBook);
}
}
/**
* 页面展示当前用户所有地址
* @param addressBook
* @return
*/
@GetMapping("/list")
public ResultBean<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentID());
log.info("addressBook = " + addressBook);
LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper();
lqw.eq(addressBook.getUserId() != null, AddressBook::getUserId, addressBook.getUserId());
lqw.orderByDesc(AddressBook::getIsDefault).orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> list = addressBookService.list(lqw);
return ResultBean.success(list);
}
/**
* 设置默认地址
* 该功能主要服务于用户结算订单时返回默认收货地址
* @param addressBook
* @return
*/
@PutMapping("/default")
@Transactional
public ResultBean<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
LambdaUpdateWrapper<AddressBook> luw = new LambdaUpdateWrapper<>();
// 将当前的默认地址更改为非默认地址
luw.eq(AddressBook::getUserId, BaseContext.getCurrentID());
// update addressBook set is_default = 0 where user_id = ()
luw.set(AddressBook::getIsDefault, 0);
addressBookService.update(luw);
// 将用户传来的新地址,设置为默认地址
addressBook.setIsDefault(1);
addressBookService.updateById(addressBook);
return ResultBean.success(addressBook);
}
/**
* 根据用户id查询地址簿,该方法主要是在更新/删除地址簿页面回显当前地址簿信息
* @param id
* @return
*/
@GetMapping("/{id}")
public ResultBean<AddressBook> getAddressByID(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return ResultBean.success(addressBook);
}
return ResultBean.error("对象超时或消失");
}
/**
* 修改地址
* @param addressBook
* @return
*/
@Transactional
@PutMapping()
public ResultBean<AddressBook> update(@RequestBody AddressBook addressBook) {
log.info("address" + addressBook);
if (addressBook != null) {
addressBookService.updateById(addressBook);
}
return ResultBean.success(addressBook);
}
/**
* 删除地址
* @param ids
* @return
*/
@Transactional
@DeleteMapping()
public ResultBean<String> delete(Long ids) {
log.info("ids" + ids);
// 判断当前地址是否为默认地址,如果是默认地址,则不可删除
if (ids != null) {
AddressBook addressBook = addressBookService.getById(ids);
if (addressBook.getIsDefault() == 1) {
throw new DeleteException("默认地址不可删除");
}
addressBookService.removeById(ids);
}
return ResultBean.success("删除成功");
}
可以看到以下图片中,左侧我们需要展示分类表,而右侧则是需要展示对应的菜品列表,并且这里存在一个bug,就是在展示菜品列表的右下侧,理应变成一个“选择规格”的图标,因为我们为菜品设置了口味规格,所以需要用户去选择口味。
而这里,左侧的展示,在我们写分类页面的list请求时就已经写完了,右侧的展示,则是在菜品功能的list请求中写完了,但是在该list请求中,我们返回的对象为Dish,也就是菜品,而菜品表里面是不包含口味信息的,所以我们需要对该list请求进行些许的改进。

原菜品list请求:
@GetMapping("/list")
public ResultBean<List<Dish>> list(Dish dish) {
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 对菜品的销售状态进行过滤
lqw.eq(Dish::getStatus, 1);
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lqw);
return ResultBean.success(list);
}
优化后:
/**
* 前端传来的路径为list?categoryId=xxx,所以参数可以直接写Long categoryId
* 但是为了代码的包容性,所以选择Dish对象来存储参数
* update:这里出现了一些问题,就是在顾客进入菜单页面时,原本应该显示“选择规格”的菜品并没有显示
* 是因为当前返回的数据是dish对象,于是我们将返回对象更新为dishDto,并且将对应菜品的口味信息添加进去
* @param dish
* @return
*/
@GetMapping("/list")
public ResultBean<List<DishDto>> list(Dish dish) {
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 对菜品的销售状态进行过滤
lqw.eq(Dish::getStatus, 1);
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lqw);
List<DishDto> dtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
// 这里是展示当前菜品的分类名称,不过其实这段代码也可以不写(只要没有需求的话)
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
// 这里是将口味信息返回到dishdto中
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lqwFlavors = new LambdaQueryWrapper();
lqwFlavors.eq(DishFlavor::getDishId, dishId).orderByDesc(DishFlavor::getUpdateTime);
if (dishId != null) {
List<DishFlavor> dishFlavors = dishFlavorService.list(lqwFlavors);
dishDto.setFlavors(dishFlavors);
}
return dishDto;
}).collect(Collectors.toList());
return ResultBean.success(dtoList);
}
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击+将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。我们可以通过点击到导航栏左侧的外卖员小图标来展开购物车列表。
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:
1、点击加入购物车或者+按钮,页面发送jax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送jax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送jax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。

@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
IShoppingCartService shoppingCartService;
/**
* 将菜品添加进购物车(当前菜品数量+1)
* @param session
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public ResultBean<ShoppingCart> add(HttpSession session, @RequestBody ShoppingCart shoppingCart) {
// 设置用户id(处理userId=null,)
Long userId = BaseContext.getCurrentID();
shoppingCart.setUserId(userId);
// 查询当前加入购物车的菜品是否存在于购物车,如果存在,则该菜品数量+1(处理number=null)
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper();
lqw.eq(ShoppingCart::getUserId, userId);
// 判断当前菜品的分类
if (shoppingCart.getDishId() != null) {
lqw.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
} else {
lqw.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart cart = shoppingCartService.getOne(lqw);
if (cart != null) {
// 如果已经存在,而就在原来数量基础上加一
Integer number = cart.getNumber();
cart.setNumber(number + 1);
shoppingCartService.updateById(cart);
return ResultBean.success(cart);
}
// 如果,则如果不存在,则添加到购物车,数量默认就是一
shoppingCart.setNumber(1);
// 设置creatTime,这里为什么不使用注解的方式进行字段的自动填充呢?
// 因为在自动填充时,我们统一对createTime以及其他属性进行填充
// 但是在shoppingCart表中,只有一个createTime属性,所以会报错
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
return ResultBean.success(shoppingCart);
}
}
/**
* 展示购物车中的菜品
* @return
*/
@GetMapping("/list")
public ResultBean<List<ShoppingCart>> list() {
Long userId = BaseContext.getCurrentID();
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper();
lqw.eq(ShoppingCart::getUserId, userId).orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(lqw);
return ResultBean.success(list);
}
/**
* 清空购物车
* @return
*/
@DeleteMapping("clean")
@Transactional
public ResultBean<String> clean() {
Long userId = BaseContext.getCurrentID();
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper();
lqw.eq(ShoppingCart::getUserId, userId);
shoppingCartService.remove(lqw);
return ResultBean.success("清空成功");
}
/**
* 当前购物车中的菜品数量-1
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
public ResultBean<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
Long userId = BaseContext.getCurrentID();
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper();
lqw.eq(ShoppingCart::getUserId, userId);
Long dishId = shoppingCart.getDishId();
if (dishId != null) {
lqw.eq(ShoppingCart::getDishId, dishId);
} else {
lqw.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart one = shoppingCartService.getOne(lqw);
Integer number = one.getNumber();
one.setNumber(number - 1);
shoppingCartService.updateById(one);
return ResultBean.success(one);
}
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮则完成下单操作。

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
1、在购物车中点击去结算按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送jax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击去支付按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
@Slf4j
@RestController
@RequestMapping("/order")
public class OrdersController {
@Autowired
IOrdersService ordersService;
@Autowired
IOrderDetailService orderDetailService;
@Autowired
IDishService dishService;
@PostMapping("/submit")
public ResultBean<String> submit(@RequestBody Orders orders) {
ordersService.submit(orders);
return ResultBean.success("订单创建成功");
}
}
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersService {
@Autowired
IShoppingCartService shoppingCartService;
@Autowired
IOrdersService ordersService;
@Autowired
IUserService userService;
@Autowired
IAddressBookService addressBookService;
@Autowired
IOrderDetailService orderDetailService;
@Override
@Transactional
public void submit(Orders orders) {
// 获取当前用户id
Long userId = BaseContext.getCurrentID();
// 查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
shoppingCartLambdaQueryWrapper
.eq(ShoppingCart::getUserId, userId);
List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
if (shoppingCartList == null || shoppingCartList.size() == 0) {
throw new RuntimeException("购物车为空,不能下单!");
}
// 查询用户数据
User user = userService.getById(userId);
// 查询地址数据(这里页面传回来了addressBookId)
AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
if (addressBook == null) {
throw new RuntimeException("用户地址为空,不能下单!");
}
AtomicInteger amount = new AtomicInteger(0); // 总金额
long orderId = IdWorker.getId(); // 订单Id,这里使用了IdWorker来生成,真实业务中是存在专门的id算法
// 准备订单明细的数据(顺便计算一下总金额)
List<OrderDetail> orderDetailList = shoppingCartList.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
// 计算总金额
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
// 向订单表插入数据(需要大量填充数据)
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(userId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表插入数据,一条数据
this.save(orders);
// 向订单明细表插入数据
orderDetailService.saveBatch(orderDetailList);
// 清空购物车数据
shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
}
}
/**
* 用户订单页面
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public ResultBean<Page<OrderDto>> page(int page, int pageSize) {
Long userId = BaseContext.getCurrentID();
Page<Orders> ordersPage = new Page<>(page, pageSize);
// order
LambdaQueryWrapper<Orders> lqw = new LambdaQueryWrapper();
lqw.eq(Orders::getUserId, userId);
ordersService.page(ordersPage, lqw);
// orderDto
Page<OrderDto> orderDtoPage = new Page<>(page, pageSize);
BeanUtils.copyProperties(ordersPage, orderDtoPage, "records");
List<Orders> records = ordersPage.getRecords();
List<OrderDto> orderDtoList = records.stream().map((item) -> {
OrderDto orderDto = new OrderDto();
BeanUtils.copyProperties(item, orderDto);
// 获取orderDetail
LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();
orderDetailLambdaQueryWrapper
.eq(OrderDetail::getOrderId, item.getId());
List<OrderDetail> orderDetailList = orderDetailService.list(orderDetailLambdaQueryWrapper);
// 设置菜品数量
orderDto.setSumNum(orderDetailList.size());
orderDto.setOrderDetails(orderDetailList);
return orderDto;
}).collect(Collectors.toList());
orderDtoPage.setRecords(orderDtoList);
return ResultBean.success(orderDtoPage);
}
1、使用gitee创建一个代码仓库,具体步骤略;
2、将本地代码推送到gitee的远程仓库中,具体步骤略;
3、创建一个分支,将该分支也推送到gitee的远程仓库汇总,具体步骤略;
1、在项目的pom.xml文件中导入spring data redis的maven坐标:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
2、在项目中编写redis的配置文件(如果有密码也需要配置密码);
spring:
redis:
host: 192.168.124.129
port: 6379
database: 0
3、更换redis的key的序列化器:
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* 该方法主要是为了外部连接redis时,提供一个我们能够清楚直观地看到数据
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 默认的key序列化器 JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
备注:在springboot的autoconfigure中,有一个redisautoconfigure,当我们项目中不存在redisTemplate这个bean的时候,它就会去自动创建这样一个bean;

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的。
现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:
1、在服务端JserController中注入RedisTemplate对象,用于操作Redis
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
IUserService userService;
@Autowired
RedisTemplate redisTemplate;
}
2、在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
/**
* 发送验证码,并将手机信息以及验证码存放在session中
* @param session
* @param user
* @return
*/
@PostMapping("/sendMsg")
public ResultBean<String> sendMsg(HttpSession session, @RequestBody User user) {
// 获取手机号
String phone = user.getPhone();
// 随机生成验证码(这里使用的是工具类生成,但事实上,我们应该使用阿里云的短信服务来生成验证码)
if (phone != null) {
Integer code = ValidateCodeUtils.generateValidateCode(4);
log.info("code=" + code);
// session.setAttribute(phone, code);
// 将验证码缓存到redis中,有效期为五分钟
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
return ResultBean.success("短信发送成功");
}
return ResultBean.error("短信发送失败");
}
3、在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
/**
* 登录:
* @param session
* @param map 因为页面传来的参数仅靠User是无法接收的,所以定义了一个Map来接受键值对(或者新建一个UserDto)
* @return
*/
@PostMapping("/login")
@Transactional
public ResultBean<User> login(HttpSession session, @RequestBody Map map) {
log.info("map=" + map);
// 获取手机号
String phone = map.get("phone").toString();
// 获取验证码
String code = map.get("code").toString();
// Object attribute = session.getAttribute(phone);
// 从redis获取验证码
Object attribute = redisTemplate.opsForValue().get(phone);
// 注意,这里必须要转为String,并且不可以使用强转的方式进行转换
String codeInSession = attribute.toString();
// 如果满足session不为空,但是session中存放的code和参数传来的code一致,则说明验证码通过
// 但是该用户为第一次登录,应该为其默认注册
if (codeInSession != null && codeInSession.equals(code)) {
// 对表进行操作
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper();
lqw.eq(User::getPhone, phone);
User user = userService.getOne(lqw);
// 为第一次登录的用户执行注册服务
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user", user.getId());
// 如果用户登录成功,则删除redis中的验证码
redisTemplate.delete(phone);
return ResultBean.success(user);
}
return ResultBean.error("登录失败");
}
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的
查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现
在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
1、改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis。需要注意的是,
/**
* 前端传来的路径为list?categoryId=xxx,所以参数可以直接写Long categoryId
* 但是为了代码的包容性,所以选择Dish对象来存储参数
* update:这里出现了一些问题,就是在顾客进入菜单页面时,原本应该显示“选择规格”的菜品并没有显示
* 是因为当前返回的数据是dish对象,于是我们将返回对象更新为dishDto,并且将对应菜品的口味信息添加进去
* @param dish
* @return
*/
@GetMapping("/list")
public ResultBean<List<DishDto>> list(Dish dish) {
// 向redis中查询数据,如果查到了,则直接返回
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); // 存入redis中的key
List<DishDto> dtoList = null;
dtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
if (dtoList != null) {
return ResultBean.success(dtoList);
}
// 如果没有查到,则继续执行
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 对菜品的销售状态进行过滤
lqw.eq(Dish::getStatus, 1);
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lqw);
dtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
// 这里是将口味信息返回到dishdto中
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lqwFlavors = new LambdaQueryWrapper();
lqwFlavors.eq(DishFlavor::getDishId, dishId).orderByDesc(DishFlavor::getUpdateTime);
if (dishId != null) {
List<DishFlavor> dishFlavors = dishFlavorService.list(lqwFlavors);
dishDto.setFlavors(dishFlavors);
}
return dishDto;
}).collect(Collectors.toList());
// 将dish数据缓存到redis(60分钟之后过期)
redisTemplate.opsForValue().set(key, dtoList, 60, TimeUnit.MINUTES);
return ResultBean.success(dtoList);
}
2、改造DishController的save和update以及delete方法,加入清理缓存的逻辑,因为在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。
/**
* 更新菜品
* @param dishDto
* @return
*/
@PutMapping
public ResultBean<String> update(@RequestBody DishDto dishDto) {
// 这里肯定不能直接去调用dishService的save方法,因为我们需要对两张表进行操作
log.info(dishDto.toString());
// // 清空所有菜品的缓存数据!
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);
// 因为我们存入到redis中的键值对是某个分类下的所有菜品
// 所以我们可以选择清理指定菜品下的分类缓存数据
String key = "dish_" + dishDto.getCategoryId() + "_1";
redisTemplate.delete(key);
dishService.updateWithFlavors(dishDto);
return ResultBean.success("修改成功!");
}
/**
* 新增菜品
* @param dishDto
* @return
*/
@Transactional
@PostMapping
public ResultBean<String> dish(@RequestBody DishDto dishDto) {
// 这里肯定不能直接去调用dishService的save方法,因为我们需要对两张表进行操作
log.info(dishDto.toString());
String key = "dish_" + dishDto.getCategoryId() + "_*";
redisTemplate.delete(key);
dishService.saveWithFlavors(dishDto);
return ResultBean.success("新增成功!");
}
/**
* 删除菜品
* @param ids
* @return
*/
@Transactional
@DeleteMapping()
public ResultBean<String> delete(Long ids) {
log.info("ids" + ids);
// 因为这里没办法拿到分类id,所以我们就将redis中所有分类缓存清空
Set keys = redisTemplate.keys("dish_*");
redisTemplate.delete(keys);
dishService.removeById(ids);
return ResultBean.success("删除成功");
}
3、将测试完成的代码合并到主分支!
前面我们已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的ist方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长;
现在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
1、导入maven坐标:spring-boot-starter-data-redis,spring-boot-starter-cache
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
2、配置application.yml
spring:
cache:
redis:
time-to-live: 1800000 # 缓存有效时间(30分钟)
3、在启动类上加入@EnableCaching注解,开启缓存注解功能;
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
@EnableCaching
public class ReggieTakeOutApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieTakeOutApplication.class, args);
}
}
4、在SetmealController的list方法上加入@Cacheable注解
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")
@GetMapping("/list")
public ResultBean<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
lqw.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
lqw.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
lqw.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(lqw);
return ResultBean.success(list);
}
然后你就发现测试的时候报了500异常!原因是因为这里的返回对象是ResultBean,而这个对象是无法序列化的,所以就返回了一个错误,说是返回结果需要实现序列化接口;

解决方法也简单,只要去实现序列化接口即可!
@Data
public class ResultBean<T> implements Serializable {
...
}
5、在SetmealController的save和delete方法上加入CacheEvicti注解,allEntries = true表示清空当前缓存分类下的所有缓存;
/**
* 删除套餐
* @param ids
* @return
*/
@CacheEvict(value = "setmealCache", allEntries = true)
@DeleteMapping()
public ResultBean<String> delete(@RequestParam List<Long> ids) {
setmealService.removeWithDish(ids);
return ResultBean.success("删除成功!");
}
/**
* 新增套餐
* @return
*/
@CacheEvict(value = "setmealCache", allEntries = true)
@PostMapping
public ResultBean<String> save(@RequestBody SetmealDto setmealDto) {
setmealService.saveWithDish(setmealDto);
return ResultBean.success("新增成功!");
}
6、推送到主分支!