|
|
@@ -180,6 +180,7 @@ class GrcPlugin(IVSPlg):
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError(message='Session expired.')
|
|
|
|
|
|
+ # 提取核心参数
|
|
|
res_id_match = re.search(r'resource\[(\d+)\]\s*=', resp.text)
|
|
|
if res_id_match:
|
|
|
self.resource_id = res_id_match.group(1)
|
|
|
@@ -192,34 +193,39 @@ class GrcPlugin(IVSPlg):
|
|
|
if tok_match:
|
|
|
self.token = tok_match.group(1)
|
|
|
|
|
|
- if not self.rp_id or not self.token:
|
|
|
+ if not getattr(self, 'rp_id', None) or not getattr(self, 'token', None):
|
|
|
self._log("Failed to extract rp_id or token from HTML")
|
|
|
raise NotFoundError(message='rp_id or token not found')
|
|
|
|
|
|
- default_length = None
|
|
|
+ # 默认一小时长度 (3600秒)
|
|
|
+ default_length = 3600
|
|
|
len_match = re.search(r'default_length\s*=\s*(\d+)', resp.text)
|
|
|
if len_match:
|
|
|
default_length = int(len_match.group(1))
|
|
|
|
|
|
+ # 开放时间规则
|
|
|
open_times = None
|
|
|
ot_match = re.search(r'open_times\s*=\s*\[(.*?)\]', resp.text)
|
|
|
if ot_match:
|
|
|
- open_times = [int(x) for x in ot_match.group(1).split(',')]
|
|
|
+ open_times =[int(x) for x in ot_match.group(1).split(',')]
|
|
|
|
|
|
+ # 排期结束时间
|
|
|
season_end_ts = 9999999999
|
|
|
season_match = re.search(r'season\s*=\s*\[(\d+),(\d+)\]', resp.text)
|
|
|
if season_match:
|
|
|
season_end_ts = int(season_match.group(2))
|
|
|
-
|
|
|
- cursor_match = re.search(r'Date\.UTC\((\d+),(\d+),(\d+),(\d+)\)', resp.text)
|
|
|
- if cursor_match:
|
|
|
- y, m, d, h = map(int, cursor_match.groups())
|
|
|
- start_date = datetime(y, m + 1, d, h, tzinfo=timezone.utc)
|
|
|
- else:
|
|
|
- start_date = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0)
|
|
|
-
|
|
|
-
|
|
|
- scan_start_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0)
|
|
|
+
|
|
|
+ # 提取全局页面屏蔽的假日例外期 (ecache)
|
|
|
+ ecache_blocks =[]
|
|
|
+ ecache_match = re.search(r'ecache\s*=\s*\{data:\s*\[(.*?)\]\}', resp.text)
|
|
|
+ if ecache_match:
|
|
|
+ # 匹配形如[1775433600,1775519970,0] 的数据
|
|
|
+ triplets = re.findall(r'\[(\d+),\s*(\d+),\s*\d+\]', ecache_match.group(1))
|
|
|
+ for t0, t1 in triplets:
|
|
|
+ ecache_blocks.append((int(t0), int(t1)))
|
|
|
+
|
|
|
+ scan_start_dt = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
+ current_time_ts = int(time.time())
|
|
|
days_total_scan = 60
|
|
|
chunk_size = 30
|
|
|
valid_slots_map: dict[datetime.date, list[TimeSlot]] = {}
|
|
|
@@ -228,54 +234,73 @@ class GrcPlugin(IVSPlg):
|
|
|
chunk_start = scan_start_dt + timedelta(days=i)
|
|
|
json_data = self._fetch_schedule_data(chunk_start, chunk_size)
|
|
|
|
|
|
- booked_ts: Set[int] = set()
|
|
|
- blocked_ranges: List[Tuple[int, int]] =[]
|
|
|
-
|
|
|
+ # 把已经预定的订单和节假日统一收集为“阻挡物实体墙”
|
|
|
+ all_blocks: List[Tuple[int, int]] = ecache_blocks.copy()
|
|
|
if 'app' in json_data:
|
|
|
for item in json_data['app']:
|
|
|
- booked_ts.add(int(item[0]))
|
|
|
+ all_blocks.append((int(item[0]), int(item[1])))
|
|
|
if 'exc' in json_data:
|
|
|
for item in json_data['exc']:
|
|
|
- blocked_ranges.append((int(item[0]), int(item[1])))
|
|
|
+ all_blocks.append((int(item[0]), int(item[1])))
|
|
|
|
|
|
for day_offset in range(chunk_size):
|
|
|
current_day = chunk_start + timedelta(days=day_offset)
|
|
|
start_min, end_min = self._get_daily_schedule(open_times, current_day)
|
|
|
if start_min is None or start_min >= end_min: continue
|
|
|
|
|
|
- curr_min = start_min
|
|
|
- while curr_min + (default_length / 60) <= end_min:
|
|
|
- slot_hour = int(curr_min // 60)
|
|
|
- slot_minute = int(curr_min % 60)
|
|
|
- slot_dt = current_day.replace(hour=slot_hour, minute=slot_minute, second=0, microsecond=0)
|
|
|
- slot_ts = int(slot_dt.timestamp())
|
|
|
- curr_min += (default_length / 60)
|
|
|
-
|
|
|
- if slot_ts < time.time(): continue
|
|
|
- if slot_ts >= season_end_ts: continue
|
|
|
- if slot_ts in booked_ts: continue
|
|
|
-
|
|
|
- is_blocked = False
|
|
|
- check_ts = slot_ts + 1
|
|
|
- for b_start, b_end in blocked_ranges:
|
|
|
- if b_start <= check_ts < b_end:
|
|
|
- is_blocked = True
|
|
|
- break
|
|
|
- if is_blocked: continue
|
|
|
-
|
|
|
- payload = {
|
|
|
- "resource_id": self.resource_id,
|
|
|
- "timestamp": slot_ts,
|
|
|
- "datetime": slot_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
- }
|
|
|
- time_slot = TimeSlot(
|
|
|
- time=slot_dt.strftime("%H:%M"),
|
|
|
- label=json.dumps(payload)
|
|
|
- )
|
|
|
- date_key = slot_dt.date()
|
|
|
- if date_key not in valid_slots_map:
|
|
|
- valid_slots_map[date_key] = []
|
|
|
- valid_slots_map[date_key].append(time_slot)
|
|
|
+ start_ts = int(current_day.timestamp()) + start_min * 60
|
|
|
+ end_ts = int(current_day.timestamp()) + end_min * 60
|
|
|
+
|
|
|
+ curr_ts = start_ts
|
|
|
+
|
|
|
+ # 开始执行官方引擎 1:1 的“碰撞与吸附算法”
|
|
|
+ while curr_ts + default_length <= end_ts:
|
|
|
+ if curr_ts < current_time_ts or curr_ts >= season_end_ts:
|
|
|
+ curr_ts += 1800 # 安全步进
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 【核心机制 1】:强制网格吸附 (Snap to grid)
|
|
|
+ # 官方代码 start=precalc_constraints('0') 意味着仅允许在整点 (0分) 建立预约
|
|
|
+ dt = datetime.fromtimestamp(curr_ts, tz=timezone.utc)
|
|
|
+ if dt.minute != 0:
|
|
|
+ # 发现不在整点 (如11:30),立刻强制向前吸附到下一个整点 (加上剩余的分钟数)
|
|
|
+ minutes_to_add = 60 - dt.minute
|
|
|
+ curr_ts += minutes_to_add * 60
|
|
|
+ continue # 时间已改变,重新循环执行检查
|
|
|
+
|
|
|
+ slot_end = curr_ts + default_length
|
|
|
+
|
|
|
+ # 【核心机制 2】:贪婪碰撞检测 (Collision detection)
|
|
|
+ overlapping_end = 0
|
|
|
+ for b_start, b_end in all_blocks:
|
|
|
+ # 区间交集判断:起点早于障碍物终点 且 终点晚于障碍物起点 -> 发生碰撞
|
|
|
+ if curr_ts < b_end and slot_end > b_start:
|
|
|
+ if b_end > overlapping_end:
|
|
|
+ overlapping_end = b_end
|
|
|
+
|
|
|
+ if overlapping_end > 0:
|
|
|
+ # 碰撞触发:指针直接抛弃当前区间,跳跃到“阻挡物最晚结束的时间点”
|
|
|
+ curr_ts = overlapping_end
|
|
|
+ # 下一轮循环时,【机制1】会自动将其吸附回合理的网格位!
|
|
|
+ else:
|
|
|
+ # 完美过检:没有碰撞且处于正确网格,记录合法 Slot
|
|
|
+ payload = {
|
|
|
+ "resource_id": self.resource_id,
|
|
|
+ "timestamp": curr_ts,
|
|
|
+ "datetime": dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
+ }
|
|
|
+ time_slot = TimeSlot(
|
|
|
+ time=dt.strftime("%H:%M"),
|
|
|
+ label=json.dumps(payload)
|
|
|
+ )
|
|
|
+ date_key = dt.date()
|
|
|
+ if date_key not in valid_slots_map:
|
|
|
+ valid_slots_map[date_key] =[]
|
|
|
+
|
|
|
+ valid_slots_map[date_key].append(time_slot)
|
|
|
+
|
|
|
+ # 已放入一个格后,指针按订单长度向后推移
|
|
|
+ curr_ts += default_length
|
|
|
|
|
|
if valid_slots_map:
|
|
|
res.success = True
|