Java 项目实战:交友项目 - 查询视频列表

【Qt】使用QMediaPlayer播放视频 window平台可以安装K-Lite,linux平台一般各发行版都会默认安装GSteam。播放RTSP视频时,可以使用VLC播放器搭建一个视频推流服务器,用于拉流的客户端的调试。3、如果没有添加Qt环境变量,请把关联的Qt5MultimediaWidgets.dll和Qt5Multimedia.dll放到进程路径下否则也会报ServiceMissingError错误。2、需要安装解码器,否则报 ServiceMissingError 无法找到服务的错误。1、需要在.pro工程文件中引用对应模块。 阅读详情

概述

小视频页是滑动即刷的信息流,列表接口的好坏直接决定首页体验。客户端要的是一页「带作者资料的短视频卡片」,而不是一堆裸的 Mongo 文档。这一篇把 GET /smallVideos 这条链路从方案到代码完整讲透:推荐视频优先(Redis 里的 vid 列表)、无推荐时按时间倒序分页兜底,再批查用户资料组装 VideoVo 返回。查询策略为什么这样做、接口文档长什么样先在需求侧想清楚,再把代码落到工程。

业务场景

小视频 Tab 的交互很直接:双列瀑布流铺满封面图,上滑加载更多,点进某条全屏播放。这是典型的信息流页面,用户对它的预期只有两条,内容要「新」(有人持续发布)、加载要快。

列表数据从哪来?课程给出的需求规则只有一句话,但信息量足够:

如果有推荐的视频,优先返回推荐的视频内容;如果没有,按照时间倒序查询数据库。

拆开看,这其实是两个数据源、一种视图:

数据源内容排序依据何时使用
Redis 推荐列表(VIDEOS_RECOMMEND_ + 用户 id)运营/推荐系统为该用户挑好的 vid 串推荐系统给的顺序(即推荐优先级)有推荐数据且够分一页
Mongo video 集合全量视频表created 倒序(最新的在前)无推荐数据,或推荐数据已被翻完

这里容易和圈子动态的列表混在一起,放一张对照表:

圈子动态小视频
推荐数据 Redis keyMOVEMENTS_RECOMMEND_VIDEOS_RECOMMEND_
兜底数据Mongo movement 集合Mongo video 集合
兜底排序创建时间倒序created 倒序
关注关系好友动态查 timeline关注关系存 Redis(FOCUS_USER_KEY_ 前缀,见关注功能篇),本模块只用于 hasFocus 状态

套路完全同构:圈子动态的推荐列表也是从 Redis 取推荐 pid、翻完了回落数据库。理解了圈子动态的列表实现,这一节几乎是「换个集合再走一遍」。
顺带回答一个自然的疑问,为什么不直接查数据库? 视频表的量级远大于用户自己产生的关系数据,直接翻库意味着每次刷新都是一次全表范围的 skip/limit 深分页;推荐数据放 Redis,一是把热门内容挡在数据库前面,二是保留了按用户个性化排序的可能。数据库只做「兜底」,这是后面 SpringCache 缓存之外的另一层削峰。

查询设计:推荐优先 + 时间倒序兜底

把上面两条规则翻译成执行流程,是这样一个顺序判断(判断条件比直觉多,因为推荐数据的量是任意的):

无

有

否

是

有

无

请求 page/pagesize

Redis 中有推荐 vid 串?

按 created 倒序查 Mongo

逗号切分得到 vid 数组

起始条数 小于 数组长度?
即 page 从 1 起仍在推荐区内

skip/limit 截取本页 vid
vid in 批查 Mongo

本页查到数据?

组装 VideoVo 返回

对应到工程代码,三个方法各管一段:

mf
├── mf-app-server          # SmallVideosService.queryVideoList:流程编排 + VO 组装
├── mf-dubbo-interface     # VideoApi:findMovementsByVids / queryVideoList
├── mf-dubbo-mongo         # VideoApiImpl:Mongo 查询实现
└── mf-model               # Video(实体)/ VideoVo(返回对象)

第一步:推荐区,Redis 取 vid 串,vid in 批查

推荐数据不是 Set,也不是 Hash,而是一个逗号分隔的 vid 字符串,key 为 VIDEOS_RECOMMEND_ 拼用户 id:

public static final String VIDEOS_RECOMMEND = "VIDEOS_RECOMMEND_";
//1、查询redis数据
String redisKey = Constants.VIDEOS_RECOMMEND + UserHolder.getUserId();
String redisValue = redisTemplate.opsForValue().get(redisKey);
//2、判断redis数据是否存在,判断redis中数据是否满足本次分页条数
List<Video> list = new ArrayList<>();
int redisPages = 0;
if(!StringUtils.isEmpty(redisValue)) {
    //3、如果redis数据存在,根据VID查询数据
    String[] values = redisValue.split(",");
    //判断当前页的起始条数是否小于数组总数
    if( (page -1) * pagesize < values.length) {
        List<Long> vids = Arrays.stream(values).skip((page - 1) * pagesize).limit(pagesize)
                .map(e->Long.valueOf(e))
                .collect(Collectors.toList());
        //调用API根据VID数组查询视频数据
        list = videoApi.findMovementsByVids(vids);
    }
    redisPages = PageUtil.totalPage(values.length,pagesize);
}

注意分页发生在 Redis 侧而不是数据库侧:skip((page-1)*pagesize).limit(pagesize) 是在 vid 数组上截取本页要哪几条,然后一次性 vid in [...] 查 Mongo。推荐顺序由数组顺序决定,Mongo 查回来不会保持这个顺序(in 查询无排序),这是「推荐优先级」最终排序的隐患之一,属于实读时能看出的真实瑕疵。

方法名 findMovementsByVids 带着明显的复制粘贴痕迹,「Movements」是圈子动态的词汇,功能上它就是「按 vid 批查视频」:

@Override
public List<Video> findMovementsByVids(List<Long> vids) {
    Query query = Query.query(Criteria.where("vid").in(vids));
    return mongoTemplate.find(query, Video.class);
}

第二步:兜底区,created 倒序分页

三种情况会走到兜底:Redis 里压根没推荐数据、推荐 vid 翻完了(起始条数越界)、vid 批查结果为空(推荐的视频已被删)。兜底查询不看用户,只按时间倒序取一页:

//4、如果redis数据不存在,分页查询视频数据
if(list.isEmpty()) {
    //page的计算规则:传入的页码 -- redis查询的总页数
    list = videoApi.queryVideoList(page - redisPages, pagesize);
}
@Override
public List<Video> 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);
}

三个实读结论,回答「到底排除谁、按什么排」:

  • 不排除自己:查全量视频表,自己发的视频也会出现在列表里;
  • 不排除已看过的:教学工程没有观看历史,翻页刷新会重复出现;
  • 排序就是 created 倒序,没有任何热度加权。

页码换算:page - redisPages 的坑

推荐区和兜底区在「一页」的尺度上要无缝衔接。假如推荐串里有 23 条、每页 10 条,则第 1~3 页(redisPages = 3)从推荐区出;用户请求第 4 页时,推荐区已越界,落到兜底区,但 Mongo 的页码不能还是 4,得减去推荐区占掉的部分,4 - 3 = 1,即从视频表第 1 页接着取。

Mongo video 集合

Redis 推荐区(23条 / 3页)

请求第4页 → page-redisPages=1

第1页
vid 0~9

第2页
vid 10~19

第3页
vid 20~22 + 空7条

第1页
created 最新10条

第2页 ...

这个换算有个前提:两次翻页之间推荐串不能变化。推荐数据一刷新,页码错位,用户会看到重复或跳过的内容。这类一致性问题在缓存篇会再碰面。

互动状态怎么算

关注关系(FOCUS_USER_KEY_ 前缀,存 Redis,后续章节落库)和点赞状态(VIDEO_LIKE_HASHKEY)都是「当前登录用户 × 视频」的维度,天然无法缓存复用。这里只做需求定位:hasLiked、hasFocus 是 VideoVo 的固定字段,本章代码里 VideoVo.init 先统一置 0 占位,点赞/关注互动实现在后续章节再逐个填充,列表接口的字段结构现在就定死,避免前端跟着改。

接口与文件清单

完整链路涉及的代码分布如下,代码实现一节会逐文件展开:

文件所在模块职责
SmallVideosControllermf-app-server接收 GET /smallVideos 请求,透传分页参数
SmallVideosServicemf-app-server编排整条链路:Redis 推荐 → Mongo 兜底 → 组装 VO
VideoApimf-dubbo-interface定义 queryVideoList / findMovementsByVids 两个 Dubbo 接口
VideoApiImplmf-dubbo-mongoMongoTemplate 实现分页与 vid 批查
VideoVomf-model封装返回给客户端的视图对象
mf
├── mf-app-server
│   └── src/main/java/com/mf/server
│       ├── controller
│       │   └── SmallVideosController.java
│       ├── service
│       │   └── SmallVideosService.java
│       └── interceptor
│           └── UserHolder.java            # ThreadLocal 持有当前登录用户 id
├── mf-dubbo
│   ├── mf-dubbo-interface
│   │   └── src/main/java/com/mf/dubbo/api
│   │       └── VideoApi.java
│   └── mf-dubbo-mongo
│       └── src/main/java/com/mf/dubbo/api
│           └── VideoApiImpl.java
└── mf-model
    └── src/main/java/com/mf/model
        ├── mongo
        │   └── Video.java                  # video 集合实体
        └── vo
            └── VideoVo.java                # 返回给客户端的视图对象

Controller:列表接口

列表接口沿用发布视频同一个 Controller,@RequestMapping("/smallVideos") 类级路径 + 无参 @GetMapping,所以实际请求路径就是 GET /smallVideos?page=1&pagesize=10(实测工程代码,路径与接口文档一致)。

package com.mf.server.controller;

import com.mf.model.vo.PageResult;
import com.mf.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 {

    @Autowired
    private SmallVideosService videosService;

    /**
     * 视频列表
     * 请求路径:GET /smallVideos?page=1&pagesize=10
     * page、pagesize 均有默认值,客户端不传也能查第一页
     */
    @GetMapping
    public ResponseEntity queryVideoList(@RequestParam(defaultValue = "1")  Integer page,
                                         @RequestParam(defaultValue = "10") Integer pagesize) {
        PageResult result = videosService.queryVideoList(page, pagesize);
        return ResponseEntity.ok(result);
    }
}

Controller 没有任何业务逻辑,连登录用户 id 都不取,后面 Service 里用 UserHolder 从 ThreadLocal 拿,拦截器在进 Controller 之前已经把 token 解析完放进去了。

Service:queryVideoList 完整实现

查询策略

推荐池放 Redis:VIDEOS_RECOMMEND_ + userId 这个 key 里存着推荐系统算好的 vid 列表,逗号分隔。查询时两种情况:

存在

是

否

不存在

是

否

收到 page / pagesize

读 Redis
VIDEOS_RECOMMEND_ + userId

推荐 vid 列表存在?

起始条数 < vid 总数?

skip/limit 截取本页 vid
videoApi.findMovementsByVids 批查

list 为空

计算 redisPages
= 推荐列表总页数

videoApi.queryVideoList
page - redisPages
Mongo 时间倒序分页

list 为空?

提取 userIds

userInfoApi.findByIds 批查用户资料

VideoVo.init 逐条组装

PageResult 返回

页码换算是这段代码最容易看漏的地方:推荐列表占 2 页时,客户端请求第 3 页,落到 Mongo 里其实是第 1 页,所以才有 page - redisPages。

完整代码

package com.mf.server.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.PageUtil;
import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.mf.autoconfig.template.OssTemplate;
import com.mf.commons.utils.Constants;
import com.mf.dubbo.api.UserInfoApi;
import com.mf.dubbo.api.VideoApi;
import com.mf.model.domain.UserInfo;
import com.mf.model.mongo.Video;
import com.mf.model.vo.ErrorResult;
import com.mf.model.vo.PageResult;
import com.mf.model.vo.VideoVo;
import com.mf.server.exception.BusinessException;
import com.mf.server.interceptor.UserHolder;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class SmallVideosService {

    @Autowired
    private FastFileStorageClient client;

    @Autowired
    private FdfsWebServer webServer;

    @Autowired
    private OssTemplate ossTemplate;

    @DubboReference
    private VideoApi videoApi;

    @DubboReference
    private UserInfoApi userInfoApi;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 分页查询视频列表
     * @Cacheable 是本章后续 SpringCache 部分加上的(方法结果按 userId_page_pagesize 缓存),
     * 分析执行链路时先当它不存在
     */
    @Cacheable(
            value = "videos",
            key = "T(com.mf.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize")
    public PageResult queryVideoList(Integer page, Integer pagesize) {

        //1、拼接推荐列表的 Redis key,读取推荐系统预置的 vid 串(格式如 "1001,1002,1003")
        String redisKey = Constants.VIDEOS_RECOMMEND + UserHolder.getUserId();
        String redisValue = redisTemplate.opsForValue().get(redisKey);

        //2、推荐列表存在时,判断推荐数据是否够分到当前页
        List<Video> list = new ArrayList<>();
        int redisPages = 0;
        if(!StringUtils.isEmpty(redisValue)) {
            //3、vid 之间以逗号分隔,切分后按分页规则截取本页的 vid
            String[] values = redisValue.split(",");
            //当前页的起始条数小于数组总数,才说明本页还有推荐数据可取
            if((page - 1) * pagesize < values.length) {
                List<Long> vids = Arrays.stream(values).skip((page - 1) * pagesize).limit(pagesize)
                        .map(e -> Long.valueOf(e))
                        .collect(Collectors.toList());
                //4、按 vid 批量查询推荐视频
                list = videoApi.findMovementsByVids(vids);
            }
            //推荐列表能凑成的总页数(Hutool 工具:totalCount / pageSize 向上取整)
            redisPages = PageUtil.totalPage(values.length, pagesize);
        }

        //5、推荐数据不足或不存在时,走时间倒序分页兜底
        if(list.isEmpty()) {
            //页码换算:传入页码减去推荐列表占掉的页数,得到 Mongo 里的真实页码
            list = videoApi.queryVideoList(page - redisPages, pagesize);
        }

        //6、提取视频列表中所有发布者的用户 id(Hutool 反射取字段,自动去重由 findByIds 内部处理)
        List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class);

        //7、一次性批查所有用户资料,避免循环内逐条查(N+1)
        Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);

        //8、逐条组装 VideoVo
        List<VideoVo> vos = new ArrayList<>();
        for (Video video : list) {
            UserInfo info = map.get(video.getUserId());
            //虚拟造数可能存在 userId 在用户表中无记录的情况,跳过防止空指针
            if(info != null) {
                VideoVo vo = VideoVo.init(info, video);
                vos.add(vo);
            }
        }

        return new PageResult(page, pagesize, 0L, vos);
    }
}

三个细节值得停留一下:

  • Constants.VIDEOS_RECOMMEND 的值是 "VIDEOS_RECOMMEND_",和后面互动标记用的 FOCUS_USER_KEY = "FOCUS_USER_KEY_" 一样,都定义在 mf-commons 的 Constants 里,Redis key 统一处管理。
  • PageUtil 来自 Hutool 的 cn.hutool.core.util.PageUtil,totalPage(total, size) 做向上取整,10 条推荐、每页 10 条时返回 1 页。
  • 第 5 步的 page - redisPages 在讲义源码里留着一行 //page=1 ? 的疑问注释,当推荐页数比请求页码还大时这里会算出 0 或负数,skip 收到负数会抛 IllegalArgumentException。实际场景里推荐池条数有限(通常只有一页多),这个边界很少触发,但造数测试时如果 vid 塞得太多就会撞上。

「排除已看过」的说明

需求分析里提过「已看过的视频不再出现在推荐列表」这类想法。实测工程代码,queryVideoList 里没有任何排除逻辑,推荐去重完全交给推荐系统写 Redis 之前处理,列表链路只管照单查库。后续引入 SpringCache 缓存的也是「列表结果」而非「已看记录」。如果你要在自己项目里加「排除已看」,正确的挂点是第 5 步之前(对 vid 列表过滤),而不是查完再删。

Dubbo API:接口定义与 Mongo 实现

VideoApi 接口

package com.mf.dubbo.api;

import com.mf.model.mongo.Video;
import java.util.List;

public interface VideoApi {

    //保存小视频,返回 objectId 字符串
    String save(Video video);

    //根据 vid 批量查询视频(推荐列表用)
    List<Video> findMovementsByVids(List<Long> vids);

    //按时间倒序分页查询视频(无推荐时兜底)
    List<Video> queryVideoList(int page, Integer pagesize);
}

VideoApiImpl 实现

package com.mf.dubbo.api;

import com.mf.dubbo.utils.IdWorker;
import com.mf.model.mongo.Video;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import java.util.List;

@DubboService
public class VideoApiImpl implements VideoApi {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private IdWorker idWorker;

    @Override
    public String save(Video video) {
        //1、设置属性:vid 用 IdWorker 生成,created 记录毫秒时间戳
        video.setVid(idWorker.getNextId("video"));
        video.setCreated(System.currentTimeMillis());
        //2、保存到 video 集合
        mongoTemplate.save(video);
        //3、返回 objectId 字符串
        return video.getId().toHexString();
    }

    /**
     * 根据 vid 批量查询视频列表
     */
    @Override
    public List<Video> findMovementsByVids(List<Long> vids) {
        Query query = Query.query(Criteria.where("vid").in(vids));
        return mongoTemplate.find(query, Video.class);
    }

    /**
     * 分页查询视频列表,按 created 倒序(最新发布的在前)
     */
    @Override
    public List<Video> 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);
    }
}

queryVideoList 的写法是 Spring Data MongoDB 分页的固定套路:new Query() 不带任何 Criteria 条件表示全集合查询,skip((page-1) * pagesize) 跳过前面页的数据,limit(pagesize) 截取本页,with(Sort...) 挂排序。注意排序依据是 created(发布时间戳),不是 _id,两者时序基本一致,但显式按 created 排序语义更清楚。

这里有个隐患先记下:video 集合默认没有为 created 建索引,数据量上来后 sort + skip + limit 会在内存里排序。数据量小(教学库几百条)时无所谓,生产上应给 created 加索引,深分页则要换成「基于游标」的分页方式。

VideoVo:两个字段表的拼接

VideoVo 把 Mongo 实体 Video 和 MySQL 侧的 UserInfo 揉成一个对象。组装靠两次 BeanUtils.copyProperties,同名属性自动拷贝,不同名的手动补。Video 实体只有视频自身的字段,列表页要展示的却远不止这些,VideoVo 多出来的部分分两类:

字段来源说明
avatar / nickname用户资料 UserInfo头像、昵称
coverVideo.picUrl 改名封面图
signatureVideo.text 改名发布时配的文字
likeCount / commentCountVideo 原生计数直接透传
hasLiked / hasFocusRedis 互动状态本章默认 0,后续互动章节填充

用户资料是批查的,不是一条视频查一次,先把本页所有视频的 userId 提出来,一次 findByIds 拿回 Map,循环里 O(1) 取用:

//5、提取视频列表中所有的用户id
List<Long> userIds = CollUtil.getFieldValues(list, "userId", Long.class);
//6、查询用户信息
Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, null);
//7、构建返回值
List<VideoVo> vos = new ArrayList<>();
for (Video video : list) {
    UserInfo info = map.get(video.getUserId());
    if(info != null) {
        VideoVo vo = VideoVo.init(info, video);
        vos.add(vo);
    }
}

这也是跨存储组装的标准姿势:Mongo 存视频、用户资料在另一套存储,靠一次批量查询 + Map 关联,把 N+1 次远程调用压成 1 次。空资料的视频直接跳过(info != null 判断),而不是让整个请求失败。

完整 VO 定义与组装逻辑如下:

package com.mf.model.vo;

import com.mf.model.domain.UserInfo;
import com.mf.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;       //头像(来自 UserInfo)
    private String nickname;     //昵称(来自 UserInfo)

    private String id;           //视频 objectId 字符串
    private String cover;        //封面(对应 Video.picUrl)
    private String videoUrl;     //视频 URL
    private String signature;    //发布视频时输入的文字(对应 Video.text)

    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 用户属性:avatar、nickname 同名拷贝
        BeanUtils.copyProperties(userInfo, vo);   //source, target
        //copy 视频属性:videoUrl、likeCount、commentCount 同名拷贝
        BeanUtils.copyProperties(item, vo);
        //不同名属性手动补
        vo.setCover(item.getPicUrl());
        vo.setId(item.getId().toHexString());
        vo.setSignature(item.getText());
        //互动状态默认 0,本章未实现判存
        vo.setHasFocus(0);
        vo.setHasLiked(0);
        return vo;
    }
}

字段来源对照:

VideoVo 字段来源拷贝方式
userIdVideo.userId(同名,两次 copy 中后一次覆盖)自动
avatar / nicknameUserInfo自动(同名)
idVideo.id(ObjectId → 十六进制字符串)手动
coverVideo.picUrl手动(改名)
videoUrlVideo.videoUrl自动(同名)
signatureVideo.text手动(改名)
likeCount / commentCountVideo 同名字段自动
hasLiked / hasFocus本章硬编码 0手动置 0

hasLiked / hasFocus 这里要如实说:工程代码里没有做 Redis 判存,init 里直接置 0。也就是说此刻列表上所有视频都显示「未点赞、未关注」,哪怕你真的赞过。完成这两状态的思路与圈子互动完全一致,从 Redis 的 LIKE_KEY_ / FOCUS_USER_KEY_ 标记里 isMember 判定,再 setHasLiked(1)。这是留给后续互动功能补的口子,字段先在 VO 里占位,保证接口契约稳定,客户端不用等后端加字段。

关注列表与推荐列表的实现差异

工程只实现了推荐列表优先 + 时间倒序兜底这一条链路,没有独立的「关注列表」接口。两者的实现差异值得对比着理解,因为分页思路完全不同:

维度推荐列表(本章实现)关注列表(思路)
数据源Redis VIDEOS_RECOMMEND_ + userId 存 vid 串Redis FOCUS_USER_ + userId 集合存关注者 id
查询方式按 vid in 查询(推荐池本来就只有几十条)先查关注者 id,再按 userId in 查视频
分页位置在 Redis 列表上 skip/limit 截取 vid两种:视频少则 in 后内存分页;多则 Mongo query 里带 userId in + skip/limit
页码换算需要 page - redisPages 混合页码换算不需要,单一数据源
排序推荐系统给的顺序(vid 串顺序即优先级)created 倒序

关注列表的落地大致是这样(思路代码,非工程实读内容):

//思路:Service 里先取关注集合,再让 VideoApi 支持按 userId 列表分页查
Set<String> focusIds = redisTemplate.opsForSet().members("FOCUS_USER_" + UserHolder.getUserId());
List<Long> userIds = focusIds.stream().map(Long::valueOf).collect(Collectors.toList());
//VideoApi 增加一个按发布者分页的方法:
//Query query = new Query(Criteria.where("userId").in(userIds))
//    .limit(pagesize).skip((page - 1) * pagesize)
//    .with(Sort.by(Sort.Order.desc("created")));

推荐列表用 vid 串而不是 Set,正是因为推荐结果有顺序语义,List 截取天然保序,Set 的 members 不保证顺序。

性能问题:SpringCache 是明确要补的一刀

把上面的流程串一遍,一次列表请求至少包含:1 次 Redis GET、1 次 Mongo 查询、1 次用户资料批查、外加 VO 组装。前两项随 page/pagesize 变化,但结果集对同一用户同一页是稳定的,用户来回滑动时,同样的查询在反复执行。

把 queryVideoList 拆开看,每次用户下拉刷新都会完整执行三段:

  1. Mongo 分页查询:findMovementsByVids 或 sort + skip + limit,其中兜底分支的全集合排序在无索引时是内存排序;
  2. 批查用户资料:userInfoApi.findByIds 走 Dubbo + MySQL,一次 in 查询,开销可控但每次刷新都发生;
  3. 组装开销:反射取字段 + 逐条 BeanUtils.copyProperties。

关键在于这三段的结果对同一个 userId_page_pagesize 组合是不变的,视频列表不会因为多刷一次就变。也就是说每次刷新都在重复计算一份相同答案,这正是缓存的教科书场景。工程最终版的代码里已经加上了 @Cacheable 注解(实读 SmallVideosService):

//查询视频列表
@Cacheable(
        value = "videos",
        key = "T(com.mf.server.interceptor.UserHolder).getUserId()+'_'+#page+'_'+#pagesize")
public PageResult queryVideoList(Integer page, Integer pagesize) { ... }

本章后续用 SpringCache 解决:方法上加 @Cacheable(就是前面 Service 代码里那行注解),结果按 userId_page_pagesize 缓存进 Redis,第二次同参请求直接命中,三段开销归零。这里只点出「为什么要缓存」:列表查询高频、结果可复用、失效时机明确(发布视频时清空)。SpringCache 的注解模型、缓存对齐与失效策略,属于本章 SpringCache 系列的内容。缓存粒度、过期策略、发布新视频后如何 @CacheEvict 失效,这些属于本章 SpringCache 系列的内容,那里展开。

先在这里留个思考题:@Cacheable 的 key 为什么要拼上 userId?,因为推荐列表是按用户隔离的(VIDEOS_RECOMMEND_ + userId),不拼 userId 的话,A 用户的推荐页会串给 B 用户。

整条链路的时序

MySQL (user_info) UserInfoApi (Dubbo) MongoDB VideoApi (Dubbo) Redis SmallVideosService Controller 客户端 MySQL (user_info) UserInfoApi (Dubbo) MongoDB VideoApi (Dubbo) Redis SmallVideosService Controller 客户端 alt [推荐列表存在且够分页] [无推荐或不足] GET /smallVideos?page=1&pagesize=10 (Authorization: token) 拦截器解析 token,UserHolder 存 userId queryVideoList(page, pagesize) GET VIDEOS_RECOMMEND_106 "1001,1002,..."(或空) split + skip/limit 截取本页 vid findMovementsByVids(vids) {vid: {$in: [...]}} List<Video> page - redisPages 页码换算 queryVideoList(page, pagesize) sort({created:-1}).skip().limit() List<Video> CollUtil.getFieldValues 提取 userIds findByIds(userIds, null) where id in (...) List<UserInfo> Map<Long, UserInfo> VideoVo.init 逐条组装(hasLiked/hasFocus=0) PageResult JSON( queryset: VideoVo 列表 )

需求分析版时序图(参与者命名略有不同,描述同一条链路):

UserInfoApi(Dubbo) VideoApi(Dubbo) Redis SmallVideosService SmallVideoController 客户端 UserInfoApi(Dubbo) VideoApi(Dubbo) Redis SmallVideosService SmallVideoController 客户端 alt [有推荐数据且本页仍在推荐区] [无推荐/越界/结果为空] GET /smallVideos?page&pagesize queryVideoList(page, pagesize) GET VIDEOS_RECOMMEND_{userId} vid 串切分、skip/limit 截取 findMovementsByVids(vids) List<Video> queryVideoList(page - redisPages, pagesize) created 倒序 List<Video> findByIds(userIds) Map<Long, UserInfo> VideoVo.init(info, video) 组装 PageResult(items=VideoVo[]) JSON

原型线框图

小视频列表页(双列瀑布流)与全屏播放页,文案取原型原文:

┌─────────────────────────────┐  ┌─────────────────────────────┐
│  推荐                        │  │ [更多]        @小美  [关注] │
│                             │  │  ──────────────────────     │
│  ┌─────────┐ ┌─────────┐    │  │  (全屏视频播放区)             │
│  │ 封面图   │ │ 封面图   │    │  │                             │
│  │         │ │         │    │  │                        ♡ 10 │
│  └─────────┘ └─────────┘    │  │                        ✎ 15 │
│  1024程序员节  1024程序员节   │  │                             │
│                             │  │  ┌─────────────────────┐    │
│  ┌─────────┐ ┌─────────┐    │  │  │ 说点什么             │    │
│  │ 封面图   │ │ 封面图   │    │  │  └─────────────────────┘    │
│  │         │ │         │    │  │  小视频标题不超过20字          │
│  └─────────┘ └─────────┘    │  │  点击更多出弹框 拉黑、举报      │
│  1024程序员节  1024程序员节   │  │  点击评论,弹出评论框,显示     │
│                             │  │  评论内容及评论信息。          │
│  ┌─────────────────────┐    │  │                             │
│  │ + (上传 / 录制)      │    │  │ [交友] [圈子] [小视频] [消息] [我的] │
│  └─────────────────────┘    │  │                             │
└─────────────────────────────┘  └─────────────────────────────┘
        列表页(一屏一图)                  播放页(一屏一图)

要点:

  • 顶部 Tab 文案「推荐」,对应这里的推荐优先逻辑;
  • 双列瀑布流,每个卡片 = 封面图 + 标题文案(「1024程序员节」,即 VideoVo 的 cover + signature)+ 互动计数;
  • 卡片标题有约束:「小视频标题不超过20字」,发布端要校验,列表端按此宽度截断展示;
  • 右下角悬浮「+」按钮,展开「上传 / 录制」,即发布视频入口(属发布篇,不在这里展开);
  • 播放页右上角「更多」出弹框(拉黑、举报);作者信息旁是「关注」按钮,状态即 hasFocus;
  • 评论入口点击弹出评论面板(「共15条评论」、「说点什么」),属后续视频评论篇。

API 速览

接口文档(EasyYapi)约定如下:

项值
请求路径/smallVideos
请求方式GET
请求参数page(页码,默认 1)、pagesize(每页条数,默认 10)
响应结构PageResult:pages/pagesize/counts/items
items 元素VideoVo

请求与响应示例:

// GET /smallVideos?page=1&pagesize=5
{
  "pages": 8,
  "pagesize": 5,
  "counts": 36,
  "items": [
    {
      "userId": 106,
      "avatar": "https://mf-oss.oss-cn-qingdao.aliyuncs.com/images/106.jpg",
      "nickname": "小美",
      "id": "61a5f1e0a3b4c2d8e6f0a1b2",
      "cover": "https://oss.example.com/cover/1.jpg",
      "videoUrl": "http://192.168.136.160:8888/group1/M00/00/01/video_1.mp4",
      "signature": "1024程序员节",
      "likeCount": 10,
      "hasLiked": 0,
      "hasFocus": 0,
      "commentCount": 15
    }
  ]
}

异常与边界

场景现象处理
无任何视频推荐区、兜底区都为空返回空 items 的 PageResult,前端展示占位,不报错
推荐串为空串/nullredisValue 判空后直接走兜底StringUtils.isEmpty 拦截,等价于无推荐
推荐的视频已从 video 集合删除vid in 查询结果少于预期甚至为空结果为空则整体回落兜底区;部分缺失则该页数量变少
关注的人没发过视频关注列表无内容本模块视频列表不区分关注视图,hasFocus 只标记状态;关注维度内容不会在本接口为空
分页越界(page 很大)skip 超出集合总量Mongo 返回空列表,按空页处理
视频发布人是虚拟/不存在用户UserInfo 查不到,组装时空指针循环里 info != null 跳过;根治靠清理脏数据(讲义实测:测试环境 video 集合有虚拟用户数据导致 NPE,删除后恢复)
互动状态判断开销每条视频查一次 Redis 判点赞/关注本章未启用(默认 0);互动章节实现时需批量化,避免循环单查
分页无稳定排序感知客户端连续翻页,偶见视频重复或丢失sort + skip 本身稳定,但如果没有排序(漏写 Sort.by),Mongo 不保证返回顺序,skip 的意义就没了。本章代码按 created 倒序,别删那一行
互动状态 key 拼错hasLiked 永远显示未赞判存时 key 拼写与写入时不一致(如 LIKE_KEY_ 漏了下划线),isMember 永远 false。key 全部走 Constants 常量,禁止手写字符串
批查变 N+1列表接口一次请求发出 10 次 Dubbo + 10 条 SQL循环里逐条 userInfoApi.findById(single)。正确写法是先提取 userIds 再一次 findByIds
counts 口径不一致点赞数与详情页对不上Video.likeCount 是发布侧冗余计数,互动时必须同步 inc 更新,否则列表数与互动详情两套口径
用户表脏数据空指针列表接口偶发 500,日志 NPE造数时 video.userId 在 user_info 表无对应记录,init 里 copyProperties(userInfo) 抛 NPE。本章用 if(info != null) 跳过,或清理脏数据
视频文件删除后引用失效列表封面 404picUrl/videoUrl 存的是 FastDFS/OSS 地址,文件删除后 Mongo 文档还在。删除视频要「先删文件、再删文档」或标记删除
页码换算出现非正数skip 抛 IllegalArgumentException推荐列表页数大于请求页码时 page - redisPages <= 0。测试造数 vid 过多时容易触发,兜底分支前对换算结果做 Math.max(1, ...)

最值得展开的是空指针和互动状态两行。空指针问题在讲义里被明确记录为「运行测试时极易出现」,根因是 MongoDB 这类文档库不维护跨集合引用约束,video.userId 指向一个不存在的用户资料毫无报错,直到组装 VO 时才炸。这决定了列表接口必须把「引用失效」当作常态防御,而不是异常。互动状态的开销则是下一篇之前的伏笔:一页 10 条视频,hasLiked、hasFocus 各判一次就是 20 次 Redis 往返,若逐条查会显著拖慢接口。批量化(一次 mget/pipeline)或干脆连 VO 一起缓存,是两个方向的解法,后者由 SpringCache 承担。

验证要点

按课程的操作路径验证:

  1. 造数:用 Postman 走发布视频流程,向 video 集合插入若干条不同用户、不同时间的视频;手工向 Redis 写入 VIDEOS_RECOMMEND_{userId}(逗号分隔 vid,条数设为非整页数,如 23 条配 pagesize=10,专门考验页码换算)。
  2. 列表核对:GET /smallVideos?page=1&pagesize=10,核对第 1~3 页返回的是推荐 vid 对应的视频(可用 signature 打标记区分);第 4 页起应切换为 created 倒序的全量视频。
  3. 边界核对:把推荐串清空后请求,确认直接走兜底;请求一个超出总量的 page,确认返回空列表不报错;删除一条推荐 vid 对应的视频文档,确认该页条数变少但不 500。
  4. 差异确认:同一用户同一页连续请求两次,结果一致(为下一篇验证 SpringCache 命中做基线)。

按下面的步骤可以在本地把整条链路跑通并逐字段核对。

第一步:mongosh 造数。往 video 集合插两条测试视频(userId 用库里真实存在的用户 id):

use mf

db.video.insertMany([
  {
    vid: NumberLong(9001),
    created: NumberLong(1727654400001),
    userId: NumberLong(106),
    text: "测试视频一",
    picUrl: "https://oss.example.com/cover1.jpg",
    videoUrl: "http://192.168.136.160:8888/group1/M00/00/00/video1.mp4",
    likeCount: 3, commentCount: 1, loveCount: 0
  },
  {
    vid: NumberLong(9002),
    created: NumberLong(1727654400002),
    userId: NumberLong(1),
    text: "测试视频二",
    picUrl: "https://oss.example.com/cover2.jpg",
    videoUrl: "http://192.168.136.160:8888/group1/M00/00/00/video2.mp4",
    likeCount: 0, commentCount: 0, loveCount: 0
  }
])

第二步:预置推荐列表(也可跳过,直接测时间倒序兜底分支):

redis-cli set "VIDEOS_RECOMMEND_106" "9001,9002"

第三步:调接口(token 换成自己登录后拿到的):

curl -s "http://localhost:18080/smallVideos?page=1&pagesize=5" \
  -H "Authorization: <token>" | python3 -m json.tool

第四步:逐字段核对 VideoVo。响应里的每条视频应满足:

  • id 为 24 位十六进制字符串(ObjectId 的 toHexString);
  • cover 等于造数时的 picUrl,videoUrl 原样带出;
  • signature 等于 text(「测试视频一」),证明手动映射生效;
  • nickname / avatar 是 userId 对应用户的资料,证明批查生效;
  • likeCount = 3(视频一)、hasLiked = 0、hasFocus = 0(本章未判存,恒为 0,符合预期)。

第五步:验证分页与推荐优先。造 12 条视频、pagesize=10:

  • 无 Redis 推荐时,第 1 页 10 条按 created 倒序,第 2 页剩 2 条;
  • 写入 15 个 vid 的推荐串后,第 1、2 页来自推荐池(vid 顺序),第 3 页起 page - redisPages 换算后走 Mongo 兜底,数据恰好无缝衔接;
  • 把某条 video 的 userId 改成 999999(用户表不存在的 id),该条从列表消失且不报错,验证空指针防护。

常见误解澄清

误解实际情况
推荐列表用 Redis Set 存,查询时 SMEMBERS 取 id实读为 String 值存逗号分隔 vid 串,split(",") 后在应用内存里分页;FOCUS_USER_ 才是关注关系的前缀,两者不是一回事
视频模块有独立的「关注列表」接口(先查关注关系再查这些人的视频)实读只有一个 /smallVideos 接口:推荐优先 + 时间倒序兜底。关注关系在本模块只体现为 hasFocus 字段,不构成查询条件
列表会排除自己发的视频和已看过的视频实读 queryVideoList 查全量 video 集合,不排除任何人,也没有观看历史过滤
hasLiked/hasFocus 在这里就已实现本章 VideoVo.init 统一置 0 占位,互动状态在后续点赞/关注章节填充
兜底查询按热度或推荐分排序只按 created 倒序,无任何加权
推荐区翻完后页码从 1 重新算传入页码需减去推荐区总页数(page - redisPages),否则内容跳接错位
findMovementsByVids 是动态接口名字是复制自圈子动态的残留,功能是按 vid 批查视频
视频列表数据存在 MySQL 的 video 表里视频存 MongoDB 的 video 集合,用户资料才在 MySQL user_info 表,一次列表请求跨两个存储
hasLiked 会返回真实的点赞状态本章恒为 0,判存逻辑在后续互动章节才补上
推荐列表是从数据库实时算出来的是推荐系统提前算好 vid 串写进 Redis 的,列表接口只做截取和查库
page - redisPages 是笔误是有意的页码换算:推荐页占掉的页数要从总页码里减掉
findMovementsByVids 名字里的 movements 是复制粘贴忘了改确实是,这个方法名沿用了动态(movement)模块的命名,方法功能与名字不完全匹配,工程里普遍这么叫,读代码时按注释理解即可
PageResult 的 total 是真实总数本章传的是 0L,占位值;客户端实际按「下拉还有没有数据」加载更多,不依赖 total

官方文档

总结

  • 列表方案是「推荐优先、数据库兜底」:Redis VIDEOS_RECOMMEND_ 存 vid 串承担排序与削峰,Mongo video 集合按 created 倒序兜底;
  • 分页跨两个数据源,靠 redisPages 做页码平移,代价是推荐数据刷新时页码错位;
  • 列表链路四层:Controller 收参 → Service 编排 → Dubbo API → MongoTemplate,与动态模块同构,写熟一条其余照搬。复杂点全在 Redis vid 串的截取和 page - redisPages 页码换算上;
  • VO 组装是跨存储批查的标准动作:一次 findByIds + Map 关联,info != null 防引用失效。组装 VideoVo 靠两次 copyProperties + 三处手动映射,hasLiked/hasFocus 本章占位置 0,判存留给互动章节;
  • 每次刷新重复执行 Mongo 分页 + 批查用户 + 组装三段开销,结果却是确定的,这是引出 SpringCache 的直接动机,@Cacheable 注解已在本章后续就位;
  • 测试阶段最容易翻车的是用户表脏数据导致的空指针,if(info != null) 那行别省;
  • 关注列表与推荐列表分页思路完全不同,本章只实现了推荐优先链路,关注维度不构成查询条件;
  • 下一篇进入同章的存储部分:FastDFS:引入与内部结构已经讲过这套分布式文件系统的架构,如果还没读,建议从那里接着看,列表里的 videoUrl 到底是怎么来的,答案在那个章节。
Android功耗系列专题理论之十一:高通QCOM平台待机功耗问题分析方法 本文详细介绍了高通QCOM平台待机功耗问题的分析方法。首先强调测试环境标准化的重要性,包括关闭无线功能、日志记录和非必要传感器。重点分析了硬件连接检查、驱动配置验证、电源管理策略优化等关键步骤。文章详细讲解了射频校准、时钟源/中断分析、子系统状态检查等专业技术方法,并提供了具体的命令行操作示例。此外还介绍了外设拆解、外灌电压测试等硬件排查手段,以及功耗分解板、波形分析等高级诊断技术。最后强调了固件版本验证和环境因素测试的必要性,为工程师提供了系统性的功耗问题排查思路。 阅读详情

相关推荐

用STM32的SPI接口读取MT6835磁编码器,手把手教你避开数据读取的坑

本文详细介绍了如何使用STM32的SPI接口读取MT6835磁编码器,并提供了21个实战细节与避坑指南。从硬件连接、SPI配置到数据解析和实时调试,全面解析了读取位置信息过程中的常见问题及解决方案,帮助工程师在工业控制、伺服电机等场景中高效应用AMR技术。

weixin_30729609的博客 1441

Java 项目实战: 交友项目 - 视频功能:业务概述

小视频模块作为交友App中类抖音的沉浸式内容消费入口,区别于圈子动态的熟人社交,聚焦陌生人间的流量传播。其核心架构基于分层存储:视频文件存于自建FastDFS,封面图用OSS,元数据则由MongoDB管理。通过推荐系统优先提供视频列表,不足时补查数据库,实现高效分页。关键设计包括:vid数字唯一标识用于推荐权重计算,点赞/关注等互动数据冗余在Video实体中提升读取性能,且关注行为直接影响推荐算法。整体数据流清晰,为后续缓存优化与发布实现奠定基础。

Wang的专栏 337

win10任务栏文件夹设置成竖形列表样式

win10任务栏文件夹设置成竖形列表样式

ftzchina的博客 7081

java项目---探花交友

一、项目介绍 1、项目背景 在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展, 促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信 息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形 式丰富多样。 2、市场分析 探花交友项目定位于 陌生人交友市场。 根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资 的总额却大幅增长,达到68%

weixin_39868387的博客 1万+

光伏电站发电功率预测实战:从基础物理模型到LSTM时间序列分析

本文系统介绍了光伏电站发电功率预测的实战方法,从基础的物理晴空模型到先进的LSTM时间序列分析。通过结合电工杯等竞赛中常用的代码与模型,详细阐述了如何利用历史数据和数值天气预报构建高精度预测体系,并针对阴雨天气等预测瓶颈提出了分场景建模与模型融合等优化策略,为电站运维和电网调度提供可靠的数据支持。

pp12345的博客 216

Shopee商品详情页反爬实战:手把手逆向x-sap-Ri和x-sap-Sec请求头加密

本文详细解析了Shopee商品详情页反爬机制中x-sap-Ri和x-sap-Sec请求头参数的加密逻辑,通过JS逆向技术手把手教你定位加密入口、分析哈希算法与对称加密实现,并提供完整实战案例和应对反爬策略的高级技巧,助力开发者突破电商平台数据采集难点。

weixin_27875131的博客 1826

Java 项目实战: 交友项目 - 小视频模块实战:访客记录、FastDFS 存储与 SpringCache 缓存

本文实现小视频模块核心功能:基于MongoDB记录“谁看过我”访客数据,使用FastDFS分布式文件系统存储视频与缩略图,通过Dubbo服务完成发布与列表接口。结合Spring Cache注解(@Cacheable/@CacheEvict)对视频列表缓存30秒,发布后自动清空缓存,提升性能并保证数据实时性。工程新增视频、访客实体及缓存配置,三条技术线(MongoDB、FastDFS、Redis)协同支撑完整链路。

Wang的专栏 326

LeRobot 入门教程(一)软件环境、部署流程及摄像头配置

🤖 LeRobot:开源机器人学习平台 摘要:LeRobot是一个基于PyTorch的开源机器人学习平台,提供模型、数据集和工具,旨在降低机器人技术门槛。平台包含经过验证的模仿学习和强化学习方法,并提供预训练模型、人类演示数据集及模拟环境。安装需从源码获取,支持Python 3.10环境,可选扩展包括仿真环境(如gym-aloha)、电机控制(Dynamixel/Feetech SDK)和实验跟踪(Weights & Biases)。该平台托管在HuggingFace,并设有社区Discord频道

2506_90492529的博客 3417

基于springboot的校园交友平台网站系统java+vue项目

基于 Java + Spring Boot 的校园交友平台网站系统为学生们提供了一个便捷的交流和交友空间。 后端采用 Spring Boot 框架,确保系统的稳定运行和高效处理。它管理着用户信息、交友动态、互动数据等。当学生注册并完善个人资料后,后端会安全地存储这些信息,并根据用户设置的兴趣爱好等标签进行匹配推荐。 系统提供多种交友方式。学生可以发布自己的动态,分享生活点滴、兴趣爱好和学习心得等,后端会将这些动态推送给相关的用户,促进交流互动。同时,用户可以通过搜索功能查找具有共同兴趣的伙伴,后端快速响应查

全网粉丝10W+、全栈领域优质创作者、掘金、阿里云等社区博客专家、专注于全栈领域和毕业项目实战 1045

深度强化学习 | 基于DQN实现Flappy Bird游戏与分析

在Flappy Bird中,玩家需要通过控制小鸟安全穿过随机长度的水管来得分。本文基于深度Q网络DQN来实现Flappy Bird游戏的自主探索与学习

FRIGIDWINTER的博客 5913

WindowsServer2022蓝牙音频开启方法

为Windowsserver 2022 添加蓝牙音频驱动的方法和工具

thebestismin的专栏 2705

SpringBoot交友APP项目实战(详细介绍+案例源码) - 9.小视频(SpringCache缓存)

搭建一个Java框架的移动端交友APP,涵盖登录、注册、动态、即时通讯、视频(算法推荐)、个人中心等功能; 技术选型: Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo,即时通讯选用环信即时通讯

yuan 2799

学Simulink——基于Simulink的超前-滞后校正控制器设计与仿真建模示例

本文完成了基于 Simulink 的超前-滞后校正控制器设计与仿真,实现了:✅掌握超前与滞后校正的物理意义与设计流程✅分步设计复合控制器,兼顾动态与稳态✅在 Simulink 中搭建闭环系统并验证卓越性能✅达成高稳定裕度、高精度、快速响应三大目标。

xiaoheshang_123的博客 78

国际社交新趋势:Java实现多语言短视频交友功能

Java技术构建的全球化短视频交友系统通过SpringBoot+MySQL分库分表实现高并发处理,QPS达5000+;采用Uni-app实现多端复用,开发效率提升60%。系统集成104种语言实时翻译,支持23种货币支付,在东南亚市场单日礼物收入突破50万美元。通过WebSocket+Redis实现实时互动,消息推送成功率99.7%,结合GeoHash算法优化LBS社交。安全方面采用AES-256加密和自动化审核(准确率98%),未来将融合AI推荐和元宇宙社交,如AR试穿转化率提升35%。该系统已在多地区验证

小马总的博客 3048

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

Java 项目实战:交友项目 - 保存访客记录

在用户详情接口中同步记录访客数据,通过mfController.personalInfo触发,构建Visitors实体并调用Dubbo接口保存。核心逻辑包括:以userId为被访者、visitorUserId为当前登录人,使用date(毫秒时间戳)和visitDate(yyyyMMdd格式)实现按天去重;缘分值score从推荐表冗余获取,提升查询效率。采用同步写入,教学阶段链路清晰,生产环境建议异步化处理。

Wang的专栏 514

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

探花交友_第9章_小视频方案(新版)

探花交友_第9章_小视频方案(新版) 1. 我的访客 1.1 需求分析 1.1.1 功能说明 1.1.2 数据库表 1.2 记录访客数据 tanhua-model tanhua-app-server tanhua-dubbo-interface tanhua-dubbo-mongo 1.3 首页谁看过我 需求分析 tanhua-model tanhua-app-server MovementController MovementService tanhua-dubbo-interface tanhua-du

无 1510

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

Java 项目实战: 交友项目 - 发布视频的代码实现

发布小视频接口需同步处理封面(OSS)与视频(FastDFS)上传,并落库至MongoDB。核心链路为:接收MultipartFile → 校验 → 分别上传至OSS/FastDFS → 构造Video对象 → Dubbo调用save接口写入数据库。关键点包括:配置spring.servlet.multipart.max-file-size=30MB避免1MB默认限制;确保vid与_id双主键设计提升查询效率;落库后必须校验返回的videoId非空,否则用户将面临“视频发不出去”的死链问题。

Wang的专栏 294

java项目交友网如何实现,基于jsp的交友网站-JavaEE实现交友网站 - java项目源码

基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的交友网站, 该项目可用各类java课程设计大作业中, 交友网站的系统架构分为前后台两部分, 最终实现在线上进行交友网站各项功能,实现了诸如用户管理, 登录注册, 权限管理等功能, 并实现对各类交友网站相关的实体进行管理。该交友网站为一个采用mvc设计模式进行开发B/S架构项目,并采用分层架构对项目进行架构, 分为...

weixin_39949894的博客 742

Java 项目实战:交友项目 - 首页谁看过我

首页「谁看过我」通过Redis记录用户上次查看时间,实现增量查询:仅推送新访客,保持卡片动态更新。数据源自MongoDB的visitors集合,按来访时间倒序取最新5条,排序基于毫秒级时间戳,不依赖缘分值。接口独立于今日最佳,调用链路涵盖Controller、Service、Dubbo层及缓存读写。关键点在于补全“写回”游标逻辑,确保下次查询准确增量。

Wang的专栏 334

计算机毕业设计java网络交友 Java技术构建的网络社交平台设计与实现 基于Java的在线交友系统开发

本文介绍了一个基于Java技术的网络交友系统毕业设计项目,采用SSM框架和MySQL数据库开发。系统包含用户注册登录、交友信息管理、相册分享、交流论坛、公告管理等核心功能,分为会员和管理员两个角色。项目通过技术、经济和操作三方面可行性论证,采用B/S架构实现用户友好交互。系统设计包含详细的流程图、功能模块图和E-R图,并提供了完整的源码程序、数据库和论文文档。该方案旨在解决传统交友平台信息管理混乱的问题,为用户提供高效安全的网络社交体验。

毕设源码_刘学姐 1036

Java 项目实战: 交友项目 - 查询单条动态:动态详情页的接口与实现

本文详解动态详情页的数据来源与实现链路:通过 _id(ObjectId)查询单条动态,pid(Long自增)仅用于推荐系统;详情页接口 GET /movements/{id} 返回动态本体及用户信息,评论列表由独立 /comments 接口按 publishId 分页拉取;核心流程为 Controller 接收路径参数 → Service 查询动态与用户 → MovementsVo.init 组装数据 → 评论列表分页查询;强调前端仅使用 _id,避免将 pid 误用于详情查询,澄清双 id 分工边界。

Wang的专栏 137

【Spring开发】SpringCloud交友项目实战第2篇:介绍 《明日交友》,1、项目介绍【附代码文档】

1、Dubbo的前世今生 1、项目介绍 1.1、项目背景 1.2、市场分析 1.3、目标用户群体 1.4、使用场景 1.5、竞争对手分析 1.7、技术方案 2、前后端分离开发思想 3、开发工具与环境搭建 3.1、开发工具 3.2、环境搭建 4、sms_code验证玛 4.1 阿里云短信 4.2 自动装配 1. 完善用户信息 1.2 百度人脸识别 1.2.1 概述 1.2.2 账号申请 1.2.4 抽取模板工具 2.2 更新用户资料 2.2.1 接口文档 2.2.2 代码实现 3. 统一token处理 3.1

小帅说java 856

【源码集锦】基于Java语言的相亲交友系统-相亲交友APP-相亲交友平台的技术剖析

本文档详细描述了一个婚恋相亲交友系统的设计与实现。系统旨在解决传统相亲模式中的信息不对称和效率低下问题,通过互联网技术提供多元化和个性化的交友渠道。系统采用分层架构,包括表现层、业务逻辑层和数据层,使用Java、SpringBoot、MySQL、Redis、Vue.js等技术实现。主要功能模块包括用户管理、匹配推荐、互动交流和安全管理,支持多种注册登录方式、智能匹配、实时聊天、动态发布和实名认证等功能。系统实施与部署涉及硬件设备规划、软件安装配置、系统测试等步骤,并强调日常维护、故障处理和系统升级的重要性。

juzige03的博客 1320

Java 项目实战:交友项目 - 我的访客:需求说明

本文详解交友项目“我的访客”功能的数据设计与实现:基于MongoDB的visitors集合,记录用户被查看行为。通过userId、visitorUserId与visitDate组合去重,确保一天内仅记录一次;date字段用于排序与增量查询,score为推荐系统快照值,提升列表查询效率。访客记录在查看详情页时同步写入,无需额外接口,兼顾准确性与性能。

Wang的专栏 507

安排,探花交友项目基于 SSM+Dubbo+ElasticSearch geo+Spark+MongoDB

最近有小伙伴问我有没有最新的实战项目教程,经过一番查找,总算发现了一个技术栈比较全的项目。它是一个交友项目实战开发项目,用到的技术栈有SSM、Redis、MQ、Dubbo、ElasticSearch geo、Spark、MongoDB、系统推荐...等技术。部分页面截图技术架构图如下:来源:视频资源来自网络,如有侵权请告知删除完整项目教程及资料代码,怎么领取?关注公众号...

架构文摘 306

【Spring开发】SpringCloud交友项目实战第7篇:MongoDB入门【附代码文档】

1、Dubbo的前世今生 1、项目介绍 1.1、项目背景 1.2、市场分析 1.3、目标用户群体 1.4、使用场景 1.5、竞争对手分析 1.7、技术方案 2、前后端分离开发思想 3、开发工具与环境搭建 3.1、开发工具 3.2、环境搭建 4、sms_code验证玛 4.1 阿里云短信 4.2 自动装配 1. 完善用户信息 1.2 百度人脸识别 1.2.1 概述 1.2.2 账号申请 1.2.4 抽取模板工具 2.2 更新用户资料 2.2.1 接口文档 2.2.2 代码实现 3. 统一token处理 3.1

小帅说java 782
上一篇: Java 项目实战: 交友项目 - 发布视频的代码实现
下一篇: Java 项目实战:交友项目视频功能——关注与取消关注的需求分析与代码实现
Wang's Blog
博客等级 码龄12年 7418粉丝 2409原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值