文章目录
一、项目整体
- 软件开发流程:需求分析,设计,编码,测试,上线运维;
- 软件环境:开发环境(development),测试环境(testing),生产环境(production);
- 系统管理后台功能:员工登陆/退出、员工管理、分类管理、菜品管理、套餐管理、(订单明细);
- 用户端:登陆/退出、点餐-菜单、点餐-购物车、下单(支付功能未实现)、个人信息–地址簿;
- 数据库表:员工表employee、菜品和套餐分类表category、菜品表dish、套餐表setmeal、套餐菜品关系表setmeal_dish、菜品口味关系表dish_flavor、用户表(C端)user、地址簿表address_book、购物车表shopping_cart、订单表orders、订单明细表order_detail;
- 通用结果类R:
- 属性:编码code、信息msg、数据data(泛型);
- 方法:成功方法
success()
– 设置data
和code=1
;错误方法error()
– 设置msg
和code = 0
;
二、员工相关功能
2.1 登陆退出功能
- 后台系统登陆功能:
- 页面提交的密码password进行md5加密处理;
- 登录成功,将员工id存入Session, 并返回登录成功结果:
request.getSession().setAttribute("employee",emp.getId());
- 后台系统退出功能:清理Session中的用户id:
request.getSession().removeAttribute("employee");
- 完善登录功能:增加登陆后才能访问相关页面:
- 登录校验过滤器:自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑;
- 定义不需要处理的请求路径 – 登陆、退出请求,用户端、后台管理端静态资源;如果不需要处理,则直接放行–Spring中提供的路径匹配器AntPathMatcher;
- 使用session判断是否登陆:
request.getSession().getAttribute("employee")
; - 需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解;
2.2 员工管理相关功能
- 新增员工:
- 初始密码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违背值唯一性约束异常;
- 初始密码123456,需要进行md5加密处理:
- 分页查询:
- 需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象:
MybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor())
; - 分页构造器Page,分页查询的结果也存放在里面。
- 需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象:
- 启用/禁用员工账号:
- 在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
- 在Controller中创建update方法:
- 编辑员工信息:先查询
getById()
再提交修改update()
; - 公共字段<创建时间、更新时间、创建人、更新人>自动填充:MybatisPlus公共字段自动填充:
- 在实体类的属性上加入
@TableField
注解,指定自动填充的策略 – INSERT、UPDATE、INSERT_UPDATE; - 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现
MetaObjectHandler
接口 – 该接口中不能直接获得HttpSession对象进而获得登录用户的id来填充创建人、更新人信息;
- 获取登录用户id:
- 客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理 – 使用Thread的局部变量
ThreadLocal
–ThreadLocal
为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值; - 基于
ThreadLocal
封装BaseContext
工具类,并基于ThreadLocal
的set()
、get()
方法封装静态方法:setCurrentId(Long id)
、getCurrentId()
; - 实现:在
LoginCheckFilter
的doFilter
方法中从session中获取当前登录用户idrequest.getSession().getAttribute("employee")
,并借助封装的工具类BaseContext.setCurrentId(empId)
来设置当前线程的线程局部变量的值(用户id);
- 客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理 – 使用Thread的局部变量
- 在
MyMetaObjectHandler
的updateFill(MetaObject metaObject)
、insertFill(MetaObject metaObject)
方法中通过BaseContext.getCurrentId()
来获得当前线程所对应的线程局部变量的值(用户id);
- 在实体类的属性上加入
三、菜品相关功能
3.1 分类相关功能
- 新增分类:
- 两种类型的分类:菜品分类和套餐分类;分类的排序用来控制移动端分类列表的展示顺序;
- 数据模型:
- 分类表category表;
- 分类的名称 name 唯一、不能重复;类型 type 区分菜品和套餐;
- 分类表category表;
- 代码实现:
- 实体类(使用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
中处理自定义异常 – 增加方法捕获自定义的异常CustomException
:exceptionHandler(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
;
- 在引导类上加上注解
- 提交过来的数据包含菜品口味信息 – 不能用菜品类Dish来封装,要增加封装flavors属性 – DTO(Data Transfer Object(数据传输对象),一般用于展示层与服务层之间的数据传输),
- 新建菜品页面打开后会先请求菜品分类数据 – 菜品分类查询,在
- 菜品分页查询:
- 分页查询:
- 构造分页条件对象;
- 构建查询及排序条件;
- 执行分页条件查询;
- 遍历分页查询列表数据,根据分类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;
- 实体类 SetmealDish – 套餐菜品关系;Setmeal – 套餐类在分类的删除功能时已经引进;DTO类SetmealDto – 套餐数据传输对象,继承自 Setmeal ,额外包含
- 实现:
- 新建套餐,先请求套餐分类数据展示到下拉框中显示供选择,已实现,即查询分类数据,传递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> dtoPage
:BeanUtils.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
来控制事务;
- 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除 – 根据id查询,并查询是否有状态statuse为1的 – 如果有,不能删除,抛出自定义异常
- 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,跳转到移动首页;
- 在UserController中增加登录的方法
- 修改检查用户登陆状态的
4.3 用户地址簿功能
- 地址簿:移动端消费者用户的地址信息,同一个用户可以有多个地址信息,但是只能有一个默认地址;
- 数据模型:地址簿表 – address_book表;字段
is_default
– 默认地址,0表示否,1表示是; - 类准备:
- 实体类 AddressBook – 创建时间、创建人、修改时间、修改人自动填充
@TableField
; - Mapper接口 AddressBookMapper;
- 业务层接口 AddressBookService;
- 业务层实现类 AddressBookServiceImpl;
- 控制层 AddressBookController;
- 实体类 AddressBook – 创建时间、创建人、修改时间、修改人自动填充
- 地址簿功能:
- 新增地址:
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查询菜品列表(包含菜品口味列表),查询的菜品基本信息已实现(新建套餐时添加菜品根据菜品分类查询菜品列表),口味信息未包含:
- 修改
DishController
的list
方法,将返回值类型由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)
; - 根据条件查询套餐数据;
- 在
- 根据分类ID查询菜品列表(包含菜品口味列表),查询的菜品基本信息已实现(新建套餐时添加菜品根据菜品分类查询菜品列表),口味信息未包含:
5.2 购物车
- 需求:
- 将菜品添加到购物车,如果设置了口味信息,则需要选择规格后才能加入;
- 将套餐添加到购物车,直接加入;
- 购物车中修改菜品、套餐的数量;
- 清空购物车;
- 数据模型:
- 购物车数据表 shopping_cart 表
- 数据是关联用户的 –
user_id
字段; - 菜品列表展示出来的既有套餐,又有菜品,前端提交过来的是套餐就保存
setmeal_id
,选择的是菜品就保存dish_id
; - 同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可;
- 购物车数据表 shopping_cart 表
- 类准备:
- 实体类 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中创建list方法:
- 清空购物车:
- 在ShoppingCartController中创建clean方法:
public R<String> clean()
; - 获取当前登录用户,根据登录用户ID,删除购物车数据;
- 在ShoppingCartController中创建clean方法:
- 添加购物车:
5.3 下单
- 需求:
- 点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面;
- 点击 “去支付” 按钮则完成下单操作(需要资质,支付功能不实现);
- 数据模型:
- 订单表orders表:存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等);
- 订单明细表order_detail表(一对多关系,一个订单关联多个订单明细):存储订单关联的套餐及菜品的信息;
- 订单表orders表:存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等);
- 类准备:
- 实体类 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)
; - 根据地址ID
orders.getAddressBookId
, 查询地址数据addressBookService.getById(addressBookId)
– 如果查询地址为空,抛出自定义异常CustomException
不能下单; - 生成订单号:
IdWorker.getId();
; - 组装订单数据, 保存订单数据 – 一条数据:
this.save(orders)
; - 组装订单明细数据, 批量保存订单明细 – 多条数据
orderDetailService.saveBatch(orderDetails)
; - 删除当前用户的购物车列表数据
shoppingCartService.remove(wrapper)
; - 计算购物车商品的总金额时,为保证我们每一次执行的累加计算是一个原子操作,用到了JDK中提供的一个原子类
AtomicInteger
。
- 获得当前用户id:
- 在
六、优化 – 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);
;
- 在服务端UserController中注入RedisTemplate对象,用于操作Redis:
6.2 缓存菜品信息
- 移动端菜品展示功能:
- 原方案:请求服务端
DishController
的list
方法,根据前端提交的查询条件(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的list方法上加入
- 清理套餐缓存:
- 在SetmealController的save和delete方法上加入
@CacheEvict
注解;
- 在SetmealController的save和delete方法上加入
- 查询套餐缓存:
七、优化 – 读写分离
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脚本文件,自动部署项目;