Spaces:
Running
Running
import asyncio | |
from collections import Counter | |
from datetime import datetime, timedelta | |
import re | |
import os | |
from itertools import permutations | |
import json | |
from src.firestore_db import FirestoreDB | |
from src.cache import Cache | |
from src.logger import logger | |
from config.settings import Settings | |
class LotteryCore: | |
def __init__(self, db: FirestoreDB, cache: Cache): | |
self.db = db | |
self.cache = cache | |
self.initialized = False | |
logger.info("[LotteryCore] Khởi tạo với Firestore") | |
self.prize_mapping = { | |
"đặc biệt": "DB", | |
"giải nhất": "Nhat", | |
"giải nhì": "Nhi", | |
"giải ba": "Ba", | |
"giải tư": "Tu", | |
"giải năm": "Nam", | |
"giải sáu": "Sau", | |
"giải bảy": "Bay", | |
"giải tám": "Tam" | |
} | |
async def initialize(self): | |
if self.initialized: | |
return | |
try: | |
logger.info("[initialize] Khởi tạo cơ bản, không dùng vector") | |
self.initialized = True | |
except Exception as e: | |
logger.error(f"[initialize] Lỗi khi khởi tạo: {str(e)}") | |
raise | |
def _get_day_name(self, weekday: int) -> str: | |
days = {0: "Hai", 1: "Ba", 2: "Tư", 3: "Năm", 4: "Sáu", 5: "Bảy", 6: "Chủ Nhật"} | |
return days.get(weekday, "Không xác định") | |
async def parse_date(self, date_str: str) -> tuple[str, datetime] | None: | |
formats = [ | |
"%d-%m-%Y", "%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d", | |
"%d-%m-%y", "%y-%m-%d", "%d/%m/%y", "%y/%m/%d", | |
"%d.%m.%Y", "%d-%m-%Y", "%d/%m/%Y", "%Y.%m.%d", | |
"%m/%d/%Y", "%m-%d-%Y", "%d%m%Y", "%Y%m%d" | |
] | |
for fmt in formats: | |
try: | |
parsed = datetime.strptime(date_str, fmt) | |
if parsed.year < 100: | |
parsed = parsed.replace(year=parsed.year + 2000) | |
return parsed.strftime("%d-%m-%Y"), parsed | |
except ValueError: | |
continue | |
logger.warning(f"[parse_date] Invalid date format: {date_str}") | |
return None | |
def tach_hang(self, so): | |
so_clean = so.replace('@', '') | |
so = "@" * (6 - len(so_clean)) + so_clean | |
return { | |
"trieu": so[0] if so[0] != '@' else '@', | |
"tramnghin": so[1] if so[1] != '@' else '@', | |
"chucnghin": so[2] if so[2] != '@' else '@', | |
"nghin": so[3] if so[3] != '@' else '@', | |
"chuc": so[4] if so[4] != '@' else '@', | |
"donvi": so[5] if so[5] != '@' else '@' | |
} | |
def create_longs(self, data): | |
if not isinstance(data.get("giai"), dict): | |
logger.warning(f"[create_longs] 'giai' không phải map") | |
return {f"L{i}": ["@"] * 18 for i in range(1, 7)} | |
lo = [] | |
for giai in ["DB", "Nhat", "Nhi", "Ba", "Tu", "Nam", "Sau", "Bay", "Tam"]: | |
if giai in data["giai"]: | |
for so in data["giai"][giai]: | |
if not so.isdigit(): | |
logger.warning(f"[create_longs] Số không hợp lệ trong {giai}: {so}") | |
lo.append("@@@@@@") | |
continue | |
so_clean = so.replace('@', '') | |
so = "@" * (6 - len(so_clean)) + so_clean | |
lo.append(so) | |
logger.debug(f"[create_longs] Số lô thu thập: {len(lo)}") | |
while len(lo) < 18: | |
lo.append("@@@@@@") | |
lo = lo[:18] | |
longs = {f"L{i}": [] for i in range(1, 7)} | |
for so in lo: | |
hang = self.tach_hang(so) | |
longs["L1"].append(str(hang["donvi"])) | |
longs["L2"].append(str(hang["chuc"])) | |
longs["L3"].append(str(hang["nghin"])) | |
longs["L4"].append(str(hang["chucnghin"])) | |
longs["L5"].append(str(hang["tramnghin"])) | |
longs["L6"].append(str(hang["trieu"])) | |
return longs | |
def _map_dai_name(self, dai_name: str) -> str: | |
dai_name_clean = dai_name.lower().replace(" ", "").replace("chủnhật", "").replace("chu", "") | |
dai_mapping = { | |
"xskh": "xskh", | |
"khánhhòa": "xskh", | |
"khanhhoa": "xskh", | |
"khánhhoà": "xskh", | |
"khanhhoà": "xskh", | |
"xổsốkhánhhòa": "xskh", | |
"xosokhanhhoa": "xskh", | |
"kshoa": "xskh", | |
"khhoa": "xskh", | |
"xsdl": "xsdl", | |
"đắklắk": "xsdl", | |
"daklak": "xsdl", | |
"đăklăk": "xsdl", | |
"daklăk": "xsdl", | |
"xổsốđắklắk": "xsdl", | |
"xosodaklak": "xsdl", | |
"daklac": "xsdl", | |
"đăklăc": "xsdl", | |
"xspy": "xspy", | |
"phúyên": "xspy", | |
"phuyen": "xspy", | |
"phuyén": "xspy", | |
"phúyền": "xspy", | |
"phuyeen": "xspy", | |
"xổsốphúyên": "xspy", | |
"xosophuyen": "xspy", | |
"xsqng": "xsqng", | |
"quảngngãi": "xsqng", | |
"quangngai": "xsqng", | |
"quảngngai": "xsqng", | |
"quangngãi": "xsqng", | |
"xổsốquảngngãi": "xsqng", | |
"xosoquangngai": "xsqng", | |
"qngai": "xsqng", | |
"xsgl": "xsgl", | |
"gialai": "xsgl", | |
"gialay": "xsgl", | |
"xổsốgialai": "xsgl", | |
"xosogialai": "xsgl", | |
"xsbd": "xsbd", | |
"bìnhđịnh": "xsbd", | |
"binhdinh": "xsbd", | |
"bìnhđinh": "xsbd", | |
"binhđinh": "xsbd", | |
"binhdịnh": "xsbd", | |
"xổsốbìnhđịnh": "xsbd", | |
"xosobinhdinh": "xsbd", | |
"bdinh": "xsbd", | |
} | |
mapped_dai = dai_mapping.get(dai_name_clean, dai_name_clean) | |
if mapped_dai == dai_name_clean and mapped_dai not in Settings.DAI_CONFIG: | |
logger.warning(f"[_map_dai_name] Tên đài không được ánh xạ: {dai_name}") | |
else: | |
logger.info(f"[_map_dai_name] Ánh xạ đài: {dai_name} -> {mapped_dai}") | |
return mapped_dai | |
def _validate_number(self, prize: str, number: str) -> bool: | |
expected_lengths = { | |
"DB": 6, "Nhat": 5, "Nhi": 5, "Ba": 5, "Tu": 5, | |
"Nam": 4, "Sau": 4, "Bay": 3, "Tam": 2 | |
} | |
expected_length = expected_lengths.get(prize) | |
if not expected_length: | |
logger.warning(f"[_validate_number] Unknown prize: {prize}") | |
return False | |
if not number.isdigit(): | |
logger.warning(f"[_validate_number] Number is not numeric: {number}") | |
return False | |
if len(number) != expected_length: | |
logger.warning(f"[_validate_number] Number length {len(number)} does not match expected {expected_length} for prize {prize}") | |
return False | |
return True | |
async def add_lottery_result(self, text: str) -> str: | |
try: | |
# Chuẩn hóa toàn bộ văn bản: thay thế tab và khoảng trắng liên tiếp | |
text = re.sub(r'[\t\s]+', ' ', text.strip()) | |
# Khôi phục xuống dòng trước tên giải | |
for prize in self.prize_mapping: | |
text = re.sub(rf'({prize}\s)', f'\n\\1', text, flags=re.IGNORECASE) | |
lines = text.split('\n') | |
if not lines: | |
logger.debug("[add_lottery_result] Empty input text") | |
return "Lỗi: Văn bản rỗng." | |
header_match = re.match( | |
r"Xổ số\s+(.+?)(?:\s+Thứ\s+\w+|Chủ\s+Nhật)?\s*(\d{2}[-./]\d{2}[-./]\d{4})\b", | |
lines[0], | |
re.IGNORECASE | |
) | |
if not header_match: | |
logger.debug(f"[add_lottery_result] Invalid header format: {lines[0]}") | |
return "Lỗi: Dòng đầu phải có định dạng 'Xổ số <Đài> [Thứ X/Chủ Nhật] DD-MM-YYYY'. Ví dụ: 'Xổ số Đắk Lắk 17-06-2025'." | |
dai_name = header_match.group(1).strip() | |
ngay = header_match.group(2).replace("/", "-").replace(".", "-") | |
dai = self._map_dai_name(dai_name) | |
parsed_date = await self.parse_date(ngay) | |
if not parsed_date: | |
logger.debug(f"[add_lottery_result] Invalid date format: {ngay}") | |
return f"Lỗi: Định dạng ngày không hợp lệ: {ngay}." | |
ngay_normalized, date = parsed_date | |
if dai in Settings.DAI_CONFIG and date.weekday() not in Settings.DAI_CONFIG[dai]["days"]: | |
day_name = date.strftime('%A') | |
expected_days = ', '.join([self._get_day_name(d) for d in Settings.DAI_CONFIG[dai]["days"]]) | |
logger.debug(f"[add_lottery_result] Invalid day: {ngay} is {day_name}, expected {expected_days} for {dai}") | |
return f"Lỗi: Đài {dai} chỉ quay vào {expected_days}. Ngày {ngay} là {day_name}." | |
giai_map = self.prize_mapping | |
expected_counts = {"DB": 1, "Nhat": 1, "Nhi": 1, "Ba": 2, "Tu": 7, "Nam": 1, "Sau": 3, "Bay": 1, "Tam": 1} | |
expected_lengths = {"DB": 6, "Nhat": 5, "Nhi": 5, "Ba": 5, "Tu": 5, "Nam": 4, "Sau": 4, "Bay": 3, "Tam": 2} | |
giai = {} | |
current_giai = None | |
numbers = [] | |
error_lines = [] | |
for i, line in enumerate(lines[1:], 1): | |
line = re.sub(r'[\t\s]+', ' ', line.strip()) | |
line = re.sub(r'[.\-,;\s]+', ' ', line).strip() | |
logger.debug(f"[add_lottery_result] Processing line {i}: '{line}'") | |
if not line or "xổ số miền" in line.lower(): | |
continue | |
for name, key in giai_map.items(): | |
if re.match(rf"^{name}\b", line, re.IGNORECASE): | |
if current_giai and numbers: | |
giai[current_giai] = numbers | |
numbers = [] | |
current_giai = key | |
line = re.sub(rf"^{name}\b", "", line, flags=re.IGNORECASE).strip() | |
break | |
if current_giai: | |
number_list = [num.strip() for num in line.split() if num.strip().isdigit()] | |
if not number_list: | |
error_lines.append(f"Dòng {i}: '{line}' (không tìm thấy số hợp lệ)") | |
numbers.extend(number_list) | |
logger.debug(f"[add_lottery_result] Extracted numbers for {current_giai}: {number_list}") | |
if current_giai and numbers: | |
giai[current_giai] = numbers | |
if not giai: | |
error_msg = "Lỗi: Không tìm thấy giải nào. Vui lòng kiểm tra định dạng kết quả, ví dụ: '/load_lottery Xổ số Đắk Lắk 17-06-2025\nĐặc biệt 123456\nGiải nhất 12345'." | |
if error_lines: | |
error_msg += "\nCác dòng lỗi:\n" + "\n".join(error_lines) | |
logger.debug(f"[add_lottery_result] No results parsed from input: {text}") | |
return error_msg | |
for key in expected_counts: | |
if key not in giai: | |
logger.debug(f"[add_lottery_result] Missing prize: {key}") | |
return f"Lỗi: Thiếu giải {key}. Vui lòng kiểm tra đầu vào." | |
if len(giai[key]) != expected_counts[key]: | |
logger.debug(f"[add_lottery_result] Incorrect number count for {key}: expected {expected_counts[key]}, got {len(giai[key])}") | |
return f"Lỗi: Giải {key} phải có {expected_counts[key]} số, nhận được {len(giai[key])} số." | |
for num in giai[key]: | |
if not self._validate_number(key, num): | |
logger.debug(f"[add_lottery_result] Invalid number for {key}: {num}") | |
return f"Lỗi: Số trong giải {key} phải có {expected_lengths[key]} chữ số và chỉ chứa số, nhận được '{num}'." | |
doc_id = f"lottery-{ngay_normalized}_{dai}" | |
if (await self.db.lottery_results.document(doc_id).get()).exists: | |
logger.debug(f"[add_lottery_result] Data already exists: {doc_id}") | |
return f"Dữ liệu cho {dai} ngày {ngay} đã tồn tại." | |
data = {"ngay": ngay_normalized, "dai": dai, "giai": giai} | |
if await self.db.save_lottery(data, is_vector=False): | |
logger.info(f"[add_lottery_result] Lưu kết quả thành công cho {dai} ngày {ngay}") | |
await self.cache.clear(type="lottery") # Sửa từ delete_all thành clear | |
return f"Đã thêm kết quả xổ số cho {dai} ngày {ngay}." | |
logger.error(f"[add_lottery_result] Failed to save data for {dai} ngày {ngay}") | |
return f"Lỗi khi lưu kết quả xổ số cho {dai} ngày {ngay}." | |
except Exception as e: | |
logger.error(f"[add_lottery_result] Lỗi: {str(e)}") | |
return f"Lỗi: {str(e)}" | |
async def load_lottery(self, text: str, override: bool = False) -> str: | |
try: | |
# Chuẩn hóa toàn bộ văn bản: thay thế tab và khoảng trắng liên tiếp | |
text = re.sub(r'[\t\s]+', ' ', text.strip()) | |
# Khôi phục xuống dòng trước tên giải | |
for prize in self.prize_mapping: | |
text = re.sub(rf'({prize}\s)', f'\n\\1', text, flags=re.IGNORECASE) | |
lines = text.split('\n') | |
if not lines: | |
logger.debug("[load_lottery] Empty input text") | |
return "Lỗi: Văn bản rỗng." | |
if lines[0].lower().startswith(tuple(Settings.DAI_CONFIG.keys())): | |
parts = lines[0].split(" ", 2) | |
if len(parts) >= 2 and parts[0].lower() in Settings.DAI_CONFIG: | |
dai = parts[0].lower() | |
ngay = parts[1].replace("/", "-").replace(".", "-") | |
lines = lines[1:] if len(parts) == 2 else [parts[2]] + lines[1:] | |
else: | |
logger.debug(f"[load_lottery] Invalid format: {lines[0]}") | |
return "Lỗi: Định dạng không đúng. Vui lòng dùng: '<mã đài> DD-MM-YYYY' hoặc 'Xổ số <Đài> [Thứ X] DD-MM-YYYY'. Ví dụ: 'Xổ số Đắk Lắk 17-06-2025'." | |
else: | |
header_match = re.match( | |
r"Xổ số\s+(.+?)(?:\s+Thứ\s+\w+|Chủ\s+Nhật)?\s*(\d{2}[-./]\d{2}[-./]\d{4})\b", | |
lines[0], | |
re.IGNORECASE | |
) | |
if not header_match: | |
logger.debug(f"[load_lottery] Invalid header format: {lines[0]}") | |
return "Lỗi: Dòng đầu phải có định dạng 'Xổ số <Đài> [Thứ X/Chủ Nhật] DD-MM-YYYY'. Ví dụ: 'Xổ số Đắk Lắk 17-06-2025'." | |
dai_name = header_match.group(1).strip() | |
ngay = header_match.group(2).replace("/", "-").replace(".", "-") | |
dai = self._map_dai_name(dai_name) | |
parsed_date = await self.parse_date(ngay) | |
if not parsed_date: | |
logger.debug(f"[load_lottery] Invalid date format: {ngay}") | |
return f"Lỗi: Định dạng ngày không hợp lệ: {ngay}." | |
ngay_normalized, date = parsed_date | |
if dai in Settings.DAI_CONFIG and date.weekday() not in Settings.DAI_CONFIG[dai]["days"]: | |
day_name = date.strftime('%A') | |
expected_days = ', '.join([self._get_day_name(d) for d in Settings.DAI_CONFIG[dai]["days"]]) | |
logger.debug(f"[load_lottery] Invalid day: {ngay} is {day_name}, expected {expected_days} for {dai}") | |
return f"Lỗi: Đài {dai} chỉ quay vào {expected_days}. Ngày {ngay} là {day_name}." | |
giai_map = self.prize_mapping | |
expected_counts = {"DB": 1, "Nhat": 1, "Nhi": 1, "Ba": 2, "Tu": 7, "Nam": 1, "Sau": 3, "Bay": 1, "Tam": 1} | |
expected_lengths = {"DB": 6, "Nhat": 5, "Nhi": 5, "Ba": 5, "Tu": 5, "Nam": 4, "Sau": 4, "Bay": 3, "Tam": 2} | |
giai = {} | |
current_giai = None | |
numbers = [] | |
error_lines = [] | |
for i, line in enumerate(lines[1:], 1): | |
line = re.sub(r'[\t\s]+', ' ', line.strip()) | |
line = re.sub(r'[.\-,;\s]+', ' ', line).strip() | |
logger.debug(f"[load_lottery] Processing line {i}: '{line}'") | |
if not line or "xổ số miền" in line.lower(): | |
continue | |
for name, key in giai_map.items(): | |
if re.match(rf"^{name}\b", line, re.IGNORECASE): | |
if current_giai and numbers: | |
giai[current_giai] = numbers | |
numbers = [] | |
current_giai = key | |
line = re.sub(rf"^{name}\b", "", line, flags=re.IGNORECASE).strip() | |
break | |
if current_giai: | |
number_list = [num.strip() for num in line.split() if num.strip().isdigit()] | |
if not number_list: | |
error_lines.append(f"Dòng {i}: '{line}' (không tìm thấy số hợp lệ)") | |
numbers.extend(number_list) | |
logger.debug(f"[load_lottery] Extracted numbers for {current_giai}: {number_list}") | |
if current_giai and numbers: | |
giai[current_giai] = numbers | |
if not giai: | |
error_msg = "Lỗi: Không tìm thấy giải nào. Vui lòng kiểm tra định dạng kết quả, ví dụ: '/load_lottery Xổ số Đắk Lắk 17-06-2025\nĐặc biệt 123456\nGiải nhất 12345'." | |
if error_lines: | |
error_msg += "\nCác dòng lỗi:\n" + "\n".join(error_lines) | |
logger.debug(f"[load_lottery] No results parsed from input: {text}") | |
return error_msg | |
for key in expected_counts: | |
if key not in giai: | |
logger.debug(f"[load_lottery] Missing prize: {key}") | |
return f"Lỗi: Thiếu giải {key}. Vui lòng kiểm tra đầu vào." | |
if len(giai[key]) != expected_counts[key]: | |
logger.debug(f"[load_lottery] Incorrect number count for {key}: expected {expected_counts[key]}, got {len(giai[key])}") | |
return f"Lỗi: Giải {key} phải có {expected_counts[key]} số, nhận được {len(giai[key])} số." | |
for num in giai[key]: | |
if not self._validate_number(key, num): | |
logger.debug(f"[load_lottery] Invalid number for {key}: {num}") | |
return f"Lỗi: Số trong giải {key} phải có {expected_lengths[key]} chữ số và chỉ chứa số, nhận được '{num}'." | |
doc_id = f"lottery-{ngay_normalized}_{dai}" | |
if not override and (await self.db.lottery_results.document(doc_id).get()).exists: | |
logger.debug(f"[load_lottery] Data already exists: {doc_id}") | |
return f"Dữ liệu cho {dai} ngày {ngay} đã tồn tại." | |
data = {"ngay": ngay_normalized, "dai": dai, "giai": giai} | |
if await self.db.save_lottery(data, is_vector=False): | |
logger.info(f"[load_lottery] Lưu kết quả thành công cho {dai} ngày {ngay}") | |
await self.cache.clear(type="lottery") # Sửa từ delete_all thành clear | |
return f"Kết quả xổ số đã được nạp thành công: Đã nạp kết quả xổ số cho {dai} ngày {ngay}." | |
logger.error(f"[load_lottery] Failed to save data for {dai} ngày {ngay}") | |
return f"Lỗi khi lưu kết quả xổ số cho {dai} ngày {ngay}." | |
except Exception as e: | |
logger.error(f"[load_lottery] Lỗi: {str(e)}") | |
return f"Lỗi: {str(e)}" | |
async def load_from_json(self, json_file_path: str) -> str: | |
try: | |
if not os.path.exists(json_file_path) or os.path.getsize(json_file_path) == 0: | |
logger.debug(f"[load_from_json] File not found or empty: {json_file_path}") | |
return f"Tệp {json_file_path} không tồn tại hoặc rỗng." | |
with open(json_file_path, 'r', encoding='utf-8') as f: | |
data_list = json.load(f) | |
if not isinstance(data_list, list): | |
logger.debug("[load_from_json] JSON data is not a list") | |
return "Dữ liệu JSON không phải là danh sách." | |
batch_size = 100 | |
success_count = 0 | |
processed_keys = set() | |
batch = [] | |
for data in data_list: | |
ngay = data.get("ngay") | |
dai = data.get("dai") | |
giai = data.get("giai") | |
if not all([ngay, dai, giai]): | |
logger.warning(f"[load_from_json] Bỏ qua bản ghi thiếu dữ liệu: ngay={ngay}, dai={dai}") | |
continue | |
parsed_date = await self.parse_date(ngay) | |
if not parsed_date: | |
logger.warning(f"[load_from_json] Ngày không hợp lệ: {ngay}") | |
continue | |
ngay_normalized, date = parsed_date | |
dai = self._map_dai_name(dai) | |
if dai not in Settings.DAI_CONFIG: | |
logger.error(f"[load_from_json] Đài không hợp lệ: {dai}, bỏ qua bản ghi.") | |
continue | |
if date.weekday() not in Settings.DAI_CONFIG[dai]["days"]: | |
logger.warning(f"[load_from_json] Ngày {ngay} không hợp lệ cho đài {dai}") | |
continue | |
key = f"lottery-{ngay_normalized}_{dai}" | |
if key in processed_keys: | |
continue | |
processed_keys.add(key) | |
data["ngay"] = ngay_normalized | |
data["dai"] = dai | |
batch.append(data) | |
if len(batch) >= batch_size: | |
for data in batch: | |
try: | |
async with asyncio.timeout(10): | |
if await self.db.save_lottery(data, is_vector=False): | |
success_count += 1 | |
except asyncio.TimeoutError: | |
logger.error(f"[load_from_json] Timeout: {data['ngay']}_{data['dai']}") | |
except Exception as e: | |
logger.error(f"[load_from_json] Lỗi khi lưu: {str(e)}") | |
batch = [] | |
if batch: | |
for data in batch: | |
try: | |
async with asyncio.timeout(10): | |
if await self.db.save_lottery(data, is_vector=False): | |
success_count += 1 | |
except asyncio.TimeoutError: | |
logger.error(f"[load_from_json] Timeout: {data['ngay']}_{data['dai']}") | |
except Exception as e: | |
logger.error(f"[load_from_json] Lỗi khi lưu: {str(e)}") | |
logger.info(f"[load_from_json] Đã lưu {success_count} bản ghi") | |
return f"Đã tải và lưu {success_count} bản ghi từ {json_file_path}." | |
except json.JSONDecodeError: | |
logger.debug(f"[load_from_json] Invalid JSON in file: {json_file_path}") | |
return f"Lỗi: Tệp {json_file_path} không chứa JSON hợp lệ." | |
except Exception as e: | |
logger.error(f"[load_from_json] Lỗi: {str(e)}") | |
return f"Lỗi: {str(e)}" | |
async def migrate_lottery_data(self) -> str: | |
try: | |
logger.info("[migrate_lottery_data] Bắt đầu chuẩn hóa dữ liệu") | |
moved_results = 0 | |
deleted_count = 0 | |
async for doc in await self.db.stream(data_type="lottery-results"): | |
doc_id = doc.id | |
data = doc.to_dict() | |
ngay = data.get("ngay") | |
dai = data.get("dai") | |
if not ngay or not dai: | |
logger.warning(f"Skipping invalid document: {doc_id}, ngay={ngay}, dai={dai}") | |
continue | |
parsed_date = await self.parse_date(ngay) | |
if not parsed_date: | |
logger.warning(f"Skipping invalid date in {doc_id}: {ngay}") | |
continue | |
ngay_normalized, _ = parsed_date | |
new_dai = self._map_dai_name(dai) | |
data["ngay"] = ngay_normalized | |
data["dai"] = new_dai | |
new_doc_id = f"lottery-{ngay_normalized}_{new_dai}" | |
if doc_id != new_doc_id: | |
await self.db.lottery_results.document(new_doc_id).set(data, merge=True) | |
await self.db.lottery_results.document(doc_id).delete() | |
moved_results += 1 | |
deleted_count += 1 | |
else: | |
await self.db.lottery_results.document(doc_id).set(data, merge=True) | |
moved_results += 1 | |
logger.info(f"[migrate_lottery_data] Hoàn tất: {moved_results} bản ghi, {deleted_count} bản xóa") | |
return f"Hoàn tất: {moved_results} bản ghi, {deleted_count} bản xóa." | |
except Exception as e: | |
logger.error(f"[migrate_lottery_data] Lỗi: {str(e)}") | |
return f"Lỗi: {str(e)}" | |
async def analyze(self, dai: str, start_date: str, end_date: str) -> dict: | |
try: | |
cache_key = f"lottery:{dai}:{start_date}:{end_date}" | |
cached = await self.cache.get(cache_key, type="lottery") | |
if cached: | |
return cached | |
parsed_start = await self.parse_date(start_date) | |
parsed_end = await self.parse_date(end_date) | |
if not parsed_start or not parsed_end: | |
logger.debug(f"[analyze] Invalid date format: start={start_date}, end={end_date}") | |
return {"frequencies": {}, "triplets": [], "error": "Định dạng ngày không hợp lệ."} | |
start_date_str, _ = parsed_start | |
end_date_str, _ = parsed_end | |
data = await self.db.get_lottery(dai, start_date_str, end_date_str, data_type="results", select_fields=["ngay", "dai", "giai"]) | |
if not data: | |
logger.debug(f"[analyze] No data found for {dai} from {start_date} to {end_date}") | |
return {"frequencies": {}, "triplets": [], "error": f"Không tìm thấy dữ liệu cho {dai} từ {start_date} đến {end_date}."} | |
hang_data = {"trieu": [], "tramnghin": [], "chucnghin": [], "nghin": [], "chuc": [], "donvi": []} | |
for entry in data: | |
for giai, so_list in entry["giai"].items(): | |
for so in so_list: | |
hang = self.tach_hang(so) | |
for key in hang_data: | |
hang_data[key].append(hang[key]) | |
stats = {} | |
top_numbers = {} | |
for key in hang_data: | |
counter = Counter(x for x in hang_data[key] if x != '@') | |
stats[key] = [{"number": num, "count": count} for num, count in counter.items()] | |
top_numbers[key] = counter.most_common(1)[0][0] if counter else '@' | |
numbers = [top_numbers[key] for key in ["trieu", "tramnghin", "chucnghin", "nghin", "chuc", "donvi"]] | |
triplets = [''.join(t) for t in permutations(numbers, 3)] | |
counter = Counter(triplets) | |
top_triplets = [triplet for triplet, _ in counter.most_common(5)] | |
result = { | |
"frequencies": stats, | |
"triplets": top_triplets | |
} | |
await self.cache.set(cache_key, result, type="lottery") | |
return result | |
except Exception as e: | |
logger.error(f"[analyze] Lỗi: {str(e)}") | |
return {"frequencies": {}, "triplets": [], "error": f"Lỗi: {str(e)}"} | |
async def get_schedule(self, dai: str = None) -> str: | |
if dai: | |
dai = dai.lower() | |
if dai in Settings.DAI_CONFIG: | |
days = ', '.join([self._get_day_name(d) for d in Settings.DAI_CONFIG[dai]["days"]]) | |
return f"Xổ số {Settings.DAI_CONFIG[dai]['name']} ({dai.upper()}) quay vào {days} hàng tuần." | |
logger.debug(f"[get_schedule] Unknown dai: {dai}") | |
return f"Lỗi: Không tìm thấy lịch cho đài {dai}." | |
result = "Lịch xổ số các đài:\n" | |
for dai_code, config in Settings.DAI_CONFIG.items(): | |
days = ', '.join([self._get_day_name(d) for d in config["days"]]) | |
result += f"- {dai_code.upper()} ({config['name']}): {days}\n" | |
result += "Gửi tên đài để xem chi tiết, ví dụ: 'Lịch XSKH'." | |
return result | |
async def add_lottery_vectors(self, json_file_path: str) -> str: | |
logger.info("[add_lottery_vectors] Tính năng vector đã bị tắt") | |
return "Tính năng lưu vector xổ số đã bị tắt." |