
简介2025年最新维护的去水印小程序源码面向小程序开发者与个人站长提供免服务器即可运行的视频与图集去水印方案支持灵活对接第三方去水印接口并内置微信流量主广告位以实现广告变现。资源包共144个文件核心包括js逻辑脚本、json全局配置、wxml与wxss页面结构样式、png图标素材等压缩包仅1.17MB轻量无加密便于快速部署和二次开发。目前已有206人学习下载适合个人开发者或想利用流量主变现的运营者参考。从版本更新记录来看功能已覆盖视频解析、图集解析、激励与插屏广告全局配置、每日广告次数限制、下载中转站、无水印视频封面下载与文案复制并支持自定义下载合法域名与微信客服设置资源还附带安装教程开发者替换接口或对接自有接口后即可上线也可作为学习小程序广告变现与解析逻辑的参考源码。1. 免服务器跑通解析链路这个去水印小程序的取舍逻辑在 2025 年这个节点去水印小程序早就不是调一个接口那么简单的事。拿这套 V1.5.1 源码来说它最值钱的地方不是解析速度而是把免服务器、流量主、接口可替换三条命脉拧到了一起。你不需要自己买一台 ECS也不用维护接口服务只需要改一个接口地址剩下的逻辑全在前端跑完。适合谁适合手里已经有解析接口资源但不想再为服务端运维花时间的人也适合想用流量主收入覆盖 API 成本的个人开发者。拆开看会发现这个项目的架构思路非常直白uniapp 负责多端编译接口适配层负责屏蔽不同解析服务商的差异广告层则把流量主规则磨平成了几个可配置开关。我理解下来它就是一个典型的配置驱动 运行时兜底的轻量级小程序样板。2. uniapp 全前端链路与去水印接口适配层设计2.1 为什么选 uniapp 而不是原生小程序这个源码能免服务器跑起来的前提是整套逻辑都放在了小程序前端而后端只剩一个第三方解析接口。用 uniapp 的价值在于微信、抖音、百度小程序的基础库差异被框架尽量拉平了uni.request、uni.downloadFile这类 API 在不同端上都有对应实现。项目里几乎没碰平台私有 API所以把manifest.json里的 AppID 一换理论上就能发布到不同平台。选 uniapp 还有一个现实考量小程序的代码包体积限制在 2MB 以内原生写法如果图片资源、组件库压得不干净很容易超限。uniapp 的 tree-shaking 相对友好这个项目跑下来的代码包大约在 300KB 左右余量很大。不过要注意去水印功能依赖的网络请求、文件下载、广告组件这几类 API 在 uniapp 里多数是 Promise 化或回调化混用的源码里用了回调写法如果你想用async/await做统一收口建议在适配层包一层 Promise后面第 2.2 节会说具体怎么接。2.2 接口适配层的核心换接口不换代码V1.0 版本完成的就是解析功能和去水印接口对接。所谓替换去水印接口即可使用落在代码上其实是做了两层抽象第一层把不同服务商的字段名映射到项目内部统一的result结构上。第二层对可能缺失的封面图、图集列表做默认值兜底避免解析成功但渲染报错。// utils/parser.js 去水印接口适配层 const FIELD_ALIAS { videoUrl: [video_url, play_addr, no_watermark_url, url], cover: [cover, video_cover, thumbnail], images: [images, image_list, photos], desc: [desc, caption, title] }; function normalizeResponse(raw) { const data raw.data || raw; const result { type: video, videoUrl: , cover: , desc: , images: [] }; // 视频字段优先取第一个命中的字段 for (const key of FIELD_ALIAS.videoUrl) { if (data[key]) { result.videoUrl data[key]; break; } } // 封面同上 for (const key of FIELD_ALIAS.cover) { if (data[key]) { result.cover data[key]; break; } } // 图集判断只要 images 类是数组就切换类型 for (const key of FIELD_ALIAS.images) { if (Array.isArray(data[key]) data[key].length 0) { result.type images; result.images data[key]; break; } } result.desc data.desc || data.caption || ; return result; } export function parseVideo(shareUrl) { return new Promise((resolve, reject) { uni.request({ url: https://your-api.example.com/api/parse, method: POST, data: { url: shareUrl }, success: (res) { // 部分接口成功时返回 code0部分直接返回 data if (res.statusCode 200 (res.data.code 0 || res.data.data)) { resolve(normalizeResponse(res.data)); } else { reject(new Error(res.data.msg || 解析失败)); } }, fail: reject }); }); }这段代码要解决的是双方约定问题服务商返回的字段叫play_addr你前端存的却是videoUrl一旦换服务商不能把这个映射散落到每个页面里改。normalizeResponse把所有可能性收敛到一个函数内以后换接口只需要改FIELD_ALIAS的字段名即可。参数说明请求体建议带url字段部分老接口还要带source或type以区分抖音、快手等来源。Promise化的好处是可以搭配Promise.race做接口超时控制比如 8 秒没返回就提示用户换个分享口令再试。还有一个细节res.data.data的可能性很大有些服务商接口异常时会返回{ code: 500, data: {} }所以先判断code再判断data是否存在顺序反了会出现空对象里的video_url永远取不到的情况。2.3 视频与图集的解析差异处理视频和图集的处理逻辑天然不同视频走downloadFile拿临时文件再saveVideoToPhotosAlbum图集要遍历图片数组逐个下载保存。这套源码在 V1.0 里就把这两条链路分开实现了页面里通过result.type做判断。图集保存的性能优化有一点值得说明如果一次性循环 10 张图片分别调uni.saveImageToPhotosAlbum微信会频繁弹授权确认用户体验很差。实际项目里通常会先uni.authorize一次性申请相册权限再循环保存。async function saveImagesToAlbum(imageList) { // 先统一申请权限避免每张图都触发授权弹窗 await new Promise((resolve, reject) { uni.authorize({ scope: scope.writePhotosAlbum, success: resolve, fail: reject }); }); let successCount 0; for (let i 0; i imageList.length; i) { try { await new Promise((resolve, reject) { uni.saveImageToPhotosAlbum({ filePath: imageList[i], success: resolve, fail: reject }); }); successCount; } catch (e) { console.warn(第${i 1}张保存失败, e.errMsg); } } return successCount; }这段实现有几个参数值得你留意scope.writePhotosAlbum是微信相册权限的固定 scope 名没有授权过的用户第一次调用saveImageToPhotosAlbum会自动弹窗但拒绝过之后再调用就不会再弹必须引导去uni.openSetting手动打开。successCount的设计很实用返回给前端可以提示已保存 8/10 张而不是失败的 2 张让用户以为整个任务都失败了。3. 流量主接入广告位类型、触发策略与全局开关3.1 流量主开通条件与广告位创建流程广告收入是这个免服务器项目最重要的回血渠道。微信流量主的开通条件在 2025 年依然是累计独立访客不低于 1000 且无违规记录去水印类目因为被判定为视频工具的边界模糊区审核时被要求补充隐私保护声明的概率较大后面第 5 章会细说。开通流量主后需要在「公总台 → 流量主 → 广告位管理」分别创建激励视频、插屏、Banner 广告位。每个广告位会生成形如adunit-xxxxxxxxxxxxxxxx的 ID源码里把这些 ID 集中放到了一个配置对象中// config/ad.js 流量主广告位全局配置 export const AD_CONFIG { rewarded: { parse: adunit-xxxx1, download: adunit-xxxx2, dailyLimit: 10, downloadDailyLimit: 8 }, interstitial: { enabled: true, triggerPage: detail } };这个配置对象对应 V1.1 版本里最重要的更新流量主广告位全局配置。它把广告位 ID 和业务解耦后续想替换广告位只需要改这一个文件不需要翻页面代码。表格可以更直观地说明各广告位的接入位置广告位类型广告位 ID 字段触发时机源码里的控制开关激励视频-解析rewarded.parse点击提取视频按钮后触发showRewardedAd前校验开关激励视频-下载rewarded.download点击下载到相册后触发downloadDailyLimit控制频次插屏广告interstitial从详情页返回首页时展示enabled为 false 时不创建实例Banner不推荐列表页底部常驻源码未内置可自行扩展不建议在图集保存成功的回调里直接挂广告那是一个用户完成率最高的瞬间弹广告会影响复访率。3.2 激励视频广告的封装与观看确认激励视频是流量主收入的大头。源码里写了一个统一的激励视频入口每次调用都会检查广告位 ID 是否存在以及用户当天是否还能触发激励视频。// utils/ads.js 激励视频广告统一入口 function showRewardedAd(adUnitId, onReward) { if (!adUnitId) { console.warn(广告位 ID 未配置直接放行); onReward(true); return; } const videoAd uni.createRewardedVideoAd({ adUnitId }); videoAd.onLoad(() { console.log(激励视频加载成功); }); videoAd.onError((err) { console.error(激励视频拉取失败, err.errMsg); // 广告降级拉取失败直接通过避免阻塞主流程 onReward(true); }); videoAd.onClose((res) { if (res res.isEnded) { onReward(true); } else { uni.showToast({ title: 看完广告再操作哦, icon: none }); onReward(false); } }); videoAd.show().catch(() { videoAd.load().then(() videoAd.show()).catch(() { onReward(true); }); }); }这段代码回答了一个关键问题用户中途关闭激励视频该不该放行下载源码的做法是拦截——只有res.isEnded为 true 时才发奖励直接关掉的会给一个 Toast。这对应了摘要里反复提到的版本优化点从 V1.1 到 V1.5.1 都在打磨这一块的体验。另外注意onError里的降级处理广告拉取失败时直接onReward(true)放行因为广告失败还要用户再操作一次流失率会明显上升。参数设计上V1.1 新增的每天需要用户观看的激励广告次数在这里落成了dailyLimit字段并且区分了解析和下载两套次数。下载资源控制的观看次数独立配置阈值差异可以在AD_CONFIG里直接调。3.3 插屏广告的触发时机与避坑插屏广告是另一个收入来源但触发时机比激励视频更讲究。源码里通过triggerPage: detail这个配置指定了插屏的展示页面实际代码逻辑通常会这样做// 在页面 onShow 或返回动作后触发 export function showInterstitialIfEnabled() { if (!AD_CONFIG.interstitial.enabled) return; const interstitialAd uni.createInterstitialAd({ adUnitId: adunit-xxxx3 }); interstitialAd.onError((err) { console.warn(插屏广告加载失败忽略, err.errMsg); }); interstitialAd.show().catch(() { // 插屏广告拉取失败直接忽略不能阻塞业务 }); }插屏广告最容易出事的地方是同一个页面多次触发实例导致内存泄漏。uniapp 里页面实例销毁时广告实例不会自动销毁建议在onUnload里调用interstitialAd.destroy()。另一个坑是不要在onLoad里立刻弹插屏这时候页面渲染还没完成极其容易收到fail回调。源码处理方式是放在详情页返回动作之后给用户一个完成某件事的间隙。4. 版本迭代里的关键功能实现次数限制、中转站与权限校验4.1 每日观看次数限制的存储与判定逻辑V1.5.1 的更新日志只有一句话优化每日超过观看广告次数还弹窗提示看广告。听起来是小改动但实际涉及一个判断顺序问题。最初的版本是用户点击下载就弹广告广告关掉之后才发现今天次数超限体验割裂。优化后变成先校验当天剩余次数没次数则直接提示并退出根本不创建广告实例。function checkDailyLimit(key, limit) { const today new Date().toDateString(); const cacheKey ${key}_${today}; const record uni.getStorageSync(cacheKey); if (!record) { uni.setStorageSync(cacheKey, 1); return { allowed: true, remain: limit - 1 }; } const used Number(record); if (used limit) { return { allowed: false, remain: 0 }; } uni.setStorageSync(cacheKey, used 1); return { allowed: true, remain: limit - used - 1 }; }这里用toDateString()拼进缓存 key 是一个值得注意的细节。如果你用Date.now()存时间戳跨天之后还要比较当前时间是否大于零点多一步且容易错。直接用日期字符串做 key自然过期逻辑最少。uni.setStorageSync虽然是同步方法但在这种高频短逻辑里性能损耗可以忽略比异步的uni.setStorage更适合做次数计数。参数调整建议dailyLimit建议设置在 8 到 12 之间。设太高用户一天看十几个广告会流失设太低广告填充率不足、流量主收入也上不去。V1.5.1 这个优化本质就是把超限后不弹广告当成了一个独立的体验路径。4.2 下载中转站与合法域名的兜底策略V1.4 提到新增支持配置下载中转站需要自己有中转地址这一条看着冷门其实是解决下载失败最有效的手段。去水印接口返回的直链通常指向短视频平台的 CDN 域名这类域名不可能全部加进小程序的downloadFile合法域名里而微信只允许最多配置 200 个合法域名因此直链直接下载非常容易报url not in domain list。中转站的原理是在你自己控制的域名上做一层反向代理类似下面配置的逻辑{ transferHost: https://your-proxy.example.com, fallbackDomains: [] }前端代码在下载时判断如果当前直链域名不在白名单内就把downloadUrl拼上中转地址。function buildDownloadUrl(videoUrl) { const legalDomains uni.getAccountInfoSync().miniProgram.legalDomain || []; const host new URL(videoUrl).host; if (legalDomains.includes(host)) { return videoUrl; } // 中转站格式中转地址 encodeURIComponent(原始直链) return ${transferHost}?url${encodeURIComponent(videoUrl)}; }这段代码要注意两点uni.getAccountInfoSync()需要基础库 2.2.2 以上且返回的legalDomain是已配置并审核通过的合法域名列表中转站参数名没有统一标准有的服务要求target或path对接前需确认。所谓需要自己有中转地址指的就是这个代理服务需要你自行部署在云函数或轻量服务器上它反过来会引入后端成本这也是该版本要求自行准备的原因。同样在 V1.4 里源码还加了收集未添加的下载合法域名。实现方式是在每次下载失败且错误信息包含domain字样时把直链域名写入本地列表并在后台展示方便小程序管理员去公总台补齐合法域名。这个设计很实用因为解析接口可能隔三差五换 CDN 域名靠人工发现永远滞后。4.3 相册权限校验与封面下载、文案复制V1.5 的更新内容集中在保存链路优化相册权限校验新增无水印视频封面下载以及文案复制。权限校验这里说的不是saveImageToPhotosAlbum的默认弹窗而是更前置的uni.getSetting检查。首次授权直接弹窗没问题但用户拒绝过一次之后再调用 API 微信不会二次弹窗而是直接走fail回调。源码里为此加了权限状态探测function ensureAlbumAuth() { return new Promise((resolve, reject) { uni.getSetting({ success(res) { const auth res.authSetting[scope.writePhotosAlbum]; if (auth false) { // 曾经拒绝过只能引导手动打开 uni.showModal({ title: 需要相册权限, content: 请在设置中开启相册权限否则无法保存到手机, success(modalRes) { if (modalRes.confirm) { uni.openSetting({ success(settingRes) { const newAuth settingRes.authSetting[scope.writePhotosAlbum]; newAuth ? resolve() : reject(); } }); } else { reject(); } } }); } else if (auth true) { resolve(); } else { // undefined从未请求过直接授权即可 uni.authorize({ scope: scope.writePhotosAlbum, success: () resolve(), fail: reject }); } } }); }); }这个三段式判断覆盖了未授权 / 已拒绝 / 已授权全部分支比直接用uni.authorize更省心。封面下载本质和下载视频一致先用uni.downloadFile拿封面临时路径再saveImageToPhotosAlbum。区别是封面图体积小发起下载前可以额外加一个缩略图参数部分解析接口支持?sizesmall或?thumb1能省流量。文案复制则对应摘要里的文案复制功能利用uni.setClipboardData把视频描述或文案复制到剪贴板。这里有一个容易被忽略的细节uni.setClipboardData调用后系统会自带一个内容已复制的 Toast如果代码接着又调用uni.showToast会出现两个 Toast 叠加。源码里做了一层延迟处理在复制成功的回调里 1.5 秒后再展示自己的 Toast。5. 上线前检查清单与低频错误速查5.1 发布前的三处必查配置去水印小程序因为涉及外部接口和内容下载审核被拒的常见原因集中在三处都建议在提交体验版之前自查第一manifest.json里的mp-weixin.appid是否替换成你自己的 AppID同时request合法域名和downloadFile合法域名必须包含你的解析接口域名和中转站域名。需要在公总台的「开发管理 → 开发设置 → 服务器域名」里配置配置完有约 15 分钟的生效延迟。第二微信后台「设置 → 服务内容声明 → 用户隐私保护指引」要声明相册权限用途。2023 年后微信强制要求未声明的话uni.authorize会直接返回fail而不会弹窗很多人测试时遇到点了保存没反应就是这个原因。第三类目选择会对流量主审核产生影响。去水印功能更容易通过视频工具或图片处理这类工具类目。如果类目选了社交-社区基本不可能过审。如果提示小程序功能涉及内容版权则需要调整功能描述把重点放在用户仅可用于个人合法用途、不得传播侵权内容上。5.2 高频失败错误速查错误信息常见原因处理方式url not in domain list接口域名未配置到 request 合法域名将解析接口域名加进 request 合法域名downloadFile:fail url not in domain list视频直链域名未配置到 downloadFile 合法域名使用中转站代理或补充合法域名并等 15 分钟saveImageToPhotosAlbum:fail auth deny用户拒绝过相册权限调用uni.openSetting手动引导createRewardedVideoAd:fail no ad该广告位暂无广告填充不做拦截直接放行业务逻辑uploadFile:fail中转站证书链不完整检查中转域名 HTTPS 证书iOS 对证书要求较严no ad这个错误很多时候不是配置错误而是账户没有积累足够的广告投放标签。新流量主第一天往往没广告需要保持广告位开启并正常触发让微信广告系统完成特征收集通常两三天后填充率才会升上来。这就是为什么源码在onError里做降级放行的原因广告没填充不能把用户也卡住。5.3 用 adUnitId 区分测试与生产环境的技巧流量主后台每次新建广告位都会分配一个新的adunit-开头 ID这套源码的AD_CONFIG适合以此做环境切换// 开发环境用一个测试广告位上线前切换到正式 ID const ENV dev; export const AD_CONFIG { rewarded: { parse: ENV dev ? adunit-dev-test1 : adunit-prod-parse, download: ENV dev ? adunit-dev-test2 : adunit-prod-download } };测试广告位填入真实但异常的 ID 会有两个效果一是触发onError后走降级分支可以顺带验证广告模块的容错二是避免测试环境下真实计费。上线前把ENV改成prod即可不会影响页面逻辑。再配合第 4.1 节的每日次数限制在测试环境把dailyLimit调成 1就能快速验证超限后不再弹广告这个 V1.5.1 的修复点是否生效。最后还有一个容易被忽略的点真机预览时广告拉取会比模拟器稳定很多调试激励视频和插屏务必用真机。uni.getSetting权限状态在开发者工具里和真机上的返回值不完全一致尤其是scope.writePhotosAlbum的初始值工具里经常直接返回true导致权限分支测不到。本文还有配套的精品资源点击获取