[TOC]

写在前面

本人参照黑马程序员瑞吉外卖视频进行学习,并完善了剩余功能。

项目搭建

主要使用的技术

  • 后端:springboot,spring,springmvc,mybatis,mybatis-plus

  • 前端:html,css,js,vue,elementui

搭建数据库(reggie)/表

数据库表图

具体表中的各个字段所代表的意义都已在数据库设计表中的注释提及

表名 表的描述
employee(员工表) 用于存放后台管理人员的信息
category(分类表) 分类表中存放的是菜品的分类和套餐的分类
dish(菜品表) 存放的是菜品的信息,一个菜品必属于一个菜品分类,一个菜品分类也可以有不止一种菜品
dish_flavor(菜品口味表) 存放的是菜品口味的信息,一个菜品可以有多种口味,对应着就会有多条数据是隶属于一个菜品的,其中已经用菜品的id把对应的菜品和菜品口味关联好
setmeal(套餐表) 存放的是套餐的信息,一个套餐必属于一个套餐分类,一个套餐分类也可以有不止一种套餐
setmeal_dish(套餐菜品对应表) 存放的是套餐里所包含的菜品的信息(因为一个套餐是由若干个菜品相组成的),此处存放的信息就是该套餐下所包含的是哪些菜品
orders(订单表) 存放的是用户下单之后的订单的简单信息(包括订单号,订单状态,收货人,联系电话,地址,支付金额,下单时间)
order_details(订单明细表) 存放的是用户下单之后的订单的更多信息(包含了用户购买的是哪些菜品或套餐)
address_book(地址簿表) 存放的是用户的地址信息,也包含默认地址等
shoppingcart(购物车表) 存放的是用户通过移动端点击对应的菜品或套餐所加入的数据,清空购物车自然就是清空该用户的购物车数据
user(用户表) 存放的是用户的基本信息

Idea中的基本配置

通过mybatis-plus来逆向创建对应的pojo,mapper,service接口和serviceImpl实体类即可。

创建通用的R类(通用返回结果类)

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
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* @param <T>
*/
@Data
public class R<T> {

private Integer code; //编码:1成功,0和其它数字为失败

private String msg; //错误信息

private T data; //数据

private Map map = new HashMap(); //动态数据

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

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

public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}

}


设置静态资源映射

主要是让Spring可以扫描backendfront下的静态资源(HTML,CSS.JS)

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}

后台员工登录登出功能

员工登录功能

员工登录功能

  1. 将页面提交过来的password进行md5加密

  2. 根据用户提交的用户名查询数据库

  3. 如果用户不存在则退出

  4. 密码比对,如果不成功则退出

  5. 查看账号是否已被禁用

  6. 登录成功,将员工的id存入Session(重点)中,(此处获得这个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
27
28
29
30
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {

//1.将页面提交过来的password进行md5加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());

//2.根据用户提交的用户名查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);

//3.如果用户不存在则退出
if (emp == null) {
return R.error("登录失败");
}

//4.密码比对,如果不成功则退出
if (!password.equals(emp.getPassword())) {
return R.error("登录失败");
}

//5.查看账号是否已被禁用
if (emp.getStatus() == 0) {
return R.error("账号已禁用");
}
//登录成功,将员工的id存入Session中,并返回登录成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}

员工登出功能

  1. 将登录时存在Session中的id释放出来

  2. 返回结果(注意:此时业面跳转不显示退出成功的原因是:因为显示退出成功html页面已经关闭,故在登录页看不到退出成功的显示)

1
2
3
4
5
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}

完善员工登录登出

需求分析

因为可以不通过登录而直接访问员工管理的页面,这显然是不合理的,需要通过设置一个拦截器,去让用户必须先登录才能访问员工管理页面。

  1. 获取本次请求的URI

  2. 定义不需要处理的请求路径(即一个字符串数组)

  3. 创建一个PATH_MATCHER来比对路径上的通配符

  4. 判断本次请求是否需要处理

  5. 如果不需要处理,则直接放行

  6. 判断登录状态,如果已登录,则直接放行

  7. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据

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
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html

log.info("拦截到请求:{}",requestURI);

//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",//登录的请求路径
"/employee/logout",//登出的请求路径
"/backend/**",//后台的静态资源
"/front/**"//移动端的静态资源
};

//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);

//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}

//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}

log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;

}

/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}

踩坑点

若没加.getSession()则会使登录成功后一直重新回到登录页面

1
2
3
//登录成功,将员工的id存入Session中,并返回登录成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);

如果还有问题,可以尝试去清除一下浏览器缓存


新增员工

新增员工功能


需求分析:因为前端页面展示的让用户新增员工时所填的信息有限,一部分employee分装对象中的属性,即表中的字段需要填入默认值,故该方法用于接收前端页面所传递过来的包装好的employee对象,并将该对象存进表中

功能分析:

  1. 设置默认密码(使用md5加密处理)

  2. 获取登录时传入Session中的id

  3. 添加创建人信息(该创建人为Session中存入的id

  4. 添加修改人信息(该修改人为Session中存入的id

  5. 添加创建时间

  6. 添加更新时间

  7. 将该对象的属性存入表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {

//设置默认密码(使用md5加密处理)
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

//获取登录时传入Session中的id
Long empId = (Long) request.getSession().getAttribute("employee");
//添加创建人信息
employee.setCreateUser(empId);
//添加修改人信息
employee.setUpdateUser(empId);
//添加创建时间
employee.setCreateTime(LocalDateTime.now());
//添加更新时间
employee.setUpdateTime(LocalDateTime.now());
//将该对象的属性存入表中
employeeService.save(employee);

return R.success("新增员工成功");
}

完善新增员工功能

需求分析

因为employee表中的username字段被设置为了唯一的约束,故在前端页面填写时输入相同的username会抛出异常,故需要做出解决,提出错误信息。

功能实现:

  1. 配置全局异常的一个类,让所有Controller层的类的异常都经过该类处理。

  2. 解决上述索引唯一的异常,为前端展示错误信息。


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
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());

if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}

return R.error("未知错误");
}
}


员工分页查询显示到前端

分页查询功能

需求分析:

前端发送get请求,把page当前页,pageSize每页显示条数,name查询名等参数传入controller层,后端进行分页查询和条件查询并把查询对象传回给前端

功能实现:

  1. 添加mybatis-plus的分页插件
  2. 构造分页查询器
  3. 构造条件查询器
  4. 添加过滤条件(这里使用like而不是eq
  5. 添加排序条件
  6. 执行查询,返回结果(结果中返回的pageInfo对象是因为查询完后会将数据封装到该对象中,并且与前端中相响应)

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {

//构造分页查询器
Page<Employee> pageInfo = new Page<>(page, pageSize);
//构造条件查询器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(StringUtils.isNotBlank(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);

return R.success(pageInfo);
}

补充点

前端分页条

该处可去前端页面修改list.html


启用/禁用员工账号

启用/禁用员工账号功能实现

需求分析:

管理员账号admin可以对员工账号进行启用和禁用操作,而其他用户不可进行该操作

代码实现:

本质上是一个update操作,statusid已经从前端传入,需要额外更改的是更新人和更新时间。

1
2
3
4
5
6
7
8
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}

注意点

原因分析:

jsLong类型的数据的处理时丢失了精度,导致前端发送过来的id与数据库中的id不一致

解决方案:

json中的Long型数据转成字符串,在配置中添加对应的方法即可。

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
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 扩展mvc框架的消息转换器(写在WebMVCConfig中)
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}

编辑员工信息功能

编辑员工信息功能实现

需求分析:

用户点击编辑时进入编辑界面,此时登录页面时可以看到用户原本的信息,再经过修改信息后保存到数据库。


回显功能实现(进入编辑页面可看到用户原始信息)

1
2
3
4
5
6
7
8
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
if (employee!=null){
return R.success(employee);
}
return R.error("没有查询到员工信息");
}

编辑功能实现

注意:此处直接调用的是启用/禁用员工账号时的方法,因为传入的是一个employee对象,故该方法是一个通用的 更新方法。


公共字段自动填充

为什么要对公共字段填充

因为不论是在员工管理处还是菜品或者套餐管理处都需要添加诸如createTimeupdateTimecreateUser,以及updateUser这四个字段,故可以使用mybatis-plus提供的公共字段自动填充方法,省去手动为用户填写这四个信息。

如何实现


1.在employee表中这四个字段上添加相应属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;//此处要修改为LocalDateTime,因为数据库中与这里使用的类型不一致

/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;//与上同理

/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private Long createUser;

/**
* 修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

注意updateTimeupdateUser需要在创建和更改时都做更改


2.可将原新增员工功能及更改员工信息功能处的相应的方法注释去


3.基于ThreadLocal封装工具类,用户保存和获取当前登录用户id

创建该工具类,因为在一次Http请求中线程是唯一的,故可以通过该方式得到存入Session中存放的id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}

/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}

4.自定义元数据对象处理器(即公共字段自动填充类)

该类中重写两个方法,分别用于插入操作和更新操作时的自动填充。

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
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}

/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());

long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);

metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}

5.将Session中存放的id存入线程中

在已登录的情况下将Session中存放的id存入线程中。

1
2
3
4
5
6
7
8
//4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}

小结(逻辑)

在一次Http请求中线程是不会发生改变的,也就是说,在发送请求给后端时,经过过滤器,再经过Controller,最后在MyMetaObjecthandler实现自动填充方法时的线程的id都是不会发生改变的。

需要使用到线程的原因是因为在自动填充更新人和创建人的时候,需要将用户的id传入,但在MyMetaObjecthandler类中无法直接获得到Session中的id,故需要使用该方法。


新增分类

新增分类功能实现

1
2
3
4
5
@PostMapping
public R<String> save(@RequestBody Category category) {
categoryService.save(category);
return R.success("新增分类成功");
}

分类分页查询显示数据到页面上

分类分页查询功能实现

1
2
3
4
5
6
7
8
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
Page<Category> pageInfo = new Page<>(page,pageSize);
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(Category::getSort);
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}

删除分类

注意事项

注意:因为分类表中包含着菜品分类和套餐分类,而对应的菜品表和套餐表中存在着一些关联,故规定菜品表(套餐表)中存在关联的分类不允许删除。

代码功能实现

  • CategoryController类中调用自定义方法
1
2
3
4
5
@DeleteMapping
public R<String> delete(Long ids){
categoryService.remove(ids);
return R.success("删除分类成功");
}

  • CategoryService接口中创建自定义方法remove
1
2
3
public interface CategoryService extends IService<Category> {
public void remove(Long ids);
}

  • CategoryServiceImpl实现类中实现方法

    通过查询菜品表或商品表中CategoryId与传入的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
27
28
29
30
31
32
33
34
35
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

@Autowired
DishService dishService;

@Autowired
SetmealService setmealService;

/**
* 根据id删除,删除前需判断条件
* @param ids
*/
@Override
public void remove(Long ids) {
//查看是否关联菜品
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);
int count1 = dishService.count(dishLambdaQueryWrapper);
if (count1 > 0) {
//抛出业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查看是否关联套餐
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, ids);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0) {
//抛出业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//无关联则进行删除
super.removeById(ids);
}
}

  • 自定义异常类CustomException
1
2
3
4
5
6
7
8
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}

  • 将该异常交于之前定义的全局异常处理器处理
1
2
3
4
5
6
7
8
9
10
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());

return R.error(ex.getMessage());
}

修改分类

修改分类功能实现

1
2
3
4
5
@PutMapping
public R<String> update(@RequestBody Category category){
categoryService.updateById(category);
return R.success("修改分类成功");
}

文件上传与下载

功能分析

用于新增菜品时的图片的上传和回显

代码实现

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
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

@Value("${reggie.path}")
private String basePath;

/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());

//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg

//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}

try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}

/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){

try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

//输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();

response.setContentType("image/jpeg");

int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}

//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

新增菜品

功能分析

点击新建菜品后,页面会立即发送一个请求(获取菜品分类信息列表)

菜品图片得上传和回显由之前完成

此处新增菜品所上传得数据不只有菜品表的信息,还有菜品口味表的信息,故接受数据需要一个新的类

代码实现

  • 菜品分类展示为下拉列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 新增菜品(套餐)时下拉菜品(套餐)分类列表
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);

return R.success(list);
}
  • 新建一个类用于接受前端发送的数据
1
2
3
4
5
6
7
8
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();//菜品口味列表

private String categoryName;//分类名称

private Integer copies;
}
  • 新添加一个方法用于一次修改两张表
1
2
3
4
public interface DishService extends IService<Dish> {
//新增菜品,需要同时操作两张表
public void saveWithFlavor(DishDto dishDto);
}
  • 实现该方法

    注意:此处需要开始事务,并且由前端传入的菜品口味数据中没有与之对应的菜品id,故需要拿到菜品口味的列表集合进行id的赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService{

@Autowired
DishFlavorService dishFlavorService;

@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//将菜品信息保存到菜品表中
this.save(dishDto);

//将菜品口味存入菜品口味表
Long dishId = dishDto.getId();

List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId);
}
dishFlavorService.saveBatch(flavors);
}
}

菜品分页展示

功能分析

与员工和分类的查询的不同之处在于:Dish表中所有的只是分类的id而不是分类的名称,但前端页面需要展示的是分类的名称而不是id

代码实现

在基础上加以改变

  • 将分页对象pageInfo拷贝给dishDtoPage,但不拷贝records这个集合(该集合存储的是前端传入的数据)
  • 将List传给List的同时,将分类的名称也传入List
  • 实现方式为用列表存储多个DishDto对象,通过分类id得到分类对象,再通过分类对象得到分类名称,将分类名称放入DishDto对象中,并将Dish中的其他属性拷贝至DishDto中,最后统一由list收集DishDto对象。
  • 将list对象传入分页对象的recodes属性中。
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
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){

//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();

//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);

//执行分页查询
dishService.page(pageInfo,queryWrapper);

//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

List<Dish> records = pageInfo.getRecords();

List<DishDto> list=new ArrayList<>();

for (Dish record : records) {
//每轮循环创造一个新的disDto对象
DishDto dishDto = new DishDto();
//得到分类id
Long categoryId = record.getCategoryId();
//根据id得到对应的分类对象
Category category = categoryService.getById(categoryId);
if(category!=null) {
//根据该对象得到分类的名称
String categoryName = category.getName();
//将分类名称存入dishDto对象中
dishDto.setCategoryName(categoryName);
}
//将其余属性拷贝到该对象中
BeanUtils.copyProperties(record,dishDto);

list.add(dishDto);
}

// List<DishDto> list = records.stream().map((item) -> {
// DishDto dishDto = new DishDto();
//
// BeanUtils.copyProperties(item,dishDto);
//
// Long categoryId = item.getCategoryId();//分类id
// //根据id查询分类对象
// Category category = categoryService.getById(categoryId);
//
// if(category != null){
// String categoryName = category.getName();
// dishDto.setCategoryName(categoryName);
// }
// return dishDto;
// }).collect(Collectors.toList());

dishDtoPage.setRecords(list);

return R.success(dishDtoPage);
}

菜品的批量起售和停售

需要修改:若菜品关联了套餐,则不允许停售

需求分析

请求路径

注意点:@PathVariable的使用,在有多个参数时需加上占位符的值才能让值传入

代码实现

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/status/{status}")
public R<String> changeStatus(@PathVariable("status") Integer status, Long[] ids) {
for (Long id : ids) {
Dish dish = dishService.getById(id);
if (dish != null) {
dish.setStatus(status);
dishService.updateById(dish);
}
}
return R.success("菜品售卖状态修改成功");
}

修改菜品信息

菜品信息回显功能

  • 功能分析

将已有的菜品信息和菜品口味信息查询出来传回前端。

  • 代码实现
  • DishService接口中创建一个方法用于一次查两张表。
1
2
//根据id查询两张表
DishDto getByIdWithFlavor(Long id);

  • 实现该方法

    将两张表的数据分别查出,再存入同一个dishDto对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional
public DishDto getByIdWithFlavor(Long id) {

Dish dish = this.getById(id);

DishDto dishDto = new DishDto();

BeanUtils.copyProperties(dish, dishDto);

LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, id);
List<DishFlavor> list = dishFlavorService.list(queryWrapper);

dishDto.setFlavors(list);

return dishDto;
}

  • controller层调用该方法
1
2
3
4
5
6
7
8
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id) {
DishDto dishDto = dishService.getByIdWithFlavor(id);
if (dishDto != null) {
return R.success(dishDto);
}
return R.error("没有查询到菜品信息");
}

修改菜品功能

  • DishService接口中创建一个方法用于一次修改两张表。
1
2
//修改菜品,需要同时操作两张表
void updateWithFlavor(DishDto dishDto);

  • 实现该方法
  1. 更新菜品基本信息

  2. 删除菜品口味原有信息

  3. 新增菜品口味信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//将菜品信息更新到菜品表中
this.updateById(dishDto);

//先将口味表的数据清空
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(queryWrapper);

//再将菜品口味存入菜品口味表

Long dishId = dishDto.getId();

List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId);
}
dishFlavorService.updateBatchById(flavors);
}

菜品的批量删除(逻辑删除)

需求分析

需注意:若该菜品正处于起售状态不能删除,若该菜品关联了其他套餐不能删除

代码实现

主要代码:

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
@Override
@Transactional
public void removeByIdWithFlavor(Long[] ids) {

for (Long id : ids) {
//需注意:若该菜品正处于起售状态不能删除,若该菜品关联了其他套餐不能删除

//删除菜品

//若该菜品正处于起售状态不能删除(抛异常)
Dish dish = this.getById(id);
if (dish.getStatus().equals(1)){
throw new CustomException("该菜品正处于起售状态不能删除");
}

//若该菜品关联了其他套餐不能删除(抛异常)
List<SetmealDish> list = setmealDishService.list();
for (SetmealDish setmealDish : list) {
if (id.equals(setmealDish.getDishId())){
throw new CustomException("该菜品关联了其他套餐不能删除");
}
}

LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(id != null, Dish::getId, id);

this.remove(queryWrapper);

//删除菜品口味
LambdaQueryWrapper<DishFlavor> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.eq(DishFlavor::getDishId, id);
dishFlavorService.remove(queryWrapper1);
}

}

新增套餐

需求分析

  1. 要获得一个套餐分类的下拉列表

  2. 将菜品分类的id将菜品组查询出来(注意只查询出起售状态的菜品)

  3. 将套餐信息保存至数据库中(保存套餐的基本信息,保存套餐所包含的菜品的信息)


代码实现

  • 获得一个套餐分类的下拉列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 新增菜品(套餐)时下拉菜品(套餐)分类列表
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);

return R.success(list);
}

  • 将菜品分类的id将菜品组查询出来(注意只查询出起售状态的菜品)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//只查询起售的菜品
queryWrapper.eq(Dish::getStatus,1);

queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}

  • 将套餐信息保存至数据库中(主要代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐基本信息
this.save(setmealDto);

//保存套餐包含的菜品
String id = String.valueOf(setmealDto.getId());

List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(id);
}
setmealDishService.saveBatch(setmealDishes);
}

套餐分页展示

需求分析

与菜品的分页展示逻辑相同,都需要特别注意前端需要的是套餐的名称而不是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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){

//构造分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();

//条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null, Setmeal::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

//执行分页查询
setmealService.page(pageInfo,queryWrapper);

//对象拷贝
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");

List<Setmeal> records = pageInfo.getRecords();

List<SetmealDto> list = new ArrayList<>();

for (Setmeal record : records) {

SetmealDto setmealDto = new SetmealDto();
//得到分类id
Long categoryId = record.getCategoryId();
//根据id得到对应的分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
//根据该对象得到分类的名称
String categoryName = category.getName();
//将分类名称存入dishDto对象中
setmealDto.setCategoryName(categoryName);
}
//将其余属性拷贝到该对象中
BeanUtils.copyProperties(record, setmealDto);

list.add(setmealDto);
}

setmealDtoPage.setRecords(list);

return R.success(setmealDtoPage);
}

套餐的批量起售和停售

需求分析

与菜品的批量起售和停售功能逻辑相同

代码实现

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/status/{status}")
public R<String> changeStatus(@PathVariable("status") Integer status, Long[] ids) {
for (Long id : ids) {
Setmeal setmeal = setmealService.getById(id);
if (setmeal != null) {
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
}
return R.success("套餐售卖状态修改成功");
}

修改套餐功能

套餐信息回显功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional
//(套餐回显功能)
public SetmealDto getByIdWithDish(Long id) {
Setmeal setmeal = this.getById(id);

SetmealDto setmealDto = new SetmealDto();

BeanUtils.copyProperties(setmeal, setmealDto);

LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(id != null, SetmealDish::getSetmealId, id);
List<SetmealDish> list = setmealDishService.list(queryWrapper);

setmealDto.setSetmealDishes(list);

return setmealDto;
}

修改套餐功能

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 updateWithDish(SetmealDto setmealDto) {

//将套餐信息更新到菜品表中
this.updateById(setmealDto);

//先将套餐菜品关系表的数据清空
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
setmealDishService.remove(queryWrapper);

//再将套餐菜品关系填入表中

String id = String.valueOf(setmealDto.getId());

List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(id);
}
setmealDishService.saveBatch(setmealDishes);
}

套餐的批量删除

注意

处于起售状态的套餐不能删除,与菜品删除不同的是,删除套餐可不用删除菜品

1
2
3
4
5
6
7
8
9
10
11
12
@Override
@Transactional
public void removeByIdWithDish(Long[] ids) {
for (Long id : ids) {
Setmeal setmeal = this.getById(id);
if (setmeal.getStatus() == 1) {
throw new CustomException("存在套餐正处于起售状态不能删除");
}
this.removeById(id);
}
}


订单明细

见39.后台按条件查看订单

移动端短信验证登录(登出)

获取验证码

  • 导入maven坐标
1
2
3
4
5
6
7
8
9
10
11
<!--阿里云短信服务-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>

  • 修改LoginCheckFilter
1
2
3
4
5
6
7
8
9
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/user/sendMsg",
"/user/login"
};

1
2
3
4
5
6
7
8
9
//4-2、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("user") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("user"));

Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}

  • 获取验证码

注意:此处因为没有阿里云短信服务的签名,所以真实发送短信的步骤就注释了,但生成的验证码可在控制台通过日志的形式查看。

  1. 获取手机号

  2. 生成随机的四位验证码

  3. 调用阿里云的短信服务API发送短信(已注释)

  4. 将验证码存入Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session) {
//获取手机号
String phone = user.getPhone();
if (phone != null) {
//生成随机的四位验证码
String code = String.valueOf(ValidateCodeUtils.generateValidateCode(4));
log.info("code={}", code);

//调用阿里云的短信服务API发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);

//将验证码存入Session
session.setAttribute(phone, code);

return R.success("短信验证码发送成功");
}

return R.error("短信验证码发送失败");
}

登录移动端

  • 具体流程
  1. 获取前端发送过来的手机号

  2. 获取前端发送过来的验证码

  3. 从session中取出生成的验证码

  4. 如果能比对成功,证明登录成功

  5. 根据手机号判断是否为新用户,若是新用户则自动注册

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
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session) {

//获取手机号
String phone = map.get("phone").toString();
//获取验证码
String code = map.get("code").toString();
//从session中取出生成的验证码
Object codeInSession = session.getAttribute(phone);
//比对验证码是否相同
if (codeInSession != null && codeInSession.equals(code)) {
//如果能比对成功,证明登录成功

LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(phone != null, User::getPhone, phone);
User user = userService.getOne(queryWrapper);
if (user == null) {
//根据手机号判断是否为新用户,若是新用户则自动注册
user = new User();
user.setPhone(phone);
userService.save(user);
}
session.setAttribute("user", user.getId());
return R.success(user);
}
return R.error("登录失败");
}

登出移动端

注意要释放session

1
2
3
4
5
6
@PostMapping("/loginout")
public R<String> loginout(HttpServletRequest request) {
//释放session
request.getSession().removeAttribute("user");
return R.success("退出成功");
}

新增地址

需求分析

注意:要设置地址对应的用户id

代码实现

1
2
3
4
5
6
@PostMapping
public R<String> save(@RequestBody AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());
addressBookService.save(addressBook);
return R.success("新增地址成功");
}

地址列表显示

需求分析

将当前登录用户的所有地址显示为列表展示到移动端页面

注意:要设置地址对应的用户id


代码实现

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
//设置地址对应的用户id
addressBook.setUserId(BaseContext.getCurrentId());
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(addressBook.getUserId() != null, AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> list = addressBookService.list(queryWrapper);

return R.success(list);
}

设置默认地址

需求分析

  1. 先把该用户的所有地址都不设置为默认地址(故把所有该用户的is_default字段都设置为0)

  2. 设置该用户所选择的地址为默认地址


代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @PutMapping("/default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
//将该用户的所有地址都不设置为默认地址
Long userId = BaseContext.getCurrentId();
LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(userId != null, AddressBook::getUserId, userId);
updateWrapper.set(AddressBook::getIsDefault, 0);
addressBookService.update(updateWrapper);
//将选择的该用户的地址设置为默认值
addressBook.setIsDefault(1);
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
}

查询用户的默认地址

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/default")
public R<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);

//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = addressBookService.getOne(queryWrapper);

if (null == addressBook) {
return R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}

修改地址(需修改)

地址的回显功能

1
2
3
4
5
6
7
8
@GetMapping("/{id}")
public R<AddressBook> getById(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
}
return R.error("没有查询到该用户地址");
}

修改地址功能

1
2
3
4
5
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook) {
addressBookService.updateById(addressBook);
return R.success("修改收货地址成功");
}

删除地址

需求分析

此处无过多的逻辑分析,即使是默认地址也可以进行删除。


代码实现

1
2
3
4
5
@DeleteMapping
public R<String> delete(Long ids) {
addressBookService.removeById(ids);
return R.success("删除收货地址成功");
}

移动端主页展示(菜品、套餐)

注意点

  1. 菜品,套餐的分类功能在前面已经写过了。移动端展示主页面需要该分类功能和购物车展示功能同时成功才能展示出来。

  2. 点击对应的菜品分类可查询出该分类下的所有菜品,注意该方法已经在之前新增套餐时的功能2中编写,但是当时只是让DishDto对象中新保存了菜品的名称,没有保存菜品的口味,故需要对该方法进行加强。

  3. 点击对应的套餐分类可查询出该分类下的所有套餐,需要添加该方法。


重新编写查询当前分类下的所有菜品功能

注意:只是新增了菜品口味的功能,不影响前面使用的功能(仅仅是对功能加强)

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
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus, 1);

//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);

List<DishDto> dishDtoList = new ArrayList<>();

for (Dish dish1 : list) {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish1, dishDto);
//获得菜品分类的id
Long categoryId = dish1.getCategoryId();
Category category = categoryService.getById(categoryId);
//根据id查询分类对象
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//获取当前菜品的id
Long dishId = dish1.getId();
LambdaQueryWrapper<DishFlavor> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.eq(DishFlavor::getDishId,dishId);
List<DishFlavor> list1 = dishFlavorService.list(queryWrapper1);
dishDto.setFlavors(list1);

dishDtoList.add(dishDto);
}

return R.success(dishDtoList);
}

编写查询当前分类下的所有套餐功能

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

List<Setmeal> list = setmealService.list(queryWrapper);

return R.success(list);
}

添加购物车

需求分析

  1. 设置当前购物车是哪个用户的

  2. 判断添加的是菜品还是套餐

  3. 如果能查找出来对应的符合该用户且符合该菜品(套餐)这两个条件的购物车对象,则在数量上加一

  4. 如果查询不出来,则新增该购物车对象,并设置数量为1,且设置创建时间


代码实现

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
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);

//设置当前购物车是哪个用户的
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);

LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, userId);

//判断添加的是菜品还是套餐
Long dishId = shoppingCart.getDishId();
if (dishId != null) {
//添加的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
//添加的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart shoppingCartOne = shoppingCartService.getOne(queryWrapper);

if (shoppingCartOne != null) {
//如果存在该对象,则数量加一
Integer number = shoppingCartOne.getNumber();
shoppingCartOne.setNumber(number + 1);
shoppingCartService.updateById(shoppingCartOne);
} else {
//如果不存在该对象,则新增该购物车对象,并设置数量为1,且设置创建时间
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
shoppingCartOne = shoppingCart;
}
return R.success(shoppingCartOne);
}


查看购物车

需求分析

根据每个用户的id来查,每个用户只能看到自己的购物车


代码实现

1
2
3
4
5
6
7
8
9
10
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(userId != null, ShoppingCart::getUserId, userId);
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

return R.success(list);
}

清空购物车

需求分析

注意:仅删除该用户的所有购物车数据


代码实现

1
2
3
4
5
6
7
8
@DeleteMapping("/clean")
public R<String> clean() {
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(userId != null, ShoppingCart::getUserId, userId);
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}

删减购物车

需求分析

  1. 设置当前购物车是哪个用户的

  2. 判断删减的是菜品还是套餐(因为前端发送过来的数据要么为DishId,要么为SetmealId

  3. 判断要删减的该对象的数量是否为1

  4. 若为1,则删除该数据

  5. 若不为1,则将数量减一


代码实现

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
@PostMapping("/sub")
public R<String> sub(@RequestBody ShoppingCart shoppingCart) {
log.info("数据:{}", shoppingCart);
//设置当前购物车是哪个用户的
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, userId);

//判断删减的是菜品还是套餐
Long dishId = shoppingCart.getDishId();
if (dishId != null) {
//删减的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
//删减的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}

ShoppingCart shoppingCartOne = shoppingCartService.getOne(queryWrapper);
Integer number = shoppingCartOne.getNumber();
//判断数量是否为1或大于1
if (number == 1) {
//数量为1则删除该该菜品(套餐)信息
Long id = shoppingCartOne.getId();
shoppingCartService.removeById(id);
} else {
//数量不为1则该菜品(套餐)数量减1
shoppingCartOne.setNumber(number - 1);
shoppingCartService.updateById(shoppingCartOne);
}
return R.success("删减商品成功");
}

下单

需求分析

前提:点击去支付后

  1. 前端页面自动访问查询默认地址功能(前面已写好)

  2. 前端页面自动访问查询购物车信息功能(前面已写好)

具体步骤

  1. 获得当前用户id

  2. 查询当前用户的购物车数据

  3. 若购物车为空,则抛出异常

  4. 查询用户数据

  5. 查询地址数据

  6. 封装订单表数据(1条)

  7. 封装订单明细表数据(多条:原购物车有几条数据这里就有几条)

  8. 向订单表插入数据,一条数据

  9. 向订单明细表插入数据,多条数据

  10. 清空购物车数据


代码实现

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
@Override
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();

//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);

if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}

//查询用户数据
User user = userService.getById(userId);

//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}

long orderId = IdWorker.getId();//订单号

AtomicInteger amount = new AtomicInteger(0);

List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());


orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(userId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表插入数据,一条数据
this.save(orders);

//向订单明细表插入数据,多条数据
orderDetailService.saveBatch(orderDetails);

//清空购物车数据
shoppingCartService.remove(wrapper);
}

用户查看自己的订单

需求分析

需将订单明细表中的数据也查询出来,故这里需要使用OrderDto

需注意:在遍历的时候直接使用构造条件来查询导致eq叠加,从而导致后面查询的数据都是null,所有该处选择将方法方法外面进行调用


代码实现

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
//避免在遍历的时候直接使用构造条件来查询导致eq叠加,从而导致后面查询的数据都是null
public List<OrderDetail> getOrderDetailListByOrderId(Long orderId) {
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
return orderDetailList;
}

//移动端分页展示订单
@GetMapping("/userPage")
public R<Page> list(int page, int pageSize) {
Page<Orders> ordersPage = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>();

LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(Orders::getOrderTime);
queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
ordersService.page(ordersPage, queryWrapper);

List<Orders> records = ordersPage.getRecords();

ArrayList<OrdersDto> list = new ArrayList<>();


for (Orders record : records) {

OrdersDto ordersDto = new OrdersDto();
//获取订单id
Long orderId = record.getId();

List<OrderDetail> list1 = this.getOrderDetailListByOrderId(orderId);

BeanUtils.copyProperties(record, ordersDto);
ordersDto.setOrderDetails(list1);

list.add(ordersDto);
}

ordersDtoPage.setRecords(list);

return R.success(ordersDtoPage);
}

后台按条件查看订单

需求分析

此处需要添加三个条件:

  1. 订单号模糊查询

  2. 大于某时间

  3. 小于某时间


代码实现

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String number, String beginTime, String endTime) {
Page<Orders> ordersPage = new Page<>(page, pageSize);
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(Orders::getOrderTime)
.like(number != null, Orders::getId, number)
.ge(beginTime != null, Orders::getOrderTime, beginTime)
.le(endTime != null, Orders::getOrderTime, endTime);

ordersService.page(ordersPage, queryWrapper);
return R.success(ordersPage);
}

后台修改订单状态

需求分析

前端发送过来的数据包含需改变的status和订单id,故可直接更改订单状态。


代码实现

1
2
3
4
5
@PutMapping
public R<String> changeStatus(@RequestBody Orders orders) {
ordersService.updateById(orders);
return R.success("修改订单状态成功");
}

移动端点击套餐图片查看套餐具体菜品

需求分析

  1. 通过前端传入的套餐id在套餐菜品关系表中查询出该套餐下的所有菜品

  2. 将dish对象拷贝给dishDto对象

  3. 将存dishDto对象的集合传回前端


代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @GetMapping("/dish/{id}")
public R<List<DishDto>> dish(@PathVariable("id") Long id) {
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId, id);
//获取该套餐下的所有菜品
List<SetmealDish> list = setmealDishService.list(queryWrapper);

List<DishDto> dishDtos = new ArrayList<>();

for (SetmealDish setmealDish : list) {
DishDto dishDto = new DishDto();
String dishId = setmealDish.getDishId();
Dish dish = dishService.getById(dishId);
BeanUtils.copyProperties(dish, dishDto);

dishDtos.add(dishDto);
}

return R.success(dishDtos);
}
}

再来一单功能

需求分析

只有订单表中的status为4 的时候才能有再来一单的功能

点击再来一单按钮前端页面会直接跳转到购物车页面

根据userId,删除该用户此时所剩的购物车数据(清空购物车)

需要将原来购物车数据复制到购物车中


代码实现

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
@PostMapping("/again")
public R<String> again(@RequestBody Map<String,String> map) {
//前端页面会直接跳转到购物车页面
//故需要先将购物车中数据清除

//根据userId,删除该用户此时所剩的购物车数据(清空购物车)
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(userId != null, ShoppingCart::getUserId, userId);
shoppingCartService.remove(queryWrapper);

//需要将原来购物车数据复制到购物车中
String id = map.get("id");
LambdaQueryWrapper<OrderDetail> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.eq(OrderDetail::getOrderId,id);
List<OrderDetail> list = orderDetailService.list(queryWrapper1);

ArrayList<ShoppingCart> shoppingCartsLists = new ArrayList<>();
for (OrderDetail orderDetail : list) {
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
shoppingCart.setImage(orderDetail.getImage());
Long dishId = orderDetail.getDishId();
Long setmealId = orderDetail.getSetmealId();
if (dishId != null) {
//如果是菜品那就添加菜品的查询条件
shoppingCart.setDishId(dishId);
} else {
//添加到购物车的是套餐
shoppingCart.setSetmealId(setmealId);
}
shoppingCart.setName(orderDetail.getName());
shoppingCart.setDishFlavor(orderDetail.getDishFlavor());
shoppingCart.setNumber(orderDetail.getNumber());
shoppingCart.setAmount(orderDetail.getAmount());
shoppingCart.setCreateTime(LocalDateTime.now());

shoppingCartsLists.add(shoppingCart);
}
shoppingCartService.saveBatch(shoppingCartsLists);

return R.success("操作成功");
}

写在后面

该项目后续可用redismysql主从复制,nginx等技术优化,还需后续完善…….