merge: BIZ-87 中奖比对算法修复 + P2/P3 迭代

修复中奖比对算法(BIZ-87):
- 新增 find_draw_by_gen_time() 窗口归属模型
- 修复所有生成记录都比对最新一期的 bug

P2/P3 代码质量改进:
- 删除死代码 load_latest_draw / find_next_draw_after
- 抽取 _row_to_draw 公共函数
- compare_type 细化为 same_day / cross_day / waiting
- 边界条件 <= 改为 < 严格对齐规则 2.2
- 删除不可达 elif 死分支

审核通过:架构/产品/COO/运维 四方审核 
This commit is contained in:
2026-07-09 08:33:40 +08:00
+85 -111
View File
@@ -117,89 +117,82 @@ def load_history_dataframe():
return _load_history(CONFIG['history_file']) return _load_history(CONFIG['history_file'])
def load_latest_draw(): def _row_to_draw(row):
"""加载最近一期开奖数据,返回 dict 或 None。""" """将 DataFrame 行转换为开奖数据 dict(公共函数)。
供 find_draw_by_gen_time 及其他需要解析开奖记录的函数统一调用。
"""
s = str(row.get('号码', '')).strip()
reds, blue = parse_number_string(s)
draw_date_str = str(row.get('开奖时间', '')).strip()
draw_date = None
try:
draw_date = datetime.strptime(draw_date_str[:10], '%Y-%m-%d')
except (ValueError, TypeError):
try:
dt = pd.to_datetime(draw_date_str, errors='coerce')
if pd.notna(dt):
draw_date = dt.to_pydatetime()
except Exception:
pass
return {
'date': draw_date_str,
'date_obj': draw_date,
'period': str(row.get('期数', '')).strip(),
'reds': reds,
'blue': blue,
'raw': {k: str(row.get(k, '')).strip() for k in ['开奖时间', '期数', '号码', '和值特征', '奇偶比', '大小比', '跨度']}
}
def find_draw_by_gen_time(gen_time):
"""根据生成时间找到归属的开奖期。
比对规则(窗口归属模型):
- 每个开奖日 D 有一个归属窗口 (D_prev 20:00, D 20:00)
- 生成时间 T 落在哪个窗口内 → 与该开奖日 D 比对
- 按开奖日升序遍历,找到第一个 D 使得 T < D 20:00
- 如果 T 同日且 T < 20:00 → 同日比对(规则 2.1),compare_type='same_day'
- 如果 T 在 D 之前某日 → 仍归属 D(窗口归属),compare_type='cross_day'
- 如果 T 同日且 T ≥ 20:00 → 归属下一个开奖日(规则 2.2)
- 如果 T 超过所有开奖日的 20:00 → 等待下一期,compare_type='waiting'
返回 (draw_dict, compare_type) 或 (None, 'error')
"""
try: try:
df = load_history_dataframe() df = load_history_dataframe()
if df is None or df.empty: if df is None or df.empty:
return None return None, 'no_data'
# 解析日期,按开奖时间降序找最近一期
df['_dt'] = pd.to_datetime(df['开奖时间'], errors='coerce') df['_dt'] = pd.to_datetime(df['开奖时间'], errors='coerce')
df = df.sort_values('_dt', ascending=False).reset_index(drop=True) df = df.dropna(subset=['_dt']).sort_values('_dt', ascending=True).reset_index(drop=True)
if df.empty: if df.empty:
return None return None, 'no_data'
row = df.iloc[0] # 遍历所有开奖日(升序),找到第一个 D 使得 gen_time < D 20:00
s = str(row.get('号码', '')).strip() for i in range(len(df)):
reds, blue = parse_number_string(s) draw_date = df.iloc[i]['_dt']
draw_8pm = draw_date.replace(hour=20, minute=0, second=0, microsecond=0)
draw_date_str = str(row.get('开奖时间', '')).strip() if gen_time < draw_8pm:
draw_date = None # gen_time 落在 D 的窗口内 → 与 D 比对
try: draw = _row_to_draw(df.iloc[i])
draw_date = datetime.strptime(draw_date_str[:10], '%Y-%m-%d')
except (ValueError, TypeError):
try:
dt = pd.to_datetime(draw_date_str, errors='coerce')
if pd.notna(dt):
draw_date = dt.to_pydatetime()
except Exception:
pass
return { # 判断是同日还是跨日窗口归属
'date': draw_date_str, if gen_time.date() == draw_date.date():
'date_obj': draw_date, compare_type = 'same_day' # 同日,20:00 前
'period': str(row.get('期数', '')).strip(), else:
'reds': reds, compare_type = 'cross_day' # 跨日窗口归属
'blue': blue,
'raw': {k: str(row.get(k, '')).strip() for k in ['开奖时间', '期数', '号码', '和值特征', '奇偶比', '大小比', '跨度']} return draw, compare_type
}
# gen_time 超过所有开奖日的 20:00 → 等待下一期
last_draw = _row_to_draw(df.iloc[-1])
return last_draw, 'waiting'
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return None return None, 'error'
def find_next_draw_after(date_obj):
"""查找给定日期之后最近一期的开奖数据,返回 dict 或 None。"""
try:
df = load_history_dataframe()
if df is None or df.empty:
return None
df['_dt'] = pd.to_datetime(df['开奖时间'], errors='coerce')
# 找开奖日期 > date_obj 的最近一期
future = df[df['_dt'] > date_obj].sort_values('_dt', ascending=True)
if future.empty:
return None
row = future.iloc[0]
s = str(row.get('号码', '')).strip()
reds, blue = parse_number_string(s)
draw_date_str = str(row.get('开奖时间', '')).strip()
draw_date = None
try:
draw_date = datetime.strptime(draw_date_str[:10], '%Y-%m-%d')
except (ValueError, TypeError):
try:
dt = pd.to_datetime(draw_date_str, errors='coerce')
if pd.notna(dt):
draw_date = dt.to_pydatetime()
except Exception:
pass
return {
'date': draw_date_str,
'date_obj': draw_date,
'period': str(row.get('期数', '')).strip(),
'reds': reds,
'blue': blue,
'raw': {k: str(row.get(k, '')).strip() for k in ['开奖时间', '期数', '号码', '和值特征', '奇偶比', '大小比', '跨度']}
}
except Exception as e:
traceback.print_exc()
return None
# ============================================================ # ============================================================
# 认证装饰器(可选) # 认证装饰器(可选)
@@ -510,48 +503,29 @@ def api_compare_record(record_id):
if not tickets: if not tickets:
return jsonify({'success': False, 'error': '无号码数据可比对'}), 400 return jsonify({'success': False, 'error': '无号码数据可比对'}), 400
# 加载历史开奖数据,找最近一 # 根据生成时间找到归属的开奖
latest_draw = load_latest_draw() # 规则:生成时间 < 开奖日 20:00 → 与当日开奖比对;>= 20:00 → 与下次开奖日比对
if not latest_draw or not latest_draw.get('reds'): draw_to_compare, compare_type = find_draw_by_gen_time(gen_time)
if draw_to_compare is None:
return jsonify({'success': False, 'error': '无法加载开奖数据'}), 500 return jsonify({'success': False, 'error': '无法加载开奖数据'}), 500
# 确定比对的开奖期 if not draw_to_compare.get('reds'):
# 规则:生成时间 < 开奖日 20:00 → 与当日开奖比对;>= 20:00 → 与下次开奖日比对 return jsonify({'success': False, 'error': '开奖数据号码解析失败'}), 500
draw_to_compare = None
compare_type = ''
latest_date = latest_draw.get('date_obj') if compare_type == 'waiting':
if latest_date: # 生成时间超过所有已记录开奖日 → 等待下一期
# 当日 20:00 的时间点 return jsonify({
draw_day_8pm = latest_date.replace(hour=20, minute=0, second=0, microsecond=0) 'success': True,
'data': {
if gen_time < draw_day_8pm: 'status': 'waiting',
# 生成时间在开奖日 20:00 之前 → 与当日(最近一期)比对 'message': f'生成时间 {gen_time_str} 超过最近开奖日 {draw_to_compare["date"]} 20:00,下次开奖尚未举行,请等待开奖后再次比对。',
draw_to_compare = latest_draw 'gen_time': gen_time_str,
compare_type = 'current' 'latest_draw_date': draw_to_compare['date'],
else: 'latest_draw_period': draw_to_compare['period'],
# 生成时间开奖日 20:00 之后 → 与下次开奖日比对 'compare_rule': '生成时间开奖日 20:00 → 等待下次开奖日比对'
next_draw = find_next_draw_after(latest_date) }
if next_draw: })
draw_to_compare = next_draw
compare_type = 'next'
else:
# 暂无下次开奖数据,返回等待状态
return jsonify({
'success': True,
'data': {
'status': 'waiting',
'message': f'生成时间 {gen_time_str} 在最近开奖日 {latest_draw["date"]} 20:00 之后,下次开奖尚未举行,请等待开奖后再次比对。',
'gen_time': gen_time_str,
'latest_draw_date': latest_draw['date'],
'latest_draw_period': latest_draw['period'],
'compare_rule': '生成时间 ≥ 开奖日 20:00 → 等待下次开奖日比对'
}
})
else:
# 无法解析开奖日期,直接用最近一期
draw_to_compare = latest_draw
compare_type = 'current_fallback'
# 执行比对 # 执行比对
draw_reds = set(draw_to_compare['reds']) draw_reds = set(draw_to_compare['reds'])
@@ -607,7 +581,7 @@ def api_compare_record(record_id):
'status': 'compared', 'status': 'compared',
'gen_time': gen_time_str, 'gen_time': gen_time_str,
'compare_type': compare_type, 'compare_type': compare_type,
'compare_rule': '生成时间 < 开奖日 20:00 → 与当日开奖比对' if compare_type == 'current' else '生成时间 ≥ 开奖日 20:00 → 与下次开奖日比对', 'compare_rule': f'生成时间 {gen_time_str} 归属开奖期 {draw_to_compare["period"]}{draw_to_compare["date"]}',
'draw': { 'draw': {
'date': draw_to_compare['date'], 'date': draw_to_compare['date'],
'period': draw_to_compare['period'], 'period': draw_to_compare['period'],