# 双色球自动化系统 — 架构设计文档
| 字段 | 值 |
|------|-----|
| 文档编号 | 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
├──