191 lines
6.1 KiB
SQL
191 lines
6.1 KiB
SQL
-- Petstore Phase 0: persist immutable business events for funnels and timelines.
|
||
-- Target: MySQL. Run after 20260801_create_store_customer.sql.
|
||
-- One-time migration; back up the database before execution.
|
||
|
||
CREATE TABLE t_business_event (
|
||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||
event_id VARCHAR(36) NOT NULL COMMENT '随机事件 ID',
|
||
event_type VARCHAR(64) NOT NULL COMMENT '过去时业务事件类型',
|
||
event_version INT NOT NULL DEFAULT 1,
|
||
store_id BIGINT NOT NULL,
|
||
store_customer_id BIGINT NULL COMMENT '门店客户稳定关系,可空',
|
||
aggregate_type VARCHAR(32) NOT NULL,
|
||
aggregate_id BIGINT NOT NULL,
|
||
actor_user_id BIGINT NULL,
|
||
actor_role VARCHAR(16) NULL,
|
||
source VARCHAR(32) NOT NULL,
|
||
occurred_at DATETIME(3) NOT NULL,
|
||
metadata_json TEXT NOT NULL COMMENT '仅低敏白名单维度 JSON',
|
||
idempotency_key VARCHAR(128) NULL,
|
||
create_time DATETIME(3) NOT NULL,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uk_business_event_event_id (event_id),
|
||
UNIQUE KEY uk_business_event_idempotency (idempotency_key),
|
||
KEY idx_be_store_type_time (store_id, event_type, occurred_at),
|
||
KEY idx_be_aggregate_time (aggregate_type, aggregate_id, occurred_at),
|
||
KEY idx_be_store_customer_time (store_customer_id, occurred_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='不可变业务事件';
|
||
|
||
-- 历史预约只回填可确定的“预约已创建”,不猜测无法还原的开始服务时间。
|
||
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(),
|
||
'appointment_created',
|
||
1,
|
||
a.store_id,
|
||
sc.id,
|
||
'appointment',
|
||
a.id,
|
||
a.created_by_user_id,
|
||
actor.role,
|
||
CASE
|
||
WHEN a.created_by_user_id IS NULL THEN 'migration'
|
||
WHEN a.created_by_user_id = a.customer_user_id THEN 'customer'
|
||
ELSE 'admin'
|
||
END,
|
||
COALESCE(a.create_time, a.appointment_time, NOW()),
|
||
CASE
|
||
WHEN a.created_by_user_id IS NULL
|
||
THEN '{"bookingOrigin":"migration"}'
|
||
WHEN a.created_by_user_id = a.customer_user_id
|
||
THEN '{"bookingOrigin":"customer"}'
|
||
ELSE '{"bookingOrigin":"admin"}'
|
||
END,
|
||
CONCAT('appointment_created:', a.id),
|
||
NOW()
|
||
FROM t_appointment a
|
||
LEFT JOIN t_store_customer sc
|
||
ON sc.store_id = a.store_id
|
||
AND sc.customer_user_id = a.customer_user_id
|
||
AND sc.deleted = 0
|
||
LEFT JOIN t_user actor
|
||
ON actor.id = a.created_by_user_id
|
||
AND actor.deleted = 0
|
||
WHERE a.deleted = 0
|
||
AND a.store_id IS NOT NULL;
|
||
|
||
-- 历史报告提交是可靠事实;不保存 report_token 或其 hash。
|
||
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(),
|
||
'report_submitted',
|
||
1,
|
||
r.store_id,
|
||
sc.id,
|
||
'report',
|
||
r.id,
|
||
r.author_staff_id,
|
||
actor.role,
|
||
CASE WHEN r.author_staff_id IS NULL THEN 'migration' ELSE 'admin' END,
|
||
COALESCE(r.create_time, NOW()),
|
||
'{}',
|
||
CONCAT('report_submitted:', r.id),
|
||
NOW()
|
||
FROM t_report r
|
||
LEFT JOIN t_store_customer sc
|
||
ON sc.store_id = r.store_id
|
||
AND sc.customer_user_id = r.customer_user_id
|
||
AND sc.deleted = 0
|
||
LEFT JOIN t_user actor
|
||
ON actor.id = r.author_staff_id
|
||
AND actor.deleted = 0
|
||
WHERE r.deleted = 0
|
||
AND r.store_id IS NOT NULL;
|
||
|
||
-- 有报告的完成时间可由报告创建时间可靠近似;无报告的历史 done 不做猜测。
|
||
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(),
|
||
'service_completed',
|
||
1,
|
||
r.store_id,
|
||
sc.id,
|
||
'appointment',
|
||
r.appointment_id,
|
||
r.author_staff_id,
|
||
actor.role,
|
||
CASE WHEN r.author_staff_id IS NULL THEN 'migration' ELSE 'admin' END,
|
||
COALESCE(r.create_time, NOW()),
|
||
'{}',
|
||
CONCAT('service_completed:', r.appointment_id),
|
||
NOW()
|
||
FROM t_report r
|
||
LEFT JOIN t_store_customer sc
|
||
ON sc.store_id = r.store_id
|
||
AND sc.customer_user_id = r.customer_user_id
|
||
AND sc.deleted = 0
|
||
LEFT JOIN t_user actor
|
||
ON actor.id = r.author_staff_id
|
||
AND actor.deleted = 0
|
||
WHERE r.deleted = 0
|
||
AND r.store_id IS NOT NULL
|
||
AND r.appointment_id IS NOT NULL;
|
||
|
||
-- 历史留资每条只形成一个 lead_submitted 事实;不复制手机号、IP、openid 或退订 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(),
|
||
'lead_submitted',
|
||
1,
|
||
l.store_id,
|
||
sc.id,
|
||
'report_lead',
|
||
l.id,
|
||
NULL,
|
||
NULL,
|
||
'public_report',
|
||
COALESCE(l.consent_at, l.create_time, l.update_time, NOW()),
|
||
'{"repeatSubmit":false}',
|
||
CONCAT('lead_submitted:', l.id),
|
||
NOW()
|
||
FROM t_report_lead l
|
||
LEFT 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;
|
||
|
||
-- Verification queries. All four counts should be 0 before application release.
|
||
SELECT COUNT(*) AS appointments_without_created_event
|
||
FROM t_appointment a
|
||
LEFT JOIN t_business_event e
|
||
ON e.idempotency_key = CONCAT('appointment_created:', a.id)
|
||
WHERE a.deleted = 0
|
||
AND a.store_id IS NOT NULL
|
||
AND e.id IS NULL;
|
||
|
||
SELECT COUNT(*) AS reports_without_submitted_event
|
||
FROM t_report r
|
||
LEFT JOIN t_business_event e
|
||
ON e.idempotency_key = CONCAT('report_submitted:', r.id)
|
||
WHERE r.deleted = 0
|
||
AND r.store_id IS NOT NULL
|
||
AND e.id IS NULL;
|
||
|
||
SELECT COUNT(*) AS leads_without_submitted_event
|
||
FROM t_report_lead l
|
||
LEFT JOIN t_business_event e
|
||
ON e.idempotency_key = CONCAT('lead_submitted:', l.id)
|
||
WHERE l.store_id IS NOT NULL
|
||
AND e.id IS NULL;
|
||
|
||
SELECT COUNT(*) AS events_without_store
|
||
FROM t_business_event
|
||
WHERE store_id IS NULL;
|