-- ============================================================================= -- AMI 连接与语音链路增量(一公寓一 Asterisk 实例) -- 依据:公寓端/19 设计文档;公寓端/05 表 5-12(SIP 叫乘链路)、表 5-13(AMI 事件映射); -- docs/10 表 8-3(AMI 不出本机)、表 8-4(端口 7058)、D5(一进程一 gyid) -- 口径: -- 1) 新增 1 张映射表 apt_ast_instance:gyid 唯一 -> 一公寓一实例(UK 兜底)。 -- 这是对「59 张表」基线的显式偏离,已在公寓端/19 §7 登记(新增第 60 张)。 -- 2) AMI 口令**不落库**:只存环境变量引用 ami_secret_ref(红钱:口令不进仓库)。 -- 3) 复用既有 apt_ast_ami_session / apt_call_session / apt_call_event / -- apt_tts_utterance / apt_recording,仅补列,不新建语音表。 -- 4) 幂等:建表 IF NOT EXISTS,补列走 apt_add_column_if_missing。 -- ============================================================================= SET NAMES utf8mb4; -- ----------------------------------------------------------------------------- -- 1. 公寓 ↔ Asterisk 实例映射(1:1) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS apt_ast_instance ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '代理主键', gyid VARCHAR(32) NOT NULL COMMENT '公寓隔离标识(D5:一进程一 gyid)', instance_code VARCHAR(64) NOT NULL COMMENT '实例编码,如 AST-REV01-001', display_name VARCHAR(64) NULL COMMENT '展示名,如 1 号楼机房 Asterisk', ami_host VARCHAR(128) NOT NULL COMMENT 'AMI 地址;生产固定 127.0.0.1(AMI 不出本机)', ami_port INT NOT NULL DEFAULT 5038 COMMENT 'AMI 端口;现网配置值 7058', ami_user VARCHAR(64) NOT NULL COMMENT 'AMI 账号', ami_secret_ref VARCHAR(128) NOT NULL COMMENT '口令引用(环境变量名),禁止存明文', tls_enabled TINYINT NOT NULL DEFAULT 0 COMMENT '0 明文 / 1 TLS(TLS 端口另配)', conference VARCHAR(64) NOT NULL DEFAULT 'Conf-01' COMMENT '会议桥标识(D11 未定前不改)', dial_context VARCHAR(64) NOT NULL DEFAULT 'from-internal' COMMENT '拨号计划上下文', dial_prefix VARCHAR(16) NULL COMMENT '外呼前缀(如 9),本地分机为空', channel_tech VARCHAR(16) NOT NULL DEFAULT 'PJSIP' COMMENT '话机通道技术:PJSIP / SIP', recording_base_url VARCHAR(256) NULL COMMENT '录音 HTTP 基址(只对内网)', connect_timeout_ms INT NOT NULL DEFAULT 3000 COMMENT 'TCP 建连超时', action_timeout_ms INT NOT NULL DEFAULT 5000 COMMENT 'AMI Action 等待 Response 超时', ping_interval_sec INT NOT NULL DEFAULT 30 COMMENT '心跳间隔(Action: Ping)', ping_timeout_sec INT NOT NULL DEFAULT 5 COMMENT '单次心跳超时', reconnect_initial_ms INT NOT NULL DEFAULT 1000 COMMENT '重连初始退避', reconnect_max_ms INT NOT NULL DEFAULT 60000 COMMENT '重连最大退避(封顶)', state VARCHAR(20) NOT NULL DEFAULT 'DOWN' COMMENT 'DOWN/CONNECTING/UP/DEGRADED/DISABLED', last_connected_at DATETIME(3) NULL COMMENT '最近登录成功时刻', last_ping_at DATETIME(3) NULL COMMENT '最近心跳成功时刻', last_error VARCHAR(256) NULL COMMENT '最近一次失败原因(脱敏,不含口令)', enabled TINYINT NOT NULL DEFAULT 1 COMMENT '0 停用(不建连、不叫乘)', biz_version BIGINT NOT NULL DEFAULT 1 COMMENT '数据库条件更新乐观锁', 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_astinstance_gyid (gyid), KEY idx_astinstance_gyid_state (gyid, state, enabled), CONSTRAINT ck_astinstance_state CHECK (state IN ('DOWN','CONNECTING','UP','DEGRADED','DISABLED')), CONSTRAINT ck_astinstance_enabled CHECK (enabled IN (0,1)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='公寓-Asterisk 实例映射与 AMI 连接配置(一公寓一实例)'; -- 默认实例:只写占位地址与口令引用,口令由部署环境 APT_AMI_SECRET 注入 INSERT INTO apt_ast_instance (gyid, instance_code, display_name, ami_host, ami_port, ami_user, ami_secret_ref, conference, dial_context, state, enabled, create_time, update_time) SELECT 'APT-REV01-001', 'AST-REV01-001', '本机 Asterisk(AMI 仅本机)', '127.0.0.1', 5038, 'apt', 'APT_AMI_SECRET', 'Conf-01', 'from-internal', 'DOWN', 1, NOW(3), NOW(3) WHERE NOT EXISTS ( SELECT 1 FROM apt_ast_instance WHERE gyid = 'APT-REV01-001' AND deleted = 0 ); -- ----------------------------------------------------------------------------- -- 2. AMI 会话补列:心跳与断连原因(表已有 connected_at/disconnected_at/reconnect_count/state) -- ----------------------------------------------------------------------------- CALL apt_add_column_if_missing('apt_ast_ami_session', 'last_ping_at', "last_ping_at DATETIME(3) NULL COMMENT '最近心跳成功时刻'"); CALL apt_add_column_if_missing('apt_ast_ami_session', 'last_error', "last_error VARCHAR(256) NULL COMMENT '本次连接断开的失败原因(脱敏)'"); CALL apt_add_column_if_missing('apt_ast_ami_session', 'close_reason', "close_reason VARCHAR(64) NULL COMMENT 'CLIENT_CLOSE / IO_ERROR / PING_TIMEOUT / AUTH_FAILED / SHUTDOWN'"); -- ----------------------------------------------------------------------------- -- 3. 通话会话补列:双腿(房间/值班台)与通道映射 -- ----------------------------------------------------------------------------- CALL apt_add_column_if_missing('apt_call_session', 'leg_role', "leg_role VARCHAR(16) NOT NULL DEFAULT 'ROOM' COMMENT '腿角色:ROOM 房间 / DUTY 值班台 / TTS 播报'"); CALL apt_add_column_if_missing('apt_call_session', 'channel', "channel VARCHAR(128) NULL COMMENT 'AMI Channel(PJSIP/1001-00000001),事件反查用'"); CALL apt_add_column_if_missing('apt_call_session', 'originate_action_id', "originate_action_id VARCHAR(64) NULL COMMENT 'Originate ActionID,OriginateResponse 关联用'"); CALL apt_add_column_if_missing('apt_call_session', 'ami_session_id', "ami_session_id VARCHAR(64) NULL COMMENT '所属 AMI 连接会话(apt_ast_ami_session.ami_session_id)'"); -- ----------------------------------------------------------------------------- -- 4. TTS 播报补列:与会话/序号绑定,避免并发时互相覆盖(docs/10 表 10-13 TTS 槽位风险) -- ----------------------------------------------------------------------------- CALL apt_add_column_if_missing('apt_tts_utterance', 'session_id', "session_id VARCHAR(64) NULL COMMENT '关联会话;一次入会一条播报链'"); CALL apt_add_column_if_missing('apt_tts_utterance', 'utterance_seq', "utterance_seq BIGINT NOT NULL DEFAULT 1 COMMENT '同会话单调递增,禁止共用槽位'"); CALL apt_add_column_if_missing('apt_tts_utterance', 'played_at', "played_at DATETIME(3) NULL COMMENT '播放完成时刻'"); CALL apt_add_column_if_missing('apt_tts_utterance', 'error_msg', "error_msg VARCHAR(256) NULL COMMENT '失败原因(不改叫乘结论,只告警)'"); -- 会话按通道反查(事件热点路径) SET @apt_idx_channel = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_call_session' AND INDEX_NAME = 'idx_callsession_gyid_channel'); SET @apt_sql_channel = IF(@apt_idx_channel = 0, 'ALTER TABLE apt_call_session ADD KEY idx_callsession_gyid_channel (gyid, channel)', 'SELECT 1'); PREPARE apt_stmt_channel FROM @apt_sql_channel; EXECUTE apt_stmt_channel; DEALLOCATE PREPARE apt_stmt_channel; SET @apt_idx_action = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_call_session' AND INDEX_NAME = 'idx_callsession_gyid_action'); SET @apt_sql_action = IF(@apt_idx_action = 0, 'ALTER TABLE apt_call_session ADD KEY idx_callsession_gyid_action (gyid, originate_action_id)', 'SELECT 1'); PREPARE apt_stmt_action FROM @apt_sql_action; EXECUTE apt_stmt_action; DEALLOCATE PREPARE apt_stmt_action; -- ----------------------------------------------------------------------------- -- 5. 校验 -- ----------------------------------------------------------------------------- SELECT '=== 27. 公寓-实例映射(应 1 行,一公寓一实例)===' AS section; SELECT gyid, instance_code, ami_host, ami_port, ami_user, ami_secret_ref, conference, dial_context, state, enabled FROM apt_ast_instance WHERE deleted = 0 ORDER BY gyid; SELECT '=== 28. 口令不落明文(应 0:secret_ref 不得是疑似明文口令)===' AS section; SELECT COUNT(*) AS plaintext_secret_rows FROM apt_ast_instance WHERE deleted = 0 AND (ami_secret_ref LIKE '% %' OR ami_secret_ref LIKE '%.%' OR CHAR_LENGTH(ami_secret_ref) > 64); SELECT '=== 29. AMI 会话补列(应 3)===' AS section; SELECT COUNT(*) AS ami_session_columns FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'apt_ast_ami_session' AND COLUMN_NAME IN ('last_ping_at','last_error','close_reason'); SELECT '=== 30. 会话/TTS 补列(应 8:call_session 4 + tts 4)===' AS section; SELECT COUNT(*) AS call_tts_columns FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND ((TABLE_NAME = 'apt_call_session' AND COLUMN_NAME IN ('leg_role','channel','originate_action_id','ami_session_id')) OR (TABLE_NAME = 'apt_tts_utterance' AND COLUMN_NAME IN ('session_id','utterance_seq','played_at','error_msg'))); SELECT '=== 31. 会话索引(应 2)===' 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_call_session' AND INDEX_NAME IN ('idx_callsession_gyid_channel','idx_callsession_gyid_action') GROUP BY INDEX_NAME;