ThinkPHP 6 + Vue 3实战:从零搭建智能停车场管理系统 📅 发布时间:2026/9/8 0:03:27 👁 浏览次数: 做停车场系统听起来不算什么大项目但真正动手之后你会发现这里面的门槛全在细节上。车位状态的实时更新、计费规则的边界条件、支付回调的幂等处理、高峰期的并发扣费每一处都能让一个“看起来能用”的Demo直接翻车。我这次用ThinkPHP 6 Vue 3从零搭了一套智能停车场停车缴费管理系统前端负责交互展示后端只输出JSON接口整体走的是前后端分离的架构。整套系统已经在一家小型商业停车场跑了几个月支持车牌识别录入、手动入场、自动计费、扫码支付、出场校验、订单管理和计费规则配置。这篇文章会把我在设计数据库、写计费算法、对接支付和部署上线过程中踩过的坑和沉淀下来的方案完整拆给你适合正在做毕业设计、接私活或者公司内部需要一套轻量停车管理系统的朋友参考。1. 项目整体设计与技术选型思路1.1 为什么选ThinkPHP 6 Vue 3这对组合先聊技术选型。市面上停车管理系统的方案很多有纯Java Spring Boot的有Go写的也有PHP做后端加小程序端的。我这次选ThinkPHP 6 Vue 3核心原因是这个组合最适合中小型停车场的实际场景。ThinkPHP 6是目前ThinkPHP的LTS版本相比ThinkPHP 5在底层架构上做了不少调整核心改成了依赖注入容器路由、中间件、事件系统全都是独立组件用起来比5.x顺手得多。关键是它上手门槛低一个懂PHP的开发者一周内就能进入业务开发状态。对于停车场这种业务逻辑不算极其复杂、但CRUD操作量很大的系统ThinkPHP的模型层和查询构造器写起来效率很高。Vue 3这边组合式APIComposition API让状态管理和逻辑复用比Vue 2的选项式API清爽太多了。停车场的监控大屏、收费端和管理后台本质上是大量数据展示加高频交互的页面Vue 3的响应式系统和组件化开发正好贴合这个需求。加上Element Plus提供现成的表格、表单、弹窗组件后台管理页面的开发速度能比传统jQuery方案快两倍以上。这套组合还有一个隐藏优势部署成本极低。ThinkPHP跑在PHP-FPM上一个2核4G的云服务器就能扛住一家中型停车场的日均流量Vue打包后的静态文件交给Nginx托管前后端都放在同一台机器上运维压力很小。1.2 系统整体架构前后端分离怎么分层以前用ThinkPHP做项目基本都是服务端渲染页面控制器里既要查数据库又要拼HTML前端逻辑和后端逻辑糊在一起。这次我直接走前后端分离ThinkPHP只负责输出JSONVue负责渲染页面通过Axios发HTTP请求拿数据。整个系统的架构分成了三层展示层Vue 3 Vite Element Plus构建的SPA单页应用包含监控大屏、收费端、管理后台三个子模块。服务层ThinkPHP 6提供RESTful API接口负责鉴权、业务逻辑处理、计费计算、支付对接和数据持久化。数据层MySQL 8.0存储业务数据Redis缓存车位状态和热点数据使用定时任务处理超时订单和异常记录。前端和后端通过JSON格式的数据交互接口风格按照RESTful规范来比如POST /api/entry表示入场POST /api/calcFee表示计算费用POST /api/notify表示支付回调。这么分的第一个好处是开发可以并行前端不用等后端写完接口可以先Mock数据开发页面第二个好处是将来如果要扩展小程序端或者IOS/Android App后端接口可以直接复用不需要再写一套服务端渲染页面。1.3 环境版本与服务规划开发环境我列一下照着装就能跑起来后端PHP 8.0 ThinkPHP 6.0.12LTS MySQL 8.0 Redis 6.0前端Node.js 16.20 Vue 3.4 Vite 5.0 Element Plus 2.4服务器Nginx 1.24 PHP-FPMCentOS 7.9代码仓库Gitee私有仓库方便团队协作PHP版本我特意选了8.0以上因为ThinkPHP 6对PHP 8的兼容性已经非常成熟而且PHP 8的性能相比7.x有明显的提升。命名参数、构造器属性提升、match表达式这些新语法写起来也舒服。前端包管理器用的npm没有上pnpm。原因很简单项目体量不大pnpm的优势体现不出来npm在团队协作时大家更熟减少沟通成本。2. 数据库设计与计费规则拆解2.1 核心数据表结构说明停车场系统虽然看着功能不复杂但数据库设计如果不提前规划好后面改起来能让人崩溃。我这边总共设计了六张核心表停车场表、车辆表、停车记录表、订单表、计费规则表、操作日志表。停车场表保存停车场基本信息包括名称、总车位数、地址、收费开关状态。车辆表区分临时车和月租车月租车有过期时间字段。停车记录表是整个系统最核心的表每一次车辆入场出场都对应一条记录里面存车牌号、入场时间、出场时间、停车时长、应收金额、实收金额、状态等字段。订单表关联停车记录保存支付流水信息包括订单号、支付渠道支付宝/微信、支付单号、支付状态、回调时间等。计费规则表比较灵活支持配置免费时长、首小时价格、续费单价、单日封顶金额、不同车型的不同费率。操作日志表记录关键操作比如强制抬杆、手动出场、费率修改这类操作方便出问题后追溯。主要表结构如下CREATE TABLE tp_parking_record ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, plate_no varchar(20) NOT NULL COMMENT 车牌号, car_type tinyint(1) NOT NULL DEFAULT 1 COMMENT 1-临时车 2-月租车, entry_time datetime DEFAULT NULL COMMENT 入场时间, exit_time datetime DEFAULT NULL COMMENT 出场时间, duration_seconds int(11) DEFAULT 0 COMMENT 停车时长(秒), fee decimal(10,2) DEFAULT 0.00 COMMENT 应收金额, paid_fee decimal(10,2) DEFAULT 0.00 COMMENT 实收金额, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1-在场 2-已出场 3-异常, lot_id int(11) NOT NULL DEFAULT 1 COMMENT 停车场ID, created_at datetime DEFAULT NULL, updated_at datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_plate_status (plate_no, status), KEY idx_entry_time (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;停车记录表这里有两个容易踩的坑。第一个是车牌号一定要加联合索引(plate_no, status)因为查询某个车当前是否在场是最频繁的操作不加索引数据量大了之后会慢到难以接受。第二个是金额字段必须用decimal(10,2)不能图省事用float浮点数计算精度问题在涉及钱的场景里是绝对不能妥协的。2.2 计费规则设计的完整思路计费规则是整个系统的灵魂。停车场收费看着简单就一句话“按停车时长收费”但真到实现层面各种边界条件能把人绕晕。我这边把计费规则拆成了四个维度车型临时车/月租车、时段日间/夜间、阶梯首小时/续费小时/封顶价、特殊规则免费时长、跨天处理。以我实际跑的这套商业停车场为例计费规则是这样的临时车首小时5元超过1小时后每30分钟加收2元不足30分钟按30分钟计算单日24小时封顶30元。免费时长入场后15分钟内出场不收费。这个逻辑要特别注意如果停车时长小于等于900秒费用直接置为0。跨天处理停车时长跨过凌晨零点重新计算封顶金额。比如第一天停了8小时收费17元第二天又停了5小时收费9元总计26元而不是简单按总时长13小时算首小时加续费。用PHP实现这个计费算法的时候我先把计算逻辑封装成了一个独立的计费服务类不跟控制器粘在一起。这样测试方便将来要调费率也不用来回改控制器。?php namespace app\service; class ParkingFeeService { protected $rule; public function __construct(array $rule) { $this-rule $rule; } public function calcFee(int $durationSeconds, string $entryTime, string $exitTime): float { // 免费时长判断 if ($durationSeconds $this-rule[free_minutes] * 60) { return 0.00; } $entry strtotime($entryTime); $exit strtotime($exitTime); // 跨天处理按自然日拆分 $days $this-splitByDay($entry, $exit); $totalFee 0.00; foreach ($days as $day) { $dayDuration $day[end] - $day[start]; $dayFee $this-calcDayFee($dayDuration); $totalFee $dayFee; } // 封顶判断单日封顶在calcDayFee里已处理 return round($totalFee, 2); } protected function splitByDay(int $start, int $end): array { $days []; $current $start; while ($current $end) { $dayEnd strtotime(date(Y-m-d 23:59:59, $current)); if ($dayEnd $end) { $days[] [start $current, end $end]; } else { $days[] [start $current, end $dayEnd]; } $current $dayEnd 1; } return $days; } protected function calcDayFee(int $dayDurationSeconds): float { // 首小时费用 $fee $this-rule[first_hour_price]; if ($dayDurationSeconds 3600) { $extraSeconds $dayDurationSeconds - 3600; $unitSeconds $this-rule[unit_minutes] * 60; // 按30分钟一个计费单位 $units (int) ceil($extraSeconds / $unitSeconds); $fee $units * $this-rule[unit_price]; } // 单日封顶 if ($fee $this-rule[max_price_per_day]) { $fee $this-rule[max_price_per_day]; } return $fee; } }这段代码里有两个细节值得注意。第一是ceil取整函数在这里用得恰到好处不足30分钟按30分钟计算比如超了1小时零1分钟就按2个计费单位算第二是单日封顶在calcDayFee内部处理这样跨天的时候每天都能独立封顶符合实际停车场的收费习惯。2.3 数据库索引和事务设计经验停车场的数据库操作有几个高频场景查询车辆是否在场、查询停车记录列表、统计车位占用情况。这些查询条件组合起来索引设计就很有讲究。我自己实践后的索引设计是停车记录表建(plate_no, status)联合索引处理“查某辆车在场记录”的请求(entry_time)单列索引处理按时间范围查记录的请求订单表(order_no)加唯一索引保证订单号不重复。事务设计上入场和出场操作都需要保证数据一致性。入场时先查车辆是否已在场内有则在场记录就返回错误没有才插入新记录这里用事务包住防止两个人同时扫同一个车牌导致重复入场。出场时先锁住停车记录行计算费用和更新状态必须在一个事务里不然可能出现扣了钱但记录没更新状态这种惨案。ThinkPHP 6里使用事务的方式很简单Db::transaction(function () use ($recordId, $fee) { // 更新停车记录 // 写入订单 // 更新车位状态 });3. 后端接口实现与关键业务逻辑3.1 入场、出场、缴费三大核心接口入场接口是整个系统数据流动的起点。当车牌识别摄像机识别到一个车牌号或者收费员手动输入车牌点击入场后前端就会调用POST /api/entry。后端要做的事情包括判断该车是否已经在场内如果是月租车检查是否过期创建停车记录更新车位占用数。出场接口稍微复杂一些。前端会先调用POST /api/calcFee查询费用展示给车主车主扫码支付后微信或支付宝会异步回调POST /api/notify通知支付结果后端确认金额无误后更新停车记录状态为已出场然后向道闸发送抬杆指令。这里有个业务顺序问题值得思考是先抬杆再结算还是先结算再抬杆我最终的做法是出场时先锁定停车记录生成支付订单车主完成支付后回调更新状态之后前端轮询到已支付状态再呼叫抬杆。这样能避免那种“杆抬了车走了但钱没付”的情况。入场接口的核心代码如下public function entry(Request $request) { $plateNo strtoupper(trim($request-post(plate_no))); $carType $request-post(car_type, 1); if (empty($plateNo)) { return json([code 400, msg 车牌号不能为空]); } // 检查是否已在场内 $exists ParkingRecord::where(plate_no, $plateNo) -where(status, 1) -find(); if ($exists) { return json([code 400, msg 该车已在停车场内请勿重复入场]); } Db::startTrans(); try { $record ParkingRecord::create([ plate_no $plateNo, car_type $carType, entry_time date(Y-m-d H:i:s), status 1 ]); // 更新车位占用数 ParkingLot::where(id, 1)-dec(available_count)-update(); Db::commit(); return json([code 0, msg 入场成功, data [record_id $record-id]]); } catch (\Exception $e) { Db::rollback(); return json([code 500, msg 入场失败 . $e-getMessage()]); } }3.2 支付对接流程与回调幂等处理支付我同时接了微信支付和支付宝这两种方式流程上大同小异后端生成预支付订单返回支付二维码链接或支付串给前端前端展示二维码用户扫码完成支付支付平台异步通知后端接口后端更新订单状态。对接支付时最怕的是回调重复通知。微信和支付宝为了保证支付结果一定送达会多次重复发送回调通知如果后端不在处理时做幂等校验就会出现订单状态被覆盖、停车记录重复更新这类问题。幂等处理的思路很直接在回调处理方法里先查订单表判断当前订单是否已经是已支付状态如果是直接返回成功不再重复处理业务逻辑。同时回调处理要放在事务里保证订单状态和停车记录状态更新的一致性。public function notify(Request $request) { $orderNo $request-post(out_trade_no); $order Order::where(order_no, $orderNo)-find(); if (!$order) { return 订单不存在; } // 幂等判断已支付直接返回成功 if ($order-status 2) { return success; } Db::startTrans(); try { $order-status 2; $order-paid_time date(Y-m-d H:i:s); $order-trade_no $request-post(trade_no); $order-save(); // 更新停车记录状态 $record ParkingRecord::find($order-record_id); $record-status 2; $record-exit_time date(Y-m-d H:i:s); $record-paid_fee $order-amount; $record-save(); Db::commit(); return success; } catch (\Exception $e) { Db::rollback(); return fail; } }一个容易忽略的细节是回调接口返回的内容必须严格符合支付平台的要求。微信支付回调要求返回xmlreturn_code![CDATA[SUCCESS]]/return_code/xml这种格式支付宝返回success字符串如果格式不对支付平台就会一直重试造成大量无效请求。3.3 ThinkPHP中间件实现接口鉴权和跨域处理前后端分离的项目里接口鉴权和跨域是两个必须处理的问题。我这里用ThinkPHP 6的中间件机制统一处理。跨域问题在开发环境特别烦人。前端跑在Vite的5173端口后端跑在Nginx的8080端口端口不同就会触发浏览器的同源策略限制。解决办法是在ThinkPHP里自定义一个跨域中间件设置Access-Control-Allow-Origin响应头。?php namespace app\middleware; class CrossDomain { public function handle($request, \Closure $next) { header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With); if ($request-isOptions()) { return response(, 204); } return $next($request); } }接口鉴权我实现了两层。第一层是登录态校验后台管理员登录成功后后端返回一个JWT Token前端存在localStorage里后续请求通过Authorization请求头携带中间件里校验Token有效性和过期时间。第二层是操作权限校验比如只有管理员角色的用户才能修改计费规则、查看财务报表这类敏感接口。ThinkPHP 6的路由分组加上中间件配置非常简单Route::group(api, function () { Route::post(entry, Parking/entry); Route::post(exit, Parking/exit); Route::post(calcFee, Parking/calcFee); })-middleware([\app\middleware\CrossDomain::class, \app\middleware\AuthCheck::class]);3.4 Redis缓存优化车位状态查询停车场系统有一个很常见的性能瓶颈车位剩余数量的实时统计。如果每次车主扫码打开小程序查车位后端都去MySQL里数一遍有哪几条停车记录状态是在场再来个总数减去在场数高峰期能把数据库打崩。我的方案是在Redis里维护一个实时的车位占用数字。入场时在Redis里减一出场时在Redis里加一前端查车位状态直接读Redis不查数据库。这样不仅性能好还能减轻数据库压力。用ThinkPHP操作Redis很简单框架已经内置了缓存处理类。我把Redis缓存设计成一个服务类统一封装车位数量的增减和读取避免在控制器里散落一堆缓存操作代码。public static function changeAvailableCount($lotId, $delta) { $key parking:lot:available: . $lotId; $current Cache::get($key); if ($current false) { // Redis中没有缓存时从数据库初始化 $lot ParkingLot::find($lotId); $current $lot-available_count; Cache::set($key, $current, 3600); } $newCount $current $delta; Cache::set($key, $newCount, 3600); return $newCount; }使用Redis之后有一个需要注意的地方缓存和数据库的一致性问题。我采用的做法是每天凌晨统一次从数据库重建缓存运行期间Redis为主数据源操作日志里记录所有变更万一出现异常可以追溯修复。4. Vue 3前端实现与页面拆解4.1 项目初始化和环境配置前端这部分我用的Vite作为构建工具Vite冷启动速度和热更新速度比Webpack快一个数量级开发体验非常爽。创建项目的命令很简单npm create vitelatest parking-web -- --template vue项目创建之后需要安装Vue Router做页面路由、Pinia做状态管理、Axios发HTTP请求、Element Plus做UI组件库。安装依赖的时候建议用npm install不要用npm install -g全局装每个项目保持独立的依赖环境避免版本冲突。Vite的配置文件vite.config.js里做了两件事一是配置了开发服务器端口和代理二是设置路径别名让符号可以快速访问src目录。import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这里配置代理特别重要。开发环境下前端页面请求/api/entry这个路径时没有代理的话浏览器会直接请求http://localhost:5173/api/entry结果肯定是404。配置代理后Vite会把这个请求转发到http://localhost:8080/api/entry完美绕过跨域问题。4.2 监控大屏、收费端、管理后台三个子模块我把前端拆成了三个相对独立的子模块通过路由和侧边栏菜单切换。监控大屏是给停车场管理层看的页面上半部分是车位总览用环形进度条展示总车位数、已占用数、剩余数下半部分是实时进出记录表格最新一条入场/出场的记录置顶高亮。这里的轮询请求用setInterval定时器每10秒拉取一次最新数据实际使用下来压力不大。收费端是给岗亭收费员用的这是整个系统交互最核心的页面。收费员输入车牌号后前端调/api/calcFee接口拿停车信息页面上展示入场时间、停车时长、应收金额然后点击收款按钮生成支付二维码。这里的交互要求快、准、稳一个操作最多三步完成不能让车主在旁边等太久。管理后台功能最杂包括停车记录查询、订单管理、计费规则配置、月租车管理、财务报表等。管理后台我大量用到Element Plus的el-table、el-form、el-dialog组件配合Vue 3的reactive和ref做状态管理。4.3 用Vue 3组合式API封装接口请求我前端封装了一个统一的Axios实例请求拦截器里自动加上JWT Token响应拦截器处理业务错误码和HTTP异常这样各个页面里写接口调用时不用重复处理这些逻辑。// src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 0) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request在组合式API里调用接口我习惯把每个业务模块的接口封装成一个独立的Hook函数比如useParking管理入场出场操作useOrder管理订单查询和支付操作这样组件里只需要调用这些函数逻辑非常清晰。4.4 前端支付交互与二维码展示扫码支付是前端交互最复杂的环节。用户点击“确认缴费”后前端调用后端接口拿到支付二维码的链接然后用qrcode这个npm包把链接转成二维码图片展示在弹窗中。同时启动一个定时器每2秒轮询订单状态一旦发现订单已支付立刻关闭弹窗并提示成功。这个轮询操作要特别注意清理定时器不然用户关闭弹窗后定时器还在跑浪费请求资源。正确做法是在关闭弹窗时clearInterval或者使用Vue 3的onBeforeUnmount钩子清理。script setup import { ref, onBeforeUnmount } from vue import QRCode from qrcode const dialogVisible ref(false) const qrCodeUrl ref() let pollTimer null async function openPayDialog(orderNo, qrCodeContent) { qrCodeUrl.value await QRCode.toDataURL(qrCodeContent) dialogVisible.value true startPoll(orderNo) } function startPoll(orderNo) { pollTimer setInterval(async () { const res await checkOrderStatus(orderNo) if (res.data.status 2) { clearInterval(pollTimer) dialogVisible.value false ElMessage.success(支付成功) } }, 2000) } onBeforeUnmount(() { if (pollTimer) clearInterval(pollTimer) }) /script这里有个体验细节二维码内容如果是支付宝或微信的支付链接直接用qrcode转图片就行如果是微信Native支付后端返回的是一个weixin://wxpay/...开头的特殊链接这种链接扫码后会自动唤起微信支付。一定不能把这种链接原样展示成文本让用户复制体验太差了。5. 项目部署实战与常见问题排查5.1 Nginx配置和前端打包上线项目开发完成后的部署流程前端和后端是分开的。前端执行npm run build会在dist目录下生成静态文件把这些文件上传到服务器的/var/www/parking-web目录即可。Nginx的配置我直接给出实际在用的版本。需要注意两个关键点一是PHP请求要转发给PHP-FPM处理二是前端是SPA单页应用所有路由都要try_files重写到index.html。server { listen 80; server_name parking.example.com; root /var/www/parking-web; index index.html; # 前端静态文件 location / { try_files $uri $uri/ /index.html; } # 后端API接口 location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }后端代码上传到服务器后需要安装PHP依赖。ThinkPHP 6使用Composer管理依赖在项目根目录执行composer install --no-dev即可。然后要修改.env环境配置文件把数据库连接信息、Redis配置、支付密钥等敏感信息都放在这里不要把密钥写死在代码里。5.2 上线后遇到的5个真实问题与解决方法系统上线至今我遇到了一系列问题挑了五个最有代表性的写在这里。第一个问题是金额误差。有用户停车2小时05分钟计算出的费用跟人工计算不一致。排查后发现是PHP的浮点数运算精度问题。0.1 0.2在PHP里得到0.30000000000000004直接用浮点数做累加就会出现传说中的精度误差。解决办法是把所有金额运算统一用整型分来算最后展示时才转换成元。第二个问题高一峰期接口超时。停车场出口在晚上六点到八点的高峰期出口扫码缴费的请求量突然增大数据库连接数被打满。优化方案是加Redis缓存热点数据计费规则、车位数量同时用数据库连接池让连接复用而不是每次请求都重新建立。第三个问题是支付回调重复处理导致停车记录被重复更新。前面提过微信支付回调在网络抖动时会重复推送通知如果后端不校验订单状态就会出现费用已缴但系统再次显示未缴费的情况。我的解决方案在订单表加了一个status字段的查询校验已支付订单直接返回成功。第四个问题是Vue页面在手机端出现白屏。排查发现是兼容性问题Element Plus 2.x默认不兼容部分老旧浏览器解决方法是根据实际使用场景在main.js里引入浏览器前缀补全。如果车载终端或广告屏用的安卓7以下系统还需要额外处理WebView的兼容问题。第五个问题是ThinkPHP的SQL查询日志怎么开启。线上排查慢查询和怪问题时没有SQL日志简直寸步难行。TP6里在config/log.php中配置不同的日志级别或者用框架的Db::listen方法监听SQL执行// 开启SQL日志 Db::listen(function($sql, $time, $explain) { Log::write([SQL] . $sql . [ . $time . ms], sql); });这段代码一般放在app/common.php或者自定义的服务提供者里这样每次执行SQL都会记录到日志文件线上排查问题时能直接看到每一条SQL的耗时和执行结果。5.3 监控告警与日常巡检建议系统稳定运行不能只靠开发时写代码运维侧的监控告警也必不可少。我目前实现了三个维度的线上监控。一是接口健康检查。写了一个定时脚本每5分钟请求一次入场和查费接口如果接口返回非预期数据或响应时间超过3秒就通过企业微信机器人推告警给开发人员。二是数据库慢查询监控。开启MySQL的slow_query_log超过1秒的SQL自动记录每周汇总分析一次针对频率最高的慢查询做索引优化或缓存改造。三是磁盘和内存监控。利用系统的定时任务检查挂载磁盘空间和PHP-FPM进程内存占用低于阈值时自动清理日志文件。日常巡检方面我每周会做一次订单状态一致性检查写SQL对比订单表和停车记录表找出已支付但记录未更新这类异常数据。每月会核对一次财务报表和实际收款记录确保支付渠道的账单跟系统数据对得上。6. 支付对接的坑与安全合规注意事项6.1 微信支付、支付宝申请接入的必备条件支付接入对整个项目来说既关键又繁琐。我调试支付功能的时候花了两天时间大部分时间都消耗在申请商户号和配置各种密钥证书上。微信支付需要申请微信商户号个人开发者可以选择个体工商户或企业资质个人主体无法申请。申请时需要提供营业执照、法人身份证、银行账户等资料审核通过后会在商户平台拿到商户号mch_id和AppSecret。支付宝方面需要到开放平台注册开发者账号创建应用后获得AppID和应用私钥然后签约对应的产品支付宝电脑网站支付、手机网站支付或当面付。有一个经验想说开发调试时可以使用支付平台提供的沙箱环境。微信支付沙箱环境是独立于正式环境的接口域名是https://api.mch.weixin.qq.com/sandboxnew需要用正式商户号去申请沙箱密钥。支付宝沙箱环境更简单开放平台可以直接用沙箱应用测试不用真实商户号。6.2 金额精度与支付安全的关键设计支付系统涉及资金安全代码规范上必须严格遵守几条原则。第一金额一律存分为单位。数据库里用int类型存储金额对应的分数比如5元存50030元存3000。虽然前端展示时需要转成带两位小数的元但这个转换放在展示层做后端逻辑全用整数运算彻底规避浮点数误差问题。第二支付回调验签不能省。微信支付回调会带上签名信息后端收到通知后需要按支付平台的规则重新计算签名并对比防止伪造回调。TP6里配合官方SDK处理即可不要图省事跳过这一步。第三订单号必须全局唯一。我用日期 随机数的方式生成订单号比如20240315103000123456再加数据库唯一索引兜底。曾经遇到过一次并发场景下同时生成了两个一样的订单号唯一索引直接拦住避免了脏数据。6.3 月租车和特殊场景的计费扩展临时车的计费逻辑相对简单但实际的停车场业务里还有月租车、固定车位用户、VIP客户等角色。我做系统时在这些方面做了一定的抽取设计方便后续扩展。月租车统一通过车辆表里的car_type字段区分月租车入场时不计算费用出场直接抬杆放行。月租车是否过期在入场时校验如果已过期会提示收费员转临时车流程收费。这里还有一个细节月租车转临时车后如果当天再次入场就按临时车重新计费不能沿用月租规则。免费时长、夜间优惠、会员打折这类特殊规则我都在计费规则表里预留了配置位。虽然当前商业停车场只用到了首小时加续费的简单模型但表结构上已经能支持更复杂的规则组合后续有需求加配置就行不用改代码。7. 项目复盘与实用经验总结7.1 开发时间线和团队协作经验这次项目从需求确认到部署上线前后一共花了25天。第一天到第三天是做需求沟通和数据库设计第四天到第十五天是后端接口开发第八天开始前端并行开发第十六天到第二十天联调支付和场内测试最后五天处理部署上线和小范围试运行期间的问题反馈。团队协作上最值得说的经验是接口文档先行。我们在后端写接口之前先用Apifox把所有的接口URL、请求参数、响应格式定义好。前后端并行开发时前端看着接口文档Mock数据写页面后端照着接口文档实现功能联调阶段几乎没有出现“你接口字段名字不对”这种问题。一个小建议是接口返回格式统一约定成这样的结构{ code: 0, msg: success, data: {} }code为0表示成功非0表示业务错误msg是提示信息data放业务数据。这个约定能省掉很多前后端沟通成本。7.2 从这套系统复盘得到的技术收获写完这套系统我对“技术选型如何服务业务场景”有了更深的理解。ThinkPHP这类PHP框架虽然被很多人吐槽不够“高大上”但它的开发效率和部署便捷性在中小型项目中是实打实的优势。Vue 3的工程化能力配合Element Plus组件库让后台类项目的前端开发速度提升了不是一星半点。停车场管理系统虽然业务逻辑不算极其复杂但“计费精度、并发扣费、支付回调幂等”这些通用问题覆盖了大部分管理系统的核心痛点。做完这套项目后再去做其他管理系统类的项目很多代码和设计思想是可以平移复用的。我个人在实际操作中的体会是这种偏业务型的系统真正难的不是写代码而是把一个看似简单的问题想周全。你永远不知道现场会发生什么情况——比如司机在出口处扫了码但是手机没网比如两个车同时入场被系统识别成同一个车牌比如凌晨断电重启后计费规则失效。只有在设计阶段就想清楚这些边界情况系统才有可能扛住真实的业务考验。最后再分享一个小技巧给停车场系统写代码时一定要把“强制抬杆”这个功能做出来虽然平时用不上但碰到设备异常的时候它就是救命的工具。