Skip to content

feat: reservation_service(optimize performance) - #8

Open
yangl0610 wants to merge 17 commits into
QSCTech:mainfrom
yangl0610:main
Open

feat: reservation_service(optimize performance)#8
yangl0610 wants to merge 17 commits into
QSCTech:mainfrom
yangl0610:main

Conversation

@yangl0610

@yangl0610 yangl0610 commented Apr 30, 2026

Copy link
Copy Markdown

Summary

  • 多场地多时间段预约ReservationSlotSelection / PlanSlotSelection 每个 slot 携带完整的 campus/venue/start/end 上下文,支持跨场馆多选;ReservationSubmitRequest / ReservationPlanRequest 去除顶层冗余字段
  • /reservations/slots 补全执行上下文:返回字段新增 venue_idvenue_site_idspace_idtime_idtokenweek_start_date,前端可直接构建 ReservationSlotSelection 提交预约;预约窗口未开放时降级返回 template 时间段骨架(available=false),支持计划预约选场
  • 调度器修复ListDueScheduled 扩展查询 failed 状态,使失败计划每小时重试;materializeOne 兼容 failed→submitting 原子切换;新增 MarkExpiredFailed 定期将预约日期已过的 failed 记录标记为 expired,防止无效重试
  • 路由冲突修复:统一预约路由路径参数名为 {roomId}(UUID 类型),消除 Gin 路由树 panic
  • 测试:新增 service 层纯函数单元测试及 repository 集成测试

Test plan

  • go test ./internal/service/... 全部通过
  • 启动服务无 panic,所有路由正常注册
  • GET /reservations/slots?... 预约窗口未开放时返回 template 降级数据
  • 设置 DB_HOSTgo test ./internal/repository/... 集成测试通过
  • 调度器 POST /internal/tasks/reservation-materialize 返回包含 expired 字段

🤖 Generated with Claude Code

yangl0610 and others added 17 commits April 20, 2026 12:56
- ListVenues / ListSlots 对接 TYYS API
- 修复数值类型 ID 解析问题
- OpenAPI 补充可选值文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
背景:同学在约场地时(尤其是提前很久的 plan 模式)往往可以接受多个
时间段、多个场地,甚至多个校区。将这种灵活性直接编码进 API,避免在
预约时强制只选一个场地。

主要变更:

【API / OpenAPI】
- ReservationSlotSelection 新增 campus_name / venue_name / venue_id /
  start_time / end_time,每个 slot 自带完整上下文,支持跨场馆多选。
- ReservationSubmitRequest / PreviewInput 去除顶层冗余的 campus/venue/
  start/end,改为从各 slot 中携带。
- 新增 ReservationPlanSlotSelection,ReservationPlanRequest 用
  plan_slots[] 替代原 preferred_space_ids。
- Preview / Submit / Plan 三个端点改用 roomPublicId(UUID 路径参数),
  符合 public_id 迁移方向。

【Service 层】
- SlotSelection 增加 campus/venue/start/end/venue_id 字段;trySlots
  每次尝试前同步写回 record,DB 始终记录当前正在尝试的场地信息。
- ReservationPreviewInput / ReservationPlanInput 去除冗余顶层字段。
- PlanSlotSelection:完整的计划意图结构(相当于 SlotSelection 减去
  materialize 时才能确定的 time_id / token / week_start)。
- CreatePlan:reserve_open_at 取所有 PlanSlots 中最早的预约窗口时间,
  确保调度器在第一个场馆开窗时即可触发。
- resolvePreferredSlots:按 (campus, venue) 分组查 ListSlots,按
  space_id 过滤,不做时间范围过滤,由 trySlots 逐个尝试。

【Handler 层】
- 补全 4 个缺失的 handler 方法:ListReservationTemplates、
  CreateRoomReservationPlan、TriggerReservationMaterialize、
  TriggerReservation。
- 所有对外接口一律用 public_id(UUID)查询房间和预约记录。
- 新增 RoomService.GetByPublicID stub。

【Model】
- RoomReservation.PreferredSpaceIDs 重命名为 PlanSlots,存储
  []PlanSlotSelection 的 JSON 序列化。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
背景:
- ListDueScheduled 只查 scheduled 状态,导致 failed 的计划永不重试
- 当预约开放时间到达但部分场馆 ListSlots 尚不可用时,计划被标记为
  failed 后无法被调度器再次尝试
- failed 中预约时段已过实际时间的记录会被无限重试,浪费调度资源

主要变更:

【Repository】
- ListDueScheduled 扩展为查询 reservation_status IN ('scheduled','failed'),
  使 failed 状态的计划每小时也能被重试(兜底场馆窗口延迟开放的情况)
- 新增 MarkExpiredFailed:将 failed 且 reservation_date < today 的记录
  标记为 expired,阻止调度器继续无效重试

【Service】
- materializeOne:AtomicTransitionStatus 兼容 scheduled 和 failed 两种
  初始状态,均可原子切换到 submitting
- MaterializeResult 新增 Expired 字段
- MaterializePlans:每次执行后调用 MarkExpiredFailed 清理当日前的过期记录

【API / OpenAPI】
- ReservationMaterializeResult 新增 expired 字段,返回本次清理数量

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
背景:
- Gin 不允许同一路径前缀下出现两个不同名通配符(:roomId vs
  :roomPublicId),导致服务启动 panic
- /reservations/slots 返回的字段缺少提交预约所需的执行上下文
  (venue_site_id/space_id/time_id/token 等),前端无法直接构建
  ReservationSlotSelection
- 预约窗口未开放时 slots 端点直接报错,无法服务计划预约场景

主要变更:

【路由修复】
- RoomPublicIdPath component parameter 的 name 从 roomPublicId 改为
  roomId,三条预约路由路径从 {roomPublicId} 改为 {roomId},消除
  Gin 路由树冲突;handler 参数名同步更新,UUID 查询逻辑不变

【ReservationSlot schema 扩展】
- 新增字段:campus_name / venue_name / venue_id / venue_site_id /
  space_id / time_id / token / week_start_date
- 所有字段均为可选;预约窗口未开放时 token/week_start_date 为空

【ListSlots 逻辑重写】
- Step 1:从 VenueInfo 解析 venueId/venueSiteId 及 resolvedCampus/
  resolvedVenue
- Step 2:尝试调用 TYYS ReservationDayInfo(可能失败)
- Step 3:dayInfo 失败 → 调用 listSlotsFromTemplate 降级,返回
  template 时间段骨架,available=false,供计划预约选场使用
- Step 5:dayInfo 成功 → 每个 slot 携带完整上下文(venue_id/
  venue_site_id/space_id/time_id/token/week_start_date)

【listSlotsFromTemplate】
- 调用 ListTemplates 获取分场(Spaces)和时间段模板(TimeSlots)
- 以 tmpl.Spaces 为分场列表(不依赖 VenueInfo 层是否携带 spaceId)
- 无分场数据时用空占位,保证时间段仍能返回

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- walkSlots 修复:正确解析 TYYS dayInfo 中 spaceId(父对象 id)和 timeId(子对象 key)
- ListTemplates 改为调 ListSlots(tomorrow) 并剥离实时字段,返回扁平 space×timeslot 列表
- ReservationTemplateResponse 用 slots[] 替换原 spaces[]+time_slots[] 两级结构
- SubmitOrPlan:对开放窗口内 slot 依次尝试实时提交,全败降级 CreatePlan
- SubmitRoomReservation handler 改走 SubmitOrPlan
- Preview 对窗口未开放的 slot 跳过 TYYS 调用,直接标 available=false 并说明开放时间
- resolvePreferredSlots 修复:同 space 多时段不再互相覆盖,匹配时加时间段过滤

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
保留 HEAD 中的完整实现:RoomPublicIdPath int64 参数类型、SubmitOrPlan/CreatePlan/MaterializePlans/TriggerReservation 等新功能。
upstream merge 带入了一个 not-implemented 存根,与已有的真实实现重复导致编译失败。
- ListVenues / ListSlots 改用 cachedVenueInfo(15min TTL),避免每次请求都打 TYYS
- ListSlots 在预约窗口未开放时直接返回 template,跳过无意义的实时 DayInfo 请求
- Preview 改为并发调用 ReservationOrderInfo(最大并发 3),耗时从 O(n×latency) 降至 O(latency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yangl0610 yangl0610 changed the title 支持多场地多时间段预约 + slots 接口补全 + 调度器修复 feat: reservation_service(optimize performance) May 29, 2026
@serein6174

Copy link
Copy Markdown
Collaborator

Review

1. Preview 可能会影响后续 Submit

目前 Preview 会实际调用 TYYS /reservation/order/info 做预校验,但 Submit 阶段的 ReserveV2 内部又会再次调用同一个接口。

本地测到的现象:

  • 对同一个最新可约 slot,先调用 preview,返回 available=true
  • 随后用同一个 slot 调 submit,会稳定失败:
tyys api /venue-server/api/reservation/order/info failed with code 250: 场地预约失败,预约步骤异常
  • 如果跳过 preview,直接 submit 同一个 slot,则可以成功。

这说明 /reservation/order/info 可能不是无副作用的纯查询接口,重复调用会破坏后续下单流程。当前“预览确认后提交”的链路反而可能导致实际预约失败。

请排查具体问题并修复

2. Server 启动不应强依赖 ZJU_USERNAME / ZJU_PASSWORD

当前 cmd/server/main.go 在启动阶段直接执行:

auth, err := zjulogin.NewFromEnv()

如果环境中没有配置 ZJU_USERNAME / ZJU_PASSWORD,整个后端服务会直接退出。这样会导致房间、用户等非预约功能也无法使用。

建议改成:

  • TYYS client 懒初始化,只在调用预约相关功能时检查配置;
  • 或者在未配置 TYYS 账号时仅禁用预约功能,普通接口仍然正常启动。

3. DayInfo 仍然缺少缓存

目前 VenueInfo 已经有缓存,但 ListSlots 在开放预约窗口内仍然每次都会调用:

s.tyys.ReservationDayInfo(ctx, params)

如果前端频繁刷新同一天、同场馆的 slots,这部分仍然会比较慢,也会增加 TYYS 请求压力。

建议按以下维度做短 TTL 缓存:

(sport_type, campus_name, venue_name, reservation_date)

TTL 可以先设置为 10–30 秒。最终提交前仍以 submit 阶段的最新状态为准。

4. 注意和 main 分支的 invite_code / buddy_code 改动合并

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants