23 跨端数据格式统一与联调测试

跨端数据格式统一与联调测试

关联:索引

要解决的问题

章节内容(本讲核心):

与前置知识衔接(避免重复):

项目工坊:

项目选择建议(本讲统一)

学生任务:

大模型任务:

作业:


统一协议草案(本讲统一口径)

1. 顶层 Envelope(跨端统一)

{
  "schema_version": "1.0.0",
  "trace_id": "T-20260415-a1b2c3",
  "msg_id": "M-20260415-0001",
  "source": "web",
  "target": "ros2",
  "topic": "/sorting_arm/cmd",
  "event": "arm.command.request",
  "ts_ms": 1776200000123,
  "content_type": "application/json",
  "payload": {
    "device_id": "arm_01",
    "action": "stop",
    "params": {
      "reason": "user_click"
    }
  }
}

2. 错误回执统一格式(建议)

{
  "schema_version": "1.0.0",
  "trace_id": "T-20260415-a1b2c3",
  "msg_id": "M-20260415-0002",
  "source": "ros2",
  "target": "web",
  "topic": "/sorting_arm/status",
  "event": "arm.command.reject",
  "ts_ms": 1776200000456,
  "content_type": "application/json",
  "payload": {
    "ok": false,
    "code": "BAD_FIELD_TYPE",
    "message": "params.speed should be number",
    "field": "payload.params.speed"
  }
}

  1. 联调失败最多的不是“连不上”,而是“字段语义不一致”。
  2. 没有统一 Envelope,日志很难跨端串起来。
  3. 没有错误回执规范,学生只能看到“失败”,看不到“为什么失败”。

1) 命名约定(本课程统一)

2) 必填与选填建议

3) 类型约束建议

type Envelope<TPayload extends Record<string, unknown>> = {
  schema_version: string
  trace_id: string
  msg_id: string
  source: string
  target?: string
  topic?: string
  event: string
  ts_ms: number
  content_type?: 'application/json'
  payload: TPayload
}

type ValidateResult<T> =
  | { ok: true; data: T }
  | { ok: false; code: string; message: string; field?: string }

function isObject(x: unknown): x is Record<string, unknown> {
  return typeof x === 'object' && x !== null && !Array.isArray(x)
}

export function validateEnvelope(input: unknown): ValidateResult<Envelope<Record<string, unknown>>> {
  if (!isObject(input)) {
    return { ok: false, code: 'BAD_BODY', message: 'body must be object' }
  }

  const requiredStringFields = ['schema_version', 'trace_id', 'msg_id', 'source', 'event'] as const
  for (const field of requiredStringFields) {
    if (typeof input[field] !== 'string' || input[field] === '') {
      return { ok: false, code: 'BAD_FIELD', message: `${field} must be non-empty string`, field }
    }
  }

  if (typeof input.ts_ms !== 'number' || !Number.isFinite(input.ts_ms)) {
    return { ok: false, code: 'BAD_FIELD_TYPE', message: 'ts_ms must be finite number', field: 'ts_ms' }
  }

  if (!isObject(input.payload)) {
    return { ok: false, code: 'BAD_FIELD_TYPE', message: 'payload must be object', field: 'payload' }
  }

  return { ok: true, data: input as Envelope<Record<string, unknown>> }
}
from pydantic import BaseModel, Field
from typing import Dict, Any, Optional, Literal

class EnvelopeModel(BaseModel):
    schema_version: str = Field(min_length=1)
    trace_id: str = Field(min_length=1)
    msg_id: str = Field(min_length=1)
    source: str = Field(min_length=1)
    target: Optional[str] = None
    topic: Optional[str] = None
    event: str = Field(min_length=1)
    ts_ms: int
    content_type: Optional[Literal["application/json"]] = None
    payload: Dict[str, Any]
异常类型 示例 策略 日志要求
字段缺失 trace_id 拒绝(Reject) 记录 msg_id、缺失字段
字段类型错误 ts_ms: "1776..." 拒绝(Reject) 记录字段路径与期望类型
可修复轻微异常 target 缺失 降级(Default) 记录默认值来源
额外未知字段 payload.extra_x 兼容(Pass-through) 记录 schema_version

六、练习(至少 2 题)

  1. validateEnvelope 扩展为:ts_ms 允许秒级时间戳输入并自动转毫秒。
    提示:先判断值域再转换,转换后仍要保证有限数。
  2. 设计一个 event 命名规范(例如 domain.action.stage),并给出 3 个本项目示例。
    提示:至少覆盖请求、成功回执、失败回执三类。

八、学生任务(提交物与标准)

  1. 链路连通性检查(连接是否建立)
  2. 协议合法性检查(Envelope 是否通过)
  3. 业务语义检查(event/payload 是否符合预期)
  4. 异常回执检查(错误是否可定位)
  5. 回归复测(修复后是否破坏其他场景)

1) 启动联调所需进程(本讲配套项目:直连 rosbridge)

# 终端 A:启动 rosbridge(示例)
ros2 launch rosbridge_server rosbridge_websocket_launch.xml port:=9090 address:=0.0.0.0
# 终端 B:启动 ROS2 模拟节点(示例)
cd 10_cross_end_e2e_lab/ros2_ws
colcon build --symlink-install
source install/setup.bash
ros2 run sorting_arm_mock arm_mock
# 终端 C:启动 Web 项目(本讲综合项目)
cd 10_cross_end_e2e_lab
npm.cmd install
npm.cmd run dev

1.1) 可选:引入 FastAPI 网关(扩展路线)

推荐启动方式(示例):

# 在你的 FastAPI 项目目录执行(示例)
fastapi dev

2) 联调时的最小消息流(Web 侧示例)

{
  "schema_version": "1.0.0",
  "trace_id": "T-20260415-001",
  "msg_id": "M-req-001",
  "source": "web",
  "target": "ros2",
  "topic": "/sorting_arm/cmd",
  "event": "arm.command.request",
  "ts_ms": 1776200100123,
  "payload": {
    "device_id": "arm_01",
    "action": "home",
    "params": {}
  }
}

建议每条日志至少包含:

示例日志(JSON line):

{"trace_id":"T-20260415-001","msg_id":"M-req-001","event":"arm.command.request","stage":"ros2.validate","result":"fail","error_code":"BAD_FIELD_TYPE","field":"payload.params.speed"}

2.1) ROS2 侧联调自检(推荐按顺序执行)

目标:在 ROS2 侧用最少命令确认“消息已到达 + Envelope 合法 + trace_id 能串联 + 回执能匹配”。

  1. 确认 rosbridge 暴露了目标话题(类型与 QoS 也能看到):
ros2 topic info -v /sorting_arm/cmd
ros2 topic info -v /sorting_arm/status
  1. 抓取一条 /sorting_arm/cmd 并检查 data 字段是否是 Envelope JSON:
ros2 topic echo /sorting_arm/cmd --once
  1. 用一键校验脚本验证 Envelope 的必填字段(推荐复制执行):
ros2 topic echo /sorting_arm/cmd --once | python3 - << 'PY'
import json, re, sys

text = sys.stdin.read()
m = re.search(r"data:\\s*'(?P<j>.*)'\\s*$", text, re.S)
if not m:
    print("FAIL: cannot find std_msgs/String.data")
    raise SystemExit(1)

raw = m.group("j").replace("\\\\'", "'")
obj = json.loads(raw)
need = ["schema_version","trace_id","msg_id","source","event","ts_ms","payload"]
miss = [k for k in need if k not in obj]
if miss:
    print("FAIL: missing fields:", miss)
    raise SystemExit(2)
if not isinstance(obj["payload"], dict):
    print("FAIL: payload must be object")
    raise SystemExit(3)
print("OK: trace_id =", obj["trace_id"], "event =", obj["event"])
PY
  1. 抓取一条 /sorting_arm/status,核对两件事:
ros2 topic echo /sorting_arm/status --once

常见错误快速定位:

用例编号 场景 输入 预期输出 判定标准
TC-01 正常下发 合法 Envelope + action=home 回执 ok=true 前后端与 ROS2 日志 trace 一致
TC-02 缺失字段 trace_id 回执 ok=false + BAD_FIELD 错误字段路径准确
TC-03 类型错误 ts_ms 为字符串 回执 BAD_FIELD_TYPE 不进入执行节点
TC-04 未知字段 payload 增加 debug_x 允许通过并 warning 主流程成功且有兼容日志
TC-05 重放消息 重复 msg_id 去重或拒绝 无重复执行

四、练习(至少 2 题)

  1. 设计“时间戳异常”测试:秒/毫秒/微秒混入时,系统如何识别并处理?
    提示:先定义判定阈值,再定义修复策略与拒绝策略。
  2. 设计“部分链路失败”测试:网关校验通过但 ROS2 执行失败,前端如何展示状态?
    提示:区分 validate_failexecute_fail 两类错误码。

课后作业

参考与延伸

Markdown 与代码自检清单(已完成)

  1. 标题层级连续:######,未出现跳级。
  2. 列表、表格、代码块闭合正确,代码块均包含语言标签(json / ts / python / bash / text)。
  3. 所有命令均附用途说明与风险提示(如 0.0.0.0 暴露范围说明)。
  4. TypeScript 与 Python 代码语法结构完整,可直接复制到对应文件再细化业务字段。
  5. 协议示例、字段约定、测试用例三部分口径一致(trace_id/msg_id/ts_ms 命名统一)。