基于PHP的双路召回新闻推荐系统设计与实现 📅 发布时间:2026/9/16 17:34:49 👁 浏览次数: 简介这份毕业设计项目以个性化新闻推荐为核心融合用户行为分析与内容特征提取面向计算机相关专业学生完成推荐系统课题或实际新闻平台开发。项目提供完整可运行的Web系统涵盖新闻采集、分类展示、用户画像构建和推荐结果生成等环节。压缩包共2000个文件约21.38MB以HTML、JavaScript、PHP、CSS等为主要类型其中HTML负责页面结构JS实现前端交互与异步请求PHP处理后端业务逻辑CSS完成界面样式另有JSON与SQL文件存放配置数据和数据库结构便于理解分层开发思路。已有212人学习下载。资源中不仅包含推荐算法核心代码还提供数据文件、部署脚本及说明文档可帮助快速搭建环境、复现推荐流程并对课题设计、论文撰写和答辩演示都有参考价值。1. 从“管理系统毕业设计”到“双路召回新闻推荐”的距离每年毕业设计里有大量“新闻管理系统”无非是后台发布、前台展示最多加个搜索。这个项目不一样的地方在于它没有把重点放在内容生产端而是用用户行为和内容特征两条线同时做推荐召回。从文件列表里能看到 php_xxtea.c、xxtea.c、bootstrap.css、froala_editor.pkgd.css 这些组件说明它不是一个纯 Demo而是把前端展示、后台编辑、参数加密、推荐算法都串起来了。如果你是学 PHP 方向、想做一个能讲清楚“推荐”而非“CRUD”的毕业设计这套代码的思路可以直接复用。下文我会按后端数据设计、推荐接口、加密扩展、前端资源组织、调优验证这条线逐步拆解尽量让你照着能跑起来。2. 双路推荐的后端设计用户行为加权与内容特征匹配2.1 为什么单路推荐放在新闻场景下不够用只做基于内容的推荐系统会持续给用户推相似主题的新闻用户一旦看腻就跳走。只做基于协同过滤的行为推荐新新闻没有用户行为永远没有曝光机会。新闻的时效性很强用户今天看体育明天可能看科技行为窗口必须短。这个项目选择把两条路并行一条统计用户最近几天的点击、收藏、分享行为找出“热度高且用户愿意看的新闻”另一条提取用户历史兴趣的关键词去匹配内容特征最接近的新闻。最后把两个召回列表做一个加权融合这样既有行为依据也有内容兜底。2.2 数据表设计把行为权重和内容标签落进 MySQL推荐系统落地第一步是数据结构。项目里常见的表设计是 news、user_behavior、news_tag 三张核心表。下面是我根据这套系统整理后的简化 schemaCREATE TABLE news ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, category VARCHAR(50) DEFAULT , content TEXT, publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user_behavior ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, news_id INT UNSIGNED NOT NULL, behavior_type TINYINT NOT NULL COMMENT 1浏览 2点击 3收藏 4分享, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE news_tag ( news_id INT UNSIGNED NOT NULL, tag VARCHAR(50) NOT NULL, weight DECIMAL(5,4) DEFAULT 0.1000, PRIMARY KEY (news_id, tag) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user_behavior 的 behavior_type 字段是召回召回里最关键的行为权重依据。这里的设计意图是浏览只代表用户滑到了页面点击代表有主动意愿收藏和分享则是强正向反馈。实际权重可以在榜单参数里调整比如浏览 0.5、点击 1.0、收藏 2.0、分享 3.0。news_tag 表里可以存新闻关键词也可以存从正文提取出的高频词权重字段用 0-1 归一化。2.3 行为召回一个带权重的实时 SQL 查询项目在 PHP 端直接查 MySQL 做召回常见做法是限制最近 7 天行为窗口按行为权重汇总出用户感兴趣的新闻列表。比如用户今天看了 10 条新闻但只给其中一条收藏那么收藏那条的得分要远超其余九条。SQL 可以这样写SELECT news_id, SUM(CASE behavior_type WHEN 1 THEN 0.5 WHEN 2 THEN 1.0 WHEN 3 THEN 2.0 WHEN 4 THEN 3.0 END) AS score FROM user_behavior WHERE user_id :user_id AND created_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY news_id ORDER BY score DESC, MAX(created_at) DESC LIMIT 50;这里的:user_id是预处理参数避免 SQL 注入。DATE_SUB(NOW(), INTERVAL 7 DAY)设置的是滑动时间窗口天数作为行为衰减边界。如果希望更近期行为占更重比例可以再乘一个指数衰减因子比如EXP(-DATEDIFF(NOW(), created_at) * 0.3)。我一般会把得分再归一化到 0-1 之间方便和内容匹配得分调和。2.4 内容匹配基于标签向量的余弦相似度实现内容匹配不能只靠 SQL需要在 PHP 中把用户感兴趣标签和新闻标签做向量计算。假设用户最近看过“足球、英超、转会”这些标签那么系统会去找这些标签上重叠最多的新闻。这里可以先把用户历史行为转换成标签特征再和 news_tag 表做匹配。下面是一个用 PHP 实现的简易余弦相似度函数function cosineSimilarity(array $userVec, array $newsVec): float { $dot 0.0; $modA 0.0; $modB 0.0; foreach ($userVec as $tag $weight) { if (isset($newsVec[$tag])) { $dot $weight * $newsVec[$tag]; } $modA $weight * $weight; } foreach ($newsVec as $weight) { $modB $weight * $weight; } if ($modA 0.0 || $modB 0.0) { return 0.0; } return $dot / (sqrt($modA) * sqrt($modB)); }这个函数接受两个关联数组键是标签名值是权重。$dot是两个向量点乘的结果只有两个向量同时存在的标签才会累加。分母通过对各自权重求平方和再开根号得到模长。如果用户向量或新闻向量为空直接返回 0。在项目里我把用户向量的来源定义为最近 7 天收藏和分享的新闻标签累加权重新闻向量直接来自 news_tag 表。3. 从埋点到接口新闻推荐系统的可用化改造3.1 前端行为埋点上报点击和浏览推荐要工作第一步必须有行为数据。项目前端会在新闻列表页和详情页写入一段 JS把用户浏览的新闻 ID 上报给后端。function reportBehavior(newsId, behaviorType) { const img new Image(); img.src /api/report.php?news_id newsId behavior_type behaviorType user_id localStorage.getItem(user_id); } document.addEventListener(DOMContentLoaded, function () { document.querySelectorAll(.news-item).forEach(function (el) { el.addEventListener(click, function () { reportBehavior(el.dataset.newsId, 2); }); }); });这里用Image对象上报而不是 fetch是为了避免影响主流程同时天然绕过跨域问题。behavior_type由调用方传入1 表示页面停留超过阈值后的浏览2 表示点击进入详情3 和 4 由收藏/分享按钮触发。上报接口只需把参数写入 user_behavior 表。由于上报请求不关心返回体可以直接返回 1px GIF 图片。3.2 候选集融合排序解决行为稀疏和冷启动同时拿到行为召回新闻列表和内容匹配新闻列表后需要合并。我的做法是行为召回列表限制 50 条内容匹配列表限制 30 条然后对每个新闻的分数进行最大最小归一化再按 0.6 和 0.4 的权重融合。function mergeCandidates(array $behaviorList, array $contentList, array $weight [0.6, 0.4]) { $candidates []; foreach ($behaviorList as $newsId $score) { $candidates[$newsId][behavior_score] $score; $candidates[$newsId][content_score] 0; } foreach ($contentList as $newsId $score) { if (!isset($candidates[$newsId])) { $candidates[$newsId][behavior_score] 0; } $candidates[$newsId][content_score] $score; } $final []; foreach ($candidates as $newsId $scores) { $normBehavior $scores[behavior_score] / max($behaviorList [1]); $normContent $scores[content_score] / max($contentList [1]); $final[$newsId] $weight[0] * $normBehavior $weight[1] * $normContent; } arsort($final); return $final; }max($behaviorList [1])用来取行为得分最大值同时避免空数组时除以 0。归一化后两个分数都在 0-1 区间加权求和后按降序排列。这个代码的调参空间就在$weight上行为数据充足时可以加大$weight[0]新用户行为极少时建议把内容匹配权重提高到 0.8否则推荐结果会抓瞎。3.3 推荐接口设计让前端只关心列表输出项目里推荐接口可以用 recommend.php 实现路径类似/api/recommend.php?user_idxxxlimit20。接口内部调用上面两步召回和融合逻辑最后返回 JSON 给前端。$userId (int)$_GET[user_id]; $limit isset($_GET[limit]) ? min((int)$_GET[limit], 30) : 20; $behaviorList getBehaviorScoreList($userId); $contentList getContentMatchList($userId); $mergedList mergeCandidates($behaviorList, $contentList); $ids array_slice(array_keys($mergedList), 0, $limit); $result []; foreach ($ids as $id) { $result[] [ news_id $id, rank_score round($mergedList[$id], 4), ]; } header(Content-Type: application/json; charsetutf-8); echo json_encode($result);这段代码里limit限制最大 30防止前端一次拿太多数据导致响应缓慢。rank_score是最终融合得分前端可以按这个值做“相关推荐”文字展示。如果想让接口更安全这里就需要接入用户 ID 的加密校验这正好对应项目里的 XXTEA 文件。3.4 用 Froala 编辑器配合 Bootstrap 搭建设备后台新闻推荐系统的后台和前台分开。前台用 bootstrap.min.css 和 animate.css 实现新闻卡片、布局动画后台发布新闻时项目引入了froala_editor.pkgd.css以及对应的 JS。Froala 是一个成熟所见即所得富文本编辑器适合直接粘贴带格式的新闻内容。我在集成时发现一个问题Froala 自带的 CSS 会覆盖 Bootstrap 的按钮样式所以需要在编辑页把两者的加载顺序定为先 Bootstrap再 Froala最后是自己的 site.css。link relstylesheet hrefstatic/bootstrap.min.css link relstylesheet hrefstatic/froala_editor.pkgd.css link relstylesheet hrefstatic/site.csssite.css专门用来调整编辑区到 Bootstrap 栅格之间的间距。Froala 生成的内容保留在content字段正文发布后前端拿到这段 HTML 直接渲染不需要额外转义文章格式。4. 用 XXTEA 加密与前端框架补齐生产级细节4.1 用户 ID 不加密会有什么问题推荐接口如果直接用明文 user_id用户把 102 改成 103 就能看到别人的推荐列表。毕业设计往往忽略这一点但项目里出现了php_xxtea.c和xxtea.c说明作者有意对参数做加密。XXTEA 是 Tiny Encryption Algorithm 的扩展版算法轻量适合嵌入 PHP 扩展层也可以很方便地用在 URL 参数签名里。它解决的不是推荐准确率问题而是用户标识的可伪造性问题。4.2 编译 php_xxtea 扩展的常见做法在 Linux 环境下拿到php_xxtea.c和xxtea.c后先确认两个文件在同一目录然后执行手动编译流程。项目里的merge.bat是 Windows 下用来把两个 C 源文件合并成一个php_xxtea.c的辅助脚本方便在 Windows 下用 phpize 编译扩展。常见命令如下phpize ./configure --with-php-config/usr/bin/php-config make sudo make install编译完成后在 php.ini 中增加一行extensionxxtea.so然后重启 PHP-FPM。注意php_xxtea.c编译出的扩展名一般是xxtea.so不是php_xxtea.so。装好之后用php -m | grep xxtea验证扩展是否加载。如果编译报错多半是 phpize 与当前 PHP 版本不一致优先检查php-config路径。4.3 用 XXTEA 加密推荐接口参数安装扩展后代码里就可以直接使用xxtea_encrypt和xxtea_decrypt。下面是在推荐接口中接入加密校验的示例$key your-secret-key-here; $encryptedUserId $_GET[token] ?? ; $plain xxtea_decrypt(base64_decode($encryptedUserId), $key); if ($plain false || !is_numeric($plain)) { http_response_code(403); exit; } $userId (int)$plain;前端在拉取推荐时需要先从某个登录接口获取加密后的 token$userId 102; $token base64_encode(xxtea_encrypt((string)$userId, $key)); $url /api/recommend.php?token . urlencode($token);这里一定用base64_encode把二进制密文转成 URL 安全字符串直接用xxtea_encrypt返回的原始值很容易丢字符。密钥要放在 PHP 配置文件里不要写在前端脚本。解密失败直接返回 403避免对非法请求做后续查询。4.4 前端资源如何与推荐数据配合项目中bootstrap.css、bootstrap.min.css、animate.css两个版本并存很容易在页面上出现样式冲突。我的处理方式是页面只用 bootstrap.min.css 和 animate.css去掉未压缩的 bootstrap.css避免开发环境下加载两份一样的样式。froala_editor.pkgd.css只出现在后台编辑页。site.css是自定义样式用来覆盖 Froala 编辑区域的字体和图片尺寸保证发布出来的新闻详情页与前台卡片风格一致。推荐列表渲染时用 Bootstrap 的栅格系统配合 animate.css 的fadeInUp动画一个简单的新闻卡片组就能在移动端和桌面端同时有较好表现。5. 冷启动、评估指标与日志排错推荐系统的“最后一公里”5.1 冷启动处理新用户和新新闻的兜底策略新用户没有任何行为行为召回必然为空。我的处理方案是在召回阶段检测到行为列表为空时直接退回全站热门新闻列表。热门新闻的定义可以简单用最近 24 小时浏览人数SQL 如下SELECT news_id, COUNT(DISTINCT user_id) AS uv FROM user_behavior WHERE behavior_type IN (1,2) AND created_at DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY news_id ORDER BY uv DESC, MAX(created_at) DESC LIMIT 20;新新闻没有浏览行为可以在news表发布时将publish_time和status作为过滤条件在内容匹配召回时额外加入最新 10 条新闻保证它们有机会进入候选集避免冷启动期间永远只推荐旧内容。5.2 离线评估一个简单的推荐命中率脚本为了评估推荐效果我在项目里加了一个离线校验脚本evaluate.php思路是把过去 7 天用户的点击行为分成两类前 6 天作为训练数据生成推荐列表最后 1 天作为测试数据看测试日用户真正点击的新闻里有多少条出现在推荐列表中。$hit 0; $total 0; foreach ($users as $userId) { $recommendIds getRecommendation($userId, 20); $realClickIds getRealClickIds($userId, $testDayStart, $testDayEnd); foreach ($realClickIds as $newsId) { $total; if (in_array($newsId, $recommendIds)) { $hit; } } } $hitRate $total 0 ? $hit / $total : 0;运行脚本后我会打印出hit_rate。当命中率低于 3% 时优先检查行为日志是否完整命中率在 5% 到 10% 之间算正常超过 15% 就要怀疑是不是推荐结果只推了爆款导致用户不看也得看。5.3 日志埋点缺失与时间窗口的坑最常见的问题是用户在列表页停留很久却只有一条浏览记录原因是埋点只在 DOMContentLoaded 时触发一次没有监听滚动。另一个问题是时间窗口设置得太长比如一个月用户在暑假前看考研资料开学后全推考研题。项目里把行为窗口设置为 7 天但新闻发布频率高的时段建议缩短到 3 天。日志排查时我一般直接查 user_behavior 表里最近 1 小时的数据量如果总量过少先查 Nginx 访问日志确认/api/report.php是否被浏览器拦截。5.4 把 X 因素压缩到配置项而不是改代码推荐系统的融合权重、行为窗口、各类行为权重都应该放到一个 PHP 配置数组中统一管理。这样在答辩时可以直接演示调整参数后推荐列表的变化而不用临时改查询语句。项目里这套系统已经有这个雏形我把它进一步细化成了recommend_config.php内容包含紧凑版参数说明return [ behavior_window_days 7, behavior_weight [1 0.5, 2 1.0, 3 2.0, 4 3.0], merge_weights [0.6, 0.4], candidate_limit 50, content_match_limit 30, fallback_hot_limit 20, ];每次调整后重新调用接口马上就能看到结果变化。如果项目最终要打包上交merge.bat配合site.css、froala_editor.pkgd.css这些前端资源能让你快速在另一台机器上重新编译扩展、搭建后台、跑通推荐接口而不需要逐个文件排查依赖。本文还有配套的精品资源点击获取