ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Node.js + Vue 实战:从零搭建校园二手交易平台

Node.js + Vue 实战:从零搭建校园二手交易平台 简介一份面向计算机专业毕业设计场景的NodejsVue校园二手物品交易平台论文文档适合需要完成类似课题或学习B/S架构全栈开发的学生参考。文档完整包含中英文摘要、目录、研究背景、技术选型介绍并重点展开管理员与用户两大功能模块的设计分析涉及Nodejs非阻塞I/O、Vue响应式数据绑定、MySQL数据存储和B/S架构等关键技术点同时结合软件组件化、逻辑与数据分离等工程实践方便读者理解从需求分析到系统实现的全过程。包体为单个docx文件大小3.8MB内容结构清晰便于直接阅读和二次编辑。目前已有93人学习下载对于正在搭建校园二手交易系统或撰写毕业设计论文的读者可从中获取系统结构设计、模块划分和论文撰写思路有效节省从零梳理的时间。1. Node.js Vue 做校园二手交易平台到底在做什么学校的跳蚤市场群里每天被“出教材”“收吉他”刷屏消息几分钟就被淹没。大学生手里有教材、台灯、自行车毕业季要么贱卖要么直接丢买家想低价收卖家想快速出两端都在同一个物理空间里却缺一个结构化撮合渠道。标题里这个基于 Node.js Vue 的校园二手物品交易平台做的就是这件事用 Vue 画界面用 Node.js 管业务逻辑和数据存取把发布商品、浏览搜索、私聊议价、下单交易跑成一个完整 Web 应用。文章按开发顺序拆架构选型、后端接口、前端页面、部署验证。适合刚入门全栈、想用真实项目串起 Vite、Express、MySQL 的开发者也适合已经在公司写单一技术栈、想看看全链路怎么配合的工程师。如果这个项目同时是你的毕业设计下面这套落地路径也可以当作系统实现部分的技术骨架。2. 校园二手平台的架构选型与核心模型设计2.1 为什么是 Node.js Vue两个名字背后的选型逻辑先讲清楚这两个框架在项目里的角色。Node.js 是服务端运行环境用 JavaScript 写接口Vue 是前端框架负责页面渲染和用户交互。这对组合在校园项目中高频出现不是因为性能天花板高而是因为发展链路短前后端用同一种语言新入门开发者不需要同时维护一套 JavaScript 前端和一套 Java 后端Node.js 的事件循环模型天然适合二手交易这种读多写少、不涉及复杂 CPU 运算的场景Vue 的渐进式设计让没有后端经验的人也能从静态页面平滑过渡到数据驱动。对比其他选型场景可以看得更清楚。如果团队里有人已经熟练 Spring Boot基于 Spring Boot Vue 也是成熟方案但启动成本对课程设计或个人自建项目偏高如果平台未来要承载闲鱼那种量级的海量并发和图像识别推荐Node.js 不是第一选择换成 Go 或 Java 服务端配合独立推荐算法更合理。做技术选型要看到边界Node.js Vue 适合中小规模、业务迭代快、开发人力有限的校园平台不适合一上来就规划千万级用户的架构。下面这张表是我在选型时会和自己确认的对照依据维度Node.js ExpressSpring Boot这个项目适合谁前后端语言一致性前后端均 JavaScript后端 Java需另学一套新手选前者启动成本npm install 即可需 JDK、Maven、IDE 配置课程设计选前者典型并发能力事件循环I/O 密集场景表现好线程池模型CPU 密集更强校园规模无显著差异生态成熟度Express 中间件齐全企业级组件更全两者均可2.2 数据库模型用户、商品、订单与聊天二手交易和普通电商最大的区别在商品状态机。二手商品有“在售、已被预订、已下架”三种状态订单有“买家发起、卖家确认、交易完成、已取消”四种状态。设计表结构时如果状态字段写成应用层的魔法字符串后面统计业务数据会非常痛苦。我一般给状态字段加 TINYINT 数值在代码里用枚举对象统一管理。下面是核心表结构用 MySQL 8 示例CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号校园场景唯一标识, nickname VARCHAR(30) NOT NULL DEFAULT COMMENT 昵称, password_hash CHAR(60) NOT NULL COMMENT bcrypt哈希非明文, avatar_url VARCHAR(255) DEFAULT COMMENT 头像地址, credit_score INT NOT NULL DEFAULT 100 COMMENT 信用分, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, seller_id INT UNSIGNED NOT NULL COMMENT 卖家用户ID, title VARCHAR(80) NOT NULL COMMENT 商品标题, description TEXT COMMENT 详细描述, price DECIMAL(10,2) NOT NULL COMMENT 价格用DECIMAL不用FLOAT, category TINYINT NOT NULL COMMENT 1教材/2数码/3生活/4运动/5其他, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在售 1已预订 2已下架, image_urls JSON COMMENT 最多9张图JSON数组, view_count INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_seller_id (seller_id), KEY idx_category_status (category, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二手商品表;参数说明密码字段用password_hash存储 bcrypt 哈希固定长度 60 字符不要用 VARCHAR(32) 去塞 MD5MySQL 里这个字段被设计成定长 CHAR(60)能避免行移动带来的碎片价格字段用 DECIMAL(10,2) 而不是 FLOAT避免浮点数比较时出现 19.99 不等于 19.99 的问题idx_category_status是复合索引因为商品列表页最常见的筛选条件是“某个分类下的在售商品”单独给 category 或 status 建索引都只能走其中一个。订单和聊天记录建议拆成两张表。订单表记录交易主流程聊天记录表记录买卖双方沟通内容。校园平台不需要完整即时通讯聊天记录表只要具备“买家在商品详情页发起咨询卖家在消息中心回复”的能力就够了。2.3 接口与应用契约统一响应体与错误码前后端分离项目的协作基础是接口契约。我项目里通常用一个小的 response 结构统一包裹无论成功还是失败都是同一个字段格式{ code: 0, message: ok, data: {} }业务错误码分段管理0 为成功40000 段为参数错误40100 段为未登录或 token 过期40300 段为权限不足50000 段为服务端异常。前端 axios 拦截器里只判断 code 是否为 0非 0 就直接弹 toast。这样比混合判断 HTTP 状态码和 body 要省事得多HTTP 状态码仍然保留用于 Nginx 层和网关层做监控告警。接口错误信息要写清楚是哪个字段不合法比如{ code: 40001, message: price 必须是正数 }前端可以直接把 message 展示给用户省一套错误文案映射。3. 用 Express MySQL 把后端接口跑起来3.1 初始化 Express 项目与必要依赖常见做法是先用 npm init -y 生成 package.json再手动加依赖。我一般固定装这些express、mysql2、jsonwebtoken、bcryptjs、cors、dotenv。mysql2 比 mysql 包多了 Promise 支持和预处理语句bcryptjs 是纯 JS 实现避免在 Windows 上编译 node-gyp 出问题。这里会碰到一个频率最高的报错在 PowerShell 里执行 npm install 时报 “npm : 无法加载文件 …\npm.ps1因为在此系统上禁止运行脚本”。这是 PowerShell 执行策略问题不是 npm 本身损坏。解决办法是以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned再重开终端或者改用 CMD 执行 npm。这个坑和 Node.js 版本无关新版安装包不会自动去改 PowerShell 的 ExecutionPolicy。mkdir campus-trade-server cd campus-trade-server npm init -y npm install express mysql2 jsonwebtoken bcryptjs cors dotenv --save npm install nodemon --save-dev目录结构按下面这种模块划分把 controllers、services、models 拆开避免把所有 SQL 堆在路由回调里。这个拆分是给后续维护留余地不是仪式感商品列表要加筛选条件时只改 service 和 model路由层不动接口层不用回归。src/ app.js 入口文件 routes/ 路由定义 controllers/ 参数校验与响应组装 services/ 业务逻辑 models/ SQL 查询 middlewares/ 鉴权、错误处理 config/index.js 读取环境变量先写一个最小可启动的 app.jsconst express require(express); const cors require(cors); const itemRouter require(./routes/item); require(dotenv).config(); const app express(); app.use(cors()); app.use(express.json()); app.use(/api/items, itemRouter); app.use((err, req, res, next) { res.status(err.status || 500).json({ code: err.code || 50000, message: err.message || 服务器内部错误 }); }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(server running at http://localhost:${PORT}); });逻辑说明cors() 要放在路由前面否则前端跨域请求会被浏览器拦截express.json() 用于解析请求体中的 JSON没有它req.body 会一直是 undefined错误处理中间件必须放在所有路由之后Express 才能捕获 next(err) 传过来的异常。环境变量走 dotenv把端口、数据库密码、JWT 密钥放在 .env 文件里不要写死进代码这个文件要加进 .gitignore。3.2 用连接池访问 MySQL 与中间件顺序mysql2 的建议用法是创建连接池而不是每次 new Connection。连接池的默认配置需要按业务调const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, maxIdle: 5, idleTimeout: 60000, enableKeepAlive: true }); module.exports pool;参数说明connectionLimit 是最大连接数校园平台并发不高10 个足够设高了反而浪费数据库内存maxIdle 是允许保留的空闲连接数避免高峰期频繁建立新连接idleTimeout 是空闲连接关闭前的等待时间超过这个时间没有请求就回收enableKeepAlive 对经过 NAT 和云数据库的长时间运行很关键不然 MySQL 默认隔一段时间会主动断开空闲连接。写查询的时候用预处理语句pool.query(?, [])而不是字符串拼接既防 SQL 注入也让 MySQL 可以复用查询计划。Express 中间件的执行顺序是一个常被忽略的细节路由文件里的中间件、认证中间件、错误处理中间件的先后顺序直接影响接口行为。整理成下面的顺序参考中间件作用放置位置cors处理跨域所有业务路由之前express.json解析 JSON 请求体所有读 req.body 的接口之前authMiddlewareJWT 鉴权需要登录态的接口之前errorHandler统一异常出口所有路由之后3.3 登录鉴权从注册接口到 JWT 中间件注册和登录接口的实现逻辑比较固定。注册时先查学号是否已存在再用 bcryptjs.hashSync(password, 10) 生成哈希存库登录时用 bcryptjs.compareSync 比对明文密码与哈希。JWT 令牌用 jsonwebtoken.sign(payload, secret, { expiresIn: 7d }) 生成payload 里只放 userId 和 studentNo不要放昵称头像这些可能变化的信息避免 token 失效前展示旧资料。const jwt require(jsonwebtoken); function authMiddleware(req, res, next) { const header req.headers.authorization; if (!header || !header.startsWith(Bearer )) { return res.status(401).json({ code: 40100, message: 未登录 }); } const token header.slice(7); try { const payload jwt.verify(token, process.env.JWT_SECRET); req.userId payload.userId; next(); } catch (e) { return res.status(401).json({ code: 40101, message: 登录已过期请重新登录 }); } }注意 token 存哪里这个问题。localStorage 有 XSS 风险httpOnly Cookie 更安全但要额外处理 CSRF。校园项目一般用 localStorage 配合前端不引入外部脚本够用如果后续接入真实支付再考虑换成 Cookie 方案。JWT 密钥要放到 .env 里长度至少 32 位不要用 “secret” 这种字符串不然别人看一眼前端代码就能伪造管理员 token。3.4 商品列表接口完整链路与分页参数商品列表是平台访问量最大的接口设计成 GET /api/items支持分页、分类、关键词、排序四个参数。分页参数 page 从 1 开始pageSize 限制最大 50避免有人一次拉全表。排序字段做白名单校验只允许 created_at、price、view_count前端传什么就拼什么会让数据库报错并暴露表结构。关键词搜索用 LIKE 关联 title再把匹配到的结果按 “标题包含关键词优先、描述包含其次” 排序校园用户搜 “高数” 时想要的是书名里的结果不是详情里顺带提了一句的结果。const getItems async (req, res, next) { try { const { page 1, pageSize 10, category, keyword, sort created_at:desc } req.query; const offset (page - 1) * pageSize; let sql SELECT id, title, price, category, status, image_urls, view_count, created_at FROM item WHERE status 0; const params []; if (category) { sql AND category ?; params.push(Number(category)); } if (keyword) { sql AND title LIKE ?; params.push(%${keyword}%); } const allowSort [created_at, price, view_count]; const [sortField, sortDir] sort.split(:); if (allowSort.includes(sortField)) { sql ORDER BY ${sortField} ${sortDir asc ? ASC : DESC}; } sql LIMIT ? OFFSET ?; params.push(Number(pageSize), offset); const [rows] await pool.query(sql, params); res.json({ code: 0, message: ok, data: rows }); } catch (err) { next(err); } };参数说明LIMIT 和 OFFSET 用参数形式传入不能直接拼进字符串虽然这里参数是数字但养成习惯能避免后续有字符串参数时踩注入的坑排序字段created_at不能写成item.created_at这种带前缀的形式否则白名单校验会拦截前端每页默认 10 条滚动触底时 page 加 1直到返回的 rows 长度小于 pageSize 时停止加载这种判断方式比依赖 total 字段更简单也少一次 COUNT 查询。4. 用 Vue3 Vite 实现前台页面与状态管理4.1 初始化 Vue3 项目和常见安装坑前端工程化部分常见做法是直接用 Vite 脚手架建 Vue3 项目npm create vitelatest campus-trade-web -- --template vue cd campus-trade-web npm install npm install vue-router4 pinia axios这里会碰到 Node.js 版本问题。Vite 5 要求 Node 18如果本机还是 Node 16npm install 时有 warning启动 dev server 时直接报错。建议装 nvm-windows 做多版本管理nvm install 18后nvm use 18切到项目需要的版本不要为了迁就旧项目把系统 Node 一直锁在 16。另一个经常出现的问题是 npm install 卡在 idealTree 阶段通常是因为公司 npm 镜像源问题执行npm config set registry https://registry.npmmirror.com可以缓解但这只是镜像源切换不要把它和 Node.js 升级混为一谈。main.js 初始化文件保持简洁只做挂载和插件注册import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.mount(#app)Vue3 项目里应用实例、路由实例、Pinia 实例三者通过 app.use 串联顺序影响不大但 createPinia() 要在组件访问 store 之前完成注册。4.2 路由设计、动态参数与懒加载校园二手平台的页面比普通企业官网多一个“商品详情页”这个页面会被频繁打开和返回路由设计上要保证从列表页到详情页再返回列表页时列表滚动位置不丢失。Vue Router 默认不保存滚动位置需要配置 scrollBehavior 记住当前路由的 scrollTop否则用户从详情页返回时直接回到顶部体验很差。路由级懒加载是一开始就要做的事{ path: /, name: home, component: () import(../views/Home.vue) }, { path: /items/:id, name: item-detail, component: () import(../views/ItemDetail.vue) }注意/items/:id这种动态参数路由的一个经典行为从 /items/1 导航到 /items/2 时Vue 会复用同一个组件实例不会重新走 mounted 钩子。很多人在商品详情页连续点“看了又看”的推荐项时发现页面内容不更新排查半天找不到原因。解决办法是在组件里对 route.params.id 做 watch或者给 router-view 加 :key“route.fullPath” 强制重建组件。4.3 用 axios 封装接口请求与拦截器前端所有对后端的请求应该收敛到一个 api 层不要在组件里散落 fetch。下面这个 request.js 是项目里常见做法做了一层响应拦截import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 8000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( res { if (res.data.code ! 0) { if (res.data.code 40100) { router.push(/login) } ElMessage.error(res.data.message) return Promise.reject(res.data) } return res.data.data }, err { ElMessage.error(网络异常请稍后重试) return Promise.reject(err) } )参数说明baseURL 设为 /api开发环境靠 Vite 的 proxy 配置转发到后端 3000 端口生产环境由 Nginx 做反向代理前端代码里不用出现具体 IPtimeout 设为 8 秒避免慢接口把页面长时间挂起响应拦截器把后端返回的 data 字段直接解掉组件里拿到的就是真实业务数据不用每个页面都写 res.data.data。这套封装在遇到文件上传接口时要单独开一个实例因为上传接口需要自己处理 Content-Type 头。4.4 Pinia 管理用户信息为什么不用 VuexVue3 项目里状态管理的选择官方推荐 Pinia。和 Vuex 4 相比Pinia 去掉了 mutation同步异步都在 action 里处理调试时少一层概念。校园二手平台需要全局管理的内容不多当前登录用户、收藏列表、首页搜索条件从搜索页跳详情页再返回时搜索条件不能丢。对比项Vuex 4PiniaVue3 响应式支持需要额外适配原生支持TypeScript 推导较弱类型友好模块化写法modules 嵌套每个 store 独立热更新需配置内置import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ profile: null, token: localStorage.getItem(token) || }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, logout() { this.token this.profile null localStorage.removeItem(token) } } })状态持久化是个容易遗漏的问题。Pinia 默认只存内存刷新页面 state 就没了所以 token 要同步写进 localStorage页面刷新后在 main.js 或路由守卫里根据 token 重新拉取用户信息。不要拿 cookie 存 token一来涉及跨域时 cookie 的 SameSite 属性容易搞错二来这个信息本就不需要发给服务端以外的第三方。4.5 列表页体验优化防抖、虚拟滚动与图片懒加载校园二手物品列表是典型的长列表关键词搜索会频繁发请求输入防抖是必须的。用 300ms 防抖即可不必上复杂库。图片懒加载直接用原生 loading“lazy”商品图本身就比较多后端返回的 image_urls 是 JSON 数组前端解析后优先加载首图。滚动分页要记录一个 loading 状态防止快速滚动时重复请求同一页数据这比防抖本身更能避免后端压力。5. 从本地到线上交易链路验证与进阶技巧5.1 用 curl 走一遍交易链路后端接口写完后先用 curl 验证再写前端页面能省下不少联调时间。完整的交易链路验证是注册、登录、发布商品、查询列表、创建订单curl -X POST http://localhost:3000/api/auth/register \ -H Content-Type: application/json \ -d {studentNo:2022001,password:test123456,nickname:测试买家} curl -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {studentNo:2022001,password:test123456} \ -c cookie.txt登录返回的 token 要拿到后续请求的 Authorization 头里。这一步能验证接口契约、密码哈希、JWT 签发三个环节是否正常。如果发布商品接口返回 401先检查 token 是否过期再看 Authorization 头格式是否为 Bearer 空格 token。5.2 用 EXPLAIN 找出列表查询慢的原因商品表数据量到万级之后分页变慢的第一个排查点是 OFFSET。LIMIT 1000, 10 会让数据库把前面 1000 行全扫一遍再丢弃这时要看 EXPLAIN 的结果EXPLAIN SELECT id, title, price FROM item WHERE status 0 AND category 1 ORDER BY created_at DESC LIMIT 10 OFFSET 1000;如果 type 是 ALL说明没走索引。备选方案是改造成 “上一页最后一条记录的排序值” 游标分页前端传 lastCreatedAt后端用WHERE created_at ?取下一页这在大深度分页时比 OFFSET 稳定。但这个方案会让用户无法跳转到任意页码校园平台通常只需要滚动加载能接受这个约束。5.3 给平台加两个实战细节站内信与定时下架最后说两个能让项目比普通 demo 更有完成度的细节。第一个是站内信通知买家下单后要向卖家推送一条“你的商品已被预订”的消息。实现上不需要引入 MQ在订单状态变更的事务里插入一条通知记录卖家消息中心的未读数通过SELECT COUNT(*) WHERE is_read 0查询即可等用户量上来再换 WebSocket 推送。第二个是商品超时下架。二手物品讲究时效可以每天早上跑一次定时任务把超过 30 天未成交的在售商品自动改为下架。Node.js 生态里用 node-schedule 就能实现const schedule require(node-schedule); schedule.scheduleJob(0 0 2 * * *, async () { await pool.query( UPDATE item SET status 2 WHERE status 0 AND created_at DATE_SUB(NOW(), INTERVAL 30 DAY) ); });注意DATE_SUB(NOW(), INTERVAL 30 DAY)用的是数据库时间而不是服务器时间避免服务器时区配置不一致导致误下架。定时任务要记录执行日志至少记录影响行数否则上线后商品莫名其妙消失会很难排查。移动端真机调试时把项目打包后放到 Nginx 上再用手机访问局域网地址如果发现样式错乱优先检查 viewport 标签和 rem 适配方案Vue 项目打包后的布局异常通常出现在这两个环节。本文还有配套的精品资源点击获取
返回列表