瑞吉外卖项目-包含扩展功能-5W字详细图文讲解-附源码(移动端部分)

707 阅读47分钟

后台部分的链接juejin.cn/spost/72426…

移动端-验证码模块

短信发送

短信服务介绍

​ 目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。

常用短信服务:

  • 阿里云
  • 华为云
  • 腾讯云
  • 京东
  • 梦网
  • 乐信

我们项目使用的是阿里云的短信服务。

阿里云短信服务

应用场景:

  • 验证码

    验证码1
  • 短信通知

    短信通知1
  • 推广短信

    广告推送1

​ 感兴趣的小伙伴可以去阿里云官网的产品中搜索短信服务,我们项目中使用的是第一类的短信验证码。

阿里云短信服务-注册账号

阿里云官网: www.aliyun.com/

点击官网首页注册按钮,跳转到如下注册页面

阿里云注册页面1

​ 注册登记号后,点击控制台后并搜索短信服务进去里面,选择国内消息进入到文本短信。

短信服务-国内消息

​ 点击添加签名后我们进入填写基本信息就可以了。

添加签名

短信服务-添加签名

​ 这里我测回并修改了签名,改成了吃饱饱坏蜀黍了。

短信模板管理

短信模板

​ 这个赠送的短信模板,好像是因为我是学生认证所以才送的,这里我推荐大家自己申请一个,然后和前面申请的签名绑定。

阿里云短信服务-设置AccessKey

光标移动到用户头像上,在弹出的窗口中点击[AccessKey 管理]

AccessKey管理

​ 选择子用户AccessKey更安全一些,不然直接使用AccessKey权限太高了,泄露后危险太大。

选择子用户AccessKey

​ 不需要控制台,因为我们可以直接使用当前的账户在页码操作,这里我们需要的是使用它提供的API接口来操作。

选择API接口

验证码验证

短信用户创建成功后

短信服务添加权限

​ 如果咱们的key和密码泄露后,我们可以在认证管理往下拉,然后对对应的key进行操作,禁用和删除都是可以的。

短信密钥泄露处理

​ 笔者本人目前为在校大三的学生,所以在注册阿里云的时候使用了学生认证。

代码开发

使用阿里云短信服务发送短信,可以参照官方提供的文档即可

具体开发步骤:

  1. 导入maven坐标

    <!--阿里云短信服务sdk-->
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.5.16</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
                <version>2.1.0</version>
            </dependency>
    
  2. 调用API

    /**
     * 短信发送工具类
     */
    public class SMSUtils {
    
    	/**
    	 * 发送短信
    	 * @param signName 签名
    	 * @param templateCode 模板
    	 * @param phoneNumbers 手机号
    	 * @param param 参数
    	 */
    	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
    		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
    		IAcsClient client = new DefaultAcsClient(profile);
    
    		SendSmsRequest request = new SendSmsRequest();
    		request.setSysRegionId("cn-hangzhou");
    		request.setPhoneNumbers(phoneNumbers);
    		request.setSignName(signName);
    		request.setTemplateCode(templateCode);
    		request.setTemplateParam("{\"code\":\""+param+"\"}");
    		try {
    			SendSmsResponse response = client.getAcsResponse(request);
    			System.out.println("短信发送成功");
    		}catch (ClientException e) {
    			e.printStackTrace();
    		}
    	}
    
    }
    

    ​ 这个工具类直接在资料里的工具类中就能看到,咱们直接复制进项目中就可以,单独开一个Utils包来存放工具类就可以了。

    API密钥输入

​ 箭头所指的地方填上我们前面在阿里云短信服务那边申请的AccessKey和密钥。

需要的API密钥

手机验证码登录

需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。

手机验证码登录的优点:

  • 方便快捷,无需注册,直接登录
  • 使用短信验证码作为登录凭证,无需记忆密码
  • 安全

登录流程:

输入手机号>获取验证码>输入验证码>点击登录>登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识。

手机验证码登录页面

数据模型

通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:

user表结构

前期准备工作

代码开发-梳理交互过程

在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:

  1. 在登录页面(front/page/login.html)输入手机号,点击[获取验证码] 按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信。
  2. 在登录页面输入验证码,点击[登录] 按钮,发送ajax请求,在服务端处理登录请求。

​ 开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

代码开发-准备工作

​ 在开发业务功能前,先将需要用到的类和接口基本结构创建好:实体类 User (直接从课程资料中导入即可)

  • Mapper接口 UserMapper
  • 业务层接口 UserService
  • 业务层实现类 UserServicelmpl
  • 控制层 UserController
  • 工具类SMSUtils、ValidateCodeUtils (直接从课程资料中导入即可)

​ 使用MP代码生成器的小伙伴,直接复制导入工具类即可。

代码开发-修改LoginCheckFilter

​ 前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。

过滤器放行

​ 并且第四步那边添加一个4-2,用来判断移动端用户是否登录了,这里的userId就是我们之后登录后会保存用户的id到session中,这样过滤器就能够检验我们是否登录了。

// 4-2、判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("userId") != null) {
            log.info("移动端用户已登录,用户id为:" + request.getSession().getAttribute("userId"));

            //设置线程局部变量,供公共字段自动填充用
            Long userId = (Long) request.getSession().getAttribute("userId");
            BaseContext.setCurrentId(userId);

            filterChain.doFilter(request, response);
            return;
        }

​ 前端当我们点击获取验证码的时候会去调用获取验证码的时候,它会去调用getCode()点击函数,但是我们这块的点击函数黑马给的资料中是缺少67行的sendMsgApi这个api请求的,因此我们需要补充这行代码,并且在移动端front文件下api包的login.js中添加上对应的sendMsgApi请求函数。

前端点击获取验证码调用的方法

移动端登录login.js

代码开发(Session)

代码开发-获得验证码

​ 我们生成验证码使用的是工具类ValidateCodeUtils,发送短信验证码使用的是SMSUtils类,在ValidateCodeUtils中有一个generateValidateCode是用来生成4或6位的验证码的,之后我们直接调用这个方法即可

/**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
public static String generateValidateCode(int length,String phone){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code.toString();
    }

​ 当我们在移动端登录页面输入手机号后点击获取验证码,可以看到前端页面发送的url,以及其携带的手机号参数,因此我们需要在Controller层中定义控制方法来接收传递过来的手机号参数,并生成验证码

获取验证码url

Controller层

/**
     * 移动端通过手机号获取验证码(session实现)
     * @param user
     * @return
     */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user,HttpSession session) {
        //获取手机号
        String phone = user.getPhone();

        if (StringUtils.isNoneEmpty(phone)) {
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4);
            //将验证码code保存到session中
            session.setAttribute("code",code);
            log.info("验证码,code为:{}",code);

            //调用阿里云提供的短信API完成发送短信
            //SMSUtils.sendMessage("?","?",phone,code);

            return R.success("手机验证码短信发送成功");
        }

        return R.error("短信发送失败!!!");
    }

​ 因为这块不涉及到与数据库持久层的操作,也没有复杂的业务逻辑,因此直接在控制层进行方法调用并返回即可,SMSUtils.sendMessage这个方法就看各位小伙伴自己的条件了,如果有条件的在申请好签名以及模板后,将签名和模板分别填入即可。

代码开发-用户登录

​ 如图当我们输入手机和验证码后,前端页面发送的url请求为:http://localhost/user/login,并且请求的参数为json形式,里头有phone和code两个参数。

移动端登录url

​ 因为黑马给的前端资料有些问题,这块可能有的小伙伴只有电话号码phone这个参数,并没有code这个参数,我们需要去static/front/page/login.html文件中78行左右添加或修改成这两段代码。

// 将手机号保存到浏览器的session
sessionStorage.setItem("userPhone",this.form.phone)
// 将移动端用户信息保存到浏览器的本地存储空间
localStorage.setItem('user',JSON.stringify(res.data))

登录html代码修改

Controller层

​ 因为前端发送的url请求中包含了phone和code两个参数,如果光用User类型的参数来接收是无法接收的,因为User实体类中没有定义code,因此这里我们可以有两个方法:

  • 像我们之前一样定义一个Dto类(数据传输服务对象)让其继承User,然后在多定义一个code属性即可。
  • 使用Map对象来接收参数,这样参数和值之间就是使用key-vlue来存储的了。

这里笔者选择的是和黑马的一样选择了使用Map对象来接收。

/**
     * 移动端登录
     * @param map
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map<String,String> map, HttpSession session) {
        log.info("用户登录提交的手机号和验证码为:{}",map);

        //获取手机号
        String phone = map.get("phone");

        //获取验证码
        String code = map.get("code");

        //从redis中获取保存的验证码
        String cacheCode = validateCodeUtils.get(phone);
        log.info("手机号:{},在redis中的验证码为:{}",phone,cacheCode);

        if (cacheCode == null) {
            return R.error("验证码已过期!!!");
        }

        if (code.equals(cacheCode)) {
            //如果能够比对成功,说明登录成功
            //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
            LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
            lqw.eq(User::getPhone, phone);
            User user = userService.getOne(lqw);
            if (user == null) {
                user = new User();
                user.setPhone(phone);
                user.setName("新用户:"+phone);
                userService.save(user);
            }
            //将用户id保存到session中,供过滤器检测用户是否登录
            session.setAttribute("userId", user.getId());

            return R.success(user);
        }
        return R.error("登录失败,验证码错误!!!");
    }

​ 在里面进行登录的时候我们添加了一些业务判断,如果当前手机号不在数据库中user表中有存储,那么说明我们这个用户没有注册过,那么我们就帮其自动注册一个账户。

温馨提示

​ 使用session对象来存储固然简便,但是相对来说有些固定简略,因为往往在显示当中验证码是有时间限制的,不会长时间有效,并且保存在了session当中,也容易被人通过浏览器的开发者模式查看当前会话中存储的数据来获取验证码code。

​ 因此通常我们都是把验证码保存到了缓存中,并且通过设置缓存的保存时间来删除存储的数据来实现验证码的时效性。

代码开发(redis)

前期配置工作

​ 需要大家先自己去下载redis,windows版安装包下载地址:github.com/tporadowski…

​ 使用redis来缓存,那么首先我们需要导入maven坐标,因为springboot已经整合有redis缓存,所以我们不需要导入版本。

<!--Redis缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

​ 在App启动类上添加@EnableCaching注解开启缓存。

开启缓存注解

​ 在yml文件中添加相应的配置,里面一些配置,这里验证码的过期时间我设置的是600秒过期,也就是10分钟,小伙伴可以根据自己的需求来更改。

spring:
  #配置redis
  cache:
    type: redis
    redis:
      #是否用前缀
      use-key-prefix: false
      #指定key的前缀
#      key-prefix: sms_
      #是否缓存空值
      cache-null-values: false
      #设置生命周期
      time-to-live: 600s
  redis:
    host: localhost
    port: 6379
代码开发-修改ValidateCodeUtils

​ 因为我们使用redis缓存来存储验证码,因此我们需要对验证码工具类做一些调整,在ValidateCodeUtils类上添加@Component将其装入spring容器中管理,因为后面我们使用到的@CachePut和@Cacheable注解本质都是bean,既然是bean我们就需要将其转入容器中进行管理,以便后期的使用。

package com.huaishushu.Utils;

import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 * 随机生成验证码工具类
 */
@Component
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    @CachePut(value = "phoneCode",key = "#phone")
    public String generateValidateCode(int length,String phone){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code.toString();
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    @CachePut(value = "phoneCode",key = "#phone")
    public String generateValidateCode4String(int length,String phone){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }


    /**
     * 用来获取redis缓存中电话号码phone所对应的code验证码
     * @param phone
     * @return
     */
    @Cacheable(value = "phoneCode",key = "#phone")
    public String get(String phone) {
        return null;
    }
}

​ 我在验证码生成工具类中在生成验证码的方法上添加@CachePut,并设置其在redis中存储的空间为"phoneCode",其key为phone,vlue为该的方法放回置,并且在后面新定义了一个get方法来获取redis中存储验证码数据。

代码开发-获取验证码

​ 当我们在移动端登录页面输入手机号后点击获取验证码,可以看到前端页面发送的url,以及其携带的手机号参数,因此我们需要在Controller层中定义控制方法来接收传递过来的手机号参数,并生成验证码。

Controller层

/**
     * 移动端通过手机号获取验证码
     * @param user
     * @return
     */
    //@PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user) {
        //获取手机号
        String phone = user.getPhone();

        if (StringUtils.isNoneEmpty(phone)) {
            //生成随机的4位验证码,并且在生成验证码的同时以phone为key,存储了验证码code
            String code = validateCodeUtils.generateValidateCode(4,phone);
            log.info("验证码,code为:{}",code);

            //调用阿里云提供的短信API完成发送短信
            //SMSUtils.sendMessage("?","?",phone,code);

            return R.success("手机验证码短信发送成功");
        }

        return R.error("短信发送失败!!!");
    }

​ 因为这块不涉及到与数据库持久层的操作,也没有复杂的业务逻辑,因此直接在控制层进行方法调用并返回即可,SMSUtils.sendMessage这个方法就看各位小伙伴自己的条件了,如果有条件的在申请好签名以及模板后,将签名和模板分别填入即可。

​ 我们前面修改了ValidateCodeUtils类中的generateValidateCode方法,添加了一个String类型的phone参数就是为了就是为了以phone为key,验证码code为vlue来存储到redis中的。

​ 从下图我们可以看到里面存在了已手机号19716415316为key存储的数据了,并且其验证码code的值能正常保存,之后我们将其取出并对比即可。

redis中保存的数据

redis存储的验证码转存失败,建议直接上传图片文件
代码开发-用户登录

如图当我们输入手机和验证码后,前端页面发送的url请求为:http://localhost/user/login,并且请求的参数为json形式,里头有phone和code两个参数。

转存失败,建议直接上传图片文件

​ 因为黑马给的前端资料有些问题,这块可能有的小伙伴只有电话号码phone这个参数,并没有code这个参数,我们需要去static/front/page/login.html文件中78行左右添加或修改成这两段代码。

// 将手机号保存到浏览器的session
sessionStorage.setItem("userPhone",this.form.phone)
// 将移动端用户信息保存到浏览器的本地存储空间
localStorage.setItem('user',JSON.stringify(res.data))

Controller层

​ 因为前端发送的url请求中包含了phone和code两个参数,如果光用User类型的参数来接收是无法接收的,因为User实体类中没有定义code,因此这里我们可以有两个方法:

  • 像我们之前一样定义一个Dto类(数据传输服务对象)让其继承User,然后在多定义一个code属性即可。
  • 使用Map对象来接收参数,这样参数和值之间就是使用key-vlue来存储的了。

这里笔者选择的是和黑马的一样选择了使用Map对象来接收。

/**
     * 移动端登录
     * @param map
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map<String,String> map, HttpSession session) {
        log.info("用户登录提交的手机号和验证码为:{}",map);

        //获取手机号
        String phone = map.get("phone");

        //获取验证码
        String code = map.get("code");

        //从redis中获取保存的验证码
        String cacheCode = validateCodeUtils.get(phone);
        log.info("手机号:{},在redis中的验证码为:{}",phone,cacheCode);

        if (cacheCode == null) {
            return R.error("验证码已过期!!!");
        }

        if (code.equals(cacheCode)) {
            //如果能够比对成功,说明登录成功
            //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
            LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
            lqw.eq(User::getPhone, phone);
            User user = userService.getOne(lqw);
            if (user == null) {
                user = new User();
                user.setPhone(phone);
                userService.save(user);
            }
            //将用户id保存到session中,供过滤器检测用户是否登录
            session.setAttribute("userId", user.getId());

            return R.success(user);
        }
        return R.error("登录失败,验证码错误!!!");
    }

​ 在里面进行登录的时候我们添加了一些业务判断,如果当前手机号不在数据库中user表中有存储,那么说明我们这个用户没有注册过,那么我们就帮其自动注册一个账户。

邮箱接收验证码和发送验证码登录

​ 前面我们使用的是使用阿里云,短信服务的方式来发送验证码短信来获取验证码进而进行登录的,这个服务是需要花钱的,因此我们可以换一个方式,如果学习过springboot的小伙伴应该知道springboot整合了邮件的发送功能。

前期准备工作

导入maven坐标
<!--发送邮件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
yml文件

​ 在yml文件中设置必要的配置信息,这个是必须配置的

spring: 
  #设置邮箱
  mail:
    # 邮件传输的协议以及供应商的名称
    host: smtp.163.com
    # 邮箱号(用来发送验证码的邮箱号),填写自己的邮箱
    username: 
    # 密钥,填写自己的密钥
    password: 

​ 这里的密钥字不是咱们邮箱的密码,而是在邮箱中设置开启POP3/SMTP/IMAP服务后获取的密钥,将获取的密钥自己保存一份,这个密钥的作用就是可以从第三方平台访问使用邮箱的钥匙。

开启邮箱密钥的步骤1

​ 选择开启容易一项即可,然后发送短信给它,之后页面就会反馈一个密钥给你了,这个密钥只显示一次,所以要记得保存。

邮箱开启密钥步骤2

再再yml文件中最下方自定义一些我们自己到时要发送邮箱时的一些属性。

#自定义的邮箱数据
myMail:
  #发送人(我们自己的邮箱),此处替换成自己的就可以
  from: ************@163.com
  #接收人,此处替换成自己的就可以
  to: **********@qq.com
  #标题
  subject: 验证码
工具类

​ 在Utils文件夹下创建我们的发送邮箱的工具类SendMailUtils

/**
 * @Author HuaiShuShu
 * @Date 2023/5/23 15:49
 * @PackageName:com.itheima.service.impl
 * @Description: 发送邮件的工具类
 * @Version 1.0
 */
@Component
public class SendMailUtils {

    @Autowired
    private JavaMailSender javaMailSender;


    //发送人(我们自己的邮箱)
    @Value("${myMail.from}")
    private String from;

    //接收人
    @Value("${myMail.to}")
    private String to;

    //标题
    @Value("${myMail.subject}")
    private String subject;

    public void sendMail(String code) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from+"(吃饱饱坏蜀黍)");
        message.setTo(to);
        message.setSubject(subject);
        message.setText(code);

        javaMailSender.send(message);
    }
}

代码实现

​ 在UserController.sendMsg()方法中使用该方法,将生成的验证码传入到该方法中调用,之后邮箱就会调用该方法发送验证码。

/**
     * 移动端通过手机号获取验证码
     * @param user
     * @return
     */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user) {
        //获取手机号
        String phone = user.getPhone();

        if (StringUtils.isNoneEmpty(phone)) {
            //生成随机的4位验证码,并且在生成验证码的同时以phone为key,存储了验证码code
            String code = validateCodeUtils.generateValidateCode(4,phone);
            log.info("验证码,code为:{}",code);

            //使用邮箱发送验证码登录
            sendMailUtils.sendMail(code);

            return R.success("手机验证码短信发送成功");
        }

        return R.error("短信发送失败!!!");
    }

功能测试

邮箱发验证码功能测试1

邮箱发送验证码功能测试2

用户退出

​ 页面用户退出所发送的url请求。

用户退出url

​ 因为用户退出的业务逻辑比较简单,首先因为我们在过滤器中判断用户是否已经登录,依据的是用户的id是否有保存在session当中,而userId是我们在用户登录的时候保存进session当中的,因此我们要实现用户退出,删除掉session当中的userId即可。

Controller层

/**
     * 用户退出
     * @param session
     * @return
     */
    @PostMapping("loginout")
    public R<String> loginout(HttpSession session) {
        //删除session中保存的用户id
        session.removeAttribute("userId");
        return R.success("成功退出登录");
    }

功能测试

输入手机号,获得验证码

  • 移动端功能测试2

输入验证码

  • 移动端测试登录1

点击登录

  • 移动端功能测试3

移动端-地址簿模块

需求分析

​ 地址,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。

地址蒲需求分析

数据模型

​ 用户的地址信息会存储在address book表,即地址簿表中。具体表结构如下:

地址簿数据模型

构建代码类

功能代码清单:

  • 实体类 AddressBook (直接从课程资料中导入即可)
  • Mapper接口 AddressBookMapper
  • 业务层接口 AddressBookService
  • 业务层实现类 AddressBookServicelmpl
  • 控制层 AddressBookController (直接从课程资料中导入即可)

​ 和笔者一样通过MP的代码生成器生成的项目结构的小伙伴,可以直接有这些类和接口了,但是要在实体类中的公共字段部分添加@TableField,以及在is_delete属性上添加@TableLogic注解来实现逻辑删除,还需要把isDefault属性的类型改为Integer不然会影响后面地址在前端页面的显示,并且在之后的AddressBookController 中修改成@RequestMapping("/addressBook")。

​ 黑马提供的资料当中是缺少这些类的,但是可以在源码的day6中解压压缩包获取里面的类,也可以通过我我分享的网盘来获取,我的资料都是经过补全的,当然也可以直接复制下面的代码来生成,其他的控制层、业务层、持久层都是和前面的结构一样,大家自行创建就可以了。

/**
 * <p>
 * 地址管理
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@TableName("address_book")
@Data
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 收货人
     */
    private String consignee;

    /**
     * 性别 0 女 1 男
     */
    private Integer sex;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 省级区划编号
     */
    private String provinceCode;

    /**
     * 省级名称
     */
    private String provinceName;

    /**
     * 市级区划编号
     */
    private String cityCode;

    /**
     * 市级名称
     */
    private String cityName;

    /**
     * 区级区划编号
     */
    private String districtCode;

    /**
     * 区级名称
     */
    private String districtName;

    /**
     * 详细地址
     */
    private String detail;

    /**
     * 标签
     */
    private String label;

    /**
     * 默认 0 否 1是
     */
    private Integer isDefault;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    /**
     * 修改
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    /**
     * 是否删除
     */
    @TableLogic
    private Integer isDeleted;
}

业务实现

进入地址簿

  1. 进入地址簿步骤1

  2. 进入地址簿步骤2

代码开发-添加地址

需求分析

​ 当我们进入地址簿管理页面后,点击添加收货地址并后填写数据点击保存后,前端页面会发送url以及其携带的参数。

添加地址url

代码实现

​ 又需求分析后,我们可以知道当我们填写好数据后并点击保存地址时,会发送一个json类型的参数,并且我们可以发现参数中的所有属性均是AddressBook实体类中的属性,所以我们可以在Controller层定义一个控制方法其参数为AddressBook类型来接收前端传递的url。

Controller层

/**
 * <p>
 * 地址管理 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

    @Autowired
    private IAddressBookService addressBookService;

    /**
     * 添加收货地址
     * @param addressBook
     * @return
     */
    @PostMapping
    public R<String> addressBook(@RequestBody AddressBook addressBook) {
        addressBookService.addressBook(addressBook);
        return R.success("新增地址成功");
    }

}

Service接口

/**
 * <p>
 * 地址管理 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface IAddressBookService extends IService<AddressBook> {

    /**
     * 添加收货地址
     * @param addressBook
     * @return
     */
    void addressBook(AddressBook addressBook);
}

Service实现类

/**
 * <p>
 * 地址管理 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
@Slf4j
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements IAddressBookService {

    @Autowired
    private AddressBookMapper addressBookMapper;

    /**
     * 添加收货地址
     * @param addressBook
     * @return
     */
    @Override
    public void addressBook(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        int insert = addressBookMapper.insert(addressBook);
        if (insert <= 0) {
            throw new CustomException("添加新地址失败");
        }
        log.info("用户新增收货地址:{}",addressBook);
    }
}

​ 实现类这里要注意一下,因为从前端页面传递回来的参数中是没有userId的,所以我们要是使用之前定义的前面线程工程类,来给AddressBook变量赋值userId的值,实现逻辑和我们前面获取存储员工模块的功能一样。

功能测试

​ 填好数据并点击保存地址时,数据库中该表能够正常添加数据并保存。

添加地址功能测试

代码开发-显示当前用户的所有地址

需求分析

​ 当我们点击地址管理后,地址管理页面首先会发送url请求,来向服务器获取当前用户的所有地址并显示到当前页面中。

显示用户对应的url

代码实现

​ 虽然url中没有携带参数,但是我们在控制方法中还是定义一个AddressBook对象的参数,为我们以后做扩展可以用,比如模糊查询地址或者手机号什么的来查询地址。

Controller层

/**
     * 查询当前用户的所有地址
     * @param addressBook
     * @return
     */
    @GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook) {
        log.info("查询当前用户的所有地址,其id为:{}",BaseContext.getCurrentId());
        List<AddressBook> addressBooks = addressBookService.list(addressBook);

        return R.success(addressBooks);
    }

Service接口

/**
     * 查询当前用户的所有地址
     * @param addressBook
     * @return
     */
    List<AddressBook> list(AddressBook addressBook);

Service实现类

/**
     * 查询当前用户的所有地址
     * @param addressBook
     * @return
     */
    @Override
    public List<AddressBook> list(AddressBook addressBook) {
        LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
        //根据当前的用户id来查询
        lqw.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        lqw.orderByDesc(AddressBook::getUpdateTime);
        List<AddressBook> addressBooks = addressBookMapper.selectList(lqw);
        return addressBooks;
    }

​ 这里的实现类是根据当前登录用户的id来进行查询的,并且将查询的结果进行了降序排序。

功能测试

查询所有地址成功

代码开发-修改地址

需求分析

​ 当我们点击修改按钮进去到修改地址页面后,会有如下两次请求:

  1. 页面(front/page/address-edit.html)发送ajax请求,将当前地址的id传递给服务器,服务端接的控制方法接收到请求并处理后将当前地址的数据返回给前端页面。

    地址修改页面的数据回显

  2. 当我们修改好数据后,前端页面发送ajax请求,将填写好的数据已json的形式发送给服务器,后端的控制方法接收到后,将其更新到数据库中。

    保存修改地址url

代码实现-数据回显

​ 根据第一次页面的url请求,我们可以发现其是将地址的id保存到了请求路径当中,这个需要我们在控制方法中使用占位符和

Controller层

/**
     * 根据id来查询地址
     * 在修改地址页面中起到数据回显的作用
     * @param id
     * @return
     */
    @GetMapping("{id}")
    private R<AddressBook> get(@PathVariable("id") Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        if (addressBook != null) {
            return R.success(addressBook);
        }else {
            return R.error("没有这个地址id");
        }
    }

​ 因为数据回显较为简单,因此我们就直接写在控制层就可以了。

代码实现-修改地址保存

​ 通过需求分析我们可以知道其参数类型和添加地址时是一样的,因此我们还是定义一个AddressBook对象的参数来接收这些url当中的json参数,之后我们在将其更新到数据库即可,因为其用户id是不变,所以其控制方法也简单,直接根据实体类的id来更新修改即可。

Controller层

/**
     * 修改地址
     * @param addressBook
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody AddressBook addressBook) {
        boolean flag = addressBookService.updateById(addressBook);
        if (flag) {
            return R.success("修改地址成");
        } else {
            return R.error("修改地址失败!!!");
        }
    }
功能测试

​ 地址修改页面的数据回显成功

修改地址数据回显功能测试

修改地址成功

修改地址成功能测试

代码开发-设置默认地址

需求分析

​ 当我们在地址管理页面中点击设置成默认地址后,前端页面会发送ajax请求,其url请求如下图所示。

设置默认地址url

代码实现

​ 由需求分析可知,我们需要在控制层Controller中定义控制方法来接收该请求所携带的地址id,并根据该地址id来修改默认地址。

Controller层

/**
     * 修改默认地址
     * @param addressBook
     * @return
     */
    @PutMapping("/default")
    public R<String> setDefault(@RequestBody AddressBook addressBook) {
        addressBookService.setDefault(addressBook);
        return R.success("设置默认地址成功");
    }

Service接口

/**
     * 修改默认地址
     * @param addressBook
     * @return
     */
    void setDefault(AddressBook addressBook);

Service实现类

/**
     * 修改默认地址
     * @param addressBook
     * @return
     */
    @Transactional
    @Override
    public void setDefault(AddressBook addressBook) {
        //获取当前用户id
        addressBook.setUserId(BaseContext.getCurrentId());

        //先将当前用户的其他的地址都设置成不是默认的地址
        LambdaUpdateWrapper<AddressBook> luw = new LambdaUpdateWrapper<>();
        luw.eq(AddressBook::getUserId, addressBook.getUserId());
        luw.set(AddressBook::getIsDefault, 0);
        addressBookMapper.update(null, luw);

        //在将用户选择的当前地址id的地址设置成默认地址
        addressBook.setIsDefault(1);
        addressBookMapper.updateById(addressBook);

    }

​ 实现类中我们需要先获取到当前登录用户的id,在这之后实现该业功能的思路是可以先将当前用户的所有地址都设置为不是默认的地址先,之后在单独根据地址的id来设置地址的默认状态将其修改为默认地址。

功能测试
默认地址功能测试

移动端-菜品展示

需求分析

​ 用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示 选择规格 按钮,否则显示 + 按钮,当我们点击不同的分类信息,那么就是展示不同分类中的菜品信息,并且当我们点击选择规格后,显示选择口味窗口,并且点击在套餐的页面中点击套餐的图,可以查看套餐中的菜品信息。

菜品展示需求分析

前台显示套餐中的菜品信息需求分析

代码开发-梳理交互过程

在开发代码之前,需要梳理一下前端页面和服务端的交互过程:

  1. 页面(front/index.html)发送ajax请求,获取分类数据 (菜品分类和套餐分类)
  2. 页面发送aiax请求,获取第一个分类下的菜品或者套餐开发菜品展示功能。
  3. 点击套餐页面中的套餐图片时,发送ajax请求,获取套餐中的菜品详情,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。
解决分类列表和菜品显示问题

​ 因为获取分类数据的控制方法我们在前面的菜品添加业务的时候就已经完成了,这块我们就不用再去写了,而之所以我们登录进去后空白什么都不显示,是因为前端代码index.html的298行代码处,它同时调用了两个js的方法,并且要两个方法都成功相应它才会渲染页面。

获取分类的url请求:

  • 移动端页面的获取分类url

​ 获取购物车内商品信息的url:

  • 移动端首页获取购物车内商品的url

​ 因此我们可以先暂时的修改前端的代码,稍微修改一下main.js中的获取购物车内商品的集合的url请求,修改成31行,并启用,把30行注释掉,让cartListApi(data)这个方法去访问我们front下的一个cartData.json文件,这个文件黑马给的资料中没有,但是可以在其源码中获取,也可以在我提供给各位的资料中获取,或者在复制下面的代码自己创建一个cartData.json文件也是可以的。

移动端首页的url

修改移动端登录进去后,购物车的url

cartData.json文件

{"code":1,"msg":null,"data":[],"map":{}}

​ 此处的json文件就只是返回一个假数据,我们写死的,让我们在写菜品展示模块时方便调试和开发而已,之后还是会把main.js中的31行的代码注释掉恢复30行代码,恢复原样来开发购物车模块的。

温馨提示

​ 有的小伙伴修改了main.js和添加了cartData.json文件文件移动端页面首页还是不显示,这时候我们可以看一下移动端发送的请求是不是还是http://localhost/shoppingCart/list,如果还是那么我们需要清理一下浏览器的记录,因为main.js加载在了浏览器中,我们在项目中修改了并没有加载到浏览器,所以清理一下浏览器记录就可以了。

代码实现-展示菜品

​ 因为我们我们的菜品大多是设置有口味信息的,所以菜品信息那不应该只显示一个 + 号,而是应该显示选择规格这个按钮的,这是因为我们的前面带菜品菜品控制器中实现的时候,返回的类型是List,因为Dish实体类中并没有定义口味的属性,所以它没法显示,所以这块是我们需要修改的,我们可以将其改为DishDto,因为Dto中继承了dish的基本属性,并且还有DishFlavor口味的属性信息。

  • 移动端菜品展示dish的类型

  • 菜品展示功能测试

​ 因此我们需要先注释掉我们前面在Dish控制器、接口、实现类中写的list方法,并重新来编辑代码来实现即可,我们知道要使用DishDto后我们就可以大致来完成了。

前台菜品展示-注释掉list方法

Controller层

/**
     * 根据条件来查询菜品信息
     * 适用于前台和后台进行菜品展示
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish) {
        List<DishDto> dishDtoList = dishService.listByCategoryId(dish);
        return R.success(dishDtoList);
    }

Service接口

/**
     * 根据条件来查询菜品信息
     * 适用于前台和后台进行菜品展示
     * @param dish
     * @return
     */
    List<DishDto> listByCategoryId(Dish dish);

Service实现类

/**
     * 根据条件来查询菜品信息
     * 适用于前台和后台进行菜品展示
     * @param dish
     * @return
     */
    @Override
    public List<DishDto> listByCategoryId(Dish dish) {
        LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //查询状态为1的数据,就是起售的
        lqw.eq(Dish::getStatus, 1);
        //添加排序条件
        lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> dishList = dishMapper.selectList(lqw);

        //这块用的是stream().map(字节流加lmabe)
        List<DishDto> dishDtoList = dishList.stream().map((itme) -> {
            DishDto dishDto = new DishDto();
            //因为是新new出来的,里面什么数据都没有,因此需要拷贝赋值
            BeanUtils.copyProperties(itme, dishDto);
            Long categoryId = itme.getCategoryId();
            Category category = categoryService.getById(categoryId);
            //此处是因为当我们自己添加或通过sql文件导入菜品时并没有设置分类,所有可能是会为空的
            if (category != null) {
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            //先查询到当前菜品的口味信息
            Long dishId = itme.getId();
            LambdaQueryWrapper<DishFlavor> dishDtoLambdaQueryWrapper = new LambdaQueryWrapper<>();
            dishDtoLambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
            List<DishFlavor> dishFlavorList = dishFlavorService.list(dishDtoLambdaQueryWrapper);
            //将当前菜品的口味信息赋值给Dto中的口味集合。
            dishDto.setFlavors(dishFlavorList);
            return dishDto;
        }).collect(Collectors.toList());

        return dishDtoList;
    }

​ 此处实现类中前面查询到list部分都是和之前一样的,但是之后我们要想办法将dishList赋值给给dishDtoList,因为我们最后要返回的时List,而这块的实现方式我们前面其实已经写过很多了,比如我们在之前的菜品分页查询中,我们要想办法在后台的菜品管理页面中展示菜品的同时显示菜品的分类名称,所以我们可以参考这部分的代码:

  1. ​ 因此我们可以将前面菜品分页显示List给List部分的代码拷贝过来,并保留着部分代码,因为这部分只是将dish的基本信息拷贝给了dishDto,并且为dishDto赋值了分类名称,并不影响我们的代码逻辑。
  2. ​ 我们可以在该遍历中添加条件根据菜品id来查询dish_flavor表中该菜品的口味信息,并将其赋值给dishDto中的List flavors,这样我们的口味信息就能够逐一赋值了,之后返回List给前端页面即可。

​ 我们也不用担心前台页面实现了菜品展示,后台的菜品管理页面是否会收到影响,这个是不用担心的,因为dishDto继承了dish,所以在菜品页面中需要展示的属性信息,dishDto中都有,所以不会有影响。

功能测试

前台菜品展示

  • 前台菜品展示
  • 前台菜品展示口味信息

后台菜品管理页面

  • 后台菜品展示
代码实现-展示套餐菜品信息

​ 下图是前台中套餐显示菜品的的url其中携带了两个参数一个是分类信息的id(categoryId)一个是套餐的启用状态status,这个list中我们的SetmealController控制器类中并没有定义实现,因此我们需要定义一个控制方法去响应该url请求。

​ 注意:url请求中携带的两个参数并不是json形式的,因此不可用@RequestBody注解。

前台套餐菜品信息的显示

Controller层

/**
     * 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
     * 此处返回值类型是List<Setmeal>也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    public R<List<SetmealDto>> list(Setmeal setmeal) {
        log.info("根据条件来展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息,条件为:{}",setmeal);
        List<SetmealDto> setmealDtoList =  setmealService.listSetmealDto(setmeal);

        return R.success(setmealDtoList);
    }

Service接口

/**
     * 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
     * 此处返回值类型是List<Setmeal>也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
     * @param setmeal
     * @return
     */
    List<SetmealDto> listSetmealDto(Setmeal setmeal);

Service实现类

/**
     * 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
     * 此处返回值类型是List<Setmeal>也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
     * @param setmeal
     * @return
     */
    @Override
    public List<SetmealDto> listSetmealDto(Setmeal setmeal) {
        //先根据分类id来查询套餐的基本信息
        LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
        lqw.eq(Setmeal::getCategoryId, setmeal.getCategoryId());
        lqw.eq(Setmeal::getStatus, setmeal.getStatus());
        List<Setmeal> setmealList = setmealMapper.selectList(lqw);
        //开始为SetmealDto赋值
        List<SetmealDto> setmealDtoList = setmealList.stream().map(item -> {
            SetmealDto setmealDto = new SetmealDto();
            //将Setmeal的基本信息拷贝给setmealDto
            BeanUtils.copyProperties(item, setmealDto);
            //根据套餐的id来查询套餐中菜品的信息,并将其赋值给SetmealDto中的List<SetmealDish> setmealDishes
            Long setmealId = item.getId();
            LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
            setmealDishLambdaQueryWrapper.eq(SetmealDish::getSetmealId, setmealId);
            List<SetmealDish> setmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
            setmealDto.setSetmealDishes(setmealDishList);

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

        return setmealDtoList;
    }

​ 我们实现类返回的数据类型是List的类型,这块笔者一开始思考的套餐信息的展示中可能会涉及到套餐中菜品信息的展示,因此我在实现类中为SetmealDto对象赋值并返回对应的数据,虽然这块的套餐的展示没有涉及到菜品的信息展示,但是可以保留这块实现类,为后期的扩展做准备,比如在套餐管理页面中显示每个套餐关联的菜品信息。

功能测试
前台套餐信息的显示 前台套餐信息展示功能测试1
代码实现-点击套餐显示菜品详情

​ 当我们在前台点击套餐图片时,前端页面会发送以下的请求,我们可以看到其传输的路径中有一个套餐的id,因此我们在Controller层中应该定义使用占位符来接收这个id来响应其的需求。

点击套餐发送的url

​ 这里我们还需要分析一下我们定义方法后,返回给前端的是什么类型的数据,我们可以通过以下步骤:

  1. ​ 这里我们可以去D:\学习资料\学习\java\实战项目\瑞吉外卖项目\资料\产品原型\瑞吉外卖前台(用户端)\点餐-菜单.html的html中找到,可以看到其要显示菜品的图片以及价格和份数,这里我们很容易以为传输的类型为SetmealDto类型的数据,因为所有要求都符合,但是当我们去main.js通过响应的请求找到对应的数据后就可以发现不是了
  • 产品原型-套餐中的菜品

  • 获取套餐的全部菜品的js方法

  1. 在通过按Ctrl点击那个方法名,我们可以进入到首页的调用该方法的位置。
  • 首页中获取套餐菜品的位置
  1. 之后通过Ctrl+F查找setMealDialog差不多在196行的位置可以看到有一个描述的属性,这个是只有菜品才会有的。
  • 前台套餐菜品描述的位置

​ 因此我们确定了返回DishDto的数据先,但是又可以看到185行有一个list,说明需要遍历集合的,进而可以确定Controller层返回的是一个List类型的数据了。

Controller层

/**
     * 前台查看套餐信息中的菜品信息
     * 根据套餐id来查询菜品的信息
     * @param setmealId
     * @return
     */
    @GetMapping("/dish/{SetmealId}")
    public R<List<DishDto>> getListDishDtoById(@PathVariable("SetmealId") Long setmealId) {
        log.info("根据套餐id来查询菜品的信息,ID为:{}",setmealId);
        List<DishDto> dishDtoList = setmealService.getListDishDtoById(setmealId);

        return R.success(dishDtoList);
    }

Service接口

/**
     * 前台查看套餐信息中的菜品信息
     * 根据套餐id来查询菜品的信息
     * @param setmealId
     * @return
     */
    List<DishDto> getListDishDtoById(Long setmealId);

Service实现类

/**
     * 前台查看套餐信息中的菜品信息
     * 根据套餐id来查询菜品的信息
     * @param setmealId
     * @return
     */
    @Override
    public List<DishDto> getListDishDtoById(Long setmealId) {
        //查询当前套餐中所对应的菜品信息(setmeal_dish表)
        LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SetmealDish::getSetmealId, setmealId);
        List<SetmealDish> setmealDishList = setmealDishService.list(lqw);

        List<DishDto> dishDtoList = setmealDishList.stream().map(item -> {
            DishDto dishDto = new DishDto();
            //通过菜品id查询基本的菜品信息
            String dishId = item.getDishId();
            Dish dish = dishService.getById(dishId);
            BeanUtils.copyProperties(dish, dishDto);
            //将套餐中菜品所对应的份数赋值给Dto
            dishDto.setCopies(item.getCopies());

            //查询菜品所对应的口味信息并赋值给Dto
            LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
            dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
            List<DishFlavor> dishFlavorList = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
            dishDto.setFlavors(dishFlavorList);

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

        return dishDtoList;
    }

​ 实现类的业务逻辑是我们通过传递的套餐id去setmeal_dish表中查找菜品的list集合信息,之后通过遍历该集合,并在集合中通过菜品id去Dish表中获取Dish对象,并将Dish对象赋值给DishDto,之后在通过菜品id来查询菜品所对应的口味信息,之后将口味信息也赋值给Dto。

功能测试

移动端-购物车

需求分析

​ 移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击 + 将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量也可以清空购物车。

购物车需求分析

数据模型

​ 购物车对应的数据表为shopping_cart表,具体表结构如下:

购物车数据模型

代码开发

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

  1. 点击加入购物车按钮或者+ 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车加入购物车
  2. 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
  3. 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

​ 开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可

代码开发-准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类 shoppingCart (直接从课程资料中导入即可)
  • Mapper接口 ShoppingCartMapper
  • 业务层接口 shoppingCartService
  • 业务层实现类 ShoppingCartServicelmpl
  • 控制层 shoppingCartController

​ 使用MP代码生成器的小伙伴将shoppingCartController的@RequestMapping修改成@RequestMapping(/shoppingCart)即可。

​ 如果是自己手敲项目结构的,那么其他层的类按照之前的形式创建即可。

代码实现-添加购物车

​ 如需求分析中分析,当我们点击加入购物车或者点击 + 号将菜品、套餐加入购物车时,页面发送的url请求的结构都相同,唯一不同的就是里面的参数而已。

菜品:

  • 菜品添加入购物车url

套餐:

  • 套餐添加入购物车url

​ 可以看到二者请求响应的控制方法时一样的,只是菜品的请求参数中又口味和菜品id,套餐中有套餐id,只是参数不同罢了,因此我们都可以使用ShoppingCart的形参来接收。

Controller层

/**
     * 添加购物车
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
        ShoppingCart cart = shoppingCartService.add(shoppingCart);

        return R.success(cart);
    }

Service接口

/**
     * 添加购物车
     * @param shoppingCart
     * @return
     */
    ShoppingCart add(ShoppingCart shoppingCart);

Service实现类

/**
 * <p>
 * 购物车 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Slf4j
@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements IShoppingCartService {

    @Autowired
    private ShoppingCartMapper shoppingCartMapper;

    /**
     * 存储上一个菜品的口味,
     * 用来辅助菜品添加购物车,
     * 实现连续添加同一口味的菜品
     */
    private String dishFlavor;


    /**
     * 添加购物车
     * @param shoppingCart
     * @return
     */
    @Override
    public ShoppingCart add(ShoppingCart shoppingCart) {
        //获取当前用户的id,并为其赋值
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);
        //设置创建时间为当前时间
        shoppingCart.setCreateTime(LocalDateTime.now());
        //当时选择规格按钮添加菜品的时候,则必然会有口味,则这时更新口味
        if (shoppingCart.getDishFlavor() != null) {
            dishFlavor = shoppingCart.getDishFlavor();
        }
        //如果当前加入购物车的是个菜品,并且没有口味,说明目前调用的是连续添加同一口味的菜品
        if (shoppingCart.getDishId() != null && shoppingCart.getDishFlavor() == null) {
            //将选择规格时选择的口味赋值给它
            shoppingCart.setDishFlavor(dishFlavor);
        }

        //添加之前先查询当前口味的菜品或套餐是否存在于购物车
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(shoppingCart.getSetmealId() != null, ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
        lqw.eq(shoppingCart.getDishId() != null, ShoppingCart::getDishId, shoppingCart.getDishId());
        lqw.eq(ShoppingCart::getUserId, userId);
        lqw.eq(StringUtils.isNoneEmpty(shoppingCart.getDishFlavor()), ShoppingCart::getDishFlavor, shoppingCart.getDishFlavor());

        ShoppingCart cart = shoppingCartMapper.selectOne(lqw);

        //判断是否存在购物车
        if (cart != null) {
            //存在购物车则数量+1
            Integer number = cart.getNumber();
            cart.setNumber(number + 1);
            shoppingCartMapper.updateById(cart);
        } else {
            //不存在购物车,则将商品的数量设置为1
            shoppingCart.setNumber(1);
            shoppingCartMapper.insert(shoppingCart);

            cart = shoppingCart;
        }
        return cart;
    }
}

​ 这处实现类,我们的核心业务逻辑是判断添加的商品它是否存在,而判断是否存在的依据是:

  • 菜品是否存在,存在的话,口味是否相同,相同数量加一,不相同则将其当作新商品加入购物车
  • 套餐是否存在,存在的话数量加一,不存在则当做新商品加入购物车

​ 因此整体的业务逻辑:

  1. 定义一个成员变量来存储上一次通过选择规格按钮,选择口味的业务操作添加菜品进入购物车的菜品口味。
  2. 设置用户的id和时间(时间为可选设置)。
  3. 判断当前加入购物车的商品中的ShoppingCart对象是否携带有口味这个属性,如果有的话则说明一定时通过选择规格按钮添加的商品进入购物车,并且必然时菜品,因为套餐是没有口味的。
  4. 判断当前加入购物车的是否为菜品,并且还没有口味的属性,如果是,则说明这必然是通过连续添加同一个口味的菜品进购物车的业务操作,因此该ShoppingCart对象添加口味属性,使用的判断条件为是否拥有DishId属性以及DishFlavor口味属性。
  5. 通过MP的条件构造器来添加条件,通过判断参数中的 dishId和SetmealId的参数是否为空即可查询到其为菜品还是套餐了,又因为我们该添加了口味的条件,所以就可以确定菜品的唯一值了,当然前提条件是当前用户的购物车,所以还要设置用户的id为条件来查询。
  6. 通过判断查询结果cart是否为空来实现对购物车中是否有该商品的判断。
  7. 通过判断分支来进行不同的业务逻辑操作。
功能测试
添加购物车功能测试1

添加购物车功能测试2

代码实现-购物车内商品展示

​ 要实现我们的查看购物车商品之前要去main.js文件中将31行注释掉,恢复30行的url,这是我们前面在实现前台菜品展示时修改了这部分的url请求,现在我们要实现购物车的商品展示了所以要恢复这块内容了,修改了js文件之后要记得清除以下浏览器的缓存,不然刚修改的js不易生效。

购物车内商品展示js

​ 当我们进去到移动端页面时或者点击购物车图标时,其会发送以下的url请求给服务器来获取购物车内的商品信息,因此我们只需要在控制层实现控制方法来响应其即可。

购物车展示商品的url

Controller层

/**
     * 查询当前用户购物车内的商品信息
     * @return
     */
    @GetMapping("list")
    public R<List<ShoppingCart>> list() {
        log.info("查询id为:{}  的用户购物车内的商品信息",BaseContext.getCurrentId());
        List<ShoppingCart> shoppingCartList = shoppingCartService.listShoppingCart();

        return R.success(shoppingCartList);
    }

Service接口

/**
     * 查询当前用户购物车内的商品信息
     * @return
     */
    List<ShoppingCart> listShoppingCart();

Service实现类

/**
     * 查询当前用户购物车内的商品信息
     * @return
     */
    @Override
    public List<ShoppingCart> listShoppingCart() {
        //获取当前用户的id
        Long userId = BaseContext.getCurrentId();
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(ShoppingCart::getUserId, userId);
        //按添加时间来个降序
        lqw.orderByDesc(ShoppingCart::getCreateTime);
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.selectList(lqw);

        return shoppingCartList;
    }

​ 整体的实现逻辑比较简单,通过线程工具类来获取当前登录用户的id,之后通过MP的条件构造器来添加条件查询并返回值即可。

温馨提示

  1. ​ 当我们实现了相应的方法后,我们去前台可以看到当前用户购物车内的商品信息时,但是当我们添加不同口味的同样菜品时,会显示同样的名称只显示一个数量不同,无法分辨口味的数量,因此我们可以在index.html的127行左右添加下面这行语句。
<div class="dishFlavor">{{item.dishFlavor}}</div>

购物车内的菜品显示口味

  1. 当我们实现在购物车内显示菜品信息后,在菜单中添加过的菜品不能在选择规格了,这个我们只需要修改index.html的67行左右的加号按钮所绑定的点击函数即可,将其修改为以下代码,这样我们在点击 + 按钮时就弹出口味选择框了,可以选择口味并加入购物车。

    <img src="./images/add.png" @click.prevent.stop="chooseFlavorClick(item)"/>
    

    按加号按钮进入规格框转存失败,建议直接上传图片文件

功能测试
购物车内商品信息显示功能测试

代码实现-减少购物车内商品的数量

​ 这块功能的实现笔者这边遇到了困难,在点击 - 按钮时候前端发送url请求只有一个菜品id或者商品id,并没有携带口味这个属性,因为笔者前面实现的过程中考虑了口味这块的属性不同口味的菜品算作了一个新的商品加入购物车。

​ 所以在减少购物车内菜品数量的时候需要传递口味和id一起后端才能知道要对哪个菜品进行减少更新操作,因此缺少了口味属性的参数,笔者是后端的,所以实在没想到什么好的解决方式。

  • 购物车-菜品数量减少url

  • 购物车-套餐数量减少url

移动端-订单模块

数据模型

用户下单业务对应的数据表为orders表和order detail表

orders: 订单表

  • 订单表数据模型

order_detail: 订单明细表

  • 订单详情表数据模型

代码开发-用户下单

需求分析

​ 移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点去 “去支付” 按钮则完成下单操作。

用户下单的需求分析

​ 我们暂时只做到订单详情表这块了,因为支付功能的开发,需要我们去支付宝获取微信申请相关的资质的。

代码开发-梳理交互过程

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程

1、在购物车中点击 “去结算” 按钮,页面跳转到订单确认页面去结算。

2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址。

获取登录用户的默认地址url

3、在订单确认页面,发送aiax请求,请求服务端获取当前登录用户的购物车数据。

订单-获取购物车内所有商品信息url

4、在订单确认页面点击 “去支付”按钮,发送ajax请求,请求服务端完成下单操作。

去支付的url

开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可

代码实现-获取当前登录用户的默认地址

​ 当我们点击去结算进入订单确认页面后,前端页面发送两个url请求:

  1. 一个是http://localhost/shoppingCart/list,获取当前购物车内所有的商品信息,其对应的是代码梳理交互中的步骤三

  1. 一个是http://localhost/addressBook/default,获取登录用户的默认地址,其对应的是梳理交互过程中的步骤二

​ 因此我们在地址对应的控制层和业务层当中实现具体的控制方法,响应其url请求。

Controller层

/**
     * 查询默认地址
     */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
        //获取登录用户的id
        Long userId = BaseContext.getCurrentId();
        LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
        lqw.eq(AddressBook::getUserId, userId);
        lqw.eq(AddressBook::getIsDefault, 1);

        AddressBook addressBook = addressBookService.getOne(lqw);

        if (addressBook == null) {
            return R.error("没有找到该对象");
        } else {
            return R.success(addressBook);
        }
    }

​ 因为实现比较简单而且也没有什么参数,就是获取当前用户id,然后使用MP的条件构造器来设置用户id以及默认地址的条件,进行查询,之后进行判断该地址对象是否为空即可。

​ 因此较为简单,而且不复杂,就直接写在了Controller层了。

代码开发-添加订单(去支付)

准备工作

在实现业务功能前,先将需要用到的类和接口基本结构创建好

  • 实体类 Orders、OrderDetail (直接从课程资料中导入即可)
  • Mapper接口 OrderMapper、OrderDetailMapper
  • 业务层接口 OrderService、OrderDetailService
  • 业务层实现类 OrderServicelmpl、OrderDetailServicelmpl
  • 控制层 OrderController、OrderDetailController

​ 使用MP的小伙伴代码生成器的小伙伴需要修改OrderDetailController的为@RequestMapping("/orderDetail")

代码实现

​ 当我们点击 “去支付” 按钮后,前端页面会发送一个url请求给服务器,并且里面携带有addressBookId(地址id)、payMethod(支付方式)、remark(备注),这些都是Orders实体类中的属性,因此我们在控制方法中可以使用Orders对象来接收这些参数。

Controller层

/**
 * <p>
 * 订单表 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Slf4j
@RestController
@RequestMapping("/order")
public class OrdersController {

    @Autowired
    private IOrdersService ordersService;

    /**
     * 用户下单
     * 操作两张表,一个订单表orders,一张订单详情表order_detail
     * @param orders
     * @return
     */
    @PostMapping("/submit")
    public R<String> submit(@RequestBody Orders orders) {
        log.info("添加订单:{}",orders);
        ordersService.submit(orders);

        return R.success("用户下单成功");
    }


}

Service接口

/**
 * <p>
 * 订单表 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface IOrdersService extends IService<Orders> {

    /**
     * 添加订单
     * 操作两张表,一个订单表orders,一张订单详情表order_detail
     * @param orders
     * @return
     */
    void submit(Orders orders);
}

Service实现类

/**
 * <p>
 * 订单表 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersService {


    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private IOrderDetailService orderDetailService;

    @Autowired
    private IUserService userService;

    @Autowired
    private IAddressBookService addressBookService;

    @Autowired
    private IShoppingCartService shoppingCartService;

    @Override
    @Transactional
    public void submit(Orders orders) {

        //获取当前的用户信息
        Long userId = BaseContext.getCurrentId();
        User user = userService.getById(userId);

        //获取当前的地址信息
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if (addressBook == null) {
            throw new CustomException("用户地址有误,无法下单,请检查地址是否正确!!!");
        }

        //获取登录用户购物车内的商品信息
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(ShoppingCart::getUserId, userId);
        List<ShoppingCart> shoppingCartList = shoppingCartService.list(lqw);
        if (shoppingCartList == null && shoppingCartList.size() == 0) {
            throw new CustomException("购物车为空,无法下单!!!");
        }

        //总金额,AtomicInteger类型不用担心线程的问题。
        AtomicInteger amount = new AtomicInteger(0);
        //订单号
        long orderId = IdWorker.getId();

        //为订单详情表的对象赋值,进而获得List<OrderDetail>,用来给订单详情表插入数据
        List<OrderDetail> orderDetailList = shoppingCartList.stream().map(item -> {
            OrderDetail orderDetail = new OrderDetail();
            //订单号
            orderDetail.setOrderId(orderId);
            //商品数
            orderDetail.setNumber(item.getNumber());
            //商品口味
            orderDetail.setDishFlavor(item.getDishFlavor());
            //菜品id
            orderDetail.setDishId(item.getDishId());
            //套餐id
            orderDetail.setSetmealId(item.getSetmealId());
            //菜品或商品名称
            orderDetail.setName(item.getName());
            //商品图片
            orderDetail.setImage(item.getImage());
            //商品单价
            orderDetail.setAmount(item.getAmount());
            //计算总金额,multiply(作为为相乘)
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());

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


        //为订单对象Orders orders赋值,为插入订单表做准备
        orders.setId(orderId);
        orders.setNumber(String.valueOf(orderId));
        //创建时间
        orders.setOrderTime(LocalDateTime.now());
        //付款时间
        orders.setCheckoutTime(LocalDateTime.now());
        //支付状态
        orders.setStatus(2);
        //总金额
        orders.setAmount(new BigDecimal(amount.get()));
        //用户id(用户)
        orders.setUserId(userId);
        //用户名称
        orders.setUserName(user.getName());
        //收货人(地址)
        orders.setConsignee(addressBook.getConsignee());
        //收货人电话
        orders.setPhone(addressBook.getPhone ());
        //收货地址(省名称+市名称+区名称+详细地址)
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName ())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName () == null ? "" : addressBook.getDistrictName())
                +(addressBook.getDetail() == null ? "" : addressBook.getDetail()));

        //向订单表内插入数据,一条
        ordersMapper.insert(orders);

        //向订单详情表插入数据,多条
        orderDetailService.saveBatch(orderDetailList);

        //清空登录用户的购物车
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);

    }
}

​ 这块实现类的代码较长,但是整体的业务逻辑并不复杂,总的可以分为以下几步:

  1. 获取当前登录用户的用户信息
  2. 获得当前的地址信息(根据order参数传递过来的地址id获取)。
  3. 获取当前用户购物车内的商品信息。
  4. 使用AtomicInteger原子操作类,定义用来存储总金额的变量,原因为:对于Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。num++解析为num=num+1,明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题。
  5. 使用MP的IdWorker类(id工具类)来生成订单id。
  6. 使用stream流来变量获取的List对象,并为新创建的OrderDetail对象赋值,最后收集并赋值给List对象,为之后的订单详情表插入做准备。
  7. 在stream流遍历的同时,累加计算购物车内总的商品金额。
  8. 为形参Order对象其余属性赋值,为之后的订单表插入数据做准备。
  9. 向订单表插入数据。
  10. 向订单详情表插入数据。
  11. 清空购物车,根据当前登录用户的id来删除就行了。

​ 总体逻辑并不复杂,就是比较繁琐,细心就基本可以完成了。

功能测试

​ 点击去支付后,跳转到下单成功界面。

前台下单功能测试

代码开发-查看订单记录

需求分析

​ 当我们完成支付的时候可以点击查看订单、或者在用户首页中点击查看历史订单进入订单页面,又或者当我们进入用户首页的时候其会显示时间最新的一条订单信息,这时候可能就有小伙伴们说不知道咱们这个订单需要显示一些什么消息,后端该返回什么信息给前端,我们可以去到产品原型中找到对应的产品需求概述。

​ 我们可以看到移动端中我们显示我的订单时,每条对应的订单中要显示订单中的详情信息,这些时OrderDetail实体类中的属性,而上面的订单时间、支付状态、总金额等是Order实体类的数据。

因此我们应该思考到我们需要使用到关于订单的Dto了。

移动端订单的产品原型

移动端查看订单需求分析

准备工作

​ 在Dto文件夹中新建一个Dto的类,其命名为OrdersDto,其代码如下:

/**
 * @Author HuaiShuShu
 * @Date 2023/6/7 15:49
 * @PackageName:com.huaishushu.dto
 * @Description: 订单和订单详情中的菜品信息的显示
 * @Version 1.0
 */
@Data
public class OrdersDto extends Orders {

    private List<OrderDetail> orderDetails;

}

代码梳理

  1. 当我们进入到我的订单页面时,会发送以下url移动端我的订单url1

  2. 当我们进入用户首页时,其会发发送以下url移动端订单分页url2

​ 从中我们分析发现其调用的方法都是一样的,只是参数不同,一个是分页显示显示5条信息,一个是只显示一条。

​ 因此我们在Controller层实现相关控制方法来响应其即可。

Controller层

/**
     * 查询当前用户的订单,并进行分页
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/userPage")
    public R<Page> userPage(Integer page, Integer pageSize) {
        Page<OrdersDto> ordersDtoPage = ordersService.userPage(page, pageSize);
        log.info("移动端拥有查看订单的分页信息,{}",ordersDtoPage.getRecords());
        return R.success(ordersDtoPage);
    }

Service接口

/**
     * 查询当前用户的订单,并进行分页
     * @param page
     * @param pageSize
     * @return
     */
    Page<OrdersDto> userPage(Integer page, Integer pageSize);

Service实现类

/**
     * 查询当前用户的订单,并进行分页
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    public Page<OrdersDto> userPage(Integer page, Integer pageSize) {
        //获取当前用户的id
        Long userId = BaseContext.getCurrentId();

        //创建Orders的分页构造器
        Page<Orders> ordersPage = new Page<>(page, pageSize);
        LambdaQueryWrapper<Orders> lqw = new LambdaQueryWrapper<>();
        lqw.eq(Orders::getUserId, userId);
        //添加排序,使支付状态进度不高的排在前面,之后根据下单时间排序
        lqw.orderByAsc(Orders::getStatus).orderByDesc(Orders::getOrderTime);
        ordersMapper.selectPage(ordersPage, lqw);

        //构建OrdersDto的分页构造器
        Page<OrdersDto> ordersDtoPage = new Page<>();
        //对象拷贝,但不拷贝“records”
        BeanUtils.copyProperties(ordersPage, ordersDtoPage, "records");

        List<Orders> ordersList = ordersPage.getRecords();
        List<OrdersDto> ordersDtoList = ordersList.stream().map(item -> {
            OrdersDto ordersDto = new OrdersDto();
            //因为ordersDto是新new出来的,其属性为空值,因此需要为其赋值
            BeanUtils.copyProperties(item, ordersDto);
            //查询订单的详细信息并为其Dto赋值
            LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(OrderDetail::getOrderId, item.getNumber());
            List<OrderDetail> orderDetailList = orderDetailService.list(lambdaQueryWrapper);
            ordersDto.setOrderDetails(orderDetailList);

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

        ordersDtoPage.setRecords(ordersDtoList);

        return ordersDtoPage;
    }

​ 实现类中的业务处理逻辑和之前后台菜品分页查询,分类信息分页查询那些逻辑差不多,整体的思路如下:

  1. 因为查看的是我的订单,所以先获取当前登录用户的id
  2. 构建泛型为Order的分页构造器ordersPage,并根据用户userId来查询分页数据
  3. 构建泛型为OrdersDto的分页构造器ordersDtoPage,使用对象拷贝工具BeanUtils类将ordersPage除 “records” 以外的属性全部拷贝给ordersDtoPage
  4. 从ordersPage中取出records得ordersList,进行steam流的遍历,在遍历内新构建一个OrdersDto对象ordersDto,此时的ordersDto内的属性是为空的,因为遍历的遍历item为订单表中每个订单信息,所以使用BeanUtils将item拷贝给ordersDto,此时的ordersDto就具备订单的基本信息了,但是还是缺少订单详情中的商品信息
  5. 通过条件构造器并已订单订单号为条件来进行查询,得到的List对象orderDetailList就包含了当前订单的商品详情信息,之后将orderDetailList赋值给ordersDto中的List
  6. 将steam遍历得到的数据收集成List数据赋值给ordersDtoList,最后返回即可。

功能测试

移动端订单分页查询功能测试 移动端订单分页查询功能测试1

代码开发-再来一单

需求分析

​ 当我们在订单页面点击再来一单时,系统自动帮我们点击订单中的信息,加入购物车,并实现跳转到菜单页面。

再来一单需求分析总和

代码实现

​ 当我们点击再来一单的时候,前端页面会发送以下的url,其中携带了订单的id。

再来一单url

​ 因此我们需要根据这个订单id,来完成 “再来一单” 的业务逻辑开发。

Controller层

/**
     * 再来一单
     * @param orders
     * @return
     */
    @PostMapping("/again")
    public R<String> again(@RequestBody Orders orders) {
        log.info("再来一单的订单id:{}",orders.getId());
        ordersService.again(orders);

        return R.success("再来一单成功");
    }

Service接口

/**
     * 再来一单
     * @param orders
     * @return
     */
    void again(Orders orders);

Service实现类

/**
     * 再来一单
     * @param orders
     * @return
     */
    @Override
    @Transactional
    public void again(Orders orders) {
        //获取用户id
        Long userId = BaseContext.getCurrentId();

        //查询订单详情中的商品信息
        LambdaQueryWrapper<OrderDetail> lqw = new LambdaQueryWrapper<>();
        lqw.eq(OrderDetail::getOrderId, orders.getId());
        List<OrderDetail> orderDetailList = orderDetailService.list(lqw);
        if (orderDetailList == null || orderDetailList.size() == 0) {
            throw new RuntimeException("当前订单有误,请刷新页面!!!");
        }

        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(item -> {
            //构建购物车对象
            ShoppingCart shoppingCart = new ShoppingCart();
            //给购物车对象赋值
            //菜品、套餐名称
            shoppingCart.setName(item.getName());
            //图片
            shoppingCart.setImage(item.getImage());
            //用户id
            shoppingCart.setUserId(userId);
            //菜品id
            shoppingCart.setDishId(item.getDishId());
            //套餐id
            shoppingCart.setSetmealId(item.getSetmealId());
            //口味
            shoppingCart.setDishFlavor(item.getDishFlavor());
            //数量
            shoppingCart.setNumber(item.getNumber());
            //金额
            shoppingCart.setAmount(item.getAmount());
            //创建时间
            shoppingCart.setCreateTime(LocalDateTime.now());

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

        //将购物车对象插入到数据库中
        shoppingCartService.saveBatch(shoppingCartList);
    }

​ 整体的业务逻辑比较简单,就是先根据订单id去查询订单详细表,之后得到List orderDetailList对象,之后遍历并给购物车ShoppingCart对象赋值就可以了,最后将steam得到的数据收集并赋值给List orderDetailList,最后将其批量插入数据库shopping_cart表即可。

订单模块

订单的分页查询

需求分析

​ 我们可以从产品原型中查看到后台的订单明细的功能要求,以及数据显示,并且还要去能够根据订单号、日期等进行订单的查询。

后台订单需求分析

​ 我们进入订单明细管理页面后会发送url请求给服务器进行分页查询的显示,并且当我们输入订单号以及选择日期范围后会将订单号以及开始时间和终止时间作为参数一起传递给服务器。

后台订单的分页查询需求分析

代码开发

​ 因此我们只需要在Orders订单所对应的控制层和业务层中实现相关的方法来响应,因为其传递的传递的参数中有开始时间和终止时间,这两个参数在其他实体类中都是不存在的。

​ 所以这块的控制方法我们可以直接在控制方法的形参部分定义同名的参数即可,这块的两个世界参数的类型定义为字符串的类型,订单号也是。

Controller层

/**
     * 后台订单的分页查询
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime 开始时间(下单)
     * @param endTime   终止时间(下单)
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pageSize,String number, String beginTime, String endTime) {
        log.info("订单查询:开始时间和结束时间:{}·······{}", beginTime, endTime);

        Page<Orders> ordersPage = ordersService.page(page, pageSize, number, beginTime, endTime);

        return R.success(ordersPage);
    }

Service接口

/**
     * 后台订单的分页查询
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime 开始时间(下单)
     * @param endTime   终止时间(下单)
     * @return
     */
    Page<Orders> page(Integer page, Integer pageSize,String number, String beginTime, String endTime);

Service实现类

/**
     * 后台订单的分页查询
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime 开始时间(下单)
     * @param endTime   终止时间(下单)
     * @return
     */
    @Override
    public Page<Orders> page(Integer page, Integer pageSize,String number, String beginTime, String endTime) {

        //条件查询构造器
        LambdaQueryWrapper<Orders> lqw = new LambdaQueryWrapper<>();
        lqw.between(StringUtils.isNotEmpty(beginTime) && StringUtils.isNotEmpty(endTime), Orders::getOrderTime, beginTime, beginTime);
        lqw.like(StringUtils.isNotEmpty(number), Orders::getNumber, number);
        //分页查询构造器
        Page<Orders> ordersPage = new Page<>(page, pageSize);
        ordersMapper.selectPage(ordersPage, lqw);

        return ordersPage;
    }

功能测试

订单明细功能测试

修改订单状态

需求分析

​ 当我们点击派送、完成后,并点击确认后,前端页面发送url请求给服务器,其中携带了两个参数一个是订单id和状态status的参数,这应为修改后的状态了。

订单派送需求分析

订单状态修改需求分析 订单状态修改url2

代码开发

​ 因为url中携带的两个参数均在order实体类中有定义,因此我们可以使用Order对象来作为参数来接收响应。

​ 因为整体的业务实现过程比较简单,就是单纯的根据订单id来修改订单的状态,因此我就写在控制层了。

Controller层

/**
     * 修改订单状态
     * @param orders
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Orders orders) {
        //log.info("修改订单状态:{}", orders);

        LambdaUpdateWrapper<Orders> luw = new LambdaUpdateWrapper<>();
        luw.set(Orders::getStatus, orders.getStatus());
        luw.eq(Orders::getNumber, orders.getId());
        boolean flag = ordersService.update(luw);
        if (flag) {
            return R.success("修改成功");
        }

        return R.error("修改订单状态失败!!!");
    }

功能测试

修改订单状态功能测试

可能遇到的问题

乱码问题

​ 有的小伙伴在项目开发过程中会遇到乱码问题,这个情况我们只需要在yml文件中添加字符编码的设置即可(UTF-8)

server:
  port: 80
  servlet:
    # 实现编码统一
    encoding:
      charset: utf-8
      enabled: true
      force: true

bean循环内嵌

​ 在这处业务开发中,有的小伙伴可能会遇到以下的报错提示,这里的报错信息显示:

	不鼓励依赖循环引用,默认情况下禁止使用循环引用。更新应用程序以删除 Bean 之间的依赖循环。作为最后的手段,可以通过将spring.main.allow-circular-references设置为true来自动打破循环。


解决方法:

​ 前面的报错信息也提示我们了,所以我们在yml文件中以下属性即可

spring:
	main:
		allow-circular-references: true