{
  "openapi": "3.0.3",
  "info": {
    "title": "江苏国信微信小程序 H5 免登",
    "version": "1.0.0",
    "description": "增量同步到 Apifox：江苏国信微信小程序通过手机号申请一次性 ticket，H5 消费 ticket 换取智慧食堂 Access-Token。"
  },
  "servers": [
    {
      "url": "https://zhctdev.yyangpt.cn/aizhct",
      "description": "开发环境"
    }
  ],
  "tags": [
    {
      "name": "用户认证模块/H5免登",
      "description": "第三方小程序嵌入 H5 免登接口"
    }
  ],
  "paths": {
    "/api/sso/ticket/create": {
      "post": {
        "tags": [
          "用户认证模块/H5免登"
        ],
        "summary": "江苏国信小程序申请 H5 免登 ticket",
        "description": "小程序方后端在用户已登录后，使用可信手机号向智慧食堂后端申请一次性 ticket。该接口必须由小程序后端调用，不允许在小程序前端调用，也不能把签名密钥下发到前端。\n\n业务规则：\n- provider 固定为 jsgx_wechat_applet_001。\n- mobile 必须是小程序后端登录态中的可信手机号。\n- 后端校验 provider、timestamp、nonce、sign 后查询智慧食堂账号。\n- 手机号不存在时返回手机号不存在。\n- ticket 默认 300 秒有效，只能消费一次。\n- redirect_url 由数据库 yoshop_sso_provider.redirect_domain 生成，形如 https://{redirect_domain}/web/#/home?ticket={ticket}。\n\n签名规则：\n- 参与签名字段：provider、mobile、timestamp、nonce。\n- 按字段名 ASCII 升序排序。\n- 使用 RFC3986 URL 编码拼接查询串。\n- 使用双方线下约定 secret 计算 HMAC-SHA256，输出小写 hex。\n- 示例待签名字符串：mobile=13800000000&nonce=random_abc123&provider=jsgx_wechat_applet_001&timestamp=1710000000。",
        "operationId": "createJsgxWechatAppletSsoTicket",
        "x-apifox-folder": "用户认证模块/H5免登",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/CreateSsoTicketRequest"
              },
              "examples": {
                "default": {
                  "summary": "申请 ticket",
                  "value": {
                    "provider": "jsgx_wechat_applet_001",
                    "mobile": "13800000000",
                    "timestamp": 1710000000,
                    "nonce": "random_abc123",
                    "sign": "0f6e000000000000000000000000000000000000000000000000000000000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "申请成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateSsoTicketSuccessResponse"
                },
                "examples": {
                  "success": {
                    "summary": "成功响应",
                    "value": {
                      "status": 200,
                      "message": "ok",
                      "data": {
                        "ticket": "sso_随机字符串",
                        "expires_in": 300,
                        "redirect_url": "https://jsgx.yyangpt.cn/web/#/home?ticket=sso_%E9%9A%8F%E6%9C%BA%E5%AD%97%E7%AC%A6%E4%B8%B2"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "请求参数错误",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "badMobile": {
                    "summary": "手机号格式错误",
                    "value": {
                      "status": 400,
                      "message": "手机号格式错误",
                      "data": []
                    }
                  },
                  "missingNonce": {
                    "summary": "缺少 nonce",
                    "value": {
                      "status": 400,
                      "message": "缺少nonce",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "签名或时间戳错误",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "expired": {
                    "summary": "请求已过期",
                    "value": {
                      "status": 401,
                      "message": "请求已过期",
                      "data": []
                    }
                  },
                  "badSign": {
                    "summary": "签名错误",
                    "value": {
                      "status": 401,
                      "message": "签名错误",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "provider 不支持或未启用",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "providerUnsupported": {
                    "summary": "provider 不支持",
                    "value": {
                      "status": 403,
                      "message": "provider不支持",
                      "data": []
                    }
                  },
                  "providerDisabled": {
                    "summary": "provider 未启用",
                    "value": {
                      "status": 403,
                      "message": "provider未启用",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "手机号没有对应智慧食堂账号",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "mobileNotFound": {
                    "summary": "手机号不存在",
                    "value": {
                      "status": 404,
                      "message": "手机号不存在",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "重复 nonce 或手机号不唯一",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "nonceUsed": {
                    "summary": "nonce 已使用",
                    "value": {
                      "status": 409,
                      "message": "nonce已使用",
                      "data": []
                    }
                  },
                  "duplicateMobile": {
                    "summary": "手机号对应多个账号",
                    "value": {
                      "status": 409,
                      "message": "手机号对应多个账号，无法自动登录",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "服务端 SSO provider 配置缺失",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "missingSecret": {
                    "summary": "未配置密钥",
                    "value": {
                      "status": 500,
                      "message": "SSO provider未配置密钥",
                      "data": []
                    }
                  },
                  "missingRedirectDomain": {
                    "summary": "未配置跳转域名",
                    "value": {
                      "status": 500,
                      "message": "SSO provider未配置跳转域名",
                      "data": []
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/sso/ticket/consume": {
      "post": {
        "tags": [
          "用户认证模块/H5免登"
        ],
        "summary": "H5 消费免登 ticket 换取 Access-Token",
        "description": "H5 打开 https://{redirect_domain}/web/#/home?ticket={ticket} 后自动调用该接口，消费一次性 ticket 并换取智慧食堂现有 Access-Token。小程序方一般不需要直接调用。\n\n业务规则：\n- provider 固定为 jsgx_wechat_applet_001。\n- ticket 只能使用一次，消费后立即失效。\n- ticket 过期、已消费或不存在时返回 ticket 已失效，请重新从小程序进入。\n- 成功后 H5 清理 URL 中的 ticket，并写入现有登录态。",
        "operationId": "consumeJsgxWechatAppletSsoTicket",
        "x-apifox-folder": "用户认证模块/H5免登",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ConsumeSsoTicketRequest"
              },
              "examples": {
                "default": {
                  "summary": "消费 ticket",
                  "value": {
                    "provider": "jsgx_wechat_applet_001",
                    "ticket": "sso_随机字符串"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "消费成功",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsumeSsoTicketSuccessResponse"
                },
                "examples": {
                  "success": {
                    "summary": "成功响应",
                    "value": {
                      "status": 200,
                      "message": "ok",
                      "data": {
                        "token": "智慧食堂Access-Token",
                        "system_user_id": 123,
                        "user_name": "张三",
                        "department_name": "某学校",
                        "mobile": "138****0000"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "缺少 ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "missingTicket": {
                    "summary": "缺少 ticket",
                    "value": {
                      "status": 400,
                      "message": "缺少ticket",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "ticket 无效或已失效",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "ticketExpired": {
                    "summary": "ticket 已失效",
                    "value": {
                      "status": 401,
                      "message": "ticket已失效，请重新从小程序进入",
                      "data": []
                    }
                  },
                  "ticketInvalid": {
                    "summary": "ticket 无效",
                    "value": {
                      "status": 401,
                      "message": "ticket无效",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "provider 不支持",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "providerUnsupported": {
                    "summary": "provider 不支持",
                    "value": {
                      "status": 403,
                      "message": "provider不支持",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "用户不存在",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommonErrorResponse"
                },
                "examples": {
                  "userNotFound": {
                    "summary": "用户不存在",
                    "value": {
                      "status": 404,
                      "message": "用户不存在",
                      "data": []
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateSsoTicketRequest": {
        "type": "object",
        "required": [
          "provider",
          "mobile",
          "timestamp",
          "nonce",
          "sign"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "description": "固定值：jsgx_wechat_applet_001",
            "example": "jsgx_wechat_applet_001"
          },
          "mobile": {
            "type": "string",
            "description": "小程序后端从登录态取得的可信手机号",
            "example": "13800000000"
          },
          "timestamp": {
            "type": "integer",
            "description": "Unix 秒级时间戳，默认允许误差 300 秒",
            "example": 1710000000
          },
          "nonce": {
            "type": "string",
            "description": "随机字符串，同一 provider 下不可重复",
            "example": "random_abc123"
          },
          "sign": {
            "type": "string",
            "description": "HMAC-SHA256 签名，小写 hex",
            "example": "0f6e000000000000000000000000000000000000000000000000000000000000"
          }
        }
      },
      "ConsumeSsoTicketRequest": {
        "type": "object",
        "required": [
          "provider",
          "ticket"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "description": "固定值：jsgx_wechat_applet_001",
            "example": "jsgx_wechat_applet_001"
          },
          "ticket": {
            "type": "string",
            "description": "申请 ticket 接口返回的一次性 ticket",
            "example": "sso_随机字符串"
          }
        }
      },
      "CreateSsoTicketSuccessResponse": {
        "type": "object",
        "required": [
          "status",
          "message",
          "data"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "example": 200
          },
          "message": {
            "type": "string",
            "example": "ok"
          },
          "data": {
            "type": "object",
            "required": [
              "ticket",
              "expires_in",
              "redirect_url"
            ],
            "properties": {
              "ticket": {
                "type": "string",
                "example": "sso_随机字符串"
              },
              "expires_in": {
                "type": "integer",
                "description": "ticket 剩余有效秒数",
                "example": 300
              },
              "redirect_url": {
                "type": "string",
                "description": "小程序前端最终打开的 H5 地址，域名来自 yoshop_sso_provider.redirect_domain",
                "example": "https://jsgx.yyangpt.cn/web/#/home?ticket=sso_%E9%9A%8F%E6%9C%BA%E5%AD%97%E7%AC%A6%E4%B8%B2"
              }
            }
          }
        }
      },
      "ConsumeSsoTicketSuccessResponse": {
        "type": "object",
        "required": [
          "status",
          "message",
          "data"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "example": 200
          },
          "message": {
            "type": "string",
            "example": "ok"
          },
          "data": {
            "type": "object",
            "required": [
              "token",
              "system_user_id",
              "user_name",
              "department_name",
              "mobile"
            ],
            "properties": {
              "token": {
                "type": "string",
                "description": "智慧食堂 Access-Token",
                "example": "智慧食堂Access-Token"
              },
              "system_user_id": {
                "type": "integer",
                "description": "智慧食堂用户 ID",
                "example": 123
              },
              "user_name": {
                "type": "string",
                "example": "张三"
              },
              "department_name": {
                "type": "string",
                "example": "某学校"
              },
              "mobile": {
                "type": "string",
                "description": "脱敏手机号",
                "example": "138****0000"
              }
            }
          }
        }
      },
      "CommonErrorResponse": {
        "type": "object",
        "required": [
          "status",
          "message",
          "data"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "example": 400
          },
          "message": {
            "type": "string",
            "example": "手机号格式错误"
          },
          "data": {
            "type": "array",
            "items": {},
            "example": []
          }
        }
      }
    }
  }
}
