165 lines
5.9 KiB
SQL
165 lines
5.9 KiB
SQL
-- 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;
|