做文献搜索系统这个方向的人不少,但大多数一开始把劲全使在“搜索”两个字上,结果做出来的是一个只能按标题精确匹配的文献管理后台。我见过好几个毕业设计都栽在这里:界面做得花团锦簇,一到真正检索文献时就露馅,分页乱了、关键词高亮没有、高级筛选形同虚设。这个课题的全称是“Nodejs+vue文献搜索系统的设计与实现”,名字里其实已经划定了技术边界——后端用 Node.js,前端用 Vue,前后端分离。这个组合非常典型,也非常适合拿来练手,但正因为典型,网上能抄的代码太多,反而很少有人讲清楚“为什么这么做”以及“怎么做才不像玩具项目”。
这篇文章我会从需求拆解、技术选型、数据库设计、核心代码实现、环境配置、部署调试几个维度,把整套系统从头到尾过一遍。适合正在做这个课题的学生,也适合打算从零接触 Node.js + Vue 前后端分离的开发者。我不打算给你一份纯代码抄写手册,而是把设计思路和实操中真正会踩的坑一并讲透,让你做完之后不仅能把论文写漂亮,还能在答辩时解释清楚每一个关键决策。
1. 项目拆解:文献搜索系统到底在解决什么问题
1.1 这个系统的真实使用场景
文献搜索系统听起来很学术,实际上它的使用场景非常具体:用户需要在一个文献库中,通过标题、作者、关键词、摘要、分类等维度,快速找到自己想要的文献,并进一步查看详情、下载原文或收藏备用。和百度搜索不一样,文献搜索讲究的是精确匹配与多条件组合过滤,光靠一个模糊查询根本撑不起业务逻辑。
举例来说,一个用户输入“机器学习”,他想要的可能是标题里带“机器学习”的文献,也可能是摘要里提到“机器学习”的论文,还可能是某个作者的研究方向。不考虑学科分类的检索就是一锅粥。因此系统必须提供的核心能力包括:多字段检索、分词后的模糊匹配、结果分页、按时间或相关度排序、以及配套的文献管理功能(录入、编辑、删除、分类、收藏)。
从开发角度看,这就是一个典型的前后端分离项目。Node.js 负责提供 RESTful API,Vue 负责渲染页面。文献搜索的核心业务落点在 API 设计上,前端只是把参数组合好发给后端,再把结果渲染成列表。很多学生把这个逻辑做反了,前端把全表数据查出来再内存过滤,导致文献量一大页面就卡死,这是绝对要避免的。
1.2 必须拆出来的功能模块
我在设计这类系统时喜欢画一张功能清单,先把角色和操作边界划清楚,再考虑实现。这个课题至少需要分三个角色:游客、注册用户、管理员。游客可能只允许浏览和搜索,注册用户可以收藏文献,管理员负责文献的录入、修改、分类管理。
核心功能模块大致有:
- 文献检索模块:支持标题、作者、关键词、摘要的多字段模糊匹配,支持组合筛选,返回分页结果。
- 文献详情模块:展示文献的完整元数据,包括发表期刊、年份、卷期、页码、DOI、摘要全文等。
- 分类管理模块:按学科或研究方向维护文献分类树,检索时可以限定分类范围。
- 用户管理模块:注册、登录、会话保持,以及权限控制。
- 收藏模块:注册用户可以对文献进行收藏,并在个人中心查看收藏列表。
- 文献管理模块:管理员后台的增删改查,通常需要单独的前端路由和接口。
在答辩时,你要能明确说出每个模块对应的页面、接口和数据表,而不是笼统地说“我这个系统能搜索和登录”。模块边界越清楚,系统和论文的可写性都越强。
1.3 一条文献从录入到被搜索的全过程
很多初学者对“数据从哪来”没有概念。实际项目中,文献数据要么通过批量导入,要么由管理员手动录入。以管理员录入为例,前端提交一篇文献的标题、作者、分类ID、摘要等信息到后端,后端对参数做校验后写入数据库,随后这条数据就进入了检索范围。
当用户在前端搜索框输入关键词并点击搜索时,前端会发出一个带有 query 参数的请求,后端通过 SQL 的 LIKE 语句或专门的全文检索能力去匹配数据,按相关度或时间排序后返回当前页的数据。用户点击某条结果,前端路由跳转到详情页,把文献ID传给详情接口,后端返回完整数据渲染页面。整个过程看起来简单,每个环节都有大量细节要处理,比如参数效验、SQL注入防范、搜索关键词的空格处理、分页参数越界等等,这些会直接影响系统的稳定性和答辩时专家提问的观感。
2. 技术选型:为什么是Node.js + Vue,而不是别家
2.1 选型思路的层层拆解
课题名称里已经指定了 Node.js 和 Vue,但选型背后仍然有值得展开的逻辑。Node.js 最大的优势是前后端语言统一,JavaScript 一套语法贯穿到底,学生不需要同时掌握两套语言体系。传统 Java 后端配合 Vue 前端当然可以,但开发链路更重,部署也要依赖 JDK、Tomcat,对于一个本科毕设来说学习成本偏高。
Node.js 搭配 Express 框架,写一个 RESTful API 非常快。以文献列表接口为例,JAVA 需要写实体类、Mapper、Service、Controller,还要配数据库连接池;Node.js 里用一个路由回调加上 knex 或 mysql2 数据库驱动就行,整个链路更短,出错时排查也直观。Vue 这边更不用纠结,它本身已经是国内企业级前端的事实标准,组件化开发让你把文献列表、搜索表单、分页组件拆开写,维护成本远低于 jQuery 时代的字符串拼接页面。
2.2 后端的框架选择:Express、Koa 还是 Egg
选 Node.js 后端框架时,很多学生会纠结。我的建议很明确:非特殊需求不要用 Koa,直接选 Express 或 Egg。
Koa 是 Express 原班人马打造的新一代框架,洋葱模型非常优雅,但它把很多内置功能拆成了独立中间件,教学过程中会增加许多抽象概念。而对一个文献搜索系统来说,Express 的中间件机制和路由系统足够强大,社区资料多,遇到问题搜一下就有答案。Egg 则是在企业中常用的企业级框架,约定优于配置,但它的目录结构比较复杂,不适合快速上手。
我自己做这类项目时会选择 Express 4.18 以上版本,自带 body-parser 能力,配合 mysql2 使用 Promise 连接池,写出来的代码清晰可维护。如果你打算后续扩展功能,比如加一个爬虫定时抓取文献数据,Express 也不会是瓶颈。
2.3 前端版本与 UI 组件库
Vue 的主版本选择同样是个常见纠结点。Vue 2 生态成熟,Element UI 组件库稳定又完整,很多老项目都在用。Vue 3 配合 Element Plus 是目前的趋势,Composition API 让逻辑复用更方便,而且 Vite 构建速度远快于 Webpack。如果是从零开始,我会直接选 Vue 3 + Element Plus + Vite,不仅更现代,答辩时也有话聊,比如“为什么不用 Webpack 而用 Vite——因为 Vite 基于原生 ES Module,冷启动更快”。
UI 组件库建议不要花太多时间自己写按钮和表格。Element Plus 的 el-table、el-pagination、el-form、el-input 等组件覆盖了文献搜索系统需要的全部交互场景。唯一需要你动手写的是搜索结果列表的展示布局和分页状态联动。
2.4 数据库选择:MySQL 还是 MongoDB
文献数据是典型的结构化数据:标题、作者、期刊、年份、摘要、分类、下载链接,字段相对固定,彼此还有外键关系(比如文献和分类是多对一关系)。这种情况下用 MySQL 是稳妥的选择。
MongoDB 虽然做模糊搜索舒服一些,但在多表关联、事务处理、以及答辩时被问“如何保证数据一致性”这类问题上会比较被动。MySQL 配合 InnoDB 引擎足够稳定,所有毕设项目里的常用架构都会讲到,参考资料也更丰富。搜索性能方面,数据量在十万级以下时,MySQL 的 LIKE '%xxx%' 配合合理的索引策略完全能扛住,不需要一上来就引 Elasticsearch,那是过度设计。
3. 数据库设计与API设计:系统的地基工程
3.1 核心表结构设计
数据库设计是这类系统里最值得花时间的地方,表建不好后面所有接口都难受。以文献搜索系统为例,最少需要这几张表:
- 用户表(sys_user):id、username、password、nickname、role、create_time
- 文献表(paper):id、title、authors、abstract、keywords、category_id、source、year、volume、issue、pages、doi、file_url、create_time
- 分类表(category):id、name、parent_id,用于构建树形分类结构
- 收藏表(favorite):id、user_id、paper_id、create_time,联合唯一索引保证用户不重复收藏同一篇文献
- 浏览记录表(可以视需求取舍,做统计或者首页推荐时很好用,也是一个答辩加分项)
写建表语句时一定要加上合适的索引。文献表的 title、author、keywords 字段要加普通索引,因为搜索时大概率会用到。如果按年份排序,year 字段也可以配合组合索引。分类表的主键要自增,parent_id 设默认值为 0,表示顶级分类。
3.2 搜索接口的参数设计
搜索接口是最核心的接口,参数设计会直接影响前后端联调的体验。我建议采用以下风格:
GET /api/paper/search 参数: keyword 搜索关键词,可空,空代表不限定 categoryId 分类ID,可空,精确匹配 author 作者,可空,模糊匹配 year 年份,可空,精确匹配 page 当前页码,默认1 pageSize 每页数量,默认10返回 JSON 结构统一为:
{ "code": 200, "message": "success", "data": { "total": 126, "list": [ { "id": 1, "title": "基于深度学习的图像识别综述", "author": "张三", "year": "2023" } ] } }后端在拿到 keyword 时需要做一个标准化处理:先 trim 去掉首尾空格,再判断是否为空字符串。千万不要让空关键词进入 LIKE 查询,会导致全表扫描。分页参数要校验正整数,page 不能小于 1,pageSize 建议限制在 100 以内,避免有人一次性拉全表。
3.3 关键词搜索的实现方案
MySQL 下最自然的实现是 LIKE 拼接。一个基础查询写法如下:
WHERE 1=1 AND (title LIKE '%机器学习%' OR keywords LIKE '%机器学习%' OR abstract LIKE '%机器学习%') AND category_id = ? ORDER BY year DESC, id DESC LIMIT ?, ?这里有一个重要的点:优先匹配 title 的文献要排到前面,可以在排序时加上一个相关性计算的字段。比如用 CASE WHEN 判断 title 是否命中,命中为 1,否则为 0,再按这个字段降序。这样搜“机器学习”时,标题里带这个词的文献会排在最前,体验比单纯按年份排序好很多。
如果数据量超过几十万条,LIKE '%keyword%' 的前置通配符会让索引失效,那就需要考虑中文分词方案,比如引入全文索引、或接入 Elasticsearch。对于毕业设计级别的数据规模,完全不需要这一步,但你要在论文里提一句“系统采用了基于数据库模糊检索的方案,适用于中小规模文献库,未来可扩展至全文检索引擎”,这会显得你思考过边界。
4. 核心环节实操:把系统从零跑起来
4.1 Node.js 环境搭建与 npm 脚本执行限制
很多人第一步就卡在环境上。Windows 下安装 Node.js 后,打开终端执行npm -v可能报这样一个错:
npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这个问题的根源是 Windows PowerShell 的执行策略限制。Node.js 自带的 npm 命令本质是一个.ps1脚本,而系统默认策略会拦截这类脚本的执行。解决办法有三个:
第一种,以管理员身份打开 PowerShell,执行:
Set-ExecutionPolicy RemoteSigned然后输入 Y 确认。RemoteSigned 表示本机脚本可以运行,从互联网下载的脚本需要有数字签名。
第二种,如果你不想改全局策略,也可以在项目目录下单独执行:
powershell -ExecutionPolicy Bypass -Command "npm install"第三种,完全绕开 PowerShell,改用 CMD 或 Git Bash。CMD 下执行 npm 不存在 PowerShell 的执行策略问题。我自己最常用的其实是 VSCode 的集成终端选择 Command Prompt 或 Git Bash,省心思。
装好之后建议顺便换一下 npm 镜像源,国内直接装依赖大概率会卡住:
npm config set registry https://registry.npmmirror.com后面 install 的速度和稳定性会好很多。
4.2 后端工程初始化与核心接口实现
后端我通常建一个server目录,用 Express 初始化。第一步是npm init -y,然后安装依赖:
npm install express mysql2 cors jsonwebtoken bcryptjscors是为了解决跨域问题。开发时前端跑在 5173 端口,后端跑在 3000 端口,跨域是必然的。用cors中间件一行解决:
const cors = require('cors'); app.use(cors());数据库连接部分,使用 mysql2 的 Promise 接口:
const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: '123456', database: 'literature_search', waitForConnections: true, connectionLimit: 10, queueLimit: 0, });搜索接口的完整实现我给一个简化版,重点是动态 SQL 的构建方式:
const buildSearchSql = (params) => { let sql = 'SELECT id, title, authors, year, abstract FROM paper WHERE 1=1'; const conditions = []; const values = []; if (params.keyword) { const kw = `%${params.keyword}%`; conditions.push('(title LIKE ? OR keywords LIKE ? OR abstract LIKE ?)'); values.push(kw, kw, kw); } if (params.categoryId) { conditions.push('category_id = ?'); values.push(params.categoryId); } if (params.author) { conditions.push('authors LIKE ?'); values.push(`%${params.author}%`); } if (conditions.length > 0) { sql += ' AND ' + conditions.join(' AND '); } sql += ' ORDER BY CASE WHEN title LIKE ? THEN 0 ELSE 1 END, year DESC, id DESC'; values.push(`%${params.keyword}%`); return { sql, values }; };这里要特别注意一个隐藏bug:如果 keyword 为空,ORDER BY CASE WHEN title LIKE ?那里还会推入一个空字符串的模糊匹配,所有记录的 ELSE 分支相同,不会影响排序,但会导致额外的计算。更干净的做法是对 keyword 是否为空做二次判断,动态拼接排序子句,不要无脑加参数。
返回结果的 total 也很关键。你需要先执行SELECT COUNT(*) FROM paper WHERE ...再执行分页查询,不能先查出来再取数组长度,因为那只是当前页的长度。我在项目里会把两个查询放进一个事务批次里,避免数据在两次查询之间发生变化。
4.3 前端初始化、路由与搜索页面
前端部分用 Vite 创建 Vue 3 项目:
npm create vite@latest web -- --template vue cd web npm install npm install element-plus vue-router axios路由设计直接对应功能模块:
const routes = [ { path: '/', component: Home }, { path: '/search', component: SearchPage }, { path: '/paper/:id', component: PaperDetail }, { path: '/login', component: Login }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'admin' } }, ];这里就要讲到“vue路由参数”这个热点。搜索结果点击进入详情页时,路径是/paper/123,前端用route.params.id拿到文献 ID,再请求详情接口:
const id = route.params.id; const res = await axios.get(`/api/paper/${id}`);注意,在 Vue Router 4 里,this.$route.params.id这种 Options API 写法依然可用,但如果用 Composition API,需要在 setup 中通过useRoute()获取。千万不要直接在 onMounted 里同步读 route 然后在异步回调里沿用已经过期的值,要用watch监听route.params.id的变化,这样从一篇文献跳到另一篇时页面才会正确刷新。
搜索页面的核心是表单数据绑定和请求参数组装。我会用 reactive 对象保存搜索条件:
const searchForm = reactive({ keyword: '', categoryId: null, author: '', year: '', page: 1, pageSize: 10, }); const fetchSearch = async () => { const params = { ...searchForm }; Object.keys(params).forEach((key) => { if (params[key] === '' || params[key] === null) delete params[key]; }); const res = await axios.get('/api/paper/search', { params }); total.value = res.data.data.total; list.value = res.data.data.list; };搜索结果用el-table展示或者自定义卡片列表都可以。最常见的坑是切换页码后忘记把scrollTop滚回顶部,用户翻第 2 页还停留在第 1 页的滚动位置,体验很糟糕。所以每次分页变化后要执行window.scrollTo(0, 0)。
4.4 前后端联调与代理配置
开发阶段前后端分离,直接让前端请求http://localhost:3000/api/...也不是不行,但一旦接口路径调整,要改的地方很多。更优雅的方案是在 Vite 里配置代理,让前端请求相对路径/api时自动转发到后端:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, });这样前端代码里axios.get('/api/paper/search')会由 Vite 开发服务器代理到http://localhost:3000/api/paper/search。上线部署时后端接口地址变了,只要在 Nginx 层同样配置/api代理即可,前端代码一行都不用改。
联调阶段最容易出现的另一个问题是字段大小写不一致。我见过后端返回username,前端却用userName去取,取出来是 undefined,页面上一堆空数据,排查半天才发现是命名差异。解决方法是提前约定好前后端字段命名风格,要么全用驼峰,要么全用下划线,不要混用。我的习惯是接口 JSON 统一用小驼峰,数据库字段统一用下划线,在 SQL 查询时通过 AS 别名把create_time映射成createTime。
5. 常见问题与排查技巧实录
做这个系统时,下面这一批问题是出现频率很高的,我按实际处理经验整理成了一个表,方便你对照排查。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| npm 命令无法执行,报 powershell 执行策略错误 | Windows 默认禁止运行 .ps1 脚本 | 管理员执行Set-ExecutionPolicy RemoteSigned,或者改用 CMD |
| 前端请求后端接口提示跨域 | 前后端端口不一致,未配置 CORS | 后端引入cors中间件,或前端配置 Vite 代理 |
| 搜索中文关键词返回空结果 | 数据库连接字符集不是 utf8mb4 | 创建库时指定 character set,连接字符串加charset=UTF8MB4_UNICODE_CI |
| 分页显示总条数不对 | 先查列表后统计条数,拿到的是当前页数量 | 单独执行 COUNT 聚合查询,不要直接取列表长度 |
| 输入关键词后搜索很久才出结果 | 大字段 LIKE 无索引,且查询条件过多 | 对 title、keywords 建立索引,精简 OR 条件 |
| 页面点击搜索后表单自动刷新 | button 默认 type 为 submit 触发表单提交 | 设置type="button",或调用 preventDefault |
| Vue 路由跳转后页面数据不刷新 | 没有监听路由参数变化 | 使用watch监听route.params.id |
| 请求返回 401,登录后依然失败 | Token 没存到 localStorage 或者请求头没带 | 检查 axios 拦截器是否设置了Authorization: Bearer token |
| 详情页刷新后 404 | 前端路由用了 history 模式,Nginx 未做 fallback | Nginx 配置try_files $uri $uri/ /index.html; |
| 生产环境静态资源加载 404 | 前端 build 后 base 路径不对 | Vite 设置base: './',或 Nginx 指定正确 root 目录 |
5.1 npm 脚本执行被禁止的后遗症
除了前面提到的执行策略,还有一个连带问题。很多人在设置完执行策略后依然发现npm install报红,原因可能是 npm 缓存模块损坏。这种情况别急着重装 Node,先执行:
npm cache clean --force如果网络下载很慢,再检查 registry 镜像。我遇到过最头疼的一个问题是镜像源指向了旧地址,安装 Express 时一直 404。后来分析日志才发现 registry 配的是https://registry.npm.taobao.org这个已经失效的域名。现在统一改用https://registry.npmmirror.com就没出过问题。
5.2 Vue DevTools 和调试工具
前端调试几乎离不开 Vue DevTools。安装时很多人直接在扩展商店搜,但版本和浏览器内核不匹配会导致按钮灰色无法启用。Vue 3 项目要安装对应的 Vue.js Devtools 版本,扩展安装完后还需要重启浏览器。如果图标还是不亮,检查页面是否运行在 http/https 环境下,直接双击本地 html 文件打开的是 file:// 协议,DevTools 默认不会注入。
调试搜索接口时,别光看页面效果,用浏览器的 Network 面板看请求 URL 和参数列表。我最常教学生的排查路径是:先看请求有没有发出去,再看请求参数对不对,然后看后端返回的 JSON 结构,最后才看渲染逻辑。很多前后端对接问题都出在中间这三步。
5.3 搜索性能和数据库连接优化
有一回我帮一个学生调系统,文献表里导入了 3 万条测试数据,搜索“卷积神经”竟然用了 4 秒。分析后发现他给表加的是单列索引,但查询条件是title OR keywords OR abstract,MySQL 优化器根本用不上索引,三个字段都做全表扫描。
我帮他改成了组合索引方案,把 title 和 keywords 做成一个复合索引,同时在应用层做了一次字段选择,不让 abstract 参与模糊匹配除非用户明确勾选“搜索摘要”。这样查询时间从 4 秒降到了 300 毫秒以内。这说明一个道理:搜索接口不是简单拼 SQL,而是要把业务需求和数据库特性结合起来做取舍。
另外,数据库连接用完要释放。如果你用 mysql2 的连接池,connection.query之后别忘了connection.release(),或者直接用pool.query让连接池内部管理生命周期。不释放连接的话,连接池很快被打满,系统就表现为“用着用着接口全部超时”,这个问题在产品环境特别容易踩。
5.4 管理员后台和 Electron 打包的零碎问题
如果做的是增强版,需要把整个项目打包成桌面应用,可能会用到 Electron。Electron 打包 Vue 项目时,最常见的是路径问题。打包后页面白屏,打开控制台发现资源加载路径不对,原因就是 Vite 打包后默认使用绝对路径/assets/...,在 Electron 的 file:// 协议下解析失败。解决办法是在 vite.config.js 里把base设为'./',让资源加载改为相对路径。
还有主进程和渲染进程通信的问题。有热词问“electron 主渲染进程 ipc 通信和 vue 有关系吗”,答案是:Electron 的 IPC 通信与 Vue 本身无关,Vue 只是运行在渲染进程里的 UI 框架,通信还是要通过ipcRenderer和ipcMain两个模块。别想着用 Vue 的响应式数据直接跨进程通信,数据必须通过 IPC 的通道传递,这个坑绕不开。
在开发环境里,electron 主进程需要通过loadURL('http://localhost:5173')加载 Vite 开发服务器地址;生产环境才用loadFile('dist/index.html')加载打包后的文件。这两者逻辑要分开写,否则 debug 的时候一片黑屏。
6. 从毕业设计到可落地项目:几个容易被追问的问题
答辩时老师大概率会问几个“为什么”和“如果”:
“如果用户搜索时输入了特殊字符,比如单引号,会不会出问题?” 这里考察的是 SQL 注入。使用参数化查询后,SQL 语句结构固定,输入值只作为数据处理,不会改变语句逻辑,因此单引号等特殊字符不会造成注入。这个问题你可以直接流利地回答出来,因为用的就是 mysql2 的
?占位符。“文献数据量达到千万级别,现在的搜索方案还能用吗?” 诚实的回答是性能会明显下降,因为
LIKE '%keyword%'无法利用 B+ 树索引。你可以说系统架构上预留了扩展接口,未来可引入 Elasticsearch 或 MySQL 全文索引。这个回答展示了边界认知,比假装无敌要加分。“分类表为什么要设计成自关联?” 自关联允许无限层级分类,比如“计算机 -> 人工智能 -> 机器学习”,如果只用单一分类字段就实现不了这个层次结构。前端渲染时可以递归遍历分类树,后端查询子分类文献时也可以用递归查询或者一次性取出全部分类后在内存中构建树。
“收藏功能怎么防止重复数据?” 讲清楚唯一索引就行。在 favorite 表里给 user_id 和 paper_id 加联合唯一索引,数据库层面保障,即使前端重复点击也不会产生脏数据,应用层再配合一个查询去判断当前状态。
这些问题并不可怕,前提是你真的自己动手实现了这些功能,而不是只会背八股。如果项目是团队协作,或者你想让它看起来更完整,建议补一个简单的日志中间件记录每次搜索的关键词和耗时,这不仅是运维友好的体现,还能做搜索热词分析,哪怕只是展示在管理员面板里,也是一个很不错的亮点。
6.1 路由守卫与权限控制的落地
前端做权限控制,最常见的就是路由守卫。Vue Router 提供了全局前置守卫,可以在每次路由跳转前检查用户 token 和角色:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.role === 'admin' && localStorage.getItem('role') !== 'admin') { next('/'); } else { next(); } });但要注意,前端的路由守卫只能控制页面显示,真正的权限校验必须发生在后端。每次请求管理员接口时,后端都要验证 JWT 的签名和角色信息。否则别人拿个普通用户的 token 直接调删除接口,数据照样会被删掉,前端 guard 形同虚设。这个点至少要在论文的安全性分析里写一段。
6.2 登录状态保持与 Token 刷新
登录接口是后端实现的,通常流程是用户提交账号密码,后端用 bcryptjs 对比哈希密码,验证通过后签一个 JWT 返回前端。前端把 token 存到 localStorage 里,并在 axios 拦截器里带上:
axios.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });这个方案简单直接,要注意 JWT 的过期时间不能太长,也不能太短。我用的习惯是 2 小时过期,管理员操作如果中途过期就重新登录。如果想让体验更好,可以做 refresh token 机制,但这是加分项,不是必需项。对毕设项目来说,能用 JWT 把登录注册做通的已经超过一半人了。
6.3 一个完整项目的目录结构参考
最后给出一个我觉得顺手且适合论文描述的项目目录结构:
literature-search/ ├── server/ # 后端工程 │ ├── app.js # 入口 │ ├── routes/ # 路由定义 │ ├── controllers/ # 业务逻辑 │ ├── models/ # 数据库访问 │ ├── middlewares/ # 鉴权、日志等中间件 │ └── utils/ # 工具函数 ├── web/ # 前端工程 │ ├── src/ │ │ ├── api/ # 接口封装 │ │ ├── router/ # 路由 │ │ ├── views/ # 页面组件 │ │ ├── components/ # 公共组件 │ │ └── store/ # 状态管理(如需要) │ └── vite.config.js └── sql/ # 数据库初始化脚本把这个结构放进论文里的“系统实现”章节,导师一眼就能看出项目是在工程化思维下组织的,而不是三个大文件堆完的草稿。
我个人在实际操作中还有一个习惯:把每一篇文献的唯一标识(比如 DOI)在录入时就做去重校验。文献搜索系统最怕的就是库里出现大量重复数据,一旦标题和 DOI 重复,搜索结果里就会出现多条看起来一模一样的文献,用户体验非常糟糕。在后端的录入接口里加一个唯一索引约束,或者在 service 层写一个比对逻辑,虽然只多写几十行代码,但给整个系统带来的可靠性提升是非常明显的。
如果你正好卡在这个课题上,我建议你别再对着网上那些三年前的旧代码琢磨了,先按这篇文章里说的把环境搭好,再把数据库表建出来,对应着把搜索和登录两个核心流程跑通,这个系统已经完成了六成。剩下的功能都是一点点叠加的工程问题,遇到哪一个就去解决哪一个。