Java项目苍穹外卖:接口开发(1)

员工管理

新增员工

产品原型

接口设计

【注意】本项目约定:

管理端发出的请求,统一使用/admin作为前缀。

用户端发出的请求,统一使用/user作为前缀。

数据库设计(employee表)

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status Int 账号状态 1正常 0锁定
create_time Datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

代码开发

1.根据新增员工接口设计对应的DTO

1
2
3
4
5
6
7
8
9
10
11
package com.sky.dto;//sky-pojo工程

@Data
public class EmployeeDTO implements Serializable {//序列化
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}

(1)注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据。

使用DTO的好处:假设你数据库中定义了User类,包含用户名、密码、邮箱、手机号等等;当用户登录时一般只需要输入用户名和密码,那么传入服务端的用户名和密码就可以在controller层封装到UserDto实体类中。DTO解决了在客户端和服务器端之间传递大量数据的问题,但是客户端往往需要更细粒度的数据访问。

DTO数据传输对象详解_dto撖寡情-CSDN博客

Spring Boot中的数据传输对象(DTO)_springboot dto-CSDN博客

(2)序列化Serializable

Serializable是什么,为什么要实现Serializable接口?-CSDN博客

2.后端统一返回结果Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.sky.result;//sky-common工程

//后端统一返回结果
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据

public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}

public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}

public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}

3.在EmployeeController中创建新增员工方法,接收前端提交的参数。

1
2
3
4
5
6
7
8
//新增员工
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}

4.在EmployeeService接口中声明新增员工方法。

1
2
//新增员工
void save(EmployeeDTO employeeDTO);

5.在EmployeeServiceImpl中实现新增员工方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);
//设置账号的状态,默认正常状态,1表示正常,0表示锁定
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);
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.insert(employee);
}

BeanUtils.copyPropertiesSpring BeanUtils:灵活高效的JavaBean操作助手-CSDN博客

6.在EmployeeMapper中声明insert方法。

1
2
3
4
//插入员工数据
@Insert("insert into employee (name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user)" +
"VALUES (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime},#{updateTime},#{createUser}, #{updateUser})")
void insert(Employee employee);
处理已存在用户名异常

通过全局异常处理器来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.sky.handler;//sky-server工程

//全局异常处理器,处理项目中抛出的业务异常
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

//处理SQL异常
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];//第三个单词为username
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
设置创建人id和修改人id

需要动态获取当前登录员工的id

员工登录成功后会生成JWT令牌并响应给前端:

1
2
3
4
5
6
7
8
//EmployeeController
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);

后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//JwtTokenAdminInterceptor
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
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

ThreadLocal并不是一个Thread,而是Thread局部变量

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

  • public void set(T value):设置当前线程的线程局部变量的值。
  • public T get():返回当前线程所对应的线程局部变量的值。
  • public void remove():移除当前线程的线程局部变量。

注意:客户端发送的每次请求,后端的Tomcat服务器都会分配一个单独的线程来处理请求

参考链接:

史上最全ThreadLocal详解(一)-CSDN博客

Java 中的 ThreadLocal 是如何实现线程资源隔离的? - 面试鸭 - 程序员求职面试刷题神器

Java四大引用:

Java:强引用,软引用,弱引用和虚引用_强弱引用-CSDN博客

初始工程中已经封装了ThreadLocal操作的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sky.context;//sky-common工程

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();
}
}

在拦截器中解析出当前登录员工id,并放入线程局部变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//sky-server工程:package com.sky.interceptor;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);//添加这句代码即可!!!
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}

Service中获取线程局部变量中的值。

功能测试

1.通过接口文档测试。

由于JWT令牌校验失败,导致EmployeeControllersave方法没有被调用。

解决办法:调用员工登录接口获得一个合法的JWT令牌,将合法的JWT令牌添加到全局参数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.sky.interceptor;//sky-server工程

//jwt令牌校验的拦截器
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;

//校验jwt
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
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;
}
}
}

2.通过前后端联调测试。

注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。

员工分页查询

产品原型

接口设计

代码开发

1.根据分页查询接口设计对应的DTO

1
2
3
4
5
6
7
8
package com.sky.dto;//sky-pojo工程

@Data
public class EmployeePageQueryDTO implements Serializable {
private String name;//员工姓名
private int page;//页码
private int pageSize;//每页显示记录数
}

2.后面所有的分页查询,统一都封装成PageResult对象。

1
2
3
4
5
6
7
8
9
10
package com.sky.result;

//封装分页查询结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}

3.员工信息分页查询后端返回的对象类型为:Result<PageResult>。根据接口定义创建分页查询方法。

1
2
3
4
5
6
7
8
//员工分页查询
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}

4.在EmployeeService接口中声明pageQuery方法。

1
2
//员工分页查询
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

5.在EmployeeServiceImpl中实现pageQuery方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//员工分页查询
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
//Page类是继承ArrayList的集合
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total, records);
}

注意:此处使用mybatis的分页插件PageHelper来简化分页代码的开发。底层基于mybatis的拦截器实现。

【PageHelp原理】PageHelp底层是基于ThreadLocal实现的,通过把page(包含pageNumpageSize)存储到存储空间,在进行分页查询之前,通过ThreadLocalpageNumpageSize取出,然后在SQL语句查询时动态的把limit关键字拼进去。

6.在EmployeeMapper中声明pageQuery方法。

1
2
//分页查询
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

7.在EmployeeMapper.xml(路径:sky-server/src/main/resources/mapper/EmployeeMapper.xml)中编写SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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.EmployeeMapper">
<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>
</mapper>
操作时间字段显示错误

解决方式:

方式一:在属性上加入注解,对日期进行格式化。

1
2
3
4
5
6
7
8
9
10
package com.sky.entity;

public class Employee implements Serializable {
//其他的属性省略了
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

方式二:在WebMvcConfiguration中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sky.config;//sky-server工程

//配置类,注册web层相关组件
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
//扩展Spring MVC框架的消息转换器
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
log.info("扩展消息转换器");
//创建消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入容器中,设置索引0,表示该消息转化器顺序排在第一位,优先使用
converters.add(0, converter);
}
}

【补充】

1.@DateTimeFormat@JsonFormat

参考链接:

@JsonFormat 和 @DateTimeFormat 时间格式化注解详解(不看血亏)_jsonformat注解-CSDN博客

Spring @DateTimeFormat日期格式化时注解浅析分享_datetimeformatter注解用法-CSDN博客

总结:@DateTimeFormat只是规定了前端传给后端的时间格式,但是@JsonFormat才能控制前端如何显示时间。

2.消息转换器

参考链接:

一步到位 SpringBoot 序列化与消息转换器 (你需要的这里都有)_objectmapper和messageconverter的关系与区别-CSDN博客

Spring MVC 消息转换器_springmvc消息转换器-CSDN博客

WebMvcConfigurer和WebMvcConfigurationSupport(MVC配置)-CSDN博客

重学SpringBoot3-WebMvcAutoConfiguration类-CSDN博客

@DateTimeFormat:当从requestParam中获取string参数并需要转化为Date类型时,会根据此注解的参数pattern的格式进行转化。

@JsonFormat:当从请求体中获取json字符序列,需要反序列化为对象时,时间类型会按照这个注解的属性内容进行处理。

这两个注解需要加在实体类的对应字段上即可。

3.自定义序列化

参考链接:

【自定义序列化器】⭐️通过继承JsonSerializer和实现WebMvcConfigurer类完成自定义序列化_webmvcconfigurer 自定义jackson-CSDN博客

SpringBoot中Jackson实现自定义序列化和反序列化总结_jackson 自定义反序列化-CSDN博客

实现自定义序列化和反序列化控制的5种方式-腾讯云开发者社区-腾讯云

启用禁用员工账号

产品原型

业务规则:

  1. 可以对状态为“启用” 的员工账号进行“禁用”操作。
  2. 可以对状态为“禁用”的员工账号进行“启用”操作。
  3. 状态为“禁用”的员工账号不能登录系统。

接口设计

代码开发

1.根据接口设计中的请求参数形式对应的在EmployeeController中创建启用禁用员工账号的方法。

1
2
3
4
5
6
7
8
//启用禁用员工账号
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status, Long id){
log.info("启用禁用员工账号:{},{}", status, id);
employeeService.startOrStop(status, id);
return Result.success();
}

2.在EmployeeService接口中声明启用禁用员工账号的业务方法。

1
2
//启用禁用员工账号
void startOrStop(Integer status, Long id);

3.在EmployeeServiceImpl中实现启用禁用员工账号的业务方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//启用禁用员工账号
@Override
public void startOrStop(Integer status, Long id) {
//update employee set status = ? where id = ?
//法一:
/* Employee employee = new Employee();
employee.setStatus(status);
employee.setId(id);*/

//法二:
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}

4.在EmployeeMapper接口中声明update方法。

1
2
//根据主键动态修改属性
void update(Employee employee);

5.在EmployeeMapper.xml中编写SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<update id="update" parameterType="Employee">
-- parameterType可以省略。可以只写Employee,不用写上包名。
-- 因为配置文件设置mybatis: type-aliases-package: com.sky.entity 整体扫描了这个包,
-- Employee就在这个包里面,就统一扫到为这些实体创建了别名,所以可以只写别名Employee,不用写完整的包名
update employee
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</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>

编辑员工

产品原型

接口设计

编辑员工功能涉及到两个接口:

1.根据id查询员工信息。

2.编辑员工信息。

代码开发

1.在EmployeeController中创建getById方法。

1
2
3
4
5
6
7
8
//根据id查询员工信息
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工信息:{}", id);
Employee employee = employeeService.getById(id);
return Result.success(employee);
}

2.在EmployeeService接口中声明getById方法:

1
2
//根据id查询员工信息
Employee getById(Long id);

3.在EmployeeServiceImpl中实现getById方法:

1
2
3
4
5
6
7
//根据id查询员工信息
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");//加密密码,加强安全性
return employee;
}

4.在EmployeeMapper接口中声明getById方法:

1
2
3
//根据id查询员工信息
@Select("select * from employee where id = #{id}")
Employee getById(Long id);

5.在EmployeeController中创建update方法:

1
2
3
4
5
6
7
8
//编辑员工信息
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO) {
log.info("编辑员工信息:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}

6.在EmployeeService接口中声明update方法:

1
2
//编辑员工信息
void update(EmployeeDTO employeeDTO);

7.在EmployeeServiceImpl中实现update方法:

1
2
3
4
5
6
7
8
9
//编辑员工信息
@Override
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}

分类模块

产品原型

业务规则

  • 分类名称必须是唯一的。
  • 分类按照类型可以分为菜品分类和套餐分类。
  • 新添加的分类状态默认为“禁用”。

接口设计

  • 新增分类
  • 分类分页查询
  • 根据id删除分类
  • 修改分类
  • 启用禁用分类
  • 根据类型查询分类

数据库设计(category表)

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 分类名称 唯一
type int 分类类型 1菜品分类 2套餐分类
sort int 排序字段 用于分类数据的排序
status int 状态 1启用 0禁用
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

代码开发

1.CategoryController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.sky.controller.admin;

//分类管理
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

@Autowired
private CategoryService categoryService;

/**
* 新增分类
* @param categoryDTO
* @return
*/
@PostMapping
@ApiOperation("新增分类")
public Result<String> save(@RequestBody CategoryDTO categoryDTO){
log.info("新增分类:{}", categoryDTO);
categoryService.save(categoryDTO);
return Result.success();
}

/**
* 分类分页查询
* @param categoryPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分类分页查询")
public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
log.info("分页查询:{}", categoryPageQueryDTO);
PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
return Result.success(pageResult);
}

/**
* 删除分类
* @param id
* @return
*/
@DeleteMapping
@ApiOperation("删除分类")
public Result<String> deleteById(Long id){
log.info("删除分类:{}", id);
categoryService.deleteById(id);
return Result.success();
}

/**
* 修改分类
* @param categoryDTO
* @return
*/
@PutMapping
@ApiOperation("修改分类")
public Result<String> update(@RequestBody CategoryDTO categoryDTO){
categoryService.update(categoryDTO);
return Result.success();
}

/**
* 启用、禁用分类
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用分类")
public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
categoryService.startOrStop(status,id);
return Result.success();
}

/**
* 根据类型查询分类
* @param type
* @return
*/
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
List<Category> list = categoryService.list(type);
return Result.success(list);
}
}

2.CategoryMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.sky.mapper;

@Mapper
public interface CategoryMapper {

/**
* 插入数据
* @param category
*/
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Category category);

/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

/**
* 根据id删除分类
* @param id
*/
@Delete("delete from category where id = #{id}")
void deleteById(Long id);

/**
* 根据id修改分类
* @param category
*/
void update(Category category);

/**
* 根据类型查询分类
* @param type
* @return
*/
List<Category> list(Integer type);
}

3.DishMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.mapper;

@Mapper
public interface DishMapper {

/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);

}

4.SetmealMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.mapper;

@Mapper
public interface SetmealMapper {

/**
* 根据分类id查询套餐的数量
* @param id
* @return
*/
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);

}

5.CategoryService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.sky.service;

public interface CategoryService {

/**
* 新增分类
* @param categoryDTO
*/
void save(CategoryDTO categoryDTO);

/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

/**
* 根据id删除分类
* @param id
*/
void deleteById(Long id);

/**
* 修改分类
* @param categoryDTO
*/
void update(CategoryDTO categoryDTO);

/**
* 启用、禁用分类
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);

/**
* 根据类型查询分类
* @param type
* @return
*/
List<Category> list(Integer type);
}

6.CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.sky.service.impl;

//分类业务层
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

@Autowired
private CategoryMapper categoryMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;

/**
* 新增分类
* @param categoryDTO
*/
public void save(CategoryDTO categoryDTO) {
Category category = new Category();
//属性拷贝
BeanUtils.copyProperties(categoryDTO, category);

//分类状态默认为禁用状态0
category.setStatus(StatusConstant.DISABLE);

//设置创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());

categoryMapper.insert(category);
}

/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
//下一条sql进行分页,自动加入limit关键字分页
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}

/**
* 根据id删除分类
* @param id
*/
public void deleteById(Long id) {
//查询当前分类是否关联了菜品,如果关联了就抛出业务异常
Integer count = dishMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
}

//查询当前分类是否关联了套餐,如果关联了就抛出业务异常
count = setmealMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
}

//删除分类数据
categoryMapper.deleteById(id);
}

/**
* 修改分类
* @param categoryDTO
*/
public void update(CategoryDTO categoryDTO) {
Category category = new Category();
BeanUtils.copyProperties(categoryDTO,category);

//设置修改时间、修改人
category.setUpdateTime(LocalDateTime.now());
category.setUpdateUser(BaseContext.getCurrentId());

categoryMapper.update(category);
}

/**
* 启用、禁用分类
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Category category = Category.builder()
.id(id)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
categoryMapper.update(category);
}

/**
* 根据类型查询分类
* @param type
* @return
*/
public List<Category> list(Integer type) {
return categoryMapper.list(type);
}
}

7.CategoryMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?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 = #{type}
</if>
</where>
order by sort asc , create_time desc
</select>

<update id="update" parameterType="Category">
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>

<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>
</mapper>

菜品管理

公共字段自动填充

问题分析

业务表中的公共字段:

序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人id bigint
3 update_time 修改时间 datetime
4 update_user 修改人id bigint

公共代码:

1
2
3
4
5
//设置当前记录的创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());

实现思路

序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update
  • 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法。
  • 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。
  • Mapper的方法上加入AutoFill注解。

技术点:枚举、注解、AOP、反射。

代码开发

1.自定义注解AutoFill

1
2
3
4
5
6
7
8
9
package com.sky.annotation;

//自定义注解,用于标识某个方法需要进行功能字段自动填充处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}

2.自定义切面AutoFillAspect,完善自定义切面AutoFillAspectautoFill方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.sky.aspect;

//自定义切面,实现公共字段自动填充处理逻辑
@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("开始进行公共字段自动填充");

//获取到当前被拦截的方法上的数据库操作类型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型

//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity = args[0];//约定传进来的第一个参数就是Employee对象

//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();

//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT) {
//为4个公共字段赋值
try{
Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getMethod(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) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE) {
//为2个公共字段赋值
try{
Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

3.在Mapper接口的方法上加入AutoFill注解。(CategoryMapper.javaEmployeeMapper.java的插入和更新操作都需要添加AutoFill注解)

1
2
3
4
5
6
7
8
9
//插入员工数据
@Insert("insert into employee (name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user)" +
"VALUES (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime},#{updateTime},#{createUser}, #{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);

//根据主键动态修改属性
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);

4.将业务层为公共字段赋值的代码注释掉。

新增菜品

产品原型

业务规则:

  1. 菜品名称必须是唯一的。
  2. 菜品必须属于某个分类下,不能单独存在。
  3. 新增菜品时可以根据情况选择菜品的口味。
  4. 每个菜品必须对应一张图片。

接口设计

根据类型查询分类(已完成)

文件上传

新增菜品

数据库设计

dish菜品表
字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 菜品名称 唯一
category_id bigint 分类id 逻辑外键
price decimal(10,2) 菜品价格
image varchar(255) 图片路径
description varchar(255) 菜品描述
status int 售卖状态 1起售 0停售
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id
dish_flavor口味表
字段名 数据类型 说明 备注
id bigint 主键 自增
dish_id bigint 菜品id 逻辑外键
name varchar(32) 口味名称
value varchar(255) 口味值

代码开发

文件上传接口

application-dev.yml

1
2
3
4
5
6
sky:
alioss:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id:
access-key-secret:
bucket-name: srr-web-tlias

application.yml

1
2
3
4
5
6
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}

OssConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sky.config;

//配置类,用于创建AliOssUtil对象
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean //保证整个spring容器里面只有一个AliOssUtil对象
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}

CommonController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.sky.controller.admin;

//通用接口
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
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 (Exception e){
log.error("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
新增菜品接口

1.根据新增菜品接口设计对应的DTODishDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.sky.dto;

@Data
public class DishDTO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//口味
private List<DishFlavor> flavors = new ArrayList<>();
}

2.DishController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sky.controller.admin;

//菜品管理
@RestController
@RequestMapping("/admin/dish")
@Api("菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
//新增菜品
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}

3.DishService

1
2
3
4
5
6
7
package com.sky.service;

@Service
public interface DishService {
//新增菜品和对应的口味
public void saveWithFlavor(DishDTO dishDTO);
}

4.DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.sky.service.impl;

@Service
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
//新增菜品和对应的口味
@Override
@Transactional //设计多个数据表的操作,使用事务注解保证数据的一致性
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入1条数据
dishMapper.insert(dish);

//获取insert语句生成的主键值
Long dishId = dish.getId();

List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {//设置每个口味对应的菜品的id
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
}

5.DishMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sky.mapper;

@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);

//插入菜品数据
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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.DishMapper">
<!--
useGeneratedKeys:true 表示获取这条语句insert语句执行后生成的主键值
keyProperty="id" 表示将主键值赋给传进来的对象的id属性
-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (status, name, category_id, price, image, description, create_time, update_time, create_user,update_user)
values (#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
</insert>
</mapper>

6.DishFlavorMapper

1
2
3
4
5
6
7
package com.sky.mapper;

@Mapper
public interface DishFlavorMapper {
//批量插入口味数据
void insertBatch(List<DishFlavor> flavors);
}
1
2
3
4
5
6
7
8
9
10
11
<?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.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="dishFlavor" separator=",">
(#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value})
</foreach>
</insert>
</mapper>

菜品分页查询

产品原型

业务规则:

  1. 根据页码展示菜品信息。
  2. 每页展示10条数据。
  3. 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询。

接口设计

代码开发

1.根据菜品分页查询接口定义设计对应的DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sky.dto;

@Data
public class DishPageQueryDTO implements Serializable {
private int page;
private int pageSize;
private String name;
//分类id
private Integer categoryId;
//状态 0表示禁用 1表示启用
private Integer status;

}

2.根据菜品分页查询接口定义设计对应的VO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.sky.vo;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//更新时间
private LocalDateTime updateTime;
//分类名称
private String categoryName;
//菜品关联的口味
private List<DishFlavor> flavors = new ArrayList<>();

//private Integer copies;
}

3.根据接口定义创建DishControllerpage分页查询方法。

1
2
3
4
5
6
7
8
//菜品分页查询
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}

4.在DishService中扩展分页查询方法。

1
2
//菜品分页查询
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

5.在DishServiceImpl中实现分页查询方法。

1
2
3
4
5
6
7
//菜品分页查询
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}

6.在DishMapper接口中声明pageQuery方法。

1
2
//菜品分页查询
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

7.在DishMapper.xml中编写SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*, c.name categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>

删除菜品

产品原型

业务规则:

  1. 可以一次删除一个菜品,也可以批量删除菜品。
  2. 起售中的菜品不能删除。
  3. 被套餐关联的菜品不能删除。
  4. 删除菜品后,关联的口味数据也需要删除掉。

接口设计

数据库设计

image-20241108112931152

代码开发

1.根据删除菜品的接口定义在DishController中创建方法。

1
2
3
4
5
6
7
8
//菜品批量删除
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}

2.在DishService接口中声明deleteBatch方法。

1
2
//菜品批量删除
void deleteBatch(List<Long> ids);

3.在DishServiceImpl中实现deleteBatch方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//菜品批量删除
@Transactional
@Override
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除:是否存在起售中的菜品
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if(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.DISH_BE_RELATED_BY_SETMEAL);
}

//删除菜品表中的菜品数据
//方法一:
/* for(Long id : ids){
dishMapper.deleteById(id);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);
}*/

//方法二:
//根据菜品id集合批量删除菜品数据:delete from dish where id in (?,?,?)
dishMapper.deleteByIds(ids);
//根据菜品id集合批量删除关联的口味数据:delete from dish_flavor where dish_id in (?,?,?)
dishFlavorMapper.deleteByDishIds(ids);

}

4.在DishMapper中声明getById方法,并配置SQL。在DishMapper中声明deleteById方法并配置SQL

1
2
3
4
5
6
7
8
9
10
//根据主键查询菜品
@Select("select * from dish where id = #{id}")
Dish getById(Long id);

//根据主键删除菜品数据
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);

//根据菜品id集合批量删除菜品
void deleteByIds(List<Long> ids);
1
2
3
4
5
6
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>

5.创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL。在DishFlavorMapper中声明deleteByDishId方法并配置SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.mapper;

@Mapper
public interface DishFlavorMapper {
//批量插入口味数据
void insertBatch(List<DishFlavor> flavors);

//根据菜品id删除对应的口味数据
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long id);

//根据菜品id集合批量删除关联的口味数据
void deleteByDishIds(List<Long> dishIds);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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.DishFlavorMapper">

<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="dishFlavor" separator=",">
(#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value})
</foreach>
</insert>

<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
#{dishId}
</foreach>
</delete>

</mapper>

修改菜品

产品原型

接口设计

根据id查询菜品

根据类型查询分类(已实现)
文件上传(已实现)
修改菜品

代码开发

根据id查询菜品接口开发

1.DishController

1
2
3
4
5
6
7
8
//根据id查询菜品
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:{}", id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}

2.DishService

1
2
//根据id查询菜品和对应的口味数据
DishVO getByIdWithFlavor(Long id);

3.DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//根据id查询菜品
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);

//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

//将查询到的数据封装到VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishFlavors);
// 这里不需要给categoryName赋值,因为前端在点击修改时发送了http://localhost/api/category/list?type=1请求,
// 该请求会返回所有分类,前端通过categoryId即可得到categoryName进行回显
return dishVO;
}

4.DishFlavorMapper

1
2
3
//根据菜品id查询对应的口味数据
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
修改菜品接口开发

1.DishController

1
2
3
4
5
6
7
8
//修改菜品
@PutMapping()
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}

2.DishService

1
2
//根据id修改菜品基本信息和对应的口味信息
void updateWithFlavor(DishDTO dishDTO);

3.DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//根据id修改菜品基本信息和对应的口味信息
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//修改菜品表基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {//设置每个口味对应的菜品的id
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}

4.DishMapper

1
2
3
//根据id动态修改菜品数据
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);

5.DishMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<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>
where id = #{id}
</update>

菜品起售停售

接口设计

代码开发

1.DishController

1
2
3
4
5
6
7
8
//菜品起售停售
@PostMapping("status/{status}")
@ApiOperation("菜品起售停售")
public Result startOrStop(@PathVariable Integer status, Long id){
log.info("菜品起售停售:{},{}",status, id);
dishService.startOrStop(status, id);
return Result.success();
}

2.DishService

1
2
//菜品起售停售
void startOrStop(Integer status, Long id);

3.DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//菜品起售停售
@Override
@Transactional
public void startOrStop(Integer status, Long id) {
//菜品起售停售
Dish dish = Dish.builder().id(id).status(status).build();
dishMapper.update(dish);

if(status == StatusConstant.DISABLE){
// 如果是停售操作,还需要将包含当前菜品的套餐也停售
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
// select setmeal_id from setmeal_dish where dish_id in (?,?,?)
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();
setmealMapper.update(setmeal);
}
}
}
}

4.SetmealMapper

1
2
3
//根据id修改套餐
@AutoFill(value = OperationType.UPDATE)
void update(Setmeal setmeal);

5.SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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.SetmealMapper">

<update id="update" parameterType="Setmeal">
update setmeal
<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="status != null">
status = #{status},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>

</mapper>

套餐管理

新增套餐

产品原型

业务规则:

  • 套餐名称唯一。
  • 套餐必须属于某个分类。
  • 套餐必须包含菜品。
  • 名称、分类、价格、图片为必填项。
  • 添加菜品窗口需要根据分类类型来展示菜品。
  • 新增的套餐默认为停售状态。

接口设计

  • 根据类型查询分类(已完成)。
  • 根据分类id查询菜品。
  • 图片上传(已完成)。
  • 新增套餐。

数据库设计

setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 套餐名称 唯一
category_id bigint 分类id 逻辑外键
price decimal(10,2) 套餐价格
image varchar(255) 图片路径
description varchar(255) 套餐描述
status int 售卖状态 1起售 0停售
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
setmeal_id bigint 套餐id 逻辑外键
dish_id bigint 菜品id 逻辑外键
name varchar(32) 菜品名称 冗余字段
price decimal(10,2) 菜品单价 冗余字段
copies int 菜品份数

代码实现

1.DishController

1
2
3
4
5
6
7
8
//根据分类id查询菜品
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
log.info("根据分类id查询菜品:{}",categoryId);
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}

2.DishService

1
2
//根据分类id查询菜品
List<Dish> list(Long categoryId);

3.DishServiceImpl

1
2
3
4
5
6
7
//根据分类id查询菜品
@Override
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder().categoryId(categoryId).status(StatusConstant.ENABLE).build();
List<Dish> list = dishMapper.list(dish);
return list;
}

4.DishMapper

1
2
//根据分类id查询菜品
List<Dish> list(Dish dish);

5.DishMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="list" resultType="Dish" parameterType="Dish">
select * from dish
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>

6.SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sky.controller.admin;

@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;

//新增套餐
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO){
log.info("新增套餐:{}",setmealDTO);
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}

7.SetmealService

1
2
3
4
5
6
package com.sky.service;

public interface SetmealService {
//新增套餐
void saveWithDish(SetmealDTO setmealDTO);
}

8.SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.sky.service.impl;

@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;

//新增套餐,同时需要保存套餐和菜品的关联关系
@Override
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id
Long setmealId = setmeal.getId();

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
setmealDishMapper.insertBatch(setmealDishes);
}
}

9.SetmealMapper

1
2
3
//新增套餐
@AutoFill(value = OperationType.INSERT)
void insert(Setmeal setmeal);

10.SetmealMapper.xml

1
2
3
4
5
6
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal
(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
#{createUser}, #{updateUser})
</insert>

11.SetmealDishMapper

1
2
//批量保存套餐和菜品的关联关系
void insertBatch(List<SetmealDish> setmealDishes);

12.SetmealDishMapper.xml

1
2
3
4
5
6
7
8
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id, dish_id, name, price, copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId}, #{sd.dishId}, #{sd.name}, #{sd.price}, #{sd.copies})
</foreach>
</insert>

套餐分页查询

产品原型

业务规则:

  • 根据页码进行分页展示。
  • 每页展示10条数据。
  • 可以根据需要,按照套餐名称、分类、售卖状态进行查询。

接口设计

代码实现

1.SetmealController

1
2
3
4
5
6
7
8
//分页查询
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){
log.info("分页查询:{}",setmealPageQueryDTO);
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}

2.SetmealService

1
2
//分页查询
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

3.SetmealServiceImpl

1
2
3
4
5
6
7
//分页查询
@Override
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize());
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}

4.SetmealMapper

1
2
//分页查询
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

5.SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select s.*, c.name categoryName from setmeal s
left join category c
on s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>

删除套餐

产品原型

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐。
  • 起售中的套餐不能删除。

接口设计

代码实现

1.SetmealController

1
2
3
4
5
6
7
8
//批量删除套餐
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
log.info("批量删除套餐:{}", ids);
setmealService.deleteBatch(ids);
return Result.success();
}

2.SetmealService

1
2
//批量删除套餐
void deleteBatch(List<Long> ids);

3.SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//批量删除套餐
@Override
public void deleteBatch(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(StatusConstant.ENABLE == setmeal.getStatus()){
//起售中的套餐不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
});

ids.forEach(setmealId -> {
//删除套餐表中的数据
setmealMapper.deleteById(setmealId);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(setmealId);
});
}

4.SetmealMapper

1
2
3
4
5
6
7
//根据id查询套餐
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);

//根据id删除套餐
@Delete("delete from setmeal where id = #{setmealId}")
void deleteById(Long setmealId);

5.SetmealDishMapper

1
2
3
//根据套餐id删除套餐和菜品的关联关系
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);

修改套餐

产品原型

接口设计

  • 根据id查询套餐。
  • 根据类型查询分类(已完成)。
  • 根据分类id查询菜品(已完成)。
  • 图片上传(已完成)。
  • 修改套餐。

代码实现

1.SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//根据id查询套餐,用于修改页面回显数据
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getById(@PathVariable Long id){
log.info("根据id查询套餐:{}", id);
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}

//修改套餐
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO){
log.info("修改套餐:{}",setmealDTO);
setmealService.update(setmealDTO);
return Result.success();
}

2.SetmealService

1
2
3
4
5
//根据id查询套餐
SetmealVO getByIdWithDish(Long id);

//修改套餐
void update(SetmealDTO setmealDTO);

3.SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public SetmealVO getByIdWithDish(Long id) {
Setmeal setmeal = setmealMapper.getById(id);
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}

//修改套餐
@Override
public void update(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);

//1、修改套餐表,执行update
setmealMapper.update(setmeal);

//套餐id
Long setmealId = setmeal.getId();

//2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId);

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.insertBatch(setmealDishes);
}

4.SetmealDishMapper

1
2
3
//根据套餐id查询套餐和菜品的关联关系
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);

起售停售套餐

产品原型

业务规则:

  • 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作。
  • 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端。
  • 起售套餐时,如果套餐内包含停售的菜品,则不能起售。

接口设计

代码实现

1.SetmealController

1
2
3
4
5
6
7
8
//套餐起售停售
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id){
log.info("套餐起售停售:{},{}",status, id);
setmealService.startOrStop(status, id);
return Result.success();
}

2.SetmealService

1
2
//套餐起售停售
void startOrStop(Integer status, Long id);

3.SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//套餐起售停售
@Override
public void startOrStop(Integer status, Long id) {
//起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
if(status == StatusConstant.ENABLE){
//select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
List<Dish> dishList = dishMapper.getBySetmealId(id);
if(dishList != null && dishList.size() > 0){
dishList.forEach(dish -> {
if(StatusConstant.DISABLE == dish.getStatus()){
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
});
}
}
Setmeal setmeal = Setmeal.builder().id(id).status(status).build();
setmealMapper.update(setmeal);
}

4.DishMapper

1
2
3
4
//根据套餐id查询菜品
@Select("select d.* from dish d left join setmeal_dish s " +
"on d.id = s.dish_id where s.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);

Redis

Redis入门

Redis简介

Redis是一个基于内存的key-value结构数据库。

优点:基于内存存储,读写性能高。适合存储热点数据(热点商品、资讯、新闻)。企业应用广泛。

官网:https://redis.io

中文网:https://www.redis.net.cn/

Redis下载与安装

Redis安装包分为Windows版和Linux版:

RedisWindows版属于绿色软件,直接解压即可使用,解压后目录结构如下:

Redis服务启动与停止

1.服务启动命令:redis-server.exe redis.windows.conf

2.Redis服务默认端口号为6379,通过快捷键Ctrl + C即可停止Redis服务。

3.客户端连接命令:redis-cli.exe

4.通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接:

  • -h ip地址
  • -p 端口号
  • -a 密码(如果需要)

5.设置Redis服务密码,修改redis.windows.confrequirepass 123456

注意:

  • 修改密码后需要重启Redis服务才能生效。
  • Redis配置文件中#表示注释。

6.Redis客户端图形工具:Another Redis Desktop Manager

Redis数据类型

5种常用数据类型介绍

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:

  • 字符串string
  • 哈希hash
  • 列表list
  • 集合set
  • 有序集合sorted set / zset

各种数据类型的特点

字符串(string):普通字符串,Redis中最简单的数据类型。

哈希(hash):也叫散列,类似于Java中的HashMap结构。

列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList

集合(set):无序集合,没有重复元素,类似于Java中的HashSet

有序集合(sorted set / zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素。

Redis常用命令

字符串操作命令

命令 含义
SET key value 设置指定key的值
GET key 获取指定key的值
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
SETNX key value 只有在 key 不存在时设置 key 的值
1
2
3
4
5
6
7
8
9
10
> set name surourou
OK
> set age 23
OK
> setex token 60 whfiewrhfoiwhfoilwejrfoilw
OK
> setnx name srr
0
> setnx nickname mm
1

哈希操作命令

Redis hash是一个string类型的fieldvalue的映射表,hash特别适合用于存储对象。

命令 含义
HSET key field value 将哈希表 key 中的字段 field 的值设为 value
HGET key field 获取存储在哈希表中指定字段的值
HDEL key field 删除存储在哈希表中的指定字段
HKEYS key 获取哈希表中所有字段
HVALS key 获取哈希表中所有值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> hset student name surourou
1
> hset student age 23
1
> hget student name
surourou
> hkeys student
name
age
> hvals student
surourou
23
> hdel student age
1
> hkeys student
name

列表操作命令

Redis列表是简单的字符串列表,按照插入顺序排序。

命令 含义
LPUSH key value1 [value2] 将一个或多个值插入到列表头部(左边)
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素(右边)
LLEN key 获取列表长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> lpush arr 1 2 3
3
> rpush 4 5 6
2
> rpush arr 4 5 6
6
> lrange arr 0 -1
3
2
1
4
5
6
> rpop arr
6
> llen arr
5

集合操作命令

Redissetstring类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据。

命令 含义
SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回给定所有集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SREM key member1 [member2] 删除集合中一个或多个成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> sadd set1 1 2 1
2
> sadd set2 1 3 8
3
> smembers set1
1
2
> scard set1
2
> sinter set1 set2
1
> sunion set1 set2
1
2
3
8
> srem set2 3
1
> smembers set2
1
8

有序集合操作命令

Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。(默认根据score升序排序。)

命令 含义
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
ZREM key member [member …] 移除有序集合中的一个或多个成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> zadd zset1 1 srr 2 surourou 3 surrou
3
> zrange zset1 0 -1
srr
surourou
surrou
> zincrby zset1 8 surourou
10
> zrange zset1 0 -1
srr
surrou
surourou
> zrem zset1 surrou
1
> zrange zset1 0 -1
srr
surourou

通用命令

Redis的通用命令是不分数据类型的,都可以使用的命令。

命令 含义
KEYS pattern 查找所有符合给定模式( pattern)的 key
EXISTS key 检查给定 key 是否存在
TYPE key 返回 key 所储存的值的类型
DEL key 该命令用于在 key 存在是删除 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
> keys *
set1
arr
age
name
student
zset1
set2
nickname
> exists arr
1
> type name
string
> type zset1
zset
> del nickname
1
> keys a*
arr
age
> keys *
set1
arr
age
name
student
zset1
set2

在Java中操作Redis

Redis的Java客户端

RedisJava客户端很多,常用的几种:

  • Jedis
  • Lettuce
  • Spring Data Redis

Spring Data RedisSpring的一部分,对Redis底层开发包进行了高度封装。在Spring项目中,可以使用Spring Data Redis来简化操作。

Spring Data Redis使用方式

操作步骤:

1.导入Spring Data Redismaven坐标。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.配置Redis数据源。

application-dev.yml文件。

1
2
3
4
5
6
sky:
redis:
host: localhost
port: 6379
password:
database: 1

application.yml文件。

1
2
3
4
5
6
spring:
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}

3.编写配置类,创建RedisTemplate对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.sky.config;

@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象");
//RedisConnectionFactory是redis连接工厂,starter创建好连接工厂对象,并放到容器当中,参数声明即可注入
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis中key的序列花器
redisTemplate.setKeySerializer(new StringRedisSerializer());//字符串类型的redis序列化器
return redisTemplate;
}
}

4.通过RedisTemplate对象操作Redis

RedisTemplate针对大量api进行了归类封装,将同一数据类型的操作封装为对应的Operation接口,具体分类如下:

  • ValueOperationsstring数据操作。
  • SetOperationsset类型数据操作。
  • ZSetOperationszset类型数据操作。
  • HashOperationshash类型的数据操作。
  • ListOperationslist类型的数据操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.sky.test;//文件路径:src/test/java/com/sky/test/SpringDataRedisTest.java

@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;

@Test
public void testRedisTemplate() {
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}

//操作字符串类型的数据
@Test
public void testString(){
redisTemplate.opsForValue().set("city","广东");
String city = (String) redisTemplate.opsForValue().get("city");
System.out.println(city);
redisTemplate.opsForValue().set("code", "123456", 3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock", 1);
redisTemplate.opsForValue().setIfAbsent("lock", 2);
}

//操作哈希类型的数据
@Test
public void testHash(){
HashOperations hashOperations = redisTemplate.opsForHash();

hashOperations.put("100", "name", "srr");
hashOperations.put("100", "age", "23");

String name = (String) hashOperations.get("100", "name");
System.out.println(name);

Set keys = hashOperations.keys("100");
System.out.println(keys);

List values = hashOperations.values("100");
System.out.println(values);

hashOperations.delete("100", "age");
}

//操作列表类型的数据
@Test
public void testList(){
ListOperations listOperations = redisTemplate.opsForList();

listOperations.leftPushAll("mylist", "a", "b", "c");
listOperations.rightPush("mylist", "d");

List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);

listOperations.rightPop("mylist");

Long size = listOperations.size("mylist");
System.out.println(size);
}

//操作集合类型的数据
@Test
public void testSet(){
SetOperations setOperations = redisTemplate.opsForSet();

setOperations.add("set1", "a", "b", "c", "a", "d");
setOperations.add("set2", "a", "b", "b", "x", "y");

Set members = setOperations.members("set1");
System.out.println(members);

Long size = setOperations.size("set1");
System.out.println(size);

Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);

Set union = setOperations.union("set1", "set2");
System.out.println(union);

setOperations.remove("set1", "a", "c");
}

//操作有序集合类型的数据
@Test
public void testZSet(){
ZSetOperations zSetOperations = redisTemplate.opsForZSet();

zSetOperations.add("zset1", "a", 10);
zSetOperations.add("zset2", "b", 20);
zSetOperations.add("zset3", "c", 8);

Set zset = zSetOperations.range("zset1", 0, -1);
System.out.println(zset);

zSetOperations.incrementScore("zset1", "c", 10);

zSetOperations.remove("zset1", "b");
}

//通用命令操作
@Test
public void testCommon(){
Set keys = redisTemplate.keys("*");
System.out.println(keys);

Boolean name = redisTemplate.hasKey("name");
Boolean set1 = redisTemplate.hasKey("set1");

for(Object key : keys){
DataType type = redisTemplate.type(key);
System.out.println(type);
}

redisTemplate.delete("mylist");
}
}

店铺营业状态设置

产品原型

接口设计

设置营业状态。

管理端查询营业状态。

用户端查询营业状态。

本项目约定:

  • 管理端发出的请求,统一使用/admin作为前缀。
  • 用户端发出的请求,统一使用/user作为前缀。

营业状态数据存储方式:基于Redis的字符串来进行存储。约定:1表示营业,0表示打烊。

代码开发

1.管理端ShopController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.sky.controller.admin;

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;

public static final String KEY = "SHOP_STATUS";

//设置店铺的营业状态
@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 status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取店铺的营业状态:{}", status == 1 ? "营业中" : "打烊中");
return Result.success(status);
}
}

2.用户端ShopController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.sky.controller.user;

@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;

public static final String KEY = "SHOP_STATUS";

//获取店铺的营业状态
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取店铺的营业状态:{}", status == 1 ? "营业中" : "打烊中");
return Result.success(status);
}
}

注意:因为管理端和用户端都命名为ShopController,在创建Bean时都会使用默认的名字shopController发生冲突,出现报错。

报错:Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'shopController' for bean class [com.sky.controller.user.ShopController] conflicts with existing, non-compatible bean definition of same name and class [com.sky.controller.admin.ShopController]

解决:分别给两个ShopController命名为:@RestController("adminShopController")@RestController("userShopController")


Java项目苍穹外卖:接口开发(1)
http://surourou8.github.io/2024/11/21/Java项目苍穹外卖:接口开发(1)/
作者
Su Rourou
发布于
2024年11月21日
许可协议