怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际旅游预订的移动端,uni-app 一套代码发小程序和 App。核心链路是选日期 → 搜列表 → 看详情 → 下单 → 等确认。用户跨时区、多币种,网络条件差异大。
为什么选这三个模块 旅游客户端的难点和电商客户端不一样:电商是「选商品加购物车」,旅游是「先选日期区间,日期变了整页数据全变」;电商价格固定,旅游价格随日期和供应商实时变动,还要处理"你看到的价和下单价不一致";电商下单即成功,旅游下单后要等供应商确认,客户端得处理这个中间态。这三个都能讲出独立的前端设计。

模块一:日期区间选择与联动

  1. 日期区间选择与联动(日历组件 + 区间校验 + 状态驱动重查)★★★
    简历这样写 日历区间选择与全局联动(uni-app + 自绘日历 + 区间态状态机 + 请求竞态控制):自绘日历支持区间选择、按日展示价格与售罄、跨月连续滚动,日期区间作为全局状态驱动列表与详情重查,切换日期时用请求序号丢弃过期响应避免旧数据覆盖新结果;日期一律按目的地当地日期处理,不做时区转换。切换日期后列表更新的可感知延迟约 350ms,快速连续改日期时的旧响应覆盖问题改造后未再出现,日历首次渲染(含三个月价格)约 120ms
    展开完整拆解
    为什么要这么设计

    旅游客户端和电商客户端最大的差异,是「日期」是一个贯穿全站的强状态。电商用户进来就能看商品,旅游用户不选日期什么都看不了——因为价格和是否有房都取决于日期。

    第一版把日期当成一个普通的筛选项,出了一串问题。

    一是快速改日期导致数据错乱。用户连续点了三次日期,发了三个请求,第一个请求最慢最后返回,把最新的结果覆盖了——界面上显示的是第一次选的日期的价格,而日历上高亮的是第三次的日期。用户看到的价格和日期对不上。

    二是日期状态散落在各页面。列表页有一份、详情页有一份、下单页又有一份,用户在详情页改了日期返回列表,列表还是旧日期。更糟的是下单时用的日期和用户最后看到的不一致。

    三是日历上不展示价格,用户要逐个试。用户想找便宜的日子,只能选一天看一次价格,来回切十几次。把每日价格直接画在日历上,是这个业务里体验提升最大的一处。

    四是时区把日期搞错了一天。客户端用本地时间构造日期对象,用户在东八区订东京(东九区)的酒店,跨零点时算出来的日期差一天。

    所以四个设计:请求竞态用序号控制日期提升为全局唯一状态日历上展示每日价格与售罄日期用纯日期字符串不用时间戳

    整体链路
    日期作为全局唯一状态(不允许各页面各存一份) │ ├─ 全局 store:checkIn / checkOut / 目的地 / 人数房间数 │ 日期用「YYYY-MM-DD」字符串,不用 Date 对象也不用时间戳 │ 原因:日期是日历概念,不带时区;用时间戳必然踩时区坑 │ ├─ 任何页面改日期 → 只改 store → 由 store 驱动各页重查 │ └─ 下单时从 store 取,保证与用户最后看到的一致 日历组件(自绘,因为要在格子里画价格) │ ├─ 数据:一次拉三个月的「每日价格 + 是否可售」 │ 拉太少用户翻月要等,拉太多首屏慢,三个月是折中 │ ├─ 渲染:日期数字 + 当日最低价 + 售罄置灰 │ 价格用小字号,售罄格子不可点 │ ├─ 区间选择状态机 │ 空 → 点第一次 = 选中入住日,进「待选离店」 │ 待选离店 → 点更晚的日期 = 完成区间 │ → 点更早的日期 = 重新作为入住日(不报错) │ 完成 → 再点 = 重新开始选 │ ├─ 区间校验(前端先拦,服务端仍要独立校验) │ 离店必须晚于入住 / 最多住 N 晚 / 最多提前 N 天 │ 区间内不能包含售罄日 → 包含则提示并高亮那一天 │ └─ 跨月:连续滚动而不是翻页 跨月区间要能正确高亮(选 10-30 到 11-02) 切换日期后的重查(竞态控制是关键) │ ├─ 每次发请求前 seq++,请求带上当前 seq │ ├─ 响应回来时比对 seq │ seq 等于当前值 → 采用 │ seq 小于当前值 → 丢弃(这是过期响应) │ ├─ 同时用 loading 骨架占位,避免闪烁 │ └─ 请求失败要区分「网络失败」和「无房」 无房是正常业务结果,要展示空态 + 推荐相邻日期 不能显示成错误页 时区处理(业务定义,不是技术选择) ├─ 日期一律是目的地当地日期,全链路不做时区转换 ├─ 界面上标注「酒店当地时间」 └─ 构造「今天」时用服务端下发的目的地当前日期,不用客户端本地时间
    分步拆解
    1. 日期用字符串不用时间戳,这是最重要的一条。YYYY-MM-DD 是纯日历概念,本身不带时区,传递和比较都不会出错。一旦用了 Date 对象或时间戳,任何一层做了时区转换就会错开一天,而且这类 bug 只在特定时区用户身上出现,几乎无法复现。
    2. 「今天」要用服务端下发的目的地日期。客户端本地时间不可信(可以改,也可能时区不对)。判断「不能选过去的日期」这类逻辑必须基于服务端给的目的地当前日期。
    3. 日期状态必须全局唯一。各页面各存一份必然不一致。做法是提升到全局 store,各页面只读不写自己的副本,改动统一走 store。下单时也从 store 取,保证和用户最后看到的一致。
    4. 请求竞态用自增序号丢弃过期响应。每次发请求前序号加一,响应回来比对——序号小于当前值就说明这是过期响应,直接丢弃。这比「取消上一个请求」更可靠,因为小程序端的请求取消支持不一致,而序号比对是纯逻辑,各端行为一致。
    5. 日历上画价格,一次拉三个月。这是体验收益最大的一步——用户能直接看出哪几天便宜。拉三个月是折中:拉一个月翻月要等,拉一年首屏慢。翻到边界时预取下一批。
    6. 区间选择状态机要处理「点了更早的日期」。用户选了入住日之后,如果点了一个更早的日期,正确行为是把它当作新的入住日重新开始,而不是报错「离店不能早于入住」。这是个小细节但对体验影响明显。
    7. 区间内包含售罄日要明确指出是哪一天。只提示「所选日期不可订」用户不知道问题在哪,要高亮那一天并给出提示,最好还能推荐相邻的可订区间。
    8. 跨月区间的高亮要正确。选 10 月 30 日到 11 月 2 日,两个月的格子都要正确显示区间状态。用连续滚动而不是翻页可以让用户直观看到跨月区间。
    9. 「无房」是业务结果不是错误。要展示空态并推荐替代方案(相邻日期、相似酒店),不能显示成网络错误页——用户会以为是应用坏了而反复重试。
    10. 前端校验之后服务端必须独立校验一遍。前端校验只为体验(即时反馈),接口会被直接调用,服务端的校验才是有效约束。
    关键决策与取舍

    为什么自绘日历而不用现成组件。现成的日历组件几乎都不支持「在格子里显示价格」和「按日售罄置灰」,而这两个正是旅游业务的核心需求。改造现成组件的成本超过自绘,而且现成组件的跨端表现不一致(小程序和 App 的渲染差异)。代价是要自己处理跨月、闰年、区间高亮、无障碍这些细节,工作量不小。判断依据是「核心需求是否被现成方案覆盖」——如果只需要选单个日期,绝对应该用现成的。

    一次拉三个月的价格数据有体积代价。90 天 × 每天一个价格和状态,压缩后不大,但如果一个酒店有十个房型就是 900 条。所以日历上只展示「当日最低价」,由服务端聚合后返回,客户端不做聚合。这既减少了数据量也避免了前端算错。

    踩过的坑:请求竞态导致价格和日期对不上。用户快速改日期,三个请求并发,最慢的那个最后返回覆盖了界面。用户看到的是 A 日期的价格、日历高亮的是 C 日期,然后按这个价格下单,结果实付金额不同,投诉。修法是序号比对丢弃过期响应。这个坑的通用教训是:任何「用户输入驱动的异步查询」都必须处理竞态,搜索联想、筛选、分页都是同一类问题。

    踩过的坑二:用 new Date('2025-10-01') 解析日期,在部分端上差一天。这个字符串被当成 UTC 零点解析,转成本地时间后在负时区就变成了 9 月 30 日。而不同端的 JS 引擎对无时区日期字符串的处理还不完全一致。修法是全程不把日期字符串转成 Date 对象——比较用字符串比较(YYYY-MM-DD 的字典序就是时间序),加减天数用专门的纯日期工具函数。这是日期处理里最经典的坑,能讲出来说明真踩过。

    没做的部分:没做「灵活日期」搜索(比如「10 月任意三天,帮我找最便宜的」)。这需要服务端做区间扫描,客户端要展示多个候选区间,交互设计复杂度高,当时没做。

    数字是怎么测的

    日历首次渲染耗时:在组件挂载前后打时间戳,量「数据到手 → 三个月格子渲染完成」的时间。约 120ms。要说明是多少个格子、是否含价格、测试机型——低端机上会明显更慢,只报好机型的数字不真实。

    切换日期后的可感知延迟:量从「用户点击确认日期」到「列表首屏内容更新完成」。约 350ms(含一次网络请求)。要区分「命中缓存」和「实时查询」两种情况分别报

    「旧响应覆盖问题未再出现」怎么验证:构造场景测试——用工具让第一个请求延迟 3 秒返回、后续请求正常,然后快速连续切换日期三次,检查最终展示的数据是否对应最后一次选择。改造前每次都能复现,改造后未再出现。这类竞态问题用确定性场景验证,不要用比率。

    不要报「预订转化率提升 X%」。那受价格、供给、活动影响,不是客户端优化能独立归因的。可以报的是「日历上展示价格后,用户切换日期的平均次数下降」——如果有埋点数据的话,这个指标和这次改动的因果关系相对清晰。

    面试追问
    Q:用户快速连续切换日期,怎么保证界面显示的是最后一次选择的结果? A:用请求序号比对。每次发请求前把一个自增序号加一,请求带上当前序号,响应回来时比对:序号等于当前值才采用,小于当前值说明是过期响应,直接丢弃为什么不用「取消上一个请求」:小程序端对请求取消的支持不一致,而且即使调了取消,响应可能已经在路上了;序号比对是纯逻辑,各端行为完全一致,更可靠。这个模式适用于所有「用户输入驱动的异步查询」——搜索联想、筛选、分页都是同一类问题。配套还要做两件事:loading 骨架占位避免内容闪烁,以及把日期状态提到全局 store,保证日历高亮、列表数据、下单时用的日期是同一个来源。
    Q:日期为什么不用时间戳传? A:因为日期是日历概念,不是时刻。「10 月 1 日入住」指的是酒店当地的 10 月 1 日,它本身不带时区。一旦用时间戳,就必须选一个时区来表达这一天的某个时刻,而链路上任何一层做了时区转换就会错开一天。我们踩过的具体坑是 new Date('2025-10-01')——这个字符串被当成 UTC 零点解析,转成本地时间后在负时区变成了 9 月 30 日,而且不同端的 JS 引擎处理还不完全一致。正确做法是全程用 YYYY-MM-DD 字符串:比较直接用字符串比较(这个格式的字典序就是时间序),加减天数用专门的纯日期工具函数,绝不转成 Date 对象。另外「今天」要用服务端下发的目的地当前日期,客户端本地时间既可能被改也可能时区不对。
    Q:为什么要自己写日历组件? A:因为核心需求现成组件不支持——我们需要在日期格子里显示当天的最低价、把售罄的日期置灰不可点,这两个是旅游业务的关键体验(用户能一眼看出哪几天便宜、哪几天没房,不用逐个试)。市面上的日历组件基本只做日期选择,改造它们的成本超过自绘,而且现成组件在小程序和 App 上的渲染表现常有差异。代价是要自己处理跨月连续滚动、闰年、区间高亮、区间内含售罄日的提示这些细节,工作量不小。但如果需求只是选单个日期,我一定用现成的——判断依据是「核心需求是否被现成方案覆盖」,为了一个次要需求自绘是浪费。
    Q:用户选的日期里有一天没房,前端怎么处理? A:整个区间不可订(用户不可能中间一晚没地方住),但提示要具体。做法是:明确指出是哪一天没房并在日历上高亮那一天,而不是笼统地说「所选日期不可订」;推荐替代方案——相邻的可订区间、或者同酒店的其他房型、或者附近的相似酒店。还要区分「无房」和「网络错误」:无房是正常业务结果,要展示空态加推荐;网络错误才展示重试。把无房显示成错误页会让用户以为应用坏了而反复重试。另外前端校验只为即时反馈,服务端必须独立校验一遍——接口会被直接调用,前端拦不住。

模块二:价格展示与下单一致性

  1. 价格展示与下单一致性(分层价格 + 二次确认 + 多币种展示)★★★
    简历这样写 价格展示与下单一致性保障(uni-app + 分层价格策略 + 提交前二次确认 + Intl 多币种格式化):列表用参考价并显式标注、详情实时查、提交前必须二次确认价格,价格变动时弹出对比明细要求用户重新确认,绝不静默按新价提交;金额全程以最小货币单位整数传递、前端只做展示格式化不做运算,逐日价格明细可展开核对。价格不一致引发的客诉改造后未再出现,提交前二次确认拦下价格变动的情况每日仍有发生(说明这道防线在真实工作)。
    展开完整拆解
    为什么要这么设计

    旅游业务的价格不在我们手上——一部分来自供应商实时接口,随时可变。这带来一个电商很少遇到的问题:用户看到的价格和他点下单时的价格可能不一样

    第一版为了列表页快,用了缓存价格,然后直接拿这个价格去下单。三类事故。

    一是价格不一致的投诉。列表显示 500,下单扣了 550。用户认为平台虚假标价,这不只是体验问题,在部分市场是监管风险

    二是前端算金额算错了。总价、税费、优惠后金额都在前端算,用浮点数乘除,出现「三晚 199.99 显示成 599.96999」这种情况。而且前端算出来的金额和服务端算的不一致,以谁为准说不清。

    三是多币种展示混乱。写死了货币符号在前面、小数点两位、逗号做千分位,结果在部分地区完全不符合当地习惯(有的用点做千分位、日元没有小数位)。

    所以三个设计:价格分层(列表参考价、详情实时价、下单前二次确认)前端绝不做金额运算格式化统一走 Intl 按 locale 处理

    整体链路
    价格三层(按用户意向强度分配成本,这是核心思路) │ ├─ 列表页 用服务端缓存价 │ 界面上明确标注「参考价」,允许有偏差 │ 追求速度:用户在浏览阶段,意向弱 │ ├─ 详情页 实时向供应商查 │ 用户已表达意向,多等几百毫秒可接受 │ 展开可见逐日价格明细(用户会逐日核对) │ └─ 提交前 再确认一次(这是最关键的一道) 客户端提交下单请求 → 服务端回查供应商 │ ├─ 价格未变 → 直接进入支付 │ └─ 价格变了 → 返回「需重新确认」而不是直接下单 客户端弹出对比:原价 X → 现价 Y,差额 Z 用户点「确认按新价」才继续 用户取消 → 回到详情页并刷新价格 涨价和降价都要确认(降价也要告知,这是正向体验) 金额的传递与展示(前端只展示,不运算) │ ├─ 服务端返回:{ amount: 19999, currency: "USD", scale: 2 } │ 最小货币单位的整数,不用浮点数 │ ├─ 前端展示:Intl.NumberFormat(locale, {style:'currency'}) │ 货币符号位置、千分位、小数位数全部由 Intl 按 locale 决定 │ 日元这类零小数位货币自动正确处理 │ ├─ 前端不做任何金额运算 │ 总价、税费、优惠后金额、汇率换算 一律服务端算好返回 │ 前端连「单价 × 天数」都不算 │ └─ 逐日明细可展开:每一天的日期 + 当日价格 + 小计 因为用户真的会逐日核对,明细透明能减少客诉 提交下单的按钮状态(防重复提交) ├─ 点击即锁定按钮,同时置全局「有进行中的下单」标记 ├─ 二次确认弹窗期间按钮保持锁定 └─ 结果未知(超时)时不允许直接重试,先查询订单是否已创建
    分步拆解
    1. 列表页用缓存价但必须明确标注。标注「参考价」或「起」是诚实告知,用户有预期就不会觉得被骗。不标注而用缓存价是最糟的组合——既不准又不说。
    2. 详情页开始走实时查询。用户点进详情说明有意向,此时值得花几百毫秒拿准确价格。这一层是「参考价」到「真实价」的过渡。
    3. 提交前的二次确认是最后也是最重要的一道。客户端发起下单时服务端回查供应商,价格变了就返回「需重新确认」而不是直接下单。绝不能静默按新价扣款。
    4. 降价也要告知用户。只在涨价时确认、降价时静默按低价成交,看起来对用户有利,但会让用户困惑(我看的是 500 怎么扣了 450)。降价时也弹出告知,这是正向的信任建设。
    5. 金额一律用最小货币单位的整数传递。19999 表示 199.99 美元。这从根上消灭了浮点精度问题。注意不同货币的小数位数不同(日元 0 位、部分货币 3 位),所以要带 scale 字段,不能一律除以 100。
    6. 前端绝不做金额运算,这是纪律不是技术。只要允许前端算,就一定会有人在某个角落写 price * nights,然后出精度问题或者和服务端算的不一致。连「单价乘天数」都由服务端算好返回。
    7. 格式化全部走 Intl。货币符号位置、千分位符号、小数位数各地区习惯不同,自己写格式化函数一定会漏。Intl 是浏览器原生能力,规则最全。
    8. 逐日明细必须可展开。旅游用户真的会逐日核对(因为每天价格不同),把明细摊开展示能显著减少「为什么是这个总价」的咨询。
    9. 提交按钮点击即锁定,且要有全局标记。用户可能返回上一页再进来点,只锁单个按钮不够。这和电商下单的防重复提交是同一套逻辑。
    关键决策与取舍

    二次确认会降低转化,这是明确的代价。多一次弹窗、多一次点击,一部分用户会在这里流失。但价格不一致的代价更大——客诉、退款、平台信誉、甚至监管问题。取舍方向在支付相关的场景必须是「宁可慢一点、宁可少成交,不可扣错钱」。缓解手段是把价格变动的发生率降低(提高详情页价格的实时性和缓存的准确度),而不是取消这道确认。

    列表页用缓存价的偏差要控制在可解释的范围。如果缓存过期时间太长,价格偏差会很大,用户点进详情发现价格变了很多,即使有标注也会不满。所以缓存的过期时间要短(秒级到分钟级),宁可多查几次也别让偏差太大。

    踩过的坑:前端算了汇率换算。为了展示用户本币价格,前端拿了一个汇率接口自己算。结果汇率缓存过期、和服务端下单时用的汇率不一致,展示价和实付价差了几块钱,用户投诉。修法是汇率换算完全由服务端做,前端只展示服务端返回的本币金额这个坑的通用教训是:任何会影响实付金额的计算都不能放在前端,包括汇率、税费、优惠。

    踩过的坑二:二次确认弹窗期间用户能返回,导致状态错乱。弹窗弹出后用户按了物理返回键,弹窗关了但下单流程的状态还在「等确认」,再点提交时逻辑走乱了。修法是弹窗期间拦截返回手势,或者返回时显式重置整个下单状态移动端的任何中间态都要考虑「用户按返回键」这个动作。

    没做的部分:没做价格变动的历史提示(比如「此价格 2 小时前是 480,现在涨了」)。这对用户决策有帮助,但需要价格历史数据且可能引发「为什么涨价」的咨询,产品上没定,没做。

    数字是怎么测的

    「价格不一致客诉未再出现」怎么验证:依据是客服工单的分类统计,要能说出改造前的具体案例(「列表 500 实扣 550」)。同时要报「二次确认拦下价格变动的次数」——这个数字每天都有,它证明这道防线在真实工作,而不是摆设。报「防线拦下了多少次」比报「问题降为 0」可信得多。

    金额精度:写单测覆盖各币种的展示(美元两位、日元零位、含千分位的大额),以及各 locale 下的格式化输出。这类正确性改造用「测试用例数量与覆盖的币种/地区」说明,比用百分比有说服力。

    详情页实时查价的耗时:要报出来,因为这是二次确认之外用户能感知到的成本。并且要区分「命中缓存」和「回源供应商」两种情况。

    不要报「价格准确率 100%」。价格在供应商手上随时可变,客户端不可能保证「准确」,只能保证「不一致时被拦住」。把能力描述准确,比吹一个绝对数字重要。

    面试追问
    Q:用户在列表看到 500,点进去变成 550,怎么办? A:这在旅游业务里无法完全避免,因为价格在供应商手上随时可变,所以设计目标是「让不一致被拦住并被解释」而不是「让它不发生」。三层处理:列表页明确标注「参考价」(诚实告知,用户有预期);详情页实时查拿真实价;提交前再确认一次,价格变了弹出对比明细「原价 500 → 现价 550」,用户确认才继续,取消就回详情页刷新。绝不能静默按新价扣款——那是监管和法律风险不只是体验问题。还有个细节:降价也要弹确认,只在涨价时提示、降价时静默成交会让用户困惑(我看的 500 怎么扣了 450),主动告知反而是信任建设。
    Q:三晚的总价,前端算还是后端算? A:后端算,前端连乘法都不做。三个原因。一是精度:JS 的浮点数运算会出现 199.99 × 3 = 599.9699999 这类问题。二是一致性:前端算的和后端算的必须完全一致,否则以谁为准说不清,而只要两边各算一次就必然会有不一致的时候(比如优惠规则更新了前端没跟上)。三是旅游的价格是分日的,不是单价乘天数——每天价格不同,还有连住优惠、税费、服务费,规则复杂且会变。所以服务端返回总价 + 逐日明细,前端只负责展示和格式化。金额用最小货币单位的整数传(19999 表示 199.99),并带 currency 和 scale 字段——不同货币小数位数不同,日元是 0 位,不能一律除以 100。汇率换算也必须服务端做,我们踩过前端自己算汇率导致展示价和实付价差几块钱的坑。
    Q:二次确认会不会影响转化率? A:会,这是明确接受的代价。多一次弹窗多一次点击,一定有用户在这里流失。但价格不一致的代价更大——客诉、退款、平台信誉,在部分市场还是监管问题。在支付相关场景,取舍方向必须是「宁可少成交,不可扣错钱」。正确的优化方向不是取消这道确认,而是降低价格变动的发生率:缩短列表页缓存的过期时间、提高详情页价格的实时性、对频繁变价的供应商降权。另外可以优化确认的体验——把差额、原因(比如「该房型余量减少」)说清楚,让用户觉得是合理的变动而不是平台在坑他。能说出「我知道有代价、代价是什么、为什么值得」,比声称「没有副作用」可信。
    Q:多币种展示,你怎么处理不同地区的格式差异? A:全部交给 Intl.NumberFormat,不自己写格式化函数。差异比想象的多:货币符号在前还是在后、小数位数(美元 2 位、日元 0 位、部分中东货币 3 位)、千分位用逗号还是点还是空格、负数怎么表示。自己写一定会漏,而 Intl 是浏览器原生能力,规则最全且随系统更新。做法是服务端返回 { amount: 整数, currency: "JPY", scale: 0 },前端用 Intl.NumberFormat(locale, { style: 'currency', currency }) 格式化。注意 locale 和 currency 是两个独立维度——一个日本用户可能想看美元价格,此时 locale 是 ja-JP 而 currency 是 USD,格式化要按日本习惯展示美元金额。这个区分容易被搞混。

模块三:订单等待确认与状态同步

  1. 订单等待确认与状态同步(中间态展示 + 退避轮询 + 前后台恢复)★★★
    简历这样写 订单确认中间态与状态同步(uni-app + 状态机驱动 UI + 退避轮询 + 前后台切换恢复 + 本地待确认队列):预订不是即时成交,客户端如实展示「等酒店确认」中间态并给出预计时长;结果用退避轮询 + 推送双通道获取,轮询在前后台切换与应用重启后能基于发起时间恢复;超时不判失败而转「处理中」并引导至订单列表。等待期的用户重复下单改造后未再出现,应用被杀后重启仍能自动补查待确认订单,确认结果的可感知延迟中位约 28 秒
    展开完整拆解
    为什么要这么设计

    这是旅游客户端和电商客户端最大的差异:付款成功不等于订单成功。付完钱还要等供应商确认,可能几十秒,也可能失败。客户端必须把这个中间态处理好。

    第一版照电商做,支付成功就跳「预订成功」页。后果很直接。

    一是拒单时用户已经建立了预期。他看到「预订成功」,截图发给同行的朋友,第二天收到「很抱歉酒店无房」的通知。这时的投诉强度远高于一开始就告知「正在确认」。

    二是等待期用户重复下单。第二版改成显示「确认中」但没有阻止操作,用户等了二十秒觉得卡住了,返回重新下了一单,结果订了两间房

    三是轮询在切后台后停了。用户在等待期切到别的应用看了一分钟回来,页面还卡在「确认中」转圈——因为定时器被系统挂起了。

    四是应用被杀后这单就"消失"了。用户重启应用,订单列表里那单还是待确认,但客户端不知道要去查,用户以为丢了。

    所以四个设计:如实展示中间态并给预期时长等待期禁止重复下单轮询要能在前后台切换与重启后恢复超时不判失败

    整体链路
    状态机驱动界面(客户端不自行判定,只映射服务端状态) │ 服务端状态 客户端展示 ├─ 待支付 → 去支付 ├─ 待确认 → 「正在向酒店确认,通常 1 分钟内完成」+ 进度动画 │ 禁止再次下单(全局标记 + 入口置灰) ├─ 已确认 → 预订成功 + 确认单号 + 入住须知 ├─ 确认失败 → 说明原因 + 补偿方案(换房型/退款进度) ├─ 处理中 → 「确认时间较长,结果将在订单列表更新」 └─ 已取消 → 取消详情 + 退款进度 支付成功后的等待流程 │ ├─ 立即本地持久化「待确认订单」记录 │ 订单号 / 发起时间 / 最后查询时间 │ 写 storage,应用被杀也不丢 │ ├─ 进入等待页,展示中间态与预计时长 │ 拦截物理返回(或返回时提示「订单仍在确认中」) │ 置全局「有进行中的预订」标记 → 其他入口禁止下单 │ ├─ 双通道获取结果 │ ├─ 退避轮询:2s → 3s → 5s → 8s … 上限约 60s │ └─ 推送:服务端确认后推一条,收到即立刻查一次 │ ├─ 前后台切换处理(移动端必做) │ 切后台 → 定时器可能被挂起,不依赖它 │ 回前台 → 立刻查一次,并按「已过时间」重算是否继续轮询 │ ├─ 结果处理 │ 已确认 → 成功页,清除本地待确认记录 │ 确认失败 → 失败页 + 补偿说明,清除记录 │ 超时未出结果 → 跳「处理中」页,保留本地记录 │ 明确告知「结果会在订单列表更新」 │ 不显示「失败」,不提供「重新预订」入口 │ └─ 应用启动时扫描本地待确认记录 → 各自补查一次 查到终态就更新并清除记录 超过保留期仍无终态 → 清除记录并提示去订单列表查看 订单列表的状态同步 ├─ 每次进入列表主动刷新(不依赖缓存) ├─ 列表里待确认的订单展示倒计时或「确认中」标识 └─ 下拉刷新触发批量查询
    分步拆解
    1. 如实展示「确认中」,并给出预期时长。把中间态藏起来(直接显示成功)是最糟的做法——拒单时处理成本翻倍。「正在确认,通常 1 分钟内完成」这句话能显著降低焦虑和重复操作。
    2. 等待期必须禁止重复下单,这是防重复订房的关键。不只是锁当前页的按钮——用户会返回列表、重新搜索、再进详情。要有全局标记,让所有下单入口在有进行中的预订时都置灰并提示。
    3. 本地持久化待确认记录,在支付成功后立刻写。写 storage 而不是只放内存。应用被系统杀掉、用户强退,重启后还能知道「有一单没确认」并主动补查。顺序是「先记录,后等待」。
    4. 轮询要退避且有上限。从 2 秒逐步拉到 60 秒上限。固定高频轮询在供应商确认慢时会打出几十个请求。
    5. 前后台切换必须处理,这是移动端和 Web 最大的差异。切后台定时器会被挂起,所以不能依赖定时器的连续性。做法是监听前后台事件,回前台时立刻查一次,并用「记录的发起时间」和当前时间算出已过时长,据此决定还要不要继续轮询。
    6. 推送作为加速通道,不作为唯一依赖。服务端确认后推一条消息,客户端收到就立刻查。但推送会丢(用户关了通知权限、系统限制),所以轮询必须独立存在。
    7. 超时绝不显示「失败」。可能已经确认成功了只是客户端没拿到。显示失败会引导用户重新预订,那才是真正的损失(订两间房)。正确做法是跳「处理中」页,明确告知结果会在订单列表更新,且不提供重新预订入口
    8. 拒单要展示原因和补偿方案,不能只说失败。「酒店已满房,我们正在为您寻找同价位替代房源」或「已全额退款,预计 3 个工作日到账」——给出下一步比只说结果重要得多。
    9. 客户端不自行判定状态,只映射服务端状态。所有状态都从服务端来,客户端只负责展示对应的界面。任何「客户端觉得应该是成功了」的逻辑都是 bug 来源。
    10. 订单列表每次进入主动刷新。这是兜底路径——即使等待页的轮询和推送都失效,用户进列表就能看到真实状态。
    关键决策与取舍

    如实展示中间态会降低「下单成功」的即时满足感,但整体是正收益。用户看到「确认中」确实不如「预订成功」爽,一部分用户会焦虑。但对比拒单时的投诉强度,如实告知的总成本低得多。缓解手段是把预期说清楚(「通常 1 分钟内」)、给进度反馈、以及对自营库存的订单走即时确认并打「立即确认」标签,让用户可以主动选择确定性更高的房源。

    拦截返回键有争议,我们选了「提示而不强拦」。硬拦返回会让用户觉得被困住(尤其是他想去查点别的)。做法是返回时弹提示「订单仍在确认中,可在订单列表查看进度」,允许离开但保留本地记录和全局标记。这样既不困住用户,也防住了重复下单。

    踩过的坑:切后台回来轮询停了,页面永远转圈。用户在等待期切出去看了两分钟,回来页面还在「确认中」,实际上订单早就确认成功了。原因是定时器被系统挂起后没有恢复机制。修法是监听前后台切换事件,回前台立刻查一次,并基于「发起时间」重算轮询状态移动端的任何定时逻辑都必须考虑前后台切换,这是和 Web 开发最大的思维差异。

    踩过的坑二:全局标记只在内存里,应用重启后失效导致重复下单。用户在等待期杀掉应用重开,「有进行中的预订」标记丢了,他又下了一单。修法是全局标记也基于本地持久化的待确认记录来判断,而不是一个内存变量。移动端凡是需要跨会话生效的状态,都必须落 storage。

    没做的部分:没做等待期的「预计剩余时间」倒计时。理论上可以根据历史确认耗时预估,但预估不准反而更焦虑(说 30 秒结果 2 分钟),所以只给了一个模糊的「通常 1 分钟内」。

    数字是怎么测的

    确认结果的可感知延迟:客户端埋点,量从「支付成功」到「界面展示最终状态」的时间,取中位数约 28 秒。要用中位数而不是平均值——少数超时转「处理中」的订单会把平均值拉到分钟级。并且要单独报「转处理中的比例或数量」,只报中位数会掩盖长尾。

    「重复下单未再出现」怎么验证:构造场景手工演练——在等待期尝试所有可能的重复下单路径(返回列表再下单、重新搜索进详情下单、杀应用重启后下单),验证每条路径都被拦住。杀应用重启这条最容易漏,也正是我们踩过坑的地方。

    三类中断场景验证:切后台——等待期切出去停留两分钟再回来,验证能立刻拿到结果;断网——等待期开飞行模式,恢复后验证能继续查询;杀应用——等待期强杀,重启后验证自动补查。这三个必须能说出演练步骤和预期结果,说「我做了兜底」但描述不出验证方式,会被认为没真做。

    不要报「预订成功率」。拒单主要由供应商房态准确性决定,客户端只能减少损失不能提高成功率。可以报的是「因客户端原因导致的重复订单数」——这个是我们能影响的,而且用绝对数。

    面试追问
    Q:付款成功但订单还要等确认,客户端怎么展示? A:如实展示「正在向酒店确认」并给出预期时长,不要显示成「预订成功」。原因是拒单时的处理成本差别巨大——如果用户已经看到「成功」并截图发给同行的人,第二天告诉他没房,投诉强度远高于一开始就说「正在确认」。配套三件事:给进度反馈(动画 + 「通常 1 分钟内完成」);禁止重复下单(全局标记,所有下单入口置灰,不只是当前页按钮);本地持久化这条待确认记录,应用被杀重启后还能补查。另外可以做一个体验优化:自营库存的订单能即时确认,就在列表页给这类房源打「立即确认」标签,让在意确定性的用户主动选择。
    Q:用户在等待确认时切到后台,回来发现还在转圈,为什么?怎么修? A:因为定时器在后台被系统挂起了,轮询停了,页面状态没更新。这是移动端和 Web 最大的差异之一——Web 里 setInterval 基本会继续跑(只是可能降频),小程序和 App 在后台会被冻结。修法是不依赖定时器的连续性:监听前后台切换事件,回前台时立刻主动查一次;同时基于本地记录的「发起时间」和当前时间算出已经过去多久,据此决定是继续轮询还是直接转「处理中」,而不是依赖内存里的轮询次数计数。推广一下:移动端任何跨时间的逻辑(轮询、倒计时、超时判断)都不能依赖定时器累积,必须基于时间戳重算。
    Q:轮询超时了还没结果,你显示什么? A:绝不显示「失败」,显示「处理中,结果将在订单列表更新」,并且不提供「重新预订」入口。原因是订单可能已经确认成功了,只是客户端没拿到结果——显示失败会引导用户重新下单,那就订了两间房,是真金白银的损失。同时做几件兜底:保留本地待确认记录,应用下次启动时自动补查;订单列表每次进入主动刷新依赖服务端推送作为加速通道。这和支付链路的「未知态」是完全相同的原则:客户端超时只意味着「我不知道结果」,不意味着「失败」,此时唯一正确的动作是查询而不是判定。把「未知」当成第三种状态而不是归到失败里,是这类问题的核心认知。
    Q:用户在等待期把应用杀了,重启后怎么办? A:靠支付成功时就写入的本地待确认记录。顺序很重要——支付成功后第一件事是把「订单号 + 发起时间」写进 storage,然后才进入等待页。这样即使应用被系统杀掉或用户强退,重启时扫描这些记录就知道「有一单没确认」,各自补查一次,查到终态就更新并清除记录。我们踩过的坑是「有进行中的预订」这个全局标记只放在内存里,重启后标记丢了,用户又下了一单。修法是让这个标记也基于持久化记录来判断推广一下:移动端凡是需要跨会话生效的状态都必须落 storage,内存变量在移动端是随时会消失的。另外记录要有保留期,超期仍无终态就清除并提示用户去订单列表查看,避免脏记录永久累积。

没有匹配的内容,换个关键词试试。

项目拆解 · 旅游预订端(uni-app)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据