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

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

前端发起的请求:

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

登录功能对应的数据库表中的员工表,所以需要针对员工表进行一系列架构,例如实体类,mapper,service,controller:
Mapper:
@Mapper
public interface EmployeeMapper extends BaseMapper {
}
Service:
public interface IEmployeeService extends IService {
}@Service
public class EmployeeServiceImpl extends ServiceImpl implements IEmployeeService {}
Controller:
@RestController
@RequestMapping("/emploee")
public class EmployeeController {@Autowiredpublic IEmployeeService employeeService;
}
并且在经过上面步骤之后,我们需要一个通过结果类,因为我们会编写非常多的Controller,我们应该将返回的结果进行统一!也就是将服务端响应后返回到页面的数据都应该被封装为一个统一的对象!代码如下:
@Data
public class ResultBean {private Integer code; //编码:1成功,0和其它数字为失败private String msg; //错误信息private T data; //数据private Map map = new HashMap(); //动态数据public static ResultBean success(T object) {ResultBean result = new ResultBean<>();result.data = object;result.code = 1;return result;}public static ResultBean error(String msg) {ResultBean result = new ResultBean();result.msg = msg;result.code = 0;return result;}public ResultBean add(String key, Object value) {this.map.put(key, value);return this;}
}
具体业务实现代码:
@RestController
@RequestMapping("/employee")
public class EmployeeController {@Autowiredpublic 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 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();@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;// 获得本次请求的uriString 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*/@PostMappingpublic ResultBean 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());//获得当前登录用户的idLong 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 {@Beanpublic 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(int page, int pageSize, String name) {log.info("page={},pageSize={},name={}" + page, pageSize, name);// 构造分页选择器Page pageInfo = new Page(page, pageSize);// 构造条件选择器LambdaQueryWrapper 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*/@Overrideprotected void extendMessageConverters(List> converters) {// 创建一个新的消息转化器MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();// 设置消息转换器messageConverter.setObjectMapper(new JacksonObjectMapper());// 将消息转换器追加到Mvc的转换器集合中,index表示优先级converters.add(0, messageConverter);}
}
/*** 更改用户状态* @param request* @param employee 网页已经传入的参数中已经包含用户id和状态码了* @return*/@PutMappingpublic ResultBean 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 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*/@Overridepublic void insertFill(MetaObject metaObject) {log.info("meta" + metaObject);}/*** 更新操作自动填充* @param metaObject*/@Overridepublic 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 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*/@Overridepublic 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*/@Overridepublic void updateFill(MetaObject metaObject) {metaObject.setValue("updateTime", LocalDateTime.now());metaObject.setValue("updateUser", BaseContext.getCurrentID());}
}
涉及的表结构

@RestController
@RequestMapping("/category")
public class CategoryController {@AutowiredICategoryService categoryService;/*** 添加分类(添加菜品或者套餐)* @param category* @return*/@PostMappingpublic ResultBean 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(int page, int pageSize, String name) {// 构造分页选择器Page pageInfo = new Page(page, pageSize);// 构造条件选择器LambdaQueryWrapper 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 implements ICategoryService {@AutowiredIDishService dishService;@AutowiredISetmealService setmealService;/*** 根据id删除分类(菜品或套餐)* 因为删除某种套餐时,可能下面关联着某些菜品,所以需要对删除操作增加一个判断* @param id*/@Overridepublic void remove(Long id) {// 查询当前分类(菜品分类)是否关联了菜品,如果关联了,抛出一个业务异常LambdaQueryWrapper dishLqw = new LambdaQueryWrapper();dishLqw.eq(Dish::getCategoryId, id);int dishCount = dishService.count(dishLqw);if(dishCount > 0) {// 抛出业务异常throw new DeleteException("当前菜品分类下存在关联菜品,无法删除!");}// 查询当前分类(套餐类型)是否关联了菜品,如果关联了,抛出一个业务异常LambdaQueryWrapper 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 exceptionHandle(DeleteException e) {log.info("捕获到该异常" + e.getMessage());return ResultBean.error(e.getMessage());}
5、在Category的Controller中,编写页面发起删除请求的处理方法,并在其中调用重写过的remove方法;
/*** 删除分类(删除菜品分类或套餐类型)* @param ids* @return*/@DeleteMapping()public ResultBean delete(Long ids) {log.info("id=" + ids);categoryService.remove(ids);return ResultBean.success("删除成功!");}
/*** 修改分类信息* @param category* @return*/@PutMapping()public ResultBean update(@RequestBody Category category) {log.info( "category=" + category);categoryService.updateById(category);return ResultBean.success("修改成功!");}
文件上传
文件上传时,对页面的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 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 category) {// 条件构造器LambdaQueryWrapper lqw = new LambdaQueryWrapper();// 查询条件if (category.getType() != null) {lqw.eq(Category::getType, category.getType());}// 排序条件(按照)lqw.orderByAsc(Category::getSort).orderByDesc(Category::getCreateTime);List list = categoryService.list(lqw);return ResultBean.success(list);}
文件的上传与下载
上文有提及!
保存菜品信息
假如此时存在一个情况,前端页面提交的数据并没有对应的实体类用于接收,例如:

数据需要两个实习类配合才能接收参数,所以这个时候,我们都会设置一个dto,用于传输数据(一般用于展示层和服务层)。
@Data
public class DishDto extends Dish {private List flavors = new ArrayList<>();private String categoryName;private Integer copies;
}
@Service
public class DishServiceImpl extends ServiceImpl implements IDishService {@Autowiredprivate IDishFlavorService dishFlavorService;/*** 新增菜品,同时更新菜品对应的口味数据* @param dishDto*/@Override@Transactional // 事务控制public void saveWithFlavors(DishDto dishDto) {// 将菜品的基本信息保存到菜品中this.save(dishDto);// 但需要注意的是,这里只是将菜品的信息保存到了菜品表中// 我们是需要对菜品表和菜品口味表这两张表进行操作// 通过流的方式遍历集合,并对集合中的每个元素进行属性赋值Long id = dishDto.getId(); // 菜品ID,因为前端提交的口味信息中国,只有name和value字段// 而对应的口味表还却有一个dish_id字段,所以我们还需要将这个字段封装进去List flavors = dishDto.getFlavors();flavors.stream().map((item) -> {item.setDishId(id);return item;}).collect(Collectors.toList());// 将菜品口味细细保存到菜品中dishFlavorService.saveBatch(flavors);}
}
@PostMappingpublic ResultBean 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(int page, int pageSize, String name) {// 分页构造器Page pageInfo = new Page<>(page, pageSize);Page dishDtoPage = new Page<>(page, pageSize);// 条件选择器LambdaQueryWrapper 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 records = pageInfo.getRecords();// 通过流的方式去赋值List list = records.stream().map((item)->{// 返回的集合对象DishDtoDishDto dishDto = new DishDto();// 对象拷贝(因为此时的dishDto是空的,于是我们需要将item赋值给dishDto)BeanUtils.copyProperties(item, dishDto);// 获得categoryIdLong categoryId = item.getCategoryId();// 通过categoryService.getById获得Category对象Category category = categoryService.getById(categoryId);// 通过Category对象获得categoryNameString categoryName = category.getName();// 将categoryName赋值给dishDtodishDto.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 getByIdWithFlavors(@PathVariable Long id) {DishDto dishDto = dishService.getByIdWithFlavors(id);return ResultBean.success(dishDto);}
/*** 根据菜品id来查询对应的菜品以及口味信息* @param id* @return*/@Overridepublic DishDto getByIdWithFlavors(Long id) {// 查询菜品基本信息Dish dish = this.getById(id);// 将菜品对象拷贝到dishDto对象DishDto dishDto = new DishDto();BeanUtils.copyProperties(dish, dishDto);// 查询菜品对应的口味信息LambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(DishFlavor::getDishId, id);List dishFlavors = dishFlavorService.list(lqw);dishDto.setFlavors(dishFlavors);return dishDto;}
@PutMappingpublic ResultBean 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 dish) {LambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List list = dishService.list(lqw);return ResultBean.success(list);}
新增套餐功能的代码:
@Service
public class SetmealServiceImpl extends ServiceImpl implements ISetmealService {@AutowiredISetmealDishService iSetmealDishService;@Override@Transactionalpublic void saveWithDish(SetmealDto setmealDto) {this.save(setmealDto);List setmealDishes = setmealDto.getSetmealDishes();setmealDishes.stream().map((item) -> {item.setSetmealId(setmealDto.getId());return item;}).collect(Collectors.toList());iSetmealDishService.saveBatch(setmealDishes);}
}
/*** 新增套餐* @return*/@PostMappingpublic ResultBean save(@RequestBody SetmealDto setmealDto) {setmealService.saveWithDish(setmealDto);return ResultBean.success("新增成功!");}
@GetMapping("/page")public ResultBean page(int page, int pageSize, String name) {Page pageInfo = new Page<>(page, pageSize);Page setmealDtoPage = new Page<>(page, pageSize);LambdaQueryWrapper 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 records = pageInfo.getRecords();// 通过流的方式去赋值List 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@Transactionalpublic void removeWithDish(List ids) {// 查询套餐的状态,如果是处于停售状态,则可以进行删除LambdaQueryWrapper lqw = new LambdaQueryWrapper();// select count(*) from setmeal where id in (ids...) and status = 1lqw.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 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 delete(@RequestParam List 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。
com.aliyun aliyun-java-sdk-core 4.5.16
com.aliyun aliyun-java-sdk-dysmsapi 1.1.0
在集成开发环境中导入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 TokenDefaultProfile 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****");//短信模板CODErequest.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 {@AutowiredIUserService userService;/*** 发送验证码,并将手机信息以及验证码存放在session中* @param session* @param user* @return*/@PostMapping("/sendMsg")public ResultBean 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")@Transactionalpublic ResultBean 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 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 {@AutowiredIAddressBookService addressBookService;/*** 新增地址* @param addressBook* @return*/@PostMappingpublic ResultBean add(@RequestBody AddressBook addressBook) {log.info("address" + addressBook);addressBook.setUserId(BaseContext.getCurrentID());// 判断当前用户的地址簿是否存在有地址(是否存在有默认地址),如果没有,则将这次新增的地址设置为默认地址LambdaQueryWrapper 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 addressBook) {addressBook.setUserId(BaseContext.getCurrentID());log.info("addressBook = " + addressBook);LambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(addressBook.getUserId() != null, AddressBook::getUserId, addressBook.getUserId());lqw.orderByDesc(AddressBook::getIsDefault).orderByDesc(AddressBook::getUpdateTime);List list = addressBookService.list(lqw);return ResultBean.success(list);}
/*** 设置默认地址* 该功能主要服务于用户结算订单时返回默认收货地址* @param addressBook* @return*/@PutMapping("/default")@Transactionalpublic ResultBean setDefault(@RequestBody AddressBook addressBook) {LambdaUpdateWrapper 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 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 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 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 dish) {LambdaQueryWrapper 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 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(Dish dish) {LambdaQueryWrapper 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 list = dishService.list(lqw);List 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 lqwFlavors = new LambdaQueryWrapper();lqwFlavors.eq(DishFlavor::getDishId, dishId).orderByDesc(DishFlavor::getUpdateTime);if (dishId != null) {List 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 {@AutowiredIShoppingCartService shoppingCartService;/*** 将菜品添加进购物车(当前菜品数量+1)* @param session* @param shoppingCart* @return*/@PostMapping("/add")public ResultBean add(HttpSession session, @RequestBody ShoppingCart shoppingCart) {// 设置用户id(处理userId=null,)Long userId = BaseContext.getCurrentID();shoppingCart.setUserId(userId);// 查询当前加入购物车的菜品是否存在于购物车,如果存在,则该菜品数量+1(处理number=null)LambdaQueryWrapper 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() {Long userId = BaseContext.getCurrentID();LambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(ShoppingCart::getUserId, userId).orderByDesc(ShoppingCart::getCreateTime);List list = shoppingCartService.list(lqw);return ResultBean.success(list);}
/*** 清空购物车* @return*/@DeleteMapping("clean")@Transactionalpublic ResultBean clean() {Long userId = BaseContext.getCurrentID();LambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(ShoppingCart::getUserId, userId);shoppingCartService.remove(lqw);return ResultBean.success("清空成功");}
/*** 当前购物车中的菜品数量-1* @param shoppingCart* @return*/@PostMapping("/sub")public ResultBean sub(@RequestBody ShoppingCart shoppingCart) {Long userId = BaseContext.getCurrentID();LambdaQueryWrapper 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 {@AutowiredIOrdersService ordersService;@AutowiredIOrderDetailService orderDetailService;@AutowiredIDishService dishService;@PostMapping("/submit")public ResultBean submit(@RequestBody Orders orders) {ordersService.submit(orders);return ResultBean.success("订单创建成功");}
}
@Service
public class OrdersServiceImpl extends ServiceImpl implements IOrdersService {@AutowiredIShoppingCartService shoppingCartService;@AutowiredIOrdersService ordersService;@AutowiredIUserService userService;@AutowiredIAddressBookService addressBookService;@AutowiredIOrderDetailService orderDetailService;@Override@Transactionalpublic void submit(Orders orders) {// 获取当前用户idLong userId = BaseContext.getCurrentID();// 查询当前用户的购物车数据LambdaQueryWrapper shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, userId);List 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 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(int page, int pageSize) {Long userId = BaseContext.getCurrentID();Page ordersPage = new Page<>(page, pageSize);// orderLambdaQueryWrapper lqw = new LambdaQueryWrapper();lqw.eq(Orders::getUserId, userId);ordersService.page(ordersPage, lqw);// orderDtoPage orderDtoPage = new Page<>(page, pageSize);BeanUtils.copyProperties(ordersPage, orderDtoPage, "records");List records = ordersPage.getRecords();List orderDtoList = records.stream().map((item) -> {OrderDto orderDto = new OrderDto();BeanUtils.copyProperties(item, orderDto);// 获取orderDetailLambdaQueryWrapper orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, item.getId());List 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坐标:
org.springframework.boot spring-boot-starter-data-redis
2、在项目中编写redis的配置文件(如果有密码也需要配置密码);
spring:redis:host: 192.168.124.129port: 6379database: 0
3、更换redis的key的序列化器:
@Configuration
public class RedisConfig extends CachingConfigurerSupport {/*** 该方法主要是为了外部连接redis时,提供一个我们能够清楚直观地看到数据* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate
备注:在springboot的autoconfigure中,有一个redisautoconfigure,当我们项目中不存在redisTemplate这个bean的时候,它就会去自动创建这样一个bean;

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的。
现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:
1、在服务端JserController中注入RedisTemplate对象,用于操作Redis
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@AutowiredIUserService userService;@AutowiredRedisTemplate redisTemplate;
}
2、在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
/*** 发送验证码,并将手机信息以及验证码存放在session中* @param session* @param user* @return*/@PostMapping("/sendMsg")public ResultBean 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")@Transactionalpublic ResultBean 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 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(Dish dish) {// 向redis中查询数据,如果查到了,则直接返回String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); // 存入redis中的keyList dtoList = null;dtoList = (List) redisTemplate.opsForValue().get(key);if (dtoList != null) {return ResultBean.success(dtoList);}// 如果没有查到,则继续执行LambdaQueryWrapper 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 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 lqwFlavors = new LambdaQueryWrapper();lqwFlavors.eq(DishFlavor::getDishId, dishId).orderByDesc(DishFlavor::getUpdateTime);if (dishId != null) {List 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*/@PutMappingpublic ResultBean 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@PostMappingpublic ResultBean 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 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
org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-data-redis
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 setmeal){LambdaQueryWrapper 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 list = setmealService.list(lqw);return ResultBean.success(list);}
然后你就发现测试的时候报了500异常!原因是因为这里的返回对象是ResultBean,而这个对象是无法序列化的,所以就返回了一个错误,说是返回结果需要实现序列化接口;

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