PS:纯小白,有部分原理知识按照自己的理解来写的,可能专业术语使用不当,欢迎大家批评指出错误。
参考视频出处:黑马程序员Java项目实战《苍穹外卖》https://www.bilibili.com/video/BV1TP411v7v6/?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click&vd_source=234efc646b1bb4e708f30f94d7060b16
本文只写了在练习该项目中比较困难的部分,仍有部分业务功能未写进来,后续会慢慢补充,未完待续.....
整体架构
dto常用于传输数据,作为参数,接收到的dto可以将封装的内容交给实体对象中的内容
业务实现
员工业务实现
员工登录与退出
接口文档
请求参数为body类型,将其封装在EmployeeLoginDTO类中,添加注解@RequestBody,用于将HTTP请求体中的JSON数据绑定到参数对象上。
调用EmployeeService接口方法login
/**
* 员工登录
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对
// 需要进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
}
密码加密:使用DigestUtils工具类中的方法
登陆成功后,生成JWT令牌,实现一个拦截器,每次请求业务时进行JWT令牌的校验。

拦截器的实现
1.创建一个配置类(普通Java类),并使用@ConfigurationProperties注解指定配置属性的前缀,该配置应当配置在application.yml文件中
驼峰命名,admin-secret-key -> adminSecretKey
2.定义拦截器的实现类,并将配置类以依赖注入的形式引入进来,方便使用其属性,如图1。
3.定义好拦截器类后,在Sever层中创建一个WebMvcConfiguration类,该类被声明为配置类,用于注册web层的相关组件。
依赖注入的关系:
JwtProperties -> JwtTokenAdminInterceptor -> WebMvcConfiguration
ThreadLocal
每一次请求就是一个线程,在一次请求未结束之前,共享内容ThreadLocal内容
由于每次请求会被拦截器拦截,拦截到请求后会通过BaseContext.setCurrendId将token中的id取出保存到ThreadLocal中。
BaseContetxt类中封装了ThreadLocal类对象,并定义了threadlocal方法对应的set和get方法
新增员工
将前端传递来的参数封装进EmployeeDTO类中,在sever层中将DTO类数据封装给实体类
驼峰映射:使数据库字段与Java对象属性之间进行映射
员工的分页查询
分页查询参数
name字段非必须,用于模糊匹配。page字段为查询的第几页开始,pageSize为一页显示多少张
前端的相应参数为Query请求参数而非Body。
Query和Body
数据传递位置不同:Body 数据在请求主体中,Query 数据在 URL 查询字符串中。
参数传递
在controller层,使用一个对象来封装传递来的三个参数,这种方式是可行的。不过需要保证的是,类中的属性名要与传入参数的名字保持一致,不一致需要用注解进行绑定
返回值
分页查询的返回值有两个:①分页查询的员工数据集合需要渲染到前端②查询到的符合条件的记录数。 因此可以使用pageResult对象进行封装,并指定Result的泛型为pageResult,这样pageResult中data的类型也被指定为泛型(之前返回值为Result没写泛型,所以data数据的默认泛型为Object类型)
分页查询插件PageHelper
pageHelper的底层用到ThreadLocal,在分页查询之前,将page和pageSize从ThreadLocal中取出,将Limit动态拼接到sql语句中。
代码问题
方法一:用到了JsonFormat的序列化与反序列化,缺点是对每一个属性都要添加
在WebMvcConfiguration配置类中,声明了一系列web组件,在其中扩展MVC的消息转换器。重写父类中的extendMessageConverters方法进行扩展。
启用、禁用员工账号
涉及到两种类型参数路径参数,应当添加@PathVariable注解
@Build注解
在调用service层方法时,新创建一个employee对象,为了避免冗余的set方法,在Employee类上声明@Build注解,快速生成一个实例对象
修改员工信息
将前端传递的请求参数封装在EmployeeDTO类中,在Service层传递给Employee对象,调用上一个方法创建的动态SQL,SetStatus方法中实现了修改所有属性的sql语句,直接调用即可
分类业务实现
分类业务导入
分类业务涉及到三张表,类别表category,菜品表dish,套餐表setmeal
删除分类
根据id删除分类需要判断当前分类下是否有菜品或者套餐,如果有则不允许删除并且抛出异常
修改与添加分类
问题分析
在管理员工业务,分类业务,后续的菜品,套餐管理业务中,均需要涉及修改或者增加操作,每次操作都需要设置创建时间、修改时间、创建人、修改人的信息,使代码冗余。因此使用AOP面向切面编程解决问题
除此之外,利用自定义注解来替代切入点表达式更好。
①首先自定义注解,用于标识需要进行公共字段自动填充
②自定义切面类AutoFillAspect
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 定义切入点表达式
*/
//添加自定义的注解
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autofillPointCut(){}
//指定切入点表达式,定义前置通知
@Before("autofillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("执行自动填充");
//获取到当前被拦截方法上的数据库操作类型
MethodSignature signature =(MethodSignature)joinPoint.getSignature();//方法对象签名
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//方法上的注解对象
OperationType operationType = autoFill.value();//获取数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
//排除空指针的特殊情况
if(args ==null || args.length==0){
return;
}
/**
* args[0]:约定实体对象放在方法参数的第一个位置
* 由于后续功能开发,返回的实体对象的类型不确定,统一使用Obiect类型
*/
Object entry = args[0];
//对实体对象公共属性进行赋值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
if(operationType==OperationType.INSERT){
//插入操作需要为四个公共字段赋值
try {
Method setCreateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性进行赋值
setCreateTime.invoke(entry, now);
setCreateUser.invoke(entry, currentId);
setUpdateTime.invoke(entry, now);
setUpdateUser.invoke(entry, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}else if(operationType==OperationType.UPDATE){
//更新操作为两个字段赋值
try {
Method setUpdateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entry, now);
setUpdateUser.invoke(entry, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
动态调用方法,指定是哪个对象来调用,传递的参数是什么。
③在Mapper的方法上加入AutoFill注解,并指定value属性
④由于是前置通知,在点击业务按钮后,前置通知拦截到该方法,并执行前置通知中定义的逻辑,获取到现在传入的employee对象,对其字段进行填充,随后再调用原始操作
反射
通过获取一个类的类对象,才能通过反射获取到类中的属性,变量,方法等。
一、获取class对象的三种方式
①Class.forname("全类名")
②类对象.class
③对象.getClass()
编写java源文件后,扩展名为.java。使用java编辑器将java源文件编译成字节码文件,编译成功后,java编辑器会在硬盘上生成与每个类对应的.class文件,每个.class文件对应一个类。在这个阶段,是在硬盘中完成的,获取类对象使用方式①
在运行时,将字节码文件加载到内存中,这一过程为加载阶段,使用方式②
在内存创建对象,运行阶段使用方式③
菜品相关接口
上传图片
配置文件yml
表明在启动spring boot项目时会激活名为dev的配置文件,可以将硬编码的部分交给dev文件,再通过占位符的形式将参数传递给该配置文件,使得隐藏内容,图1:application.yml,图2""-dev.yml


阿里云配置属性类
阿里云工具类(导入即可,按需求修改内容)
工具类中定义了4个属性,上传文件的方法upload,返回值为在阿里云oss中文件的名字
工具类添加到IOC容器
文件上传Controller层
添加菜品
传递参数
将前端的请求参数封装进DishDTO类中
涉及多表
DishDTO类中既包含了1个菜品的信息,还包含了该菜品的多个口味。所以涉及到将数据拆分出来插入到不同的表中。
一、菜品表Dish
由于在之前实现了通过注解将共同的属性进行赋值,所以只需要在mapper方法上添加@AutoFill(value=?)注解并指明数据库操作的类型
经过插入操作,dish类的categoryId属性被赋值,但主键值并未赋值
而在选择好该菜品的口味后,需要将该菜品的主键值传给口味表,实现两个表的一对多的关系
二、口味表Dish_Flavor
使用foreach标签进行遍历拼接,collection的值应为传入集合的名字
事务控制
由于对两张表的操作是连续的不能间断的,因此使用事务控制,保证了事务的原子性。
在方法上添加Transactional注解表示开启事务管理。同时需要在Spring Boot项目的启动类上添加@EnableTransactionManagement注解标识开启注解方式的事务管理。
菜品的分页查询
在向前端返回的数据中,额外有一项categoryName,该属性属于category表的id属性。涉及到多表查询,由于dish表所需要全部数据,而后表只需要一项数据,所以左外连接后表,同时分页查询,分类名称的模糊查询。
返回的VO对象中分类名称为categoryName,而表中字段名为name,在讲结果集映射到java对象时,通常会使用数据库表中的原始列名进行映射,导致映射将失败。
菜品的批量删除
将单个删除的情况也看作为批量删除的情况,请求参数为query
在控制器方法参数前添加@RequestParam注解,用于将请求参数绑定到控制器方法的参数上。它可以处理查询字符串参数、表单数据和路径变量
删除条件
1.查询起售状态---当前正在起售菜品不能删除
2.是否关联着套餐--关联着套餐不能删除
3.在删除菜品的时候,需要将该菜品对应的口味全部删除
根据Id查询菜品信息
返回数据
当点击修改菜品按钮时,首先根据菜品的id进行业务查询,将菜品的信息回显给前端
返回的data类型为DishVO,其中为dish类属性+菜品口味的列表。
处理逻辑,分两次查询,分别调用dishMapper和dishFlavorMapper层方法,将两次查询结果封装进dishVO对象中。
根据id修改菜品
完成上一步回显操作后,进行修改各类信息。
问题分析
某些菜品本来有一定的口味,若在其原有的基础上添加,或者需要删除掉原有的口味信息,由于情况复杂,采用:在提交修改之前将原有的口味全部删除一遍,再将改好后的口味信息从前端传递给后端。
套餐接口
项目已导入
Redis数据库
Redis数据库中的数据类型
在Java中操作Redis数据库
③编写配置类
④通过RedisTemplate对象来操作Redis
设置店铺的营业状态和查询状态
由于店铺的营业状态为单个值,无需使用mysql数据库为其单独创建一张表,而且涉及到经常改变,所以Redis数据库的Key-Value结构更有优势
微信小程序开发
微信登陆过程
微信登陆
定义配置文件
由于商家端和用户移动端都需要对是否登陆操作进行校验,因此在商家端和用户端都需要用到jwt技术来验证是否登录
配置属性类
将配置文件中的属性值传递给微信用户生成jwt令牌相关配置,并将该类添加进IOC容器管理
接口文档
Controller层
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信登录的code:{}", userLoginDTO.getCode());
//将返回的结果id和openid封装在User实体类中
User user = userService.wxlogin(userLoginDTO);
//创建JWt令牌
//将获取到的id和openid添加进hashmap封装金jwt令牌中
Map<String, Object> claims = new HashMap<>();
//openid字段无需封装进jwt令牌因为VO对象中有属性用于接受openid
claims.put(JwtClaimsConstant.USER_ID,user.getId());
//将配置类自动注入获取到其中的SecretKey和UserTtl,生成JWT令牌
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
//封装VO对象
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId()) //封装用户id(在本地数据库中唯一)
.openid(user.getOpenid()) //openid(在微信服务器中唯一)
.token(token)
.build();
return Result.success(userLoginVO);
}
}
Service层
向微信服务端接口发起http请求,并传入所需的四个参数,获取到返回的openid。
HttpClient返回值为JSON格式,其中包含了所需要的openid。获取到其中openid的方法:
①获取到JSON对象 ②JSON对象的getString方法获取到键为openid的值
判断openid是否为空,如果为空抛出业务异常,不为空返回
public class UserServiceImpl implements UserService {
//定义常量--微信服务端地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@Override
public User wxlogin(UserLoginDTO userLoginDTO) {
//调用微信接口获取openid
String openid = getString(userLoginDTO);
if(openid==null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断该用户是否为新用户--查找openid在用户表中是否存在
User user = userMapper.getByopenid(openid);
//如果为新用户,则自动注册
if(user==null){
//将当前新用户信息插入到数据库中
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
return user;
}
/**
* 调用微信服务端接口
* @param userLoginDTO
* @return
*/
private String getString(UserLoginDTO userLoginDTO) {
//创建微信接口所需要传递的参数集合
Map<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code", userLoginDTO.getCode());
map.put("grant_type","authorization_code"); //四个官方要求传递的固定参数
String json = HttpClientUtil.doGet(WX_LOGIN, map);
//判断openid是否为空--登陆失败,抛出业务异常
//获取到json对象
JSONObject jsonObject = JSON.parseObject(json);
//获取到键为openid对应的值
String openid = jsonObject.getString("openid");
return openid;
}
}
封装逻辑:user中封装获取到的id和openid,生成的jwt令牌为"token",id封装进jwt令牌中,三者封装在userLoginVO对象中。
缓存菜品数据
原因:通过Redis来缓存菜品数据,减少数据库的查询操作。
缓存菜品(代码版)
将菜品内容以String类型存入Redis中
删除缓存
传入的pattern为通配表达式
Spring Cache框架
简化代码开发。该框架实现了基于注解的缓存功能。Spring Cache提供了一层抽象,在底层可以切换不同的缓存实现,如EHCache、Caffeine、Redis。
常用注解
最终缓存到Redis中的样式为setmeal::categoryId
添加购物车
接口文档:其中dishId和setmealId不能同时传递
逻辑分析
1.添加的购物车需要知道当前用户是哪个用户
2.若当前购物车中有该商品,则数目+1
3.新商品需要判断为菜品还是套餐进行插入
4.新插入的商品数目设置为1
/**
* 添加购物车
*
* @param shoppingCartDTO
*/
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 创建实体对象
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
//设置当前用户的userId
shoppingCart.setUserId(BaseContext.getCurrentId());
//查询购物车数据只可能为0或者1,不能为多
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
//查询购物车表,如果已存在当前商品,则数目+1
if (list != null && list.size() > 0) {
//列表中唯一的数据
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber() + 1);
//更新菜品数目
shoppingCartMapper.updateNumberById(cart);
} else {
//若不存在,则插入商品信息
Long setmealId = shoppingCartDTO.getSetmealId();
Long dishId = shoppingCartDTO.getDishId();
//根据传递的为菜品还是套餐来查询表获取到name,image等属性
if (dishId != null) {
//当前添加为菜品
Dish dish = dishMapper.getById(dishId);
//属性拷贝到cart
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
} else {
//当前添加为套餐
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
//统一设置数目与时间
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
}
shoppingCartMapper.insert(shoppingCart);
}
地址簿模块难点
设置默认地址
传递来的JSON格式的数据可以被绑定到类中的属性,其中传递的id值可以自动绑定到AddressBook类中的id属性上
用户下单
返回参数
Controller层
前端传递参数封装进OrdersSubmitDTO对象,返回值封装进orderSubmitVO对象
业务逻辑
订单基本信息单独存放,订单中的菜品存放到另一张表
1.首先处理业务异常,当购物车为空,地址簿为空的情况,各自抛出定义的异常
2.获取当前用户的购物车内容
3.向订单表插入一条数据
4.向订单明细表插入多条数据
/**
* 用户订单提交
* @param ordersSubmitDTO
* @return
*/
@Override
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//处理各种业务异常(地址簿为空,购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());// 根据地址簿id查询地址
if(addressBook == null){
//没有查到地址,抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//获取当前用户id
Long userid = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userid);
//获取用户购物车内容
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list == null || list.size() == 0){
//当前购物车为空
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//向订单表提交一条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
//设置订单号--以当前系统的时间戳
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userid);
orderMapper.insert(orders);
//获取到当前订单id
Long orderId = orders.getId();
//创建存放n条明细的集合,使插入操作就做一次提高效率
List<OrderDetail> orderDetailList = new ArrayList<OrderDetail>();
//向订单明细表插入n条数据
for (ShoppingCart r : list) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(r,orderDetail);
orderDetail.setOrderId(orderId);
//添加到集合中
orderDetailList.add(orderDetail);
}
//插入
orderDetailMapper.insertBench(orderDetailList);
//清空当前用户的购物车
shoppingCartMapper.deleteByUserId(userid);
//封装返回VO对象
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orderId)
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.orderTime(orders.getOrderTime()).build();
return orderSubmitVO;
}
订单支付
导入订单支付功能代码,熟悉微信支付流程
用户端历史订单模块
历史订单查询
逻辑分析:分页查询,根据订单状态动态查询,返回的结果有订单的基本信息(Orders)并且还要有订单明细(Orders_detail),因此选择返回对象封装进OrderVO对象中(其继承自Orders并且还有订单明细属性)
①:返回结果有total和records,total为分页查询的订单总数,records为查询结果(OrderVO类)
②:分页查询的结果泛型为订单类,遍历该集合,获取到每一个订单对象以及查询到每个订单对应的订单明细,订单对象的信息经过属性拷贝到OrderVO继承下来的属性中,订单明细集合赋值给OrderVO中新加的属性。将每一个封装好的OrderVO对象封装成集合,再将该集合封装进PageResult对象的records属性中。
取消订单
逻辑分析:
- 待支付和待接单状态下,用户可直接取消订单
- 商家已接单状态下,用户取消订单需电话沟通商家
- 派送中状态下,用户取消订单需电话沟通商家
- 如果在待接单状态下取消订单,需要给用户退款
- 取消订单后需要将订单状态修改为“已取消”
根据订单状态进行判断,微信退款
再来一单(stream流)
业务逻辑:将再来一单的菜品重新添加到购物车中
①判断当前用户id(将菜品添加到本用户的购物车中)
②订单明细对象OrderDetails---->购物车对象shoppingCart
③将购物车对象集合批次添加进数据库
/**
* 再来一单
*
* @param id
*/
public void repetition(Long id) {
// 查询当前用户id
Long userId = BaseContext.getCurrentId();
// 根据订单id查询当前订单详情
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
// 将订单详情对象转换为购物车对象
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
ShoppingCart shoppingCart = new ShoppingCart();
// 将原订单详情里面的菜品信息重新复制到购物车对象中,忽略“id”属性的值
BeanUtils.copyProperties(x, shoppingCart, "id");
shoppingCart.setUserId(userId);
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
// 将购物车对象批量添加到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}
增强for的细节:修改增强for中的变量,不会改变集合中原本的数据
xml
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>
Stream流知识补充
一、lambda表达式
Lambda表达式只能用于简化函数式接口的匿名内部类的写法
二、stream流
①stream流的中间方法map
map方法的参数为一个函数式接口,可以简写成lambda表达式
②collect方法
收集流中的数据,放到集合中(List、set、Map)
List:collect(collectors.toList())
Map:collect(collectors.toMap(键的规则、值的规则))
商家端订单管理模块
订单搜索
接口分析
返回结果中orerDishes为OrderVO对象中的属性值,代表将菜品明细以字符串的形式进行返回,不使用List集合,因此涉及到查询菜品明细并转换成字符串对象
业务逻辑:分页查询,支持模糊搜索
/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
List<OrderVO> orderVOList = getOrderVOList(page);
return new PageResult(page.getTotal(), orderVOList);
}
private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();
List<Orders> ordersList = page.getResult();
//判断订单列表是否为空
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
//将菜品明细转换成String类型
String orderDishes = getOrderDishesStr(orders);
// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}
/**
* 根据订单id获取菜品信息字符串
*
* @param orders
* @return
*/
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());
// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}
拒单
业务逻辑:修改订单状态为已取消,只有处于待接单状态才能拒单,知名拒单原因,如果用户已经付款,需要执行退款操作
/**
* 拒单
*
* @param ordersRejectionDTO
*/
@Override
public void reject(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
//处理业务异常--只有"待接单"状态可以执行拒单操作
Orders order = orderMapper.getByOrderId(ordersRejectionDTO.getId());
if (order == null || order.getStatus() != 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//如果已经完成支付则进行退款
if (order.getPayStatus() == 1) {
String refund = weChatPayUtil.refund(order.getNumber(), order.getNumber(), new BigDecimal(0.01), new BigDecimal(0.01));
log.info("申请退款:{}",refund);
}
//设置订单状态为已取消并设置拒单原因
Orders orderupdate = Orders.builder()
.id(ordersRejectionDTO.getId())
.status(Orders.CANCELLED)
.rejectionReason(ordersRejectionDTO.getRejectionReason())
.cancelTime(LocalDateTime.now())
.build();
//更新订单状态
orderMapper.update(orderupdate);
}
Spring task
spring 任务调度工具,按照约定时间自动执行某个代码逻辑
使用步骤
订单状态处理
代码
/**
* 处理用户支付超时任务
*/
@Scheduled(cron = "0 * * * * ?") //每分钟输出一次
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
//定义当前时间往前推15分钟
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
//查询当前状态为待支付,并且下单时间小于当前时间往前推15分钟的订单
List<Orders> orders = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT,time);
if (orders.size()>0 && orders != null) {
//自动取消超时订单
for (Orders order : orders) {
//修改订单信息
order.setCancelReason("用户支付超时");
order.setCancelTime(LocalDateTime.now());
order.setStatus(Orders.CANCELLED);
orderMapper.update(order);
}
}
}
/**
* 处理一直处于派送中状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("定时处理一直处于派送状态的订单:{}", LocalDateTime.now());
//当前时间减去一天时间,相当于处理上一天当前时间的订单
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> orders = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS,time);
if (orders.size()>0 && orders != null) {
//自动取消超时订单
for (Orders order : orders) {
//修改订单信息
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
}
}
}
WebSocket
websocket基础
websocket的使用步骤
客户端:
① 前端网页支持websocket技术并使用js实现了websocket
服务器端
② 导入websocket的maven坐标
前端
websocketServer组件和WebSocketConfiguration配置类的关系
Apache ECharts
营业额统计
在指定时间范围内查询营业额
返回数据格式
这里返回数据不能用datelist.toString,其返回的结果为[1,2,3]带有中括号,而StringUtils.join返回结果为1,2,3不带括号。
StringUtils.join为Apache Commons Lang3 库提供的一个实用方法,将数组或集合中的元素用指定分隔符连接成字符串。
销量Top10统计
业务逻辑:查询在指定时间范围内的订单,再查订单明细表中的菜品,统计菜品的总数目,并且是已完成订单的菜品明细。难点在SQL语句的编写
多表查询,分组查询
知识点补充:分组查询,聚合函数
分组查询往往与聚合函数搭配使用
Apache POI
使用POI在java程序中操作Excel等文件的读写操作
HttpServletResponse
对象在 Spring 控制器方法中作为参数传递,用于向客户端发送响应。这是通过在响应中写入生成的文件数据来实现的。使用 response.getOutputStream()
获取响应输出流,并将生成的 Excel 文件数据写入其中。
this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx")
获取当前类的类加载器,然后通过该类加载器获取项目资源目录中的文件输入流。
在 Spring Boot 项目中,getResourceAsStream
方法可以加载 src/main/resources
目录中的文件。src/main/resources
目录中的资源文件会被打包到最终的 JAR 文件中,因此可以使用类加载器加载这些文件。