目录
员工管理模块
分类管理模块
菜品管理模块
套餐管理模块
用户端模块
第一天-项目介绍和环境搭建
1.项目介绍:
通过此项目的学习可以了解到企业开发的完整流程以及开发经验,增强自己的需求分析设计能力,对所学技术进行灵活应用提高编码,解决各种异常提高调试能力
用户端应用:外卖菜品展示 ;客户浏览菜品包含餐厅信息,菜品分类,菜品选取以及用户个人信息的修改
系统管理后台:外卖菜品管理;管理员登录,进行菜品和套餐的增删改查,同时还有订单的处理
主要技术栈:
基础知识:javaSE,javaWeb, 数据库:MySQL
框架:SSM(Spring,SpringMVC,Mybatis/MP),SpringBoot
项目管理:Maven, Git
2.环境搭建:
软件开发流程:
②.分析设计, UI设计 , 概要设计,数据库设计等
③.编码,项目编码,单元测试
④.测试(测试报告)和运维(部署配置)
三种环境:开发环境,测试环境,生产环境(上线环境) 数据库创建 :在可视化工具中导入对应的sql文件
运行成功后,对应的表如下:
然后建立maven项目导入对应依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--快速生成pojo对象-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--数据层持久化技术-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--将java对象转为json数据-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--实现通用操作-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<scope>runtime</scope>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
yml配置文件:(包含MP的驼峰命名和日志以及id生成策略为雪花算法)
server:
port: 8080
spring:
application:
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/foodie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: lyl
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
导入对应的web页面,要在springboot的static目录下,这样才能过滤静态资源
运行效果如下:
至此环境搭建完成,可以开始模块功能的开发。
第二天-登录功能模块开发
三步 需求分析,代码开发,功能测试
1.需求分析
登录页面如下,点击登录前端发送请求,后端处理请求并通过数据库查询对应的employee表是否有此信息
返回的数据格式为 code data msg 需要进行封装
2.代码开发
包结构如下: 三层架构Mapper,Service,Controller,其中前面使用Mp的通用Mapper来快速开发,控制器中的映射路径兼容前端发送的请求
前后端数据协议
通一数据格式,包括code 响应码,data 数据 ,msg 提示信息
@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;
}
}
登录方法login()的设计 -三重校验(用户名,密码,员工状态)
代码实现:controller中实现数据校验
// 员工登录方法
@PostMapping("/login")
//传入http对象来保存登录是否成功到session中
public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){
//将获取的密码进行md5加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//根据用户名查询数据库
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Employee::getUsername,null!=employee.getUsername());
Employee emp = employeeService.getOne(wrapper);
//如果emp为空查询失败
if (emp==null){
return R.error("登录失败");
}
//比对密码不一致
if (!employee.getPassword().equals(password)){
return R.error("登录失败");
}
//查看员工状态是否禁用
if (emp.getStatus()==0){
return R.error("账号禁用");
}
//条件判断完毕即登录成功,存入session中返回正确的信息
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
3.功能测试
先查看数据库中的员工信息,其中密码进行了md5加密
然后在客户端输入错误的用户名密码进行测试
同时也返回了错误的信息格式,即R对象,此时的R对象通过SpringMVC转为了json数据
输入正确的用户名和密码,则成功登录
第三天-退出功能模块开发
1.需求分析
在员工管理系统的右上角有退出登录的功能,此功能为通过前端页面绑定的一个logout方法
而名字为双向绑定的用户名 userinfo.name
然后点击退出按钮 跳转至vue的方法中,此方法先调用再次封装的api()方法中完成一次请求
2.代码编写
因此我们需要先编写一个控制器完成这次请求,这里退出的同时需要清除session中的id
// 员工退出方法
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//首先要清除之前存入的session数据(即员工id信息)
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
同时点击退出在前端会进行判断,然后移除浏览器中的员工信息
3.代码测试
观察浏览器中员工信息
点击退出,跳转值登录页面 观察浏览器中的员工信息,也进行了移除
4.登录功能完善
在解决登录功能后,我们也可以不通过登录,而直接去访问index.html页面,从而导致功能失效,因此需要将所有页面请求进行拦截或者过滤,判断用户是否进行了登录,没有则跳转至登录页面,从而控制所有的页面。
自定义过滤器:
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("请求过滤");
//放行
filterChain.doFilter(servletRequest,servletResponse);
}
}
测试是否能过滤:控制台打印
处理逻辑:
实现:
//路径匹配器
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("请求过滤" + request.getRequestURI());
//拦截所有请求
String URI = request.getRequestURI();
//登录和退出,以及静态资源不需要进行过滤
String[] urls = new String[]{
"/employee/login", "/employee/logout", "/backend/**", "/front/**"
};
//判断是否放行
boolean Check = check(urls,URI);
if (Check){
filterChain.doFilter(request,response);
return;
}else {
//判断是否登录
if (request.getSession().getAttribute("employee")!=null){
filterChain.doFilter(request,response);
return;
}else {
//否则跳转至登录界面
//这里是通过页面的响应拦截器来响应所有的response实现自动跳转登录页面
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
}
}
/**
* 通过路径匹配器来判断此次请求是否需要处理
*/
public boolean check(String[] urls, String URL) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, URL);
if (match) {
return true;
}
}
return false;
}
}
通过一个路径匹配器来匹配通配符*,获取所有的路径进行判断:
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
同时进行了逻辑处理后,所有的响应,由前端拦截器拦截并跳转登录页面
第四天-新增员工模块开发
1.需求分析
在管理员界面点击添加员工
跳转至backend/page/member/add.html 即一个表单填完后保存到employee表中
2.数据模型
3.代码实现
基本流程:
先编写控制器,并使用日志打印,可以接收到前端的数据
进一步完善数据库中的信息,md5加密登录密码,创建修改日期,和创建人的id
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//设置初始密码并用md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//设置其它信息
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
//调用Mp的save方法
employeeService.save(employee);
return null;
}
4.功能测试
启动项目,添加一个员工
数据库中查看,添加成功
当再次添加名叫tom的用户时,由于username是唯一注解 报错
因此需要全局异常处理器,来捕获这些异常,方便前端处理,增强用户体验
5.全局异常处理器
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
//选择要处理的异常
@ExceptionHandler(Exception.class)
public R<String> Exception(Exception e){
log.error(e.getMessage());
return R.error("添加失败");
}
}
测试:成功捕获此异常并在管理打印添加失败
第五天-员工分页查询模块开发
1.需求分析
当数据量比较大时,为了减轻服务器的压力,同时美化页面,将数据进行分页展示
同时为了能精准定位到数据,进行查询功能开发
具体的思路如下:先接收前端的分页参数,通过Mp进行处理,返回json数据展示页面
2.代码实现
首先找到前端返回的控制器路径
编写controller,参数在url上,使用GET请求
@GetMapping("/page")
public R<Page> page(Integer page,Integer pageSize,String name){
log.info(page+pageSize+name);
return null;
}
测试参数是否能接收:
输入员工姓名,同时也接收到了name
参数能正常接收,接下来就是分页控制器的使用,在MP中需要自定义配置分页拦截器
@Configuration
public class MybatisPlusPage {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
//定义mp拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//添加乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
分页查询控制器:
//员工信息分页查询
@GetMapping("/page")
public R<Page> page(Integer page,Integer pageSize,String name){
log.info(page+pageSize+name);
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper();
//员工姓名
wrapper.like(name!=null,Employee::getName,name);
//排序条件
wrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,wrapper);
return R.success(pageInfo);
}
3.功能测试
启动项目,进行断点调试,在构造完pageInfo对象后里面的total,records都为null
同时前端页面也没有数据
前端的数据由R对象接收,并由Pageinfo对象中的数据填入
继续调试,直至返回R对象,此时pageInfo对象中存储了查询的数据
前端接收响应的数据进行填写
第六天-员工账号管理模块开发
1.需求分析
现在进行的是后端管理系统的开发,因此需要对员工进行操作权限的管理
管理员 admin 可以进行禁用和启用操作,当员工操作被禁用时无法进行管理操作
普通用户,没有此操作只有编辑操作
当管理员点击编辑按钮 启用和禁用操作时,前端触发事件
传递了 id 和 状态
前端,调用此方法,同时发送axious请求给控制器
2.模块开发
根据id修改员工状态
//根据id修改员工信息 (管理员admin)
@PutMapping
public R<String> update(@RequestBody Employee employee){
log.info(employee.toString());
return null;
}
测试是否能接收到参数,前端首先管理员admin登录然后点击禁用按钮,发送请求,同时断点拦截到此控制器,传递了id和status
放行断点,随后控制台打印日志
进一步编写控制器
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
// log.info(employee.toString());
//获取当前修改的用户和时间
employee.setUpdateUser((Long)request.getSession().getAttribute("employee"));
employee.setUpdateTime(LocalDateTime.now());
employeeService.updateById(employee);
return R.success(null);
}
这里要返回R对象可以不传对象,前端接收code就行 调用查询方法实现页面异步刷新
3.功能测试
进行断点调试,禁用tom员工
发现没有报错,但是状态也没进行修改,进入控制台查看sql ,发现没有修改成功
问题解决:
这里修改tom员工的状态信息,发现失败,查看sql后发现这里修改的id是:
而数据库中tom员工的id为,发现后面两位发生了精度丢失,可以想到应该是前端发送参数时出现了精度丢失,从而没有定位到id导致修改操作失败,根本原因就是,由雪花算法生成的id为19位,而前端接收id后由JS处理只能到16位,后面则会自动四舍五入,因此传递的id不一致
解决:将服务端给页面响应的json数据中,将id long 型数据统一转为String类型字符串
实现:对象转换器 ,底层通过jackson对java对象进行序列化和反序列化
比如在后端控制器返回数据时,通过responseBody注解将java对象转为json数据,在控制器接收前端传递的参数时,会自动将json数据转为java对象
对象转换器:
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);
}
}
消息转换器,MVC配置类中配置
@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
//扩展mvc框架 消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将对象转换器追加到消息转换器
converters.add(0,messageConverter);
}
}
启动项目,实现动态修改员工状态
同时数据库也记录了相关修改信息
当然也可以在pojo中对需要进行消息转换的字段进行注解配置,这样可以实现上述功能
第七天-员工信息编辑模块开发
1.需求分析
点击编辑按钮
跳转到了add.html,这里和新增员工共用一个页面
判断逻辑(list页面中)
我们进入add.html页面,当vue加载完毕调用了此方法
通过此方法我们可以获取url地址的参数,即点击编辑后按钮后跳转的url
获取id之后调用此方法,传入得到的id进行查询
此时页面是报错的,因为虽然进入了修改页面,但是此时没有返回要修改的信息
查看前端发送的ajax请求,编写对应的控制器即可
2.代码开发
通过前端的请求路径可知,为Get请求,并且url中会传入参数id,最后将查询到的员工进行判断
//根据id查询员工信息
@GetMapping("/{id}")
public R<Employee> findUserById(@PathVariable Long id){
log.info("根据id查信息");
Employee employee = employeeService.getById(id);
if (employee!=null){
return R.success(employee);
} else {
return R.error("没有此员工信息");
}
}
3.功能测试
启动项目,点击编辑按钮,成功查询到此员工信息
返回的json数据
修改员工性别为 女 点击保存
再次查看 修改成功
这里控制器并没有调用update方法,为什么就能修改成功呢,因为当点击保存按钮时,自动调用editEmplyee()方法,而此方法会调用控制器中的update方法完成修改。
第八天-分类管理分页模块开发
1.公共字段自动填充
在进入菜品模块开发之前,需要对之前员工管理模块开发进行优化,在新增员工时,需要获取当前时间,管理员信息,修改员工时也需要获取这些信息,因此对于重复的代码,我们需要提取出来就行统一的Aop处理,这样就可以降低耦合,提高代码可维护性,这里使用MP的公共字段自动填充
实现步骤:
在Pojo中进行注解配置
//MP 插入数据时填充字段
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//在插入和更新是填充字段
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
元数据对象处理器:(实现MetaObjectHandler接口)
/**
* 元数据对象处理器
*/
@Component
@Slf4j
public class MetaHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("insert");
log.info(metaObject.toString());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("update");
log.info(metaObject.toString());
}
}
测试:控制台打印日志,表示生效
进一步编写,对应pojo的字段注解,(这里用户先写定值)zhezhe
@Component
@Slf4j
public class MetaHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}
@Override
public void updateFill(MetaObject metaObject) {
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
}
这里怎么解决用户id的获取呢?通过httpSession是不行的,我们可以通过JDK提供的一个线程类来保存当前线程的数据,同时获取数据,而在进行一系列前后端数据传递,拦截器,MP字段填充都是共用一个线程,因此可以获取当前操作人的id
新建工具类
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
在过滤器中set当前用户id,并在填充器中替换 new Long(1)
测试可以发现,当修改用户时,修改时间和用户id都传到了字段填充器中
2.分页模块开发
1.需求分析
在添加菜品时需要提供对应的菜品分类,这样在前台能进行分类展示
对应的后端管理页面如下:
导入菜品类 Category
package com.lyl.Pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
private Integer sort;
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
private Integer isDeleted;
}
2.代码设计
对应的mapper接口
Service层
Controller层
先查看对应页面的请求方式和路径
控制器编写
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize){
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper();
//排序条件
wrapper.orderByDesc(Category::getUpdateTime);
//执行查询
categoryService.page(pageInfo,wrapper);
return Result.success(pageInfo);
}
}
3.功能测试
启动页面,先从前端获取分页参数,进入控制器执行查询,最后返回json数据
这里分类类型,在数据库中由数字0,1表示,展示到前端页面进行了逻辑判断
第九天-分类管理增改模块开发
1.新增菜品模块开发
1.需求分析
在list.html页面中点击左上角的新增按钮,进入addClass方法选择对应的按钮
点击确定按钮绑定表单提交,发送ajax请求,将数据传递给后端
发送请求接口
2.代码编写
有了请求的路径,就可以编写对应的控制器,来接收前端数据,在进行处理响应
//新增菜品分类
@PostMapping
public Result<Category> save(@RequestBody Category category){
categoryService.save(category);
return Result.success(category);
}
3.代码测试
新增一个凉菜,点击保存,并进行分页查询后成功添加
控制台打印的sql如下:
2.修改菜品模块开发
1.需求分析
前端修改页面,和员工操作一样点击修改首先要获取对应id的数据,即完成一次按id查询并返回
点击修改按钮调用此方法,由于vue数据双向绑定,在通过分页后的数据回填到了数据模型中
点击确定后,查看调试工具,发送put ajax请求
2.代码编写
对应的控制器为,这里由于公共自动自动填充则不需要设置修改时间和修改人的信息
//修改指定分类
@PutMapping
public Result<String> update(@RequestBody Category category){
categoryService.updateById(category);
return Result.success("修改成功");
}
3.代码测试
将粤菜的序号改成4
可以发现修改成功
观察控制台sql语句,底层调用了update方法,并将前端获取的信息传给控制器进行修改
对应的ajax请求
第十天-分类管理删除模块开发
1.需求分析
删除指定菜品在点击删除按钮时需要传入对应的id,而如果此分类关联了菜品或套餐则不能删除
对应的前端路径以及传递的id参数:
前端逻辑代码
跳转的ajax请求为:
2.代码编写
由于前端传递的id参数时ids为了能匹配到控制器中的id要改成和前端页面发送的参数一样
//删除指定的菜品
@DeleteMapping
public Result<String> deleteById(Long ids){
categoryService.removeById(ids);
return Result.success("删除成功");
}
3.代码测试
删除凉菜:
成功删除
页面获取了凉菜的ids并传递到了控制器中执行removeByid方法最后调用mapper操作数据库
这里操作完毕后调用分页查询方法,实现页面自动刷新数据
这里init()方法就是 调用分页功能的方法
4.删除功能完善
由于在删除对应分类时,有些分类属于套餐或菜品里面的,不能将此分类删除,因此需要先查询分类是否属于套餐或菜品中的,在进行删除。
先导入对应的菜品类和套餐类,并设置对应接口
在业务层接口进行逻辑判断,先查看分类是否包含菜品和套餐,然后在进行删除
可以发现菜品dish关联着category_id ,同时套餐setmeal也关联着category_id
因此在业务层利用MP的warpper和传入的category的id进行条件判断eq
@Service
public class CategoryServiceimpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
//根据id删除分类
@Override
public void remove(Long id){
LambdaQueryWrapper<Dish> dish = new LambdaQueryWrapper<>();
LambdaQueryWrapper<Setmeal> setmeal = new LambdaQueryWrapper<>();
//添加查询条件
dish.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dish);
setmeal.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmeal);
//先判断菜品是否关联菜品,并抛出业务异常
if (count1>0){
throw new CustomException("当前分类关联菜品,不能删除");
}
//在判断分类是否关联套餐,抛异常
if (count2>0){
throw new CustomException("当前分类关联套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
这里当分类关联菜品和套餐后需要在前端提示用户不能删除,因此需要抛出异常,并用全局异常处理器来接收,这里没说是捕获抛出异常,因为这是业务功能中发现的异常,需要自定义异常捕获
public class CustomException extends RuntimeException{
public CustomException(String msg){
super(msg);
}
}
在全局异常处理器中,选择捕获自定义异常,因此当分类有关联时,处理器会抛出异常给前台
//处理的自定义异常
@ExceptionHandler(CustomException.class)
public Result<String> Exception(CustomException e){
return Result.error(e.getMessage());
}
功能测试:删除第一个有关联的菜品,前台报错并提示信息
这里信息是通过多层传递发出的,首先在业务层判断count大于0后 new 一个自定义异常对象,并将信息msg传入自定义异常的构造器中,同时又继承了运行异常使用super将信息给runtime异常,在最后的全局异常处理器中拦截自定义异常,并通过getmessage()获取信息,最后返回前端
第十一天-文件上传下载模块开发
1.文件的上传和下载(本质io流)
需求分析:不管是管理后台还是前端都需要图片,因此需要进行上传和下载
服务器文件接收(通过spring-web封装的api):
文件下载(本质服务器以流的形式回给浏览器)
2.文件上传代码实现:导入upload.html页面并查看页面请求
后端代码编写:根据前端ajax请求编写控制器来接收图片
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
/**
* 接收上传的文件
* @return
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
log.info(file.toString());
return null;
}
}
同时观察控制台,打印了日志,表示已经接收到了文件,但此时文件是临时存放的文件,需要转存到指定位置,否则此请求完成后临时文件会删除
3.文件上传优化测试
作为临时文件需要转存到硬盘中进行持久化,这里通过spring的@Value来动态获取路径
@Value("${foodie.path}")
private String basePath;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
//临时文件
log.info(file.toString());
try { //转存到硬盘中
file.transferTo(new File(basePath+"hello.jpg"));
}catch (IOException e){
e.printStackTrace();
}
return null;
}
yml中,通过测试在D盘中生成了hello.jpg图片
此时还需要将生成图片名进行动态调整,改成获取原始文件名,并使用UUID拼接
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${foodie.path}")
private String basePath;
/**
* 接收上传的文件
* @return
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
//临时文件
log.info(file.toString());
//获取文件原始文件名
String orginalFilename = file.getOriginalFilename();
//截取后缀
String substring = orginalFilename.substring(orginalFilename.lastIndexOf("."));
//使用UUID拼接后缀
String filename = UUID.randomUUID().toString()+substring; //XXX.jpg
//在将前端上传的图片持久化之前需要先判断当前目录是否存在
File dir = new File(basePath);
if (!dir.exists()){
dir.mkdirs(); //不存在自动创建目录
}
try { //转存到硬盘中
file.transferTo(new File(basePath+filename));
}catch (IOException e){
e.printStackTrace();
}
//后端新增菜品需要获取对应菜品图片的名字存入数据库
return Result.success(filename);
}
4.文件的下载展示
当文件上传至服务器后,前端需要获取对应图片的名字并通过src进行图片展示,这里发送请求,并传递了响应的数据
控制器编写:(主要通过输入输出流来获取图片信息)
@GetMapping("/download")
//文件下载
public void download(HttpServletResponse response,String name) throws IOException {
//通过输入流获取文件内容
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();
}
功能测试:点击上传的图片
底层执行流程为先上传图片upload控制器,然后返回图片名,页面在调用download控制器,下载对应的图片,并进行展示,这样就完成了菜品图片的上传和下载
第十二天-新增菜品模块开发
1.需求分析
数据模型: 新增一个菜品,对应的有菜品的名称价格描述等等,属于一个菜品类,同时关联着分类(是属于菜系还是套餐),另外每个菜品还有对应的口味,因此需要两个数据模型,这里新增菜品就向菜品表加数据,口味则菜品口味表
菜品口味表中关联着每个菜品,对每个菜品提供口味选择,比如id为139..114的菜品在此表中有两个属性忌口和辣度。
环境搭建
新增菜品思路分析
在add.html中先获取菜品分类数据(对应type=1),对应的ajax请求为:
2.代码设计 (根据ajax请求编写)
//根据type查询对应的分类(这里查询所有菜品分类)
@GetMapping("list")
public Result<List<Category>> findlist(Category category){
//条件构造器
LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper();
//添加查询条件
wrapper.eq(category.getType()!=null,Category::getType,category.getType());
//添加排序条件
wrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(wrapper);
return Result.success(list);
}
测试发现点击新增菜品,出现对应的菜品分类,同时图片的上传和下载也可以进行
点击保存发送ajax请求,将json数据发送给控制器
此表单数据不能简单的用一个对象去接收,需要使用数据传输对象DTO
数据传输对象DTO:(用于封装页面提交的数据)
这里在菜品业务层先进行数据处理
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存基本信息到菜品表
this.save(dishDto);
//菜品id
Long dishDtoId = dishDto.getId();
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
//lambda表达式,将集合转为流
flavors.stream().map((item)-> {
item.setDishId(dishDtoId);
return item;
}).collect(Collectors.toList());
//保存菜品口味搭配菜品口味表
dishFlavorService.saveBatch(flavors);
}
控制器编写:
//新增菜品
@PostMapping
public Result<String> save(DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return Result.success("新增成功");
}
3.功能测试
添加一个菜品,点击保存
在编辑器中,使用断点调试发现,以及将表单数据传递给了控制器接收
查看数据库,成功添加!(在分页查询功能下也有显示)
第十三天-菜品分页查询模块开发
点击菜品管理,发送分页查询的ajax请求
当然也可以通过前端的food下的list.html,找到对应的ajax请求,这里返回分页查询对象进行分页
2.代码编写
DishContorller控制器编写(先进行分页展示)
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize,String name) {
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//模糊查询
wrapper.like(name!=null,Dish::getName,name);
wrapper.orderByDesc(Dish::getUpdateTime);
//执行查询
dishService.page(pageInfo, wrapper);
return Result.success(pageInfo);
}
3.功能测试
此时点击菜品管理页面如下:进行了分页查询但是图片并没有上传下载并且没有返回菜品分类,因此需要优化
图片显示优化
图片的上传和下载是通过CommonController提供的请求进行,而最后文件保存在了磁盘中,在文件进行下载时,获取磁盘中(即进行上传的文件)的图片,将图片id与菜品的id绑定进行展示。因此我们需要将资料中的图片放到指定的文件夹中,在yml中:
这样在进行图片下载时就能下载对应图片了
同时也可以查看分页响应代码,在每个菜品的id绑定着对应的图片,当添加新菜品时上传对应图片生成图片的UUID ,服务器下载并展示
菜品分类优化:由于菜品分类属于多表查询,菜品表关联着分类表,在前端接收时没有对应的属性,因此使用数据传输对象来接收,同时传递此对象即可完成联表。
控制器编写:
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize,String name) {
//构造分页器 页码, 每页数
Page<Dish> pageInfo = new Page(page, pageSize);
//这里涉及多表需要展示其他属性
Page<DishDto> dishDtoPage = new Page<>();
//构造过滤条件
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//模糊查询,对应左上角按名字查询
wrapper.like(name!=null,Dish::getName,name);
//列表排序条件
wrapper.orderByDesc(Dish::getUpdateTime);
//执行查询
dishService.page(pageInfo, wrapper);
//将分页对象复制给此对象
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item)->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
//分类id
Long categoryId = item.getCategoryId();
//分类对象
Category category = categoryService.getById(categoryId);
if (category!=null){ //菜品分类不为空才赋值
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
//将集合对象赋值给Records
dishDtoPage.setRecords(list);
return Result.success(dishDtoPage);
}
启动项目页面如下:成功解决图片显示和菜品分类问题
第十四天-菜品修改模块开发
1.需求分析
修改数据大致思路,是先获取根据id数据的信息,点击保存按钮在将表单数据提交给控制器(这里共用新增表单add.html),与之前不同的是,这里表单关联着分类,需要先获取菜品分类数据,同时图片也需要请求服务器进行下载
观察调试器发现点击修改,发送4次ajax请求(先查询分类数据,再根据id回显数据)
点击保存后发生ajax请求
2.代码设计
业务层:
//根据id查询
@Override
public DishDto getByIdWithFlavor(Long id) {
//从dish表查询菜品基本信息
Dish dish = this.getById(id);
//拷贝给DTO
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应口味信息
LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(wrapper);
return dishDto;
}
//修改菜品
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新菜品表
this.updateById(dishDto);
//先清理口味表
LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper();
wrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(wrapper);
//更新口味表
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item)-> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
表现层:
//根据id查询菜品信息和口味信息,这里要查数据传输对象(多种对象组合)
@GetMapping("/{id}")
public Result<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return Result.success(dishDto);
}
//修改菜品
@PutMapping
public Result<String> update(@RequestBody DishDto dishDto){
//不能简单保存此对象需要将两表进行更新
dishService.updateWithFlavor(dishDto);
return Result.success("修改成功");
}
3.功能测试
点击第一个宫保鸡丁。数据成功回显
同时查看控制器响应数据:为json数据,里面封装着DishDTO对象
将其改为湘菜,查看对应响应修改成功
第十五天-新增套餐模块开发
1.需求分析
先观察套餐管理界面,本质上就是菜品的集合
新增套餐,需要先确定分类,然后添加菜品和上传图片,需要套餐_菜品的数据模型,并搭建对应的架构环境。
这里由于套餐关联着许多菜品,因此需要DTO来进行数据传输
6次ajax交互过程
同菜品分类一样,点击新增套餐页面,会先发送ajax请求用于查询对应的套餐分类有哪些(在category表中进行分类查询)
同时观察页面出现分类管理中的套餐数据
点击添加菜品,会发送第二次ajax请求,用来查询所有的菜品信息(这里对应的type为1)
上述分类查询基于如下两行代码
第三次ajax请求,会获取所有菜品信息,通过菜品id来进行查询对应的菜品分类
对应的ajax请求为:
2.代码设计
前两次ajax请求是基于CategoryController中的list来进行分类查询,而后面的图片上传和下载则是基于CommonController来实现,因此只需要完成菜品查询和最后数据提交的ajax请求即可
菜品查询(DishController)通过categoryid来查询对应菜品
//查询指定菜品
@GetMapping("/list")
public Result<List<Dish>> list(Dish dish){
//条件构造器
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//添加查询条件
wrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
List<Dish> list = dishService.list(wrapper);
return Result.success(list);
}
此时控制器需要优化,菜品有状态status,当停售后则不用显示
启动项目,观察控制台sql,成功添加查询条件
点击保存,观察对应ajax请求,同时由于数据类型复杂,因此需要DTO来封装套餐
//套餐DTO
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
在业务层进行数据处理,对两张表进行insert操作
@Autowired
private SetmealDishService setmealDishService;
@Override
@Transactional
public void saveWithDish(@RequestBody SetmealDto setmealDto) {
//保存套餐的基本信息 操作setmeal 执行insert
this.save(setmealDto);
//保存套餐和菜品的管理信息 操作setmeal_dish
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
控制器调用
//保存套餐
@PostMapping
public Result<String> save( SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return Result.success("新增成功");
}
3.功能测试
新增套餐,点击保存
控制台打印sql如下:
观察数据库成功添加!
第十六天-套餐分页模块开发
1.需求分析
2.代码设计
//套餐分页
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize,String name){
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//构造过滤条件
LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper();
//按名字查询
wrapper.like(name!=null,Setmeal::getName,name);
//按更新时间降序排序
wrapper.orderByDesc(Setmeal::getUpdateTime);
//执行查询
setmealService.page(pageInfo,wrapper);
return Result.success(pageInfo);
}
3.功能测试
查看对应的页面发现下面套餐图片并不展示,而上面的套餐是上个模块通过新增套餐添加的图片
查看调试器后发现,虽然图片进行了上传和下载,并绑定到了儿童套餐中,但是没有显示,因此可以得出图片并不存在,即此套餐是由于之前就有的测试用例,并没有添加对应的图片,因此需要手动添加对应图片,此时还要修改图片对应的id不然服务器无法下载正确图片
下载图片至资源中,并复制对应id进行修改,重启项目,修改成功
此时还是有问题,没有显示对应的套餐分类,由于我们响应的是Setmeal实体并没有复杂的属性,因此还需要联立分类实体,此时就需要DTO数据传输对象来进行处理
控制器修改:
//套餐分页
@GetMapping("/page")
public Result<Page> page(Integer page, Integer pageSize,String name){
//构造分页器 页码, 每页数
Page pageInfo = new Page(page, pageSize);
//将分页查询结果进行对象拷贝
Page<SetmealDto> dtoPage = new Page<>();
//构造过滤条件
LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper();
//按名字查询
wrapper.like(name!=null,Setmeal::getName,name);
//按更新时间降序排序
wrapper.orderByDesc(Setmeal::getUpdateTime);
//执行查询
setmealService.page(pageInfo,wrapper);
//对象拷贝 //忽略records,泛型不一致records返回list集合
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
//先将分页属性拷贝给dto
BeanUtils.copyProperties(item,setmealDto);
//分类id查询对应name
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category!=null){
//通过分类对象获取名称
String categoryName = category.getName();
//将名称赋值给DTO,此时dto有了页面完整的属性
setmealDto.setCategoryName(categoryName);
}
//返回dto
return setmealDto;
}).collect(Collectors.toList());
//将返回的集合赋值给dto对象
dtoPage.setRecords(list);
//返回DTO对象
return Result.success(dtoPage);
}
观察页面,成功修改
第十七天-套餐删除模块开发
1.思路分析
删除指定的套餐,或者批量删除,售卖中的套餐无法删除,要先停售
当删除多个套餐时,观察ajax请求,会将多个套餐的ids拼接到url上
2.代码设计
在套餐控制器中编写删除接口(这里参数用集合接收因为有多个套餐),并进行断点调试
//删除套餐
@DeleteMapping
public Result<String> deleteById(@RequestParam List<Long> ids){
log.info("ids为"+ids);
return null;
}
当点击批量删除时,应该传入两个套餐
发现页面的参数成功传入到控制器中
由于套餐关联着菜品,删除套餐时要也要删除对应的关系表,即要操作两张表,这里在业务层完成对两张表的删除操作
@Override
public void removeWithDish(List<Long> ids) {
//先判断套餐的状态
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//在指定的套餐中,查询对应的状态是否有起售
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
//将条件传入,计算count的值是否大于0,有起售则不能进行批量删除
int count = this.count(queryWrapper);
if (count>0){
//不能删除抛出一个业务异常
throw new CustomException("套餐正在售卖,不能删除");
}
//能删除先删除套餐表中的套餐
this.removeByIds(ids);
//再删除套餐关系表中的关联信息
setmealDishService.removeById(ids);
}
这里删除Setmeal_Dish表不能通过ids来进行删除,ids是Setmeal表的主键,与此表无关,需要通过ids进行条件查询来获取Setmeal_Dish表中的Setmeal_id
最后在控制器中调用业务方法
3.功能测试
选择删除第一个套餐,成功发送ajax请求
由于此套餐在起售无法删除
随后修改儿童套餐其售卖状态为停售
进行批量删除操作,成功删除观察数据库表对应关联也进行了删除只剩商务套餐一个关联
第十八天-移动端登录模块开发
1.需求分析
在用户端实现用户的登录,这里通过发送邮箱来获取验证码进行登录,使用javamail
对发送验证码的功能进行请求过滤,不能被拦截
对应的交互过程(这里使用邮箱发送验证码,我修改了前端的正则表达式)
第一次的ajax请求,为点击发送验证码时,对应的api为
第二次ajax请求对应的api为
2.代码设计
发送验证码接口(调用发送邮箱的工具)
@Autowired
SendEmail sendEmail;
//获取对应邮箱的验证码
@PostMapping("/sendMsg")
public Result<String> SendMsg(@RequestBody User user, HttpSession session){
//获取邮箱号
String phone = user.getPhone();
//获取验证码
String code = ValidateCodeUtils.generateValidateCode4String(6);
//调用对应的邮箱服务
sendEmail.sendMail( "2041742155@qq.com",phone, "吃货外卖","短信验证码为"+code);
//将验证码存入session进行比对
session.setAttribute(phone,code);
return Result.success("验证码发送成功");
}
发送邮箱工具类
/**
* 发送qq邮箱工具
* 导入对应的stater
* 在yml中进行配置
* 调用对应的api
*/
@Component
public class SendEmail {
@Resource
private JavaMailSender sender;
public void sendMail(String from,String to,String subjext,String context) {
SimpleMailMessage message = new SimpleMailMessage();
//将信息封装到message中
message.setFrom(from+"(XX)");
message.setTo(to);
message.setSubject(subjext);
message.setText(context);
sender.send(message);
}
}
3.代码测试
验证码发送
点击获取验证码,同时观察控制台发送的ajax请求
手机qq上也收到对应的验证码
4.登录功能模块开发
在接收到对应的code验证码后,点击登录完成跳转,这里要先判断一下是否能接收到参数,来进行登录的参数校验,由于参数为邮箱和验证码,不能单一的用user对象接收,可以使用DTO或者MAP集合来接收参数
通过断点调试发现后台可以接收到这两个参数
随后进行控制器的逻辑验证来进行登录功能
//登录验证接口
@PostMapping("/login")
public Result<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(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if (user==null){
//新用户注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
//成功登录后把userid存入session
session.setAttribute("user",user.getId());
return Result.success(user);
}
//失败
return Result.error("登录失败");
}
功能测试
输入邮箱获取对应的验证码,并点击登录
成功登录
由于是新用户,后台则为此用户创建了信息,观察数据库
第十九天-菜品展示与购物车模块开发
1.菜品展示模块开发
ajax交互过程
观察移动端调试器两次请求都已经通过之前的代码完成调用回显
第一次ajax请求,用于展示左侧的分类管理,调用之前分类展示接口(新增菜品或套餐选择对应分类)
第二次ajax请求,用于展示默认的第一个分类下的菜品,调用之前菜品展示的接口(新增套餐时选择对应菜品)
此时每个菜品还缺少对应的口味选择
观察控制器发现,返回的集合为菜品类,并没有对应口味信息,因此需要改造一个控制器
控制器优化:
//查询指定菜品
@GetMapping("/list")
public Result<List<DishDto>> list(Dish dish){
//条件构造器
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper();
//添加查询条件
wrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//当菜品停售后不显示
wrapper.eq(Dish::getStatus,1);
//添加排序条件
wrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(wrapper);
List<DishDto> dtolist = list.stream().map((item)->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
//当前菜品id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishId);
//当前菜品口味集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return Result.success(dtolist);
}
测试发现出现对应的规格选择
但是套餐仍然有bug,需要查询对应套餐中的菜品
在SetmealController中加入新接口
//查询对应的套餐中的菜品
@GetMapping("/list")
public Result<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
//查询对应id的菜品和状态
wrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
wrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
wrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(wrapper);
return Result.success(list);
}
测试成功显示!
2.购物车模块开发
需求分析
数据模型
ajax交互过程
添加购物车接口
//添加菜品或者套餐
@PostMapping("/add")
public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
//设置当前操作的用户id
Long userid = BaseContext.getCurrentId();
shoppingCart.setUserId(userid);
//先获取菜品id ,在一次add的ajax请求中因为套餐和菜品只会传一个id
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userid);
//通过菜品id判断当前购物车中添加的是菜品还是套餐
if (dishId!=null){
//购物车中为菜品
wrapper.eq(ShoppingCart::getDishId,dishId);
}else {
//购物车中为套餐
wrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//获取当前菜品或者套餐是否在购物车中
ShoppingCart serviceOne = shoppingCartService.getOne(wrapper);
//判断是否以及添加过一次
if (serviceOne!=null) {
//如果已经存在,将number加一
Integer number = serviceOne.getNumber();
serviceOne.setNumber(number+1);
shoppingCartService.updateById(serviceOne);
}else {
//如果不存在,添加购物车,默认数量设置为1
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
//添加完后为其赋值,下次则不为null,即将number+1
serviceOne = shoppingCart;
}
return Result.success(serviceOne);
}
功能测试
购物车展示,清空接口
//查询当前用户的购物车内容
@GetMapping("/list")
public Result<List<ShoppingCart>> list(){
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
wrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(wrapper);
return Result.success(list);
}
//清空购物车
@DeleteMapping("/clean")
public Result<String> clean(){
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(wrapper);
return Result.success("删除成功");
}
功能测试
第二十天-用户下单模块开发
1.需求分析
前面三个ajax请求都已经在之前实现了,因此只需要完成去支付的ajax请求即可
2.代码设计
搭建好基本的接口
业务层处理订单
@Service
@Transactional
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
@Override
public void submit(Orders orders) {
//先获取当前用户id
Long userid = BaseContext.getCurrentId();
//获取购物车中的菜品和套餐
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper();
wrapper.eq(ShoppingCart::getUserId,userid);
//此时通过userid查询出来的购物车数据为一条集合
List<ShoppingCart> list = shoppingCartService.list(wrapper);
if (list==null || list.size()==0){
throw new CustomException("购物车为空,不能支付");
}
//查询用户数据
User user = userService.getById(userid);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
//生成订单号
long orderId = IdWorker.getId();
//原子操作,线程安全
AtomicInteger amount = new AtomicInteger(0);
//遍历购物车计算总金额,并处理订单明细对象
List<OrderDetail> orderDetails = list.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()));
//向订单明细表插入数据,多条数据
orderDetailService.saveBatch(orderDetails);
//清空购物车数据
shoppingCartService.remove(wrapper);
}
}
3.代码测试
首先选好对应的菜品,点击去结算
后台完成对应的四次ajax请求
点击去支付,提示下单成功
结尾:当然到这里并没有完结,只是把基本的功能开发完毕,项目还有很多bug和缺陷,还有许多要优化的地方,我们下篇见