目录

  • 1、小视频功能说明
  • 2、技术方案
  • 3、FastDFS
    • 3.1、FastDFS是什么?
    • 3.2、工作原理
    • 3.3、文件的上传
    • 3.4、文件的下载
  • 4、FastDFS环境搭建
    • 4.1、搭建服务
    • 4.2、java client
      • 4.2.1、application.properties
      • 4.2.2、测试
  • 5、发布小视频
    • 5.1、编写pojo
    • 5.2、定义接口
    • 5.3、实现
    • 5.4、接口服务
      • 5.4.1、VideoController
      • 5.4.2、VideoService
      • 5.4.3、测试
  • 6、小视频列表
    • 6.1、定义dubbo服务
    • 6.2、实现dubbo服务
    • 6.3、定义VideoVo
    • 6.4、VideoController
    • 6.5、VideoService
  • 7、视频点赞
  • 8、小视频评论
    • 8.1、评论列表
    • 8.2、发布评论
    • 8.3、评论点赞
    • 8.4、关注用户
      • 8.4.1、定义dubbo服务
      • 8.4.2、服务实现

1、小视频功能说明

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。

2、技术方案

对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。

  • 对于存储而言,小视频的存储量以及容量都是非常巨大的

    • 所以我们选择自己搭建分布式存储系统 FastDFS进行存储
  • 对于推荐算法,我们将采用多种权重的计算方式进行计算
  • 对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速,当然了这需要额外购买CDN服务

3、FastDFS

3.1、FastDFS是什么?

FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

3.2、工作原理

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

每个 tracker 节点地位平等。收集 Storage 集群的状态。

Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

3.3、文件的上传

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

3.4、文件的下载

客户端下载请求到Tracker服务,Tracker返回给客户端storage的信息,客户端根据这些信息进行请求storage获取到文件。

4、FastDFS环境搭建

4.1、搭建服务

我们使用docker进行搭建。

#拉取镜像
docker pull delron/fastdfs#创建tracker容器
docker create --network=host --name tracker --restart=always -v fdfs-tracker:/var/fdfs delron/fastdfs tracker
#启动容器
docker start tracker#创建storage容器
docker create --network=host --name storage --restart=always -e TRACKER_SERVER=192.168.111.131:22122 -v fdfs-storage:/var/fdfs -e GROUP_NAME=group1 delron/fastdfs storage#启动容器
docker start storage#进入storage容器,到storage的配置文件中配置http访问的端口,配置文件在/etc/fdfs目录下的storage.conf。
docker exec -it storage /bin/bash#默认的http端口为8888,可以修改也可以配置
# the port of the web server on this storage server
http.server_port=8888#配置nginx,在/usr/local/nginx目录下,修改nginx.conf文件
#默认配置如下:server {listen       8888;server_name  localhost;location ~/group[0-9]/ {ngx_fastdfs_module;}error_page   500 502 503 504  /50x.html;location = /50x.html {root html;}}#默认的存储路径为/var/fdfs/data

4.2、java client

导入依赖:

<dependency><groupId>com.github.tobato</groupId><artifactId>fastdfs-client</artifactId><version>1.26.7</version><exclusions><exclusion><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId></exclusion></exclusions>
</dependency>

4.2.1、application.properties

# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs.so-timeout = 1501
fdfs.connect-timeout = 601
#缩略图生成参数
fdfs.thumb-image.width= 150
fdfs.thumb-image.height= 150
#TrackerList参数,支持多个
fdfs.tracker-list=192.168.111.131:22122
#访问路径
fdfs.web-server-url=http://192.168.111.131:8888/

4.2.2、测试

package com.oldlu.server;import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.io.File;
import java.io.IOException;@RunWith(SpringRunner.class)
@SpringBootTest
public class TestFastDFS {@Autowiredprotected FastFileStorageClient storageClient;@Testpublic void testUpload(){String path = "C:\\Users\\zhijun\\Desktop\\pics\\1.jpg";File file = new File(path);try {StorePath storePath = this.storageClient.uploadFile(FileUtils.openInputStream(file), file.length(), "jpg", null);System.out.println(storePath); //StorePath [group=group1, path=M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg]System.out.println(storePath.getFullPath());//group1/M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg} catch (IOException e) {e.printStackTrace();}}
}

5、发布小视频

5.1、编写pojo

在dubbo接口工程中编写pojo:

package com.oldlu.dubbo.server.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;import java.util.List;@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "video")
public class Video implements java.io.Serializable {private static final long serialVersionUID = -3136732836884933873L;private ObjectId id; //主键idprivate Long userId;private String text; //文字private String picUrl; //视频封面文件private String videoUrl; //视频文件private Long created; //创建时间private Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看private List<Long> seeList; //部分可见的列表private List<Long> notSeeList; //不给谁看的列表private String longitude; //经度private String latitude; //纬度private String locationName; //位置名称
}

5.2、定义接口

package com.oldlu.dubbo.server.api;import com.oldlu.dubbo.server.pojo.Video;public interface VideoApi {/*** 保存小视频** @param video* @return*/Boolean saveVideo(Video video);}

5.3、实现

package com.oldlu.dubbo.server.api;import com.alibaba.dubbo.config.annotation.Service;
import com.oldlu.dubbo.server.pojo.Video;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;@Service(version = "1.0.0")
public class VideoApiImpl implements VideoApi {@Autowiredprivate MongoTemplate mongoTemplate;@Overridepublic Boolean saveVideo(Video video) {if(video.getUserId() == null){return false;}video.setId(ObjectId.get());video.setCreated(System.currentTimeMillis());this.mongoTemplate.save(video);return true;}
}

5.4、接口服务

5.4.1、VideoController

package com.oldlu.server.controller;import com.oldlu.server.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
@RequestMapping("smallVideos")
public class VideoController {@Autowiredprivate VideoService videoService;@PostMappingpublic ResponseEntity<Void> saveVideo(@RequestParam(value = "videoThumbnail", required = false) MultipartFile picFile,@RequestParam(value = "videoFile", required = false) MultipartFile videoFile) {try {Boolean bool = this.videoService.saveVideo(picFile, videoFile);if(bool){return ResponseEntity.ok(null);}} catch (Exception e) {e.printStackTrace();}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}
}

5.4.2、VideoService

package com.oldlu.server.service;import com.alibaba.dubbo.config.annotation.Reference;
import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.oldlu.dubbo.server.api.VideoApi;
import com.oldlu.dubbo.server.pojo.Video;
import com.oldlu.server.pojo.User;
import com.oldlu.server.utils.UserThreadLocal;
import com.oldlu.server.vo.PicUploadResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;@Service
public class VideoService {@Autowiredprivate PicUploadService picUploadService;@Autowiredprotected FastFileStorageClient storageClient;@Autowiredprivate FdfsWebServer fdfsWebServer;@Reference(version = "1.0.0")private VideoApi videoApi;public Boolean saveVideo(MultipartFile picFile, MultipartFile videoFile) {User user = UserThreadLocal.get();Video video = new Video();video.setUserId(user.getId());video.setSeeType(1);try {//上传封面图片PicUploadResult picUploadResult = this.picUploadService.upload(picFile);video.setPicUrl(picUploadResult.getName()); //图片路径//上传视频StorePath storePath = storageClient.uploadFile(videoFile.getInputStream(),videoFile.getSize(),StringUtils.substringAfter(videoFile.getOriginalFilename(), "."),null);video.setVideoUrl(fdfsWebServer.getWebServerUrl() + "/" + storePath.getFullPath());this.videoApi.saveVideo(video);return true;} catch (Exception e) {e.printStackTrace();}return false;}
}

5.4.3、测试

如果上传视频,会导致异常,是因为请求太大的缘故:

解决:application.properties

spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=30MB

6、小视频列表

6.1、定义dubbo服务

package com.oldlu.dubbo.server.api;import com.oldlu.dubbo.server.pojo.Video;
import com.oldlu.dubbo.server.vo.PageInfo;public interface VideoApi {/*** 保存小视频** @param video* @return*/Boolean saveVideo(Video video);/*** 分页查询小视频列表,按照时间倒序排序** @param page* @param pageSize* @return*/PageInfo<Video> queryVideoList(Integer page, Integer pageSize);}

6.2、实现dubbo服务

package com.oldlu.dubbo.server.api;import com.alibaba.dubbo.config.annotation.Service;
import com.oldlu.dubbo.server.pojo.Video;
import com.oldlu.dubbo.server.vo.PageInfo;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;import java.util.List;@Service(version = "1.0.0")
public class VideoApiImpl implements VideoApi {@Autowiredprivate MongoTemplate mongoTemplate;@Overridepublic Boolean saveVideo(Video video) {if (video.getUserId() == null) {return false;}video.setId(ObjectId.get());video.setCreated(System.currentTimeMillis());this.mongoTemplate.save(video);return null;}@Overridepublic PageInfo<Video> queryVideoList(Integer page, Integer pageSize) {Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.desc("created")));Query query = new Query().with(pageable);List<Video> videos = this.mongoTemplate.find(query, Video.class);PageInfo<Video> pageInfo = new PageInfo<>();pageInfo.setRecords(videos);pageInfo.setPageNum(page);pageInfo.setPageSize(pageSize);pageInfo.setTotal(0); //不提供总数return pageInfo;}
}

6.3、定义VideoVo

package com.oldlu.server.vo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo {private String id;private Long userId;private String avatar; //头像private String nickname; //昵称private String cover; //封面private String videoUrl; //视频URLprivate String signature; //签名private Integer likeCount; //点赞数量private Integer hasLiked; //是否已赞(1是,0否)private Integer hasFocus; //是是否关注 (1是,0否)private Integer commentCount; //评论数量
}

6.4、VideoController

@RestController
@RequestMapping("smallVideos")
public class VideoController {
/*** 查询小视频列表** @param page* @param pageSize* @return*/@GetMappingpublic ResponseEntity<PageResult> queryVideoList(@RequestParam(value = "page", defaultValue = "1") Integer page,@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {try {if (page <= 0) {page = 1;}PageResult pageResult = this.videoService.queryVideoList(page, pageSize);if (null != pageResult) {return ResponseEntity.ok(pageResult);}} catch (Exception e) {e.printStackTrace();}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}
}

6.5、VideoService

public PageResult queryVideoList(Integer page, Integer pageSize) {User user = UserThreadLocal.get();PageResult pageResult = new PageResult();pageResult.setPage(page);pageResult.setPagesize(pageSize);pageResult.setPages(0);pageResult.setCounts(0);PageInfo<Video> pageInfo = this.videoApi.queryVideoList(page, pageSize);List<Video> records = pageInfo.getRecords();List<VideoVo> videoVoList = new ArrayList<>();List<Long> userIds = new ArrayList<>();for (Video record : records) {VideoVo videoVo = new VideoVo();videoVo.setUserId(record.getUserId());videoVo.setCover(record.getPicUrl());videoVo.setVideoUrl(record.getVideoUrl());//因为ObjectId是16进制所以拿16进制的toHexString转videoVo.setId(record.getId().toHexString());videoVo.setSignature("我就是我~");Long commentCount = this.quanZiApi.queryCommentCount(videoVo.getId(), 2);videoVo.setCommentCount(commentCount == null ? 0 : commentCount.intValue()); // 评论数String followUserKey = "VIDEO_FOLLOW_USER_" + user.getId() + "_" + videoVo.getUserId();videoVo.setHasFocus(this.redisTemplate.hasKey(followUserKey) ? 1 : 0); //是否关注String userKey = "QUANZI_COMMENT_LIKE_USER_" + user.getId() + "_" + videoVo.getId();videoVo.setHasLiked(this.redisTemplate.hasKey(userKey) ? 1 : 0); //是否点赞(1是,0否)String key = "QUANZI_COMMENT_LIKE_" + videoVo.getId();String value = this.redisTemplate.opsForValue().get(key);if (StringUtils.isNotEmpty(value)) {videoVo.setLikeCount(Integer.valueOf(value)); //点赞数} else {videoVo.setLikeCount(0);}if (!userIds.contains(record.getUserId())) {userIds.add(record.getUserId());}videoVoList.add(videoVo);}if (userIds.size() > 0) {QueryWrapper<UserInfo> queryWrapper = new QueryWrapper();queryWrapper.in("user_id", userIds);List<UserInfo> userInfos = this.userInfoService.queryList(queryWrapper);for (VideoVo videoVo : videoVoList) {for (UserInfo userInfo : userInfos) {if (videoVo.getUserId().longValue() == userInfo.getUserId().longValue()) {videoVo.setNickname(userInfo.getNickName());videoVo.setAvatar(userInfo.getLogo());break;}}}}pageResult.setItems(videoVoList);return pageResult;}

7、视频点赞

点赞逻辑与之前的圈子点赞一样,所以实现也是一样的。

VideoController:

    /*** 视频点赞** @param videoId 视频id* @return*/@PostMapping("/{id}/like")public ResponseEntity<Long> likeComment(@PathVariable("id") String videoId) {return this.movementsController.likeComment(videoId);}/*** 取消点赞** @param videoId* @return*/@PostMapping("/{id}/dislike")public ResponseEntity<Long> disLikeComment(@PathVariable("id") String videoId) {return this.movementsController.disLikeComment(videoId);}

8、小视频评论

小视频的评论与圈子的评论逻辑类似,所以也可以使用同一套逻辑进行实现。

8.1、评论列表

    /*** 评论列表*/@GetMapping("/{id}/comments")public ResponseEntity<PageResult> queryCommentsList(@PathVariable("id") String videoId,@RequestParam(value = "page", defaultValue = "1") Integer page,@RequestParam(value = "pagesize", defaultValue = "10") Integer pagesize) {return this.commentsController.queryCommentsList(videoId, page, pagesize);}

8.2、发布评论

mock地址:https://mock.boxuegu.com/project/164/interface/api/72919

    /*** 提交评论** @param param* @param videoId* @return*/@PostMapping("/{id}/comments")public ResponseEntity<Void> saveComments(@RequestBody Map<String, String> param,@PathVariable("id") String videoId) {param.put("movementId", videoId);return this.commentsController.saveComments(param);}

8.3、评论点赞

 /*** 评论点赞** @param publishId* @return*/@PostMapping("/comments/{id}/like")public ResponseEntity<Long> commentsLikeComment(@PathVariable("id") String publishId) {return this.movementsController.likeComment(publishId);}/*** 评论取消点赞** @param publishId* @return*/@PostMapping("/comments/{id}/dislike")public ResponseEntity<Long> disCommentsLikeComment(@PathVariable("id") String publishId) {return this.movementsController.disLikeComment(publishId);}

8.4、关注用户

关注用户是关注小视频发布的作者,这样我们后面计算推荐时,关注的用户将权重更重一些。

8.4.1、定义dubbo服务

package com.oldlu.dubbo.server.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "follow_user")
public class FollowUser implements java.io.Serializable{private static final long serialVersionUID = 3148619072405056052L;private ObjectId id; //主键idprivate Long userId; //用户idprivate Long followUserId; //关注的用户idprivate Long created; //关注时间
}
package com.oldlu.dubbo.server.api;import com.oldlu.dubbo.server.pojo.Video;
import com.oldlu.dubbo.server.vo.PageInfo;public interface VideoApi {/*** 保存小视频** @param video* @return*/Boolean saveVideo(Video video);/*** 分页查询小视频列表,按照时间倒序排序** @param page* @param pageSize* @return*/PageInfo<Video> queryVideoList(Integer page, Integer pageSize);/*** 关注用户** @param userId* @param followUserId* @return*/Boolean followUser(Long userId, Long followUserId);/*** 取消关注用户** @param userId* @param followUserId* @return*/Boolean disFollowUser(Long userId, Long followUserId);}

接口实现:

 //VideoApiImpl:   @Overridepublic Boolean followUser(Long userId, Long followUserId) {try {FollowUser followUser = new FollowUser();followUser.setId(ObjectId.get());followUser.setUserId(userId);followUser.setFollowUserId(followUserId);followUser.setCreated(System.currentTimeMillis());this.mongoTemplate.save(followUser);return true;} catch (Exception e) {e.printStackTrace();}return false;}@Overridepublic Boolean disFollowUser(Long userId, Long followUserId) {Query query = Query.query(Criteria.where("userId").is(userId).and("followUserId").is(followUserId));DeleteResult deleteResult = this.mongoTemplate.remove(query, FollowUser.class);return deleteResult.getDeletedCount() > 0;}

8.4.2、服务实现

//VideoController:/*** 视频用户关注*/@PostMapping("/{id}/userFocus")public ResponseEntity<Void> saveUserFocusComments(@PathVariable("id") Long userId) {try {Boolean bool = this.videoService.followUser(userId);if (bool) {return ResponseEntity.ok(null);}} catch (Exception e) {e.printStackTrace();}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}/*** 取消视频用户关注*/@PostMapping("/{id}/userUnFocus")public ResponseEntity<Void> saveUserUnFocusComments(@PathVariable("id") Long userId) {try {Boolean bool = this.videoService.disFollowUser(userId);if (bool) {return ResponseEntity.ok(null);}} catch (Exception e) {e.printStackTrace();}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}
//VideoService:/*** 关注用户** @param userId* @return*/public Boolean followUser(Long userId) {User user = UserThreadLocal.get();this.videoApi.followUser(user.getId(), userId);//记录已关注String followUserKey = "VIDEO_FOLLOW_USER_" + user.getId() + "_" + userId;this.redisTemplate.opsForValue().set(followUserKey, "1");return true;}/*** 取消关注** @param userId* @return*/public Boolean disFollowUser(Long userId) {User user = UserThreadLocal.get();this.videoApi.disFollowUser(user.getId(), userId);String followUserKey = "VIDEO_FOLLOW_USER_" + user.getId() + "_" + userId;this.redisTemplate.delete(followUserKey);return true;}

FastDFS布式文件系统仿抖音小视频实现相关推荐

  1. uni-app直播实例|仿抖音小视频|uniapp仿陌陌直播

    优直播uni-liveShow是基于vue+uni-app+vuex+nvue+swiper等技术开发仿制抖音|火山小视频/陌陌直播实战项目,支持编译到三端(H5.小程序.App端) 且兼容效果一致. ...

  2. 基于Uni-APP多端「h5+小程序+App」高仿抖音小视频|直播|聊天实例

    uni-ttLive 基于uni-app+uView-ui跨端开发短视频+直播聊天实例. 全新研发的一款多端仿制抖音短视频+直播+聊天项目,基于uniApp+Vue.js+Vuex+Nvue+uVie ...

  3. 微信小程序——仿抖音短视频切换效果

    一直以为抖音短视频切换假如用小程序做的话应该是比较简单的,直接用swiper实现就好,但在实际写的过程中才发现没那么简单,要控制的逻辑还是挺多的. 还是先看效果 体验路径 自定义组件系列>> ...

  4. 一对一交友源码,仿抖音短视频源码,搭建的秘密你了解多少?

    一对一交友源码,仿抖音短视频源码,搭建的秘密你了解多少? 5G技术马上到来,现在直播遇到的一些问题,比如延迟.卡顿.掉线等,很有可能这些情况就不复存在了.而且,其他的一些高科技产品会运用到直播过程中, ...

  5. 短视频源码仿抖音短视频APP源码短视频平台源码短视频源码

    [WoShop仿抖音短视频源码的主要功能] 1.短视频带货:关联商品的短视频封面会有商品标识,短视频内容中会弹出商品链接 2.直播带货:短视频源码支持直播功能,直播间内可开启带货功能 3.邀请赚钱:用 ...

  6. 仿抖音短视频APP源码如何开发抖音类似特效

    仿抖音短视频APP源码如何开发抖音类似特效 1.特效概览 特效列表 特效列表 2.『灵魂出窍』 抖音的实现效果如下: 灵魂出窍 我的实现效果如下: ezgif.com-rotate.gif 代码实现 ...

  7. 从零开始搭建仿抖音短视频APP-后端开发短视频业务模块(3)

    项目持续更新中: 仿抖音短视频APP专栏 目录 视频详情页展示的数据层实现 视频详情页展示的api实现 短视频下载.复制连接.二维码展示 视频保存到相册 复制链接 二维码展示 实现转为私密或公开视频 ...

  8. 从零开始搭建仿抖音短视频APP--后端开发粉丝业务模块(3)

    项目持续创作中: 仿抖音短视频APP专栏 目录 实现用户点赞视频 用户取消点赞 用户是否点赞视频的判断 实现用户点赞视频 用户在点赞我们的视频后,我们需要去实现一些相关的业务, 这里需要对应到数据库的 ...

  9. 从零开始搭建仿抖音短视频APP--后端开发粉丝业务模块(1)

    项目持续更新中: 仿抖音短视频APP专栏 目录 关注短视频博主 Redis技术妙用 取消关注博主 关注短视频博主 在我们的主页中,刷视频时是可以在头像下方进行一个关注 点击头像,进入对方的主页也是可以 ...

最新文章

  1. ECC加密算法入门介绍
  2. gocron - 定时任务管理系统
  3. 2017.4.5下午
  4. 涨知识!提单及运输业务中常用的一些代码、术语及意义!
  5. 庄闲分析软件安卓版_关于公布湖南省2020年第七批软件产品评估结果的通知
  6. 全球 android手机排行 2013,全球Android手机性能排行 三星Note 3夺魁
  7. filter my task = true implementation logic
  8. spark应用程序_Sparklens:Spark应用程序优化工具
  9. springboot错误: 找不到或无法加载主类
  10. Python 之父 Guido van Rossum 退休失败,正式加入微软搞开源!
  11. [诗歌]个人作诗集锦
  12. 接口测试基本操作与常用接口测试工具
  13. 安装inotify-tools,用inotifywait命令监听文件或目录的访问信息
  14. linux内存寻址解析 (一)
  15. 这10个idea小技巧,让我的开发效率提升了10倍
  16. 简单的Java商城项目记录
  17. 苹果计算机磁盘格式,Mac怎么将ntfs格式的磁盘格式化
  18. ipv6有必要打开吗_路由器中的IPv6功能需不需要开启?
  19. 系统性谈谈软件可靠性——第2讲:软件可靠性度量及分配
  20. linux文件系统 ubi,UBI 文件系统移植 sys 设备信息

热门文章

  1. Hadoop 3.2 环境搭建
  2. 【创想QQ图标点亮辅助工具●V2.0官方版】
  3. 计算机笔试教案,计算机面试教案.doc
  4. 软件测试面试:如何测试一个杯子
  5. php多语言翻译替换,多语言系统翻译
  6. android mp3 编码,Android 如何采用Lame编码器编码mp3文件
  7. 如何在微信小程序添加漂流瓶_漂流在数海中
  8. 为了保护视力,请对Vista/Win7/XP作如下设置
  9. java虚拟机编译顺序_深入理解Java虚拟机(程序编译与代码优化)
  10. C++和Python混合编程:C++调用Python函数