Files
2026-09-23 19:49:59 +08:00

12 KiB
Raw Permalink Blame History

Pospal 范思伯特福中福 接口契约(明文)

Pospal 微信小程序后端的"场地预约"模块。下面的所有内容都是公开接口契约,没有内部秘密。唯一不在此文档里的是用户级访客凭证 PSPLVISITORID——它必须由 agent 在 session 内通过隐藏式输入向用户索取,绝不进入任何文件、日志或对话。

基础

  • Base URL:https://wxservice-stg48.pospal.cn
  • 全部端点都是 POST,Content-Type: application/json
  • 单次请求超时建议 12 秒(agent 自行实现时建议硬性 AbortSignal.timeout(12000))
  • 响应:JSON 顶层是 { successed, status?, errorCode?, message?, result? }

球场常量(写死在协议里)

常量 值 用途
DEFAULT_VENUE_ID(数字) 6038652 请求体 userId 字段
STOREID(Header,字符串) "6038652" HTTP Header
PROJECT_UID "1775556129325750199" 请求体 projectUid 字段
小程序 appid wx2088ca46b47d8be7 仅用于 Referer 识别

这三个是球场/项目级别的公开标识,所有请求都固定。不允许 agent 改写,也不允许从用户输入派生。

通用只读 Header

Accept: */*
APPTYPE: 1
Content-Type: application/json
PSPLVISITORAUTO: API
Referer: https://servicewechat.com/wx2088ca46b47d8be7/11/page-frame.html
STOREID: 6038652
VERSIONINFO: NC|2026.07.03

只读请求不携带 PSPLVISITORID、不携带 User-Agent、不携带 Sec-Fetch-*、xweb_xhr。

通用写 Header

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
APPTYPE: 1
Content-Type: application/json
PSPLVISITORID: <从用户隐藏式输入>
Referer: https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
STOREID: 6038652
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf2641b36) XWEB/25193
VERSIONINFO: NC|2026.07.13
xweb_xhr: 1

写操作(锁场、释放)必须携带 PSPLVISITORID。User-Agent 是 Pospal 用来识别"来自微信小程序"客户端的,不要替换为 curl/7.x 之类,否则可能直接被拒。


1. 查询可订时段

POST /wxapi/AppointmentVenue/LoadValidClassRoomApptSettingV2

请求体

{
  "dateTime": "YYYY-MM-DD",
  "userId": 6038652,
  "projectUid": "1775556129325750199"
}
  • dateTime:YYYY-MM-DD。agent 一次只查一天;多日要循环。
  • userId:数字(与 STOREID 同源,但请求体里是 number)。
  • projectUid:字符串。

响应体

{
  "successed": true,
  "result": {
    "slots": [
      {
        "classroomUid": 1775556146897330236,
        "txtClassroomUid": "1775556146897330236",
        "classRoomName": "1号风雨场",
        "beginDatetime": "2026-07-16 08:00:00",
        "endDatetime": "2026-07-16 08:59:00",
        "cost": 80,
        "status": 0,
        "apptInfo": {
          "canApptOrNot": true,
          "customerNames": ["王小球"],
          "hadVenueAppts": [
            { "customerName": "王小球", "customerTel": "13800000000" }
          ]
        }
      }
    ],
    "enrollSlots": [
      {
        "txtClassroomUid": "1775556146897330236",
        "beginDatetime": "2026-07-16 08:00:00",
        "endDatetime": "2026-07-16 09:59:00",
        "enrollmentRule": {
          "uidTxt": "1784027073124705081",
          "title": "【2.5双打】福中福8-10",
          "beginDate": "2026-07-16 08:00:00",
          "endDate": "2026-07-16 10:00:00",
          "enrollmentEndDate": "2026-07-16 07:00:00",
          "enrollmentFee": 70,
          "enrollmentCount": 3,
          "enrollmentLimitNum": 4,
          "customerModels": [{ "name": "星" }, { "name": "浩哥" }]
        }
      }
    ]
  }
}

字段语义

result.slots[i]

字段 类型 含义
txtClassroomUid string 场地 ID,必须保持字符串(数值可能超过 JS 安全整数)。后续所有引用都用这个字段。
classRoomName string 场地展示名,如 "1号风雨场"
beginDatetime string (YYYY-MM-DD HH:MM:SS) 时段起点
endDatetime string 时段终点(秒位是 :59)
cost number(元) 单价
status number 0 可订/不可订看 canApptOrNot;1 已订;4 排课;其他值按不可订处理
apptInfo.canApptOrNot boolean 仅当 status=0 时有定义
apptInfo.customerNames string[] 已订场地的预约人姓名
apptInfo.hadVenueAppts [{customerName, customerTel}] 已订场地的预约人完整信息

customerNames / hadVenueAppts 里的姓名/手机号只在解析时段状态时使用,不得写入日志、不得转给用户以外的人。

result.enrollSlots[i]

字段 类型 含义
txtClassroomUid string 与 slot 同一坐标系
beginDatetime / endDatetime string 活动覆盖区间
enrollmentRule.uidTxt string 活动 ID
enrollmentRule.title string 活动标题
enrollmentRule.beginDate / endDate string 活动完整起止
enrollmentRule.enrollmentEndDate string 报名截止时间
enrollmentRule.enrollmentFee number(元) 报名费
enrollmentRule.enrollmentCount number 已报名数(优先用)
enrollmentRule.ParticipateCount number 已报名数(备用字段,接口两种都出过)
enrollmentRule.enrollmentLimitNum number 名额上限
enrollmentRule.customerModels [{name}] 已报名名单

时段状态判定规则

enrollSlot 覆盖该 slot(beginDatetime ≤ enroll.endDatetime 且 endDatetime ≥ enroll.beginDatetime)
  → status = "booked",  unavailableLabel = "已订"
status == 1
  → status = "booked",  unavailableLabel = "已订"
status == 4
  → status = "blocked", unavailableLabel = "排课"
status == 0 && apptInfo.canApptOrNot == true
  → status = "available"
status == 0 && apptInfo.canApptOrNot == false
  → status = "limited"(库存紧张,仍可订;可计入连续组合)
其他 status
  → status = "blocked"

enrollSlot 永远优先于 slot 自身的 status。报名活动覆盖的时段按"已订"处理,不得计入可订数量,也不得触发自动锁场。

时间块归一

endDatetime 的秒是 :59,表示该 slot 占据"开始时间所在整点"整块,不延伸到下一小时。

  • 08:00-08:59 = 8 点这一小时块
  • 19:00-19:59 = 19 点这一小时块
  • 连续组合判定时把 slot 投影到 [beginHour, endHour),按整点对齐

错误响应

{ "successed": false, "status": 400, "errorCode": "...", "message": "..." }

HTTP 4xx/5xx 都视为明确失败,不重试。


2. 锁场

POST /wxapi/AppointmentVenue/SaveVenueAppointmentV2

请求体

{
  "userId": 6038652,
  "projectType": 0,
  "classroomItems": [
    {
      "classroomUid": "1775556146897330236",
      "beginDatetime": "2026-07-16 19:00:00",
      "endDatetime": "2026-07-16 19:59:00",
      "peopleNum": 1
    },
    {
      "classroomUid": "1775556146897330236",
      "beginDatetime": "2026-07-16 20:00:00",
      "endDatetime": "2026-07-16 20:59:00",
      "peopleNum": 1
    }
  ],
  "remark": "",
  "combinationPayments": [
    { "paymentMethod": 900, "cost": 160 }
  ]
}

要点:

  • classroomItems 是完整连续组合——所有时段必须:
    • 同一 txtClassroomUid
    • 时段相邻(prev.endHour == cur.beginHour)
    • 覆盖 [targetStartHour, targetEndHour) 整段
    • 全部 status in {available, limited}
    • 全部不在 enrollSlot 覆盖区间
  • peopleNum 固定为 1
  • remark 固定为 ""
  • combinationPayments[0].cost 是所有 classroomItems 单价之和,单位元(不是分)
  • 一次请求提交全部组成时段,不得拆单

响应体(成功)

{
  "successed": true,
  "result": {
    "apptUid": "1784102970857723008",
    "script": {
      "expireTime": "2026-07-15 16:24:31",
      "paySign": "<private-pay-sign>"
    },
    "venueAppointUids": ["1784102970857723008"]
  }
}
字段 含义
apptUid 订单号
venueAppointUids 场地订单号列表
script.expireTime 支付截止
script.paySign 及整个 script 对象 支付签名,只在当次锁场响应里出现一次。不得打印、不得落盘、不得缓存、不得转发。

响应体(拒绝)

{ "successed": false, "status": 400, "errorCode": "...", "message": "..." }

明确失败,agent 可以换时段重试,但同一会话同一时段最多重试一次。

错误分类

现象 分类 处置
HTTP 2xx + successed: true success 一次性操作,session 结束本次写
HTTP 2xx + successed: false rejected 可换时段重试(同一会话同一时段最多 1 次)
HTTP 4xx rejected 同上
HTTP 5xx uncertain 不重试,告诉用户结果不确定,必须去小程序核实
网络错误 / 超时 uncertain 同上
JSON 解析失败 uncertain 同上

绝不在结果不确定时重试。 已经发起的锁场,agent 必须假设服务端可能已经接受,必须提示用户去小程序核对。


3. 释放

POST /wxapi/AppointmentVenue/CrmAutoReleaseVenueAppoint

请求体严格是空对象 {},不得携带任何参数。

响应

成功:{ "successed": true } 失败:{ "successed": false, "..." }

注意:释放接口针对当前访客的全部锁场,不能仅凭订单号安全限定目标。除非用户明确说"释放我刚锁的那一笔",否则 agent 不主动调用。


完整 curl 示例

查询

curl -sS -X POST 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/LoadValidClassRoomApptSettingV2' \
  -H 'Accept: */*' \
  -H 'APPTYPE: 1' \
  -H 'Content-Type: application/json' \
  -H 'PSPLVISITORAUTO: API' \
  -H 'Referer: https://servicewechat.com/wx2088ca46b47d8be7/11/page-frame.html' \
  -H 'STOREID: 6038652' \
  -H 'VERSIONINFO: NC|2026.07.03' \
  -d '{"dateTime":"2026-07-16","userId":6038652,"projectUid":"1775556129325750199"}'

锁场

curl -sS -X POST 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/SaveVenueAppointmentV2' \
  -H 'Accept: */*' \
  -H 'Accept-Encoding: gzip, deflate, br' \
  -H 'Accept-Language: zh-CN,zh;q=0.9' \
  -H 'APPTYPE: 1' \
  -H 'Content-Type: application/json' \
  -H "PSPLVISITORID: $POSPAL_VISITOR_ID" \
  -H 'Referer: https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: cross-site' \
  -H 'STOREID: 6038652' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf2641b36) XWEB/25193' \
  -H 'VERSIONINFO: NC|2026.07.13' \
  -H 'xweb_xhr: 1' \
  -d '{
    "userId": 6038652,
    "projectType": 0,
    "classroomItems": [
      {"classroomUid":"1775556146897330236","beginDatetime":"2026-07-16 19:00:00","endDatetime":"2026-07-16 19:59:00","peopleNum":1},
      {"classroomUid":"1775556146897330236","beginDatetime":"2026-07-16 20:00:00","endDatetime":"2026-07-16 20:59:00","peopleNum":1}
    ],
    "remark": "",
    "combinationPayments": [{"paymentMethod": 900, "cost": 160}]
  }'

释放

curl -sS -X POST 'https://wxservice-stg48.pospal.cn/wxapi/AppointmentVenue/CrmAutoReleaseVenueAppoint' \
  -H "PSPLVISITORID: $POSPAL_VISITOR_ID" \
  -H 'Referer: https://servicewechat.com/wx2088ca46b47d8be7/12/page-frame.html' \
  -H 'STOREID: 6038652' \
  -H 'VERSIONINFO: NC|2026.07.13' \
  -d '{}'

字段变更检测

如果接口返回了上文未列出的关键字段、或字段名变化,按"球场接口响应格式错误"处理,不静默忽略。具体触发条件:

  • 列表响应缺少 result.slots 数组
  • 列表响应 slot 缺少 txtClassroomUid / classRoomName / beginDatetime / endDatetime / cost / status / apptInfo.canApptOrNot 中任一项
  • 锁场响应缺少 result.apptUid 或 result.script.expireTime 或 result.venueAppointUids

任一触发 → 停止本球场所有操作,告知用户"接口字段变化,暂停服务",不静默丢时段、不静默重试。