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

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

实习级这一档是干什么的 下单支付链路、长列表性能、跨端包体积治理这些,实习生大概率碰不到。这一档收的是客户端里实习生真的会被分派、但仍然有明确技术含量的活。下面三个模块彼此独立——只做过一个就只写一个,不用凑齐。
怎么讲才不显得 low,这是这一档最要紧的事 「我做了个图片上传」和「用户一次选 20 张图,全并发上传在弱网下全部超时,而且压缩本身在低端机上会把内存打满,所以我做了并发控制加逐张压缩,失败的单张重试而不是整批重来」——同一件事,后者面试官会顺着追问。客户端这一档的破解办法是把「设备和网络的真实条件」讲出来:用户会在电梯里操作、会用三年前的安卓机、会拒绝定位权限,这些约束才是需求的来源,也是技术含量所在。
三条自检 一、能说出不这么做会怎样(不控并发弱网下全挂、不做权限兜底用户拒绝定位后功能全废、不清草稿用户会重复发同一条评价);二、能说出你踩过的具体坑(哪次低端机压图崩了、哪次用户拒绝权限之后没有出路);三、能说出量级(一次最多几张图、压缩前后多大、目标机型是什么档位)。三条都有就能写,缺一条先补。
项目背景设定 跨境商城的用户端,uni-app + Vue 3,发布到微信小程序与 App 双端。这一页收的是客户端里最高频的三块基础能力,不是交易核心链路。
为什么这三块值得写 它们的技术含量来自客户端特有的约束:设备性能差异大、网络不可靠、系统权限用户可以拒绝、应用会被切到后台。这些约束在服务端是不存在的,所以这三块能讲出后端讲不了的东西。共同点是每一块都有一个「在我的手机上好好的,用户那边就出问题」的过程——而这恰好是客户端开发最核心的经验

模块一:图片选择、压缩与上传

  1. 图片选择、压缩与上传(并发控制 + 逐张压缩防内存峰值 + 单张失败重试 + 服务端签发上传凭证)★★★
    简历这样写 多图上传链路(uni-app + 分平台能力抽象 + 并发控制 + 压缩策略 + 上传凭证):用户一次可选多张图,原实现全部并发上传且并行压缩,弱网下大量超时、低端机在压缩阶段内存峰值过高甚至闪退;改为逐张压缩、限制并发数上传,压缩按最大边长与目标体积而非固定比例(避免细节图被压花);每张图独立维护状态,失败仅重试该张而不是整批重来,并保留已成功的结果;上传凭证由服务端签发限时令牌,客户端不持有长期密钥;chooseImage 数量上限、compressImage 支持度等平台差异收敛到一层能力抽象里。改造后弱网场景下多图上传的成功率明显提升,低端机的压缩阶段闪退不再复现。
    展开完整拆解
    为什么要这么设计

    先说清这块和商品列表那边的「图片治理」不是一回事:那边是展示侧(懒加载、格式、尺寸、CDN 裁剪),这边是上传侧——用户从相册选图、压缩、传到我们的存储。约束完全不同。

    第一版是最直觉的写法:chooseImage 拿到路径数组,Promise.all 全部压缩,再 Promise.all 全部上传。在我的手机上一次传五张图,两秒搞定。然后就收到了四类反馈。

    一是弱网下全军覆没。用户在地铁里传图,十张全并发,带宽被瓜分,每一条都慢到超时,最后一张都没成功。而如果串行传,至少能成几张。并发在网络好的时候是优化,在网络差的时候是灾难——这一点我当时完全没想到。

    二是低端机在压缩阶段闪退。并行压缩十张图,每张都要解码成位图,内存峰值直接超了低端机的上限。表现是应用闪退,用户以为是「传图会崩」。而我在旗舰机上测完全正常。

    三是失败了要整批重来。Promise.all 有一个失败整个 reject,我的处理是提示「上传失败请重试」。用户已经成功传了八张,重试时又从第一张开始——不仅慢,还可能产生重复的图。

    四是压缩把细节图压花了。我用了固定的压缩质量,对普通照片没问题,但用户传的商品瑕疵凭证(比如衣服的一个线头)压完就看不清了,客服无法判定,售后纠纷。

    所以四个改动:逐张压缩(把内存峰值从「N 张」降到「1 张」)、限制上传并发数(在网络好和网络差之间取平衡)、每张图独立状态、失败只重试单张压缩按最大边长和目标体积而非固定质量

    这个模块最想说的一句话是:客户端的难点不是实现功能,是「我的设备和用户的设备不一样」。并发、内存、网络这三件事在开发机上都不成问题,而它们恰好是这个功能的全部工作量。所以我后来养成的习惯是:任何涉及批量和媒体处理的功能,都要在低端机加弱网下测一遍。

    整体链路
    选择(平台差异收敛到能力抽象层) │ ├─ chooseImage / chooseMedia 的数量上限各端不同 │ 抽一层统一接口,业务侧只声明「最多选几张」 │ 超出上限时给明确提示,不要静默截断 │ ├─ 已选数量 + 本次选择数量 要合并校验 │ 常见 bug:已有 8 张,又选 5 张,超了上限却没拦 │ └─ 拿到的是临时路径,注意它可能失效(切后台、系统清理) 压缩(逐张做,不并行) │ ├─ 逐张:解码一张 → 压缩 → 释放 → 下一张 │ 并行压缩的内存峰值是 N 张的和,低端机直接闪退 │ ├─ 策略按「最大边长 + 目标体积」而不是固定质量 │ 固定质量会把细节图压花(瑕疵凭证看不清会引发售后纠纷) │ 做法:先按最大边长缩放,再逐步降质量直到进入目标体积 │ ├─ 小于阈值的图直接跳过压缩 │ 本来就小的图再压一次是纯浪费,还可能变模糊 │ └─ compressImage 各端支持度不同 → 不支持则降级为「只校验不压缩」 降级要有上限:超过体积上限的图在不支持压缩的端上直接拒绝 上传(限制并发,每张独立状态) │ ├─ 先向服务端取上传凭证(限时令牌 + 限定路径前缀) │ 客户端绝不持有长期密钥 —— 反编译就能拿到 │ ├─ 并发数限制在小的固定值,其余排队 │ 并发在好网下是优化,在弱网下是灾难(带宽瓜分导致全部超时) │ ├─ 每张图一个独立状态机: │ 待压缩 → 压缩中 → 待上传 → 上传中 → 成功 / 失败 │ 每张显示自己的进度,失败的单独标红可点重试 │ ├─ 失败只重试该张,已成功的结果保留 │ 整批重来会浪费已完成的工作,还可能产生重复图 │ └─ 重试有次数上限,超了给手动重试入口(不无限自动重试) 前后台与中断 │ ├─ 切后台时上传可能被系统挂起 → 回前台后检查状态并续传或重试 │ ├─ 临时文件路径可能已失效 → 检测到失效时提示重新选择该张 │ 这一条容易漏:用户切出去十分钟回来,路径已经不可读了 │ └─ 页面卸载时取消进行中的上传任务(不要留后台任务继续跑) 提交 ├─ 业务提交时只带已成功的图片地址列表 └─ 有失败未处理的图 → 提交前明确询问(继续提交还是先处理)
    分步拆解
    1. 压缩必须逐张做,这是防闪退的关键。并行压缩的内存峰值是所有图的和,而压缩要把图解码成位图,一张高分辨率照片解码后可能几十 MB。逐张的代价是总耗时变长,但它把峰值内存从 N 倍降到 1 倍——在低端机上这是能不能用的区别,不是快慢的区别。
    2. 上传并发要限制在一个小的固定值,不要全并发也不要纯串行。全并发在弱网下带宽被瓜分,每条都超时;纯串行在好网下浪费带宽。取一个小的并发数是折中。更进一步可以根据网络类型动态调整,但要注意网络类型 API 的准确性有限,不能完全依赖。
    3. 压缩策略要按「最大边长 + 目标体积」而不是固定质量。固定质量对不同内容的效果差别很大——风景照压得很好,细节图(商品瑕疵、文字凭证)会压花。做法是先按最大边长缩放,再逐步降质量直到进入目标体积,这样不同内容都能得到可接受的结果
    4. 本来就小的图跳过压缩。再压一次是纯浪费,而且可能因为二次编码变模糊。设一个阈值,小于它就直接上传。
    5. 每张图必须有独立的状态和进度。整体一个进度条的问题是:用户不知道是哪张卡住了、失败了也不知道是哪张。独立状态是「失败只重试单张」的前提。
    6. 失败只重试单张,保留已成功的结果。Promise.all 的语义(一个失败全部 reject)在这里是错的,要用「各自结算」的方式收集结果。用户传了十张成了八张,重试的应该只是那两张。
    7. 重试有次数上限,超了转手动。无限自动重试在弱网下会持续耗电和流量,而且用户看不到尽头。失败几次之后应该把控制权交回用户(给一个明显的重试按钮),而不是自己一直试。
    8. 上传凭证必须服务端签发,客户端不持有长期密钥。把存储的 AK/SK 打进客户端,反编译或者抓包就能拿到,等于把存储桶的写权限公开了。正确做法是每次上传前向服务端换一个限时、限定路径前缀的令牌。这一条是安全底线。
    9. 平台差异收敛到一层能力抽象里。chooseImage 的数量上限、compressImage 的支持度、返回路径的形态各端不同。业务代码里不应该出现条件编译,应该调统一接口,差异关在抽象层里。
    10. 已选数量和本次选择数量要合并校验。常见 bug:限制 9 张,已有 8 张,用户又选了 5 张,代码只校验了本次的 5 张没超上限,结果变成 13 张。
    11. 临时路径会失效,要检测并给出路。用户选完图切出去十分钟再回来,临时文件可能已被系统清理。此时上传会失败且错误信息很难懂,正确做法是检测到失效时明确提示「该图片已失效,请重新选择」。
    12. 切后台的上传要能恢复。系统可能挂起上传任务。回前台时检查每张图的状态,进行中但实际已中断的要重试。
    13. 页面卸载要取消进行中的任务。否则用户已经离开了,后台还在传,浪费流量而且结果无处可用。
    14. 提交时如果还有失败的图,要明确询问而不是静默忽略。用户可能没注意到有一张失败了,静默提交会让他的评价缺一张关键凭证。
    关键决策与取舍

    逐张压缩的代价是总耗时明显变长,我认为必须接受。并行压缩快得多,但它把内存峰值推到低端机的上限之上——快和崩之间没有取舍空间。缓解办法是把压缩和上传流水线化:第一张压完就开始传,同时压第二张,这样用户感知的等待时间接近「压一张的时间 + 传全部的时间」而不是两段相加。这个流水线是纯收益,没有额外风险。

    并发数选一个固定的小值,而不是按网络类型动态调。动态调听起来更聪明,但网络类型 API 的准确性有限(显示 4G 实际很慢的情况很常见),而且判断错了反而更糟。取一个在弱网下也能工作的小并发数,是更稳的选择——它在好网下损失一点速度,但在所有网络条件下都可用。这个取舍偏向了下限保障。

    压缩失败时选择「降级为不压缩但校验体积」,而不是直接拒绝。某些端不支持压缩 API。直接拒绝会让那个平台的用户完全用不了这个功能。降级的代价是可能上传一张较大的图,所以配一个体积硬上限——超过上限的在不支持压缩的端上才拒绝。这是「尽量让功能可用,但守住底线」的思路。

    踩过的坑一:在旗舰机上测完全正常,用户的低端机闪退。这个坑让我改变了测试习惯——凡是涉及批量处理和媒体解码的功能,必须在低端机上测。而且要测「用户会做的极端操作」:一次选满上限、选大尺寸原图、连续操作。我后来专门借了一台三年前的安卓机放在工位上,这件事的收益远超它的成本。

    踩过的坑二:Promise.all 的语义用错了。它的「一个失败全部失败」在批量任务里几乎总是错的选择。批量任务需要的是「各自结算、汇总结果」,而不是「全成功或全失败」。这个认知后来在做任何批量操作时都在用——先问「部分成功是不是可接受的状态」,如果是,就不能用全或无的语义。

    踩过的坑三:把存储的密钥写在客户端配置里。为了图快,直接用了存储服务的长期密钥。代码评审时被指出来:客户端代码在用户手上,反编译或者抓包就能拿到,等于把存储桶的写权限公开了。修法是服务端签发限时令牌并限定路径前缀。这件事让我理解了「客户端没有秘密」这个原则——任何打进客户端的东西都要假设它是公开的。

    踩过的坑四:用户切后台十分钟回来,上传全失败且提示看不懂。临时文件路径失效,上传接口报了一个底层错误码,我原样弹给了用户。修法是检测路径可读性并给出人能理解的提示。这类问题的通用教训是:底层错误不能原样透给用户,要翻译成「他能采取行动」的话。

    没做的部分:没做断点续传。单张商品图压缩后通常不大,重传一次的成本可以接受,而断点续传需要分片、记录偏移、服务端配合合并,复杂度和收益不成比例。如果业务变成上传视频,那断点续传就是必需的了——判据是「单个文件重传一次的代价能不能接受」。

    数字是怎么测的

    弱网成功率:用开发工具的网络限速模拟弱网(或者真的去信号差的地方),一次选满上限的图,重复若干轮,统计「全部成功的轮次比例」和「平均成功张数」。要报后者——因为改造前的典型失败形态是「一张都没成」,改造后是「都成了或者少数几张要重试」,只看「全部成功率」会丢掉这个差别。测试要说清限速档位和图片张数,否则数字不可比。

    低端机闪退「不再复现」:这是断言型结论,报法是「在指定机型上,一次选满上限的原图,反复操作若干次未再复现闪退」,并说明机型档位和内存大小。不要说「解决了内存问题」——你只能证明在测过的机型上不再复现。

    内存峰值:用平台的性能面板看压缩阶段的内存曲线,对比并行与逐张两种实现。报峰值的量级差异(N 倍降到 1 倍)而不是精确 MB 数,因为它取决于图片分辨率。

    压缩效果要按内容类型分别报。普通照片和细节图(文字、瑕疵)的压缩表现完全不同。只报平均压缩率会掩盖「细节图被压花」这个问题,而那恰好是我踩过的坑。做法是准备两组样本分别报体积和主观可辨识度。

    耗时要拆开报:选择、压缩、上传三段各自多久。因为优化手段完全不同(压缩靠策略、上传靠并发)。只给总耗时无法说明你知道时间花在哪。

    不要报什么:不要报「上传成功率 99.9%」——弱网下不可能,而且这个数字取决于用户网络而不是你的代码。该报的是「同样网络条件下的对比」。也不要报「压缩率 70%」——取决于原图质量,且掩盖了细节图的问题。

    面试追问
    Q:为什么不全并发上传?现在网络都很快。 A:因为并发在网络好的时候是优化,在网络差的时候是灾难。我们真实遇到的:用户在地铁里一次传十张,全并发之后带宽被瓜分,每一条都慢到超时,最后一张都没成功;而如果限制并发数,至少能陆续成几张,剩下的重试。关键认知是:并发的收益上限是带宽,而带宽是固定的;但并发的代价——每条连接都可能因为过慢而超时——是随并发数放大的。所以在带宽受限时,降低并发反而提升总吞吐。至于为什么不按网络类型动态调并发:网络类型 API 的准确性有限,显示 4G 实际很慢的情况很常见,判断错了比固定值更糟。我选了一个在弱网下也能工作的小并发数,牺牲好网下的一点速度,换所有网络条件下都可用。这类「保下限」的取舍在客户端很常见,因为你无法选择用户的环境。
    Q:压缩为什么不用固定质量参数?那样最简单。 A:因为固定质量对不同内容的效果差别很大,而我踩过这个坑:普通照片压得很好,但用户上传的商品瑕疵凭证(衣服上的一个线头、包装上的一行小字)压完就看不清了。后果不是「图不好看」,是客服无法判定售后责任,引发纠纷。所以改成按「最大边长 + 目标体积」——先缩放到合理尺寸,再逐步降质量直到进入目标体积,这样不同内容都能得到可接受的结果。另外还加了两条:本来就小的图跳过压缩(二次编码只会变模糊),以及给用户一个「原图上传」的选项用于确实需要清晰度的场景。这件事的通用教训是:一个参数对所有输入都合适,通常意味着你还没见过足够多样的输入。
    Q:上传凭证由服务端签发,那不是多一次请求吗?直接把密钥配在客户端不行? A:绝对不行,这是安全底线。客户端代码在用户手上,任何打进包里的东西都要假设它是公开的——反编译能拿到,抓包也能拿到。如果配的是存储服务的长期密钥,等于把整个存储桶的写权限公开了:别人可以往里传任意文件、可以覆盖已有文件、可以把你的存储当免费图床刷爆流量。我在代码评审时被指出这一点,之前完全没意识到。正确做法是每次上传前向服务端换一个限时(分钟级)、限定路径前缀、限定操作类型的令牌,这样即使令牌泄露,能造成的损害也被限定在很小的范围和很短的时间内。至于多一次请求的成本:可以把取凭证和业务的其他初始化请求合并,或者一次取一批(够本次上传用),而且这个成本和风险完全不在一个量级上,不构成取舍。「客户端没有秘密」这个原则我现在做任何涉及密钥的事情都会先想一遍。

模块二:收货地址与地图选点

  1. 收货地址与地图选点(权限拒绝必须有出路 + 请求时机后置 + 逆地理结果需人工补全 + 地址结构化存储)★★
    简历这样写 收货地址与地图选点(uni-app + 定位与地图能力抽象 + 权限状态机 + 地址结构化):把定位权限当成用户的选择而非可依赖的能力——权限请求时机后置到用户主动点「定位」时而非进页面即弹,被拒绝或不可用时提供手动选择城市与手动填写的完整路径,功能不因权限缺失而不可用;逆地理编码结果只用于预填省市区与参考位置,门牌号强制由用户补全(坐标能定位到楼栏但定位不到几号房,直接当地址用会造成配送失败);地址按省市区编码加详细地址结构化存储而非单一字符串,便于运费与可达性判断;定位与地图组件的平台差异收敛到能力抽象层。改造后因地址不完整导致的配送异常反馈明显下降。
    展开完整拆解
    为什么要这么设计

    第一版的地址页很朴素:进页面就请求定位、拿到坐标做逆地理编码、把返回的地址字符串填进输入框、用户点保存。看着挺智能,四个问题都出在「我把权限和定位结果当成了可靠的东西」。

    一是一进页面就弹权限,用户直接拒绝。他还不知道你要干什么就被弹窗打断,第一反应是点「拒绝」。而系统权限被拒绝一次之后,再想请求就弹不出来了(要引导他去系统设置里改),相当于永久失去了这个能力。这是最典型的时机错误。

    二是权限被拒绝之后功能全废。我的代码只写了「拿到定位 → 填地址」这一条路径。用户拒绝之后,页面就一直转圈或者一片空白,他压根没法填地址,只能退出去。这个 bug 影响的是「所有拒绝过定位的用户」,比例不低。

    三是逆地理编码的地址直接用,配送出问题。返回的是「某市某区某路 123 号」——这是坐标所在的位置,但用户住在 3 号楼 502 室,而这部分坐标里没有。我把它当完整地址存了,配送员到了楼下找不到人。坐标能告诉你「在哪个楼栏」,但告诉不了「几号房」,这个差距就是配送成功与失败的差距。

    四是地址存成一个字符串,后面全是麻烦。运费要按省份算、有些商品有区域限售、仓库要按区域分配——这些都需要结构化的省市区,从一个字符串里去解析是不可靠的(用户可能自己改过、可能少写了市)。

    所以四个改动:权限请求时机后置(用户点「定位」才请求,此时他知道为什么要权限)、权限被拒绝有完整的替代路径(手动选城市 + 手动填地址)、逆地理结果只做预填、门牌号强制补全地址按省市区编码加详细地址结构化存储

    这个模块最想说的一句话是:权限不是能力,是用户的选择。所以任何依赖权限的功能,都必须有一条不依赖权限的路径——不是降级体验,是要能完成同一件事。用户拒绝定位之后照样要能填地址下单,这不是可选项。

    整体链路
    权限状态机(四种状态,每种都有出路) │ ├─ 未请求过 → 不主动弹,等用户点「定位到当前位置」 │ 进页面即弹的结果是被直接拒绝,而拒绝后再难请求 │ ├─ 已授权 → 走定位链路 │ ├─ 已拒绝 → 不再重复请求(弹不出来),显示「手动选择城市」 │ 并给一个「如何开启定位」的引导入口(跳系统设置) │ └─ 不可用 → 系统定位关闭 / 平台不支持 / 超时 一律走手动路径,不要卡在加载态 手动路径必须能完成同一件事(不是降级体验) ├─ 三级联动选择省市区(数据本地内置或首屏预取) ├─ 详细地址手动输入 └─ 这条路径要能独立走完并保存,不依赖任何定位能力 定位链路 │ ├─ 请求定位(带超时,超时即转手动,不要一直转圈) │ ├─ 拿到坐标 → 逆地理编码 → 得到省市区 + 参考位置 │ ├─ 预填省市区(可改)+ 参考位置填入详细地址的前半部分 │ └─ 门牌号部分留空并聚焦,提示「请补充楼栋与门牌号」 坐标只能定位到楼栏,补全靠用户 —— 这一步不能省 地图选点(可选增强,不是必需路径) ├─ 在地图上拖动选点 → 逆地理 → 同样只做预填 ├─ 地图组件各端差异(微信 map 组件 / App 端)收敛到抽象层 └─ 地图加载失败要能跳过,不阻塞填地址 保存前校验 │ ├─ 结构化字段必填:省 / 市 / 区(存编码不只存名称) ├─ 详细地址长度与内容校验:太短的(少于阈值)提示可能不完整 ├─ 手机号格式按国家/地区规则校验(跨境场景多国号码格式不同) └─ 可达性校验:调服务端确认该区域是否可配送(不可达要在保存前说) 存储结构(不存单一字符串) ├─ 省市区编码 + 名称快照(编码用于计算,名称快照用于展示历史) ├─ 详细地址(用户填写部分) ├─ 坐标(用于配送侧参考,不作为地址主体) └─ 收件人 / 手机号 / 是否默认 默认地址 ├─ 首个地址自动设为默认 ├─ 删除默认地址时要自动指定新的默认(否则下单页没有地址可选) └─ 设为默认的操作要幂等(连点两次不该产生两个默认)
    分步拆解
    1. 权限请求时机必须后置到用户主动触发。进页面就弹,用户不知道为什么要给,第一反应是拒绝;而系统权限被拒绝一次之后就很难再请求了(需要引导去系统设置)。正确做法是先展示「定位到当前位置」按钮,用户点了才请求——此时他清楚地知道权限用来干什么,同意率会明显高。
    2. 权限被拒绝之后不要反复请求。系统不会再弹,你的请求只是静默失败。要做的是切换到手动路径并给一个「如何开启」的引导(能跳系统设置就跳)。
    3. 手动路径必须能独立完成整件事,不是降级体验。用户拒绝定位之后照样要能填完地址下单。这一条是这个模块的底线——如果手动路径不完整,那就是「用户不给权限就不能买东西」,业务上不可接受。
    4. 定位要带超时,超时就转手动。室内、地下室、隧道里定位可能一直拿不到结果。没有超时的话页面就一直转圈,用户不知道该等还是该走。超时后明确说「定位超时,请手动选择」。
    5. 逆地理编码的结果只做预填,门牌号强制由用户补全。这是这个模块最容易做错的一条。坐标能定位到「哪个楼栏」,定位不到「几号房」,直接当完整地址存下去,配送员到了楼下找不到人。做法是把逆地理的结果填入详细地址的前半部分,光标停在后面并提示「请补充楼栋与门牌号」。
    6. 省市区要预填但允许修改。逆地理有时会把边界地区判错(尤其是行政区划调整过的地方)。预填是为了省事,不是为了锁定。
    7. 省市区要存编码,不只存名称。运费计算、区域限售、仓库分配都依赖编码。名称会变(行政区划调整、改名),编码相对稳定。同时存一份名称快照用于展示历史地址——否则区划改名之后,用户看到自己的旧地址变成了另一个名字会困惑。
    8. 详细地址要做长度和完整性提示。只填「XX 路」这种明显不完整的,在保存前提示「地址似乎不完整,是否补充楼栋门牌」。注意是提示不是拦截——有些地址确实很短(比如某个知名建筑)。
    9. 手机号校验要按国家地区规则。跨境场景下各国号码位数和格式不同,用一套国内规则去校验会把国际号码全判为非法。做法是按选择的国家应用对应规则,规则不确定时放宽(只校验长度范围)。
    10. 可达性校验要在保存前做。调服务端确认该区域能不能配送。不做的话用户保存了地址、加了购物车、下单时才被告知「该区域不支持配送」,那时候他已经付出了很多操作。
    11. 地图选点是增强不是必需,加载失败要能跳过。地图组件较重且各端差异大,它不该成为填地址的阻塞环节。加载失败时隐藏地图区域,手动填写照常可用。
    12. 删除默认地址时要自动指定新的默认。否则用户的下单页会出现「没有可选地址」,而他明明有其他地址。这个 bug 很常见且很影响转化。
    13. 设为默认要幂等。连点两次不该产生两个默认地址。服务端要保证同一用户只有一个默认(唯一约束或者更新时先清旧的)。
    14. 平台差异收敛到能力抽象层。定位 API、地图组件、权限查询方式各端不同。业务代码里不该出现条件编译,只调统一接口。
    关键决策与取舍

    权限请求后置的代价是「用户可能压根不点那个按钮」,也就是定位功能的使用率会下降。进页面就弹的话,一部分用户会顺手同意。但我认为这个取舍是对的:顺手同意的用户里有相当比例是被打断后随便点的,而被拒绝的代价是永久性的(系统不再弹窗)。后置之后同意的人是真的想用这个功能,而且没同意的人也能通过手动路径完成任务,业务上没有损失。判据是:这个权限是「锦上添花」还是「功能前提」——填地址属于前者(手动能填),所以可以后置;如果是扫码支付这种没有摄像头权限就完全做不了的,那就该在用户明确要扫码时请求,并把「为什么需要」说清楚。

    门牌号强制补全 vs 直接用逆地理结果,选强制补全,接受多一步操作。直接用更「智能」、少一步操作,但它产出的地址有相当比例是不可配送的。而配送失败的代价(重新联系、二次配送、用户投诉)远大于让用户多打几个字。缓解办法是把光标自动停在需要补充的位置并给出示例(「如 3 号楼 502 室」),让这一步尽量轻

    地址存结构化字段而不是单一字符串,代价是表结构和表单都更复杂。单字符串最简单,但运费、限售、仓储分配都需要行政区划。从字符串解析行政区划是不可靠的——用户可能自己改过、可能少写了市、可能用了简称。判据是「下游有没有系统需要按结构化字段做计算」,有就必须结构化,这个成本躲不过去。

    踩过的坑一:进页面就请求权限,被拒绝率高得离谱。而且更糟的是我当时以为「拒绝了下次再问」,实际上系统不会再弹——这个用户永久失去了定位能力,除非他自己去系统设置里改。这件事让我理解了权限请求是「一次性机会」,所以时机和上下文比什么都重要:要在用户明确想要这个能力的那一刻请求。

    踩过的坑二:权限被拒绝之后页面卡在加载态。我只写了成功路径,拒绝时那个 Promise 既不 resolve 也没被 catch,页面就一直转圈。用户以为是网络问题,反复刷新。教训是:任何依赖系统能力的调用,都要显式处理「用户拒绝」这个分支,而它不是异常,是正常的业务分支。我后来把权限做成了一个显式的状态机(未请求 / 已授权 / 已拒绝 / 不可用),四种状态各有 UI,就不会漏了。

    踩过的坑三:把逆地理的地址当完整地址存,配送失败。客服反馈「配送员找不到收件人」,查出来是一批地址都只到楼栏级别。这个坑的教训是要理解每个数据源能提供什么精度——坐标的精度是米级,能定位到楼,但「几号房」这个信息在物理世界里就不在坐标系统里,指望它给出来是搞错了数据的能力边界。

    踩过的坑四:删除默认地址后没有指定新默认,下单页没地址可选。用户明明有三个地址,下单页显示「请添加收货地址」。这类问题的通用模式是「删除一个有特殊角色的记录时,那个角色要有人接手」——默认地址、主账号、主图都是同一类问题。我后来遇到类似场景都会先问一句「删了之后这个角色归谁」。

    没做的部分:没做地址智能解析(用户粘贴一整段文字,自动拆出省市区和手机号)。这个功能体验很好而且技术上可行,但解析的准确率不可能到 100%,而解析错了用户不一定会检查——错误的地址比让他自己填更糟。如果要做,我的方案是解析后必须让用户逐字段确认,而不是直接填入并保存

    数字是怎么测的

    权限同意率:埋点统计「请求权限的次数」与「同意的次数」,对比时机后置前后。要说清分母是什么——后置之后请求次数本身大幅下降(只有点按钮的人才触发),所以同意率上升是自然的,不能只报这个比例。更有意义的指标是「最终成功保存地址的用户比例」,它衡量的是整个流程的通过率,不受权限请求次数变化的影响。这一点必须主动说明,否则用同意率来证明改进是有误导性的。

    权限四种状态的覆盖用手工验证清单:未请求、已授权、已拒绝、系统定位关闭,四种状态各走一遍,断言每种都能通过手动路径完成保存。这是断言型的验证,报「四种状态全部有可用路径」而不是百分比。「已拒绝」这个状态要在真机上测——模拟器的权限行为和真机不一致。

    地址完整性:「详细地址中包含楼栋或门牌信息的比例」,用规则粗略判定(是否含数字加单位)。要承认这个判定是粗略的。改造前后对比这个比例,它是「门牌号强制补全」这个改动最直接的证据

    配送异常反馈:客服工单里「地址不详无法配送」这一类的绝对数量变化。用绝对数不用比率,因为订单量本身在变。这个数字是业务侧的,要说清是从客服系统的工单分类里取的,不是自己估的。

    定位超时的阈值怎么定:统计真实环境下定位耗时的分布(室内、室外分别测),取一个覆盖大部分成功情况的值作为超时。要说清这个值是测出来的而不是拍的,以及室内定位明显更慢这个事实。

    不要报什么:不要报「定位准确率」——那是系统和地图服务的能力,不是你的代码决定的。不要报「地址填写成功率 99%」——如果分母是「进入地址页的人」,这个数字包含了大量中途放弃的正常行为。该报的是「四种权限状态都有可用路径」和「地址完整性比例的变化」。

    面试追问
    Q:为什么不进页面就请求定位?用户同意了体验更好啊。 A:因为权限请求是一次性机会,用错了是永久损失。进页面就弹,用户还不知道你要干什么就被打断,第一反应是点拒绝;而系统权限被拒绝之后,你的后续请求会静默失败,弹窗压根不出现——这个用户就永久失去了定位能力,除非他自己去系统设置里改,而几乎没人会去。我们真实的数据是后置之前拒绝率高得离谱。后置之后,点「定位到当前位置」的用户是明确想用这个功能的,同意率高得多。判据我总结成:这个权限是「锦上添花」还是「功能前提」。填地址属于前者——手动也能填完,所以可以后置,而且必须保证手动路径完整。如果是扫码这种没权限就完全做不了的,那也不该进页面就弹,而是在用户点「扫码」的那一刻请求,并且在请求前用一句话说明为什么需要核心原则是:在用户已经理解「为什么需要」的时刻请求。
    Q:逆地理编码已经返回了详细地址,为什么还要用户补门牌号? A:因为坐标和地址的精度边界不同。逆地理编码回答的是「这个坐标在哪」,它能定位到街道和楼栏;但配送需要的是「用户住在哪」,这里面的「3 号楼 502 室」在物理世界里就不存在于坐标系统中——同一个坐标点上下有几十户人家。指望逆地理给出门牌号,是搞错了这个数据源的能力边界。我们真实的后果是客服反馈「配送员找不到收件人」,查出来一批地址都只到楼栏级别。所以现在的做法是逆地理结果只填详细地址的前半部分,光标自动停在后面并给出示例提示「如 3 号楼 502 室」——让补全这一步尽量轻,但不能省。这件事的通用教训是:用任何数据源之前先问「它能提供什么精度」,不要因为它返回了一个看起来完整的字符串就假设它是完整的。
    Q:地址存一个完整字符串多简单,为什么要拆成省市区编码? A:因为下游有系统需要按行政区划做计算:运费按省份分档、部分商品有区域限售、仓库要按区域就近分配、跨境场景还要判断是否在可清关范围内。这些都需要确定的行政区划,而从一个用户手写的字符串里解析行政区划是不可靠的——他可能少写了市、可能用简称、可能自己改过顺序。判据是:下游有没有系统需要按结构化字段做计算,有就必须结构化,这个成本躲不过去。另外还有一个容易忽略的点:要同时存编码和名称快照。编码用于计算(相对稳定),名称快照用于展示历史地址——因为行政区划会调整会改名,如果只存编码、展示时实时查名称,用户会发现自己三年前的旧地址变成了另一个区名,会以为地址被改了。这一条是我们后来补上的。

模块三:评价发布与草稿

  1. 评价发布与草稿(草稿自动保存与提交后清理 + 幂等防重复提交 + 前置提示不替代服务端校验 + 失败保住输入)★★
    简历这样写 商品评价发布(uni-app + 本地草稿 + 幂等提交 + 图文混排表单):以「用户输入过的内容一个字都不能丢」为第一原则——草稿按订单与商品维度自动保存到本地,切后台、被杀进程、提交失败后均可恢复,提交成功则立即清理(不清会导致用户下次进来看到旧草稿并重复发布);防重复提交不依赖按钮禁用(切后台回来页面状态可能重置),而以提交中状态加客户端生成的幂等键为准,服务端按幂等键去重;敏感词与字数在客户端做前置提示以提升体验,但明确服务端校验才是防护;图文混排的字数统计与图片数量分别计量,超限提示定位到具体项。上线后重复评价与「写了半天丢失」两类反馈不再出现。
    展开完整拆解
    为什么要这么设计

    评价发布看起来就是一个表单加几张图,第一版两天做完。但它踩的坑几乎都在「用户不是一次性顺利填完」这件事上。

    一是写了半天丢了。用户写了两百字的评价、传了五张图,中途被电话打断切到后台,回来页面已经被系统回收重建,内容全空。他的反应是「这什么破 App」,然后不写了。而评价对电商是很重要的内容资产,流失一条就是流失一条。

    二是重复发布。提交时网络慢,用户以为没点上,又点了一次。结果发出两条一样的评价。我加了按钮禁用,但用户切后台再回来,页面状态重建,按钮又可点了——禁用只是个视觉状态,它挡不住真实的重复请求。

    三是草稿没清导致的怪问题。加了草稿功能之后出现新问题:用户成功发布了评价,下次进这个页面又看到了上次的草稿内容,他以为没发成功,又发了一次。草稿功能反而制造了重复发布。

    四是敏感词拦截的位置错了。我只在客户端做了敏感词过滤,觉得够了。后来发现有人直接调接口发布违规内容——客户端校验对绕过前端的人完全无效。

    所以四个改动:草稿自动保存并在成功后立即清理防重复用「提交中状态 + 幂等键」而不是按钮禁用客户端校验只做前置提示、服务端校验才是防护提交失败时草稿必须保住

    这个模块最想说的一句话是:表单类功能的第一原则是「用户输入过的内容一个字都不能丢」。其他都可以商量,这一条不行——因为输入是用户付出的成本,丢了就是白费他的时间,而这件事对体验的伤害远大于任何加载慢。把这条当成硬约束之后,草稿、失败恢复、切后台保护这些需求自然就出来了。

    整体链路
    草稿(按订单与商品维度存,不是全局一份) │ ├─ 键 = 用户ID + 订单号 + 商品ID │ 用户可能同时给一个订单里的多个商品写评价 │ 全局一份草稿会互相覆盖 │ ├─ 触发保存:输入防抖后保存 + 切后台立即保存 + 页面卸载保存 │ 切后台那次最关键 —— 系统可能之后就把页面回收了 │ ├─ 存的内容:文字 + 已上传成功的图片地址 + 评分 + 时间戳 │ 未上传成功的图不存(临时路径会失效,存了也恢复不出来) │ ├─ 进页面时检测草稿 → 有则询问「是否恢复上次未完成的评价」 │ 询问而不是静默填入:用户可能想重新写 │ └─ 草稿有过期时间,过久的自动清理(避免用户看到很旧的内容) 提交(防重复靠状态与幂等键,不靠按钮禁用) │ ├─ 客户端生成幂等键:订单号 + 商品ID + 本次编辑会话ID │ 在进入页面(或恢复草稿)时生成一次,整个编辑期不变 │ 重试用同一个键 → 服务端能识别是同一次提交 │ ├─ 提交中状态:内存标记 + 按钮 loading │ 按钮禁用只是视觉,切后台回来状态可能重置 │ 所以真正的判据是幂等键 —— 服务端去重才是可靠的 │ ├─ 提交前校验(前置提示,不是防护): │ 字数下限 / 图片数量上限 / 敏感词提示 / 有失败图片的询问 │ ├─ 服务端:按幂等键去重 + 完整的内容校验(敏感词、频次、资格) │ 客户端校验可被绕过,服务端是唯一可靠的关口 │ └─ 返回结果分类处理: 成功 → 立即清草稿 → 跳转成功页 幂等命中 → 视为成功(说明上一次其实成了)→ 同样清草稿 内容被拒 → 保住草稿 + 明确告知哪部分不合规 网络失败 → 保住草稿 + 可重试(用同一幂等键) 图文混排的计量 ├─ 文字与图片分别计量,不混成一个「内容长度」 ├─ 字数按有效字符算(去掉首尾空白与连续空行) └─ 超限提示要定位:「文字超出 30 字」「图片超出 3 张(第 7-9 张)」 失败与中断 ├─ 提交失败绝不清草稿(这是最容易写错的地方) ├─ 页面卸载前若有未保存改动 → 先保存再走 └─ 恢复草稿后重新生成编辑会话ID?不 —— 沿用原键,避免重复发布
    分步拆解
    1. 草稿键要带订单和商品维度。一个订单里可能有多个商品要分别评价,用全局一份草稿会互相覆盖——用户给 A 商品写了一半去给 B 商品写,回来发现 A 的内容变成了 B 的。
    2. 切后台时必须立即保存,这是最关键的一次保存。因为系统之后可能就把页面回收了,没有第二次机会。输入防抖保存是常规手段,但切后台这个时机不能漏。同样页面卸载前也要保存。
    3. 未上传成功的图片不要存进草稿。它们只有临时路径,而临时路径会失效,存了也恢复不出来,反而会在恢复时显示一堆裂图。只存已经上传成功、拿到正式地址的图。
    4. 恢复草稿要询问,不要静默填入。用户可能想重新写。静默填入会让他困惑「这些字是哪来的」。询问的成本是一次弹窗,收益是用户始终清楚发生了什么。
    5. 草稿要有过期时间。三个月前写了一半的评价,现在恢复出来对用户没有意义甚至会困惑。设一个合理的过期期限自动清理。
    6. 提交成功后必须立即清草稿,这一条不清会制造重复发布。我们踩过:用户成功发布了,下次进来又看到草稿,以为没发成功,又发一次。草稿功能本身反而成了重复发布的来源,这个因果关系很反直觉。
    7. 防重复提交不能只靠按钮禁用。禁用是视觉状态,切后台再回来页面状态可能被重建,按钮又可点。真正可靠的是客户端生成幂等键、服务端按键去重——这样即使发出了两个请求,也只会产生一条评价。
    8. 幂等键要在进入编辑时生成一次并保持不变,重试要用同一个键。如果每次点提交都生成新键,那幂等就失效了。键的语义是「这一次编辑成果」,不是「这一次点击」。
    9. 幂等命中要视为成功,不是报错。服务端发现这个键已经提交过了,应该返回「成功」(并带上已有的评价 ID),因为对用户来说他的目的达成了。返回「重复提交」会让他困惑「到底成没成」。而且这时也要清草稿。
    10. 提交失败绝不清草稿,这是最容易写错的地方。清草稿的代码要严格只放在成功分支(含幂等命中),不能放在 finally 里——放在 finally 里意味着无论成功失败都清,用户的内容就在失败时被清掉了。这类错误很隐蔽,因为正常路径下表现完全正常。
    11. 客户端的敏感词和字数校验是前置提示,服务端校验才是防护。这一条和后台富文本那边是同一个原则。客户端校验能被绕过(直接调接口),所以它的价值只在于让用户尽早知道问题,不能替代服务端。
    12. 文字和图片分别计量,不要合成一个「内容长度」。用户需要知道是字太多还是图太多。超限提示要定位到具体项:「文字超出 30 字」「图片超出 3 张(第 7 到 9 张)」,而不是笼统的「内容超限」。
    13. 字数按有效字符算。去掉首尾空白和连续空行再计数,否则用户敲了一堆回车就能凑够字数下限,而这类评价对其他买家没有价值。
    14. 有上传失败的图片时,提交前要明确询问。用户可能没注意到有一张失败了。静默提交会让他的评价缺一张关键凭证(尤其是差评时的证据图)。
    关键决策与取舍

    草稿存本地而不是存服务端,是按「这个场景需不需要跨设备」判断的。存服务端的好处是换手机也能恢复,但评价这个场景用户几乎不会换设备继续写,而存服务端要多一套接口、要处理草稿的并发与清理、还涉及未发布内容的合规存储问题。本地存储的代价是清缓存或换设备会丢,但这个场景下概率很低。如果是长表单(比如商家入驻资料,用户可能填几天),那就该存服务端。

    恢复草稿选择「询问」而不是「静默填入」,接受多一次交互。静默填入更顺滑,但用户会困惑内容是哪来的,而且他可能本来想重新写。询问让状态始终对用户透明。这里有个细节:询问的文案要说清是什么时候的草稿(「你有一份 2 小时前未完成的评价」),时间信息能帮他判断要不要恢复。

    幂等键由客户端生成而不是先向服务端申请。先申请更「标准」(服务端可以做更多控制),但它多一次网络往返,而且在弱网下这次往返本身就可能失败,反而增加了失败面。客户端生成的代价是键的唯一性依赖客户端逻辑正确——所以键的构成要包含足够的业务维度(订单号 + 商品 ID + 编辑会话),而不是用一个纯随机数(纯随机的话每次编辑都是新键,起不到防重作用)。

    踩过的坑一:清草稿的代码放在了 finally 里。正常路径下完全正常,但提交失败时草稿也被清了——用户写了两百字,网络失败,内容全没。他重写一遍,又失败,又没了。这个 bug 是「异常路径上的正确性」问题,功能测试完全测不出来,因为测试时网络都是好的。我后来的习惯是:凡是涉及清理用户数据的代码,一定要明确它在哪个分支,绝不放在 finally 或者公共出口里。

    踩过的坑二:加了草稿之后反而出现重复发布。成功提交没清草稿,用户下次进来看到旧内容以为没发成,又发一次。这个因果很反直觉——一个用来提升体验的功能,因为缺了一步清理,制造了新的业务问题。教训是新增任何有状态的功能,都要把它的完整生命周期走一遍:创建、更新、成功后、失败后、过期后,每个环节都要有明确行为。

    踩过的坑三:只在客户端做敏感词过滤。有人直接调接口发布违规内容。我当时的想法是「用户又不会自己调接口」,但这个假设对恶意用户不成立。修法之外,更重要的是理解客户端校验和服务端校验的职责根本不同:前者为体验(尽早告知),后者为防护(唯一不可绕过)。

    踩过的坑四:按钮禁用挡不住重复提交。用户点提交、网络慢、切到微信回消息、回来页面被重建、按钮恢复可点、他又点了一次。禁用是视觉层的,页面重建之后就没了。这件事让我理解了「防重复必须在服务端有最终判据」——客户端的所有措施都只是减少概率。

    没做的部分:没做草稿的多设备同步和「继续在电脑上写」。评价场景用不上。也没做富文本(评价只支持纯文字加图片),因为评价内容会展示给所有买家,富文本会带来和后台商品详情一样的清洗和 XSS 问题,而评价压根不需要格式——这是主动选择不引入复杂度,不是没能力做。

    数字是怎么测的

    「写了半天丢失」这类问题的验证用场景清单,逐个手工走:切后台再回来、切后台被系统回收后回来(可以用开发者选项限制后台进程来模拟)、提交失败后、页面被返回手势关闭后。四个场景各断言内容能恢复。报法是「四个中断场景全部可恢复」而不是百分比。其中「被系统回收」这个场景必须真机模拟,模拟器不会真的回收页面。

    重复提交的验证要构造慢请求:把提交接口延迟数秒,在等待期间反复点提交、以及切后台再回来点提交,断言服务端只产生一条评价不构造延迟测不出来,因为本地网络太快,第二次点击时第一次已经返回了。这一点必须说明。

    草稿清理的验证要正反两面:提交成功后断言草稿已清(再进页面不提示恢复);提交失败后断言草稿还在。后一条是我踩过的坑,所以必须有用例守着。

    「重复评价反馈不再出现」:客服工单里这一类的绝对数量变化,说明是从工单分类取的。用绝对数不用比率。

    字数统计的验证:造包含首尾空白、连续空行、表情符号、多语言字符的输入,断言计数符合预期。表情和多语言字符要特别测——它们在不同平台的字符长度计算可能不一致(代理对问题),而跨境业务里用户会用各种语言写评价。

    不要报什么:不要报「草稿保存成功率 100%」——存储写入可能失败(容量满),而且这个数字没有测量意义。该报的是「四个中断场景可恢复」和「构造慢请求下只产生一条评价」这两个可验证的行为。

    面试追问
    Q:防重复提交,把按钮禁用掉不就行了? A:不行,我们真实踩过。按钮禁用是视觉层的状态,而客户端的页面状态是不可靠的——用户点了提交、网络慢、切到微信回消息、回来时页面已经被系统回收并重建,按钮恢复成可点状态,他又点了一次。结果发出两条评价。禁用能挡住「快速连点」,挡不住「页面状态被重置」。所以真正的判据必须在服务端:客户端生成一个幂等键(订单号 + 商品 ID + 本次编辑会话),整个编辑期保持不变、重试也用同一个键,服务端按键去重。这样即使发出两个请求,也只产生一条评价。而且要注意幂等命中时服务端该返回「成功」而不是「重复提交」——对用户来说他的目的达成了,返回错误会让他以为没发成又去重试。这件事的通用认知是:任何客户端的防护措施都只能降低概率,最终判据必须在服务端。
    Q:加了草稿功能反而出现重复发布,这是怎么发生的? A:因为提交成功之后我没清草稿。用户成功发布了评价,下次进这个页面,草稿还在,系统提示「是否恢复上次未完成的评价」——他以为上次没发成功,就恢复并又发了一次。第二次被幂等挡住还好,但当时幂等还没做,所以真的发出了两条。这个因果关系很反直觉:一个用来「防止内容丢失」的功能,因为缺了一步清理,制造了「内容重复」的新问题。它给我的教训是新增任何有状态的功能,都要把生命周期完整走一遍:创建、更新、成功后、失败后、过期后,每个环节的行为都要明确定义。我当时只想了「怎么存」和「怎么恢复」,漏掉了「什么时候该消失」。顺带说一个相关的坑:清草稿的代码不能放在 finally 里——那样提交失败时草稿也被清了,用户的内容在最需要保住的时候丢了,而这个 bug 在正常网络下完全测不出来。
    Q:这三个模块(上传、地址、评价)看起来都是些小功能,你觉得它们有什么共同点值得讲? A:有一条很明确的共同点:它们的技术含量都来自「我的设备和用户的设备不一样、我的网络和用户的网络不一样、我的操作和用户的操作不一样」。上传那块是我在旗舰机好网下测没问题,用户在低端机弱网下全崩;地址那块是我假设用户会给定位权限,实际上很多人拒绝;评价那块是我假设用户一次性顺利填完,实际上他会被电话打断、会切后台、会重复点提交。三个模块的所有工作量,都在处理「用户的真实环境和真实行为」。另一条共同点是客户端的所有校验和状态都不可靠,最终判据必须在服务端——上传凭证要服务端签发、敏感词要服务端校验、防重复要服务端按幂等键去重。这两条是我在这段实习里最主要的收获,它们比任何具体 API 的用法都更能迁移到别的功能上。

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

项目拆解 · 商城客户端基础能力(uni-app · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据