瑞吉外卖
主要讲解后端代码思路
1,环境搭建
数据库环境搭建
导入项目所需的jar包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
配置application.yml文件
server:
port: 8080
spring:
application:
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
password: lss
username: root
mail:
host: smtp.qq.com
username: 854511856@qq.com
password: uxgfymlnqokvbcie
default-encoding: UTF-8
test-connection: true
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
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
reggie:
path: D:\photos\
导入前端资源
backend:客户端
front:移动端
创建配置类WebMVCConfig,设置静态资源映射,可以访问前端页面
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
2,优化前功能开发
1,后台登录功能
1)登录页面展示
2)查看登录请求信息
通过浏览器调试工具,可以发现,点击登录按钮时,页面会发送请求(请求地址为http://localhost:8080/employee/login)并提供参数(username和password)
前端代码
3)后端代码
创建Controller、Service、Mapper
在controller中创建登录方法
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Resource(name = "employeeService")
private EmployeeService employeeService;
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
if(emp == null){
return R.error("登录失败");
}
if(!emp.getPassword().equals(password)){
return R.error("登录失败");
}
if(emp.getStatus() == 0){
return R.error("账号已禁用");
}
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
}
2,后台退出功能
需求分析
前端代码
后端代码
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
3,完善登录功能
@Component
@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;
String requestURI = request.getRequestURI();
log.info("拦截到请求: {}",requestURI);
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login"
};
boolean check = check(urls, requestURI);
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
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;
}
}
4,新增员工功能
需求分析
数据模型
代码开发
前端代码
后端代码
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employeeService.save(employee);
return R.success("新增员工成功");
}
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class GlobalExceptionHandler {
@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("失败了");
}
}
5,员工信息分页查询功能
代码开发
前端代码
后端代码
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page = {},pageSize = {},name = {}",page,pageSize,name);
Page pageInfo = new Page(page,pageSize);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
queryWrapper.orderByDesc(Employee::getUpdateTime);
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
6,启用/禁用员工账号功能
需求分析
前端代码
后端代码
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
功能测试
代码修复
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);
}
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
converters.add(0,messageConverter);
}
7,编辑员工信息功能
需求分析
代码开发
前端代码
第一次交互
后端代码
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
第二次交互
前端代码
8,公共字段自动填充功能
问题分析
代码实现
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info(metaObject.toString());
log.info("公共字段自动填充insert...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info(metaObject.toString());
log.info("公共字段自动填充update...");
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
功能完善
9,新增分类功能
需求分析
后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcwPR1ib-1660546100845)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815110958284.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-buRGa9GN-1660546100847)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815111008037.png)]
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zoQX5P6U-1660546100848)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815111037960.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxFTj6yf-1660546100848)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815111047924.png)]
数据模型
新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTB65LM3-1660546100848)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815111156292.png)]
需要注意,category表中对name字段加入了唯一约束,保证分类的名称是唯一的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHwZleE2-1660546100849)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815111218312.png)]
代码开发
1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55pqVhM7-1660546100849)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815112058497.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgXyHmoo-1660546100849)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815112106509.png)]
前端代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-coojjyKC-1660546100850)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815112029344.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nL9dU3Hf-1660546100850)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815112158945.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2tRrq2sc-1660546100850)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815112226836.png)]
后端代码
@PostMapping
public R<String> save(@RequestBody Category category) {
log.info("category:{}", category);
categoryService.save(category);
return R.success("新增分类成功");
}
10,分类信息分页查询功能、
需求分析
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上
前端代码
页面中使用的是ElementUI提供的分页组件进行分页条的展示
页面中创建VUE对象完成后会调用init方法,在init方法中发送ajax请求并提交分页参数(page、pageSize),请求服务端Controller进行分页查询
后端代码
@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);
}
11,删除分类功能
需求分析
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYb0zE2p-1660546100852)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815113424023.png)](https://img-blog.csdnimg.cn/d8ec7c9cd5674df9b8a4c89c017e4ac7.png)
后端代码
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);
//categoryService.removeById(ids);
categoryService.remove(ids);
return R.success("分类信息删除成功");
}
功能完善
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServiceImpl和SetmealServiceImpl
具体实现步骤:
1)创建自定义业务异常类
2)在CategoryService中扩展remove方法
3)在CategoryServiceImpl中实现remove方法
4)在GlobalExceptionHandler中处理自定义异常
12,修改分类功能
需求分析
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击修改按钮时,弹出修改窗口并回显数据
2、点击确定按钮,发送ajax请求,将修改窗口的分类信息以json方式提交给服务端
3、服务端接收分类信息,并进行处理,完成后给页面响应
4、页面接收到服务端响应信息后进行相应处理
后端代码
13,文件上传下载功能
文件上传
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的form表单有如下要求:
lmethod=“post” 采用post方式提交数据
lenctype=“multipart/form-data” 采用multipart格式上传文件
ltype=“file” 使用input的file控件上传
举例:
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。
例如ElementUI中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
lcommons-fileupload
lcommons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
文件下载
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
文件下载,页面端可以使用标签展示下载的图片
14,新增菜品功能
需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。
所以在新增菜品时,涉及到两个表:
dish 菜品表
dish_flavor 菜品口味表
数据模型
菜品表dish:
菜品口味表dish_flavor:
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类 DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
Mapper接口 DishFlavorMapper
业务层接口 DishFlavorService
业务层实现类 DishFlavorServiceImpl
控制层 DishController
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器,直接使用我们前面开发的CommonController的upload方法来处理即可。
3、页面发送请求进行图片下载,将上传的图片进行回显,直接使用我们前面开发的CommonController的download方法来处理即可。
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
导入DishDto
新增菜品时,如果页面添加了对应的口味,最终需要向dish和dish_flavor两个表插入数据,为了保证数据一致性,需要启用事务管理,可以在项目启动类上加入EnableTransactionManagement注解:
在DishController中提供save方法,此处先保证数据能够正确接收到,具体逻辑后续再完善:
在DishService接口中扩展saveWithFlavor方法:
在DishServiceImpl类中实现saveWithFlavor方法:
在DishController中调用业务层方法完成菜品和口味的保存
15,菜品信息分页查询功能
需求分析
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
16,修改菜品功能
需求分析
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作
代码开发
在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
页面发送ajax请求,请求服务端获取分类数据,用于修改页面中菜品分类下拉框中数据展示。
前面我们在开发新增菜品时已经完成了查询分类数据的代码开发,此处直接使用此方法即可,如下:
页面发送ajax请求,请求服务端,根据id查询当前菜品信息和对应的口味信息,用于修改页面中菜品信息回显。
在DishService接口中扩展getByIdWithFlavor方法:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uZLYK0RW-1660546100861)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220815134827795.png)](https://img-blog.csdnimg.cn/9f5a9ef058bf40dda04e52444c245236.png)
在DishServiceImpl中实现getByIdWithFlavor方法:
页面发送ajax请求,请求服务端,根据id查询当前菜品信息和对应的口味信息,用于修改页面中菜品信息回显。
在DishController中创建get方法:
页面发送请求,请求服务端进行图片下载,用于修改页面图片回显。
前面我们已经在CommonController中提供了下载方法download,此处直接使用即可。
点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端。
在修改菜品信息时需要注意,除了要更新dish菜品表,还需要更新dish_flavor菜品口味表。
在DishService接口中扩展方法updateWithFlavor:
在DishServiceImpl中实现方法updateWithFlavor:
在DishController中创建update方法:
17,新增套餐功能
需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。
所以在新增套餐时,涉及到两个表:
setmeal 套餐表
setmeal_dish 套餐菜品关系表
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类 SetmealDish
DTO SetmealDto
Mapper接口 SetmealDishMapper
业务层接口 SetmealDishService
业务层实现类 SetmealDishServiceImpl
控制层 SetmealController
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
在DishController中创建list方法,根据条件查询菜品信息:
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器,直接使用我们前面开发的CommonController的upload方法来处理即可。
5、页面发送请求进行图片下载,将上传的图片进行回显,直接使用我们前面开发的CommonController的download方法来处理即可。
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
在SetmealController中提供save方法,此处先保证数据能够正确接收到,具体逻辑后续再完善:
在SetmealService接口中扩展saveWithDish方法:
在SetmealServiceImpl类中实现saveWithDish方法:
在SetmealController中调用业务层方法完成套餐的保存:
18,套餐信息分页查询功能
需求分析
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
在SetmealController中创建分页查询方法
19,删除套餐功能
需求分析
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
代码开发
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程:
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
在SetmealController中创建delete方法,接收页面提交的参数,具体逻辑后续再完善:
在SetmealService接口中扩展removeWithDish方法:
完善SetmealController的delete方法:
20,邮箱发送短信验证码功能
1,导入依赖
<!--mail短信依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2,编写resources下的yml配置文件
spring:
mail:
host: smtp.qq.com
# 你的QQ邮箱,这里为发件人,填自己的即可
username: *********@qq.com
# 你的QQ邮箱授权码
password: ************
三、获取QQ邮箱授权码
1、打开QQ邮箱网页版点击设置
2、打开设置后点击账户
3、在账户页下拉找到POP3/SMTP服务开启获得授权码
4、把授权码编写到resources下的yml里即可
代码开发
前端代码
第一次请求
第二次请求
后端代码
1、编写Service层的UserService
public interface UserService extends IService<User> {
//发送邮件
void sendMsg(String to,String subject,String text);
}
2、编写Service里Impl层的UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
//把yml配置的邮箱号赋值到from
@Value("${spring.mail.username}")
private String from;
//发送邮件需要的对象
@Autowired
private JavaMailSender javaMailSender;
//邮件发送人
@Override
public void sendMsg(String to, String subject, String text) {
//发送简单邮件,简单邮件不包括附件等别的
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(text);
//发送邮件
javaMailSender.send(message);
}
}
3,在过滤器放行验证码请求和登录请求
4,编写UserController层
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
//获取验证码
@PostMapping("/sendMsg")
public R<String> sendMsg(HttpSession session, @RequestBody User user){
//获取邮箱号
//相当于发送短信定义的String to
String email = user.getPhone();
String subject = "瑞吉外卖";
//StringUtils.isNotEmpty字符串非空判断
if (StringUtils.isNotEmpty(email)) {
//发送一个四位数的验证码,把验证码变成String类型
String code = ValidateCodeUtils.generateValidateCode(4).toString();
String text = "【瑞吉外卖】您好,您的登录验证码为:" + code + ",请尽快登录";
log.info("验证码为:" + code);
//发送短信
userService.sendMsg(email,subject,text);
//将验证码保存到session当中
session.setAttribute(email,code);
return R.success("验证码发送成功");
}
return R.error("验证码发送异常,请重新发送");
}
//登录
@PostMapping("/login")
//Map存JSON数据
public R<User> login(HttpSession session,@RequestBody Map map){
//获取邮箱,用户输入的
String phone = map.get("phone").toString();
//获取验证码,用户输入的
String code = map.get("code").toString();
//获取session中保存的验证码
Object sessionCode = session.getAttribute(phone);
//如果session的验证码和用户输入的验证码进行比对,&&同时
if (sessionCode != null && sessionCode.equals(code)) {
//要是User数据库没有这个邮箱则自动注册,先看看输入的邮箱是否存在数据库
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
//获得唯一的用户,因为手机号是唯一的
User user = userService.getOne(queryWrapper);
//要是User数据库没有这个邮箱则自动注册
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
//取邮箱的前五位为用户名
user.setName(phone.substring(0,6));
userService.save(user);
}
//不保存这个用户名就登不上去,因为过滤器需要得到这个user才能放行,程序才知道你登录了
session.setAttribute("user", user.getId());
return R.success(user);
}
return R.error("登录失败");
}
}
21,导入用户地址簿相关功能代码
需求分析
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
数据模型
用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
功能代码清单:
实体类 AddressBook
Mapper接口 AddressBookMapper
业务层接口 AddressBookService
业务层实现类 AddressBookServiceImpl
控制层 AddressBookController
22,菜品展示
需求分析
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示
按钮,否则显示+按钮。
代码开发
在开发代码之前,需要梳理一下前端页面和服务端的交互过程:
1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
在修改DishController的list方法,原来此方法的返回值类型为:R<List>。为了满足移动端对数据的要求(菜品基本信息和菜品对应的口味信息),现在需要将方法的返回值类型改为:R<List>
在SetmealController中创建list方法,根据条件查询套餐数据。
23,购物车
需求分析
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击
将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
数据模型
购物车对应的数据表为shopping_cart表,具体表结构如下:
代码开发
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:
1、点击
或者
按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类 ShoppingCart
Mapper接口 ShoppingCartMapper
业务层接口 ShoppingCartService
业务层实现类 ShoppingCartServiceImpl
控制层 ShoppingCartController
添加购物车
查看购物车
清空购物车
24,用户下单
需求分析
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的
按钮,页面跳转到订单确认页面,点击 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
按钮则完成下单操作。
数据模型
用户下单业务对应的数据表为orders表和order_detail表:
orders:订单表
order_detail:订单明细表
在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
1、在购物车中点击
按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击 按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类 Orders、OrderDetail
Mapper接口 OrderMapper、OrderDetailMapper
业务层接口 OrderService、OrderDetailService
业务层实现类 OrderServiceImpl、OrderDetailServiceImpl
控制层 OrderController、OrderDetailController
在OrderController中创建submit方法:
在OrderService接口中扩展submit方法:
@Transactional
public void submit(Orders orders) {
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
User user = userService.getById(userId);
AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetails = new ArrayList<>();
for (ShoppingCart shoppingCart : shoppingCarts) {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(shoppingCart.getNumber());
orderDetail.setDishFlavor(shoppingCart.getDishFlavor());
orderDetail.setDishId(shoppingCart.getDishId());
orderDetail.setSetmealId(shoppingCart.getSetmealId());
orderDetail.setName(shoppingCart.getName());
orderDetail.setImage(shoppingCart.getImage());
orderDetail.setAmount(shoppingCart.getAmount());
amount.addAndGet(shoppingCart.getAmount().multiply(new BigDecimal(shoppingCart.getNumber())).intValue());
orderDetails.add(orderDetail);
}
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(queryWrapper);
}