root 1 ماه پیش
والد
کامیت
1cecea002e
1فایلهای تغییر یافته به همراه76 افزوده شده و 51 حذف شده
  1. 76 51
      plugins/grc_plugin.py

+ 76 - 51
plugins/grc_plugin.py

@@ -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