feat: 建立QQBot消息推送持久化模型

This commit is contained in:
sunlei 2026-07-24 03:29:52 +08:00
parent ddc0e9580b
commit 5b34a4d8b1
17 changed files with 663 additions and 0 deletions

View File

@ -1106,6 +1106,76 @@ SELECT role.`id`, menu.`id`
FROM `admin_role` role
JOIN `admin_menu` menu ON menu.`name` LIKE 'QqBot%'
WHERE role.`role_code` IN ('super', 'admin')
AND role.`status` = 1
AND role.`is_deleted` = 0
AND menu.`is_deleted` = 0;
CREATE TABLE IF NOT EXISTS `qqbot_message_subscription` (
`id` bigint NOT NULL, `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) DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT 1, `remark` varchar(500) DEFAULT 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),
PRIMARY KEY (`id`), 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, `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) DEFAULT 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), PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `qqbot_message_publish_binding` (
`id` bigint NOT NULL, `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) DEFAULT 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),
PRIMARY KEY (`id`), 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, `binding_id` bigint NOT NULL, `target_type` varchar(16) NOT NULL, `target_id` varchar(64) NOT NULL, `target_name` varchar(120) DEFAULT NULL,
`active_key` varchar(300) DEFAULT 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),
PRIMARY KEY (`id`), 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, `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) DEFAULT NULL, `fanout_lease_until` datetime(6) DEFAULT NULL, `last_error_code` varchar(64) DEFAULT NULL, `last_error_message` varchar(500) DEFAULT 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),
PRIMARY KEY (`id`), 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `qqbot_message_delivery` (
`id` bigint NOT NULL, `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) DEFAULT NULL, `processing_lease_until` datetime(6) DEFAULT NULL, `send_log_id` bigint DEFAULT NULL, `last_error_code` varchar(64) DEFAULT NULL, `last_error_message` varchar(500) DEFAULT 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),
PRIMARY KEY (`id`), 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`),`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
JOIN `admin_menu` menu ON menu.`name` LIKE 'QqBot%'
WHERE role.`role_code` IN ('super', 'admin')
AND role.`status` = 1
AND role.`is_deleted` = 0
AND menu.`is_deleted` = 0;

View File

@ -1242,3 +1242,107 @@ CREATE TABLE IF NOT EXISTS qqbot_napcat_webui_gateway_audit (
KEY idx_napcat_webui_gateway_audit_account_event (account_id, event_type),
KEY idx_napcat_webui_gateway_audit_admin_time (admin_user_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
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)
) 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;

View File

@ -630,3 +630,20 @@ ON DUPLICATE KEY UPDATE
enabled = VALUES(enabled),
cooldown_seconds = VALUES(cooldown_seconds),
is_deleted = 0;
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), 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;

View File

@ -22,6 +22,29 @@ SELECT 'napcat_login_event' AS table_name, COUNT(*) AS row_count FROM napcat_log
SELECT 'napcat_risk_mode' AS table_name, COUNT(*) AS row_count FROM napcat_risk_mode;
SELECT 'qqbot_plugin_task' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task;
SELECT 'qqbot_plugin_task_run' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task_run;
SELECT 'qqbot_message_subscription' AS table_name, COUNT(*) AS row_count FROM qqbot_message_subscription;
SELECT 'qqbot_message_template' AS table_name, COUNT(*) AS row_count FROM qqbot_message_template;
SELECT 'qqbot_message_publish_binding' AS table_name, COUNT(*) AS row_count FROM qqbot_message_publish_binding;
SELECT 'qqbot_message_publish_target' AS table_name, COUNT(*) AS row_count FROM qqbot_message_publish_target;
SELECT 'qqbot_message_event' AS table_name, COUNT(*) AS row_count FROM qqbot_message_event;
SELECT 'qqbot_message_delivery' AS table_name, COUNT(*) AS row_count FROM qqbot_message_delivery;
SELECT 'seed_qqbot_message_template' AS check_name, COUNT(*) AS matched_rows
FROM qqbot_message_template
WHERE id = 2041700000000200601
AND name = 'STUN 映射端口变更默认模板'
AND source_key = 'network.stun.mapping-port-changed'
AND content = '当前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 auth_code IN (
'QqBot:MessageSubscription:List', 'QqBot:MessageSubscription:Create', 'QqBot:MessageSubscription:Update', 'QqBot:MessageSubscription:Delete', 'QqBot:MessageSubscription:Toggle',
'QqBot:MessageTemplate:List', 'QqBot:MessageTemplate:Create', 'QqBot:MessageTemplate:Update', 'QqBot:MessageTemplate:Delete', 'QqBot:MessageTemplate:Toggle', 'QqBot:MessageTemplate:Preview',
'QqBot:Account:MessagePush:List', 'QqBot:Account:MessagePush:Create', 'QqBot:Account:MessagePush:Update', 'QqBot:Account:MessagePush:Delete', 'QqBot:Account:MessagePush:Toggle'
) AND status = 1 AND is_deleted = 0;
SELECT 'qqbot_napcat_webui_gateway_audit table exists' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.tables

View File

@ -399,4 +399,36 @@ VALUES
(2041700000000010003, 2041700000000100010),
(2041700000000010003, 2041700000000100011);
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`), `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
JOIN `admin_menu` menu ON menu.`name` LIKE 'QqBot%'
WHERE role.`role_code` IN ('super', 'admin')
AND role.`status` = 1
AND role.`is_deleted` = 0
AND menu.`is_deleted` = 0;
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -0,0 +1,64 @@
import type { EntityManager } from 'typeorm';
export type SystemMessageScalar = boolean | null | number | string;
export type SystemMessageFanoutStatus = 'accepted' | 'completed' | 'failed' | 'processing' | 'retry';
export type SystemMessageDeliveryStatus = 'cancelled' | 'failed' | 'pending' | 'processing' | 'retry' | 'success' | 'superseded' | 'waiting_ddns';
export type QqbotMessagePushTargetType = 'group' | 'private';
export type SystemMessageTemplateToken = { kind: 'text'; value: string } | { key: string; kind: 'variable' };
export interface SystemMessageSourceVariableDefinition { description: string; example: string; key: string; label: string; type: 'boolean' | 'number' | 'string'; }
export interface SystemMessageSourceFieldDefinition { dependsOn?: string; key: string; label: string; optionCollection: 'ddnsRecords' | 'portForwards'; required: true; type: 'select'; }
export interface SystemMessageSourceDefinition { description: string; displayName: string; sourceKey: string; subscriptionFields: SystemMessageSourceFieldDefinition[]; variables: SystemMessageSourceVariableDefinition[]; version: 1; }
export interface StunMappingPortChangedSubscriptionConfig { ddnsRecordId: string; portForwardId: string; }
export interface StunMappingPortChangedOptionsResponse { ddnsRecords: Array<{ disabledReasonCode: null | string; eligible: boolean; fqdn: string; id: string; name: string; portForwardId: string; }>; portForwards: Array<{ disabledReasonCode: null | string; eligible: boolean; externalPort: number; id: string; internalPort: number; name: string; protocol: 'tcp' | 'udp'; }>; }
export interface MessageSubscriptionView { createTime: string; enabled: boolean; id: string; invalidReasonCode: null | string; name: string; remark: null | string; sourceConfig: StunMappingPortChangedSubscriptionConfig; sourceKey: string; sourceName: string; sourceSummary: string; updateTime: string; valid: boolean; }
export interface MessageTemplateView { content: string; createTime: string; enabled: boolean; id: string; name: string; referenceCount: number; remark: null | string; sourceKey: string; sourceName: string; updateTime: string; }
export interface MessageTemplatePreview { renderedMessage: string; variables: Record<string, boolean | number | string>; }
export interface QqbotMessagePublishTargetView { enabled: boolean; id: string; targetId: string; targetName: null | string; targetType: QqbotMessagePushTargetType; }
export interface QqbotMessagePublishBindingView { available: boolean; createTime: string; enabled: boolean; id: string; invalidReasonCode: null | string; sourceKey: string; sourceName: string; subscriptionId: string; subscriptionName: string; targets: QqbotMessagePublishTargetView[]; templateId: string; templateName: string; updateTime: string; }
export type SystemMessageDeliveryReadiness =
| { reasonCode: null; status: 'ready'; variables: Record<string, boolean | number | string>; }
| { reasonCode: 'ddns_not_synced'; status: 'waiting_ddns'; variables: Record<string, boolean | number | string>; }
| { reasonCode: string; status: 'cancelled' | 'superseded'; variables?: never; };
export interface SystemMessageSourceAdapter {
readonly definition: SystemMessageSourceDefinition;
inspectSubscription(config: Record<string, unknown>): Promise<{ invalidReasonCode: null | string; sourceSummary: string; valid: boolean; }>;
listSubscriptionOptions(): Promise<Record<string, unknown>>;
normalizeSubscriptionConfig(input: unknown): Promise<{ canonicalConfig: Record<string, string>; resourceKey: string; sourceSummary: string; }>;
resolveDelivery(input: { eventPayload: Record<string, SystemMessageScalar>; subscriptionConfig: Record<string, unknown>; }): Promise<SystemMessageDeliveryReadiness>;
validateEventPayload(payload: Record<string, unknown>): Record<string, SystemMessageScalar>;
}
export interface SystemMessageEventInput { eventId: string; occurredAt: string; payload: Record<string, SystemMessageScalar>; resourceKey: string; sourceKey: string; }
export const SYSTEM_MESSAGE_EVENT_STAGER = Symbol('SYSTEM_MESSAGE_EVENT_STAGER');
export interface SystemMessageEventStager { stage(manager: EntityManager, input: SystemMessageEventInput): Promise<'accepted' | 'duplicate'>; }
export const SYSTEM_MESSAGE_DELIVERY_COORDINATOR = Symbol('SYSTEM_MESSAGE_DELIVERY_COORDINATOR');
export interface SystemMessageDeliveryCoordinator { notifyDdnsSynced(input: { appliedAddress: string; ddnsRecordId: string; }): Promise<void>; requestDrain(): void; }
export interface MessageSubscriptionListQuery { enabled?: boolean; name?: string; pageNo?: number; pageSize?: number; sourceKey?: string; }
export interface MessageSubscriptionInput { enabled: boolean; name: string; remark?: string; sourceConfig: Record<string, unknown>; sourceKey: string; }
export interface MessageTemplateListQuery { enabled?: boolean; name?: string; pageNo?: number; pageSize?: number; sourceKey?: string; }
export interface MessageTemplateInput { content: string; enabled: boolean; name: string; remark?: string; sourceKey: string; }
export interface QqbotMessagePublishTargetInput { targetId: string; targetName?: string; targetType: QqbotMessagePushTargetType; }
export interface QqbotMessagePublishBindingInput { enabled: boolean; subscriptionId: string; targets: QqbotMessagePublishTargetInput[]; templateId: string; }
export interface QqbotMessagePushTargetOption { label: string; targetId: string; targetType: QqbotMessagePushTargetType; }
export interface QqbotMessagePushTargetOptionsResponse { available: boolean; options: QqbotMessagePushTargetOption[]; reasonCode: null | string; }
export interface StrictPlainTextSendInput { attemptNumber: number; deliveryId: string; message: string; selfId: string; targetId: string; targetType: QqbotMessagePushTargetType; }
export interface QqbotSendAttemptErrorOptions { code: string; message: string; retryable: boolean; sendLogId: null | string; }
export class SystemMessageContractError extends Error {
/** Creates a stable, non-sensitive domain contract error. */
constructor(public readonly code: string) { super(code); this.name = 'SystemMessageContractError'; }
}
export const QQBOT_MESSAGE_PUSH_TABLE_CONTRACT = {
binding: ['id', 'subscription_id', 'account_id', 'self_id', 'template_id', 'active_key', 'enabled', 'is_deleted', 'create_time', 'update_time'],
delivery: ['id', 'message_event_id', 'publish_target_id', 'binding_id', 'subscription_id', 'self_id', 'target_type', 'target_id', 'template_id', 'template_content', 'variable_snapshot', 'rendered_message', 'status', 'attempt_count', 'next_attempt_at', 'processing_lease_until', 'send_log_id', 'last_error_code', 'last_error_message', 'expires_at', 'create_time', 'update_time'],
event: ['id', 'event_id', 'source_key', 'resource_key', 'occurred_at', 'payload', 'fanout_status', 'fanout_attempt_count', 'next_fanout_at', 'fanout_lease_until', 'last_error_code', 'last_error_message', 'create_time', 'update_time'],
subscription: ['id', 'name', 'source_key', 'source_config', 'source_config_digest', 'active_key', 'enabled', 'remark', 'is_deleted', 'create_time', 'update_time'],
target: ['id', 'binding_id', 'target_type', 'target_id', 'target_name', 'active_key', 'enabled', 'is_deleted', 'create_time', 'update_time'],
template: ['id', 'name', 'source_key', 'content', 'enabled', 'remark', 'is_deleted', 'create_time', 'update_time'],
} as const;

View File

@ -12,6 +12,12 @@ export const QQBOT_CORE_DOMAIN_CONTRACT = {
'qqbot_send_task',
'qqbot_send_log',
'qqbot_dedupe_event',
'qqbot_message_subscription',
'qqbot_message_template',
'qqbot_message_publish_binding',
'qqbot_message_publish_target',
'qqbot_message_event',
'qqbot_message_delivery',
],
status: {
accountTable: 'qqbot_account',

View File

@ -0,0 +1,37 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtDateTimeColumn, KtUpdateDateColumn } from '@/common';
import type { QqbotMessagePushTargetType, SystemMessageDeliveryStatus, SystemMessageScalar } from '../../../contract/message-push/qqbot-message-push.types';
@Entity('qqbot_message_delivery')
@Index('uk_qqbot_message_delivery_event_target', ['messageEventId', 'publishTargetId'], { unique: true })
@Index('idx_qqbot_message_delivery_dispatch', ['status', 'nextAttemptAt'])
@Index('idx_qqbot_message_delivery_lease', ['processingLeaseUntil'])
@Index('idx_qqbot_message_delivery_history', ['subscriptionId', 'messageEventId'])
export class QqbotMessageDelivery {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ name: 'message_event_id', type: 'bigint' }) messageEventId: string;
@Column({ name: 'publish_target_id', type: 'bigint' }) publishTargetId: string;
@Column({ name: 'binding_id', type: 'bigint' }) bindingId: string;
@Column({ name: 'subscription_id', type: 'bigint' }) subscriptionId: string;
@Column({ length: 64, name: 'self_id' }) selfId: string;
@Column({ length: 16, name: 'target_type' }) targetType: QqbotMessagePushTargetType;
@Column({ length: 64, name: 'target_id' }) targetId: string;
@Column({ name: 'template_id', type: 'bigint' }) templateId: string;
@Column({ name: 'template_content', type: 'text' }) templateContent: string;
@Column({ name: 'variable_snapshot', type: 'json' }) variableSnapshot: Record<string, SystemMessageScalar>;
@Column({ name: 'rendered_message', type: 'text' }) renderedMessage: string;
@Column({ length: 32 }) status: SystemMessageDeliveryStatus;
@Column({ default: 0, name: 'attempt_count', type: 'int', unsigned: true }) attemptCount: number;
@KtDateTimeColumn({ name: 'next_attempt_at', nullable: true, precision: 6 }) nextAttemptAt: KtDateTime | null;
@KtDateTimeColumn({ name: 'processing_lease_until', nullable: true, precision: 6 }) processingLeaseUntil: KtDateTime | null;
@Column({ name: 'send_log_id', nullable: true, type: 'bigint' }) sendLogId: null | string;
@Column({ length: 64, name: 'last_error_code', nullable: true }) lastErrorCode: null | string;
@Column({ length: 500, name: 'last_error_message', nullable: true }) lastErrorMessage: null | string;
@KtDateTimeColumn({ name: 'expires_at', precision: 6 }) expiresAt: KtDateTime;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this delivery task is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -0,0 +1,28 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtDateTimeColumn, KtUpdateDateColumn } from '@/common';
import type { SystemMessageFanoutStatus, SystemMessageScalar } from '../../../contract/message-push/qqbot-message-push.types';
@Entity('qqbot_message_event')
@Index('uk_qqbot_message_event_event_id', ['eventId'], { unique: true })
@Index('idx_qqbot_message_event_dispatch', ['fanoutStatus', 'nextFanoutAt'])
@Index('idx_qqbot_message_event_lease', ['fanoutLeaseUntil'])
export class QqbotMessageEvent {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ length: 128, name: 'event_id' }) eventId: string;
@Column({ length: 128, name: 'source_key' }) sourceKey: string;
@Column({ length: 128, name: 'resource_key' }) resourceKey: string;
@KtDateTimeColumn({ name: 'occurred_at', precision: 6 }) occurredAt: KtDateTime;
@Column({ type: 'json' }) payload: Record<string, SystemMessageScalar>;
@Column({ default: 'accepted', length: 32, name: 'fanout_status' }) fanoutStatus: SystemMessageFanoutStatus;
@Column({ default: 0, name: 'fanout_attempt_count', type: 'int', unsigned: true }) fanoutAttemptCount: number;
@KtDateTimeColumn({ name: 'next_fanout_at', nullable: true, precision: 6 }) nextFanoutAt: KtDateTime | null;
@KtDateTimeColumn({ name: 'fanout_lease_until', nullable: true, precision: 6 }) fanoutLeaseUntil: KtDateTime | null;
@Column({ length: 64, name: 'last_error_code', nullable: true }) lastErrorCode: null | string;
@Column({ length: 500, name: 'last_error_message', nullable: true }) lastErrorMessage: null | string;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this outbox event is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -0,0 +1,21 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
@Entity('qqbot_message_publish_binding')
@Index('uk_qqbot_message_publish_binding_active_key', ['activeKey'], { unique: true })
export class QqbotMessagePublishBinding {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ name: 'subscription_id', type: 'bigint' }) subscriptionId: string;
@Column({ name: 'account_id', type: 'bigint' }) accountId: string;
@Column({ length: 64, name: 'self_id' }) selfId: string;
@Column({ name: 'template_id', type: 'bigint' }) templateId: string;
@Column({ length: 255, name: 'active_key', nullable: true }) activeKey: null | string;
@Column({ default: true }) enabled: boolean;
@Column({ default: false, name: 'is_deleted' }) isDeleted: boolean;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this account binding is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -0,0 +1,22 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
import type { QqbotMessagePushTargetType } from '../../../contract/message-push/qqbot-message-push.types';
@Entity('qqbot_message_publish_target')
@Index('uk_qqbot_message_publish_target_active_key', ['activeKey'], { unique: true })
export class QqbotMessagePublishTarget {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ name: 'binding_id', type: 'bigint' }) bindingId: string;
@Column({ length: 16, name: 'target_type' }) targetType: QqbotMessagePushTargetType;
@Column({ length: 64, name: 'target_id' }) targetId: string;
@Column({ length: 120, name: 'target_name', nullable: true }) targetName: null | string;
@Column({ length: 300, name: 'active_key', nullable: true }) activeKey: null | string;
@Column({ default: true }) enabled: boolean;
@Column({ default: false, name: 'is_deleted' }) isDeleted: boolean;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this publish target is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -0,0 +1,22 @@
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
@Entity('qqbot_message_subscription')
@Index('uk_qqbot_message_subscription_active_key', ['activeKey'], { unique: true })
export class QqbotMessageSubscription {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ length: 100 }) name: string;
@Column({ length: 128, name: 'source_key' }) sourceKey: string;
@Column({ name: 'source_config', type: 'json' }) sourceConfig: Record<string, unknown>;
@Column({ length: 64, name: 'source_config_digest', type: 'char' }) sourceConfigDigest: string;
@Column({ length: 255, name: 'active_key', nullable: true }) activeKey: null | string;
@Column({ default: true }) enabled: boolean;
@Column({ length: 500, nullable: true }) remark: null | string;
@Column({ default: false, name: 'is_deleted' }) isDeleted: boolean;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this subscription is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -0,0 +1,19 @@
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
import { ensureSnowflakeId, KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
@Entity('qqbot_message_template')
export class QqbotMessageTemplate {
@PrimaryColumn({ type: 'bigint' }) id: string;
@Column({ length: 100 }) name: string;
@Column({ length: 128, name: 'source_key' }) sourceKey: string;
@Column({ type: 'text' }) content: string;
@Column({ default: true }) enabled: boolean;
@Column({ length: 500, nullable: true }) remark: null | string;
@Column({ default: false, name: 'is_deleted' }) isDeleted: boolean;
@KtCreateDateColumn({ name: 'create_time', precision: 6 }) createTime: KtDateTime;
@KtUpdateDateColumn({ name: 'update_time', precision: 6 }) updateTime: KtDateTime;
/** Assigns the Snowflake primary key before this template is persisted. */
@BeforeInsert()
createId() { ensureSnowflakeId(this); }
}

View File

@ -41,6 +41,12 @@ import { QqbotRateLimitService } from '@/modules/qqbot/core/application/send/qqb
import { QqbotSendController } from '@/modules/qqbot/core/contract/send/qqbot-send.controller';
import { QqbotSendLog } from '@/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity';
import { QqbotSendService } from '@/modules/qqbot/core/application/send/qqbot-send.service';
import { QqbotMessageDelivery } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-delivery.entity';
import { QqbotMessageEvent } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-event.entity';
import { QqbotMessagePublishBinding } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-publish-binding.entity';
import { QqbotMessagePublishTarget } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-publish-target.entity';
import { QqbotMessageSubscription } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-subscription.entity';
import { QqbotMessageTemplate } from '@/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-template.entity';
export { QQBOT_CORE_DOMAIN_CONTRACT } from './contract/qqbot-core.contract';
@ -55,6 +61,12 @@ export const QQBOT_CORE_ENTITIES = [
QqbotConversation,
QqbotDedupe,
QqbotMessage,
QqbotMessageDelivery,
QqbotMessageEvent,
QqbotMessagePublishBinding,
QqbotMessagePublishTarget,
QqbotMessageSubscription,
QqbotMessageTemplate,
QqbotRule,
QqbotSendLog,
];

View File

@ -222,6 +222,12 @@ describe('QQBot core module contract', () => {
'QqbotConversation',
'QqbotDedupe',
'QqbotMessage',
'QqbotMessageSubscription',
'QqbotMessageTemplate',
'QqbotMessagePublishBinding',
'QqbotMessagePublishTarget',
'QqbotMessageEvent',
'QqbotMessageDelivery',
'QqbotRule',
'QqbotSendLog',
]),

View File

@ -0,0 +1,83 @@
import { getMetadataArgsStorage } from 'typeorm';
import {
QQBOT_CORE_ENTITIES,
} from '../../../../src/modules/qqbot/core/qqbot-core.module';
import { QqbotMessageDelivery } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-delivery.entity';
import { QqbotMessageEvent } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-event.entity';
import { QqbotMessagePublishBinding } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-publish-binding.entity';
import { QqbotMessagePublishTarget } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-publish-target.entity';
import { QqbotMessageSubscription } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-subscription.entity';
import { QqbotMessageTemplate } from '../../../../src/modules/qqbot/core/infrastructure/persistence/message-push/qqbot-message-template.entity';
const messagePushEntities = [
QqbotMessageSubscription,
QqbotMessageTemplate,
QqbotMessagePublishBinding,
QqbotMessagePublishTarget,
QqbotMessageEvent,
QqbotMessageDelivery,
];
const expectedColumns = {
qqbot_message_delivery: [
'id', 'message_event_id', 'publish_target_id', 'binding_id',
'subscription_id', 'self_id', 'target_type', 'target_id', 'template_id',
'template_content', 'variable_snapshot', 'rendered_message', 'status',
'attempt_count', 'next_attempt_at', 'processing_lease_until',
'send_log_id', 'last_error_code', 'last_error_message', 'expires_at',
'create_time', 'update_time',
],
qqbot_message_event: [
'id', 'event_id', 'source_key', 'resource_key', 'occurred_at', 'payload',
'fanout_status', 'fanout_attempt_count', 'next_fanout_at',
'fanout_lease_until', 'last_error_code', 'last_error_message',
'create_time', 'update_time',
],
qqbot_message_publish_binding: [
'id', 'subscription_id', 'account_id', 'self_id', 'template_id',
'active_key', 'enabled', 'is_deleted', 'create_time', 'update_time',
],
qqbot_message_publish_target: [
'id', 'binding_id', 'target_type', 'target_id', 'target_name',
'active_key', 'enabled', 'is_deleted', 'create_time', 'update_time',
],
qqbot_message_subscription: [
'id', 'name', 'source_key', 'source_config', 'source_config_digest',
'active_key', 'enabled', 'remark', 'is_deleted', 'create_time',
'update_time',
],
qqbot_message_template: [
'id', 'name', 'source_key', 'content', 'enabled', 'remark',
'is_deleted', 'create_time', 'update_time',
],
} as const;
/** Reads the table name declared by a TypeORM entity. */
const tableNameOf = (entity: object) =>
getMetadataArgsStorage().tables.find((table) => table.target === entity)
?.name;
/** Reads the database column names declared by a TypeORM entity. */
const columnNamesOf = (entity: object) =>
getMetadataArgsStorage()
.columns.filter((column) => column.target === entity)
.map((column) => `${column.options.name || column.propertyName}`);
describe('QQBot message-push persistence contract', () => {
it('registers the six message-push entities in QQBot Core', () => {
expect(QQBOT_CORE_ENTITIES).toEqual(
expect.arrayContaining(messagePushEntities),
);
});
it('maps every required message-push column with string Snowflake IDs', () => {
for (const entity of messagePushEntities) {
const tableName = tableNameOf(entity);
expect(tableName).toBeTruthy();
expect(columnNamesOf(entity)).toEqual(
expect.arrayContaining([...expectedColumns[tableName as keyof typeof expectedColumns]]),
);
}
});
});

View File

@ -0,0 +1,97 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
const tables = [
'qqbot_message_subscription',
'qqbot_message_template',
'qqbot_message_publish_binding',
'qqbot_message_publish_target',
'qqbot_message_event',
'qqbot_message_delivery',
] as const;
const menuEntries = [
['2041700000000100413', 'QqBotMessageSubscription', 'QqBot:MessageSubscription:List'],
['2041700000000100414', 'QqBotMessageTemplate', 'QqBot:MessageTemplate:List'],
['2041700000000120461', 'QqBotMessageSubscriptionList', 'QqBot:MessageSubscription:List'],
['2041700000000120462', 'QqBotMessageSubscriptionCreate', 'QqBot:MessageSubscription:Create'],
['2041700000000120463', 'QqBotMessageSubscriptionUpdate', 'QqBot:MessageSubscription:Update'],
['2041700000000120464', 'QqBotMessageSubscriptionDelete', 'QqBot:MessageSubscription:Delete'],
['2041700000000120465', 'QqBotMessageSubscriptionToggle', 'QqBot:MessageSubscription:Toggle'],
['2041700000000120471', 'QqBotMessageTemplateList', 'QqBot:MessageTemplate:List'],
['2041700000000120472', 'QqBotMessageTemplateCreate', 'QqBot:MessageTemplate:Create'],
['2041700000000120473', 'QqBotMessageTemplateUpdate', 'QqBot:MessageTemplate:Update'],
['2041700000000120474', 'QqBotMessageTemplateDelete', 'QqBot:MessageTemplate:Delete'],
['2041700000000120475', 'QqBotMessageTemplateToggle', 'QqBot:MessageTemplate:Toggle'],
['2041700000000120476', 'QqBotMessageTemplatePreview', 'QqBot:MessageTemplate:Preview'],
['2041700000000120481', 'QqBotAccountMessagePushList', 'QqBot:Account:MessagePush:List'],
['2041700000000120482', 'QqBotAccountMessagePushCreate', 'QqBot:Account:MessagePush:Create'],
['2041700000000120483', 'QqBotAccountMessagePushUpdate', 'QqBot:Account:MessagePush:Update'],
['2041700000000120484', 'QqBotAccountMessagePushDelete', 'QqBot:Account:MessagePush:Delete'],
['2041700000000120485', 'QqBotAccountMessagePushToggle', 'QqBot:Account:MessagePush:Toggle'],
] as const;
/** Reads SQL with case and quote differences removed for contract assertions. */
const readNormalizedSql = (relativePath: string) =>
readFileSync(resolve(process.cwd(), relativePath), 'utf8')
.toLowerCase()
.replace(/`/g, '')
.replace(/\s+/g, ' ');
describe('QQBot message-push SQL contract', () => {
const bootstrapSql = readNormalizedSql('sql/qqbot-init.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');
const vbenSql = readNormalizedSql('sql/vben-admin-init.sql');
it.each(tables)('declares %s in current and bootstrap schema SQL', (table) => {
expect(bootstrapSql).toContain(`create table if not exists ${table}`);
expect(schemaSql).toContain(`create table if not exists ${table}`);
});
it('keeps six-table indexes, datetime precision, and JSON fields aligned', () => {
for (const sql of [bootstrapSql, schemaSql]) {
expect(sql).toContain('uk_qqbot_message_subscription_active_key');
expect(sql).toContain('uk_qqbot_message_publish_binding_active_key');
expect(sql).toContain('uk_qqbot_message_publish_target_active_key');
expect(sql).toContain('uk_qqbot_message_event_event_id');
expect(sql).toContain('uk_qqbot_message_delivery_event_target');
expect(sql).toContain('idx_qqbot_message_event_dispatch');
expect(sql).toContain('idx_qqbot_message_event_lease');
expect(sql).toContain('idx_qqbot_message_delivery_dispatch');
expect(sql).toContain('idx_qqbot_message_delivery_lease');
expect(sql).toContain('idx_qqbot_message_delivery_history');
expect(sql).toContain('source_config json');
expect(sql).toContain('payload json');
expect(sql).toContain('variable_snapshot json');
expect(sql).toContain('datetime(6)');
}
});
it('seeds the default template and every stable menu node idempotently', () => {
for (const sql of [bootstrapSql, seedSql]) {
expect(sql).toContain('2041700000000200601');
expect(sql).toContain('network.stun.mapping-port-changed');
expect(sql).toContain('当前stun的端口已变更为${{endpoint}}');
expect(sql).toContain('where not exists');
}
for (const [id, name, authCode] of menuEntries) {
for (const sql of [bootstrapSql, seedSql, vbenSql]) {
expect(sql).toContain(id);
expect(sql).toContain(name.toLowerCase());
expect(sql).toContain(authCode.toLowerCase());
}
}
});
it('verifies every persisted message-push table and seeded menu contract', () => {
for (const table of tables) {
expect(verifySql).toContain(table);
}
expect(verifySql).toContain('2041700000000200601');
expect(verifySql).toContain('qqbot:messagesubscription:list');
expect(verifySql).toContain('qqbot:account:messagepush:toggle');
});
});