# BD 手册：assistant_trash_event 清理 + ODS_STORE_GOODS_SALES 修复 + dim_staff_ex 修复 + DWS 精度扩展 + ODS 库存 siteid 注入

> 日期：2026-03-01
> Prompt 摘要：清理 assistant_trash_event 残留代码/DDL 文档；修复 ODS_STORE_GOODS_SALES 窗口配置以恢复商品销售数据拉取；修复 dim_staff_ex FACT_MAPPINGS 列名映射错误；DWS 层 7 个 ratio/margin 字段 numeric 精度扩展（P1）；ODS goods_stock_summary 加 siteid 列 + DWD 映射补全（P2）
> 直接原因：联调发现的 P1/P2 问题 + 三个独立问题的批量修复

---

## 一、变更说明

### 1.1 assistant_trash_event 残留清理

表 `dwd.dwd_assistant_trash_event` 和 `dwd.dwd_assistant_trash_event_ex` 已于 2026-02-22 DROP，
上游 ODS 表 `ods.assistant_cancellation_records` 同步 DROP。本次清理残留的代码引用和 DDL 文档。

**DDL 文档删除项：**

| 文件 | 删除内容 |
|------|---------|
| `etl_feiqiu__ods.sql` | PK 约束 `assistant_cancellation_records_pkey`，索引 `idx_assistant_cancellation_records_fetched_at_*`（2 条） |
| `etl_feiqiu__dwd.sql` | PK 约束 `dwd_assistant_trash_event_pkey`、`dwd_assistant_trash_event_ex_pkey`，索引 `idx_dwd_assistant_trash_event_*`（2 条） |
| `dwd-amount-duration-calibration.md` | 章节 2.11（助教废除事件主表）、存疑字段 #15、数据新鲜度行 |

**代码删除项（前序已完成）：**
- `utils/json_store.py` — API 路径映射
- `tasks/utility/manual_ingest_task.py` — FILE_MAPPING / TABLE_SPECS
- `quality/consistency_checker.py` — ODS_TABLE_TO_JSON_FILE / ODS_TABLE_TO_TASK_CODE
- `scripts/refresh_json_and_audit.py` — ACTUAL_LIST_KEY
- `scripts/run_compare_v3.py` / `run_compare_v3_fixed.py` — TABLES 列表

### 1.2 ODS_STORE_GOODS_SALES 窗口配置修复

**问题：** `ods_tasks.py` 中 `ODS_STORE_GOODS_SALES` 的 `requires_window=False`，导致 API `/TenantGoods/GetGoodsSalesList` 不传 `startTime/endTime` 参数，始终返回 0 条记录。

**修复：**
- `requires_window` → `True`
- 新增 `time_fields=("startTime", "endTime")`

**数据恢复：** 以 30 天窗口切分，回填 2025-07-07 ~ 2026-03-01，ODS 新增 26,759 条，DWD 层 `dwd_store_goods_sale` 从 17,563 → 26,759 条，时间范围延伸至 2026-02-25。

### 1.3 dim_staff_ex FACT_MAPPINGS 列名修复

**问题：** `dwd_load_task.py` 中 `dim_staff_ex` 的 FACT_MAPPINGS 使用驼峰列名（`cashierpointid`、`groupid` 等），但 ODS 表 `staff_info_master` 实际列名为下划线风格（`cashier_point_id`、`group_id` 等），导致 SCD2 合并 SQL 执行报错，整表被跳过。

**修复映射：**

| DWD 列 | 修复前（错误） | 修复后（正确） |
|--------|---------------|---------------|
| `cashier_point_id` | `cashierpointid` | `cashier_point_id` |
| `cashier_point_name` | `cashierpointname` | `cashier_point_name` |
| `group_id` | `groupid` | `group_id` |
| `group_name` | `groupname` | `group_name` |
| `system_user_id` | `systemuserid` | `system_user_id` |
| `tenant_org_id` | `tenantorgid` | `tenant_org_id` |
| `user_roles` | `userroles` | `user_roles` |

**数据恢复：** DWD 装载后 `dim_staff_ex` 从 0 行 → 15 行。

### 1.4 [P1] DWS 层 numeric 精度扩展（举一反三）

**问题：** `dws.dws_assistant_finance_analysis.gross_margin` 定义为 `numeric(5,4)`，只能存 ±0.9999。当 `cost_daily > revenue_total`（亏损场景），`gross_margin = gross_profit / revenue_total` 可能 < -1，导致 INSERT 溢出报错。举一反三排查发现 7 个同类风险字段。

**修复：**

| 表 | 字段 | 修复前 | 修复后 |
|----|------|--------|--------|
| `dws.cfg_performance_tier` | `bonus_deduction_ratio` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_assistant_finance_analysis` | `gross_margin` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_assistant_recharge_commission` | `commission_ratio` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_assistant_salary_calc` | `bonus_deduction_ratio` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_finance_discount_detail` | `discount_ratio` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_finance_income_structure` | `income_ratio` | `numeric(5,4)` | `numeric(7,4)` |
| `dws.dws_member_assistant_intimacy` | `burst_multiplier` | `numeric(6,4)` | `numeric(7,4)` |

**依赖视图处理：** 7 个 `app.v_dws_*` RLS 视图先 DROP 再重建。

**Python 防御：** `assistant_finance_task.py` 中 `gross_margin` 计算加 clamp 到 ±999.9999。

**迁移脚本：** `db/etl_feiqiu/migrations/20260301_dws_numeric_precision_fix.sql`

### 1.5 [P2] ODS goods_stock_summary 加 siteid + DWD 映射补全

**问题：** `dwd.dwd_goods_stock_summary` DDL 定义了 `site_id bigint` 和 `tenant_id bigint`，但 FACT_MAPPINGS 缺少映射，导致 DWD 层 site_id 始终为 NULL。

**根因分析：**
- API `GetGoodsStockReport` 返回的记录不含 `siteId`/`tenantId`（已从 JSON 缓存确认）
- ODS 表 `ods.goods_stock_summary` 也没有 `siteid` 列
- 请求参数中有 `siteId`，但 ODS 入库时未注入到记录中

**修复（三层联动）：**
1. ODS 表加列：`ALTER TABLE ods.goods_stock_summary ADD COLUMN siteid bigint`
2. ODS 入库通用注入：`_insert_records_schema_aware` 中，当 ODS 表有 `siteid` 列但记录不含时，从 `app.store_id` 配置注入
3. DWD FACT_MAPPINGS 补映射：`("site_id", '"siteid"', "bigint")`
4. 已有数据回填：从 `ods.goods_stock_movements` 推断 siteid 回填 3216 条

**迁移脚本：** `db/etl_feiqiu/migrations/20260301_ods_goods_stock_summary_add_siteid.sql`

---

## 二、兼容性影响

| 子系统 | 影响 |
|--------|------|
| ETL | `assistant_trash_event` 相关任务已无代码引用，无影响；`ODS_STORE_GOODS_SALES` 恢复正常窗口拉取；`dim_staff_ex` 恢复正常 SCD2 装载；DWS ratio 字段精度扩展后不再溢出；ODS 库存汇总入库时自动注入 siteid |
| 后端 API | 7 个 `app.v_dws_*` RLS 视图已重建，字段类型从 numeric(5,4) 变为 numeric(7,4)，API 返回值精度不变（仍为 4 位小数），无破坏性影响 |
| 小程序 | 无影响 |
| 管理后台 | 商品销售相关报表数据将恢复完整；库存汇总将携带 site_id |

---

## 三、回滚策略

### 3.1 assistant_trash_event
纯文档清理，无需回滚。如需恢复，从 git 历史还原对应行即可。

### 3.2 ODS_STORE_GOODS_SALES
```python
# 回滚 ods_tasks.py：
requires_window=True  →  requires_window=False
# 删除 time_fields=("startTime", "endTime") 行
```

### 3.3 dim_staff_ex
```python
# 回滚 dwd_load_task.py FACT_MAPPINGS：
("cashier_point_id", "cashier_point_id", "bigint")  →  ("cashier_point_id", "cashierpointid", "bigint")
# 其余 6 个字段同理恢复驼峰写法
```
如需清空已装载数据：`TRUNCATE dwd.dim_staff_ex;`

### 3.4 [P1] DWS numeric 精度
```sql
-- 回滚脚本：db/etl_feiqiu/migrations/20260301_dws_numeric_precision_fix_rollback.sql
-- 注意：如果已有数据超出原精度范围（如 gross_margin > 0.9999），回滚会失败
-- 需先清理超范围数据：
UPDATE dws.dws_assistant_finance_analysis SET gross_margin = LEAST(0.9999, GREATEST(-0.9999, gross_margin));
-- 然后执行回滚（同样需要先 DROP 再重建视图）
```
Python 回滚：删除 `assistant_finance_task.py` 中的 clamp 行。

### 3.5 [P2] ODS siteid 列
```sql
-- ODS 列不可逆删除（已有数据依赖），但可置 NULL：
UPDATE ods.goods_stock_summary SET siteid = NULL;
-- DWD FACT_MAPPINGS 回滚：删除 ("site_id", '"siteid"', "bigint") 行
-- ODS 入库注入回滚：删除 ods_tasks.py 中 "通用 siteid 注入" 代码块
```

---

## 四、验证 SQL

```sql
-- 1. 确认 assistant_trash_event 表已不存在
SELECT count(*) FROM information_schema.tables
WHERE table_schema = 'dwd' AND table_name LIKE '%assistant_trash_event%';
-- 预期：0

-- 2. 确认 ODS 商品销售数据已回填
SELECT count(*) as cnt, min(create_time) as min_time, max(create_time) as max_time
FROM ods.store_goods_sales_records WHERE fetched_at IS NOT NULL;
-- 预期：cnt > 40000, max_time >= 2026-02-25

-- 3. 确认 DWD 商品销售数据已更新
SELECT count(*) as cnt, max(create_time) as max_time FROM dwd.dwd_store_goods_sale;
-- 预期：cnt > 25000, max_time >= 2026-02-25

-- 4. 确认 dim_staff_ex 已有数据
SELECT count(*) as cnt FROM dwd.dim_staff_ex WHERE scd2_is_current = 1;
-- 预期：cnt = 15（与 dim_staff 一致）

-- 5. 确认 dim_staff_ex 关键字段非全 NULL
SELECT count(*) as has_system_user FROM dwd.dim_staff_ex
WHERE scd2_is_current = 1 AND system_user_id IS NOT NULL;
-- 预期：> 0

-- 6. [P1] 确认 DWS ratio 字段精度已扩展
SELECT table_name, column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_schema = 'dws'
  AND column_name IN ('gross_margin', 'bonus_deduction_ratio', 'commission_ratio',
                      'discount_ratio', 'income_ratio', 'burst_multiplier')
ORDER BY table_name;
-- 预期：所有行 numeric_precision=7, numeric_scale=4

-- 7. [P1] 确认 RLS 视图已重建
SELECT table_schema || '.' || table_name FROM information_schema.views
WHERE table_schema = 'app'
  AND table_name IN ('v_cfg_performance_tier', 'v_dws_assistant_finance_analysis',
                     'v_dws_assistant_recharge_commission', 'v_dws_assistant_salary_calc',
                     'v_dws_finance_discount_detail', 'v_dws_finance_income_structure',
                     'v_dws_member_assistant_intimacy');
-- 预期：7 行

-- 8. [P2] 确认 ODS goods_stock_summary 有 siteid 列且已回填
SELECT count(*) as total, count(siteid) as filled, count(DISTINCT siteid) as distinct_sites
FROM ods.goods_stock_summary;
-- 预期：filled = total, distinct_sites = 1

-- 9. [P2] 确认 DWD FACT_MAPPINGS 生效（下次 DWD_LOAD 后验证）
SELECT count(*) as has_site_id FROM dwd.dwd_goods_stock_summary WHERE site_id IS NOT NULL;
-- 预期：下次 ETL 运行后 > 0（当前可能仍为 0，需重跑 DWD_LOAD）
```
