IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 2021-11-10 谷粒学院技术总结-前台 -> 正文阅读

[JavaScript知识库]2021-11-10 谷粒学院技术总结-前台

目录

一、前台首页显示

1、Banner

2、首页讲师课程

二、登录

1、JWT+token实现单点登录

?1、JWT

2、JWT的原则

3、JWT使用

2、单点登录

3、腾讯云短信注册/手机号登录

1、手机号登录

2、注册

三、课程详情页

1、课程购买模块

2、视频播放模块

3、课程评论模块

评论显示

添加评论

初始化评论区用户头像

4、其他课程基本信息显示模块

5、全部课程模块

四、课程微信支付功能

1、购买按钮跳转

2、创建订单方法

3、进入支付页面

4、监听支付状态



前台系统使用基于Vue的Nuxt.js轻量级服务端渲染框架开发

一、前台首页显示

1、Banner

利用SpringCache做缓存,本质上是利用Rdis做缓存

首先配置一个配置类

@Configuration
@EnableCaching//开启缓存
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

}

配置reidsTemplate,设置它的序列化方式,主要就是配置redis的序列化方式,默认SpringCache的序列化方式是jdk序列化,详情见下面链接中SpringCache部分2021-09-30 商城分布式高级篇技术总结_Young的博客-CSDN博客

2、首页讲师课程

这里没有用到缓存,因为可能会经常更换

    /**
     * 根据id降序查出前8个课程与4个老师并返回,用于前端首页展示
     * @return 查出的课程和讲师列表
     */
    @GetMapping("indexTeacherCourseInfo")
    public R getTeacherCourse(){
        //根据id降序查出前4位老师
        QueryWrapper<EduTeacher> teacherWrapper = new QueryWrapper<>();
        teacherWrapper.orderByDesc("id");
        teacherWrapper.last("limit 4");
        List<EduTeacher> teacherList = teacherService.list(teacherWrapper);

        //根据id降序查出前8个课程
        QueryWrapper<EduCourse> courseWrapper = new QueryWrapper<>();
        courseWrapper.orderByDesc("id");
        courseWrapper.last("limit 8");
        List<EduCourse> courseList = courseService.list(courseWrapper);

        return R.ok().data("teachers",teacherList).data("courses",courseList);
    }

首页显示8个热门课程和4个热门讲师,分页查询,这里按照id降序查询

二、登录

1、JWT+token实现单点登录

?1、JWT

JWT头

JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示

{
  "alg": "HS256",
  "typ": "JWT"
}

有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法。

2、JWT的原则

JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。

3、JWT使用

1、添加jwt工具依赖

<dependencies>
    <!-- JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

2、创建JWT工具类

/**
 * @author
 */
public class JwtUtils {

    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

2、单点登录

1、登录接口

前端会传递一个member对象

    /**
     * 用户登录方法
     *
     * @param member 用户信息
     * @return token用于实现SSO
     */
    @PostMapping("login")
    public R loginUser(@RequestBody Member member) {
        //调用service方法实现登录
        //返回token值,使用jwt生成
        String token = memberService.login(member);
        return R.ok().data("token", token);
    }

2、判断接收的信息

/**
     * 用户登录方法
     */
    @Override
    public String login(Member member) {
        //获取手机号码和密码
        String mobile = member.getMobile();
        String password = member.getPassword();

        //判空
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
            throw new GuliException(20001,"登陆失败,手机号码或密码不能为空");
        }

        //判断手机号码是否正确
        QueryWrapper<Member> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Member user = this.getOne(wrapper);
        if (user == null){
            throw new GuliException(20001,"该手机号码未注册");
        }

        //判断密码是否正确
        /*
        由于数据库中存储的是MD5加密的密码,不能使用明文密码直接比较,应转为md5
         */
        if (!MD5.encrypt(password).equals(user.getPassword())){
            throw new GuliException(20001,"密码不正确");
        }
        //判断用户是否被禁用
        if (user.getIsDisabled()){
            throw new GuliException(20001,"用户被禁用");
        }
        //登录成功,使用JWT生成一个token,注意不能使用前端回传的对象,要使用数据库查出的对象
        return JwtUtils.getJwtToken(user.getId(), user.getNickname());
    }

如果登录通过,那么会利用JWT工具类生成一个token字符串返回

2、前端拦截

登录成功后,设置一个cookie值loginToken,作用域为localhost,前端拦截器发现这个值会拦截下,取出值放入请求头中

// http request 拦截器
service.interceptors.request.use(
  config => {
  //debugger
  if (cookie.get('loginToken')) {
    config.headers['token'] = cookie.get('loginToken');
  }
    return config
  },
  err => {
    return Promise.reject(err);
  }),

?如下:

那么后面的请求都会携带这个cookie

3、实现单点登录关键步骤

?紧接着,调用下面的方法,通过cookie中的token字段请求接口,解析出用户的信息再次放入cookie

            //3 在utils/request.js中写一个http request 拦截器,用于将token放入请求头
            //4 调用方法,通过token获取用户信息
            login.getUserIdByToken()
              .then((res) => {
                // this.loginInfo=res.data.data.userInfo
                //5 放入cookie
                cookie.set('userInfo',JSON.stringify(res.data.data.userInfo),{domain: 'localhost'})
                //跳转到首页
                // this.$router.push({ path: "/" });用另一种方法
                window.location.href="/"
              })

接口如下:

    /**
     * 根据请求头中的token获取用户信息
     *
     * @param req request请求
     * @return 用户信息
     */
    @GetMapping("getMemberInfoByToken")
    public R getMemberInfoByToken(HttpServletRequest req) {
        //调用JwtUtils工具类,利用请求头获取token进行解析得到用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(req);
        Member member = memberService.getById(memberId);
        return R.ok().data("userInfo", member);
    }

利用工具类,从请求头中获取token并解析出用户信息返回?

每次在我们进入抽取的页面公共头时,都会调用通过cookie获取用户信息的方法

?方法如下:

    //创建方法,从cookie中获取用户信息
    getUserInfoFromCookie() {
      var userStr = cookie.get('userInfo');
      //把字符串转为json对象
      if (userStr) {
        //TODO 这块在手机号登录需要这样写,目前已解决
        this.loginInfo = JSON.parse(userStr);
        // this.loginInfo = userStr;
      }
    },

?从cookie中获取用户信息,由于它是一个字符串,用json解析,当拿到后,登录表单会判断是否为空,不为空则显示用户信息,为空仍然显示登录,实现完毕

3、腾讯云短信注册/手机号登录

1、手机号登录

引入依赖

        <!--腾讯短信服务依赖-->
        <dependency>
            <groupId>com.tencentcloudapi</groupId>
            <artifactId>tencentcloud-sdk-java</artifactId>
            <version>3.1.270</version>
        </dependency>

?点击登录后请求接口

    /**
     * 用户登录方法
     */
    @Override
    public String login(Member member) {
        //获取手机号码和密码
        String mobile = member.getMobile();
        String password = member.getPassword();

        //判空
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
            throw new GuliException(20001,"登陆失败,手机号码或密码不能为空");
        }

        //判断手机号码是否正确
        QueryWrapper<Member> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Member user = this.getOne(wrapper);
        if (user == null){
            throw new GuliException(20001,"该手机号码未注册");
        }

        //判断密码是否正确
        /*
        由于数据库中存储的是MD5加密的密码,不能使用明文密码直接比较,应转为md5
         */
        if (!MD5.encrypt(password).equals(user.getPassword())){
            throw new GuliException(20001,"密码不正确");
        }
        //判断用户是否被禁用
        if (user.getIsDisabled()){
            throw new GuliException(20001,"用户被禁用");
        }
        //登录成功,使用JWT生成一个token,注意不能使用前端回传的对象,要使用数据库查出的对象
        return JwtUtils.getJwtToken(user.getId(), user.getNickname());
    }

登录成功后返回token

2、注册

点击发送短信,请求接口

    /**
     * 发送短信方法
     * @param phone 手机号码
     * @return 成功或失败信息
     */
    @PostMapping("sendCode/{phone}")
    public R sendCode(@PathVariable("phone") String phone){

        //从Redis中取验证码,取到就返回
        String codeInRedis = redisTemplate.opsForValue().get(phone);
        if (!StringUtils.isEmpty(codeInRedis)){
            return R.ok();
        }

        //若Redis中没有,调用service发送短信方法
        boolean res = msmService.send(phone);

        if (res){
            return R.ok();
        }else {
            return R.error().message("短信发送失败");
        }
    }

?因为验证码存入redis,按一定规则,每个用户设置一个固定的key对应验证码,发送前,先尝试取验证阿妈,如果不为空,返回成功,如果为空,调用我们写的发送方法。

   /**
     * 发送短信方法
     */
    @Override
    public boolean send(String phone) {

        //生成四位数验证码交腾讯云发送
        String code = RandomUtil.getFourBitRandom();

        //发送验证码
        boolean res = TxCode.SendCode("+86" + phone, code);

        if (res){
            //发送成功后将验证码放到Redis中设置存活时间5分钟
            redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
            return true;
        }else {
            throw new GuliException(20001,"验证码发送失败");
        }

    }

发送短信的SendCode()方法如下:

public class TxCode {
    private static final String SECRET_ID = "AKID2tTnqc7LJ0bvzlGUR7WfYAUXIn1ye4D5";
    private static final String SECRET_KEY= "d06vSjjSt3wYuznLccKmysVsxlHXWDLC";

    public static boolean SendCode(String phoneNumber,String code) {
        try {
            // 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
            // 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
            Credential cred = new Credential(SECRET_ID, SECRET_KEY);
            
            // 实例化要请求产品的client对象,clientProfile是可选的
            SmsClient client = new SmsClient(cred, "ap-guangzhou");
            // 实例化一个请求对象,每个接口都会对应一个request对象
            SendSmsRequest req = new SendSmsRequest();
            String[] phoneNumberSet = {phoneNumber};
            req.setPhoneNumberSet(phoneNumberSet);

            req.setSmsSdkAppId("1400563681");
            req.setSignName("Young的平台");
            req.setTemplateId("1089470");

            String[] templateParamSet = {code, "5"};
            req.setTemplateParamSet(templateParamSet);

            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
            SendSmsResponse resp = client.SendSms(req);

            // 输出json格式的字符串回包
            System.out.println(SendSmsResponse.toJsonString(resp));

            //获取发送状态,若成功则返回true
            for (SendStatus sendStatus : resp.getSendStatusSet()) {
                if ("send success".equals(sendStatus.getMessage()) &&
                        "Ok".equals(sendStatus.getCode())){
                    return true;
                }
            }
        } catch (TencentCloudSDKException e) {
            e.printStackTrace();
        }
        //若不成,返回false
        return false;
    }
}

发送成功,则返回true,失败返回false?

三、课程详情页

1、课程购买模块

用户已经登录

点击课程,进入详情页面,会调用初始化方法,初始化课程的各个信息填充页面,如果课程价格为0或购买状态为已经购买,那么显示立即观看,否则显示立即购买

课程详情页初始化

//判断是否购买
    initCourseInfo() {
      course.getCourseInfo(this.courseId).then((res) => {
        console.log("getCourseInfo-->",res.data.data);
        (this.courseBaseInfo = res.data.data.courseDetails),
          (this.chapterVideoList = res.data.data.chapterVideos),
          (this.isBuy = res.data.data.isBuy);
      });
    },

请求接口

   /**
     * 课程详情页查询
     * @param courseId 课程id
     * @return 查询到的详情页信息
     */
    @GetMapping("getCustCourseInfo/{courseId}")
    public R getCustCourseInfo(@PathVariable String courseId,
                               HttpServletRequest req){
        //根据课程id查询课程信息
        CourseDetailsVo courseDetailsVo = courseService.getBaseCourseInfo(courseId);

        //根据课程id查询章节小节信息
        List<ChapterVo> chapterVoList = chapterService.getChapterVideoByCourseId(courseId);

        //查询购买状态
        //需要对id进行判空,否则会造成isBuy无值现象
        String memberId = JwtUtils.getMemberIdByJwtToken(req);
        boolean isBuy = false;
        if (!StringUtils.isEmpty(memberId)){
            isBuy = orderClient.queryBuyStatus(memberId, courseId);
        }
        return R.ok().data("courseDetails",courseDetailsVo).data("chapterVideos",chapterVoList).data("isBuy",isBuy);
    }

通过jwt解析用户信息,解析出用户的id,然后传入查询课程状态的方法,返回值isBuy代表状态

    @Override
    public boolean queryBuyStatus(String userId, String courseId) {
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("member_id",userId);
        wrapper.eq("course_id",courseId);
        wrapper.select("status");
        Order order = this.getOne(wrapper);
        if (order != null){
            return order.getStatus() == 1;
        }
        return false;
    }

这个课程状态决定了显示立即观看还是立即购买

2、视频播放模块

该页面一加载,会获取三个信息,课程基本信息,章节小节信息,isBuy字段

视频树形菜单通过章节小节信息展示,绑定一个超链接跳转到阿里云存储课程id的动态路由,请求阿里云获取视频播放凭证

?接口如下:

    /**
     * 通过视频id获取阿里云加密视频的播放凭证
     * @param videoId 视频id
     * @return 视频的播放凭证
     */
    @GetMapping("getPlayAuth/{videoId}")
    public R getPlayAuth(@PathVariable String videoId){
        try{
            DefaultAcsClient client = InitVodClient.initVodClient();
            //创建获取凭证的request和response
            GetVideoPlayAuthRequest req = new GetVideoPlayAuthRequest();
            //设置视频id
            req.setVideoId(videoId);
            //得到凭证
            GetVideoPlayAuthResponse resp = client.getAcsResponse(req);
            String playAuth = resp.getPlayAuth();
            System.out.println("播放凭证:【"+playAuth+"】");
            return R.ok().data("playAuth",playAuth);
        }catch (Exception e){
            e.printStackTrace();
            throw new GuliException(20001,"视频凭证获取失败");
        }
    }

待页面渲染完毕,执行mounted()中的阿里云播放器组件,填入凭证和视频id即可播放?

3、课程评论模块

评论显示

调用分页查询方法查询所有评论,用户头像,评论时间

添加评论

初始化时先取出cookie中用户信息赋值,添加评论时判断用户是否登录,登录了调用添加方法,没登录调用路由跳转到登录页面

初始化评论区用户头像

    //评论输入框左边图片初始化
    initCommentInfo() {
      comment
        .initCommentInfo()
        .then((res) => {
          this.com.content = res.data.data.content;
          this.com.nickname = res.data.data.nickname;
          this.com.avatar = res.data.data.avatar;
        })
        .catch((error) => {
          console.log(error);
        });
    }

接口:?

    @GetMapping("initCommentInfo")
    public R initCommentInfo(@Param("req") HttpServletRequest req){
        //从请求头拿出cookie解析出用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(req);
        //远程调用查询用户信息
        R res = ucenterClient.getMemberByMemberId(memberId);
        Map<String, Object> data = res.getData();

        //将查到的用户信息转换为字符串类型
        String memberJson = (String) data.get("memberJson");
        Gson gson = new Gson();
        //解析成对象
        Member member = gson.fromJson(memberJson, Member.class);
        System.out.println("member----->"+member);
       //拿出用户名和头像返回前端
        String nickname = member.getNickname();
        String avatar = member.getAvatar();
        return R.ok().data("nickname",nickname).data("avatar",avatar);
    }

前端回显?

4、其他课程基本信息显示模块

按照组件进行绑定即可

5、全部课程模块

进入页面初始化全部课程并且分页,初始化一级菜单,把一级分类,课程销量,人气,价格封装成一个vo进行排序。

核心方法,切换分页,默认为1,默认大小为8,会携带我们封装的vo进行分页查询。

    //3 分页切换方法
    gotoPage(page) {
      course.getCustCourse(page, this.limit, this.searchObj).then((res) => {
        this.dataInfo = res.data.data;
      });
    }

?按照条件拼接sql,然后调用此方法进行条件查询带分页

四、课程微信支付功能

1、购买按钮跳转

首先点击立即购买,如果用户信息不为空,调用生成订单方法,成功则携带订单号跳转至订单页面,如果信息为空提示登录才能购买

        createOrders() {
      if (this.userInfo && this.userInfo != null) {
        order.createOrder(this.courseId).then((res) => {
          //返回订单号
          //跳转到支付
          this.$router.push({ path: "/order/" + res.data.data.orderId });
        });
      } else {
        // window.location.href='/login'
        this.$message({
          type: "warning",
          message: "登录后才能购买!",
        });
      }
    }

2、创建订单方法

 /**
     * 根据课程id和token中的用户id生成订单,返回订单号
     */
    @Override
    public String createOrder(String courseId,String memberIdByJwtToken) {
        //通过远程调用获取用户信息,此类在公共模块中,因为我们需要Member类
        //不能跨模块调用,因此写了一个一样的
        MemberOrder memberOrder = ucenterClient.getMemberOrder(memberIdByJwtToken);

        //通过远程调用获取课程信息
        CourseDetailsVoOrder courseOrder = eduClient.getCourseOrder(courseId);

        //创建Order对象,设置需要的数据
        Order order = new Order();
        order.setOrderNo(OrderNoUtil.getOrderNo());
        order.setCourseId(courseId);
        order.setCourseTitle(courseOrder.getTitle());
        order.setCourseCover(courseOrder.getCover());
        order.setTeacherName("test");
        order.setTotalFee(courseOrder.getPrice());
        order.setMemberId(memberIdByJwtToken);
        order.setMobile(memberOrder.getMobile());
        order.setNickname(memberOrder.getNickname());
        order.setStatus(0);//是否支付0未1已
        order.setPayType(1);//支付类型1微信2支付宝
        order.setIsDeleted(0);
        //保存到数据库
        baseMapper.insert(order);

        return order.getOrderNo();
    }

将订单保存到数据库,订单号返回给前端,此时订单状态仍然为0,未购买,携带订单id跳转到支付界面

3、进入支付页面

?去支付按钮跳转:

        //去支付按钮
      toPay(){
        this.$router.push({path: '/pay/'+this.order.orderNo})
      }

携带着订单id,进入生成二维码的页面?

?通过订单id调用生成二维码方法

  /**
   * 生成支付二维码
   * @param {订单编号} orderId 
   * @returns map集合
   */
  createQrCode(orderId) {
    return request({
      url: `/eduorder/paylog/createQrCode/${orderId}`,
      method: 'get'
    })
  }

接口如下:

通过订单id获取订单信息

设置请求参数,以及回调地址,

利用HttpClient发送post请求,返回值为xml格式,利用微信提供的工具类转换为map,获取里面的二维码地址和操作状态,以及可以设置一些我们需要的金额,订单号等参数

    /**
     * 生成微信支付二维码
     */
    @Override
    public Map createQrCode(String orderNo) {

        try {
            //1 根据订单号查询订单信息
            QueryWrapper<Order> wrapper = new QueryWrapper<>();
            wrapper.eq("order_no", orderNo);
            Order order = orderService.getOne(wrapper);
            //2 使用map设置生成二维码需要的参数
            Map map = new HashMap();
            map.put("appid", "wx74862e0dfcf69954");
            map.put("mch_id", "1558950191");
            map.put("nonce_str", WXPayUtil.generateNonceStr());
            map.put("body", order.getCourseTitle());//课程名
            map.put("out_trade_no", orderNo);//课程编号
            map.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue() + "");
            map.put("spbill_create_ip", "127.0.0.1");//服务器域名
            map.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");//回调
            map.put("trade_type", "NATIVE");//生成二维码的支付类型

            //3 发送httpclient请求,传递参数xml格式,微信支付提供的固定地址
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
            client.setXmlParam(WXPayUtil.generateSignedXml(map, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
            client.setHttps(true);//支持https
            //4 得到发送请求返回结果
            client.post();
            String content = client.getContent();//返回的是xml格式,需要转换为map返回
            Map<String, String> contentMap = WXPayUtil.xmlToMap(content);
            //5 contentMap返回的值中,没有页面需要的其他数据,再封装成map
            Map resMap = new HashMap<>();
            resMap.put("out_trade_no", orderNo);
            resMap.put("course_id", order.getCourseId());
            resMap.put("total_fee", order.getTotalFee());
            resMap.put("result_code", contentMap.get("result_code"));//二维码操作状态
            resMap.put("code_url", contentMap.get("code_url"));//二维码地址

            return resMap;
        } catch (Exception e) {
            e.printStackTrace();
            throw new GuliException(20001, "支付失败!");
        }
    }

4、监听支付状态

  //每隔3秒调用一次查询订单状态方法
  mounted() {
    //计时器方法
    this.timer = setInterval(() => {
      //该参数为我们发送请求时的订单号
      this.getPayStatus(this.payObj.out_trade_no);
    }, 3000);
  }

接口如下

    /**
     * 查询支付状态,并修改支付表和订单表
     * @param orderNo 订单编号
     * @return 支付状态
     */
    @GetMapping("queryPayStatus/{orderNo}")
    public R queryPayStatus(@PathVariable String orderNo) {
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("order_id",orderNo);
        Map<String,String> map = payService.queryPayStatus(orderNo);
        System.out.println("查询订单状态的map---->"+map);
        if (map.isEmpty()){
            return R.error().message("支付出错!");
        }
        //map不为空,获取订单状态返回
        if (map.get("trade_state").equals("SUCCESS")){
            //添加记录到支付表,并且更新订单表
            payService.updateOrderStatus(map);
            return R.ok().message("支付成功!");
        }
        return R.ok().code(25000).message("支付中...");
    }

queryPayStatus()方法:

    //根据订单号查询订单支付状态
    @Override
    public Map<String, String> queryPayStatus(String orderNo) {
        try {
            //1 封装参数
            Map m = new HashMap<>();
            m.put("appid", "wx74862e0dfcf69954");
            m.put("mch_id", "1558950191");
            m.put("out_trade_no", orderNo);
            m.put("nonce_str", WXPayUtil.generateNonceStr());

            //2 发送httpclient请求
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
            client.setXmlParam(WXPayUtil.generateSignedXml(m, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
            client.setHttps(true);

            //3 发送
            client.post();
            String xml = client.getContent();
            //4、转成Map
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            //5、返回
            return resultMap;
        } catch (Exception e) {
            e.printStackTrace();
            throw new GuliException(20001, "查询支付状态失败!");
        }
    }

如果成功,调用保存支付记录方法payService.updateOrderStatus(map);保存支付信息,这个方法也会更新订单表中status字段,同时保存信息到支付记录表

    //向支付表添加记录,同时更新订单表支付状态
    @Override
    public void updateOrderStatus(Map<String, String> map) {
        //从查询订单状态返回的map中查找订单号
        String orderNo = map.get("out_trade_no");
        //根据订单号查询订单信息
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no", orderNo);
        Order order = orderService.getOne(wrapper);
        //更新订单表状态
        if (order.getStatus().intValue() == 1) {
            return;
        }
        order.setStatus(1);//已支付
        orderService.updateById(order);
        //支付完成后向支付表添加数据
        PayLog payLog = new PayLog();
        payLog.setOrderNo(order.getOrderNo());//支付订单号
        payLog.setPayTime(new Date());
        payLog.setPayType(1);//支付类型
        payLog.setTotalFee(order.getTotalFee());//总金额(分)
        payLog.setTradeState(map.get("trade_state"));//支付状态
        payLog.setTransactionId(map.get("transaction_id"));//流水号,微信返回的
        payLog.setAttr(JSONObject.toJSONString(map));
        baseMapper.insert(payLog);//插入到支付日志表

    }

最后,携带课程信息跳转到课程页面,显示课程状态?

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-14 21:32:45  更:2021-11-14 21:34:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 21:29:15-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码