CREATE TABLE IF NOT EXISTS qipai_cleaning_settlement_reversals ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, reversal_no VARCHAR(64) NOT NULL, settlement_id BIGINT UNSIGNED NOT NULL, amount_delta_cents BIGINT NOT NULL, reason VARCHAR(512) NOT NULL, reversed_by BIGINT UNSIGNED NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_cleaning_settlement_reversal_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_cleaning_settlement_reversal_settlement FOREIGN KEY (settlement_id) REFERENCES qipai_cleaning_settlements(id), CONSTRAINT fk_qipai_cleaning_settlement_reversal_actor FOREIGN KEY (reversed_by) REFERENCES qipai_users(id), CONSTRAINT chk_qipai_cleaning_settlement_reversal_amount CHECK (amount_delta_cents <= 0), UNIQUE KEY uq_qipai_cleaning_settlement_reversal_no (tenant_id, reversal_no), UNIQUE KEY uq_qipai_cleaning_settlement_reversal_parent (tenant_id, settlement_id), KEY idx_qipai_cleaning_settlement_reversal_created (tenant_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS qipai_cleaning_settlement_events ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, settlement_id BIGINT UNSIGNED NOT NULL, from_status VARCHAR(32) NULL, to_status VARCHAR(32) NOT NULL, action VARCHAR(32) NOT NULL, actor_id BIGINT UNSIGNED NOT NULL, actor_type VARCHAR(16) NOT NULL DEFAULT 'USER', trace_id VARCHAR(64) NOT NULL, note VARCHAR(512) NOT NULL DEFAULT '', metadata JSON NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), CONSTRAINT fk_qipai_cleaning_settlement_event_tenant FOREIGN KEY (tenant_id) REFERENCES qipai_tenants(id), CONSTRAINT fk_qipai_cleaning_settlement_event_parent FOREIGN KEY (settlement_id) REFERENCES qipai_cleaning_settlements(id), CONSTRAINT fk_qipai_cleaning_settlement_event_actor FOREIGN KEY (actor_id) REFERENCES qipai_users(id), CONSTRAINT chk_qipai_cleaning_settlement_event_actor_type CHECK ( actor_type IN ('USER', 'SYSTEM') ), UNIQUE KEY uq_qipai_cleaning_settlement_event_trace (tenant_id, settlement_id, action, trace_id), KEY idx_qipai_cleaning_settlement_event_history (tenant_id, settlement_id, created_at, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ALTER TABLE qipai_cleaning_settlements ADD COLUMN cancelled_by BIGINT UNSIGNED NULL AFTER paid_by, ADD COLUMN cancelled_at DATETIME(3) NULL AFTER paid_at, ADD COLUMN payout_request_no VARCHAR(64) NOT NULL DEFAULT '' AFTER payout_channel, ADD COLUMN payout_request_key VARCHAR(128) GENERATED ALWAYS AS ( CASE WHEN payout_request_no <> '' THEN CONCAT(payout_channel, ':', payout_request_no) ELSE NULL END ) STORED AFTER payout_request_no, ADD CONSTRAINT fk_qipai_cleaning_settlements_cancelled_by FOREIGN KEY (cancelled_by) REFERENCES qipai_users(id), ADD KEY idx_qipai_cleaning_settlement_cancelled (tenant_id, status, cancelled_at), ADD UNIQUE KEY uq_qipai_cleaning_settlement_payout_request (tenant_id, payout_request_key); ALTER TABLE qipai_cleaning_settlement_items DROP INDEX uq_qipai_cleaning_settlement_item_task_user, ADD COLUMN reversal_id BIGINT UNSIGNED NULL AFTER reward_cents, ADD COLUMN reversed_at DATETIME(3) NULL AFTER reversal_id, ADD COLUMN active_share_key VARCHAR(128) GENERATED ALWAYS AS ( CASE WHEN reversed_at IS NULL THEN CONCAT(CAST(task_id AS CHAR), ':', CAST(cleaner_user_id AS CHAR)) ELSE NULL END ) STORED AFTER reversed_at, ADD CONSTRAINT fk_qipai_cleaning_settlement_items_reversal FOREIGN KEY (reversal_id) REFERENCES qipai_cleaning_settlement_reversals(id), ADD UNIQUE KEY uq_qipai_cleaning_settlement_active_share (tenant_id, active_share_key), ADD KEY idx_qipai_cleaning_settlement_item_reversal (tenant_id, reversal_id); UPDATE qipai_cleaning_settlements SET cancelled_by = generated_by, cancelled_at = COALESCE(cancelled_at, updated_at) WHERE status = 'CANCELLED' AND cancelled_by IS NULL; INSERT IGNORE INTO qipai_cleaning_settlement_reversals (tenant_id, reversal_no, settlement_id, amount_delta_cents, reason, reversed_by, created_at) SELECT s.tenant_id, CONCAT('CLR-M09C-', s.id), s.id, -CAST(s.total_reward_cents AS SIGNED), 'M09-C historical cancellation baseline', s.cancelled_by, COALESCE(s.cancelled_at, s.updated_at) FROM qipai_cleaning_settlements s WHERE s.status = 'CANCELLED' AND s.deleted_at IS NULL; UPDATE qipai_cleaning_settlement_items i INNER JOIN qipai_cleaning_settlement_reversals r ON r.tenant_id = i.tenant_id AND r.settlement_id = i.settlement_id SET i.reversal_id = r.id, i.reversed_at = r.created_at WHERE i.reversed_at IS NULL; UPDATE qipai_cleaning_task_members m INNER JOIN qipai_cleaning_settlement_items i ON i.tenant_id = m.tenant_id AND i.task_id = m.task_id AND i.cleaner_user_id = m.user_id INNER JOIN qipai_cleaning_settlements s ON s.tenant_id = i.tenant_id AND s.id = i.settlement_id AND s.deleted_at IS NULL SET m.settled_at = CASE WHEN s.status = 'PAID' AND i.reversed_at IS NULL THEN COALESCE(s.paid_at, m.settled_at, UTC_TIMESTAMP(3)) ELSE NULL END WHERE i.id IS NOT NULL; UPDATE qipai_cleaning_tasks t SET t.status = 'COMPLETED', t.settled_at = NULL WHERE t.status = 'SETTLED' AND EXISTS ( SELECT 1 FROM qipai_cleaning_task_members pending WHERE pending.tenant_id = t.tenant_id AND pending.task_id = t.id AND pending.removed_at IS NULL AND pending.reward_cents > 0 AND pending.settled_at IS NULL ); INSERT IGNORE INTO qipai_cleaning_settlement_events (tenant_id, settlement_id, from_status, to_status, action, actor_id, actor_type, trace_id, note, metadata, created_at) SELECT s.tenant_id, s.id, NULL, s.status, 'MIGRATE', s.generated_by, 'SYSTEM', CONCAT('m09c-', s.id), 'M09-C settlement history baseline', JSON_OBJECT('source', '2026081006'), s.created_at FROM qipai_cleaning_settlements s WHERE s.deleted_at IS NULL; INSERT IGNORE INTO qipai_schema_migrations (version, name) VALUES ('2026081006', 'm09c_cleaning_settlement_integrity');