Files
tennis-book/skills/tennis-booking/references/protocol.md
2026-09-23 19:49:59 +08:00

368 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```http
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
```http
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`
### 请求体
```json
{
"dateTime": "YYYY-MM-DD",
"userId": 6038652,
"projectUid": "1775556129325750199"
}
```
- `dateTime`:YYYY-MM-DD。agent 一次只查一天;多日要循环。
- `userId`:数字(与 `STOREID` 同源,但请求体里是 number)。
- `projectUid`:字符串。
### 响应体
```json
{
"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)`,按整点对齐
### 错误响应
```json
{ "successed": false, "status": 400, "errorCode": "...", "message": "..." }
```
HTTP 4xx/5xx 都视为明确失败,**不重试**。
---
## 2. 锁场
`POST /wxapi/AppointmentVenue/SaveVenueAppointmentV2`
### 请求体
```json
{
"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` 单价之和**,单位**元**(不是分)
- **一次请求提交全部组成时段,不得拆单**
### 响应体(成功)
```json
{
"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` 对象 | **支付签名,只在当次锁场响应里出现一次。不得打印、不得落盘、不得缓存、不得转发。** |
### 响应体(拒绝)
```json
{ "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 示例
### 查询
```bash
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"}'
```
### 锁场
```bash
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}]
}'
```
### 释放
```bash
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`
任一触发 → 停止本球场所有操作,告知用户"接口字段变化,暂停服务",不静默丢时段、不静默重试。