访客邀请上线记:从星链直连到持久化异步任务链路

导读

访客邀请功能要求”提交即生效”:免审批、访客自动进通行用户、权限自动下发到魔点门禁。真正的工作量不在接口本身,而在把”审批通过后直连星链下发权限”改成一条可重试、可追踪、可人工兜底的持久化异步任务链路。本篇记录接口契约、合并冲突的坑,以及一次状态机走完一整圈的全链路模拟联调。

前言

9 月 3 日的主题是访客邀请。被访人在访客登记页点”访客邀请”,填访客姓名、手机号、证件号、拜访时间和车牌,提交即生效——没有审批流。后端落在 through-service,难点不在表单字段,而在权限下发:访客的通行权限要推到魔点/星链平台,这一步直接决定访客能不能刷开门禁。

一、接口契约:免审批,提交即生效

POST /through/visitorAppointment/invite,几个值得注意的契约设计:

  • 服务端强制覆盖source=4(访客邀请)、staffId/userId=当前登录人——前端传了也会被覆盖,身份类字段永远不信客户端;
  • 手机号服务端强制非空,证件号码按身份证校验码规则校验;
  • 提交即生效:后端自动审核通过,访客自动进通行用户、车辆自动建档,列表立即可见(来源列显示”访客邀请”)。
E2E 实测请求原文(节选)
{
  "staffName": "test", "visitAddress": "测试园区A栋12层",
  "beginDate": 1770000000000, "endDate": 1770014400000,
  "visitorInfoList": [{
    "visitorName": "蓝带车访客", "telephone": "13900003333",
    "certificateType": "0", "certificateNumber": "440101199001011233",
    "relationCarList": [{ "carType": "0", "licensePlateNumber": "粤A12345" }]
  }]
}

二、为什么把直连改成持久化异步任务

原有设计是审批通过后直接同步调星链接口下发权限。这有两个老毛病:请求线程被外部平台拖住,失败了就丢了——权限没下发,访客站在门禁前干瞪眼。

改造后的链路:

异步任务链路
POST /invite(免审批)
  └─ 同一事务:pass_user/车辆落库 + modianPermissionTaskService.enqueue(主表id)
        ← INSERT IGNORE + appointment_id 唯一索引,请求到此结束
调度器每分钟 dispatch:
  ├─ adapter-service.enabled=false → 直接返回(任务躺着不动)
  └─ 领任务 → 拿 JWT → bridge 验签/园区映射 → POST 星链
       ├─ code=0/200  → SUCCESS(终态)
       ├─ 5xx        → RETRYABLE_FAILURE(1/5/15/30/60 分钟退避)
       ├─ 4xx/未映射  → FAILED_FINAL(人工重试)
       └─ 网络/解析异常 → UNKNOWN_MANUAL(PROCESSING 超 10 分钟也转人工)

同步直连与异步任务链路对比
图 1:直连是”请求线程扛一切”,异步链路是”落库即返回,调度器兜底”

设计的几个关键点:INSERT IGNORE + 唯一索引保证同一预约不会重复入队;SUCCESS 是终态,人工重试接口只接受 FAILED_FINAL / UNKNOWN_MANUAL;退避按 1/5/15/30/60 分钟拉开,不给下游平台添堵。语义变化也需求方拍板记录在案:同一预约二次审批不再重下发——只影响氚云改期场景,访客邀请一次性提交无此问题。

三、合并的坑:自动合并成功 ≠ 语义正确

本次合并 访客邀请 分支与 adapter 分支,冲突只有 2 个文件,但其中一个埋了雷:adapter 侧删除 UserClient 声明的 hunk 被 git 自动采纳,而我方的部门回填代码还在调用它——自动合并出了编译必炸的结果

合并冲突:并集与语义核对
图 2:过滤器拒绝路径取并集没问题;删声明的 hunk 必须全文件核对引用

教训一句话:跨分支删声明的合并,必须全文件核对引用。git 只保证文本不冲突,不保证语义正确。另一个文件 InternalApiDenyFilter 就是正面例子——双方新增的拒绝路径取并集,各四条两条,相安无事。

四、全链路模拟:状态机走完一整圈

联调分两层。层 1 只起主工程,验证合并不破坏业务:提交 invite 后主表 source=4modian_permission_task 恰一条 PENDING 且与提交同秒同事务、门岗码正常、enabled=false 时 dispatch 原样返回——全过。

层 2 起 bridge + mock 星链(node 起在 19099,恒回 code=0,请求落日志当断言点),然后故意让任务失败一次

步骤 结果
预约园区改成未映射 → dispatch FAILED_FINAL + PARK_NOT_MAPPED ✅(顺带证明 JWT 换 token/验签链路通)
人工重试接口 任务回 PENDING ✅
改成已映射园区 → dispatch SUCCESS,provider_record_id 回填 mock 返回值 ✅

状态机走完 PENDING → FAILED_FINAL →(人工重试)→ PENDING → SUCCESS 一整圈。失败路径是被真实走过的,不是只看代码觉得它会工作——这是这次联调最值得保留的动作。

五、经验总结

  • 身份类字段(操作人、来源)永远服务端覆盖,不信客户端;
  • 外部平台调用默认会挂:持久化任务 + 退避 + 人工兜底是标配,不是过度设计;
  • 终态设计要回答”谁还能动它”:SUCCESS 不收人工重试,就是幂等的最后一道闸;
  • git 自动合并成功后,对删声明的 hunk 做全文件引用核对;
  • 联调主动制造一次失败,让状态机的每一段都被真实踩过。

结语

表单接口半天就能写完,让访客”提交即生效、权限必达”的是背后那条异步任务链路。把失败当成常态设计进去,状态机的每个格子都真实走一遍——上线那晚才能睡得着。