1. 选题冷思考:为什么“基于NodeJS的宠物网站”能成为毕设常青树
计算机毕业设计这个事,每年都有一大批同学在选题阶段反复纠结。要么担心题目太简单,答辩时被老师追问到怀疑人生;要么担心题目太复杂,做了三个月连主流程都没跑通。而“基于NodeJS宠物网站的设计与实现”这个题目,几乎是各大高校和网络平台上出现频率最高的宠物类毕设选题之一。它火不是没道理的,它的业务逻辑足够完整,技术栈又足够主流,关键是做出来的效果还很直观——有页面、有交互、有数据流动,答辩演示时非常抓眼球。
先说这个题目为什么适合作为毕业设计。宠物网站的核心业务无非是宠物信息展示、宠物领养、宠物用品购买、用户管理、订单管理等,这些功能覆盖了Web开发中最常见的增删改查、文件上传、登录鉴权、前后端交互等核心技能点。用一个宠物网站,你就能把大学四年学到的Web开发知识串成一条完整的业务线。而且宠物行业自带展示属性,宠物图片可爱、数据可视化效果直观,评阅老师看演示时观感上天然占便宜。
然后是技术选型的问题。标题里的关键词很明确——NodeJS,但也拌着Java、PHP、Python、小程序这些热词。这就引出一个几乎所有做毕设的同学都会问的问题:同样是做Web系统,我到底该选哪个?我的建议是,如果你已经有把握使用Java或PHP,那就用自己熟悉的;如果你的基础一般、又想快速做出来一个能跑能演示的系统,NodeJS是一个非常折中的选择。原因后面我会详细展开。
从另一个角度看,这个题目之所以“必看必学”,是因为它有着极强的可扩展性。你既可以做成一个传统的服务端渲染网站,也可以做成前后端分离架构配合小程序端;既可以只做简单的网上展示,也可以接上支付、地图、消息推送这些进阶功能。也就是说,无论你是什么水平,都能在这个题目里找到适合自己的实现方式。下面我按一套我实际带学生和做项目时验证过的完整流程,把这个题目从选题到答辩的每一步掰开讲清楚。
1.1 NodeJS在毕设选题中的真实定位
先说NodeJS到底是个什么水平的技术。很多同学听到NodeJS第一反应是“这不就是个前端框架吗”,这是个很大的误区。NodeJS是一个基于Chrome V8引擎的JavaScript运行时环境,它让你可以用JavaScript写后端服务。对于会前端的同学来说,学习成本极低;对于只会后端语言的同学来说,它又是一条快速掌握全栈能力的捷径。
放到毕设的实际场景里,NodeJS有几个很现实的优点。第一是生态成熟,npm上几乎能找到你需要的任何功能包,做登录有jsonwebtoken,做文件上传有多中间件,做数据库操作有mysql2、sequelize等,基本不用自己重复造轮子。第二是开发效率高,JavaScript的语法本身就比Java、C++这类静态语言灵活,同样的功能用NodeJS写,代码量通常只有Java的一半不到。第三是前后端通吃,你可以用一套语言同时写前端页面和后端接口,这对一个要在几个月内搞定整个项目的学生来说,是非常重要的优势。
当然也要承认NodeJS的一些短板,比如不适合做CPU密集型任务、生态里包的质量良莠不齐等。但说句实在话,对于一个宠物网站的毕业设计来说,这些短板根本碰不到。你处理的就是几百条宠物数据、几十个用户的并发请求,NodeJS单线程事件驱动的模型应对这种场景绰绰有余。选它,不是为了追求技术上的极致,而是为了在有限的时间里拿到一个完整、可运行、可展示的结果。
1.2 同赛道技术栈对比:Java、PHP、Python、小程序各有何优劣
毕设选题时,很多人会纠结要不要换一个更“主流”的技术。这里我把常见的几个选择放在一起做个对比,方便你根据自己的实际情况来决定:
| 技术栈 | 上手难度 | 开发速度 | 毕业设计评分友好度 | 适合人群 |
|---|---|---|---|---|
| NodeJS + Express | 低 | 快 | 高,业务逻辑清晰即可,且能体现前端洞察 | 有前端基础,或想快速开发出完整系统的人 |
| Java + Spring Boot | 中高 | 中慢 | 高,企业级框架加分,但对新手不够友好 | 系统学过Java、熟悉Maven和Spring的人 |
| PHP + ThinkPHP/Laravel | 低 | 快 | 中,传统但缺乏亮点,容易显得“太经典” | 学过PHP语法、熟悉MySQL组合拳的人 |
| Python + Flask/Django | 低中 | 快 | 中高,代码简洁,但部分学校不把Python当Web主流 | 熟悉Python、想写代码少一点的人 |
| 小程序 + 云开发 | 低中 | 极快 | 中,只做小程序可能业务量不够,最好配合后端 | 想蹭小程序热度、做移动端展示的人 |
从这张表能看出来,Java强在“框架权威性”,但在毕设周期内你可能要花大量时间处理配置和依赖问题。PHP强在传统,但如果你做的是“宠物网站”这种业务,用PHP实现出来的效果跟NodeJS差不多,反而显得技术栈偏老。Python强在代码简洁,但Web生态的成熟度相比NodeJS还是略逊。小程序适合做展示端,但单独拿一个小程序当毕设,业务体量往往会显得单薄。
我的看法是:如果你不是Java高手,也不是冲着某个具体方向去,NodeJS是兼顾开发效率和评分效果的最优解。特别是宠物网站这种以页面展示和交互为主的系统,JavaScript那套事件驱动模式写起来非常顺手,页面效果也能做得更灵动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆解业务模块:宠物网站到底要做什么,才能让评阅老师觉得“完整”
很多同学做毕设失败,不是代码写不出来,而是不知道“做到什么程度算完”。一个宠物网站听起来简单,但如果只做一个宠物列表加一个后台,那跟课程设计没区别,答辩基本会冷场。要让评阅老师觉得你的系统是下了功夫的,业务模块至少要覆盖三条线:用户前台、管理后台、核心业务流程。“用户能注册登录,管理员能发布宠物,用户能查看并申请领养或购买”是最低标准;在你时间允许的情况下,往这个主干上再加收藏、留言、公告、订单状态流转,整个系统一下子就立体起来了。
我自己带学生做这个题目时,通常会把系统拆成两套端口、三类角色。两套端口分别是PC端网站和手机端小程序/适配页面,三类角色分别是普通用户、商家(宠物店管理员)、系统管理员。下面分层讲清楚每部分具体做什么。
2.1 用户端:从宠物展示到领养/购买申请的核心链路
用户端是评阅老师第一眼看到的部分,它的体验和完整度直接决定了答辩的初始印象分。一个合格的用户端至少需要包含以下几个功能点:
- 用户注册与登录:手机号或邮箱注册,密码加密存储,登录后使用Token标识用户身份。
- 宠物展示与分类筛选:按宠物类型(猫、狗、小宠)和品种做分类,支持关键词搜索、价格区间筛选、列表分页。
- 宠物详情页:宠物图片轮播、基本信息(年龄、性别、疫苗情况)、性格描述、健康状态、所属店铺或救助站。
- 领养/购买申请:用户对感兴趣的宠物提交申请或直接下单,填写理由或备注信息。
- 个人中心:查看自己发布的宠物收藏、申请记录、订单记录、收货或领养状态。
- 公告与资讯:网站运营方发布宠物资讯、领养须知等。
这里有一个特别容易踩的坑:很多同学把大量精力放在页面样式上,却忽略了业务闭环。比如用户点击“我要领养”之后没有任何反馈,或者申请完就查不到进度。这就是典型的“只有环,没有闭”。你需要确保用户在每一步操作后都能在个人中心看到对应状态——申请已提交、审核中、审核通过、已领养/已购买、已取消。这个状态机的设计,正是评阅老师看重的地方,也恰恰是你和课程设计拉开差距的核心。
2.2 管理端:宠物信息管理与申请审核的权限体系
管理端是体现系统完整度和权限控制能力的关键,也是评阅老师提问最多的地方。它不需要花里胡哨的界面,但功能必须对得上用户端的操作。
管理员端核心模块如下:
- 宠物管理:管理员或店铺账号发布宠物信息,上传宠物图片,设置宠物状态(在售、已预约、已领养、已下架),编辑宠物详情,删除违规内容。
- 领养/订单管理:查看用户提交的领养申请,进行审核(通过/驳回);处理购买订单,更新订单状态。
- 用户管理:查看注册用户列表,禁用/启用账号,重置密码。
- 公告管理:编辑和发布系统公告,控制公告的显示与下线。
- 数据统计(可选加分项):统计每日新增用户数、宠物发布数、订单成交量,用简单的柱状图或数值卡片展示。
权限管理这块建议至少实现两套角色:管理员和店铺(商家)。商家只能管理自己名下的宠物和订单,管理员能看到全局数据。如果项目时间比较紧,可以先用一个“role”字段区分管理员和普通用户,但答辩时你要能说清楚权限模型的设计思路,否则老师问到“不同角色如何做权限隔离”时就会卡壳。
3. 数据库设计:宠物业务的核心表结构与状态流转
数据库设计是毕设的逻辑骨架,也是最容易被答辩老师追问的地方。我的建议是:先画清楚表关系,再动手写接口。很多同学上来就写代码,做到一半发现订单表和宠物表关系理不清,反复改表结构,白白浪费大量时间。这里我把宠物网站项目的核心表结构直接列出来,你可以根据实际需要增删字段。
3.1 用户表、宠物表、订单表的结构与关系
先看最核心的几张表。用户表负责存储网站的用户和管理员信息,字段设计如下:
sql复制CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码(bcrypt加密)',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:1普通用户 2店铺商家 3管理员',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
宠物表是整个系统的核心资源,设计时需要注意把宠物的基本属性、健康状态、所属人、状态等字段都考虑全:
sql复制CREATE TABLE `pet` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '宠物名称',
`category` varchar(20) NOT NULL COMMENT '宠物类型:猫/狗/小宠',
`breed` varchar(50) DEFAULT NULL COMMENT '品种',
`age` varchar(20) DEFAULT NULL COMMENT '年龄,如:3个月/2岁',
`gender` tinyint(1) DEFAULT NULL COMMENT '性别:0公 1母',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '价格,0表示免费领养',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图',
`images` text COMMENT '轮播图,多张用逗号分隔',
`description` text COMMENT '宠物介绍',
`health_status` varchar(255) DEFAULT NULL COMMENT '疫苗/驱虫/绝育情况',
`owner_id` int(11) NOT NULL COMMENT '发布者ID,对应user表',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1在售/待领养 2已预约 3已售/已领养 4下架',
`view_count` int(11) DEFAULT '0' COMMENT '浏览量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category`),
KEY `idx_owner` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物表';
订单表用来承载用户对宠物的购买或领养动作,表结构如下:
sql复制CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` int(11) NOT NULL COMMENT '下单用户ID',
`pet_id` int(11) NOT NULL COMMENT '宠物ID',
`type` tinyint(1) NOT NULL COMMENT '订单类型:1购买 2领养',
`amount` decimal(10,2) DEFAULT '0.00' COMMENT '金额',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1待审核/待付款 2已通过/已付款 3已完成 4已取消 5已拒绝',
`apply_reason` varchar(500) DEFAULT NULL COMMENT '申请领养理由',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`handle_time` datetime DEFAULT NULL COMMENT '管理员处理时间',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user` (`user_id`),
KEY `idx_pet` (`pet_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
除了这三张核心表,还可以根据功能扩展收藏表(favorites:user_id + pet_id联合唯一)、公告表(notice)、轮播图表(banner)等。表之间的主外键关系不要过度设计,保持简单清晰即可。我用一句话总结表结构设计原则:每张表表达一个实体,每个业务动作都能在一个状态字段中找到落点。
3.2 领养与购买流程中的状态机设计思路
状态设计是整个数据库设计里最体现功底的部分。以宠物表为例,status字段从“在售/待领养”到“已预约”再到“已售/已领养”,这是一条完整的状态流转线。订单表更是如此。我建议你在设计阶段就把状态流转图画出来(哪怕只是画在纸上),这是答辩时一个非常硬核的亮点。
比如领养流程的状态流转可以设计成这样:
- 用户浏览宠物详情,点击“申请领养”,填写申请理由,生成预约订单,宠物状态从“待领养”变为“已预约”。
- 管理员在后台看到申请,审核通过——订单状态变为“已通过”,宠物状态变为“已领养”;审核不通过——订单状态变为“已拒绝”,宠物状态恢复为“待领养”。
- 如果是购买流程,则多一步支付动作(毕设中通常用“模拟支付”代替),订单状态从“待付款”变为“已付款”,宠物状态从“在售”变为“已售”。
这个状态机的关键点在于“状态的联动”。你需要在代码里保证宠物状态和订单状态的一致性:不能订单已经审核通过,宠物还在“已预约”状态。常见的实现方式是在订单状态变更的事务中同时更新宠物表的状态,或者在订单表的接口里联动更新宠物表。
我个人推荐把状态常量定义在一个独立的配置文件中,比如:
javascript复制// config/status.js
module.exports = {
PET_STATUS: {
ON_SALE: 1, // 在售/待领养
RESERVED: 2, // 已预约
SOLD: 3, // 已售/已领养
OFF: 4 // 下架
},
ORDER_STATUS: {
PENDING: 1, // 待审核/待付款
APPROVED: 2, // 已通过/已付款
DONE: 3, // 已完成
CANCELED: 4, // 已取消
REJECTED: 5 // 已拒绝
}
};
这样做的好处有很多:第一,代码可读性高,不会到处写魔法数字;第二,答辩时你可以直接展示这个状态配置文件,说明你对业务流转有全局理解;第三,后续改状态逻辑时只需要动一处。
4. NodeJS后端实战:从初始化项目到核心接口落地
确定了表结构,下一步就是动手写后端。我先说几个新手最容易在环境配置上卡住的问题,再给出一条我已经重复验证过的项目搭建路径。
如果你用的是Windows系统,在安装NodeJS后执行npm命令时,很可能遇到一个非常经典的问题:
bash复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
这个问题的本质是Windows PowerShell的执行策略默认不允许运行.ps1脚本文件。解决办法很简单:以管理员身份打开PowerShell,执行下面的命令,然后选“是”:
bash复制Set-ExecutionPolicy RemoteSigned
如果你不想动系统级策略,也可以直接用CMD(命令提示符)而不是PowerShell跑npm,或者使用npx来绕过。这个坑我见无数同学踩过,卡了一晚上,其实就一行命令的事。另外,如果你是下载了nodejs安装包安装的,记得确认一下环境变量是否自动配置好了——安装完成后在CMD里执行 node -v 和 npm -v,两个都能输出版本号才算成功。
然后是npm源的问题。国内直接使用npm官方源下载依赖速度很慢,甚至经常超时。建议配置成淘宝镜像:
bash复制npm config set registry https://registry.npmmirror.com
配置完之后,安装依赖的速度会有质的提升。这一步做不做,会直接影响到你后面每次npm install的体验。
4.1 项目目录结构与Express框架搭建
我推荐使用Express框架,它足够轻量、文档多、参考资料丰富。项目目录结构可以这样规划:
code复制pet-website/
├── app.js # 入口文件,初始化Express实例
├── package.json
├── .env # 环境变量配置
├── config/
│ ├── db.js # 数据库连接配置
│ └── status.js # 状态常量定义
├── routes/
│ ├── user.js # 用户相关路由
│ ├── pet.js # 宠物相关路由
│ ├── order.js # 订单相关路由
│ └── upload.js # 文件上传路由
├── controllers/ # 业务逻辑层
│ ├── userController.js
│ ├── petController.js
│ └── orderController.js
├── middlewares/
│ ├── auth.js # 登录鉴权中间件
│ └── errorHandler.js # 统一错误处理
├── utils/
│ └── response.js # 统一返回格式
└── public/ # 静态资源文件
└── uploads/ # 上传的宠物图片
初始化项目的命令:
bash复制mkdir pet-website && cd pet-website
npm init -y
npm install express mysql2 cors dotenv jsonwebtoken bcryptjs multer
简单解释一下这几个依赖包的用途:mysql2是NodeJS连接MySQL的驱动,支持Promise异步写法;cors处理跨域请求,因为你的前端可能和小程序端不在同一个域名下;dotenv用来加载.env配置文件里的敏感信息;jsonwebtoken负责生成和校验JWT登录凭证;bcryptjs用来对用户密码进行哈希加密;multer处理文件上传。
入口文件app.js的骨架如下:
javascript复制const express = require('express');
const cors = require('cors');
const path = require('path');
require('dotenv').config();
const userRouter = require('./routes/user');
const petRouter = require('./routes/pet');
const orderRouter = require('./routes/order');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// 路由挂载
app.use('/api/user', userRouter);
app.use('/api/pet', petRouter);
app.use('/api/order', orderRouter);
// 统一错误处理
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
code: err.status || 500,
message: err.message || '服务器内部错误'
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
4.2 登录鉴权与宠物信息核心接口实现
接下来直接实现两个最核心的模块:用户登录鉴权和宠物信息接口。先看登录逻辑,密码必须使用bcryptjs加密后存储,不能明文保存。注册接口的代码可以这样写:
javascript复制// controllers/userController.js
const db = require('../config/db');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
// 用户注册
exports.register = async (req, res, next) => {
try {
const { username, password, phone } = req.body;
if (!username || !password) {
return res.json({ code: 400, message: '用户名和密码不能为空' });
}
// 检查用户名是否已存在
const [exist] = await db.query('SELECT id FROM user WHERE username = ?', [username]);
if (exist.length > 0) {
return res.json({ code: 400, message: '用户名已存在' });
}
// 密码加密
const hashPassword = bcrypt.hashSync(password, 10);
await db.query('INSERT INTO user (username, password, phone) VALUES (?, ?, ?)',
[username, hashPassword, phone || null]);
res.json({ code: 200, message: '注册成功' });
} catch (err) {
next(err);
}
};
// 用户登录,签发JWT
exports.login = async (req, res, next) => {
try {
const { username, password } = req.body;
const [rows] = await db.query('SELECT * FROM user WHERE username = ?', [username]);
if (rows.length === 0) {
return res.json({ code: 400, message: '用户不存在' });
}
const user = rows[0];
const isValid = bcrypt.compareSync(password, user.password);
if (!isValid) {
return res.json({ code: 400, message: '密码错误' });
}
// 生成token,有效期24小时
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '24h' }
);
res.json({
code: 200,
message: '登录成功',
data: {
token,
id: user.id,
username: user.username,
role: user.role,
avatar: user.avatar
}
});
} catch (err) {
next(err);
}
};
登录发送的JWT会在后续每次请求中放到请求头里,后端通过一个auth中间件来校验。这个中间件就是典型的“所有需要登录状态的接口都要过一道”的关卡:
javascript复制// middlewares/auth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
// 从Authorization头里取token
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : req.headers['x-token'];
if (!token) {
return res.json({ code: 401, message: '未登录或登录已过期' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
req.user = decoded;
next();
} catch (err) {
return res.json({ code: 401, message: 'Token无效或已过期' });
}
};
宠物信息接口是用户端最常调用的接口,列表接口需要支持分页、分类筛选、关键词搜索和多条件组合查询。这里用一个小例子展示查询逻辑:
javascript复制// controllers/petController.js
const db = require('../config/db');
// 宠物列表:分页 + 分类筛选 + 关键词搜索
exports.getPetList = async (req, res, next) => {
try {
const { page = 1, pageSize = 12, category = '', keyword = '', minPrice, maxPrice } = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
// 动态拼接查询条件
let where = 'WHERE status IN (1, 2)';
const params = [];
if (category) {
where += ' AND category = ?';
params.push(category);
}
if (keyword) {
where += ' AND (name LIKE ? OR breed LIKE ?)';
params.push(`%${keyword}%`, `%${keyword}%`);
}
if (minPrice) {
where += ' AND price >= ?';
params.push(minPrice);
}
if (maxPrice) {
where += ' AND price <= ?';
params.push(maxPrice);
}
// 查询数据
const [list] = await db.query(
`SELECT id, name, category, breed, age, price, cover_image, health_status, view_count
FROM pet ${where} ORDER BY create_time DESC LIMIT ? OFFSET ?`,
[...params, parseInt(pageSize), offset]
);
// 查询总数
const [countRows] = await db.query(`SELECT COUNT(*) AS total FROM pet ${where}`, params);
res.json({
code: 200,
data: {
list,
total: countRows[0].total,
page: parseInt(page),
pageSize: parseInt(pageSize)
}
});
} catch (err) {
next(err);
}
};
注意这里用LIMIT ? OFFSET ?做分页时,mysql2的占位符会自动处理数字类型,但MySQL的LIMIT子句里不能用字符串参数直接替换,所以需要先parseInt,这也是一个实战中容易踩的坑。
4.3 文件上传接口与图片访问路径
宠物图片上传是一个很容易被忽略但实际必须处理的功能。multer的DiskStorage模式可以很方便地把文件保存到本地目录:
javascript复制// routes/upload.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const router = express.Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '../public/uploads'));
},
filename: function (req, file, cb) {
const ext = path.extname(file.originalname);
const filename = Date.now() + '-' + Math.round(Math.random() * 1e9) + ext;
cb(null, filename);
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 限制5MB
fileFilter: (req, file, cb) => {
const allowTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowTypes.includes(ext)) {
cb(null, true);
} else {
cb(new Error('仅支持图片文件'));
}
}
});
// 单图上传接口
router.post('/image', upload.single('file'), (req, res) => {
if (!req.file) {
return res.json({ code: 400, message: '上传失败' });
}
const url = `/uploads/${req.file.filename}`;
res.json({ code: 200, message: '上传成功', data: { url } });
});
module.exports = router;
这里的一个重要设计是:上传成功后的图片URL,建议返回相对路径(/uploads/xxx.jpg),前端在拼接时用当前部署域名加路径。这样无论你是本地开发还是部署到云服务器,都不会因为写死域名导致链接失效。
5. 前端与小程序端:一套业务如何在两个端口落地
后端接口写好后,就要处理前端展示。这个题目下,前端一般有两个形态:PC端网站和微信小程序端。无论做哪个,核心都是把后端返回的数据渲染到页面上,并实现用户操作交互。
5.1 PC端页面的核心交互与前后端联调思路
PC端我建议使用原生HTML + CSS + JavaScript或者Vue 2/3,具体看个人熟练程度。如果时间充裕,推荐Vue + Element UI的组合,做出来的后台管理界面效率高、观感好;如果不想引入前端工程化,直接原生三件套 + jQuery也是可以的,毕设对前端框架没有强制要求。
页面交互的核心是“数据流”。以宠物列表页为例,流程是:页面加载 -> 向后端发GET请求 /api/pet/list -> 拿到JSON数据 -> 渲染成宠物卡片 -> 用户点击“领养” -> 弹出申请表单 -> 提交POST请求 -> 刷新页面状态。这个循环做顺了,后面所有页面都是这个模式的变体。
前后端联调时最容易出现的问题就是跨域。如果你用Vue开发,开发服务器在8080端口,后端NodeJS跑在3000端口,那么前端调用后端接口就属于跨域请求。解决办法已经在后端用cors()中间件处理过了,但你要确保前端请求确实带上了正确的请求头和参数格式。推荐两个调试工具:浏览器开发者工具里的Network面板(看请求状态码和返回数据)和Postman(单独测试后端接口)。遇到问题先判断是前端没发对,还是后端没处理好。
5.2 小程序端的适配要点与登录态获取
如果你打算做小程序端,一定要注意几个微信小程序特有的技术问题。热词里提到的“小程序获取登录后的微信用户失败”就是典型场景。小程序获取用户信息的正确姿势是:
- 用户在小程序端点击登录,调用 wx.login() 获取临时code。
- 把code发送到自己的后端接口。
- 后端通过 code 换取 openid 和 session_key(需要调用微信接口认证,这要求你的服务器有已认证的小程序AppSecret)。
- 后端生成自定义登录态token返回给小程序端。
这里有一个很常见的坑:小程序端不能直接把openid当作登录凭证返回给前端,因为openid一旦泄露,任何人都能冒充那个用户。正确的做法是后端用自己的JWT把openid包装成token,前端保存token用于后续请求。
小程序访问后端接口时,还有几个容易踩的坑:
- 每一个请求的域名需要在小程序管理后台配置到“服务器域名”白名单里,否则真机预览时请求会失败。
- 开发阶段可以在“详情 -> 本地设置”里勾选“不校验合法域名”,让localhost可以请求通过。
- 小程序里的网络请求不是浏览器环境,没有跨域概念,不需要cors,但你后端的接口返回必须是JSON格式且HTTP状态码正确。
- 图片资源必须用https地址,如果你把宠物图片传到本地服务器,小程序上是无法显示的。解决办法是使用对象存储(如阿里云OSS、腾讯云COS)或HTTPS服务器。
小程序端和后端的交互逻辑跟PC端完全一样,只是把axios/fetch换成了wx.request。我建议你优先把PC端做主流程,小程序端做部分核心功能即可——比如宠物展示、搜索、详情、申请领养这四个模块。这样既体现了多端适配能力,又不至于工作量翻倍。
6. 部署上线与答辩现场:项目跑通只是开始,讲清楚才是高分
代码写完、功能测完,很多同学就以为万事大吉了。实际上,“能运行”和“能答辩”之间还有两段路要走:部署到服务器上做线上演示,以及准备好答辩讲解的思路和常见问题。这两个环节决定了你最终的成绩上限。
6.1 把NodeJS项目部署到云服务器的完整步骤
如果你有云服务器(哪怕是学生优惠的轻量应用服务器),强烈建议把项目部署上去,答辩时用线上地址现场演示,效果比本地跑好太多。
部署流程基本上是这样的:
- 服务器上安装NodeJS环境。推荐使用nvm管理多版本:
bash复制curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
nvm install 16
nvm use 16
- 安装MySQL,创建数据库并导入表结构:
bash复制sudo apt update
sudo apt install mysql-server
mysql -u root -p < pet_website.sql
- 把项目代码传到服务器(可以用git、scp、或者宝塔面板直接上传)。
- 在服务器上执行 npm install 安装依赖,配置.env环境变量(数据库账号密码、JWT密钥等)。
- 使用PM2启动项目并设置为开机自启:
bash复制npm install -g pm2
pm2 start app.js --name pet-website
pm2 save
pm2 startup
- 使用Nginx配置反向代理,把域名/公网IP的80端口转发到NodeJS的3000端口。这样一个基本的部署就完成了。
这里我要特别提醒一个坑:如果使用云服务器,记得在云厂商的安全组规则里放行对应端口。很多同学部署完了发现访问不了,排查一圈才发现是安全组没开80端口或3000端口。
6.2 答辩演示的流程设计和高频问题准备
答辩演示的时间通常只有5-10分钟,你要提前设计好演示顺序,把最核心的功能和最有亮点的部分放到前面。我的建议演示路径是这样的:
- 先打开系统首页,花10秒展示整体界面和宠物分类,给评委一个直观印象。
- 演示用户注册登录流程,说明密码加密存储和JWT鉴权机制。
- 演示宠物列表的搜索和筛选功能,展示一个宠物详情页。
- 演示领养/购买申请流程,然后切到管理后台,审核通过这个申请,再切回用户端展示状态变化。这样评委能看到一个完整的业务闭环。
- 最后时间允许时,展示数据库表结构或项目目录,说明技术架构。
答辩时老师最爱问的高频问题,我整理成了一张表,你可以提前准备好答案:
| 典型问题 | 建议回答思路 |
|---|---|
| 为什么选择NodeJS技术栈? | 结合开发效率、前后端同语言、生态完善来回答,不要只答“好上手” |
| 用户密码是怎么存储的? | 说明bcrypt加盐哈希原理,解释为什么不能明文存储 |
| JWT和Session有什么区别? | 说明JWT无状态、适合分布式部署,Session适合传统单机应用 |
| 宠物状态和订单状态怎么保持一致? | 说明事务处理或状态联动更新的实现方式 |
| 项目有哪些可以改进的地方? | 主动说可以接Redis缓存热门宠物、消息队列处理订单并发、支付功能等 |
答辩的本质上考察的是“你有没有真的动手做”。只要代码确实是自己一行行写出来的,这些问题都不会成为障碍。
最后,作为一个带过不少人做毕设的人,我想说的是,做宠物网站这个题目,真正的收获不在于代码量,而在于你把这个系统的业务逻辑、数据结构、前后端交互完整走了一遍。哪怕中间踩了很多坑也没关系,踩坑的过程就是学习的过程。做完这个项目,你不仅拿到了一份能顺利通过答辩的毕设,更重要的是建立起了“从需求到代码再到部署”的完整项目思维——这个能力,是你毕业后进入职场最受用的底层能力。别贪多求全,先把核心流程跑通,再慢慢往上面加花活,稳扎稳打才是王道。
