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

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

项目背景设定 企业服务 SaaS 的移动端,uni-app 一套代码发小程序、H5、以及嵌在企业微信和钉钉容器里的应用。核心场景是待办审批——用户在手机上处理报销、请假、采购的审批。
为什么选这三个模块 B 端移动端和 C 端移动端的关注点几乎相反:C 端怕用户不来,B 端怕用户漏审(漏一个审批会卡住别人的工作,是会被投诉到 IT 管理员那里的);C 端追求转化,B 端追求一次操作绝不出错(重复审批会让流程跳两步);C 端不管数据外泄,B 端的企业数据截图外传是真实风险。这三个模块分别对应「不漏」「不错」「不泄」。

模块一:待办中心与消息触达

  1. 待办中心与消息触达(未读单一来源 + 增量拉取 + 角标一致)★★★
    简历这样写 待办中心与消息触达一致性(uni-app + 增量拉取合并 + 已读水位 + 推送信号触发刷新):待办列表走增量拉取加本地合并而非每次全量;未读数由服务端聚合下发作为唯一来源,推送、应用角标、列表红点三处共用同一个数值,推送只作为「该刷新了」的信号而不直接采信其携带的数字;多端已读通过会话级水位同步。「角标有数字但点进去没有待办」的问题改造后未再出现,冷启动到待办首屏约 600ms(命中本地缓存时更快)。
    展开完整拆解
    为什么要这么设计

    B 端审批应用的第一诉求是「别让我漏了」。漏一个审批不是少看一条内容,而是卡住了别人的工作——同事的报销打不了款、采购单走不下去,最后会投诉到 IT 管理员那里,管理员再来找我们。

    第一版的问题全都出在「状态不一致」上。

    一是角标不准,这是最招人烦的。应用角标显示 3,点进去待办列表是空的(因为那三条在网页端已经审了);或者反过来,有新待办但角标没变。用户被骗几次之后就不信角标了,从此靠自己想起来去看,那这个功能等于白做。根因是角标的数字是客户端自己算的,而列表数据来自另一次请求,两个来源必然会不一致。

    二是推送里带了数字,而这个数字是发推送那一刻的。推送发出后用户在网页端审了两条,等他点开推送,客户端直接用推送里的旧数字更新角标,又不一致了

    三是每次进来全量拉待办。用户有几十条待办,每次切前台、每次下拉都全量拉一遍,流量和延迟都浪费。

    四是多端已读不同步。手机和网页同时登录,网页审完了手机上那条还在列表里,用户点进去发现「该单据已被处理」。

    所以四个设计:未读数只有一个来源(服务端聚合)推送只当信号不当数据列表增量拉取已读用会话级水位同步

    整体链路
    未读数只有一个来源(这是角标准确的前提) │ ├─ 服务端提供一个聚合接口:返回各分类的未读数 │ 待我审批 N1 / 我发起的进行中 N2 / 抄送我的未读 N3 │ ├─ 客户端所有展示未读的地方都读这一个来源 │ 应用角标 / 底部 tab 红点 / 列表页分类标签 │ → 三处永远一致,因为它们是同一个数 │ └─ 客户端绝不自己算未读(不用「列表里有几条未读」推导) 原因:列表是分页的,本地只有一部分数据,算不准 推送的正确用法(只当信号) │ ├─ 服务端有新待办 → 推送一条通知 │ 推送体里可以带标题摘要(给用户看) │ 但不带「未读数」这个值(或者带了客户端也不用) │ ├─ 客户端收到推送 → 触发「去拉一次未读聚合接口」 │ 拿到的是当前真实值,而不是推送发出时的值 │ └─ 为什么:推送发出到用户点开可能隔了很久 期间他可能在网页端处理了几条,推送里的数字已经过期 角标更新的四个时机(少一个就会不准) ├─ 收到推送时(前台) ├─ 应用切到前台时 ├─ 完成一次审批操作后 └─ 退出登录时清零(不清会残留给下一个登录的人看到) 待办列表的增量拉取 │ ├─ 客户端记「上次同步的时间戳或版本号」 │ ├─ 请求带上它 → 服务端只返回变更过的条目 │ 新增的待办 / 已被处理的(要从本地移除)/ 内容有更新的 │ ├─ 客户端按单据 ID 合并到本地列表 │ 新增插入、已处理移除、有更新的替换 │ ├─ 首次进入或版本差距过大 → 服务端返回「需全量」标记 │ 客户端退回一次全量拉取(否则长期不登录的设备永远同步不上) │ └─ 本地列表持久化,冷启动先渲染缓存再增量刷新 → 首屏几乎无等待,数据随后更新 多端已读同步 ├─ 已读不是逐条标记,而是「会话级水位」(读到哪条了) ├─ 任一端处理了单据 → 服务端状态变更 → 其他端下次增量拉取时移除 └─ 在线时通过推送信号即时触发刷新,不用等用户手动下拉
    分步拆解
    1. 未读数必须由服务端聚合下发,客户端绝不自己算。这是角标准确的唯一办法。客户端本地只有分页的一部分数据,用「本地有几条未读」推导必然不准。服务端一个聚合接口返回各分类的未读数,客户端所有展示未读的地方都读这一个值。
    2. 推送只当信号,不采信它携带的数字。推送发出到用户点开可能隔了几小时,期间他可能在网页端处理了几条。收到推送后去拉一次聚合接口拿当前真实值,这比直接用推送里的数字可靠得多。
    3. 角标更新的时机要覆盖四个,漏一个就会不准。收到推送时、切到前台时、完成审批操作后、退出登录时清零。最后一条容易漏——不清零的话,下一个人在这台设备登录会看到上一个人的角标数字。
    4. 列表用增量拉取,且要能退回全量。带上「上次同步位点」,服务端只返回变更条目。但必须支持「版本差距过大时返回需全量标记」,否则长期不登录的设备永远同步不上(中间的变更记录可能已被清理)。
    5. 增量返回里必须包含「已被处理的条目」。只返回新增是不够的——在网页端处理掉的那些要从本地列表移除,否则用户点进去才发现「该单据已被处理」。
    6. 本地列表持久化,冷启动先渲染缓存。启动时先把本地缓存的列表画出来(几乎无等待),同时发起增量请求,数据回来再更新。这是首屏体验的关键——B 端用户打开应用就是要处理待办,等一秒转圈很影响观感。
    7. 缓存要标注「数据可能不是最新」的状态。先渲染缓存时顶部给一个细微的刷新指示,数据更新完消失。不能让用户以为看到的就是最新的——他可能基于旧列表做判断。
    8. 待办要分类而不是一个大列表。「待我审批」「我发起的」「抄送我的」「已办」四类,各自有独立的未读数。「待我审批」是唯一需要红点提醒的,其他三类不该打扰用户。
    9. 处理完一条要立刻从列表移除并更新未读数,不等下次刷新。用户审完返回列表,如果那条还在,他会以为没成功。本地乐观移除 + 后台校准
    10. 已读用会话级水位而不是逐条标记。逐条标记会带来写放大和多端同步的复杂度。水位只需要同步一个数字,这和 IM 的已读设计是同一个思路。
    关键决策与取舍

    「未读数单一来源」的代价是多一次请求。本来列表接口顺便返回未读数就行,现在要单独调一次聚合接口。但这一次请求换来的是三处状态永远一致,而角标不准的体验代价远大于一次轻量请求。聚合接口要做得很轻(只返回几个数字,服务端走缓存计数),这样调用成本可以忽略。

    推送不能作为唯一触达手段,必须有兜底。推送会丢——用户关了通知权限、系统限制后台唤醒、企业容器内的推送策略不同。所以「切前台时刷新」这个时机是必需的兜底,它保证用户只要打开应用就能看到真实状态。把推送当成加速而不是依赖,这个定位要说清。

    踩过的坑:退出登录没清角标,下一个人看到了上一个人的数字。共用设备(比如车间的平板)上,A 退出后 B 登录,角标还是 A 的 5 条待办,B 点进去列表是空的,B 以为系统坏了。修法是退出登录时显式清零角标并清空本地缓存列表。这个坑提醒了另一件更严重的事:本地缓存的待办列表里有企业数据,退出登录必须彻底清理,否则是数据泄露。

    踩过的坑二:增量同步的位点用了客户端本地时间。客户端时间不准(或者用户手动改过),导致位点算错,有一段时间的待办永远拉不到——用户漏审了几条,被投诉。修法是位点由服务端下发(服务端在每次响应里返回「本次同步位点」,客户端原样存下来下次带回),客户端完全不参与位点的计算。凡是需要服务端理解的时间或序号,都不能由客户端生成。

    没做的部分:没做待办的智能排序(按紧急度、按发起人重要性排)。目前是按时间倒序。这需要业务规则和数据支撑,而且排序规则一旦复杂,用户会找不到某条待办(他记得是按时间排的),当时判断风险大于收益。

    数字是怎么测的

    「角标不一致未再出现」怎么验证:构造场景演练——手机和网页同时登录,在网页处理掉几条,验证手机角标在切前台后正确更新;发一条推送后先在网页处理掉,再点开推送,验证角标是当前真实值而不是推送里的旧值;退出登录再登录另一个账号,验证角标清零。这三个场景正是我们踩过坑的地方,必须能说出演练步骤。

    冷启动到待办首屏:在应用启动和列表首次渲染完成处打时间戳。约 600ms,要分「命中本地缓存」和「首次安装无缓存」两种情况报——前者几乎无等待(先渲染缓存),后者取决于网络。只报好的那个是选择性呈现。

    增量拉取的效果:抓包对比全量与增量的响应体大小和条目数。要说明测试条件(本地已有多少条、期间变更了几条),因为「增量」的优势取决于变更比例——如果全变了增量就没优势。

    不要报「审批及时率提升」或「漏审率下降」。这些取决于用户自己的工作习惯和工作量,客户端只能保证「不因为系统原因让他看不到」。可以报的是「因角标或列表不一致导致的支持工单数量」变化,这个是我们能归因的。

    面试追问
    Q:应用角标为什么老是不准?你怎么解决的? A:根因是未读数有多个来源。以前角标是客户端自己算的(本地列表里有几条未读),列表数据来自另一次请求,两个来源必然不一致;而且客户端本地只有分页的一部分数据,压根算不准。解法是让未读数只有一个来源:服务端提供一个轻量的聚合接口返回各分类未读数,客户端所有展示未读的地方(应用角标、tab 红点、列表分类标签)都读这一个值,三处永远一致。还有两个关键点推送只当信号不采信它带的数字——推送发出到用户点开可能隔几小时,期间他可能在网页端处理了几条,所以收到推送后要去拉一次聚合接口拿当前真实值;角标更新时机要覆盖四个——收到推送、切到前台、完成审批后、退出登录清零,最后一条最容易漏,共用设备上不清零会让下一个登录的人看到上一个人的数字。
    Q:推送丢了,用户就看不到新待办了? A:不会,因为推送只是加速手段,不是依赖。推送确实会丢——用户关了通知权限、系统限制后台唤醒、企业微信和钉钉容器内的推送策略各不相同。所以必须有兜底:「切到前台时刷新」这个时机是必需的,它保证用户只要打开应用就能看到真实状态;再加上下拉刷新和定时轮询(低频,只在前台)。这个定位要说清楚:推送让用户「更早知道」,前台刷新保证他「一定能知道」。反过来如果把推送当唯一触达,就会出现「用户不打开应用永远不知道有待办」的情况——而 B 端漏审的后果是卡住别人的工作,这个风险不能接受。所以对超时未处理的待办,服务端还会有定时提醒(比如超过一天未审批再推一次,或者走邮件),这是第三层兜底。
    Q:用户在网页端审完了,手机上那条待办怎么消失? A:靠增量拉取时返回「已被处理的条目」。很多人做增量只返回新增,那就会出现「手机列表里还有那条,点进去才发现已被处理」。正确做法是增量响应里包含三类变更:新增的(插入本地列表)、已被处理的(从本地移除)、内容有更新的(替换),客户端按单据 ID 合并。触发时机是收到推送信号、切前台、下拉刷新。另外增量必须能退回全量——如果客户端的同步位点太旧(长期不登录,中间的变更记录已被清理),服务端要返回「需全量」标记让客户端重新拉一次,否则这台设备永远同步不上。还有个坑:同步位点必须由服务端下发(每次响应返回本次位点,客户端原样存下次带回),不能用客户端本地时间算——我们踩过,用户手机时间不准导致一段时间的待办永远拉不到,漏审被投诉。
    Q:待办列表要不要做本地缓存? A:要,但必须处理好两件事。做缓存的理由是首屏体验——B 端用户打开应用就是要处理待办,冷启动转圈一秒很影响观感,所以先渲染本地缓存的列表,同时发起增量请求,数据回来再更新第一件要处理的是「要让用户知道这可能不是最新的」:顶部给一个细微的刷新指示,更新完消失,否则用户可能基于旧列表做判断。第二件更重要:退出登录必须彻底清理本地缓存——待办列表里有企业数据(报销金额、采购内容、人员信息),留在设备上是数据泄露风险,尤其是共用设备的场景。我们踩过角标没清零的坑,顺着这个坑才意识到缓存数据也必须清。B 端应用的本地缓存策略必须把「数据敏感性」作为第一考虑,这和 C 端很不一样。

模块二:审批操作的可靠性

  1. 审批操作的可靠性(幂等提交 + 未知态处理 + 批量部分成功)★★★
    简历这样写 审批提交的幂等与结果确定性(uni-app + 客户端幂等键 + 三态结果 + 批量逐条结果 + 意见草稿本地暂存):审批提交带客户端生成并持久化的幂等键,同一操作重试不会产生两条审批记录;结果分成功 / 明确失败 / 未知三态,未知态先查询单据当前节点再决定是否可重试,绝不直接提示失败;批量审批返回逐条结果并在界面上区分成功与失败项,审批意见与附件本地暂存可跨会话恢复。压测弱网重复提交下重复审批记录未再出现,批量审批部分失败时用户能明确知道哪几条没成功
    展开完整拆解
    为什么要这么设计

    审批是一次性的、不可逆的、且会驱动流程前进的操作。它出错的后果比 C 端的点赞失败严重得多。

    第一版的实现很朴素:点「通过」调接口,成功跳回列表,失败弹提示。三类事故。

    一是重复审批导致流程跳步。用户在电梯里点「通过」,转圈很久没反应,又点了一次。服务端处理了两次,而流程引擎的逻辑是「收到通过就推进到下一节点」,于是单据一次跳了两步,跳过了本该有的一级审批。这个问题是最严重的——它绕过了审批规则。

    二是超时被判定成失败,用户重新审。客户端设了 10 秒超时,超时后提示「审批失败,请重试」。但那个请求在第 20 秒成功了。用户看到失败又审了一次,又是重复。

    三是批量审批不知道哪条失败了。用户勾选了 12 条一起通过,接口返回「部分失败」,但不知道是哪几条。用户只能一条条点开看,或者干脆全部重审一遍(重审又触发重复问题)。

    第四个是体验问题但很招人烦:写了很长的审批意见,提交失败后意见没了,要重新写。

    所以四个设计:客户端生成并持久化幂等键结果分三态且未知态先查询批量返回逐条结果意见与附件本地暂存。这套逻辑和之前电商下单、本地生活核销的处理是同一个模式——凡是「一次操作对应一次不可逆业务变更」的场景,都要这么做。

    整体链路
    单条审批提交 │ ├─ 打开审批页时就生成幂等键并写入本地 │ 幂等键 = 单据 ID + 节点 ID + 操作类型 + 客户端操作 ID │ 顺序很重要:先记录,后提交 │ → 应用崩溃重启后还知道「有一个提交没确认」 │ ├─ 点击提交 → 按钮立即锁定 + 全局标记「有进行中的审批」 │ ├─ 提交(带幂等键、审批意见、附件 ID 列表) │ 附件先单独上传拿到 ID,提交时只传 ID │ → 提交请求很小,重试成本低 │ └─ 结果三态 ├─ 成功 → 更新本地列表(移除该条)+ 刷新未读 + 清幂等记录 ├─ 明确失败 → 展示具体原因,允许重试(换新幂等键) │ 单据已被他人处理 / 你不是当前审批人 / 流程已结束 └─ 未知(超时 / 网络错误) └─ 不提示失败!转入查询兜底 按幂等键或单据 ID 查当前节点与审批记录 ├─ 已有我的审批记录 → 当作成功 └─ 确实没有 → 才允许用同一幂等键重试 批量审批(部分成功是常态,不是异常) │ ├─ 提交:一次请求带多条单据 + 各自的幂等键 │ ├─ 服务端逐条处理,返回逐条结果 │ [ {单据A: 成功}, {单据B: 失败, 原因: 已被他人处理}, ... ] │ 绝不返回一个整体的成功或失败 │ └─ 界面展示 成功的从列表移除 失败的保留并标红 + 显示各自原因 顶部汇总「10 条成功,2 条失败」 提供「仅重试失败项」按钮(失败项换新幂等键) 意见与附件的本地暂存 ├─ 审批意见随输入实时暂存(按单据 ID 存) ├─ 已上传成功的附件 ID 也暂存 ├─ 提交失败或应用被杀 → 重新进入时恢复 └─ 提交成功后清除该单据的暂存 应用启动时的未确认提交检查 └─ 扫描本地幂等记录 → 逐个查询单据当前状态 有我的审批记录 → 标记为已完成,清除记录 没有 → 提示用户「上次的审批可能未提交成功,请确认」 → 绝不自动重新提交(那可能造成重复)
    分步拆解
    1. 幂等键由客户端生成,而且要在提交之前就持久化。服务端生成的话,客户端重试时带不上同一个键。顺序是「先写本地记录,后发请求」——这样即使应用在请求过程中崩溃,重启后还知道有一个提交没确认。
    2. 幂等键要包含节点 ID。只用「单据 ID + 操作」不够——同一张单据可能在流程里多次经过同一个人(先审批后又被驳回重审),不带节点 ID 会导致第二次审批被误判成重复而被幂等掉
    3. 附件先单独上传,提交时只传 ID。这样提交请求很小,重试成本低;而且附件上传失败不会导致整个审批提交失败。附件上传和审批提交是两个独立的可重试单元。
    4. 结果必须分三态,这是整个模块的核心。成功、明确失败、未知。绝大多数事故来自把「未知」当成「失败」。超时、网络中断、请求被取消,这些情况下服务端可能已经成功了。未知态下绝不能提示「失败请重试」。
    5. 未知态要转入查询兜底。按幂等键或单据 ID 查询「这张单据上有没有我的审批记录」。有就当成功,确实没有才允许用同一个幂等键重试(用同一个键,这样即使前一次其实成功了也不会重复)。
    6. 明确失败的原因要具体。「单据已被他人处理」「你不是当前审批人」「流程已结束」对应完全不同的后续动作。笼统的「审批失败」会让用户反复尝试。
    7. 批量审批必须返回逐条结果,把「部分成功」当成常态设计。批量场景下部分失败几乎必然发生(有些单据被他人先处理了)。返回一个整体的成功或失败是设计缺陷——用户不知道该重试哪些。
    8. 批量的界面要区分成功项和失败项。成功的移除,失败的保留并标红显示各自原因,顶部给汇总,并提供「仅重试失败项」。「仅重试失败项」是关键——不然用户只能全部重来,又会碰到重复问题。
    9. 审批意见随输入实时暂存。用户可能写几百字,提交失败后没了会非常恼火。按单据 ID 存本地,提交成功后清除。
    10. 应用启动时检查未确认的提交,但绝不自动重新提交。扫描本地幂等记录,逐个查询单据状态:有我的审批记录就清除记录;没有就提示用户确认,让人决定自动重提可能造成重复审批,这个风险不能由系统自己承担。
    关键决策与取舍

    未知态先查询会让用户多等几秒,这个代价必须接受。弱网下超时之后再发一次查询请求,可能又要等几秒。但对比重复审批导致流程跳步的后果——绕过了审批规则、要人工回滚流程、可能涉及资金——多等几秒完全不算什么。取舍方向和支付链路一致:在不可逆操作上,宁可慢,不可错。

    幂等键持久化带来了「脏记录」的管理成本。本地会累积一些未确认的幂等记录(应用被杀、用户放弃操作)。处理办法是设保留期(比如 7 天),过期自动清理,并且启动时检查一次。不清理会让本地存储越来越大,也会在很久之后弹出莫名其妙的「上次审批未确认」提示。

    为什么不做离线审批。用户一定会问「地铁里没网能不能先审,联网后自动提交」。技术上不该做:客户端无法验证「我现在还是不是当前审批人」(可能已被他人处理或流程已变更),如果允许离线记录后自动提交,联网后可能提交到一个已经不该由我审批的节点上,产生错误的审批记录。我们的做法是「离线时保存草稿但不提交」——意见和附件都存着,联网后引导用户确认后再提交。「操作不阻塞」和「离线也能生效」是两件事,这和本地生活核销的判断完全一致。

    踩过的坑:重复提交导致流程跳两步。弱网下用户连点两次「通过」,服务端处理了两次,而流程引擎的逻辑是「收到通过就推进到下一节点」,单据一次跳了两步,跳过了本该有的一级审批,事后是财务对账时发现某笔报销没经过部门经理审批。这是最严重的一类问题——它不是数据错,是绕过了业务规则。修法是幂等键 + 服务端用「单据 ID + 节点 ID」做条件更新(where current_node = 期望节点),第二次请求因为节点已经变了而不生效。幂等和状态机条件更新要一起用,只有一个都不够。

    踩过的坑二:幂等键漏了节点 ID,驳回重审时被误幂等。单据被驳回后重新提交,又走到同一个人这里,他第二次审批时用的幂等键和第一次一样,被服务端当成重复请求直接返回了上次的结果,实际上这次审批没生效,单据卡住。修法是幂等键里加节点 ID(或者加流转轮次)。幂等键的组成必须覆盖「什么情况下算同一次操作」的完整语义,少一个维度就会误判。

    没做的部分:没做审批的撤回(审批通过后想撤回)。这需要流程引擎支持逆向流转,涉及已经产生的下游动作(比如已经打款了)怎么处理,业务规则复杂,当时是走「作废重新发起」。

    数字是怎么测的

    重复审批验证:用工具限速模拟弱网,对同一单据连点 5 次「通过」,检查审批记录表里产生了几条、单据当前节点前进了几步。两个都要查——只查审批记录可能看不出流程跳步。改造前每轮可复现,改造后未再出现。还要测驳回重审场景(验证第二次审批不会被误幂等掉),这是我们踩过的第二个坑。

    未知态处理验证:构造「请求实际成功但响应丢失」的场景(用代理工具丢弃响应),验证客户端不显示失败、转入查询、查到审批记录后当作成功。这类场景手工很难造,用抓包工具的响应丢弃功能可以稳定复现。

    批量部分成功验证:勾选 12 条,其中 2 条在提交前用另一个账号先处理掉,验证返回逐条结果、界面正确区分成功与失败、「仅重试失败项」只重试那 2 条。

    不要报「审批成功率 99.9%」。审批失败的主要原因是业务原因(单据已被他人处理、不是当前审批人),这是正常业务结果不是系统故障,算进成功率没有意义。可以报的是「因重复提交导致的错误审批记录数」——这个是我们能影响且口径清晰的,而且用绝对数。

    也不要报「零重复审批」。绝对化断言。正确表述是「压测多轮未出现,且服务端有状态机条件更新作为第二道防线」。

    面试追问
    Q:用户网络慢,连点了两次「通过」,会怎样? A:如果不处理,后果比想象的严重——我们踩过:服务端处理了两次,而流程引擎的逻辑是「收到通过就推进到下一节点」,单据一次跳了两步,跳过了本该有的一级审批,是财务对账时才发现某笔报销没经过部门经理。这不是数据错,是绕过了业务规则。防护要两层:客户端幂等键(打开审批页时就生成并持久化,重试带同一个键),服务端状态机条件更新where current_node = 期望节点,第二次请求因为节点已变而不生效)。两层都要有——幂等表可能因为并发或缓存问题漏判,条件更新是最后防线。另外客户端还要点击即锁定按钮并置全局标记,因为用户可能返回列表再进来点。
    Q:提交超时了,你显示什么? A:绝不显示「审批失败,请重试」——那个请求可能在超时之后成功了,用户看到失败又审一次就是重复。正确做法是把超时归入「未知」这第三态,转入查询兜底:按幂等键或单据 ID 查「这张单据上有没有我的审批记录」,有就当成功,确实没有才允许用同一个幂等键重试(用同一个键的意义是——即使前一次其实成功了,重试也不会产生第二条记录)。界面上显示「正在确认审批结果」而不是「失败」。这和电商支付、本地生活核销的处理是完全同一个模式:客户端超时只意味着「我不知道结果」,唯一正确的动作是查询而不是判定。凡是「一次操作对应一次不可逆业务变更」的场景,都必须有这个三态设计。
    Q:用户批量审批了 12 条,其中 2 条失败,怎么处理? A:接口必须返回逐条结果,绝不能返回一个整体的成功或失败。批量场景下部分失败几乎必然发生(有些单据被他人先处理了),把它当异常处理是设计缺陷。界面上:成功的 10 条从列表移除失败的 2 条保留并标红,各自显示具体原因(「已被张三处理」「流程已结束」),顶部给汇总「10 条成功,2 条失败」,并提供「仅重试失败项」按钮。「仅重试失败项」是关键——如果只能全部重来,用户会把已成功的 10 条又提交一遍,虽然幂等能挡住,但白白多了请求也增加了出错面。失败项重试时要换新的幂等键(因为是新的一次操作意图),而未知态重试才用同一个键,这两个场景要区分清楚。
    Q:地铁里没网,能不能先审批,联网后自动提交? A:技术上不该做,我会拒绝这个需求并解释原因。客户端无法验证「我现在还是不是当前审批人」——单据可能已被他人处理、流程可能被管理员改了、我可能已经不在这个审批节点上。如果允许离线记录后自动提交,联网时可能把审批提交到一个已经不该由我审批的节点上,产生错误的审批记录,而审批记录是有合规意义的(审计要查谁批的)。我们能提供的是「离线保存草稿但不提交」:审批意见和已选附件都存在本地,联网后引导用户确认后再提交。「操作不阻塞」和「离线也能生效」是两件事,前者能做后者不能——这和本地生活到店核销的判断完全一致(那里也是商家要求离线核销,我们只提供离线可操作但状态待确认)。能把「不能做」转化成「能做的替代方案 + 明确的风险说明」,比硬做出一个有隐患的功能好。

模块三:容器内免登与数据防泄露

  1. 容器内免登与数据防泄露(免登换会话 + 静默续期 + 水印与审计)★★★
    简历这样写 企业容器免登与数据防泄露(uni-app + 容器免登授权码换会话 + 会话静默续期 + 动态水印 + 访问审计):应用嵌在企业微信与钉钉容器内时走免登流程(取容器临时授权码,由服务端换取企业身份并建立会话,客户端不接触任何凭证换取逻辑);会话静默续期不打断用户填写,过期时保留草稿再引导重新授权;敏感页叠加含用户标识与时间的动态水印并记录访问审计,对截屏的管控能力按端如实分级。免登后进入待办无需输入账号,会话过期导致的表单内容丢失改造后未再出现
    展开完整拆解
    为什么要这么设计

    B 端移动应用有个 C 端不会遇到的部署形态:它经常不是独立 App,而是嵌在企业微信、钉钉这类企业容器里的一个工作台应用。用户点开工作台图标就要能用。

    第一版是直接照独立应用做的,三个问题。

    一是还要输账号密码,用户不接受。员工的认知是「我已经登录企业微信了,为什么还要再登一次」。而且他大概率不记得这个 SaaS 的密码(是 IT 管理员给他开的账号)。结果是应用的日活极低,IT 管理员来投诉「你们的东西员工不用」。

    二是会话过期的时机极其伤人。用户写了几百字的审批意见、传了三张附件,点提交时弹出「登录已过期,请重新登录」,重新登录回来内容全没了。这个体验会让用户直接放弃在手机上审批。

    三是企业数据的外泄风险是真实的。审批单里有报销金额、采购价格、人员薪资,员工截图发到外部群是真实发生过的事。客户的安全部门在采购评审时会直接问「你们怎么防截图」。

    所以三个设计:容器内走免登会话静默续期且过期时保住草稿水印加审计提高泄露的追溯性

    第三点要特别说清一个认知:纯软件手段防不住截屏(用户可以用另一台手机拍屏)。所以目标不是「防住」而是「提高泄露成本并让泄露可追溯」——这个定位说不清楚,在面试里会被认为不懂安全。

    整体链路
    容器内免登(凭证换取一律在服务端,客户端不碰) │ ├─ 应用在容器内启动 → 调容器 API 拿一个临时授权码 │ 这个码是一次性的、短时效的、只能由服务端兑换 │ ├─ 客户端把授权码发给我们的服务端 │ ├─ 服务端用「授权码 + 企业应用的密钥」向容器的开放平台兑换 │ 拿到容器内的用户标识(比如企业内的 userid) │ → 密钥只在服务端,客户端永远拿不到 │ ├─ 服务端按「租户 + 容器用户标识」找到我们系统里的账号 │ 找不到 → 返回「未开通」而不是自动创建 │ (自动创建会导致越权:容器里的人不等于该用这个应用的人) │ └─ 建立会话,返回会话凭证(短时效)+ 刷新凭证(长时效) 多容器差异走能力抽象层 ├─ 企业微信 / 钉钉 / 独立 App / 浏览器 H5 各自的免登 API 不同 ├─ 抽象成统一接口:getAuthCode() ├─ 内部按端条件编译各自实现 └─ 不支持免登的端(浏览器 H5)→ 返回「不支持」,走账号登录 会话静默续期(不打断用户操作) │ ├─ 会话凭证短时效,刷新凭证长时效 │ ├─ 请求返回「凭证过期」→ 拦截器自动用刷新凭证换新的 │ 并发请求时只发起一次刷新,其余请求排队等结果 │ (否则会同时发起多个刷新,互相把对方的刷新凭证作废) │ ├─ 刷新成功 → 用新凭证重放原请求,用户完全无感知 │ └─ 刷新也失败(长期未使用)→ 才需要重新授权 此时必须先保住草稿:审批意见、已上传附件 ID 都写本地 重新授权成功后回到原页面并恢复草稿 数据防泄露(目标是提高成本与可追溯,不是绝对防住) │ ├─ 动态水印(敏感页叠加) │ 内容:用户姓名 + 工号 + 当前时间 │ 样式:半透明、倾斜、平铺满屏、置于内容之上 │ 实现:canvas 生成后作为背景,或多个绝对定位文本层 │ → 截图外传后能追溯到是谁截的 │ ├─ 截屏管控(能力按端如实分级,不能夸大) │ App 端:可监听截屏事件 → 记审计 + 提示用户 │ 部分平台可设置禁止截屏窗口标记 │ 小程序端:能力受限,基本做不到禁止 │ → 所以主要依赖水印 + 审计,而不是依赖禁止 │ ├─ 访问审计 │ 谁在什么时间查看了哪张单据(尤其是含金额的) │ 异常访问检测:短时间大量浏览、非工作时间批量查看 │ └─ 退出登录 / 会话失效 → 彻底清理本地缓存 待办列表、草稿、附件缓存全部清掉(都含企业数据)
    分步拆解
    1. 凭证兑换必须在服务端,客户端只传授权码。兑换需要企业应用的密钥,密钥一旦下发到客户端就等于公开(反编译、抓包都能拿到),拿到密钥就能冒充应用换取任意用户的身份。这是免登流程的安全底线。
    2. 容器用户标识找不到对应账号时,返回「未开通」而不是自动创建。容器里有全公司的人,但不是所有人都该用这个 SaaS(可能只买了 50 个席位)。自动创建等于越权开通,而且会让计费也算错。
    3. 各容器的免登 API 差异走能力抽象层。企业微信、钉钉、独立 App、浏览器的免登方式完全不同,抽象成一个 getAuthCode(),内部按端条件编译。不支持免登的端返回「不支持」让业务层降级到账号登录,而不是静默失败
    4. 会话用「短时效凭证 + 长时效刷新凭证」的组合。短时效凭证降低泄露风险,刷新凭证让用户不用频繁登录。这是标准做法,但并发刷新是必踩的坑,见下一条。
    5. 并发刷新必须做成「只发起一次,其余排队等结果」。页面同时发了五个请求都返回凭证过期,如果各自去刷新,后面的刷新会把前面刚换到的凭证作废(很多实现里刷新凭证是一次性的),结果全部失败并跳登录。做法是用一个共享的刷新中的 Promise,谁先触发谁去刷,其余等它的结果。
    6. 刷新成功后要自动重放原请求。用户完全无感知——他点了提交,中间悄悄刷新了凭证,然后请求成功。不能让用户重新点一次。
    7. 刷新也失败时,先保草稿再引导授权。这是体验的关键。审批意见、已上传的附件 ID 都写本地,重新授权成功后回到原页面并恢复。做不到这一点,用户就不会在手机上写长意见了。
    8. 水印要包含可追溯到个人的信息。用户姓名加工号加时间。只放公司 logo 的水印没有追溯价值——泄露后不知道是谁。样式要半透明倾斜平铺,覆盖在内容之上(不能被内容遮住),也不能挡住阅读。
    9. 截屏管控的能力必须如实分级,不能夸大。App 端可以监听截屏事件(记审计并提示用户),部分平台能设置禁止截屏;小程序端基本做不到禁止。所以整体策略是依赖水印和审计,而不是依赖禁止。对外沟通时要说清能力边界。
    10. 访问审计要记录「谁看了哪张单据」,并做异常检测。短时间大量浏览、非工作时间批量查看这类行为要告警。审计的价值在事后追溯,异常检测的价值在事中发现。
    11. 会话失效和退出登录都要彻底清理本地缓存。待办列表、草稿、附件缓存全都含企业数据。只清会话凭证是不够的——下一个人登录这台设备时,本地缓存的待办列表还在。
    关键决策与取舍

    免登降低了登录门槛,但也把身份的信任链交给了容器。如果容器方的授权码机制被攻破,我们的免登就被绕过了。缓解手段是免登只用于「进入应用」,敏感操作仍要二次验证——比如查看薪资类单据、修改银行卡信息时要求输入密码或走容器的二次验证。「免登不等于免验证」这个分层要说出来。

    水印会影响可读性,这是明确的取舍。水印太淡没有威慑和追溯价值(截图后看不清),太重影响阅读。我们的做法是只在含敏感信息的页面加水印(金额、人员信息),普通页面不加;并且水印文字用较大间距平铺而不是密集覆盖。不要全站无差别加水印——那会让整个应用看起来很难用,而大部分页面并不敏感。

    踩过的坑:并发刷新凭证导致集体掉线。用户切回前台,页面同时发起了未读数、待办列表、用户信息三个请求,全部返回凭证过期,三个请求各自去刷新,后两次刷新用的是已被作废的刷新凭证,全部失败,应用直接跳到登录页。用户的反馈是「你们的应用老是自己退出登录」。修法是刷新逻辑做成单例:用一个模块级的变量存「正在刷新的 Promise」,并发请求都 await 同一个 Promise,刷新完成后统一重放。这是拦截器实现里最经典的坑,几乎每个项目都会踩一次。

    踩过的坑二:免登时自动创建了账号,导致席位被超额占用。早期为了让用户「开箱即用」,容器用户标识找不到对应账号时就自动创建一个。结果客户只买了 50 个席位,但公司三百人都点开过工作台,系统里创建了三百个账号,计费和权限都乱了,而且这些人本来不该有访问权。修法是找不到账号就返回「未开通,请联系管理员」,账号必须由 IT 管理员显式开通。B 端的「便利」不能越过「授权」这条线。

    没做的部分:没做设备绑定与异地登录管控(同一账号在陌生设备登录时要求二次验证)。客户的安全部门提过,需要设备指纹和风险评估,当时只做了会话记录与手动踢下线。

    数字是怎么测的

    「免登后无需输入账号」怎么验证:在企业微信和钉钉两个容器里各走一遍完整流程,验证点开工作台图标后直接进入待办列表。还要测异常分支:容器里的人在我们系统里没有账号(应显示「未开通」而不是自动创建)、授权码过期(应重新获取而不是报错)、在浏览器里打开(应降级到账号登录)。这三个分支是免登功能真正容易出问题的地方。

    「表单内容丢失未再出现」怎么验证:构造场景——在审批页写好意见并上传附件,手动让会话失效(后台踢掉会话),然后点提交,验证:先尝试静默刷新;刷新失败则保存草稿并引导重新授权;授权后回到原页面且意见和附件都在。这个链路要完整演练。

    并发刷新的验证:让页面同时发起多个需要鉴权的请求并使会话过期,检查网络面板里刷新接口只被调用了一次,且所有原请求都被正确重放。这是个可以精确验证的点。

    不要报「防止了数据泄露」或「杜绝截图外传」。纯软件手段防不住(用另一台手机拍屏就绕过了所有管控)。正确的表述是能力描述:「敏感页含可追溯到个人的水印」「App 端截屏有审计记录」「访问行为可审计并有异常检测」。并且要主动说明小程序端无法禁止截屏这个能力边界——如实说出局限,比声称做到了更可信。

    面试追问
    Q:嵌在企业微信里的应用,免登是怎么实现的? A:核心流程是「客户端拿临时授权码,服务端换身份」。客户端调容器提供的 API 拿一个一次性、短时效的授权码,把它发给我们的服务端;服务端用「授权码 + 企业应用密钥」向容器的开放平台兑换,拿到容器内的用户标识,再按「租户 + 容器用户标识」找到我们系统里的账号,最后建立会话。安全底线是密钥只能在服务端——一旦下发到客户端就等于公开(反编译抓包都能拿到),拿到密钥就能冒充应用换取任意用户身份。还有个必须注意的点:容器用户标识在我们系统里找不到账号时,要返回「未开通」而不是自动创建。我们踩过这个坑——为了「开箱即用」自动创建,结果客户只买 50 个席位但三百人点开过工作台,系统里创建了三百个账号,计费和权限全乱了。B 端的便利不能越过授权这条线。
    Q:会话过期时用户正在填一个很长的表单,怎么处理? A:分两层。第一层是尽量不打断——会话用「短时效凭证 + 长时效刷新凭证」,请求返回过期时由拦截器自动用刷新凭证换新的,然后重放原请求,用户完全无感知。第二层是刷新也失败时(长期未使用)必须先保住草稿:审批意见、已上传的附件 ID 都写本地存储,引导重新授权,授权成功后回到原页面并恢复内容。做不到这一点,用户就不会在手机上写长意见了。
    这里有个必踩的坑:并发刷新。用户切回前台时页面同时发了几个请求,全部返回过期,如果各自去刷新,后面的刷新用的是已被作废的刷新凭证(很多实现里刷新凭证是一次性的),全部失败,应用直接跳登录页。修法是把刷新逻辑做成单例——用一个模块级变量存「正在刷新的 Promise」,并发请求都 await 同一个,刷新完统一重放。
    Q:员工把审批单截图发到外部群,你怎么防? A:先说清结论:纯软件手段防不住——用另一台手机拍屏就绕过了所有管控。所以目标不是「防住」而是「提高泄露成本并让泄露可追溯」。具体做法三层:动态水印,敏感页叠加「姓名 + 工号 + 时间」,半透明倾斜平铺覆盖在内容之上,截图外传后能追溯到是谁截的(只放公司 logo 的水印没有追溯价值);截屏审计,App 端可以监听截屏事件并记录审计、提示用户「本次操作已记录」,这个提示本身就有威慑作用;访问审计与异常检测,记录谁在什么时间看了哪张单据,对「短时间大量浏览」「非工作时间批量查看」这类行为告警。能力边界要如实说:小程序端基本无法禁止截屏,所以整体策略是依赖水印和审计而不是依赖禁止。另外水印只加在敏感页,全站无差别加会让应用很难用,而大部分页面并不敏感。
    Q:免登了,那敏感操作也直接放行吗? A:不行,免登不等于免验证。免登只解决「进入应用」这一步,它把身份的信任链交给了容器——如果容器方的授权码机制被攻破,或者用户的手机被别人拿到(企业微信已登录状态),免登就被绕过了。所以要做操作分级:普通操作(查看待办、审批常规单据)走免登身份即可;敏感操作要二次验证——查看薪资类单据、修改银行卡信息、审批超过一定金额的单据,要求输入密码、或走容器提供的二次验证(比如企业微信的身份核验)、或短信验证码。分级的依据是「这个操作出错的后果」,不是技术难度。另外会话本身也要有时效,不能因为免登方便就把会话设成永久有效——那样手机丢了就是长期风险。这个「便利与安全分层」的判断,是 B 端设计里必须表达清楚的部分。

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

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