JiduClient 是整个 OTX 代理插件中与集度云端服务进行 HTTPS 双向认证通信的核心枢纽类。它以单例模式运行,封装了 libcurl HTTP 客户端能力,并通过 host 路由 + 请求类型分发 + 预处理/后处理 的三段式调用框架,统一管理四大服务网关(EOL、PKI、Diag、Config)的所有 API 请求。其核心价值在于:上层 OTX 诊断脚本只需传递一个整数类型的接口编号(iType)和 JSON 入参,即可透明地完成环境路由、证书加载、身份认证签名、请求构造、响应解析与数据中心缓存的全流程操作。
类继承体系与在系统中的定位
JiduClient 继承自 HttpClient,后者是对 libcurl 的 C++ 封装,提供 post() 和 get() 两个基础原子操作。JiduClient 在这一层之上叠加了三项关键能力:多服务网关路由、双向 TLS 证书管理、以及基于 ECDSA 签名的 API 代理认证。
classDiagram
class HttpClient {
-string clientCert
-string clientKey
-string token
+post(url, params, response) bool
+get(url, response) bool
+setClientCert(cert) bool
+setClientKey(key) bool
+setToken(token) bool
}
class JiduClient {
-static JiduClient* instance
+string deviceSn
+string eolHost
+string pkiHost
+string cfgHost
+string diagHost
+callServer(iType, inParams, outBuffer, outBufferSize) int
+loadEnvirment() bool
+loadParam() bool
+init() void
-gen_pem(p12Path, passwd, cert, key) void
-parsed(response) int
-getAccessToken(token) bool
-callServer_preHandler(iType, inParams) int
-callServer_affterHandler(iType, inParams, response) int
-desensitize(obj) void
-add_powerType(inParams) void
-add_carType(inParams) void
}
HttpClient <|-- JiduClient : 继承
JiduClient ..> EnvConfig : 获取环境配置
JiduClient ..> DataCenter : 读写缓存
JiduClient ..> vtable : 路由表查询Sources: jidu_client.h, HttpClient.h
四大服务网关与 host 路由架构
JiduClient 对接集度后端的四个独立服务网关,每个网关有不同的认证方式与 URL 模式。所有 API 端点的路由信息集中定义在 vtable(位于 jidu_uri.h)中:
typedef struct invoke { int sn = 0; string method; string host; string url; } invoke_t;
invoke_t vtable[100] = { ... };Sources: jidu_uri.h
路由表通过数组索引(即 iType)将每个接口映射到 {HTTP方法, 目标host, URI路径} 三元组。四个 host 的值及其通信策略如下表:
| Host 标识 | 服务网关 | 典型用途 | 认证方式 | URL 构建模式 |
|---|---|---|---|---|
"eol" | EOL 产线服务 | 车辆订单、CCP 数据、TPMS、工位结果 | 无需 token,直连 HTTPS | eolHost + url |
"pki" | PKI 证书服务 | 安全常量查询、证书签发、密钥管理 | 双向 TLS 证书认证(PEM 客户端证书 + 私钥) | pkiHost + url |
"diag" | 诊断服务(售后) | 车辆信息、ECU 列表、证书操作、OTA 升级 | Bearer Token + ECDSA 签名 | diagHost + "/api/business/apiProxy/invoke?lang=" + language |
"cfg" | 配置服务 | VSP 配置上传与状态查询 | Bearer Token + ECDSA 签名 | cfgHost + "/business/apiProxy/invoke" |
这种按 host 分流的架构使得 callServer() 的核心逻辑成为一个 if-else 路由决策链:先查 vtable 获取目标 host,再根据 host 类型执行完全不同的认证与请求构造逻辑。
Sources: jidu_client.cpp, jidu_uri.h
callServer() 请求处理流程
callServer(int iType, string inParams, char* outBuffer, unsigned int outBufferSize) 是 JiduClient 的唯一公开入口方法。其完整处理流程如下图所示:
flowchart TD
A["callServer(iType, inParams, outBuffer, outBufferSize)"] --> B["获取互斥锁 std::lock_guard"]
B --> C["清理上次同 iType 响应缓存"]
C --> D["init() 检查环境是否变更"]
D --> E["记录日志: callServer.interface"]
E --> F["callServer_preHandler() 前置处理"]
F --> G{"vtable[iType].host ?"}
G -->|"diag / cfg"| H["getAccessToken() 获取 Bearer Token"]
H --> I{"Token 有效?"}
I -->|"否"| ERR["返回 -1"]
I -->|"是"| J["构造 API Proxy JSON 包体"]
J --> K["ECDSA 签名: ECDSA_sign(inParams+url+method+serviceName+timestamp, keyPem)"]
K --> L["HttpClient::post() 发送请求"]
G -->|"eol"| M{"IsIntegrationTesting() ?"}
M -->|"是"| N["加载本地 Mock 数据 (JSON/XML)"]
M -->|"否"| O["HttpClient::get() 或 post()"]
G -->|"pki"| P["设置客户端 TLS 证书: setClientCert + setClientKey"]
P --> Q["HttpClient::get() 或 post()"]
L --> R["记录请求元信息到 DataCenter"]
N --> R
O --> R
Q --> R
R --> S{"response 是否为空?"}
S -->|"是"| RERR["设置 errMsg, 返回 -2"]
S -->|"否"| T["parsed() 解析 code 字段"]
T --> U{"code == 0 或 200?"}
U -->|"否"| V["提取 message/msg 到 errMsg, 返回 -2"]
U -->|"是"| W["日志打印 (PKI 接口脱敏)"]
W --> X["callServer_affterHandler() 后置处理"]
X --> Y{"后置处理结果 < 0?"}
Y -->|"是"| YERR["设置 errMsg, 返回错误码"]
Y -->|"否"| Z["memcpy 到 outBuffer, 返回 response.length()"]这个流程中有几个关键设计决策值得注意:
全局互斥锁:
static std::mutex mLock确保所有callServer()调用串行化执行,防止并发请求导致DataCenter缓存状态不一致。jidu_client.cpp环境热检测:每次调用都会执行
init(),其内部通过EnvConfig::checkEnvironment()判断运行环境是否变更,若变更则重新加载证书和主机配置。jidu_client.cpp集成测试 Mock 机制:当
IsIntegrationTesting()为 true 时(通过 XML 配置文件Diagnosis.Entry.exe.config中的IsIntegrationTesting键判断),EOL 请求不会真实发出,而是读取本地EolServerMockData\目录下的 JSON 或 XML 文件作为模拟响应。这为离线测试提供了完整的 fake server 能力。jidu_client.cpp
Sources: jidu_client.cpp
双向 TLS 认证:PKI 网关的客户端证书机制
PKI(公钥基础设施)服务是集度安全体系中最敏感的网关,处理车辆证书签发、密钥查询等操作。与 PKI 通信时,JiduClient 需要同时提供 客户端证书(client certificate) 和 客户端私钥(client private key),实现完整的双向 TLS 认证。
证书加载链路
flowchart LR
A["pver 文件 (加密的JSON)"] -->|"loadParam()"| B["解密 certPwd"]
B -->|"DES-CBC 解密"| C["获得明文密码"]
C -->|"gen_pem()"| D["parse_cert_p12()"]
D -->|"从 P12 提取"| E["certPem (X.509)"]
D -->|"从 P12 提取"| F["keyPem (私钥)"]
E -->|"setClientCert()"| G["curl blob_clientCert"]
F -->|"setClientKey()"| H["curl blob_clientKey"]loadParam() 从应用程序目录下的 pver 文件读取设备身份信息。该文件是加密的 JSON 结构,包含 deviceID(设备序列号)和 certPwd(证书密码,使用 DES-CBC 算法加密)。解密使用的硬编码密钥为 "siuhuali"。解密后的明文密码配合 certName 字段定位到 .p12 证书文件,再通过 parse_cert_p12() 函数解析出 PEM 格式的证书和私钥。
// loadParam() 中的核心解密逻辑
int bLen = base64_decode(tempString, (int)strlen(tempString), data, 64);
int rLen = des_cbc_pkcs5_decrypt(data, bLen, (char*)"siuhuali", certPwd, 64);Sources: jidu_client.cpp, algorithm.h
证书使用时机
PKI 认证证书仅在 host 为 "pki" 时设置。在 callServer() 的 pki 分支中:
setClientCert(this->certPem);
setClientKey(this->keyPem);这会在底层 libcurl 中启用双向 TLS —— 服务端会验证客户端证书是否由受信任的 CA 签发,同时客户端也验证服务端证书链。值得注意的是,HttpClient 中将 CURLOPT_SSL_VERIFYPEER 和 CURLOPT_SSL_VERIFYHOST 均设为 false,意味着客户端不验证服务端证书(这与"双向认证"中服务端验证客户端的方向一致,但客户端验证服务端被跳过)。
Sources: jidu_client.cpp, HttpClient.cpp
ECDSA 签名认证:Diag/Cfg 网关的 API Proxy 机制
诊断服务(diag)和配置服务(cfg)使用另一种认证模式 —— API 代理 + ECDSA 数字签名。请求通过统一的 /api/business/apiProxy/invoke 端点转发,原始请求参数被包装在一个 JSON 代理信封中:
{
"serviceName": "diagnostic-adapter-service",
"method": "POST", // 原始 HTTP 方法
"contentType": "application/json",
"invoke": "/uds/device/fetch/vehicle/info", // 原始 URI
"content": "{...}", // 原始请求体 JSON
"timestamp": "1712345678",
"signature": "MEUCIQD...==", // ECDSA 签名
"serialNo": "DEVICE_SN_001" // 设备序列号
}签名的生成逻辑是:将 content + invoke + method + serviceName + timestamp 五个字段拼接为明文字符串,然后使用设备私钥 (this->keyPem) 进行 ECDSA 签名。服务端用对应的公钥验签,确保请求来源合法且未被篡改。
string plainMsg = inParams + vtable[iType].url + vtable[iType].method + serviceName + timeStamp;
ecdsa_signature(plainMsg, this->keyPem, signature);同时,请求头中附带 Authorization: Bearer <token> 字段。Token 来源于诊断仪本地的 profile\profile.json 文件(用户登录后持久化的用户令牌),通过 getAccessToken() 方法读取。
Sources: jidu_client.cpp, jidu_client.cpp
预处理与后处理:请求生命周期钩子
callServer_preHandler 和 callServer_affterHandler 构成了 JiduClient 的 模板方法模式:在请求发送前清理目标缓存,在响应返回后将数据写入 DataCenter。
callServer_preHandler:缓存清理
在请求发出前,根据 iType 清空 DataCenter 中对应的静态字段,确保旧数据不会污染新结果。典型映射关系:
| iType | 清理的 DataCenter 字段 | 说明 |
|---|---|---|
| 4 | m_VehicleOrderInfo.strTpmsId | 胎压 ID |
| 6, 59 | m_EcuSafetyConstant | ECU 安全常量映射表 |
| 19, 58 | m_EolKey (7 个字段) | EOL 全密钥 |
| 20, 56 | m_EcuCertInfo (vid, ecuCa, ecuCertInfo) | ECU 证书信息 |
| 21, 55 | m_aesKey | AES 密钥映射表 |
| 26 | m_VehicleOrderInfo (swpn, ccp, vehicleType, ecuList) | 车辆订单信息 |
| 27 | m_OtaCert (caCert, clientCert) | OTA 证书 |
| 79 | m_vecEbocInfo | EBOC 选装件列表 |
| 88 | m_VehicleOrderInfo.vecListEcuNew | 新版 ECU 列表 |
Sources: jidu_client.cpp
callServer_affterHandler:响应解析与缓存写入
后置处理器负责将服务器返回的 JSON 数据解析后写入 DataCenter 对应的静态字段。这是一个大型 switch-case 结构(约 500 行),覆盖了 20+ 个接口类型的特定解析逻辑。例如,iType=26(工厂环境获取车辆订单信息)会解析 vin、ccpCode、vehicleType、eboc、baseLine、displayVersion、swpn、vehicleEnvironment 等字段,并遍历 ecuList 数组构建 JD_VEHICLE_ORDER_t 结构体写入 DataCenter。
Sources: jidu_client.cpp
环境与工厂的多维配置矩阵
loadEnvirment() 方法实现了 环境 × 工厂 的二维配置矩阵。环境分为 Dev、Test、Staging、Prod 四档(通过 App_Env_t 枚举定义),工厂分为 PMA 和 DMA 两种。每种组合对应独立的证书目录和服务主机域名。
graph TB
subgraph "环境维度 (App_Env_t)"
DEV["Dev (1)"]
TEST["Test (2)"]
STAGING["Staging (3)"]
PROD["Prod (4)"]
end
subgraph "工厂维度"
PMA["PMA 工厂"]
DMA["DMA 工厂"]
AFTER["售后/工程 (无工厂)"]
end
DEV --> CERT_DEV["pkiCertificate/{Factory}/Dev/"]
TEST --> CERT_TEST["pkiCertificate/{Factory}/Test/"]
STAGING --> CERT_STAG["pkiCertificate/{Factory}/Staging/"]
PROD --> CERT_PROD["pkiCertificate/{Factory}/Prod/"]此外,loadEnvirment() 还支持通过本地 WebApi.json 文件覆盖默认的 host 配置。该 JSON 文件按工厂(PMA/DMA)和环境分组,允许运维人员在不重新编译的情况下修改服务端点 URL。特殊的 staging_2_prod() 配置项允许在 Staging 环境下将 EOL 请求路由到生产环境(用于日志上传场景)。
环境默认值为 App_Env_Staging,存储在 EnvConfig 单例中。
Sources: jidu_client.cpp, jidu_macro.h
parsed():响应码提取与嵌套解包
parsed() 方法负责解析 HTTP 响应中的业务状态码。集度服务端 API 的通用响应格式为 {"code": 0, "data": {...}, "message": "..."}。parsed() 同时兼容 code 为字符串或数字的两种情况。当 code == 0 或 code == 200 时,如果 data 内部嵌套了另一个 {"code":..., "data":...} 结构,则会递归解析直到提取出最内层的 data 对象。当 code 非成功状态时,提取 message 或 msg 字段存入 DataCenter::errMsg。
Sources: jidu_client.cpp
辅助方法详解
add_powerType() 与 add_carType()
这两个方法用于在 PKI 和 diag 请求的入参 JSON 中自动注入车辆属性。powerType 来自 DataCenter::m_vehicleInfo.powerType(0=400V 平台,2=800V 平台,其中 2 会被归一化为 1),carType 来自 DataCenter::m_vehicleInfo.carTypeId(1=MarsOne,2=Venus)。它们同时兼容 JSON 格式和 URL 查询字符串格式的入参。
Sources: jidu_client.cpp
desensitize()
对 PKI 接口返回的响应 JSON 进行递归脱敏处理:遍历 JSON 树中所有字符串值,调用 MyStringUtils::desensitize() 将敏感数据(如密钥、证书内容)替换为脱敏标识。脱敏仅在日志打印时生效(Tracer::json),不影响实际写入 DataCenter 和 outBuffer 的原始数据。
Sources: jidu_client.cpp
staging_2_prod() / IsIntegrationTesting() / getMockDataFormat()
这三个文件级函数通过解析 Diagnosis.Entry.exe.config XML 配置文件来控制调试行为:
staging_2_prod():在 Staging 环境下是否将日志上传到 Prod 环境IsIntegrationTesting():是否启用集成测试 Mock 模式getMockDataFormat():Mock 数据文件格式(json或xml)
Sources: jidu_client.cpp
与其他模块的协作关系
JiduClient 处于整个 OTX 代理插件的通信中枢位置,通过以下路径与周边模块交互:
| 协作模块 | 交互方式 | 数据流向 |
|---|---|---|
| EnvConfig | getInstance()->getEnvironment() / getAppPath() / checkEnvironment() | 读取环境配置 |
| DataCenter | 通过 callServer_affterHandler 写入静态字段 | JiduClient → DataCenter(写入) |
| HttpClient | 继承关系,调用 post() / get() / setClientCert() / setClientKey() | JiduClient → HttpClient(委托) |
| 加密算法集 | 调用 ecdsa_signature() / base64_decode() / des_cbc_pkcs5_decrypt() / parse_cert_p12() | JiduClient → algorithm(调用) |
| CVehicleConfig | getInstance()->getDiagnosisDir() 获取 Token 文件路径 | 读取配置目录 |
| Tracer | 日志输出(logger / error / warn / debug / json / hex) | JiduClient → Tracer(写入日志) |
OTX 脚本层(通过 jidu.h) | callServer() C 函数直接委托到 JiduClient::getInstance()->callServer() | OTX 脚本 → JiduClient(入口) |
Sources: jidu.cpp, jidu_client.cpp
补充阅读建议
在理解 JiduClient 的通信架构后,建议按以下顺序深入阅读相关模块:
- HttpClient:基于 libcurl 的 HTTP 请求基类与证书管理 — 理解底层 HTTP 传输、TLS 证书设置、以及请求日志脱敏的具体实现
- callServer 接口设计:请求类型路由、预处理/后处理与脱敏机制 — 深入剖析 vtable 路由表与所有 95 个接口类型的完整清单
- DataCenter:车辆订单、安全常量、证书信息的统一缓存中心 — 理解 JiduClient 写入数据的目的地及缓存结构
- EnvConfig:多环境(Dev/Test/Staging/Prod)配置切换机制 — 理解
loadEnvirment()所依赖的环境配置来源