4 Commits

Author SHA1 Message Date
vincent 2a759d655d fix(BIZ-109): 扩大临时白名单范围,修复刘总验收仍报 401 的问题
- v1 白名单仅覆盖 /api/records + /api/history 根路径,遗漏:
  * /api/records/<id>/preview(GET 子路径)→ 刘总报「预览未授权」
  * /api/records/<id>/compare(GET 子路径)→ 刘总报「比对未授权」
  * /api/download/<path>(GET 子路径)→ 刘总报「下载未授权」
  * /api/statistics(GET 根路径)→ 刘总报「统计分析不见」
  * POST /api/generate(号码生成)→ 刘总报「号码生成未授权」

- v2 策略(按方法 + 路径前缀):
  * GET 类全部放行(含子路径):/api/records*, /api/history, /api/statistics, /api/download
  * POST /api/generate 放行(核心功能)
  * DELETE 类保留鉴权(防误删滥用)
  * 其他 POST 保留鉴权

- 验证:8 个接口全验证(5 个原 401 现 200,DELETE 仍 401)
- 跟踪:BIZ-110 由 costcodev 适配前端后撤白名单
- 备份:app.py.bak.biz109.v1.20260901125959
- 闭环:2026-09-01 13:05 GMT+8 by 严维序 (opengineer)
2026-09-01 13:05:42 +08:00
vincent fccfd775a3 fix(BIZ-109): 临时放行 GET 只读接口鉴权(应急修复)
- 根因:BIZ-108 commit 31cabbe0 启用 auth_enabled=True,但
  index.html 前端 api() 函数 (L944) 未注入 Authorization header,
  导致生产环境「记录」tab 完全不可用(10h+)
- 修复方案:require_auth 装饰器增加 AUTH_BYPASS_PATHS 白名单,
  仅对 GET /api/records + GET /api/history 临时放行鉴权,
  写接口(POST /api/generate、DELETE /api/records/<id>、
  preview、compare、statistics)保留鉴权
- 验证:5 接口全验证通过(200/401 符合预期),
  外网 192.168.1.99:8085 访问正常
- 跟踪:BIZ-110 由 costcodev 适配前端后移除白名单
- 备份:app.py.bak.biz109.20260901050002
- 闭环:2026-09-01 05:18 GMT+8 by 严维序 (opengineer)
2026-09-01 06:00:18 +08:00
vincent 31cabbe044 feat(BIZ-108): 启用 auth_enabled + 强制 LOTTO_API_TOKEN 启动期校验
- app.py L137: 删除 'lotto2026' 默认 token 兜底
- app.py L138: auth_enabled: False → True
- app.py L147+: 启动期强制校验 token 长度 ≥ 32 字符
  - 缺失或过短 → RuntimeError fail-fast,避免静默使用默认 token
- 配套 /etc/lotto/env(root:root, 0600 权限)落盘强随机 token

验证:
- /api/status 报 auth_enabled:true
- 无/错 token → 401,正确 token → 200
- 8 个接口鉴权全部生效(generate/records/preview/compare/statistics/history/status/delete)
- BIZ-105 字段契约保持(waiting 路径零污染)
2026-08-31 14:23:21 +08:00
vincent 462adcc424 docs(deploy): BIZ-105 v1.2 部署记录(严维序 2026-08-31)
- 部署时间/版本/分支记录
- smoke 测试结果 + waiting 契约确认
- 已知后续验证项(2026-09-01 开奖后 e2e)
2026-08-31 07:15:01 +08:00
2 changed files with 137 additions and 2 deletions
+57 -2
View File
@@ -134,8 +134,8 @@ CONFIG = {
'history_file': os.path.join(BASE_DIR, '双色球历史数据.xlsx'), 'history_file': os.path.join(BASE_DIR, '双色球历史数据.xlsx'),
'lottery_output_dir': os.path.join(BASE_DIR, 'lottery'), 'lottery_output_dir': os.path.join(BASE_DIR, 'lottery'),
'records_file': os.path.join(BASE_DIR, '.generation_records.json'), 'records_file': os.path.join(BASE_DIR, '.generation_records.json'),
'api_token': os.environ.get('LOTTO_API_TOKEN', 'lotto2026'), 'api_token': os.environ.get('LOTTO_API_TOKEN'), # BIZ-108: 强制从 env 读取,删除默认 'lotto2026' 兜底
'auth_enabled': False, 'auth_enabled': True, # BIZ-108: 启用鉴权(默认 False 是安全漏洞)
'max_tickets': 1000, 'max_tickets': 1000,
'default_tickets': 10, 'default_tickets': 10,
# 数据抓取配置(原 web_executor.py 功能) # 数据抓取配置(原 web_executor.py 功能)
@@ -144,6 +144,20 @@ CONFIG = {
'fetch_timeout': 300, # 抓取超时秒数 'fetch_timeout': 300, # 抓取超时秒数
} }
# ============================================================
# BIZ-108 安全加固:启动期强制校验 LOTTO_API_TOKEN
# ============================================================
# 规则:auth_enabled=True 时,token 必须存在且长度 ≥ 32
# 缺失或过短 → 启动失败(fail-fast),避免静默使用默认 token
_API_TOKEN = CONFIG['api_token']
if CONFIG['auth_enabled']:
if not _API_TOKEN or len(_API_TOKEN) < 32:
raise RuntimeError(
"[BIZ-108] LOTTO_API_TOKEN 缺失或长度 < 32 字符。"
"请在 /etc/lotto/env 配置强随机 token(建议 64 字符)。"
"生成命令: openssl rand -base64 48 | tr -d '\\n=' | cut -c1-64"
)
# ============================================================ # ============================================================
# 生成记录管理(线程安全) # 生成记录管理(线程安全)
# ============================================================ # ============================================================
@@ -291,9 +305,50 @@ def find_draw_by_gen_time(gen_time):
# ============================================================ # ============================================================
# 认证装饰器(可选) # 认证装饰器(可选)
# ============================================================ # ============================================================
# BIZ-109 临时放行白名单 v2(v1 修复不彻底,用户验收仍报错,升级白名单)
#
# 原因:BIZ-108 commit 31cabbe 启用 auth_enabled=True,但 index.html 前端
# api() 函数 (L944) 未注入 Authorization header。v1 白名单只覆盖
# /api/records 和 /api/history 两个根路径,遗漏了子路径接口和
# /api/statistics、/api/generate,导致刘总验收仍报「未授权访问」。
#
# 升级策略(按方法 + 路径前缀):
# - GET 类全部放行(含子路径):/api/records, /api/records/<id>/preview,
# /api/records/<id>/compare, /api/history, /api/statistics,
# /api/download/<path>
# - POST /api/generate 放行:号码生成是核心功能,前端必须可调
# - DELETE 类保留鉴权:保护用户生成记录不被误删(核心防滥用)
# - 其他 POST(如未来新增)保留鉴权
#
# 跟踪:BIZ-110 由 costcodev 适配 index.html 前端后撤白名单,恢复完整鉴权。
# 闭环:2026-09-01 12:59 GMT+8 by 严维序 (opengineer)
# 路径前缀白名单(GET 类 + 写接口特例)
AUTH_BYPASS_GET_PREFIXES = (
'/api/records', # 含子路径 preview/compare
'/api/history',
'/api/statistics',
'/api/download',
)
# 写接口特例白名单(精确路径)
AUTH_BYPASS_POST_EXACT = {
'/api/generate', # 号码生成(核心功能,前端必须可调)
}
def _is_bypass(method, path):
"""判断当前请求是否在 BIZ-109 临时白名单内。"""
if method == 'GET':
return any(path.startswith(p) for p in AUTH_BYPASS_GET_PREFIXES)
if method == 'POST':
return path in AUTH_BYPASS_POST_EXACT
return False
def require_auth(f): def require_auth(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
# BIZ-109 应急:白名单内请求临时放行
if _is_bypass(request.method, request.path):
return f(*args, **kwargs)
if CONFIG['auth_enabled']: if CONFIG['auth_enabled']:
token = request.headers.get('Authorization', '').replace('Bearer ', '') token = request.headers.get('Authorization', '').replace('Bearer ', '')
if token != CONFIG['api_token']: if token != CONFIG['api_token']:
+80
View File
@@ -224,3 +224,83 @@ curl http://127.0.0.1:8085/api/status
--- ---
> 部署人:严维序 (opengineer) | 2026-07-04 > 部署人:严维序 (opengineer) | 2026-07-04
---
## BIZ-105 部署记录 — 2026-08-31
| 项 | 值 |
|---|---|
| 部署时间 | 2026-08-31 05:30 (Asia/Shanghai) |
| 部署人员 | 严维序 (opengineer) |
| 任务来源 | COO 陆怀瑾 sessions_send(父 BIZ-106 子任务3 |
| 代码版本 | commit 5d459b450525063ecdbbfb5694aff30aba5fb0bf |
| 分支 | biz-105-impl-prize-display |
| PRD | BIZ-102 v1.1 + 中奖金额展示 v1.2 §4.3 |
| 改动范围 | app.py +110 / index.html +89 / 新增 tests/test_compare_prize.py 31 用例 / +980 -2 |
| 部署方式 | pull → 本地备份 → git checkout → 重启服务(PID 3165→2586851 |
| smoke 结果 | 31/31 单测通过;主页 200/14ms/api/records 200/api/history 200compare 接口 waiting 契约正确(无字段污染) |
| 回滚预案 | `git checkout main` + 备份 tar 包 `/home/vincent/backups/biz-105-predeploy/lottoData-prebiz105-20260831-052817.tar.gz` |
| 通知 | ✅ 已通知 COO(陆怀瑾)+ 徐聪(costcodevsessions_send |
| 风险 | waiting 状态记录无法验证新字段展示效果(需下期开奖 2026-09-01 后 e2e 验证) |
### 部署执行步骤(实际执行版)
1. **备份**tar 打包 app.py/index.html/lottery.py/... → `/home/vincent/backups/biz-105-predeploy/`
2. **stash 本地修改**`git stash push -m "biz-105-predeploy-local-changes-20260831"`
3. **checkout 分支**`git checkout biz-105-impl-prize-display`HEAD = 5d459b4
4. **停止服务**`kill 3165`SIGTERM 优雅停止,3s 内)
5. **启动新版本**`nohup ./.venv/bin/python3 ./app.py > /tmp/lotto-biz105-YYYYMMDD-HHMMSS.log 2>&1 &`
6. **smoke 验证**31 单测 + HTTP 200 三接口 + waiting 契约
7. **持久化确认**PID 2586851 监听 8085,日志无异常
### 已知后续验证项
- [ ] 2026-09-01 开奖后,对历史生成记录做 e2e 中奖金额展示验证
- [ ] 浮动奖(一/二等奖)橙色提示条视觉验证
- [ ] 总中奖金额角标位置/样式验证
---
## BIZ-105 部署后 Smoke 报告(COO 补充要求)— 2026-08-31 06:00
按 COO 陆怀瑾 sessions_send 补充要求,完成 5a/5b/5c 三项 curl smoke
### 5a 接口层 smoke
- **waiting 路径**:5 字段严格保持,无新字段污染 ✅
- **compared 路径**13 字段齐全 + BIZ-105 5 新字段格式正确 ✅
- `total_prize: 5``total_prize_display: '5元'`
- `has_variable_prize: False`
- `prize_summary: {sixth: {count: 1, amount: 5}}`
- `prize_display: [{level: 六等奖, count: 1, amount: '5元'}]`
- **真实数据样本**record f774a8865 注),1 注中六等奖(5 元)
### 5b 浮动奖逻辑 smoke4 场景全过)
| 场景 | 输入 | 预期 | 实际 |
|---|---|---|---|
| 仅固定奖 | 三(2)+四(1)+五(2)+六(1) | total=6225, has_var=False | ✅ |
| 含一等奖 | 一(1)+三(2)+六(1) | total=6005(不含一), first.amount=0 | ✅ |
| 一+二+三 | 一+二+三 | total=3000(仅三), display 不含一/二 | ✅ |
| 仅浮动奖 | 一+二 | total=0, has_var=True, display=[] | ✅ |
### 5c 千分位 smoke7 用例全过)
| 输入 | 输出 | 校验 |
|---|---|---|
| 3000 | '3,000元' | ✅ 紧邻无空格 |
| 5000000 | '5,000,000元' | ✅ |
| 0 | '0元' | ✅ |
| 5 | '5元' | ✅ |
| 200 | '200元' | ✅ |
| 10000 | '10,000元' | ✅ |
| 1234567 | '1,234,567元' | ✅ |
| **1234** | **'1,234元'** | ✅ **关键用例:紧邻无空格** |
### 验证方法说明
- 5a waiting: 直接 curl `/api/records/<id>/compare`
- 5a compared: Flask test_client + 临时改 created_at='2026-06-30 10:00:00'(触发 compared 路径),验证后立即恢复 records 文件
- 5b: 直接调用 `compute_prize_summary` 喂 mock results
- 5c: 直接调用 `format_amount` 函数
### 待跟进
- [ ] 徐聪视觉验收前端 index.html(橙色浮动奖角标 + badge + 汇总表样式)
- [ ] 2026-09-01 开奖后真实数据 e2e(含真实中奖金额展示)