feat(cleaning): close settlement integrity and reconciliation
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
DELETE FROM qipai_schema_migrations
|
||||
WHERE version = '2026062627';
|
||||
|
||||
DELETE duplicate_item
|
||||
FROM qipai_cleaning_settlement_items duplicate_item
|
||||
INNER JOIN qipai_cleaning_settlement_items retained_item
|
||||
ON retained_item.tenant_id = duplicate_item.tenant_id
|
||||
AND retained_item.task_id = duplicate_item.task_id
|
||||
AND retained_item.id < duplicate_item.id;
|
||||
|
||||
ALTER TABLE qipai_cleaning_settlement_items
|
||||
DROP FOREIGN KEY fk_qipai_cleaning_settlement_items_cleaner,
|
||||
DROP INDEX uq_qipai_cleaning_settlement_item_task_user,
|
||||
|
||||
@@ -22,6 +22,7 @@ WHERE table_schema = DATABASE()
|
||||
'idx_qipai_cleaning_task_member_user',
|
||||
'idx_qipai_cleaning_task_member_task',
|
||||
'uq_qipai_cleaning_settlement_item_task_user',
|
||||
'uq_qipai_cleaning_settlement_active_share',
|
||||
'idx_qipai_cleaning_settlement_items_cleaner'
|
||||
)
|
||||
GROUP BY table_name, index_name;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
DELETE FROM qipai_schema_migrations WHERE version = '2026081006';
|
||||
|
||||
ALTER TABLE qipai_cleaning_settlement_items
|
||||
DROP FOREIGN KEY fk_qipai_cleaning_settlement_items_reversal;
|
||||
|
||||
DELETE reversed
|
||||
FROM qipai_cleaning_settlement_items reversed
|
||||
INNER JOIN qipai_cleaning_settlement_items active
|
||||
ON active.tenant_id = reversed.tenant_id
|
||||
AND active.task_id = reversed.task_id
|
||||
AND active.cleaner_user_id = reversed.cleaner_user_id
|
||||
AND active.reversed_at IS NULL
|
||||
WHERE reversed.reversed_at IS NOT NULL;
|
||||
|
||||
DELETE older
|
||||
FROM qipai_cleaning_settlement_items older
|
||||
INNER JOIN qipai_cleaning_settlement_items newer
|
||||
ON newer.tenant_id = older.tenant_id
|
||||
AND newer.task_id = older.task_id
|
||||
AND newer.cleaner_user_id = older.cleaner_user_id
|
||||
AND newer.id > older.id
|
||||
WHERE older.reversed_at IS NOT NULL AND newer.reversed_at IS NOT NULL;
|
||||
|
||||
ALTER TABLE qipai_cleaning_settlement_items
|
||||
DROP INDEX uq_qipai_cleaning_settlement_active_share,
|
||||
DROP INDEX idx_qipai_cleaning_settlement_item_reversal,
|
||||
DROP COLUMN active_share_key,
|
||||
DROP COLUMN reversed_at,
|
||||
DROP COLUMN reversal_id,
|
||||
ADD UNIQUE KEY uq_qipai_cleaning_settlement_item_task_user
|
||||
(tenant_id, task_id, cleaner_user_id);
|
||||
|
||||
ALTER TABLE qipai_cleaning_settlements
|
||||
DROP FOREIGN KEY fk_qipai_cleaning_settlements_cancelled_by,
|
||||
DROP INDEX idx_qipai_cleaning_settlement_cancelled,
|
||||
DROP INDEX uq_qipai_cleaning_settlement_payout_request,
|
||||
DROP COLUMN payout_request_key,
|
||||
DROP COLUMN payout_request_no,
|
||||
DROP COLUMN cancelled_at,
|
||||
DROP COLUMN cancelled_by;
|
||||
|
||||
DROP TABLE IF EXISTS qipai_cleaning_settlement_events;
|
||||
DROP TABLE IF EXISTS qipai_cleaning_settlement_reversals;
|
||||
@@ -0,0 +1,136 @@
|
||||
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');
|
||||
@@ -0,0 +1,39 @@
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name IN (
|
||||
'qipai_cleaning_settlement_events',
|
||||
'qipai_cleaning_settlement_reversals'
|
||||
)
|
||||
ORDER BY table_name;
|
||||
|
||||
SELECT table_name, column_name, generation_expression
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND ((table_name = 'qipai_cleaning_settlement_items'
|
||||
AND column_name IN ('reversal_id', 'reversed_at', 'active_share_key'))
|
||||
OR (table_name = 'qipai_cleaning_settlements'
|
||||
AND column_name IN (
|
||||
'cancelled_by', 'cancelled_at', 'payout_request_no', 'payout_request_key'
|
||||
))
|
||||
OR (table_name = 'qipai_cleaning_settlement_events'
|
||||
AND column_name = 'actor_type'))
|
||||
ORDER BY table_name, column_name;
|
||||
|
||||
SELECT table_name, index_name
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND ((table_name = 'qipai_cleaning_settlement_items'
|
||||
AND index_name = 'uq_qipai_cleaning_settlement_active_share')
|
||||
OR (table_name = 'qipai_cleaning_settlement_events'
|
||||
AND index_name = 'uq_qipai_cleaning_settlement_event_trace')
|
||||
OR (table_name = 'qipai_cleaning_settlement_reversals'
|
||||
AND index_name = 'uq_qipai_cleaning_settlement_reversal_parent')
|
||||
OR (table_name = 'qipai_cleaning_settlements'
|
||||
AND index_name = 'uq_qipai_cleaning_settlement_payout_request'))
|
||||
GROUP BY table_name, index_name
|
||||
ORDER BY table_name, index_name;
|
||||
|
||||
SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version = '2026081006';
|
||||
Reference in New Issue
Block a user