My Computer · 2025/09/12 3

苹果cms 增强收藏和追剧提醒功能

先前做过一个版本,大部分是前端js执行,后端的代码也有问题,导致系统占用非常高,所以又重做了,前端只有一个js,其他全部在后端执行。配合:苹果cms 修复开启页面缓存后用户登录状态不一致问题 - N把刀 使用效果更佳。

一、功能概述(简要)

  • 收藏
    • 详情页“收藏/已收藏”实时切换;游客点击跳转登录。
    • 接口:GET /api.php/follow/statePOST /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_vodmac_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_followmac_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_epquality_from / quality_totitle/cover/url
    • update_labelvod_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 Streamstream:vod_update(事件通道)
  • 消费组vodcg;消费者名:c<pid>
  • 单实例锁:lock:vod_worker(值为进程 PID,TTL=25s 左右)
  • VOD 基线 Hash:vodstate:<vod_id>,字段:epq(画质)
  • List 兜底(可选):queue:vod_update

4) 处理算法(简化版流程)

  1. 启动时:
    • 连接 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 >
  2. 对每条事件(vod_id):
    • mac_vod:拿 vod_play_url 并解析当前 ep、quality(quality 映射如:TS=1, TC=2, 抢先=3, …, 4K=8;无则 0)。
    • vodstate:<vid>(基线):若无 →
      • 恢复模式:尝试用 mac_notice 的最新记录恢复 epPrev/qPrev,若有差异则立即 emitbaseline recovered ... -> emit);否则执行baseline init(只建基线,不发通知)。
    • 若有基线:
      • 判定epNow > epPrev 或(epNow==epPrev && qualityNow>qualityPrev)→ “更新”;
      • label-only:若 ep 不变但标题变化(可由 lists 的 update_label 呈现)→ 视开关 NOTICE_ON_LABEL_CHANGE=true 发通知(epPrev->epNow 会显示同值)。
    • 订阅用户mac_follow UNION mac_ulogulog_type IN (2,5))去重;
    • 合并/入库mac_notice
      • 若同一用户同一剧有最新一条status=0,并与本次更新连续或画质提升 → 更新其 to_ep/quality_to/updated_at
      • 否则插入新行(status=0)。
    • 更新 vodstate:<vid>epNow/qNow);写日志。
  3. 循环中定期续约 lock:vod_worker(防“单实例锁不存在”)。
  4. 捕获异常;必要时自动重建消费组

六、前端集成

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.php
  • application/api/controller/Notice.php
  • application/api/controller/Health.php
  • application/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(开机自启)

不提供源码下载,前前后后修改的文件太多了,实在懒得整理。