跳转至

第四十四章 · 万钧

万钧的邮件是一月六日七点零三分到的。

标题:「跨境结算失败率优化方案——请于今日下午三点前提交。」

正文四行。第一行写「截止十二月,结算成功率百分之九十九点六二,低于合同承诺的百分之九十九点九」。第二行写「请在一周内提交降低失败率的方案」。第三行写「第一版先发我看过之后再补充细节」。第四行写「今天下午三点」——和第三行矛盾,第三行说一周,第四行说今天。

陈稳打了两个字:「收到。下午三点前给框架。方案正文周五前提交。」发送。

万钧没有回。

七点四十。五楼西区。工位屏幕边缘贴着一张黄色便利贴——刘铮写的:「开发环境已配好。Go 1.22.1。make run 起服务。」右下角画了一个箭头,指向回车键。

make run。编译通过。启动日志最后一行:「server started on :9090」。

他跑了三分钟。服务没有崩。

刘铮九点才到。他打开 internal/service/settlement.go,一行一行往下读。

一千一百行。四个 RPC 对应了大约六百行。多出来的五百行是状态机转换逻辑:创建→待合规→合规中→待结算→结算中→已结算→结算失败。七个状态,每段大约七十行。

他画了一张状态图。圈出「结算失败」状态的前置条件:合规不通过、银行超时、金额不一致、汇率波动超限。四个出口。

在图下面加了一行:「未知原因——47笔。不在状态图里。」

九点整。刘铮把咖啡杯放在桌上,看了一眼终端——服务在跑。

「你跑了?」

「跑了。没崩。」

刘铮看了一眼那张纸。「万钧给你发邮件了。他早上会发这种东西。习惯就好。你不用今天交方案。他知道你做不出来。他要的是你回应他。」

「那假如我说'我今天能交'呢。」

「他不会信。」刘铮说。「但他会看你怎么说'我今天能交'。你说了之后做了什么。这才是他看的。」

十点十分。他把四个出口的排查脚本各写了一个版本。合规的查 compliance_check_result 表,银行的查 bank_callback_log,金额的查 settlement_amount 和 transaction_amount 比对,汇率的查 fx_rate_snapshot。四个脚本,大约一百二十行 Go——在线教程第五章的函数定义部分开着,写两行看一行。

十一点。第一版脚本跑完。合规拦截四十七笔,银行超时五十二笔,金额不一致三十一笔。

他给刘铮发了一条消息:「fx_rate_snapshot 表在哪。」

「一号库 trade_flow 下。只读权限。」

历史数据只有三天。汇率的排查路径断了——百分之三十二的失败无法溯源到汇率波动,因为没有历史数据。

记事本上:

已知原因:合规47 + 银行52 + 金额不一致31 = 130
未知原因:47
汇率相关:[待确认,历史汇率数据仅保留3天]
总计:423

他在「不可分类」下面画了一条线。

十一点四十。万钧的消息。

「下午两点半。五楼小会议室。」

没有问号。陈稳回了一个字:「好。」


十二点十分。食堂。他端着一碗面找了一个靠窗的位置。

对面坐的人不是林栩——灰色衬衫,深灰色西裤,袖口卷了两圈。

万钧。

「纪衡推荐你的时候说了一句话。他说你做过一次数据治理,把三个表统一了。三个月没有人动手的事,你一个人做了。你知道他说这个的时候在证明什么吗。」

陈稳没有接。

「他在证明你能干脏活。」万钧夹了一筷子青菜。「我需要的不是能干脏活的人。我需要的是知道什么活该干、什么活不该干的人。」

「我在结算组做过一个拦截器。上线四十三天零资损,灰度一百八十四万次请求全部通过。」

「那是你组里的活。现在你在我这儿。」万钧顿了一下。「你知道跨境结算和国内结算最大的区别是什么吗。」

「链路长度。十九比七。」

「不对。链路长是结果,不是原因。原因是——国内结算的规则是确定的。跨境结算的规则不是确定的。汇率不确定。合规不确定。银行端的处理时间不确定。你面对的是一个参数在变化的系统。」

「那怎么保证百分之九十九点九。」

「保证不了。」万钧说。「所以合同里那条百分之九十九点九,靠的不是系统保证,是人跟客户谈的。客户问的时候——」

「换一种说法。」

万钧看了他大概三秒。「谁教你的。」

「没有人。我自己学的。」

万钧没有继续这个话题。他把餐盘端起来。「下午两点半的方案不用写了。我要的不是方案。我要你在一周内,把四十七条查不出来原因的那笔账,至少找出二十条的根因。方向你自己定。」

他把盘子放到回收口上。

陈稳坐在窗边。那碗面已经凉了。他吃了几口,把汤喝了。


下午一点二十。记事本第一行:「万钧要的不是方案。是要结果。」

第二行:「四十七条未知原因。一周内至少找出二十条。」

第三行:「假设——未知原因不是单一故障。是多个环节的异常交织在一起,落在一个分类无法覆盖的状态里。」

他把第三行圈了起来。如果是多环节异常交织,就不能沿着单根因去查。要沿着时间线——把四十七笔交易的完整链路日志拼出来,看交汇点在哪里。

SELECT * FROM trade_flow.trade_import_record WHERE status = 'failed' AND fail_reason IS NULL。四十七行。导出 CSV。

分布均匀。没有集中在某时间段。

加一列 currency_pair。EUR/CNY 十二笔。USD/CNY 十一笔。JPY/CNY 八笔。GBP/CNY 六笔。HKD/CNY 五笔。EUR/CNY 和 USD/CNY 占了二十三笔——将近一半。

打开 fx_rate_snapshot。一月五日 EUR/CNY 七点八二五三。USD/CNY 七点二四七六。一月四日 EUR/CNY 七点八四一七。USD/CNY 七点二六六二。两天之间,分别跌了零点零一六四和零点零一八六。

计算每笔的 settlement_amount / transaction_amount。第九笔 USD/CNY:一万美金,结算金额七万一千八百二十元人民币,汇率七点一八二。一月五日中间价七点二四七六——低了将近百分之一。

九笔 USD/CNY 全部算完。结算汇率分布在七点一八到七点二六之间,大部分低于当日中间价。EUR/CNY 也一样,分布在七点七一和七点八二之间。

SettlementRecord 的 fx_rate 注释:「结算汇率(来源:成交时汇率快照)」。

成交时汇率快照,不是结算当日的汇率。如果交易在汇率波动大的时候发起——比如上午九点一刻之前——成交价和结算均价之间就有一根差值。排查系统不保留历史汇率,无法识别这根差值,归为未知。

他把推论写下来。漏洞:如果是汇率快照机制的问题,失败交易应该集中在汇率刷新之前。把四十七笔 created_at 按小时分布做了一遍——九点到十点三笔,十点到十一点五笔,十一点到十二点四笔,十二点到十三点两笔。没有集中在九点一刻前。

划掉。不是汇率快照。


下午两点。饮水机。走廊窗户对着另一栋楼的玻璃幕墙,反射着下午的光。

手机震了一下。何晚:「今天几点回。」

「不确定。晚。」

「晚上吃什么。」

「我做。」

「你几点能到。」

「八点。」

「那我来做。你回来吃就行。」

走回工位的时候,刘铮从屏幕后面探了一下头。「你知道以前他要的量,没有人在一周内完成过吗。」

「几周。」

「两周。最快也是两周。」

「他说的是至少二十条。不是全部。」

陈稳坐下来,在 CSV 里加了一列 time_since_order——订单发起时间到结算失败的时间差。最长十九小时,最短四十分钟。按币种对分组:EUR/CNY 平均十一小时。USD/CNY 平均四小时。

SettlementRecord 的 timeout_config:「settlement_timeout_seconds: 14400」。四小时。

USD/CNY 在阈值内。EUR/CNY 超出近三倍。

翻到 currency_config:

currency_pair_config:
  EUR/CNY:
    settlement_timeout: 28800(八小时)
  USD/CNY:
    settlement_timeout: 14400(四小时)

— 超时配置分币种对。EUR/CNY 八小时阈值,平均耗时十一小时——超时三小时。USD/CNY 四小时阈值,平均耗时四小时——边缘。

记事本:「问题不是汇率。是超时阈值与实际链路耗时不一致。EUR/CNY 的链路耗时超过了配置容忍上限,导致流程在等待阶段被标记为失败——但系统把超时导致的后续异常归为『未知』,因为超时本身不是一个独立的失败码。」

四十七笔的 settlement_timeout 配置值全查出来。EUR/CNY 超时的十二笔全在八小时阈值外。USD/CNY 边缘三笔。JPY/CNY 边缘八笔。

二十三条。

文档标题:「跨境结算失败率优化方案——根因分析部分」。正文:「四十七笔未知原因中,二十三条经链路耗时分析确认为超时阈值与链路实际耗时不一致导致。建议调整对应币种对超时阈值或优化链路耗时。」

发送给万钧。抄送刘铮。

三点五十九分。

万钧的回信四点零三分到了:「收到。明天上午十点,再讲一遍。」


五点四十。地铁。窗玻璃上映着自己的影子。他想起老梁说过的话:「技术上的问题,大部分不是技术问题。是你以为问题出在这里,但它出在你说不清楚的地方。」

超时阈值。链路耗时。历史汇率不保留。数据质量不校验。

他说不清楚的,不是技术本身。是要花多少时间才能全部修完。

而万钧只给了一周。


七点十分。家门。

何晚在厨房里,锅盖半开着,蒸汽往上扑。

「回来了。」

「嗯。」

「今天第一天正式干活。怎么样。」

「万钧给我发了一封邮件。早上七点。让我下午三点交方案。中午坐到我桌上吃饭。说方案不用写了。换成另一个任务。」

何晚把火关了,转过身。「他坐到你桌上吃饭。」

「食堂。」陈稳说。「我坐在窗边。他走过来坐下了。」

「说了什么。」

陈稳把中午的对话大致复述了一遍。

「所以你今天第一天就做了他一周的任务。」

「不全。四十七条里找了二十三。」

「那你明天讲什么。」

他站在厨房门口。「不知道他明天要看什么。」

「那你今天觉得怎么样。」

「不是没高兴。是不知道明天是不是还有一样的。」

何晚看着他,没有追问。她转过身,把火重新打开。

「饭好了叫你。」

他站在门口没有走。「你今天做的是什么。」

「红烧排骨。跟上次一样。」

「上次的很好吃。」

何晚的手在锅铲上停了一下。「你第一次主动说好吃。」


八点四十。书房。

他把三页逻辑链写了三遍。第一遍技术语言。第二遍删掉所有 Go 术语,换成业务语言。第三遍只留三句话:

「四十七笔未知失败的根因已定位:超时阈值与链路耗时不一致,故障码缺失导致无法归因。建议分三步修复,预估实施周期两周。预期改善幅度:未知失败率下降百分之四十九。」

字数一百零七。

十点。他站在窗前。对面没有那扇亮着的窗。新的办公楼窗外只有另一栋楼的黑影和一盏路灯。灯的底座上停着一只鸟,缩着脖子。

他站了一会儿。鸟也没有动。

十点十五分。卧室。何晚已经躺下了。

「明天几点。」

「十点。万钧办公室。」

「那你睡吧。」

天花板是平整的白色。没有裂缝。

他在黑暗里睁着眼睛。他想起今天早上那条消息——「收到。下午三点前给框架。」——他当时不知道万钧要的是什么。现在知道了。万钧不要框架,不要方案。万钧要他自己定义什么是「完成」。

他在七个小时里从零跑到这个瞬间,但他不知道自己跑得对不对。

翻了个身。床垫轻轻地弹了一下。

「没睡着?」何晚的声音。

「在想明天的事。」

「你已经做完了。」

「不知道做完的标准是什么。」

「你今天找到了二十三条。明天再讲一遍。这就是标准。」

过了大概三十秒,他说:「明天讲完之后呢。后天做什么。大后天做什么。」

「你以前不会这么想。你以前只会想明天做什么。」

「因为不提前想,来不及。」

何晚没有说话。房间里安静了大概一分钟。

她说:「你变了。你以前不知道来不及。现在你知道了。」


一月七日。上午九点五十。五楼东侧走廊尽头。

万钧的办公室门开着。他站在门口敲了一下门框。

万钧抬起头。「进来。」

他坐下来。万钧没有让他正式开始的意思,自己在看屏幕。过了大约二十秒,万钧把屏幕转过来。

屏幕上不是他的方案。是刘铮的终端。上面是他的排查脚本——四十七笔交易导出的 CSV 和处理结果。

「你在结算组的时候,一天能处理多少条异常。」

「看情况。如果是走查数据,千条左右。如果是排查根本原因,看根因的深度。」

「那你昨天的速度,在你原来的组里算快还是慢。」

「算快。」

万钧看着他不说话。

「那今天是第一天。你的速度还会变。」

万钧把屏幕转回去。点开那封邮件。「你的方案我看了。超时阈值与链路耗时不一致,排查系统无超时独立失败码。你这个推论成立的前提是——超时失败应该被标记为超时,但现有系统没有独立码。」

「对。」

「那如果我给你一个独立码,你的排查脚本最快多久能接入。」

「两天。」

「那就加上。超时阈值调整的事我让刘铮跟你一起评估。你做完排查脚本接入,他做阈值调整。周五之前合入。」

他起身。「不是我说你能做。是你自己说的。」

陈稳走出办公室的时候,走廊上的光从西边的窗户照进来。九点五十八分。他用了八分钟。

回到工位。刘铮从屏幕后面探了一下头。

「通过了?」

「通过了。排查脚本接入独立失败码。两天。周五之前合入。」

刘铮靠在椅背上。「你知道他上次批准一个人改超时阈值,花了多久吗。两周。你从收到邮件到查出二十三条,花了多久。」

陈稳看了一眼终端右上角的时钟。「九小时。」


下午两点。排查脚本第一版写完。跑三遍,输出一致。合入主分支,commit message 一行:「feat: 失败交易排查脚本——按币种对、超时阈值、链路耗时三栏交叉分析。覆盖四十七笔未知失败。」

何晚的消息:「今天几点。」

「应该六点。」

「做排骨。」

「昨天的还有剩。」

「再做一份。昨天的你吃太快了。」

刘铮在他背后说了一句:「万钧让你明天上午之前把独立失败码的接口规范定下来。他要发合作方确认。」

「可以。今晚出。」

他打开 proto/trade/settlement.proto,在 SettlementRecord 里加了一个枚举:

enum FailReasonCode {
  UNKNOWN = 0;
  COMPLIANCE_REJECTED = 1;
  BANK_TIMEOUT = 2;
  AMOUNT_MISMATCH = 3;
  FX_RATE_EXCEEDED = 4;
  TIMEOUT_THRESHOLD_EXCEEDED = 5;
}

每一条失败写入时带一个码。排查脚本就不需要再猜了。

但改 proto 意味着合作方接口要跟着改。他在 TIMEOUT_THRESHOLD_EXCEEDED 后面加了一行注释:「预估两到三周完成合作方接口适配」。

发送给刘铮。六分钟后回:「接口规范已阅。合作方那边我让商务下周去谈。你先在内部排查系统里用。」

陈稳关掉文档。他站起来。走廊尽头的窗外有晚霞,红色夹着灰色。

他端着水杯站在窗前。

今天比昨天少写了一半的笔记。不是因为事少。是有些事不需要记下来才记得住了。

他想起 trade_import_record 的源头数据不可核。林栩的那句话:「代码就是文档。」万钧说的:「规则不是确定的。」

刚刚提交的那个 FailReasonCode 枚举。加了它,未知失败就有分类了。但分类不是解决。分类只是把原来没有名字的问题贴上一个名字。真正的解决——让四十七笔不再出现——需要链路本身变短,超时阈值变长,数据质量变好。那些不在他的两天计划里。

晚上八点整。邮件发出。标题:「排查脚本超时相关变更——内部排查系统与外部合作方适配说明」。约一千字。

他把那张便利贴从屏幕边缘撕下来,扔进笔筒。笔筒里有一支黑色签字笔,笔帽上有一圈牙印。

走到电梯口。电梯正从一楼往上走,在某一层停了一下。他站在那里等着。电梯到了。门开了。里面有一张快递单,折过的角朝上。他没有捡。

进了电梯。按了一楼。

电梯往下走。信号一格一格地弱下去。他想起一件事——今天没有想过结算组的事。没有想过 settle_reconciliation。没有想过 10.14.1.88。没有想过老梁的搪瓷杯。没有想过那扇亮着的窗。

他不知道自己应该觉得松一口气,还是不应该。

一楼。门开了。大厅的灯亮着。

「这么晚。」

「加班。」

「天天加班?」

「新来的。」

保安点了一下头。

陈稳走到门口,风把他的头发吹乱了一秒。他拉了一下外套拉链,往地铁站的方向走。路灯亮着。路面上有几片落叶,被风推着往同一个方向滑。

他没有回头。

一月七日。第二天。他不知道明天会来什么。但他知道后天之前,排查脚本要合入。周五之前,独立失败码要上线。下周起,商务要去谈合作方接口适配。

那些不在他的方案里。他在万钧面前只说了自己能做的。

剩下的部分,他想过。

但没有说出来。


← 第四十三章 归零 · 目录 · 第四十五章 恶心 →