微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -0,0 +1,203 @@
# 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 精度扩展P1ODS 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-01ODS 新增 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
```

View File

@@ -0,0 +1,166 @@
# BD_Manualbiz Schema AI 表(对话记录 + 消息 + 缓存)
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:`db/zqyy_app/migrations/2026-03-08__create_ai_tables.sql`
> 关联 SPEC`05-miniapp-ai-integration`P5 AI 集成层)
---
## 1. 变更说明
### 新增表3 张)
| # | 表名 | 用途 | 字段数 |
|---|------|------|--------|
| 1 | `biz.ai_conversations` | AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条 | 8 |
| 2 | `biz.ai_messages` | AI 消息记录:对话中的每条消息(输入/输出/系统) | 6 |
| 3 | `biz.ai_cache` | AI 应用缓存:各应用的结构化输出结果 | 9 |
### 表字段明细
#### biz.ai_conversations8 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `user_id` | VARCHAR(50) | NOT NULL | 用户 ID系统自动调用时为 `system` |
| `nickname` | VARCHAR(100) | NOT NULL DEFAULT '' | 用户昵称 |
| `app_id` | VARCHAR(30) | NOT NULL | 应用标识app1_chat / app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note / app7_customer / app8_consolidation |
| `site_id` | BIGINT | NOT NULL | 门店 ID多门店隔离 |
| `source_page` | VARCHAR(100) | 可空 | 来源页面标识 |
| `source_context` | JSONB | 可空 | 页面上下文 JSON |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
#### biz.ai_messages6 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `conversation_id` | BIGINT | NOT NULL, FK → `biz.ai_conversations(id)` ON DELETE CASCADE | 关联对话 |
| `role` | VARCHAR(10) | NOT NULL, CHECK IN ('user', 'assistant', 'system') | 消息角色 |
| `content` | TEXT | NOT NULL | 消息内容 |
| `tokens_used` | INTEGER | 可空 | 本条消息消耗的 token 数 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
#### biz.ai_cache9 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `cache_type` | VARCHAR(30) | NOT NULL, CHECK 7 个枚举值 | 缓存类型(见下方枚举) |
| `site_id` | BIGINT | NOT NULL | 门店 ID多门店隔离 |
| `target_id` | VARCHAR(100) | NOT NULL | 目标 ID含义因 cache_type 而异 |
| `result_json` | JSONB | NOT NULL | 结构化输出结果 |
| `score` | INTEGER | 可空 | 评分:仅应用 6 使用1-10 分) |
| `triggered_by` | VARCHAR(100) | 可空 | 触发来源标识 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `expires_at` | TIMESTAMPTZ | 可空 | 可选过期时间 |
### cache_type 枚举值与 target_id 约定
| cache_type | target_id 格式 | 示例 |
|---|---|---|
| `app2_finance` | 时间维度编码 | `this_month`, `last_week` |
| `app3_clue` | member_id | `12345` |
| `app4_analysis` | `{assistant_id}_{member_id}` | `100_12345` |
| `app5_tactics` | `{assistant_id}_{member_id}` | `100_12345` |
| `app6_note_analysis` | member_id | `12345` |
| `app7_customer_analysis` | member_id | `12345` |
| `app8_clue_consolidated` | member_id | `12345` |
### 约束与索引
| 表 | 约束/索引名 | 类型 | 说明 |
|----|-----------|------|------|
| `ai_conversations` | `idx_ai_conv_user_site` | INDEX | `(user_id, site_id, created_at DESC)` — 用户历史对话列表查询 |
| `ai_conversations` | `idx_ai_conv_app_site` | INDEX | `(app_id, site_id, created_at DESC)` — 按应用查询对话 |
| `ai_messages` | FK `conversation_id` | FK | → `biz.ai_conversations(id)` ON DELETE CASCADE |
| `ai_messages` | `chk_ai_msg_role` | CHECK | `role IN ('user', 'assistant', 'system')` |
| `ai_messages` | `idx_ai_msg_conv` | INDEX | `(conversation_id, created_at)` — 对话消息列表 |
| `ai_cache` | `chk_ai_cache_type` | CHECK | 7 个枚举值 |
| `ai_cache` | `idx_ai_cache_lookup` | INDEX | `(cache_type, site_id, target_id, created_at DESC)` — 查询最新缓存 |
| `ai_cache` | `idx_ai_cache_cleanup` | INDEX | `(cache_type, site_id, target_id, created_at)` — 清理超限记录ASC 排序便于删除最旧) |
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。AI 表属于 `biz` Schema不参与 ETL 流程 |
| 后端 API | 直接依赖。P5 AI 模块(`apps/backend/app/ai/`将基于这三张表实现对话持久化、缓存读写、SSE 流式对话等功能 |
| 小程序 | 间接依赖。小程序通过后端 AI API 间接使用对话和缓存数据 |
| 管理后台 | 暂无影响。后续可能增加 AI 调用统计和缓存管理界面 |
| `member_retention_clue` | 间接关联。App8维客线索整理的结果同时写入 `ai_cache``member_retention_clue` 表 |
| 现有 `biz` Schema | 兼容。仅新增 3 张表不修改已有对象coach_tasks、notes、trigger_jobs 等) |
---
## 3. 回滚策略
按逆序 DROPCASCADE 处理外键依赖):
```sql
-- 删除索引
DROP INDEX IF EXISTS biz.idx_ai_cache_cleanup;
DROP INDEX IF EXISTS biz.idx_ai_cache_lookup;
DROP INDEX IF EXISTS biz.idx_ai_msg_conv;
DROP INDEX IF EXISTS biz.idx_ai_conv_app_site;
DROP INDEX IF EXISTS biz.idx_ai_conv_user_site;
-- 删除表ai_messages 依赖 ai_conversations需先删或用 CASCADE
DROP TABLE IF EXISTS biz.ai_cache CASCADE;
DROP TABLE IF EXISTS biz.ai_messages CASCADE;
DROP TABLE IF EXISTS biz.ai_conversations CASCADE;
```
注意:
- `ai_messages` 通过 FK 依赖 `ai_conversations`CASCADE 会级联删除消息
- 如果表中已有数据,需先备份再执行回滚
- 回滚不会删除 `biz` Schema 本身
---
## 4. 验证 SQL
```sql
-- 1. 验证 3 张 AI 表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'biz'
AND table_name IN ('ai_conversations', 'ai_messages', 'ai_cache')
ORDER BY table_name;
-- 预期:返回 3 行ai_cache, ai_conversations, ai_messages
-- 2. 验证 ai_conversations 字段数量和关键字段
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
ORDER BY ordinal_position;
-- 预期:返回 8 行,包含 id/user_id/nickname/app_id/site_id/source_page/source_context/created_at
-- 3. 验证 ai_messages 的外键和 CHECK 约束
SELECT conname, contype, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_messages'::regclass
ORDER BY conname;
-- 预期:包含 chk_ai_msg_roleCHECK role IN ...)和 ai_messages_conversation_id_fkeyFK → ai_conversations
-- 4. 验证 ai_cache 的 CHECK 约束7 个枚举值)
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_cache'::regclass AND contype = 'c';
-- 预期:返回 1 行 chk_ai_cache_type包含 7 个枚举值
-- 5. 验证索引全部存在5 个)
SELECT indexname
FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN (
'idx_ai_conv_user_site', 'idx_ai_conv_app_site',
'idx_ai_msg_conv',
'idx_ai_cache_lookup', 'idx_ai_cache_cleanup'
)
ORDER BY indexname;
-- 预期:返回 5 行
```

View File

@@ -94,6 +94,7 @@
| 日期 | 字段 | 修正内容 |
|------|------|---------|
| 2026-02-20 | `site_assistant_id` | ODS 源从 `order_assistant_id`(订单级 ID修正为 `site_assistant_id`(助教档案 ID |
| 2026-02-26 | (下游)`table_area_name` | DWS 任务 `_extract_service_records()` 原错误引用 `asl.table_area_name`(本表无此列),改为 JOIN `dwd.dim_table``site_table_area_name`。详见 `BD_Manual_fix_dws_assistant_daily_table_area.md` |
---

View File

@@ -0,0 +1,186 @@
# BD_Manualdws.biz_date() 函数与物化视图营业日重建
> 变更类型:新增函数 + 重建物化视图
> Schema`dws`
> 迁移脚本 1`db/etl_feiqiu/migrations/2026-02-27__add_biz_date_function.sql`
> 迁移脚本 2`db/etl_feiqiu/migrations/2026-02-27__rebuild_mv_with_biz_date.sql`
> DDL 基线:`docs/database/ddl/etl_feiqiu__dws.sql`
> 关联需求Requirements 9.1, 9.2, 9.3, 9.4
---
## 1. 变更说明
### 1.1 新增函数:`dws.biz_date(timestamptz, int)`
| 属性 | 值 |
|------|-----|
| 函数签名 | `dws.biz_date(ts timestamptz, cutoff_hour int DEFAULT 8) RETURNS date` |
| 语言 | SQL |
| 特性 | `IMMUTABLE PARALLEL SAFE` |
| 逻辑 | `(ts - make_interval(hours => cutoff_hour))::date` |
| 等价 Python | `neozqyy_shared.datetime_utils.business_date()` |
将时间戳减去 `cutoff_hour` 小时后取日期,实现营业日归属。默认 `cutoff_hour=8`,即 08:00 前的时间戳归属前一天。
### 1.2 重建物化视图8 个)
`CURRENT_DATE` 替换为 `dws.biz_date(NOW())`,使物化视图的数据范围与 DWS 任务的营业日口径一致。
| 物化视图 | 原条件 | 新条件 |
|---------|--------|--------|
| `mv_dws_assistant_daily_detail_l1` | `stat_date >= (CURRENT_DATE - '1 day')` | `stat_date >= (dws.biz_date(NOW()) - '1 day')` |
| `mv_dws_assistant_daily_detail_l2` | `stat_date >= (CURRENT_DATE - '30 days')` | `stat_date >= (dws.biz_date(NOW()) - '30 days')` |
| `mv_dws_assistant_daily_detail_l3` | `stat_date >= (CURRENT_DATE - '90 days')` | `stat_date >= (dws.biz_date(NOW()) - '90 days')` |
| `mv_dws_assistant_daily_detail_l4` | `date_trunc('month', CURRENT_DATE) ± 6 mons` | `date_trunc('month', dws.biz_date(NOW())) ± 6 mons` |
| `mv_dws_finance_daily_summary_l1` | `stat_date >= (CURRENT_DATE - '1 day')` | `stat_date >= (dws.biz_date(NOW()) - '1 day')` |
| `mv_dws_finance_daily_summary_l2` | `stat_date >= (CURRENT_DATE - '30 days')` | `stat_date >= (dws.biz_date(NOW()) - '30 days')` |
| `mv_dws_finance_daily_summary_l3` | `stat_date >= (CURRENT_DATE - '90 days')` | `stat_date >= (dws.biz_date(NOW()) - '90 days')` |
| `mv_dws_finance_daily_summary_l4` | `date_trunc('month', CURRENT_DATE) ± 6 mons` | `date_trunc('month', dws.biz_date(NOW())) ± 6 mons` |
索引在重建后重新创建,结构不变。
---
## 2. 兼容性说明
| 影响范围 | 说明 |
|---------|------|
| ETL 任务 | `MvRefreshTask``DWS_MV_REFRESH_*`)执行 `REFRESH MATERIALIZED VIEW` 不受影响,视图定义变更对刷新逻辑透明 |
| 后端 API | 无直接影响。后端通过 DWS 表查询,物化视图仅用于加速查询 |
| 管理后台 | 无影响。前端不直接查询物化视图 |
| 小程序 | 无影响 |
| 字段映射 | 物化视图列结构不变(`SELECT *`),仅 WHERE 条件变更 |
| `biz_date()` 函数 | 标记为 `IMMUTABLE`,可安全用于索引表达式和物化视图定义 |
---
## 3. 回滚策略
### 3.1 回滚函数
```sql
DROP FUNCTION IF EXISTS dws.biz_date(timestamptz, int);
```
> 注意需先回滚物化视图3.2),否则依赖此函数的视图定义会阻止删除。
### 3.2 回滚物化视图(恢复自然日口径)
使用 `scripts/migrate/migrate_finalize.py` 中的原始定义重建:
```sql
BEGIN;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_assistant_daily_detail_l1;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_assistant_daily_detail_l2;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_assistant_daily_detail_l3;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_assistant_daily_detail_l4;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_finance_daily_summary_l1;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_finance_daily_summary_l2;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_finance_daily_summary_l3;
DROP MATERIALIZED VIEW IF EXISTS dws.mv_dws_finance_daily_summary_l4;
-- 用原始定义重建CURRENT_DATE 版本)
CREATE MATERIALIZED VIEW dws.mv_dws_assistant_daily_detail_l1 AS
SELECT * FROM dws.dws_assistant_daily_detail
WHERE stat_date >= (CURRENT_DATE - '1 day'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_assistant_daily_detail_l2 AS
SELECT * FROM dws.dws_assistant_daily_detail
WHERE stat_date >= (CURRENT_DATE - '30 days'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_assistant_daily_detail_l3 AS
SELECT * FROM dws.dws_assistant_daily_detail
WHERE stat_date >= (CURRENT_DATE - '90 days'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_assistant_daily_detail_l4 AS
SELECT * FROM dws.dws_assistant_daily_detail
WHERE stat_date >= (date_trunc('month', CURRENT_DATE::timestamptz) - '6 mons'::interval)
AND stat_date < date_trunc('month', CURRENT_DATE::timestamptz) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_finance_daily_summary_l1 AS
SELECT * FROM dws.dws_finance_daily_summary
WHERE stat_date >= (CURRENT_DATE - '1 day'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_finance_daily_summary_l2 AS
SELECT * FROM dws.dws_finance_daily_summary
WHERE stat_date >= (CURRENT_DATE - '30 days'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_finance_daily_summary_l3 AS
SELECT * FROM dws.dws_finance_daily_summary
WHERE stat_date >= (CURRENT_DATE - '90 days'::interval) WITH DATA;
CREATE MATERIALIZED VIEW dws.mv_dws_finance_daily_summary_l4 AS
SELECT * FROM dws.dws_finance_daily_summary
WHERE stat_date >= (date_trunc('month', CURRENT_DATE::timestamptz) - '6 mons'::interval)
AND stat_date < date_trunc('month', CURRENT_DATE::timestamptz) WITH DATA;
-- 重建索引
CREATE INDEX idx_mv_assistant_daily_l1 ON dws.mv_dws_assistant_daily_detail_l1 USING btree (site_id, stat_date, assistant_id);
CREATE INDEX idx_mv_assistant_daily_l2 ON dws.mv_dws_assistant_daily_detail_l2 USING btree (site_id, stat_date, assistant_id);
CREATE INDEX idx_mv_assistant_daily_l3 ON dws.mv_dws_assistant_daily_detail_l3 USING btree (site_id, stat_date, assistant_id);
CREATE INDEX idx_mv_assistant_daily_l4 ON dws.mv_dws_assistant_daily_detail_l4 USING btree (site_id, stat_date, assistant_id);
CREATE INDEX idx_mv_finance_daily_l1 ON dws.mv_dws_finance_daily_summary_l1 USING btree (site_id, stat_date);
CREATE INDEX idx_mv_finance_daily_l2 ON dws.mv_dws_finance_daily_summary_l2 USING btree (site_id, stat_date);
CREATE INDEX idx_mv_finance_daily_l3 ON dws.mv_dws_finance_daily_summary_l3 USING btree (site_id, stat_date);
CREATE INDEX idx_mv_finance_daily_l4 ON dws.mv_dws_finance_daily_summary_l4 USING btree (site_id, stat_date);
COMMIT;
```
---
## 4. 验证 SQL
### 4.1 确认 `biz_date()` 函数存在且行为正确
```sql
-- 08:00 前归属前一天
SELECT dws.biz_date('2026-01-15 07:59:59+08'::timestamptz) AS should_be_0114;
-- 预期2026-01-14
-- 08:00 起归属当天
SELECT dws.biz_date('2026-01-15 08:00:00+08'::timestamptz) AS should_be_0115;
-- 预期2026-01-15
-- 月末边界
SELECT dws.biz_date('2026-02-01 07:00:00+08'::timestamptz, 8) AS should_be_0131;
-- 预期2026-01-31
```
### 4.2 确认 8 个物化视图已重建且定义包含 `biz_date`
```sql
SELECT matviewname, definition LIKE '%biz_date%' AS uses_biz_date
FROM pg_matviews
WHERE schemaname = 'dws'
AND matviewname LIKE 'mv_dws_%'
ORDER BY matviewname;
-- 预期8 行uses_biz_date 全部为 true
```
### 4.3 确认物化视图索引完整
```sql
SELECT indexname, tablename
FROM pg_indexes
WHERE schemaname = 'dws'
AND tablename LIKE 'mv_dws_%'
ORDER BY indexname;
-- 预期8 个索引assistant_daily l1-l4 + finance_daily l1-l4
```
### 4.4 确认物化视图有数据(刷新后)
```sql
SELECT 'assistant_l1' AS mv, COUNT(*) FROM dws.mv_dws_assistant_daily_detail_l1
UNION ALL
SELECT 'assistant_l2', COUNT(*) FROM dws.mv_dws_assistant_daily_detail_l2
UNION ALL
SELECT 'assistant_l3', COUNT(*) FROM dws.mv_dws_assistant_daily_detail_l3
UNION ALL
SELECT 'assistant_l4', COUNT(*) FROM dws.mv_dws_assistant_daily_detail_l4
UNION ALL
SELECT 'finance_l1', COUNT(*) FROM dws.mv_dws_finance_daily_summary_l1
UNION ALL
SELECT 'finance_l2', COUNT(*) FROM dws.mv_dws_finance_daily_summary_l2
UNION ALL
SELECT 'finance_l3', COUNT(*) FROM dws.mv_dws_finance_daily_summary_l3
UNION ALL
SELECT 'finance_l4', COUNT(*) FROM dws.mv_dws_finance_daily_summary_l4;
```

View File

@@ -0,0 +1,215 @@
# BD_Manualbiz Schema 业务表(助教任务系统 + 备注系统 + 触发器调度)
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:
> - `db/zqyy_app/migrations/2026-02-27__p4_create_biz_tables.sql`(建表)
> - `db/zqyy_app/migrations/2026-02-27__p4_seed_trigger_jobs.sql`(种子数据)
> 关联 SPEC`04-miniapp-core-business`P4 小程序核心业务模块)
---
## 1. 变更说明
### 新增表4 张)
| # | 表名 | 用途 | 字段数 |
|---|------|------|--------|
| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因等 | 15 |
| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃的追溯链 | 9 |
| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 14 |
| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 9 |
### 表字段明细
#### biz.coach_tasks15 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID多门店隔离 |
| `assistant_id` | BIGINT | NOT NULL | 助教 ID |
| `member_id` | BIGINT | NOT NULL | 客户 ID |
| `task_type` | VARCHAR(50) | NOT NULL | 任务类型:`high_priority_recall` / `priority_recall` / `follow_up_visit` / `relationship_building` |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'active' | 状态:`active` / `inactive` / `completed` / `abandoned` |
| `priority_score` | NUMERIC(5,2) | 可空 | 优先级分数,取 `max(WBI, NCI)` 快照 |
| `expires_at` | TIMESTAMPTZ | 可空 | 有效期时间戳NULL 表示无限期 |
| `is_pinned` | BOOLEAN | DEFAULT FALSE | 是否置顶 |
| `abandon_reason` | TEXT | 可空 | 放弃原因(放弃时必填) |
| `completed_at` | TIMESTAMPTZ | 可空 | 完成时间 |
| `completed_task_type` | VARCHAR(50) | 可空 | 完成时的任务类型快照 |
| `parent_task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 父任务 ID自引用 |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
#### biz.coach_task_history9 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `task_id` | BIGINT | NOT NULL, FK → `biz.coach_tasks(id)` | 关联任务 |
| `action` | VARCHAR(50) | NOT NULL | 操作类型:`created` / `type_changed` / `pinned` / `abandoned` / `cancel_abandon` / `expired` / `completed` |
| `old_status` | VARCHAR(20) | 可空 | 变更前状态 |
| `new_status` | VARCHAR(20) | 可空 | 变更后状态 |
| `old_task_type` | VARCHAR(50) | 可空 | 变更前任务类型 |
| `new_task_type` | VARCHAR(50) | 可空 | 变更后任务类型 |
| `detail` | JSONB | 可空 | 附加详情(如放弃原因等) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 记录时间 |
#### biz.notes14 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `user_id` | INTEGER | NOT NULL | 小程序用户 ID |
| `target_type` | VARCHAR(50) | NOT NULL | 目标类型(如 `member` |
| `target_id` | BIGINT | NOT NULL | 目标 ID如 member_id |
| `type` | VARCHAR(20) | NOT NULL DEFAULT 'normal' | 备注类型:`normal` / `follow_up` / `abandon_reason` |
| `content` | TEXT | NOT NULL | 备注内容 |
| `rating_service_willingness` | SMALLINT | CHECK (1-5),可空 | 再次服务意愿评分 |
| `rating_revisit_likelihood` | SMALLINT | CHECK (1-5),可空 | 再来店可能性评分 |
| `task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 关联任务 |
| `ai_score` | SMALLINT | 可空 | AI 应用 6 评分P5 实现) |
| `ai_analysis` | TEXT | 可空 | AI 分析结果P5 实现) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
#### biz.trigger_jobs9 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | SERIAL | PK | 自增主键 |
| `job_type` | VARCHAR(100) | NOT NULL | 任务类型标识,映射到 Python handler |
| `job_name` | VARCHAR(100) | NOT NULL, UNIQUE | 任务名称(唯一) |
| `trigger_condition` | VARCHAR(20) | NOT NULL | 触发方式:`cron` / `interval` / `event` |
| `trigger_config` | JSONB | NOT NULL | 触发配置cron 表达式 / 间隔秒数 / 事件名) |
| `last_run_at` | TIMESTAMPTZ | 可空 | 上次运行时间 |
| `next_run_at` | TIMESTAMPTZ | 可空 | 下次运行时间event 类型为 NULL |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'enabled' | 状态:`enabled` / `disabled` |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### 约束与索引
| 表 | 约束/索引名 | 类型 | 说明 |
|----|-----------|------|------|
| `coach_tasks` | `idx_coach_tasks_site_assistant_member_type` | UNIQUE INDEX (partial) | `(site_id, assistant_id, member_id, task_type) WHERE status = 'active'`,保证同一组合下活跃任务最多一条 |
| `coach_tasks` | `idx_coach_tasks_assistant_status` | INDEX | `(site_id, assistant_id, status)`,助教任务列表查询加速 |
| `coach_tasks` | FK `parent_task_id` | FK | → `biz.coach_tasks(id)`,自引用 |
| `coach_task_history` | FK `task_id` | FK | → `biz.coach_tasks(id)` |
| `notes` | `idx_notes_target` | INDEX | `(site_id, target_type, target_id)`,按目标查询备注加速 |
| `notes` | CHECK `rating_service_willingness` | CHECK | `BETWEEN 1 AND 5` |
| `notes` | CHECK `rating_revisit_likelihood` | CHECK | `BETWEEN 1 AND 5` |
| `notes` | FK `task_id` | FK | → `biz.coach_tasks(id)` |
| `trigger_jobs` | UNIQUE `job_name` | UNIQUE | 触发器名称唯一 |
### 种子数据4 条触发器配置)
| job_name | job_type | trigger_condition | trigger_config | next_run_at | 说明 |
|----------|----------|-------------------|----------------|-------------|------|
| `task_generator` | `task_generator` | `cron` | `{"cron_expression": "0 4 * * *"}` | 次日 04:00 | 每日凌晨 4:00 运行任务生成器 |
| `task_expiry_check` | `task_expiry_check` | `interval` | `{"interval_seconds": 3600}` | NOW() + 1h | 每小时检查过期任务 |
| `recall_completion_check` | `recall_completion_check` | `event` | `{"event_name": "etl_data_updated"}` | NULL | ETL 数据更新后触发召回完成检测 |
| `note_reclassify_backfill` | `note_reclassify_backfill` | `event` | `{"event_name": "recall_completed"}` | NULL | 召回完成后触发备注回溯重分类 |
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无直接影响。`biz` Schema 表不参与 ETL 流程,但任务生成器通过 FDW 只读访问 ETL 库的 WBI/NCI/RS 指数数据 |
| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现任务 CRUD`/api/xcx/tasks`)、备注 CRUD`/api/xcx/notes`)、触发器调度等功能 |
| 小程序 | 间接依赖。小程序通过后端 API 间接使用任务列表、备注功能 |
| 管理后台 | 暂无影响。后续可能增加任务监控和触发器管理界面 |
| FDW 配置 | 无影响。`fdw_etl` Schema 独立于 `biz`,任务生成器和召回检测器通过 FDW 只读查询 ETL 库 |
| `auth` Schema | 间接依赖。任务生成器通过 `auth.user_assistant_binding` 确定助教与小程序用户的映射关系 |
| `public` Schema | 无影响。`member_retention_clue` 表独立于本次变更 |
| 现有 `biz` Schema | 兼容。`biz` Schema 已由 P1 迁移脚本创建,本次仅在其中新增 4 张表,不修改已有对象 |
---
## 3. 回滚策略
按逆序 `DROP TABLE IF EXISTS CASCADE`(迁移脚本末尾已包含注释形式的回滚语句):
```sql
-- 先删除种子数据(如需保留表结构)
DELETE FROM biz.trigger_jobs
WHERE job_name IN (
'task_generator',
'task_expiry_check',
'recall_completion_check',
'note_reclassify_backfill'
);
-- 删除索引
DROP INDEX IF EXISTS biz.idx_notes_target;
DROP INDEX IF EXISTS biz.idx_coach_tasks_assistant_status;
DROP INDEX IF EXISTS biz.idx_coach_tasks_site_assistant_member_type;
-- 删除表按逆序CASCADE 处理外键依赖)
DROP TABLE IF EXISTS biz.trigger_jobs CASCADE;
DROP TABLE IF EXISTS biz.notes CASCADE;
DROP TABLE IF EXISTS biz.coach_task_history CASCADE;
DROP TABLE IF EXISTS biz.coach_tasks CASCADE;
```
注意:
- `CASCADE` 会级联删除依赖对象(外键引用的子表数据)
- 如果表中已有业务数据,需先备份再执行回滚
- 回滚不会删除 `biz` Schema 本身(由 P1 创建,其他表可能依赖)
---
## 4. 验证 SQL
```sql
-- 1. 验证 biz Schema 下 4 张业务表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'biz'
AND table_name IN ('coach_tasks', 'coach_task_history', 'notes', 'trigger_jobs')
ORDER BY table_name;
-- 预期:返回 4 行coach_task_history, coach_tasks, notes, trigger_jobs
-- 2. 验证 coach_tasks 表字段数量和关键字段
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'coach_tasks'
ORDER BY ordinal_position;
-- 预期:返回 15 行,包含 id/site_id/assistant_id/member_id/task_type/status 等
-- 3. 验证部分唯一索引存在
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'biz' AND indexname = 'idx_coach_tasks_site_assistant_member_type';
-- 预期:返回 1 行indexdef 包含 "WHERE ((status)::text = 'active'::text)"
-- 4. 验证 notes 表的 CHECK 约束(评分 1-5
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.notes'::regclass AND contype = 'c';
-- 预期:返回 2 行,分别约束 rating_service_willingness 和 rating_revisit_likelihood 在 1-5 范围
-- 5. 验证种子数据4 条触发器配置
SELECT job_name, job_type, trigger_condition,
trigger_config->>'cron_expression' AS cron,
trigger_config->>'interval_seconds' AS interval_sec,
trigger_config->>'event_name' AS event
FROM biz.trigger_jobs
WHERE job_name IN ('task_generator', 'task_expiry_check', 'recall_completion_check', 'note_reclassify_backfill')
ORDER BY job_name;
-- 预期:返回 4 行
-- note_reclassify_backfill | event | recall_completed
-- recall_completion_check | event | etl_data_updated
-- task_expiry_check | interval| interval_seconds=3600
-- task_generator | cron | 0 4 * * *
-- 6. 验证查询索引存在
SELECT indexname
FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN ('idx_coach_tasks_assistant_status', 'idx_notes_target')
ORDER BY indexname;
-- 预期:返回 2 行
```

View File

@@ -0,0 +1,88 @@
# BD_Manualdim_groupbuy_package_ex 新增团购详情字段
> 日期2026-03-05
> 涉及库:`etl_feiqiu` / `test_etl_feiqiu`
> 迁移脚本:`db/etl_feiqiu/migrations/2026-03-05__add_detail_fields_to_dim_groupbuy_package_ex.sql`
> 直接原因整合团购详情接口QueryPackageCouponInfo需在 DWD 扩展表中存储可用台区、助教服务、关联门店等维度信息
> Prompt 摘要etl-coupon-detail spec — 需求 4 验收标准 1
---
## 1. 变更说明
### 变更内容
`dwd.dim_groupbuy_package_ex` 表新增 4 个 JSONB 列,用于存储从团购详情接口提取的维度数据。
| Schema | 表 | 新增列 | 类型 | 说明 |
|--------|-----|--------|------|------|
| dwd | dim_groupbuy_package_ex | table_area_ids | JSONB | 可用台区 ID 列表(来自详情接口 tableAreaId |
| dwd | dim_groupbuy_package_ex | table_area_names | JSONB | 可用台区名称列表(来自详情接口 tableAreaNameList |
| dwd | dim_groupbuy_package_ex | assistant_services | JSONB | 助教服务关联(来自详情接口 packageCouponAssistants |
| dwd | dim_groupbuy_package_ex | groupon_site_infos | JSONB | 关联门店信息(来自详情接口 grouponSiteInfos |
所有列均为 NULLABLE使用 `ADD COLUMN IF NOT EXISTS` 确保幂等性。
### 数据来源
ODS 层 `ods.group_buy_package_details` 表(由 `ODS_GROUP_PACKAGE` 任务的详情拉取子流程写入),通过 `coupon_id = groupbuy_package_id` 关联后在 DWD 加载时合并。
---
## 2. 兼容性影响
| 组件 | 影响 | 说明 |
|------|------|------|
| ETL DWD 加载 | 需配合修改 | `dwd_load_task.py` 需新增 LEFT JOIN 逻辑从 ODS 详情表读取并映射到这 4 个字段Task 4.2/4.3 |
| ETL SCD2 | 自动兼容 | 新增 JSONB 字段自动纳入 `_is_row_changed` 变更检测 |
| 后端 API | 无影响 | 当前无接口直接查询 dim_groupbuy_package_ex 的详情字段 |
| 小程序 | 无影响 | 不直接使用 DWD 层表 |
| RLS 视图 | 无影响 | dim_groupbuy_package_ex 无 RLS 视图 |
---
## 3. 回滚策略
```sql
ALTER TABLE dwd.dim_groupbuy_package_ex
DROP COLUMN IF EXISTS table_area_ids,
DROP COLUMN IF EXISTS table_area_names,
DROP COLUMN IF EXISTS assistant_services,
DROP COLUMN IF EXISTS groupon_site_infos;
```
回滚后需同步撤销 `dwd_load_task.py` 中对应的 LEFT JOIN 和字段映射逻辑。
---
## 4. 验证 SQL
```sql
-- 1. 确认 4 个新列存在且类型正确
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'dwd'
AND table_name = 'dim_groupbuy_package_ex'
AND column_name IN ('table_area_ids', 'table_area_names', 'assistant_services', 'groupon_site_infos')
ORDER BY ordinal_position;
-- 预期4 行data_type 均为 'jsonb'is_nullable 均为 'YES'
-- 2. 确认列 COMMENT 已写入
SELECT a.attname AS column_name, d.description AS comment
FROM pg_catalog.pg_attribute a
JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = a.attnum
WHERE n.nspname = 'dwd'
AND c.relname = 'dim_groupbuy_package_ex'
AND a.attname IN ('table_area_ids', 'table_area_names', 'assistant_services', 'groupon_site_infos')
ORDER BY a.attnum;
-- 预期4 行,每行 comment 非 NULL
-- 3. 确认表主键和索引未受影响
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'dwd'
AND tablename = 'dim_groupbuy_package_ex';
-- 预期:原有 4 个索引不变pkey + 3 个辅助索引)
```

View File

@@ -16,8 +16,8 @@
| 列名 | 类型 | 默认值 | 业务含义 | 取值范围/示例 |
|------|------|--------|---------|-------------|
| `contribution_id` | BIGINT (SERIAL) | nextval 序列 | 自增主键PK | 自增 |
| `site_id` | INTEGER NOT NULL | — | 门店 ID | 飞球门店 ID |
| `tenant_id` | INTEGER NOT NULL | — | 租户 ID | 飞球租户 ID |
| `site_id` | BIGINT NOT NULL | — | 门店 ID | 飞球门店 ID |
| `tenant_id` | BIGINT NOT NULL | — | 租户 ID | 飞球租户 ID |
| `assistant_id` | BIGINT NOT NULL | — | 助教 ID | 飞球助教 ID |
| `assistant_nickname` | VARCHAR(100) | NULL | 助教昵称 | 中文昵称 |
| `stat_date` | DATE NOT NULL | — | 统计日期 | `2025-01-15` |

View File

@@ -0,0 +1,94 @@
# BD_Manual修复 dim_staff_ex 列映射 rankname → rank_name
> 影响表:`dwd.dim_staff_ex`
> ODS 源表:`ods.staff_info_master`
> 修复日期2026-02-26
> 代码位置:`apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py`
> 触发场景:`FLOW_API_FULL` 执行时 `DWD_LOAD_FROM_ODS` 阶段 dim_staff_ex 加载失败
---
## 1. 变更说明
DWD 加载任务 `DwdLoadTask` 中,`dwd.dim_staff_ex` 的列映射定义错误:
```python
# 修复前(错误)
("rank_name", "rankname", None)
# 修复后(正确)
("rank_name", "rank_name", None)
```
映射元组含义:`(dwd_列名, ods_源列名, 类型转换)`
ODS 表 `ods.staff_info_master` 的实际列名为 `rank_name`(带下划线),而非 `rankname`。此错误导致 PostgreSQL 报 `UndefinedColumn: 字段 "rankname" 不存在`dim_staff_ex 的 SCD2 合并在每个窗口段均失败(共 4 次)。
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL DWD 层 | dim_staff_ex 恢复正常加载rank_name 字段将正确从 ODS 映射 |
| DWS 层 | 无直接影响(当前无 DWS 任务依赖 dim_staff_ex.rank_name |
| 后端 API | 无影响(后端通过 FDW 读取,表结构未变) |
| 小程序 | 无影响 |
| DDL | 无变更,表结构不变 |
---
## 3. 回滚策略
此修复仅涉及 Python 代码中的列映射字符串,无 DDL 变更。
回滚步骤:
1.`dwd_load_task.py` 中 dim_staff_ex 映射恢复为 `("rank_name", "rankname", None)`
2. 注意:回滚后 dim_staff_ex 将再次无法加载 rank_name 字段
已加载的数据无需回滚——修复前该字段从未成功写入。
---
## 4. 验证 SQL
```sql
-- 验证 1确认 ODS 源表列名为 rank_name非 rankname
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'ods'
AND table_name = 'staff_info_master'
AND column_name IN ('rank_name', 'rankname');
-- 预期:仅返回 rank_name
-- 验证 2确认 DWD 目标表存在 rank_name 列
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'dwd'
AND table_name = 'dim_staff_ex'
AND column_name = 'rank_name';
-- 预期:返回 1 行
-- 验证 3修复后重跑 ETL检查 dim_staff_ex 是否有 rank_name 非空数据
SELECT COUNT(*) AS total,
COUNT(rank_name) AS has_rank_name
FROM dwd.dim_staff_ex
WHERE scd2_is_current = 1;
-- 预期has_rank_name > 0取决于上游数据是否有值
-- 验证 4对比 ODS 与 DWD 的 rank_name 一致性
SELECT s.id AS staff_id, s.rank_name AS ods_rank_name, d.rank_name AS dwd_rank_name
FROM ods.staff_info_master s
JOIN dwd.dim_staff_ex d ON s.id = d.staff_id AND d.scd2_is_current = 1
WHERE s.rank_name IS DISTINCT FROM d.rank_name
LIMIT 10;
-- 预期:修复并重跑后返回 0 行
```
---
## 5. 映射修正记录
| 日期 | 字段 | 修正内容 |
|------|------|---------|
| 2026-02-26 | `rank_name` | ODS 源列名从 `rankname` 修正为 `rank_name`,与 `ods.staff_info_master` DDL 一致 |

View File

@@ -0,0 +1,132 @@
# BD_Manual修复 DWS_ASSISTANT_DAILY 缺失 table_area_name 列
> 影响任务:`DWS_ASSISTANT_DAILY`
> 涉及表:`dwd.dwd_assistant_service_log`(读取)、`dwd.dim_table`(新增 JOIN、`dws.dws_assistant_daily`(写入)
> 修复日期2026-02-26
> 代码位置:`apps/etl/connectors/feiqiu/tasks/dws/assistant_daily_task.py` → `_extract_service_records()`
> 触发场景:`FLOW_API_FULL` 执行时 DWS 阶段首个任务失败,级联导致后续 13 个 DWS 任务全部 `InFailedSqlTransaction`
---
## 1. 变更说明
`_extract_service_records()` 方法的 SQL 原先直接从 `dwd.dwd_assistant_service_log` 读取 `asl.table_area_name`但该表实际不存在此列DDL 可确认)。
修复方式:通过 LEFT JOIN `dwd.dim_table` 获取台区名称。
```sql
-- 修复前(错误)
SELECT ...
asl.table_area_name,
...
FROM dwd.dwd_assistant_service_log asl
LEFT JOIN dwd.dwd_assistant_service_log_ex ex ...
-- 修复后(正确)
SELECT ...
COALESCE(dt.site_table_area_name, '') AS table_area_name,
...
FROM dwd.dwd_assistant_service_log asl
LEFT JOIN dwd.dwd_assistant_service_log_ex ex ...
LEFT JOIN dwd.dim_table dt
ON asl.site_table_id = dt.table_id
AND dt.scd2_is_current = 1
```
JOIN 条件说明:
- `asl.site_table_id = dt.table_id`:通过台桌 ID 关联维度表
- `dt.scd2_is_current = 1`:仅取当前有效的 SCD2 版本
- `COALESCE(..., '')`dim_table 无匹配时回退空字符串,避免 NULL 传播
---
## 2. 级联失败说明
此 bug 不仅导致 `DWS_ASSISTANT_DAILY` 本身失败,还因 DWS 阶段共享同一数据库连接且无逐任务 rollback使 psycopg2 进入 `InFailedSqlTransaction` 状态,后续 13 个 DWS/INDEX 任务全部失败:
- DWS_ASSISTANT_CUSTOMER, DWS_ASSISTANT_SALARY, DWS_ASSISTANT_FINANCE, DWS_ASSISTANT_MONTHLY
- DWS_MEMBER_CONSUMPTION, DWS_MEMBER_VISIT
- DWS_FINANCE_DAILY, DWS_FINANCE_RECHARGE, DWS_FINANCE_INCOME_STRUCTURE, DWS_FINANCE_DISCOUNT_DETAIL
- DWS_WINBACK_INDEX, DWS_NEWCONV_INDEX, DWS_RELATION_INDEX
修复此根因后,上述任务均可恢复正常执行。
---
## 3. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL DWS 层 | `dws_assistant_daily` 恢复正常写入,`table_area_name` 来源从不存在的列改为 dim_table 维度表 |
| 后续 DWS 任务 | 级联失败消除,所有 DWS 任务可正常执行 |
| 后端 API | 无影响DWS 聚合表结构未变) |
| 管理后台 | 助教日报表将正确显示台区名称 |
| DDL | 无变更,无新增表/列 |
---
## 4. 回滚策略
此修复仅涉及 Python 代码中的 SQL 查询,无 DDL 变更。
回滚步骤:
1.`assistant_daily_task.py``_extract_service_records()` 的 SQL 恢复为 `asl.table_area_name`,移除 `LEFT JOIN dwd.dim_table`
2. 注意:回滚后 DWS_ASSISTANT_DAILY 将再次失败
已写入的 DWS 数据如需回滚:
```sql
-- 清除修复后写入的 dws_assistant_daily 数据(按需执行)
DELETE FROM dws.dws_assistant_daily
WHERE stat_date >= '2025-11-01';
```
---
## 5. 验证 SQL
```sql
-- 验证 1确认 dwd_assistant_service_log 确实没有 table_area_name 列
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'dwd'
AND table_name = 'dwd_assistant_service_log'
AND column_name = 'table_area_name';
-- 预期:返回 0 行
-- 验证 2确认 dim_table 存在 site_table_area_name 列
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'dwd'
AND table_name = 'dim_table'
AND column_name = 'site_table_area_name';
-- 预期:返回 1 行
-- 验证 3检查 JOIN 关联覆盖率service_log 的 site_table_id 在 dim_table 中的匹配率)
SELECT
COUNT(*) AS total_records,
COUNT(dt.table_id) AS matched_dim_table,
ROUND(COUNT(dt.table_id)::numeric / NULLIF(COUNT(*), 0) * 100, 1) AS match_pct
FROM dwd.dwd_assistant_service_log asl
LEFT JOIN dwd.dim_table dt
ON asl.site_table_id = dt.table_id
AND dt.scd2_is_current = 1
WHERE asl.is_delete = 0;
-- 预期match_pct 接近 100%
-- 验证 4修复后重跑 ETL检查 dws_assistant_daily 是否有数据
SELECT stat_date, COUNT(*) AS rows
FROM dws.dws_assistant_daily
WHERE stat_date >= '2025-11-01'
GROUP BY stat_date
ORDER BY stat_date
LIMIT 10;
-- 预期:有数据行返回
```
---
## 6. 代码引用
- 修复文件:`apps/etl/connectors/feiqiu/tasks/dws/assistant_daily_task.py``_extract_service_records()`
- 关联 BD Manual`BD_Manual_assistant_service_records.md`dwd_assistant_service_log 字段映射文档)
- dim_table DDL`docs/database/ddl/etl_feiqiu__dwd.sql`(含 `site_table_area_name` 列定义)

View File

@@ -0,0 +1,92 @@
# BD_Manualods.group_buy_package_details 团购套餐详情表
> 日期2026-03-05
> 涉及库:`etl_feiqiu` / `test_etl_feiqiu`
> DDL 路径:`db/etl_feiqiu/ods/group_buy_package_details.sql`
> 直接原因整合团购详情接口QueryPackageCouponInfo新建 ODS 详情表存储每个团购套餐的详情数据
> Prompt 摘要etl-coupon-detail spec — 需求 3 验收标准 1-4
---
## 1. 变更说明
### 变更内容
新建 `ods.group_buy_package_details` 表,用于存储 `QueryPackageCouponInfo` 详情接口的原始数据。
| Schema | 表 | 操作 | 说明 |
|--------|-----|------|------|
| ods | group_buy_package_details | 新建 | 团购套餐详情,主键 `coupon_id`,含 12 个结构化字段 + 6 个 JSONB 数组字段 + 3 个 ETL 元数据字段 |
### 数据获取方式
通过 `ODS_GROUP_PACKAGE` 任务的 `detail_endpoint` 二级详情拉取子流程:
- 主流程拉取团购列表 → `ods.group_buy_packages`
- 子流程遍历每个 `id`,串行调用 `QueryPackageCouponInfo` → 本表
- 全量快照模式UPSERT on `coupon_id`
### 关键字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `coupon_id` | BIGINT PK | 团购套餐 ID= group_buy_packages.id |
| `table_area_ids` | JSONB | 可用台区 ID 列表 |
| `table_area_names` | JSONB | 可用台区名称列表 |
| `assistant_services` | JSONB | 助教服务关联数组 |
| `groupon_site_infos` | JSONB | 关联门店信息数组 |
| `package_services` | JSONB | 套餐服务数组(待调研) |
| `coupon_details_list` | JSONB | 券明细数组(待调研) |
| `content_hash` | TEXT | 内容哈希,用于变更检测 |
| `payload` | JSONB | 完整原始 JSON 响应 |
---
## 2. 兼容性影响
| 组件 | 影响 | 说明 |
|------|------|------|
| ETL ODS 层 | 新增表 | `ODS_GROUP_PACKAGE` 任务通过 `detail_endpoint` 配置自动写入 |
| ETL DWD 层 | 需配合修改 | `dwd_load_task.py` 需 LEFT JOIN 本表将 4 个 JSONB 字段合并到 `dim_groupbuy_package_ex` |
| 后端 API | 无影响 | 当前无接口直接查询本表 |
| 小程序 | 无影响 | 不直接使用 ODS 层表 |
---
## 3. 回滚策略
```sql
DROP TABLE IF EXISTS ods.group_buy_package_details;
```
回滚后需同步移除 `ODS_GROUP_PACKAGE` 任务中的 `detail_endpoint` 相关配置。
---
## 4. 验证 SQL
```sql
-- 1. 确认表存在且主键正确
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'ods'
AND table_name = 'group_buy_package_details'
ORDER BY ordinal_position;
-- 预期22 列coupon_id 为 BIGINT NOT NULL
-- 2. 确认主键约束
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'ods'
AND table_name = 'group_buy_package_details'
AND constraint_type = 'PRIMARY KEY';
-- 预期1 行pk_group_buy_package_details
-- 3. 确认 JSONB 列存在
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'ods'
AND table_name = 'group_buy_package_details'
AND data_type = 'jsonb'
ORDER BY ordinal_position;
-- 预期8 行table_area_ids, table_area_names, assistant_services, groupon_site_infos, package_services, coupon_details_list, payload
```

View File

@@ -0,0 +1,159 @@
# BD 手册维客线索表member_retention_clue
## 概述
`zqyy_app` / `test_zqyy_app` 业务库中新建 `member_retention_clue` 表,替代原 `member_birthday_manual` 表。维客线索是助教为会员记录的销售/维护线索,采用"大类 + 摘要 + 详情"三层结构,覆盖客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈六个维度。
生日信息不再单独建表,作为"客户基础"大类下的一条线索记录。
## 变更说明
| 库 | Schema | 表 | 变更类型 | 说明 |
|----|--------|---|---------|------|
| zqyy_app / test_zqyy_app | public | member_birthday_manual | 删除 | 旧表,生日单独记录方案废弃 |
| zqyy_app / test_zqyy_app | public | member_retention_clue | 新建 | 维客线索表 |
| zqyy_app / test_zqyy_app | public | member_retention_clue.source | 新增列 | 2026-02-27 补齐线索来源字段 |
| zqyy_app / test_zqyy_app | public | member_retention_clue.category | 约束变更 | 2026-03-08 枚举对齐:`客户基础信息``客户基础` |
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| member_id | BIGINT | NOT NULL | 会员 ID |
| category | VARCHAR(20) | NOT NULL, CHECK | 线索大类枚举6 值,见下方枚举表) |
| summary | VARCHAR(200) | NOT NULL | 摘要:重点信息 |
| detail | TEXT | 可为空 | 详情:分析及扩展说明 |
| recorded_by_assistant_id | BIGINT | — | 记录助教 ID |
| recorded_by_name | VARCHAR(50) | — | 记录助教姓名 |
| recorded_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录时间 |
| site_id | BIGINT | NOT NULL | 门店 ID多门店隔离 |
| source | VARCHAR(20) | NOT NULL DEFAULT 'manual' | 线索来源2026-02-27 新增) |
### category 枚举值
> 2026-03-08 枚举对齐:`客户基础信息` → `客户基础`P5 spec 评审决定,与 AI 应用 Prompt 统一)
| 值 | 说明 |
|----|------|
| 客户基础 | 生日、职业、偏好时段等基础画像 |
| 消费习惯 | 消费频次、客单价、消费时段等 |
| 玩法偏好 | 中式/斯诺克/美式偏好、技术水平等 |
| 促销偏好 | 对储值活动、套餐、折扣的敏感度 |
| 社交关系 | 常带朋友、固定球搭子、社交圈等 |
| 重要反馈 | 客户提出的需求、投诉、建议等 |
### source 枚举值2026-02-27 新增)
| 值 | 说明 |
|----|------|
| manual | 助教手动录入(默认值) |
| ai_consumption | 应用 3消费分析自动生成 |
| ai_note | 应用 6备注分析自动提取 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| member_retention_clue_pkey | PRIMARY KEY | id | 主键 |
| chk_retention_clue_category | CHECK | category | 限制大类枚举值 |
| idx_retention_clue_member | INDEX (btree) | member_id | 按会员查询 |
| idx_retention_clue_site | INDEX (btree) | site_id | 按门店查询 |
| idx_retention_clue_category | INDEX (btree) | (member_id, category) | 按会员+大类查询 |
## 兼容性
- **后端 API**`POST /api/member-birthday` 废弃,替换为 `POST /api/retention-clue``GET /api/retention-clue/{member_id}``DELETE /api/retention-clue/{clue_id}`
- **source 字段**2026-02-27`POST /api/retention-clue` 接受可选 `source` 参数,默认 `manual``GET` 返回中包含 `source` 字段。已有数据自动填充 `DEFAULT 'manual'`,向后兼容
- **ETL Connector**DWS 任务移除 FDW 读取 `member_birthday_manual` 的逻辑,生日仅从 `dim_member.birthday`API 来源)读取
- **FDW**`fdw_app.member_birthday_manual` 外部表需在 ETL 库侧同步更新为 `fdw_app.member_retention_clue`(含 `source` 列)
- **小程序**:助教端调用新 API 提交维客线索
- **H5 原型**customer-detail 和 task-detail 页面"消费习惯"板块改为"维客线索"
## 回滚策略
### 仅回滚 source 列2026-02-27 变更)
```sql
BEGIN;
ALTER TABLE member_retention_clue DROP COLUMN IF EXISTS source;
COMMIT;
```
### 完整回滚(整表)
```sql
BEGIN;
DROP TABLE IF EXISTS member_retention_clue CASCADE;
-- 如需恢复旧表,执行归档的 2026-02-22__C2_member_birthday_manual.sql
COMMIT;
```
## 验证步骤
```sql
-- 1. 确认旧表已删除
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'member_birthday_manual';
-- 预期0 行
-- 2. 确认新表存在
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'member_retention_clue';
-- 预期1 行
-- 3. 确认列结构完整10 列)
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'member_retention_clue'
ORDER BY ordinal_position;
-- 预期id, member_id, category, summary, detail,
-- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id, source
-- 4. 确认 CHECK 约束
SELECT conname FROM pg_constraint
WHERE conrelid = 'member_retention_clue'::regclass AND contype = 'c';
-- 预期chk_retention_clue_category
-- 5. 确认索引3 + 主键)
SELECT indexname FROM pg_indexes
WHERE tablename = 'member_retention_clue';
-- 预期4 行
-- 6. 确认表注释
SELECT obj_description('member_retention_clue'::regclass, 'pg_class');
-- 预期:包含"维客线索"
-- 7. 确认 source 列存在且默认值正确2026-02-27
SELECT column_name, data_type, column_default, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'member_retention_clue'
AND column_name = 'source';
-- 预期1 行varchar, 'manual'::character varying, NO
-- 8. 确认 source 列注释
SELECT col_description(
(SELECT oid FROM pg_class WHERE relname = 'member_retention_clue'),
(SELECT ordinal_position FROM information_schema.columns
WHERE table_name = 'member_retention_clue' AND column_name = 'source')
);
-- 预期:包含 'manual' / 'ai_consumption' / 'ai_note'
-- 9. 确认已有数据的 source 分布
SELECT source, COUNT(*) FROM member_retention_clue GROUP BY source;
-- 预期:全部为 'manual'(或空表)
```
## 关联文件
- 迁移脚本(建表):`db/zqyy_app/migrations/2026-02-26__refactor_birthday_to_retention_clue.sql`
- 迁移脚本source 列):`db/zqyy_app/migrations/2026-02-27__add_source_to_retention_clue.sql`
- 迁移脚本category 枚举对齐):`db/zqyy_app/migrations/2026-03-08__align_retention_clue_category_enum.sql`
- FDW 反向映射(生产):`db/fdw/setup_fdw_reverse.sql`
- FDW 反向映射(测试):`db/fdw/setup_fdw_reverse_test.sql`
- 后端路由:`apps/backend/app/routers/member_retention_clue.py`
- 后端模型:`apps/backend/app/schemas/member_retention_clue.py`
- H5 原型:`docs/h5_ui/pages/customer-detail.html``docs/h5_ui/pages/task-detail.html`
- 旧表文档(已归档):`docs/database/_archived/BD_Manual_member_birthday_manual.md`
- 旧 FDW 文档(已归档):`docs/database/_archived/BD_Manual_fdw_reverse_member_birthday.md`

View File

@@ -0,0 +1,128 @@
# BD_Manualtenant_id INTEGER → BIGINT 迁移
> 日期2026-03-03
> 涉及库:`etl_feiqiu` / `test_etl_feiqiu`、`zqyy_app` / `test_zqyy_app`
> 迁移脚本:
> - `db/etl_feiqiu/migrations/2026-03-03__alter_tenant_id_int_to_bigint.sql`
> - `db/zqyy_app/migrations/2026-03-03__alter_tenant_id_int_to_bigint.sql`
> 直接原因:飞球 tenant_id如 2790683160709957远超 int4 上限2,147,483,647导致写入溢出
> Prompt 摘要:修复 tenant_id int4 溢出问题,迁移为 bigint
---
## 1. 变更说明
### 变更前
| 库 | Schema | 表 | 列 | 类型 |
|----|--------|----|----|------|
| etl_feiqiu | dws | dws_assistant_order_contribution | tenant_id | INTEGER (int4) NOT NULL |
| zqyy_app | auth | site_code_mapping | tenant_id | INTEGER (int4) NULL |
### 变更后
| 库 | Schema | 表 | 列 | 类型 |
|----|--------|----|----|------|
| etl_feiqiu | dws | dws_assistant_order_contribution | tenant_id | BIGINT (int8) NOT NULL |
| zqyy_app | auth | site_code_mapping | tenant_id | BIGINT (int8) NULL |
### 级联影响
| 对象 | 类型 | 处理方式 |
|------|------|---------|
| `app.v_dws_assistant_order_contribution` (ETL 库) | RLS 视图 | DROP → ALTER → 重建SELECT * |
| `fdw_etl.v_dws_assistant_order_contribution` (App 库) | FDW 外部表 | DROP → IMPORT FOREIGN SCHEMA 重新导入 |
| `app.v_dws_assistant_order_contribution` (App 库) | RLS 视图 | 自动继承 FDW 外部表类型 |
---
## 2. 兼容性影响
| 组件 | 影响 | 说明 |
|------|------|------|
| ETL 任务 | 无影响 | `assistant_order_contribution_task.py` 从 DWD 层读取 tenant_id已是 bigint写入 DWS 现在类型匹配 |
| 后端 API | 无影响 | 通过 FDW 视图读取,类型自动跟随源表 |
| 小程序 | 无影响 | 不直接使用 tenant_id |
| `init_test_user.py` | 已更新 | 移除 `_safe_tenant_id()` 中的 int4 范围检查降级逻辑 |
---
## 3. 回滚策略
### ETL 库回滚
```sql
BEGIN;
DROP VIEW IF EXISTS app.v_dws_assistant_order_contribution CASCADE;
ALTER TABLE dws.dws_assistant_order_contribution ALTER COLUMN tenant_id TYPE integer;
CREATE OR REPLACE VIEW app.v_dws_assistant_order_contribution AS
SELECT * FROM dws.dws_assistant_order_contribution
WHERE site_id = current_setting('app.current_site_id')::bigint;
GRANT SELECT ON app.v_dws_assistant_order_contribution TO app_reader;
COMMIT;
```
### App 库回滚
```sql
BEGIN;
ALTER TABLE auth.site_code_mapping ALTER COLUMN tenant_id TYPE integer;
COMMIT;
```
### 代码回滚
恢复 `scripts/ops/init_test_user.py``_safe_tenant_id()` 的 int4 范围检查逻辑。
---
## 4. 验证 SQL
### ETL 库test_etl_feiqiu
```sql
-- 1. 确认 dws 表 tenant_id 类型
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_schema = 'dws'
AND table_name = 'dws_assistant_order_contribution'
AND column_name = 'tenant_id';
-- 预期data_type = 'bigint', udt_name = 'int8'
-- 2. 确认 RLS 视图存在且类型正确
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'app'
AND table_name = 'v_dws_assistant_order_contribution'
AND column_name = 'tenant_id';
-- 预期data_type = 'bigint'
-- 3. 确认全库无残留 int4 tenant_id
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'tenant_id' AND udt_name = 'int4';
-- 预期0 行
```
### App 库test_zqyy_app
```sql
-- 1. 确认 auth 表 tenant_id 类型
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_schema = 'auth'
AND table_name = 'site_code_mapping'
AND column_name = 'tenant_id';
-- 预期data_type = 'bigint', udt_name = 'int8'
-- 2. 确认 FDW 外部表类型正确
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'fdw_etl'
AND table_name = 'v_dws_assistant_order_contribution'
AND column_name = 'tenant_id';
-- 预期data_type = 'bigint'
-- 3. 确认全库无残留 int4 tenant_id
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'tenant_id' AND udt_name = 'int4';
-- 预期0 行
```

View File

@@ -14,6 +14,7 @@
| `etl_feiqiu__app.sql` | etl_feiqiu | app | RLS 视图层43 视图,无表) |
| `zqyy_app__public.sql` | zqyy_app | public | 小程序业务表12 表) |
| `zqyy_app__auth.sql` | zqyy_app | auth | 用户认证与权限8 表) |
| `zqyy_app__biz.sql` | zqyy_app | biz | 核心业务表(任务/备注/触发器4 表) |
| `fdw.sql` | — | — | FDW 正向跨库映射配置etl→app |
## 数据字典BD_Manual — ODS→DWD 字段映射)

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / appRLS 视图层)
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -322,6 +322,9 @@ SELECT recharge_order_id,
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
-- ⚠️ consume_money 透传自 DWD 层存在三种历史口径A/B/CAPI 消费端不应直接展示或参与计算。
-- 应使用 items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money
-- settle_type 枚举1=台桌结账, 3=商城订单, 6=退货订单, 7=退款订单(本表无 is_delete 字段)
CREATE OR REPLACE VIEW app.v_dwd_settlement_head AS
SELECT order_settle_id,
tenant_id,
@@ -683,7 +686,7 @@ SELECT id,
platform_fee_amount,
recharge_cash_inflow,
card_consume_total,
cash_card_consume,
recharge_card_consume,
gift_card_consume,
cash_outflow_total,
cash_balance_change,
@@ -992,7 +995,8 @@ SELECT id,
total_discount,
actual_pay,
cash_pay,
cash_card_pay,
balance_pay,
recharge_card_pay,
gift_card_pay,
groupbuy_pay,
table_duration_min,

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / core跨门店标准化维度/事实)
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dwd明细数据层
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -616,27 +616,6 @@ CREATE TABLE dwd.dwd_assistant_service_log_ex (
operator_name text
);
CREATE TABLE dwd.dwd_assistant_trash_event (
assistant_trash_event_id bigint NOT NULL,
site_id bigint,
table_id bigint,
table_area_id bigint,
assistant_no character varying(32),
assistant_name character varying(64),
charge_minutes_raw integer,
abolish_amount numeric(18,2),
trash_reason character varying(255),
create_time timestamp with time zone,
tenant_id bigint
);
CREATE TABLE dwd.dwd_assistant_trash_event_ex (
assistant_trash_event_id bigint NOT NULL,
table_name character varying(64),
table_area_name character varying(64),
assistant_no_int integer
);
CREATE TABLE dwd.dwd_goods_stock_movement (
site_goods_stock_id bigint NOT NULL,
tenant_id bigint,
@@ -1170,8 +1149,6 @@ ALTER TABLE dwd.dim_tenant_goods ADD CONSTRAINT dim_tenant_goods_pkey PRIMARY KE
ALTER TABLE dwd.dim_tenant_goods_ex ADD CONSTRAINT dim_tenant_goods_ex_pkey PRIMARY KEY (tenant_goods_id, scd2_start_time);
ALTER TABLE dwd.dwd_assistant_service_log ADD CONSTRAINT dwd_assistant_service_log_pkey PRIMARY KEY (assistant_service_id);
ALTER TABLE dwd.dwd_assistant_service_log_ex ADD CONSTRAINT dwd_assistant_service_log_ex_pkey PRIMARY KEY (assistant_service_id);
ALTER TABLE dwd.dwd_assistant_trash_event ADD CONSTRAINT dwd_assistant_trash_event_pkey PRIMARY KEY (assistant_trash_event_id);
ALTER TABLE dwd.dwd_assistant_trash_event_ex ADD CONSTRAINT dwd_assistant_trash_event_ex_pkey PRIMARY KEY (assistant_trash_event_id);
ALTER TABLE dwd.dwd_goods_stock_movement ADD CONSTRAINT dwd_goods_stock_movement_pkey PRIMARY KEY (site_goods_stock_id);
ALTER TABLE dwd.dwd_goods_stock_summary ADD CONSTRAINT dwd_goods_stock_summary_pkey PRIMARY KEY (site_goods_id, fetched_at);
ALTER TABLE dwd.dwd_groupbuy_redemption ADD CONSTRAINT dwd_groupbuy_redemption_pkey PRIMARY KEY (redemption_id);
@@ -1262,8 +1239,6 @@ CREATE INDEX idx_dwd_assistant_service_log_time_create_time ON dwd.dwd_assistant
CREATE INDEX idx_dwd_assistant_service_log_time_pk_118fd0d3 ON dwd.dwd_assistant_service_log USING btree (create_time, assistant_service_id);
CREATE INDEX idx_dwd_assistant_service_log_time_pk_3fb2dede ON dwd.dwd_assistant_service_log USING btree (start_use_time, assistant_service_id);
CREATE INDEX idx_dwd_assistant_service_log_time_start_use_time ON dwd.dwd_assistant_service_log USING btree (start_use_time);
CREATE INDEX idx_dwd_assistant_trash_event_time_create_time ON dwd.dwd_assistant_trash_event USING btree (create_time);
CREATE INDEX idx_dwd_assistant_trash_event_time_pk_0b64af2a ON dwd.dwd_assistant_trash_event USING btree (create_time, assistant_trash_event_id);
CREATE INDEX idx_dwd_groupbuy_redemption_time_create_time ON dwd.dwd_groupbuy_redemption USING btree (create_time);
CREATE INDEX idx_dwd_groupbuy_redemption_time_pk_create_time_redemption_id ON dwd.dwd_groupbuy_redemption USING btree (create_time, redemption_id);
CREATE INDEX idx_dwd_payment_time_create_time ON dwd.dwd_payment USING btree (create_time);

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dws汇总数据层
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -42,8 +42,11 @@ CREATE SEQUENCE IF NOT EXISTS dws.dws_platform_settlement_id_seq AS bigint;
CREATE TABLE dws.cfg_area_category (
category_id integer DEFAULT nextval('dws.cfg_area_category_category_id_seq'::regclass) NOT NULL,
source_area_name character varying(100) NOT NULL,
source_table_name character varying(100) DEFAULT NULL,
category_code character varying(20) NOT NULL,
category_name character varying(50) NOT NULL,
display_name character varying(50) DEFAULT NULL,
short_name character varying(20) DEFAULT NULL,
match_type character varying(10) DEFAULT 'EXACT'::character varying NOT NULL,
match_priority integer DEFAULT 100 NOT NULL,
is_active boolean DEFAULT true NOT NULL,
@@ -51,6 +54,9 @@ CREATE TABLE dws.cfg_area_category (
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
-- 唯一约束:(source_area_name, source_table_name) 支持同一台区下按台桌细分
CREATE UNIQUE INDEX uk_cfg_area_category
ON dws.cfg_area_category (source_area_name, COALESCE(source_table_name, ''));
CREATE TABLE dws.cfg_assistant_level_price (
price_id integer DEFAULT nextval('dws.cfg_assistant_level_price_price_id_seq'::regclass) NOT NULL,
@@ -102,7 +108,7 @@ CREATE TABLE dws.cfg_performance_tier (
min_hours numeric(10,2) NOT NULL,
max_hours numeric(10,2),
base_deduction numeric(10,2) DEFAULT 0 NOT NULL,
bonus_deduction_ratio numeric(5,4) DEFAULT 0 NOT NULL,
bonus_deduction_ratio numeric(7,4) DEFAULT 0 NOT NULL,
vacation_days integer DEFAULT 0 NOT NULL,
vacation_unlimited boolean DEFAULT false NOT NULL,
is_new_hire_tier boolean DEFAULT false NOT NULL,
@@ -215,7 +221,7 @@ CREATE TABLE dws.dws_assistant_finance_analysis (
revenue_room numeric(14,2) DEFAULT 0 NOT NULL,
cost_daily numeric(14,2) DEFAULT 0 NOT NULL,
gross_profit numeric(14,2) DEFAULT 0 NOT NULL,
gross_margin numeric(5,4) DEFAULT 0 NOT NULL,
gross_margin numeric(7,4) DEFAULT 0 NOT NULL,
service_count integer DEFAULT 0 NOT NULL,
service_hours numeric(10,2) DEFAULT 0 NOT NULL,
room_service_count integer DEFAULT 0 NOT NULL,
@@ -265,8 +271,8 @@ CREATE TABLE dws.dws_assistant_monthly_summary (
CREATE TABLE dws.dws_assistant_order_contribution (
contribution_id bigint DEFAULT nextval('dws.dws_assistant_order_contribution_contribution_id_seq'::regclass) NOT NULL,
site_id integer NOT NULL,
tenant_id integer NOT NULL,
site_id bigint NOT NULL,
tenant_id bigint NOT NULL,
assistant_id bigint NOT NULL,
assistant_nickname character varying(100),
stat_date date NOT NULL,
@@ -291,7 +297,7 @@ CREATE TABLE dws.dws_assistant_recharge_commission (
recharge_order_no character varying(50),
recharge_amount numeric(12,2) DEFAULT 0 NOT NULL,
commission_amount numeric(12,2) DEFAULT 0 NOT NULL,
commission_ratio numeric(5,4),
commission_ratio numeric(7,4),
import_batch_no character varying(50),
import_file_name character varying(200),
import_time timestamp with time zone,
@@ -323,7 +329,7 @@ CREATE TABLE dws.dws_assistant_salary_calc (
base_course_price numeric(10,2) DEFAULT 0 NOT NULL,
bonus_course_price numeric(10,2) DEFAULT 0 NOT NULL,
base_deduction numeric(10,2) DEFAULT 0 NOT NULL,
bonus_deduction_ratio numeric(5,4) DEFAULT 0 NOT NULL,
bonus_deduction_ratio numeric(7,4) DEFAULT 0 NOT NULL,
base_income numeric(12,2) DEFAULT 0 NOT NULL,
bonus_income numeric(12,2) DEFAULT 0 NOT NULL,
room_income numeric(12,2) DEFAULT 0 NOT NULL,
@@ -366,7 +372,7 @@ CREATE TABLE dws.dws_finance_daily_summary (
platform_fee_amount numeric(14,2) DEFAULT 0 NOT NULL,
recharge_cash_inflow numeric(14,2) DEFAULT 0 NOT NULL,
card_consume_total numeric(14,2) DEFAULT 0 NOT NULL,
cash_card_consume numeric(14,2) DEFAULT 0 NOT NULL,
recharge_card_consume numeric(14,2) DEFAULT 0 NOT NULL,
gift_card_consume numeric(14,2) DEFAULT 0 NOT NULL,
cash_outflow_total numeric(14,2) DEFAULT 0 NOT NULL,
cash_balance_change numeric(14,2) DEFAULT 0 NOT NULL,
@@ -394,7 +400,7 @@ CREATE TABLE dws.dws_finance_discount_detail (
discount_type_code character varying(30) NOT NULL,
discount_type_name character varying(50) NOT NULL,
discount_amount numeric(14,2) DEFAULT 0 NOT NULL,
discount_ratio numeric(5,4) DEFAULT 0 NOT NULL,
discount_ratio numeric(7,4) DEFAULT 0 NOT NULL,
usage_count integer DEFAULT 0 NOT NULL,
affected_orders integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
@@ -429,7 +435,7 @@ CREATE TABLE dws.dws_finance_income_structure (
category_code character varying(30) NOT NULL,
category_name character varying(50) NOT NULL,
income_amount numeric(14,2) DEFAULT 0 NOT NULL,
income_ratio numeric(5,4) DEFAULT 0 NOT NULL,
income_ratio numeric(7,4) DEFAULT 0 NOT NULL,
order_count integer DEFAULT 0 NOT NULL,
duration_minutes integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
@@ -564,7 +570,7 @@ CREATE TABLE dws.dws_member_assistant_intimacy (
score_recency numeric(10,4),
score_recharge numeric(10,4),
score_duration numeric(10,4),
burst_multiplier numeric(6,4),
burst_multiplier numeric(7,4),
raw_score numeric(14,6),
display_score numeric(4,2),
calc_time timestamp with time zone DEFAULT now() NOT NULL,
@@ -720,7 +726,7 @@ CREATE TABLE dws.dws_member_recall_index (
CREATE TABLE dws.dws_member_spending_power_index (
spi_id bigint DEFAULT nextval('dws.dws_member_spending_power_index_spi_id_seq'::regclass) NOT NULL,
site_id integer NOT NULL,
site_id bigint NOT NULL,
member_id bigint NOT NULL,
spend_30 numeric(14,2) DEFAULT 0,
spend_90 numeric(14,2) DEFAULT 0,
@@ -767,7 +773,8 @@ CREATE TABLE dws.dws_member_visit_detail (
total_discount numeric(12,2) DEFAULT 0 NOT NULL,
actual_pay numeric(12,2) DEFAULT 0 NOT NULL,
cash_pay numeric(12,2) DEFAULT 0 NOT NULL,
cash_card_pay numeric(12,2) DEFAULT 0 NOT NULL,
balance_pay numeric(12,2) DEFAULT 0 NOT NULL,
recharge_card_pay numeric(12,2) DEFAULT 0 NOT NULL,
gift_card_pay numeric(12,2) DEFAULT 0 NOT NULL,
groupbuy_pay numeric(12,2) DEFAULT 0 NOT NULL,
table_duration_min integer DEFAULT 0 NOT NULL,
@@ -1310,7 +1317,7 @@ SELECT id,
platform_fee_amount,
recharge_cash_inflow,
card_consume_total,
cash_card_consume,
recharge_card_consume,
gift_card_consume,
cash_outflow_total,
cash_balance_change,
@@ -1357,7 +1364,7 @@ SELECT id,
platform_fee_amount,
recharge_cash_inflow,
card_consume_total,
cash_card_consume,
recharge_card_consume,
gift_card_consume,
cash_outflow_total,
cash_balance_change,
@@ -1404,7 +1411,7 @@ SELECT id,
platform_fee_amount,
recharge_cash_inflow,
card_consume_total,
cash_card_consume,
recharge_card_consume,
gift_card_consume,
cash_outflow_total,
cash_balance_change,
@@ -1451,7 +1458,7 @@ SELECT id,
platform_fee_amount,
recharge_cash_inflow,
card_consume_total,
cash_card_consume,
recharge_card_consume,
gift_card_consume,
cash_outflow_total,
cash_balance_change,
@@ -1483,3 +1490,69 @@ CREATE INDEX idx_mv_finance_daily_l2 ON dws.mv_dws_finance_daily_summary_l2 USIN
CREATE INDEX idx_mv_finance_daily_l3 ON dws.mv_dws_finance_daily_summary_l3 USING btree (site_id, stat_date);
CREATE INDEX idx_mv_finance_daily_l4 ON dws.mv_dws_finance_daily_summary_l4 USING btree (site_id, stat_date);
-- =============================================================================
-- 项目标签表2026-03-07 新增)
-- =============================================================================
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_project_tag_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_member_project_tag_id_seq AS bigint;
CREATE TABLE dws.dws_assistant_project_tag (
id BIGSERIAL NOT NULL,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
assistant_id BIGINT NOT NULL,
time_window VARCHAR(40) NOT NULL,
category_code VARCHAR(30) NOT NULL,
category_name VARCHAR(50) NOT NULL,
short_name VARCHAR(10) NOT NULL,
duration_seconds BIGINT NOT NULL DEFAULT 0,
total_seconds BIGINT NOT NULL DEFAULT 0,
percentage NUMERIC(5,4) NOT NULL DEFAULT 0,
is_tagged BOOLEAN NOT NULL DEFAULT FALSE,
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pk_dws_assistant_project_tag PRIMARY KEY (id),
CONSTRAINT uk_dws_assistant_project_tag
UNIQUE (site_id, assistant_id, time_window, category_code)
);
COMMENT ON TABLE dws.dws_assistant_project_tag IS '助教项目标签按时间窗口计算各项目时长占比≥25%分配标签';
COMMENT ON COLUMN dws.dws_assistant_project_tag.time_window IS '时间窗口THIS_MONTH/THIS_QUARTER/LAST_MONTH/LAST_3_MONTHS_EXCL_CURRENT/LAST_QUARTER/LAST_6_MONTHS';
COMMENT ON COLUMN dws.dws_assistant_project_tag.is_tagged IS '占比≥0.25时为TRUE表示该助教拥有此项目标签';
CREATE TABLE dws.dws_member_project_tag (
id BIGSERIAL NOT NULL,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
time_window VARCHAR(40) NOT NULL,
category_code VARCHAR(30) NOT NULL,
category_name VARCHAR(50) NOT NULL,
short_name VARCHAR(10) NOT NULL,
duration_seconds BIGINT NOT NULL DEFAULT 0,
total_seconds BIGINT NOT NULL DEFAULT 0,
percentage NUMERIC(5,4) NOT NULL DEFAULT 0,
is_tagged BOOLEAN NOT NULL DEFAULT FALSE,
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pk_dws_member_project_tag PRIMARY KEY (id),
CONSTRAINT uk_dws_member_project_tag
UNIQUE (site_id, member_id, time_window, category_code)
);
COMMENT ON TABLE dws.dws_member_project_tag IS '客户项目标签按时间窗口计算各项目消费时长占比≥25%分配标签';
COMMENT ON COLUMN dws.dws_member_project_tag.time_window IS '时间窗口LAST_30_DAYS/LAST_60_DAYS';
COMMENT ON COLUMN dws.dws_member_project_tag.is_tagged IS '占比≥0.25时为TRUE表示该客户拥有此项目标签';
-- 部分索引(加速看板查询)
CREATE INDEX idx_apt_site_window_tagged
ON dws.dws_assistant_project_tag (site_id, time_window)
WHERE is_tagged = TRUE;
CREATE INDEX idx_mpt_site_window_tagged
ON dws.dws_member_project_tag (site_id, time_window)
WHERE is_tagged = TRUE;

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / metaETL 调度元数据)
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / ods原始数据层
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -77,28 +77,6 @@ CREATE TABLE ods.assistant_accounts_master (
payload jsonb NOT NULL
);
CREATE TABLE ods.assistant_cancellation_records (
id bigint NOT NULL,
siteid bigint,
siteprofile jsonb,
assistantname text,
assistantabolishamount numeric(18,2),
assistanton integer,
pdchargeminutes integer,
tableareaid bigint,
tablearea text,
tableid bigint,
tablename text,
trashreason text,
createtime timestamp without time zone,
tenant_id bigint,
content_hash text NOT NULL,
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now(),
payload jsonb NOT NULL
);
CREATE TABLE ods.assistant_service_records (
id bigint NOT NULL,
tenant_id bigint,
@@ -215,6 +193,7 @@ CREATE TABLE ods.goods_stock_summary (
rangesalemoney numeric(18,2),
rangeinventory numeric(18,4),
currentstock numeric(18,4),
siteid bigint,
content_hash text NOT NULL,
source_file text,
source_endpoint text,
@@ -1066,7 +1045,6 @@ CREATE TABLE ods.tenant_goods_master (
-- 约束(主键 / 唯一 / 外键)
ALTER TABLE ods.assistant_accounts_master ADD CONSTRAINT assistant_accounts_master_pkey PRIMARY KEY (id, content_hash);
ALTER TABLE ods.assistant_cancellation_records ADD CONSTRAINT assistant_cancellation_records_pkey PRIMARY KEY (id, content_hash);
ALTER TABLE ods.assistant_service_records ADD CONSTRAINT assistant_service_records_pkey PRIMARY KEY (id, content_hash);
ALTER TABLE ods.goods_stock_movements ADD CONSTRAINT goods_stock_movements_pkey PRIMARY KEY (sitegoodsstockid, content_hash);
ALTER TABLE ods.goods_stock_summary ADD CONSTRAINT goods_stock_summary_pkey PRIMARY KEY (sitegoodsid, content_hash);
@@ -1091,8 +1069,6 @@ ALTER TABLE ods.tenant_goods_master ADD CONSTRAINT tenant_goods_master_pkey PRIM
-- 索引
CREATE INDEX idx_assistant_accounts_master_fetched_at_fetched_at ON ods.assistant_accounts_master USING btree (fetched_at);
CREATE INDEX idx_assistant_accounts_master_fetched_pk_d986993f ON ods.assistant_accounts_master USING btree (fetched_at, id, content_hash);
CREATE INDEX idx_assistant_cancellation_records_fetched_at_fetched_at ON ods.assistant_cancellation_records USING btree (fetched_at);
CREATE INDEX idx_assistant_cancellation_records_fetched_pk_258b411c ON ods.assistant_cancellation_records USING btree (fetched_at, id, content_hash);
CREATE INDEX idx_assistant_service_records_fetched_at_fetched_at ON ods.assistant_service_records USING btree (fetched_at);
CREATE INDEX idx_assistant_service_records_fetched_pk_e200787c ON ods.assistant_service_records USING btree (fetched_at, id, content_hash);
CREATE INDEX idx_goods_stock_movements_fetched_at_fetched_at ON ods.goods_stock_movements USING btree (fetched_at);

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- FDW 跨库映射(在 zqyy_app 中执行)
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源db/fdw/setup_fdw.sql
-- =============================================================================

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / auth用户认证与权限
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -42,7 +42,7 @@ CREATE TABLE auth.site_code_mapping (
site_code character varying(10) NOT NULL,
site_id bigint NOT NULL,
site_name character varying(200),
tenant_id integer,
tenant_id bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
@@ -86,7 +86,7 @@ CREATE TABLE auth.users (
wx_avatar_url text,
nickname character varying(100),
phone character varying(20),
status character varying(20) DEFAULT 'pending'::character varying NOT NULL,
status character varying(20) DEFAULT 'new'::character varying NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);

View File

@@ -0,0 +1,89 @@
-- =============================================================================
-- zqyy_app / biz核心业务表任务/备注/触发器))
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS biz;
-- 序列
CREATE SEQUENCE IF NOT EXISTS biz.coach_task_history_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.coach_tasks_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.notes_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.trigger_jobs_id_seq AS integer;
-- 表
CREATE TABLE biz.coach_task_history (
id bigint DEFAULT nextval('biz.coach_task_history_id_seq'::regclass) NOT NULL,
task_id bigint NOT NULL,
action character varying(50) NOT NULL,
old_status character varying(20),
new_status character varying(20),
old_task_type character varying(50),
new_task_type character varying(50),
detail jsonb,
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.coach_tasks (
id bigint DEFAULT nextval('biz.coach_tasks_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
assistant_id bigint NOT NULL,
member_id bigint NOT NULL,
task_type character varying(50) NOT NULL,
status character varying(20) DEFAULT 'active'::character varying NOT NULL,
priority_score numeric(5,2),
expires_at timestamp with time zone,
is_pinned boolean DEFAULT false,
abandon_reason text,
completed_at timestamp with time zone,
completed_task_type character varying(50),
parent_task_id bigint,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.notes (
id bigint DEFAULT nextval('biz.notes_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
user_id integer NOT NULL,
target_type character varying(50) NOT NULL,
target_id bigint NOT NULL,
type character varying(20) DEFAULT 'normal'::character varying NOT NULL,
content text NOT NULL,
rating_service_willingness smallint,
rating_revisit_likelihood smallint,
task_id bigint,
ai_score smallint,
ai_analysis text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.trigger_jobs (
id integer DEFAULT nextval('biz.trigger_jobs_id_seq'::regclass) NOT NULL,
job_type character varying(100) NOT NULL,
job_name character varying(100) NOT NULL,
trigger_condition character varying(20) NOT NULL,
trigger_config jsonb NOT NULL,
last_run_at timestamp with time zone,
next_run_at timestamp with time zone,
status character varying(20) DEFAULT 'enabled'::character varying NOT NULL,
created_at timestamp with time zone DEFAULT now()
);
-- 约束(主键 / 唯一 / 外键)
ALTER TABLE biz.coach_task_history ADD CONSTRAINT coach_task_history_task_id_fkey FOREIGN KEY (task_id) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.coach_task_history ADD CONSTRAINT coach_task_history_pkey PRIMARY KEY (id);
ALTER TABLE biz.coach_tasks ADD CONSTRAINT coach_tasks_parent_task_id_fkey FOREIGN KEY (parent_task_id) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.coach_tasks ADD CONSTRAINT coach_tasks_pkey PRIMARY KEY (id);
ALTER TABLE biz.notes ADD CONSTRAINT notes_task_id_fkey FOREIGN KEY (task_id) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.notes ADD CONSTRAINT notes_pkey PRIMARY KEY (id);
ALTER TABLE biz.trigger_jobs ADD CONSTRAINT trigger_jobs_pkey PRIMARY KEY (id);
ALTER TABLE biz.trigger_jobs ADD CONSTRAINT trigger_jobs_job_name_key UNIQUE (job_name);
-- 索引
CREATE INDEX idx_coach_tasks_assistant_status ON biz.coach_tasks USING btree (site_id, assistant_id, status);
CREATE UNIQUE INDEX idx_coach_tasks_site_assistant_member_type ON biz.coach_tasks USING btree (site_id, assistant_id, member_id, task_type) WHERE ((status)::text = 'active'::text);
CREATE INDEX idx_notes_target ON biz.notes USING btree (site_id, target_type, target_id);

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / public小程序业务表
-- 生成日期2026-02-25
-- 生成日期2026-02-27
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -9,7 +9,7 @@ CREATE SCHEMA IF NOT EXISTS public;
-- 序列
CREATE SEQUENCE IF NOT EXISTS public.admin_users_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS public.approvals_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS public.member_birthday_manual_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS public.member_retention_clue_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS public.permissions_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS public.roles_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS public.tasks_id_seq AS bigint;
@@ -37,15 +37,17 @@ CREATE TABLE public.approvals (
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE public.member_birthday_manual (
id bigint DEFAULT nextval('member_birthday_manual_id_seq'::regclass) NOT NULL,
CREATE TABLE public.member_retention_clue (
id bigint DEFAULT nextval('member_retention_clue_id_seq'::regclass) NOT NULL,
member_id bigint NOT NULL,
birthday_value date NOT NULL,
category character varying(20) NOT NULL,
summary character varying(200) NOT NULL,
detail text,
recorded_by_assistant_id bigint,
recorded_by_name character varying(50),
recorded_at timestamp with time zone DEFAULT now() NOT NULL,
source character varying(20) DEFAULT 'assistant'::character varying,
site_id bigint NOT NULL
site_id bigint NOT NULL,
source character varying(20) DEFAULT 'manual'::character varying NOT NULL
);
CREATE TABLE public.permissions (
@@ -149,8 +151,10 @@ ALTER TABLE admin_users ADD CONSTRAINT admin_users_username_key UNIQUE (username
ALTER TABLE approvals ADD CONSTRAINT approvals_approver_id_fkey FOREIGN KEY (approver_id) REFERENCES users(id);
ALTER TABLE approvals ADD CONSTRAINT approvals_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE;
ALTER TABLE approvals ADD CONSTRAINT approvals_pkey PRIMARY KEY (id);
ALTER TABLE member_birthday_manual ADD CONSTRAINT member_birthday_manual_pkey PRIMARY KEY (id);
ALTER TABLE member_birthday_manual ADD CONSTRAINT uk_member_birthday_manual UNIQUE (member_id, recorded_by_assistant_id);
ALTER TABLE member_retention_clue ADD CONSTRAINT member_retention_clue_pkey PRIMARY KEY (id);
ALTER TABLE member_retention_clue ADD CONSTRAINT chk_retention_clue_category CHECK (
category IN ('客户基础', '消费习惯', '玩法偏好', '促销偏好', '社交关系', '重要反馈')
);
ALTER TABLE permissions ADD CONSTRAINT permissions_pkey PRIMARY KEY (id);
ALTER TABLE permissions ADD CONSTRAINT permissions_resource_action_key UNIQUE (resource, action);
ALTER TABLE role_permissions ADD CONSTRAINT role_permissions_permission_id_fkey FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE;
@@ -175,8 +179,9 @@ ALTER TABLE users ADD CONSTRAINT users_wx_openid_key UNIQUE (wx_openid);
CREATE INDEX idx_admin_users_site ON public.admin_users USING btree (site_id);
CREATE INDEX idx_approvals_site_id ON public.approvals USING btree (site_id);
CREATE INDEX idx_approvals_task_id ON public.approvals USING btree (task_id);
CREATE INDEX idx_mbd_member ON public.member_birthday_manual USING btree (member_id);
CREATE INDEX idx_mbd_site_id ON public.member_birthday_manual USING btree (site_id);
CREATE INDEX idx_retention_clue_category ON public.member_retention_clue USING btree (member_id, category);
CREATE INDEX idx_retention_clue_member ON public.member_retention_clue USING btree (member_id);
CREATE INDEX idx_retention_clue_site ON public.member_retention_clue USING btree (site_id);
CREATE INDEX idx_roles_site_id ON public.roles USING btree (site_id);
CREATE INDEX idx_scheduled_tasks_next_run ON public.scheduled_tasks USING btree (next_run_at) WHERE (enabled = true);
CREATE INDEX idx_scheduled_tasks_site ON public.scheduled_tasks USING btree (site_id);