作为一名长期从事企业级应用开发的工程师,我最近完成了一个基于SSM框架的微信小程序代驾管理系统。这个项目源于当前代驾服务市场的快速增长需求,以及传统代驾服务在效率和服务质量方面的痛点。
代驾行业近年来发展迅猛,据不完全统计,仅2023年全国代驾服务订单量就突破了5亿单。然而,传统的电话预约、人工派单模式存在响应慢、信息不对称、服务质量难以保证等问题。我们的系统正是为了解决这些问题而设计的。
在技术选型上,我们采用了成熟的SSM(Spring+SpringMVC+MyBatis)框架组合:
选择SSM框架主要基于以下考虑:
系统采用典型的三层架构:
code复制┌───────────────────────────────────────┐
│ 微信小程序前端 │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ SpringMVC控制器层 │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ Service业务层 │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ MyBatis持久层 │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ MySQL数据库 │
└───────────────────────────────────────┘
前后端通过RESTful API进行数据交互,接口返回统一格式的JSON数据。
用户注册流程采用手机号+验证码方式,核心代码如下:
java复制// UserController.java
@PostMapping("/register")
public Result register(@RequestBody User user, String verifyCode) {
// 验证码校验
if(!verifyService.checkVerifyCode(user.getPhone(), verifyCode)) {
return Result.error("验证码错误");
}
// 密码加密
user.setPassword(DigestUtils.md5Hex(user.getPassword()));
user.setCreateTime(new Date());
// 保存用户
if(userService.save(user)) {
return Result.success("注册成功");
}
return Result.error("注册失败");
}
密码存储采用MD5加密,虽然现在推荐使用更安全的BCrypt,但考虑到项目规模和性能要求,MD5在加盐处理后仍可满足基本安全需求。
系统采用基于角色的访问控制(RBAC)模型:
sql复制-- 数据库表设计
CREATE TABLE `user` (
`user_id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`phone` varchar(20) NOT NULL,
`role` enum('user','driver','admin') NOT NULL DEFAULT 'user',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在Spring Security配置中,我们通过注解方式控制接口访问权限:
java复制@PreAuthorize("hasRole('admin')")
@GetMapping("/admin/users")
public Result listUsers() {
// 管理员才能访问的用户列表接口
}
代驾订单有多种状态,我们使用状态模式来管理订单生命周期:
code复制[待接单] → [已接单] → [服务中] → [已完成]
│ │ │
└→ [已取消] ←───────┘
状态转换的核心逻辑:
java复制// OrderServiceImpl.java
public Result changeOrderStatus(Long orderId, OrderStatus newStatus) {
Order order = orderMapper.selectById(orderId);
OrderStatus currentStatus = order.getStatus();
// 验证状态转换是否合法
if(!currentStatus.canTransferTo(newStatus)) {
return Result.error("非法状态转换");
}
// 执行状态更新
order.setStatus(newStatus);
order.setUpdateTime(new Date());
orderMapper.updateById(order);
// 记录状态变更日志
orderLogService.recordStatusChange(orderId, currentStatus, newStatus);
return Result.success();
}
系统采用基于距离的订单分配策略:
java复制// OrderDispatchService.java
public Driver dispatchOrder(Order order) {
// 1. 获取订单起点坐标
Location startLocation = order.getStartLocation();
// 2. 查询附近5公里内空闲司机
List<Driver> availableDrivers = driverService.findAvailableDrivers(
startLocation,
5, // 5公里范围
DriverStatus.IDLE
);
// 3. 按距离排序并选择最近的司机
availableDrivers.sort(Comparator.comparingDouble(
d -> LocationUtils.distance(d.getLocation(), startLocation)
));
return availableDrivers.isEmpty() ? null : availableDrivers.get(0);
}
实际项目中,这个算法可以进一步优化,考虑司机评分、接单率等因素。
系统支持微信支付和余额支付两种方式。微信支付对接微信小程序支付API:
java复制// PaymentController.java
@PostMapping("/wxpay")
public Result createWxPayment(@RequestBody PaymentRequest request) {
// 1. 验证订单
Order order = orderService.getById(request.getOrderId());
if(order == null || !order.canPay()) {
return Result.error("订单不可支付");
}
// 2. 调用微信支付统一下单接口
WxPayUnifiedOrderRequest wxRequest = new WxPayUnifiedOrderRequest();
wxRequest.setBody("代驾服务费");
wxRequest.setOutTradeNo(generateTradeNo());
wxRequest.setTotalFee(order.getAmount());
wxRequest.setOpenid(request.getOpenid());
try {
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(wxRequest);
return Result.success(result);
} catch (WxPayException e) {
log.error("微信支付失败", e);
return Result.error("支付创建失败");
}
}
支付成功后,系统会通过微信小程序订阅消息通知用户:
java复制// 支付成功回调处理
@PostMapping("/wxpay/notify")
public String handleWxPayNotify(@RequestBody String xmlData) {
// 验证签名和处理支付结果
WxPayOrderNotifyResult result = wxPayService.parseOrderNotifyResult(xmlData);
if("SUCCESS".equals(result.getResultCode())) {
orderService.paySuccess(result.getOutTradeNo());
// 发送支付成功通知
messageService.sendPaySuccessMessage(result.getOpenid(), result.getOutTradeNo());
}
return WxPayNotifyResponse.success("OK");
}
系统主要包含以下核心表:
用户相关表:
user:用户基本信息driver_info:司机详细信息user_address:用户常用地址订单相关表:
order:订单主表order_log:订单状态变更日志order_evaluation:订单评价系统管理表:
system_config:系统配置operation_log:操作日志sql复制CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`driver_id` bigint DEFAULT NULL COMMENT '司机ID',
`start_address` varchar(255) NOT NULL COMMENT '起点地址',
`start_location` point NOT NULL COMMENT '起点坐标',
`end_address` varchar(255) NOT NULL COMMENT '终点地址',
`end_location` point NOT NULL COMMENT '终点坐标',
`estimated_distance` decimal(10,2) NOT NULL COMMENT '预估距离(公里)',
`estimated_amount` decimal(10,2) NOT NULL COMMENT '预估金额',
`actual_amount` decimal(10,2) DEFAULT NULL COMMENT '实际金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_driver_id` (`driver_id`),
KEY `idx_status` (`status`),
SPATIAL KEY `idx_start_location` (`start_location`),
SPATIAL KEY `idx_end_location` (`end_location`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代驾订单表';
为提高查询性能,我们在以下字段上建立了索引:
order_no(唯一索引)小程序主要包含以下页面:
使用微信小程序地图组件实现位置选择和路线展示:
javascript复制// pages/order/create.js
Page({
data: {
markers: [],
polyline: [],
startPoint: null,
endPoint: null
},
// 选择起点
chooseStartLocation() {
wx.chooseLocation({
success: (res) => {
this.setData({ startPoint: res });
this.calculateDistance();
}
});
},
// 计算距离和预估费用
calculateDistance() {
if(this.data.startPoint && this.data.endPoint) {
wx.request({
url: 'https://apis.map.qq.com/ws/distance/v1/',
data: {
mode: 'driving',
from: `${this.data.startPoint.latitude},${this.data.startPoint.longitude}`,
to: `${this.data.endPoint.latitude},${this.data.endPoint.longitude}`,
key: '您的腾讯地图KEY'
},
success: (res) => {
const distance = res.data.result.elements[0].distance; // 米
const duration = res.data.result.elements[0].duration; // 秒
const amount = this.calculateAmount(distance);
this.setData({ distance, duration, amount });
}
});
}
},
// 计算费用
calculateAmount(distance) {
const basePrice = 39; // 起步价39元
const unitPrice = 5; // 每公里5元
const km = distance / 1000;
return basePrice + Math.max(0, km - 3) * unitPrice;
}
});
使用WebSocket实现用户与司机的实时通信:
javascript复制// 建立WebSocket连接
const socket = wx.connectSocket({
url: 'wss://yourdomain.com/ws',
success: () => {
console.log('连接成功');
}
});
// 监听消息
socket.onMessage((res) => {
const message = JSON.parse(res.data);
switch(message.type) {
case 'ORDER_ACCEPTED':
this.showDriverInfo(message.data);
break;
case 'DRIVER_LOCATION':
this.updateDriverMarker(message.data);
break;
case 'CHAT_MESSAGE':
this.addChatMessage(message.data);
break;
}
});
// 发送消息
function sendMessage(content) {
socket.send({
data: JSON.stringify({
type: 'CHAT_MESSAGE',
data: {
content,
timestamp: Date.now()
}
})
});
}
所有API接口都进行了以下安全防护:
采用基于注解的细粒度权限控制:
java复制@RestController
@RequestMapping("/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public Result listUsers() {
// 仅管理员可访问
}
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
@PostMapping("/orders/audit")
public Result auditOrder(@RequestBody AuditRequest request) {
// 管理员和运营人员可访问
}
}
使用Redis缓存热点数据:
java复制// OrderServiceImpl.java
@Cacheable(value = "orders", key = "#orderId")
public Order getOrderById(Long orderId) {
return orderMapper.selectById(orderId);
}
@CachePut(value = "orders", key = "#order.id")
public Order updateOrder(Order order) {
orderMapper.updateById(order);
return order;
}
@CacheEvict(value = "orders", key = "#orderId")
public void deleteOrder(Long orderId) {
orderMapper.deleteById(orderId);
}
使用Spring的@Async注解处理非实时任务:
java复制// NotificationService.java
@Async
public void sendOrderNotification(Order order) {
// 发送短信通知
smsService.send(order.getUserPhone(), "您的代驾订单已被接单");
// 发送小程序模板消息
wxTemplateMsgService.sendOrderAcceptedMsg(
order.getUserOpenid(),
order.getOrderNo()
);
}
采用Docker容器化部署:
dockerfile复制# Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/driver-system.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
使用Docker Compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- redis
- mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: driver_db
ports:
- "3306:3306"
问题描述:支付回调有时会延迟或丢失,导致订单状态不同步。
解决方案:
问题描述:高峰期多个司机同时抢单可能导致订单被重复分配。
解决方案:
核心代码示例:
java复制// 使用Redis分布式锁
public boolean dispatchWithLock(Long orderId) {
String lockKey = "order:dispatch:" + orderId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁,有效期10秒
boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, 10, TimeUnit.SECONDS
);
if(locked) {
// 执行业务逻辑
return doDispatch(orderId);
}
return false;
} finally {
// 释放锁
if(requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
问题描述:附近司机查询在数据量大时性能下降明显。
解决方案:
这个代驾管理系统项目从需求分析到最终上线历时4个月,期间遇到了不少技术挑战,但也收获了很多宝贵的经验。系统上线后运行稳定,日均处理订单量达到5000+,获得了用户和司机的一致好评。
几个关键的技术收获:
未来可能的改进方向: