
一、 项目架构与技术选型
在开始之前,我们要明确这个项目的核心逻辑:移动端负责展示与交互,后端负责内容分发与业务逻辑。
- 后端:SpringBoot + MyBatis-Plus + Redis + MinIO(对象存储)
- 理由:SpringBoot 生态成熟,处理高并发请求(如视频流、点赞)稳定;Redis 用于缓存热门视频和做分布式锁;MinIO 自建私有云存储,比 OSS 成本更低,适合副业项目起步。
- 前端:Uniapp (Vue3) + TypeScript
- 理由:一套代码,多端发布(iOS、Android、H5、小程序),性价比极高。
二、 后端核心实现
1. 数据库设计
我们需要核心的几张表:视频表、用户表、点赞表。
CREATE TABLE `tb_video` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '发布者ID',
`video_url` varchar(255) NOT NULL COMMENT '视频地址',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面地址',
`title` varchar(50) DEFAULT NULL COMMENT '视频标题',
`like_count` int(11) DEFAULT '0' COMMENT '点赞数',
`is_deleted` tinyint(1) DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 视频流接口(核心 Feed 流)
仿抖音的核心是“滑动刷新”,我们需要一个高效的分页查询接口。这里使用 MyBatis-Plus 简化开发。
Entity 实体类 (Video.java):
@Data
@TableName("tb_video")
public class Video {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String videoUrl;
private String coverUrl;
private String title;
private Integer likeCount;
// getter/setter 省略,使用 Lombok @Data
}
Service 业务层 (VideoService.java):
这里演示基础的分页查询。在生产环境中,通常结合 Redis 做推荐算法(如基于权重的 Feed 流)。
@Service
public class VideoService {
@Autowired
private VideoMapper videoMapper;
/**
* 分页获取视频列表(仿抖音首页Feed流)
*/
public IPage<Video> getVideoFeed(long current, long size) {
Page<Video> page = new Page<>(current, size);
// 按照创建时间倒序排列
QueryWrapper<Video> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("create_time");
return videoMapper.selectPage(page, queryWrapper);
}
}
Controller 控制层 (VideoController.java):
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@GetMapping("/feed")
public Result getFeed(@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "5") long size) {
IPage<Video> videoPage = videoService.getVideoFeed(current, size);
return Result.success(videoPage);
}
}
3. 文件上传接口 (MinIO)
视频上传是短视频应用最消耗资源的部分。
@PostMapping("/upload")
public Result uploadVideo(MultipartFile file) {
try {
// 1. 生成唯一文件名
String fileName = UUID.randomUUID().toString() + ".mp4";
// 2. 调用 MinIO 工具类上传(需自行配置 MinIOClient)
String url = MinIOUtil.upload(file.getInputStream(), fileName);
// 3. 返回访问地址
return Result.success(url);
} catch (Exception e) {
e.printStackTrace();
return Result.error("上传失败");
}
}
三、 前端核心实现
1. 视频滑页组件
这是仿抖音的灵魂,使用 swiper 组件实现垂直滑动。
<template>
<view class="container">
<swiper
class="swiper-box"
:vertical="true"
:circular="true"
@change="onSwiperChange"
>
<swiper-item v-for="(item, index) in videoList" :key="item.id">
<view class="video-item">
<!-- 视频播放器 -->
<video
:id="'video-' + index"
:src="item.videoUrl"
:controls="false"
:loop="true"
:autoplay="index === currentIndex"
objectFit="contain"
class="player-video"
@play="handlePlay"
></video>
<!-- 侧边互动栏(点赞、评论) -->
<view class="sidebar">
<view class="avatar">
<image :src="item.userAvatar" mode="aspectFill"></image>
</view>
<view class="action-btn" @click="toggleLike(item)">
<text class="iconfont">{{ item.isLiked ? '❤️' : '🤍' }}</text>
<text class="count">{{ item.likeCount }}</text>
</view>
</view>
<!-- 底部信息 -->
<view class="bottom-info">
<text class="username">@{{ item.userName }}</text>
<text class="desc">{{ item.title }}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue';
import { getVideoFeed } from '@/api/video'; // 假设封装了API请求
const videoList = ref([]);
const currentIndex = ref(0);
onMounted(async () => {
await loadVideos();
// 自动播放第一个视频(部分平台需用户交互后才能播放)
playVideo(0);
});
const loadVideos = async () => {
const res = await getVideoFeed(1, 5); // 调用后端接口
videoList.value = res.records;
};
const onSwiperChange = (e) => {
// 停止上一个视频
pauseVideo(currentIndex.value);
// 更新当前索引
currentIndex.value = e.detail.current;
// 播放当前视频
playVideo(currentIndex.value);
};
const playVideo = (index) => {
const videoContext = uni.createVideoContext(`video-${index}`);
videoContext.play();
};
const pauseVideo = (index) => {
const videoContext = uni.createVideoContext(`video-${index}`);
videoContext.pause();
};
const toggleLike = (item) => {
item.isLiked = !item.isLiked;
item.likeCount += item.isLiked ? 1 : -1;
// 此处调用后端接口更新数据库
};
</script>
<style>
.container { height: 100vh; width: 100vw; background: #000; }
.swiper-box { height: 100%; width: 100%; }
.video-item { height: 100%; width: 100%; position: relative; }
.player-video { height: 100%; width: 100%; }
/* 侧边栏样式 */
.sidebar { position: absolute; right: 10px; bottom: 100px; display: flex; flex-direction: column; align-items: center; }
.avatar { width: 50px; height: 50px; border-radius: 50%; border: 2px solid #fff; overflow: hidden; margin-bottom: 20px; }
.action-btn { display: flex; flex-direction: column; align-items: center; margin-bottom: 20px; color: #fff; }
/* 底部信息样式 */
.bottom-info { position: absolute; left: 10px; bottom: 20px; width: 80%; color: #fff; }
.username { font-weight: bold; font-size: 16px; margin-bottom: 5px; display: block; }
.desc { font-size: 14px; opacity: 0.9; }
</style>
四、 避坑指南与上线优化
结合项目经验,这里有几个关键点需要注意:
- 视频加载优化:不要一次性加载所有视频,使用“预加载”机制。在用户滑动到
index+1时,在后台静默加载下一个视频资源。 - 视频格式转码:用户上传的视频格式五花八门,后端必须使用 FFmpeg 进行统一转码(转为 H.264 编码的 MP4),否则在 Android 和 iOS 上会出现兼容性问题。
- CDN 加速:如果你想真正做到“可上线”,视频资源必须走 CDN。MinIO 只是存储,必须结合 CDN 回源配置,否则用户播放会卡顿。
- Uniapp 原生插件:对于视频的高级特效(滤镜、贴纸),Uniapp 默认的
<video>标签能力有限,可能需要购买或引入原生插件(如 DCloud 市场的短视频 SDK)。 这套方案跑通后,你不仅拥有了一个可演示的 App,更掌握了一条完整的“短视频后端+跨端前端”技术链。对于做副业来说,这既可以作为外包项目的案例,也可以作为你自己打造独立产品的起点。












评论(0)