简介:短视频去水印微信小程序带后台的完整工具源码,面向有 PHP/Laravel 基础的小程序开发者、个人站长或需要快速上线工具类应用的产品运营者。它提供前端解析与后端管理一体化方案:用户端可快速获取无水印视频,服务端基于 PHP 实现,支持限制解析次数、接入流量主等功能,既能防止接口被滥用,也能凭借广告位产生收益;后台设有丰富控制项,可灵活配置解析规则与用户权限。资源包共 171 个文件,压缩后约 955KB;其中 70 个 PHP 文件承担核心服务与接口逻辑,JS、JSON 配合 WXML、WXSS 完成小程序端功能与界面,PNG、JPG 为界面素材,附带 Laravel 入口与环境配置,部署前需安装 Composer,如使用 Laravel 框架还需确认 Swoole 扩展。目前已有 86 人学习下载,适合用于学习小程序与 Laravel 后台的联动开发,也可直接二次改造为商用去水印工具平台。
1. 为什么去水印小程序要把“限次”和“流量主”放到后端
短视频平台的分享链接,并不是拿到之后改一个 URL 参数就能稳定去掉水印的。更麻烦的是,微信小程序里直接对第三方视频域名发请求,既受request合法域名限制,又要面对平台的反爬校验。真正能长期跑的去水印工具,几乎都把“解析”和“限次”放在自己的服务器上:小程序只负责传回分享文案,后台去跟短视频平台交互,再把无水印播放地址交回前端。
把次数限制做在后端还有一个直接原因:如果只在客户端用wx.setStorage存一个剩余次数,用户清理缓存或者重装小程序就能绕过,流量主广告也基本形同虚设。这套源码在 Laravel 后台里通过 Redis 按openid + 日期计数,后台又能查看解析记录,小程序端再根据“观看广告解锁”的闭环把收益链路补上。适合想快速跑通工具站变现的 PHP 工程师,也适合手里有微信小程序账号、想低成本做一个带后台上限工具的人。
2. 源码目录与环境准备:Laravel、Composer、Swoole 先跑通
2.1 根目录哪些文件值得先看
拿到压缩包后先别急着打开一个前端 HTML,这不是纯静态页面项目。根目录里的artisan是 Laravel 的命令行入口,部署、迁移、队列调度都会用到它;web.config是 Windows Server + IIS 的重写规则文件,如果你用的是 Nginx,这个文件基本没用;.env.example是环境变量模板,最终要复制成.env才能跑;fswatch则是一个 macOS 下监听文件变更的辅助脚本,与去水印业务本身无关。
| 文件 / 目录 | 作用 | 部署注意点 |
|---|---|---|
artisan | Laravel CLI,执行迁移、缓存清理、队列 | 确保storage/目录可写 |
web.config | IIS 伪静态规则 | 换 Nginx 时要转换成 rewrite 规则 |
.env.example | 数据库、Redis、APP_KEY 等环境变量模板 | 复制成.env后逐个确认 |
.editorconfig | 编辑器代码风格统一 | 本地开发用,线上可忽略 |
fswatch | 文件变更监听脚本 | 不影响业务,误删不影响运行 |
真正要改的业务代码在app/Http/Controllers、routes/api.php和app/Services这类目录里。先确认composer.json里写了哪些依赖,再看routes/api.php里暴露了哪些接口,比乱翻页面文件要高效得多。
2.2 Composer 安装与环境变量配置
项目摘要里明确提到“使用前需要确认已安装 composer”,说明后端强依赖 Composer 而不是把 vendor 目录直接打成压缩包。在服务器上先把 PHP 和 Composer 装好,然后按顺序执行:
cd /var/www/video-tool cp .env.example .env composer install --no-dev --prefer-dist --optimize-autoloader php artisan key:generate php artisan migrate --seed命令的含义是按顺序做四件事:复制环境变量模板、安装 PHP 依赖、生成 Laravel 的APP_KEY、初始化数据库表。APP_KEY会写进.env,它影响 Session、加密和部分缓存逻辑,缺失时小程序登录和后台登录都会报错。
migrate --seed会写入初始管理员账号和配置项。如果源码里没有提供database/seeds或数据表文件,就不要强行执行这步,直接到后台安装向导里初始化。
2.3 Swoole 扩展:不是可选项,是隐藏依赖
摘要里已经提醒“如果用户使用 Laravel 框架,还需要确认是否已安装 swoole 扩展”。我在实际部署时遇到过一种情况:程序在普通 PHP-FPM 下能跑通首页,但解析接口一请求就 500,原因就是代码里用了Swoole\Coroutine\go做并发请求,而 PHP 环境根本没有加载 swoole 扩展。
先在服务器上检查:
php -v php -m | grep swoole pecl install swoole如果php -m里没有 swoole,先用pecl install swoole安装,然后在php.ini里加extension=swoole.so,重启 PHP 服务后再检查。常见的做法是配合hhxsv5/laravel-s这类包把 Laravel 变成常驻内存服务,部署时用 Nginx 反向代理到 1215 端口:
composer require hhxsv5/laravel-s php artisan laravels publish php artisan laravels start收到包含vendor目录的压缩包时,还要注意本地装的是 PHP 7.4 还是 8.x。swoole 版本对 PHP 版本有要求,装不上时优先检查是否用了过旧的 swoole 版本,再检查 PHP 是否开启了线程安全。Windows 本地开发可以先跳过 swoole,但 Linux 服务器上线前必须补齐。
3. 核心解析接口与防刷限次:从分享链接到无水印地址
3.1 解析原理:为什么不能在前端写死一个 API
短视频平台的水印地址并不是一个固定开关。常见的解析过程是:小程序把用户复制的整段分享文案提交给后端,后端从文案里提取短链接,跟随 302 跳转拿到真实页面,再从页面 HTML 或首包 JSON 里找到视频 ID 和播放地址,最后对播放地址做参数替换去掉水印。
| 平台类型 | 链接特征 | 常见去水印处理 |
|---|---|---|
| 部分短视频平台 | 短链域名 + 随机码 | 跟随重定向后解析页面 JSON |
| 部分平台 | playwm参数 | 替换成play或直接移除参数 |
| 部分平台 | 返回加密数据 | 需要调用平台自己的解析接口 |
这些规则单独放前端虽然也能实现,但小程序正式版必须配置合法域名,而平台接口域名通常无法提前预知。放到后端之后,域名变化、接口地址变更、UA 风控都只影响服务端,小程序端不用频繁发版。
3.2 Laravel 接口实现:校验、缓存、限次一次做完
在routes/api.php里注册一个解析接口:
Route::post('/v1/parse', [VideoParseController::class, 'resolve']);控制器里我这个版本会同时处理 URL 校验、结果缓存、Redis 限次和广告开关:
<?php namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Redis; class VideoParseController extends Controller { public function resolve(Request $request) { $url = trim($request->input('url')); if (!$this->isShortLink($url)) { return response()->json(['code' => 422, 'msg' => '链接格式不正确']); } $openid = $request->header('X-Openid', ''); if ($this->outOfLimit($openid)) { return response()->json(['code' => 403, 'msg' => '今日次数用完,需观看广告解锁']); } $cacheKey = 'video:url:' . md5($url); $cached = Redis::get($cacheKey); if ($cached) { return response()->json([ 'code' => 0, 'data' => json_decode($cached, true), 'need_ad' => $this->shouldShowAd($openid), ]); } $result = $this->fetchNoWatermarkUrl($url); if ($result['status'] !== 0) { return response()->json(['code' => 500, 'msg' => '解析失败,请换一段分享文案再试']); } Redis::setex($cacheKey, 43200, json_encode($result['data'], JSON_UNESCAPED_UNICODE)); $this->incrLimit($openid); return response()->json([ 'code' => 0, 'data' => $result['data'], 'need_ad' => $this->shouldShowAd($openid), ]); } }这段代码的关键点有三个:先查 Redis 缓存,同一链接二次请求不重复解析也不重复扣次数;outOfLimit负责判断当天剩余次数;need_ad决定小程序端要不要弹出流量主广告。注意X-Openid只是开发阶段的临时方案,正式环境必须用wx.login的 code 换 openid 后存在服务端 Session 里,不能完全信任请求头。
限次写入部分我拆成了两个私有方法:
private function outOfLimit(string $openid): bool { $key = 'parse:limit:' . $openid . ':' . now()->toDateString(); $count = (int) Redis::get($key); return $count >= config('parse.max_daily_times'); } private function incrLimit(string $openid): void { $key = 'parse:limit:' . $openid . ':' . now()->toDateString(); Redis::pipeline(function ($pipe) use ($key) { $pipe->incr($key); $pipe->expire($key, 86400); }); }Redis key 里拼上日期,天然支持每天重置;用pipeline保证incr和expire是连续操作,避免一个请求把 key 的过期时间一直往后推。这里如果不用 Redis,也可以用数据表记录,但高并发下 Redis 的原子性更好,也不会拖慢数据库.
3.3 配置化策略:频率、超时、平台适配
把限次、超时、广告展示频率放到配置文件里,比在控制器里写死更便于改后台。我一般会在config/parse.php里这样设计:
| 配置项 | 含义 | 建议值 |
|---|---|---|
max_daily_times | 每人每天免费解析次数 | 3 |
request_timeout | 请求平台接口超时时间 | 3 秒 |
cache_expire | 同一视频地址缓存时间 | 43200 秒 |
ad_frequency | 每解析几次触发广告 | 3 |
对平台解析器,我会建议把每个平台拆成独立类。比如app/Services/Adapters/DouyinAdapter.php只负责处理抖音,KuaishouAdapter.php只负责处理快手。这样平台返回结构一改,只需要改对应 Adapter,不需要动控制器和 Redis 限次逻辑。
4. 小程序端对接、加载页改造与流量主广告位设置
4.1 请求封装和进入加载页的 token 检查
小程序端如果默认请求后台地址,第一次进入时会先请求一个接口或显示一个加载页。很多下载包里的加载页会把后台域名暴露出来,这对运营者来说不是好事。我一般会在pages/index/index.js的onLoad里先检查本地 token,而不是请求后台:
const app = getApp() Page({ data: { videoUrl: '', loading: false }, onLoad() { const token = wx.getStorageSync('mp_token') if (token) { app.globalData.token = token } }, parseVideo(shareUrl) { if (!shareUrl) { wx.showToast({ title: '请先复制分享链接', icon: 'none' }) return } this.setData({ loading: true }) wx.request({ url: `${app.globalData.baseUrl}/api/v1/parse`, method: 'POST', header: { 'Content-Type': 'application/json', 'X-Openid': app.globalData.openid || '' }, data: { url: shareUrl }, success: (res) => { if (res.data.code === 0) { this.setData({ videoUrl: res.data.data.play_url }) } else if (res.data.code === 403) { this.showRewardedAd() } else { wx.showToast({ title: res.data.msg, icon: 'none' }) } }, fail: () => { wx.showToast({ title: '网络异常,请检查请求域名', icon: 'none' }) }, complete: () => { this.setData({ loading: false }) } }) } })注意wx.request的success只代表 HTTP 请求返回,真正的业务状态要看res.data.code。后端返回403时不弹“系统繁忙”,而是直接走广告解锁逻辑,这样用户知道下一步该怎么办。
4.2 剩余次数与“看广告解锁”的联动
流量主广告位申请下来之后,后端返回的need_ad字段才真正有意义。如果每次成功解析都弹广告,用户很快会流失;如果完全不弹,流量主收益又为零。常见做法是后端按ad_frequency配置,比如每满 3 次才要求看一次广告。
小程序端用wx.createRewardedVideoAd创建激励视频广告实例:
showRewardedAd() { const ad = wx.createRewardedVideoAd({ adUnitId: 'adunit-xxxxxxxxxxxxx' }) ad.onClose((res) => { if (res && res.isEnded) { this.parseVideo(this.data.shareUrl) } else { wx.showToast({ title: '看完广告才能解锁', icon: 'none' }) } }) ad.show().catch(() => { ad.load().then(() => ad.show()) }) }onClose里的res.isEnded用来判断用户是否完整看完广告。很多刷量方案会提前关闭广告,这里如果判断不严,就会变成用户拿到解析结果但平台不结算广告收益。广告位 ID 不要写在页面里,建议放在app.js的globalData或后台配置中,小程序发版后还能远程改。
如果二开把这套源码转成 uni-app,原生wx.createRewardedVideoAd要换成uni.createRewardedVideoAd,否则在 HBuilderX 打包时会遇到兼容问题。
4.3 后台管理:解析日志、次数阈值和广告开关
带后台工具意味着不只是前端能解析,还需要一个管理界面看谁在解析、解析失败多不多。拿到源码后先看routes/web.php里有没有admin前缀:
Route::prefix('admin')->middleware('auth')->group(function () { Route::get('logs', [AdminController::class, 'logs']); Route::post('config', [AdminController::class, 'updateConfig']); });后台至少需要三块信息:当日解析总数、按 openid 排序的解析记录、限次配置。把max_daily_times和ad_frequency做成表单写入缓存,比直接改配置文件的体验好很多。解析日志建议单独建一张表,不要把每条日志都直接写进 Laravel 默认的logs/文件,否则后台筛选和统计会非常痛苦。
| 后台菜单 | 数据来源 | 运营价值 |
|---|---|---|
| 今日解析趋势 | 解析记录表按小时聚合 | 判断高峰,决定是否扩容 |
| 用户解析排行 | openid 分组统计 | 识别异常刷量 |
| 失败链接列表 | 记录 status != 0 的请求 | 快速发现平台接口失效 |
| 限次配置 | Redis 或数据库 | 修改免费次数和广告频率 |
5. 上线前验证:小程序抓包、次数绕过与解析失败兜底
5.1 用 Charles/mitmproxy 验证真实请求
小程序开发版可以勾选“不校验合法域名”,但正式版必须把接口域名配置到微信公众平台的request 合法域名里,而且域名不能是 IP 或带端口。上线前最好用 Charles 或 mitmproxy 抓包确认请求体,而不是只在小程序开发者工具里看 preview。
先在电脑上模拟一次解析请求:
curl -X POST https://api.yourdomain.com/api/v1/parse \ -H "Content-Type: application/json" \ -H "X-Openid: test_openid" \ -d '{"url":"https://v.douyin.com/xxxx/"}'如果返回code: 0且data.play_url是 http 链接,要注意小程序正式版不允许请求不带 HTTPS 的播放地址。部分解析接口返回的是 HTTP 链接,需要在后端统一替换成 HTTPS,或者由服务器做一次跳转代理。
5.2 限次绕过测试:查 Redis 而不是查前端提示
前端提示“今日次数用完”不一定说明限次真正生效。把同一个 openid 连续请求 3 次以上,再去 Redis 里看 key:
redis-cli keys "parse:limit:*" redis-cli ttl "parse:limit:test_openid:2026-01-01"ttl应该是接近 86400 的剩余秒数。如果 key 不存在,说明incrLimit没有被调用;如果 key 存在但值超过max_daily_times,检查是不是每次请求都走缓存命中提前返回,这时应该给缓存命中单独设置计数规则,而不是完全不扣次数。
更隐蔽的问题是 Nginx 把用户 IP 透传给 Laravel 时,小程序里拿到的用户身份不可靠。不要用 IP 作为唯一限次依据,手机切到 4G 换个 IP 就失效了。按 openid 限次是基本操作,如果担心伪造,再叠加一个微信登录的 session_token 校验。
5.3 解析失败兜底:看 parse 日志再改适配器
短视频平台改版时,最典型的错误是返回 200 但 JSON 结构里找不到视频 ID。这类问题不是前端能处理好的,必须让后端把原始返回记录下来。我会在fetchNoWatermarkUrl里单独写一条日志通道:
catch (\Throwable $e) { \Illuminate\Support\Facades\Log::channel('parse')->error($e->getMessage(), [ 'url' => $url, 'openid' => $openid, 'trace' => $e->getTraceAsString(), ]); }排查时直接看日志文件:
tail -f storage/logs/parse.log先确认到底是网络超时、平台返回结构变化,还是小黑屋和风控,再决定是调整 UA、增加代理还是更新 Adapter。把每个平台解析器单独拆类之后,平台规则变化时只改一个类文件,后端控制器和广告逻辑都不用动,这也是这套源码后期维护最重要的收益点。
本文还有配套的精品资源,点击获取