软件开发整体介绍
前端搭建
在非中文目录中双击nginx.exe然后浏览器访问localhost即可
后端搭建
基础准备
导入初始文件
使用git进行版本控制
创建本地仓库和远程仓库,提交Git
连接数据库
连接数据库把资料中的文件放入运行即可
前后端联调测试
苍穹外卖项目接口文档
Nginx反向代理
前端发送的请求,是如何请求到后端服务器的?
nginx反向代理,就是将前端发送的动态请求由nginx转发到后端服务器.
使用Nginx的好处:
- 提高访问速度
- 进行负载均衡
- 保证后端服务的安全
员工管理模块
新增员工
编写新增员工功能,需要注意密码进行默认加密,通过调用常量(避免硬编码)进行设置.前端所传的数据和POJO属性差别较大时,编写DTO进行数据封装
功能测试进行前后端联调测试,之前需要获取Jwt令牌
完善需要避免用户名重复 处理异常.
通过全局异常处理类进行处理,捕获到用户名重复的异常然后进行加工返回.
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){//Duplicate entry 'lans' for key 'idx_username'String message=ex.getMessage();if(message.contains("Duplicate entry")){//创建数组 通过空格分隔成一个个对象String[] split=message.split(" ");//取出第三个元素 即usernameString username=split[2];//作为提示信息拼接String msg=username+ MessageConstant.ALREADY_EXISTS;return Result.success(msg);}else{return Result.error(MessageConstant.UNKNOWN_ERROR);}
处理异常.完善创建人id和修改人id
从携带的token令牌中解析获取其中的id然后放入ThreadLocal(客户端每一次发起的请求都是一个线程)存储空间,需要id时取出
员工分页查询
通过PageHelper插件实现分页查询的功能.
Query参数是一种在HTTP请求中用于传递额外信息的参数类型,它具有直观易懂、便于传递简单参数等特点。
- Body Parameters通常用于POST、PUT等请求中,以传递复杂的数据结构(如JSON、XML等)。
- Query Parameters则更适合传递简单的键值对参数。
请求参数是Query,他直接拼接在URL后面,通过DTO进行封装成EmployeePageQueryDTO,三个参数name(不一定有),page,pageSize.接收的参数类型就是EmployeePageQueryDTO.
name就需要用到模糊查询+分页查询的方式,需要用到动态SQL,不用注解来对数据库来操作,而是用到xml文件,另外返回结果是总记录数和当前页面数据的集合,通过再次封装一个返回类来作为返回值.
@Data //get set
@AllArgsConstructor //有参构造
@NoArgsConstructor //无参构造
public class PageResult implements Serializable {private long total; //总记录数private List records; //当前页数据集合
}
那么Controller层返回的就是返回的是一个包含 PageResult
类型的 Result
对象/
service层利用PageHelper实现分页查询,只需要开启分页查询并传入page和pageSize两个参数即可.调用mapper层方法对数据库进行操作.剩下的就是对返回值的处理.和编写SQL语句.由于是动态查询,普通注解无法满足要求,通过xml文件进行配置动态sql.
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {//开始分页查询 利用PageHelperPageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());Page<Employee> page=employeeMapper.pageQuery(employeePageQueryDTO);long total = page.getTotal();List<Employee> result = page.getResult();return new PageResult(total,result);
}
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper"><select id="pageQuery" resultType="com.sky.entity.Employee">select * from employee<where><if test="name !=null and name != ''">and name like concat('%',#{name},'%')</if></where>order by create_time desc</select>
</mapper>
前后端联调发现,最后操作时间格式不对,需进行代码完善
启用禁用员工账号
根据前端传过来的用户id修改用户账号状态.
采用@PostMapping("/status/{status}")和@PathVariable Integer status动态接收前端传入一个status
编写动态SQL update 可后续编辑员工再次使用该方法.
编辑员工
先根据id查询用户信息,用户点击修改按钮时执行此功能,然后进行修改.通过@RequestBody接收请求体中的数据通过反序列化封装在EmployeeDTO中进行操作
分类管理
基本与员工管理逻辑相同,不在赘述
菜品管理
公共字段自动填充
每次向这些表中插入字段的时候每次都要编写相同的代码,这样比较冗余而且后期不方便维护.
通过切面来统一进行处理公共字段,进行赋值.
首先进行自定义注解@AutoFill方便标识哪些方法需要进行自动字段填充.即insert和update方法.可通过枚举类来固定数据库操作类型
/*** @author 刘宇* 自定义注解,用于标识某个方法的功能字段需要进行自动填充*/
@Target(ElementType.METHOD) //该注解用在方法上
@Retention(RetentionPolicy.RUNTIME) //运行时生效
public @interface AutoFill {//数据库操作类型:UPDATE INSERTOperationType value();
}
然后定义一个切面类,通过拦截执行加入了该注解的方法,对拦截下的update和insert进行增强,实现自动填充字段
package com.sky.aspect;import .../*** @author 刘宇* 自定义切面,实现公共字段自动填充*/
@Component
@Aspect
@Slf4j
public class AutoFillAspect {/*切入点对execution 这个包进行和添加了该注解的进行自动填充字段拦截*/@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;}Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId= BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射赋值if(operationType==OperationType.INSERT){try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);Method setUpdateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);Method setCreateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);//赋值setCreateTime.invoke(entity,now);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);setCreateUser.invoke(entity,currentId);} catch (Exception e) {throw new RuntimeException(e);}}else if(operationType==OperationType.UPDATE){try {Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {throw new RuntimeException(e);}}}
}
新增菜品
新增菜品需要加入菜品图片,这就需要一个上传文件的功能.
需要使用到阿里云服务
然后新增菜品需要对两张表进行操作,即口味表和菜品表,
SpringBoot新增菜品模块开发(事务管理+批量插入+主键回填)
菜品分页
主要涉及到多表查询 因为每道菜有他自己的口味,但口味表和菜品表不在一个表中,故需要用到多表查询,而且一个菜品可能有多种口味也可以不设置口味,故还需要用到外连接去查所有的菜品表.然后会有查询条件,需要用到动态SQL,最后可以排个序
<select id="pageQuery" resultType="com.sky.vo.DishVO">select d.*,c.name as categoryName from dish as d left outer join category as c on d.category_id=c.id<where><if test="name != null">and d.name like concat('%',#{name},'%')</if><if test="categoryId != null">and d.category_id=#{categoryId}</if><if test="status != null">and d.status = #{status}</if></where>order by d.create_time desc
</select>
删除菜品
涉及到多表操作,需要进入一个事务注解@Transactional//事务的一致性来保证事务的一致性 不能删除的可以通过异常抛出,定义自定义异常然后把常量放进去抛出
用户可批量或单个删除菜品.注意该菜品1.不能是在售状态 2.在售套餐中不能包含该菜品 3.该菜品关联口味也要删除.
业务层中需要进行如上判断,不能是在售可以根据前端传来的id进行判断.套餐中是否存在可通过查询表中是否有对应数据,不为空就说明关联了套餐.查询套餐需要用到一个动态SQL,通过foreach循环解析出所有需要删除菜品的id,然后进行查询是否在套餐表中,口味删除通过菜品id即可
/*** 菜品删除* @param ids*/
@Override
public void deleteById(List<Long> ids) {//判断当前菜品是否能够删除?是否正在起售中for(Long id:ids){Dish dish=dishMapper.getById(id);if(dish.getStatus()== StatusConstant.ENABLE){//起售中不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}//是否套餐关联了List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if (setmealIds!=null&&setmealIds.size()>0){//当前菜品已被关联不能删throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}//删除菜品表中的数据for (Long id : ids) {dishMapper.deleteById(ids);//删除口味表中的数据dishFlavorMapper.deleteByDishId(id);}
}
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">select setmeal_id from setmeal_dish where dish_id in<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">#{dishId}</foreach>
</select>
代码优化:每次删除单个菜品都需要操作一次数据库,如果操作大量数据就导致性能方面的问题,把单个删除变成多个删除
<delete id="deleteByIds">delete from dish where id in<foreach collection="ids" open="(" close=")" item="id" separator=",">#{id}</foreach>
</delete>
菜品的同理
<delete id="deleteByDishIds">delete from dish_flavor where dish_id in<foreach collection="dishIds" separator="," item="dishId" open="(" close=")">#{dishId}</foreach>
</delete>
修改菜品
分为四个接口,先根据id来查询数据进行回显操作,根据类型查询菜品分类(用于修改分类),然后文件上传,修改菜品
@Override
public void updateWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO,dish);//修改基本信息dishMapper.update(dish);//删除所有的口味数据dishFlavorMapper.deleteByDishId(dishDTO.getId());//重新插入口味数据List<DishFlavor>flavors=dishDTO.getFlavors();if(flavors!=null&&flavors.size()>0){flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishDTO.getId());});dishFlavorMapper.insertBatch(flavors);}
}
店铺营业状态设置
营业状态数据的存储方式:基于Redis的字符串来进行存储.
1表示营业 0表示打样
管理端和用户端都应该能查询到店铺的营业状态,但用户端不能设置营业状态.
两者的请求路径不同.控制器名称若设置相同可以通过@RestController("adminShopController")和@RestController("userShopController")来区分
基于Redis就没有只有一层了即控制层
套餐管理
新增套餐
首先需要编写查询套餐分类的接口.
添加菜品时,结合产品原型来看,根据用户是否进行搜索和是否进行选择菜品分类来动态编写SQL.其中name部分需要进行模糊查询
由于文件上传部分已经完成,只需编写一个新增套餐的方法.
新增套餐时需要注意,要保证套餐和菜品的关联关系.
通过SQL自己生成的套餐id进行关联
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">insert into setmeal(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
</insert>
useGeneratedKeys
:
-
- 这个属性用于指示MyBatis是否应该使用JDBC的getGeneratedKeys方法来获取数据库自动生成的主键值(例如,自增主键)。
- 当设置为
true
时,MyBatis会在执行插入操作后,通过JDBC的getGeneratedKeys方法获取数据库生成的主键值,并将其赋值给指定的属性。
keyProperty
:
-
- 这个属性用于指定哪个属性应该接收由数据库生成的主键值。
- 通常,这个属性应该对应你的Java对象(在这个例子中是
Setmeal
对象)中用于存储主键的字段名。 - 当
useGeneratedKeys
设置为true
时,MyBatis会将获取到的主键值设置到这个指定的属性中。
套餐分页查询
分页查询 连表查询 需要用到左外连接和动态SQL
删除套餐
起售中的套餐不能删除
修改套餐
查询套餐
修改:删除原有套餐 新增套餐 删除原有关联关系 新增关联关系
套餐起售停售
修改状态即可.注意起售套餐时,套餐中不能存在停售的菜品
商品浏览
查询菜品
根据分类id查询菜品
查询套餐
查询套餐相关联的菜品
缓存
缓存菜品
用户端小程序展示的菜品数据都是通过查询数据库获得的,当用户端访问量较大时,数据库访问压力也随之增大.而Redis数据库是通过内存来保存数据的,查询数据库本质上时磁盘IO操作,内存操作相对于磁盘操作性能高很多,可以通过Redis来缓存菜品数据,减少数据库查询操作.
缓存逻辑:
- 每个分类下的菜品保存一份缓存数据
- 数据库中的菜品数据有变更时清理缓存数据
改造查询方法,具体实现
/*** 根据分类id查询菜品* 利用Redis缓存数据* @param categoryId* @return*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {//构建Redis中的key,规则 dish_分类idString key="dish_"+categoryId;//查询Redis中是否存在菜品数据List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);if (list != null && list.size() > 0) {//存在 直接返回 无需查询数据库return Result.success(list);}//不存在 查询数据库Dish dish = new Dish();dish.setCategoryId(categoryId);dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品list = dishService.listWithFlavor(dish);redisTemplate.opsForValue().set(key,list);return Result.success(list);
}
清理缓存数据
修改数据库后,要及时清理缓存,保证数据一致.
包括:起售停售菜品,修改菜品,删除操作菜品,新建菜品
通过Spring Cache框架 用注解进行缓存操作
@CacheEvict(cacheNames = "setmealCache",allEntries=true)清除名为setmealCache的缓存中的所有内容
@CacheEvict(cacheNames="setmealCache",key = "#setmealDTO.categoryId")精确根据传入的 setmealDTO
对象的 categoryId
属性值,从 setmealCache
缓存中移除对应的条目。
@Cacheable(cacheNames = "setmealCache",key = "#categoryId")
如果缓存 setmealCache
中已经存在以 categoryId
为键的数据,则直接返回该数据,否则执行该方法并将结果存入 setmealCache
缓存中,键为 categoryId
。
添加购物车
创建购物车表
用户端发送的请求会携带token令牌,拦截器中对令牌进行解析,并获得用户id,可通过ThreadLocal.getCurrentId获得userId.
前端所传过来的DTO包含三个动态参数 dishId setmealId dishFlavor
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {ShoppingCart shoppingCart = new ShoppingCart();BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);Long userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);//动态SQL对三个字段进行//判断当前加入到购物车中的商品是否已经存在了if (list != null && list.size() > 0) {ShoppingCart shoppingCart1 = list.get(0);shoppingCart1.setNumber(shoppingCart.getNumber() + 1);//updateshoppingCartMapper.updateNumberById(shoppingCart1);} else {//不存在//菜品Long dishId = shoppingCartDTO.getDishId();if (dishId != null && dishId > 0) {Dish dish = dishMapper.getById(dishId);shoppingCart.setName(dish.getName());shoppingCart.setImage(dish.getImage());shoppingCart.setAmount(dish.getPrice());} else {//dishId为null,setmealId就一定不为null 套餐Long setmealId = shoppingCartDTO.getSetmealId();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);}
}
查看购物车
根据userId查询数据库即可
清空购物车
删除数据库中购物车内容,同样从ThreadLocal中获得userId
地址模块
基本的增删改查
注意设置默认地址时,把原本所有的地址都设为非默认,再把这个设为默认.
用户下单
1.需要进行业务异常处理,购物车为空,地址簿为空需要抛出相应异常
2.向订单表插入一条数据
3.向订单细节表插入n条数据
4.清空购物车
5.封装返回值
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {//处理各种业务异常 购物车数据为空 地址簿为空AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());if(addressBook == null){throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);}//1.根据用户id查询购物车数据ShoppingCart shoppingCart =new ShoppingCart();Long userId = BaseContext.getCurrentId();shoppingCart.setUserId(userId);List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);if(list==null||list.size()==0){throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);}//2.向订单表发送插入1条数据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.setUserId(userId);orders.setPhone(addressBook.getPhone());orders.setConsignee(addressBook.getConsignee());//收货人orderMapper.insert(orders);//返回主键值订单id给订单细节表//3.向订单细节表插入n条数据List<OrderDetail> orderDetailList = new ArrayList<>();for (ShoppingCart cart : list) {OrderDetail orderDetail = new OrderDetail();BeanUtils.copyProperties(cart,orderDetail);orderDetail.setOrderId(orders.getId());orderDetailList.add(orderDetail);}orderDetailMapper.insertBatch(orderDetailList);//4.清空当前用户的购物车数据shoppingCartMapper.deleteById(userId);//5.封装VO返回结果OrderSubmitVO orderSubmitVO = new OrderSubmitVO();orderSubmitVO.setId(orders.getId());orderSubmitVO.setOrderTime(orders.getOrderTime());orderSubmitVO.setOrderAmount(orders.getAmount());orderSubmitVO.setOrderNumber(orders.getNumber());return orderSubmitVO;
}
订单支付
???
C端订单接口
查询历史订单
是一个分页动态查询,根据前端动态传来的status等条件进行动态查询,由于会涉及到时间,如订单创建时间和订单结束时间来查询,在xml配置映射文件时会用到<>,注意转义字符的使用. >大于 < 小于
<if test="beginTime !=null">and begin_time>=#{beginTime}
</if>
<if test="endTime!=null">and end_time<=#{endTime}
</if>
查看订单详细
通过订单号查询订单详细然后将数据封装为VO返回
public OrderVO details(Long id) {//根据订单id查询订单ordersOrders orders = orderMapper.getById(id);//根据订单id查询订单详细信息List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(id);OrderVO orderVO = new OrderVO();BeanUtils.copyProperties(orders,orderVO);orderVO.setOrderDetailList(orderDetails);return orderVO;
}
用户取消订单
不能取消订单的情况:订单不能为空,订单状态必须为1待付款 或 2待接单
待付款情况下需要进行退款 然后更新订单状态,取消原因和取消时间
public void userCancelById(Long id) throws Exception {// 根据id查询订单Orders ordersDB = orderMapper.getById(id);// 校验订单是否存在if (ordersDB == null) {throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);}//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消if (ordersDB.getStatus() > 2) {throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}Orders orders = new Orders();orders.setId(ordersDB.getId());// 订单处于待接单状态下取消,需要进行退款if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {//调用微信支付退款接口weChatPayUtil.refund(ordersDB.getNumber(), //商户订单号ordersDB.getNumber(), //商户退款单号new BigDecimal(0.01),//退款金额,单位 元new BigDecimal(0.01));//原订单金额//支付状态修改为 退款orders.setPayStatus(Orders.REFUND);}// 更新订单状态、取消原因、取消时间orders.setStatus(Orders.CANCELLED);orders.setCancelReason("用户取消");orders.setCancelTime(LocalDateTime.now());orderMapper.update(orders);
}
再来一单
通过订单id查询订单详细信息,然后将订单的详细信息转化为购物车对象,将菜品信息复制进去然后将购物车对象批量添加到数据库中去
订单管理
搜索订单
管理端通过开始时间结束时间等条件进行查询,返回的数据包括
查看订单详细
根据订单id来查看订单详细信息的集合返回
统计各个订单数量
用到统计函数,sql:select count(*) from orders where status=#{status}
接单
将订单id和修改的订单状态封装进Orders然后修改即可
拒单
根据id查询订单,只有订单状态为待接单才能拒单,若用户已支付需要进行退款.需要设置退款原因,修改订单状态,更新取消时间
派送订单
根据id查询订单状态,处于已接单状态才可进行下步操作,修改状态 更新数据库
完成订单
根据id查询订单状态,处于派送中状态才可进行下步操作,修改状态 更新数据库
定时处理
用户下单后一直处于待支付状态,要进行一个定时处理
用户收到货后商家没有点击完成按钮,订单一直处于派送中,要进行一个定时处理
package com.sky.task;import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.List;/*** @author 刘宇*/
@Component
@Slf4j
public class OrderTask {@Autowiredprivate OrderMapper orderMapper;/*** 处理超时订单*/@Scheduled(cron="0 * * * * ? ")public void processTimeoutOrder(){log.info("定时处理超时订单:{}", LocalDateTime.now());// 待付款状态 and orderTime < (当前时间-15min)LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(-15);List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, localDateTime);if(ordersList!=null&&ordersList.size()>0){for(Orders order:ordersList){order.setStatus(Orders.CANCELLED);order.setCancelReason("订单超时,自动取消");order.setCancelTime(LocalDateTime.now());orderMapper.update(order);}}}/*** 处理一直处于派送中的订单*/@Scheduled(cron="0 0 1 * * *")public void processDeliveryOrder(){log.info("处理一直处于派送中的订单:{}", LocalDateTime.now());//select * from orders where status=#{status}LocalDateTime time=LocalDateTime.now().plusMinutes(-60);List<Orders> orders = orderMapper.getDeliverying(Orders.DELIVERY_IN_PROGRESS,time);if(orders!=null&&orders.size()>0){for(Orders order:orders){order.setStatus(Orders.COMPLETED);orderMapper.update(order);}}}
}
来单提醒
用户下单并支付后,系统需要通知商家,语音播报,弹出提示框.
// 通过websocket向客户端浏览器推送消息type orderId contentMap map =new HashMap<>();map.put("type",1);//1表示来单提醒map.put("orderId",ordersDB.getId());map.put("content","订单号"+outTradeNo);//将map集合转为JSON字符串String jsonString = JSONObject.toJSONString(map);webSocketServer.sendToAllClient(jsonString);
用户催单
类似
@Override
public void reminder(Long id) {//查看订单是否存在Orders orders=orderMapper.getById(id);if(orders==null){throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}Map map = new HashMap();map.put("type",2);//2表示客户催单map.put("orderId",id);map.put("content","订单号"+orders.getNumber());String json = JSONObject.toJSONString(map);webSocketServer.sendToAllClient(json);
}
数据统计
营业额统计
合计订单状态为已完成的订单金额.基于折线图展示营业额数据.根据时间选择区间,展示每天的营业额数据.
提交给前端的为一个日期和营业额的字符串集合,并以逗号分隔
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03private String dateList;//营业额,以逗号分隔,例如:406.0,1520.0,75.0private String turnoverList;}
前端传入一个开始和结束日期(近七日...近半月),需要将开始到结束日期所有日子获得并存放到集合中,然后通过查询订单表获取营业额总和.
/*** 指定时间区间的营业额数据* @param startDate* @param endDate* @return*/
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate startDate, LocalDate endDate) {//计算日期//用于存放从begin到end范围内的每天的日期List<LocalDate> dateList = new ArrayList<>();dateList.add(startDate);while(!startDate.equals(endDate)){//计算指定日期后一天的日期startDate = startDate.plusDays(1);dateList.add(startDate);}//获得营业额数据//存放每天的营业额List<Double> turnoverList = new ArrayList<>();for (LocalDate localDate : dateList) {//select sum(amount) from orders where order_time>begin and order_time<end_time and status=5//开始时间 编写为年月日时分秒格式LocalDateTime begin = LocalDateTime.of(localDate, LocalTime.MIN);LocalDateTime end=LocalDateTime.of(localDate,LocalTime.MAX);Map map = new HashMap();map.put("begin",begin);map.put("end",end);map.put("status", Orders.COMPLETED);Double turnover = orderMapper.sumByMap(map);turnover=turnover==null?0.0:turnover;turnoverList.add(turnover);}TurnoverReportVO turnoverReportVO = new TurnoverReportVO();turnoverReportVO.setDateList(StringUtils.join(dateList,","));turnoverReportVO.setTurnoverList(StringUtils.join(turnoverList,","));return turnoverReportVO;
}
用户统计
统计用户的数量,需要统计总用户量和新增用户量.
总用户量满足 用户账号创建时间在这一天前即可
新用户要求 用户创建时间在这一天中
@Override
public UserReportVO getUserStatistics(LocalDate startDate, LocalDate endDate) {List<LocalDate> dateList = new ArrayList<>();dateList.add(startDate);while(!startDate.equals(endDate)){startDate = startDate.plusDays(1);dateList.add(startDate);}//存放每天的新增的用户数量 select count(id)from user where create_time <? and create_time>?List<Integer> newUserList = new ArrayList<>();//存放每天总的用户数量 select count(id) from user where create_time < ?List<Integer> totalUserList = new ArrayList<>();//获得当前日期的初始和结束for (LocalDate localDate : dateList) {LocalDateTime begin=LocalDateTime.of(localDate,LocalTime.MIN);LocalDateTime end=LocalDateTime.of(localDate,LocalTime.MAX);Map map =new HashMap();map.put("end",end);Integer totalUser = userMapper.countByMap(map);//总用户数量map.put("begin",begin);Integer newUser=userMapper.countByMap(map);totalUser=totalUser==null?0:totalUser;totalUserList.add(totalUser);newUser=newUser==null?0:newUser;newUserList.add(newUser);}UserReportVO userReportVO = new UserReportVO();userReportVO.setDateList(StringUtils.join(dateList,","));userReportVO.setTotalUserList(StringUtils.join(totalUserList,","));userReportVO.setNewUserList(StringUtils.join(newUserList,","));return userReportVO;
}
订单统计
与前面类似
销量排名统计
通过柱形图展示销量排名,包括菜品和套餐
通过连表查询进行查询数据并统计排名
select od.name,sum(od.number) as number from order_detail od,orders o
where od.id=o.id and o.status = 5 and order_time>? and order_time <?
group by od.name order by number desc limit 0,10
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {LocalDateTime beginTime = LocalDateTime.of(begin,LocalTime.MIN);LocalDateTime endTime = LocalDateTime.of(end,LocalTime.MAX);List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime,endTime);List<String>names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());String nameList = StringUtils.join(names,",");List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());String numberList = StringUtils.join(numbers, ",");return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();
}
工作台
Excel报表
微信小程序开发
HttpClient
导入阿里云的start包的时候已经引入了HttpClient的jar包了,无需手动再导入
GET请求
@Test
public void testGET() throws IOException {//创建HttpClient对象CloseableHttpClient httpClient = HttpClients.createDefault();//创建请求对象HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");//发送请求,接收返回结果CloseableHttpResponse response = httpClient.execute(httpGet);//获得服务器返回的状态码int statusCode=response.getStatusLine().getStatusCode();System.out.println("返回给服务端的状态码:"+statusCode);HttpEntity entity=response.getEntity();String body= EntityUtils.toString(entity);System.out.println("服务端返回的数据为:"+body);//关闭数据response.close();httpClient.close();
}
POST请求
@Test
public void testPost() throws IOException {//创建HttpClient对象CloseableHttpClient httpClient = HttpClients.createDefault();//创建请求对象HttpPost httpPost=new HttpPost("http://localhost:8080/admin/employee/login");JSONObject jsonObject=new JSONObject();jsonObject.put("username","admin");jsonObject.put("password","123456");StringEntity entity=new StringEntity(jsonObject.toString());//指定请求编码方式entity.setContentEncoding("UTF-8");//数据格式entity.setContentType("application/json");httpPost.setEntity(entity);//发送请求CloseableHttpResponse response=httpClient.execute(httpPost);//解析返回结果int statusCode=response.getStatusLine().getStatusCode();System.out.println("响应码为:"+statusCode);HttpEntity entity1=response.getEntity();String body=EntityUtils.toString(entity1);System.out.println("相应数据为:"+body);//关闭资源response.close();httpClient.close();
}
微信小程序开发
首先注册小程序,通过开发者工具完成开发
微信登录流程
需求:基于微信登录实现小程序的登录功能 如果是新用户就需要自动注册
小程序段发送请求并携带授权码,通过授权码调用微信的接口服务,返回令牌包含用户唯一标识.
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {log.info("微信用户登录:{}", userLoginDTO);User user=userService.weLogin(userLoginDTO);//为微信用户生成Jwt令牌Map<String,Object>claims=new HashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());//读取配置文件 调用方法生成Jwt令牌String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);//封装返回值UserLoginVO userLoginVO=new UserLoginVO();userLoginVO.setToken(token);userLoginVO.setId(user.getId());userLoginVO.setOpenid(user.getOpenid());//前端携带过来的idreturn Result.success(userLoginVO);
}
然后再业务层通过该方法获取用户openid
private String getOpenid(String code){//调用微信接口服务获得当前用户的openIdMap<String,String>map=new HashMap<>();map.put("appid", weChatProperties.getAppid());map.put("secret",weChatProperties.getSecret());map.put("js_code",code);map.put("grant_type","authorization_code");String json=HttpClientUtil.doGet(WX_LOGIN,map);JSONObject jsonObject = JSON.parseObject(json);String openid=jsonObject.getString("openid");return openid;
}
商品浏览功能代码
所学
Apache POI
应用场景:
- 银行网银系统交易到处交易明细
- 各种业务系统到处Excel报表
- 批量到处业务数据
Apache ECharts-数据可视化技术
WebSocket协议
应用场景:
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票基金报价时事更新
任务调度工具 Spring Task
Spring Task是Spring框架提供的一个轻量级的任务调度工具,它允许开发者在Spring应用中方便地实现定时任务、异步任务等功能,无需引入额外的复杂的任务调度框架
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格隔开,每个域代表一个含义
每个域的含义分别为:秒,分钟,小时,日,月,周,年(可选)..日期可能和星期冲突,只有写一个,另一个写?
可访问在线Cron表达式生成器来在线生成cron表达式
package com.sky.task;import .../*** @author 刘宇*/
@Component
@Slf4j
public class MyTask {/*** 定时任务*/@Scheduled(cron="0/5 * * * * ?")public void executeTask(){log.info("定时任务开始执行:{}",new Date());}
}
缓存框架 Spring Cache
在Spring框架中,缓存抽象提供了一种简化缓存使用的机制,使得开发者能够更专注于业务逻辑,而不用过多关注缓存的具体实现。你提到的几个注解(@CacheEvict
和 @Cacheable
)是Spring Cache提供的关键注解,用于管理缓存中的数据。
@CacheEvict
:
-
- 用于从缓存中移除数据。
cacheNames
或value
属性指定了要操作的缓存的名称。allEntries
属性为true
时,表示清除缓存中的所有条目。key
属性用于指定要移除的具体缓存项的键。- 清除所有缓存项:
java复制代码@CacheEvict(cacheNames = "setmealCache", allEntries = true)
这行代码会清除名为 setmealCache
的缓存中的所有条目。
-
- 精确清理缓存项:
java复制代码@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")
这行代码会根据传入的 setmealDTO
对象的 categoryId
属性值,从 setmealCache
缓存中移除对应的条目。
@Cacheable
:
-
- 用于标记一个方法的返回值是可缓存的。如果缓存中存在指定键的数据,则直接返回缓存中的数据,否则执行方法并将结果存入缓存。
cacheNames
或value
属性指定了要使用的缓存的名称。key
属性用于指定缓存项的键。- 缓存方法返回值:
java复制代码@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
这行代码表示,如果缓存 setmealCache
中已经存在以 categoryId
为键的数据,则直接返回该数据,否则执行该方法并将结果存入 setmealCache
缓存中,键为 categoryId
。
Redis
Redis
利用Redis进行缓存
Redis数据库是通过内存来保存数据的,查询数据库本质上时磁盘IO操作,内存操作相对于磁盘操作性能高很多,可以通过Redis来缓存菜品数据,减少数据库查询操作.
将第一次查询数据库所得到的数据,利用合适的方式存入Redis即可.
/*** 根据分类id查询菜品* 利用Redis缓存数据* @param categoryId* @return*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {//构建Redis中的key,规则 dish_分类idString key="dish_"+categoryId;//查询Redis中是否存在菜品数据List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);if (list != null && list.size() > 0) {//存在 直接返回 无需查询数据库return Result.success(list);}//不存在 查询数据库Dish dish = new Dish();dish.setCategoryId(categoryId);dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品list = dishService.listWithFlavor(dish);redisTemplate.opsForValue().set(key,list);return Result.success(list);
}
常用方法??
BeanUtils.copyProperties(a,b);将a中的属性拷贝到b对象中
PageHelper.startPage(Page,PageSize);开启分页查询
事务
在启动类上加@EnableTransactionManagement //开启注解方式的事务管理
然后就可以通过注解设置
lombok
PageHelper
阿里云服务
上传文件
把存储的图片上传到云服务器,数据库存储的是该图片的访问地址.需要通过Maven加入阿里云依赖
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>${aliyun.sdk.oss}</version>
</dependency>
然后通过java代码实现,但为了解耦,采用一种更优雅的方式.即通过将Access Key ID和Access Key Secret等数据配置到配置文件中.
然后通过@ConfigurationProperties(prefix="sky.alioss")注解将配置文件中的属性绑定到java对象中
package com.sky.properties;import ...@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;}
,然后把具体实现的代码放入工具类中
package com.sky.utils;import ...@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;/*** 文件上传** @param bytes* @param objectName* @return*/public String upload(byte[] bytes, String objectName) {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);try {// 创建PutObject请求。ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}//文件访问路径规则 https://BucketName.Endpoint/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}
}
然后通过配置类初始化工具类对象放入容器中进行统一管理.
package com.sky.config;import .../*** @author 刘宇* 这是一个配置类,用于初始化AliOssUtil对象*/
@Configuration
@Slf4j
public class OssConfiguration {@Beanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeyId(),aliOssProperties.getBucketName());}
}
编写接口的时候,同样需注意上传的文件名需要进行UUID处理防止重名
//原式文件名
String originalFileName=file.getOriginalFilename();
//截取文件名的扩展名
String extension=originalFileName.substring(originalFileName.lastIndexOf("."));
//通过UUID防止重名
String objectName= UUID.randomUUID().toString()+extension;
//返回文件的请求路径
String filePath=aliOssUtil.upload(file.getBytes(),objectName);
return Result.success(filePath);
处理请求参数
1. @PathVariable
用途:用于从 URL 路径中提取变量。
适用场景:当您需要从 URL 路径中动态获取某些值时,例如获取资源的 ID 或其他标识符。
示例:
java复制代码@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) { // 根据 id 查找用户 User user = userService.findById(id); return ResponseEntity.ok(user);
}
在这个例子中,{id}
是一个路径变量,@PathVariable Long id
用于将其值提取为方法参数 id
。
2. @RequestBody
用途:用于将请求体(通常是 JSON 或 XML)中的数据反序列化为 Java 对象。
适用场景:当您需要从客户端接收复杂的对象或数据结构时,例如创建或更新资源时的表单数据。
示例:
java复制代码@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) { // 创建新用户 User createdUser = userService.create(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
在这个例子中,请求体中的数据被反序列化为 User
对象,并作为方法参数 user
传递。
3. @RequestParam
用途:用于从请求参数(查询字符串)中获取数据。
适用场景:当您需要从 URL 的查询字符串中获取简单的数据(如字符串、数字等)时。
示例:
java复制代码@GetMapping("/users")
public ResponseEntity<List<User>> getUsersByPage( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { // 根据分页参数获取用户列表 Page<User> userPage = userService.findAll(PageRequest.of(page, size)); return ResponseEntity.ok(userPage.getContent());
}
在这个例子中,page
和 size
是查询字符串中的参数,@RequestParam
注解用于将它们提取为方法参数。
总结
@PathVariable
:用于从 URL 路径中提取变量,通常用于获取资源的 ID 或其他标识符。@RequestBody
:用于将请求体中的数据反序列化为 Java 对象,通常用于处理复杂的表单数据。@RequestParam
:用于从查询字符串中获取数据,通常用于处理简单的请求参数。
动态查询
mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.sky.entityconfiguration:#开启驼峰命名map-underscore-to-camel-case: true
这段配置文件是用于配置MyBatis框架的,通常放在Spring Boot项目的application.yml
或application.properties
文件中。MyBatis是一个支持普通SQL查询、存储过程和高级映射的持久层框架。它消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plain Old Java Objects,简单的Java对象)映射成数据库中的记录。
下面是对这段配置文件的详细解释:
- mapper-locations:
-
classpath:mapper/*.xml
指定了MyBatis的mapper文件的位置。这些mapper文件包含了SQL语句和映射规则,用于将数据库查询结果映射到Java对象中。这里的配置表示mapper文件位于项目的classpath
下的mapper
目录中,且文件扩展名为.xml
。
- type-aliases-package:
-
com.sky.entity
指定了MyBatis的类型别名包。这意味着MyBatis会扫描这个包下的所有Java类,并将它们的简单类名(首字母小写)注册为别名。例如,如果有一个名为User
的类在com.sky.entity
包下,那么你可以在MyBatis的mapper文件中使用user
作为这个类的别名。
- configuration:
-
- 这是MyBatis的核心配置部分,用于设置MyBatis的行为。
- map-underscore-to-camel-case:
-
-
true
表示开启驼峰命名自动映射。在数据库设计中,很多表字段使用下划线(如user_name
)来分隔单词,而在Java的POJO中,通常使用驼峰命名法(如userName
)。开启这个选项后,MyBatis会自动将数据库中的下划线命名转换为Java对象中的驼峰命名,从而避免了手动编写大量的映射规则。
-
总的来说,这段配置文件通过指定mapper文件的位置、类型别名的包以及MyBatis的核心配置(如驼峰命名转换),为MyBatis的使用提供了必要的配置信息。这使得开发者能够更加方便地使用MyBatis进行数据库操作,而不需要关心底层的JDBC代码和复杂的映射规则。
ThreadLocal
API文档管理工具 YApi和Swagger
YApi
- 定义:YApi是一个现代化的、快速、免费且开源的API文档管理平台。它旨在提供更高效、更友好的接口管理服务,支持团队协作,帮助团队更好地管理、分享和使用API文档。
- 功能:
-
- 接口管理:支持接口的创建、修改、删除,以及版本控制功能。
- 接口调试:提供在线调试接口的功能,方便查看请求和响应的详细信息。
- 接口测试:提供接口测试功能,支持断言、参数化等测试技术。
- 文档生成:自动生成接口文档,支持Markdown格式,方便团队协作。
- 团队协作:支持多用户协作,共同管理和维护API文档。
- 特点:YApi提供了一个可视化的界面,使得接口的管理和使用变得更加直观和便捷。此外,它还支持从Swagger导入接口数据,方便用户在不同工具之间进行迁移。
Swagger
Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
- 定义:Swagger是一个用于设计、构建和文档化RESTful API的工具集。它提供了一系列工具,如Swagger Editor(用于编辑Swagger规范)、Swagger UI(用于可视化API文档)和Swagger Codegen(用于根据API定义生成客户端库、server stubs等)。
- 功能:
-
- API设计:支持定义API的结构、参数、请求和响应格式等信息,帮助开发者更轻松地创建和管理API。
- 文档生成:根据API的定义自动生成易于理解的文档,支持多种格式的输出。
- 在线调试:提供在线接口调试页面,方便开发者进行接口测试和调试。
- 特点:Swagger通过定义API的规范,使得API的设计、构建和文档化变得更加标准化和自动化。它还提供了一套可视化的工具,使得API的查看、测试和调试变得更加方便。此外,Swagger与多种编程语言和框架都具有良好的兼容性,使得它在实际开发中得到了广泛的应用。
应用knife4j
使用swagger定义接口及接口相关信息,可以生成接口文档以及在线接口调试界面
Knife4j是为java MVC框架集成Swagger生成API文档的增强解决方案
1.导入knife4j的Maven坐标
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>${knife4j}</version>
</dependency>
2.在配置类中加入knife4j相关配置
package com.sky.config;import .../*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");}/*** 通过knife4j生成接口文档* @return*/@Beanpublic Docket docket() {ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller")).paths(PathSelectors.any()).build();return docket;}/*** 设置静态资源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}
}
3.设置静态资源映射,否则接口文档页面无法访问
YApi是设计阶段使用的工具,管理和维护接口
常用注解
TODO
在IDEA中设置TODO如// TODO 后期需要进行md5加密,然后再进行比对
就可以快捷的查找需要完善的功能
异常处理器
通过@RestControllerAdvice @ExceptionHandler注解编写异常处理类
@RestControllerAdvice
是一个方便的注解,用于定义一个全局的控制器增强器(Controller Advice)。它主要用来处理全局异常、全局数据绑定等
@ExceptionHandler
注解用于定义一个方法,该方法用于处理特定类型的异常。可以在控制器类(Controller)中单独使用,也可以在通过 @ControllerAdvice
或 @RestControllerAdvice
注解的类中全局使用。
handler包下的异常处理器,编写全局异常处理器处理异常,根据异常的类型进行处理并返回特定信息
实例
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){//Duplicate entry 'lans' for key 'idx_username'String message=ex.getMessage();if(message.contains("Duplicate entry")){//创建数组 通过空格分隔成一个个对象String[] split=message.split(" ");//取出第三个元素 即usernameString username=split[2];//作为提示信息拼接String msg=username+ MessageConstant.ALREADY_EXISTS;return Result.success(msg);}else{return Result.error(MessageConstant.UNKNOWN_ERROR);}
}
可自定义异常类然后进行统一处理
拦截器
自定义拦截器后需要再进行注册
package com.sky.interceptor;import .../*** jwt令牌校验的拦截器*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt** @param request* @param response* @param handler* @return* @throws Exception*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getUserTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);Long userId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());log.info("当前用户id:", userId);BaseContext.setCurrentId(userId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}
package com.sky.config;import ...import java.util.List;/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;@Autowiredprivate JwtTokenUserInterceptor jwtTokenUserInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");registry.addInterceptor(jwtTokenUserInterceptor).addPathPatterns("/user/**").excludePathPatterns("/user/user/login").excludePathPatterns("/user/shop/status");}/*** 通过knife4j生成管理端接口文档* @return*/@Beanpublic Docket docket1() {log.info("准备生成管理端接口文档...");ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("管理端接口").apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")).paths(PathSelectors.any()).build();return docket;}/*** 通过knife4j生成用户端接口文档* @return*/@Beanpublic Docket docket2() {log.info("准备生成用户端接口文档...");ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("用户端接口").apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller.user")).paths(PathSelectors.any()).build();return docket;}/*** 设置静态资源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {log.info("开始设置静态资源映射...");registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}/*** 扩展SpringMVC框架的消息转换器*/protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("扩展消息转换器...");//创建一个消息转换器MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据converter.setObjectMapper(new JacksonObjectMapper());//将自己的消息转换器加入到容器中converters.add(0,converter);}
}
自定义注解
Java自定义注解-CSDN博客
类用法
常量类
将所有的提示信息封装到一个常量类里面,设置一系列常量
通过常量可以避免硬编码,方便后期维护.
所有返回的常量结果都可以设置对应的常量类
返回结果类
定义一个类来作为返回后端统一的返回结果
package com.sky.result;import lombok.Data;import java.io.Serializable;/*** 后端统一返回结果* @param <T>*/
@Data
public class Result<T> implements Serializable {private Integer code; //编码:1成功,0和其它数字为失败private String msg; //错误信息private T data; //数据public static <T> Result<T> success() {Result<T> result = new Result<T>();result.code = 1;return result;}public static <T> Result<T> success(T object) {Result<T> result = new Result<T>();result.data = object;result.code = 1;return result;}public static <T> Result<T> error(String msg) {Result result = new Result();result.msg = msg;result.code = 0;return result;}}
DTO VO
前端所提交的数据和实体类中对应的属性差别比较大时,建议用DTO来封装数据
VO用于操作数据库后返回给前端所封装的数据,通常需要继承序列化接口.Serializable
配置类
自定义配置类
通过配置属性类这种方式,把配置项封装成一个java对象通过Spring注入
通过使用@ConfigurationProperties注解
@ConfigurationProperties(prefix = "sky.jwt")
是 Spring Boot 中的一个注解,用于简化配置属性的绑定。这个注解通常被用在类定义上,表示该类的一个或多个字段将会绑定到配置文件(如 application.properties
或 application.yml
)中指定的前缀下的属性上。
具体到这个注解:
@ConfigurationProperties
:这是主注解,用于启用配置属性的绑定功能。prefix = "sky.jwt"
:这个属性指定了配置文件中属性的前缀。也就是说,Spring Boot 将会查找所有以sky.jwt
开头的配置项,并将它们自动绑定到标注了这个注解的类的对应字段上。
package com.sky.properties;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {/*** 管理端员工生成jwt令牌相关配置*/private String adminSecretKey;private long adminTtl;private String adminTokenName;/*** 用户端微信用户生成jwt令牌相关配置*/private String userSecretKey;private long userTtl;private String userTokenName;}
应用配置类
自定义拦截器,消息转换器
package com.sky.config;import ...import java.util.List;/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");}/*** 通过knife4j生成接口文档* @return*/@Beanpublic Docket docket() {ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller")).paths(PathSelectors.any()).build();return docket;}/*** 设置静态资源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}/*** 扩展SpringMVC框架的消息转换器*/protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {//创建一个消息转换器MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据converter.setObjectMapper(new JacksonObjectMapper());//将自己的消息转换器加入到容器中converters.add(0,converter);}
}