From 988586da8470f71a38f97bfbc8b5c6529668d7ab Mon Sep 17 00:00:00 2001 From: Mars Date: Tue, 24 Jun 2025 10:52:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=A2=84=E6=B5=8B=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E5=B1=95=E7=A4=BA=E6=A0=B7=E5=BC=8F=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=89=80=E6=9C=89=E9=A2=84=E6=B5=8B?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=92=8C=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E7=BA=A2=E7=90=83=E8=93=9D=E7=90=83=E4=B8=80=E8=A1=8C?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=EF=BC=8C=E9=97=B4=E8=B7=9D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=9B=86=E6=88=90=E9=A2=84=E6=B5=8B?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 ++- backend/app/api/endpoints/lottery.py | 42 +++ backend/app/api/v1/lottery.py | 42 +++ backend/app/main.py | 18 ++ backend/app/services/lottery.py | 265 +++++++++++++++++ backend/app/services/prediction_service.py | 26 +- frontend/src/App.vue | 9 +- frontend/src/api/lottery.js | 13 + frontend/src/views/Home.vue | 160 ++++++++++- frontend/src/views/Prediction.vue | 56 +++- start_system.py | 315 +++++++++++++++++++++ test_backend.py | 153 ++++++++++ 使用指南.md | 172 +++++++++++ 13 files changed, 1282 insertions(+), 26 deletions(-) create mode 100644 start_system.py create mode 100644 test_backend.py create mode 100644 使用指南.md diff --git a/README.md b/README.md index 4d6092c..c2549e3 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ - 数据查询和筛选 - 数据导出 - 手动数据录入 -- 自动数据更新 +- **自动数据更新** ⭐ 新功能 ### 2. 基础统计分析 - 号码出现频率统计 @@ -101,7 +101,22 @@ lottery/ ## 安装和运行 -### 后端 +### 快速启动 ⭐ 推荐 +使用一键启动脚本(推荐方式): +```bash +# 确保已安装Python 3.8+和Node.js 16+ +python start_system.py +``` + +脚本会自动: +- 检查Python版本和依赖 +- 启动后端服务(端口8000) +- 启动前端服务(端口5173) +- 显示访问地址和使用说明 + +### 手动启动 + +#### 后端 1. 创建虚拟环境 ```bash python -m venv venv @@ -124,7 +139,7 @@ pip install -r requirements.txt uvicorn main:app --reload --host 0.0.0.0 --port 8000 ``` -### 前端 +#### 前端 1. 安装依赖 ```bash cd frontend @@ -151,12 +166,18 @@ npm run build - 导入过程自动去重:只导入数据库中未出现过的开奖日期数据。 - 支持 pandas DataFrame 或 JSON 文件导入。 -#### 数据自动/手动更新 -- 系统支持从聚合数据API自动获取最新开奖记录。 +#### 数据自动/手动更新 ⭐ 新功能 +- **前端更新功能**: 在首页提供"更新双色球"、"更新大乐透"、"更新全部"三个按钮 +- **API更新接口**: 支持通过API接口更新数据 + - `POST /api/v1/lottery/update/ssq` - 更新双色球数据 + - `POST /api/v1/lottery/update/dlt` - 更新大乐透数据 + - `POST /api/v1/lottery/update/all` - 更新所有彩票数据 - 更新逻辑: - - 只插入数据库中未出现过的开奖日期(`open_time` 唯一性判断)。 - - 插入顺序为开奖日期升序,保证历史数据先入库,最新数据最后入库。 - - 日志记录于 `lottery_update.log`。 + - 从聚合数据API自动获取最新开奖记录 + - 只插入数据库中未出现过的开奖日期(`open_time` 唯一性判断) + - 插入顺序为开奖日期升序,保证历史数据先入库,最新数据最后入库 + - 实时显示更新结果和新增记录数量 + - 更新完成后自动刷新首页最新开奖信息 ##### 手动更新 ```bash diff --git a/backend/app/api/endpoints/lottery.py b/backend/app/api/endpoints/lottery.py index 27316a4..68c88fd 100644 --- a/backend/app/api/endpoints/lottery.py +++ b/backend/app/api/endpoints/lottery.py @@ -105,3 +105,45 @@ async def import_dlt_data( # 清理临时文件 if os.path.exists(file_path): os.remove(file_path) + + +@router.post("/update/ssq") +async def update_ssq_data(db: Session = Depends(get_db)): + """更新双色球数据""" + try: + result = LotteryService.update_ssq_data(db) + return { + "success": True, + "message": f"双色球数据更新成功,新增 {result.get('new_count', 0)} 条记录", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新双色球数据失败: {str(e)}") + + +@router.post("/update/dlt") +async def update_dlt_data(db: Session = Depends(get_db)): + """更新大乐透数据""" + try: + result = LotteryService.update_dlt_data(db) + return { + "success": True, + "message": f"大乐透数据更新成功,新增 {result.get('new_count', 0)} 条记录", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新大乐透数据失败: {str(e)}") + + +@router.post("/update/all") +async def update_all_lottery_data(db: Session = Depends(get_db)): + """更新所有彩票数据""" + try: + result = LotteryService.update_all_lottery_data(db) + return { + "success": True, + "message": "所有彩票数据更新完成", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新彩票数据失败: {str(e)}") diff --git a/backend/app/api/v1/lottery.py b/backend/app/api/v1/lottery.py index ff16e46..3a9ba4b 100644 --- a/backend/app/api/v1/lottery.py +++ b/backend/app/api/v1/lottery.py @@ -129,3 +129,45 @@ def import_dlt_data(file: UploadFile = File(...), db: Session = Depends(get_db)) tmp_path = tmp.name count = LotteryService.import_dlt_data(db, tmp_path) return {"imported": count} + + +@router.post("/update/ssq") +async def update_ssq_data(db: Session = Depends(get_db)): + """更新双色球数据""" + try: + result = LotteryService.update_ssq_data(db) + return { + "success": True, + "message": f"双色球数据更新成功,新增 {result.get('new_count', 0)} 条记录", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新双色球数据失败: {str(e)}") + + +@router.post("/update/dlt") +async def update_dlt_data(db: Session = Depends(get_db)): + """更新大乐透数据""" + try: + result = LotteryService.update_dlt_data(db) + return { + "success": True, + "message": f"大乐透数据更新成功,新增 {result.get('new_count', 0)} 条记录", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新大乐透数据失败: {str(e)}") + + +@router.post("/update/all") +async def update_all_lottery_data(db: Session = Depends(get_db)): + """更新所有彩票数据""" + try: + result = LotteryService.update_all_lottery_data(db) + return { + "success": True, + "message": "所有彩票数据更新完成", + "data": result + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新彩票数据失败: {str(e)}") diff --git a/backend/app/main.py b/backend/app/main.py index 520df1d..ceb2cbc 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -34,6 +34,24 @@ app.include_router( app.include_router( prediction_router, prefix=f"{settings.API_V1_STR}/prediction", tags=["prediction"]) + +@app.get("/") +def read_root(): + """根路径,用于健康检查""" + return { + "message": "欢迎使用彩票数据分析系统", + "version": "1.0.0", + "status": "running", + "docs": "/docs" + } + + +@app.get("/health") +def health_check(): + """健康检查端点""" + return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"} + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/app/services/lottery.py b/backend/app/services/lottery.py index 0cfafc4..721e170 100644 --- a/backend/app/services/lottery.py +++ b/backend/app/services/lottery.py @@ -247,3 +247,268 @@ class LotteryService: def get_latest_dlt(db: Session) -> Optional[DLTLottery]: """获取最新一期大乐透开奖记录""" return db.query(DLTLottery).order_by(desc(DLTLottery.open_time)).first() + + @staticmethod + def update_ssq_data(db: Session) -> Dict[str, Any]: + """更新双色球数据""" + import requests + import logging + from datetime import datetime + from requests.adapters import HTTPAdapter + from requests.packages.urllib3.util.retry import Retry + + # 配置日志 + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # API配置 + api_key = "7a4beb6175a2c4dacf6cf9cab43bfe6f" + api_url = "http://apis.juhe.cn/lottery/history" + + # 配置请求会话 + session = requests.Session() + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504] + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # 获取数据库中所有已存在的开奖日期 + existing_open_times = set( + row.open_time.strftime('%Y-%m-%d') + for row in db.query(SSQLottery.open_time).all() + ) + logger.info(f"数据库已存在{len(existing_open_times)}个开奖日期") + + # 从API获取数据 + params = { + 'key': api_key, + 'lottery_id': 'ssq', + 'page': 1, + 'page_size': 50 + } + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + response = session.get( + api_url, params=params, headers=headers, timeout=10) + response.raise_for_status() + data = response.json() + + if data.get('error_code') != 0: + raise Exception(f"API返回错误: {data.get('reason', '未知错误')}") + + if not data.get('result') or not data['result'].get('lotteryResList'): + raise Exception("API返回数据格式不正确") + + # 处理数据 + new_data = [] + lottery_list = sorted( + data['result']['lotteryResList'], + key=lambda x: x['lottery_date'], + reverse=True + ) + + for item in lottery_list: + # 验证数据 + try: + draw_date = datetime.strptime( + item['lottery_date'], '%Y-%m-%d') + if draw_date > datetime.now(): + continue + if not item['lottery_no'].isdigit(): + continue + + numbers = item['lottery_res'].split(',') + if len(numbers) != 7: + continue + if not all(1 <= int(n) <= 33 for n in numbers[:6]) or not 1 <= int(numbers[6]) <= 16: + continue + + if item['lottery_date'] in existing_open_times: + continue + + new_data.append({ + 'issue': item['lottery_no'], + 'open_time': draw_date.date(), + 'red_ball_1': int(numbers[0]), + 'red_ball_2': int(numbers[1]), + 'red_ball_3': int(numbers[2]), + 'red_ball_4': int(numbers[3]), + 'red_ball_5': int(numbers[4]), + 'red_ball_6': int(numbers[5]), + 'blue_ball': int(numbers[6]) + }) + except Exception as e: + logger.warning(f"处理数据项失败: {e}") + continue + + if not new_data: + return {"new_count": 0, "message": "数据已是最新"} + + # 按开奖日期升序排序 + new_data = sorted(new_data, key=lambda x: x['open_time']) + + # 保存到数据库 + for item in new_data: + lottery = SSQLottery(**item) + db.add(lottery) + + db.commit() + + return { + "new_count": len(new_data), + "message": f"成功更新双色球数据,新增{len(new_data)}条记录", + "latest_issue": new_data[-1]['issue'] if new_data else None, + "latest_date": new_data[-1]['open_time'].strftime('%Y-%m-%d') if new_data else None + } + + except Exception as e: + db.rollback() + logger.error(f"更新双色球数据失败: {str(e)}") + raise e + + @staticmethod + def update_dlt_data(db: Session) -> Dict[str, Any]: + """更新大乐透数据""" + import requests + import logging + from datetime import datetime + from requests.adapters import HTTPAdapter + from requests.packages.urllib3.util.retry import Retry + + # 配置日志 + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # API配置 + api_key = "7a4beb6175a2c4dacf6cf9cab43bfe6f" + api_url = "http://apis.juhe.cn/lottery/history" + + # 配置请求会话 + session = requests.Session() + retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504] + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # 获取数据库中所有已存在的开奖日期 + existing_open_times = set( + row.open_time.strftime('%Y-%m-%d') + for row in db.query(DLTLottery.open_time).all() + ) + logger.info(f"数据库已存在{len(existing_open_times)}个开奖日期") + + # 从API获取数据 + params = { + 'key': api_key, + 'lottery_id': 'dlt', + 'page': 1, + 'page_size': 50 + } + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + response = session.get( + api_url, params=params, headers=headers, timeout=10) + response.raise_for_status() + data = response.json() + + if data.get('error_code') != 0: + raise Exception(f"API返回错误: {data.get('reason', '未知错误')}") + + if not data.get('result') or not data['result'].get('lotteryResList'): + raise Exception("API返回数据格式不正确") + + # 处理数据 + new_data = [] + lottery_list = sorted( + data['result']['lotteryResList'], + key=lambda x: x['lottery_date'], + reverse=True + ) + + for item in lottery_list: + # 验证数据 + try: + draw_date = datetime.strptime( + item['lottery_date'], '%Y-%m-%d') + if draw_date > datetime.now(): + continue + if not item['lottery_no'].isdigit(): + continue + + numbers = item['lottery_res'].split(',') + if len(numbers) != 7: + continue + if not all(1 <= int(n) <= 35 for n in numbers[:5]) or not all(1 <= int(n) <= 12 for n in numbers[5:]): + continue + + if item['lottery_date'] in existing_open_times: + continue + + new_data.append({ + 'issue': item['lottery_no'], + 'open_time': draw_date.date(), + 'front_ball_1': int(numbers[0]), + 'front_ball_2': int(numbers[1]), + 'front_ball_3': int(numbers[2]), + 'front_ball_4': int(numbers[3]), + 'front_ball_5': int(numbers[4]), + 'back_ball_1': int(numbers[5]), + 'back_ball_2': int(numbers[6]) + }) + except Exception as e: + logger.warning(f"处理数据项失败: {e}") + continue + + if not new_data: + return {"new_count": 0, "message": "数据已是最新"} + + # 按开奖日期升序排序 + new_data = sorted(new_data, key=lambda x: x['open_time']) + + # 保存到数据库 + for item in new_data: + lottery = DLTLottery(**item) + db.add(lottery) + + db.commit() + + return { + "new_count": len(new_data), + "message": f"成功更新大乐透数据,新增{len(new_data)}条记录", + "latest_issue": new_data[-1]['issue'] if new_data else None, + "latest_date": new_data[-1]['open_time'].strftime('%Y-%m-%d') if new_data else None + } + + except Exception as e: + db.rollback() + logger.error(f"更新大乐透数据失败: {str(e)}") + raise e + + @staticmethod + def update_all_lottery_data(db: Session) -> Dict[str, Any]: + """更新所有彩票数据""" + try: + ssq_result = LotteryService.update_ssq_data(db) + dlt_result = LotteryService.update_dlt_data(db) + + return { + "ssq": ssq_result, + "dlt": dlt_result, + "total_new": ssq_result.get("new_count", 0) + dlt_result.get("new_count", 0) + } + except Exception as e: + raise e diff --git a/backend/app/services/prediction_service.py b/backend/app/services/prediction_service.py index 4b5e5c8..94198ca 100644 --- a/backend/app/services/prediction_service.py +++ b/backend/app/services/prediction_service.py @@ -367,6 +367,7 @@ class PredictionService: # 如果是双色球,还需要生成蓝球 predicted_blue = None + predicted_blues = None if lottery_type == 'ssq': # 分析蓝球规律 blue_numbers = [draw.blue_ball for draw in recent_draws] @@ -378,6 +379,26 @@ class PredictionService: predicted_blue = random.choice(common_blues) else: predicted_blue = random.randint(1, 16) + else: + # 大乐透后区号码分析 + back1 = [draw.back_ball_1 for draw in recent_draws] + back2 = [draw.back_ball_2 for draw in recent_draws] + from collections import Counter + c1 = Counter(back1) + c2 = Counter(back2) + # 取出现频率适中的号码 + + def pick_blues(counter): + items = counter.most_common() + if not items: + return random.randint(1, 12) + mid = len(items) // 2 + return items[mid][0] + b1 = pick_blues(c1) + b2 = pick_blues(c2) + if b1 == b2: + b2 = random.choice([i for i in range(1, 13) if i != b1]) + predicted_blues = sorted([b1, b2]) return { "success": True, @@ -389,7 +410,8 @@ class PredictionService: "consecutive_count": most_common_patterns['consecutive_count'][0][0] if most_common_patterns['consecutive_count'] else 1 }, "predicted_numbers": best_numbers, - "predicted_blue": predicted_blue if lottery_type == 'ssq' else None + "predicted_blue": predicted_blue if lottery_type == 'ssq' else None, + "predicted_blues": predicted_blues if lottery_type == 'dlt' else None } def get_ensemble_prediction(self, lottery_type: str, periods: int = 100) -> Dict: @@ -523,7 +545,7 @@ class PredictionService: 'method': '模式分析', 'numbers': pattern_result['predicted_numbers'], 'blue': pattern_result['predicted_blue'] if lottery_type == 'ssq' else None, - 'blues': pattern_result['predicted_blues'] if lottery_type == 'dlt' else None, + 'blues': pattern_result.get('predicted_blues') if lottery_type == 'dlt' else None, 'confidence': '中' }) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4192de0..11e1044 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -39,14 +39,15 @@ diff --git a/frontend/src/api/lottery.js b/frontend/src/api/lottery.js index c319291..2b523d0 100644 --- a/frontend/src/api/lottery.js +++ b/frontend/src/api/lottery.js @@ -152,5 +152,18 @@ export const lotteryApi = { } throw new Error('返回数据格式错误') }) + }, + + // 更新彩票数据 + updateSSQData() { + return api.post('/update/ssq') + }, + + updateDLTData() { + return api.post('/update/dlt') + }, + + updateAllLotteryData() { + return api.post('/update/all') } } \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index df869bf..a178c2d 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -1,5 +1,55 @@