# coding=utf-8 import hashlib import logging import os import sys import uuid from datetime import datetime project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if project_root not in sys.path: sys.path.append(project_root) from DbKit.Db import Db from Config.Config import DB_URL, PRICE_FLATTEN_TO_24H_GLOBAL from Model.StationProfile import StationProfile from Model.StationStatus import StationStatus from Model.StationPriceSchedule import StationPriceSchedule from Apps.YeLiTe.Config.Setting import PRICE_FLATTEN_TO_24H import re logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) class YeLiTeService: def __init__(self): self.db = Db(db_url=DB_URL) self.station_profile_model = StationProfile() self.station_status_model = StationStatus() self.station_price_schedule_model = StationPriceSchedule() self.operator = "驿来特" def generate_id(self): return str(uuid.uuid4()) def get_hash(self, s: str) -> str: return hashlib.md5(s.encode('utf-8')).hexdigest() def _to_float(self, value) -> float: """ 从字符串中提取浮点数,过滤掉非数字字符 """ if value is None: return 0.0 if isinstance(value, (int, float)): return float(value) import re try: # 提取第一个匹配的数字部分(包含小数点) match = re.search(r"[-+]?\d*\.?\d+", str(value)) if match: return float(match.group()) return 0.0 except: return 0.0 async def init_db(self): await self.db.init_db() async def close_db(self): await self.db.close() async def log_task_start(self, task_id): """记录任务开始""" from datetime import datetime try: await self.db.save("t_crawl_task_log", { "task_id": task_id, "operator": self.operator, "start_time": datetime.now(), "status": "running" }, "task_id") except Exception as e: logger.error(f"记录任务开始日志失败: {e}") async def log_task_end(self, task_id, count, status, error_msg=None): """记录任务结束""" from datetime import datetime try: end_time = datetime.now() # 假设 start_time 已经在数据库中,我们可以通过 task_id 更新 # 这里的 duration 计算可以在 SQL 中做,或者先查出来再算 await self.db.update("t_crawl_task_log", { "task_id": task_id, "end_time": end_time, "station_count": count, "status": status, "error_msg": error_msg }, "task_id") except Exception as e: logger.error(f"更新任务结束日志失败: {e}") async def process_price_detail_data( self, station_name, hourly_schedule, address=None, distance=None, total_piles=None, free_piles=None, piles_detail=None, parking_info=None, ) -> bool: if not station_name or not hourly_schedule: return False station_hash = self.get_hash(station_name) now = datetime.now() schedule_to_save = hourly_schedule standardized_piles = [] total = total_piles or 0 free = free_piles or 0 if isinstance(piles_detail, list): for idx, p in enumerate(piles_detail): try: t = int(p.get("total", 0)) f = int(p.get("idle", p.get("free", 0))) total += t free += f standardized_piles.append( { "pile_no": f"G{idx+1}", "type": p.get("type", "未知"), "power": "", "status_text": f"空闲{f}/总{t}", "remark": "列表页忙闲", } ) except Exception: continue elif isinstance(piles_detail, str): piles_str = piles_detail nums = re.findall(r"\d+", piles_str) if len(nums) >= 2: try: f_val = int(nums[-2]) t_val = int(nums[-1]) free += f_val total += t_val standardized_piles.append( { "pile_no": "G1", "type": "未知", "power": "", "status_text": piles_str, "remark": "列表页忙闲(字符串)", } ) except Exception: pass if standardized_piles: total_piles = total free_piles = free piles_detail = standardized_piles use_flatten = (PRICE_FLATTEN_TO_24H_GLOBAL or PRICE_FLATTEN_TO_24H) and isinstance(hourly_schedule, list) if use_flatten: tmp = [None] * 24 for p in hourly_schedule: try: start_str = p.get("start") or "00:00" end_str = p.get("end") or "00:00" start_hour = int(start_str.split(":")[0]) end_hour = int(end_str.split(":")[0]) if end_hour == 0 and start_hour != 0: end_hour = 24 if end_hour < start_hour: end_hour += 24 price_val = self._to_float(p.get("price")) curr = start_hour while curr < end_hour: hour_idx = curr % 24 tmp[hour_idx] = { "start": f"{hour_idx:02d}:00", "end": f"{(hour_idx + 1):02d}:00", "price": price_val, } curr += 1 except Exception as e: logger.error(f"解析价格时段失败: {p}, error: {e}") last_valid = None for item in tmp: if item: last_valid = item break if last_valid: for i in range(24): if tmp[i] is None: new_item = last_valid.copy() new_item["start"] = f"{i:02d}:00" new_item["end"] = f"{(i+1):02d}:00" tmp[i] = new_item else: last_valid = tmp[i] if any(tmp): schedule_to_save = tmp async with await self.db.get_session() as session: profile_id = self.generate_id() await self.station_profile_model.save( session=session, id=profile_id, station_hash=station_hash, operator=self.operator, station_name=station_name, address=address, valid_start_time=now, ) schedule_id = self.generate_id() await self.station_price_schedule_model.save( session=session, id=schedule_id, station_hash=station_hash, schedule_json=schedule_to_save, valid_start_time=now, ) current_hour = now.hour current_price_info = {} if isinstance(schedule_to_save, list) and 0 <= current_hour < len(schedule_to_save): try: current_price_info = schedule_to_save[current_hour] or {} except: current_price_info = {} elif isinstance(schedule_to_save, dict): current_price_info = schedule_to_save status_id = self.generate_id() await self.station_status_model.save( session=session, id=status_id, station_hash=station_hash, total_piles=total_piles, free_piles=free_piles, piles_detail_json=piles_detail, parking_info=parking_info, distance=distance, current_price=current_price_info.get("price"), valid_start_time=now, ) await session.commit() cnt = len(schedule_to_save) if isinstance(schedule_to_save, list) else 1 logger.info(f"成功保存驿来特场站数据: {station_name} | 距离: {distance} | 价格条数: {cnt}") return True async def save_station_profile_and_status(self, station_name, address=None, total_piles=None, free_piles=None, piles_detail=None, parking_info=None, distance=None): """ 一次性写入场站基础信息与状态,用于列表页兜底 """ if not station_name: return False station_hash = self.get_hash(station_name) now = datetime.now() async with await self.db.get_session() as session: profile_id = self.generate_id() await self.station_profile_model.save( session=session, id=profile_id, station_hash=station_hash, operator=self.operator, station_name=station_name, address=address, valid_start_time=now, ) status_id = self.generate_id() await self.station_status_model.save( session=session, id=status_id, station_hash=station_hash, total_piles=total_piles, free_piles=free_piles, piles_detail_json=piles_detail, parking_info=parking_info, distance=distance, current_price=None, valid_start_time=now, ) await session.commit() logger.info(f"兜底写入驿来特场站基础信息与状态: {station_name} | 桩数: {free_piles}/{total_piles} | 距离: {distance}") return True async def process_station_list_vl(self, image_path, json_metadata, device_info=None, max_count=None) -> list: """ 基于 VL 模式处理场站列表 (整页识别) """ # 驿来特目前逻辑待实现,先占位 return []