ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue智能密室逃脱管理系统:从场次状态机到实时进度推送的工程实践

SpringBoot+Vue智能密室逃脱管理系统:从场次状态机到实时进度推送的工程实践 密室逃脱门店这两年越来越火但很多店主的管理方式还停留在微信群接龙 Excel排班 对讲机喊话的阶段。朋友开了一家密室店周末高峰期订单撞车、场次重叠、玩家提前到了找不到预订记录、游戏里线索卡住只能靠店员隔着门吼这些问题每周都在重复发生。我帮他做的这套基于SpringBoot Vue的智能密室逃脱游戏信息管理系统目标就是把主题-场次-订单-玩家-游戏进度整条链路管起来让玩家在小程序或前台完成预订让店员在后台一目了然看到所有场次状态让游戏过程中的线索解锁、倒计时提醒、进度记录全部自动化。开发中有不少业务上的取舍和工程上的坑这篇文章把它们完整讲一遍适合正打算做线下门店管理类系统的朋友参考尤其是密室、剧本杀、沉浸式剧场这类强流程、强状态管理的场景。这类系统表面看是订票系统实际比订票复杂得多。我得先理清楚一个核心问题密室逃脱的一场游戏到底涉及哪些角色、哪些状态、哪些时间节点否则数据库表设计出来也是错的。1. 密室逃脱业务的独特需求为什么这不是普通的订票系统1.1 线下密室门店的实际运营流程先说清楚一家典型密室门店一天是怎么运转的。门店有多个不同主题的密室房间比如丧尸围城、古墓探险、校园怪谈每个主题在同一天会排多个场次每个场次能容纳的人数不同有的4人起开、最多8人。玩家通过电话、微信或者到店现场预订某个主题的某个场次时段到店后核销入场进入房间开始游戏。游戏开始后玩家需要在规定时间内通过解谜、寻找线索、破解机关最终从房间逃出。这个流程里最容易被忽略但最致命的是场次的状态流转。一个场次不是有预订和没预订这么简单它从排期开始就处于待开场状态玩家到场核销后变成进行中游戏结束变成已结束如果因为人数不足没有开场或者玩家临时取消还要有已取消状态。每个场次还有开始时间和结束时间下一场能不能按时开取决于上一场是否准时结束。我一开始给朋友做需求梳理时他给我列了十来个功能什么房间管理、订单管理、会员管理、评价管理但真正坐下来聊了两小时后我们发现系统最核心的实体根本不是这些散点而是**场次**——它就像一个剧本的舞台所有元素都围绕它展开。每个场次关联着一个主题、一组玩家订单、一条游戏进度记录、若干条已触发的线索以及一整套时间节点。想清楚这一点整个系统的主干就出来了。1.2 智能二字到底智能在哪里标题里写了智能这个词很容易变成营销噱头但这个系统里它确实落地了几个实打实的功能。第一是场次冲突检测。同一个密室房间的物理属性和NPC配置决定了它不可能同时开两场所以排场次时系统要自动检查时间重叠不能让新场次和已有场次的时间段交叉。这个我用的是简单的区间重叠判断后来发现还有更细的场景比如有的主题需要同一个NPC串场两个不同房间的场次虽然不冲突但共用NPC就可能撞车最后我把NPC资源也纳入冲突检测维度。第二是超时自动释放。很多门店会被放鸽子的订单坑惨玩家订了晚上八点场结果七点五十打电话说不来了场次空着又来不及卖给别人。系统做了一个规则预订后15分钟内未支付自动取消开场前2小时取消退还定金开场后未到店自动释放名额。这些规则全部用SpringBoot的定时任务 状态机组合实现每天自动执行。第三是游戏过程中的实时进度管理。这是我最喜欢的一部分。玩家在密室里每解开一道锁、找到一个线索、按下某个开关都需要向系统上报事件。系统根据预设的剧情树判断该解锁什么新线索、该给玩家推送什么提示、倒计时还剩多少。这就不是简单的信息记录了而是一个轻量级剧情推进引擎。1.3 盘点每个模块要解决的真实问题把业务吃透之后我划分出以下模块每个模块都有明确的问题域主题管理维护密室主题的票价、人数上下限、难度、时长、背景介绍。场次排期按日期、时间段、房间、NPC资源生成场次处理冲突。订单管理玩家预订、支付、改签、取消、退款。玩家管理记录玩家基本信息和历史游戏数据比如逃出率、通关时长。游戏记录每一场游戏的完整过程包括玩家提交的每个答案、触发的线索、使用提示次数。线索与机关管理维护剧情树、线索层级、机关命令。评价模块玩家体验后的评分和评论反哺门店运营。数据看板按日、按周统计场次上座率、订单转化率、热门主题。这一步非常重要。我见过太多项目一上来就建表结果做到一半发现订单和游戏记录的关系没想清楚只能推翻重来。所以这篇文章的第二、三章我会把设计决策和落地过程写清楚第四、五章专门讲开发实测踩过的坑。2. 技术选型的权衡过程SpringBoot Vue组合的取舍2.1 后端为什么锁定SpringBoot而不考虑其他框架这个项目的后端技术栈比较保守选了SpringBoot但我不是跟风而是基于实际需求做了一轮对比。先说为什么不用传统的SSHSpring Struts Hibernate。Struts现在几乎没有新项目用了社区活跃度和招人难度都不划算光是维护XML配置就能劝退人。SpringBoot最大的好处是**约定大于配置**起步快内嵌Tomcat一个main方法就能把应用跑起来不用单独部署WAR包。对于密室门店这种中小型项目开发效率和部署便捷度是第一位的SpringBoot天然匹配。然后对比一下SpringCloud。星球内部讨论过要不要直接上微服务我否了这个方案。门店系统高峰期的并发量再大也就是几百人同时操作一个单体应用完全扛得住。微服务带来的服务拆分、服务注册、配置中心、分布式事务这些复杂度对团队来说是纯粹的成本没有任何收益。等项目真到了多门店、多区域、高并发的阶段再拆也不迟。SpringBoot本身的生态也是我坚定的原因。Spring Data JPA、MyBatis-Plus、Spring WebSocket、Spring Task、Redis Starter这些场景基本上都有现成的starter。尤其是我需要定时释放超时订单、用Redis缓存场次状态、用WebSocket推送游戏进度这套组合在SpringBoot里集成几乎零成本。2.2 前端为什么选Vue不选React前端用Vue也不是因为Vue比React更好而是基于自己的开发团队情况和项目特点做的取舍。参与这个项目的成员以前写过Vue2上手Vue3几乎没有学习成本。Vue的单文件组件SFC把模板、脚本、样式放在一个文件里比JSX的写法更直观尤其适合做后台管理系统这种以表单、表格、列表为主的中后台界面。Element Plus组件库直接提供抽屉、弹窗、表单校验、分页表格能省掉大量重复的UI工作。对比React说实话React的生态和函数式思维非常强但它的组件化写法对团队成员的要求更高Hooks的依赖数组、memo、useCallback这些概念需要长期练习才能写好。而Vue的响应式系统是自动追踪依赖的心智负担低写得快。项目交付周期就两个月选Vue能显著降低团队出Bug的概率。Vue路由用vue-router处理页面跳转和权限拦截很方便。后台管理系统需要区分管理员和店员两种角色我在路由配置里加了全局前置守卫未登录跳转到登录页无权限跳转到403页。这种场景Vue生态里都有成熟方案组合起来非常顺。2.3 从数据库选型到部署方式的整体考量数据库选了MySQL 8.0原因是业务数据是典型的关系型数据订单、场次、玩家之间的关联查询非常多用MySQL很合适。MySQL 8.0在窗口函数、JSON支持、性能优化上比5.7强不少后面做数据统计报表时用窗口函数确实方便很多。缓存层面引入Redis。密室系统里场次的实时状态已预订名额、当前进行到哪个环节、剩余时间会被频繁查询和更新如果全部走MySQL高并发下的压力很大。Redis的Hash结构很适合存场次状态比如field: scheduleId, value: currentPlayingState利用TTL还能实现场次开场倒计时。另外秒杀场次时用Redis的incr原子操作防止超卖这个后面细讲。部署上前后端分开打包一起放在一台云服务器上。SpringBoot打JAR包Vue项目构建后生成静态文件放到Nginx的html目录Nginx监听80端口把/api开头的请求反向代理到后端的8080端口。这套部署方式最省成本一台2核4G的服务器就能跑得很稳。Nginx的关键配置提一下server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } 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; } }try_files是为了解决Vue路由的history模式刷新后404的问题必须配置。proxy_pass解决前后端跨域开发环境下我会用Vite的proxy配置部署环境用Nginx代理两个环境的跨域方案是分开的。3. 核心模块落地从数据库表设计到接口实现3.1 核心表结构设计与状态机建模我把表结构先铺出来这是整个项目的地基。核心表大概有以下几张表名用途关键字段theme密室主题表id, name, price, min_players, max_players, duration, difficulty, descriptionschedule场次排期表id, theme_id, room_id, start_time, end_time, status, current_players, max_playersplayer玩家表id, nickname, phone, password, levelbooking_order订单表id, order_no, schedule_id, player_id, status, pay_status, pay_amount, create_timegame_progress游戏进度表id, schedule_id, current_stage, remaining_seconds, statusclue线索表id, theme_id, parent_id, stage, content, unlock_conditiongame_event游戏事件记录表id, schedule_id, event_type, event_data, create_timeevaluation评价表id, order_id, score, content, create_time最值得讲的是状态机建模。场次状态和订单状态我都用TINYINT存数字枚举值然后在Java里用枚举类管理不直接散落魔法数字。场次状态我定义了五种待开场(0)、已开始(1)、已结束(2)、已取消(3)、已满员锁定(4)。注意已满员锁定不是终态而是有人下单后触发满员时防止继续超卖的一个中间状态等有人取消订单或者游戏开场后自动恢复或流转。订单状态更复杂一些已创建待支付(0)、已支付(1)、已核销入场(2)、已取消(3)、已完成(4)、已退款(5)。状态流转有严格方向比如待支付只能到已支付或者已取消已支付可以到已核销、已取消、已退款已核销之后只能到已完成。我写状态机不是简单用if判断而是建了一个订单状态流转的MapOrderStatus, ListOrderStatus每次转换时校验当前状态是否允许转换到目标状态。后面店员误操作或者定时任务释放订单时这条规则帮我拦住了好几次非法状态跳转。3.2 场次排期与预订去重的实现逻辑场次排期最核心的需求是防止同一房间的时间段重叠。我先实现了最简单的区间重叠判断SELECT COUNT(*) FROM schedule WHERE room_id #{roomId} AND status IN (0, 1, 2) AND start_time #{endTime} AND end_time #{startTime}这条SQL能查出已存在的场次和待新增场次在时间上是否有交集。但仅仅查room_id还不够还记得前面提到的NPC资源冲突吗同样一个NPC如果被安排在两场同时进行的游戏里真正到店运营时就会出问题。所以我把这条SQL扩展成同时检查房间和NPC两个维度NPC冲突也走同一个区间重叠逻辑。预订去重我用了双重防线。前端提交订单时后端先查当前场次的current_players是否小于max_players这是第一道检查。但高并发下两个请求同时查到还有名额就会一起下单导致超卖所以第二道防线是给schedule表加version字段做乐观锁Update(UPDATE schedule SET current_players current_players 1, version version 1 WHERE id #{scheduleId} AND current_players max_players AND version #{version}) int reduceStock(Param(scheduleId) Long scheduleId, Param(version) Integer version);这条SQL的WHERE条件保证只有当前人数还没满时才执行更新影响行数为1才表示扣减成功否则抛出该场次名额已满的异常。实际压测时20个并发请求同时抢一个只剩1个名额的场次最终只有1个订单创建成功验证了乐观锁的可靠性。3.3 游戏过程中的实时进度推送WebSocket Redis游戏开始后玩家在密室里的所有操作都需要实时上报给系统同时系统要把倒计时、线索解锁提示、下一关卡信息实时推送到店员的监控端和玩家的辅助端比如平板。这个场景天然适合WebSocket。后端我用Spring的WebSocketHandler来处理连接。每个场次对应一个WebSocket通道连接URL里带上scheduleId作为标识比如ws://localhost:8080/ws/game?scheduleId123。一旦有玩家端或店员端连接系统就能根据scheduleId建立会话映射。游戏过程中产生的每个事件都先写入MySQL的game_event表保证数据可追溯同时还通过WebSocket推送给所有关注这场游戏的客户端。事件类型包括线索触发、密码输入、门锁开启、提示请求、超时预警等。Redis在实时进度里起到当前状态缓存 分布式锁的作用。比如当前剩余秒数我不会在每次事件时都去MySQL查而是存在Redis的String里倒计时每秒更新一次。开场时把剩余秒数写入Redis每秒钟由前端根据WS推送更新时间戳后端只在关键事件节点把最终时间写回MySQL。这块性能压力小很多。这里还要考虑一个问题玩家中途可能想放弃游戏或者游戏超时了还没逃出。我在倒计时归零时自动推送游戏结束逃生失败的事件同时把场次状态从已开始流转为已结束。这个逻辑不依赖玩家端是否在线而是由后端定时任务在Redis的键过期时触发回调来兜底防止玩家中途关掉页面导致游戏卡死。3.4 线索机关管理与剧情推进规则密室体验的智能很大程度体现在线索机关管理上。每个主题的剧情不是一条直路而是一棵剧情树。我把线索分成多层节点根线索初始提示-子线索根据玩家操作逐步解锁-隐藏支线。每个线索节点有几个关键属性stage表示它属于第几阶段unlock_condition表示解锁条件content是具体文字或语音内容media_url可以挂载一段视频线索。这里我补充一个细节密室里的部分线索是视频形式为了在手机或平板上流畅播放且防盗录我用的是m3u8格式的视频流前端用video.js配合HLS支持播放。这个在技术实现上比普通MP4多一道转码和切片但实测在弱网环境下加载速度确实好很多而且天然支持拖动进度条。剧情推进的核心是一个规则引擎方法public ListClue handlePlayerEvent(Long scheduleId, String eventType, String payload) { GameProgress progress getProgress(scheduleId); ListClue available clueService.getAvailableClues(progress.getCurrentStage()); ListClue unlocked new ArrayList(); for (Clue clue : available) { if (matchCondition(clue.getUnlockCondition(), eventType, payload)) { unlocked.add(clue); gameEventService.record(scheduleId, CLUE_UNLOCK, clue.getId()); } } return unlocked; }这个方法会接收玩家的事件比如在古墓房间的某个墙角输入了 CODE: 1024然后遍历当前阶段可用的线索判断unlock_condition是否匹配事件。匹配成功后新线索通过WebSocket推送到玩家端同时记录到game_event表里。这条链路的难点是线索的时序控制。有的线索必须在某个时间之后才解锁有的必须在前置线索被玩家阅读过之后才触发。我都通过unlock_condition里的JSON条件表达式来实现比如{time_after: 600, required_clue: 1023, event: CODE:1024}这样写灵活、不写死逻辑。实测下来主题策划人员在不改代码的情况下也能通过后台配置新的剧情分支这个设计是项目里性价比最高的一块。4. 开发实测中踩过的坑版本、配置与性能4.1 SpringBoot版本过高引发的配置问题与回退决策项目一开始我图新SpringBoot选了当时的3.x版本结果开发到第四天就出了幺蛾子。情况是这样的SpringBoot 3.x基于Jakarta EE 9规范把javax.*的包名换成了jakarta.*很多老版本的第三方库没有跟上导致集成时直接报ClassNotFoundException。先是MyBatis的官方starter在3.x下兼容不好调试了半天换新版本才搞定然后又是某个文件上传工具库不兼容编译一直报错。更麻烦的是Spring Security 6.x的配置方式也变了网上大量资料还是针对Security 5.x的写法。我查了排查链路启动日志里先看到Failed to load ApplicationContext然后是BeanCreationException最里层是ClassNotFoundException: javax.servlet.Filter。一看到javax就明白了这是包名迁移问题。我的决策是回退到SpringBoot 2.7.18这是2.x系列的最后一个版本既保留了大量第三方库兼容又有比较新的安全补丁。回退之后MyBatis、Spring Security的配置都能复用社区成熟方案开发进度立刻恢复正常。这里给各位一个建议不是新项目就一定要用最新版本SpringBoot要综合评估团队熟悉度和第三方库兼容性。2.7.x在2024年之后虽然维护周期过了但对于一个小型门店系统来说稳定性优先级远高于版本新鲜度。4.2 Vue环境配置与依赖安装的雷区Vue项目开发踩的坑集中在环境配置上。当时团队里有台Windows电脑安装了最新的Node 20结果npm install一直报ERESOLVE unable to resolve dependency tree。查下去发现是某个组件库的依赖和Node 20的npm版本解析策略冲突。解决路径是这样的先执行npm install --legacy-peer-deps临时绕过依赖树冲突把项目跑起来然后用npm ls查看依赖树确认冲突来源是一个老版本的第三方库需要旧版Vue。最终方案是把Vue组件库升级到兼容Vue 3.4的版本然后删掉node_modules和package-lock.json重新安装问题彻底解决。Vue项目里还有两个几乎每个项目都会犯的问题。第一个是开发环境的跨域代理。Vite默认的baseURL如果写死成http://localhost:8080部署到线上肯定要改代码。我的做法是在.env.development和.env.production里配置不同的环境变量比如VITE_API_BASEhttp://localhost:8080在代码里用import.meta.env.VITE_API_BASE动态取这样切换环境不用改源码。第二个是刷新页面404。这个和前面说的Nginxtry_files配置有关如果部署时漏了Vue路由的history模式刷新任何二级页面都会白屏坑了好几个第一次用Vue的同事后来我在部署文档里把这段配置放在最前面标红。4.3 MyBatis分页插件在多表关联查询下的深坑后台管理系统离不开分页我用的是MyBatis的分页插件PageHelper。大部分场景下它很好用一行PageHelper.startPage(pageNum, pageSize)后面直接跟查询SQL就完事。但我在订单列表加玩家昵称 主题名称 场次时间三个维度筛选时分页就出问题了。现象是分页的总条数和实际查询结果对不上有时候一页显示不满有时候重复显示数据。排查链路如下打开分页插件的SQL日志发现执行那条分页count语句时插件自动生成的SELECT COUNT(0)把主查询里的多表JOIN全带上了又因为订单表和玩家表是多对多关系JOIN产生的笛卡尔积导致count结果偏大但分页查询又用DISTINCT去重了两边口径完全不同。解决方案有两个方向。最简单的是改造查询SQL把多表JOIN拆成两步先查出符合筛选条件的scheduled_id分页列表再根据ID批量查订单详情这样PageHelper内部的count和目标列表查询都只针对booking_order单表不会出现JOIN膨胀。如果确实需要一次JOIN查询可以在业务层自己写COUNT语句避免使用PageHelper的自动count// 方式一拆分查询 ListLong ids orderMapper.findIdsByConditions(pageNum, pageSize, condition); ListOrderVO list orderMapper.findOrdersByIds(ids); // 方式二自定义count Long total orderMapper.countByConditions(condition); ListOrderVO list PageHelper.startPage(pageNum, pageSize) .doSelectPage(() - orderMapper.findPageByConditions(condition));我这里最终选了方案一。因为订单查询本身就经常要加新的筛选条件拆分查询后每个查询的语义都清晰也方便针对复杂条件走不同的索引。4.4 WebSocket连接稳定性与断线重连WebSocket在局域网测试时一切正常但部署到云服务器后就遇到了诡异问题玩家端隔几分钟WebSocket自动断开有时候彻底连不上。排查链路首先看客户端的onclose事件触发时机发现是Nginx的默认proxy_read_timeout是60秒一旦WebSocket连接空闲超过60秒Nginx就会主动切断连接。解决方案是在Nginx的location里显式配置WebSocket的升级和超时参数location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }Connection: upgrade是WebSocket握手成功的关键如果没有这行Nginx层就把升级请求当成普通HTTP转发客户端永远连不上。proxy_read_timeout设成3600秒防止空闲断连。但即使Nginx配置对了客户端在弱网环境下还是可能断开不能指望网络永远正常。所以我在前端封装了一个WebSocket工具类核心逻辑是心跳检测 指数退避重连每30秒发送一次ping消息如果连续三次没收到pong就主动断开连接然后按1秒、2秒、4秒、8秒...的退避间隔重连重连成功后恢复订阅原来的scheduleId。这套机制上线后基本没有再被投诉过掉线问题。5. 系统测试与门店实测效果5.1 并发预订场景的压力测试密室门店周末高峰期有个很真实的问题某个热门主题的热门时段放出来十几个人同时抢最后几个名额。我给系统做了并发测试模拟20个用户同时提交同一个场次的预订请求。测试工具用JMeter创建20个线程组每个线程并发请求创建订单接口场次最大容量设置为8人。测试结果很干净19个请求返回已满员异常只有8个请求成功创建了订单如果后端还有库存检查延迟可能先返回成功再异步校验这里最终以数据库的current_players字段为准确保不多卖一个名额。测试中还发现一个性能问题下单接口在并发时单次响应时间偶尔飙升到1.5秒排查后发现是事务里同步发送了短信通知短信服务商接口响应慢拖累了整个下单事务。我做了两个优化短信通知改为异步线程池发送核心下单事务只保留数据库操作和Redis更新。优化后单次响应时间稳定在200毫秒以内。5.2 现场实测中的流程差异与修正压测通过后朋友安排在一家门店试运营了两周这一跑又暴露了好几个设计时没考虑到的问题。第一个问题是我以为核销入场就是员工拿手机拍一下二维码结果门店实际用的是扫码枪。扫码枪输入二维码字符串后自动回车所以前端的核销页面必须监听回车键自动提交否则店员每次扫码后还要手动点一次确认按钮高峰期全挤在前台。改完这个交互后店员说好用多了。第二个问题是线索解锁的时间条件。我们的规则引擎支持time_after条件意思是游戏开始多少秒之后才允许解锁某个线索。但实测时发现很多线索卡在玩家已经在正确的位置做对了动作但因为时间没到没解锁玩家就在那干等体验很糟。后来改成如果玩家在时间条件未满足时做出了正确动作就把该事件标记为待解锁并推送给店员端由店员判断是否人工提前解锁。这是一个典型的规则引擎需要人工干预兜底的场景。第三个问题是游戏结束后的复盘数据。玩家逃出后店员会在广播里恭喜一下但玩家更想知道自己用了多少次提示、每个支线是否触发、通关时间排名。这些数据系统都记录了但最开始只展示在后台没有对玩家端开放。后来我加了一个游戏报告页面用卡片展示每阶段耗时和提示次数玩家离场后在手机端就能看这项反而成了店里点评里被提到最多的小亮点。6. 上线后的维护心得与扩展方向系统稳定运行到现在维护层面有几个经验想分享。数据库要定期做慢查询日志分析。后台的订单筛选页面如果加上时间范围查询SQL可能会因为日期条件没走索引而全表扫描。我在MySQL里给booking_order.create_time、schedule.start_time这些字段建了组合索引并把慢查询阈值设为1秒配合EXPLAIN分析执行计划基本能把性能问题消灭在早期。Nginx的访问日志也要关注。如果发现大量请求堆积在/api/schedule/list接口可能是前端轮询设计不合理该用WebSocket推送给全改了没用轮询。很多前端同学习惯用setInterval去拉数据短时间内没问题长跑起来对服务器压力不小建议能用推送就用推送方案。关于业务扩展方向这套系统的架构给后续留了很好的余地。一个是小程序端现在玩家预订还要通过店员在PC后台操作下一步我会把它接到微信小程序把场次查询、下单支付、游戏报告全部对C端开放另一个是智能硬件对接目前的线索机关还停在软件层面如果要联动真正的电子锁、震动地板、烟雾机理论上可以在game_event表的事件类型里扩展一个HARDWARE_COMMAND通过MQTT下发指令到局域网内的智能网关。Data大屏也一样目前数据报表是后台图表后面可以单独抽一个只读的看板页面投到门店大厅电视上。最后分享一点我这次项目里最大的体会技术坑都好填业务梳理才是最耗时间、又最容易被低估的部分。如果你也准备做类似的门店管理系统建议先把门店一周真实运营的每个动作、每个时间卡口列成流程图再让店员反复确认最后再动代码。这个前置工作做得越细后面返工的概率越低。像场次状态机、订单流转规则这种东西画在白板上的时候觉得懂了真正落到表设计的时候才知道自己是不是真的懂了。这套系统的代码不是最复杂的但它能让一个三个店员、八个主题的密室店从容度过周末高峰期每个场次都按节奏推进玩家玩得明白、店员管得轻松这就是它存在的意义。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进