今天主要处理了一个和潮流分析有关的 Bug,问题现象是:断面分析列表接口中,部分设备返回的"基态正向限额"和"基态反向限额"为空,但从业务上看这些设备应该是有约束限额的。
虽然这次最终代码改动也不算特别大,但排查过程对我来说收获挺明显的。因为这个问题不是简单的空指针或者字段没赋值,而是涉及"字段到底应该从哪里来"的业务口径问题。
一、问题背景
一开始是前端同事反馈,在断面分析页面中,有些断面的基态正向限额和基态反向限额没有展示出来。
我先看了对应接口,大致是断面列表查询接口。这个接口会根据案例 ID 查询算法输出的断面潮流数据,然后组装成前端需要的 DTO 返回。列表中除了断面名称、最大潮流、最大负载率、越限次数等指标外,还包含两个重要字段:
基态正向限额
基态反向限额
从页面表现来看,不是所有断面都为空,而是部分断面为空。这种问题一般不能直接猜,需要先确认数据来源和计算链路。
二、先梳理原来的计算逻辑
我顺着接口往下看,发现原来的逻辑并不是直接从约束配置表中读取限额,而是通过算法结果中的两个字段反推:
断面潮流 powerFlow
负载率 loadRatio
原来的计算思路大概是:
正向限额 = abs(powerFlow) / loadRatio
反向限额 = 正向限额取负
这个公式本身是能理解的。因为负载率通常可以理解为:
loadRatio = abs(powerFlow) / limit
所以在只有潮流和负载率的情况下,反过来就可以推导出 limit。也就是说,原代码大概率是因为算法结果表里没有直接存限额字段,所以通过结果数据反推了一个限额值。这个写法在部分数据完整的情况下可以工作,但是它有几个隐含前提:
loadRatio 不能为空
loadRatio 不能为 0
算法结果中的 loadRatio 必须和当前限额口径一致
正向限额和反向限额默认是对称的
这几个前提只要有一个不满足,就可能出现问题。
三、真正导致为空的原因
继续看代码后,我发现限额是在遍历潮流曲线时计算出来的。只有当 loadRatio 大于 0 时,才会执行反推逻辑。也就是说,如果某个断面的 loadRatio 为空、为 0,或者整条曲线都没有有效负载率,那么正向限额就一直算不出来,最后返回 DTO 时就是 null。反向限额又是通过正向限额取负得到的,而正向限额为空,反向限额自然也为空。这就解释了为什么是"部分设备为空",而不是全部为空。因为不同断面的算法结果数据完整程度不一样,有些断面有有效 loadRatio,可以反推出限额;有些断面没有有效 loadRatio,就无法反推。
但是从业务角度看,断面的限额不应该完全依赖算法结果反推。限额本质上是约束配置数据,不是运行结果数据。运行结果可以用来计算负载率、越限状态、最大潮流等指标,但基态正向限额和基态反向限额更应该优先取配置表中的约束值。
四、最终修改思路
确认原因后,我没有直接在原公式上继续打补丁,比如给 loadRatio 为空时随便补默认值。因为这样虽然能让字段不为空,但不一定符合业务逻辑。最后采用的思路是:
优先从断面约束配置中读取 positiveLimit 和 negativeLimit
如果约束配置没有查到,再保留原来的 powerFlow/loadRatio 反推逻辑作为兜底
这样做有两个好处:
第一,数据来源更符合业务含义。基态限额属于约束配置,应该优先来自约束配置表,而不是从结果曲线中反推。
第二,兼容原有逻辑。如果某些历史数据没有配置限额,系统仍然可以尝试通过潮流和负载率反推,避免影响已有功能。
五、代码实现上的几个点
这次实现主要改在断面分析列表查询服务中。首先,我新增了断面约束配置 DAO 的依赖,用来查询当前案例下的断面约束数据。然后在查询断面潮流数据之后,根据 caseId 和案例时间范围,一次性查出相关断面的约束配置,并按 sectionId 组装成 Map。这里没有在循环每个断面时单独查数据库,而是先批量查出来,再放到 Map 里。这样做可以避免 N+1 查询问题。大致逻辑是:
先查询当前案例下的断面约束数据
按 sectionId 分组
每个 sectionId 保存一组正向限额和反向限额
构建 DTO 时,根据当前断面的 sectionId 从 Map 中取限额
如果配置里有 positiveLimit,就用配置值
如果配置里有 negativeLimit,就用配置值
如果配置值为空,再走原来的反推结果
这次还加了一个内部类,用来临时承载断面的正向限额和反向限额。这样 Map 的结构会比直接使用多个 Map 更清晰,也方便后面扩展。
六、为什么不能只用正向限额取负
排查过程中我也重新理解了"正向限额"和"反向限额"的业务含义。原逻辑里反向限额是通过正向限额取负得到的,这看起来很自然,但它隐含了一个假设:断面正向和反向的限额绝对值一定相同。在实际业务中,这个假设不一定成立。不同方向的约束可能来自不同的安全边界,正向和反向限额不一定完全对称。所以这次修改后,如果配置表中存在 negativeLimit,就直接使用配置中的反向限额,而不是简单地用 positiveLimit 取负。只有在配置表没有反向限额,并且原来的反推限额存在时,才继续使用"正向限额取负"作为兜底。
七、接口验证过程中的问题
这次联调时还遇到了一个 Swagger 调试的小问题。
一开始我把查询地址中的日期参数进行了 URL 编码,比如空格变成 %20,冒号变成 %3A。结果接口返回了表单验证失败,提示 Date 类型转换失败。后来发现,在 Swagger 里如果直接把已经编码过的字符串填进去,后端拿到的可能就是字面量的 %20 和 %3A,而不是正常的日期字符串,所以转换失败。另外这个断面列表接口中,有些参数只是用于置顶展示,不是真正过滤。例如 deviceId 和 deviceIdList 的含义就不一样。如果想过滤某个设备,应该传 deviceIdList;如果只传 deviceId,可能只是把对应设备放到列表前面,并不会减少响应数据量。这也提醒我,调接口时不能只看参数名字,还要看后端实际怎么用。
八、验证结果
代码修改完成后,我本地执行了编译,确认没有编译错误。然后通过接口返回数据对比,重点看了之前为空的两个字段:
basePosLimit
baseNegLimit
修改后,对于约束配置中存在限额的断面,接口可以正常返回基态正向限额和基态反向限额。对于没有配置数据的情况,也不会破坏原有逻辑,仍然会尝试通过潮流和负载率进行兜底计算。