每年到这个时间点,总能收到一堆学弟学妹的消息,问毕业设计到底做什么选题合适。如果仔细去看近几年计算机类毕设选题的变化,会发现前后端分离、全栈小系统这类题目占的比例越来越大。原因倒也简单:一是能够完整体现四年所学;二是工作量可控,三两个人时间排得开;三是系统可演示、可扩展,论文也好写。而“Nodejs+vue3的家政系统”正是其中非常典型、也非常适合拿来深挖的一个题目。
这篇文章我把整套系统从选题分析、环境搭建、前后端设计,到数据库表结构、接口联调、答辩准备的完整链路都梳理一遍。这篇内容面向的读者很明确:准备用Nodejs和Vue3写毕业设计或者课程设计,但又对这套技术栈不熟、心里没底的人。你不需要已经有很强的全栈经验,只要耐心跟着把每一块想清楚,这题目基本就能稳稳落地。
1. 家政系统整体设计与模块拆解
很多人上来就写代码,这是毕设里最常见的翻车方式。家政系统这种管理类项目,本身不存在什么高难算法,核心价值在于业务逻辑是否完整、模块划分是否清晰。所以第一步,一定是先把系统边界和功能结构定下来。
1.1 家政系统的核心业务流程
家政系统要解决的业务问题很直白:把有家政服务需求的用户和能够提供服务的家政人员连接起来,再在中间加上订单管理、服务评价、后台审核这些管理动作。如果只用一句话概括,这就是一个带服务撮合属性的信息管理系统。
梳理业务流程时,可以先把角色列清楚。系统一般有三类角色:管理员、普通用户、家政服务人员。管理员负责审核阿姨入驻资料、管理服务分类、处理用户反馈;用户负责浏览服务、下单、支付、评价;家政人员负责接单、完成服务、提现结算。这三类角色之间的操作关系,就是系统所有页面和接口的出发点。
拿一个最核心的流程来举例:用户浏览到“日常保洁”这一服务分类,点进去看到了家政人员的资料卡片,包括服务次数、好评率、接单量,用户选好时间提交预约订单,家政人员在线抢单或由管理员派单,服务完成后用户确认結单并评价。这个流程走通,系统的主体任务就完成了。
1.2 功能模块划分与页面清单
确定好角色和核心流程后,就可以把功能模块拆得足够细。根据实际项目管理经验,家政系统的功能模块通常包含这几块:
- 用户端小程序/H5页面模块:注册登录、服务分类展示、家政人员列表与详情、预约下单、订单列表与详情、服务评价、个人中心、收藏管理。
- 家政人员端模块:个人资质信息维护、服务档期设置、订单接单与拒绝、服务状态流转、收益记录、提现申请。
- 管理后台模块:登录与权限校验、服务分类与项目维护、家政人员入驻审核、订单管理、用户管理、评价管理、数据统计看板。
- 公共模块:轮播图管理、公告管理、系统参数配置、登录鉴权中间件。
这个功能列表看起来不复杂,但要注意的是,每写一个功能都要想清楚它的数据来源和流向。比如订单状态从“待接单”变到“服务中”再到“待评价”,这个状态机谁触发、何时触发,必须在设计阶段就明确,否则后面前后端联调一定出乱子。
1.3 为什么推荐用Nodejs+Vue3技术组合
选Nodejs作为后端,大多数人第一个疑虑是:这么写能不能过答辩?答案是不仅能过,而且只要把道理讲清楚,反而能成为一个加分项。
第一,Nodejs使用JavaScript语言,前后端语言统一,意味着你只需要精通一种语言,就能独立完成全栈开发。对于个人开发为主的毕设场景,这大大降低了研发成本。
第二,Nodejs特别适合家政系统这种I/O密集型的业务场景。系统里大量动作是增删改查和请求转发,比如用户下单、查询阿姨档期、更新订单状态,本质都是轻计算、重I/O。Nodejs的异步事件驱动模型在处理这类短小高频请求时有天然优势。
第三,生态成熟,Express或者Koa框架都足够稳定,连mysql的驱动、jwt鉴权库、参数校验库都现成可用。
前端选Vue3也同理,Composition API把逻辑按功能聚合之后,维护一个中后台系统的体验比Vue2舒服很多。配上Element Plus这种成熟组件库,后台管理页面一周内就能拉出原型。对需要写论文画架构图的同学来说,前端用Vue3、后端用Nodejs、中间用RESTful API通信这套方案,本身就非常标准,图形化表达也很直观。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开题前必须搞定的环境准备与工程化配置
这个部分,是每年卡住最多人的地方。代码本身倒没有多复杂,反而是Nodejs安装、npm报错、Vue项目创建这些“环境问题”劝退了大量选手。热搜里满是“npm.ps1无法加载文件”、“nodejs安装及环境配置”,足以说明这是个共性问题。
2.1 Nodejs安装与环境变量配置的细节
到Nodejs官网下载LTS版本的安装包,记住要选LTS而不是Current。LTS代表长期支持版本,稳定性更好,很多第三方依赖对它的兼容测试也更充分。安装时一路默认下一步即可,但有一个点必须注意:安装目录最好不要带空格和中文,默认的 C:\Program Files\nodejs 虽然能用,但后续在PowerShell里碰到目录带空格引发的引号问题还是挺烦的。个人建议安装时手动改成 C:\nodejs。
安装完成后验证是否成功,打开命令行工具输入:
bash复制node -v
npm -v
能够输出版本号,就说明安装成功了。如果提示“node不是内部或外部命令”,基本可以断定环境变量没有配置好,需要手动打开系统属性里的“环境变量”,在Path中把Nodejs的安装目录和全局模块目录加进去。
这里有几个常见的坑值得多提一句。很多同学机器上原来就装过老版本Nodejs,再装新版本时版本冲突导致node命令混乱。排查时先运行 where node,把历史残留的路径删干净再重新安装。另外,npm默认全局安装路径如果不想往系统盘塞东西,可以用下面命令把全局路径改到自己创建的目录上:
bash复制npm config set prefix "D:\nodejs_global"
npm config set cache "D:\nodejs_cache"
这样之后安装任何全局工具(例如后面会用到的一些脚手架)都会落到非系统盘,避免C盘空间告急,也方便统一管理。
2.2 新电脑必踩的PowerShell执行策略问题
如果你在VS Code的终端里执行任何npm命令时,遇到下面这串提示:
text复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本
别慌,这是PowerShell的执行策略起了拦截作用,并不是Nodejs装坏了。Windows默认禁止执行本地脚本,而npm.ps1恰恰就是个PowerShell脚本。解决办法是管理员方式打开PowerShell,执行:
bash复制Set-ExecutionPolicy RemoteSigned
然后选Y确认,再重新打开终端就能正常跑了。顺便记一下:如果哪天觉得不放心想改回默认策略,执行 Get-ExecutionPolicy 查看当前状态,用 Set-ExecutionPolicy Restricted 就能还原。这个报错未来在任意Node项目里都可能遇到,属于“早踩早踏实”的经典坑。
2.3 创建Vue3项目与集成Element Plus
环境问题解决后,就可以用Vite来创建Vue3项目,注意创建命令不同版本略有差异。推荐使用下面的方式:
bash复制# 使用npm创建vite项目
npm create vite@latest housekeeping-admin -- --template vue
# 进入项目并安装依赖
cd housekeeping-admin
npm install
如果需要用到TypeScript版本,模板参数改成 vue-ts 即可。Vite创建的项目默认已经带了Vue3 SFC支持,开发热更新非常快,强烈不建议再回头用Vue CLI创建Vue3项目,Vite已经是现代Vue项目的标准方式了。
接着安装路由和UI组件库,这是中后台系统的标配:
bash复制npm install vue-router@4
npm install element-plus
npm install axios
Element Plus按需引入还是完整引入,可以根据习惯来。如果为了省事,直接在main.js里全量引入也完全够用,毕竟毕设系统不可能在意几十KB的体积差异。但如果在答辩时想展示自己对性能有考虑,可以在项目里配合unplugin-auto-import做按需引入,这个属于加分项。
全局引入的写法贴在下面,方便直接抄:
javascript复制// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
到这里,前端项目的骨架已经拉起来了。数据库方面,如果你本机没装MySQL管理工具,建议直接装一个集成环境(例如PHPStudy或者Docker跑MySQL),把数据库搞定后接下来就可以连着做数据库设计,这个分工后面详说。
3. 从数据库设计到核心接口实现的实操过程
架构思路和环境准备好之后,就进入这个系统的核心开发段。为了不让内容停留在空谈阶段,我用“家政人员模块”和“订单模块”来演示具体的实现。这两个模块基本覆盖了项目的重难点。
3.1 家政系统的数据库表设计思路
数据库设计要抛开“存数据”的表象,真正去思考业务关系。家政系统最少需要这几张核心表:
- user(用户表):id、用户名、密码(加密后保存)、手机号、角色(1用户/2阿姨/3管理员)、头像、状态、创建时间。
- worker_profile(家政人员信息扩展表):id、user_id、真实姓名、身份证号、服务分类id、服务区域、服务单价、简介、实名认证状态、好评率。
- service_category(服务分类表):id、分类名、图标、排序、是否展示。
- order(订单表):id、订单号、用户id、家政人员id、服务项目id、服务时间、服务地址、金额、状态(待支付/待接单/待服务/服务中/已完成/已取消)、创建时间。
- comment(评价表):id、订单id、用户id、评分、内容、创建时间。
- admin_audit(入驻审核记录表):id、家政人员id、提交材料、审核状态、管理员id、审核时间。
表之间关系不复杂,但设计时要想清楚两个业务细节。
第一个是要不要订单表和评价表分离。答案是必须。评价描述的是“服务完成后用户对这次服务的反馈”,它基于订单产生,但不应直接挂在订单字段里,否则日后要做统计报表时查询会很别扭。
第二个是家政人员的报价是挂在人员档案上还是服务分类上。毕设系统的业务通常是阿姨自己定价,所以把单价字段放在家政人员扩展表里更合理。当用户浏览同一种服务时,不同阿姨价格不同,页面展示逻辑也更自然。
下面给出用户表和订单表的SQL参考,后面项目里可以直接改改列名复用:
sql复制CREATE TABLE `user` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL COMMENT '登录名',
`password` VARCHAR(255) NOT NULL COMMENT '哈希后的密码',
`phone` VARCHAR(20) DEFAULT NULL,
`role` TINYINT NOT NULL DEFAULT 1 COMMENT '1用户 2家政人员 3管理员',
`avatar` VARCHAR(255) DEFAULT NULL,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `order` (
`id` INT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`user_id` INT NOT NULL,
`worker_id` INT NOT NULL,
`service_time` DATETIME NOT NULL COMMENT '预约上门时间',
`address` VARCHAR(255) NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1待接单 2待服务 3服务中 4待评价 5已完成 6已取消',
`remark` VARCHAR(255) DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字符集一定要用utf8mb4,不要用utf8。utf8在MySQL里最多只支持3字节编码,遇到生僻字或部分表情符号(对,就是那个经典的emoji乱码问题)根本存不进去。现在随便哪个线上系统都默认utf8mb4,毕设里写这个细节,答辩老师听了会觉得你确实有真实项目经验。
3.2 后端工程创建与Express接口骨架
后端目录我建议单独建一个文件夹,跟前端平级,命名成 housekeeping-server:
bash复制mkdir housekeeping-server && cd housekeeping-server
npm init -y
npm install express mysql2 cors jsonwebtoken bcryptjs
Express是目前Nodejs后端用起来最顺手、教程最多、坑最少的框架,非常适合第一次写完整后端的人。mysql2比老牌的mysql包多支持了Promise,配合 async/await 写代码要舒服得多。
为毕设项目把工程复杂化是没必要的,但起码的目录分层还是要做到。我常用的简单分层方式:
app.js入口文件,绑定路由和中间件config/db.js数据库连接池middleware/auth.js登录态校验中间件routes/存放各模块路由controllers/存放具体业务处理方法
数据库连接池配置可以参考下面写法,连接池是生产环境标配,笔试或答辩时被问到“为什么不用单连接”也可以顺势解释:MySQL连接数有限,频繁创建销毁连接开销大,连接池复用的方式能显著降低响应延迟。
javascript复制// config/db.js
const mysql = require('mysql2/promise')
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: '123456',
database: 'housekeeping',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
charset: 'utf8mb4'
})
module.exports = pool
3.3 家政人员列表接口的完整实现
下面用一个“按服务分类查询家政人员列表”的接口作为例子,走一遍后端Router到Controller的完整过程。这个接口对应前端家政人员列表页,数据包括基本档案和服务单价。
javascript复制// controllers/workerController.js
const pool = require('../config/db')
// 根据服务分类查询入驻并通过审核的家政人员列表
exports.getWorkerList = async (req, res) => {
try {
const { categoryId, page = 1, pageSize = 10 } = req.query
const offset = (page - 1) * pageSize
const where = ['wp.audit_status = 1']
const params = []
if (categoryId) {
where.push('wp.category_id = ?')
params.push(categoryId)
}
const whereSql = 'WHERE ' + where.join(' AND ')
const [rows] = await pool.query(
`SELECT wp.id, wp.real_name, wp.avatar, wp.service_price, wp.good_rate,
sc.name AS category_name
FROM worker_profile wp
LEFT JOIN service_category sc ON wp.category_id = sc.id
${whereSql}
ORDER BY wp.order_count DESC
LIMIT ? OFFSET ?`,
[...params, Number(pageSize), Number(offset)]
)
const [countResult] = await pool.query(
`SELECT COUNT(*) AS total
FROM worker_profile wp
${whereSql}`,
params
)
res.json({
code: 200,
data: {
list: rows,
total: countResult[0].total,
page: Number(page),
pageSize: Number(pageSize)
}
})
} catch (err) {
console.error(err)
res.status(500).json({ code: 500, message: '服务器内部错误' })
}
}
这个接口里几个细节值得说道说道。
第一是服务分类用LEFT JOIN而不是直接存分类名字段,这是标准数据库范式设计的做法,保证分类改名后不需要批量更新历史数据。第二是分页参数用 Number() 做了类型转换,因为从URL query里取出来的参数默认是字符串,如果你直接把 pageSize 拼进SQL里的LIMIT,mysql2在某些情况下会报语法错误。第三是筛选条件故意只取 audit_status = 1 的记录,未通过审核的阿姨不能出现在前台列表里,这条业务规则是后台审核功能能闭环的前提。
下面在路由文件里绑定一下:
javascript复制// routes/worker.js
const express = require('express')
const router = express.Router()
const workerController = require('../controllers/workerController')
router.get('/list', workerController.getWorkerList)
module.exports = router
然后在 app.js 注册路由前缀:
javascript复制app.use('/api/worker', require('./routes/worker'))
app.listen(3000, () => console.log('server running at http://localhost:3000'))
到这里,后端一个常规列表接口已经可以从浏览器访问通了。前端请求 /api/worker/list?categoryId=5&page=1&pageSize=10 就会拿到JSON结构的数据,到这一步前后端就已经能关联起来。
这里做一次启动自测非常关键。建议先在浏览器地址栏直接访问接口地址,看到返回的JSON后再开始写前端,不要一上来就联调。理由很现实:如果接口还没验证就写前端页面,一旦页面数据不对,你根本分不清是前端渲染链路的问题、请求封装的问题、还是后端接口本身就没通。自己给自己埋了排查地雷。
3.4 登录鉴权与JWT的落地方式
家政系统有两个端需要登录态管理,前台用户和后台管理员。可以用同一套JWT方案覆盖,在token上标记角色字段即可。
JWT的实际使用方式是:用户登录成功后,后端用服务端密钥签发一个token,里面包含userId和role,设置好过期时间,返回给前端。前端每次请求自动带上这个token,后端在受保护的路由上拦截并解析。
签发token的代码核心部分:
javascript复制const jwt = require('jsonwebtoken')
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET || 'housekeeping_secret',
{ expiresIn: '7d' }
)
然后写一个校验中间件:
javascript复制// middleware/auth.js
const jwt = require('jsonwebtoken')
module.exports = (req, res, next) => {
const header = req.headers.authorization || ''
const token = header.startsWith('Bearer ') ? header.slice(7) : null
if (!token) {
return res.status(401).json({ code: 401, message: '未提供认证令牌' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'housekeeping_secret')
req.user = decoded
next()
} catch (err) {
return res.status(401).json({ code: 401, message: '登录已过期,请重新登录' })
}
}
有一点必须提醒:不要让前端页面跳转逻辑决定接口是否安全。路由级的鉴权中间件才是真正的安全边界。前端隐藏按钮只是体验优化,后端必须在每个需要身份的接口上做token校验,这个属于重要但不紧急的安全习惯。
3.5 前端Composition API组装逻辑页面
前端核心页面写起来其实有非常强的套路。以家政人员列表页面为例,用Vue3的组合式API实现数据请求、加载状态、分页和条件查询的组装。
页面模板大概长这样,列出卡片式家政人员信息:
vue复制<template>
<div class="worker-list">
<el-row :gutter="20">
<el-col :span="8" v-for="worker in list" :key="worker.id">
<el-card class="worker-card">
<div class="info">
<el-avatar :src="worker.avatar" :size="64" />
<div>
<h4>{{ worker.real_name }}</h4>
<p>{{ worker.category_name }} · 好评率{{ worker.good_rate }}%</p>
<p class="price">¥{{ worker.service_price }}/次</p>
</div>
</div>
<el-button type="primary" @click="goDetail(worker.id)">查看详情</el-button>
</el-card>
</el-col>
</el-row>
<el-pagination
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
v-model:current-page="page"
/>
</div>
</template>
逻辑部分用Composition API组织:
vue复制<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { getWorkerList } from '@/api/worker'
const router = useRouter()
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const categoryId = ref('')
const loading = ref(false)
const fetchList = async () => {
loading.value = true
try {
const res = await getWorkerList({
page: page.value,
pageSize: pageSize.value,
categoryId: categoryId.value
})
list.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
onMounted(fetchList)
watch(page, fetchList)
const goDetail = (id) => {
router.push(`/worker/${id}`)
}
</script>
写到这里,把script里几个成员的用途、页面上的按钮事件、分页变化触发重新请求,这三个维度的数据流搞明白,Vue3页面的核心读写模式就基本掌握了。
再具体说说axios请求封装。真实项目里一定会有统一的请求实例,至少要处理基础路径、请求超时、token注入和统一错误提示。
javascript复制// api/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器:自动带token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:统一处理业务错误
request.interceptors.response.use(
response => {
if (response.data.code === 200 || response.data.code === undefined) {
return response.data
}
ElMessage.error(response.data.message || '请求失败')
return Promise.reject(new Error(response.data.message))
},
error => {
ElMessage.error(error.response?.data?.message || '网络异常')
return Promise.reject(error)
}
)
export default request
其实每次写axios封装都想多说一句:把后端返回的错误信息统一在上面这个地方弹出来,而不是在每个页面里写try/catch后手动处理错误,是可以省下大量重复工作的,也让页面逻辑干净很多。
4. 前后端联调中的常见问题与排查实录
联调是毕设开发最痛的一段。很多新人写前端时没问题,写后端时也没问题,两台同时开就疯狂报错,而且错误千奇百怪。整理几个毕设项目里高频出现的问题,早点看清,少熬夜。
4.1 跨域问题与本地代理配置
当你用Vite跑前端页面(默认是 http://localhost:5173),直接去请求后端接口(默认是 http://localhost:3000),十有八九会在浏览器控制台看到CORS错误。浏览器安全机制默认禁止跨域请求数据。
解决方案有两条,个人建议直接走代理这条路。先在后端设置允许跨域:如果用 cors 中间件,就调用 app.use(cors())。但这对生产环境来说太随意了,本地开发倒无所谓。更好的方式是前端开启Vite代理,把 /api 开头的请求转发到后端端口,前后端服务保持当前同源。
在 vite.config.js 中增加:
javascript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})
配置完成后,前端请求 /api/worker/list,开发服务器会自动把请求转发到 http://localhost:3000/api/worker/list。用这种方法,不需要在后端代码里额外开放跨域,而且和线上部署场景更贴近,答辩时讲得出来。
4.2 状态管理要先想清楚是否必要
很多同学一看Vue3项目就条件反射地装上Pinia,用没用得上先不管,装完还时常迷惑“到底哪里该用store”。这里给个非常明确的边界。
家政系统里到底哪些状态需要全局共享?最典型的就是登录用户信息。用户登录后,用户基本信息(头像、昵称、角色)在导航栏、个人中心、下单页面等多个互不关联的组件中都要读取。再比如订单状态一旦被某个组件修改,订单列表页、订单详情页和下拉栏角标都要同步更新。这些场景用Pinia就非常合适。
把用户信息放进Pinia的写法如下:
javascript复制// store/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null
}),
getters: {
isLogin(state) {
return Boolean(state.token)
}
},
actions: {
setLoginInfo(token, userInfo) {
this.token = token
this.userInfo = userInfo
localStorage.setItem('token', token)
},
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
}
}
})
反过来,如果只是单个页面内部组件间传值,用ref定义在父组件里再通过props传下来就够了。很多新手最喜欢犯的错是:为了修改一个列表项的展示状态,放进了store,导致整个应用状态混乱、点击一处全体联动。组内状态和全局状态分不清,在项目答辩里很容易被老师追问翻车。
4.3 computed与响应式使用不当的经典问题
Vue3里热度很高的computed,在实际业务场景中出现的频率比想象还高。比如用户下单时要同时展示“服务原价”、“优惠金额”和“最终应付金额”,如果每次都在模板里写表达式,模板会很臃肿而且容易算错。更干净的做法是用computed把多重计算逻辑收敛起来。
vue复制<script setup>
import { ref, computed } from 'vue'
const servicePrice = ref(1000)
const couponAmount = ref(300)
const discountRate = ref(0.9)
const payAmount = computed(() => {
const baseTotal = servicePrice.value * discountRate.value
const finalTotal = Math.max(baseTotal - couponAmount.value, 0)
return finalTotal.toFixed(2)
})
</script>
<template>
<p>原价:¥{{ servicePrice }}</p>
<p>优惠后应付:¥{{ payAmount }}</p>
</template>
为什么用computed而不是直接在页面标签里写方法调?因为conputed会基于响应式依赖做缓存,只有依赖项变化时才会重新求值,性能更好。而普通函数在模板里每次渲染都会重新执行一遍。例如上面这个例子,如果服务价格不变,页面因为别的原因重新渲染,computed根本不会重复执行计算,这就是价值所在。
另一个导致Vue3响应式失效的经典坑是解构props或者reactive对象。很多人会在setup里图方便,直接把props对象解构出来用,结果页面怎么刷新数据都是死数。原因在于解构的过程中,响应式引用已经丢失了。正确做法是保留完整对象访问,或者使用 toRefs 转为ref后使用:
vue复制<script setup>
import { toRefs } from 'vue'
const props = defineProps({
orderNo: String
})
const { orderNo } = toRefs(props)
</script>
每年写Vue3的人都容易在这一块踩坑,搜热词里也有大量的“vue3 用…”、“vue3引用失效”,基本都指向了这个点。
4.4 生命周期与路由缓存引起的页面不刷新
还有一个在后台管理页面很经典的问题:从订单列表进入订单详情,再返回列表,页面数据还是旧的。这通常不是因为组件没写对,而是因为列表页被路由缓存了,组件不会重新创建,所以 onMounted 里的数据加载逻辑不会再次执行。
理清Vue3的生命周期就能顺手解决。列表页在普通情况下每次进入都会走“挂载”阶段,但如果用了KeepAlive缓存组件,它从缓存恢复时会走 onActivated 钩子,而不是重新挂载:
vue复制<script setup>
import { onActivated, onMounted } from 'vue'
const loadData = async () => {
// 每次进入页面时获取最新订单数据
}
onMounted(loadData)
onActivated(loadData)
</script>
这样处理之后,哪怕页面处于缓存状态,用户从详情页回到列表时也会刷新数据。家政后台这类场景特别受用:用户浏览多个服务后返回分类列表,如果列表一直不刷新,新的阿姨永远展示不出来,体验很糟糕。后面前端有更多页面时,关于 onActivated 的出现频率会远高于 onMounted。
5. 开发后期容易被忽略的收尾工作与答辩经验
系统功能做完只算走了一半路。开题报告、中期检查、论文说明、答辩Pre,每一个环节都有导致你延毕感的幺蛾子。这套流程走到收尾阶段的经验,分享一些实实在在的。
5.1 梳理完整流程并写测试用例
开发期容易一头扎进页面写代码,系统跑通主流程就自以为大功告成。等你着手写论文时才会发现,技术文档需要用文字明确写出全流程的输入、输出和异常分支。如果代码没有测试过各种场景,论文里举的例子就只敢写成功路径,整体观感非常单薄,答辩老师一看就觉得是假的。
建议至少做一轮业务封闭测试,把这几类case都走一遍:
- 正常流程:注册普通用户 - 浏览分类 - 下单 - 管理员派单 - 服务完成 - 用户评价。
- 异常分支:用户未登录直接下单会被弹到登录页;余额不足提交订单失败;上传非图片格式的资质材料被拦截。
- 权限控制:普通用户访问管理员接口返回401;家政人员不能修改订单金额字段。
- 数据容错:并发点击多次提交订单只会生成一笔订单;搜索关键词不存在时展示空态而不是报错。
这些内容不光能给论文里的系统测试章节补充大量素材,也能提升系统演示环节的操作流畅度。很多同学在答辩演示时现场翻车,往往是没提前测过这些基础场景。
5.2 目录结构、接口文档与Git提交规范
代码托管习惯也要提前建立。如果目前还没使用Git,建议立刻用起来,只需掌握最基础的几条命令就足够应付单人毕设:
bash复制git init
git add .
git commit -m "完成用户模块"
git push origin main
交论文初稿前,把Git仓库链接附到附录里,也是证明自己开发过程真实有效的一个方式。每条commit信息保持语义清晰即可,比如“feat: 新增订单状态流转接口”,不需要花里胡哨的规范体系,但至少不要一条“update”走天下。
接口文档也值得写。团队开发时接口文档是协作约定,单人开发时它就是帮你唤起记忆的笔记。一篇“Nodejs家政系统后端接口清单.md”放在项目根目录,按模块列出method、path、query/body参数、返回JSON示例。一两个星期后再去改代码,保存调试时间不说,论文的技术设计章节素材也有了。
5.3 纸质文档和答辩讲稿的构思建议
开题报告和最终论文,重点不是堆代码,而是交代清楚“为什么要做”“怎么设计”“如何实现”。建议把系统的部署方式也写入论文。这个常见吗?非常常见,通过项目名称、项目启动脚本、数据库初始化脚本即可把这个部分写清楚。
写开题时,通常需要重点交代:系统背景(家政服务O2O/信息共享)、开发意义(提升服务撮合率)、可行性分析(技术可行,经济成本低)、拟解决问题(订单状态管理、服务保障)。这里的难点其实不在于技术有多先进,而在于逻辑要严密。
答辩讲稿的准备思路建议如下:
- 2分钟以内完成项目背景和角色介绍,不要拖。
- 重点讲3个技术亮点:RESTful接口设计、JWT鉴权方案、Composition API的组件复用。每一块都结合具体业务(鉴权对应后台审核流程、订单状态设计对应派单逻辑)。
- 准备好3个技术深挖问题的回答:Nodejs单线程如何应对高并发(答主线程调度加异步I/O);为什么家表格中不直接存用户名(答范式设计);Vite和Webpack的区别(答开发服务器的依赖预构建与HMR策略)。
- 针对演示环节做两套准备:快乐路径一套、断网故障预案一套。断网时准备用现场截图快速展示核心功能,防止整体事故。
写在最后
我当年做这个题目时最深的体会是:毕设的关键不在于用了多牛的技术,而在于能否把一件普通的事情做到逻辑自洽、细节完整。从数据库字段设计到前端组件拆分,到接口状态返回码统一,每一处微小的思考都会反映在整个系统质量上。
如果现在对Nodejs和Vue3还比较生疏,建议先从官方的“快速上手”教程入手,搭建一个Todo应用,理解前后端如何通过接口联动,再回来动手写家政系统。这个流程走通后,后续系统的搭建都会顺畅很多。遇到环境问题或报错信息,耐心搜索、分析再解决,几次下来,这些原本“劝退”的问题反而会变成你成长最快的部分。这套系统只要一步步稳扎稳打写完,答辩真没有想象中那么可怕。
