1. 项目背景与技术选型思考
校园外卖系统作为高校生活服务的重要一环,其技术实现需要兼顾快速开发与稳定运行。我选择Node.js+Vue+ElementUI这套技术栈,主要基于以下实际考量:
后端选择Express的三大理由:
- 中间件机制完美适配外卖业务的流程化处理(订单状态变更、骑手调度等)
- 路由分层清晰,比如将用户模块、订单模块、骑手模块分别建立router文件
- 与MySQL的配合经过大量项目验证,特别是事务处理方面稳定可靠
前端选型Vue+ElementUI的实战优势:
- 双向数据绑定特性在表单密集的外卖系统中显著提升开发效率
- ElementUI的布局组件(如el-container)和表单校验规则开箱即用
- 饿了么团队的实际业务验证了这套UI在订单类系统的适用性
踩坑提示:曾尝试用Ant Design Vue做骑手轨迹页面,发现地图集成不如ElementUI+高德方案流畅,最终回退到ElementUI
2. 数据库设计与核心表结构
2.1 MySQL表关系规划
sql复制CREATE TABLE `rider` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '骑手姓名',
`phone` char(11) NOT NULL COMMENT '联系方式',
`status` tinyint(1) DEFAULT '1' COMMENT '1空闲 2配送中',
`location` point NOT NULL COMMENT '当前坐标',
`score` decimal(3,1) DEFAULT '5.0' COMMENT '评分',
PRIMARY KEY (`id`),
SPATIAL KEY `location` (`location`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表设计关键点:
- 使用DECIMAL(10,2)存储金额避免浮点误差
- 建立复合索引(user_id, create_time)优化查询
- 预留JSON字段存储动态扩展的订单信息
2.2 空间数据优化实践
骑手位置追踪使用MySQL的GIS功能:
javascript复制// 更新骑手位置
const [results] = await pool.query(
'UPDATE rider SET location = ST_PointFromText(?) WHERE id = ?',
[`POINT(${lng} ${lat})`, riderId]
);
// 查询3公里内骑手
const [riders] = await pool.query(
`SELECT id, name,
ST_Distance_Sphere(location, ST_PointFromText(?)) as distance
FROM rider WHERE status = 1
HAVING distance < 3000 ORDER BY distance LIMIT 5`,
[`POINT(${lng} ${lat})`]
);
3. 核心业务逻辑实现
3.1 订单状态机设计
javascript复制// 订单状态枚举
const ORDER_STATUS = {
PENDING: 1, // 待支付
PAID: 2, // 已支付
ACCEPTED: 3, // 已接单
DELIVERING: 4, // 配送中
COMPLETED: 5, // 已完成
CANCELLED: 6 // 已取消
};
// 状态转换校验中间件
function validateStatusTransition(current, next) {
const allowed = {
[ORDER_STATUS.PENDING]: [ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED],
[ORDER_STATUS.PAID]: [ORDER_STATUS.ACCEPTED, ORDER_STATUS.CANCELLED],
// ...其他状态转换规则
};
return allowed[current]?.includes(next);
}
3.2 骑手智能调度算法
基于距离和评分加权计算:
javascript复制async function dispatchOrder(orderId) {
const order = await getOrderLocation(orderId);
const riders = await findNearbyRiders(order.restaurantLocation);
// 综合评分算法
const scoredRiders = riders.map(r => ({
...r,
score: (r.distance * 0.3) + ((10 - r.userScore) * 0.7)
})).sort((a,b) => a.score - b.score);
const bestRider = scoredRiders[0];
await assignOrderToRider(orderId, bestRider.id);
}
4. 前后端交互关键实现
4.1 Vuex状态管理设计
javascript复制// store/modules/order.js
export default {
state: {
currentOrder: null,
historyOrders: []
},
mutations: {
UPDATE_ORDER_STATUS(state, { orderId, status }) {
const order = state.currentOrder ||
state.historyOrders.find(o => o.id === orderId);
if (order) order.status = status;
}
},
actions: {
async refreshOrderStatus({ commit }, orderId) {
const res = await axios.get(`/api/orders/${orderId}/status`);
commit('UPDATE_ORDER_STATUS', {
orderId,
status: res.data.status
});
}
}
}
4.2 ElementUI表单增强实践
vue复制<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="phone" label="联系电话">
<el-input v-model="form.phone">
<template #append>
<el-button @click="getLocation">定位</el-button>
</template>
</el-input>
</el-form-item>
<!-- 动态表单字段 -->
<el-form-item
v-for="(item,index) in dynamicItems"
:key="index"
:prop="'items.' + index + '.value'"
:rules="{required: true, message: '不能为空'}">
<el-input v-model="item.value" />
</el-form-item>
</el-form>
</template>
5. 性能优化专项
5.1 MySQL查询优化
慢查询解决方案:
- 为订单表添加覆盖索引:
sql复制ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); - 分页查询优化:
sql复制-- 反例(大数据量时性能差) SELECT * FROM orders LIMIT 10000, 20; -- 正例(基于上次ID优化) SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20;
5.2 Node.js层缓存策略
使用redis实现多级缓存:
javascript复制async function getOrderDetails(orderId) {
const cacheKey = `order:${orderId}`;
let data = await redis.get(cacheKey);
if (!data) {
data = await db.query('SELECT * FROM orders WHERE id = ?', [orderId]);
// 设置缓存,订单数据缓存5分钟
await redis.setex(cacheKey, 300, JSON.stringify(data));
}
return data;
}
6. 实际部署中的经验教训
环境配置的坑:
- Node.js版本问题:必须使用LTS版本(如16.x),某些npm包在最新版会报错
- MySQL时区设置:务必在连接池配置时区
javascript复制const pool = mysql.createPool({ timezone: '+08:00' }); - Express的bodyParser需要显式配置:
javascript复制app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true }));
微信支付集成技巧:
javascript复制// 签名算法关键步骤
function createSign(params, key) {
const stringA = Object.keys(params)
.filter(k => params[k] && k !== 'sign')
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
const stringSignTemp = `${stringA}&key=${key}`;
return crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();
}
7. 扩展功能实现思路
7.1 骑手轨迹实时追踪
技术方案:
- WebSocket保持长连接
- 高德地图JS API绘制路径
- 轨迹压缩算法减少数据量
javascript复制// 前端接收轨迹点
socket.on('locationUpdate', (data) => {
const path = this.map.getPath();
path.push([data.lng, data.lat]);
this.marker.setPosition([data.lng, data.lat]);
// 使用Douglas-Peucker算法压缩轨迹
if (path.length % 10 === 0) {
this.simplifiedPath = simplify(path, 0.0001);
this.polyline.setPath(this.simplifiedPath);
}
});
7.2 智能派单算法进阶
考虑更多维度:
- 骑手当前负载(未完成订单数)
- 交通工具类型(电动车/自行车)
- 商家特殊要求(某些餐厅指定骑手)
javascript复制function calculateRiderScore(rider, order) {
const baseScore = rider.score * 0.5;
const distanceScore = normalizeDistance(rider.distance) * 0.3;
const loadScore = (1 - rider.currentOrders / 5) * 0.2;
// 特殊商家加分项
if (order.restaurant.requiresCertified && rider.isCertified) {
return baseScore + distanceScore + loadScore + 0.5;
}
return baseScore + distanceScore + loadScore;
}
在项目上线后,我们发现骑手端的GPS定位漂移问题需要特别处理。通过实践总结出有效的坐标纠偏方法:采集连续5个坐标点,剔除明显偏离的异常点后取平均值,同时结合道路数据Snap到最近路线上。这种处理使轨迹显示更加平滑准确,骑手实际配送路径与系统显示的匹配度提升了40%。
