仿B站项目

我自己花钱买的项目,如果涉及到侵权的话,联系我即可,微信号Dankedemimi

RESTful接口:

RESTful 接口:Representational State Transfer,中文为表述性状态转移,REST指的是一组架构约束条件和原则。

RESTful表述的是资源的状态性转移,在Web中资源就是URI(Uniform Resource Identifier)。

HTTP方法的语义:

GET                       获取指定的资源

DELETE                删除指定的资源

POST                     发送数据给服务器 1.在公告板,新闻组,邮件列表或类似的文章组中发布消息                                 2.通过注册新增用户 3.向数据处理程序提供一批数据,例如提交一个表单。

说白了POST 做的就是新增操作,将资源提交给服务器的。

PUT                        使用请求中的负载创建或者替换目标资源。PUT和POST的区别在于PUT是幂                                 等的,而POST不是。幂等的含义可以理解为调用一次与连续调用多次是等价                                  的。

POST和PUT,POST表示创建资源,PUT表示更新资源。而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。POST不具有幂等性,其他全部具有幂等性。

RESTful接口URL命名规则:

1.HTTP方法后跟的URL必须是名词且统一成名词复数形式

2.URL中不采用大小写混合的驼峰命名,尽量采用全小写单词,如果需要连接多个单词,则采用“-”连接。

RESTful接口URL分级原则:

1.一级用来定位资源分类,如/users即表示需要定位到用户相关资源。

2.二级仍然用来定位具体某个资源,如果/users/20即表示id为20的用户,再入/users/20/fans/1即表示id为20的用户的id为1的粉丝。

通用功能与配置:

加解密工具:com.imooc.bilibili.service.util

Json数据返回类:com.imooc.bilibili.dao.domain

Json信息转换配置:com.imooc.bilibili.service.config

全局异常处理配置:com.imooc.bilibili.service.handler  (设置在com.imooc.bilibili.domain.exception包下,是各种的异常)

加密工具:AES,RSA,MD5

RSA:非对称加密算法,有公钥和私钥之分,公钥用来数据加密,私钥用来数据解密。公钥一般是放在外部进行加密的,比如我们会将这个公钥发送给前端,当用户提交密码的时候,我们不可能让他直接明文传输到后端,所以会使用公钥给这个用户的信息加密。私钥需要被放置在服务器端保证安全性。

加密安全性很高,但是加密速度很慢,所以在这个项目中,我们让RSA加密来完成用户登录的加密,因为这个操作需要很高的安全性,但是用户只需要登陆一次就可以进行别的操作了,所以速度也是可以慢一些的。

AES:对称加密算法,加密和解密使用相同的密钥。加密速度非常快,适合经常发送数据的场合。

MD5:单向加密算法,加密速度非常快,不需要密钥,但是安全性不高。需要搭配随机盐值使用。

用户注册与登录:

这里用到了两个数据库表,分别是t_user,t_user_info

使用API,Service,Dao三层的操作完成了用户的注册和登录功能。

基于JWT的用户token验证:

用户在浏览器中提供用户名和密码,并且将其发送给服务器。然后服务器会进行一个验证,如果提供的用户信息都是正确的,服务器就会生成一个JWT令牌,JWT令牌里面会包含一些声明(例如用户ID,用户名,过期时间等)和密钥。JWT本身是无状态的,服务器不需要在本地存储令牌信息。服务器会将生成的JWT令牌返回给浏览器。浏览器会将JWT令牌存储在本地的localstorage或者sessionstorage里面,二者的区别是,localstorage的数据没有过期时间,会一直保存在浏览器中,但是sessionStorage的数据仅仅会在当前会话中有效,关闭浏览器或者标签页之后就会被清除。如果当用户需要访问一些会保护的信息时,可以将JWT令牌加入到请求的请求头或者请求体中,然后发送给服务器。服务器收到了JWT令牌之后,会验证一下里面的用户信息是否正确并且看一下是否在有效期内。如果都符合,就证明是有效的,服务器端会处理请求并且返回响应的数据,否则返回未授权的错误。

JWT(JSON WEB TOKEN):是一种规范,包含着三部分,头部(header),载荷(payload),签名(signature)。

JWT头部:声明的类型,加密算法等(通常使用SHA256)

JWT载荷:存放有效信息,一般包含签发者,所面向的用户,接受方,过期时间,签发时间以及唯一身份标识(在项目中就是userId)。

JWT签名:主要由头部,载荷以及秘钥组合加密来组成。

优点:安全性高,分布式系统下扩展性强。因为token并不会存储在某一个服务器上面,而是存储在前端。服务端做的只是验证token罢了。而且还有跨语言支持,无论是PHP,Java什么的语言都可以接受。

public class TokenUtil {

    private static final String ISSUER = "签发者";

    public static String generateToken(Long userId) throws Exception{
        Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
        //这里设置的是过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.SECOND,30);

        //withKeyId---->唯一的身份标识
        //withIssuer---->签发者
        //withExpiresAt---->过期时间30秒
        //sign---->生成签名的方法,使用RSA加密来将刚才的所有信息进行加密。
        return JWT.create().withKeyId(String.valueOf(userId))
                .withIssuer(ISSUER)
                .withExpiresAt(calendar.getTime())
                .sign(algorithm);
    }
    //这里的异常不要抛出去。
    public static Long verifyToken(String token){
        //在这里前端发过来的token,如果出了异常要捕捉到,并且把这个特定的状态码返回给前端,这样前端可以
        //来调整,比如说过期了的话,可以刷新。
        try{
            Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
            JWTVerifier verifier = JWT.require(algorithm).build();
            //解密后的JWT就是DecodedJWT这个类型的
            DecodedJWT jwt = verifier.verify(token);
            String userId = jwt.getKeyId();
            return Long.valueOf(userId);
        }catch(TokenExpiredException e){
            throw new ConditionException("555","token过期了!");
        }catch(Exception e){
            throw new ConditionException("非法用户token!");
        }
    }
}

这里的应用场景就是,只要你登陆成功,我就把token发给你,但是你要想获取用户信息,你再拿着你的token过来,让我验证一下,我把你的加密的token剥开了,里面的唯一用户标识拿出来,然后根据唯一用户标识,我再查user(这里的user多加了一个userInfo的属性,然后也会查到userInfo的信息,用user.serUserInfo(userInfo)这种方法给封装进去,然后返回给前端)。基本上就是说,只要前端带着数据来就一定要放在token中,然后发给后端这个token。让后端去检验你的token是否是正常的。

在后面,修改了这个token登录验证的思想。我使用的是双令牌实现登录升级。

如果使用单单使用AccessToken的形式实现,那么可能会出现的情况是,如果我正在使用B站这个软件,那么有可能就是会突然发现我的token过期了,然后立马要求我重新登陆,影响我的使用体验。还有一种可能性就是,我的token没有过期,但是我退出登录了,但是还可以做那些只有在登录情况下做的操作。

为了避免这种情况会发生。我设计了一个双token验证登录的方法。

首先是用户第一次登录成功的时候,后端会生成两个token,分别是AccessToken和RefreshToken这两个,会将这两个东西封装在Map<String,String>中,一起发送给前端,前端依然存在自己的localStorage里面。每次登录的时候也只是从AccessToken中提取出唯一标识的UserId,然后验证。但是不一样的是,当我后端第一次生成AccesToken和RefreshToken的时候,我会将RefreshToken存在数据库中的t_refresh_token的这个数据表中。然后设置这个的过期时间是7天,然后AccessToken设置的过期时间是1小时,也就是说一小时到了就要刷新一下AccessToken。当我AccessToken过期的时候,前端会发送一个HttpServletRequest请求,我调用getHeader()方法,从请求头中提取出RefreshToken,然后去后端数据库找一找这个RefreshToken,并且从RefreshToken中将UserId获取。如果我找到了,那么就刷新一下AccessToken就可以了,这里的刷新AccessToken其实就是后端给前端返回一个return TokenUtil.generatedToken(userId)。也就是说重新发送一下,如果没有找到的话,那么就是给前端返回一个异常,异常的内容是”token过期“,异常的code是555,告诉前端,你真的需要重新登陆了。那么最后一个流程是,当我要退出登录的时候,会将数据库里面对应的RefreshToken也删除,那这样就保证了,我不会在没有RefreshToken的情况下去做更多的操作,因为一个小时到期的时候,发现数据库中refreshToken也过期了,这个时候就还是要给前端返回一个异常,异常内容是token已经过期了,异常code值是555。

用户关注与动态提醒模块:

得到一个用户关注的所有的up主的用户列表+基本信息+给这些用户进行分类:

得到一个用户关注的所有的信息这个操作很简单,首先有一个userId,在t_user_following这张表里面按照userId,查出所有的userFollowing,然后提取出每一个UserFollowing的followingID,这里的followingId其实就是所有被userId关注的up主的所有的userId。然后有了这些所有up主的userId之后,我就可以查找到所有这些up主的用户信息,通过userService里面的userDao,给userDao新添加一个方法————》getUserInfoByUserIds

这个方法如下:在user.xml中

<select id="getUserInfoByUserIds" resultType="com.imooc.bilibili.domain.UserInfo">
         select
            *
         from
            t_user_info
         where
             1=1
             <if test="userIdList !=null and userIdList.size > 0">
                 and userId in
                <foreach collection="userIdList" item = "userId" index = "index" open="(" close=")" separator=",">
                    #{userId}
                </foreach>
             </if>
</select>

然后经过一个遍历:

for(UserFollowing userFollowing : list){
    //按理来说应该是一一对应的,但是为什么这么写,也是因为
    //在UserDao中用userId去查userInfo的时候,有一个where条件后面是1 = 1 and userId in.....
    //有可能走的是1=1这个条件,所以必须得有这个双循环筛选。
    for(UserInfo userInfo : userInfoList){
        if(userFollowing.getFollowingId().equals(userInfo.getUserId())){
            userFollowing.setUserInfo(userInfo);
        }
    }
}

也就是说给userFollowing多加了一个属性是UserInfo,也就是说每一个userFollowing都带有着被关注者的userInfo(用户信息)。

现在就其实已经得到了所有的用户信息了,但是我们还想实现额外的一个事情就是将这些被关注者分为特别关心,悄悄关注,默认分组这三部分全部都输出到前端上。所以需要有第三步,也就是接下来的操作。

首先给FollowingGroup这个类加一个属性,就是followingUserInfoList,这个属性包含着特别关心,悄悄关注以及默认分组中的其中一个组的所有的up主的用户信息。这里切记,FollowingGroup这个数据表,第一个字段id的种类就只有3个,分别是1,2,3!!!!!!!然后下面找到FollowingGroup和UserFollowing的关联的字段是哪个,会发现其实FollowingGroup中的id就是UserFollowing中的groupId,以这个为条件去筛查,都为1的就全放到infoList中,然后将infoList再放到第一个FollowingGroup的(一共3个)FollowingUserInfoList这个属性中,以此类推,都为2的放到第二个FollowingUserInfoList的属性中,都为3的........每一个FollowingGroup中都有该类分组的所有被关注的up主的全部用户信息。但是这里多了一个result结果,装了4个元素,返回给前端的。
 

//第三步了
List<FollowingGroup> groupList = followingGroupService.getByUserId(userId);
FollowingGroup allGroup = new FollowingGroup();
allGroup.setName(UserConstant.USER_FOLLOWING_GROUP_ALL_NAME);
allGroup.setFollowingUserInfoList(userInfoList);
List<FollowingGroup> result = new ArrayList<>();
result.add(allGroup);
for(FollowingGroup group : groupList){
    List<UserInfo> infoList = new ArrayList<>();
    for(UserFollowing userFollowing : list){
        if(group.getId().equals(userFollowing.getGroupId())){
            infoList.add(userFollowing.getUserInfo());
        }
    }
    group.setFollowingUserInfoList(infoList);
    //这里其实是只有4个元素
    //第一个元素是全部关注的对象
    result.add(group);
}
return result;

得到一个用户的粉丝列表+查询是否是互关:

先通过userId,在userFollowing这张表中找出所有的粉丝,也就是说followingId=userId的那帮人,就都是该用户的粉丝。再从userFollowing中,提取出所有粉丝的id,然后通过id查找出所有粉丝的userInfo,也就是所有粉丝的基本信息。然后给userInfo添加一个新字段是boolean类型的,followed,给所有的followed的初始状态赋为false。然后在通过userId找到该用户关注的up主,检查用户关注的up主这个群体有没有用户的粉丝,如果有,那就是互相关注。

/是否被关注的状态进行了初始化
for(UserFollowing fan : fanList){
    for(UserInfo userInfo : userInfoList){
        if(fan.getUserId().equals(userInfo.getUserId())){
            userInfo.setFollowed(false);
            //粉丝的信息
            fan.setUserInfo(userInfo);
        }
    }
    //这个用户关注的up主都有哪些 全存在了followingList里面
    for(UserFollowing following : followingList){
        //该用户关注的人里面有没有该用户的粉丝
        if(following.getFollowingId().equals(fan.getUserId())){
            fan.getUserInfo().setFollowed(true);
        }
    }
}
return fanList;

UserFollowing是有userInfo这个字段的,之前添加过,然后userInfo里面有followed这个字段的。如果是互相关注的话,里面这个followed字段一定是true。

UserFollowing包含userInfo字段,userInfo包含followed字段。

这个是实现的分页查询。

//分页查询的接口
@GetMapping("/user-infos")
public JsonResponse<PageResult<UserInfo>> pageListUserInfos(@RequestParam Integer no,@RequestParam Integer size,String nick){
    Long userId = userSupport.getCurrentUserId();
    JSONObject params = new JSONObject();
    //no是页数
    params.put("no",no);
    //每页的数量
    params.put("size",size);
    params.put("userId",userId);
    //这里返回的都是查找到的up主,但是这里面有的是已经关注过的。
    PageResult<UserInfo> result = userService.pageListUserInfos(params);
    if(result.getTotal()>0){
        List<UserInfo> checkUserInfoList = userFollowingService.checkFollowingStatus(result.getList(), userId);
        result.setList(checkUserInfoList);
    }
    return new JsonResponse<>(result);
}

在这里新建了一个类叫做PageResult类,存储的是LIst<UserInfo>。首先从UserSupport类里面把userId拿到,然后传参的时候也要传no(代表的页数),还有参数要传的是size,每页的大小。然后就传入到JSONObject这个类里面,第一步先查出到底一共有多少个符合nick模糊查询的数据。然后将起始数据start和每页要查多少条数据都传给xml,继续搜寻。找到所有的userInfo然后返回给PageResult,也返回给API层。然后最后检查遍历一下,看看分页查询找到的这些个用户里面,有哪些是已经关注的了,把他们标注出来!!!

动态提醒:

观察者模式中观察者和主题之间是松耦合的关系,他们之间没有代理人。但是订阅发布模式中,发布者和订阅者之间是有代理人的。发布者和订阅者彼此之间并不知道对方,完全由代理人来执行事项。

订阅发布模式:

订阅发布模式是一种消息传递模式,发布者不会将发布的动态直接传递给订阅者,而是将消息分类别的发布,订阅者可以订阅一个或多个他们感兴趣的类别,从而只接受他们感兴趣的消息。这种模式大大降低了发布者和订阅者之间的耦合性。而且支持动态的传递消息,系统更加灵活。

缺点:增加了一定的未知性,因为发布者不知道他们的消息被谁接收了。如果订阅者处理消息的速度慢,可能会导致消息的堆积,影响系统的性能。

观察者模式:

观察者模式定义了对象之间的一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于他的对象全部都会接收到状态改变的通知并且被自动更新。建立了一种一对多的通信机制,当一个对象状态改变的时候,所有依赖的对象都会接收到通知。

缺点:观察者模式中如果存在循环依赖可能会导致系统崩溃。

订阅发布模式和观察者模式的适用场景是什么?

订阅发布模式和观察者模式的优缺点分别是什么?

在实际的软件项目中,你是如何选择和使用这两种模式的?

这两种模式在处理并发和异步问题时有何不同?

如果有一个大量的观察者和订阅者,你会如何优化这两种模式?

动态提醒:

这里使用RocketMQ:纯Java编写的开源消息中间件,特点是:高性能,低延迟,分布式事务。

Redis:高性能缓存工具,数据存储在内存中,读写速度非常快。

具体实现例子就是:UP主发表了一个动态,然后这个消息会发送给每一个订阅的人,但是这个动态的数据就存在Redis当中,发布的消息,每一个订阅的人都能够通过操作Redis数据库去拿到这个数据并且使用他们。

RocketMQ的知识点:

RocketMQ 有一个name server(名称服务器),还有一个broker(代理服务器)。百分之七八十的工作量都是由代理服务器来完成的。

RBAC权限控制:

 RBAC权限控制模型————>>Role-Based Access Control

RBAC是基于角色的权限控制。模型主要分为4个层级,分别是RBAC0,RBAC1,RBAC2,RBAC3。在这个项目中围绕着RBAC模型一共有5个关键词,1.用户:注册用户  2.角色:会员0级——会员6级 3.权限:视频投稿,动态发布,各种弹幕功能 4.资源:页面和一些页面元素 5.操作:点击按钮,跳转或者增删改查等都算是操作。

t_user_role 是 t_user 和 t_auth_role 的关联表 也就是用户表和角色表的关联表。

t_auth_role_element_operation 是 t_auth_role 和 t_auth_element_operation 的关联表 也就是角色表和页面元素操作表。

t_auth_role_menu 是 t_auth_role 和 t_auth_menu 的关联表 也就是角色表和页面访问表的关联表。

这里如果为了避免连表查询,我在t_auth_role中加入了两个属性,分别是:roleName和roleCode。记录role里面的两个属性。然后这里我还在t_auth_role_element_operation里面加入了一个冗余属性——AuthElementOperation。在t_auth_role_menu里面加入了一个冗余属性——AuthMenu。所以在写XML语句的时候涉及到联合查询但是记得要设计一个resultMap映射。

SpringAOP编程:

前面讲的是如何控制前端页面和页面元素的权限,后面这部分要讲的是如何控制后端数据的权限。

StringAOP术语:

连接点(join point):对应的被拦截的对象。

切点(point cut):通过正则或指示器的规则来适配连接点。

切面(aspect):可以定义切点,各类通知和引入的内容。

通知(advice):分为前置通知(before),后置通知(after),事后返回通知(afterReturning),异常通知(afterThrowing)。

织入(weaving):为原有服务(service)对象生成代理对象,然后将与切点匹配的连接点拦截,并将各类通知加入约定流程。

目标对象(target):被代理对象。

这里我使用了SpringAOP编程的思想来解决这个问题的。

第一个切面类:ApiLimitedRoleAspect 接口权限控制

对于后端的权限控制,主要是两个方面,一个是能不能获取到这个接口,能不能进入这个接口,这个API。首先会根据userId,获取到这个userId所有的对应的Role(角色),获取这个比较简单,因为user和role之间有一个关联的表,我可以得到UserRole这个类,然后直接从这个类里面得到RoleCode,那么这个userId对应的所有的roleCode都放在一个集合里面。然后每一个接口前都有一个限制的禁止集合类,在进入接口前都会先验证一下,这两个集合是不是有交集,如果有交集的话,那么就不能执行这个接口的操作。

第二个切面类:DataLimitedAspect 数据权限控制

通过joinPoint这个类,执行getArgs()这个方法,得到我们要增强的这个方法的参数,看看参数里面有没有Usermoment这个类型的参数,如果有的话,我们调用一下他们的getType()方法,得到他们的type类型。然后跟我们自己的角色的code比较一下,因为每一个code对应一个type。通过将code和type对比。我们就可以知道要不要给数据操作权限了。

进入第四章了:加油!!!!!

视频与弹幕功能(分为视频和弹幕两部分):

FastDFS文件服务器搭建,相关工具类开发:

FastDFS是开源的轻量级分布式文件系统,用于解决大数据量存储和负载均衡等问题。

优点:支持HTTP协议传输文件(结合Nginx),对文件内容做Hash处理,节约磁盘空间,同时也支持负载均衡。

缺点:相比较OSS来说整体性能差。

适用于中小系统。

FastDFS的两个角色:

1.跟踪服务器(Tracker):前端去请求一个文件资源的时候,不是先去找存储服务器的,而是先去找Tracker,Tracker作为一个中介,会告诉客户端他这个请求应该去哪个storage节点,这种设计会在很大程度上减轻单个Storage节点的压力,同时也提高了整个系统的并发处理能力和容错能力。Tracker也会负责管理所有的Storage节点,包括记录每个节点的状态,容量,读写负载。

2.存储服务器(Storage):存储视频文件,是以组(Group)为单位,每个组内可以有多台存储服务器,在一个组内,数据是互相备份的,也就是说在同一个组里面,一个服务器有其他服务器各自的内容。除了文件的存储,连同文件属性的存储(Meta Data)都保存在存储服务器上。

Nginx:

是一个反向代理的服务器,代理其实就是中间人,客户端通过代理发送请求到互联网上的服务器。

Nginx的主要特点:跨平台,跨系统。配置很简单,内存消耗比较小,稳定性高。

Nginx在这个项目中的作用:1.反向代理 2.负载均衡(因为Nginx后面也有很多的Tracker,将Tracker的配置们都写入到Nginx的服务器里面,从而让Nginx来选择使用哪一个Tracker,这一步也可以理解为负载均衡)所以其实这个项目有两层的负载均衡。

正向代理的特点:

服务端不知道客户端,客户端知道代理端。

服务端看到的是从代理端发送过来的。 

反向代理的特点:

服务端知道客户端,客户端不知道代理端。

客户端不知道代理端存在的,他以为自己是在和服务端进行交互。但实际上请求是发送到了代理端,由代理端再来给服务器进行发送。

整个的流程:

关于FastDFSUtil这个类的封装:

断点续传:

如果传大文件会出现的问题:

1.文件过大,带宽就紧张,请求速度就下降。

2.如果上传过程中上传失败了,那么就需要重新上传,非常让人崩溃。

断点续传其实就是给大文件进行分片,拆分成无数个小文件,每个小文件就几兆,把这些分片按照顺序一个一个的上传,好处就是当我们碰到断点的时候(就是上传时候受到的中断等),我们直接从断点开始重新上传后面的内容即可。

去查看FileAPi,其中有两个方法,分别是uploadFileBySlices(MultipartFile slice,String fildMd5,Integer sliceNo, Integer totalSliceNo) throws Exception{}和convertFileToSlices(MultipartFile multipartFile)。第一个是处理分片文件的,使用到了Redis,第二个是将文件分片的方法,使用到了RandomAccessFile,这个可以随机访问的类,可以在文件中任意一个位置进行随机读取。

秒传:

首先是要获得文件的MD5哈希值,在这里后端会返回一个32位的16进制的字符串,以下是生成MD5哈希值的过程:

以下是秒传的逻辑代码:

首先先根据MD5哈希值查一下,数据库里面有没有我们要的文件,如果没有的话那么就执行断点续传,将我们自己的文件存入数据库中,然后返回给前端文件路径(url)。如果有的话,在数据库里面通过查找MD5哈希值找到了我们要的文件,那么就将查找到的文件的url也返回给前端,总之返回的都是url。

但其实在实际开发中,断点续传和对文件MD5加密都是由前端来完成的。

视频投稿:

瀑布流视频列表:

视频在线播放:

我们前端在请求一个视频资源的时候,其实是会自动进行分片请求的,也就是一片一片的请求我们想要的视频,其实这个视频在线播放功能和视频下载的功能是一样的。Request Header里面有一个Range属性,Range:412983034-495240385029 这个代表多少字节到多少字节,这个就是请求的视频的分片部分。前端会不断地请求,以此来使得视频可以连续播放。但是有一个问题就是,当前端获取视频分片成功的时候,会将文件的路径返回到前端,而这样我们就可以复制下来这个路径自己去下载了,会对B站造成损失,因为比如说,我们只允许会员才能够下载视频,那么如果这样的话,普通用户也可以去下载视频了。而且也不够安全,因为前端的代码可以通过开发者工具看见。

这样方式不够好,所以我使用的下面这种方式:

我在后端给这些数据包裹了一下。

也就是说,后端返回视频信息的时候,后端只返回文件的相对路径。前端在请求后端接口的时候再带上相对路径以及其他在服务端配置的参数,获取完整的信息,然后再去服务器请求相关的内容。最后请求到的数据都是以流的形式获取到的。后端拿到流的数据之后,再返回给前端,前端拿到流的信息,就可以实现视频播放了。

视频点赞,投币,收藏,视频评论的功能

这里面视频点赞,投币,收藏这三部分其实是一样的业务逻辑:

点赞部分:点赞视频,取消点赞,查询视频点赞数量

有一个表是t_video_like表,有userId,videoId,里面记录了哪些用户点赞了哪些视频。如果点赞的话,首先select一下,看看自己有没有点关注,如果已经点了关注的话,那么就返回已经点关注了这个信息给前端。如果select了表格,返回了null,那就是还没点关注呢,那么我们就把userId,videoId,createTime封装到VideoLike这个类的对象里面,然后执行一个insert操作,插入到t_video_like表中。

取消点赞就执行一个delete操作。

查看点赞数量,首先明白游客身份也可以查看点赞数量,所以只需要有videoId就行了,然后执行select找一找该videoId有多少点赞的用户(count(1))。然后同时可以查看一下,自己的userId是不是null,如果是null的话,那么就是游客身份,肯定没有点赞。如果userId不是null,就会查看一下到底t_video_like里面有没有自己点赞的记录,然后返回给前端,也就是返回给前端的有点赞数量也有自己是否给该视频点赞的记录,用一个Map集合形式返回的。

收藏部分:收藏视频,取消收藏,查询视频收藏数量

收藏部分是和点赞基本上完全一样的。关系表是t_video_collection

投币部分:视频投币,查询视频投币数量

投币部分所需要的表有两个,一个是t_video_coin表格,记录的是各个用户和各个视频之间的投币关系,另一个是t_user_coin,这个记录的是用户有多少剩余的硬币可以投。投币的业务逻辑比较简单,首先用户在投币之前要先计算一下,自己的硬币余额和投币的数量是否相匹配,也就是投币的数量是不是要比硬币余额大,如果大的话,那就要返回”硬币不够“的信息给前端,如果小的话,可以正常进行。先从t_video_coin这个表格中去检查,检查当前自己的这个账号投了多少的币,如果没投的话,那么这个投币操作就变成了对于t_video_coin表格的一个插入操作,如果投了的话,那就是对于t_video_coin表格的一个更新操作,更新里面的amount的数量。更新完了t_video_coin表格也要记得更新t_user_coin表格里面的剩余的amount的信息。注意整个过程的操作要有@Transactional注释。

查询视频投币数量所执行的就是select语句,但是在select的时候,以videoId为参数传进去,找到这个videoId的所有对应的VideoCoin,然后sum(amount)计算一下,这个视频,一共被投过多少的币。

视频评论部分:

实现了两个功能接口,分别是添加视频评论和分页查询视频评论。

添加视频评论的话需要加一个新的表,t_video_comment这个表,这里面最关键的两个字段是userId和videoId。videoId这里是为了表明,我评论的是哪一个视频,userId这里代表的是,评论是哪个用户发出来的。如果甲回复了我的评论,那这个就是二级评论。那么乙再去回复贾的评论,这个就也是二级评论。一共只有两级评论。

那么在这里,就只是一级评论的话,是没有rootId的,但是如果有二级评论回复了一级评论,那么就有一个rootId字段来标识我回复的是哪个一级评论。还需要知道我回复的评论是哪个用户的评论,还有一个replyUserId。如果是一级评论,也就是甲评论up主,那甲回复的这条评论的replyUserId就是up主的userId。如果乙回复甲的话,乙回复甲的这条评论的replyUserId就变成了甲的userId。

那么在程序中,我写了一个videoComment的类。

1.这里面会增加一个冗余字段,也就是 childList,List<VideoComment>。

2.还会增加一个冗余字段,也就是UserInfo,也就是包含了用户的昵称等详细信息(这个人就是创建这个评论的那个用户)。

添加视频评论:很简单,insert语句罢了。

分页查询视频评论:传入no和size两个参数,no代表的是页数,size代表的是这一页有多少个评论,多少条数据。

查询一级评论:

先查一下有多少条,算出来total。

在select语句的条件下,

select * from t_video_comment where videoId = #{videoId} and rootId is null order by id desc limit #{start}, #{limit}, 如果rootId是null的话,那么就代表是一级评论。

在一级评论的前提下再去查二级评论:

流式计算的方式将前面查到的所有的一级评论videoComment的id全部都取出来,被称为rootIdList。将rootIdList传入二级评论的搜索语句。

然后再搜寻一遍,但是where条件变了:

select * from t_video_comment where

            rootId in 

            <foreach collection="rootIdList" item="rootId open="(" separator=",">

                    #{rootId}

            </foreach>

order by id

然后分别把一级评论列表和二级评论列表里面的userId提取出来,也就是获得到了这些评论是来自于哪些用户的,把这两类用户合并到一个列表里面,然后根据这些用户userId查找到这些所有用户的信息。

视频详情:说白了就是返回视频的信息和发布视频的用户的信息

返回给前端一个Map类型:Map<String,Object>;

分别装着 map.put("video",video); map.put("userInfo",userInfo);

根据videoId查到video,然后又查到userId,根据userId返回userInfo。然后将video和userInfo都包装好返回给前端。

弹幕系统

业务场景:客户端针对于某一视频创建了弹幕,发送后端进行处理,后端需要对所有正在观看该视频的用户全部都推送这个弹幕。

实现方式:两种,分别是短连接和长连接。

短连接实现方案:所有观看视频的客户端不断轮询后端,若有新的弹幕则拉取后进行显示。

短连接实现方案的缺点是:轮询的效率低,非常浪费资源(因为HTTP协议只能由客户端向服务端发起,故必须不停连接后端)。因为如果我后端没有处理新的弹幕的话,而客户端一直在轮询后端,也就是不停的连接后端,浪费资源。

长连接的话:客户端可以给服务端发送消息,服务端也可以给客户端发送消息,并且可以使用订阅发布模式去推送消息。使用的是WebSocketService协议,这种协议是基于TCP的一种网络协议,可以实现长连接,并且可以双向的,全双工的发送消息进行通信。

消息推送:

使用订阅发布模式的话,那么首先还是会遍历ConcurrentHashMap(存储WebSocketService的),然后动态的生成生产者和消费者,然后收到的消息是以String类型传入到后端的,后端在遍历WebSocketService的时候首先会将消息和每个sessionId都装在JSONObject里面,然后将其整体转化成字节数组,然后和生产者一起发送给消费者。当然这个弹幕也要存在mysql数据库和redis数据库当中。

消费者在收到之后,也是先将字节数组转化成字符串然后再转化成JSONObject对象,然后从中取出来sessionId和message,然后根据sessionId可以从ConcurrentHashMap里面找到对应的WebSocketService,然后执行WebSocketService.sendMessage(message)这个方法。发送给客户端消息。

通过这种方式,就完成了消息的推送。

弹幕异步存储:

因为其实客户端这边不关注我们是否将数据什么时候存储到的数据库,甚至不关心我们是否存储到数据库里面了。所以就可以采用异步的方式存储数据。

异步的方式存储首先性能更高。在同步存储中,如果一个事务正在进行磁盘IO操作,那么其他事务必须等待这个事务释放锁之后才能操作。但是异步存储中,首先磁盘IO操作会被延迟,锁就会很快被释放,这样就减少了锁争用和死锁的可能性。

在线人数统计:

设置一个@Scheduled(fixedRate=5000)也就是每5秒向客户端推送一下在线人数是多少。还是遍历存储WebSocketService的那个ConcurrentHashMap,然后判断一下是否session还开着。如果开着,就将ONLINE_COUNT.get()然后存入JSONObject,然后执行webSocketService.sendMessage(jsonObject.toJSONString());

查询弹幕:

首先查询弹幕,先查询Redis数据库,如果Redis里面没有,才会去查MySQL数据库。如果Redis里面没有,在MySQL里面查到了,那就先将数据写入Redis当中,然后再返回给客户端数据。

然后,只有登录的客户才可以去查询一段时间内创建的弹幕。(传入startTime,endTime,videoId)

然后,如果是游客,那么查询弹幕的时候就是返回当下视频所有的弹幕。(传入startTime,endTime,videoId,但是startTime=null,endTime=null)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值