搭建前后端分离主流项目完整步骤——在线教育系统(阿里云服务器部署上线)

前言:

需要源码评论或私我

项目技术栈如下图所示:
在这里插入图片描述
本次博客分前后端+部署服务器三个步骤来写
先来看看实现效果:

在线教育系统完整三步骤

一.后端技术栈

1.ssm+mysql:

项目目录:
在这里插入图片描述

(1)mapper层(与数据库交互的) 与对应xml实现逻辑

首先是数据库配置:application.properties


server.port=8081

#==============================数据库相关配置========================================
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xdclass?useUnicode=true&useJDBCCompliantTimezoneShift=true&serverTimezone=UTC&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

#使用阿里巴巴druid数据源,默认使用自带的
#spring.datasource.type =com.alibaba.druid.pool.DruidDataSource
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#配置扫描
mybatis.mapper-locations=classpath:mapper/*.xml

#配置xml的结果别名
mybatis.type-aliases-package=net.xdclass.online_xdclass.model.entity

注意:
此句话挺重要的,用来扫描这些与数据库连接的xml(都在resources下的mapper路径下)
在这里插入图片描述

1.视频下单接口(VideoOrderMapper)
package net.xdclass.online_xdclass.mapper;

import net.xdclass.online_xdclass.model.entity.VideoOrder;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface VideoOrderMapper {

    /**
     * 查询用户是否购买过此商品
     * @param userId
     * @param vidoeId
     * @param state
     * @return
     */
    VideoOrder findByUserIdAndVideoIdAndState(@Param("user_id") int userId, @Param("video_id") int videoId, @Param("state") int state);


    /**
     * 下单
     * @return
     */
    int saveOrder(VideoOrder videoOrder);


    /**
     * 视频列表
     * @param userId
     * @return
     */
    List<VideoOrder> listOrderByUserId(@Param("user_id") Integer userId);
}

和对应的xml实现代码:
(这里的resultType代表映射的是哪个类型的变量,都是在videoOrder这个类里有对应的变量,见代码后的图片)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="net.xdclass.online_xdclass.mapper.VideoOrderMapper">


    <select id="findByUserIdAndVideoIdAndState" resultType="VideoOrder">

        select * from video_order where  user_id = #{user_id} and video_id = #{video_id} and state = #{state}

    </select>


    <insert id="saveOrder" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
        INSERT INTO `video_order` (`out_trade_no`, `state`, `create_time`, `total_fee`, `video_id`,
         `video_title`, `video_img`, `user_id`)
        VALUES
        (#{outTradeNo,jdbcType=VARCHAR},#{state,jdbcType=INTEGER},#{createTime,jdbcType=TIMESTAMP},#{totalFee,jdbcType=INTEGER},
        #{videoId,jdbcType=INTEGER},#{videoTitle,jdbcType=VARCHAR},#{videoImg,jdbcType=VARCHAR},#{userId,jdbcType=INTEGER});

    </insert>



    <select id="listOrderByUserId" resultType="VideoOrder">

        select * from video_order where user_id=#{user_id} order by create_time desc

    </select>

</mapper>

VideoOrder实体类变量(负责映射对应的数据库变量)
在这里插入图片描述
VideoOrder(视频下单接口)对应的数据库表
在这里插入图片描述

2.视频查询接口(VideoMapper)
package net.xdclass.online_xdclass.mapper;

import net.xdclass.online_xdclass.model.entity.Video;
import net.xdclass.online_xdclass.model.entity.VideoBanner;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface VideoMapper{

    /**
     * 查询视频列表
     * @return
     */
    List<Video> listVideo();

    /**
     * 首页轮播图列表
     * @return
     */
    List<VideoBanner> listVideoBanner();

    /**
     * 查询视频详情
     * @param videoId
     * @return
     */
    Video findDetailById(@Param("video_id") int videoId);

    /**
     * 简单查询视频信息
     * @param videoId
     * @return
     */
    Video findById(@Param("video_id") int videoId);
}

对应xml实现:
其中findDetailById用到了三表联合查询,用到了resultMap(不懂得可以看https://blog.csdn.net/weixin_45678130/article/details/113781320
像这里的property:chapterList是video这个类的一个变量,他存在于Chapter这个类
下面的column对应的数据库字段,property对应java类里面是如何书写的这个字段的
其中p'ro
其中video包含chaper
在这里插入图片描述
其中chaper(章类)包含episodeList这个集对象
在这里插入图片描述
episode:
在这里插入图片描述
xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="net.xdclass.online_xdclass.mapper.VideoMapper">



    <select id="listVideo" resultType="Video">

        select * from video

    </select>


    <select id="findById" resultType="Video">
                select * from video where id=#{video_id}
    </select>

    <select id="listVideoBanner" resultType="VideoBanner">

        select  * from video_banner order by weight asc

    </select>



    <resultMap id="VideoDetailResultMap" type="Video">

        <id column="id" jdbcType="INTEGER" property="id"/>

        <result column="title" jdbcType="VARCHAR" property="title"/>
        <result column="summary" jdbcType="VARCHAR" property="summary"/>
        <result column="cover_img" jdbcType="VARCHAR" property="coverImg"/>
        <result column="price" jdbcType="INTEGER" property="price"/>
        <result column="point" jdbcType="DOUBLE" property="point"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>


        <collection property="chapterList" ofType="Chapter">
            <id column="chapter_id" jdbcType="INTEGER" property="id"/>
            <result column="chapter_title" jdbcType="VARCHAR" property="title"/>
            <result column="ordered" jdbcType="INTEGER" property="ordered"/>
            <result column="chapter_create_time" jdbcType="TIMESTAMP" property="createTime"/>

            <collection property="episodeList" ofType="Episode">
                <id column="episode_id" jdbcType="INTEGER" property="id"/>
                <result column="num" jdbcType="INTEGER" property="num"/>
                <result column="episode_title" jdbcType="VARCHAR" property="title"/>
                <result column="episode_ordered" jdbcType="INTEGER" property="ordered"/>
                <result column="play_url" jdbcType="VARCHAR" property="playUrl"/>
                <result column="free" jdbcType="INTEGER" property="free"/>
                <result column="episode_create_time" jdbcType="TIMESTAMP" property="createTime"/>
            </collection>

        </collection>
        
    </resultMap>


    <select id="findDetailById" resultMap="VideoDetailResultMap">

        select
        v.id, v.title,v.summary,v.cover_img,v.price,v.point,v.create_time,
        c.id as chapter_id, c.title as chapter_title, c.ordered,c.create_time as chapter_create_time,
        e.id as episode_id,e.num, e.title as episode_title,e.ordered as episode_ordered,e.play_url,e.free,e.create_time as episode_create_time

        from video v

        left join chapter c on v.id=c.video_id

        left join episode e on c.id= e.chapter_id

        where v.id = #{video_id}
        order by c.ordered,e.num asc


    </select>
    

</mapper>
3.用户登录查询接口(UserMapper)
package net.xdclass.online_xdclass.mapper;

import net.xdclass.online_xdclass.model.entity.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {

    int save(User user);

    User findByPhone(@Param("phone") String phone);


    User findByPhoneAndPwd(@Param("phone") String phone, @Param("pwd") String pwd);

    User findByUserId(@Param("user_id") Integer userId);
}

对应xml实现:
其中save实现的是把用户信息储存到数据库中

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="net.xdclass.online_xdclass.mapper.UserMapper">


    <insert id="save" parameterType="User">

        INSERT  INTO user (name, pwd, head_img, phone , create_time)
        values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
        #{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})

    </insert>



    <!--根据手机号查询用户信息-->
    <select id="findByPhone" resultType="User">

        select  * from user where phone =#{phone}

    </select>


    <!--根据手机号和密码找用户-->
    <select id="findByPhoneAndPwd" resultType="User">

        select  * from user where phone =#{phone} and pwd = #{pwd}


    </select>


    <select id="findByUserId" resultType="User">

      select  * from user where id=#{user_id}

    </select>

</mapper>
(2)service层(业务逻辑层-上有controller下有mapper层) 与对应impl实现接口逻辑
1.用户登录逻辑(UserServiceImpl)

ps:不要忘记加入@service注解,加入ioc容器。同时注入usermapper
先来讲一下parseToUser这个方法:先看一下是不是同时有phone,name,pwd这些变量,有的话放到user对象然后返回回来
其中map中的containsKey和get见如下
在这里插入图片描述
在这里插入图片描述

package net.xdclass.online_xdclass.service.impl;

import net.xdclass.online_xdclass.model.entity.User;
import net.xdclass.online_xdclass.mapper.UserMapper;
import net.xdclass.online_xdclass.service.UserService;
import net.xdclass.online_xdclass.utils.CommonUtils;
import net.xdclass.online_xdclass.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Map;
import java.util.Random;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    

    @Override
    public int save(Map<String, String> userInfo) {

        User user = parseToUser(userInfo);
        if( user != null){
           return userMapper.save(user);
        }else {
            return -1;
        }

    }


    @Override
    public String findByPhoneAndPwd(String phone, String pwd) {

        User user = userMapper.findByPhoneAndPwd(phone, CommonUtils.MD5(pwd));

        if(user == null){
            return null;

        }else {
            String token = JWTUtils.geneJsonWebToken(user);
            return token;
        }

    }

    @Override
    public User findByUserId(Integer userId) {

        User user = userMapper.findByUserId(userId);
        return user;
    }

    /**
     * 解析 user 对象
     * @param userInfo
     * @return
     */
    private User parseToUser(Map<String,String> userInfo) {

        if(userInfo.containsKey("phone") && userInfo.containsKey("pwd") && userInfo.containsKey("name")){
            User user = new User();
            user.setName(userInfo.get("name"));
            user.setHeadImg(getRandomImg());
            user.setCreateTime(new Date());
            user.setPhone(userInfo.get("phone"));
            String pwd = userInfo.get("pwd");
            //MD5加密
            user.setPwd(CommonUtils.MD5(pwd));

            return user;
        }else {
            return null;
        }

    }

    /**
     * 放在CDN上的随机头像
     */
    private static final String [] headImg = {
            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/12.jpeg",
            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/11.jpeg",
            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/13.jpeg",
            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/14.jpeg",
            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/15.jpeg"
    };

    private String getRandomImg(){
        int size =  headImg.length;
        Random random = new Random();
        int index = random.nextInt(size);
        return headImg[index];
    }

}

其中JWTUtils.geneJsonWebToken(user) 及md5加密请看 目录->公共类

2.视频查询逻辑(VideoServiceImpl)

里面的方法分别是列举出所有视频,视频轮播图,或根据id查询视频
根据id查询视频 用到了三表联合查询,比较复杂,可看mapper层中的视频查询接口

package net.xdclass.online_xdclass.service.impl;

import net.xdclass.online_xdclass.model.entity.Video;
import net.xdclass.online_xdclass.model.entity.VideoBanner;
import net.xdclass.online_xdclass.mapper.VideoMapper;
import net.xdclass.online_xdclass.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;


@Service
public class VideoServiceImpl implements VideoService {

    @Autowired
    private VideoMapper videoMapper;



    @Override
    public List<Video> listVideo() {

        return videoMapper.listVideo();
    }



    @Override
    public List<VideoBanner> listBanner() {
        return videoMapper.listVideoBanner();
    }




    @Override
    public Video findDetailById(int videoId) {

        // 需要使用mybaits关联复杂查询
        Video video = videoMapper.findDetailById(videoId);

        return video;
    }
}

3.视频查询逻辑(VideoServiceImpl)

其中的@Transactional注解是拦截器注解,请看目录中拦截器一栏
save方法的业务逻辑便是先查询一下该userid和videoid是否出现过,也就是是否买过此视频,如果买过,则此方法return 0结束。反之新建一个videoOrder对象,然后赋值即可,同时生成了此视频播放记录

package net.xdclass.online_xdclass.service.impl;

import net.xdclass.online_xdclass.exception.XDException;
import net.xdclass.online_xdclass.mapper.*;
import net.xdclass.online_xdclass.model.entity.Episode;
import net.xdclass.online_xdclass.model.entity.PlayRecord;
import net.xdclass.online_xdclass.model.entity.Video;
import net.xdclass.online_xdclass.model.entity.VideoOrder;
import net.xdclass.online_xdclass.model.request.VideoOrderRequest;
import net.xdclass.online_xdclass.service.VideoOrderService;
import net.xdclass.online_xdclass.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.UUID;


@Service

public class VideoOrderServiceImpl implements VideoOrderService {


    @Autowired
    private VideoOrderMapper videoOrderMapper;



    @Autowired
    private VideoMapper videoMapper;


    @Autowired
    private EpisodeMapper episodeMapper;


    @Autowired
    private PlayRecordMapper playRecordMapper;


    /**
     * 下单操作
     * 未来版本:优惠券抵扣,风控用户检查,生成订单基础信息,生成支付信息
     * @param userId
     * @param videoId
     * @return
     */
    @Override
    @Transactional
    public int save(int userId, int videoId) {

        //判断是否已经购买
        VideoOrder videoOrder = videoOrderMapper.findByUserIdAndVideoIdAndState(userId,videoId,1);

        if(videoOrder!=null){return  0;}

        Video video = videoMapper.findById(videoId);

        VideoOrder newVideoOrder = new VideoOrder();
        newVideoOrder.setCreateTime(new Date());
        newVideoOrder.setOutTradeNo(UUID.randomUUID().toString());
        newVideoOrder.setState(1);
        newVideoOrder.setTotalFee(video.getPrice());
        newVideoOrder.setUserId(userId);

        newVideoOrder.setVideoId(videoId);
        newVideoOrder.setVideoImg(video.getCoverImg());
        newVideoOrder.setVideoTitle(video.getTitle());

        int rows = videoOrderMapper.saveOrder(newVideoOrder);



        //生成播放记录
        if(rows == 1){
            Episode episode = episodeMapper.findFirstEpisodeByVideoId(videoId);
            if(episode == null){
                throw  new XDException(-1,"视频没有集信息,请运营人员检查");
            }
            PlayRecord playRecord = new PlayRecord();
            playRecord.setCreateTime(new Date());
            playRecord.setEpisodeId(episode.getId());
            playRecord.setCurrentNum(episode.getNum());
            playRecord.setUserId(userId);
            playRecord.setVideoId(videoId);
            playRecordMapper.saveRecord(playRecord);
        }

        return rows;
    }


    @Override
    public List<VideoOrder> listOrderByUserId(Integer userId) {

        return videoOrderMapper.listOrderByUserId(userId);
    }
}

(3)Controller层(与web前端接口交互的)
1.用户登录控制层(UserController)

首先全部依赖这个接口:(“api/v1/pri/user”)
然后再后面继续添加对应路径
比如:
api/v1/pri/user/register 是注册接口
api/v1/pri/user/login 是登录接口
api/v1/pri/user/find_by_token 根据用户id查询用户信息

package net.xdclass.online_xdclass.controller;

import net.xdclass.online_xdclass.model.entity.User;
import net.xdclass.online_xdclass.model.request.LoginRequest;
import net.xdclass.online_xdclass.service.UserService;
import net.xdclass.online_xdclass.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@RestController
@RequestMapping("api/v1/pri/user")
public class UserController {



    @Autowired
    private UserService userService;

    /**
     * 注册接口
     * @param userInfo
     * @return
     */
    @PostMapping("register")
    public JsonData register(@RequestBody Map<String,String> userInfo ){

        int rows = userService.save(userInfo);

        return rows == 1 ? JsonData.buildSuccess(): JsonData.buildError("注册失败,请重试");

    }


    /**
     * 登录接口
     * @param loginRequest
     * @return
     */
    @PostMapping("login")
    public JsonData login(@RequestBody LoginRequest loginRequest){

        String token = userService.findByPhoneAndPwd(loginRequest.getPhone(), loginRequest.getPwd());

        return token == null ?JsonData.buildError("登录失败,账号密码错误"): JsonData.buildSuccess(token);

    }


    /**
     * 根据用户id查询用户信息
     * @param request
     * @return
     */
    @GetMapping("find_by_token")
    public JsonData findUserInfoByToken(HttpServletRequest request){

        Integer userId = (Integer) request.getAttribute("user_id");

        if(userId == null){
            return JsonData.buildError("查询失败");
        }

        User user =  userService.findByUserId(userId);

        return JsonData.buildSuccess(user);

    }


}

其中上面控制层代码中的登录接口中 参数为LoginRequest,就是下面的代码,接口传入了如下类的数据

package net.xdclass.online_xdclass.model.request;

/**
 * 登录 request
 */
public class LoginRequest {

    private String phone;

    private String pwd;

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

2.查询视频信息控制层(VideoController)

总接口:api/v1/pub/video

package net.xdclass.online_xdclass.controller;

import net.xdclass.online_xdclass.model.entity.Video;
import net.xdclass.online_xdclass.model.entity.VideoBanner;
import net.xdclass.online_xdclass.service.VideoService;
import net.xdclass.online_xdclass.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("api/v1/pub/video")
public class VideoController {


    @Autowired
    private VideoService videoService;


    /**
     * 轮播图列表
     * @return
     */
    @GetMapping("list_banner")
    public JsonData indexBanner(){


        List<VideoBanner> bannerList =  videoService.listBanner();


        int i = 1/0;

        return JsonData.buildSuccess(bannerList);

    }


    /**
     * 视频列表
     * @return
     */
    @RequestMapping("list")
    public JsonData listVideo(){

        List<Video> videoList = videoService.listVideo();
        return JsonData.buildSuccess(videoList);
    }


    /**
     * 查询视频详情,包含章,集信息
     * @param videoId
     * @return
     */
    @GetMapping("find_detail_by_id")
    public JsonData findDetailById(@RequestParam(value = "video_id",required = true)int videoId){


        Video video = videoService.findDetailById(videoId);

        return JsonData.buildSuccess(video);

    }


}

3.视频下单控制层(VideoOrderController)
package net.xdclass.online_xdclass.controller;

import net.xdclass.online_xdclass.model.entity.VideoOrder;
import net.xdclass.online_xdclass.model.request.VideoOrderRequest;
import net.xdclass.online_xdclass.service.VideoOrderService;
import net.xdclass.online_xdclass.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@RestController
@RequestMapping("/api/v1/pri/order")
public class VideoOrderController {


    @Autowired
    private VideoOrderService videoOrderService;


    /**
     * 下单接口
     * @return
     */
    @RequestMapping("save")
    public JsonData saveOrder(@RequestBody VideoOrderRequest videoOrderRequest, HttpServletRequest request){

        Integer userId = (Integer) request.getAttribute("user_id");


        int rows = videoOrderService.save(userId, videoOrderRequest.getVideoId());

        return rows == 0 ? JsonData.buildError("下单失败"):JsonData.buildSuccess();
    }


    /**
     * 订单列表
     * @param request
     * @return
     */
    @GetMapping("list")
    public JsonData listOrder(HttpServletRequest request){
        Integer userId = (Integer) request.getAttribute("user_id");

        List<VideoOrder> videoOrderList = videoOrderService.listOrderByUserId(userId);

        return JsonData.buildSuccess(videoOrderList);

    }


}

(4)公共层(jwt实现 很重要!)

在这里插入图片描述

1.CommonUtils(实现md5 不用死记硬背)
package net.xdclass.online_xdclass.utils;

import java.security.MessageDigest;

/**
 * 工具类
 */
public class CommonUtils {


    /**
     * MD5加密工具类
     * @param data
     * @return
     */
    public static String MD5(String data)  {
        try {
            java.security.MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }

            return sb.toString().toUpperCase();
        } catch (Exception exception) {
        }
        return null;

    }

}

2.jsondata
package net.xdclass.online_xdclass.utils;

public class JsonData {

    /**
     * 状态码 0表示成功过,1表示处理中,-1 表示失败
     */
    private Integer code;

    /**
     * 业务数据
     */
    private Object data;

    /**
     * 信息表示
     */
    private String msg;

    public  JsonData(){}

    public  JsonData(Integer code, Object data, String msg){
        this.code = code;
        this.data = data;
        this.msg = msg;
    }


    /**
     * 成功,不用返回数据
     * @return
     */
    public static JsonData buildSuccess(){
        return new JsonData(0,null,null);
    }

    /**
     * 成功,返回数据
     * @param data
     * @return
     */
    public static JsonData buildSuccess(Object data){
        return new JsonData(0,data,null);
    }


    /**
     * 失败,固定状态码
     * @param msg
     * @return
     */
    public static JsonData buildError(String  msg){
        return new JsonData(-1 ,null,msg);
    }


    /**
     * 失败,自定义错误码和信息
     * @param code
     * @param msg
     * @return
     */
    public static JsonData buildError(Integer code , String  msg){
        return new JsonData(code ,null,msg);
    }


    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

3.JWTUtils(很重要)

其中用户登录时 如果满足要求 则会生成一个token 就是利用jwt来实现)请见下面代码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package net.xdclass.online_xdclass.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.xdclass.online_xdclass.model.entity.User;

import java.util.Date;

/**
 * Jwt工具类
 * 注意点:
 * 1、生成的token, 是可以通过base64进行解密出明文信息
 * 2、base64进行解密出明文信息,修改再进行编码,则会解密失败
 * 3、无法作废已颁布的token,除非改秘钥
 */
public class JWTUtils {


    /**
     * 过期时间,一周
     */
    private  static final long EXPIRE = 60000 * 60 * 24 * 7;
    //private  static final long EXPIRE = 1;


    /**
     * 加密秘钥
     */
    private  static final String SECRET = "xdclass.net168";


    /**
     * 令牌前缀
     */
    private  static final String TOKEN_PREFIX = "xdclass";


    /**
     * subject
     */
    private  static final String SUBJECT = "xdclass";


    /**
     * 根据用户信息,生成令牌
     * @param user
     * @return
     */
    public static String geneJsonWebToken(User user){

        String token = Jwts.builder().setSubject(SUBJECT)
                .claim("head_img",user.getHeadImg())
                .claim("id",user.getId())
                .claim("name",user.getName())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .signWith(SignatureAlgorithm.HS256,SECRET).compact();

        token = TOKEN_PREFIX + token;


        return token;
    }


    /**
     * 校验token的方法
     * @param token
     * @return
     */
    public static Claims checkJWT(String token){

        try{

            final  Claims claims = Jwts.parser().setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX,"")).getBody();

            return claims;

        }catch (Exception e){
            return null;
        }

    }



}

(5)config层(拦截器配置)
1.LoginInterceptor (登录校验成功放行)

根据用户登录产生的token值来判断 看是否过期来选择是否放行
不成功的话会响应json数据给前端

package net.xdclass.online_xdclass.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import net.xdclass.online_xdclass.utils.JWTUtils;
import net.xdclass.online_xdclass.utils.JsonData;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 进入到controller之前的方法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        try {

            String accesToken = request.getHeader("token");
            if (accesToken == null) {
                accesToken = request.getParameter("token");
            }

            if (StringUtils.isNotBlank(accesToken)) {
                Claims claims = JWTUtils.checkJWT(accesToken);
                if (claims == null) {
                    //告诉登录过期,重新登录
                    sendJsonMessage(response, JsonData.buildError("登录过期,重新登录"));
                    return false;
                }

                Integer id = (Integer) claims.get("id");
                String name = (String) claims.get("name");

                request.setAttribute("user_id", id);
                request.setAttribute("name", name);

                return true;

            }

        }catch (Exception e){}

        sendJsonMessage(response, JsonData.buildError("登录过期,重新登录"));

        return false;
    }


    /**
     * 响应json数据给前端
     * @param response
     * @param obj
     */
    public static void sendJsonMessage(HttpServletResponse response, Object obj){

        try{
            ObjectMapper objectMapper = new ObjectMapper();
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.print(objectMapper.writeValueAsString(obj));
            writer.close();
            response.flushBuffer();
        }catch (Exception e){
            e.printStackTrace();
        }


    }




    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

2.loginInterceptor (拦截器注册和路径校验配置)
  • 拦截器配置

  • 不用权限可以访问url /api/v1/pub/

  • 要登录可以访问url /api/v1/pri/

InterceptorConfig.java:

package net.xdclass.online_xdclass.config;

import net.xdclass.online_xdclass.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器配置
 *
 * 不用权限可以访问url    /api/v1/pub/
 * 要登录可以访问url    /api/v1/pri/
 */

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {


    @Bean
    LoginInterceptor loginInterceptor(){
        return new LoginInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        //拦截全部
        registry.addInterceptor(loginInterceptor()).addPathPatterns("/api/v1/pri/*/*/**")
                //不拦截哪些路径   斜杠一定要加
                .excludePathPatterns("/api/v1/pri/user/login","/api/v1/pri/user/register");

        WebMvcConfigurer.super.addInterceptors(registry);

    }
}

二.前端技术栈

1.搭建项目前的准备:

(1)npm和node的介绍:

1.什么是NodeJS: Node.js 就是运⾏在服务端的 JavaScript
2.什么是npm: nodejs的包管理⼯具,可以下载使⽤公共仓库的包,类似maven 包安装分为本地安
装(local)、全局安装(global)两种
在这里插入图片描述
在这里插入图片描述

(2)npm切换为淘宝镜像:

在这里插入图片描述

npm install -g cnpm --registry=https://registry.npm.taobao.org

这样就可以使⽤ cnpm 命令来安装模块了:

cnpm install [name]

在这里插入图片描述

(3)新版Vue + 脚⼿架Vue-Cli 4.3 安装:

什么是VUE ⼀套⽤于构建⽤户界⾯的渐进式框架。与其它⼤型框架不同的是,Vue 被设计为可以
⾃底向上逐层应⽤。Vue 的核⼼库只关注视图层,不仅易于上⼿,还便于与第三⽅库或既有项⽬整合
安装新版Vue (直接使⽤ cli)

npm install vue (可以不⽤)

什么是VUE-CLI Vue 提供了⼀个官⽅的 CLI,为单⻚⾯应⽤ (SPA) 快速搭建繁杂的脚⼿架
安装 新版vue-cli

cnpm install -g @vue/cli
cnpm install -g @vue/cli-init

使⽤vue-cli创建项⽬(测试项⽬,验证vue环境)

vue create my-project

2.vue的学习:

(1)Vue的指令:

在这里插入图片描述

<div id="app">
 <div v-if="Math.random() > 0.5 "> ⼤于0.5 </div>
 <div v-else>⼩于0.5 </div>
 
</div>
<script>
 new Vue({
 //绑定到哪个元素
 el:'#app',
 //数据源
 data:{
 },
 

 methods: {
 
 }
 })
</script>
div id="app">
 <ol>
 <li v-for=" user in users ">
 {{user.name}}
 </li>
 </ol>
</div>
<script>
 new Vue({
 //绑定到哪个元素
 el:'#app',
 //数据源
 data:{
 users:[
 {name: "Anna⼩姐姐"},
 {name: "⽼王"},
 {name: "⼆当家⼩D"},
 ]
 },
 //⾃定义⽅法
 methods: {
 
 }
 })
</script>
<div id="app">
        <p>{{phone}}</p>
        手机号<input v-model="phone">
    </div>
    <script>
        new Vue({
            el:"#app",
            data:{
                phone:"000"
            },
            methods: {
                
            }
        })
    </script>
 <div id="#app">
        <p>{{title}}</p>
        <button v-on:click="change"></button>
    </div>
    <script>
        new Vue({
            el:"#app",
            data:{
                title:"ddd"
            },
            methods: {
                change:function(){
                    this.title="hh"+this.title
                }
            }
        })
    </script>

在这里插入图片描述

(2)Vue的组件:

在这里插入图片描述

 <div id="#app">
    <xd_component></xd_component>
     <xd_component></xd_component>
     <xd_component></xd_component>
     <xd_component></xd_component>
   </div>
   <script>
       Vue.component('xd_compoent',{
           data:function(){
               return {
                   count:0
               }
           },
           template:'<button v-on:click="count++">点击{{count}}</button>'

       })
       new Vue({
           el:"#app",
           data:{

           },
           methods: {
               
           }
       })
   </script>
(3)ES6的语法

在这里插入图片描述
在这里插入图片描述

//以前js定义函数
var sum = function(num1,num2) {
 return num1 + num2;
};
 
// 使⽤箭头函数
let sum = (num1,num2) => num1 + num2;
function getVideo(make, model, value) {
 return {
 // 简写变量
 make, // 等同于 make: make
 model, // 等同于 model: model
 value, // 等同于 value: value
 };
}
let video = getVideo('java', 'java', 99);
 output: {
 make: 'java',
 model:'java',
 value: 99,
}

3.项目搭建:

(0)接口方法汇总及底部导航栏组件

import axios from '../request'

//注册接口
export const registerApi = (phone, pwd , name)=> axios.post("/api/v1/pri/user/register",{
    "phone":phone,
    "pwd":pwd,
    "name":name
})

//登录接口
export const loginApi = (phone, pwd) => axios.post("/api/v1/pri/user/login",{
    phone,
    pwd
})


//轮播图接口
export const getBanner = () => axios.get("/api/v1/pub/video/list_banner")

//视频列表接口
export const getVideoList = ()=> axios.get("/api/v1/pub/video/list")


//视频详情
export const getVideoDetail = (vid)=> axios.get("/api/v1/pub/video/find_detail_by_id?",{
    params: {
        video_id:vid
    }
})

//下单接口
export const saveOrder = (token, vid)=>axios.post("/api/v1/pri/order/save",{
    "video_id":vid
},{
    headers:{
        "token":token
    }
})

//订单列表
export const getOrderList = (token)=>axios.get("/api/v1/pri/order/list",{
    params:{
        "token":token
    }
})

//用户信息接口
export const getUserInfo = (token)=>axios.get("/api/v1/pri/user/find_by_token",{
    params:{
        "token":token
    }
})

底部导航栏:

<template>
  <div class="tab">
    <cube-tab-bar v-model="selectedLabelSlots" @click="changHandler">
      <cube-tab
        v-for="(item) in tabs"
        :icon="item.icon"
        :label="item.label"
        :key="item.path"
        :value="item.path"
      ></cube-tab>
    </cube-tab-bar>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedLabelSlots: "/",
      tabs: [
        {
          label: "首页",
          icon: "cubeic-home",
          path: "/"
        },
        {
          label: "我的订单",
          icon: "cubeic-like",
          path: "/order"
        },
        {
          label: "个人中心",
          icon: "cubeic-person",
          path: "/personal"
        }
      ]
    };
  },
  methods: {
      changHandler(path){
          //this.$route.path是当前路径
          if(path !== this.$route.path){
              this.$router.push(path)
          }
      }
  },
    //vue实例生命周期 created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
    //vue实例生命周期 mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行额外的操作    
    //https://cn.vuejs.org/v2/guide/instance.html#%E5%AE%9E%E4%BE%8B%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90
  created(){
      //默认路由选择器,比如刷新页面,需要重新进到当前路由
      this.selectedLabelSlots = this.$route.path
  }
 
};

</script>

<!--SCSS是一种CSS预处理语言, scoped 是指这个scss样式 只作用于当前组件-->
<style lang="scss" scoped>
.tab {
  position: fixed;
  bottom: 0;
  z-index: 999;
  background-color:#fff;
  width: 100%;
  border-top: 1px solid rgba($color: #000000, $alpha: 0.1);
} 
.cube-tab_active {
  color: #3bb149;
}
</style>
(1)注册页面开发

在这里插入图片描述
其中registerApi方法是从getdata中引用过来的(按需加载)也就是后端ssm写好的接口函数
整体vue布局用了cube组件

<template>
  <div class="main">
    <cube-form :model="model" @submit="submitHandler">
      
      <cube-form-group>
          <!--名称-->
        <cube-form-item :field="fields[0]"></cube-form-item>
        <!--手机号-->
        <cube-form-item :field="fields[1]"></cube-form-item>
        <!--密码-->
        <cube-form-item :field="fields[2]"></cube-form-item>
      </cube-form-group>


      <cube-form-group>
        <cube-button type="submit">注册</cube-button>
      </cube-form-group>


    </cube-form>
    <router-link to="/login" class="reg">登录</router-link> 
  </div>
</template>

<script>
//注册接口
import { registerApi } from "@/api/getData.js";
export default {
  data() {
    return {
      model: {
        phoneValue: "",
        pwdValue: "",
        nameValue: ""
      },
      fields: [ {
          type: "input",
          modelKey: "nameValue",
          label: "名称",
          props: {
            placeholder: "请输入名称"
          },
          rules: {
            required: true
          },
          messages: {
            required: "名称不能为空"
          }
        },
        {
          type: "input",
          modelKey: "phoneValue",
          label: "手机号",
          props: {
            placeholder: "请输入手机"
          },
          rules: {
            required: true
          },
          messages: {
            required: "手机号不能为空"
          }
        },
        {
          type: "input",
          modelKey: "pwdValue",
          label: "密码",
          props: {
            placeholder: "请输入密码",
            type: "password",
            eye: {
              open: false
            }
          },
          rules: {
            required: true
          },
          messages: {
            required: "密码不能为空"
          }
        }
       
      ]
    };
  },
  methods: {
    submitHandler(e, model) {
      e.preventDefault();
      //调用注册接口
      registerApi(model.phoneValue, model.pwdValue, model.nameValue).then(
        res => {
          if (res.data.code === 0) {
            const toast = this.$createToast({
              txt: "注册成功",
              type: "correct",
              time: 1500
            });
            toast.show();
          }
        }
      );
    }
  }
};
</script>
<style lang="scss" scoped>
.main {
  padding: 50px 5% 0;
  text-align: center;
}
//注册
.cube-btn {
  margin-top: 20px;
}
// 登录
.reg {
  display: inline-block;
  margin-top: 30px;
  font-size: 18px;
}
</style>

最后会传到user数据库表中
在这里插入图片描述

(2)登录页面开发

在这里插入图片描述
其中跟注册页面开发代码类似,比较大的区别是这三句,用来储存token值
并用到了跳转路由,如果登录成功跳到personal页面
在这里插入图片描述

<template>
  <div class="main">
    <cube-form :model="model" @submit="submitHandler">
      
      <cube-form-group>
       
        <!--手机号-->
        <cube-form-item :field="fields[0]"></cube-form-item>
        <!--密码-->
        <cube-form-item :field="fields[1]"></cube-form-item>
      </cube-form-group>

      <cube-form-group>
        <cube-button type="submit">登录</cube-button>
      </cube-form-group>

    </cube-form>
    <router-link to="/register" class="reg">注册</router-link> 
  </div>
</template>

<script>
//登录接口
import { loginApi } from "@/api/getData.js";
export default {
  data() {
    return {
      model: {
        phoneValue: "",
        pwdValue: ""
      },
      fields: [
        {
          type: "input",
          modelKey: "phoneValue",
          label: "手机号",
          props: {
            placeholder: "请输入手机"
          },
          rules: {
            required: true
          },
          messages: {
            required: "手机号不能为空"
          }
        },
        {
          type: "input",
          modelKey: "pwdValue",
          label: "密码",
          props: {
            placeholder: "请输入密码",
            type: "password",
            eye: {
              open: false
            }
          },
          rules: {
            required: true
          },
          messages: {
            required: "密码不能为空"
          }
        }
      ]
    };
  },
  methods: {
    submitHandler(e, model) {
      e.preventDefault();
      //调用注册接口
      loginApi(model.phoneValue, model.pwdValue).then(
        res => {
          if (res.data.code === 0) {
            //登录成功,跳转到个人中心
            localStorage.setItem('token',res.data.data)
            
            this.$store.dispatch('setToken',res.data.data)

            //跳转页面, 根据业务需要
            this.$router.push({path:'/personal'})

          }else{
             const toast = this.$createToast({
              txt: "登录失败",
              type: "error",
              time: 1500
            });
            toast.show();
          }
        }
      );
    }
  }
};
</script>
<style lang="scss" scoped>
.main {
  padding: 50px 5% 0;
  text-align: center;
}
// 登录
.cube-btn {
  margin-top: 20px;
}
//注册
.reg {
  display: inline-block;
  margin-top: 30px;
  font-size: 18px;
}
</style>
(3)个人登录页面开发

在这里插入图片描述
其中导入的defaultHeadImg是一个默认图片,如果没有登录,则默认显示此图片
其中CommonFooter组件是提前写好的尾部导航栏组件,请见下面
在这里插入图片描述
先是mounted挂载方法 如果获取到有token 则执行方法(getInfo)用于给info导入值,然后info双向可以交互到页面上
在这里插入图片描述

<template>
  <div>
    <div class="container">
        <div class="p_top">
            <div>
                <img :src='info.head_img || defaultHeadImg' alt="头像"/>
                <router-link to="/login" v-if = "getToken === ''"> 
                <p>立刻登录</p>
                </router-link>
                <p v-else>{{info.name}} </p>
            </div>
        </div>
        <button v-if="getToken !== ''" class="green" @click="signOut">
            退出登录
        </button>
    </div>
    <common-footer></common-footer>
  </div>
</template>

<script>
import CommonFooter from "@/components/CommonFooter";
import { getUserInfo } from "@/api/getData.js";
import defaultHeadImg from "@/assets/logo.png";

export default {
  components: {
    CommonFooter
  },

  data() {
    return {
      info: {},
      defaultHeadImg: defaultHeadImg
    };
  },

  computed: {
    getToken() {
      return this.$store.state.token;
    }
  },

  methods: {
    //获取用户信息
    async getInfo() {
      try {
        const result = await getUserInfo(this.getToken);
        if (result.data.code === 0) {
          this.info = result.data.data;
        }
      } catch (error) {
        console.log(error);
      }
    },

    //退出登录
    async signOut() {
      //清除token
      await this.$store.dispatch('clearToken');
      localStorage.removeItem("token");

      //刷新页面
      location.reload();
    }
  },

  mounted() {
    if (this.getToken) {
      this.getInfo();
    }
  }
};
</script>

<style lang="scss" scoped>
.container {
  // 顶部头像区域
  .p_top {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 0;
    background-color: #2c3f54;
    div {
      text-align: center;
      img {
        width: 60px;
        height: 60px;
        border-radius: 50px;
      }
      p {
        font-size: 16px;
        color: #fff;
        margin-top: 10px;
      }
    }
  }
}
// 退出登录
.green {
  display: block;
  background-color: #3bb149;
  border: none;
  outline: none;
  width: 80%;
  height: 40px;
  margin: 20px auto 0;
  color: #fff;
  border-radius: 20px;
}
</style>
(4)HOME主页面开发

在这里插入图片描述
在这里插入图片描述

1.先介绍一下Banner(轮播图)

也就是这里,这里可以自动切换图片
在这里插入图片描述
这里的props从父组件获值请见等会的Home.Vue

<template>
  <div>
    <cube-slide  :data="banners">
      <cube-slide-item
        v-for="(item, index) in banners"
        :key="index">

        <a :href="item.url">
          <img :src="item.img"  style="width:100%"/>
        </a>
      
      </cube-slide-item>
    </cube-slide>
  </div>
</template>

<script>
export default {
    //获取父组件传递过来的值
    props:{
        banners:{
            type:Array,
            required:true
        }
    }
};
</script>

<style lang="scss" scoped>
</style>
2.介绍一下VideoList(页面上的视频列表)

在这里插入图片描述

<template>
  <div class="list-content">
    <div class="list">
      <!-- 遍历视频 -->
      <router-link
        :key="item.id"
        :to="{ path: '/coursedetail', query: { video_id: item.id } }"
        class="course"
        v-for="item in videoList"
      >
        <div class="item_img">
          <img :src="item.cover_img" />
        </div>
        <div class="video_info">
          <div class="c_title">{{ item.title }}</div>
          <div class="price">{{ item.price / 100 }}</div>
        </div>
      </router-link>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    videoList: {
      type: Array,
      required: true,
    },
  },
}
</script>

<style lang="scss" scoped>
//列表包裹层边距
.list-content {
  margin-top: 20px;
  padding: 0 13px;
}
//视频包括层
.list {
  display: flex; //设置flex布局
  flex-wrap: wrap; //换行排列
  justify-content: space-between; //两端对齐
  padding-bottom: 55px;
}
//视频个体层
.course {
  width: 48%;
  margin-bottom: 17px;
}
//视频图片
.item_img {
  font-size: 0; //消除图片元素产生的间隙
  box-shadow: 0 4px 11px 0 rgba(43, 51, 59, 0.6); //设置图片阴影,rgba前三个参数是颜色编码,最后一个是透明度
  border-radius: 8px; //设置图片圆角
  img {
    width: 100%;
    border-radius: 8px;
  }
}
.c_title {
  //设置超过两行隐藏 start
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  word-break: break-all;
  //设置超过两行隐藏 end
  font-size: 11px;
  height: 26px;
  line-height: 13px;
  margin-top: 10px;
  color: #2b333b;
}
//价格
.price {
  margin-top: 8px;
  font-size: 12px;
  color: #d93f30;
}
</style>

3.总页面(拼接1 2页面)

在这里插入图片描述
这里的:就是传入值(v-blind)
然后getbanner和getvideolist方法就是获取数据库里的信息,是后端java项目写好的了
在这里插入图片描述

<template>
  <div>
    <!-- 轮播图组件 -->
    <home-banner :banners="banners"></home-banner>
    <!-- 视频列表组件 -->
    <video-list :videoList="videoList"></video-list>
    <!-- 底部导航栏组件 -->
    <common-footer></common-footer>
  </div>
</template>


<script>
import HomeBanner from "./Component/Banner";
import VideoList from "./Component/VideoList";
import CommonFooter from "@/components/CommonFooter";
import { getBanner, getVideoList } from "@/api/getData.js";

export default {
  //注册组件
  components: {
    HomeBanner,
    VideoList,
    CommonFooter
  },
  //声明数据源
  data() {
    return {
      banners: [],
      videoList: []
    };
  },
  //定义方法
  methods: {

     // 获取轮播图数据
    async getBannerData() {
      try {
        const result = await getBanner();
        console.log(result);
        console.log(result.data.code == 0)
        if (result.data.code == 0) {
          this.banners = result.data.data;
        }
      }catch(error){
          console.lo(error)
      }
    },

    //获取视频列表
    async getVList(){
        try{
            const result = await getVideoList();
            if (result.data.code == 0) {
                this.videoList = result.data.data;
            }
        }catch(error){
            console.lo(error)
        }
    }
  },
  mounted(){
      //页面渲染完成调用方法获取数据
      this.getBannerData();
      this.getVList()
  }
};
</script>

<style lang="scss" scoped></style>

在这里插入图片描述

(5)视频详细章集信息开发

在这里插入图片描述
在这里插入图片描述

1.总页面(子组件全部导入)
<template>
    <div>
        
        <!--顶部返回组件-->
        <detail-header :videoInfo="videoInfo"></detail-header>

        <!--视频介绍组件-->
        <detail-course :videoInfo="videoInfo"></detail-course>

        <!--视频tab简介组件-->
         <detail-tab :videoInfo="videoInfo" :chapterList="chapterList"></detail-tab> 

        <!--底部立刻购买-->
        <footer>
            <router-link :to="{path:'/pay',query:{video_id:this.$route.query.video_id}}" class="user_buy">
                <button>立刻购买</button>            
            </router-link>
        </footer> 

    </div>
</template>

<script>
//引入组件
import DetailHeader from './Components/Header'
import DetailCourse from './Components/Course'
import DetailTab from './Components/Tab'

import { getVideoDetail } from "@/api/getData.js";


export default {
    //注册组件
    components:{
        DetailHeader,
        DetailCourse,
        DetailTab
    },

    data(){
        return {
            //视频信息
            videoInfo:{},
            //章集
            chapterList:[]
        }
    },

    methods:{
        //获取视频详情
        async getDetail(vid){
            try{
               const result =  await getVideoDetail(vid)
               if(result.data.code == 0){
                   this.videoInfo = result.data.data;
                   this.chapterList = result.data.data.chapter_list;
               }

            }catch(error){
                console.log(error)
            }
        }
    },

    mounted(){
        //渲染完成后拿数据
        console.log(this.$route.query.video_id)
        this.getDetail(this.$route.query.video_id);

    }
}
</script>

<style lang="scss" scoped>
//底部
footer {
  // fixed固定在底部
  position: fixed;
  bottom: 0;
  width: 100%;
  padding: 8px 0;
  background-color: #fff;
  z-index: 999;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.05);
}
//设置购买按钮样式
button {
  display: block;
  color: #fff;
  margin: 0 auto;
  background-color: #d93f30;
  height: 34px;
  line-height: 34px;
  border-radius: 17px;
  width: 80%;
  border: none;
  font-size: 15px;
  text-align: center;
}
</style>
2.目录组件
<template>
    <div class="cate_box">

        <div>
            <ul class="content" v-for="(item, ind) in chapterList" :key="item.id">
                    <h1>{{ind +1}}&nbsp;{{item.title}} </h1>

                    <li class="sub_cate" v-for="(item,subind) in chapterList[ind].episode_list" :key="item.id">
                    <span class="sub_title">{{ind+1}}-{{subind+1}} &nbsp;{{item.title}}  </span>    
                    </li>
            </ul>

        </div>

    </div>

</template>

<script>
export default {
    //从父组获取章集信息
    props:{
        chapterList:{
            type:Array,
            required:true
        }
    }

}
</script>

<style lang="scss" scoped>
// 目录包裹层设置边距
.cate_box {
  padding: 0 15px 50px;
  background-color: #fff;
  margin: 15px 0;
}

//每一章包裹层
.content {
  padding: 10px;
  // 章标题
  & h1 {
    font-size: 16px;
    width: 100%;
    margin-bottom: 15px;
    font-weight: bolder;
    // 设置章标题过长,超过行宽度省略隐藏
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}

//集包裹层
.sub_cate {
  font-size: 12px;
  padding: 10px 0;
  //集标题
  .sub_title {
    // 设置集标题过长,超过行宽度省略隐藏
    display: block;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}
</style>
3.课程信息组件

在这里插入图片描述

<template>
    <div class="c_wrapper">
        <!-- 视频信息缩略层 -->
        <div class="course">

            <div class="l_img">
                <img :src="videoInfo.cover_img" :title="videoInfo.title">
            </div>

            <div class="r_txt">
                <div class="txt">
                <span>综合评分:</span>
                <p>{{ videoInfo.point }}</p>

                </div>
                <div class="txt">
                <span>价格:</span>
                <p>{{ videoInfo.price/100 }}</p>
                </div>
                
            </div>


        </div>
  </div>
</template>

<script>
    export default {
        // 从父组件获取视频信息
        props: {
            videoInfo: {
                type: Object,
                required: true
            }
        }
    }
</script>

<style lang="scss" scoped>
//包裹层
.c_wrapper {
    padding: 0 14px;
}
//视频信息包裹层
.course {
  margin:14px 0;
  display:flex;//设置flex,左右布局
}
//视频左边图片层
.l_img {
  height:88px;
  margin-right:14px;
  & img {
    height:100%;
    border-radius:15px;
  }   
}
// 视频右边文字包裹层
.r_txt {
    padding:6px 0;
    font-size:12px;
    flex:1;//设置1可自动伸缩占用剩余空间
}
//每行文字层(综合评分、价格)
.txt {
    // 设置flex让文字两端对齐
    display:flex;
    justify-content:space-between;
    line-height:16px;
    & p {
        text-align:center;
        width:40%;
        color:#3bb149;
    }   
    & i {
        color:#666;
        font-weight:bolder;
        width:60%;
        & span {
            color:#2b333b;
            font-size:12px;
        }
    }      
}
  
</style>
4.头部组件

在这里插入图片描述
在这里插入图片描述
返回上一组件

<template>
    <div>
        <header>
            <div class="header">
                <span @click="$router.back(-1)"> <i class="cubeic-back"></i> </span>
                <div class="title">
                    {{videoInfo.title}}
                </div>
            </div>
        </header>

    </div>
</template>

<script>
export default {
    
    props:{
        videoInfo:{
            type:Object,
            required:true
        }
    }


}
</script>

<style lang="scss" scoped>
.header {
    display: flex;//flex左右布局
    background-color: #07111b;
    padding: 10px 20px;
    color: #fff;
}  
// 返回箭头
.cubeic-back {
    color: #fff;
    margin-right:5px;
}
//视频标题
.title {
    font-size: 16px;
    width: 80%;
    //超出省略
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}
</style>
5.简介中的图片组件

在这里插入图片描述

<template>
    <div> <img class="summary" :src="videoInfo.summary"/> </div>
</template>
<script>
export default {
    props:{
        videoInfo:{
            type:Object,
            required:true
        }
    }
}
</script>

<style lang="scss" scoped>
.summary {
  width:100%;
  padding-bottom:50px;
  margin:15px 0;
}
  
</style>
6.导航栏组件(分简介和目录)
   <component :videoInfo="videoInfo" :chapterList="chapterList" :is='selectedLabel==="简介"?"Summary":"Catalog" '>
    </component>

上面代码用来判断是简介还是目录 显示对应组件

<template>
    
    <div>
        <cube-tab-bar v-model="selectedLabel" show-slider>
            <cube-tab v-for="item in tabs" :label="item.label" :key="item.label">
            </cube-tab>
        </cube-tab-bar>
        
        <component :videoInfo="videoInfo" :chapterList="chapterList" :is='selectedLabel==="简介"?"Summary":"Catalog" '>
        </component>

    </div>

</template>

<script>
import Summary from './Summary'
import Catalog from './Catalog'

export default {

    components:{
        Summary,
        Catalog
    },

    props:{
        videoInfo:{
            type:Object,
            required:true
        },
        chapterList:{
            type:Array,
            required:true
        }

    },

    data(){
        return{
            selectedLabel:"简介",
            tabs:[
                {
                    label:"简介"
                },{
                    label:"目录"
                }
            ]
        }
    }
}
</script>
(6)支付开发

在这里插入图片描述
其中页面布局中穿插到了videoinfo变量的引用,用到了{{}}
其中getDatil是获取视频详情,如果请求成功,则返回数据给videoInfo,最终渲染到页面上( 请见所有的方法接口)
其中主要函数pay调用了saveOrder方法,传入了token和视频id号两个参数,然后返回result 判断是否成功。pay函数是@click点击调用
在这里插入图片描述
一开始挂载的是mounted方法,调用的是getDetail方法,传入的参数很重要
在这里插入图片描述
点击对应的视频,则query会传入对应的参数,并且路由会跳转到对应页面
在这里插入图片描述

<template>
  <div>
    <!--视频信息-->
    <div class="info">
      <p class="info_title">商品信息</p>
      <div class="box">
        <div class="imgdiv">
          <img alt="课程照片" :src="videoinfo.cover_img" />
        </div>
        <div class="textdiv">
          <p class="c_title">{{videoinfo.title}}</p>
          <p class="price">:&nbsp;&nbsp; {{(videoinfo.price / 100).toFixed(2)}}</p>
        </div>
      </div>
    </div>
    <!--顶部支付-->
    <div class="footer">
      <p class="money">实付:&nbsp;&nbsp; {{(videoinfo.price / 100).toFixed(2)}}</p>
      <p class="submit" @click="pay">立刻支付</p>
    </div>
  </div>
</template>

<script>
import { getVideoDetail, saveOrder } from "@/api/getData.js";

export default {
  data() {
    return {
      videoinfo: {}
    };
  },
  methods: {
    //获取视频详情
    async getDetail(vid) {
      try {
        const result = await getVideoDetail(vid);
        if (result.data.code == 0) {
          this.videoinfo = result.data.data;
        }
      } catch (error) {
        console.log(error);
      }
    },

    //下单
    async pay() {
      try {
        const result = await saveOrder(
          this.$store.state.token,
          this.$route.query.video_id
        );

        if (result.data.code == 0) {
          const toast = this.$createToast({
            txt: "购买成功",
            type: "correct",
            time: 2000,
            onTimeout: () => {
              this.$router.push({ path: "order" });
            }
          });
          toast.show();
        }else{
             const toast = this.$createToast({
            txt: "我擦",
            type: "error",
            time: 1500
          });
          toast.show();
        }
      } catch (error) {
        console.log(error);
      }
    }
  },
  mounted() {
    this.getDetail(this.$route.query.video_id);
  }
};
</script>


<style lang="scss" scoped>
// 视频标题
.info_title {
  padding: 10px 20px;
  background-color: #fff;
  border-bottom: 1px solid #d9dde1;
}

.box {
  background-color: #fff;
  box-sizing: border-box;
  padding: 20px;
  display: flex;
  margin-bottom: 15px;
  .imgdiv {
    width: 105px;
    height: 59px;
    flex-shrink: 0;
    img {
      width: 100%;
      height: 100%;
    }
  }

  .textdiv {
    margin-left: 20px;
    height: 59px;
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    .price {
      flex-shrink: 0;
    }
  }
}
//底部
.footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 50px;
  background-color: #fff;
  display: flex;
  justify-content: space-between;
  box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.1);
  font-size: 16px;
  .money {
    height: 50px;
    line-height: 50px;
    flex: 2;
    text-align: center;
    background-color: #fff;
  }
  .submit {
    height: 50px;
    line-height: 50px;
    flex: 1;
    text-align: center;
    background-color: #ff2d50;
    color: #fff;
  }
}
</style>
(7)订单页面开发

在这里插入图片描述

<template>
    <div class="main">
        <!--订单列表-->
        <div class="list" v-if="orders.length > 0">
            
            <div class="box" v-for="(item, index) of orders" :key="index">
                <router-link :to="{path:'/coursedetail', query:{ video_id : item.video_id }}">
                    <div class="smallbox">
                        
                        <div class="imgdiv">
                            <img :src="item.video_img" alt="小滴课堂课程图片"/>
                        </div>

                        <div class="textdiv">
                            <p class="title"> {{item.video_title}} </p>
                            <p class="price"> {{(item.total_fee / 100).toFixed(2)}}</p>
                        </div>

                    </div>
                </router-link>
            </div>

        </div>

        <div class="no_order" v-else>
            <p>暂未购买课程 </p>
        </div>

    <!--底部导航-->
        <common-footer></common-footer>

    </div>
</template>
<script>

import CommonFooter from '@/components/CommonFooter'
import { getOrderList } from "@/api/getData.js";

export default {

    components:{
        CommonFooter
    },

    data(){
        return{
            orders:[]
        }
    },

    methods:{
        //获取订单列表
    async getOrderList(){
        try{
            const result =  await getOrderList(this.$store.state.token)
            if(result.data.code == 0){
                this.orders = result.data.data || []
            }

        }catch(error){
            console.log(error)
        } 
    }

    },
    mounted(){
        this.getOrderList();
    }
    
}
</script>

<style lang="scss" scoped>
.list {
  padding: 0 20px;
}

// 视频个体
.box {
  padding: 20px 0;
  background-color: #fff;
  border-bottom: 1px solid #ddd;
  // 标题
  .title {
    font-size: 14px;
    margin-bottom: 15px;
  }
  // 订单详情
  .smallbox {
    //flex左右排列,两端对齐
    display: flex;
    justify-content: space-between;
    .imgdiv {
      width: 90px;
      height: 60px;
      flex-shrink: 0;
      img {
        width: 100%;
        height: 100%;
        border-radius: 10px;
      }
    }
    .textdiv p {
      margin-top: 10px;
      padding-left: 10px;
    }
  }
}

.no_order {
  margin-top: 50px;
  text-align: center;
}
</style>

三.阿里云服务器部署

正在更

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值