{
  "openapi": "3.0.3",
  "info": {"title": "上禾检测结果扫码关联", "version": "1.0.0"},
  "paths": {
    "/api/shanghe.claim/bind": {
      "post": {
        "summary": "上禾检测结果扫码关联当前登录人员",
        "tags": ["上禾体检一体机"],
        "x-apifox-folder": "上禾体检一体机",
        "description": "## 1. 接口概述\n用户检测任意一项或多项后，扫描设备二维码，将已上传结果关联当前登录本人。手机号已关联本人时再次扫码幂等返回同一报告；未匹配手机号也可扫码关联。后续同一检测会话的数据自动追加。\n\n## 2. 接口信息\nPOST /api/shanghe.claim/bind。小程序登录接口，非厂家设备回调。部署在 /aizhct 的环境须在环境基础地址包含 /aizhct。数据库先执行 store/db/v2/v2.35.15.sql，数据库qr_secret填写设备原始RC4密钥，无需AES主密钥和SHANGHE环境开关。当前为开发实现，真机导入新密钥后验证。\n\n## 3. 请求头\nAccess-Token 为当前小程序登录令牌（不是设备 X-Access-Token）。Content-Type: application/json。沿用小程序现有 platform、企业环境请求头。\n\n## 4. 请求参数\nqrUrl：必填字符串，最多2048字符，为微信 options.q 单次 URL 解码后的完整 HTTPS URL，格式为 https://域名/企业标识/企业编码/shanghe/设备追加的完整加密串。域名、企业标识和企业编码不写死；企业编码当前仅属于URL前缀，不新增业务判断、不覆盖enterprise-no，沿用既有登录请求头与Nginx分流；路径段shanghe为固定业务标识，其后整段都是密文，不按最后一个斜杠截取。不会访问该URL，归属仍按当前登录项目的设备登记与密钥校验。保留密文中的 /、+、=；不得传 deviceID、UserID、目标人员ID代替二维码。scancodeTime：可选整数，微信扫码时间秒，仅供辅助，不用作有效期凭据。仅支持《智能设备二维码通讯协议（在线）》V1.0：RC4解密明文为 type_reportid_deviceID_UserID_recordNo，type固定为1，UserID直接使用，不做Base64解码；reportid是设备本地报告ID，不用作扫码时间。按当前项目登记的deviceID与recordNo精确定位检测会话，再核对UserID。首次认领按数据库expires_at校验，默认由检测时间加1800秒生成；同人重复认领幂等。不再支持旧V2.0六字段二维码。qrUrl允许末尾单个?scancode_time=数字或&scancode_time=数字（1到12位），该元数据不参与解密或有效期计算。\n\n## 5. 响应格式\n沿用 status/message/data/trace_id。HTTP通常200，必须检查顶层业务 status。成功 status=200；首次关联 duplicate=false，同人重复 duplicate=true。data.measuredModules 只包含已上传模块，不要求四项齐全。\n\n## 6. 状态码与调用方处理\n401 LOGIN_REQUIRED：登录后恢复。400 QR_INVALID：请求形状非法。405 METHOD_NOT_ALLOWED：改用POST。429 RATE_LIMITED：每登录人员及来源IP每分钟最多60次。409 RESULT_NOT_READY：最多8次、间隔2秒等待上传。409 QR_INVALID/QR_EXPIRED/DEVICE_UNAVAILABLE/CLAIM_AMBIGUOUS/CLAIMED_BY_OTHER/CLAIM_CONFLICT：显示原因，禁止覆盖归属。500 SYSTEM_ERROR：保留检测结果，可重试。过期已关联报告应从本人档案查看。\n\n## 7. 响应字段\nsessionId 为检测会话ID；recordNo 为厂家检测流水；claimStatus 固定 CLAIMED；measureTime 为检测日期时间；measuredModules 为 height_weight、body_composition、left_arm、right_arm 的子集；duplicate 表示同人已关联。失败 data.errorCode 为稳定错误标识。成功后跳转既有对应日期体重档案（日期聚合，不承诺会话独占视图）。核心关联/指标事务成功后执行附属身高体重同步；失败记录待重试，后续请求补偿。\n\n## 8. 链路排查\n使用 trace_id 与 sessionId 关联脱敏日志，禁止记录完整 qrUrl、明文UserID、RC4密钥或登录令牌。QRCode是认领凭证，Apifox仅用合成测试设备的二维码，不保存真实用户二维码。",
        "parameters": [
          {"name":"Access-Token","in":"header","required":true,"description":"当前小程序登录令牌","schema":{"type":"string"}},
          {"name":"platform","in":"header","required":false,"schema":{"type":"string","example":"MP-WEIXIN"}}
        ],
        "requestBody": {
          "required":true,
          "content":{"application/json":{"schema":{"type":"object","required":["qrUrl"],"additionalProperties":false,"properties":{"qrUrl":{"type":"string","maxLength":2048,"description":"options.q 单次解码后的完整二维码URL；示例为占位，不可直接认领"},"scancodeTime":{"type":"integer","description":"扫码时间秒，可选辅助字段"}}},"examples":{"scan":{"summary":"任意模块完成后扫码","value":{"qrUrl":"https://szs.yyangpt.cn/haerbin/10056/shanghe/<合成设备生成的Base64密文>","scancodeTime":1788747677}}}}}
        },
        "responses": {
          "200": {
            "description":"HTTP 200；按顶层业务status和data.errorCode处理",
            "content":{"application/json":{"schema":{"type":"object","required":["status","message","data","trace_id"],"properties":{"status":{"type":"integer"},"message":{"type":"string"},"trace_id":{"type":"string"},"data":{"oneOf":[{"type":"object","required":["sessionId","recordNo","claimStatus","measureTime","measuredModules","duplicate"],"properties":{"sessionId":{"type":"integer","format":"int64"},"recordNo":{"type":"string"},"claimStatus":{"type":"string","enum":["CLAIMED"]},"measureTime":{"type":"string"},"measuredModules":{"type":"array","items":{"type":"string","enum":["height_weight","body_composition","left_arm","right_arm"]}},"duplicate":{"type":"boolean"}}},{"type":"object","required":["errorCode"],"properties":{"errorCode":{"type":"string","enum":["LOGIN_REQUIRED","RESULT_NOT_READY","QR_INVALID","QR_EXPIRED","DEVICE_UNAVAILABLE","CLAIM_AMBIGUOUS","CLAIMED_BY_OTHER","CLAIM_CONFLICT","RATE_LIMITED","SYSTEM_ERROR","METHOD_NOT_ALLOWED"]}}}]}}},"examples":{
              "partial":{"summary":"只测身高体重即可关联","value":{"status":200,"message":"关联成功","data":{"sessionId":1,"recordNo":"TEST-001","claimStatus":"CLAIMED","measureTime":"2026-09-07 10:20:25","measuredModules":["height_weight"],"duplicate":false},"trace_id":"test-trace"}},
              "phoneThenScan":{"summary":"手机号已关联本人，再扫码幂等","value":{"status":200,"message":"关联成功","data":{"sessionId":1,"recordNo":"TEST-001","claimStatus":"CLAIMED","measureTime":"2026-09-07 10:20:25","measuredModules":["height_weight","body_composition","left_arm","right_arm"],"duplicate":true},"trace_id":"test-trace"}},
              "uploadPending":{"summary":"数据还未上传，有限重试","value":{"status":409,"message":"检测数据正在上传，请稍后重试","data":{"errorCode":"RESULT_NOT_READY"},"trace_id":"test-trace"}},
              "otherUser":{"summary":"已关联其他人，不允许抢占","value":{"status":409,"message":"该检测结果已关联其他人员","data":{"errorCode":"CLAIMED_BY_OTHER"},"trace_id":"test-trace"}},
              "expired":{"summary":"二维码过期","value":{"status":409,"message":"二维码已过期","data":{"errorCode":"QR_EXPIRED"},"trace_id":"test-trace"}},
              "login":{"summary":"需要登录","value":{"status":401,"message":"请先登录","data":{"errorCode":"LOGIN_REQUIRED"},"trace_id":"test-trace"}}
            }}}
          }
        }
      }
    }
  }
}
