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

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

项目背景设定 本地生活到店业务,uni-app 一套代码覆盖用户端(找店、买券)和商家端(扫码核销)。核心链路是定位找店 → 下单买券 → 到店核销。商家端的使用环境很差——店内网络不稳定、设备是低端安卓机、店员操作快且不耐等。
为什么选这三个模块 本地生活的技术难点集中在和物理世界打交道:定位不准、网络断续、设备摄像头质量参差。这些问题没有优雅的纯技术解法,必须靠降级策略和交互设计兜住,而这恰恰是最能体现工程判断的地方。

模块一:定位与附近门店

  1. 定位与附近门店(多级定位降级 + 地图性能 + 权限交互)★★★
    简历这样写 定位与附近门店模块(uni-app + 系统定位 + 地图组件 + 多级降级):设计四级定位降级链(精确定位 → 缓存位置 → IP 粗定位 → 手动选城市),任一级可用即渲染列表,不因定位失败白屏;地图标注点用聚合与视口内渲染控制数量,列表与地图共用一次数据请求。附近门店首屏可见时间由「等定位返回(最长十几秒)」变为1 秒内出内容(缓存位置命中时),定位不可用场景下功能仍可用。
    展开完整拆解
    为什么要这么设计

    最初的实现是「进页面 → 请求定位 → 拿到坐标 → 请求附近门店 → 渲染」。这个串行链路在真实环境下问题很大。

    一是定位很慢甚至拿不到。室内、地下商场、开了省电模式的手机,定位可能要十几秒甚至超时失败。用户看到的是长时间白屏,然后一个「定位失败」的提示,页面什么都没有。

    二是权限被拒绝就等于功能不可用。用户第一次进来被弹权限框,很多人会直接点拒绝(尤其是没有说明为什么要定位的时候)。拒绝之后页面完全空白,用户只能退出。

    三是地图标注点太多导致卡顿。把附近所有门店都打到地图上,密集区域几百个标注点,低端机上拖动地图明显卡。

    核心判断是:定位是一个「不可靠的外部依赖」,不能把它当成必然成功的前置步骤。所有和外部世界打交道的能力(定位、摄像头、网络)都要按「随时可能失败」来设计。所以做成多级降级链,任何一级能拿到一个可用的位置就先渲染

    整体链路
    进入页面(不等定位,立刻做能做的事) │ ├─ 立刻读缓存位置(上次成功的坐标 + 时间戳) │ └─ 存在且未过期 → 立即请求门店列表并渲染(首屏 1s 内出内容) │ └─ 并行发起精确定位(不阻塞上面的渲染) │ ├─ 成功 → 与缓存位置比较 │ ├─ 偏移小 → 不刷新(避免列表突然跳变) │ └─ 偏移大 → 提示「位置已更新」+ 用户点击才刷新 │ └─ 失败 → 走降级链 ├─ 2. 缓存位置(哪怕过期了也用,标注「基于上次位置」) ├─ 3. IP 粗定位(城市级,够用来展示城市热门门店) └─ 4. 手动选城市(始终提供入口,不是兜底才出现) 权限交互 ├─ 弹系统权限框之前,先用自定义说明页讲清用途 │ (直接弹系统框的拒绝率明显更高) ├─ 被拒绝后不反复弹,改为页面内的「开启定位」提示条 └─ 提供跳系统设置的引导(各端 API 不同,走能力抽象层) 地图渲染 ├─ 只渲染当前视口内的标注点 ├─ 密集区域做聚合(显示「12」的簇,点击展开) ├─ 标注点上限保护(超过 N 个强制聚合) └─ 列表与地图共用一次请求的数据,不各请求一遍
    分步拆解
    1. 缓存上次成功的位置,这是首屏提速的关键。用户的位置在短时间内不会大变,上次的坐标足够用来渲染附近门店。存坐标和时间戳,进页面立刻读出来先渲染,不等这次定位的结果
    2. 精确定位并行进行,拿到后谨慎刷新。如果新位置和缓存位置差得不多,不要自动刷新列表——用户正在看的内容突然变了是很差的体验。只有偏移较大时才提示,并且让用户主动点击刷新。「拿到更好的数据不等于应该立刻用」是这里的关键判断。
    3. 降级链的每一级都要有明确的用户提示。用过期缓存位置时要标注「基于上次位置,点击更新」;用 IP 粗定位时要说明「显示城市热门门店」。不能静默降级——用户看到不准确的距离会以为是 bug。
    4. 手动选城市要始终可见,不只是兜底。用户可能就是想看别的城市(出差前查、给家人查)。把它做成常驻的入口,而不是只在定位失败时才出现。
    5. 权限申请前先做自我说明。直接弹系统权限框,用户没有上下文,拒绝率高;而系统权限框被拒绝后再想申请就很麻烦(要引导去系统设置)。做法是先用一个自定义的说明页讲清「用于查找附近门店」,用户点了同意再弹系统框
    6. 被拒绝后不要反复弹。反复弹权限框非常招人烦,而且部分平台会限制弹出次数。改为在页面顶部放一个可关闭的提示条,用户想开的时候自己点。
    7. 地图只渲染视口内的标注点。监听地图的视野变化,只把当前可见范围内的门店打上标注。要加防抖——拖动地图时视野变化是连续的,每次变化都重算标注会卡死。
    8. 密集区域做聚合。相近的多个门店合并成一个显示数字的簇,点击后放大或者展开列表。聚合的计算最好在服务端做(按缩放级别返回聚合结果),前端算聚合在低端机上有性能压力。
    9. 列表和地图共用一次请求。常见的错误是列表请求一次、地图再请求一次,数据还可能不一致(用户在列表看到 A 店但地图上没有)。一次请求,两处渲染。
    10. 距离由服务端算。前端算距离要处理坐标系差异(不同地图服务的坐标系不同,混用会有偏移),而且列表排序必须和服务端一致。坐标系不一致导致的偏移是这个领域的经典坑,统一由服务端处理最安全。
    关键决策与取舍

    用过期缓存位置的风险。用户从一个城市飞到另一个城市,缓存位置还是原来的城市,会看到完全不相关的门店。所以缓存要带时间戳并且有过期判断,超过一定时间的缓存只用来快速渲染但同时强制发起新定位,并且明确标注「基于上次位置」。取舍是「先给内容」优于「给准确内容」,但必须让用户知道这是不准确的。

    为什么不把定位做成阻塞的。阻塞的好处是数据一定准确,不会出现「先展示一批然后跳变」的情况。但代价是白屏时间不可控(依赖外部系统),而且失败时完全没内容。我们的判断是:不可靠的外部依赖不能放在关键路径上。这个原则在定位、支付回调、三方审核这些场景都适用,是可以横向迁移的经验。

    踩过的坑:小程序端定位授权后仍然拿不到坐标。用户明确同意了权限,但接口返回失败。排查发现是系统级的定位服务被关闭了(不是应用权限,是手机的 GPS 总开关)。应用权限和系统定位开关是两件事,只判断前者会误判。修法是失败时区分错误码——权限被拒绝、系统定位未开启、定位超时,三种情况给完全不同的引导文案。笼统的「定位失败请重试」对用户毫无帮助。

    踩过的坑二:坐标系不一致导致门店位置偏移几百米。定位接口返回的坐标系和地图组件使用的坐标系不同,直接把坐标打到地图上,门店位置整体偏移。这类问题肉眼很难第一时间发现(地图上看起来是对的,只是偏了一点),是用户反馈「导航到的地方不对」才定位到。修法是明确每个环节的坐标系并统一转换,并且在测试用例里加入已知地点的坐标校验

    没做的部分:没做定位结果的平滑处理。用户在移动中(比如坐车),定位会持续跳动导致距离数字不停变化。理想做法是做位置的平滑滤波和更新节流,我们只是简单地降低了更新频率。

    数字是怎么测的

    首屏可见时间:在页面 onLoad 和门店列表首次渲染处打时间戳。要分场景报:缓存位置命中时 1 秒内出内容;首次安装(无缓存)时仍然要等定位,这种情况下的耗时取决于定位本身,前端优化不了。只报好的场景是选择性呈现,会被追问「第一次进来呢」。

    定位耗时分布:这个值得埋点——记录定位从发起到返回的耗时,看分布。会发现长尾很长(P95 可能是好几秒,还有一部分直接超时),这个数据正是「不能把定位放在关键路径上」的证据。用数据支撑设计决策,比说「定位可能很慢」有力得多。

    不要报「定位成功率从 X% 提升到 Y%」。定位成功率主要由设备、环境、系统决定,前端改不了。前端能改的是「定位失败时功能是否可用」,这个用定性描述——改造前定位失败页面空白,改造后有降级内容。

    地图性能:可以报标注点数量的控制效果(改造前密集区域几百个标注、拖动明显掉帧;改造后聚合到几十个以内、拖动流畅)。要报测试机型,因为这类问题只在低端机上显现。

    面试追问
    Q:用户拒绝了定位权限,你的页面还能提供什么? A:完整的功能,只是精度降低。按降级链:先用 IP 做城市级定位(后端根据请求 IP 判断城市,不需要任何权限),展示这个城市的热门门店;同时把「手动选城市」和「搜索门店」做成显眼的入口,用户可以自己指定。距离字段在没有精确位置时直接不显示,而不是显示一个错误的距离——显示错的比不显示更糟。核心是:定位是用来提升体验的,不是功能的前置条件。把它当前置条件是设计上的错误。
    Q:拿到新的精确定位后,为什么不直接刷新列表? A:因为用户正在看的内容突然变化是很差的体验。典型场景:用户进页面看到列表,正要点第三家店,这时定位回来了,列表刷新重排,他点到了另一家店。所以做法是比较位置偏移——偏移在几百米内说明附近门店基本不变,不刷新;偏移大才提示「位置已更新,点击刷新」,让用户主动触发。这是「数据正确性」和「交互稳定性」的取舍,交互稳定性在这里更重要,因为偏移小的时候刷新带来的正确性提升几乎为零。
    Q:地图上几百个标注点卡顿,除了聚合还有什么办法? A:几个方向。只渲染视口内的(最直接,用户看不到的不渲染);按缩放级别控制密度(缩得很远时只显示重点门店,放大后才显示全部);用自定义图层替代逐个标注(部分地图 SDK 支持一次性绘制大量点的图层,性能远好于创建几百个标注对象);简化标注样式(带图片和文字的自定义标注比纯图标贵得多)。聚合的计算放服务端也是重要一步——按缩放级别预先算好聚合结果返回,前端不做几何计算。这几个手段可以叠加,实际是组合使用。
    Q:怎么判断用户是不是真的在门店附近(比如核销要求到店)? A:前端的定位结果不能作为判断依据,因为可以被伪造(模拟定位工具很常见)。前端只负责采集和展示,判断必须在服务端做,而且要结合多个信号:定位坐标(作为参考不作为凭证)、扫码得到的门店动态码(码里带门店 ID 和时间戳,这个比定位可信)、商家端的确认操作。最可靠的方式是「商家扫用户的码」或者「用户扫商家的动态码」——物理上必须在同一个地方才能完成。如果业务确实需要位置校验,服务端要做异常检测(同一账号短时间在相距很远的地方核销、定位精度异常高或异常低)。这个问题问的是安全意识,答「前端判断距离」会被直接否掉。

模块二:到店核销

  1. 到店核销(扫码 + 防重复核销 + 弱网与离线兜底)★★★
    简历这样写 商家端到店核销链路(uni-app + 扫码 + 动态码 + 幂等提交 + 本地待同步队列):核销以服务端结果为唯一凭证,提交带幂等键、结果分「成功 / 已核销 / 无效 / 未知」四态,未知态先查询再决定重试;针对店内弱网设计本地待同步队列(记录待确认的核销、恢复网络后自动补提交),并在界面上明确区分「已确认」与「待确认」。压测下重复核销由每轮可复现降至未再复现,断网场景下店员操作不中断且恢复后自动补齐。
    展开完整拆解
    为什么要这么设计

    核销是本地生活里最容易出纠纷的环节,因为它一次操作对应真实的商品或服务交付。出错的后果很具体:重复核销导致用户的券被多扣一次;核销失败但商家已经把东西给了,商家亏损。

    第一版的实现是「扫码 → 提交核销 → 显示结果」。问题在店内环境集中爆发:

    一是店内网络极差。很多店铺在商场内部或者地下,网络时断时续。提交核销经常转圈很久,店员会重复点。二是店员操作节奏很快——高峰期一分钟核销十几单,任何等待都是压力,而且他们会连续快速扫码,容易出现上一单还没确认就扫了下一单。三是设备是低端机,扫码识别慢、界面响应慢。

    核心判断和支付链路一样:「未知」是必须显式处理的第三态。但核销比支付多一个约束——它必须能在弱网甚至短时断网下继续工作,因为店员不能因为网络问题就停止营业。所以引入了本地待同步队列。

    整体链路
    扫码 │ ├─ 识别二维码 → 解析出券码 / 动态码 ├─ 立刻本地校验(格式、是否属于本商家、是否在本地已核销列表里) │ └─ 本地就能判定无效的直接提示,不发请求(省一次弱网往返) │ ├─ 生成幂等键(券码 + 商家 + 本次操作 ID),持久化到本地队列 │ 状态 = 待确认 │ ├─ 提交核销请求 │ ├─ 结果四态 │ ├─ 成功 → 队列项标记已确认,展示核销成功 + 券信息 │ ├─ 已核销 → 明确提示「该券已于 X 时间核销」(不是报错) │ ├─ 无效 → 提示原因(已过期 / 不适用本店 / 券不存在) │ └─ 未知(超时/断网) │ └─ 不判失败,界面显示「待确认」 │ └─ 后台按幂等键重试或查询,直到有明确结果 │ └─ 界面上「已确认」与「待确认」视觉区分明显 待确认的单据不允许再次核销同一张券 网络恢复 / 应用启动 └─ 扫描本地队列中的「待确认」项 └─ 逐个查询服务端状态(用幂等键)→ 更新为最终状态 └─ 有争议的(服务端说没有这笔)→ 标记异常,提示联系客服 界面纪律 ├─ 提交中按钮锁定,且同一券码在待确认期间不可再提交 ├─ 核销成功要有明显的视觉与声音反馈(店员不看屏幕也知道成功了) └─ 待确认数量常驻显示,店员知道有几笔没同步
    分步拆解
    1. 先做本地校验再发请求。券码格式、是否属于本商家、是否在本地已核销记录里——这些本地就能判断的先判断,能拦掉一部分无效操作,省掉弱网下的一次往返。但本地校验只能用来「快速拒绝」,不能用来「确认通过」,通过必须服务端说了算。
    2. 幂等键在提交前生成并持久化。不是提交时才生成,而是先写本地队列再发请求。这样即使请求发出后应用崩溃,重启时也知道有这么一笔待确认的操作。顺序很重要:先记录,后执行。
    3. 结果必须四态。「已核销」要单独一态而不是归到失败——它对店员的含义完全不同(这张券确实用过了,不是系统出错),提示文案要给出上次核销的时间和门店,店员据此和用户沟通。把「已核销」显示成「核销失败」会导致店员重复尝试和不必要的纠纷。
    4. 未知态显示「待确认」而不是失败。界面上明确标出这一笔状态未定,后台继续查询。关键约束是:待确认期间同一张券不允许再次提交核销,这是防重复的核心。
    5. 本地待同步队列要能持久化和恢复。写入 storage,应用启动和网络恢复时扫描队列,逐个查询确认。查询用幂等键而不是重新提交——重新提交虽然有幂等保护,但查询语义更清晰、也不会因为幂等实现的漏洞出问题。
    6. 动态码优于静态码。如果是用户出示码给商家扫,静态码可以被截图转发(用户把码发给朋友,朋友去别的店核销)。动态码带时间戳并定期刷新,截图很快失效。这是业务安全的关键设计,不是技术优化。
    7. 反馈要多通道。核销成功要有明显的视觉变化加声音或震动。店员在忙的时候不会盯着屏幕,声音反馈让他们不看屏幕也知道成功了。这是从实际观察店员操作得出的需求,能讲出这种细节说明真的调研过使用场景
    8. 待确认数量常驻显示。让店员知道当前有几笔没同步完,网络恢复后自动清零。不能让状态是隐藏的——店员需要知道自己的操作是否都生效了,否则会不信任系统。
    9. 异常要有出口。如果查询后服务端说「没有这笔核销」,说明请求压根没到,这时要标记异常并提示店员重新操作或联系客服。不要静默丢弃,也不要自动重新核销(那可能造成重复)。
    关键决策与取舍

    为什么不做真正的「离线核销」(断网时直接判定成功)。这个需求商家一定会提——「网断了我也要能核销」。但技术上不能给出真正的离线核销,因为客户端无法验证券的有效性和是否已被使用,如果允许离线成功,同一张券可以在多台设备上离线核销多次,恢复后才发现冲突,那时东西已经给出去了。我们的方案是「离线可操作,但状态是待确认」——店员的操作不被阻塞(可以继续接待下一个客人),但界面明确告知这笔还没确认,商家自己决定要不要先交付。这个取舍要讲清楚:我们解决的是「操作不中断」,不是「离线也能确认」。把这两件事混为一谈是设计上的错误。

    「待确认」状态给商家带来的心理成本。商家会问「待确认到底能不能给东西」。这不是技术问题而是业务规则问题——需要和业务方定清楚:低客单价的可以先交付(风险可控),高客单价的等确认。前端要做的是把状态和金额都清楚展示,让商家有信息做决定。承认「有些问题的答案不在技术层面」,是成熟度的体现。

    踩过的坑:店员连续快速扫码导致串单。上一笔还在提交中,店员已经扫了下一张码,界面上的券信息被新的覆盖,店员看到的核销成功提示对应的是哪一笔说不清了。修法是把核销做成明确的队列而不是单个当前项——每次扫码创建一条独立记录进列表,各自显示自己的状态,界面上是一个列表而不是一个「当前券」。这个改动是交互模型的改变,不是加个 loading 能解决的。

    踩过的坑二:扫码在低端机上识别很慢。店员要举着手机对好几秒。优化了几点:降低扫码的预览分辨率(识别不需要高清)、限制识别频率(不用每帧都识别)、加对焦提示和取景框(引导用户把码放在中心)、支持手动输入券码兜底(码磨损或者摄像头坏了还能用)。手动输入这个兜底很重要,它是「物理设备可能失效」的应对。

    没做的部分:没做商家端的核销数据本地统计(断网期间店员看不到今日核销笔数汇总)。理论上待同步队列的数据可以本地统计,但涉及和服务端数据的对齐,当时没做完整。

    数字是怎么测的

    重复核销:脚本模拟弱网加快速重复提交同一张券(限速后连点 5 次),检查服务端产生了几条核销记录、券被扣了几次。改造前每轮可复现,改造后未再复现。这类正确性问题必须用可复现的测试来说明,用线上比率反而不可信(分母不好定,而且量小时统计不显著)。

    断网恢复的验证:手工演练——开飞行模式,连续核销 3 笔(都进待确认),恢复网络,验证 3 笔都自动确认成功且没有重复。要能说出演练步骤和预期结果,这比说「我做了离线队列」有说服力。

    扫码识别耗时:在低端机上测「从摄像头对准码到识别成功」的时间,多次取中位数。要报机型。这个指标店员感知最直接,也是最容易被商家投诉的点。

    不要报「核销成功率」。核销失败的主要原因是券本身无效或已使用,这是正常业务结果不是系统问题,把它算进成功率会得出一个没有意义的数字。可以报的是「未知态最终被正确确认的比例」或者更实在的「异常单据数量」。

    面试追问
    Q:商家要求断网也能核销成功,你怎么和他们解释? A:讲清风险而不是讲技术。如果允许离线判定成功,同一张券可以在多台设备上各核销一次,恢复网络后发现冲突,但商品已经交付了,损失谁承担说不清。所以技术上不能给出这个保证。我们能提供的是「操作不被阻塞」——店员可以连续操作,不用等网络,界面明确标出哪些还没确认。同时和业务方一起定规则:低客单价的可以先交付(风险可控且商家自己承担),高客单价的建议等确认。这样商家的实际诉求(不影响接客效率)被满足了,而风险是明示且可控的。关键是把「不能做」转化成「能做的替代方案 + 明确的风险归属」。
    Q:用户的券码被截图转发给别人用了,怎么防? A:静态码防不住,必须用动态码——码的内容带时间戳和签名,客户端每隔一段时间刷新,服务端校验时间窗口。截图几十秒后就失效。更强的方式是反向扫码——让用户扫商家的动态码(商家的码在店内屏幕上不断刷新),这样用户必须物理在店内。还可以加辅助手段:核销时要求用户确认(推一条消息给用户,用户确认才生效)、服务端异常检测(同一用户短时间在不同城市核销)。要说明这几种手段的成本和体验代价——动态码需要网络刷新(弱网下用户的码可能刷不出来),反向扫码需要商家有屏幕,不是所有场景都适用。
    Q:待同步队列里的数据存在本地,用户卸载重装或者清缓存了怎么办? A:本地队列丢了,但不影响最终正确性,因为服务端才是真相。丢失的后果是:那几笔待确认的核销,商家端不知道结果了(既没确认成功也没标记失败)。兜底靠服务端侧的对账——服务端有所有收到的核销请求记录,商家可以在核销记录列表里看到实际结果。所以商家端一定要有「核销记录」页面从服务端拉取,不能只依赖本地队列。本地队列是为了体验(不阻塞操作、自动补提交),不是为了数据可靠性,这个定位要说清楚。数据可靠性永远靠服务端。
    Q:核销和退款冲突怎么处理?比如用户在核销的同时申请了退款。 A:这是并发冲突,必须在服务端用锁或者状态机解决,前端做不了。服务端对券的状态流转要加锁(或者用条件更新),核销和退款两个操作只能有一个成功。前端要做的是正确展示失败原因——如果核销失败是因为券正在退款中,提示文案要说清楚,而不是笼统的「核销失败」,否则店员会反复尝试。另外顺序上要有业务规则:通常已核销的券不允许退款(服务已交付),退款中的券不允许核销。这个问题的答案重点在「识别出这是服务端的职责」,如果试图在前端解决并发冲突,方向就错了。

模块三:图片采集与上传

  1. 图片采集与上传(本地压缩 + 队列化上传 + 失败续传与水印)★★★
    简历这样写 图片采集与上传模块(uni-app + 本地压缩 + 并发受控队列 + 直传对象存储 + Canvas 水印):多图上传改为本地压缩后进受控并发队列,单张失败独立重试不影响整批,草稿与待上传项本地持久化可跨会话恢复;按业务需要在客户端叠加时间与门店水印。单张上传体积由平均约 3.2MB 降至 300KB 上下,弱网(限速 256KB/s)下九张图整批完成率由不足半数提升到多数场景可完成
    展开完整拆解
    为什么要这么设计

    这个模块服务两个场景:商家上传门店和商品图、用户发布到店评价(带图)。最初的实现是「选完图后循环调上传接口」,问题很多。

    一是原图太大。现在手机拍的照片单张三四兆,九张就是近三十兆。在店内的弱网环境下这基本传不完。二是循环上传没有并发控制——九张图同时发起,互相抢带宽,每一张都变慢,而且小程序有并发请求上限,超出的直接失败。三是一张失败整批失败,用户要重新选图重新传,非常挫败。四是应用被切后台或杀掉,已上传的进度全丢。

    所以四个方向对应四个改造:本地压缩(从源头减少数据量,收益最大)、受控并发队列单张独立重试本地持久化草稿

    其中本地压缩是投入产出比最高的——它把问题规模降了一个数量级,比任何传输层的优化都有效。这个判断和视频上传是一样的:能减少数据量就先减少数据量。

    整体链路
    选择图片(相机 / 相册,多选) │ ├─ 立即生成本地预览(用本地临时路径,不等上传) │ └─ 用户可以立刻继续填写表单,上传在后台进行 │ ├─ 逐张处理 │ ├─ 读取尺寸 → 按最大边长等比缩放 │ ├─ Canvas 绘制 → 导出(控制质量参数) │ ├─ 需要水印的:在 Canvas 上叠加时间/门店文字 │ └─ 处理后大小仍超阈值 → 再降一档质量重试 │ ├─ 进上传队列(并发上限 2~3) │ ├─ 每项:本地路径 + 处理后文件 + 状态 + 重试次数 │ ├─ 状态:待上传 / 上传中 / 成功 / 失败(可重试) │ └─ 单张失败 → 独立重试(退避),不影响其他项 │ ├─ 上传方式:申请凭证 → 直传对象存储 → 拿到 URL │ └─ 全部成功后才允许提交表单 └─ 有失败项 → 高亮显示,提供单张重传按钮 持久化 ├─ 草稿(表单内容 + 已成功的图片 URL + 待上传的本地路径)写 storage └─ 重新进入页面 → 检测草稿 → 询问是否恢复 界面纪律 ├─ 每张图独立显示进度与状态(不是一个总进度条) ├─ 失败的图明确可见且可单独重试 └─ 离开页面前提示「有图片未上传完成」
    分步拆解
    1. 预览用本地路径,立刻可见。用户选完图立刻看到缩略图,不等上传完成。上传在后台进行,用户可以继续填表单。这一步是体验的关键——把上传从「阻塞步骤」变成「后台任务」。
    2. 压缩按最大边长而不是固定尺寸。等比缩放到最大边长不超过某个值(比如 1280 或 1920,看业务对清晰度的要求),保持宽高比。不要固定成正方形或者固定宽高,那会拉伸变形或者裁掉内容。
    3. 压缩后要校验结果,必要时再降一档。同样的尺寸和质量参数,不同内容的图压出来大小差别很大(细节丰富的照片压不下去)。所以压完检查大小,超过阈值就降低质量再压一次。要有下限,不能无限降(降太多就糊了),到了下限还超就提示用户换图。
    4. 并发上限设 2 到 3。受小程序的并发请求限制,也因为并发太高在弱网下反而更慢(互相抢带宽,每个都超时)。这个值要实测——不同网络条件下的最优值不同,可以按实测网速动态调整。
    5. 单张失败独立重试。每一项维护自己的状态和重试次数,失败了退避重试,重试到上限后标记为失败并让用户手动重传。绝对不能一张失败就整批重来——那意味着已经传成功的八张也白费了。
    6. 水印在客户端用 Canvas 叠加。把图绘制到 Canvas 上,再画上时间、门店名等文字,然后导出。注意水印文字的大小要按图片尺寸等比计算,写死字号会导致大图上水印很小、小图上水印很大。
    7. 草稿持久化。表单内容、已上传成功的图片 URL、还没传完的本地路径,全部写 storage。用户中途离开或者应用被杀,回来时提示恢复。已成功的 URL 要复用,不能重传。
    8. 每张图独立显示状态。不用一个总进度条,因为总进度条无法表达「第三张失败了」。每张缩略图上叠加自己的进度和状态图标。失败的要明显可见并且能单独点重传。
    9. 离开页面要提示。有未完成的上传时,用户点返回要弹确认(「有图片未上传完成,离开将保存为草稿」)。这比静默丢弃好得多。
    关键决策与取舍

    压缩质量的取舍。压太狠画质肉眼可见地糊,商家会抱怨门店照片不好看;压太轻起不到作用。做法是按业务场景分档——商品主图和门店门面照要求高一些(压得轻),评价配图和凭证照可以压得狠一些。不要一套参数打天下,不同用途的图对画质的要求差很多。

    水印放客户端还是服务端。客户端加水印的好处是所见即所得、不增加服务端成本;坏处是可以被绕过(用改包工具直接传没水印的图)。所以判断依据是水印的用途:如果是为了美观和信息展示(时间、门店),客户端加没问题;如果是作为凭证的防篡改水印(比如证明某张照片是在某时某地拍的),必须服务端加,客户端加的不可信。这个区分要说出来,说明理解「客户端不可信」这个前提。

    踩过的坑:Canvas 压缩后图片旋转 90 度。手机拍的照片带 EXIF 的方向信息,直接绘制到 Canvas 时部分端会忽略这个信息,导出的图是躺倒的。修法是先读取方向信息,绘制前做对应的旋转变换。这个坑在不同端表现不一致(有的端的 Canvas 会自动处理,有的不会),所以必须在每个端都实测。这也是跨端开发的典型问题——同一段代码在不同端行为不同。

    踩过的坑二:大图在低端机上 Canvas 处理导致崩溃。一张四千万像素的图绘制到 Canvas,内存占用是宽乘高乘 4 字节,低端机直接被系统杀掉。修法是先判断原图尺寸,超过阈值的分步缩放(先缩到中间尺寸,再缩到目标尺寸),避免一次性创建巨大的 Canvas。这个坑说明「压缩本身也要考虑资源消耗」——为了省流量而崩溃是得不偿失的。

    没做的部分:没做上传的后台续传(应用切后台时继续上传)。App 端理论上可以做(要处理系统的后台执行限制),小程序端做不到。当时的处理是切后台时暂停、回前台时继续,配合草稿持久化保证不丢。

    数字是怎么测的

    上传体积:取一批真实照片样本(要覆盖不同场景——室内暗光、室外强光、细节丰富的菜品图),统计压缩前后的平均大小。样本量要说得出来(比如 30 张),不然「平均」没有意义。改造前平均约 3.2MB,改造后 300KB 上下。

    弱网整批完成率:用限速工具把带宽限到 256KB/s,同一批九张图重复上传 20 次,统计整批全部成功的次数。用「不足半数」和「多数场景可完成」这种模糊量级而不是精确百分比——20 次的样本量算出 45% 这种精度是假的。

    可以补充的指标:单张上传的耗时中位数、失败后重试的成功率、草稿恢复的使用频次。其中「草稿恢复被用到的次数」很有说服力——它直接证明这个功能是真实需要的,不是过度设计。

    不要报「图片上传成功率 99%」。这类高精度的比率一被追问统计方式就说不清(分母是什么?重试算一次还是多次?),而且 99% 这个数字在弱网场景下不真实。

    面试追问
    Q:为什么并发上限设 2 到 3,不是越多越快吗? A:不是。三个约束。一是平台限制——小程序对同时进行的请求数有上限,超了会直接失败,而且这个失败不是网络错误,排查很困难。二是弱网下并发越多越慢——总带宽固定,并发多了每个连接分到的带宽就少,每个都变慢,更容易触发超时,反而整批失败。三是要给其他请求留余量,上传把连接占满的话,页面的其他接口(比如提交表单)也发不出去了。实测下来 2 到 3 是拐点,再往上加总耗时几乎不变但失败率上升。可以做的进阶优化是按实测网速动态调整并发数,好网络多开几个,弱网降到 1 个。
    Q:客户端加的水印能被绕过,那这个水印还有意义吗? A:看用途。如果水印是为了信息展示和美观(照片上标个拍摄时间和门店名,方便查看),客户端加完全够用——没有人会为了去掉一个装饰性水印去改包。如果水印是作为凭证的防伪手段(比如证明这张照片确实是在这个门店这个时间拍的,用于核验或者纠纷处理),那客户端加的没有任何证明力,必须服务端加,而且服务端加的水印也只能证明「图片经过了我们的服务」,不能证明拍摄地点和时间(那需要更强的手段,比如结合定位、设备信息、时间戳签名,而且这些都可以被伪造到一定程度)。诚实的答案是:如果业务真的需要照片的可信性,纯软件手段做不到强保证,只能提高伪造成本。
    Q:用户传了九张图,第五张一直失败,你的界面怎么表现? A:第五张的缩略图上明确显示失败状态(红色边框加感叹号图标),点击可以单独重传或者删除换一张,其他八张保持成功状态不受影响。提交按钮的状态要明确——如果业务要求所有图必须上传成功,按钮置灰并提示「还有 1 张图片未上传成功」;如果允许部分成功,就允许提交但要告知。绝对不能做的两件事:一是用一个总进度条卡在 88% 不动(用户不知道哪张出了问题、也不知道要做什么);二是整批重来(已成功的八张白费)。失败信息必须精确定位到具体项并给出可执行的操作,这是错误处理的基本原则。
    Q:草稿恢复的时候,之前上传成功的图片 URL 还有效吗? A:这取决于对象存储的清理策略。已上传但未被业务引用的文件是「孤儿文件」,通常服务端会有定时任务清理(否则存储会无限增长)。所以草稿恢复时如果隔了很久,那些 URL 可能已经失效。处理办法有几种:清理任务的保留期设得比草稿有效期长(比如文件保留 7 天,草稿 3 天过期);或者草稿恢复时校验一下 URL 是否还可访问,失效的标记为需要重新上传。还有个更彻底的做法是草稿也提交到服务端(作为未发布的记录),这样文件就被引用了不会被清理,代价是服务端要支持草稿存储。这个问题问的是「前后端边界」的意识,能想到孤儿文件清理这一层说明考虑得比较完整。

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

项目拆解 · 本地生活到店(uni-app)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据