苍穹外卖-后端部分

news2024/12/26 23:48:15

软件开发整体介绍

前端搭建

在非中文目录中双击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);
    }
}

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

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

相关文章

3D电子商务是什么?如何利用3D技术提升销售转化?

在数字化浪潮席卷全球的今天&#xff0c;网上购物已成为消费者日常生活中不可或缺的一部分。然而&#xff0c;尽管其便捷性无可比拟&#xff0c;但传统电商模式中的“看不见、摸不着”问题始终困扰着消费者与商家。商品是否符合期望、尺寸是否合适、颜色是否真实……这些不确定…

EXCEL延迟退休公式

如图&#xff1a; A B为手工输入 C2EOMONTH(A2,B2*12) D2EOMONTH(C2,IF(C2>DATEVALUE("2025-1-1"),INT((DATEDIF(DATEVALUE("2025-1-1"),C2,"m")4)/4),0)) E2EOMONTH(A2,B2*12IF(EOMONTH(A2,B2*12)>DATEVALUE("2025-1-1"),INT(…

OpenSSL 自签名

参考文档&#xff1a;unigui开发人员工作手册2021 参考文章&#xff1a;保姆级OpenSSL下载及安装教程-CSDN博客 下载 Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions 进入后向下拉找到下载位置&#xff0c;建议下载二进制版本的精简版&#xff0c…

DevOps工程技术价值流:加速业务价值流的落地实践与深度赋能

DevOps的兴起&#xff0c;得益于敏捷软件开发的普及与IT基础设施代码化管理的革新。敏捷宣言虽已解决了研发流程中的诸多挑战&#xff0c;但代码开发仅是漫长价值链的一环&#xff0c;开发前后的诸多问题仍亟待解决。与此同时&#xff0c;虚拟化和云计算技术的飞跃&#xff0c;…

R语言贝叶斯分析:INLA 、MCMC混合模型、生存分析肿瘤临床试验、间歇泉喷发时间数据应用|附数据代码...

全文链接&#xff1a;https://tecdat.cn/?p38273 多模态数据在统计学中并不罕见&#xff0c;常出现在观测数据来自两个或多个潜在群体或总体的情况。混合模型常用于分析这类数据&#xff0c;它利用不同的组件来对数据中的不同群体或总体进行建模。本质上&#xff0c;混合模型是…

Python酷库之旅-第三方库Pandas(218)

目录 一、用法精讲 1021、pandas.DatetimeIndex.inferred_freq属性 1021-1、语法 1021-2、参数 1021-3、功能 1021-4、返回值 1021-5、说明 1021-6、用法 1021-6-1、数据准备 1021-6-2、代码示例 1021-6-3、结果输出 1022、pandas.DatetimeIndex.indexer_at_time方…

从基础到进阶,Dockerfile 如何使用环境变量

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 什么是 Dockerfile 环境变量?🔖1. `ENV` 指令🔖2. `ARG` 指令🔖语法:🔖使用 `ARG` 的例子:📝 如何使用环境变量提高 Dockerfile 的灵活性🔖1. 动态配置环境🔖2. 配置不同的运行环境🔖3. 多…

2002.6 Partitioning the UMLS semantic network.划分 UMLS 语义网络

Partitioning the UMLS semantic network | IEEE Journals & Magazine | IEEE Xplore 问题 统一医学语言系统&#xff08;UMLS&#xff09;语义网络中的语义类型&#xff08;ST&#xff09;在知识表示和应用中存在不足&#xff0c;例如 ST 的组织方式缺乏直观性和可解释性…

帽子矩阵--记录

帽子矩阵&#xff08;Hat Matrix&#xff09;并不是由某一位具体的科学家单独发明的&#xff0c;而是逐渐在统计学和线性代数的发展过程中形成的。帽子矩阵的概念最早出现在20世纪初的统计学文献中&#xff0c;尤其是在回归分析的研究中得到了广泛应用。然而&#xff0c;具体是…

vue面试题8|[2024-11-14]

问题1&#xff1a;什么是渐进式框架? vue.js router vuex element ...插件 vue.js 渐0 router 渐1 vuex 渐2 vue.js只是一个核心库&#xff0c;比如我再添加一个router或者vuex&#xff0c;不断让项目壮大&#xff0c;就是渐进式框…

web与网络编程

使用HTTP协议访问Web 通过发送请求获取服务器资源的Web浏览器等&#xff0c;被成为客户端(client)。 Web使用一种名为HTTP(超文本传输协议)的协议作为规范&#xff0c;完成从客户端到服务器端等一系列运作流程。 可以说&#xff0c;Web时建立在HTTP协议上通信的。 网络基础T…

docker 部署freeswitch(非编译方式)

一&#xff1a;安装部署 1.拉取镜像 参考&#xff1a;https://hub.docker.com/r/safarov/freeswitch docker pull safarov/freeswitch 2.启动镜像 docker run --nethost --name freeswitch \-e SOUND_RATES8000:16000 \-e SOUND_TYPESmusic:en-us-callie \-v /home/xx/f…

opencv kdtree pcl kdtree 效率对比

由于项目中以一个环节需要使用kdtree ,对性能要求比较严苛&#xff0c;所以看看那个kdtree效率高一些。对比了opencv和pcl。 #include <array> #include <deque> #include <fstream> #include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp…

ab (Apache Bench)的使用

Apache Bench&#xff08;ab&#xff09;是一个用于基准测试HTTP Web服务器的命令行工具&#xff0c;广泛用于评估和优化Web服务器的性能。以下是关于Apache Bench的详细介绍&#xff0c;包括其功能、使用方法、常用参数和输出结果解析。 功能 性能测试&#xff1a;通过模拟多…

【数据分享】全国农产品成本收益资料汇编(1953-2024)

数据介绍 一、《全国农产品成本收益资料汇编 2024》收录了我国2023年主要农产品生产成本和收益资料及 2018年以来六年的成本收益简明数据。其中全国性数据均未包括香港、澳门特别行政区和台湾省数据。 二、本汇编共分七个部分,即:第一部分,综合;第二部分,各地区粮食、油料;第…

基于OpenCV的图片人脸检测研究

目录 摘要 第一章 引言 第二章 基于 OpenCV 的图片人脸检测 2.1 实现原理 2.2 代码实现与分析 2.3 代码详细分析 第三章 实验结果与分析 第四章 OpenCV 人脸检测的优势与局限性 4.1 优势 4.2 局限性 第五章 结论 第六章 未来展望 参考文献 摘要 人脸检测是计算机视…

BI(Bilinear interpolation)双线性插值实现上采样

在深度学习中 上采样是将图像放大 如上图所示 要求放大后的图像坐标(2,1)处的像素值 要找到目标图像中对应的原图像素 需要与扩大前和扩大后的边长比相乘得到一个坐标(1.5,0.75) 对应原图中没有一个像素点是重合的 蓝色框框的像素值与红色框框的四个点的像素值有关 相关的计算方…

多模态大模型简介

多模态大模型是机器学习领域的一个新兴趋势&#xff0c;它结合了文本、图像、音频等多种数据模态&#xff0c;以实现更全面和深入的信息理解和处理。这种模型能够处理跨模态任务&#xff0c;如图像标注、视觉问答、文本到图像的生成等&#xff0c;是人工智能领域的重要进展。 技…

Python 正则表达式的一些介绍和使用方法说明(数字、字母和数字、电子邮件地址、网址、电话号码(简单)、IPv4 )

## 正则表达式的概念和用途 正则表达式&#xff08;Regular Expression&#xff0c;简称Regex&#xff09;是对字符串操作的一种逻辑公式&#xff0c;由一些事先定义好的特定字符以及这些特定字符的组合所构成。这些特定字符及其组合被用来描述在搜索文本时要匹配的一个或多个…

java排序算法汇总

一、排序算法我介绍 1.1、介绍 排序也称排序算法(Sort Algorithm)&#xff0c;排序是将一组数据&#xff0c;依指定的顺序进行排列的过程。 1.2、排序的分类&#xff1a; 1) 内部排序&#xff1a;指将需要处理的所有数据都加载到内部存储器中进行排序。 2) 外部排序法&…