《瑞吉外卖》项目
一、功能框架图、技术框架图及技术亮点
1.功能框架图
2.技术框架图
3.技术亮点
(1)使用Redis缓存
当用户数量较多时,系统访问量大,频繁的访问数据库,数据库压力大,系统的性能下降,用户体验感差。因此使用Redis对数据进行缓存,从而减小数据库的压力,在数据更新时删除缓存,从而保证数据库和缓存的一致性,同时有效提高系统的性能和访问速度。
(2)使用MySQL主从复制,进行读写分离
读和写数据的所有压力全都由一台数据库承担,压力大,数据库服务器磁盘损坏则数据丢失,单点故障。使用 MySQL进行主从复制,主库进行写操作,从库进行读操作,从而减轻数据库负担,增大系统承受能力,提高系统性能。本项目使用Sharding-JDBC在程序中实现读写分离。
(3)前后端分离部署,使用Nginx进行反向代理及负载均衡
前端页面部署到Nginx服务器中,后端代码部署到后端服务器中,使用Nginx对后端服务器进行反向代理,使用户只需要访问Nginx服务器便可获得后端服务器的服务(便于后期扩展集群,提高系统并发量)。
二、开发环境搭建
1.数据库环境搭建
(1)创建数据库
(2)运行sql脚本文件,创建表格
(3)数据表
2.Maven项目搭建
(1)创建空项目
创建完后,检查编码、mvn仓库、jdk的配置。
(2)导入pom文件和application.yml配置文件
(3)创建启动类ReggieApplication
//lombok提供的查看日志的注解
@Slf4j
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class);
//将日志输出到控制台
log.info("项目启动成功。。。");
}
}
(4)导入项目所需的静态资源将静态资源放在resources目录下。
在创建SpringBoot项目的时候,resources目录下有static和template,系统会默认在这两个目录底下寻找前端静态资源,而我们是放在resources下的backend和front目录中,系统找不到静态资源(404),所以要进行静态资源映射,让系统从backend和front中寻找静态资源。
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
//addResourceHandler是访问路径;addResourceLocations是映射后的真实路径,结尾必须要加上/,不然找不到。
//http://localhost:8080/backend/index.html
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
三、后台管理端登录功能开发
1.需求分析
从产品原型出发,分析登录功能:输入用户名和密码,点击登录按钮,查看登录请求信息:打开开发者工具(F12),点击登录按钮后,页面会发送请求(请求地址为http://localhost:8080/employee/login)并提交请求参数(json数据)。
2.代码开发
(1)创建通用返回结果类R
@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;
}
}
(2)创建Mapper
@Mapper
public interface EmployeeMapper extends BaseMapper<EmployeeMapper> {
}
(3)创建Service接口和实现类
public interface EmployeeService extends IService<Employee> { }
@Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService{ }(4)创建Controller
@Slf4j @RestController @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; }(5)创建登录方法
1)分析登录逻辑
2)代码开发
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
//1、将页面提交的明文密码进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());//加密后的密码
//2、根据页面提交的用户名来查数据库
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(lqw);
//3、如果没有查询到则返回失败结果
if(emp == null){
return R.error("登录失败!");
}
//4、比对密码,如果不一致则返回失败结果
if(!emp.getPassword().equals(password)){
return R.error("登录失败!");
}
//5、查看员工状态,如果已禁用状态,则返回员工已禁用结果
if(emp.getStatus() == 0){
return R.error("账号已禁用!");
}
//6、登录成功,将用户id存入Session并返回成功结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
3.功能测试
四、后台退出功能开发
1.需求分析
点击页面中的退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。2.代码开发
(1)逻辑分析
(2)代码开发
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//1.清理Session中保存的当前员工登录的id
request.getSession().removeAttribute("employee");
//2.返回成功信息
return R.success("退出成功!");
}
3.功能测试
五、完善登录功能
1.需求分析
之前的登录功能存在一个bug,不需要登录直接输入index.html也能访问员工管理页面。因此,我们需要完善登录功能,只有登录成功后才能访问员工管理页面,如果未登录自动跳转到登录页面。
使用过滤器或拦截器判断用户是否登录,若未登录,就跳转到登录页面。
2.代码开发
(1)逻辑分析
(2)代码开发
1)创建自定义过滤器
//配置要拦截的资源路径
@WebFilter("/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@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());//拦截到请求:/backend/page/login/login.html
filterChain.doFilter(request,response);
}
}
2)在启动类上加注解@ServletComponentScan
3)完善过滤器中的处理逻辑
@WebFilter("/*")
@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();
log.info("拦截到请求:{}",requestURI);
//定义不需要被拦截处理的uri数组
String[] uris = new String[]{
"/employee/login",//登录请求
"/employee/logout",//退出请求
"/backend/**",//前台静态资源
"/front/**"//移动端静态资源
};
boolean match = check(uris, request);
//3、如果不需要处理,则直接放行
if (match) {
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request, response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户{}已登录",request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return;
}
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
//2、判断本次请求是否需要处理
public boolean check(String[] uris, HttpServletRequest request) {
for (String uri : uris) {
boolean match = PATH_MATCHER.match(uri, request.getRequestURI());
if (match) return match;
}
return false;
}
}
3.功能测试
六、员工管理功能开发
1.新增员工功能
(1)需求分析
点击“添加员工”,跳转到新增页面。整个程序的执行过程:
- 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端;
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存;
- Service调用Mapper操作数据库,保存数据。
【注意】在employee表中,username作为登录账号是唯一的,因此我们给username字段设置了唯一约束(unique)。
(2)代码开发
在Controller中定义新增员工的save()方法。
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
log.info("新增员工:{}", employee.toString());
//设置初始密码123456(需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);
//添加员工
employeeService.save(employee);
//返回结果信息
return R.success("成功添加新员工!");
}
(3)功能测试
(4)异常处理——账号已存在
基于AOP思想,采用全局异常处理器,集中统一地处理项目中的异常。

/**
* 全局异常处理器
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})//设置拦截被{}里的注解(annotation)注释的controller中的异常
@ResponseBody
@Slf4j
public class ExceptionHandlerAdvice { //异常处理方法
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)//设置要拦截的异常
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
log.error(ex.getMessage());//ex.getMessage()=="Duplicate entry 'zhangsan' for key 'idx_username'"
//判断该异常是否包含“Duplicate entry”即二次录入的错误,如果包含,则是因为账号重已存在。
if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "账号已存在!";
return R.error(msg);
}
return R.error("未知错误");
}
}
【tips】请求-响应式开发需要重点分析数据的流转过程和数据格式:
请求地址、请求方式、请求数据、响应数据、数据格式等。
2.员工信息分页查询功能
(1)需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
- 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
(2)代码开发
1)设置分页拦截器
在MP配置类中设置分页拦截器。
@Configuration
public class MPConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mpi=new MybatisPlusInterceptor();
mpi.addInnerInterceptor(new PaginationInnerInterceptor());
return mpi;
}
}
2)创建分页查询方法
@GetMapping("/page")
public R<Page> selectByPage(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}", page, pageSize, name);
//构造分页构造器
Page<Employee> pageInfo = new Page<>(page, pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNotEmpty(name), Employee::getName, name);
//执行查询
employeeService.page(pageInfo, lqw);
return R.success(pageInfo);
}
(3)功能测试
3.启用/禁用员工账号功能
(1)需求分析
- 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录;
- 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,普通用户登录系统后不显示启用、禁用按钮;
- 页面发送ajax请求,将参数(id、 status)提交到服务端。
(2)代码开发
@PutMapping
public R<String> updateById(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//设置修改时间和修改人
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser((Long)request.getSession().getAttribute("employee"));
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
(3)id不一致问题★
服务端响应给页面的数据中id的值为19位数字,类型为long,而页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id就改变了,即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
1)如何解决?
在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。
2)具体实现
提供对象转换器JacksonObjectMapper类,基于Jackson进行Java对象到json数据的转换。在WebMvcConfig中重写extendMessageConverters方法,扩展mvc框架的消息转换器,将对象转换器设置到消息转换器中,并将消息转换器追加到mvc框架的转换器List集合中。
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器对象,SpringMvc自带8个默认的转换器
MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();
//将对象转换器设置到消息转换器中
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将加了对象转换器的消息转换器追加到mvc框架的转换器List集合中,并放在第一个位置,这样就会优先使用这个消息转换器
converters.add(0,messageConverter);
}
(4)功能测试
4.编辑员工信息
(1)需求分析
点击编辑按钮,跳转到回显了员工信息的编辑页面,进行修改,点击保存完成编辑。
- 点击编辑按钮时,页面跳转到add.html(新增和修改员工都用add.html),并在url中携带员工id
- 在add.html页面获取url中的员工id
- 发送ajax请求,同时提交员工id
- 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
- 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
- 修改员工信息,点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
- 服务端接收员工信息,并进行处理,完成后给页面响应
-
页面接收到服务端响应信息后进行相应处理
(2)代码开发
1)回显员工信息代码
@GetMapping("/{id}")
public R<Employee> selectById(@PathVariable Long id) {
log.info("根据id查员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("未查询到该员工!");
}
2)保存代码保存代码部分可以复用之前启用/禁用员工时的updateById方法,不需要再写个保存方法。
(3)功能测试
5.公共字段自动填充
MP提供公共字段自动填充功能,即在插入或更新时为指定字段赋予指定的值,避免重复代码。
(1)问题分析
在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段即多个表中都有的字段。
可以使用Mybatis Plus提供的公共字段自动填充功能对公共字段进行统一处理。
(2)代码实现
1)在实体类的公共字段属性上加入@TableField注解,指定自动填充策略(fill = 填充策略)
@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;
2)按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
使用ThreadLocal在验证登录时的过滤器中通过Session获取当前登录用户的id,并用ThreadLocal的set方法设置当前线程的线程局部变量的值(id),然后在需要用到id时调用ThreadLocal的get方法获取id。
3)具体实现
①编写基于ThreadLocal封装的工具类BaseContext,用于保存和获取当前登录用户的id
public class BaseContext {
private static ThreadLocal<Long> threadLocal=new ThreadLocal<>();
public static void setId(Long id){
threadLocal.set(id);
}
public static Long getId(){
Long id = threadLocal.get();
return id;
}
}
②在LogincheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
③在MyMeta0bjectHandler的方法中调用BaseContext获取登录用户的id
【★注意★】
①在元数据对象处理器中不能获得HttpSession对象,所以我们不能通过Session来获取登录用户id;可以通过JDK提供的ThreadLocal类来实现。客户端发送的每次http请求,在服务端都会分配一个新线程(Thread)来处理,在处理过程中涉及到的Filter、Controller、MetaObjectHandler都属于同一个线程。
②ThreadLocal并不是一个线程,而是线程的局部变量,可以用来存储线程数据。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。即ThreadLocal为每个线程提供单独的存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
③ThreadLocal常用方法:public void set(T value) 设置当前线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
七、分类管理功能开发
1.新增分类
(1)需求分析
1)后台系统的分类管理可以管理分类信息,分类包括两种类型,分别是新增菜品分类和新增套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
2)分类对应的数据库表是category,其中name字段加了unique约束。
3)新增菜品分类和新增套餐分类的请求地址和提交的json数据结构相同,所以服务端只需要提供一个方法即可。
(2)代码开发
1)创建实体类Category
2)创建Mapper
3)创建Service接口和实现类
4)创建Controller,创建save方法
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category.toString());
categoryService.save(category);
return R.success("新增成功!");
}
(3)功能测试
2.分类信息分页查询
(1)需求分析
分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
- 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
(2)代码开发
在Controller中创建分页方法
@GetMapping("/page")
public R<Page> selectByPage(int page,int pageSize){
Page<Category> pageInfo=new Page<>(page,pageSize);
//构造条件构造器对象
LambdaQueryWrapper<Category> lqw=new LambdaQueryWrapper<>();
//添加排序条件,根据sort字段进行排序
lqw.orderByAsc(Category::getSort);
//进行分页查询
categoryService.page(pageInfo,lqw);
return R.success(pageInfo);
}
(3)功能测试
3.删除分类★
(1)需求分析
对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
- 页面发送ajax请求,将参数(id)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service删除数据
- Service调用Mapper操作数据库
(2)代码开发
1)在CategoryService中增加删除未关联菜品/套餐的分类的删除方法remove(),并在Impl中重写该方法★
public interface CategoryService extends IService<Category> {
void remove(Long id);
}
@Override
public void remove(Long id) {
//查询要删除的菜品分类是否关联菜品,若关联,抛出一个异常
LambdaQueryWrapper<Dish> dishLqw = new LambdaQueryWrapper<>();
//添加查询条件,根据id进行查询
dishLqw.eq(Dish::getCategoryId, id);
int dishCount = dishService.count(dishLqw);
if (dishCount > 0) {
//关联菜品,抛出一个异常
throw new CustomException("当前菜品分类关联了菜品,不能删除!");
}
//查询要删除的套餐分类是否关联套餐,若关联,抛出一个异常
LambdaQueryWrapper<Setmeal> setmealLqw = new LambdaQueryWrapper<>();
setmealLqw.eq(Setmeal::getCategoryId, id);
int setmealCount = setmealService.count(setmealLqw);
if (setmealCount > 0) {
//关联套餐,抛出一个异常
throw new CustomException("当前套餐分类关联了套餐,不能删除!");
}
//要删除的菜品分类没有关联菜品,删除
super.removeById(id);
}
2)在重写remove方法时,需要抛出异常,所以自定义一个异常类,并添加到异常处理器中
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
/**
* 删除分类异常处理方法
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
(3)功能测试
4.修改分类
(1)需求分析
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
(2)代码开发
1)这次回显分类信息交给前端
2)在Controller中创建修改方法
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category.toString());
categoryService.updateById(category);
return R.success("修改成功!");
}
(3)功能测试
八、菜品管理业务功能开发
1.文件上传/下载
(1)文件上传介绍
文件上传(upload):是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
1)文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件
type="file" 使用input的file控件上传
2)服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:commons-fileupload和commons-io。Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件。
(2)文件下载介绍
文件下载(download):是指将文件从服务器传输到本地计算机的过程。 通过浏览器进行文件下载,通常有两种表现形式:
①以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录;
②直接在浏览器中打开。
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
(3)代码开发
1)文件上传
请求地址:"/common/upload",请求方式为POST请求。
创建文件上传/下载Controller。
//在application.yml中配置转存路径path
//动态获取yml配置文件中的basePath
@Value("${reggie.path}")
private String basePath;
/**
* 上传文件
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {//注意!这里的参数名不能乱起,必须与请求中的name一致!
log.info(file.toString());
//使用UUID动态命名转存文件名★
//1.动态获取file文件后缀suffix
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//2.使用UUID动态生成file文件名,并将文件名和suffix拼接起来
String fileName = UUID.randomUUID().toString() + suffix;
//判断转存路径是否存在,若不存在则新建该文件夹★
File dir=new File(basePath);
if(!dir.exists()){
dir.mkdir();
}
//file只是一个临时文件,需要 转存 到指定位置,否则不是真正的上传,本次请求完成后该临时文件会删除。
try {
//将file临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));//这里转存指定位置路径和文件名要动态获取★
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
2)文件下载
①直接在浏览器中打开文件:文件上传完会回调handleAvatarSuccess方法,?后面的name拿到的就是上传文件的文件名,然后发送请求(红框),在Controller中写个下载文件的方法处理这个请求。
直接在浏览器中打开文件这种文件下载方式主要是通过输入流读取文件内容,再通过输出流将文件写回浏览器,在浏览器展示图片文件。
/**
* 下载文件
* @param name 上传的文件的名字
* @param response 为了从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];//将文件内容读到这个byte数组中
while ((len = fileInputStream.read(bytes)) != -1) {//通过输入流读取文件内容
//通过输出流将文件写回浏览器
outputStream.write(bytes, 0, len);
outputStream.flush();
}
//释放资源
fileInputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
②以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录;
2.新增菜品
(1)需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
(2)数据模型
新增菜品,其实就是将录入的菜品信息加dish表,如果添加了口味做法,还需要向dish_flavor表添加数据。在代码开发前先将需要用到的实体类、Mapper接口、Service接口和实现类以及Controller创建好。
(3)代码开发
新增菜品时前端页面和服务端的交互过程如下,开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
- 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
@GetMapping("/list")
public R<List<Category>> getCatoryByType(Category category) {
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
//添加条件
lqw.eq(category.getType() != null, Category::getType, category.getType());
//添加排序条件
lqw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> categories = categoryService.list(lqw);
return R.success(categories);
}
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,采用POST请求将菜品相关数据以json形式提交到服务端
从请求数据可以看出,页面提交的json数据中包括Dish实体类中的属性和flavors,而flavors是与DishFlavor实体类有关,所以这里需要提供一个DishDto类,继承Dish并添加新的属性,用于封装页面提交的数据。
DTO(Data Transfer Object):数据传输对象,一般用于web层与服务层之间的数据传输。
1)DishDto
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
2)在DishService中创建新增菜品并添加口味的方法saveWithFlavor()
/**
* 新增菜品并保存口味数据,涉及dish表和dish_flavor表
*
* @param dishDto
*/
@Override
@Transactional//因为涉及多张表,所以需要开启事务
public void saveWithFlavors(DishDto dishDto) {
//保存菜品基本信息到dish表
this.save(dishDto);
//保存菜品口味数据到dish_flavor表
//1.获取菜品id
Long dishId = dishDto.getId();
//2.将菜品id存入每个flavor
List<DishFlavor> dishFlavors = dishDto.getFlavors();
for (DishFlavor dishFlavor : dishFlavors) {
dishFlavor.setDishId(dishId);
}
//3.将口味数据存入dish_flavor表中的对应菜品(通过菜品id来实现)
dishFlavorService.saveBatch(dishFlavors);
}
【tips】因为涉及多张表,所以要开启事务3)在DishFlavorController中创建创建新增菜品并添加口味的方法save()
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavors(dishDto);
return R.success("新增菜品成功!");
}
(4)功能测试
3.菜品分页查询
(1)需求分析
菜品分页查询除了要展示dish的基本信息外,还要展示图片和菜品分类名称,这就涉及到文件下载和category表。
(2)代码开发
1)菜品分页查询时前端页面和服务端的交互过程如下,开发菜品信息分页查询功能,就是在服务端处理前端页面发送的这2次请求即可。
- 页面发送ajax请求,将分页查询参数(page、pageSize、查询菜品时才会发name)提交到服务端,获取分页数据;
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示。
/**
* 菜品信息分页查询★
* @param page
* @param pageSize
* @param dishName
* @return
*/
@GetMapping("/page")
public R<Page> getByPage(int page, int pageSize, String dishName) {
//创建分页构造器对象
Page<Dish> pageInfo = new Page<>(page, pageSize);
Page<DishDto> dishDtoPage=new Page<>();
//创建条件构造器
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
//添加条件,根据菜品名称查询和排序条件
lqw.like(StringUtils.isNotEmpty(dishName), Dish::getName, dishName);
lqw.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo, lqw);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> dishes = pageInfo.getRecords();
List<DishDto> dishDtos = new ArrayList<>();
for (Dish dish : dishes) {
DishDto dishDto=new DishDto();
//根据category_id获取categoryName
Long categoryId = dish.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
//将dish和categoryName存到dishDto中
BeanUtils.copyProperties(dish,dishDto);
dishDto.setCategoryName(categoryName);
}
dishDtos.add(dishDto);
}
//将查到的dishDto添加到Page中
dishDtoPage.setRecords(dishDtos);
return R.success(dishDtoPage);
}
(3)功能测试
4.修改菜品
(1)需求分析
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品信息并进行修改,最后点击确定按钮完成修改操作。
(2)代码开发
1)修改菜品时前端页面(add.html)和服务端的交互过程(处理4次请求):
- 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(在新增菜品时已完成)
- 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
/**
* 获取菜品和口味信息
* @param id
* @return
*/
@Override
public DishDto getDishAndFlavor(Long id) {
//从dish表中根据id查询菜品信息
Dish dish = this.getById(id);
//从dish_flavor表中根据dish_id查询菜品对应的口味信息
LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper();
lqw.eq(DishFlavor::getDishId, dish.getId());
List<DishFlavor> flavorList = dishFlavorService.list(lqw);
//将dish和flavor添加到DTO中
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish, dishDto);
dishDto.setFlavors(flavorList);
return dishDto;
}
②在Controller中处理该次请求:
/**
* 根据id查询菜品和口味信息,涉及dish和dish_flavor两张表
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> getDishAndFlavorById(@PathVariable() Long id) {
DishDto dishDto = dishService.getDishAndFlavor(id);
return R.success(dishDto);
}
- 页面发送请求,请求服务端进行图片下载,用于页图片回显(已实现)
- 点击保存按钮,页面发送ajax请求,将修改后的菜品数据以json形式提交到服务端
①先在Service中创建更新菜品和口味信息的新方法,并在实现类中重写该方法:
/**
* 修改菜品和口味数据
* @param dishDto
*/
@Override
@Transactional//因为涉及多张表,所以需要开启事务
public void updateWithFlavors(DishDto dishDto) {
//更新dish表
this.updateById(dishDto);
//更新dish_flavor表,可以先删除delete当前菜品对应的口味数据,再添加insert修改后的口味数据
//1.删除当前菜品对应的口味数据
dishFlavorService.removeById(dishDto.getId());
//2.添加修改后的口味数据
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishDto.getId());
}
dishFlavorService.saveBatch(flavors);
}
②在Controller中处理该次请求:/**
* 保存修改后的菜品和口味信息
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
dishService.updateWithFlavors(dishDto);
return R.success("修改菜品成功!");
}
(3)功能测试
5.(批量)停售/启售
(1)需求分析
点击停售/启售按钮页面会发送请求,向服务器发送数据:
(2)代码开发
/**
* (批量)停售/启售
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable int status, Long[] ids) {
for (Long id : ids) {
Dish dish = dishService.getById(id);
dish.setStatus(status);
dishService.updateById(dish);
}
String msg = status == 1 ? "启售" : "停售";
return R.success("批量" + msg + "成功!");
}
(3)功能测试
6.(批量)删除
(1)需求分析
点击删除按钮页面会发送请求,向服务器发送数据:
(2)代码开发
/**
* (批量)删除
* @param ids
* @return
*/
@DeleteMapping
public R<String> deleteByIds(Long[] ids){
for (Long id : ids) {
dishService.removeById(id);
}
return R.success("删除成功!");
}
(3)功能测试
九、套餐管理业务开发
1.新增套餐
(1)需求分析
在新增套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。新增套餐就是将套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。
(2)代码开发
1)新增套餐时前端页面和服务端的6次交互过程:
- 页面(backend/page/comboladd.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(新增菜品时已实现)
- 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(已实现)
- 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
2)代码开发
①根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中:
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> getByConditions(Dish dish) {
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> dishList = dishService.list(lqw);
return R.success(dishList);
}
②将套餐相关数据以json形式提交到服务端
先在service中创建保存套餐信息和套餐与菜品关联关系信息的方法,然后在controller中创建方法处理请求:
/**
* 保存套餐信息和套餐与菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//将套餐信息保存到setmeal表
this.save(setmealDto);
//将套餐和菜品的关联信息保存到setmeal_dish表
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealDto.getId());
}
setmealDishService.saveBatch(setmealDishes);
}
/**
* 新增套餐,涉及setmeal表和setmeal_dish表
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return R.success("添加套餐成功!");
}
(3)功能测试
2.套餐信息分页查询
(1)需求分析
(2)代码开发
1)套餐分页查询时前端页面和服务端的2次交互过程:
- 页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示
/**
* 分页查询
* @param page
* @param pageSize
* @param setmealName
* @return
*/
@GetMapping("/page")
public R<Page> getByPage(int page, int pageSize, String setmealName) {
Page<Setmeal> setmealPage = new Page<>(page, pageSize);
Page<SetmealDto> setmealDtoPage=new Page<>();
BeanUtils.copyProperties(setmealPage,setmealDtoPage,"records");
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
lqw.like(setmealName != null, Setmeal::getName, setmealName);
lqw.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(setmealPage,lqw);
List<Setmeal> setmeals = setmealPage.getRecords();
List<SetmealDto> setmealDtos = new ArrayList<>();
for (Setmeal setmeal : setmeals) {
SetmealDto setmealDto=new SetmealDto();
BeanUtils.copyProperties(setmeal,setmealDto);
Long categoryId = setmeal.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
setmealDto.setCategoryName(category.getName());
}
setmealDtos.add(setmealDto);
}
setmealDtoPage.setRecords(setmealDtos);
return R.success(setmealDtoPage);
}
(3)功能测试
3.修改套餐
(1)需求分析
点击修改按钮,转到修改套餐页面,回显原套餐数据,修改后点击保存。
(2)代码开发
1)修改套餐时前端页面(add.html)和服务端的交互过程:
- 页面发送ajax请求,请求服务端获取分类数据,用于套餐分类下拉框中数据展示(已实现)
- 页面发送ajax请求,请求服务端,根据id查询当前套餐信息,用于套餐信息回显
- 页面发送请求,请求服务端进行图片下载,用于页面图片回显(已实现)
- 点击保存按钮,页面发送ajax请求,将修改后的套餐数据以json形式提交到服务端
2)代码开发
①回显套餐数据:先在SetmealService中添加根据id查询套餐信息及其与菜品关联关系的getById方法,并在实现类中重写该方法;然后在Controller中处理Get请求
/**
* 根据id获取套餐信息及套餐与菜品的关联关系
* @param id
*/
@Override
public SetmealDto getWithDish(Long id) {
Setmeal setmeal = this.getById(id);
LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
lqw.eq(SetmealDish::getSetmealId, id);
List<SetmealDish> setmealDishList = setmealDishService.list(lqw);
SetmealDto setmealDto = new SetmealDto();
setmealDto.setSetmealDishes(setmealDishList);
BeanUtils.copyProperties(setmeal, setmealDto);
return setmealDto;
}
/**
* 根据id获取套餐信息及套餐与菜品的关联关系
* @param id
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> getById(@PathVariable Long id) {
SetmealDto setmealDto = setmealService.getWithDish(id);
return R.success(setmealDto);
}
②保存修改数据:先在SetmealService中添加修改套餐信息及其与菜品关联关系的updateWithDish方法,并在实现类中重写该方法;然后在Controller中处理Put请求
/**
* 修改套餐,同时保存套餐和菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void updateWithDish(SetmealDto setmealDto) {
//将修改后的套餐信息保存到setmeal表
this.updateById(setmealDto);
//将修改后的套餐和菜品的关联信息保存到setmeal_dish表:可以 先删除 对应的关联关系,再将修改后的关联关系 添加 到setmeal_dish表中
setmealDishService.removeById(setmealDto.getId());
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealDto.getId());
}
setmealDishService.saveBatch(setmealDishes);
}
/**
* 保存修改后的套餐信息及套餐与菜品的关联关系
* @param setmealDto
* @return
*/
@PutMapping
public R<String> updateWithDish(@RequestBody SetmealDto setmealDto){
setmealService.updateWithDish(setmealDto);
return R.success("修改成功!");
}
(3)功能测试
4.停售/启售套餐
(1)需求分析
点击停售/启售按钮页面会发送请求,向服务器发送数据:
(2)代码开发
/**
* 批量启售/停售
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable int status,@RequestParam List<Long> ids){
for (Long id : ids) {
Setmeal setmeal = setmealService.getById(id);
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
return R.success("修改售卖状态成功!");
}
(3)功能测试
5.删除套餐
(1)需求分析
点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
(2)代码开发
先在SetmealService中添加删除套餐信息及其与菜品关联关系的deleteWithDish方法,并在实现类中重写该方法;然后在Controller中处理Delete请求
/**
* 删除套餐及关联关系
* @param ids
*/
@Override
@Transactional
public void deleteWithDish(List<Long> ids) {
//查询套餐状态,停售可删除,售卖中的不可删除
//select count(*) from setmeal where id in (?,?,?) and status = 1;
//构造条件查询,添加两个条件
LambdaQueryWrapper<Setmeal> lqwSetmeal = new LambdaQueryWrapper<>();
lqwSetmeal.in(Setmeal::getId, ids);
lqwSetmeal.eq(Setmeal::getStatus, 1);
int saleCount = this.count(lqwSetmeal);
if (saleCount > 0) {
//表示有套餐在售卖,不能删除,抛出业务异常
throw new CustomException("套餐售卖中,不能删除!");
}
//可以删除,删除套餐信息和关联信息
//删除套餐信息
this.removeByIds(ids);
//删除关联信息
LambdaQueryWrapper<SetmealDish> lqwSetmealDish=new LambdaQueryWrapper<>();
lqwSetmealDish.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lqwSetmealDish);
}
/**
* (批量)删除,若启售中则先停售再删除,删除套餐时,既要删除套餐信息,又要删除关联信息
* @param ids
* @return
*/
@DeleteMapping
public R<String> deleteByIds(@RequestParam List<Long> ids) {
setmealService.deleteWithDish(ids);
return R.success("删除成功!");
}
(3)功能测试
十、手机验证码登录(移动端开发)
1.短信发送验证码(使用邮箱代替短信发送验证码)
(1)邮箱发送验证码
(2)使用SpringBoot发送邮件
1)导入依赖
<!--mail短信依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>2)编写yml配置文件
spring: mail: host: smtp.qq.com # 发件人QQ邮箱 username: *********@qq.com # 发件人QQ邮箱授权码 password: ************3)获取QQ邮箱授权码
4)编写业务层
①在UserService和Impl中添加sendMsg方法
//把yml配置的邮箱号赋值到sender
@Value("${spring.mail.username}")
private String sender;
//发送邮件需要的对象
@Resource
private JavaMailSender javaMailSender;
/**
* 发送邮件
* @param receiver 接收方
* @param subject 邮件主题
* @param text 邮件内容
*/
@Override
public void sendMsg(String receiver, String subject, String text) {
//接收简单邮件(不包括附件等别的文件的邮件)
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(sender);
message.setTo(receiver);
message.setSubject(subject);
message.setText(text);
//发送邮件
javaMailSender.send(message);
}
②在Controller中处理获取验证码的post请求/**
* 发送验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpServletRequest request) {
//获取邮箱(这里用的是phone)
String userPhone = user.getPhone();
String subject = "瑞吉外卖登录验证码";
String text = "【瑞吉外卖】您好!登录验证码为:";
if (userPhone != null) {
//生成4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("验证码为{}", code);
//通过邮箱发送验证码
userService.sendMsg(userPhone, subject, text + code);
//将生成的验证码保存到session中(用于验证用户验证码是否正确)
request.getSession().setAttribute(userPhone, code);
return R.success("验证码已发送");
}
return R.error("验证码发送失败");
}
2.手机验证码登录
(1)需求分析
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能,涉及user表。
登录流程:输入手机号→获取验证码→输入验证码→点击登录→登录成功
(2)代码开发
1)登录时前端页面和服务端的2次交互过程:
- 在登录页面(front/page/login.html)输入手机号(邮箱),点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API(sendMsg方法)给指定手机号(影响)发送验证码
- 在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
2)代码开发
/**
* 验证验证码登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map<String, String> map, HttpSession session) {
//获取用户邮箱
String userPhone = map.get("phone");
//获取用户填的验证码
String code = map.get("code");
//获取session中保存的验证码
Object codeInSession = session.getAttribute(userPhone);
//通过比较判断验证码是否正确
if (codeInSession != null && codeInSession.equals(code)) {
//验证码正确,登录成功,查询user表看是不是新用户,是新用户就添加进去
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone, userPhone);
User user = userService.getOne(lqw);
if (user == null) {
user = new User();
user.setPhone(userPhone);
userService.save(user);
}
//登录成功,将用户id存到session,用于检查用户是否登录
session.setAttribute("user",user.getId());
return R.success(user);
}
return R.error("登录失败!");
}
(3)功能测试
十一、移动端菜品展示、购物车、下单功能开发
1.菜品展示
(1)需求分析
用户登录成功后跳转到系统首页,在首页需要根据分类(category)来展示菜品(dish)和套餐(setmeal)。如果菜品设置了口味信息需要展示 [选择规格] 按钮,否则显示 [+] 按钮。
(2)代码开发
1)2次交互过程
- 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类) (已实现)
- 页面发送ajax请求,获取第一个分类下的菜品(修改DishFlavorController中的getDishWithFlavor方法)或者套餐(在SetmealController中提供查询套餐信息)
2)代码开发
/**
* 根据条件查询套餐信息
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> getSetmeal(Setmeal setmeal) {
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
lqw.eq(Setmeal::getCategoryId, setmeal.getCategoryId());
lqw.eq(Setmeal::getStatus, setmeal.getStatus());
lqw.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> setmealList = setmealService.list();
return R.success(setmealList);
}
(3)功能测试
2.购物车功能开发(包含四个操作)
(1)需求分析
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击 [+] 将当前套餐加入购物车。在购物车中可以增加/减少菜品和套餐的数量,也可以清空购物车。
涉及shopping_cart表。
(2)代码开发
1)购物车操作时前端页面和服务端的交互过程:
- 点击 [加入购物车] 或者 [+] 按钮,页面发送ajax请求,请求服务端,将菜品(发dish_id)或者套餐(发setmeal_id)添加到购物车
- 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
- 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
2)代码开发
①添加菜品/套餐到购物车
/**
* 添加菜品/套餐添加到购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> save(@RequestBody ShoppingCart shoppingCart) {
//设置当前用户id(user_id),表示该购物车属于该id的用户
Long user_id = BaseContext.getId();
shoppingCart.setUserId(user_id);
//同一菜品/套餐多次加入购物车不是在表中增加一条新数据,而是修改number字段。因此先根据user_id和dish_id/setmeal_id查询新加菜品/套餐在不在表中,在就直接修改number字段(默认为1)
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId, user_id);
Long dishId = shoppingCart.getDishId();
if (dishId != null) {
//表示添加的是菜品
lqw.eq(ShoppingCart::getDishId, dishId);
} else {
//表示添加的是套餐
lqw.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//查询菜品/套餐是否在表中
ShoppingCart shoppingCartOne = shoppingCartService.getOne(lqw);
if (shoppingCartOne != null) {
//已在表中,number字段+1
Integer number = shoppingCartOne.getNumber();
shoppingCartOne.setNumber(number + 1);
shoppingCartService.updateById(shoppingCartOne);
} else {
//不在表中,加到表中
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
shoppingCartOne = shoppingCart;
}
return R.success(shoppingCartOne);
}
②减少购物车中的菜品/套餐
/**
* 从购物车中减少菜品/套餐
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
//获取用户id
Long userId = BaseContext.getId();
//根据id查询购物车
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId, userId);
Long dishId = shoppingCart.getDishId();
Long setmealId = shoppingCart.getSetmealId();
if (dishId != null) {
//如果是菜品
lqw.eq(ShoppingCart::getDishId, dishId);
} else {
//如果是套餐
lqw.eq(ShoppingCart::getSetmealId, setmealId);
}
ShoppingCart shoppingCartOne = shoppingCartService.getOne(lqw);
Integer number = shoppingCartOne.getNumber();
if (number == 1) {//若number == 1 remove
shoppingCartService.remove(lqw);
} else {//如果number != 1,number--
shoppingCartOne.setNumber(number - 1);
shoppingCartService.updateById(shoppingCartOne);
}
return R.success(shoppingCartOne);
}
③查看购物车
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
//根据用户id查询对应用户的购物车里的信息
Long userId = BaseContext.getId();
LambdaQueryWrapper<ShoppingCart> lqw=new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId,userId);
lqw.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(lqw);
return R.success(list);
}
④清空购物车
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean() {
Long userId = BaseContext.getId();
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId, userId);
shoppingCartService.remove(lqw);
return R.success("清空购物车成功!");
}
(3)功能测试
3.用户下单
(1)需求分析
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 【去结算】 按钮,页面跳转到订单确认页面,点击 【去支付】 按钮则完成下单操作。
涉及订单表orders表和订单明细表order_detail表。
(2)代码开发
1)交互过程:
- 在购物车中点击 【去结算】 按钮,页面跳转到订单确认页面(前端实现)
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址(已实现)
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据(已实现)
- 在订单确认页面点击 【去支付】 按钮,发送ajax请求,请求服务端完成下单操作
2)代码开发
先在OrderService及其实现类中添加submit方法,再在OrderController中调用该方法。
/**
* 用户下单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
//获取当前用户id
Long userId = BaseContext.getId();
//查询用户购物车数据
LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrappernew = new LambdaQueryWrapper<>();
shoppingCartLambdaQueryWrappernew.eq(ShoppingCart::getUserId, userId);
List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrappernew);
if (shoppingCartList == null || shoppingCartList.size() == 0) {
throw new CustomException("购物车为空,不能下单");
}
//计算订单总金额,封装订单明细
long orderId = IdWorker.getId();//生成订单号
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetailList = new ArrayList<>();
for (ShoppingCart shoppingCart : shoppingCartList) {
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());
orderDetailList.add(orderDetail);
}
//查询用户数据及地址
User user = userService.getById(userId);
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if (addressBook == null) {
throw new CustomException("用户地址信息有误,不能下单");
}
//设置orders中的其他属性
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()));
//向orders表插入数据
this.save(orders);
//向order_detail表插入数据
orderDetailService.saveBatch(orderDetailList);
//清空购物车
shoppingCartService.remove(shoppingCartLambdaQueryWrappernew);
}
/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submitOrder(@RequestBody Orders orders){
orderService.submit(orders);
return R.success("下单成功!");
}
(3)功能测试
