<<回到导览
1. 环境搭建
1.1.项目结构介绍
项目整体结构介绍
名称 | 说明 |
---|---|
sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
sky-pojo | 子模块,存放实体类、VO、DTO等 |
sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
sky-pojo模块介绍
说明 | |
---|---|
Entity | 实体,通常和数据库中的表对应 |
DTO | 数据传输对象,通常用于程序中各层之间传递数据 |
VO | 视图对象,为前端展示数据提供的对象 |
POJO | 普通Java对象,只有属性和对应的getter和setter |
1.2.数据库环境搭建
-
sql文件
表名 含义 employee 员工表 category 分类表 dish 菜品表 dish flavor 菜品口味表 setmeal 套餐表 setmeal_dish 套餐菜品关系表 user 用户表 address book 地址表 shopping_cart 购物车表 orders 订单表 order_detail 订单明细表
1.3.Nginx简介
-
反向代理:将前端发送的动态请求由nginx转发到后端服务器
优点:
- 提高访问数据(缓存)
- 进行负载均衡(大量请求按指定方式分配给集群中的每台服务器,如轮询)
- 保证后端服务安全(后端服务不能直接通过前端访问)
-
反向代理配置方式
-
负载均衡配置方式
1.4.完善登录功能
要求:将密码通过MD5加密方式对明文密码加密后储存,提高安全性
注意:MD5只能单向加密,即正常情况下,只能加密,不能解密
小技巧:发现要做的模块,现在不着急做(及生成代办注释),可以用TODO注释如:
// TODO 后期需要进行md5加密,然后再进行比对
这样我们可以通过窗口看到代办注释
对密码进行MD5加密(EmployeeServiceImpl.java)
// 对明文密码进行MD5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
我们将数据库中的密码改写成MD5加密后的,例如密码为123456,MD5加密后为e10adc3949ba59abbe56e057f20f883e
1.5.导入接口文档
-
前后端分离开发流程
接下来我们用apifox导入api
-
创建项目
-
导入
1.6.Swagger
-
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
官网:https://swagger.io/
Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
-
使用方式
-
导入Knife4j坐标
<!-- Knife4j坐标 --> <dependency> <groupld>com.github.xiaoymin</groupld> <artifactld>knife4j-spring-boot-starter</artifactld> <version>3.0.2</version> </dependency>
-
在配置类中加入knife4j相关配置(sky-server/src/main/java/com/sky/config)
// 通过knife4j生成接口文档 @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; }
-
启动服务后,可以在浏览器输入http://localhost:8080/doc.html,查看生成的接口文档,并进行测试
-
-
Swagger常用注解
注解 说明 @Api 用在类上,例如Controller,表示对类的说明 @ApiModel 用在类上,例如entity、DTO、VO @ApiModelProperty 用在属性上,描述属性信息 @ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用 @RestController @RequestMapping("/admin/employee") @Slf4j // 1. @Api,对类的说明 @Api(tags = "员工相关接口") public class EmployeeController { // ... }
@PostMapping("/login") // 4. @ApiOperation,用在方法上,说明方法的用途、作用 @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) { // ... }
@Data // 2. @ApiModel,用在entity、DTO、VO类上 @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable { // 3. @ApiModelProperty, 用在属性上,描述属性信息 @ApiModelProperty("用户名") private String username; // ... }
添加完毕后,重启程序,文档会发生相应变化
2.员工模块
2.1.新增员工
代码:
-
Controller
@ApiOperation("新增员工") @PostMapping public Result save(@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工:{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); }
-
Service
public interface EmployeeService { // 新增员工 void save(EmployeeDTO employeeDTO); }
// impl // 新增员工 @Override public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); // 对象属性拷贝 BeanUtils.copyProperties(employeeDTO, employee); // 设置账号状态,默认正常状态 employee.setStatus(StatusConstant.ENABLE); // 设置密码,默认密码123456 employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); // 设置当前记录的创建、修改时间 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); // 设置当前记录创建人id和修改人id // TODO 后期需要改为当前登录用户的id employee.setCreateUser(10L); employee.setUpdateUser(10L); employeeMapper.insert(employee); }
-
Mapper
// 插入员工数据 @Insert( "insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user)" + "values" + "(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") void insert(Employee employee);
调试:
-
发送登录请求,获取token
-
配置全局参数
删掉员工登录选修卡,可以看见token被成功添加到请求头
-
测试新增员工接口
数据库成功添加员工信息
前后端联调成功
功能完善:
-
录入的用户名已经存在,抛出异常后未处理
我们可以看到的是,报错类型是SQLIntegrityConstraintViolationException,
捕获这个sql异常并处理
/* sql异常 */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ String message = ex.getMessage(); if(message.contains("Duplicate entry")){ String[] split = message.split(" "); String username = split[2]; return Result.error(username + MessageConstant.ALREADY_EXIT); }else{ return Result.error(MessageConstant.UNKNOWN_ERROR); } }
-
创建人id和修改人id设置为固定值
员工登录成功后,生成JWT令牌并相应给前端
// controller/admin/EmployeeController.java //登录成功后,生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims);
在校验令牌时,拦截器进行拦截并将令牌中的id解析出来
// interceptor/JwtTokenAdminInterceptor.java //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); // 从JWT令牌中解析出id Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; }
我们可以利用ThreadLocal将id传输到我们需要使用id的service实现类中
ThreadLocal并不是Thread而是Thread的局部变量
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
客户端发送的每一次请求都是一个单独的线程
ThreadLocal常用方法 说明 public void set(T value) 设置当前线程的线程局部变量的值 public T get() 返回当前线程的线程局部变量的值 public void remove() 移除当前线程的线程局部变量的值 在外面使用ThreadLocal时,常常会封装成一个工具类
// sky-common/src/main/java/com/sky/context/BaseContext.java public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }
调用ThreadLocal完善功能
//2、校验令牌 try { Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 存入 BaseContext.setCurrentId(empId); } catch (Exception ex) { // ... }
// 设置当前记录创建人id和修改人id employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId());
2.2.员工分页查询
业务规则:
- 根据页码显示员工信息
- 每页展示10条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
代码:
-
Controller
// 员工分页查询 @ApiOperation("员工分页查询") @GetMapping("/page") public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) { log.info("员工分页查询,参数为{}", employeePageQueryDTO); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); }
-
Service
// 员工分页查询 PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
// 员工分页查询 @Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { // 开始分页查询 PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); long total = page.getTotal(); List<Employee> records = page.getResult(); return new PageResult(total, records); }
-
Mapper
// 分页查询 Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<select id="pageQuery" resultType="com.sky.entity.Employee"> select * from employee <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> </where> order by create_time desc </select>
2.3.启用禁用账号
业务规则:
- 启用/禁用的相互切换
- 状态为禁用的员工账号不能登录系统
代码:
-
Controller
// 启用禁用员工账号 @ApiOperation("启用/禁用员工账号") @PostMapping("/status/{status}") public Result startOrStop(@PathVariable Integer status, long id) { log.info("启用/禁用员工账号:{},{}", status, id); employeeService.startOrStop(status, id); return Result.success(); }
-
Service
// 禁用或启用员工 void startOrStop(Integer status, long id);
// 禁用或启用员工 @Override public void startOrStop(Integer status, long id) { // Employee employee = new Employee(); // employee.setId(id); // employee.setStatus(status); Employee employee = Employee.builder() .status(status) .id(id) .build(); // 设置更新时间 employee.setUpdateTime(LocalDateTime.now()); // 设置当前记录修改人id employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.update(employee); }
-
Mapper
// 根据主键动态修改属性 void update(Employee employee);
<update id="update" parameterType="Employee"> update employee <set> <if test="name != null">name = #{name},</if> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="phone != null">phone = #{phone},</if> <if test="sex != null">sex = #{sex},</if> <if test="idNumber != null">id_Number = #{idNumber},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update>
2.4.编辑员工信息
业务规则:
- 根据id查询员工信息
- 编辑员工信息
代码:
-
Controller
// 根据id查询员工 @ApiOperation("根据id查询员工") @GetMapping("/{id}") public Result<Employee> getById(@PathVariable Long id) { log.info("根据id={}, 查询员工",id); Employee employee = employeeService.getById(id); return Result.success(employee); } // 编辑员工信息 @ApiOperation("编辑员工信息") @PutMapping public Result update(@RequestBody EmployeeDTO employeeDTO) { log.info("编辑员工信息:{}",employeeDTO); employeeService.update(employeeDTO); return Result.success(); }
-
Service
// 根据id查询员工 Employee getById(long id); // 更新员工信息 void update(EmployeeDTO employeeDTO);
// 根据id查询员工 @Override public Employee getById(long id) { Employee employee = employeeMapper.getById(id); employee.setPassword("****"); return employee; } @Override public void update(EmployeeDTO employeeDTO) { Employee employee = new Employee(); // 对象属性拷贝 BeanUtils.copyProperties(employeeDTO, employee); // 设置更新时间 employee.setUpdateTime(LocalDateTime.now()); // 设置当前记录修改人id employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.update(employee); }
-
Mapper
// 根据id查询员工 @Select("select * from employee where id = #{id}") Employee getById(Long id);
2.5.公共字段填充
在上面业务表中存在一些公共字段,在更新、插入数据库时需要手动填充,造成代码冗余,我们可以通过自动填充公共字段来解决这个问题
字段名 | 含义 | 数据类型 | 操作类型 |
---|---|---|---|
create_time | 创建时间 | datetime | insert |
create_user | 创建人id | bigint | insert |
update_time | 修改时间 | datetime | insert、update |
update_user | 修改人id | bigint | insert、update |
实现思路:
-
自定义注解 AutoFill,用于标识需要进行公共字段填充的方法
// sky-server/src/main/java/com/sky/annotation/AutoFill.java // 自定义注解,用于标识某个方法需要进行功能字段自动填充 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { // 数据库操作类型:update insert OperationType value(); }
-
自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
// 自定义切面,实现公共字段自动填充处理逻辑 @Aspect @Component @Slf4j public class AutoFillAspect { // 切入点 @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){ } // 前置通知,在通知中进行公共字段的赋值 @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.info("开始进行公共字段自动填充..."); // 1.获取到当前被拦截的方法上的数据库操作类型 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象 AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解 OperationType operationType = autoFill.value(); // 获得数据库操作类型 // 2.获取到当前被拦截的方法的参数--实体对象 Object[] args = joinPoint.getArgs(); if(args == null || args.length == 0){ return; } Object entity = args[0]; // 3.准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); // 4.根据当前不同的操作类型,为对应的属性通过反射赋值 if(operationType == OperationType.INSERT){ // 为4个公共字段赋值 try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); // 通过反射为对象属性赋值 setCreateTime.invoke(entity, now); setCreateUser.invoke(entity, currentId); setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { throw new RuntimeException(e); } }else if(operationType == OperationType.UPDATE){ // 为2个公共字段赋值 try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); // 通过反射为对象属性赋值 setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { throw new RuntimeException(e); } } } }
-
在Mapper 的方法上加入 AutoFl 注解
// 插入员工数据 @AutoFill(value = OperationType.INSERT) @Insert( "insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user)" + "values" + "(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") void insert(Employee employee); // 根据主键动态修改属性 @AutoFill(value = OperationType.UPDATE) void update(Employee employee);
我们再删除Server实现类中手动填充字段的代码,重启程序,更新时间会随着操作变化,则自动填充设置成功
3.菜品分类模块
3.1.前言
虽然这个模块视频中的老师是导入已经写好的代码,但是我还是按照文档手写了一遍,也是收获满满。由于时间原因,前面的代码都是跟着老师敲的,而我又资质愚钝,效果并不是很好。在独立地写完这个模块时,也是花了很多时间去试错(比如分页查询部分,代码一切正常,但是就是没有返回结果,控制台也不报错,排错了很久才发现是Controller中没有返回,也是被自己蠢笑了QwQ)。
在写代码中我也总结出了自己的一些小经验:
-
在选择将查询语句是否写入xml文件中,一般来说,插入和删除一般不会写入xml文件,而更新和查询会。
因为插入和查询语句很简单,一般是根据id来进行操作,而不会判断值是否为空。 -
在最开始的时候我并不理解很什么是DTO和VO的作用,后来也渐渐明白,DTO就是把前端在发送请求所携带的数据时封装的对象,VO是后端给前端发送的对象,因为前端并不会使用数据库的全部数据,Entity是和数据库中的表的字段对应的对象,POJO则是普通的java对象,DTO、VO、Entity都属于POJO
3.2.分类分页查询
-
Controller
@RestController @RequestMapping("/admin/category") @Slf4j @Api(tags = "菜品分类相关接口") public class CategoryController { @Autowired private CategoryService categoryService; // 分类分页查询 @GetMapping("/page") @ApiOperation("菜品分类分页查询") public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO) { log.info("菜品类分页查询,参数为{}", categoryPageQueryDTO); PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO); return Result.success(pageResult); } }
-
Service
public interface CategoryService { // 分类分页查询 PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO); }
// impl @Service public class CategoryServiceImpl implements CategoryService { @Autowired private CategoryMapper categoryMapper; // 分类分页查询 @Override public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) { PageHelper.startPage(categoryPageQueryDTO.getPage(), categoryPageQueryDTO.getPageSize()); Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO); long total = page.getTotal(); List<Category> records = page.getResult(); return new PageResult(total, records); } }
-
Mapper
@Mapper public interface CategoryMapper { // 分类分页查询 Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO); }
<!-- xml --> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.CategoryMapper"> <!-- 菜品分页查询 --> <select id="pageQuery" resultType="com.sky.entity.Category"> select * from category <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="type != null and type != ''"> and type = #{type} </if> </where> order by create_time desc </select> </mapper>
3.3.修改菜品分类
-
Controller
// 修改分类 @PutMapping @ApiOperation("修改分类") public Result update(@RequestBody CategoryDTO categoryDTO) { log.info("修改分类:{}", categoryDTO); categoryService.update(categoryDTO); return Result.success(); }
-
Service
// 修改分类 void update(CategoryDTO categoryDTO);
// 修改分类 @Override public void update(CategoryDTO categoryDTO) { Category category = new Category(); // 对象属性拷贝 BeanUtils.copyProperties(categoryDTO, category); categoryMapper.update(category); }
-
Mapper
// 修改分类 @AutoFill(value = OperationType.UPDATE) void update(Category category);
<!-- xml --> <!-- 修改分类 --> <update id="update"> update category <set> <if test="type != null">type = #{type},</if> <if test="name != null">name = #{name},</if> <if test="sort != null">sort = #{sort},</if> <if test="status != null">status = #{status},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateUser != null">update_user = #{updateUser}</if> </set> where id = #{id} </update>
3.4.启用禁用分类
-
Controller
// 启用、禁用分类 @PostMapping("/status/{status}") @ApiOperation("启用/禁用分类") public Result startOrStop(@PathVariable Integer status, long id) { log.info("禁用/启用菜品分类:{},{}", id, status); categoryService.startOrStop(id, status); return Result.success(); }
-
Service
// 启用、禁用分类 void startOrStop(long id, Integer status);
// 启用、禁用分类 @Override public void startOrStop(long id, Integer status) { Category category = Category.builder() .id(id) .status(status) .build(); categoryMapper.update(category); }
3.5.新增分类
-
Controller
// 新增分类 @PostMapping @ApiOperation("新增分类") public Result save(@RequestBody CategoryDTO categoryDTO) { log.info("新增分类:{}", categoryDTO); categoryService.save(categoryDTO); return Result.success(); }
-
Service
// 新增分类 void save(CategoryDTO categoryDTO);
// 新增分类 @Override public void save(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO, category); categoryMapper.save(category); }
-
Mapper
// 新增分类 @AutoFill(value = OperationType.INSERT) @Insert("insert into category (type, name, sort, status, create_time, create_user, update_time, update_user) " + "value (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{createUser}, #{updateTime}, #{updateUser})") void save(Category category);
3.6.根据id删除分类
-
Controller
// 根据id删除分类 @DeleteMapping @ApiOperation("根据id删除分类") public Result delete(long id){ log.info("根据id={}删除菜品", id); categoryService.deleteById(id); return Result.success(); }
-
Service
// 根据id删除分类 void deleteById(long id);
// 新增分类 @Override public void save(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO, category); categoryMapper.save(category); }
-
Mapper
// 根据id删除分类 @Delete("delete from category where id = #{id}") void delete(long id);
3.7.根据类型查询分类
-
Controller
// 根据类型查询分类 @GetMapping("/list") @ApiOperation("根据类型查询分类") public Result<List<Category>> list(String type) { log.info("根据类型type={}查询分类", type); List<Category> list = categoryService.list(type); return Result.success(list); }
-
Service
// 根据类型查询 List<Category> list(String type);
// impl // 根据类型查询 @Override public List<Category> list(String type) { return categoryMapper.list(type); }
-
Mapper
// 根据类型查询 List<Category> list(String type);
<!-- 根据类型查询 --> <select id="list" resultType="Category"> select * from category where status = 1 <if test="type != null"> and type = #{type} </if> order by sort asc,create_time desc </select>
4.菜品模块
4.1.菜品分页查询
-
Controller
// 菜品分页查询 @GetMapping("page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}",dishPageQueryDTO); PageResult page = dishService.page(dishPageQueryDTO); return Result.success(page); }
-
Service
// 菜品分页查询 PageResult page(DishPageQueryDTO dishPageQueryDTO);
// impl // 菜品分页查询 @Override public PageResult page(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<Dish> page = dishMapper.page(dishPageQueryDTO); long total = page.getTotal(); List<Dish> records = page.getResult(); return new PageResult(total, records); }
-
Mappper
// 分类分页查询 Page<Dish> page(DishPageQueryDTO dishPageQueryDTO);
<!-- xml --> <select id="page" resultType="com.sky.entity.Dish"> select * from dish <where> <if test="name != null"> name like concat('%', #{name}, '%' );</if> <if test="categoryId != null">category_id = #{categoryId};</if> <if test="status != null">status = #{status};</if> </where> </select>
4.2.根据id查询菜品
-
Controller
// 根据id查询菜品 @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result<DishVO> getById(@PathVariable Long id){ log.info("根据id查询菜品:{}",id); DishVO list = dishService.getById(id); return Result.success(list); }
-
Service
// 根据id查询菜品 DishVO getById(Long id);
// impl // 根据id查询菜品 @Override public DishVO getById(Long id) { return dishMapper.getById(id); }
-
Mappper
// 根据id查询菜品 @Select("select * from dish where id = #{id}") DishVO getById(Long categoryId);
4.3.修改菜品
-
Controller
// 修改菜品 @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}",dishDTO); dishService.update(dishDTO); return Result.success(); }
-
Service
// 修改菜品 void update(DishDTO dishDTO);
// impl // 修改菜品 @Override public void update(DishDTO dishDTO) { Dish dish = Dish.builder() .id(dishDTO.getId()) .name(dishDTO.getName()) .price(dishDTO.getPrice()) .image(dishDTO.getImage()) .description(dishDTO.getDescription()) .status(dishDTO.getStatus()) .categoryId(dishDTO.getCategoryId()) .build(); dishMapper.update(dish); }
-
Mappper
// 修改菜品 @AutoFill(OperationType.UPDATE) void update(Dish dish);
<!-- xml --> <!-- 修改菜品 --> <update id="update"> update dish <set> <if test="name != null">name = #{name},</if> <if test="categoryId != null">category_id = #{categoryId},</if> <if test="price != null">price = #{price},</if> <if test="image != null">image = #{image},</if> <if test="description != null">description = #{description},</if> <if test="status != null">status = #{status},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateUser != null">update_user = #{updateUser}</if> </set> </update>
4.4.菜品起售停售
-
Controller
// 菜品起售停售 @PostMapping("status/{status}") @ApiOperation("菜品起售停售") public Result startOrStop(@PathVariable Integer status, Long id){ log.info("菜品起售停售:{}, {}", status, id); dishService.startOrStop(id, status); return Result.success(); }
-
Service
// 菜品起售停售 void startOrStop(Long id, Integer status);
// impl // 菜品起售停售 @Override public void startOrStop(Long id, Integer status) { Dish dish = Dish.builder() .id(id) .status(status) .build(); dishMapper.update(dish); if(status == StatusConstant.DISABLE){ // 如果是停售操作,还需要将包含当前菜品的套餐也停售 List<Long> dishIds = new ArrayList<>(); dishIds.add(id); List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds); if(setmealIds != null && setmealIds.size() > 0){ for(Long setmealId : setmealIds){ Setmeal setmeal = Setmeal.builder() .id(setmealId) .status(StatusConstant.DISABLE) .build(); setmealDishMapper.update(setmeal); } } } }
-
Mapper
// sky-server/src/main/java/com/sky/mapper/SetmealDishMapper.java // 根据id修改套餐 @AutoFill(OperationType.UPDATE) void update(Setmeal setmeal);
<!-- sky-server/src/main/resources/mapper/SetmealDishMapper.xml --> <!-- 根据id修改套餐 --> <update id="update"> update setmeal <set> <if test="name != null">name = #{name}</if> <if test="name != null">category_id = #{categoryId}</if> <if test="name != null">price = #{price}</if> <if test="name != null">status = #{status}</if> <if test="name != null">description = #{description}</if> <if test="name != null">image = #{image}</if> <if test="name != null">update_time = #{updateTime}</if> <if test="name != null">update_user = #{updateUser}</if> </set> </update>
4.5.文件上传
-
配置oss
sky: alioss: endpoint: ${sky.alioss.endpoint} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret} bucket-name: ${sky.alioss.bucket-name}
配置属性类
// sky-common/src/main/java/com/sky/properties/AliOssProperties.java @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
# sky-server/src/main/resources/application-dev.yml sky: alioss: endpoint: oss-cn-beijing.aliyuncs.com access-key-id: LTAI5tKr2ZB866rBJea9ucuJ access-key-secret: gLx3lSngXHXv5SUyBW2c29XhHL5XQq bucket-name: sky-jiaqi
-
Controller
在项目的sky-common/src/main/java/com/sky/utils/AliOssUtil.java中,已经写好了请求oss的工具类,我们在yml文件中配置好bucket仓库,然后调用公里类中的upload方法上传文件即可
// sky-server/src/main/java/com/sky/controller/admin/CommonController.java // 通用接口 @RestController @Slf4j @RequestMapping("/admin/common") @Api(tags = "通用接口") public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info("文件上传:{}", file); try { // 原始文件名 String originalFilename = file.getOriginalFilename(); // 截取原始文件名的后缀 String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); // 构造新的文件名 String objectName = UUID.randomUUID().toString() + extension; // 文件请求路径 String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e); } return Result.error(MessageConstant.UPLOAD_FAILED); } }
4.6.新增菜品
业务规则:
- 菜品名称必须统一
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据1情况选择菜品的口味
- 每个菜品必须对应一张图片
-
Controller
// 新增菜品 @PostMapping() @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO) { log.info("新增菜品{}", dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); }
-
Service
// 修改菜品 void update(DishDTO dishDTO);
// impl // 新增菜品 @Override @Transactional public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); // 向菜品表插入一条数据 dishMapper.insert(dish); // 获取insert语句生成的主键值 Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(flavor -> { flavor.setDishId(dishId); }); // 向口味表插入n条数据 dishFlavorMapper.insertBatch(flavors); } }
-
Mappper
// 向菜品表插入一条数据 @AutoFill(OperationType.INSERT) void insert(Dish dish);
<!-- xml --> <!-- 向菜品表中插入一条数据 --> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, create_time, update_time, create_user, update_user, status) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}); </insert>
@Mapper public interface DishFlavorMapper { // 向口味表插入n条数据 void insertBatch(List<DishFlavor> flavors); }
<!-- xml --> <mapper namespace="com.sky.mapper.DishFlavorMapper"> <insert id="insertBatch"> insert into dish_flavor (dish_id, name, value) values <foreach collection="flavors" item="df" separator=","> (#{df.dishId}, #{df.name}, #{df.value}) </foreach> </insert> </mapper>
4.7.批量删除菜品
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
代码
-
Controller
// 菜品批量删除 @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); return Result.success(); }
-
Service
// 菜品批量删除 void deleteBatch(List<Long> ids);
// impl // 菜品批量删除 @Override @Transactional public void deleteBatch(List<Long> ids) { // 是否存在起售中的菜品 for (Long id : ids) { Dish dish = dishMapper.getById(id); if(Objects.equals(dish.getStatus(), StatusConstant.ENABLE)){ // 当前菜品处于起售中,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } // 判断菜品是否关联套餐 List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIds != null && setmealIds.size() > 0 ){ // 当前菜品被套餐关联 throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH); } // 删除菜品表中的菜品数据 for (Long id : ids) { dishMapper.deleteById(id); // 删除菜品关联的口味数据 dishFlavorMapper.deleteByDishId(id); } }
-
Mappper
-
根据菜品id查询套餐id
// sky-server/src/main/java/com/sky/mapper/SetmealDishMapper.java // 根据菜品id查询套餐id List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
<!-- sky-server/src/main/resources/mapper/SetmealDishMapper.xml --> <!-- xml --> <!-- 根据菜品id查询套餐id --> <select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="dishIds" item="dishId" separator="," open="(" close=")"> #{dishId} </foreach> </select>
-
删除
// 根据id删除菜品表数据 @Delete("delete from dish where id = #{id}") void deleteById(Long id);
// 删除菜品关联的口味数据 @Delete("delete from dish_flavor where dish_id = #{dish_id}") void deleteByDishId(Long dishId);
-
5.Redis
Redis是一个基于内存的 key-value 结构数据库。
- 基于内存存储,读写性能高
- 适合存储热点数据 (热点商品、资讯 新闻)
- 企业应用广泛
下载地址:
- Windows版下载地址:https://github.com/microsoftarchive/redis/releases
- Linux版下载地址:https://download.redis.io/releasesl
-
解压后目录
-
启动redis服务(默认端口号为6379)
在安装的目录下运行
redis-server.exe redis.windows.conf
可以修改密码(也可以不设置)
在后面对Redis的操作,我们大多采用软件Another Redis Desktop Manager来完成
5.1.数据类型
- 字符串(string):普通字符串,Redis中最简单的数据类型
- 哈希(hash):也叫散列,类似于Java中的HashMap结构
- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
- 有序集合(sorted set / zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
5.2.常用命令
-
字符串
命令 说明 SET key value 设置指定key的值 GET key 获取指定key的值 SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒 SETNX key value 只有在 key 不存在时设置 key 的值 -
哈希
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象
key相对于对象名,field相当于属性名,value相当于属性值
命令 说明 HSET key field value 将哈希表 key 中的字段field 的值设为 value HGET key field 获取存储在哈希表中指定字段的值 HDEL key field 删除存储在哈希表中的指定字段 HKEYS key 获取哈希表中所有字段 HVALS key 获取哈希表中所有值 -
列表
Redis 列表是简单的字符串列表,按照插入顺序排序
命令 说明 LPUSH key value1 [value2] 将一个或多个值插入到列表头部 LRANGE key start stop 获取列表指定范围内的元素(从0开始) RPOP key 移除并获取列表最后一个元素 LLEN key 获取列表长度 -
集合
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据
命令 说明 SADD key member1 [member2] 向集合添加一个或多个成员 SMEMBERS key 返回集合中的所有成员 SCARD key 获取集合的成员数 SINTER key1 [key2] 返回给定所有集合的交集 SUNION key1 [key2] 返回所有给定集合的并集 SREM key member1 [member2] 删除集合中一个或多个成员 -
有序集合
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。
命令 说明 ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员 ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员 ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment ZREM key member [member …] 移除有序集合中的一个或多个成员 -
通用命令
命令 说明 KEYS pattern 查找所有符合给定模式( pattern)的 key,通配符为 *
EXISTS key 检查给定 key 是否存在 TYPE key 返回 key 所储存的值的类型 DEL key 该命令用于在 key 存在时删除 key
5.3.java中操作Redis
常用的redis的java客户端:
- Jedis
- Lettuce
- Spring Data Redis(推荐)
操作步骤:
-
导入Spring Data Redis 的maven坐标(初始项目已经导入)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置Redis数据源()
database配置项,在启动redis服务后,默认创建16个数据库(0~15),默认为0、
# application.yml # 该配置项与datasource平级 redis: host: ${sky.redis.host} port: ${sky.redis.port} # password: 这里我没有设置密码,所有没有写 database: ${sky.redis.database}
# application-dev.yml # 该配置项与datasource平级 redis: host: localhost port: 6379 # password: 这里我没有设置密码,所有没有写 database: 0
-
编写配置类,创建RedisTemplate对象
// sky-server/src/main/java/com/sky/config/RedisConfiguration.java @Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { log.info("开始创建redis模版对象"); RedisTemplate redisTemplate = new RedisTemplate(); // 设置redis的连接工程对象 redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
-
通过RedisTemplate对象操作Redis
在对数据进行操作时,可以将对象先复制,减少代码冗余
@Test public void test() { // 操作字符串的对象 ValueOperations valueOperations = redisTemplate.opsForValue(); // 操作哈希的对象 HashOperations hashOperations = redisTemplate.opsForHash(); // 操作列表的对象 ListOperations listOperations = redisTemplate.opsForList(); // 操作集合的对象 SetOperations setOperations = redisTemplate.opsForSet(); // 操作有序集合的对象 ZSetOperations zSetOperations = redisTemplate.opsForZSet(); }
对字符串类型操作
@Test public void testString() { // set(添加) redisTemplate.opsForValue().set("key1", "value1"); // get(获取) String city = (String) redisTemplate.opsForValue().get("key1"); System.out.println(city); // setex(限时字符串) redisTemplate.opsForValue().set("key2", "111", 60, TimeUnit.SECONDS); // setnx(没有就添加字符串) redisTemplate.opsForValue().setIfAbsent("lock", "1"); // 因为上面已经设置了lock,所有这里不会设置成功 redisTemplate.opsForValue().setIfAbsent("lock", "2"); }
对hash类型操作
// 操作hash类型的数据 @Test public void testHsh() { HashOperations hashOperations = redisTemplate.opsForHash(); // hset(添加) hashOperations.put("ob", "name", "Tom"); hashOperations.put("ob", "age", "18"); // hget(获取) String name = (String)hashOperations.get("ob", "name"); System.out.println(name); // hvals(获取该键的所有values) List values = hashOperations.values("ob"); System.out.println(values); // hdel(删除) hashOperations.delete("ob", "name"); }
对列表类型操作
@Test public void testList() { ListOperations listOperations = redisTemplate.opsForList(); // lpush(添加元素) listOperations.leftPush("lst", "1"); listOperations.leftPushAll("lst", "2", "3", "4"); // lrange(范围获取) List list = listOperations.range("lst", 0, -1); System.out.println(list); //[4, 3, 2, 1] // rpop(右弹出一个元素) Object i1 = listOperations.rightPop("lst"); System.out.println(i1); // 1 // llen(列表元素个数) Long len = listOperations.size("lst"); System.out.println(len); // 3 }
对集合进行操作
@Test public void testSet() { SetOperations setOperations = redisTemplate.opsForSet(); // sadd(添加) setOperations.add("set1", "a", "b", "c"); setOperations.add("set2", "d", "e", "f"); // smembers(获取集合元素) Set members = setOperations.members("set1"); System.out.println(members); // [b, c, a] // scard(元素个数) Long size = setOperations.size("set1"); System.out.println(size); // 3 // sinter(求交集) Set intersect = setOperations.intersect("set1", "set2"); System.out.println(intersect); // [] // sunion(求并集) Set union = setOperations.union("set1", "set2"); System.out.println(union); // [b, a, c, d, f, e] // srem(移除元素) setOperations.remove("set1", "a"); }
对有序集合进行操作
@Test public void testZSet() { ZSetOperations zSetOperations = redisTemplate.opsForZSet(); // zadd(添加) zSetOperations.add("zset1", "a", 1); zSetOperations.add("zset1", "b", 2); zSetOperations.add("zset1", "c", 3); // zincrby(赋权重) zSetOperations.incrementScore("zset1", "a", 3); zSetOperations.incrementScore("zset1", "b", 2); zSetOperations.incrementScore("zset1", "c", 1); // zrange(范围获取) Set orderset = zSetOperations.range("zset1", 0, -1); // 最先打印有序列表权重最大的元素 System.out.println(orderset); // [a, b, c] // zrem(删除) zSetOperations.remove("zset1", "a"); }
通用命令操作
@Test public void testCommon() { // keys(获取key) Set keys = redisTemplate.keys("*"); System.out.println(keys); // [ob, name, set1, obj, lock, set2, key1, zset1, lst] // exists(查询key是否存在) redisTemplate.hasKey("name"); redisTemplate.hasKey("set1"); // type(查看key的类型) for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type); // HASH STRING... } // del(删除) redisTemplate.delete("name"); }
5.4.店铺模块
业务规则:
-
管理端设置、查询营业状态
@RestController("adminShopController") @RequestMapping("/admin/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController { public static final String KEY = "shop_status"; @Autowired private RedisTemplate redisTemplate; // 设置店铺营业状态 @PutMapping("/{status}") @ApiOperation("设置店铺营业状态") public Result setStatus(@PathVariable Integer status) { log.info("设置店铺营业状态:{}", status == 1?"营业中":"打烊中"); redisTemplate.opsForValue().set(KEY, status); return Result.success(); } // 查询店铺状态 @GetMapping("/status") @ApiOperation("获取店铺营业状态") public Result<Integer> getStatus() { Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}", shopStatus == 1? "营业中":"打烊中"); return Result.success(shopStatus); } }
-
用户端查询营业状态
@RestController("userShopController") @RequestMapping("/user/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController { public static final String KEY = "shop_status"; @Autowired private RedisTemplate redisTemplate; // 查询店铺状态 @GetMapping("/status") @ApiOperation("获取店铺营业状态") public Result<Integer> getStatus() { Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}", shopStatus == 1? "营业中":"打烊中"); return Result.success(shopStatus); } }
-
我们可以通过修改WebMvcConfiguration.config配置文件,将接口分为管理端和用户端
// 将docket拆分为docket1、docket2两个文档,并进行分组 @Bean public Docket docket1() { log.info("准备生成接口文档..."); ApiInfo apiInfo = new ApiInfoBuilder() // 省略... Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") // 省略... return docket; } @Bean public Docket docket2() { log.info("准备生成接口文档..."); ApiInfo apiInfo = new ApiInfoBuilder() // 省略... Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") // 省略... return docket; }
分组完成
5.5 关于Redis启动错误
报错:Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
- D:\APP\Redis\Redis-x64-3.2.100>
redis-cli.exe
- 127.0.0.1:6379>
shutdown
- not connected>
exit