1. 项目背景与技术选型
家庭服务预约管理系统是近年来市场需求快速增长的一类应用,它能够有效连接服务提供者和家庭用户,解决传统电话预约效率低下、服务记录难以追踪等问题。我们选择Node.js+Vue+ElementUI+Express+MySQL这套技术栈,主要基于以下几个考量:
技术栈优势分析:
- Node.js:异步I/O特性适合高并发的预约请求处理,npm生态丰富(每天处理3000+请求时,事件循环机制比传统线程池模式节省40%内存)
- Vue+ElementUI:组件化开发效率高,ElementUI的表格、表单、日历等组件完美契合后台管理系统需求(实测比从零开发节省60%时间)
- Express:轻量级路由和中间件机制,适合快速构建RESTful API(一个基础接口开发仅需15分钟)
- MySQL:关系型数据库保证事务完整性,5.7+版本支持JSON字段满足灵活的业务扩展
实际开发中发现:ElementUI 2.15.10版本与Vue 3存在兼容性问题,建议锁定版本为ElementUI 2.13.2 + Vue 2.6.14组合
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 整体架构图
采用前后端分离架构:
code复制[浏览器] ←HTTP→ [Nginx] ←反向代理→ [Node.js] ←MySQL驱动→ [数据库]
↑
[静态资源]
2.2 核心模块划分
-
用户模块
- JWT身份验证(access_token有效期2小时+refresh_token机制)
- 权限控制(RBAC模型,细粒度到按钮级别)
-
服务管理
- 三级分类体系(家电维修→空调维修→壁挂式空调)
- 动态定价算法(基础价+时段系数+紧急程度系数)
-
预约引擎
- 时间片冲突检测(B+树索引优化查询)
- 微信/短信通知(阿里云SDK集成)
-
支付对账
- 微信支付/支付宝双渠道(定时任务每日凌晨2点对账)
-
评价系统
- 敏感词过滤(AC自动机算法,5000+词库)
- 评分加权计算(新用户权重0.8,老用户1.2)
3. 数据库设计要点
3.1 关键表结构
sql复制CREATE TABLE `orders` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL COMMENT '业务流水号',
`user_id` INT(11) NOT NULL,
`service_id` MEDIUMINT(8) NOT NULL,
`time_slots` JSON NOT NULL COMMENT '{"date":"2023-07-15","periods":[1,2]}',
`status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已预约 2-服务中 3-已完成',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orderno` (`order_no`),
KEY `idx_user` (`user_id`),
KEY `idx_service` (`service_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 性能优化实践
- 时间范围查询:为
time_slots字段添加虚拟列并建立索引
sql复制ALTER TABLE orders ADD COLUMN service_date DATE
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(time_slots, '$.date'))) VIRTUAL;
CREATE INDEX idx_service_date ON orders(service_date);
- 连接池配置:使用
mysql2库,建议配置
javascript复制pool: {
max: 50, // 最大连接数
min: 10, // 最小保持连接
acquire: 30000, // 获取连接超时(ms)
idle: 10000 // 连接空闲时间(ms)
}
4. 前端关键实现
4.1 预约日历组件
基于ElementUI二次开发的时间选择器:
vue复制<template>
<el-date-picker
v-model="selectedDate"
type="date"
:disabled-date="disableDates"
@change="loadTimeSlots">
</el-date-picker>
<div v-for="slot in timeSlots" :key="slot.id">
<el-button
:type="slot.available ? 'primary' : 'info'"
@click="selectSlot(slot)">
{{ slot.startTime }}-{{ slot.endTime }}
</el-button>
</div>
</template>
<script>
export default {
methods: {
disableDates(time) {
return time.getTime() < Date.now() - 8.64e7 ||
time.getDay() === 0; // 禁用过去日期和周日
}
}
}
</script>
4.2 性能优化技巧
- 路由懒加载:减少首屏加载时间
javascript复制const ServiceList = () => import('./views/ServiceList.vue')
- API请求节流:使用lodash的throttle
javascript复制import { throttle } from 'lodash'
methods: {
search: throttle(function(keyword) {
this.$http.get('/api/services', { params: { q: keyword } })
}, 500)
}
- 本地缓存策略:服务分类数据使用localStorage缓存
javascript复制// 获取服务分类
async loadCategories() {
let cached = localStorage.getItem('serviceCategories')
if (cached) {
this.categories = JSON.parse(cached)
} else {
const res = await this.$http.get('/api/categories')
localStorage.setItem('serviceCategories', JSON.stringify(res.data))
}
}
5. 后端核心逻辑
5.1 预约冲突检测算法
javascript复制async checkConflict(serviceId, date, periods) {
const overlapping = await Order.findAll({
where: {
service_id: serviceId,
status: [1, 2], // 只检查"已预约"和"服务中"状态
[Op.and]: [
Sequelize.literal(`JSON_EXTRACT(time_slots, '$.date') = '${date}'`),
Sequelize.literal(`JSON_OVERLAPS(
JSON_EXTRACT(time_slots, '$.periods'),
'[${periods}]'
)`)
]
}
});
return overlapping.length > 0;
}
5.2 支付回调处理
javascript复制router.post('/payment/notify', async (ctx) => {
const { order_no, status } = ctx.request.body;
// 防重处理
const cacheKey = `payment:${order_no}`;
if (await redis.get(cacheKey)) {
ctx.status = 200;
return;
}
await redis.set(cacheKey, 1, 'EX', 3600);
// 事务处理
const transaction = await sequelize.transaction();
try {
const order = await Order.findOne({ where: { order_no } });
if (status === 'success') {
await order.update({ status: 1 }, { transaction });
await sendSMS(order.user_id, '预约成功通知');
}
await transaction.commit();
} catch (err) {
await transaction.rollback();
logger.error('支付回调处理失败', err);
}
});
6. 部署实践
6.1 PM2关键配置
javascript复制module.exports = {
apps: [{
name: 'service-booking',
script: './bin/www',
instances: 'max', // 根据CPU核心数自动扩展
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '1G', // 内存超过1G自动重启
error_file: '/var/log/pm2/err.log',
out_file: '/var/log/pm2/out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm'
}]
}
6.2 性能监控方案
- ELK日志收集:Filebeat + Elasticsearch + Kibana
- APM工具:使用Elastic APM监控接口性能
javascript复制const apm = require('elastic-apm-node').start({
serviceName: 'booking-api',
serverUrl: 'http://apm-server:8200'
});
// 在Express中插入中间件
app.use(apm.middleware.express());
7. 踩坑与解决方案
ElementUI表格固定列渲染问题:
当表格同时设置fixed和scoped-slot时,滚动条区域可能出现渲染异常。解决方案:
vue复制<el-table :data="list" style="width: 100%">
<el-table-column prop="date" label="日期" width="180" fixed>
<template #default="{row}">
<span>{{ row.date | formatDate }}</span> <!-- 必须用span包裹 -->
</template>
</el-table-column>
</el-table>
MySQL时区问题:
服务器返回的时间与本地相差8小时,需在连接配置中添加:
javascript复制const sequelize = new Sequelize(db, user, pass, {
dialect: 'mysql',
dialectOptions: {
useUTC: false, // 禁用UTC转换
timezone: '+08:00' // 东八区时间
}
})
文件上传内存溢出:
使用express-fileupload时大文件会导致内存暴涨,应配置limits:
javascript复制app.use(fileUpload({
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB限制
abortOnLimit: true,
responseOnLimit: '文件大小超过限制'
}));
