简介:这是一套面向知识付费创业者和开发者的三端互通小程序系统源码,覆盖微信小程序、PC端与H5公众号,支持DIY首页、卡密/图文/视频/音频/网盘等多类型资源发布、二级分销、代理分站、流量主广告和社群挂载等运营功能,适合快速搭建知识变现平台或用于二次开发学习。资源包共含2000个文件,压缩包约165.41MB,以js逻辑脚本、html页面、ts与css样式代码为主,辅以jpg/png/gif等界面素材、sql数据库文件、md说明文档和php服务端文件,目录结构完整,便于按前端、后端、配置与文档分类查阅。已有882人学习下载。通过这套V3.5.6版本源码,读者可获取完整的开源代码、DIY设计配置样例、多种资源发布与分销功能实现,以及代理分站和移动端自定义方案,能直接部署测试,也能作为开发知识付费系统时的功能参考。
1. 2024 年做内容变现,为什么绕不开三端互通 + 资源采集
2024 年做内容变现的人几乎都撞上同一个需求:课程得同时在小程序商城、公众号 H5、电脑网页里卖,用户在手机上付了钱,回到电脑还得能看,订单和会员不能各算各的账。资源牛知识付费系统这类开源版,就是冲着「小程序 + PC + H5 三端数据互通 + 资源采集」这套组合来的。
它的价值在于把原本要三套前端、一套后台、外加采集脚本的工程量,压缩成一份能部署、能改改就上线的开源项目,适合手里已有视频、文档等数字资源,想快速开知识付费小程序的个人站长和小团队。
一个反直觉的结论是:三端互通真正的难点不在界面适配,而在会员、订单、支付回调这些数据逻辑。界面只是最上层那层皮,数据层立不住,三端必然各说各话。
2. 三端数据互通的设计先决条件:会员、订单、课程表到底怎么建
拿到这类开源版,我第一件事不是改界面,而是把数据库表结构翻开看。三端数据互通不是让三个端长得像,而是同一个用户在任意一端登录后,看到同一份会员信息、同一份订单、同一份已购课程。要做到这点,后端必须是一套库、一套 API,前端分三条线去接。每张表的设计都决定了后面联调会不会返工,这一章讲的是最核心的三个设计点。
2.1 先定架构:uniapp 出一套小程序 + H5,PC 管理端单独走 API
常见做法是后端用 ThinkPHP 或 Laravel 这类 PHP 框架,小程序和 H5 共用一套 uniapp 代码。uniapp 一套代码能编译出微信小程序、H5、App,这是资源牛这类系统省事的关键。PC 管理端则是独立的 Vue 管理后台,给站长维护课程、会员、订单、采集任务。三条前端线都不直接连数据库,只通过 HTTP API 读写,所有业务判断收口在后端。
第一个原则就此定死:前端永不写业务 SQL,任何读写都经过后端统一鉴权。实现层就是一套全局中间件,三端同一个接口。
// 伪代码:API 鉴权中间件,小程序、H5、PC 共用 public function handle($request, \Closure $next) { $token = $request->header('Authorization', ''); $token = str_replace('Bearer ', '', $token); $user = db('member')->where('token', $token)->find(); if (!$user || $user['token_expire'] < time()) { return json(['code' => 401, 'msg' => '登录已过期']); } $request->user = $user; // 当前用户挂到请求上,后续业务直接用 return $next($request); }这套中间件的逻辑三端一致:小程序 wx.request、H5 axios、PC axios 都在请求头里带同一个 Authorization Bearer token,后端只认这个 token 对应的 member 记录。token_expire 我一般设 7 天,小程序端配合静默续期,用户无感知。值得注意的 trade-off:token 存在 member 表意味着同一账号后登录的端会顶掉先登录的端,如果想支持多端同时在线,要给 token 单独建表并加 device 字段,这是开源版默认不做的事,需要自己补。
2.2 三端共用的三张核心表:member、order、course_access
三端互通的本质是:同一 user_id 在任何端登录,看到的订单和已购课程完全一致。下面三张表是我认为最核心的骨架,建表时我会按这个思路核对开源版的字段。
-- 会员表:三端共用,user_id 全局唯一 CREATE TABLE `member` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `openid` varchar(64) DEFAULT NULL COMMENT '小程序 openid,H5/PC 为空', `unionid` varchar(64) DEFAULT NULL COMMENT '微信开放平台下同一账号统一标识', `nickname` varchar(64) NOT NULL DEFAULT '', `phone` varchar(20) DEFAULT NULL COMMENT '手机号,H5/PC 端登录凭据', `token` varchar(64) DEFAULT NULL, `token_expire` int(11) DEFAULT 0, `reg_channel` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1=小程序 2=H5 3=PC', `created_at` int(11) DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_openid` (`openid`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:渠道只用 channel 字段区分,不拆表 CREATE TABLE `order` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `order_sn` varchar(32) NOT NULL COMMENT '全局唯一订单号', `user_id` int(11) NOT NULL, `course_id` int(11) NOT NULL, `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `channel` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1=小程序 2=H5 3=PC', `status` tinyint(4) NOT NULL DEFAULT 10 COMMENT '10待支付 20已支付 30已关闭 40已退款', `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付单号,回调幂等用', `paid_at` int(11) DEFAULT 0, `created_at` int(11) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_sn` (`order_sn`), UNIQUE KEY `uk_trans` (`transaction_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程权限表:三端「买了就能看」的依据 CREATE TABLE `course_access` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `course_id` int(11) NOT NULL, `created_at` int(11) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_course` (`user_id`, `course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;三端互通不是三套库做同步,而是所有端读写同一张 order。channel 字段只用于统计这单从哪个端来的,不影响订单归属。用户识别分两条路:小程序端用 openid,H5 和 PC 端用手机号登录;如果接了微信开放平台,unionid 能把同一微信用户在三个端串起来,这是最干净的方案。course_access 表是三端统一的「已购凭证」,不管用户从哪个端付的钱,只要这张表有记录,任意端都能看课。
2.3 支付回调和订单状态机:数据互通最容易翻车的一层
订单状态机我按四态设计:10 待支付、20 已支付、30 已关闭、40 已退款。其中 10 到 20 的迁移只能由支付回调触发,前端不能自己改状态,这是原则。三端会走三种不同支付方式:小程序用 wx.requestPayment,H5 用公众号支付,PC 用扫码。三个渠道回调同一个接口,入账逻辑不幂等就是灾难。
// 微信支付回调:三端同一套入账逻辑 public function wxNotify() { $data = $this->parseWxNotify(); // 验签、解密,失败直接返回 fail $order = db('order')->where('order_sn', $data['out_trade_no'])->find(); if (!$order || $order['status'] != 10) { return 'success'; // 非待支付状态直接返回,避免重复入账 } $updated = db('order')->where('order_sn', $order['order_sn']) ->where('status', 10) // 乐观锁:只更新待支付单 ->update([ 'status' => 20, 'transaction_id' => $data['transaction_id'], 'paid_at' => time() ]); if ($updated) { // 开通课程权限:这步和订单更新要放同一事务 db('course_access')->insert([ 'user_id' => $order['user_id'], 'course_id' => $order['course_id'], 'created_at' => time() ]); } return 'success'; }两个关键参数:transaction_id 唯一索引防重复入账,微信重试回调时第二次插入直接撞唯一键,权限不会开两遍;乐观锁 where status=10 保证并发回调下只有第一次能更新成功。我以为最坑的是订单更新成功但 course_access 插入失败,用户付了钱看不到课,所以这两步必须放同一个事务,任一步失败就返回 fail 让微信继续重试。前端只问「course_access 有没有记录」,不判断支付渠道,这样三端的看课逻辑天然一致。
3. 本地跑通资源牛开源版:部署、双端编译和三端联调的完整路径
拿到开源版别急着传服务器,先在本地跑通整个链路。我给的最小路径是三步:后端起服务,管理端能登录,uniapp 分别编译出 H5 和小程序并在三端完成一次真实下单。本地能跑通,上线就只是换域名和配 https 的问题。
3.1 环境准备:PHP 版本、MySQL、小程序 AppID 三件事
先核对环境,开源版跑不起来八成是版本问题,不是代码问题。
| 软件 | 推荐版本 | 用途 | 注意 |
|---|---|---|---|
| PHP | 8.1 / 8.2 | 后端运行 | 7.4 能跑但别用,很多依赖已不兼容 |
| MySQL | 5.7+ / 8.0 | 数据存储 | 建库必须 utf8mb4,否则中文变乱码 |
| Composer | 2.x | 安装 PHP 依赖 | 国内先配镜像源,否则装到超时 |
| HBuilderX | 最新稳定版 | 编译 uniapp | 也可用 VSCode 加 uni-app 插件 |
| 微信开发者工具 | 最新稳定版 | 跑小程序 | 需要真实 AppID |
小程序端编译必须要一个真实 AppID。用测试号能打开界面,但 wx.requestPayment 这类支付接口不可用,联调支付还得换真号。本地开发阶段,在微信开发者工具里勾选「不校验合法域名」,这样 http://127.0.0.1 的接口也能请求;上线前再把 https 正式域名配进小程序后台白名单并取消这个勾选。
3.2 后端启动:导入数据库、改 .env、起本地服务
后端以 ThinkPHP 为例,换 Laravel 只是命令不同,思路一致。
# 1. 还原依赖 composer install --no-dev # 2. 复制环境配置并修改数据库连接 cp .env.example .env # 编辑 .env:DB_HOST=127.0.0.1 DB_PORT=3306 # DB_DATABASE=resource_niu DB_USERNAME=root DB_PASSWORD=你的密码 # 3. 导入数据库 mysql -uroot -p resource_niu < install/resource_niu.sql # 4. 起本地开发服务,默认 127.0.0.1:8000 php think run -p 8000.env 里除了数据库连接,还有 APP_URL、token 密钥、微信支付三组参数。token 密钥在开源版里通常是占位符,上线前必须换成自己的随机串,否则别人拿默认密钥可以伪造登录态,这是这类开源版最常见的真实漏洞。导入 SQL 报错,先查 MySQL 版本和库字符集,utf8mb4 没配好会直接在 utf8 字段上炸。
3.3 uniapp 双端编译:一套代码怎么指向两个不同域名
小程序和 H5 的域名不一样:H5 是你自己的站点域名,小程序必须是配置进后台白名单的 https 域名。代码只有一套,用 uni-app 的条件编译区分,我习惯的做法是单独抽一个 config 文件。
// utils/config.js const ENV = { development: { h5Base: 'http://127.0.0.1:8000/api', mpBase: 'http://127.0.0.1:8000/api' // 开发者工具可关域名校验 }, production: { h5Base: 'https://www.yourdomain.com/api', // H5 和 PC 共用正式 API 域名 mpBase: 'https://api.yourdomain.com/api' // 小程序必须白名单内的 https 域名 } }; // #ifdef H5 export const BASE_URL = ENV[process.env.NODE_ENV === 'development' ? 'development' : 'production'].h5Base; // #endif // #ifdef MP-WEIXIN export const BASE_URL = ENV[process.env.NODE_ENV === 'development' ? 'development' : 'production'].mpBase; // #endif小程序端不能像 H5 那样自由跨域,请求域名必须在微信后台「开发管理-服务器域名」里配置,且只认 https。H5 端如果后端没配跨域头,在 nginx 加 Access-Control-Allow-Origin 即可。PC 管理端是独立 Vue 工程,我建议各配各的 .env,不要硬塞进 uniapp 仓库,混在一起发版容易互相污染。如果 H5 端要接入企业微信客服,页面还得额外加载微信 JS-SDK 并调 openCustomerServiceChat,这不是开源版默认功能,需要自己补。
小程序请求封装我固定做成 Promise 形式:
// utils/request.js import { BASE_URL } from '@/utils/config.js'; export function request(path, { method = 'GET', data = {} } = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + path, method, data, header: { Authorization: 'Bearer ' + (uni.getStorageSync('token') || '') }, success: (res) => { if (res.data.code === 401) { uni.removeStorageSync('token'); uni.navigateTo({ url: '/pages/login/index' }); reject(res.data); return; } resolve(res.data); }, fail: reject }); }); }uni.request 在 H5 端走 ajax,在小程序端走 wx.request,封装层让业务代码不用感知端差异。401 统一跳登录是必写的,否则三端里有一端 token 过期,用户会卡在「界面像登录了但所有接口都报错」的假死状态。
3.4 三端联调自检:登录、下单、回调各跑一遍
本地跑通的定义不是能打开首页,而是完整走一遍业务主链。我按四步自检:
- PC 管理端注册一个账号,去数据库 member 表确认 user_id 和 reg_channel=3。
- 同一手机号在 H5 端登录,再去小程序端用手机号登录,确认两个端拿到同一个 user_id。这一步是三端数据互通的第一道验证,对不上就是登录逻辑有分支没走统一表。
- 任一端下载单。本地没有真实微信商户号时,常见做法是后端留一个测试回调接口,手动把订单推到已支付并写入 course_access,等联调阶段再换真实回调地址。
- 去另外两个端刷新已购列表,确认课程出现。三端都看到同一门课,数据互通才算真通了。
local 环境跑通这套链路,大概一个小时。你会发现大部分时间花在环境配置而不是业务代码上,这是开源版的常态。
4. 资源采集模块落地:抓取、定时入库、防重复的参数怎么设才不翻车
资源牛这类开源版自带的采集,开箱即用往往只够演示。我自己跑生产采集,一定拆成四步:抓列表、抓详情、清洗、入库,全部走命令行任务,绝不让前端请求实时触发爬虫。实时爬意味着用户在等你的接口,目标站一慢,你的页面就超时,这是架构错误。另外提醒一句:采集前先确认目标站内容是否有授权,技术能做不代表事情该做。
4.1 采集链路设计:列表解析、正文抽取、图片转存
命令行的采集任务长这样。以 ThinkPHP 命令为例,核心是选器和频率两个控制点。
// application/command/collect.php public function handle() { $sourceUrl = $this->argument('url'); // 目标站列表页 $html = $this->fetch($sourceUrl); // curl 带 UA 抓取 $links = $this->parseLinks($html); // 解析出详情页 URL 和标题 foreach ($links as $item) { $detail = $this->fetch($item['url']); $course = [ 'title' => trim(strip_tags($detail->find('.article-title')->html())), 'content' => $detail->find('.article-content')->html(), 'cover' => $this->saveCover($detail->find('img')->src), // 图片转存本地/OSS 'source_url' => $item['url'] ]; $this->saveCourse($course); usleep(random_int(500000, 1500000)); // 每次抓取睡 0.5~1.5 秒 } }选器是采集最大的坑。目标站改一次前端,你的正则和选择器全作废。我会把所有选器表达式提到配置文件里,目标站改版只改配置不动代码。saveCover 必须把远程图片下载到本地或 OSS,否则目标站一加防盗链,你的课程封面和正文图片全裂。抓取参数我固定成:超时 10 秒、UA 用真实浏览器 UA、失败重试 2 次、单批最多 50 条。这个量级下,一个批次的执行时间可控在十分钟内。
4.2 定时任务与频率控制:别把目标站打挂也别封了自己
频率控制是采集活下来的前提。每分钟跑一次的任务,一小时内就会被目标站的 web 防火墙盯上,轻则 IP 临时封禁,重则目标站直接屏蔽你的抓取 UA。我的 crontab 配置:
crontab -e # 每 30 分钟采集一次,错开整点和半点 */30 * * * * cd /www/wwwroot/resource_niu && php think collect >> storage/logs/collect.log 2>&1 # 每天凌晨 4 点重跑失败任务 0 4 * * * cd /www/wwwroot/resource_niu && php think collect --retry >> storage/logs/collect-retry.log 2>&1代码里的 usleep 随机延迟是刻意加的,让请求间隔不是固定节奏,固定节奏最容易被风控识别。单批 50 条跑完就停,不要把整个目标站一次性拉完。你没那么缺内容,目标站也没那么抗打。--retry 的重试任务只捞 collect_log 里 status=2 的失败记录,避免每天凌晨全库扫一遍浪费时间。
4.3 清洗去重与入库:md5 指纹加唯一索引双保险
去重只查标题是新手最爱犯的错。目标站经常在标题后追加栏目名或日期,同一条内容换了标题就被当成新课再采一遍。我在采集日志表和课程表里同时做了两道防线:
CREATE TABLE `collect_log` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `source_url` varchar(255) NOT NULL, `title` varchar(255) NOT NULL, `content_md5` char(32) NOT NULL DEFAULT '' COMMENT '标题+正文指纹', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待处理 1成功 2失败 3重复', `error_msg` varchar(255) DEFAULT NULL, `created_at` int(11) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_source_url` (`source_url`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;入库前先算指纹:
// 标题 + 正文前 500 字做 md5,比全文 md5 稳 $fingerprint = md5($course['title'] . mb_substr($course['content'], 0, 500)); $exists = db('course')->where('content_md5', $fingerprint)->find(); if ($exists) { $this->logDuplicate($course['source_url']); return; // 标记为重复,跳过 }只取正文前 500 字算指纹,是因为目标站详情页正文末尾常带随机推荐位或统计代码,全文 md5 会导致同一篇文章因为尾部小尾巴不同被反复入库。source_url 唯一索引是第二道保险,就算查重逻辑漏了,同一条来源链接也插不进第二遍。视频资源我建议只存地址不下载,文档资源可以下载转存,但下载任务单独起 worker,别和抓取任务串在一起,一个超时拖死一批。
4.4 采集日志与断点续采:没有日志的采集等于黑匣子
生产采集不是一次跑完的。目标站改版、服务器重启、目标站超时都会中断。我会把每条抓取状态写进 collect_log,失败记录 error_msg,重跑时 --retry 只处理 status=2 的记录。连续失败 5 次以上的 source_url 自动停采,避免每次定时任务都拿同一个坏链接反复试。采集是脏活,日志就是后悔药;没有日志的采集任务等于黑匣子,出了问题只能从头再跑,而且永远说不清哪条采过哪条没采过。
5. 避坑:三端互通和资源采集最容易翻车的 5 个高频问题
这些坑我基本都踩过一遍,每条按「现象 → 原因 → 解决」写,大多是部署后一两周内会撞上的问题。提前看过,能省掉好几个凌晨。
5.1 H5 在微信里登录态反复失效
现象:H5 端在微信里打开,昨天还好好的,今天接口全报 401,重新登录后过一会儿又掉。 原因:开源版默认把 token 存 localStorage,微信浏览器在内存回收或深色模式切换时可能清掉;更常见的是后端 token_expire 配得太短,有人把 7 天写成了 7200 秒。 解决:token_expire 按业务设到 7 到 15 天;H5 端加静默续期,每次请求成功发现剩余有效期不足 1 天就调 /refresh-token 换新 token。小程序端 token 存 storage,逻辑一样,但小程序没有微信浏览器清 localStorage 的问题,重点查后端有效期。
5.2 小程序支付回调后订单停在待支付
现象:用户在小程序里付了钱,订单状态没变,course_access 没写入,用户投诉售后。 原因:商户平台配的回调地址是 http 或内网地址,微信根本打不到;另一个常见原因是回调里更新订单成功,但插入 course_access 报错没捕获,订单已支付但权限没开。 解决:商户平台回调地址必须是公网可访问的 https 地址;把更新订单和写入 course_access 放同一个数据库事务,任一步失败整体回滚并返回 fail,让微信按策略重试。排查时先在小程序开发者工具的 Network 面板看回调接口响应,再看商户后台回调记录,两步就能定位是网络没通还是事务没提交。
5.3 采集资源重复入库
现象:课程列表里同一个标题出现好几次,内容一模一样,用户以为你在刷数据量。 原因:去重只查 title,而目标站会在标题后追加日期或栏目后缀,同一条内容换标题就被当新课;或者查重和插入之间没有唯一索引兜底,并发任务同时跑时出现空窗。 解决:按 4.3 的做法,标题加正文前 500 字做 md5 指纹,课程表建 content_md5 唯一索引当第二道锁;并发场景下给采集任务加互斥锁,用 Redis 锁或数据库锁保证同一时间只有一个采集进程在写课程表。
5.4 三端看到的时间差 8 小时
现象:小程序里订单和课程显示的时间比实际晚 8 小时,H5 和 PC 显示却正常。这种问题最玄学,因为数据明明是同一份。 原因:后端存的是 int 时间戳,本身没有时区概念。问题出在小程序端展示时用 new Date(timestamp).toLocaleString() 做了本地格式化,而测试机的时区是 UTC;H5 端浏览器时区恰好是东八区,所以看起来正常。 解决:后端接口只返回 int 时间戳,不返回 date 字符串;前端统一用一个 dateUtil 工具转成东八区字符串,三端调同一个函数。不要在业务代码里散落 toLocaleString 的调用,散落一次就埋一颗雷。
5.5 域名白名单、类目资质和小程序年审的三重夹击
现象:本地全通,提审被拒,提示类目不含「教育 - 在线教育」;或者线上正式版白屏,而开发者工具里一切正常。 原因:知识付费涉及虚拟支付,微信要求小程序服务类目必须包含在线教育并上传对应资质;正式版不走开发者工具的「不校验合法域名」,白名单里少一个域名就整个挂。 解决:提审前先核对后台服务类目,配好教育类资质;服务器域名白名单把 request、uploadFile、downloadFile 三类合法域名全部加齐,课程附件若放在 OSS,OSS 域名也要加进 downloadFile。再有就是年审,我吃过这个亏:项目放了两个月没动,提审时才发现小程序已过期,重新年审又耗了一周。开发者工具提示「开发版已过期」就重新扫码,但小程序主体年审过期没人提醒你,自己记好到期时间。
6. 进阶:给开源版补一个分销开关,并写三端一致性校验脚本
很多开源版不带分销,或者带了但逻辑写死。我给自己部署的版本加了一个可控的分销开关,做法很朴素:一张配置表加一个订单字段。config 表里存 distribute_enable,order 表加 referrer_id。下单时请求带 share_uid 且开关为 1,就在支付成功回调里把分享人 id 写进订单;结算走后台手动,不搞自动提现。加开关的价值是先小范围灰度,数据对得上再放开,避免返佣算错自己垫钱。分销逻辑和支付回调放一起,就是防止有人下单后分享关系对不上账。
三端数据互通上线后会悄悄坏掉,最典型的就是订单已支付但权限没给。我每次发版前在服务器上跑这两条 SQL,输出为空才敢发布:
-- A. 已支付但没有开课权限的异常单 SELECT o.order_sn, o.user_id, o.course_id, o.status FROM `order` o LEFT JOIN course_access ca ON ca.user_id = o.user_id AND ca.course_id = o.course_id WHERE o.status = 20 AND ca.id IS NULL; -- B. 有开课权限但没有对应已支付订单的脏数据 SELECT ca.user_id, ca.course_id FROM course_access ca LEFT JOIN `order` o ON o.user_id = ca.user_id AND o.course_id = ca.course_id AND o.status = 20 WHERE o.id IS NULL;第一条查支付成功但权限没写,基本是回调事务没提交;第二条查权限比订单多,多半是测试时手动插数据忘了清。我自己的习惯是每周五下午把采集日志和这两条对账 SQL 一起过一遍,十分钟的事,已经避免过好几次「用户说买了课看不到」的售后。做知识付费系统,功能可以少,数据不能乱;三端互通不是上线那天的事,是每次改完支付、改完采集都要回来验一遍的事。希望帮到你。
本文还有配套的精品资源,点击获取