P2 逐域切换:analytics 改读 public 干净层 + C2 CI 门禁落地(#319 路线图) #322

Closed
opened 2026-10-14 11:06:01 +08:00 by tanzk · 5 comments
Owner

背景

#319 数据分层路线图 P2(逐域切换)。P0(#319)已完成 21 张信源表移入 ods schema;P1(#320)已完成套餐维度干净层样板。本单为 P2:令 services/analytics/** 按 refund → complaint → reception 逐域改读 public 干净层,并把契约 C2(analytics 禁止直读 ods 源表)落为 CI 门禁。

现状侦察(2026-10-14 代码实查)

  • analytics 目录内 无 ods. 字面引用;实际靠 #319 落地的连接级 search_path=public,ods 兜底,以非限定名裸 SQL + ORM 模型直读 ods 源表。
  • 静态扫描违规 80 条:orm_model 64(import schema=ods 的模型类,集中在 today/* 与 _core/cash/category/complaint 等)、sql_table 16(裸 SQL FROM/JOIN 直读 refunds/wallet_withdrawals/cs_records/device_combos/manual_refunds/shanmao_refunds/refund_record_reports/external_complaints/card_complaints)。
  • 影响域:refund(refund.py / refund_chain.py / rr_views.py / cash.py / category.py)、complaint(complaint.py / rr_views.py)���reception(reception.py / today/_reception.py / _staff.py)、today 汇总(_kpi / _queue / _trend / _refund / _staff / _reception)。
  • 注:service_tickets 等仍留 public,不属违规面。

C2 门禁已落地(本单交付之一,随本单首次提交)

  • 检查器 scripts/check_analytics_ods_isolation.py:棘轮(ratchet) 模式,扫描三类违规(ods_prefix / sql_table / orm_model),与基线 scripts/analytics_ods_isolation_baseline.txt(80 条存量)比对——新增违规阻断 CI,减少则提示收敛。
  • CI job analytics-ods-isolation(.gitea/workflows/test.yml):纯 python3 静态扫描,不依赖 docker。
  • 棘轮理由:80 条存量违规不能一次性清零(须逐域切换),但必须立刻阻止继续新增。随每域切换完成,该域条目从基线删除,直至清空 = C2 完全生效。

执行方案(逐域切换)

按 refund → complaint → reception 顺序,每个域:

  1. 明确该域读 ods 的每个用途,判定 public 干净层能否直接承载;
  2. 不可直接承载的,设计承载对象(enrich 现有宽表 refund_wide/cash_flow_wide,或新建域投影)——须先对齐(见下);
  3. 切换 analytics 读路径到干净层;
  4. 删除该域基线条目(--update-baseline);
  5. 双读对账(差异须可解释)。

待对齐(关键设计问题,须人工拍板)

  1. 干净层承载方式:enrich 现有宽表 vs 新建域投影表 vs public 视图?干净层 30 表是否已覆盖各域分析所需(注意 #318 曾暴露 by_package / category_by_package 口径分裂)。
  2. 切换是否改变看板数字(#320 套餐维度已出现 71% / 49% 抽样差异)——业务接受度与差异归因口径。
  3. 是否需要「双读影子表」过渡期(新旧并行比对)再切。

验收标准 / 关闭条件

  • refund 域 analytics 读路径脱离 ods 源表,基线该域条目清零,双读对账差异可解释
  • complaint 域同上
  • reception 域同上
  • 基线 analytics_ods_isolation_baseline.txt 清空(CI C2 门禁完全生效)
  • ruff + pytest 全量通过;生产冒烟;页面 ⓘ 真身位置按需更新

关联

  • 前置:#319(P0 schema 落地)、#320(P1 干净层样板)
  • 关联排查:#321(current_combo_name 与订单源 37% 不一致——投诉域切换的前置)
  • 契约:C1–C4(#319 正文);#318(口径分裂前车之鉴)
## 背景 #319 数据分层路线图 P2(逐域切换)。P0(#319)已完成 21 张信源表移入 `ods` schema;P1(#320)已完成套餐维度干净层样板。本单为 **P2**:令 `services/analytics/**` 按 refund → complaint → reception 逐域改读 public 干净层,并把契约 **C2**(analytics 禁止直读 ods 源表)落为 CI 门禁。 ## 现状侦察(2026-10-14 代码实查) - analytics 目录内 **无 `ods.` 字面引用**;实际靠 #319 落地的连接级 `search_path=public,ods` 兜底,以**非限定名裸 SQL** + **ORM 模型**直读 ods 源表。 - 静态扫描违规 **80 条**:`orm_model` 64(import schema=ods 的模型类,集中在 today/* 与 _core/cash/category/complaint 等)、`sql_table` 16(裸 SQL FROM/JOIN 直读 refunds/wallet_withdrawals/cs_records/device_combos/manual_refunds/shanmao_refunds/refund_record_reports/external_complaints/card_complaints)。 - 影响域:refund(refund.py / refund_chain.py / rr_views.py / cash.py / category.py)、complaint(complaint.py / rr_views.py)���reception(reception.py / today/_reception.py / _staff.py)、today 汇总(_kpi / _queue / _trend / _refund / _staff / _reception)。 - 注:`service_tickets` 等仍留 public,**不属**违规面。 ## C2 门禁已落地(本单交付之一,随本单首次提交) - 检查器 `scripts/check_analytics_ods_isolation.py`:**棘轮(ratchet)** 模式,扫描三类违规(ods_prefix / sql_table / orm_model),与基线 `scripts/analytics_ods_isolation_baseline.txt`(80 条存量)比对——**新增违规阻断 CI**,减少则提示收敛。 - CI job `analytics-ods-isolation`(.gitea/workflows/test.yml):纯 python3 静态扫描,不依赖 docker。 - 棘轮理由:80 条存量违规不能一次性清零(须逐域切换),但必须**立刻阻止继续新增**。随每域切换完成,该域条目从基线删除,直至清空 = C2 完全生效。 ## 执行方案(逐域切换) 按 refund → complaint → reception 顺序,每个域: 1. 明确该域读 ods 的每个用途,判定 public 干净层能否直接承载; 2. 不可直接承载的,**设计承载对象**(enrich 现有宽表 refund_wide/cash_flow_wide,或新建域投影)——须先对齐(见下); 3. 切换 analytics 读路径到干净层; 4. 删除该域基线条目(`--update-baseline`); 5. 双读对账(差异须可解释)。 ## 待对齐(关键设计问题,须人工拍板) 1. 干净层承载方式:**enrich 现有宽表** vs **新建域投影表** vs **public 视图**?干净层 30 表是否已覆盖各域分析所需(注意 #318 曾暴露 by_package / category_by_package 口径分裂)。 2. 切换是否改变看板数字(#320 套餐维度已出现 71% / 49% 抽样差异)——业务接受度与差异归因口径。 3. 是否需要「双读影子表」过渡期(新旧并行比对)再切。 ## 验收标准 / 关闭条件 - [x] refund 域 analytics 读路径脱离 ods 源表,基线该域条目清零,双读对账差异可解释 - [x] complaint 域同上 - [x] reception 域同上 - [x] 基线 `analytics_ods_isolation_baseline.txt` 清空(CI C2 门禁完全生效) - [x] ruff + pytest 全量通过;生产冒烟;页面 ⓘ 真身位置按需更新 ## 关联 - 前置:#319(P0 schema 落地)、#320(P1 干净层样板) - 关联排查:#321(current_combo_name 与订单源 37% 不一致——投诉域切换的前置) - 契约:C1–C4(#319 正文);#318(口径分裂前车之鉴)
Author
Owner

P2 refund 域切换设计草案 v2(修订:口径不变原则)

v2 修订说明:据用户纠正——切换只换数据源,不换口径;不相干的记录/字段不得引入("贸然加进来是负担,后续去掉更麻烦")。本版据此收紧承载方案,并撤销"接受口径扩大"的假设。

〇、口径不变原则(硬约束,本次新增)

  1. 切换前后看板数字不得变化。只把读的"表"从 ods 源换成 public 干净层,过滤条件原样保留。
  2. 不得因"宽表自带更多行/字段"而扩大口径:如 refund_wide 里的线下已退款 / 状态未填等与当前分析无关的记录,不得进入。
  3. 只放明确有用的对象与字段;不预置、不"顺手"、不"先加上以后再说"。
  4. 无现成干净对象且非本轮明确有用者 → 本轮不引入、不新建。

一、改造清单(refund 域,基线 18 条)

文件 ods 读 用途 本轮处置
refund.py refunds ×5 A路退款事件流(线上+已退款) ✅ 切 refund_wide(加等价过滤)
refund_chain.py refunds / wallet_withdrawals A→B 转化链路 ⏸ 待定(见决策 4,B路粒度不明)
refund.py wallet_withdrawals B路提现 ⏸ 待定
refund.py manual_refunds(D) D路手动退款 ⛔ 无干净对象 → 不引入
refund.py shanmao_refunds(C) C路(独立业务) ⛔ 无干净对象 → 不引入
refund.py cs_records ×5 客服过程/关键词/责任 ⛔ 无干净对象 → 不引入
refund.py refund_record_reports 退款记录报表 ⛔ 无干净对象 → 不引入
refund.py BmsOrder 订单 ⛔ 无干净对象 → 不引入
refund.py device_combos 套餐 ✅ 已完成(#320 → dim_device_combo)
category.py device_combos 套餐 ✅ 已完成(#320)
cash.py / category.py / rr_views.py 见基线 现金流/品类/复访 ⛔ 依赖无干净对象者,本轮不引入

二、承载方案(口径不变)

ods 读 承载对象 切换方式(保口径)
refunds(现口径=线上+已退款) refund_wide 查询 必须施加等价过滤:refund_method='线上' AND status='已退款' AND device_sn IS NOT NULL AND created_at_raw IS NOT NULL → 行集与现状一致
device_combos dim_device_combo ✅ 按事件时点 JOIN(#320 已完成)

其余全部不引���(无干净对象,且非本轮明确有用)。

三、口径不变的验证方法(切换前置)

对同窗口做双源 diff,证明零差异:

  • count(*) 与 sum(refund_amount):ods.refunds(现过滤) vs refund_wide(等价过滤)必须相等;
  • 抽样明细按 refund_no 对拍。
  • 差异非 0 → 说明 refund_wide 不是保口径超集,停止切换并回报。

四、关键决策(收紧后)

  1. 口径扩大是否接受 → 已撤销:口径不变(原则〇)。
  2. wallet_withdrawals / refund_chain 的 B路提现:分析真正需要的字段是否在 refund_wide.wallet_*(wallet_id/op_time/op_amount)中?只取明确有用的列;若提现失败明细确实需要 → 才考虑承载,否则不引入。
  3. C/D 路与 cs_records:本轮不引入(无干净对象、非本轮明确有用);留待各自域,确认"明确有用"时再评估。

五、分期(收敛后)

阶段 内容 前置
0 ✅ device_combos → dim_device_combo #320
1 refunds → refund_wide(等价过滤,零差异验证) 双源 diff 通过
2+ 其余:不主动引入,只在确认"明确有用"且口径不变时逐项评估 —

六、C2 棘轮门禁

CI job analytics-ods-isolation(#322,提交 95e9ff6)已阻止新增违规。本域切换仅收敛 refunds 条目;其余条目允许保留(因不引入无关对象)。基线不清空也可接受——C2 与"口径不变"冲突时,后者优先。

## P2 refund 域切换设计草案 v2(修订:**口径不变**原则) > v2 修订说明:据用户纠正——切换**只换数据源,不换口径**;不相干的记录/字段**不得**引入("贸然加进来是负担,后续去掉更麻烦")。本版据此收紧承载方案,并撤销"接受口径扩大"的假设。 ### 〇、口径不变原则(硬约束,本次新增) 1. **切换前后看板数字不得变化**。只把读的"表"从 `ods` 源换成 public 干净层,**过滤条件原样保留**。 2. **不得**因"宽表自带更多行/字段"而扩大口径:如 `refund_wide` 里的**线下已退款 / 状态未填**等**与当前分析无关的记录,不得进入**。 3. **只放明确有用**的对象与字段;不预置、不"顺手"、不"先加上以后再说"。 4. 无现成干净对象且非本轮明确有用者 → **本轮不引入、不新建**。 ### 一、改造清单(refund 域,基线 18 条) | 文件 | ods 读 | 用途 | 本轮处置 | |---|---|---|---| | `refund.py` | `refunds` ×5 | A路退款事件流(线上+已退款) | ✅ 切 `refund_wide`(加等价过滤) | | `refund_chain.py` | `refunds` / `wallet_withdrawals` | A→B 转化链路 | ⏸ 待定(见决策 4,B路粒度不明) | | `refund.py` | `wallet_withdrawals` | B路提现 | ⏸ 待定 | | `refund.py` | `manual_refunds`(D) | D路手动退款 | ⛔ 无干净对象 → 不引入 | | `refund.py` | `shanmao_refunds`(C) | C路(独立业务) | ⛔ 无干净对象 → 不引入 | | `refund.py` | `cs_records` ×5 | 客服过程/关键词/责任 | ⛔ 无干净对象 → 不引入 | | `refund.py` | `refund_record_reports` | 退款记录报表 | ⛔ 无干净对象 → 不引入 | | `refund.py` | `BmsOrder` | 订单 | ⛔ 无干净对象 → 不引入 | | `refund.py` | `device_combos` | 套餐 | ✅ 已完成(#320 → `dim_device_combo`) | | `category.py` | `device_combos` | 套餐 | ✅ 已完成(#320) | | `cash.py` / `category.py` / `rr_views.py` | 见基线 | 现金流/品类/复访 | ⛔ 依赖无干净对象者,本轮不引入 | ### 二、承载方案(口径不变) | ods 读 | 承载对象 | 切换方式(**保口径**) | |---|---|---| | `refunds`(现口径=线上+已退款) | **`refund_wide`** | 查询 **必须施加等价过滤**:`refund_method='线上' AND status='已退款' AND device_sn IS NOT NULL AND created_at_raw IS NOT NULL` → 行集与现状一致 | | `device_combos` | **`dim_device_combo`** | ✅ 按事件时点 JOIN(#320 已完成) | **其余全部不引���**(无干净对象,且非本轮明确有用)。 ### 三、口径不变的验证方法(切换前置) 对同窗口做**双源 diff**,证明零差异: - `count(*)` 与 `sum(refund_amount)`:`ods.refunds`(现过滤) vs `refund_wide`(等价过滤)**必须相等**; - 抽样明细按 `refund_no` 对拍。 - 差异非 0 → 说明 `refund_wide` 不是保口径超集,**停止切换**并回报。 ### 四、关键决策(收紧后) 1. ~~口径扩大是否接受~~ → **已撤销**:口径不变(原则〇)。 2. `wallet_withdrawals` / `refund_chain` 的 B路提现:分析真正需要的字段是否在 `refund_wide.wallet_*`(wallet_id/op_time/op_amount)中?**只取明确有用的列**;若提现失败明细确实需要 → 才考虑承载,否则**不引入**。 3. C/D 路与 `cs_records`:**本轮不引入**(无干净对象、非本轮明确有用);留待各自域,确认"明确有用"时再评估。 ### 五、分期(收敛后) | 阶段 | 内容 | 前置 | |---|---|---| | 0 ✅ | `device_combos` → `dim_device_combo` | #320 | | 1 | `refunds` → `refund_wide`(等价过滤,零差异验证) | 双源 diff 通过 | | 2+ | 其余:**不主动引入**,只在确认"明确有用"且口径不变时逐项评估 | — | ### 六、C2 棘轮门禁 CI job `analytics-ods-isolation`(#322,提交 `95e9ff6`)已阻止**新增**违规。本域切换仅收敛 `refunds` 条目;其余条目**允许保留**(因不引入无关对象)。基线不清空也可接受——C2 与"口径不变"冲突时,**后者优先**。
Author
Owner

P2 refund 域切换设计草案 v3(实测校准:列不足,阶段1 不成立)

v3 修订说明:据用户"不相干/没必要的字段不要加;后续需要就去原始表补充表结构;设计整合表就是为了精简"的约束,对 refund_wide 做了列级实测审计(v2 只验证了行级,未验证列级)。结论:refund_wide 不是 refund.py 的保口径·保列承载对象,阶段1 取消。

〇、口径不变原则(硬约束,沿 v2)

  1. 切换前后看板数字不得变化;只换读的表,过滤条件原样保留。
  2. 不得因宽表自带更多行/字段而扩大口径;不相干的记录不进。
  3. 只放明确有用的对象与字段;不预置、不"顺手"、不"先加上以后再说";不必要的字段不进。
  4. 无现成干净对象且非本轮明确有用者 → 本轮不引入、不新建。

一、列级实测(决定性,本次新增)

refund_wide 现有列 = refund 冗余 9 列(refund_no/method/status/reason/operator/auditor/device_sn/amount/created_at_raw)

  • ticket 4 列 + wallet 3 列 + external 3 列 + 物化 1 列。

refund.py 事件流从 refunds 实际读取的列,有 10 列 refund_wide 没有:

缺失列 refund.py 用途
actual_amount 金额 COALESCE(actual_amount, refund_amount, 0)(多处)
actual_refund_time 对账归日 / 现金流归日
quality_status stats_quality_excluded_sql 质检排除
package_name 套餐统计
merchant_name 商户统计
remark 涉诉判定(备注涉诉)
audit_remark 涉诉判定
platform_order_no / third_order_no 导出明细
updated_at_raw 归日回退

→ 用 refund_wide 承载 = 必须新增 10 列。这与"精简 / 不加不相干字段"冲突;且这 10 列对 refund.py 是必要的,一旦加上,refund_wide 就退化成 refunds + joins,失去精简意义。

二、行级实测(沿 v2,补充说明)

  • refund_wide 行范围(A-20260908-001 LOCKED)= 线上+已退款 ∪ 带钱包关联 ∪ SN 有外部投诉 → 是并集/超集,需额外收窄过滤才等于现有口径。
  • 本地 csctalon 快照:A 路口径 diff=931,但该快照 matched_at 仅到 08-13、缺行落在 08-11~08-17 → 属物化滞后,非结构缺口,不能据此判定;生产曾观测 2 条非零差异,仍待生产实测定性。

三、结论

P2 refund 阶段1(refund.py → refund_wide)不成立,原因:列不足(10 列)+ 行非保口径超集。强行切换 = 扩表 + 加收窄过滤,三条原则(精简/口径不变/不加不相干)全违反。

四、承载方案(供决策,二选一)

方案 内容 与"精简/口径不变/不加不相干"
A(推荐) refund 域干净层 = 精简投影视图 public.refund_clean:CREATE VIEW ... AS SELECT <analytics 实际消费的列> FROM ods.refunds ✅ 单表单一来源;✅ 零冗余行/列;✅ 口径天然不变(同表);✅ 后续需要新列 → 改视图定义即可(对应"回原始表补充结构");✅ C2 收敛 refunds 条目
B 维持现状(analytics 继续读 refunds→ods),C2 基线保留该条目,待"明确有用"时再评估 ✅ 不引入任何新对象(最贴合"不相干不要放进来");✖ P2 本域无进展

五、其余域(沿 v2)

  • device_combos → dim_device_combo:✅ 已完成(#320)。
  • wallet_withdrawals/refund_chain/manual_refunds/shanmao_refunds/cs_records/refund_record_reports/BmsOrder:本轮不引入(无干净对象,非本轮明确有用)。

六、C2 棘轮门禁

CI job analytics-ods-isolation(#322,95e9ff6)已阻止新增违规;baseline 存量条目允许保留(C2 与"口径不变/精简"冲突时,后者优先)。

## P2 refund 域切换设计草案 v3(实测校准:**列**不足,阶段1 不成立) > v3 修订说明:据用户"不相干/没必要的字段不要加;后续需要就去原始表补充表结构;设计整合表就是为了精简"的约束,对 `refund_wide` 做了**列级实测审计**(v2 只验证了行级,未验证列级)。结论:`refund_wide` 不是 refund.py 的保口径·保列承载对象,**阶段1 取消**。 ### 〇、口径不变原则(硬约束,沿 v2) 1. 切换前后看板数字**不得变化**;只换读的表,过滤条件原样保留。 2. **不得**因宽表自带更多行/字段而扩大口径;不相干的记录**不进**。 3. **只放明确有用**的对象与字段;不预置、不"顺手"、不"先加上以后再说";**不必要的字段不进**。 4. 无现成干净对象且非本轮明确有用者 → 本轮**不引入、不新建**。 ### 一、列级实测(决定性,本次新增) `refund_wide` 现有列 = refund 冗余 9 列(refund_no/method/status/reason/operator/auditor/device_sn/amount/created_at_raw) + ticket 4 列 + wallet 3 列 + external 3 列 + 物化 1 列。 `refund.py` 事件流从 `refunds` 实际读取的列,**有 10 列 `refund_wide` 没有**: | 缺失列 | refund.py 用途 | |---|---| | `actual_amount` | 金额 `COALESCE(actual_amount, refund_amount, 0)`(多处) | | `actual_refund_time` | 对账归日 / 现金流归日 | | `quality_status` | `stats_quality_excluded_sql` 质检排除 | | `package_name` | 套餐统计 | | `merchant_name` | 商户统计 | | `remark` | 涉诉判定(备注涉诉) | | `audit_remark` | 涉诉判定 | | `platform_order_no` / `third_order_no` | 导出明细 | | `updated_at_raw` | 归日回退 | → 用 `refund_wide` 承载 = 必须**新增 10 列**。这与"精简 / 不加不相干字段"冲突;且这 10 列对 refund.py 是**必要的**,一旦加上,`refund_wide` 就退化成 `refunds + joins`,失去精简意义。 ### 二、行级实测(沿 v2,补充说明) - `refund_wide` 行范围(A-20260908-001 LOCKED)= 线上+已退款 **∪** 带钱包关联 **∪** SN 有外部投诉 → 是**并集/超集**,需额外收窄过滤才等于现有口径。 - 本地 `csctalon` 快照:A 路口径 diff=931,但该快照 `matched_at` 仅到 08-13、缺行落在 08-11~08-17 → 属**物化滞后**,非结构缺口,**不能**据此判定;生产曾观测 2 条非零差异,仍待生产实测定性。 ### 三、结论 **P2 refund 阶段1(refund.py → refund_wide)不成立**,原因:列不足(10 列)+ 行非保口径超集。强行切换 = 扩表 + 加收窄过滤,三条原则(精简/口径不变/不加不相干)全违反。 ### 四、承载方案(供决策,二选一) | 方案 | 内容 | 与"精简/口径不变/不加不相干" | |---|---|---| | **A(推荐)** | refund 域干净层 = **精简投影视图** `public.refund_clean`:`CREATE VIEW ... AS SELECT <analytics 实际消费的列> FROM ods.refunds` | ✅ 单表单一来源;✅ 零冗余行/列;✅ 口径天然不变(同表);✅ 后续需要新列 → 改视图定义即可(对应"回原始表补充结构");✅ C2 收敛 `refunds` 条目 | | **B** | 维持现状(analytics 继续读 `refunds`→ods),C2 基线**保留**该条目,待"明确有用"时再评估 | ✅ 不引入任何新对象(最贴合"不相干不要放进来");✖ P2 本域**无进展** | ### 五、其余域(沿 v2) - `device_combos` → `dim_device_combo`:✅ 已完成(#320)。 - `wallet_withdrawals`/`refund_chain`/`manual_refunds`/`shanmao_refunds`/`cs_records`/`refund_record_reports`/`BmsOrder`:本轮**不引入**(无干净对象,非本轮明确有用)。 ### 六、C2 棘轮门禁 CI job `analytics-ods-isolation`(#322,`95e9ff6`)已阻止**新增**违规;baseline 存量条目允许保留(C2 与"口径不变/精简"冲突时,后者优先)。
Author
Owner

发布核验 · refund 域方案A(refund_clean 视图)(2026-10-14)

不闭单:本条仅为 refund 域的落地+发布凭证;P2 其余域(wallet_withdrawals / refund_chain / manual_refunds / shanmao_refunds / cs_records / refund_record_reports / BmsOrder)后续另行推进。

变更与发布

项 内容
commit cc37a68 #322 方案A: refund 域 analytics 改读 public.refund_clean 精简视图(19 files, +287/-163)
迁移 a9b0c1d2e3f4(down_revision f8a9b0c1d2e3),生产启动日志实测:Running upgrade f8a9b0c1d2e3 -> a9b0c1d2e3f4
发布 标准入口 deploy/upgrade.sh --git(main),自动备份后 git-20261014101804 → git-20261014125424,backend healthy

生产实测凭证(可复现)

  1. 视图结构:relkind='v',恰 21 列(information_schema 实测列清单与迁移逐字一致)。
  2. 行数对账:SELECT count(*) FROM public.refund_clean = FROM ods.refunds = 201,348 = 201,348(投影视图口径零差异)。
  3. 非缓存镜像:运行中容器内 /app/app/models/refund.py 含 refund_clean 标记、迁移文件含 a9b0c1d2e3f4(排除 COPY 层缓存命中旧镜像)。
  4. 读路径冒烟:在生产容器内以部署代码执行 analytics.refund.refund_decision_overview(db)(覆盖含补列的 GROUP BY 路径),返回 9 个块完整、summary 22 键,无异常。
  5. 写入路径未动:Refund(ods.refunds)写入模型零改动,upsert 入库 / actual_refund_time 回填路径不受视图影响。

发布前门禁

  • C2 棘轮:当前违规 64 · 基线 64,无新增违规(基线 80→64,收敛 16 条)。
  • ruff:changed files All checks passed!。
  • pytest(9 个 analytics 相关文件 109 用例):107 通过;2 个失败(TestRrViewsDbSmoke CompoundSelect.c、test_ops_overview_device_type)已用 git stash 在干净 HEAD 复现,确属预存失败、与本次无关。

遗留(非本次范围)

  • governance_detector._SKIP_DATE_TABLES 是否登记 refund_clean(派生视图)待评估。
  • alembic autogenerate 对 mapped 视图模型可能生成伪迁移,后续评估 include_object 钩子。
## 发布核验 · refund 域方案A(refund_clean 视图)(2026-10-14) **不闭单**:本条仅为 refund 域的落地+发布凭证;P2 其余域(wallet_withdrawals / refund_chain / manual_refunds / shanmao_refunds / cs_records / refund_record_reports / BmsOrder)后续另行推进。 ### 变更与发布 | 项 | 内容 | |---|---| | commit | `cc37a68` #322 方案A: refund 域 analytics 改读 public.refund_clean 精简视图(19 files, +287/-163) | | 迁移 | `a9b0c1d2e3f4`(down_revision `f8a9b0c1d2e3`),生产启动日志实测:`Running upgrade f8a9b0c1d2e3 -> a9b0c1d2e3f4` | | 发布 | 标准入口 `deploy/upgrade.sh --git`(main),自动备份后 git-20261014101804 → git-20261014125424,backend healthy | ### 生产实测凭证(可复现) 1. **视图结构**:`relkind='v'`,恰 21 列(information_schema 实测列清单与迁移逐字一致)。 2. **行数对账**:`SELECT count(*) FROM public.refund_clean` = `FROM ods.refunds` = **201,348 = 201,348**(投影视图口径零差异)。 3. **非缓存镜像**:运行中容器内 `/app/app/models/refund.py` 含 `refund_clean` 标记、迁移文件含 `a9b0c1d2e3f4`(排除 COPY 层缓存命中旧镜像)。 4. **读路径冒烟**:在生产容器内以部署代码执行 `analytics.refund.refund_decision_overview(db)`(覆盖含补列的 GROUP BY 路径),返回 9 个块完整、summary 22 键,无异常。 5. **写入路径未动**:`Refund`(ods.refunds)写入模型零改动,upsert 入库 / actual_refund_time 回填路径不受视图影响。 ### 发布前门禁 - C2 棘轮:`当前违规 64 · 基线 64,无新增违规`(基线 80→64,收敛 16 条)。 - ruff:changed files `All checks passed!`。 - pytest(9 个 analytics 相关文件 109 用例):107 通过;2 个失败(`TestRrViewsDbSmoke` CompoundSelect.c、`test_ops_overview_device_type`)已用 `git stash` 在干净 HEAD 复现,确属**预存失败、与本次无关**。 ### 遗留(非本次范围) - `governance_detector._SKIP_DATE_TABLES` 是否登记 `refund_clean`(派生视图)待评估。 - alembic autogenerate 对 mapped 视图模型可能生成伪迁移,后续评估 `include_object` 钩子。
Author
Owner

关闭凭证核验 (2026-10-14)

交付物:commit 89d1057(10 张新增 public 干净视图 + 既有 refund_clean 共 11 张;analytics 14 文件改读;C2 基线 80→0),已 push main。

正文验收标准 核验方式(可复现) 结论
refund 域读路径脱离 ods、基线该域清零 python3 scripts/check_analytics_ods_isolation.py → 「当前违规 0 · 基线 0,门禁通过」;refund/refund_chain/rr_views/cash/category 已改读 *_clean 通过
complaint 域同上 同上;complaint/rr_views 改读 card_complaint_clean/external_complaint_clean/cs_record_clean 通过
reception 域同上 同上;reception/today/_reception/_staff 改读 cs_record_clean 通过
基线清空(C2 完全生效) scripts/analytics_ods_isolation_baseline.txt 仅余表头(80→0) 通过
ruff + pytest 全量通过 ruff:All checks passed;pytest:见下「存量失败」→ 降级接受 降级接受
生产冒烟 / 页面 ⓘ 真身位置 见下生产核验;本方案为薄视图·口径逐字不变,看板数字不变 → ⓘ 无需变更(显式声明) 通过

生���核验(2026-10-14,节点 VM400;标准入口 deploy/upgrade.sh --git:csctalon-src → git-20261014094251,健康检查通过)

  • alembic current = c3d4e5f6a7b8 (head)(迁移 c3d4e5f6a7b8_issue322_clean_views_batch 已应用)
  • public 下 11 张 *_clean 视图存在
  • 行数对账(视图 vs ods 源表,11/11 完全一致):refund 201540/201540、bms_order 544648/544648、device_combo 8393612/8393612、card_complaint 783/783、external_complaint 2839/2839、cs_record 489436/489436、wallet_withdrawal 36248/36248、device_flow_usage 958/958、manual_refund 10652/10652、refund_record_report 39241/39241、shanmao_refund 2066/2066
  • 视图定义核验:pg_views 中 11/11 为纯列投影(FROM ods.<t>,无 WHERE/JOIN/GROUP/CASE/DISTINCT)→ 口径逐字不变的结构保证
  • 运行时冒烟:容器内直调 analytics 入口 6/6 OK(cash_out_breakdown / reception_overview_trend / operator_ticket_stats / refund_decision_overview / today_kpi / reception_per_capita)

存量 pytest 失败(非本单引入,降级接受依据)

  • test_analytics_core_smoke.py::...preset_complaint_subtype_rootcause_heatmap(CompoundSelect.c)——回 HEAD 复跑复现
  • test_category_ops_overview.py::test_ops_overview_device_type(随机 type_code 断言)——回 HEAD 复跑复现
  • test_b2_2_ai_data_supply.py::test_supply_device_behavior_profile(ai_data_supply,未涉 analytics/_clean/ods)
  • test_llm_tiers.py ×2(LLM 层级 provider 名,与 #322 无关)

经 grep 确认上述用例均不 import analytics / *_clean / ods,与本单变更面无交集。
结论:以「全量 5 项失败均为存量、与 #322 变更面无交集」降级验收。

审计样本(供业务口径逐项复核;落本地 workdata/,含 PII 不入 git)

  • workdata/view_samples/*.csv:11 张干净视图各 20 行样本
  • workdata/interface_samples/*.json:46 个 analytics 接口默认视图结果 + _index.json
## 关闭凭证核验 (2026-10-14) **交付物**:commit `89d1057`(10 张新增 public 干净视图 + 既有 refund_clean 共 11 张;analytics 14 文件改读;C2 基线 80→0),已 push `main`。 | 正文验收标准 | 核验方式(可复现) | 结论 | |---|---|---| | refund 域读路径脱离 ods、基线该域清零 | `python3 scripts/check_analytics_ods_isolation.py` → 「当前违规 0 · 基线 0,门禁通过」;refund/refund_chain/rr_views/cash/category 已改读 `*_clean` | 通过 | | complaint 域同上 | 同上;complaint/rr_views 改读 `card_complaint_clean`/`external_complaint_clean`/`cs_record_clean` | 通过 | | reception 域同上 | 同上;reception/today/_reception/_staff 改读 `cs_record_clean` | 通过 | | 基线清空(C2 完全生效) | `scripts/analytics_ods_isolation_baseline.txt` 仅余表头(80→0) | 通过 | | ruff + pytest 全量通过 | ruff:`All checks passed`;pytest:见下「存量失败」→ 降级接受 | 降级接受 | | 生产冒烟 / 页面 ⓘ 真身位置 | 见下生产核验;本方案为**薄视图·口径逐字不变**,看板数字不变 → ⓘ 无需变更(显式声明) | 通过 | **生���核验**(2026-10-14,节点 VM400;标准入口 `deploy/upgrade.sh --git`:csctalon-src → git-20261014094251,健康检查通过) - `alembic current` = `c3d4e5f6a7b8 (head)`(迁移 `c3d4e5f6a7b8_issue322_clean_views_batch` 已应用) - `public` 下 11 张 `*_clean` 视图存在 - **行数对账(视图 vs ods 源表,11/11 完全一致)**:refund 201540/201540、bms_order 544648/544648、device_combo 8393612/8393612、card_complaint 783/783、external_complaint 2839/2839、cs_record 489436/489436、wallet_withdrawal 36248/36248、device_flow_usage 958/958、manual_refund 10652/10652、refund_record_report 39241/39241、shanmao_refund 2066/2066 - **视图定义核验**:`pg_views` 中 11/11 为纯列投影(`FROM ods.<t>`,无 WHERE/JOIN/GROUP/CASE/DISTINCT)→ 口径逐字不变的结构保证 - **运行时冒烟**:容器内直调 analytics 入口 6/6 OK(cash_out_breakdown / reception_overview_trend / operator_ticket_stats / refund_decision_overview / today_kpi / reception_per_capita) **存量 pytest 失败(非本单引入,降级接受依据)** - `test_analytics_core_smoke.py::...preset_complaint_subtype_rootcause_heatmap`(CompoundSelect.c)——回 HEAD 复跑复现 - `test_category_ops_overview.py::test_ops_overview_device_type`(随机 type_code 断言)——回 HEAD 复跑复现 - `test_b2_2_ai_data_supply.py::test_supply_device_behavior_profile`(ai_data_supply,未涉 analytics/_clean/ods) - `test_llm_tiers.py` ×2(LLM 层级 provider 名,与 #322 无关) > 经 grep 确认上述用例均不 import `analytics` / `*_clean` / `ods`,与本单变更面无交集。 > 结论:以「全量 5 项失败均为存量、与 #322 变更面无交集」**降级验收**。 **审计样本**(供业务口径逐项复核;落本地 `workdata/`,含 PII 不入 git) - `workdata/view_samples/*.csv`:11 张干净视图各 20 行样本 - `workdata/interface_samples/*.json`:46 个 analytics 接口默认视图结果 + `_index.json`
tanzk closed this issue 2026-10-14 17:55:27 +08:00
Author
Owner

遗留评估闭环:governance_detector 是否需跳过 refund_clean 等 *_clean 视图

结论:不需要,实证如下(2026-10-14)

  • governance_detector 的表枚举基于 information_schema.tables(public+ods),*_clean 视图理论上会进入枚举面;
  • 但 #322 发布后实测生产 governance_findings 共 71 条,其中涉及 _clean 视图的为 0 条 —— 现有检查类型(外键/索引/日期列/PII 等)对纯列投影视图不产生发现;
  • 若未来新增「逐表存储/行数/膨胀」类检查,再按需把 CLEAN_VIEWS 键集加入跳过名单(单一来源 app/models/ods_clean.py)。

即:不增加代码,留档实证,后续新检查类型接入时参照本条。

## 遗留评估闭环:governance_detector 是否需跳过 refund_clean 等 *_clean 视图 **结论:不需要,实证如下(2026-10-14)** - governance_detector 的表枚举基于 `information_schema.tables`(public+ods),`*_clean` 视图理论上会进入枚举面; - 但 #322 发布后实测生产 `governance_findings` 共 **71 条,其中涉及 `_clean` 视图的为 0 条** —— 现有检查类型(外键/索引/日期列/PII 等)对纯列投影视图不产生发现; - 若未来新增「逐表存储/行数/膨胀」类检查,再按需把 `CLEAN_VIEWS` 键集加入跳过名单(单一来源 `app/models/ods_clean.py`)。 即:不增加代码,留档实证,后续新检查类型接入时参照本条。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tanzk/csctalon#322