随着“微短剧”市场的井喷式爆发,无论是网文IP改编、原创剧情拍摄,还是简单的搬运切片,都需要一个稳定、高效且极具扩展性的内容分发平台。然而,市面上的商业短剧系统往往价格高昂,且核心代码加密,不仅存在安全隐患,还严重制约了后期的功能迭代和业务拓展。
源码及演示:v.dyedus.top
对于追求极致掌控力和高度定制化的开发者与创业者而言,拥有一套真正意义上的“全开源、无加密”短剧系统源码,才是抓住这波风口的核心利器。
本文将围绕一套基于 PHP 后端 + UniApp 前端 的全开源短剧系统,进行深度的技术拆解。我们将从底层的架构设计、数据库建模,到核心的付费播放逻辑、多端打包适配,带你全方位领略这套系统的精妙之处。无论你是想直接部署上线,还是希望基于此进行二次开发,这篇文章都将是你不可或缺的权威指南。
一、 为什么选择 PHP + UniApp 构建短剧系统?
在正式启动之前,我们需要厘清技术选型的底层逻辑。为什么这套开源方案坚定地选择了 PHP 作为后端主力,以及 UniApp 作为前端跨端解决方案?
1. PHP 后端:高性能与开发效率的完美平衡
尽管在常驻内存的编程语言(如 Go、Java)面前,PHP 常被视为“传统”,但凭借其极其成熟的生态和极高的开发效率,PHP 依然是中小型音视频项目的首选。
- 丰富的音视频处理生态: 依托于 FFmpeg 的 PHP 扩展(如
php-ffmpeg),我们可以在后端正向实现视频的上传、转码、截图和 HLS 切片,无需依赖昂贵的第三方云服务。 - 卓越的框架支持: 本系统底层采用了目前工业界最标准的 Laravel 或 ThinkPHP 8 框架,提供了完善的 ORM、中间件、队列系统和强大的依赖注入容器,保证了代码的优雅性和可维护性。
- 低成本部署: PHP 的 LNMP (Linux + Nginx + MySQL + PHP) 架构可以说是世界上部署成本最低、兼容性最好的方案,单机日均支撑几万活跃用户轻而易举。
2. UniApp 前端:一次编写,多端发行
短剧应用的场景极度依赖碎片化流量,这意味着我们必须覆盖 H5(微信浏览器)、微信小程序、抖音小程序以及原生的 iOS/Android APP。
- 真正的跨端能力: UniApp 基于 Vue.js 语法,通过条件编译和统一的运行时,真正做到了“一套代码,多端运行”。这为企业节省了至少 50% 的前端开发人力成本。
- 原生体验的妥协与突破: 针对短剧核心的“上下滑切换”体验,我们放弃了传统的
swiper组件,而是引入了renderjs技术,直接操作底层 DOM 并结合 CSS3 硬件加速,实现了堪比原生 APP 的丝滑触控体验。
二、 系统架构设计与核心目录结构
优秀的代码结构是后续二次开发的基石。本系统严格遵循 MVC(模型-视图-控制器)设计模式,并在此基础上引入了 Service(服务层)和 Repository(仓储层)来解耦业务逻辑。
1. 后端 PHP 目录结构概览
app/
├── Controllers/ # 控制器层:处理 HTTP 请求与响应
│ ├── Api/ # 面向 UniApp 的 API 接口控制器
│ │ ├── VideoController.php # 视频流接口
│ │ ├── PayController.php # 支付回调接口
│ │ └── UserController.php # 用户认证接口
│ └── Admin/ # 后台管理控制器
├── Models/ # 模型层:定义数据表结构与关联关系
│ ├── Video.php # 短剧主模型
│ ├── Episode.php # 剧集模型
│ └── Order.php # 订单模型
├── Services/ # 服务层:封装核心复杂业务逻辑
│ ├── VideoService.php # 视频转码与播放鉴权
│ └── PaymentService.php # 支付下单与退款处理
└── Http/Middleware/ # 中间件:拦截请求进行鉴权或日志
└── UserAuth.php # 用户Token解析中间件
2. 前端 UniApp 目录结构概览
uni-app-project/
├── pages/ # 页面组件
│ ├── index/ # 首页(推荐、热门)
│ ├── play/ # 核心播放页(上下滑组件)
│ └── user/ # 个人中心与钱包
├── static/ # 静态资源(图片、字体)
├── store/ # Vuex 状态管理(用户登录态、系统配置)
├── common/ # 通用工具类(HTTP请求封装、常量定义)
└── manifest.json # 应用清单(配置APP图标、小程序AppID等)
三、 数据库核心建模:为海量视频与订单保驾护航

短剧系统的核心是“剧目”、“剧集”和“订单”。为了应对高并发的读写请求,数据库的设计必须兼顾范式与性能。以下是核心表结构的精简示例:
1. 短剧主表 (fa_video)
存储短剧的封面、标题、分类等聚合信息。
CREATE TABLE `fa_video` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL COMMENT '短剧标题',
`cover` varchar(500) DEFAULT '' COMMENT '封面图',
`description` text COMMENT '剧情简介',
`actor` varchar(255) DEFAULT '' COMMENT '主演',
`total_episodes` int(11) DEFAULT '0' COMMENT '总集数',
`is_end` tinyint(1) DEFAULT '0' COMMENT '是否完结:0=连载中,1=已完结',
`hits` int(11) DEFAULT '0' COMMENT '总播放量',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '单集价格(0表示免费)',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1=正常,0=下架',
`createtime` int(11) DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_status_hits` (`status`, `hits`) -- 优化热门榜单查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短剧主表';
2. 剧集表 (fa_video_episode)
存储每一集的具体视频地址和解锁条件。
CREATE TABLE `fa_video_episode` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`video_id` int(11) NOT NULL COMMENT '关联的短剧ID',
`episode_no` int(11) DEFAULT '1' COMMENT '第几集',
`title` varchar(200) DEFAULT '' COMMENT '本集标题',
`video_url` text COMMENT '视频播放地址 (OSS或本地)',
`try_watch_seconds` int(11) DEFAULT '60' COMMENT '试看秒数(0代表不可试看,999代表免费)',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '本集解锁价格',
`createtime` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_video_episode` (`video_id`, `episode_no`), -- 防止数据错乱
KEY `idx_video_id` (`video_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短剧剧集表';
3. 用户观看记录与订单表
记录用户的付费行为和观看进度,是实现“续播”和“购买记录”的基础。
四、 核心功能模块深度技术剖析
一套合格的短剧系统,绝不仅仅是简单的“视频上传+展示”。其背后的核心难点在于:视频的安全播放、灵活的付费体系以及多端适配。
1. 视频上传与安全播放机制(防止盗链与盗录)
为了节省服务器带宽并保证视频安全,本系统默认采用“本地/云存储 + CDN 分发”的模式,并结合签名的临时 URL 进行防盜链。
后端生成临时播放地址的逻辑(PHP伪代码):
// app/Services/VideoService.php
public function generatePlayUrl($episodeId, $userId) {
$episode = Episode::find($episodeId);
// 1. 检查是否免费或已购买
$canWatch = $this->checkPermission($userId, $episode->video_id, $episodeId);
if (!$canWatch) {
throw new \Exception("无权观看,请先购买");
}
// 2. 如果是云存储(如阿里云OSS),生成带有过期时间的签名URL
// 假设视频有效期为 300 秒,防止链接被扩散
$client = new \OSS\OssClient($accessKeyId, $accessKeySecret, $endpoint);
$signedUrl = $client->signUrl($bucket, $episode->video_url, 300, "GET");
// 3. 记录观看日志(用于后续推荐算法分析)
WatchLog::create(['user_id' => $userId, 'episode_id' => $episodeId]);
return $signedUrl;
}
2. 灵活的付费解锁体系
短剧变现的核心在于“卡点付费”。系统支持多种解锁模式:
- 单集购买: 用户按集付费,门槛低,适合长剧。
- 全剧购买: 打包价,折扣力度大,促进冲动消费。
- VIP会员: 包月/包年,畅看全站(结合后台会员组配置)。
支付回调是重中之重。以微信小程序支付为例,后端接收异步通知后的处理逻辑:
// 处理微信支付回调
public function notify(Request $request) {
$notifyData = $request->post(); // 获取微信回调数据
// 验签逻辑省略...
$order = Order::where('out_trade_no', $notifyData['out_trade_no'])->first();
if ($order && $notifyData['result_code'] == 'SUCCESS') {
DB::transaction(function () use ($order) {
// 1. 更新订单状态
$order->status = 'paid';
$order->save();
// 2. 记录用户解锁记录到 user_episode 表
UserEpisode::create([
'user_id' => $order->user_id,
'episode_id' => $order->episode_id,
'video_id' => $order->video_id
]);
// 3. 增加用户观看时长/次数统计(可选)
});
}
return response('<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>', 200, ['Content-Type' => 'text/xml']);
}
3. UniApp 前端核心:丝滑的上下滑播放体验
在 UniApp 中实现类似抖音的上下滑动体验,需要巧妙结合 swiper 组件和视频上下文 videoContext。
前端核心逻辑(Vue.js):
<template>
<view class="container">
<swiper :current="currentIndex" @change="onSwiperChange" :vertical="true" :duration="200" class="swiper-box">
<swiper-item v-for="(item, index) in videoList" :key="item.id">
<view class="video-card">
<!-- 使用条件编译,在H5和小程序端使用不同的播放器内核 -->
<!-- #ifdef H5 -->
<video
:id="'video_' + item.id"
:src="item.play_url"
:controls="false"
:show-fullscreen-btn="false"
:enable-progress-gesture="false"
object-fit="cover"
class="video-player"
:loop="true"
@timeupdate="onTimeUpdate"
></video>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<!-- APP端可使用原生VideoView或plus.video,性能更佳 -->
<!-- #endif -->
<!-- 右侧交互栏:头像、点赞、评论、分享 -->
<view class="right-menu">
<view class="menu-item" @tap="handleLike(item)">
<text class="iconfont icon-like"></text>
<text class="count">{
{ item.likes }}</text>
</view>
<view class="menu-item" @tap="showComment(item)">
<text class="iconfont icon-comment"></text>
<text class="count">{
{ item.comments }}</text>
</view>
</view>
<!-- 底部信息栏:标题、简介、选集 -->
<view class="bottom-info">
<text class="title">{
{ item.title }} - 第{
{ currentEpisode.episode_no }}集</text>
<text class="desc">{
{ item.description }}</text>
<scroll-view class="episode-scroll" scroll-x="true">
<view class="episode-list">
<view
v-for="ep in episodes"
:key="ep.id"
class="episode-item"
:class="{active: ep.id === currentEpisode.id}"
@tap="switchEpisode(ep)"
>
{
{ ep.episode_no }}
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
data() {
return {
currentIndex: 0,
videoList: [],
episodes: [],
currentEpisode: {
},
videoContexts: []
};
},
onLoad(options) {
this.loadVideoData(options.video_id);
},
methods: {
async loadVideoData(videoId) {
const res = await this.$api.getVideoDetail({
id: videoId });
this.videoList = [res.data.video];
this.episodes = res.data.episodes;
this.currentEpisode = this.episodes[0];
// 初始化所有视频上下文
this.$nextTick(() => {
this.videoList.forEach(v => {
this.videoContexts[v.id] = uni.createVideoContext('video_' + v.id, this);
});
this.playCurrentVideo();
});
},
onSwiperChange(e) {
let oldIndex = this.currentIndex;
this.currentIndex = e.detail.current;
// 停止上一个视频
if (this.videoContexts[this.videoList[oldIndex].id]) {
this.videoContexts[this.videoList[oldIndex].id].pause();
}
// 播放当前视频
this.playCurrentVideo();
},
playCurrentVideo() {
const currentVideo = this.videoList[this.currentIndex];
if (this.videoContexts[currentVideo.id]) {
this.videoContexts[currentVideo.id].play();
}
},
switchEpisode(ep) {
this.currentEpisode = ep;
// 重新加载视频源的逻辑...
}
}
};
</script>
<style scoped>
.container, .swiper-box, .video-card {
width: 100vw;
height: 100vh;
background-color: #000;
}
.video-player {
width: 100%;
height: 100%;
}
.right-menu {
position: absolute;
right: 20rpx;
bottom: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.menu-item {
margin-bottom: 40rpx;
color: #fff;
text-align: center;
}
.iconfont {
font-size: 60rpx;
}
.count {
display: block;
font-size: 24rpx;
margin-top: 8rpx;
}
.bottom-info {
position: absolute;
left: 20rpx;
right: 120rpx;
bottom: 40rpx;
color: #fff;
}
.title {
font-size: 32rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.desc {
font-size: 26rpx;
opacity: 0.8;
display: block;
margin-bottom: 20rpx;
}
.episode-scroll {
width: 100%;
white-space: nowrap;
}
.episode-list {
display: inline-flex;
}
.episode-item {
padding: 10rpx 20rpx;
background-color: rgba(255,255,255,0.2);
border-radius: 8rpx;
margin-right: 15rpx;
color: #fff;
font-size: 24rpx;
}
.episode-item.active {
background-color: #ff4757;
}
</style>
五、 二次开发实战指南:从零添加一个“演员表”模块

“全开源无加密”的最大优势在于可以任意修改底层逻辑。假设现在你的运营团队提出需求:“我们需要在短剧详情页展示主演的头像和简介,并且点击演员可以跳转到他的其他作品。”
以下是标准的二次开发流:
Step 1:创建数据库迁移文件与模型
首先在 PHP 端扩展数据库结构。
php think make:migration AddActorTable
// database/migrations/xxxx_AddActorTable.php
use think\migration\Migrator;
use think\migration\db\Column;
class AddActorTable extends Migrator
{
public function up()
{
$table = $this->table('actor', ['comment' => '演员表']);
$table->addColumn('name', 'string', ['limit' => 50, 'comment' => '演员名称'])
->addColumn('avatar', 'string', ['limit' => 255, 'default' => '', 'comment' => '头像'])
->addColumn('bio', 'text', ['null' => true, 'comment' => '简介'])
->addColumn('createtime', 'integer', ['default' => 0])
->create();
}
public function down()
{
$this->table('actor')->drop();
}
}
Step 2:编写 API 接口与业务逻辑
创建控制器 ActorController 并添加获取演员详情及关联视频的接口。
// app/controllers/api/ActorController.php
class ActorController
{
// 获取演员详情及其参演作品
public function detail($id)
{
$actor = ActorModel::find($id);
if (!$actor) {
return json(['code' => 404, 'msg' => '演员不存在']);
}
// 假设有中间表 actor_video 关联演员和短剧
$videos = $actor->videos()->with('episodes')->select();
return json([
'code' => 1,
'msg' => 'success',
'data' => [
'actor' => $actor,
'videos' => $videos
]
]);
}
}
Step 3:UniApp 前端页面开发与联调
在 UniApp 项目中创建 pages/actor/detail.vue 页面,通过 onLoad 获取路由参数 id 并请求数据渲染。
<!-- pages/actor/detail.vue -->
<template>
<view class="actor-container">
<view class="actor-header">
<image :src="actor.avatar" mode="aspectFill" class="avatar"></image>
<text class="name">{
{ actor.name }}</text>
</view>
<view class="actor-bio">
<text class="section-title">简介</text>
<text class="bio-text">{
{ actor.bio || '暂无简介' }}</text>
</view>
<view class="actor-videos">
<text class="section-title">参演作品 ({
{ videos.length }})</text>
<scroll-view scroll-x class="video-scroll">
<view v-for="video in videos" :key="video.id" class="video-item" @tap="goToVideo(video.id)">
<image :src="video.cover" mode="aspectFill"></image>
<text class="video-title">{
{ video.title }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
actor: {
},
videos: []
};
},
onLoad(options) {
if (options.id) {
this.fetchActorDetail(options.id);
}
},
methods: {
async fetchActorDetail(id) {
const res = await this.$api.getActorDetail({
id });
if (res.code === 1) {
this.actor = res.data.actor;
this.videos = res.data.videos;
uni.setNavigationBarTitle({
title: this.actor.name
});
}
},
goToVideo(videoId) {
uni.navigateTo({
url: `/pages/play/play?video_id=${
videoId}`
});
}
}
};
</script>
<style scoped>
.actor-container {
padding: 30rpx; background-color: #f8f8f8; min-height: 100vh; }
.actor-header {
display: flex; flex-direction: column; align-items: center; margin-bottom: 40rpx; }
.avatar {
width: 150rpx; height: 150rpx; border-radius: 50%; margin-bottom: 20rpx; }
.name {
font-size: 36rpx; font-weight: bold; }
.section-title {
font-size: 32rpx; font-weight: bold; display: block; margin-bottom: 20rpx; color: #333; }
.bio-text {
font-size: 28rpx; color: #666; line-height: 1.6; }
.video-scroll {
width: 100%; white-space: nowrap; margin-top: 20rpx; }
.video-item {
display: inline-block; width: 200rpx; margin-right: 20rpx; vertical-align: top; }
.video-item image {
width: 200rpx; height: 280rpx; border-radius: 8rpx; }
.video-title {
font-size: 24rpx; color: #333; display: block; white-space: normal; margin-top: 10rpx; }
</style>
六、 部署与运维:从源码到线上生产环境

拿到开源源码后,如何快速将其部署到线上供用户使用?这里我们推荐使用 Docker 进行容器化部署,以保证环境的一致性。
1. 服务器基础环境
- 系统: Ubuntu 22.04 LTS
- Web服务: Nginx 1.24+
- 数据库: MySQL 5.7+ / MariaDB 10.2+
- PHP版本: PHP 8.1+ (需安装 fileinfo, redis, gd, openssl 等扩展)
2. Nginx 伪静态与视频跨域配置
为了保障 H5 端的视频能正常播放并处理可能的 OPTIONS 预检请求,Nginx 配置需包含以下关键项:
server {
listen 80;
server_name your_domain.com;
root /www/wwwroot/short_drama/public; # 指向Laravel/TP的public目录
index index.php index.html index.htm;
# 解决前端H5视频播放跨域问题
location ~* \.(mp4|avi|mov)$ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header Access-Control-Allow-Headers X-Requested-With;
# 断点续传支持
max_ranges 0;
}
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=$1 last;
break;
}
}
location ~ \.php$ {
fastcgi_pass unix:/tmp/php-cgi-81.sock; # 根据实际PHP版本调整
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
3. 定时任务与队列守护
系统依赖 Cron 来处理过期订单清理、统计数据生成等后台任务。
# 编辑 crontab
crontab -e
# 添加以下任务(假设PHP可执行文件路径为 /usr/bin/php,项目路径为 /www/wwwroot/short_drama)
* * * * * cd /www/wwwroot/short_drama && /usr/bin/php think schedule:run >> /dev/null 2>&1
七、 总结与展望
在这篇文章中,我们深度解剖了一套基于 PHP 和 UniApp 构建的开源短剧系统。从底层的数据库设计、核心的付费播放鉴权逻辑,到前端的丝滑滑动体验以及实际的二次开发案例,不难发现:这套全开源、无加密的源码,不仅为企业和个人开发者省去了高昂的试错成本,更为后续的定制化商业运营提供了无限可能。