diff --git a/docs/architecture/Lotto-Architecture-v1.md b/docs/architecture/Lotto-Architecture-v1.md new file mode 100644 index 0000000..6ce828e --- /dev/null +++ b/docs/architecture/Lotto-Architecture-v1.md @@ -0,0 +1,1225 @@ +# 双色球自动化系统 — 架构设计文档 + +| 字段 | 值 | +|------|-----| +| 文档编号 | ADR-LOTTO-001 | +| 版本 | v1.0 | +| 创建日期 | 2026-07-03 | +| 架构师 | 梁思筑(Serena) | +| 评审状态 | 待评审 | +| 关联 PRD | PRD-双色球 WebUI-v1.0(沈路明) | +| 关联 Issue | BIZ-74 / 父任务 BIZ-68 | + +--- + +## 1. 系统概述 + +### 1.1 业务定位 + +双色球自动化系统是一套面向内部用户的福利彩票双色球辅助工具,核心功能包括: +- **数据采集**:每日自动从 55128.cn 抓取双色球历史开奖数据 +- **号码生成**:基于历史数据分析(热冷号、奇偶比、大小比、和值、跨度)+ 随机策略生成推荐号码 +- **Web 展示**:提供响应式 Web UI,支持 PC + 移动端访问,局域网内可多人同时使用 + +### 1.2 用户场景 + +| 场景 | 角色 | 触发方式 | 预期结果 | +|------|------|----------|----------| +| 生成号码 | 刘总/团队成员 | Web UI 点击「生成」 | 3s 内返回号码列表 + 统计指标 | +| 查看历史 | 刘总/团队成员 | Web UI 切换 Tab | 分页查看历史开奖数据 | +| 下载结果 | 刘总/团队成员 | Web UI 点击「下载」 | 获取 Excel 文件 | +| 自动抓取 | 系统 | Cron 定时触发 | 凌晨 2:30 自动更新历史数据 | + +### 1.3 系统边界 + +- 数据来源:55128.cn(外部网站,仅读取) +- 用户终端:PC/移动端浏览器(内网访问) +- 数据存储:本地 Excel + JSON 文件 +- 服务进程:Flask 单进程 + threaded=True +- 部署环境:Ubuntu Server (192.168.1.99) + +## 2. 系统架构图 + +### 2.1 系统拓扑 + +``` +┌────────────────────────────────────────────────────┐ +│ 用户浏览器 │ +│ (PC / 移动端 / 微信内置) │ +│ http://192.168.1.99:8085 │ +└───────────────────────┬────────────────────────────┘ + │ HTTP (内网) + ▼ +┌────────────────────────────────────────────────────┐ +│ Flask Web 服务 (app.py) │ +│ 监听 0.0.0.0:8085 │ +│ ┌──────────┬──────────┬──────────┬──────────────┐ │ +│ │ /api/ │ /api/ │ /api/ │ /api/ │ │ +│ │ generate │ history │ records │ statistics │ │ +│ ├──────────┼──────────┼──────────┼──────────────┤ │ +│ │ /api/ │ /api/ │ /api/ │ / │ │ +│ │ download │ status │ config │ (index.html) │ │ +│ └──────────┴──────────┴──────────┴──────────────┘ │ +└───────┬────────────────────┬───────────────────────┘ + │ │ + ▼ ▼ +┌───────────────┐ ┌──────────────────┐ +│ lottery.py │ │ fetch_data.py │ +│ 号码生成核心 │ │ 数据抓取脚本 │ +│ (NumPy+Pandas)│ │ (requests+BS4) │ +└───────┬───────┘ └────────┬─────────┘ + │ │ + ▼ ▼ +┌──────────────────────────────────────┐ +│ 本地文件系统 │ +│ ┌─────────────┬──────────────────┐ │ +│ │双色球历史数据 │.generation_ │ │ +│ │ .xlsx │records.json │ │ +│ └─────────────┴──────────────────┘ │ +│ ┌──────────────────────────────────┐│ +│ │ lottery/ (生成结果 Excel 目录) ││ +│ └──────────────────────────────────┘│ +└──────────────────────────────────────┘ +``` + +### 2.2 数据流 + +``` +数据抓取流: + 55128.cn → HTTP GET → fetch_data.py → BeautifulSoup 解析 → Excel 写入 + +号码生成流: + 双色球历史数据.xlsx → lottery.py → 统计分析 → 策略调整 → Excel 输出 → app.py JSON 返回 + +前端数据流: + index.html → fetch() → app.py API → JSON → DOM 渲染 + +记录管理流: + 生成 → add_record() → .generation_records.json + 删除 → remove file + update JSON +``` + +### 2.3 模块依赖图 + +```mermaid +graph TD + A[index.html
前端 UI] -->|HTTP API| B[app.py
Flask 后端] + B -->|import| C[lottery.py
号码生成器] + B -->|read/write| D[.generation_records.json] + B -->|read| E[双色球历史数据.xlsx] + C -->|read| E + C -->|write| F[lottery/*.xlsx] + B -->|send_file| F + G[fetch_data.py
数据抓取] -->|HTTP GET| H[55128.cn] + G -->|write| E + I[web_executor.py
抓取控制台] -->|subprocess| G + K[Cron
fetch_daily.sh] -->|exec| G + L[web_console.html] -->|HTTP API| I +``` + +## 3. 技术选型对比 + +### 3.1 后端框架 + +| 维度 | Flask (当前选择) | FastAPI | Django | +|------|:-:|:-:|:-:| +| 学习成本 | ★★★★★ 团队熟悉 | ★★★★ 需学异步 | ★★★ 概念重 | +| 异步支持 | ★★ 需 threaded | ★★★★★ 原生 async | ★★ 有限 | +| 依赖体积 | ★★★★★ 轻量 | ★★★★ 较轻 | ★★ 笨重 | +| 适合度 | ✅ 项目规模小、路由少 | 未来升级 | 过度设计 | + +**决策**:维持 Flask。现有代码已基于 Flask 开发且运行稳定;项目路由仅 8 个,无异步需求。 + +### 3.2 前端方案 + +| 维度 | 原生 HTML/CSS/JS (当前) | Vue 3 | React | +|------|:-:|:-:|:-:| +| 构建工具 | 无需 | 需 Vite | 需 Vite | +| 交互复杂度 | ★★ 满足当前 | ★★★★★ | ★★★★★ | +| 包体积 | 0 KB | ~30KB(gz) | ~40KB(gz) | +| 适合度 | ✅ 4 Tab + 表单 | 未来增强 | 过度设计 | + +**决策**:维持原生 HTML/CSS/JS。当前 UI 仅 4 个 Tab + 少量表单交互,引入框架不合理。 + +### 3.3 数据存储方案 + +| 维度 | Excel + JSON (当前) | SQLite | MySQL | +|------|:-:|:-:|:-:| +| 运维成本 | ★★★★★ 零运维 | ★★★★ 嵌入式 | ★★★ 独立部署 | +| 并发读写 | ★ 文件锁 | ★★★★ | ★★★★★ | +| 数据量需求 | <1MB | <1GB | 任意 | +| 适合度 | ✅ 120条/12KB | 可选升级 | 过度设计 | + +**决策**:维持 Excel + JSON。数据量极小(120 条记录、12KB),无并发写入场景。若未来 >10,000 条迁移 SQLite。 + +### 3.4 部署方式 + +| 维度 | systemd + Cron (当前) | Docker Compose | 裸进程 | +|------|:-:|:-:|:-:| +| 隔离性 | ★★ 系统级 | ★★★★★ 容器 | ★ 无 | +| 开机自启 | ★★★★★ systemd | ★★★★ restart:always | ★★ 手动 | +| 适合度 | ✅ 内网单服务 | 未来扩展 | 不推荐 | + +**决策**:维持 systemd + Cron。内网单服务,引入 Docker 增加不必要复杂度。 + +## 4. 模块详细设计(单文件/单函数级) + +### 4.1 app.py — Flask Web 主服务 + +**路径**: `/home/vincent/Studio/lottoData/app.py` +**职责**: 提供 Web UI 和 REST API,系统唯一入口 +**规模**: ~490 行 +**依赖**: Flask, lottery.py, pandas, openpyxl, numpy + +#### 4.1.1 模块配置 + +```python +CONFIG = { + 'host': '0.0.0.0', # 监听地址(局域网可访问) + 'port': 8085, # 端口 + 'history_file': '双色球历史数据.xlsx', + 'lottery_output_dir': 'lottery/', + 'records_file': '.generation_records.json', + 'api_token': 'lotto2026', # auth_enabled=True 时生效 + 'auth_enabled': False, # 认证开关 + 'max_tickets': 1000, # 单次生成上限 + 'default_tickets': 10, # 默认注数 +} +``` + +#### 4.1.2 函数清单 + +| 函数 | 输入 | 输出 | 依赖 | 说明 | +|------|------|------|------|------| +| `load_records()` | 无 | `list[dict]` | JSON 文件 | 加载生成记录 | +| `save_records(records)` | `list[dict]` | 无 | JSON 文件 | 保存生成记录 | +| `add_record(strategy, num_tickets, filename)` | str, int, str | `dict` | load/save | 添加一条记录 | +| `require_auth(f)` | Flask route | decorator | CONFIG | API 认证装饰器 | +| `api_generate()` | POST body | JSON | lottery.py | **核心**:生成号码 | +| `get_statistics_data()` | 无 | `dict` | pandas,numpy | 计算统计数据 | +| `api_statistics()` | 无 | JSON | get_statistics_data | GET 统计接口 | +| `api_records()` | query: page,page_size | JSON | load_records | 分页获取记录 | +| `api_delete_record(record_id)` | URL param | JSON | load/save_records | 删除记录+文件 | +| `api_download(filepath)` | URL param | File | send_file | 下载 Excel | +| `api_history()` | query: page,search | JSON | pandas | 分页历史数据 | +| `api_status()` | 无 | JSON | os | 系统状态 | +| `api_config()` | 无 | JSON | CONFIG | 前端配置 | +| `index()` | 无 | HTML | send_from_directory | 返回 index.html | + +#### 4.1.3 核心接口:api_generate() + +``` +输入: POST /api/generate + Body: {"num_tickets": int (1-1000), "strategy": "advanced"|"basic"} + +处理流程: + 1. 解析并校验请求参数(num_tickets 1-1000,strategy advanced/basic) + 2. 实例化 DoubleColorBallGenerator(history_file) + 3. generator.load_history_data() 加载历史数据 + 4. generator.generate_multiple_tickets(num_tickets, strategy) 生成号码 + 5. generator.save_to_excel() 保存结果文件 + 6. add_record() 记录生成历史 + 7. get_statistics_data() 获取统计信息 + 8. 构建 JSON 响应(前端最多展示 50 注) + +输出: { + success: true, + data: { + tickets: [{index, reds[6], blue, sum_value, odd_even, size_ratio, span}], + total: int, filename: str, download_url: str, record: dict, statistics: dict + } +} + +边界条件: + - num_tickets < 1 或 > 1000 → 400 + - strategy 非 advanced/basic → 400 + - 历史数据文件不存在 → 500 + - load_history_data() 返回 False → 500 + - tickets_df.empty → 500 + - save_to_excel 返回 None → 500 +``` + +#### 4.1.4 核心接口:get_statistics_data() + +``` +处理流程: + 1. 读取 Excel(header=None, iloc[1:] 跳过描述行) + 2. 解析红球(6个)+蓝球(1个)从拼接字符串(每2字符=1球) + 3. Counter 统计频次 → 排序取热冷号 + 4. 统计奇偶比/大小比频次 + 5. numpy 计算和值/跨度的 min/max/mean/std + +输出: { + hot_reds: [int...15], cold_reds: [int...15], + hot_blues: [int...8], + common_odd_even: "3:3", common_size_ratio: "4:2", + sum_range: {min, max, mean, std}, + span_range: {min, max, mean, std}, + history_count: int +} + +边界条件: + - Excel 不存在 → 返回 {} + - 数据行为空 → 返回 {} + - 号码字符串长度 <14 → 跳过该行 + - 号码超出范围 → 跳过 +``` + +#### 4.1.5 核心接口:api_history() + +``` +处理流程: + 1. 读取 Excel(header=None, iloc[1:]) + 2. 硬编码列名: ['开奖日期','期号','红球','开机号','和值特征','奇偶形态','大小比','奇偶形态2','跨度','其他'] + 3. 解析红球列表(前12字符每2位) + 蓝球(第13-14位) + 4. search 非空 → 全列字符串匹配过滤 + 5. 分页切片 → JSON + +⚠️ 已知问题: + - 每次请求都重新读取 Excel,无缓存(120 条可接受) + - 列名硬编码,Excel 格式变化需同步修改 + - 搜索全列匹配可能误匹配 +``` + +#### 4.1.6 核心接口:api_download(filepath) + +``` +安全检查: + 1. os.path.normpath(filepath) 规范化 + 2. 拒绝以 ../ 开头 → 403 + 3. 拒绝以 / 开头 → 403 + 4. 拼接 BASE_DIR + filepath,检查文件存在 + +⚠️ 安全建议(costcodev 实施时加固): + - 增加 os.path.realpath() 确保最终路径在 BASE_DIR 内 + - 当前 normpath 检查可被符号链接绕过 +``` + +### 4.2 lottery.py — 号码生成核心 + +**路径**: `/home/vincent/Studio/lottoData/lottery.py` +**职责**: 基于历史数据分析 + 随机策略生成双色球号码 +**规模**: ~1100 行 +**依赖**: pandas, numpy, openpyxl, datetime, collections.Counter, re + +#### 4.2.1 类定义:DoubleColorBallGenerator + +```python +class DoubleColorBallGenerator: + def __init__(self, history_file="双色球历史数据.xlsx", config=None): + # config 默认值: + # hot_red_count=15, cold_red_count=10, hot_blue_count=8 + # hot_blue_probability=0.7, max_adjustment_attempts=20 +``` + +#### 4.2.2 方法清单 + +| 方法 | 输入 | 输出 | 说明 | +|------|------|------|------| +| `__init__(history_file, config)` | str, dict | None | 初始化配置 | +| `load_history_data()` | 无 | bool | 加载 Excel + 统计计算 | +| `_calculate_statistics()` | 无 | None | 计算热冷号/奇偶/大小/和值/跨度 | +| `get_hot_red_balls(n=10)` | int | `list[int]` | 红球热号 TOP n | +| `get_cold_red_balls(n=10)` | int | `list[int]` | 红球冷号 TOP n | +| `get_hot_blue_balls(n=5)` | int | `list[int]` | 蓝球热号 TOP n | +| `parse_ratio(ratio_str)` | str | tuple | 解析比例字符串 | +| `_select_hot_cold_balls()` | 无 | tuple | 选择热冷号组合 | +| `_adjust_odd_even_ratio(red_balls)` | list | list | 调整奇偶比至 2:4~4:2 | +| `_adjust_size_ratio(red_balls)` | list | list | 调整大小比至 2:4~4:2 | +| `_adjust_sum_range(red_balls)` | list | list | 调整和值至 [90,120] | +| `_adjust_span_range(red_balls)` | list | list | 调整跨度至 [22,30] | +| `_select_blue_ball()` | 无 | int | 选蓝球(热号概率0.7) | +| `generate_single_ticket_advanced()` | 无 | list[6]+int | 高级策略生成单注 | +| `generate_single_ticket_basic()` | 无 | list[6]+int | 基础策略生成单注 | +| `generate_multiple_tickets(n, strategy)` | int, str | DataFrame | 批量生成号码 | +| `save_to_excel(df, n, strategy)` | df, int, str | str\|None | 保存为 Excel | +| `display_statistics()` | 无 | None | 控制台输出统计 | +| `run_tests()` | 无 | None | 内置测试 | +| `main()` | 无 | None | CLI 入口 | + +#### 4.2.3 关键算法:generate_single_ticket_advanced() + +``` +高级策略生成流程: + 1. _select_hot_cold_balls() → 从热号选3-4个 + 冷号选2-3个 = 6个 + 2. 排序红球 + 3. _adjust_odd_even_ratio() → 目标奇偶比 2:4~4:2,尝试上限20次 + 4. _adjust_size_ratio() → 目标大小比 2:4~4:2 + 5. _adjust_sum_range() → 目标和值 [90, 120] + 6. _adjust_span_range() → 目标跨度 [22, 30] + 7. 红球去重 + 范围校验 (1-33) + 8. _select_blue_ball() → 70%热号 + 30%随机,范围1-16 + 9. 返回 reds[6] + blue + +边界条件: + - 历史数据不足10条 → 警告但可生成 + - 调整超 max_adjustment_attempts → 接受当前结果 + - 红球不允许重复 +``` + +#### 4.2.4 关键算法:generate_multiple_tickets() + +``` +输入: num_tickets (1-1000), strategy ("advanced"|"basic") +处理: + 1. 循环 num_tickets 次 + 2. 每次调用 generate_single_ticket_advanced/basic() + 3. 计算每注统计指标(和值、奇偶比、大小比、跨度) + 4. 构建 pandas DataFrame +输出 DataFrame 列: 序号 | 红球1-6 | 蓝球 | 和值 | 奇偶比 | 大小比 | 跨度 + +边界条件: + - 单注生成失败 → 跳过并记日志 + - 全部失败 → 返回空 DataFrame + - 1000注耗时预估 5-8s +``` + +#### 4.2.5 关键算法:save_to_excel() + +``` +输入: tickets_df, num_tickets, strategy +处理: + 1. 文件名: 双色球_YYYYMMDD_HHMMSS_策略_N注.xlsx + 2. 保存到 lottery/ 目录(不存在自动创建) + 3. openpyxl 写入 +输出: 文件完整路径 or None(失败) +``` + +--- + +### 4.3 fetch_data.py — 数据抓取脚本 + +**路径**: `/home/vincent/Studio/lottoData/fetch_data.py` +**职责**: 从 55128.cn 抓取双色球历史开奖数据,更新本地 Excel +**规模**: ~130 行 +**依赖**: requests, beautifulsoup4, pandas, openpyxl + +#### 4.3.1 函数清单 + +| 函数 | 输入 | 输出 | 说明 | +|------|------|------|------| +| `fetch_lottery_data()` | 无 | `list[list]`\|None | HTTP GET + BS4 解析表格 | +| `save_to_excel(data_rows)` | `list[list]` | bool | DataFrame → Excel 覆盖写入 | +| `main()` | 无 | None | CLI 入口 | + +#### 4.3.2 fetch_lottery_data() 详细设计 + +``` +处理流程: + 1. requests.get(URL, headers=HEADERS, timeout=30) + 2. BeautifulSoup(response.text, "html.parser") + 3. soup.find("table") → 定位数据表格 + 4. 遍历 table.find_all("tr") → 解析每行 + 5. 提取:期号、日期、红球6个、蓝球、和值、奇偶比、大小比、跨度 + 6. 返回 data_rows + +边界条件: + - HTTP 超时(30s) → 返回 None + - 非 200 → raise_for_status() + - 表格不存在 → 返回 None + - 行数据不完整 → 跳过 + +数据源说明: + - URL: https://www.55128.cn/kjh/fcssq-history-120.htm + - 内容:最近 120 期开奖数据 + - 频率:每日 1 次(Cron 2:30) + - 无需登录、无 API 限流 +``` + +#### 4.3.3 save_to_excel() 详细设计 + +``` +处理: + 1. 构建 pandas DataFrame + 2. 覆盖写入 双色球历史数据.xlsx + +⚠️ 覆盖写入决策: + - data_rows 最多 120 行,数据量极小 + - 覆盖过程中服务短暂数据不可用(毫秒级,可接受) + - 替代方案"增量追加"复杂度高、收益低 +``` + +--- + +### 4.4 web_executor.py — 数据抓取 Web 控制台 + +**路径**: `/home/vincent/Studio/lottoData/web_executor.py` +**职责**: 提供 Web 界面手动触发数据抓取 +**规模**: ~220 行 +**依赖**: Flask, subprocess, threading + +#### 4.4.1 函数清单 + +| 函数 | 输入 | 输出 | 说明 | +|------|------|------|------| +| `load_status()` | 无 | dict | 从 .fetch_status.json 加载 | +| `save_status()` | 无 | None | 保存状态到文件 | +| `index()` | 无 | HTML | 返回 web_console.html | +| `api_status()` | 无 | JSON | 获取抓取状态 | +| `api_execute()` | 无 | JSON | 异步触发 fetch_data.py | +| `check_dependencies()` | 无 | JSON | 检查依赖 | + +#### 4.4.2 api_execute() 设计 + +``` +处理流程: + 1. 检查 is_running 状态锁 + 2. 已在运行 → 返回 409 + 3. 启动 threading.Thread → subprocess.Popen 执行 fetch_data.py + 4. 更新全局 execution_status + .fetch_status.json + +边界条件: + - 并发触发 → status_lock 保护,返回 409 + - subprocess 退出码非 0 → 记录 last_error + - ⚠️ 缺少 subprocess 超时(建议加 timeout=120s) +``` + +#### 4.4.3 与 app.py 的关系 + +``` +app.py (端口 8085) → 号码生成/历史/记录/统计 +web_executor.py (端口 5000) → 手动触发抓取 + +两个独立 Flask 服务,共享数据文件但端口不同。 +⚠️ 架构建议:长期应合并为单一服务,通过路由区分功能。 +``` + +--- + +### 4.5 index.html — 前端 UI + +**路径**: `/home/vincent/Studio/lottoData/index.html` +**职责**: 响应式 Web UI,4 个功能 Tab +**规模**: ~1170 行(HTML + CSS + JS 单文件) +**依赖**: 无外部 CDN + +#### 4.5.1 页面结构 + +``` +index.html +├──