接蓝牙打印机看起来就是「连上设备、把小票内容写进去」。我按文档调通了「打印一行英文」之后以为做完了,实际问题一个接一个,四类。
一是中文打出来是乱码。我直接把字符串转成字节写进去,英文正常、中文全是乱码。原因是打印机默认按特定的中文编码解析,而我传的是另一种编码的字节。而小程序环境里没有现成的编码转换能力,这一步要自己解决。
二是内容长一点就打出乱码或者只打一半。一张小票几百字节,我一次性写进去,结果是前面正常后面乱掉。查了才知道低功耗蓝牙的单次写入有长度上限,超出的部分设备根本收不到或者收乱了。而这个问题在我测「打印一行英文」时完全不出现——因为一行英文没超过上限。
三是高峰期多单同时来,打印全乱。几个订单同时进来,我为每单各起一次写入,几路数据交叉写到同一个设备上,打出来的小票内容互相串了。
四是打印失败没有任何反馈,后厨漏做单。打印机没纸了、被人碰掉了、蓝牙断了——我的实现里这些情况都是静默失败。商家不知道有单没打出来,后厨不知道要做这道菜,顾客等了半小时来问才发现。这是最严重的问题,因为它直接造成了经营损失。
所以四个改动:中文做编码转换后再转字节、按长度上限分包并串行写入加包间间隔、加打印队列串行处理、逐单记录打印状态并支持补打、连接状态常驻可见。
这个模块最想说的一句话是:对接硬件的时候,「调通一个最简单的例子」和「功能可用」之间隔着所有的边界条件。我打通「一行英文」花了半天,处理中文编码、分包、并发、失败反馈花了两周。而这四个问题在最简单的例子里一个都不会出现——这也是为什么硬件对接类的活看起来简单实际不简单。
内置精简码表而不是全量码表,也不是走服务端转换。全量码表体积很大,会明显拖慢小程序启动;走服务端转换(后端返回已编码的字节)能省本地体积,但引入了网络依赖——而打印恰恰是断网时也该能用的功能(订单已经在本地了,小票该能打出来)。精简码表的代价是可能遇到码表外的字符,所以要做降级(替换为占位符)并记录下来用于后续补充。判据是「这个功能能不能依赖网络」——打印是本地操作,不该被网络阻塞。
分包串行 + 包间间隔,代价是打印速度变慢。并行写入理论上更快,但设备端收到的顺序不可控,内容会错乱——这不是性能问题而是正确性问题。包间间隔的取舍更微妙:间隔越长越稳定但越慢。我的做法是在真实设备上试出一个「不丢包的最小间隔」,并且做成可配置——因为不同型号的处理速度不同,写死一个值会在某些设备上失效。这类「和硬件打交道的参数」我认为都应该可配置,因为你不可能测遍所有型号。
失败时中止整张而不是从中间续写。续写能省掉重复打印的纸,但会产生半张残票——后厨拿到残票可能少做菜品,那个损失比一张纸大得多。判据是「不完整的输出会不会被误用」——小票会被后厨当作生产指令,所以宁可重打整张。
踩过的坑一:一次性写入几百字节,打出来前面正常后面乱掉。我最初完全不知道有单次写入长度上限这回事,因为我测试用的「打印一行英文」根本没超过上限。这个坑让我明白:硬件对接里「最简单的例子能跑」几乎不提供任何保证——所有的边界条件(长度、编码、并发、时序)在最简单的例子里都不会出现。后来我做硬件相关的事,第一件事就是去查协议的各种限制,而不是先跑通一个例子。
踩过的坑二:包间没有间隔,出现随机缺字。这个问题最难查,因为它是概率性的——同一张小票有时候正常有时候缺字。我一开始怀疑是编码问题、怀疑是内容里有特殊字符,查了很久。最后是在加了日志逐包记录之后,发现失败的位置是随机的,才想到是时序问题。教训是:概率性的问题通常和时序或并发有关,而不是和内容有关——这个判断能省很多时间。
踩过的坑三:打印失败静默处理,后厨漏做单,顾客等了半小时。这是最严重的一个,因为它造成了真实的经营损失和顾客投诉。根因是我把打印当成了一个「尽力而为」的操作,失败了就算了。但对商家来说小票是生产指令,打不出来就等于这个订单不存在。修法是逐单记录状态、显著提示未打印订单、支持补打。教训是:判断一个操作能不能「失败了就算了」,要看它的下游有没有人依赖它的输出——有人依赖,就必须有失败反馈和补救手段。
没做的部分:没做打印机状态的主动读取(缺纸、过热)。部分型号支持回读状态,但协议差异大、不是所有设备都支持,做成通用能力的成本很高。我的替代方案是「写入成功但商家反馈没出票」时提示他检查纸张——不完美但覆盖了主要场景。这个取舍我会诚实说明是能力限制而不是不需要。
分包相关的数字要报清三件事:单包上限、一张小票的字节数与包数、包间间隔。比如「一张标准小票约几百字节,按上限切成十几包,包间间隔若干毫秒」。这几个数字一报,面试官就知道你真的处理过这个问题。
成功率必须在真实设备上连续打印测:报「连续打印 N 张,成功 M 张、失败原因分布」。要说明测了几种型号——不同型号的表现差别很大,只测一台不能说明问题。
包间间隔的取值要说清是怎么定的:「从较小值开始逐步增加,找到不再丢包的最小值,并留一定余量」。直接说一个数字而不说怎么来的,就是拍的。
并发场景的验证:同时触发多个打印任务,断言每张小票内容完整且不互相混杂。这条要真的并发触发,不是顺序调用。
编码正确性:准备一批包含常用汉字、标点、数字混排的小票内容,实际打印并逐字核对。这一步只能人工核对,要诚实说明;同时记录码表未覆盖的字符出现频率。
排版正确性:用不同纸张宽度和中英文混排内容,断言分割线对齐、不折行错位。
失败与补打:拔掉打印机电源制造失败,断言该单被标记为失败且界面出现显著提示;恢复后补打,断言只出一张票(幂等生效)。
不要报什么:不要报「打印成功率 100%」——硬件会缺纸、会断连,不可能百分百。该报的是「单包上限与实际分包数」「多型号下的连续打印成功率与失败原因分布」「包间间隔的确定方法」「并发打印内容不混杂」「失败可补打且幂等」这几件可核对的事。
接单工作台第一版做得很直接:拉订单列表、新订单来了播一次提示音、每单下面「接单」「出餐」两个按钮。单人测试完全正常,放到真实门店里全是问题,四类。
一是提示只响一次,高峰期必漏单。新订单来了响一声,但那个时候店员正在打包、在收钱、在跟顾客说话,一声过去就没了。而系统认为「我提醒过了」。结果是订单在那躺着没人接,超时之后顾客投诉、平台判罚。
二是设备息屏或者小程序被切到后台,回来之后少了几单。我用定时轮询拉新订单,但小程序切到后台会被挂起,定时器不再执行;平板息屏也一样。回到前台后我的定时器接着跑,只拉「最新的」,中间那段时间的订单没有被补上——它们既没有被提醒,也没有出现在我以为的位置上。
三是店里两台设备同时操作,状态乱了。前台一台平板、后厨一台,两个人同时点了同一单的「接单」。我的实现里两个请求都返回成功(后端没做并发保护,前端也没有),状态被重复流转,还产生了两条操作记录。更糟的情况是一个点「接单」一个点「取消」,最终状态取决于哪个请求先到。
四是列表按下单时间排序,最急的单在最下面。一个已经超时的单、一个顾客催过的单,因为下单早所以排在后面(我用的是倒序)。店员从上往下处理,最急的最后才看到。
所以四个改动:未处理订单持续提醒并随等待时长升级、回到前台立即补偿拉取并比对本地状态、状态流转收敛为状态机并做并发保护、默认按超时与催单优先排序。
这个模块最想说的一句话是:「我提醒过了」不等于「他知道了」。系统的责任不是「发出一次通知」,而是「确保这件事被处理,没被处理就继续提醒」。这个转变听起来是产品思路,但它对应的技术实现完全不同——前者是一次性事件,后者需要维护「未处理集合」并持续检查。
if 判断在多设备并发下会产生不可枚举的状态组合。持续提醒的代价是「吵」,这是必须接受的。产品最初担心商家嫌吵,但漏单的代价(顾客投诉、平台判罚、门店评分下降)远大于吵。我的处理是让吵变得可控而不是取消它:间隔随等待时长递进(刚来的不急、久了才急)、强度分级、并给一个显式的静音开关且记录静音时长。关键判断是「如果不给显式的静音开关,他会用不可控的方式绕过」——直接关掉设备声音,那时候我们就完全失去提醒能力了。给一个可控的出口,比堵死所有出口更安全。
长连接与轮询都做,不是二选一。只做长连接的问题不是「实时性不够」而是「断开了之后不自知」——它会安静地失效。只做轮询的问题是实时性差且请求浪费。两者叠加的代价是实现复杂一些、要处理重复推送(靠订单标识去重)。判据是「漏一单的代价」——在这个场景下漏单代价很高,所以值得付双份的实现成本。
并发保护选「带前置状态由服务端校验」而不是「前端锁」。前端锁只能防同一台设备的重复点击,防不住两台设备。带前置状态的代价是每次操作要传当前状态、被拒绝时要处理最新状态的回显。判据很简单:真实场景里就是多台设备(前台一台、后厨一台),所以并发保护必须在服务端。
踩过的坑一:提示只响一次,高峰期漏单。这是最直接造成业务损失的问题。我的思维误区是把提醒当成了「事件通知」——发出去就完成了我的责任。但对商家来说系统的责任是「确保这件事被处理」。修法是从「新订单事件」改成「维护未处理订单集合并持续提醒」。这个转变听起来是产品思路,但它对应的技术实现完全不同:前者是一次性的回调,后者要维护集合状态、要有定时检查、要处理集合的增删。我认为这是这个模块最有价值的一点。
踩过的坑二:小程序切后台被挂起,回来之后少了几单。我的轮询定时器在挂起期间不执行,而恢复后我只拉「最新 N 条」,中间那段的订单被跳过了——它们既没被提醒也没出现在列表该有的位置。教训有两层:一是要知道平台会挂起你的代码(这是限制不是 bug,不能靠「让定时器继续跑」来解决);二是增量同步必须带位置游标,「拉最新 N 条」在有间断的场景下一定会丢数据。
踩过的坑三:两台设备同时接单,两个请求都成功。状态被重复流转,还产生了两条操作记录。更糟的是一个点接单一个点取消时,最终状态取决于哪个请求先到——完全不可控。修法是操作带前置状态由服务端校验。教训是:只要用户可能在多个设备或多个标签页上操作,前端的任何互斥都是无效的,必须在服务端做。
没做的部分:没做离线接单(断网时先在本地接单,恢复后同步)。它能覆盖门店断网的场景,但风险很高——本地接单后如果订单已被顾客取消或被系统超时关闭,同步时会冲突,而这个冲突的处理涉及资金和履约,不该由客户端决定。我的替代方案是断网时明确提示「网络异常,请勿依据当前列表出餐」,把风险显式暴露给商家而不是假装能用。
漏单率是这个模块的核心指标,定义要说清:我们用「超时未接单的订单数占比」作为代理指标。要说明这个指标受门店忙碌程度影响,所以要看同一批门店改造前后的对比,而不是不同门店之间比。
提醒有效性用「从订单到达到被接单的中位时长」衡量。持续提醒之后这个时长应该下降。用中位数,因为少数极端值会让平均值失真。
挂起恢复的验证是断言型用例:切后台一段时间、期间注入若干新订单、切回前台,断言这些订单全部出现在列表中且触发了提醒。这条要真的切后台测,在开发工具里模拟不一定准,要在真机上做。
并发保护:用两个客户端同时对同一订单发起接单,断言只有一个成功、另一个收到明确的「已被其他设备处理」提示且界面刷新到最新状态。
长连接断开的兜底验证:人为断开长连接,断言轮询仍能拉到新订单并提醒。这条最容易漏测,因为正常情况下长连接是好的。
本地先响应的失败回滚:让接单请求失败,断言界面状态回滚到「待接单」并给出提示,而不是停留在「已接单」。假成功比失败更危险。
排序正确性:构造包含超时单、催单、预约单、普通单的数据,断言排序符合优先级规则、同级内按时间正序。
不要报什么:不要报「漏单率降为零」——门店可能整个店都在忙,系统提醒再多也没人看。该报的是「同批门店超时未接单占比的前后对比」「订单到接单的中位时长下降」「切后台期间的订单全部补齐并提醒」「并发操作只有一个生效且另一个有明确提示」这几件可核对的事。
这一块最初的实现只有两个开关:一个「营业中 / 休息中」,每个菜品一个「售罄」勾选。逻辑上没有任何复杂度,问题全出在它和门店真实经营的错配上,四类。
一是打烊之后在途订单没人管。老板到点直接点「打烊」,但还有几单已经付款、还没做完。门店状态变成休息后,顾客在 App 上查到「门店已休息」,以为自己的单不做了,打电话来问;而店里也可能真的忘了这几单(列表被打烊状态影响了展示)。
二是只能标「售罄」,表达不了「还剩几份」。这是老板反复提的需求:某道菜还剩三份,他想让这三份卖完自动售罄,而不是靠自己盯着。用只有布尔值的开关,他的实际做法是估摸着提前手动点售罄——那三份就卖不出去了,是实实在在的损失。
三是店里多台设备状态不一致。前台平板上标了某道菜售罄,后厨那台设备上还显示可售(因为它上次拉数据是十分钟前)。更严重的是顾客端也有延迟,顾客点了已经售罄的菜,做不出来只能退单。
四是估清第二天要手动一个个恢复。昨天售罄的十几道菜,今天开店要逐个取消售罄。老板经常忘,结果是当天这些菜一直卖不出去,他到中午才发现。
所以四个改动:打烊前检查并显式列出在途订单、估清支持数量模式且归零自动转售罄、状态改为长连接实时同步加前台恢复补偿、估清次日自动恢复并保留手动批量入口。另外补了多设备同时修改的版本校验。
这个模块最想说的一句话是:一个开关能不能表达用户的真实意图,决定了这个功能是帮他还是逼他做变通。只有「售罄」这个布尔开关的时候,老板的应对是提前手动点售罄——这是明确的经营损失,而且我们从数据上看不出来(只看到「他点了售罄」,看不到「他其实还有三份没卖」)。用户的变通行为往往是需求没被满足的信号,而它不会出现在工单里。
数量扣减放在服务端而不是客户端。客户端扣减实现简单、响应快,但多个顾客同时下单时必然超卖——而超卖的后果是退单和客诉,代价远大于那点响应速度。判据是「这个计算的结果会不会被并发影响」:会的话就必须在有唯一裁决者的地方算。客户端可以做乐观显示,但真实扣减由服务端定。
多设备冲突选「版本校验 + 提示最新值」而不是「后写覆盖」。后写覆盖实现最简单,但会出现「后厨把数量改成 2,前台用十分钟前的旧数据点了个操作,数量被改回 5」这种情况——而没人知道发生了什么。版本校验的代价是要处理冲突提示、用户要重新操作一次。判据是「静默覆盖的后果是否可察觉」——这里不可察觉(数量默默变回旧值,直到超卖才暴露),所以必须显式拒绝。
估清默认次日自动恢复,而不是保持售罄直到手动取消。「保持」更保守(不会误恢复),但实际情况是老板经常忘记恢复,整天卖不出去——而这个损失他自己也感知不到(他不知道那道菜今天一份都没卖)。自动恢复的风险是「本来就该长期下架的菜被恢复了」,所以配了「长期售罄」标记作为例外。判据是「哪种默认行为的错误代价更小、更容易被发现」:误恢复了他会看到菜品在售、可以立刻再标;忘记恢复则完全无感知。
踩过的坑一:打烊没有检查在途订单。顾客已付款、订单还没做完,他在 App 上查到「门店已休息」,以为单不做了,打电话来问;客服也不知道怎么解释。教训是:任何「切断服务」的操作,都要先检查有没有正在进行中的事情——这和「删除数据前要检查引用」是同一类思维。我当时只想到了「打烊 = 改一个状态」,没想到这个状态会被顾客解读成「我的订单不做了」。
踩过的坑二:只做了布尔的售罄开关,老板的应对是提前手动点售罄。而这个变通行为在数据上完全看不出来——我们只看到「他点了售罄」,看不到「他其实还有三份没卖」。是老板在一次沟通里说的:「我怕卖超了不好意思跟客人说,所以剩几份的时候我就直接点售罄了」。教训是:用户的变通行为往往是需求没被满足的信号,而它不会出现在工单里——因为他不认为这是系统的问题,他认为这是他自己的应对办法。所以要主动去问「你平时是怎么处理这种情况的」,而不是等他来提需求。
踩过的坑三:多设备状态不同步,顾客点到已售罄的菜。前台标了售罄,但后厨设备和顾客端都还是旧数据,顾客下单成功,做不出来只能退单。教训是:只要同一份数据会在多个终端上被查看和修改,「同步」就不是可选的优化而是功能的一部分。我最初把它当成了「刷新一下就好」的小问题,实际它直接造成了退单。
没做的部分:没做基于历史销量的备货与估清建议。它有价值(能提前提醒「这道菜通常这个点会卖完」),但需要销量预测能力,而且建议错了会误导备货。我做的是最基础的版本:统计各菜品的售罄时长与频率给老板看,让他自己判断——提供事实而不是提供结论,这在我这个层级是更合适的选择。
「因售罄导致的退单数」是这个模块最有说服力的指标。它直接对应「估清是否及时同步到顾客端」。报法是改造前后同批门店的对比,并说明统计口径(退单原因是怎么归类的)。
多设备同步延迟要实测并报清测法:在 A 设备标记售罄,记录 B 设备与顾客端反映变化的时间。要分两种情况报:长连接正常时(应在秒级内)、长连接断开走补偿拉取时(取决于恢复时机)。只报前者是不完整的。
数量模式的正确性要做并发测试:剩余数量设为 3,并发下 5 单,断言只有 3 单成功、其余提示售罄,且最终数量为零并自动转售罄。这条必须真并发,顺序下单测不出超卖。
版本冲突:两个客户端各持旧版本同时修改,断言一个成功、另一个被拒绝并收到最新值。
打烊检查:构造存在在途订单的场景,断言打烊被拦下并列出了这些订单;处理完之后可正常打烊。
次日自动恢复:断言普通售罄被恢复、标记为「长期售罄」的没有被恢复。第二条是例外路径,最容易漏测。
本地先响应的回滚:让标记售罄的请求失败,断言界面回滚为「在售」并提示,而不是停留在「已售罄」——假成功会让他以为标了。
不要报什么:不要报「估清准确率」——这取决于老板自己标得准不准,不是系统能力。该报的是「因售罄导致的退单数前后对比」「长连接正常与断开两种情况下的同步延迟」「并发下不超卖」「长期售罄不被自动恢复」这几件可核对的事。
没有匹配的内容,换个关键词试试。
项目拆解 · 门店经营端(uni-app · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据