前言:
需要源码评论或私我
项目技术栈如下图所示:
本次博客分前后端+部署服务器三个步骤来写
先来看看实现效果:
在线教育系统完整三步骤
一.后端技术栈
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类里面是如何书写的这个字段的
其中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}}章 {{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}} {{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">¥: {{(videoinfo.price / 100).toFixed(2)}}</p>
</div>
</div>
</div>
<!--顶部支付-->
<div class="footer">
<p class="money">实付: {{(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>
三.阿里云服务器部署
正在更