做校园网站的公司,深圳大鹏新区葵涌街道,公司网站怎样添加和修改内容,网站建设需要的技术路线目录 Day01业务开发
一、项目总体介绍与展示
二、软件开发整体介绍
#xff08;一#xff09;软件开发流程
三、瑞吉外卖项目介绍
#xff08;一#xff09;项目介绍
#xff08;二#xff09;技术选型功能架构
1.技术选型——
编辑2.功能架构——
编辑 一软件开发流程
三、瑞吉外卖项目介绍
一项目介绍
二技术选型功能架构
1.技术选型——
编辑2.功能架构——
编辑 三角色
四、开发环境搭建
一数据库环境搭建
二maven项目搭建
启动测试
导入
创建配置映射类
五、后台登录功能开发
一需求分析
二代码开发
1.创建实体类Employee和employee表进行映射
2.编写controller层
3.导入通用返回结果类
六、后台退出功能开发
一需求分析
完善登陆添加过滤器
Day02员工管理业务开发
一、启用/禁用员工账号显示不同
一需求分析
管理员——
普通员工——
二代码开发
四代码修复
1.问题描述
2.具体修复步骤
1.自定义的全局转化器
2.在MVC配置文件中追加上面的自定义的全局转化器
二、编辑员工信息
Day03分类业务开发
一、公共字段自动填充
一需求分析
二代码开发
1目前问题——createTime和UpdateTime是固定值应该设置为动态值
2解决方案——threadlocal类解决
4什么是Threadlocal——Thread的局部变量
5代码流程
二、新增分类 三、分类信息分页查询 四、删除分类
五、修改分类
Day04菜品管理业务开发
一、文件上传下载
二、新增菜品 三、菜品信息分页查询
四. 修改菜品(回显和保存修改都是两张表)
菜品信息的回显
保存修改(重点) Day05套餐管理
1. 新增套餐
2. 套餐分页查询 3.删除套餐信息
前台开发手机端
账户登陆
短信发送
代码实现
短信验证码登陆
发送验证码给的资料有点残缺这里修改了
地址管理
导入用户地址簿
手机端展示
菜品展示 套餐展示
购物车
添加菜品和套餐进购物车
查看购物车
清空购物车
减少购物车点菜品或者套餐
用户订单
用户下单功能 用户查看自己订单 Day01业务开发
一、项目总体介绍与展示
1.移动端与管理后台展示 2.项目上线后 3.管理后台登录 4.管理后台详细页面 二、软件开发整体介绍
一软件开发流程 二角色分工 三软件环境 三、瑞吉外卖项目介绍
一项目介绍 两端应用 二技术选型功能架构
1.技术选型——
2.功能架构—— 三角色 四、开发环境搭建
一数据库环境搭建 二maven项目搭建
pom文件 server: port: 9001 spring: application: name: ccTakeOut datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ruiji?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf-8zeroDateTimeBehaviorconvertToNulluseSSLfalseallowPublicKeyRetrievaltrue username: root password: 333 redis: host: localhost # 本地IP 或是 虚拟机IP port: 6379 # password: root database: 0 # 默认使用 0号db cache: redis: time-to-live: 1800000 # 设置缓存数据的过期时间30分钟 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 启动测试
创建测试类并启动 导入前端页面 导入
在默认页面和前台页面的情况下直接把这俩拖到resource目录下直接访问是访问不到的因为被mvc框架拦截了 所以我们要编写一个映射类放行这些资源
创建配置映射类 访问成功 五、后台登录功能开发
一需求分析 2.浏览器F12进入追踪服务 3.整体流程梳理如下—— 为什么点击了登录按钮会转发请求到后端路径/employee/login? 4.code、data、meg信息 二代码开发
1.创建实体类Employee和employee表进行映射
1.MP的智能命名映射 2.编写controller层
使用finalRequiredArgsconstructor注入 3.导入通用返回结果类
通用返回结果服务端响应的数据最终都会封装成此对象 4.controller具体实现 5.加密算法与登录全流程
这里两个字符串的比较没法用!来实现只能equals再取反来判断 直接上代码这里没有涉及service层的操作 查看缓存在浏览器里面的信息—— 六、后台退出功能开发
一需求分析 /*** param request 删除request作用域中的session对象就按登陆的request.getSession().setAttribute(employ,empResult.getId());删除employee就行* return*/PostMapping(/logout)public Result login(HttpServletRequest request) {//尝试删除try {request.getSession().removeAttribute(employ);}catch (Exception e){//删除失败return Result.error(登出失败);}return Result.success(登出成功);}完善登陆添加过滤器
这里的话用户直接url资源名可以随便访问所以要加个拦截器没有登陆时不给访问自动跳转到登陆页面 过滤器配置类注解WebFilter(filterName拦截器类名首字母小写urlPartten“要拦截的路径比如/*”) 判断用户的登陆状态这块之前因为存入session里面有一个名为employee的对象那么只需要看看这个session还在不在就知道他是否在登陆状态 注意想存或者想获取的话就都得用HttpServletRequest的对象来进行获取别的request对象拿不到的 前端拦截器完成跳转到登陆页面不在后端做处理 Day02员工管理业务开发
本章内容介绍
新增员工 员工信息分页查询 启用/禁用员工账号 编辑员工信息 新增员工功能前端对手机号和身份证号长度做了一个校验 改造一下Employee实体类通用id雪花自增算法来新增id 这里用service接口继承的MybatisPlus的功能 注入一下就可以使用了插入方法 基本上都是自动CRUD访问路径com.cc.controller.EmployeeController
PostMapping
public RString addEmployee(HttpServletRequest request,RequestBody Employee employee){// log.info(employee.toString());//设置初始密码employee.setPassword(DigestUtils.md5DigestAsHex(123456.getBytes(StandardCharsets.UTF_8)));employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());employee.setCreateUser((long)request.getSession().getAttribute(id));employee.setUpdateUser((long)request.getSession().getAttribute(id));employeeService.save(employee);return R.success(新增员工成功);
}处理数据库插入重复名字异常 全局异常处理器来处理异常
关键点在ControllerAdvice和ExceptionHandler一个用来拦截方法一个用来处理异常
ControllerAdvice捕获方法后有异常就处理 ControllerAdvice(annotations {RestController.class, Controller.class}) ResponseBody//java对象转为json格式的数据 Slf4j public class GlobalExceptionHandler { //用来捕获插入重复数据异常 ExceptionHandler(SQLIntegrityConstraintViolationException.class) public RString exceptionHandler (SQLIntegrityConstraintViolationException exception){ log.error(exception.getMessage()); return R.error(failed); } } //用来捕获插入重复数据异常 ExceptionHandler(SQLIntegrityConstraintViolationException.class) public RString exceptionHandler (SQLIntegrityConstraintViolationException exception){ if (exception.getMessage().contains(Duplicate entry)){ String[] split exception.getMessage().split( );//根据空格符分割数组 String msg split[2] 已存在; return R.error(msg); } return R.error(unknown error); } 三、员工信息分页查询 page对象内部 里面包含了查询构造器的使用 具体的细节在这个包下com.cc.controller.EmployeeController.page /*** 分页展示员工列表接口、查询某个员工* param page 查询第几页* param pageSize 每页一共几条数据* param name 查询名字name的数据* return 返回Page页*/GetMapping(/page)public ResultPage page(int page, int pageSize,String name){//分页构造器,Page(第几页, 查几条)Page pageInfo new Page(page, pageSize);//查询构造器LambdaQueryWrapperEmployee lambdaQueryWrapper new LambdaQueryWrapper();//过滤条件.like(什么条件下启用模糊查询模糊查询字段被模糊插叙的名称)lambdaQueryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);//添加排序lambdaQueryWrapper.orderByDesc(Employee::getCreateTime);//查询分页、自动更新employeeService.page(pageInfo, lambdaQueryWrapper);//返回查询结果 return Result.success(pageInfo);} 一、启用/禁用员工账号显示不同
一需求分析
管理员—— 普通员工—— 前端页面禁用启用按钮实现分析 二代码开发
后端流程梳理 /*** 根据id修改员工信息* param employee* return*/PutMappingpublic RString update(HttpServletRequest request,RequestBody Employee employee){log.info(employee.toString());Long empId (Long)request.getSession().getAttribute(employee);employee.setUpdateTime(LocalDateTime.now());employee.setUpdateUser(empId);employeeService.updateById(employee);return R.success(员工信息修改成功);}
运行时发现错误 四代码修复
1.问题描述 2.具体修复步骤 1.自定义的全局转化器 2.在MVC配置文件中追加上面的自定义的全局转化器 重新启动后一切都可以了—— //JacksonObjectMapper
package com.itzq.reggie.common;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;/*** 对象映射器:基于jackson将Java对象转为json或者将json转为Java对象* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]*/
public class JacksonObjectMapper extends ObjectMapper {public static final String DEFAULT_DATE_FORMAT yyyy-MM-dd;public static final String DEFAULT_DATE_TIME_FORMAT yyyy-MM-dd HH:mm:ss;public static final String DEFAULT_TIME_FORMAT HH:mm:ss;public JacksonObjectMapper() {super();//收到未知属性时不报异常this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);//反序列化时属性不存在的兼容处理this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);SimpleModule simpleModule new SimpleModule().addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))).addSerializer(BigInteger.class, ToStringSerializer.instance).addSerializer(Long.class, ToStringSerializer.instance).addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));//注册功能模块 例如可以添加自定义序列化器和反序列化器this.registerModule(simpleModule);}
}/*** 扩展mvc框架的消息转换器* param converters*/
Override
protected void extendMessageConverters(ListHttpMessageConverter? converters) {//创建消息转换器对象MappingJackson2HttpMessageConverter messageConverter new MappingJackson2HttpMessageConverter();//设置对象转化器底层使用jackson将java对象转为jsonmessageConverter.setObjectMapper(new JacksonObjectMapper());//将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0表示设置在第一个位置避免被其它转换器接收从而达不到想要的功能)converters.add(0,messageConverter);} 二、编辑员工信息
GetMapping(/{id})
public REmployee getById(PathVariable Long id){log.info(根据id查询员工信息。。。);Employee employee employeeService.getById(id);if (employee ! null){return R.success(employee);}return R.error(没有查询到该员工信息);
} 修改回显数据后点击保存会发送一个update的请求给后端前面我们已经写了这个update的controller所以只需要在前端跳转发请求就行这样就实现了方法的复用减少了代码两
Day03分类业务开发
一、公共字段自动填充
一需求分析
二代码开发 Slf4j Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { log.info(公共字段自动填充[insert]); log.info(metaObject.toString()); metaObject.setValue(createTime, LocalDateTime.now()); metaObject.setValue(updateTime, LocalDateTime.now()); metaObject.setValue(createUser, new Long(1)); metaObject.setValue(updateUser, new Long(1)); } Override public void updateFill(MetaObject metaObject) { log.info(公共字段自动填充[update]); 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)); } } 三功能测试 四功能完善
1目前问题——createTime和UpdateTime是固定值应该设置为动态值 2解决方案——threadlocal类解决 4什么是Threadlocal——Thread的局部变量 5代码流程 然后为了动态的获取员工的id,这里我们使用了threadLocal这个局部变量来获取和存储员工id;
创建一个工具类来设置和获取threadLocal中的员工id, 注意要先把数据设置进threadLocal中才能获取到
package com.itheima.reggie.common;/*** author LJM* create 2022/4/16* 基于ThreadLocal封装工具类用户保存和获取当前登录用户id*/
public class BaseContext {//用来存储用户idprivate static ThreadLocalLong threadLocal new ThreadLocal();/*** 设置值* param id*/public static void setCurrentId(Long id){threadLocal.set(id);}/*** 获取值* return*/public static Long getCurrentId(){return threadLocal.get();}
} 在前面我们写的LongCheckFilter这个过滤器中把这个地方的代码加上添加和保存id的代码
//4、判断登录状态如果已登录则直接放行if(request.getSession().getAttribute(employee) ! null){//log.info(用户已登录用户id为{},request.getSession().getAttribute(employee));//把用户id存储到本地的threadLocalLong emId (Long) request.getSession().getAttribute(employee);BaseContext.setCurrentId(emId);filterChain.doFilter(request,response);return;}
把处理器中的静态id改为动态获取
metaObject.setValue(createUser, BaseContext.getCurrentId());
metaObject.setValue(updateUser,BaseContext.getCurrentId()); 二、新增分类 数据模型
从资料去复制实体Category类到entity包
数据库中的表结构 创建mapper: package com.itheima.reggie.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itheima.reggie.entity.Category; import org.apache.ibatis.annotations.Mapper; Mapper public interface CategoryMapper extends BaseMapperCategory { } package com.itheima.reggie.service; import com.baomidou.mybatisplus.extension.service.IService; import com.itheima.reggie.entity.Category; /** * author LJM * create 2022/4/16 */ public interface CategoryService extends IServiceCategory { } package com.itheima.reggie.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itheima.reggie.entity.Category; import com.itheima.reggie.mapper.CategoryMapper; import com.itheima.reggie.service.CategoryService; import org.springframework.stereotype.Service; /** * author LJM * create 2022/4/16 */ Service public class CategoryServiceImpl extends ServiceImplCategoryMapper, Category implements CategoryService { } 编写controller /** * 新增套餐分类 * param category * return */ PostMapping public RString save(RequestBody Category category){ log.info({category} ,category); categoryService.save(category); return R.success(新增分类成功); } 三、分类信息分页查询 /*** 分页查询* param page* param pageSize* return*/GetMapping(/page)public RPage page(int page,int pageSize){//创建一个分页构造器PageCategory categoryPage new Page(page,pageSize);//创建一个条件构造器 用来排序用的 注意这个条件构造器一定要使用泛型否则使用条件查询这个方法的时候会报错LambdaQueryWrapperCategory queryWrapper new LambdaQueryWrapper();//添加排序条件 根据sort字段进行排序queryWrapper.orderByAsc(Category::getSort);categoryService.page(categoryPage,queryWrapper);return R.success(categoryPage);} 四、删除分类
代码实现 注意这里的删除功能是不完整的因为可能需要删除的数据是与其他表关联的所以删除之前要先判断该条数据是否与其他表中的数据关联 /*** 根据id来删除分类的数据* param id* return*/DeleteMapping()public RString delete(RequestParam(ids) Long ids){ //注意这里前端传过来的数据是idscategoryService.removeById(ids);return R.success(分类信息删除成功);} 创建对应的mapper:
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;/*** author LJM* create 2022/4/16*/
Mapper
public interface DishMapper extends BaseMapperDish {}
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;
/*** author LJM* create 2022/4/16*/
Mapper
public interface SetmealMapper extends BaseMapperSetmeal {
}
创建service
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Dish;/*** author LJM* create 2022/4/16*/
public interface DishService extends IServiceDish {
}
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Setmeal;public interface SetmealService extends IServiceSetmeal {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.mapper.DishMapper;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** author LJM* create 2022/4/16*/
Service
Slf4j
public class DishServiceImpl extends ServiceImplDishMapper, Dish implements DishService {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.SetmealMapper;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** author LJM* create 2022/4/16*/
Service
Slf4j
public class SetmealServiceImpl extends ServiceImplSetmealMapper, Setmeal implements SetmealService {
}
添加自定义的service方法(就是我们需要的业务mybatis没有提供所以就需要自己另外在service创建新的方法并且在相关的业务中实现) //在CategoryService中定义自己需要的方法直接写就行 void remove(Long id); 在CategoryService实现类中重写该方法 自定义异常类因为这里需要抛异常了 package com.itheima.reggie.common;/*** 自定义业务异常类*/
public class CustomException extends RuntimeException {public CustomException(String message){super(message);}
}
//然后在外面前面写的GlobalExceptionHandler全局异常捕获器中添加该异常这样就可以把相关的异常信息显示给前端操作的人员看见/*** 处理自定义的异常为了让前端展示我们的异常信息这里需要把异常进行全局捕获然后返回给前端* param exception* return*/ExceptionHandler(CustomException.class)public RString exceptionHandle(CustomException exception){log.error(exception.getMessage()); //报错记得打日志//这里拿到的message是业务类抛出的异常信息我们把它显示到前端return R.error(exception.getMessage());}
/*** 根据id删除 分类删除之前需要进行判断是否有关联数据* param id*/Overridepublic void remove(Long id) {LambdaQueryWrapperDish dishLambdaQueryWrapper new LambdaQueryWrapper();//添加查询条件dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);//注意:这里使用count方法的时候一定要传入条件查询的对象否则计数会出现问题计算出来的是全部的数据的条数int count dishService.count(dishLambdaQueryWrapper);//查询当前分类是否关联了菜品如果已经管理直接抛出一个业务异常if (count 0){//已经关联了菜品抛出一个业务异常throw new CustomException(当前分类项关联了菜品,不能删除);}//查询当前分类是否关联了套餐如果已经管理直接抛出一个业务异常LambdaQueryWrapperSetmeal setmealLambdaQueryWrapper new LambdaQueryWrapper();setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);//注意:这里使用count方法的时候一定要传入条件查询的对象否则计数会出现问题计算出来的是全部的数据的条数int setmealCount setmealService.count(setmealLambdaQueryWrapper);if (setmealCount 0){//已经关联了套餐抛出一个业务异常throw new CustomException(当前分类项关联了套餐,不能删除);}//正常删除super.removeById(id);}
然后在controller调用刚刚实现的方法就行把之前的remove方法给删除就行重新调用我们自己实现的方法 /*** 根据id来删除分类的数据* param id* return*/DeleteMappingpublic RString delete(RequestParam(ids) Long id){ //注意这里前端传过来的数据是idscategoryService.remove(id);return R.success(分类信息删除成功);} 五、修改分类 这里的编辑的数据回显前端已经帮我们做好了所以我们就不需要去数据库查询了这样可以减少对数据库的操作 /*** 根据id修改分类* param category* return*/PutMappingpublic RString update(RequestBody Category category){categoryService.updateById(category);return R.success(修改分类信息成功);}
记得在对应的实体类加上公共字段的值设置前面我们配置了这个所以这里只需要加注解就行 //创建时间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; Day04菜品管理业务开发
管理端—— 客户端——
一、文件上传下载 参数名有要求的 接收的文件类型一定是 方法名(MultipartFile 前端上传的文件名称) 所以后端的接收名字也得改为file
上传逻辑实现
具体的存储路径写在配置文件里了 用Value注入到业务里就可以了
后端具体代码的实现
yml配置文件配置上传图片的存储位置 reggie: path: E:\reggie\ package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;/*** author LJM* create 2022/4/16* 文件上传和下载*/
RestController
RequestMapping(/common)
public class CommonController {Value(${reggie.path})private String basePath;/*** 文件的上传* param file* return*/PostMapping(/upload)public RString upload(MultipartFile file){//这个file是一个临时文件需要转存到指定位置否则本次请求完成后临时文件会删除//拿到文件的原始名String originalFilename file.getOriginalFilename();//拿到文件的后缀名 比如 .png .jpgString suffix originalFilename.substring(originalFilename.lastIndexOf(.));//使用uuid生成的作为文件名的一部分这样可以防止文件名相同造成的文件覆盖String fileName UUID.randomUUID().toString() suffix;//创建一个目录对象看传文件的时候接收文件的目录存不存在File dir new File(basePath);if (!dir.exists()){//文件目录不存在直接创建一个目录dir.mkdirs();}try {//把前端传过来的文件进行转存file.transferTo(new File(basePath fileName));}catch (IOException e){e.printStackTrace();}return R.success(fileName);}GetMapping(/download)public void download(String name, HttpServletResponse response){try {//输入流通过输入流读取文件内容 这里的name是前台用户需要下载的文件的文件名//new File(basePath name) 是为了从存储图片的地方获取用户需要的图片对象FileInputStream fileInputStream new FileInputStream(new File(basePath name));//输出流通过输出流将文件写回浏览器ServletOutputStream outputStream response.getOutputStream();//设置写回去的文件类型response.setContentType(image/jpeg);//定义缓存区准备读写文件int len 0 ;byte[] buff new byte[1024];while ((len fileInputStream.read(buff)) ! -1){outputStream.write(buff,0,len);outputStream.flush();}//关流outputStream.close();fileInputStream.close();}catch (Exception e){e.printStackTrace();}}
}
二、新增菜品 需求分析 数据模型 代码开发 创建相关的mapper和service层
package com.itheima.reggie.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;Mapper
public interface DishFlavorMapper extends BaseMapperDishFlavor {
}
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.DishFlavor;public interface DishFlavorService extends IServiceDishFlavor {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.DishFlavor;
import com.itheima.reggie.mapper.DishFlavorMapper;
import com.itheima.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;/*** author LJM* create 2022/4/16*/
Service
public class DishFlavorServiceImpl extends ServiceImplDishFlavorMapper, DishFlavor implements DishFlavorService {
}
编写controller 在CategoryController书写查询代码不过这里的返回值和参数接收值可能和自己想的有点不一样。。。这个的返回值和参数值 值得多思考一下 这里之所以返回list集合是因为这个要展示的数据是引用类型的数据集集合可以存放任意类型的数据 /*** 根据条件查询分类数据* param category* return*/GetMapping(/list)//这个接口接收到参数其实就是一个前端传过来的type,这里之所以使用Category这个类来接受前端的数据是为了以后方便//因为这个Category类里面包含了type这个数据,返回的数据多了你自己用啥取啥就行private RListCategory list(Category category){//条件构造器LambdaQueryWrapperCategory queryWrapper new LambdaQueryWrapper();//添加查询条件queryWrapper.eq(category.getType() ! null,Category::getType,category.getType());//添加排序条件 使用两个排序条件,如果sort相同的情况下就使用更新时间进行排序queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);ListCategory list categoryService.list(queryWrapper);return R.success(list);}
测试的返回数据: 接收页面提交的数据涉及两张表 点击保存按钮的时候把前端的json数据提交到后台后台接收数据对数据进行处理要与两张表打交道一个是dish一个是dish_flavor表
先用前端页面向后端发一次请求看看前端具体的请求是什么我们好写controller然后再看前端提交携带的参数是什么我们好选择用什么类型的数据来接收
看下图这是前端传过来的具体参数我们需要什么参数类型来接收这些数据就大概知道了因为这里传过来的参数比较复杂所以这里有两种方式进行封装第一创建与这些数据对应的实体类dto 第二使用map来接收 这里我们选择使用第一种方式
package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;Data
public class DishDto extends Dish {private ListDishFlavor flavors new ArrayList();private String categoryName; //后面要用的private Integer copies; //后面要用的
} 后端代码
在DishService中新增一个方法 //新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish dish_flavor void saveWithFlavor(DishDto dishDto); Autowired
private DishFlavorService dishFlavorService;
/*** 新增菜品同时保存对应的口味数据* param dishDto*/
Override
Transactional //涉及到对多张表的数据进行操作,需要加事务需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {//保存菜品的基本信息到菜品表dish中this.save(dishDto);Long dishId dishDto.getId();//为了把dishId set进flavors表中//拿到菜品口味ListDishFlavor flavors dishDto.getFlavors();//这里对集合进行赋值 可以使用循环或者是stream流flavors flavors.stream().map((item) -{//拿到的这个item就是这个DishFlavor集合item.setDishId(dishId);return item; //记得把数据返回去}).collect(Collectors.toList()); //把返回的集合搜集起来,用来被接收//把菜品口味的数据到口味表 dish_flavor 注意dish_flavor只是封装了name value 并没有封装dishId(从前端传过来的数据发现的,然而数据库又需要这个数据)dishFlavorService.saveBatch(dishDto.getFlavors()); //这个方法是批量保存
} 在启动类开启事务 加上这个注解就行 EnableTransactionManagement
controller 层的代码
package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import com.itheima.reggie.dto.DishDto;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** author LJM* create 2022/4/16*/
RestController
RequestMapping(/dish)
Slf4j
public class DishController {Autowiredprivate DishService dishService;/*** 新增菜品* param dishDto* return*/PostMappingpublic RString save(RequestBody DishDto dishDto){ //前端提交的是json数据的话我们在后端就要使用这个注解来接收参数否则接收到的数据全是nulldishService.saveWithFlavor(dishDto);return R.success(新增菜品成功);}
} 三、菜品信息分页查询
需求分析 图片下载的请求前面已经写好了前端也写好了相关的请求所以第二步的图片下载和展示就不需要我们管了
代码编写
controller层的代码:不过这里是有bug的后面会改善
/*** 菜品信息分页查询* param page* param pageSize* param name* return*/
GetMapping(/page)
public RPage page(int page,int pageSize,String name){//构造一个分页构造器对象PageDish dishPage new Page(page,pageSize);//构造一个条件构造器LambdaQueryWrapperDish queryWrapper new LambdaQueryWrapper();//添加过滤条件 注意判断是否为空 使用对name的模糊查询queryWrapper.like(name ! null,Dish::getName,name);//添加排序条件 根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错但是前端展示的时候这个菜品分类这一数据就为空return R.success(dishPage);
} 功能完善引入了DishDto
package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;Data
public class DishDto extends Dish {private ListDishFlavor flavors new ArrayList();private String categoryName;private Integer copies; //后面用的
}
/*** 菜品信息分页查询* param page* param pageSize* param name* return*/GetMapping(/page)public RPage page(int page,int pageSize,String name){//构造一个分页构造器对象PageDish dishPage new Page(page,pageSize);PageDishDto dishDtoPage new Page(page,pageSize);//上面对dish泛型的数据已经赋值了这里对DishDto我们可以把之前的数据拷贝过来进行赋值//构造一个条件构造器LambdaQueryWrapperDish queryWrapper new LambdaQueryWrapper();//添加过滤条件 注意判断是否为空 使用对name的模糊查询queryWrapper.like(name ! null,Dish::getName,name);//添加排序条件 根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//获取到dish的所有数据 records属性是分页插件中表示分页中所有的数据的一个集合ListDish records dishPage.getRecords();ListDishDto list records.stream().map((item) -{//对实体类DishDto进行categoryName的设值DishDto dishDto new DishDto();//这里的item相当于Dish 对dishDto进行除categoryName属性的拷贝BeanUtils.copyProperties(item,dishDto);//获取分类的idLong categoryId item.getCategoryId();//通过分类id获取分类对象Category category categoryService.getById(categoryId);if ( category ! null){//设置实体类DishDto的categoryName属性值String categoryName category.getName();dishDto.setCategoryName(categoryName);}return dishDto;}).collect(Collectors.toList());//对象拷贝 使用框架自带的工具类第三个参数是不拷贝到属性BeanUtils.copyProperties(dishPage,dishDtoPage,records);dishDtoPage.setRecords(list);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错但是前端展示的时候这个菜品分类这一数据就为空//所以进行了上面的一系列操作return R.success(dishDtoPage);}
records的值 protected ListT records; 功能测试 四. 修改菜品(回显和保存修改都是两张表) 菜品信息的回显
在service添加自己要实现的方法 //根据id来查询菜品信息和对应的口味信息 DishDto getByIdWithFlavor(Long id); 方法的 实现 Autowired private DishFlavorService dishFlavorService; /** * 根据id来查询菜品信息和对应的口味信息 * param id * return */ Override public DishDto getByIdWithFlavor(Long id) { //查询菜品的基本信息 从dish表查询 Dish dish this.getById(id); //查询当前菜品对应的口味信息,从dish_flavor查询 条件查询 LambdaQueryWrapperDishFlavor queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(DishFlavor::getDishId,dish.getId()); ListDishFlavor flavors dishFlavorService.list(queryWrapper); //然后把查询出来的flavors数据set进行 DishDto对象 DishDto dishDto new DishDto(); //把dish表中的基本信息copy到dishDto对象因为才创建的dishDto里面的属性全是空 BeanUtils.copyProperties(dish,dishDto); dishDto.setFlavors(flavors); return dishDto; } controller 层的编写 /** * 根据id来查询菜品信息和对应的口味信息 * param id * return */ GetMapping(/{id}) public RDishDto get(PathVariable Long id){ //这里返回什么数据是要看前端需要什么数据,不能直接想当然的就返回Dish对象 DishDto dishDto dishService.getByIdWithFlavor(id); return R.success(dishDto); } 保存修改(重点)
保存修改设计两张表的数据的修改
DishService中添加自己实现的方法 //更新菜品信息同时还更新对应的口味信息 void updateWithFlavor(DishDto dishDto); 相关的实现 Override Transactional public void updateWithFlavor(DishDto dishDto) { //更新dish表的基本信息 因为这里的dishDto是dish的子类 this.updateById(dishDto); //更新口味信息---》先清理再重新插入口味信息 //清理当前菜品对应口味数据---dish_flavor表的delete操作 LambdaQueryWrapperDishFlavor queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(DishFlavor::getDishId,dishDto.getId()); dishFlavorService.remove(queryWrapper); //添加当前提交过来的口味数据---dish_flavor表的insert操作 ListDishFlavor flavors dishDto.getFlavors(); //下面这段流的代码我注释,然后测试发现一次是报dishId没有默认值(先测)两次可以得到结果(后测重新编译过清除缓存过),相隔半个小时 //因为这里拿到的flavorsz只有name和value(这是在设计数据封装的问题),不过debug测试的时候发现有时候可以拿到全部数据,有时候又不可以... 所以还是加上吧。。。。。 flavors flavors.stream().map((item) - { item.setDishId(dishDto.getId()); return item; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); } Day05套餐管理
1. 新增套餐 分析⭐️ 和上一个开发类似 前端提交过来的信息包含套餐基本信息和套餐与菜品关联信息 因此需要设置一个setmealDtoDto中包含套餐基本信息和套餐与菜品关联信息 后端在setmealController中接收这个Dto然后新增业务方法去处理Dto 业务方法 ①将dto基本信息传入到套餐基本信息表中 ②将套餐id和这个对象中的list集合中的数据添加到套餐菜品表中 ③涉及操作两张表需要加入transactional注解要么同时成功要么同时失败 需求分析: 数据模型 功能实现一回显添加菜品 根据分类categoryId来去相应dish表中查询信息进而回显信息 /*** 根据categoryId,回显对应分类下的菜品信息* param dish* return*/
GetMapping(/list)
public RListDish list(Dish dish){LambdaQueryWrapperDish queryWrapper new LambdaQueryWrapper();//两个eq信息queryWrapper.eq(dish ! null,Dish::getCategoryId,dish.getCategoryId());queryWrapper.eq(Dish::getStatus,1);//添加排序条件queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);ListDish list dishService.list(queryWrapper);return R.success(list);
}功能实现二 实现添加菜品功能 这里要注意setMealId在前端传过来的数据没有需要将前端基本信息添加到SetMeal表中才能得到相应的Id然后为套餐菜品对象赋值上值 Service
public class SetMealServiceImpl extends ServiceImplSetMealMapper, Setmeal implements SetMealService {Autowiredprivate SetMealDishService setMealDishService;/*** 新增菜品套餐* param setmealDto*/Overridepublic void saveWithDish(SetmealDto setmealDto) {//1.调用setMeal本有的功能顺便得到套餐idthis.save(setmealDto);//2.将从数据库得到的套餐id封装回setmealDishes对象中ListSetmealDish setmealDishes setmealDto.getSetmealDishes();setmealDishes.stream().map((item) - {item.setSetmealId(setmealDto.getId());return item;}).collect(Collectors.toList());//3.setMealDishService.saveBatch(setmealDishes);}
}2. 套餐分页查询 功能与day04的菜品信息分类查询相似在套餐管理界面套餐分类字段显示的是categoryId对应的中文但在数据库里查询到的是categoryId因此需要利用categoryId查询到categoryName并赋值给数据传输对象SetmealDto /*** 套餐分页查询* param page* param pageSize* param name* return*/
GetMapping(/page)
public RPage list(int page, int pageSize, String name){//分页构造器对象PageSetmeal pageInfo new Page(page, pageSize);PageSetmealDto dtoPage new Page();//构造查询条件对象LambdaQueryWrapperSetmeal queryWrapper new LambdaQueryWrapper();queryWrapper.eq(name ! null, Setmeal::getName, name);//操作数据库setMealService.page(pageInfo,queryWrapper);//对象拷贝BeanUtils.copyProperties(pageInfo,dtoPage,records);ListSetmeal records pageInfo.getRecords();ListSetmealDto list records.stream().map((item) - {SetmealDto setmealDto new SetmealDto();BeanUtils.copyProperties(item, setmealDto);//获取categoryIdLong categoryId item.getCategoryId();Category category categoryService.getById(categoryId);if (category ! null) {String categoryName category.getName();setmealDto.setCategoryName(categoryName);}return setmealDto;}).collect(Collectors.toList());dtoPage.setRecords(list);return R.success(dtoPage);
}3.删除套餐信息 需求分析 提供一个方法处理删除一个和删除多个请求 代码开发
注意点
①接受前端ids数据传过来的数据本身是数组形式所以加不加注解无所谓但是List是列表所以要加注解RequestParam
②根据id删除套餐不仅删除套餐也删除关联套餐表中的信息
业务逻辑SetMealServiceImpl 1.查询套餐状态确定是否可用删除 2.如果不能删除抛出一个业务异常提示在售卖中 3.如果可以删除先删除套餐表中的数据 4.删除关系表中的数据 /*** 根据ids删除套餐信息* param ids*/
Override
Transactional
public void removeWithDish(ListLong ids) {// 1.查询套餐状态确定是否可用删除//SQL语句select count(*) from setMeal where id in ids and status 1;LambdaQueryWrapperSetmeal queryWrapper new LambdaQueryWrapper();queryWrapper.in(Setmeal::getId,ids);queryWrapper.eq(Setmeal::getStatus,1);int count this.count(queryWrapper);// 2.如果不能删除抛出一个业务异常提示**在售卖中**if(count 0){throw new CustomException(商品还在销售不能删除);}// 3.如果可以删除先删除套餐表中的数据this.removeByIds(ids);// 4.删除关系表中的数据//根据套餐id去关系表中去查数据然后匹配删除//delete from setMealDish where setmealId in idsLambdaQueryWrapperSetmealDish lambdaQueryWrapper new LambdaQueryWrapper();lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);setMealDishService.remove(lambdaQueryWrapper);}起售停售操作(SetMealServiceImpl) 这里采用遍历操作实现批量停售起售不太好应该用mp的具体更新方法操作等学了mp之后再来补吧希望还记得 /*** 更改售卖状态* param ids* param status 1表示启售0表示停售*/
Override
public void changeStatus(ListLong ids, int status) {//改变售卖状态for (int i 0; i ids.size(); i) {Long id ids.get(i);//根据id得到每个dish菜品。Setmeal setmeal this.getById(id);setmeal.setStatus(status);this.updateById(setmeal);}}前台开发手机端
账户登陆
短信发送
阿里云短信业务教程
代码实现
官方文档地址 导入Maven dependency groupIdcom.aliyun/groupId artifactIdaliyun-java-sdk-core/artifactId version4.5.16/version /dependency dependency groupIdcom.aliyun/groupId artifactIdaliyun-java-sdk-dysmsapi/artifactId version1.1.0/version /dependency 然后调用api: package com.itheima.reggie.utils; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile; /** * 短信发送工具类 */ public class SMSUtils { /** * 发送短信 * param signName 签名 * param templateCode 模板 * param phoneNumbers 手机号 * param param 参数 */ public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){ DefaultProfile profile DefaultProfile.getProfile(cn-hangzhou, , ); IAcsClient client new DefaultAcsClient(profile); SendSmsRequest request new SendSmsRequest(); request.setSysRegionId(cn-hangzhou); request.setPhoneNumbers(phoneNumbers); request.setSignName(signName); request.setTemplateCode(templateCode); request.setTemplateParam({\code\:\param\}); try { SendSmsResponse response client.getAcsResponse(request); System.out.println(短信发送成功); }catch (ClientException e) { e.printStackTrace(); } } } 短信验证码登陆
需求分析
数据模型 前后端交互过程 代码开发 导入user实体类
创建userMapper:
导入工具类 发送验证码给的资料有点残缺这里修改了
注意这个资料有点残缺补全
在longin.html中找到这个获取验证码的方法把一行注释然后添加一行代码一行
getCode(){this.form.code const regex /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;if (regex.test(this.form.phone)) {this.msgFlag false//this.form.code (Math.random()*1000000).toFixed(0)sendMsgApi({phone:this.form.phone}) //添加的}else{this.msgFlag true}
},
在login.js中添加一个方法
function sendMsgApi(data){return $axios({url:/user/sendMsg,method:post,data})
} 在登陆拦截器LongCheckFilter中添加新的白名单
String[] urls new String[]{/employee/login,/employee/logout,/backend/**,/front/**,/common/**,/user/sendMsg, //移动端发送短信/user/login // 移动端登陆
};
并且在里面继续添加一个手机端登陆状态的放行判断
//4-2判断移动端登录状态如果已登录则直接放行if(request.getSession().getAttribute(user) ! null){//log.info(用户已登录用户id为{},request.getSession().getAttribute(user));//把用户id存储到本地的threadLocalLong userId (Long) request.getSession().getAttribute(user);BaseContext.setCurrentId(userId);filterChain.doFilter(request,response);return;} 编写controller
Autowired
private UserService userService;/*** 发送手机短信验证码* param user* return*/
PostMapping(/sendMsg)
public RString sendMsg(RequestBody User user, HttpSession session){//获取手机号String phone user.getPhone();if (StringUtils.isNotEmpty(phone)){//随机生成的4为验证码Integer integerCode ValidateCodeUtils.generateValidateCode(4);String code integerCode.toString();log.info(code{},code);//调用阿里云提供的短信服务api完成发送短信 这里个人用户申请不了阿里云短信服务的签名所以这里在后台输出了//SMSUtils.sendMessage(,,,);//把验证码存起来 这里使用session来存放验证码当然也可以存到redissession.setAttribute(phone,code);return R.success(手机验证码发送成功);}return R.error(手机验证码发送失败);
} 功能测试访问手机端输入手机号看能不能在后台打印验证码
使用验证码登陆(使用map接收数据) 注意测试的时候发现前端页面明明填了验证码发现验证码并没有被携带在前端的请求参数中所以后端也没有拿到验证码这个数据一看就是前端发请求的地方的参数携带少了修改一下参数就行 async btnLogin(){ if(this.form.phone this.form.code){ this.loading true //const res await loginApi({phone:this.form.phone}) 这里是资料给的代码 const res await loginApi(this.form) //这里是自己加的 .... } controller层代码 /** * 移动端用户登录 * param map * param session * return */ PostMapping(/login) public RUser login(RequestBody Map map, HttpSession session){ //log.info(map.toString()); //获取手机号 String phone map.get(phone).toString(); //获取验证码 String code map.get(code).toString(); //从Session中获取保存的验证码 Object codeInSession session.getAttribute(phone); //进行验证码的比对页面提交的验证码和Session中保存的验证码比对 if(codeInSession ! null codeInSession.equals(code)){ //如果能够比对成功说明登录成功 LambdaQueryWrapperUser 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); } //这一行容易漏。。保存用户登录状态 session.setAttribute(user,user.getId()); //在session中保存用户的登录状态,这样才过滤器的时候就不会被拦截了 return R.success(user); } return R.error(登录失败); } 功能测试验证码正确后跳转到手机端 地址管理
导入用户地址簿 地址表 这里直接导入现成的AddressBookController
package com.itheima.reggie.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.AddressBook;
import com.itheima.reggie.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;import java.util.List;/*** 地址簿管理*/
Slf4j
RestController
RequestMapping(/addressBook)
public class AddressBookController {Autowiredprivate AddressBookService addressBookService;/*** 新增*/PostMappingpublic RAddressBook save(RequestBody AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());log.info(addressBook:{}, addressBook);addressBookService.save(addressBook);return R.success(addressBook);}/*** 设置默认地址*/PutMapping(default)public RAddressBook setDefault(RequestBody AddressBook addressBook) {log.info(addressBook:{}, addressBook);LambdaUpdateWrapperAddressBook wrapper new LambdaUpdateWrapper();wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());wrapper.set(AddressBook::getIsDefault, 0);//SQL:update address_book set is_default 0 where user_id ?addressBookService.update(wrapper);addressBook.setIsDefault(1);//SQL:update address_book set is_default 1 where id ?addressBookService.updateById(addressBook);return R.success(addressBook);}/*** 根据id查询地址*/GetMapping(/{id})public R get(PathVariable Long id) {AddressBook addressBook addressBookService.getById(id);if (addressBook ! null) {return R.success(addressBook);} else {return R.error(没有找到该对象);}}/*** 查询默认地址*/GetMapping(default)public RAddressBook getDefault() {LambdaQueryWrapperAddressBook queryWrapper new LambdaQueryWrapper();queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());queryWrapper.eq(AddressBook::getIsDefault, 1);//SQL:select * from address_book where user_id ? and is_default 1AddressBook addressBook addressBookService.getOne(queryWrapper);if (null addressBook) {return R.error(没有找到该对象);} else {return R.success(addressBook);}}/*** 查询指定用户的全部地址*/GetMapping(/list)public RListAddressBook list(AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());log.info(addressBook:{}, addressBook);//条件构造器LambdaQueryWrapperAddressBook queryWrapper new LambdaQueryWrapper();queryWrapper.eq(null ! addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());queryWrapper.orderByDesc(AddressBook::getUpdateTime);//SQL:select * from address_book where user_id ? order by update_time descreturn R.success(addressBookService.list(queryWrapper));}
}
手机端展示
菜品展示 前端重点代码 //获取所有的菜品分类
function categoryListApi() {return $axios({url: /category/list,method: get,})}//获取购物车内商品的集合
function cartListApi(data) {return $axios({url: /shoppingCart/list,method: get,params:{...data}})
} 我们发现前端的展示页面中请求到了category的数据服务器也响应了数据给前端页面但是我们也看见了手机端并没有展示相关的套餐数据这是因为在加载页面的时候前端一共发了两个list请求具体的请求看上面的前端代码请求购物车信息的请求返回404所以导致category的数据也没有被展示
这里我们先在其他地方静态的接收购物车的请求这样就可以先显示category的数据先用假数据测试一下修改购物车的请求地址
//获取购物车内商品的集合
function cartListApi(data) {return $axios({//url: /shoppingCart/list,url:/front/cartData.json,method: get,params:{...data}})
} // 假数据文件 {code:1,msg:null,data:[],map:{}} 功能测试 但是我们也发现了bug就是展示的菜品没有对应的口味信息比如甜度辣度。。。我们之前是添加过相关的口味数据的
这是因为我们在请求获取菜品信息的时候我们返回的数据是RListDish Dish这个类是没有相关的口味信息的所以即便前端请求了这个口味的信息但是后端是没有给它返回的所以体现在前端就是口味信息没有展示出来所以我们需要对DishController中的list接口进行修改bug修复
//方法改造
GetMapping(/list)
public RListDishDto list(Dish dish){ //会自动映射的//这里可以传categoryId,但是为了代码通用性更强,这里直接使用dish类来接受因为dish里面是有categoryId的,以后传dish的其他属性这里也可以使用//构造查询条件LambdaQueryWrapperDish queryWrapper new LambdaQueryWrapper();queryWrapper.eq(dish.getCategoryId() ! null ,Dish::getCategoryId,dish.getCategoryId());//添加条件查询状态为1起售状态的菜品queryWrapper.eq(Dish::getStatus,1);//添加排序条件queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);ListDish list dishService.list(queryWrapper);//进行集合的泛型转化ListDishDto dishDtoList list.stream().map((item) -{DishDto dishDto new DishDto();//为一个新的对象赋值一定要考虑你为它赋过几个值否则你自己都不知道就返回了null的数据//为dishDto对象的基本属性拷贝BeanUtils.copyProperties(item,dishDto);Long categoryId item.getCategoryId();Category category categoryService.getById(categoryId);if (category ! null){String categoryName category.getName();dishDto.setCategoryName(categoryName);}//为dishdto赋值flavors属性//当前菜品的idLong dishId item.getId();//创建条件查询对象LambdaQueryWrapperDishFlavor lambdaQueryWrapper new LambdaQueryWrapper();lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);//select * from dish_flavor where dish_id ?//这里之所以使用list来条件查询那是因为同一个dish_id 可以查出不同的口味出来,就是查询的结果不止一个ListDishFlavor dishFlavorList dishFlavorService.list(lambdaQueryWrapper);dishDto.setFlavors(dishFlavorList);return dishDto;}).collect(Collectors.toList());return R.success(dishDtoList);
} 套餐展示
套餐展示的前端请求地址和携带的参数 在SetmealController中添加相应的方法来接收前端的请求 /*** 根据条件查询套餐数据* param setmeal* return*/GetMapping(/list)public RListSetmeal list(Setmeal setmeal){LambdaQueryWrapperSetmeal queryWrapper new LambdaQueryWrapper();queryWrapper.eq(setmeal.getCategoryId()!null,Setmeal::getCategoryId,setmeal.getCategoryId());queryWrapper.eq(setmeal.getStatus()!null,Setmeal::getStatus,setmeal.getStatus());queryWrapper.orderByDesc(Setmeal::getUpdateTime);ListSetmeal list setmealService.list(queryWrapper);return R.success(list);}
功能测试 购物车
添加菜品和套餐进购物车
数据模型 购物车对应的数据表为shopping_cart具体结构如下从资料中导入相关的实体类就行 代码开发
加入购物车功能
加入购物车的前端请求和携带的参数 开发准备工作
导入实体类ShoppingCart
创建对应的mapper等
controller
Autowired
private ShoppingCartService shoppingCartService;/*** 添加购物车* param shoppingCart* return*/
PostMapping(/add)
public RShoppingCart add(RequestBody ShoppingCart shoppingCart){//先设置用户id,指定当前是哪个用户的购物车数据 因为前端没有传这个id给我们,但是这个id又非常重要数据库这个字段不能为null,// 所以要想办法获取到,我们在用户登录的时候就已经保存了用户的idLong currentId BaseContext.getCurrentId();shoppingCart.setUserId(currentId);Long dishId shoppingCart.getDishId();LambdaQueryWrapperShoppingCart queryWrapper new LambdaQueryWrapper();queryWrapper.eq(ShoppingCart::getUserId,currentId);if (dishId ! null){//添加到购物车的是菜品queryWrapper.eq(ShoppingCart::getDishId,dishId);}else {//添加到购物车的是套餐queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());}//查询当前菜品是否或者是套餐是否在购物车中//SQL:select * from shopping_cart where user_id ? and dish_id/setmeal_id ?ShoppingCart cartServiceOne shoppingCartService.getOne(queryWrapper);if (cartServiceOne ! null) {//如果已经存在,就在原来的数量基础上加一Integer number cartServiceOne.getNumber();cartServiceOne.setNumber(number1);shoppingCartService.updateById(cartServiceOne);}else {//如果不存在,则添加到购物车数量默认是1shoppingCart.setNumber(1);shoppingCartService.save(shoppingCart);cartServiceOne shoppingCart;}return R.success(cartServiceOne);
} 查看购物车
把相关的前端请求地址给改掉, 不再请求假数据至于存放假数据的json文件是否删除看你自己删不删都没什么影响
//获取购物车内商品的集合
function cartListApi(data) {return $axios({url: /shoppingCart/list,//url:/front/cartData.json,method: get,params:{...data}})
}
注意一定要有用户的概念不同用户看到的购物车是不一样的
controller
/*** 查看购物车* return* 前端没有传数据给我们这里就不用接收了*/
GetMapping
public RListShoppingCart list(){LambdaQueryWrapperShoppingCart queryWrapper new LambdaQueryWrapper();queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());queryWrapper.orderByAsc(ShoppingCart::getCreateTime);ListShoppingCart list shoppingCartService.list(queryWrapper);return R.success(list);
}
清空购物车
/*** 清空购物车* return*/
DeleteMapping(/clean)
public RString clean(){//sql:delete from shopping_cart where userId LambdaQueryWrapperShoppingCart queryWrapper new LambdaQueryWrapper();queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());shoppingCartService.remove(queryWrapper);return R.success(清空购物车成功);}
减少购物车点菜品或者套餐
前端请求 http://localhost:8080/shoppingCart/sub
请求方式post
携带参数可能是dish_id 也可能是 setmealId所以我们需要实体类shoppingCart来接收; 遇到的bug: 就是购物车里面的菜品和套餐的数量可能会减少至负数所以这里我们要在入库前做一次判断把数据库的该字段设置为无符号字段所以当num数小于0的时候就会报错500接口异常但是左下角的小购物车还是会显示菜品为0所以在后端的代码也要进行判断操作 在ShoppingCartController中添加下面的接口方法来接收请求这个是修改了一次的代码思路用的是评论区一个老哥提供的思路 /*** 客户端的套餐或者是菜品数量减少设置* 没必要设置返回值* param shoppingCart*/PostMapping(/sub)Transactionalpublic RShoppingCart sub(RequestBody ShoppingCart shoppingCart){Long dishId shoppingCart.getDishId();LambdaQueryWrapperShoppingCart queryWrapper new LambdaQueryWrapper();//代表数量减少的是菜品数量if (dishId ! null){//通过dishId查出购物车对象queryWrapper.eq(ShoppingCart::getDishId,dishId);//这里必须要加两个条件否则会出现用户互相修改对方与自己购物车中相同套餐或者是菜品的数量queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());ShoppingCart cart1 shoppingCartService.getOne(queryWrapper);cart1.setNumber(cart1.getNumber()-1);Integer LatestNumber cart1.getNumber();if (LatestNumber 0){//对数据进行更新操作shoppingCartService.updateById(cart1);}else if(LatestNumber 0){//如果购物车的菜品数量减为0那么就把菜品从购物车删除shoppingCartService.removeById(cart1.getId());}else if (LatestNumber 0){return R.error(操作异常);}return R.success(cart1);}Long setmealId shoppingCart.getSetmealId();if (setmealId ! null){//代表是套餐数量减少queryWrapper.eq(ShoppingCart::getSetmealId,setmealId).eq(ShoppingCart::getUserId,BaseContext.getCurrentId());ShoppingCart cart2 shoppingCartService.getOne(queryWrapper);cart2.setNumber(cart2.getNumber()-1);Integer LatestNumber cart2.getNumber();if (LatestNumber 0){//对数据进行更新操作shoppingCartService.updateById(cart2);}else if(LatestNumber 0){//如果购物车的套餐数量减为0那么就把套餐从购物车删除shoppingCartService.removeById(cart2.getId());}else if (LatestNumber 0){return R.error(操作异常);}return R.success(cart2);}//如果两个大if判断都进不去return R.error(操作异常);} 用户订单
用户下单功能
需求分析
注意这里只是把用户的支付订单保存到数据库因为真正的支付功能是需要去申请支付资质的个人用户很难申请到
数据模型用户下单业务对应的数据表为order表和order_detail表
order:订单表 order_detail订单明细表 代码开发
前端和服务器的交互过程
第一次交互 然后点击去支付然后前端就会发生http://localhost:8080/order/submit这个请求并且携带三个参数一个是地址一个是字符方式一个是客户的备注 开发准备工作
导入实体类orderorder_details
创建mapper等
controller层开发
前端请求的地址和携带的参数前面已经分析过了;
/*** 用户下单* param orders* return*/
PostMapping(/submit)
public RString submit(RequestBody Orders orders){orderService.submit(orders);return R.success(下单成功);
} service添加submit方法 /** * 用户下单 * param orders */ public void submit(Orders orders); 方法的实现
Autowired
private ShoppingCartService shoppingCartService;Autowired
private UserService userService;Autowired
private AddressBookService addressBookService;Autowired
OrderDetailService orderDetailService;/*** 用户下单* param orders*/
Override
Transactional
public void submit(Orders orders) {//前端请求携带的参数是没有用户id的,所以要获取用户idLong userId BaseContext.getCurrentId();//创建查询条件LambdaQueryWrapperShoppingCart queryWrapper new LambdaQueryWrapper();queryWrapper.eq(ShoppingCart::getUserId,userId);//查询当前用户的购物车数据ListShoppingCart shoppingCarts shoppingCartService.list(queryWrapper);if (shoppingCarts null || shoppingCarts.size()0){throw new CustomException(购物车为空,不能下单);}//查询用户数据User user userService.getById(userId);//查询用户地址Long addressBookId orders.getAddressBookId();AddressBook addressBook addressBookService.getById(addressBookId);if (addressBooknull){throw new CustomException(地址信息有误,不能下单);}Long orderId IdWorker.getId();//使用工具生成订单号orders.setId(orderId);//进行购物车的金额数据计算 顺便把订单明细给计算出来AtomicInteger amount new AtomicInteger(0);//使用原子类来保存计算的金额结果//这个item是集合中的每一个shoppingCarts对象,是在变化的ListOrderDetail orderDetails shoppingCarts.stream().map((item)-{//每对item进行一次遍历就产生一个新的orderDetail对象,然后对orderDetail进行设置,然后返回被收集,被封装成一个集合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());//单份的金额//addAndGet进行累加 item.getAmount()单份的金额 multiply乘 item.getNumber()份数amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());return orderDetail;}).collect(Collectors.toList());//向订单插入数据,一条数据 因为前端传过来的数据太少了,所以我们需要对相关的属性进行填值orders.setOrderTime(LocalDateTime.now());orders.setCheckoutTime(LocalDateTime.now());orders.setStatus(2);//Amount是指订单总的金额orders.setAmount(new BigDecimal(amount.get()));//总金额orders.setUserId(userId);orders.setNumber(String.valueOf(orderId));if (user.getName() ! null){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);//清空购物车数据 queryWrapper封装了userId我们直接使用这个条件来进行删除就行shoppingCartService.remove(queryWrapper);
} 用户查看自己订单
/*** 用户订单分页查询* param page* param pageSize* return*/
GetMapping(/userPage)
public RPage page(int page, int pageSize){//分页构造器对象PageOrders pageInfo new Page(page,pageSize);//构造条件查询对象LambdaQueryWrapperOrders queryWrapper new LambdaQueryWrapper();//添加排序条件根据更新时间降序排列queryWrapper.orderByDesc(Orders::getOrderTime);orderService.page(pageInfo,queryWrapper);return R.success(pageInfo);
}