尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

档案馆预约系统实战:uniapp+PHP+Node.js完整落地方案

档案馆预约系统实战:uniapp+PHP+Node.js完整落地方案 做过政务类预约系统的朋友应该都有体会这类项目看着不大但“坑”一点不比大型电商少。今天分享的是档案馆参观预约系统的完整落地方案技术栈是微信小程序客户端 uniapp跨端框架后端主服务用PHP辅助服务用Node.js管理后台用Vue。整个项目从数据库建模到部署上线前后折腾了大约三周我把设计思路、关键代码、踩过的坑都整理出来了。这套系统解决的核心问题是档案馆作为人流密集的公共服务场馆既要满足公众参观需求又要控制瞬时在馆人数、实现实名可追溯。纯现场排队的方式在节假日基本失控人工登记身份证件速度慢、容易出错也无法提前掌握参观人数。预约系统把“现场排队”变成“线上分时预约”用户在小程序里选日期、选时段、填实名信息后台自动校验冲突、控制容量、生成核销二维码管理员在Vue后台审核、核销、看数据报表。无论你是刚接触uniapp的初学者还是已经在做预约类项目的开发者这套设计和代码都有直接可抄的部分。1. 项目整体拆解与选型逻辑1.1 为什么是 PHP Node.js 混合后端不少人一看“PHP Node.js”就觉得奇怪一个项目干嘛用两种后端语言这不是炫技背后有很现实的原因。档案馆这类系统的部署环境通常比较特殊很多单位有内网服务器运维习惯偏向传统LAMP/LNMPPHP几乎是“默认选项”部署简单、出问题好排查。但预约系统有一个绕不开的场景高峰期的并发写入。比如9点放第2天号源同一秒可能有几百人同时预约这时候数据库写入的冲突处理、Redis队列的消费、延迟任务未核销自动取消都需要异步能力。PHP处理同步请求很快但做常驻内存的消费者进程比较别扭。于是我把异步、定时、推送相关的部分交给Node.jsPHP专注业务API两边通过Redis队列通信——这也是目前中小型团队非常实用的组合。具体分工是这样的PHP 8.2 ThinkPHP 8用户注册登录、预约下单、取消预约、管理员审核、数据统计这些主业务API。Node.js 20 BullMQRedis队列处理预约成功后的短信通知、核销码邮件发送、预约日当天8点批量推送提醒、过期未核销自动释放名额。Vue 3 Element Plus给管理员用的PC后台预约审核、核销查询、黑名单管理、数据看板。uniappVue3语法 uview-plus微信小程序端用户端的全部界面。选uniapp而不是原生微信小程序核心原因是开发和维护成本。后期如果档案馆想上支付宝小程序或者抖音小程序原生写法基本要重写uniapp换平台编译就行。另外uniapp的uni.request、uni.login已经封装了跨端差异配合uview-plus组件库表单页、列表页开发速度比原生快一倍以上。1.2 核心需求解析做预约系统前需求必须拆到“字段级”。档案馆预约和景区预约最大的区别在于实名制要求更严格进馆需要人证比对所以必须收集真实姓名、身份证号、手机号未成年人还要填监护人信息。这意味着业务上必然出现两个独立实体参观者档案visitor和预约记录appointment。从用户端看核心流程四步走登录微信授权获取openid绑定手机号手机号需实名验证。选时段看未来7天日历选一个有余量的日期再选上午/下午时段。填信息新增/选择参观人多人可以一次预约最多5人。拿凭证预约成功后生成二维码到馆出示核销。从管理端看核心诉求三条审核确认部分团体预约需要人工审核、现场核销扫二维码验证身份、数据统计每日入馆量、爽约率、热门时段。我画了一张很粗的状态流转图供你在脑子里建立模型本文章不放图片直接文字描述预约记录状态PENDING待审核→APPROVED已通过→VISITED已核销入馆任何一步都可以CANCELED用户取消或NO_SHOW爽约。后台管理员还可以把恶意占号用户拉进BLACKLIST黑名单。这套状态机是所有功能开发的地基。前面没定清楚后面写接口一定会返工。2. 数据库建模与核心业务设计2.1 表结构设计详解我直接用实际的DDL来说话这套表结构是我在实际项目中调过三轮的版本。数据库用MySQL 8.0字符集统一utf8mb4排序规则utf8mb4_general_ci。参观者表visitorCREATE TABLE visitor ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, name VARCHAR(32) NOT NULL, id_card VARCHAR(255) NOT NULL COMMENT 身份证号AES加密存储, id_card_hash CHAR(64) NOT NULL COMMENT 身份证号SHA256用于查重, phone VARCHAR(20) NOT NULL, visitor_type TINYINT NOT NULL DEFAULT 0 COMMENT 0个人 1团体, org_name VARCHAR(64) DEFAULT NULL COMMENT 团体单位名称, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid), UNIQUE KEY uk_id_card_hash (id_card_hash), KEY idx_phone (phone) ) ENGINEInnoDB;重点说明身份证号属于敏感个人信息数据库里绝对不能存明文。存两列——加密列用于生产环境读取hash列用于唯一性检查。加密用AES-256-CBC密钥放在服务端环境变量里代码库里不出现。这个方法政务类的项目尤其重要等保测评的时候是加分项。预约记录表appointment_recordCREATE TABLE appointment_record ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, appointment_no VARCHAR(32) NOT NULL COMMENT 预约编号如YG202408150001, visitor_id INT UNSIGNED NOT NULL, schedule_id INT UNSIGNED NOT NULL COMMENT 场次ID, app_date DATE NOT NULL COMMENT 参观日期, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, visitor_count TINYINT NOT NULL DEFAULT 1 COMMENT 人数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已核销 3已取消 4爽约, qr_code VARCHAR(255) DEFAULT NULL COMMENT 核销二维码内容, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_appointment_no (appointment_no), KEY idx_app_date_status (app_date, status), KEY idx_visitor (visitor_id) ) ENGINEInnoDB;场次/容量表appointment_scheduleCREATE TABLE appointment_schedule ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, app_date DATE NOT NULL COMMENT 日期, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, capacity INT NOT NULL DEFAULT 100 COMMENT 总容量, booked_count INT NOT NULL DEFAULT 0 COMMENT 已预约数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可约 0停约, UNIQUE KEY uk_date_slot (app_date, time_slot) ) ENGINEInnoDB;这个表是并发控制的关键后面预约接口就是靠它的行锁来避免超卖。管理员表admin_user、黑名单表blacklist、操作日志表operation_log我就一笔带过了核心字段就是账号、密码hash、角色权限以及被拉黑用户ID、原因、失效时间管理员操作留痕。2.2 时段容量与预约限制的关键点容量设计方面档案馆不是每小时滚动放号的一般是上午场9:00-12:00和下午场14:00-17:00两个时段。但有些馆有讲解服务会细分到具体讲解批次。我在表里用time_slot字段区分上下午如果要支持批次可以把time_slot改成批次编号或者加一个batch_id逻辑是类似的这里以最简单的上下午为例说明。防黄牛、防刷号我做了三层限制单用户预约上限同一openid即同一个微信用户在同一个app_date只能有一条状态为“待审核/已通过”的预约记录也就是说你不能再重复约同一天。7天内取消次数限制取消次数超过3次暂停预约权限7天。否则有人反复占号取消会把真实观众挤掉。爽约惩罚预约成功但未核销标记为爽约每月累计2次爽约下个月禁止预约。这是最常见的痛点不惩罚的话实际到馆率可能只有60%。这些限制的实现位置第一层在PHP业务代码查询判断第二、三层在预约下单前做条件检查。都是为了保护公共资源的公平性这个设计在政务类预约需求里基本是标配。2.3 预约下单的并发与事务处理这里直接放PHP核心代码。预约接口最怕的就是“两个人同时抢最后一个名额”导致预约数量超过容量。解决办法是数据库事务加行锁public function reserve(Visitor $user, $date, $slot, $visitorCount) { $pdo $this-getPdo(); $pdo-beginTransaction(); try { // 1. 锁住场次记录防止并发超卖 $stmt $pdo-prepare( SELECT id, capacity, booked_count FROM appointment_schedule WHERE app_date ? AND time_slot ? FOR UPDATE ); $stmt-execute([$date, $slot]); $schedule $stmt-fetch(PDO::FETCH_ASSOC); if (!$schedule || $schedule[status] 0) { throw new \Exception(该时段不可预约); } if ($schedule[booked_count] $visitorCount $schedule[capacity]) { throw new \Exception(该时段余票不足); } // 2. 检查当天是否已有预约防重复 $check $pdo-prepare( SELECT id FROM appointment_record WHERE visitor_id ? AND app_date ? AND status IN (0, 1) LIMIT 1 ); $check-execute([$user[id], $date]); if ($check-fetch()) { throw new \Exception(当天已有预约记录); } // 3. 生成预约编号、二维码内容并插入 $appointmentNo YG . date(Ymd) . str_pad((string)mt_rand(1, 9999), 4, 0, STR_PAD_LEFT); $qrContent json_encode([ no $appointmentNo, date $date, slot $slot, count $visitorCount ]); $insert $pdo-prepare( INSERT INTO appointment_record (appointment_no, visitor_id, schedule_id, app_date, time_slot, visitor_count, qr_code, status) VALUES (?, ?, ?, ?, ?, ?, ?, 1) ); $insert-execute([$appointmentNo, $user[id], $schedule[id], $date, $slot, $visitorCount, $qrContent]); // 4. 更新已预约数 $update $pdo-prepare( UPDATE appointment_schedule SET booked_count booked_count ? WHERE id ? ); $update-execute([$visitorCount, $schedule[id]]); $pdo-commit(); return [appointment_no $appointmentNo, qr_content $qrContent]; } catch (\Exception $e) { $pdo-rollBack(); throw $e; } }注意几个细节FOR UPDATE是MySQL的悲观行锁锁住的是appointment_schedule中对应的那行数据并发请求只能串行执行这是保证不超卖最简单可靠的方式。预约编号别用数据库自增ID裸奔给用户看一是可被遍历二是位数不固定。我用的格式是YG前缀 日期 随机4位够用。如果要更严格可以加redis自增序列。二维码内容包含预约编号、日期、时段、人数核销的时候通过预约编号查库比对而不是只认二维码本身防止有人PS二维码。这套事务逻辑在压力测试下100并发抢同一个场次20个名额没出现超额已经跑过实测。预约类项目的核心就在这一段务必吃透。3. 前后端实现与联调细节3.1 小程序端uniapp中的登录与会话管理uniapp写微信小程序登录流程是固定的uni.login拿code → 后端调微信接口换openid → 签发自定义登录态token → 小程序存储token。我封装了一个统一请求库request.js核心思路是每次请求自动带上token遇到401就重新静默登录避免用户看到“登录过期”弹窗。// request.js 核心封装 const BASE_URL https://api.example.com; let isRefreshing false; function request(options) { return new Promise((resolve, reject) { const token uni.getStorageSync(token); uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: token ? Bearer token : , Content-Type: application/json }, success: (res) { if (res.statusCode 401) { // token失效静默重新登录 if (!isRefreshing) { isRefreshing true; login().then(() { isRefreshing false; uni.request({ ... }); // 重发原请求 }); } } else if (res.statusCode 200) { if (res.data.code 0) { resolve(res.data.data); } else { uni.showToast({ title: res.data.message, icon: none }); reject(res.data); } } }, fail: (err) reject(err) }); }); }这个封装在实际开发中非常实用。你在热搜词里看到的“顶部导航栏高度”问题在小程序里也是必踩的点。因为不同手机状态栏高度不同自定义导航栏时高度要动态计算// 获取状态栏高度适配刘海屏 const systemInfo uni.getSystemInfoSync(); this.statusBarHeight systemInfo.statusBarHeight; this.navBarHeight systemInfo.statusBarHeight 44 px;日历和时段选择是小程序端的核心交互。日历我直接用了uview-plus的u-calendar配置了禁用日期范围——当天之前的日期不可选超过7天的不可选。时段列表用u-grid展示每个格子显示“上午 9:00-12:00 余量xx”余量实时从接口获取。注意余量展示要做缓存5秒内不重复请求避免频繁打后端。3.2 管理后台Vue3预约审核与核销后台用Vue3 Element Plus功能不长但有个点我特别想提醒审核表格不要简简单单做个全表查询分页。预约数据一多不带条件的全表扫描会直接压垮MySQL。我做了一个组合查询接口GET /api/admin/audit/list?page1size20status1app_date2024-08-15keyword张三keyword支持模糊搜索姓名/预约编号/手机号密文要注意手机号如果是明文才能模糊所以在设计上前端对手机号还是明文存储身份证号才加密这样业务和合规都能兼顾。后端SQL用WHERE status ? AND app_date ? AND (name LIKE ? OR appointment_no LIKE ?)这种动态拼接方式配合appointment_record(status, app_date)联合索引单表百万级数据查询都能稳定在几十毫秒内。核销功能是档案馆工作人员最关心的。我的方案是管理员打开核销页使用扫码枪或手机摄像头扫用户二维码系统解析二维码内容拿到appointment_no调用核销接口核销成功会显示“预约人姓名人数入馆时间”同时后台记录操作员信息。这里有个坑扫码枪本质上是个模拟键盘的设备它扫描二维码后会把内容直接“打”到输入框所以要监听输入框的change事件自动触发查询而不是让管理员再点一次“核销”按钮。3.3 PHP侧跨域与CORS的坑前后端分离必然遇到跨域。PHP处理CORS有个典型的坑是OPTIONS预检请求。很多人的代码只处理了GET/POST没处理OPTIONS浏览器直接报跨域错误。在ThinkPHP中间件里加这么一段public function handle($request, \Closure $next) { $origin $request-header(Origin); if ($origin) { header(Access-Control-Allow-Origin: . $origin); header(Access-Control-Allow-Credentials: true); } header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With); if ($request-method() OPTIONS) { return response(, 204); } return $next($request); }注意Access-Control-Allow-Origin不能写成*因为一旦要携带Cookie或Authorization头浏览器就要求必须指定具体来源。生产环境最好维护一个白名单数组动态判断Origin防止任意网站调用你的接口。前端调试时还会遇到一个问题HTTP无法在微信小程序真机上请求必须HTTPS域名备案。开发阶段我用uniapp的H5模式在浏览器调试接口走本机局域网IP等到真机预览再切换正式地址。这个流程一定要提前规划我见太多人开发到一半才发现域名没配、证书没弄整个联调瘫痪。4. 避坑记录与常见问题速查4.1 环境安装阶段的三个高频问题先说一个几乎每个新手都会撞上的问题在Windows下执行npm install或npm run serve报错提示npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这是PowerShell执行策略限制不是npm坏了。解决办法Set-ExecutionPolicy -Scope CurrentUser RemoteSigned执行后选Y确认。或者用管理员身份在Windows PowerShell里执行Set-ExecutionPolicy RemoteSigned临时放开。这个问题的原因很简单Windows的PowerShell出于安全考虑默认禁止运行未签名的ps1脚本而npm、vue-cli这些命令行工具恰恰是通过.ps1脚本启动的。这个问题在Node.js安装教程里很少写但实际开发中遇到概率接近100%。第二个常见问题是PHP版本从7升级到8之后项目突然一堆报错。主要是PHP 8把很多ext函数的行为改得更严格了比如each()函数被移除、字符转义规则变化。我的建议是新项目一律用PHP 8.2老项目迁移前先用php -l全量检查语法再用PHPUnit回归一遍核心业务。第三个问题是MySQL时区。前后端联调时发现时间差8小时十有八九是数据库连接时区没设置。PHP的PDO连接串里加上cursorClass和时区配置$dsn mysql:host127.0.0.1;port3306;dbnamearchive;charsetutf8mb4; /// PDO 连接后立即执行时区设置 $pdo-exec(SET time_zone 08:00);同时PHP侧date_default_timezone_set(Asia/Shanghai)这样数据库时间、PHP时间、用户展示时间全部一致不要在代码里再手动加减8小时那是灾难。4.2 微信小程序专属的坑域名白名单小程序后台必须配置 request合法域名且必须是HTTPS。开发阶段可以在开发者工具里勾选“不校验合法域名”但真机预览时这个选项无效。二维码生成别在前端用canvas手动画二维码坑太多了网络图片临时路径、canvas层级遮挡。直接用后端生成好的图片地址返回给前端渲染配合uni.downloadFile保存到相册。服务端用phpqrcode类库生成一行代码搞定。列表加载更多预约列表用onReachBottom触底加载注意用加载状态锁防止重复请求。我写的分页逻辑是前端维护page和hasMore每次请求后端返回has_next字段前端根据它决定是否显示“加载更多”提示。手机号快速填入真实项目里用户最烦手输手机号最好用uni.login 微信提供的手机号快捷验证组件button open-typegetPhoneNumber用户点一下授权就能拿到手机号。但注意这个能力只对企业认证的小程序开放。4.3 线上运行后的三个必查优化项目上线后别急着不管我列出三个真正影响体验的优化点一是预约余量的实时性。高峰期用户反复刷新余量会导致后端压力我加了一层Redis缓存场次余量写入Redis读接口只走RedisMySQL的booked_count在异步队列里批量更新。具体做法预约下单事务完成后DEL掉对应场次的缓存键读余量时先查Redis没有就回源MySQL并写入缓存设置过期时间30秒。二是核销记录留痕。现场核销必须记录操作管理员、核销时间、设备IP这是出了纠纷比如“我没预约怎么扫不出来”唯一的追溯手段。我在核销接口里强制要求管理员token并把操作日志写入operation_log。三是数据看板的SQL优化。统计“今日入馆量”要实时“上月爽约率”可以每天凌晨用定时任务算好存进统计表。不要每次打开看板都全表聚合数据量大了之后页面会卡死。澎湃的档案预约群体会出现一天几千条记录看似不多但加上一个月的聚合和关联查询没索引真扛不住。4.4 常见问题速查表症状原因解决方案npm脚本无法运行PowerShell执行策略见上文执行Set-ExecutionPolicy小程序请求全失败域名未配置/未备案/非HTTPS小程序后台配置白名单务必备案预约数据超卖并发下booked_count更新丢更新事务内SELECT...FOR UPDATE行锁二维码显示一片白canvas绘制异步未完成改用后端生成二维码图片返回页面时间差8小时PHP/MySQL时区不一致统一Asia/ShanghaiPDO连接设置时区预约列表下滑卡顿一次性加载全部数据分页触底加载最多一次20条审核表格查询慢缺联合索引加(app_date,status)联合索引核销时手机扫不了码二维码内容格式不标准二维码内容只放预约编号别放中文字符太长容易错4.5 部署时的一个顺序建议最后说一个部署层面的经验很多人栽在这里不要把PHP、Node.js、MySQL一股脑装在同一台机器上。档案馆项目虽然并发不高但PHP和Node.js的常驻进程会产生端口和内存资源的竞争。我当时的部署形态是两台云服务器服务器ANginx PHP-FPM MySQL跑主业务API和数据库。服务器BNode.jsPM2托管 Redis跑队列消费者和定时任务。Nginx代理规则里/api/*转发到PHP/ws/*和/tasks/*转发到Node.js。Redis作为两台服务器之间的“消息管道”PHP处理完预约后向队列push一条消息Node.js的消费者立刻拿到消息去发通知。这个结构的好处是即使Node.js服务出问题也只会影响通知和定时任务用户最核心的预约接口不会挂。再说点个人体会项目做完回头看这套系统的复杂度其实不在代码量而在“预约”这个动作的边界情况太多了。今天用户迟到怎么办明天有人爽约要不要释放名额节假日要不要加场次某个团体临时取消40人后名额怎么回流——每一个小问题背后都对应着一条状态分支。我的建议是动手写第一行代码之前花两天时间和甲方一起把这些问题全部敲定成文字规则哪怕只是写在Excel里。后面90%的返工都是因为需求边界没定清而不是技术实现不了。与其抢时间码代码不如前期多问几个“如果”。如果你想在这个基础上扩展比较实用的方向有对接省政务服务App的预约入口、增加团体预约的附件材料单位介绍信、安全承诺书、把爽约率高的用户加入灰名单后智能释放名额。这些功能都不难但都很能体现预约系统的管理水平。希望这篇实操总结能帮你少踩几个坑哪怕只省下半天调试时间也算值了。
返回列表