合并 main 分支并融合订单列表最小化与 SQL 治理

- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义
- 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试)
- 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知
- 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
yml2213
2026-09-05 11:43:13 +08:00
294 changed files with 30171 additions and 13439 deletions
@@ -0,0 +1,35 @@
-- +goose Up
INSERT INTO permissions (code, name, resource, action) VALUES
('backup:view', '查看备份状态', 'backup', 'view'),
('backup:schedule:update', '修改备份定时', 'backup', 'schedule_update')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
resource = VALUES(resource),
action = VALUES(action);
INSERT IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.code = 'super_admin' AND p.code IN ('backup:view', 'backup:schedule:update');
INSERT INTO system_configs (`key`, `value`, description) VALUES
('backup.schedule_enabled', 'true', '在线备份定时任务是否启用'),
('backup.full_hour', '4', '每日全量备份小时(Asia/Shanghai)'),
('backup.full_minute', '30', '每日全量备份分钟(Asia/Shanghai)'),
('backup.binlog_interval_minutes', '1', 'binlog 归档间隔分钟(1-5)')
ON DUPLICATE KEY UPDATE description = VALUES(description);
-- +goose Down
DELETE rp FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
WHERE p.code IN ('backup:view', 'backup:schedule:update');
DELETE FROM permissions WHERE code IN ('backup:view', 'backup:schedule:update');
DELETE FROM system_configs WHERE `key` IN (
'backup.schedule_enabled',
'backup.full_hour',
'backup.full_minute',
'backup.binlog_interval_minutes'
);
@@ -0,0 +1,21 @@
-- +goose Up
-- 修正历史外部上传中以 VX/WX 表示微信扫码、但被默认归类为 QQ 的账号。
UPDATE game_accounts AS a
JOIN rental_listings AS l ON l.account_id = a.id
SET a.server_region = '微信'
WHERE a.server_region = 'QQ'
AND EXISTS (
SELECT 1
FROM listing_uploads AS lu
WHERE lu.listing_id = l.id
)
AND (
a.login_platform LIKE '%微信%'
OR LOWER(TRIM(a.login_platform)) LIKE 'vx%'
OR LOWER(TRIM(a.login_platform)) LIKE 'wx%'
);
-- +goose Down
-- 此数据修复不可逆,避免将已修正的微信账号错误回退为 QQ。
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE users
ADD COLUMN token_version BIGINT NOT NULL DEFAULT 0 COMMENT '用户会话版本,递增后旧令牌失效';
-- +goose Down
ALTER TABLE users
DROP COLUMN token_version;
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE manual_disbursements
ADD COLUMN custom_category_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '自定义出款分类名称';
-- +goose Down
ALTER TABLE manual_disbursements
DROP COLUMN custom_category_name;
@@ -0,0 +1,44 @@
-- +goose Up
CREATE TABLE operating_expenses (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
expense_no VARCHAR(64) NOT NULL COMMENT '运营开支单号',
category VARCHAR(50) NOT NULL COMMENT '开支类别',
payee_name VARCHAR(100) NOT NULL COMMENT '收款方或单位名称',
amount_cent BIGINT NOT NULL COMMENT '开支金额(分)',
occurred_at DATETIME NOT NULL COMMENT '实际开支时间',
remark VARCHAR(500) NOT NULL COMMENT '开支用途备注',
voucher_url VARCHAR(500) NOT NULL DEFAULT '' COMMENT '开支凭证地址',
status VARCHAR(16) NOT NULL DEFAULT 'paid' COMMENT '状态: paid已记账/voided已作废',
created_by BIGINT UNSIGNED NOT NULL COMMENT '录入管理员ID',
voided_by BIGINT UNSIGNED NULL COMMENT '作废管理员ID',
voided_at DATETIME NULL COMMENT '作废时间',
void_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '作废原因',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_operating_expenses_no (expense_no),
KEY idx_operating_expenses_occurred (status, occurred_at),
KEY idx_operating_expenses_created (created_at),
KEY idx_operating_expenses_category (category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='运营开支记账';
INSERT INTO permissions (code, name, resource, action) VALUES
('finance:operating_expense', '管理运营开支', 'finance', 'operating_expense')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
resource = VALUES(resource),
action = VALUES(action);
INSERT IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.code IN ('super_admin', 'finance') AND p.code = 'finance:operating_expense';
-- +goose Down
DELETE rp FROM role_permissions rp
JOIN permissions p ON p.id = rp.permission_id
WHERE p.code = 'finance:operating_expense';
DELETE FROM permissions WHERE code = 'finance:operating_expense';
DROP TABLE IF EXISTS operating_expenses;
@@ -0,0 +1,24 @@
-- +goose Up
-- 本地历史数据库可能已占用 000053 版本但未包含该字段,此迁移负责补齐。
-- +goose StatementBegin
SET @custom_category_column_exists = (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'manual_disbursements'
AND column_name = 'custom_category_name'
);
SET @custom_category_sql = IF(
@custom_category_column_exists = 0,
'ALTER TABLE manual_disbursements ADD COLUMN custom_category_name VARCHAR(50) NOT NULL DEFAULT '''' COMMENT ''自定义出款分类名称''',
'SELECT 1'
);
PREPARE custom_category_statement FROM @custom_category_sql;
EXECUTE custom_category_statement;
DEALLOCATE PREPARE custom_category_statement;
-- +goose StatementEnd
-- +goose Down
-- 补偿迁移不删除字段,避免回滚时误删由 000053 创建的列。
@@ -0,0 +1,70 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS chat_admin_conversation_states (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT UNSIGNED NOT NULL COMMENT '会话ID',
admin_user_id BIGINT UNSIGNED NOT NULL COMMENT '管理员ID',
remark VARCHAR(128) NOT NULL DEFAULT '' COMMENT '管理员个人备注',
last_read_message_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后已读消息ID',
last_read_at DATETIME NULL COMMENT '最后已读时间',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_chat_admin_conversation_state (conversation_id, admin_user_id),
KEY idx_chat_admin_state_admin (admin_user_id, conversation_id),
KEY idx_chat_admin_state_read (admin_user_id, last_read_message_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员会话个人状态';
INSERT INTO chat_admin_conversation_states (
conversation_id, admin_user_id, remark, last_read_message_id, last_read_at
)
SELECT
cp.conversation_id,
cp.participant_id,
cp.remark,
COALESCE((
SELECT MAX(cm.id)
FROM chat_messages cm
WHERE cm.conversation_id = cp.conversation_id
AND cp.last_read_at IS NOT NULL
AND cm.created_at <= cp.last_read_at
), 0),
cp.last_read_at
FROM chat_participants cp
WHERE cp.participant_type = 'admin'
AND (cp.remark <> '' OR cp.last_read_at IS NOT NULL)
ON DUPLICATE KEY UPDATE
remark = VALUES(remark),
last_read_message_id = GREATEST(
chat_admin_conversation_states.last_read_message_id,
VALUES(last_read_message_id)
),
last_read_at = CASE
WHEN chat_admin_conversation_states.last_read_at IS NULL THEN VALUES(last_read_at)
WHEN VALUES(last_read_at) IS NULL THEN chat_admin_conversation_states.last_read_at
ELSE GREATEST(chat_admin_conversation_states.last_read_at, VALUES(last_read_at))
END;
ALTER TABLE chat_conversations
ADD INDEX idx_chat_conversations_status_activity (status, last_message_at, id),
ADD INDEX idx_chat_conversations_type_scene_activity (type, support_scene, last_message_at, id);
-- 后台客服列表需要按商品快速定位最新订单,避免为每个会话执行全表排序。
ALTER TABLE rental_orders
ADD INDEX idx_rental_orders_listing_id_id (listing_id, id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE rental_orders
DROP INDEX idx_rental_orders_listing_id_id;
ALTER TABLE chat_conversations
DROP INDEX idx_chat_conversations_type_scene_activity,
DROP INDEX idx_chat_conversations_status_activity;
DROP TABLE IF EXISTS chat_admin_conversation_states;
-- +goose StatementEnd
@@ -0,0 +1,37 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE chat_messages
ADD COLUMN admin_attention_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客服关注类型: user_inquiry用户咨询, order_paid支付成功' AFTER attachment_urls,
ADD KEY idx_chat_messages_attention (conversation_id, admin_attention_type, id);
UPDATE chat_messages
SET admin_attention_type = 'user_inquiry'
WHERE sender_type = 'user' AND admin_attention_type = '';
INSERT INTO chat_admin_conversation_states (
conversation_id, admin_user_id, remark, last_read_message_id, last_read_at
)
SELECT c.id, au.id, '', COALESCE(c.last_message_id, 0), c.last_message_at
FROM chat_conversations c
JOIN admin_users au ON au.status = 'active'
JOIN admin_user_roles aur ON aur.admin_user_id = au.id
JOIN roles r ON r.id = aur.role_id AND r.code = 'cs'
LEFT JOIN chat_admin_conversation_states cas
ON cas.conversation_id = c.id AND cas.admin_user_id = au.id
WHERE cas.id IS NULL;
UPDATE chat_admin_conversation_states cas
JOIN chat_conversations c ON c.id = cas.conversation_id
SET cas.last_read_message_id = COALESCE(c.last_message_id, 0),
cas.last_read_at = c.last_message_at
WHERE cas.last_read_message_id = 0;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE chat_messages
DROP KEY idx_chat_messages_attention,
DROP COLUMN admin_attention_type;
-- +goose StatementEnd
@@ -0,0 +1,37 @@
-- +goose Up
CREATE TABLE process_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
business_type VARCHAR(32) NOT NULL,
business_id BIGINT UNSIGNED NOT NULL,
stage VARCHAR(32) NOT NULL DEFAULT '',
action VARCHAR(64) NOT NULL,
actor_type VARCHAR(16) NOT NULL,
actor_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
actor_name VARCHAR(128) NOT NULL DEFAULT '',
target_type VARCHAR(16) NOT NULL DEFAULT '',
target_id BIGINT UNSIGNED NULL,
target_name VARCHAR(128) NOT NULL DEFAULT '',
content TEXT NOT NULL,
reason TEXT NOT NULL,
payload JSON NOT NULL,
attachment_urls JSON NOT NULL,
state_before JSON NOT NULL,
state_after JSON NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_process_event_business (business_type, business_id, id),
KEY idx_process_event_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单与提号不可变过程留痕';
ALTER TABLE rental_orders
ADD COLUMN account_source VARCHAR(32) NOT NULL DEFAULT 'internal' COMMENT '订单创建时账号来源快照' AFTER account_snapshot,
ADD COLUMN source_channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '订单创建时外部来源渠道快照' AFTER account_source;
-- +goose Down
ALTER TABLE rental_orders
DROP COLUMN source_channel,
DROP COLUMN account_source;
DROP TABLE IF EXISTS process_events;
@@ -0,0 +1,52 @@
-- +goose Up
-- 高频客服列表不再实时扫描全部订单与聊天消息;以下字段是可由业务记录重建的读取快照。
ALTER TABLE chat_conversations
ADD COLUMN latest_order_id BIGINT UNSIGNED NULL COMMENT '发布群最新订单ID快照' AFTER listing_id,
ADD COLUMN latest_order_no VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发布群最新订单号快照' AFTER latest_order_id,
ADD COLUMN latest_order_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT '发布群最新订单状态快照' AFTER latest_order_no,
ADD COLUMN latest_order_handoff_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT '发布群最新交接状态快照' AFTER latest_order_status,
ADD COLUMN latest_order_refund_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT '发布群最新退款状态快照' AFTER latest_order_handoff_status,
ADD COLUMN last_attention_message_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一条需客服关注消息ID' AFTER latest_order_refund_status,
ADD COLUMN last_admin_message_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一条客服消息ID' AFTER last_attention_message_id,
ADD KEY idx_chat_conversations_latest_order_status (latest_order_status, last_message_at, id);
UPDATE chat_conversations AS c
JOIN (
SELECT ro.id, ro.listing_id, ro.order_no, ro.status, ro.handoff_status, ro.refund_status
FROM rental_orders AS ro
JOIN (
SELECT listing_id, MAX(id) AS id
FROM rental_orders
GROUP BY listing_id
) AS latest ON latest.id = ro.id
) AS o ON o.listing_id = c.listing_id
SET c.latest_order_id = o.id,
c.latest_order_no = o.order_no,
c.latest_order_status = o.status,
c.latest_order_handoff_status = o.handoff_status,
c.latest_order_refund_status = o.refund_status
WHERE c.order_id IS NULL;
UPDATE chat_conversations AS c
LEFT JOIN (
SELECT conversation_id,
COALESCE(MAX(CASE WHEN admin_attention_type <> '' OR sender_type = 'user' THEN id END), 0) AS attention_id,
COALESCE(MAX(CASE WHEN sender_type = 'admin' THEN id END), 0) AS admin_id
FROM chat_messages
GROUP BY conversation_id
) AS m ON m.conversation_id = c.id
SET c.last_attention_message_id = COALESCE(m.attention_id, 0),
c.last_admin_message_id = COALESCE(m.admin_id, 0);
-- +goose Down
ALTER TABLE chat_conversations
DROP KEY idx_chat_conversations_latest_order_status,
DROP COLUMN last_admin_message_id,
DROP COLUMN last_attention_message_id,
DROP COLUMN latest_order_refund_status,
DROP COLUMN latest_order_handoff_status,
DROP COLUMN latest_order_status,
DROP COLUMN latest_order_no,
DROP COLUMN latest_order_id;
@@ -0,0 +1,58 @@
-- +goose Up
-- 财务仪表盘会按完成/取消日期读取提号,并按调整创建日汇总利润。
-- 补齐复合索引,避免数据增长后统计退化为全表扫描。
-- 该版本曾在部分开发库中部分执行,以下写法兼容已有同名索引后再次运行。
-- +goose StatementBegin
SET @pickup_completed_index_exists = (
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'admin_pickups'
AND index_name = 'idx_admin_pickups_status_completed_at'
);
SET @pickup_completed_index_sql = IF(
@pickup_completed_index_exists = 0,
'ALTER TABLE admin_pickups ADD KEY idx_admin_pickups_status_completed_at (status, completed_at)',
'SELECT 1'
);
PREPARE pickup_completed_index_statement FROM @pickup_completed_index_sql;
EXECUTE pickup_completed_index_statement;
DEALLOCATE PREPARE pickup_completed_index_statement;
SET @pickup_cancelled_index_exists = (
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'admin_pickups'
AND index_name = 'idx_admin_pickups_status_cancelled_at'
);
SET @pickup_cancelled_index_sql = IF(
@pickup_cancelled_index_exists = 0,
'ALTER TABLE admin_pickups ADD KEY idx_admin_pickups_status_cancelled_at (status, cancelled_at)',
'SELECT 1'
);
PREPARE pickup_cancelled_index_statement FROM @pickup_cancelled_index_sql;
EXECUTE pickup_cancelled_index_statement;
DEALLOCATE PREPARE pickup_cancelled_index_statement;
SET @pickup_adjustment_index_exists = (
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'admin_pickup_financial_adjustments'
AND index_name = 'idx_admin_pickup_financial_adjustments_created_at'
);
SET @pickup_adjustment_index_sql = IF(
@pickup_adjustment_index_exists = 0,
'ALTER TABLE admin_pickup_financial_adjustments ADD KEY idx_admin_pickup_financial_adjustments_created_at (created_at)',
'SELECT 1'
);
PREPARE pickup_adjustment_index_statement FROM @pickup_adjustment_index_sql;
EXECUTE pickup_adjustment_index_statement;
DEALLOCATE PREPARE pickup_adjustment_index_statement;
-- +goose StatementEnd
-- +goose Down
-- 补偿迁移不删除索引,避免回滚时误删此前已存在的同名索引。
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE listing_uploads
ADD KEY idx_listing_uploads_created_admin (created_at, matched_admin_id);
-- +goose Down
ALTER TABLE listing_uploads
DROP KEY idx_listing_uploads_created_admin;
@@ -0,0 +1,18 @@
-- +goose Up
-- 000021/000049 创建表时未显式指定排序规则,在 MySQL 8 中会继承
-- utf8mb4_0900_ai_ci。财务出款查询会与 utf8mb4_unicode_ci 的订单、
-- 提现和手工出款表做 UNION,混用排序规则会触发 Error 1271。
ALTER TABLE admin_pickups
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE admin_pickup_financial_adjustments
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose Down
ALTER TABLE admin_pickup_financial_adjustments
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
ALTER TABLE admin_pickups
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
@@ -0,0 +1,13 @@
-- +goose Up
-- 财务仪表盘的待线下出款金额不受日期限制,需要持续读取全部待出款订单。
-- 覆盖查询条件与汇总金额,避免通过低区分度状态索引回表扫描。
ALTER TABLE rental_orders
ADD KEY idx_rental_orders_offline_pending_amount (
settlement_mode,
offline_settlement_status,
offline_settlement_amount_cent
);
-- +goose Down
ALTER TABLE rental_orders
DROP KEY idx_rental_orders_offline_pending_amount;