四条线与一条密文:访客邀请全链路复盘与审查捡出的存量 bug

导读

把访客邀请的四条线(提交生效、出码、权限下发、通行回流)完整摊开,回答”前端拿到二维码时权限到底下发了没有”;同日审查同事的短信 mock 提交,捡出一个更让人意外的存量 bug——真实审批短信的收件手机号,可能从来都是一串加密后的乱码。

前言

9 月 3 日访客邀请的异步链路联调跑通后,9 月 4 日做两件事:把整个功能的全链路文档补齐(含外部平台交互),以及审查短信 mock 提交。文档是给自己和后来人看的”作战地图”,审查则意外地让一条潜伏已久的存量 bug 浮出水面。

一、四条线,互不阻塞

访客邀请看起来是”一个按钮”,实际是四条独立的线:

四条线总览
【提交生效线·同步】前端 → 网关 → invite 端点 → 一个事务落库+入队 → 秒回 id
【出码线·同步】     前端 → getVisitorQrCodes → qrContent 字符串 → 前端渲染二维码
【下发线·异步】     调度器每分钟 → dispatch → bridge(JWT) → 星链 → 回写任务状态
【回流线·异步·默认关】bridge 定时拉氚云 → 回推内部接口 → inbox 幂等 → 通行记录
这个结构里最重要的认知是:前端拿到二维码时,权限下发可能还没发生;下发失败也不影响业务已生效。三条线解耦意味着每条线的失败模式可以独立设计——下发挂了调度器会退避重试,出码只依赖主表数据,提交事务里只做最快的落库。

四条线:提交、出码、下发、回流
图 1:四条线互不阻塞——二维码秒出,权限下发由调度器兜底

二、出码线:单访客单码,以及一个静默的域名坑

出码接口 getVisitorQrCodes 有两道闸门:仅 source=4(访客邀请),且仅邀约人本人(userId 比对),其余一律返回空数组。恒返回一条门岗码,qrContent 拼的是核验 H5 的地址,参数带证件号和手机号明文——这是既有核验页的契约,后端不出图,图片由前端渲染。

这里埋着一个值得警惕的坑:H5 域名按环境配置,测试环境漏配会静默生成指向生产地址的二维码——扫出来能开,核验的是生产数据。配置类错误的可怕之处就在于它不报错。

三、下发线状态机:每个格子都要有出路

调度器每分钟领到期任务(上限 20 条),状态机比前一天记录的更完整:

modian_permission_task 状态机
PENDING ─claim→ PROCESSING ─┬→ SUCCESS(终态,记 provider_record_id)
                            ├→ RETRY_WAIT ──退避 1/5/15/30/60min,超限→ FAILED_FINAL
                            ├→ FAILED_FINAL(4xx / 园区未映射 / 找不到钉钉id)
                            └→ UNKNOWN_MANUAL(异常 / 响应无 data / PROCESSING 超10分钟)
FAILED_FINAL / UNKNOWN_MANUAL ─人工重试→ PENDING(attempt 归零)

几个设计细节值得单独看:领取用乐观 UPDATE(抢不到就跳过,天然防多实例重复领取);PROCESSING 超过 10 分钟自动转 UNKNOWN_MANUAL(防执行中僵死);人工重试只接受 FAILED_FINAL/UNKNOWN_MANUAL——SUCCESS 是终态,因为星链侧重复授权有副作用,”不确定结果”的任务不允许自动重复下发。幂等靠的是 appointment_id 唯一索引 + INSERT IGNORE,代价(已接受并记录):同一预约二次审批不重下发。

四、审查捡出的存量 bug:短信发给了”密文”

同事提交了短信 mock(测试全绿:through 53 / integrate-iot 5),审查 mock 的用法时发现它调用了 getVisitorPhoneStr()——一个解密 getter。顺着这条线往回看老代码,看到了不该看的东西:

sendSms() 的存量 bug(L680)
// 入口 L137 已把 visitorPhone 加密成 hex,走到 sendSms 时字段已是密文
masSmsReq.setMobiles(Collections.singletonList(dto.getVisitorPhone()));   // ← 密文!
// 修法(一行):换解密 getter
masSmsReq.setMobiles(Collections.singletonList(dto.getVisitorPhoneStr()));

成因链条清晰:入口加密 → 后续所有读取都是密文 → 短信收件号变成 f1875d04... 这样的乱码 → 发送异常被 try-catch 吞掉,只在日志留一句”短信发送失败”。结论很扎心:所有来源的访客审批短信大概率从未真实发出过

密文短信:信件寄往一串乱码地址
图 2:审查 mock 的新代码,反而照出了老代码的存量 bug

但审查结论写的是”先记录、暂不改“,改前先过业务确认:问一句”访客审批通过短信有人收到过吗”——如果从来没人收到,bug 实锤且没人依赖;如果有人说收到过,说明链路另有蹊跷,先查清再动。修一个”看起来明显的 bug”之前,先确认它是不是真的没人依赖——这是审查纪律里最容易被忽略的一条。

五、经验总结

  • 全链路文档的价值在于把”线的边界”画出来:哪条同步、哪条异步、哪条默认关,谁阻塞谁一目了然;
  • 状态机设计检查表:每个状态都要有出路,终态要回答”谁还能动它”,超时僵死要有自动出口;
  • 配置类错误不报错——测试环境漏配域名会静默指向生产,这类坑只能靠清单和 review 拦;
  • 审查别人代码时顺手对照”新老同款逻辑”——mock 的正确用法照出了老代码的密文 bug;
  • 发现存量 bug 先记录、找业务确认影响面,再动手——改动一个没人依赖的错误行为也可能制造意外。

结语

这一天没有写多少新代码,但产出的两样东西都比代码长寿:一张四条线的作战地图,和一份带确认动作的待改清单。文档和审查都是慢功夫,省的是将来某个深夜的排查时间。