petstore-backend/db/migrations/20260802_create_follow_up_task.sql

165 lines
5.9 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Petstore Phase 1可执行回访任务与再次预约归因
-- 前置20260802_create_store_customer_timeline.sql 已执行。
-- ReportLead 保留宠主同意/来源事实FollowUpTask 承载门店操作状态。
CREATE TABLE t_follow_up_task (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
store_id BIGINT NOT NULL,
store_customer_id BIGINT NOT NULL,
source_lead_id BIGINT NOT NULL,
source_report_id BIGINT NULL,
status VARCHAR(16) NOT NULL COMMENT 'pending | in_progress | completed | canceled',
outcome VARCHAR(24) NULL COMMENT 'rebooked | not_interested | invalid_contact | unsubscribed',
due_date DATE NOT NULL,
assigned_user_id BIGINT NULL,
attempt_count INT NOT NULL DEFAULT 0,
last_attempt_outcome VARCHAR(24) NULL COMMENT 'no_answer | follow_later',
last_contacted_at DATETIME NULL,
last_contacted_by_user_id BIGINT NULL,
closed_at DATETIME NULL,
closed_by_user_id BIGINT NULL,
rebooked_appointment_id BIGINT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_follow_up_task_public_id UNIQUE (task_id),
INDEX idx_follow_up_store_status_due (store_id, status, due_date),
INDEX idx_follow_up_customer_status (store_customer_id, status),
INDEX idx_follow_up_lead_status (source_lead_id, status),
INDEX idx_follow_up_rebook (rebooked_appointment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门店回访执行任务';
-- 只为仍待处理的历史留资建立开放任务;不得猜测已发送/取消记录的真实结果。
INSERT INTO t_follow_up_task (
task_id, store_id, store_customer_id, source_lead_id, source_report_id,
status, outcome, due_date, assigned_user_id, attempt_count,
last_attempt_outcome, last_contacted_at, last_contacted_by_user_id,
closed_at, closed_by_user_id, rebooked_appointment_id,
create_time, update_time, version
)
SELECT
UUID(),
l.store_id,
sc.id,
l.id,
l.report_id,
'pending',
NULL,
COALESCE(l.remind_date, CURRENT_DATE),
NULL,
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
COALESCE(l.create_time, l.update_time, NOW()),
COALESCE(l.update_time, l.create_time, NOW()),
0
FROM t_report_lead l
JOIN t_store_customer sc
ON sc.store_id = l.store_id
AND sc.phone = l.phone
AND sc.deleted = 0
WHERE l.store_id IS NOT NULL
AND l.remind_status = 'pending';
-- 历史开放任务形成可靠的 task_created 事实不复制手机号、IP、token 或微信标识。
INSERT INTO t_business_event (
event_id, event_type, event_version, store_id, store_customer_id,
aggregate_type, aggregate_id, actor_user_id, actor_role, source,
occurred_at, metadata_json, idempotency_key, create_time
)
SELECT
UUID(),
'follow_up_created',
1,
t.store_id,
t.store_customer_id,
'follow_up_task',
t.id,
NULL,
NULL,
'migration',
t.create_time,
CONCAT('{"dueDate":"', t.due_date, '"}'),
CONCAT('follow_up_created:', t.id),
NOW()
FROM t_follow_up_task t;
-- 验证:以下五项必须全部为 0。
SELECT COUNT(*) AS pending_leads_without_open_task_count
FROM t_report_lead l
LEFT JOIN t_follow_up_task t
ON t.source_lead_id = l.id
AND t.status IN ('pending', 'in_progress')
WHERE l.store_id IS NOT NULL
AND l.remind_status = 'pending'
AND t.id IS NULL;
SELECT COUNT(*) AS invalid_follow_up_customer_scope_count
FROM t_follow_up_task t
LEFT JOIN t_store_customer sc
ON sc.id = t.store_customer_id
AND sc.store_id = t.store_id
LEFT JOIN t_store_customer canonical
ON canonical.id = CASE
WHEN sc.deleted = 0 THEN sc.id
ELSE sc.merged_into_store_customer_id
END
AND canonical.store_id = t.store_id
AND canonical.deleted = 0
AND canonical.merged_into_store_customer_id IS NULL
WHERE sc.id IS NULL OR canonical.id IS NULL;
SELECT COUNT(*) AS invalid_follow_up_state_count
FROM t_follow_up_task t
WHERE t.status NOT IN ('pending', 'in_progress', 'completed', 'canceled')
OR (t.status IN ('pending', 'in_progress')
AND (t.outcome IS NOT NULL OR t.closed_at IS NOT NULL OR t.closed_by_user_id IS NOT NULL
OR t.rebooked_appointment_id IS NOT NULL))
OR (t.status = 'pending' AND t.assigned_user_id IS NOT NULL)
OR (t.status = 'in_progress' AND t.assigned_user_id IS NULL)
OR (t.status = 'completed'
AND (COALESCE(t.outcome, '') NOT IN ('rebooked', 'not_interested', 'invalid_contact')
OR t.closed_at IS NULL OR t.closed_by_user_id IS NULL
OR t.last_contacted_at IS NULL OR t.last_contacted_by_user_id IS NULL
OR t.attempt_count < 1))
OR (t.status = 'canceled'
AND (COALESCE(t.outcome, '') <> 'unsubscribed' OR t.closed_at IS NULL
OR t.closed_by_user_id IS NOT NULL))
OR (t.last_attempt_outcome IS NOT NULL
AND (t.last_attempt_outcome NOT IN ('no_answer', 'follow_later')
OR t.last_contacted_at IS NULL OR t.last_contacted_by_user_id IS NULL))
OR (t.outcome = 'rebooked' AND t.rebooked_appointment_id IS NULL)
OR (COALESCE(t.outcome, '') <> 'rebooked' AND t.rebooked_appointment_id IS NOT NULL)
OR (t.rebooked_appointment_id IS NOT NULL AND NOT EXISTS (
SELECT 1
FROM t_appointment a
WHERE a.id = t.rebooked_appointment_id
AND a.store_id = t.store_id
AND a.deleted = 0
))
OR t.attempt_count < 0;
SELECT COUNT(*) AS duplicate_open_follow_up_task_count
FROM (
SELECT source_lead_id
FROM t_follow_up_task
WHERE status IN ('pending', 'in_progress')
GROUP BY source_lead_id
HAVING COUNT(*) > 1
) duplicated;
SELECT COUNT(*) AS follow_up_tasks_without_created_event_count
FROM t_follow_up_task t
LEFT JOIN t_business_event e
ON e.idempotency_key = CONCAT('follow_up_created:', t.id)
AND e.event_type = 'follow_up_created'
AND e.store_id = t.store_id
AND e.aggregate_type = 'follow_up_task'
AND e.aggregate_id = t.id
WHERE e.id IS NULL;