在前后端开发联调前 的提交20260223
This commit is contained in:
207
scripts/ops/export_bug_report.py
Normal file
207
scripts/ops/export_bug_report.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user