From 76a23639eebee7df4faf211f5a13e83542fd1f73 Mon Sep 17 00:00:00 2001 From: Neo Date: Wed, 22 Apr 2026 21:55:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20app2a=20DWS=20=E6=96=B0=E5=88=97=20?= =?UTF-8?q?+=20ai=5Fcache=20CHECK=20=E7=BA=A6=E6=9D=9F=E6=94=BE=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql: - dws.dws_finance_area_daily 增加 member_order_count 列 (integer NOT NULL DEFAULT 0) - 重建 app.v_dws_finance_area_daily RLS 视图暴露新列 - 同步重建 dws.v_dws_finance_area_daily(遵守双 schema 规则) - 列顺序因 PostgreSQL CREATE OR REPLACE VIEW 限制必须加在末尾 2. db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql: - biz.ai_cache.chk_ai_cache_type CHECK 约束放开 app2a_finance_area 新值 - DROP 旧 7 项 CHECK + CREATE 含 8 项的新 CHECK(新增 app2a_finance_area) 3. docs/database/changes/ 两份变更文档: - 变更说明 + 兼容性 + 回滚策略 + 3-4 条验证 SQL 测试库已执行 + 验证通过。生产库待上线窗口按 checklist 跑。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...20260423__app2a_add_member_order_count.sql | 180 +++++++++++++++ .../20260423__ai_cache_allow_app2a.sql | 70 ++++++ .../2026-04-23__ai_cache_allow_app2a.md | 120 ++++++++++ .../2026-04-23__app2a_member_order_count.md | 214 ++++++++++++++++++ 4 files changed, 584 insertions(+) create mode 100644 db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql create mode 100644 db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql create mode 100644 docs/database/changes/2026-04-23__ai_cache_allow_app2a.md create mode 100644 docs/database/changes/2026-04-23__app2a_member_order_count.md diff --git a/db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql b/db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql new file mode 100644 index 0000000..deb108f --- /dev/null +++ b/db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql @@ -0,0 +1,180 @@ +-- ============================================================= +-- Migration: 20260423__app2a_add_member_order_count.sql +-- Purpose: 为 app2a 区域财务洞察增加 dws 层区域级会员订单数列 +-- Related: docs/ai/app2_finance_multi_app_design.md v2 · Phase B +-- Author: Claude + Neo +-- ============================================================= +-- +-- 改动说明: +-- 1. dws.dws_finance_area_daily 增加 member_order_count 列(默认 0,允许新历史数据为 0) +-- 2. 重建 RLS 视图 app.v_dws_finance_area_daily,暴露新列 +-- 3. 若存在 dws.v_dws_finance_area_daily 同名镜像视图,同步重建 +-- +-- 后置依赖: +-- - ETL loader apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily.py +-- 需要同步改造:按 (area_code, stat_date) 聚合 DWD 订单时, +-- 将 is_member_order = true 的订单数合入 member_order_count +-- - 改造前历史数据 member_order_count = 0;改造后新增数据生效 +-- - 如需历史回填:scripts/ops/backfill_area_member_order.py(本迁移不含) +-- +-- 回滚策略: +-- 见文件末 ROLLBACK 段 +-- +-- 验证 SQL: +-- 见文件末 VERIFICATION 段 +-- ============================================================= + +BEGIN; + +-- ------------------------------------------------------------ +-- Step 1: dws 层表加列 +-- ------------------------------------------------------------ +ALTER TABLE dws.dws_finance_area_daily + ADD COLUMN IF NOT EXISTS member_order_count integer NOT NULL DEFAULT 0; + +COMMENT ON COLUMN dws.dws_finance_area_daily.member_order_count IS + '会员订单数 (区域粒度, 从 DWD 聚合 is_member_order=true 的订单计数, 默认 0)'; + +-- ------------------------------------------------------------ +-- Step 2: 重建 app schema 的 RLS 视图 +-- - 保留原 RLS 过滤条件 (current_setting('app.current_site_id')) +-- - PostgreSQL 限制:CREATE OR REPLACE VIEW 仅允许末尾加列,不能中间插入 +-- - 故 member_order_count 加到末尾(逻辑归属仍为单位经济,顺序不影响语义) +-- ------------------------------------------------------------ +CREATE OR REPLACE VIEW app.v_dws_finance_area_daily AS +SELECT + id, + site_id, + tenant_id, + stat_date, + area_code, + table_fee_amount, + goods_amount, + assistant_pd_amount, + assistant_cx_amount, + gross_amount, + discount_groupbuy, + discount_vip, + discount_manual, + discount_gift_card, + discount_rounding, + discount_other, + discount_total, + confirmed_income, + cash_pay_amount, + cash_paper_amount, + scan_pay_amount, + groupbuy_pay_amount, + recharge_cash_inflow, + cash_inflow_total, + cash_outflow_total, + cash_balance_change, + card_consume_total, + recharge_card_consume, + gift_card_consume, + recharge_cash, + first_recharge_cash, + renewal_cash, + order_count, + created_at, + updated_at, + member_order_count -- 新增列(PostgreSQL CREATE OR REPLACE VIEW 限制:只能末尾加) + FROM dws.dws_finance_area_daily + WHERE site_id = (current_setting('app.current_site_id'::text))::bigint; + +COMMENT ON VIEW app.v_dws_finance_area_daily IS + '区域级财务日粒度 RLS 视图 | app2a 区域财务洞察数据源 | 2026-04-23 新增 member_order_count'; + +-- ------------------------------------------------------------ +-- Step 3: 重建 dws schema 镜像 RLS 视图 +-- (按 CLAUDE.md "RLS 视图双 Schema 规则" 要求,dws 和 app 两个 schema 的 RLS 视图必须同步) +-- ------------------------------------------------------------ +CREATE OR REPLACE VIEW dws.v_dws_finance_area_daily AS +SELECT + id, + site_id, + tenant_id, + stat_date, + area_code, + table_fee_amount, + goods_amount, + assistant_pd_amount, + assistant_cx_amount, + gross_amount, + discount_groupbuy, + discount_vip, + discount_manual, + discount_gift_card, + discount_rounding, + discount_other, + discount_total, + confirmed_income, + cash_pay_amount, + cash_paper_amount, + scan_pay_amount, + groupbuy_pay_amount, + recharge_cash_inflow, + cash_inflow_total, + cash_outflow_total, + cash_balance_change, + card_consume_total, + recharge_card_consume, + gift_card_consume, + recharge_cash, + first_recharge_cash, + renewal_cash, + order_count, + created_at, + updated_at, + member_order_count -- 新增列(PostgreSQL CREATE OR REPLACE VIEW 限制:只能末尾加) + FROM dws.dws_finance_area_daily + WHERE site_id = (current_setting('app.current_site_id'::text))::bigint; + +COMMENT ON VIEW dws.v_dws_finance_area_daily IS + '区域级财务日粒度 RLS 视图 (dws schema 镜像) | 与 app.v_dws_finance_area_daily 双 schema 规则要求下同步'; + +COMMIT; + +-- ============================================================= +-- VERIFICATION (事后手工执行) +-- ============================================================= +-- 1. 列存在性 +-- SELECT column_name, data_type, column_default, is_nullable +-- FROM information_schema.columns +-- WHERE table_schema = 'dws' +-- AND table_name = 'dws_finance_area_daily' +-- AND column_name = 'member_order_count'; +-- -- 期望: 1 行, integer, 0, NO +-- +-- 2. 视图暴露新列 +-- SELECT column_name +-- FROM information_schema.columns +-- WHERE table_schema = 'app' +-- AND table_name = 'v_dws_finance_area_daily' +-- AND column_name = 'member_order_count'; +-- -- 期望: 1 行 +-- +-- 3. 数据校验 (ETL loader 改造并跑一次后执行) +-- SET app.current_site_id = <测试门店 ID>; +-- SELECT s.site_id, +-- s.stat_date, +-- s.member_order_count AS full_store_member_orders, +-- COALESCE(SUM(a.member_order_count), 0) AS sum_area_member_orders, +-- s.member_order_count - COALESCE(SUM(a.member_order_count), 0) AS diff +-- FROM app.v_dws_finance_daily_summary s +-- LEFT JOIN app.v_dws_finance_area_daily a +-- ON s.site_id = a.site_id AND s.stat_date = a.stat_date +-- WHERE s.stat_date >= CURRENT_DATE - INTERVAL '7 days' +-- GROUP BY s.site_id, s.stat_date, s.member_order_count +-- HAVING s.member_order_count <> COALESCE(SUM(a.member_order_count), 0); +-- -- 期望: 0 行 (全店会员订单数 = 区域之和) + +-- ============================================================= +-- ROLLBACK +-- ============================================================= +-- BEGIN; +-- DROP VIEW IF EXISTS app.v_dws_finance_area_daily CASCADE; +-- DROP VIEW IF EXISTS dws.v_dws_finance_area_daily CASCADE; +-- ALTER TABLE dws.dws_finance_area_daily DROP COLUMN IF EXISTS member_order_count; +-- -- 重新 CREATE VIEW app.v_dws_finance_area_daily (不含新列) - 从 db/etl_feiqiu/schemas/app.sql:714 复制 +-- COMMIT; diff --git a/db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql b/db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql new file mode 100644 index 0000000..4c355b5 --- /dev/null +++ b/db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql @@ -0,0 +1,70 @@ +-- ============================================================= +-- Migration: 20260423__ai_cache_allow_app2a.sql +-- Purpose: 放开 biz.ai_cache.chk_ai_cache_type CHECK 约束, +-- 允许新 cache_type 'app2a_finance_area'(app2a 区域财务洞察) +-- Related: docs/ai/app2_finance_multi_app_design.md v2 · Phase F +-- Author: Claude + Neo +-- ============================================================= +-- +-- 改动说明: +-- 1. DROP 旧 CHECK 约束 chk_ai_cache_type +-- 2. CREATE 新 CHECK 约束,加入 'app2a_finance_area' 值 +-- +-- 兼容性: +-- - 旧 cache_type 值全部保留,仅新增一个允许值 +-- - 无需数据迁移 +-- +-- 回滚策略: +-- 见文件末 ROLLBACK 段 +-- +-- 验证 SQL: +-- 见文件末 VERIFICATION 段 +-- ============================================================= + +BEGIN; + +-- 1. DROP 旧约束 +ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_type; + +-- 2. CREATE 新约束(加入 app2a_finance_area) +ALTER TABLE biz.ai_cache ADD CONSTRAINT chk_ai_cache_type + CHECK (cache_type IN ( + 'app2_finance', + 'app2a_finance_area', -- 2026-04-23 新增(区域财务洞察 · 64 组合) + 'app3_clue', + 'app4_analysis', + 'app5_tactics', + 'app6_note_analysis', + 'app7_customer_analysis', + 'app8_clue_consolidated' + )); + +COMMIT; + +-- ============================================================= +-- VERIFICATION(事后手工执行) +-- ============================================================= +-- 1. 约束允许值检查 +-- SELECT pg_get_constraintdef(oid) +-- FROM pg_constraint +-- WHERE conname = 'chk_ai_cache_type'; +-- -- 期望:返回的 CHECK 里包含 'app2a_finance_area' +-- +-- 2. 插入新 cache_type 应成功 +-- INSERT INTO biz.ai_cache (cache_type, site_id, target_id, result_json, status, expires_at) +-- VALUES ('app2a_finance_area', 1, 'test__all', '{}', 'valid', NOW() + INTERVAL '1 day'); +-- -- 期望:成功插入 +-- DELETE FROM biz.ai_cache WHERE cache_type = 'app2a_finance_area' AND site_id = 1 AND target_id = 'test__all'; + +-- ============================================================= +-- ROLLBACK +-- ============================================================= +-- BEGIN; +-- ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_type; +-- ALTER TABLE biz.ai_cache ADD CONSTRAINT chk_ai_cache_type +-- CHECK (cache_type IN ( +-- 'app2_finance', 'app3_clue', 'app4_analysis', 'app5_tactics', +-- 'app6_note_analysis', 'app7_customer_analysis', 'app8_clue_consolidated' +-- )); +-- COMMIT; +-- 注:回滚前必须先 DELETE 所有 cache_type = 'app2a_finance_area' 的记录,否则 CHECK 会失败 diff --git a/docs/database/changes/2026-04-23__ai_cache_allow_app2a.md b/docs/database/changes/2026-04-23__ai_cache_allow_app2a.md new file mode 100644 index 0000000..9477424 --- /dev/null +++ b/docs/database/changes/2026-04-23__ai_cache_allow_app2a.md @@ -0,0 +1,120 @@ +# 数据库变更:ai_cache.chk_ai_cache_type 约束放开 app2a_finance_area + +> 日期:2026-04-23 +> 迁移脚本:[db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql](../../db/zqyy_app/migrations/20260423__ai_cache_allow_app2a.sql) +> 相关设计:[docs/ai/app2_finance_multi_app_design.md](../ai/app2_finance_multi_app_design.md) · Phase F +> 涉及库:`zqyy_app`(biz schema) +> 风险等级:**低**(仅放开约束,不影响已有数据) + +--- + +## 1 · 变更说明 + +### 约束变更 + +| Schema.Table | 约束名 | 变更类型 | 允许值 | +|---|---|---|---| +| `biz.ai_cache` | `chk_ai_cache_type` | **替换**(DROP + CREATE) | 旧 7 项 + 新增 `app2a_finance_area` | + +**新约束完整定义**: +```sql +CHECK (cache_type IN ( + 'app2_finance', + 'app2a_finance_area', -- 新增 + 'app3_clue', + 'app4_analysis', + 'app5_tactics', + 'app6_note_analysis', + 'app7_customer_analysis', + 'app8_clue_consolidated' +)) +``` + +### 其他检查 + +- `ai_run_logs.app_type` · **无 CHECK 约束**(VARCHAR(30)),无需改动 +- `ai_trigger_jobs.*` · **无相关 CHECK 约束**,无需改动 + +--- + +## 2 · 兼容性影响 + +### 对已有数据 + +- 旧 7 项 cache_type 值全部保留,现有数据不动 +- 无需数据迁移 + +### 对后端代码 + +- `CacheTypeEnum.APP2A_FINANCE_AREA` 枚举已在 `apps/backend/app/ai/schemas.py` 添加 +- `AICacheService.write_cache(cache_type='app2a_finance_area', ...)` 本约束放开后即可成功写入 +- `_SUPPORTED_APP_TYPES` / `dispatcher.run_single_app` / `build_app2a_area_prompt` 链路在 Phase C 已完成 + +### 对小程序 + +- [board-finance.ts](../../apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts) `_loadAIInsights` 已按 area 动态选 `cache_type` +- 后端 xcx API `/api/ai/cache/{cache_type}` 白名单自动涵盖(基于 `CacheTypeEnum`) + +--- + +## 3 · 回滚策略 + +### 回滚前置条件 + +**必须先删除所有 `cache_type = 'app2a_finance_area'` 的记录**,否则 CHECK 会拒绝。 + +```sql +-- 回滚预备 +DELETE FROM biz.ai_cache WHERE cache_type = 'app2a_finance_area'; + +-- 回滚 DDL +BEGIN; +ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_type; +ALTER TABLE biz.ai_cache ADD CONSTRAINT chk_ai_cache_type + CHECK (cache_type IN ( + 'app2_finance', 'app3_clue', 'app4_analysis', 'app5_tactics', + 'app6_note_analysis', 'app7_customer_analysis', 'app8_clue_consolidated' + )); +COMMIT; +``` + +--- + +## 4 · 验证 SQL + +### 验证 1 · 约束允许值包含 app2a_finance_area + +```sql +SELECT pg_get_constraintdef(oid) + FROM pg_constraint + WHERE conname = 'chk_ai_cache_type'; +-- 期望:返回的 CHECK 中含 'app2a_finance_area' +``` + +### 验证 2 · 插入新 cache_type 应成功 + +```sql +INSERT INTO biz.ai_cache (cache_type, site_id, target_id, result_json, status, expires_at) +VALUES ('app2a_finance_area', 1, 'test__all', '{}'::jsonb, 'valid', NOW() + INTERVAL '1 day'); +-- 期望:成功插入 +-- 清理: +DELETE FROM biz.ai_cache WHERE cache_type = 'app2a_finance_area' AND site_id = 1 AND target_id = 'test__all'; +``` + +### 验证 3 · 旧 cache_type 插入仍正常 + +```sql +INSERT INTO biz.ai_cache (cache_type, site_id, target_id, result_json, status, expires_at) +VALUES ('app2_finance', 1, 'test__backward_compat', '{}'::jsonb, 'valid', NOW() + INTERVAL '1 day'); +-- 期望:成功插入 +DELETE FROM biz.ai_cache WHERE cache_type = 'app2_finance' AND site_id = 1 AND target_id = 'test__backward_compat'; +``` + +--- + +## 5 · 变更记录 + +| 日期 | 操作 | 执行人 | +|---|---|---| +| 2026-04-23 | 迁移脚本产出 + 测试库执行通过 | Claude + Neo | +| 待定 | 生产库执行 | Neo(上线 checklist) | diff --git a/docs/database/changes/2026-04-23__app2a_member_order_count.md b/docs/database/changes/2026-04-23__app2a_member_order_count.md new file mode 100644 index 0000000..e5d0316 --- /dev/null +++ b/docs/database/changes/2026-04-23__app2a_member_order_count.md @@ -0,0 +1,214 @@ +# 数据库变更:dws_finance_area_daily 增加 member_order_count 列 + +> 日期:2026-04-23 +> 迁移脚本:[db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql](../../db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql) +> 相关设计文档:[docs/ai/app2_finance_multi_app_design.md](../ai/app2_finance_multi_app_design.md) · Phase B +> 涉及库:`etl_feiqiu`(六层 DWS) +> 风险等级:**低**(新增列带默认值,对历史行为无破坏) + +--- + +## 1 · 变更说明 + +### 新增列 + +| Schema | 表 | 列名 | 类型 | 默认值 | Nullable | 说明 | +|---|---|---|---|---|---|---| +| `dws` | `dws_finance_area_daily` | `member_order_count` | `integer` | `0` | `NOT NULL` | 会员订单数(区域粒度,从 DWD 聚合 `is_member_order = true` 的订单计数) | + +### 更新视图 + +| Schema | 视图 | 变更 | +|---|---|---| +| `app` | `v_dws_finance_area_daily` | `CREATE OR REPLACE` · 暴露 `member_order_count` 列(**追加到视图末尾**)· RLS 过滤保持 | +| `dws` | `v_dws_finance_area_daily` | `CREATE OR REPLACE` · 暴露 `member_order_count` 列(**追加到视图末尾**)· RLS 过滤保持 · 遵守双 schema 规则 | + +**列顺序说明**:PostgreSQL `CREATE OR REPLACE VIEW` 仅允许在视图**末尾**追加列,不能在中间插入。因此 `member_order_count` 放在 `updated_at` 之后(而非紧邻 `order_count`)。列顺序不影响语义,后端查询按列名访问。 + +### 新增文档 + +无(本文档即为同步文档) + +--- + +## 2 · 兼容性影响 + +### 对 ETL(飞球 Connector) + +- **必须改造** · `apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily*.py`(对应 loader) + - 聚合 DWD 订单数据时按 `(area_code, stat_date)` 分组 + - 将 `is_member_order = true` 的订单数归入 `member_order_count` 列 + - 上线前 ETL 未改造时历史与新增数据均为 `0`(接受降级) + +### 对后端 API + +- **新增 · 向后兼容** · 后端查询 `app.v_dws_finance_area_daily` 选取列时增加 `member_order_count` 即可访问 +- 已知消费者:`apps/backend/app/ai/prompts/app2a_finance_area_prompt.py`(新建,本设计文档 Phase C 实施) +- 其他读 `v_dws_finance_area_daily` 的代码:未读新列则无影响 + +### 对小程序字段映射 + +- 无直接影响(小程序不直接读 DWS 视图,通过后端 `/api/board/finance` 中转) +- P2 上线后,面板的"区域会员订单占比"派生字段会生效(经 app2a prompt 内部计算,不通过 board API) + +### 对 FDW 跨库映射 + +- `etl_feiqiu.app.v_dws_finance_area_daily` 通过 FDW 映射到 `zqyy_app.etl_app` schema(若已映射),需确认 FDW 侧是否要重导(FDW foreign table 的列定义可能需同步) +- **验证项**:`zqyy_app` 端查询 `etl_app.v_dws_finance_area_daily` 是否能看到新列,若不能,需重跑 FDW 刷新脚本 `db/fdw/` 下对应 remap 脚本 + +--- + +## 3 · 回滚策略 + +### 回滚步骤(按顺序执行) + +```sql +BEGIN; + +-- 1. 先 DROP 两个 RLS 视图(否则 DROP COLUMN 会因依赖失败) +DROP VIEW IF EXISTS app.v_dws_finance_area_daily; +DROP VIEW IF EXISTS dws.v_dws_finance_area_daily; + +-- 2. DROP 新列 +ALTER TABLE dws.dws_finance_area_daily + DROP COLUMN IF EXISTS member_order_count; + +-- 3. 重建两个 RLS 视图(去掉 member_order_count · 从 db/etl_feiqiu/schemas/app.sql:714 复制原定义) +-- 注意:因 member_order_count 在原视图中是末尾追加,回滚时只需去掉该列即可 +-- app schema +CREATE OR REPLACE VIEW app.v_dws_finance_area_daily AS +SELECT id, site_id, tenant_id, stat_date, area_code, + table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount, + gross_amount, + discount_groupbuy, discount_vip, discount_manual, + discount_gift_card, discount_rounding, discount_other, discount_total, + confirmed_income, + cash_pay_amount, cash_paper_amount, scan_pay_amount, groupbuy_pay_amount, + recharge_cash_inflow, cash_inflow_total, cash_outflow_total, cash_balance_change, + card_consume_total, recharge_card_consume, gift_card_consume, + recharge_cash, first_recharge_cash, renewal_cash, + order_count, + created_at, updated_at + FROM dws.dws_finance_area_daily + WHERE site_id = (current_setting('app.current_site_id'::text))::bigint; + +-- dws schema(双 schema 规则) +CREATE OR REPLACE VIEW dws.v_dws_finance_area_daily AS +SELECT id, site_id, tenant_id, stat_date, area_code, + table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount, + gross_amount, + discount_groupbuy, discount_vip, discount_manual, + discount_gift_card, discount_rounding, discount_other, discount_total, + confirmed_income, + cash_pay_amount, cash_paper_amount, scan_pay_amount, groupbuy_pay_amount, + recharge_cash_inflow, cash_inflow_total, cash_outflow_total, cash_balance_change, + card_consume_total, recharge_card_consume, gift_card_consume, + recharge_cash, first_recharge_cash, renewal_cash, + order_count, + created_at, updated_at + FROM dws.dws_finance_area_daily + WHERE site_id = (current_setting('app.current_site_id'::text))::bigint; + +COMMIT; +``` + +### 回滚前置条件 + +- 确认无正在运行的 ETL loader 写入 `member_order_count` +- 确认 `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` 已 `git revert`(否则查询缺列会报错) +- 确认 FDW 映射同步回滚(若 FDW foreign table 显式列出 member_order_count,需重建 foreign table) + +### 数据回填回滚 + +- 新列默认 0 · 无需数据回填即可回滚 +- 若已执行过 `scripts/ops/backfill_area_member_order.py`(未来脚本),回滚后历史数据会丢失 member_order_count 值 · 但 DWD 原始订单数据在 `dwd.dwd_orders` 保留 · 可从 DWD 重新回填 + +--- + +## 4 · 验证 SQL + +### 验证 1 · 列存在性 + +```sql +SELECT column_name, + data_type, + column_default, + is_nullable + FROM information_schema.columns + WHERE table_schema = 'dws' + AND table_name = 'dws_finance_area_daily' + AND column_name = 'member_order_count'; + +-- 期望:1 行 · integer · 0 · NO +``` + +### 验证 2 · 双 schema RLS 视图暴露新列 + +```sql +SELECT table_schema, table_name, column_name + FROM information_schema.columns + WHERE table_name = 'v_dws_finance_area_daily' + AND column_name = 'member_order_count' + ORDER BY table_schema; + +-- 期望:2 行 · (app, v_dws_finance_area_daily, member_order_count) + (dws, v_dws_finance_area_daily, member_order_count) +``` + +### 验证 3 · 全店 = 区域和(ETL loader 改造完成后执行) + +```sql +-- 设置测试门店 ID +SET app.current_site_id = 2790685415443269; + +-- 比较最近 7 天 全店会员订单数 vs 区域会员订单数之和 +SELECT s.site_id, + s.stat_date, + s.member_order_count AS full_store_member_orders, + COALESCE(SUM(a.member_order_count), 0) AS sum_area_member_orders, + s.member_order_count - COALESCE(SUM(a.member_order_count), 0) AS diff + FROM app.v_dws_finance_daily_summary s + LEFT JOIN app.v_dws_finance_area_daily a + ON s.site_id = a.site_id + AND s.stat_date = a.stat_date + WHERE s.stat_date >= CURRENT_DATE - INTERVAL '7 days' + GROUP BY s.site_id, s.stat_date, s.member_order_count +HAVING s.member_order_count <> COALESCE(SUM(a.member_order_count), 0); + +-- 期望:0 行(全店会员订单数 = 所有区域之和) +-- 注:若 ETL loader 尚未改造,此 SQL 返回若干行属正常降级状态(区域侧全为 0) +``` + +### 验证 4 · RLS 过滤正常工作 + +```sql +-- 未设置 app.current_site_id 时应返回空 +RESET app.current_site_id; +SELECT COUNT(*) FROM app.v_dws_finance_area_daily; +-- 期望:0 行 或 报错 "current_setting not found" + +-- 设置后应返回该门店数据 +SET app.current_site_id = 2790685415443269; +SELECT site_id, COUNT(*) FROM app.v_dws_finance_area_daily GROUP BY site_id; +-- 期望:1 行(仅 site_id = 2790685415443269) +``` + +--- + +## 5 · 关联变更 + +| 关联项 | 状态 | 说明 | +|---|---|---| +| ETL loader 改造 | ⏳ 待实施 · Phase B2 | `apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily*.py` 按 `is_member_order` 聚合 | +| `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` | ⏳ 待实施 · Phase C | 消费新列计算区域级会员占比 | +| FDW 跨库映射刷新 | ⏳ 待确认 | 若 `zqyy_app` 端通过 FDW 访问 `etl_feiqiu.app.v_dws_finance_area_daily`,需确认 foreign table 是否需重建 | +| DWS 快照 DDL 同步 | ⏳ 待执行 | 本迁移合并后运行 `PYTHONUTF8=1 python tools/db/gen_consolidated_ddl.py` 刷新 `docs/database/ddl/` 快照 | + +--- + +## 6 · 变更记录 + +| 日期 | 操作 | 执行人 | +|---|---|---| +| 2026-04-23 | 迁移脚本产出 | Claude + Neo | +| 待定 | 测试库执行 + 验证 SQL 通过 | Neo | +| 待定 | 生产库执行 | Neo |