返回博客

「透传代理」不等于「无责代理」:中间层字段缺失的沉默数据损毁

我们给某第三方 POS 系统写了一个反向同步接口——把会员注册/变更事件实时推送给 POS 侧。代码看起来极其简洁:接收前端 POST,转手调 POS SDK,把响应直接返回。上线后一切正常,POS 那边也返回 success。

直到几周后,运营拿着导出报表来问:"为什么这 95 个会员没有门店绑定?"

现象

POS 系统里确实存在这 95 条记录,注册时间、手机号都正确,但 counterCodeBelonging(门店归属码)字段全部为 NULL。

回查日志,接口调用全部返回 200,POS SDK 没有报错。问题不是接口挂了,而是数据写进去就是错的。

根因

出问题的控制器长这样:

public function updateNew()
{
    $body = $this->request->post();             // 原始前端载荷
    $resp = $this->posClient->updateNew($body); // 原封不动转发
    return json($resp);
}

这个控制器:

  • 没有任何字段校验
  • 没有回填逻辑
  • 没有值格式检查

当前端(PC 后台、移动端 H5、管理员手动导入、Java 服务批量写入……)其中某条入口漏掉了 counterCodeBelonging,控制器把空值直接传给 POS。POS 接受了,因为它把该字段当可选处理,照样返回 success。

这就是沉默损坏:外部服务不报错,本地无日志异常,数据静悄悄地带着 NULL 落库了。

坑:透传代理把前端不完整的载荷直接转发给第三方

第三方 API 通常对可选字段宽容,成功响应不代表数据完整写入。如果控制器只是 $external->call($req->all()),它就变成了一根哑管道——前端漏字段,它照单全收,下游永久损毁。

而且这个问题往往在数天乃至数周之后才浮现(导出/报表/下游消费者报错),到那时原始请求早已不在,只剩症状。

正解

代理层应当承担三项职责:强制必填字段、从本地数据回填、校验值的格式

public function updateNew()
{
    $body = $this->request->post();
 
    // 1. 必填字段清单
    $required = ['mobile', 'organizationId', 'counterCodeBelonging'];
    foreach ($required as $k) {
        if (empty($body[$k])) {
            // 2. 先尝试从本地数据回填,回填失败再报错
            $body[$k] = $this->backfill($k, $body);
            if (empty($body[$k])) {
                $this->error("missing required field: {$k}");
            }
        }
    }
 
    // 3. 值格式校验
    if (!preg_match('/^\d{10,}$/', $body['organizationId'])) {
        $this->error("organizationId malformed");
    }
 
    return json($this->posClient->updateNew($body));
}
 
private function backfill($key, array $body)
{
    // 手机号 → fans 表 → 门店归属码
    if ($key === 'counterCodeBelonging' && !empty($body['mobile'])) {
        return Db::name('fans')
            ->where('mobile', $body['mobile'])
            ->value('offline_store_id');
    }
    return null;
}

回填逻辑的关键在于:本地数据库里往往已经有能推导出目标字段的信息(这里是手机号 → 粉丝表 → 门店 ID),只是前端入口忘记传了。代理层在这个唯一的汇聚节点上补齐,比在六个入口逐一修前端要稳健得多。

正解:代理层 = 契约执行层,不是哑管道

在控制器的唯一汇聚点做三件事:

  1. 必填校验——不完整请求直接 4xx,不要静默放行
  2. 本地回填——先查本地已有信息补齐,再决定是否拒绝
  3. 值格式校验——防止字段值混淆(比如门店码和机构 ID 传反)

这三十几行代码换来的是:所有入口(PC / H5 / 批量导入 / 内部服务)自动获得自愈能力,下游数据质量由代理层兜底保证。

别信「我们会修前端」

「前端多入口」是现实:PC 后台、移动端、管理员手动操作、定时批量任务,任何一条路都可能漏字段。代理层是唯一能看到全部流量的节点,在这里做一次,胜过在六个入口各修一遍——而且前端还会随着版本迭代再度漂移。

一句话外卖

透传代理是债务,不是设计;每一个 $external->call($req->all()) 背后都藏着一张未来的数据质检账单。