发生了什么

用户在某第三方电子签平台完成了合同签署,第三方那边已经生成新合同并归档成功。我方系统需要主动回查第三方拿到合同号再写回自己的库。但这次回查的远程调用超时了(约 30 秒无响应),代码捕获到这个异常后,走了和查询成功但没查到合同完全一样的分支:静默返回,既没回写合同号,也没重试,也没告警。于是本地合同记录里合同号一直为空。之后第三方的签署完成回调过来,要用合同号反查本地记录,结果查不到,导致签约完成状态和快照更新都没触发。用户端表现为:第三方已签成功,但我方后台仍显示签约中,还错误地提示摄像头未绑。最后只能人工调后门接口把合同号补回、再手动推进到已签约。

本质

被破坏的约束是调用失败和查询到空结果是两种完全不同的状态,不能合并处理。超时意味着我不知道对方到底有没有、结果未知,而空结果意味着确认了、就是没有。代码把前者当成后者吞掉,等于用一个不确定的失败冒充了一个确定的否定结论,让关键数据合同号该落库时没落库。更糟的是异常被静默吞没、没重试、没告警,于是错误状态可以长期存在无人发现,后续依赖该数据的回调环节全部连环失败。

下次遇到这些场景要警惕

调用外部依赖时,一定把调用异常或超时和正常返回空分成两条路:异常要么重试、要么标记为可恢复的失败状态并告警,绝不能 catch 完就当没数据静默返回。凡是回查到回写关键字段再到后续回调依赖该字段的链路,要给回写加重试和最终失败告警,并让后续回调在字段缺失时能触发补偿(比如反过来主动查一次)而不是直接失败。测试上专门造下游超时或抛异常的场景,断言数据不会被写成空、会触发重试或告警、状态不会停在中间态;也要覆盖第三方已成功但我方未回写的数据不一致场景,验证有对账或补偿能自愈。

下面用代码把超时被当空结果吞掉和修复讲清楚。

问题版本:catch 里不区分异常类型,直接当没查到返回。

ContractInfo queryContract(String sourceCode) {
    try {
        return openApi.query(sourceCode); // 远程调用,可能超时抛 RpcException
    } catch (Exception e) {
        // 把超时或调用异常和真的没数据混为一谈,静默返回空
        log.info("根据 sourceCode 未查询到合同信息");
        return null;
    }
}

// 调用方:拿到 null 就以为暂时没有,不回写、不重试、不告警
ContractInfo info = queryContract(sourceCode);
if (info == null) return; // 合同号永远写不进来,状态卡死

修复版本:区分调用失败和确认无数据,失败要重试加告警,不能静默吞。

ContractInfo queryContract(String sourceCode) {
    try {
        return openApi.query(sourceCode); // 正常返回,可能有数据也可能确认无数据
    } catch (RpcException e) {
        // 超时或调用异常是结果未知,不能当成 null 返回
        log.error("回查合同调用异常,需重试或告警, sourceCode={}", sourceCode, e);
        throw new RetryableException(e); // 交给上层重试或标记失败
    }
}
// 调用方:区分确认无数据和调用失败,失败走重试加告警加补偿
try {
    ContractInfo info = retryTemplate.execute(ctx -> queryContract(sourceCode));
    if (info == null) {
        return; // 确认无数据:可以静默等待下次
    }
    writeBackContractNumber(record, info.getContractNumber()); // 回写合同号
} catch (RetryableException e) {
    // 重试仍失败:标记为待补偿加告警,别让状态悄悄卡死
    record.markPending();
    alert("合同回查持续失败,合同号未回写", sourceCode);
}

前后端边界:这条完全是后端问题,后端回查逻辑把超时吞成了空、导致合同号没落库。前端的 BOS 后台、商户端只是如实展示后端返回的状态,签约中、摄像头未绑这些错误提示都是后端数据没更新造成的连锁表现,前端本身不需要改。修复点全在后端:区分异常、加重试告警、给第三方已成功但本地未回写这类不一致加对账补偿。