这个模块的前提和别的移动端页面完全不同:它的用户很可能没有网络,而且这不是偶发情况,是常态。
境外出行的网络处境很具体:下了飞机漫游没开通、当地电话卡还没买到、酒店 Wi-Fi 需要先在浏览器里登录(而登录页在没网时打不开)、地铁和景区里信号差。而用户在这些时刻恰恰最需要看行程——他要知道酒店地址怎么念给出租车司机、要出示入住凭证、要确认下一段航班的时间。
第一版把它当普通页面做:进页面请求接口、拿到数据渲染。无网时显示「网络异常,请检查网络」。这个体验在境外是灾难性的——用户站在酒店前台,打不开凭证,而他没有网络去打开它,只能求助前台的 Wi-Fi 或者用别人的手机热点。我们收到的反馈里这一类的情绪最强。
所以这一页的设计原则是「假设网络不可用」,而不是「网络不可用时降级」——顺序反了,实现方式就完全不同。
第一是主动预缓存,而不是按需缓存。按需缓存(用户看过就缓存)的问题是用户往往在需要的时候才第一次看——他到了酒店才点开凭证,那时已经没网了。所以要临近出行时主动把关键数据下载下来:行程详情、每个项目的地址与联系电话、凭证图片与二维码内容、退改政策。预缓存的时机是「出行前还有网络的时候」,比如订单确认后、出行前一天。
第二是渲染一律本地优先。不是「先请求,失败了读缓存」,而是「先读缓存渲染,同时后台请求刷新」。这两种顺序的差别在弱网下极其明显:前者要等请求超时才显示内容(可能十几秒),后者立刻有内容。
第三是新鲜度必须显式标注。本地优先带来一个新问题:用户看到的可能是旧数据。而行程数据是会变的(航班改时间、酒店换房型)。所以要明确显示「更新于 X」以及「当前为离线展示」——让用户知道这份数据的时效性,而不是让他以为看到的一定是最新的。这一条和值班大屏「数据陈旧要显式提示」是同一条原则。
第四是缓存分级。端上存储有限,不能什么都长期存。凭证与地址优先级最高、长期保留;行程详情次之;推荐内容、图片轮播这类可随时淘汰。分级的依据是「无网时用户最需要什么」。
第五是离线态下要区分「可用」与「需联网」的功能,而不是统一报错。查看凭证、看地址、看行程可以离线;而修改订单、联系客服、实时航班动态必须联网。要在界面上明确区分,不要让用户去点一个注定失败的按钮。
「假设网络不可用」这个原则的顺序很关键,它不是措辞差异。如果原则是「网络不可用时降级」,实现上就是在正常请求流程上加一层 catch,失败时读缓存——而这条路径很少被测试、缓存内容也不完整(因为是按需缓存的副产物)。如果原则是「假设网络不可用」,实现上就是缓存是主数据源、网络是刷新手段,离线路径就是主路径,天天在跑,不可能出问题。取舍是要主动做预缓存(消耗流量与存储),而且要维护缓存的完整性与新鲜度。判断依据是「无网的发生概率」——普通电商 App 里无网是偶发,加个兜底就够;境外出行场景里无网是常态,必须当主路径设计。
本地优先渲染的代价是「可能展示旧数据」,所以新鲜度标注是它的必要配套。不标注的话,用户看到一个航班时间就以为是最新的,而实际航班可能已经改时间了——这在出行场景里的后果是误机。所以「本地优先」和「新鲜度显式标注」必须一起做,只做前者是危险的。这和值班大屏「宁可显示『数据已停止更新 3 分钟』,也不要显示一个看起来正常的旧数字」是完全同一条判断——只是这里的后果更严重。
缓存分级按「无网时用户最需要什么」而不是按数据量。直觉上会优先缓存小的、淘汰大的。但凭证图片可能不小,而它恰恰是最不能丢的——用户在前台出示不了凭证,其他所有缓存都没有意义。所以分级的依据必须是业务重要性,存储不够就淘汰推荐内容和轮播图这些「有网时再拉也行」的东西。
踩过的坑:用户在酒店前台打不开凭证。第一版无网时显示「网络异常,请检查网络」,而用户就是因为没网络才打不开——这个提示对他毫无帮助,甚至是一种嘲讽。他只能求助前台的 Wi-Fi 或者借别人的热点。我们收到的反馈里这一类情绪最强。修法是预缓存加本地优先。教训是:错误提示要考虑「用户是否有能力按提示行动」——「请检查网络」在一个压根没有网络可用的场景里是无效提示,正确的做法是让功能在那个场景下压根不需要网络。
踩过的坑二:按需缓存等于没缓存。我们最初做了缓存,但是「用户看过的才缓存」;而用户的行为恰恰是「到了地方才第一次点开凭证」,那时已经没网,缓存里什么都没有。修法是改为主动预缓存,在出行前还有网络时下载好。教训是:缓存策略要匹配用户的真实访问模式——按需缓存假设「用户会先在有网时访问一遍」,而这个假设在出行场景里不成立。
踩过的坑三:本地优先上线后,用户看到了过期的航班时间。航班改了时间,用户在无网状态下看到的还是旧时间,而界面上没有任何提示,他按旧时间去机场。虽然最终没有造成误机(航司也发了短信),但这是很严重的隐患。修法是显式标注更新时间与离线状态,并且对「时间敏感」的项目额外提示「请确认最新时间」。教训是:本地优先必须配新鲜度标注,两者是一个整体,只做前者是把风险转移给了用户。
踩过的坑四:预缓存不管网络类型,在漫游下消耗了用户的流量。预热逻辑一开始没判断网络类型,有用户在境外漫游状态下被下载了一批凭证图片,流量费很贵,来投诉。修法是预热优先在 Wi-Fi 下执行,蜂窝网络下只拉必要的文本数据、图片延后。教训是:主动下载类行为必须判断网络类型与计费环境——尤其在国际业务里,「有网络」不等于「可以随便用网络」。
没做的部分:没做完整的离线地图(无网时也能导航到酒店)。需求很真实但离线地图包体积大、授权成本高,当时的做法是缓存地址文本与当地语言的地址(能给出租车司机看)以及一张静态地图截图。也没做行程的多人共享(同行人共享一份行程),需要账号关系与权限设计,排期没排上。
离线可用性:确定性验证,而且是这个模块最该报的——断开全部网络,检查行程详情、地址与电话、凭证与二维码是否都能正常查看。报的是「覆盖了哪些内容」这个清单,比任何比率有说服力。
预缓存命中率:报用户首次打开行程详情时命中预缓存的比例。这个数字直接证明「主动预缓存」比「按需缓存」有效——按需缓存在这个指标上应该接近零(因为用户就是第一次看)。
首屏时间:报弱网条件下的首屏时间,改造前后对比。关键是要说明改造前是「等请求超时才显示内容」,这个归因让数字有意义。
「打不开凭证」类反馈:报相关工单或反馈的数量,改造前后对比。这是这个模块最直接的业务指标,而且工单数外部可核查。
新鲜度标注的有效性:报因看到旧数据而产生的问题数(比如按旧航班时间行动)。这个数字很难直接测,诚实的表述是「加标注后未再出现相关反馈,但我们无法证明用户都注意到了标注」——承认度量的局限比编一个数字好。
缓存占用与淘汰:报行程缓存的存储占用与各级别的占比,以及淘汰触发次数。说明凭证与地址从未被淘汰过。
预热的流量消耗:要主动报——Wi-Fi 与蜂窝下的预热数据量,说明蜂窝下只拉文本、图片延后。这是踩过投诉之后的改动,主动交代比等人问好。
不要报「100% 离线可用」。实时航班动态、修改订单这些功能本质上需要联网,我们的设计也是明确区分的。正确表述是「哪些内容离线可用(给清单)、预缓存命中率、弱网首屏时间的变化、新鲜度标注与其度量局限、预热的网络类型策略」。
跨时区是旅游业务里最容易出错、后果又最严重的一类问题:算错时间就是误机。
而它的第一个陷阱是语言上的歧义:「明天早上 8 点的航班」——是出发地的 8 点还是目的地的 8 点?对用户来说这句话是模糊的,而如果界面上只写「08:00」,用户会用自己所在地的时间去理解它。
第一版的实现犯了一个基础错误:时间以本地时间字符串存储与展示(比如「2024-06-01 08:00」)。这带来两个问题。
一是随设备时区漂移。用户在出发地看是 8 点,飞到目的地之后设备时区变了,同一条数据渲染出来变成了另一个时间——而航班时间压根没变。用户会以为航班改时间了。
二是提醒错时。我们的提醒是按「本地时间到点」触发的,用户跨时区后,提醒在错误的时刻响了——可能提前几小时,也可能已经过了。
所以第一个设计是时间一律以绝对时刻加时区标识存储。绝对时刻保证唯一性(不管设备在哪个时区,它指的都是同一个瞬间);时区标识用于正确展示(要按事件发生地的时区渲染,因为「航班 8 点起飞」指的是出发机场当地的 8 点)。
第二个设计是展示时的双标注。光按当地时区渲染还不够——用户在飞机上、在中转机场,他的「当前时间感」和事件发生地可能不同。所以在跨时区场景下同时标注两个时间:「当地 08:00(你所在地 14:00)」。这一条直接消掉了歧义,而它只是一个展示层的改动。
第三个是提醒必须基于绝对时刻调度。不能用「本地时间到点」触发,因为设备时区会变。用绝对时刻调度,无论用户在哪,提醒都在正确的瞬间响。
第四个是通知权限的兜底。这是很容易被忽略但影响很大的一环:用户没开通知权限,我们的所有出行提醒都是静默失效的——而我们以为已经提醒过他了。而出行提醒的失效后果可能是误机。
所以要做两件事:在应用内也做醒目提醒(用户打开应用时明确展示「2 小时后的航班」);在关键行程前主动引导开启通知权限,并说明理由(「开启后我们会在登机前提醒你」)。
第五个是地理围栏提醒(比如临近酒店时提示可以办理入住)。这类提醒体验很好,但地理围栏是持续耗电的。所以要按行程阶段动态开关:只在接近相关时间窗口时才注册围栏,过了就注销;同时限制同时注册的围栏数量(各端都有上限)。
时间存「绝对时刻 + 时区标识」而不是只存绝对时刻。只存绝对时刻(时间戳)看起来更干净,但展示时不知道该按哪个时区渲染——按用户当前时区渲染是错的(航班时间指的是机场当地时间),而「事件发生地的时区」这个信息只能显式存下来。代价是数据结构复杂一点、而且要保证时区标识的准确性(机场所在城市的时区要有可靠的数据源)。这个决策的一般形式是:时间的语义不只是「哪一瞬间」,还包括「按谁的时钟表达」,两者都要存。
双标注是这个模块性价比最高的改动。它不改任何存储和调度逻辑,只是在展示层多显示一个时间,但它直接消掉了「8 点是谁的 8 点」这个歧义。取舍是界面上信息密度增加,缓解手段是只在跨时区场景才双标注(用户所在地与事件发生地时区不同时才显示第二个),同城的行程不加这个噪音。我认为这类「改展示而不改逻辑」的解法值得主动去找——很多歧义类的问题不需要复杂的技术方案。
通知权限缺失要做应用内兜底,而不是只在设置里提示。「去设置里开启通知」这类提示用户经常忽略。而出行提醒的失效后果是误机,不能接受静默失效。所以应用内也做醒目提醒——用户打开应用时就能看到「2 小时后的航班」。代价是它只在用户主动打开应用时才生效,覆盖不了「完全没打开」的情况,所以引导开启通知权限仍然要做,两者是叠加而不是替代。判断依据是「这个提醒失效的后果有多严重」——后果严重的提醒必须有多条触达路径。
踩过的坑:以本地时间字符串存储,用户跨时区后看到的时间变了。用户在出发地看是 8 点,飞到目的地后同一条数据渲染成了另一个时间,而航班时间压根没变;有用户来问「你们是不是改了我的航班时间」。修法是改为绝对时刻加时区标识存储。教训是:任何跨时区场景下的时间都不能用本地时间字符串表达——它的含义依赖于「读它的人在哪个时区」,而这个上下文是不稳定的。这条我在本地生活的营业时段判定上也遇到过(「早餐 6:00-10:00」是商家当地还是服务器时区),是同一类问题的不同形态。
踩过的坑二:提醒按本地时间调度,跨时区后在错误的时刻响。用户飞到目的地后,登机提醒提前了几小时响,或者已经过了才响。修法是基于绝对时刻调度。教训是:调度类逻辑必须用绝对时间,展示类逻辑才用本地时间——把两者混用是最常见的时区 bug 来源。
踩过的坑三:通知权限没开,所有出行提醒静默失效。我们的提醒系统在后台正常运行、日志也显示「已发送」,但用户压根没收到——因为他没开通知权限。而我们以为已经提醒过他了,这是最危险的部分。修法是应用内兜底提醒加主动引导开启权限。教训是:依赖系统能力的功能必须检查该能力是否可用,并且「不可用」要被当成一种需要处理的状态而不是忽略——权限、后台运行、网络,都属于这类。
踩过的坑四:地理围栏注册数超上限,后面的围栏静默注册失败。我们给行程里的每个项目都注册了围栏,一个多日行程有十几个项目,超过了平台上限,后面的注册失败而且没有报错,表现是「有些地点的提醒从来不响」。修法是按行程阶段动态注册(只注册接近时间窗口的)并限制数量。教训是:有配额上限的系统能力,注册失败往往是静默的,必须主动检查注册结果——这和「storage 写超限静默失败」是同一类问题。
没做的部分:没做航班实时动态的主动推送(延误、登机口变更)。这需要接航司数据源并处理推送时效,属于服务端能力,端上只做了展示。也没做基于用户位置的智能提醒时间调整(离机场远就早点提醒),需要路况数据,当时提醒时间是固定可配的。
时区正确性:确定性验证,而且要模拟设备时区变更——在出发地时区看行程、切换设备时区到目的地、检查事件时间不变、双标注正确更新。这类用例必须常驻,因为它在单一时区的开发环境里测不出来。
夏令时:确定性验证——构造夏令时切换日附近的行程,检查时间计算正确。这一项要专门做,而且要说明用的是标准时区数据而不是自己算偏移。
提醒的准时性:报提醒实际触发时刻与预期时刻的偏差,并专门验证跨时区场景(设备时区变更后提醒仍在正确瞬间触发)。
通知权限的覆盖:报通知权限开启率,以及引导开启后的转化率。更重要的是报「未开启权限的用户中,通过应用内提醒看到行程提示的比例」——这个数字说明兜底路径的实际覆盖。
时间歧义类反馈:报「航班时间是不是改了」这类咨询的数量,改造前后对比。这是双标注最直接的业务收益,而且工单数可核查。
地理围栏:报围栏注册成功率(改造前超上限后静默失败)与同时注册的围栏数量上限。同时要报耗电对比——动态开关前后挂机一段时间的电量差,并说明机型。
提醒的实际效果:如果能拿到,报提醒触达后用户打开应用的比例。这说明提醒是有效的而不是被忽略的。
不要报「提醒 100% 准时到达」。系统通知的到达受系统策略、省电模式、权限影响,端上无法保证。正确表述是「时区正确性与夏令时由常驻用例保证、提醒基于绝对时刻调度、通知权限缺失有应用内兜底及其覆盖比例、围栏注册成功率与耗电对比」。
凭证出示这一刻,是整条链路上失败代价最高的时刻:用户站在酒店前台或者景区闸机前,身后可能有人排队,工作人员在等他。这时候任何一点不顺畅都会被放大成很强的负面体验。
而第一版在这一刻的表现很差,问题有四层。
第一是凭证是服务端生成的图片,需要联网下载。没网就打不开——而用户恰恰经常在没网的时候需要它(前面模块讲过这个处境)。
第二是屏幕亮度。用户为了省电把亮度调得很低,或者在户外强光下,扫码设备读不出来。工作人员会让他「调亮一点」,用户手忙脚乱地去拉控制中心——这个体验在排队的时候很难受。而且如果屏幕在这期间自动息屏,还要重新解锁重新打开。
第三是动态码在无网时完全不可用。有些凭证是动态码(定期刷新以防截图盗用)。第一版的实现是「刷新失败就不显示」,于是无网时用户看到一个空白或者一个「加载失败」——而这时他最需要的是能出示任何一种有效凭证。
第四是扫码失败时没有退路。扫码设备故障、码磨损、系统对不上,这些都会发生。而工作人员的常规做法是改为手工核验——查订单号、核对姓名。但第一版的出示页只有一个大二维码,订单号要退回上一页去找,用户在压力下翻页面找信息,体验极差。
所以四个设计,全部围绕「这一刻不能失败」。
第一是保存码内容而不是码图片,端上离线渲染。二维码的本质是一串内容加一套编码规则——只要把内容存下来,端上就能自己画出来,压根不需要网络。这个改动是这个模块最关键的一步,它把凭证从「需要网络的资源」变成了「本地可生成的东西」。
第二是进入出示态自动提升屏幕亮度并保持常亮,退出后恢复。「退出后恢复」很重要——不能把用户的亮度永久改掉,那是对用户设置的侵犯。
第三是动态码在无网时降级为长效静态码并明确标注。产品上要接受这个降级(安全性略低但可用),并且标注出来让工作人员知道。核心判断是:能出示一个安全性稍低的有效凭证,远好于出示不了任何东西。
第四是一屏聚合必要信息加备用路径。出示页上除了码,还要有姓名、订单号、联系电话、地址——这些正是扫码失败时工作人员会问的。另外提供复制订单号和离线可见的客服电话(客服电话必须离线可见,因为需要它的时候通常就是出问题的时候)。
保存码内容而不是码图片,这是一个「换视角就解决了」的改动。把凭证当成「服务端生成的图片资源」,就必然需要下载、需要网络、需要缓存图片文件;而把它当成「一串内容 + 端上的渲染能力」,网络依赖就整个消失了。代价是端上要引入二维码渲染能力(包体积增加一点),而且要保证渲染出的码与服务端一致(编码参数、纠错级别要对齐)。取舍毫无疑问值得——包体积的一点增加换来了「凭证永远可用」。我认为这类改动值得主动去找:当某个功能有难以消除的依赖时,先问「这个依赖是本质的还是实现方式带来的」。
动态码在无网时降级为静态码,这是安全性与可用性的正面取舍。动态码的目的是防截图盗用,降级为长效静态码确实降低了安全性。但反过来看:不降级的结果是用户压根出示不了凭证,而他是真实的、已付款的客人。核心判断是「能出示一个安全性稍低的有效凭证,远好于出示不了任何东西」。缓解手段是明确标注为备用凭证(让工作人员知道可以额外核对身份)、静态码的有效期仍然有上限、并且记录使用情况便于事后审计。这个取舍要和业务方一起做,因为它涉及风险承担。
一屏聚合必要信息,依据是「观察工作人员在扫码失败时做什么」。这个设计不是从技术出发的,而是从「扫码失败之后现场会发生什么」倒推的:工作人员会问姓名、要订单号、可能要打电话核实。把这些信息和码放在同一屏,等于把「备用流程」也准备好了。取舍是出示页信息更多、要注意不能挤占码的面积——码是主体,聚合信息是备用,视觉权重要分清。
踩过的坑:凭证是需要下载的图片,用户在前台没网打不开。这是我们收到的情绪最强的一类反馈——用户站在前台,屏幕上转圈,身后有人排队。修法是改为保存码内容并在端上离线渲染。教训是:面对「某个功能强依赖网络」的问题,先问这个依赖是本质的还是实现方式带来的——这里的网络依赖完全是「用图片承载码」这个实现方式带来的,换成内容加渲染就消失了。
踩过的坑二:没处理屏幕亮度,扫码经常读不出来。用户为省电把亮度调到很低,或者在户外强光下屏幕反光,扫码设备读不出来;工作人员让他调亮,他手忙脚乱去拉控制中心,而屏幕还可能自动息屏。修法是进入出示态自动提升亮度并保持常亮,退出后恢复。教训是:涉及硬件交互的界面(扫码、拍照、NFC)要主动控制设备状态,不能假设用户的设备处于合适的状态——用户没有义务为了用你的功能先去改系统设置。
踩过的坑三:动态码刷新失败就不显示,无网时凭证完全不可用。用户看到一个「加载失败」,而他手里明明有一张已付款的有效订单。修法是降级为长效静态码并标注。教训是:安全机制的失败不应该导致功能完全不可用——要设计一条「安全性降低但仍可用」的路径,否则安全机制会在最需要功能的时候把功能关掉。
踩过的坑四:扫码失败后用户要退回上一页找订单号。扫码设备故障时工作人员改为手工核验,而出示页上只有一个大二维码,订单号在别的页面;用户在压力下翻页面找信息,而这时他往往还在纠结「是不是我的手机有问题」。修法是一屏聚合姓名、订单号、电话、地址并提供复制。教训是:关键流程的界面设计要考虑「这一步失败之后现场会发生什么」,并把备用流程需要的东西也准备好——只设计成功路径的界面,在失败时会把用户推入慌乱。
没做的部分:没做离线的凭证有效性自校验(端上验证签名以防伪造凭证)。技术上可行但需要端上持有验证密钥,密钥泄露的风险大于收益,最后依赖工作人员的扫码系统去校验。也没做把凭证加入系统卡包(钱包类应用),各端支持差异大而且需要额外的资质。
离线出示:确定性验证,这个模块最该报的——断开全部网络,检查凭证码能正常渲染并可被扫码设备识别。「可被识别」这一步必须真的用扫码设备测,只检查「码显示出来了」是不够的(端上渲染的编码参数可能和服务端不一致)。
码的一致性:报端上渲染的码与服务端生成的码在扫码设备上的识别一致性。这是「改为端上渲染」带来的新风险,必须验——编码参数、纠错级别要对齐。
扫码成功率:报凭证被扫码设备一次识别成功的比例,改造前后对比(亮度控制上线前后)。这个数据要靠线下实测或客诉数据推断,端上拿不到扫码设备的结果,这个局限要诚实说明。
「打不开凭证」类反馈:报相关工单数量,改造前后对比。这是这个模块最直接的业务指标。
亮度恢复:确定性验证——退出出示态后检查屏幕亮度恢复为用户原有设置。这一项容易漏,而漏了就是对用户设置的侵犯。
动态码降级:确定性验证——无网状态下打开动态码凭证,检查降级为静态码、可正常显示、且有明确标注。三项都要验。
备用路径的使用:报「复制订单号」的点击次数。这个数字有双重意义:证明备用路径在被真实使用;如果它的使用率很高,说明扫码失败的频率不低,值得去查扫码链路本身。
不要报「凭证 100% 可出示」。设备没电、应用被卸载、本地数据被清理,这些情况仍会导致出示失败。正确表述是「离线渲染与识别由实测用例保证、端上与服务端码的一致性验证方式、亮度控制与恢复、动态码的降级路径、备用核验信息的聚合与其使用数据」,并说明扫码成功率的度量局限(端上拿不到扫码设备的结果)。
没有匹配的内容,换个关键词试试。
项目拆解 · 行程助手与出行中(uni-app)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据