第3章:指令数据结构设计

好,咱们进入第三个话题。前面聊了指令的生命周期和通信协议,现在该聊聊指令本身长什么样了。

说白了,指令数据结构就是车和云端之间的「共同语言」。你发个"开空调",车得知道是开哪个空调、开多少度、开多久。这些信息怎么组织?这就是本章要解决的问题。

3.1 为什么需要JSON Schema

我记得刚入行那会儿,团队里指令格式全靠口头约定。今天你加个字段,明天他改个类型,后天就出线上事故了。后来我学乖了——必须用Schema来约束。

JSON Schema就像一张蓝图。它告诉你:

  • 哪些字段是必须的
  • 字段的值是什么类型
  • 取值范围是多少
  • 字段之间有没有依赖关系

举个例子,一个简单的车辆控制指令Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "VehicleControlCommand",
  "type": "object",
  "required": ["commandId", "commandType", "timestamp", "params"],
  "properties": {
    "commandId": {
      "type": "string",
      "description": "全局唯一指令ID,UUID格式",
      "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
    },
    "commandType": {
      "type": "string",
      "enum": ["DOOR_LOCK", "DOOR_UNLOCK", "AC_ON", "AC_OFF", "WINDOW_UP", "WINDOW_DOWN", "ENGINE_START", "ENGINE_STOP"]
    },
    "timestamp": {
      "type": "integer",
      "description": "Unix时间戳,毫秒级",
      "minimum": 1600000000000
    },
    "params": {
      "type": "object",
      "description": "指令参数,根据commandType不同而变化"
    }
  }
}
我的小技巧: 实际项目中,我会把Schema放在Git仓库里,每次修改都要走PR评审。这样谁改了啥,一目了然。

3.2 指令类型枚举设计

指令类型不能随便写字符串。你想想看,有人写"lock_door",有人写"DoorLock",还有人写"锁门"...这不乱套了?

我建议用枚举来约束。Python里可以用Enum类:

from enum import Enum

class CommandType(Enum):
    DOOR_LOCK = "DOOR_LOCK"
    DOOR_UNLOCK = "DOOR_UNLOCK"
    AC_ON = "AC_ON"
    AC_OFF = "AC_OFF"
    WINDOW_UP = "WINDOW_UP"
    WINDOW_DOWN = "WINDOW_DOWN"
    ENGINE_START = "ENGINE_START"
    ENGINE_STOP = "ENGINE_STOP"
    HORN_ON = "HORN_ON"
    HORN_OFF = "HORN_OFF"
    LIGHT_ON = "LIGHT_ON"
    LIGHT_OFF = "LIGHT_OFF"
    SUNROOF_OPEN = "SUNROOF_OPEN"
    SUNROOF_CLOSE = "SUNROOF_CLOSE"
    SEAT_HEAT_ON = "SEAT_HEAT_ON"
    SEAT_HEAT_OFF = "SEAT_HEAT_OFF"
    REMOTE_START = "REMOTE_START"
    REMOTE_STOP = "REMOTE_STOP"
    VEHICLE_LOCATE = "VEHICLE_LOCATE"
    VEHICLE_ALARM = "VEHICLE_ALARM

为什么要用枚举?我在项目里吃过亏。有一次,一个同事把"DOOR_LOCK"写成了"DOOR_LOCk"(注意最后那个小写k),结果线上跑了三天才发现。从那以后,所有指令类型必须走枚举校验。

3.3 参数校验规则

参数校验是重中之重。你想想,如果用户发了个"温度调到500度"的指令,车机要是真执行了,那不得出大事?

我一般把参数校验分成三层:

校验层级 校验内容 示例
第一层:类型校验 参数类型是否正确 temperature必须是number
第二层:范围校验 参数值是否在合理范围内 temperature必须在16-30之间
第三层:业务校验 参数组合是否合理 AC_ON时temperature不能为空

来看一个具体的参数校验实现:

def validate_ac_params(params: dict) -> bool:
    """空调指令参数校验"""
    # 第一层:类型校验
    if not isinstance(params.get("temperature"), (int, float)):
        return False
    if not isinstance(params.get("mode"), str):
        return False
    if not isinstance(params.get("fan_speed"), int):
        return False
    
    # 第二层:范围校验
    if not (16 <= params["temperature"] <= 30):
        return False
    if params["mode"] not in ["COOL", "HEAT", "VENT", "AUTO"]:
        return False
    if not (1 <= params["fan_speed"] <= 7):
        return False
    
    # 第三层:业务校验
    # 制热模式下温度不能低于20度
    if params["mode"] == "HEAT" and params["temperature"] < 20:
        return False
    # 自动模式下风速必须设为0(由系统自动控制)
    if params["mode"] == "AUTO" and params["fan_speed"] != 0:
        return False
    
    return True
避坑指南: 我曾经遇到过一个问题——参数校验通过了,但车机执行时报错。后来发现是参数的单位搞错了。云端发的是摄氏度,车机内部用的是华氏度。所以,参数校验里一定要包含单位信息。

3.4 完整的指令数据结构

把上面这些整合起来,一个完整的指令数据结构大概长这样:

{
  "commandId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "commandType": "AC_ON",
  "timestamp": 1700000000000,
  "source": "APP",
  "userId": "user_12345",
  "vin": "LSVAU2A38N2100001",
  "params": {
    "temperature": 24,
    "mode": "COOL",
    "fan_speed": 3,
    "air_circulation": "OUTER",
    "ac_switch": true
  },
  "priority": 5,
  "ttl": 30,
  "signature": "sha256_hash_here"
}

这里我特别想强调一下signature字段。嗯,这个很重要。指令在传输过程中可能被篡改,所以一定要加签名。我习惯用HMAC-SHA256,密钥存在HSM里。

3.5 实战中的注意事项

最后分享几个我在实战中踩过的坑:

  • 字段命名要统一: 别一会儿用camelCase,一会儿用snake_case。我统一用snake_case,因为Python后端解析方便。
  • 版本号不能少: 指令结构会迭代,加个version字段,方便兼容旧版本。
  • 空值处理要明确: 有些字段允许为空,有些不行。在Schema里用nullable明确标注。
  • 日志要记录原始报文: 出问题的时候,原始报文是最重要的排查依据。

核心要点:

  • JSON Schema是指令结构的「宪法」,必须严格执行
  • 指令类型用枚举约束,避免字符串拼写错误
  • 参数校验分三层:类型、范围、业务逻辑
  • 签名机制是安全底线,不能省略

好了,这一章就到这里。下一章咱们聊聊指令下发时的并发控制和幂等性设计——这可是个容易踩坑的地方。