先前做过一个版本,大部分是前端js执行,后端的代码也有问题,导致系统占用非常高,所以又重做了,前端只有一个js,其他全部在后端执行。配合:苹果cms 修复开启页面缓存后用户登录状态不一致问题 - N把刀 使用效果更佳。
一、功能概述(简要)
- 收藏
- 详情页“收藏/已收藏”实时切换;游客点击跳转登录。
- 接口:
GET /api.php/follow/state、POST /api.php/follow/toggle。 - 数据落地:
mac_follow(系统新表)+ 兼容旧日志mac_ulog(type=2、5)。
- 追更提醒
- 100% 后端判断(Worker 事件驱动 + Redis),前端只“展示与确认”。
- 采集器/保存 VOD 成功后:
XADD stream:vod_update * vod_id <id>推事件。 - Worker 秒级消费,比较集数/画质、或仅标题变化(label-only) → 生成
mac_notice。 - 前端在全站任意页面都会拉
/api.php/notice/list并弹“更新气泡”;- 可展开条目、逐条/整批“知道了(ACK)”;
- 游客:每日一次登录引导气泡。
- 与缓存解耦:整页可大胆缓存,个体化数据通过 API 洞穿。
前端预览 未登录状态下收藏

追更提醒气泡

展开

二、数据库与索引
有表:
mac_vod、mac_ulog(原有)。新增或确认如下表与索引。
1) 收藏表:mac_follow
CREATE TABLE IF NOT EXISTS `mac_follow` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`vod_id` BIGINT UNSIGNED NOT NULL,
`created_at` INT UNSIGNED NOT NULL DEFAULT 0,
`updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_user_vod` (`user_id`,`vod_id`),
KEY `idx_vod_user` (`vod_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2) 通知表:mac_notice
CREATE TABLE IF NOT EXISTS `mac_notice` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`vod_id` BIGINT UNSIGNED NOT NULL,
`from_ep` INT UNSIGNED NOT NULL DEFAULT 0,
`to_ep` INT UNSIGNED NOT NULL DEFAULT 0,
`quality_from` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`quality_to` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`status` TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0=未读;1=已读
`created_at` INT UNSIGNED NOT NULL DEFAULT 0,
`updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_status_time` (`user_id`,`status`,`updated_at`),
KEY `idx_vod_user` (`vod_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
旧表 mac_ulog 用于兼容上线前收藏(ulog_type IN (2,5)),worker 会合并 mac_follow 与 mac_ulog 两类订阅人群。
三、Nginx 重写与路由
1) Nginx(重点让 /api.php/... 能直达接口)
rewrite ^/api.php(.*)$ /api.php?s=$1 last;
一般都会开启url重写,这一步可以省略。
2) ThinkPHP 路由(application/route.php)
use think\Route;
// 收藏与状态
Route::get('api/follow/state', 'api/Follow/state');
Route::post('api/follow/toggle', 'api/Follow/toggle');
// 通知拉取 & 确认
Route::get('api/notice/list', 'api/Notice/lists');
Route::post('api/notice/ack', 'api/Notice/ack'); // ids 批量确认
Route::post('api/notice/ack_vod', 'api/Notice/ackVod'); // 按剧确认(不再提醒本轮)
// 播放进度(可选)
Route::post('api/progress/report', 'api/Progress/report');
Route::get('api/progress/last', 'api/Progress/last');
// 健康检查
Route::get('api/health/ping', 'api/Health/ping');
四、后端 API(控制器要点)
目录:/www/wwwroot/tv/application/api/controller
1) Follow.php(核心逻辑)
state():返回{ok, login_required, followed, text}toggle():切换或设置收藏;写mac_follow;做限流(user_id 级、每分钟 N 次)
关键点:
- 表名必须是
mac_follow(之前出过mac_mac_follow的前缀错误)。 toggle成功后返回followed:true/false与按钮文案。
2) Notice.php(列表与确认)
lists():按用户拉取未读通知,按 VOD 聚合,并返回每部剧的:ids(本轮未读通知 id 列表,给 ACK 用)from_ep / to_ep、quality_from / quality_to、title/cover/urlupdate_label:从vod_play_url解析的“最后一条标题”或“集数最大标题”(前端优先展示它)
ack():按ids[]批量确认已读(将status=1)。ackVod():按vod_id一键将本剧所有未读置已读。
务必在 lists() 查 VOD 时包含 vod_play_url 字段,并设置:
'update_label' => $this->parseUpdateLabelSimple((string)($v['vod_play_url'] ?? '')),
解析函数(智能版)
在 Notice.php 类中包含(名称保持一致):
private function parseUpdateLabelSimple(string $playUrl): string
{
$s = (string)$playUrl;
if ($s === '') return '';
$s = str_replace(["\r\n", "\r"], "\n", $s);
$isUrl = function (string $x): bool {
$x = trim($x);
if ($x === '') return false;
if (preg_match('~^(?:https?://|//)~i', $x)) return true;
if (preg_match('~\.(m3u8|mp4|flv|avi|mkv)(?:\?|#|$)~i', $x)) return true;
return false;
};
$maxEp = 0; $labelForMaxEp = ''; $lastNonEmpty = '';
$segments = explode('$$$', $s);
foreach ($segments as $seg) {
$seg = trim($seg); if ($seg==='') continue;
$lines = preg_split('/[\n#]+/', $seg);
foreach ($lines as $line) {
$line = trim($line); if ($line==='') continue;
$label = '';
if (strpos($line,'$')!==false) { list($left,) = explode('$',$line,2); $label = trim($left); }
else if (!$isUrl($line)) { $label = $line; }
if ($label==='') continue;
$lastNonEmpty = $label;
$n=0;
if (preg_match('/第?\s*(\d{1,4})\s*(?:集|期)$/u',$label,$m)
|| preg_match('/(?:^|[^\d])(\d{1,4})(?:集|期)?$/u',$label,$m)) { $n=intval($m[1]); }
elseif (mb_strpos($label,'大结局')!==false) { $n=999999; }
if ($n>$maxEp) { $maxEp=$n; $labelForMaxEp=$label; }
}
}
return $labelForMaxEp!=='' ? $labelForMaxEp : $lastNonEmpty;
}
3) Health.php(健康检查)
public function ping(){ return json(['ok'=>1,'time'=>time()]); }
补充: 在 Vod 的 saveData($data)(采集/保存入口)里最后加上 XADD(try/catch 包裹):
$redis = new \Redis();
if ($redis->connect($host,$port,1.0)) {
if ($auth) $redis->auth($auth);
$redis->rawCommand('XADD','stream:vod_update','*','vod_id',(string)$vodIdForEvent);
}
这样每次新增/更新 VOD 都会投事件。
五、Worker(核心)
1) 位置与启动
- 脚本:
/www/wwwroot/tv/bin/vod_worker_standalone.php - 守护:
systemd服务名vod-worker(见后文自启配置)
2) 外部依赖
- PHP CLI(与网站同版本);
- phpredis(已内置在大多数环境);
- Redis 7+;
- MySQL(PDO)。
3) 关键常量与键位
- Redis Stream:
stream:vod_update(事件通道) - 消费组:
vodcg;消费者名:c<pid> - 单实例锁:
lock:vod_worker(值为进程 PID,TTL=25s 左右) - VOD 基线 Hash:
vodstate:<vod_id>,字段:ep、q(画质) - List 兜底(可选):
queue:vod_update
4) 处理算法(简化版流程)
- 启动时:
- 连接 MySQL / Redis;
XGROUP CREATE stream:vod_update vodcg 0 MKSTREAM(若群不存在);- 抢
lock:vod_worker(SET NX EX);拿不到就退出(避免多实例); - 进入循环:
XREADGROUP GROUP vodcg c<pid> COUNT=BATCH BLOCK=BLOCK_MS STREAMS stream:vod_update >
- 对每条事件(
vod_id):- 读
mac_vod:拿vod_play_url并解析当前 ep、quality(quality 映射如:TS=1, TC=2, 抢先=3, …, 4K=8;无则 0)。 - 读
vodstate:<vid>(基线):若无 →- 恢复模式:尝试用
mac_notice的最新记录恢复epPrev/qPrev,若有差异则立即 emit(baseline recovered ... -> emit);否则执行baseline init(只建基线,不发通知)。
- 恢复模式:尝试用
- 若有基线:
- 判定:
epNow > epPrev或(epNow==epPrev && qualityNow>qualityPrev)→ “更新”; - label-only:若 ep 不变但标题变化(可由 lists 的
update_label呈现)→ 视开关NOTICE_ON_LABEL_CHANGE=true发通知(epPrev->epNow会显示同值)。
- 判定:
- 订阅用户:
mac_followUNIONmac_ulog(ulog_type IN (2,5))去重; - 合并/入库
mac_notice:- 若同一用户同一剧有最新一条且
status=0,并与本次更新连续或画质提升 → 更新其to_ep/quality_to/updated_at; - 否则插入新行(
status=0)。
- 若同一用户同一剧有最新一条且
- 更新
vodstate:<vid>(epNow/qNow);写日志。
- 读
- 循环中定期续约
lock:vod_worker(防“单实例锁不存在”)。 - 捕获异常;必要时自动重建消费组。
六、前端集成
1) 全站脚本:/static/js/btn-follow.js
特点:
- A 段:收藏按钮逻辑(若页面上存在
.mac_ulog.btn-collect[data-id]则初始化;不存在也不 return)。 - B 段:追更提醒气泡(总是拉
/api.php/notice/list渲染),因此首页/列表/搜索页都会弹。
其中文案优先用
update_label,没有才退回to_ep。
七、自启与状态/修复脚本
1) systemd 服务单元:/etc/systemd/system/vod-worker.service
[Unit]
Description=Feikuai Vod Update Worker
After=network.target
[Service]
Type=simple
User=www
Group=www
ExecStart=/usr/bin/php /www/wwwroot/tv/bin/vod_worker_standalone.php
Restart=always
RestartSec=3
WorkingDirectory=/www/wwwroot/tv
StandardOutput=append:/www/wwwroot/tv/runtime/log/vod_worker.log
StandardError=append:/www/wwwroot/tv/runtime/log/vod_worker.log
Environment=PHP_MEMORY_LIMIT=256M
[Install]
WantedBy=multi-user.target
启用:
systemctl daemon-reload
systemctl enable --now vod-worker
systemctl status vod-worker -n 50
2) 状态脚本:/www/wwwroot/tv/bin/vod_worker_status.sh
- 检查进程、锁(
lock:vod_worker)、Stream/Group/Consumer、PENDING、日志尾部。
3) 一键“修复脚本”:/www/wwwroot/tv/bin/vod_worker_fix.sh
用途:当发现“锁/消费组/消费者丢失”或“xReadGroup 报错”等,执行自动修复与重启。
#!/usr/bin/env bash
set -euo pipefail
PHP=/usr/bin/php
ROOT=/www/wwwroot/tv
LOG="$ROOT/runtime/log/vod_worker.log"
SERVICE=vod-worker
REDIS_CLI=redis-cli
STREAM=stream:vod_update
GROUP=vodcg
LOCK=lock:vod_worker
echo "[fix] stop service..."
systemctl stop "$SERVICE" || true
echo "[fix] del lock if exists..."
$REDIS_CLI DEL "$LOCK" >/dev/null || true
echo "[fix] ensure stream & group..."
$REDIS_CLI XGROUP CREATE "$STREAM" "$GROUP" 0 MKSTREAM >/dev/null 2>&1 || true
echo "[fix] rotate log..."
mkdir -p "$ROOT/runtime/log"
if [ -f "$LOG" ]; then mv "$LOG" "$LOG.$(date +%Y%m%d%H%M%S)"; fi
touch "$LOG"
echo "[fix] start service..."
systemctl start "$SERVICE"
sleep 1
systemctl status "$SERVICE" -n 20
echo "[fix] ok."
授权并使用:
chmod +x /www/wwwroot/tv/bin/vod_worker_fix.sh
/www/wwwroot/tv/bin/vod_worker_fix.sh
全部代码与文件:
application/route.php(API 路由示例)application/api/controller/Follow.phpapplication/api/controller/Notice.phpapplication/api/controller/Health.phpapplication/api/controller/Progress.php(可选)bin/vod_worker_standalone.php(独立 Worker,含单实例锁/消费组自愈)bin/vod_worker_status.sh(状态检查)bin/vod_worker_fix.sh(一键修复/重建消费组并重启)static/js/btn-follow.js(全站脚本:A 收藏按钮;B 追更提醒始终运行)systemd/vod-worker.service(开机自启)
不提供源码下载,前前后后修改的文件太多了,实在懒得整理。
大佬的站点能看一下吗~
现在在git上的苹果cms官方(magicblack)下载的源码有后门了,前辈,这个怎么处理呀
可以试试早期版本