瑞吉外卖整理

一、项目整体

  1. 软件开发流程:需求分析,设计,编码,测试,上线运维;
  2. 软件环境:开发环境(development),测试环境(testing),生产环境(production);
  3. 系统管理后台功能:员工登陆/退出、员工管理、分类管理、菜品管理、套餐管理、(订单明细);
  4. 用户端:登陆/退出、点餐-菜单、点餐-购物车、下单(支付功能未实现)、个人信息–地址簿;
  5. 数据库表:员工表employee、菜品和套餐分类表category、菜品表dish、套餐表setmeal、套餐菜品关系表setmeal_dish、菜品口味关系表dish_flavor、用户表(C端)user、地址簿表address_book、购物车表shopping_cart、订单表orders、订单明细表order_detail;
  6. 通用结果类R
    • 属性:编码code、信息msg、数据data(泛型);
    • 方法:成功方法success() – 设置datacode=1;错误方法error() – 设置msgcode = 0

二、员工相关功能

2.1 登陆退出功能

  1. 后台系统登陆功能:
    • 页面提交的密码password进行md5加密处理;
    • 登录成功,将员工id存入Session, 并返回登录成功结果:request.getSession().setAttribute("employee",emp.getId());
  2. 后台系统退出功能:清理Session中的用户id:request.getSession().removeAttribute("employee");
  3. 完善登录功能:增加登陆后才能访问相关页面:
    • 登录校验过滤器:自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑;
    • 定义不需要处理的请求路径 – 登陆、退出请求,用户端、后台管理端静态资源;如果不需要处理,则直接放行–Spring中提供的路径匹配器AntPathMatcher;
    • 使用session判断是否登陆:request.getSession().getAttribute("employee")
    • 需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解;

2.2 员工管理相关功能

  1. 新增员工:
    • 初始密码123456,需要进行md5加密处理:DigestUtils.md5DigestAsHex("123456".getBytes())
    • 新增员工已存在时的异常处理:
      • 在Controller方法中加入 try…catch 进行异常捕获–冗余,其他业务也需要;
      • 在项目中定义一个通用的全局异常处理器:
    • 全局异常处理器类public class GlobalExceptionHandler,加上注解 @ControllerAdvice(annotations = {RestController.class, Controller.class}),通过属性annotations指定拦截哪一类的Controller方法;
    • 在异常处理器的方法exceptionHandler(SQLIntegrityConstraintViolationException ex)上加上注解@ExceptionHandler(SQLIntegrityConstraintViolationException.class)来指定拦截的是哪一类型的异常 – 这里是SQL违背值唯一性约束异常;
  2. 分页查询:
    • 需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象:MybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor())
    • 分页构造器Page,分页查询的结果也存放在里面。
  3. 启用/禁用员工账号:
    • 在Controller中创建update方法:public R<String> update(HttpServletRequest request,@RequestBody Employee employee),根据id修改员工信息;
    • 问题:功能并未实现 – 请求过来的id在数据库表中并不存在;
    • long型数据json化中的问题: js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提交的id和数据库中的id不一致(该id来自于分页查询到的员工信息从服务器传递到前端时js对long型数据处理损失了精度);让分页查询返回给前端的json格式数据库中, long类型的属性, 不直接转换为数字类型, 转换为字符串类型。SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter
    • 引入JacksonObjectMapper;指定了在进行json数据序列化及反序列化时, LocalDateTime、LocalDate、LocalTime的处理方式, 以及BigInteger及Long类型数据,直接转换为字符串,不再让js对长整型数据进行处理;
    • 在WebMvcConfig中重写方法extendMessageConverters
  4. 编辑员工信息:先查询getById()再提交修改update()
  5. 公共字段<创建时间、更新时间、创建人、更新人>自动填充:MybatisPlus公共字段自动填充:
    • 在实体类的属性上加入@TableField注解,指定自动填充的策略 – INSERT、UPDATE、INSERT_UPDATE;
    • 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口 – 该接口中不能直接获得HttpSession对象进而获得登录用户的id来填充创建人、更新人信息;
      修改员工信息时的执行流程
    • 获取登录用户id:
      • 客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理 – 使用Thread的局部变量ThreadLocalThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值;
      • 基于ThreadLocal封装BaseContext工具类,并基于ThreadLocalset()get()方法封装静态方法:setCurrentId(Long id)getCurrentId()
      • 实现:在LoginCheckFilterdoFilter方法中从session中获取当前登录用户idrequest.getSession().getAttribute("employee"),并借助封装的工具类BaseContext.setCurrentId(empId)来设置当前线程的线程局部变量的值(用户id);
    • MyMetaObjectHandlerupdateFill(MetaObject metaObject)insertFill(MetaObject metaObject)方法中通过BaseContext.getCurrentId()来获得当前线程所对应的线程局部变量的值(用户id);

三、菜品相关功能

3.1 分类相关功能

  • 新增分类:
    • 两种类型的分类:菜品分类和套餐分类;分类的排序用来控制移动端分类列表的展示顺序;
    • 数据模型:
      • 分类表category表;
        分类表
      • 分类的名称 name 唯一、不能重复;类型 type 区分菜品和套餐;
    • 代码实现:
      • 实体类(使用lombok.Data);
      • Mapper接口CategoryMapper;
      • 业务层接口CategoryService;
      • 业务层实现类CategoryServiceImpl;
      • 控制层CategoryController;
  • 分类信息分页查询:
    • Mybatis Plus的分页构造器Page,条件构造器添加排序条件 – 新增分类时填写的排序值;
  • 删除分类:
    • 正常删除:categoryService.removeById(id);
    • 当分类关联了菜品或者套餐时,此分类不允许删除 – 根据当前分类的ID,查询该分类下是否存在菜品、套餐:
      • 类准备:
        • 菜品(Dish)及套餐(Setmeal)实体类;
        • Mapper接口DishMapper和SetmealMapper;
        • Service接口DishService和SetmealService;
        • Service实现类DishServiceImpl和SetmealServiceImpl;
      • 创建自定义异常public class CustomException extends RuntimeException,该异常构造方法中直接往上抛;
      • 在CategoryService中扩展remove方法,在CategoryServiceImpl中实现remove方法:删除前根据分类 id 分别查询菜品数据、套餐数据 – 如果有关联菜品、套餐,抛出自定义异常CustomException,并封装异常信息;
      • 在全局异常处理器GlobalExceptionHandler中处理自定义异常 – 增加方法捕获自定义的异常CustomExceptionexceptionHandler(CustomException ex),添加注解@ExceptionHandler(CustomException.class)
      • 如果没有关联,走正常删除super.removeById(id)
    • 改造CategoryController中的删除的delete方法为调用categoryService中自定义的remove方法,不再用removeById
  • 修改分类:
    • 数据回显前端实现:点击修改按钮时携带列表中的行数据;
    • 修改:CategoryController中定义update方法,调用categoryService.updateById(category)

3.2 菜品相关功能

  • 文件上传和下载:
    • 上传:
      • 前端:基于form表单 – post方式提交、采用multipart格式上传文件、使用input的file控件上传;
      • 服务端:只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件:public R<String> upload(MultipartFile file)
      • 实现:
        • 获取文件的原始文件名, 通过原始文件名获取文件后缀 suffix ;
        • 通过UUID重新声明文件名UUID.randomUUID().toString() + suffix, 避免文件名称重复造成文件覆盖;
        • 创建文件存放目录 – 从配置文件yml中读取@Value("${reggie.path}")
        • 将上传的临时文件转存到指定位置file.transferTo()
        • 将文件名返回给前端;
    • 下载:
      • 通过浏览器进行文件下载,就是服务端将文件以流的形式写回浏览器的过程;
      • 实现:
        • 定义输入流,通过输入流读取文件内容;
        • 通过response对象,获取到输出流;
        • 通过response对象设置响应数据格式(image/jpeg);
        • 通过输入流读取文件数据,然后通过上述的输出流写回浏览器;
        • 关闭资源。
  • 新增菜品:
    • 数据模型:两张表 – 菜品表dish、菜品口味表dish_flavor,dish_flavor中关联菜品ID;
    • 实体类、业务层、数据层:菜品相关在分类相关功能已导入,菜品口味实体类DishFlavor,Mapper接口DishFlavorMapper、业务层接口DishFlavorService、业务层实现类DishFlavorServiceImpl、控制层DishController – 菜品及菜品口味的相关操作统一使用一个Controller;
    • 实现:
      • 新建菜品页面打开后会先请求菜品分类数据 – 菜品分类查询,在CategoryController中增加list方法实现菜品分类查询并按照sort排序字段进行升序排序;
      • 图片上传:文件上传功能已实现;
      • 图片下载 – 上传的图片进行回显:文件下载功能已实现;
      • 提交菜品相关数据到服务器:
        • 提交过来的数据包含菜品口味信息 – 不能用菜品类Dish来封装,要增加封装flavors属性 – DTO(Data Transfer Object(数据传输对象),一般用于展示层与服务层之间的数据传输),DishDto实体类 – 继承自Dish实体类,增加了菜品口味列表、分类名称 – 用于后面分页查询的时候由categoryId获得分类名称后返回给前端渲染出来名称而不是一个Id、份数;
        • DishController定义方法public R<String> save(@RequestBody DishDto dishDto)新增菜品;
        • DishService中增加方法saveWithFlavor,操作dish、dish_flavor两张表;
        • DishServiceImpl中实现方法saveWithFlavor
          • 保存菜品基本信息;
          • 获取保存的菜品ID – 准备为菜品口味对象属性dishId赋值;
          • 获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值;
        • 操作了两张表,为了保证数据的一致性,需要在方法上加上注解 @Transactional来控制事务:
          • 在引导类上加上注解@EnableTransactionManagement, 开启对事务的支持;
          • Service层方法上加注解@Transactional
  • 菜品分页查询:
    • 分页查询:
      • 构造分页条件对象;
      • 构建查询及排序条件;
      • 执行分页条件查询;
      • 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称
      • 封装数据并返回 – 将分页数据查询到的关于 Dish 实体的 Page 拷贝到关于 DishDto 实体的 Page,并封装 categoryName 属性;
    • 根据分页结果中的图片名称进行图片文件下载回显;
  • 菜品修改:
    • 携带菜品id进入修改页面(和新增菜品页面复用同一个前端页面)后请求菜品分类数据 – 已实现;
    • 根据id查询当前菜品信息,用于菜品信息回显 – 包括口味信息;
      • DishService接口中扩展getByIdWithFlavor方法;
      • DishServiceImpl中实现方法:
        • 根据ID查询菜品的基本信息;
        • 根据菜品的ID查询菜品口味列表数据;
        • 组装数据并返回;
      • DishController中创建get方法
    • 发送请求,请求服务端进行图片下载,用于页图片回显 – 已实现;
    • 点击保存,发送请求,服务端执行修改 – 包括 dish_flavor 菜品口味表:
      • DishService接口中扩展方法updateWithFlavor
      • DishServiceImpl中实现方法:
        • 更新dish表基本信息;
        • 更新菜品口味表数据 – 原则:先删除,再添加;
        • 同时操作两张表 – 方法上添加@Transactional控制事务;
      • DishController中创建update方法;

3.3 套餐相关功能

套餐 – 菜品的集合;

  • 新增套餐
    • 数据模型:
      • setmeal表 – 套餐表:套餐名称name字段是不允许重复,创建唯一索引;
      • setmeal_dish表 – 套餐菜品关系表:关联套餐表的主键setmeal_id
    • 类准备:
      • 实体类 SetmealDish – 套餐菜品关系;Setmeal – 套餐类在分类的删除功能时已经引进;DTO类SetmealDto – 套餐数据传输对象,继承自 Setmeal ,额外包含 List<SetmealDish> 集合和categoryName分类名称用于后面分页查询的数据回显;
      • Mapper接口 SetmealDishMapper;
      • 业务层接口 SetmealDishService,业务层实现类 SetmealDishServiceImpl;
      • 控制层 SetmealController;
    • 实现:
      • 新建套餐,先请求套餐分类数据展示到下拉框中显示供选择,已实现,即查询分类数据,传递type参数为2;
      • 点击添加菜品,先请求服务端获取菜品分类数据 – 同样是查询分类列表,传递type为1;
      • 点击某个菜品分类,根据菜品分类请求菜品数据:
        • 在DishController中定义方法list,接收Dish类型的参数 – 接收分类ID – categoryId:;
        • 根据菜品分类categoryId进行查询;
        • 限定菜品的状态为启售状态(status为1);
        • 对查询的结果进行排序;
      • 套餐图片上传 – 文件上传,已实现;
      • 套餐图片下载回显 – 文件下载,已实现;
      • 保存 – 将套餐数据提交到服务端,包含套餐菜品关系列表:
        • SetmealDto 对前端json数据进行封装 – 主要是为了接收与套餐关联的菜品列表;
        • SetmealController中定义方法save,新增套餐;
        • SetmealService中定义方法saveWithDish;
        • SetmealServiceImpl实现方法saveWithDish:
          • 保存套餐基本信息this.save(setmealDto);
          • 获取套餐关联的菜品集合,并为集合中的每一个元素赋值套餐ID(setmealId);
          • 批量保存套餐关联的菜品集合setmealDishService.saveBatch(setmealDishes);
  • 套餐分页查询:
    套餐分页查询
    • 查询到的信息:套餐基本信息、套餐的分类名称 – 在分类表中查询;
    • 实现:
      • 套餐信息分页页面加载,将分页查询参数(page、pageSize、name)提交到服务端;
      • 服务端返回分页数据:
        • 分页查询:
          • 构建分页条件对象;
          • 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序;
          • 执行分页查询
        • 根据分类ID(categoryId), 查询套餐分类名称(categoryName),并最终将套餐的基本信息及分类名称信息封装到SetmealDto:
          • 将查询到的Page<Setmeal> pageInfo拷贝到Page<SetmealDto> dtoPageBeanUtils.copyProperties(pageInfo,dtoPage,"records");
          • 每一个套餐根据categoryId查询到分类对象category,再将categoryName封装到setmealDto中;
      • 页面根据图片名称请求服务端进行图片下载显示 – 已实现;
  • 删除套餐:
    • 需求:可以单个删除,也可以批量删除;售卖中的套餐需要先停售再删除;单个删除、批量删除都是通过提交要删除的套餐id向服务器发起请求;
    • 实现:
      • SetmealService接口定义方法removeWithDish:删除套餐,同时需要删除套餐和菜品的关联数据;
      • SetmealServiceImpl中实现方法removeWithDish(List<Long> ids)
        • 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除 – 根据id查询,并查询是否有状态statuse为1的 – 如果有,不能删除,抛出自定义异常throw new CustomException("套餐正在售卖中,不能删除")
        • 可以删除,删除套餐数据this.removeByIds(ids)
        • 删除套餐关联的菜品数据 – 套餐菜品关系表:setmealDishService.remove(lambdaQueryWrapper)
        • 操作两张表 – 在方法上加注解@Transactional来控制事务;
      • SetmealController中定义删除套餐方法delete(@RequestParam List<Long> ids)

四、用户相关功能

4.1 短信发送

  • 短信服务:
    • 实现短信发送功能 – 调用第三方提供的短信服务即可;
  • 阿里云短信服务:
    • 注册
    • 开通短信服务
    • 设置短信签名 – 短信签名:短信发送者的署名,表示发送方的身份 – 申请短信签名主要针对企业开发,需要审核资质;
      短信签名
    • 设置短信模板 – 包含短信发送内容、场景、变量信息;
      短信模板
    • 设置AccessKey – 访问阿里云 API 的密钥;
      • AccessKey – 具有账户的完全权限,相对来说不安全;
      • 子用户AccessKey – 分配比较低的权限,即使泄露也不影响其他云服务,相对安全;
    • 给子用户配置权限 – 短信服务操作;
    • 禁用/删除AccessKey:如果使用过程中AccessKey泄露删除泄露的,再创建一个新的AccessKey,保存好AccessKeyId和AccessKeySecret;
  • 代码开发:可以参照官方提供的文档 – 官方文档
    • 引入对应的依赖;
    • 将官方提供的main方法封装为一个工具类 – SMSUtils;

4.2 手机验证码登陆

  • 数据模型:用户user表,手机号作为区分用户的标识;
  • 类准备:
    • 实体类 User;
    • Mapper接口 UserMapper;
    • 业务层接口 UserService;
    • 业务层实现类 UserServiceImpl;
    • 控制层 UserController;
    • 工具类SMSUtils – 改造的阿里云短信发送的工具类、ValidateCodeUtils – 验证码生成的工具类;
  • 实现:
    • 修改检查用户登陆状态的LoginCheckFilter
      • 将手机验证码登陆的两个请求 – 获取验证码和登陆放行 – 添加到不需要处理的请求路径数组里面;
      • LoginCheckFilter 中判定如果移动端用户已登陆,将用户登陆信息存入ThreadLocal中后放行,后面需要获取当前登录用户ID直接从ThreadLocal获取;
    • 输入手机号后,点击【获取验证码】按钮,发送请求给服务端 – 服务端调用短信服务API给指定手机号发送验证码短信(实际实现为验证码输出在控制台):
      • UserController中创建方法sendMsg(@RequestBody User user, HttpSession session)
      • 生成4位随机验证码ValidateCodeUtils.generateValidateCode(4).toString()
      • 调用阿里云提供的短信服务API完成发送短信(实际为通过日志输出在控制台);
      • 将生成的验证码保存到Session:session.setAttribute(phone,code),方便后续登录时进行比对;
    • 登陆页面输入验证码,点击【登陆】按钮,发送请求 – 服务端处理登陆请求:
      • 在UserController中增加登录的方法login(@RequestBody Map map, HttpSession session)
      • 获取前端传递的手机号和验证码map.get("phone").toString() map.get("code").toString()
      • 从Session中获取到手机号对应的正确的验证码session.getAttribute(phone)
      • 进行验证码的比对:
        • 对比失败,直接返回错误信息;
        • 比对成功:根据手机号查询当前用户,如果不存在 – 为新用户,自动注册userService.save(user)
      • 登陆成功,把当前用户的手机号存储在sessionStorage,跳转到移动首页;

4.3 用户地址簿功能

  • 地址簿:移动端消费者用户的地址信息,同一个用户可以有多个地址信息,但是只能有一个默认地址;
  • 数据模型:地址簿表 – address_book表;字段is_default – 默认地址,0表示否,1表示是;
  • 类准备:
    • 实体类 AddressBook – 创建时间、创建人、修改时间、修改人自动填充@TableField
    • Mapper接口 AddressBookMapper;
    • 业务层接口 AddressBookService;
    • 业务层实现类 AddressBookServiceImpl;
    • 控制层 AddressBookController;
  • 地址簿功能:
    • 新增地址:
      • AddressBookController中新增方法:save(@RequestBody AddressBook addressBook)
      • 需要记录当前是哪个用户的地址(关联当前登录用户),从ThreadLoca中取出存储的用户ID – 工具类BaseContext
    • 设置默认地址:
      • 新增方法:setDefault(@RequestBody AddressBook addressBook)
      • 可以有很多地址,但是默认地址只能有一个 – 先将该用户所有地址的is_default更新为0 , 然后将当前的设置的默认地址的is_default设置为1;
    • 根据ID查询地址:
      • 新增方法:get(@PathVariable Long id)
    • 查询默认地址:
      • 新增方法:getDefault(),查询时根据当前登录用户ID并指定条件is_default为1;
    • 查询指定用户的全部地址:
      • 新增方法:list(AddressBook addressBook),根据当前登录用户ID;

五、点餐相关功能

5.1 菜品展示

  • 需求:
    • 系统首页:
      • 根据分类来展示菜品和套餐;
      • 如果菜品设置了口味信息显示“选择规格”按钮,否则是“+”按钮;
        系统首页
  • 实现:
    • 页面请求获取分类数据(菜品分类和套餐分类) – 已实现;
    • 请求获取第一个分类下的菜品列表或者套餐列表 – 前端根据分类的类别type来决定是向套餐请求数据还是向菜品请求数据:
      • 根据分类ID查询菜品列表(包含菜品口味列表),查询的菜品基本信息已实现(新建套餐时添加菜品根据菜品分类查询菜品列表),口味信息未包含:
        • 修改DishControllerlist方法,将返回值类型由R<List<Dish>>修改为R<List<DishDto>>从而可以封装菜品的口味列表:public R<List<DishDto>> list(Dish dish)
        • 根据分类ID查询目前正在启售的菜品列表;
        • 遍历菜品列表,并查询菜品的分类信息(主要是分类名称,用于在前端显示时显示分类名称而不是分类ID)及菜品的口味列表;
        • 组装数据DishDto,并返回;
      • 根据分类ID查询套餐列表:
        • SetmealController中创建list方法:public R<List<Setmeal>> list(Setmeal setmeal)
        • 根据条件查询套餐数据;

5.2 购物车

  • 需求:
    • 将菜品添加到购物车,如果设置了口味信息,则需要选择规格后才能加入;
    • 将套餐添加到购物车,直接加入;
    • 购物车中修改菜品、套餐的数量;
    • 清空购物车;
  • 数据模型
    • 购物车数据表 shopping_cart 表
       shopping_cart 表
    • 数据是关联用户的 – user_id字段;
    • 菜品列表展示出来的既有套餐,又有菜品,前端提交过来的是套餐就保存setmeal_id,选择的是菜品就保存dish_id
    • 同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可;
  • 类准备
    • 实体类 ShoppingCart;
    • Mapper接口 ShoppingCartMapper;
    • 业务层接口 ShoppingCartService;
    • 业务层实现类 ShoppingCartServiceImpl;
    • 控制层 ShoppingCartController;
  • 功能实现:
    • 添加购物车
      • ShoppingCartController中创建add方法:public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart)
      • 获取当前登录用户 – 基于ThreadLocal的BaseContext.getCurrentId() ,为购物车对象赋值;
      • 根据当前登录用户ID及 本次添加的菜品ID/套餐ID,查询购物车数据是否存在;
      • 如果已经存在,就在原来数量基础上加1;
      • 如果不存在,则添加到购物车,数量默认就是1;
    • 查询购物车
      • 在ShoppingCartController中创建list方法:public R<List<ShoppingCart>> list()
      • 根据当前登录用户ID查询购物车列表;
      • 对查询的结果进行创建时间的倒序排序;
    • 清空购物车
      • 在ShoppingCartController中创建clean方法:public R<String> clean()
      • 获取当前登录用户,根据登录用户ID,删除购物车数据;

5.3 下单

  • 需求:
    • 点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面;
    • 点击 “去支付” 按钮则完成下单操作(需要资质,支付功能不实现);
      下单功能需求
  • 数据模型:
    • 订单表orders表:存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等);
      订单表
    • 订单明细表order_detail表(一对多关系,一个订单关联多个订单明细):存储订单关联的套餐及菜品的信息;
      订单明细表
  • 类准备:
    • 实体类 Orders、OrderDetail;
    • Mapper接口 OrderMapper、OrderDetailMapper;
    • 业务层接口 OrderService、OrderDetailService;
    • 业务层实现类 OrderServiceImpl、OrderDetailServiceImpl;
    • 控制层 OrderController、OrderDetailController;
  • 功能实现:
    • 前端购物车点击去结算,跳转到订单确认页面(前端实现);
    • 订单确认页面请求服务端获取当前登录用户的默认地址(用户地址簿功能已实现);
    • 订单确认页面请求服务端获取购物车数据(购物车功能已实现);
    • 点击“去支付”,请求服务端完成下单操作:
      • OrderController中创建submit方法:public R<String> submit(@RequestBody Orders orders)
      • OrderService中定义submit方法,来处理下单的具体逻辑:public void submit(Orders orders)
        • 获得当前用户id:BaseContext.getCurrentId()
        • 查询当前用户的购物车数据shoppingCartService.list(wrapper) – 如果查询购物车为空,抛出自定义异常CustomException不能下单;
        • 根据当前登录用户id, 查询用户数据userService.getById(userId)
        • 根据地址IDorders.getAddressBookId, 查询地址数据addressBookService.getById(addressBookId) – 如果查询地址为空,抛出自定义异常CustomException不能下单;
        • 生成订单号:IdWorker.getId();
        • 组装订单数据, 保存订单数据 – 一条数据:this.save(orders)
        • 组装订单明细数据, 批量保存订单明细 – 多条数据orderDetailService.saveBatch(orderDetails)
        • 删除当前用户的购物车列表数据shoppingCartService.remove(wrapper)
        • 计算购物车商品的总金额时,为保证我们每一次执行的累加计算是一个原子操作,用到了JDK中提供的一个原子类AtomicInteger

六、优化 – Redis缓存

6.1 缓存短信验证码

  • 原方案:随机生成的验证码保存在HttpSession中,但一般验证码都是需要设置过期时间,存在HttpSession中就无法设置;
  • 用Redis缓存短信验证码实现:
    • 在服务端UserController中注入RedisTemplate对象,用于操作Redis:private RedisTemplate redisTemplate;
    • 在UserController的sendMsg方法中,将生成的验证码保存到Redis,并设置过期时间:redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
    • 在UserController的login方法中,从Redis中获取生成的验证码redisTemplate.opsForValue().get(phone);,如果登录成功则删除Redis中缓存的验证码redisTemplate.delete(phone);

6.2 缓存菜品信息

  • 移动端菜品展示功能:
    • 原方案:请求服务端DishControllerlist方法,根据前端提交的查询条件(categoryId)进行数据库查询操作;
    • 问题:高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长;
  • Redis缓存菜品信息思路:
    • 前端点击一个分类就展示该分类下的菜品:缓存时,可以根据菜品的分类,缓存多份数据;查询时,点击的是哪个分类,我们就查询该分类下的菜品缓存数据;
    • 用户端查询菜品:改造DishController的list方法,先从Redis中获取分类对应的菜品数据:
      • 如果有缓存则直接返回,无需查询数据库;
      • 没有则查询数据库,并将查询到的菜品数据存入Redis;
    • 后台管理系统新增、修改菜品:如果数据库中的数据发生变化,需要及时清理缓存数据,否则就会造成缓存数据与数据库数据不一致的情况 – 改造DishController的save和update方法,加入清理缓存的逻辑;
  • 实现:
    • 查询菜品缓存
      • 在DishController中注入RedisTemplate;
      • list方法中,查询数据库之前,先查询缓存, 缓存中有数据, 直接返回;
      • 如果redis不存在,查询数据库,并将数据库查询结果,缓存在redis,并设置过期时间;
    • 清理菜品缓存
      • 保存菜品save时清理缓存:清理当前添加菜品分类下的缓存:redisTemplate.delete(key);
      • 更新菜品update时清理缓存:清理所有分类下的菜品缓存:redisTemplate.delete(keys); – 更新菜品时后台员工可能修改该菜品的分类,那么原分类少一个菜品、新分类多一个菜品,有两个菜品分类数据变化。

6.3 Spring Cache

  • Spring Cache – 基于注解的缓存功能的框架;
  • 常用注解:
    • @EnableCaching:开启缓存注解功能;
    • @Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中;
    • @CachePut:将方法的返回值放到缓存中;
    • @CacheEvict:将一条或多条数据从缓存中删除;

6.4 缓存套餐数据

  • 移动端套餐查看功能:SetmealController的list方法 – 高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长 – 利用Redis进行缓存优化;
  • 环境准备:
    • 导入Spring Cache和Redis相关maven坐标;
    • 在application.yml中配置缓存数据的过期时间;
    • 在启动类上加入@EnableCaching注解,开启缓存注解功能;
  • 实现:
    • 查询套餐缓存
      • 在SetmealController的list方法上加入@Cacheable注解;
      • @Cacheable 将方法的返回值R缓存在Redis中,而在Redis中存储对象,该对象是需要被序列化的 – 让R实现 Serializable 接口;
    • 清理套餐缓存
      • 在SetmealController的save和delete方法上加入@CacheEvict注解;

七、优化 – 读写分离

读写分离

7.1 MySQL的主从复制

  • 原理:记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句的二进制日志(BINLOG) – 不包括数据查询语句;
    主从复制原理
  • MySQL复制过程:
    • MySQL master 将数据变更写入二进制日志( binary log);
    • slave将master的binary log拷贝到它的中继日志(relay log);
    • slave重做中继日志中的事件,将数据变更反映它自己的数据;

7.2 读写分离

ShardingJDBC + 项目application.yml中配置数据源相关信息;

八、优化 – Nginx

8.1 部署静态资源

  • 将前端静态资源文件复制到Nginx安装目录下的html目录中;

8.2 反向代理

  • 正向代理:正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器;
    正向代理
  • 反向代理:对于用户而言,反向代理服务器就相当于目标服务器,用户不需要知道目标服务器的地址,也无须在用户端作任何设定;
    反向代理
  • nginx反向代理:在nginx.conf中配置反向代理:
server {
	listen 82;
	server_name localhost;
	location / {
		proxy_pass http://192.168.200.201:8080; #反向代理配置,将请求转发到指定服	务
	}
}

8.3 负载均衡

  • 在nginx中配置负载均衡:
#upstream指令可以定义一组服务器
upstream targetserver{
	server 192.168.200.201:8080;
	server 192.168.200.201:8081;
} 
server {
	listen 8080;
	server_name localhost;
	location / {
		proxy_pass http://targetserver;
	}
}
  • 负载均衡策略:
    • 轮询;
    • weight:权重方式;
    • ip_hash:依据ip分配方式;
    • least_conn:依据最少连接方式;
    • url_hash:依据url分配方式;
    • fair:依据响应时间方式;

九、优化 – Linux部署

9.1 部署架构

部署架构

  • 虚拟机架设两台Linux服务器分别部署Nginx、Tomcat、主从复制功能的两台Mysql;
  • 自己主机部署redis;

9.2 前端部署

  • 在服务器A(192.168.138.100)中安装Nginx,将前端资源整个目录上传到Nginx的html目录下;
  • 修改Nginx配置文件nginx.conf:
server {
	listen 80;
	server_name localhost;
	
	// 配置静态资源加载目录
	location / {
		root html/dist;
		index index.html;
	} 
	
	// 反向代理配置
	location ^~ /api/ {
		rewrite ^/api/(.*)$ /$1 break;
		proxy_pass http://192.168.138.101:8080;
	} 
	
	location = /50x.html {
		root html;
	}
}

9.3 反向代理配置

  • 见9.2中修改nginx.conf时的反向代理配置:
  • 如果请求当前nginx,并且请求的路径如果是 /api/ 开头,将会被该location处理;
  • 在该location中,主要配置了两个信息: rewrite(url重写) 和 proxy_pass(反向代理);
    • 重写就是向tomcat请求时去掉了请求路径的/api/,从而不用修改原代码,所以前端代码里需要追加一个/api/;
    • 反向代理:proxy_pass http://192.168.138.101:8080;

9.4 服务端部署

  • 在服务器B(192.168.138.101)中安装jdk、git、maven、MySQL;
  • 使用git clone命令将git远程仓库的代码克隆下来;
  • 将提供的reggieStart.sh文件上传到服务器B,通过chmod命令设置执行权限;
  • 执行reggieStart.sh脚本文件,自动部署项目;
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值