feat: 支持大湾区球场监控
This commit is contained in:
367
skills/tennis-booking/references/protocol.md
Normal file
367
skills/tennis-booking/references/protocol.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# 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`
|
||||
|
||||
任一触发 → 停止本球场所有操作,告知用户"接口字段变化,暂停服务",不静默丢时段、不静默重试。
|
||||
Reference in New Issue
Block a user