{
  "openapi": "3.0.3",
  "info": {
    "title": "上禾 SH-V20 V3.06 接口增量",
    "version": "1.0.0",
    "description": "上禾体检一体机获取令牌并回传检测结果。实际接口路径依据 ai_api 当前 ThinkPHP 默认分发规则。"
  },
  "paths": {
    "/api/shanghe.callback/token": {
      "post": {
        "summary": "上禾设备获取 Access Token",
        "description": "仅用于已登记为 TOKEN 模式的上禾设备。令牌与 deviceNo 绑定，有效期由 ydy_health_device.token_ttl 决定。",
        "operationId": "shangheV306IssueToken",
        "tags": ["上禾体检一体机"],
        "x-apifox-folder": "上禾体检一体机",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/ShangheTokenRequest"},
              "example": {
                "deviceNo": "SHV20-001",
                "grantType": "client_credentials",
                "appId": "由康比特分配",
                "appSecret": "由康比特分配"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "协议通过 HTTP 200 返回业务码；调用方必须以 recode 判断结果。",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ShangheResponse"},
                "examples": {
                  "success": {"summary": "成功", "value": {"recode": 2000, "remsg": "success", "datas": {"accessToken": "64位访问令牌"}}},
                  "badRequest": {"summary": "参数错误", "value": {"recode": 4000, "remsg": "deviceNo不能为空", "datas": null}},
                  "unauthorized": {"summary": "设备或凭据无权访问", "value": {"recode": 1000, "remsg": "无权访问", "datas": null}},
                  "parseError": {"summary": "JSON解析失败", "value": {"recode": 5000, "remsg": "JSON解析失败", "datas": null}},
                  "internalError": {"summary": "内部错误", "value": {"recode": 5001, "remsg": "system error", "datas": null}}
                }
              }
            }
          }
        }
      }
    },
    "/api/shanghe.callback/result": {
      "post": {
        "summary": "上禾 SH-V20 V3.06 检测结果回调",
        "description": "接收单条或多条检测结果。TOKEN 模式必须携带 X-Access-Token；NONE 模式不携带。相同 deviceNo + recordNo 的完整补传复用原会话并合并指标；完全重复报文不重复产生副作用。请求体最大 8MB，wave/ecgImg 解码后单个附件最大 6MB且仅支持真实 JPEG/PNG。手机号未唯一匹配时仍保存无效会话和 raw，并返回接收成功。",
        "operationId": "shangheV306ResultCallback",
        "tags": ["上禾体检一体机"],
        "x-apifox-folder": "上禾体检一体机",
        "parameters": [
          {
            "name": "X-Access-Token",
            "in": "header",
            "required": false,
            "description": "TOKEN 模式必填；NONE 模式不填。令牌必须与请求中的 deviceNo 一致。",
            "schema": {"type": "string", "maxLength": 128},
            "example": "64位访问令牌"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/ShangheResultRequest"},
              "examples": {
                "single": {
                  "summary": "单条检测",
                  "value": {
                    "deviceNo": "SHV20-001",
                    "macAddr": "00:11:22:33:44:55",
                    "deviceModel": "SH-V20",
                    "unitNo": "UNIT-001",
                    "unitName": "测试机构",
                    "datas": [{"recordNo": "20260825103520", "measureTime": "2026-08-25 10:35:20", "loginType": "0", "userID": "13800138000", "height": "171.5", "weight": "73.7", "bmi": "25.1", "fatRate": "17.6", "muscle": "52.1", "bmr": "1583", "sbp": "120", "dbp": "80", "hr": "72", "sbpR": "118", "dbpR": "78", "hrR": "70", "pi": "3.1", "zwtqq": "12.5"}]
                  }
                },
                "multiple": {
                  "summary": "多条检测",
                  "value": {
                    "deviceNo": "SHV20-001",
                    "deviceModel": "SH-V20",
                    "datas": [
                      {"recordNo": "R-001", "measureTime": "2026-08-25 10:35:20", "userID": "13800138000", "weight": "73.7"},
                      {"recordNo": "R-002", "measureTime": "2026-08-25 10:40:20", "userID": "13900139000", "weight": "65.2"}
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "协议通过 HTTP 200 返回业务码；调用方必须以 recode 判断结果。",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ShangheResponse"},
                "examples": {
                  "success": {"summary": "可靠接收", "value": {"recode": 2000, "remsg": "success", "datas": null}},
                  "unauthorized": {"summary": "设备无权访问", "value": {"recode": 1000, "remsg": "设备未登记或已停用", "datas": null}},
                  "tokenInvalid": {"summary": "Token缺失或失效", "value": {"recode": 2001, "remsg": "accessToken缺失或失效", "datas": null}},
                  "badRequest": {"summary": "参数或附件错误", "value": {"recode": 4000, "remsg": "datas必须为非空数组", "datas": null}},
                  "parseError": {"summary": "JSON解析失败", "value": {"recode": 5000, "remsg": "JSON解析失败", "datas": null}},
                  "internalError": {"summary": "保存失败", "value": {"recode": 5001, "remsg": "system error", "datas": null}}
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ShangheTokenRequest": {
        "type": "object",
        "required": ["deviceNo", "grantType", "appId", "appSecret"],
        "properties": {
          "deviceNo": {"type": "string", "description": "厂家设备编号，必须已登记并属于当前商城"},
          "grantType": {"type": "string", "enum": ["client_credentials"]},
          "appId": {"type": "string"},
          "appSecret": {"type": "string", "format": "password", "writeOnly": true}
        }
      },
      "ShangheResultRequest": {
        "type": "object",
        "required": ["deviceNo", "datas"],
        "additionalProperties": true,
        "properties": {
          "deviceNo": {"type": "string"},
          "macAddr": {"type": "string"},
          "deviceModel": {"type": "string", "example": "SH-V20"},
          "unitNo": {"type": "string"},
          "unitName": {"type": "string"},
          "datas": {"type": "array", "minItems": 1, "items": {"$ref": "#/components/schemas/ShangheMeasurement"}}
        }
      },
      "ShangheMeasurement": {
        "type": "object",
        "required": ["recordNo", "measureTime"],
        "additionalProperties": true,
        "description": "V3.06未知扩展字段允许接收并保存在raw中；已审核字段同时结构化。",
        "properties": {
          "recordNo": {"type": "string", "description": "厂家报告编号；同设备下用于幂等和整体补传合并"},
          "measureTime": {"type": "string", "description": "测量时间", "example": "2026-08-25 10:35:20"},
          "loginType": {"type": "string"},
          "userID": {"type": "string", "description": "用户输入的大陆手机号；支持去除+86、0086、空格和短横线"},
          "height": {"type": "string", "description": "身高 cm"},
          "weight": {"type": "string", "description": "体重 kg"},
          "bmi": {"type": "string"},
          "fatRate": {"type": "string", "description": "体脂率 %"},
          "fat": {"type": "string", "description": "脂肪量 kg"},
          "waterRate": {"type": "string", "description": "体水分率 %"},
          "muscleRate": {"type": "string", "description": "肌肉率 %"},
          "muscle": {"type": "string", "description": "肌肉量 kg"},
          "fatSubCutRate": {"type": "string", "description": "皮下脂肪率 %"},
          "vfal": {"type": "string", "description": "内脏脂肪指数"},
          "bmr": {"type": "string", "description": "基础代谢 kcal"},
          "fatFree": {"type": "string", "description": "去脂体重 kg"},
          "bone": {"type": "string", "description": "骨量 kg"},
          "protein": {"type": "string", "description": "蛋白质量 kg，不是蛋白率"},
          "waterICW": {"type": "string", "description": "细胞内液 kg"},
          "waterECW": {"type": "string", "description": "细胞外液 kg"},
          "idealWeight": {"type": "string", "description": "理想体重 kg"},
          "weAdjus": {"type": "string", "description": "体重控制量 kg"},
          "faAdjus": {"type": "string", "description": "脂肪控制量 kg"},
          "muAdjus": {"type": "string", "description": "肌肉控制量 kg"},
          "bodyAge": {"type": "string", "description": "身体年龄"},
          "bodyScore": {"type": "string", "description": "身体评分"},
          "skeletalMuscle": {"type": "string", "description": "骨骼肌量 kg"},
          "bodyShape": {"type": "string", "description": "体型"},
          "obesity": {"type": "string", "description": "肥胖度 %"},
          "whr": {"type": "string", "description": "腰臀比"},
          "sbp": {"type": "string", "description": "收缩压 mmHg"},
          "dbp": {"type": "string", "description": "舒张压 mmHg"},
          "hr": {"type": "string", "description": "心率 bpm"},
          "sbpR": {"type": "string", "description": "右臂收缩压 mmHg；结构化为systolic_bp/right_arm"},
          "dbpR": {"type": "string", "description": "右臂舒张压 mmHg；结构化为diastolic_bp/right_arm"},
          "hrR": {"type": "string", "description": "右臂心率 bpm；结构化为heart_rate/right_arm"},
          "pi": {"type": "string", "description": "灌注指数"},
          "zwtqq": {"type": "string", "description": "坐位体前屈最佳值 cm"},
          "wave": {"type": "string", "format": "byte", "description": "波形图片Base64；真实JPEG/PNG，解码后不超过6MB"},
          "ecgImg": {"type": "string", "format": "byte", "description": "心电图片Base64；真实JPEG/PNG，解码后不超过6MB"}
        }
      },
      "ShangheResponse": {
        "type": "object",
        "required": ["recode", "remsg", "datas"],
        "properties": {
          "recode": {"type": "integer", "enum": [1000, 2000, 2001, 4000, 5000, 5001]},
          "remsg": {"type": "string"},
          "datas": {"nullable": true, "description": "Token成功时为accessToken对象，结果回调成功时为null"}
        }
      }
    }
  }
}
