资质审核第一版的实现很直接:商家上传几张图、状态置为「待审核」、运营点「通过」或「驳回」。功能测试全过,上线之后四个问题陆续暴露,而且都不是功能 bug。
一是驳回之后商家反复提交。驳回原因是一个自由文本框,运营写「材料不清晰」。商家不知道是哪一张不清晰,只能把全部材料重拍一遍再交。再被驳回,再重拍。有的商家来回提交了七八次——每一次都要占用一次审核人力。
二是同一张营业执照被多个门店重复使用。我完全没做证件号的唯一性校验。结果是有人用一张执照开了十几家门店,其中一部分是明显的违规操作。这个问题是风控同学发现的,不是我们自己发现的。
三是资质过期完全没人管。营业执照、食品经营许可证都有有效期,我把有效期存下来了,但没有任何逻辑去检查它。上线几个月后有一批门店的许可证已经过期,还在正常接单——这是合规风险。问题的本质是:到期这件事没有任何用户操作会触发它,商家不会主动来说「我的证过期了」。
四是资质材料的访问没有任何控制。图片直接存在公开可读的地址上,拿到链接的人都能看到营业执照、法人身份证。而且谁看过、什么时候看的,完全没有记录。
所以四个改动:状态收敛成显式状态机、驳回原因结构化到字段级、证件号唯一性校验加冲突转人工、到期前分级提醒加到期后降级、材料改私有存储加时效凭证并记录访问。
这个模块最想说的一句话是:「时间到了状态应该变化」这类需求,是功能测试永远发现不了的。提交、审核、驳回这些都有用户操作来触发,测试点一遍就能覆盖;而「有效期到了要处理」没有任何操作会触发它,只有靠定时任务主动去扫。我漏掉它不是因为技术难,是因为我的思维停留在「用户会做什么」,没想到「时间会做什么」。
if 判断会让状态变成不可枚举的组合,后面加「即将到期」「已过期」时会到处漏改。到期后选「降级」而不是「直接下线」。直接下线在合规上最干净,但它的误伤代价很高:有效期是人工录入或识别出来的,录错一位数字就会让一家正常营业的门店突然停业,而门店的损失是实打实的营业额。降级(限制接单、隐藏门店、保留登录与申诉)把误伤的代价压到可挽回的范围。判据是「两类错误的代价对比」:让过期门店多营业几小时的风险,小于错误停业一家正常门店的损失。但这个判断有边界——如果是食品安全类的强制许可,我们的处理会更严格,因为那类风险的性质不同。
证件号冲突选「转人工」而不是「直接拒绝」。直接拒绝实现简单,但会挡住连锁品牌这类合法的一照多店场景。转人工的代价是增加了审核工作量。判据是「这个规则的例外是否合法且常见」——一照多店在连锁场景下既合法又常见,所以不能做成硬规则。这一条我总结成:唯一性约束在业务上很少是绝对的,做成「检测并提示」比做成「强制拒绝」更安全。
驳回原因结构化的代价是运营填写麻烦了一点(要选字段、选分类)。但收益有两层:一是商家能精确知道补什么,重复提交次数明显下降;二是原因可统计,能反向改进前端引导。而且运营的填写成本其实降低了——原来他要用文字描述清楚是哪张有什么问题,现在选两个下拉就够了。这类「看起来增加了步骤实际减少了工作量」的改动,说服使用方的关键是让他自己体验一遍。
踩过的坑一:完全漏掉了有效期检查这个需求。我把有效期字段存下来了,但没有任何逻辑去用它。功能测试全过(因为测试用例都是围绕用户操作设计的),上线几个月后才发现有一批门店的许可证已过期还在接单。这是我在这个项目里最大的教训:我的思维停留在「用户会做什么」,没想到「时间会做什么」。后来我给自己加了一个自检项:凡是数据里有「有效期」「截止时间」「过期时间」这类字段,就必须回答「谁来检查它、检查到了做什么」——存了字段却不用,等于埋了一个几个月后才爆的雷。
踩过的坑二:没做证件号唯一性校验,被人用一张执照开了十几家店。而且是风控同学发现的,不是我们。教训是:涉及身份、证件、资质这类数据,唯一性校验应该是默认动作,即使需求文档没写。需求文档写的是「支持上传资质」,但「资质」这个词本身就隐含了「它代表一个特定主体」的语义。
踩过的坑三:资质图片存在公开可读的地址上。是安全同学扫出来的。我当时的想法是「链接很长猜不到」——这是典型的错误认知,因为链接会出现在日志、浏览器历史、转发的截图里。修法是改私有存储加时效凭证。教训是「难以猜到」不等于「有访问控制」。
没做的部分:没做资质材料的自动识别(OCR 提取证件号与有效期)。它能减少人工录入错误(而录错有效期正是「直接下线」方案的主要风险来源),但需要接第三方识别服务、还要处理识别错误的兜底。我把它作为后续建议提了,并用「有效期录入错误的实际发生次数」作为依据。
重复提交次数是这个模块最有说服力的指标:统计改造前后「从首次提交到审核通过的平均提交次数」。要说清样本区间和样本量,并注意剔除审核标准调整带来的影响。这个数字直接对应「驳回原因结构化」的价值。
有效期治理的效果报「已过期但仍可接单的门店数」:改造前这个数字是未知的(没人在查),改造后应该是零或接近零。报法要诚实:改造前的数字是我上线扫描任务后第一次跑出来的存量,不是「一直有监控」。
提醒的有效性用「到期前完成续期的比例」衡量:分级提醒之后这个比例应该上升。要分渠道统计(横幅 / 短信 / 站内),这样能知道哪个渠道真正有效——我们的结论是后台横幅最有效。
证件号冲突检测:报「检出的冲突数」以及其中「经人工核查确认违规的比例」。后一个比例很重要——如果绝大多数冲突都是合法连锁,说明这个规则的提示阈值需要调整,否则只是给运营增加工作量。
状态机的正确性用断言型用例:枚举全部非法流转,断言都被拒绝。这类用例写起来机械但很有效,而且状态机改动后能直接回归。
访问控制的验证:断言未授权请求无法获取材料、凭证过期后失效、每次查看都产生了访问记录。第三条最容易漏测。
不要报什么:不要报「审核效率提升 N%」——审核耗时主要取决于运营的熟练度和积压量。该报的是「平均提交次数下降」「过期仍可接单门店数归零」「到期前续期比例与分渠道效果」「冲突检出数及违规确认比例」这几件可核对的事。
公告下发第一版是最简单的写法:运营在后台填标题正文、选一个范围、点发送,接口里查出符合条件的门店,循环给每家插一条站内消息。测试环境几十家门店,秒级完成,看起来完全没问题。
一是生产环境门店量大,接口直接超时。循环插入几万条,接口在网关层面就超时了。而且更糟的是——接口超时了但循环还在跑,运营不知道发到哪了,他又点了一次发送,于是前面那批门店收到了两条一样的公告。
二是中断后无法续跑。循环跑到一半服务重启(发版),已经发了的和没发的分不清。只能全部重发一遍,于是又是重复。
三是范围写错导致全量误发。运营想发给某个城市的餐饮门店,但品类条件没选上,结果发给了那个城市的全部门店。发出去就收不回来了——那时候还没有撤回功能。
四是公告没有时效。一个促销活动的公告,活动结束了公告还挂在商家后台首页。而「到期下架」这件事同样没有任何用户操作会触发它——运营发完就不会再回来了。
所以四个改动:任务化分批投递、投递加幂等键、下发前展示命中门店数与抽样名单、加生效失效时间并由定时任务上下架,另外补了撤回。
这个模块最想说的一句话是:「循环调用」这个写法在数据量小的时候是最简单最直接的选择,但它有三个必然的问题——超时、不可续跑、重复。而这三个问题只要量级上来就一定会遇到,不存在「我们量小所以不用管」——因为量级是会涨的,而涨到出问题的那一刻,你正在处理一次线上事故。
圈选结果固化快照 vs 投递时实时查询。实时查询的好处是「投递时状态最新」(比如投递期间新入驻的门店也能收到)。但它有一个致命问题:实际收到的和预览确认的不一致,那预览确认就白做了——而预览确认是拦住误发的关键环节。我选固化快照,代价是投递期间的新增门店收不到这次公告。判据是「预览确认的可信度比覆盖完整性更重要」,因为误发的代价(发给不该发的几万家)远大于漏发几家新店的代价(下次公告会覆盖到)。
幂等键的粒度选「公告 + 门店」而不是「请求」。按请求做幂等只能防「同一个请求重复提交」,防不住「运营点了两次发送」(那是两个不同的请求)。按「公告 + 门店」做,无论经过多少次重试、多少个请求,同一家门店对同一条公告只会收到一次。这个粒度的选择直接决定了幂等有没有用——我最初想按请求做,幸好在设计时就发现挡不住重复点击。
强制确认用顶部持续提示而不是阻断弹窗。阻断弹窗的「触达确定性」更高,但商家后台是他处理订单的工作台,阻断会直接影响他的经营——而且他会随手点掉确认(只为了继续工作),那个「确认」就变成了无意义的点击,反而削弱了确认记录的证明力。持续提示的触达率略低但确认是真实的。这个取舍我认为很关键:为了拿到一个「确认」而强迫用户点击,得到的确认是没有价值的。
踩过的坑一:同步循环导致接口超时,而超时后循环还在跑,运营又点了一次发送。结果前面那批门店收到两条一样的公告。这个坑同时暴露了三个问题:同步循环撑不住量级、没有幂等、运营看不到进度所以会重复操作。我最初只想修「超时」这一个问题(想着加大超时时间),后来才明白三个问题是连着的——不可观测导致重复操作、没有幂等导致重复操作变成重复数据。教训是:批量操作的「异步化 + 幂等 + 进度可见」是一套,缺一个都不行。
踩过的坑二:运营漏选了品类条件,公告发给了一个城市的全部门店。发出去收不回来(当时还没有撤回功能)。这件事推动了两个改动:下发前的命中数与抽样名单预览、以及撤回能力。其中抽样名单的作用超出预期——运营看到抽样里有明显不该收到的门店,立刻就发现条件错了,而单看「命中 3421 家」这个数字他判断不出对错。
踩过的坑三:公告没有失效时间,活动结束一个月了公告还挂在商家后台首页。是商家反馈的。和模块一的资质到期是同一类问题:到点该变的状态,没有任何用户操作会触发它。教训我总结成一句:凡是内容有「时效」概念的,就必须同时设计「谁在什么时候让它失效」。
没做的部分:没做公告的多语言与个性化变量(比如在正文里插入门店名)。个性化变量需要模板引擎和变量校验(变量名写错怎么办、取不到值怎么办),复杂度不低。而当前的公告基本都是通用内容,个性化需求主要来自「精准营销」场景,那属于另一个系统。
投递能力报「一次下发的门店量级与总耗时」,并说清分批大小与并发度。只报「支持全量下发」没有意义。要报「多少家门店、分多少批、总耗时多少、期间接口无超时」。
续跑能力是断言型用例:投递到一半强制中断(杀进程或模拟重启),断言重启后从游标继续、已投递的门店不重复收到。这条要靠打桩制造中断。
幂等性验证要覆盖三种重复来源:同一请求重试、消息重投、运营重复点击发送。断言三种情况下同一门店都只收到一条。第三种最容易漏测但正是我们踩的坑。
预览准确性:断言预览的命中数与实际投递的门店数一致(因为用了快照,应该完全一致)。如果不一致,说明快照机制没生效。
生效与失效:断言到达生效时间后公告出现、到达失效时间后不再展示但数据仍可查。要用可控的时间点测,不要等真实时间到。
撤回完整性:断言撤回后未投递部分停止、已投递部分在商家侧不再展示、已读记录仍保留。第三条容易被误删。
已读率的报法要注意口径:分母是投递成功数还是命中数?我们用投递成功数,并且单独报投递失败数——把两件事混在一个比例里会掩盖投递问题。
不要报什么:不要报「公告触达率 100%」——投递成功不等于商家看到了。该报的是「某量级下的下发总耗时与无超时」「中断后正确续跑且不重复」「三种重复来源都被幂等挡住」「预览数与实际投递数一致」这几件可核对的事。
评价功能第一版做得很轻:用户提交评价存一条记录,门店详情页查这家店的全部评价算平均分。四个问题,其中两个是正确性问题、两个是性能问题。
一是可以无消费刷评。我只校验了登录态,没有校验这个用户在这家店有没有真实的已完成订单。结果是有人批量注册账号给自己的店刷好评、给竞争对手刷差评。而且同一个订单可以反复评价——没有做一单一评的约束。
二是平均分实时算,门店列表变慢查询。门店列表要展示每家店的评分,我在列表查询里对每家店的评价表做聚合统计。门店量和评价量都涨起来之后,这个列表接口成了整个商家侧最慢的接口。而它是用户访问量最大的接口之一。
三是商家申诉恶意评价「没有效果」。审核通过后我把评价标记为隐藏,但门店的平均分没有变——因为平均分是实时算的,而我的隐藏逻辑漏了在统计里排除隐藏的评价。商家的感受是「申诉成功了但分数没变,等于没用」。
四是商家回复发出去就改不了。回复是公开可见的,有商家打错字了、或者情绪化回复之后想改。完全不能改太僵化,但完全可以随时改也有问题——用户看到的回复可能和他截图时的不一样,出纠纷时说不清。
所以四个改动:订单绑定校验加一单一评的唯一约束、评分聚合改增量维护、下架时聚合值同步回滚、回复给一个有限的可编辑窗口。
这个模块最想说的一句话是:「统计值」和「明细」必须保持一致,而一旦你把统计值缓存或者预聚合了,「保持一致」就变成了一件需要显式设计的事。实时算的时候一致性是天然的(每次都从明细算),改成增量维护之后,任何影响明细的操作都必须同步更新聚合值——我漏掉的正是「隐藏评价」这条路径。
选增量维护聚合值而不是继续实时统计(或者加缓存)。加缓存是更简单的改法,但缓存的问题是失效策略难做——评价随时会新增,缓存要么很快过期(等于没缓存)要么数据陈旧(分数不准)。增量维护的代价是「一致性变成了需要显式设计的事」:每一条影响明细的路径都要记得更新聚合。我选增量维护,但配了定时校准兜底——因为我知道自己一定会漏某条路径(事实证明确实漏了)。判据是「读多写少且读的性能要求高」:门店列表的访问量远大于评价提交量,所以把成本转移到写侧是对的。
聚合表存「总数 + 总分」而不是「平均分」。存平均分看起来更直观,但增量更新时要反推总分再算新平均分,浮点误差会累积;而且「各星级数量」这类需求也需要明细维度的计数。存原始的可加减量、展示时再计算,是聚合设计的通用原则。
回复给有限可编辑窗口,是在「僵化」和「不可信」之间取的点。完全不可改:商家打错一个字就只能一直挂着,而且他会转而联系客服要求删除,成本转移到了客服。随时可改:用户截图投诉时,商家可以先改掉再说「我没说过那句话」——公开内容的可信度就没了。有限窗口加修改留档,让笔误可修正而恶意篡改可追溯。
踩过的坑一:申诉通过后只隐藏了评价,没有回滚聚合值。商家反馈「申诉成功了但我的分数没变,等于没用」。根因是我从「实时统计」改成「增量维护」的时候,只处理了「新增评价」这一条路径,漏了「隐藏」「删除」「改分」这几条。这个坑的通用教训是:把一个「每次都重算」的逻辑改成「增量维护」,本质上是把一致性从天然的变成了需要显式保证的——所以必须先把「所有会影响这个值的路径」列成清单,逐一处理。我当时没列清单,凭印象改,就漏了。
踩过的坑二:只校验登录态,被批量注册的账号刷评。是运营从数据异常里发现的(某家店短时间内涌入大量五星好评)。教训是:凡是「用户对某个对象的行为」都要问一句「他有资格做这件事吗」——评价的资格是「有真实的已完成订单」。需求文档写的是「用户可以评价门店」,但「用户」这个词隐含了「消费过的用户」。
踩过的坑三:一单一评只在代码里判断,并发下被重复提交成功。用户手快点了两次提交,两个请求都查到「未评价过」,然后都插入成功了。修法是加数据库唯一约束。教训是:并发场景下「先查再写」不构成约束,唯一性必须由数据库保证。
没做的部分:没做评价的排序与精选(把有价值的评价排前面)。它需要定义「有价值」(有图、字数、点赞数、时效性的加权),而这个权重的定义是产品决策不是技术决策,而且会直接影响商家利益(决定哪些评价被看到)。我提了方案但认为不该由我来定权重。
门店列表性能改善要报清「评价量级」这个变量。因为实时统计的耗时和评价量成正比。报法是「某家评价量为 N 的门店,详情页评分部分的查询耗时从 X 降到 Y;门店列表页在返回 20 家店时总耗时从 A 降到 B」,并说明改造后的耗时与评价量基本无关——这个「无关」才是增量维护的核心收益。
聚合准确性用对账验证,这是最重要的指标:定时任务用明细重算并与聚合值比对,报「对账差异数长期为零」。如果有差异,要能定位到是哪条路径漏了更新。这个对账机制本身就是这个模块质量的证明。
各路径的聚合更新用断言型用例逐一覆盖:新增、隐藏、下架、删除、改分、恢复,每条路径后都断言聚合值正确。这是我踩的坑的直接回归,也是最有价值的一组用例。
刷评拦截效果报「按原因分类的拦截量」:订单不存在、订单未完成、重复评价各拦了多少。不要只报一个总数——分类之后能看出哪种攻击方式最多。
一单一评的并发验证:并发提交同一订单的评价,断言只有一条成功入库。要真的并发压,不是顺序调两次。
可编辑窗口:断言窗口内可修改且留下历史版本、窗口外被拒绝。
申诉下架的完整性:断言下架后三件事都做了——展示隐藏、聚合值回滚(平均分实际变化)、双方收到通知。第二条是坑的直接回归。
不要报什么:不要报「刷评减少了 N%」——你不知道真实的刷评基数。该报的是「对账差异长期为零」「六条变更路径的聚合更新逐一验证」「并发下一单一评只成功一条」「列表耗时与评价量解耦」这几件可核对的事。
没有匹配的内容,换个关键词试试。
项目拆解 · 商家入驻与经营支撑(后端 · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据