停车场系统源码二次开发:计费引擎与支付回调核心避坑指南
简介这是一份停车场系统完整源码配套小程序端与后台管理适合需要快速搭建智慧停车平台的开发者或项目团队。系统兼容市面上主流相机设备相机识别后自动上传云端并校验硬件序列号支持微信、支付宝、银行接口支付还可按停车场指定商户收款同时提供附近停车场查询、车位预约、断电断网时岗亭app接管录入等功能。技术栈为Java SpringBoot2 Dubbo OAuth2数据库采用MySQL/MongoDB/Redis底层即时通讯基于Netty4后台管理使用vue-typescript-admin-template。资源共1239个文件包含Java源码与class编译产物、小程序wxml/wxss/js前端、properties/yml/xml配置、SQL建库脚本、PNG界面图等压缩包8.46MB。已有1422人学习。整套代码目录结构清晰附带部署说明可直接导入数据库与开发工具二次开发适用于毕业设计、课设及商业项目改造。1. 停车场系统源码解决的不只是“先绑定车牌再抬杆”拿到一套停车场系统源码有人当天跑通有人一周还在对账。差距不在界面而在两处计费规则写在哪、支付回调怎么对账。这套含前后端与部署说明的停车场系统源码核心是要把车辆入场、计费、支付、出场全链路串成一条可靠的数据流而不只是“先绑定车牌再抬杆”的演示工程。它适合三类人想给某个停车场做日常运营管理的负责人需要从零搭后台和小程序的开发者以及买来学习“硬件支付小程序”完整业务闭环的新手。你不用从摄像头协议开始造轮子但必须搞清车牌识别结果、订单状态、计费规则这三件事在系统里是怎么流转的。本文按“拆解系统→跑通后端→对接小程序→部署上线→验证用例”的顺序讲所有示例都围绕一套最常见的 Java 后端 原生小程序方案展开命令和参数可以直接照抄。2. 前后端必懂的系统拆解计费引擎、支付回调和车牌识别是三个核心先说技术栈为什么这么选。这套源码常见做法是后端用 Spring Boot MySQL Redis小程序端用平台原生语法开发管理后台用 Vue。选型理由很实际Spring Boot 生态成熟、网上能查到的坑位最多MySQL 负责订单和车位这些强一致数据Redis 扛入场出场的高频读和防重复放行。小程序原生的好处是不用引入额外框架拿到源码改页面结构、换接口地址就能跑。很多新手拿到源码先看登录注册这顺序错了。停车场系统真正的核心是三个计费引擎怎么算钱不留漏洞、支付回调怎么对账不重复、车牌识别结果怎么可靠地变成一条停车记录。三个点只要有一个是坏的线上运营一定出账目问题而且要到月底对账时才发现。2.1 计费引擎按分钟分段、跨天、封顶规则全写在库表里计费引擎不能把价格写死在 Java 代码里。常见做法是把规则放进一张 fee_rule 表运营改价格不用动代码。核心字段包括免费时长、区间起点与终点、区间费用、每日封顶以及这条规则是给临时车、月卡车还是免费车用的。CREATE TABLE fee_rule ( id bigint NOT NULL AUTO_INCREMENT, lot_id bigint NOT NULL COMMENT 所属停车场ID, car_type tinyint NOT NULL COMMENT 车辆类型1临时车 2月卡车 3免费车, free_minutes int NOT NULL DEFAULT 0 COMMENT 入场后免费时长单位分钟, start_minute int NOT NULL DEFAULT 0 COMMENT 计费区间起点单位分钟, end_minute int NOT NULL DEFAULT 0 COMMENT 计费区间终点单位分钟, charge_amount decimal(10,2) NOT NULL COMMENT 该区间费用单位元, daily_cap decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 单日封顶费用单位元, is_cyclic tinyint NOT NULL DEFAULT 0 COMMENT 是否循环计费1是 0否, PRIMARY KEY (id) );这张表的设计思路是“按分钟查区间”。比如“首小时 10 元后每 30 分钟 3 元24 小时封顶 30 元”可以拆成三条规则0 到 60 分钟收 10 元60 到 90 分钟收 3 元90 到 120 分钟收 3 元后续每 30 分钟继续循环。计算时把停车总时长减去免费时长再按区间累加最后和 daily_cap 取较小值。跨天是计费最容易做错的地方。不要按自然日拆分两段分别计算因为“24 小时封顶”和“跨天后重新计第一小时”完全是两套规则。正确做法是先把入场到出场的总分钟数算出来对总时长套用循环区间累加再和封顶比较。这样 22:00 入场次日 08:00 出场得到的是一个连续时长而不是两个自然日账单。2.2 支付与回调小程序拉起支付后端靠异步通知记账支付流程的时序是固定的小程序调后端预支付接口拿到支付参数拉起支付面板用户确认后支付服务商先返回给小程序一个前端结果同时用异步通知把最终结果打到后端回调地址。前端结果只能用来刷新界面提示能不能入账必须以异步通知为准。订单状态机建议只保留四种状态待支付、已支付、已退款、已关闭。入场时生成的订单进入待支付异步通知成功后进入已支付出场校验已支付才放行超过 15 分钟未支付的订单定时关闭。订单状态含义触发动作PAY_WAIT待支付出场生成订单时PAY_SUCCESS已支付收到异步通知且校验通过REFUNDED已退款人工退款或异常退款处理CLOSED已关闭定时任务超时未支付回调接口必须做幂等。支付服务商的异步通知可能重复推送多次甚至先通知后面的订单再通知前面的订单。如果回调处理时不判断当前订单状态同一笔支付会被记两次流水月底账目必乱。回调的第一步是验签验签失败直接返回失败让服务商继续重试第二步查订单第三步判断订单如果已经是已支付状态直接返回成功不再更新任何数据。2.3 车牌识别与硬件对接摄像头结果不落库一切白搭车牌识别来源通常有两种接法。第一种是摄像头或识别一体机通过 HTTP 主动上报识别结果后端提供接口接收第二种是后端定时去设备 SDK 拉取识别结果。绝大多数成品停车场系统用的是第一种因为摄像头厂商都会提供 HTTP 上报能力部署简单。上报接口最怕的是重复上报导致一辆车被记录两次入场。解决办法是接口里按“设备编号 抓拍时间 车牌号”做去重缓存里放一个 30 秒的标记。同时要记录识别置信度低于 90% 的结果不要直接放行而是进待确认列表由岗亭人工确认。车牌识别结果如果没有写进停车记录表并附上抓拍图片地址后续所有计费、结算、纠纷追溯都没有依据。2.4 角色权限从岗亭到管理后台至少要四类账号停车场系统不是只有车主一个角色。源码里至少要能看到四类角色的权限划分超级管理员、停车场运营管理、岗亭值班、车主小程序端。四类角色在前端页面、后端接口、数据范围上都要分开否则岗亭人员能改订单金额或者普通车主能调到管理后台接口都是事故隐患。角色端关键权限超级管理员管理后台停车场配置、费率修改、财务对账运营管理员管理后台订单查询、月卡办理、退款审核岗亭值班岗亭端手动入场、手动出场、异常放行车主小程序绑定车牌、查询订单、支付、开票明确了这套权限边界再看源码时就不容易迷路。很多二次开发的坑本质上不是代码难写而是刚拿到工程时不知道哪些模块是主链路、哪些是附属功能。计费、支付、车牌记录是主链路界面和权限是外壳调试时优先保证主链路数据正确页面好看不好看反而是后话。3. 跑通后端的最小动作从初始化 SQL 到接口自测后端跑通的标志不是管理后台能打开而是能用 curl 模拟一辆车入场、出场、生成订单并算出正确费用。整个过程只需要三件事建好核心表、启动服务、调用接口。先看核心表再看业务代码最后用命令行自测。3.1 初始化核心表结构停车记录、订单、车位状态停车记录表是系统的账本字段要能回答“这辆车什么时候进的、什么时候出的、停了几分钟、收多少钱”。订单表负责支付状态。车位表负责剩余车位。三张表的关系是停车记录是主表订单与停车记录一对一车位与停车记录是状态关系。CREATE TABLE park_record ( id bigint NOT NULL AUTO_INCREMENT, plate_no varchar(20) NOT NULL COMMENT 车牌号, car_type tinyint NOT NULL DEFAULT 1 COMMENT 1临时 2月卡 3免费, lot_id bigint NOT NULL COMMENT 停车场ID, enter_time datetime NOT NULL COMMENT 入场时间, exit_time datetime DEFAULT NULL COMMENT 出场时间, duration_minutes int DEFAULT 0 COMMENT 停车时长分钟, fee_amount decimal(10,2) DEFAULT 0.00 COMMENT 应收金额, status tinyint NOT NULL DEFAULT 1 COMMENT 1在场 2已出场 3异常, PRIMARY KEY (id), KEY idx_plate_lot (plate_no, lot_id), KEY idx_status (status) ); CREATE TABLE parking_order ( id bigint NOT NULL AUTO_INCREMENT, record_id bigint NOT NULL COMMENT 停车记录ID, order_no varchar(64) NOT NULL COMMENT 业务订单号, plate_no varchar(20) NOT NULL COMMENT 车牌号, total_fee decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已退款 3已关闭, pay_time datetime DEFAULT NULL COMMENT 支付时间, callback_count int NOT NULL DEFAULT 0 COMMENT 收到回调次数用于排查重复通知, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) );这两张表是整套停车场源码的地基。注意订单表里加了 callback_count这在生产环境非常有用。支付服务商重复回调时你可以从数字上看出这个订单被通知了多少次而不是靠猜。业务上要保证一张停车记录只能生成一条有效订单订单号必须唯一所以订单表对 order_no 建了唯一索引。3.2 入场、出场与计费的完整链路代码入场接口要做的事按顺序拆开查车牌是否是有效月卡、查车位是否还有余量、写停车记录、更新车位占用状态。一个月卡车入场不生成订单只写记录。PostMapping(/api/park/enter) public Long enter(RequestBody EnterDTO dto) { // 1. 查月卡有效月卡直接标记为月卡入场 MonthlyCard card monthlyCardMapper.findValidByPlate( dto.getPlateNo(), dto.getLotId(), new Date()); int carType card ! null ? 2 : 1; // 2. 查车位余量优先读缓存缓存没有再查库 int available parkingSpaceCache.available(dto.getLotId()); if (available 0) { throw new BizException(车位已满); } // 3. 创建停车记录状态为在场 ParkRecord record new ParkRecord(); record.setPlateNo(dto.getPlateNo()); record.setCarType(carType); record.setLotId(dto.getLotId()); record.setEnterTime(new Date()); record.setStatus(1); parkRecordMapper.insert(record); // 4. 扣减车位并写缓存 parkingSpaceCache.occupy(dto.getLotId()); return record.getId(); }入场参数里最重要的是 plateNo 和 lotId。plateNo 要做大小写归一化不然“粤A12345”和“粤a12345”会被当成两辆车。lotId 用于区分多停车场部署单停车场时固定传 1。月卡判断要同时校验证件有效期只判断“存在”不判断“未过期”会导致过期月卡还能免费出场。出场接口比入场复杂因为要同时完成计费和生成订单。计费引擎的核心是算准分钟数再按规则表累加。public long calcFee(ParkRecord record) { // 月卡车辆不走临时计费 if (record.getCarType() 2 || record.getCarType() 3) { return 0L; } long minutes Duration.between( record.getEnterTime().toInstant(), record.getExitTime().toInstant()).toMinutes(); if (minutes 0) { minutes 1; // 至少按1分钟计避免免费钻空子 } // 读取该停车场的临时车规则 ListFeeRule rules feeRuleMapper.findByLotAndType( record.getLotId(), 1); long billable Math.max(0, minutes - freeMinutes(rules)); long fee 0; long cycleLength totalCycleLength(rules); while (billable 0) { for (FeeRule rule : rules) { if (billable 0) break; long inRange Math.min(billable, rule.getEndMinute() - rule.getStartMinute()); fee inRange / 60.0 * rule.getChargePerHour(); billable - inRange; } // 循环区间如果规则没有封顶按最大区间长度兜底 } // 封顶费用不能超过 daily_cap * 停车的自然天数 long cap dailyCap(rules) * daysBetween(record); return Math.min(fee, cap); }这段代码有几个参数值得说明。billable 是扣掉免费时长后的可计费分钟数cycleLength 是循环区间总长度如果规则是“每 30 分钟 3 元”循环区间就是 30 分钟dailyCap 是 24 小时封顶值。封顶按自然天数计算是兼容“跨天”场景的常用妥协方案虽然严格意义上有边界误差但绝大多数停车场运营接受这个口径。计费函数返回的单位是“分”传给前端时再除以 100避免浮点数精度问题。出场接口拿到费用后生成订单订单金额只信任后端算出的 fee不信任前端传过来的金额。前端传金额只能用于展示入库必须以后端重算为准。3.3 用 curl 自测接口不依赖页面的最快验证方式服务启动后先不要急着打开小程序。用 curl 直接打接口能最快确认主链路通不通。下面两个命令模拟“一辆临时车入场 65 分钟后出场”。# 模拟车辆入场返回停车记录ID curl -X POST http://127.0.0.1:8080/api/park/enter \ -H Content-Type: application/json \ -d {plateNo:粤A12345,lotId:1} # 模拟车辆出场生成待支付订单 curl -X POST http://127.0.0.1:8080/api/park/exit \ -H Content-Type: application/json \ -d {recordId:1}第一条命令的响应里会包含 recordId第二条命令需要把它填进去。如果入场接口返回“车位已满”先检查车位表里有没有初始化数据。很多源码自带 SQL 里只建了表没插停车场和车位基础数据这是部署后第一个拦路虎。如果出场接口返回金额为 0检查 fee_rule 表有没有插入规则以及规则里的 free_minutes 是不是把 65 分钟全免掉了。自测时建议直接开一个独立测试库不要把生产数据混进来。测试库初始化后用脚本连续跑 10 次出入场再人工核对订单金额比打开页面点来点去快得多也更容易发现计费函数里的边界漏洞。4. 小程序端把页面接上接口登录、绑定车牌、支付三个环节后端接口通顺后再看小程序端。小程序源码里需要改的地方集中在三个文件请求封装、个人中心页、支付回调页。拿到源码先别急着改 UI先把接口地址和登录逻辑打通页面自然就有数据了。4.1 请求封装与 token 处理统一入口避免到处改地址小程序端的请求不能每个页面单独写一遍。标准做法是封装一个 request 函数所有页面调用它。这个函数负责拼接基础地址、带上 token、统一处理 401 和网络错误。function request(path, method, data {}) { const token storage.get(token) || ; return new Promise((resolve, reject) { platformRequest({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: Bearer token }, success: (res) { if (res.data.code 401) { storage.remove(token); // 跳转登录页 } else if (res.data.code 0) { resolve(res.data.data); } else { reject(new Error(res.data.message)); } }, fail: (err) reject(err) }); }); }这个封装做了三件必要的事token 自动附加、401 自动踢回登录、统一按业务码解析数据。BASE_URL 是后端对外地址上线前要改成 HTTPS 域名。注意 success 里判断的是 res.data.code不是 HTTP 状态码这是前后端约定的业务码体系如果源码里的返回结构是 code/data/message这套写法直接通用如果不是要按源码实际结构调整。4.2 登录与车牌绑定先拿到身份再绑定车牌小程序登录的标准动作是调用平台登录接口拿到临时凭证把这个凭证发给后端后端换出自己的 token 返回给小程序。后续所有请求都带这个 token。车牌绑定必须在登录之后做绑定关系是“用户 openId 对应车牌号”这样才能保证用户只能查看和支付自己名下的订单。async function login() { const code await platformLogin(); const res await request(/api/auth/login, POST, { code }); storage.set(token, res.token); } async function bindPlate(plateNo) { const normalized plateNo.trim().toUpperCase(); const res await request(/api/car/bind, POST, { plateNo: normalized }); return res; }登录接口返回的 token 建议同时设置过期时间存到本地存储时带上 expiresIn。车牌绑定前一定要做 trim 和 toUpperCase很多莫名其妙的“查不到车牌”都是前后端对大小写处理不一致导致的。后端接收车牌时也要做同样的归一化两端都做才能形成闭环。4.3 支付拉起与状态轮询前端别等回调要主动查支付完成后的界面更新不能依赖支付面板返回的结果因为那个结果在弱网环境下可能丢失。可靠做法是拉起支付后面临“支付结果未知”状态小程序端定时去后端查订单状态查到已支付再刷新页面。async function payOrder(orderId) { const params await request(/api/order/pay, POST, { orderId }); await platformPay(params); // 支付完成后轮询订单状态最多10次每次间隔2秒 for (let i 0; i 10; i) { const order await request(/api/order/ orderId, GET); if (order.status 1) { return { paid: true, order }; } await sleep(2000); } return { paid: false, order: null }; }轮询为什么是 10 次而不是无限次支付回调通常几秒内到达10 次覆盖 20 秒已经绰绰有余。如果 20 秒还没变状态再轮询意义不大应该提示用户“支付结果确认中”让后端定时任务兜底。真正入账的依据永远是后端异步通知前端轮询到已支付只是为了刷新界面这一点要在代码注释里写明避免后来维护的人看不见以为前端轮询就是入账逻辑。5. 部署与避坑一套源码跑不起来九成卡在这几处部署说明写得再详细到了真机环境照样有一堆玄学问题。我见过的情况里九成不是源码本身的问题而是部署环境的基础配置不对。下面这五条是停车场系统部署最常见的翻车点每条都是“现象→原因→解决”的结构能帮你省下至少两天的排查时间。5.1 跨天计费日期错乱22:00 入场次日 08:00 出场费用按两个自然日算了现象测试跨天停车时发现计费结果和规则表对不上明明写了 24 小时封顶 30 元实际却收了 50 多。原因服务器时间、MySQL 时间和业务代码用了三个时区口径。代码里取 new Date() 存库MySQL 连接串里却指定了 UTC展示时前端又按本地时区解析导致日期边界整体偏移跨天场景被割裂成两天。解决统一时区口径。MySQL 连接串加 serverTimezoneAsia/Shanghai服务器 timezone 改成 Asia/Shanghai业务代码里所有日期计算用带时区的对象不要用本地时间隐式转换。跨天计费函数里的“自然日”按业务时区计算而不是数据库服务器时区。部署后立刻测一个 22:00 入场、次日 08:00 出场的用例再测一个连续停放 25 小时的用例这两个通过基本说明时区没毛病。5.2 小程序真机请求全失败开发工具能通真机预览一片红现象开发工具里接口全部正常一扫码真机预览所有请求都报“网络错误”但手机本身网络没问题。原因小程序平台对线上请求有严格的域名白名单校验。开发工具可以勾选“不校验合法域名”真机没有这个开关所有请求域名必须在平台侧完成配置而且必须 HTTPS。另一个隐性问题是证书链不完整有些证书服务商给的是中间证书Nginx 配置只贴了主证书安卓真机校验严格就会失败。解决在小程序管理后台把后端域名加进 request 合法域名列表域名必须备案。Nginx 配置 HTTPS 时把 fullchain 证书链完整配置进去不要只贴 crt 文件。开发阶段可以在开发工具勾选“不校验域名”先联调但上线前一定要去掉勾选用真机重新验证一遍所有接口。5.3 支付回调重复通知月底对账金额对不上现象某天对账发现一笔订单在支付流水里出现了两笔收入但订单表里只有一条数据金额差正好等于某一笔重复订单的金额。原因支付服务商为了保证通知送达会以递增间隔重试回调直到商户返回成功。如果回调处理代码没有幂等判断每次都执行一次更新就会造成支付流水重复记账。还有一类情况是代码里用了“先查后更”的方式两个线程同时查到待支付状态同时对数据库做更新也会重复入账。解决回调处理必须做两件事。第一更新订单时用状态条件更新例如 UPDATE parking_order SET status 1 WHERE order_no ? AND status 0影响行数为 0 就说明已经被处理过直接返回成功。第二支付流水表对“支付服务商流水号”建唯一索引重复插入直接报错并捕获记录。幂等不是靠业务代码的判断是靠数据库约束兜底。5.4 车位缓存与数据库不一致重启后车位全空车辆重复入场现象服务重启后已停满的停车场显示还有大量空位保安手动录入了多辆重复入场车全部显示在场。原因车位余量只存在 Redis 里重启后缓存丢失系统以为所有车位都空着。反过来还有一种情况定时任务把缓存中的占用数写回数据库但写入时机和入场动作错位缓存和库互相覆盖。停车场项目的车位状态必须支持重启恢复不能把 Redis 当作唯一数据源。解决车位余量的权威数据放在数据库车位表里Redis 只作为读缓存。服务启动时执行一次缓存重建从数据库加载当前占用数。入场扣减时先更新数据库再删缓存或更新缓存不要先更新缓存再异步写库。如果源码里没有重建逻辑部署说明里一定要补一条启动命令先清空车位相关缓存再启动服务。这个顺序错了线上必出重复入场。5.5 车牌识别摄像头只在内网生效部署到公网后设备上报全失败现象在办公室局域网联调识别一体机正常上报车牌系统能自动抬杆。部署到云服务器后同一台设备识别正常但后端收不到任何上报数据。原因摄像头配置的上报地址是内网 IP公网不可达。部分摄像头虽然支持 HTTP 上报但固件对目标地址有限制只能访问局域网另外云服务器公网 IP 与停车场内网之间没有打通摄像头自然找不到后端。解决不要在公网后端直接等摄像头上报正确拓扑是停车场本地放一个边缘网关摄像头上报到网关网关再通过公网 API 转发到云后端。如果临时调试也可以用内网穿透工具把后端地址映射到摄像头所在网络但那只适合联调不适合生产。方案选型时优先选支持“后端主动拉取”的摄像头 SDK主动拉取可以完全绕开内网上报的连通性问题代价是实时性略低设置 3 秒拉一次足够应对车辆进出。6. 验证这套源码值不值得投入使用三个必测用例一套源码让你快速跑通不等于能直接上生产。我每次拿到新的停车场源码都会用三个用例做最终验收每个用例都对应一次真实踩坑记录你可以直接照着设计测试数据。第一个用例是跨天停车计费。设计一辆 18:00 入场、次日 09:00 出场的临时车全程 15 小时。预期结果免费时长 15 分钟扣掉后按区间规则累加并受单日封顶约束。这个用例能同时暴露时区、封顶、循环区间三个问题。如果结果比人工计算结果高或低先查规则表数据再查跨天封顶代码。第二个用例是并发入场和重复支付。用脚本并发向入场接口发送 20 个请求其中 5 个是同一车牌。预期结果同车牌只创建一条在场记录不重复入场剩余车位扣减量等于实际入场的不同车牌数而不是请求数。支付侧则是重复向回调接口发送同一笔成功通知两次预期结果订单表和流水表都只有一条记录。这个用例检验的是防重入和幂等直接影响运营对账。第三个用例是月卡与免费时长边界。一张当月有效的月卡车入场应直接标记为月卡车辆且不生成计费订单。一辆临停车入场 14 分钟出场免费时长 15 分钟应收 0 元入场 16 分钟出场应收至少一个计费最小单位。很多源码在这里栽跟头把免费时长边界判断写成 minutes freeTime 而不是 minutes freeTime导致 15 分钟整免和 15 分 01 秒起步的处理不一致。下面是一份我会在部署前逐项确认的自检清单也是你评估“这套源码是否值得投入”的参考底线。清单里任何一项不通过都不要急着正式运营。检查项通过标准跨天计费22:00 入场次日 08:00 出场金额符合规则表人工计算幂等回调同一笔回调重复推送 10 次订单状态不变流水不重复车位一致性模拟 20 辆车入场后重启服务剩余车位数不变化证书与域名真机小程序能正常请求全部接口无网络错误月卡边界过期月卡不再免费有效月卡不生成订单免费时长边界恰好在免费时长内出场收 0 元超时 1 分钟正常计费这套验证跑完你基本能判断这份停车场系统源码的成熟度。计费、支付、车位、小程序四个主链路全部通过才有放进生产的基础。我个人的习惯是把跨天计费用例放在部署后的第一件事因为大多数停车场源码的演示数据都不会覆盖跨天场景而它恰恰是上线第一个月最容易出客诉的地方。希望这些方法和踩坑记录帮到你至少让你少走一遍我走过的弯路。本文还有配套的精品资源点击获取