1. 项目概述:SSM+Vue车位租赁系统全栈解决方案
这个基于SSM+Vue的车位租赁系统,是我去年为一个商业园区开发的智慧停车解决方案。当时园区管理方正面临车位周转率低、人工管理成本高的问题,传统纸质登记方式导致30%的车位在高峰时段处于闲置状态。我们通过这套系统实现了车位资源可视化、预约自动化、结算电子化,上线三个月后车位利用率提升了45%。
系统采用经典的前后端分离架构:后端使用SSM框架(Spring+SpringMVC+MyBatis)构建RESTful API,前端采用Vue.js实现动态交互界面,数据库选用MySQL 8.0存储业务数据。特别值得一提的是,我们通过LW(Layui-WangEditor)富文本组件实现了公告通知的图文混排功能,让物业管理人员可以像使用Word一样编辑停车须知。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构解析
2.1 后端SSM框架组合
Spring 5.2作为控制反转容器,通过声明式事务管理保证了车位状态变更和支付记录生成的原子性。这里有个实际开发中的经验:我们在@Transactional注解中特别配置了rollbackFor=Exception.class,因为支付宝回调超时异常曾导致过数据不一致问题。
SpringMVC采用REST风格设计API,一个典型的车位查询接口是这样的:
java复制@GetMapping("/parking-spaces")
public ResultVO<List<ParkingSpace>> getAvailableSpaces(
@RequestParam String zone,
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm") Date startTime,
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm") Date endTime) {
// 时空重叠查询逻辑
}
MyBatis 3.5配合PageHelper分页插件处理复杂查询,比如这个车位状态历史记录查询:
xml复制<select id="selectUsageHistory" resultMap="historyMap">
SELECT * FROM parking_record
WHERE space_id = #{spaceId}
<if test="startDate != null">
AND end_time >= #{startDate}
</if>
ORDER BY start_time DESC
</select>
2.2 前端Vue.js实现
采用Vue CLI 4搭建项目骨架,几个关键实现点:
- 使用Vue Router的导航守卫控制权限:
javascript复制router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next()
}
})
- 车位状态实时展示采用WebSocket:
javascript复制const socket = new WebSocket(`wss://${location.host}/ws/parking`)
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
store.commit('updateSpaceStatus', data)
}
- 日历组件优化:改造了vue2-datepicker使其支持半小时为单位的时段选择,关键修改点在
timePickerOptions配置:
javascript复制timePickerOptions: {
start: '08:00',
step: '00:30',
end: '22:00'
}
3. 数据库设计要点
3.1 核心表结构
sql复制CREATE TABLE `parking_space` (
`id` INT NOT NULL AUTO_INCREMENT,
`zone` VARCHAR(10) COMMENT '区域编号A/B/C',
`number` VARCHAR(20) COMMENT '车位编号',
`type` ENUM('standard','disabled','charging') DEFAULT 'standard',
`status` ENUM('available','reserved','in_use') DEFAULT 'available',
`hourly_rate` DECIMAL(10,2) DEFAULT 5.00,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_zone_number` (`zone`,`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别说明几个设计决策:
- 将车位状态与预约状态分离,避免业务逻辑冲突
- 使用DECIMAL存储费率而非FLOAT,防止金额计算精度问题
- 建立(zone,number)联合唯一索引,避免车位重复登记
3.2 预约业务表设计
sql复制CREATE TABLE `reservation` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`space_id` INT NOT NULL,
`start_time` DATETIME NOT NULL,
`end_time` DATETIME NOT NULL,
`actual_end_time` DATETIME DEFAULT NULL,
`total_fee` DECIMAL(10,2) DEFAULT NULL,
`payment_status` TINYINT DEFAULT 0 COMMENT '0未支付 1已支付 2已退款',
`plate_number` VARCHAR(20) NOT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_time_range` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
重要提示:时间范围查询必须建立复合索引,我们曾因漏建索引导致在10万条记录查询时出现800ms延迟
4. 关键业务逻辑实现
4.1 车位冲突检测算法
在预约时最复杂的业务就是时间冲突检测,我们最终采用的方案是:
java复制public boolean checkAvailability(Integer spaceId, LocalDateTime start, LocalDateTime end) {
return reservationMapper.selectOverlappingReservations(spaceId, start, end) == 0;
}
// MyBatis查询实现
@Select("SELECT COUNT(*) FROM reservation WHERE space_id = #{spaceId} " +
"AND ((start_time < #{end} AND end_time > #{start}) " +
"OR (actual_end_time IS NULL AND end_time > #{start}))")
int selectOverlappingReservations(@Param("spaceId") Integer spaceId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
这个方案考虑了三种冲突情况:
- 新预约开始时间落在已有预约时段内
- 新预约结束时间落在已有预约时段内
- 已有预约未按时结束(actual_end_time为NULL)
4.2 计费规则引擎
采用策略模式实现不同车型的差异化计费:
java复制public interface BillingStrategy {
BigDecimal calculateFee(LocalDateTime start, LocalDateTime end, BigDecimal baseRate);
}
@Component("standard")
public class StandardBilling implements BillingStrategy {
@Override
public BigDecimal calculateFee(LocalDateTime start, LocalDateTime end, BigDecimal baseRate) {
long minutes = ChronoUnit.MINUTES.between(start, end);
return baseRate.multiply(BigDecimal.valueOf((minutes + 29) / 30)); // 不足半小时按半小时计
}
}
@Component("monthly")
public class MonthlyBilling implements BillingStrategy {
@Override
public BigDecimal calculateFee(LocalDateTime start, LocalDateTime end, BigDecimal baseRate) {
return baseRate.multiply(BigDecimal.valueOf(0.6)); // 月卡6折优惠
}
}
5. 开发中遇到的典型问题
5.1 微信支付回调处理
最初版本没有做好幂等控制,导致重复回调时生成多条支付记录。改进方案:
java复制@Transactional
public boolean handlePaymentNotify(String outTradeNo, String transactionId) {
// 1. 检查是否已处理过
PaymentRecord existing = paymentMapper.selectByTransactionId(transactionId);
if (existing != null) return true;
// 2. 乐观锁更新
int updated = reservationMapper.updatePaymentStatus(
outTradeNo,
PaymentStatus.PAID.getCode(),
PaymentStatus.UNPAID.getCode());
if (updated == 0) {
log.warn("支付状态更新失败,可能已被其他回调处理");
return false;
}
// 3. 记录支付流水
PaymentRecord record = new PaymentRecord();
record.setTransactionId(transactionId);
paymentMapper.insert(record);
return true;
}
5.2 车位状态同步延迟
前端最初采用轮询方式获取车位状态,导致服务器压力大。后改用WebSocket+增量更新方案:
java复制@ServerEndpoint("/ws/parking")
public class ParkingSpaceEndpoint {
private static final Map<Integer, Session> spaceSubscriptions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("zone") String zone) {
// 订阅指定区域的车位更新
}
public static void notifySpaceUpdate(ParkingSpace space) {
Session session = spaceSubscriptions.get(space.getId());
if (session != null) {
session.getAsyncRemote().sendText(JSON.toJSONString(space));
}
}
}
6. 部署优化实践
6.1 Nginx配置要点
nginx复制# Vue静态资源缓存
location /static {
alias /var/www/parking/static;
expires 365d;
add_header Cache-Control "public";
}
# API反向代理
location /api {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 75s;
proxy_read_timeout 300s;
}
# WebSocket代理
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
6.2 数据库连接池配置
在Spring配置中优化Druid连接池参数:
properties复制# 初始连接数
spring.datasource.druid.initial-size=5
# 最小空闲连接
spring.datasource.druid.min-idle=5
# 最大活跃连接
spring.datasource.druid.max-active=20
# 获取连接等待超时时间(毫秒)
spring.datasource.druid.max-wait=60000
# 连接有效性检测SQL
spring.datasource.druid.validation-query=SELECT 1
# 检测空闲连接间隔(毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000
这套配置在我们压力测试中表现最佳,200并发时平均响应时间保持在300ms以内。有个值得注意的教训:最初设置的max-active=50反而导致在高并发时出现大量连接超时,降低到20后系统更稳定。
