Files
tennis-book/docs/architecture.md
2026-07-15 20:09:02 +08:00

70 lines
8.7 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.
# 架构说明
## 为什么选 Electron
本项目的主要复杂度不是绘制桌面窗口而是长期维护多个非统一的微信小程序接口不同签名方式、Cookie、请求头、代理、登录状态和多步骤预约流程。Electron 主进程可以直接复用 Node.js 网络生态,并与 Chromium 渲染层保持清晰隔离,能以最低的适配成本覆盖这些差异。
Tauri 的安装包更小、空闲内存通常更低,但它会把接口适配层拆成 Rust 命令或 sidecar。当前业务没有强约束要求最小包体却明确要求持续定制多个 JavaScript/HTTP 流程,因此 Electron 的开发与排障成本更低。
## 运行边界
```text
React renderer
| typed window.tennisBook API
Preload allowlist
| ipcRenderer.invoke
Electron main process
| AdapterRegistry + CourtSettingsStore
CourtAdapter A / CourtAdapter B / ...
| HTTP, cookies, signatures, booking workflow
微信小程序后端 API
```
渲染层只知道统一的 `CourtSummary``AvailabilityDay`。某个球场的原始响应、鉴权和预约步骤不能穿过 IPC 边界。
`AvailabilityDay` 可包含界面明确需要展示的预约人昵称和手机号,但适配器必须从第三方原始响应中逐项提取,不能把原始预约对象整体穿过 IPC这些信息只保留在当前查询的内存状态中不记录、不缓存、不落盘。
preload 必须构建为 CommonJS `.js`。项目启用了 Electron renderer sandbox而 sandboxed preload 没有 ESM 上下文;不能通过关闭沙箱来迁就 `.mjs` 构建产物。
## 每球场运行时配置
`PSPLVISITORID` 等运行时凭证按 `courtId` 独立存储在 Electron `userData` 目录。范思伯特福中福的场地 ID 固定为适配器私有常量,只在请求边界映射到第三方 `userId` 字段和 `STOREID` 请求头,不经过 IPC也不在配置界面暴露。设置弹窗通过专用白名单 IPC 读取凭证原值并回填密码框,默认遮挡;关闭弹窗立即清除渲染层中的输入值和设置对象,其他查询及预约接口不返回凭证。
可订列表刷新链路不读取球场凭证,`CourtAdapter.getAvailability()` 也不接收运行时设置,从类型边界保证列表请求不能携带 `PSPLVISITORID`。只有下单和释放写操作在主进程互斥区内读取最新凭证。
配置文件使用临时文件加重命名的方式原子替换。共享层始终用字符串表达外部 ID避免超出 JavaScript 安全整数范围;具体适配器负责校验接口是否接受该范围并转换为请求格式。
应用对外显示名称为 `Tennis Book`,但 Electron `userData` 固定为 `appData/tennis-book`。显示名称与持久化路径解耦,避免品牌名称或大小写变化后配置、待支付订单和监控任务看似丢失。
## 接入一个新球场
1.`src/main/adapters/<court-id>/` 创建适配器。
2. 实现 `CourtAdapter``court``getAvailability()`
3. 在适配器内部完成原始响应到共享模型的转换。
4. 将适配器加入 `src/main/adapters/registry.ts`
5. 为登录失效、空数据和接口字段变化添加契约测试。
## 预约写操作
公共预约状态机为:`可订 -> 二次确认 -> 重新查询校验 -> 结果待确认记录 -> 下单锁场 -> 待支付 -> 取消释放/支付完成`。具体请求体、版本头、访客凭证和响应解析只存在于球场适配器中。所有写操作按球场在主进程串行化,待支付或结果不确定期间禁止修改该球场凭证。
下单前由主进程重新查询同一天的数据,并使用重新查询得到的场地 UID、时间和价格构造请求拒绝已订、排课和活动报名时段。渲染层传入的价格只作为用户确认金额如与最新价格不同则拒绝下单并要求重新确认。第三方 UID 不接受渲染层输入。
范思伯特福中福的 `SaveVenueAppointmentV2` 只创建待支付订单并锁场,不代表支付成功。支付脚本不经过 IPC、不记录日志、不落盘待支付订单的非敏感摘要按球场持久化以便应用重启后仍能恢复“取消释放”入口。POST 发送前先持久化结果待确认记录,网络超时或响应解析失败时保留“尝试释放”入口,避免生成不可恢复的孤儿锁场。
`CrmAutoReleaseVenueAppoint` 使用当前球场配置与空 JSON 请求体释放当前访客的锁场,并不能绑定订单号。发送释放请求前先持久化 `release-uncertain`;若响应超时或进程中断,系统禁止自动重试,用户必须在小程序核实后显式清理本机记录。远端确认释放后先持久化 `release-confirmed` 标记,再删除摘要,避免清理失败后重复调用全局释放接口。支付脚本的过期时间只用于展示,不作为场地锁已经释放的依据。
## 空场监控
监控任务由主进程持久化和调度,应用通过单实例锁避免重复调度。日期可指定为创建时的今日或明日,也可以留空。指定日期的任务到达当天结束时间后自动停用;未指定日期的任务持续滚动检查主界面当前支持的未来 7 天,跨日后自动向后滚动一天且不会过期。轮询间隔固定为 10 秒;本地时间进入新的整点时绕过剩余轮询冷却立即检查一次,以捕获整点释放的新场地。同一整点只触发一次,并复用现有串行调度,不与尚未结束的检查并发。应用启动和休眠后恢复也会立即调度一次。今日已经结束的指定日期范围不能创建。相同球场和日期的请求在同一轮中复用,并分别过滤 `available/limited` 时段。
任务持久化上一次可订时段 ID 集合,只对新增 ID 通知;同一时段持续可订时不会重复通知,先消失再重新出现则再次通知。暂停和删除会让在途响应失效。提醒同时使用 Electron 系统通知和主进程待消费队列中的应用内提示,渲染器启动较晚或同轮出现多条提醒时也不会覆盖;默认只提醒,只有任务被用户显式授权自动预定时才会下单。带 `enrollment` 报名活动的时段属于已订场地,即使上游同时返回可预约标记,也必须从可订统计和监控提醒中排除。
监控时间范围采用完整连续区间语义。例如 `19:0021:00` 只有在同一天同一个 `classroomUid``19:0019:59``20:0020:59` 均可订时才形成一个命中;只有其中一段、两段来自不同场地、中间有缺口或包含报名活动都不命中。上游以 `xx:59` 表示小时结束时,公共层将其归一为下一小时的排他边界。去重 ID、可订数量、提醒文案和自动预定均以整个连续组合为单位。
任务可显式开启一次性自动预定。自动预定只消费本轮新出现的完整连续组合,多项命中时按日期、开始时间、总价和场地名稳定选择一个,并复用 `AdapterRegistry.createBooking` 的实时可用性、逐时段价格复核、同场连续性校验、球场级串行锁和待支付订单持久化。组合中的全部时段通过同一笔 `classroomItems` 请求提交,任一组成时段复核失败则整笔不提交。成功后任务转为“待支付”并关闭自动预定,后续轮询只读不写。同一球场已有待支付、提交结果不确定或释放结果不确定记录时,不发起下单,任务持久化为“已阻止”并要求订单处理完成后由用户重新授权。明确失败且本机没有待处理订单时立即重试一次;第二次明确失败后熔断。网络超时等结果不确定场景禁止重试并立即熔断,避免第一次已经锁场时重复提交。进程在 `attempting` 状态退出后,重启时只有确认的 `pending-payment` 恢复为“待支付”,提交或释放结果不确定时恢复为“已阻止”并要求人工核实。
每个任务在同一持久化记录中维护累计检查、成功、部分失败、完全失败、提醒次数和空场观测次数;同时保留近 30 个自然日每日汇总与最近 240 条检查、异常、提醒及生命周期日志。旧版任务缺少遥测字段时在读取阶段补齐默认值,不改变文件版本。任务详情只通过独立 IPC 按需读取,任务列表仅返回轻量统计摘要。日志只包含时间、日期范围、计数和脱敏错误摘要,不得保存凭证、预约人信息、完整接口响应或请求头。
为避免 10 秒轮询反复序列化整个任务文件,遥测先在主进程内存实时累计,每 5 分钟将所有变化任务合并为一次原子快照;提醒、暂停、恢复和到期等关键节点立即落盘。正常退出通过 `before-quit` 等待最终快照完成,异常退出最多损失当前 5 分钟的非关键检查统计,任务定义、去重集合和关键事件不受影响。