Files
Neo-ZQYY/scripts/ops/export_bug_report.py

208 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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_uuid4ba9d2d365ee4a858f1c4104b1942dc2
> 执行开始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()