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