接手这个智能停车场管理系统的时候,我刚从上一个排期特别紧的项目里抽身出来。客户那边停车场刚完成硬件改造,但原有的一套进出场记录系统基本靠人工登记,高峰期出入口排长队,车主经常因为计费争议闹到物业办公室。需求很明确:做一个让车主能自主查询车位、扫码进出场、在线支付费用的平台,同时给管理员一个能实时监控全场状态的后台。技术栈定的是SpringBoot+Vue,这是当时团队最熟悉、也最能控制交付风险的组合。
这个选题其实很典型。SpringBoot负责后端接口、业务逻辑和数据库交互,Vue负责前端页面和交互体验,两者通过Restful API通信,就是标准的“前后端分离”架构。整套系统做下来,覆盖了登录鉴权、车位管理、订单计费、统计报表、异常处理这些几乎所有管理系统都要面对的问题,所以不管你是拿它做毕业设计、个人实战项目,还是给中小型停车场做数字化改造的参考原型,这套方案都能直接落地。
我写这篇文章,会把我在需求分析、数据库设计、后端接口实现、前端页面联调以及部署上线过程中踩过的坑和最终采用的方案完整梳理一遍。内容会偏实战,尽量不扯虚的,每个环节我都会说清楚为什么这么做,以及哪些地方容易出事。
1. 系统整体设计与技术选型
1.1 为什么选择SpringBoot+Vue前后端分离
先聊技术选型。市面上做管理系统能用的框架很多,Django、Flask、Go的Gin都能做,但SpringBoot在这个场景下确实有它不可替代的优势。
第一,SpringBoot解决了传统SSH、SSM框架配置繁琐的问题。早期用Spring MVC做项目,光是XML配置就要写一大堆,数据源、事务管理器、视图解析器、拦截器,每个都要单独配置。SpringBoot用自动装配机制把这些常见的配置全部内置了,我只需要引入对应的starter依赖,框架会自动根据类路径下的jar包来创建对应的Bean。比如我引入spring-boot-starter-data-redis,RedisTemplate就会自动注册到容器里,不需要手动写一串配置类。
第二,生态成熟度在这类管理系统里是压倒性的优势。权限控制有Spring Security和Shiro,接口文档有Swagger和Knife4j,持久层有MyBatis-Plus,这些组件的稳定性和资料丰富程度,让团队协作时的沟通成本低很多。对于预算有限的停车管理项目来说,稳定比炫技重要得多。
Vue这一侧的选择也很明确。Vue的双向数据绑定让表单类的页面写起来非常舒服,组件化的开发方式可以把车位地图、收费规则配置、报表图表这些部分独立出来复用。Vue Router做页面路由非常顺手,特别是动态路由这块,可以根据用户的角色权限来动态挂载对应的菜单和页面,这个特性我在后面权限管理时会详细说。
前后端分离还有个实际好处:部署时可以独立扩展。后端接口压力大的时候可以多挂几个实例做负载均衡,前端静态文件放在Nginx里就行,两边互不干扰。这对停车场这种可能有多个出入口、多个管理员同时操作的系统来说很实用。
1.2 系统模块划分与核心流程设计
在动手写代码之前,我先把整个系统的功能边界划清楚。停车场管理系统如果边做边想,很容易做成一个什么都往里塞的大杂烩。我最终定下来的模块划分是这样的:
- 车主端(微信小程序或H5):车位查询、预约车位、扫码进场、扫码出场、在线支付、停车记录查询
- 管理端(Vue后台):车位管理、车辆管理、订单管理、收费规则配置、月卡管理、报表统计、管理员账号管理
- 设备对接层:道闸控制器、摄像头识别(车牌识别)、地感线圈检测
核心的业务流程可以抽象成三条主链路:
第一条是进场链路。车辆到达入口,摄像头识别车牌号,系统判断这个车牌是临时车还是月卡车,临时车自动抬杆放行并创建一条进行中的停车记录,月卡车校验有效期后放行。这个链路最核心的点就是“进场记录”要保证唯一性和原子性,不能出现同一辆车重复进场记录的情况。
第二条是出场链路。车辆到达出口,摄像头再次识别车牌,系统查询进行中的停车记录,计算停车时长和费用,展示给车主缴费。缴费完成后抬杆放行,同时更新停车记录状态为已完成。这个链路的核心是计费规则的准确性和并发控制。
第三条是管理链路。管理员登录后台,可以实时查看车位的占用情况、每辆车的进出记录、当天的收入汇总。这个相对简单,主要是统计查询和可视化展示。
这样划分之后,后端接口的边界就很清楚了。每个模块对应一组Controller,业务逻辑在Service层做,数据操作通过Mapper层访问MySQL,缓存和分布式锁用Redis,文件上传用MinIO存储车牌识别照片。整个结构是标准的DDD分层思想,不算复杂,但胜在清晰。
2. 数据库设计与核心表结构
2.1 数据建模的关键考量
数据库设计是整个系统里最不该省功夫的部分。停车场系统的数据有一个特点:写入频繁,查询维度多样,而且金额相关字段必须绝对准确。基于这些特点,我在设计表结构时做了几个关键决策。
第一,停车记录表单独拆分,不跟车位表做物理外键关联。原因很简单:一辆车在一个停车场可能会多次进出,车位是会复用的,如果停车记录直接挂车位ID的外键,每次车辆离场释放车位后,这个关联关系就要置空,操作起来很别扭。我选择让停车记录表只保存车位编号的冗余字段,业务上通过逻辑来保证一致性,不依赖数据库外键。这样做还有一个好处,就是以后如果要支持多停车场扩展,只需要在这个字段里带上停车场标识就可以。
第二,金额字段使用DECIMAL类型,千万不要用FLOAT或者DOUBLE。这个是我最早做财务相关功能时踩过的坑,浮点数在计算分账的时候会出现0.1+0.2不等于0.3的情况,放到停车收费场景里,哪怕一分钱的误差,月底跟物业对账的时候都是灾难。DECIMAL(10,2)足够覆盖绝大多数停车场的单笔金额范围。
第三,所有核心表都加上create_time、update_time、deleted这三个字段。前两个是审计字段,排查问题的时候非常有用。deleted是逻辑删除标志,这是我特意做的决定,因为停车记录涉及财务对账,物理删除会破坏数据链,万一之后需要追溯,数据没了就麻烦了。MyBatis-Plus提供了@TableLogic注解,逻辑删除用起来成本很低。
2.2 车位表与停车记录表设计细节
车位表的设计相对简单,核心字段是车位编号、区域、类型(普通/充电桩/无障碍)、状态。重点是状态字段的取值设计。我用的是0-空闲 1-占用 2-预留 3-故障四种状态,其中“预留”是给月卡固定车位和预约车辆用的,“故障”用于维护中的车位。很多设计会把预留和占用混在一起,实际运营时如果某个车位被预约了,管理员在后台看到的状态应该是“预留”,而不是“占用”,这关系到后续统计报表的准确性。
停车记录表是系统里最重要的表,我列出核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,雪花算法生成 |
| plate_number | varchar | 车牌号 |
| car_type | tinyint | 车辆类型:0临时车 1月卡车 |
| entry_time | datetime | 进场时间 |
| exit_time | datetime | 离场时间,未离场则为null |
| duration_minutes | int | 停车时长(分钟) |
| total_amount | decimal | 应收金额 |
| paid_amount | decimal | 实付金额 |
| status | tinyint | 0进行中 1已完成 2已支付待离场 3异常 |
| entry_image | varchar | 进场抓拍照片URL |
| exit_image | varchar | 离场抓拍照片URL |
这里有个细节要注意:exit_time为什么允许为null?因为车辆进场后可能长时间未出场,如果一开始就设置成NOT NULL,进场记录创建时就要填一个默认值,这会让“是否已离场”的判断变得麻烦。允许null反而简单,查询正在停放的车辆时直接WHERE exit_time IS NULL就行。
月卡表单独设计,字段包括车牌号、套餐类型(月卡/季卡/年卡)、生效时间、到期时间、状态。月卡是停车场重要的预付收入来源,到期前7天系统需要自动给车主发送续费提醒,这个可以在后端写一个定时任务来实现,SpringBoot的@Scheduled注解就能搞定,不用引入额外的任务调度框架。
用户表设计分为管理员表和车主表。管理员表主要给后台登录用,角色字段区分超级管理员和普通管理员。车主表关联车牌号和手机号,主要用于小程序端的注册登录和车辆绑定。这里有个设计建议:管理员表和车主表最好分开,因为他们的字段差异很大,混在一张表里会导致大量字段冗余,查询效率也会受影响。
3. 后端核心功能实现
3.1 登录鉴权与JWT实现
登录鉴权我选的是JWT方案,没有用Spring Security那套重的OAuth2流程。停车场管理系统的用户角色简单,就管理员和车主两种,不需要复杂的第三方授权,JWT无状态、跨域友好的特点正好合适。
JWT的token结构分三段:Header、Payload、Signature。Header里记录加密算法,Payload里面放用户ID、用户名、角色、过期时间。Signature部分用密钥对Header和Payload做签名,防止篡改。后端每个需要登录的接口都经过一个拦截器,从请求头的Authorization字段里取出token,解析校验签名,然后把用户信息放到ThreadLocal的一个上下文对象里,这样在Service层就直接可以拿到当前登录用户的信息。
我封装了一个简单的JWT工具类,核心逻辑如下:
public String generateToken(String userId, String role) { long expire = System.currentTimeMillis() + jwtProperties.getExpireTime(); return Jwts.builder() .setSubject(userId) .claim("role", role) .setExpiration(new Date(expire)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }使用HS256对称加密算法,密钥配置在application.yml里。有个必须注意的地方:生产环境的JWT密钥一定不要写在代码里硬编码,我见过很多项目直接把密钥写死在工具类中,结果密钥泄露之后整个系统的登录体系就形同虚设。正确的做法是通过环境变量或者配置中心来动态注入,部署的时候再设置具体的值。
还有一点是关于token过期的处理。车主的操作频次不高,过期时间可以设置24小时;管理员的token建议2小时就过期,因为后台涉及敏感操作,过期后要求重新登录是安全底线。为了减少管理员的频繁登录,也可以做成双token方案:一个accessToken短期有效,一个refreshToken长期有效,accessToken过期时用refreshToken静默续期。我这个项目里为了控制复杂度没有做,但如果是商业化项目,这个优化值得考虑。
3.2 车位状态实时更新的并发控制
停车场的车位状态并发控制,是这个系统里最考验细节的地方。想想这个场景:同一时刻,两个入口的摄像头各识别到一个车牌,系统同时处理两笔进场请求,都要查询A区03号车位是否空闲,结果两个请求都查到“空闲”,于是都分配了这个车位,同时写入两条进场记录。这就产生了数据竞争。
解决这个问题有几种方案,我最终采用的是Redis分布式锁加乐观锁的组合。
进场处理的流程是这样的:
public EntryResult handleEntry(String plateNumber) { // 生成唯一业务流水号,用于幂等判断 String requestId = UUID.randomUUID().toString(); String lockKey = "parking:entry:" + plateNumber; boolean locked = redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS); if (!locked) { throw new ServiceException("系统正在处理该车辆,请勿重复操作"); } try { // 先判断是否有进行中的停车记录,防止重复进场 ParkingRecord record = recordMapper.selectActiveRecord(plateNumber); if (record != null) { throw new ServiceException("该车辆已在场内,无需重复进场"); } // 分配车位 // 这里使用乐观锁更新车位状态 int updated = parkingSpaceMapper.casOccupy(); if (updated == 0) { throw new ServiceException("暂无可分配车位"); } // 创建停车记录 // ... } finally { redisLock.unlock(lockKey); } }锁的粒度很重要。我这个方案里锁的是车牌号,不是车位。这意味着同一辆车不会并发处理两次进场请求,不同车辆进场互不干扰。如果锁的是车位,那么两个入口同时来车,都得等同一个锁释放,性能会明显下降。
分布式锁的Redis实现要用setIfAbsent加过期时间保证原子性,不能用先get后set的操作。释放锁的时候要校验持有者,防止锁过期后其他线程获取了锁,前一个线程误删了别人的锁。
车位表的casOccupy方法,对应SQL是这样的:
UPDATE parking_space SET status = 1, version = version + 1 WHERE id = #{spaceId} AND status = 0 AND version = #{oldVersion}这里用版本号做乐观锁。如果更新影响的行数为0,说明车位状态已经变了,就要重新找空闲车位。MyBatis-Plus内置了乐观锁插件,只需要给实体加@Version注解,然后在配置类里注册OptimisticLockerInnerInterceptor即可,不复杂,但一定要加上。
3.3 计费规则的灵活配置
计费规则是停车场管理系统里最容易出bug的模块之一。各个停车场的计费差异很大:有的按小时计费,首小时10元,之后每小时5元;有的按分钟累计,每30分钟3元,不足30分钟按30分钟算;有的晚上8点到次日8点有夜间封顶价。如果把这些规则硬编码在代码里,那每次客户改价格都要发版,想想就痛苦。
我的方案是做一个计费规则表,把规则参数化存储。核心字段包括:
- 规则名称(如“标准日夜计费”)
- 计费类型(按小时/按30分钟)
- 首时段单价和时长(比如首小时10元)
- 续时时长和单价
- 单次计费上限(封顶价格)
- 夜间时段设置(开始时间、结束时间、夜间单价)
- 免费时长(比如前15分钟免费)
计费引擎放在Service层,核心思路是分段计算。举个例子,某停车场规则是:前15分钟免费,首小时10元,之后每30分钟3元,单日封顶50元。车辆停了5小时20分钟,计算过程就是:
总时长 = 320分钟 免费时长扣除 = 320 - 15 = 305分钟(计费时长) 首小时 = 10元 剩余计费 = 305 - 60 = 245分钟 按30分钟为单位向上取整 = 245 / 30 = 8.17,向上取整为9个计费单位 续时费用 = 9 * 3 = 27元 总计 = 10 + 27 = 37元,未超过单日封顶50元,应收37元这里有一个关键点:向上取整还是向下取整,会直接导致客诉。我最终实现时是把不足一个计费单元统一按一个计费单元处理,这个逻辑写清楚注释,并且在前端页面展示明细,让车主清楚看到每个时间段的金额计算方式。很多计费纠纷都是因为车主觉得金额不明白,明细展示出来能减少大量投诉。
对于月卡车,计费逻辑简单很多,只要校验车牌在月卡表里有有效记录,就直接放行,不生成临时车订单。这里要注意的是月卡车如果超时出场(比如包月车辆在非通行时段出场),要不要额外收费,这个需要跟物业确认,我在系统里做成可配置项。
免费时长的处理也有细节。如果车辆在免费时长内进出,订单状态直接标记为“已完成-免费”,金额为0,但记录还是要保留,这能反映车场真实的进出流量。
4. 前端Vue实现与联调实战
4.1 Vue环境搭建与路由设计
前端用的是Vue3 + Vite + Element Plus这套组合。Vite构建工具在开发阶段的冷启动速度比Webpack快很多,配置也简洁,对新手友好。Vue3的Composition API配合setup语法糖,写业务逻辑的时候比Options API清晰,特别是涉及多个状态联动的时候,代码不会像原来那样散落在各种methods里。
路由设计上,我使用了静态路由加动态路由混合的方案。静态路由只保留登录页和404页,其余的业务路由都是登录成功后根据用户的角色权限动态生成的。这部分用到了Vue Router的addRoute方法。
动态路由的核心逻辑是这样的:
// 登录成功后,根据角色获取对应的菜单和路由配置 const menuList = await getUserMenus(); // 将后端返回的路由配置转换为组件对象 const dynamicRoutes = transformRoutes(menuList); // 动态添加路由 dynamicRoutes.forEach(route => { router.addRoute(route); });动态路由有一个很常见的坑:页面刷新后,动态添加的路由会消失,因为路由是在内存中维护的。所以必须在全局前置守卫里加一个初始化逻辑,判断store中是否有用户信息和路由表,如果没有,则重新从后端拉取菜单和路由信息,然后调用addRoute,同时用next({...to, replace: true})重新导航一次,确保当前要访问的页面被成功匹配。
菜单的权限控制在管理系统里是不能省的。我用Pinia做状态管理,把用户信息、菜单列表、按钮权限都放在store里。页面上的操作按钮,比如“删除订单”“修改价格”这些敏感操作,通过自定义指令v-permission来判断当前用户是否有对应的权限标识,没有权限的直接从DOM上移除。这种方式比起单纯在代码里写v-if判断要干净得多,权限标识可以统一维护在一张清单里。
4.2 车位状态可视化与实时看板
管理后台最核心的页面是车位总览。我一开始想用WebSocket做实时推送,后来考虑了一下,停车场的并发量远没有那么大,出入口道闸的识别和抬杆动作是低频事件,用轮询就够了,没必要为了“实时”引入WebSocket的复杂度。
轮询方案里我选的是3秒钟请求一次车位状态接口。为了让前端体验流畅,每次请求返回的是全量车位状态数据,数量在几十到几百个之间,JSON序列化之后不超过几十KB,性能上没有压力。
车位地图我用CSS Grid实现,每个车位是一个小方块,状态通过颜色区分:绿色为空闲,红色为占用,黄色为预留,灰色为故障。每个方块绑定了一个点击事件,点击后弹出该车位的详情,包括当前停放的车辆、入场时间、预计费用。这个页面我用了一个小组件来封装:
<template> <div class="parking-grid" :style="gridStyle"> <div v-for="space in spaceList" :key="space.id" class="space-item" :class="'status-' + space.status" @click="handleSpaceClick(space)"> <span>{{ space.spaceNo }}</span> </div> </div> </template>这里有个细节,CSS Grid的gridTemplateColumns是根据车场区域的列数动态计算出来的,因为不同区域的车位数和排布方式不一样,不能写死。通过computed属性根据areaColumns动态生成样式字符串。
统计报表这块我用的ECharts。ECharts是个纯前端的图表库,不需要服务端渲染,数据源从后端的报表接口拿。主要展示三个维度:24小时车流量趋势图(折线图)、车位利用率热力图(堆叠柱状图)、近7日收入对比(柱状图)。ECharts的配置项多,但常用的就是data、xAxis、yAxis、series这几个,踩坑主要在自适应容器宽度的问题,需要在窗口resize时调用chart.resize()方法。
报表接口在后端做了聚合查询,用MySQL的DATE_FORMAT函数对时间字段分组统计:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, SUM(total_amount) AS revenue FROM parking_record WHERE create_time >= #{startTime} AND create_time <= #{endTime} GROUP BY day ORDER BY day这里如果数据量大,查询会很慢,所以记得在create_time字段上建索引,这是个必须注意的点。
4.3 前后端联调的常见坑
前后端分离的项目,联调阶段是最折磨人的。我总结几个这个项目里遇到的典型问题。
第一个问题,跨域配置。开发环境下前端跑在Vite的5173端口,后端接口在8080端口,浏览器会阻止跨域请求。解决方案是在后端加一个CORS配置类,允许指定来源的跨域请求:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }生产环境下通常会用Nginx做反向代理,把前端的/api请求转发到后端的8080端口,这样浏览器看到的页面和接口是同源的,跨域问题就消失了。所以这个CORS配置主要是给开发环境用的。
第二个问题,Axios请求拦截器和响应拦截器的配合。我在请求拦截器里统一从Pinia的store里拿token,加到请求头的Authorization字段。响应拦截器里统一处理错误码,比如token过期时后端返回401,前端拦截到这个状态码后,清除本地存储,跳转到登录页,并弹出提示信息。这样每个接口的调用方不需要单独写错误处理逻辑,代码会干净很多。
第三个问题,日期时间的序列化格式不一致。后端返回的时间默认是ISO格式的2024-03-15T14:30:00,中间有个T字母,前端展示的时候不太好看。我在后端的application.yml里加了一个统一的日期格式配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这样前后端约定俗成,所有接口返回的时间都是yyyy-MM-dd HH:mm:ss的格式,省去前端到处做转换的麻烦。
5. 部署上线与常见问题排查
5.1 服务器部署方案
部署方案我用的是Docker Compose,把整个应用栈编排成一个全家桶。停车场这种规模的系统,一台2核4G的云服务器就完全够用。我的docker-compose.yml里包含了以下服务:
- MySQL 8.0容器,数据目录挂载到宿主机持久化
- Redis 6.2容器,主要用于分布式锁和缓存
- MinIO容器,用于存储停车抓拍照片
- 后端SpringBoot应用容器,打成Jar包后制作成镜像
- Nginx容器,挂载前端Vue构建产物和反向代理配置
这里有一个重要的部署细节:SpringBoot应用容器要等MySQL和Redis就绪后再启动。我用的方式是docker-compose的depends_on配合健康检查,不然第一次启动经常会报数据库连接失败,重启几次才能成功。
后端应用的Dockerfile大概长这样:
FROM openjdk:17-jre-slim WORKDIR /app COPY target/parking-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]生产环境的配置我通过环境变量注入,比如数据库密码、Redis密码、MinIO的AccessKey,这些都不能写在镜像里。用Docker Compose的environment字段配合宿主机宿主机.env文件管理,既不提交到代码仓库,也方便在服务器上直接修改。
前端构建的时候有个坑要提前说。Vue项目打包默认的资源路径是根目录/,如果你用Nginx的某个子路径来访问前端页面,一定要在vite.config.js里设置base: './',不然部署后静态资源会全部404。我当时在这个问题上折腾了快一下午,最后发现就这么一行配置的事。
5.2 高频踩坑清单
最后是我在开发这个项目过程中整理出来的高频问题,每一条都是实际踩过的坑,按出现频率排个序:
| 常见问题 | 具体表现 | 解决方案 |
|---|---|---|
| 数据库连接池连接耗尽 | 高峰期接口报Connection is not available | 合理配置HikariCP的maximum-pool-size,我设为20,同时检查是否有慢查询拖住了连接 |
| 车牌号大小写不一致 | 同一辆车进场是“京A12345”,出场识别成“京a12345”,查不到进场记录 | 车牌号统一转大写再存储,查询时也先转大写 |
| 付费后状态未更新 | 车主缴费成功但道闸不抬杆 | 检查支付回调接口的幂等性,支付回调因为网络重试可能被调多次,用Redis记录处理状态 |
| 明明有空位但分配失败 | 并发场景下乐观锁更新返回0 | 增加重试机制,最多重试3次,每次都重新定位空闲车位 |
| 报表数据与线下记录对不上 | 金额汇总差几分钱 | 确认是否把逻辑删除的数据也统计进去了,统一加deleted=0条件 |
这里我想重点展开说第一个问题。HikariCP是SpringBoot默认的数据库连接池,默认的maximum-pool-size只有10。我遇到的场景是停车场早晚高峰时,几十个请求同时进来,查询车位、创建记录、更新状态这些操作都要占用数据库连接,如果再有个别慢查询占着连接不放,很容易就达到连接池上限,后续请求全部排队,接口响应时间飙升到几十秒,表现就是系统看起来像卡死了。
解决思路有两步。第一步是把连接池上限调大并配置合理的超时时间,但这不是根本办法。第二步是排查是哪些SQL拖慢了查询速度,通过MySQL的慢查询日志定位,给常用查询字段加索引,比如parking_record表的plate_number、create_time,parking_space表的status。索引建好之后,高峰期接口响应时间从原来的几百毫秒降到了几十毫秒,连接池压力自然就缓解了。
关于车牌号统一大写的问题,这个坑特别隐蔽。摄像头的车牌识别引擎偶尔会输出小写字母,后端如果直接用识别结果去查库,匹配不上就会判定为“未找到进场记录”,车辆堵在出口,体验极差。我在后端加了一个工具方法,所有车牌入场和出场处理时都先做处理,转成大写再去查询和存储。同时数据库字段的排序规则也改成大小写不敏感的utf8mb4_general_ci,这样哪怕有历史脏数据也能匹配上。
还有一个值得说的经验是日志规范。我把关键业务操作比如车辆进场、出场、支付成功这些写入了专门的业务日志表,配合MDC(Mapped Diagnostic Context)把requestId和车牌号串起来。排查问题的时候,用一条查询就能看到某个车牌从进场到支付的全过程日志,比翻原始日志文件效率高很多。
最后再分享一个个人心得
这套停车管理系统从需求梳理到上线跑通,前后用了一个多月。我最大的体会是:做一个管理系统,技术难点其实不在某个单独的知识点上,而在于把散落的细节串联起来的能力。数据库字段设计差一个状态,可能导致前端要多写几十行判断逻辑;并发控制少一把锁,高峰时段就可能出现一辆车占用两个车位的诡异数据。这些坑每一个单看都不大,但合在一起就是个麻烦。
如果你也想自己动手做类似的系统,我的建议是先画清楚业务流程图,把进出的正常流程和异常分支全部过一遍再动手。代码写错了能改,流程如果错了,返工的代价要大得多。整套代码的版本管理用Git,每天一个小提交,随时能回退到能跑的状态,这种习惯在项目推进中帮了我大忙。停车场这种项目后续还能扩展的方向也有不少,比如对接微信小程序端的预约车位、与第三方支付平台的对账系统、甚至基于停车数据的车场运营分析报告,都是在这个骨架上能持续迭代的功能。先把基础打牢,后面的事情就顺了。