苍穹外卖-后端部分

软件开发整体介绍

前端搭建

在非中文目录中双击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(" ");
        //取出第三个元素 即username
        String 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) {
    //开始分页查询 利用PageHelper
    PageHelper.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 INSERT
    OperationType 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_分类id
    String 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);//update
        shoppingCartMapper.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查询订单orders
    Orders 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 {
    @Autowired
    private 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 content
        Map 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-03
    private String dateList;

    //营业额,以逗号分隔,例如:406.0,1520.0,75.0
    private 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());//前端携带过来的id
    return Result.success(userLoginVO);
}

然后再业务层通过该方法获取用户openid

private String getOpenid(String code){
    //调用微信接口服务获得当前用户的openId
    Map<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_分类id
    String 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/ObjectName
        StringBuilder 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 {
    @Bean
    public 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/*.xml
  type-aliases-package: com.sky.entity
  configuration:
    #开启驼峰命名
    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 {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");
    }

    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public 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(" ");
        //取出第三个元素 即username
        String 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 {

    @Autowired
    private 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 {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    @Autowired
    private 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
     */
    @Bean
    public 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
     */
    @Bean
    public 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 {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");
    }

    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public 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);
    }
}
相关推荐
hanbarger1 小时前
mybatis框架——缓存,分页
java·spring·mybatis
苹果醋31 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园1 小时前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10432 小时前
java web springboot
java·spring boot·后端
smile-yan2 小时前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
果冻的猿宇宙2 小时前
Maven 中央仓库访问过慢的解决方案--设置国内镜像
maven·镜像·仓库·aliyun·国内镜像·mirror
哆啦 AI 梦2 小时前
【Maven】如何解决Maven循环依赖?
maven·循环依赖
Earnest~2 小时前
Maven极简安装&配置-241223
java·maven
皮蛋很白2 小时前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea