简介本资源是一套面向Java后端与Vue前端初学者的毕业设计级新闻推荐系统实战项目适用于计算机专业本科生课程设计、毕设参考及全栈开发入门实践。项目基于SpringBoot构建后端服务集成MySQL实现用户管理、新闻CRUD与行为日志存储采用Vue.js开发响应式前端界面并完整配套开题报告与毕业论文覆盖需求分析、系统设计、算法逻辑基于用户行为的协同过滤雏形、安全防护密码加密、SQL注入防范等关键环节。压缩包共743个文件含90个Java核心业务类、38个Vue组件、153个JS交互脚本、44个CSS样式文件及162个SVG图标资源辅以SQL建表语句、启动批处理脚本.bat和配置文件yml结构清晰、模块解耦明确整体大小为16.83MB。目前已有53人学习下载读者可直接运行调试、理解前后端联调流程、复用数据库设计与推荐模块骨架并借鉴规范的学术文档撰写范式。1. 为什么一个“新闻推荐系统”毕业设计90%的学生卡在数据冷启动和实时性断层上你手里的 SpringBoot MySQL Vue 新闻推荐系统不是在搭一个能跑通的 Demo而是在模拟一个真实内容平台最棘手的两个断层新用户没行为、新新闻没曝光冷启动以及用户刚点完一篇国际时政后台却还在推三天前的本地天气实时性断层。这不是代码写不写得出来的问题——SpringBoot 启动类三行就能跑Vue 页面用 Element Plus 拖一拖就出来。真正让答辩老师皱眉、让导师反复问“你怎么验证推荐效果”的是这背后三个被忽略的硬骨头第一MySQL 里存的不是“新闻表用户表点击日志表”而是必须带时间戳粒度到秒级的行为流水、带人工标注的新闻标签体系、以及可回溯的推荐结果快照第二Vue 前端不能只渲染标题列表得埋下用户停留时长、滚动深度、二次返回率这些隐式反馈信号第三“推荐系统”四个字在毕业论文里不能只靠协同过滤公式撑场面得用SpringBoot 的 Scheduled Redis ZSet 实现分钟级热度衰减更新否则你的“实时推荐”就是个玄学词。这篇笔记不讲 SpringBoot 自动装配原理也不教 Vue3 Composition API只聚焦一个目标让你的毕设系统在答辩现场能当场演示“用户刚搜‘AI芯片’首页立刻刷出三篇带热度权重的新文章”且所有数据链路可查、可复现、可解释。适合正在写开题报告、卡在“技术路线图怎么画才不像拼凑”或已写完代码但测试时发现推荐结果全是随机排序的同学。2. 从零建库MySQL 表结构不是 ER 图作业而是推荐逻辑的物理映射新闻推荐系统的数据库设计本质是把推荐算法的输入输出关系翻译成可索引、可关联、可回溯的物理表。很多同学直接照搬博客教程建news、user、click_log三张表结果后期加实时热度计算时发现click_log没带毫秒级时间戳加标签权重时发现news表里category字段是 varchar(50) 却存着“科技/人工智能/芯片/英伟达”这种斜杠分隔字符串——这不是字段类型问题是没想清楚每张表承载的业务语义。下面给出我在某高校毕业设计指导中验证过、能支撑后续所有推荐策略的最小可行表结构重点标出与常规设计差异点。2.1 核心四张表为什么必须拆出 news_tag_rel 和 user_profile-- 新闻主表关键在 publish_time 精确到秒且 status 字段预留审核流 CREATE TABLE news ( id bigint NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 标题, content_summary text COMMENT 摘要用于文本相似度, publish_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间精确到秒, status tinyint NOT NULL DEFAULT 1 COMMENT 1-已发布,2-草稿,3-已下架, source varchar(50) DEFAULT unknown COMMENT 来源如原创、转载-XX网, PRIMARY KEY (id), INDEX idx_publish_time (publish_time), INDEX idx_status_time (status, publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 新闻标签关联表强制多对多避免 category 字段存斜杠字符串 CREATE TABLE news_tag_rel ( news_id bigint NOT NULL, tag_id bigint NOT NULL, weight decimal(3,2) NOT NULL DEFAULT 1.00 COMMENT 该标签对本新闻的权重人工标注或NLP提取, PRIMARY KEY (news_id, tag_id), KEY idx_tag_id (tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 标签主表tag_name 必须唯一且带 level 字段支持层级如科技是L1人工智能是L2 CREATE TABLE tag ( id bigint NOT NULL AUTO_INCREMENT, tag_name varchar(50) NOT NULL UNIQUE, level tinyint NOT NULL DEFAULT 1 COMMENT 层级1为根类目, parent_id bigint DEFAULT NULL COMMENT 父标签ID用于树形展开, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户行为日志表关键在 event_type 细分和 event_time 精确到毫秒 CREATE TABLE user_behavior_log ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, news_id bigint NOT NULL, event_type varchar(20) NOT NULL COMMENT view-浏览,click-点击,like-点赞,duration-停留时长(秒),scroll_depth-滚动深度(%), event_value varchar(100) DEFAULT NULL COMMENT 事件值如duration存127.5,scroll_depth存85, event_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 事件时间精确到毫秒, PRIMARY KEY (id), KEY idx_user_time (user_id, event_time), KEY idx_news_time (news_id, event_time), KEY idx_event_type (event_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明news_tag_rel表的存在是为了让后续做基于内容的推荐Content-Based时能直接JOIN tag表做标签向量计算而不是在应用层用FIND_IN_SET解析字符串——后者在百万级数据时会全表扫描。user_behavior_log.event_type拆成view/click/duration等细粒度类型是因为不同行为代表不同强度的偏好信号一次click比三次view权重更高而duration 60s的view可能比一次click更说明兴趣深度。这些信号在后续构建用户画像时会按不同系数加权。event_time使用datetime(3)而非timestamp是因为timestamp有范围限制1970-2038且时区转换易出错datetime(3)直接存服务端时间配合 SpringBoot 的CreatedDate注解即可自动填充。2.2 开题报告里必须写的“数据字典”不是字段列表而是推荐逻辑注释在开题报告的“数据库设计”章节不要只贴 CREATE TABLE 语句。要像下面这样用表格说明每个关键字段如何服务于推荐逻辑表名字段名类型推荐场景中的作用论文里可展开的分析点newspublish_timedatetime计算新闻时效性衰减因子decay e^(-λ*(now-published))λ 参数如何通过 A/B 测试确定不同类别新闻如突发新闻 vs 深度报道是否应设不同 λnews_tag_relweightdecimal(3,2)构建新闻标签向量vector Σ(tag_embedding * weight)人工标注成本高能否用 TF-IDF 替代实验对比准确率下降多少user_behavior_logevent_typeevent_valuevarchar区分显式/隐式反馈like是强正样本duration5s视为负样本如何定义“负样本”直接删掉短时浏览记录还是赋予负权重这个表格的价值在于让导师一眼看出你不是在堆砌技术名词而是理解每个字段在推荐流水线中的位置。答辩时被问到“为什么用 datetime(3)”你就能答“因为实时热度计算需要毫秒级时间差而 timestamp 在跨时区部署时会导致 decay 因子计算偏差”。3. SpringBoot 推荐引擎别再只写 UserController核心在 RecommenderService 的三层调度很多同学的 SpringBoot 项目里RecommenderService就是一个Service类里面塞了getRecommendationsByUserId(Long userId)方法调用几个JpaRepository查询就完事。这根本不是推荐系统这是“按 ID 查数据库”。真正的推荐引擎必须有离线计算、近线更新、在线服务三层分离。我带过的某高校毕业设计中一个学生最初也是这么写的结果答辩时老师问“如果用户刚点了 5 篇体育新闻第 6 篇推荐为什么还是财经”他答不上来——因为他的服务根本没有“用户最近行为”的概念。3.1 离线层用 Scheduled MyBatis 批量计算用户兴趣向量非实时但必须可追溯Service public class OfflineRecommender { Autowired private UserBehaviorLogMapper behaviorLogMapper; Autowired private NewsTagRelMapper newsTagRelMapper; Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void buildUserInterestVectors() { // 步骤1获取过去7天有行为的用户ID避免全表扫描 ListLong activeUserIds behaviorLogMapper.selectActiveUserIds( DateUtils.addDays(new Date(), -7) ); for (Long userId : activeUserIds) { // 步骤2查询该用户7天内所有行为按event_type加权 ListUserBehaviorLog logs behaviorLogMapper.selectByUserIdAndTimeRange( userId, DateUtils.addDays(new Date(), -7), new Date() ); // 步骤3聚合行为生成用户兴趣向量简化版统计各标签出现频次 MapLong, Double tagScoreMap new HashMap(); for (UserBehaviorLog log : logs) { if (click.equals(log.getEventType()) || like.equals(log.getEventType())) { // 获取该新闻的所有标签及权重 ListNewsTagRel tags newsTagRelMapper.selectByNewsId(log.getNewsId()); for (NewsTagRel rel : tags) { double baseScore 1.0; if (like.equals(log.getEventType())) baseScore 2.0; // 点赞权重翻倍 tagScoreMap.merge(rel.getTagId(), baseScore * rel.getWeight(), Double::sum); } } } // 步骤4存入 user_interest_vector 表需提前建表 saveUserInterestVector(userId, tagScoreMap); } } private void saveUserInterestVector(Long userId, MapLong, Double tagScoreMap) { // 实际项目中这里会批量插入此处简化为单条 UserInterestVector vector new UserInterestVector(); vector.setUserId(userId); vector.setVectorJson(JSON.toJSONString(tagScoreMap)); // 存JSON字符串便于调试 vector.setUpdateTime(new Date()); userInterestVectorMapper.insert(vector); } }参数说明与踩坑提示Scheduled(cron 0 0 2 * * ?)的 cron 表达式表示每天 2:00 执行切勿写成Scheduled(fixedDelay 86400000)——后者是固定间隔若某次计算耗时 2 小时下次就会在 4:00 执行导致数据延迟。cron 是绝对时间触发更可控。selectActiveUserIds()方法必须加索引KEY idx_user_time (user_id, event_time)否则SELECT DISTINCT user_id FROM user_behavior_log WHERE event_time ?会全表扫描。vectorJson存 JSON 字符串而非序列化二进制是因为毕业论文答辩时老师可能要求你当场导出某用户的兴趣向量看内容——JSON 可读二进制不可读。3.2 近线层用 Redis ZSet 实现分钟级新闻热度更新解决“新新闻没人推”问题冷启动的核心矛盾是新发布的新闻没有用户行为协同过滤无法计算相似度。解决方案是用内容热度替代行为热度而热度必须随时间衰减。MySQL 不适合高频更新所以用 Redis ZSet。Service public class HotNewsService { Autowired private StringRedisTemplate redisTemplate; // 新闻热度Keyhot_news:20240520日期维度便于清理 private static final String HOT_NEWS_KEY_PREFIX hot_news:; /** * 当新闻被点击时更新其热度加分 */ public void incrementHotScore(Long newsId) { String key HOT_NEWS_KEY_PREFIX LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // 分数 当前分数 1ZINCRBY 原子操作 redisTemplate.opsForZSet().incrementScore(key, String.valueOf(newsId), 1.0); } /** * 每分钟执行对所有新闻热度乘以衰减因子e^-0.1 ≈ 0.9048 */ Scheduled(fixedRate 60000) public void decayHotScores() { String key HOT_NEWS_KEY_PREFIX LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // 获取当前所有新闻ID SetString newsIds redisTemplate.opsForZSet().range(key, 0, -1); if (newsIds ! null !newsIds.isEmpty()) { // 批量更新先获取原分数再乘以衰减因子 MapString, Double scoresToUpdate new HashMap(); for (String id : newsIds) { Double score redisTemplate.opsForZSet().score(key, id); if (score ! null) { scoresToUpdate.put(id, score * 0.9048); } } // 批量设置新分数注意ZAdd 是覆盖不是累加 redisTemplate.opsForZSet().add(key, scoresToUpdate); } } /** * 获取今日热度Top50新闻供首页“热点”栏目 */ public ListLong getHotNewsTop50() { String key HOT_NEWS_KEY_PREFIX LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); SetString topIds redisTemplate.opsForZSet().reverseRange(key, 0, 49); return topIds.stream() .map(Long::parseLong) .collect(Collectors.toList()); } }为什么用 ZSet 而不是 HashZSet 天然支持按分数排序reverseRange而 Hash 需要HGETALL再在 Java 层排序性能差一个数量级。ZINCRBY是原子操作多实例部署时不会因并发点击导致分数错误。衰减用fixedRate 60000而非 cron是因为热度衰减必须严格按分钟进行cron 有执行延迟风险。4. Vue 前端不是套模板而是把“用户行为”变成可上报的结构化数据很多同学的 Vue 页面就是一个el-card循环v-fornews in newsList点进去跳转router.push(/detail?idid)。这完全丢失了推荐系统最关键的隐式反馈闭环。你的前端必须主动采集、结构化、并可靠上报用户行为否则后端的推荐模型就是闭门造车。4.1 新闻卡片组件埋点不是加 onclick而是用 IntersectionObserver 监测真实曝光template div classnews-card :data-news-idnews.id !-- 卡片内容 -- h3{{ news.title }}/h3 p{{ news.summary }}/p !-- 关键用 ref 绑定 DOM 元素供 IntersectionObserver 监测 -- div refcardRef classcard-observer/div /div /template script import { onMounted, ref, defineProps } from vue import { reportExposure } from /api/behavior export default { name: NewsCard, props: { news: { type: Object, required: true } }, setup(props) { const cardRef ref(null) onMounted(() { if (!cardRef.value) return // 创建 IntersectionObserver监测卡片是否进入视口 const observer new IntersectionObserver( (entries) { entries.forEach(entry { if (entry.isIntersecting) { // 触发曝光上报用户ID、新闻ID、曝光时间、视口占比 reportExposure({ userId: localStorage.getItem(userId) || anonymous, newsId: props.news.id, exposureTime: Date.now(), visibleRatio: entry.intersectionRatio // 0.0 ~ 1.0 }) // 上报后停止监听避免重复上报 observer.unobserve(cardRef.value) } }) }, { threshold: 0.1 // 当10%区域可见时触发 } ) observer.observe(cardRef.value) }) return { cardRef } } } /script逻辑说明IntersectionObserver比onscrollgetBoundingClientRect()更高效且能精确获取intersectionRatio可见比例这是判断用户是否“真看了”的关键指标。visibleRatio 0.5的曝光比visibleRatio 0.1的权重高得多。reportExposure()是封装好的 API内部会将数据发往/api/behavior/exposure接口后端对应BehaviorController.exposure()方法最终存入user_behavior_log表event_typeexposureevent_value存 JSON 字符串{visibleRatio:0.72}。localStorage.getItem(userId)是假设你已在登录后存了用户ID若未登录则用anonymous后续做冷启动推荐时可对匿名用户统一用热度榜。4.2 详情页用 beforeRouteLeave 捕获停留时长解决“点开就关”的误判script import { onBeforeRouteLeave, getCurrentInstance } from vue import { reportDuration } from /api/behavior export default { name: NewsDetail, setup() { const startTime Date.now() onBeforeRouteLeave((to, from, next) { const duration Date.now() - startTime // 只有停留超过3秒才上报有效时长过滤误点 if (duration 3000) { reportDuration({ userId: localStorage.getItem(userId) || anonymous, newsId: getCurrentInstance().proxy.$route.params.id, duration: Math.round(duration / 1000) // 秒为单位 }) } next() }) return {} } } /script为什么不用mountedbeforeUnmountbeforeUnmount在 Vue3 中已被废弃且unmount不一定代表用户离开页面比如路由跳转时组件销毁但用户仍在应用内。onBeforeRouteLeave是 Vue Router 提供的导航守卫精准捕获用户即将离开当前路由的时刻此时startTime到当前时间的差值才是真实的页面停留时长。duration 3000的阈值是经验值低于3秒大概率是误点或快速滑走不构成有效兴趣信号。5. 避坑指南答辩时被问住的5个高频问题及其血泪答案写毕业论文最怕的不是代码写不出来而是答辩现场被导师一句“你这个设计实际跑起来会怎样”问得哑口无言。以下是我在某高校连续三年参与毕业设计答辩后整理出的5个最高频、最致命的坑每一条都来自真实翻车现场。5.1 现象推荐结果每天凌晨2点后突变之前推财经之后全推体育原因离线计算buildUserInterestVectors()每天只跑一次且未做平滑过渡。用户凌晨1:59的行为要等到2:00计算完才生效导致推荐结果在2:00整点发生“硬切换”。解决在RecommenderService.getRecommendations()中同时查询离线向量和近线行为。例如// 获取离线计算的用户兴趣向量缓存1小时 MapLong, Double offlineVector getUserOfflineVector(userId); // 获取最近1小时内用户实时行为从Redis或MySQL查 ListUserBehaviorLog recentLogs getRecentBehavior(userId, 3600); // 混合打分offlineVector * 0.7 realTimeScore * 0.3这样推荐结果是渐进式更新而非整点突变。5.2 现象MySQL 查询user_behavior_log超时show processlist显示大量Sending data原因user_behavior_log表数据量超100万后SELECT * FROM user_behavior_log WHERE user_id ? ORDER BY event_time DESC LIMIT 20没有复合索引MySQL 先扫描全表找user_id再排序最后取20条。解决必须创建联合索引ALTER TABLE user_behavior_log ADD INDEX idx_user_time (user_id, event_time);注意顺序user_id在前event_time在后。这样WHERE user_id ?能用上索引ORDER BY event_time DESC也能直接利用索引有序性无需额外排序。5.3 现象Vue 前端上报曝光行为后端user_behavior_log表里event_time时间戳比客户端晚8小时原因前端Date.now()返回的是浏览器本地时间含时区后端 SpringBoot 默认用服务器时区如Asia/Shanghai但 MySQL 的datetime字段不存时区信息直接存入会导致时间错位。解决前后端统一用 UTC 时间。前端上报时// 改为传 UTC 时间戳毫秒 reportExposure({ ...otherParams, exposureTime: new Date().getTime() // getTime() 返回 UTC 时间戳 })后端 Controller 接收后直接存入event_time字段MySQLdatetime存的是字面值不转换这样所有时间都是 UTC避免时区混乱。5.4 现象Redis ZSet 热度榜里同一篇新闻出现多次分数不一致原因incrementHotScore()和decayHotScores()并发执行。A线程刚ZINCRBY加1B线程马上ZADD覆盖旧分数导致A的1丢失。解决用 Lua 脚本保证原子性。在decayHotScores()中不先ZREVRANGE再ZADD而是用 Lua 脚本一次性完成“读-算-写”// Lua 脚本对ZSet中所有成员分数乘以0.9048 String script local members redis.call(ZRANGE, KEYS[1], 0, -1) for i,v in ipairs(members) do local score redis.call(ZSCORE, KEYS[1], v) if score then redis.call(ZADD, KEYS[1], score*0.9048, v) end end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(key));Lua 脚本在 Redis 单线程中执行彻底避免竞态。5.5 现象开题报告写“采用协同过滤算法”但代码里只有SELECT * FROM news ORDER BY RAND() LIMIT 10原因把“推荐系统”当成技术名词堆砌没想清楚算法落地需要什么数据支撑。协同过滤需要用户-物品交互矩阵而你的user_behavior_log表里只有click没有dislike或skip矩阵极度稀疏。解决在开题报告中诚实写明“因数据规模限制采用混合推荐策略”并明确分层新用户/新新闻用基于热度的HotNewsService.getHotNewsTop50()有行为的老用户用基于标签的OfflineRecommender生成兴趣向量再JOIN news_tag_rel查相似新闻有实时行为的用户用getRecentBehavior()获取最近点击直接推荐同标签新闻这样既真实又体现工程思维——不是所有场景都适合复杂算法。6. 让答辩老师眼前一亮的技巧用“推荐归因分析”功能把黑匣子变成可解释白盒答辩时最打动老师的不是你写了多少行代码而是你能说清楚“为什么给这个用户推这篇文章依据是什么” 我在指导某高校毕业设计时让学生加了一个不起眼但极有说服力的功能推荐归因面板。它不增加核心推荐逻辑只在返回推荐结果时附带一条人类可读的归因说明比如“因您过去3天点击了5篇‘人工智能’标签新闻且本文标签匹配度达0.92”。这个功能实现简单但价值巨大。6.1 后端在推荐结果 DTO 中增加reason字段// 推荐结果DTO public class RecommendationDto { private Long newsId; private String title; private String reason; // 新增归因说明 // getter/setter... } // 在 RecommenderService.getRecommendations() 中组装 public ListRecommendationDto getRecommendations(Long userId) { ListNews recommendedNews new ArrayList(); // ...原有推荐逻辑得到 newsList ... // 为每条新闻生成归因 for (News news : newsList) { String reason generateReason(userId, news); RecommendationDto dto new RecommendationDto(); dto.setNewsId(news.getId()); dto.setTitle(news.getTitle()); dto.setReason(reason); result.add(dto); } return result; } private String generateReason(Long userId, News news) { // 场景1来自热度榜 if (isFromHotNews(news.getId())) { return 热门新闻今日点击量TOP10; } // 场景2来自用户兴趣匹配 MapLong, Double userVector getUserOfflineVector(userId); if (userVector ! null) { double maxMatch 0.0; String bestTag ; // 计算新闻标签与用户兴趣向量的余弦相似度 ListNewsTagRel tags newsTagRelMapper.selectByNewsId(news.getId()); for (NewsTagRel rel : tags) { Double userScore userVector.get(rel.getTagId()); if (userScore ! null userScore maxMatch) { maxMatch userScore; bestTag tagMapper.selectById(rel.getTagId()).getTagName(); } } if (maxMatch 0) { return String.format(兴趣匹配您关注“%s”标签匹配度%.2f, bestTag, maxMatch); } } // 场景3来自实时行为 ListUserBehaviorLog recentClicks getRecentBehavior(userId, 3600); for (UserBehaviorLog log : recentClicks) { if (log.getEventType().equals(click)) { News clickedNews newsMapper.selectById(log.getNewsId()); if (clickedNews ! null hasSameTag(clickedNews, news)) { return 实时相关您1小时前点击了同类新闻; } } } return 系统推荐; }6.2 前端在新闻卡片右下角显示小标签并悬停查看完整归因template div classnews-card h3{{ news.title }}/h3 p{{ news.summary }}/p !-- 归因标签小字体灰色右下角 -- div classreason-tag clickshowReasonDialog(news.reason) {{ shortReason(news.reason) }} span v-ifnews.reason.length 15 classellipsis.../span /div !-- 归因弹窗 -- el-dialog v-modeldialogVisible title推荐理由 width40% p styleline-height: 1.6{{ currentReason }}/p template #footer el-button clickdialogVisible false关闭/el-button /template /el-dialog /div /template script export default { data() { return { dialogVisible: false, currentReason: } }, methods: { shortReason(reason) { // 截取前15个字符中文按字符数算 return reason.length 15 ? reason : reason.substring(0, 15); }, showReasonDialog(reason) { this.currentReason reason; this.dialogVisible true; } } } /script style scoped .reason-tag { position: absolute; bottom: 8px; right: 8px; font-size: 12px; color: #999; background: #f5f5f5; padding: 2px 6px; border-radius: 2px; } .ellipsis { display: inline; } /style为什么这个技巧能加分它把抽象的“推荐算法”转化成了用户可感知、可理解的决策过程符合人机交互最佳实践。它倒逼你在设计阶段就思考推荐逻辑的可解释性而不是事后补文档。答辩时老师点开任意一张卡片看到“兴趣匹配您关注‘人工智能’标签匹配度0.87”会立刻明白你的系统不是随机推荐而是有据可依。这个功能我一般会在开题报告的“创新点”里写成“提出轻量级推荐归因机制提升系统可解释性与用户信任度”不吹嘘技术难度但直击评审痛点。最后说句实在话毕业设计不是要做出工业级系统而是证明你掌握了需求拆解、技术选型、数据建模、问题排查这一整套工程能力。当你能把user_behavior_log表的索引为什么这么建、ZINCRBY为什么比HINCRBY合适、IntersectionObserver怎么避免误报这些细节讲清楚答辩就已经赢了一半。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?