苍穹外卖-后端部分

软件开发整体介绍

前端搭建

在非中文目录中双击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>
  1. useGeneratedKeys:
    • 这个属性用于指示MyBatis是否应该使用JDBC的getGeneratedKeys方法来获取数据库自动生成的主键值(例如,自增主键)。
    • 当设置为true时,MyBatis会在执行插入操作后,通过JDBC的getGeneratedKeys方法获取数据库生成的主键值,并将其赋值给指定的属性。
  1. 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配置映射文件时会用到<>,注意转义字符的使用. &gt;大于 &lt; 小于

<if test="beginTime !=null">and begin_time&gt;=#{beginTime}
</if>
<if test="endTime!=null">and end_time&lt;=#{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提供的关键注解,用于管理缓存中的数据。

  1. @CacheEvict
    • 用于从缓存中移除数据。
    • cacheNamesvalue 属性指定了要操作的缓存的名称。
    • allEntries 属性为 true 时,表示清除缓存中的所有条目。
    • key 属性用于指定要移除的具体缓存项的键。
    • 清除所有缓存项
java复制代码@CacheEvict(cacheNames = "setmealCache", allEntries = true)

这行代码会清除名为 setmealCache 的缓存中的所有条目。

    • 精确清理缓存项
java复制代码@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")

这行代码会根据传入的 setmealDTO 对象的 categoryId 属性值,从 setmealCache 缓存中移除对应的条目。

  1. @Cacheable
    • 用于标记一个方法的返回值是可缓存的。如果缓存中存在指定键的数据,则直接返回缓存中的数据,否则执行方法并将结果存入缓存。
    • cacheNamesvalue 属性指定了要使用的缓存的名称。
    • 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());  
}

在这个例子中,pagesize 是查询字符串中的参数,@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.ymlapplication.properties文件中。MyBatis是一个支持普通SQL查询、存储过程和高级映射的持久层框架。它消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plain Old Java Objects,简单的Java对象)映射成数据库中的记录。

下面是对这段配置文件的详细解释:

  1. mapper-locations:
    • classpath:mapper/*.xml 指定了MyBatis的mapper文件的位置。这些mapper文件包含了SQL语句和映射规则,用于将数据库查询结果映射到Java对象中。这里的配置表示mapper文件位于项目的classpath下的mapper目录中,且文件扩展名为.xml
  1. type-aliases-package:
    • com.sky.entity 指定了MyBatis的类型别名包。这意味着MyBatis会扫描这个包下的所有Java类,并将它们的简单类名(首字母小写)注册为别名。例如,如果有一个名为User的类在com.sky.entity包下,那么你可以在MyBatis的mapper文件中使用user作为这个类的别名。
  1. 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

  1. 定义:YApi是一个现代化的、快速、免费且开源的API文档管理平台。它旨在提供更高效、更友好的接口管理服务,支持团队协作,帮助团队更好地管理、分享和使用API文档。
  2. 功能
    • 接口管理:支持接口的创建、修改、删除,以及版本控制功能。
    • 接口调试:提供在线调试接口的功能,方便查看请求和响应的详细信息。
    • 接口测试:提供接口测试功能,支持断言、参数化等测试技术。
    • 文档生成:自动生成接口文档,支持Markdown格式,方便团队协作。
    • 团队协作:支持多用户协作,共同管理和维护API文档。
  1. 特点:YApi提供了一个可视化的界面,使得接口的管理和使用变得更加直观和便捷。此外,它还支持从Swagger导入接口数据,方便用户在不同工具之间进行迁移。

Swagger

Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

  1. 定义:Swagger是一个用于设计、构建和文档化RESTful API的工具集。它提供了一系列工具,如Swagger Editor(用于编辑Swagger规范)、Swagger UI(用于可视化API文档)和Swagger Codegen(用于根据API定义生成客户端库、server stubs等)。
  2. 功能
    • API设计:支持定义API的结构、参数、请求和响应格式等信息,帮助开发者更轻松地创建和管理API。
    • 文档生成:根据API的定义自动生成易于理解的文档,支持多种格式的输出。
    • 在线调试:提供在线接口调试页面,方便开发者进行接口测试和调试。
  1. 特点: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.propertiesapplication.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);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/61104.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

剧本杀门店预约小程序,解锁沉浸式推理体验

一、开发背景 剧本杀作为一种热门娱乐游戏&#xff0c;深受大众的欢迎&#xff0c;但随着市场的快速发展&#xff0c;竞争也在不断加大&#xff0c;对于剧本杀线下商家来说面临着发展创新。 剧本杀线下门店数量目前正在逐渐增加&#xff0c;竞争激烈&#xff0c;而门店的获客…

【WPF】Prism学习(二)

Prism Commands 1.命令&#xff08;Commanding&#xff09; 1.1. ViewModel的作用&#xff1a; ViewModel不仅提供在视图中显示或编辑的数据&#xff0c;还可能定义一个或多个用户可以执行的动作或操作。这些用户可以通过用户界面&#xff08;UI&#xff09;执行的动作或操作…

学者观察 | 元计算、人工智能和Web 3.0——山东大学教授成秀珍

导语 成秀珍教授提出元计算是在开放的零信任环境下整合算力资源打通数据壁垒构建自进化智能的新质生产力技术&#xff0c;是一种新计算范式&#xff1b;区块链是Web3.0的核心技术之一&#xff0c;有助于保障开放零信任环境下&#xff0c;用户、设备和服务间去中心化数据流通的…

学习笔记022——Ubuntu 安装 MySQL8.0版本踩坑记录

目录 1、查看可安装 MySQL 版本 2、Ubuntu安装 MySQL8.0 3、MySQL8.0 区分大小写问题 4、MySQL8.0 设置sql_mode 5、MySQL8.0 改端口33060&#xff08;个人遇到问题&#xff09; 1、查看可安装 MySQL 版本 ## 列出可用的MySQL版本&#xff08;列出所有可用的MySQL版本以…

「AI Infra 软件开源不是一个选项,而是必然」丨云边端架构和 AI Infra专场回顾@RTE2024

在人工智能和开源技术蓬勃发展的当下&#xff0c;AI Infra 项目正经历着日新月异的变革。从跨平台运行时到云边端 AI 基础设施&#xff0c;再到多模态知识助手&#xff0c;创新浪潮席卷而来。这些进步不仅显著提升了技术指标&#xff0c;也为实时音视频处理、边缘计算、大模型应…

《Python制作动态爱心粒子特效》

一、实现思路 粒子效果&#xff1a; – 使用Pygame模拟粒子运动&#xff0c;粒子会以爱心的轨迹分布并运动。爱心公式&#xff1a; 爱心的数学公式&#xff1a; x16sin 3 (t),y13cos(t)−5cos(2t)−2cos(3t)−cos(4t) 参数 t t 的范围决定爱心形状。 动态效果&#xff1a; 粒子…

免费实时图片编辑工具:MagicQuill

参看&#xff1a; https://huggingface.co/spaces/AI4Editing/MagicQuill 人工智能交互式图像编辑&#xff1a;可以制定涂改增加删除

web——upload-labs——第九关——特殊字符::$DATA绕过

特殊字符::$DATA绕过 典型绕过场景 在一些系统中&#xff0c;::$DATA 被用于绕过文件路径的限制。比如&#xff1a; 路径过滤绕过&#xff1a;如果系统有某种机制来检查和限制文件路径&#xff08;例如&#xff0c;禁止访问某些系统目录或敏感文件&#xff09;&#xff0c;通…

本地部署 excalidraw

本地部署 excalidraw 0. 引言1. 本地部署 excalidraw2. 访问 excalidraw 0. 引言 Excalidraw 编辑器是一款开源虚拟手绘白板&#xff0c;支持协作且端到端加密。 1. 本地部署 excalidraw git clone https://github.com/excalidraw/excalidraw.git; cd excalidrawvi docker-c…

《Java核心技术 卷I》用户界面AWT事件继承层次

AWT事件继承层次 EventObject类有一个子类AWTEvent&#xff0c;它是所有AWT事件类的父类。 Swing组件会生成更多其他事件对象&#xff0c;都直接拓展自EventObject而不是AWTEvent。 AWT将事件分为底层(low-level)事件和语义事件。 语义事件&#xff1a;表示用户的动作事件&…

三周精通FastAPI:42 手动运行服务器 - Uvicorn Gunicorn with Uvicorn

官方文档&#xff1a;Server Workers - Gunicorn with Uvicorn - FastAPI 使用 fastapi 运行命令 可以直接使用fastapi run命令来启动FastAPI应用&#xff1a; fastapi run main.py如创建openapi.py文件&#xff1a; from fastapi import FastAPIapp FastAPI(openapi_url&…

整理iPhone空间:iphone怎么删除相簿

随着时间的积累&#xff0c;我们的iPhone中不仅会堆积大量照片&#xff0c;还可能会有多个不再需要的相簿。这些相簿不仅占用存储空间&#xff0c;还可能使相册应用变得杂乱无章。本文将探讨iphone怎么删除相簿&#xff0c;并介绍精简iPhone相册的技巧&#xff0c;使你的相册管…

路漫漫其修远兮,吾将上下而求索---第一次使用github的过程记录和个人感受

文章目录 1.仓库位置2.新建仓库3.配置仓库4.克隆和上传5.推荐文章和我的感受 1.仓库位置 这个仓库的位置就是在我们的这个个人主页的右上角&#xff1b;如果是第一次注册账号的话&#xff0c;这个主页里面肯定是不存在仓库的&#xff0c;需要我们自己手动的进行创建&#xff1…

ICML24最新开源时序基础模型MOMENT

论文标题&#xff1a;MOMENT: A Family of Open Time-series Foundation Models 论文链接&#xff1a;https://arxiv.org/pdf/2402.03885 前言 当前时间序列数据上预训练大型模型面临以下挑战&#xff1a;(1) 缺乏大型且统一的公共时间序列数据集&#xff0c;(2) 时间序列特…

SpringBoot Data Redis连接Redis-Cluster集群

使用SpringBoot Data Redis无法连接Redis-Cluster集群 最近在研究系统高并发下的缓存架构&#xff0c;因此自己在自己买的云服务器上搭建好Redis 5.0 版本的集群后&#xff0c;使用springboot的 RedisTemplate连接是发现总是访问不到集群节点。上网百度了发现没有好的解决办法&…

鸿蒙中服务卡片数据的获取和渲染

1. 2.在卡片中使用LocalStorageProp接受传递的数据 LocalStorageProp("configNewsHead") configNewsHeadLocal: ConfigNewsHeadInfoItem[] [] 注意&#xff1a;LocalStorageProp括号中的为第一步图片2中的键 3.第一次在服务卡片的第一个卡片中可能会获取不到数据…

ARM64环境部署EFK8.15.3收集K8S集群容器日志

环境规划 主机IP系统部署方式ES版本CPU架构用户名密码192.168.1.225Ubuntu 22.04.4 LTSdockerelasticsearch:8.15.3ARM64elasticllodyi4TMmZD ES集群部署 创建持久化目录(所有节点) mkdir -p /data/es/{data,certs,logs,plugins} mkdir -p /data/es/certs/{ca,es01}服务器…

应用系统开发(12) Zync中实现数字相敏检波

在 Xilinx Zynq 系列(如 Zynq-7000 或 Zynq UltraScale+)中实现数字相敏检波(DSP,Digital Synchronous Detection)可以通过硬件(PL部分,FPGA逻辑)和软件(PS部分,ARM Cortex-A 处理器)的协同工作来实现。以下是一个详细的设计方法,包括基本原理和 Zynq 的实现步骤。…

力扣hot100-->二分查找

目录 二分查找 1. 33. 搜索旋转排序数组 2. 34. 在排序数组中查找元素的第一个和最后一个位置 3. 240. 搜索二维矩阵 II 3. 287. 寻找重复数 二分查找 1. 33. 搜索旋转排序数组 中等 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&am…

南京邮电大学《智能控制技术》课后作业

一、问题一 复现二输入单输出模糊控制系统&#xff0c;改动其中一到两个环节&#xff08;隶属度设置、规则等&#xff09;&#xff0c;对比修改前后控制效果。 定义模糊 %Fuzzy Control for water tank clear all; close all;anewfis(fuzz_tank);%Fuzzy Inference System stru…