尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PHP有声听书系统源码架构与部署:从分集管理到防盗链

PHP有声听书系统源码架构与部署:从分集管理到防盗链 简介PHP有声听书系统完整源码是一套基于PHP开发的在线听书平台项目面向有一定Web基础、希望了解整站开发流程的PHP学习者和开发者。压缩包含1213个文件以338个PHP核心逻辑文件、210个HTML页面、108个JS脚本及32个CSS样式为主同时提供4个SQL数据库文件和nginx.conf、.htaccess等URL重写配置整体仅8.36MB部署轻量。系统涵盖用户注册登录、有声书检索、播放控制、听书历史等常见功能涉及MySQL数据表设计、前端交互、音频流处理与URL重写等关键技术点。包内还有404个GIF演示图、PNG/JPG图片和SWF组件便于查看页面效果并辅助理解业务模块目录中nginx.conf、jquery-ui等文件结构清晰适合课程设计、毕业项目或个人练手。目前已有146人学习下载可帮助读者快速掌握从后端接口到前端播放器的完整实现思路。1. PHP有声听书系统源码到底解决了什么问题做网站的人几乎都被问过这么一句有没有一套PHP有声听书系统源码问的人通常带着两种期待要么是想快速搭一个音频内容站把小说、讲座、课程录成音频后批量上架要么是想基于源码二次开发做成会员制产品。市面上的答案两极分化一类是ThinkPHP写的老后台配一个呆板播放页另一类是把WordPress套了层皮就敢叫“完整源码”。真正的有声听书系统核心不是上传和列表而是分集管理、音频流输出、播放进度记录和防盗链这一整套链路。这套链路做扎实了源码才有资格叫“完整”。这篇顺着PHP项目的实际架构把有声听书系统从表结构到部署再到防盗链的细节拆开讲适合手里已经拿到源码但不知道怎么改、怎么部署的人。2. 源码架构与数据表设计先把PHP项目的骨架看清楚2.1 目录结构与技术选型怎么判断一套源码的成色拿到任何一份PHP源码我先不看功能列表先看目录。有声听书系统和普通内容管理系统最大的区别在于多了一个“音频资源层”所以目录里至少应该区分出application或app、public、runtime、upload这四块。application下面按模块拆成admin、api、index三个目录分别对应后台管理、移动端接口和前台页面。upload目录单独放音频文件权限要单独设置不能跟着PHP一起被解析。技术选型上市面上流传的有声听书系统源码大多是ThinkPHP 5.x或ThinkPHP 6.x写的也有Laravel和原生PHP混写的。ThinkPHP出现频率最高原因很实际模板标签上手快、文档全、虚拟主机部署简单。Laravel的源码通常更规范但部署门槛高很多老服务器PHP版本不够。原生PHP的源码要特别小心因为“完整”两个字在原生项目里经常意味着没有框架、没有命名空间、SQL全部裸写后续改起来非常痛苦。我判断源码成色的第二个标准是看有没有独立的API模块。有声听书系统最常见的落地形态是App或小程序后端提供JSON接口前端播放器只认接口。如果源码里只有index前台页面没有api模块那它就只能跑在浏览器里谈不上“系统”。下面这个目录结构是常见的有声听书系统的标准布局php-audio-book/ ├── application/ │ ├── admin/ # 后台管理 │ │ ├── controller/ │ │ └── view/ │ ├── api/ # 接口层给小程序/App用 │ │ └── controller/ │ └── index/ # 前台页面 ├── public/ │ ├── index.php # 入口文件 │ └── static/ # 静态资源 ├── upload/ │ └── audio/ # 音频文件按日期分目录 ├── runtime/ # 缓存和日志 └── thinkphp/ # 框架核心这套结构的好处是接口、后台、前台物理隔离互不干扰。api模块只输出JSONadmin模块只处理后台操作index只负责渲染页面。音频上传到upload目录后不走PHP解析由Nginx直接处理静态请求这样能减少PHP进程的压力。如果你手里的源码没有api目录那它大概率只是“有声书前台”不是“系统”。2.2 核心数据表专辑、分集、用户、订单之间的关系有声听书系统的表结构比一般CMS多出两个关键东西专辑album和分集episode。文章模型只有一个content字段听书系统要的是“一本书对应多个音频文件”每个音频文件还要有章节号、时长、大小、播放次数这些属性。推荐的建表语句如下CREATE TABLE episode ( id int(11) NOT NULL AUTO_INCREMENT, album_id int(11) NOT NULL COMMENT 专辑ID, title varchar(255) NOT NULL COMMENT 分集标题, audio_url varchar(500) NOT NULL COMMENT 音频文件地址, duration int(11) DEFAULT 0 COMMENT 音频时长单位秒, file_size int(11) DEFAULT 0 COMMENT 文件大小单位字节, sort int(11) DEFAULT 0 COMMENT 排序权重越小越靠前, play_count int(11) DEFAULT 0 COMMENT 播放次数, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, create_time int(11) DEFAULT NULL, PRIMARY KEY (id), KEY idx_album_sort (album_id,sort) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分集表;这张表是整个系统的主动脉。album_id关联专辑sort控制分集顺序duration和file_size在音频上传时通过PHP读取并写入。audio_url不直接存完整路径只存upload/audio/2025/06/xxx.mp3这种相对路径好处是以后换域名或换存储方式不用改数据。从查询角度看idx_album_sort这个联合索引要让播放器“按专辑取分集列表”的请求直接命中索引避免文件排序。专辑表album包含标题、封面、作者、简介、分类ID、是否付费这些字段user表负责会员order表记录购买记录。常见的关联方式是episode.album_id指向album.idalbum.status控制整本书是否可见分集表再单独控制每一集的上下架。有几个用处一是做单集试听整本书下架时已购用户还能听已购买的集数二是在小说更新连载时新集先下架审核审核通过再上架。音频版权的问题很多源码是忽略的。若要做付费听书order表里至少要有album_id、user_id、amount、pay_type、trade_no、status这几个字段若做会员制则需要在user表加expire_time。这两条路不冲突通常的做法是会员免费听全站非会员单集购买。3. 分集管理与播放器对接PHP有声听书系统的核心链路3.1 音频上传与分集的PHP实现格式、大小、路径怎么控制音频上传是听书系统里最容易出问题的环节。前端交上来的文件可能是MP3、M4A、AAC也可能是伪装成MP3的恶意脚本。PHP端第一步要做的不是接文件而是校验。下面这段代码是典型的administrator上传处理逻辑public function upload() { $file $this-request-file(audio); if (!$file) { return json([code 1, msg 请选择音频文件]); } $allowExt [mp3, m4a, aac, wav, ogg]; $ext strtolower($file-getExtension()); // 1. 校验后缀 if (!in_array($ext, $allowExt)) { return json([code 1, msg 不支持的音频格式]); } // 2. 校验MIME类型防止脚本文件伪装 $mime $file-getMime(); if (strpos($mime, audio/) ! 0) { return json([code 1, msg 文件类型不合法]); } // 3. 校验大小单文件不超过100MB if ($file-getSize() 100 * 1024 * 1024) { return json([code 1, msg 音频文件不能超过100MB]); } // 4. 按日期分目录存储文件名用随机串 $savePath audio/ . date(Y/m/d); $newName md5(uniqid() . $file-getInfo(name)) . . . $ext; $file-move(ROOT_PATH . public/uploads/ . $savePath, $newName); if ($file-getError()) { return json([code 1, msg $file-getError()]); } // 5. 存入分集表 $data [ album_id input(post.album_id), title input(post.title), audio_url /uploads/ . $savePath . / . $newName, file_size $file-getSize(), sort input(post.sort, 0), create_time time() ]; Db::name(episode)-insert($data); return json([code 0, msg 上传成功]); }这段代码的逻辑重点在后缀校验之上叠加了MIME校验audio开头才放行文件名用md5(uniqid())重写杜绝中文文件名带来的URL编码问题存储目录按年/月/日拆分避免单个目录文件过多。第5步写入数据库时duration字段还没有值需要后续处理。这里Upload类的move方法是ThinkPHP自带的如果是原生PHP对应的就是move_uploaded_file()校验逻辑一致只是文件移动方式不同。upload_max_filesize和post_max_size如果小于100MB上面这段代码还没走到校验就被PHP拦了。这是部署问题第四章会讲php.ini里的对应参数。3.2 输出分集列表的JSON接口给小程序和App用后台传完音频前端播放器怎么拿分集列表常见的做法是提供一个接口返回按照专辑分组的分集数据。这段代码放在api模块里供App和小程序调用public function getEpisodes() { $albumId (int) input(get.album_id); $page max(1, (int) input(get.page, 1)); $size min(50, (int) input(get.size, 20)); if (!$albumId) { return json([code 1, msg album_id不能为空]); } $list Db::name(episode) -where(album_id, $albumId) -where(status, 1) -order(sort ASC, id ASC) -page($page, $size) -field(id,title,audio_url,duration,file_size,play_count) -select(); $total Db::name(episode) -where(album_id, $albumId) -where(status, 1) -count(); return json([ code 0, data [ list $list, total $total, page $page, size $size ] ]); }接口的排序用sort ASC, id ASC保证手动拖过的顺序优先同一排序权重下按ID递增。page和size做分页控制size最大50防止一次拉全量数据把移动端流量烧光。field字段里故意不放create_time和status接口少返回无用字段减少传输体积。这套接口设计有一个容易被忽略的点audio_url返回的是相对路径App端拿到后要自己拼域名。更好的做法是在返回前做一个拼接例如foreach ($list as $key $item) { $list[$key][audio_url] https:// . $_SERVER[HTTP_HOST] . $item[audio_url]; }拼接时不推荐用$_SERVER[HTTP_HOST]因为如果后台通过IP访问过HTTP_HOST会留下缓存要么在配置文件里配一个audio_domain常量要么在Nginx层做路径重写把相对路径交给前端去拼。3.3 前端播放器如何对接PHP接口跨域、自动播放、续播接口写好后浏览器端用audio标签就能播放。这里要解决三件事跨域、自动播放策略、续播。下面这段JavaScript放在前台页面先请求接口拿到分集列表再实例化audioconst audio document.getElementById(player); let episodeList []; let currentIndex 0; async function loadEpisodes(albumId) { const res await fetch(/api/getEpisodes?album_id${albumId}); const json await res.json(); if (json.code 0) { episodeList json.data.list; playEpisode(0); } } function playEpisode(index) { if (index 0 || index episodeList.length) return; currentIndex index; const ep episodeList[index]; audio.src ep.audio_url; audio.play().catch(() { // 浏览器自动播放策略拦截需用户点击后才能播放 document.getElementById(playBtn).style.display block; }); // 记录播放进度 saveProgress(ep.id, 0); }与PHP直出页面不同跨域问题时若接口域名是api.example.com页面域名是www.example.com需要在接口响应头加上Access-Control-Allow-Origin。ThinkPHP的做法是在api模块的初始化方法里写header(Access-Control-Allow-Origin: . $_SERVER[HTTP_ORIGIN]); header(Access-Control-Allow-Credentials: true); header(Access-Control-Allow-Methods: GET, POST, OPTIONS);自动播放是另一个重灾区。移动端浏览器普遍不允许带声音的自动播放必须等用户触摸。做法是监测play()返回的Promise如果被reject就显示一个遮罩按钮用户点击后再播放。续播功能则需要后端记录播放位置。3.4 播放进度记录断点续播的数据库方案续播听起来简单做起来有不少细节。客户端每隔5到10秒上报一次当前播放位置服务端只保留每个用户每集的最新进度。表结构用user_id加episode_id做联合唯一键上报时直接更新public function saveProgress() { $userId (int) input(post.user_id); $episodeId (int) input(post.episode_id); $position (int) input(post.position); $duration (int) input(post.duration); if (!$userId || !$episodeId || $position 0) { return json([code 1, msg 参数错误]); } Db::execute( INSERT INTO play_progress (user_id, episode_id, position, duration, update_time) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE position VALUES(position), duration VALUES(duration), update_time VALUES(update_time), [$userId, $episodeId, $position, $duration, time()] ); return json([code 0, msg ok]); }这里用INSERT加ON DUPLICATE KEY UPDATE代替先查再改是为了减少一次查询。play_progress表需要给user_id和episode_id建联合唯一索引否则这个SQL会一直插入重复记录。position存的是秒数duration是音频总时长这两个字段在“重新开始播放”按钮里会用到当position离duration不到3秒时前端自动跳转到下一集当用户手动点击进度条后端按最新值覆盖。进度上报的频率要克制播放中每10秒一次就够了timeupdate事件触发太频繁一次听一节40分钟的音频会产生240多次请求对服务器压力不小。另外一个细节是如果后端要在后台展示“听到第几集”需要在episode表里额外存一个last_episode_id字段每次上报时更新。展示层做联表查询把续播信息带到首页推荐位这是很多源码没做的功能但对用户留存有明显的帮助。4. 部署到LNMP宝塔、Nginx伪静态与PHP参数的坑4.1 部署前的PHP扩展与版本核对在宝塔面板部署PHP有声听书系统源码第一步不是建站而是先核对PHP版本和扩展。ThinkPHP 6要求PHP 7.2.5以上实际生产环境我推荐用PHP 7.4或PHP 8.0兼容性和性能平衡得比较好。有些老源码跑在PHP 5.6上装到PHP 8会直接白屏因为很多PHP 5时代的写法比如mysql_*开头的函数在PHP 7里已经删除。那类源码在PHP 8里不是改一行配置能解决的建议直接拒绝。部署前用phpinfo()或命令行确认以下扩展都在扩展名用途缺失时的表现fileinfo上传文件的MIME类型检测上传接口直接报错curl调用第三方支付、短信接口支付模块白屏pdo_mysql数据库连接首页白屏、数据库错误openssl微信支付回调验签支付回调失败gd图片缩略、验证码生成后台验证码不显示exif读取音频元信息时长探测失败宝塔面板在PHP设置页可以一键安装扩展安装完重启PHP-FPM生效。第3章的upload()函数里调用了$file-getMime()fileinfo缺了这个方法就直接报“Call to undefined function”这个错最常见也最好排查。4.2 Nginx伪静态与音频文件直出ThinkPHP和Laravel都需要伪静态配置否则访问首页会报404。在宝塔站点设置里伪静态选thinkphp模板即可手动配置如下location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~* ^/uploads/.*\.(mp3|m4a|aac|wav|ogg)$ { expires 30d; add_header Cache-Control public; access_log off; }第一段是把所有不存在的文件路径重写到index.php由ThinkPHP路由解析。第二段是音频文件的直出规则upload路径下的音频文件不经过PHP由Nginx直接返回文件配合expires设置30天浏览器缓存能显著降低PHP压力。需要注意第二段location必须写在第一段之后Nginx会优先匹配更具体的规则否则可能被rewrite规则截走。音频直出之后播放器加载速度会快很多但也要做防盗链否则别人把你音频地址贴到任何地方都能播。防盗链的做法下一章细说这里只提一点Nginx的valid_referers可以拦截大部分盗链但对付不了用程序直接下载的人要真正限制播放权限得靠签名URL。4.3 php.ini关键参数与上传限制大部分有声听书系统源码在“上传失败”这个问题上责任都在PHP配置而不是代码。下面是php.ini里必须检查的参数参数名推荐值说明upload_max_filesize100M单文件最大上传体积post_max_size120MPOST数据限制必须大于upload_max_filesizemax_execution_time300大文件上传处理时间memory_limit256M内存上限处理音频元信息时不能太小max_file_uploads20单次上传文件个数post_max_size必须大于upload_max_filesize否则文件上传会被截断且不报错后台显示“上传失败”却看不到具体原因。max_execution_time在读取大音频文件时长时也非常关键getID3之类的库要读完整个文件头慢的服务器上300秒都未必够。如果上传目录已经放入open_basedir限制还要确认upload目录在允许路径内不然move()会报权限错误。部署在宝塔上直接在网站目录的“防跨站攻击”设置里放行uploads目录即可。4.4 高频报错与对应处理方案部署过程中有几个高频报错属于见过一次以后就能秒杀的级别PHP 8下报fatal error: directive track_errors is no longer available in PHP出自php.ini的track_errors配置PHP 7.4之后这个指令被移除。直接把php.ini里的track_errors On注释掉重启PHP-FPM就好。验证码不显示后台登录页验证码是空白的多半是gd扩展没装或字体文件路径不对。先在phpinfo里确认gd已启用再去application/admin/conf里检查字体路径是否写死。音频播放卡顿或无法拖动进度条如果音频文件没走Nginx直出而是被PHP读取后转发会导致不支持Range请求播放器无法拖进度。解决方法是确认Nginx配置里没有把uploads目录交给PHP处理。JSON接口返回HTML接口路径被伪静态规则错误地重写到了index控制器。检查路由规则api模块需要单独放行。这些报错在PHP源码部署里几乎人人都会遇到先看运行日志再看配置不要一上来就改代码。Nginx的error.log和PHP的慢日志记录得足够详细定位也很快。5. 源码到手后更值得做的几个进阶改造5.1 给audio_url加签名防盗链前面的Nginx valid_referers只能挡浏览器盗链挡不住下载工具。PHP侧更可控的做法是签名URL播放接口返回的不是原始音频地址而是带过期时间戳和hash的地址。Nginx通过location规则校验hash不合法直接403。签名生成代码可以写在API模块里private function signUrl($path, $expire 3600) { $secret your-sign-secret; $expireTime time() $expire; $sign md5($expireTime . $path . $secret); return /audio/{$expireTime}/{$sign}{$path}; }返回给播放器的audio_url变成/audio/1750000000/9f8c5a2b1d/upload/audio/2025/06/xxx.mp3Nginx端需要这样校验location ~ ^/audio/([0-9])/([a-z0-9]{32})/(.*)$ { set $expire $1; set $sign $2; set $path /$3; if ($expire $time_iso8601) { return 403; } if ($sign ! md5($expire . $path . your-sign-secret)) { return 403; } alias /data/www/your-site/public$path; }这样改造后音频地址只在指定时间内有效过期后播放器拿到的403就直接加载失败。secret只存在服务器端前端看不到也没有验证绕过的可能。5.2 上传时同步读取音频时长episode表的duration字段一直空着播放器显示不了总时长。常见的做法是用getID3库在上传时读取require_once /path/to/getID3/getid3/getid3.php; $getID3 new getID3; $audioInfo $getID3-analyze($file-getInfo(tmp_name)); $duration isset($audioInfo[playtime_seconds]) ? (int) $audioInfo[playtime_seconds] : 0;把读到的playtime_seconds写入episode表即可。getID3对MP3、M4A、OGG的解析都很稳定唯一的坑是它需要读完整个音频文件头大文件耗时明显建议放到队列里异步处理PHP可以用redis加一个简单的队列进程处理。5.3 把单站源码改成可配置的多站点版本如果手里这套源码要交付给不同客户使用一定不要把站点名称、版权信息、客服联系方式写死在模板里。在config里加一个site配置项模板里统一用{$site.name}、{$site.icp}这种变量输出。第一次部署时跑一个安装脚本把这些配置写入数据库后台可改一劳永逸。这样做的收益不止是省事而是真正把“源码”变成“产品”。有声听书系统的竞争力不在代码量而在后续的运营支持音频批量上传、封面自动缩略、播放数据统计这些功能是客户真正愿意付费的点。把基础链路跑通、把签名防盗链和时长探测做好这套PHP源码才算真正值得交付。本文还有配套的精品资源点击获取
返回列表