From 52bee5f2e5ddcdafc383778e82976eabd41abfeb Mon Sep 17 00:00:00 2001 From: bizwings Date: Thu, 9 Jul 2026 07:56:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(BIZ-87):=20=E4=BF=AE=E5=A4=8D=E4=B8=AD?= =?UTF-8?q?=E5=A5=96=E6=AF=94=E5=AF=B9=E7=AE=97=E6=B3=95=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=8C=89=E7=94=9F=E6=88=90=E6=97=B6=E9=97=B4=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E6=9C=9F=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:所有生成记录都在比对最新一期开奖,而非根据生成时间匹配对应期号。 修复: - 新增 find_draw_by_gen_time() 函数,采用窗口归属模型 - 每个开奖日 D 的归属窗口为 (D_prev 20:00, D 20:00] - 生成时间落在哪个窗口内 → 与该开奖日 D 比对 - 规则 2.1:生成时间 < 开奖日 20:00 → 与当日开奖比对 - 规则 2.2:生成时间 ≥ 开奖日 20:00 → 与下次开奖日比对 - 超过所有开奖日 → 返回 waiting 状态 修改 api_compare_record() 使用新函数替换 load_latest_draw + find_next_draw_after 逻辑 测试验证: - gen 6/29 10:00 → 6/30 2026074 ✅ - gen 6/28 19:00 → 6/28 2026073 ✅ - gen 6/28 21:00 → 6/30 2026074 ✅ - gen 7/4 18:22 → waiting(7/5 未开奖)✅ Co-authored-by: multica-agent --- app.py | 141 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 40 deletions(-) diff --git a/app.py b/app.py index 6273fc9..efff22c 100644 --- a/app.py +++ b/app.py @@ -201,6 +201,86 @@ def find_next_draw_after(date_obj): traceback.print_exc() return None + +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) + - 如果 T 在 D 之前某日 → 仍归属 D(窗口归属) + - 如果 T 同日且 T ≥ 20:00 → 归属下一个开奖日(规则 2.2) + - 如果 T 超过所有开奖日的 20:00 → 等待下一期 + + 返回 (draw_dict, compare_type) 或 (None, 'waiting') + """ + try: + df = load_history_dataframe() + if df is None or df.empty: + return None, 'no_data' + + df['_dt'] = pd.to_datetime(df['开奖时间'], errors='coerce') + df = df.dropna(subset=['_dt']).sort_values('_dt', ascending=True).reset_index(drop=True) + + if df.empty: + return None, 'no_data' + + def row_to_draw(row): + 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 ['开奖时间', '期数', '号码', '和值特征', '奇偶比', '大小比', '跨度']} + } + + # 遍历所有开奖日(升序),找到第一个 D 使得 gen_time ≤ D 20:00 + for i in range(len(df)): + draw_date = df.iloc[i]['_dt'] + draw_8pm = draw_date.replace(hour=20, minute=0, second=0, microsecond=0) + + if gen_time <= draw_8pm: + # gen_time 落在 D 的窗口内 → 与 D 比对 + draw = row_to_draw(df.iloc[i]) + + # 判断是同日还是窗口归属 + gen_date_only = gen_time.date() + draw_date_only = draw_date.date() + + if gen_date_only == draw_date_only and gen_time < draw_8pm: + compare_type = 'current' # 同日,20:00 前 + elif gen_date_only == draw_date_only and gen_time >= draw_8pm: + # 同日 20:00 后 → 应该归属下一期(但上面条件已判断 ≤,所以这种情况不会进到这里) + # 这个分支在逻辑上不会到达,但保留防御性代码 + continue + else: + compare_type = 'current' # 窗口归属(生成时间在 D 之前某个时间点,归属 D) + + return draw, compare_type + + # gen_time 超过所有开奖日的 20:00 → 等待下一期 + last_draw = row_to_draw(df.iloc[-1]) + return last_draw, 'waiting' + except Exception as e: + traceback.print_exc() + return None, 'error' + # ============================================================ # 认证装饰器(可选) # ============================================================ @@ -510,48 +590,29 @@ def api_compare_record(record_id): if not tickets: return jsonify({'success': False, 'error': '无号码数据可比对'}), 400 - # 加载历史开奖数据,找最近一期 - latest_draw = load_latest_draw() - if not latest_draw or not latest_draw.get('reds'): + # 根据生成时间找到归属的开奖期 + # 规则:生成时间 < 开奖日 20:00 → 与当日开奖比对;>= 20:00 → 与下次开奖日比对 + draw_to_compare, compare_type = find_draw_by_gen_time(gen_time) + + if draw_to_compare is None: return jsonify({'success': False, 'error': '无法加载开奖数据'}), 500 - # 确定比对的开奖期 - # 规则:生成时间 < 开奖日 20:00 → 与当日开奖比对;>= 20:00 → 与下次开奖日比对 - draw_to_compare = None - compare_type = '' + if not draw_to_compare.get('reds'): + return jsonify({'success': False, 'error': '开奖数据号码解析失败'}), 500 - latest_date = latest_draw.get('date_obj') - if latest_date: - # 当日 20:00 的时间点 - draw_day_8pm = latest_date.replace(hour=20, minute=0, second=0, microsecond=0) - - if gen_time < draw_day_8pm: - # 生成时间在开奖日 20:00 之前 → 与当日(最近一期)比对 - draw_to_compare = latest_draw - compare_type = 'current' - else: - # 生成时间在开奖日 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' + if compare_type == 'waiting': + # 生成时间超过所有已记录开奖日 → 等待下一期 + return jsonify({ + 'success': True, + 'data': { + 'status': 'waiting', + 'message': f'生成时间 {gen_time_str} 超过最近开奖日 {draw_to_compare["date"]} 20:00,下次开奖尚未举行,请等待开奖后再次比对。', + 'gen_time': gen_time_str, + 'latest_draw_date': draw_to_compare['date'], + 'latest_draw_period': draw_to_compare['period'], + 'compare_rule': '生成时间 ≥ 开奖日 20:00 → 等待下次开奖日比对' + } + }) # 执行比对 draw_reds = set(draw_to_compare['reds']) @@ -607,7 +668,7 @@ def api_compare_record(record_id): 'status': 'compared', 'gen_time': gen_time_str, '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': { 'date': draw_to_compare['date'], 'period': draw_to_compare['period'], From 93295db357b10c16c52622a3e47ab6b55feea26d Mon Sep 17 00:00:00 2001 From: bizwings Date: Thu, 9 Jul 2026 08:23:21 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(BIZ-87):=20P2/P3=20=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3=E2=80=94=E2=80=94=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2-1: 删除死代码 load_latest_draw() 和 find_next_draw_after() - 两个函数在修复后已无任何调用点,删除避免误导维护者 P2-2: 抽取 row_to_draw 为模块级 _row_to_draw() 公共函数 - 消除三处重复代码,统一调用入口 P2-3: compare_type 细化为 same_day / cross_day / waiting - 原 'current' 语义模糊,现区分同日 vs 跨日归属 P2-4: 边界条件修复 <= → < - 严格对齐规则 2.2:生成时间 ≥ 20:00 归属下一期 - 恰好 20:00:00 现在正确归属下一期 P3: 删除不可达的 elif 死分支 - 移除注释已说明不会到达的防御性代码 验证:8 个测试用例全部通过(含 20:00:00 边界) Co-authored-by: multica-agent --- app.py | 163 ++++++++++++++------------------------------------------- 1 file changed, 38 insertions(+), 125 deletions(-) diff --git a/app.py b/app.py index efff22c..407cc3c 100644 --- a/app.py +++ b/app.py @@ -117,104 +117,47 @@ def load_history_dataframe(): return _load_history(CONFIG['history_file']) -def load_latest_draw(): - """加载最近一期开奖数据,返回 dict 或 None。""" +def _row_to_draw(row): + """将 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: - df = load_history_dataframe() - if df is None or df.empty: - return None - - # 解析日期,按开奖时间降序找最近一期 - df['_dt'] = pd.to_datetime(df['开奖时间'], errors='coerce') - df = df.sort_values('_dt', ascending=False).reset_index(drop=True) - - if df.empty: - return None - - row = df.iloc[0] - s = str(row.get('号码', '')).strip() - reds, blue = parse_number_string(s) - - draw_date_str = str(row.get('开奖时间', '')).strip() - draw_date = None + draw_date = datetime.strptime(draw_date_str[:10], '%Y-%m-%d') + except (ValueError, TypeError): 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 - - -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 + 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] + - 每个开奖日 D 有一个归属窗口 (D_prev 20:00, D 20:00) - 生成时间 T 落在哪个窗口内 → 与该开奖日 D 比对 - - 简化实现:按开奖日升序遍历,找到第一个 D 使得 T ≤ D 20:00 - - 如果 T 同日且 T < 20:00 → 同日比对(规则 2.1) - - 如果 T 在 D 之前某日 → 仍归属 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 → 等待下一期 + - 如果 T 超过所有开奖日的 20:00 → 等待下一期,compare_type='waiting' - 返回 (draw_dict, compare_type) 或 (None, 'waiting') + 返回 (draw_dict, compare_type) 或 (None, 'error') """ try: df = load_history_dataframe() @@ -227,55 +170,25 @@ def find_draw_by_gen_time(gen_time): if df.empty: return None, 'no_data' - def row_to_draw(row): - 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 ['开奖时间', '期数', '号码', '和值特征', '奇偶比', '大小比', '跨度']} - } - - # 遍历所有开奖日(升序),找到第一个 D 使得 gen_time ≤ D 20:00 + # 遍历所有开奖日(升序),找到第一个 D 使得 gen_time < D 20:00 for i in range(len(df)): draw_date = df.iloc[i]['_dt'] draw_8pm = draw_date.replace(hour=20, minute=0, second=0, microsecond=0) - if gen_time <= draw_8pm: + if gen_time < draw_8pm: # gen_time 落在 D 的窗口内 → 与 D 比对 - draw = row_to_draw(df.iloc[i]) + draw = _row_to_draw(df.iloc[i]) - # 判断是同日还是窗口归属 - gen_date_only = gen_time.date() - draw_date_only = draw_date.date() - - if gen_date_only == draw_date_only and gen_time < draw_8pm: - compare_type = 'current' # 同日,20:00 前 - elif gen_date_only == draw_date_only and gen_time >= draw_8pm: - # 同日 20:00 后 → 应该归属下一期(但上面条件已判断 ≤,所以这种情况不会进到这里) - # 这个分支在逻辑上不会到达,但保留防御性代码 - continue + # 判断是同日还是跨日窗口归属 + if gen_time.date() == draw_date.date(): + compare_type = 'same_day' # 同日,20:00 前 else: - compare_type = 'current' # 窗口归属(生成时间在 D 之前某个时间点,归属 D) + compare_type = 'cross_day' # 跨日窗口归属 return draw, compare_type # gen_time 超过所有开奖日的 20:00 → 等待下一期 - last_draw = row_to_draw(df.iloc[-1]) + last_draw = _row_to_draw(df.iloc[-1]) return last_draw, 'waiting' except Exception as e: traceback.print_exc()