fix: scope pilot metrics to explicit cohort

This commit is contained in:
malei 2026-08-02 02:35:58 +08:00
parent 93d49b18b1
commit 21846dba8b
3 changed files with 236 additions and 28 deletions

View File

@ -155,7 +155,7 @@ BUILD_FIRST=1 deploy/release-preflight.sh
mvn test mvn test
``` ```
试点期的聚合指标使用只读脚本 `db/queries/pilot_metrics.sql`脚本默认统计最近 30 天,可在当前数据库 session 覆盖 `@pilot_from/@pilot_to/@pilot_store_id`输出不包含客户或员工直接标识。只能由只读账号/只读副本执行,完整发布仍以 31 项 production preflight 为数据门禁。 试点期的聚合指标使用只读脚本 `db/queries/pilot_metrics.sql`执行前必须在当前数据库 session 显式填写 35 个 `@pilot_store_ids`,并确认统一的 `@pilot_from/@pilot_to`;默认门店集合为 `0`,不会意外汇总全部门店或历史。输出不包含客户或员工直接标识。只能由只读账号/只读副本执行,完整发布仍以 31 项 production preflight 为数据门禁。
最小测试基线P0 稳定化批次): 最小测试基线P0 稳定化批次):
- `SessionTokenServiceTest`:签发/校验/篡改/过期 - `SessionTokenServiceTest`:签发/校验/篡改/过期

View File

@ -1,67 +1,228 @@
-- Petstore Pilot Metrics v1MySQL 5.7+/8.x只读 -- Petstore Pilot Metrics v2MySQL 5.7+/8.x只读
-- 默认统计最近 30 天;发布/试点负责人可在当前 session 覆盖三个变量后逐段执行。 -- 默认时间窗为最近 30 天,但默认门店集合为 0不返回业务数据
-- 执行人必须把 @pilot_store_ids 显式改为 35 个试点门店内部 ID例如 '101,205,309'。
-- 输出仅含 store_id、计数、比率和时效不含客户/员工 PII。 -- 输出仅含 store_id、计数、比率和时效不含客户/员工 PII。
SET @pilot_to = DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY); SET @pilot_to = DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY);
SET @pilot_from = DATE_SUB(@pilot_to, INTERVAL 30 DAY); SET @pilot_from = DATE_SUB(@pilot_to, INTERVAL 30 DAY);
SET @pilot_store_id = NULL; SET @pilot_store_ids = '0';
-- 1. 核心服务与报告漏斗。[from, to),各事实按稳定 aggregate_id 去重。 -- 1. 核心服务与报告漏斗。[from, to),各事实按稳定 aggregate_id 去重。
-- 比率只认同窗且时间顺序正确的 cohort避免跨窗口事件把比率推到 100% 以上。
SELECT SELECT
e.store_id, e.store_id,
COUNT(DISTINCT CASE WHEN e.event_type = 'appointment_created' THEN e.aggregate_id END) AS appointment_created_count, COUNT(DISTINCT CASE WHEN e.event_type = 'appointment_created' THEN e.aggregate_id END) AS appointment_created_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END) AS service_started_count, COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END) AS service_started_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'service_completed' THEN e.aggregate_id END) AS complete_service_count, COUNT(DISTINCT CASE WHEN e.event_type = 'service_completed' THEN e.aggregate_id END) AS complete_service_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END) AS report_submitted_count, COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END) AS report_submitted_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'report_sent' THEN e.aggregate_id END) AS report_sent_count, COUNT(DISTINCT CASE
COUNT(DISTINCT CASE WHEN e.event_type IN ('report_opened', 'report_reopened') THEN e.aggregate_id END) AS report_opened_count, WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END) AS report_sent_count,
COUNT(DISTINCT CASE
WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END) AS report_opened_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END) AS lead_submitted_count, COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END) AS lead_submitted_count,
COUNT(DISTINCT CASE WHEN e.event_type = 'rebook_created' THEN e.aggregate_id END) AS rebook_created_count, COUNT(DISTINCT CASE WHEN e.event_type = 'rebook_created' THEN e.aggregate_id END) AS rebook_created_count,
ROUND( ROUND(
100 * COUNT(DISTINCT CASE WHEN e.event_type = 'service_completed' THEN e.aggregate_id END) 100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'service_completed'
AND EXISTS (
SELECT 1 FROM t_business_event started
WHERE started.store_id = e.store_id
AND started.event_type = 'service_started'
AND started.aggregate_type = 'appointment'
AND started.aggregate_id = e.aggregate_id
AND started.occurred_at >= @pilot_from
AND started.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END), 0), / NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'service_started' THEN e.aggregate_id END), 0),
1 1
) AS service_completion_rate_pct, ) AS service_completion_rate_pct,
ROUND( ROUND(
100 * COUNT(DISTINCT CASE WHEN e.event_type = 'report_sent' THEN e.aggregate_id END) 100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END), 0), / NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'report_submitted' THEN e.aggregate_id END), 0),
1 1
) AS report_sent_rate_pct, ) AS report_sent_rate_pct,
ROUND( ROUND(
100 * COUNT(DISTINCT CASE WHEN e.event_type IN ('report_opened', 'report_reopened') THEN e.aggregate_id END) 100 * COUNT(DISTINCT CASE
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'report_sent' THEN e.aggregate_id END), 0), WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END)
/ NULLIF(COUNT(DISTINCT CASE
WHEN e.event_type = 'report_sent'
AND EXISTS (
SELECT 1 FROM t_business_event submitted
WHERE submitted.store_id = e.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = e.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END), 0),
1 1
) AS report_open_rate_pct, ) AS report_open_rate_pct,
ROUND( ROUND(
100 * COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END) 100 * COUNT(DISTINCT CASE
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type IN ('report_opened', 'report_reopened') THEN e.aggregate_id END), 0), WHEN e.event_type = 'lead_submitted'
AND lead_fact.report_id IS NOT NULL
AND EXISTS (
SELECT 1
FROM t_business_event opened
JOIN t_business_event sent
ON sent.store_id = opened.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = opened.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= opened.occurred_at
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE opened.store_id = e.store_id
AND opened.event_type IN ('report_opened', 'report_reopened')
AND opened.aggregate_type = 'report'
AND opened.aggregate_id = lead_fact.report_id
AND opened.occurred_at >= @pilot_from
AND opened.occurred_at <= e.occurred_at
)
THEN lead_fact.report_id
END)
/ NULLIF(COUNT(DISTINCT CASE
WHEN e.event_type IN ('report_opened', 'report_reopened')
AND EXISTS (
SELECT 1
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at <= sent.occurred_at
WHERE sent.store_id = e.store_id
AND sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.aggregate_id = e.aggregate_id
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at <= e.occurred_at
)
THEN e.aggregate_id
END), 0),
1 1
) AS opened_to_lead_rate_pct, ) AS opened_to_lead_rate_pct,
ROUND( ROUND(
100 * COUNT(DISTINCT CASE WHEN e.event_type = 'rebook_created' THEN e.aggregate_id END) 100 * COUNT(DISTINCT CASE
WHEN e.event_type = 'rebook_created'
AND rebook.source_lead_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM t_business_event submitted_lead
WHERE submitted_lead.store_id = e.store_id
AND submitted_lead.event_type = 'lead_submitted'
AND submitted_lead.aggregate_type = 'report_lead'
AND submitted_lead.aggregate_id = rebook.source_lead_id
AND submitted_lead.occurred_at >= @pilot_from
AND submitted_lead.occurred_at <= e.occurred_at
)
THEN rebook.source_lead_id
END)
/ NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END), 0), / NULLIF(COUNT(DISTINCT CASE WHEN e.event_type = 'lead_submitted' THEN e.aggregate_id END), 0),
1 1
) AS lead_to_rebook_rate_pct ) AS lead_to_rebook_rate_pct
FROM t_business_event e FROM t_business_event e
LEFT JOIN t_report_lead lead_fact
ON e.event_type = 'lead_submitted'
AND lead_fact.id = e.aggregate_id
AND lead_fact.store_id = e.store_id
LEFT JOIN t_follow_up_task rebook
ON e.event_type = 'rebook_created'
AND rebook.rebooked_appointment_id = e.aggregate_id
AND rebook.store_id = e.store_id
WHERE e.occurred_at >= @pilot_from WHERE e.occurred_at >= @pilot_from
AND e.occurred_at < @pilot_to AND e.occurred_at < @pilot_to
AND (@pilot_store_id IS NULL OR e.store_id = @pilot_store_id) AND FIND_IN_SET(CAST(e.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY e.store_id GROUP BY e.store_id
ORDER BY e.store_id; ORDER BY e.store_id;
-- 2. 报告发送时效:提交后 24 小时内是否形成首次显式发送事实。 -- 2. 报告发送时效:只纳入已经完整观察 24 小时的提交 cohort
SELECT SELECT
submitted.store_id, submitted.store_id,
COUNT(*) AS submitted_report_count, COUNT(*) AS matured_submitted_report_count,
SUM(CASE SUM(CASE
WHEN sent.occurred_at IS NOT NULL WHEN sent.occurred_at IS NOT NULL
AND sent.occurred_at < DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR) AND sent.occurred_at >= submitted.occurred_at
AND sent.occurred_at <= DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR)
THEN 1 ELSE 0 THEN 1 ELSE 0
END) AS sent_within_24h_count, END) AS sent_within_24h_count,
ROUND( ROUND(
100 * SUM(CASE 100 * SUM(CASE
WHEN sent.occurred_at IS NOT NULL WHEN sent.occurred_at IS NOT NULL
AND sent.occurred_at < DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR) AND sent.occurred_at >= submitted.occurred_at
AND sent.occurred_at <= DATE_ADD(submitted.occurred_at, INTERVAL 24 HOUR)
THEN 1 ELSE 0 THEN 1 ELSE 0
END) / NULLIF(COUNT(*), 0), END) / NULLIF(COUNT(*), 0),
1 1
@ -75,8 +236,8 @@ LEFT JOIN t_business_event sent
WHERE submitted.event_type = 'report_submitted' WHERE submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report' AND submitted.aggregate_type = 'report'
AND submitted.occurred_at >= @pilot_from AND submitted.occurred_at >= @pilot_from
AND submitted.occurred_at < @pilot_to AND submitted.occurred_at < DATE_SUB(@pilot_to, INTERVAL 24 HOUR)
AND (@pilot_store_id IS NULL OR submitted.store_id = @pilot_store_id) AND FIND_IN_SET(CAST(submitted.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY submitted.store_id GROUP BY submitted.store_id
ORDER BY submitted.store_id; ORDER BY submitted.store_id;
@ -111,11 +272,11 @@ SELECT
FROM t_follow_up_task t FROM t_follow_up_task t
WHERE t.due_date >= DATE(@pilot_from) WHERE t.due_date >= DATE(@pilot_from)
AND t.due_date < DATE(@pilot_to) AND t.due_date < DATE(@pilot_to)
AND (@pilot_store_id IS NULL OR t.store_id = @pilot_store_id) AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY t.store_id GROUP BY t.store_id
ORDER BY t.store_id; ORDER BY t.store_id;
-- 4. 约 200 次完整服务样本闸门:全历史按店 + 最后一行全店合计。 -- 4. 约 200 次完整服务样本闸门:严格使用试点窗口,按店 + 最后一行试点店合计。
SELECT SELECT
e.store_id, e.store_id,
COUNT(DISTINCT e.aggregate_id) AS complete_service_count, COUNT(DISTINCT e.aggregate_id) AS complete_service_count,
@ -124,10 +285,12 @@ SELECT
FROM t_business_event e FROM t_business_event e
WHERE e.event_type = 'service_completed' WHERE e.event_type = 'service_completed'
AND e.aggregate_type = 'appointment' AND e.aggregate_type = 'appointment'
AND (@pilot_store_id IS NULL OR e.store_id = @pilot_store_id) AND e.occurred_at >= @pilot_from
AND e.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(e.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY e.store_id WITH ROLLUP; GROUP BY e.store_id WITH ROLLUP;
-- 5. 指标可靠性反例:项均须为 0完整发布还必须通过 31 项 production preflight。 -- 5. 指标可靠性反例:项均须为 0完整发布还必须通过 31 项 production preflight。
SELECT COUNT(*) AS completed_services_without_submitted_report_event_count SELECT COUNT(*) AS completed_services_without_submitted_report_event_count
FROM t_business_event completed FROM t_business_event completed
LEFT JOIN t_report r LEFT JOIN t_report r
@ -142,7 +305,9 @@ LEFT JOIN t_business_event submitted
WHERE completed.event_type = 'service_completed' WHERE completed.event_type = 'service_completed'
AND completed.aggregate_type = 'appointment' AND completed.aggregate_type = 'appointment'
AND (r.id IS NULL OR submitted.id IS NULL) AND (r.id IS NULL OR submitted.id IS NULL)
AND (@pilot_store_id IS NULL OR completed.store_id = @pilot_store_id); AND completed.occurred_at >= @pilot_from
AND completed.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(completed.store_id AS CHAR), @pilot_store_ids) > 0;
SELECT COUNT(*) AS invalid_rebook_attribution_count SELECT COUNT(*) AS invalid_rebook_attribution_count
FROM t_follow_up_task t FROM t_follow_up_task t
@ -158,14 +323,52 @@ LEFT JOIN t_business_event e
AND e.aggregate_id = t.rebooked_appointment_id AND e.aggregate_id = t.rebooked_appointment_id
WHERE t.outcome = 'rebooked' WHERE t.outcome = 'rebooked'
AND (a.id IS NULL OR e.id IS NULL) AND (a.id IS NULL OR e.id IS NULL)
AND (@pilot_store_id IS NULL OR t.store_id = @pilot_store_id); AND t.closed_at >= @pilot_from
AND t.closed_at < @pilot_to
AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0;
SELECT COUNT(*) AS duplicate_open_follow_up_task_count SELECT COUNT(*) AS duplicate_open_follow_up_task_count
FROM ( FROM (
SELECT t.store_id, t.source_lead_id SELECT t.store_id, t.source_lead_id
FROM t_follow_up_task t FROM t_follow_up_task t
WHERE t.status IN ('pending', 'in_progress') WHERE t.status IN ('pending', 'in_progress')
AND (@pilot_store_id IS NULL OR t.store_id = @pilot_store_id) AND FIND_IN_SET(CAST(t.store_id AS CHAR), @pilot_store_ids) > 0
GROUP BY t.store_id, t.source_lead_id GROUP BY t.store_id, t.source_lead_id
HAVING COUNT(*) > 1 HAVING COUNT(*) > 1
) duplicated; ) duplicated;
SELECT SUM(anomaly_count) AS invalid_metric_event_order_count
FROM (
SELECT COUNT(*) AS anomaly_count
FROM t_business_event sent
JOIN t_business_event submitted
ON submitted.store_id = sent.store_id
AND submitted.event_type = 'report_submitted'
AND submitted.aggregate_type = 'report'
AND submitted.aggregate_id = sent.aggregate_id
WHERE sent.event_type = 'report_sent'
AND sent.aggregate_type = 'report'
AND sent.occurred_at < submitted.occurred_at
AND sent.occurred_at >= @pilot_from
AND sent.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(sent.store_id AS CHAR), @pilot_store_ids) > 0
UNION ALL
SELECT COUNT(*) AS anomaly_count
FROM t_business_event rebooked
JOIN t_follow_up_task task
ON task.store_id = rebooked.store_id
AND task.rebooked_appointment_id = rebooked.aggregate_id
JOIN t_business_event submitted_lead
ON submitted_lead.store_id = rebooked.store_id
AND submitted_lead.event_type = 'lead_submitted'
AND submitted_lead.aggregate_type = 'report_lead'
AND submitted_lead.aggregate_id = task.source_lead_id
WHERE rebooked.event_type = 'rebook_created'
AND rebooked.aggregate_type = 'appointment'
AND rebooked.occurred_at < submitted_lead.occurred_at
AND rebooked.occurred_at >= @pilot_from
AND rebooked.occurred_at < @pilot_to
AND FIND_IN_SET(CAST(rebooked.store_id AS CHAR), @pilot_store_ids) > 0
) anomalies;

View File

@ -26,7 +26,12 @@ class PilotMetricsQueryContractTest {
assertTrue(normalized.contains("sent_within_24h_rate_pct")); assertTrue(normalized.contains("sent_within_24h_rate_pct"));
assertTrue(normalized.contains("on_time_completion_rate_pct")); assertTrue(normalized.contains("on_time_completion_rate_pct"));
assertTrue(normalized.contains("rebook_created")); assertTrue(normalized.contains("rebook_created"));
assertTrue(normalized.contains("@pilot_store_id")); assertTrue(normalized.contains("set @pilot_store_ids = '0'"));
assertTrue(normalized.contains("petstore pilot metrics v2"));
assertTrue(normalized.contains("find_in_set(cast("));
assertTrue(normalized.contains("matured_submitted_report_count"));
assertTrue(normalized.contains("invalid_metric_event_order_count"));
assertFalse(normalized.contains("@pilot_store_id = null"));
} }
@Test @Test