在首页
和我的
功能卡片中,包含我的访客数据
我的
功能中,展示全部历史访客记录visitors集合
{"_id": ObjectId("60176f785098b26d1c3ff043"),"date": NumberLong("1612148600271"),"userId": NumberLong("99"),"visitorUserId": NumberLong("2"),"score": 72,"from": "首页","visitDate": "20210101"
}
注意 : 记录访客数据, 在查询用户详情的接口中记录即可, 不需要额外的接口提供
APP用户点击查看推荐用户详情之后需要记录访客信息 , 数据保存在MongoDB中 , 存储结构如下
在tanhua-model
项目中定义Visitors对象
package com.tanhua.model.mongo;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 = "visitors")
public class Visitors implements java.io.Serializable{private static final long serialVersionUID = 2811682148052386573L;private ObjectId id;private Long userId; //我的idprivate Long visitorUserId; //来访用户idprivate String from; //来源,如首页. 圈子等private Long date; //来访时间private String visitDate;//来访日期private Double score; //得分
}
项目中,用户对用户的所有访问信息都应该记录到数据库中,学习阶段仅以访问今日佳人详情接口为案例添加数据。
修改tanhua-app-server
中的查看佳人详情功能,保存访客数据
在tanhua-dubbo-interface
模块创建VisitorsApi
接口, 定义保存访客数据的接口
public interface VisitorsApi {/*** 保存访客数据*/String save(Visitors visitors);
}
在tanhua-dubbo-mongo
模块创建VisitorsApiImpl
实现类, 实现保存访客数据的接口
注意 : 访客数据一天只记录一次
@DubboService
public class VisitorsApiImpl implements VisitorsApi {@Autowiredprivate MongoTemplate mongoTemplate;@Override/*** 保存访客数据* 对于同一个用户,一天之内只能保存一次访客数据*/public void save(Visitors visitors) {//1、查询访客数据Query query = Query.query(Criteria.where("userId").is(visitors.getUserId()).and("visitorUserId").is(visitors.getVisitorUserId()).and("visitDate").is(visitors.getVisitDate()));//2、不存在,保存if(!mongoTemplate.exists(query,Visitors.class)) {mongoTemplate.save(visitors);}}
}
接口文档 : http://118.25.197.221:3000/project/10/interface/api/77
在tanhua-model
项目中定义封装数据的vo对象
package com.tanhua.model.vo;import com.tanhua.model.domain.UserInfo;
import com.tanhua.model.mongo.Visitors;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitorsVo {private Long id; //用户idprivate String avatar;private String nickname;private String gender; //性别 man womanprivate Integer age;private String[] tags;private Long fateValue; //缘分值/*** 在vo对象中,补充一个工具方法,封装转化过程*/public static VisitorsVo init(UserInfo userInfo, Visitors visitors) {VisitorsVo vo = new VisitorsVo();BeanUtils.copyProperties(userInfo,vo);if(userInfo.getTags() != null) {vo.setTags(userInfo.getTags().split(","));}vo.setFateValue(visitors.getScore().longValue());return vo;}
}
修改tanhua-app-server
模块中MovementController
和MovementService
/*** 谁看过我
*/
@GetMapping("visitors")
public ResponseEntity queryVisitorsList(){List list = movementService.queryVisitorsList();return ResponseEntity.ok(list);
}
//首页-访客列表
public List queryVisitorsList() {//1、查询访问时间String key = Constants.VISITORS_USER;String hashKey = String.valueOf(UserHolder.getUserId());String value = (String) redisTemplate.opsForHash().get(key, hashKey);Long date = StringUtils.isEmpty(value) ? null:Long.valueOf(value);//2、调用API查询数据列表 ListList list = visitorsApi.queryMyVisitors(date,UserHolder.getUserId());if(CollUtil.isEmpty(list)) {return new ArrayList<>();}//3、提取用户的idList userIds = CollUtil.getFieldValues(list, "visitorUserId", Long.class);//4、查看用户详情Map map = userInfoApi.findByIds(userIds, null);//5、构造返回List vos = new ArrayList<>();for (Visitors visitors : list) {UserInfo userInfo = map.get(visitors.getVisitorUserId());if(userInfo != null) {VisitorsVo vo = VisitorsVo.init(userInfo, visitors);vos.add(vo);}}return vos;
}
在tanhua-dubbo-interface
项目在VisitorsApi
中定义查询访客列表的方法
/*** 查询我的访客数据,存在2种情况:* 1. 我没有看过我的访客数据,返回前5个访客信息* 2. 之前看过我的访客,从上一次查看的时间点往后查询5个访客数据* @param date 上一次查询时间* @return*/
List queryMyVisitors(Long date, Long userId);
在tanhua-dubbo-mongo
项目中VisitorsApiImpl
中实现查询访客列表的方法
//查询首页访客列表
public List queryMyVisitors(Long date, Long userId) {Criteria criteria = Criteria.where("userId").is(userId);if(date != null) {criteria.and("date").gt(date);}Query query = Query.query(criteria).limit(5).with(Sort.by(Sort.Order.desc("date")));return mongoTemplate.find(query,Visitors.class);
}
小视频功能类似于抖音. 快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。
视频发布(视频:容量大,视频存储到什么位置?)
查询视频列表(问题:数据库表)
关注视频作者
视频播放(客户端获取视频的URL地址,自动的播放)
效果:
查看详情:
评论:
点赞:
视频存储
对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。
FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传. 下载等服务。
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 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
客户端下载请求到Tracker服务,Tracker返回给客户端storage的信息,客户端根据这些信息进行请求storage获取到文件。
企业中搭建FastDFS是一个比较繁琐和复杂的过程(多个服务器之间的配合和配置等,专业的人员搭建),但是在学习阶段。由于所有的组件全部配置到linux虚拟机,已docker运行。所以linux的内存有要求(运行的过程中,可能会出现fastdfs的容器,启动之后自动关闭,表示虚拟机内存不足,适当的扩大内存),学习环境中使用一台调度服务器,一台存储服务器
我们使用docker进行搭建。目前所有的组件全部以docker的形式配置
#进入目录
cd /root/docker-file/fastdfs/
#启动
docker-compose up -d
#查看容器
docker ps -a
#FastDFS占用虚拟机资源较多,如果启动时发现Tracker或者Storage没有正常启动,
#使用如下命令 重启即可
#docker-compose restart
FastDFS调度服务器地址:192.168.136.160:22122
FastDFS存储服务器地址:http://192.168.136.160:8888/
找到tanhua-app-server
的pom文件,打开fastdfs的依赖如下
com.github.tobato fastdfs-client 1.26.7 ch.qos.logback logback-classic
找到tanhua-app-server
的application.yml,添加FastDFS的配置
# 分布式文件系统FDFS配置
fdfs:so-timeout: 1500connect-timeout: 600#缩略图生成参数thumb-image:width: 150height: 150#TrackerList参数,支持多个tracker-list: 192.168.136.160:22122web-server-url: http://192.168.136.160:8888/
在tanhua-app-server
编写测试类,测试文件上传到FastDFS
package com.tanhua.server.test;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.tanhua.server.TanhuaServerApplication;
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.FileInputStream;
import java.io.FileNotFoundException;@RunWith(SpringRunner.class)
@SpringBootTest(classes = TanhuaServerApplication.class)
public class TestFastDFS {//从调度服务器获取,一个目标存储服务器,上传@Autowiredprivate FastFileStorageClient client;@Autowiredprivate FdfsWebServer webServer;// 获取存储服务器的请求URL@Testpublic void testFileUpdate() throws FileNotFoundException {//1. 指定文件File file = new File("D:\\1.jpg");//2. 文件上传StorePath path = client.uploadFile(new FileInputStream(file),file.length(), "jpg", null);//3. 拼接访问路径String url = webServer.getWebServerUrl() + path.getFullPath();}
}
存储服务器:
{"_id" : ObjectId("5fa60707ed0ad13fa89925cc"),"vid" : NumberLong(1),"userId" : NumberLong(1),"text" : "我就是我不一样的烟火~","picUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/video_1.png","videoUrl" : "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/images/video/1576134125940400.mp4","created" : NumberLong(1604716296066),"likeCount" : 0,"commentCount" : 0,"loveCount" : 0,"_class" : "com.tanhua.domain.mongo.Video"
}
接口文档 : http://192.168.136.160:3000/project/19/interface/api/214
在tanhua-model
项目中定义小视频的实体类Video
package com.tanhua.model.mongo;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 = "video")
public class Video implements java.io.Serializable {private static final long serialVersionUID = -3136732836884933873L;private ObjectId id; //主键idprivate Long vid; //自动增长private Long created; //创建时间private Long userId;private String text; //文字private String picUrl; //视频封面文件,URLprivate String videoUrl; //视频文件,URLprivate Integer likeCount=0; //点赞数private Integer commentCount=0; //评论数private Integer loveCount=0; //喜欢数
}
在tanhua-app-server
中的创建SmallVideoController
和SmallVideosService
创建控制器类, 定义控制方法接收客户端上传小视频请求
package com.tanhua.server.controller;import com.tanhua.model.vo.PageResult;
import com.tanhua.server.service.SmallVideosService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;@RestController
@RequestMapping("/smallVideos")
public class SmallVideosController {@Autowiredprivate SmallVideosService videosService;/*** 发布视频* 接口路径:POST* 请求参数:* videoThumbnail:封面图* videoFile:视频文件*/@PostMappingpublic ResponseEntity saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws IOException {videosService.saveVideos(videoThumbnail,videoFile);return ResponseEntity.ok(null);}
}
创建SmallVideosService
编写代码完成小视频上传逻辑
package com.tanhua.server.service;import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
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.tanhua.autoconfig.template.OssTemplate;
import com.tanhua.dubbo.api.mongo.VideoApi;
import com.tanhua.model.mongo.Video;
import com.tanhua.server.interceptor.UserHolder;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;@Service
public class SmallVideosService {@Autowiredprivate OssTemplate ossTemplate;@Autowiredprivate FastFileStorageClient fastFileStorageClient;@Autowiredprivate FdfsWebServer webServer;@DubboReferenceprivate VideoApi videoApi;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 发布小视频*/public void saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) throws if(videoFile.isEmpty() || videoThumbnail.isEmpty()) {throw new BusinessException(ErrorResult.error());}//1、将视频上传到FastDFS,获取访问URLString filename = videoFile.getOriginalFilename(); // abc.mp4filename = filename.substring(filename.lastIndexOf(".")+1);StorePath storePath = client.uploadFile(videoFile.getInputStream(), videoFile.getSize(), filename, null);String videoUrl = webServer.getWebServerUrl() + storePath.getFullPath();//2、将封面图片上传到阿里云OSS,获取访问的URLString imageUrl = ossTemplate.upload(videoThumbnail.getOriginalFilename(), videoThumbnail.getInputStream());//3、构建Videos对象Video video = new Video();video.setUserId(UserHolder.getUserId());video.setPicUrl(imageUrl);video.setVideoUrl(videoUrl);video.setText("我就是我,不一样的烟火");//4、调用API保存数据String videoId = videoApi.save(video);if(StringUtils.isEmpty(videoId)) {throw new BusinessException(ErrorResult.error());}}
}
在tanhua-dubbo-interface
项目中创建VideoApi
接口 , 定义保存小视频的方法
package com.tanhua.dubbo.api.mongo;import com.tanhua.model.mongo.Video;public interface VideoApi {/*** 保存小视频*/void save(Video video);
}
在tanhua-dubbo-mongo
项目中创建VideoApiImpl
接口 , 实现保存小视频的方法
@DubboService
public class VideoApiImpl implements VideoApi {@Autowiredprivate MongoTemplate mongoTemplate;@Autowiredprivate IdWorker idWorker;@Overridepublic String save(Video video) {//1、设置属性video.setVid(idWorker.getNextId("video"));video.setCreated(System.currentTimeMillis());//2、调用方法保存对象mongoTemplate.save(video);//3、返回对象idreturn video.getId().toHexString();}
}
对于SpringBoot工程进行文件上传,默认支持最大的文件是1M。为了解决这个问题,需要在application.yml中配置文件限制
如果上传视频,会导致异常,是因为请求太大的缘故:
在tanhua-server工程的application.yml中添加解析器,配置请求文件和请求体 , 设置文件大小
spring:servlet:multipart:max-file-size: 30MBmax-request-size: 30MB
接口文档 : http://118.25.197.221:3000/project/10/interface/api/227
小视频的列表查询的实现需要注意的是,如果有推荐视频,优先返回推荐视频,如果没有,按照时间倒序查询视频表。
在tanhua-model
项目中创建VideoVo
封装接口返回数据
package com.tanhua.model.vo;import com.tanhua.model.domain.UserInfo;
import com.tanhua.model.mongo.Video;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;import java.io.Serializable;@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo implements Serializable {private Long userId;private String avatar; //头像private String nickname; //昵称private String id;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; //评论数量public static VideoVo init(UserInfo userInfo, Video item) {VideoVo vo = new VideoVo();//copy用户属性BeanUtils.copyProperties(userInfo, vo);//copy视频属性BeanUtils.copyProperties(item, vo);vo.setCover(item.getPicUrl());vo.setId(item.getId().toHexString());vo.setSignature(item.getText());vo.setHasFocus(0);vo.setHasLiked(0);return vo;}
}
在tanhua-app-server
中的SmallVideoController
和SmallVideosService
编写方法完成查询功能
在SmallVideoController
定义方法接收请求
/**
* 视频列表
*/
@GetMapping
public ResponseEntity queryVideoList(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pagesize) {PageResult result = videosService.queryVideoList(page, pagesize);return ResponseEntity.ok(result);
}
在SmallVideosService
中实现分页查询小视频列表方法
public PageResult queryVideoList(Integer page, Integer pagesize) {//1、查询redis数据String redisKey = Constants.VIDEOS_RECOMMEND +UserHolder.getUserId();String redisValue = redisTemplate.opsForValue().get(redisKey);//2、判断redis数据是否存在,判断redis中数据是否满足本次分页条数List
在tanhua-dubbo-interface
项目中的VideoApi
中定义分页查询小视频列表的方法
//根据vid查询数据列表
List
在tanhua-dubbo-mongo
项目中的VideoApiImpl
中实现分页查询小视频列表的方法
/*** 根据vid查询小视频列表*/
@Override
public List findMovementsByVids(List vids) {Query query = Query.query(Criteria.where("vid").in(vids));return mongoTemplate.find(query,Video.class);
}/*** 分页查询小视频数据*/
@Override
public List queryVideoList(int page, Integer pagesize) {Query query = new Query().limit(pagesize).skip((page -1) * pagesize).with(Sort.by(Sort.Order.desc("created")));return mongoTemplate.find(query,Video.class);
}
在运行测试时,及其容易出现空指针等异常。
之所以出现这类问题“”或者空指针异常,是由于MongoDB中非关系数据库,不能自动约束检测表关系。我们检查Video数据库表得知。其中有几条数据的发布人是虚拟构造,在用户表中并不存在
解决思路很简单,删除错误数据即可
Spring Cache是Spring提供的通用缓存框架。它利用了AOP,实现了基于注解的缓存功能,使开发者不用关心底层使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。用户使用Spring Cache,可以快速开发一个很不错的缓存功能。
名称 | 解释 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其进行缓存 |
@CacheEvict | 清空缓存 |
导入SpringDataRedis的依赖,并在application.yml中配置 (略)
然后在启动类注解@EnableCaching开启缓存
@SpringBootApplication
@EnableCaching //开启缓存
public class DemoApplication{public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
package com.tanhua.server.test;import com.tanhua.domain.db.UserInfo;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;@Service
public class UserInfoService {//根据id查询public UserInfo queryById(Long userId) {//从数据库查询UserInfo user = new UserInfo();user.setId(userId);user.setNickname("ceshi");return user;}//根据id修改public void update(Long userId) {UserInfo user = new UserInfo();user.setId(userId);user.setNickname("itcast");}
}
@Cacheable
注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。
@Cacheable(value = "user",key = "#userId")
public UserInfo queryById(Long userId) {//从数据库查询UserInfo user = new UserInfo();user.setId(userId);user.setNickname("ceshi");return user;
}
此处的value
是必需的,它指定了你的缓存存放在哪块命名空间。
@CachEvict
的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。
//根据id修改
@CacheEvict(value = "user",key = "#userId")
public void update(Long userId) {//修改用户UserInfo user = new UserInfo();user.setId(userId);user.setNickname("itcast");
}
在tanhua-app-server
修改VideoService,分页列表存入缓存,发布视频删除缓存
//查询视频列表
@Cacheable(value="videos",key = "T(com.tanhua.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize") //userid _ page_pagesize
public PageResult queryVideoList(Integer page, Integer pagesize) {.....
}
/*** 发布小视频*/
@CacheEvict(value="videos",allEntries = true) //清空缓存
public void saveVideos(MultipartFile videoThumbnail, MultipartFile videoFile) {.....
}