-- ============================================================================= -- SIP 分机 / AOR / Contact 与房间的统一管理(承接 03_domain_03_voice.sql) -- 口径: -- 1) apt_sip_endpoint = 分机(PJSIP endpoint),一房一分机,extension 不可改号; -- 2) apt_sip_aor = 分机对应的 AOR(1:1,max_contacts=1 与文件一致); -- 3) apt_sip_contact = AOR 下的 contact(话机 IP 绑定事实,含历史); -- 4) apt_sip_binding = 房间 ↔ 分机的生效区间(历史可追溯,ACTIVE 唯一); -- 5) 同一 gyid 下「一个 IP 只能有一个 ACTIVE contact」= 同一 IP 不得重复绑定; -- 用生成列 + 唯一索引在库层兜底,服务层再做一次可读的冲突前置校验。 -- 幂等:建表 IF NOT EXISTS,补列/补索引走 information_schema 判断 + 动态 SQL。 -- ============================================================================= SET NAMES utf8mb4; -- ----------------------------------------------------------------------------- -- 1. endpoint 补列:话机 IP(与 contact 一致)与来源 -- ----------------------------------------------------------------------------- CALL apt_add_column_if_missing('apt_sip_endpoint', 'ip_address', "ip_address VARCHAR(64) NULL COMMENT '话机 IP(与 ACTIVE contact 的 ip_address 一致,解绑后清空)'"); CALL apt_add_column_if_missing('apt_sip_endpoint', 'source', "source VARCHAR(20) NOT NULL DEFAULT 'ROOM' COMMENT 'ROOM=随房间自动创建 / MANUAL=手工登记'"); -- ----------------------------------------------------------------------------- -- 2. AOR 表(与 endpoint 1:1) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS apt_sip_aor ( id BIGINT NOT NULL AUTO_INCREMENT, gyid VARCHAR(32) NOT NULL, aor_id VARCHAR(64) NOT NULL COMMENT 'AOR 标识,如 AOR-', endpoint_id VARCHAR(64) NOT NULL COMMENT '所属分机(1:1)', max_contacts INT NOT NULL DEFAULT 1 COMMENT 'Asterisk max_contacts', remove_existing TINYINT NOT NULL DEFAULT 1 COMMENT 'Asterisk remove_existing:新注册顶掉旧 contact', default_expiration INT NOT NULL DEFAULT 3600 COMMENT 'contact 默认过期秒数', qualify_frequency INT NOT NULL DEFAULT 30 COMMENT 'OPTIONS 探测间隔秒', sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '仅 PENDING/SYNCED/FAILED/DRIFT', config_version BIGINT NOT NULL DEFAULT 1, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_aor_gyid_id (gyid, aor_id), UNIQUE KEY uk_aor_gyid_endpoint (gyid, endpoint_id), KEY idx_aor_gyid_sync (gyid, sync_state), CONSTRAINT ck_aor_sync CHECK (sync_state IN ('PENDING','SYNCED','FAILED','DRIFT')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SIP AOR(与 endpoint 1:1,先库后 Asterisk)'; -- ----------------------------------------------------------------------------- -- 3. Contact 表(话机 IP 绑定事实) -- active_ip 生成列:仅 ACTIVE 且未删除时取值,配合唯一索引实现 -- 「同 gyid 下一个 IP 只能有一个 ACTIVE contact」。 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS apt_sip_contact ( id BIGINT NOT NULL AUTO_INCREMENT, gyid VARCHAR(32) NOT NULL, contact_id VARCHAR(64) NOT NULL, aor_id VARCHAR(64) NOT NULL, endpoint_id VARCHAR(64) NOT NULL, uri VARCHAR(255) NOT NULL COMMENT 'sip:@:', ip_address VARCHAR(64) NOT NULL, port INT NOT NULL DEFAULT 5060, transport VARCHAR(8) NOT NULL DEFAULT 'udp', user_agent VARCHAR(128) NULL, source VARCHAR(20) NOT NULL DEFAULT 'STATIC' COMMENT 'STATIC=手工绑定 / DYNAMIC=注册上报', state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/RELEASED/EXPIRED', last_seen_at DATETIME(3) NULL, released_at DATETIME(3) NULL, release_remark VARCHAR(128) NULL, active_ip VARCHAR(64) GENERATED ALWAYS AS (CASE WHEN state = 'ACTIVE' AND deleted = 0 THEN ip_address END) STORED, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_contact_gyid_id (gyid, contact_id), UNIQUE KEY uk_contact_gyid_active_ip (gyid, active_ip), KEY idx_contact_gyid_endpoint (gyid, endpoint_id, state), KEY idx_contact_gyid_aor (gyid, aor_id, state), CONSTRAINT ck_contact_state CHECK (state IN ('ACTIVE','RELEASED','EXPIRED')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SIP contact(话机 IP 绑定事实,保留历史;同 IP 唯一 ACTIVE)'; -- ----------------------------------------------------------------------------- -- 4. binding 兜底:一个 endpoint 同时只能有一条 ACTIVE 绑定 -- ----------------------------------------------------------------------------- SET @apt_binding_col = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_binding' AND COLUMN_NAME = 'active_endpoint'); SET @apt_sql_binding_col = IF(@apt_binding_col = 0, 'ALTER TABLE apt_sip_binding ADD COLUMN active_endpoint VARCHAR(64) GENERATED ALWAYS AS (CASE WHEN state = ''ACTIVE'' AND deleted = 0 THEN endpoint_id END) STORED', 'SELECT 1'); PREPARE stmt_binding_col FROM @apt_sql_binding_col; EXECUTE stmt_binding_col; DEALLOCATE PREPARE stmt_binding_col; SET @apt_binding_idx = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_binding' AND INDEX_NAME = 'uk_binding_gyid_active_ep'); SET @apt_sql_binding_idx = IF(@apt_binding_idx = 0, 'ALTER TABLE apt_sip_binding ADD UNIQUE KEY uk_binding_gyid_active_ep (gyid, active_endpoint)', 'SELECT 1'); PREPARE stmt_binding_idx FROM @apt_sql_binding_idx; EXECUTE stmt_binding_idx; DEALLOCATE PREPARE stmt_binding_idx; -- ----------------------------------------------------------------------------- -- 5. 校验 -- ----------------------------------------------------------------------------- SELECT '=== 32. SIP 表就绪(应 4:endpoint/binding/aor/contact)===' AS section; SELECT COUNT(*) AS sip_tables FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('apt_sip_endpoint','apt_sip_binding','apt_sip_aor','apt_sip_contact'); SELECT '=== 33. endpoint 补列(应 2)===' AS section; SELECT COUNT(*) AS endpoint_columns FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_endpoint' AND COLUMN_NAME IN ('ip_address','source'); SELECT '=== 34. 同 IP 唯一 ACTIVE 的库层兜底(应 1)===' AS section; SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_contact' AND INDEX_NAME = 'uk_contact_gyid_active_ip' GROUP BY INDEX_NAME; SELECT '=== 35. 一 endpoint 仅一条 ACTIVE 绑定(应 1)===' AS section; SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_sip_binding' AND INDEX_NAME = 'uk_binding_gyid_active_ep' GROUP BY INDEX_NAME;