苍穹外卖项目部分详解以及部分涉及到的知识点

PS:纯小白,有部分原理知识按照自己的理解来写的,可能专业术语使用不当,欢迎大家批评指出错误。

参考视频出处:黑马程序员Java项目实战《苍穹外卖》https://www.bilibili.com/video/BV1TP411v7v6/?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click&vd_source=234efc646b1bb4e708f30f94d7060b16

本文只写了在练习该项目中比较困难的部分,仍有部分业务功能未写进来,后续会慢慢补充,未完待续.....

整体架构

dto常用于传输数据,作为参数,接收到的dto可以将封装的内容交给实体对象中的内容

业务实现

员工业务实现

员工登录与退出

接口文档

请求参数为body类型,将其封装在EmployeeLoginDTO类中,添加注解@RequestBody,用于将HTTP请求体中的JSON数据绑定到参数对象上。

调用EmployeeService接口方法login

 /**
     * 员工登录
     *
     * @param employeeLoginDTO
     * @return
     */
    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);

        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        // 需要进行md5加密,然后再进行比对
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

        //3、返回实体对象
        return employee;
    }

密码加密:使用DigestUtils工具类中的方法

登陆成功后,生成JWT令牌,实现一个拦截器,每次请求业务时进行JWT令牌的校验。

图1
拦截器的实现

1.创建一个配置类(普通Java类),并使用@ConfigurationProperties注解指定配置属性的前缀,该配置应当配置在application.yml文件中

驼峰命名,admin-secret-key -> adminSecretKey

2.定义拦截器的实现类,并将配置类以依赖注入的形式引入进来,方便使用其属性,如图1。

3.定义好拦截器类后,在Sever层中创建一个WebMvcConfiguration类,该类被声明为配置类,用于注册web层的相关组件。

依赖注入的关系:

JwtProperties -> JwtTokenAdminInterceptor -> WebMvcConfiguration
ThreadLocal

每一次请求就是一个线程,在一次请求未结束之前,共享内容ThreadLocal内容

由于每次请求会被拦截器拦截,拦截到请求后会通过BaseContext.setCurrendId将token中的id取出保存到ThreadLocal中。

BaseContetxt类中封装了ThreadLocal类对象,并定义了threadlocal方法对应的set和get方法

新增员工

将前端传递来的参数封装进EmployeeDTO类中,在sever层中将DTO类数据封装给实体类

驼峰映射:使数据库字段与Java对象属性之间进行映射

员工的分页查询

分页查询参数

name字段非必须,用于模糊匹配。page字段为查询的第几页开始,pageSize为一页显示多少张

前端的相应参数为Query请求参数而非Body。

Query和Body

数据传递位置不同:Body 数据在请求主体中,Query 数据在 URL 查询字符串中。

        

参数传递

在controller层,使用一个对象来封装传递来的三个参数,这种方式是可行的。不过需要保证的是,类中的属性名要与传入参数的名字保持一致,不一致需要用注解进行绑定

返回值

分页查询的返回值有两个:①分页查询的员工数据集合需要渲染到前端②查询到的符合条件的记录数。 因此可以使用pageResult对象进行封装,并指定Result的泛型为pageResult,这样pageResult中data的类型也被指定为泛型(之前返回值为Result没写泛型,所以data数据的默认泛型为Object类型)

分页查询插件PageHelper

pageHelper的底层用到ThreadLocal,在分页查询之前,将page和pageSize从ThreadLocal中取出,将Limit动态拼接到sql语句中。

代码问题

方法一:用到了JsonFormat的序列化与反序列化,缺点是对每一个属性都要添加

在WebMvcConfiguration配置类中,声明了一系列web组件,在其中扩展MVC的消息转换器。重写父类中的extendMessageConverters方法进行扩展。

启用、禁用员工账号

涉及到两种类型参数路径参数,应当添加@PathVariable注解

@Build注解

在调用service层方法时,新创建一个employee对象,为了避免冗余的set方法,在Employee类上声明@Build注解,快速生成一个实例对象

修改员工信息

将前端传递的请求参数封装在EmployeeDTO类中,在Service层传递给Employee对象,调用上一个方法创建的动态SQL,SetStatus方法中实现了修改所有属性的sql语句,直接调用即可

分类业务实现

分类业务导入

分类业务涉及到三张表,类别表category,菜品表dish,套餐表setmeal

删除分类

根据id删除分类需要判断当前分类下是否有菜品或者套餐,如果有则不允许删除并且抛出异常

修改与添加分类

问题分析

在管理员工业务,分类业务,后续的菜品,套餐管理业务中,均需要涉及修改或者增加操作,每次操作都需要设置创建时间、修改时间、创建人、修改人的信息,使代码冗余。因此使用AOP面向切面编程解决问题

除此之外,利用自定义注解来替代切入点表达式更好。

①首先自定义注解,用于标识需要进行公共字段自动填充

②自定义切面类AutoFillAspect

@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 定义切入点表达式
     */
    //添加自定义的注解
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autofillPointCut(){}

    //指定切入点表达式,定义前置通知
    @Before("autofillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("执行自动填充");

        //获取到当前被拦截方法上的数据库操作类型
        MethodSignature signature =(MethodSignature)joinPoint.getSignature();//方法对象签名
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//方法上的注解对象
        OperationType operationType = autoFill.value();//获取数据库操作类型

        //获取到当前被拦截的方法的参数--实体对象
        Object[] args = joinPoint.getArgs();
        //排除空指针的特殊情况
        if(args ==null || args.length==0){
            return;
        }
        /**
         * args[0]:约定实体对象放在方法参数的第一个位置
         * 由于后续功能开发,返回的实体对象的类型不确定,统一使用Obiect类型
         */
        Object entry = args[0];

        //对实体对象公共属性进行赋值
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        if(operationType==OperationType.INSERT){
            //插入操作需要为四个公共字段赋值
            try {
                Method setCreateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性进行赋值
                setCreateTime.invoke(entry, now);
                setCreateUser.invoke(entry, currentId);
                setUpdateTime.invoke(entry, now);
                setUpdateUser.invoke(entry, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }else if(operationType==OperationType.UPDATE){
            //更新操作为两个字段赋值
            try {
                Method setUpdateTime = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entry.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                setUpdateTime.invoke(entry, now);
                setUpdateUser.invoke(entry, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

动态调用方法,指定是哪个对象来调用,传递的参数是什么。

③在Mapper的方法上加入AutoFill注解,并指定value属性

④由于是前置通知,在点击业务按钮后,前置通知拦截到该方法,并执行前置通知中定义的逻辑,获取到现在传入的employee对象,对其字段进行填充,随后再调用原始操作

反射

通过获取一个类的类对象,才能通过反射获取到类中的属性,变量,方法等。

一、获取class对象的三种方式

①Class.forname("全类名")

②类对象.class

③对象.getClass()

编写java源文件后,扩展名为.java。使用java编辑器将java源文件编译成字节码文件,编译成功后,java编辑器会在硬盘上生成与每个类对应的.class文件,每个.class文件对应一个类。在这个阶段,是在硬盘中完成的,获取类对象使用方式①

在运行时,将字节码文件加载到内存中,这一过程为加载阶段,使用方式②

在内存创建对象,运行阶段使用方式③

菜品相关接口

上传图片

配置文件yml

表明在启动spring boot项目时会激活名为dev的配置文件,可以将硬编码的部分交给dev文件,再通过占位符的形式将参数传递给该配置文件,使得隐藏内容,图1:application.yml,图2""-dev.yml

图1
图2
阿里云配置属性类

阿里云工具类(导入即可,按需求修改内容)

工具类中定义了4个属性,上传文件的方法upload,返回值为在阿里云oss中文件的名字

工具类添加到IOC容器

文件上传Controller层

添加菜品

传递参数

将前端的请求参数封装进DishDTO类中

涉及多表

DishDTO类中既包含了1个菜品的信息,还包含了该菜品的多个口味。所以涉及到将数据拆分出来插入到不同的表中。

一、菜品表Dish

由于在之前实现了通过注解将共同的属性进行赋值,所以只需要在mapper方法上添加@AutoFill(value=?)注解并指明数据库操作的类型

经过插入操作,dish类的categoryId属性被赋值,但主键值并未赋值

而在选择好该菜品的口味后,需要将该菜品的主键值传给口味表,实现两个表的一对多的关系

二、口味表Dish_Flavor

使用foreach标签进行遍历拼接,collection的值应为传入集合的名字

事务控制

由于对两张表的操作是连续的不能间断的,因此使用事务控制,保证了事务的原子性。

在方法上添加Transactional注解表示开启事务管理。同时需要在Spring Boot项目的启动类上添加@EnableTransactionManagement注解标识开启注解方式的事务管理。

菜品的分页查询

在向前端返回的数据中,额外有一项categoryName,该属性属于category表的id属性。涉及到多表查询,由于dish表所需要全部数据,而后表只需要一项数据,所以左外连接后表,同时分页查询,分类名称的模糊查询。

返回的VO对象中分类名称为categoryName,而表中字段名为name,在讲结果集映射到java对象时,通常会使用数据库表中的原始列名进行映射,导致映射将失败。

菜品的批量删除

将单个删除的情况也看作为批量删除的情况,请求参数为query

在控制器方法参数前添加@RequestParam注解,用于将请求参数绑定到控制器方法的参数上。它可以处理查询字符串参数、表单数据和路径变量

删除条件

1.查询起售状态---当前正在起售菜品不能删除

2.是否关联着套餐--关联着套餐不能删除

3.在删除菜品的时候,需要将该菜品对应的口味全部删除

根据Id查询菜品信息

返回数据

当点击修改菜品按钮时,首先根据菜品的id进行业务查询,将菜品的信息回显给前端

返回的data类型为DishVO,其中为dish类属性+菜品口味的列表。

处理逻辑,分两次查询,分别调用dishMapper和dishFlavorMapper层方法,将两次查询结果封装进dishVO对象中。

根据id修改菜品

完成上一步回显操作后,进行修改各类信息。

问题分析

某些菜品本来有一定的口味,若在其原有的基础上添加,或者需要删除掉原有的口味信息,由于情况复杂,采用:在提交修改之前将原有的口味全部删除一遍,再将改好后的口味信息从前端传递给后端。

套餐接口

项目已导入

Redis数据库

Redis数据库中的数据类型

在Java中操作Redis数据库

③编写配置类

④通过RedisTemplate对象来操作Redis

设置店铺的营业状态和查询状态

由于店铺的营业状态为单个值,无需使用mysql数据库为其单独创建一张表,而且涉及到经常改变,所以Redis数据库的Key-Value结构更有优势

微信小程序开发

微信登陆过程

微信登陆

定义配置文件

由于商家端和用户移动端都需要对是否登陆操作进行校验,因此在商家端和用户端都需要用到jwt技术来验证是否登录

配置属性类

将配置文件中的属性值传递给微信用户生成jwt令牌相关配置,并将该类添加进IOC容器管理

接口文档

Controller层

public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private JwtProperties jwtProperties;


    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        log.info("微信登录的code:{}", userLoginDTO.getCode());

        //将返回的结果id和openid封装在User实体类中
        User user = userService.wxlogin(userLoginDTO);

        //创建JWt令牌
        //将获取到的id和openid添加进hashmap封装金jwt令牌中
        Map<String, Object> claims = new HashMap<>();

        //openid字段无需封装进jwt令牌因为VO对象中有属性用于接受openid
        claims.put(JwtClaimsConstant.USER_ID,user.getId());
        //将配置类自动注入获取到其中的SecretKey和UserTtl,生成JWT令牌
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

        //封装VO对象
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())   //封装用户id(在本地数据库中唯一)
                .openid(user.getOpenid())   //openid(在微信服务器中唯一)
                .token(token)
                .build();
        return Result.success(userLoginVO);
    }
}

Service层

向微信服务端接口发起http请求,并传入所需的四个参数,获取到返回的openid。

HttpClient返回值为JSON格式,其中包含了所需要的openid。获取到其中openid的方法:

①获取到JSON对象 ②JSON对象的getString方法获取到键为openid的值

判断openid是否为空,如果为空抛出业务异常,不为空返回

public class UserServiceImpl implements UserService {

    //定义常量--微信服务端地址
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;

    @Autowired
    private UserMapper userMapper;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @Override
    public User wxlogin(UserLoginDTO userLoginDTO) {
        //调用微信接口获取openid
        String openid = getString(userLoginDTO);
        if(openid==null){
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        //判断该用户是否为新用户--查找openid在用户表中是否存在
        User user = userMapper.getByopenid(openid);

        //如果为新用户,则自动注册
        if(user==null){
            //将当前新用户信息插入到数据库中
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }
        return user;
    }

    /**
     * 调用微信服务端接口
     * @param userLoginDTO
     * @return
     */
    private String getString(UserLoginDTO userLoginDTO) {
        //创建微信接口所需要传递的参数集合
        Map<String, String> map = new HashMap<>();
        map.put("appid",weChatProperties.getAppid());
        map.put("secret",weChatProperties.getSecret());
        map.put("js_code", userLoginDTO.getCode());
        map.put("grant_type","authorization_code"); //四个官方要求传递的固定参数
        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        //判断openid是否为空--登陆失败,抛出业务异常
        //获取到json对象
        JSONObject jsonObject = JSON.parseObject(json);
        //获取到键为openid对应的值
        String openid = jsonObject.getString("openid");
        return openid;
    }

}

封装逻辑:user中封装获取到的id和openid,生成的jwt令牌为"token",id封装进jwt令牌中,三者封装在userLoginVO对象中。

缓存菜品数据

原因:通过Redis来缓存菜品数据,减少数据库的查询操作。

缓存菜品(代码版)

将菜品内容以String类型存入Redis中

删除缓存

传入的pattern为通配表达式

Spring Cache框架

简化代码开发。该框架实现了基于注解的缓存功能。Spring Cache提供了一层抽象,在底层可以切换不同的缓存实现,如EHCache、Caffeine、Redis。

常用注解

最终缓存到Redis中的样式为setmeal::categoryId

添加购物车

接口文档:其中dishId和setmealId不能同时传递

逻辑分析

1.添加的购物车需要知道当前用户是哪个用户

2.若当前购物车中有该商品,则数目+1

3.新商品需要判断为菜品还是套餐进行插入

4.新插入的商品数目设置为1

 /**
     * 添加购物车
     *
     * @param shoppingCartDTO
     */
    @Override
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 创建实体对象
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        //设置当前用户的userId
        shoppingCart.setUserId(BaseContext.getCurrentId());
        //查询购物车数据只可能为0或者1,不能为多
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        //查询购物车表,如果已存在当前商品,则数目+1
        if (list != null && list.size() > 0) {
            //列表中唯一的数据
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            //更新菜品数目
            shoppingCartMapper.updateNumberById(cart);

        } else {
            //若不存在,则插入商品信息
            Long setmealId = shoppingCartDTO.getSetmealId();
            Long dishId = shoppingCartDTO.getDishId();
            //根据传递的为菜品还是套餐来查询表获取到name,image等属性
            if (dishId != null) {
                //当前添加为菜品
                Dish dish = dishMapper.getById(dishId);
                //属性拷贝到cart
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());

            } else {
                //当前添加为套餐
                Setmeal setmeal = setmealMapper.getById(setmealId);
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());

            }
            //统一设置数目与时间
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
        }
        shoppingCartMapper.insert(shoppingCart);

    }

地址簿模块难点

设置默认地址

传递来的JSON格式的数据可以被绑定到类中的属性,其中传递的id值可以自动绑定到AddressBook类中的id属性上

用户下单

返回参数

Controller层

前端传递参数封装进OrdersSubmitDTO对象,返回值封装进orderSubmitVO对象

业务逻辑

订单基本信息单独存放,订单中的菜品存放到另一张表

1.首先处理业务异常,当购物车为空,地址簿为空的情况,各自抛出定义的异常

2.获取当前用户的购物车内容

3.向订单表插入一条数据

4.向订单明细表插入多条数据

    /**
     * 用户订单提交
     * @param ordersSubmitDTO
     * @return
     */
    @Override
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {

        //处理各种业务异常(地址簿为空,购物车为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());// 根据地址簿id查询地址
        if(addressBook == null){
            //没有查到地址,抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        //获取当前用户id
        Long userid = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userid);
        //获取用户购物车内容
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if(list == null || list.size() == 0){
            //当前购物车为空
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        //向订单表提交一条数据
        Orders orders = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO,orders);
        orders.setOrderTime(LocalDateTime.now());
        orders.setPayStatus(Orders.UN_PAID);
        orders.setStatus(Orders.PENDING_PAYMENT);
        //设置订单号--以当前系统的时间戳
        orders.setNumber(String.valueOf(System.currentTimeMillis()));
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());
        orders.setUserId(userid);
        orderMapper.insert(orders);
        //获取到当前订单id
        Long orderId = orders.getId();

        //创建存放n条明细的集合,使插入操作就做一次提高效率
        List<OrderDetail> orderDetailList = new ArrayList<OrderDetail>();
        //向订单明细表插入n条数据
        for (ShoppingCart r : list) {
            OrderDetail orderDetail = new OrderDetail();
            BeanUtils.copyProperties(r,orderDetail);
            orderDetail.setOrderId(orderId);
            //添加到集合中
            orderDetailList.add(orderDetail);
        }
        //插入
        orderDetailMapper.insertBench(orderDetailList);
        //清空当前用户的购物车
        shoppingCartMapper.deleteByUserId(userid);
        //封装返回VO对象
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(orderId)
                .orderNumber(orders.getNumber())
                .orderAmount(orders.getAmount())
                .orderTime(orders.getOrderTime()).build();
        return orderSubmitVO;
    }

订单支付

导入订单支付功能代码,熟悉微信支付流程

用户端历史订单模块

历史订单查询

逻辑分析:分页查询,根据订单状态动态查询,返回的结果有订单的基本信息(Orders)并且还要有订单明细(Orders_detail),因此选择返回对象封装进OrderVO对象中(其继承自Orders并且还有订单明细属性)

①:返回结果有total和records,total为分页查询的订单总数,records为查询结果(OrderVO类)

②:分页查询的结果泛型为订单类,遍历该集合,获取到每一个订单对象以及查询到每个订单对应的订单明细,订单对象的信息经过属性拷贝到OrderVO继承下来的属性中,订单明细集合赋值给OrderVO中新加的属性。将每一个封装好的OrderVO对象封装成集合,再将该集合封装进PageResult对象的records属性中。

取消订单

逻辑分析:

- 待支付和待接单状态下,用户可直接取消订单
- 商家已接单状态下,用户取消订单需电话沟通商家
- 派送中状态下,用户取消订单需电话沟通商家
- 如果在待接单状态下取消订单,需要给用户退款
- 取消订单后需要将订单状态修改为“已取消”

根据订单状态进行判断,微信退款

再来一单(stream流)

业务逻辑:将再来一单的菜品重新添加到购物车中

①判断当前用户id(将菜品添加到本用户的购物车中)

②订单明细对象OrderDetails---->购物车对象shoppingCart

③将购物车对象集合批次添加进数据库

	/**
     * 再来一单
     *
     * @param id
     */
    public void repetition(Long id) {
        // 查询当前用户id
        Long userId = BaseContext.getCurrentId();

        // 根据订单id查询当前订单详情
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

        // 将订单详情对象转换为购物车对象
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
            ShoppingCart shoppingCart = new ShoppingCart();

            // 将原订单详情里面的菜品信息重新复制到购物车对象中,忽略“id”属性的值
            BeanUtils.copyProperties(x, shoppingCart, "id");
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());

            return shoppingCart;
        }).collect(Collectors.toList());

        // 将购物车对象批量添加到数据库
        shoppingCartMapper.insertBatch(shoppingCartList);
    }

增强for的细节:修改增强for中的变量,不会改变集合中原本的数据

xml

<insert id="insertBatch" parameterType="list">
        insert into shopping_cart
        (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
        values
        <foreach collection="shoppingCartList" item="sc" separator=",">
            (#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
        </foreach>
</insert>

Stream流知识补充

一、lambda表达式

Lambda表达式只能用于简化函数式接口的匿名内部类的写法

二、stream流

①stream流的中间方法map

map方法的参数为一个函数式接口,可以简写成lambda表达式

②collect方法

收集流中的数据,放到集合中(List、set、Map)

List:collect(collectors.toList())

Map:collect(collectors.toMap(键的规则、值的规则))

商家端订单管理模块

订单搜索

接口分析

返回结果中orerDishes为OrderVO对象中的属性值,代表将菜品明细以字符串的形式进行返回,不使用List集合,因此涉及到查询菜品明细并转换成字符串对象

业务逻辑:分页查询,支持模糊搜索

/**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        // 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
        List<OrderVO> orderVOList = getOrderVOList(page);

        return new PageResult(page.getTotal(), orderVOList);
    }

    private List<OrderVO> getOrderVOList(Page<Orders> page) {
        // 需要返回订单菜品信息,自定义OrderVO响应结果
        List<OrderVO> orderVOList = new ArrayList<>();

        List<Orders> ordersList = page.getResult();
        //判断订单列表是否为空
        if (!CollectionUtils.isEmpty(ordersList)) {
            for (Orders orders : ordersList) {
                // 将共同字段复制到OrderVO
                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                //将菜品明细转换成String类型
                String orderDishes = getOrderDishesStr(orders);

                // 将订单菜品信息封装到orderVO中,并添加到orderVOList
                orderVO.setOrderDishes(orderDishes);
                orderVOList.add(orderVO);
            }
        }
        return orderVOList;
    }

    /**
     * 根据订单id获取菜品信息字符串
     *
     * @param orders
     * @return
     */
    private String getOrderDishesStr(Orders orders) {
        // 查询订单菜品详情信息(订单中的菜品和数量)
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
        List<String> orderDishList = orderDetailList.stream().map(x -> {
            String orderDish = x.getName() + "*" + x.getNumber() + ";";
            return orderDish;
        }).collect(Collectors.toList());

        // 将该订单对应的所有菜品信息拼接在一起
        return String.join("", orderDishList);
    }

拒单

业务逻辑:修改订单状态为已取消,只有处于待接单状态才能拒单,知名拒单原因,如果用户已经付款,需要执行退款操作

    /**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    @Override
    public void reject(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        //处理业务异常--只有"待接单"状态可以执行拒单操作
        Orders order = orderMapper.getByOrderId(ordersRejectionDTO.getId());
        if (order == null || order.getStatus() != 2) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }
        //如果已经完成支付则进行退款
        if (order.getPayStatus() == 1) {
            String refund = weChatPayUtil.refund(order.getNumber(), order.getNumber(), new BigDecimal(0.01), new BigDecimal(0.01));
            log.info("申请退款:{}",refund);
        }

        //设置订单状态为已取消并设置拒单原因
        Orders orderupdate = Orders.builder()
                .id(ordersRejectionDTO.getId())
                .status(Orders.CANCELLED)
                .rejectionReason(ordersRejectionDTO.getRejectionReason())
                .cancelTime(LocalDateTime.now())
                .build();
        //更新订单状态
        orderMapper.update(orderupdate);
    }

 Spring task

spring 任务调度工具,按照约定时间自动执行某个代码逻辑

使用步骤

订单状态处理

代码

 /**
     * 处理用户支付超时任务
     */
    @Scheduled(cron = "0 * * * * ?") //每分钟输出一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());

        //定义当前时间往前推15分钟
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        //查询当前状态为待支付,并且下单时间小于当前时间往前推15分钟的订单
        List<Orders> orders = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT,time);


        if (orders.size()>0 && orders != null) {
            //自动取消超时订单
            for (Orders order : orders) {
                //修改订单信息
                order.setCancelReason("用户支付超时");
                order.setCancelTime(LocalDateTime.now());
                order.setStatus(Orders.CANCELLED);
                orderMapper.update(order);
            }
        }
    }


    /**
     * 处理一直处于派送中状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("定时处理一直处于派送状态的订单:{}", LocalDateTime.now());
        //当前时间减去一天时间,相当于处理上一天当前时间的订单
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> orders = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS,time);

        if (orders.size()>0 && orders != null) {
            //自动取消超时订单
            for (Orders order : orders) {
                //修改订单信息
                order.setStatus(Orders.COMPLETED);
                orderMapper.update(order);
            }
        }
    }

WebSocket

websocket基础

websocket的使用步骤

客户端:

① 前端网页支持websocket技术并使用js实现了websocket

服务器端

② 导入websocket的maven坐标

前端

websocketServer组件和WebSocketConfiguration配置类的关系

Apache ECharts

营业额统计

在指定时间范围内查询营业额

返回数据格式

这里返回数据不能用datelist.toString,其返回的结果为[1,2,3]带有中括号,而StringUtils.join返回结果为1,2,3不带括号。

StringUtils.join为Apache Commons Lang3 库提供的一个实用方法,将数组或集合中的元素用指定分隔符连接成字符串。

销量Top10统计

业务逻辑:查询在指定时间范围内的订单,再查订单明细表中的菜品,统计菜品的总数目,并且是已完成订单的菜品明细。难点在SQL语句的编写

多表查询,分组查询

知识点补充:分组查询,聚合函数

分组查询往往与聚合函数搭配使用

Apache POI

使用POI在java程序中操作Excel等文件的读写操作

HttpServletResponse 对象在 Spring 控制器方法中作为参数传递,用于向客户端发送响应。这是通过在响应中写入生成的文件数据来实现的。使用 response.getOutputStream() 获取响应输出流,并将生成的 Excel 文件数据写入其中。

this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx")

获取当前类的类加载器,然后通过该类加载器获取项目资源目录中的文件输入流。

在 Spring Boot 项目中,getResourceAsStream 方法可以加载 src/main/resources 目录中的文件。src/main/resources 目录中的资源文件会被打包到最终的 JAR 文件中,因此可以使用类加载器加载这些文件。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值