Files
XQKqueue/docs/visitor-mini-program-integration.md
2026-07-22 10:38:13 +08:00

20 KiB
Raw Blame History

景区排队叫号系统|游客端小程序对接文档

项目 内容
文档版本 v1.0
更新时间 2026-07-22
适用范围 景区官方微信小程序接入游客取号、排队状态查询
接口前缀 /api/public
鉴权方式 公开接口;取号后使用 public_token 查询本人状态
当前实现依据 现有游客端 API 与 H5 页面

本文描述的是当前系统已经实现的公开接口。正式联调前,需要由后端补充实际的 API_BASE_URL、前端 WEB_BASE_URL 和生产域名白名单。

1. 接入结论

推荐由小程序原生页面直接调用公开 API

  1. 调用项目列表,展示当前可取号的项目。
  2. 用户提交项目、同行人数和手机号,创建排队号码。
  3. 保存接口返回的 public_token
  4. 使用 public_token 轮询排队状态,直到号码完成、过号或取消。

系统当前不要求小程序传递登录 Cookie、Authorization 或员工/管理员账号。小程序只应调用 /api/public/*,不要调用 /api/staff/*/api/admin/*/api/events

快速接入也可以直接使用现有 H5 页面,但推荐仅作为过渡方案:

  • 取号页:/visitor
  • 指定号码状态页:/visitor/{public_token}

H5 页面的手机号查号能力目前仅用于运营测试,生产环境接口会被关闭,不能作为正式的小程序找回排队状态方案。

2. 接入前准备

小程序方需要配置以下变量:

API_BASE_URL=https://<queue-domain>
WEB_BASE_URL=https://<web-domain>

如果前端和 API 通过同一个域名发布,两个值可以相同。当前生产路由约定为:

  • /api/* 转发到 Go API 服务;
  • 其他路径转发到游客端前端服务。

请求要求:

  • 使用 HTTPS
  • 将 API 域名加入微信小程序“request 合法域名”;
  • API_BASE_URL 不要以 / 结尾;
  • POST 请求发送 Content-Type: application/json
  • 建议发送 Accept: application/json
  • 服务端响应带 Cache-Control: no-store,小程序不要缓存项目列表和排队状态;
  • 可选发送 X-Request-ID,服务端会在响应中返回同名请求 ID便于联调排查。

当前公开接口没有 CORS 或登录 Cookie 依赖。若使用 web-view,还需要把 WEB_BASE_URL 加入小程序业务域名,并由小程序页面打开 H5 地址。

3. 游客业务流程

sequenceDiagram
    participant MP as 小程序
    participant API as 排队 API
    participant Visitor as 游客

    MP->>API: GET /api/public/projects
    API-->>MP: 返回 RUNNING 项目及人数范围
    Visitor->>MP: 选择项目、填写同行人数和手机号
    MP->>API: POST /api/public/projects/{id}/tickets
    API-->>MP: 201 + ticket + public_token
    MP->>MP: 持久化 public_token
    loop 页面可见时每 35 秒
        MP->>API: GET /api/public/status/{public_token}
        API-->>MP: 号码状态、前方人数、预计等待时间
    end
    API-->>MP: status=CALLED
    MP-->>Visitor: 提示立即前往现场

4. 通用接口约定

4.1 URL 与路径参数

所有路径参数都需要进行 URL 编码。project_id 是项目 UUID不是项目名称也不是项目编码。public_token 是取号接口返回的原始 token不能自行截断、转换大小写或重新生成。

4.2 成功响应

成功响应均为 JSON。创建号码首次成功返回 HTTP 201 Created;使用同一个幂等键重试时,服务端会返回第一次请求保存的同一份响应。

4.3 错误响应

错误统一为以下结构:

{
  "error": {
    "code": "INVALID_PARTY_SIZE",
    "message": "本项目每个号码可绑定 1 到 10 人",
    "details": {
      "min_party_size": 1,
      "max_party_size": 10
    }
  }
}

小程序应根据 error.code 分支处理,不要根据 message 文案做逻辑判断。details 不是所有错误都会返回。

5. 公开 API

5.1 获取可取号项目

GET /api/public/projects

无需请求体和鉴权。

响应示例:

{
  "projects": [
    {
      "id": "2c6c3d19-6c6b-4b2d-8dd4-2c5b718c8b80",
      "name": "云栖观光车",
      "status": "RUNNING",
      "visitor_notice": "请您在景区附近等候,注意听从工作人员指引。",
      "min_party_size": 1,
      "max_party_size": 10
    }
  ]
}

字段说明:

字段 类型 说明
id string 项目 UUID后续取号必须使用此值
name string 项目名称
status string 当前只返回 RUNNING 项目
visitor_notice string/null 项目官方提示,可为空
min_party_size integer 一个排队号码允许绑定的最少人数,包含边界
max_party_size integer 一个排队号码允许绑定的最多人数,包含边界

接口只返回当前状态为 RUNNING 的项目,并按项目名称排序。返回空数组表示当前没有可取号项目。

5.2 游客取号

POST /api/public/projects/{project_id}/tickets
Content-Type: application/json
Idempotency-Key: <unique-key>

请求示例:

{
  "phone": "13800138000",
  "party_size": 2,
  "last_name": "张",
  "honorific": "先生",
  "allow_duplicate": false
}

请求字段:

字段 必填 类型 说明
phone string 取号人手机号。允许数字、空格、短横线、括号和可选的首位 +;规范化后必须为 715 位数字
party_size integer 本号同行人数,必须在项目返回的 min_party_sizemax_party_size 范围内
last_name string 姓氏,最多 40 个字符;不需要现场叫号称呼时可不传
honorific string 只能是 游客先生女士;不传或传空时按 游客 处理
allow_duplicate boolean 默认 false。只有用户明确确认后,才可改为 true

注意:服务端会拒绝未知字段、非法 JSON 和一个请求中包含多个 JSON 对象。小程序只传本文定义的字段。

成功响应示例:

{
  "ticket": {
    "id": "4b2b7f54-0b6a-4b2b-9c5d-5fd5f2fc2d41",
    "display_number": "00042",
    "ticket_number": "00042",
    "status": "WAITING",
    "party_size": 2,
    "phone_last4": "8000",
    "created_at": "2026-07-22T08:00:00Z"
  },
  "public_token": "<returned-public-token>",
  "public_url": "/visitor/<returned-public-token>",
  "status_path": "/api/public/status/<returned-public-token>",
  "revision": 42
}

字段说明:

字段 类型 说明
ticket.id string 排队号码记录 ID一般不需要用于查询
ticket.ticket_number string 展示给游客的排队号码。请直接展示,不要自行补零或拼接前缀
ticket.display_number string ticket_number 兼容返回;优先使用 ticket_number
ticket.status string 新建时为 WAITING
ticket.party_size integer 本号绑定人数
ticket.phone_last4 string 手机号尾四位;系统不会在公开响应中返回完整手机号
ticket.created_at string ISO 8601 时间
public_token string 私密状态凭证;必须持久化保存
public_url string H5 状态页相对路径,不是 API 地址
status_path string 状态查询 API 相对路径
revision integer 创建后的队列修订号,可用于判断数据是否变化

幂等要求

Idempotency-Key 必填,长度 8128 个字符且不能包含空白。推荐每次用户“确认取号”生成一个 UUID并在请求完成前保存该 key。

  • 用户重复点击、网络超时、前端未收到响应:使用同一个 key 和完全相同的请求体重试。
  • 同一个 key 不能用于不同请求体,否则返回 IDEMPOTENCY_KEY_REUSED
  • 一旦收到明确的业务错误(重复手机号除外),可以丢弃该 key。

重复手机号处理

同一个项目中,该手机号已有 WAITINGCALLEDARRIVED 状态号码时,接口返回:

409 Conflict
{
  "error": {
    "code": "DUPLICATE_PHONE",
    "message": "该手机号已有活动号码,请确认后继续"
  }
}

小程序应向用户确认是否继续取号。用户确认后,保持请求体其他字段不变,将 allow_duplicate 改为 true,并生成新的 Idempotency-Key 再提交。

取号常见错误

HTTP error.code 处理建议
400 INVALID_IDINVALID_JSONIDEMPOTENCY_KEY_REQUIRED 修正请求后再提交
404 PROJECT_NOT_FOUND 刷新项目列表,不要继续使用旧项目 ID
409 PROJECT_NOT_RUNNINGQUEUE_NOT_RUNNING 提示项目暂不可取号,重新获取项目列表
409 DUPLICATE_PHONE 进入用户确认流程
422 INVALID_PHONE 提示手机号格式错误
422 INVALID_PARTY_SIZE 使用项目接口返回的最新人数范围
422 INVALID_LAST_NAMEINVALID_HONORIFIC 修正可选字段
429 PUBLIC_TICKET_RATE_LIMITED 读取 Retry-After 秒数,等待后再试

当前实现的公开取号限流为同一客户端 IP 每分钟最多 20 次,具体阈值以后端配置和网关策略为准,小程序不要将该阈值写死为业务规则。

5.3 查询单个号码状态

GET /api/public/status/{public_token}

无需请求体和鉴权。public_token 是取号成功时返回的私密凭证,必须进行 URL 编码。

响应示例:

{
  "ticket_number": "00042",
  "display_number": "00042",
  "project_name": "云栖观光车",
  "status": "WAITING",
  "party_size": 2,
  "phone_last4": "8000",
  "tickets_ahead": 6,
  "people_ahead": 18,
  "queue_position": 7,
  "latest_called_number": "00035",
  "estimated_wait": {
    "available": true,
    "estimate_minutes": 20,
    "min_minutes": 20,
    "max_minutes": 20
  },
  "visitor_notice": "请您在景区附近等候,注意听从工作人员指引。",
  "experienced_people": 120,
  "called_at": null,
  "last_updated_at": "2026-07-22T08:12:00Z",
  "project": {
    "id": "2c6c3d19-6c6b-4b2d-8dd4-2c5b718c8b80",
    "name": "云栖观光车",
    "status": "RUNNING"
  },
  "ticket": {
    "display_number": "00042",
    "status": "WAITING",
    "party_size": 2,
    "joined_at": "2026-07-22T08:00:00Z",
    "called_at": null,
    "arrived_at": null,
    "completed_at": null,
    "missed_at": null
  },
  "eta": {
    "available": true,
    "estimate_minutes": 20,
    "min_minutes": 20,
    "max_minutes": 20
  },
  "revision": 42,
  "server_time": "2026-07-22T08:12:01Z"
}

小程序实现时优先使用顶层字段。当前响应中 etaestimated_wait 内容一致,建议统一使用 estimated_waitticket 对象用于兼容现有 H5不建议把它作为唯一数据来源。

状态查询字段:

字段 类型 说明
ticket_number string 游客排队号码
display_number string 兼容字段,通常与 ticket_number 相同
project_name string 项目名称
status string 号码状态,见下表
party_size integer 本号人数
phone_last4 string 手机号尾四位
tickets_ahead integer/null 前方等待中的号码数;非 WAITING 状态通常为 0
people_ahead integer/null 前方等待中的总人数
queue_position integer/null 当前排队位置,从 1 开始;非 WAITING 状态为 null
latest_called_number string/null 当前最新已叫号码,可能为空
estimated_wait object 预计等待时间,见下文
visitor_notice string/null 项目官方提示
experienced_people integer 当日累计绑定人数展示指标
called_at string/null 叫号时间
last_updated_at string 服务端数据更新时间
project object 项目 ID、名称和当前状态
revision integer 队列修订号
server_time string 服务端当前时间ISO 8601

时间字段均为 ISO 8601 格式;建议按服务端时间展示,不要假定服务端一定使用小程序设备本地时区。

预计等待时间

{
  "available": true,
  "estimate_minutes": 20,
  "min_minutes": 20,
  "max_minutes": 20
}

available=false 时,可能返回:

{
  "available": false,
  "estimate_minutes": 0,
  "min_minutes": 0,
  "max_minutes": 0,
  "reason": "queue_not_running"
}

常见 reason

  • queue_not_running:项目或队列暂停/未运行;
  • ticket_not_waiting:号码已经叫到、到场、完成、过号或取消;
  • missing_interval_configuration:暂时没有可用的预计时间配置。

只有 available=true 时才展示预计分钟数;否则展示“暂不可估算”,不要把 0 理解为“马上叫到”。

状态查询找不到 token 时返回 HTTP 404STATUS_NOT_FOUND。这通常表示 token 未保存、被截断、被错误编码,或该号码不再存在;当前没有公开的 token 刷新接口。

5.4 手机号查号(当前仅测试环境)

POST /api/public/status/search
Content-Type: application/json

请求体:

{
  "phone": "13800138000"
}

该接口返回同一手机号当前活动号码的状态列表,但当前代码在 APP_ENV=production 时固定返回 404 STATUS_NOT_FOUND。它的设计注释也明确要求正式上线前替换为短信验证码或外部身份接口。

因此:

  • 生产小程序不得依赖该接口找回号码;
  • 不要把手机号作为 URL 参数;
  • 取号成功后必须保存 public_token
  • 若业务必须支持“换设备/丢失 token 后查号”,需要另行设计 OTP、微信身份绑定或景区会员身份接口。

测试环境下该接口被调用过于频繁时会返回 HTTP 429、错误码 PUBLIC_QUERY_RATE_LIMITED,并通过 Retry-After 告知等待秒数。

6. 号码状态与页面展示建议

status 建议展示 处理方式
WAITING 正在排队、前方号码、前方人数、预计等待时间 持续轮询
CALLED 已到号,请前往现场 强提醒,可继续短轮询
ARRIVED 已到场,请按现场指引等待 可降低轮询频率
COMPLETED 本次排队已完成 停止轮询
MISSED 已过号,请联系工作人员 停止轮询,展示现场处理提示
CANCELED 排队已取消 停止轮询

project.status=PAUSED 且号码仍为 WAITING 时,号码会被保留,但 estimated_wait.availablefalse。恢复运行后重新查询即可获得新的预计时间。

当前游客公开状态接口不保证返回 entrancemessage 字段。到号提示应使用小程序自己的通用文案,并展示项目 visitor_notice;如后端后续补充现场入口字段,小程序可以按可选字段兼容展示。

7. 轮询与小程序生命周期

当前没有面向游客的公开 SSE 推送接口,/api/events 需要员工登录,不能用于小程序游客端。

建议策略:

  • 首次打开状态页立即请求一次;
  • 页面可见时每 35 秒请求一次;
  • 小程序进入后台时停止轮询,回到前台后立即刷新;
  • 网络错误时保留最近一次成功数据,并提供“重试”;
  • 收到 429 时按 Retry-After 退避,不要连续重试;
  • COMPLETEDMISSEDCANCELED 后停止轮询;
  • 不要把 revision 当作本地状态的唯一来源,始终以最新成功响应为准。

8. 原生小程序调用示例

以下示例只展示接口调用方式,实际项目请补充请求封装、错误提示和小程序生命周期处理。

const API_BASE_URL = 'https://<queue-domain>';

function requestProjects() {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${API_BASE_URL}/api/public/projects`,
      method: 'GET',
      header: { Accept: 'application/json' },
      success: (res) => res.statusCode === 200 ? resolve(res.data) : reject(res.data),
      fail: reject,
    });
  });
}

function createTicket(projectId, payload, idempotencyKey) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${API_BASE_URL}/api/public/projects/${encodeURIComponent(projectId)}/tickets`,
      method: 'POST',
      header: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      data: payload,
      success: (res) => res.statusCode === 201 ? resolve(res.data) : reject(res.data),
      fail: reject,
    });
  });
}

function getTicketStatus(publicToken) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${API_BASE_URL}/api/public/status/${encodeURIComponent(publicToken)}`,
      method: 'GET',
      header: { Accept: 'application/json' },
      success: (res) => res.statusCode === 200 ? resolve(res.data) : reject(res.data),
      fail: reject,
    });
  });
}

取号成功后的关键处理:

const result = await createTicket(
  projectId,
  { phone, party_size: partySize, allow_duplicate: false },
  idempotencyKey,
);

wx.setStorageSync('visitor_public_token', result.public_token);
// 后续使用 result.public_token 调用 getTicketStatus不要依赖手机号查号。

idempotencyKey 应由一次用户取号意图唯一生成,并在网络重试期间复用;用户开启“重复取号”确认后必须生成新 key。

9. WebView 过渡接入

如果小程序方暂时不实现原生页面,可以使用:

${WEB_BASE_URL}/visitor
${WEB_BASE_URL}/visitor/${encodeURIComponent(public_token)}

建议流程:

  1. 使用 WebView 打开 /visitor 完成取号;
  2. 取号成功后由 H5 页面跳转到 /visitor/{public_token}
  3. 如由小程序自己拿到 token则直接拼接第二个地址打开状态页。

注意:

  • /visitor 当前包含“手机号查号”入口,但生产 API 会禁用该入口;
  • public_url 是 H5 相对路径,status_path 是 API 相对路径,两者用途不同;
  • WebView 需要配置业务域名API 域名需要配置 request 合法域名;
  • 不要把 token 发送到统计、客服或日志系统,也不要在页面标题、分享参数中暴露完整手机号。

10. 安全与隐私要求

public_token 等价于“查看该号码状态的私密凭证”,虽然不包含完整手机号,但持有 token 的人可以查询对应排队状态。小程序方必须:

  • 只通过 HTTPS 传输;
  • 将 token 存入小程序本地存储,避免写入普通业务日志;
  • 不在 URL 之外再拼接手机号、姓氏等个人信息;
  • 不在埋点、错误上报和客服截图中上传完整 token
  • 不展示或记录公开接口返回之外的个人信息;
  • 不把公开接口当作员工操作接口使用;
  • token 丢失时不要用手机号直接绕过身份校验,转入后续 OTP/身份绑定方案。

11. 联调验收清单

场景 预期
项目列表 只展示 RUNNING 项目;正确展示人数范围和官方提示
正常取号 返回 HTTP 201、号码和 public_token,并能查询状态
重复点击 相同请求体 + 相同幂等键不会产生第二个号码
网络超时 使用原幂等键重试,得到原始结果
重复手机号 先收到 DUPLICATE_PHONE,用户确认后新 key + allow_duplicate=true 才能继续
人数越界 返回 INVALID_PARTY_SIZE,按项目列表的范围修正
项目暂停 号码保留,状态仍可查询,预计时间不可用
叫号 status=CALLED,展示到号强提醒和 called_at
终态 COMPLETEDMISSEDCANCELED 后停止轮询
token 丢失 明确提示无法直接通过手机号恢复,不能调用生产手机号查号接口替代
限流 正确处理 HTTP 429 和 Retry-After,不死循环重试

12. 当前能力边界与待确认事项

以下事项不属于当前游客公开 API正式上线前需要产品、景区方和后端共同确认

  1. 生产环境的“换设备/丢失 token 后查号”方案:短信 OTP、微信身份绑定或景区会员身份。
  2. 是否需要游客取消排队、重新取号、过号重排等公开操作;当前没有对应的游客 API。
  3. 实际 API_BASE_URLWEB_BASE_URL、HTTPS 证书和微信域名白名单。
  4. 项目 UUID 与景区小程序内部项目配置的映射方式。建议每次以 /api/public/projects 返回值为准,不在小程序写死 UUID。
  5. 到号后的现场入口、客服电话、导航信息。当前接口只返回项目官方提示,不保证返回 entrancemessage