简介:这份双端影视APP无加密修复版源码,面向希望快速搭建自有影视平台的开发者与站长,基于苹果CMS对接,只需开启API开放接口即可运行,支持一键生成安卓与苹果客户端,省去分别开发的成本。资源包共673个文件,以312个png界面素材、120个js脚本、54个php后端文件、46个css样式及32个html页面为主,另含字体、图标与sql数据库文件,压缩包约36.91MB,结构完整便于二次修改。功能覆盖首页幻灯片与搜索栏、播放页、代理中心广告位,三种运营模式、代理分销、卡密充值、视频下载、播放记录与收藏、用户留言、首页公告、缓存清理、聚合直播,并支持绑定设备码防乱注册及邀请注册奖励设置。附带的安装教程说明了后台源码上传、config1.php数据库配置与data.sql导入流程,配合HBuilderX即可完成打包。目前已有197人学习,适合想低成本验证影视运营模式的读者参考。
1. 双端影视APP无加密修复版源码:一套能直接跑通前后端的实战包
最近在折腾一套双端影视APP的源码,拿到的版本标注是“无加密修复版”,附带一份教程文档。说实话,影视类APP的源码包市面上流通的不少,但大多数要么核心逻辑被加密成黑匣子,要么后端接口对不上前端调用,跑起来一堆红字报错。这套修复版最大的不同在于:前后端代码全部明文可读,数据库结构完整,接口定义和前端请求能一一对应,教程里还标了几个常见崩溃点的处理方式。
它解决的核心问题是“拿到源码跑不起来”这个老毛病。适合两类人:一是想学影视APP完整架构的开发者,从前端UI到后端接口到数据库设计能一条龙看明白;二是需要快速搭建一个可演示的影视类应用原型的团队,省去从零搭架子时间。技术栈是典型的移动端混合方案,前端双端(Android/iOS)共用一套业务逻辑,后端提供RESTful接口,数据库用MySQL。下面从环境搭建开始,一步步拆这套源码怎么落地。
2. 环境搭建与项目结构拆解:从解压到第一个接口通
2.1 后端运行环境与依赖安装
这套源码的后端基于PHP+MySQL的经典组合,框架层面没有用重型框架,而是轻量级的自建路由加PDO封装。好处是依赖少、部署快,坏处是有些地方需要手动配置。我一般会先确认PHP版本,源码里用了一些7.4+的特性,比如箭头函数和类型属性,所以PHP版本不能低于7.4。
先看后端目录结构,解压后大概是这样:
backend/ ├── config/ │ └── database.php # 数据库连接配置 ├── core/ │ ├── Router.php # 路由调度 │ └── Request.php # 请求封装 ├── api/ │ ├── video.php # 视频相关接口 │ ├── user.php # 用户相关接口 │ └── category.php # 分类接口 ├── admin/ # 后台管理接口 └── public/ └── index.php # 入口文件安装依赖用Composer,源码根目录下有composer.json,但依赖不多,主要就是几个工具类库。执行:
cd backend composer install --no-dev--no-dev参数是跳过开发依赖,生产环境部署时用得上。如果本地调试需要看详细错误,可以不加这个参数。
数据库配置在config/database.php里,需要改这几个参数:
// config/database.php return [ 'host' => '127.0.0.1', // 数据库地址 'port' => 3306, // 端口 'dbname' => 'movie_app', // 数据库名 'username' => 'root', // 用户名 'password' => 'your_pass', // 密码 'charset' => 'utf8mb4', // 字符集,必须utf8mb4 ];字符集这里有个坑:源码里存了emoji和特殊符号的影评内容,如果数据库用utf8而不是utf8mb4,插入时会截断报错。建库时就要指定:
CREATE DATABASE movie_app DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后导入源码包里的database.sql文件,表结构就齐了。导入命令:
mysql -u root -p movie_app < database.sql导入完成后检查一下表数量,正常应该有12张表左右,包括用户表、视频表、分类表、收藏表、评论表、播放记录表等。如果少表,说明SQL文件没导完整,重新导一次。
2.2 前端双端工程结构与编译配置
前端部分用的是跨平台方案,Android和iOS共用一套业务代码,通过条件编译处理平台差异。工程目录大致如下:
frontend/ ├── common/ # 公共业务逻辑 │ ├── api/ # 接口请求封装 │ ├── utils/ # 工具函数 │ └── store/ # 状态管理 ├── android/ # Android平台特有代码 ├── ios/ # iOS平台特有代码 ├── pages/ # 页面文件 │ ├── index/ # 首页 │ ├── detail/ # 详情页 │ ├── search/ # 搜索页 │ └── user/ # 个人中心 └── manifest.json # 应用配置文件接口请求封装在common/api/request.js里,这里需要改后端地址:
// common/api/request.js const BASE_URL = 'http://你的后端地址:端口/api'; function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token过期,跳转登录 uni.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { reject(res.data); } }, fail: reject }); }); }BASE_URL必须改成实际后端地址,本地调试时如果是真机运行,不能用localhost,要用局域网IP。这个坑几乎每个人都会踩一次:模拟器上localhost能通,真机上就不行,因为localhost指向手机自己。
编译打包时,Android和iOS分别走各自的构建流程。Android用HBuilderX云打包或本地打包都行,iOS需要证书和描述文件。源码教程里给了详细的打包步骤截图,按着走基本没问题。唯一要注意的是应用图标和启动图要替换成自己的,不然审核可能被拒。
3. 核心功能模块实现:视频流、用户体系与播放记录
3.1 视频列表与详情接口的数据流转
视频模块是整个APP的核心,涉及列表展示、分类筛选、详情获取、播放地址解析几个环节。先看后端视频接口的实现逻辑。
视频列表接口在api/video.php里,接收分类ID、页码、每页数量三个参数:
// api/video.php 视频列表 public function list() { $categoryId = $_GET['category_id'] ?? 0; $page = max(1, intval($_GET['page'] ?? 1)); $pageSize = min(50, intval($_GET['page_size'] ?? 20)); $offset = ($page - 1) * $pageSize; $where = "status = 1"; $params = []; if ($categoryId > 0) { $where .= " AND category_id = :category_id"; $params[':category_id'] = $categoryId; } $sql = "SELECT id, title, cover, score, category_id, play_count FROM videos WHERE {$where} ORDER BY id DESC LIMIT {$offset}, {$pageSize}"; $list = $this->db->query($sql, $params); // 获取总数用于分页 $countSql = "SELECT COUNT(*) as total FROM videos WHERE {$where}"; $total = $this->db->query($countSql, $params)[0]['total']; return [ 'code' => 200, 'data' => [ 'list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $pageSize ] ]; }这里有几个参数需要留意:pageSize做了上限50的限制,防止客户端传个超大值把数据库拖垮;status = 1表示只查已上架的视频;排序用id DESC保证新视频在前。分页用LIMIT offset, pageSize,数据量大时offset效率会下降,但影视类APP通常几千条数据,问题不大。
详情接口多了一个播放地址的获取逻辑。源码里播放地址不是直接存在视频表里的,而是单独一张video_sources表,一个视频对应多个播放源:
// api/video.php 视频详情 public function detail() { $id = intval($_GET['id'] ?? 0); if ($id <= 0) { return ['code' => 400, 'msg' => '参数错误']; } $video = $this->db->query( "SELECT * FROM videos WHERE id = :id AND status = 1", [':id' => $id] ); if (empty($video)) { return ['code' => 404, 'msg' => '视频不存在']; } // 获取播放源 $sources = $this->db->query( "SELECT id, source_name, play_url, quality FROM video_sources WHERE video_id = :vid AND status = 1 ORDER BY sort ASC", [':vid' => $id] ); $video[0]['sources'] = $sources; // 播放量+1 $this->db->execute( "UPDATE videos SET play_count = play_count + 1 WHERE id = :id", [':id' => $id] ); return ['code' => 200, 'data' => $video[0]]; }播放源单独建表的好处是:一个视频可以挂多个线路,某个线路挂了可以切另一个;不同清晰度也可以分开配置。sort字段控制前端显示顺序,数字小的排前面。
前端详情页拿到数据后,渲染播放器。播放器用的是原生video组件,支持HLS和MP4两种格式。如果播放地址是m3u8格式,需要确保后端返回的Content-Type正确,否则iOS上可能播不了。
3.2 用户注册登录与Token鉴权链路
用户体系涉及注册、登录、Token生成与校验三个环节。源码里Token没有用JWT,而是简单的“随机字符串+数据库存储”方案。虽然不如JWT优雅,但胜在简单可控,适合这套轻量级架构。
注册接口的核心逻辑:
// api/user.php 注册 public function register() { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $email = trim($_POST['email'] ?? ''); // 基础校验 if (strlen($username) < 3 || strlen($username) > 20) { return ['code' => 400, 'msg' => '用户名长度3-20位']; } if (strlen($password) < 6) { return ['code' => 400, 'msg' => '密码至少6位']; } // 检查用户名是否已存在 $exists = $this->db->query( "SELECT id FROM users WHERE username = :u", [':u' => $username] ); if (!empty($exists)) { return ['code' => 400, 'msg' => '用户名已存在']; } // 密码哈希 $hash = password_hash($password, PASSWORD_BCRYPT); $this->db->execute( "INSERT INTO users (username, password, email, created_at) VALUES (:u, :p, :e, NOW())", [':u' => $username, ':p' => $hash, ':e' => $email] ); return ['code' => 200, 'msg' => '注册成功']; }密码用password_hash做bcrypt哈希,这是PHP里比较稳妥的做法。不要用md5或sha1,那些已经不安全了。
登录成功后生成Token:
// api/user.php 登录 public function login() { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $user = $this->db->query( "SELECT id, username, password FROM users WHERE username = :u", [':u' => $username] ); if (empty($user) || !password_verify($password, $user[0]['password'])) { return ['code' => 400, 'msg' => '用户名或密码错误']; } // 生成Token $token = bin2hex(random_bytes(32)); $expireAt = date('Y-m-d H:i:s', strtotime('+7 days')); // 存Token $this->db->execute( "INSERT INTO user_tokens (user_id, token, expire_at) VALUES (:uid, :token, :expire)", [':uid' => $user[0]['id'], ':token' => $token, ':expire' => $expireAt] ); return [ 'code' => 200, 'data' => [ 'token' => $token, 'user_id' => $user[0]['id'], 'username' => $user[0]['username'] ] ]; }Token有效期7天,存在user_tokens表里。每次请求前端在Header里带Token,后端中间件校验有效性和过期时间。过期了就返回401,前端跳登录页重新获取。
这里有个细节:Token没有做刷新机制,7天一到就必须重新登录。如果要做无感刷新,可以在Token快过期时自动续期,但源码里没实现,需要自己加。
3.3 播放记录与收藏功能的数据库设计
播放记录和收藏是提升用户粘性的功能,实现上都是“用户ID+视频ID”的关联表。播放记录多了一个“看到第几秒”的字段,用于续播。
播放记录表结构:
CREATE TABLE `play_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `video_id` int(11) NOT NULL, `progress` int(11) DEFAULT 0 COMMENT '播放进度,单位秒', `duration` int(11) DEFAULT 0 COMMENT '视频总时长,单位秒', `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_video` (`user_id`,`video_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;uk_user_video唯一索引保证一个用户对一个视频只有一条记录,重复插入时用ON DUPLICATE KEY UPDATE更新进度:
// api/user.php 上报播放进度 public function reportProgress() { $userId = $this->getUserId(); // 从Token解析 $videoId = intval($_POST['video_id'] ?? 0); $progress = intval($_POST['progress'] ?? 0); $duration = intval($_POST['duration'] ?? 0); $this->db->execute( "INSERT INTO play_records (user_id, video_id, progress, duration) VALUES (:uid, :vid, :p, :d) ON DUPLICATE KEY UPDATE progress = :p2, duration = :d2", [ ':uid' => $userId, ':vid' => $videoId, ':p' => $progress, ':d' => $duration, ':p2' => $progress, ':d2' => $duration ] ); return ['code' => 200, 'msg' => 'ok']; }前端在播放器里监听时间更新事件,每隔10秒上报一次进度。不要每秒都上报,那样请求太频繁,后端压力大。10秒是个比较平衡的间隔。
收藏功能更简单,就是插入和删除:
// 收藏 public function addFavorite() { $userId = $this->getUserId(); $videoId = intval($_POST['video_id'] ?? 0); $this->db->execute( "INSERT IGNORE INTO favorites (user_id, video_id) VALUES (:uid, :vid)", [':uid' => $userId, ':vid' => $videoId] ); return ['code' => 200, 'msg' => '收藏成功']; } // 取消收藏 public function removeFavorite() { $userId = $this->getUserId(); $videoId = intval($_POST['video_id'] ?? 0); $this->db->execute( "DELETE FROM favorites WHERE user_id = :uid AND video_id = :vid", [':uid' => $userId, ':vid' => $videoId] ); return ['code' => 200, 'msg' => '已取消收藏']; }INSERT IGNORE避免重复收藏报错,配合唯一索引使用。前端收藏按钮做状态切换,已收藏显示实心图标,未收藏显示空心图标。
4. 避坑与常见问题排查:那些教程里没写的翻车点
4.1 接口返回正常但前端显示空白
现象:后端接口用Postman测试返回200,数据也正常,但APP页面上什么都不显示。
原因:最常见的是跨域问题。浏览器和APP的请求机制不同,APP里没有跨域限制,但如果用HBuilderX的内置浏览器调试,就会遇到跨域。另外,前端请求封装里对返回数据的解析路径可能和后端实际返回结构不一致。
解决:先确认后端返回的JSON结构,比如是{code:200, data:{list:[...]}}还是{code:200, data:[...]}。然后检查前端request.js里resolve(res.data.data)这一行,如果后端返回的data直接是数组,这里没问题;如果data里还有一层,就要改成resolve(res.data.data.list)。跨域问题在APP真机运行时不存在的,如果只在浏览器调试时出现,可以给后端加CORS头:
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Content-Type, Token'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS');4.2 视频播放到一半卡住或黑屏
现象:视频能开始播放,但播到某个时间点就卡住,或者直接黑屏。
原因:播放地址失效是最常见的。影视类APP的播放源通常有时效性,源码里存的播放地址如果是外部链接,过一段时间就会失效。另外,m3u8格式的切片如果某个ts文件缺失,也会导致卡住。
解决:先检查video_sources表里的play_url是否还能访问。如果失效了,需要更新播放源。源码里没有做播放源自动检测,可以自己加一个定时任务,定期检查播放地址的可用性。另外,前端播放器要加错误监听:
// 播放器错误处理 videoContext.onError((err) => { console.error('播放错误', err); // 尝试切换下一个播放源 if (currentSourceIndex < sources.length - 1) { currentSourceIndex++; videoContext.src = sources[currentSourceIndex].play_url; } else { uni.showToast({ title: '播放失败,请稍后重试', icon: 'none' }); } });4.3 数据库导入报错“Unknown collation”
现象:导入database.sql时提示Unknown collation: 'utf8mb4_0900_ai_ci'。
原因:SQL文件是在MySQL 8.0环境下导出的,用了8.0特有的排序规则。如果你的MySQL是5.7或更低版本,就不认识这个排序规则。
解决:用文本编辑器打开SQL文件,把utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci,保存后再导入。或者升级MySQL到8.0版本。我一般建议用5.7,兼容性好,很多虚拟主机也都支持。
4.4 Token失效后前端没有正确跳转登录
现象:Token过期后,接口返回401,但APP没有跳转到登录页,而是卡在当前页面或者报网络错误。
原因:前端请求封装里对401的处理逻辑不完整。有些页面直接调用了uni.request而没有走封装的request函数,导致401没有被统一拦截。
解决:确保所有接口请求都走common/api/request.js里的封装函数。另外,在401处理里加一个防抖,避免多个接口同时返回401时重复跳转:
let isRedirecting = false; // 401处理 if (res.data.code === 401) { if (!isRedirecting) { isRedirecting = true; uni.removeStorageSync('token'); uni.navigateTo({ url: '/pages/login/login', complete: () => { isRedirecting = false; } }); } reject(res.data); }4.5 打包后APP启动闪退
现象:在HBuilderX里真机运行正常,但打包成APK安装后一打开就闪退。
原因:常见的有两个:一是应用签名问题,打包时用了默认签名或者签名不完整;二是权限配置缺失,比如没有在manifest.json里声明网络权限。
解决:Android打包必须用自有证书,不能用公共测试证书。在manifest.json的app-plus节点下确认权限配置:
{ "app-plus": { "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.INTERNET\"/>", "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>" ] } } } }网络权限是必须的,没有这个权限,APP连后端接口都请求不了,自然闪退。
5. 进阶技巧:播放源自动检测与接口性能优化
5.1 播放源可用性自动检测脚本
影视类APP最头疼的问题就是播放源失效。手动一个个检查不现实,我一般会写一个定时脚本,批量检测播放地址的HTTP状态码,把失效的标记出来。
// cron/check_sources.php 播放源检测脚本 require_once __DIR__ . '/../config/database.php'; $db = new PDO( "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4", $config['username'], $config['password'] ); // 取出所有启用的播放源 $sources = $db->query( "SELECT id, play_url FROM video_sources WHERE status = 1" )->fetchAll(PDO::FETCH_ASSOC); $invalidIds = []; foreach ($sources as $source) { $ch = curl_init($source['play_url']); curl_setopt($ch, CURLOPT_NOBODY, true); // 只发HEAD请求,不下载内容 curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 5秒超时 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过SSL验证,避免误判 curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // 非200状态码视为失效 if ($httpCode !== 200) { $invalidIds[] = $source['id']; echo "失效: ID={$source['id']} URL={$source['play_url']} Code={$httpCode}\n"; } } // 批量标记失效 if (!empty($invalidIds)) { $placeholders = implode(',', array_fill(0, count($invalidIds), '?')); $stmt = $db->prepare( "UPDATE video_sources SET status = 0 WHERE id IN ({$placeholders})" ); $stmt->execute($invalidIds); echo "共标记 " . count($invalidIds) . " 个失效源\n"; }这个脚本用HEAD请求而不是GET,只拿响应头不下载内容,速度快很多。CURLOPT_TIMEOUT设5秒,避免慢速源拖垮整个检测流程。检测结果把失效源的status置为0,前端就不会再展示这些源了。
脚本可以配crontab定时执行,比如每天凌晨3点跑一次:
# 每天凌晨3点检测播放源 0 3 * * * /usr/bin/php /path/to/cron/check_sources.php >> /var/log/source_check.log 2>&15.2 接口缓存与数据库查询优化
视频列表接口是访问量最大的,每次请求都查数据库压力不小。我一般会加一层文件缓存,把热门分类的列表数据缓存几分钟。
// api/video.php 带缓存的列表接口 public function listWithCache() { $categoryId = intval($_GET['category_id'] ?? 0); $page = max(1, intval($_GET['page'] ?? 1)); // 缓存key,只缓存前3页 $cacheKey = "video_list_{$categoryId}_{$page}"; $cacheFile = __DIR__ . "/../cache/{$cacheKey}.json"; // 缓存有效期5分钟 if ($page <= 3 && file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 300) { return json_decode(file_get_contents($cacheFile), true); } // 走正常查询逻辑 $result = $this->list(); // 调用之前的list方法 // 写入缓存 if ($page <= 3 && $result['code'] === 200) { if (!is_dir(dirname($cacheFile))) { mkdir(dirname($cacheFile), 0755, true); } file_put_contents($cacheFile, json_encode($result)); } return $result; }缓存只针对前3页,因为大部分用户不会翻到很后面。缓存时间5分钟,平衡了数据新鲜度和数据库压力。缓存文件放在cache目录,记得给写权限。
数据库层面,给videos表的category_id和status加联合索引:
ALTER TABLE videos ADD INDEX idx_category_status (category_id, status);这个索引能让列表查询的WHERE category_id = X AND status = 1走索引,速度提升明显。另外play_count字段如果用于排序,也可以加索引,但更新频繁的字段加索引会影响写入性能,需要权衡。
5.3 前端播放器预加载与体验优化
用户点开详情页才加载播放地址,会有明显等待感。可以在列表页就预加载第一个播放源的信息,点进去直接能播。
// pages/index/index.vue 列表页预加载 onLoad() { this.loadVideoList(); }, methods: { async loadVideoList() { const list = await api.getVideoList({ page: 1 }); this.videoList = list; // 预加载前3个视频的详情 const preloadCount = Math.min(3, list.length); for (let i = 0; i < preloadCount; i++) { this.preloadVideoDetail(list[i].id); } }, async preloadVideoDetail(videoId) { try { const detail = await api.getVideoDetail({ id: videoId }); // 存入缓存 uni.setStorageSync(`video_detail_${videoId}`, detail); } catch (e) { // 预加载失败不影响正常流程 } } }详情页onLoad时先读缓存,有缓存直接渲染,同时后台静默请求最新数据更新。这样用户感觉是秒开的。
// pages/detail/detail.vue onLoad(options) { const videoId = options.id; // 先读缓存 const cached = uni.getStorageSync(`video_detail_${videoId}`); if (cached) { this.videoData = cached; this.initPlayer(cached.sources[0].play_url); } // 再请求最新数据 this.fetchDetail(videoId); }这套组合拳下来,详情页的打开速度能提升不少。预加载的请求量不大,3个视频的详情接口并发请求,后端完全扛得住。
从那以后我每次拿到新的影视类源码,都会先跑一遍播放源检测,再给列表接口加缓存,最后把前端预加载配上。这三步做完,基本就能从“能跑”变成“好用”了。希望帮到你。
本文还有配套的精品资源,点击获取