petstore-backend/db/migrations/20260801_create_business_event.sql
2026-08-01 22:29:20 +08:00

191 lines
6.1 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 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;