# -*- coding: utf-8 -*- """ 导出 DWS_ASSISTANT_DAILY BUG 修复报告到 SYSTEM_LOG_ROOT。 用法:python scripts/ops/export_bug_report.py """ from __future__ import annotations import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from _env_paths import get_output_path REPORT = r"""# DWS_ASSISTANT_DAILY BUG 修复报告 > 生成时间:{now} > 执行 run_uuid:4ba9d2d365ee4a858f1c4104b1942dc2 > 执行开始:2026-02-21 15:29:20 --- ## 1. BUG 概述 ETL 执行过程中 `DWS_ASSISTANT_DAILY` 任务失败,根因是 `assistant_daily_task.py` 中 `_extract_trash_records` 方法的 SQL 引用了 `dwd.dwd_assistant_trash_event` 表中不存在的字段。 ### 错误信息 ``` psycopg2.errors.UndefinedColumn: 错误: 字段 "assistant_service_id" 不存在 LINE 3: assistant_service_id, ^ ``` ### 级联影响 `DWS_ASSISTANT_DAILY` 失败后,psycopg2 连接进入 `InFailedSqlTransaction` 状态, 级联导致以下 8 个任务全部失败: | # | 任务代码 | 失败原因 | |---|---------|---------| | 1 | DWS_ASSISTANT_DAILY | 根因:UndefinedColumn | | 2 | DWS_ASSISTANT_MONTHLY | InFailedSqlTransaction(级联) | | 3 | DWS_ASSISTANT_CUSTOMER | InFailedSqlTransaction(级联) | | 4 | DWS_ASSISTANT_SALARY | InFailedSqlTransaction(级联) | | 5 | DWS_ASSISTANT_FINANCE | InFailedSqlTransaction(级联) | | 6 | ODS_SETTLEMENT_RECORDS | InFailedSqlTransaction(级联) | | 7 | ODS_PAYMENT | InFailedSqlTransaction(级联) | | 8 | ODS_REFUND | InFailedSqlTransaction(级联) | | 9 | DWS_BUILD_ORDER_SUMMARY | InFailedSqlTransaction(级联) | 从 `ODS_TABLE_USE` 开始,task_executor 的连接恢复机制生效,后续任务恢复正常执行。 --- ## 2. 根因分析 ### 2.1 错误 SQL(修复前) ```sql SELECT assistant_service_id, trash_seconds, trash_reason, trash_time FROM dwd.dwd_assistant_trash_event WHERE site_id = %s AND DATE(trash_time) >= %s AND DATE(trash_time) <= %s ``` ### 2.2 `dwd_assistant_trash_event` 实际表结构 | 字段名 | 类型 | 说明 | |--------|------|------| | assistant_trash_event_id | BIGINT (PK) | 废除事件 ID | | site_id | BIGINT | 门店 ID | | table_id | BIGINT | 台桌 ID | | table_area_id | BIGINT | 区域 ID | | assistant_no | VARCHAR(32) | 助教编号 | | assistant_name | VARCHAR(64) | 助教姓名 | | charge_minutes_raw | INTEGER | 废除时长(分钟) | | abolish_amount | NUMERIC(18,2) | 废除金额 | | trash_reason | VARCHAR(255) | 废除原因 | | create_time | TIMESTAMPTZ | 废除时间 | | tenant_id | BIGINT | 租户 ID | ### 2.3 字段映射错误 | 错误引用 | 实际字段 | 说明 | |----------|---------|------| | `assistant_service_id` | `assistant_trash_event_id` | PK 名称不同 | | `trash_seconds` | `charge_minutes_raw` | 单位不同(分钟 vs 秒) | | `trash_time` | `create_time` | 字段名不同 | ### 2.4 深层设计缺陷 废除表 `dwd_assistant_trash_event` 没有 `assistant_service_id` 外键, 无法与服务记录表 `dwd_assistant_service_log` 做 1:1 关联。 原代码的 `_build_trash_index` 用 `assistant_service_id` 做 key 构建索引, `_aggregate_by_assistant_date` 用 `service_id in trash_index` 判断服务是否被废除。 即使 SQL 字段名修正后,这个匹配逻辑在设计上也是无效的——两个 ID 不同源。 --- ## 3. 修复方案 ### 3.1 文件 `apps/etl/connectors/feiqiu/tasks/dws/assistant_daily_task.py` ### 3.2 修改点(共 4 处) #### (1) `_extract_trash_records` — SQL 字段名修正 ```sql -- 修复后 SELECT assistant_trash_event_id, charge_minutes_raw * 60 AS trash_seconds, trash_reason, create_time AS trash_time, table_id, assistant_name FROM dwd.dwd_assistant_trash_event WHERE site_id = %s AND DATE(create_time) >= %s AND DATE(create_time) <= %s ``` #### (2) `_extract_service_records` — JOIN _ex 表取 is_trash ```sql -- 新增 LEFT JOIN 和 is_trash 字段 SELECT asl.assistant_service_id, ... DATE(asl.start_use_time) AS service_date, COALESCE(ex.is_trash, 0) AS is_trash FROM dwd.dwd_assistant_service_log asl LEFT JOIN dwd.dwd_assistant_service_log_ex ex ON asl.assistant_service_id = ex.assistant_service_id WHERE asl.site_id = %s AND DATE(asl.start_use_time) >= %s AND DATE(asl.start_use_time) <= %s AND asl.is_delete = 0 ``` #### (3) `_build_trash_index` — key 改为 assistant_trash_event_id ```python # 修复前 service_id = record.get('assistant_service_id') # 修复后 event_id = record.get('assistant_trash_event_id') ``` #### (4) `_aggregate_by_assistant_date` — 废除判断改用 is_trash ```python # 修复前 is_trashed = service_id in trash_index # 修复后 is_trashed = bool(record.get('is_trash', 0)) ``` 废除时长也从 `trash_index[service_id]` 改为直接用 `income_seconds`。 ### 3.3 设计决策说明 `dwd_assistant_service_log_ex` 表的 `is_trash` 字段来自上游 SaaS 系统的 `assistant_service_records` API,是服务记录级别的废除标记,比跨表匹配更可靠。 废除时长统计改用服务记录自身的 `income_seconds`(即该服务的计费时长), 而非从废除表取 `charge_minutes_raw`(废除事件的计费分钟数), 因为两者无法 1:1 关联。 --- ## 4. 验证计划 修复将在下次 ETL 执行时生效。验证步骤: 1. 重新提交包含 `DWS_ASSISTANT_DAILY` 的执行 2. 确认无 SQL 错误 3. 检查 `dws.dws_assistant_daily` 表中 `trashed_count` / `trashed_seconds` 是否合理 4. 对比 `dwd_assistant_service_log_ex.is_trash = 1` 的记录数与 DWS 汇总的 `trashed_count` --- ## 5. 回滚方案 如需回滚,恢复 `assistant_daily_task.py` 到修改前版本即可。 DWS 表数据可通过重新执行 `DWS_ASSISTANT_DAILY` 任务覆盖。 """ def main(): out_dir = get_output_path("SYSTEM_LOG_ROOT") now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") content = REPORT.replace("{now}", now) out_file = out_dir / "2026-02-21__dws_assistant_daily_bug_fix.md" out_file.write_text(content, encoding="utf-8") print(f"BUG 修复报告已导出: {out_file}") if __name__ == "__main__": main()