周末去博物馆,线上预约几乎成了必经环节。真正做过博物馆预约系统的人都知道,这个业务看着简单,做起来却坑很多:用户端要接小程序、App、H5,管理端要处理场次排期和退改签,现场还要有闸机核验和手持终端。我近期完整参与了一个基于多平台的博物馆预约系统项目,三个端分别是用户端、管理端和核验端,整个项目从需求评审到上线,踩了不少坑,也沉淀了一套可以直接复用的三端架构思路。这篇文章就把整个方案拆开讲清楚,想少走弯路的同学建议看完。
很多人把三端理解成“三个页面”,实际上三端是三条完全不同的业务链路:用户触达、业务管理、线下履约。三者数据同源,但技术特点、交互逻辑、稳定性要求都差别很大。做系统设计时,如果只盯着“做一个预约页面”,后面一定翻车。正常顺序应该是先把三端角色和职责定清楚,再定数据结构,最后才是画界面。
1. 三端整体设计与业务链路拆解
1.1 三端到底指哪三端
先给结论,这个项目里的三端是:
- 用户端:面向普通观众的预约入口,覆盖微信小程序、微信公众号H5、支付宝小程序。观众在这里选日期、选场次、填身份信息、提交预约、获取电子票二维码。
- 管理端:面向博物馆运营人员的Web后台,负责展馆、场次、票务规则、放票量、退票审核、黑名单、数据统计等操作。
- 核验端:面向现场工作人员和设备,部署在闸机、手持PDA或平板电脑上,通过扫码核验预约凭证,完成入场、拦截、放行、记录等操作。
三端从字面上看是“三个客户端”,但本质上对应着一个预约业务从产生到消费的全过程。用户端产生订单,管理端配置供给和规则,核验端消费凭证。任何一端掉链子,预约体验都会崩。
我自己做需求梳理时,习惯画一张角色职责表,把每端能做什么、不能做什么、依赖谁,先用表格固定下来,再进入开发。
| 端 | 主要使用者 | 核心职责 | 典型形态 |
|---|---|---|---|
| 用户端 | 观众 | 查场次、预约、退改、查票 | 微信小程序、H5、App |
| 管理端 | 运营/馆方人员 | 配置场馆场次、管理余票、审核数据 | PC Web后台 |
| 核验端 | 现场工作人员 | 扫票、放行、拦截、异常处理 | 闸机、PDA、平板 |
这个表看起来简单,但能避免很多需求扯皮。比如“预约成功后是否可以改签”,这个问题必须用户端和管理端一起定,因为改签涉及余票变动和库存回补,核验端还要适配新场次的入园逻辑。不提前把职责画清楚,开发到一半才提改签,改造成本极高。
1.2 业务链路与数据流转
三端的数据流其实是一个闭环。管理端先录入博物馆信息、设置开放时间、配置门票库存和预约规则。这些配置下发后,用户端才能展示可预约的场次。观众在用户端提交预约,服务端生成预约订单,同时扣减对应场次库存。到参观当天,观众到场出示二维码,核验端扫码后把状态改为“已核销”。
我特别想强调“状态机”这件事。预约订单在三个端流转时,状态必须严格统一。这个项目里我定义了完整的状态机:
- 待支付:适用于收费特展,免费预约基本不会进入这个状态。
- 已预约:预约成功,等待到场。
- 已取消:用户主动取消或管理员取消。
- 已过期:预约场次结束且未核销,自动过期。
- 已核销:用户完成入场,订单终结。
这个状态机是后续所有端联调的基础。用户端展示什么按钮、管理端能否退票、核验端是否允许放行,全部依赖订单状态。实际开发时,我强烈建议把这些状态和迁移条件做成后端枚举,前端只做展示。如果前端自己拼状态,很容易出现“用户端显示已取消,核验端还能扫出票”的情况。
另一个关键点是“库存扣减与回补”。观众取消预约,库存要回补;用户爽约未到,库存不能回补(因为场次已经过去)。这个逻辑看起来简单,但一旦涉及跨天、跨场次,就容易出问题。我们曾经遇到用户预约上午场,上午未到场,下午又想取消,结果系统把下午场的库存给回补了,就是因为事件回调里没判断场次时间。
1.3 为什么必须多平台,而不是单一渠道
很多博物馆会问:既然有官方App,为什么还要做小程序和H5?真实答案是:你得让用户在“想预约的时候”用最顺手的方式完成预约。
小程序适合高频和社交传播,用户从公众号文章或朋友分享点进来,不需要额外下载,微信授权就能预约;H5适合嵌入官网、第三方平台和一些无法内嵌小程序的场景;App则在深度服务(会员积分、文创预订、多馆通约)上有优势。更重要的是,很多第三方流量入口(如城市服务平台、文旅小程序矩阵)走的是H5或API对接,如果系统不支持多平台,就白白丢失流量。
做三端时,我的建议是“一套服务端API,多端复用”。用户端不管是什么形态,都调用同一套预约接口。这样可以避免各平台预约规则不一致,也方便统一处理库存和防刷逻辑。多端页面可以各自开发,但业务逻辑必须收敛在服务端。
2. 技术选型与核心方案设计
2.1 三端各自的技术栈选择
技术选型这件事,我踩过一次大坑。早期团队给用户端分别开发了原生微信小程序、支付宝小程序和独立H5项目,三套代码,三个排期,维护成本直接翻倍。后来重构时统一换成uni-app,才把成本压下来。
具体选型方案:
- 用户端:uni-app + Vue3,一套代码编译到微信小程序、支付宝小程序和H5。遇到平台差异,用条件编译拆开处理。
- 管理端:Vue3 + Element Plus + Vite,PC Web后台,采用前后端分离结构。
- 核验端:Android原生App + 离线缓存,部署在手持PDA和闸机工控机上。为什么不选Web?因为现场网络不稳定,离线可用是必须的。
服务端统一用Spring Boot 3 + MyBatis-Plus,数据存MySQL,缓存用Redis。接口鉴权采用JWT + Refresh Token,用户端和管理端走不同登录体系。核验端单独发长效凭证,避免现场频繁重新登录。
选这套组合的原因很简单:招人容易、生态成熟、踩坑资料多。像博物馆这种非互联网核心业务的系统,稳定性比酷炫重要得多,没必要追求冷门技术。
2.2 预约核心数据模型设计
数据库设计是这个系统的地基。早期版本出现过同一个订单在用户端显示成功、管理端显示失败的问题,排查到最后发现是字段状态冗余导致的。后来统一了核心表结构。
几张核心表:
- museum:博物馆基础信息,包括名称、地址、开放时间、简介。
- venue / exhibition_hall:馆内展厅或场馆,对应独立预约项目。
- session:场次表,核心字段有场馆ID、开始时间、结束时间、总库存、已预约数、状态。
- order:预约订单表,字段包括订单号、用户ID、场次ID、预约日期、状态、创建时间、取消时间。
- order_ticket:票码表,一个订单可以包含多张票,但博物馆场景通常一人一票,每个票有独立二维码。
- user:用户表,微信openid、手机号、姓名、身份证号(按需)。
这里必须注意“场次”和“库存”的设计。博物馆预约的库存不是简单的“总票数”,而是“每个场次有独立库存”。比如上午场限流1000人,下午场限流1000人。库存挂在session维度,而不是museum维度。很多新手容易把库存设计成博物馆维度,导致上午场满了,整个馆都无法预约。
票码设计也要提前想清楚。订单和票是一对多关系,但核销是按票扫码的。一个用户预约成功后拿到一个二维码,里面绑定ticketId。二维码建议直接存32位随机串,不要用订单号加密,更不要把用户身份证直接拼进二维码,涉及敏感信息。
2.3 并发扣减与防超卖方案
博物馆预约最常见的线上事故就是“超卖”。比如配置了1000张票,最后预约出去1200人,现场直接挤爆。
超卖问题的根源是库存扣减没有做原子操作。常规方案有三种:
- 方案一:数据库乐观锁,update session set stock = stock - 1 where id = ? and stock > 0。这种方案简单,但高并发下数据库压力大,容易行锁竞争。
- 方案二:Redis预扣库存,先decr判断结果是否小于0,小于0则回滚。速度快,能抗住瞬时流量,但Redis和数据库数据一致性要做补偿。
- 方案三:Redis + Lua脚本原子扣减,再异步同步数据库。这个方案兼顾性能和一致性,是这个项目实际采用的方案。
具体做法是,预约接口先执行一段Lua脚本,把用户预约请求放入队列,同时在Redis里执行库存扣减。如果扣减成功,再异步写订单到MySQL;如果MySQL写入失败,通过定时任务补偿回滚库存。
这里要给一个避坑提示:Redis库存扣减千万不能用“先GET再SET”的非原子操作,并发下百分百超卖。必须用DECR或Lua脚本保证原子性。
另外,防超卖还要处理“同一用户并发预约同一场次”的问题。用户在App里快速点击两次提交,后端可能同时收到两个请求。我们增加了用户维度分布式锁,对同一用户同一场次加锁,保证同一秒内只能生成一个订单。锁的过期时间设了10秒,避免用户长时间占用锁导致其他请求排队太久。
3. 三端功能拆解与实操实现
3.1 用户端:从查询场次到生成票码
用户端的主流程是:进入系统 -> 选择场馆 -> 选择日期 -> 选择场次 -> 填写预约人信息 -> 提交 -> 成功出票。
第一步,场馆和场次列表。这个接口必须做缓存,因为所有用户进入首页第一眼看到的就是可预约场次。我们把场馆信息和最近三天的场次余票缓存到Redis,缓存时间10秒。为什么是10秒?太长会导致管理端改库存后前端不及时展示,太短又扛不住流量。
第二步,选择场次后,用户填写预约信息。免费预约一般只需要姓名和手机号,收费特展或实名制场馆需要身份证号。这里有一个经验:身份证号一定要做格式校验和后端脱敏。前端校验只能卡格式,后端必须做真实性和格式二次校验,同时加密存储。
第三步,提交预约。用户端提交后,后端返回预约结果。免费预约直接返回“预约成功”,收费预约需要跳转支付。这个项目早期免费预约也做了“待确认”状态,导致用户反复提交,后来改成提交即成功,体验才正常。
第四步,生成票码。预约成功后,服务端生成ticketId和对应的二维码内容串。二维码内容是一个短字符串,比如“MUS”+ 日期 + 随机UUID。注意不要把整个订单JSON塞进二维码,现场扫码枪和闸机的解码能力有限,内容越短越稳。
用户端还有一个高频操作是“查看我的预约”。列表页要支持按状态筛选,包括待使用、已使用、已取消、已过期。过去有个坑是,已过期订单和已取消订单在界面上不区分,用户分不清到底是自己取消的还是没用过期,后来增加了状态筛选才解决问题。
3.2 管理端:排期、库存与数据统计
管理端是很多项目经理容易忽视的端。实际上,管理端的开发量不比用户端少,而且更考验业务梳理能力。
管理端核心模块包括:
- 场馆管理:维护博物馆、展厅、开放状态。
- 场次管理:创建场次、设置时间段、设置总库存、设置预约规则(提前几天、截止时间)。
- 订单管理:查看全量订单,支持退票、改签、异常标记。
- 黑名单管理:对恶意预约、频繁取消、刷票用户进行限制。
- 数据统计:按日、按周、按月统计预约量、核销率、爽约率、热门时段。
这里重点讲一下场次管理的实操。创建场次时,系统要自动校验时间段冲突,不能一个展厅同时存在两个重叠场次。我们采用数据库唯一索引来兜底,对“展厅ID+开始时间”加唯一约束,同时前端做时间选择器限制。别小看这个校验,出现过运营人员手滑创建了两个重叠场次,直接导致预约量翻倍超出接待能力。
库存设置的逻辑也要讲清楚。管理端设置的是“场次总库存”,而不是“剩余库存”。剩余库存由系统实时计算,管理端只负责设定上限和调整放票策略。比如遇到热门展览,运营人员会把某个场次的库存从500临时调到800,这时系统要支持“动态扩容”。动态扩容的实现就是在session表里update总库存,同时Redis里的库存也要同步加回去。
数据统计这块,建议直接分库分表前先做好索引。预约订单表按日期频繁查询,必须建立联合索引(session_id, status, create_time)。我们曾经因为漏了status索引,统计“今日核销数”时走了全表扫描,数据库CPU直接打满。
管理端的权限模型也要提前设计。馆方人员不止一个角色,超级管理员能看到全部数据,普通运营只能操作自己的场馆。我用RBAC模型,用户表、角色表、权限表、用户角色关联表四张表,前端再根据权限码控制按钮显隐。
3.3 核验端:扫码放行与离线处理
核验端是项目上线后最容易被吐槽的端,因为场馆现场环境太复杂了。核验端要处理几种核验模式:
- 正常核验:用户出示二维码,核验端扫描后请求服务端校验,返回结果“有效”,点击放行。
- 离线核验:网络断开时,核验端本地缓存已下载的预约数据,扫描后先本地匹配,记录入场日志,待网络恢复后上传。
- 异常核验:二维码已过期、已取消、已核销、非当日场次、身份证不符等,需要拦截并提示原因。
离线核验是这个项目里技术难度最高的模块。我们设计了一个“每日白名单”机制:每天晚上,服务端把次日所有待核销的订单数据打包下发到核验端本地,包括订单号、票码、场次、用户姓名脱敏信息。现场扫码时,核验端先在本地数据库查,查到且状态正确就放行,网络恢复后把核验记录POST到服务端,服务端做幂等处理。
这里有个细节,本地下发数据不能包含身份证号等敏感信息,只保留脱敏信息。因为核验端设备可能放在公共区域,如果设备丢失,敏感数据泄露风险很大。我们只下发“姓名最后一个字+手机号后四位”用于人工核对。
核验端建议强制使用Android设备而不是iOS,因为闸机工控机基本都是Android主板,且PDA的扫码枪兼容性也以Android为主。如果做iOS版,后续接入闸机、身份证读卡器会非常麻烦。
另外,核验端的界面要做“大字模式”,现场工作人员普遍一边排队一边操作,界面太小容易按错。重点信息(状态、姓名、场次时间)要一眼可见,操作按钮要足够大。
4. 常见问题与排查技巧实录
这个项目上线三个月,我整理了几个高频问题,每一个都在线上真实发生过,排查思路值得记住。
| 问题现象 | 根本原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 明明有余票,用户却预约失败 | Redis缓存库存与MySQL库存不一致 | 对比Redis key和session表stock字段 | 增加定时对账任务,以数据库为准回写Redis |
| 二维码已核销,但用户端仍显示待使用 | 核验端网络异常,核验记录未上传 | 查核验端本地上传日志和服务端接口日志 | 上传接口增加幂等,本地保留核验记录直至确认成功 |
| 上午场的票,下午还能扫出来 | 票码按订单维度生成,未绑定具体场次时间 | 扫描订单绑定session的开始时间 | 校验时强制比对session日期和时段 |
| 预约成功短信迟迟收不到 | 短信发送依赖第三方通道,高峰排队 | 查短信服务商发送状态回调 | 用户端增加“票码查看”入口,短信仅作辅助通知 |
| 管理端统计核销率超过100% | 同一订单被重复统计 | 检查核销接口是否幂等,数据库是否有唯一约束 | 核验记录表加unique key(ticket_id) |
4.1 库存不一致问题
这是预约系统最经典的坑。我们第一次压测时就发现,Redis显示剩余500张,数据库实际只剩200张。原因是一次并发压测时,Lua脚本扣减了Redis库存,但异步写订单时数据库连接池满了,写入失败,库存没有回补。
解决方案分两层。第一层,写订单失败时立即发起库存回补,回补动作也走Lua脚本。第二层,增加一个定时对账任务,每5分钟扫描一次Redis库存和数据库差额,不一致时以数据库为准强制校准。对账任务里要加告警,差值超过设定阈值就要通知值班人员。
4.2 核验端离线数据上传丢失
现场工作人员经常遇到这种情况:手机网络不好,扫了几个人的票都显示放行,但回后台一看核销记录少了。排查发现,上传接口没有做重试和幂等,网络抖动时请求失败就直接丢了。
修复时我先在核验端本地建了一张sync_log表,扫码核验先写本库,标记sync_status=0,上传成功后置为1。每次启动或网络恢复时,把所有sync_status=0的记录重新上传。服务端核销接口接收参数里带ticketId和核验时间,核销表对ticketId建唯一索引。这样即使重复上传也不会重复扣减。
4.3 时区与跨天场次问题
博物馆偶尔有凌晨场次,比如科技馆的夜场活动。普通用户习惯认为是“今晚的活动”,但系统日期是第二天凌晨。如果不对时区做统一处理,用户端显示日期、核验端校验日期就会出现偏差。
我们统一在服务端用epoch时间戳存储,前端按本地时区格式化展示。核验端校验时,把“场次开始时间”和“当前时间”都转成时间戳比较,不比较字符串日期。这个改完以后,跨天场次再也没出过问题。
5. 防刷、限流与数据安全策略
博物馆预约系统最头疼的是黄牛刷票,尤其是热门免费馆,放票瞬间服务器被打满,真人用户根本进不去。这个项目里我做了几件事。
第一,接口限流。预约提交接口按用户维度做限流,同一个openid或手机号10秒内最多请求5次。IP维度做更严格的限制,同一IP每秒超过20次直接拒绝。我们用Redis的滑动窗口实现限流,简单可靠。
第二,风控校验。针对高频预约、异常时段操作、多个账号同一设备登录等情况,加风控判断。比如一个设备ID在短时间内注册了多个微信账号并预约,系统直接拉黑设备指纹。
第三,预约频次限制。同一用户每天最多预约一个场次,每周最多三次。预约成功后未核销且未取消的,不允许再约其他场次。这个业务规则需要在服务端校验,不能只在前端遮罩。
第四,实名认证。部分场馆要求实名制预约,用户上传身份证号后,服务端对接公安接口或通过三要素验证。这些信息传输必须走HTTPS,存储必须加密,日志里不能打印完整身份证号。我们统一做了AES加密存储,展示时只露出前六位和后四位。
数据安全这块,建立了一个日常巡检机制:定期检查接口权限,确保核验端接口不能被普通客户端伪造调用;管理端后台强制HTTPS访问,登录加验证码;导出报表时对手机号、身份证号默认脱敏。
6. 上线前压测与部署经验
这个系统上线前,我们做过一次完整的压测,发现了很多平时测不出来的问题。压测工具用的JMeter,模拟不同场次同时抢票的场景。结论是:单机版服务端只能扛住每秒40个预约请求,超过就会超时;Redis的库存扣减没有问题,瓶颈反而在订单写库。
针对瓶颈,做了三层优化:
- 数据库连接池扩容,从默认10调到50。
- 订单表按日期分表,比如order_20250629,避免单表数据量过大。
- 预约接口做异步化处理,前端提交后立即返回“处理中”,订单创建结果通过轮询或WebSocket通知。这个改动对用户体验影响很大,但能扛住瞬时峰值。
部署架构上,我推荐至少三台应用服务器加一台Redis和一主两从MySQL。Redis一定要开启持久化,否则宕机后库存缓存丢失,会导致系统全部不可用。MySQL配置主从同步,从库做日常统计查询,主库只负责写和核心读。
日志链路必须全链路追踪。用户端、管理端、核验端的请求都要带上traceId,服务端打印日志时统一输出traceId和userId。排查问题的时候,没有traceId就是在大海捞针。
7. 实际体会与后续扩展方向
做这套三端系统的过程里,我最深的体会是:核验端的稳定性比花哨功能更重要。很多团队把精力放在用户端界面好不好看、管理端报表多不多,结果现场扫码频繁失败,参观者排队骂街,前面做的所有体验优化都白费了。核验端必须保证离线可用、扫码灵敏、状态判断准确,这是博物馆预约系统的生命线。
另一个体会是“数据一致性必须用机制保证,而不是靠人肉修复”。库存对账、订单幂等、上传重试,这些机制前期虽然开发成本高,但上线后能省掉大量运维精力。我见过一些项目出了数据问题就手工改库,短期是解决了,长期一定是灾难。
后续这个系统还可以往两个方向扩展。一个是“预约即服务”,对接讲解预约、教育活动报名、文创提货,把预约系统升级成博物馆的观众服务平台;另一个是“数据反哺运营”,通过预约数据和爽约率分析,指导场馆调整开放策略,比如把冷门时段做成低峰专场,吸引错峰参观。三端架构定好以后,这些扩展都只是加模块的事情。
如果让我给正在做类似系统的人一个建议,我会说:先把三端的职责边界画清楚,再把订单状态机和库存扣减方案定下来,最后再开始写代码。这三件事想明白了,项目就成功了一半。剩下的,都是工程量问题,慢慢做总能做完。