广东水文项目后端接口开发与优化实践

在大型企业级应用开发中,与第三方平台的接口集成是不可避免的任务。如何在保证系统稳定性的同时,高效地完成接口对接和优化,是每个后端开发者都需要面对的课题。本文以广东水文监测系统为背景,记录了视频图片鉴权改造和等值面外部接口开发两个典型场景的完整实践过程,希望能为有类似需求的开发者提供参考。

前言

广东水文监测系统是一个基于 RuoYi-Vue 框架开发的水文监测后台管理系统,采用 Java + Spring Boot 技术栈,实现了水位、降雨、蒸发、气象等多类监测数据的采集、存储和展示功能。系统需要与多个第三方平台进行对接,包括视频监控平台、GIS 服务平台等。

在实际开发过程中,我们遇到了两个具有代表性的技术挑战:

  1. 视频图片鉴权问题:视频平台接口返回的图片资源需要鉴权,但远程签发接口权限不稳定,需要切换到本地签名方案
  2. 等值面外部接口开发:需要调用第三方 GIS 服务获取降雨等值面数据,而不是使用本地算法

这两个场景虽然业务不同,但都涉及第三方接口集成,都需要在保证功能正确性的同时,考虑性能、缓存、安全等因素。接下来我将详细记录这两个场景的完整解决方案。

第一部分:视频图片鉴权改造

1.1 背景与问题

这次改造的起点,是视频设备接口需要返回 thumbnailAuth 字段,供前端访问视频平台上的缩略图、告警图片等静态资源。

最早的实现方案是严格按照视频平台文档来做,基本流程如下:

  1. 先通过 /resourceSharing/oauth/token 获取共享账号 token
  2. 再带着 token 调用 /lisa/access/sign
  3. 从接口返回中取得鉴权头部信息:
    • x-apig-nonce
    • x-apig-timestamp
    • x-apig-appid
    • x-apig-sign
    • x-datax-code
  4. 将这组鉴权头返回给前端,用于图片资源访问

从实现层面看,这条链路并不复杂,但联调中很快遇到了平台权限问题:接口会返回 401,提示“共享用户无权限访问该接口”。

这意味着问题不再是单纯的代码通不通,而变成了两个方向都需要确认:

  • 当前代码是不是按平台要求调用了接口
  • 即使代码调用方式正确,共享账号本身是否具备 /lisa/access/sign 的访问权限

1.2 问题定位过程

最初的技术判断

最开始根据仓库现有实现,平台是这样识别共享账号的:

  • video.api.mobile 用于调用 /resourceSharing/oauth/token
  • 平台返回一个 token
  • 后续所有视频接口都在请求头里带这个 token

也就是说,平台侧对“共享账号是谁”的识别,至少在当前代码里,是通过 mobile -> token 这条链路完成的。

旧版图片鉴权实现也是沿着这个思路写的:

  • 配置读取 video.api.image-app-id
  • 获取 token
  • 调用 /lisa/access/sign
  • 解析返回值并缓存 9 分钟

联调日志给出的信号

联调中出现过几类不同的错误:

  • 请求缺少token
  • 共享用户无权限访问该接口
  • thumbnailAuth 返回为空

这几类错误其实对应着不同层次的问题:

  • “请求缺少 token”说明调用方式不对
  • “共享用户无权限访问该接口”说明请求已经到平台了,但账号权限不足
  • thumbnailAuth 为空则可能是业务逻辑没有把鉴权结果正确注入返回对象

在当时的上下文下,旧实现一度看上去是成立的,因为日志已经能证明 token 获取成功、视频列表接口可用、播放地址转换接口可用。但问题在于,图片鉴权这一条链路始终被平台权限卡住,没法稳定联调完成。

新线索带来的方案变化

随后拿到了一段 Lisa 平台的第三方图片鉴权工具类,核心逻辑是:

1
2
3
4
String nonce = "access_" + UUID.randomUUID().toString().substring(0, 6);
String timestamp = String.valueOf(System.currentTimeMillis());
String preSign = nonce + timestamp + accessCode + passWord + timestamp;
String sign = sha256(preSign);

然后本地直接生成:

  • x-apig-nonce
  • x-apig-timestamp
  • x-apig-appid
  • x-apig-sign
  • x-datax-code

这条线索的重要性在于,它提供了另一种完全不同的实现路径:

  • 不依赖 /lisa/access/sign
  • 不依赖该接口的权限开通状态
  • 只依赖共享账号 accessCode/password

在平台文档与平台工具类之间存在差异的情况下,最终我们明确转向:不走 lisa-sign,改为本地生成图片鉴权头

1.3 改动目标

这次改动的目标非常明确:

  1. 保留现有视频主链路能力:token 获取继续保留,视频列表、播放地址、云台等接口仍按原方式工作
  2. 仅替换图片鉴权这一路:不再调用 /lisa/access/sign,改为根据 accessCode/password 本地生成鉴权头
  3. 保持对前端接口结构兼容thumbnailAuth 字段仍然存在,返回字段名不变
  4. 保持缓存策略稳定:鉴权信息仍缓存 9 分钟,避免频繁重新生成

换句话说,这不是大面积重构视频模块,而是在“最小变更集”的原则下,对图片鉴权来源做了一次替换。

1.4 具体代码改动

1.4.1 扩展视频配置模型

文件:cnsci-common/src/main/java/cn/cnsci/common/config/VideoApiProperties.java

新增了两个配置项:

  • accessCode
  • password

这样 Spring 在读取 video.api 配置时,就能把第三方共享账号凭据注入到 VideoService 中。

保留了原有字段:

  • baseUrl
  • mobile
  • imageAppId
  • timeout

这里保留 imageAppId 的原因不是当前逻辑还在用,而是为了兼容已有配置和后续回退可能性,避免这次改动顺手把无关字段清掉,导致额外风险。

1.4.2 新增本地图片鉴权工具类

文件:cnsci-common/src/main/java/cn/cnsci/common/utils/http/LisaTripartiteAuthUtils.java

新增了一个专门的工具类,职责很单一:根据共享账号 accessCode/password 生成 Lisa 图片访问鉴权头。

核心逻辑:

  1. 校验 accessCode/password 是否为空
  2. 生成 access_ 前缀的随机 nonce
  3. 生成当前毫秒级时间戳
  4. 按平台工具类给出的规则拼接签名原文:nonce + timestamp + accessCode + password + timestamp
  5. 使用 SHA-256 生成十六进制签名
  6. 组装并返回完整 header map

这样做的好处是:

  • 逻辑集中,不污染 VideoService
  • 以后如果签名规则有变化,只需修改一个地方
  • 单元测试也更容易覆盖

1.4.3 替换 VideoService 的实现

文件:cnsci-admin/src/main/java/cn/cnsci/web/service/VideoService.java

这是这次最核心的业务改动。

旧逻辑

  • 先查 Redis 缓存
  • 缓存未命中后,读取 imageAppId
  • 获取 token
  • 调用 /lisa/access/sign
  • 解析接口返回的 data
  • 成功后写缓存

新逻辑

  • 先查 Redis 缓存
  • 缓存未命中后,直接调用 LisaTripartiteAuthUtils.generateAuthHeader()
  • 如果 accessCode/password 未配置,则记录 warn 日志并返回 null
  • 如果生成成功,则写入缓存 9 分钟
  • 返回给上层调用者

这一步之后,图片鉴权信息的来源已经彻底切成“本地生成”,不会再依赖平台是否开放 /lisa/access/sign

1.4.4 调整应用配置

文件:cnsci-admin/src/main/resources/application.yml

新增配置:

1
2
3
4
video:
api:
access-code: gdswnltsgc
password: PulklN*c@))IDhlJN~Pp

同时保留了 image-app-id: appid,并补充说明:当前分支已经不再使用 image-app-id/lisa/access/sign,但字段仍保留。

测试配置文件 cnsci-admin/src/test/resources/application-video-test.yml 也同步补上了测试用的 access-code/password

1.4.5 重写图片鉴权单元测试

文件:cnsci-admin/src/test/java/cn/cnsci/web/service/VideoServiceImageAuthTest.java

测试由原来的“模拟 /lisa/access/sign HTTP 服务返回”改成了“验证本地签名生成逻辑”。

覆盖了两个核心场景:

场景一:本地生成成功

验证点包括:

  • 返回结果不为空
  • x-apig-appid == accessCode
  • x-datax-code == accessCode
  • x-apig-nonceaccess_ 开头
  • x-apig-timestamp 已生成
  • x-apig-sign 与测试端按同样算法计算出的值一致
  • Redis 缓存写入被正确调用,过期时间为 9 分钟

场景二:配置缺失时降级

验证当 accessCode 缺失时:

  • 方法返回 null
  • 不写 Redis 缓存

这类测试非常关键,因为它不仅验证“正常情况能工作”,也验证“配置不完整时系统会如何表现”。

1.5 为什么这次改动更顺畅

从工程角度看,这次切换到本地生成方案,比原来走 /lisa/access/sign 更顺畅,主要有以下几个原因。

1. 去掉了一个外部依赖点

原方案需要额外依赖一个远程接口 /lisa/access/sign,而且这个接口还受平台权限控制。

只要其中任意一环有问题,都可能失败:

  • token 是否正确
  • imageAppId 是否正确
  • 接口是否开放权限
  • 平台返回结构是否稳定
  • 联调环境是否一致

本地生成后,这条链路直接缩短为:

  • 配置存在
  • 算法正确
  • 缓存正确

可控性明显更高。

2. 不再受平台账号权限阻塞

旧方案最大的现实问题不是代码写不出来,而是共享账号对 /lisa/access/sign 的权限不稳定或未开通。

这类问题属于典型的“外部平台约束”,排查成本高,等待时间长,而且开发过程中很容易陷入误判:

  • 到底是接口文档不对
  • 还是账号没权限
  • 还是请求头不完整
  • 还是 appid 不对

本地生成方案直接绕过这个卡点,把问题收敛到后端自己的实现范围内。

3. 更适合缓存设计

旧方案获取的是平台返回的一次性鉴权结果;新方案虽然也是生成 10 分钟有效的鉴权头,但本地可随时重建,因此缓存策略更自然。

当前仍然使用 9 分钟缓存,理由没有变:

  • 平台侧有效期 10 分钟
  • 本地缓存压缩到 9 分钟,给过期留缓冲
  • 避免把边缘过期数据返回给前端

4. 测试更直接

旧测试需要模拟 HTTP 服务端行为;新测试只需要验证签名规则和缓存行为。

这会带来两个好处:

  • 测试更稳定,不依赖本地起临时 server
  • 测试更聚焦业务核心,而不是围着 HTTP 交互打转

1.6 需要注意的风险与边界

这次方案虽然更顺,但也不是没有边界条件。

1. 真实密码进入了配置文件

当前为了尽快跑通,已经把 access-code/password 直接写入 application.yml

这在联调阶段是最省事的,但从长期维护和安全角度看不是最佳方案。后续更建议:

  • 改用环境变量
  • 或者使用更安全的配置托管方式
  • 至少不要把真实凭据继续扩散到更多环境文件和文档中

2. 平台算法如果变更,需要同步更新

本地生成方案的前提是:平台验签规则稳定,且与当前拿到的工具类一致。

如果未来平台调整了:

  • nonce 规则
  • sign 拼接顺序
  • 哈希算法
  • header 字段名

那么本地生成逻辑也必须同步更新。

3. 旧字段仍保留,避免误删

当前 image-app-id 还在配置模型和 yml 中保留,但本分支下已经不参与图片鉴权。

这属于一种“兼容保留”,优点是降低回退成本,缺点是容易让后来接手的人误以为它还在起作用。

因此在后续整理配置时,最好再加一轮说明文档,明确哪些字段仍在使用,哪些只是历史兼容字段。

第二部分:等值面外部接口开发

2.1 需求背景

现有 /api/rain/contour 接口使用本地 IDW(反距离加权)算法生成等值面,现需要新增接口调用第三方 GIS 服务获取等值面数据。

[!info] 第三方接口信息

  • 地址http://10.144.38.28:9981/isosurface/calculateRainfallIsosurface
  • 请求方式:POST (JSON)
  • 鉴权方式:RSA(与批量字典接口相同)

这个需求的背景是:本地算法虽然灵活,但计算效率受数据量和算法复杂度限制;而第三方 GIS 服务专门针对空间数据分析进行了优化,能够提供更高效、更专业的等值面生成能力。

2.2 API 设计

项目
路径 POST /api/rain/contour/external
请求方式 POST
Content-Type application/json

请求参数

参数 类型 必填 说明
startTime String 开始时间,默认当天08:00
endTime String 结束时间,默认当前小时前
scale Double 精度,默认0.1
addvcd String 行政区划码
bsnm String 流域ID
stnm String 站点名称
frgrd String 报讯等级
admauth String 管理单位

2.3 实现步骤

2.3.1 配置添加

application.yml 中添加 gis-isosurface 配置:

1
2
3
4
5
6
7
8
third-party:
api:
gis-isosurface:
base-url: http://10.144.38.28:9981
app-id: ${third-party.api.hydrology.app-id}
app-name: ${third-party.api.hydrology.app-name}
public-key: ${third-party.api.hydrology.public-key}
timeout: 30000

同时在 ThirdPartyApiProperties.java 中添加 gisIsosurface 属性。

2.3.2 业务逻辑实现

新增业务逻辑类 RainfallContourExternalBiz.java,核心处理流程如下:

1
2
3
4
5
6
7
// 固定筛选:只查询雨量站(PP)
filterParams.put("sttps", Collections.singletonList("PP"));

// 过滤经纬度为空的测站
List<RainfallRealtimeResponse> validStations = stations.stream()
.filter(s -> s.getLGTD() != null && s.getLTTD() != null)
.collect(Collectors.toList());

2.3.3 Controller 开发

RainfallRealtimeController.java 中新增 /contour/external 接口:

1
2
3
// 直接返回第三方响应的JSON,不再次包装
Object jsonObject = JSON.parse(result);
return AjaxResult.success(jsonObject);

2.3.4 DTO 扩展

RainfallContourQueryDTO.java 中添加 scale 字段,用于控制等值面生成的精度。

2.4 处理逻辑流程

1
2
3
4
5
6
7
8
9
10
11
graph TD
A[开始] --> B{传入时间参数?}
B -->|是| C[使用传入的时间]
B -->|否| D[计算默认时间]
D --> E[当天08:00 → 当前小时前]
C --> F[查询测站]
F --> G[过滤: 只保留雨量站PP]
G --> H[过滤: 经纬度不为空]
H --> I[提取STCD列表]
I --> J[调用第三方API]
J --> K[返回响应]

2.5 遇到的问题及解决

问题一:GET 请求报错

[!bug] 错误信息

1
Request method 'GET' not supported
  • 原因:接口是 @PostMapping,但请求用了 GET
  • 解决:使用 POST 请求

这是一个很低级但容易犯的错误。在接口文档中明确标注了 POST 方式,但测试时很容易惯性使用 GET 请求。

问题二:返回格式嵌套

[!warning] 问题描述
返回值被再次包装成 AjaxResult,导致格式嵌套

  • 原因:直接 return success(result) 把字符串又包装了一层
  • 解决:先用 JSON.parse() 解析第三方响应,再返回
1
2
3
4
5
6
// 修复前
return success(result);

// 修复后
Object jsonObject = JSON.parse(result);
return AjaxResult.success(jsonObject);

这个问题很有意思。原始设计中,我们希望统一响应格式,所以所有接口都返回 AjaxResult。但第三方接口返回的本身就是一个 JSON 字符串(里面还包含了 XML 格式的 KML 数据),如果直接包装,会导致两层 JSON 解析,客户端使用时会非常麻烦。

解决思路是:既然第三方已经返回了结构化的 JSON,我们只需要把它解析出来,再放入我们的响应结构中。这样客户端拿到的是干净的嵌套结构。

问题三:测站类型过滤

[!warning] 问题描述
ZZ(水位站)数据也被传入第三方

  • 原因:没有过滤测站类型
  • 解决:添加 STTP = 'PP' 过滤,只保留雨量站
1
filterParams.put("sttps", Collections.singletonList("PP"));

这个问题暴露出一个业务理解上的偏差:等值面分析通常只需要降雨数据,而不需要水位数据。虽然从技术上说,把所有测站数据都传过去也能生成结果,但这会增加网络传输量、第三方处理时间,而且生成的结果可能包含不相关的数据。

正确的做法是在调用第三方接口前,先进行业务过滤,只保留雨量站(PP 类型)的数据。

2.6 测试验证

测试命令

1
2
3
4
5
6
7
8
9
# 无参数(使用默认时间)
curl -X POST "http://localhost:8086/api/rain/contour/external" \
-H "Content-Type: application/json" \
-d '{}'

# 指定时间
curl -X POST "http://localhost:8086/api/rain/contour/external" \
-H "Content-Type: application/json" \
-d '{"startTime": "2026-03-17 08:00:00", "endTime": "2026-03-17 15:00:00", "scale": 0.1}'

响应格式

1
2
3
4
5
6
7
8
9
{
"errorCode": 0,
"errorKeyword": null,
"errorParam": {},
"message": null,
"success": true,
"errorLevel": null,
"data": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml>..."
}

2.7 技术要点总结

  1. 第三方API集成:使用 ThirdPartyApiClient.sendPostJson() 调用
  2. 测站过滤:通过 stationFilterParams 传递过滤条件
  3. 时间默认值:参考 RainfallRealtimeServiceImpl.calculateTimeRange() 逻辑
  4. 响应处理:需要先 JSON.parse() 解析,避免嵌套

第三部分:经验与总结

3.1 第三方接口集成的一般性原则

通过这两个项目实践,我总结出第三方接口集成的一些一般性原则:

1. 优先选择可控性强的方案

在视频图片鉴权的案例中,最初我们选择了依赖远程接口的方案,虽然这个方案在理论上更“规范”,但它引入了一个不可控的外部依赖。当这个依赖出现问题时,我们很难快速定位和解决。

相比之下,本地签名方案虽然看起来不够“规范”,但它把控制权掌握在自己手中,不依赖平台的接口权限,不受平台服务稳定性的影响。在实际工程中,可控性往往比理论上的规范性更重要

2. 做好降级和容错

无论是调用第三方接口还是本地生成鉴权信息,都要考虑失败的情况。在视频图片鉴权方案中,我们保留了配置缺失时的降级逻辑:当 accessCode/password 未配置时,返回 null 而不是抛出异常。这样即使配置有问题,也不会影响整个系统的运行。

3. 缓存策略要合理

对于可以重复使用的鉴权信息、配置信息等,应该设计合理的缓存策略。在视频图片鉴权案例中,我们使用 9 分钟的缓存(平台有效期 10 分钟),既能减少重复计算,又能在平台端失效前主动刷新,避免边缘情况。

4. 测试要覆盖边界条件

在等值面接口开发中,我们遇到了返回格式嵌套、请求方式错误等问题。这些问题如果只在正常路径上测试,是发现不了的。因此,测试用例不仅要覆盖正常情况,还要覆盖:

  • 参数缺失的情况
  • 参数边界值的情况
  • 异常情况

5. 做好日志记录

在调用第三方接口时,日志记录非常重要。它不仅能帮助我们排查问题,还能让我们了解接口的调用情况和性能表现。建议记录:

  • 请求的参数
  • 请求的时间
  • 响应的状态
  • 响应的耗时
  • 异常信息

3.2 最小变更集原则

在视频图片鉴权改造中,我们始终坚持“最小变更集”的原则:只替换图片鉴权这一路,保留其他所有功能。这种做法的优点是:

  1. 降低风险:只改动了必要的代码,影响范围小
  2. 易于回滚:如果新方案有问题,可以快速回退到旧方案
  3. 便于测试:只需要测试改动的部分,不需要回归整个模块

3.3 文档与配置管理

在这两个项目中,我们也注意到文档和配置管理的重要性:

  1. 配置模型扩展:新增配置项时,要在对应的 Properties 类中添加字段,并在注释中说明用途
  2. 保留兼容字段:像 imageAppId 这样的字段,即使不再使用,也建议保留一段时间,避免回滚时出现问题
  3. 说明文档:对于复杂的配置项,最好在注释中说明其用途和可能的影响

结语

本文记录了广东水文监测系统中两个典型的后端接口开发场景:视频图片鉴权改造和等值面外部接口开发。这两个场景虽然业务不同,但都涉及与第三方平台的接口集成,都需要在功能、性能、可维护性之间做权衡。

通过这两个项目,我深刻体会到:

  1. 工程问题没有绝对的最优解:远程签发方案理论上更规范,本地生成方案实践上更可控。选择哪个方案,要根据实际情况来判断。

  2. 最小变更集是个好原则:不要为了“美观”而做不必要的改动,每一个改动都应该有明确的目的和预期效果。

  3. 测试覆盖要全面:不仅要测正常路径,还要测边界条件和异常情况。

  4. 日志和文档很重要:好的日志能帮助我们快速定位问题,好的文档能帮助后来的开发者理解系统的设计意图。

希望本文的内容能对有类似需求的开发者有所帮助。如果有任何问题或建议,欢迎交流讨论。