chore: 文档与 IDE 配置整理

- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro)
- CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/
- 新增 /spec-close、/pre-change 两个工作流命令
- DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表)
- BD_Manual → BD_manual 命名统一(48 个文件)
- 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数)
- 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表)
- 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档)
- docs/database/README.md 索引更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:02:37 +08:00
parent 8228b3fa37
commit 70324d8542
185 changed files with 13595 additions and 1219 deletions

View File

@@ -1,22 +1,26 @@
# BD_Manualbiz Schema AI 表(对话记录 + 消息 + 缓存)
# BD_Manualbiz Schema AI 表(对话记录 + 消息 + 缓存 + 运行日志 + 调度记录
> 目标库:`test_zqyy_app`(通过 `APP_DB_DSN` 连接)
> 迁移脚本:
> - `db/zqyy_app/migrations/2026-03-08__create_ai_tables.sql`(初始建表)
> - `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`RNS1.4 CHAT 扩展)
> 关联 SPEC`05-miniapp-ai-integration`P5 AI 集成层)、`rns1-chat-integration`RNS1.4 CHAT 对齐与联调收尾
> - `db/zqyy_app/migrations/2026-03-22__p14_ai_module.sql`P14 DashScope 迁移 + 调度器完善
> - `db/zqyy_app/migrations/2026-03-23__p15_ai_monitoring.sql`P15 AI 监控后台 — alert_status + BRIN 索引)
> 关联 SPEC`05-miniapp-ai-integration`P5 AI 集成层)、`rns1-chat-integration`RNS1.4 CHAT 对齐与联调收尾)、`P14-ai-dashscope-migration`P14 DashScope 迁移)、`ai-monitoring-testing`P15 AI 监控后台)
---
## 1. 变更说明
### 新增表(3 张P5 初始建表
### 新增表(5 张
| # | 表名 | 用途 | 字段数(初始→当前) |
|---|------|------|---------------------|
| 1 | `biz.ai_conversations` | AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条 | 8 → 13 |
| 2 | `biz.ai_messages` | AI 消息记录:对话中的每条消息(输入/输出/系统) | 6 → 7 |
| 3 | `biz.ai_cache` | AI 应用缓存:各应用的结构化输出结果 | 9 |
| # | 表名 | 用途 | 字段数(初始→当前) | 来源 |
|---|------|------|---------------------|------|
| 1 | `biz.ai_conversations` | AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条 | 8 → 14 | P5 |
| 2 | `biz.ai_messages` | AI 消息记录:对话中的每条消息(输入/输出/系统) | 6 → 7 | P5 |
| 3 | `biz.ai_cache` | AI 应用缓存:各应用的结构化输出结果 | 9 → 10 | P5 |
| 4 | `biz.ai_run_logs` | **P14 新增** — AI 运行记录:每次 DashScope API 调用的详细日志 | 14 → 15 | P14, P15 |
| 5 | `biz.ai_trigger_jobs` | **P14 新增** — 调度运行记录:每次 AI 事件触发的编排执行记录 | 13 | P14 |
### RNS1.4 CHAT 模块扩展字段2026-03-20
@@ -25,9 +29,26 @@
| 1 | `biz.ai_conversations` | `context_type`, `context_id`, `title`, `last_message`, `last_message_at` | 多入口对话复用 + 历史列表展示与排序 |
| 2 | `biz.ai_messages` | `reference_card` | 引用卡片 JSON客户概览等结构化上下文数据 |
### P14 DashScope 迁移扩展字段2026-03-22
| # | 表名 | 新增字段 | 用途 |
|---|------|---------|------|
| 1 | `biz.ai_conversations` | `session_id` | 百炼 session_id格式 `conv_{id}_{ts}`),仅 App1 使用 |
| 2 | `biz.ai_cache` | `status` | 缓存状态valid / expired / invalidated / generating |
### P15 AI 监控后台扩展2026-03-23
| # | 表名 | 变更类型 | 说明 |
|---|------|---------|------|
| 1 | `biz.ai_run_logs` | 新增字段 `alert_status` | 告警处理状态NULL / pending / acknowledged / ignored |
| 2 | `biz.ai_run_logs` | 新增约束 `chk_ai_run_logs_alert_status` | CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')) |
| 3 | `biz.ai_run_logs` | 新增部分索引 `idx_ai_run_logs_alert` | (alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open') — 告警列表查询 |
| 4 | `biz.ai_run_logs` | 新增 BRIN 索引 `idx_ai_run_logs_created_brin` | BRIN (created_at) WITH (pages_per_range = 32) — Dashboard 聚合优化 |
| 5 | `biz.ai_run_logs` | 回填 | 已有 status IN ('failed','timeout','circuit_open') 的记录 alert_status 设为 'pending' |
### 表字段明细
#### biz.ai_conversations13 字段)
#### biz.ai_conversations14 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
@@ -39,11 +60,12 @@
| `source_page` | VARCHAR(100) | 可空 | 来源页面标识 |
| `source_context` | JSONB | 可空 | 页面上下文 JSON |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `context_type` | VARCHAR(20) | 可空 | **RNS1.4 新增** — 对话关联上下文类型task(任务)/ customer(客户)/ coach(助教)/ general(通用) |
| `context_id` | VARCHAR(50) | 可空 | **RNS1.4 新增** — 关联上下文 IDtask 入口为 taskIdcustomer 入口为 customerIdcoach 入口为 coachIdgeneral 为 NULL |
| `title` | VARCHAR(200) | 可空 | **RNS1.4 新增** — 对话标题:自定义 > 上下文名称 > 首条消息前20字 |
| `context_type` | VARCHAR(20) | 可空 | **RNS1.4 新增** — 对话关联上下文类型task / customer / coach / general |
| `context_id` | VARCHAR(50) | 可空 | **RNS1.4 新增** — 关联上下文 ID |
| `title` | VARCHAR(200) | 可空 | **RNS1.4 新增** — 对话标题 |
| `last_message` | TEXT | 可空 | **RNS1.4 新增** — 最后一条消息内容摘要截断至100字 |
| `last_message_at` | TIMESTAMPTZ | 可空 | **RNS1.4 新增** — 最后消息时间,用于历史列表排序和对话复用时限判断 |
| `last_message_at` | TIMESTAMPTZ | 可空 | **RNS1.4 新增** — 最后消息时间 |
| `session_id` | VARCHAR(100) | 可空 | **P14 新增** — 百炼 session_id格式 `conv_{conversation_id}_{created_timestamp}`,仅 App1 使用 |
#### biz.ai_messages7 字段)
@@ -57,7 +79,7 @@
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `reference_card` | JSONB | 可空 | **RNS1.4 新增** — 引用卡片 JSON`{type, title, summary, data}`,用于展示客户概览等结构化上下文数据 |
#### biz.ai_cache9 字段)
#### biz.ai_cache10 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
@@ -70,6 +92,7 @@
| `triggered_by` | VARCHAR(100) | 可空 | 触发来源标识 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `expires_at` | TIMESTAMPTZ | 可空 | 可选过期时间 |
| `status` | VARCHAR(20) | DEFAULT 'valid', CHECK | **P14 新增** — 缓存状态valid有效/ expired已过期/ invalidated手动失效/ generating生成中 |
### cache_type 枚举值与 target_id 约定
@@ -89,14 +112,62 @@
|----|-----------|------|------|
| `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_conversations` | `idx_ai_conv_context` | INDEX条件 | **RNS1.4**`(user_id, site_id, context_type, context_id, last_message_at DESC) WHERE context_type IS NOT NULL` |
| `ai_conversations` | `idx_ai_conv_last_msg` | INDEX | **RNS1.4**`(user_id, site_id, last_message_at DESC NULLS LAST)` |
| `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` | `chk_ai_cache_status` | CHECK | **P14**`status IN ('valid', 'expired', 'invalidated', 'generating')` |
| `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 排序便于删除最旧) |
| `ai_conversations` | `idx_ai_conv_context` | INDEX(条件) | **RNS1.4 新增**`(user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST) WHERE context_type IS NOT NULL` — 上下文对话查找(多入口复用) |
| `ai_conversations` | `idx_ai_conv_last_msg` | INDEX | **RNS1.4 新增**`(user_id, site_id, last_message_at DESC NULLS LAST)` — 历史列表排序优化CHAT-1 倒序) |
| `ai_cache` | `idx_ai_cache_cleanup` | INDEX | `(cache_type, site_id, target_id, created_at)` — 清理超限记录 |
| `ai_run_logs` | `idx_ai_run_logs_site_app` | INDEX | **P14**`(site_id, app_type)` |
| `ai_run_logs` | `idx_ai_run_logs_created` | INDEX | **P14**`(created_at)` — Token 预算聚合 |
| `ai_run_logs` | `idx_ai_run_logs_status` | INDEX | **P14**`(status)` |
| `ai_run_logs` | `chk_ai_run_logs_alert_status` | CHECK | **P15**`alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')` |
| `ai_run_logs` | `idx_ai_run_logs_alert` | INDEX部分 | **P15**`(alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open')` — 告警列表查询优化 |
| `ai_run_logs` | `idx_ai_run_logs_created_brin` | BRIN INDEX | **P15**`BRIN (created_at) WITH (pages_per_range = 32)` — Dashboard 聚合查询优化,适合按时间顺序插入的表 |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_site` | INDEX | **P14**`(site_id, event_type)` |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_dedup` | INDEX条件 | **P14**`(event_type, member_id, site_id, created_at) WHERE status NOT IN ('skipped_duplicate')` — 去重 |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_status` | INDEX | **P14**`(status)` |
#### biz.ai_run_logs15 字段P14 新增 + P15 扩展)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `app_type` | VARCHAR(30) | NOT NULL | 应用类型app1_chat / app2_finance / ... / app8_consolidate |
| `trigger_type` | VARCHAR(20) | NOT NULL | 触发类型user / scheduled / event / forced |
| `member_id` | BIGINT | 可空 | 会员 ID |
| `request_prompt` | TEXT | 可空 | 请求 prompt截断前 2000 字符) |
| `response_text` | TEXT | 可空 | 响应文本 |
| `tokens_used` | INTEGER | DEFAULT 0 | 消耗 token 数 |
| `latency_ms` | INTEGER | 可空 | 调用延迟(毫秒) |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态pending / running / success / failed / timeout / budget_exceeded |
| `error_message` | TEXT | 可空 | 错误信息 |
| `session_id` | VARCHAR(100) | 可空 | 百炼 session_id仅 App1 |
| `alert_status` | VARCHAR(20) | DEFAULT NULL, CHECK | **P15 新增** — 告警处理状态NULL非告警记录/ pending待处理/ acknowledged已确认/ ignored已忽略。仅 status IN ('failed','timeout','circuit_open') 的记录才设置此字段 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 |
| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 |
#### biz.ai_trigger_jobs13 字段P14 新增)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `event_type` | VARCHAR(30) | NOT NULL | 事件类型consumption / dws_completed / note_created / task_assigned |
| `connector_type` | VARCHAR(30) | DEFAULT 'feiqiu' | 连接器类型 |
| `member_id` | BIGINT | 可空 | 会员 ID |
| `payload` | JSONB | 可空 | 附加数据 |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态pending / running / completed / failed / skipped_duplicate / budget_exceeded |
| `is_forced` | BOOLEAN | DEFAULT false | 是否强制执行(跳过去重检查) |
| `app_chain` | VARCHAR(100) | 可空 | 调用链描述,如 `app3→app8→app7` |
| `started_at` | TIMESTAMPTZ | 可空 | 开始时间 |
| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 |
| `error_message` | TEXT | 可空 | 错误信息 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 |
---
@@ -104,19 +175,56 @@
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。AI 表属于 `biz` Schema不参与 ETL 流程 |
| 后端 API | 直接依赖。P5 AI 模块(`apps/backend/app/ai/`基于这三张表实现对话持久化、缓存读写、SSE 流式对话等功能 |
| 后端 APIRNS1.4 | **直接依赖**。CHAT 模块`apps/backend/app/routers/xcx_chat.py``apps/backend/app/services/chat_service.py`)依赖 `ai_conversations` 的 5 个新字段(`context_type`/`context_id`/`title`/`last_message`/`last_message_at`)实现多入口对话复用、历史列表展示与排序;依赖 `ai_messages.reference_card` 存储引用卡片 JSON |
| ETL 任务 | **P14 新增**DWS 任务完成后通过 `utils/ai_trigger.py` 发送 HTTP 触发事件到后端 `ai_trigger_jobs`,失败不中断 ETL 流程 |
| 后端 API | 直接依赖。P5 AI 模块(`apps/backend/app/ai/`)基于这表实现对话持久化、缓存读写、SSE 流式对话等功能 |
| 后端 APIRNS1.4 | **直接依赖**。CHAT 模块依赖 `ai_conversations` 的 5 个 RNS1.4 字段实现多入口对话复用 |
| 后端 APIP14 | **直接依赖**`ai_run_logs` 用于 Token 预算聚合BudgetTracker`ai_trigger_jobs` 用于事件去重和调度记录;`ai_conversations.session_id` 用于百炼会话管理;`ai_cache.status` 用于缓存状态控制 |
| 小程序 | 间接依赖。小程序通过后端 AI API 间接使用对话和缓存数据 |
| 小程序RNS1.4 | **接依赖**`pages/chat/chat.ts``pages/chat-history/chat-history.ts` 通过 CHAT-1/2/3/4 端点间接依赖新字段(`title`→对话标题、`lastMessage`→摘要、`timestamp`→排序、`referenceCard`→引用卡片渲染) |
| 管理后台 | 暂无影响。后续可能增加 AI 调用统计和缓存管理界面 |
| `member_retention_clue` | 间接关联。App8维客线索整理的结果同时写入 `ai_cache``member_retention_clue` 表 |
| 现有 `biz` Schema | 兼容。P5 新增 3 张表RNS1.4 仅在已有表上 ADD COLUMN / CREATE INDEX不修改已有字段或约束 |
| 管理后台 | **P15 直接依赖**admin-web AI 监控后台4 个页面)依赖 `ai_run_logs.alert_status` 实现告警管理(确认/忽略),依赖 BRIN 索引优化 Dashboard 聚合查询性能 |
| `member_retention_clue` | 间接关联。App8 结果同时写入 `ai_cache``member_retention_clue`P14 实现幂等 DELETE+INSERT |
---
## 3. 回滚策略
### 3d. 回滚 P15 AI 监控后台2026-03-23 迁移)
按逆序 DROP 新增索引、约束和字段:
```sql
BEGIN;
DROP INDEX IF EXISTS biz.idx_ai_run_logs_created_brin;
DROP INDEX IF EXISTS biz.idx_ai_run_logs_alert;
ALTER TABLE biz.ai_run_logs DROP CONSTRAINT IF EXISTS chk_ai_run_logs_alert_status;
ALTER TABLE biz.ai_run_logs DROP COLUMN IF EXISTS alert_status;
COMMIT;
```
注意:
- 回滚后 admin-web AI 监控后台的告警管理功能将不可用
- Dashboard 聚合查询性能可能下降(失去 BRIN 索引)
- 回滚不影响 P14 的核心功能(调度器、预算追踪等)
### 3c. 回滚 P14 DashScope 迁移2026-03-22 迁移)
按逆序 DROP 新增表和字段:
```sql
-- 删除 P14 新增约束和字段
ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_status;
ALTER TABLE biz.ai_cache DROP COLUMN IF EXISTS status;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS session_id;
-- 删除 P14 新增表
DROP TABLE IF EXISTS biz.ai_trigger_jobs;
DROP TABLE IF EXISTS biz.ai_run_logs;
```
注意:
- 回滚后 P14 AI 调度器dispatcher、Token 预算追踪BudgetTracker将无法正常工作
- `ai_run_logs``ai_trigger_jobs` 中的数据将丢失,需先备份
- 回滚不影响 P5 和 RNS1.4 的功能
### 3a. 回滚 RNS1.4 CHAT 扩展2026-03-20 迁移)
按逆序 DROP 新增索引和字段:
@@ -185,7 +293,7 @@ SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
ORDER BY ordinal_position;
-- 预期:返回 13P5 原始 8 字段 + RNS1.4 新增 5 字段
-- 预期:返回 14P5 原始 8 + RNS1.4 新增 5 + P14 session_id = 14
-- 3. 验证 ai_messages 的外键和 CHECK 约束
SELECT conname, contype, pg_get_constraintdef(oid) AS constraint_def
@@ -198,7 +306,7 @@ ORDER BY conname;
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 个枚举值
-- 预期:返回 2chk_ai_cache_type 含 7 个枚举值chk_ai_cache_status 含 4 个状态值)
-- 5. 验证 P5 初始索引全部存在5 个)
SELECT indexname
@@ -244,3 +352,101 @@ WHERE schemaname = 'biz' AND tablename = 'ai_conversations'
-- idx_ai_conv_context — 含 WHERE context_type IS NOT NULL 条件
-- idx_ai_conv_last_msg — (user_id, site_id, last_message_at DESC NULLS LAST)
```
### 4c. P14 DashScope 迁移验证
```sql
-- 9. 验证 ai_run_logs 表存在且字段正确
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_run_logs'
ORDER BY ordinal_position;
-- 预期:返回 15 行
-- 10. 验证 ai_trigger_jobs 表存在且字段正确
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_trigger_jobs'
ORDER BY ordinal_position;
-- 预期:返回 13 行
-- 11. 验证 ai_run_logs 索引(含 PK
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs';
-- 预期6 行PK + 3 P14 索引 + 2 P15 索引)
-- 12. 验证 ai_trigger_jobs 索引(含去重部分索引)
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_trigger_jobs';
-- 预期4 行PK + 3 个索引)
-- 13. 验证 ai_conversations.session_id 字段
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
AND column_name = 'session_id';
-- 预期1 行varchar(100)
-- 14. 验证 ai_cache.status 字段 + CHECK 约束
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_cache'
AND column_name = 'status';
-- 预期1 行varchar(20)default 'valid'
SELECT conname FROM pg_constraint
WHERE conrelid = 'biz.ai_cache'::regclass AND conname = 'chk_ai_cache_status';
-- 预期1 行
```
### 4d. P15 AI 监控后台验证
```sql
-- 15. 验证 ai_run_logs.alert_status 字段存在
SELECT column_name, data_type, character_maximum_length, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_run_logs'
AND column_name = 'alert_status';
-- 预期1 行varchar(20)default NULL
-- 16. 验证 alert_status CHECK 约束
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_run_logs'::regclass
AND conname = 'chk_ai_run_logs_alert_status';
-- 预期1 行CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored'))
-- 17. 验证 P15 新增索引(部分索引 + BRIN 索引)
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs'
AND indexname IN ('idx_ai_run_logs_alert', 'idx_ai_run_logs_created_brin');
-- 预期2 行
-- idx_ai_run_logs_alert — 含 WHERE status IN ('failed', 'timeout', 'circuit_open')
-- idx_ai_run_logs_created_brin — USING brin
-- 18. 验证 ai_run_logs 总索引数P14 3个 + P15 2个 + PK = 6
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs';
-- 预期6 行
-- 19. 验证回填结果:失败/超时/熔断记录的 alert_status 应为 'pending'
SELECT COUNT(*) AS unset_alerts
FROM biz.ai_run_logs
WHERE status IN ('failed', 'timeout', 'circuit_open')
AND alert_status IS NULL;
-- 预期0所有失败记录已回填为 'pending'
```
### P15 admin API 查询模式说明
P15 admin-web AI 监控后台引入以下典型查询模式:
| 查询场景 | SQL 模式 | 使用索引 |
|---------|---------|---------|
| Dashboard 今日统计 | `SELECT COUNT(*), AVG(latency_ms) FROM ai_run_logs WHERE created_at >= 今日零点` | `idx_ai_run_logs_created_brin` |
| Dashboard 7天趋势 | `SELECT date_trunc('day', created_at), COUNT(*) FROM ai_run_logs WHERE created_at >= 7天前 GROUP BY 1` | `idx_ai_run_logs_created_brin` |
| 告警列表 | `SELECT * FROM ai_run_logs WHERE status IN ('failed','timeout','circuit_open') AND alert_status = 'pending' ORDER BY created_at DESC` | `idx_ai_run_logs_alert` |
| 告警确认/忽略 | `UPDATE ai_run_logs SET alert_status = 'acknowledged' WHERE id = ?` | PK |
| 调用记录分页 | `SELECT * FROM ai_run_logs WHERE site_id = ? AND app_type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?` | `idx_ai_run_logs_site_app` |
| Token 预算聚合 | `SELECT SUM(tokens_used) FROM ai_run_logs WHERE created_at >= 今日零点 AND status = 'success'` | `idx_ai_run_logs_created` (B-tree) |

View File

@@ -15,7 +15,7 @@
### 新增角色
- `app_reader`:只读角色(`LOGIN`),拥有 `app` Schema 的 `USAGE` + `SELECT` 权限
### 新增视图(39 张)
### 新增视图(49 张)
**DWD 层11 张,全部含 `site_id` 过滤):**
@@ -48,6 +48,7 @@
| `app.v_dws_assistant_salary_calc` | `dws.dws_assistant_salary_calc` |
| `app.v_dws_assistant_customer_stats` | `dws.dws_assistant_customer_stats` |
| `app.v_dws_assistant_finance_analysis` | `dws.dws_assistant_finance_analysis` |
| `app.v_dws_assistant_order_contribution` | `dws.dws_assistant_order_contribution` |
| `app.v_dws_finance_daily_summary` | `dws.dws_finance_daily_summary` |
| `app.v_dws_finance_income_structure` | `dws.dws_finance_income_structure` |
| `app.v_dws_finance_recharge_summary` | `dws.dws_finance_recharge_summary` |
@@ -59,6 +60,9 @@
| `app.v_dws_assistant_project_tag` | `dws.dws_assistant_project_tag` |
| `app.v_dws_member_project_tag` | `dws.dws_member_project_tag` |
| `app.v_dws_member_spending_power_index` | `dws.dws_member_spending_power_index` |
| `app.v_dws_coach_area_hours` | `dws.dws_coach_area_hours` |
| `app.v_dws_finance_area_daily` | `dws.dws_finance_area_daily` |
| `app.v_dws_finance_board_cache` | `dws.dws_finance_board_cache` |
**DWS 层 — cfg_* 配置表5 张,无 `site_id`**
@@ -70,15 +74,24 @@
| `app.v_cfg_index_parameters` | `dws.cfg_index_parameters` | 同上 |
| `app.v_cfg_area_category` | `dws.cfg_area_category` | DISTINCT 去重到 category 级别,排除 SPECIAL/OTHER按 sort_order 排序。用于项目类型筛选器CONFIG-1。2026-03-20 新增。 |
**快捷别名视图7 张,简化常用查询路径):**
| 视图 | 源视图 | 说明 |
|------|--------|------|
| `app.v_assistant` | `app.v_dim_assistant` | 助教维度快捷别名 |
| `app.v_assistant_daily` | `app.v_dws_assistant_daily_detail` | 助教日明细快捷别名 |
| `app.v_finance_daily` | `app.v_dws_finance_daily_summary` | 财务日汇总快捷别名 |
| `app.v_member` | `app.v_dim_member` | 会员维度快捷别名 |
| `app.v_member_consumption` | `app.v_dws_member_consumption_summary` | 会员消费汇总快捷别名 |
| `app.v_order_summary` | `app.v_dws_order_summary` | 订单汇总快捷别名 |
| `app.v_site` | `dwd.dim_site` | 门店维度快捷别名 |
### 权限配置
| 角色 | Schema | 权限 |
|------|--------|------|
| `app_reader` | `app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
### P2 预留(注释形式,暂不创建)
- `dws.dws_assistant_order_contribution``app.v_dws_assistant_order_contribution`
> `v_dws_member_spending_power_index`、`v_dws_assistant_project_tag`、`v_dws_member_project_tag` 已于 2026-03-19 正式创建(迁移脚本 `2026-03-19_add_board_rls_views.sql`)。
> `v_dws_finance_recharge_summary` 已于 2026-03-20 重建,新增 6 个赠送卡细分字段(`gift_liquor_balance`、`gift_table_fee_balance`、`gift_voucher_balance`、`gift_liquor_recharge`、`gift_table_fee_recharge`、`gift_voucher_recharge`)。迁移脚本:`db/zqyy_app/migrations/2026-03-20_rebuild_rls_view_gift_breakdown.sql`。关联 SPEC`gift-card-breakdown`。
@@ -119,7 +132,7 @@ DROP ROLE IF EXISTS app_reader;
-- 1. 验证 app Schema 存在
SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app';
-- 2. 验证视图数量(应为 39 张:原 35 + 2026-03-19 新增 3 + 2026-03-20 新增 1
-- 2. 验证视图数量(应为 49 张:11 DWD + 26 DWS + 5 cfg + 7 快捷别名
SELECT count(*) FROM information_schema.views WHERE table_schema = 'app';
-- 3. 验证 app_reader 角色存在且有 app Schema 权限

View File

@@ -4,24 +4,27 @@
> 迁移脚本:
> - `db/zqyy_app/migrations/2026-02-25__p3_create_auth_tables.sql`(建表)
> - `db/zqyy_app/migrations/2026-02-25__p3_seed_roles_permissions.sql`(种子数据)
> - `db/zqyy_app/migrations/2026-03-23__add_head_coach_manager_roles.sql`(新增 head_coach、manager 角色及权限映射)
> - `db/zqyy_app/migrations/2026-03-23__add_rejection_count_and_cancelled_status.sql`申请审核流程增强rejection_count + cancelled 状态)
> 关联 SPEC`miniapp-auth-system`P3 小程序用户认证系统)
---
## 1. 变更说明
### 新增表(8 张)
### 新增表(9 张)
| # | 表名 | 用途 | 主要字段 |
|---|------|------|---------|
| 1 | `auth.users` | 微信用户主表 | `id`(PK), `wx_openid`(UK), `wx_union_id`, `wx_avatar_url`, `nickname`, `phone`, `status`(默认 `pending`), `created_at`, `updated_at` |
| 2 | `auth.user_applications` | 用户入驻申请表 | `id`(PK), `user_id`(FK→users), `site_code`, `site_id`, `applied_role_text`, `employee_number`, `phone`, `status`(默认 `pending`), `reviewer_id`, `review_note`, `created_at`, `reviewed_at` |
| 3 | `auth.site_code_mapping` | 球房ID与门店映射表 | `id`(PK), `site_code`(UK), `site_id`(UK), `site_name`, `tenant_id`, `created_at` |
| 1 | `auth.users` | 微信用户主表 | `id`(PK), `wx_openid`(UK), `wx_union_id`, `wx_avatar_url`, `nickname`, `phone`, `avatar_url`, `status`(默认 `new`), `rejection_count`(默认 0累计被拒次数), `created_at`, `updated_at` |
| 2 | `auth.user_applications` | 用户入驻申请表 | `id`(PK), `user_id`(FK→users), `site_code`, `site_id`, `applied_role_text`, `employee_number`, `phone`, `status`(默认 `pending`,可选值: pending/approved/rejected/cancelled), `reviewer_id`, `review_note`, `created_at`, `reviewed_at` |
| 3 | `auth._archived_site_code_mapping` | [已废弃] 球房ID与门店映射表NS4.1 重命名,替代方案:`biz.sites` + `biz.site_code_history` | `id`(PK), `site_code`(UK), `site_id`(UK), `site_name`, `tenant_id`, `created_at` |
| 4 | `auth.roles` | 角色定义表 | `id`(PK), `code`(UK), `name`, `description`, `created_at` |
| 5 | `auth.permissions` | 权限定义表 | `id`(PK), `code`(UK), `name`, `description`, `created_at` |
| 6 | `auth.role_permissions` | 角色-权限关联表 | `role_id`(FK→roles), `permission_id`(FK→permissions),联合主键 |
| 7 | `auth.user_site_roles` | 用户-门店-角色关联表 | `id`(PK), `user_id`(FK→users), `site_id`, `role_id`(FK→roles), `created_at``(user_id, site_id, role_id)` 唯一约束 |
| 8 | `auth.user_assistant_binding` | 用户-人员绑定表 | `id`(PK), `user_id`(FK→users), `site_id`, `assistant_id`(可空), `staff_id`(可空), `binding_type`, `created_at` |
| 9 | `auth.tenant_admins` | 租户管理员表(详见 `BD_Manual_tenant_admin_tables.md` | `id`(PK), `username`(UK), `password_hash`, `display_name`, `tenant_id`, `managed_site_ids`, `is_active`, `deleted_at`, `created_by`, `created_at`, `last_login_at` |
### 约束与索引
@@ -29,14 +32,15 @@
|----|-----------|------|------|
| `users` | `uq_users_wx_openid` | UNIQUE | 微信 openid 唯一 |
| `users` | `ix_users_wx_openid` | INDEX | openid 查询加速 |
| `site_code_mapping` | `uq_site_code_mapping_site_code` | UNIQUE | 球房ID 唯一 |
| `site_code_mapping` | `uq_site_code_mapping_site_id` | UNIQUE | site_id 唯一映射 |
| `site_code_mapping` | `ix_site_code_mapping_site_code` | INDEX | site_code 查询加速 |
| `site_code_mapping` | `uq_site_code_mapping_site_code` | UNIQUE | 球房ID 唯一(已废弃,表已重命名为 `_archived_site_code_mapping` |
| `site_code_mapping` | `uq_site_code_mapping_site_id` | UNIQUE | site_id 唯一映射(已废弃) |
| `site_code_mapping` | `ix_site_code_mapping_site_code` | INDEX | site_code 查询加速(已废弃) |
| `roles` | `uq_roles_code` | UNIQUE | 角色 code 唯一 |
| `permissions` | `uq_permissions_code` | UNIQUE | 权限 code 唯一 |
| `role_permissions` | PK `(role_id, permission_id)` | PRIMARY KEY | 联合主键 |
| `role_permissions` | `fk_role_permissions_role_id` | FK | → `auth.roles(id)` CASCADE |
| `role_permissions` | `fk_role_permissions_permission_id` | FK | → `auth.permissions(id)` CASCADE |
| `user_applications` | `user_applications_status_check` | CHECK | status IN ('pending', 'approved', 'rejected', 'cancelled') |
| `user_applications` | `fk_user_applications_user_id` | FK | → `auth.users(id)` CASCADE |
| `user_applications` | `ix_user_applications_user_id` | INDEX | user_id 查询加速 |
| `user_applications` | `ix_user_applications_status` | INDEX | status 过滤加速 |
@@ -65,17 +69,21 @@
|------|------|-------------|
| `coach` | 助教 | 球房助教,可查看任务和助教看板 |
| `staff` | 员工 | 球房员工,可查看任务和数据看板 |
| `site_admin` | 店铺管理员 | 单店管理员,可查看所有看板 |
| `tenant_admin` | 租户管理员 | 租户级管理员,拥有全部权限 |
| `head_coach` | 教练 | 主教练,负责训练助教,可查看任务和全部看板 |
| `manager` | 管理员 | 球房管理员,可查看任务和全部看板,未来将与 staff 进一步区分权限 |
#### 角色-权限映射14 条)
> 注:`site_admin` 和 `tenant_admin` 已于 2026-03-23 从小程序 RBAC 体系中清理。租户/店铺管理员的区分通过 `auth.tenant_admins.admin_type` 列实现,不依赖 `auth.roles` 表。
#### 角色-权限映射11 条)
| 角色 | 权限列表 | 权限数 |
|------|---------|--------|
| `coach` | `view_tasks`, `view_board_coach` | 2 |
| `staff` | `view_tasks`, `view_board` | 2 |
| `site_admin` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 |
| `tenant_admin` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 |
| `head_coach` | `view_tasks`, `view_board` | 2 |
| `manager` | `view_tasks`, `view_board`, `view_board_finance`, `view_board_customer`, `view_board_coach` | 5 |
> 注:`site_admin`5 条)和 `tenant_admin`5 条)的权限映射已于 2026-03-23 删除,角色体系隔离后总映射从 24 条减为 11 条head_coach 仅分配 view_tasks + view_board非全部 5 个权限)。详见迁移脚本 `2026-03-23__cleanup_roles_add_admin_type.sql`。
---
@@ -100,10 +108,10 @@
```sql
-- 先删除种子数据(如需保留表结构)
DELETE FROM auth.role_permissions
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin'))
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager'))
AND permission_id IN (SELECT id FROM auth.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach'));
DELETE FROM auth.roles WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin');
DELETE FROM auth.roles WHERE code IN ('coach', 'staff', 'head_coach', 'manager');
DELETE FROM auth.permissions WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach');
-- 删除表按逆序CASCADE 处理外键依赖)
@@ -113,7 +121,7 @@ DROP TABLE IF EXISTS auth.user_applications CASCADE;
DROP TABLE IF EXISTS auth.role_permissions CASCADE;
DROP TABLE IF EXISTS auth.permissions CASCADE;
DROP TABLE IF EXISTS auth.roles CASCADE;
DROP TABLE IF EXISTS auth.site_code_mapping CASCADE;
DROP TABLE IF EXISTS auth._archived_site_code_mapping CASCADE;
DROP TABLE IF EXISTS auth.users CASCADE;
```
@@ -124,17 +132,17 @@ DROP TABLE IF EXISTS auth.users CASCADE;
## 4. 验证 SQL
```sql
-- 1. 验证 auth Schema 下 8 张认证表全部存在
-- 1. 验证 auth Schema 下 9 张认证表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'auth'
AND table_name IN (
'users', 'user_applications', 'site_code_mapping',
'users', 'user_applications', '_archived_site_code_mapping',
'roles', 'permissions', 'role_permissions',
'user_site_roles', 'user_assistant_binding'
'user_site_roles', 'user_assistant_binding', 'tenant_admins'
)
ORDER BY table_name;
-- 预期:返回 8
-- 预期:返回 9
-- 2. 验证种子数据5 条权限
SELECT COUNT(*) AS perm_count
@@ -142,28 +150,111 @@ FROM auth.permissions
WHERE code IN ('view_tasks', 'view_board', 'view_board_finance', 'view_board_customer', 'view_board_coach');
-- 预期5
-- 3. 验证种子数据4 条角色
-- 3. 验证种子数据4 条角色site_admin/tenant_admin 已于 2026-03-23 删除)
SELECT COUNT(*) AS role_count
FROM auth.roles
WHERE code IN ('coach', 'staff', 'site_admin', 'tenant_admin');
WHERE code IN ('coach', 'staff', 'head_coach', 'manager');
-- 预期4
-- 4. 验证角色-权限映射数量
-- 4. 验证角色-权限映射数量11 条)
SELECT r.code AS role_code, COUNT(rp.permission_id) AS perm_count
FROM auth.roles r
JOIN auth.role_permissions rp ON r.id = rp.role_id
GROUP BY r.code
ORDER BY r.code;
-- 预期coach=2, site_admin=5, staff=2, tenant_admin=5共 14 条映射
-- 预期coach=2, head_coach=2, manager=5, staff=2共 11 条映射,无 site_admin/tenant_admin
-- 5. 验证关键约束存在
SELECT conname, contype
FROM pg_constraint
WHERE conrelid IN (
'auth.users'::regclass,
'auth.site_code_mapping'::regclass,
'auth._archived_site_code_mapping'::regclass,
'auth.user_site_roles'::regclass
)
ORDER BY conrelid::regclass::text, conname;
-- 预期:包含 uq_users_wx_openid、uq_site_code_mapping_site_code、uq_site_code_mapping_site_id、uq_user_site_roles_user_site_role 等
-- 预期:包含 uq_users_wx_openid、uq_site_code_mapping_site_code(已废弃表 _archived_site_code_mapping、uq_site_code_mapping_site_id、uq_user_site_roles_user_site_role 等
```
---
---
## 5. 变更记录2026-03-23 申请审核流程增强
### 5.1 变更说明
| 对象 | 变更类型 | 说明 |
|------|---------|------|
| `auth.users.rejection_count` | 新增字段 | `integer NOT NULL DEFAULT 0`,累计被管理员拒绝的申请次数,达到 3 次自动将 `status` 设为 `disabled` |
| `auth.user_applications.status` CHECK 约束 | 修改 | 新增 `'cancelled'` 可选值(用户主动取消申请),约束名 `user_applications_status_check` |
### 5.2 业务规则
- 管理员拒绝申请时:`rejection_count += 1`,第 3 次自动将 `users.status` 设为 `disabled`
- 用户主动取消(`cancelled`)不计入 `rejection_count`
- `cancelled` 状态的申请不在管理端申请列表中显示
- `disabled` 用户不允许重新申请,需管理员手动解除(功能待开发)
### 5.3 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL | 无影响,不涉及 ETL 库 |
| 后端 API | 直接依赖。`reject_application` 增加 rejection_count 累加逻辑;新增 `cancel_application` 服务和 `/api/xcx/cancel-application` 端点;`/api/xcx/me` 返回 `latestApplication` 详情 |
| 小程序 | 间接依赖。reviewing 页展示申请详情+重新申请按钮no-permission 页区分 rejected/disabled 状态 |
| 管理后台 | 间接依赖。拒绝端点自动累加 rejection_count 并触发禁用;申请列表排除 cancelled |
### 5.4 回滚策略
```sql
-- 回滚 rejection_count 字段
ALTER TABLE auth.users DROP COLUMN IF EXISTS rejection_count;
-- 回滚 status CHECK 约束(恢复为不含 cancelled
ALTER TABLE auth.user_applications DROP CONSTRAINT IF EXISTS user_applications_status_check;
ALTER TABLE auth.user_applications
ADD CONSTRAINT user_applications_status_check
CHECK (status IN ('pending', 'approved', 'rejected'));
-- 注意:回滚前需确认无 status='cancelled' 的记录,否则约束添加会失败
-- UPDATE auth.user_applications SET status = 'pending' WHERE status = 'cancelled';
```
### 5.5 验证 SQL
```sql
-- 1. 验证 rejection_count 字段存在且默认值正确
SELECT column_name, data_type, column_default, is_nullable
FROM information_schema.columns
WHERE table_schema = 'auth' AND table_name = 'users' AND column_name = 'rejection_count';
-- 预期integer, 0, NO
-- 2. 验证 CHECK 约束包含 cancelled
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'auth.user_applications'::regclass
AND conname = 'user_applications_status_check';
-- 预期:包含 'cancelled'
-- 3. 验证现有数据 rejection_count 默认值
SELECT COUNT(*) AS users_with_zero_rejection
FROM auth.users
WHERE rejection_count = 0;
-- 预期:等于 users 表总行数(所有现有用户默认 0
-- 4. 验证 cancelled 状态可正常写入dry-run 验证)
-- INSERT INTO auth.user_applications (user_id, site_code, site_id, applied_role_text, phone, status)
-- VALUES (1, 'TEST', 1, '测试', '13800000000', 'cancelled');
-- 预期:不报 CHECK 约束错误
```
---
<!-- AI_CHANGELOG
| 日期 | Prompt | 变更 |
|------|--------|------|
| 2026-03-23 | 角色路由+页面权限守卫 | 更新角色表从 4 条到 6 条,新增 head_coach/manager 角色及权限映射,更新验证 SQL 和回滚策略 |
| 2026-03-23 | 申请审核流程增强 | users 新增 rejection_count 字段user_applications.status CHECK 约束增加 cancelled新增第 5 节变更记录 |
| 2026-03-23 | 角色体系隔离+店铺管理员 | auth.roles 删除 site_admin/tenant_admin小程序 RBAC 不需要);角色从 6 条减为 4 条role_permissions 从 24 条减为 11 条head_coach 仅 view_tasks+view_board |
-->

View File

@@ -4,24 +4,28 @@
> 迁移脚本:
> - `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 小程序核心业务模块
> - `db/zqyy_app/migrations/2026-03-24__p17_task_engine_ownership.sql`P17 客户归属与转移
> - `db/zqyy_app/migrations/2026-03-24__p18_task_engine_dashboard.sql`P18 运营看板字段扩展)
> 关联 SPEC`04-miniapp-core-business`P4、`P17-assistant-ownership-task-engine`P17、`P18-admin-task-engine-dashboard`P18
---
## 1. 变更说明
### 新增表(4 张)
### 新增表(6 张)
| # | 表名 | 用途 | 字段数 |
|---|------|------|--------|
| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因等 | 15 |
| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃的追溯链 | 9 |
| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 15 |
| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 9 |
| # | 表名 | 用途 | 字段数 | 来源 |
|---|------|------|--------|------|
| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因、转移追踪等 | 18 | P4+P17 |
| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃/转移的追溯链 | 9 | P4 |
| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 15 | P4 |
| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 12 | P4+P18+P23 |
| 5 | `biz.cfg_task_generator_params` | 任务引擎参数配置表:支持全局默认 + 门店级覆盖 | 7 | P17+P18 |
| 6 | `biz.coach_task_transfer_log` | 客户转移日志表:记录每次转移的完整上下文 | 11 | P17 |
### 表字段明细
#### biz.coach_tasks15 字段)
#### biz.coach_tasks18 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
@@ -30,7 +34,7 @@
| `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` |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'active' | 状态:`active` / `inactive` / `completed` / `abandoned` / `transferred`P17/ `pending_review`P17 |
| `priority_score` | NUMERIC(5,2) | 可空 | 优先级分数,取 `max(WBI, NCI)` 快照 |
| `expires_at` | TIMESTAMPTZ | 可空 | 有效期时间戳NULL 表示无限期 |
| `is_pinned` | BOOLEAN | DEFAULT FALSE | 是否置顶 |
@@ -38,6 +42,9 @@
| `completed_at` | TIMESTAMPTZ | 可空 | 完成时间 |
| `completed_task_type` | VARCHAR(50) | 可空 | 完成时的任务类型快照 |
| `parent_task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 父任务 ID自引用 |
| `transfer_count` | INTEGER | NOT NULL DEFAULT 0 | 该客户在此任务链上的累计转移次数P17 新增) |
| `transferred_from` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 转移来源任务 IDP17 新增) |
| `transferred_at` | TIMESTAMPTZ | 可空 | 转移发生时间P17 新增) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
@@ -47,7 +54,7 @@
|------|------|------|------|
| `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` |
| `action` | VARCHAR(50) | NOT NULL | 操作类型:`created` / `type_changed` / `type_change_close` / `pinned` / `abandoned` / `cancel_abandon` / `expired` / `completed` / `expires_at_filled` / `transferred_out`P17/ `transferred_in`P17 |
| `old_status` | VARCHAR(20) | 可空 | 变更前状态 |
| `new_status` | VARCHAR(20) | 可空 | 变更后状态 |
| `old_task_type` | VARCHAR(50) | 可空 | 变更前任务类型 |
@@ -75,20 +82,53 @@
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
| `score` | SMALLINT | CHECK (1-5),可空 | 备注星星评分,助教创建备注时可选填写,不参与 AI 分析RNS1.1 新增) |
#### biz.trigger_jobs9 字段)
#### biz.trigger_jobs12 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | SERIAL | PK | 自增主键 |
| `job_type` | VARCHAR(100) | NOT NULL | 任务类型标识,映射到 Python handler |
| `job_name` | VARCHAR(100) | NOT NULL, UNIQUE | 任务名称(唯一) |
| `job_type` | VARCHAR(100) | NOT NULL | 任务类型标识,映射到 Python handler`_JOB_REGISTRY` 注册键) |
| `job_name` | VARCHAR(100) | NOT NULL, UNIQUE | 任务名称(唯一标识,如 `task_generator` |
| `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` |
| `description` | TEXT | 可空 | 任务中文描述管理后台页面展示P23 新增) |
| `last_error` | TEXT | 可空 | 最后一次执行异常的错误信息,成功后清空为 NULLP23 新增) |
| `last_stats` | JSONB | 可空 | 最近一次执行的统计结果 JSON`{"created":5,"replaced":2,"skipped":10,"transferred":1}`P18 新增) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
#### biz.cfg_task_generator_params7 字段P17+P18
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | 可空 | NULL=全局默认非NULL=门店级覆盖 |
| `param_key` | VARCHAR(64) | NOT NULL | 参数键名 |
| `param_value` | NUMERIC | NOT NULL | 参数值 |
| `description` | TEXT | 可空 | 参数说明 |
| `updated_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 更新时间 |
| `updated_by` | BIGINT | 可空 | 最近修改人 user_id用于审计追溯P18 新增) |
继承链:代码默认 → 全局默认site_id IS NULL→ 门店覆盖site_id = ?
#### biz.coach_task_transfer_log11 字段P17 新增)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `member_id` | BIGINT | NOT NULL | 客户 ID |
| `from_assistant_id` | BIGINT | NOT NULL | 原助教 ID |
| `to_assistant_id` | BIGINT | NOT NULL | 新助教 ID |
| `from_task_id` | BIGINT | NOT NULL, FK → `biz.coach_tasks(id)` | 原任务 ID |
| `to_task_id` | BIGINT | FK → `biz.coach_tasks(id)`,可空 | 新任务 ID |
| `transfer_reason` | TEXT | 可空 | 转移原因描述 |
| `guard_checks` | JSONB | 可空 | 三重保护检查结果快照 |
| `transfer_score` | NUMERIC | 可空 | 转移候选得分 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
### 约束与索引
| 表 | 约束/索引名 | 类型 | 说明 |
@@ -96,6 +136,7 @@
| `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_tasks` | FK `fk_coach_tasks_transferred_from` | FK | → `biz.coach_tasks(id)`转移来源任务P17 新增) |
| `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` |
@@ -103,6 +144,31 @@
| `notes` | CHECK `score` | CHECK | `score IS NULL OR (score >= 1 AND score <= 5)`RNS1.1 新增) |
| `notes` | FK `task_id` | FK | → `biz.coach_tasks(id)` |
| `trigger_jobs` | UNIQUE `job_name` | UNIQUE | 触发器名称唯一 |
| `cfg_task_generator_params` | UNIQUE `(site_id, param_key)` | UNIQUE | 全局+门店级参数唯一约束P17 新增) |
| `coach_task_transfer_log` | FK `from_task_id` | FK | → `biz.coach_tasks(id)`P17 新增) |
| `coach_task_transfer_log` | FK `to_task_id` | FK | → `biz.coach_tasks(id)`P17 新增) |
| `coach_task_transfer_log` | `idx_transfer_log_site_created` | INDEX | `(site_id, created_at DESC)`P17 新增) |
| `coach_task_transfer_log` | `idx_transfer_log_member` | INDEX | `(member_id, created_at DESC)`P17 新增) |
### P17 种子数据13 条任务引擎参数)
| param_key | param_value | description |
|-----------|-------------|-------------|
| `high_priority_recall_threshold` | 7.0 | max(WBI,NCI) 超过此值生成高优先召回 |
| `priority_recall_threshold` | 5.0 | max(WBI,NCI) 超过此值生成优先召回 |
| `rs_min_for_relationship` | 1.0 | RS ≤ 此值不生成关系构建 |
| `rs_max_for_relationship` | 6.0 | RS ≥ 此值不生成关系构建 |
| `consecutive_recall_fail_cycles` | 3 | 连续失败多少轮触发客户转移 |
| `min_wbi_for_transfer` | 5.0 | WBI 低于此值不触发转移 |
| `guard_assistant_coverage_ratio` | 0.5 | 绑定率低于此值禁用转移 |
| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 |
| `transfer_score_w_rs` | 0.5 | 转移候选排序RS 权重 |
| `transfer_score_w_ms` | 0.3 | 转移候选排序MS 权重 |
| `transfer_score_w_ml` | 0.2 | 转移候选排序ML 权重 |
| `max_transfer_count` | 2 | 单客户最大累计转移次数 |
| `follow_up_visit_retention_hours` | 48 | 回访任务最低保留时长(小时) |
> 所有参数 `site_id IS NULL`(全局默认),门店可通过插入 `site_id = ?` 的行覆盖。
### 种子数据4 条触发器配置)
@@ -120,13 +186,13 @@
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无直接影响。`biz` Schema 表不参与 ETL 流程,但任务生成器通过 FDW 只读访问 ETL 库的 WBI/NCI/RS 指数数据 |
| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现任务 CRUD`/api/xcx/tasks`)、备注 CRUD`/api/xcx/notes`)、触发器调度等功能 |
| 后端 API | 直接依赖。FastAPI 后端将基于这些表实现任务 CRUD`/api/xcx/tasks`)、备注 CRUD`/api/xcx/notes`)、触发器调度等功能。P17 新增:`task_generator.py` 完全重写,入口改为 OS 归属对;`fdw_queries.py` 新增 4 个批量查询方法 |
| 小程序 | 间接依赖。小程序通过后端 API 间接使用任务列表、备注功能 |
| 管理后台 | 暂无影响。后续可能增加任务监控和触发器管理界面 |
| 管理后台 | P18 已实施。`admin_task_engine` router 提供 9 个端点(转移日志分页+历史、待审核任务分页+重新分配+关闭、参数管理 CRUD前端 3 个页面TransferLog/PendingReview/TaskEngineConfig通过 `taskEngine.ts` API 层调用 |
| FDW 配置 | 无影响。`fdw_etl` Schema 独立于 `biz`,任务生成器和召回检测器通过 FDW 只读查询 ETL 库 |
| `auth` Schema | 间接依赖。任务生成器通过 `auth.user_assistant_binding` 确定助教与小程序用户的映射关系 |
| `auth` Schema | 间接依赖。P17 仍通过 `auth.user_assistant_binding` 获取 site_ids 和助教绑定信息(转移保护检查) |
| `public` Schema | 无影响。`member_retention_clue` 表独立于本次变更 |
| 现有 `biz` Schema | 兼容。`biz` Schema 已由 P1 迁移脚本创建,本次仅在其中新增 4 张表,不修改已有对象 |
| 现有 `biz` Schema | 兼容。`coach_tasks` 新增 3 字段均有默认值(`transfer_count DEFAULT 0`,其余可空),不影响现有查询。新增 `transferred`/`pending_review` 状态值7 个下游模块均使用显式 status 过滤,不会误匹配 |
---
@@ -170,6 +236,8 @@ RNS1.2(客户与助教接口)新增 3 个端点,引用了以下 biz/public
## 3. 回滚策略
### P4 回滚(原始 4 张表)
按逆序 `DROP TABLE IF EXISTS CASCADE`(迁移脚本末尾已包含注释形式的回滚语句):
```sql
@@ -194,6 +262,32 @@ DROP TABLE IF EXISTS biz.coach_task_history CASCADE;
DROP TABLE IF EXISTS biz.coach_tasks CASCADE;
```
### P17 回滚(增量变更)
```sql
-- 1. 删除 P17 种子数据
DELETE FROM biz.cfg_task_generator_params WHERE site_id IS NULL;
-- 2. 删除 P17 新增表
DROP TABLE IF EXISTS biz.coach_task_transfer_log;
DROP TABLE IF EXISTS biz.cfg_task_generator_params;
-- 3. 删除 P17 新增字段
ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transfer_count;
ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transferred_from;
ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS transferred_at;
-- 注意enum 值transferred/pending_review一旦添加无法直接删除需重建类型
```
### P18 回滚(字段扩展)
```sql
-- 删除 P18 新增字段
ALTER TABLE biz.trigger_jobs DROP COLUMN IF EXISTS last_stats;
ALTER TABLE biz.cfg_task_generator_params DROP COLUMN IF EXISTS updated_by;
```
注意:
- `CASCADE` 会级联删除依赖对象(外键引用的子表数据)
- 如果表中已有业务数据,需先备份再执行回滚
@@ -204,52 +298,90 @@ DROP TABLE IF EXISTS biz.coach_tasks CASCADE;
## 4. 验证 SQL
```sql
-- 1. 验证 biz Schema 下 4 张业务表全部存在
-- 1. 验证 biz Schema 下 6 张业务表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'biz'
AND table_name IN ('coach_tasks', 'coach_task_history', 'notes', 'trigger_jobs')
AND table_name IN ('coach_tasks', 'coach_task_history', 'notes', 'trigger_jobs',
'cfg_task_generator_params', 'coach_task_transfer_log')
ORDER BY table_name;
-- 预期:返回 4coach_task_history, coach_tasks, notes, trigger_jobs
-- 预期:返回 6
-- 2. 验证 coach_tasks 表字段数量和关键字段
-- 2. 验证 coach_tasks 表字段数量P4 原 15 + P17 新增 3 = 18
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 等
-- 预期:返回 18 行,包含 transfer_count/transferred_from/transferred_at
-- 3. 验证部分唯一索引存在
-- 3. 验证 P17 新增字段的默认值和约束
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'coach_tasks'
AND column_name IN ('transfer_count', 'transferred_from', 'transferred_at');
-- 预期transfer_count: integer, NOT NULL, DEFAULT 0
-- transferred_from: bigint, YES (nullable)
-- transferred_at: timestamp with time zone, YES (nullable)
-- 4. 验证 cfg_task_generator_params 表结构
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'cfg_task_generator_params'
ORDER BY ordinal_position;
-- 预期6 行id, site_id, param_key, param_value, description, updated_at
-- 5. 验证 P17 种子数据13 条全局默认参数)
SELECT param_key, param_value, description
FROM biz.cfg_task_generator_params
WHERE site_id IS NULL
ORDER BY param_key;
-- 预期:返回 13 行
-- 6. 验证 coach_task_transfer_log 表结构
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'coach_task_transfer_log'
ORDER BY ordinal_position;
-- 预期11 行id, site_id, member_id, from_assistant_id, to_assistant_id,
-- from_task_id, to_task_id, transfer_reason, guard_checks, transfer_score, created_at
-- 7. 验证 P17 新增索引
SELECT indexname
FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN ('idx_transfer_log_site_created', 'idx_transfer_log_member')
ORDER BY indexname;
-- 预期:返回 2 行
-- 8. 验证 transferred_from 外键约束
SELECT conname
FROM pg_constraint
WHERE conrelid = 'biz.coach_tasks'::regclass
AND conname = 'fk_coach_tasks_transferred_from';
-- 预期:返回 1 行
-- 9. 验证部分唯一索引存在P4 原有)
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
-- 10. 验证种子数据4 条触发器配置P4 原有
SELECT job_name, job_type, trigger_condition
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 行
-- 11. 验证 P18 新增字段trigger_jobs.last_stats
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'trigger_jobs' AND column_name = 'last_stats';
-- 预期:返回 1 行data_type = 'jsonb'is_nullable = 'YES'
-- 12. 验证 P18 新增字段cfg_task_generator_params.updated_by
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'cfg_task_generator_params' AND column_name = 'updated_by';
-- 预期:返回 1 行data_type = 'bigint'is_nullable = 'YES'
```

View File

@@ -34,9 +34,11 @@
### 导入的外部表
通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 38 张,2026-03-19 新增 3 张 BOARD 看板视图
通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 46 张,含 11 DWD + 23 DWS + 5 cfg + 7 快捷别名
- 11 张 DWD 视图:`v_dim_member``v_dim_assistant``v_dim_member_card_account``v_dim_table``v_dwd_settlement_head``v_dwd_table_fee_log``v_dwd_assistant_service_log``v_dwd_recharge_order``v_dwd_store_goods_sale``v_dim_staff``v_dim_staff_ex`
- 27 张 DWS 视图(含 4 张 cfg_* 配置表):`v_dws_member_consumption_summary``v_dws_member_visit_detail`
- 23 张 DWS 视图(含 `v_dws_assistant_project_tag``v_dws_member_project_tag``v_dws_member_spending_power_index``v_dws_assistant_order_contribution`
- 5 张 cfg_* 配置表视图(`v_cfg_performance_tier``v_cfg_assistant_level_price``v_cfg_bonus_rules``v_cfg_index_parameters``v_cfg_area_category`
- 7 张快捷别名视图(`v_assistant``v_assistant_daily``v_finance_daily``v_member``v_member_consumption``v_order_summary``v_site`
### 权限配置

View File

@@ -217,6 +217,7 @@ SELECT n.nspname AS schema_name, d.defaclacl AS default_acl
| 项目 | 状态 | 说明 |
|------|------|------|
| `source` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `source VARCHAR(20) NOT NULL DEFAULT 'manual'`2026-02-27FDW 外部表定义未包含。当前无 ETL 任务需要此字段,但未来如需读取线索来源需先更新外部表 |
| `is_hidden` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `is_hidden BOOLEAN NOT NULL DEFAULT false`2026-03-20NS4 迁移FDW 外部表定义未包含。如 ETL 任务需要过滤隐藏线索需先更新外部表 |
| DWS 任务消费 | 📋 待规划 | 原 `member_birthday_manual` 的 DWS 消费逻辑已移除。维客线索的 DWS 聚合任务尚未规划 |
### source 列同步方法(备用)

View File

@@ -14,6 +14,7 @@
| 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 枚举对齐:`客户基础信息``客户基础` |
| zqyy_app / test_zqyy_app | public | member_retention_clue.is_hidden | 新增列 | 2026-03-20 NS4 租户管理后台:隐藏/显示控制 |
### 表结构
@@ -29,6 +30,7 @@
| recorded_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 记录时间 |
| site_id | BIGINT | NOT NULL | 门店 ID多门店隔离 |
| source | VARCHAR(20) | NOT NULL DEFAULT 'manual' | 线索来源2026-02-27 新增) |
| is_hidden | BOOLEAN | NOT NULL DEFAULT false | 是否隐藏true=管理后台保留但小程序不展示2026-03-20 新增) |
### category 枚举值
@@ -65,6 +67,7 @@
- **后端 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'`,向后兼容
- **is_hidden 字段**2026-03-20 NS4租户管理后台线索隐藏/显示控制。`DEFAULT false` 保证已有数据兼容。小程序端线索查询已追加 `WHERE is_hidden = false` 条件,隐藏线索仅在管理后台可见。租户管理后台路由 `tenant_clues.py` 提供 `PATCH /api/tenant/clues/{id}/visibility` 端点切换状态
- **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 提交维客线索
@@ -102,13 +105,13 @@ SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'member_retention_clue';
-- 预期1 行
-- 3. 确认列结构完整10 列)
-- 3. 确认列结构完整11 列)
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
-- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id, source, is_hidden
-- 4. 确认 CHECK 约束
SELECT conname FROM pg_constraint
@@ -143,6 +146,23 @@ SELECT col_description(
-- 9. 确认已有数据的 source 分布
SELECT source, COUNT(*) FROM member_retention_clue GROUP BY source;
-- 预期:全部为 'manual'(或空表)
-- 10. 确认 is_hidden 列存在且默认值正确2026-03-20
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 = 'is_hidden';
-- 预期1 行boolean, false, NO
-- 11. 确认已有数据的 is_hidden 分布2026-03-20
SELECT is_hidden, COUNT(*) FROM member_retention_clue GROUP BY is_hidden;
-- 预期:全部为 falseDEFAULT false 保证兼容)
-- 12. 确认列结构完整11 列,含 is_hidden
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'member_retention_clue';
-- 预期11
```
## 关联文件
@@ -150,9 +170,11 @@ SELECT source, COUNT(*) FROM member_retention_clue GROUP BY source;
- 迁移脚本(建表):`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`
- 迁移脚本is_hidden 列):`db/zqyy_app/migrations/2026-03-xx__ns4_member_clue_is_hidden.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/routers/tenant_clues.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`

View File

@@ -0,0 +1,53 @@
# BD 手册auth.users.avatar_url 字段
## 概述
`auth.users` 表新增 `avatar_url` 字段,存储用户头像的相对路径。
## 字段定义
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `avatar_url` | `VARCHAR(500)` | `NULL` | 头像相对路径,格式 `avatars/{user_id}.jpg` |
## 数据流
1. 小程序端通过 `<button open-type="chooseAvatar">` 获取微信头像临时路径
2. 通过 `wx.uploadFile` 上传到 `POST /api/xcx/avatar/upload`
3. 后端保存文件到 `AVATAR_EXPORT_PATH/{user_id}.jpg`(覆盖式,幂等)
4. 数据库 `avatar_url` 更新为 `avatars/{user_id}.jpg`(相对路径)
5. 小程序通过 `GET /api/xcx/avatar/{user_id}` 获取头像文件
## 关联接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/xcx/avatar/upload` | POST | 上传头像,更新 avatar_url |
| `/api/xcx/avatar/{user_id}` | GET | 获取头像文件FileResponse |
| `/api/xcx/me` | GET | 返回 avatar_url 字段 |
## 设计决策
- 审核表 `auth.user_applications` 不冗余 `avatar_url`,通过 JOIN `auth.users` 获取(头像可能更新)
- 文件命名按 `user_id`,覆盖式保存(幂等,无历史版本)
- 文件大小限制 2MB空文件拒绝
## 迁移脚本
`db/zqyy_app/migrations/20260324_add_avatar_url_to_users.sql`
## 环境变量
`AVATAR_EXPORT_PATH` — 头像文件存储目录,缺失时后端报 500 错误
## 回滚
```sql
ALTER TABLE auth.users DROP COLUMN IF EXISTS avatar_url;
```
## 验证
```sql
SELECT id, avatar_url FROM auth.users WHERE avatar_url IS NOT NULL LIMIT 5;
```

View File

@@ -0,0 +1,254 @@
# BD 手册biz 注册体系表NS4.1 registry
## 概述
NS4.1 注册体系新增 4 张表,全部位于 `biz` Schema建立「连接器 → 租户 → 店铺」三级注册体系。合并原 `auth.site_code_mapping`,统一管理上游 SaaS 系统、租户和店铺的关系并为简写ID 提供归属和变更历史。
所有表位于 `zqyy_app` / `test_zqyy_app` 数据库。
## 变更原因
- NS4.1 注册体系设计,建立项目级「连接器 → 租户 → 店铺」三级结构
- 合并原 `auth.site_code_mapping``biz.sites`增加租户关联和简写ID 变更历史管理
- 数据来源:种子数据从 `auth.site_code_mapping` 迁移ETL 增量同步从 `dwd.dim_site`
## 变更说明
| 库 | Schema | 表 | 变更类型 | 说明 |
|----|--------|---|---------|------|
| zqyy_app | biz | connectors | 新建 | 连接器注册表 |
| zqyy_app | biz | tenants | 新建 | 租户注册表 |
| zqyy_app | biz | sites | 新建 | 店铺注册表(合并原 auth.site_code_mapping |
| zqyy_app | biz | site_code_history | 新建 | 简写ID 变更历史表 |
| zqyy_app | auth | site_code_mapping | 废弃重命名 | → `auth._archived_site_code_mapping` |
---
## 1. biz.connectors — 连接器注册表
记录本项目接入的上游 SaaS 系统。当前仅有飞球feiqiu一个连接器预留多连接器扩展能力。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | SERIAL | PRIMARY KEY | 自增主键 |
| connector_key | VARCHAR(50) | UNIQUE NOT NULL | 连接器标识(如 `'feiqiu'` |
| display_name | VARCHAR(100) | NOT NULL | 显示名称 |
| is_active | BOOLEAN | NOT NULL DEFAULT true | 是否启用 |
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| connectors_pkey | PRIMARY KEY | id | 主键 |
| connectors_connector_key_key | UNIQUE | connector_key | 连接器标识唯一 |
### 种子数据
```sql
INSERT INTO biz.connectors (connector_key, display_name) VALUES ('feiqiu', '飞球');
```
---
## 2. biz.tenants — 租户注册表
连接器下的租户,`tenant_id` 来自上游系统。同一连接器下 `tenant_id` 唯一。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | SERIAL | PRIMARY KEY | 自增主键 |
| connector_id | INTEGER | NOT NULL FK → biz.connectors(id) | 所属连接器 |
| tenant_id | BIGINT | NOT NULL | 上游系统租户 ID |
| tenant_name | VARCHAR(200) | — | 租户名称 |
| is_active | BOOLEAN | NOT NULL DEFAULT true | 是否启用 |
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 更新时间 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| tenants_pkey | PRIMARY KEY | id | 主键 |
| tenants_connector_id_tenant_id_key | UNIQUE | (connector_id, tenant_id) | 同一连接器下租户唯一 |
| tenants_connector_id_fkey | FOREIGN KEY | connector_id → biz.connectors(id) | 外键关联连接器 |
### 种子数据
```sql
INSERT INTO biz.tenants (connector_id, tenant_id, tenant_name)
VALUES (1, 2790683160709957, '朗朗桌球');
```
---
## 3. biz.sites — 店铺注册表
合并原 `auth.site_code_mapping`增加租户关联和简写ID 管理。`site_id` 来自上游系统,全局唯一。`site_code` 为当前生效的简写ID6 位字符3+3 格式),全局唯一。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | SERIAL | PRIMARY KEY | 自增主键 |
| tenant_id | INTEGER | NOT NULL FK → biz.tenants(id) | 所属租户 |
| site_id | BIGINT | NOT NULL UNIQUE | 上游系统门店 ID |
| site_name | VARCHAR(200) | — | 门店名称 |
| site_code | VARCHAR(6) | UNIQUE | 当前生效的简写ID3+3 格式,如 `LLQ001` |
| site_label | VARCHAR(50) | — | 门店标签 |
| is_active | BOOLEAN | NOT NULL DEFAULT true | 是否启用 |
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 更新时间 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| sites_pkey | PRIMARY KEY | id | 主键 |
| sites_site_id_key | UNIQUE | site_id | 上游门店 ID 唯一 |
| sites_site_code_key | UNIQUE | site_code | 简写ID 全局唯一 |
| sites_tenant_id_fkey | FOREIGN KEY | tenant_id → biz.tenants(id) | 外键关联租户 |
### 数据迁移
```sql
-- 从 auth.site_code_mapping 迁移真实数据
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_code)
SELECT t.id, scm.site_id, scm.site_name, scm.site_code
FROM auth.site_code_mapping scm
JOIN biz.tenants t ON t.tenant_id = scm.tenant_id
WHERE scm.tenant_id IS NOT NULL;
-- ETL 增量同步补充:通过 FDW 读取 dwd.dim_sitescd2_is_current=1
-- 补充 auth.site_code_mapping 中没有但 dwd.dim_site 中有的店铺
```
---
## 4. biz.site_code_history — 简写ID 变更历史表
增量记录所有使用过的简写ID。`site_code` 全局唯一(含历史),确保已退役的 code 不会被重新分配。每个 `site_id` 最多一条 `is_current=true` 记录。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | SERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 关联 biz.sites.site_id |
| site_code | VARCHAR(6) | NOT NULL UNIQUE | 简写ID全局唯一含历史 |
| is_current | BOOLEAN | NOT NULL DEFAULT false | true=当前生效,每个 site_id 最多一条 |
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| retired_at | TIMESTAMPTZ | — | 退役时间is_current=false 时设置) |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| site_code_history_pkey | PRIMARY KEY | id | 主键 |
| site_code_history_site_code_key | UNIQUE | site_code | 简写ID 全局唯一(含历史) |
### 初始数据
```sql
-- 为已有 site_code 的店铺创建历史记录
INSERT INTO biz.site_code_history (site_id, site_code, is_current)
SELECT site_id, site_code, true
FROM biz.sites
WHERE site_code IS NOT NULL;
```
---
## 兼容性
| 组件 | 影响 |
|------|------|
| 后端 API | 全部切换到 `biz.sites` + `biz.site_code_history`。新增 `admin_registry` 路由模块租户列表、店铺列表、简写ID 管理)。`admin_tenant_admins` 路由中 `tenant_id``biz.tenants` 选择。`tenant_users` 路由中 site_code 查询从 `auth.site_code_mapping` 切换到 `biz.sites` |
| ETL | 无直接影响。店铺同步通过 FDW 只读访问 ETL 库 `dwd.dim_site`,写入 `biz.sites`。ETL 流程本身不变 |
| 小程序 | 无需改动。用户申请时的 site_code 验证由后端 API 透明切换到 `biz.sites` + `biz.site_code_history` |
| 管理后台admin-web | 新增注册体系 API 调用(`src/api/registry.ts`),租户管理员创建流程从 `biz.tenants`/`biz.sites` 选择 |
| 原 auth.site_code_mapping | 迁移完成后重命名为 `auth._archived_site_code_mapping`,保留供回滚 |
## 回滚策略
### 完整回滚(逆序 DROP + 恢复原表)
```sql
BEGIN;
-- 1. 逆序删除注册体系表
DROP TABLE IF EXISTS biz.site_code_history CASCADE;
DROP TABLE IF EXISTS biz.sites CASCADE;
DROP TABLE IF EXISTS biz.tenants CASCADE;
DROP TABLE IF EXISTS biz.connectors CASCADE;
-- 2. 恢复原表(如已重命名)
ALTER TABLE IF EXISTS auth._archived_site_code_mapping
RENAME TO site_code_mapping;
COMMENT ON TABLE auth.site_code_mapping IS '店铺简写ID 映射表(已恢复)';
COMMIT;
```
注意:
- 回滚前需确认后端代码已切换回 `auth.site_code_mapping` 查询
- `CASCADE` 会级联删除依赖对象
- 如果 `biz.sites` 中已有新增店铺ETL 同步补充的),回滚后这些数据将丢失
## 验证 SQL
```sql
-- 1. 验证 biz.sites 中已有 site_code 的店铺数量
SELECT COUNT(*) FROM biz.sites WHERE site_code IS NOT NULL;
-- 预期:与原 auth.site_code_mapping 中有 site_code 的行数一致
-- 2. 验证 sites 与 site_code_history 的一致性
SELECT s.site_id, s.site_code, h.is_current
FROM biz.sites s
LEFT JOIN biz.site_code_history h
ON h.site_id = s.site_id AND h.site_code = s.site_code
WHERE s.site_code IS NOT NULL;
-- 预期:所有行的 h.is_current = true每个有 code 的店铺在历史表中有对应的当前记录)
-- 3. 验证三级注册体系关联完整性
SELECT c.connector_key, t.tenant_name, COUNT(s.id) AS site_count
FROM biz.connectors c
JOIN biz.tenants t ON t.connector_id = c.id
LEFT JOIN biz.sites s ON s.tenant_id = t.id
GROUP BY c.connector_key, t.tenant_name;
-- 预期:至少 1 行feiqiu / 朗朗桌球 / Nsite_count > 0
-- 4. 验证 4 张注册体系表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'biz'
AND table_name IN ('connectors', 'tenants', 'sites', 'site_code_history')
ORDER BY table_name;
-- 预期:返回 4 行
-- 5. 验证 site_code 全局唯一性sites + history 无冲突)
SELECT site_code, COUNT(*) AS cnt
FROM biz.site_code_history
GROUP BY site_code
HAVING COUNT(*) > 1;
-- 预期:返回 0 行(每个 site_code 在历史表中最多出现一次)
-- 6. 验证种子数据
SELECT connector_key, display_name FROM biz.connectors WHERE connector_key = 'feiqiu';
-- 预期1 行feiqiu / 飞球)
SELECT tenant_id, tenant_name FROM biz.tenants WHERE tenant_id = 2790683160709957;
-- 预期1 行2790683160709957 / 朗朗桌球)
```
## 关联文件
- DDL 基线biz`docs/database/ddl/zqyy_app__biz.sql`
- 迁移脚本:`db/zqyy_app/migrations/2026-03-22__ns41_registry_tables.sql`
- 后端路由:`apps/backend/app/routers/admin_registry.py`
- 后端 Schema`apps/backend/app/schemas/admin_registry.py`
- 管理员路由:`apps/backend/app/routers/admin_tenant_admins.py`
- 前端 API`apps/admin-web/src/api/registry.ts`
- Spec`.kiro/specs/admin-web-enhancement/`
- PRD`docs/prd/Neo_Specs/NS4.1-tenant-admin-redesign.md`

View File

@@ -0,0 +1,76 @@
# BD 手册biz.dws_assistant_task_monthly
> 助教任务月度统计汇总表
## 基本信息
| 属性 | 值 |
|------|-----|
| Schema | biz |
| 表名 | dws_assistant_task_monthly |
| 所属库 | zqyy_app |
| 创建日期 | 2026-03-31 |
| 写入方 | `task_generator._update_task_stats()` |
| 读取方 | 看板 / 绩效报表(待接入) |
| 幂等策略 | UPSERT`ON CONFLICT (site_id, assistant_id, stat_month) DO UPDATE` |
## 用途
按助教 + 自然月维度汇总任务引擎的创建/完成/放弃/转移数据。每次 `task_generator.run()` 执行时,对当前门店的所有助教重新计算当月统计并 upsert。
## 字段定义
| 字段名 | 类型 | 可空 | 默认值 | 说明 |
|--------|------|------|--------|------|
| id | BIGSERIAL | NOT NULL | 自增 | 主键 |
| site_id | BIGINT | NOT NULL | — | 门店 ID |
| assistant_id | BIGINT | NOT NULL | — | 助教 ID |
| stat_month | DATE | NOT NULL | — | 统计月份(月初日期,如 2026-03-01 |
| recall_created | INT | NOT NULL | 0 | 当月创建的召回任务数high_priority + priority |
| follow_up_created | INT | NOT NULL | 0 | 当月创建的回访任务数 |
| relationship_created | INT | NOT NULL | 0 | 当月创建的关系构建任务数 |
| total_created | INT | NOT NULL | 0 | 当月创建的任务总数 |
| recall_completed | INT | NOT NULL | 0 | 当月完成的召回任务数 |
| follow_up_completed | INT | NOT NULL | 0 | 当月完成的回访任务数 |
| total_completed | INT | NOT NULL | 0 | 当月完成的任务总数 |
| abandoned_count | INT | NOT NULL | 0 | 当月放弃的任务数 |
| transferred_count | INT | NOT NULL | 0 | 当月转移的任务数 |
| updated_at | TIMESTAMPTZ | NOT NULL | NOW() | 最后更新时间 |
## 约束与索引
| 名称 | 类型 | 字段 |
|------|------|------|
| PK | PRIMARY KEY | id |
| UQ | UNIQUE | (site_id, assistant_id, stat_month) |
| idx_task_monthly_site_month | INDEX | (site_id, stat_month DESC) |
| idx_task_monthly_assistant | INDEX | (assistant_id, stat_month DESC) |
## 数据来源
统计数据从 `biz.coach_tasks` 表聚合:
-`assigned_assistant_id` + `DATE_TRUNC('month', created_at)` 分组
- `recall_created` = COUNT WHERE task_type IN ('high_priority_recall', 'priority_recall')
- `follow_up_created` = COUNT WHERE task_type = 'follow_up_visit'
- `relationship_created` = COUNT WHERE task_type = 'relationship_building'
- `*_completed` = COUNT WHERE status = 'completed' AND 对应 task_type
- `abandoned_count` = COUNT WHERE status = 'abandoned'
- `transferred_count` = COUNT WHERE status = 'transferred'
## 兼容性
- ETL无直接依赖表由后端 task_generator 写入)
- 后端 API待接入看板/绩效接口
- 小程序:无直接依赖
## 回滚策略
```sql
DROP TABLE IF EXISTS biz.dws_assistant_task_monthly;
```
## 关联变更
- 迁移脚本:`db/zqyy_app/migrations/2026-03-31__task_stats_tables.sql`
- 审计记录:`docs/audit/changes/2026-03-31__task-engine-overhaul.md`
- 关联表:`biz.coach_tasks`(数据源)、`dws.dws_member_assistant_relation_index`C 层历史总计字段)

View File

@@ -0,0 +1,83 @@
# BD_ManualFDW 财务区域查询映射fdw_finance_area
> 目标库:后端通过 `get_etl_readonly_connection(site_id)` 直连 ETL 库
> 关联 SPECboard-finance-dws-area-refactor
> 日期2026-03-28
---
## 1. 变更说明
### 新增 RLS 视图ETL 库 app Schema 自动导出)
本次新增的两张 DWS 表各有一个 RLS 视图,后端通过直连 ETL 库的 `app.v_*` 视图访问(与 RNS1.2 以来的模式一致,不使用 `fdw_etl.*` 外部表)。
| 视图 | 源表 | 用途 |
|------|------|------|
| `dws.v_dws_finance_area_daily` | `dws.dws_finance_area_daily` | 区域日粒度财务数据overview/revenue 板块) |
| `dws.v_dws_finance_board_cache` | `dws.dws_finance_board_cache` | 已完成周期缓存overview 8 项指标) |
### 后端查询函数
| 函数 | 视图 | 用途 |
|------|------|------|
| `get_finance_overview_area()` | `v_dws_finance_area_daily` | 按 area_code 聚合 overview 8 项指标 |
| `get_finance_revenue_area()` | `v_dws_finance_area_daily` | 按 area_code 聚合 revenue 板块数据 |
| `get_finance_board_cache()` | `v_dws_finance_board_cache` | 查询缓存 |
| `set_finance_board_cache()` | `dws.dws_finance_board_cache` | 写入/更新缓存 |
### RLS 隔离
所有查询通过 `SET LOCAL app.current_site_id = :site_id` 设置门店隔离参数RLS 视图的 `WHERE site_id = current_setting('app.current_site_id')::bigint` 自动过滤。
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。FDW 配置不修改 ETL 库 |
| 后端 API | `fdw_queries.py` 新增 4 个函数,`board_service.py` 改为调用新函数 |
| 小程序 | 无直接影响API 签名不变) |
| 现有 FDW 外部表 | 无影响。新视图通过直连 ETL 库访问,不经过 `fdw_etl` Schema |
---
## 3. 回滚策略
后端回滚:
1. 恢复 `board_service.py` 到旧逻辑(从 `dws_finance_daily_summary` 取数)
2. 移除 `fdw_queries.py` 中的 4 个新函数
数据库回滚:
```sql
DROP VIEW IF EXISTS dws.v_dws_finance_board_cache;
DROP TABLE IF EXISTS dws.dws_finance_board_cache;
DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;
DROP TABLE IF EXISTS dws.dws_finance_area_daily;
```
---
## 4. 验证 SQL
```sql
-- 1. 验证 RLS 视图可访问(需先设置 site_id
SET LOCAL app.current_site_id = '1';
SELECT COUNT(*) FROM dws.v_dws_finance_area_daily;
-- 2. 验证缓存视图可访问
SET LOCAL app.current_site_id = '1';
SELECT COUNT(*) FROM dws.v_dws_finance_board_cache;
-- 3. 验证 app_reader 角色有 SELECT 权限
SELECT has_table_privilege('app_reader', 'dws.v_dws_finance_area_daily', 'SELECT') AS daily_ok,
has_table_privilege('app_reader', 'dws.v_dws_finance_board_cache', 'SELECT') AS cache_ok;
-- 4. 验证区域日粒度数据完整性(每天 9 行)
SET LOCAL app.current_site_id = '1';
SELECT stat_date, COUNT(*) AS cnt
FROM dws.v_dws_finance_area_daily
GROUP BY stat_date
HAVING COUNT(*) != 9;
```

View File

@@ -0,0 +1,74 @@
# BD 手册idx_coach_tasks_rb_unique_active
| 字段 | 值 |
|------|-----|
| 数据库 | zqyy_app |
| Schema | biz |
| 表 | coach_tasks |
| 索引名 | idx_coach_tasks_rb_unique_active |
| 类型 | UNIQUE (partial) |
| 创建日期 | 2026-03-25 |
| 迁移脚本 | `db/zqyy_app/migrations/2026-03-25__relationship_building_baseline.sql` |
## 索引定义
```sql
CREATE UNIQUE INDEX idx_coach_tasks_rb_unique_active
ON biz.coach_tasks (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active';
```
## 用途
保证每个 `(site_id, assistant_id, member_id)` 组合最多存在 1 条 `status = 'active'``relationship_building` 任务。
支持 `_generate_baseline_relationship_tasks()` 中的 upsert 操作:
```sql
INSERT INTO biz.coach_tasks (...)
VALUES (...)
ON CONFLICT (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active'
DO NOTHING
```
## 业务背景
保底 relationship_building 任务:对每个助教,所有确切发生过服务关系(`session_count > 0`)的客户都生成一条 relationship_building 任务。partial unique index 确保幂等性——重复运行不会产生重复任务。
## 影响范围
- 写入方:`task_generator._generate_baseline_relationship_tasks()` — upsert 依赖此索引
- 读取方:`task_manager.get_task_list_v2()` — 查询不直接使用此索引,但受益于去重保证
- 不影响其他 task_type 的任务partial index 仅覆盖 `relationship_building` + `active`
## 回滚
```sql
DROP INDEX IF EXISTS biz.idx_coach_tasks_rb_unique_active;
```
回滚后 `_generate_baseline_relationship_tasks()``ON CONFLICT` 子句会报错(无匹配索引),需同步回滚代码。
## 验证 SQL
```sql
-- 1. 确认索引存在
SELECT indexname, indexdef FROM pg_indexes
WHERE indexname = 'idx_coach_tasks_rb_unique_active';
-- 2. 确认无重复
SELECT site_id, assistant_id, member_id, COUNT(*)
FROM biz.coach_tasks
WHERE task_type = 'relationship_building' AND status = 'active'
GROUP BY site_id, assistant_id, member_id
HAVING COUNT(*) > 1;
-- 应返回 0 行
-- 3. 测试 upsert 幂等性dry run
EXPLAIN INSERT INTO biz.coach_tasks
(site_id, assistant_id, member_id, task_type, status, priority_score)
VALUES (1, 1, 1, 'relationship_building', 'active', 0)
ON CONFLICT (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active'
DO NOTHING;
```

View File

@@ -0,0 +1,230 @@
# BD 手册public Schema — RBAC 与工作流8 表)
> 目标库:`zqyy_app`(通过 `APP_DB_DSN` 连接)
> DDL 位置:`docs/database/ddl/zqyy_app__public.sql`
> 关联文档:`BD_manual_scheduled_tasks.md`(调度相关 3 表)、`BD_manual_member_retention_clue.md`(维客线索表)
---
## 1. 概述
`public` Schema 共 12 张表,本文档覆盖其中 RBAC角色权限+ 工作流(任务审批)共 8 张表。其余 4 张表已有独立文档:
| 表 | 文档 |
|----|------|
| `scheduled_tasks``task_queue``task_execution_log` | `BD_manual_scheduled_tasks.md` |
| `member_retention_clue` | `BD_manual_member_retention_clue.md` |
### 表关系
```
admin_users管理后台登录账户roles 数组字段)
users ──┬── user_roles ── roles ── role_permissions ── permissions
│ (用户-角色映射) (角色) (角色-权限映射) (权限)
├── tasks任务指派creator_id / assignee_id → users
│ │
└── approvals审批记录approver_id → userstask_id → tasks
```
---
## 2. 表结构
### public.admin_users9 字段)
管理后台admin-web / tenant-admin登录账户。通过 JWT `aud=admin``aud=tenant-admin` 认证。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | SERIAL | PK | 自增主键 |
| `username` | VARCHAR(64) | NOT NULL, UNIQUE | 登录用户名 |
| `password_hash` | VARCHAR(256) | NOT NULL | bcrypt 密码哈希 |
| `display_name` | VARCHAR(128) | 可空 | 显示名称 |
| `site_id` | BIGINT | NOT NULL | 所属门店 ID |
| `is_active` | BOOLEAN | DEFAULT true | 是否启用 |
| `roles` | TEXT[] | NOT NULL, DEFAULT '{site_admin}' | 角色数组(`site_admin` / `tenant_admin` 等) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
> 注意:`admin_users.roles` 是 TEXT 数组字段(快捷权限标记),与 `roles` 表的 RBAC 体系并行使用。admin-web 当前主要依赖此数组字段做权限判断。
### public.users8 字段)
小程序端用户C 端会员),通过微信 code → JWT `aud=miniapp` 认证。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `wx_openid` | TEXT | UNIQUE可空 | 微信 OpenID登录唯一标识 |
| `mobile` | TEXT | 可空 | 手机号 |
| `nickname` | TEXT | 可空 | 用户昵称 |
| `status` | INTEGER | DEFAULT 1 | 状态1=正常) |
| `site_id` | BIGINT | NOT NULL | 所属门店 ID |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
### public.roles5 字段)
角色定义表,按门店隔离。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | SERIAL | PK | 自增主键 |
| `name` | TEXT | NOT NULL, UNIQUE | 角色名称(全局唯一) |
| `description` | TEXT | 可空 | 角色描述 |
| `site_id` | BIGINT | NOT NULL | 所属门店 ID |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### public.permissions4 字段)
权限定义表(资源+动作组合)。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | SERIAL | PK | 自增主键 |
| `resource` | TEXT | NOT NULL | 资源标识(如 `etl``users` |
| `action` | TEXT | NOT NULL | 操作标识(如 `read``write``execute` |
| `description` | TEXT | 可空 | 权限描述 |
### public.role_permissions2 字段)
角色-权限关联表(多对多)。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `role_id` | INTEGER | PK, FK → roles(id) ON DELETE CASCADE | 角色 ID |
| `permission_id` | INTEGER | PK, FK → permissions(id) ON DELETE CASCADE | 权限 ID |
### public.user_roles3 字段)
用户-角色关联表(多对多),按门店隔离。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `user_id` | BIGINT | PK, FK → users(id) ON DELETE CASCADE | 用户 ID |
| `role_id` | INTEGER | PK, FK → roles(id) ON DELETE CASCADE | 角色 ID |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
### public.tasks9 字段)
任务指派表,用于门店内任务管理。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `title` | TEXT | NOT NULL | 任务标题 |
| `description` | TEXT | 可空 | 任务描述 |
| `status` | TEXT | DEFAULT 'pending' | 状态:`pending` / `in_progress` / `completed` |
| `assignee_id` | BIGINT | FK → users(id),可空 | 负责人 ID |
| `creator_id` | BIGINT | FK → users(id),可空 | 创建人 ID |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
### public.approvals7 字段)
审批记录表,关联到任务。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `task_id` | BIGINT | FK → tasks(id) ON DELETE CASCADE可空 | 关联任务 ID |
| `approver_id` | BIGINT | FK → users(id),可空 | 审批人 ID |
| `status` | TEXT | DEFAULT 'pending' | 状态:`pending` / `approved` / `rejected` |
| `comment` | TEXT | 可空 | 审批意见 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
---
## 3. 约束与索引
| 表 | 约束/索引名 | 类型 | 说明 |
|----|-----------|------|------|
| `admin_users` | `admin_users_pkey` | PK | `(id)` |
| `admin_users` | `admin_users_username_key` | UNIQUE | `(username)` |
| `admin_users` | `idx_admin_users_site` | INDEX | `(site_id)` |
| `users` | `users_pkey` | PK | `(id)` |
| `users` | `users_wx_openid_key` | UNIQUE | `(wx_openid)` |
| `users` | `idx_users_site_id` | INDEX | `(site_id)` |
| `users` | `idx_users_mobile` | INDEX | `(mobile)` |
| `roles` | `roles_pkey` | PK | `(id)` |
| `roles` | `roles_name_key` | UNIQUE | `(name)` |
| `roles` | `idx_roles_site_id` | INDEX | `(site_id)` |
| `permissions` | `permissions_pkey` | PK | `(id)` |
| `permissions` | `permissions_resource_action_key` | UNIQUE | `(resource, action)` |
| `role_permissions` | `role_permissions_pkey` | PK | `(role_id, permission_id)` |
| `role_permissions` | FK `role_id` | FK | → roles(id) ON DELETE CASCADE |
| `role_permissions` | FK `permission_id` | FK | → permissions(id) ON DELETE CASCADE |
| `user_roles` | `user_roles_pkey` | PK | `(user_id, role_id)` |
| `user_roles` | FK `user_id` | FK | → users(id) ON DELETE CASCADE |
| `user_roles` | FK `role_id` | FK | → roles(id) ON DELETE CASCADE |
| `user_roles` | `idx_user_roles_site_id` | INDEX | `(site_id)` |
| `tasks` | `tasks_pkey` | PK | `(id)` |
| `tasks` | FK `assignee_id` | FK | → users(id) |
| `tasks` | FK `creator_id` | FK | → users(id) |
| `tasks` | `idx_tasks_site_id` | INDEX | `(site_id)` |
| `tasks` | `idx_tasks_status` | INDEX | `(status)` |
| `tasks` | `idx_tasks_assignee_id` | INDEX | `(assignee_id)` |
| `approvals` | `approvals_pkey` | PK | `(id)` |
| `approvals` | FK `task_id` | FK | → tasks(id) ON DELETE CASCADE |
| `approvals` | FK `approver_id` | FK | → users(id) |
| `approvals` | `idx_approvals_site_id` | INDEX | `(site_id)` |
| `approvals` | `idx_approvals_task_id` | INDEX | `(task_id)` |
---
## 4. 兼容性影响
| 组件 | 影响 |
|------|------|
| admin-web | 直接依赖。登录用 `admin_users`,权限判断用 `admin_users.roles` 数组 |
| tenant-admin | 直接依赖。登录也用 `admin_users``aud=tenant-admin`),按 `site_id` 隔离 |
| 后端 API | `admin_users` 用于 JWT 签发和验证RBAC 表roles/permissions/user_roles用于细粒度权限控制 |
| 小程序 | 通过 `users` 表完成微信登录注册和用户信息管理 |
| ETL | 无直接影响 |
---
## 5. 验证 SQL
```sql
-- 1. 验证 8 张表存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('admin_users', 'users', 'roles', 'permissions',
'role_permissions', 'user_roles', 'tasks', 'approvals')
ORDER BY table_name;
-- 预期8 行
-- 2. 验证 admin_users 字段数量
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'admin_users';
-- 预期9
-- 3. 验证 admin_users.roles 默认值
SELECT column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'admin_users' AND column_name = 'roles';
-- 预期:'{site_admin}'::text[]
-- 4. 验证外键关系
SELECT tc.table_name, tc.constraint_name, ccu.table_name AS references_table
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
WHERE tc.table_schema = 'public'
AND tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name IN ('approvals', 'tasks', 'role_permissions', 'user_roles')
ORDER BY tc.table_name;
-- 预期7 行外键
-- 5. 验证 permissions 唯一约束
SELECT constraint_name
FROM information_schema.table_constraints
WHERE table_schema = 'public' AND table_name = 'permissions' AND constraint_type = 'UNIQUE';
-- 预期permissions_resource_action_key
```

View File

@@ -0,0 +1,134 @@
# BD 手册scheduled_tasks 新增字段P16 最小运行间隔)
## 概述
P16 为 `public.scheduled_tasks` 表新增 3 个字段,支持调度任务最小运行间隔机制。调度器轮询时检查并发状态和间隔约束,避免任务重复执行。
所有变更位于 `zqyy_app` / `test_zqyy_app` 数据库,`public` Schema。
## 变更原因
- P16 调度任务最小运行间隔需求:管理员可为每个任务配置最小间隔,任务即使调度到期也不会在间隔内重复执行
- 新增 `last_success_at` 字段区分"最后执行时间"和"最后成功时间",支持更精确的间隔计算
## 变更说明
| 库 | Schema | 表 | 变更类型 | 说明 |
|----|--------|---|---------|------|
| zqyy_app | public | scheduled_tasks | 新增字段 ×4 | min_run_interval_value, min_run_interval_unit, last_success_at, min_run_intervals |
---
## 新增字段明细
| 列名 | 类型 | 约束 | 默认值 | 说明 |
|------|------|------|--------|------|
| min_run_interval_value | INTEGER | NOT NULL | 0 | 最小间隔数值0=无限制,与现有行为完全一致) |
| min_run_interval_unit | VARCHAR(20) | NOT NULL | 'minutes' | 间隔单位:`minutes` / `hours` / `days` |
| last_success_at | TIMESTAMPTZ | — | NULL | 最后一次成功执行的时间 |
| min_run_intervals | JSONB | NOT NULL | '{}' | 每任务子代码级别的最小运行间隔配置key=task_code, value=interval_seconds |
### DDL
```sql
ALTER TABLE scheduled_tasks ADD COLUMN min_run_interval_value INTEGER NOT NULL DEFAULT 0;
ALTER TABLE scheduled_tasks ADD COLUMN min_run_interval_unit VARCHAR(20) NOT NULL DEFAULT 'minutes';
ALTER TABLE scheduled_tasks ADD COLUMN last_success_at TIMESTAMPTZ;
COMMENT ON COLUMN scheduled_tasks.min_run_interval_value IS '最小间隔数值0=无限制)';
COMMENT ON COLUMN scheduled_tasks.min_run_interval_unit IS '间隔单位minutes/hours/days';
COMMENT ON COLUMN scheduled_tasks.last_success_at IS '最后一次成功执行的时间';
```
---
## 业务逻辑
调度器(`scheduler.py`)轮询时对每个到期任务执行以下检查:
1. **并发检查**`last_status = 'running'` → 跳过本次入队,日志记录 `skipped_concurrent`
2. **间隔检查**`min_run_interval_value > 0``now() - last_run_at < min_interval_seconds` → 跳过本次执行,推进 `next_run_at`,日志记录 `skipped_interval`
3. **首次执行**`last_run_at IS NULL`(从未执行)→ 跳过间隔检查,正常执行
4. **强制执行**`force=true` 参数绕过所有检查(并发 + 间隔),直接入队
### 间隔转换
```python
def _convert_interval_to_seconds(value: int, unit: str) -> int:
multipliers = {"minutes": 60, "hours": 3600, "days": 86400}
return value * multipliers.get(unit, 60)
```
### last_success_at 更新规则
- 任务成功完成时:`last_status='completed'`, `last_success_at=NOW()`
- 任务失败时:`last_status='failed'``last_success_at` 不变
---
## 兼容性
| 组件 | 影响 |
|------|------|
| 后端 API | `POST /api/schedules``PUT /api/schedules/{id}` 请求体新增 `min_run_interval_value``min_run_interval_unit``GET /api/schedules` 响应新增 3 个字段。`POST /api/schedules/{id}/run` 新增 `force` 查询参数 |
| 前端 ScheduleTab | 创建/编辑表单新增「最小运行间隔」配置行。列表新增「最小间隔」和「上次成功」列。手动执行确认框新增「强制执行」Checkbox |
| ETL | 无影响。ETL 任务调度由 `scheduled_tasks` 表驱动,新增字段默认值 0 表示无限制,向后兼容 |
| 小程序 | 无影响 |
| 调度器 | `scheduler.py``check_and_enqueue()` 新增并发检查和间隔检查逻辑 |
## 回滚策略
```sql
BEGIN;
ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS min_run_interval_value;
ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS min_run_interval_unit;
ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_success_at;
COMMIT;
```
注意:
- 回滚后所有任务恢复为无间隔限制的行为
- 回滚前需确认后端代码已移除对这 3 个字段的引用
- `last_success_at` 数据丢失不可恢复
## 验证 SQL
```sql
-- 1. 验证新增字段存在且类型正确
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_name = 'scheduled_tasks'
AND column_name IN ('min_run_interval_value', 'min_run_interval_unit', 'last_success_at');
-- 预期3 行
-- min_run_interval_value | integer | 0
-- min_run_interval_unit | character varying | 'minutes'::character varying
-- last_success_at | timestamp with time zone | NULL
-- 2. 验证现有任务的新字段默认值
SELECT id, name, min_run_interval_value, min_run_interval_unit, last_success_at
FROM scheduled_tasks
LIMIT 5;
-- 预期min_run_interval_value=0, min_run_interval_unit='minutes', last_success_at=NULL除非已手动配置
-- 3. 验证已配置间隔的任务数量
SELECT COUNT(*) FROM scheduled_tasks WHERE min_run_interval_value > 0;
-- 预期:≥ 0初始状态为 0配置后递增
-- 4. 验证字段注释
SELECT col_description(
(SELECT oid FROM pg_class WHERE relname = 'scheduled_tasks'),
(SELECT attnum FROM pg_attribute WHERE attrelid = 'scheduled_tasks'::regclass AND attname = 'min_run_interval_value')
);
-- 预期:'最小间隔数值0=无限制)'
```
## 关联文件
- DDL 基线public`docs/database/ddl/zqyy_app__public.sql`
- 迁移脚本:`db/zqyy_app/migrations/2026-03-22__p16_min_run_interval.sql`
- 调度器逻辑:`apps/backend/app/services/scheduler.py`
- 后端路由:`apps/backend/app/routers/schedules.py`
- 后端 Schema`apps/backend/app/schemas/schedules.py`
- 前端组件:`apps/admin-web/src/components/ScheduleTab.tsx`
- Spec`.kiro/specs/admin-web-enhancement/`
- PRD`docs/prd/specs/P16-task-min-run-interval.md`

View File

@@ -0,0 +1,105 @@
# BD 手册user_site_roles / user_assistant_binding 软删除
## 变更日期
2026-03-24
## 变更说明
`auth.user_site_roles``auth.user_assistant_binding` 两张表上新增软删除字段,替代原有的物理删除(`DELETE`)。
### 新增字段
| 表 | 字段 | 类型 | 默认值 | 说明 |
|----|------|------|--------|------|
| `auth.user_site_roles` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_site_roles` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |
| `auth.user_assistant_binding` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_assistant_binding` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |
### 新增索引
| 索引名 | 表 | 类型 | 说明 |
|--------|-----|------|------|
| `ix_user_site_roles_active` | `user_site_roles` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |
| `ix_user_assistant_binding_active` | `user_assistant_binding` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |
## 兼容性影响
### 后端 API已同步修改
所有查询 `user_site_roles``user_assistant_binding` 的位置均已添加 `AND is_removed = false` 过滤:
- `xcx_auth.py`登录、me 接口、切换门店、刷新令牌、获取门店列表、dev 调试接口
- `tenant_users.py`:用户列表、编辑用户、更新绑定、移除用户
- `role.py`:权限查询、门店列表、角色检查
- `task_manager.py`:获取助教 ID
- `task_generator.py`:门店助教规模检查、入驻时间保护
- `performance_service.py`:助教信息查询
### ETL
无直接影响。ETL 不写入这两张表。
### 小程序
无代码改动。被移除的用户在小程序端会因角色查询返回空而进入已有的无权限路由。
### 租户管理后台
`remove_user` 操作从 `DELETE` 改为 `UPDATE SET is_removed = true, removed_at = now()`
## 查询规则(强制)
所有读取 `user_site_roles``user_assistant_binding` 的 SELECT 查询,必须包含 `AND is_removed = false`(或等效的 JOIN 条件 `AND xxx.is_removed = false`)。
例外:
- 管理后台需要查看已移除记录的场景(如审计日志)
- dev 调试接口中的物理删除操作(仅开发模式)
## 回滚策略
```sql
-- 1. 恢复所有被软删除的记录
UPDATE auth.user_site_roles SET is_removed = false, removed_at = NULL WHERE is_removed = true;
UPDATE auth.user_assistant_binding SET is_removed = false, removed_at = NULL WHERE is_removed = true;
-- 2. 删除索引
DROP INDEX IF EXISTS auth.ix_user_site_roles_active;
DROP INDEX IF EXISTS auth.ix_user_assistant_binding_active;
-- 3. 删除字段
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS is_removed;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS is_removed;
```
注意:回滚后需同步还原后端代码中所有 `AND is_removed = false` 过滤条件,并将 `remove_user` 恢复为 `DELETE`
## 验证 SQL
```sql
-- 1. 确认字段存在且默认值正确
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = 'auth'
AND table_name IN ('user_site_roles', 'user_assistant_binding')
AND column_name IN ('is_removed', 'removed_at')
ORDER BY table_name, column_name;
-- 2. 确认部分索引存在
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'auth'
AND indexname IN ('ix_user_site_roles_active', 'ix_user_assistant_binding_active');
-- 3. 确认现有数据未被误标记(所有记录 is_removed 应为 false
SELECT 'user_site_roles' AS tbl, COUNT(*) AS total,
COUNT(*) FILTER (WHERE is_removed = true) AS removed
FROM auth.user_site_roles
UNION ALL
SELECT 'user_assistant_binding', COUNT(*),
COUNT(*) FILTER (WHERE is_removed = true)
FROM auth.user_assistant_binding;
-- 4. 确认活跃记录查询走部分索引EXPLAIN 检查)
EXPLAIN SELECT * FROM auth.user_site_roles WHERE user_id = 1 AND site_id = 1 AND is_removed = false;
```
## 迁移脚本
`db/zqyy_app/migrations/20260324_soft_delete_user_site_roles_and_binding.sql`

View File

@@ -0,0 +1,544 @@
# BD 手册租户管理后台表NS4 tenant-admin-web
## 概述
NS4 租户管理后台新增 6 张表,分布在 `auth``biz` 两个 Schema 中。`auth.tenant_admins` 为租户管理员认证表,与小程序 `auth.users`(微信登录)完全隔离;`biz` Schema 下 5 张表支撑 Excel 数据上传功能上传日志、助教奖罚、3 张 staging 暂存表)。
所有表位于 `zqyy_app` / `test_zqyy_app` 数据库。
## 变更说明
| 库 | Schema | 表 | 变更类型 | 说明 |
|----|--------|---|---------|------|
| zqyy_app | auth | tenant_admins | 新建 | 租户管理员认证表 |
| zqyy_app | biz | excel_upload_log | 新建 | Excel 上传记录表 |
| zqyy_app | biz | salary_adjustments | 新建 | 助教奖罚明细表 |
| zqyy_app | biz | stg_finance_expense | 新建 | 财务支出暂存表 |
| zqyy_app | biz | stg_platform_income | 新建 | 团购收入暂存表 |
| zqyy_app | biz | stg_recharge_commission | 新建 | 充值业绩归属暂存表 |
---
## 1. auth.tenant_admins — 租户管理员表
独立于小程序 `auth.users`,使用用户名+密码登录JWT `aud=tenant-admin` 隔离。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| username | VARCHAR(50) | UNIQUE NOT NULL | 登录用户名 |
| password_hash | VARCHAR(255) | NOT NULL | bcrypt 哈希密码 |
| display_name | VARCHAR(100) | — | 显示名称 |
| tenant_id | BIGINT | NOT NULL | 所属租户 ID |
| managed_site_ids | BIGINT[] | NOT NULL | 管辖门店 ID 列表(数据隔离依据) |
| admin_type | VARCHAR(20) | NOT NULL DEFAULT 'tenant_admin' | 管理员类型tenant_admin租户管理员/ site_admin店铺管理员CHECK 约束 |
| is_active | BOOLEAN | DEFAULT true | 账号状态false=禁用,登录返回 403仅控制启用/禁用,与软删除无关) |
| deleted_at | TIMESTAMPTZ | DEFAULT NULL | 软删除时间戳NULL=正常,非 NULL=已删除(与 is_active 分离) |
| created_by | BIGINT | — | 创建者(管理员 ID |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| last_login_at | TIMESTAMPTZ | — | 最后登录时间 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| tenant_admins_pkey | PRIMARY KEY | id | 主键 |
| tenant_admins_username_key | UNIQUE | username | 用户名唯一 |
| chk_admin_type | CHECK | admin_type | admin_type IN ('tenant_admin', 'site_admin') |
| idx_tenant_admin_tenant | INDEX (btree) | tenant_id | 按租户查询 |
| idx_tenant_admins_active_not_deleted | INDEX (btree, partial) | is_active WHERE deleted_at IS NULL | 加速列表和登录查询(仅索引未删除记录) |
| idx_tenant_admins_username_lower | UNIQUE (partial) | LOWER(username) WHERE deleted_at IS NULL | 大小写不敏感唯一约束2026-03-23 |
---
## 2. biz.excel_upload_log — Excel 上传记录表
记录每次 Excel 上传的批次信息,支撑上传→校验→冲突→确认的完整流程。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| upload_type | VARCHAR(30) | NOT NULL, CHECK | 模板类型(见枚举) |
| file_name | VARCHAR(255) | NOT NULL | 原始文件名 |
| uploaded_by | BIGINT | NOT NULL | 上传人(管理员 ID |
| row_count | INTEGER | DEFAULT 0 | 数据行数 |
| conflict_count | INTEGER | DEFAULT 0 | 冲突行数 |
| resolved_count | INTEGER | DEFAULT 0 | 已解决冲突数 |
| status | VARCHAR(20) | NOT NULL, CHECK | 批次状态(见枚举) |
| error_detail | JSONB | — | 错误详情 / 临时缓存上传数据 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 上传时间 |
| confirmed_at | TIMESTAMPTZ | — | 确认写入时间 |
### upload_type 枚举值
| 值 | 说明 |
|----|------|
| expense | 财务支出 |
| platform_income | 团购收入 |
| salary_adj | 助教奖罚 |
| recharge_commission | 充值业绩归属 |
### status 枚举值
| 值 | 说明 |
|----|------|
| pending | 待确认(已上传校验通过,等待用户确认写入) |
| confirmed | 已确认(数据已写入目标表) |
| failed | 失败(写入过程出错,已回滚) |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| excel_upload_log_pkey | PRIMARY KEY | id | 主键 |
| CHECK (upload_type) | CHECK | upload_type | 限制模板类型枚举 |
| CHECK (status) | CHECK | status | 限制状态枚举 |
| idx_excel_log_site | INDEX (btree) | (site_id, created_at DESC) | 按门店+时间查询 |
---
## 3. biz.salary_adjustments — 助教奖罚明细表
直接写入 biz Schema非 staging记录助教扣款/奖金明细。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| assistant_id | BIGINT | — | 匹配到的助教 ID可空人员匹配失败时为 NULL |
| assistant_name | VARCHAR(100) | NOT NULL | 助教姓名Excel 原始值) |
| assistant_number | VARCHAR(50) | NOT NULL | 助教编号Excel 原始值) |
| salary_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM |
| adjustment_type | VARCHAR(20) | NOT NULL, CHECK | 类型deduction扣款/ bonus奖金 |
| amount | NUMERIC(12,2) | NOT NULL, CHECK (> 0) | 金额(正数) |
| reason | VARCHAR(200) | NOT NULL | 原因说明 |
| upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| created_by | BIGINT | — | 上传人(管理员 ID |
### adjustment_type 枚举值
| 值 | Excel 中文值 | 说明 |
|----|-------------|------|
| deduction | 扣款 | 扣款 |
| bonus | 奖金 | 奖金 |
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| salary_adjustments_pkey | PRIMARY KEY | id | 主键 |
| CHECK (adjustment_type) | CHECK | adjustment_type | 限制类型枚举 |
| CHECK (amount) | CHECK | amount | 金额必须 > 0 |
| salary_adjustments_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 |
| idx_salary_adj_site_month | INDEX (btree) | (site_id, salary_month) | 按门店+月份查询 |
| idx_salary_adj_assistant_month | INDEX (btree) | (assistant_id, salary_month) | 按助教+月份查询 |
---
## 4. biz.stg_finance_expense — 财务支出暂存表
通过 Excel 上传写入,等待 ETL 同步到正式表。
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| expense_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM |
| category | VARCHAR(50) | NOT NULL | 支出类别8 值枚举) |
| amount | NUMERIC(12,2) | NOT NULL | 金额 |
| remark | TEXT | — | 备注 |
| upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间NULL=未同步) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### 冲突检测主键
`(site_id, expense_month, category)` — 同门店同月份同类别视为冲突。
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| stg_finance_expense_pkey | PRIMARY KEY | id | 主键 |
| stg_finance_expense_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 |
---
## 5. biz.stg_platform_income — 团购收入暂存表
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| income_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM |
| platform_name | VARCHAR(100) | NOT NULL | 平台名称 |
| amount | NUMERIC(12,2) | NOT NULL | 收入金额 |
| remark | TEXT | — | 备注 |
| upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间NULL=未同步) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### 冲突检测主键
`(site_id, income_month, platform_name)` — 同门店同月份同平台视为冲突。
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| stg_platform_income_pkey | PRIMARY KEY | id | 主键 |
| stg_platform_income_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 |
---
## 6. biz.stg_recharge_commission — 充值业绩归属暂存表
### 表结构
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| recharge_date | DATE | NOT NULL | 充值日期 |
| member_name | VARCHAR(100) | NOT NULL | 会员名称 |
| recharge_amount | NUMERIC(12,2) | NOT NULL | 充值金额 |
| assigned_assistant | VARCHAR(100) | NOT NULL | 归属助教 |
| reward_amount | NUMERIC(12,2) | NOT NULL | 奖励金额 |
| upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间NULL=未同步) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### 冲突检测主键
`(site_id, recharge_date, member_name, assigned_assistant)` — 同门店同日期同会员同助教视为冲突。
### 约束与索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| stg_recharge_commission_pkey | PRIMARY KEY | id | 主键 |
| stg_recharge_commission_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 |
---
## 兼容性
- **后端 API**5 个新路由模块tenant_auth / tenant_users / tenant_excel / tenant_clues / admin_tenant_admins全部依赖上述表
- **ETL**3 张 staging 表stg_finance_expense / stg_platform_income / stg_recharge_commission`synced_at` 字段供 ETL 同步标记ETL 读取 `synced_at IS NULL` 的行进行同步
- **小程序**:无直接影响(租户管理后台独立认证体系)
- **管理后台admin-web**:新增租户管理员 CRUD 页面,调用 admin_tenant_admins 路由
## 回滚策略
### 完整回滚(按依赖顺序)
```sql
BEGIN;
-- 先删除有外键依赖的表
DROP TABLE IF EXISTS biz.salary_adjustments CASCADE;
DROP TABLE IF EXISTS biz.stg_finance_expense CASCADE;
DROP TABLE IF EXISTS biz.stg_platform_income CASCADE;
DROP TABLE IF EXISTS biz.stg_recharge_commission CASCADE;
-- 再删除被引用的表
DROP TABLE IF EXISTS biz.excel_upload_log CASCADE;
-- 最后删除认证表
DROP TABLE IF EXISTS auth.tenant_admins CASCADE;
-- 清理序列CASCADE 已处理,此处为显式确认)
DROP SEQUENCE IF EXISTS biz.excel_upload_log_id_seq;
DROP SEQUENCE IF EXISTS biz.salary_adjustments_id_seq;
DROP SEQUENCE IF EXISTS biz.stg_finance_expense_id_seq;
DROP SEQUENCE IF EXISTS biz.stg_platform_income_id_seq;
DROP SEQUENCE IF EXISTS biz.stg_recharge_commission_id_seq;
DROP SEQUENCE IF EXISTS auth.tenant_admins_id_seq;
COMMIT;
```
## 验证步骤
```sql
-- 1. 确认 auth.tenant_admins 表存在且列完整12 列,含 admin_type + deleted_at
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'auth' AND table_name = 'tenant_admins'
ORDER BY ordinal_position;
-- 预期12 行id, username, password_hash, display_name, tenant_id,
-- managed_site_ids, admin_type, is_active, deleted_at, created_by, created_at, last_login_at
-- 2. 确认 tenant_admins 唯一约束
SELECT conname, contype FROM pg_constraint
WHERE conrelid = 'auth.tenant_admins'::regclass;
-- 预期:包含 tenant_admins_pkey (p) 和 tenant_admins_username_key (u)
-- 3. 确认 biz.excel_upload_log 表存在且 CHECK 约束正确
SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint
WHERE conrelid = 'biz.excel_upload_log'::regclass AND contype = 'c';
-- 预期2 行upload_type CHECK 和 status CHECK
-- 4. 确认 biz.salary_adjustments 外键和 CHECK 约束
SELECT conname, contype, pg_get_constraintdef(oid) FROM pg_constraint
WHERE conrelid = 'biz.salary_adjustments'::regclass AND contype IN ('c', 'f');
-- 预期3 行adjustment_type CHECK, amount CHECK, upload_batch_id FK
-- 5. 确认 3 张 staging 表的外键
SELECT c.conname, c.conrelid::regclass AS table_name
FROM pg_constraint c
WHERE c.confrelid = 'biz.excel_upload_log'::regclass AND c.contype = 'f';
-- 预期4 行salary_adjustments + 3 张 staging 表各 1 个 FK
-- 6. 确认索引
SELECT schemaname, tablename, indexname FROM pg_indexes
WHERE tablename IN ('tenant_admins', 'excel_upload_log', 'salary_adjustments',
'stg_finance_expense', 'stg_platform_income', 'stg_recharge_commission')
ORDER BY tablename, indexname;
-- 预期tenant_admins 3 个pkey + idx_tenant_admin_tenant + idx_tenant_admins_active_not_deleted
-- excel_upload_log 2 个pkey + idx_excel_log_site
-- salary_adjustments 3 个pkey + idx_salary_adj_site_month + idx_salary_adj_assistant_month
-- stg_* 各 1 个pkey
-- 7. 确认 6 张表均可查询
SELECT 'auth.tenant_admins' AS tbl, COUNT(*) FROM auth.tenant_admins
UNION ALL SELECT 'biz.excel_upload_log', COUNT(*) FROM biz.excel_upload_log
UNION ALL SELECT 'biz.salary_adjustments', COUNT(*) FROM biz.salary_adjustments
UNION ALL SELECT 'biz.stg_finance_expense', COUNT(*) FROM biz.stg_finance_expense
UNION ALL SELECT 'biz.stg_platform_income', COUNT(*) FROM biz.stg_platform_income
UNION ALL SELECT 'biz.stg_recharge_commission', COUNT(*) FROM biz.stg_recharge_commission;
-- 预期6 行,各表行数 ≥ 0
```
## NS4.1 变更补充2026-03-22
### auth.tenant_admins 行为变更
NS4.1 对 `auth.tenant_admins` 表做了以下调整:
#### deleted_at 软删除字段(已合并入主 DDL
`deleted_at` 字段已合并到主迁移脚本 `2026-03-20__ns4_tenant_admin_tables.sql` 中,不再需要独立的 ALTER TABLE 迁移。字段语义:
- `NULL` = 正常记录
-`NULL` = 已删除(时间戳记录删除时间)
-`is_active` 分离:`is_active` 控制启用/禁用,`deleted_at` 控制软删除
- 部分索引 `idx_tenant_admins_active_not_deleted` 仅索引 `deleted_at IS NULL` 的记录
#### 软删除逻辑
`DELETE /api/admin/tenant-admins/{id}` 端点实际执行软删除:将 `is_active` 设置为 `false`,不物理删除行。已禁用的管理员再次删除返回 409。
```sql
-- 软删除
UPDATE auth.tenant_admins SET is_active = false WHERE id = :id AND is_active = true;
```
#### tenant_id 来源变更
创建管理员时,`tenant_id` 不再是自由输入,而是从 `biz.tenants` 表中选择。后端校验 `tenant_id``biz.tenants` 中存在且 `is_active=true`
```sql
-- 创建时校验
SELECT id FROM biz.tenants WHERE id = :tenant_id AND is_active = true;
```
#### username 可编辑
`PATCH /api/admin/tenant-admins/{id}` 端点支持修改 `username`。修改时校验全局唯一性(大小写不敏感),冲突返回 409 Conflict。
```sql
-- 唯一性校验(大小写不敏感)
SELECT id FROM auth.tenant_admins WHERE LOWER(username) = LOWER(:new_username) AND id != :current_id AND deleted_at IS NULL;
```
#### 列表过滤
`GET /api/admin/tenant-admins` 端点默认只返回 `is_active=true` 的记录。新增 `include_inactive` 查询参数,设为 `true` 时返回所有记录(含已禁用)。
列表查询 JOIN `biz.tenants` 获取 `tenant_name` 字段。
---
## 关联文件
- DDL 基线auth`docs/database/ddl/zqyy_app__auth.sql`
- DDL 基线biz`docs/database/ddl/zqyy_app__biz.sql`
- 迁移脚本:`db/zqyy_app/migrations/2026-03-20__ns4_tenant_admin_tables.sql`
- 迁移脚本:`db/zqyy_app/migrations/2026-03-23__case_insensitive_username.sql`
- 迁移脚本:`db/zqyy_app/migrations/2026-03-23__cleanup_roles_add_admin_type.sql`
- 后端路由:`apps/backend/app/routers/tenant_auth.py``tenant_users.py``tenant_excel.py``tenant_clues.py``admin_tenant_admins.py``tenant_site_admins.py`
- 后端 Schema`apps/backend/app/schemas/tenant_excel.py``admin_tenant_admins.py`
- 认证模块:`apps/backend/app/auth/tenant_admins.py`
- Spec`.kiro/specs/tenant-admin-web/`
---
## NS4.2 变更补充2026-03-23
### auth.tenant_admins 用户名大小写不敏感
#### 变更说明
登录、创建、编辑管理员时,用户名统一转小写存储,查询使用 `LOWER()` 比较。避免 `Admin``admin` 被视为不同账号。
#### 新增索引
| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| idx_tenant_admins_username_lower | UNIQUE (partial) | LOWER(username) WHERE deleted_at IS NULL | 大小写不敏感唯一约束 |
#### 代码变更
| 文件 | 变更 |
|------|------|
| `tenant_auth.py` | 登录查询 `WHERE LOWER(username) = LOWER(%s)` |
| `admin_tenant_admins.py` | 创建 `INSERT ... VALUES (LOWER(%s), ...)`;编辑 `SET username = LOWER(%s)`;唯一性校验 `LOWER()` 比较 |
#### 兼容性
- 后端 API登录和 CRUD 接口透明兼容,无需前端改动
- ETL无影响
- 小程序:无影响(独立认证体系)
- 管理后台admin-web无需改动后端统一处理
#### 回滚策略
```sql
-- 1. 删除函数索引
DROP INDEX IF EXISTS auth.idx_tenant_admins_username_lower;
-- 2. 代码回滚:恢复 WHERE username = %s不带 LOWER
-- 注意:已小写化的用户名不可逆,但不影响功能
```
#### 验证 SQL
```sql
-- 1. 确认索引存在
SELECT indexname FROM pg_indexes
WHERE tablename = 'tenant_admins' AND indexname = 'idx_tenant_admins_username_lower';
-- 预期1 行
-- 2. 确认无大写用户名残留
SELECT username FROM auth.tenant_admins WHERE username != LOWER(username);
-- 预期0 行
-- 3. 确认大小写不敏感登录可用
SELECT id FROM auth.tenant_admins WHERE LOWER(username) = LOWER('Admin') AND deleted_at IS NULL;
-- 预期:与 SELECT ... WHERE LOWER(username) = 'admin' 结果一致
```
---
## NS4.3 变更补充2026-03-23
### 角色体系隔离 + 店铺管理员支持
#### 变更说明
| 对象 | 变更类型 | 说明 |
|------|---------|------|
| `auth.tenant_admins.admin_type` | 新增字段 | `VARCHAR(20) NOT NULL DEFAULT 'tenant_admin'`,区分租户管理员(`tenant_admin`)和店铺管理员(`site_admin` |
| `chk_admin_type` | 新增约束 | `CHECK (admin_type IN ('tenant_admin', 'site_admin'))` |
| `auth.roles``site_admin` / `tenant_admin` | 删除 | 小程序 RBAC 体系不需要这两个角色,租户/店铺管理员的区分通过 `admin_type` 列实现 |
| `auth.role_permissions` | 删除关联 | 删除 `site_admin` / `tenant_admin` 对应的 10 条权限映射 |
| `auth.roles``head_coach` / `manager` | 新增 | 小程序端新增教练和管理员角色 |
#### 业务规则
- 租户管理员(`admin_type='tenant_admin'`):可管理所有管辖门店,可创建/编辑/删除店铺管理员
- 店铺管理员(`admin_type='site_admin'`):仅可管理分配的门店,不可创建其他管理员
- 登录后界面一致,仅数据范围不同(由 `managed_site_ids` 控制)
- 用户名格式:`{第一个管辖店铺的 site_code} + 最长 50 字符`
- 只有 `admin_type='tenant_admin'` 的管理员可以访问 `/api/tenant/site-admins/*` 端点
#### 新增后端端点(店铺管理员 CRUD
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| GET | `/api/tenant/site-admins` | 列出店铺管理员 | 仅 tenant_admin |
| POST | `/api/tenant/site-admins` | 创建店铺管理员 | 仅 tenant_admin |
| PATCH | `/api/tenant/site-admins/{id}` | 编辑店铺管理员 | 仅 tenant_admin |
| DELETE | `/api/tenant/site-admins/{id}` | 删除店铺管理员(软删除) | 仅 tenant_admin |
| POST | `/api/tenant/site-admins/{id}/reset-password` | 重置密码 | 仅 tenant_admin |
#### 代码变更
| 文件 | 变更 |
|------|------|
| `tenant_auth.py` | 登录查询加入 `admin_type`JWT 签发包含 `admin_type` |
| `tenant_admins.py` | `CurrentTenantAdmin` dataclass 加入 `admin_type` 字段 |
| `tenant_site_admins.py` | 新增路由模块5 个 CRUD 端点 |
| `main.py` | 注册 `tenant_site_admins` 路由 |
| `admin_tenant_admins.py` | SQL 查询加入 `ta.admin_type` 列 |
| `xcx_auth.py` | `dev-switch-role` 硬编码更新(删除 site_admin/tenant_admin新增 head_coach/manager |
#### 兼容性
| 组件 | 影响 |
|------|------|
| 后端 API | 直接依赖。JWT 新增 `admin_type` 字段;新增 5 个店铺管理员端点admin-web 列表接口返回 `admin_type` |
| 租户管理后台tenant-admin | 直接依赖。菜单根据 `adminType` 动态显示;新增店铺管理员管理页面 |
| 管理后台admin-web | 间接依赖。租户管理员列表新增"类型"列显示 |
| 小程序 | 间接依赖。`auth.roles` 删除 site_admin/tenant_admin新增 head_coach/manager |
| ETL | 无影响 |
#### 回滚策略
```sql
BEGIN;
-- 1. 回滚 admin_type 列
ALTER TABLE auth.tenant_admins DROP CONSTRAINT IF EXISTS chk_admin_type;
ALTER TABLE auth.tenant_admins DROP COLUMN IF EXISTS admin_type;
-- 2. 回滚角色变更(恢复 site_admin/tenant_admin删除 head_coach/manager
DELETE FROM auth.role_permissions
WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('head_coach', 'manager'));
DELETE FROM auth.roles WHERE code IN ('head_coach', 'manager');
INSERT INTO auth.roles (code, name, description)
VALUES ('site_admin', '店铺管理员', '单店管理员,可查看所有看板和审核用户'),
('tenant_admin', '租户管理员', '连锁管理员,可管理多店铺和所有功能');
-- 恢复 site_admin/tenant_admin 的权限映射(各 5 条)
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r, auth.permissions p
WHERE r.code IN ('site_admin', 'tenant_admin');
-- 3. 代码回滚:移除 tenant_site_admins 路由JWT 移除 admin_type恢复 dev-switch-role 硬编码
COMMIT;
```
#### 验证 SQL
```sql
-- 1. 确认 admin_type 列存在且默认值正确
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = 'auth' AND table_name = 'tenant_admins' AND column_name = 'admin_type';
-- 预期admin_type, character varying, 'tenant_admin'::character varying
-- 2. 确认 CHECK 约束
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'auth.tenant_admins'::regclass AND conname = 'chk_admin_type';
-- 预期chk_admin_type, CHECK ((admin_type)::text = ANY (...))
-- 3. 确认角色体系4 条,无 site_admin/tenant_admin
SELECT code, name FROM auth.roles ORDER BY id;
-- 预期coach, staff, head_coach, manager
-- 4. 确认角色-权限映射11 条)
SELECT r.code, COUNT(rp.permission_id) AS perm_count
FROM auth.roles r
JOIN auth.role_permissions rp ON r.id = rp.role_id
GROUP BY r.code ORDER BY r.code;
-- 预期coach=2, head_coach=2, manager=5, staff=2共 11 条head_coach 仅 view_tasks+view_board
```

View File

@@ -2,7 +2,9 @@
## DDL 基线(`ddl/` 子目录)
从测试库自动导出的完整 DDL按 schema 分文件。重新生成:`python scripts/ops/gen_consolidated_ddl.py`
从测试库自动导出的完整 DDL按 schema 分文件。重新生成:`PYTHONUTF8=1 python tools/db/gen_consolidated_ddl.py`
最近一次刷新:**2026-04-05**(合并了截至 2026-03-31 的全部迁移)。
| 文件 | 数据库 | Schema | 内容 |
|------|--------|--------|------|
@@ -10,12 +12,13 @@
| `etl_feiqiu__ods.sql` | etl_feiqiu | ods | 原始数据层23 表) |
| `etl_feiqiu__dwd.sql` | etl_feiqiu | dwd | 明细数据层42 表) |
| `etl_feiqiu__core.sql` | etl_feiqiu | core | 跨门店标准化7 表) |
| `etl_feiqiu__dws.sql` | etl_feiqiu | dws | 汇总数据层36 + 物化视图 |
| `etl_feiqiu__dws.sql` | etl_feiqiu | dws | 汇总数据层38 表) |
| `etl_feiqiu__app.sql` | etl_feiqiu | app | RLS 视图层(仅视图,无表) |
| `zqyy_app__public.sql` | zqyy_app | public | 小程序业务表12 表) |
| `zqyy_app__auth.sql` | zqyy_app | auth | 用户认证与权限(8 表) |
| `zqyy_app__biz.sql` | zqyy_app | biz | 核心业务表(任务/备注/触发器/AI7 表) |
| `fdw.sql` | — | — | FDW 正向跨库映射配置etl→app |
| `zqyy_app__auth.sql` | zqyy_app | auth | 用户认证与权限(9 表) |
| `zqyy_app__biz.sql` | zqyy_app | biz | 核心业务表(21 表) |
| `fdw.sql` | — | — | FDW 正向映射zqyy_app → etl_feiqiu.app |
| `fdw_reverse.sql` | — | — | FDW 反向映射etl_feiqiu → zqyy_app |
## 业务库文档BD_Manual — zqyy_app / 跨模块)
@@ -23,31 +26,39 @@
| 文件 | 说明 |
|------|------|
| `BD_Manual_auth_tables.md` | zqyy_app auth schema 表结构 |
| `BD_Manual_biz_tables.md` | zqyy_app biz schema 表结构 |
| `BD_Manual_ai_tables.md` | zqyy_app AI 相关表 |
| `BD_Manual_auth_biz_schemas.md` | auth + biz schema 建库说明 |
| `BD_Manual_app_schema_rls_views.md` | app schema RLS 视图 |
| `BD_Manual_fdw_etl_setup.md` | FDW 跨库映射配置 |
| `BD_Manual_member_retention_clue.md` | zqyy_app 维客线索表 |
| `BD_manual_public_rbac_tables.md` | zqyy_app public schema RBAC 与工作流admin_users/users/roles/permissions/user_roles/tasks/approvals8 表) |
| `BD_manual_scheduled_tasks.md` | zqyy_app public schema 调度任务表scheduled_tasks/task_queue/task_execution_log |
| `BD_manual_auth_tables.md` | zqyy_app auth schema 表结构9 表) |
| `BD_manual_auth_biz_schemas.md` | auth + biz schema 建库说明 |
| `BD_manual_biz_tables.md` | zqyy_app biz schema 核心业务表coach_tasks/notes/trigger_jobs12 字段) |
| `BD_manual_biz_registry_tables.md` | zqyy_app biz schema 注册体系connectors/tenants/sites/site_code_history |
| `BD_manual_ai_tables.md` | zqyy_app biz schema AI 相关表ai_conversations/ai_messages/ai_cache/ai_run_logs/ai_trigger_jobs |
| `BD_manual_tenant_admin_tables.md` | zqyy_app auth/biz 租户管理后台表tenant_admins/excel_upload_log/salary_adjustments/stg_* |
| `BD_manual_member_retention_clue.md` | zqyy_app 维客线索表 |
| `BD_manual_app_schema_rls_views.md` | ETL app schema RLS 视图49 视图) |
| `BD_manual_fdw_etl_setup.md` | FDW 正向跨库映射配置zqyy_app → etl_feiqiu |
| `BD_manual_fdw_reverse_retention_clue.md` | FDW 反向映射etl_feiqiu → zqyy_app 维客线索) |
## 归档(`_archived/` 子目录)
已吸收进 DDL 基线的迁移变更记录,仅供历史参考:
- 迁移变更类 BD_Manual加列、改约束、删表、FDW 变更、tenant_id 类型变更等)
- 迁移变更类 BD_Manual加列、改约束、删表、FDW 变更、tenant_id 类型变更、tenant_admins 软删除等)
- `etl_feiqiu_schema_migration.md`(旧迁移汇总)
- `zqyy_app_admin_web_tables.md`(建表记录)
## 注意事项
## 文档分工
- `fdw.sql` 仅包含正向映射etl_feiqiu → zqyy_app反向映射zqyy_app → etl_feiqiu的可执行脚本在 `db/fdw/setup_fdw_reverse*.sql`
- DDL 基线最近一次从测试库导出日期2026-03-15合并了 2026-02-27 至 2026-03-09 的全部迁移)
- ETL 专属文档ODS→DWD 映射、DWS 表文档):`apps/etl/connectors/feiqiu/docs/database/`
| 位置 | 管辖范围 |
|------|---------|
| `docs/database/`(本目录) | 业务库(zqyy_app) BD_Manual + 跨模块(FDW/RLS) + DDL 基线 |
| `apps/etl/connectors/feiqiu/docs/database/` | ETL 专属(ODS/DWD/DWS 表级文档、API 映射) |
| `db/` | 运行时资产(迁移脚本/FDW 配置/建库脚本) |
| `tools/db/` | 数据库工具(DDL 生成/验证/一致性检查) |
## 相关资源
- 种子数据:已合并进各 DDL 文件末尾(不再单独维护)
- FDW 配置(可执行):`db/fdw/`(含正向 + 反向 + 测试环境版本)
- DDL 生成脚本:`scripts/ops/gen_consolidated_ddl.py`
- 迁移脚本(活跃):`db/etl_feiqiu/migrations/``db/zqyy_app/migrations/`当前已清空1.0 基线已统一
- DDL 生成脚本:`tools/db/gen_consolidated_ddl.py`
- 迁移脚本(活跃):`db/etl_feiqiu/migrations/`11 个)`db/zqyy_app/migrations/`8 个
- 迁移脚本归档:`db/_archived/ddl_baseline_2026-02-22/`

View File

@@ -0,0 +1,59 @@
# BD 手册 — auth.tenant_admins 新增 deleted_at 软删除字段
## 变更日期
2026-03-22
## 变更说明
### 新增字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `deleted_at` | `TIMESTAMPTZ` | `NULL` | 软删除时间戳NULL=正常,非 NULL=已删除 |
### 新增索引
| 索引名 | 说明 |
|--------|------|
| `idx_tenant_admins_active_not_deleted` | 部分索引 `(is_active) WHERE deleted_at IS NULL`,加速列表和登录查询 |
### 语义变更
- 删除与禁用分离:`is_active` 仅控制启用/禁用,`deleted_at` 控制软删除
- DELETE 接口改为设置 `deleted_at = NOW()`,不再检查 `is_active` 状态
- 所有查询(列表、登录、编辑、重置密码)默认过滤 `deleted_at IS NULL`
- 用户名唯一性校验仅在未删除记录中生效
## 兼容性影响
| 模块 | 影响 |
|------|------|
| 后端 DELETE 接口 | 改为设置 `deleted_at`,不再返回 409已禁用 |
| 后端列表接口 | 新增 `deleted_at IS NULL` 过滤,已删除记录不再出现 |
| 租户登录接口 | 新增 `deleted_at IS NULL` 过滤,已删除账号无法登录 |
| 编辑/重置密码接口 | WHERE 条件加 `deleted_at IS NULL`,已删除记录返回 404 |
| 前端 | 无需改动,删除按钮已存在 |
| ETL | 无影响(不涉及 tenant_admins 表) |
## 回滚策略
```sql
DROP INDEX IF EXISTS auth.idx_tenant_admins_active_not_deleted;
ALTER TABLE auth.tenant_admins DROP COLUMN IF EXISTS deleted_at;
```
回滚后需同步还原后端代码中的 DELETE 接口逻辑。
## 验证 SQL
```sql
-- 1) 字段存在性
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'auth' AND table_name = 'tenant_admins' AND column_name = 'deleted_at';
-- 预期1 行
-- 2) 索引存在性
SELECT indexname FROM pg_indexes
WHERE schemaname = 'auth' AND tablename = 'tenant_admins'
AND indexname = 'idx_tenant_admins_active_not_deleted';
-- 预期1 行
-- 3) 现有数据不受影响
SELECT COUNT(*) FROM auth.tenant_admins WHERE deleted_at IS NOT NULL;
-- 预期0
```

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / appRLS 视图层)
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -39,6 +39,17 @@ SELECT id,
FROM dws.dws_assistant_daily_detail d;
;
CREATE OR REPLACE VIEW app.v_cfg_area_category AS
SELECT DISTINCT category_code,
category_name,
display_name,
short_name,
sort_order
FROM dws.cfg_area_category
WHERE ((is_active = true) AND ((category_code)::text <> ALL ((ARRAY['SPECIAL'::character varying, 'OTHER'::character varying])::text[])))
ORDER BY sort_order;
;
CREATE OR REPLACE VIEW app.v_cfg_assistant_level_price AS
SELECT price_id,
level_code,
@@ -104,19 +115,6 @@ SELECT tier_id,
FROM dws.cfg_performance_tier;
;
CREATE OR REPLACE VIEW app.v_cfg_area_category AS
SELECT DISTINCT
category_code,
category_name,
display_name,
short_name,
sort_order
FROM dws.cfg_area_category
WHERE is_active = TRUE
AND category_code NOT IN ('SPECIAL', 'OTHER')
ORDER BY sort_order;
;
CREATE OR REPLACE VIEW app.v_dim_assistant AS
SELECT assistant_id,
user_id,
@@ -606,6 +604,26 @@ SELECT contribution_id,
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_assistant_project_tag AS
SELECT id,
site_id,
tenant_id,
assistant_id,
time_window,
category_code,
category_name,
short_name,
duration_seconds,
total_seconds,
percentage,
is_tagged,
computed_at,
created_at,
updated_at
FROM dws.dws_assistant_project_tag
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_assistant_recharge_commission AS
SELECT id,
site_id,
@@ -671,6 +689,92 @@ SELECT id,
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_coach_area_hours AS
SELECT id,
site_id,
tenant_id,
stat_month,
assistant_id,
area_code,
base_hours,
bonus_hours,
room_hours,
effective_hours,
trashed_hours,
base_service_count,
bonus_service_count,
room_service_count,
created_at,
updated_at
FROM dws.dws_coach_area_hours
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
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);
;
CREATE OR REPLACE VIEW app.v_dws_finance_board_cache AS
SELECT id,
site_id,
time_range,
area_code,
start_date,
end_date,
prev_start_date,
prev_end_date,
occurrence,
discount,
discount_rate,
confirmed_revenue,
cash_in,
cash_out,
cash_balance,
balance_rate,
data_fingerprint,
computed_at,
created_at,
updated_at
FROM dws.dws_finance_board_cache
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_finance_daily_summary AS
SELECT id,
site_id,
@@ -691,6 +795,8 @@ SELECT id,
confirmed_income,
cash_inflow_total,
cash_pay_amount,
cash_paper_amount,
scan_pay_amount,
groupbuy_pay_amount,
platform_settlement_amount,
platform_fee_amount,
@@ -963,7 +1069,54 @@ SELECT newconv_id,
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_member_project_tag AS
SELECT id,
site_id,
tenant_id,
member_id,
time_window,
category_code,
category_name,
short_name,
duration_seconds,
total_seconds,
percentage,
is_tagged,
computed_at,
created_at,
updated_at
FROM dws.dws_member_project_tag
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_member_spending_power_index AS
SELECT spi_id,
site_id,
member_id,
spend_30,
spend_90,
recharge_90,
orders_30,
orders_90,
visit_days_30,
visit_days_90,
avg_ticket_90,
active_weeks_90,
daily_spend_ewma_90,
score_level_raw,
score_speed_raw,
score_stability_raw,
score_level_display,
score_speed_display,
score_stability_display,
raw_score,
display_score,
calc_time,
created_at,
updated_at
FROM dws.dws_member_spending_power_index
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW app.v_dws_member_visit_detail AS
SELECT id,
@@ -1182,34 +1335,3 @@ SELECT site_id,
FROM core.dim_site s;
;
-- 2026-03-19 新增BOARD 看板所需的 3 个 RLS 视图
-- 迁移脚本db/etl_feiqiu/migrations/2026-03-19_add_board_rls_views.sql
CREATE OR REPLACE VIEW app.v_dws_assistant_project_tag AS
SELECT id, site_id, tenant_id, assistant_id, time_window,
category_code, category_name, short_name,
duration_seconds, total_seconds, percentage, is_tagged,
computed_at, created_at, updated_at
FROM dws.dws_assistant_project_tag
WHERE site_id = current_setting('app.current_site_id')::bigint;
CREATE OR REPLACE VIEW app.v_dws_member_project_tag AS
SELECT id, site_id, tenant_id, member_id, time_window,
category_code, category_name, short_name,
duration_seconds, total_seconds, percentage, is_tagged,
computed_at, created_at, updated_at
FROM dws.dws_member_project_tag
WHERE site_id = current_setting('app.current_site_id')::bigint;
CREATE OR REPLACE VIEW app.v_dws_member_spending_power_index AS
SELECT spi_id, site_id, member_id,
spend_30, spend_90, recharge_90,
orders_30, orders_90, visit_days_30, visit_days_90,
avg_ticket_90, active_weeks_90, daily_spend_ewma_90,
score_level_raw, score_speed_raw, score_stability_raw,
score_level_display, score_speed_display, score_stability_display,
raw_score, display_score,
calc_time, created_at, updated_at
FROM dws.dws_member_spending_power_index
WHERE site_id = current_setting('app.current_site_id')::bigint;

View File

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

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dwd明细数据层
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -284,7 +284,11 @@ CREATE TABLE dwd.dim_member_ex (
scd2_start_time timestamp with time zone NOT NULL,
scd2_end_time timestamp with time zone,
scd2_is_current integer,
scd2_version integer
scd2_version integer,
other_pay_money_sum numeric(18,2),
last_consume_time timestamp with time zone,
non_consume_day_num integer,
first_consumption integer
);
CREATE TABLE dwd.dim_site (
@@ -617,7 +621,9 @@ CREATE TABLE dwd.dwd_assistant_service_log_ex (
composite_grade_time timestamp with time zone,
assistant_team_name text,
operator_id bigint,
operator_name text
operator_name text,
deduct_leave_seconds integer DEFAULT 0,
order_from integer
);
CREATE TABLE dwd.dwd_goods_stock_movement (
@@ -660,7 +666,8 @@ CREATE TABLE dwd.dwd_goods_stock_summary (
current_stock numeric(18,4),
site_id bigint,
tenant_id bigint,
fetched_at timestamp with time zone NOT NULL
fetched_at timestamp with time zone NOT NULL,
create_time timestamp with time zone
);
CREATE TABLE dwd.dwd_groupbuy_redemption (
@@ -986,7 +993,8 @@ CREATE TABLE dwd.dwd_settlement_head_ex (
salesman_name character varying(100),
order_remark character varying(255),
operator_id bigint,
salesman_user_id bigint
salesman_user_id bigint,
order_from integer
);
CREATE TABLE dwd.dwd_store_goods_sale (
@@ -1045,7 +1053,10 @@ CREATE TABLE dwd.dwd_store_goods_sale_ex (
push_money numeric(18,2),
is_single_order integer,
sales_type integer,
operator_id bigint
operator_id bigint,
activity_amount numeric(18,2) DEFAULT 0,
activity_id bigint DEFAULT 0,
order_from integer
);
CREATE TABLE dwd.dwd_table_fee_adjust (
@@ -1128,7 +1139,8 @@ CREATE TABLE dwd.dwd_table_fee_log_ex (
operator_id bigint,
salesman_user_id bigint,
salesman_org_id bigint,
order_consumption_type integer
order_consumption_type integer,
order_from integer
);
-- 约束(主键 / 唯一 / 外键)

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / dws汇总数据层
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -21,6 +21,9 @@ CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_order_contribution_contribution_
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_project_tag_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_recharge_commission_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_assistant_salary_calc_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_coach_area_hours_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_area_daily_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_board_cache_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_daily_summary_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_discount_detail_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS dws.dws_finance_expense_summary_id_seq AS bigint;
@@ -364,6 +367,86 @@ CREATE TABLE dws.dws_assistant_salary_calc (
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE dws.dws_coach_area_hours (
id bigint DEFAULT nextval('dws.dws_coach_area_hours_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
tenant_id bigint NOT NULL,
stat_month date NOT NULL,
assistant_id bigint NOT NULL,
area_code character varying(20) NOT NULL,
base_hours numeric(10,2) DEFAULT 0 NOT NULL,
bonus_hours numeric(10,2) DEFAULT 0 NOT NULL,
room_hours numeric(10,2) DEFAULT 0 NOT NULL,
effective_hours numeric(10,2) DEFAULT 0 NOT NULL,
trashed_hours numeric(10,2) DEFAULT 0 NOT NULL,
base_service_count integer DEFAULT 0 NOT NULL,
bonus_service_count integer DEFAULT 0 NOT NULL,
room_service_count integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE dws.dws_finance_area_daily (
id bigint DEFAULT nextval('dws.dws_finance_area_daily_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
tenant_id bigint NOT NULL,
stat_date date NOT NULL,
area_code character varying(20) NOT NULL,
table_fee_amount numeric(14,2) DEFAULT 0 NOT NULL,
goods_amount numeric(14,2) DEFAULT 0 NOT NULL,
assistant_pd_amount numeric(14,2) DEFAULT 0 NOT NULL,
assistant_cx_amount numeric(14,2) DEFAULT 0 NOT NULL,
gross_amount numeric(14,2) DEFAULT 0 NOT NULL,
discount_groupbuy numeric(14,2) DEFAULT 0 NOT NULL,
discount_vip numeric(14,2) DEFAULT 0 NOT NULL,
discount_manual numeric(14,2) DEFAULT 0 NOT NULL,
discount_gift_card numeric(14,2) DEFAULT 0 NOT NULL,
discount_rounding numeric(14,2) DEFAULT 0 NOT NULL,
discount_other numeric(14,2) DEFAULT 0 NOT NULL,
discount_total numeric(14,2) DEFAULT 0 NOT NULL,
confirmed_income numeric(14,2) DEFAULT 0 NOT NULL,
cash_pay_amount numeric(14,2) DEFAULT 0 NOT NULL,
cash_paper_amount numeric(14,2) DEFAULT 0 NOT NULL,
scan_pay_amount numeric(14,2) DEFAULT 0 NOT NULL,
groupbuy_pay_amount numeric(14,2) DEFAULT 0 NOT NULL,
recharge_cash_inflow numeric(14,2) DEFAULT 0 NOT NULL,
cash_inflow_total 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,
card_consume_total 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,
recharge_cash numeric(14,2) DEFAULT 0 NOT NULL,
first_recharge_cash numeric(14,2) DEFAULT 0 NOT NULL,
renewal_cash numeric(14,2) DEFAULT 0 NOT NULL,
order_count integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE dws.dws_finance_board_cache (
id bigint DEFAULT nextval('dws.dws_finance_board_cache_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
time_range character varying(20) NOT NULL,
area_code character varying(20) NOT NULL,
start_date date NOT NULL,
end_date date NOT NULL,
prev_start_date date,
prev_end_date date,
occurrence numeric(14,2) DEFAULT 0 NOT NULL,
discount numeric(14,2) DEFAULT 0 NOT NULL,
discount_rate numeric(8,4) DEFAULT 0 NOT NULL,
confirmed_revenue numeric(14,2) DEFAULT 0 NOT NULL,
cash_in numeric(14,2) DEFAULT 0 NOT NULL,
cash_out numeric(14,2) DEFAULT 0 NOT NULL,
cash_balance numeric(14,2) DEFAULT 0 NOT NULL,
balance_rate numeric(8,4) DEFAULT 0 NOT NULL,
data_fingerprint character varying(64),
computed_at timestamp with time zone DEFAULT now() NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE dws.dws_finance_daily_summary (
id bigint DEFAULT nextval('dws.dws_finance_daily_summary_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
@@ -406,7 +489,9 @@ CREATE TABLE dws.dws_finance_daily_summary (
guest_order_count integer DEFAULT 0 NOT NULL,
avg_order_amount numeric(12,2) DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
cash_paper_amount numeric(14,2) DEFAULT 0 NOT NULL,
scan_pay_amount numeric(14,2) DEFAULT 0 NOT NULL
);
CREATE TABLE dws.dws_finance_discount_detail (
@@ -481,14 +566,14 @@ CREATE TABLE dws.dws_finance_recharge_summary (
total_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
cash_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
gift_liquor_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_table_fee_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_voucher_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_liquor_recharge numeric(14,2) DEFAULT 0 NOT NULL,
gift_table_fee_recharge numeric(14,2) DEFAULT 0 NOT NULL,
gift_voucher_recharge numeric(14,2) DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
gift_voucher_recharge numeric(14,2) DEFAULT 0 NOT NULL
);
CREATE TABLE dws.dws_goods_stock_daily_summary (
@@ -631,7 +716,13 @@ CREATE TABLE dws.dws_member_assistant_relation_index (
ml_display numeric(4,2) DEFAULT 0 NOT NULL,
calc_time timestamp with time zone DEFAULT now() NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
recall_created_total integer DEFAULT 0 NOT NULL,
recall_completed_total integer DEFAULT 0 NOT NULL,
follow_up_created_total integer DEFAULT 0 NOT NULL,
follow_up_completed_total integer DEFAULT 0 NOT NULL,
total_created integer DEFAULT 0 NOT NULL,
total_completed integer DEFAULT 0 NOT NULL
);
CREATE TABLE dws.dws_member_consumption_summary (
@@ -976,6 +1067,12 @@ ALTER TABLE dws.dws_assistant_project_tag ADD CONSTRAINT uk_dws_assistant_projec
ALTER TABLE dws.dws_assistant_recharge_commission ADD CONSTRAINT dws_assistant_recharge_commission_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_assistant_salary_calc ADD CONSTRAINT dws_assistant_salary_calc_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_assistant_salary_calc ADD CONSTRAINT uk_dws_assistant_salary UNIQUE (site_id, assistant_id, salary_month, assistant_level_code);
ALTER TABLE dws.dws_coach_area_hours ADD CONSTRAINT dws_coach_area_hours_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_coach_area_hours ADD CONSTRAINT dws_coach_area_hours_site_id_stat_month_assistant_id_area_c_key UNIQUE (site_id, stat_month, assistant_id, area_code);
ALTER TABLE dws.dws_finance_area_daily ADD CONSTRAINT dws_finance_area_daily_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_finance_area_daily ADD CONSTRAINT dws_finance_area_daily_site_id_stat_date_area_code_key UNIQUE (site_id, stat_date, area_code);
ALTER TABLE dws.dws_finance_board_cache ADD CONSTRAINT dws_finance_board_cache_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_finance_board_cache ADD CONSTRAINT dws_finance_board_cache_site_id_time_range_area_code_key UNIQUE (site_id, time_range, area_code);
ALTER TABLE dws.dws_finance_daily_summary ADD CONSTRAINT dws_finance_daily_summary_pkey PRIMARY KEY (id);
ALTER TABLE dws.dws_finance_daily_summary ADD CONSTRAINT uk_dws_finance_daily UNIQUE (site_id, stat_date);
ALTER TABLE dws.dws_finance_discount_detail ADD CONSTRAINT dws_finance_discount_detail_pkey PRIMARY KEY (id);
@@ -1099,6 +1196,92 @@ CREATE INDEX idx_mv_finance_daily_l3 ON dws.mv_dws_finance_daily_summary_l3 USIN
CREATE INDEX idx_mv_finance_daily_l4 ON dws.mv_dws_finance_daily_summary_l4 USING btree (site_id, stat_date);
-- 视图
CREATE OR REPLACE VIEW dws.v_dws_coach_area_hours AS
SELECT id,
site_id,
tenant_id,
stat_month,
assistant_id,
area_code,
base_hours,
bonus_hours,
room_hours,
effective_hours,
trashed_hours,
base_service_count,
bonus_service_count,
room_service_count,
created_at,
updated_at
FROM dws.dws_coach_area_hours
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
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);
;
CREATE OR REPLACE VIEW dws.v_dws_finance_board_cache AS
SELECT id,
site_id,
time_range,
area_code,
start_date,
end_date,
prev_start_date,
prev_end_date,
occurrence,
discount,
discount_rate,
confirmed_revenue,
cash_in,
cash_out,
cash_balance,
balance_rate,
data_fingerprint,
computed_at,
created_at,
updated_at
FROM dws.dws_finance_board_cache
WHERE (site_id = (current_setting('app.current_site_id'::text))::bigint);
;
CREATE OR REPLACE VIEW dws.v_member_recall_priority AS
SELECT dws_member_winback_index.site_id,
dws_member_winback_index.tenant_id,
@@ -1510,625 +1693,3 @@ 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);
-- =============================================================================
-- 种子数据DWS 配置表初始数据(绩效档位、等级定价、奖金规则、区域分类、技能映射)
-- =============================================================================
-- =============================================================================
-- DWS 配置表初始数据
-- 版本: v4.0
-- 创建日期: 2026-02-01
-- 更新日期: 2026-02-21
-- AI_CHANGELOG [2026-02-21] 取消全文注释,数据已写入 test_etl_feiqiu
-- 新增 2025-01-01~2026-02-28 统一提成档位基础课18元/小时打赏课40%
-- 新增 GUARANTEE 保底奖金规则按等级初级12000/中级16000/高级18000/星级23000
-- 历史分档口径截止日期调整为 2024-12-31
-- 描述: 初始化配置表数据,包含绩效档位、等级定价、奖金规则、区域分类、技能映射
-- =============================================================================
-- =============================================================================
-- 1. cfg_performance_tier - 绩效档位配置(含历史口径)
-- 数据来源DWS 数据库处理需求.md
-- 三段时间线:
-- 2000-01-01 ~ 2024-12-31: 旧方案6档阶梯抽成
-- 2025-01-01 ~ 2026-02-28: 统一提成不分档基础课18元/小时打赏课40%
-- 2026-03-01 ~ 9999-12-31: 新方案5档阶梯抽成
-- =============================================================================
TRUNCATE TABLE dws.cfg_performance_tier RESTART IDENTITY CASCADE;
INSERT INTO dws.cfg_performance_tier (
tier_code, tier_name, tier_level,
min_hours, max_hours,
base_deduction, bonus_deduction_ratio, vacation_days, vacation_unlimited,
is_new_hire_tier, effective_from, effective_to, description
) VALUES
-- 旧方案至2024-12-31
('T0', '0档-淘汰压力', 0,
0, 100,
28.00, 0.50, 3, FALSE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案H<100专业课抽成28元/小时打赏课抽成50%休假3天'),
('T1', '1档-及格档', 1,
100, 130,
18.00, 0.40, 4, FALSE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案100≤H<130专业课抽成18元/小时打赏课抽成40%休假4天'),
('T2', '2档-良好档', 2,
130, 160,
15.00, 0.38, 4, FALSE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案130≤H<160专业课抽成15元/小时打赏课抽成38%休假4天'),
('T3', '3档-优秀档', 3,
160, 190,
13.00, 0.35, 5, FALSE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案160≤H<190专业课抽成13元/小时打赏课抽成35%休假5天'),
('T4', '4档-卓越加速档', 4,
190, 220,
10.00, 0.33, 6, FALSE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案190≤H<220专业课抽成10元/小时打赏课抽成33%休假6天'),
('T5', '5档-冠军加速档', 5,
220, NULL,
8.00, 0.30, 0, TRUE,
FALSE, '2000-01-01', '2024-12-31',
'旧方案H≥220专业课抽成8元/小时打赏课抽成30%,休假自由'),
-- 2025-01-01 ~ 2026-02-28: 统一提成(不分档,所有助教统一规则)
-- CHANGE 2026-02-21 | 新增统一提成档位基础课球房提成18元/小时打赏课球房提成40%
('T0', '统一档', 0,
0, NULL,
18.00, 0.40, 0, FALSE,
FALSE, '2025-01-01', '2026-02-28',
'2025-01-01~2026-02-28统一规则基础课球房提成18元/小时打赏课球房提成40%,不分档位'),
-- 新方案2026-03-01起
('T0', '0档-淘汰压力', 0,
0, 120,
28.00, 0.50, 3, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案H<120专业课抽成28元/小时打赏课抽成50%休假3天'),
('T1', '1档-及格档', 1,
120, 150,
18.00, 0.40, 4, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案120≤H<150专业课抽成18元/小时打赏课抽成40%休假4天'),
('T2', '2档-良好档', 2,
150, 180,
13.00, 0.35, 5, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案150≤H<180专业课抽成13元/小时打赏课抽成35%休假5天'),
('T3', '3档-优秀档', 3,
180, 210,
10.00, 0.30, 6, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案180≤H<210专业课抽成10元/小时打赏课抽成30%休假6天'),
('T4', '4档-销冠竞争', 4,
210, NULL,
8.00, 0.25, 0, TRUE,
FALSE, '2026-03-01', '9999-12-31',
'新方案H≥210专业课抽成8元/小时打赏课抽成25%,休假自由');
-- =============================================================================
-- 2. cfg_assistant_level_price - 助教等级定价
-- 说明:
-- - level_code 来自 dim_assistant.assistant_level
-- - 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级
-- - 价格为客户支付价格(对外价格),助教收入=客户支付-档位抽成
-- - 包厢课基础课统一138元/小时(不随等级变化)
-- =============================================================================
TRUNCATE TABLE dws.cfg_assistant_level_price RESTART IDENTITY CASCADE;
INSERT INTO dws.cfg_assistant_level_price (
level_code, level_name,
base_course_price, bonus_course_price,
effective_from, effective_to, description
) VALUES
(10, '初级',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'初级助教基础课98元/时附加课190元/时(客户支付价格)'),
(20, '中级',
108.00, 190.00,
'2000-01-01', '9999-12-31',
'中级助教基础课108元/时附加课190元/时(客户支付价格)'),
(30, '高级',
118.00, 190.00,
'2000-01-01', '9999-12-31',
'高级助教基础课118元/时附加课190元/时(客户支付价格)'),
(40, '星级',
138.00, 190.00,
'2000-01-01', '9999-12-31',
'星级助教基础课138元/时附加课190元/时(客户支付价格)'),
(8, '助教管理',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'助教管理:不参与客户服务计费,默认按初级价格');
-- =============================================================================
-- 3. cfg_bonus_rules - 奖金规则配置
-- 说明:
-- - SPRINT: 冲刺奖金历史口径至2024-12-31
-- - GUARANTEE: 保底月薪线2025-01-01~2026-02-28按等级区分
-- * 保底规则:总课时达标 + 打赏课≥10小时 → 触发保底月薪线
-- * 保底含义:实发 = MAX(课时收入+奖金, 保底金额),非额外奖金
-- * rule_code 中 LV10/LV20/LV30/LV40 对应 level_code
-- - TOP_RANK: Top3排名奖金2026-03-01起
-- CHANGE 2026-02-21 | 新增 GUARANTEE 保底奖金规则
-- =============================================================================
TRUNCATE TABLE dws.cfg_bonus_rules RESTART IDENTITY CASCADE;
INSERT INTO dws.cfg_bonus_rules (
rule_type, rule_code, rule_name,
threshold_hours, rank_position, bonus_amount,
is_cumulative, priority,
effective_from, effective_to, description
) VALUES
-- 冲刺奖金历史口径至2024-12-31
('SPRINT', 'SPRINT_190', '冲刺奖金190',
190.00, NULL, 300.00,
FALSE, 1,
'2000-01-01', '2024-12-31',
'历史口径业绩≥190小时获得300元冲刺奖金不累计'),
('SPRINT', 'SPRINT_220', '冲刺奖金220',
220.00, NULL, 800.00,
FALSE, 2,
'2000-01-01', '2024-12-31',
'历史口径业绩≥220小时获得800元冲刺奖金覆盖190档'),
-- 保底奖金2025-01-01 ~ 2026-02-28
-- 按助教等级区分需同时满足总课时和打赏课最低时数≥10小时
('GUARANTEE', 'GUAR_LV10', '初级保底奖金',
130.00, NULL, 12000.00,
FALSE, 10,
'2025-01-01', '2026-02-28',
'初级保底完成130小时课程含≥10小时打赏课保底月薪线12000元实发=MAX(课时收入+奖金, 12000)'),
('GUARANTEE', 'GUAR_LV20', '中级保底奖金',
150.00, NULL, 16000.00,
FALSE, 20,
'2025-01-01', '2026-02-28',
'中级保底完成150小时课程含≥10小时打赏课保底月薪线16000元实发=MAX(课时收入+奖金, 16000)'),
('GUARANTEE', 'GUAR_LV30', '高级保底奖金',
160.00, NULL, 18000.00,
FALSE, 30,
'2025-01-01', '2026-02-28',
'高级保底完成160小时课程含≥10小时打赏课保底月薪线18000元实发=MAX(课时收入+奖金, 18000)'),
('GUARANTEE', 'GUAR_LV40', '星级保底奖金',
170.00, NULL, 23000.00,
FALSE, 40,
'2025-01-01', '2026-02-28',
'星级保底完成170小时课程含≥10小时打赏课保底月薪线23000元实发=MAX(课时收入+奖金, 23000)'),
-- Top排名奖金2026-03-01起
('TOP_RANK', 'TOP_1', 'Top1排名奖金',
NULL, 1, 1000.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第一获得1000元并列都算'),
('TOP_RANK', 'TOP_2', 'Top2排名奖金',
NULL, 2, 600.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第二获得600元并列都算'),
('TOP_RANK', 'TOP_3', 'Top3排名奖金',
NULL, 3, 400.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第三获得400元并列都算');
-- =============================================================================
-- 4. cfg_area_category - 台区分类映射(纯台桌级精确映射)
-- 说明:
-- - 每台桌一行精确映射source_area_name=区域, source_table_name=台桌名
-- - 不使用 LIKE 模糊匹配,仅 EXACT + DEFAULT 兜底
-- - 数据来源: 用户提供的完整台桌清单2026-03-09
-- =============================================================================
TRUNCATE TABLE dws.cfg_area_category RESTART IDENTITY CASCADE;
INSERT INTO dws.cfg_area_category (
source_area_name, source_table_name, category_code, category_name,
display_name, short_name,
match_type, match_priority, is_active, description, sort_order
) VALUES
-- ============ BILLIARD 🎱 中式/追分 (sort_order=10) ============
-- A区18台
('A区', 'A1', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A1', 10),
('A区', 'A2', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A2', 10),
('A区', 'A3', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A3', 10),
('A区', 'A4', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A4', 10),
('A区', 'A5', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A5', 10),
('A区', 'A6', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A6', 10),
('A区', 'A7', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A7', 10),
('A区', 'A8', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A8', 10),
('A区', 'A9', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A9', 10),
('A区', 'A10', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A10', 10),
('A区', 'A11', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A11', 10),
('A区', 'A12', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A12', 10),
('A区', 'A13', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A13', 10),
('A区', 'A14', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A14', 10),
('A区', 'A15', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A15', 10),
('A区', 'A16', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A16', 10),
('A区', 'A17', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A17', 10),
('A区', 'A18', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'A区-A18', 10),
-- B区15台
('B区', 'B1', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B1', 10),
('B区', 'B2', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B2', 10),
('B区', 'B3', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B3', 10),
('B区', 'B4', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B4', 10),
('B区', 'B5', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B5', 10),
('B区', 'B6', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B6', 10),
('B区', 'B7', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B7', 10),
('B区', 'B8', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B8', 10),
('B区', 'B9', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B9', 10),
('B区', 'B10', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B10', 10),
('B区', 'B11', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B11', 10),
('B区', 'B12', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B12', 10),
('B区', 'B13', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B13', 10),
('B区', 'B14', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B14', 10),
('B区', 'B15', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'B区-B15', 10),
-- C区6台
('C区', 'C1', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C1', 10),
('C区', 'C2', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C2', 10),
('C区', 'C3', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C3', 10),
('C区', 'C4', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C4', 10),
('C区', 'C5', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C5', 10),
('C区', 'C6', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'C区-C6', 10),
-- VIP包厢 BILLIARD3台
('VIP包厢', 'VIP1', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'VIP包厢-VIP1', 10),
('VIP包厢', 'VIP2', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'VIP包厢-VIP2', 10),
('VIP包厢', 'VIP3', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'VIP包厢-VIP3', 10),
-- TV台1台
('TV台', 'TV', 'BILLIARD', '🎱 中式/追分', '🎱 中式/追分', '🎱', 'EXACT', 10, TRUE, 'TV台-TV', 10),
-- ============ SNOOKER 斯诺克 ============
('VIP包厢', 'VIP5', 'SNOOKER', '斯诺克', '斯诺克', '', 'EXACT', 10, TRUE, 'VIP包厢-VIP5→斯诺克', 20),
('斯诺克区', 'S1', 'SNOOKER', '斯诺克', '斯诺克', '', 'EXACT', 10, TRUE, '斯诺克区-S1', 20),
('斯诺克区', 'S2', 'SNOOKER', '斯诺克', '斯诺克', '', 'EXACT', 10, TRUE, '斯诺克区-S2', 20),
('斯诺克区', 'S3', 'SNOOKER', '斯诺克', '斯诺克', '', 'EXACT', 10, TRUE, '斯诺克区-S3', 20),
('斯诺克区', 'S4', 'SNOOKER', '斯诺克', '斯诺克', '', 'EXACT', 10, TRUE, '斯诺克区-S4', 20),
-- ============ MAHJONG 🀄 麻将/棋牌 ============
('666', '董事办', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '666-董事办', 30),
('666', '666', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '666-666', 30),
('麻将房', '1', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '麻将房-1', 30),
('麻将房', 'M2', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '麻将房-M2', 30),
('麻将房', 'M3', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '麻将房-M3', 30),
('麻将房', 'M4', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '麻将房-M4', 30),
('麻将房', 'M5', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '麻将房-M5', 30),
('M7', 'M7', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, 'M7-M7', 30),
('M7', '大包麻将房', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, 'M7-大包麻将房', 30),
('M8', 'M8', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, 'M8-M8', 30),
('发财', '发财', 'MAHJONG', '🀄 麻将/棋牌', '🀄 麻将/棋牌', '🀄', 'EXACT', 10, TRUE, '发财-发财', 30),
-- ============ KTV 🎤 团建/K歌 ============
('K包', '常乐', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'K包-常乐', 40),
('K包', '幸会(纯k)', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'K包-幸会(纯k)', 40),
('K包', '虚拟188', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'K包-虚拟188', 40),
('K包', '888', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'K包-888', 40),
('k包活动区', '大包', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'k包活动区-大包', 40),
('k包活动区', '小包', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, 'k包活动区-小包', 40),
('幸会158', '纯k', 'KTV', '🎤 团建/K歌', '🎤 团建/K歌', '🎤', 'EXACT', 10, TRUE, '幸会158-纯k', 40),
-- ============ SPECIAL 补时长/虚拟台 ============
('补时长', '补时长', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长', 900),
('补时长', '补时长2', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长2', 900),
('补时长', '补时长3', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长3', 900),
('补时长', '补时长4', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长4', 900),
('补时长', '补时长5', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长5', 900),
('补时长', '补时长6', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长6', 900),
('补时长', '补时长7', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '补时长-补时长7', 900),
('虚拟台', '虚拟台1号', 'SPECIAL', '补时长', '补时长', '', 'EXACT', 10, TRUE, '虚拟台-虚拟台1号', 900),
-- ============ OTHER 兜底 ============
('DEFAULT', NULL, 'OTHER', '其他', '其他', '', 'DEFAULT', 999, TRUE, '兜底规则:无法匹配的归入其他', 999);
-- =============================================================================
-- 5. cfg_skill_type - 技能→课程类型映射
-- 说明:
-- - 将 skill_id 映射到课程类型
-- - 基础课/陪打: skill_id = 2791903611396869
-- - 附加课/超休: skill_id = 2807440316432197
-- - 避免依赖 skill_name 文本匹配
-- =============================================================================
TRUNCATE TABLE dws.cfg_skill_type RESTART IDENTITY CASCADE;
INSERT INTO dws.cfg_skill_type (
skill_id, skill_name,
course_type_code, course_type_name,
is_active, description
) VALUES
(2791903611396869, '台球基础陪打',
'BASE', '基础课',
TRUE, '基础课:陪打服务,按助教等级计价'),
(2807440316432197, '台球超休服务',
'BONUS', '附加课',
TRUE, '附加课:超休/激励课固定190元/小时'),
(2807440316432198, '包厢服务',
'BASE', '基础课',
TRUE, '包厢服务归入基础课统计统一按138元/小时计价');
-- =============================================================================
-- 6~8. 优惠类型/支出类型/平台类型 — 作为代码常量使用,不单独建表
-- =============================================================================
-- 优惠类型: GROUPBUY/VIP/GIFT_CARD/MANUAL/ROUNDING/BIG_CUSTOMER/OTHER
-- 支出类型: RENT/UTILITY/PROPERTY/SALARY/REIMBURSE/PLATFORM_FEE/OTHER
-- 平台类型: MEITUAN/DOUYIN/DIANPING/OTHER
-- =============================================================================
-- 验证数据插入
-- =============================================================================
DO $
DECLARE
v_tier_count INTEGER;
v_price_count INTEGER;
v_bonus_count INTEGER;
v_area_count INTEGER;
v_skill_count INTEGER;
BEGIN
SELECT COUNT(*) INTO v_tier_count FROM dws.cfg_performance_tier;
SELECT COUNT(*) INTO v_price_count FROM dws.cfg_assistant_level_price;
SELECT COUNT(*) INTO v_bonus_count FROM dws.cfg_bonus_rules;
SELECT COUNT(*) INTO v_area_count FROM dws.cfg_area_category;
SELECT COUNT(*) INTO v_skill_count FROM dws.cfg_skill_type;
RAISE NOTICE '配置数据初始化完成:';
RAISE NOTICE ' - cfg_performance_tier: % 条', v_tier_count;
RAISE NOTICE ' - cfg_assistant_level_price: % 条', v_price_count;
RAISE NOTICE ' - cfg_bonus_rules: % 条', v_bonus_count;
RAISE NOTICE ' - cfg_area_category: % 条', v_area_count;
RAISE NOTICE ' - cfg_skill_type: % 条', v_skill_count;
END;
$;
-- =============================================================================
-- 种子数据指数算法参数NCI/WBI/RS/OS/MS/ML/SPI
-- =============================================================================
-- =============================================================================
-- 指数算法参数初始化脚本
-- 版本: v3.0
-- 创建日期: 2026-02-13
-- 描述: 仅保留 RS / OS / MS / ML / NCI / WBI 指数参数(已移除 RECALL / INTIMACY
-- AI_CHANGELOG [2026-02-13] 移除 RECALL/INTIMACY 参数及 ML 废弃参数source_mode/recharge_attribute_hours
-- =============================================================================
-- 清理旧版指数参数
DELETE FROM dws.cfg_index_parameters WHERE index_type IN ('RECALL', 'INTIMACY');
-- 清理 ML 已废弃参数
DELETE FROM dws.cfg_index_parameters WHERE index_type = 'ML' AND param_name IN ('source_mode', 'recharge_attribute_hours');
INSERT INTO dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
('NCI', 'active_new_penalty', 0.200000, 'active-new suppression multiplier', DATE '2026-02-06'),
('NCI', 'active_new_recency_days', 7.000000, 'active-new recency window (days)', DATE '2026-02-06'),
('NCI', 'active_new_visit_threshold_14d', 2.000000, 'active-new threshold in 14d visits', DATE '2026-02-06'),
('NCI', 'amount_base_M0', 300.000000, 'spend log base M0', DATE '2026-02-06'),
('NCI', 'balance_base_B0', 500.000000, 'balance log base B0', DATE '2026-02-06'),
('NCI', 'compression_mode', 0.000000, 'compression mode', DATE '2026-02-06'),
('NCI', 'enable_stop_high_balance_exception', 0.000000, 'enable high-balance STOP exception', DATE '2026-02-06'),
('NCI', 'ewma_alpha', 0.200000, 'EWMA alpha', DATE '2026-02-06'),
('NCI', 'h_recharge', 7.000000, 'recharge decay half-life (days)', DATE '2026-02-06'),
('NCI', 'high_balance_threshold', 1000.000000, 'high-balance threshold', DATE '2026-02-06'),
('NCI', 'lookback_days_recency', 60.000000, 'recency lookback window (days)', DATE '2026-02-06'),
('NCI', 'new_days_threshold', 30.000000, 'new member days threshold', DATE '2026-02-06'),
('NCI', 'new_recharge_max_visits', 10.000000, 'max visits for new-recharge grouping', DATE '2026-02-06'),
('NCI', 'new_visit_threshold', 2.000000, 'new member visit threshold', DATE '2026-02-06'),
('NCI', 'no_touch_days_new', 3.000000, 'no-touch threshold (days)', DATE '2026-02-06'),
('NCI', 'percentile_lower', 5.000000, 'lower percentile', DATE '2026-02-06'),
('NCI', 'percentile_upper', 95.000000, 'upper percentile', DATE '2026-02-06'),
('NCI', 'recharge_recent_days', 14.000000, 'recent recharge window (days)', DATE '2026-02-06'),
('NCI', 'salvage_end', 60.000000, 'salvage decay end day', DATE '2026-02-06'),
('NCI', 'salvage_start', 30.000000, 'salvage decay start day', DATE '2026-02-06'),
('NCI', 't2_target_days', 7.000000, 'second-visit target window (days)', DATE '2026-02-06'),
('NCI', 'use_smoothing', 1.000000, 'enable smoothing', DATE '2026-02-06'),
('NCI', 'value_w_bal', 0.800000, 'value weight for balance', DATE '2026-02-06'),
('NCI', 'value_w_spend', 1.000000, 'value weight for spend', DATE '2026-02-06'),
('NCI', 'visit_lookback_days', 180.000000, 'visit history lookback (days)', DATE '2026-02-06'),
('NCI', 'w_need', 1.600000, 'need weight', DATE '2026-02-06'),
('NCI', 'w_re', 0.800000, 'recharge pressure weight', DATE '2026-02-06'),
('NCI', 'w_value', 1.000000, 'value weight', DATE '2026-02-06'),
('NCI', 'w_welcome', 1.000000, 'welcome-stage weight', DATE '2026-02-06'),
('NCI', 'welcome_window_days', 3.000000, 'welcome outreach window for first touch (days)', DATE '2026-02-06'),
('WBI', 'amount_base_M0', 300.000000, 'spend log base M0', DATE '2026-02-06'),
('WBI', 'balance_base_B0', 500.000000, 'balance log base B0', DATE '2026-02-06'),
('WBI', 'compression_mode', 0.000000, 'compression mode', DATE '2026-02-06'),
('WBI', 'enable_stop_high_balance_exception', 0.000000, 'enable high-balance STOP exception', DATE '2026-02-06'),
('WBI', 'ewma_alpha', 0.200000, 'EWMA alpha', DATE '2026-02-06'),
('WBI', 'h_recharge', 7.000000, 'recharge decay half-life (days)', DATE '2026-02-06'),
('WBI', 'high_balance_threshold', 1000.000000, 'high-balance threshold', DATE '2026-02-06'),
('WBI', 'lookback_days_recency', 60.000000, 'recency lookback window (days)', DATE '2026-02-06'),
('WBI', 'new_days_threshold', 30.000000, 'new member days threshold', DATE '2026-02-06'),
('WBI', 'new_recharge_max_visits', 10.000000, 'max visits for new-recharge grouping', DATE '2026-02-06'),
('WBI', 'new_visit_threshold', 2.000000, 'new member visit threshold', DATE '2026-02-06'),
('WBI', 'overdue_alpha', 2.000000, 'overdue fallback alpha', DATE '2026-02-06'),
('WBI', 'overdue_weight_blend_min_samples', 8.000000, 'minimum samples to fully trust weighted overdue CDF', DATE '2026-02-07'),
('WBI', 'overdue_weight_halflife_days', 30.000000, 'overdue weighted-CDF interval half-life (days)', DATE '2026-02-07'),
('WBI', 'percentile_lower', 5.000000, 'lower percentile', DATE '2026-02-06'),
('WBI', 'percentile_upper', 95.000000, 'upper percentile', DATE '2026-02-06'),
('WBI', 'recency_gate_days', 14.000000, 'recency suppression gate center (days)', DATE '2026-02-06'),
('WBI', 'recency_gate_slope_days', 3.000000, 'recency suppression slope (days)', DATE '2026-02-06'),
('WBI', 'recency_hard_floor_days', 14.000000, 'hard floor for winback recency (days)', DATE '2026-02-06'),
('WBI', 'recharge_recent_days', 14.000000, 'recent recharge window (days)', DATE '2026-02-06'),
('WBI', 'use_smoothing', 1.000000, 'enable smoothing', DATE '2026-02-06'),
('WBI', 'value_w_bal', 1.000000, 'value weight for balance', DATE '2026-02-06'),
('WBI', 'value_w_spend', 1.000000, 'value weight for spend', DATE '2026-02-06'),
('WBI', 'visit_lookback_days', 180.000000, 'visit history lookback (days)', DATE '2026-02-06'),
('WBI', 'w_drop', 1.000000, 'drop weight', DATE '2026-02-06'),
('WBI', 'w_over', 2.000000, 'overdue weight', DATE '2026-02-06'),
('WBI', 'w_re', 0.400000, 'recharge pressure weight', DATE '2026-02-06'),
('WBI', 'w_value', 1.200000, 'value weight', DATE '2026-02-06')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 关系指数RS/OS/MS/ML参数
-- 生效时间:北京时间 2026-01-01按数据库日期管理
-- =============================================================================
INSERT INTO dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
-- RS关系强度
('RS', 'lookback_days', 60.000000, '服务行为回溯窗口(天)', DATE '2026-01-01'),
('RS', 'session_merge_hours', 4.000000, '会话合并阈值(小时)', DATE '2026-01-01'),
('RS', 'incentive_weight', 1.500000, '激励课权重', DATE '2026-01-01'),
('RS', 'halflife_session', 14.000000, '会话半衰期(天)', DATE '2026-01-01'),
('RS', 'halflife_last', 10.000000, '最近一次服务半衰期(天)', DATE '2026-01-01'),
('RS', 'weight_f', 1.000000, '频次项权重', DATE '2026-01-01'),
('RS', 'weight_d', 0.700000, '时长项权重', DATE '2026-01-01'),
('RS', 'gate_alpha', 0.600000, '最近服务门控指数', DATE '2026-01-01'),
('RS', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('RS', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('RS', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('RS', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('RS', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01'),
-- OS归属份额
('OS', 'min_rs_raw_for_ownership', 0.050000, '参与归属计算的最小RS_raw', DATE '2026-01-01'),
('OS', 'min_total_rs_raw', 0.100000, '形成稳定归属的最小sum_rs', DATE '2026-01-01'),
('OS', 'ownership_main_threshold', 0.600000, '主责阈值', DATE '2026-01-01'),
('OS', 'ownership_comanage_threshold', 0.350000, '共管阈值', DATE '2026-01-01'),
('OS', 'ownership_gap_threshold', 0.150000, '主责与次席份额差阈值', DATE '2026-01-01'),
('OS', 'eps', 0.000001, '数值稳定项', DATE '2026-01-01'),
-- MS升温动量
('MS', 'lookback_days', 60.000000, '服务行为回溯窗口(天)', DATE '2026-01-01'),
('MS', 'session_merge_hours', 4.000000, '会话合并阈值(小时)', DATE '2026-01-01'),
('MS', 'incentive_weight', 1.500000, '激励课权重', DATE '2026-01-01'),
('MS', 'halflife_short', 7.000000, '短期半衰期(天)', DATE '2026-01-01'),
('MS', 'halflife_long', 30.000000, '长期半衰期(天)', DATE '2026-01-01'),
('MS', 'eps', 0.000001, '数值稳定项', DATE '2026-01-01'),
('MS', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('MS', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('MS', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('MS', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('MS', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01'),
-- ML付费关联
('ML', 'lookback_days', 60.000000, '充值行为回溯窗口(天)', DATE '2026-01-01'),
('ML', 'amount_base', 500.000000, '金额压缩基准', DATE '2026-01-01'),
('ML', 'halflife_recharge', 21.000000, '充值半衰期(天)', DATE '2026-01-01'),
('ML', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('ML', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('ML', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('ML', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('ML', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- SPI消费力指数参数
-- 生效时间:北京时间 2026-02-23
-- =============================================================================
INSERT INTO dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
-- 窗口参数
('SPI', 'spend_window_short_days', 30.000000, '短期消费窗口(天)', DATE '2026-02-23'),
('SPI', 'spend_window_long_days', 90.000000, '长期消费窗口(天)', DATE '2026-02-23'),
('SPI', 'ewma_alpha_daily_spend', 0.300000, '日消费 EWMA 平滑系数', DATE '2026-02-23'),
-- 金额压缩基数(基于典型台球门店消费水平的初始默认值)
('SPI', 'amount_base_spend_30', 500.000000, '30天消费额压缩基数', DATE '2026-02-23'),
('SPI', 'amount_base_spend_90', 1500.000000, '90天消费额压缩基数', DATE '2026-02-23'),
('SPI', 'amount_base_ticket_90', 200.000000, '90天客单价压缩基数', DATE '2026-02-23'),
('SPI', 'amount_base_recharge_90', 1000.000000, '90天充值额压缩基数', DATE '2026-02-23'),
('SPI', 'amount_base_speed_abs', 100.000000, '绝对速度压缩基数', DATE '2026-02-23'),
('SPI', 'amount_base_ewma_90', 50.000000, '日消费EWMA压缩基数', DATE '2026-02-23'),
-- Level 子分权重
('SPI', 'w_level_spend_30', 0.300000, 'Level子分30天消费权重', DATE '2026-02-23'),
('SPI', 'w_level_spend_90', 0.350000, 'Level子分90天消费权重', DATE '2026-02-23'),
('SPI', 'w_level_ticket_90', 0.200000, 'Level子分90天客单权重', DATE '2026-02-23'),
('SPI', 'w_level_recharge_90', 0.150000, 'Level子分90天充值权重', DATE '2026-02-23'),
-- Speed 子分权重
('SPI', 'w_speed_abs', 0.500000, 'Speed子分绝对速度权重', DATE '2026-02-23'),
('SPI', 'w_speed_rel', 0.300000, 'Speed子分相对速度权重', DATE '2026-02-23'),
('SPI', 'w_speed_ewma', 0.200000, 'Speed子分EWMA速度权重', DATE '2026-02-23'),
-- 总分权重
('SPI', 'weight_level', 0.600000, 'SPI总分Level子分权重', DATE '2026-02-23'),
('SPI', 'weight_speed', 0.300000, 'SPI总分Speed子分权重', DATE '2026-02-23'),
('SPI', 'weight_stability', 0.100000, 'SPI总分Stability子分权重', DATE '2026-02-23'),
-- 稳定性参数
('SPI', 'stability_window_days', 90.000000, '稳定性计算窗口(天)', DATE '2026-02-23'),
('SPI', 'use_stability', 1.000000, '是否启用稳定性子分0=关闭,1=启用', DATE '2026-02-23'),
-- 映射与平滑
('SPI', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-02-23'),
('SPI', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-02-23'),
('SPI', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-02-23'),
('SPI', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-02-23'),
('SPI', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-02-23'),
-- 速度计算
('SPI', 'speed_epsilon', 0.000001, '速度计算防除零小量', DATE '2026-02-23')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 验证
-- =============================================================================
DO $
DECLARE
rs_count INTEGER;
os_count INTEGER;
ms_count INTEGER;
ml_count INTEGER;
nci_count INTEGER;
wbi_count INTEGER;
spi_count INTEGER;
BEGIN
SELECT COUNT(*) INTO rs_count
FROM dws.cfg_index_parameters
WHERE index_type = 'RS';
SELECT COUNT(*) INTO os_count
FROM dws.cfg_index_parameters
WHERE index_type = 'OS';
SELECT COUNT(*) INTO ms_count
FROM dws.cfg_index_parameters
WHERE index_type = 'MS';
SELECT COUNT(*) INTO ml_count
FROM dws.cfg_index_parameters
WHERE index_type = 'ML';
SELECT COUNT(*) INTO nci_count
FROM dws.cfg_index_parameters
WHERE index_type = 'NCI';
SELECT COUNT(*) INTO wbi_count
FROM dws.cfg_index_parameters
WHERE index_type = 'WBI';
SELECT COUNT(*) INTO spi_count
FROM dws.cfg_index_parameters
WHERE index_type = 'SPI';
RAISE NOTICE 'RS 参数数量: %', rs_count;
RAISE NOTICE 'OS 参数数量: %', os_count;
RAISE NOTICE 'MS 参数数量: %', ms_count;
RAISE NOTICE 'ML 参数数量: %', ml_count;
RAISE NOTICE '新客转化参数数量: %', nci_count;
RAISE NOTICE '唤回指数参数数量: %', wbi_count;
RAISE NOTICE 'SPI 消费力指数参数数量: %', spi_count;
END $;
SELECT
index_type,
param_name,
param_value,
description,
effective_from
FROM dws.cfg_index_parameters
ORDER BY index_type, param_name, effective_from;

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / metaETL 调度元数据)
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -75,105 +75,3 @@ ALTER TABLE meta.etl_run ADD CONSTRAINT etl_run_pkey PRIMARY KEY (run_id);
ALTER TABLE meta.etl_task ADD CONSTRAINT etl_task_pkey PRIMARY KEY (task_id);
ALTER TABLE meta.etl_task ADD CONSTRAINT etl_task_task_code_store_id_key UNIQUE (task_code, store_id);
-- =============================================================================
-- 种子数据ODS 任务注册
-- =============================================================================
WITH target_store AS (
SELECT 2790685415443269::bigint AS store_id -- TODO: 替换为实际 store_id
),
task_codes AS (
SELECT unnest(ARRAY[
'ODS_ASSISTANT_ACCOUNT',
'ODS_ASSISTANT_LEDGER',
'ODS_SETTLEMENT_RECORDS',
'ODS_TABLE_USE',
'ODS_PAYMENT',
'ODS_REFUND',
'ODS_PLATFORM_COUPON',
'ODS_MEMBER',
'ODS_MEMBER_CARD',
'ODS_MEMBER_BALANCE',
'ODS_RECHARGE_SETTLE',
'ODS_GROUP_PACKAGE',
'ODS_GROUP_BUY_REDEMPTION',
'ODS_INVENTORY_STOCK',
'ODS_INVENTORY_CHANGE',
'ODS_TABLES',
'ODS_GOODS_CATEGORY',
'ODS_STORE_GOODS',
'ODS_STORE_GOODS_SALES',
'ODS_TABLE_FEE_DISCOUNT',
'ODS_TENANT_GOODS'
]) AS task_code
)
INSERT INTO meta.etl_task (task_code, store_id, enabled)
SELECT t.task_code, s.store_id, TRUE
FROM task_codes t CROSS JOIN target_store s
ON CONFLICT (task_code, store_id) DO UPDATE
SET enabled = EXCLUDED.enabled;
-- =============================================================================
-- 种子数据:调度任务注册
-- =============================================================================
WITH target_store AS (
SELECT 2790685415443269::bigint AS store_id -- TODO: 替换为实际 store_id
),
task_codes AS (
SELECT unnest(ARRAY[
'ASSISTANTS',
'COUPON_USAGE',
'CHECK_CUTOFF',
'DWD_LOAD_FROM_ODS',
'DWD_QUALITY_CHECK',
'INIT_DWD_SCHEMA',
'INIT_DWS_SCHEMA',
'INIT_ODS_SCHEMA',
'INVENTORY_CHANGE',
'LEDGER',
'MANUAL_INGEST',
'MEMBERS',
'MEMBERS_DWD',
'ODS_JSON_ARCHIVE',
'ORDERS',
'PACKAGES_DEF',
'PAYMENTS',
'PAYMENTS_DWD',
'PRODUCTS',
'REFUNDS',
'TABLE_DISCOUNT',
'TABLES',
'TICKET_DWD',
'TOPUPS',
'DWS_BUILD_ORDER_SUMMARY',
'DWS_ASSISTANT_DAILY',
'DWS_ASSISTANT_MONTHLY',
'DWS_ASSISTANT_CUSTOMER',
'DWS_ASSISTANT_SALARY',
'DWS_ASSISTANT_FINANCE',
'DWS_MEMBER_CONSUMPTION',
'DWS_MEMBER_VISIT',
'DWS_FINANCE_DAILY',
'DWS_FINANCE_RECHARGE',
'DWS_FINANCE_INCOME_STRUCTURE',
'DWS_FINANCE_DISCOUNT_DETAIL',
'DWS_GOODS_STOCK_DAILY',
'DWS_GOODS_STOCK_WEEKLY',
'DWS_GOODS_STOCK_MONTHLY',
'DWS_WINBACK_INDEX',
'DWS_NEWCONV_INDEX',
'DWS_RELATION_INDEX',
'DWS_ASSISTANT_PROJECT_TAG',
'DWS_MEMBER_PROJECT_TAG',
'DWS_ML_MANUAL_IMPORT'
]) AS task_code
)
INSERT INTO meta.etl_task (task_code, store_id, enabled)
SELECT t.task_code, s.store_id, TRUE
FROM task_codes t CROSS JOIN target_store s
ON CONFLICT (task_code, store_id) DO UPDATE
SET enabled = EXCLUDED.enabled,
updated_at = now();

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- etl_feiqiu / ods原始数据层
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -148,7 +148,9 @@ CREATE TABLE ods.assistant_service_records (
content_hash text NOT NULL,
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now()
fetched_at timestamp with time zone DEFAULT now(),
deduct_leave_seconds integer DEFAULT 0,
order_from integer
);
CREATE TABLE ods.goods_stock_movements (
@@ -198,7 +200,8 @@ CREATE TABLE ods.goods_stock_summary (
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now(),
payload jsonb NOT NULL,
siteid bigint
siteid bigint,
createtime timestamp without time zone
);
CREATE TABLE ods.group_buy_package_details (
@@ -394,7 +397,11 @@ CREATE TABLE ods.member_profiles (
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now(),
payload jsonb NOT NULL,
birthday date
birthday date,
other_pay_money_sum numeric(18,2),
last_consume_time timestamp without time zone,
non_consume_day_num integer,
first_consumption integer
);
CREATE TABLE ods.member_stored_value_cards (
@@ -719,7 +726,8 @@ CREATE TABLE ods.settlement_records (
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now(),
payload jsonb NOT NULL
payload jsonb NOT NULL,
orderfrom integer
);
CREATE TABLE ods.site_tables_master (
@@ -939,7 +947,10 @@ CREATE TABLE ods.store_goods_sales_records (
content_hash text NOT NULL,
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now()
fetched_at timestamp with time zone DEFAULT now(),
activity_amount numeric(18,2) DEFAULT 0,
activity_id bigint DEFAULT 0,
order_from integer
);
CREATE TABLE ods.table_fee_discount_records (
@@ -1025,7 +1036,8 @@ CREATE TABLE ods.table_fee_transactions (
content_hash text NOT NULL,
source_file text,
source_endpoint text,
fetched_at timestamp with time zone DEFAULT now()
fetched_at timestamp with time zone DEFAULT now(),
order_from integer
);
CREATE TABLE ods.tenant_goods_master (

View File

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

View File

@@ -0,0 +1,86 @@
-- =============================================================================
-- FDW 反向映射配置(生产环境)— 在 etl_feiqiu 数据库中执行
-- 用途:通过 postgres_fdw 将 zqyy_app.member_retention_clue 只读映射到 etl_feiqiu
-- 使 ETL DWS 任务无需直接连接业务库即可读取维客线索数据。
-- 方向etl_feiqiu → zqyy_app与 setup_fdw.sql 的 zqyy_app → etl_feiqiu 方向相反)
-- 前提zqyy_app 数据库已部署 member_retention_clue 表
-- 测试环境版本setup_fdw_reverse_test.sql指向 test_zqyy_app
-- CHANGE 2026-02-26 | member_birthday_manual → member_retention_clue维客线索重构
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 1. 安装 postgres_fdw 扩展(如已安装则跳过)
-- -----------------------------------------------------------------------------
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- -----------------------------------------------------------------------------
-- 2. 创建外部服务器(指向 zqyy_app 业务库)
-- 部署时按实际环境替换 host / port
-- -----------------------------------------------------------------------------
CREATE SERVER IF NOT EXISTS zqyy_app_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'zqyy_app', port '5432');
-- -----------------------------------------------------------------------------
-- 3. 创建用户映射(只读角色)
-- -----------------------------------------------------------------------------
CREATE USER MAPPING IF NOT EXISTS FOR etl_user
SERVER zqyy_app_server
OPTIONS (user 'app_reader', password '***');
-- -----------------------------------------------------------------------------
-- 4. 创建目标 schema存放来自业务库的外部表
-- -----------------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS fdw_app;
-- -----------------------------------------------------------------------------
-- 5. 创建外部表member_retention_clue
-- 映射 zqyy_app.public.member_retention_clueETL 侧只读
-- -----------------------------------------------------------------------------
CREATE FOREIGN TABLE IF NOT EXISTS fdw_app.member_retention_clue (
id BIGINT,
member_id BIGINT,
category VARCHAR(20),
summary VARCHAR(200),
detail TEXT,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ,
site_id BIGINT
) SERVER zqyy_app_server
OPTIONS (schema_name 'public', table_name 'member_retention_clue');
-- -----------------------------------------------------------------------------
-- 6. 授权:允许 etl_user 访问 fdw_app schema 及其外部表
-- -----------------------------------------------------------------------------
GRANT USAGE ON SCHEMA fdw_app TO etl_user;
GRANT SELECT ON ALL TABLES IN SCHEMA fdw_app TO etl_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app GRANT SELECT ON TABLES TO etl_user;
-- =============================================================================
-- 回滚脚本(按逆序执行)
-- =============================================================================
-- ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user;
-- REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user;
-- REVOKE USAGE ON SCHEMA fdw_app FROM etl_user;
-- DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
-- DROP SCHEMA IF EXISTS fdw_app CASCADE;
-- DROP USER MAPPING IF EXISTS FOR etl_user SERVER zqyy_app_server;
-- DROP SERVER IF EXISTS zqyy_app_server CASCADE;
-- =============================================================================
-- 验证 SQL
-- =============================================================================
-- 1. 确认外部服务器存在
-- SELECT srvname, srvoptions FROM pg_foreign_server
-- WHERE srvname = 'zqyy_app_server';
-- 2. 确认外部表列结构完整9 列)
-- SELECT column_name, data_type FROM information_schema.columns
-- WHERE table_schema = 'fdw_app' AND table_name = 'member_retention_clue'
-- ORDER BY ordinal_position;
-- 3. 确认外部表可读取
-- SELECT COUNT(*) FROM fdw_app.member_retention_clue;

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / auth用户认证与权限
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -10,12 +10,22 @@ CREATE SCHEMA IF NOT EXISTS auth;
CREATE SEQUENCE IF NOT EXISTS auth.permissions_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.roles_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.site_code_mapping_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.tenant_admins_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS auth.user_applications_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.user_assistant_binding_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.user_site_roles_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS auth.users_id_seq AS integer;
-- 表
CREATE TABLE auth._archived_site_code_mapping (
id integer DEFAULT nextval('auth.site_code_mapping_id_seq'::regclass) NOT NULL,
site_code character varying(10) NOT NULL,
site_id bigint NOT NULL,
site_name character varying(200),
tenant_id bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.permissions (
id integer DEFAULT nextval('auth.permissions_id_seq'::regclass) NOT NULL,
code character varying(100) NOT NULL,
@@ -37,13 +47,19 @@ CREATE TABLE auth.roles (
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE auth.site_code_mapping (
id integer DEFAULT nextval('auth.site_code_mapping_id_seq'::regclass) NOT NULL,
site_code character varying(10) NOT NULL,
site_id bigint NOT NULL,
site_name character varying(200),
tenant_id bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL
CREATE TABLE auth.tenant_admins (
id bigint DEFAULT nextval('auth.tenant_admins_id_seq'::regclass) NOT NULL,
username character varying(50) NOT NULL,
password_hash character varying(255) NOT NULL,
display_name character varying(100),
tenant_id bigint NOT NULL,
managed_site_ids _int8 NOT NULL,
is_active boolean DEFAULT true,
created_by bigint,
created_at timestamp with time zone DEFAULT now(),
last_login_at timestamp with time zone,
deleted_at timestamp with time zone,
admin_type character varying(20) DEFAULT 'tenant_admin'::character varying NOT NULL
);
CREATE TABLE auth.user_applications (
@@ -68,7 +84,9 @@ CREATE TABLE auth.user_assistant_binding (
assistant_id bigint,
staff_id bigint,
binding_type character varying(20) NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
created_at timestamp with time zone DEFAULT now() NOT NULL,
is_removed boolean DEFAULT false NOT NULL,
removed_at timestamp with time zone
);
CREATE TABLE auth.user_site_roles (
@@ -76,7 +94,9 @@ CREATE TABLE auth.user_site_roles (
user_id integer NOT NULL,
site_id bigint NOT NULL,
role_id integer NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
created_at timestamp with time zone DEFAULT now() NOT NULL,
is_removed boolean DEFAULT false NOT NULL,
removed_at timestamp with time zone
);
CREATE TABLE auth.users (
@@ -88,10 +108,17 @@ CREATE TABLE auth.users (
phone character varying(20),
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
updated_at timestamp with time zone DEFAULT now() NOT NULL,
rejection_count integer DEFAULT 0 NOT NULL,
avatar_url character varying(500)
);
-- 约束(主键 / 唯一 / 外键)
ALTER TABLE auth._archived_site_code_mapping ADD CONSTRAINT site_code_mapping_pkey PRIMARY KEY (id);
ALTER TABLE auth._archived_site_code_mapping ADD CONSTRAINT site_code_mapping_site_code_key UNIQUE (site_code);
ALTER TABLE auth._archived_site_code_mapping ADD CONSTRAINT site_code_mapping_site_id_key UNIQUE (site_id);
ALTER TABLE auth._archived_site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_code UNIQUE (site_code);
ALTER TABLE auth._archived_site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_id UNIQUE (site_id);
ALTER TABLE auth.permissions ADD CONSTRAINT permissions_pkey PRIMARY KEY (id);
ALTER TABLE auth.permissions ADD CONSTRAINT permissions_code_key UNIQUE (code);
ALTER TABLE auth.permissions ADD CONSTRAINT uq_permissions_code UNIQUE (code);
@@ -103,11 +130,8 @@ ALTER TABLE auth.role_permissions ADD CONSTRAINT role_permissions_pkey PRIMARY K
ALTER TABLE auth.roles ADD CONSTRAINT roles_pkey PRIMARY KEY (id);
ALTER TABLE auth.roles ADD CONSTRAINT roles_code_key UNIQUE (code);
ALTER TABLE auth.roles ADD CONSTRAINT uq_roles_code UNIQUE (code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_pkey PRIMARY KEY (id);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_site_code_key UNIQUE (site_code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT site_code_mapping_site_id_key UNIQUE (site_id);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_code UNIQUE (site_code);
ALTER TABLE auth.site_code_mapping ADD CONSTRAINT uq_site_code_mapping_site_id UNIQUE (site_id);
ALTER TABLE auth.tenant_admins ADD CONSTRAINT tenant_admins_pkey PRIMARY KEY (id);
ALTER TABLE auth.tenant_admins ADD CONSTRAINT tenant_admins_username_key UNIQUE (username);
ALTER TABLE auth.user_applications ADD CONSTRAINT fk_user_applications_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_applications ADD CONSTRAINT user_applications_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE auth.user_applications ADD CONSTRAINT user_applications_pkey PRIMARY KEY (id);
@@ -126,60 +150,14 @@ ALTER TABLE auth.users ADD CONSTRAINT uq_users_wx_openid UNIQUE (wx_openid);
ALTER TABLE auth.users ADD CONSTRAINT users_wx_openid_key UNIQUE (wx_openid);
-- 索引
CREATE INDEX ix_site_code_mapping_site_code ON auth.site_code_mapping USING btree (site_code);
CREATE INDEX ix_site_code_mapping_site_code ON auth._archived_site_code_mapping USING btree (site_code);
CREATE INDEX idx_tenant_admin_tenant ON auth.tenant_admins USING btree (tenant_id);
CREATE INDEX idx_tenant_admins_active_not_deleted ON auth.tenant_admins USING btree (is_active) WHERE (deleted_at IS NULL);
CREATE INDEX ix_user_applications_status ON auth.user_applications USING btree (status);
CREATE INDEX ix_user_applications_user_id ON auth.user_applications USING btree (user_id);
CREATE INDEX idx_user_assistant_binding_active ON auth.user_assistant_binding USING btree (user_id, site_id) WHERE (is_removed = false);
CREATE INDEX idx_user_site_roles_active ON auth.user_site_roles USING btree (user_id, site_id) WHERE (is_removed = false);
CREATE INDEX ix_user_site_roles_user_site ON auth.user_site_roles USING btree (user_id, site_id);
CREATE INDEX ix_users_status ON auth.users USING btree (status);
CREATE INDEX ix_users_wx_openid ON auth.users USING btree (wx_openid);
-- =============================================================================
-- 种子数据权限列表5 条)
-- =============================================================================
INSERT INTO auth.permissions (code, name, description) VALUES
('view_tasks', '查看任务', '允许查看任务列表和任务详情'),
('view_board', '查看看板', '允许查看数据看板概览'),
('view_board_finance', '查看财务看板', '允许查看财务相关的数据看板'),
('view_board_customer', '查看客户看板', '允许查看客户相关的数据看板'),
('view_board_coach', '查看助教看板', '允许查看助教相关的数据看板')
ON CONFLICT (code) DO NOTHING;
-- =============================================================================
-- 种子数据默认角色4 条)
-- =============================================================================
INSERT INTO auth.roles (code, name, description) VALUES
('coach', '助教', '球房助教,可查看任务和助教看板'),
('staff', '员工', '球房员工,可查看任务和数据看板'),
('site_admin', '店铺管理员', '单店管理员,可查看所有看板'),
('tenant_admin', '租户管理员', '租户级管理员,拥有全部权限')
ON CONFLICT (code) DO NOTHING;
-- =============================================================================
-- 种子数据:角色-权限映射14 条)
-- =============================================================================
INSERT INTO auth.role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth.roles r
CROSS JOIN auth.permissions p
WHERE (r.code, p.code) IN (
('coach', 'view_tasks'),
('coach', 'view_board_coach'),
('staff', 'view_tasks'),
('staff', 'view_board'),
('site_admin', 'view_tasks'),
('site_admin', 'view_board'),
('site_admin', 'view_board_finance'),
('site_admin', 'view_board_customer'),
('site_admin', 'view_board_coach'),
('tenant_admin', 'view_tasks'),
('tenant_admin', 'view_board'),
('tenant_admin', 'view_board_finance'),
('tenant_admin', 'view_board_customer'),
('tenant_admin', 'view_board_coach')
)
ON CONFLICT (role_id, permission_id) DO NOTHING;

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / biz核心业务表任务/备注/触发器))
-- 生成日期2026-03-20
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -10,9 +10,23 @@ CREATE SCHEMA IF NOT EXISTS biz;
CREATE SEQUENCE IF NOT EXISTS biz.ai_cache_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.ai_conversations_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.ai_messages_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.ai_run_logs_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.ai_trigger_jobs_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.cfg_task_generator_params_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.coach_task_history_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.coach_task_transfer_log_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.coach_tasks_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.connectors_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS biz.dws_assistant_task_monthly_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.excel_upload_log_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.notes_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.salary_adjustments_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.site_code_history_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS biz.sites_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS biz.stg_finance_expense_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.stg_platform_income_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.stg_recharge_commission_id_seq AS bigint;
CREATE SEQUENCE IF NOT EXISTS biz.tenants_id_seq AS integer;
CREATE SEQUENCE IF NOT EXISTS biz.trigger_jobs_id_seq AS integer;
-- 表
@@ -25,7 +39,8 @@ CREATE TABLE biz.ai_cache (
score integer,
triggered_by character varying(100),
created_at timestamp with time zone DEFAULT now() NOT NULL,
expires_at timestamp with time zone
expires_at timestamp with time zone,
status character varying(20) DEFAULT 'valid'::character varying
);
CREATE TABLE biz.ai_conversations (
@@ -36,12 +51,13 @@ CREATE TABLE biz.ai_conversations (
site_id bigint NOT NULL,
source_page character varying(100),
source_context jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
context_type character varying(20),
context_id character varying(50),
title character varying(200),
last_message text,
last_message_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now() NOT NULL
session_id character varying(100)
);
CREATE TABLE biz.ai_messages (
@@ -50,10 +66,54 @@ CREATE TABLE biz.ai_messages (
role character varying(10) NOT NULL,
content text NOT NULL,
tokens_used integer,
reference_card jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
reference_card jsonb
);
CREATE TABLE biz.ai_run_logs (
id bigint DEFAULT nextval('biz.ai_run_logs_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
app_type character varying(30) NOT NULL,
trigger_type character varying(20) NOT NULL,
member_id bigint,
request_prompt text,
response_text text,
tokens_used integer DEFAULT 0,
latency_ms integer,
status character varying(20) DEFAULT 'pending'::character varying NOT NULL,
error_message text,
session_id character varying(100),
created_at timestamp with time zone DEFAULT now() NOT NULL,
finished_at timestamp with time zone,
alert_status character varying(20) DEFAULT NULL::character varying
);
CREATE TABLE biz.ai_trigger_jobs (
id bigint DEFAULT nextval('biz.ai_trigger_jobs_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
event_type character varying(30) NOT NULL,
connector_type character varying(30) DEFAULT 'feiqiu'::character varying,
member_id bigint,
payload jsonb,
status character varying(20) DEFAULT 'pending'::character varying NOT NULL,
is_forced boolean DEFAULT false,
app_chain character varying(100),
started_at timestamp with time zone,
finished_at timestamp with time zone,
error_message text,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.cfg_task_generator_params (
id bigint DEFAULT nextval('biz.cfg_task_generator_params_id_seq'::regclass) NOT NULL,
site_id bigint,
param_key character varying(64) NOT NULL,
param_value numeric NOT NULL,
description text,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
updated_by bigint
);
CREATE TABLE biz.coach_task_history (
id bigint DEFAULT nextval('biz.coach_task_history_id_seq'::regclass) NOT NULL,
task_id bigint NOT NULL,
@@ -66,6 +126,20 @@ CREATE TABLE biz.coach_task_history (
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.coach_task_transfer_log (
id bigint DEFAULT nextval('biz.coach_task_transfer_log_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
member_id bigint NOT NULL,
from_assistant_id bigint NOT NULL,
to_assistant_id bigint NOT NULL,
from_task_id bigint NOT NULL,
to_task_id bigint,
transfer_reason text,
guard_checks jsonb,
transfer_score numeric,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.coach_tasks (
id bigint DEFAULT nextval('biz.coach_tasks_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
@@ -81,7 +155,50 @@ CREATE TABLE biz.coach_tasks (
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()
updated_at timestamp with time zone DEFAULT now(),
transfer_count integer DEFAULT 0 NOT NULL,
transferred_from bigint,
transferred_at timestamp with time zone
);
CREATE TABLE biz.connectors (
id integer DEFAULT nextval('biz.connectors_id_seq'::regclass) NOT NULL,
connector_key character varying(50) NOT NULL,
display_name character varying(100) NOT NULL,
is_active boolean DEFAULT true NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.dws_assistant_task_monthly (
id bigint DEFAULT nextval('biz.dws_assistant_task_monthly_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
assistant_id bigint NOT NULL,
stat_month date NOT NULL,
recall_created integer DEFAULT 0 NOT NULL,
follow_up_created integer DEFAULT 0 NOT NULL,
relationship_created integer DEFAULT 0 NOT NULL,
total_created integer DEFAULT 0 NOT NULL,
recall_completed integer DEFAULT 0 NOT NULL,
follow_up_completed integer DEFAULT 0 NOT NULL,
total_completed integer DEFAULT 0 NOT NULL,
abandoned_count integer DEFAULT 0 NOT NULL,
transferred_count integer DEFAULT 0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.excel_upload_log (
id bigint DEFAULT nextval('biz.excel_upload_log_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
upload_type character varying(30) NOT NULL,
file_name character varying(255) NOT NULL,
uploaded_by bigint NOT NULL,
row_count integer DEFAULT 0,
conflict_count integer DEFAULT 0,
resolved_count integer DEFAULT 0,
status character varying(20) NOT NULL,
error_detail jsonb,
created_at timestamp with time zone DEFAULT now(),
confirmed_at timestamp with time zone
);
CREATE TABLE biz.notes (
@@ -102,6 +219,89 @@ CREATE TABLE biz.notes (
score smallint
);
CREATE TABLE biz.salary_adjustments (
id bigint DEFAULT nextval('biz.salary_adjustments_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
assistant_id bigint,
assistant_name character varying(100) NOT NULL,
assistant_number character varying(50) NOT NULL,
salary_month character varying(7) NOT NULL,
adjustment_type character varying(20) NOT NULL,
amount numeric(12,2) NOT NULL,
reason character varying(200) NOT NULL,
upload_batch_id bigint,
created_at timestamp with time zone DEFAULT now(),
created_by bigint
);
CREATE TABLE biz.site_code_history (
id integer DEFAULT nextval('biz.site_code_history_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
site_code character varying(6) NOT NULL,
is_current boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
retired_at timestamp with time zone
);
CREATE TABLE biz.sites (
id integer DEFAULT nextval('biz.sites_id_seq'::regclass) NOT NULL,
tenant_id integer NOT NULL,
site_id bigint NOT NULL,
site_name character varying(200),
site_code character varying(6),
site_label character varying(50),
is_active boolean DEFAULT true NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.stg_finance_expense (
id bigint DEFAULT nextval('biz.stg_finance_expense_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
expense_month character varying(7) NOT NULL,
category character varying(50) NOT NULL,
amount numeric(12,2) NOT NULL,
remark text,
upload_batch_id bigint,
synced_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.stg_platform_income (
id bigint DEFAULT nextval('biz.stg_platform_income_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
income_month character varying(7) NOT NULL,
platform_name character varying(100) NOT NULL,
amount numeric(12,2) NOT NULL,
remark text,
upload_batch_id bigint,
synced_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.stg_recharge_commission (
id bigint DEFAULT nextval('biz.stg_recharge_commission_id_seq'::regclass) NOT NULL,
site_id bigint NOT NULL,
recharge_date date NOT NULL,
member_name character varying(100) NOT NULL,
recharge_amount numeric(12,2) NOT NULL,
assigned_assistant character varying(100) NOT NULL,
reward_amount numeric(12,2) NOT NULL,
upload_batch_id bigint,
synced_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now()
);
CREATE TABLE biz.tenants (
id integer DEFAULT nextval('biz.tenants_id_seq'::regclass) NOT NULL,
connector_id integer NOT NULL,
tenant_id bigint NOT NULL,
tenant_name character varying(200),
is_active boolean DEFAULT true NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.trigger_jobs (
id integer DEFAULT nextval('biz.trigger_jobs_id_seq'::regclass) NOT NULL,
job_type character varying(100) NOT NULL,
@@ -111,7 +311,10 @@ CREATE TABLE biz.trigger_jobs (
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()
created_at timestamp with time zone DEFAULT now(),
last_error text,
description text,
last_stats jsonb
);
-- 约束(主键 / 唯一 / 外键)
@@ -119,12 +322,42 @@ ALTER TABLE biz.ai_cache ADD CONSTRAINT ai_cache_pkey PRIMARY KEY (id);
ALTER TABLE biz.ai_conversations ADD CONSTRAINT ai_conversations_pkey PRIMARY KEY (id);
ALTER TABLE biz.ai_messages ADD CONSTRAINT ai_messages_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES biz.ai_conversations(id) ON DELETE CASCADE;
ALTER TABLE biz.ai_messages ADD CONSTRAINT ai_messages_pkey PRIMARY KEY (id);
ALTER TABLE biz.ai_run_logs ADD CONSTRAINT ai_run_logs_pkey PRIMARY KEY (id);
ALTER TABLE biz.ai_trigger_jobs ADD CONSTRAINT ai_trigger_jobs_pkey PRIMARY KEY (id);
ALTER TABLE biz.cfg_task_generator_params ADD CONSTRAINT cfg_task_generator_params_pkey PRIMARY KEY (id);
ALTER TABLE biz.cfg_task_generator_params ADD CONSTRAINT cfg_task_generator_params_site_id_param_key_key UNIQUE (site_id, param_key);
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_task_transfer_log ADD CONSTRAINT coach_task_transfer_log_from_task_id_fkey FOREIGN KEY (from_task_id) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.coach_task_transfer_log ADD CONSTRAINT coach_task_transfer_log_to_task_id_fkey FOREIGN KEY (to_task_id) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.coach_task_transfer_log ADD CONSTRAINT coach_task_transfer_log_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 fk_coach_tasks_transferred_from FOREIGN KEY (transferred_from) REFERENCES biz.coach_tasks(id);
ALTER TABLE biz.coach_tasks ADD CONSTRAINT coach_tasks_pkey PRIMARY KEY (id);
ALTER TABLE biz.connectors ADD CONSTRAINT connectors_pkey PRIMARY KEY (id);
ALTER TABLE biz.connectors ADD CONSTRAINT connectors_connector_key_key UNIQUE (connector_key);
ALTER TABLE biz.dws_assistant_task_monthly ADD CONSTRAINT dws_assistant_task_monthly_pkey PRIMARY KEY (id);
ALTER TABLE biz.dws_assistant_task_monthly ADD CONSTRAINT dws_assistant_task_monthly_site_id_assistant_id_stat_month_key UNIQUE (site_id, assistant_id, stat_month);
ALTER TABLE biz.excel_upload_log ADD CONSTRAINT excel_upload_log_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.salary_adjustments ADD CONSTRAINT salary_adjustments_upload_batch_id_fkey FOREIGN KEY (upload_batch_id) REFERENCES biz.excel_upload_log(id);
ALTER TABLE biz.salary_adjustments ADD CONSTRAINT salary_adjustments_pkey PRIMARY KEY (id);
ALTER TABLE biz.site_code_history ADD CONSTRAINT site_code_history_pkey PRIMARY KEY (id);
ALTER TABLE biz.site_code_history ADD CONSTRAINT site_code_history_site_code_key UNIQUE (site_code);
ALTER TABLE biz.sites ADD CONSTRAINT sites_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES biz.tenants(id);
ALTER TABLE biz.sites ADD CONSTRAINT sites_pkey PRIMARY KEY (id);
ALTER TABLE biz.sites ADD CONSTRAINT sites_site_code_key UNIQUE (site_code);
ALTER TABLE biz.sites ADD CONSTRAINT sites_site_id_key UNIQUE (site_id);
ALTER TABLE biz.stg_finance_expense ADD CONSTRAINT stg_finance_expense_upload_batch_id_fkey FOREIGN KEY (upload_batch_id) REFERENCES biz.excel_upload_log(id);
ALTER TABLE biz.stg_finance_expense ADD CONSTRAINT stg_finance_expense_pkey PRIMARY KEY (id);
ALTER TABLE biz.stg_platform_income ADD CONSTRAINT stg_platform_income_upload_batch_id_fkey FOREIGN KEY (upload_batch_id) REFERENCES biz.excel_upload_log(id);
ALTER TABLE biz.stg_platform_income ADD CONSTRAINT stg_platform_income_pkey PRIMARY KEY (id);
ALTER TABLE biz.stg_recharge_commission ADD CONSTRAINT stg_recharge_commission_upload_batch_id_fkey FOREIGN KEY (upload_batch_id) REFERENCES biz.excel_upload_log(id);
ALTER TABLE biz.stg_recharge_commission ADD CONSTRAINT stg_recharge_commission_pkey PRIMARY KEY (id);
ALTER TABLE biz.tenants ADD CONSTRAINT tenants_connector_id_fkey FOREIGN KEY (connector_id) REFERENCES biz.connectors(id);
ALTER TABLE biz.tenants ADD CONSTRAINT tenants_pkey PRIMARY KEY (id);
ALTER TABLE biz.tenants ADD CONSTRAINT tenants_connector_id_tenant_id_key UNIQUE (connector_id, tenant_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);
@@ -132,32 +365,26 @@ ALTER TABLE biz.trigger_jobs ADD CONSTRAINT trigger_jobs_job_name_key UNIQUE (jo
CREATE INDEX idx_ai_cache_cleanup ON biz.ai_cache USING btree (cache_type, site_id, target_id, created_at);
CREATE INDEX idx_ai_cache_lookup ON biz.ai_cache USING btree (cache_type, site_id, target_id, created_at DESC);
CREATE INDEX idx_ai_conv_app_site ON biz.ai_conversations USING btree (app_id, site_id, created_at DESC);
CREATE INDEX idx_ai_conv_user_site ON biz.ai_conversations USING btree (user_id, site_id, created_at DESC);
CREATE INDEX idx_ai_conv_context ON biz.ai_conversations USING btree (user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST) WHERE (context_type IS NOT NULL);
CREATE INDEX idx_ai_conv_last_msg ON biz.ai_conversations USING btree (user_id, site_id, last_message_at DESC NULLS LAST);
CREATE INDEX idx_ai_conv_user_site ON biz.ai_conversations USING btree (user_id, site_id, created_at DESC);
CREATE INDEX idx_ai_msg_conv ON biz.ai_messages USING btree (conversation_id, created_at);
CREATE INDEX idx_ai_run_logs_alert ON biz.ai_run_logs USING btree (alert_status, created_at DESC) WHERE ((status)::text = ANY ((ARRAY['failed'::character varying, 'timeout'::character varying, 'circuit_open'::character varying])::text[]));
CREATE INDEX idx_ai_run_logs_created ON biz.ai_run_logs USING btree (created_at);
CREATE INDEX idx_ai_run_logs_created_brin ON biz.ai_run_logs USING brin (created_at) WITH (pages_per_range='32');
CREATE INDEX idx_ai_run_logs_site_app ON biz.ai_run_logs USING btree (site_id, app_type);
CREATE INDEX idx_ai_run_logs_status ON biz.ai_run_logs USING btree (status);
CREATE INDEX idx_ai_trigger_jobs_dedup ON biz.ai_trigger_jobs USING btree (event_type, member_id, site_id, created_at) WHERE ((status)::text <> 'skipped_duplicate'::text);
CREATE INDEX idx_ai_trigger_jobs_site ON biz.ai_trigger_jobs USING btree (site_id, event_type);
CREATE INDEX idx_ai_trigger_jobs_status ON biz.ai_trigger_jobs USING btree (status);
CREATE INDEX idx_transfer_log_member ON biz.coach_task_transfer_log USING btree (member_id, created_at DESC);
CREATE INDEX idx_transfer_log_site_created ON biz.coach_task_transfer_log USING btree (site_id, created_at DESC);
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_task_monthly_assistant ON biz.dws_assistant_task_monthly USING btree (assistant_id, stat_month DESC);
CREATE INDEX idx_task_monthly_site_month ON biz.dws_assistant_task_monthly USING btree (site_id, stat_month DESC);
CREATE INDEX idx_excel_log_site ON biz.excel_upload_log USING btree (site_id, created_at DESC);
CREATE INDEX idx_notes_target ON biz.notes USING btree (site_id, target_type, target_id);
CREATE INDEX idx_salary_adj_assistant_month ON biz.salary_adjustments USING btree (assistant_id, salary_month);
CREATE INDEX idx_salary_adj_site_month ON biz.salary_adjustments USING btree (site_id, salary_month);
-- =============================================================================
-- 种子数据触发器配置4 条)
-- =============================================================================
INSERT INTO biz.trigger_jobs (job_type, job_name, trigger_condition, trigger_config, next_run_at)
VALUES
('task_generator', 'task_generator', 'cron',
'{"cron_expression": "0 7 * * *"}',
(CURRENT_DATE + 1) + INTERVAL '7 hours'),
('task_expiry_check', 'task_expiry_check', 'interval',
'{"interval_seconds": 3600}',
NOW() + INTERVAL '1 hour'),
('recall_completion_check', 'recall_completion_check', 'event',
'{"event_name": "etl_data_updated"}',
NULL),
('note_reclassify_backfill', 'note_reclassify_backfill', 'event',
'{"event_name": "recall_completed"}',
NULL)
ON CONFLICT (job_name) DO NOTHING;

View File

@@ -1,6 +1,6 @@
-- =============================================================================
-- zqyy_app / public小程序业务表
-- 生成日期2026-03-15
-- 生成日期2026-04-05
-- 来源:测试库(通过脚本自动导出)
-- =============================================================================
@@ -24,7 +24,8 @@ CREATE TABLE public.admin_users (
site_id bigint NOT NULL,
is_active boolean DEFAULT true,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
updated_at timestamp with time zone DEFAULT now(),
roles _text DEFAULT '{site_admin}'::text[] NOT NULL
);
CREATE TABLE public.approvals (
@@ -47,7 +48,8 @@ CREATE TABLE public.member_retention_clue (
recorded_by_name character varying(50),
recorded_at timestamp with time zone DEFAULT now() NOT NULL,
site_id bigint NOT NULL,
source character varying(20) DEFAULT 'manual'::character varying NOT NULL
source character varying(20) DEFAULT 'manual'::character varying NOT NULL,
is_hidden boolean DEFAULT false NOT NULL
);
CREATE TABLE public.permissions (
@@ -83,7 +85,11 @@ CREATE TABLE public.scheduled_tasks (
run_count integer DEFAULT 0,
last_status character varying(20),
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
updated_at timestamp with time zone DEFAULT now(),
min_run_interval_value integer DEFAULT 0 NOT NULL,
min_run_interval_unit character varying(20) DEFAULT 'minutes'::character varying NOT NULL,
last_success_at timestamp with time zone,
min_run_intervals jsonb DEFAULT '{}'::jsonb NOT NULL
);
CREATE TABLE public.task_execution_log (
@@ -101,7 +107,8 @@ CREATE TABLE public.task_execution_log (
error_log text,
summary jsonb,
created_at timestamp with time zone DEFAULT now(),
schedule_id uuid
schedule_id uuid,
config jsonb
);
CREATE TABLE public.task_queue (
@@ -198,20 +205,3 @@ CREATE INDEX idx_user_roles_site_id ON public.user_roles USING btree (site_id);
CREATE INDEX idx_users_mobile ON public.users USING btree (mobile);
CREATE INDEX idx_users_site_id ON public.users USING btree (site_id);
-- =============================================================================
-- 种子数据Web 管理后台默认管理员账号
-- 默认密码admin123bcrypt hashcost=12
-- 生产环境部署后务必立即修改密码
-- =============================================================================
INSERT INTO admin_users (username, password_hash, display_name, site_id, is_active)
VALUES (
'admin',
'$2b$12$2MTWlJKL0HTgHIkv5Rmpie2pQ9PkeJu0iciLbzPEpPcA94ZakIQzq',
'默认管理员',
1,
TRUE
)
ON CONFLICT (username) DO NOTHING;