fix: 增加QQBot消息推送生产迁移
This commit is contained in:
parent
227e5c0899
commit
f8ca44f9d0
4
API.md
4
API.md
@ -459,9 +459,9 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
#### SQL、发布与回滚
|
||||
|
||||
既有环境使用幂等增量入口 `sql/qqbot-init.sql`;只有一次性、可丢弃的全量初始化环境才依次使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布顺序是:备份六表及相关菜单/角色授权行 → 应用对应 SQL 入口 → 验证表、唯一/调度索引、默认模板与权限 → 先验证 API 再发布 Admin → 创建订阅和逐账号绑定 → 使用授权非生产目标完成有界 A→B 验收。
|
||||
既有环境使用本功能幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;包含历史迁移的 `sql/qqbot-init.sql` 不能作为本功能生产迁移。只有一次性、可丢弃的全量初始化环境才依次使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。发布顺序是:备份六表及相关菜单/角色授权行 → 应用对应 SQL 入口 → 验证表、17 个精确索引、默认模板与权限 → 先验证 API 再发布 Admin → 创建订阅和逐账号绑定 → 使用授权非生产目标完成有界 A→B 验收。
|
||||
|
||||
回滚先停用全部发布绑定,再回滚 Admin 和 API,并保留事件、投递及发送日志;Network Agent、端口转发、STUN Keeper 和 DDNS 继续运行。Jenkins/K8s 通过只证明版本已部署,不能替代真实 CRUD、页面或事件到消息的功能验收。当前已有实现和自动化 API 证据,但因缺少安全隔离的本地前置条件,真实本地 CRUD、Admin 页面、数据库支持的 Outbox/DDNS 流程和授权 QQ 投递仍未验证;本次文档变更没有推送、部署或执行生产 SQL。
|
||||
回滚先停用全部发布绑定,再回滚 Admin 和 API,并保留事件、投递及发送日志;Network Agent、端口转发、STUN Keeper 和 DDNS 继续运行。Jenkins/K8s 通过只证明版本已部署,不能替代真实 CRUD、页面或事件到消息的功能验收。每次发布必须分别记录生产 SQL、真实 API、页面、Outbox/DDNS 和授权 QQ 投递证据;没有明确授权的 QQ 群或 QQ 号时不得任选目标,并应把真实投递单独标记为未验证。
|
||||
|
||||
### NapCat Runtime Profile
|
||||
|
||||
|
||||
@ -233,14 +233,14 @@ bash scripts/bangdream-render-smoke.sh --operation-key bangdream.event.stage --t
|
||||
QQBot 系统消息推送按以下顺序发布和回滚:
|
||||
|
||||
1. 备份 `qqbot_message_subscription`、`qqbot_message_template`、`qqbot_message_publish_binding`、`qqbot_message_publish_target`、`qqbot_message_event`、`qqbot_message_delivery`,以及本功能相关的 `admin_menu` / `admin_role_menu` 行。
|
||||
2. 既有环境只应用审查后的幂等增量入口 `sql/qqbot-init.sql`;仅一次性、可丢弃的全量初始化环境按顺序使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`,不要把两种入口混用。
|
||||
3. 验证六表、活动自然键与事件/事件-目标唯一键、事件和投递调度/租约索引、默认模板,以及页面/按钮菜单和角色授权。
|
||||
2. 既有环境只应用本功能的幂等增量入口 `sql/qqbot-message-push-init.sql`,随后执行只读的 `sql/qqbot-message-push-verify.sql`;不要把包含历史迁移的 `sql/qqbot-init.sql` 作为本功能生产迁移。仅一次性、可丢弃的全量初始化环境按顺序使用 `sql/refactor-v3/00-full-schema.sql`、`01-seed-core.sql`、`99-verify.sql`。
|
||||
3. 验证六表、17 个精确索引、默认模板、18 个页面/按钮菜单和活动 `super/admin` 角色授权。
|
||||
4. 先发布并验证 API 健康检查、旧 QQBot 发送能力和新只读接口,再发布并验证 Admin。
|
||||
5. 管理员显式创建订阅,并在每个发布账号中选择模板、配置群聊/私聊目标和启用绑定;随后用授权的非生产目标做一次有界 A→B 端口变化、DDNS 门禁、发送日志和幂等验收。
|
||||
6. 回滚时先停用全部消息推送绑定,再回滚 Admin 和 API;保留事件、投递和 `qqbot_send_log` 历史供审计,不停止 Network Agent、端口转发、STUN Keeper 或 DDNS。
|
||||
7. Jenkins/K8s 成功只属于部署证据,不能代替真实 CRUD、页面、Outbox/DDNS 或 QQ 投递功能验收。
|
||||
|
||||
当前分支已有实现和自动化 API 证据;由于缺少安全隔离的本地前置条件,真实本地 CRUD、Admin 页面、数据库支持的 Outbox/DDNS 流程和授权 QQ 投递仍未验证。本次文档变更没有推送、部署或执行生产 SQL,不能据此声明功能已上线或已完整验收。
|
||||
每次发布记录必须分别写明代码部署、生产 SQL、真实 CRUD、Admin 页面、Outbox/DDNS 和授权 QQ 投递的证据;没有明确授权的 QQ 群或 QQ 号时不得任选目标,且必须把真实投递标记为未验证,不能用 Jenkins/K8s 成功替代。
|
||||
|
||||
## 来源与许可证
|
||||
|
||||
|
||||
@ -1163,12 +1163,12 @@ WHERE NOT EXISTS (
|
||||
);
|
||||
|
||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`) VALUES
|
||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"系统消息订阅"}',1,10),
|
||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"系统消息模板"}',1,11),
|
||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
||||
(2041700000000120461,2041700000000100413,'QqBotMessageSubscriptionList',NULL,NULL,NULL,'QqBot:MessageSubscription:List','button','{"title":"common.list"}',1,0),(2041700000000120462,2041700000000100413,'QqBotMessageSubscriptionCreate',NULL,NULL,NULL,'QqBot:MessageSubscription:Create','button','{"title":"common.create"}',1,0),(2041700000000120463,2041700000000100413,'QqBotMessageSubscriptionUpdate',NULL,NULL,NULL,'QqBot:MessageSubscription:Update','button','{"title":"common.edit"}',1,0),(2041700000000120464,2041700000000100413,'QqBotMessageSubscriptionDelete',NULL,NULL,NULL,'QqBot:MessageSubscription:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120465,2041700000000100413,'QqBotMessageSubscriptionToggle',NULL,NULL,NULL,'QqBot:MessageSubscription:Toggle','button','{"title":"启停"}',1,0),
|
||||
(2041700000000120471,2041700000000100414,'QqBotMessageTemplateList',NULL,NULL,NULL,'QqBot:MessageTemplate:List','button','{"title":"common.list"}',1,0),(2041700000000120472,2041700000000100414,'QqBotMessageTemplateCreate',NULL,NULL,NULL,'QqBot:MessageTemplate:Create','button','{"title":"common.create"}',1,0),(2041700000000120473,2041700000000100414,'QqBotMessageTemplateUpdate',NULL,NULL,NULL,'QqBot:MessageTemplate:Update','button','{"title":"common.edit"}',1,0),(2041700000000120474,2041700000000100414,'QqBotMessageTemplateDelete',NULL,NULL,NULL,'QqBot:MessageTemplate:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120475,2041700000000100414,'QqBotMessageTemplateToggle',NULL,NULL,NULL,'QqBot:MessageTemplate:Toggle','button','{"title":"启停"}',1,0),(2041700000000120476,2041700000000100414,'QqBotMessageTemplatePreview',NULL,NULL,NULL,'QqBot:MessageTemplate:Preview','button','{"title":"预览"}',1,0),
|
||||
(2041700000000120481,2041700000000100410,'QqBotAccountMessagePushList',NULL,NULL,NULL,'QqBot:Account:MessagePush:List','button','{"title":"common.list"}',1,0),(2041700000000120482,2041700000000100410,'QqBotAccountMessagePushCreate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Create','button','{"title":"common.create"}',1,0),(2041700000000120483,2041700000000100410,'QqBotAccountMessagePushUpdate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Update','button','{"title":"common.edit"}',1,0),(2041700000000120484,2041700000000100410,'QqBotAccountMessagePushDelete',NULL,NULL,NULL,'QqBot:Account:MessagePush:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120485,2041700000000100410,'QqBotAccountMessagePushToggle',NULL,NULL,NULL,'QqBot:Account:MessagePush:Toggle','button','{"title":"启停"}',1,0)
|
||||
ON DUPLICATE KEY UPDATE `pid`=VALUES(`pid`),`path`=VALUES(`path`),`component`=VALUES(`component`),`redirect`=VALUES(`redirect`),`auth_code`=VALUES(`auth_code`),`type`=VALUES(`type`),`meta`=VALUES(`meta`),`status`=VALUES(`status`),`sort`=VALUES(`sort`),`is_deleted`=0;
|
||||
ON DUPLICATE KEY UPDATE `pid`=VALUES(`pid`),`name`=VALUES(`name`),`path`=VALUES(`path`),`component`=VALUES(`component`),`redirect`=VALUES(`redirect`),`auth_code`=VALUES(`auth_code`),`type`=VALUES(`type`),`meta`=VALUES(`meta`),`status`=VALUES(`status`),`sort`=VALUES(`sort`),`is_deleted`=0;
|
||||
|
||||
INSERT IGNORE INTO `admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT role.`id`, menu.`id`
|
||||
|
||||
198
sql/qqbot-message-push-init.sql
Normal file
198
sql/qqbot-message-push-init.sql
Normal file
@ -0,0 +1,198 @@
|
||||
-- QQBot 系统消息推送生产增量迁移。
|
||||
-- 仅创建本功能六表、默认模板、菜单和角色授权,不执行历史 QQBot 迁移。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_subscription (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
source_key VARCHAR(128) NOT NULL,
|
||||
source_config JSON NOT NULL,
|
||||
source_config_digest CHAR(64) NOT NULL,
|
||||
active_key VARCHAR(255) NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(500) NULL,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uk_qqbot_message_subscription_active_key (active_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_template (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
source_key VARCHAR(128) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(500) NULL,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_publish_binding (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
subscription_id BIGINT NOT NULL,
|
||||
account_id BIGINT NOT NULL,
|
||||
self_id VARCHAR(64) NOT NULL,
|
||||
template_id BIGINT NOT NULL,
|
||||
active_key VARCHAR(255) NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uk_qqbot_message_publish_binding_active_key (active_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_publish_target (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
binding_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
target_name VARCHAR(120) NULL,
|
||||
active_key VARCHAR(300) NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uk_qqbot_message_publish_target_active_key (active_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_event (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
event_id VARCHAR(128) NOT NULL,
|
||||
source_key VARCHAR(128) NOT NULL,
|
||||
resource_key VARCHAR(128) NOT NULL,
|
||||
occurred_at DATETIME(6) NOT NULL,
|
||||
payload JSON NOT NULL,
|
||||
fanout_status VARCHAR(32) NOT NULL DEFAULT 'accepted',
|
||||
fanout_attempt_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
next_fanout_at DATETIME(6) NULL,
|
||||
fanout_lease_until DATETIME(6) NULL,
|
||||
last_error_code VARCHAR(64) NULL,
|
||||
last_error_message VARCHAR(500) NULL,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uk_qqbot_message_event_event_id (event_id),
|
||||
KEY idx_qqbot_message_event_dispatch (fanout_status, next_fanout_at),
|
||||
KEY idx_qqbot_message_event_lease (fanout_lease_until),
|
||||
KEY idx_qqbot_message_event_source_resource_order (source_key, resource_key, occurred_at, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_message_delivery (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
message_event_id BIGINT NOT NULL,
|
||||
publish_target_id BIGINT NOT NULL,
|
||||
binding_id BIGINT NOT NULL,
|
||||
subscription_id BIGINT NOT NULL,
|
||||
self_id VARCHAR(64) NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
template_id BIGINT NOT NULL,
|
||||
template_content TEXT NOT NULL,
|
||||
variable_snapshot JSON NOT NULL,
|
||||
rendered_message TEXT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
attempt_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
next_attempt_at DATETIME(6) NULL,
|
||||
processing_lease_until DATETIME(6) NULL,
|
||||
send_log_id BIGINT NULL,
|
||||
last_error_code VARCHAR(64) NULL,
|
||||
last_error_message VARCHAR(500) NULL,
|
||||
expires_at DATETIME(6) NOT NULL,
|
||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
UNIQUE KEY uk_qqbot_message_delivery_event_target (message_event_id, publish_target_id),
|
||||
KEY idx_qqbot_message_delivery_dispatch (status, next_attempt_at),
|
||||
KEY idx_qqbot_message_delivery_lease (processing_lease_until),
|
||||
KEY idx_qqbot_message_delivery_history (subscription_id, message_event_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO qqbot_message_template (id, name, source_key, content, enabled, remark, is_deleted)
|
||||
SELECT
|
||||
2041700000000200601,
|
||||
'STUN 映射端口变更默认模板',
|
||||
'network.stun.mapping-port-changed',
|
||||
'当前STUN的端口已变更为${{endpoint}}',
|
||||
1,
|
||||
'系统默认模板',
|
||||
0
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM qqbot_message_template
|
||||
WHERE source_key = 'network.stun.mapping-port-changed'
|
||||
AND name = 'STUN 映射端口变更默认模板'
|
||||
AND is_deleted = 0
|
||||
);
|
||||
|
||||
INSERT INTO admin_menu (
|
||||
id,
|
||||
pid,
|
||||
name,
|
||||
path,
|
||||
component,
|
||||
redirect,
|
||||
auth_code,
|
||||
type,
|
||||
meta,
|
||||
status,
|
||||
sort
|
||||
) VALUES
|
||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
||||
(2041700000000120461,2041700000000100413,'QqBotMessageSubscriptionList',NULL,NULL,NULL,'QqBot:MessageSubscription:List','button','{"title":"common.list"}',1,0),
|
||||
(2041700000000120462,2041700000000100413,'QqBotMessageSubscriptionCreate',NULL,NULL,NULL,'QqBot:MessageSubscription:Create','button','{"title":"common.create"}',1,0),
|
||||
(2041700000000120463,2041700000000100413,'QqBotMessageSubscriptionUpdate',NULL,NULL,NULL,'QqBot:MessageSubscription:Update','button','{"title":"common.edit"}',1,0),
|
||||
(2041700000000120464,2041700000000100413,'QqBotMessageSubscriptionDelete',NULL,NULL,NULL,'QqBot:MessageSubscription:Delete','button','{"title":"common.delete"}',1,0),
|
||||
(2041700000000120465,2041700000000100413,'QqBotMessageSubscriptionToggle',NULL,NULL,NULL,'QqBot:MessageSubscription:Toggle','button','{"title":"启停"}',1,0),
|
||||
(2041700000000120471,2041700000000100414,'QqBotMessageTemplateList',NULL,NULL,NULL,'QqBot:MessageTemplate:List','button','{"title":"common.list"}',1,0),
|
||||
(2041700000000120472,2041700000000100414,'QqBotMessageTemplateCreate',NULL,NULL,NULL,'QqBot:MessageTemplate:Create','button','{"title":"common.create"}',1,0),
|
||||
(2041700000000120473,2041700000000100414,'QqBotMessageTemplateUpdate',NULL,NULL,NULL,'QqBot:MessageTemplate:Update','button','{"title":"common.edit"}',1,0),
|
||||
(2041700000000120474,2041700000000100414,'QqBotMessageTemplateDelete',NULL,NULL,NULL,'QqBot:MessageTemplate:Delete','button','{"title":"common.delete"}',1,0),
|
||||
(2041700000000120475,2041700000000100414,'QqBotMessageTemplateToggle',NULL,NULL,NULL,'QqBot:MessageTemplate:Toggle','button','{"title":"启停"}',1,0),
|
||||
(2041700000000120476,2041700000000100414,'QqBotMessageTemplatePreview',NULL,NULL,NULL,'QqBot:MessageTemplate:Preview','button','{"title":"预览"}',1,0),
|
||||
(2041700000000120481,2041700000000100410,'QqBotAccountMessagePushList',NULL,NULL,NULL,'QqBot:Account:MessagePush:List','button','{"title":"common.list"}',1,0),
|
||||
(2041700000000120482,2041700000000100410,'QqBotAccountMessagePushCreate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Create','button','{"title":"common.create"}',1,0),
|
||||
(2041700000000120483,2041700000000100410,'QqBotAccountMessagePushUpdate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Update','button','{"title":"common.edit"}',1,0),
|
||||
(2041700000000120484,2041700000000100410,'QqBotAccountMessagePushDelete',NULL,NULL,NULL,'QqBot:Account:MessagePush:Delete','button','{"title":"common.delete"}',1,0),
|
||||
(2041700000000120485,2041700000000100410,'QqBotAccountMessagePushToggle',NULL,NULL,NULL,'QqBot:Account:MessagePush:Toggle','button','{"title":"启停"}',1,0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
pid = VALUES(pid),
|
||||
name = VALUES(name),
|
||||
path = VALUES(path),
|
||||
component = VALUES(component),
|
||||
redirect = VALUES(redirect),
|
||||
auth_code = VALUES(auth_code),
|
||||
type = VALUES(type),
|
||||
meta = VALUES(meta),
|
||||
status = VALUES(status),
|
||||
sort = VALUES(sort),
|
||||
is_deleted = 0;
|
||||
|
||||
INSERT IGNORE INTO admin_role_menu (role_id, menu_id)
|
||||
SELECT role.id, menu.id
|
||||
FROM admin_role role
|
||||
CROSS JOIN admin_menu menu
|
||||
WHERE role.role_code IN ('super', 'admin')
|
||||
AND role.status = 1
|
||||
AND role.is_deleted = 0
|
||||
AND menu.id IN (
|
||||
2041700000000100413,
|
||||
2041700000000100414,
|
||||
2041700000000120461,
|
||||
2041700000000120462,
|
||||
2041700000000120463,
|
||||
2041700000000120464,
|
||||
2041700000000120465,
|
||||
2041700000000120471,
|
||||
2041700000000120472,
|
||||
2041700000000120473,
|
||||
2041700000000120474,
|
||||
2041700000000120475,
|
||||
2041700000000120476,
|
||||
2041700000000120481,
|
||||
2041700000000120482,
|
||||
2041700000000120483,
|
||||
2041700000000120484,
|
||||
2041700000000120485
|
||||
)
|
||||
AND menu.status = 1
|
||||
AND menu.is_deleted = 0;
|
||||
226
sql/qqbot-message-push-verify.sql
Normal file
226
sql/qqbot-message-push-verify.sql
Normal file
@ -0,0 +1,226 @@
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name IN (
|
||||
'qqbot_message_subscription',
|
||||
'qqbot_message_template',
|
||||
'qqbot_message_publish_binding',
|
||||
'qqbot_message_publish_target',
|
||||
'qqbot_message_event',
|
||||
'qqbot_message_delivery'
|
||||
)
|
||||
ORDER BY table_name;
|
||||
|
||||
SELECT table_name, index_name
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name IN (
|
||||
'qqbot_message_subscription',
|
||||
'qqbot_message_template',
|
||||
'qqbot_message_publish_binding',
|
||||
'qqbot_message_publish_target',
|
||||
'qqbot_message_event',
|
||||
'qqbot_message_delivery'
|
||||
)
|
||||
ORDER BY table_name, index_name;
|
||||
|
||||
WITH required_index AS (
|
||||
SELECT 'qqbot_message_subscription' AS table_name, 'PRIMARY' AS index_name, 0 AS non_unique, 1 AS seq_in_index, 'id' AS column_name
|
||||
UNION ALL SELECT 'qqbot_message_subscription', 'uk_qqbot_message_subscription_active_key', 0, 1, 'active_key'
|
||||
UNION ALL SELECT 'qqbot_message_template', 'PRIMARY', 0, 1, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_publish_binding', 'PRIMARY', 0, 1, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_publish_binding', 'uk_qqbot_message_publish_binding_active_key', 0, 1, 'active_key'
|
||||
UNION ALL SELECT 'qqbot_message_publish_target', 'PRIMARY', 0, 1, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_publish_target', 'uk_qqbot_message_publish_target_active_key', 0, 1, 'active_key'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'PRIMARY', 0, 1, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'uk_qqbot_message_event_event_id', 0, 1, 'event_id'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_dispatch', 1, 1, 'fanout_status'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_dispatch', 1, 2, 'next_fanout_at'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_lease', 1, 1, 'fanout_lease_until'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', 1, 1, 'source_key'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', 1, 2, 'resource_key'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', 1, 3, 'occurred_at'
|
||||
UNION ALL SELECT 'qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', 1, 4, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'PRIMARY', 0, 1, 'id'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'uk_qqbot_message_delivery_event_target', 0, 1, 'message_event_id'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'uk_qqbot_message_delivery_event_target', 0, 2, 'publish_target_id'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'idx_qqbot_message_delivery_dispatch', 1, 1, 'status'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'idx_qqbot_message_delivery_dispatch', 1, 2, 'next_attempt_at'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'idx_qqbot_message_delivery_lease', 1, 1, 'processing_lease_until'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'idx_qqbot_message_delivery_history', 1, 1, 'subscription_id'
|
||||
UNION ALL SELECT 'qqbot_message_delivery', 'idx_qqbot_message_delivery_history', 1, 2, 'message_event_id'
|
||||
),
|
||||
actual_index AS (
|
||||
SELECT table_name, index_name, non_unique, seq_in_index, column_name
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name IN (
|
||||
'qqbot_message_subscription',
|
||||
'qqbot_message_template',
|
||||
'qqbot_message_publish_binding',
|
||||
'qqbot_message_publish_target',
|
||||
'qqbot_message_event',
|
||||
'qqbot_message_delivery'
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
'qqbot_message_push_required_indexes' AS check_name,
|
||||
COUNT(DISTINCT CONCAT(required_index.table_name, '/', required_index.index_name)) AS expected_index_count,
|
||||
COUNT(*) AS expected_column_count,
|
||||
COUNT(actual_index.index_name) AS matched_column_count,
|
||||
COUNT(*) - COUNT(actual_index.index_name) AS missing_column_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM actual_index unexpected
|
||||
LEFT JOIN required_index expected
|
||||
ON expected.table_name = unexpected.table_name
|
||||
AND expected.index_name = unexpected.index_name
|
||||
AND expected.non_unique = unexpected.non_unique
|
||||
AND expected.seq_in_index = unexpected.seq_in_index
|
||||
AND BINARY expected.column_name = BINARY unexpected.column_name
|
||||
WHERE expected.table_name IS NULL
|
||||
) AS unexpected_column_count
|
||||
FROM required_index
|
||||
LEFT JOIN actual_index
|
||||
ON actual_index.table_name = required_index.table_name
|
||||
AND actual_index.index_name = required_index.index_name
|
||||
AND actual_index.non_unique = required_index.non_unique
|
||||
AND actual_index.seq_in_index = required_index.seq_in_index
|
||||
AND BINARY actual_index.column_name = BINARY required_index.column_name;
|
||||
|
||||
SELECT 'seed_qqbot_message_template' AS check_name, COUNT(*) AS matched_rows
|
||||
FROM qqbot_message_template
|
||||
WHERE id = 2041700000000200601
|
||||
AND BINARY name = BINARY 'STUN 映射端口变更默认模板'
|
||||
AND BINARY source_key = BINARY 'network.stun.mapping-port-changed'
|
||||
AND BINARY content = BINARY '当前STUN的端口已变更为${{endpoint}}'
|
||||
AND enabled = 1
|
||||
AND is_deleted = 0;
|
||||
|
||||
SELECT 'seed_qqbot_message_push_menu' AS check_name, COUNT(*) AS matched_rows
|
||||
FROM admin_menu
|
||||
WHERE id IN (
|
||||
2041700000000100413,
|
||||
2041700000000100414,
|
||||
2041700000000120461,
|
||||
2041700000000120462,
|
||||
2041700000000120463,
|
||||
2041700000000120464,
|
||||
2041700000000120465,
|
||||
2041700000000120471,
|
||||
2041700000000120472,
|
||||
2041700000000120473,
|
||||
2041700000000120474,
|
||||
2041700000000120475,
|
||||
2041700000000120476,
|
||||
2041700000000120481,
|
||||
2041700000000120482,
|
||||
2041700000000120483,
|
||||
2041700000000120484,
|
||||
2041700000000120485
|
||||
)
|
||||
AND status = 1
|
||||
AND is_deleted = 0;
|
||||
|
||||
WITH expected_menu AS (
|
||||
SELECT 2041700000000100413 AS id, 2041700000000100400 AS pid, 'QqBotMessageSubscription' AS name, 'QqBot:MessageSubscription:List' AS auth_code, 10 AS sort
|
||||
UNION ALL SELECT 2041700000000100414, 2041700000000100400, 'QqBotMessageTemplate', 'QqBot:MessageTemplate:List', 11
|
||||
UNION ALL SELECT 2041700000000120461, 2041700000000100413, 'QqBotMessageSubscriptionList', 'QqBot:MessageSubscription:List', 0
|
||||
UNION ALL SELECT 2041700000000120462, 2041700000000100413, 'QqBotMessageSubscriptionCreate', 'QqBot:MessageSubscription:Create', 0
|
||||
UNION ALL SELECT 2041700000000120463, 2041700000000100413, 'QqBotMessageSubscriptionUpdate', 'QqBot:MessageSubscription:Update', 0
|
||||
UNION ALL SELECT 2041700000000120464, 2041700000000100413, 'QqBotMessageSubscriptionDelete', 'QqBot:MessageSubscription:Delete', 0
|
||||
UNION ALL SELECT 2041700000000120465, 2041700000000100413, 'QqBotMessageSubscriptionToggle', 'QqBot:MessageSubscription:Toggle', 0
|
||||
UNION ALL SELECT 2041700000000120471, 2041700000000100414, 'QqBotMessageTemplateList', 'QqBot:MessageTemplate:List', 0
|
||||
UNION ALL SELECT 2041700000000120472, 2041700000000100414, 'QqBotMessageTemplateCreate', 'QqBot:MessageTemplate:Create', 0
|
||||
UNION ALL SELECT 2041700000000120473, 2041700000000100414, 'QqBotMessageTemplateUpdate', 'QqBot:MessageTemplate:Update', 0
|
||||
UNION ALL SELECT 2041700000000120474, 2041700000000100414, 'QqBotMessageTemplateDelete', 'QqBot:MessageTemplate:Delete', 0
|
||||
UNION ALL SELECT 2041700000000120475, 2041700000000100414, 'QqBotMessageTemplateToggle', 'QqBot:MessageTemplate:Toggle', 0
|
||||
UNION ALL SELECT 2041700000000120476, 2041700000000100414, 'QqBotMessageTemplatePreview', 'QqBot:MessageTemplate:Preview', 0
|
||||
UNION ALL SELECT 2041700000000120481, 2041700000000100410, 'QqBotAccountMessagePushList', 'QqBot:Account:MessagePush:List', 0
|
||||
UNION ALL SELECT 2041700000000120482, 2041700000000100410, 'QqBotAccountMessagePushCreate', 'QqBot:Account:MessagePush:Create', 0
|
||||
UNION ALL SELECT 2041700000000120483, 2041700000000100410, 'QqBotAccountMessagePushUpdate', 'QqBot:Account:MessagePush:Update', 0
|
||||
UNION ALL SELECT 2041700000000120484, 2041700000000100410, 'QqBotAccountMessagePushDelete', 'QqBot:Account:MessagePush:Delete', 0
|
||||
UNION ALL SELECT 2041700000000120485, 2041700000000100410, 'QqBotAccountMessagePushToggle', 'QqBot:Account:MessagePush:Toggle', 0
|
||||
)
|
||||
SELECT 'seed_qqbot_message_push_menu_mismatch' AS check_name, COUNT(*) AS invalid_rows
|
||||
FROM expected_menu expected
|
||||
LEFT JOIN admin_menu actual ON actual.id = expected.id
|
||||
WHERE actual.id IS NULL
|
||||
OR NOT (BINARY actual.name <=> BINARY expected.name)
|
||||
OR NOT (BINARY actual.auth_code <=> BINARY expected.auth_code)
|
||||
OR actual.pid <> expected.pid
|
||||
OR actual.sort <> expected.sort
|
||||
OR actual.status <> 1
|
||||
OR actual.is_deleted <> 0;
|
||||
|
||||
SELECT 'seed_qqbot_message_push_page_menu' AS check_name, COUNT(*) AS matched_rows
|
||||
FROM admin_menu
|
||||
WHERE (
|
||||
id = 2041700000000100413
|
||||
AND pid = 2041700000000100400
|
||||
AND BINARY name = BINARY 'QqBotMessageSubscription'
|
||||
AND BINARY path = BINARY '/qqbot/message-subscription'
|
||||
AND BINARY component = BINARY '/qqbot/message-subscription/list'
|
||||
AND BINARY auth_code = BINARY 'QqBot:MessageSubscription:List'
|
||||
AND BINARY JSON_UNQUOTE(JSON_EXTRACT(meta, '$.title')) = BINARY '消息订阅'
|
||||
AND sort = 10
|
||||
)
|
||||
OR (
|
||||
id = 2041700000000100414
|
||||
AND pid = 2041700000000100400
|
||||
AND BINARY name = BINARY 'QqBotMessageTemplate'
|
||||
AND BINARY path = BINARY '/qqbot/message-template'
|
||||
AND BINARY component = BINARY '/qqbot/message-template/list'
|
||||
AND BINARY auth_code = BINARY 'QqBot:MessageTemplate:List'
|
||||
AND BINARY JSON_UNQUOTE(JSON_EXTRACT(meta, '$.title')) = BINARY '消息模板'
|
||||
AND sort = 11
|
||||
);
|
||||
|
||||
SELECT 'seed_qqbot_message_push_role_grant_missing' AS check_name, COUNT(*) AS missing_rows
|
||||
FROM admin_role role
|
||||
CROSS JOIN admin_menu menu
|
||||
LEFT JOIN admin_role_menu role_menu
|
||||
ON role_menu.role_id = role.id
|
||||
AND role_menu.menu_id = menu.id
|
||||
WHERE role.role_code IN ('super', 'admin')
|
||||
AND role.status = 1
|
||||
AND role.is_deleted = 0
|
||||
AND menu.id IN (
|
||||
2041700000000100413,
|
||||
2041700000000100414,
|
||||
2041700000000120461,
|
||||
2041700000000120462,
|
||||
2041700000000120463,
|
||||
2041700000000120464,
|
||||
2041700000000120465,
|
||||
2041700000000120471,
|
||||
2041700000000120472,
|
||||
2041700000000120473,
|
||||
2041700000000120474,
|
||||
2041700000000120475,
|
||||
2041700000000120476,
|
||||
2041700000000120481,
|
||||
2041700000000120482,
|
||||
2041700000000120483,
|
||||
2041700000000120484,
|
||||
2041700000000120485
|
||||
)
|
||||
AND menu.status = 1
|
||||
AND menu.is_deleted = 0
|
||||
AND role_menu.menu_id IS NULL;
|
||||
|
||||
SELECT 'duplicate_event_id_groups' AS check_name, COUNT(*) AS invalid_rows
|
||||
FROM (
|
||||
SELECT event_id
|
||||
FROM qqbot_message_event
|
||||
GROUP BY event_id
|
||||
HAVING COUNT(*) > 1
|
||||
) duplicate_events;
|
||||
|
||||
SELECT 'duplicate_event_target_groups' AS check_name, COUNT(*) AS invalid_rows
|
||||
FROM (
|
||||
SELECT message_event_id, publish_target_id
|
||||
FROM qqbot_message_delivery
|
||||
GROUP BY message_event_id, publish_target_id
|
||||
HAVING COUNT(*) > 1
|
||||
) duplicate_deliveries;
|
||||
@ -641,12 +641,12 @@ WHERE NOT EXISTS (
|
||||
);
|
||||
|
||||
INSERT INTO admin_menu (id, pid, name, path, component, redirect, auth_code, type, meta, status, sort) VALUES
|
||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"系统消息订阅"}',1,10),
|
||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"系统消息模板"}',1,11),
|
||||
(2041700000000100413,2041700000000100400,'QqBotMessageSubscription','/qqbot/message-subscription','/qqbot/message-subscription/list',NULL,'QqBot:MessageSubscription:List','menu','{"icon":"lucide:bell-ring","title":"消息订阅"}',1,10),
|
||||
(2041700000000100414,2041700000000100400,'QqBotMessageTemplate','/qqbot/message-template','/qqbot/message-template/list',NULL,'QqBot:MessageTemplate:List','menu','{"icon":"lucide:message-square-plus","title":"消息模板"}',1,11),
|
||||
(2041700000000120461,2041700000000100413,'QqBotMessageSubscriptionList',NULL,NULL,NULL,'QqBot:MessageSubscription:List','button','{"title":"common.list"}',1,0),(2041700000000120462,2041700000000100413,'QqBotMessageSubscriptionCreate',NULL,NULL,NULL,'QqBot:MessageSubscription:Create','button','{"title":"common.create"}',1,0),(2041700000000120463,2041700000000100413,'QqBotMessageSubscriptionUpdate',NULL,NULL,NULL,'QqBot:MessageSubscription:Update','button','{"title":"common.edit"}',1,0),(2041700000000120464,2041700000000100413,'QqBotMessageSubscriptionDelete',NULL,NULL,NULL,'QqBot:MessageSubscription:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120465,2041700000000100413,'QqBotMessageSubscriptionToggle',NULL,NULL,NULL,'QqBot:MessageSubscription:Toggle','button','{"title":"启停"}',1,0),
|
||||
(2041700000000120471,2041700000000100414,'QqBotMessageTemplateList',NULL,NULL,NULL,'QqBot:MessageTemplate:List','button','{"title":"common.list"}',1,0),(2041700000000120472,2041700000000100414,'QqBotMessageTemplateCreate',NULL,NULL,NULL,'QqBot:MessageTemplate:Create','button','{"title":"common.create"}',1,0),(2041700000000120473,2041700000000100414,'QqBotMessageTemplateUpdate',NULL,NULL,NULL,'QqBot:MessageTemplate:Update','button','{"title":"common.edit"}',1,0),(2041700000000120474,2041700000000100414,'QqBotMessageTemplateDelete',NULL,NULL,NULL,'QqBot:MessageTemplate:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120475,2041700000000100414,'QqBotMessageTemplateToggle',NULL,NULL,NULL,'QqBot:MessageTemplate:Toggle','button','{"title":"启停"}',1,0),(2041700000000120476,2041700000000100414,'QqBotMessageTemplatePreview',NULL,NULL,NULL,'QqBot:MessageTemplate:Preview','button','{"title":"预览"}',1,0),
|
||||
(2041700000000120481,2041700000000100410,'QqBotAccountMessagePushList',NULL,NULL,NULL,'QqBot:Account:MessagePush:List','button','{"title":"common.list"}',1,0),(2041700000000120482,2041700000000100410,'QqBotAccountMessagePushCreate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Create','button','{"title":"common.create"}',1,0),(2041700000000120483,2041700000000100410,'QqBotAccountMessagePushUpdate',NULL,NULL,NULL,'QqBot:Account:MessagePush:Update','button','{"title":"common.edit"}',1,0),(2041700000000120484,2041700000000100410,'QqBotAccountMessagePushDelete',NULL,NULL,NULL,'QqBot:Account:MessagePush:Delete','button','{"title":"common.delete"}',1,0),(2041700000000120485,2041700000000100410,'QqBotAccountMessagePushToggle',NULL,NULL,NULL,'QqBot:Account:MessagePush:Toggle','button','{"title":"启停"}',1,0)
|
||||
ON DUPLICATE KEY UPDATE pid = VALUES(pid), path = VALUES(path), component = VALUES(component), redirect = VALUES(redirect), auth_code = VALUES(auth_code), type = VALUES(type), meta = VALUES(meta), status = VALUES(status), sort = VALUES(sort), is_deleted = 0;
|
||||
ON DUPLICATE KEY UPDATE pid = VALUES(pid), name = VALUES(name), path = VALUES(path), component = VALUES(component), redirect = VALUES(redirect), auth_code = VALUES(auth_code), type = VALUES(type), meta = VALUES(meta), status = VALUES(status), sort = VALUES(sort), is_deleted = 0;
|
||||
|
||||
INSERT IGNORE INTO admin_role_menu (role_id, menu_id)
|
||||
SELECT role.id, menu.id
|
||||
|
||||
@ -401,8 +401,8 @@ VALUES
|
||||
|
||||
INSERT INTO `admin_menu` (`id`, `pid`, `name`, `path`, `component`, `redirect`, `auth_code`, `type`, `meta`, `status`, `sort`)
|
||||
VALUES
|
||||
(2041700000000100413, 2041700000000100400, 'QqBotMessageSubscription', '/qqbot/message-subscription', '/qqbot/message-subscription/list', NULL, 'QqBot:MessageSubscription:List', 'menu', '{"icon":"lucide:bell-ring","title":"系统消息订阅"}', 1, 10),
|
||||
(2041700000000100414, 2041700000000100400, 'QqBotMessageTemplate', '/qqbot/message-template', '/qqbot/message-template/list', NULL, 'QqBot:MessageTemplate:List', 'menu', '{"icon":"lucide:message-square-plus","title":"系统消息模板"}', 1, 11),
|
||||
(2041700000000100413, 2041700000000100400, 'QqBotMessageSubscription', '/qqbot/message-subscription', '/qqbot/message-subscription/list', NULL, 'QqBot:MessageSubscription:List', 'menu', '{"icon":"lucide:bell-ring","title":"消息订阅"}', 1, 10),
|
||||
(2041700000000100414, 2041700000000100400, 'QqBotMessageTemplate', '/qqbot/message-template', '/qqbot/message-template/list', NULL, 'QqBot:MessageTemplate:List', 'menu', '{"icon":"lucide:message-square-plus","title":"消息模板"}', 1, 11),
|
||||
(2041700000000120461, 2041700000000100413, 'QqBotMessageSubscriptionList', NULL, NULL, NULL, 'QqBot:MessageSubscription:List', 'button', '{"title":"common.list"}', 1, 0),
|
||||
(2041700000000120462, 2041700000000100413, 'QqBotMessageSubscriptionCreate', NULL, NULL, NULL, 'QqBot:MessageSubscription:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120463, 2041700000000100413, 'QqBotMessageSubscriptionUpdate', NULL, NULL, NULL, 'QqBot:MessageSubscription:Update', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
@ -420,7 +420,7 @@ VALUES
|
||||
(2041700000000120484, 2041700000000100410, 'QqBotAccountMessagePushDelete', NULL, NULL, NULL, 'QqBot:Account:MessagePush:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
(2041700000000120485, 2041700000000100410, 'QqBotAccountMessagePushToggle', NULL, NULL, NULL, 'QqBot:Account:MessagePush:Toggle', 'button', '{"title":"启停"}', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`pid` = VALUES(`pid`), `path` = VALUES(`path`), `component` = VALUES(`component`), `redirect` = VALUES(`redirect`), `auth_code` = VALUES(`auth_code`), `type` = VALUES(`type`), `meta` = VALUES(`meta`), `status` = VALUES(`status`), `sort` = VALUES(`sort`), `is_deleted` = 0;
|
||||
`pid` = VALUES(`pid`), `name` = VALUES(`name`), `path` = VALUES(`path`), `component` = VALUES(`component`), `redirect` = VALUES(`redirect`), `auth_code` = VALUES(`auth_code`), `type` = VALUES(`type`), `meta` = VALUES(`meta`), `status` = VALUES(`status`), `sort` = VALUES(`sort`), `is_deleted` = 0;
|
||||
|
||||
INSERT IGNORE INTO `admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT role.`id`, menu.`id`
|
||||
|
||||
@ -166,14 +166,17 @@ WHERE status IN ('waiting_ddns', 'pending', 'processing', 'retry');
|
||||
|
||||
Before applying SQL, take a transaction-consistent backup of all six
|
||||
`qqbot_message_*` push tables plus the relevant `admin_menu` and
|
||||
`admin_role_menu` rows. Roll back by disabling publish bindings first, then
|
||||
rolling back Admin and API while retaining events, deliveries, and send logs;
|
||||
do not use `DROP TABLE` or history deletion as an application rollback.
|
||||
`admin_role_menu` rows. Existing environments must apply only
|
||||
`sql/qqbot-message-push-init.sql`, followed by the read-only
|
||||
`sql/qqbot-message-push-verify.sql`; the broader `sql/qqbot-init.sql` contains
|
||||
historical migrations and is not the production entry for this feature. Roll
|
||||
back by disabling publish bindings first, then rolling back Admin and API while
|
||||
retaining events, deliveries, and send logs; do not use `DROP TABLE` or history
|
||||
deletion as an application rollback.
|
||||
|
||||
Verification output must contain only counts or ID/index summaries. Never
|
||||
select/project or print raw `payload`, `rendered_message`, `target_id`,
|
||||
credentials, provider objects, or production values. The implementation and
|
||||
automated API evidence exist, but real local CRUD, browser pages,
|
||||
database-backed Outbox/DDNS flow, and authorized QQ delivery remain unverified
|
||||
because safe local prerequisites are unavailable; this documentation change
|
||||
performed no push, deployment, or production SQL.
|
||||
credentials, provider objects, or production values. Release evidence must
|
||||
record database, HTTP, browser, Outbox/DDNS, and authorized QQ delivery gates
|
||||
separately. Never choose a QQ target without explicit authorization, and never
|
||||
use deployment success as a substitute for an unverified delivery gate.
|
||||
|
||||
@ -9,6 +9,16 @@ type MenuEntry = readonly [
|
||||
sort: string,
|
||||
];
|
||||
|
||||
type MessageMenuTitle = readonly [id: string, title: string];
|
||||
|
||||
type IndexDefinition = readonly [
|
||||
tableName: string,
|
||||
indexName: string,
|
||||
nonUnique: string,
|
||||
sequence: string,
|
||||
columnName: string,
|
||||
];
|
||||
|
||||
const messagePushTableDefinitions = {
|
||||
qqbot_message_subscription: {
|
||||
columns: [
|
||||
@ -118,6 +128,38 @@ const menuEntries: readonly MenuEntry[] = [
|
||||
['2041700000000120485', '2041700000000100410', 'QqBotAccountMessagePushToggle', 'QqBot:Account:MessagePush:Toggle', '0'],
|
||||
];
|
||||
|
||||
const messageMenuTitles: readonly MessageMenuTitle[] = [
|
||||
['2041700000000100413', '消息订阅'],
|
||||
['2041700000000100414', '消息模板'],
|
||||
];
|
||||
|
||||
const requiredIndexDefinitions: readonly IndexDefinition[] = [
|
||||
['qqbot_message_subscription', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_subscription', 'uk_qqbot_message_subscription_active_key', '0', '1', 'active_key'],
|
||||
['qqbot_message_template', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_publish_binding', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_publish_binding', 'uk_qqbot_message_publish_binding_active_key', '0', '1', 'active_key'],
|
||||
['qqbot_message_publish_target', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_publish_target', 'uk_qqbot_message_publish_target_active_key', '0', '1', 'active_key'],
|
||||
['qqbot_message_event', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_event', 'uk_qqbot_message_event_event_id', '0', '1', 'event_id'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_dispatch', '1', '1', 'fanout_status'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_dispatch', '1', '2', 'next_fanout_at'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_lease', '1', '1', 'fanout_lease_until'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', '1', '1', 'source_key'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', '1', '2', 'resource_key'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', '1', '3', 'occurred_at'],
|
||||
['qqbot_message_event', 'idx_qqbot_message_event_source_resource_order', '1', '4', 'id'],
|
||||
['qqbot_message_delivery', 'PRIMARY', '0', '1', 'id'],
|
||||
['qqbot_message_delivery', 'uk_qqbot_message_delivery_event_target', '0', '1', 'message_event_id'],
|
||||
['qqbot_message_delivery', 'uk_qqbot_message_delivery_event_target', '0', '2', 'publish_target_id'],
|
||||
['qqbot_message_delivery', 'idx_qqbot_message_delivery_dispatch', '1', '1', 'status'],
|
||||
['qqbot_message_delivery', 'idx_qqbot_message_delivery_dispatch', '1', '2', 'next_attempt_at'],
|
||||
['qqbot_message_delivery', 'idx_qqbot_message_delivery_lease', '1', '1', 'processing_lease_until'],
|
||||
['qqbot_message_delivery', 'idx_qqbot_message_delivery_history', '1', '1', 'subscription_id'],
|
||||
['qqbot_message_delivery', 'idx_qqbot_message_delivery_history', '1', '2', 'message_event_id'],
|
||||
];
|
||||
|
||||
const advanceSqlQuoteState = (sql: string, index: number, quoted: boolean) => {
|
||||
if (sql[index] !== "'") return { quoted, skipNext: false };
|
||||
|
||||
@ -253,6 +295,19 @@ const extractMessagePushMenuRows = (sql: string) => {
|
||||
] as MenuEntry);
|
||||
};
|
||||
|
||||
const extractMessagePushMenuTitles = (sql: string): MessageMenuTitle[] => {
|
||||
const statements = [...sql.matchAll(/insert into admin_menu \([^)]*\) values (.*?) on duplicate key update/gs)];
|
||||
const statement = statements.find((candidate) => candidate[1].includes(menuEntries[0][0]));
|
||||
expect(statement).toBeTruthy();
|
||||
return extractSqlTuples(statement?.[1] || '')
|
||||
.map(splitSqlTuple)
|
||||
.filter((fields) => messageMenuTitles.some(([id]) => id === unquoteSqlValue(fields[0])))
|
||||
.map((fields) => [
|
||||
unquoteSqlValue(fields[0]),
|
||||
JSON.parse(unquoteSqlValue(fields[8])).title,
|
||||
] as MessageMenuTitle);
|
||||
};
|
||||
|
||||
const extractMessagePushMenuRoleGrantStatement = (sql: string) => {
|
||||
const statements = splitSqlStatements(sql);
|
||||
const menuSeedIndex = statements.findIndex((statement) =>
|
||||
@ -299,6 +354,23 @@ const extractVerificationStatement = (sql: string, checkName: string) => {
|
||||
return statement || '';
|
||||
};
|
||||
|
||||
const extractRequiredIndexDefinitions = (statement: string): IndexDefinition[] =>
|
||||
[...statement.matchAll(/(?:select|union all select) '([^']+)'(?: as table_name)?, '([^']+)'(?: as index_name)?, (\d+)(?: as non_unique)?, (\d+)(?: as seq_in_index)?, '([^']+)'(?: as column_name)?/g)]
|
||||
.map(([, tableName, indexName, nonUnique, sequence, columnName]) =>
|
||||
[tableName, indexName, nonUnique, sequence, columnName] as IndexDefinition);
|
||||
|
||||
const summarizeIndexDefinitions = (
|
||||
required: readonly IndexDefinition[],
|
||||
actual: readonly IndexDefinition[],
|
||||
) => {
|
||||
const requiredKeys = new Set(required.map((definition) => definition.join('\u0000')));
|
||||
const actualKeys = new Set(actual.map((definition) => definition.join('\u0000')));
|
||||
return {
|
||||
missing: [...requiredKeys].filter((key) => !actualKeys.has(key)).length,
|
||||
unexpected: [...actualKeys].filter((key) => !requiredKeys.has(key)).length,
|
||||
};
|
||||
};
|
||||
|
||||
const extractExpectedMenuCte = (statement: string) => {
|
||||
const cteStart = statement.match(/with expected_menu as \(/);
|
||||
expect(cteStart).toBeTruthy();
|
||||
@ -334,6 +406,8 @@ const extractExpectedMenuIds = (statement: string) =>
|
||||
|
||||
describe('QQBot message-push SQL contract', () => {
|
||||
const bootstrapSql = readNormalizedSql('sql/qqbot-init.sql');
|
||||
const migrationSql = readNormalizedSql('sql/qqbot-message-push-init.sql');
|
||||
const migrationVerifySql = readNormalizedSql('sql/qqbot-message-push-verify.sql');
|
||||
const schemaSql = readNormalizedSql('sql/refactor-v3/00-full-schema.sql');
|
||||
const seedSql = readNormalizedSql('sql/refactor-v3/01-seed-core.sql');
|
||||
const verifySql = readNormalizedSql('sql/refactor-v3/99-verify.sql');
|
||||
@ -373,7 +447,7 @@ describe('QQBot message-push SQL contract', () => {
|
||||
|
||||
it('keeps each table DDL and index tuple exact in current and refactor schemas', () => {
|
||||
for (const [table, expected] of Object.entries(messagePushTableDefinitions)) {
|
||||
for (const sql of [bootstrapSql, schemaSql]) {
|
||||
for (const sql of [bootstrapSql, schemaSql, migrationSql]) {
|
||||
const block = extractCreateTableBlock(sql, table);
|
||||
let expectedColumns = sql === bootstrapSql
|
||||
? expected.columns.map((column) => column.endsWith(' null') && !column.endsWith('not null')
|
||||
@ -381,7 +455,7 @@ describe('QQBot message-push SQL contract', () => {
|
||||
: column)
|
||||
: expected.columns;
|
||||
const expectedIndexes = [...expected.indexes];
|
||||
if (sql === schemaSql) {
|
||||
if (sql !== bootstrapSql) {
|
||||
expectedColumns = expectedColumns.map((column, index) => index === 0
|
||||
? `${column} primary key`
|
||||
: column);
|
||||
@ -408,7 +482,7 @@ describe('QQBot message-push SQL contract', () => {
|
||||
and name = 'STUN 映射端口变更默认模板' and is_deleted = 0
|
||||
);
|
||||
`);
|
||||
for (const sql of [bootstrapSql, seedSql]) expect(sql).toContain(expectedTemplate);
|
||||
for (const sql of [bootstrapSql, seedSql, migrationSql]) expect(sql).toContain(expectedTemplate);
|
||||
});
|
||||
|
||||
it('verifies the default template stable values with case-sensitive text comparisons', () => {
|
||||
@ -433,16 +507,23 @@ describe('QQBot message-push SQL contract', () => {
|
||||
});
|
||||
|
||||
it('keeps every stable menu tuple together and exactly once in every seed file', () => {
|
||||
for (const sql of [bootstrapSql, seedSql, vbenSql]) {
|
||||
for (const sql of [bootstrapSql, seedSql, vbenSql, migrationSql]) {
|
||||
const rows = extractMessagePushMenuRows(sql);
|
||||
expect(rows).toHaveLength(18);
|
||||
expect(new Set(rows.map(([id]) => id)).size).toBe(18);
|
||||
expect(rows).toEqual(menuEntries);
|
||||
expect(sql).toMatch(/name\s*=\s*values\(name\)/);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the production menu titles aligned with the Admin route titles', () => {
|
||||
for (const sql of [bootstrapSql, seedSql, vbenSql, migrationSql]) {
|
||||
expect(extractMessagePushMenuTitles(sql)).toEqual(messageMenuTitles);
|
||||
}
|
||||
});
|
||||
|
||||
it('grants only the seeded message-push menus to active super and admin roles after every menu seed', () => {
|
||||
for (const sql of [bootstrapSql, seedSql, vbenSql]) {
|
||||
for (const sql of [bootstrapSql, seedSql, vbenSql, migrationSql]) {
|
||||
const roleGrant = extractMessagePushMenuRoleGrantStatement(sql);
|
||||
expect(roleGrant).toContain('insert ignore into admin_role_menu (role_id, menu_id)');
|
||||
expect(extractMenuRoleGrantIds(roleGrant)).toEqual(menuEntries.map(([id]) => id));
|
||||
@ -454,16 +535,71 @@ describe('QQBot message-push SQL contract', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the production migration scoped to message-push creation and idempotent seeds', () => {
|
||||
expect(migrationSql).not.toMatch(/\b(?:alter table|drop table|drop column|truncate table|delete from)\b/);
|
||||
expect(migrationSql).not.toContain('set foreign_key_checks = 0');
|
||||
expect(migrationSql.match(/create table if not exists qqbot_message_/g)).toHaveLength(6);
|
||||
expect(migrationSql).toContain('on duplicate key update');
|
||||
expect(migrationSql).toContain('insert ignore into admin_role_menu');
|
||||
});
|
||||
|
||||
it('keeps the production verification scoped to safe summaries', () => {
|
||||
for (const table of Object.keys(messagePushTableDefinitions)) {
|
||||
expect(migrationVerifySql).toContain(`'${table}'`);
|
||||
}
|
||||
expect(extractVerificationStatement(
|
||||
migrationVerifySql,
|
||||
'seed_qqbot_message_template',
|
||||
)).toContain("binary content = binary '当前STUN的端口已变更为${{endpoint}}'");
|
||||
expect(extractVerificationStatement(
|
||||
migrationVerifySql,
|
||||
'seed_qqbot_message_push_menu',
|
||||
)).toContain('count(*) as matched_rows');
|
||||
const requiredIndexes = extractVerificationStatement(
|
||||
migrationVerifySql,
|
||||
'qqbot_message_push_required_indexes',
|
||||
);
|
||||
expect(extractRequiredIndexDefinitions(requiredIndexes)).toEqual(requiredIndexDefinitions);
|
||||
expect(requiredIndexes).toContain('actual_index.non_unique = required_index.non_unique');
|
||||
expect(requiredIndexes).toContain('actual_index.seq_in_index = required_index.seq_in_index');
|
||||
expect(requiredIndexes).toContain('binary actual_index.column_name = binary required_index.column_name');
|
||||
expect(requiredIndexes).toContain('unexpected_column_count');
|
||||
const pageMenus = extractVerificationStatement(
|
||||
migrationVerifySql,
|
||||
'seed_qqbot_message_push_page_menu',
|
||||
);
|
||||
expect(pageMenus).toContain("binary json_unquote(json_extract(meta, '$.title')) = binary '消息订阅'");
|
||||
expect(pageMenus).toContain("binary json_unquote(json_extract(meta, '$.title')) = binary '消息模板'");
|
||||
expect(extractVerificationStatement(
|
||||
migrationVerifySql,
|
||||
'seed_qqbot_message_push_role_grant_missing',
|
||||
)).toContain('role_menu.menu_id is null');
|
||||
expect(migrationVerifySql).not.toMatch(/\b(?:insert|update|delete|alter|drop|truncate|replace)\b/);
|
||||
});
|
||||
|
||||
it('rejects a same-name index whose uniqueness, sequence, or column definition drifts', () => {
|
||||
const driftedDefinitions = requiredIndexDefinitions.map((definition) =>
|
||||
definition[1] === 'idx_qqbot_message_event_dispatch' && definition[3] === '2'
|
||||
? [definition[0], definition[1], definition[2], definition[3], 'occurred_at'] as IndexDefinition
|
||||
: definition);
|
||||
expect(summarizeIndexDefinitions(requiredIndexDefinitions, driftedDefinitions)).toEqual({
|
||||
missing: 1,
|
||||
unexpected: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses an exact mismatch CTE and null-safe predicates for every stable menu tuple', () => {
|
||||
const mismatch = extractVerificationStatement(verifySql, 'seed_qqbot_message_push_menu_mismatch');
|
||||
expect(extractMismatchMenuRows(mismatch)).toEqual(menuEntries);
|
||||
expect(mismatch).toContain('left join admin_menu actual on actual.id = expected.id');
|
||||
expect(mismatch).toContain('not (binary actual.name <=> binary expected.name)');
|
||||
expect(mismatch).toContain('not (binary actual.auth_code <=> binary expected.auth_code)');
|
||||
expect(mismatch).toContain('actual.pid <> expected.pid');
|
||||
expect(mismatch).toContain('actual.sort <> expected.sort');
|
||||
expect(mismatch).toContain('actual.status <> 1');
|
||||
expect(mismatch).toContain('actual.is_deleted <> 0');
|
||||
for (const sql of [verifySql, migrationVerifySql]) {
|
||||
const mismatch = extractVerificationStatement(sql, 'seed_qqbot_message_push_menu_mismatch');
|
||||
expect(extractMismatchMenuRows(mismatch)).toEqual(menuEntries);
|
||||
expect(mismatch).toContain('left join admin_menu actual on actual.id = expected.id');
|
||||
expect(mismatch).toContain('not (binary actual.name <=> binary expected.name)');
|
||||
expect(mismatch).toContain('not (binary actual.auth_code <=> binary expected.auth_code)');
|
||||
expect(mismatch).toContain('actual.pid <> expected.pid');
|
||||
expect(mismatch).toContain('actual.sort <> expected.sort');
|
||||
expect(mismatch).toContain('actual.status <> 1');
|
||||
expect(mismatch).toContain('actual.is_deleted <> 0');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses an exact cardinality CTE with expected, actual, and missing counts', () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user